@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
@@ -13,6 +13,7 @@ import type { Keyring } from './keyring.js';
13
13
  import type { PasswordHashDeps } from './password.js';
14
14
  import type { Db } from '../db/db.js';
15
15
  import type { AuditEmitter } from './audit_emitter.js';
16
+ import type { ConnectionCloser, RealtimeCloser } from '../actions/connection_closer.js';
16
17
  /**
17
18
  * Stateless capabilities bundle for fuz_app backends.
18
19
  *
@@ -24,7 +25,7 @@ export interface AppDeps {
24
25
  * Hardened secret-file read — used for the bootstrap token (the file that
25
26
  * mints the keeper account). Production wiring passes the runtime's
26
27
  * `read_secure_file` (`FsSecureReadDeps`), which rejects symlinks,
27
- * group/other-accessible modes, and oversized files; both the boot-time
28
+ * non-regular files, group/other-accessible modes, and oversized files; both the boot-time
28
29
  * availability probe and the request-time read go through this one
29
30
  * capability so the probe can never be laxer than the read it gates.
30
31
  */
@@ -47,6 +48,16 @@ export interface AppDeps {
47
48
  * context.
48
49
  */
49
50
  audit: AuditEmitter;
51
+ /**
52
+ * Closes live connections — WebSocket and SSE — when a credential is
53
+ * revoked. Every revocation handler queues its close on this
54
+ * (`queue_connection_close`), so it must reach every transport of the
55
+ * backend: `create_app_backend` creates it empty, and `create_app_server`
56
+ * adds each WebSocket transport it mounts and its audit stream registry. A
57
+ * transport mounted by hand is added through the `connection_closer`
58
+ * option of `register_ws_endpoint` / `create_audit_log_sse`.
59
+ */
60
+ connection_closer: RealtimeCloser;
50
61
  /**
51
62
  * Optional content-addressed byte store. Present only on backends that
52
63
  * serve binary content (facts) — minimal consumers leave it unset. The
@@ -78,4 +89,21 @@ export interface ActionFactoryDeps {
78
89
  /** Bound audit emitter for fire-and-forget audit writes. */
79
90
  audit: AuditEmitter;
80
91
  }
92
+ /**
93
+ * Capabilities for the action factories whose handlers end credentials —
94
+ * `create_account_actions`, `create_admin_actions`, and the
95
+ * `create_standard_rpc_actions` bundle over them. `ActionFactoryDeps` plus
96
+ * the closer their revocations close live connections through. `AppDeps`
97
+ * and `RouteFactoryDeps` satisfy it structurally.
98
+ */
99
+ export interface RevokingActionFactoryDeps extends ActionFactoryDeps {
100
+ /**
101
+ * Closes the live connections of a revoked credential, after the
102
+ * revocation commits. Required rather than optional: a revocation that
103
+ * closes nothing leaves an open WebSocket or SSE stream running on a dead
104
+ * credential. A backend with no live-connection surface passes
105
+ * `noop_connection_closer`.
106
+ */
107
+ connection_closer: ConnectionCloser;
108
+ }
81
109
  //# sourceMappingURL=deps.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"deps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAEhE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACvB;;;;;;;OAOG;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,0CAA0C;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,6EAA6E;IAC7E,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,yBAAyB;IACzB,EAAE,EAAE,EAAE,CAAC;IACP,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,KAAK,EAAE,YAAY,CAAC;IACpB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IACjC,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,4DAA4D;IAC5D,KAAK,EAAE,YAAY,CAAC;CACpB"}
1
+ {"version":3,"file":"deps.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/deps.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAEhE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAExF;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACvB;;;;;;;OAOG;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,0CAA0C;IAC1C,OAAO,EAAE,OAAO,CAAC;IACjB,6EAA6E;IAC7E,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,yBAAyB;IACzB,EAAE,EAAE,EAAE,CAAC;IACP,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,KAAK,EAAE,YAAY,CAAC;IACpB;;;;;;;;OAQG;IACH,iBAAiB,EAAE,cAAc,CAAC;IAClC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,SAAS,CAAC;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AAEnD;;;;;;;;GAQG;AACH,MAAM,WAAW,iBAAiB;IACjC,kCAAkC;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,4DAA4D;IAC5D,KAAK,EAAE,YAAY,CAAC;CACpB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,yBAA0B,SAAQ,iBAAiB;IACnE;;;;;;OAMG;IACH,iBAAiB,EAAE,gBAAgB,CAAC;CACpC"}
@@ -1 +1 @@
1
- {"version":3,"file":"migrations.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AA+BH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,wDAAwD;AACxD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,aAAa,CAAC,MAAM,CAA8B,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CA6K5C,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AA+BH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,wDAAwD;AACxD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,aAAa,CAAC,MAAM,CAA8B,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CA0M5C,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
@@ -237,6 +237,33 @@ export const auth_migrations = [
237
237
  up: async (db) => {
238
238
  await db.query('ALTER TABLE auth_session DROP COLUMN IF EXISTS last_seen_at');
239
239
  }
240
+ },
241
+ // The expiry-audit stamp that makes the offer sweep idempotent.
242
+ //
243
+ // An expired pending offer has no terminal state — expiry is computed from
244
+ // `expires_at`, never written — so without a stamp every
245
+ // `cleanup_expired_role_grant_offers` run re-audited every expired offer.
246
+ // The sweep now claims rows by setting `expire_audited_at` in the same
247
+ // transaction as their `role_grant_offer_expire` audit rows, so each expiry
248
+ // is audited exactly once and concurrent sweeps claim disjoint rows.
249
+ //
250
+ // Not a lifecycle state: the column stays out of the
251
+ // `role_grant_offer_single_terminal` CHECK and the pending predicate, and
252
+ // off the wire. A re-offer of a stamped row clears it (the create upsert),
253
+ // so the refreshed offer's own expiry is audited too. The partial index
254
+ // serves exactly the sweep's claim predicate.
255
+ {
256
+ name: 'role_grant_offer_expire_audited_at',
257
+ up: async (db) => {
258
+ await db.query('ALTER TABLE role_grant_offer ADD COLUMN IF NOT EXISTS expire_audited_at TIMESTAMPTZ NULL');
259
+ await db.query(`CREATE INDEX IF NOT EXISTS role_grant_offer_expire_sweep
260
+ ON role_grant_offer (expires_at)
261
+ WHERE accepted_at IS NULL
262
+ AND declined_at IS NULL
263
+ AND retracted_at IS NULL
264
+ AND superseded_at IS NULL
265
+ AND expire_audited_at IS NULL`);
266
+ }
240
267
  }
241
268
  ];
242
269
  /** Pre-composed migration namespace for auth tables. */
@@ -0,0 +1,105 @@
1
+ /**
2
+ * The account-grain credential a request resolved to, and the re-check a
3
+ * long-lived connection runs on it once it is registered.
4
+ *
5
+ * The auth middleware chain (daemon token → session cookie → bearer) resolves a
6
+ * credential and leaves it on the Hono context as separate keys.
7
+ * `ResolvedAuth` is that credential gathered into one value
8
+ * (`get_resolved_auth`), so the connections that outlive their request — a
9
+ * WebSocket, an SSE stream — can carry it past the handshake and re-read it
10
+ * with `revalidate_resolved_auth`.
11
+ *
12
+ * @module
13
+ */
14
+ import type { Context } from 'hono';
15
+ import type { QueryDeps } from '../db/query_deps.js';
16
+ import { type CredentialType } from '../hono_context.js';
17
+ /**
18
+ * An authenticated request's credential, account-grain — no actor.
19
+ *
20
+ * `token_hash` is set for a session, `api_token_id` for an API token, and both
21
+ * are `null` for a daemon token. The twin of the Rust spine's `ResolvedAuth`,
22
+ * which carries the account row and token scope as well; here those stay on
23
+ * the Hono context.
24
+ */
25
+ export interface ResolvedAuth {
26
+ /** The authenticated account. */
27
+ account_id: string;
28
+ /** How the request was authenticated. */
29
+ credential_type: CredentialType;
30
+ /** Blake3 hash of the session token, or `null` for non-session credentials. */
31
+ token_hash: string | null;
32
+ /** `api_token.id` for a bearer credential, else `null`. */
33
+ api_token_id: string | null;
34
+ }
35
+ /**
36
+ * Gather the credential the auth middleware resolved for this request.
37
+ *
38
+ * Reads the keys the session, bearer, and daemon-token middleware set and
39
+ * keeps only the one that belongs to the winning credential type — a daemon
40
+ * token overrides a session cookie sent alongside it without clearing that
41
+ * session's hash, so the hash is carried for a session credential alone.
42
+ *
43
+ * @param c - the request context, after the auth middleware chain
44
+ * @returns the resolved credential, or `null` for an unauthenticated request
45
+ */
46
+ export declare const get_resolved_auth: (c: Context) => ResolvedAuth | null;
47
+ /**
48
+ * Re-check that a resolved credential still authenticates: `true` when it
49
+ * does, `false` when it has been revoked, has expired, or its account is gone.
50
+ *
51
+ * For a connection that outlives its request — a WebSocket, an SSE stream —
52
+ * and is authorized once, at open. Such a connection is closed on revocation
53
+ * by identity, but only once it is *registered*; between the credential read
54
+ * and the registration a revocation closes nothing. So the opener registers
55
+ * first (un-admitted — `BackendWebsocketTransport.register_pending` /
56
+ * `SubscriberRegistry.subscribe_pending`), then calls this, then admits. A
57
+ * revocation that committed before this read is seen by it; one whose close
58
+ * ran after the registration found the pending entry.
59
+ *
60
+ * That needs each statement here to see everything committed before it
61
+ * started, so `deps.db` must be the pool-level `Db` (autocommit) or a
62
+ * `READ COMMITTED` transaction — never a `REPEATABLE READ` / `SERIALIZABLE`
63
+ * transaction opened before the registration, whose snapshot would predate
64
+ * it.
65
+ *
66
+ * What is re-read, per credential — each through the same predicate the
67
+ * resolve path used, so the two cannot disagree about what "valid" means (the
68
+ * session and account legs call the middleware's own queries; the token leg
69
+ * shares its `WHERE` fragment):
70
+ *
71
+ * - **Session** — the `auth_session` row still exists, is unexpired, and
72
+ * belongs to the resolved account.
73
+ * - **API token** — the `api_token` row still exists, is unexpired, and
74
+ * belongs to the resolved account (by id; the raw token is not kept).
75
+ * - **Daemon token** — nothing of its own. The token has no row, and no
76
+ * revocation event: rotation closes no connection, so there is no close an
77
+ * opening connection could have missed. Its keeper account is still
78
+ * re-read.
79
+ * - **Every credential** — the account row still exists and is not
80
+ * soft-deleted.
81
+ *
82
+ * It re-checks the **credential**, not what a revocation chose to close: a
83
+ * `logout` closes every connection of the account but deletes only its own
84
+ * session, so another session's connection opening concurrently passes this
85
+ * check and is refused only if that account-wide close found its pending
86
+ * registration — the same two outcomes an already-open connection has.
87
+ *
88
+ * **No side effects.** Unlike the bearer middleware this never touches
89
+ * `api_token.last_used_at` / `last_used_ip` — re-checking a credential is not
90
+ * using it — and it writes nothing else.
91
+ *
92
+ * A `ResolvedAuth` whose credential type lacks its own key (a session with no
93
+ * `token_hash`, a bearer with no `api_token_id`) is a shape the middleware
94
+ * never builds; it is answered `false`.
95
+ *
96
+ * The twin of the Rust spine's `revalidate_resolved_auth`.
97
+ *
98
+ * @param deps - query dependencies; the pool-level `Db`, not a snapshot transaction
99
+ * @param resolved - the credential the request authenticated with
100
+ * @returns whether the credential still authenticates
101
+ * @throws Error propagated from the database. The caller must fail closed — an
102
+ * unchecked credential is not a valid one.
103
+ */
104
+ export declare const revalidate_resolved_auth: (deps: QueryDeps, resolved: ResolvedAuth) => Promise<boolean>;
105
+ //# sourceMappingURL=resolved_auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolved_auth.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/resolved_auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAIN,KAAK,cAAc,EACnB,MAAM,oBAAoB,CAAC;AAM5B;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC5B,iCAAiC;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,yCAAyC;IACzC,eAAe,EAAE,cAAc,CAAC;IAChC,+EAA+E;IAC/E,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,2DAA2D;IAC3D,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,GAAI,GAAG,OAAO,KAAG,YAAY,GAAG,IAU7D,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,eAAO,MAAM,wBAAwB,GACpC,MAAM,SAAS,EACf,UAAU,YAAY,KACpB,OAAO,CAAC,OAAO,CAsBjB,CAAC"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * The account-grain credential a request resolved to, and the re-check a
3
+ * long-lived connection runs on it once it is registered.
4
+ *
5
+ * The auth middleware chain (daemon token → session cookie → bearer) resolves a
6
+ * credential and leaves it on the Hono context as separate keys.
7
+ * `ResolvedAuth` is that credential gathered into one value
8
+ * (`get_resolved_auth`), so the connections that outlive their request — a
9
+ * WebSocket, an SSE stream — can carry it past the handshake and re-read it
10
+ * with `revalidate_resolved_auth`.
11
+ *
12
+ * @module
13
+ */
14
+ import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY } from '../hono_context.js';
15
+ import { query_account_by_id } from './account_queries.js';
16
+ import { query_api_token_live_account } from './api_token_queries.js';
17
+ import { AUTH_SESSION_TOKEN_HASH_KEY } from './request_context.js';
18
+ import { query_session_get_valid } from './session_queries.js';
19
+ /**
20
+ * Gather the credential the auth middleware resolved for this request.
21
+ *
22
+ * Reads the keys the session, bearer, and daemon-token middleware set and
23
+ * keeps only the one that belongs to the winning credential type — a daemon
24
+ * token overrides a session cookie sent alongside it without clearing that
25
+ * session's hash, so the hash is carried for a session credential alone.
26
+ *
27
+ * @param c - the request context, after the auth middleware chain
28
+ * @returns the resolved credential, or `null` for an unauthenticated request
29
+ */
30
+ export const get_resolved_auth = (c) => {
31
+ const account_id = c.get(ACCOUNT_ID_KEY) ?? null;
32
+ const credential_type = c.get(CREDENTIAL_TYPE_KEY) ?? null;
33
+ if (account_id === null || credential_type === null)
34
+ return null;
35
+ return {
36
+ account_id,
37
+ credential_type,
38
+ token_hash: credential_type === 'session' ? (c.get(AUTH_SESSION_TOKEN_HASH_KEY) ?? null) : null,
39
+ api_token_id: credential_type === 'api_token' ? (c.get(AUTH_API_TOKEN_ID_KEY) ?? null) : null
40
+ };
41
+ };
42
+ /**
43
+ * Re-check that a resolved credential still authenticates: `true` when it
44
+ * does, `false` when it has been revoked, has expired, or its account is gone.
45
+ *
46
+ * For a connection that outlives its request — a WebSocket, an SSE stream —
47
+ * and is authorized once, at open. Such a connection is closed on revocation
48
+ * by identity, but only once it is *registered*; between the credential read
49
+ * and the registration a revocation closes nothing. So the opener registers
50
+ * first (un-admitted — `BackendWebsocketTransport.register_pending` /
51
+ * `SubscriberRegistry.subscribe_pending`), then calls this, then admits. A
52
+ * revocation that committed before this read is seen by it; one whose close
53
+ * ran after the registration found the pending entry.
54
+ *
55
+ * That needs each statement here to see everything committed before it
56
+ * started, so `deps.db` must be the pool-level `Db` (autocommit) or a
57
+ * `READ COMMITTED` transaction — never a `REPEATABLE READ` / `SERIALIZABLE`
58
+ * transaction opened before the registration, whose snapshot would predate
59
+ * it.
60
+ *
61
+ * What is re-read, per credential — each through the same predicate the
62
+ * resolve path used, so the two cannot disagree about what "valid" means (the
63
+ * session and account legs call the middleware's own queries; the token leg
64
+ * shares its `WHERE` fragment):
65
+ *
66
+ * - **Session** — the `auth_session` row still exists, is unexpired, and
67
+ * belongs to the resolved account.
68
+ * - **API token** — the `api_token` row still exists, is unexpired, and
69
+ * belongs to the resolved account (by id; the raw token is not kept).
70
+ * - **Daemon token** — nothing of its own. The token has no row, and no
71
+ * revocation event: rotation closes no connection, so there is no close an
72
+ * opening connection could have missed. Its keeper account is still
73
+ * re-read.
74
+ * - **Every credential** — the account row still exists and is not
75
+ * soft-deleted.
76
+ *
77
+ * It re-checks the **credential**, not what a revocation chose to close: a
78
+ * `logout` closes every connection of the account but deletes only its own
79
+ * session, so another session's connection opening concurrently passes this
80
+ * check and is refused only if that account-wide close found its pending
81
+ * registration — the same two outcomes an already-open connection has.
82
+ *
83
+ * **No side effects.** Unlike the bearer middleware this never touches
84
+ * `api_token.last_used_at` / `last_used_ip` — re-checking a credential is not
85
+ * using it — and it writes nothing else.
86
+ *
87
+ * A `ResolvedAuth` whose credential type lacks its own key (a session with no
88
+ * `token_hash`, a bearer with no `api_token_id`) is a shape the middleware
89
+ * never builds; it is answered `false`.
90
+ *
91
+ * The twin of the Rust spine's `revalidate_resolved_auth`.
92
+ *
93
+ * @param deps - query dependencies; the pool-level `Db`, not a snapshot transaction
94
+ * @param resolved - the credential the request authenticated with
95
+ * @returns whether the credential still authenticates
96
+ * @throws Error propagated from the database. The caller must fail closed — an
97
+ * unchecked credential is not a valid one.
98
+ */
99
+ export const revalidate_resolved_auth = async (deps, resolved) => {
100
+ const { account_id } = resolved;
101
+ let credential_is_live;
102
+ switch (resolved.credential_type) {
103
+ case 'session': {
104
+ if (resolved.token_hash === null)
105
+ return false;
106
+ const session = await query_session_get_valid(deps, resolved.token_hash);
107
+ credential_is_live = session?.account_id === account_id;
108
+ break;
109
+ }
110
+ case 'api_token': {
111
+ if (resolved.api_token_id === null)
112
+ return false;
113
+ const owner = await query_api_token_live_account(deps, resolved.api_token_id);
114
+ credential_is_live = owner === account_id;
115
+ break;
116
+ }
117
+ case 'daemon_token':
118
+ credential_is_live = true;
119
+ break;
120
+ }
121
+ if (!credential_is_live)
122
+ return false;
123
+ return (await query_account_by_id(deps, account_id)) !== undefined;
124
+ };
@@ -27,13 +27,15 @@ export declare const ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN = "GLOBAL";
27
27
  * The full `role_grant_offer` column set, named explicitly so a row read
28
28
  * fails loud on schema drift (see `ACCOUNT_COLUMNS` in
29
29
  * `auth/account_queries.ts` for the outage class). Column order follows
30
- * `RoleGrantOffer` and `ROLE_GRANT_OFFER_SCHEMA` below.
30
+ * `RoleGrantOffer` and `ROLE_GRANT_OFFER_SCHEMA` below, with
31
+ * `expire_audited_at` last — the appended `role_grant_offer_expire_audited_at`
32
+ * migration adds it after the frozen `CREATE TABLE`.
31
33
  *
32
34
  * Lives here rather than in `auth/role_grant_offer_queries.ts` because two
33
35
  * query modules project this table — see the placement rule in
34
36
  * `db/sql_columns.ts`.
35
37
  */
36
- export declare const ROLE_GRANT_OFFER_COLUMNS: readonly ["id", "from_actor_id", "to_account_id", "to_actor_id", "role", "scope_kind", "scope_id", "message", "created_at", "expires_at", "accepted_at", "declined_at", "decline_reason", "retracted_at", "superseded_at", "resulting_role_grant_id"];
38
+ export declare const ROLE_GRANT_OFFER_COLUMNS: readonly ["id", "from_actor_id", "to_account_id", "to_actor_id", "role", "scope_kind", "scope_id", "message", "created_at", "expires_at", "accepted_at", "declined_at", "decline_reason", "retracted_at", "superseded_at", "resulting_role_grant_id", "expire_audited_at"];
37
39
  /**
38
40
  * The `role_grant_offer` timestamp override, by row qualifier (`''` bare,
39
41
  * `o` for the history JOIN, `u` for the `updated` CTE's outer select). Lives
@@ -1 +1 @@
1
- {"version":3,"file":"role_grant_offer_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,mHAAmH;AACnH,eAAO,MAAM,oCAAoC,yCAAyC,CAAC;AAE3F;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,WAAW,CAAC;AAEjE;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,uPAiBmB,CAAC;AAEzD;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,8DAOhC,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,oCAAoC,QAMI,CAAC;AAEtD,eAAO,MAAM,uBAAuB,qzCAkClC,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qCAAqC,2XAYpB,CAAC;AAE/B,+EAA+E;AAC/E,eAAO,MAAM,4BAA4B,kOAMX,CAAC"}
1
+ {"version":3,"file":"role_grant_offer_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,mHAAmH;AACnH,eAAO,MAAM,oCAAoC,yCAAyC,CAAC;AAE3F;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,WAAW,CAAC;AAEjE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,wBAAwB,4QAkBmB,CAAC;AAEzD;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,8DAQhC,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,oCAAoC,QAMI,CAAC;AAKtD,eAAO,MAAM,uBAAuB,qzCAkClC,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qCAAqC,2XAYpB,CAAC;AAE/B,+EAA+E;AAC/E,eAAO,MAAM,4BAA4B,kOAMX,CAAC"}
@@ -28,7 +28,9 @@ export const ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN = 'GLOBAL';
28
28
  * The full `role_grant_offer` column set, named explicitly so a row read
29
29
  * fails loud on schema drift (see `ACCOUNT_COLUMNS` in
30
30
  * `auth/account_queries.ts` for the outage class). Column order follows
31
- * `RoleGrantOffer` and `ROLE_GRANT_OFFER_SCHEMA` below.
31
+ * `RoleGrantOffer` and `ROLE_GRANT_OFFER_SCHEMA` below, with
32
+ * `expire_audited_at` last — the appended `role_grant_offer_expire_audited_at`
33
+ * migration adds it after the frozen `CREATE TABLE`.
32
34
  *
33
35
  * Lives here rather than in `auth/role_grant_offer_queries.ts` because two
34
36
  * query modules project this table — see the placement rule in
@@ -50,7 +52,8 @@ export const ROLE_GRANT_OFFER_COLUMNS = [
50
52
  'decline_reason',
51
53
  'retracted_at',
52
54
  'superseded_at',
53
- 'resulting_role_grant_id'
55
+ 'resulting_role_grant_id',
56
+ 'expire_audited_at'
54
57
  ];
55
58
  /**
56
59
  * The `role_grant_offer` timestamp override, by row qualifier (`''` bare,
@@ -68,7 +71,8 @@ export const role_grant_offer_expr = iso8601_timestamp_expr(ROLE_GRANT_OFFER_COL
68
71
  'accepted_at',
69
72
  'declined_at',
70
73
  'retracted_at',
71
- 'superseded_at'
74
+ 'superseded_at',
75
+ 'expire_audited_at'
72
76
  ]);
73
77
  /**
74
78
  * Outer `SELECT` over an `updated` CTE of `role_grant_offer` rows, joining
@@ -81,6 +85,9 @@ export const role_grant_offer_expr = iso8601_timestamp_expr(ROLE_GRANT_OFFER_COL
81
85
  export const ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT = `SELECT ${qualify_columns(ROLE_GRANT_OFFER_COLUMNS, 'u', role_grant_offer_expr('u'))}, grantor.account_id AS from_account_id
82
86
  FROM updated u
83
87
  JOIN actor grantor ON grantor.id = u.from_actor_id`;
88
+ // Frozen v1 body — `expire_audited_at` is added by the appended
89
+ // `role_grant_offer_expire_audited_at` migration (with its partial sweep
90
+ // index), so a fresh bootstrap and an old deployed DB converge on one shape.
84
91
  export const ROLE_GRANT_OFFER_SCHEMA = `
85
92
  CREATE TABLE IF NOT EXISTS role_grant_offer (
86
93
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -112,7 +112,9 @@ export declare class RoleGrantOfferActorAccountMismatchError extends Error {
112
112
  * `(to_account, role, scope)` while pending upserts the existing row,
113
113
  * refreshing `message` and `expires_at` (and `to_actor_id` — supplying
114
114
  * a different `to_actor_id` on re-offer narrows the existing row to the
115
- * named actor; supplying null widens it back to account-grain). A
115
+ * named actor; supplying null widens it back to account-grain) and clearing
116
+ * `expire_audited_at`, so a re-offer of an expired, already-audited row gets
117
+ * its own `role_grant_offer_expire` when the refreshed expiry passes. A
116
118
  * different grantor offering the same `(to_account, role, scope)` creates
117
119
  * a distinct row — multiple pending grantors coexist. After a terminal
118
120
  * state, a re-offer is a fresh INSERT.
@@ -176,7 +178,7 @@ export declare const query_role_grant_offer_retract: (deps: QueryDeps, offer_id:
176
178
  *
177
179
  * Expired offers are filtered server-side (`expires_at > NOW()`) so the
178
180
  * inbox never surfaces a row that can no longer be accepted. The periodic
179
- * sweep (`query_role_grant_offer_sweep_expired`) handles audit tombstoning.
181
+ * sweep (`query_role_grant_offer_sweep_expired`) audits each expiry once.
180
182
  */
181
183
  export declare const query_role_grant_offer_list: (deps: QueryDeps, to_account_id: string) => Promise<Array<RoleGrantOffer>>;
182
184
  /**
@@ -191,12 +193,19 @@ export declare const query_role_grant_offer_history_for_account: (deps: QueryDep
191
193
  */
192
194
  export declare const query_role_grant_offer_find_pending: (deps: QueryDeps, offer_id: string) => Promise<RoleGrantOffer | null>;
193
195
  /**
194
- * Return pending offers whose `expires_at` has passed.
196
+ * Claim the pending offers whose `expires_at` has passed and whose expiry has
197
+ * not been audited yet, stamping `expire_audited_at` on each, and return them
198
+ * soonest-expiry first.
195
199
  *
196
- * Callers fire `role_grant_offer_expire` audit events for each row. The schema
197
- * does not tombstone the row, so callers are responsible for their own
198
- * idempotency (e.g. check whether a `role_grant_offer_expire` audit event
199
- * already exists for the offer id).
200
+ * The stamp is the sweep's idempotency: a claimed row never matches again, so
201
+ * each expiry is audited exactly once. It must share a transaction with the
202
+ * caller's `role_grant_offer_expire` audit inserts — committed together, or
203
+ * rolled back together so the next sweep retries. Concurrent sweeps claim
204
+ * disjoint rows: a row another sweep has stamped but not committed blocks on
205
+ * its row lock, then fails the re-checked `expire_audited_at IS NULL`.
206
+ * `cleanup_expired_role_grant_offers` is the caller.
207
+ *
208
+ * @mutates `role_grant_offer` rows - stamps `expire_audited_at` on each claimed row
200
209
  */
201
210
  export declare const query_role_grant_offer_sweep_expired: (deps: QueryDeps) => Promise<Array<RoleGrantOffer>>;
202
211
  /** Input for `query_accept_offer`. */
@@ -1 +1 @@
1
- {"version":3,"file":"role_grant_offer_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_queries.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"role_grant_offer_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_queries.ts"],"names":[],"mappings":"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"}
@@ -118,7 +118,9 @@ export class RoleGrantOfferActorAccountMismatchError extends Error {
118
118
  * `(to_account, role, scope)` while pending upserts the existing row,
119
119
  * refreshing `message` and `expires_at` (and `to_actor_id` — supplying
120
120
  * a different `to_actor_id` on re-offer narrows the existing row to the
121
- * named actor; supplying null widens it back to account-grain). A
121
+ * named actor; supplying null widens it back to account-grain) and clearing
122
+ * `expire_audited_at`, so a re-offer of an expired, already-audited row gets
123
+ * its own `role_grant_offer_expire` when the refreshed expiry passes. A
122
124
  * different grantor offering the same `(to_account, role, scope)` creates
123
125
  * a distinct row — multiple pending grantors coexist. After a terminal
124
126
  * state, a re-offer is a fresh INSERT.
@@ -170,7 +172,8 @@ export const query_role_grant_offer_create = async (deps, input) => {
170
172
  DO UPDATE SET
171
173
  to_actor_id = EXCLUDED.to_actor_id,
172
174
  message = EXCLUDED.message,
173
- expires_at = EXCLUDED.expires_at
175
+ expires_at = EXCLUDED.expires_at,
176
+ expire_audited_at = NULL
174
177
  RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS, role_grant_offer_expr(''))}`, [
175
178
  input.from_actor_id,
176
179
  input.to_account_id,
@@ -269,7 +272,7 @@ const resolve_terminal_or_missing = async (deps, offer_id, scope) => {
269
272
  *
270
273
  * Expired offers are filtered server-side (`expires_at > NOW()`) so the
271
274
  * inbox never surfaces a row that can no longer be accepted. The periodic
272
- * sweep (`query_role_grant_offer_sweep_expired`) handles audit tombstoning.
275
+ * sweep (`query_role_grant_offer_sweep_expired`) audits each expiry once.
273
276
  */
274
277
  export const query_role_grant_offer_list = async (deps, to_account_id) => {
275
278
  return deps.db.query(`SELECT ${columns_sql(ROLE_GRANT_OFFER_COLUMNS, role_grant_offer_expr(''))} FROM role_grant_offer
@@ -308,21 +311,36 @@ export const query_role_grant_offer_find_pending = async (deps, offer_id) => {
308
311
  return row ?? null;
309
312
  };
310
313
  /**
311
- * Return pending offers whose `expires_at` has passed.
314
+ * Claim the pending offers whose `expires_at` has passed and whose expiry has
315
+ * not been audited yet, stamping `expire_audited_at` on each, and return them
316
+ * soonest-expiry first.
312
317
  *
313
- * Callers fire `role_grant_offer_expire` audit events for each row. The schema
314
- * does not tombstone the row, so callers are responsible for their own
315
- * idempotency (e.g. check whether a `role_grant_offer_expire` audit event
316
- * already exists for the offer id).
318
+ * The stamp is the sweep's idempotency: a claimed row never matches again, so
319
+ * each expiry is audited exactly once. It must share a transaction with the
320
+ * caller's `role_grant_offer_expire` audit inserts — committed together, or
321
+ * rolled back together so the next sweep retries. Concurrent sweeps claim
322
+ * disjoint rows: a row another sweep has stamped but not committed blocks on
323
+ * its row lock, then fails the re-checked `expire_audited_at IS NULL`.
324
+ * `cleanup_expired_role_grant_offers` is the caller.
325
+ *
326
+ * @mutates `role_grant_offer` rows - stamps `expire_audited_at` on each claimed row
317
327
  */
318
328
  export const query_role_grant_offer_sweep_expired = async (deps) => {
319
- return deps.db.query(`SELECT ${columns_sql(ROLE_GRANT_OFFER_COLUMNS, role_grant_offer_expr(''))} FROM role_grant_offer
320
- WHERE accepted_at IS NULL
321
- AND declined_at IS NULL
322
- AND retracted_at IS NULL
323
- AND superseded_at IS NULL
324
- AND expires_at <= NOW()
325
- ORDER BY role_grant_offer.expires_at ASC`);
329
+ return deps.db.query(`WITH claimed AS (
330
+ UPDATE role_grant_offer
331
+ SET expire_audited_at = NOW()
332
+ WHERE accepted_at IS NULL
333
+ AND declined_at IS NULL
334
+ AND retracted_at IS NULL
335
+ AND superseded_at IS NULL
336
+ AND expires_at <= NOW()
337
+ AND expire_audited_at IS NULL
338
+ -- raw columns; the outer select formats them
339
+ RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)}
340
+ )
341
+ SELECT ${qualify_columns(ROLE_GRANT_OFFER_COLUMNS, 'c', role_grant_offer_expr('c'))}
342
+ FROM claimed c
343
+ ORDER BY c.expires_at ASC, c.id ASC`);
326
344
  };
327
345
  /**
328
346
  * Whether `grantor_actor_id` holds an active **global** `admin` on an active
@@ -60,6 +60,15 @@ export interface RoleGrantOffer {
60
60
  */
61
61
  superseded_at: string | null;
62
62
  resulting_role_grant_id: Uuid | null;
63
+ /**
64
+ * When the expiry sweep audited this offer's `role_grant_offer_expire` —
65
+ * the claim stamp `query_role_grant_offer_sweep_expired` sets in the same
66
+ * transaction as the audit row, so each expiry is audited once. Not a
67
+ * lifecycle state (an expired offer is still pending by the terminal
68
+ * columns) and server-only: `to_role_grant_offer_json` drops it, and the
69
+ * strict `RoleGrantOfferJson` would reject it. A re-offer clears it.
70
+ */
71
+ expire_audited_at: string | null;
63
72
  }
64
73
  /**
65
74
  * A superseded offer row annotated with the grantor's `account_id`.
@@ -1 +1 @@
1
- {"version":3,"file":"role_grant_offer_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAI9C,mEAAmE;AACnE,eAAO,MAAM,mCAAmC,MAAM,CAAC;AAEvD,yFAAyF;AACzF,eAAO,MAAM,+BAA+B,QAA2B,CAAC;AAExE,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC9B,EAAE,EAAE,IAAI,CAAC;IACT,aAAa,EAAE,IAAI,CAAC;IACpB,aAAa,EAAE,IAAI,CAAC;IACpB;;;;;;;;;;;OAWG;IACH,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;;;OAKG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,uBAAuB,EAAE,IAAI,GAAG,IAAI,CAAC;CACrC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACtD,eAAe,EAAE,IAAI,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACzC,aAAa,EAAE,IAAI,CAAC;IACpB,aAAa,EAAE,IAAI,CAAC;IACpB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,IAAI,CAAC;CACjB;AAED,wDAAwD;AACxD,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;kBAoD4D,CAAC;AAC5F,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,gEAAgE;AAChE,eAAO,MAAM,wBAAwB,GAAI,OAAO,cAAc,KAAG,kBAiB/D,CAAC"}
1
+ {"version":3,"file":"role_grant_offer_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_offer_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAI9C,mEAAmE;AACnE,eAAO,MAAM,mCAAmC,MAAM,CAAC;AAEvD,yFAAyF;AACzF,eAAO,MAAM,+BAA+B,QAA2B,CAAC;AAExE,wDAAwD;AACxD,MAAM,WAAW,cAAc;IAC9B,EAAE,EAAE,IAAI,CAAC;IACT,aAAa,EAAE,IAAI,CAAC;IACpB,aAAa,EAAE,IAAI,CAAC;IACpB;;;;;;;;;;;OAWG;IACH,WAAW,EAAE,IAAI,GAAG,IAAI,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;;;OAKG;IACH,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,uBAAuB,EAAE,IAAI,GAAG,IAAI,CAAC;IACrC;;;;;;;OAOG;IACH,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,eAAgB,SAAQ,cAAc;IACtD,eAAe,EAAE,IAAI,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,WAAW,yBAAyB;IACzC,aAAa,EAAE,IAAI,CAAC;IACpB,aAAa,EAAE,IAAI,CAAC;IACpB;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,IAAI,CAAC;CACjB;AAED,wDAAwD;AACxD,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;kBAoD4D,CAAC;AAC5F,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,gEAAgE;AAChE,eAAO,MAAM,wBAAwB,GAAI,OAAO,cAAc,KAAG,kBAiB/D,CAAC"}
@@ -62,7 +62,14 @@ export interface CreateSessionAndSetCookieOptions {
62
62
  * persists the session row, optionally enforces a per-account session limit,
63
63
  * and sets the signed cookie.
64
64
  *
65
+ * The cap deletes the sessions it evicts, but not the connections they
66
+ * opened. A caller that passes `max_sessions` closes the returned sessions'
67
+ * connections once its transaction commits — `queue_connection_close` with a
68
+ * `session` target, as `POST /login` does. A caller that mints for an account
69
+ * created in the same transaction (signup, bootstrap) evicts nothing.
70
+ *
71
+ * @returns the token hashes of the sessions the cap evicted, empty without a cap
65
72
  * @mutates `auth_session` table - inserts the new session row (and evicts older rows when `max_sessions` is set)
66
73
  */
67
- export declare const create_session_and_set_cookie: (options: CreateSessionAndSetCookieOptions) => Promise<void>;
74
+ export declare const create_session_and_set_cookie: (options: CreateSessionAndSetCookieOptions) => Promise<Array<string>>;
68
75
  //# sourceMappingURL=session_middleware.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session_middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_middleware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAGvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EACN,KAAK,cAAc,EAKnB,MAAM,qBAAqB,CAAC;AAQ7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,MAAM,GAAG,SAEX,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,OAAO,MAAM,EACb,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,IAOF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EAAE,GAAG,OAAO,EAAE,SAAS,cAAc,CAAC,CAAC,CAAC,KAAG,IAMhF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,EAClD,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,KAChC,iBAcF,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,gCAAgC;IAChD,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,kDAAkD;IAClD,IAAI,EAAE,SAAS,CAAC;IAChB,2CAA2C;IAC3C,CAAC,EAAE,OAAO,CAAC;IACX,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,6BAA6B,GACzC,SAAS,gCAAgC,KACvC,OAAO,CAAC,IAAI,CAad,CAAC"}
1
+ {"version":3,"file":"session_middleware.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_middleware.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAGvD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EACN,KAAK,cAAc,EAKnB,MAAM,qBAAqB,CAAC;AAQ7B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,MAAM,GAAG,SAEX,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,EACnC,GAAG,OAAO,EACV,OAAO,MAAM,EACb,SAAS,cAAc,CAAC,CAAC,CAAC,KACxB,IAOF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oBAAoB,GAAI,CAAC,EAAE,GAAG,OAAO,EAAE,SAAS,cAAc,CAAC,CAAC,CAAC,KAAG,IAMhF,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,yBAAyB,GAAI,SAAS,EAClD,SAAS,OAAO,EAChB,SAAS,cAAc,CAAC,SAAS,CAAC,KAChC,iBAcF,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,gCAAgC;IAChD,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,kDAAkD;IAClD,IAAI,EAAE,SAAS,CAAC;IAChB,2CAA2C;IAC3C,CAAC,EAAE,OAAO,CAAC;IACX,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;IACnB,oCAAoC;IACpC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACxC,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,6BAA6B,GACzC,SAAS,gCAAgC,KACvC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAavB,CAAC"}
@@ -70,6 +70,13 @@ export const create_session_middleware = (keyring, options) => {
70
70
  * persists the session row, optionally enforces a per-account session limit,
71
71
  * and sets the signed cookie.
72
72
  *
73
+ * The cap deletes the sessions it evicts, but not the connections they
74
+ * opened. A caller that passes `max_sessions` closes the returned sessions'
75
+ * connections once its transaction commits — `queue_connection_close` with a
76
+ * `session` target, as `POST /login` does. A caller that mints for an account
77
+ * created in the same transaction (signup, bootstrap) evicts nothing.
78
+ *
79
+ * @returns the token hashes of the sessions the cap evicted, empty without a cap
73
80
  * @mutates `auth_session` table - inserts the new session row (and evicts older rows when `max_sessions` is set)
74
81
  */
75
82
  export const create_session_and_set_cookie = async (options) => {
@@ -78,9 +85,8 @@ export const create_session_and_set_cookie = async (options) => {
78
85
  const token_hash = hash_session_token(session_token);
79
86
  const expires_at = new Date(Date.now() + AUTH_SESSION_LIFETIME_MS);
80
87
  await query_create_session(deps, token_hash, account_id, expires_at);
81
- if (max_sessions != null) {
82
- await query_session_enforce_limit(deps, account_id, max_sessions);
83
- }
88
+ const evicted = max_sessions != null ? await query_session_enforce_limit(deps, account_id, max_sessions) : [];
84
89
  const cookie_value = await create_session_cookie_value(keyring, session_token, session_options);
85
90
  set_session_cookie(c, cookie_value, session_options);
91
+ return evicted;
86
92
  };