@sixb/pg 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.
Files changed (88) hide show
  1. package/dist/agents/rows.d.ts +8 -5
  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/auth-storage/auth-storage.d.ts +4 -1
  9. package/dist/auth-storage/auth-storage.d.ts.map +1 -1
  10. package/dist/auth-storage/device-authorizations.d.ts +29 -0
  11. package/dist/auth-storage/device-authorizations.d.ts.map +1 -0
  12. package/dist/auth-storage/rows.d.ts +19 -1
  13. package/dist/auth-storage/rows.d.ts.map +1 -1
  14. package/dist/auth-storage/shared.d.ts +1 -1
  15. package/dist/auth-storage/shared.d.ts.map +1 -1
  16. package/dist/index.d.ts +6 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +8327 -5838
  19. package/dist/index.js.map +28 -19
  20. package/dist/migrations.d.ts.map +1 -1
  21. package/dist/objects/index.d.ts +2 -0
  22. package/dist/objects/index.d.ts.map +1 -0
  23. package/dist/{pg-object-query-compiler.d.ts → objects/query-compiler.d.ts} +25 -4
  24. package/dist/objects/query-compiler.d.ts.map +1 -0
  25. package/dist/objects/read-scope.d.ts +17 -0
  26. package/dist/objects/read-scope.d.ts.map +1 -0
  27. package/dist/objects/reader.d.ts +50 -0
  28. package/dist/objects/reader.d.ts.map +1 -0
  29. package/dist/objects/rows.d.ts +47 -0
  30. package/dist/objects/rows.d.ts.map +1 -0
  31. package/dist/objects/selected-reader.d.ts +8 -0
  32. package/dist/objects/selected-reader.d.ts.map +1 -0
  33. package/dist/{pg-object-storage.d.ts → objects/storage.d.ts} +12 -20
  34. package/dist/objects/storage.d.ts.map +1 -0
  35. package/dist/pg-ai-cost-storage.d.ts +2 -1
  36. package/dist/pg-ai-cost-storage.d.ts.map +1 -1
  37. package/dist/pg-ai-limit-storage.d.ts +34 -0
  38. package/dist/pg-ai-limit-storage.d.ts.map +1 -0
  39. package/dist/pg-ai-usage-storage.d.ts +28 -0
  40. package/dist/pg-ai-usage-storage.d.ts.map +1 -1
  41. package/dist/pg-execution-storage.d.ts +3 -2
  42. package/dist/pg-execution-storage.d.ts.map +1 -1
  43. package/dist/share-grant-storage.d.ts +11 -0
  44. package/dist/share-grant-storage.d.ts.map +1 -0
  45. package/dist/share-session-storage.d.ts +11 -0
  46. package/dist/share-session-storage.d.ts.map +1 -0
  47. package/dist/storage-errors.d.ts +2 -0
  48. package/dist/storage-errors.d.ts.map +1 -1
  49. package/dist/transactions.d.ts +9 -2
  50. package/dist/transactions.d.ts.map +1 -1
  51. package/package.json +4 -4
  52. package/src/agents/rows.ts +38 -9
  53. package/src/agents/runs.ts +148 -11
  54. package/src/agents/threads.ts +1 -17
  55. package/src/ai-cost-groups.ts +102 -0
  56. package/src/auth-storage/auth-storage.ts +84 -0
  57. package/src/auth-storage/device-authorizations.ts +131 -0
  58. package/src/auth-storage/rows.ts +41 -0
  59. package/src/auth-storage/shared.ts +1 -0
  60. package/src/index.ts +24 -4
  61. package/src/migrations/029-model-accounting.sql +24 -0
  62. package/src/migrations/030-ai-usage-limits.sql +94 -0
  63. package/src/migrations/031-subagent-runs.sql +97 -0
  64. package/src/migrations/032-conversation-run-spec.sql +23 -0
  65. package/src/migrations/033-retire-agent-definitions.sql +28 -0
  66. package/src/migrations/034-device-authorizations.sql +24 -0
  67. package/src/migrations/035-share-grants.sql +57 -0
  68. package/src/migrations/036-share-sessions.sql +173 -0
  69. package/src/migrations.ts +22 -0
  70. package/src/objects/index.ts +1 -0
  71. package/src/{pg-object-query-compiler.ts → objects/query-compiler.ts} +185 -100
  72. package/src/objects/read-scope.ts +324 -0
  73. package/src/objects/reader.ts +604 -0
  74. package/src/objects/rows.ts +146 -0
  75. package/src/objects/selected-reader.ts +118 -0
  76. package/src/objects/storage.ts +305 -0
  77. package/src/pg-ai-cost-storage.ts +71 -24
  78. package/src/pg-ai-limit-storage.ts +901 -0
  79. package/src/pg-ai-usage-storage.ts +34 -0
  80. package/src/pg-execution-storage.ts +21 -2
  81. package/src/pg-workflow-run-storage.ts +8 -8
  82. package/src/share-grant-storage.ts +236 -0
  83. package/src/share-session-storage.ts +127 -0
  84. package/src/storage-errors.ts +6 -0
  85. package/src/transactions.ts +46 -5
  86. package/dist/pg-object-query-compiler.d.ts.map +0 -1
  87. package/dist/pg-object-storage.d.ts.map +0 -1
  88. package/src/pg-object-storage.ts +0 -734
@@ -1,4 +1,10 @@
1
- import { assertAgentRunExecution } from "@sixb/core/internal/agent-run-storage-provider"
1
+ import {
2
+ assertAgentRunExecution,
3
+ assertConversationAgentRunSpec,
4
+ assertCreateSubagentRunInput,
5
+ assertSubagentRunResult,
6
+ subagentRunMatchesCreateInput,
7
+ } from "@sixb/core/internal/agent-run-storage-provider"
2
8
  import { normalizeRequesterGroupIds } from "@sixb/core/internal/auth"
3
9
  import { serializeSixbFailure } from "@sixb/core/internal/errors"
4
10
  import {
@@ -7,7 +13,9 @@ import {
7
13
  type AgentRunStore,
8
14
  AgentStorageError,
9
15
  type ConfirmAgentRunExecutionOwnershipInput,
16
+ type ConversationAgentRunRecord,
10
17
  type CreateAgentRunInput,
18
+ type CreateSubagentRunInput,
11
19
  type ExecutionStorage,
12
20
  type FinishAgentRunInput,
13
21
  type FinishQueuedAgentRunInput,
@@ -15,6 +23,7 @@ import {
15
23
  type ListAgentRunsResult,
16
24
  type ReclaimAgentRunInput,
17
25
  type StartAgentRunInput,
26
+ type SubagentRunRecord,
18
27
  } from "@sixb/core/storage"
19
28
  import type { SQLClient, SqlParameter } from "../pg-client"
20
29
  import { appendRunListFilters, hasEmptyStatuses, queryRunList } from "../run-list-query"
@@ -30,14 +39,14 @@ export class PgAgentRunStore implements AgentRunStore {
30
39
  private readonly executions: ExecutionStorage
31
40
  ) {}
32
41
 
33
- async create(input: CreateAgentRunInput): Promise<AgentRunRecord> {
42
+ async create(input: CreateAgentRunInput): Promise<ConversationAgentRunRecord> {
43
+ assertConversationAgentRunSpec(input.spec, "SixbPg")
34
44
  const createdAt = input.createdAt ?? new Date()
35
45
  await assertAgentRunExecution({
36
46
  executions: this.executions,
37
47
  projectId: input.projectId,
38
48
  executionId: input.executionId,
39
49
  runId: input.id,
40
- agentId: input.agentId,
41
50
  })
42
51
 
43
52
  try {
@@ -54,7 +63,6 @@ export class PgAgentRunStore implements AgentRunStore {
54
63
  `[SixbPg] Agent thread '${input.threadId}' not found for project '${input.projectId}'.`
55
64
  )
56
65
  }
57
-
58
66
  if (thread.active_run_id !== null) {
59
67
  throw new AgentStorageError(
60
68
  "active_run_exists",
@@ -67,9 +75,10 @@ export class PgAgentRunStore implements AgentRunStore {
67
75
  project_id,
68
76
  id,
69
77
  execution_id,
78
+ kind,
70
79
  thread_id,
71
- agent_id,
72
80
  trigger_message_id,
81
+ spec,
73
82
  requester_group_ids,
74
83
  status,
75
84
  attempt,
@@ -78,9 +87,10 @@ export class PgAgentRunStore implements AgentRunStore {
78
87
  ${input.projectId},
79
88
  ${input.id},
80
89
  ${input.executionId},
90
+ ${"conversation"},
81
91
  ${input.threadId},
82
- ${input.agentId},
83
92
  ${input.triggerMessageId},
93
+ ${JSON.stringify(input.spec)}::text::jsonb,
84
94
  ${JSON.stringify(normalizeRequesterGroupIds(input.requesterGroupIds))}::text::jsonb,
85
95
  ${"queued"},
86
96
  ${0},
@@ -94,7 +104,11 @@ export class PgAgentRunStore implements AgentRunStore {
94
104
  WHERE project_id = ${input.projectId} AND id = ${input.threadId}
95
105
  `
96
106
 
97
- return rowToRunRecord(row)
107
+ const created = rowToRunRecord(row)
108
+ if (created.kind !== "conversation") {
109
+ throw new Error(`[SixbPg] Agent run insert '${input.id}' returned the wrong run kind.`)
110
+ }
111
+ return created
98
112
  })
99
113
  } catch (error) {
100
114
  if (isUniqueViolation(error)) {
@@ -108,6 +122,108 @@ export class PgAgentRunStore implements AgentRunStore {
108
122
  }
109
123
  }
110
124
 
125
+ async createSubagent(input: CreateSubagentRunInput): Promise<SubagentRunRecord> {
126
+ assertCreateSubagentRunInput(input, "SixbPg")
127
+ const execution = await assertAgentRunExecution({
128
+ executions: this.executions,
129
+ projectId: input.projectId,
130
+ executionId: input.executionId,
131
+ runId: input.id,
132
+ })
133
+
134
+ try {
135
+ return await runPgTransaction(this.sql, async (tx) => {
136
+ const [parent] = await tx<AgentRunRow[]>`
137
+ SELECT * FROM agent_runs
138
+ WHERE project_id = ${input.projectId} AND id = ${input.parentRunId}
139
+ FOR UPDATE
140
+ `
141
+ if (!parent || parent.kind !== "conversation") {
142
+ throw new AgentStorageError(
143
+ "run_not_found",
144
+ `[SixbPg] Parent Agent run '${input.parentRunId}' was not found.`
145
+ )
146
+ }
147
+
148
+ const [existingRow] = await tx<AgentRunRow[]>`
149
+ SELECT * FROM agent_runs
150
+ WHERE project_id = ${input.projectId} AND id = ${input.id}
151
+ `
152
+ if (existingRow) {
153
+ const existing = rowToRunRecord(existingRow)
154
+ if (subagentRunMatchesCreateInput(existing, input)) return existing
155
+ throw new AgentStorageError(
156
+ "duplicate_id",
157
+ `[SixbPg] Subagent run '${input.id}' already exists with different immutable inputs.`
158
+ )
159
+ }
160
+ if (parent.status !== "running") {
161
+ throw new AgentStorageError(
162
+ "invalid_state",
163
+ `[SixbPg] Parent Agent run '${parent.id}' is not running (status '${parent.status}').`
164
+ )
165
+ }
166
+ if (
167
+ execution.source.type !== "execution" ||
168
+ execution.source.executionId !== parent.execution_id
169
+ ) {
170
+ throw new AgentStorageError(
171
+ "invalid_input",
172
+ `[SixbPg] Subagent execution '${execution.id}' is not a child of parent execution '${parent.execution_id}'.`
173
+ )
174
+ }
175
+ if (parent.execution_token !== input.parentExecutionToken) {
176
+ throw new AgentStorageError(
177
+ "execution_lost",
178
+ `[SixbPg] Parent Agent run '${parent.id}' is no longer owned by this execution.`
179
+ )
180
+ }
181
+ const parentRecord = rowToRunRecord(parent)
182
+
183
+ const [countRow] = await tx<{ count: string | number }[]>`
184
+ SELECT COUNT(*)::bigint AS count FROM agent_runs
185
+ WHERE project_id = ${input.projectId}
186
+ AND parent_run_id = ${input.parentRunId}
187
+ AND status IN (${"queued"}, ${"running"})
188
+ `
189
+ const activeChildren = Number(countRow?.count ?? 0)
190
+ if (activeChildren >= input.maxActiveChildren) {
191
+ throw new AgentStorageError(
192
+ "active_child_limit",
193
+ `[SixbPg] Agent run '${parent.id}' already has ${activeChildren} active subagents.`
194
+ )
195
+ }
196
+
197
+ const [row] = await tx<AgentRunRow[]>`
198
+ INSERT INTO agent_runs (
199
+ project_id, id, execution_id, kind, parent_run_id, spawn_key, spec,
200
+ requester_group_ids, status, attempt, created_at
201
+ ) VALUES (
202
+ ${input.projectId}, ${input.id}, ${input.executionId}, ${"subagent"},
203
+ ${input.parentRunId}, ${input.spawnKey},
204
+ ${JSON.stringify(input.spec)}::text::jsonb,
205
+ ${JSON.stringify(parentRecord.requesterGroupIds)}::text::jsonb,
206
+ ${"queued"}, ${0}, ${input.createdAt ?? new Date()}
207
+ )
208
+ RETURNING *
209
+ `
210
+ const created = rowToRunRecord(row)
211
+ if (created.kind !== "subagent") {
212
+ throw new Error(`[SixbPg] Subagent insert '${input.id}' returned the wrong run kind.`)
213
+ }
214
+ return created
215
+ })
216
+ } catch (error) {
217
+ if (isUniqueViolation(error)) {
218
+ throw new AgentStorageError(
219
+ "duplicate_id",
220
+ `[SixbPg] Subagent run '${input.id}' conflicts with an existing durable spawn.`
221
+ )
222
+ }
223
+ throw error
224
+ }
225
+ }
226
+
111
227
  async start(input: StartAgentRunInput): Promise<AgentRunRecord> {
112
228
  const startedAt = input.startedAt ?? new Date()
113
229
  return runPgTransaction(this.sql, async (tx) => {
@@ -198,6 +314,20 @@ export class PgAgentRunStore implements AgentRunStore {
198
314
  return runPgTransaction(this.sql, async (tx) => {
199
315
  const run = await this.lockRunning(tx, input.projectId, input.id)
200
316
 
317
+ if (input.status === "succeeded" && run.kind === "subagent") {
318
+ assertSubagentRunResult(input.result, run.id, "SixbPg")
319
+ }
320
+ if (
321
+ input.status === "succeeded" &&
322
+ run.kind === "conversation" &&
323
+ input.result !== undefined
324
+ ) {
325
+ throw new AgentStorageError(
326
+ "invalid_input",
327
+ `[SixbPg] Conversational Agent run '${run.id}' cannot persist a subagent result.`
328
+ )
329
+ }
330
+
201
331
  if (run.execution_token !== input.executionToken) {
202
332
  throw new AgentStorageError(
203
333
  "execution_lost",
@@ -213,6 +343,7 @@ export class PgAgentRunStore implements AgentRunStore {
213
343
  finish_reason = ${input.finishReason ?? null},
214
344
  error = ${errorValue}::text::jsonb,
215
345
  diagnostics = ${input.diagnostics === undefined ? null : JSON.stringify(input.diagnostics)}::text::jsonb,
346
+ result = ${input.status === "succeeded" && input.result !== undefined ? JSON.stringify(input.result) : null}::text::jsonb,
216
347
  execution_token = ${null},
217
348
  execution_queue_lease_expires_at = ${null},
218
349
  completed_at = ${completedAt}
@@ -262,7 +393,7 @@ export class PgAgentRunStore implements AgentRunStore {
262
393
  }
263
394
 
264
395
  async list(input: ListAgentRunsInput): Promise<ListAgentRunsResult> {
265
- if (hasEmptyStatuses(input)) {
396
+ if (hasEmptyStatuses(input) || input.kinds?.length === 0) {
266
397
  return { runs: [], hasMore: false, total: 0 }
267
398
  }
268
399
 
@@ -270,14 +401,19 @@ export class PgAgentRunStore implements AgentRunStore {
270
401
  const params: SqlParameter[] = [input.projectId]
271
402
  let index = 2
272
403
 
404
+ if (input.kinds) {
405
+ whereClauses.push(`kind = ANY($${index++}::text[])`)
406
+ params.push(input.kinds as string[])
407
+ }
408
+
273
409
  if (input.threadId) {
274
410
  whereClauses.push(`thread_id = $${index++}`)
275
411
  params.push(input.threadId)
276
412
  }
277
413
 
278
- if (input.agentId) {
279
- whereClauses.push(`agent_id = $${index++}`)
280
- params.push(input.agentId)
414
+ if (input.parentRunId) {
415
+ whereClauses.push(`parent_run_id = $${index++}`)
416
+ params.push(input.parentRunId)
281
417
  }
282
418
 
283
419
  index = appendRunListFilters(
@@ -336,6 +472,7 @@ export class PgAgentRunStore implements AgentRunStore {
336
472
  }
337
473
 
338
474
  private async releaseThread(sql: SQLClient, run: AgentRunRow, completedAt: Date): Promise<void> {
475
+ if (run.kind !== "conversation") return
339
476
  await sql`
340
477
  UPDATE agent_threads SET active_run_id = ${null}, updated_at = ${completedAt}
341
478
  WHERE project_id = ${run.project_id} AND id = ${run.thread_id} AND active_run_id = ${run.id}
@@ -23,7 +23,6 @@ export class PgAgentThreadStore implements AgentThreadStore {
23
23
  INSERT INTO agent_threads (
24
24
  project_id,
25
25
  id,
26
- agent_id,
27
26
  owner_principal_type,
28
27
  owner_principal_id,
29
28
  title,
@@ -33,7 +32,6 @@ export class PgAgentThreadStore implements AgentThreadStore {
33
32
  ) VALUES (
34
33
  ${input.projectId},
35
34
  ${input.id},
36
- ${input.agentId},
37
35
  ${input.ownerPrincipal.type},
38
36
  ${input.ownerPrincipal.id},
39
37
  ${input.title ?? null},
@@ -66,10 +64,7 @@ export class PgAgentThreadStore implements AgentThreadStore {
66
64
  }
67
65
 
68
66
  async list(input: ListAgentThreadsInput): Promise<ListAgentThreadsResult> {
69
- if (
70
- (input.statuses !== undefined && input.statuses.length === 0) ||
71
- (input.agentIds !== undefined && input.agentIds.length === 0)
72
- ) {
67
+ if (input.statuses !== undefined && input.statuses.length === 0) {
73
68
  return { threads: [], hasMore: false, total: 0 }
74
69
  }
75
70
 
@@ -77,17 +72,6 @@ export class PgAgentThreadStore implements AgentThreadStore {
77
72
  const params: SqlParameter[] = [input.projectId]
78
73
  let index = 2
79
74
 
80
- if (input.agentId) {
81
- whereClauses.push(`agent_id = $${index++}`)
82
- params.push(input.agentId)
83
- }
84
-
85
- if (input.agentIds) {
86
- const placeholders = input.agentIds.map(() => `$${index++}`)
87
- whereClauses.push(`agent_id IN (${placeholders.join(", ")})`)
88
- params.push(...input.agentIds)
89
- }
90
-
91
75
  if (input.statuses) {
92
76
  const placeholders = input.statuses.map(() => `$${index++}`)
93
77
  whereClauses.push(`status IN (${placeholders.join(", ")})`)
@@ -0,0 +1,102 @@
1
+ import {
2
+ type AiModelCallGroupRow,
3
+ aiModelCallGroupFragmentFromRow,
4
+ buildAiModelCallGroupsFromFragments,
5
+ normalizeAiModelCallAccountingQuery,
6
+ } from "@sixb/core/internal/ai-cost-storage-provider"
7
+ import type { ListAiModelCallGroupsInput, ListAiModelCallGroupsResult } from "@sixb/core/storage"
8
+ import type { PgStoreClient } from "./transactions"
9
+
10
+ /** Paginate initiating executions, then aggregate their matching calls without loading usage payloads. */
11
+ export async function listAiModelCallGroups(
12
+ sql: PgStoreClient,
13
+ input: ListAiModelCallGroupsInput
14
+ ): Promise<ListAiModelCallGroupsResult> {
15
+ const query = normalizeAiModelCallAccountingQuery(input)
16
+ const parameters = [
17
+ query.projectId,
18
+ query.from.toISOString(),
19
+ query.to.toISOString(),
20
+ query.providerId ?? null,
21
+ query.modelId ?? null,
22
+ query.valuationStatus ?? null,
23
+ ]
24
+ const [count] = await sql.unsafe<{ total: string }[]>(COUNT_SQL, parameters)
25
+ const rows = await sql.unsafe<AiModelCallGroupRow[]>(PAGE_SQL, [
26
+ ...parameters,
27
+ query.limit,
28
+ query.offset,
29
+ ])
30
+ const total = Number(count?.total)
31
+ if (!Number.isSafeInteger(total) || total < 0)
32
+ throw new RangeError("[Sixb] Invalid model-call group count.")
33
+ const items = buildAiModelCallGroupsFromFragments(rows.map(aiModelCallGroupFragmentFromRow))
34
+ return { items, total, hasMore: query.offset + items.length < total }
35
+ }
36
+
37
+ const FILTERED_SQL = `
38
+ WITH filtered AS (
39
+ SELECT COALESCE(root_agent.execution_id, usage.execution_id) AS root_execution_id,
40
+ usage.execution_id, usage.provider_id, usage.requested_model_id, usage.occurred_at,
41
+ usage.total_tokens,
42
+ COALESCE(cost.status, 'unvalued') AS valuation_status,
43
+ cost.currency AS amount_currency, cost.amount_nanos,
44
+ root_agent.id AS root_agent_run_id,
45
+ root_agent.thread_id AS root_thread_id,
46
+ direct_agent.kind AS direct_kind,
47
+ direct_agent.id AS direct_run_id,
48
+ direct_agent.parent_run_id AS parent_run_id,
49
+ workflow_node.node_id AS workflow_agent_step_id,
50
+ workflow_agent.node_run_id AS workflow_node_run_id,
51
+ workflow_node.workflow_id AS workflow_id,
52
+ workflow_node.workflow_run_id AS workflow_run_id
53
+ FROM ai_model_call_usage AS usage
54
+ LEFT JOIN agent_runs AS direct_agent
55
+ ON direct_agent.project_id = usage.project_id AND direct_agent.execution_id = usage.execution_id
56
+ LEFT JOIN agent_runs AS root_agent
57
+ ON root_agent.project_id = usage.project_id
58
+ AND root_agent.id = COALESCE(direct_agent.parent_run_id, direct_agent.id)
59
+ LEFT JOIN workflow_agent_node_runs AS workflow_agent
60
+ ON workflow_agent.project_id = usage.project_id AND workflow_agent.execution_id = usage.execution_id
61
+ LEFT JOIN workflow_node_runs AS workflow_node
62
+ ON workflow_node.project_id = workflow_agent.project_id AND workflow_node.id = workflow_agent.node_run_id
63
+ LEFT JOIN ai_model_call_valuations AS cost
64
+ ON cost.project_id = usage.project_id AND cost.usage_record_id = usage.id
65
+ WHERE usage.project_id = $1
66
+ AND usage.occurred_at >= $2::timestamptz
67
+ AND usage.occurred_at < $3::timestamptz
68
+ AND ($4::text IS NULL OR usage.provider_id = $4)
69
+ AND ($5::text IS NULL OR usage.requested_model_id = $5)
70
+ AND ($6::text IS NULL OR COALESCE(cost.status, 'unvalued') = $6)
71
+ ), roots AS (
72
+ SELECT root_execution_id, MAX(occurred_at) AS last_call_at
73
+ FROM filtered GROUP BY root_execution_id
74
+ )
75
+ `
76
+ const COUNT_SQL = `${FILTERED_SQL} SELECT CAST(COUNT(*) AS TEXT) AS total FROM roots`
77
+ const PAGE_SQL = `${FILTERED_SQL}, page AS (
78
+ SELECT root_execution_id FROM roots
79
+ ORDER BY last_call_at DESC, root_execution_id COLLATE "C" ASC LIMIT $7 OFFSET $8
80
+ )
81
+ SELECT filtered.root_execution_id, execution_id, provider_id, requested_model_id,
82
+ MIN(occurred_at) AS first_call_at, MAX(occurred_at) AS last_call_at,
83
+ CAST(COUNT(*) AS TEXT) AS call_count,
84
+ CAST(COUNT(total_tokens) AS TEXT) AS token_count,
85
+ CAST(SUM(total_tokens) AS TEXT) AS total_tokens,
86
+ valuation_status, amount_currency,
87
+ CAST(SUM(amount_nanos / 1000000000000) AS TEXT) AS amount_high,
88
+ CAST(SUM((amount_nanos / 1000000) % 1000000) AS TEXT) AS amount_middle,
89
+ CAST(SUM(amount_nanos % 1000000) AS TEXT) AS amount_low,
90
+ MAX(root_agent_run_id) AS root_agent_run_id,
91
+ MAX(root_thread_id) AS root_thread_id,
92
+ MAX(direct_kind) AS direct_kind,
93
+ MAX(direct_run_id) AS direct_run_id,
94
+ MAX(parent_run_id) AS parent_run_id,
95
+ MAX(workflow_agent_step_id) AS workflow_agent_step_id,
96
+ MAX(workflow_node_run_id) AS workflow_node_run_id,
97
+ MAX(workflow_id) AS workflow_id,
98
+ MAX(workflow_run_id) AS workflow_run_id
99
+ FROM filtered JOIN page ON page.root_execution_id = filtered.root_execution_id
100
+ GROUP BY filtered.root_execution_id, execution_id, provider_id, requested_model_id,
101
+ valuation_status, amount_currency
102
+ `
@@ -1,8 +1,11 @@
1
1
  import type {
2
2
  AuthStorage,
3
+ CompleteDeviceAuthorizationInput,
4
+ CompleteDeviceAuthorizationResult,
3
5
  CompleteMagicLinkSignInInput,
4
6
  CompleteOidcSignInInput,
5
7
  CompleteSignInResult,
8
+ DeviceAuthorizationRecord,
6
9
  GroupMembershipRecord,
7
10
  InvitationRecord,
8
11
  SessionRecord,
@@ -19,6 +22,10 @@ import {
19
22
  runPgTransaction,
20
23
  } from "../transactions"
21
24
  import { PgAuthAccessTokenStore } from "./access-tokens"
25
+ import {
26
+ getPgDeviceAuthorizationById,
27
+ PgAuthDeviceAuthorizationStore,
28
+ } from "./device-authorizations"
22
29
  import { PgAuthGroupMembershipStore } from "./group-memberships"
23
30
  import { PgAuthUserIdentityStore } from "./identities"
24
31
  import { PgAuthInvitationStore } from "./invitations"
@@ -77,6 +84,7 @@ export class PgAuthStorage implements AuthStorage {
77
84
  readonly groupMemberships: PgAuthGroupMembershipStore
78
85
  readonly magicLinks: PgAuthMagicLinkStore
79
86
  readonly oidcAuthorizationAttempts: PgAuthOidcAuthorizationAttemptStore
87
+ readonly deviceAuthorizations: PgAuthDeviceAuthorizationStore
80
88
 
81
89
  constructor(options: PgAuthStorageOptions) {
82
90
  this.sql = options.sql
@@ -90,6 +98,43 @@ export class PgAuthStorage implements AuthStorage {
90
98
  this.groupMemberships = new PgAuthGroupMembershipStore(this.sql)
91
99
  this.magicLinks = new PgAuthMagicLinkStore(this.sql)
92
100
  this.oidcAuthorizationAttempts = new PgAuthOidcAuthorizationAttemptStore(this.sql)
101
+ this.deviceAuthorizations = new PgAuthDeviceAuthorizationStore(this.sql)
102
+ }
103
+
104
+ async completeDeviceAuthorization(
105
+ input: CompleteDeviceAuthorizationInput
106
+ ): Promise<CompleteDeviceAuthorizationResult> {
107
+ return runPgTransaction(this.sql, async (tx) => {
108
+ const authorization = await getPgDeviceAuthorizationById(tx, input.projectId, input.id)
109
+ if (!authorization) {
110
+ throw new AuthStorageError(
111
+ "missing_device_authorization",
112
+ `[Sixb] Device authorization '${input.id}' not found for project '${input.projectId}'.`
113
+ )
114
+ }
115
+ assertCompletableDeviceAuthorization(authorization, input)
116
+ const accessToken = await new PgAuthAccessTokenStore(tx).create(input.accessToken)
117
+ const [consumed] = await tx`
118
+ UPDATE auth_device_authorizations
119
+ SET status = 'consumed', consumed_at = ${input.completedAt}
120
+ WHERE project_id = ${input.projectId} AND id = ${input.id} AND status = 'approved'
121
+ RETURNING *
122
+ `
123
+ if (!consumed) {
124
+ throw new AuthStorageError(
125
+ "invalid_device_authorization",
126
+ "[Sixb] Device authorization was already consumed."
127
+ )
128
+ }
129
+ return {
130
+ authorization: {
131
+ ...authorization,
132
+ status: "consumed" as const,
133
+ consumedAt: new Date(input.completedAt),
134
+ },
135
+ accessToken,
136
+ }
137
+ })
93
138
  }
94
139
 
95
140
  async completeMagicLinkSignIn(
@@ -796,3 +841,42 @@ function assertSignInSessionAudience(
796
841
  `[Sixb] Sign-in session audience '${sessionAudience}' does not match stored auth audience '${storedAudience}' for project '${projectId}'.`
797
842
  )
798
843
  }
844
+
845
+ function assertCompletableDeviceAuthorization(
846
+ authorization: DeviceAuthorizationRecord,
847
+ input: CompleteDeviceAuthorizationInput
848
+ ): void {
849
+ if (authorization.deviceCodeHash !== input.deviceCodeHash) {
850
+ throw new AuthStorageError(
851
+ "invalid_device_authorization",
852
+ "[Sixb] Invalid device authorization."
853
+ )
854
+ }
855
+ if (authorization.status === "pending") {
856
+ throw new AuthStorageError(
857
+ "pending_device_authorization",
858
+ "[Sixb] Device authorization is pending."
859
+ )
860
+ }
861
+ if (authorization.status === "denied") {
862
+ throw new AuthStorageError(
863
+ "device_authorization_denied",
864
+ "[Sixb] Device authorization was denied."
865
+ )
866
+ }
867
+ if (
868
+ authorization.status !== "approved" ||
869
+ authorization.expiresAt <= input.completedAt ||
870
+ !authorization.approvedUserId ||
871
+ !authorization.approvedSessionId ||
872
+ input.accessToken.projectId !== input.projectId ||
873
+ input.accessToken.subjectType !== "user" ||
874
+ input.accessToken.subjectId !== authorization.approvedUserId ||
875
+ input.accessToken.createdBySessionId !== authorization.approvedSessionId
876
+ ) {
877
+ throw new AuthStorageError(
878
+ "invalid_device_authorization",
879
+ "[Sixb] Device authorization is invalid."
880
+ )
881
+ }
882
+ }
@@ -0,0 +1,131 @@
1
+ import type {
2
+ AuthDeviceAuthorizationStore,
3
+ CreateDeviceAuthorizationInput,
4
+ DeviceAuthorizationRecord,
5
+ } from "@sixb/core/storage"
6
+ import { AuthStorageError, MAX_PENDING_DEVICE_AUTHORIZATIONS } from "@sixb/core/storage"
7
+ import {
8
+ authLockKey,
9
+ lockAdvisoryKeys,
10
+ type PgStoreClient,
11
+ runPgTransaction,
12
+ } from "../transactions"
13
+ import type { PgAuthDeviceAuthorizationRow } from "./rows"
14
+ import { rowToDeviceAuthorizationRecord } from "./rows"
15
+ import { assertNonEmpty, mapUniqueConstraintError } from "./shared"
16
+
17
+ export class PgAuthDeviceAuthorizationStore implements AuthDeviceAuthorizationStore {
18
+ constructor(private readonly sql: PgStoreClient) {}
19
+
20
+ async create(input: CreateDeviceAuthorizationInput): Promise<DeviceAuthorizationRecord> {
21
+ return runPgTransaction(this.sql, async (tx) => {
22
+ await lockAdvisoryKeys(tx, [authLockKey("device-authorizations", input.projectId)])
23
+ await tx`
24
+ DELETE FROM auth_device_authorizations
25
+ WHERE project_id = ${input.projectId} AND expires_at <= ${input.createdAt}
26
+ `
27
+ const [pending] = await tx<{ readonly count: string | number }[]>`
28
+ SELECT COUNT(*)::bigint AS count FROM auth_device_authorizations
29
+ WHERE project_id = ${input.projectId} AND status = 'pending'
30
+ `
31
+ if (Number(pending?.count ?? 0) >= MAX_PENDING_DEVICE_AUTHORIZATIONS) {
32
+ throw new AuthStorageError(
33
+ "device_authorization_limit_exceeded",
34
+ `[Sixb] Project '${input.projectId}' has too many pending device authorizations.`
35
+ )
36
+ }
37
+ try {
38
+ const [row] = await tx<PgAuthDeviceAuthorizationRow[]>`
39
+ INSERT INTO auth_device_authorizations (
40
+ project_id, id, device_code_hash, user_code, client_name, token_name,
41
+ token_expires_at, status, created_at, expires_at
42
+ ) VALUES (
43
+ ${assertNonEmpty(input.projectId, "Project id")},
44
+ ${assertNonEmpty(input.id, "Device authorization id")},
45
+ ${assertNonEmpty(input.deviceCodeHash, "Device code hash")},
46
+ ${assertNonEmpty(input.userCode, "User code")},
47
+ ${assertNonEmpty(input.clientName, "Client name")},
48
+ ${assertNonEmpty(input.tokenName, "Token name")},
49
+ ${input.tokenExpiresAt}, 'pending', ${input.createdAt}, ${input.expiresAt}
50
+ ) RETURNING *
51
+ `
52
+ return rowToDeviceAuthorizationRecord(row)
53
+ } catch (error) {
54
+ mapUniqueConstraintError(
55
+ error,
56
+ "duplicate_device_authorization",
57
+ `[Sixb] Device authorization '${input.id}' already exists for project '${input.projectId}'.`
58
+ )
59
+ }
60
+ })
61
+ }
62
+
63
+ async getById(params: { readonly projectId: string; readonly id: string }) {
64
+ return getPgDeviceAuthorizationById(this.sql, params.projectId, params.id)
65
+ }
66
+
67
+ async getByUserCode(params: { readonly projectId: string; readonly userCode: string }) {
68
+ const [row] = await this.sql<PgAuthDeviceAuthorizationRow[]>`
69
+ SELECT * FROM auth_device_authorizations
70
+ WHERE project_id = ${params.projectId} AND user_code = ${params.userCode}
71
+ `
72
+ return row ? rowToDeviceAuthorizationRecord(row) : null
73
+ }
74
+
75
+ async approve(params: {
76
+ readonly projectId: string
77
+ readonly id: string
78
+ readonly userId: string
79
+ readonly sessionId: string
80
+ readonly approvedAt: Date
81
+ }) {
82
+ const [row] = await this.sql<PgAuthDeviceAuthorizationRow[]>`
83
+ UPDATE auth_device_authorizations
84
+ SET status = 'approved', approved_user_id = ${params.userId},
85
+ approved_session_id = ${params.sessionId}, approved_at = ${params.approvedAt}
86
+ WHERE project_id = ${params.projectId} AND id = ${params.id}
87
+ AND status = 'pending' AND expires_at > ${params.approvedAt}
88
+ AND EXISTS (
89
+ SELECT 1 FROM auth_sessions
90
+ WHERE project_id = ${params.projectId} AND id = ${params.sessionId}
91
+ AND user_id = ${params.userId} AND revoked_at IS NULL
92
+ AND expires_at > ${params.approvedAt}
93
+ AND (absolute_expires_at IS NULL OR absolute_expires_at > ${params.approvedAt})
94
+ )
95
+ RETURNING *
96
+ `
97
+ if (!row) throw invalidTransition(params.id, params.projectId)
98
+ return rowToDeviceAuthorizationRecord(row)
99
+ }
100
+
101
+ async deny(params: { readonly projectId: string; readonly id: string; readonly deniedAt: Date }) {
102
+ const [row] = await this.sql<PgAuthDeviceAuthorizationRow[]>`
103
+ UPDATE auth_device_authorizations
104
+ SET status = 'denied', denied_at = ${params.deniedAt}
105
+ WHERE project_id = ${params.projectId} AND id = ${params.id}
106
+ AND status = 'pending' AND expires_at > ${params.deniedAt}
107
+ RETURNING *
108
+ `
109
+ if (!row) throw invalidTransition(params.id, params.projectId)
110
+ return rowToDeviceAuthorizationRecord(row)
111
+ }
112
+ }
113
+
114
+ export async function getPgDeviceAuthorizationById(
115
+ sql: PgStoreClient,
116
+ projectId: string,
117
+ id: string
118
+ ) {
119
+ const [row] = await sql<PgAuthDeviceAuthorizationRow[]>`
120
+ SELECT * FROM auth_device_authorizations
121
+ WHERE project_id = ${projectId} AND id = ${id}
122
+ `
123
+ return row ? rowToDeviceAuthorizationRecord(row) : null
124
+ }
125
+
126
+ function invalidTransition(id: string, projectId: string): AuthStorageError {
127
+ return new AuthStorageError(
128
+ "invalid_device_authorization",
129
+ `[Sixb] Device authorization '${id}' is no longer pending for project '${projectId}'.`
130
+ )
131
+ }