@sixb/sqlite 0.1.6 → 0.1.8
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/agents/rows.d.ts +7 -4
- package/dist/agents/rows.d.ts.map +1 -1
- package/dist/agents/runs.d.ts +3 -2
- package/dist/agents/runs.d.ts.map +1 -1
- package/dist/agents/threads.d.ts.map +1 -1
- package/dist/ai-cost-groups.d.ts +5 -0
- package/dist/ai-cost-groups.d.ts.map +1 -0
- package/dist/ai-cost-storage.d.ts +2 -1
- package/dist/ai-cost-storage.d.ts.map +1 -1
- package/dist/ai-limit-storage.d.ts +35 -0
- package/dist/ai-limit-storage.d.ts.map +1 -0
- package/dist/ai-usage-storage.d.ts +1 -0
- package/dist/ai-usage-storage.d.ts.map +1 -1
- package/dist/auth-storage/access-tokens.d.ts +1 -1
- package/dist/auth-storage/access-tokens.d.ts.map +1 -1
- package/dist/auth-storage/auth-storage.d.ts +4 -1
- package/dist/auth-storage/auth-storage.d.ts.map +1 -1
- package/dist/auth-storage/device-authorizations.d.ts +29 -0
- package/dist/auth-storage/device-authorizations.d.ts.map +1 -0
- package/dist/auth-storage/rows.d.ts +19 -1
- package/dist/auth-storage/rows.d.ts.map +1 -1
- package/dist/auth-storage/shared.d.ts +1 -1
- package/dist/auth-storage/shared.d.ts.map +1 -1
- package/dist/execution-storage.d.ts +3 -2
- package/dist/execution-storage.d.ts.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3443 -789
- package/dist/index.js.map +29 -20
- package/dist/migrations.d.ts.map +1 -1
- package/dist/objects/index.d.ts +3 -0
- package/dist/objects/index.d.ts.map +1 -0
- package/dist/{object-query-compiler.d.ts → objects/query-compiler.d.ts} +16 -1
- package/dist/objects/query-compiler.d.ts.map +1 -0
- package/dist/objects/read-scope.d.ts +17 -0
- package/dist/objects/read-scope.d.ts.map +1 -0
- package/dist/objects/reader.d.ts +51 -0
- package/dist/objects/reader.d.ts.map +1 -0
- package/dist/objects/rows.d.ts +33 -0
- package/dist/objects/rows.d.ts.map +1 -0
- package/dist/objects/selected-reader.d.ts +9 -0
- package/dist/objects/selected-reader.d.ts.map +1 -0
- package/dist/{object-storage.d.ts → objects/storage.d.ts} +12 -9
- package/dist/objects/storage.d.ts.map +1 -0
- package/dist/share-grant-storage.d.ts +20 -0
- package/dist/share-grant-storage.d.ts.map +1 -0
- package/dist/share-session-storage.d.ts +19 -0
- package/dist/share-session-storage.d.ts.map +1 -0
- package/dist/storage-errors.d.ts +1 -0
- package/dist/storage-errors.d.ts.map +1 -1
- package/dist/transactions.d.ts +8 -0
- package/dist/transactions.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agents/rows.ts +36 -9
- package/src/agents/runs.ts +152 -13
- package/src/agents/threads.ts +2 -17
- package/src/ai-cost-groups.ts +100 -0
- package/src/ai-cost-storage.ts +67 -25
- package/src/ai-limit-storage.ts +981 -0
- package/src/ai-usage-storage.ts +24 -1
- package/src/auth-storage/access-tokens.ts +77 -68
- package/src/auth-storage/auth-storage.ts +80 -1
- package/src/auth-storage/device-authorizations.ts +153 -0
- package/src/auth-storage/rows.ts +41 -0
- package/src/auth-storage/shared.ts +1 -0
- package/src/execution-storage.ts +22 -3
- package/src/index.ts +24 -3
- package/src/migrations/029-model-accounting.sql +79 -0
- package/src/migrations/030-ai-usage-limits.sql +100 -0
- package/src/migrations/031-subagent-runs.sql +231 -0
- package/src/migrations/032-conversation-run-spec.sql +78 -0
- package/src/migrations/033-retire-agent-definitions.sql +89 -0
- package/src/migrations/034-device-authorizations.sql +22 -0
- package/src/migrations/035-share-grants.sql +66 -0
- package/src/migrations/036-share-sessions.sql +226 -0
- package/src/migrations.ts +22 -0
- package/src/objects/index.ts +2 -0
- package/src/{object-query-compiler.ts → objects/query-compiler.ts} +104 -54
- package/src/objects/read-scope.ts +316 -0
- package/src/objects/reader.ts +526 -0
- package/src/objects/rows.ts +123 -0
- package/src/objects/selected-reader.ts +119 -0
- package/src/objects/storage.ts +296 -0
- package/src/share-grant-storage.ts +252 -0
- package/src/share-session-storage.ts +189 -0
- package/src/storage-errors.ts +4 -0
- package/src/transactions.ts +32 -0
- package/src/workflow-run-storage.ts +8 -8
- package/dist/object-query-compiler.d.ts.map +0 -1
- package/dist/object-storage.d.ts.map +0 -1
- package/src/object-storage.ts +0 -709
package/src/ai-usage-storage.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isModelReasoning, normalizeModelProviderIds } from "@sixb/core/models"
|
|
1
2
|
import type { ReadonlyJsonObject } from "@sixb/core/storage"
|
|
2
3
|
import {
|
|
3
4
|
type AiModelCallUsageInput,
|
|
@@ -129,7 +130,9 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
|
|
|
129
130
|
attempt,
|
|
130
131
|
call_id,
|
|
131
132
|
provider_id,
|
|
133
|
+
provider_ids,
|
|
132
134
|
requested_model_id,
|
|
135
|
+
requested_reasoning,
|
|
133
136
|
response_model_id,
|
|
134
137
|
response_id,
|
|
135
138
|
input_tokens,
|
|
@@ -145,7 +148,7 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
|
|
|
145
148
|
occurred_at,
|
|
146
149
|
recorded_at
|
|
147
150
|
) VALUES (
|
|
148
|
-
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
151
|
+
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
|
149
152
|
)
|
|
150
153
|
`
|
|
151
154
|
)
|
|
@@ -156,7 +159,9 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
|
|
|
156
159
|
record.attempt,
|
|
157
160
|
record.callId,
|
|
158
161
|
record.providerId,
|
|
162
|
+
record.providerIds === undefined ? null : JSON.stringify(record.providerIds),
|
|
159
163
|
record.requestedModelId,
|
|
164
|
+
record.requestedReasoning === undefined ? null : JSON.stringify(record.requestedReasoning),
|
|
160
165
|
record.responseModelId ?? null,
|
|
161
166
|
record.responseId,
|
|
162
167
|
record.usage.inputTokens ?? null,
|
|
@@ -228,7 +233,13 @@ export class SqliteAiUsageStorage implements AiUsageStorage {
|
|
|
228
233
|
callId: row.call_id,
|
|
229
234
|
requesterGroupIds: groupRows.map((group) => group.group_id),
|
|
230
235
|
providerId: row.provider_id,
|
|
236
|
+
...(row.provider_ids === null
|
|
237
|
+
? {}
|
|
238
|
+
: { providerIds: normalizeModelProviderIds(JSON.parse(row.provider_ids)) }),
|
|
231
239
|
requestedModelId: row.requested_model_id,
|
|
240
|
+
...(row.requested_reasoning === null
|
|
241
|
+
? {}
|
|
242
|
+
: { requestedReasoning: requestedReasoningFromRow(row.requested_reasoning) }),
|
|
232
243
|
...(row.response_model_id === null ? {} : { responseModelId: row.response_model_id }),
|
|
233
244
|
responseId: row.response_id,
|
|
234
245
|
usage: {
|
|
@@ -281,7 +292,9 @@ interface AiUsageRow {
|
|
|
281
292
|
readonly attempt: number
|
|
282
293
|
readonly call_id: string
|
|
283
294
|
readonly provider_id: string
|
|
295
|
+
readonly provider_ids: string | null
|
|
284
296
|
readonly requested_model_id: string
|
|
297
|
+
readonly requested_reasoning: string | null
|
|
285
298
|
readonly response_model_id: string | null
|
|
286
299
|
readonly response_id: string
|
|
287
300
|
readonly input_tokens: number | null
|
|
@@ -336,6 +349,16 @@ function usageFromRow(row: AiUsageRow): AiModelCallUsageInput {
|
|
|
336
349
|
}
|
|
337
350
|
}
|
|
338
351
|
|
|
352
|
+
export function requestedReasoningFromRow(
|
|
353
|
+
value: string
|
|
354
|
+
): AiModelCallUsageRecord["requestedReasoning"] {
|
|
355
|
+
const parsed: unknown = JSON.parse(value)
|
|
356
|
+
if (!isModelReasoning(parsed)) {
|
|
357
|
+
throw new Error("[SixbSqlite] Stored AI usage requested reasoning is invalid.")
|
|
358
|
+
}
|
|
359
|
+
return parsed
|
|
360
|
+
}
|
|
361
|
+
|
|
339
362
|
function assertNonBlankProjectId(projectId: string): void {
|
|
340
363
|
if (typeof projectId !== "string" || projectId.trim().length === 0) {
|
|
341
364
|
throw new TypeError("[Sixb] AI usage projectId must be nonblank.")
|
|
@@ -22,23 +22,22 @@ import {
|
|
|
22
22
|
toIso,
|
|
23
23
|
} from "./shared"
|
|
24
24
|
|
|
25
|
-
export
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
export function createAuthAccessToken(
|
|
26
|
+
db: Database,
|
|
27
|
+
input: CreateAuthAccessTokenInput
|
|
28
|
+
): AccessTokenRecord {
|
|
29
|
+
const id = assertNonEmpty(input.id, "Access token id")
|
|
30
|
+
const projectId = assertNonEmpty(input.projectId, "Project id")
|
|
31
|
+
const name = assertNonEmpty(input.name, "Access token name")
|
|
32
|
+
const subjectId = assertNonEmpty(input.subjectId, "Access token subject id")
|
|
33
|
+
const tokenHash = assertNonEmpty(input.tokenHash, "Access token hash")
|
|
34
|
+
assertKindMatchesSubject(input)
|
|
35
|
+
assertSubjectExists(db, projectId, input.subjectType, subjectId)
|
|
36
|
+
const groupIds = input.groupIds === undefined ? undefined : normalizeGroupIds(input.groupIds)
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
`
|
|
38
|
+
try {
|
|
39
|
+
db.query(
|
|
40
|
+
`
|
|
42
41
|
INSERT INTO auth_access_tokens (
|
|
43
42
|
project_id,
|
|
44
43
|
id,
|
|
@@ -55,38 +54,47 @@ export class SqliteAuthAccessTokenStore implements AuthAccessTokenStore {
|
|
|
55
54
|
expires_at
|
|
56
55
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
57
56
|
`
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
57
|
+
).run(
|
|
58
|
+
projectId,
|
|
59
|
+
id,
|
|
60
|
+
name,
|
|
61
|
+
input.kind,
|
|
62
|
+
input.subjectType,
|
|
63
|
+
subjectId,
|
|
64
|
+
tokenHash,
|
|
65
|
+
serializeOptionalStringArray(groupIds),
|
|
66
|
+
input.createdByPrincipal?.type ?? null,
|
|
67
|
+
input.createdByPrincipal?.id ?? null,
|
|
68
|
+
input.createdBySessionId ?? null,
|
|
69
|
+
toIso(input.createdAt),
|
|
70
|
+
toIso(input.expiresAt)
|
|
71
|
+
)
|
|
72
|
+
} catch (error) {
|
|
73
|
+
mapUniqueConstraintError(
|
|
74
|
+
error,
|
|
75
|
+
"duplicate_access_token",
|
|
76
|
+
`[Sixb] Access token '${id}' already exists for project '${projectId}'.`
|
|
77
|
+
)
|
|
78
|
+
}
|
|
81
79
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
const row = db
|
|
81
|
+
.query("SELECT * FROM auth_access_tokens WHERE project_id = ? AND id = ?")
|
|
82
|
+
.get(projectId, id) as SqliteAuthAccessTokenRow | null
|
|
83
|
+
const token = row ? rowToAccessTokenRecord(row) : null
|
|
84
|
+
if (!token) {
|
|
85
|
+
throw new AuthStorageError(
|
|
86
|
+
"missing_access_token",
|
|
87
|
+
`[Sixb] Access token '${id}' not found for project '${projectId}'.`
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
return token
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class SqliteAuthAccessTokenStore implements AuthAccessTokenStore {
|
|
94
|
+
constructor(private readonly db: Database) {}
|
|
95
|
+
|
|
96
|
+
async create(input: CreateAuthAccessTokenInput): Promise<AccessTokenRecord> {
|
|
97
|
+
return createAuthAccessToken(this.db, input)
|
|
90
98
|
}
|
|
91
99
|
|
|
92
100
|
async getById(params: {
|
|
@@ -246,32 +254,33 @@ export class SqliteAuthAccessTokenStore implements AuthAccessTokenStore {
|
|
|
246
254
|
lastUsedIpAddress,
|
|
247
255
|
}
|
|
248
256
|
}
|
|
257
|
+
}
|
|
249
258
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
throw new AuthStorageError(
|
|
261
|
-
"missing_user",
|
|
262
|
-
`[Sixb] User '${subjectId}' not found for project '${projectId}'.`
|
|
263
|
-
)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const row = this.db
|
|
267
|
-
.query("SELECT * FROM auth_service_accounts WHERE project_id = ? AND id = ?")
|
|
268
|
-
.get(projectId, subjectId) as SqliteAuthServiceAccountRow | null
|
|
259
|
+
function assertSubjectExists(
|
|
260
|
+
db: Database,
|
|
261
|
+
projectId: string,
|
|
262
|
+
subjectType: CreateAuthAccessTokenInput["subjectType"],
|
|
263
|
+
subjectId: string
|
|
264
|
+
): void {
|
|
265
|
+
if (subjectType === "user") {
|
|
266
|
+
const row = db
|
|
267
|
+
.query("SELECT * FROM auth_users WHERE project_id = ? AND id = ?")
|
|
268
|
+
.get(projectId, subjectId) as SqliteAuthUserRow | null
|
|
269
269
|
if (row) return
|
|
270
270
|
throw new AuthStorageError(
|
|
271
|
-
"
|
|
272
|
-
`[Sixb]
|
|
271
|
+
"missing_user",
|
|
272
|
+
`[Sixb] User '${subjectId}' not found for project '${projectId}'.`
|
|
273
273
|
)
|
|
274
274
|
}
|
|
275
|
+
|
|
276
|
+
const row = db
|
|
277
|
+
.query("SELECT * FROM auth_service_accounts WHERE project_id = ? AND id = ?")
|
|
278
|
+
.get(projectId, subjectId) as SqliteAuthServiceAccountRow | null
|
|
279
|
+
if (row) return
|
|
280
|
+
throw new AuthStorageError(
|
|
281
|
+
"missing_service_account",
|
|
282
|
+
`[Sixb] Service account '${subjectId}' not found for project '${projectId}'.`
|
|
283
|
+
)
|
|
275
284
|
}
|
|
276
285
|
|
|
277
286
|
function assertKindMatchesSubject(input: CreateAuthAccessTokenInput): void {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite"
|
|
2
2
|
import type {
|
|
3
3
|
AuthStorage,
|
|
4
|
+
CompleteDeviceAuthorizationInput,
|
|
5
|
+
CompleteDeviceAuthorizationResult,
|
|
4
6
|
CompleteMagicLinkSignInInput,
|
|
5
7
|
CompleteOidcSignInInput,
|
|
6
8
|
CompleteSignInResult,
|
|
9
|
+
DeviceAuthorizationRecord,
|
|
7
10
|
GroupMembershipRecord,
|
|
8
11
|
InvitationRecord,
|
|
9
12
|
SessionRecord,
|
|
@@ -19,7 +22,11 @@ import {
|
|
|
19
22
|
runImmediateTransaction,
|
|
20
23
|
type SqliteStoreConnection,
|
|
21
24
|
} from "../transactions"
|
|
22
|
-
import { SqliteAuthAccessTokenStore } from "./access-tokens"
|
|
25
|
+
import { createAuthAccessToken, SqliteAuthAccessTokenStore } from "./access-tokens"
|
|
26
|
+
import {
|
|
27
|
+
getDeviceAuthorizationById,
|
|
28
|
+
SqliteAuthDeviceAuthorizationStore,
|
|
29
|
+
} from "./device-authorizations"
|
|
23
30
|
import { SqliteAuthGroupMembershipStore } from "./group-memberships"
|
|
24
31
|
import { SqliteAuthUserIdentityStore } from "./identities"
|
|
25
32
|
import { SqliteAuthInvitationStore } from "./invitations"
|
|
@@ -76,6 +83,7 @@ export class SqliteAuthStorage implements AuthStorage {
|
|
|
76
83
|
readonly groupMemberships: SqliteAuthGroupMembershipStore
|
|
77
84
|
readonly magicLinks: SqliteAuthMagicLinkStore
|
|
78
85
|
readonly oidcAuthorizationAttempts: SqliteAuthOidcAuthorizationAttemptStore
|
|
86
|
+
readonly deviceAuthorizations: SqliteAuthDeviceAuthorizationStore
|
|
79
87
|
|
|
80
88
|
constructor(options: SqliteAuthStorageOptions = {}) {
|
|
81
89
|
this.connection = openSqliteStoreConnection(options)
|
|
@@ -95,6 +103,38 @@ export class SqliteAuthStorage implements AuthStorage {
|
|
|
95
103
|
this.groupMemberships = new SqliteAuthGroupMembershipStore(this.db)
|
|
96
104
|
this.magicLinks = new SqliteAuthMagicLinkStore(this.db)
|
|
97
105
|
this.oidcAuthorizationAttempts = new SqliteAuthOidcAuthorizationAttemptStore(this.db)
|
|
106
|
+
this.deviceAuthorizations = new SqliteAuthDeviceAuthorizationStore(this.db)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async completeDeviceAuthorization(
|
|
110
|
+
input: CompleteDeviceAuthorizationInput
|
|
111
|
+
): Promise<CompleteDeviceAuthorizationResult> {
|
|
112
|
+
return runImmediateTransaction(this.db, () => {
|
|
113
|
+
const authorization = getDeviceAuthorizationById(this.db, input.projectId, input.id)
|
|
114
|
+
if (!authorization) {
|
|
115
|
+
throw new AuthStorageError(
|
|
116
|
+
"missing_device_authorization",
|
|
117
|
+
`[Sixb] Device authorization '${input.id}' not found for project '${input.projectId}'.`
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
assertCompletableDeviceAuthorization(authorization, input)
|
|
121
|
+
const accessToken = createAuthAccessToken(this.db, input.accessToken)
|
|
122
|
+
this.db
|
|
123
|
+
.query(`
|
|
124
|
+
UPDATE auth_device_authorizations
|
|
125
|
+
SET status = 'consumed', consumed_at = ?
|
|
126
|
+
WHERE project_id = ? AND id = ? AND status = 'approved'
|
|
127
|
+
`)
|
|
128
|
+
.run(toIso(input.completedAt), input.projectId, input.id)
|
|
129
|
+
return {
|
|
130
|
+
authorization: {
|
|
131
|
+
...authorization,
|
|
132
|
+
status: "consumed",
|
|
133
|
+
consumedAt: new Date(input.completedAt),
|
|
134
|
+
},
|
|
135
|
+
accessToken,
|
|
136
|
+
}
|
|
137
|
+
})
|
|
98
138
|
}
|
|
99
139
|
|
|
100
140
|
async completeMagicLinkSignIn(
|
|
@@ -782,3 +822,42 @@ function assertSignInSessionAudience(
|
|
|
782
822
|
`[Sixb] Sign-in session audience '${sessionAudience}' does not match stored auth audience '${storedAudience}' for project '${projectId}'.`
|
|
783
823
|
)
|
|
784
824
|
}
|
|
825
|
+
|
|
826
|
+
function assertCompletableDeviceAuthorization(
|
|
827
|
+
authorization: DeviceAuthorizationRecord,
|
|
828
|
+
input: CompleteDeviceAuthorizationInput
|
|
829
|
+
): void {
|
|
830
|
+
if (authorization.deviceCodeHash !== input.deviceCodeHash) {
|
|
831
|
+
throw new AuthStorageError(
|
|
832
|
+
"invalid_device_authorization",
|
|
833
|
+
"[Sixb] Invalid device authorization."
|
|
834
|
+
)
|
|
835
|
+
}
|
|
836
|
+
if (authorization.status === "pending") {
|
|
837
|
+
throw new AuthStorageError(
|
|
838
|
+
"pending_device_authorization",
|
|
839
|
+
"[Sixb] Device authorization is pending."
|
|
840
|
+
)
|
|
841
|
+
}
|
|
842
|
+
if (authorization.status === "denied") {
|
|
843
|
+
throw new AuthStorageError(
|
|
844
|
+
"device_authorization_denied",
|
|
845
|
+
"[Sixb] Device authorization was denied."
|
|
846
|
+
)
|
|
847
|
+
}
|
|
848
|
+
if (
|
|
849
|
+
authorization.status !== "approved" ||
|
|
850
|
+
authorization.expiresAt <= input.completedAt ||
|
|
851
|
+
!authorization.approvedUserId ||
|
|
852
|
+
!authorization.approvedSessionId ||
|
|
853
|
+
input.accessToken.projectId !== input.projectId ||
|
|
854
|
+
input.accessToken.subjectType !== "user" ||
|
|
855
|
+
input.accessToken.subjectId !== authorization.approvedUserId ||
|
|
856
|
+
input.accessToken.createdBySessionId !== authorization.approvedSessionId
|
|
857
|
+
) {
|
|
858
|
+
throw new AuthStorageError(
|
|
859
|
+
"invalid_device_authorization",
|
|
860
|
+
"[Sixb] Device authorization is invalid."
|
|
861
|
+
)
|
|
862
|
+
}
|
|
863
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { Database } from "bun:sqlite"
|
|
2
|
+
import type {
|
|
3
|
+
AuthDeviceAuthorizationStore,
|
|
4
|
+
CreateDeviceAuthorizationInput,
|
|
5
|
+
DeviceAuthorizationRecord,
|
|
6
|
+
} from "@sixb/core/storage"
|
|
7
|
+
import { AuthStorageError, MAX_PENDING_DEVICE_AUTHORIZATIONS } from "@sixb/core/storage"
|
|
8
|
+
import { runImmediateTransaction } from "../transactions"
|
|
9
|
+
import type { SqliteAuthDeviceAuthorizationRow } from "./rows"
|
|
10
|
+
import { rowToDeviceAuthorizationRecord } from "./rows"
|
|
11
|
+
import { assertNonEmpty, mapUniqueConstraintError, toIso } from "./shared"
|
|
12
|
+
|
|
13
|
+
export class SqliteAuthDeviceAuthorizationStore implements AuthDeviceAuthorizationStore {
|
|
14
|
+
constructor(private readonly db: Database) {}
|
|
15
|
+
|
|
16
|
+
async create(input: CreateDeviceAuthorizationInput): Promise<DeviceAuthorizationRecord> {
|
|
17
|
+
return runImmediateTransaction(this.db, () => {
|
|
18
|
+
this.db
|
|
19
|
+
.query("DELETE FROM auth_device_authorizations WHERE project_id = ? AND expires_at <= ?")
|
|
20
|
+
.run(input.projectId, toIso(input.createdAt))
|
|
21
|
+
const pending = this.db
|
|
22
|
+
.query(
|
|
23
|
+
"SELECT COUNT(*) AS count FROM auth_device_authorizations WHERE project_id = ? AND status = 'pending'"
|
|
24
|
+
)
|
|
25
|
+
.get(input.projectId) as { readonly count: number }
|
|
26
|
+
if (pending.count >= MAX_PENDING_DEVICE_AUTHORIZATIONS) {
|
|
27
|
+
throw new AuthStorageError(
|
|
28
|
+
"device_authorization_limit_exceeded",
|
|
29
|
+
`[Sixb] Project '${input.projectId}' has too many pending device authorizations.`
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
this.db
|
|
34
|
+
.query(`
|
|
35
|
+
INSERT INTO auth_device_authorizations (
|
|
36
|
+
project_id, id, device_code_hash, user_code, client_name, token_name,
|
|
37
|
+
token_expires_at, status, created_at, expires_at
|
|
38
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)
|
|
39
|
+
`)
|
|
40
|
+
.run(
|
|
41
|
+
assertNonEmpty(input.projectId, "Project id"),
|
|
42
|
+
assertNonEmpty(input.id, "Device authorization id"),
|
|
43
|
+
assertNonEmpty(input.deviceCodeHash, "Device code hash"),
|
|
44
|
+
assertNonEmpty(input.userCode, "User code"),
|
|
45
|
+
assertNonEmpty(input.clientName, "Client name"),
|
|
46
|
+
assertNonEmpty(input.tokenName, "Token name"),
|
|
47
|
+
toIso(input.tokenExpiresAt),
|
|
48
|
+
toIso(input.createdAt),
|
|
49
|
+
toIso(input.expiresAt)
|
|
50
|
+
)
|
|
51
|
+
} catch (error) {
|
|
52
|
+
mapUniqueConstraintError(
|
|
53
|
+
error,
|
|
54
|
+
"duplicate_device_authorization",
|
|
55
|
+
`[Sixb] Device authorization '${input.id}' already exists for project '${input.projectId}'.`
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
return requireById(this.db, input.projectId, input.id)
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getById(params: { readonly projectId: string; readonly id: string }) {
|
|
63
|
+
return getById(this.db, params.projectId, params.id)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async getByUserCode(params: { readonly projectId: string; readonly userCode: string }) {
|
|
67
|
+
const row = this.db
|
|
68
|
+
.query("SELECT * FROM auth_device_authorizations WHERE project_id = ? AND user_code = ?")
|
|
69
|
+
.get(params.projectId, params.userCode) as SqliteAuthDeviceAuthorizationRow | null
|
|
70
|
+
return row ? rowToDeviceAuthorizationRecord(row) : null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async approve(params: {
|
|
74
|
+
readonly projectId: string
|
|
75
|
+
readonly id: string
|
|
76
|
+
readonly userId: string
|
|
77
|
+
readonly sessionId: string
|
|
78
|
+
readonly approvedAt: Date
|
|
79
|
+
}) {
|
|
80
|
+
return runImmediateTransaction(this.db, () => {
|
|
81
|
+
const result = this.db
|
|
82
|
+
.query(`
|
|
83
|
+
UPDATE auth_device_authorizations
|
|
84
|
+
SET status = 'approved', approved_user_id = ?, approved_session_id = ?, approved_at = ?
|
|
85
|
+
WHERE project_id = ? AND id = ? AND status = 'pending' AND expires_at > ?
|
|
86
|
+
AND EXISTS (
|
|
87
|
+
SELECT 1 FROM auth_sessions
|
|
88
|
+
WHERE project_id = ? AND id = ? AND user_id = ?
|
|
89
|
+
AND revoked_at IS NULL AND expires_at > ?
|
|
90
|
+
AND (absolute_expires_at IS NULL OR absolute_expires_at > ?)
|
|
91
|
+
)
|
|
92
|
+
`)
|
|
93
|
+
.run(
|
|
94
|
+
params.userId,
|
|
95
|
+
params.sessionId,
|
|
96
|
+
toIso(params.approvedAt),
|
|
97
|
+
params.projectId,
|
|
98
|
+
params.id,
|
|
99
|
+
toIso(params.approvedAt),
|
|
100
|
+
params.projectId,
|
|
101
|
+
params.sessionId,
|
|
102
|
+
params.userId,
|
|
103
|
+
toIso(params.approvedAt),
|
|
104
|
+
toIso(params.approvedAt)
|
|
105
|
+
)
|
|
106
|
+
if (result.changes !== 1) throw invalidTransition(params.id, params.projectId)
|
|
107
|
+
return requireById(this.db, params.projectId, params.id)
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async deny(params: { readonly projectId: string; readonly id: string; readonly deniedAt: Date }) {
|
|
112
|
+
return runImmediateTransaction(this.db, () => {
|
|
113
|
+
const result = this.db
|
|
114
|
+
.query(`
|
|
115
|
+
UPDATE auth_device_authorizations
|
|
116
|
+
SET status = 'denied', denied_at = ?
|
|
117
|
+
WHERE project_id = ? AND id = ? AND status = 'pending' AND expires_at > ?
|
|
118
|
+
`)
|
|
119
|
+
.run(toIso(params.deniedAt), params.projectId, params.id, toIso(params.deniedAt))
|
|
120
|
+
if (result.changes !== 1) throw invalidTransition(params.id, params.projectId)
|
|
121
|
+
return requireById(this.db, params.projectId, params.id)
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getDeviceAuthorizationById(db: Database, projectId: string, id: string) {
|
|
127
|
+
return getById(db, projectId, id)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getById(db: Database, projectId: string, id: string) {
|
|
131
|
+
const row = db
|
|
132
|
+
.query("SELECT * FROM auth_device_authorizations WHERE project_id = ? AND id = ?")
|
|
133
|
+
.get(projectId, id) as SqliteAuthDeviceAuthorizationRow | null
|
|
134
|
+
return row ? rowToDeviceAuthorizationRecord(row) : null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function requireById(db: Database, projectId: string, id: string): DeviceAuthorizationRecord {
|
|
138
|
+
const record = getById(db, projectId, id)
|
|
139
|
+
if (!record) {
|
|
140
|
+
throw new AuthStorageError(
|
|
141
|
+
"missing_device_authorization",
|
|
142
|
+
`[Sixb] Device authorization '${id}' not found for project '${projectId}'.`
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
return record
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function invalidTransition(id: string, projectId: string): AuthStorageError {
|
|
149
|
+
return new AuthStorageError(
|
|
150
|
+
"invalid_device_authorization",
|
|
151
|
+
`[Sixb] Device authorization '${id}' is no longer pending for project '${projectId}'.`
|
|
152
|
+
)
|
|
153
|
+
}
|
package/src/auth-storage/rows.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Principal } from "@sixb/core"
|
|
2
2
|
import type {
|
|
3
3
|
AccessTokenRecord,
|
|
4
|
+
DeviceAuthorizationRecord,
|
|
4
5
|
GroupMembershipRecord,
|
|
5
6
|
GroupMembershipSource,
|
|
6
7
|
InvitationRecord,
|
|
@@ -145,6 +146,24 @@ export interface SqliteAuthOidcAttemptRow {
|
|
|
145
146
|
readonly consumed_at: string | null
|
|
146
147
|
}
|
|
147
148
|
|
|
149
|
+
export interface SqliteAuthDeviceAuthorizationRow {
|
|
150
|
+
readonly project_id: string
|
|
151
|
+
readonly id: string
|
|
152
|
+
readonly device_code_hash: string
|
|
153
|
+
readonly user_code: string
|
|
154
|
+
readonly client_name: string
|
|
155
|
+
readonly token_name: string
|
|
156
|
+
readonly token_expires_at: string
|
|
157
|
+
readonly status: DeviceAuthorizationRecord["status"]
|
|
158
|
+
readonly approved_user_id: string | null
|
|
159
|
+
readonly approved_session_id: string | null
|
|
160
|
+
readonly created_at: string
|
|
161
|
+
readonly expires_at: string
|
|
162
|
+
readonly approved_at: string | null
|
|
163
|
+
readonly denied_at: string | null
|
|
164
|
+
readonly consumed_at: string | null
|
|
165
|
+
}
|
|
166
|
+
|
|
148
167
|
export function rowToUserRecord(row: SqliteAuthUserRow): UserRecord {
|
|
149
168
|
return {
|
|
150
169
|
id: row.id,
|
|
@@ -319,6 +338,28 @@ export function rowToOidcAuthorizationAttemptRecord(
|
|
|
319
338
|
}
|
|
320
339
|
}
|
|
321
340
|
|
|
341
|
+
export function rowToDeviceAuthorizationRecord(
|
|
342
|
+
row: SqliteAuthDeviceAuthorizationRow
|
|
343
|
+
): DeviceAuthorizationRecord {
|
|
344
|
+
return {
|
|
345
|
+
id: row.id,
|
|
346
|
+
projectId: row.project_id,
|
|
347
|
+
deviceCodeHash: row.device_code_hash,
|
|
348
|
+
userCode: row.user_code,
|
|
349
|
+
clientName: row.client_name,
|
|
350
|
+
tokenName: row.token_name,
|
|
351
|
+
tokenExpiresAt: new Date(row.token_expires_at),
|
|
352
|
+
status: row.status,
|
|
353
|
+
approvedUserId: row.approved_user_id ?? undefined,
|
|
354
|
+
approvedSessionId: row.approved_session_id ?? undefined,
|
|
355
|
+
createdAt: new Date(row.created_at),
|
|
356
|
+
expiresAt: new Date(row.expires_at),
|
|
357
|
+
approvedAt: row.approved_at ? new Date(row.approved_at) : undefined,
|
|
358
|
+
deniedAt: row.denied_at ? new Date(row.denied_at) : undefined,
|
|
359
|
+
consumedAt: row.consumed_at ? new Date(row.consumed_at) : undefined,
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
322
363
|
export function serializeOptionalRecord(
|
|
323
364
|
value: Readonly<Record<string, unknown>> | undefined
|
|
324
365
|
): string | null {
|
package/src/execution-storage.ts
CHANGED
|
@@ -6,14 +6,20 @@ import {
|
|
|
6
6
|
normalizeExecutionRecord,
|
|
7
7
|
validateExecutionRecordReferences,
|
|
8
8
|
} from "@sixb/core/internal/execution-storage"
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
CreateExecutionInput,
|
|
11
|
+
ExecutionRecord,
|
|
12
|
+
ExecutionStorage,
|
|
13
|
+
ShareSessionStorage,
|
|
14
|
+
} from "@sixb/core/storage"
|
|
10
15
|
import { ExecutionStorageError } from "@sixb/core/storage"
|
|
11
16
|
import type { SqliteAuthStorage } from "./auth-storage"
|
|
12
17
|
|
|
13
18
|
export class SqliteExecutionStorage implements ExecutionStorage {
|
|
14
19
|
constructor(
|
|
15
20
|
private readonly db: Database,
|
|
16
|
-
private readonly auth: SqliteAuthStorage
|
|
21
|
+
private readonly auth: SqliteAuthStorage,
|
|
22
|
+
private readonly shareSessions: Pick<ShareSessionStorage, "getById">
|
|
17
23
|
) {}
|
|
18
24
|
|
|
19
25
|
async create(input: CreateExecutionInput): Promise<ExecutionRecord> {
|
|
@@ -21,6 +27,7 @@ export class SqliteExecutionStorage implements ExecutionStorage {
|
|
|
21
27
|
await validateExecutionRecordReferences(record, {
|
|
22
28
|
auth: this.auth,
|
|
23
29
|
getExecution: (params) => this.getById(params),
|
|
30
|
+
getShareSession: (params) => this.shareSessions.getById(params),
|
|
24
31
|
})
|
|
25
32
|
const row = executionRecordToStorageRow(record)
|
|
26
33
|
|
|
@@ -47,8 +54,11 @@ export class SqliteExecutionStorage implements ExecutionStorage {
|
|
|
47
54
|
authority_primitive_kind,
|
|
48
55
|
authority_primitive_id,
|
|
49
56
|
authority_kernel_operation,
|
|
57
|
+
authority_delegation_kind,
|
|
58
|
+
authority_delegation_id,
|
|
59
|
+
authority_delegation_session_id,
|
|
50
60
|
created_at
|
|
51
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
61
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
52
62
|
`
|
|
53
63
|
)
|
|
54
64
|
.run(
|
|
@@ -70,6 +80,9 @@ export class SqliteExecutionStorage implements ExecutionStorage {
|
|
|
70
80
|
row.authorityPrimitiveKind,
|
|
71
81
|
row.authorityPrimitiveId,
|
|
72
82
|
row.authorityKernelOperation,
|
|
83
|
+
row.authorityDelegationKind,
|
|
84
|
+
row.authorityDelegationId,
|
|
85
|
+
row.authorityDelegationSessionId,
|
|
73
86
|
row.createdAt.toISOString()
|
|
74
87
|
)
|
|
75
88
|
} catch (error) {
|
|
@@ -124,6 +137,9 @@ function toStorageRow(row: SqliteExecutionRow): ExecutionStorageRow {
|
|
|
124
137
|
authorityPrimitiveKind: row.authority_primitive_kind,
|
|
125
138
|
authorityPrimitiveId: row.authority_primitive_id,
|
|
126
139
|
authorityKernelOperation: row.authority_kernel_operation,
|
|
140
|
+
authorityDelegationKind: row.authority_delegation_kind,
|
|
141
|
+
authorityDelegationId: row.authority_delegation_id,
|
|
142
|
+
authorityDelegationSessionId: row.authority_delegation_session_id,
|
|
127
143
|
createdAt: new Date(row.created_at),
|
|
128
144
|
}
|
|
129
145
|
}
|
|
@@ -154,5 +170,8 @@ interface SqliteExecutionRow {
|
|
|
154
170
|
readonly authority_primitive_kind: ExecutionStorageRow["authorityPrimitiveKind"]
|
|
155
171
|
readonly authority_primitive_id: string | null
|
|
156
172
|
readonly authority_kernel_operation: ExecutionStorageRow["authorityKernelOperation"]
|
|
173
|
+
readonly authority_delegation_kind: ExecutionStorageRow["authorityDelegationKind"]
|
|
174
|
+
readonly authority_delegation_id: string | null
|
|
175
|
+
readonly authority_delegation_session_id: string | null
|
|
157
176
|
readonly created_at: string
|
|
158
177
|
}
|