@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.
- package/README.md +4 -0
- package/dist/actions/CLAUDE.md +1 -1
- package/dist/auth/CLAUDE.md +1 -1
- package/dist/auth/audit_emitter.d.ts +9 -5
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.d.ts +7 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +16 -1
- package/dist/auth/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +11 -1
- package/dist/auth/migrations.d.ts +7 -0
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +25 -8
- package/dist/db/CLAUDE.md +9 -5
- package/dist/db/cell_audit_queries.d.ts +4 -2
- package/dist/db/cell_audit_queries.d.ts.map +1 -1
- package/dist/db/cell_audit_queries.js +4 -2
- package/dist/db/pg_error.d.ts +12 -0
- package/dist/db/pg_error.d.ts.map +1 -1
- package/dist/db/pg_error.js +15 -1
- package/dist/http/CLAUDE.md +78 -18
- package/dist/http/db_routes.d.ts +96 -3
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +263 -46
- package/dist/http/error_schemas.d.ts +12 -1
- package/dist/http/error_schemas.d.ts.map +1 -1
- package/dist/http/error_schemas.js +12 -1
- package/dist/http/pending_effects.d.ts +5 -5
- package/dist/http/pending_effects.d.ts.map +1 -1
- package/dist/http/pending_effects.js +1 -1
- package/dist/testing/CLAUDE.md +21 -0
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +5 -1
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +53 -1
- package/dist/testing/cross_backend/query_shape.d.ts +10 -0
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -0
- package/dist/testing/cross_backend/query_shape.js +128 -0
- package/dist/ui/CLAUDE.md +9 -4
- package/dist/ui/table_state.svelte.d.ts +18 -5
- package/dist/ui/table_state.svelte.d.ts.map +1 -1
- package/dist/ui/table_state.svelte.js +22 -7
- package/dist/ui/ui_format.d.ts.map +1 -1
- package/dist/ui/ui_format.js +4 -0
- 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)
|
package/dist/actions/CLAUDE.md
CHANGED
|
@@ -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`.
|
package/dist/auth/CLAUDE.md
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
55
|
-
* are already rollback-resilient because they run
|
|
56
|
-
* transaction, so
|
|
57
|
-
*
|
|
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
|
|
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":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAmB9C;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,
|
|
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,
|
|
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
|
|
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"}
|
package/dist/auth/migrations.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
9
|
-
* `audit_log.metadata
|
|
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
|
|
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
|
|
9
|
-
* `audit_log.metadata
|
|
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
|
*/
|
package/dist/db/pg_error.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/db/pg_error.js
CHANGED
|
@@ -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
|
|
29
|
+
export const is_pg_unique_violation = (error) => pg_error_code(error) === '23505';
|
package/dist/http/CLAUDE.md
CHANGED
|
@@ -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` —
|
|
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 (
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
`create_db_route_specs({db_type, db_name, extra_stats?, log?})
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
- `GET /
|
|
499
|
-
- `
|
|
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
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
|