Organize src/ into concern folders (http, auth, admin, plugin-host, ui); co-locate tests, move plugin-api barrel into plugin-host, sync docs + AGENTS layout

This commit is contained in:
2026-06-24 00:23:55 +02:00
parent 6d316c4888
commit de22f51c12
113 changed files with 282 additions and 265 deletions
+91
View File
@@ -0,0 +1,91 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { parseCookies, serializeCookie } from "./cookie.ts";
// parseCookies returns a null-prototype map; spread into a plain object to deep-equal.
const flat = (header: string | undefined): Record<string, string> => ({ ...parseCookies(header) });
test("parseCookies returns an empty object for an absent or empty header", () => {
assert.deepEqual(flat(undefined), {});
assert.deepEqual(flat(""), {});
});
test("parseCookies splits pairs, trims, keeps `=` in values, and skips nameless/`=`-less pairs", () => {
assert.deepEqual(flat("a=1; b=2"), { a: "1", b: "2" });
assert.deepEqual(flat(" a = 1 ;b= 2"), { a: "1", b: "2" });
assert.deepEqual(flat("t=ey.Jh=="), { t: "ey.Jh==" }); // `=` inside a base64/JWT value is kept
assert.deepEqual(flat("novalue; =orphan; a=1"), { a: "1" });
});
test("parseCookies decodes percent-encoded values, raw on malformed", () => {
assert.equal(parseCookies("a=one%20two").a, "one two");
assert.equal(parseCookies("a=%E0%A4%A").a, "%E0%A4%A"); // invalid escape → untouched, no throw
});
test("parseCookies strips one layer of surrounding double-quotes", () => {
assert.equal(parseCookies('a="quoted"').a, "quoted");
});
test("parseCookies keeps the first occurrence of a duplicate name", () => {
assert.equal(parseCookies("a=first; a=second").a, "first");
});
test("parseCookies is not vulnerable to prototype pollution", () => {
const parsed = parseCookies("__proto__=polluted; a=1");
assert.equal(Object.getPrototypeOf(parsed), null); // null-prototype map
assert.equal(parsed["__proto__"], "polluted"); // stored as a plain own key, not the prototype
assert.equal(parsed.a, "1");
assert.equal(Object.getPrototypeOf({}), Object.prototype); // global prototype untouched
});
test("serializeCookie emits name=value, encoding specials but leaving JWT chars (-_.) readable", () => {
assert.equal(serializeCookie("session", "abc"), "session=abc");
assert.equal(serializeCookie("session", "a b&c"), "session=a%20b%26c");
assert.equal(serializeCookie("session", "ab-_.cd"), "session=ab-_.cd");
});
test("serializeCookie appends the secure-by-default attribute flags", () => {
const out = serializeCookie("session", "x", { httpOnly: true, path: "/", sameSite: "Lax", secure: true });
assert.equal(out, "session=x; Path=/; HttpOnly; SameSite=Lax; Secure");
});
test("serializeCookie writes Max-Age (incl. non-positive, expire-now) and rejects a non-integer", () => {
assert.match(serializeCookie("a", "1", { maxAge: 600 }), /; Max-Age=600(;|$)/);
assert.match(serializeCookie("a", "1", { maxAge: 0 }), /; Max-Age=0(;|$)/);
assert.match(serializeCookie("a", "1", { maxAge: -1 }), /; Max-Age=-1(;|$)/);
assert.throws(() => serializeCookie("a", "1", { maxAge: 1.5 }), /integer/);
});
test("serializeCookie writes Expires from a Date and rejects invalid or out-of-range ones", () => {
assert.match(serializeCookie("a", "1", { expires: new Date(0) }), /; Expires=Thu, 01 Jan 1970 00:00:00 GMT/);
assert.throws(() => serializeCookie("a", "1", { expires: new Date("nope") }), /Expires/);
// toUTCString() of a year > 9999 yields a 6-digit year browsers may reject — fail loud instead.
assert.throws(() => serializeCookie("a", "1", { expires: new Date(8640000000000000) }), /Expires/);
});
test("serializeCookie writes Domain/Path and rejects empty or injecting values", () => {
const out = serializeCookie("a", "1", { domain: "example.com", path: "/admin" });
assert.match(out, /; Domain=example\.com/);
assert.match(out, /; Path=\/admin/);
assert.throws(() => serializeCookie("a", "1", { domain: "" }), /domain/); // misconfigured deploy
assert.throws(() => serializeCookie("a", "1", { path: "" }), /path/);
assert.throws(() => serializeCookie("a", "1", { path: "/x; Domain=evil.com" }), /path/); // attribute injection
assert.throws(() => serializeCookie("a", "1", { domain: "evil\r\nSet-Cookie: x=y" }), /domain/); // header split
});
test("serializeCookie rejects SameSite=None without Secure (browsers would drop it)", () => {
assert.throws(() => serializeCookie("a", "1", { sameSite: "None" }), /Secure/);
assert.doesNotThrow(() => serializeCookie("a", "1", { sameSite: "None", secure: true }));
});
test("serializeCookie rejects an invalid cookie name", () => {
assert.throws(() => serializeCookie("bad name", "1"), /name/);
assert.throws(() => serializeCookie("a;b", "1"), /name/);
});
test("serializeCookie and parseCookies round-trip an arbitrary value", () => {
const value = "header.payload.sig with spaces & symbols=";
const setCookie = serializeCookie("session", value, { httpOnly: true });
const cookieHeader = setCookie.split("; ")[0]; // browsers send only name=value
assert.equal(parseCookies(cookieHeader).session, value);
});