capsite/lib: MapStore and patterns work-alikes
Enough of @agoric/store's MapStore and @endo/patterns' M for the
credential-rotation design: makeMapStore(label, { keyShape, valueShape },
kv) keeps entries in a DO's sync KV under store:<label>:, and readOnly()
hands out the queries alone. Facets are workerd RpcTargets in place of
endo's Far.
Unlike endo, error messages never include keys or values: the values
here are credentials, and messages cross RPC into logs.
Generated-by: Claude Opus 5.5
This commit is contained in:
1 parent
1d8a220bbc
commit
9179bdbf38
10 files changed
+2586
No files matched your search
@@ -0,0 +1,87 @@
|
||||
import { env } from "cloudflare:workers";
|
||||
import { evictDurableObject } from "cloudflare:test";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { M, matches, mustMatch } from "../src/patterns.js";
|
||||
|
||||
type TestEnv = { STORE_DO: DurableObjectNamespace<import("./test-worker.js").StoreHost> };
|
||||
const testEnv = env as unknown as TestEnv;
|
||||
|
||||
const SECRET = "session=s3kr1t";
|
||||
|
||||
/** the message an RPC call fails with, or "" if it succeeds */
|
||||
async function errorOf(call: Promise<unknown>): Promise<string> {
|
||||
try {
|
||||
await call;
|
||||
return "";
|
||||
} catch (err) {
|
||||
return String(err);
|
||||
}
|
||||
}
|
||||
|
||||
describe("patterns", () => {
|
||||
it("M.string() matches strings only; a literal matches only itself", () => {
|
||||
expect(matches("x", M.string())).toBe(true);
|
||||
expect(matches(1, M.string())).toBe(false);
|
||||
expect(matches(undefined, M.any())).toBe(true);
|
||||
expect(matches("cookie", "cookie")).toBe(true);
|
||||
expect(matches("authorization", "cookie")).toBe(false);
|
||||
});
|
||||
|
||||
it("mustMatch names the shape but not the specimen", () => {
|
||||
expect(() => mustMatch(SECRET, "cookie", "jar: key")).toThrow('jar: key: must match "cookie"');
|
||||
expect(() => mustMatch(SECRET, "cookie", "jar: key")).not.toThrow(SECRET);
|
||||
});
|
||||
});
|
||||
|
||||
describe("MapStore over RPC", () => {
|
||||
const fresh = () => testEnv.STORE_DO.get(testEnv.STORE_DO.newUniqueId());
|
||||
|
||||
it("rotates: the admin's set is what the read-only view sees next", async () => {
|
||||
const store = await fresh().cookies();
|
||||
const rd = await store.readOnly();
|
||||
await store.init("cookie", "session=old");
|
||||
expect(await rd.get("cookie")).toBe("session=old");
|
||||
await store.set("cookie", "session=new");
|
||||
expect(await rd.entries()).toEqual([["cookie", "session=new"]]);
|
||||
});
|
||||
|
||||
it("the read-only view has no write methods", async () => {
|
||||
const store = await fresh().cookies();
|
||||
const rd = (await store.readOnly()) as unknown as { set(k: string, v: string): Promise<void> };
|
||||
expect(await errorOf(rd.set("cookie", SECRET))).not.toBe("");
|
||||
expect(await store.has("cookie")).toBe(false);
|
||||
});
|
||||
|
||||
it("the key shape admits only `cookie`; errors carry no values", async () => {
|
||||
const store = await fresh().cookies();
|
||||
const err = await errorOf(store.init("authorization", SECRET));
|
||||
expect(err).toContain('cookie: key: must match "cookie"');
|
||||
expect(err).not.toContain(SECRET);
|
||||
expect(await errorOf(store.init("cookie", 42 as unknown as string))).toContain("cookie: value: must match M.string()");
|
||||
});
|
||||
|
||||
it("init refuses a present key; set and get refuse a missing one", async () => {
|
||||
const store = await fresh().cookies();
|
||||
expect(await errorOf(store.set("cookie", SECRET))).toContain("cookie: key not found");
|
||||
expect(await errorOf(store.get("cookie"))).toContain("cookie: key not found");
|
||||
await store.init("cookie", SECRET);
|
||||
const err = await errorOf(store.init("cookie", SECRET));
|
||||
expect(err).toContain("cookie: key already registered");
|
||||
expect(err).not.toContain(SECRET);
|
||||
});
|
||||
|
||||
it("survives eviction: entries live in DO storage", async () => {
|
||||
const stub = fresh();
|
||||
{
|
||||
// disposed before eviction: a live store stub holds the DO open
|
||||
using store = await stub.cookies();
|
||||
await store.init("cookie", "session=durable");
|
||||
}
|
||||
await evictDurableObject(stub);
|
||||
using again = await stub.cookies();
|
||||
expect(await again.get("cookie")).toBe("session=durable");
|
||||
expect(await again.getSize()).toBe(1);
|
||||
await again.delete("cookie");
|
||||
expect(await again.keys()).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
/** @file the tests' main module: a DO that hosts stores on its own storage. */
|
||||
import { DurableObject } from "cloudflare:workers";
|
||||
import { M } from "../src/patterns.js";
|
||||
import { makeMapStore, type MapStore } from "../src/store.js";
|
||||
|
||||
/** hosts the store from the credential-rotation sketch: one key, `cookie` */
|
||||
export class StoreHost extends DurableObject {
|
||||
cookies(): MapStore<string> {
|
||||
return makeMapStore<string>("cookie", { keyShape: "cookie", valueShape: M.string() }, this.ctx.storage.kv);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
fetch: () => new Response("store tests", { status: 404 }),
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["@cloudflare/vitest-plugin/types"]
|
||||
},
|
||||
"include": ["./**/*", "../src/**/*", "../worker-configuration.d.ts"]
|
||||
}
|
||||
Reference in new issue
Block a user