@sixb/sqlite 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/action-run-storage.d.ts +4 -0
  2. package/dist/action-run-storage.d.ts.map +1 -1
  3. package/dist/activity-storage.d.ts +20 -0
  4. package/dist/activity-storage.d.ts.map +1 -0
  5. package/dist/agents/agent-storage.d.ts.map +1 -1
  6. package/dist/agents/threads.d.ts +4 -0
  7. package/dist/agents/threads.d.ts.map +1 -1
  8. package/dist/ai-limit-storage.d.ts.map +1 -1
  9. package/dist/ai-usage-storage.d.ts.map +1 -1
  10. package/dist/artifact-storage.d.ts +22 -0
  11. package/dist/artifact-storage.d.ts.map +1 -0
  12. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  13. package/dist/auth-storage/magic-links.d.ts.map +1 -1
  14. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -1
  15. package/dist/auth-storage/rows.d.ts +0 -3
  16. package/dist/auth-storage/rows.d.ts.map +1 -1
  17. package/dist/auth-storage/sessions.d.ts +2 -11
  18. package/dist/auth-storage/sessions.d.ts.map +1 -1
  19. package/dist/auth-storage/shared.d.ts +0 -2
  20. package/dist/auth-storage/shared.d.ts.map +1 -1
  21. package/dist/execution-storage.d.ts +2 -1
  22. package/dist/execution-storage.d.ts.map +1 -1
  23. package/dist/index.d.ts +4 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +933 -128
  26. package/dist/index.js.map +32 -30
  27. package/dist/migrations.d.ts +2 -0
  28. package/dist/migrations.d.ts.map +1 -1
  29. package/dist/objects/storage.d.ts.map +1 -1
  30. package/dist/ontology-storage/sources.d.ts +2 -1
  31. package/dist/ontology-storage/sources.d.ts.map +1 -1
  32. package/dist/pipeline-run-storage.d.ts.map +1 -1
  33. package/dist/projection-run-storage.d.ts.map +1 -1
  34. package/dist/rules-storage.d.ts.map +1 -1
  35. package/dist/share-grant-storage.d.ts +2 -1
  36. package/dist/share-grant-storage.d.ts.map +1 -1
  37. package/dist/share-session-storage.d.ts.map +1 -1
  38. package/dist/sync-run-storage.d.ts.map +1 -1
  39. package/dist/timeseries-storage.d.ts.map +1 -1
  40. package/dist/transactions.d.ts +0 -1
  41. package/dist/transactions.d.ts.map +1 -1
  42. package/dist/webhook-run-storage.d.ts.map +1 -1
  43. package/dist/workflow-intervention-storage.d.ts.map +1 -1
  44. package/dist/workflow-run-storage.d.ts +4 -0
  45. package/dist/workflow-run-storage.d.ts.map +1 -1
  46. package/package.json +3 -3
  47. package/src/action-run-storage.ts +27 -5
  48. package/src/activity-storage.ts +588 -0
  49. package/src/agents/agent-storage.ts +0 -5
  50. package/src/agents/threads.ts +27 -0
  51. package/src/ai-limit-storage.ts +0 -2
  52. package/src/ai-usage-storage.ts +0 -4
  53. package/src/artifact-storage.ts +356 -0
  54. package/src/auth-storage/auth-storage.ts +0 -23
  55. package/src/auth-storage/magic-links.ts +1 -6
  56. package/src/auth-storage/oidc-attempts.ts +1 -6
  57. package/src/auth-storage/rows.ts +0 -6
  58. package/src/auth-storage/sessions.ts +21 -42
  59. package/src/auth-storage/shared.ts +3 -20
  60. package/src/execution-storage.ts +56 -0
  61. package/src/index.ts +11 -10
  62. package/src/migrations/061-drop-auth-audience.sql +16 -0
  63. package/src/migrations/062-artifacts.sql +71 -0
  64. package/src/migrations/063-drop-output-diagnostics.sql +19 -0
  65. package/src/migrations/064-activity.sql +61 -0
  66. package/src/migrations/065-activity-timeline.sql +19 -0
  67. package/src/migrations/066-activity-interactions.sql +20 -0
  68. package/src/migrations.ts +36 -0
  69. package/src/objects/storage.ts +0 -5
  70. package/src/ontology-storage/sources.ts +41 -0
  71. package/src/pipeline-run-storage.ts +0 -5
  72. package/src/projection-run-storage.ts +0 -5
  73. package/src/rules-storage.ts +0 -5
  74. package/src/share-grant-storage.ts +25 -2
  75. package/src/share-session-storage.ts +0 -2
  76. package/src/sync-run-storage.ts +0 -5
  77. package/src/timeseries-storage.ts +0 -5
  78. package/src/transactions.ts +8 -8
  79. package/src/webhook-run-storage.ts +0 -3
  80. package/src/workflow-intervention-storage.ts +0 -5
  81. package/src/workflow-run-storage.ts +27 -5
@@ -0,0 +1,356 @@
1
+ import type { Database, SQLQueryBindings } from "bun:sqlite"
2
+ import {
3
+ artifactStorageErrors,
4
+ parseArtifactFrameGrantRow,
5
+ parseArtifactRow,
6
+ parseArtifactVersionRow,
7
+ } from "@sixb/core/internal/storage"
8
+ import type {
9
+ ArtifactFrameGrantRecord,
10
+ ArtifactRecord,
11
+ ArtifactStorage,
12
+ ArtifactVersionInput,
13
+ ArtifactVersionRecord,
14
+ CreateArtifactInput,
15
+ DeleteArtifactInput,
16
+ DeleteArtifactVersionsByOriginInput,
17
+ DeleteExpiredArtifactFrameGrantsInput,
18
+ FindArtifactFrameGrantInput,
19
+ GetArtifactInput,
20
+ GetArtifactVersionInput,
21
+ ListArtifactsInput,
22
+ ListArtifactsResult,
23
+ ListArtifactVersionsInput,
24
+ ListArtifactVersionsResult,
25
+ ListReferencingVersionsInput,
26
+ PublishArtifactVersionInput,
27
+ PublishedArtifactVersion,
28
+ } from "@sixb/core/storage"
29
+ import { runImmediateTransaction, type SqliteStoreConnection } from "./transactions"
30
+
31
+ const ARTIFACT_COLUMNS = `
32
+ project_id AS projectId, id, kind, title,
33
+ owner_principal_type AS ownerPrincipalType, owner_principal_id AS ownerPrincipalId,
34
+ latest_version AS latestVersion, created_at AS createdAt, updated_at AS updatedAt
35
+ `
36
+
37
+ const VERSION_COLUMNS = `
38
+ project_id AS projectId, artifact_id AS artifactId, version, title, entry, files, outline, refs,
39
+ author_principal_type AS authorPrincipalType, author_principal_id AS authorPrincipalId,
40
+ origin_thread_id AS originThreadId, origin_run_id AS originRunId, created_at AS createdAt
41
+ `
42
+
43
+ const FRAME_GRANT_COLUMNS = `
44
+ project_id AS projectId, token_hash AS tokenHash, artifact_id AS artifactId, version, targets,
45
+ created_at AS createdAt, expires_at AS expiresAt
46
+ `
47
+
48
+ type Row = Record<string, unknown>
49
+
50
+ export class SqliteArtifactStorage implements ArtifactStorage {
51
+ private readonly db: Database
52
+
53
+ constructor(connection: SqliteStoreConnection) {
54
+ this.db = connection.db
55
+ }
56
+
57
+ async create(input: CreateArtifactInput): Promise<PublishedArtifactVersion> {
58
+ return runImmediateTransaction(this.db, () => {
59
+ const createdAt = input.version.createdAt.toISOString()
60
+ const artifact = this.db
61
+ .query(
62
+ `INSERT INTO artifacts (
63
+ project_id, id, kind, title, owner_principal_type, owner_principal_id,
64
+ latest_version, next_version, created_at, updated_at
65
+ ) VALUES (?, ?, ?, ?, ?, ?, 1, 2, ?, ?)
66
+ ON CONFLICT (project_id, id) DO NOTHING
67
+ RETURNING ${ARTIFACT_COLUMNS}`
68
+ )
69
+ .get(
70
+ input.projectId,
71
+ input.id,
72
+ input.kind,
73
+ input.version.title,
74
+ input.owner.type,
75
+ input.owner.id,
76
+ createdAt,
77
+ createdAt
78
+ ) as Row | null
79
+ if (!artifact) throw artifactStorageErrors.exists(input.id)
80
+ return {
81
+ artifact: parseArtifactRow(artifact),
82
+ version: this.insertVersion(input.projectId, input.id, 1, input.version),
83
+ }
84
+ })
85
+ }
86
+
87
+ async publish(input: PublishArtifactVersionInput): Promise<PublishedArtifactVersion> {
88
+ return runImmediateTransaction(this.db, () => {
89
+ const current = this.db
90
+ .query(
91
+ "SELECT kind, next_version AS nextVersion FROM artifacts WHERE project_id = ? AND id = ?"
92
+ )
93
+ .get(input.projectId, input.artifactId) as {
94
+ readonly kind: ArtifactRecord["kind"]
95
+ readonly nextVersion: number
96
+ } | null
97
+ if (!current) throw artifactStorageErrors.notFound(input.artifactId)
98
+ if (current.kind !== input.kind) {
99
+ throw artifactStorageErrors.kindMismatch(input.artifactId, current.kind, input.kind)
100
+ }
101
+ const version = this.insertVersion(
102
+ input.projectId,
103
+ input.artifactId,
104
+ current.nextVersion,
105
+ input.version
106
+ )
107
+ const artifact = this.db
108
+ .query(
109
+ `UPDATE artifacts
110
+ SET title = ?, latest_version = ?, next_version = ?, updated_at = ?
111
+ WHERE project_id = ? AND id = ?
112
+ RETURNING ${ARTIFACT_COLUMNS}`
113
+ )
114
+ .get(
115
+ input.version.title,
116
+ version.version,
117
+ version.version + 1,
118
+ input.version.createdAt.toISOString(),
119
+ input.projectId,
120
+ input.artifactId
121
+ ) as Row
122
+ return { artifact: parseArtifactRow(artifact), version }
123
+ })
124
+ }
125
+
126
+ async get(input: GetArtifactInput): Promise<ArtifactRecord | null> {
127
+ const row = this.db
128
+ .query(`SELECT ${ARTIFACT_COLUMNS} FROM artifacts WHERE project_id = ? AND id = ?`)
129
+ .get(input.projectId, input.id) as Row | null
130
+ return row ? parseArtifactRow(row) : null
131
+ }
132
+
133
+ async list(input: ListArtifactsInput): Promise<ListArtifactsResult> {
134
+ const where = ["a.project_id = ?"]
135
+ const params: SQLQueryBindings[] = [input.projectId]
136
+ if (input.owner) {
137
+ where.push("a.owner_principal_type = ? AND a.owner_principal_id = ?")
138
+ params.push(input.owner.type, input.owner.id)
139
+ }
140
+ if (input.originThreadId !== undefined) {
141
+ where.push(
142
+ `EXISTS (
143
+ SELECT 1 FROM artifact_versions v
144
+ WHERE v.project_id = a.project_id AND v.artifact_id = a.id AND v.origin_thread_id = ?
145
+ )`
146
+ )
147
+ params.push(input.originThreadId)
148
+ }
149
+ if (input.kind !== undefined) {
150
+ where.push("a.kind = ?")
151
+ params.push(input.kind)
152
+ }
153
+ const clause = where.join(" AND ")
154
+ const { total } = this.db
155
+ .query(`SELECT COUNT(*) AS total FROM artifacts a WHERE ${clause}`)
156
+ .get(...params) as { readonly total: number }
157
+ const offset = input.offset ?? 0
158
+ const rows = this.db
159
+ .query(
160
+ `SELECT ${ARTIFACT_COLUMNS} FROM artifacts a WHERE ${clause}
161
+ ORDER BY a.updated_at DESC, a.id ASC LIMIT ? OFFSET ?`
162
+ )
163
+ .all(...params, input.limit ?? -1, offset) as Row[]
164
+ return {
165
+ artifacts: rows.map(parseArtifactRow),
166
+ hasMore: offset + rows.length < total,
167
+ total,
168
+ }
169
+ }
170
+
171
+ async getVersion(input: GetArtifactVersionInput): Promise<ArtifactVersionRecord | null> {
172
+ const row = this.db
173
+ .query(
174
+ `SELECT ${VERSION_COLUMNS} FROM artifact_versions
175
+ WHERE project_id = ? AND artifact_id = ? AND version = ?`
176
+ )
177
+ .get(input.projectId, input.artifactId, input.version) as Row | null
178
+ return row ? parseArtifactVersionRow(row) : null
179
+ }
180
+
181
+ async listVersions(input: ListArtifactVersionsInput): Promise<ListArtifactVersionsResult> {
182
+ const { total } = this.db
183
+ .query(
184
+ "SELECT COUNT(*) AS total FROM artifact_versions WHERE project_id = ? AND artifact_id = ?"
185
+ )
186
+ .get(input.projectId, input.artifactId) as { readonly total: number }
187
+ const offset = input.offset ?? 0
188
+ const rows = this.db
189
+ .query(
190
+ `SELECT ${VERSION_COLUMNS} FROM artifact_versions
191
+ WHERE project_id = ? AND artifact_id = ?
192
+ ORDER BY version DESC LIMIT ? OFFSET ?`
193
+ )
194
+ .all(input.projectId, input.artifactId, input.limit ?? -1, offset) as Row[]
195
+ return {
196
+ versions: rows.map(parseArtifactVersionRow),
197
+ hasMore: offset + rows.length < total,
198
+ total,
199
+ }
200
+ }
201
+
202
+ async listReferencingVersions(
203
+ input: ListReferencingVersionsInput
204
+ ): Promise<ArtifactVersionRecord[]> {
205
+ // An address names the artifact when the id is followed by nothing, a version, a file, or a
206
+ // part.
207
+ const prefix = `artifact:${input.artifactId}`
208
+ const rows = this.db
209
+ .query(
210
+ `SELECT ${VERSION_COLUMNS} FROM artifact_versions v
211
+ WHERE (project_id, artifact_id, version) IN (
212
+ SELECT project_id, id, latest_version FROM artifacts WHERE project_id = ?
213
+ ) AND EXISTS (
214
+ SELECT 1 FROM json_each(v.refs) r
215
+ WHERE substr(json_extract(r.value, '$.address'), 1, ?) = ?
216
+ AND substr(json_extract(r.value, '$.address'), ? + 1, 1) IN ('', '@', '/', '#')
217
+ )
218
+ ORDER BY artifact_id`
219
+ )
220
+ .all(input.projectId, prefix.length, prefix, prefix.length) as Row[]
221
+ return rows.map(parseArtifactVersionRow)
222
+ }
223
+
224
+ async createFrameGrant(record: ArtifactFrameGrantRecord): Promise<void> {
225
+ runImmediateTransaction(this.db, () => {
226
+ const exists = this.db
227
+ .query("SELECT 1 FROM artifacts WHERE project_id = ? AND id = ?")
228
+ .get(record.projectId, record.artifactId)
229
+ if (!exists) throw artifactStorageErrors.notFound(record.artifactId)
230
+ this.db
231
+ .query(
232
+ `INSERT INTO artifact_frame_grants
233
+ (project_id, token_hash, artifact_id, version, targets, created_at, expires_at)
234
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
235
+ )
236
+ .run(
237
+ record.projectId,
238
+ record.tokenHash,
239
+ record.artifactId,
240
+ record.version,
241
+ JSON.stringify(record.targets),
242
+ record.createdAt.toISOString(),
243
+ record.expiresAt.toISOString()
244
+ )
245
+ })
246
+ }
247
+
248
+ async findFrameGrant(
249
+ input: FindArtifactFrameGrantInput
250
+ ): Promise<ArtifactFrameGrantRecord | null> {
251
+ const row = this.db
252
+ .query(
253
+ `SELECT ${FRAME_GRANT_COLUMNS} FROM artifact_frame_grants
254
+ WHERE project_id = ? AND token_hash = ?`
255
+ )
256
+ .get(input.projectId, input.tokenHash) as Row | null
257
+ return row ? parseArtifactFrameGrantRow(row) : null
258
+ }
259
+
260
+ async deleteExpiredFrameGrants(input: DeleteExpiredArtifactFrameGrantsInput): Promise<number> {
261
+ return this.db
262
+ .query(
263
+ `DELETE FROM artifact_frame_grants WHERE rowid IN (
264
+ SELECT rowid FROM artifact_frame_grants
265
+ WHERE project_id = ? AND expires_at < ?
266
+ ORDER BY expires_at LIMIT ?
267
+ )`
268
+ )
269
+ .run(input.projectId, input.expiredBefore.toISOString(), input.limit).changes
270
+ }
271
+
272
+ async delete(input: DeleteArtifactInput): Promise<boolean> {
273
+ // Versions and frame grants go with the artifact through ON DELETE CASCADE.
274
+ return (
275
+ this.db
276
+ .query("DELETE FROM artifacts WHERE project_id = ? AND id = ?")
277
+ .run(input.projectId, input.id).changes > 0
278
+ )
279
+ }
280
+
281
+ async deleteVersionsByOrigin(input: DeleteArtifactVersionsByOriginInput): Promise<number> {
282
+ return runImmediateTransaction(this.db, () => {
283
+ const affected = this.db
284
+ .query(
285
+ `SELECT DISTINCT artifact_id AS artifactId FROM artifact_versions
286
+ WHERE project_id = ? AND origin_run_id = ?`
287
+ )
288
+ .all(input.projectId, input.runId) as { readonly artifactId: string }[]
289
+ const deleted = this.db
290
+ .query("DELETE FROM artifact_versions WHERE project_id = ? AND origin_run_id = ?")
291
+ .run(input.projectId, input.runId).changes
292
+ for (const { artifactId } of affected) {
293
+ this.repointLatest(input.projectId, artifactId)
294
+ }
295
+ return deleted
296
+ })
297
+ }
298
+
299
+ /** Point an artifact at its newest remaining version, or delete it when none remains. */
300
+ private repointLatest(projectId: string, artifactId: string): void {
301
+ const latest = this.db
302
+ .query(
303
+ `SELECT version, title, created_at AS createdAt FROM artifact_versions
304
+ WHERE project_id = ? AND artifact_id = ? ORDER BY version DESC LIMIT 1`
305
+ )
306
+ .get(projectId, artifactId) as {
307
+ readonly version: number
308
+ readonly title: string
309
+ readonly createdAt: string
310
+ } | null
311
+ if (!latest) {
312
+ this.db
313
+ .query("DELETE FROM artifacts WHERE project_id = ? AND id = ?")
314
+ .run(projectId, artifactId)
315
+ return
316
+ }
317
+ this.db
318
+ .query(
319
+ `UPDATE artifacts SET title = ?, latest_version = ?, updated_at = ?
320
+ WHERE project_id = ? AND id = ?`
321
+ )
322
+ .run(latest.title, latest.version, latest.createdAt, projectId, artifactId)
323
+ }
324
+
325
+ private insertVersion(
326
+ projectId: string,
327
+ artifactId: string,
328
+ version: number,
329
+ input: ArtifactVersionInput
330
+ ): ArtifactVersionRecord {
331
+ const row = this.db
332
+ .query(
333
+ `INSERT INTO artifact_versions (
334
+ project_id, artifact_id, version, title, entry, files, outline, refs,
335
+ author_principal_type, author_principal_id, origin_thread_id, origin_run_id, created_at
336
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
337
+ RETURNING ${VERSION_COLUMNS}`
338
+ )
339
+ .get(
340
+ projectId,
341
+ artifactId,
342
+ version,
343
+ input.title,
344
+ input.entry,
345
+ JSON.stringify(input.files),
346
+ JSON.stringify(input.outline),
347
+ JSON.stringify(input.references),
348
+ input.author.type,
349
+ input.author.id,
350
+ input.origin?.threadId ?? null,
351
+ input.origin?.runId ?? null,
352
+ input.createdAt.toISOString()
353
+ ) as Row
354
+ return parseArtifactVersionRow(row)
355
+ }
356
+ }
@@ -16,7 +16,6 @@ import type {
16
16
  UserRecord,
17
17
  } from "@sixb/core/storage"
18
18
  import { AuthStorageError } from "@sixb/core/storage"
19
- import { installFreshSqliteSchema } from "../migrations"
20
19
  import {
21
20
  closeSqliteStoreConnection,
22
21
  openSqliteStoreConnection,
@@ -90,10 +89,6 @@ export class SqliteAuthStorage implements AuthStorage {
90
89
  this.connection = openSqliteStoreConnection(options)
91
90
  this.db = this.connection.db
92
91
 
93
- if (this.connection.installFreshSchema) {
94
- installFreshSqliteSchema(this.db)
95
- }
96
-
97
92
  this.users = new SqliteAuthUserStore(this.db)
98
93
  this.identities = new SqliteAuthUserIdentityStore(this.db)
99
94
  this.serviceAccounts = new SqliteAuthServiceAccountStore(this.db)
@@ -202,7 +197,6 @@ export class SqliteAuthStorage implements AuthStorage {
202
197
  }
203
198
 
204
199
  validateCompleteSessionInput(this.db, projectId, input.session)
205
- assertSignInSessionAudience(projectId, input.session.audience, magicLink.audience)
206
200
 
207
201
  let newUserId: string | undefined
208
202
  if (shouldCreateUser) {
@@ -340,7 +334,6 @@ export class SqliteAuthStorage implements AuthStorage {
340
334
  }
341
335
 
342
336
  validateCompleteSessionInput(this.db, projectId, input.session)
343
- assertSignInSessionAudience(projectId, input.session.audience, attempt.audience)
344
337
 
345
338
  let newUserId: string | undefined
346
339
  if (shouldCreateUser) {
@@ -700,7 +693,6 @@ export class SqliteAuthStorage implements AuthStorage {
700
693
  projectId: input.projectId,
701
694
  userId: input.userId,
702
695
  strategyId: input.strategyId,
703
- audience: input.session.audience,
704
696
  tokenHash: input.session.tokenHash,
705
697
  createdAt: input.session.createdAt,
706
698
  expiresAt: input.session.expiresAt,
@@ -773,21 +765,6 @@ export class SqliteAuthStorage implements AuthStorage {
773
765
  }
774
766
  }
775
767
 
776
- function assertSignInSessionAudience(
777
- projectId: string,
778
- sessionAudience: string,
779
- storedAudience: string
780
- ): void {
781
- if (sessionAudience === storedAudience) {
782
- return
783
- }
784
-
785
- throw new AuthStorageError(
786
- "invalid_input",
787
- `[Sixb] Sign-in session audience '${sessionAudience}' does not match stored auth audience '${storedAudience}' for project '${projectId}'.`
788
- )
789
- }
790
-
791
768
  function assertCompletableDeviceAuthorization(
792
769
  authorization: DeviceAuthorizationRecord,
793
770
  input: CompleteDeviceAuthorizationInput
@@ -1,5 +1,4 @@
1
1
  import type { Database } from "bun:sqlite"
2
- import { resolveAuthSessionAudience } from "@sixb/core"
3
2
  import type {
4
3
  AuthMagicLinkStore,
5
4
  CreateAuthMagicLinkInput,
@@ -28,7 +27,6 @@ export class SqliteAuthMagicLinkStore implements AuthMagicLinkStore {
28
27
  const id = assertNonEmpty(input.id, "Magic link id")
29
28
  const projectId = assertNonEmpty(input.projectId, "Project id")
30
29
  const strategyId = assertNonEmpty(input.strategyId, "Strategy id")
31
- const audience = resolveAuthSessionAudience(input.audience)
32
30
  const email = normalizeEmail(input.email)
33
31
  const tokenHash = assertNonEmpty(input.tokenHash, "Magic link token hash")
34
32
 
@@ -53,20 +51,18 @@ export class SqliteAuthMagicLinkStore implements AuthMagicLinkStore {
53
51
  project_id,
54
52
  id,
55
53
  strategy_id,
56
- audience,
57
54
  email,
58
55
  token_hash,
59
56
  return_to,
60
57
  created_at,
61
58
  expires_at
62
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
59
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
63
60
  `
64
61
  )
65
62
  .run(
66
63
  projectId,
67
64
  id,
68
65
  strategyId,
69
- audience,
70
66
  email,
71
67
  tokenHash,
72
68
  input.returnTo ?? null,
@@ -85,7 +81,6 @@ export class SqliteAuthMagicLinkStore implements AuthMagicLinkStore {
85
81
  id,
86
82
  projectId,
87
83
  strategyId,
88
- audience,
89
84
  email,
90
85
  tokenHash,
91
86
  returnTo: input.returnTo,
@@ -1,5 +1,4 @@
1
1
  import type { Database } from "bun:sqlite"
2
- import { resolveAuthSessionAudience } from "@sixb/core"
3
2
  import type {
4
3
  AuthOidcAuthorizationAttemptStore,
5
4
  CreateOidcAuthorizationAttemptInput,
@@ -26,7 +25,6 @@ export class SqliteAuthOidcAuthorizationAttemptStore implements AuthOidcAuthoriz
26
25
  const id = assertNonEmpty(input.id, "OIDC authorization attempt id")
27
26
  const projectId = assertNonEmpty(input.projectId, "Project id")
28
27
  const strategyId = assertNonEmpty(input.strategyId, "Strategy id")
29
- const audience = resolveAuthSessionAudience(input.audience)
30
28
  const stateHash = assertNonEmpty(input.stateHash, "OIDC state hash")
31
29
  const nonceHash = assertNonEmpty(input.nonceHash, "OIDC nonce hash")
32
30
  const codeVerifier = assertNonEmpty(input.codeVerifier, "OIDC code verifier")
@@ -46,21 +44,19 @@ export class SqliteAuthOidcAuthorizationAttemptStore implements AuthOidcAuthoriz
46
44
  project_id,
47
45
  id,
48
46
  strategy_id,
49
- audience,
50
47
  state_hash,
51
48
  nonce_hash,
52
49
  code_verifier,
53
50
  return_to,
54
51
  created_at,
55
52
  expires_at
56
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
53
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
57
54
  `
58
55
  )
59
56
  .run(
60
57
  projectId,
61
58
  id,
62
59
  strategyId,
63
- audience,
64
60
  stateHash,
65
61
  nonceHash,
66
62
  codeVerifier,
@@ -80,7 +76,6 @@ export class SqliteAuthOidcAuthorizationAttemptStore implements AuthOidcAuthoriz
80
76
  id,
81
77
  projectId,
82
78
  strategyId,
83
- audience,
84
79
  stateHash,
85
80
  nonceHash,
86
81
  codeVerifier,
@@ -64,7 +64,6 @@ export interface SqliteAuthSessionRow {
64
64
  readonly id: string
65
65
  readonly user_id: string
66
66
  readonly strategy_id: string
67
- readonly audience: SessionRecord["audience"]
68
67
  readonly token_hash: string
69
68
  readonly created_at: string
70
69
  readonly expires_at: string
@@ -127,7 +126,6 @@ export interface SqliteAuthMagicLinkRow {
127
126
  readonly project_id: string
128
127
  readonly id: string
129
128
  readonly strategy_id: string
130
- readonly audience: MagicLinkRecord["audience"]
131
129
  readonly email: string
132
130
  readonly token_hash: string
133
131
  readonly return_to: string | null
@@ -141,7 +139,6 @@ export interface SqliteAuthOidcAttemptRow {
141
139
  readonly project_id: string
142
140
  readonly id: string
143
141
  readonly strategy_id: string
144
- readonly audience: OidcAuthorizationAttemptRecord["audience"]
145
142
  readonly state_hash: string
146
143
  readonly nonce_hash: string
147
144
  readonly code_verifier: string
@@ -230,7 +227,6 @@ export function rowToSessionRecord(row: SqliteAuthSessionRow): SessionRecord {
230
227
  projectId: row.project_id,
231
228
  userId: row.user_id,
232
229
  strategyId: row.strategy_id,
233
- audience: row.audience,
234
230
  tokenHash: row.token_hash,
235
231
  createdAt: new Date(row.created_at),
236
232
  expiresAt: new Date(row.expires_at),
@@ -321,7 +317,6 @@ export function rowToMagicLinkRecord(row: SqliteAuthMagicLinkRow): MagicLinkReco
321
317
  id: row.id,
322
318
  projectId: row.project_id,
323
319
  strategyId: row.strategy_id,
324
- audience: row.audience,
325
320
  email: row.email,
326
321
  tokenHash: row.token_hash,
327
322
  returnTo: row.return_to ?? undefined,
@@ -339,7 +334,6 @@ export function rowToOidcAuthorizationAttemptRecord(
339
334
  id: row.id,
340
335
  projectId: row.project_id,
341
336
  strategyId: row.strategy_id,
342
- audience: row.audience,
343
337
  stateHash: row.state_hash,
344
338
  nonceHash: row.nonce_hash,
345
339
  codeVerifier: row.code_verifier,
@@ -1,8 +1,9 @@
1
1
  import type { Database } from "bun:sqlite"
2
- import type { AuthSessionAudience } from "@sixb/core"
3
2
  import type {
4
3
  AuthSessionStore,
5
4
  CreateAuthSessionInput,
5
+ ListAuthSessionsInput,
6
+ ListAuthSessionsResult,
6
7
  RotateSessionRefreshTokenResult,
7
8
  SessionRecord,
8
9
  } from "@sixb/core/storage"
@@ -33,70 +34,62 @@ export class SqliteAuthSessionStore implements AuthSessionStore {
33
34
  return getSessionById(this.db, params)
34
35
  }
35
36
 
36
- async getActiveByUserId(params: {
37
+ async listActiveByUserId(params: {
37
38
  readonly projectId: string
38
39
  readonly userId: string
39
- readonly audience: AuthSessionAudience
40
40
  readonly now: Date
41
- }): Promise<SessionRecord | null> {
42
- const row = this.db
41
+ }): Promise<readonly SessionRecord[]> {
42
+ const rows = this.db
43
43
  .query(
44
44
  `
45
45
  SELECT *
46
46
  FROM auth_sessions
47
47
  WHERE project_id = ?
48
48
  AND user_id = ?
49
- AND audience = ?
50
49
  AND revoked_at IS NULL
51
50
  AND expires_at > ?
52
51
  AND (absolute_expires_at IS NULL OR absolute_expires_at > ?)
53
- ORDER BY created_at DESC, id DESC
54
- LIMIT 1
52
+ ORDER BY COALESCE(last_seen_at, created_at) DESC, created_at DESC, id DESC
55
53
  `
56
54
  )
57
- .get(
55
+ .all(
58
56
  params.projectId,
59
57
  params.userId,
60
- params.audience,
61
58
  toIso(params.now),
62
59
  toIso(params.now)
63
- ) as SqliteAuthSessionRow | null
60
+ ) as SqliteAuthSessionRow[]
64
61
 
65
- return row ? rowToSessionRecord(row) : null
62
+ return rows.map(rowToSessionRecord)
66
63
  }
67
64
 
68
- async listActiveByUserId(params: {
69
- readonly projectId: string
70
- readonly userId: string
71
- readonly now: Date
72
- }): Promise<readonly SessionRecord[]> {
65
+ async listByUserId(params: ListAuthSessionsInput): Promise<ListAuthSessionsResult> {
73
66
  const rows = this.db
74
67
  .query(
75
68
  `
76
69
  SELECT *
77
70
  FROM auth_sessions
78
- WHERE project_id = ?
79
- AND user_id = ?
80
- AND revoked_at IS NULL
81
- AND expires_at > ?
82
- AND (absolute_expires_at IS NULL OR absolute_expires_at > ?)
83
- ORDER BY COALESCE(last_seen_at, created_at) DESC, created_at DESC, id DESC
71
+ WHERE project_id = ? AND user_id = ? AND created_at <= ?
72
+ ORDER BY created_at DESC, id DESC
73
+ LIMIT ? OFFSET ?
84
74
  `
85
75
  )
86
76
  .all(
87
77
  params.projectId,
88
78
  params.userId,
89
- toIso(params.now),
90
- toIso(params.now)
79
+ params.before ? toIso(params.before.createdAt) : "9999-12-31T23:59:59.999Z",
80
+ params.limit + 1,
81
+ params.before?.skip ?? 0
91
82
  ) as SqliteAuthSessionRow[]
92
83
 
93
- return rows.map(rowToSessionRecord)
84
+ return {
85
+ sessions: rows.slice(0, params.limit).map(rowToSessionRecord),
86
+ hasMore: rows.length > params.limit,
87
+ }
94
88
  }
95
89
 
96
90
  async findValidByTokenHash(params: {
97
91
  readonly projectId: string
98
92
  readonly id: string
99
- readonly audience: AuthSessionAudience
100
93
  readonly tokenHash: string
101
94
  readonly now: Date
102
95
  }): Promise<SessionRecord | null> {
@@ -105,7 +98,6 @@ export class SqliteAuthSessionStore implements AuthSessionStore {
105
98
  if (
106
99
  !row ||
107
100
  row.refresh_token_hash !== null ||
108
- row.audience !== params.audience ||
109
101
  row.token_hash !== params.tokenHash ||
110
102
  !isLiveSessionRow(row, params.now)
111
103
  ) {
@@ -190,7 +182,6 @@ export class SqliteAuthSessionStore implements AuthSessionStore {
190
182
  async renewIfValid(params: {
191
183
  readonly projectId: string
192
184
  readonly id: string
193
- readonly audience: AuthSessionAudience
194
185
  readonly tokenHash: string
195
186
  readonly now: Date
196
187
  readonly expiresAt: Date
@@ -209,7 +200,6 @@ export class SqliteAuthSessionStore implements AuthSessionStore {
209
200
  last_seen_at = ?
210
201
  WHERE project_id = ?
211
202
  AND id = ?
212
- AND audience = ?
213
203
  AND token_hash = ?
214
204
  AND refresh_token_hash IS NULL
215
205
  AND revoked_at IS NULL
@@ -217,17 +207,7 @@ export class SqliteAuthSessionStore implements AuthSessionStore {
217
207
  AND (absolute_expires_at IS NULL OR absolute_expires_at > ?)
218
208
  `
219
209
  )
220
- .run(
221
- expiresAt,
222
- expiresAt,
223
- now,
224
- params.projectId,
225
- params.id,
226
- params.audience,
227
- params.tokenHash,
228
- now,
229
- now
230
- )
210
+ .run(expiresAt, expiresAt, now, params.projectId, params.id, params.tokenHash, now, now)
231
211
 
232
212
  return result.changes === 0 ? null : getSessionById(this.db, params)
233
213
  })
@@ -269,7 +249,6 @@ export class SqliteAuthSessionStore implements AuthSessionStore {
269
249
  async revokeActiveForUser(params: {
270
250
  readonly projectId: string
271
251
  readonly userId: string
272
- readonly audience?: AuthSessionAudience
273
252
  readonly revokedAt: Date
274
253
  }): Promise<readonly SessionRecord[]> {
275
254
  return runImmediateTransaction(this.db, () => revokeActiveSessionsForUser(this.db, params))