The dump worker becomes a page cache: a hit is served from DO SQLite storage with no diigo call; a miss asks the webapp instance in the cache's wiring and etches the page. - two independent caches, one behavior (PagedCache): DiigoCache keeps every item; PublicCache keeps shared items only, dropping private rows before they are stored; served at /public/<username> (HTML page form, or JSON) - create flow on one port: /_connect relays webapp's mint form, /_mint proxies the mint, /_wire wires a private or public cache; /_publish adds a public cache later - ids are webkeys: kept out of URLs (POST bodies), shown and logged only as a hash fingerprint; saved to the password manager under the holder's petname (never sent to the server) - items keyed on diigo's link_id; username from u_name; unkeyable items and every failure path are logged; each page read logs hit/miss - tests run the real worker + DOs in the vitest pool against a fake webapp (tests/fake-webapp.js) Generated-by: opencode (GLM) Generated-by: Claude Opus 5.5
139 lines
4.3 KiB
TypeScript
139 lines
4.3 KiB
TypeScript
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
|
|
});
|
|
}); |