1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>plugin-context test</title>
</head>
<body>
<!-- Serve the repo root (python3 -m http.server) and open /javascript/src/plugin-context/test.html. The title ends PASS or FAIL. -->
<pre id="out"></pre>
<script type="module">
import { createPluginContext } from "./index.js";
const out = document.getElementById("out");
const say = (line) => { out.textContent += `${line}\n`; console.log(line); };
const assert = (ok, message) => { if (!ok) throw new Error(message); };
const eq = (a, b, message) => assert(JSON.stringify(a) === JSON.stringify(b), `${message}: ${JSON.stringify(a)} !== ${JSON.stringify(b)}`);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const dbName = `plugin-context-test-${crypto.randomUUID()}`;
const ctx = createPluginContext({
id: "atproto-integrations",
dbName,
storage: { clients: { indexes: [] }, pending: { indexes: ["createdAt"] }, sessions: { indexes: ["userId", "did", "updatedAt"] }, replays: { indexes: ["exp"] }, locks: { indexes: [] } },
settingsSchema: { sessionKey: { type: "secret" }, siteIntegrations: { type: "string" } },
});
const rawRow = async (key) => {
const db = await new Promise((resolve) => (indexedDB.open(dbName).onsuccess = (event) => resolve(event.target.result)));
const value = await new Promise((resolve) => (db.transaction("rows").objectStore("rows").get(key).onsuccess = (event) => resolve(event.target.result)));
db.close();
return value;
};
const tests = {
async "crud and versioned cas"() {
const { clients } = ctx.storage;
eq(await clients.get("a"), null, "absent get");
await clients.put("a", { n: 1, gone: undefined });
eq(await clients.get("a"), { n: 1 }, "json round trip");
assert(await clients.exists("a"), "exists");
const v = await clients.getVersioned("a");
eq((await clients.compareAndSet("a", null, { n: 2 })).applied, false, "create-only on existing");
const set = await clients.compareAndSet("a", v.revision, { n: 2 });
assert(set.applied && set.revision !== v.revision, "cas with revision");
eq((await clients.compareAndSet("a", v.revision, { n: 3 })).applied, false, "stale revision");
eq((await clients.compareAndDelete("a", v.revision)).applied, false, "stale delete");
eq((await clients.compareAndDelete("a", set.revision)).applied, true, "delete");
eq(await clients.delete("a"), false, "delete absent");
},
async "query where, orderBy, limit, cursor"() {
const { sessions } = ctx.storage;
await sessions.putMany([1, 2, 3, 4, 5].map((i) => ({ id: `s${i}`, data: { userId: i % 2 ? "u1" : "u2", did: "did:plc:x", updatedAt: i } })));
const mine = await sessions.query({ where: { userId: "u1" }, limit: 100 });
eq(mine.items.map((item) => item.id), ["s1", "s3", "s5"], "equality");
const newest = await sessions.query({ where: { did: "did:plc:x" }, orderBy: { updatedAt: "desc" }, limit: 2 });
eq([newest.items.map((item) => item.id), newest.hasMore], [["s5", "s4"], true], "orderBy desc + limit");
const next = await sessions.query({ where: { did: "did:plc:x" }, orderBy: { updatedAt: "desc" }, limit: 2, cursor: newest.cursor });
eq(next.items.map((item) => item.id), ["s3", "s2"], "cursor");
eq(await sessions.count({ updatedAt: { gte: 2, lt: 4 } }), 2, "range count");
eq(await sessions.count({ userId: { in: ["u2"] } }), 2, "in");
eq((await sessions.getMany(["s1", "nope"])).size, 1, "getMany");
eq(await sessions.deleteMany(["s1", "s2", "nope"]), 2, "deleteMany");
},
async "replay guard (service-auth.ts rememberJti)"() {
const { replays } = ctx.storage;
const now = Math.floor(Date.now() / 1000);
await replays.put("old jti", { exp: now - 10 });
const remember = async (jti) => {
const stale = await replays.query({ where: { exp: { lt: now } }, limit: 100 });
if (stale.items.length) await replays.deleteMany(stale.items.map((item) => item.id));
return (await replays.compareAndSet(`iss ${jti}`, null, { exp: now + 60 })).applied;
};
const raced = await Promise.all([remember("j1"), remember("j1"), remember("j1")]);
eq(raced.filter(Boolean).length, 1, "only one use of a jti");
eq(await replays.exists("old jti"), false, "stale pruned");
},
async "lease lock (lock.ts storageLock) serializes"() {
const { locks } = ctx.storage;
const acquire = async (name) => {
for (;;) {
const held = await locks.getVersioned(name);
if (!held || held.value.expiresAt < Date.now()) {
const claimed = await locks.compareAndSet(name, held?.revision ?? null, { expiresAt: Date.now() + 30_000 });
if (claimed.applied) return claimed.revision;
}
await sleep(5);
}
};
let inside = 0;
let max = 0;
await Promise.all(Array.from({ length: 5 }, async () => {
const revision = await acquire("scope name");
max = Math.max(max, ++inside);
await sleep(10);
inside--;
await locks.compareAndDelete("scope name", revision);
}));
eq(max, 1, "one holder at a time");
},
async "settings key race (seal.ts sealingKey)"() {
const keys = await Promise.all(Array.from({ length: 4 }, async (_, i) => {
const created = `key-${i}`;
const result = await ctx.settings.compareAndSet("sessionKey", null, created);
return result.applied ? created : ctx.settings.get("sessionKey");
}));
eq(new Set(keys).size, 1, "every caller sees the one key");
await ctx.settings.set("siteIntegrations", "at://a\nat://b");
eq(await ctx.settings.get("siteIntegrations"), "at://a\nat://b", "set/get");
eq((await ctx.settings.list("site")).map((entry) => entry.key), ["siteIntegrations"], "list prefix");
},
async "secret settings encrypted at rest"() {
const key = await ctx.settings.get("sessionKey");
const stored = (await rawRow(["settings", "sessionKey"])).value;
assert(stored.$secret && !JSON.stringify(stored).includes(key), "ciphertext in IndexedDB");
eq((await rawRow(["settings", "siteIntegrations"])).value, "at://a\nat://b", "non-secret stays plain");
eq((await ctx.settings.list("session"))[0].value, key, "list decrypts");
const meta = await rawRow(["meta", "secretKey"]);
assert(meta instanceof CryptoKey && !meta.extractable, "non-extractable key");
const other = createPluginContext({ id: "other-plugin", dbName, settingsSchema: { sessionKey: { type: "secret" } } });
let refused = false;
await other.settings.get("sessionKey").catch(() => (refused = true));
assert(refused, "ciphertext bound to plugin id");
},
async "updateIf guard and counters"() {
const { clients } = ctx.storage;
await clients.put("stock", { left: 2, name: "x" });
const take = () => clients.updateIf("stock", { where: { left: { gt: 0 } }, delta: { left: { dec: 1 } } });
const results = await Promise.all([take(), take(), take()]);
eq(results.filter((result) => result.applied).length, 2, "no oversell");
eq(await clients.get("stock"), { left: 0, name: "x" }, "counter at zero");
eq((await clients.updateIf("stock", { where: {}, set: { name: "y" } })).data, { left: 0, name: "y" }, "set");
eq((await clients.updateIf("missing", { where: {}, set: { name: "y" } })).applied, false, "never inserts");
eq((await clients.updateIf("stock", { where: {}, delta: { name: { inc: 1 } } })).applied, false, "non-integer counter");
await clients.delete("stock");
},
async "persists across contexts"() {
const again = createPluginContext({ id: "atproto-integrations", dbName, storage: { sessions: {} } });
eq(await again.storage.sessions.count(), 3, "same db, new context");
eq(again.storage.locks, undefined, "undeclared collection absent");
},
async "wasm runs statusphere module"() {
const module = new Uint8Array(await (await ctx.http.fetch(new URL("../../../target/wasm32-unknown-unknown/release/statusphere.wasm", import.meta.url).href)).arrayBuffer());
const output = await ctx.wasm.run({ module, entry: "set_status", input: { status: "🦋", did: "did:plc:x", now: "2026-11-07T00:00:00.000Z", state: null, event: { type: "start" } } });
eq(output.calls?.[0]?.nsid, "com.atproto.repo.listRecords", "first call");
let mismatch;
await ctx.wasm.run({ module, entry: "set_status", sha256: "0".repeat(64) }).catch((error) => (mismatch = error.code));
eq(mismatch, "WASM_DIGEST_MISMATCH", "digest check");
},
async "wasm that never returns times out without blocking the page"() {
// exports memory, alloc, and spin(ptr, len) -> i64 that loops forever
const spin = new Uint8Array([0,97,115,109,1,0,0,0, 1,12,2,96,1,127,1,127,96,2,127,127,1,126, 3,3,2,0,1, 5,3,1,0,1, 7,25,3,6,109,101,109,111,114,121,2,0,5,97,108,108,111,99,0,0,4,115,112,105,110,0,1, 10,16,2,4,0,65,0,11,9,0,3,64,12,0,11,66,0,11]);
const quick = createPluginContext({ id: "spin", dbName, wasmTimeoutMs: 300 });
let ticks = 0;
const ticker = setInterval(() => ticks++, 20);
const started = performance.now();
let code;
await quick.wasm.run({ module: spin, entry: "spin" }).catch((error) => (code = error.code));
clearInterval(ticker);
eq(code, "WASM_TIMEOUT", "timeout code");
assert(performance.now() - started < 2000, "gave up near the limit");
assert(ticks >= 5, `page kept running (${ticks} ticks)`);
},
async "site, url, log"() {
eq(ctx.site.url, location.origin, "site url");
eq(ctx.url("/x"), `${location.origin}/x`, "url helper");
ctx.log.warn("log check", { ok: true });
},
};
let failed = 0;
for (const [name, test] of Object.entries(tests)) {
try {
await test();
say(`ok ${name}`);
} catch (error) {
failed++;
say(`FAIL ${name}: ${error.message}`);
}
}
indexedDB.deleteDatabase(dbName);
document.title = failed ? `FAIL ${failed}` : "PASS";
say(document.title);
</script>
</body>
</html>