@sixb/sqlite 0.1.5 → 0.1.7
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/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1937 -387
- package/dist/index.js.map +20 -17
- package/dist/migrations.d.ts.map +1 -1
- package/dist/ontology-storage/materialization-state.d.ts +7 -3
- package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
- package/dist/ontology-storage/materializations.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/index.ts +6 -0
- 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.ts +18 -0
- package/src/ontology-storage/materialization-state.ts +53 -33
- package/src/ontology-storage/materializations.ts +4 -5
- package/src/workflow-run-storage.ts +8 -8
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/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "@sixb/core/internal/storage-operation-scope"
|
|
21
21
|
import {
|
|
22
22
|
type AiCostStorage,
|
|
23
|
+
type AiLimitStorage,
|
|
23
24
|
type AiUsageStorage,
|
|
24
25
|
createTransactionStorageProxy,
|
|
25
26
|
StorageTransactionError,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
import { SqliteActionRunStorage } from "./action-run-storage"
|
|
29
30
|
import { SqliteAgentStorage } from "./agents"
|
|
30
31
|
import { SqliteAiCostStorage } from "./ai-cost-storage"
|
|
32
|
+
import { SqliteAiLimitStorage } from "./ai-limit-storage"
|
|
31
33
|
import { SqliteAiUsageStorage } from "./ai-usage-storage"
|
|
32
34
|
import { SqliteAuthStorage } from "./auth-storage"
|
|
33
35
|
import { SqliteConnectorConnectionStorage } from "./connector-connection-storage"
|
|
@@ -84,6 +86,7 @@ export class SqliteStorage implements MigrationCapableStorage {
|
|
|
84
86
|
readonly agents: SqliteAgentStorage
|
|
85
87
|
readonly aiUsage: AiUsageStorage
|
|
86
88
|
readonly aiCosts: AiCostStorage
|
|
89
|
+
readonly aiLimits: AiLimitStorage
|
|
87
90
|
readonly actionRuns: SqliteActionRunStorage
|
|
88
91
|
readonly pipelineRuns: SqlitePipelineRunStorage
|
|
89
92
|
readonly syncRuns: SqliteSyncRunStorage
|
|
@@ -147,6 +150,7 @@ export class SqliteStorage implements MigrationCapableStorage {
|
|
|
147
150
|
this.agents = createAgentOperationScope(stores.agents, scope)
|
|
148
151
|
this.aiUsage = createOperationScopedFacade(stores.aiUsage, scope)
|
|
149
152
|
this.aiCosts = createOperationScopedFacade(stores.aiCosts, scope)
|
|
153
|
+
this.aiLimits = createOperationScopedFacade(stores.aiLimits, scope)
|
|
150
154
|
this.actionRuns = createOperationScopedFacade(stores.actionRuns, scope)
|
|
151
155
|
this.pipelineRuns = createOperationScopedFacade(stores.pipelineRuns, scope)
|
|
152
156
|
this.timeseries = createOperationScopedFacade(readTimeseries, readScope)
|
|
@@ -303,6 +307,7 @@ function createSqliteStores(
|
|
|
303
307
|
agents: new SqliteAgentStorage({ connection, executions }),
|
|
304
308
|
aiUsage: new SqliteAiUsageStorage({ connection }),
|
|
305
309
|
aiCosts: new SqliteAiCostStorage(connection),
|
|
310
|
+
aiLimits: new SqliteAiLimitStorage({ connection }),
|
|
306
311
|
actionRuns: new SqliteActionRunStorage({ connection, executions }),
|
|
307
312
|
pipelineRuns: new SqlitePipelineRunStorage({ connection, executions }),
|
|
308
313
|
timeseries: new SqliteTimeseriesStorage({ connection }),
|
|
@@ -324,6 +329,7 @@ interface SqliteStoreSet {
|
|
|
324
329
|
readonly agents: SqliteAgentStorage
|
|
325
330
|
readonly aiUsage: SqliteAiUsageStorage
|
|
326
331
|
readonly aiCosts: SqliteAiCostStorage
|
|
332
|
+
readonly aiLimits: SqliteAiLimitStorage
|
|
327
333
|
readonly actionRuns: SqliteActionRunStorage
|
|
328
334
|
readonly pipelineRuns: SqlitePipelineRunStorage
|
|
329
335
|
readonly syncRuns: SqliteSyncRunStorage
|