diff --git a/nix/bridge.nix b/nix/bridge.nix index b0dbfd3..ff50036 100644 --- a/nix/bridge.nix +++ b/nix/bridge.nix @@ -21,7 +21,7 @@ assert lib.assertMsg ((serverHost == null) == (serverPublicKey == null)) "gsmol-bridge: serverHost and serverPublicKey must be set together, or not at all"; stdenvNoCC.mkDerivation { pname = "gsmol-bridge"; - version = "0.2.1"; + version = "0.3.0"; # Respects the repo's own .gitignore, so .venv/.devbox/.jj never enter the # store path even when building from an unclean local checkout. diff --git a/test/vectors.test.mjs b/test/vectors.test.mjs index 13130bd..b578ef0 100644 --- a/test/vectors.test.mjs +++ b/test/vectors.test.mjs @@ -155,6 +155,42 @@ for (const v of vectors.ed_to_x25519) { eq("fm-too-many-keys", proto.parseFrontmatter("---\n" + "X: y\n".repeat(65) + "---\nbody").fields.subject ?? "none", "none"); } +// --- compose headers ---------------------------------------------------------- + +{ + const refused = (text) => { + try { proto.headerRow(text); return null; } catch (err) { return err instanceof proto.FieldError ? err.id : err; } + }; + eq("hdr-pair", proto.headerRow("Urgency: high").join("|"), "Urgency|high"); + eq("hdr-trimmed", proto.headerRow(" spaced-key : trimmed value ").join("|"), "spaced-key|trimmed value"); + eq("hdr-empty", proto.headerRow(" "), null); + eq("hdr-no-colon", refused("no colon"), "compose-header-colon-missing"); + eq("hdr-empty-key", refused(": value"), "compose-header-invalid-key"); + eq("hdr-bad-key", refused("bad key: v"), "compose-header-invalid-key"); + eq("hdr-long-key", refused(`${"k".repeat(65)}: v`), "compose-header-invalid-key"); + eq("hdr-inner-newline", refused("multi\nline: v"), "compose-header-invalid-key"); + eq("hdr-multiline-value", refused("k: a\nb"), "compose-header-value-multiline"); + // the client's own fields would be shadowed by a copy + for (const reserved of ["Subject: x", "subject: x", "Reply-To: x", "In-Reply-To: x", "accept: x"]) + eq(`hdr-reserved[${reserved}]`, refused(reserved), "compose-header-reserved-key"); + + const rejects = (pairs) => { + try { proto.validateHeaders(pairs); return null; } catch (err) { return err.id; } + }; + eq("hdr-valid", rejects([["Urgency", "high"]]), null); + eq("hdr-duplicate", rejects([["Urgency", "high"], ["urgency", "low"]]), "compose-header-duplicate-key"); + eq("hdr-too-large", rejects([0, 1, 2, 3, 4].map(i => [`X-Key${i}`, "v".repeat(1024)])), "compose-headers-too-large"); + eq("hdr-too-many", rejects(Array.from({ length: 61 }, (_, i) => [`K${i}`, "v"])), "compose-headers-too-many"); + + // only the sender's own fields carry into a reply + const rows = proto.replyRows({ priority: "high", "in-reply-to": "ab".repeat(32), subject: "shadow", accept: "token" }); + eq("hdr-reply-rows", rows.join("|"), "priority: high"); + + // user rows round-trip through the block the reader parses + const built = proto.buildFrontmatter([["Subject", "hi"], ["X-Mood", "fine"]], "body"); + eq("hdr-roundtrip", proto.parseFrontmatter(built).fields["x-mood"], "fine"); +} + // --- addresses (§3) ----------------------------------------------------------- { diff --git a/web/index.html b/web/index.html index df0d582..8263636 100644 --- a/web/index.html +++ b/web/index.html @@ -144,6 +144,8 @@