@fuzdev/fuz_app 0.149.0 → 0.150.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 (141) hide show
  1. package/dist/actions/CLAUDE.md +9 -7
  2. package/dist/actions/action_bridge.d.ts +7 -0
  3. package/dist/actions/action_bridge.d.ts.map +1 -1
  4. package/dist/actions/action_bridge.js +10 -0
  5. package/dist/actions/action_event_types.d.ts +1 -1
  6. package/dist/actions/action_event_types.d.ts.map +1 -1
  7. package/dist/actions/perform_action.d.ts.map +1 -1
  8. package/dist/actions/perform_action.js +8 -5
  9. package/dist/actions/register_action_ws.d.ts +8 -2
  10. package/dist/actions/register_action_ws.d.ts.map +1 -1
  11. package/dist/actions/register_action_ws.js +24 -4
  12. package/dist/actions/transports_ws_auth_guard.d.ts +6 -4
  13. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  14. package/dist/actions/transports_ws_auth_guard.js +9 -6
  15. package/dist/actions/ws_endpoint_spec.d.ts +1 -1
  16. package/dist/auth/CLAUDE.md +69 -31
  17. package/dist/auth/actor_lookup_queries.d.ts +3 -2
  18. package/dist/auth/actor_lookup_queries.d.ts.map +1 -1
  19. package/dist/auth/actor_lookup_queries.js +6 -3
  20. package/dist/auth/actor_search_queries.d.ts +5 -2
  21. package/dist/auth/actor_search_queries.d.ts.map +1 -1
  22. package/dist/auth/actor_search_queries.js +11 -4
  23. package/dist/auth/admin_action_specs.d.ts +4 -4
  24. package/dist/auth/admin_action_specs.js +4 -4
  25. package/dist/auth/admin_actions.d.ts.map +1 -1
  26. package/dist/auth/admin_actions.js +35 -6
  27. package/dist/auth/all_cell_actions.d.ts +11 -2
  28. package/dist/auth/all_cell_actions.d.ts.map +1 -1
  29. package/dist/auth/all_cell_actions.js +2 -1
  30. package/dist/auth/audit_emitter.d.ts +9 -3
  31. package/dist/auth/audit_emitter.d.ts.map +1 -1
  32. package/dist/auth/audit_emitter.js +9 -3
  33. package/dist/auth/audit_log_schema.d.ts +1 -0
  34. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  35. package/dist/auth/audit_log_schema.js +9 -5
  36. package/dist/auth/bootstrap_account.d.ts +25 -1
  37. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  38. package/dist/auth/bootstrap_account.js +37 -4
  39. package/dist/auth/bootstrap_routes.d.ts +3 -2
  40. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  41. package/dist/auth/bootstrap_routes.js +5 -4
  42. package/dist/auth/cell_action_specs.d.ts +215 -15
  43. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  44. package/dist/auth/cell_action_specs.js +103 -16
  45. package/dist/auth/cell_actions.d.ts +40 -18
  46. package/dist/auth/cell_actions.d.ts.map +1 -1
  47. package/dist/auth/cell_actions.js +126 -19
  48. package/dist/auth/cell_audit_events.d.ts +1 -1
  49. package/dist/auth/cell_audit_events.d.ts.map +1 -1
  50. package/dist/auth/cell_audit_events.js +3 -2
  51. package/dist/auth/cell_audit_metadata.d.ts +11 -0
  52. package/dist/auth/cell_audit_metadata.d.ts.map +1 -1
  53. package/dist/auth/cell_audit_metadata.js +10 -0
  54. package/dist/auth/cell_grant_action_specs.d.ts +7 -0
  55. package/dist/auth/cell_grant_action_specs.d.ts.map +1 -1
  56. package/dist/auth/cell_grant_action_specs.js +7 -0
  57. package/dist/auth/cell_grant_actions.d.ts +36 -4
  58. package/dist/auth/cell_grant_actions.d.ts.map +1 -1
  59. package/dist/auth/cell_grant_actions.js +20 -3
  60. package/dist/auth/credential_type_schema.d.ts +3 -2
  61. package/dist/auth/credential_type_schema.d.ts.map +1 -1
  62. package/dist/auth/credential_type_schema.js +3 -2
  63. package/dist/auth/role_grant_offer_action_specs.d.ts +18 -2
  64. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  65. package/dist/auth/role_grant_offer_action_specs.js +25 -3
  66. package/dist/auth/role_grant_offer_actions.d.ts +9 -4
  67. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  68. package/dist/auth/role_grant_offer_actions.js +58 -7
  69. package/dist/auth/role_grant_offer_notifications.d.ts +3 -2
  70. package/dist/auth/role_grant_offer_notifications.d.ts.map +1 -1
  71. package/dist/auth/role_grant_offer_notifications.js +3 -2
  72. package/dist/auth/role_grant_offer_queries.d.ts +20 -0
  73. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  74. package/dist/auth/role_grant_offer_queries.js +35 -0
  75. package/dist/auth/role_grant_queries.d.ts +27 -9
  76. package/dist/auth/role_grant_queries.d.ts.map +1 -1
  77. package/dist/auth/role_grant_queries.js +40 -13
  78. package/dist/auth/role_schema.d.ts +14 -8
  79. package/dist/auth/role_schema.d.ts.map +1 -1
  80. package/dist/auth/role_schema.js +6 -3
  81. package/dist/auth/self_service_role_actions.d.ts.map +1 -1
  82. package/dist/auth/self_service_role_actions.js +20 -0
  83. package/dist/db/CLAUDE.md +9 -2
  84. package/dist/db/cell_ddl.d.ts +9 -1
  85. package/dist/db/cell_ddl.d.ts.map +1 -1
  86. package/dist/db/cell_ddl.js +11 -1
  87. package/dist/db/cell_queries.d.ts +53 -7
  88. package/dist/db/cell_queries.d.ts.map +1 -1
  89. package/dist/db/cell_queries.js +61 -7
  90. package/dist/http/body_limit.d.ts +3 -1
  91. package/dist/http/body_limit.d.ts.map +1 -1
  92. package/dist/http/body_limit.js +6 -1
  93. package/dist/http/jsonrpc.d.ts +9 -0
  94. package/dist/http/jsonrpc.d.ts.map +1 -1
  95. package/dist/http/jsonrpc.js +17 -0
  96. package/dist/http/jsonrpc_errors.d.ts.map +1 -1
  97. package/dist/http/jsonrpc_errors.js +29 -20
  98. package/dist/http/proxy.d.ts +4 -2
  99. package/dist/http/proxy.d.ts.map +1 -1
  100. package/dist/http/proxy.js +7 -2
  101. package/dist/http/surface.d.ts +3 -1
  102. package/dist/http/surface.d.ts.map +1 -1
  103. package/dist/http/surface.js +7 -4
  104. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  105. package/dist/realtime/sse_auth_guard.js +3 -2
  106. package/dist/schema_meta.d.ts +32 -1
  107. package/dist/schema_meta.d.ts.map +1 -1
  108. package/dist/schema_meta.js +58 -5
  109. package/dist/server/app_server.d.ts +13 -5
  110. package/dist/server/app_server.d.ts.map +1 -1
  111. package/dist/server/app_server.js +32 -8
  112. package/dist/testing/CLAUDE.md +34 -21
  113. package/dist/testing/app_server.d.ts +3 -1
  114. package/dist/testing/app_server.d.ts.map +1 -1
  115. package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
  116. package/dist/testing/cross_backend/actor_lookup.js +27 -0
  117. package/dist/testing/cross_backend/capabilities.d.ts +2 -1
  118. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  119. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  120. package/dist/testing/cross_backend/cell_gated_create.js +109 -13
  121. package/dist/testing/cross_backend/cell_moderate.d.ts +7 -1
  122. package/dist/testing/cross_backend/cell_moderate.d.ts.map +1 -1
  123. package/dist/testing/cross_backend/cell_moderate.js +68 -4
  124. package/dist/testing/cross_backend/cell_ref_admission.d.ts.map +1 -1
  125. package/dist/testing/cross_backend/cell_ref_admission.js +18 -9
  126. package/dist/testing/cross_backend/conformance_table.d.ts +5 -0
  127. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  128. package/dist/testing/cross_backend/conformance_table.js +12 -10
  129. package/dist/testing/cross_backend/differential.d.ts +7 -0
  130. package/dist/testing/cross_backend/differential.d.ts.map +1 -1
  131. package/dist/testing/cross_backend/differential.js +17 -0
  132. package/dist/testing/cross_backend/expected_schema.json +1 -0
  133. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  134. package/dist/testing/cross_backend/full_spine_mount.js +5 -4
  135. package/dist/testing/cross_backend/role_grant_offer_lifecycle.d.ts.map +1 -1
  136. package/dist/testing/cross_backend/role_grant_offer_lifecycle.js +74 -2
  137. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +18 -19
  138. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -1
  139. package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +66 -42
  140. package/dist/testing/cross_backend/transport_composition.js +3 -3
  141. package/package.json +1 -1
@@ -70,7 +70,8 @@ dropping per-spec `*_METHOD` constants (readers dereference `.method`). See
70
70
  - `local_call` — none (no REST, no RPC, no WS, no SSE).
71
71
 
72
72
  `create_action_route_spec` throws if `spec.auth` is null (notifications and
73
- local calls cannot become routes). `create_action_event_spec` throws on any
73
+ local calls cannot become routes) or `spec.rate_limit` is set (a bridged route
74
+ never reaches the `perform_action` limiters). `create_action_event_spec` throws on any
74
75
  non-`remote_notification` kind.
75
76
 
76
77
  ## Registry compile (`actions/compile_action_registry.ts`)
@@ -177,7 +178,7 @@ and `FrontendActionHandlers`.
177
178
  Derives transport-specific specs from action specs. HTTP-specific concerns
178
179
  (path, handler, errors) come from options, not the action spec.
179
180
 
180
- - `create_action_route_spec(spec, options)` — one action → one `RouteSpec`. HTTP method defaults by `side_effects` (`true` → POST, `false` → GET; override via `options.http_method`). `route.auth` is `spec.auth` plus a derived `required_scope`. `transaction: spec.side_effects`. Throws if `spec.auth` is null.
181
+ - `create_action_route_spec(spec, options)` — one action → one `RouteSpec`. HTTP method defaults by `side_effects` (`true` → POST, `false` → GET; override via `options.http_method`). `route.auth` is `spec.auth` plus a derived `required_scope`. `transaction: spec.side_effects`. Throws if `spec.auth` is null, or if `spec.rate_limit` is set (the action limiters run in `perform_action`, which a bridged route never reaches).
181
182
 
182
183
  **The bridge carries the token-scope gate across.** A bridged route runs
183
184
  `options.handler` through the REST pipeline and never reaches
@@ -235,9 +236,10 @@ Shim responsibilities (per-request):
235
236
  6. Bind result via `perform_action_result_to_envelope(id, result)`; `c.json(envelope, result.status)`.
236
237
 
237
238
  Error paths: `ThrownJsonrpcError` (duck-typed via `err instanceof Error &&
238
- typeof err.code === 'number'` to handle cross-copy `instanceof` misses,
239
+ is_jsonrpc_error_code(err.code)` to handle cross-copy `instanceof` misses,
239
240
  e.g. when consumers like zzz throw their own `ThrownJsonrpcError`)
240
- preserves code + data verbatim. Generic throws become `internal_error` 500;
241
+ preserves code + data verbatim; an error whose numeric `code` is not a
242
+ JSON-RPC one (a `DOMException`) takes the generic path. Generic throws become `internal_error` 500;
241
243
  message is the raw error under `DEV`, "internal server error" otherwise.
242
244
 
243
245
  A result `c.json` cannot serialize (a `bigint`, a cycle) answers the same
@@ -360,7 +362,7 @@ and `allow_fallback: boolean` (default `true`). Explicit
360
362
 
361
363
  ### WS close codes (`actions/transports.ts`)
362
364
 
363
- - `WS_CLOSE_SESSION_REVOKED = 4001` — server revoked auth (reason `WS_CLOSE_SESSION_REVOKED_REASON`, `Session revoked`); client enters permanent `revoked` state, no reconnect. Also the close for an upgrade refused at admission because its credential was revoked mid-upgrade — indistinguishable on the wire by design — and for a socket whose credential expired while open, with reason `WS_CLOSE_CREDENTIAL_EXPIRED_REASON` (`Credential expired`), which the client treats as the same `revoked` state.
365
+ - `WS_CLOSE_SESSION_REVOKED = 4001` — server revoked auth (reason `WS_CLOSE_SESSION_REVOKED_REASON`, `Session revoked`); client enters permanent `revoked` state, no reconnect. Also the close for an upgrade refused at admission because its credential was revoked, or its acting actor soft-deleted, mid-upgrade — indistinguishable on the wire by design — and for a socket whose credential expired while open, with reason `WS_CLOSE_CREDENTIAL_EXPIRED_REASON` (`Credential expired`), which the client treats as the same `revoked` state.
364
366
  - `WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT = 4002` — client observed receive-silence past its receive timeout (`resolve_heartbeat_receive_timeout`).
365
367
  - `WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT = 4003` — server observed receive-silence past `DEFAULT_SERVER_HEARTBEAT_TIMEOUT` (60s).
366
368
  - `WS_CLOSE_CONNECTION_LIMIT = 4004` — server closed this socket to admit a newer one on the same account, past the per-account connection cap (reason `connection limit`). Not a revocation: the client stays `closed` with `superseded` set and no reconnect, until `connect()` is called. Twin of the Rust spine's constant.
@@ -736,8 +738,8 @@ guard reads too — one declaration, so the two can't drift:
736
738
  - `none` → nothing — every other event, and any event type fuz_app doesn't
737
739
  define
738
740
 
739
- `outcome === 'failure'` events are ignored — they carry attacker-controlled
740
- identifiers. Reacting to them would let an authenticated caller close
741
+ Only `outcome === 'success'` events act — a failure carries attacker-controlled
742
+ identifiers, and an outcome outside the enum is no revocation. Reacting to them would let an authenticated caller close
741
743
  another user's socket by guessing a session hash or token id.
742
744
 
743
745
  ## WebSocket dispatch — three layered entry points
@@ -80,11 +80,18 @@ export declare const derive_http_method: (side_effects: ActionSideEffects) => Ro
80
80
  * same holder reaches the route by dropping the credential, so the guard would
81
81
  * enforce nothing — the shape `fuz_auth_guard_resolver` refuses outright.
82
82
  *
83
+ * **A rate-limited action is refused.** The action limiters run in
84
+ * `perform_action`, which a bridged route never reaches, and a route spec has
85
+ * no limiter of its own — so bridging a `rate_limit` action would serve it
86
+ * unthrottled beside the throttled RPC and WS paths, a way around its budget.
87
+ * Serve it over RPC or WS instead.
88
+ *
83
89
  * @param spec - the action spec (must have non-null `auth`)
84
90
  * @param options - HTTP-specific options (path, handler, optional overrides)
85
91
  * @returns a `RouteSpec` ready for `apply_route_specs`
86
92
  * @throws Error if `spec.auth` is null (only `request_response` actions can
87
93
  * become routes; notifications and local calls cannot)
94
+ * @throws Error if `spec.rate_limit` is set (the bridged route would be unthrottled)
88
95
  */
89
96
  export declare const create_action_route_spec: (spec: ActionSpec, options: ActionRouteOptions) => RouteSpec;
90
97
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"action_bridge.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,+DAA+D;AAC/D,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,uGAAuG;IACvG,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACrB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACpB,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;;;;;;;;;;;OAaG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,8GAA8G;IAC9G,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC3B;AAED,gEAAgE;AAChE,MAAM,WAAW,kBAAkB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,GAAI,cAAc,iBAAiB,KAAG,WAEpE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,UAAU,EAChB,SAAS,kBAAkB,KACzB,SA4BF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,UAAU,EAChB,UAAU,kBAAkB,KAC1B,SAcF,CAAC"}
1
+ {"version":3,"file":"action_bridge.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EAAkB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAElE,+DAA+D;AAC/D,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,YAAY,CAAC;IACtB,uGAAuG;IACvG,MAAM,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACrB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,CAAC,CAAC,SAAS,CAAC;IACpB,mFAAmF;IACnF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;;;;;;;;;;;;;OAaG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,8GAA8G;IAC9G,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC3B;AAED,gEAAgE;AAChE,MAAM,WAAW,kBAAkB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,GAAI,cAAc,iBAAiB,KAAG,WAEpE,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,UAAU,EAChB,SAAS,kBAAkB,KACzB,SAmCF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,UAAU,EAChB,UAAU,kBAAkB,KAC1B,SAcF,CAAC"}
@@ -46,16 +46,26 @@ export const derive_http_method = (side_effects) => {
46
46
  * same holder reaches the route by dropping the credential, so the guard would
47
47
  * enforce nothing — the shape `fuz_auth_guard_resolver` refuses outright.
48
48
  *
49
+ * **A rate-limited action is refused.** The action limiters run in
50
+ * `perform_action`, which a bridged route never reaches, and a route spec has
51
+ * no limiter of its own — so bridging a `rate_limit` action would serve it
52
+ * unthrottled beside the throttled RPC and WS paths, a way around its budget.
53
+ * Serve it over RPC or WS instead.
54
+ *
49
55
  * @param spec - the action spec (must have non-null `auth`)
50
56
  * @param options - HTTP-specific options (path, handler, optional overrides)
51
57
  * @returns a `RouteSpec` ready for `apply_route_specs`
52
58
  * @throws Error if `spec.auth` is null (only `request_response` actions can
53
59
  * become routes; notifications and local calls cannot)
60
+ * @throws Error if `spec.rate_limit` is set (the bridged route would be unthrottled)
54
61
  */
55
62
  export const create_action_route_spec = (spec, options) => {
56
63
  if (spec.auth === null) {
57
64
  throw new Error(`Cannot derive route spec from action '${spec.method}': auth is null (only request_response actions with non-null auth can become routes)`);
58
65
  }
66
+ if (spec.rate_limit) {
67
+ throw new Error(`Cannot derive route spec from action '${spec.method}': it declares rate_limit '${spec.rate_limit}', which a bridged route would not enforce (serve it over RPC or WS instead)`);
68
+ }
59
69
  const auth = options.auth ?? spec.auth;
60
70
  // Carry the dispatcher's per-method scope gate across — see the token-scope
61
71
  // note above. An explicit capability wins; a public route has no credential
@@ -31,7 +31,7 @@ export interface ActionEventEnvironment {
31
31
  * notifications and requests. Unset falls back to `console_action_log`;
32
32
  * `null` silences them.
33
33
  */
34
- readonly log?: Logger | null;
34
+ readonly log?: ActionLog | null;
35
35
  }
36
36
  /** The logging surface the action dispatch path uses. */
37
37
  export type ActionLog = Pick<Logger, 'error' | 'warn' | 'debug'>;
@@ -1 +1 @@
1
- {"version":3,"file":"action_event_types.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_event_types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEtF,eAAO,MAAM,cAAc;;;EAAkC,CAAC;AAC9D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,eAAO,MAAM,eAAe;;;;;;EAAiE,CAAC;AAC9F,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAS9D,eAAO,MAAM,6BAA6B,EAAE,MAAM,CACjD,eAAe,EACf,aAAa,CAAC,eAAe,CAAC,CAO9B,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAW1F,CAAC;AAEF,eAAO,MAAM,8BAA8B,EAAE,MAAM,CAAC,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAU5F,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,qBAAqB,EAAE,CACtB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,gBAAgB,KACnB,CAAC,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAC,GAAG,SAAS,CAAC;IACvC,kBAAkB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,eAAe,GAAG,SAAS,CAAC;IACpE;;;;OAIG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,yDAAyD;AACzD,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;AAEjE;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAIhC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,aAAa,sBAAsB,KAAG,SAAS,GAAG,IAChB,CAAC"}
1
+ {"version":3,"file":"action_event_types.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_event_types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEtF,eAAO,MAAM,cAAc;;;EAAkC,CAAC;AAC9D,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,eAAO,MAAM,eAAe;;;;;;EAAiE,CAAC;AAC9F,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAS9D,eAAO,MAAM,6BAA6B,EAAE,MAAM,CACjD,eAAe,EACf,aAAa,CAAC,eAAe,CAAC,CAO9B,CAAC;AAEF,eAAO,MAAM,0BAA0B,EAAE,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAW1F,CAAC;AAEF,eAAO,MAAM,8BAA8B,EAAE,MAAM,CAAC,gBAAgB,EAAE,gBAAgB,GAAG,IAAI,CAU5F,CAAC;AAEF,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,qBAAqB,EAAE,CACtB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,gBAAgB,KACnB,CAAC,CAAC,KAAK,EAAE,GAAG,KAAK,GAAG,CAAC,GAAG,SAAS,CAAC;IACvC,kBAAkB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,eAAe,GAAG,SAAS,CAAC;IACpE;;;;OAIG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;CAChC;AAED,yDAAyD;AACzD,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;AAEjE;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,SAIhC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,aAAa,sBAAsB,KAAG,SAAS,GAAG,IAChB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"perform_action.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/perform_action.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"perform_action.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/perform_action.ts"],"names":[],"mappings":"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"}
@@ -64,7 +64,7 @@ import { resolve_issues_invalid_reason } from '../schema_meta.js';
64
64
  import { to_input_params } from '../http/schema_helpers.js';
65
65
  import { rate_limit_ip_key } from '../http/ip_canonical.js';
66
66
  import { dispatch_with_post_commit_rollback } from '../http/pending_effects.js';
67
- import { JSONRPC_VERSION } from '../http/jsonrpc.js';
67
+ import { JSONRPC_VERSION, is_jsonrpc_error_code } from '../http/jsonrpc.js';
68
68
  import { jsonrpc_error_messages, jsonrpc_error_code_to_http_status, http_status_to_jsonrpc_error_code, JSONRPC_ERROR_CODES, dev_only } from '../http/jsonrpc_errors.js';
69
69
  import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS, ERROR_CREDENTIAL_TYPE_REQUIRED, ERROR_TOKEN_SCOPE_REQUIRED } from '../http/error_schemas.js';
70
70
  import { is_public_auth, needs_actor, parse_acting } from '../http/auth_shape.js';
@@ -197,11 +197,14 @@ export const perform_action = async (input, deps) => {
197
197
  return await dispatch_with_post_commit_rollback(post_commit_effects, () => use_transaction ? db.transaction((tx) => execute(tx)) : execute(db));
198
198
  }
199
199
  catch (err) {
200
- // Duck-type check: Error with numeric `code` signals a JSON-RPC error.
201
- // Avoids cross-realm `instanceof` misses when consumers throw their own
202
- // `ThrownJsonrpcError` (structurally identical, different class identity).
200
+ // Duck-type check: an Error whose `code` is a JSON-RPC error code signals
201
+ // a JSON-RPC error. Avoids cross-realm `instanceof` misses when consumers
202
+ // throw their own `ThrownJsonrpcError` (structurally identical, different
203
+ // class identity). The code must be a valid JSON-RPC one, not just a
204
+ // number — a `DOMException` (`code` 20, 23, ...) is an internal error,
205
+ // logged and masked like any other.
203
206
  const error_like = err;
204
- if (err instanceof Error && typeof error_like.code === 'number') {
207
+ if (err instanceof Error && is_jsonrpc_error_code(error_like.code)) {
205
208
  const code = error_like.code;
206
209
  const status = jsonrpc_error_code_to_http_status(code);
207
210
  const error = { code, message: err.message };
@@ -54,6 +54,12 @@
54
54
  * admits a revoked credential; `docs/security.md` §Connection Admission says
55
55
  * what that rests on.
56
56
  *
57
+ * The acting actor the upgrade resolved is re-read beside the credential on
58
+ * every endpoint, gated or not: an actor soft-deleted under its live account
59
+ * keeps the account's sessions and tokens and its own grant rows, so neither
60
+ * the credential nor the role re-read would refuse it. A tombstoned acting
61
+ * actor is refused like a dead credential.
62
+ *
57
63
  * An endpoint gated on `required_roles` re-reads them beside the credential:
58
64
  * its upgrade passed the role gate before the socket registered, so a role
59
65
  * revocation landing in between would close nothing (`close_sockets_for_role`
@@ -62,9 +68,9 @@
62
68
  * admits on a global grant of any listed role, as the gate did.
63
69
  *
64
70
  * A refused upgrade is closed with `WS_CLOSE_SESSION_REVOKED` (4001, the
65
- * reason a revoked socket gets) for a dead credential,
71
+ * reason a revoked socket gets) for a dead credential or a deleted acting actor,
66
72
  * `WS_CLOSE_PERMISSION_REVOKED` (4006) for a lost role, or
67
- * `WS_CLOSE_INTERNAL_ERROR` (1011) when either re-read itself failed — fail
73
+ * `WS_CLOSE_INTERNAL_ERROR` (1011) when a re-read itself failed — fail
68
74
  * closed, never admitted unchecked. The handshake has already answered `101`,
69
75
  * so a refusal can only be a close frame.
70
76
  *
@@ -1 +1 @@
1
- {"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"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"}
@@ -54,6 +54,12 @@
54
54
  * admits a revoked credential; `docs/security.md` §Connection Admission says
55
55
  * what that rests on.
56
56
  *
57
+ * The acting actor the upgrade resolved is re-read beside the credential on
58
+ * every endpoint, gated or not: an actor soft-deleted under its live account
59
+ * keeps the account's sessions and tokens and its own grant rows, so neither
60
+ * the credential nor the role re-read would refuse it. A tombstoned acting
61
+ * actor is refused like a dead credential.
62
+ *
57
63
  * An endpoint gated on `required_roles` re-reads them beside the credential:
58
64
  * its upgrade passed the role gate before the socket registered, so a role
59
65
  * revocation landing in between would close nothing (`close_sockets_for_role`
@@ -62,9 +68,9 @@
62
68
  * admits on a global grant of any listed role, as the gate did.
63
69
  *
64
70
  * A refused upgrade is closed with `WS_CLOSE_SESSION_REVOKED` (4001, the
65
- * reason a revoked socket gets) for a dead credential,
71
+ * reason a revoked socket gets) for a dead credential or a deleted acting actor,
66
72
  * `WS_CLOSE_PERMISSION_REVOKED` (4006) for a lost role, or
67
- * `WS_CLOSE_INTERNAL_ERROR` (1011) when either re-read itself failed — fail
73
+ * `WS_CLOSE_INTERNAL_ERROR` (1011) when a re-read itself failed — fail
68
74
  * closed, never admitted unchecked. The handshake has already answered `101`,
69
75
  * so a refusal can only be a close frame.
70
76
  *
@@ -116,6 +122,7 @@ import { wait } from '@fuzdev/fuz_util/async.ts';
116
122
  import { Logger } from '@fuzdev/fuz_util/log.ts';
117
123
  import { get_request_context, has_any_scoped_role, refresh_role_grants, require_request_context, token_scope_surface_denial } from '../auth/request_context.js';
118
124
  import { get_resolved_auth, revalidate_resolved_auth } from '../auth/resolved_auth.js';
125
+ import { query_active_actors_by_account } from '../auth/account_queries.js';
119
126
  import { schedule_credential_deadline } from '../auth/credential_deadline.js';
120
127
  import { get_client_ip } from '../http/client_ip.js';
121
128
  import { flush_pending_effects, flush_post_commit_effects } from '../http/pending_effects.js';
@@ -562,14 +569,22 @@ export const register_action_ws = (options) => {
562
569
  }
563
570
  if (!upgrade_preset) {
564
571
  let credential_is_live;
572
+ let actor_is_active = true;
565
573
  let role_is_held = true;
566
574
  try {
567
575
  // pool-level `db`: the statements must see every revocation
568
576
  // committed before they start
569
577
  credential_is_live = await revalidate_resolved_auth({ db }, resolved);
578
+ // The acting actor the upgrade resolved, on every endpoint: a
579
+ // soft-deleted actor keeps its account's credential and its
580
+ // grant rows, so neither re-read above or below refuses it.
581
+ const acting_actor = upgrade_context.actor;
582
+ if (credential_is_live && acting_actor) {
583
+ actor_is_active = (await query_active_actors_by_account({ db }, account_id)).some((a) => a.id === acting_actor.id);
584
+ }
570
585
  // Global grants only, like the gate in front: a scoped grant
571
586
  // never admitted this socket.
572
- if (credential_is_live && required_roles.length > 0) {
587
+ if (credential_is_live && actor_is_active && required_roles.length > 0) {
573
588
  role_is_held = has_any_scoped_role(await refresh_role_grants(upgrade_context, { db }), required_roles, null);
574
589
  }
575
590
  }
@@ -581,7 +596,7 @@ export const register_action_ws = (options) => {
581
596
  log.info('ws upgrade admission: re-check ended by the shutdown', error);
582
597
  }
583
598
  else {
584
- log.error('ws upgrade admission: credential or role re-check failed', error);
599
+ log.error('ws upgrade admission: credential, actor, or role re-check failed', error);
585
600
  }
586
601
  refuse_upgrade(ws, WS_CLOSE_INTERNAL_ERROR, 'internal error');
587
602
  return null;
@@ -591,6 +606,11 @@ export const register_action_ws = (options) => {
591
606
  refuse_upgrade(ws, WS_CLOSE_SESSION_REVOKED, WS_CLOSE_SESSION_REVOKED_REASON);
592
607
  return null;
593
608
  }
609
+ if (!actor_is_active) {
610
+ log.info('ws upgrade admission: acting actor deleted during the upgrade', connection_id, account_id);
611
+ refuse_upgrade(ws, WS_CLOSE_SESSION_REVOKED, WS_CLOSE_SESSION_REVOKED_REASON);
612
+ return null;
613
+ }
594
614
  if (!role_is_held) {
595
615
  log.info('ws upgrade admission: role revoked during the upgrade', connection_id, account_id);
596
616
  refuse_upgrade(ws, WS_CLOSE_PERMISSION_REVOKED, WS_CLOSE_PERMISSION_REVOKED_REASON);
@@ -77,10 +77,12 @@ export type AuditEventHandler = (event: AuditLogEvent) => void;
77
77
  * - `none` → nothing — every other event, and any event type fuz_app does not
78
78
  * define
79
79
  *
80
- * Ignores `outcome === 'failure'` events — they carry attacker-controlled
81
- * identifiers (e.g. a `session_revoke` that the DB rejected still records
82
- * the submitted session_id), so reacting to them would let any authenticated
83
- * user close another user's socket by guessing a session hash or token id.
80
+ * Acts only on `outcome === 'success'` events — a failure carries
81
+ * attacker-controlled identifiers (e.g. a `session_revoke` that the DB
82
+ * rejected still records the submitted session_id), so reacting to it would
83
+ * let any authenticated user close another user's socket by guessing a
84
+ * session hash or token id. Matching the success rather than skipping the
85
+ * failure keeps an outcome outside the enum inert, as on the Rust spine.
84
86
  *
85
87
  * @param closer - what to close on — a `BackendWebsocketTransport`, or a
86
88
  * `RealtimeCloser` to reach every transport from one listener
@@ -1 +1 @@
1
- {"version":3,"file":"transports_ws_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,gBAAgB,EAAE,KAAK,MAAM,KAAG,iBA4D5E,CAAC"}
1
+ {"version":3,"file":"transports_ws_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,gBAAgB,EAAE,KAAK,MAAM,KAAG,iBA6D5E,CAAC"}
@@ -66,10 +66,12 @@ import { to_global_role_revocation, to_revocation_scope } from '../auth/audit_lo
66
66
  * - `none` → nothing — every other event, and any event type fuz_app does not
67
67
  * define
68
68
  *
69
- * Ignores `outcome === 'failure'` events — they carry attacker-controlled
70
- * identifiers (e.g. a `session_revoke` that the DB rejected still records
71
- * the submitted session_id), so reacting to them would let any authenticated
72
- * user close another user's socket by guessing a session hash or token id.
69
+ * Acts only on `outcome === 'success'` events — a failure carries
70
+ * attacker-controlled identifiers (e.g. a `session_revoke` that the DB
71
+ * rejected still records the submitted session_id), so reacting to it would
72
+ * let any authenticated user close another user's socket by guessing a
73
+ * session hash or token id. Matching the success rather than skipping the
74
+ * failure keeps an outcome outside the enum inert, as on the Rust spine.
73
75
  *
74
76
  * @param closer - what to close on — a `BackendWebsocketTransport`, or a
75
77
  * `RealtimeCloser` to reach every transport from one listener
@@ -81,8 +83,9 @@ import { to_global_role_revocation, to_revocation_scope } from '../auth/audit_lo
81
83
  */
82
84
  export const create_ws_auth_guard = (closer, log) => {
83
85
  return (event) => {
84
- // Failed mutations carry attacker-controlled metadata — never act on them.
85
- if (event.outcome === 'failure')
86
+ // only a committed revocation closes — failed mutations carry
87
+ // attacker-controlled metadata, and anything else is not a revocation
88
+ if (event.outcome !== 'success')
86
89
  return;
87
90
  const scope = to_revocation_scope(event.event_type);
88
91
  switch (scope) {
@@ -35,7 +35,7 @@ export interface WsEndpointSpec {
35
35
  * `AppServerOptions.allowed_origins`, the list the HTTP routes check.
36
36
  * Set it only to narrow one endpoint: the auth middleware's origin check
37
37
  * runs on the upgrade too, so a request reaches the endpoint only when
38
- * its origin passes both lists.
38
+ * its origin passes both lists. An empty list throws at assembly.
39
39
  */
40
40
  allowed_origins?: ReadonlyArray<RegExp>;
41
41
  /**
@@ -37,7 +37,7 @@ over the pool so rows persist when request transactions roll back. See root
37
37
  - `auth/password_argon2.ts` — `hash_password`, `verify_password`, `verify_dummy`, `argon2_password_deps`.
38
38
  - `auth/api_token.ts` — `API_TOKEN_PREFIX` (`secret_fuz_token_`), `hash_api_token`, `generate_api_token`, and the shape precheck `is_api_token_shaped` (`API_TOKEN_REGEX`: the prefix plus exactly `API_TOKEN_BODY_LENGTH` = 43 base64url chars).
39
39
  - `auth/daemon_token.ts` — `DaemonToken`, `DAEMON_TOKEN_HEADER` (`X-Daemon-Token`), `generate_daemon_token`, `validate_daemon_token`, `DaemonTokenState`.
40
- - `auth/bootstrap_account.ts` — `bootstrap_account` (one-shot, `bootstrap_lock`-protected).
40
+ - `auth/bootstrap_account.ts` — `bootstrap_account` (one-shot, `bootstrap_lock`-protected) + `read_bootstrap_token`, the read both it and `check_bootstrap_status` use: the secure read less one trailing LF, a token under `BOOTSTRAP_TOKEN_MIN_BYTES` (32) refused as unreadable with an error log.
41
41
 
42
42
  Cross-cutting notes that don't live on any single symbol:
43
43
 
@@ -77,8 +77,8 @@ Convention — `*_schema.ts` is Zod-only; `*_ddl.ts` holds DDL strings.
77
77
  All take `deps: QueryDeps = {db}` first; `query_validate_api_token` adds `log` and returns a `ValidatedApiToken` — the row with `scope` parsed, refusing (returning `undefined` for) a row whose stored scope is unreadable.
78
78
 
79
79
  - `auth/account_queries.ts` — account CRUD, actor resolution, password update with verify-write race guard, paged `query_admin_account_list`.
80
- - `auth/actor_lookup_queries.ts` — batched `actor` ⨝ `account` for the labels arc.
81
- - `auth/actor_search_queries.ts` — case-insensitive prefix search on `actor.name`, scope-filtered when not admin.
80
+ - `auth/actor_lookup_queries.ts` — batched `actor` ⨝ `account` for the labels arc; active actors of active accounts only.
81
+ - `auth/actor_search_queries.ts` — case-insensitive prefix search on `actor.name`, scope-filtered when not admin; active actors of active accounts only.
82
82
  - `auth/role_grant_queries.ts` — idempotent create, IDOR-guarded revoke (with in-tx supersede), scope-aware lookup, role/account predicates, `query_role_grant_revoke_for_scope` parent-scope cascade.
83
83
  - `auth/role_grant_offer_queries.ts` — offer create/decline/retract/list/history/sweep, atomic `query_accept_offer` with sibling supersede; error classes `RoleGrantOfferSelfTargetError` / `_AlreadyTerminalError` / `_ExpiredError` / `_NotFoundError` / `_ActorAccountMismatchError` / `_ActorMismatchError`.
84
84
  - `auth/session_queries.ts` — server-side sessions (blake3-hashed), `query_session_revoke_by_hash_unscoped` (logout only), `query_session_enforce_limit` (transaction-required; returns the evicted sessions' ids — their token hashes — so the caller closes their connections after the commit), `query_session_cleanup_expired` (returns the swept sessions' ids for the same close — `cleanup_expired_sessions` is its caller). No touch/renewal query — `expires_at` is an absolute cap set at mint (`AUTH_SESSION_LIFETIME_MS`).
@@ -147,12 +147,16 @@ the next event, and one removed mid-fan-out still hears the in-flight event
147
147
  **failure** row is announced as soon as it is written — the attempt happened
148
148
  whatever the transaction does; a **success** row is announced only after the
149
149
  request's transaction commits (a thunk on `ctx.post_commit_effects` that awaits
150
- the write) and never when the handler throws. The listeners act on a success
151
- row — the revocation guards close connections, the audit stream broadcasts it
152
- — so announcing one before its commit would close while the revoked credential
153
- still reads as valid, and a rolled-back revocation would close anyway. The
154
- Rust spine gets the same order by writing a success row in the transaction and
155
- queueing its fan-out behind the commit. `AuditEmitterContext` is therefore
150
+ the write; after the handler returns, on a route without one) and never when
151
+ the handler throws. The listeners act on a success row — the revocation guards
152
+ close connections, the audit stream broadcasts it — so announcing one before
153
+ its commit would close while the revoked credential still reads as valid, and
154
+ a rolled-back revocation would close anyway. A route that runs without a
155
+ request transaction and opens its own around just its writes (`/login`,
156
+ `/password`) emits its success row after that transaction commits. The Rust
157
+ spine follows the same order: a success row emitted inside a request
158
+ transaction is written in it with its fan-out queued behind the commit, and
159
+ `/login` and `/password` emit after their own commit. `AuditEmitterContext` is therefore
156
160
  `{pending_effects, post_commit_effects}` — `RouteContext` and `ActionContext`
157
161
  both satisfy it.
158
162
 
@@ -247,8 +251,9 @@ consumer's `validate_data` pass-through callback on `CellActionDeps`.
247
251
  - `auth/cell_action_specs.ts` — `CellVisibility` enum, `CellPath` brand,
248
252
  `CELL_KIND_LENGTH_MAX` (64 — the wire `kind` on `cell_create` and the
249
253
  `cell_list` filter; the tag is copied into the cell's audit rows),
250
- `CellJson`, the generic verb specs (`cell_create / get / update /
251
- delete / list / clone / moderate`), error reasons, and `all_cell_action_specs`
254
+ `CellJson`, `CellPolicy` (a root's contribution policy, `z.looseObject`), the
255
+ generic verb specs (`cell_create / get / update / delete / list / clone /
256
+ moderate / policy_set`), error reasons, and `all_cell_action_specs`
252
257
  (spreads in the grant / field / item / audit registries so codegen + UI
253
258
  see one cell namespace). `CellJson` has no hub axis; `path` is global. The
254
259
  directory-tree columns `parent_id` / `root_id` (containment, immutable in v1)
@@ -311,24 +316,39 @@ auth, ids)`: two queries for the whole id-set, the cells read through the
311
316
  the mounted `authorize_create` for **every** cell it writes as a parentless
312
317
  create — a denial is the same 403 `cell_create_forbidden`, so clone is no
313
318
  side door past the creation policy. The optional
314
- parent-aware `authorize_create` dep (`CellCreateAuthorize` — wider input
315
- `{kind, data, parent_id, root_id, root_data, scope_id}` than `validate_data`,
316
- returning a `CellCreateVerdict` `{allow:false} | {allow:true, moderation_required}`)
319
+ parent-aware `authorize_create` dep (`CellCreateAuthorize` — an object whose
320
+ `authorize` takes a wider input `{kind, data, parent_id, root_id, root_policy,
321
+ scope_id}` than `validate_data` and returns a `CellCreateVerdict`
322
+ `{allow:false} | {allow:true, moderation_required}`, and whose optional
323
+ `validate_policy` accepts or refuses a root policy write)
317
324
  gates create by capability; it runs after the validator + the parent-resolve
318
325
  (which **404**s `cell_not_found` on a missing or soft-deleted root as on a
319
326
  missing parent, authorizer or not).
320
327
  A **viewable**-parent / root-creation deny is **403** `cell_create_forbidden`
321
328
  (the create-path **404** mask is reserved for a hidden parent); `Allow` folds
322
329
  the moderation outcome (born `pending`+private vs `approved`+author-visibility).
323
- The authorizer is **pure** — the handler reads the governing root's `data`
324
- in-tx and hands it over as `root_data`, so a directory policy resolves
325
- `root.data.policy[kind]` without its own DB read (which also dodges the
326
- single-connection deadlock of the in-memory drivers). `None`/omitted = open create (the default;
327
- mirrors `validate_data`). `cell_moderate` is the `pending → approved | rejected`
328
- transition, gated on `can_manage_cell` of the **governing root** (admin / root
329
- owner — the author lands a 403, the anti-self-approval guard; a root owner
330
- reaches the check through the steward arm); approve flips
331
- `moderation → approved` + `visibility → public`, reject sets `rejected`.
330
+ The authorizer is **pure** — the handler reads the governing root in-tx and
331
+ hands its `policy` column over as `root_policy`, so a directory policy
332
+ resolves `root_policy[kind]` without its own DB read (which also dodges the
333
+ single-connection deadlock of the in-memory drivers). `None`/omitted = open
334
+ create (the default; mirrors `validate_data`), and every non-null policy write
335
+ refused `cell_policy_unsupported`. A root's `policy` (a JSON object, roots only
336
+ by table CHECK, opaque to the spine, readable on `CellJson`) is set at a
337
+ root's `cell_create` and changed only by `cell_policy_set` — the 404 mask, then
338
+ manage-tier (403 `cell_policy_manage_only`, an editor grant included), then
339
+ root-only (400 `cell_policy_root_only`), then the authorizer's
340
+ `validate_policy`; audited with the new and the previous policy. `cell_update`
341
+ never writes it (an editor can rewrite `data`, so a policy read from `data`
342
+ would be the editor's to loosen), and a clone is born without one.
343
+ `cell_moderate` is the `pending → approved | rejected` transition (plus
344
+ `approved → rejected`, the takedown), gated on
345
+ `can_manage_cell` of the **governing root** (admin / root owner — the author
346
+ lands a 403, the anti-self-approval guard; a root owner reaches the check
347
+ through the steward arm); approve flips
348
+ `moderation → approved` + `visibility → public`, reject sets `rejected`
349
+ (visibility untouched). The write carries the from-state: approve moves only
350
+ a pending contribution, reject a pending or approved one (the takedown); any
351
+ other transition answers 400 `cell_not_pending`.
332
352
  - `auth/cell_ref_admission.ts` — `require_new_refs_held`, the
333
353
  possession gate `cell_create` / `cell_update` / `cell_clone` run last before
334
354
  their write: a non-admin write introducing a fact ref (beyond the cell's
@@ -340,7 +360,10 @@ auth, ids)`: two queries for the whole id-set, the cells read through the
340
360
  - `auth/cell_grant_actions.ts` / `cell_field_actions.ts` /
341
361
  `cell_item_actions.ts` / `cell_audit_actions.ts` — the relation + ACL +
342
362
  audit handlers, exporting `to_grant_json` / `to_field_json` /
343
- `to_item_json` reused by the `cell_get` bundle.
363
+ `to_item_json` reused by the `cell_get` bundle. `cell_grant_create` takes an
364
+ optional `authorize_grant_create` (`CellGrantCreateAuthorize`), asked of a
365
+ manager after the spine's gates and before the write (a level change too);
366
+ `false` is 403 `cell_grant_not_authorized`, nothing written.
344
367
  - `auth/all_cell_actions.ts` — `create_all_cell_actions(deps, {roles})`, the
345
368
  handler-side twin of `all_cell_action_specs` and sibling of
346
369
  `create_standard_rpc_actions`: bundles all five cell factories (CRUD + grant
@@ -357,8 +380,8 @@ auth, ids)`: two queries for the whole id-set, the cells read through the
357
380
  Consumers spread it into `extra_events` to register the whole layer in
358
381
  one call (aggregator by design, not a compat shim).
359
382
 
360
- The cell event types (`cell_create` / `_update` / `_delete` / `_clone`,
361
- `cell_grant_create` / `_revoke`, `cell_field_set` / `_delete`,
383
+ The cell event types (`cell_create` / `_update` / `_delete` / `_clone` /
384
+ `_moderate`, `cell_policy_set`, `cell_grant_create` / `_revoke`, `cell_field_set` / `_delete`,
362
385
  `cell_item_insert` / `_move` / `_delete`) are consumer-registered extras,
363
386
  not built-ins — see §Audit event extensibility.
364
387
 
@@ -642,8 +665,8 @@ omits `confirm: true`, `ERROR_CANNOT_DELETE_KEEPER` (403) when `account_delete`
642
665
  `account_purge` targets a keeper-role account (the keeper account is never
643
666
  API-removable — auth + daemon-token both pivot on it),
644
667
  `ERROR_CANNOT_DELETE_LAST_ADMIN` (403) when `account_delete` / `account_purge`
645
- targets the sole remaining active admin (keeper-recoverable, but not in one
646
- click; soft-deleted admins don't count), `ERROR_INVITE_ACCOUNT_EXISTS_USERNAME` /
668
+ targets the sole remaining active admin (soft-deleted admins don't count; the
669
+ count locks its rows, so concurrent removals can't both pass), `ERROR_INVITE_ACCOUNT_EXISTS_USERNAME` /
647
670
  `_EMAIL` / `ERROR_INVITE_DUPLICATE` on invite create, `ERROR_INVITE_NOT_FOUND`
648
671
  on invite delete. `invite_create` empty input is rejected at the schema via
649
672
  `.refine()` and surfaces as `invalid_params` with `error.data.issues`.
@@ -694,6 +717,15 @@ event additionally records `credential_type` in metadata (defense in depth).
694
717
  - `role_grant_offer_list_action_spec` — input `{account_id?}`; output `{offers}`.
695
718
  - `role_grant_offer_history_action_spec` — input `{account_id?, limit?, offset?}`; output `{offers}`.
696
719
  - `role_grant_revoke_action_spec` — input `{actor_id, role_grant_id, reason?}`; output `{ok, revoked}`.
720
+ A global `admin` grant is guarded: the keeper account's is refused
721
+ (`ERROR_CANNOT_REVOKE_KEEPER_ADMIN`, 403) and so is one whose revoke would
722
+ leave no active admin account (`ERROR_CANNOT_REVOKE_LAST_ADMIN`, 403, under
723
+ the locked count `query_count_active_accounts_with_global_role_for_update`,
724
+ which leaves the revoked grant out). Besides the
725
+ revokee's incoming sibling offers, the revoke supersedes every pending offer
726
+ the revokee's actor made (`query_role_grant_offer_supersede_outgoing`) —
727
+ `account_delete` / `account_purge` do the same for every actor of the account
728
+ (`reason: 'account_deleted'`, audited, no notification).
697
729
  - `role_grant_assign_action_spec` — global-admin-only immediate conferral (no
698
730
  consent step); input `{to_account_id, to_actor_id?, role, scope_kind?, scope_id?}`
699
731
  (the pair as on offer-create); output `{ok, role_grant_id}`. The handler:
@@ -702,6 +734,9 @@ event additionally records `credential_type` in metadata (defense in depth).
702
734
  `admin` (`invalid_params` + `ERROR_ROLE_GRANT_BUILTIN_SCOPED`; a scoped
703
735
  `keeper` stays 403 `ERROR_ROLE_NOT_WEB_GRANTABLE`, since `keeper` is never
704
736
  web-grantable);
737
+ - runs the consumer's `RoleGrantOfferCreateAuthorize` callback (the offer
738
+ policy binds the consent-free path too — a refusal is 403
739
+ `ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED`, failure-audited);
705
740
  - resolves the target actor (named `to_actor_id` ∈ the account's active
706
741
  actors, else its sole active actor — multi-actor → `invalid_params`,
707
742
  actorless → 404);
@@ -711,6 +746,7 @@ event additionally records `credential_type` in metadata (defense in depth).
711
746
 
712
747
  No WS notification in v1. Reasons: `ERROR_INSUFFICIENT_PERMISSIONS`,
713
748
  `ERROR_ROLE_NOT_WEB_GRANTABLE`, `ERROR_ROLE_GRANT_BUILTIN_SCOPED`,
749
+ `ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED`,
714
750
  `ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH`.
715
751
 
716
752
  Every input carries `acting?: ActingActor` (registry-time invariant 2).
@@ -726,7 +762,9 @@ holder-propagation (holding the offered role confers no power to offer it). The
726
762
  global check (`has_scoped_role(_, ROLE_ADMIN, null)`) stops a scoped `admin`
727
763
  escalating into global authority over the offer surface. A consumer needing
728
764
  scope-aware delegation (e.g. a classroom teacher offering within their own
729
- scope) supplies a custom `authorize` callback.
765
+ scope) supplies a custom `authorize` callback. The callback receives
766
+ `{to_account_id, role, scope_kind, scope_id}` and also gates
767
+ `role_grant_assign`.
730
768
 
731
769
  **Builtin roles (`admin` / `keeper`) are global-only**, decided by the spine
732
770
  ahead of any callback. Offer-create refuses a scoped `admin`
@@ -778,7 +816,7 @@ Post-commit via `emit_after_commit` (see `http/CLAUDE.md` §Pending Effects):
778
816
  - Retract — `role_grant_offer_retracted` → recipient.
779
817
  - Accept — `role_grant_offer_accepted` → grantor + `_supersede` per sibling.
780
818
  - Decline — `role_grant_offer_declined` → grantor.
781
- - Revoke — `role_grant_revoke` → revokee + `_supersede` per superseded sibling.
819
+ - Revoke — `role_grant_revoke` → revokee + `_supersede` per superseded offer (the revokee's incoming siblings and its own outgoing offers, each to its grantor).
782
820
 
783
821
  Spec module is `auth/role_grant_offer_notifications.ts` — six
784
822
  `RemoteNotificationActionSpec`s with Zod params schemas and notification
@@ -832,7 +870,7 @@ required `connection_closer`. Each revoking handler queues its close
832
870
  after the handler's transaction commits and is dropped if it throws;
833
871
  `account_token_create` queues a token close for each token its cap evicts.
834
872
  Failure outcomes (`revoked: false`) queue no close — mirrors the listeners'
835
- `outcome === 'failure'` guard so attacker-guessable ids can't target
873
+ `outcome === 'success'` guard so attacker-guessable ids can't target
836
874
  arbitrary connections.
837
875
 
838
876
  ### Standard RPC bundle
@@ -37,8 +37,9 @@ export interface ActorLookupRow {
37
37
  }
38
38
  /**
39
39
  * Resolve a batch of actor ids to `(id, username, display_name)`. Empty
40
- * input fast-paths to `[]`. Hard-deleted actors (or account-cascade-
41
- * orphaned rows) drop out of the result silently.
40
+ * input fast-paths to `[]`. Hard-deleted and soft-deleted actors — and the
41
+ * actors of a soft-deleted account — drop out of the result silently,
42
+ * indistinguishable from never-existed.
42
43
  */
43
44
  export declare const query_actors_by_ids: (deps: QueryDeps, ids: ReadonlyArray<Uuid>) => Promise<Array<ActorLookupRow>>;
44
45
  //# sourceMappingURL=actor_lookup_queries.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"actor_lookup_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/actor_lookup_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC9B,EAAE,EAAE,IAAI,CAAC;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,KAAK,aAAa,CAAC,IAAI,CAAC,KACtB,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAS/B,CAAC"}
1
+ {"version":3,"file":"actor_lookup_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/actor_lookup_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,iFAAiF;AACjF,MAAM,WAAW,cAAc;IAC9B,EAAE,EAAE,IAAI,CAAC;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,KAAK,aAAa,CAAC,IAAI,CAAC,KACtB,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAW/B,CAAC"}
@@ -29,8 +29,9 @@
29
29
  */
30
30
  /**
31
31
  * Resolve a batch of actor ids to `(id, username, display_name)`. Empty
32
- * input fast-paths to `[]`. Hard-deleted actors (or account-cascade-
33
- * orphaned rows) drop out of the result silently.
32
+ * input fast-paths to `[]`. Hard-deleted and soft-deleted actors — and the
33
+ * actors of a soft-deleted account — drop out of the result silently,
34
+ * indistinguishable from never-existed.
34
35
  */
35
36
  export const query_actors_by_ids = async (deps, ids) => {
36
37
  if (ids.length === 0)
@@ -38,5 +39,7 @@ export const query_actors_by_ids = async (deps, ids) => {
38
39
  return deps.db.query(`SELECT act.id, a.username, act.name AS display_name
39
40
  FROM actor act
40
41
  JOIN account a ON a.id = act.account_id
41
- WHERE act.id = ANY($1)`, [ids]);
42
+ WHERE act.id = ANY($1)
43
+ AND act.deleted_at IS NULL
44
+ AND a.deleted_at IS NULL`, [ids]);
42
45
  };