import { describe, expect, it } from "vitest"; import { yurl32, unyurl32, hexToBytes, bytesToHex, glyphsFor, bytesInGlyphs, toWebkey, idFromWebkey, isWebkey, shashKey, type DurableObjectIdLike, } from "../src/webkeys.js"; // @ts-expect-error vite raw import import webkeysSource from "../src/webkeys.ts?raw"; const sampleHex = "ab9d14667a05453602b28218c3dc60995d890e49c1502ee1fffa2d0815b367ee"; /** mock a Cloudflare DurableObjectId */ function mockDurableObjectId(hex = sampleHex): DurableObjectIdLike { return { toString: () => hex, equals: (other: unknown) => typeof other === "object" && other !== null && "toString" in other && (other as { toString(): string }).toString() === hex, }; } describe("yurl32 and hex codecs", () => { it("matches the paper's alphabet sample", () => { expect("mhbqcmmva5ja3").toMatch(/^[a-z2-7]{13}$/); }); it("encodes 5 bits per glyph", () => { expect(glyphsFor(64)).toBe(13); expect(glyphsFor(128)).toBe(26); expect(glyphsFor(160)).toBe(32); expect(glyphsFor(256)).toBe(52); expect(bytesInGlyphs(26)).toBe(16); expect(bytesInGlyphs(52)).toBe(32); }); it("round-trips hex bytes", () => { const bytes = hexToBytes(sampleHex); expect(bytes.length).toBe(32); expect(bytesToHex(bytes)).toBe(sampleHex); }); it("round-trips bytes through yurl32", () => { const bytes = hexToBytes(sampleHex); const encoded = yurl32(bytes); expect(encoded).toMatch(/^[a-z2-7]+$/); expect(encoded.length).toBe(52); expect(unyurl32(encoded)).toEqual(bytes); }); it("handles leftovers: 0xff encodes to '74'", () => { expect(yurl32(new Uint8Array([0xff]))).toBe("74"); }); it("refuses garbage", () => { expect(() => unyurl32("0OIl")).toThrow(/not YURL32/); expect(() => unyurl32("AAAA-")).toThrow(); expect(() => hexToBytes("abc")).toThrow(/invalid hex length/); expect(() => hexToBytes("zz")).toThrow(/invalid hex byte/); }); }); describe("toWebkey and idFromWebkey (aligning with Worker unique ID)", () => { it("converts a DurableObjectId to a 52-glyph YURL32 webkey", () => { const doId = mockDurableObjectId(); const webkey = toWebkey(doId); expect(webkey.length).toBe(52); expect(webkey).toMatch(/^[a-z2-7]{52}$/); expect(isWebkey(webkey)).toBe(true); }); it("round-trips DurableObjectId -> webkey -> hex string", () => { const doId = mockDurableObjectId(); const webkey = toWebkey(doId); const hex = idFromWebkey(webkey); expect(hex).toBe(sampleHex); }); it("accepts a raw 64-char hex string as input to toWebkey", () => { const webkey = toWebkey(sampleHex); expect(webkey.length).toBe(52); expect(idFromWebkey(webkey)).toBe(sampleHex); }); it("accepts an already-hex ID in idFromWebkey for backward compatibility", () => { expect(idFromWebkey(sampleHex)).toBe(sampleHex); expect(idFromWebkey(sampleHex.toUpperCase())).toBe(sampleHex); }); it("encodes raw 16-byte (128-bit) entropy directly", () => { const bytes = new Uint8Array(16).fill(42); const webkey = toWebkey(bytes); expect(webkey.length).toBe(26); expect(isWebkey(webkey)).toBe(true); expect(isWebkey(webkey, 128)).toBe(true); expect(unyurl32(webkey)).toEqual(bytes); }); it("refuses malformed or wrong-sized webkeys in idFromWebkey", () => { expect(() => idFromWebkey("short")).toThrow(); // 26-char (128-bit) webkey is valid YURL32, but not a 256-bit Worker ID: const short128 = toWebkey(new Uint8Array(16)); expect(() => idFromWebkey(short128)).toThrow(/expected 32 bytes/); }); }); describe("shashKey (swissHash discipline)", () => { it("is one-way: identity, not authority", async () => { const doId = mockDurableObjectId(); const h = await shashKey(doId); expect(h).toMatch(/^[a-z2-7]+$/); expect(h.length).toBe(52); expect(h).not.toBe(toWebkey(doId)); }); it("is deterministic (joinable in logs)", async () => { const doId = mockDurableObjectId(); expect(await shashKey(doId)).toBe(await shashKey(doId)); }); }); describe("no ambient authority in this module", () => { it("the module's functions take their effects as arguments", () => { // strip comments before inspecting executable code const code = webkeysSource.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ""); expect(code.includes("getRandomValues")).toBe(false); expect(code.match(/crypto\.subtle/g)?.length ?? 0).toBe(1); // shashKey only }); });