diigo-cache: bookmark cache in Durable Objects (goal 1c)
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
This commit is contained in:
1 parent
c5e433cc9c
commit
6f409fc83a
15 files changed
+1664
-211
No files matched your search
@@ -0,0 +1,139 @@
|
||||
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
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user