capsite/lib: endo's own matcher data, smallcaps, and MapStoreDo

Matchers are now exactly what @endo/patterns makes: tagged records as
from pass-style's makeTagged (M.string() is match:string with payload
[], M.any() is match:any with payload undefined). Structured clone drops
their symbol-keyed and non-enumerable properties, so they cross DO
storage and Workers RPC in marshal's smallcaps encoding (the subset
patterns need).

MapStoreDo is a MapStore as a DO of its own: its creator fixes label and
(encoded) shapes once; its id is full authority, and readOnly() hands
out the queries alone.

Generated-by: Claude Opus 5.5
This commit is contained in:
dckc committed 2026-10-09 02:32:22 -05:00
1 parent 10414c2ce6
commit 357e4561c4
5 files changed
+224 -25

No files matched your search

+99 -21
View File
@@ -1,43 +1,67 @@
/**
* @file a work-alike for enough of @endo/patterns: `M.string()`, `M.any()`,
* and literal patterns (a string, number, boolean, or null matches only
* itself, so `"cookie"` as a key shape admits exactly one key).
* itself, so `"cookie"` as a key shape admits exactly one key). Matchers
* are the same data endo makes: tagged records, as from @endo/pass-style's
* `makeTagged` (frozen here, where endo would harden).
*
* Structured clone (DO storage, Workers RPC) drops a tagged record's
* symbol-keyed and non-enumerable properties, so patterns cross those
* boundaries in endo's smallcaps encoding: see {@link toSmallcaps}.
* @see https://github.com/endojs/endo/tree/master/packages/patterns
* @see https://github.com/endojs/endo/blob/master/packages/marshal/src/encodeToSmallcaps.js
*/
type MatcherKind = "string" | "any";
const PASS_STYLE = Symbol.for("passStyle");
/** a matcher made by {@link M}; recognized by identity, so it is meaningful
* only in the realm that made it (it does not survive an RPC copy) */
export type Matcher = Readonly<{ toString(): string }>;
export type Pattern = Matcher | string | number | boolean | null;
export type Tagged = Readonly<{ [Symbol.toStringTag]: string; payload: unknown }>;
export type Pattern = Tagged | string | number | boolean | null;
const kinds = new WeakMap<object, MatcherKind>();
/** @endo/pass-style's `makeTagged` */
export function makeTagged(tag: string, payload: unknown): Tagged {
return Object.freeze(
Object.create(Object.prototype, {
[PASS_STYLE]: { value: "tagged" },
[Symbol.toStringTag]: { value: tag },
payload: { value: payload, enumerable: true },
}),
) as Tagged;
}
const makeMatcher = (kind: MatcherKind, label: string): Matcher => {
const matcher = Object.freeze({ toString: () => label });
kinds.set(matcher, kind);
return matcher;
};
const isTagged = (x: unknown): x is Tagged =>
typeof x === "object" && x !== null && (x as Record<symbol, unknown>)[PASS_STYLE] === "tagged";
const STRING = makeMatcher("string", "M.string()");
const ANY = makeMatcher("any", "M.any()");
const StringShape = makeTagged("match:string", Object.freeze([]));
const AnyShape = makeTagged("match:any", undefined);
export const M = Object.freeze({
string: (): Matcher => STRING,
any: (): Matcher => ANY,
string: (): Tagged => StringShape,
any: (): Tagged => AnyShape,
});
export function matches(specimen: unknown, pattern: Pattern): boolean {
if (typeof pattern === "object" && pattern !== null) {
const kind = kinds.get(pattern);
if (kind === "string") return typeof specimen === "string";
if (kind === "any") return true;
const tag = isTagged(pattern) ? pattern[Symbol.toStringTag] : undefined;
switch (tag) {
case "match:string":
return typeof specimen === "string";
case "match:any":
return true;
default:
throw new TypeError("not a pattern");
}
}
return Object.is(specimen, pattern);
}
/** the pattern as written in code, e.g. `M.string()` or `"cookie"` */
export function describe(pattern: Pattern): string {
if (typeof pattern === "object" && pattern !== null) {
return `M.${pattern[Symbol.toStringTag].replace(/^match:/, "")}()`;
}
return JSON.stringify(pattern);
}
/**
* throw unless `specimen` matches `pattern`. Unlike endo's, the message
* names the pattern and `label` but never the specimen: specimens here
@@ -45,7 +69,61 @@ export function matches(specimen: unknown, pattern: Pattern): boolean {
*/
export function mustMatch(specimen: unknown, pattern: Pattern, label: string): void {
if (!matches(specimen, pattern)) {
const shape = typeof pattern === "object" && pattern !== null ? String(pattern) : JSON.stringify(pattern);
throw new TypeError(`${label}: must match ${shape}`);
throw new TypeError(`${label}: must match ${describe(pattern)}`);
}
}
/** a smallcaps-encoded passable, as JSON-able data */
export type Smallcaps = null | boolean | number | string | Smallcaps[] | { "#tag": string; payload: Smallcaps };
/** smallcaps quotes strings that start with one of these with `!` */
const startsSpecial = (s: string) => /^[!-\-]/.test(s);
/**
* the subset of @endo/marshal's smallcaps encoding that patterns here use:
* primitives, arrays, and tagged records (not a whole CapData: no `#`
* body prefix, no slots)
*/
export function toSmallcaps(passable: unknown): Smallcaps {
if (passable === undefined) return "#undefined";
if (passable === null || typeof passable === "boolean") return passable;
if (typeof passable === "number") {
if (Number.isNaN(passable)) return "#NaN";
if (passable === Infinity) return "#Infinity";
if (passable === -Infinity) return "#-Infinity";
return Object.is(passable, -0) ? 0 : passable;
}
if (typeof passable === "string") return startsSpecial(passable) ? `!${passable}` : passable;
if (Array.isArray(passable)) return passable.map(toSmallcaps);
if (isTagged(passable)) {
return { "#tag": toSmallcaps(passable[Symbol.toStringTag]) as string, payload: toSmallcaps(passable.payload) };
}
throw new TypeError("toSmallcaps: unsupported passable");
}
/** the inverse of {@link toSmallcaps} */
export function fromSmallcaps(encoding: Smallcaps): unknown {
if (encoding === null || typeof encoding === "boolean" || typeof encoding === "number") return encoding;
if (typeof encoding === "string") {
if (encoding.startsWith("!")) return encoding.slice(1);
switch (encoding) {
case "#undefined":
return undefined;
case "#NaN":
return NaN;
case "#Infinity":
return Infinity;
case "#-Infinity":
return -Infinity;
}
if (startsSpecial(encoding)) throw new TypeError("fromSmallcaps: unsupported special string");
return encoding;
}
if (Array.isArray(encoding)) return Object.freeze(encoding.map(fromSmallcaps));
if (typeof encoding === "object" && "#tag" in encoding) {
const { "#tag": tag, payload, ...rest } = encoding;
if (Object.keys(rest).length > 0) throw new TypeError("fromSmallcaps: extra properties on a tagged record");
return makeTagged(fromSmallcaps(tag) as string, fromSmallcaps(payload));
}
throw new TypeError("fromSmallcaps: unsupported encoding");
}
+60
View File
@@ -0,0 +1,60 @@
/**
* @file a MapStore as a Durable Object of its own, so a store can be held
* apart from whoever reads it: the DO id is the store's full authority
* (e.g. the account owner's, to rotate a credential), while a reader is
* handed only {@link MapStoreDo.readOnly}.
*/
import { DurableObject } from "cloudflare:workers";
import { fromSmallcaps, matches, toSmallcaps, type Pattern, type Smallcaps } from "./patterns.js";
import { makeMapStore, type MapStore, type ReadonlyMapStore, type StoreShapes } from "./store.js";
/** store shapes as they cross RPC and sit in storage: smallcaps-encoded
* (structured clone would strip a matcher down to its payload) */
export type EncodedShapes = { keyShape?: Smallcaps; valueShape?: Smallcaps };
export const encodeShapes = ({ keyShape, valueShape }: StoreShapes): EncodedShapes => ({
...(keyShape === undefined ? {} : { keyShape: toSmallcaps(keyShape) }),
...(valueShape === undefined ? {} : { valueShape: toSmallcaps(valueShape) }),
});
const decodeShapes = ({ keyShape, valueShape }: EncodedShapes): StoreShapes => ({
...(keyShape === undefined ? {} : { keyShape: fromSmallcaps(keyShape) as Pattern }),
...(valueShape === undefined ? {} : { valueShape: fromSmallcaps(valueShape) as Pattern }),
});
type Config = { label: string; shapes: EncodedShapes };
/** a store whose label and shapes its creator fixes, once */
export class MapStoreDo extends DurableObject {
/** one-shot: fix the label and shapes (see {@link encodeShapes}); an
* identical re-configure is a no-op */
configure(label: string, shapes: EncodedShapes): void {
const decoded = decodeShapes(shapes);
for (const shape of [decoded.keyShape, decoded.valueShape]) {
if (shape !== undefined) matches(undefined, shape); // throws on a non-pattern
}
makeMapStore(label, decoded, this.ctx.storage.kv); // throws on a bad label
const config: Config = { label, shapes: encodeShapes(decoded) };
const existing = this.ctx.storage.kv.get<Config>("config");
if (existing) {
if (JSON.stringify(existing) !== JSON.stringify(config)) {
throw new Error("already configured with a different label or shapes");
}
return;
}
this.ctx.storage.kv.put("config", config);
}
/** the store, with full authority */
store(): MapStore<unknown> {
const config = this.ctx.storage.kv.get<Config>("config");
if (!config) throw new Error("not configured: this store was never set up");
return makeMapStore(config.label, decodeShapes(config.shapes), this.ctx.storage.kv);
}
/** the store's queries alone */
readOnly(): ReadonlyMapStore<unknown> {
return this.store().readOnly();
}
}
+57 -2
View File
@@ -1,9 +1,13 @@
import { env } from "cloudflare:workers";
import { evictDurableObject } from "cloudflare:test";
import { describe, expect, it } from "vitest";
import { M, matches, mustMatch } from "../src/patterns.js";
import { M, fromSmallcaps, makeTagged, matches, mustMatch, toSmallcaps } from "../src/patterns.js";
import { encodeShapes } from "../src/store-do.js";
type TestEnv = { STORE_DO: DurableObjectNamespace<import("./test-worker.js").StoreHost> };
type TestEnv = {
STORE_DO: DurableObjectNamespace<import("./test-worker.js").StoreHost>;
MAP_STORE_DO: DurableObjectNamespace<import("../src/store-do.js").MapStoreDo>;
};
const testEnv = env as unknown as TestEnv;
const SECRET = "session=s3kr1t";
@@ -27,6 +31,28 @@ describe("patterns", () => {
expect(matches("authorization", "cookie")).toBe(false);
});
it("matchers are endo's tagged records", () => {
const s = M.string() as unknown as Record<string | symbol, unknown>;
expect(s[Symbol.for("passStyle")]).toBe("tagged");
expect(s[Symbol.toStringTag]).toBe("match:string");
expect(Object.keys(s)).toEqual(["payload"]);
expect(s.payload).toEqual([]);
expect((M.any() as unknown as Record<string, unknown>).payload).toBeUndefined();
expect(Object.isFrozen(s)).toBe(true);
});
it("smallcaps: endo's encoding, round-tripped", () => {
expect(toSmallcaps(M.string())).toEqual({ "#tag": "match:string", payload: [] });
expect(toSmallcaps(M.any())).toEqual({ "#tag": "match:any", payload: "#undefined" });
expect(toSmallcaps("#cookie")).toBe("!#cookie");
expect(toSmallcaps([NaN, -0, Infinity])).toEqual(["#NaN", 0, "#Infinity"]);
const back = fromSmallcaps(toSmallcaps(M.string())) as Parameters<typeof matches>[1];
expect(matches("x", back)).toBe(true);
expect(matches(1, back)).toBe(false);
expect(fromSmallcaps("!#cookie")).toBe("#cookie");
expect(() => matches("x", makeTagged("match:nope", []))).toThrow("not a pattern");
});
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);
@@ -85,3 +111,32 @@ describe("MapStore over RPC", () => {
expect(await again.keys()).toEqual([]);
});
});
describe("MapStoreDo", () => {
const COOKIE_SHAPES = encodeShapes({ keyShape: "cookie", valueShape: M.string() });
const fresh = () => testEnv.MAP_STORE_DO.get(testEnv.MAP_STORE_DO.newUniqueId());
it("keeps the shapes its creator chose, across eviction", async () => {
const stub = fresh();
await stub.configure("cookie", COOKIE_SHAPES);
await evictDurableObject(stub);
{
using store = await stub.store();
expect(await errorOf(store.init("authorization", SECRET))).toContain('cookie: key: must match "cookie"');
expect(await errorOf(store.init("cookie", 42))).toContain("cookie: value: must match M.string()");
await store.init("cookie", "session=a");
}
using rd = await stub.readOnly();
expect(await rd.entries()).toEqual([["cookie", "session=a"]]);
});
it("configures once: the same shapes again is a no-op, others are refused", async () => {
const stub = fresh();
await stub.configure("cookie", COOKIE_SHAPES);
await stub.configure("cookie", encodeShapes({ keyShape: "cookie", valueShape: M.string() }));
expect(await errorOf(stub.configure("cookie", encodeShapes({ keyShape: M.string() })))).toContain(
"already configured",
);
expect(await errorOf(fresh().store())).toContain("not configured");
});
});
+2
View File
@@ -3,6 +3,8 @@ import { DurableObject } from "cloudflare:workers";
import { M } from "../src/patterns.js";
import { makeMapStore, type MapStore } from "../src/store.js";
export { MapStoreDo } from "../src/store-do.js";
/** hosts the store from the credential-rotation sketch: one key, `cookie` */
export class StoreHost extends DurableObject {
cookies(): MapStore<string> {
+6 -2
View File
@@ -5,9 +5,13 @@
"main": "./tests/test-worker.ts",
"compatibility_date": "2026-09-29",
"durable_objects": {
"bindings": [{ "name": "STORE_DO", "class_name": "StoreHost" }]
"bindings": [
{ "name": "STORE_DO", "class_name": "StoreHost" },
{ "name": "MAP_STORE_DO", "class_name": "MapStoreDo" }
]
},
"exports": {
"StoreHost": { "type": "durable-object", "storage": "sqlite" }
"StoreHost": { "type": "durable-object", "storage": "sqlite" },
"MapStoreDo": { "type": "durable-object", "storage": "sqlite" }
}
}