From 933dbf7bc5d5532a3f2a8e18ba4afb02059aa5c1 Mon Sep 17 00:00:00 2001 From: Aanish Bhirud <47579874+baanish@users.noreply.github.com> Date: Fri, 20 Mar 2026 03:02:25 -0400 Subject: [PATCH] fix(arx): prefer chat-safe transport lengths --- src/lib/payload/fragment.ts | 14 ++++++++++---- tests/arx-codec.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/lib/payload/fragment.ts b/src/lib/payload/fragment.ts index 2b6edfb..516cb18 100644 --- a/src/lib/payload/fragment.ts +++ b/src/lib/payload/fragment.ts @@ -36,17 +36,23 @@ type CandidateFragment = { transportLength: number; }; +const CHAT_SAFE_ASCII_FRAGMENT_CHARS = /^[A-Za-z0-9\-._~=#]+$/; + /** - * Computes the serialized length of a fragment value as it would appear in a - * URL after browser percent-encoding of non-ASCII characters. - * Each non-ASCII UTF-8 byte is encoded as %XX (3 chars per byte). + * Computes the serialized length of a fragment value after conservative transport escaping. + * + * We count non-ASCII code points by their UTF-8 percent-encoded size, and we also treat + * ASCII punctuation outside the URL-unreserved fragment subset as escape-prone because many + * chat/link surfaces rewrite those characters even when a browser would accept them in-place. + * This keeps auto-selection aligned with the product's chat-safe fragment goal, allowing the + * `B.` base64url ARX wire shape to win when punctuation-heavy base76 would grow after sharing. */ function computeTransportLength(value: string): number { let len = 0; for (let i = 0; i < value.length; i++) { const cp = value.codePointAt(i)!; if (cp < 128) { - len += 1; + len += CHAT_SAFE_ASCII_FRAGMENT_CHARS.test(value[i]) ? 1 : 3; } else if (cp < 0x800) { len += 6; // 2 UTF-8 bytes → %XX%XX } else if (cp < 0x10000) { diff --git a/tests/arx-codec.test.ts b/tests/arx-codec.test.ts index 8243ca7..eee8888 100644 --- a/tests/arx-codec.test.ts +++ b/tests/arx-codec.test.ts @@ -296,6 +296,28 @@ describe("arx fragment round-trip", () => { expect(autoHash).toContain(`v1.arx.${getActiveDictVersion()}.`); }); + it("async arx selection can choose the chat-safe base64url wire form", async () => { + const bigEnvelope: PayloadEnvelope = { + ...envelope, + artifacts: [ + { + id: "doc", + kind: "markdown", + filename: "doc.md", + content: [ + "# Chat-safe ARX", + "", + ...Array.from({ length: 120 }, (_, index) => `- item ${index}: The quick brown fox jumps over the lazy dog.`), + ].join("\n"), + }, + ], + }; + + const autoHash = await encodeEnvelopeAsync(bigEnvelope, { codec: "arx" }); + expect(autoHash).toContain(`v1.arx.${getActiveDictVersion()}.B.`); + }); + + it("decodes arx fragments when unicode payload chars are percent-escaped", async () => { const hash = `#${await encodeEnvelopeAsync(envelope, { codec: "arx" })}`; const escapedHash = hash.replace(/[^\x00-\x7F]/g, (char) => encodeURIComponent(char));