@fuzdev/fuz_app 0.110.1 → 0.112.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 (210) hide show
  1. package/dist/actions/CLAUDE.md +15 -11
  2. package/dist/actions/action_rpc.d.ts +3 -3
  3. package/dist/actions/action_rpc.d.ts.map +1 -1
  4. package/dist/actions/action_rpc.js +4 -6
  5. package/dist/actions/peer_ping.d.ts +8 -0
  6. package/dist/actions/peer_ping.d.ts.map +1 -1
  7. package/dist/actions/peer_ping.js +8 -0
  8. package/dist/actions/peer_request.d.ts +1 -1
  9. package/dist/actions/peer_request.js +1 -1
  10. package/dist/actions/perform_action.d.ts +16 -9
  11. package/dist/actions/perform_action.d.ts.map +1 -1
  12. package/dist/actions/perform_action.js +30 -21
  13. package/dist/actions/register_action_ws.d.ts +1 -1
  14. package/dist/actions/register_action_ws.d.ts.map +1 -1
  15. package/dist/actions/register_action_ws.js +88 -21
  16. package/dist/auth/CLAUDE.md +59 -10
  17. package/dist/auth/account_action_specs.d.ts +0 -2
  18. package/dist/auth/account_action_specs.d.ts.map +1 -1
  19. package/dist/auth/account_actions.d.ts.map +1 -1
  20. package/dist/auth/account_actions.js +10 -1
  21. package/dist/auth/account_queries.d.ts +22 -0
  22. package/dist/auth/account_queries.d.ts.map +1 -1
  23. package/dist/auth/account_queries.js +70 -16
  24. package/dist/auth/account_schema.d.ts +27 -9
  25. package/dist/auth/account_schema.d.ts.map +1 -1
  26. package/dist/auth/account_schema.js +23 -3
  27. package/dist/auth/actor_search_actions.d.ts +3 -3
  28. package/dist/auth/actor_search_actions.d.ts.map +1 -1
  29. package/dist/auth/actor_search_actions.js +10 -6
  30. package/dist/auth/admin_action_specs.d.ts +0 -2
  31. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  32. package/dist/auth/api_token_queries.d.ts +9 -4
  33. package/dist/auth/api_token_queries.d.ts.map +1 -1
  34. package/dist/auth/api_token_queries.js +36 -8
  35. package/dist/auth/app_settings_queries.d.ts +8 -0
  36. package/dist/auth/app_settings_queries.d.ts.map +1 -1
  37. package/dist/auth/app_settings_queries.js +16 -3
  38. package/dist/auth/audit_log_queries.d.ts +21 -3
  39. package/dist/auth/audit_log_queries.d.ts.map +1 -1
  40. package/dist/auth/audit_log_queries.js +38 -7
  41. package/dist/auth/audit_log_schema.d.ts +0 -1
  42. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  43. package/dist/auth/auth_ddl.d.ts +6 -0
  44. package/dist/auth/auth_ddl.d.ts.map +1 -1
  45. package/dist/auth/auth_ddl.js +9 -0
  46. package/dist/auth/cell_actions.d.ts.map +1 -1
  47. package/dist/auth/cell_actions.js +3 -8
  48. package/dist/auth/cell_audit_actions.d.ts.map +1 -1
  49. package/dist/auth/cell_audit_actions.js +1 -1
  50. package/dist/auth/cell_field_actions.js +1 -1
  51. package/dist/auth/cell_grant_actions.js +1 -1
  52. package/dist/auth/cell_item_actions.js +1 -1
  53. package/dist/auth/invite_queries.d.ts +10 -0
  54. package/dist/auth/invite_queries.d.ts.map +1 -1
  55. package/dist/auth/invite_queries.js +28 -7
  56. package/dist/auth/migrations.d.ts.map +1 -1
  57. package/dist/auth/migrations.js +10 -0
  58. package/dist/auth/role_grant_offer_ddl.d.ts +34 -2
  59. package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
  60. package/dist/auth/role_grant_offer_ddl.js +61 -2
  61. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  62. package/dist/auth/role_grant_offer_queries.js +48 -53
  63. package/dist/auth/role_grant_queries.d.ts +18 -0
  64. package/dist/auth/role_grant_queries.d.ts.map +1 -1
  65. package/dist/auth/role_grant_queries.js +58 -18
  66. package/dist/auth/session_queries.d.ts +12 -3
  67. package/dist/auth/session_queries.d.ts.map +1 -1
  68. package/dist/auth/session_queries.js +26 -6
  69. package/dist/db/CLAUDE.md +34 -0
  70. package/dist/db/cell_audit_queries.d.ts.map +1 -1
  71. package/dist/db/cell_audit_queries.js +3 -1
  72. package/dist/db/cell_field_queries.d.ts +12 -3
  73. package/dist/db/cell_field_queries.d.ts.map +1 -1
  74. package/dist/db/cell_field_queries.js +25 -7
  75. package/dist/db/cell_grant_queries.d.ts +10 -2
  76. package/dist/db/cell_grant_queries.d.ts.map +1 -1
  77. package/dist/db/cell_grant_queries.js +29 -11
  78. package/dist/db/cell_item_queries.d.ts +13 -2
  79. package/dist/db/cell_item_queries.d.ts.map +1 -1
  80. package/dist/db/cell_item_queries.js +27 -7
  81. package/dist/db/cell_queries.d.ts +32 -3
  82. package/dist/db/cell_queries.d.ts.map +1 -1
  83. package/dist/db/cell_queries.js +57 -22
  84. package/dist/db/fact_queries.d.ts +7 -0
  85. package/dist/db/fact_queries.d.ts.map +1 -1
  86. package/dist/db/fact_queries.js +27 -4
  87. package/dist/db/schema_ready.d.ts +6 -1
  88. package/dist/db/schema_ready.d.ts.map +1 -1
  89. package/dist/db/schema_ready.js +6 -2
  90. package/dist/db/sql_columns.d.ts +144 -0
  91. package/dist/db/sql_columns.d.ts.map +1 -0
  92. package/dist/db/sql_columns.js +164 -0
  93. package/dist/db/sql_identifier.d.ts +6 -5
  94. package/dist/db/sql_identifier.d.ts.map +1 -1
  95. package/dist/db/sql_identifier.js +6 -5
  96. package/dist/http/CLAUDE.md +4 -2
  97. package/dist/http/jsonrpc_helpers.d.ts +13 -0
  98. package/dist/http/jsonrpc_helpers.d.ts.map +1 -1
  99. package/dist/http/jsonrpc_helpers.js +13 -0
  100. package/dist/http/route_spec.d.ts +2 -2
  101. package/dist/server/app_server.d.ts.map +1 -1
  102. package/dist/server/app_server.js +1 -2
  103. package/dist/server/serve_fact_route.d.ts.map +1 -1
  104. package/dist/server/serve_fact_route.js +7 -2
  105. package/dist/testing/CLAUDE.md +55 -28
  106. package/dist/testing/admin_integration.d.ts +0 -3
  107. package/dist/testing/admin_integration.d.ts.map +1 -1
  108. package/dist/testing/admin_integration.js +0 -1
  109. package/dist/testing/adversarial_404.d.ts.map +1 -1
  110. package/dist/testing/adversarial_404.js +4 -1
  111. package/dist/testing/adversarial_input.d.ts.map +1 -1
  112. package/dist/testing/adversarial_input.js +5 -4
  113. package/dist/testing/app_server.d.ts +4 -3
  114. package/dist/testing/app_server.d.ts.map +1 -1
  115. package/dist/testing/app_server.js +11 -5
  116. package/dist/testing/attack_surface.d.ts +16 -0
  117. package/dist/testing/attack_surface.d.ts.map +1 -1
  118. package/dist/testing/attack_surface.js +36 -13
  119. package/dist/testing/audit_completeness.d.ts +0 -3
  120. package/dist/testing/audit_completeness.d.ts.map +1 -1
  121. package/dist/testing/audit_completeness.js +0 -1
  122. package/dist/testing/auth_apps.d.ts +9 -2
  123. package/dist/testing/auth_apps.d.ts.map +1 -1
  124. package/dist/testing/auth_apps.js +9 -2
  125. package/dist/testing/cross_backend/account_lifecycle.d.ts +5 -4
  126. package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
  127. package/dist/testing/cross_backend/account_lifecycle.js +7 -0
  128. package/dist/testing/cross_backend/action_manifest.d.ts +19 -3
  129. package/dist/testing/cross_backend/action_manifest.d.ts.map +1 -1
  130. package/dist/testing/cross_backend/action_manifest.js +10 -4
  131. package/dist/testing/cross_backend/action_manifest_parity.d.ts +9 -3
  132. package/dist/testing/cross_backend/action_manifest_parity.d.ts.map +1 -1
  133. package/dist/testing/cross_backend/action_manifest_parity.js +6 -0
  134. package/dist/testing/cross_backend/actor_lookup.d.ts +3 -2
  135. package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
  136. package/dist/testing/cross_backend/body_size.d.ts +4 -2
  137. package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
  138. package/dist/testing/cross_backend/body_size.js +1 -1
  139. package/dist/testing/cross_backend/cell_crud.d.ts +2 -2
  140. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  141. package/dist/testing/cross_backend/cell_crud.js +10 -0
  142. package/dist/testing/cross_backend/cell_gated_create.d.ts +3 -3
  143. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  144. package/dist/testing/cross_backend/cell_grant_role.d.ts +2 -2
  145. package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
  146. package/dist/testing/cross_backend/cell_relations.d.ts +2 -2
  147. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
  148. package/dist/testing/cross_backend/expected_schema.json +1 -1
  149. package/dist/testing/cross_backend/fact_serving.d.ts +2 -2
  150. package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
  151. package/dist/testing/cross_backend/make_cross_backend_project.d.ts +17 -2
  152. package/dist/testing/cross_backend/make_cross_backend_project.d.ts.map +1 -1
  153. package/dist/testing/cross_backend/make_cross_backend_project.js +23 -3
  154. package/dist/testing/cross_backend/origin.d.ts +4 -2
  155. package/dist/testing/cross_backend/origin.d.ts.map +1 -1
  156. package/dist/testing/cross_backend/origin.js +1 -1
  157. package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
  158. package/dist/testing/cross_backend/peer_ping_ws.js +11 -7
  159. package/dist/testing/cross_backend/query_shape.d.ts +6 -5
  160. package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
  161. package/dist/testing/cross_backend/ready.d.ts +2 -2
  162. package/dist/testing/cross_backend/ready.d.ts.map +1 -1
  163. package/dist/testing/cross_backend/role_grant_offer_enumeration.d.ts.map +1 -1
  164. package/dist/testing/cross_backend/role_grant_offer_enumeration.js +7 -1
  165. package/dist/testing/cross_backend/setup.d.ts +26 -6
  166. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  167. package/dist/testing/cross_backend/standard.d.ts +5 -1
  168. package/dist/testing/cross_backend/standard.d.ts.map +1 -1
  169. package/dist/testing/cross_backend/standard.js +1 -5
  170. package/dist/testing/cross_backend/testing_backdoor.js +1 -1
  171. package/dist/testing/cross_backend/testing_reset_actions.d.ts +5 -0
  172. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  173. package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -1
  174. package/dist/testing/cross_backend/token_lifetime.js +6 -0
  175. package/dist/testing/cross_backend/token_scope_surface.d.ts +2 -2
  176. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  177. package/dist/testing/cross_backend/wire_shapes.d.ts +22 -0
  178. package/dist/testing/cross_backend/wire_shapes.d.ts.map +1 -0
  179. package/dist/testing/cross_backend/wire_shapes.js +41 -0
  180. package/dist/testing/data_exposure.d.ts +0 -3
  181. package/dist/testing/data_exposure.d.ts.map +1 -1
  182. package/dist/testing/data_exposure.js +0 -1
  183. package/dist/testing/db.d.ts +62 -7
  184. package/dist/testing/db.d.ts.map +1 -1
  185. package/dist/testing/db.js +86 -28
  186. package/dist/testing/integration.d.ts.map +1 -1
  187. package/dist/testing/integration.js +15 -2
  188. package/dist/testing/round_trip.d.ts +0 -3
  189. package/dist/testing/round_trip.d.ts.map +1 -1
  190. package/dist/testing/round_trip.js +0 -4
  191. package/dist/testing/rpc_attack_surface.js +2 -2
  192. package/dist/testing/rpc_round_trip.d.ts +0 -3
  193. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  194. package/dist/testing/rpc_round_trip.js +0 -1
  195. package/dist/testing/schema_generators.d.ts +15 -0
  196. package/dist/testing/schema_generators.d.ts.map +1 -1
  197. package/dist/testing/schema_generators.js +45 -11
  198. package/dist/testing/standard.d.ts +5 -1
  199. package/dist/testing/standard.d.ts.map +1 -1
  200. package/dist/testing/standard.js +2 -7
  201. package/dist/timestamp.d.ts +58 -0
  202. package/dist/timestamp.d.ts.map +1 -0
  203. package/dist/timestamp.js +57 -0
  204. package/dist/ui/AdminOverview.svelte +5 -10
  205. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  206. package/dist/ui/RoleGrantOfferHistory.svelte +3 -1
  207. package/dist/ui/RoleGrantOfferHistory.svelte.d.ts.map +1 -1
  208. package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -1
  209. package/dist/ui/role_grant_offers_state.svelte.js +6 -2
  210. package/package.json +1 -1
@@ -227,6 +227,16 @@ export const auth_migrations = [
227
227
  up: async (db) => {
228
228
  await db.query('CREATE INDEX IF NOT EXISTS idx_audit_log_metadata ON audit_log USING gin (metadata jsonb_path_ops)');
229
229
  }
230
+ },
231
+ // Drops `auth_session.last_seen_at`. The session lifetime is a hard cap
232
+ // set at mint — nothing updates the column post-mint on either spine, so
233
+ // it always equalled `created_at` (a decorative false signal the UIs
234
+ // stopped showing when the listings re-sorted by `created_at`).
235
+ {
236
+ name: 'drop_session_last_seen_at',
237
+ up: async (db) => {
238
+ await db.query('ALTER TABLE auth_session DROP COLUMN IF EXISTS last_seen_at');
239
+ }
230
240
  }
231
241
  ];
232
242
  /** Pre-composed migration namespace for auth tables. */
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Role grant offer DDL — `CREATE TABLE` + index statements and the index-side
3
- * sentinel constants the queries / migrations interpolate.
2
+ * Role grant offer DDL — `CREATE TABLE` + index statements, the index-side
3
+ * sentinel constants the queries / migrations interpolate, and the table's
4
+ * `ROLE_GRANT_OFFER_COLUMNS` projection const.
4
5
  *
5
6
  * Separated from `auth/role_grant_offer_schema.ts` so the schema module stays
6
7
  * Zod-only (paired with `auth/auth_ddl.ts` and `auth/audit_log_ddl.ts`).
@@ -22,6 +23,37 @@ export declare const ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID = "00000000-0000-0000-
22
23
  * `scope_kind = NULL` and `scope_id = NULL` together encode the global case.
23
24
  */
24
25
  export declare const ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN = "GLOBAL";
26
+ /**
27
+ * The full `role_grant_offer` column set, named explicitly so a row read
28
+ * fails loud on schema drift (see `ACCOUNT_COLUMNS` in
29
+ * `auth/account_queries.ts` for the outage class). Column order follows
30
+ * `RoleGrantOffer` and `ROLE_GRANT_OFFER_SCHEMA` below.
31
+ *
32
+ * Lives here rather than in `auth/role_grant_offer_queries.ts` because two
33
+ * query modules project this table — see the placement rule in
34
+ * `db/sql_columns.ts`.
35
+ */
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"];
37
+ /**
38
+ * The `role_grant_offer` timestamp override, by row qualifier (`''` bare,
39
+ * `o` for the history JOIN, `u` for the `updated` CTE's outer select). Lives
40
+ * beside the const for the same reason the const does — both query modules
41
+ * that project this table need it.
42
+ *
43
+ * A cascade's inner `RETURNING` deliberately projects the *raw* columns: the
44
+ * outer select over the `updated` CTE applies this, and `to_char` over an
45
+ * already-formatted text column would fail.
46
+ */
47
+ export declare const role_grant_offer_expr: (alias: string) => import("../db/sql_columns.js").ColumnExpr;
48
+ /**
49
+ * Outer `SELECT` over an `updated` CTE of `role_grant_offer` rows, joining
50
+ * the grantor's `account_id` on as `from_account_id` — the `SupersededOffer` /
51
+ * `DeclinedOffer` shape every offer-superseding cascade returns (the revoke
52
+ * cascades in `auth/role_grant_queries.ts`, decline + accept-supersede in
53
+ * `auth/role_grant_offer_queries.ts`). The CTE must be named `updated` and
54
+ * `RETURNING` the full offer row.
55
+ */
56
+ export declare const ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT: string;
25
57
  export declare const ROLE_GRANT_OFFER_SCHEMA = "\nCREATE TABLE IF NOT EXISTS role_grant_offer (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n from_actor_id UUID NOT NULL REFERENCES actor(id) ON DELETE CASCADE,\n to_account_id UUID NOT NULL REFERENCES account(id) ON DELETE CASCADE,\n to_actor_id UUID NULL REFERENCES actor(id) ON DELETE CASCADE,\n role TEXT NOT NULL,\n scope_kind TEXT NULL,\n scope_id UUID NULL,\n message TEXT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n expires_at TIMESTAMPTZ NOT NULL,\n accepted_at TIMESTAMPTZ NULL,\n declined_at TIMESTAMPTZ NULL,\n decline_reason TEXT NULL,\n retracted_at TIMESTAMPTZ NULL,\n superseded_at TIMESTAMPTZ NULL,\n resulting_role_grant_id UUID NULL REFERENCES role_grant(id) ON DELETE SET NULL,\n CONSTRAINT role_grant_offer_single_terminal CHECK (\n (accepted_at IS NOT NULL)::int\n + (declined_at IS NOT NULL)::int\n + (retracted_at IS NOT NULL)::int\n + (superseded_at IS NOT NULL)::int\n <= 1\n ),\n CONSTRAINT role_grant_offer_role_grant_iff_accepted CHECK (\n (accepted_at IS NOT NULL) = (resulting_role_grant_id IS NOT NULL)\n ),\n CONSTRAINT role_grant_offer_reason_iff_declined CHECK (\n decline_reason IS NULL OR declined_at IS NOT NULL\n ),\n CONSTRAINT role_grant_offer_scope_kind_paired CHECK (\n (scope_kind IS NULL) = (scope_id IS NULL)\n )\n)";
26
58
  /**
27
59
  * At most one pending offer per (to_account, role, scope_kind, scope, from_actor).
@@ -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;;;;;;;;;;;;;;GAcG;AAEH,mHAAmH;AACnH,eAAO,MAAM,oCAAoC,yCAAyC,CAAC;AAE3F;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,WAAW,CAAC;AAEjE,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;;;;;;;;;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,6 +1,7 @@
1
1
  /**
2
- * Role grant offer DDL — `CREATE TABLE` + index statements and the index-side
3
- * sentinel constants the queries / migrations interpolate.
2
+ * Role grant offer DDL — `CREATE TABLE` + index statements, the index-side
3
+ * sentinel constants the queries / migrations interpolate, and the table's
4
+ * `ROLE_GRANT_OFFER_COLUMNS` projection const.
4
5
  *
5
6
  * Separated from `auth/role_grant_offer_schema.ts` so the schema module stays
6
7
  * Zod-only (paired with `auth/auth_ddl.ts` and `auth/audit_log_ddl.ts`).
@@ -13,6 +14,7 @@
13
14
  *
14
15
  * @module
15
16
  */
17
+ import { iso8601_timestamp_expr, qualify_columns } from '../db/sql_columns.js';
16
18
  /** Sentinel UUID used inside the partial unique indexes to collapse `scope_id IS NULL` into a comparable value. */
17
19
  export const ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID = '00000000-0000-0000-0000-000000000000';
18
20
  /**
@@ -22,6 +24,63 @@ export const ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID = '00000000-0000-0000-0000-000
22
24
  * `scope_kind = NULL` and `scope_id = NULL` together encode the global case.
23
25
  */
24
26
  export const ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN = 'GLOBAL';
27
+ /**
28
+ * The full `role_grant_offer` column set, named explicitly so a row read
29
+ * fails loud on schema drift (see `ACCOUNT_COLUMNS` in
30
+ * `auth/account_queries.ts` for the outage class). Column order follows
31
+ * `RoleGrantOffer` and `ROLE_GRANT_OFFER_SCHEMA` below.
32
+ *
33
+ * Lives here rather than in `auth/role_grant_offer_queries.ts` because two
34
+ * query modules project this table — see the placement rule in
35
+ * `db/sql_columns.ts`.
36
+ */
37
+ export const ROLE_GRANT_OFFER_COLUMNS = [
38
+ 'id',
39
+ 'from_actor_id',
40
+ 'to_account_id',
41
+ 'to_actor_id',
42
+ 'role',
43
+ 'scope_kind',
44
+ 'scope_id',
45
+ 'message',
46
+ 'created_at',
47
+ 'expires_at',
48
+ 'accepted_at',
49
+ 'declined_at',
50
+ 'decline_reason',
51
+ 'retracted_at',
52
+ 'superseded_at',
53
+ 'resulting_role_grant_id'
54
+ ];
55
+ /**
56
+ * The `role_grant_offer` timestamp override, by row qualifier (`''` bare,
57
+ * `o` for the history JOIN, `u` for the `updated` CTE's outer select). Lives
58
+ * beside the const for the same reason the const does — both query modules
59
+ * that project this table need it.
60
+ *
61
+ * A cascade's inner `RETURNING` deliberately projects the *raw* columns: the
62
+ * outer select over the `updated` CTE applies this, and `to_char` over an
63
+ * already-formatted text column would fail.
64
+ */
65
+ export const role_grant_offer_expr = iso8601_timestamp_expr(ROLE_GRANT_OFFER_COLUMNS, [
66
+ 'created_at',
67
+ 'expires_at',
68
+ 'accepted_at',
69
+ 'declined_at',
70
+ 'retracted_at',
71
+ 'superseded_at'
72
+ ]);
73
+ /**
74
+ * Outer `SELECT` over an `updated` CTE of `role_grant_offer` rows, joining
75
+ * the grantor's `account_id` on as `from_account_id` — the `SupersededOffer` /
76
+ * `DeclinedOffer` shape every offer-superseding cascade returns (the revoke
77
+ * cascades in `auth/role_grant_queries.ts`, decline + accept-supersede in
78
+ * `auth/role_grant_offer_queries.ts`). The CTE must be named `updated` and
79
+ * `RETURNING` the full offer row.
80
+ */
81
+ 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
+ FROM updated u
83
+ JOIN actor grantor ON grantor.id = u.from_actor_id`;
25
84
  export const ROLE_GRANT_OFFER_SCHEMA = `
26
85
  CREATE TABLE IF NOT EXISTS role_grant_offer (
27
86
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -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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"}
@@ -12,7 +12,9 @@
12
12
  * @module
13
13
  */
14
14
  import { assert_row } from '../db/assert_row.js';
15
- import { ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN, ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID } from './role_grant_offer_ddl.js';
15
+ import { columns_sql, qualify_columns } from '../db/sql_columns.js';
16
+ import { ROLE_GRANT_OFFER_COLUMNS, role_grant_offer_expr, ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN, ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID, ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT } from './role_grant_offer_ddl.js';
17
+ import { query_create_role_grant, query_role_grant_by_id } from './role_grant_queries.js';
16
18
  import { query_audit_log } from './audit_log_queries.js';
17
19
  /**
18
20
  * Error thrown by offer-lifecycle queries when the offer is in a non-pending
@@ -152,7 +154,7 @@ export const query_role_grant_offer_create = async (deps, input) => {
152
154
  to_actor_id = EXCLUDED.to_actor_id,
153
155
  message = EXCLUDED.message,
154
156
  expires_at = EXCLUDED.expires_at
155
- RETURNING *`, [
157
+ RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS, role_grant_offer_expr(''))}`, [
156
158
  input.from_actor_id,
157
159
  input.to_account_id,
158
160
  input.to_actor_id ?? null,
@@ -191,11 +193,10 @@ export const query_role_grant_offer_decline = async (deps, offer_id, to_account_
191
193
  AND declined_at IS NULL
192
194
  AND retracted_at IS NULL
193
195
  AND superseded_at IS NULL
194
- RETURNING *
196
+ -- raw columns; the outer grantor select formats them
197
+ RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)}
195
198
  )
196
- SELECT u.*, grantor.account_id AS from_account_id
197
- FROM updated u
198
- JOIN actor grantor ON grantor.id = u.from_actor_id`, [offer_id, to_account_id, reason ?? null]);
199
+ ${ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT}`, [offer_id, to_account_id, reason ?? null]);
199
200
  if (updated)
200
201
  return updated;
201
202
  return resolve_terminal_or_missing(deps, offer_id, { to_account_id });
@@ -220,7 +221,7 @@ export const query_role_grant_offer_retract = async (deps, offer_id, from_actor_
220
221
  AND declined_at IS NULL
221
222
  AND retracted_at IS NULL
222
223
  AND superseded_at IS NULL
223
- RETURNING *`, [offer_id, from_actor_id]);
224
+ RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS, role_grant_offer_expr(''))}`, [offer_id, from_actor_id]);
224
225
  if (updated)
225
226
  return updated;
226
227
  return resolve_terminal_or_missing(deps, offer_id, { from_actor_id });
@@ -238,7 +239,7 @@ const resolve_terminal_or_missing = async (deps, offer_id, scope) => {
238
239
  conditions.push(`from_actor_id = $${idx++}`);
239
240
  params.push(scope.from_actor_id);
240
241
  }
241
- const row = await deps.db.query_one(`SELECT * FROM role_grant_offer WHERE ${conditions.join(' AND ')}`, params);
242
+ const row = await deps.db.query_one(`SELECT ${columns_sql(ROLE_GRANT_OFFER_COLUMNS, role_grant_offer_expr(''))} FROM role_grant_offer WHERE ${conditions.join(' AND ')}`, params);
242
243
  if (!row)
243
244
  return null;
244
245
  if (row.accepted_at || row.declined_at || row.retracted_at || row.superseded_at) {
@@ -254,14 +255,14 @@ const resolve_terminal_or_missing = async (deps, offer_id, scope) => {
254
255
  * sweep (`query_role_grant_offer_sweep_expired`) handles audit tombstoning.
255
256
  */
256
257
  export const query_role_grant_offer_list = async (deps, to_account_id) => {
257
- return deps.db.query(`SELECT * FROM role_grant_offer
258
+ return deps.db.query(`SELECT ${columns_sql(ROLE_GRANT_OFFER_COLUMNS, role_grant_offer_expr(''))} FROM role_grant_offer
258
259
  WHERE to_account_id = $1
259
260
  AND accepted_at IS NULL
260
261
  AND declined_at IS NULL
261
262
  AND retracted_at IS NULL
262
263
  AND superseded_at IS NULL
263
264
  AND expires_at > NOW()
264
- ORDER BY expires_at ASC`, [to_account_id]);
265
+ ORDER BY role_grant_offer.expires_at ASC`, [to_account_id]);
265
266
  };
266
267
  /**
267
268
  * List every offer involving an account (either direction), newest first.
@@ -269,7 +270,7 @@ export const query_role_grant_offer_list = async (deps, to_account_id) => {
269
270
  * Includes terminal offers — used by the grantor-side admin / history view.
270
271
  */
271
272
  export const query_role_grant_offer_history_for_account = async (deps, account_id, limit = 100, offset = 0) => {
272
- return deps.db.query(`SELECT o.* FROM role_grant_offer o
273
+ return deps.db.query(`SELECT ${qualify_columns(ROLE_GRANT_OFFER_COLUMNS, 'o', role_grant_offer_expr('o'))} FROM role_grant_offer o
273
274
  LEFT JOIN actor a ON a.id = o.from_actor_id
274
275
  WHERE o.to_account_id = $1 OR a.account_id = $1
275
276
  ORDER BY o.created_at DESC
@@ -280,7 +281,7 @@ export const query_role_grant_offer_history_for_account = async (deps, account_i
280
281
  * expired (server-side filter), or missing.
281
282
  */
282
283
  export const query_role_grant_offer_find_pending = async (deps, offer_id) => {
283
- const row = await deps.db.query_one(`SELECT * FROM role_grant_offer
284
+ const row = await deps.db.query_one(`SELECT ${columns_sql(ROLE_GRANT_OFFER_COLUMNS, role_grant_offer_expr(''))} FROM role_grant_offer
284
285
  WHERE id = $1
285
286
  AND accepted_at IS NULL
286
287
  AND declined_at IS NULL
@@ -298,13 +299,13 @@ export const query_role_grant_offer_find_pending = async (deps, offer_id) => {
298
299
  * already exists for the offer id).
299
300
  */
300
301
  export const query_role_grant_offer_sweep_expired = async (deps) => {
301
- return deps.db.query(`SELECT * FROM role_grant_offer
302
+ return deps.db.query(`SELECT ${columns_sql(ROLE_GRANT_OFFER_COLUMNS, role_grant_offer_expr(''))} FROM role_grant_offer
302
303
  WHERE accepted_at IS NULL
303
304
  AND declined_at IS NULL
304
305
  AND retracted_at IS NULL
305
306
  AND superseded_at IS NULL
306
307
  AND expires_at <= NOW()
307
- ORDER BY expires_at ASC`);
308
+ ORDER BY role_grant_offer.expires_at ASC`);
308
309
  };
309
310
  /**
310
311
  * Accept an offer atomically: mark accepted, insert the role_grant, stamp
@@ -346,18 +347,27 @@ export const query_accept_offer = async (deps, input) => {
346
347
  // We defer writing `accepted_at` until the role_grant row exists — the
347
348
  // `role_grant_offer_role_grant_iff_accepted` CHECK constraint demands both be set
348
349
  // (or neither) at row-visibility time.
349
- const locked = await deps.db.query_one(`SELECT * FROM role_grant_offer
350
+ //
351
+ // Trailing `expires_at > NOW() AS still_valid` rides on the FOR UPDATE row
352
+ // so the expiry branch below compares the **raw** column on Postgres' clock
353
+ // — the projected `expires_at` is floored to the second, so comparing it in
354
+ // JS would report expiry up to a second early. Twin of the Rust
355
+ // `query_accept_offer`'s `o.expires_at > NOW() AS still_valid`.
356
+ const locked_row = await deps.db.query_one(`SELECT ${columns_sql(ROLE_GRANT_OFFER_COLUMNS, role_grant_offer_expr(''))},
357
+ role_grant_offer.expires_at > NOW() AS still_valid
358
+ FROM role_grant_offer
350
359
  WHERE id = $1 AND to_account_id = $2
351
360
  FOR UPDATE`, [offer_id, to_account_id]);
352
- if (!locked) {
361
+ if (!locked_row) {
353
362
  throw new RoleGrantOfferNotFoundError(offer_id);
354
363
  }
364
+ // `still_valid` is the comparison result, not a column of the offer — split
365
+ // it off so `locked` stays exactly `ROLE_GRANT_OFFER_COLUMNS`.
366
+ const { still_valid, ...locked } = locked_row;
355
367
  if (locked.accepted_at) {
356
368
  // Race winner already committed; return the pre-existing role_grant.
357
369
  // `role_grant_offer_role_grant_iff_accepted` CHECK guarantees resulting_role_grant_id is non-null.
358
- const role_grant = assert_row(await deps.db.query_one(`SELECT * FROM role_grant WHERE id = $1`, [
359
- locked.resulting_role_grant_id
360
- ]), 'resulting_role_grant lookup');
370
+ const role_grant = assert_row(await query_role_grant_by_id(deps, locked.resulting_role_grant_id), 'resulting_role_grant lookup');
361
371
  // Multi-actor guard: two actors on the same recipient account may
362
372
  // both race an account-grain offer — the loser must not silently
363
373
  // receive the winner's role_grant (which would tell them "you got it"
@@ -379,8 +389,9 @@ export const query_accept_offer = async (deps, input) => {
379
389
  }
380
390
  // Expiry check AFTER the accepted-path: a validly-accepted offer past its
381
391
  // expires_at still returns the role_grant idempotently. Only pending offers
382
- // past expiry reach this branch.
383
- if (new Date(locked.expires_at) <= new Date()) {
392
+ // past expiry reach this branch. `still_valid` rode on the FOR UPDATE row
393
+ // above, so the comparison stays on the raw column and Postgres' clock.
394
+ if (!still_valid) {
384
395
  throw new RoleGrantOfferExpiredError(offer_id);
385
396
  }
386
397
  // Actor-targeted offer gate. When the offer is account-grain
@@ -411,37 +422,22 @@ export const query_accept_offer = async (deps, input) => {
411
422
  if (!actor_check) {
412
423
  throw new Error(`Accepting actor ${actor_id} does not belong to account ${to_account_id} (offer ${offer_id})`);
413
424
  }
414
- // Insert the role_grant. Uses the normal grant idempotency — if another
415
- // code path already granted the same (actor, role, scope_kind, scope), reuse it.
416
- const granted_role_grant = await deps.db.query_one(`INSERT INTO role_grant (actor_id, role, scope_kind, scope_id, granted_by, source_offer_id)
417
- VALUES ($1, $2, $3, $4, $5, $6)
418
- ON CONFLICT (
419
- actor_id,
420
- role,
421
- COALESCE(scope_kind, '${ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN}'),
422
- COALESCE(scope_id, '${ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID}'::uuid)
423
- )
424
- WHERE revoked_at IS NULL
425
- DO NOTHING
426
- RETURNING *`, [actor_id, locked.role, locked.scope_kind, locked.scope_id, locked.from_actor_id, locked.id]);
427
- let role_grant;
428
- if (granted_role_grant) {
429
- role_grant = granted_role_grant;
430
- }
431
- else {
432
- const existing = await deps.db.query_one(`SELECT * FROM role_grant
433
- WHERE actor_id = $1
434
- AND role = $2
435
- AND scope_kind IS NOT DISTINCT FROM $3
436
- AND scope_id IS NOT DISTINCT FROM $4
437
- AND revoked_at IS NULL`, [actor_id, locked.role, locked.scope_kind, locked.scope_id]);
438
- role_grant = assert_row(existing, 'query_accept_offer idempotent role_grant lookup');
439
- }
425
+ // Insert the role_grant through the shared idempotent grant — if another
426
+ // code path already granted the same (actor, role, scope_kind, scope), it
427
+ // returns that row. Stamps the grantor and the source offer on the row.
428
+ const role_grant = await query_create_role_grant(deps, {
429
+ actor_id,
430
+ role: locked.role,
431
+ scope_kind: locked.scope_kind,
432
+ scope_id: locked.scope_id,
433
+ granted_by: locked.from_actor_id,
434
+ source_offer_id: locked.id
435
+ });
440
436
  // Single UPDATE sets both sides of the CHECK constraint at once.
441
437
  const offer_accepted = await deps.db.query_one(`UPDATE role_grant_offer
442
438
  SET accepted_at = NOW(), resulting_role_grant_id = $2
443
439
  WHERE id = $1
444
- RETURNING *`, [locked.id, role_grant.id]);
440
+ RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS, role_grant_offer_expr(''))}`, [locked.id, role_grant.id]);
445
441
  const offer = assert_row(offer_accepted, 'mark offer accepted');
446
442
  // Supersede sibling pending offers for the same (to_account, role, scope).
447
443
  // Forecloses the "accept this other sibling later to get the role back
@@ -459,13 +455,12 @@ export const query_accept_offer = async (deps, input) => {
459
455
  AND declined_at IS NULL
460
456
  AND retracted_at IS NULL
461
457
  AND superseded_at IS NULL
462
- RETURNING *
458
+ -- raw columns; the outer grantor select formats them
459
+ RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)}
463
460
  )
464
- SELECT u.*, grantor.account_id AS from_account_id
465
- FROM updated u
466
- JOIN actor grantor ON grantor.id = u.from_actor_id`, [to_account_id, offer.role, offer.scope_id, offer.id]);
461
+ ${ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT}`, [to_account_id, offer.role, offer.scope_id, offer.id]);
467
462
  // Emit audit events in-transaction (atomic with the role_grant insert).
468
- // `RETURNING *` after the SET guarantees `offer.resulting_role_grant_id === role_grant.id`.
463
+ // The `RETURNING` after the SET guarantees `offer.resulting_role_grant_id === role_grant.id`.
469
464
  // Accept binds the actor deterministically — populate both target
470
465
  // columns to mirror `role_grant_create` (the in-tx pair) so forensic
471
466
  // queries don't have to split between the two events.
@@ -10,6 +10,15 @@ import type { Uuid } from '@fuzdev/fuz_util/id.ts';
10
10
  import type { QueryDeps } from '../db/query_deps.js';
11
11
  import type { RoleGrant, CreateRoleGrantInput } from './account_schema.js';
12
12
  import type { SupersededOffer } from './role_grant_offer_schema.js';
13
+ /**
14
+ * The full `role_grant` column set, named explicitly so a row read fails loud
15
+ * on schema drift (see `ACCOUNT_COLUMNS` in `auth/account_queries.ts` for the
16
+ * outage class). Column order follows `RoleGrant` — the frozen `fuz_auth` v0
17
+ * body plus the appended `scope_kind` / `scope_id` / `revoked_reason` /
18
+ * `source_offer_id` columns. Keep in sync with `RoleGrant` and the
19
+ * `role_grant` migration chain in `auth/auth_ddl.ts`.
20
+ */
21
+ export declare const ROLE_GRANT_COLUMNS: readonly ["id", "actor_id", "role", "scope_kind", "scope_id", "created_at", "expires_at", "revoked_at", "revoked_by", "revoked_reason", "granted_by", "source_offer_id"];
13
22
  /**
14
23
  * Grant a role_grant to an actor.
15
24
  * Idempotent — if an active role_grant already exists for this actor, role, and
@@ -31,6 +40,15 @@ import type { SupersededOffer } from './role_grant_offer_schema.js';
31
40
  * @mutates `role_grant` table - inserts a row when no active role_grant matches `(actor_id, role, scope_kind, scope_id)`
32
41
  */
33
42
  export declare const query_create_role_grant: (deps: QueryDeps, input: CreateRoleGrantInput) => Promise<RoleGrant>;
43
+ /**
44
+ * Find a role_grant by id, regardless of revoked / expired state.
45
+ *
46
+ * Unscoped by design — the one caller is `query_accept_offer`'s race-loser
47
+ * path, which resolves an already-accepted offer's `resulting_role_grant_id`
48
+ * (a row the offer's own IDOR guard has already vouched for). Authority
49
+ * checks stay at the call site.
50
+ */
51
+ export declare const query_role_grant_by_id: (deps: QueryDeps, id: Uuid) => Promise<RoleGrant | undefined>;
34
52
  /**
35
53
  * Look up the role of an active role_grant (constrained to a specific
36
54
  * actor) plus the actor's `account_id`.
@@ -1 +1 @@
1
- {"version":3,"file":"role_grant_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_queries.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"role_grant_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_grant_queries.ts"],"names":[],"mappings":"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"}
@@ -7,7 +7,41 @@
7
7
  * @module
8
8
  */
9
9
  import { assert_row } from '../db/assert_row.js';
10
- import { ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN, ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID } from './role_grant_offer_ddl.js';
10
+ import { columns_sql, iso8601_timestamp_expr, qualify_columns } from '../db/sql_columns.js';
11
+ import { ROLE_GRANT_OFFER_COLUMNS, ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN, ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID, ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT } from './role_grant_offer_ddl.js';
12
+ /**
13
+ * The full `role_grant` column set, named explicitly so a row read fails loud
14
+ * on schema drift (see `ACCOUNT_COLUMNS` in `auth/account_queries.ts` for the
15
+ * outage class). Column order follows `RoleGrant` — the frozen `fuz_auth` v0
16
+ * body plus the appended `scope_kind` / `scope_id` / `revoked_reason` /
17
+ * `source_offer_id` columns. Keep in sync with `RoleGrant` and the
18
+ * `role_grant` migration chain in `auth/auth_ddl.ts`.
19
+ */
20
+ export const ROLE_GRANT_COLUMNS = [
21
+ 'id',
22
+ 'actor_id',
23
+ 'role',
24
+ 'scope_kind',
25
+ 'scope_id',
26
+ 'created_at',
27
+ 'expires_at',
28
+ 'revoked_at',
29
+ 'revoked_by',
30
+ 'revoked_reason',
31
+ 'granted_by',
32
+ 'source_offer_id'
33
+ ];
34
+ /**
35
+ * The `role_grant` timestamp override; every read here is on the bare table.
36
+ * `created_at` / `expires_at` ride `RoleGrantSummaryJson` (twin of the
37
+ * spine's `role_grant_summary_columns`); `revoked_at` is what
38
+ * `is_role_grant_active` reads.
39
+ */
40
+ const role_grant_expr = iso8601_timestamp_expr(ROLE_GRANT_COLUMNS, [
41
+ 'created_at',
42
+ 'expires_at',
43
+ 'revoked_at'
44
+ ])('');
11
45
  /**
12
46
  * Grant a role_grant to an actor.
13
47
  * Idempotent — if an active role_grant already exists for this actor, role, and
@@ -39,7 +73,7 @@ export const query_create_role_grant = async (deps, input) => {
39
73
  )
40
74
  WHERE revoked_at IS NULL
41
75
  DO NOTHING
42
- RETURNING *`, [
76
+ RETURNING ${columns_sql(ROLE_GRANT_COLUMNS, role_grant_expr)}`, [
43
77
  input.actor_id,
44
78
  input.role,
45
79
  input.scope_kind ?? null,
@@ -51,7 +85,7 @@ export const query_create_role_grant = async (deps, input) => {
51
85
  if (inserted)
52
86
  return inserted;
53
87
  // Active role_grant already exists — return it (idempotent grant).
54
- const existing = await deps.db.query_one(`SELECT * FROM role_grant
88
+ const existing = await deps.db.query_one(`SELECT ${columns_sql(ROLE_GRANT_COLUMNS, role_grant_expr)} FROM role_grant
55
89
  WHERE actor_id = $1
56
90
  AND role = $2
57
91
  AND scope_kind IS NOT DISTINCT FROM $3
@@ -59,6 +93,15 @@ export const query_create_role_grant = async (deps, input) => {
59
93
  AND revoked_at IS NULL`, [input.actor_id, input.role, input.scope_kind ?? null, input.scope_id ?? null]);
60
94
  return assert_row(existing, 'idempotent role_grant grant');
61
95
  };
96
+ /**
97
+ * Find a role_grant by id, regardless of revoked / expired state.
98
+ *
99
+ * Unscoped by design — the one caller is `query_accept_offer`'s race-loser
100
+ * path, which resolves an already-accepted offer's `resulting_role_grant_id`
101
+ * (a row the offer's own IDOR guard has already vouched for). Authority
102
+ * checks stay at the call site.
103
+ */
104
+ export const query_role_grant_by_id = async (deps, id) => deps.db.query_one(`SELECT ${columns_sql(ROLE_GRANT_COLUMNS, role_grant_expr)} FROM role_grant WHERE id = $1`, [id]);
62
105
  /**
63
106
  * Look up the role of an active role_grant (constrained to a specific
64
107
  * actor) plus the actor's `account_id`.
@@ -136,11 +179,10 @@ export const query_revoke_role_grant = async (deps, role_grant_id, actor_id, rev
136
179
  AND o.declined_at IS NULL
137
180
  AND o.retracted_at IS NULL
138
181
  AND o.superseded_at IS NULL
139
- RETURNING o.*
182
+ -- raw columns; the outer grantor select formats them
183
+ RETURNING ${qualify_columns(ROLE_GRANT_OFFER_COLUMNS, 'o')}
140
184
  )
141
- SELECT u.*, grantor.account_id AS from_account_id
142
- FROM updated u
143
- JOIN actor grantor ON grantor.id = u.from_actor_id`, [actor_id, revoked.role, revoked.scope_id]);
185
+ ${ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT}`, [actor_id, revoked.role, revoked.scope_id]);
144
186
  return {
145
187
  id: revoked.id,
146
188
  role: revoked.role,
@@ -153,11 +195,11 @@ export const query_revoke_role_grant = async (deps, role_grant_id, actor_id, rev
153
195
  * Find all active (non-revoked, non-expired) role_grants for an actor.
154
196
  */
155
197
  export const query_role_grant_find_active_for_actor = async (deps, actor_id) => {
156
- return deps.db.query(`SELECT * FROM role_grant
198
+ return deps.db.query(`SELECT ${columns_sql(ROLE_GRANT_COLUMNS, role_grant_expr)} FROM role_grant
157
199
  WHERE actor_id = $1
158
200
  AND revoked_at IS NULL
159
201
  AND (expires_at IS NULL OR expires_at > NOW())
160
- ORDER BY created_at`, [actor_id]);
202
+ ORDER BY role_grant.created_at`, [actor_id]);
161
203
  };
162
204
  /**
163
205
  * Check if an actor has an active role_grant for a given role.
@@ -284,7 +326,7 @@ export const query_count_active_accounts_with_global_role = async (deps, role) =
284
326
  * List all role_grants for an actor (including revoked/expired).
285
327
  */
286
328
  export const query_role_grant_list_for_actor = async (deps, actor_id) => {
287
- return deps.db.query(`SELECT * FROM role_grant WHERE actor_id = $1 ORDER BY created_at DESC`, [actor_id]);
329
+ return deps.db.query(`SELECT ${columns_sql(ROLE_GRANT_COLUMNS, role_grant_expr)} FROM role_grant WHERE actor_id = $1 ORDER BY role_grant.created_at DESC`, [actor_id]);
288
330
  };
289
331
  /**
290
332
  * Find the account ID of an account that holds an active role_grant for a given role.
@@ -366,11 +408,10 @@ export const query_role_grant_revoke_for_scope = async (deps, scope_id, revoked_
366
408
  AND o.declined_at IS NULL
367
409
  AND o.retracted_at IS NULL
368
410
  AND o.superseded_at IS NULL
369
- RETURNING o.*
411
+ -- raw columns; the outer grantor select formats them
412
+ RETURNING ${qualify_columns(ROLE_GRANT_OFFER_COLUMNS, 'o')}
370
413
  )
371
- SELECT u.*, grantor.account_id AS from_account_id
372
- FROM updated u
373
- JOIN actor grantor ON grantor.id = u.from_actor_id`, [scope_id]);
414
+ ${ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT}`, [scope_id]);
374
415
  return { revoked, superseded_offers };
375
416
  };
376
417
  /**
@@ -420,10 +461,9 @@ export const query_role_grant_revoke_role = async (deps, actor_id, role, revoked
420
461
  AND o.declined_at IS NULL
421
462
  AND o.retracted_at IS NULL
422
463
  AND o.superseded_at IS NULL
423
- RETURNING o.*
464
+ -- raw columns; the outer grantor select formats them
465
+ RETURNING ${qualify_columns(ROLE_GRANT_OFFER_COLUMNS, 'o')}
424
466
  )
425
- SELECT u.*, grantor.account_id AS from_account_id
426
- FROM updated u
427
- JOIN actor grantor ON grantor.id = u.from_actor_id`, [actor_id, role]);
467
+ ${ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT}`, [actor_id, role]);
428
468
  return { revoked, superseded_offers };
429
469
  };
@@ -43,6 +43,17 @@ export declare const generate_session_token: () => string;
43
43
  * @mutates `auth_session` table - inserts a row keyed by `token_hash`
44
44
  */
45
45
  export declare const query_create_session: (deps: QueryDeps, token_hash: string, account_id: string, expires_at: Date) => Promise<void>;
46
+ /**
47
+ * The full `auth_session` column set, named explicitly so a row read fails
48
+ * loud on schema drift — `SELECT *` would silently carry a dropped or
49
+ * leftover column into the strict-validated wire shapes (see
50
+ * `ACCOUNT_COLUMNS` in `auth/account_queries.ts` for the outage class this
51
+ * discipline exists to prevent; the Rust twin already names columns at every
52
+ * session site). Keep in sync with `AuthSession` and the migration chain's
53
+ * end state — not the frozen v0 DDL in `auth/auth_ddl.ts`, which still
54
+ * creates `last_seen_at` for the appended drop migration to remove.
55
+ */
56
+ export declare const AUTH_SESSION_COLUMNS: readonly ["id", "account_id", "created_at", "expires_at"];
46
57
  /**
47
58
  * Get a session if it exists, is not expired, and has not been revoked.
48
59
  *
@@ -132,9 +143,7 @@ export declare const query_session_enforce_limit: (deps: QueryDeps, account_id:
132
143
  * List all active sessions across all accounts with usernames.
133
144
  *
134
145
  * Ordered by `created_at DESC` (newest session first), matching the
135
- * per-account listing. `last_seen_at` is still selected for the wire shape
136
- * but is decorative — nothing updates it post-mint (it always equals
137
- * `created_at`); it is slated for removal on its own twin migration.
146
+ * per-account listing.
138
147
  *
139
148
  * @param deps - query dependencies
140
149
  * @param limit - maximum entries to return
@@ -1 +1 @@
1
- {"version":3,"file":"session_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAElE;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,QAA2B,CAAC;AAEjE;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,SAElD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAO,MAEzC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,EAClB,YAAY,IAAI,KACd,OAAO,CAAC,IAAI,CAMd,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,WAAW,GAAG,SAAS,CAKjC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAMjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAU,KACR,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAK5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAc,MAAM,KAClB,OAAO,CAAC,MAAM,CAYhB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,cAAW,KACT,OAAO,CAAC,KAAK,CAAC,WAAW,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CASnD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,MAAM,CAKnF,CAAC"}
1
+ {"version":3,"file":"session_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/session_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAElE;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,QAA2B,CAAC;AAEjE;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,SAElD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,QAAO,MAEzC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,EAClB,YAAY,IAAI,KACd,OAAO,CAAC,IAAI,CAMd,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,2DAKoB,CAAC;AAQtD;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,WAAW,GAAG,SAAS,CAKjC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,IAAI,CAEd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAMjB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,EACf,YAAY,MAAM,KAChB,OAAO,CAAC,MAAM,CAMhB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAU,KACR,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAK5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,YAAY,MAAM,EAClB,cAAc,MAAM,KAClB,OAAO,CAAC,MAAM,CAYhB,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,cAAW,KACT,OAAO,CAAC,KAAK,CAAC,WAAW,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CASnD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,6BAA6B,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,MAAM,CAKnF,CAAC"}