@fuzdev/fuz_app 0.117.1 → 0.119.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 (179) hide show
  1. package/dist/actions/CLAUDE.md +301 -73
  2. package/dist/actions/action_rpc.d.ts.map +1 -1
  3. package/dist/actions/action_rpc.js +16 -1
  4. package/dist/actions/connection_closer.d.ts +136 -51
  5. package/dist/actions/connection_closer.d.ts.map +1 -1
  6. package/dist/actions/connection_closer.js +133 -34
  7. package/dist/actions/register_action_ws.d.ts +176 -24
  8. package/dist/actions/register_action_ws.d.ts.map +1 -1
  9. package/dist/actions/register_action_ws.js +581 -241
  10. package/dist/actions/register_ws_endpoint.d.ts +1 -0
  11. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  12. package/dist/actions/register_ws_endpoint.js +1 -0
  13. package/dist/actions/socket.svelte.d.ts +43 -9
  14. package/dist/actions/socket.svelte.d.ts.map +1 -1
  15. package/dist/actions/socket.svelte.js +59 -8
  16. package/dist/actions/transports.d.ts +55 -1
  17. package/dist/actions/transports.d.ts.map +1 -1
  18. package/dist/actions/transports.js +55 -1
  19. package/dist/actions/transports_ws_auth_guard.d.ts +53 -67
  20. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  21. package/dist/actions/transports_ws_auth_guard.js +87 -111
  22. package/dist/actions/transports_ws_backend.d.ts +212 -30
  23. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  24. package/dist/actions/transports_ws_backend.js +327 -70
  25. package/dist/actions/ws_endpoint_spec.d.ts +26 -13
  26. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  27. package/dist/auth/CLAUDE.md +125 -31
  28. package/dist/auth/account_actions.d.ts +12 -18
  29. package/dist/auth/account_actions.d.ts.map +1 -1
  30. package/dist/auth/account_actions.js +47 -33
  31. package/dist/auth/account_routes.d.ts +5 -11
  32. package/dist/auth/account_routes.d.ts.map +1 -1
  33. package/dist/auth/account_routes.js +40 -36
  34. package/dist/auth/account_schema.d.ts +27 -0
  35. package/dist/auth/account_schema.d.ts.map +1 -1
  36. package/dist/auth/account_schema.js +22 -0
  37. package/dist/auth/admin_actions.d.ts +8 -17
  38. package/dist/auth/admin_actions.d.ts.map +1 -1
  39. package/dist/auth/admin_actions.js +33 -21
  40. package/dist/auth/api_token_queries.d.ts +23 -2
  41. package/dist/auth/api_token_queries.d.ts.map +1 -1
  42. package/dist/auth/api_token_queries.js +39 -3
  43. package/dist/auth/audit_emitter.d.ts +72 -44
  44. package/dist/auth/audit_emitter.d.ts.map +1 -1
  45. package/dist/auth/audit_emitter.js +60 -16
  46. package/dist/auth/audit_log_route_schema.js +7 -7
  47. package/dist/auth/audit_log_routes.d.ts +31 -6
  48. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  49. package/dist/auth/audit_log_routes.js +85 -11
  50. package/dist/auth/audit_log_schema.d.ts +48 -0
  51. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  52. package/dist/auth/audit_log_schema.js +58 -0
  53. package/dist/auth/bootstrap_account.d.ts +3 -2
  54. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  55. package/dist/auth/bootstrap_account.js +3 -3
  56. package/dist/auth/cleanup.d.ts +219 -30
  57. package/dist/auth/cleanup.d.ts.map +1 -1
  58. package/dist/auth/cleanup.js +260 -45
  59. package/dist/auth/deps.d.ts +29 -1
  60. package/dist/auth/deps.d.ts.map +1 -1
  61. package/dist/auth/migrations.d.ts.map +1 -1
  62. package/dist/auth/migrations.js +27 -0
  63. package/dist/auth/resolved_auth.d.ts +105 -0
  64. package/dist/auth/resolved_auth.d.ts.map +1 -0
  65. package/dist/auth/resolved_auth.js +124 -0
  66. package/dist/auth/role_grant_offer_ddl.d.ts +4 -2
  67. package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
  68. package/dist/auth/role_grant_offer_ddl.js +10 -3
  69. package/dist/auth/role_grant_offer_queries.d.ts +16 -7
  70. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  71. package/dist/auth/role_grant_offer_queries.js +33 -15
  72. package/dist/auth/role_grant_offer_schema.d.ts +9 -0
  73. package/dist/auth/role_grant_offer_schema.d.ts.map +1 -1
  74. package/dist/auth/session_middleware.d.ts +8 -1
  75. package/dist/auth/session_middleware.d.ts.map +1 -1
  76. package/dist/auth/session_middleware.js +9 -3
  77. package/dist/auth/session_queries.d.ts +31 -6
  78. package/dist/auth/session_queries.d.ts.map +1 -1
  79. package/dist/auth/session_queries.js +31 -6
  80. package/dist/auth/standard_rpc_actions.d.ts +9 -9
  81. package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
  82. package/dist/auth/standard_rpc_actions.js +3 -3
  83. package/dist/db/CLAUDE.md +7 -1
  84. package/dist/db/migrate.d.ts +8 -13
  85. package/dist/db/migrate.d.ts.map +1 -1
  86. package/dist/db/migrate.js +9 -38
  87. package/dist/db/status.d.ts +58 -7
  88. package/dist/db/status.d.ts.map +1 -1
  89. package/dist/db/status.js +205 -97
  90. package/dist/http/CLAUDE.md +7 -3
  91. package/dist/http/db_routes.d.ts +6 -2
  92. package/dist/http/db_routes.d.ts.map +1 -1
  93. package/dist/http/db_routes.js +2 -2
  94. package/dist/periodic.d.ts +80 -0
  95. package/dist/periodic.d.ts.map +1 -0
  96. package/dist/periodic.js +122 -0
  97. package/dist/realtime/sse_auth_guard.d.ts +79 -49
  98. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  99. package/dist/realtime/sse_auth_guard.js +103 -73
  100. package/dist/realtime/subscriber_registry.d.ts +119 -8
  101. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  102. package/dist/realtime/subscriber_registry.js +184 -25
  103. package/dist/runtime/deno.d.ts.map +1 -1
  104. package/dist/runtime/deno.js +9 -6
  105. package/dist/runtime/deps.d.ts +3 -2
  106. package/dist/runtime/deps.d.ts.map +1 -1
  107. package/dist/runtime/secure_file.d.ts +23 -9
  108. package/dist/runtime/secure_file.d.ts.map +1 -1
  109. package/dist/runtime/secure_file.js +29 -10
  110. package/dist/server/app_backend.d.ts +13 -2
  111. package/dist/server/app_backend.d.ts.map +1 -1
  112. package/dist/server/app_backend.js +5 -2
  113. package/dist/server/app_server.d.ts +55 -15
  114. package/dist/server/app_server.d.ts.map +1 -1
  115. package/dist/server/app_server.js +41 -13
  116. package/dist/testing/CLAUDE.md +140 -48
  117. package/dist/testing/app_server.d.ts +2 -2
  118. package/dist/testing/app_server.d.ts.map +1 -1
  119. package/dist/testing/app_server.js +11 -2
  120. package/dist/testing/audit_drift_guard.d.ts +3 -52
  121. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  122. package/dist/testing/audit_drift_guard.js +2 -46
  123. package/dist/testing/connection_closer_helpers.d.ts +9 -22
  124. package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
  125. package/dist/testing/connection_closer_helpers.js +11 -17
  126. package/dist/testing/cross_backend/capabilities.d.ts +67 -13
  127. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  128. package/dist/testing/cross_backend/capabilities.js +27 -12
  129. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  130. package/dist/testing/cross_backend/default_backend_configs.js +21 -0
  131. package/dist/testing/cross_backend/expected_schema.json +1 -0
  132. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  133. package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +13 -4
  134. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  135. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +4 -1
  136. package/dist/testing/cross_backend/session_cap.d.ts +13 -0
  137. package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
  138. package/dist/testing/cross_backend/session_cap.js +55 -19
  139. package/dist/testing/cross_backend/sse_round_trip.d.ts +10 -2
  140. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
  141. package/dist/testing/cross_backend/sse_round_trip.js +93 -26
  142. package/dist/testing/cross_backend/testing_server_bun.js +4 -4
  143. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  144. package/dist/testing/cross_backend/ts_spine_backend_config.js +10 -2
  145. package/dist/testing/cross_backend/ws_connection_cap.d.ts +35 -0
  146. package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -0
  147. package/dist/testing/cross_backend/ws_connection_cap.js +131 -0
  148. package/dist/testing/cross_backend/ws_round_trip.d.ts +2 -1
  149. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  150. package/dist/testing/cross_backend/ws_round_trip.js +156 -19
  151. package/dist/testing/sse_round_trip.d.ts +6 -4
  152. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  153. package/dist/testing/stubs.d.ts +1 -1
  154. package/dist/testing/stubs.d.ts.map +1 -1
  155. package/dist/testing/stubs.js +6 -4
  156. package/dist/testing/transports/sse_transport.d.ts +1 -1
  157. package/dist/testing/transports/ws_client.d.ts +7 -0
  158. package/dist/testing/transports/ws_client.d.ts.map +1 -1
  159. package/dist/testing/transports/ws_raw_client.d.ts +51 -0
  160. package/dist/testing/transports/ws_raw_client.d.ts.map +1 -0
  161. package/dist/testing/transports/ws_raw_client.js +156 -0
  162. package/dist/testing/transports/ws_transport.d.ts +28 -0
  163. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  164. package/dist/testing/transports/ws_transport.js +45 -1
  165. package/dist/testing/ws_round_trip.d.ts +8 -3
  166. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  167. package/dist/testing/ws_round_trip.js +19 -4
  168. package/dist/ui/AdminAccounts.svelte +4 -3
  169. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  170. package/dist/ui/AdminOverview.svelte +3 -1
  171. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  172. package/dist/ui/CLAUDE.md +22 -6
  173. package/dist/ui/admin_accounts_state.svelte.d.ts +15 -1
  174. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  175. package/dist/ui/admin_accounts_state.svelte.js +16 -0
  176. package/dist/ui/auth_state.svelte.d.ts +25 -1
  177. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  178. package/dist/ui/auth_state.svelte.js +28 -1
  179. package/package.json +2 -2
@@ -11,7 +11,8 @@
11
11
  * transactional `db` (which would be rolled back with the parent on a
12
12
  * handler throw).
13
13
  *
14
- * Four methods cover every fan-out shape the auth domain needs:
14
+ * These methods cover every fan-out shape the auth domain needs
15
+ * (`add_listener` and `listener_count` manage the listener chain):
15
16
  *
16
17
  * - `emit(ctx, input)` — fire-and-forget pool write. Pushes the in-flight
17
18
  * promise onto `ctx.pending_effects` for post-response flushing. Errors are
@@ -22,23 +23,46 @@
22
23
  * `actor_id` / `account_id` / `ip` boilerplate every role-grant-shape audit
23
24
  * site repeated. Delegates to `emit`.
24
25
  * - `emit_pool(input)` — awaitable pool write for code paths without a
25
- * `pending_effects` queue (cleanup sweeps, ad-hoc maintenance scripts).
26
- * Same write-then-notify semantics as `emit`, just synchronous-with-await.
26
+ * request context (ad-hoc maintenance scripts). Writes, then notifies.
27
27
  * - `notify(event)` — fan out an already-written audit row (e.g. rows
28
28
  * returned by `query_accept_offer` that were inserted in-transaction by
29
- * the query layer). Runs every registered listener; per-listener throws
30
- * are isolated.
29
+ * the query layer, or the offer-expiry rows `auth/cleanup.ts` writes in
30
+ * its sweep transaction). Runs every registered listener; per-listener
31
+ * throws are isolated.
32
+ *
33
+ * ## When listeners hear about a row
34
+ *
35
+ * `emit` writes the row at once, on the pool, whatever its outcome — the write
36
+ * is outside the request's transaction, so it survives a rollback. Listener
37
+ * fan-out is a separate step, and it depends on the outcome:
38
+ *
39
+ * - A **failure** row is announced as soon as it is written. It records an
40
+ * attempt, which happened whether or not the request's transaction commits.
41
+ * - A **success** row is announced only after the request's transaction
42
+ * commits, and never when the handler throws: the fan-out is queued on
43
+ * `ctx.post_commit_effects` and awaits the write. A success row reports a
44
+ * state change, and the listeners act on it — the revocation listeners close
45
+ * connections, the audit stream broadcasts it. Announced before the commit,
46
+ * a close could run while the revoked credential still reads as valid, and a
47
+ * rolled-back revocation would close connections anyway.
48
+ *
49
+ * A success row emitted by a handler that later throws is still in the table —
50
+ * the write is outside the transaction — but is never announced.
51
+ *
52
+ * The Rust spine reaches the same order by writing a success row inside the
53
+ * transaction and queueing its fan-out behind the commit.
31
54
  *
32
55
  * Listeners are a documented registration seam — `create_app_server`
33
56
  * registers additional listeners via `add_listener` after the backend is
34
- * built (the factory-managed audit-log SSE, per-endpoint WS auth guards and
35
- * logout closers, any `extra_audit_handlers` on a `WsEndpointSpec`) before
36
- * the first request runs. Consumers can also register listeners directly on
37
- * the emitter they return from `audit_factory` for setups that don't pass
38
- * through `create_app_server`.
57
+ * built (the factory-managed audit-log SSE, per-endpoint WS auth guards, any
58
+ * `extra_audit_handlers` on a `WsEndpointSpec`) before the first request
59
+ * runs. Consumers can also register listeners directly on the emitter they
60
+ * return from `audit_factory` for setups that don't pass through
61
+ * `create_app_server`.
39
62
  *
40
63
  * @module
41
64
  */
65
+ import { emit_after_commit } from '../http/pending_effects.js';
42
66
  import { query_audit_log } from './audit_log_queries.js';
43
67
  import { builtin_audit_log_config } from './audit_log_schema.js';
44
68
  /**
@@ -71,17 +95,38 @@ export const create_audit_emitter = (options) => {
71
95
  }
72
96
  }
73
97
  };
74
- const emit_pool = async (input) => {
98
+ // The pool write. Never rejects: a failed write is logged and resolves
99
+ // `null`, so there is no row to announce.
100
+ const write = async (input) => {
75
101
  try {
76
- const event = await query_audit_log({ db }, input, audit_log_config);
77
- notify(event);
102
+ return await query_audit_log({ db }, input, audit_log_config);
78
103
  }
79
104
  catch (err) {
80
105
  log.error('Audit log write failed:', err);
106
+ return null;
81
107
  }
82
108
  };
109
+ const emit_pool = async (input) => {
110
+ const event = await write(input);
111
+ if (event)
112
+ notify(event);
113
+ };
83
114
  const base_emit = (ctx, input) => {
84
- ctx.pending_effects.push(emit_pool(input));
115
+ if (input.outcome === 'failure') {
116
+ // an attempt happened whether or not the transaction commits
117
+ ctx.pending_effects.push(emit_pool(input));
118
+ return;
119
+ }
120
+ // A success row is written now and announced after the commit — and
121
+ // not at all on rollback, when the queue is discarded. See the module
122
+ // doc, "When listeners hear about a row".
123
+ const written = write(input);
124
+ ctx.pending_effects.push(written.then(() => undefined));
125
+ emit_after_commit(ctx, async () => {
126
+ const event = await written;
127
+ if (event)
128
+ notify(event);
129
+ });
85
130
  };
86
131
  // The decorated `emit` is what `emit_role_grant_target` captures below
87
132
  // and what gets exposed on the returned object — both call shapes
@@ -111,8 +156,7 @@ export const create_audit_emitter = (options) => {
111
156
  // `emit_role_grant_target` calls the closed-over inner `emit`, not
112
157
  // `this.emit`, so the patch silently bypassed role-grant-shape emits.
113
158
  // Tests that need instrumentation pass `emit_decorator` so the wrap
114
- // is captured by the closure before the freeze (see
115
- // `create_emit_ordering_audit_factory`). The listener list stays
159
+ // is captured by the closure before the freeze. The listener list stays
116
160
  // closure-private; registration is append-only via `add_listener`
117
161
  // (`create_app_server` registers its SSE + WS listeners post-assembly,
118
162
  // by design).
@@ -32,13 +32,13 @@ export const create_audit_log_route_shape = (required_role = DEFAULT_AUDIT_STREA
32
32
  account: 'required',
33
33
  actor: 'required',
34
34
  roles: [required_role],
35
- // The stream is a session channel. A bearer that opens one cannot be
36
- // closed when it is revoked: `token_revoke` closes WS sockets keyed by
37
- // token, and SSE subscribers are not keyed that way, so a revoked
38
- // token would keep receiving audit rows for the life of the
39
- // connection. Gating the channel is what makes
40
- // `realtime/sse_auth_guard.ts`'s omission of `token_revoke` correct
41
- // rather than merely asserted.
35
+ // The stream is a session channel: a long-lived, instance-wide admin
36
+ // feed is not handed to a credential channel the deployment never
37
+ // chose. Widening the gate loses no close — the handler registers a
38
+ // bearer's stream under its API token id, and a token revocation closes
39
+ // by that id (`create_sse_connection_closer`, and the `token` scope in
40
+ // `create_sse_auth_guard`) — so widening is a consumer's explicit
41
+ // choice about the channel, not a hole in revocation.
42
42
  credential_types: ['session'],
43
43
  // Rule 3 — a narrowed token holds no audit feed. Same reasoning as the
44
44
  // WS upgrade: this is a long-lived server→client stream whose contents
@@ -13,19 +13,22 @@
13
13
  */
14
14
  import type { Logger } from '@fuzdev/fuz_util/log.ts';
15
15
  import type { RouteSpec } from '../http/route_spec.js';
16
- import { type SseStream, type SseNotification } from '../realtime/sse.js';
17
- import type { SubscribeOptions } from '../realtime/subscriber_registry.js';
16
+ import { type SseNotification } from '../realtime/sse.js';
17
+ import type { SubscriberRegistry } from '../realtime/subscriber_registry.js';
18
18
  /** Options for audit log route specs. */
19
19
  export interface AuditLogRouteOptions {
20
20
  /** Role required to access audit routes. Default `'admin'`. */
21
21
  required_role?: string;
22
22
  /**
23
- * When provided, includes an SSE route at `/audit/stream` for realtime audit events.
24
- * The `subscribe` function receives the stream, channels, and the subscriber's `account_id`
25
- * as an identity key — enabling `close_by_identity()` for auth revocation.
23
+ * When provided, includes an SSE route at `/audit/stream` for realtime audit
24
+ * events. The route registers each stream on `registry` under the
25
+ * subscriber's session hash (`scope`) and account id (`groups`, with the
26
+ * API token id beside it for a bearer) — the keys `close_by_identity()`
27
+ * closes on for auth revocation. An `AuditLogSse`
28
+ * (`create_audit_log_sse`, or `AppServerContext.audit_sse`) satisfies it.
26
29
  */
27
30
  stream?: {
28
- subscribe: (stream: SseStream<SseNotification>, options?: SubscribeOptions) => () => void;
31
+ registry: SubscriberRegistry<SseNotification>;
29
32
  log: Logger;
30
33
  };
31
34
  }
@@ -35,6 +38,28 @@ export interface AuditLogRouteOptions {
35
38
  * Returns an empty array when `options.stream` is not set — no REST routes
36
39
  * live here apart from the stream.
37
40
  *
41
+ * ## Admission
42
+ *
43
+ * A stream is authorized once, so the handler must not open one on a
44
+ * credential or role revoked while the request was in flight. The route's
45
+ * gates (401, credential channel, token scope, acting actor, role) run in the
46
+ * route-spec pipeline, before the handler and so before any stream is
47
+ * registered — a revocation landing after them would close nothing. So the
48
+ * handler registers the stream **pending**
49
+ * (`SubscriberRegistry.subscribe_pending`), then re-reads the credential
50
+ * (`revalidate_resolved_auth`) and the acting actor's role grants, and only
51
+ * then admits (`SubscriberRegistry.admit`). A revocation that committed before
52
+ * those reads is seen by them — `401 authentication_required` for a dead
53
+ * credential, `403 insufficient_permissions` for a lost role — and one whose
54
+ * close ran after the registration found the pending entry: admission then
55
+ * fails, and the response is a `200` whose body is the connect comment and
56
+ * nothing else — to an `EventSource`, a stream closed an instant after it
57
+ * opened (the client's reconnect is answered by the gates, with the precise
58
+ * status). A pending registration receives no audit row, counts toward no cap,
59
+ * and is removed when a re-read refuses the request. The twin of the Rust
60
+ * spine's `audit_stream_router`, which registers before its role read rather
61
+ * than repeating it.
62
+ *
38
63
  * @param options - optional stream wiring + role override
39
64
  * @returns the SSE route spec (when `options.stream` is provided) or an empty array
40
65
  */
@@ -1 +1 @@
1
- {"version":3,"file":"audit_log_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,EAAuB,KAAK,SAAS,EAAE,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC/F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAI3E,yCAAyC;AACzC,MAAM,WAAW,oBAAoB;IACpC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,MAAM,CAAC,EAAE;QACR,SAAS,EAAE,CAAC,MAAM,EAAE,SAAS,CAAC,eAAe,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;QAC1F,GAAG,EAAE,MAAM,CAAC;KACZ,CAAC;CACF;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,4BAA4B,GAAI,UAAU,oBAAoB,KAAG,KAAK,CAAC,SAAS,CA4B5F,CAAC"}
1
+ {"version":3,"file":"audit_log_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAUvD,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oCAAoC,CAAC;AAU7E,yCAAyC;AACzC,MAAM,WAAW,oBAAoB;IACpC,+DAA+D;IAC/D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE;QACR,QAAQ,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;QAC9C,GAAG,EAAE,MAAM,CAAC;KACZ,CAAC;CACF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,4BAA4B,GAAI,UAAU,oBAAoB,KAAG,KAAK,CAAC,SAAS,CAgF5F,CAAC"}
@@ -11,9 +11,12 @@
11
11
  *
12
12
  * @module
13
13
  */
14
- import { create_audit_log_route_shape } from './audit_log_route_schema.js';
14
+ import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS } from '../http/error_schemas.js';
15
+ import { AUTH_API_TOKEN_ID_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
16
+ import { create_audit_log_route_shape, DEFAULT_AUDIT_STREAM_ROLE } from './audit_log_route_schema.js';
15
17
  import { create_sse_response } from '../realtime/sse.js';
16
- import { AUTH_SESSION_TOKEN_HASH_KEY, require_request_context } from './request_context.js';
18
+ import { AUTH_SESSION_TOKEN_HASH_KEY, has_scoped_role, refresh_role_grants, require_request_context } from './request_context.js';
19
+ import { get_resolved_auth, revalidate_resolved_auth } from './resolved_auth.js';
17
20
  import { AUDIT_LOG_CHANNEL } from '../realtime/sse_auth_guard.js';
18
21
  /**
19
22
  * Create the optional audit-log SSE route spec.
@@ -21,17 +24,40 @@ import { AUDIT_LOG_CHANNEL } from '../realtime/sse_auth_guard.js';
21
24
  * Returns an empty array when `options.stream` is not set — no REST routes
22
25
  * live here apart from the stream.
23
26
  *
27
+ * ## Admission
28
+ *
29
+ * A stream is authorized once, so the handler must not open one on a
30
+ * credential or role revoked while the request was in flight. The route's
31
+ * gates (401, credential channel, token scope, acting actor, role) run in the
32
+ * route-spec pipeline, before the handler and so before any stream is
33
+ * registered — a revocation landing after them would close nothing. So the
34
+ * handler registers the stream **pending**
35
+ * (`SubscriberRegistry.subscribe_pending`), then re-reads the credential
36
+ * (`revalidate_resolved_auth`) and the acting actor's role grants, and only
37
+ * then admits (`SubscriberRegistry.admit`). A revocation that committed before
38
+ * those reads is seen by them — `401 authentication_required` for a dead
39
+ * credential, `403 insufficient_permissions` for a lost role — and one whose
40
+ * close ran after the registration found the pending entry: admission then
41
+ * fails, and the response is a `200` whose body is the connect comment and
42
+ * nothing else — to an `EventSource`, a stream closed an instant after it
43
+ * opened (the client's reconnect is answered by the gates, with the precise
44
+ * status). A pending registration receives no audit row, counts toward no cap,
45
+ * and is removed when a re-read refuses the request. The twin of the Rust
46
+ * spine's `audit_stream_router`, which registers before its role read rather
47
+ * than repeating it.
48
+ *
24
49
  * @param options - optional stream wiring + role override
25
50
  * @returns the SSE route spec (when `options.stream` is provided) or an empty array
26
51
  */
27
52
  export const create_audit_log_route_specs = (options) => {
28
53
  if (!options?.stream)
29
54
  return [];
30
- const { subscribe, log } = options.stream;
55
+ const { registry, log } = options.stream;
56
+ const required_role = options.required_role ?? DEFAULT_AUDIT_STREAM_ROLE;
31
57
  return [
32
58
  {
33
- ...create_audit_log_route_shape(options.required_role),
34
- handler: (c) => {
59
+ ...create_audit_log_route_shape(required_role),
60
+ handler: async (c, route) => {
35
61
  // Rule 3 is declared on the route shape's `auth.required_scope`,
36
62
  // so a narrowed token is refused ahead of the role gate rather
37
63
  // than here.
@@ -39,16 +65,64 @@ export const create_audit_log_route_specs = (options) => {
39
65
  // scope = session hash (capped → tabs-per-session limit and
40
66
  // session-specific `session_revoke` close). groups = [account_id]
41
67
  // (uncapped → coarse close on role_grant_revoke / session_revoke_all
42
- // / password_change).
68
+ // / password_change / account delete). The route shape admits
69
+ // sessions only, so `api_token_id` is null here; it is registered
70
+ // when present so that a consumer widening the credential gate
71
+ // still has a revoked token's stream closed by `token_revoke`.
43
72
  const token_hash = c.get(AUTH_SESSION_TOKEN_HASH_KEY) ?? null;
44
- const { response, stream } = create_sse_response(c, log);
45
- const unsubscribe = subscribe(stream, {
73
+ const api_token_id = c.get(AUTH_API_TOKEN_ID_KEY) ?? null;
74
+ // Register pending first: from here every revocation finds the
75
+ // stream (TSDoc, "Admission").
76
+ const pending = registry.subscribe_pending({
46
77
  channels: [AUDIT_LOG_CHANNEL],
47
78
  scope: token_hash ?? undefined,
48
- groups: [ctx.account.id]
79
+ groups: api_token_id === null ? [ctx.account.id] : [ctx.account.id, api_token_id]
49
80
  });
50
- stream.on_close(unsubscribe);
51
- return response;
81
+ // The pending registration is released on every path that does not
82
+ // admit it — a refusal, a failed re-read, a throw while building the
83
+ // response — so none of them can leak an entry nothing delivers to.
84
+ let admitted = false;
85
+ try {
86
+ // Re-read what the gates read, now that the stream is registered:
87
+ // a revocation that closed before the registration is committed,
88
+ // so these see it. A throw here fails closed — no stream opens
89
+ // unchecked. Skipped under the test-preset escape hatch, whose
90
+ // pre-baked context has no rows behind it to re-read.
91
+ if (!c.get(TEST_CONTEXT_PRESET_KEY)) {
92
+ const resolved = get_resolved_auth(c);
93
+ const credential_is_live = resolved !== null && (await revalidate_resolved_auth(route, resolved));
94
+ if (!credential_is_live) {
95
+ log.info('audit stream: credential revoked during the request', ctx.account.id);
96
+ return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
97
+ }
98
+ // Global grants only, like the `require_role` gate that ran in
99
+ // the pipeline — a scoped grant must not open the instance-wide
100
+ // stream.
101
+ if (!has_scoped_role(await refresh_role_grants(ctx, route), required_role, null)) {
102
+ log.info('audit stream: role revoked during the request', ctx.account.id);
103
+ return c.json({ error: ERROR_INSUFFICIENT_PERMISSIONS, required_roles: [required_role] }, 403);
104
+ }
105
+ }
106
+ // Admit — the cap's eviction happens here, after every gate. A
107
+ // registration a revocation closed while it was pending is not
108
+ // admitted: the stream is closed at once, so the body is the
109
+ // connect comment and nothing else, and the client reconnects
110
+ // into the gates.
111
+ const { response, stream } = create_sse_response(c, log);
112
+ admitted = registry.admit(pending, stream);
113
+ if (admitted) {
114
+ stream.on_close(pending.unsubscribe);
115
+ }
116
+ else {
117
+ log.info('audit stream: closed by a revocation before admission', ctx.account.id);
118
+ stream.close();
119
+ }
120
+ return response;
121
+ }
122
+ finally {
123
+ if (!admitted)
124
+ pending.unsubscribe();
125
+ }
52
126
  }
53
127
  }
54
128
  ];
@@ -49,6 +49,54 @@ export declare const AuditEventType: z.ZodEnum<{
49
49
  db_admin_row_delete: "db_admin_row_delete";
50
50
  }>;
51
51
  export type AuditEventType = z.infer<typeof AuditEventType>;
52
+ /**
53
+ * What a **successful** audit row invalidates — the axis both revocation
54
+ * listeners (`create_ws_auth_guard`, `create_sse_auth_guard`) dispatch on.
55
+ *
56
+ * - `none` — the row invalidates no live connection.
57
+ * - `session` — one session: the connections whose session-token hash equals
58
+ * the row's `metadata.session_id`.
59
+ * - `token` — one API token: the connections whose `api_token.id` equals the
60
+ * row's `metadata.token_id`.
61
+ * - `account` — every connection of `target_account_id ?? account_id`.
62
+ * - `role` — the target account's connections gated on the row's
63
+ * `metadata.role`.
64
+ *
65
+ * The scope says what the row invalidated, not what a transport must do about
66
+ * it. The two listeners differ on `role` on purpose: a WebSocket message is
67
+ * re-authorized per dispatch, so the socket stays open and its next message
68
+ * is refused, while a stream is authorized once at open and must be closed.
69
+ *
70
+ * Twin of the Rust spine's `RevocationScope`.
71
+ */
72
+ export type RevocationScope = 'none' | 'session' | 'token' | 'account' | 'role';
73
+ /**
74
+ * The `RevocationScope` of every builtin audit event — declared once, so the
75
+ * WebSocket and SSE revocation listeners read one answer instead of keeping a
76
+ * list each. Typed over `AuditEventType`, so a new event type does not compile
77
+ * until it declares its scope here.
78
+ *
79
+ * A scope other than `none` marks an event whose success invalidates a live
80
+ * connection's credential or access: `logout`, `password_change`,
81
+ * `session_revoke_all`, `token_revoke_all`, `account_delete`, and
82
+ * `account_purge` are account-wide, `session_revoke` and `token_revoke` name
83
+ * the one credential they ended, and `role_grant_revoke` names the lost role.
84
+ * Granting access revokes nothing, and neither does an event about a subject
85
+ * other than a connection's identity.
86
+ *
87
+ * Twin of the `revocation_scope` column of the Rust spine's
88
+ * `AUDIT_EVENT_SPECS`.
89
+ */
90
+ export declare const audit_event_revocation_scopes: Readonly<Record<AuditEventType, RevocationScope>>;
91
+ /**
92
+ * The `RevocationScope` of an `audit_log.event_type` string.
93
+ *
94
+ * An event type with no builtin entry — a consumer's own event — resolves to
95
+ * `none`: an event fuz_app has never heard of must not close connections. A
96
+ * consumer whose own event revokes closes the connections from its handler,
97
+ * with `queue_connection_close`.
98
+ */
99
+ export declare const to_revocation_scope: (event_type: string) => RevocationScope;
52
100
  /**
53
101
  * Letter start, then letters, digits, `_`, `.`, `/`, `-`. Accepts snake_case,
54
102
  * dotted, and namespaced consumer conventions; rejects empty strings, leading
@@ -1 +1 @@
1
- {"version":3,"file":"audit_log_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"audit_log_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_schema.ts"],"names":[],"mappings":"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"}
@@ -62,6 +62,64 @@ export const AUDIT_EVENT_TYPES = Object.freeze([
62
62
  ]);
63
63
  /** Zod schema for audit event types. */
64
64
  export const AuditEventType = z.enum(AUDIT_EVENT_TYPES);
65
+ /**
66
+ * The `RevocationScope` of every builtin audit event — declared once, so the
67
+ * WebSocket and SSE revocation listeners read one answer instead of keeping a
68
+ * list each. Typed over `AuditEventType`, so a new event type does not compile
69
+ * until it declares its scope here.
70
+ *
71
+ * A scope other than `none` marks an event whose success invalidates a live
72
+ * connection's credential or access: `logout`, `password_change`,
73
+ * `session_revoke_all`, `token_revoke_all`, `account_delete`, and
74
+ * `account_purge` are account-wide, `session_revoke` and `token_revoke` name
75
+ * the one credential they ended, and `role_grant_revoke` names the lost role.
76
+ * Granting access revokes nothing, and neither does an event about a subject
77
+ * other than a connection's identity.
78
+ *
79
+ * Twin of the `revocation_scope` column of the Rust spine's
80
+ * `AUDIT_EVENT_SPECS`.
81
+ */
82
+ export const audit_event_revocation_scopes = Object.freeze({
83
+ login: 'none',
84
+ logout: 'account',
85
+ bootstrap: 'none',
86
+ signup: 'none',
87
+ password_change: 'account',
88
+ session_revoke: 'session',
89
+ session_revoke_all: 'account',
90
+ token_create: 'none',
91
+ token_revoke: 'token',
92
+ token_revoke_all: 'account',
93
+ role_grant_create: 'none',
94
+ role_grant_revoke: 'role',
95
+ role_grant_offer_create: 'none',
96
+ role_grant_offer_accept: 'none',
97
+ role_grant_offer_decline: 'none',
98
+ role_grant_offer_retract: 'none',
99
+ role_grant_offer_expire: 'none',
100
+ role_grant_offer_supersede: 'none',
101
+ invite_create: 'none',
102
+ invite_delete: 'none',
103
+ account_delete: 'account',
104
+ account_purge: 'account',
105
+ account_undelete: 'none',
106
+ actor_delete: 'none',
107
+ actor_purge: 'none',
108
+ actor_undelete: 'none',
109
+ app_settings_update: 'none',
110
+ db_admin_row_delete: 'none'
111
+ });
112
+ /**
113
+ * The `RevocationScope` of an `audit_log.event_type` string.
114
+ *
115
+ * An event type with no builtin entry — a consumer's own event — resolves to
116
+ * `none`: an event fuz_app has never heard of must not close connections. A
117
+ * consumer whose own event revokes closes the connections from its handler,
118
+ * with `queue_connection_close`.
119
+ */
120
+ export const to_revocation_scope = (event_type) => Object.hasOwn(audit_event_revocation_scopes, event_type)
121
+ ? audit_event_revocation_scopes[event_type]
122
+ : 'none';
65
123
  /**
66
124
  * Letter start, then letters, digits, `_`, `.`, `/`, `-`. Accepts snake_case,
67
125
  * dotted, and namespaced consumer conventions; rejects empty strings, leading
@@ -53,8 +53,9 @@ export interface BootstrapAccountDeps {
53
53
  token_path: string;
54
54
  /**
55
55
  * Hardened secret-file read (see `FsSecureReadDeps.read_secure_file`) —
56
- * the token mints the keeper account, so a symlinked, group/other-readable,
57
- * or oversized file must refuse rather than be honored. Throws on refusal;
56
+ * the token mints the keeper account, so a symlinked, non-regular,
57
+ * group/other-readable, or oversized file must refuse rather than be
58
+ * honored. Throws on refusal;
58
59
  * every throw reads as `TOKEN_FILE_MISSING` upstream.
59
60
  */
60
61
  read_secure_file: (path: string) => Promise<Uint8Array>;
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap_account.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bootstrap_account.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EACN,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGrE,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC,gDAAgD;AAChD,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,6DAA6D;AAC7D,MAAM,WAAW,uBAAuB;IACvC,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,WAAW,EAAE;QAAE,MAAM,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAC;IACrD,wFAAwF;IACxF,kBAAkB,EAAE,OAAO,CAAC;CAC5B;AAED,gCAAgC;AAChC,MAAM,MAAM,uBAAuB,GAChC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,0BAA0B,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GACpE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,wBAAwB,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GAClE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,mBAAmB,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC;AAEjE,qFAAqF;AACrF,MAAM,MAAM,sBAAsB,GAAG,uBAAuB,GAAG,uBAAuB,CAAC;AAEvF;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,EAAE,EAAE,EAAE,CAAC;IACP,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IACxD,qBAAqB;IACrB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,6EAA6E;IAC7E,QAAQ,EAAE,IAAI,CAAC,gBAAgB,EAAE,eAAe,CAAC,CAAC;IAClD,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,oBAAoB,EAC1B,gBAAgB,MAAM,EACtB,OAAO,qBAAqB,KAC1B,OAAO,CAAC,sBAAsB,CA2EhC,CAAC"}
1
+ {"version":3,"file":"bootstrap_account.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/bootstrap_account.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EACN,mBAAmB,EACnB,0BAA0B,EAC1B,wBAAwB,EACxB,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGrE,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAEtC,gDAAgD;AAChD,MAAM,WAAW,qBAAqB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,6DAA6D;AAC7D,MAAM,WAAW,uBAAuB;IACvC,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;IACb,WAAW,EAAE;QAAE,MAAM,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,SAAS,CAAA;KAAE,CAAC;IACrD,wFAAwF;IACxF,kBAAkB,EAAE,OAAO,CAAC;CAC5B;AAED,gCAAgC;AAChC,MAAM,MAAM,uBAAuB,GAChC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,0BAA0B,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GACpE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,wBAAwB,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,GAClE;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,mBAAmB,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,CAAC;AAEjE,qFAAqF;AACrF,MAAM,MAAM,sBAAsB,GAAG,uBAAuB,GAAG,uBAAuB,CAAC;AAEvF;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACpC,EAAE,EAAE,EAAE,CAAC;IACP,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;OAMG;IACH,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IACxD,qBAAqB;IACrB,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,6EAA6E;IAC7E,QAAQ,EAAE,IAAI,CAAC,gBAAgB,EAAE,eAAe,CAAC,CAAC;IAClD,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,oBAAoB,EAC1B,gBAAgB,MAAM,EACtB,OAAO,qBAAqB,KAC1B,OAAO,CAAC,sBAAsB,CA2EhC,CAAC"}
@@ -36,9 +36,9 @@ import { query_create_role_grant } from './role_grant_queries.js';
36
36
  export const bootstrap_account = async (deps, provided_token, input) => {
37
37
  const { db, token_path, read_secure_file, delete_file, password, log } = deps;
38
38
  // 1. Read and verify token (non-destructive, before transaction). The
39
- // secure read fails loud on a symlink / permissive mode / oversized file —
40
- // every refusal masks as the same TOKEN_FILE_MISSING the wire always
41
- // carried, with the specific reason logged for the operator.
39
+ // secure read fails loud on a symlink / non-regular file / permissive mode /
40
+ // oversized file — every refusal masks as the same TOKEN_FILE_MISSING the
41
+ // wire always carried, with the specific reason logged for the operator.
42
42
  let expected_token;
43
43
  try {
44
44
  expected_token = new TextDecoder().decode(await read_secure_file(token_path)).trim();