@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.
Files changed (55) hide show
  1. package/dist/agents/rows.d.ts +7 -4
  2. package/dist/agents/rows.d.ts.map +1 -1
  3. package/dist/agents/runs.d.ts +3 -2
  4. package/dist/agents/runs.d.ts.map +1 -1
  5. package/dist/agents/threads.d.ts.map +1 -1
  6. package/dist/ai-cost-groups.d.ts +5 -0
  7. package/dist/ai-cost-groups.d.ts.map +1 -0
  8. package/dist/ai-cost-storage.d.ts +2 -1
  9. package/dist/ai-cost-storage.d.ts.map +1 -1
  10. package/dist/ai-limit-storage.d.ts +35 -0
  11. package/dist/ai-limit-storage.d.ts.map +1 -0
  12. package/dist/ai-usage-storage.d.ts +1 -0
  13. package/dist/ai-usage-storage.d.ts.map +1 -1
  14. package/dist/auth-storage/access-tokens.d.ts +1 -1
  15. package/dist/auth-storage/access-tokens.d.ts.map +1 -1
  16. package/dist/auth-storage/auth-storage.d.ts +4 -1
  17. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  18. package/dist/auth-storage/device-authorizations.d.ts +29 -0
  19. package/dist/auth-storage/device-authorizations.d.ts.map +1 -0
  20. package/dist/auth-storage/rows.d.ts +19 -1
  21. package/dist/auth-storage/rows.d.ts.map +1 -1
  22. package/dist/auth-storage/shared.d.ts +1 -1
  23. package/dist/auth-storage/shared.d.ts.map +1 -1
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1937 -387
  27. package/dist/index.js.map +20 -17
  28. package/dist/migrations.d.ts.map +1 -1
  29. package/dist/ontology-storage/materialization-state.d.ts +7 -3
  30. package/dist/ontology-storage/materialization-state.d.ts.map +1 -1
  31. package/dist/ontology-storage/materializations.d.ts.map +1 -1
  32. package/package.json +3 -3
  33. package/src/agents/rows.ts +36 -9
  34. package/src/agents/runs.ts +152 -13
  35. package/src/agents/threads.ts +2 -17
  36. package/src/ai-cost-groups.ts +100 -0
  37. package/src/ai-cost-storage.ts +67 -25
  38. package/src/ai-limit-storage.ts +981 -0
  39. package/src/ai-usage-storage.ts +24 -1
  40. package/src/auth-storage/access-tokens.ts +77 -68
  41. package/src/auth-storage/auth-storage.ts +80 -1
  42. package/src/auth-storage/device-authorizations.ts +153 -0
  43. package/src/auth-storage/rows.ts +41 -0
  44. package/src/auth-storage/shared.ts +1 -0
  45. package/src/index.ts +6 -0
  46. package/src/migrations/029-model-accounting.sql +79 -0
  47. package/src/migrations/030-ai-usage-limits.sql +100 -0
  48. package/src/migrations/031-subagent-runs.sql +231 -0
  49. package/src/migrations/032-conversation-run-spec.sql +78 -0
  50. package/src/migrations/033-retire-agent-definitions.sql +89 -0
  51. package/src/migrations/034-device-authorizations.sql +22 -0
  52. package/src/migrations.ts +18 -0
  53. package/src/ontology-storage/materialization-state.ts +53 -33
  54. package/src/ontology-storage/materializations.ts +4 -5
  55. package/src/workflow-run-storage.ts +8 -8
@@ -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 class SqliteAuthAccessTokenStore implements AuthAccessTokenStore {
26
- constructor(private readonly db: Database) {}
27
-
28
- async create(input: CreateAuthAccessTokenInput): Promise<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
- this.assertSubjectExists(projectId, input.subjectType, subjectId)
36
- const groupIds = input.groupIds === undefined ? undefined : normalizeGroupIds(input.groupIds)
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
- try {
39
- this.db
40
- .query(
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
- .run(
60
- projectId,
61
- id,
62
- name,
63
- input.kind,
64
- input.subjectType,
65
- subjectId,
66
- tokenHash,
67
- serializeOptionalStringArray(groupIds),
68
- input.createdByPrincipal?.type ?? null,
69
- input.createdByPrincipal?.id ?? null,
70
- input.createdBySessionId ?? null,
71
- toIso(input.createdAt),
72
- toIso(input.expiresAt)
73
- )
74
- } catch (error) {
75
- mapUniqueConstraintError(
76
- error,
77
- "duplicate_access_token",
78
- `[Sixb] Access token '${id}' already exists for project '${projectId}'.`
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
- const token = await this.getById({ projectId, id })
83
- if (!token) {
84
- throw new AuthStorageError(
85
- "missing_access_token",
86
- `[Sixb] Access token '${id}' not found for project '${projectId}'.`
87
- )
88
- }
89
- return token
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
- private assertSubjectExists(
251
- projectId: string,
252
- subjectType: CreateAuthAccessTokenInput["subjectType"],
253
- subjectId: string
254
- ): void {
255
- if (subjectType === "user") {
256
- const row = this.db
257
- .query("SELECT * FROM auth_users WHERE project_id = ? AND id = ?")
258
- .get(projectId, subjectId) as SqliteAuthUserRow | null
259
- if (row) return
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
- "missing_service_account",
272
- `[Sixb] Service account '${subjectId}' not found for project '${projectId}'.`
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
+ }
@@ -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 {
@@ -658,6 +658,7 @@ export function mapUniqueConstraintError(
658
658
  error: unknown,
659
659
  code:
660
660
  | "duplicate_access_token"
661
+ | "duplicate_device_authorization"
661
662
  | "duplicate_identity"
662
663
  | "duplicate_invitation"
663
664
  | "duplicate_magic_link"
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