@pramen/server 0.0.14 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/auth.d.ts CHANGED
@@ -4,10 +4,24 @@ import type { Identity } from "./sdk/acl";
4
4
  export interface VerifyStrategy {
5
5
  verify(token: string): Promise<Record<string, unknown> | null>;
6
6
  }
7
+ /** Optional, opt-in claim validation layered on top of signature + exp/nbf. All
8
+ * default OFF (unset) so existing tokens keep verifying; a deployment turns these on
9
+ * to tighten what it accepts. Shared by every strategy (they all run verifyJwt). */
10
+ export interface VerifyOptions {
11
+ /** Reject a token that has no numeric `exp` (RFC 7519 leaves exp optional; a strict
12
+ * deployment can require it so no non-expiring token is ever accepted). */
13
+ requireExp?: boolean;
14
+ /** Required audience. `payload.aud` (string or string[]) must contain at least one of
15
+ * these; a missing `aud` is rejected. Unset ⇒ `aud` not checked. */
16
+ audience?: string | string[];
17
+ /** Required issuer. `payload.iss` must equal this exactly. Unset ⇒ `iss` not checked. */
18
+ issuer?: string;
19
+ }
7
20
  /** HS256 via a shared secret. The dev/default strategy. */
8
21
  export declare class HmacStrategy implements VerifyStrategy {
9
22
  private readonly secret;
10
- constructor(secret: string);
23
+ private readonly opts;
24
+ constructor(secret: string, opts?: VerifyOptions);
11
25
  verify(token: string): Promise<Record<string, unknown> | null>;
12
26
  }
13
27
  /** RS256 verified against a remote JWKS. Public keys are fetched once and cached
@@ -16,10 +30,11 @@ export declare class HmacStrategy implements VerifyStrategy {
16
30
  export declare class JwksStrategy implements VerifyStrategy {
17
31
  readonly url: string;
18
32
  private readonly ttlMs;
33
+ private readonly opts;
19
34
  private keys;
20
35
  private fetchedAt;
21
36
  private inflight;
22
- constructor(url: string, ttlMs?: number);
37
+ constructor(url: string, ttlMs?: number, opts?: VerifyOptions);
23
38
  verify(token: string): Promise<Record<string, unknown> | null>;
24
39
  private lookup;
25
40
  private keyFor;
package/dist/auth.js CHANGED
@@ -21,9 +21,17 @@ function b64urlToBytes(s) {
21
21
  function b64urlToString(s) {
22
22
  return new TextDecoder().decode(b64urlToBytes(s));
23
23
  }
24
+ /** Does the token's `aud` claim satisfy the required audience? Token aud may be a
25
+ * string or an array; a match is any overlap with the expected audience(s). */
26
+ function audienceMatches(aud, expected) {
27
+ const claim = Array.isArray(aud) ? aud.filter((a) => typeof a === "string") : typeof aud === "string" ? [aud] : [];
28
+ const want = Array.isArray(expected) ? expected : [expected];
29
+ return want.some((w) => claim.includes(w));
30
+ }
24
31
  // Shared JWT pipeline: parse, verify the signature via the supplied function, then
25
- // validate exp/nbf. Any malformed part or a verification throw -> null (reject).
26
- async function verifyJwt(token, verifySignature) {
32
+ // validate exp/nbf and the opt-in exp-required/aud/iss claims. Any malformed part or a
33
+ // verification throw -> null (reject).
34
+ async function verifyJwt(token, verifySignature, opts = {}) {
27
35
  const parts = token.split(".");
28
36
  if (parts.length !== 3)
29
37
  return null;
@@ -52,17 +60,26 @@ async function verifyJwt(token, verifySignature) {
52
60
  return null;
53
61
  }
54
62
  const now = Math.floor(Date.now() / 1000);
55
- if (typeof payload.exp === "number" && now >= payload.exp)
63
+ const hasExp = typeof payload.exp === "number";
64
+ if (opts.requireExp && !hasExp)
65
+ return null;
66
+ if (hasExp && now >= payload.exp)
56
67
  return null;
57
68
  if (typeof payload.nbf === "number" && now < payload.nbf)
58
69
  return null;
70
+ if (opts.audience !== undefined && !audienceMatches(payload.aud, opts.audience))
71
+ return null;
72
+ if (opts.issuer !== undefined && payload.iss !== opts.issuer)
73
+ return null;
59
74
  return payload;
60
75
  }
61
76
  /** HS256 via a shared secret. The dev/default strategy. */
62
77
  export class HmacStrategy {
63
78
  secret;
64
- constructor(secret) {
79
+ opts;
80
+ constructor(secret, opts = {}) {
65
81
  this.secret = secret;
82
+ this.opts = opts;
66
83
  }
67
84
  verify(token) {
68
85
  return verifyJwt(token, async (input, signature, header) => {
@@ -70,7 +87,7 @@ export class HmacStrategy {
70
87
  return false;
71
88
  const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(this.secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
72
89
  return crypto.subtle.verify("HMAC", key, signature, new TextEncoder().encode(input));
73
- });
90
+ }, this.opts);
74
91
  }
75
92
  }
76
93
  const SINGLE_KEY = "\0single";
@@ -80,12 +97,14 @@ const SINGLE_KEY = "\0single";
80
97
  export class JwksStrategy {
81
98
  url;
82
99
  ttlMs;
100
+ opts;
83
101
  keys = new Map();
84
102
  fetchedAt = 0;
85
103
  inflight = null;
86
- constructor(url, ttlMs = 600_000) {
104
+ constructor(url, ttlMs = 600_000, opts = {}) {
87
105
  this.url = url;
88
106
  this.ttlMs = ttlMs;
107
+ this.opts = opts;
89
108
  }
90
109
  verify(token) {
91
110
  return verifyJwt(token, async (input, signature, header) => {
@@ -95,7 +114,7 @@ export class JwksStrategy {
95
114
  if (!key)
96
115
  return false;
97
116
  return crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, signature, new TextEncoder().encode(input));
98
- });
117
+ }, this.opts);
99
118
  }
100
119
  lookup(kid) {
101
120
  if (kid)
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,311 @@
1
+ #!/usr/bin/env bun
2
+ // pramen CLI — ships as the `pramen` bin of @pramen/server (see package.json `bin`).
3
+ // In-repo it's invoked via `bun run pramen <command>` (scripts/cli.ts is a thin
4
+ // wrapper around this module); published consumers get it as `pramen <command>`.
5
+ //
6
+ // pramen help
7
+ // pramen init [dir]
8
+ // pramen schema sql print CREATE TABLE for the schema
9
+ // pramen schema hash print the schema hash
10
+ // pramen schema snapshot save the current schema to .pramen/schema.json
11
+ // pramen schema diff compare the schema to the snapshot (safe vs unsafe changes)
12
+ // pramen schema status [--tenant t] [--url u] [--token jwt] compare a deployed tenant to the schema
13
+ // pramen token <sub> [roles...] [--tenant a,b] mint a dev JWT
14
+ //
15
+ // The bin uses a `bun` shebang: the `schema *` commands import your app module (a .ts
16
+ // file), and the built package's dist/ uses extensionless ESM imports — both of which
17
+ // bun resolves out of the box. Under plain Node the extensionless imports don't resolve
18
+ // (a property of the whole @pramen/server dist, not just this file), so run via bun.
19
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
20
+ import { dirname, resolve } from "node:path";
21
+ import { createTableSql } from "./runtime/ddl";
22
+ import { schemaHash } from "./runtime/migrate";
23
+ import { diffSchemaShape, schemaShape } from "./runtime/schema-diff";
24
+ import { entitiesInPartition, partitionsOf } from "./sdk/schema";
25
+ /** Mint an HS256 JWT — mirrors what a real auth service would issue, for local
26
+ * dev/testing (`pramen token`, and the default token for `schema status`). Signs
27
+ * with AUTH_SECRET when set, else the dev secret from the scaffolded oblaka.ts. */
28
+ const DEV_SECRET = "dev-secret-change-me";
29
+ function bytesToB64url(bytes) {
30
+ let bin = "";
31
+ for (const b of bytes)
32
+ bin += String.fromCharCode(b);
33
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
34
+ }
35
+ const strToB64url = (s) => bytesToB64url(new TextEncoder().encode(s));
36
+ async function sign(payload) {
37
+ const secret = process.env.AUTH_SECRET || DEV_SECRET;
38
+ const now = Math.floor(Date.now() / 1000);
39
+ const header = strToB64url(JSON.stringify({ alg: "HS256", typ: "JWT" }));
40
+ const body = strToB64url(JSON.stringify({ iat: now, exp: now + 3600, ...payload }));
41
+ const data = `${header}.${body}`;
42
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
43
+ const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data));
44
+ return `${data}.${bytesToB64url(new Uint8Array(sig))}`;
45
+ }
46
+ /** The sub-schema of a single partition (used to mirror the DO's per-partition hash,
47
+ * which migrate() computes over exactly this subset). For a single-partition app the
48
+ * subset equals the whole schema, so the hash is identical to the unpartitioned case. */
49
+ function partitionSchema(schema, partition) {
50
+ return Object.fromEntries(entitiesInPartition(schema, partition).map((t) => [t, schema[t]]));
51
+ }
52
+ const argv = process.argv.slice(2);
53
+ function flag(name) {
54
+ const i = argv.indexOf(`--${name}`);
55
+ return i >= 0 ? argv[i + 1] : undefined;
56
+ }
57
+ function positionals(args) {
58
+ const out = [];
59
+ for (let i = 0; i < args.length; i++) {
60
+ if (args[i].startsWith("--"))
61
+ i++; // skip flag + its value
62
+ else
63
+ out.push(args[i]);
64
+ }
65
+ return out;
66
+ }
67
+ function fail(msg) {
68
+ console.error(`pramen: ${msg}`);
69
+ process.exit(1);
70
+ }
71
+ async function loadApp() {
72
+ const explicit = flag("app");
73
+ const candidates = explicit ? [explicit] : ["./app.ts", "./example/app.ts"];
74
+ for (const c of candidates) {
75
+ const p = resolve(process.cwd(), c);
76
+ if (existsSync(p)) {
77
+ const mod = (await import(p));
78
+ if (mod.app?.schema)
79
+ return mod.app;
80
+ fail(`${c} does not export { app }`);
81
+ }
82
+ }
83
+ return fail(`no app found (looked for ${candidates.join(", ")}); pass --app <path>`);
84
+ }
85
+ const HELP = `pramen — reactive backend on Cloudflare
86
+
87
+ Usage: pramen <command>
88
+
89
+ help show this help
90
+ init [dir] scaffold a new project (app.ts + worker.ts + oblaka.ts)
91
+ schema sql print CREATE TABLE statements for the schema
92
+ schema hash print the schema hash
93
+ schema snapshot save the schema shape to .pramen/schema.json
94
+ schema diff compare the schema to the snapshot (safe vs unsafe)
95
+ schema status compare a deployed tenant's schema to the local schema
96
+ [--tenant t] [--url u] [--token jwt]
97
+ token <sub> [roles...] mint a dev JWT [--tenant a,b]
98
+
99
+ Flags: --app <path> to point at your app module (default ./app.ts or ./example/app.ts).`;
100
+ async function schemaCmd(sub) {
101
+ const snapshotPath = resolve(process.cwd(), ".pramen/schema.json");
102
+ if (sub === "sql") {
103
+ const { schema } = await loadApp();
104
+ for (const [table, def] of Object.entries(schema))
105
+ console.log(createTableSql(table, def) + ";");
106
+ return;
107
+ }
108
+ if (sub === "hash") {
109
+ const { schema } = await loadApp();
110
+ console.log(schemaHash(schema));
111
+ return;
112
+ }
113
+ if (sub === "snapshot") {
114
+ const { schema } = await loadApp();
115
+ mkdirSync(dirname(snapshotPath), { recursive: true });
116
+ const snap = { hash: schemaHash(schema), shape: schemaShape(schema) };
117
+ writeFileSync(snapshotPath, JSON.stringify(snap, null, 2) + "\n");
118
+ console.log(`wrote ${snapshotPath} (${Object.keys(snap.shape).length} tables)`);
119
+ return;
120
+ }
121
+ if (sub === "diff") {
122
+ const { schema } = await loadApp();
123
+ const next = schemaShape(schema);
124
+ if (!existsSync(snapshotPath)) {
125
+ console.log("no snapshot — run `pramen schema snapshot` to set a baseline.");
126
+ return;
127
+ }
128
+ const prev = JSON.parse(readFileSync(snapshotPath, "utf8")).shape;
129
+ const changes = diffSchemaShape(prev, next);
130
+ if (changes.length === 0) {
131
+ console.log("no changes since snapshot.");
132
+ return;
133
+ }
134
+ for (const c of changes) {
135
+ const where = c.column ? `${c.table}.${c.column}` : c.table;
136
+ const note = c.detail ? ` (${c.detail})` : "";
137
+ const glyph = c.destructive ? "⚠" : !c.appliesOnBoot ? "•" : "+";
138
+ const tags = (c.destructive ? " [destructive]" : "") + (!c.appliesOnBoot ? " [NOT applied on boot]" : "");
139
+ console.log(` ${glyph} ${c.kind} ${where}${note}${tags}`);
140
+ }
141
+ console.log("\nOn the next DO boot: additive changes (new table/column) auto-apply; destructive changes\n" +
142
+ "(drops, type changes, table rebuilds) are SKIPPED unless the deploy sets PRAMEN_ALLOW_DESTRUCTIVE=true\n" +
143
+ "(the schema hash is then left unwritten so a later opt-in deploy retries).");
144
+ if (changes.some((c) => c.destructive))
145
+ console.log("⚠ destructive changes rebuild the table and CAN lose data. A drop+add is applied as such unless\n" +
146
+ " the column declares `renamedFrom` (which migrates the data).");
147
+ if (changes.some((c) => !c.appliesOnBoot))
148
+ console.log("• [NOT applied on boot] a partition move — the entity's data lives in a different Durable Object,\n" +
149
+ " which needs a manual cross-DO data migration. The boot migrator won't move it; apply it yourself.\n" +
150
+ " (Modifier/constraint changes on an existing column ARE applied on boot now — a tightening one\n" +
151
+ " like adding NOT NULL/UNIQUE is gated by PRAMEN_ALLOW_DESTRUCTIVE and skipped if it can't apply.)");
152
+ return;
153
+ }
154
+ if (sub === "status") {
155
+ const { schema } = await loadApp();
156
+ const url = flag("url") ?? "http://localhost:8787";
157
+ const tenant = flag("tenant") ?? "main";
158
+ const token = flag("token") ?? (await sign({ sub: "cli", roles: ["admin"] }));
159
+ // Each partition is a distinct Durable Object class, migrated and hashed
160
+ // independently — so compare them one at a time, fetching each partition's applied
161
+ // schema from its DO. A single-(default-)partition app loops exactly once and reads
162
+ // identically to before. The default partition is addressed with no partition param.
163
+ const partitions = partitionsOf(schema);
164
+ console.log(`tenant: ${tenant}`);
165
+ for (const partition of partitions) {
166
+ if (partitions.length > 1)
167
+ console.log(`\npartition: ${partition}`);
168
+ const qs = partition === "default" ? "" : `&partition=${encodeURIComponent(partition)}`;
169
+ const res = await fetch(`${url}/admin/schema?tenant=${encodeURIComponent(tenant)}${qs}`, {
170
+ headers: { authorization: `Bearer ${token}` },
171
+ });
172
+ const body = (await res.json().catch(() => ({})));
173
+ if (!res.ok || !body.ok || !body.result)
174
+ fail(`status failed (partition ${partition}): ${body.error ?? res.status}`);
175
+ const live = body.result;
176
+ // The DO hashes only its partition's entities — mirror that here so the compared
177
+ // hashes line up (for a single partition this equals the whole-schema hash).
178
+ const subset = partitionSchema(schema, partition);
179
+ const current = schemaHash(subset);
180
+ const upToDate = live.hash === current;
181
+ console.log(`live: ${live.hash ?? "(none)"}`);
182
+ console.log(`current: ${current}`);
183
+ console.log(upToDate ? "✓ up to date" : "⚠ BEHIND — migrates on the tenant's next boot");
184
+ const want = schemaShape(subset);
185
+ for (const table of Object.keys(want)) {
186
+ const liveCols = new Set(live.tables[table] ?? []);
187
+ const missing = Object.keys(want[table].columns).filter((col) => !liveCols.has(col));
188
+ if (!live.tables[table])
189
+ console.log(` + table ${table} (not yet created live)`);
190
+ else if (missing.length)
191
+ console.log(` + ${table}: ${missing.join(", ")} (not yet added live)`);
192
+ }
193
+ }
194
+ return;
195
+ }
196
+ console.log(HELP);
197
+ }
198
+ async function tokenCmd(args) {
199
+ const pos = positionals(args);
200
+ const sub = pos[0];
201
+ if (!sub)
202
+ fail("token: <sub> required");
203
+ const roles = pos.slice(1);
204
+ const tenants = flag("tenant")?.split(",");
205
+ console.log(await sign({ sub, roles: roles.length ? roles : ["admin"], ...(tenants ? { tenants } : {}) }));
206
+ }
207
+ function initCmd(args) {
208
+ const dir = resolve(process.cwd(), positionals(args)[0] ?? ".");
209
+ mkdirSync(dir, { recursive: true });
210
+ const write = (name, content) => {
211
+ const p = resolve(dir, name);
212
+ if (existsSync(p))
213
+ return void console.log(` skip ${name} (exists)`);
214
+ writeFileSync(p, content);
215
+ console.log(` + ${name}`);
216
+ };
217
+ write("app.ts", APP_TEMPLATE);
218
+ write("worker.ts", WORKER_TEMPLATE);
219
+ write("oblaka.ts", OBLAKA_TEMPLATE);
220
+ console.log(`\nScaffolded a pramen project in ${dir}.\n`);
221
+ console.log("Next steps:");
222
+ console.log(" 1. Install deps: bun add @pramen/server && bun add -d oblaka-iac wrangler");
223
+ console.log(" 2. Generate config: bunx oblaka oblaka.ts (writes wrangler.jsonc)");
224
+ console.log(" 3. Run locally: bunx wrangler dev (serves http://localhost:8787)");
225
+ console.log("");
226
+ console.log("First request: POST http://localhost:8787/rpc/listNotes returns [] (not 403) —");
227
+ console.log("the scaffold's ACL grants the anonymous role read+create on `notes`. Tighten it");
228
+ console.log("in app.ts before shipping (see the comments there).");
229
+ }
230
+ const APP_TEMPLATE = `import { Entity, defineSchema, createApp, role, policy, allow } from "@pramen/server";
231
+
232
+ const schema = defineSchema({
233
+ notes: Entity((t) => ({ id: t.id(), title: t.text(), body: t.text(), createdAt: t.int() })),
234
+ });
235
+
236
+ const { query, mutation } = createApp(schema);
237
+
238
+ const handlers = {
239
+ listNotes: query((ctx) => ctx.db.find({ from: "notes", orderBy: { column: "id", dir: "desc" } })),
240
+ createNote: mutation((ctx, input: { title: string; body: string }) =>
241
+ ctx.db.insert("notes", { title: input.title, body: input.body, createdAt: Date.now() }),
242
+ ),
243
+ };
244
+
245
+ // ACL — deny-by-default; roles only GRANT. A caller with no verified token is the
246
+ // \`anonymous\` role, so this grants the scaffold's handlers on the first request (no
247
+ // token needed). TIGHTEN THIS before shipping: gate writes behind an authenticated
248
+ // role and scope reads with \`$identity(...)\` (see @pramen/auth and the pramen docs).
249
+ const acl = [
250
+ role("anonymous", [
251
+ policy("anon:notes:read", "notes", "read", allow()),
252
+ policy("anon:notes:create", "notes", "create", allow()),
253
+ ]),
254
+ ];
255
+
256
+ export const app = { schema, handlers, acl };
257
+ `;
258
+ const WORKER_TEMPLATE = `// The whole server entry: hand your app to createPramen and re-export the pair.
259
+ import { createPramen } from "@pramen/server/worker";
260
+ import { app } from "./app";
261
+
262
+ const pramen = createPramen(app);
263
+
264
+ export default { fetch: pramen.fetch };
265
+ export const PramenDO = pramen.PramenDO; // wrangler binds this by class_name
266
+ `;
267
+ const OBLAKA_TEMPLATE = `import { define, DurableObject, KVNamespace, R2Bucket, Worker } from "oblaka-iac";
268
+
269
+ const PROJECT = "my-pramen-app"; // unique per project — namespaces all CF resources
270
+
271
+ export default define(({ env }) => {
272
+ const vars =
273
+ env === "local" ? { AUTH_SECRET: "dev-secret-change-me", FILES_SECRET: "dev-files-secret-change-me" } : {};
274
+ return new Worker({
275
+ dir: ".",
276
+ name: PROJECT,
277
+ main: "./worker.ts",
278
+ compatibility_date: "2026-06-19",
279
+ compatibility_flags: ["nodejs_compat"],
280
+ observability: { enabled: true },
281
+ bindings: {
282
+ PRAMEN: new DurableObject({ name: PROJECT + "-store", className: "PramenDO" }),
283
+ KV: new KVNamespace({ name: PROJECT + "-kv" }),
284
+ FILES: new R2Bucket({ name: PROJECT + "-files" }),
285
+ },
286
+ vars,
287
+ });
288
+ });
289
+ `;
290
+ async function main() {
291
+ const cmd = argv[0];
292
+ switch (cmd) {
293
+ case undefined:
294
+ case "help":
295
+ case "-h":
296
+ case "--help":
297
+ console.log(HELP);
298
+ return;
299
+ case "init":
300
+ return initCmd(argv.slice(1));
301
+ case "schema":
302
+ return schemaCmd(argv[1]);
303
+ case "token":
304
+ return tokenCmd(argv.slice(1));
305
+ default:
306
+ console.error(`pramen: unknown command "${cmd}"\n`);
307
+ console.log(HELP);
308
+ process.exit(1);
309
+ }
310
+ }
311
+ await main();
@@ -35,6 +35,14 @@ export declare class PramenDOBase extends DurableObject<DoEnv> {
35
35
  * task context — a DO can't introspect its own idFromName. */
36
36
  private identityPersisted;
37
37
  private identityLoaded;
38
+ /** Live subscriptions per socket — held IN MEMORY, not in the WS attachment. The
39
+ * attachment is capped at ~2 KB by workerd, and 64 subs (each with arbitrary input
40
+ * JSON + a read-set + digest) blow past that well before MAX_SUBSCRIPTIONS. The
41
+ * tradeoff: this map is lost on DO hibernation/eviction, so a woken socket has no
42
+ * entry and is treated as having no active subscriptions — acceptable because the
43
+ * client replays its subscriptions on (re)connect. Keyed by the WebSocket object;
44
+ * cleaned up in webSocketClose. */
45
+ private readonly subsBySocket;
38
46
  constructor(ctx: DurableObjectState, env: DoEnv, app: PramenApp);
39
47
  private ensureMigrated;
40
48
  fetch(request: Request): Promise<Response>;
@@ -42,13 +50,16 @@ export declare class PramenDOBase extends DurableObject<DoEnv> {
42
50
  * pending alarm; a near-future time batches a burst of enqueues into one drain. */
43
51
  private armDrain;
44
52
  /** A privileged, system-scoped context for running task handlers (outside a request).
45
- * Task handlers get full db access + env (e.g. ctx.env.EMAIL) + their own ctx.tasks. */
53
+ * Task handlers get full db access + env (e.g. ctx.env.EMAIL) + their own ctx.tasks.
54
+ * Returns the `db` alongside the ctx so the drainer can broadcast the tables the task
55
+ * handlers touched (live queries would otherwise go stale after deferred/trigger work). */
46
56
  private taskCtx;
47
57
  /** Restore (tenant, partition) on a cold wake (e.g. an alarm with no request) so the
48
58
  * task context is scoped correctly. No-op once loaded/persisted this instance. */
49
59
  private loadIdentity;
50
60
  /** Drain due tasks. Called by the alarm (DO path) and the /__admin/tasks/drain route
51
- * (manual / cron). Returns the drain stats incl. `nextRunAt` for rescheduling. */
61
+ * (manual / cron). Returns the drain stats incl. `nextRunAt` for rescheduling, plus
62
+ * the union of tables the drained task handlers touched (for a post-commit broadcast). */
52
63
  private drainTasks;
53
64
  alarm(): Promise<void>;
54
65
  private handleDrain;
@@ -67,8 +78,15 @@ export declare class PramenDOBase extends DurableObject<DoEnv> {
67
78
  private get envBag();
68
79
  private filesFor;
69
80
  private identityOf;
70
- private getState;
71
- private setState;
81
+ /** The durable per-socket auth/routing state (identity + tenant + partition), read
82
+ * from the WS attachment. Survives hibernation; kept tiny to stay under workerd's cap. */
83
+ private getAttachment;
84
+ private setAttachment;
85
+ /** This socket's live subscriptions from the in-memory map (see `subsBySocket`). A
86
+ * hibernated/woken socket has no entry → no active subscriptions until the client
87
+ * replays them on reconnect. */
88
+ private getSubs;
89
+ private setSubs;
72
90
  private send;
73
91
  }
74
92
  /** Produce the concrete, app-bound Durable Object class. A DO is constructed by the