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