Compare commits

...

1 Commits

Author SHA1 Message Date
diegosouzapw
6e4d54a9cc feat(providers): accept JSON cookie objects in normalizeSessionCookieHeader (#9284) 2026-08-04 03:18:41 -03:00
4 changed files with 175 additions and 6 deletions

1
_tasks Symbolic link
View File

@@ -0,0 +1 @@
/home/diegosouzapw/dev/proxys/OmniRoute/_tasks

View File

@@ -0,0 +1 @@
- **feat(providers):** accept JSON cookie objects in normalizeSessionCookieHeader (#9284 — thanks @AIB1TAL0S)

View File

@@ -6,15 +6,74 @@ export function stripCookieInputPrefix(rawValue: string): string {
return withoutBearer.replace(/^cookie:/i, "").trim();
}
export function normalizeSessionCookieHeader(rawValue: string, defaultCookieName: string): string {
const normalized = stripCookieInputPrefix(rawValue);
if (!normalized) return "";
/**
* Parse a JSON array of cookie objects and produce a Cookie header string.
*
* Accepts the format exported by browser cookie-editor extensions / DevTools:
* ```json
* [
* {"name":"sso","value":"eyJ0eXAi...","domain":".example.com","path":"/"},
* {"name":"sso-rw","value":"eyJOTHER..."}
* ]
* ```
*
* Only `name` and `value` are required. Extra fields (domain, path, expires,
* httpOnly, secure, sameSite) are silently ignored — they describe the cookie
* but are not part of the `Cookie` request header.
*
* @param rawValue - The user-provided cookie string (possibly JSON).
* @returns A Cookie header string, null if the input is not JSON (pass-through).
* @throws {Error} If a JSON entry is missing the required `name` or `value` field.
*/
export function parseJsonCookiesToHeader(rawValue: string): string | null {
const trimmed = (rawValue || "").trim();
if (!trimmed || !trimmed.startsWith("[")) return null;
if (normalized.includes("=")) {
return normalized;
let parsed: unknown;
try {
parsed = JSON.parse(trimmed);
} catch {
return null;
}
return `${defaultCookieName}=${normalized}`;
if (!Array.isArray(parsed)) return null;
if (parsed.length === 0) return "";
const parts: string[] = [];
for (let i = 0; i < parsed.length; i++) {
const entry = parsed[i];
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
throw new Error(`Invalid cookie JSON at index ${i}: expected an object`);
}
const record = entry as Record<string, unknown>;
if (typeof record.name !== "string" || !record.name) {
throw new Error(`Invalid cookie JSON at index ${i}: missing required field 'name'`);
}
if (typeof record.value !== "string") {
throw new Error(`Invalid cookie JSON at index ${i}: missing required field 'value'`);
}
parts.push(`${record.name}=${record.value}`);
}
return parts.join("; ");
}
export function normalizeSessionCookieHeader(rawValue: string, defaultCookieName: string): string {
const stripped = stripCookieInputPrefix(rawValue);
if (!stripped) return "";
const jsonResult = parseJsonCookiesToHeader(stripped);
if (jsonResult !== null) {
return jsonResult;
}
if (stripped.includes("=")) {
return stripped;
}
return `${defaultCookieName}=${stripped}`;
}
/**

View File

@@ -0,0 +1,108 @@
import test from "node:test";
import assert from "node:assert/strict";
const {
parseJsonCookiesToHeader,
normalizeSessionCookieHeader,
} = await import("../../src/lib/providers/webCookieAuth.ts");
// parseJsonCookiesToHeader — unit tests
test("parseJsonCookiesToHeader: valid JSON array returns Cookie header string", () => {
const json = `[{"name":"sso","value":"eyJ0eXAi.abc.def"}]`;
assert.equal(parseJsonCookiesToHeader(json), "sso=eyJ0eXAi.abc.def");
});
test("parseJsonCookiesToHeader: multiple entries joined with ; ", () => {
const json = `[
{"name":"sso","value":"AAA.bbb"},
{"name":"sso-rw","value":"CCC.ddd"},
{"name":"cf_clearance","value":"zzz"}
]`;
assert.equal(parseJsonCookiesToHeader(json), "sso=AAA.bbb; sso-rw=CCC.ddd; cf_clearance=zzz");
});
test("parseJsonCookiesToHeader: extra optional fields are ignored gracefully", () => {
const json = `[{"name":"session","value":"abc","domain":".example.com","path":"/","httpOnly":true,"secure":true,"sameSite":"Lax"}]`;
assert.equal(parseJsonCookiesToHeader(json), "session=abc");
});
test("parseJsonCookiesToHeader: missing name throws descriptive error at correct index", () => {
const json = `[{"name":"a","value":"1"},{"value":"no-name"}]`;
assert.throws(
() => parseJsonCookiesToHeader(json),
{ message: "Invalid cookie JSON at index 1: missing required field 'name'" }
);
});
test("parseJsonCookiesToHeader: missing value throws descriptive error at correct index", () => {
const json = `[{"name":"a","value":"1"},{"name":"no-value"}]`;
assert.throws(
() => parseJsonCookiesToHeader(json),
{ message: "Invalid cookie JSON at index 1: missing required field 'value'" }
);
});
test("parseJsonCookiesToHeader: empty array returns empty string", () => {
assert.equal(parseJsonCookiesToHeader("[]"), "");
});
test("parseJsonCookiesToHeader: raw string (non-JSON) returns null (pass-through)", () => {
assert.equal(parseJsonCookiesToHeader("sso=eyJ0eXAi.abc.def"), null);
assert.equal(parseJsonCookiesToHeader("__Secure-authjs.session-token=abc"), null);
assert.equal(parseJsonCookiesToHeader("bearer xyz"), null);
});
test("parseJsonCookiesToHeader: malformed JSON returns null (pass-through, no crash)", () => {
assert.equal(parseJsonCookiesToHeader("[not valid json"), null);
assert.equal(parseJsonCookiesToHeader("{invalid}"), null);
});
test("parseJsonCookiesToHeader: empty/whitespace input returns null", () => {
assert.equal(parseJsonCookiesToHeader(""), null);
assert.equal(parseJsonCookiesToHeader(" "), null);
});
test("parseJsonCookiesToHeader: parsed non-array JSON returns null", () => {
assert.equal(parseJsonCookiesToHeader(`{"name":"test"}`), null);
});
test("parseJsonCookiesToHeader: entry with empty name throws error", () => {
const json = `[{"name":"","value":"abc"}]`;
assert.throws(
() => parseJsonCookiesToHeader(json),
{ message: "Invalid cookie JSON at index 0: missing required field 'name'" }
);
});
test("parseJsonCookiesToHeader: entry with empty value returns empty value in header", () => {
const json = `[{"name":"session","value":""}]`;
assert.equal(parseJsonCookiesToHeader(json), "session=");
});
// Integration tests via normalizeSessionCookieHeader
test("normalizeSessionCookieHeader: JSON input returns correct header", () => {
const json = `[{"name":"__Secure-authjs.session-token","value":"abc"}]`;
assert.equal(
normalizeSessionCookieHeader(json, "__Secure-authjs.session-token"),
"__Secure-authjs.session-token=abc"
);
});
test("normalizeSessionCookieHeader: JSON input with prefix stripped works", () => {
const json = `[{"name":"sso","value":"eyJ0eXAi.abc"}]`;
assert.equal(
normalizeSessionCookieHeader(`Cookie: ${json}`, "sso"),
"sso=eyJ0eXAi.abc"
);
});
test("normalizeSessionCookieHeader: raw string unchanged after JSON support added", () => {
assert.equal(
normalizeSessionCookieHeader("__Secure-authjs.session-token=abc", "__Secure-authjs.session-token"),
"__Secure-authjs.session-token=abc"
);
assert.equal(
normalizeSessionCookieHeader("bare-value", "__Secure-authjs.session-token"),
"__Secure-authjs.session-token=bare-value"
);
});