Skip to content

Commit 46cc2cb

Browse files
committed
fix(web): org-membership hash sorts keys ordinally — locale-proof cross-runtime contract
1 parent 746f249 commit 46cc2cb

2 files changed

Lines changed: 10 additions & 1 deletion

File tree

apps/web/src/lib/org-membership/message.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export type OrgAction =
1010
export const MAX_MESSAGE_AGE_SECONDS = 300;
1111

1212
export function hashPayload(payload: Record<string, unknown>): string {
13-
const sorted = Object.fromEntries(Object.entries(payload).sort(([a], [b]) => a.localeCompare(b)));
13+
const sorted = Object.fromEntries(Object.entries(payload).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
1414
return createHash("sha256").update(JSON.stringify(sorted)).digest("hex");
1515
}
1616

apps/web/tests/org-membership-message.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, it } from "node:test";
22
import assert from "node:assert/strict";
3+
import { createHash } from "node:crypto";
34
import { buildOrgMessage, hashPayload, MAX_MESSAGE_AGE_SECONDS } from "../src/lib/org-membership/message";
45

56
describe("org-membership message", () => {
@@ -16,4 +17,12 @@ describe("org-membership message", () => {
1617
it("exports the replay window", () => {
1718
assert.equal(MAX_MESSAGE_AGE_SECONDS, 300);
1819
});
20+
it("sorts keys ordinally (byte order), not locale-aware", () => {
21+
// Ordinal: "B" (0x42) < "a" (0x61). Locale-aware collation would put "a" first.
22+
const expectedHash = createHash("sha256").update('{"B":2,"a":1}').digest("hex");
23+
assert.equal(hashPayload({ a: 1, B: 2 }), expectedHash);
24+
const msg = buildOrgMessage("leave", "0xABCDEF0000000000000000000000000000000001", 1753900000, { a: 1, B: 2 });
25+
const msgManual = buildOrgMessage("leave", "0xABCDEF0000000000000000000000000000000001", 1753900000, Object.fromEntries([["B",2],["a",1]]));
26+
assert.equal(msg, msgManual);
27+
});
1928
});

0 commit comments

Comments
 (0)