@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,798 @@
1
+ import type {
2
+ AuthStorage,
3
+ CompleteMagicLinkSignInInput,
4
+ CompleteOidcSignInInput,
5
+ CompleteSignInResult,
6
+ GroupMembershipRecord,
7
+ InvitationRecord,
8
+ SessionRecord,
9
+ SuspendUserAndRevokeSessionsInput,
10
+ UserIdentityRecord,
11
+ UserRecord,
12
+ } from "@sixb/core/storage"
13
+ import { AuthStorageError } from "@sixb/core/storage"
14
+ import type { SQLClient } from "../pg-client"
15
+ import {
16
+ authLockKey,
17
+ lockAdvisoryKeys,
18
+ type PgStoreClient,
19
+ runPgTransaction,
20
+ } from "../transactions"
21
+ import { PgAuthAccessTokenStore } from "./access-tokens"
22
+ import { PgAuthGroupMembershipStore } from "./group-memberships"
23
+ import { PgAuthUserIdentityStore } from "./identities"
24
+ import { PgAuthInvitationStore } from "./invitations"
25
+ import { PgAuthMagicLinkStore } from "./magic-links"
26
+ import { PgAuthOidcAuthorizationAttemptStore } from "./oidc-attempts"
27
+ import type {
28
+ PgAuthMagicLinkRow,
29
+ PgAuthOidcAttemptRow,
30
+ PgAuthUserIdentityRow,
31
+ PgAuthUserRow,
32
+ } from "./rows"
33
+ import { rowToIdentityRecord, rowToUserRecord, serializeOptionalRecord } from "./rows"
34
+ import { PgAuthServiceAccountGroupMembershipStore } from "./service-account-group-memberships"
35
+ import { PgAuthServiceAccountStore } from "./service-accounts"
36
+ import { PgAuthSessionStore } from "./sessions"
37
+ import {
38
+ assertMagicLinkUsable,
39
+ assertNonEmpty,
40
+ assertOidcAttemptUsable,
41
+ consumeMagicLink,
42
+ consumeOidcAttempt,
43
+ createSession,
44
+ getActiveInvitationByEmail,
45
+ getIdentityRowBySubject,
46
+ getMagicLinkRowById,
47
+ getOidcAttemptRowById,
48
+ getUserRowByEmail,
49
+ getUserRowById,
50
+ mapUniqueConstraintError,
51
+ normalizeEmail,
52
+ normalizeGroupIds,
53
+ revokeActiveSessionsForUser,
54
+ upsertGroupMembership,
55
+ validateCompleteSessionInput,
56
+ } from "./shared"
57
+ import { PgAuthUserStore } from "./users"
58
+
59
+ interface AuthTransactionError {
60
+ readonly error: AuthStorageError
61
+ }
62
+
63
+ export interface PgAuthStorageOptions {
64
+ readonly sql: PgStoreClient
65
+ }
66
+
67
+ export class PgAuthStorage implements AuthStorage {
68
+ private readonly sql: PgStoreClient
69
+
70
+ readonly users: PgAuthUserStore
71
+ readonly identities: PgAuthUserIdentityStore
72
+ readonly serviceAccounts: PgAuthServiceAccountStore
73
+ readonly serviceAccountGroupMemberships: PgAuthServiceAccountGroupMembershipStore
74
+ readonly sessions: PgAuthSessionStore
75
+ readonly accessTokens: PgAuthAccessTokenStore
76
+ readonly invitations: PgAuthInvitationStore
77
+ readonly groupMemberships: PgAuthGroupMembershipStore
78
+ readonly magicLinks: PgAuthMagicLinkStore
79
+ readonly oidcAuthorizationAttempts: PgAuthOidcAuthorizationAttemptStore
80
+
81
+ constructor(options: PgAuthStorageOptions) {
82
+ this.sql = options.sql
83
+ this.users = new PgAuthUserStore(this.sql)
84
+ this.identities = new PgAuthUserIdentityStore(this.sql)
85
+ this.serviceAccounts = new PgAuthServiceAccountStore(this.sql)
86
+ this.serviceAccountGroupMemberships = new PgAuthServiceAccountGroupMembershipStore(this.sql)
87
+ this.sessions = new PgAuthSessionStore(this.sql)
88
+ this.accessTokens = new PgAuthAccessTokenStore(this.sql)
89
+ this.invitations = new PgAuthInvitationStore(this.sql)
90
+ this.groupMemberships = new PgAuthGroupMembershipStore(this.sql)
91
+ this.magicLinks = new PgAuthMagicLinkStore(this.sql)
92
+ this.oidcAuthorizationAttempts = new PgAuthOidcAuthorizationAttemptStore(this.sql)
93
+ }
94
+
95
+ async completeMagicLinkSignIn(
96
+ input: CompleteMagicLinkSignInInput
97
+ ): Promise<CompleteSignInResult> {
98
+ const result = await runPgTransaction(
99
+ this.sql,
100
+ async (tx): Promise<CompleteSignInResult | AuthTransactionError> => {
101
+ const projectId = assertNonEmpty(input.projectId, "Project id")
102
+ const completedAt = new Date(input.completedAt)
103
+ const initialMagicLink = await getMagicLinkRowById(tx, {
104
+ projectId,
105
+ id: input.magicLinkId,
106
+ })
107
+
108
+ if (!initialMagicLink) {
109
+ throw new AuthStorageError(
110
+ "missing_magic_link",
111
+ `[Sixb] Magic link '${input.magicLinkId}' not found for project '${projectId}'.`
112
+ )
113
+ }
114
+
115
+ const email = normalizeEmail(initialMagicLink.email)
116
+ await lockAdvisoryKeys(tx, [
117
+ authLockKey("bootstrap", projectId),
118
+ authLockKey("invitations", projectId, email),
119
+ authLockKey("magic-links", projectId, email),
120
+ authLockKey("users", projectId, email),
121
+ ])
122
+
123
+ const magicLink = await getMagicLinkRowById(
124
+ tx,
125
+ {
126
+ projectId,
127
+ id: input.magicLinkId,
128
+ },
129
+ { forUpdate: true }
130
+ )
131
+
132
+ this.assertMagicLinkUsable(
133
+ projectId,
134
+ input.magicLinkId,
135
+ input.tokenHash,
136
+ completedAt,
137
+ magicLink
138
+ )
139
+
140
+ const existingUserRow = await getUserRowByEmail(
141
+ tx,
142
+ {
143
+ projectId,
144
+ email,
145
+ },
146
+ { forUpdate: true }
147
+ )
148
+ const activeInvitation = await getActiveInvitationByEmail(
149
+ tx,
150
+ {
151
+ projectId,
152
+ email,
153
+ now: completedAt,
154
+ },
155
+ { forUpdate: true }
156
+ )
157
+ const shouldCreateUser = !existingUserRow
158
+ const manualGroupIds = normalizeGroupIds(input.manualGroupIds)
159
+
160
+ if (existingUserRow?.status === "suspended") {
161
+ await consumeMagicLink(tx, {
162
+ projectId,
163
+ id: input.magicLinkId,
164
+ tokenHash: input.tokenHash,
165
+ consumedAt: completedAt,
166
+ })
167
+ return {
168
+ error: new AuthStorageError(
169
+ "suspended_user",
170
+ `[Sixb] User '${existingUserRow.id}' is suspended for project '${projectId}'.`
171
+ ),
172
+ }
173
+ }
174
+
175
+ if (shouldCreateUser && !activeInvitation && !input.allowUserCreationWithoutInvitation) {
176
+ await consumeMagicLink(tx, {
177
+ projectId,
178
+ id: input.magicLinkId,
179
+ tokenHash: input.tokenHash,
180
+ consumedAt: completedAt,
181
+ })
182
+ return {
183
+ error: new AuthStorageError(
184
+ "user_creation_not_allowed",
185
+ `[Sixb] Magic link '${input.magicLinkId}' cannot create a user for project '${projectId}'.`
186
+ ),
187
+ }
188
+ }
189
+
190
+ if (
191
+ shouldCreateUser &&
192
+ !activeInvitation &&
193
+ input.allowUserCreationWithoutInvitation &&
194
+ input.requireNoActiveUsersForUserCreation &&
195
+ (await hasActiveUsers(tx, projectId))
196
+ ) {
197
+ await consumeMagicLink(tx, {
198
+ projectId,
199
+ id: input.magicLinkId,
200
+ tokenHash: input.tokenHash,
201
+ consumedAt: completedAt,
202
+ })
203
+ return {
204
+ error: new AuthStorageError(
205
+ "user_creation_not_allowed",
206
+ `[Sixb] Magic link '${input.magicLinkId}' cannot create a user for project '${projectId}'.`
207
+ ),
208
+ }
209
+ }
210
+
211
+ await validateCompleteSessionInput(tx, projectId, input.session)
212
+ assertSignInSessionAudience(projectId, input.session.audience, magicLink.audience)
213
+
214
+ let newUserId: string | undefined
215
+ if (shouldCreateUser) {
216
+ newUserId = assertNonEmpty(input.newUserId, "User id")
217
+ if (await getUserRowById(tx, { projectId, id: newUserId })) {
218
+ throw new AuthStorageError(
219
+ "duplicate_user",
220
+ `[Sixb] User '${newUserId}' already exists for project '${projectId}'.`
221
+ )
222
+ }
223
+ }
224
+
225
+ const sessionUserId = existingUserRow?.id ?? newUserId ?? input.newUserId
226
+ await lockAdvisoryKeys(tx, [authLockKey("sessions", projectId, sessionUserId)])
227
+
228
+ await consumeMagicLink(tx, {
229
+ projectId,
230
+ id: input.magicLinkId,
231
+ tokenHash: input.tokenHash,
232
+ consumedAt: completedAt,
233
+ })
234
+
235
+ const user = existingUserRow
236
+ ? rowToUserRecord(existingUserRow)
237
+ : await this.insertUser(tx, {
238
+ id: newUserId ?? assertNonEmpty(input.newUserId, "User id"),
239
+ projectId,
240
+ email,
241
+ displayName: input.newUserDisplayName,
242
+ avatarUrl: input.newUserAvatarUrl,
243
+ createdAt: completedAt,
244
+ })
245
+
246
+ const invitation = await this.acceptInvitationAndApplyGroups(tx, {
247
+ activeInvitation,
248
+ completedAt,
249
+ projectId,
250
+ user,
251
+ })
252
+ const groupMemberships = await this.applyManualGroups(tx, {
253
+ completedAt,
254
+ existing: invitation.groupMemberships,
255
+ groupIds: manualGroupIds,
256
+ projectId,
257
+ userId: user.id,
258
+ })
259
+ const session = await this.createSignInSession(tx, {
260
+ projectId,
261
+ strategyId: magicLink.strategy_id,
262
+ session: input.session,
263
+ userId: user.id,
264
+ })
265
+
266
+ return {
267
+ user,
268
+ session,
269
+ invitation: invitation.invitation,
270
+ groupMemberships,
271
+ }
272
+ }
273
+ )
274
+
275
+ if ("error" in result) {
276
+ throw result.error
277
+ }
278
+
279
+ return result
280
+ }
281
+
282
+ async completeOidcSignIn(input: CompleteOidcSignInInput): Promise<CompleteSignInResult> {
283
+ const result = await runPgTransaction(
284
+ this.sql,
285
+ async (tx): Promise<CompleteSignInResult | AuthTransactionError> => {
286
+ const projectId = assertNonEmpty(input.projectId, "Project id")
287
+ const completedAt = new Date(input.completedAt)
288
+ const attempt = await getOidcAttemptRowById(
289
+ tx,
290
+ {
291
+ projectId,
292
+ id: input.oidcAuthorizationAttemptId,
293
+ },
294
+ { forUpdate: true }
295
+ )
296
+
297
+ this.assertOidcAttemptUsable(
298
+ projectId,
299
+ input.oidcAuthorizationAttemptId,
300
+ input.stateHash,
301
+ completedAt,
302
+ attempt
303
+ )
304
+
305
+ const subject = assertNonEmpty(input.subject, "Subject")
306
+ const email = normalizeEmail(input.email)
307
+ await lockAdvisoryKeys(tx, [
308
+ authLockKey("bootstrap", projectId),
309
+ authLockKey("invitations", projectId, email),
310
+ authLockKey("identities", projectId, attempt.strategy_id, subject),
311
+ authLockKey("users", projectId, email),
312
+ ])
313
+
314
+ const identity = await getIdentityRowBySubject(
315
+ tx,
316
+ {
317
+ projectId,
318
+ strategyId: attempt.strategy_id,
319
+ subject,
320
+ },
321
+ { forUpdate: true }
322
+ )
323
+ const activeInvitation = identity
324
+ ? null
325
+ : await getActiveInvitationByEmail(
326
+ tx,
327
+ {
328
+ projectId,
329
+ email,
330
+ now: completedAt,
331
+ },
332
+ { forUpdate: true }
333
+ )
334
+ let userRow = identity
335
+ ? await getUserRowById(tx, { projectId, id: identity.user_id }, { forUpdate: true })
336
+ : await getUserRowByEmail(tx, { projectId, email }, { forUpdate: true })
337
+ const shouldCreateUser = !identity && !userRow
338
+ const manualGroupIds = normalizeGroupIds(input.manualGroupIds)
339
+
340
+ if (identity && !userRow) {
341
+ await this.consumeOidcAttempt(input, completedAt, projectId, tx)
342
+ return {
343
+ error: new AuthStorageError(
344
+ "missing_user",
345
+ `[Sixb] User '${identity.user_id}' not found for linked OIDC identity.`
346
+ ),
347
+ }
348
+ }
349
+
350
+ if (!identity && userRow && (!input.autoLinkByVerifiedEmail || !input.emailVerified)) {
351
+ await this.consumeOidcAttempt(input, completedAt, projectId, tx)
352
+ return {
353
+ error: new AuthStorageError(
354
+ "email_link_not_allowed",
355
+ `[Sixb] OIDC identity cannot auto-link to user '${userRow.id}' for project '${projectId}'.`
356
+ ),
357
+ }
358
+ }
359
+
360
+ if (userRow?.status === "suspended") {
361
+ await this.consumeOidcAttempt(input, completedAt, projectId, tx)
362
+ return {
363
+ error: new AuthStorageError(
364
+ "suspended_user",
365
+ `[Sixb] User '${userRow.id}' is suspended for project '${projectId}'.`
366
+ ),
367
+ }
368
+ }
369
+
370
+ if (shouldCreateUser && !input.emailVerified) {
371
+ await this.consumeOidcAttempt(input, completedAt, projectId, tx)
372
+ return {
373
+ error: new AuthStorageError(
374
+ "user_creation_not_allowed",
375
+ `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`
376
+ ),
377
+ }
378
+ }
379
+
380
+ if (shouldCreateUser && !activeInvitation && !input.allowUserCreationWithoutInvitation) {
381
+ await this.consumeOidcAttempt(input, completedAt, projectId, tx)
382
+ return {
383
+ error: new AuthStorageError(
384
+ "user_creation_not_allowed",
385
+ `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`
386
+ ),
387
+ }
388
+ }
389
+
390
+ if (
391
+ shouldCreateUser &&
392
+ !activeInvitation &&
393
+ input.allowUserCreationWithoutInvitation &&
394
+ input.requireNoActiveUsersForUserCreation &&
395
+ (await hasActiveUsers(tx, projectId))
396
+ ) {
397
+ await this.consumeOidcAttempt(input, completedAt, projectId, tx)
398
+ return {
399
+ error: new AuthStorageError(
400
+ "user_creation_not_allowed",
401
+ `[Sixb] OIDC authorization attempt '${input.oidcAuthorizationAttemptId}' cannot create a user for project '${projectId}'.`
402
+ ),
403
+ }
404
+ }
405
+
406
+ await validateCompleteSessionInput(tx, projectId, input.session)
407
+ assertSignInSessionAudience(projectId, input.session.audience, attempt.audience)
408
+
409
+ let newUserId: string | undefined
410
+ if (shouldCreateUser) {
411
+ newUserId = assertNonEmpty(input.newUserId, "User id")
412
+ if (await getUserRowById(tx, { projectId, id: newUserId })) {
413
+ throw new AuthStorageError(
414
+ "duplicate_user",
415
+ `[Sixb] User '${newUserId}' already exists for project '${projectId}'.`
416
+ )
417
+ }
418
+ }
419
+
420
+ await lockAdvisoryKeys(tx, [
421
+ authLockKey("sessions", projectId, userRow?.id ?? newUserId ?? input.newUserId),
422
+ ])
423
+ await this.consumeOidcAttempt(input, completedAt, projectId, tx)
424
+
425
+ let user: UserRecord
426
+ if (!userRow) {
427
+ user = await this.insertUser(tx, {
428
+ id: newUserId ?? assertNonEmpty(input.newUserId, "User id"),
429
+ projectId,
430
+ email,
431
+ displayName: input.displayName,
432
+ avatarUrl: input.avatarUrl,
433
+ createdAt: completedAt,
434
+ })
435
+ userRow = {
436
+ project_id: user.projectId,
437
+ id: user.id,
438
+ email: user.email,
439
+ display_name: user.displayName ?? null,
440
+ avatar_url: user.avatarUrl ?? null,
441
+ status: user.status,
442
+ created_at: user.createdAt,
443
+ updated_at: user.updatedAt,
444
+ }
445
+ } else {
446
+ user = rowToUserRecord(userRow)
447
+ }
448
+
449
+ const invitation = await this.acceptInvitationAndApplyGroups(tx, {
450
+ activeInvitation,
451
+ completedAt,
452
+ projectId,
453
+ user,
454
+ })
455
+ const groupMemberships = await this.applyManualGroups(tx, {
456
+ completedAt,
457
+ existing: invitation.groupMemberships,
458
+ groupIds: manualGroupIds,
459
+ projectId,
460
+ userId: user.id,
461
+ })
462
+ const nextIdentity = await this.upsertIdentity(tx, {
463
+ projectId,
464
+ strategyId: attempt.strategy_id,
465
+ subject,
466
+ userId: user.id,
467
+ claims: input.claims,
468
+ createdAt: identity ? new Date(identity.created_at) : completedAt,
469
+ updatedAt: completedAt,
470
+ })
471
+ const session = await this.createSignInSession(tx, {
472
+ projectId,
473
+ strategyId: attempt.strategy_id,
474
+ session: input.session,
475
+ userId: user.id,
476
+ })
477
+
478
+ return {
479
+ user,
480
+ session,
481
+ identity: nextIdentity,
482
+ invitation: invitation.invitation,
483
+ groupMemberships,
484
+ }
485
+ }
486
+ )
487
+
488
+ if ("error" in result) {
489
+ throw result.error
490
+ }
491
+
492
+ return result
493
+ }
494
+
495
+ async suspendUserAndRevokeSessions(
496
+ input: SuspendUserAndRevokeSessionsInput
497
+ ): Promise<UserRecord> {
498
+ return runPgTransaction(this.sql, async (tx) => {
499
+ await lockAdvisoryKeys(tx, [authLockKey("sessions", input.projectId, input.userId)])
500
+ const existing = await getUserRowById(
501
+ tx,
502
+ {
503
+ projectId: input.projectId,
504
+ id: input.userId,
505
+ },
506
+ { forUpdate: true }
507
+ )
508
+
509
+ if (!existing) {
510
+ throw new AuthStorageError(
511
+ "missing_user",
512
+ `[Sixb] User '${input.userId}' not found for project '${input.projectId}'.`
513
+ )
514
+ }
515
+
516
+ const [updated] = await tx<PgAuthUserRow[]>`
517
+ UPDATE auth_users
518
+ SET status = 'suspended',
519
+ updated_at = ${input.suspendedAt}
520
+ WHERE project_id = ${input.projectId}
521
+ AND id = ${input.userId}
522
+ RETURNING *
523
+ `
524
+
525
+ await revokeActiveSessionsForUser(tx, {
526
+ projectId: input.projectId,
527
+ userId: input.userId,
528
+ revokedAt: input.suspendedAt,
529
+ })
530
+
531
+ return rowToUserRecord(updated)
532
+ })
533
+ }
534
+
535
+ private assertMagicLinkUsable(
536
+ projectId: string,
537
+ id: string,
538
+ tokenHash: string,
539
+ completedAt: Date,
540
+ row: PgAuthMagicLinkRow | null
541
+ ): asserts row is PgAuthMagicLinkRow {
542
+ if (!row) {
543
+ throw new AuthStorageError(
544
+ "missing_magic_link",
545
+ `[Sixb] Magic link '${id}' not found for project '${projectId}'.`
546
+ )
547
+ }
548
+
549
+ assertMagicLinkUsable(projectId, id, tokenHash, completedAt, row)
550
+ }
551
+
552
+ private assertOidcAttemptUsable(
553
+ projectId: string,
554
+ id: string,
555
+ stateHash: string,
556
+ completedAt: Date,
557
+ row: PgAuthOidcAttemptRow | null
558
+ ): asserts row is PgAuthOidcAttemptRow {
559
+ if (!row) {
560
+ throw new AuthStorageError(
561
+ "missing_oidc_attempt",
562
+ `[Sixb] OIDC authorization attempt '${id}' not found for project '${projectId}'.`
563
+ )
564
+ }
565
+
566
+ assertOidcAttemptUsable(projectId, id, stateHash, completedAt, row)
567
+ }
568
+
569
+ private async insertUser(
570
+ sql: SQLClient,
571
+ input: {
572
+ readonly id: string
573
+ readonly projectId: string
574
+ readonly email: string
575
+ readonly displayName?: string
576
+ readonly avatarUrl?: string
577
+ readonly createdAt: Date
578
+ }
579
+ ): Promise<UserRecord> {
580
+ try {
581
+ const [row] = await sql<PgAuthUserRow[]>`
582
+ INSERT INTO auth_users (
583
+ project_id,
584
+ id,
585
+ email,
586
+ display_name,
587
+ avatar_url,
588
+ status,
589
+ created_at,
590
+ updated_at
591
+ ) VALUES (
592
+ ${input.projectId},
593
+ ${input.id},
594
+ ${input.email},
595
+ ${input.displayName ?? null},
596
+ ${input.avatarUrl ?? null},
597
+ ${"active"},
598
+ ${input.createdAt},
599
+ ${input.createdAt}
600
+ )
601
+ RETURNING *
602
+ `
603
+
604
+ return rowToUserRecord(row)
605
+ } catch (error) {
606
+ mapUniqueConstraintError(
607
+ error,
608
+ "duplicate_user",
609
+ `[Sixb] User '${input.id}' already exists for project '${input.projectId}'.`
610
+ )
611
+ }
612
+ }
613
+
614
+ private async acceptInvitationAndApplyGroups(
615
+ sql: SQLClient,
616
+ input: {
617
+ readonly activeInvitation: InvitationRecord | null
618
+ readonly completedAt: Date
619
+ readonly projectId: string
620
+ readonly user: UserRecord
621
+ }
622
+ ): Promise<{
623
+ readonly invitation?: InvitationRecord
624
+ readonly groupMemberships: readonly GroupMembershipRecord[]
625
+ }> {
626
+ if (!input.activeInvitation) {
627
+ return { groupMemberships: [] }
628
+ }
629
+
630
+ await sql`
631
+ UPDATE auth_invitations
632
+ SET status = 'accepted',
633
+ accepted_at = ${input.completedAt},
634
+ updated_at = ${input.completedAt}
635
+ WHERE project_id = ${input.projectId}
636
+ AND id = ${input.activeInvitation.id}
637
+ `
638
+
639
+ const invitation: InvitationRecord = {
640
+ ...input.activeInvitation,
641
+ status: "accepted",
642
+ acceptedAt: new Date(input.completedAt),
643
+ updatedAt: new Date(input.completedAt),
644
+ }
645
+ const groupMemberships: GroupMembershipRecord[] = []
646
+ for (const groupId of input.activeInvitation.groupIds) {
647
+ groupMemberships.push(
648
+ await upsertGroupMembership(sql, {
649
+ projectId: input.projectId,
650
+ userId: input.user.id,
651
+ groupId,
652
+ source: "invitation",
653
+ createdAt: input.completedAt,
654
+ })
655
+ )
656
+ }
657
+
658
+ return { invitation, groupMemberships }
659
+ }
660
+
661
+ private async applyManualGroups(
662
+ sql: SQLClient,
663
+ input: {
664
+ readonly completedAt: Date
665
+ readonly existing: readonly GroupMembershipRecord[]
666
+ readonly groupIds: readonly string[]
667
+ readonly projectId: string
668
+ readonly userId: string
669
+ }
670
+ ): Promise<readonly GroupMembershipRecord[]> {
671
+ const groupMemberships = [...input.existing]
672
+
673
+ for (const groupId of input.groupIds) {
674
+ groupMemberships.push(
675
+ await upsertGroupMembership(sql, {
676
+ projectId: input.projectId,
677
+ userId: input.userId,
678
+ groupId,
679
+ source: "manual",
680
+ createdAt: input.completedAt,
681
+ })
682
+ )
683
+ }
684
+
685
+ return groupMemberships
686
+ }
687
+
688
+ private async createSignInSession(
689
+ sql: SQLClient,
690
+ input: {
691
+ readonly projectId: string
692
+ readonly strategyId: string
693
+ readonly session: CompleteMagicLinkSignInInput["session"]
694
+ readonly userId: string
695
+ }
696
+ ): Promise<SessionRecord> {
697
+ await lockAdvisoryKeys(sql, [
698
+ authLockKey("sessions", input.projectId, input.userId),
699
+ authLockKey("sessions", input.projectId, input.userId, input.session.audience),
700
+ ])
701
+
702
+ return createSession(sql, {
703
+ id: input.session.id,
704
+ projectId: input.projectId,
705
+ userId: input.userId,
706
+ strategyId: input.strategyId,
707
+ audience: input.session.audience,
708
+ tokenHash: input.session.tokenHash,
709
+ createdAt: input.session.createdAt,
710
+ expiresAt: input.session.expiresAt,
711
+ absoluteExpiresAt: input.session.absoluteExpiresAt,
712
+ userAgent: input.session.userAgent,
713
+ ipAddress: input.session.ipAddress,
714
+ })
715
+ }
716
+
717
+ private async consumeOidcAttempt(
718
+ input: CompleteOidcSignInInput,
719
+ completedAt: Date,
720
+ projectId: string,
721
+ sql: SQLClient
722
+ ): Promise<void> {
723
+ await consumeOidcAttempt(sql, {
724
+ projectId,
725
+ id: input.oidcAuthorizationAttemptId,
726
+ stateHash: input.stateHash,
727
+ consumedAt: completedAt,
728
+ })
729
+ }
730
+
731
+ private async upsertIdentity(
732
+ sql: SQLClient,
733
+ input: {
734
+ readonly projectId: string
735
+ readonly strategyId: string
736
+ readonly subject: string
737
+ readonly userId: string
738
+ readonly claims?: Readonly<Record<string, unknown>>
739
+ readonly createdAt: Date
740
+ readonly updatedAt: Date
741
+ }
742
+ ): Promise<UserIdentityRecord> {
743
+ const [row] = await sql<PgAuthUserIdentityRow[]>`
744
+ INSERT INTO auth_user_identities (
745
+ project_id,
746
+ strategy_id,
747
+ subject,
748
+ user_id,
749
+ claims,
750
+ created_at,
751
+ updated_at
752
+ ) VALUES (
753
+ ${input.projectId},
754
+ ${input.strategyId},
755
+ ${input.subject},
756
+ ${input.userId},
757
+ ${serializeOptionalRecord(input.claims)}::text::jsonb,
758
+ ${input.createdAt},
759
+ ${input.updatedAt}
760
+ )
761
+ ON CONFLICT (project_id, strategy_id, subject)
762
+ DO UPDATE SET
763
+ user_id = excluded.user_id,
764
+ claims = excluded.claims,
765
+ updated_at = excluded.updated_at
766
+ RETURNING *
767
+ `
768
+
769
+ return rowToIdentityRecord(row)
770
+ }
771
+ }
772
+
773
+ async function hasActiveUsers(sql: SQLClient, projectId: string): Promise<boolean> {
774
+ const rows = (await sql`
775
+ SELECT 1 AS active
776
+ FROM auth_users
777
+ WHERE project_id = ${projectId}
778
+ AND status = 'active'
779
+ LIMIT 1
780
+ `) as Array<{ readonly active: number }>
781
+
782
+ return rows.length > 0
783
+ }
784
+
785
+ function assertSignInSessionAudience(
786
+ projectId: string,
787
+ sessionAudience: string,
788
+ storedAudience: string
789
+ ): void {
790
+ if (sessionAudience === storedAudience) {
791
+ return
792
+ }
793
+
794
+ throw new AuthStorageError(
795
+ "invalid_input",
796
+ `[Sixb] Sign-in session audience '${sessionAudience}' does not match stored auth audience '${storedAudience}' for project '${projectId}'.`
797
+ )
798
+ }