@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.
- package/dist/action-run-storage.d.ts +4 -9
- package/dist/action-run-storage.d.ts.map +1 -1
- package/dist/agents/agent-storage.d.ts.map +1 -1
- package/dist/ai-limit-storage.d.ts.map +1 -1
- package/dist/ai-usage-storage.d.ts.map +1 -1
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/magic-links.d.ts.map +1 -1
- package/dist/auth-storage/oidc-attempts.d.ts.map +1 -1
- package/dist/auth-storage/rows.d.ts +0 -3
- package/dist/auth-storage/rows.d.ts.map +1 -1
- package/dist/auth-storage/sessions.d.ts +0 -10
- package/dist/auth-storage/sessions.d.ts.map +1 -1
- package/dist/auth-storage/shared.d.ts +0 -2
- package/dist/auth-storage/shared.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +301 -398
- package/dist/index.js.map +27 -27
- package/dist/migrations.d.ts +2 -0
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/storage.d.ts.map +1 -1
- package/dist/pipeline-run-storage.d.ts.map +1 -1
- package/dist/projection-run-storage.d.ts.map +1 -1
- package/dist/rules-storage.d.ts.map +1 -1
- package/dist/share-grant-storage.d.ts.map +1 -1
- package/dist/share-session-storage.d.ts.map +1 -1
- package/dist/sync-run-storage.d.ts.map +1 -1
- package/dist/timeseries-storage.d.ts.map +1 -1
- package/dist/transactions.d.ts +0 -1
- package/dist/transactions.d.ts.map +1 -1
- package/dist/webhook-run-storage.d.ts.map +1 -1
- package/dist/workflow-intervention-storage.d.ts.map +1 -1
- package/dist/workflow-run-storage.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/action-run-storage.ts +104 -447
- package/src/agents/agent-storage.ts +0 -5
- package/src/ai-limit-storage.ts +0 -2
- package/src/ai-usage-storage.ts +0 -4
- package/src/auth-storage/auth-storage.ts +0 -23
- package/src/auth-storage/magic-links.ts +1 -6
- package/src/auth-storage/oidc-attempts.ts +1 -6
- package/src/auth-storage/rows.ts +0 -6
- package/src/auth-storage/sessions.ts +1 -49
- package/src/auth-storage/shared.ts +3 -20
- package/src/index.ts +1 -10
- package/src/migrations/060-terminal-action-runs.sql +149 -0
- package/src/migrations/061-drop-auth-audience.sql +16 -0
- package/src/migrations.ts +24 -0
- package/src/objects/storage.ts +0 -5
- package/src/pipeline-run-storage.ts +0 -5
- package/src/projection-run-storage.ts +0 -5
- package/src/rules-storage.ts +0 -5
- package/src/share-grant-storage.ts +0 -2
- package/src/share-session-storage.ts +0 -2
- package/src/sync-run-storage.ts +0 -5
- package/src/timeseries-storage.ts +0 -5
- package/src/transactions.ts +8 -8
- package/src/webhook-run-storage.ts +0 -3
- package/src/workflow-intervention-storage.ts +0 -5
- package/src/workflow-run-storage.ts +0 -5
package/src/ai-limit-storage.ts
CHANGED
|
@@ -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> {
|
package/src/ai-usage-storage.ts
CHANGED
|
@@ -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,
|
package/src/auth-storage/rows.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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" }
|