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