@pramen/server 0.0.48 → 0.0.49

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.
Files changed (49) hide show
  1. package/dist/auth.d.ts +4 -3
  2. package/dist/cli.js +15 -9
  3. package/dist/durable-object.d.ts +1 -0
  4. package/dist/durable-object.js +10 -5
  5. package/dist/index.d.ts +3 -3
  6. package/dist/pramen.d.ts +4 -2
  7. package/dist/runtime/acl.d.ts +6 -5
  8. package/dist/runtime/acl.js +1 -5
  9. package/dist/runtime/db.d.ts +3 -2
  10. package/dist/runtime/dispatch.d.ts +3 -1
  11. package/dist/runtime/driver.d.ts +8 -5
  12. package/dist/runtime/mail.d.ts +2 -1
  13. package/dist/runtime/protocol.d.ts +4 -3
  14. package/dist/runtime/queue-consumer.d.ts +4 -2
  15. package/dist/runtime/queue.d.ts +3 -2
  16. package/dist/runtime/read-engine.d.ts +11 -9
  17. package/dist/runtime/read-engine.js +4 -1
  18. package/dist/runtime/registry.d.ts +4 -1
  19. package/dist/runtime/registry.js +0 -3
  20. package/dist/runtime/schema-diff.d.ts +6 -6
  21. package/dist/runtime/schema-diff.js +4 -4
  22. package/dist/sdk/acl.d.ts +18 -11
  23. package/dist/sdk/handlers.d.ts +9 -3
  24. package/dist/sdk/infer.d.ts +17 -0
  25. package/dist/worker.d.ts +2 -1
  26. package/dist/worker.js +16 -10
  27. package/package.json +1 -1
  28. package/src/auth.ts +7 -6
  29. package/src/cli.ts +21 -9
  30. package/src/durable-object.ts +15 -8
  31. package/src/index.ts +6 -2
  32. package/src/pramen.ts +4 -2
  33. package/src/runtime/acl.ts +37 -31
  34. package/src/runtime/db.ts +14 -12
  35. package/src/runtime/dispatch.ts +5 -3
  36. package/src/runtime/driver.ts +11 -7
  37. package/src/runtime/mail.ts +2 -1
  38. package/src/runtime/migrate.ts +2 -1
  39. package/src/runtime/outbox.ts +2 -1
  40. package/src/runtime/protocol.ts +5 -3
  41. package/src/runtime/queue-consumer.ts +4 -2
  42. package/src/runtime/queue.ts +4 -2
  43. package/src/runtime/read-engine.ts +27 -21
  44. package/src/runtime/registry.ts +5 -1
  45. package/src/runtime/schema-diff.ts +14 -14
  46. package/src/sdk/acl.ts +29 -14
  47. package/src/sdk/handlers.ts +10 -3
  48. package/src/sdk/infer.ts +21 -0
  49. package/src/worker.ts +20 -11
package/dist/auth.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { Identity } from "./sdk/acl";
2
+ import type { JsonObject } from "./sdk/infer";
2
3
  /** Verifies a JWT and returns its claims, or null if invalid. Implementations
3
4
  * differ only in how they verify the signature. */
4
5
  export interface VerifyStrategy {
5
- verify(token: string): Promise<Record<string, unknown> | null>;
6
+ verify(token: string): Promise<JsonObject | null>;
6
7
  }
7
8
  /** Optional, opt-in claim validation layered on top of signature + exp/nbf. All
8
9
  * default OFF (unset) so existing tokens keep verifying; a deployment turns these on
@@ -22,7 +23,7 @@ export declare class HmacStrategy implements VerifyStrategy {
22
23
  private readonly secret;
23
24
  private readonly opts;
24
25
  constructor(secret: string, opts?: VerifyOptions);
25
- verify(token: string): Promise<Record<string, unknown> | null>;
26
+ verify(token: string): Promise<JsonObject | null>;
26
27
  }
27
28
  /** RS256 verified against a remote JWKS. Public keys are fetched once and cached
28
29
  * (TTL); a token with an unknown `kid` triggers one forced refetch to pick up key
@@ -35,7 +36,7 @@ export declare class JwksStrategy implements VerifyStrategy {
35
36
  private fetchedAt;
36
37
  private inflight;
37
38
  constructor(url: string, ttlMs?: number, opts?: VerifyOptions);
38
- verify(token: string): Promise<Record<string, unknown> | null>;
39
+ verify(token: string): Promise<JsonObject | null>;
39
40
  private lookup;
40
41
  private keyFor;
41
42
  private refresh;
package/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
20
20
  import { dirname, resolve } from "node:path";
21
21
  import { createTableSql } from "./runtime/ddl";
22
22
  import { schemaHash } from "./runtime/migrate";
23
- import { diffSchemaShape, schemaShape } from "./runtime/schema-diff";
23
+ import { diffSchemaFingerprint, schemaFingerprint } from "./runtime/schema-diff";
24
24
  import { entitiesInPartition, partitionsOf } from "./sdk/schema";
25
25
  /** Mint an HS256 JWT — mirrors what a real auth service would issue, for local
26
26
  * dev/testing (`pramen token`, and the default token for `schema status`). Signs
@@ -90,7 +90,7 @@ Usage: pramen <command>
90
90
  init [dir] scaffold a new project (app.ts + worker.ts + oblaka.ts)
91
91
  schema sql print CREATE TABLE statements for the schema
92
92
  schema hash print the schema hash
93
- schema snapshot save the schema shape to .pramen/schema.json
93
+ schema snapshot save the schema fingerprint to .pramen/schema.json
94
94
  schema diff compare the schema to the snapshot (safe vs unsafe)
95
95
  schema status compare a deployed tenant's schema to the local schema
96
96
  [--tenant t] [--url u] [--token jwt]
@@ -113,20 +113,23 @@ async function schemaCmd(sub) {
113
113
  if (sub === "snapshot") {
114
114
  const { schema } = await loadApp();
115
115
  mkdirSync(dirname(snapshotPath), { recursive: true });
116
- const snap = { hash: schemaHash(schema), shape: schemaShape(schema) };
116
+ const snap = { hash: schemaHash(schema), fingerprint: schemaFingerprint(schema) };
117
117
  writeFileSync(snapshotPath, JSON.stringify(snap, null, 2) + "\n");
118
- console.log(`wrote ${snapshotPath} (${Object.keys(snap.shape).length} tables)`);
118
+ console.log(`wrote ${snapshotPath} (${Object.keys(snap.fingerprint).length} tables)`);
119
119
  return;
120
120
  }
121
121
  if (sub === "diff") {
122
122
  const { schema } = await loadApp();
123
- const next = schemaShape(schema);
123
+ const next = schemaFingerprint(schema);
124
124
  if (!existsSync(snapshotPath)) {
125
125
  console.log("no snapshot — run `pramen schema snapshot` to set a baseline.");
126
126
  return;
127
127
  }
128
- const prev = JSON.parse(readFileSync(snapshotPath, "utf8")).shape;
129
- const changes = diffSchemaShape(prev, next);
128
+ // `fingerprint` was called `shape` before; keep reading an existing snapshot so an
129
+ // upgrade doesn't force a re-baseline.
130
+ const snap = JSON.parse(readFileSync(snapshotPath, "utf8"));
131
+ const prev = snap.fingerprint ?? snap["shape"] ?? {};
132
+ const changes = diffSchemaFingerprint(prev, next);
130
133
  if (changes.length === 0) {
131
134
  console.log("no changes since snapshot.");
132
135
  return;
@@ -181,7 +184,7 @@ async function schemaCmd(sub) {
181
184
  console.log(`live: ${live.hash ?? "(none)"}`);
182
185
  console.log(`current: ${current}`);
183
186
  console.log(upToDate ? "✓ up to date" : "⚠ BEHIND — migrates on the tenant's next boot");
184
- const want = schemaShape(subset);
187
+ const want = schemaFingerprint(subset);
185
188
  for (const table of Object.keys(want)) {
186
189
  const liveCols = new Set(live.tables[table] ?? []);
187
190
  const missing = Object.keys(want[table].columns).filter((col) => !liveCols.has(col));
@@ -202,7 +205,10 @@ async function tokenCmd(args) {
202
205
  fail("token: <sub> required");
203
206
  const roles = pos.slice(1);
204
207
  const tenants = flag("tenant")?.split(",");
205
- console.log(await sign({ sub, roles: roles.length ? roles : ["admin"], ...(tenants ? { tenants } : {}) }));
208
+ const claims = { sub, roles: roles.length ? roles : ["admin"] };
209
+ if (tenants)
210
+ claims.tenants = tenants;
211
+ console.log(await sign(claims));
206
212
  }
207
213
  function initCmd(args) {
208
214
  const dir = resolve(process.cwd(), positionals(args)[0] ?? ".");
@@ -76,6 +76,7 @@ export declare class PramenDOBase extends DurableObject<DoEnv> {
76
76
  private handleSchema;
77
77
  private handleAdminData;
78
78
  private ctxFor;
79
+ private widenedEnv;
79
80
  private get envBag();
80
81
  private filesFor;
81
82
  private identityOf;
@@ -164,9 +164,10 @@ export class PramenDOBase extends DurableObject {
164
164
  return new Response(null, { status: 101, webSocket: client });
165
165
  }
166
166
  const name = new URL(request.url).pathname.replace(/^\/rpc\//, "");
167
- let input;
167
+ // The RPC body is JSON — parse it into the domain type once, here at the boundary.
168
+ let input = null;
168
169
  if (request.method === "POST") {
169
- input = await request.json().catch(() => undefined);
170
+ input = ((await request.json().catch(() => null)) ?? null);
170
171
  }
171
172
  try {
172
173
  const { result, kind, touched, enqueued } = await dispatch(this.app.handlers, this.app.schema, this.driver, this.kv, this.filesFor(this.tenant), this.envBag, this.ctxFor(identity), name, input);
@@ -291,12 +292,12 @@ export class PramenDOBase extends DurableObject {
291
292
  await this.ensureMigrated();
292
293
  switch (msg.type) {
293
294
  case "subscribe":
294
- return this.onSubscribe(ws, msg.id, msg.name, msg.input);
295
+ return this.onSubscribe(ws, msg.id, msg.name, msg.input ?? null);
295
296
  case "unsubscribe":
296
297
  this.setSubs(ws, this.getSubs(ws).filter((s) => s.id !== msg.id));
297
298
  return;
298
299
  case "call":
299
- return this.onCall(ws, msg.id, msg.name, msg.input);
300
+ return this.onCall(ws, msg.id, msg.name, msg.input ?? null);
300
301
  default:
301
302
  return this.send(ws, { type: "error", id: "", error: "unknown message type" });
302
303
  }
@@ -536,8 +537,12 @@ export class PramenDOBase extends DurableObject {
536
537
  }
537
538
  // The DO env (bindings + vars + secrets) handed to handlers as ctx.env. Loosely
538
539
  // typed at the boundary so handlers can read any var/secret without a DoEnv cast.
540
+ widenedEnv = null;
539
541
  get envBag() {
540
- return this.env;
542
+ // `this.env` is fixed for the DO's lifetime, so widen it once. This getter is read
543
+ // inside the per-subscription live-query loop, where a copy per read would allocate
544
+ // one whole binding bag per subscription on every write.
545
+ return (this.widenedEnv ??= { ...this.env });
541
546
  }
542
547
  // One Files facade per DO (a DO serves one tenant). Backed by the R2 binding;
543
548
  // signing uses FILES_SECRET. Handlers mint signed urls; the bytes never enter here.
package/dist/index.d.ts CHANGED
@@ -4,10 +4,10 @@ export { isValidUuid } from "./sdk/uuid";
4
4
  export type { DefaultValue, FieldType, FieldDef, EntityFields, EntityDef, SchemaDef, RelationDef, RelationDefs, BelongsToDef, HasManyDef, ManyToManyDef, OneHasOneDef, OneHasOneInverseDef, OnDelete, } from "./sdk/schema";
5
5
  export { createApp } from "./sdk/app";
6
6
  export { query, mutation, authorizeHandler } from "./sdk/handlers";
7
- export type { Handler, HandlerContext, HandlerKind, HandlerMap, HandlerOpts, HandlerAuth, Tasks, TaskHandler, AppTaskMap, BootstrapContext, BootstrapFn } from "./sdk/handlers";
7
+ export type { EnvBag, Handler, HandlerContext, HandlerKind, HandlerMap, HandlerOpts, HandlerAuth, Tasks, TaskHandler, AppTaskMap, BootstrapContext, BootstrapFn } from "./sdk/handlers";
8
8
  export { $identity, $input, $now, allow, deny, policy, resolve, role, isAllow, isDeny, isResolver, isIdentityMarker, isInputMarker, isNowMarker } from "./sdk/acl";
9
9
  export type { Action, Identity, IdentityMarker, InputMarker, NowMarker, Policy, PolicyRule, PolicyRules, Role, Validator, WhereRule, ConditionalFields, FieldsFn, RelationAclRule, SetValue, ResolverFn, ResolverContext, ResolverDb, } from "./sdk/acl";
10
- export type { Cell, FieldsOf, InferInsert, InferRow, InferUpdate, JsonValue, ProjectedRow, RelationsOf, RelationsResult, WhereClause, WhereInput, WhereOps, } from "./sdk/infer";
10
+ export type { Cell, FieldsOf, InferInsert, InferRow, InferUpdate, JsonValue, JsonObject, SqlValue, CellValue, Row, ProjectedRow, RelationsOf, RelationsResult, WhereClause, WhereInput, WhereOps, } from "./sdk/infer";
11
11
  export { Kv, denySession, allowSession, isSessionDenied } from "./runtime/kv";
12
12
  export type { FileRef, Files, SignUploadOpts, SignDownloadOpts, HeadResult } from "./sdk/files";
13
13
  export { R2Adapter, MemoryAdapter, createFiles, handleFileRequest } from "./runtime/storage";
@@ -20,4 +20,4 @@ export { routeQueue, dispatchQueueBatch } from "./runtime/queue-consumer";
20
20
  export type { QueueContext, QueueHandler, QueueMessage, QueueBatch, AppQueueMap } from "./runtime/queue-consumer";
21
21
  export { PramenError, BadRequest, Unauthorized, Forbidden } from "./runtime/errors";
22
22
  export { sqliteDialect, postgresDialect, DoSqliteDriver, D1Driver } from "./runtime/driver";
23
- export type { Driver, Dialect, Row } from "./runtime/driver";
23
+ export type { Driver, Dialect, DriverRow } from "./runtime/driver";
package/dist/pramen.d.ts CHANGED
@@ -4,13 +4,15 @@ import { type SchemaDef } from "./sdk/schema";
4
4
  import type { AppTaskMap, HandlerMap, BootstrapFn } from "./sdk/handlers";
5
5
  import type { AppQueueMap, QueueBatch } from "./runtime/queue-consumer";
6
6
  import type { Role } from "./sdk/acl";
7
+ import type { EnvBag } from "./sdk/handlers";
8
+ import type { JsonValue } from "./sdk/infer";
7
9
  /** Injected into a public route's handler — forward a privileged mutation into the
8
10
  * tenant's DO without the handler importing any deploy-side code (so app.ts stays
9
11
  * authoring-only). The synthetic identity defaults to the admin role. */
10
12
  export interface RouteContext {
11
13
  callPrivileged(opts: {
12
14
  name: string;
13
- input?: unknown;
15
+ input?: JsonValue;
14
16
  tenant?: string;
15
17
  roles?: string[];
16
18
  }): Promise<Response>;
@@ -25,7 +27,7 @@ export interface PublicRoute {
25
27
  method: string;
26
28
  /** Exact pathname to match (e.g. "/stripe/webhook"). */
27
29
  path: string;
28
- handler: (request: Request, env: Readonly<Record<string, unknown>>, ctx: RouteContext) => Response | Promise<Response>;
30
+ handler: (request: Request, env: EnvBag, ctx: RouteContext) => Response | Promise<Response>;
29
31
  }
30
32
  /** The user-facing app: a schema, the handler map, ACL roles, and optional public
31
33
  * (pre-auth) routes. `example/app.ts` exports this shape. */
@@ -1,6 +1,7 @@
1
- import { type Action, type FieldsFn, type Identity, type PolicyRule, type ResolverDb, type Role, type Validator } from "../sdk/acl";
1
+ import { type Action, type FieldsFn, type Identity, type PolicyRule, type ResolverDb, type Role, type Validator, type WhereRule } from "../sdk/acl";
2
2
  import { type SqlExpr } from "./read-engine";
3
3
  import { PramenError } from "./errors";
4
+ import type { Row } from "../sdk/infer";
4
5
  import type { SchemaDef } from "../sdk/schema";
5
6
  export declare class AclDenied extends PramenError {
6
7
  readonly entity: string;
@@ -78,15 +79,15 @@ export declare const MAX_REL_DEPTH = 5;
78
79
  * `allowRelations` is false for single-table contexts (cell-level `when`, which is
79
80
  * evaluated in memory and cannot do a SQL round-trip): a relation key then raises a
80
81
  * clear authoring error instead of emitting a `sub` node that throws at read time. */
81
- export declare function compileScopedWhere(rule: Record<string, unknown>, entity: string, ctx: AclContext, depth?: number, allowRelations?: boolean): SqlExpr;
82
+ export declare function compileScopedWhere(rule: WhereRule, entity: string, ctx: AclContext, depth?: number, allowRelations?: boolean): SqlExpr;
82
83
  export declare function resolveScope(ctx: AclContext, entity: string, action: Action, depth?: number): Scope;
83
84
  /** Effective visible fields for one row = base ∪ matching-conditional ∪ fn-output.
84
85
  * Returns null (all fields) when the base is null or a resolver grants everything. */
85
- export declare function effectiveFields(scope: Scope, row: Record<string, unknown>, identity: Identity | null): string[] | null;
86
+ export declare function effectiveFields(scope: Scope, row: Row, identity: Identity | null): string[] | null;
86
87
  /** Forced values + validators for a write, gathered from matched write policies.
87
88
  * `set` values are resolved against the identity; later policies override earlier. */
88
89
  export interface WriteRules {
89
- set: Record<string, unknown>;
90
+ set: Row;
90
91
  validators: Validator[];
91
92
  }
92
93
  export declare function resolveWriteRules(ctx: AclContext, entity: string, action: Action): WriteRules;
@@ -94,4 +95,4 @@ export declare function resolveWriteRules(ctx: AclContext, entity: string, actio
94
95
  * target's own read scope OR a parent read policy's relation rule with directAccess. */
95
96
  export declare function resolveRelationScope(ctx: AclContext, parentEntity: string, relName: string, target: string): Scope;
96
97
  /** Project a row to the permitted fields. null = all. */
97
- export declare function projectRow(row: Record<string, unknown>, fields: string[] | null): Record<string, unknown>;
98
+ export declare function projectRow(row: Row, fields: string[] | null): Row;
@@ -85,10 +85,6 @@ function getPath(obj, path) {
85
85
  return path.split(".").reduce((acc, seg) => (acc == null ? undefined : acc[seg]), obj ?? undefined);
86
86
  }
87
87
  const UNRESOLVED = Symbol("unresolved");
88
- // Resolve a value that may be an $identity marker (against the caller), an
89
- // $input marker (against the request input — a capability/by-key grant), or a
90
- // $now marker (the evaluation instant). An unresolvable marker yields UNRESOLVED,
91
- // which makes its rule match nothing. $now always resolves.
92
88
  function resolveValue(v, identity, input) {
93
89
  if (isNowMarker(v))
94
90
  return new Date().toISOString();
@@ -180,7 +176,7 @@ function pkOf(schema, entity) {
180
176
  * are skipped: they're re-scoped against THEIR own target's read scope downstream.
181
177
  * Mirrors Db.assertReadableWhere's recursion for the top-level user `where`. */
182
178
  function assertReadableRelationWhere(where, target, fields, ctx) {
183
- const targetRels = (ctx.schema?.[target]?.relations ?? {});
179
+ const targetRels = ctx.schema?.[target]?.relations ?? {};
184
180
  for (const [k, v] of Object.entries(where)) {
185
181
  if (k === "AND" || k === "OR") {
186
182
  for (const g of v)
@@ -1,9 +1,10 @@
1
1
  import { type AclContext } from "./acl";
2
+ import type { CellValue, Row as SharedRow } from "../sdk/infer";
2
3
  import { type AggFn } from "./read-engine";
3
4
  import type { Driver } from "./driver";
4
5
  import { type EntityFields, type SchemaDef } from "../sdk/schema";
5
6
  import type { Cell, FieldsOf, InferInsert, InferRow, InferUpdate, RelationsOf, RelationsResult, WhereClause } from "../sdk/infer";
6
- type Row = Record<string, unknown>;
7
+ type Row = SharedRow;
7
8
  type Id = string | number | bigint;
8
9
  type OrderSpec<S extends SchemaDef, T extends keyof S> = {
9
10
  column: keyof FieldsOf<S[T]> & string;
@@ -215,7 +216,7 @@ export declare class Db<S extends SchemaDef = SchemaDef> {
215
216
  /** Delete a row by id within scope. Returns whether a row was deleted. */
216
217
  delete<T extends keyof S & string>(table: T, id: Id): Promise<boolean>;
217
218
  /** Escape hatch — raw SQL, NOT ACL-checked. For system/internal use only. */
218
- exec(sql: string, ...params: unknown[]): Promise<Row[]>;
219
+ exec(sql: string, ...params: CellValue[]): Promise<Row[]>;
219
220
  private returningClause;
220
221
  private scopeClause;
221
222
  }
@@ -5,6 +5,8 @@ import type { Kv } from "./kv";
5
5
  import type { Files } from "../sdk/files";
6
6
  import type { SchemaDef } from "../sdk/schema";
7
7
  import { type AppTaskMap, type HandlerContext, type HandlerKind, type HandlerMap, type Tasks } from "../sdk/handlers";
8
+ import type { EnvBag } from "../sdk/handlers";
9
+ import type { JsonValue } from "../sdk/infer";
8
10
  export interface DispatchResult {
9
11
  readonly result: unknown;
10
12
  readonly kind: HandlerKind;
@@ -17,4 +19,4 @@ export interface DispatchResult {
17
19
  export declare function tasksFacade(driver: Driver, onEnqueue?: () => void): Tasks;
18
20
  /** Bind `app.tasks` (which take a ctx) into the ctx-free `TaskMap` the drainer calls. */
19
21
  export declare function bindTasks(appTasks: AppTaskMap | undefined, ctx: HandlerContext): TaskMap;
20
- export declare function dispatch(handlers: HandlerMap, schema: SchemaDef, driver: Driver, kv: Kv, files: Files, env: Readonly<Record<string, unknown>>, acl: AclContext, name: string, input: unknown): Promise<DispatchResult>;
22
+ export declare function dispatch(handlers: HandlerMap, schema: SchemaDef, driver: Driver, kv: Kv, files: Files, env: EnvBag, acl: AclContext, name: string, input: JsonValue): Promise<DispatchResult>;
@@ -1,4 +1,7 @@
1
- export type Row = Record<string, unknown>;
1
+ /** A raw row exactly as the substrate returns it, before pramen's object↔JSON codec.
2
+ * Distinct from the decoded `Row` handlers see at the `Db` chokepoint. */
3
+ export type DriverRow = Record<string, SqlValue>;
4
+ import type { CellValue, SqlValue } from "../sdk/infer";
2
5
  export interface Dialect {
3
6
  /** Render an identifier (table/column), quoting as the backend requires. */
4
7
  id(name: string): string;
@@ -7,7 +10,7 @@ export interface Dialect {
7
10
  /** Whether INSERT/UPDATE/DELETE ... RETURNING is supported (SQLite/Postgres yes; MySQL no). */
8
11
  readonly returning: boolean;
9
12
  /** Coerce a JS value for binding (e.g. boolean → 0/1 on SQLite). */
10
- encode(v: unknown): unknown;
13
+ encode(v: CellValue): CellValue;
11
14
  }
12
15
  /** Render an identifier as a standard double-quoted name (`"order"`), guarding its
13
16
  * shape first. SQLite (DO SQLite + D1) and Postgres all accept double-quoted
@@ -27,7 +30,7 @@ export interface Driver {
27
30
  readonly dialect: Dialect;
28
31
  /** Run a parameterized statement and return the result rows (empty for writes
29
32
  * without RETURNING). Params are already dialect-encoded by the caller. */
30
- exec(sql: string, params: unknown[]): Promise<Row[]>;
33
+ exec(sql: string, params: CellValue[]): Promise<DriverRow[]>;
31
34
  /** Run `fn` inside a transaction: commit on resolve, roll back on throw. */
32
35
  transaction<T>(fn: () => Promise<T>): Promise<T>;
33
36
  /** Run a fixed sequence of write statements ATOMICALLY with FK checks deferred to the
@@ -46,7 +49,7 @@ export declare class DoSqliteDriver implements Driver {
46
49
  private readonly storage;
47
50
  readonly dialect: Dialect;
48
51
  constructor(storage: DurableObjectStorage);
49
- exec(sql: string, params: unknown[]): Promise<Row[]>;
52
+ exec(sql: string, params: CellValue[]): Promise<DriverRow[]>;
50
53
  transaction<T>(fn: () => Promise<T>): Promise<T>;
51
54
  }
52
55
  /** How a D1Driver's session is anchored (passed to `db.withSession`):
@@ -79,7 +82,7 @@ export declare class D1Driver implements Driver {
79
82
  constructor(db: D1Database, opts?: {
80
83
  start?: D1SessionStart;
81
84
  });
82
- exec(sql: string, params: unknown[]): Promise<Row[]>;
85
+ exec(sql: string, params: CellValue[]): Promise<DriverRow[]>;
83
86
  /** The session's latest bookmark (null before any query). Threaded back to the client
84
87
  * via the `x-pramen-d1-bookmark` response header so a subsequent request can anchor a
85
88
  * fresh session at it and read its own writes. */
@@ -1,4 +1,5 @@
1
1
  import type { Kv } from "./kv";
2
+ import type { EnvBag } from "../sdk/handlers";
2
3
  export interface MailAddress {
3
4
  email: string;
4
5
  name?: string;
@@ -77,4 +78,4 @@ export declare class UnconfiguredMailAdapter implements MailAdapter {
77
78
  * a synthetic dev sender — an EXPLICIT dev opt-in, never the production default.
78
79
  * - else → fail closed: a `send` throws (so a missing-MAIL_FROM prod doesn't silently
79
80
  * stash security emails in KV). */
80
- export declare function createMail(env: Readonly<Record<string, unknown>>, kv?: Kv): Mail;
81
+ export declare function createMail(env: EnvBag, kv?: Kv): Mail;
@@ -1,8 +1,9 @@
1
+ import type { JsonValue } from "../sdk/infer";
1
2
  export interface SubscribeMsg {
2
3
  type: "subscribe";
3
4
  id: string;
4
5
  name: string;
5
- input?: unknown;
6
+ input?: JsonValue;
6
7
  }
7
8
  export interface UnsubscribeMsg {
8
9
  type: "unsubscribe";
@@ -12,7 +13,7 @@ export interface CallMsg {
12
13
  type: "call";
13
14
  id: string;
14
15
  name: string;
15
- input?: unknown;
16
+ input?: JsonValue;
16
17
  }
17
18
  export type ClientMsg = SubscribeMsg | UnsubscribeMsg | CallMsg;
18
19
  export type ServerMsg = {
@@ -32,7 +33,7 @@ export type ServerMsg = {
32
33
  export interface Subscription {
33
34
  id: string;
34
35
  name: string;
35
- input: unknown;
36
+ input: JsonValue;
36
37
  /** Tables the query read — the coarse prefilter for which writes might matter. */
37
38
  tables: string[];
38
39
  /** Digest of the last result pushed — used to suppress no-op (row-level) pushes. */
@@ -1,6 +1,8 @@
1
1
  import type { Mail } from "./mail";
2
2
  import type { Queue } from "./queue";
3
3
  import type { Kv } from "./kv";
4
+ import type { EnvBag } from "../sdk/handlers";
5
+ import type { JsonValue } from "../sdk/infer";
4
6
  /** One received message (the Cloudflare Queues `Message` shape). */
5
7
  export interface QueueMessage<Body = unknown> {
6
8
  readonly id: string;
@@ -30,7 +32,7 @@ export interface QueueBatch<Body = unknown> {
30
32
  * tenant data via `ctx.callPrivileged`. */
31
33
  export interface QueueContext {
32
34
  /** The Worker environment (bindings + vars + secrets). */
33
- readonly env: Readonly<Record<string, unknown>>;
35
+ readonly env: EnvBag;
34
36
  /** Project KV (cross-tenant). */
35
37
  readonly kv: Kv;
36
38
  /** Send email (the notification path). */
@@ -41,7 +43,7 @@ export interface QueueContext {
41
43
  * The message body should carry the `tenant`. */
42
44
  callPrivileged(opts: {
43
45
  name: string;
44
- input?: unknown;
46
+ input?: JsonValue;
45
47
  tenant?: string;
46
48
  roles?: string[];
47
49
  partition?: string;
@@ -1,3 +1,4 @@
1
+ import type { EnvBag } from "../sdk/handlers";
1
2
  /** Cloudflare Queues content type for a sent message. Omitted ⇒ the platform default
2
3
  * (v8 structured clone). Use "json" for cross-runtime / external consumers. */
3
4
  export type QueueContentType = "text" | "bytes" | "json" | "v8";
@@ -64,9 +65,9 @@ export declare class MemoryQueueAdapter implements QueueAdapter {
64
65
  /** Discover the Cloudflare Queues producer bindings in an environment: any value that
65
66
  * exposes BOTH `send` and `sendBatch` functions (which excludes the email `send`-only
66
67
  * binding, KV, R2, D1, the DO namespace, …). Returns name → binding. */
67
- export declare function discoverQueueBindings(env: Readonly<Record<string, unknown>>): Record<string, QueueProducerBinding>;
68
+ export declare function discoverQueueBindings(env: EnvBag): Record<string, QueueProducerBinding>;
68
69
  /** Build `ctx.queue` from the environment: a Cloudflare adapter over the discovered
69
70
  * producer bindings. Sending to an undeclared queue fails closed (the adapter throws).
70
71
  * There is no silent capture fallback — declare the `Queue` binding and it exists in
71
72
  * dev (lopata) and miniflare too. */
72
- export declare function createQueue(env: Readonly<Record<string, unknown>>): Queue;
73
+ export declare function createQueue(env: EnvBag): Queue;
@@ -1,3 +1,5 @@
1
+ import type { WhereRule } from "../sdk/acl";
2
+ import type { CellValue, Row } from "../sdk/infer";
1
3
  import type { Dialect } from "./driver";
2
4
  export type CmpOp = "=" | "!=" | ">" | ">=" | "<" | "<=" | "LIKE";
3
5
  /** Substring match mode for the structured string operators (auto-escaping, so the
@@ -11,11 +13,11 @@ export type SqlExpr = {
11
13
  t: "cmp";
12
14
  op: CmpOp;
13
15
  col: string;
14
- value: unknown;
16
+ value: CellValue;
15
17
  } | {
16
18
  t: "in";
17
19
  col: string;
18
- values: unknown[];
20
+ values: CellValue[];
19
21
  negate: boolean;
20
22
  } | {
21
23
  t: "null";
@@ -45,28 +47,28 @@ export type SqlExpr = {
45
47
  };
46
48
  export declare const TRUE: SqlExpr;
47
49
  export declare const FALSE: SqlExpr;
48
- export declare const cmp: (op: CmpOp, col: string, value: unknown) => SqlExpr;
50
+ export declare const cmp: (op: CmpOp, col: string, value: CellValue) => SqlExpr;
49
51
  export declare const isNull: (col: string, negate?: boolean) => SqlExpr;
50
- export declare const inList: (col: string, values: unknown[], negate?: boolean) => SqlExpr;
52
+ export declare const inList: (col: string, values: CellValue[], negate?: boolean) => SqlExpr;
51
53
  export declare const strMatch: (col: string, needle: string, mode: StrMode) => SqlExpr;
52
54
  export declare const and: (...parts: SqlExpr[]) => SqlExpr;
53
55
  export declare const or: (...parts: SqlExpr[]) => SqlExpr;
54
56
  export declare const not: (expr: SqlExpr) => SqlExpr;
55
57
  /** Equality (null -> IS NULL). Used by ACL scope building and relation loads. */
56
- export declare const eq: (col: string, value: unknown) => SqlExpr;
58
+ export declare const eq: (col: string, value: CellValue) => SqlExpr;
57
59
  /** Compile a structured user predicate into a SqlExpr.
58
60
  * Shapes: { col: value } (eq) | { col: { gt, lt, in, like, isNull, … } } | { AND: [...] } | { OR: [...] } */
59
- export declare function compileWhere(input: Record<string, unknown>): SqlExpr;
61
+ export declare function compileWhere(input: WhereRule): SqlExpr;
60
62
  export interface CompiledSql {
61
63
  readonly sql: string;
62
- readonly params: unknown[];
64
+ readonly params: CellValue[];
63
65
  }
64
- export declare function compileExpr(expr: SqlExpr, dialect: Dialect, params?: unknown[]): CompiledSql;
66
+ export declare function compileExpr(expr: SqlExpr, dialect: Dialect, params?: CellValue[]): CompiledSql;
65
67
  /** Evaluate a compiled predicate against an in-memory row. Mirrors compileExpr's
66
68
  * semantics (bound-boolean coercion, NULL compares false, empty-IN, LIKE) so the
67
69
  * declarative cell-ACL `when` path can decide per-row field visibility without a
68
70
  * round-trip to SQLite. */
69
- export declare function evalExpr(expr: SqlExpr, row: Record<string, unknown>): boolean;
71
+ export declare function evalExpr(expr: SqlExpr, row: Row): boolean;
70
72
  export interface OrderBy {
71
73
  column: string;
72
74
  dir?: "asc" | "desc";
@@ -44,7 +44,10 @@ export function compileWhere(input) {
44
44
  function columnPredicate(col, v) {
45
45
  if (v !== null && typeof v === "object" && !Array.isArray(v)) {
46
46
  const ops = [];
47
- for (const [op, val] of Object.entries(v)) {
47
+ for (const [op, operand] of Object.entries(v)) {
48
+ // Markers are resolved upstream (runtime/acl.ts), so an operator's operand is a
49
+ // literal cell value by this point.
50
+ const val = operand;
48
51
  switch (op) {
49
52
  case "eq":
50
53
  ops.push(eq(col, val));
@@ -25,4 +25,7 @@ export declare function parseRegistryKey(key: string): DoRef | null;
25
25
  /** Enumerate every registered `(tenant, partition)` pair from the registry KV.
26
26
  * Paginates over the full listing (cursor / list_complete) — never truncates at the
27
27
  * 1000-key page limit. */
28
- export declare function listDOs(kv: KVNamespace): Promise<DoRef[]>;
28
+ /** The slice of KV that DO enumeration needs — narrower than the whole namespace, so
29
+ * callers (and test doubles) only have to provide `list`. */
30
+ export type KvLister = Pick<KVNamespace, "list">;
31
+ export declare function listDOs(kv: KvLister): Promise<DoRef[]>;
@@ -65,9 +65,6 @@ export function parseRegistryKey(key) {
65
65
  return { tenant: rest, partition: DEFAULT_PARTITION };
66
66
  return { tenant: rest.slice(0, sep), partition: rest.slice(sep + 1) };
67
67
  }
68
- /** Enumerate every registered `(tenant, partition)` pair from the registry KV.
69
- * Paginates over the full listing (cursor / list_complete) — never truncates at the
70
- * 1000-key page limit. */
71
68
  export async function listDOs(kv) {
72
69
  const out = [];
73
70
  let cursor;
@@ -1,6 +1,6 @@
1
1
  import type { SchemaDef } from "../sdk/schema";
2
2
  /** The comparable fingerprint of a single column: type + migration-relevant modifiers. */
3
- export interface ColumnShape {
3
+ export interface ColumnFingerprint {
4
4
  type: string;
5
5
  notNull?: boolean;
6
6
  unique?: boolean;
@@ -11,13 +11,13 @@ export interface ColumnShape {
11
11
  default?: string;
12
12
  }
13
13
  /** The comparable fingerprint of a table: its partition + each column's shape. */
14
- export interface TableShape {
14
+ export interface TableFingerprint {
15
15
  partition: string;
16
- columns: Record<string, ColumnShape>;
16
+ columns: Record<string, ColumnFingerprint>;
17
17
  }
18
18
  /** table -> table shape. The comparable surface of a schema. */
19
- export type SchemaShape = Record<string, TableShape>;
20
- export declare function schemaShape(schema: SchemaDef): SchemaShape;
19
+ export type SchemaFingerprint = Record<string, TableFingerprint>;
20
+ export declare function schemaFingerprint(schema: SchemaDef): SchemaFingerprint;
21
21
  export interface SchemaChange {
22
22
  kind: "add-table" | "drop-table" | "add-column" | "drop-column" | "change-type" | "change-column" | "move-partition";
23
23
  table: string;
@@ -34,4 +34,4 @@ export interface SchemaChange {
34
34
  * cross-DO data migration). Reported for honesty. */
35
35
  appliesOnBoot: boolean;
36
36
  }
37
- export declare function diffSchemaShape(prev: SchemaShape, next: SchemaShape): SchemaChange[];
37
+ export declare function diffSchemaFingerprint(prev: SchemaFingerprint, next: SchemaFingerprint): SchemaChange[];
@@ -17,7 +17,7 @@
17
17
  // still CANNOT be enacted on boot (a partition is a separate Durable Object — it needs a
18
18
  // manual cross-DO data migration), so it stays `appliesOnBoot: false`.
19
19
  import { partitionOf } from "../sdk/schema";
20
- function columnShape(f) {
20
+ function columnFingerprint(f) {
21
21
  const c = { type: f.type };
22
22
  if (f.notNull)
23
23
  c.notNull = true;
@@ -35,12 +35,12 @@ function columnShape(f) {
35
35
  c.default = JSON.stringify(f.default);
36
36
  return c;
37
37
  }
38
- export function schemaShape(schema) {
38
+ export function schemaFingerprint(schema) {
39
39
  const out = {};
40
40
  for (const [table, def] of Object.entries(schema)) {
41
41
  const columns = {};
42
42
  for (const [col, f] of Object.entries(def.fields))
43
- columns[col] = columnShape(f);
43
+ columns[col] = columnFingerprint(f);
44
44
  out[table] = { partition: partitionOf(schema, table), columns };
45
45
  }
46
46
  return out;
@@ -64,7 +64,7 @@ function modifierDiff(prev, next) {
64
64
  function fmt(v) {
65
65
  return v === undefined ? "—" : String(v);
66
66
  }
67
- export function diffSchemaShape(prev, next) {
67
+ export function diffSchemaFingerprint(prev, next) {
68
68
  const changes = [];
69
69
  for (const table of Object.keys(next)) {
70
70
  const pt = prev[table];