@sixb/pg 0.0.1

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 (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/agents/agent-storage.d.ts +20 -0
  4. package/dist/agents/agent-storage.d.ts.map +1 -0
  5. package/dist/agents/index.d.ts +6 -0
  6. package/dist/agents/index.d.ts.map +1 -0
  7. package/dist/agents/messages.d.ts +13 -0
  8. package/dist/agents/messages.d.ts.map +1 -0
  9. package/dist/agents/rows.d.ts +77 -0
  10. package/dist/agents/rows.d.ts.map +1 -0
  11. package/dist/agents/runs.d.ts +25 -0
  12. package/dist/agents/runs.d.ts.map +1 -0
  13. package/dist/agents/threads.d.ts +13 -0
  14. package/dist/agents/threads.d.ts.map +1 -0
  15. package/dist/auth-storage/access-tokens.d.ts +33 -0
  16. package/dist/auth-storage/access-tokens.d.ts.map +1 -0
  17. package/dist/auth-storage/auth-storage.d.ts +41 -0
  18. package/dist/auth-storage/auth-storage.d.ts.map +1 -0
  19. package/dist/auth-storage/group-memberships.d.ts +21 -0
  20. package/dist/auth-storage/group-memberships.d.ts.map +1 -0
  21. package/dist/auth-storage/identities.d.ts +17 -0
  22. package/dist/auth-storage/identities.d.ts.map +1 -0
  23. package/dist/auth-storage/index.d.ts +3 -0
  24. package/dist/auth-storage/index.d.ts.map +1 -0
  25. package/dist/auth-storage/invitations.d.ts +29 -0
  26. package/dist/auth-storage/invitations.d.ts.map +1 -0
  27. package/dist/auth-storage/magic-links.d.ts +28 -0
  28. package/dist/auth-storage/magic-links.d.ts.map +1 -0
  29. package/dist/auth-storage/oidc-attempts.d.ts +18 -0
  30. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -0
  31. package/dist/auth-storage/rows.d.ts +138 -0
  32. package/dist/auth-storage/rows.d.ts.map +1 -0
  33. package/dist/auth-storage/service-account-group-memberships.d.ts +17 -0
  34. package/dist/auth-storage/service-account-group-memberships.d.ts.map +1 -0
  35. package/dist/auth-storage/service-accounts.d.ts +14 -0
  36. package/dist/auth-storage/service-accounts.d.ts.map +1 -0
  37. package/dist/auth-storage/sessions.d.ts +55 -0
  38. package/dist/auth-storage/sessions.d.ts.map +1 -0
  39. package/dist/auth-storage/shared.d.ts +159 -0
  40. package/dist/auth-storage/shared.d.ts.map +1 -0
  41. package/dist/auth-storage/users.d.ts +19 -0
  42. package/dist/auth-storage/users.d.ts.map +1 -0
  43. package/dist/index.d.ts +127 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +11957 -0
  46. package/dist/index.js.map +55 -0
  47. package/dist/latest-run-query.d.ts +24 -0
  48. package/dist/latest-run-query.d.ts.map +1 -0
  49. package/dist/migrations.d.ts +20 -0
  50. package/dist/migrations.d.ts.map +1 -0
  51. package/dist/ontology-storage/commits.d.ts +11 -0
  52. package/dist/ontology-storage/commits.d.ts.map +1 -0
  53. package/dist/ontology-storage/index.d.ts +22 -0
  54. package/dist/ontology-storage/index.d.ts.map +1 -0
  55. package/dist/ontology-storage/materialization-session.d.ts +63 -0
  56. package/dist/ontology-storage/materialization-session.d.ts.map +1 -0
  57. package/dist/ontology-storage/materialization-state.d.ts +69 -0
  58. package/dist/ontology-storage/materialization-state.d.ts.map +1 -0
  59. package/dist/ontology-storage/materialization-writer.d.ts +15 -0
  60. package/dist/ontology-storage/materialization-writer.d.ts.map +1 -0
  61. package/dist/ontology-storage/materializations.d.ts +33 -0
  62. package/dist/ontology-storage/materializations.d.ts.map +1 -0
  63. package/dist/ontology-storage/outbox.d.ts +12 -0
  64. package/dist/ontology-storage/outbox.d.ts.map +1 -0
  65. package/dist/ontology-storage/shared.d.ts +95 -0
  66. package/dist/ontology-storage/shared.d.ts.map +1 -0
  67. package/dist/ontology-storage/sources.d.ts +25 -0
  68. package/dist/ontology-storage/sources.d.ts.map +1 -0
  69. package/dist/pg-action-run-storage.d.ts +21 -0
  70. package/dist/pg-action-run-storage.d.ts.map +1 -0
  71. package/dist/pg-client.d.ts +58 -0
  72. package/dist/pg-client.d.ts.map +1 -0
  73. package/dist/pg-object-query-compiler.d.ts +55 -0
  74. package/dist/pg-object-query-compiler.d.ts.map +1 -0
  75. package/dist/pg-object-storage.d.ts +90 -0
  76. package/dist/pg-object-storage.d.ts.map +1 -0
  77. package/dist/pg-pipeline-run-storage.d.ts +18 -0
  78. package/dist/pg-pipeline-run-storage.d.ts.map +1 -0
  79. package/dist/pg-projection-run-storage.d.ts +19 -0
  80. package/dist/pg-projection-run-storage.d.ts.map +1 -0
  81. package/dist/pg-rules-storage.d.ts +25 -0
  82. package/dist/pg-rules-storage.d.ts.map +1 -0
  83. package/dist/pg-sync-run-storage.d.ts +15 -0
  84. package/dist/pg-sync-run-storage.d.ts.map +1 -0
  85. package/dist/pg-timeseries-storage.d.ts +32 -0
  86. package/dist/pg-timeseries-storage.d.ts.map +1 -0
  87. package/dist/pg-webhook-delivery-storage.d.ts +17 -0
  88. package/dist/pg-webhook-delivery-storage.d.ts.map +1 -0
  89. package/dist/pg-webhook-run-storage.d.ts +14 -0
  90. package/dist/pg-webhook-run-storage.d.ts.map +1 -0
  91. package/dist/pg-workflow-intervention-storage.d.ts +16 -0
  92. package/dist/pg-workflow-intervention-storage.d.ts.map +1 -0
  93. package/dist/pg-workflow-run-storage.d.ts +49 -0
  94. package/dist/pg-workflow-run-storage.d.ts.map +1 -0
  95. package/dist/run-list-query.d.ts +30 -0
  96. package/dist/run-list-query.d.ts.map +1 -0
  97. package/dist/storage-errors.d.ts +16 -0
  98. package/dist/storage-errors.d.ts.map +1 -0
  99. package/dist/transactions.d.ts +10 -0
  100. package/dist/transactions.d.ts.map +1 -0
  101. package/dist/tsconfig.json +5 -0
  102. package/package.json +60 -0
  103. package/src/agents/agent-storage.ts +26 -0
  104. package/src/agents/index.ts +5 -0
  105. package/src/agents/messages.ts +144 -0
  106. package/src/agents/rows.ts +227 -0
  107. package/src/agents/runs.ts +330 -0
  108. package/src/agents/threads.ts +116 -0
  109. package/src/auth-storage/access-tokens.ts +254 -0
  110. package/src/auth-storage/auth-storage.ts +798 -0
  111. package/src/auth-storage/group-memberships.ts +47 -0
  112. package/src/auth-storage/identities.ts +85 -0
  113. package/src/auth-storage/index.ts +2 -0
  114. package/src/auth-storage/invitations.ts +280 -0
  115. package/src/auth-storage/magic-links.ts +140 -0
  116. package/src/auth-storage/oidc-attempts.ts +93 -0
  117. package/src/auth-storage/rows.ts +352 -0
  118. package/src/auth-storage/service-account-group-memberships.ts +150 -0
  119. package/src/auth-storage/service-accounts.ts +154 -0
  120. package/src/auth-storage/sessions.ts +193 -0
  121. package/src/auth-storage/shared.ts +799 -0
  122. package/src/auth-storage/users.ts +196 -0
  123. package/src/index.ts +367 -0
  124. package/src/latest-run-query.ts +42 -0
  125. package/src/migrations/001-initial-schema.sql +1195 -0
  126. package/src/migrations.ts +280 -0
  127. package/src/ontology-storage/commits.ts +115 -0
  128. package/src/ontology-storage/index.ts +35 -0
  129. package/src/ontology-storage/materialization-session.ts +426 -0
  130. package/src/ontology-storage/materialization-state.ts +1109 -0
  131. package/src/ontology-storage/materialization-writer.ts +509 -0
  132. package/src/ontology-storage/materializations.ts +939 -0
  133. package/src/ontology-storage/outbox.ts +216 -0
  134. package/src/ontology-storage/shared.ts +293 -0
  135. package/src/ontology-storage/sources.ts +658 -0
  136. package/src/pg-action-run-storage.ts +657 -0
  137. package/src/pg-client.ts +124 -0
  138. package/src/pg-object-query-compiler.ts +1545 -0
  139. package/src/pg-object-storage.ts +638 -0
  140. package/src/pg-pipeline-run-storage.ts +457 -0
  141. package/src/pg-projection-run-storage.ts +526 -0
  142. package/src/pg-rules-storage.ts +308 -0
  143. package/src/pg-sync-run-storage.ts +263 -0
  144. package/src/pg-timeseries-storage.ts +180 -0
  145. package/src/pg-webhook-delivery-storage.ts +180 -0
  146. package/src/pg-webhook-run-storage.ts +185 -0
  147. package/src/pg-workflow-intervention-storage.ts +339 -0
  148. package/src/pg-workflow-run-storage.ts +1065 -0
  149. package/src/run-list-query.ts +108 -0
  150. package/src/sql.d.ts +4 -0
  151. package/src/storage-errors.ts +72 -0
  152. package/src/transactions.ts +58 -0
@@ -0,0 +1,799 @@
1
+ import type { AuthSessionAudience, Principal } from "@sixb/core"
2
+ import type {
3
+ CompleteAuthSessionInput,
4
+ CreateAuthSessionInput,
5
+ GroupMembershipRecord,
6
+ InvitationRecord,
7
+ MagicLinkRecord,
8
+ OidcAuthorizationAttemptRecord,
9
+ SessionRecord,
10
+ UpsertAuthGroupMembershipInput,
11
+ UserRecord,
12
+ } from "@sixb/core/storage"
13
+ import { AuthStorageError } from "@sixb/core/storage"
14
+ import type { SQLClient } from "../pg-client"
15
+ import { isUniqueViolation } from "../storage-errors"
16
+ import type {
17
+ PgAuthGroupMembershipRow,
18
+ PgAuthInvitationRow,
19
+ PgAuthMagicLinkRow,
20
+ PgAuthOidcAttemptRow,
21
+ PgAuthSessionRow,
22
+ PgAuthUserIdentityRow,
23
+ PgAuthUserRow,
24
+ } from "./rows"
25
+ import {
26
+ rowToGroupMembershipRecord,
27
+ rowToInvitationRecord,
28
+ rowToMagicLinkRecord,
29
+ rowToOidcAuthorizationAttemptRecord,
30
+ rowToSessionRecord,
31
+ rowToUserRecord,
32
+ } from "./rows"
33
+
34
+ export type PgValue = string | number | Date | null
35
+
36
+ export function assertNonEmpty(value: string, label: string): string {
37
+ const normalized = value.trim()
38
+ if (!normalized) {
39
+ throw new AuthStorageError("invalid_input", `[Sixb] ${label} must be a non-empty string.`)
40
+ }
41
+ return normalized
42
+ }
43
+
44
+ export function normalizeEmail(email: string): string {
45
+ return assertNonEmpty(email, "Email").toLowerCase()
46
+ }
47
+
48
+ export function normalizeGroupIds(groupIds: readonly string[] | undefined): readonly string[] {
49
+ return [...new Set((groupIds ?? []).map((groupId) => assertNonEmpty(groupId, "Group id")))]
50
+ }
51
+
52
+ export function dateOrNow(value: Date | undefined): Date {
53
+ return value ? new Date(value) : new Date()
54
+ }
55
+
56
+ export function hasEmptyFilter(values: readonly unknown[] | undefined): boolean {
57
+ return values !== undefined && values.length === 0
58
+ }
59
+
60
+ export function appendPagination(
61
+ query: string,
62
+ params: PgValue[],
63
+ nextIndex: number,
64
+ input: { readonly limit?: number; readonly offset?: number }
65
+ ): string {
66
+ const offset = input.offset ?? 0
67
+
68
+ if (input.limit !== undefined) {
69
+ params.push(input.limit, offset)
70
+ return `${query} LIMIT $${nextIndex++} OFFSET $${nextIndex++}`
71
+ }
72
+
73
+ if (offset > 0) {
74
+ params.push(offset)
75
+ return `${query} OFFSET $${nextIndex++}`
76
+ }
77
+
78
+ return query
79
+ }
80
+
81
+ export function placeholders(startIndex: number, count: number): readonly string[] {
82
+ return Array.from({ length: count }, (_, index) => `$${startIndex + index}`)
83
+ }
84
+
85
+ export async function getUserRowById(
86
+ sql: SQLClient,
87
+ params: { readonly projectId: string; readonly id: string },
88
+ options: { readonly forUpdate?: boolean } = {}
89
+ ): Promise<PgAuthUserRow | null> {
90
+ const [row] = await sql.unsafe<PgAuthUserRow[]>(
91
+ `
92
+ SELECT *
93
+ FROM auth_users
94
+ WHERE project_id = $1
95
+ AND id = $2
96
+ ${options.forUpdate ? "FOR UPDATE" : ""}
97
+ `,
98
+ [params.projectId, params.id]
99
+ )
100
+
101
+ return row ?? null
102
+ }
103
+
104
+ export async function getUserById(
105
+ sql: SQLClient,
106
+ params: { readonly projectId: string; readonly id: string }
107
+ ): Promise<UserRecord | null> {
108
+ const row = await getUserRowById(sql, params)
109
+ return row ? rowToUserRecord(row) : null
110
+ }
111
+
112
+ export async function getUserRowByEmail(
113
+ sql: SQLClient,
114
+ params: { readonly projectId: string; readonly email: string },
115
+ options: { readonly forUpdate?: boolean } = {}
116
+ ): Promise<PgAuthUserRow | null> {
117
+ const [row] = await sql.unsafe<PgAuthUserRow[]>(
118
+ `
119
+ SELECT *
120
+ FROM auth_users
121
+ WHERE project_id = $1
122
+ AND email = $2
123
+ ${options.forUpdate ? "FOR UPDATE" : ""}
124
+ `,
125
+ [params.projectId, normalizeEmail(params.email)]
126
+ )
127
+
128
+ return row ?? null
129
+ }
130
+
131
+ export async function getUserByEmail(
132
+ sql: SQLClient,
133
+ params: { readonly projectId: string; readonly email: string }
134
+ ): Promise<UserRecord | null> {
135
+ const row = await getUserRowByEmail(sql, params)
136
+ return row ? rowToUserRecord(row) : null
137
+ }
138
+
139
+ export async function requireUserById(
140
+ sql: SQLClient,
141
+ params: { readonly projectId: string; readonly id: string }
142
+ ): Promise<UserRecord> {
143
+ const user = await getUserById(sql, params)
144
+ if (!user) {
145
+ throw new AuthStorageError(
146
+ "missing_user",
147
+ `[Sixb] User '${params.id}' not found for project '${params.projectId}'.`
148
+ )
149
+ }
150
+ return user
151
+ }
152
+
153
+ export async function getIdentityRowBySubject(
154
+ sql: SQLClient,
155
+ params: {
156
+ readonly projectId: string
157
+ readonly strategyId: string
158
+ readonly subject: string
159
+ },
160
+ options: { readonly forUpdate?: boolean } = {}
161
+ ): Promise<PgAuthUserIdentityRow | null> {
162
+ const [row] = await sql.unsafe<PgAuthUserIdentityRow[]>(
163
+ `
164
+ SELECT *
165
+ FROM auth_user_identities
166
+ WHERE project_id = $1
167
+ AND strategy_id = $2
168
+ AND subject = $3
169
+ ${options.forUpdate ? "FOR UPDATE" : ""}
170
+ `,
171
+ [params.projectId, params.strategyId, params.subject]
172
+ )
173
+
174
+ return row ?? null
175
+ }
176
+
177
+ export async function getSessionRowById(
178
+ sql: SQLClient,
179
+ params: { readonly projectId: string; readonly id: string },
180
+ options: { readonly forUpdate?: boolean } = {}
181
+ ): Promise<PgAuthSessionRow | null> {
182
+ const [row] = await sql.unsafe<PgAuthSessionRow[]>(
183
+ `
184
+ SELECT *
185
+ FROM auth_sessions
186
+ WHERE project_id = $1
187
+ AND id = $2
188
+ ${options.forUpdate ? "FOR UPDATE" : ""}
189
+ `,
190
+ [params.projectId, params.id]
191
+ )
192
+
193
+ return row ?? null
194
+ }
195
+
196
+ export async function getSessionById(
197
+ sql: SQLClient,
198
+ params: { readonly projectId: string; readonly id: string }
199
+ ): Promise<ReturnType<typeof rowToSessionRecord> | null> {
200
+ const row = await getSessionRowById(sql, params)
201
+ return row ? rowToSessionRecord(row) : null
202
+ }
203
+
204
+ export async function requireSessionById(
205
+ sql: SQLClient,
206
+ params: { readonly projectId: string; readonly id: string }
207
+ ): Promise<ReturnType<typeof rowToSessionRecord>> {
208
+ const session = await getSessionById(sql, params)
209
+ if (!session) {
210
+ throw new AuthStorageError(
211
+ "missing_session",
212
+ `[Sixb] Session '${params.id}' not found for project '${params.projectId}'.`
213
+ )
214
+ }
215
+ return session
216
+ }
217
+
218
+ export async function getInvitationGroupIds(
219
+ sql: SQLClient,
220
+ params: { readonly projectId: string; readonly invitationId: string }
221
+ ): Promise<readonly string[]> {
222
+ const rows = (await sql`
223
+ SELECT group_id
224
+ FROM auth_invitation_groups
225
+ WHERE project_id = ${params.projectId}
226
+ AND invitation_id = ${params.invitationId}
227
+ ORDER BY position ASC, group_id ASC
228
+ `) as Array<{ readonly group_id: string }>
229
+
230
+ return rows.map((row) => row.group_id)
231
+ }
232
+
233
+ export async function getInvitationById(
234
+ sql: SQLClient,
235
+ params: { readonly projectId: string; readonly id: string },
236
+ options: { readonly forUpdate?: boolean } = {}
237
+ ): Promise<InvitationRecord | null> {
238
+ const [row] = await sql.unsafe<PgAuthInvitationRow[]>(
239
+ `
240
+ SELECT *
241
+ FROM auth_invitations
242
+ WHERE project_id = $1
243
+ AND id = $2
244
+ ${options.forUpdate ? "FOR UPDATE" : ""}
245
+ `,
246
+ [params.projectId, params.id]
247
+ )
248
+
249
+ return row ? invitationRowToRecord(sql, row) : null
250
+ }
251
+
252
+ export async function requireInvitationById(
253
+ sql: SQLClient,
254
+ params: { readonly projectId: string; readonly id: string },
255
+ options: { readonly forUpdate?: boolean } = {}
256
+ ): Promise<InvitationRecord> {
257
+ const invitation = await getInvitationById(sql, params, options)
258
+ if (!invitation) {
259
+ throw new AuthStorageError(
260
+ "missing_invitation",
261
+ `[Sixb] Invitation '${params.id}' not found for project '${params.projectId}'.`
262
+ )
263
+ }
264
+ return invitation
265
+ }
266
+
267
+ export async function getActiveInvitationByEmail(
268
+ sql: SQLClient,
269
+ params: { readonly projectId: string; readonly email: string; readonly now: Date },
270
+ options: { readonly forUpdate?: boolean } = {}
271
+ ): Promise<InvitationRecord | null> {
272
+ const [row] = await sql.unsafe<PgAuthInvitationRow[]>(
273
+ `
274
+ SELECT *
275
+ FROM auth_invitations
276
+ WHERE project_id = $1
277
+ AND email = $2
278
+ AND status = 'pending'
279
+ AND expires_at > $3
280
+ ORDER BY created_at DESC, id DESC
281
+ LIMIT 1
282
+ ${options.forUpdate ? "FOR UPDATE" : ""}
283
+ `,
284
+ [params.projectId, normalizeEmail(params.email), params.now]
285
+ )
286
+
287
+ return row ? invitationRowToRecord(sql, row) : null
288
+ }
289
+
290
+ export async function replaceInvitationGroups(
291
+ sql: SQLClient,
292
+ params: {
293
+ readonly projectId: string
294
+ readonly invitationId: string
295
+ readonly groupIds: readonly string[]
296
+ }
297
+ ): Promise<void> {
298
+ await sql`
299
+ DELETE FROM auth_invitation_groups
300
+ WHERE project_id = ${params.projectId}
301
+ AND invitation_id = ${params.invitationId}
302
+ `
303
+
304
+ for (const [index, groupId] of params.groupIds.entries()) {
305
+ await sql`
306
+ INSERT INTO auth_invitation_groups (
307
+ project_id,
308
+ invitation_id,
309
+ group_id,
310
+ position
311
+ ) VALUES (
312
+ ${params.projectId},
313
+ ${params.invitationId},
314
+ ${groupId},
315
+ ${index}
316
+ )
317
+ `
318
+ }
319
+ }
320
+
321
+ export async function getMagicLinkRowById(
322
+ sql: SQLClient,
323
+ params: { readonly projectId: string; readonly id: string },
324
+ options: { readonly forUpdate?: boolean } = {}
325
+ ): Promise<PgAuthMagicLinkRow | null> {
326
+ const [row] = await sql.unsafe<PgAuthMagicLinkRow[]>(
327
+ `
328
+ SELECT *
329
+ FROM auth_magic_links
330
+ WHERE project_id = $1
331
+ AND id = $2
332
+ ${options.forUpdate ? "FOR UPDATE" : ""}
333
+ `,
334
+ [params.projectId, params.id]
335
+ )
336
+
337
+ return row ?? null
338
+ }
339
+
340
+ export async function getMagicLinkById(
341
+ sql: SQLClient,
342
+ params: { readonly projectId: string; readonly id: string }
343
+ ): Promise<MagicLinkRecord | null> {
344
+ const row = await getMagicLinkRowById(sql, params)
345
+ return row ? rowToMagicLinkRecord(row) : null
346
+ }
347
+
348
+ export async function consumeMagicLink(
349
+ sql: SQLClient,
350
+ params: {
351
+ readonly projectId: string
352
+ readonly id: string
353
+ readonly tokenHash: string
354
+ readonly consumedAt: Date
355
+ }
356
+ ): Promise<MagicLinkRecord> {
357
+ const projectId = assertNonEmpty(params.projectId, "Project id")
358
+ const id = assertNonEmpty(params.id, "Magic link id")
359
+ const tokenHash = assertNonEmpty(params.tokenHash, "Magic link token hash")
360
+ const row = await getMagicLinkRowById(sql, { projectId, id }, { forUpdate: true })
361
+
362
+ if (!row) {
363
+ throw new AuthStorageError(
364
+ "missing_magic_link",
365
+ `[Sixb] Magic link '${id}' not found for project '${projectId}'.`
366
+ )
367
+ }
368
+
369
+ assertMagicLinkUsable(projectId, id, tokenHash, params.consumedAt, row)
370
+
371
+ await sql`
372
+ UPDATE auth_magic_links
373
+ SET consumed_at = ${params.consumedAt}
374
+ WHERE project_id = ${projectId}
375
+ AND id = ${id}
376
+ `
377
+
378
+ return rowToMagicLinkRecord({
379
+ ...row,
380
+ consumed_at: params.consumedAt,
381
+ })
382
+ }
383
+
384
+ export async function getOidcAttemptRowById(
385
+ sql: SQLClient,
386
+ params: { readonly projectId: string; readonly id: string },
387
+ options: { readonly forUpdate?: boolean } = {}
388
+ ): Promise<PgAuthOidcAttemptRow | null> {
389
+ const [row] = await sql.unsafe<PgAuthOidcAttemptRow[]>(
390
+ `
391
+ SELECT *
392
+ FROM auth_oidc_authorization_attempts
393
+ WHERE project_id = $1
394
+ AND id = $2
395
+ ${options.forUpdate ? "FOR UPDATE" : ""}
396
+ `,
397
+ [params.projectId, params.id]
398
+ )
399
+
400
+ return row ?? null
401
+ }
402
+
403
+ export async function getOidcAttemptById(
404
+ sql: SQLClient,
405
+ params: { readonly projectId: string; readonly id: string }
406
+ ): Promise<OidcAuthorizationAttemptRecord | null> {
407
+ const row = await getOidcAttemptRowById(sql, params)
408
+ return row ? rowToOidcAuthorizationAttemptRecord(row) : null
409
+ }
410
+
411
+ export async function consumeOidcAttempt(
412
+ sql: SQLClient,
413
+ params: {
414
+ readonly projectId: string
415
+ readonly id: string
416
+ readonly stateHash: string
417
+ readonly consumedAt: Date
418
+ }
419
+ ): Promise<OidcAuthorizationAttemptRecord> {
420
+ const projectId = assertNonEmpty(params.projectId, "Project id")
421
+ const id = assertNonEmpty(params.id, "OIDC authorization attempt id")
422
+ const stateHash = assertNonEmpty(params.stateHash, "OIDC state hash")
423
+ const row = await getOidcAttemptRowById(sql, { projectId, id }, { forUpdate: true })
424
+
425
+ if (!row) {
426
+ throw new AuthStorageError(
427
+ "missing_oidc_attempt",
428
+ `[Sixb] OIDC authorization attempt '${id}' not found for project '${projectId}'.`
429
+ )
430
+ }
431
+
432
+ assertOidcAttemptUsable(projectId, id, stateHash, params.consumedAt, row)
433
+
434
+ await sql`
435
+ UPDATE auth_oidc_authorization_attempts
436
+ SET consumed_at = ${params.consumedAt}
437
+ WHERE project_id = ${projectId}
438
+ AND id = ${id}
439
+ `
440
+
441
+ return rowToOidcAuthorizationAttemptRecord({
442
+ ...row,
443
+ consumed_at: params.consumedAt,
444
+ })
445
+ }
446
+
447
+ export async function validateCompleteSessionInput(
448
+ sql: SQLClient,
449
+ projectId: string,
450
+ session: CompleteAuthSessionInput
451
+ ): Promise<void> {
452
+ const sessionId = assertNonEmpty(session.id, "Session id")
453
+ assertNonEmpty(session.audience, "Session audience")
454
+ assertNonEmpty(session.tokenHash, "Session token hash")
455
+ await assertSessionIdAvailable(sql, projectId, sessionId)
456
+ }
457
+
458
+ export async function assertSessionIdAvailable(
459
+ sql: SQLClient,
460
+ projectId: string,
461
+ sessionId: string
462
+ ): Promise<void> {
463
+ if (await getSessionRowById(sql, { projectId, id: sessionId })) {
464
+ throw new AuthStorageError(
465
+ "duplicate_session",
466
+ `[Sixb] Session '${sessionId}' already exists for project '${projectId}'.`
467
+ )
468
+ }
469
+ }
470
+
471
+ export async function createSession(
472
+ sql: SQLClient,
473
+ input: CreateAuthSessionInput
474
+ ): Promise<SessionRecord> {
475
+ const id = assertNonEmpty(input.id, "Session id")
476
+ const projectId = assertNonEmpty(input.projectId, "Project id")
477
+ const userId = assertNonEmpty(input.userId, "User id")
478
+ const strategyId = assertNonEmpty(input.strategyId, "Strategy id")
479
+ const audience = assertNonEmpty(input.audience, "Session audience")
480
+ const tokenHash = assertNonEmpty(input.tokenHash, "Session token hash")
481
+
482
+ await requireUserById(sql, { projectId, id: userId })
483
+ await assertSessionIdAvailable(sql, projectId, id)
484
+
485
+ try {
486
+ const [row] = await sql<PgAuthSessionRow[]>`
487
+ INSERT INTO auth_sessions (
488
+ project_id,
489
+ id,
490
+ user_id,
491
+ strategy_id,
492
+ audience,
493
+ token_hash,
494
+ created_at,
495
+ expires_at,
496
+ absolute_expires_at,
497
+ user_agent,
498
+ ip_address
499
+ ) VALUES (
500
+ ${projectId},
501
+ ${id},
502
+ ${userId},
503
+ ${strategyId},
504
+ ${audience},
505
+ ${tokenHash},
506
+ ${input.createdAt},
507
+ ${input.expiresAt},
508
+ ${input.absoluteExpiresAt ?? null},
509
+ ${input.userAgent ?? null},
510
+ ${input.ipAddress ?? null}
511
+ )
512
+ RETURNING *
513
+ `
514
+
515
+ return rowToSessionRecord(row)
516
+ } catch (error) {
517
+ mapUniqueConstraintError(
518
+ error,
519
+ "duplicate_session",
520
+ `[Sixb] Session '${id}' already exists for project '${projectId}'.`
521
+ )
522
+ }
523
+ }
524
+
525
+ export async function revokeActiveSessionsForUser(
526
+ sql: SQLClient,
527
+ params: {
528
+ readonly projectId: string
529
+ readonly userId: string
530
+ readonly audience?: AuthSessionAudience
531
+ readonly revokedAt: Date
532
+ }
533
+ ): Promise<readonly SessionRecord[]> {
534
+ const rows =
535
+ params.audience === undefined
536
+ ? await sql<PgAuthSessionRow[]>`
537
+ SELECT *
538
+ FROM auth_sessions
539
+ WHERE project_id = ${params.projectId}
540
+ AND user_id = ${params.userId}
541
+ AND revoked_at IS NULL
542
+ AND expires_at > ${params.revokedAt}
543
+ AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.revokedAt})
544
+ ORDER BY created_at ASC, id ASC
545
+ FOR UPDATE
546
+ `
547
+ : await sql<PgAuthSessionRow[]>`
548
+ SELECT *
549
+ FROM auth_sessions
550
+ WHERE project_id = ${params.projectId}
551
+ AND user_id = ${params.userId}
552
+ AND audience = ${params.audience}
553
+ AND revoked_at IS NULL
554
+ AND expires_at > ${params.revokedAt}
555
+ AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.revokedAt})
556
+ ORDER BY created_at ASC, id ASC
557
+ FOR UPDATE
558
+ `
559
+
560
+ if (params.audience === undefined) {
561
+ await sql`
562
+ UPDATE auth_sessions
563
+ SET revoked_at = ${params.revokedAt}
564
+ WHERE project_id = ${params.projectId}
565
+ AND user_id = ${params.userId}
566
+ AND revoked_at IS NULL
567
+ AND expires_at > ${params.revokedAt}
568
+ AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.revokedAt})
569
+ `
570
+ } else {
571
+ await sql`
572
+ UPDATE auth_sessions
573
+ SET revoked_at = ${params.revokedAt}
574
+ WHERE project_id = ${params.projectId}
575
+ AND user_id = ${params.userId}
576
+ AND audience = ${params.audience}
577
+ AND revoked_at IS NULL
578
+ AND expires_at > ${params.revokedAt}
579
+ AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.revokedAt})
580
+ `
581
+ }
582
+
583
+ return rows.map((row) =>
584
+ rowToSessionRecord({
585
+ ...row,
586
+ revoked_at: params.revokedAt,
587
+ })
588
+ )
589
+ }
590
+
591
+ export async function upsertGroupMembership(
592
+ sql: SQLClient,
593
+ input: UpsertAuthGroupMembershipInput
594
+ ): Promise<GroupMembershipRecord> {
595
+ const projectId = assertNonEmpty(input.projectId, "Project id")
596
+ const userId = assertNonEmpty(input.userId, "User id")
597
+ const groupId = assertNonEmpty(input.groupId, "Group id")
598
+
599
+ await requireUserById(sql, { projectId, id: userId })
600
+
601
+ await sql`
602
+ INSERT INTO auth_group_memberships (
603
+ project_id,
604
+ user_id,
605
+ group_id,
606
+ source,
607
+ created_at
608
+ ) VALUES (
609
+ ${projectId},
610
+ ${userId},
611
+ ${groupId},
612
+ ${input.source},
613
+ ${dateOrNow(input.createdAt)}
614
+ )
615
+ ON CONFLICT (project_id, user_id, group_id) DO NOTHING
616
+ `
617
+
618
+ const membership = await getGroupMembership(sql, { projectId, userId, groupId })
619
+ if (!membership) {
620
+ throw new AuthStorageError(
621
+ "missing_user",
622
+ `[Sixb] Failed to load group membership '${groupId}' for user '${userId}'.`
623
+ )
624
+ }
625
+ return membership
626
+ }
627
+
628
+ export async function removeGroupMembership(
629
+ sql: SQLClient,
630
+ params: { readonly projectId: string; readonly userId: string; readonly groupId: string }
631
+ ): Promise<GroupMembershipRecord | null> {
632
+ const projectId = assertNonEmpty(params.projectId, "Project id")
633
+ const userId = assertNonEmpty(params.userId, "User id")
634
+ const groupId = assertNonEmpty(params.groupId, "Group id")
635
+
636
+ const [row] = await sql<PgAuthGroupMembershipRow[]>`
637
+ DELETE FROM auth_group_memberships
638
+ WHERE project_id = ${projectId}
639
+ AND user_id = ${userId}
640
+ AND group_id = ${groupId}
641
+ RETURNING *
642
+ `
643
+
644
+ return row ? rowToGroupMembershipRecord(row) : null
645
+ }
646
+
647
+ export async function getGroupMembership(
648
+ sql: SQLClient,
649
+ params: { readonly projectId: string; readonly userId: string; readonly groupId: string }
650
+ ): Promise<GroupMembershipRecord | null> {
651
+ const [row] = await sql<PgAuthGroupMembershipRow[]>`
652
+ SELECT *
653
+ FROM auth_group_memberships
654
+ WHERE project_id = ${params.projectId}
655
+ AND user_id = ${params.userId}
656
+ AND group_id = ${params.groupId}
657
+ `
658
+
659
+ return row ? rowToGroupMembershipRecord(row) : null
660
+ }
661
+
662
+ export async function listMembershipsForUser(
663
+ sql: SQLClient,
664
+ params: { readonly projectId: string; readonly userId: string }
665
+ ): Promise<readonly GroupMembershipRecord[]> {
666
+ const rows = await sql<PgAuthGroupMembershipRow[]>`
667
+ SELECT *
668
+ FROM auth_group_memberships
669
+ WHERE project_id = ${params.projectId}
670
+ AND user_id = ${params.userId}
671
+ ORDER BY group_id ASC
672
+ `
673
+
674
+ return rows.map(rowToGroupMembershipRecord)
675
+ }
676
+
677
+ export async function revokeActiveMagicLinksForEmail(
678
+ sql: SQLClient,
679
+ params: { readonly projectId: string; readonly email: string; readonly revokedAt: Date }
680
+ ): Promise<readonly MagicLinkRecord[]> {
681
+ const email = normalizeEmail(params.email)
682
+ const rows = await sql<PgAuthMagicLinkRow[]>`
683
+ SELECT *
684
+ FROM auth_magic_links
685
+ WHERE project_id = ${params.projectId}
686
+ AND email = ${email}
687
+ AND consumed_at IS NULL
688
+ AND revoked_at IS NULL
689
+ AND expires_at > ${params.revokedAt}
690
+ ORDER BY created_at ASC, id ASC
691
+ FOR UPDATE
692
+ `
693
+
694
+ await sql`
695
+ UPDATE auth_magic_links
696
+ SET revoked_at = ${params.revokedAt}
697
+ WHERE project_id = ${params.projectId}
698
+ AND email = ${email}
699
+ AND consumed_at IS NULL
700
+ AND revoked_at IS NULL
701
+ AND expires_at > ${params.revokedAt}
702
+ `
703
+
704
+ return rows.map((row) =>
705
+ rowToMagicLinkRecord({
706
+ ...row,
707
+ revoked_at: params.revokedAt,
708
+ })
709
+ )
710
+ }
711
+
712
+ export function invitationCreatorColumns(principal: Principal | undefined): {
713
+ readonly createdByUserId: string | null
714
+ readonly createdByServiceAccountId: string | null
715
+ readonly createdBySystemId: string | null
716
+ } {
717
+ return {
718
+ createdByUserId: principal?.type === "user" ? principal.id : null,
719
+ createdByServiceAccountId: principal?.type === "serviceAccount" ? principal.id : null,
720
+ createdBySystemId: principal?.type === "system" ? principal.id : null,
721
+ }
722
+ }
723
+
724
+ export function assertMagicLinkUsable(
725
+ projectId: string,
726
+ id: string,
727
+ tokenHash: string,
728
+ at: Date,
729
+ row: PgAuthMagicLinkRow
730
+ ): void {
731
+ if (row.token_hash !== tokenHash || row.consumed_at || row.revoked_at) {
732
+ throw new AuthStorageError(
733
+ "invalid_magic_link",
734
+ `[Sixb] Magic link '${id}' is not valid for project '${projectId}'.`
735
+ )
736
+ }
737
+
738
+ if (new Date(row.expires_at) <= at) {
739
+ throw new AuthStorageError(
740
+ "expired_magic_link",
741
+ `[Sixb] Magic link '${id}' is expired for project '${projectId}'.`
742
+ )
743
+ }
744
+ }
745
+
746
+ export function assertOidcAttemptUsable(
747
+ projectId: string,
748
+ id: string,
749
+ stateHash: string,
750
+ at: Date,
751
+ row: PgAuthOidcAttemptRow
752
+ ): void {
753
+ if (row.state_hash !== stateHash || row.consumed_at) {
754
+ throw new AuthStorageError(
755
+ "invalid_oidc_attempt",
756
+ `[Sixb] OIDC authorization attempt '${id}' is not valid for project '${projectId}'.`
757
+ )
758
+ }
759
+
760
+ if (new Date(row.expires_at) <= at) {
761
+ throw new AuthStorageError(
762
+ "expired_oidc_attempt",
763
+ `[Sixb] OIDC authorization attempt '${id}' is expired for project '${projectId}'.`
764
+ )
765
+ }
766
+ }
767
+
768
+ export function mapUniqueConstraintError(
769
+ error: unknown,
770
+ code:
771
+ | "duplicate_access_token"
772
+ | "duplicate_identity"
773
+ | "duplicate_invitation"
774
+ | "duplicate_magic_link"
775
+ | "duplicate_oidc_attempt"
776
+ | "duplicate_service_account"
777
+ | "duplicate_session"
778
+ | "duplicate_user",
779
+ message: string
780
+ ): never {
781
+ if (isUniqueViolation(error)) {
782
+ throw new AuthStorageError(code, message)
783
+ }
784
+
785
+ throw error
786
+ }
787
+
788
+ async function invitationRowToRecord(
789
+ sql: SQLClient,
790
+ row: PgAuthInvitationRow
791
+ ): Promise<InvitationRecord> {
792
+ return rowToInvitationRecord(
793
+ row,
794
+ await getInvitationGroupIds(sql, {
795
+ projectId: row.project_id,
796
+ invitationId: row.id,
797
+ })
798
+ )
799
+ }