@sixb/sqlite 0.1.18 → 0.1.20

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 (59) hide show
  1. package/dist/action-run-storage.d.ts +4 -9
  2. package/dist/action-run-storage.d.ts.map +1 -1
  3. package/dist/agents/agent-storage.d.ts.map +1 -1
  4. package/dist/ai-limit-storage.d.ts.map +1 -1
  5. package/dist/ai-usage-storage.d.ts.map +1 -1
  6. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  7. package/dist/auth-storage/magic-links.d.ts.map +1 -1
  8. package/dist/auth-storage/oidc-attempts.d.ts.map +1 -1
  9. package/dist/auth-storage/rows.d.ts +0 -3
  10. package/dist/auth-storage/rows.d.ts.map +1 -1
  11. package/dist/auth-storage/sessions.d.ts +0 -10
  12. package/dist/auth-storage/sessions.d.ts.map +1 -1
  13. package/dist/auth-storage/shared.d.ts +0 -2
  14. package/dist/auth-storage/shared.d.ts.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +301 -398
  17. package/dist/index.js.map +27 -27
  18. package/dist/migrations.d.ts +2 -0
  19. package/dist/migrations.d.ts.map +1 -1
  20. package/dist/objects/storage.d.ts.map +1 -1
  21. package/dist/pipeline-run-storage.d.ts.map +1 -1
  22. package/dist/projection-run-storage.d.ts.map +1 -1
  23. package/dist/rules-storage.d.ts.map +1 -1
  24. package/dist/share-grant-storage.d.ts.map +1 -1
  25. package/dist/share-session-storage.d.ts.map +1 -1
  26. package/dist/sync-run-storage.d.ts.map +1 -1
  27. package/dist/timeseries-storage.d.ts.map +1 -1
  28. package/dist/transactions.d.ts +0 -1
  29. package/dist/transactions.d.ts.map +1 -1
  30. package/dist/webhook-run-storage.d.ts.map +1 -1
  31. package/dist/workflow-intervention-storage.d.ts.map +1 -1
  32. package/dist/workflow-run-storage.d.ts.map +1 -1
  33. package/package.json +3 -3
  34. package/src/action-run-storage.ts +104 -447
  35. package/src/agents/agent-storage.ts +0 -5
  36. package/src/ai-limit-storage.ts +0 -2
  37. package/src/ai-usage-storage.ts +0 -4
  38. package/src/auth-storage/auth-storage.ts +0 -23
  39. package/src/auth-storage/magic-links.ts +1 -6
  40. package/src/auth-storage/oidc-attempts.ts +1 -6
  41. package/src/auth-storage/rows.ts +0 -6
  42. package/src/auth-storage/sessions.ts +1 -49
  43. package/src/auth-storage/shared.ts +3 -20
  44. package/src/index.ts +1 -10
  45. package/src/migrations/060-terminal-action-runs.sql +149 -0
  46. package/src/migrations/061-drop-auth-audience.sql +16 -0
  47. package/src/migrations.ts +24 -0
  48. package/src/objects/storage.ts +0 -5
  49. package/src/pipeline-run-storage.ts +0 -5
  50. package/src/projection-run-storage.ts +0 -5
  51. package/src/rules-storage.ts +0 -5
  52. package/src/share-grant-storage.ts +0 -2
  53. package/src/share-session-storage.ts +0 -2
  54. package/src/sync-run-storage.ts +0 -5
  55. package/src/timeseries-storage.ts +0 -5
  56. package/src/transactions.ts +8 -8
  57. package/src/webhook-run-storage.ts +0 -3
  58. package/src/workflow-intervention-storage.ts +0 -5
  59. package/src/workflow-run-storage.ts +0 -5
@@ -45,7 +45,6 @@ import type {
45
45
  UpdateAiLimitPolicyInput,
46
46
  } from "@sixb/core/storage"
47
47
  import { AiLimitStorageError, aiLimitCalendarMonth } from "@sixb/core/storage"
48
- import { installFreshSqliteSchema } from "./migrations"
49
48
  import {
50
49
  closeSqliteStoreConnection,
51
50
  openSqliteStoreConnection,
@@ -64,7 +63,6 @@ export class SqliteAiLimitStorage implements AiLimitStorage {
64
63
 
65
64
  constructor(options: SqliteAiLimitStorageOptions = {}) {
66
65
  this.connection = openSqliteStoreConnection(options)
67
- if (this.connection.installFreshSchema) installFreshSqliteSchema(this.connection.db)
68
66
  }
69
67
 
70
68
  async createPolicy(input: CreateAiLimitPolicyInput): Promise<AiLimitPolicy> {
@@ -14,7 +14,6 @@ import {
14
14
  type SummarizeAiUsageExecutionInput,
15
15
  type SummarizeAiUsageExecutionsInput,
16
16
  } from "@sixb/core/storage"
17
- import { installFreshSqliteSchema } from "./migrations"
18
17
  import { isUniqueConstraintError } from "./storage-errors"
19
18
  import {
20
19
  closeSqliteStoreConnection,
@@ -34,9 +33,6 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
34
33
 
35
34
  constructor(options: SqliteAiUsageStorageOptions = {}) {
36
35
  this.connection = openSqliteStoreConnection(options)
37
- if (this.connection.installFreshSchema) {
38
- installFreshSqliteSchema(this.connection.db)
39
- }
40
36
  }
41
37
 
42
38
  async recordModelCall(input: RecordAiModelCallInput): Promise<RecordAiModelCallResult> {
@@ -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,5 +1,4 @@
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,
@@ -33,38 +32,6 @@ export class SqliteAuthSessionStore implements AuthSessionStore {
33
32
  return getSessionById(this.db, params)
34
33
  }
35
34
 
36
- async getActiveByUserId(params: {
37
- readonly projectId: string
38
- readonly userId: string
39
- readonly audience: AuthSessionAudience
40
- readonly now: Date
41
- }): Promise<SessionRecord | null> {
42
- const row = this.db
43
- .query(
44
- `
45
- SELECT *
46
- FROM auth_sessions
47
- WHERE project_id = ?
48
- AND user_id = ?
49
- AND audience = ?
50
- AND revoked_at IS NULL
51
- AND expires_at > ?
52
- AND (absolute_expires_at IS NULL OR absolute_expires_at > ?)
53
- ORDER BY created_at DESC, id DESC
54
- LIMIT 1
55
- `
56
- )
57
- .get(
58
- params.projectId,
59
- params.userId,
60
- params.audience,
61
- toIso(params.now),
62
- toIso(params.now)
63
- ) as SqliteAuthSessionRow | null
64
-
65
- return row ? rowToSessionRecord(row) : null
66
- }
67
-
68
35
  async listActiveByUserId(params: {
69
36
  readonly projectId: string
70
37
  readonly userId: string
@@ -96,7 +63,6 @@ export class SqliteAuthSessionStore implements AuthSessionStore {
96
63
  async findValidByTokenHash(params: {
97
64
  readonly projectId: string
98
65
  readonly id: string
99
- readonly audience: AuthSessionAudience
100
66
  readonly tokenHash: string
101
67
  readonly now: Date
102
68
  }): Promise<SessionRecord | null> {
@@ -105,7 +71,6 @@ export class SqliteAuthSessionStore implements AuthSessionStore {
105
71
  if (
106
72
  !row ||
107
73
  row.refresh_token_hash !== null ||
108
- row.audience !== params.audience ||
109
74
  row.token_hash !== params.tokenHash ||
110
75
  !isLiveSessionRow(row, params.now)
111
76
  ) {
@@ -190,7 +155,6 @@ export class SqliteAuthSessionStore implements AuthSessionStore {
190
155
  async renewIfValid(params: {
191
156
  readonly projectId: string
192
157
  readonly id: string
193
- readonly audience: AuthSessionAudience
194
158
  readonly tokenHash: string
195
159
  readonly now: Date
196
160
  readonly expiresAt: Date
@@ -209,7 +173,6 @@ export class SqliteAuthSessionStore implements AuthSessionStore {
209
173
  last_seen_at = ?
210
174
  WHERE project_id = ?
211
175
  AND id = ?
212
- AND audience = ?
213
176
  AND token_hash = ?
214
177
  AND refresh_token_hash IS NULL
215
178
  AND revoked_at IS NULL
@@ -217,17 +180,7 @@ export class SqliteAuthSessionStore implements AuthSessionStore {
217
180
  AND (absolute_expires_at IS NULL OR absolute_expires_at > ?)
218
181
  `
219
182
  )
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
- )
183
+ .run(expiresAt, expiresAt, now, params.projectId, params.id, params.tokenHash, now, now)
231
184
 
232
185
  return result.changes === 0 ? null : getSessionById(this.db, params)
233
186
  })
@@ -269,7 +222,6 @@ export class SqliteAuthSessionStore implements AuthSessionStore {
269
222
  async revokeActiveForUser(params: {
270
223
  readonly projectId: string
271
224
  readonly userId: string
272
- readonly audience?: AuthSessionAudience
273
225
  readonly revokedAt: Date
274
226
  }): Promise<readonly SessionRecord[]> {
275
227
  return runImmediateTransaction(this.db, () => revokeActiveSessionsForUser(this.db, params))
@@ -1,5 +1,4 @@
1
1
  import type { Database } from "bun:sqlite"
2
- import type { AuthSessionAudience } from "@sixb/core"
3
2
  import type {
4
3
  CompleteAuthSessionInput,
5
4
  CreateAuthSessionInput,
@@ -385,7 +384,6 @@ export function validateCompleteSessionInput(
385
384
  session: CompleteAuthSessionInput
386
385
  ): void {
387
386
  const sessionId = assertNonEmpty(session.id, "Session id")
388
- assertNonEmpty(session.audience, "Session audience")
389
387
  assertNonEmpty(session.tokenHash, "Session token hash")
390
388
  assertSessionIdAvailable(db, projectId, sessionId)
391
389
  }
@@ -407,7 +405,6 @@ export function createSession(
407
405
  const projectId = assertNonEmpty(input.projectId, "Project id")
408
406
  const userId = assertNonEmpty(input.userId, "User id")
409
407
  const strategyId = assertNonEmpty(input.strategyId, "Strategy id")
410
- const audience = assertNonEmpty(input.audience, "Session audience")
411
408
  const tokenHash = assertNonEmpty(input.tokenHash, "Session token hash")
412
409
 
413
410
  assertSessionIdAvailable(db, projectId, id)
@@ -419,7 +416,6 @@ export function createSession(
419
416
  id,
420
417
  user_id,
421
418
  strategy_id,
422
- audience,
423
419
  token_hash,
424
420
  created_at,
425
421
  expires_at,
@@ -429,14 +425,13 @@ export function createSession(
429
425
  client_name,
430
426
  access_expires_at,
431
427
  refresh_token_hash
432
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
428
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
433
429
  `
434
430
  ).run(
435
431
  projectId,
436
432
  id,
437
433
  userId,
438
434
  strategyId,
439
- audience,
440
435
  tokenHash,
441
436
  toIso(input.createdAt),
442
437
  toIso(input.expiresAt),
@@ -463,20 +458,10 @@ export function revokeActiveSessionsForUser(
463
458
  params: {
464
459
  readonly projectId: string
465
460
  readonly userId: string
466
- readonly audience?: AuthSessionAudience
467
461
  readonly revokedAt: Date
468
462
  }
469
463
  ): readonly ReturnType<typeof rowToSessionRecord>[] {
470
464
  const revokedAt = toIso(params.revokedAt)
471
- const audienceCondition = params.audience === undefined ? "" : "AND audience = ?"
472
- const selectArgs =
473
- params.audience === undefined
474
- ? [params.projectId, params.userId, revokedAt, revokedAt]
475
- : [params.projectId, params.userId, params.audience, revokedAt, revokedAt]
476
- const updateArgs =
477
- params.audience === undefined
478
- ? [revokedAt, params.projectId, params.userId, revokedAt, revokedAt]
479
- : [revokedAt, params.projectId, params.userId, params.audience, revokedAt, revokedAt]
480
465
  const rows = db
481
466
  .query(
482
467
  `
@@ -484,14 +469,13 @@ export function revokeActiveSessionsForUser(
484
469
  FROM auth_sessions
485
470
  WHERE project_id = ?
486
471
  AND user_id = ?
487
- ${audienceCondition}
488
472
  AND revoked_at IS NULL
489
473
  AND expires_at > ?
490
474
  AND (absolute_expires_at IS NULL OR absolute_expires_at > ?)
491
475
  ORDER BY created_at ASC, id ASC
492
476
  `
493
477
  )
494
- .all(...selectArgs) as SqliteAuthSessionRow[]
478
+ .all(params.projectId, params.userId, revokedAt, revokedAt) as SqliteAuthSessionRow[]
495
479
 
496
480
  db.query(
497
481
  `
@@ -499,12 +483,11 @@ export function revokeActiveSessionsForUser(
499
483
  SET revoked_at = ?
500
484
  WHERE project_id = ?
501
485
  AND user_id = ?
502
- ${audienceCondition}
503
486
  AND revoked_at IS NULL
504
487
  AND expires_at > ?
505
488
  AND (absolute_expires_at IS NULL OR absolute_expires_at > ?)
506
489
  `
507
- ).run(...updateArgs)
490
+ ).run(revokedAt, params.projectId, params.userId, revokedAt, revokedAt)
508
491
 
509
492
  return rows.map((row) =>
510
493
  rowToSessionRecord({
package/src/index.ts CHANGED
@@ -41,11 +41,7 @@ import { SqliteConnectorConnectionStorage } from "./connector-connection-storage
41
41
  import { SqliteExecutionStorage } from "./execution-storage"
42
42
  import { SqliteFileDownloadGrantStorage } from "./file-download-grant-storage"
43
43
  import { SqliteFileUploadSessionStorage } from "./file-upload-session-storage"
44
- import {
45
- createSqliteStorageMigrators,
46
- installFreshSqliteSchema,
47
- sqliteStoragePath,
48
- } from "./migrations"
44
+ import { createSqliteStorageMigrators, sqliteStoragePath } from "./migrations"
49
45
  import { SqliteObjectStorage } from "./objects"
50
46
  import { SqliteOntologyStorage, type SqliteOntologyTransactionContext } from "./ontology-storage"
51
47
  import { SqlitePipelineRunStorage } from "./pipeline-run-storage"
@@ -127,10 +123,6 @@ export class SqliteStorage implements MigrationCapableStorage {
127
123
  this.readConnection = this.connection
128
124
  }
129
125
 
130
- if (this.connection.installFreshSchema) {
131
- installFreshSqliteSchema(this.connection.db)
132
- }
133
-
134
126
  const stores = createSqliteStores(this.connection, {
135
127
  runOntologyOperation: async (run) => run(),
136
128
  transactionContext: null,
@@ -256,7 +248,6 @@ export class SqliteStorage implements MigrationCapableStorage {
256
248
  {
257
249
  db: this.connection.db,
258
250
  ownsConnection: false,
259
- installFreshSchema: false,
260
251
  },
261
252
  {
262
253
  runOntologyOperation: async (run) => run(),
@@ -0,0 +1,149 @@
1
+ -- Actions run in the request that asks for them and record their run once, when it ends. No run
2
+ -- is queued, running, or cancelled any more, and every run has started and finished. SQLite cannot
3
+ -- alter a CHECK constraint, so the table is rebuilt.
4
+ CREATE TABLE action_runs_v2 (
5
+ project_id TEXT NOT NULL,
6
+ id TEXT NOT NULL,
7
+ execution_id TEXT NOT NULL,
8
+ action_id TEXT NOT NULL,
9
+ subject_kind TEXT NOT NULL CHECK (subject_kind IN ('none', 'object')),
10
+ object_type_id TEXT,
11
+ primary_id TEXT,
12
+ status TEXT NOT NULL CHECK (status IN ('succeeded', 'failed')),
13
+ phase TEXT NOT NULL CHECK (phase IN ('validation', 'writeback', 'edits', 'commit', 'effects')),
14
+ started_at TEXT NOT NULL,
15
+ finished_at TEXT NOT NULL,
16
+ params TEXT NOT NULL,
17
+ idempotency_key TEXT NOT NULL,
18
+ writeback_status TEXT CHECK (writeback_status IS NULL OR writeback_status IN ('succeeded', 'failed')),
19
+ writeback_completed_at TEXT,
20
+ writeback_result TEXT,
21
+ writeback_error TEXT CHECK (writeback_error IS NULL OR json_valid(writeback_error)),
22
+ effects_status TEXT CHECK (effects_status IS NULL OR effects_status IN ('succeeded', 'failed')),
23
+ effects_completed_at TEXT,
24
+ effects_error TEXT CHECK (effects_error IS NULL OR json_valid(effects_error)),
25
+ error TEXT CHECK (error IS NULL OR json_valid(error)),
26
+ CHECK (
27
+ (subject_kind = 'none' AND object_type_id IS NULL AND primary_id IS NULL)
28
+ OR (subject_kind = 'object' AND object_type_id IS NOT NULL AND primary_id IS NOT NULL)
29
+ ),
30
+ CHECK ((status = 'failed') = (error IS NOT NULL)),
31
+ PRIMARY KEY (project_id, id),
32
+ UNIQUE (project_id, execution_id)
33
+ );
34
+
35
+ INSERT INTO action_runs_v2 (
36
+ project_id, id, execution_id, action_id, subject_kind, object_type_id, primary_id, status,
37
+ phase, started_at, finished_at, params, idempotency_key, writeback_status,
38
+ writeback_completed_at, writeback_result, writeback_error, effects_status,
39
+ effects_completed_at, effects_error, error
40
+ )
41
+ SELECT
42
+ project_id, id, execution_id, action_id, subject_kind, object_type_id, primary_id,
43
+ migrated_status, migrated_phase,
44
+ -- A run starts when it was queued at the latest.
45
+ COALESCE(started_at, queued_at),
46
+ migrated_finished_at,
47
+ params, idempotency_key, writeback_status,
48
+ CASE
49
+ WHEN writeback_status IS NULL THEN NULL
50
+ ELSE COALESCE(writeback_completed_at, migrated_finished_at)
51
+ END,
52
+ writeback_result, writeback_error,
53
+ CASE WHEN effects_abandoned THEN 'failed' ELSE effects_status END,
54
+ CASE
55
+ WHEN effects_abandoned THEN migrated_at
56
+ WHEN effects_status IS NULL THEN NULL
57
+ ELSE COALESCE(effects_completed_at, migrated_finished_at)
58
+ END,
59
+ CASE
60
+ WHEN effects_abandoned THEN json_object(
61
+ 'code', 'internal.unexpected',
62
+ 'message', 'Abandoned: Actions now run synchronously; these effects never finished.',
63
+ 'retryable', json('false'),
64
+ 'at', migrated_at,
65
+ 'details', json_object('actionId', action_id, 'runId', id, 'phase', 'effects')
66
+ )
67
+ ELSE effects_error
68
+ END,
69
+ -- A failure names the phase its run ended in.
70
+ CASE
71
+ WHEN migrated_error IS NULL THEN NULL
72
+ WHEN json_extract(migrated_error, '$.details.phase') IS migrated_phase THEN migrated_error
73
+ ELSE json_set(migrated_error, '$.details.phase', migrated_phase)
74
+ END
75
+ FROM (
76
+ SELECT
77
+ *,
78
+ CASE
79
+ WHEN status = 'succeeded' OR edits_committed THEN 'succeeded'
80
+ ELSE 'failed'
81
+ END AS migrated_status,
82
+ CASE
83
+ WHEN edits_committed THEN CASE WHEN phase = 'effects' THEN 'effects' ELSE 'commit' END
84
+ -- The phases `request`, `enqueue`, and `cancelled` are gone: a run that stopped in one of
85
+ -- them never got past validation.
86
+ WHEN phase IN ('validation', 'writeback', 'edits', 'commit', 'effects') THEN phase
87
+ ELSE 'validation'
88
+ END AS migrated_phase,
89
+ CASE
90
+ WHEN edits_committed THEN committed_at
91
+ WHEN status IN ('queued', 'running') THEN migrated_at
92
+ ELSE COALESCE(finished_at, migrated_at)
93
+ END AS migrated_finished_at,
94
+ edits_committed AND phase = 'effects' AND effects_status IS NULL AS effects_abandoned,
95
+ CASE
96
+ WHEN edits_committed THEN NULL
97
+ WHEN status IN ('queued', 'running') THEN json_object(
98
+ 'code', 'internal.unexpected',
99
+ 'message', 'Abandoned: Actions now run synchronously; this run never finished.',
100
+ 'retryable', json('false'),
101
+ 'at', migrated_at,
102
+ 'details', json_object('actionId', action_id, 'runId', id)
103
+ )
104
+ -- Every failed run carries a failure. One recorded before failures were kept explains
105
+ -- itself.
106
+ WHEN status IN ('failed', 'cancelled') AND error IS NULL THEN json_object(
107
+ 'code', 'internal.unexpected',
108
+ 'message', 'This run failed before Sixb recorded why.',
109
+ 'retryable', json('false'),
110
+ 'at', COALESCE(finished_at, migrated_at),
111
+ 'details', json_object('actionId', action_id, 'runId', id)
112
+ )
113
+ -- An enqueue failure, which no run can meet any more, reads as the unexpected error it
114
+ -- now is.
115
+ WHEN json_extract(error, '$.code') = 'queue.enqueue_failed'
116
+ THEN json_set(error, '$.code', 'internal.unexpected', '$.retryable', json('false'))
117
+ ELSE error
118
+ END AS migrated_error
119
+ FROM (
120
+ SELECT
121
+ run.*,
122
+ committed.committed_at,
123
+ strftime('%Y-%m-%dT%H:%M:%fZ', 'now') AS migrated_at,
124
+ -- A run left running after its edits committed, as a crash between the commit and the end
125
+ -- of the run left it, succeeded: its edits and their outbox facts are in the commit. A run
126
+ -- that stopped during its effects keeps that phase, and records effects that never ended.
127
+ -- Any other run left queued or running never finished, and nothing will resume it. A
128
+ -- cancelled run failed, and keeps the failure it recorded.
129
+ run.status IN ('queued', 'running') AND committed.committed_at IS NOT NULL
130
+ AS edits_committed
131
+ FROM action_runs AS run
132
+ LEFT JOIN ontology_commits AS committed
133
+ ON committed.origin_kind = 'action'
134
+ AND committed.project_id = run.project_id
135
+ AND committed.origin_run_id = run.id
136
+ )
137
+ );
138
+
139
+ DROP TABLE action_runs;
140
+ ALTER TABLE action_runs_v2 RENAME TO action_runs;
141
+
142
+ CREATE INDEX idx_action_runs_project_started
143
+ ON action_runs(project_id, started_at DESC);
144
+ CREATE INDEX idx_action_runs_project_action_started
145
+ ON action_runs(project_id, action_id, started_at DESC);
146
+ CREATE INDEX idx_action_runs_project_object_started
147
+ ON action_runs(project_id, object_type_id, primary_id, started_at DESC);
148
+ CREATE INDEX idx_action_runs_project_status_started
149
+ ON action_runs(project_id, status, started_at DESC);
@@ -0,0 +1,16 @@
1
+ -- A project has one browser app, so sessions, magic links and sign-in attempts no longer name the
2
+ -- one they belong to. Atlas's browser sessions end here: their cookie is no longer read. Native
3
+ -- sessions carried Atlas's name only because the column needed a value, so they stay.
4
+ UPDATE auth_sessions
5
+ SET revoked_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
6
+ WHERE audience = 'atlas'
7
+ AND refresh_token_hash IS NULL
8
+ AND revoked_at IS NULL;
9
+
10
+ DROP INDEX idx_auth_sessions_user_active;
11
+ ALTER TABLE auth_sessions DROP COLUMN audience;
12
+ CREATE INDEX idx_auth_sessions_user_active
13
+ ON auth_sessions(project_id, user_id, revoked_at, expires_at);
14
+
15
+ ALTER TABLE auth_magic_links DROP COLUMN audience;
16
+ ALTER TABLE auth_oidc_authorization_attempts DROP COLUMN audience;
package/src/migrations.ts CHANGED
@@ -271,6 +271,8 @@ export const sqliteStorageMigrations = defineMigrations({
271
271
  sqliteSql("057-reranking-model-kind", rerankingModelKindSql),
272
272
  sqliteSql("058-file-download-grants", fileDownloadGrantsSql),
273
273
  sqliteSql("059-drained-source-versions", drainedSourceVersionsSql),
274
+ sqliteSql("060-terminal-action-runs", terminalActionRunsSql),
275
+ sqliteSql("061-drop-auth-audience", dropAuthAudienceSql),
274
276
  ],
275
277
  })
276
278
 
@@ -296,6 +298,26 @@ export function sqliteStep(
296
298
  return step<Database>(id, up, options)
297
299
  }
298
300
 
301
+ // Replaying every migration costs a few hundred milliseconds, and in-memory databases are made
302
+ // often (each test's storage, for one). The schema is built once per process and each new database
303
+ // starts as a copy of it.
304
+ let freshSchema: Uint8Array | undefined
305
+
306
+ /** A new in-memory database holding the current schema, as replaying every migration leaves it. */
307
+ export function freshSqliteDatabase(): Database {
308
+ if (!freshSchema) {
309
+ const template = new Database(":memory:")
310
+ try {
311
+ template.run("PRAGMA foreign_keys = ON")
312
+ installFreshSqliteSchema(template)
313
+ freshSchema = template.serialize()
314
+ } finally {
315
+ template.close()
316
+ }
317
+ }
318
+ return Database.deserialize(freshSchema)
319
+ }
320
+
299
321
  export function installFreshSqliteSchema(db: Database): void {
300
322
  for (const migration of sqliteStorageMigrations.steps) {
301
323
  const installed = migration.up(db)
@@ -597,3 +619,5 @@ import fileDownloadGrantsSql from "./migrations/058-file-download-grants.sql" wi
597
619
  import drainedSourceVersionsSql from "./migrations/059-drained-source-versions.sql" with {
598
620
  type: "text",
599
621
  }
622
+ import terminalActionRunsSql from "./migrations/060-terminal-action-runs.sql" with { type: "text" }
623
+ import dropAuthAudienceSql from "./migrations/061-drop-auth-audience.sql" with { type: "text" }