@fuzdev/fuz_app 0.110.1 → 0.111.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 (81) hide show
  1. package/dist/actions/action_rpc.d.ts +2 -2
  2. package/dist/auth/CLAUDE.md +24 -5
  3. package/dist/auth/account_action_specs.d.ts +0 -2
  4. package/dist/auth/account_action_specs.d.ts.map +1 -1
  5. package/dist/auth/account_queries.d.ts +22 -0
  6. package/dist/auth/account_queries.d.ts.map +1 -1
  7. package/dist/auth/account_queries.js +49 -15
  8. package/dist/auth/account_schema.d.ts +7 -9
  9. package/dist/auth/account_schema.d.ts.map +1 -1
  10. package/dist/auth/account_schema.js +1 -2
  11. package/dist/auth/admin_action_specs.d.ts +0 -2
  12. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  13. package/dist/auth/api_token_queries.d.ts +9 -4
  14. package/dist/auth/api_token_queries.d.ts.map +1 -1
  15. package/dist/auth/api_token_queries.js +29 -7
  16. package/dist/auth/app_settings_queries.d.ts +8 -0
  17. package/dist/auth/app_settings_queries.d.ts.map +1 -1
  18. package/dist/auth/app_settings_queries.js +14 -3
  19. package/dist/auth/audit_log_queries.d.ts +15 -3
  20. package/dist/auth/audit_log_queries.d.ts.map +1 -1
  21. package/dist/auth/audit_log_queries.js +32 -7
  22. package/dist/auth/audit_log_schema.d.ts +0 -1
  23. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  24. package/dist/auth/auth_ddl.d.ts +6 -0
  25. package/dist/auth/auth_ddl.d.ts.map +1 -1
  26. package/dist/auth/auth_ddl.js +9 -0
  27. package/dist/auth/invite_queries.d.ts +10 -0
  28. package/dist/auth/invite_queries.d.ts.map +1 -1
  29. package/dist/auth/invite_queries.js +25 -6
  30. package/dist/auth/migrations.d.ts.map +1 -1
  31. package/dist/auth/migrations.js +10 -0
  32. package/dist/auth/role_grant_offer_ddl.d.ts +23 -2
  33. package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
  34. package/dist/auth/role_grant_offer_ddl.js +43 -2
  35. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  36. package/dist/auth/role_grant_offer_queries.js +29 -48
  37. package/dist/auth/role_grant_queries.d.ts +18 -0
  38. package/dist/auth/role_grant_queries.d.ts.map +1 -1
  39. package/dist/auth/role_grant_queries.js +43 -17
  40. package/dist/auth/session_queries.d.ts +12 -3
  41. package/dist/auth/session_queries.d.ts.map +1 -1
  42. package/dist/auth/session_queries.js +21 -6
  43. package/dist/db/CLAUDE.md +15 -0
  44. package/dist/db/cell_audit_queries.d.ts.map +1 -1
  45. package/dist/db/cell_audit_queries.js +3 -1
  46. package/dist/db/cell_field_queries.d.ts +11 -2
  47. package/dist/db/cell_field_queries.d.ts.map +1 -1
  48. package/dist/db/cell_field_queries.js +23 -7
  49. package/dist/db/cell_grant_queries.d.ts +9 -1
  50. package/dist/db/cell_grant_queries.d.ts.map +1 -1
  51. package/dist/db/cell_grant_queries.js +25 -9
  52. package/dist/db/cell_item_queries.d.ts +12 -1
  53. package/dist/db/cell_item_queries.d.ts.map +1 -1
  54. package/dist/db/cell_item_queries.js +25 -7
  55. package/dist/db/cell_queries.d.ts +29 -0
  56. package/dist/db/cell_queries.d.ts.map +1 -1
  57. package/dist/db/cell_queries.js +53 -20
  58. package/dist/db/fact_queries.d.ts +7 -0
  59. package/dist/db/fact_queries.d.ts.map +1 -1
  60. package/dist/db/fact_queries.js +19 -2
  61. package/dist/db/schema_ready.d.ts +6 -1
  62. package/dist/db/schema_ready.d.ts.map +1 -1
  63. package/dist/db/schema_ready.js +6 -2
  64. package/dist/db/sql_columns.d.ts +67 -0
  65. package/dist/db/sql_columns.d.ts.map +1 -0
  66. package/dist/db/sql_columns.js +77 -0
  67. package/dist/db/sql_identifier.d.ts +6 -5
  68. package/dist/db/sql_identifier.d.ts.map +1 -1
  69. package/dist/db/sql_identifier.js +6 -5
  70. package/dist/http/CLAUDE.md +1 -1
  71. package/dist/http/route_spec.d.ts +2 -2
  72. package/dist/server/app_server.d.ts.map +1 -1
  73. package/dist/server/app_server.js +1 -2
  74. package/dist/testing/CLAUDE.md +2 -1
  75. package/dist/testing/cross_backend/expected_schema.json +1 -1
  76. package/dist/testing/db.d.ts +18 -0
  77. package/dist/testing/db.d.ts.map +1 -1
  78. package/dist/testing/db.js +24 -1
  79. package/dist/ui/AdminOverview.svelte +5 -10
  80. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  81. package/package.json +1 -1
@@ -12,7 +12,31 @@
12
12
  * @module
13
13
  */
14
14
  import { assert_row } from '../db/assert_row.js';
15
+ import { columns_sql, qualify_columns } from '../db/sql_columns.js';
15
16
  import { AUDIT_LOG_DEFAULT_LIMIT, builtin_audit_log_config } from './audit_log_schema.js';
17
+ /**
18
+ * The full `audit_log` column set, named explicitly so a row read fails loud
19
+ * on schema drift — audit rows ride the `audit_log_list` / role-grant-history
20
+ * RPC responses and the SSE broadcast raw, so a `SELECT *` would silently
21
+ * carry a dropped or leftover column into the strict-validated wire shapes
22
+ * (see `ACCOUNT_COLUMNS` in `auth/account_queries.ts` for the outage class;
23
+ * the Rust twin names columns at every audit site). Keep in sync with
24
+ * `AuditLogEvent` and the migration chain's end state. Exported for the
25
+ * per-cell timeline read in `db/cell_audit_queries.ts`.
26
+ */
27
+ export const AUDIT_LOG_COLUMNS = [
28
+ 'id',
29
+ 'seq',
30
+ 'event_type',
31
+ 'outcome',
32
+ 'actor_id',
33
+ 'account_id',
34
+ 'target_account_id',
35
+ 'target_actor_id',
36
+ 'ip',
37
+ 'created_at',
38
+ 'metadata'
39
+ ];
16
40
  /**
17
41
  * Process-wide counter for audit metadata validation failures. `query_audit_log`
18
42
  * increments on `safeParse` mismatch and writes the row anyway (fail-open —
@@ -47,9 +71,10 @@ export const reset_audit_unknown_event_type_failures = () => {
47
71
  /**
48
72
  * Insert an audit log entry.
49
73
  *
50
- * `RETURNING *` so callers receive DB-assigned fields (`id`, `seq`,
51
- * `created_at`). Validates `metadata` against `config.metadata_schemas`;
52
- * unknown `event_type` and metadata mismatches log + bump their counters
74
+ * Returns the full row (`AUDIT_LOG_COLUMNS`) so callers receive DB-assigned
75
+ * fields (`id`, `seq`, `created_at`). Validates `metadata` against
76
+ * `config.metadata_schemas`; unknown `event_type` and metadata mismatches
77
+ * log + bump their counters
53
78
  * but write the row anyway. Consumers extend the recognized set via
54
79
  * `create_audit_log_config({extra_events})`.
55
80
  *
@@ -83,7 +108,7 @@ export const query_audit_log = async (deps, input, config = builtin_audit_log_co
83
108
  }
84
109
  const rows = await deps.db.query(`INSERT INTO audit_log (event_type, outcome, actor_id, account_id, target_account_id, target_actor_id, ip, metadata)
85
110
  VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
86
- RETURNING *`, [
111
+ RETURNING ${columns_sql(AUDIT_LOG_COLUMNS)}`, [
87
112
  input.event_type,
88
113
  input.outcome ?? 'success',
89
114
  input.actor_id ?? null,
@@ -131,7 +156,7 @@ export const query_audit_log_list = async (deps, options) => {
131
156
  const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
132
157
  const limit = options?.limit ?? AUDIT_LOG_DEFAULT_LIMIT;
133
158
  const offset = options?.offset ?? 0;
134
- return deps.db.query(`SELECT * FROM audit_log ${where} ORDER BY seq DESC LIMIT $${param_index++} OFFSET $${param_index}`, [...params, limit, offset]);
159
+ return deps.db.query(`SELECT ${columns_sql(AUDIT_LOG_COLUMNS)} FROM audit_log ${where} ORDER BY seq DESC LIMIT $${param_index++} OFFSET $${param_index}`, [...params, limit, offset]);
135
160
  };
136
161
  /**
137
162
  * List audit log entries with resolved usernames, newest first.
@@ -175,7 +200,7 @@ export const query_audit_log_list_with_usernames = async (deps, options) => {
175
200
  // has no actor binding (admin password reset, session revoke, etc.).
176
201
  // Under v1 1:1 the two branches resolve to the same username; the
177
202
  // chain is forensic future-proofing for N:1 multi-actor.
178
- return deps.db.query(`SELECT al.*,
203
+ return deps.db.query(`SELECT ${qualify_columns(AUDIT_LOG_COLUMNS, 'al')},
179
204
  COALESCE(origin_act_acc.username, origin_acc.username) AS username,
180
205
  COALESCE(target_act_acc.username, target_acc.username) AS target_username
181
206
  FROM audit_log al
@@ -199,7 +224,7 @@ export const query_audit_log_list_role_grant_history = async (deps, limit = AUDI
199
224
  // Same actor-chained JOIN as `query_audit_log_list_with_usernames` —
200
225
  // see the comment there for rationale (forensic future-proofing for
201
226
  // N:1 multi-actor; v1 1:1 picks the same username via either branch).
202
- return deps.db.query(`SELECT al.*,
227
+ return deps.db.query(`SELECT ${qualify_columns(AUDIT_LOG_COLUMNS, 'al')},
203
228
  COALESCE(origin_act_acc.username, origin_acc.username) AS username,
204
229
  COALESCE(target_act_acc.username, target_acc.username) AS target_username
205
230
  FROM audit_log al
@@ -475,7 +475,6 @@ export declare const AdminSessionJson: z.ZodObject<{
475
475
  account_id: z.core.$ZodBranded<z.ZodUUID, "Uuid", "out">;
476
476
  created_at: z.ZodString;
477
477
  expires_at: z.ZodString;
478
- last_seen_at: z.ZodString;
479
478
  username: z.ZodString;
480
479
  }, z.core.$strict>;
481
480
  export type AdminSessionJson = z.infer<typeof AdminSessionJson>;
@@ -1 +1 @@
1
- {"version":3,"file":"audit_log_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"audit_log_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/audit_log_schema.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,12 @@
7
7
  * (audit table) and `auth/role_grant_offer_ddl.ts` (offer table) — DDL lives
8
8
  * in `*_ddl.ts`, Zod schemas in `*_schema.ts`.
9
9
  *
10
+ * These strings are the body of the frozen `full_auth_schema` migration —
11
+ * never edit them (see the `auth/migrations.ts` module doc for the
12
+ * silent-outage rationale). Schema changes ship as new appended migrations,
13
+ * so a column retired later still appears below (created by v0, dropped
14
+ * downstream in the chain).
15
+ *
10
16
  * @module
11
17
  */
12
18
  export declare const ACCOUNT_SCHEMA = "\nCREATE TABLE IF NOT EXISTS account (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n username TEXT UNIQUE NOT NULL,\n email TEXT,\n email_verified BOOLEAN NOT NULL DEFAULT false,\n password_hash TEXT NOT NULL,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n created_by UUID,\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_by UUID,\n deleted_at TIMESTAMPTZ,\n deleted_by UUID\n)";
@@ -1 +1 @@
1
- {"version":3,"file":"auth_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/auth_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,eAAO,MAAM,cAAc,6ZAazB,CAAC;AAEH,eAAO,MAAM,YAAY,0ZAUvB,CAAC;AAEH,eAAO,MAAM,WAAW,wEAC0C,CAAC;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,8FACqD,CAAC;AAEzF,eAAO,MAAM,iBAAiB,2ZAU5B,CAAC;AAEH,eAAO,MAAM,kBAAkB,UAI9B,CAAC;AAEF,eAAO,MAAM,mBAAmB,0RAO9B,CAAC;AAEH,eAAO,MAAM,oBAAoB,UAGhC,CAAC;AAEF,eAAO,MAAM,gBAAgB,iUAU3B,CAAC;AAEH,eAAO,MAAM,mBAAmB,4GACsE,CAAC;AAEvG,eAAO,MAAM,yBAAyB,6FACiD,CAAC;AAExF,eAAO,MAAM,eAAe,gFAC8C,CAAC;AAE3E,eAAO,MAAM,qBAAqB,wJAIhC,CAAC;AAEH,6FAA6F;AAC7F,eAAO,MAAM,mBAAmB,yHAGP,CAAC;AAE1B,eAAO,MAAM,aAAa,6ZAUxB,CAAC;AAEH,eAAO,MAAM,cAAc,UAI1B,CAAC;AAEF,eAAO,MAAM,mBAAmB,oMAM9B,CAAC;AAEH,eAAO,MAAM,iBAAiB,sEACkC,CAAC"}
1
+ {"version":3,"file":"auth_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/auth_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,eAAO,MAAM,cAAc,6ZAazB,CAAC;AAEH,eAAO,MAAM,YAAY,0ZAUvB,CAAC;AAEH,eAAO,MAAM,WAAW,wEAC0C,CAAC;AAEnE;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,8FACqD,CAAC;AAEzF,eAAO,MAAM,iBAAiB,2ZAU5B,CAAC;AAEH,eAAO,MAAM,kBAAkB,UAI9B,CAAC;AAKF,eAAO,MAAM,mBAAmB,0RAO9B,CAAC;AAEH,eAAO,MAAM,oBAAoB,UAGhC,CAAC;AAEF,eAAO,MAAM,gBAAgB,iUAU3B,CAAC;AAEH,eAAO,MAAM,mBAAmB,4GACsE,CAAC;AAEvG,eAAO,MAAM,yBAAyB,6FACiD,CAAC;AAExF,eAAO,MAAM,eAAe,gFAC8C,CAAC;AAE3E,eAAO,MAAM,qBAAqB,wJAIhC,CAAC;AAEH,6FAA6F;AAC7F,eAAO,MAAM,mBAAmB,yHAGP,CAAC;AAE1B,eAAO,MAAM,aAAa,6ZAUxB,CAAC;AAEH,eAAO,MAAM,cAAc,UAI1B,CAAC;AAEF,eAAO,MAAM,mBAAmB,oMAM9B,CAAC;AAEH,eAAO,MAAM,iBAAiB,sEACkC,CAAC"}
@@ -7,6 +7,12 @@
7
7
  * (audit table) and `auth/role_grant_offer_ddl.ts` (offer table) — DDL lives
8
8
  * in `*_ddl.ts`, Zod schemas in `*_schema.ts`.
9
9
  *
10
+ * These strings are the body of the frozen `full_auth_schema` migration —
11
+ * never edit them (see the `auth/migrations.ts` module doc for the
12
+ * silent-outage rationale). Schema changes ship as new appended migrations,
13
+ * so a column retired later still appears below (created by v0, dropped
14
+ * downstream in the chain).
15
+ *
10
16
  * @module
11
17
  */
12
18
  // `deleted_at` is the soft-delete tombstone (delete = soft, purge = hard).
@@ -64,6 +70,9 @@ export const ROLE_GRANT_INDEXES = [
64
70
  `CREATE UNIQUE INDEX IF NOT EXISTS role_grant_actor_role_active_unique
65
71
  ON role_grant (actor_id, role) WHERE revoked_at IS NULL`
66
72
  ];
73
+ // `last_seen_at` stays in this frozen v0 body — the appended
74
+ // `drop_session_last_seen_at` migration removes it later in the chain, so a
75
+ // fresh bootstrap and an old deployed DB converge on the same shape.
67
76
  export const AUTH_SESSION_SCHEMA = `
68
77
  CREATE TABLE IF NOT EXISTS auth_session (
69
78
  id TEXT PRIMARY KEY,
@@ -8,6 +8,16 @@
8
8
  */
9
9
  import type { QueryDeps } from '../db/query_deps.js';
10
10
  import type { Invite, CreateInviteInput, InviteWithUsernamesJson } from './invite_schema.js';
11
+ /**
12
+ * The full `invite` column set, named explicitly so a row read fails loud on
13
+ * schema drift — invite rows ride the `invite_create` / `invite_list` RPC
14
+ * responses raw, so a `SELECT *` would silently carry a dropped or leftover
15
+ * column into the strict-validated wire shapes (see `ACCOUNT_COLUMNS` in
16
+ * `auth/account_queries.ts` for the outage class; the Rust twin names
17
+ * columns at every invite site). Keep in sync with `Invite` and the
18
+ * migration chain's end state.
19
+ */
20
+ export declare const INVITE_COLUMNS: readonly ["id", "email", "username", "claimed_by", "claimed_at", "created_at", "created_by"];
11
21
  /**
12
22
  * Create a new invite.
13
23
  *
@@ -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;AAErD,OAAO,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAE7F;;;;;;;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,CAElF,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;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"}
@@ -7,6 +7,25 @@
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';
11
+ /**
12
+ * The full `invite` column set, named explicitly so a row read fails loud on
13
+ * schema drift — invite rows ride the `invite_create` / `invite_list` RPC
14
+ * responses raw, so a `SELECT *` would silently carry a dropped or leftover
15
+ * column into the strict-validated wire shapes (see `ACCOUNT_COLUMNS` in
16
+ * `auth/account_queries.ts` for the outage class; the Rust twin names
17
+ * columns at every invite site). Keep in sync with `Invite` and the
18
+ * migration chain's end state.
19
+ */
20
+ export const INVITE_COLUMNS = [
21
+ 'id',
22
+ 'email',
23
+ 'username',
24
+ 'claimed_by',
25
+ 'claimed_at',
26
+ 'created_at',
27
+ 'created_by'
28
+ ];
10
29
  /**
11
30
  * Create a new invite.
12
31
  *
@@ -18,20 +37,20 @@ import { assert_row } from '../db/assert_row.js';
18
37
  export const query_create_invite = async (deps, input) => {
19
38
  const row = await deps.db.query_one(`INSERT INTO invite (email, username, created_by)
20
39
  VALUES ($1, $2, $3)
21
- RETURNING *`, [input.email ?? null, input.username ?? null, input.created_by]);
40
+ RETURNING ${columns_sql(INVITE_COLUMNS)}`, [input.email ?? null, input.username ?? null, input.created_by]);
22
41
  return assert_row(row, 'INSERT INTO invite');
23
42
  };
24
43
  /**
25
44
  * Find an unclaimed invite by email (case-insensitive).
26
45
  */
27
46
  export const query_invite_find_unclaimed_by_email = async (deps, email) => {
28
- return deps.db.query_one(`SELECT * FROM invite WHERE LOWER(email) = LOWER($1) AND claimed_at IS NULL`, [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]);
29
48
  };
30
49
  /**
31
50
  * Find an unclaimed invite by username (case-insensitive).
32
51
  */
33
52
  export const query_invite_find_unclaimed_by_username = async (deps, username) => {
34
- return deps.db.query_one(`SELECT * FROM invite WHERE LOWER(username) = LOWER($1) AND claimed_at IS NULL`, [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]);
35
54
  };
36
55
  /**
37
56
  * Find an unclaimed invite matching email and/or username, taking a
@@ -56,7 +75,7 @@ export const query_invite_find_unclaimed_by_username = async (deps, username) =>
56
75
  * @returns the matching invite (locked), or `undefined`
57
76
  */
58
77
  export const query_invite_find_unclaimed_match_for_update = async (deps, email, username) => {
59
- return deps.db.query_one(`SELECT * FROM invite WHERE claimed_at IS NULL AND (
78
+ return deps.db.query_one(`SELECT ${columns_sql(INVITE_COLUMNS)} FROM invite WHERE claimed_at IS NULL AND (
60
79
  (email IS NOT NULL AND username IS NULL
61
80
  AND $1::text IS NOT NULL AND LOWER(email) = LOWER($1::text))
62
81
  OR
@@ -99,7 +118,7 @@ export const query_invite_claim_unscoped = async (deps, invite_id, account_id) =
99
118
  * List all invites, newest first.
100
119
  */
101
120
  export const query_invite_list_all = async (deps) => {
102
- return deps.db.query(`SELECT * FROM invite ORDER BY created_at DESC`);
121
+ return deps.db.query(`SELECT ${columns_sql(INVITE_COLUMNS)} FROM invite ORDER BY created_at DESC`);
103
122
  };
104
123
  /**
105
124
  * List all invites with resolved creator/claimer usernames, newest first.
@@ -108,7 +127,7 @@ export const query_invite_list_all = async (deps) => {
108
127
  * @returns invites with `created_by_username` and `claimed_by_username`
109
128
  */
110
129
  export const query_invite_list_all_with_usernames = async (deps) => {
111
- return deps.db.query(`SELECT i.*,
130
+ return deps.db.query(`SELECT ${qualify_columns(INVITE_COLUMNS, 'i')},
112
131
  act.name AS created_by_username,
113
132
  a.username AS claimed_by_username
114
133
  FROM invite i
@@ -1 +1 @@
1
- {"version":3,"file":"migrations.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AA+BH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,wDAAwD;AACxD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,aAAa,CAAC,MAAM,CAA8B,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CAmK5C,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AA+BH,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAEtE,wDAAwD;AACxD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,EAAE,aAAa,CAAC,MAAM,CAA8B,CAAC;AAE/F;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CA6K5C,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
@@ -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,26 @@ 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
+ * Outer `SELECT` over an `updated` CTE of `role_grant_offer` rows, joining
39
+ * the grantor's `account_id` on as `from_account_id` — the `SupersededOffer` /
40
+ * `DeclinedOffer` shape every offer-superseding cascade returns (the revoke
41
+ * cascades in `auth/role_grant_queries.ts`, decline + accept-supersede in
42
+ * `auth/role_grant_offer_queries.ts`). The CTE must be named `updated` and
43
+ * `RETURNING` the full offer row.
44
+ */
45
+ export declare const ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT: string;
25
46
  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
47
  /**
27
48
  * 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;;;;;;;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,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 { 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,45 @@ 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
+ * Outer `SELECT` over an `updated` CTE of `role_grant_offer` rows, joining
57
+ * the grantor's `account_id` on as `from_account_id` — the `SupersededOffer` /
58
+ * `DeclinedOffer` shape every offer-superseding cascade returns (the revoke
59
+ * cascades in `auth/role_grant_queries.ts`, decline + accept-supersede in
60
+ * `auth/role_grant_offer_queries.ts`). The CTE must be named `updated` and
61
+ * `RETURNING` the full offer row.
62
+ */
63
+ export const ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT = `SELECT ${qualify_columns(ROLE_GRANT_OFFER_COLUMNS, 'u')}, grantor.account_id AS from_account_id
64
+ FROM updated u
65
+ JOIN actor grantor ON grantor.id = u.from_actor_id`;
25
66
  export const ROLE_GRANT_OFFER_SCHEMA = `
26
67
  CREATE TABLE IF NOT EXISTS role_grant_offer (
27
68
  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_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)}`, [
156
158
  input.from_actor_id,
157
159
  input.to_account_id,
158
160
  input.to_actor_id ?? null,
@@ -191,11 +193,9 @@ 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
+ RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)}
195
197
  )
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]);
198
+ ${ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT}`, [offer_id, to_account_id, reason ?? null]);
199
199
  if (updated)
200
200
  return updated;
201
201
  return resolve_terminal_or_missing(deps, offer_id, { to_account_id });
@@ -220,7 +220,7 @@ export const query_role_grant_offer_retract = async (deps, offer_id, from_actor_
220
220
  AND declined_at IS NULL
221
221
  AND retracted_at IS NULL
222
222
  AND superseded_at IS NULL
223
- RETURNING *`, [offer_id, from_actor_id]);
223
+ RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)}`, [offer_id, from_actor_id]);
224
224
  if (updated)
225
225
  return updated;
226
226
  return resolve_terminal_or_missing(deps, offer_id, { from_actor_id });
@@ -238,7 +238,7 @@ const resolve_terminal_or_missing = async (deps, offer_id, scope) => {
238
238
  conditions.push(`from_actor_id = $${idx++}`);
239
239
  params.push(scope.from_actor_id);
240
240
  }
241
- const row = await deps.db.query_one(`SELECT * FROM role_grant_offer WHERE ${conditions.join(' AND ')}`, params);
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
242
  if (!row)
243
243
  return null;
244
244
  if (row.accepted_at || row.declined_at || row.retracted_at || row.superseded_at) {
@@ -254,7 +254,7 @@ const resolve_terminal_or_missing = async (deps, offer_id, scope) => {
254
254
  * sweep (`query_role_grant_offer_sweep_expired`) handles audit tombstoning.
255
255
  */
256
256
  export const query_role_grant_offer_list = async (deps, to_account_id) => {
257
- return deps.db.query(`SELECT * FROM role_grant_offer
257
+ return deps.db.query(`SELECT ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)} FROM role_grant_offer
258
258
  WHERE to_account_id = $1
259
259
  AND accepted_at IS NULL
260
260
  AND declined_at IS NULL
@@ -269,7 +269,7 @@ export const query_role_grant_offer_list = async (deps, to_account_id) => {
269
269
  * Includes terminal offers — used by the grantor-side admin / history view.
270
270
  */
271
271
  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
272
+ return deps.db.query(`SELECT ${qualify_columns(ROLE_GRANT_OFFER_COLUMNS, 'o')} FROM role_grant_offer o
273
273
  LEFT JOIN actor a ON a.id = o.from_actor_id
274
274
  WHERE o.to_account_id = $1 OR a.account_id = $1
275
275
  ORDER BY o.created_at DESC
@@ -280,7 +280,7 @@ export const query_role_grant_offer_history_for_account = async (deps, account_i
280
280
  * expired (server-side filter), or missing.
281
281
  */
282
282
  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
283
+ const row = await deps.db.query_one(`SELECT ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)} FROM role_grant_offer
284
284
  WHERE id = $1
285
285
  AND accepted_at IS NULL
286
286
  AND declined_at IS NULL
@@ -298,7 +298,7 @@ export const query_role_grant_offer_find_pending = async (deps, offer_id) => {
298
298
  * already exists for the offer id).
299
299
  */
300
300
  export const query_role_grant_offer_sweep_expired = async (deps) => {
301
- return deps.db.query(`SELECT * FROM role_grant_offer
301
+ return deps.db.query(`SELECT ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)} FROM role_grant_offer
302
302
  WHERE accepted_at IS NULL
303
303
  AND declined_at IS NULL
304
304
  AND retracted_at IS NULL
@@ -346,7 +346,7 @@ export const query_accept_offer = async (deps, input) => {
346
346
  // We defer writing `accepted_at` until the role_grant row exists — the
347
347
  // `role_grant_offer_role_grant_iff_accepted` CHECK constraint demands both be set
348
348
  // (or neither) at row-visibility time.
349
- const locked = await deps.db.query_one(`SELECT * FROM role_grant_offer
349
+ const locked = await deps.db.query_one(`SELECT ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)} FROM role_grant_offer
350
350
  WHERE id = $1 AND to_account_id = $2
351
351
  FOR UPDATE`, [offer_id, to_account_id]);
352
352
  if (!locked) {
@@ -355,9 +355,7 @@ export const query_accept_offer = async (deps, input) => {
355
355
  if (locked.accepted_at) {
356
356
  // Race winner already committed; return the pre-existing role_grant.
357
357
  // `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');
358
+ const role_grant = assert_row(await query_role_grant_by_id(deps, locked.resulting_role_grant_id), 'resulting_role_grant lookup');
361
359
  // Multi-actor guard: two actors on the same recipient account may
362
360
  // both race an account-grain offer — the loser must not silently
363
361
  // receive the winner's role_grant (which would tell them "you got it"
@@ -411,37 +409,22 @@ export const query_accept_offer = async (deps, input) => {
411
409
  if (!actor_check) {
412
410
  throw new Error(`Accepting actor ${actor_id} does not belong to account ${to_account_id} (offer ${offer_id})`);
413
411
  }
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
- }
412
+ // Insert the role_grant through the shared idempotent grant — if another
413
+ // code path already granted the same (actor, role, scope_kind, scope), it
414
+ // returns that row. Stamps the grantor and the source offer on the row.
415
+ const role_grant = await query_create_role_grant(deps, {
416
+ actor_id,
417
+ role: locked.role,
418
+ scope_kind: locked.scope_kind,
419
+ scope_id: locked.scope_id,
420
+ granted_by: locked.from_actor_id,
421
+ source_offer_id: locked.id
422
+ });
440
423
  // Single UPDATE sets both sides of the CHECK constraint at once.
441
424
  const offer_accepted = await deps.db.query_one(`UPDATE role_grant_offer
442
425
  SET accepted_at = NOW(), resulting_role_grant_id = $2
443
426
  WHERE id = $1
444
- RETURNING *`, [locked.id, role_grant.id]);
427
+ RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)}`, [locked.id, role_grant.id]);
445
428
  const offer = assert_row(offer_accepted, 'mark offer accepted');
446
429
  // Supersede sibling pending offers for the same (to_account, role, scope).
447
430
  // Forecloses the "accept this other sibling later to get the role back
@@ -459,13 +442,11 @@ export const query_accept_offer = async (deps, input) => {
459
442
  AND declined_at IS NULL
460
443
  AND retracted_at IS NULL
461
444
  AND superseded_at IS NULL
462
- RETURNING *
445
+ RETURNING ${columns_sql(ROLE_GRANT_OFFER_COLUMNS)}
463
446
  )
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]);
447
+ ${ROLE_GRANT_OFFER_WITH_GRANTOR_SELECT}`, [to_account_id, offer.role, offer.scope_id, offer.id]);
467
448
  // 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`.
449
+ // The `RETURNING` after the SET guarantees `offer.resulting_role_grant_id === role_grant.id`.
469
450
  // Accept binds the actor deterministically — populate both target
470
451
  // columns to mirror `role_grant_create` (the in-tx pair) so forensic
471
452
  // 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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"}