@fuzdev/fuz_app 0.108.0 → 0.109.0

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 (45) hide show
  1. package/README.md +4 -0
  2. package/dist/actions/CLAUDE.md +1 -1
  3. package/dist/auth/CLAUDE.md +1 -1
  4. package/dist/auth/audit_emitter.d.ts +9 -5
  5. package/dist/auth/audit_emitter.d.ts.map +1 -1
  6. package/dist/auth/audit_log_schema.d.ts +7 -1
  7. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  8. package/dist/auth/audit_log_schema.js +16 -1
  9. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  10. package/dist/auth/bootstrap_routes.js +11 -1
  11. package/dist/auth/migrations.d.ts +7 -0
  12. package/dist/auth/migrations.d.ts.map +1 -1
  13. package/dist/auth/migrations.js +25 -8
  14. package/dist/db/CLAUDE.md +9 -5
  15. package/dist/db/cell_audit_queries.d.ts +4 -2
  16. package/dist/db/cell_audit_queries.d.ts.map +1 -1
  17. package/dist/db/cell_audit_queries.js +4 -2
  18. package/dist/db/pg_error.d.ts +12 -0
  19. package/dist/db/pg_error.d.ts.map +1 -1
  20. package/dist/db/pg_error.js +15 -1
  21. package/dist/http/CLAUDE.md +78 -18
  22. package/dist/http/db_routes.d.ts +96 -3
  23. package/dist/http/db_routes.d.ts.map +1 -1
  24. package/dist/http/db_routes.js +263 -46
  25. package/dist/http/error_schemas.d.ts +12 -1
  26. package/dist/http/error_schemas.d.ts.map +1 -1
  27. package/dist/http/error_schemas.js +12 -1
  28. package/dist/http/pending_effects.d.ts +5 -5
  29. package/dist/http/pending_effects.d.ts.map +1 -1
  30. package/dist/http/pending_effects.js +1 -1
  31. package/dist/testing/CLAUDE.md +21 -0
  32. package/dist/testing/audit_completeness.d.ts.map +1 -1
  33. package/dist/testing/audit_completeness.js +5 -1
  34. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
  35. package/dist/testing/cross_backend/cell_relations.js +53 -1
  36. package/dist/testing/cross_backend/query_shape.d.ts +10 -0
  37. package/dist/testing/cross_backend/query_shape.d.ts.map +1 -0
  38. package/dist/testing/cross_backend/query_shape.js +128 -0
  39. package/dist/ui/CLAUDE.md +9 -4
  40. package/dist/ui/table_state.svelte.d.ts +18 -5
  41. package/dist/ui/table_state.svelte.d.ts.map +1 -1
  42. package/dist/ui/table_state.svelte.js +22 -7
  43. package/dist/ui/ui_format.d.ts.map +1 -1
  44. package/dist/ui/ui_format.js +4 -0
  45. package/package.json +1 -1
package/README.md CHANGED
@@ -45,6 +45,10 @@ Deno.serve({port: PORT, hostname: HOST}, app.fetch);
45
45
 
46
46
  See [CLAUDE.md](CLAUDE.md) for more usage patterns and the AI-generated docs.
47
47
 
48
+ ## Contributing
49
+
50
+ [fuz.dev/contributing](https://www.fuz.dev/contributing)
51
+
48
52
  ## License
49
53
 
50
54
  [MIT](LICENSE)
@@ -217,7 +217,7 @@ ones. Matches the Rust spine, which validates handler-side.
217
217
 
218
218
  Shim responsibilities (per-request):
219
219
 
220
- 1. Parse envelope (POST body / GET query string); parse errors → `parse_error` 400.
220
+ 1. Parse envelope (POST body / GET query string); parse errors → `parse_error` 400. GET reads `method` / `id` / `params` via Hono's `c.req.query()` — first occurrence wins on a duplicated key, unknown keys are ignored, and an empty value counts as missing. This is the cross-impl contract (the Rust GET dispatch mirrors it via `fuz_http`'s `query_first`, including the id numeric-vs-string normalization), pinned by the `query_shape` cross suite.
221
221
  2. Lookup method in the `compile_action_registry`-built map; unknown → `method_not_found`.
222
222
  3. GET read restriction — GET rejected for `side_effects: true` actions.
223
223
  4. Build `PerformActionInput` from `c.var` + `get_client_ip` + `c.req.raw.signal`. Test-preset escape hatch reads `TEST_CONTEXT_PRESET_KEY` + `REQUEST_CONTEXT_KEY`.
@@ -379,7 +379,7 @@ token_create role_grant_offer_supersede
379
379
  token_revoke invite_create
380
380
  token_revoke_all invite_delete
381
381
  account_delete app_settings_update
382
- account_purge
382
+ account_purge db_admin_row_delete
383
383
  account_undelete
384
384
  actor_delete
385
385
  actor_purge
@@ -49,12 +49,16 @@ import { type AuditLogConfig, type AuditLogEvent, type AuditLogInput } from './a
49
49
  * `pending_effects` queue. The bound emitter carries its own `log`
50
50
  * reference inside the closure, so per-call contexts don't need one.
51
51
  *
52
- * Audit emits are eager-only by design: the bound emitter fires the
52
+ * Audit emits are eager by default: the bound emitter fires the
53
53
  * pool write immediately and pushes the in-flight `Promise<void>` here.
54
- * They never go through `emit_after_commit` — pool-routed audit writes
55
- * are already rollback-resilient because they run outside the request
56
- * transaction, so the post-commit timing the deferred queue provides
57
- * would only delay forensic visibility without any safety benefit.
54
+ * Attempt/failure audits never go through `emit_after_commit` —
55
+ * pool-routed writes are already rollback-resilient because they run
56
+ * outside the request transaction, so deferring them would only delay
57
+ * forensic visibility without any safety benefit. The exception is a
58
+ * **success-only** event paired with a state mutation (the
59
+ * `db_admin_row_delete` emit in `http/db_routes.ts`): there the caller
60
+ * wraps the emit in `emit_after_commit` so the trail can't claim a
61
+ * mutation whose transaction failed at COMMIT.
58
62
  *
59
63
  * Both `RouteContext` and `ActionContext` structurally satisfy this
60
64
  * shape (they each carry `pending_effects`), so handlers pass `route`
@@ -1 +1 @@
1
- {"version":3,"file":"audit_emitter.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_emitter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,OAAO,EAEN,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,mBAAmB;IACnC,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAA0B,SAAQ,mBAAmB;IACrE,0FAA0F;IAC1F,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC5B;;;;;;;;;;;;;;;;OAgBG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,mBAAmB,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAChF;;;;;;;OAOG;IACH,sBAAsB,CAAC,CAAC,SAAS,MAAM,EACtC,GAAG,EAAE,yBAAyB,EAC9B,IAAI,EAAE,mBAAmB,EACzB,KAAK,EAAE;QACN,UAAU,EAAE,CAAC,CAAC;QACd,iBAAiB,EAAE,IAAI,GAAG,IAAI,CAAC;QAC/B,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;QAC7B,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QACvC,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;KAChC,GACC,IAAI,CAAC;IACR;;;;;;;;;;OAUG;IACH,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE;;;;;;;OAOG;IACH,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IACnC;;;;;;;;;;;;;OAaG;IACH,YAAY,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAAG,IAAI,CAAC;IAC7D,+EAA+E;IAC/E,cAAc,IAAI,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,SAAS,MAAM,EAC1C,GAAG,EAAE,mBAAmB,EACxB,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,KACnB,IAAI,CAAC;AAEV;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,WAAW,CAAC;AAEhE,0CAA0C;AAC1C,MAAM,WAAW,yBAAyB;IACzC,oDAAoD;IACpD,EAAE,EAAE,EAAE,CAAC;IACP,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACzD;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,cAAc,CAAC;IAClC;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS,yBAAyB,KAAG,YAuFzE,CAAC"}
1
+ {"version":3,"file":"audit_emitter.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_emitter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,OAAO,EAEN,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,MAAM,uBAAuB,CAAC;AAE/B;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,mBAAmB;IACnC,eAAe,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAA0B,SAAQ,mBAAmB;IACrE,0FAA0F;IAC1F,SAAS,EAAE,MAAM,CAAC;CAClB;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC5B;;;;;;;;;;;;;;;;OAgBG;IACH,IAAI,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,mBAAmB,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IAChF;;;;;;;OAOG;IACH,sBAAsB,CAAC,CAAC,SAAS,MAAM,EACtC,GAAG,EAAE,yBAAyB,EAC9B,IAAI,EAAE,mBAAmB,EACzB,KAAK,EAAE;QACN,UAAU,EAAE,CAAC,CAAC;QACd,iBAAiB,EAAE,IAAI,GAAG,IAAI,CAAC;QAC/B,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;QAC7B,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QACvC,OAAO,CAAC,EAAE,SAAS,GAAG,SAAS,CAAC;KAChC,GACC,IAAI,CAAC;IACR;;;;;;;;;;OAUG;IACH,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE;;;;;;;OAOG;IACH,MAAM,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;IACnC;;;;;;;;;;;;;OAaG;IACH,YAAY,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAAG,IAAI,CAAC;IAC7D,+EAA+E;IAC/E,cAAc,IAAI,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,SAAS,MAAM,EAC1C,GAAG,EAAE,mBAAmB,EACxB,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,KACnB,IAAI,CAAC;AAEV;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,WAAW,KAAK,WAAW,CAAC;AAEhE,0CAA0C;AAC1C,MAAM,WAAW,yBAAyB;IACzC,oDAAoD;IACpD,EAAE,EAAE,EAAE,CAAC;IACP,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC;IACzD;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,cAAc,CAAC;IAClC;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAI,SAAS,yBAAyB,KAAG,YAuFzE,CAAC"}
@@ -16,7 +16,7 @@ import { Uuid } from '@fuzdev/fuz_util/id.ts';
16
16
  * Not a security boundary — in-process code has many other paths to subvert
17
17
  * audit logging.
18
18
  */
19
- export declare const AUDIT_EVENT_TYPES: readonly ["login", "logout", "bootstrap", "signup", "password_change", "session_revoke", "session_revoke_all", "token_create", "token_revoke", "token_revoke_all", "role_grant_create", "role_grant_revoke", "role_grant_offer_create", "role_grant_offer_accept", "role_grant_offer_decline", "role_grant_offer_retract", "role_grant_offer_expire", "role_grant_offer_supersede", "invite_create", "invite_delete", "account_delete", "account_purge", "account_undelete", "actor_delete", "actor_purge", "actor_undelete", "app_settings_update"];
19
+ export declare const AUDIT_EVENT_TYPES: readonly ["login", "logout", "bootstrap", "signup", "password_change", "session_revoke", "session_revoke_all", "token_create", "token_revoke", "token_revoke_all", "role_grant_create", "role_grant_revoke", "role_grant_offer_create", "role_grant_offer_accept", "role_grant_offer_decline", "role_grant_offer_retract", "role_grant_offer_expire", "role_grant_offer_supersede", "invite_create", "invite_delete", "account_delete", "account_purge", "account_undelete", "actor_delete", "actor_purge", "actor_undelete", "app_settings_update", "db_admin_row_delete"];
20
20
  /** Zod schema for audit event types. */
21
21
  export declare const AuditEventType: z.ZodEnum<{
22
22
  bootstrap: "bootstrap";
@@ -46,6 +46,7 @@ export declare const AuditEventType: z.ZodEnum<{
46
46
  actor_purge: "actor_purge";
47
47
  actor_undelete: "actor_undelete";
48
48
  app_settings_update: "app_settings_update";
49
+ db_admin_row_delete: "db_admin_row_delete";
49
50
  }>;
50
51
  export type AuditEventType = z.infer<typeof AuditEventType>;
51
52
  /**
@@ -230,6 +231,11 @@ export declare const audit_metadata_schemas: Readonly<{
230
231
  old_value: z.ZodUnknown;
231
232
  new_value: z.ZodUnknown;
232
233
  }, z.core.$loose>;
234
+ db_admin_row_delete: z.ZodObject<{
235
+ table: z.ZodString;
236
+ pk_column: z.ZodString;
237
+ id: z.ZodString;
238
+ }, z.core.$loose>;
233
239
  }>;
234
240
  /** Mapped type of metadata shapes per event type, derived from Zod schemas. */
235
241
  export type AuditMetadataMap = {
@@ -1 +1 @@
1
- {"version":3,"file":"audit_log_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"audit_log_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_schema.ts"],"names":[],"mappings":"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"}
@@ -57,7 +57,8 @@ export const AUDIT_EVENT_TYPES = Object.freeze([
57
57
  'actor_delete',
58
58
  'actor_purge',
59
59
  'actor_undelete',
60
- 'app_settings_update'
60
+ 'app_settings_update',
61
+ 'db_admin_row_delete'
61
62
  ]);
62
63
  /** Zod schema for audit event types. */
63
64
  export const AuditEventType = z.enum(AUDIT_EVENT_TYPES);
@@ -344,6 +345,20 @@ export const audit_metadata_schemas = Object.freeze({
344
345
  setting: z.string().meta({ description: 'Name of the setting that changed.' }),
345
346
  old_value: z.unknown().meta({ description: 'Setting value before the update.' }),
346
347
  new_value: z.unknown().meta({ description: 'Setting value after the update.' })
348
+ }),
349
+ // Row deleted through the generic db-admin table browser
350
+ // (`http/db_routes.ts` / the Rust spine's `fuz_db_admin`). Account-grain
351
+ // attribution (`account_id`, no `actor_id`) — the browser's gate is
352
+ // account-grain by construction. Emitted only on a successful delete;
353
+ // post-composite-refusal the delete is provably single-row, so no count.
354
+ db_admin_row_delete: z.looseObject({
355
+ table: z.string().meta({ description: 'Table the row was deleted from.' }),
356
+ pk_column: z.string().meta({
357
+ description: 'Single primary-key column the `DELETE` filtered on.'
358
+ }),
359
+ id: z.string().meta({
360
+ description: "Primary-key value of the deleted row, as the row's canonical `::text` rendering."
361
+ })
347
362
  })
348
363
  });
349
364
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bootstrap_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,EAAqB,KAAK,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEzF,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGrD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,8EAA8E;IAC9E,gBAAgB,EAAE,eAAe,CAAC;IAClC;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,uBAAuB,EAAE,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E;;;;;;;;;;OAUG;IACH,yBAAyB,EAAE,WAAW,GAAG,IAAI,CAAC;CAC9C;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnD,EAAE,EAAE,EAAE,CAAC;IACP,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,wBAAwB,EAC9B,SAAS;IAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,KACpC,OAAO,CAAC,eAAe,CAwBzB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,gBAAgB,EACtB,SAAS,qBAAqB,KAC5B,KAAK,CAAC,SAAS,CA0GjB,CAAC"}
1
+ {"version":3,"file":"bootstrap_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bootstrap_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,EAAqB,KAAK,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEzF,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAGrD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACrC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,8EAA8E;IAC9E,gBAAgB,EAAE,eAAe,CAAC;IAClC;;;OAGG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,uBAAuB,EAAE,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9E;;;;;;;;;;OAUG;IACH,yBAAyB,EAAE,WAAW,GAAG,IAAI,CAAC;CAC9C;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACxC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACnD,EAAE,EAAE,EAAE,CAAC;IACP,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,wBAAwB,EAC9B,SAAS;IAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,KACpC,OAAO,CAAC,eAAe,CAwBzB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,gBAAgB,EACtB,SAAS,qBAAqB,KAC5B,KAAK,CAAC,SAAS,CAmHjB,CAAC"}
@@ -13,7 +13,7 @@ import { bootstrap_route_shape } from './bootstrap_route_schema.js';
13
13
  import { get_route_input } from '../http/route_spec.js';
14
14
  import { get_client_ip } from '../http/client_ip.js';
15
15
  import { rate_limit_exceeded_response } from '../rate_limiter.js';
16
- import { ERROR_ALREADY_BOOTSTRAPPED } from '../http/error_schemas.js';
16
+ import { ERROR_ALREADY_BOOTSTRAPPED, ERROR_TOKEN_FILE_MISSING } from '../http/error_schemas.js';
17
17
  /**
18
18
  * Check bootstrap availability at startup.
19
19
  *
@@ -87,6 +87,16 @@ export const create_bootstrap_route_specs = (deps, options) => {
87
87
  log: deps.log
88
88
  }, input.token, input);
89
89
  if (!result.ok) {
90
+ // An unreadable token file closes the window. Bootstrap cannot
91
+ // succeed without one, and `check_bootstrap_status` already reads
92
+ // an unreadable file as unavailable at startup — so leaving the
93
+ // flag set means the two disagree, and every later request takes
94
+ // this leg and writes another audit row. The limiter would bound
95
+ // that channel; closing it ends it, and stops `/status`
96
+ // advertising a window that can't be walked through. Twin of the
97
+ // Rust spine's `bootstrap_handler`, which does the same.
98
+ if (result.error === ERROR_TOKEN_FILE_MISSING)
99
+ bootstrap_status.available = false;
90
100
  if (bootstrap_ip_rate_limiter && ip)
91
101
  bootstrap_ip_rate_limiter.record(ip);
92
102
  deps.audit.emit(route, {
@@ -37,6 +37,13 @@
37
37
  * DBs (no long-lived data) may still drop + re-bootstrap freely on a break —
38
38
  * the freeze is the contract for the deployed chain, not local iteration.
39
39
  *
40
+ * **Every entry is twinned on the Rust spine** (`fuz_auth/src/migrations.rs`),
41
+ * and the two names must match byte for byte: the `_testing_migration_tracker`
42
+ * parity gate compares the `(namespace, name, sequence)` rows both spines
43
+ * record, and a divergence there breaks the swap-freely invariant (a consumer
44
+ * pointing either implementation at one database). Append on both sides in the
45
+ * same change, so no entry below repeats the rule.
46
+ *
40
47
  * @module
41
48
  */
42
49
  import type { Migration, MigrationNamespace } from '../db/migrate.js';
@@ -1 +1 @@
1
- {"version":3,"file":"migrations.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AA+BH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,wDAAwD;AACxD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,aAAa,CAAC,MAAM,CAA8B,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CAuJ5C,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AA+BH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,wDAAwD;AACxD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,aAAa,CAAC,MAAM,CAA8B,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CAmK5C,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
@@ -37,6 +37,13 @@
37
37
  * DBs (no long-lived data) may still drop + re-bootstrap freely on a break —
38
38
  * the freeze is the contract for the deployed chain, not local iteration.
39
39
  *
40
+ * **Every entry is twinned on the Rust spine** (`fuz_auth/src/migrations.rs`),
41
+ * and the two names must match byte for byte: the `_testing_migration_tracker`
42
+ * parity gate compares the `(namespace, name, sequence)` rows both spines
43
+ * record, and a divergence there breaks the swap-freely invariant (a consumer
44
+ * pointing either implementation at one database). Append on both sides in the
45
+ * same change, so no entry below repeats the rule.
46
+ *
40
47
  * @module
41
48
  */
42
49
  import { ACCOUNT_SCHEMA, ACCOUNT_EMAIL_INDEX, ACCOUNT_USERNAME_CI_INDEX, ACTOR_SCHEMA, ACTOR_INDEX, ACTOR_NAME_LOWER_INDEX, ROLE_GRANT_SCHEMA, ROLE_GRANT_INDEXES, AUTH_SESSION_SCHEMA, AUTH_SESSION_INDEXES, API_TOKEN_SCHEMA, API_TOKEN_INDEX, BOOTSTRAP_LOCK_SCHEMA, BOOTSTRAP_LOCK_SEED, INVITE_SCHEMA, INVITE_INDEXES, APP_SETTINGS_SCHEMA, APP_SETTINGS_SEED } from './auth_ddl.js';
@@ -156,10 +163,6 @@ export const auth_migrations = [
156
163
  },
157
164
  // v2: per-credential token scoping — `api_token.scope JSONB NOT NULL`.
158
165
  //
159
- // Name must match the Rust twin's migration byte for byte
160
- // (`fuz_auth/src/migrations.rs`) or the `_testing_migration_tracker` parity
161
- // gate fails and the swap-freely invariant breaks.
162
- //
163
166
  // Three statements, and the middle one is the whole compatibility story:
164
167
  //
165
168
  // 1. Add the column nullable, so the ALTER is instant on a populated table.
@@ -201,15 +204,29 @@ export const auth_migrations = [
201
204
  // would mean the two impls could silently disagree about which credential
202
205
  // answered; the constraint makes that unrepresentable instead of merely
203
206
  // improbable.
204
- //
205
- // The name must match the Rust twin's `api_token_hash_unique_index` byte for
206
- // byte or the `_testing_migration_tracker` parity gate fails and the
207
- // swap-freely invariant breaks.
208
207
  {
209
208
  name: 'api_token_hash_unique_index',
210
209
  up: async (db) => {
211
210
  await db.query('CREATE UNIQUE INDEX IF NOT EXISTS idx_api_token_hash ON api_token(token_hash)');
212
211
  }
212
+ },
213
+ // The GIN the per-cell audit timeline assumes.
214
+ //
215
+ // `full_auth_schema` ships only btree indexes (seq, account, event_type,
216
+ // targets), but `db/cell_audit_queries.ts` reconstructs the per-cell
217
+ // timeline by OR-ing `metadata @> '{...}'::jsonb` containment clauses —
218
+ // sequential scans without this index, degrading with audit volume.
219
+ //
220
+ // `jsonb_path_ops` rather than the default `jsonb_ops`: it serves exactly
221
+ // the `@>` containment operator these queries use, with a smaller, faster
222
+ // index. Server PG and PGlite honor the opclass; PG-compatible engines
223
+ // without operator classes parse and ignore it, serving `@>` from their
224
+ // generic GIN.
225
+ {
226
+ name: 'audit_log_metadata_gin_index',
227
+ up: async (db) => {
228
+ await db.query('CREATE INDEX IF NOT EXISTS idx_audit_log_metadata ON audit_log USING gin (metadata jsonb_path_ops)');
229
+ }
213
230
  }
214
231
  ];
215
232
  /** Pre-composed migration namespace for auth tables. */
package/dist/db/CLAUDE.md CHANGED
@@ -25,7 +25,7 @@ the code.
25
25
  `baseline`), `Migration` / `MigrationNamespace` / `MigrationError`.
26
26
  - `query_deps.ts` — `QueryDeps = {db}`, the first param to every `query_*`.
27
27
  - `assert_row.ts` — `assert_row(row, context)` for INSERT … RETURNING.
28
- - `pg_error.ts` — `is_pg_unique_violation` (Postgres `23505`).
28
+ - `pg_error.ts` — `pg_error_code` (SQLSTATE extractor for `pg`/PGlite errors), `is_pg_unique_violation` (Postgres `23505`).
29
29
  - `sql_identifier.ts` — `assert_valid_sql_identifier`.
30
30
  - `status.ts` — CLI DB status utility.
31
31
  - `schema_ready.ts` — `/ready` deploy-gate core: `query_public_columns`
@@ -95,7 +95,8 @@ child_id`, fractional-index keyed): `query_cell_item_insert` (throws
95
95
  - **`cell_audit_queries.ts`** — `query_audit_log_list_by_cell`: matches
96
96
  `audit_log` rows whose `metadata` names the cell on any of six cell-domain
97
97
  keys (`cell_id` / `source_id` / `new_id` / `parent_id` / `child_id` /
98
- `target_id`), bitmap-OR over the metadata GIN.
98
+ `target_id`), bitmap-OR over `idx_audit_log_metadata` (the `jsonb_path_ops`
99
+ GIN the `audit_log_metadata_gin_index` auth migration ships).
99
100
 
100
101
  ## Fact layer
101
102
 
@@ -155,6 +156,9 @@ auth_migration_ns (built-in, fuz_auth)
155
156
 
156
157
  ### Pre-stable migration policy
157
158
 
158
- Pre-stable: when an introducing migration needs to change shape, **rewrite
159
- the original in place** rather than appending a follow-up — consumers
160
- re-bootstrap dev DBs on upgrade. See ../../../docs/migrations.md.
159
+ Scoped to the namespaces on this page (`fuz_cell` / `fuz_cell_history` /
160
+ `fuz_facts`): each is a single `full_*_schema` entry, so when it needs to
161
+ change shape, **rewrite the original in place** rather than appending a
162
+ follow-up — consumers re-bootstrap dev DBs on upgrade. The `fuz_auth`
163
+ namespace is the opposite — frozen and append-only, since consumers hold
164
+ deployed databases on it. See ../../../docs/migrations.md §Schema stability.
@@ -5,8 +5,10 @@
5
5
  * `new_id` (clone), `source_id` / `target_id` (cell_field events),
6
6
  * `parent_id` / `child_id` (cell_item events).
7
7
  *
8
- * Each `metadata @> '{...}'::jsonb` clause hits the existing GIN on
9
- * `audit_log.metadata`; Postgres bitmap-ORs the index scans together.
8
+ * Each `metadata @> '{...}'::jsonb` clause hits `idx_audit_log_metadata`
9
+ * (the `jsonb_path_ops` GIN on `audit_log.metadata`, shipped by the
10
+ * `audit_log_metadata_gin_index` migration); Postgres bitmap-ORs the index
11
+ * scans together.
10
12
  *
11
13
  * @module
12
14
  */
@@ -1 +1 @@
1
- {"version":3,"file":"cell_audit_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_audit_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAoBnD,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,SAAS,EACf,SAAS,IAAI,EACb,SAAS,oBAAoB,KAC3B,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAwB9B,CAAC"}
1
+ {"version":3,"file":"cell_audit_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_audit_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAoBnD,MAAM,WAAW,oBAAoB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,MAAM,SAAS,EACf,SAAS,IAAI,EACb,SAAS,oBAAoB,KAC3B,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAwB9B,CAAC"}
@@ -5,8 +5,10 @@
5
5
  * `new_id` (clone), `source_id` / `target_id` (cell_field events),
6
6
  * `parent_id` / `child_id` (cell_item events).
7
7
  *
8
- * Each `metadata @> '{...}'::jsonb` clause hits the existing GIN on
9
- * `audit_log.metadata`; Postgres bitmap-ORs the index scans together.
8
+ * Each `metadata @> '{...}'::jsonb` clause hits `idx_audit_log_metadata`
9
+ * (the `jsonb_path_ops` GIN on `audit_log.metadata`, shipped by the
10
+ * `audit_log_metadata_gin_index` migration); Postgres bitmap-ORs the index
11
+ * scans together.
10
12
  *
11
13
  * @module
12
14
  */
@@ -6,6 +6,18 @@
6
6
  *
7
7
  * @module
8
8
  */
9
+ /**
10
+ * Extract the error-code string from a caught error, for SQLSTATE checks.
11
+ *
12
+ * Other error families also carry string `code`s (Node FS errors:
13
+ * `ENOENT`, `ENOSPC`, …) — compare the result against specific SQLSTATE
14
+ * values, never against truthiness.
15
+ *
16
+ * @param error - the caught error
17
+ * @returns the `code` string, or `null` when the value isn't an `Error`
18
+ * or carries no string `code`
19
+ */
20
+ export declare const pg_error_code: (error: unknown) => string | null;
9
21
  /**
10
22
  * Check if an error is a PostgreSQL unique constraint violation (error code 23505).
11
23
  *
@@ -1 +1 @@
1
- {"version":3,"file":"pg_error.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/pg_error.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,OAAO,KAAG,OACmC,CAAC"}
1
+ {"version":3,"file":"pg_error.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/pg_error.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,KAAG,MAAM,GAAG,IAGhD,CAAC;AAET;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,OAAO,KAAG,OAA2C,CAAC"}
@@ -6,10 +6,24 @@
6
6
  *
7
7
  * @module
8
8
  */
9
+ /**
10
+ * Extract the error-code string from a caught error, for SQLSTATE checks.
11
+ *
12
+ * Other error families also carry string `code`s (Node FS errors:
13
+ * `ENOENT`, `ENOSPC`, …) — compare the result against specific SQLSTATE
14
+ * values, never against truthiness.
15
+ *
16
+ * @param error - the caught error
17
+ * @returns the `code` string, or `null` when the value isn't an `Error`
18
+ * or carries no string `code`
19
+ */
20
+ export const pg_error_code = (error) => error instanceof Error && 'code' in error && typeof error.code === 'string'
21
+ ? error.code
22
+ : null;
9
23
  /**
10
24
  * Check if an error is a PostgreSQL unique constraint violation (error code 23505).
11
25
  *
12
26
  * @param error - the caught error
13
27
  * @returns `true` if the error is a unique constraint violation
14
28
  */
15
- export const is_pg_unique_violation = (error) => error instanceof Error && 'code' in error && error.code === '23505';
29
+ export const is_pg_unique_violation = (error) => pg_error_code(error) === '23505';
@@ -29,7 +29,7 @@ effects, see ../../../docs/architecture.md.
29
29
  - `http/jsonrpc_errors.ts` — `ThrownJsonrpcError`, `jsonrpc_errors` throwers, HTTP-status mappings.
30
30
  - `http/jsonrpc_helpers.ts` — message builders, type guards, input/result normalizers.
31
31
  - `http/common_routes.ts` — health check + readiness probe (`/ready` schema-drift deploy gate) + authenticated server-status + surface route specs.
32
- - `http/db_routes.ts` — generic keeper-only table browser route specs (public schema).
32
+ - `http/db_routes.ts` — allowlist-gated keeper-only table browser route specs (`public` schema, minus the `NON_BROWSABLE_TABLES` credential floor).
33
33
  - `http/pending_effects.ts` — `emit_after_commit` + `dispatch_with_post_commit_rollback` (the shared rollback-discard wrapper both dispatch sites use) + `flush_pending_effects` + `flush_post_commit_effects` + `EmitAfterCommitContext`.
34
34
 
35
35
  ## Route Spec System
@@ -210,7 +210,7 @@ confirmation.
210
210
  - **Bootstrap**: `ERROR_ALREADY_BOOTSTRAPPED`, `ERROR_TOKEN_FILE_MISSING`
211
211
  - **Signup/invites**: `ERROR_NO_MATCHING_INVITE`, `ERROR_SIGNUP_CONFLICT`, `ERROR_INVITE_NOT_FOUND`, `ERROR_INVITE_DUPLICATE`, `ERROR_INVITE_ACCOUNT_EXISTS_USERNAME`, `ERROR_INVITE_ACCOUNT_EXISTS_EMAIL`
212
212
  - **Admin**: `ERROR_ROLE_NOT_WEB_GRANTABLE`, `ERROR_ROLE_GRANT_NOT_FOUND`, `ERROR_INVALID_EVENT_TYPE`
213
- - **DB browser**: `ERROR_FOREIGN_KEY_VIOLATION`, `ERROR_TABLE_NOT_FOUND`, `ERROR_TABLE_NO_PRIMARY_KEY`, `ERROR_ROW_NOT_FOUND`
213
+ - **DB browser**: `ERROR_FOREIGN_KEY_VIOLATION`, `ERROR_TABLE_NOT_FOUND`, `ERROR_TABLE_NO_PRIMARY_KEY`, `ERROR_TABLE_NOT_DELETABLE`, `ERROR_ROW_NOT_FOUND`
214
214
 
215
215
  ## Schema Helpers
216
216
 
@@ -485,36 +485,96 @@ auth-domain dependencies:
485
485
  Auth-aware variants (account status, bootstrap status) live in `auth/` —
486
486
  `http/common_routes.ts` stays generic.
487
487
 
488
- ## DB Routes (Generic Browser)
488
+ ## DB Routes (Allowlisted Browser)
489
489
 
490
490
  `http/db_routes.ts` creates keeper-only route specs for administering the
491
491
  `public` schema via `information_schema`. Wired by consumers that want a
492
- generic table browser; the factory is domain-agnostic.
493
-
494
- `create_db_route_specs({db_type, db_name, extra_stats?, log?})`:
495
-
496
- - `GET /health` — connected probe + table count + optional `extra_stats(db)`. Returns `{connected: false}` at 503 on failure
497
- - `GET /tables` — list public tables with row counts
498
- - `GET /tables/:name` — columns + rows (paginated via `?offset`/`?limit`, limit clamped to `[1, 1000]` with default 100) + total count + primary key
499
- - `DELETE /tables/:name/rows/:id` — delete by PK. Returns 400 if table has no PK (`ERROR_TABLE_NO_PRIMARY_KEY`), 404 if row missing (`ERROR_ROW_NOT_FOUND`) or table missing (`ERROR_TABLE_NOT_FOUND`), 409 on FK violation (pg error code `23503`)
492
+ table browser; the factory is domain-agnostic.
493
+
494
+ `create_db_route_specs(deps, {db_type, db_name, browsable_tables, extra_stats?, log?, non_deletable_tables?})` —
495
+ `deps` is `DbRouteDeps`, a structural `audit` slice of `AppDeps` declared in
496
+ `http/db_routes.ts` so this directory stays auth-free (`ctx.deps` satisfies it):
497
+
498
+ - `GET /health` — connected probe + table count (schema-wide by design — a did-migrations-run diagnostic) + optional `extra_stats(db)`. Returns `{connected: false}` at 503 on failure
499
+ - `GET /tables` — list browsable tables with row counts
500
+ - `GET /tables/:name` — columns + rows (paginated via `?offset`/`?limit`, limit validated to `[1, 1000]` with default 100; bytea values come back as `<N bytes>` placeholders, NULL stays NULL, column metadata untouched) + total count + `primary_key` (the single PK column, or `null` when the table has none or a composite one) + `deletable` (whether the row-`DELETE` will accept this table at all — see below)
501
+ - `DELETE /tables/:name/rows/:id` — delete by PK, compared typed as `"<pk>" = $1` (twinning the Rust spine — the untyped text-format param makes PG coerce the id with the PK type's input function, so the delete index-scans and follows the column type's semantics; an uncoercible id — 22P02/22003 — is the same clean 404 as a typed miss). `RETURNING "<pk>"::text` feeds the audit trail the row's canonical PK rendering. Returns 400 if the table is excluded by policy (`ERROR_TABLE_NOT_DELETABLE`) or has no single-column PK (`ERROR_TABLE_NO_PRIMARY_KEY`), 404 if row missing (`ERROR_ROW_NOT_FOUND`) or table missing/unbrowsable (`ERROR_TABLE_NOT_FOUND`), 409 on a referencing-row refusal (pg error `23503` foreign_key_violation, or `23001` restrict_violation — PG 18+ raises the latter for `ON DELETE RESTRICT`)
502
+
503
+ **The allowlist gates everything.** `DbRouteOptions.browsable_tables` is
504
+ required with no "all" escape hatch — the table list shows only listed tables,
505
+ and detail/`DELETE` on an unlisted table answer exactly like a table that
506
+ doesn't exist (404 `ERROR_TABLE_NOT_FOUND`, byte-identical masking). The
507
+ credential floor `NON_BROWSABLE_TABLES` (`account` — the argon2 hash corpus,
508
+ `auth_session` / `api_token` — credential digests, `bootstrap_lock`) is
509
+ subtracted from the allowlist even when a consumer names those tables. The
510
+ browse/delete transactions run under `SET LOCAL statement_timeout`
511
+ (`DB_ADMIN_STATEMENT_TIMEOUT_MS`) so no single statement outlives the bound
512
+ (per statement, not per request — the table listing's per-table `COUNT(*)`
513
+ loop can still take several in sequence).
514
+
515
+ **Single-column primary keys only.** The `DELETE` filters on one column
516
+ (`WHERE "<pk>" = $1`), which is correct only when the primary key _is_
517
+ that one column. `query_primary_key_columns` returns every PK column in
518
+ `ordinal_position` order, and the route proceeds only at length exactly 1 — a
519
+ composite key (a single-column filter would match every row sharing that
520
+ column's value and silently over-delete) and an absent key both refuse with 400
521
+ and delete nothing. `GET /tables/:name` reports `primary_key: null` in the same
522
+ two cases, so the UI (`ui/table_state.svelte.ts`) disables delete without firing
523
+ a request. Twin of the Rust spine's `fuz_db_admin` router.
524
+
525
+ **Some tables are never row-deletable.** `NON_DELETABLE_TABLES` — `audit_log`,
526
+ `bootstrap_lock`, `app_settings`, `schema_version` — refuses the `DELETE` with
527
+ 400 `ERROR_TABLE_NOT_DELETABLE` before the key-shape check, because a generic
528
+ storage endpoint has no business deleting a row whose meaning lives in the
529
+ domain layer. `audit_log` is the sharpest: it is both the trail and, since
530
+ `realtime/sse_auth_guard.ts` and the WS auth guard close live streams by
531
+ listening to audit events, part of how revocation propagates. The three
532
+ singletons each fail differently on deletion — `bootstrap_lock` leaves
533
+ `check_bootstrap_status` advertising a window `bootstrap_account.ts` then always
534
+ refuses, `app_settings` makes every settings read throw, `schema_version` is the
535
+ migration tracker (composite-keyed, so already refused — named here so the
536
+ protection survives a key-shape change). Consumers add their own via
537
+ `DbRouteOptions.non_deletable_tables`, which is unioned with the builtin set and
538
+ never replaces it.
539
+
540
+ `GET /tables/:name` reports `deletable` = exactly what the `DELETE` will accept
541
+ (single-column PK **and** not excluded), so a client hides the affordance
542
+ instead of discovering the refusal — `ui/table_state.svelte.ts` gates on
543
+ `can_delete`. `primary_key` stays a truthful report of the key shape; a policy
544
+ exclusion must not masquerade as one.
545
+
546
+ **Every successful row delete is audited.** The `DELETE` handler defers a
547
+ `deps.audit` emit through `emit_after_commit`, so the pool-routed row lands
548
+ iff the delete's transaction commits — the success-only trail can't claim a
549
+ delete that failed at COMMIT (the Rust twin commits before emitting for the
550
+ same reason). Account-grain attribution (the browser's gate is account-grain,
551
+ so no actor is claimed), metadata `{table, pk_column, id}`. Refusals emit
552
+ nothing. The completeness gate covers the event via the
553
+ `db_routes.db.test.ts` emission tests (excluded-with-justification in
554
+ `testing/audit_completeness.ts`). Twinned by `fuz_db_admin`'s emission on the
555
+ Rust spine.
500
556
 
501
557
  All four routes use the keeper auth shape (`{account: 'required', actor: 'required', roles: ['keeper'], credential_types: ['daemon_token']}`).
502
558
  Param schemas use `VALID_SQL_IDENTIFIER` regex, and every table name gets
503
559
  `assert_valid_sql_identifier()` before string-interpolating into SQL —
504
560
  the identifier validation is the only reason the interpolation is safe.
505
561
 
506
- **Do not widen the auth on these specs.** They serve paginated rows of any
507
- `public` table — `account.password_hash`, `auth_session`, `api_token` — plus
508
- row `DELETE`. `credential_types: ['daemon_token']` is what keeps every bearer
509
- credential out, and it's why the specs need no `auth.required_scope`: a daemon
510
- token resolves to `full` by construction, so no narrowed token can reach them
511
- (see ../../../docs/security.md §Token scoping). Re-auth them to a role a bearer
562
+ **Do not widen the auth on these specs.** They serve paginated rows of every
563
+ browsable table plus row `DELETE` — the credential tables sit behind the
564
+ `NON_BROWSABLE_TABLES` floor, but browsable rows can still carry secrets in
565
+ data columns (a consumer's `cell.data` is the live example).
566
+ `credential_types: ['daemon_token']` is what keeps every bearer credential
567
+ out, and it's why the specs need no `auth.required_scope`: a daemon token
568
+ resolves to `full` by construction, so no narrowed token can reach them (see
569
+ ../../../docs/security.md §Token scoping). Re-auth them to a role a bearer
512
570
  satisfies and both controls are gone at once — the token's scope stops
513
571
  applying and the credential gate stops firing. A consumer that does this owns
514
572
  the surface census for it.
515
573
 
516
574
  Interfaces exported for consumer use: `TableInfo`, `TableWithCount`,
517
- `PrimaryKeyInfo`, `ColumnInfo`, `DbRouteOptions`.
575
+ `PrimaryKeyInfo`, `ColumnInfo`, `DbRouteDeps`, `DbRouteOptions`,
576
+ `NON_DELETABLE_TABLES`, `NON_BROWSABLE_TABLES`, `DB_TABLE_ROWS_DEFAULT_LIMIT`,
577
+ `DB_TABLE_ROWS_LIMIT_MAX`, `DB_ADMIN_STATEMENT_TIMEOUT_MS`.
518
578
 
519
579
  ## Cross-Module Notes
520
580