@fuzdev/fuz_app 0.111.0 → 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 (179) hide show
  1. package/dist/actions/CLAUDE.md +15 -11
  2. package/dist/actions/action_rpc.d.ts +1 -1
  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 +37 -7
  17. package/dist/auth/account_actions.d.ts.map +1 -1
  18. package/dist/auth/account_actions.js +10 -1
  19. package/dist/auth/account_queries.d.ts.map +1 -1
  20. package/dist/auth/account_queries.js +36 -16
  21. package/dist/auth/account_schema.d.ts +20 -0
  22. package/dist/auth/account_schema.d.ts.map +1 -1
  23. package/dist/auth/account_schema.js +22 -1
  24. package/dist/auth/actor_search_actions.d.ts +3 -3
  25. package/dist/auth/actor_search_actions.d.ts.map +1 -1
  26. package/dist/auth/actor_search_actions.js +10 -6
  27. package/dist/auth/api_token_queries.d.ts.map +1 -1
  28. package/dist/auth/api_token_queries.js +11 -5
  29. package/dist/auth/app_settings_queries.d.ts.map +1 -1
  30. package/dist/auth/app_settings_queries.js +6 -4
  31. package/dist/auth/audit_log_queries.d.ts +6 -0
  32. package/dist/auth/audit_log_queries.d.ts.map +1 -1
  33. package/dist/auth/audit_log_queries.js +11 -5
  34. package/dist/auth/cell_actions.d.ts.map +1 -1
  35. package/dist/auth/cell_actions.js +3 -8
  36. package/dist/auth/cell_audit_actions.d.ts.map +1 -1
  37. package/dist/auth/cell_audit_actions.js +1 -1
  38. package/dist/auth/cell_field_actions.js +1 -1
  39. package/dist/auth/cell_grant_actions.js +1 -1
  40. package/dist/auth/cell_item_actions.js +1 -1
  41. package/dist/auth/invite_queries.d.ts.map +1 -1
  42. package/dist/auth/invite_queries.js +10 -8
  43. package/dist/auth/role_grant_offer_ddl.d.ts +11 -0
  44. package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
  45. package/dist/auth/role_grant_offer_ddl.js +20 -2
  46. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  47. package/dist/auth/role_grant_offer_queries.js +29 -15
  48. package/dist/auth/role_grant_queries.d.ts.map +1 -1
  49. package/dist/auth/role_grant_queries.js +21 -7
  50. package/dist/auth/session_queries.d.ts.map +1 -1
  51. package/dist/auth/session_queries.js +9 -4
  52. package/dist/db/CLAUDE.md +21 -2
  53. package/dist/db/cell_audit_queries.js +2 -2
  54. package/dist/db/cell_field_queries.d.ts +1 -1
  55. package/dist/db/cell_field_queries.d.ts.map +1 -1
  56. package/dist/db/cell_field_queries.js +8 -6
  57. package/dist/db/cell_grant_queries.d.ts +1 -1
  58. package/dist/db/cell_grant_queries.d.ts.map +1 -1
  59. package/dist/db/cell_grant_queries.js +12 -10
  60. package/dist/db/cell_item_queries.d.ts +1 -1
  61. package/dist/db/cell_item_queries.d.ts.map +1 -1
  62. package/dist/db/cell_item_queries.js +9 -7
  63. package/dist/db/cell_queries.d.ts +3 -3
  64. package/dist/db/cell_queries.d.ts.map +1 -1
  65. package/dist/db/cell_queries.js +6 -4
  66. package/dist/db/fact_queries.d.ts.map +1 -1
  67. package/dist/db/fact_queries.js +9 -3
  68. package/dist/db/sql_columns.d.ts +80 -3
  69. package/dist/db/sql_columns.d.ts.map +1 -1
  70. package/dist/db/sql_columns.js +91 -4
  71. package/dist/http/CLAUDE.md +3 -1
  72. package/dist/http/jsonrpc_helpers.d.ts +13 -0
  73. package/dist/http/jsonrpc_helpers.d.ts.map +1 -1
  74. package/dist/http/jsonrpc_helpers.js +13 -0
  75. package/dist/server/serve_fact_route.d.ts.map +1 -1
  76. package/dist/server/serve_fact_route.js +7 -2
  77. package/dist/testing/CLAUDE.md +53 -27
  78. package/dist/testing/admin_integration.d.ts +0 -3
  79. package/dist/testing/admin_integration.d.ts.map +1 -1
  80. package/dist/testing/admin_integration.js +0 -1
  81. package/dist/testing/adversarial_404.d.ts.map +1 -1
  82. package/dist/testing/adversarial_404.js +4 -1
  83. package/dist/testing/adversarial_input.d.ts.map +1 -1
  84. package/dist/testing/adversarial_input.js +5 -4
  85. package/dist/testing/app_server.d.ts +4 -3
  86. package/dist/testing/app_server.d.ts.map +1 -1
  87. package/dist/testing/app_server.js +11 -5
  88. package/dist/testing/attack_surface.d.ts +16 -0
  89. package/dist/testing/attack_surface.d.ts.map +1 -1
  90. package/dist/testing/attack_surface.js +36 -13
  91. package/dist/testing/audit_completeness.d.ts +0 -3
  92. package/dist/testing/audit_completeness.d.ts.map +1 -1
  93. package/dist/testing/audit_completeness.js +0 -1
  94. package/dist/testing/auth_apps.d.ts +9 -2
  95. package/dist/testing/auth_apps.d.ts.map +1 -1
  96. package/dist/testing/auth_apps.js +9 -2
  97. package/dist/testing/cross_backend/account_lifecycle.d.ts +5 -4
  98. package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
  99. package/dist/testing/cross_backend/account_lifecycle.js +7 -0
  100. package/dist/testing/cross_backend/action_manifest.d.ts +19 -3
  101. package/dist/testing/cross_backend/action_manifest.d.ts.map +1 -1
  102. package/dist/testing/cross_backend/action_manifest.js +10 -4
  103. package/dist/testing/cross_backend/action_manifest_parity.d.ts +9 -3
  104. package/dist/testing/cross_backend/action_manifest_parity.d.ts.map +1 -1
  105. package/dist/testing/cross_backend/action_manifest_parity.js +6 -0
  106. package/dist/testing/cross_backend/actor_lookup.d.ts +3 -2
  107. package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
  108. package/dist/testing/cross_backend/body_size.d.ts +4 -2
  109. package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
  110. package/dist/testing/cross_backend/body_size.js +1 -1
  111. package/dist/testing/cross_backend/cell_crud.d.ts +2 -2
  112. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  113. package/dist/testing/cross_backend/cell_crud.js +10 -0
  114. package/dist/testing/cross_backend/cell_gated_create.d.ts +3 -3
  115. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  116. package/dist/testing/cross_backend/cell_grant_role.d.ts +2 -2
  117. package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
  118. package/dist/testing/cross_backend/cell_relations.d.ts +2 -2
  119. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
  120. package/dist/testing/cross_backend/fact_serving.d.ts +2 -2
  121. package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
  122. package/dist/testing/cross_backend/make_cross_backend_project.d.ts +17 -2
  123. package/dist/testing/cross_backend/make_cross_backend_project.d.ts.map +1 -1
  124. package/dist/testing/cross_backend/make_cross_backend_project.js +23 -3
  125. package/dist/testing/cross_backend/origin.d.ts +4 -2
  126. package/dist/testing/cross_backend/origin.d.ts.map +1 -1
  127. package/dist/testing/cross_backend/origin.js +1 -1
  128. package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
  129. package/dist/testing/cross_backend/peer_ping_ws.js +11 -7
  130. package/dist/testing/cross_backend/query_shape.d.ts +6 -5
  131. package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
  132. package/dist/testing/cross_backend/ready.d.ts +2 -2
  133. package/dist/testing/cross_backend/ready.d.ts.map +1 -1
  134. package/dist/testing/cross_backend/role_grant_offer_enumeration.d.ts.map +1 -1
  135. package/dist/testing/cross_backend/role_grant_offer_enumeration.js +7 -1
  136. package/dist/testing/cross_backend/setup.d.ts +26 -6
  137. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  138. package/dist/testing/cross_backend/standard.d.ts +5 -1
  139. package/dist/testing/cross_backend/standard.d.ts.map +1 -1
  140. package/dist/testing/cross_backend/standard.js +1 -5
  141. package/dist/testing/cross_backend/testing_backdoor.js +1 -1
  142. package/dist/testing/cross_backend/testing_reset_actions.d.ts +5 -0
  143. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  144. package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -1
  145. package/dist/testing/cross_backend/token_lifetime.js +6 -0
  146. package/dist/testing/cross_backend/token_scope_surface.d.ts +2 -2
  147. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  148. package/dist/testing/cross_backend/wire_shapes.d.ts +22 -0
  149. package/dist/testing/cross_backend/wire_shapes.d.ts.map +1 -0
  150. package/dist/testing/cross_backend/wire_shapes.js +41 -0
  151. package/dist/testing/data_exposure.d.ts +0 -3
  152. package/dist/testing/data_exposure.d.ts.map +1 -1
  153. package/dist/testing/data_exposure.js +0 -1
  154. package/dist/testing/db.d.ts +44 -7
  155. package/dist/testing/db.d.ts.map +1 -1
  156. package/dist/testing/db.js +62 -27
  157. package/dist/testing/integration.d.ts.map +1 -1
  158. package/dist/testing/integration.js +15 -2
  159. package/dist/testing/round_trip.d.ts +0 -3
  160. package/dist/testing/round_trip.d.ts.map +1 -1
  161. package/dist/testing/round_trip.js +0 -4
  162. package/dist/testing/rpc_attack_surface.js +2 -2
  163. package/dist/testing/rpc_round_trip.d.ts +0 -3
  164. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  165. package/dist/testing/rpc_round_trip.js +0 -1
  166. package/dist/testing/schema_generators.d.ts +15 -0
  167. package/dist/testing/schema_generators.d.ts.map +1 -1
  168. package/dist/testing/schema_generators.js +45 -11
  169. package/dist/testing/standard.d.ts +5 -1
  170. package/dist/testing/standard.d.ts.map +1 -1
  171. package/dist/testing/standard.js +2 -7
  172. package/dist/timestamp.d.ts +58 -0
  173. package/dist/timestamp.d.ts.map +1 -0
  174. package/dist/timestamp.js +57 -0
  175. package/dist/ui/RoleGrantOfferHistory.svelte +3 -1
  176. package/dist/ui/RoleGrantOfferHistory.svelte.d.ts.map +1 -1
  177. package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -1
  178. package/dist/ui/role_grant_offers_state.svelte.js +6 -2
  179. package/package.json +1 -1
@@ -54,11 +54,6 @@ import { filter_visible_target_ids } from './cell_relation_visibility.js';
54
54
  import { to_grant_json } from './cell_grant_actions.js';
55
55
  import { to_field_json } from './cell_field_actions.js';
56
56
  import { to_item_json } from './cell_item_actions.js';
57
- const to_iso_nullable = (value) => {
58
- if (value === null)
59
- return null;
60
- return typeof value === 'string' ? value : value.toISOString();
61
- };
62
57
  /**
63
58
  * Translate the `idx_cell_path_unique` violation into a clean `conflict`
64
59
  * (409) reason. `path` is the only unique constraint a cell write can hit
@@ -92,9 +87,9 @@ export const to_cell_json = (row) => ({
92
87
  moderation: row.moderation,
93
88
  created_by: row.created_by,
94
89
  updated_by: row.updated_by,
95
- created_at: typeof row.created_at === 'string' ? row.created_at : row.created_at.toISOString(),
96
- updated_at: to_iso_nullable(row.updated_at),
97
- deleted_at: to_iso_nullable(row.deleted_at),
90
+ created_at: row.created_at,
91
+ updated_at: row.updated_at,
92
+ deleted_at: row.deleted_at,
98
93
  grant_count: row.grant_count
99
94
  });
100
95
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"cell_audit_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_audit_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAgC/F,eAAO,MAAM,yBAAyB,QAAO,KAAK,CAAC,SAAS,CA0B3D,CAAC"}
1
+ {"version":3,"file":"cell_audit_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_audit_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAuC,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AA+B/F,eAAO,MAAM,yBAAyB,QAAO,KAAK,CAAC,SAAS,CA0B3D,CAAC"}
@@ -32,7 +32,7 @@ const to_cell_audit_event_json = (row) => ({
32
32
  event_type: row.event_type,
33
33
  outcome: row.outcome,
34
34
  actor_id: row.actor_id,
35
- created_at: typeof row.created_at === 'string' ? row.created_at : row.created_at.toISOString()
35
+ created_at: row.created_at
36
36
  });
37
37
  export const create_cell_audit_actions = () => {
38
38
  const handler = async (input, ctx) => {
@@ -36,7 +36,7 @@ export const to_field_json = (row) => ({
36
36
  source_id: row.source_id,
37
37
  name: row.name,
38
38
  target_id: row.target_id,
39
- created_at: typeof row.created_at === 'string' ? row.created_at : row.created_at.toISOString()
39
+ created_at: row.created_at
40
40
  });
41
41
  /** Create the three `cell_field_*` RPC actions. */
42
42
  export const create_cell_field_actions = (deps) => {
@@ -44,7 +44,7 @@ export const to_grant_json = (row) => ({
44
44
  role: row.role,
45
45
  scope_id: row.scope_id,
46
46
  granted_by: row.granted_by,
47
- created_at: typeof row.created_at === 'string' ? row.created_at : row.created_at.toISOString()
47
+ created_at: row.created_at
48
48
  });
49
49
  /**
50
50
  * Build the audit-metadata principal envelope from a `cell_grant` row.
@@ -44,7 +44,7 @@ export const to_item_json = (row) => ({
44
44
  // on ingress).
45
45
  position: row.position,
46
46
  child_id: row.child_id,
47
- created_at: typeof row.created_at === 'string' ? row.created_at : row.created_at.toISOString()
47
+ created_at: row.created_at
48
48
  });
49
49
  const position_taken_error = () => jsonrpc_errors.invalid_params('cell_item position taken', {
50
50
  reason: ERROR_CELL_ITEM_POSITION_TAKEN
@@ -1 +1 @@
1
- {"version":3,"file":"invite_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/invite_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAE7F;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,8FAQqB,CAAC;AAEjD;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,OAAO,iBAAiB,KACtB,OAAO,CAAC,MAAM,CAQhB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,EACf,OAAO,MAAM,KACX,OAAO,CAAC,MAAM,GAAG,SAAS,CAK5B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,uCAAuC,GACnD,MAAM,SAAS,EACf,UAAU,MAAM,KACd,OAAO,CAAC,MAAM,GAAG,SAAS,CAK5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,4CAA4C,GACxD,MAAM,SAAS,EACf,OAAO,MAAM,GAAG,IAAI,EACpB,UAAU,MAAM,KACd,OAAO,CAAC,MAAM,GAAG,SAAS,CAgB5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,WAAW,MAAM,EACjB,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAQjB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAIlF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,KACb,OAAO,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAUxC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,IAAI,MAAM,KACR,OAAO,CAAC,OAAO,CAMjB,CAAC"}
1
+ {"version":3,"file":"invite_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/invite_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAGrD,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAE7F;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,8FAQqB,CAAC;AAKjD;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,OAAO,iBAAiB,KACtB,OAAO,CAAC,MAAM,CAQhB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,EACf,OAAO,MAAM,KACX,OAAO,CAAC,MAAM,GAAG,SAAS,CAK5B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,uCAAuC,GACnD,MAAM,SAAS,EACf,UAAU,MAAM,KACd,OAAO,CAAC,MAAM,GAAG,SAAS,CAK5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,4CAA4C,GACxD,MAAM,SAAS,EACf,OAAO,MAAM,GAAG,IAAI,EACpB,UAAU,MAAM,KACd,OAAO,CAAC,MAAM,GAAG,SAAS,CAgB5B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,2BAA2B,GACvC,MAAM,SAAS,EACf,WAAW,MAAM,EACjB,YAAY,MAAM,KAChB,OAAO,CAAC,OAAO,CAQjB,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,qBAAqB,GAAU,MAAM,SAAS,KAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAIlF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oCAAoC,GAChD,MAAM,SAAS,KACb,OAAO,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAUxC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,6BAA6B,GACzC,MAAM,SAAS,EACf,IAAI,MAAM,KACR,OAAO,CAAC,OAAO,CAMjB,CAAC"}
@@ -7,7 +7,7 @@
7
7
  * @module
8
8
  */
9
9
  import { assert_row } from '../db/assert_row.js';
10
- import { columns_sql, qualify_columns } from '../db/sql_columns.js';
10
+ import { columns_sql, iso8601_timestamp_expr, qualify_columns } from '../db/sql_columns.js';
11
11
  /**
12
12
  * The full `invite` column set, named explicitly so a row read fails loud on
13
13
  * schema drift — invite rows ride the `invite_create` / `invite_list` RPC
@@ -26,6 +26,8 @@ export const INVITE_COLUMNS = [
26
26
  'created_at',
27
27
  'created_by'
28
28
  ];
29
+ /** The `invite` timestamp override, by row qualifier (`''` bare, `i` for the username JOIN). */
30
+ const invite_expr = iso8601_timestamp_expr(INVITE_COLUMNS, ['claimed_at', 'created_at']);
29
31
  /**
30
32
  * Create a new invite.
31
33
  *
@@ -37,20 +39,20 @@ export const INVITE_COLUMNS = [
37
39
  export const query_create_invite = async (deps, input) => {
38
40
  const row = await deps.db.query_one(`INSERT INTO invite (email, username, created_by)
39
41
  VALUES ($1, $2, $3)
40
- RETURNING ${columns_sql(INVITE_COLUMNS)}`, [input.email ?? null, input.username ?? null, input.created_by]);
42
+ RETURNING ${columns_sql(INVITE_COLUMNS, invite_expr(''))}`, [input.email ?? null, input.username ?? null, input.created_by]);
41
43
  return assert_row(row, 'INSERT INTO invite');
42
44
  };
43
45
  /**
44
46
  * Find an unclaimed invite by email (case-insensitive).
45
47
  */
46
48
  export const query_invite_find_unclaimed_by_email = async (deps, email) => {
47
- return deps.db.query_one(`SELECT ${columns_sql(INVITE_COLUMNS)} FROM invite WHERE LOWER(email) = LOWER($1) AND claimed_at IS NULL`, [email]);
49
+ return deps.db.query_one(`SELECT ${columns_sql(INVITE_COLUMNS, invite_expr(''))} FROM invite WHERE LOWER(email) = LOWER($1) AND claimed_at IS NULL`, [email]);
48
50
  };
49
51
  /**
50
52
  * Find an unclaimed invite by username (case-insensitive).
51
53
  */
52
54
  export const query_invite_find_unclaimed_by_username = async (deps, username) => {
53
- return deps.db.query_one(`SELECT ${columns_sql(INVITE_COLUMNS)} FROM invite WHERE LOWER(username) = LOWER($1) AND claimed_at IS NULL`, [username]);
55
+ return deps.db.query_one(`SELECT ${columns_sql(INVITE_COLUMNS, invite_expr(''))} FROM invite WHERE LOWER(username) = LOWER($1) AND claimed_at IS NULL`, [username]);
54
56
  };
55
57
  /**
56
58
  * Find an unclaimed invite matching email and/or username, taking a
@@ -75,7 +77,7 @@ export const query_invite_find_unclaimed_by_username = async (deps, username) =>
75
77
  * @returns the matching invite (locked), or `undefined`
76
78
  */
77
79
  export const query_invite_find_unclaimed_match_for_update = async (deps, email, username) => {
78
- return deps.db.query_one(`SELECT ${columns_sql(INVITE_COLUMNS)} FROM invite WHERE claimed_at IS NULL AND (
80
+ return deps.db.query_one(`SELECT ${columns_sql(INVITE_COLUMNS, invite_expr(''))} FROM invite WHERE claimed_at IS NULL AND (
79
81
  (email IS NOT NULL AND username IS NULL
80
82
  AND $1::text IS NOT NULL AND LOWER(email) = LOWER($1::text))
81
83
  OR
@@ -85,7 +87,7 @@ export const query_invite_find_unclaimed_match_for_update = async (deps, email,
85
87
  (email IS NOT NULL AND username IS NOT NULL
86
88
  AND $1::text IS NOT NULL AND LOWER(email) = LOWER($1::text)
87
89
  AND LOWER(username) = LOWER($2))
88
- ) ORDER BY created_at ASC, id ASC LIMIT 1
90
+ ) ORDER BY invite.created_at ASC, invite.id ASC LIMIT 1
89
91
  FOR UPDATE`, [email, username]);
90
92
  };
91
93
  /**
@@ -118,7 +120,7 @@ export const query_invite_claim_unscoped = async (deps, invite_id, account_id) =
118
120
  * List all invites, newest first.
119
121
  */
120
122
  export const query_invite_list_all = async (deps) => {
121
- return deps.db.query(`SELECT ${columns_sql(INVITE_COLUMNS)} FROM invite ORDER BY created_at DESC`);
123
+ return deps.db.query(`SELECT ${columns_sql(INVITE_COLUMNS, invite_expr(''))} FROM invite ORDER BY invite.created_at DESC`);
122
124
  };
123
125
  /**
124
126
  * List all invites with resolved creator/claimer usernames, newest first.
@@ -127,7 +129,7 @@ export const query_invite_list_all = async (deps) => {
127
129
  * @returns invites with `created_by_username` and `claimed_by_username`
128
130
  */
129
131
  export const query_invite_list_all_with_usernames = async (deps) => {
130
- return deps.db.query(`SELECT ${qualify_columns(INVITE_COLUMNS, 'i')},
132
+ return deps.db.query(`SELECT ${qualify_columns(INVITE_COLUMNS, 'i', invite_expr('i'))},
131
133
  act.name AS created_by_username,
132
134
  a.username AS claimed_by_username
133
135
  FROM invite i
@@ -34,6 +34,17 @@ export declare const ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN = "GLOBAL";
34
34
  * `db/sql_columns.ts`.
35
35
  */
36
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;
37
48
  /**
38
49
  * Outer `SELECT` over an `updated` CTE of `role_grant_offer` rows, joining
39
50
  * the grantor's `account_id` on as `from_account_id` — the `SupersededOffer` /
@@ -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;;;;;;;GAOG;AACH,eAAO,MAAM,oCAAoC,QAEI,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;;;;;;;;;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"}
@@ -14,7 +14,7 @@
14
14
  *
15
15
  * @module
16
16
  */
17
- import { qualify_columns } from '../db/sql_columns.js';
17
+ import { iso8601_timestamp_expr, qualify_columns } from '../db/sql_columns.js';
18
18
  /** Sentinel UUID used inside the partial unique indexes to collapse `scope_id IS NULL` into a comparable value. */
19
19
  export const ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID = '00000000-0000-0000-0000-000000000000';
20
20
  /**
@@ -52,6 +52,24 @@ export const ROLE_GRANT_OFFER_COLUMNS = [
52
52
  'superseded_at',
53
53
  'resulting_role_grant_id'
54
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
+ ]);
55
73
  /**
56
74
  * Outer `SELECT` over an `updated` CTE of `role_grant_offer` rows, joining
57
75
  * the grantor's `account_id` on as `from_account_id` — the `SupersededOffer` /
@@ -60,7 +78,7 @@ export const ROLE_GRANT_OFFER_COLUMNS = [
60
78
  * `auth/role_grant_offer_queries.ts`). The CTE must be named `updated` and
61
79
  * `RETURNING` the full offer row.
62
80
  */
63
- export const ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT = `SELECT ${qualify_columns(ROLE_GRANT_OFFER_COLUMNS, 'u')}, grantor.account_id AS from_account_id
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
64
82
  FROM updated u
65
83
  JOIN actor grantor ON grantor.id = u.from_actor_id`;
66
84
  export const ROLE_GRANT_OFFER_SCHEMA = `
@@ -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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"}
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { assert_row } from '../db/assert_row.js';
15
15
  import { columns_sql, qualify_columns } from '../db/sql_columns.js';
16
- 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';
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
17
  import { query_create_role_grant, query_role_grant_by_id } from './role_grant_queries.js';
18
18
  import { query_audit_log } from './audit_log_queries.js';
19
19
  /**
@@ -154,7 +154,7 @@ export const query_role_grant_offer_create = async (deps, input) => {
154
154
  to_actor_id = EXCLUDED.to_actor_id,
155
155
  message = EXCLUDED.message,
156
156
  expires_at = EXCLUDED.expires_at
157
- RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)}`, [
157
+ RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS, role_grant_offer_expr(''))}`, [
158
158
  input.from_actor_id,
159
159
  input.to_account_id,
160
160
  input.to_actor_id ?? null,
@@ -193,6 +193,7 @@ export const query_role_grant_offer_decline = async (deps, offer_id, to_account_
193
193
  AND declined_at IS NULL
194
194
  AND retracted_at IS NULL
195
195
  AND superseded_at IS NULL
196
+ -- raw columns; the outer grantor select formats them
196
197
  RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)}
197
198
  )
198
199
  ${ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT}`, [offer_id, to_account_id, reason ?? null]);
@@ -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 ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)}`, [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 ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)} 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 ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)} 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 ${qualify_columns(ROLE_GRANT_OFFER_COLUMNS, '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 ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)} 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 ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)} 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,12 +347,23 @@ 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 ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)} 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.
@@ -377,8 +389,9 @@ export const query_accept_offer = async (deps, input) => {
377
389
  }
378
390
  // Expiry check AFTER the accepted-path: a validly-accepted offer past its
379
391
  // expires_at still returns the role_grant idempotently. Only pending offers
380
- // past expiry reach this branch.
381
- 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) {
382
395
  throw new RoleGrantOfferExpiredError(offer_id);
383
396
  }
384
397
  // Actor-targeted offer gate. When the offer is account-grain
@@ -424,7 +437,7 @@ export const query_accept_offer = async (deps, input) => {
424
437
  const offer_accepted = await deps.db.query_one(`UPDATE role_grant_offer
425
438
  SET accepted_at = NOW(), resulting_role_grant_id = $2
426
439
  WHERE id = $1
427
- RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)}`, [locked.id, role_grant.id]);
440
+ RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS, role_grant_offer_expr(''))}`, [locked.id, role_grant.id]);
428
441
  const offer = assert_row(offer_accepted, 'mark offer accepted');
429
442
  // Supersede sibling pending offers for the same (to_account, role, scope).
430
443
  // Forecloses the "accept this other sibling later to get the role back
@@ -442,6 +455,7 @@ export const query_accept_offer = async (deps, input) => {
442
455
  AND declined_at IS NULL
443
456
  AND retracted_at IS NULL
444
457
  AND superseded_at IS NULL
458
+ -- raw columns; the outer grantor select formats them
445
459
  RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)}
446
460
  )
447
461
  ${ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT}`, [to_account_id, offer.role, offer.scope_id, offer.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,7 @@
7
7
  * @module
8
8
  */
9
9
  import { assert_row } from '../db/assert_row.js';
10
- import { columns_sql, qualify_columns } from '../db/sql_columns.js';
10
+ import { columns_sql, iso8601_timestamp_expr, qualify_columns } from '../db/sql_columns.js';
11
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
12
  /**
13
13
  * The full `role_grant` column set, named explicitly so a row read fails loud
@@ -31,6 +31,17 @@ export const ROLE_GRANT_COLUMNS = [
31
31
  'granted_by',
32
32
  'source_offer_id'
33
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
+ ])('');
34
45
  /**
35
46
  * Grant a role_grant to an actor.
36
47
  * Idempotent — if an active role_grant already exists for this actor, role, and
@@ -62,7 +73,7 @@ export const query_create_role_grant = async (deps, input) => {
62
73
  )
63
74
  WHERE revoked_at IS NULL
64
75
  DO NOTHING
65
- RETURNING ${columns_sql(ROLE_GRANT_COLUMNS)}`, [
76
+ RETURNING ${columns_sql(ROLE_GRANT_COLUMNS, role_grant_expr)}`, [
66
77
  input.actor_id,
67
78
  input.role,
68
79
  input.scope_kind ?? null,
@@ -74,7 +85,7 @@ export const query_create_role_grant = async (deps, input) => {
74
85
  if (inserted)
75
86
  return inserted;
76
87
  // Active role_grant already exists — return it (idempotent grant).
77
- const existing = await deps.db.query_one(`SELECT ${columns_sql(ROLE_GRANT_COLUMNS)} FROM role_grant
88
+ const existing = await deps.db.query_one(`SELECT ${columns_sql(ROLE_GRANT_COLUMNS, role_grant_expr)} FROM role_grant
78
89
  WHERE actor_id = $1
79
90
  AND role = $2
80
91
  AND scope_kind IS NOT DISTINCT FROM $3
@@ -90,7 +101,7 @@ export const query_create_role_grant = async (deps, input) => {
90
101
  * (a row the offer's own IDOR guard has already vouched for). Authority
91
102
  * checks stay at the call site.
92
103
  */
93
- export const query_role_grant_by_id = async (deps, id) => deps.db.query_one(`SELECT ${columns_sql(ROLE_GRANT_COLUMNS)} FROM role_grant WHERE id = $1`, [id]);
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]);
94
105
  /**
95
106
  * Look up the role of an active role_grant (constrained to a specific
96
107
  * actor) plus the actor's `account_id`.
@@ -168,6 +179,7 @@ export const query_revoke_role_grant = async (deps, role_grant_id, actor_id, rev
168
179
  AND o.declined_at IS NULL
169
180
  AND o.retracted_at IS NULL
170
181
  AND o.superseded_at IS NULL
182
+ -- raw columns; the outer grantor select formats them
171
183
  RETURNING ${qualify_columns(ROLE_GRANT_OFFER_COLUMNS, 'o')}
172
184
  )
173
185
  ${ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT}`, [actor_id, revoked.role, revoked.scope_id]);
@@ -183,11 +195,11 @@ export const query_revoke_role_grant = async (deps, role_grant_id, actor_id, rev
183
195
  * Find all active (non-revoked, non-expired) role_grants for an actor.
184
196
  */
185
197
  export const query_role_grant_find_active_for_actor = async (deps, actor_id) => {
186
- return deps.db.query(`SELECT ${columns_sql(ROLE_GRANT_COLUMNS)} FROM role_grant
198
+ return deps.db.query(`SELECT ${columns_sql(ROLE_GRANT_COLUMNS, role_grant_expr)} FROM role_grant
187
199
  WHERE actor_id = $1
188
200
  AND revoked_at IS NULL
189
201
  AND (expires_at IS NULL OR expires_at > NOW())
190
- ORDER BY created_at`, [actor_id]);
202
+ ORDER BY role_grant.created_at`, [actor_id]);
191
203
  };
192
204
  /**
193
205
  * Check if an actor has an active role_grant for a given role.
@@ -314,7 +326,7 @@ export const query_count_active_accounts_with_global_role = async (deps, role) =
314
326
  * List all role_grants for an actor (including revoked/expired).
315
327
  */
316
328
  export const query_role_grant_list_for_actor = async (deps, actor_id) => {
317
- return deps.db.query(`SELECT ${columns_sql(ROLE_GRANT_COLUMNS)} 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]);
318
330
  };
319
331
  /**
320
332
  * Find the account ID of an account that holds an active role_grant for a given role.
@@ -396,6 +408,7 @@ export const query_role_grant_revoke_for_scope = async (deps, scope_id, revoked_
396
408
  AND o.declined_at IS NULL
397
409
  AND o.retracted_at IS NULL
398
410
  AND o.superseded_at IS NULL
411
+ -- raw columns; the outer grantor select formats them
399
412
  RETURNING ${qualify_columns(ROLE_GRANT_OFFER_COLUMNS, 'o')}
400
413
  )
401
414
  ${ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT}`, [scope_id]);
@@ -448,6 +461,7 @@ export const query_role_grant_revoke_role = async (deps, actor_id, role, revoked
448
461
  AND o.declined_at IS NULL
449
462
  AND o.retracted_at IS NULL
450
463
  AND o.superseded_at IS NULL
464
+ -- raw columns; the outer grantor select formats them
451
465
  RETURNING ${qualify_columns(ROLE_GRANT_OFFER_COLUMNS, 'o')}
452
466
  )
453
467
  ${ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT}`, [actor_id, role]);
@@ -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;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;AAEtD;;;;;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"}
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"}
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { hash_blake3 } from '@fuzdev/fuz_util/hash_blake3.ts';
10
10
  import { generate_random_base64url } from '../crypto.js';
11
- import { columns_sql, qualify_columns } from '../db/sql_columns.js';
11
+ import { columns_sql, iso8601_timestamp_expr, qualify_columns } from '../db/sql_columns.js';
12
12
  /**
13
13
  * Session lifetime in milliseconds (30 days).
14
14
  *
@@ -70,6 +70,11 @@ export const AUTH_SESSION_COLUMNS = [
70
70
  'created_at',
71
71
  'expires_at'
72
72
  ];
73
+ /** The `auth_session` timestamp override, by row qualifier (`''` bare, `s` for the admin listing's JOIN). */
74
+ const auth_session_expr = iso8601_timestamp_expr(AUTH_SESSION_COLUMNS, [
75
+ 'created_at',
76
+ 'expires_at'
77
+ ]);
73
78
  /**
74
79
  * Get a session if it exists, is not expired, and has not been revoked.
75
80
  *
@@ -77,7 +82,7 @@ export const AUTH_SESSION_COLUMNS = [
77
82
  * @param token_hash - blake3 hash of the session token
78
83
  */
79
84
  export const query_session_get_valid = async (deps, token_hash) => {
80
- return deps.db.query_one(`SELECT ${columns_sql(AUTH_SESSION_COLUMNS)} FROM auth_session WHERE id = $1 AND expires_at > NOW()`, [token_hash]);
85
+ return deps.db.query_one(`SELECT ${columns_sql(AUTH_SESSION_COLUMNS, auth_session_expr(''))} FROM auth_session WHERE id = $1 AND expires_at > NOW()`, [token_hash]);
81
86
  };
82
87
  /**
83
88
  * Revoke (delete) a session by its token hash, with no account scoping.
@@ -123,7 +128,7 @@ export const query_session_revoke_all_for_account = async (deps, account_id) =>
123
128
  * List sessions for an account, newest first.
124
129
  */
125
130
  export const query_session_list_for_account = async (deps, account_id, limit = 50) => {
126
- return deps.db.query(`SELECT ${columns_sql(AUTH_SESSION_COLUMNS)} FROM auth_session WHERE account_id = $1 ORDER BY created_at DESC LIMIT $2`, [account_id, limit]);
131
+ return deps.db.query(`SELECT ${columns_sql(AUTH_SESSION_COLUMNS, auth_session_expr(''))} FROM auth_session WHERE account_id = $1 ORDER BY auth_session.created_at DESC LIMIT $2`, [account_id, limit]);
127
132
  };
128
133
  /**
129
134
  * Enforce a per-account session limit by evicting the oldest sessions.
@@ -187,7 +192,7 @@ export const query_session_enforce_limit = async (deps, account_id, max_sessions
187
192
  * @returns active sessions joined with account usernames, newest first
188
193
  */
189
194
  export const query_session_list_all_active = async (deps, limit = 200) => {
190
- return deps.db.query(`SELECT ${qualify_columns(AUTH_SESSION_COLUMNS, 's')}, a.username
195
+ return deps.db.query(`SELECT ${qualify_columns(AUTH_SESSION_COLUMNS, 's', auth_session_expr('s'))}, a.username
191
196
  FROM auth_session s
192
197
  JOIN account a ON a.id = s.account_id
193
198
  WHERE s.expires_at > NOW()
package/dist/db/CLAUDE.md CHANGED
@@ -30,7 +30,26 @@ the code.
30
30
  - `sql_columns.ts` — `columns_sql` / `qualify_columns` / `omit_columns`,
31
31
  the derivation helpers every `*_COLUMNS` projection const's variants
32
32
  (alias-qualified JOIN reads, the client-safe `minus token_hash` listing)
33
- are built from, so the drift guard on the base const covers them.
33
+ are built from, so the drift guard on the base const covers them. Both
34
+ renderers take an optional `ColumnExpr` — a per-column expression
35
+ override, twin of the third argument to `fuz_db::qualify_columns` — and
36
+ alias an overridden column back to its own name (TS reads rows by name;
37
+ the Rust twin decodes positionally and needs no output name). The one
38
+ override in this repo is `iso8601_timestamp_column(alias, column)`, twin
39
+ of `fuz_db::iso8601_timestamp_column` down to the format literal
40
+ (`to_char(… AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`): every
41
+ timestamp that reaches the wire projects through it, so both spines
42
+ serialize the same instant to the same 20-character second-precision UTC
43
+ string. Each query module builds its override once with
44
+ `iso8601_timestamp_expr(TABLE_COLUMNS, [...timestamp columns])` — typed
45
+ against the const, checked at load — and passes `expr(alias)` at each
46
+ read. `timestamp.ts` (`to_iso8601_seconds` / `ISO8601_SECONDS`) is that
47
+ shape for stamps minted in TS rather than read from a row. A cascade's
48
+ inner `RETURNING` inside a `WITH updated AS (…)` stays raw — the outer
49
+ select formats, and `to_char` over already-formatted text would fail.
50
+ `fact`'s store rows are the deliberate exception: `PgFactStore` feeds
51
+ `FactMeta.created_at`, a `Date` in the `@fuzdev/fuz_util` `FactStore`
52
+ contract, and nothing serializes it.
34
53
  - `status.ts` — CLI DB status utility.
35
54
  - `schema_ready.ts` — `/ready` deploy-gate core: `query_public_columns`
36
55
  (keeps `schema_version`, unlike `query_schema_snapshot`), pure
@@ -108,7 +127,7 @@ child_id`, fractional-index keyed): `query_cell_item_insert` (throws
108
127
  (the meta read derives `minus bytes`). The derived `grant_count` stays an
109
128
  appended expression in the exported `cell_row_projection(alias)` rather than a
110
129
  `CELL_COLUMNS` entry, so the const is exactly the table's column set. Column
111
- order twins the Rust `fuz_cell` `*_COLUMNS` consts. Every const is pinned to
130
+ order twins the Rust `fuz_cell` / `fuz_fact` `*_COLUMNS` consts. Every const is pinned to
112
131
  the live schema — and every public table forced to carry a const or a named
113
132
  exemption — by `src/test/db/column_projections.db.test.ts`; the cell
114
133
  `*_queries.db.test.ts` files pin the aliased JOIN reads and `grant_count`.
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * @module
14
14
  */
15
- import { AUDIT_LOG_COLUMNS } from '../auth/audit_log_queries.js';
15
+ import { AUDIT_LOG_COLUMNS, audit_log_expr } from '../auth/audit_log_queries.js';
16
16
  import { columns_sql } from './sql_columns.js';
17
17
  /**
18
18
  * Metadata-jsonb keys cell-domain events use to name the cell. This
@@ -53,5 +53,5 @@ export const query_audit_log_list_by_cell = async (deps, cell_id, options) => {
53
53
  const where = `WHERE ${conditions.join(' AND ')}`;
54
54
  const limit_placeholder = `$${i++}`;
55
55
  params.push(options.limit);
56
- return deps.db.query(`SELECT ${columns_sql(AUDIT_LOG_COLUMNS)} FROM audit_log ${where} ORDER BY seq DESC LIMIT ${limit_placeholder}`, params);
56
+ return deps.db.query(`SELECT ${columns_sql(AUDIT_LOG_COLUMNS, audit_log_expr(''))} FROM audit_log ${where} ORDER BY seq DESC LIMIT ${limit_placeholder}`, params);
57
57
  };
@@ -23,7 +23,7 @@ export interface CellFieldRow {
23
23
  source_id: Uuid;
24
24
  name: string;
25
25
  target_id: Uuid;
26
- created_at: Date;
26
+ created_at: string;
27
27
  }
28
28
  /**
29
29
  * The full `cell_field` column set, named explicitly so a row read fails