@opengeni/db 0.28.1 → 0.31.1

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 (187) hide show
  1. package/dist/attached-browser-devices.d.ts +42 -0
  2. package/dist/attempt-tool-catalogs.d.ts +16 -0
  3. package/dist/browser-auth.d.ts +91 -0
  4. package/dist/browser-identities.d.ts +128 -0
  5. package/dist/browser-sessions.d.ts +272 -0
  6. package/dist/browser-state-artifacts.d.ts +17 -0
  7. package/dist/capability-components.d.ts +37 -0
  8. package/dist/capability-integrations.d.ts +192 -0
  9. package/dist/chunk-F5FT7BEZ.js +3535 -0
  10. package/dist/chunk-F5FT7BEZ.js.map +1 -0
  11. package/dist/chunk-ISI2TIUG.js +639 -0
  12. package/dist/chunk-ISI2TIUG.js.map +1 -0
  13. package/dist/chunk-MWDVXQOL.js +11639 -0
  14. package/dist/chunk-MWDVXQOL.js.map +1 -0
  15. package/dist/chunk-NBE6CHRI.js +955 -0
  16. package/dist/chunk-NBE6CHRI.js.map +1 -0
  17. package/dist/{chunk-DBS5HPEW.js → chunk-YHZXQ7HP.js} +760 -11
  18. package/dist/chunk-YHZXQ7HP.js.map +1 -0
  19. package/dist/codemode-operations.d.ts +117 -0
  20. package/dist/codex-token-resolver.d.ts +2 -2
  21. package/dist/company-profile-schema.d.ts +798 -0
  22. package/dist/company-profile.d.ts +268 -0
  23. package/dist/computer-sessions.d.ts +109 -0
  24. package/dist/connection-token-resolver.d.ts +9 -4
  25. package/dist/database.d.ts +52 -3
  26. package/dist/editable-artifact-durable-export.d.ts +153 -0
  27. package/dist/editable-artifact-durable-export.js +788 -0
  28. package/dist/editable-artifact-durable-export.js.map +1 -0
  29. package/dist/editable-artifact-materialization.d.ts +123 -0
  30. package/dist/editable-artifacts-schema.d.ts +4152 -0
  31. package/dist/editable-artifacts.d.ts +610 -0
  32. package/dist/editable-artifacts.js +26 -0
  33. package/dist/editable-artifacts.js.map +1 -0
  34. package/dist/environment-crypto.d.ts +8 -4
  35. package/dist/generated-images.d.ts +139 -0
  36. package/dist/index.d.ts +958 -126
  37. package/dist/index.js +31204 -12172
  38. package/dist/index.js.map +1 -1
  39. package/dist/insights.d.ts +51 -4
  40. package/dist/integration-bindings.d.ts +70 -0
  41. package/dist/integration-features.d.ts +99 -0
  42. package/dist/interaction-revisions.d.ts +3 -0
  43. package/dist/interaction-schema.d.ts +5372 -0
  44. package/dist/knowledge-source-sync-schema.d.ts +1679 -0
  45. package/dist/knowledge-source-sync.d.ts +274 -0
  46. package/dist/lossless-columns.d.ts +58 -0
  47. package/dist/lossless-json.d.ts +39 -0
  48. package/dist/memory-domain.d.ts +3 -6
  49. package/dist/memory-governance.d.ts +2 -2
  50. package/dist/memory-slack-delivery.d.ts +101 -0
  51. package/dist/new-session-drafts.d.ts +2 -2
  52. package/dist/pack-components.d.ts +76 -0
  53. package/dist/pack-installations.d.ts +86 -0
  54. package/dist/persistence-errors.d.ts +7 -14
  55. package/dist/plugin-packages.d.ts +126 -0
  56. package/dist/preference-registry.d.ts +1 -1
  57. package/dist/provision-roles.d.ts +9 -1
  58. package/dist/provision-roles.js +1 -1
  59. package/dist/runtime-posture.d.ts +18 -6
  60. package/dist/schema.d.ts +19122 -8731
  61. package/dist/schema.js +173 -1
  62. package/dist/scoped-knowledge.d.ts +97 -1
  63. package/dist/session-control.d.ts +4 -4
  64. package/dist/session-queue-commands.d.ts +19 -11
  65. package/dist/session-realtime-context.d.ts +2 -2
  66. package/dist/session-realtime-ledger.d.ts +4 -4
  67. package/dist/session-realtime-mirror.d.ts +2 -2
  68. package/dist/session-realtime-state.d.ts +1 -1
  69. package/dist/session-realtime-terminal.d.ts +8 -1
  70. package/dist/session-realtime.d.ts +7 -7
  71. package/dist/session-tool-call-settlement.d.ts +1 -1
  72. package/dist/slack-user-link-access.d.ts +54 -0
  73. package/dist/transcription-recordings-schema.d.ts +44 -6
  74. package/dist/transcription-recordings.d.ts +3 -3
  75. package/dist/turn-initiator.d.ts +2 -2
  76. package/dist/video-generation.d.ts +216 -0
  77. package/dist/video-generation.js +66 -0
  78. package/dist/video-generation.js.map +1 -0
  79. package/dist/workspace-artifacts.d.ts +1 -1
  80. package/dist/workspace-instruction-policies.d.ts +1 -1
  81. package/dist/workspace-learning-policy-schema.d.ts +932 -0
  82. package/dist/workspace-learning-policy.d.ts +161 -0
  83. package/drizzle/0065_enrollment_credential_generation.sql +10 -0
  84. package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
  85. package/drizzle/0176_lossless_canonical_json.sql +975 -0
  86. package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
  87. package/drizzle/0178_permissioned_secret_reads.sql +14 -0
  88. package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
  89. package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
  90. package/drizzle/0181_connected_machine_removal.sql +52 -0
  91. package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
  92. package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
  93. package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
  94. package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
  95. package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
  96. package/drizzle/0187_generated_image_artifacts.sql +173 -0
  97. package/drizzle/0188_image_generation_retention_failure.sql +25 -0
  98. package/drizzle/0189_retained_session_image_formats.sql +13 -0
  99. package/drizzle/0190_timeline_annotations.sql +31 -0
  100. package/drizzle/0191_editable_artifact_engine.sql +3853 -0
  101. package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
  102. package/drizzle/0193_editable_artifact_authorization.sql +190 -0
  103. package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
  104. package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
  105. package/drizzle/0196_rig_provider_images.sql +152 -0
  106. package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
  107. package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
  108. package/drizzle/0199_workspace_learning_policy.sql +980 -0
  109. package/drizzle/0201_company_profile_authority.sql +849 -0
  110. package/drizzle/0202_document_index_checkpoints.sql +85 -0
  111. package/drizzle/0203_durable_video_generation.sql +353 -0
  112. package/drizzle/0204_video_generation_funding.sql +44 -0
  113. package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
  114. package/drizzle/0206_browser_sessions.sql +646 -0
  115. package/drizzle/0207_browser_identities.sql +379 -0
  116. package/drizzle/0208_attached_browser_devices.sql +165 -0
  117. package/drizzle/0209_computer_sessions.sql +575 -0
  118. package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
  119. package/drizzle/0211_editable_artifact_session_links.sql +58 -0
  120. package/drizzle/0212_slack_installation_bindings.sql +348 -0
  121. package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
  122. package/drizzle/0214_session_activity_commit_gate.sql +262 -0
  123. package/drizzle/0215_capabilities_platform.sql +980 -0
  124. package/drizzle/0216_pack_component_ownership.sql +235 -0
  125. package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
  126. package/package.json +18 -5
  127. package/src/attached-browser-devices.ts +371 -0
  128. package/src/attempt-tool-catalogs.ts +131 -0
  129. package/src/browser-auth.ts +1896 -0
  130. package/src/browser-identities.ts +1168 -0
  131. package/src/browser-sessions.ts +2005 -0
  132. package/src/browser-state-artifacts.ts +59 -0
  133. package/src/capability-components.ts +166 -0
  134. package/src/capability-integrations.ts +1663 -0
  135. package/src/codemode-operations.ts +674 -0
  136. package/src/company-profile-schema.ts +143 -0
  137. package/src/company-profile.ts +968 -0
  138. package/src/computer-sessions.ts +1130 -0
  139. package/src/connection-token-resolver.ts +270 -25
  140. package/src/database.ts +434 -24
  141. package/src/editable-artifact-durable-export.ts +1201 -0
  142. package/src/editable-artifact-materialization.ts +893 -0
  143. package/src/editable-artifacts-schema.ts +879 -0
  144. package/src/editable-artifacts.ts +5404 -0
  145. package/src/environment-crypto.ts +22 -9
  146. package/src/generated-images.ts +740 -0
  147. package/src/index.ts +10117 -2730
  148. package/src/insights.ts +160 -13
  149. package/src/integration-bindings.ts +455 -0
  150. package/src/integration-features.ts +859 -0
  151. package/src/interaction-revisions.ts +40 -0
  152. package/src/interaction-schema.ts +1303 -0
  153. package/src/knowledge-source-sync-schema.ts +192 -0
  154. package/src/knowledge-source-sync.ts +1922 -0
  155. package/src/lossless-columns.ts +35 -0
  156. package/src/lossless-json.ts +322 -0
  157. package/src/memory-domain.ts +5 -44
  158. package/src/memory-slack-delivery.ts +801 -0
  159. package/src/pack-components.ts +954 -0
  160. package/src/pack-installations.ts +929 -0
  161. package/src/persistence-errors.ts +26 -34
  162. package/src/plugin-packages.ts +1186 -0
  163. package/src/provision-roles.ts +302 -1
  164. package/src/runtime-posture-cli.ts +9 -0
  165. package/src/runtime-posture.ts +514 -5
  166. package/src/schema.ts +5177 -2957
  167. package/src/scoped-knowledge-schema.ts +1 -1
  168. package/src/scoped-knowledge.ts +655 -3
  169. package/src/session-control.ts +133 -81
  170. package/src/session-queue-commands.ts +511 -247
  171. package/src/session-realtime-context.ts +20 -7
  172. package/src/session-realtime-ledger.ts +32 -10
  173. package/src/session-realtime-mirror.ts +4 -2
  174. package/src/session-realtime-terminal.ts +89 -21
  175. package/src/session-realtime.ts +10 -9
  176. package/src/session-tool-call-settlement.ts +38 -15
  177. package/src/slack-user-link-access.ts +702 -0
  178. package/src/transcription-recordings-schema.ts +5 -2
  179. package/src/transcription-recordings.ts +70 -40
  180. package/src/video-generation.ts +1452 -0
  181. package/src/workspace-learning-policy-schema.ts +139 -0
  182. package/src/workspace-learning-policy.ts +595 -0
  183. package/dist/chunk-DBS5HPEW.js.map +0 -1
  184. package/dist/chunk-HT5T62CC.js +0 -6803
  185. package/dist/chunk-HT5T62CC.js.map +0 -1
  186. package/dist/event-payload-sanitizer.d.ts +0 -32
  187. package/src/event-payload-sanitizer.ts +0 -377
@@ -0,0 +1,1896 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ AuthRun,
4
+ AuthRunListResponse,
5
+ AuthRunMutationResponse,
6
+ CreateInteractionInterventionRequest,
7
+ CreateNetworkRouteRequest,
8
+ CreateSiteAuthConnectionRequest,
9
+ InteractionIntervention,
10
+ InteractionInterventionListResponse,
11
+ InteractionInterventionMutationResponse,
12
+ NetworkRoute,
13
+ NetworkRouteConfiguration,
14
+ NetworkRouteConsistency,
15
+ NetworkRouteListResponse,
16
+ NetworkRouteMutationResponse,
17
+ ReportAuthRunRequest,
18
+ ResolveInteractionInterventionRequest,
19
+ SiteAuthConnection,
20
+ SiteAuthConnectionListResponse,
21
+ SiteAuthConnectionMutationResponse,
22
+ StartAuthRunRequest,
23
+ UpdateNetworkRouteRequest,
24
+ UpdateSiteAuthConnectionRequest,
25
+ type AuthRun as AuthRunValue,
26
+ type AuthRunListResponse as AuthRunListResponseValue,
27
+ type AuthRunMutationResponse as AuthRunMutationResponseValue,
28
+ type CreateInteractionInterventionRequest as CreateInteractionInterventionRequestValue,
29
+ type CreateNetworkRouteRequest as CreateNetworkRouteRequestValue,
30
+ type CreateSiteAuthConnectionRequest as CreateSiteAuthConnectionRequestValue,
31
+ type InteractionCredentialAuthorityRef,
32
+ type InteractionIntervention as InteractionInterventionValue,
33
+ type InteractionInterventionListResponse as InteractionInterventionListResponseValue,
34
+ type InteractionInterventionMutationResponse as InteractionInterventionMutationResponseValue,
35
+ type NetworkRoute as NetworkRouteValue,
36
+ type NetworkRouteConfiguration as NetworkRouteConfigurationValue,
37
+ type NetworkRouteListResponse as NetworkRouteListResponseValue,
38
+ type NetworkRouteMutationResponse as NetworkRouteMutationResponseValue,
39
+ type ReportAuthRunRequest as ReportAuthRunRequestValue,
40
+ type ResolveInteractionInterventionRequest as ResolveInteractionInterventionRequestValue,
41
+ type SiteAuthAuthority,
42
+ type SiteAuthConnection as SiteAuthConnectionValue,
43
+ type SiteAuthConnectionListResponse as SiteAuthConnectionListResponseValue,
44
+ type SiteAuthConnectionMutationResponse as SiteAuthConnectionMutationResponseValue,
45
+ type StartAuthRunRequest as StartAuthRunRequestValue,
46
+ type UpdateNetworkRouteRequest as UpdateNetworkRouteRequestValue,
47
+ type UpdateSiteAuthConnectionRequest as UpdateSiteAuthConnectionRequestValue,
48
+ } from "@opengeni/contracts";
49
+ import { and, asc, eq, inArray, sql } from "drizzle-orm";
50
+ import { type Database, setSubjectRlsContext, withRlsContext } from "./database";
51
+ import {
52
+ advanceWorkspaceInteractionRevision,
53
+ readWorkspaceInteractionRevision,
54
+ } from "./interaction-revisions";
55
+ import { safeDatabaseErrorFacts } from "./persistence-errors";
56
+ import * as schema from "./schema";
57
+
58
+ type NetworkRouteRow = typeof schema.networkRoutes.$inferSelect;
59
+ type SiteAuthConnectionRow = typeof schema.siteAuthConnections.$inferSelect;
60
+ type AuthRunRow = typeof schema.authRuns.$inferSelect;
61
+ type InterventionRow = typeof schema.interactionInterventions.$inferSelect;
62
+ type ResourceOperationRow = typeof schema.interactionResourceOperations.$inferSelect;
63
+
64
+ type InteractionMutationScope = {
65
+ accountId: string;
66
+ workspaceId: string;
67
+ actorSubjectId: string;
68
+ };
69
+
70
+ type ResourceKind = ResourceOperationRow["resourceKind"];
71
+ type ResourceOperationKind = ResourceOperationRow["kind"];
72
+
73
+ const CONSISTENT_READ = {
74
+ isolationLevel: "repeatable read",
75
+ accessMode: "read only",
76
+ } as const;
77
+
78
+ export class InteractionResourceNotFoundError extends Error {
79
+ readonly name = "InteractionResourceNotFoundError";
80
+ }
81
+
82
+ export class InteractionResourceConflictError extends Error {
83
+ readonly name = "InteractionResourceConflictError";
84
+ }
85
+
86
+ export class InteractionResourceStateError extends Error {
87
+ readonly name = "InteractionResourceStateError";
88
+ }
89
+
90
+ function iso(value: Date): string {
91
+ return value.toISOString();
92
+ }
93
+
94
+ function canonicalJsonValue(value: unknown): unknown {
95
+ if (Array.isArray(value)) return value.map(canonicalJsonValue);
96
+ if (!value || typeof value !== "object") return value;
97
+ return Object.fromEntries(
98
+ Object.entries(value as Record<string, unknown>)
99
+ .filter(([, entry]) => entry !== undefined)
100
+ .sort(([left], [right]) => left.localeCompare(right))
101
+ .map(([key, entry]) => [key, canonicalJsonValue(entry)]),
102
+ );
103
+ }
104
+
105
+ function operationDigest(value: Record<string, unknown>): string {
106
+ return createHash("sha256")
107
+ .update(JSON.stringify(canonicalJsonValue(value)))
108
+ .digest("hex");
109
+ }
110
+
111
+ function postgresConstraint(error: unknown): string | null {
112
+ return safeDatabaseErrorFacts(error).constraint ?? null;
113
+ }
114
+
115
+ function routeFromRow(row: NetworkRouteRow): NetworkRouteValue {
116
+ return NetworkRoute.parse({
117
+ id: row.id,
118
+ accountId: row.accountId,
119
+ workspaceId: row.workspaceId,
120
+ name: row.name,
121
+ status: row.status,
122
+ configuration: row.configuration,
123
+ consistency: row.consistency,
124
+ version: row.version,
125
+ createdBySubjectId: row.createdBySubjectId,
126
+ createdAt: iso(row.createdAt),
127
+ updatedAt: iso(row.updatedAt),
128
+ });
129
+ }
130
+
131
+ function siteAuthConnectionFromRow(row: SiteAuthConnectionRow): SiteAuthConnectionValue {
132
+ return SiteAuthConnection.parse({
133
+ id: row.id,
134
+ accountId: row.accountId,
135
+ workspaceId: row.workspaceId,
136
+ name: row.name,
137
+ accountLabel: row.accountLabel,
138
+ status: row.status,
139
+ origins: row.origins,
140
+ loginUrl: row.loginUrl,
141
+ verificationUrlPrefixes: row.verificationUrlPrefixes,
142
+ authorities: row.authorities,
143
+ methods: row.methods,
144
+ preferredIdentityId: row.preferredIdentityId,
145
+ preferredPlacement: row.preferredPlacement,
146
+ preferredNetworkRouteId: row.preferredNetworkRouteId,
147
+ healthPolicy: row.healthPolicy,
148
+ verificationState: row.verificationState,
149
+ lastVerifiedAt: row.lastVerifiedAt ? iso(row.lastVerifiedAt) : null,
150
+ lastVerifiedUrl: row.lastVerifiedUrl,
151
+ repairCode: row.repairCode,
152
+ version: row.version,
153
+ createdBySubjectId: row.createdBySubjectId,
154
+ createdAt: iso(row.createdAt),
155
+ updatedAt: iso(row.updatedAt),
156
+ });
157
+ }
158
+
159
+ async function lockOperation(db: Database, operationId: string): Promise<void> {
160
+ await db.execute(
161
+ sql`select pg_advisory_xact_lock(hashtextextended(${`interaction-resource:${operationId}`}, 0))`,
162
+ );
163
+ }
164
+
165
+ async function loadOperation(
166
+ db: Database,
167
+ workspaceId: string,
168
+ operationId: string,
169
+ ): Promise<ResourceOperationRow | null> {
170
+ const [row] = await db
171
+ .select()
172
+ .from(schema.interactionResourceOperations)
173
+ .where(
174
+ and(
175
+ eq(schema.interactionResourceOperations.workspaceId, workspaceId),
176
+ eq(schema.interactionResourceOperations.operationId, operationId),
177
+ ),
178
+ )
179
+ .limit(1);
180
+ return row ?? null;
181
+ }
182
+
183
+ function assertOperation(
184
+ operation: ResourceOperationRow,
185
+ expected: {
186
+ resourceKind: ResourceKind;
187
+ resourceId?: string;
188
+ kind: ResourceOperationKind;
189
+ requestDigest: string;
190
+ actorSubjectId: string;
191
+ },
192
+ ): void {
193
+ if (
194
+ operation.resourceKind !== expected.resourceKind ||
195
+ (expected.resourceId !== undefined && operation.resourceId !== expected.resourceId) ||
196
+ operation.kind !== expected.kind ||
197
+ operation.requestDigest !== expected.requestDigest ||
198
+ operation.actorSubjectId !== expected.actorSubjectId
199
+ ) {
200
+ throw new InteractionResourceConflictError(
201
+ "Operation id is already bound to another interaction request",
202
+ );
203
+ }
204
+ if (operation.state !== "completed" || !operation.result || !operation.resultVersion) {
205
+ throw new InteractionResourceStateError("Interaction operation is not complete");
206
+ }
207
+ }
208
+
209
+ async function insertCompletedOperation(
210
+ db: Database,
211
+ input: {
212
+ operationId: string;
213
+ accountId: string;
214
+ workspaceId: string;
215
+ resourceKind: ResourceKind;
216
+ resourceId: string;
217
+ kind: ResourceOperationKind;
218
+ requestDigest: string;
219
+ resultVersion: number;
220
+ result: Record<string, unknown>;
221
+ actorSubjectId: string;
222
+ },
223
+ ): Promise<void> {
224
+ await db.insert(schema.interactionResourceOperations).values({
225
+ ...input,
226
+ state: "completed",
227
+ settledAt: sql`now()`,
228
+ });
229
+ }
230
+
231
+ async function assertWorkspaceAccount(
232
+ db: Database,
233
+ accountId: string,
234
+ workspaceId: string,
235
+ ): Promise<void> {
236
+ const [workspace] = await db
237
+ .select({ id: schema.workspaces.id })
238
+ .from(schema.workspaces)
239
+ .where(and(eq(schema.workspaces.id, workspaceId), eq(schema.workspaces.accountId, accountId)))
240
+ .limit(1);
241
+ if (!workspace) throw new InteractionResourceNotFoundError("Workspace not found");
242
+ }
243
+
244
+ function credentialRefsFromRoute(
245
+ configuration: NetworkRouteConfigurationValue,
246
+ ): InteractionCredentialAuthorityRef[] {
247
+ return "credential" in configuration && configuration.credential
248
+ ? [configuration.credential]
249
+ : [];
250
+ }
251
+
252
+ function credentialRefsFromAuthorities(
253
+ authorities: readonly SiteAuthAuthority[],
254
+ ): InteractionCredentialAuthorityRef[] {
255
+ return authorities.flatMap((authority) =>
256
+ "credential" in authority && authority.credential ? [authority.credential] : [],
257
+ );
258
+ }
259
+
260
+ function credentialRefKey(reference: InteractionCredentialAuthorityRef): string {
261
+ return `${reference.connectionId}\u0000${reference.connectionSubjectId ?? ""}\u0000${reference.providerDomain}`;
262
+ }
263
+
264
+ function introducedCredentialRefs(
265
+ current: readonly InteractionCredentialAuthorityRef[],
266
+ candidate: readonly InteractionCredentialAuthorityRef[],
267
+ ): InteractionCredentialAuthorityRef[] {
268
+ const trusted = new Set(current.map(credentialRefKey));
269
+ return candidate.filter((reference) => !trusted.has(credentialRefKey(reference)));
270
+ }
271
+
272
+ async function assertCredentialAuthorities(
273
+ db: Database,
274
+ workspaceId: string,
275
+ refs: readonly InteractionCredentialAuthorityRef[],
276
+ ): Promise<void> {
277
+ if (refs.length === 0) return;
278
+ const ids = [...new Set(refs.map((reference) => reference.connectionId))];
279
+ const rows = await db
280
+ .select({
281
+ id: schema.connections.id,
282
+ subjectId: schema.connections.subjectId,
283
+ providerDomain: schema.connections.providerDomain,
284
+ status: schema.connections.status,
285
+ })
286
+ .from(schema.connections)
287
+ .where(
288
+ and(eq(schema.connections.workspaceId, workspaceId), inArray(schema.connections.id, ids)),
289
+ );
290
+ const byId = new Map(rows.map((row) => [row.id, row]));
291
+ for (const reference of refs) {
292
+ const row = byId.get(reference.connectionId);
293
+ if (
294
+ !row ||
295
+ row.subjectId !== reference.connectionSubjectId ||
296
+ row.providerDomain !== reference.providerDomain
297
+ ) {
298
+ throw new InteractionResourceNotFoundError("Credential authority not found");
299
+ }
300
+ if (row.status !== "active") {
301
+ throw new InteractionResourceStateError("Credential authority is not active");
302
+ }
303
+ }
304
+ }
305
+
306
+ async function assertActiveBrowserIdentity(
307
+ db: Database,
308
+ workspaceId: string,
309
+ identityId: string | null,
310
+ ): Promise<void> {
311
+ if (!identityId) return;
312
+ const [identity] = await db
313
+ .select({ status: schema.browserIdentities.status })
314
+ .from(schema.browserIdentities)
315
+ .where(
316
+ and(
317
+ eq(schema.browserIdentities.workspaceId, workspaceId),
318
+ eq(schema.browserIdentities.id, identityId),
319
+ ),
320
+ )
321
+ .limit(1);
322
+ if (!identity) throw new InteractionResourceNotFoundError("Preferred browser identity not found");
323
+ if (identity.status !== "active") {
324
+ throw new InteractionResourceStateError("Preferred browser identity is archived");
325
+ }
326
+ }
327
+
328
+ async function assertActiveNetworkRoute(
329
+ db: Database,
330
+ workspaceId: string,
331
+ routeId: string | null,
332
+ ): Promise<void> {
333
+ if (!routeId) return;
334
+ const [route] = await db
335
+ .select({ status: schema.networkRoutes.status })
336
+ .from(schema.networkRoutes)
337
+ .where(
338
+ and(eq(schema.networkRoutes.workspaceId, workspaceId), eq(schema.networkRoutes.id, routeId)),
339
+ )
340
+ .limit(1);
341
+ if (!route) throw new InteractionResourceNotFoundError("Preferred network route not found");
342
+ if (route.status !== "active") {
343
+ throw new InteractionResourceStateError("Preferred network route is archived");
344
+ }
345
+ }
346
+
347
+ export async function listNetworkRoutes(
348
+ db: Database,
349
+ input: { accountId: string; workspaceId: string; includeArchived?: boolean },
350
+ ): Promise<NetworkRouteListResponseValue> {
351
+ return await withRlsContext(
352
+ db,
353
+ input,
354
+ async (scopedDb) => {
355
+ const predicate = input.includeArchived
356
+ ? eq(schema.networkRoutes.workspaceId, input.workspaceId)
357
+ : and(
358
+ eq(schema.networkRoutes.workspaceId, input.workspaceId),
359
+ eq(schema.networkRoutes.status, "active"),
360
+ );
361
+ const routes = await scopedDb
362
+ .select()
363
+ .from(schema.networkRoutes)
364
+ .where(predicate)
365
+ .orderBy(asc(schema.networkRoutes.name), asc(schema.networkRoutes.id));
366
+ return NetworkRouteListResponse.parse({
367
+ revision: await readWorkspaceInteractionRevision(scopedDb, input.workspaceId),
368
+ routes: routes.map(routeFromRow),
369
+ });
370
+ },
371
+ CONSISTENT_READ,
372
+ );
373
+ }
374
+
375
+ export async function getNetworkRoute(
376
+ db: Database,
377
+ input: { accountId: string; workspaceId: string; routeId: string },
378
+ ): Promise<NetworkRouteValue> {
379
+ return await withRlsContext(
380
+ db,
381
+ input,
382
+ async (scopedDb) => {
383
+ const [row] = await scopedDb
384
+ .select()
385
+ .from(schema.networkRoutes)
386
+ .where(
387
+ and(
388
+ eq(schema.networkRoutes.workspaceId, input.workspaceId),
389
+ eq(schema.networkRoutes.id, input.routeId),
390
+ ),
391
+ )
392
+ .limit(1);
393
+ if (!row) throw new InteractionResourceNotFoundError("Network route not found");
394
+ return routeFromRow(row);
395
+ },
396
+ CONSISTENT_READ,
397
+ );
398
+ }
399
+
400
+ export async function createNetworkRoute(
401
+ db: Database,
402
+ input: InteractionMutationScope & CreateNetworkRouteRequestValue,
403
+ ): Promise<NetworkRouteMutationResponseValue> {
404
+ const request = CreateNetworkRouteRequest.parse({
405
+ operationId: input.operationId,
406
+ name: input.name,
407
+ configuration: input.configuration,
408
+ consistency: input.consistency,
409
+ });
410
+ const digest = operationDigest({
411
+ version: 1,
412
+ name: request.name,
413
+ configuration: request.configuration,
414
+ consistency: request.consistency,
415
+ actorSubjectId: input.actorSubjectId,
416
+ });
417
+ try {
418
+ return await withRlsContext(db, input, async (scopedDb) => {
419
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
420
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
421
+ await lockOperation(scopedDb, request.operationId);
422
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
423
+ if (existing) {
424
+ assertOperation(existing, {
425
+ resourceKind: "network_route",
426
+ kind: "create",
427
+ requestDigest: digest,
428
+ actorSubjectId: input.actorSubjectId,
429
+ });
430
+ const response = NetworkRouteMutationResponse.parse(existing.result);
431
+ return { ...response, replayed: true };
432
+ }
433
+ await assertCredentialAuthorities(
434
+ scopedDb,
435
+ input.workspaceId,
436
+ credentialRefsFromRoute(request.configuration),
437
+ );
438
+ const id = randomUUID();
439
+ const [row] = await scopedDb
440
+ .insert(schema.networkRoutes)
441
+ .values({
442
+ id,
443
+ accountId: input.accountId,
444
+ workspaceId: input.workspaceId,
445
+ name: request.name,
446
+ configuration: request.configuration,
447
+ consistency: request.consistency,
448
+ createOperationId: request.operationId,
449
+ createdBySubjectId: input.actorSubjectId,
450
+ updatedBySubjectId: input.actorSubjectId,
451
+ })
452
+ .returning();
453
+ if (!row) throw new Error("Network route insert returned no row");
454
+ const response = NetworkRouteMutationResponse.parse({
455
+ route: routeFromRow(row),
456
+ operationId: request.operationId,
457
+ replayed: false,
458
+ });
459
+ await insertCompletedOperation(scopedDb, {
460
+ operationId: request.operationId,
461
+ accountId: input.accountId,
462
+ workspaceId: input.workspaceId,
463
+ resourceKind: "network_route",
464
+ resourceId: id,
465
+ kind: "create",
466
+ requestDigest: digest,
467
+ resultVersion: row.version,
468
+ result: response,
469
+ actorSubjectId: input.actorSubjectId,
470
+ });
471
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
472
+ return response;
473
+ });
474
+ } catch (error) {
475
+ if (postgresConstraint(error) === "network_routes_workspace_active_name_uq") {
476
+ throw new InteractionResourceConflictError("An active network route already uses this name");
477
+ }
478
+ throw error;
479
+ }
480
+ }
481
+
482
+ export async function updateNetworkRoute(
483
+ db: Database,
484
+ input: InteractionMutationScope & { routeId: string } & UpdateNetworkRouteRequestValue,
485
+ ): Promise<NetworkRouteMutationResponseValue> {
486
+ const request = UpdateNetworkRouteRequest.parse({
487
+ operationId: input.operationId,
488
+ expectedVersion: input.expectedVersion,
489
+ ...(input.name !== undefined ? { name: input.name } : {}),
490
+ ...(input.status !== undefined ? { status: input.status } : {}),
491
+ ...(input.configuration !== undefined ? { configuration: input.configuration } : {}),
492
+ ...(input.consistency !== undefined ? { consistency: input.consistency } : {}),
493
+ });
494
+ const { operationId: _operationId, ...digestRequest } = request;
495
+ const digest = operationDigest({
496
+ version: 1,
497
+ routeId: input.routeId,
498
+ request: digestRequest,
499
+ actorSubjectId: input.actorSubjectId,
500
+ });
501
+ try {
502
+ return await withRlsContext(db, input, async (scopedDb) => {
503
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
504
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
505
+ await lockOperation(scopedDb, request.operationId);
506
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
507
+ if (existing) {
508
+ assertOperation(existing, {
509
+ resourceKind: "network_route",
510
+ resourceId: input.routeId,
511
+ kind: "update",
512
+ requestDigest: digest,
513
+ actorSubjectId: input.actorSubjectId,
514
+ });
515
+ const response = NetworkRouteMutationResponse.parse(existing.result);
516
+ return { ...response, replayed: true };
517
+ }
518
+ const [current] = await scopedDb
519
+ .select()
520
+ .from(schema.networkRoutes)
521
+ .where(
522
+ and(
523
+ eq(schema.networkRoutes.workspaceId, input.workspaceId),
524
+ eq(schema.networkRoutes.id, input.routeId),
525
+ ),
526
+ )
527
+ .for("update")
528
+ .limit(1);
529
+ if (!current) throw new InteractionResourceNotFoundError("Network route not found");
530
+ if (current.version !== request.expectedVersion) {
531
+ throw new InteractionResourceConflictError("Network route changed before this update");
532
+ }
533
+ const configuration = NetworkRouteConfiguration.parse(
534
+ request.configuration ?? current.configuration,
535
+ );
536
+ const consistency = NetworkRouteConsistency.parse(request.consistency ?? current.consistency);
537
+ await assertCredentialAuthorities(
538
+ scopedDb,
539
+ input.workspaceId,
540
+ introducedCredentialRefs(
541
+ credentialRefsFromRoute(NetworkRouteConfiguration.parse(current.configuration)),
542
+ credentialRefsFromRoute(configuration),
543
+ ),
544
+ );
545
+ const [row] = await scopedDb
546
+ .update(schema.networkRoutes)
547
+ .set({
548
+ name: request.name ?? current.name,
549
+ status: request.status ?? current.status,
550
+ configuration,
551
+ consistency,
552
+ version: current.version + 1,
553
+ updatedBySubjectId: input.actorSubjectId,
554
+ updatedAt: sql`now()`,
555
+ })
556
+ .where(
557
+ and(
558
+ eq(schema.networkRoutes.workspaceId, input.workspaceId),
559
+ eq(schema.networkRoutes.id, input.routeId),
560
+ eq(schema.networkRoutes.version, request.expectedVersion),
561
+ ),
562
+ )
563
+ .returning();
564
+ if (!row) throw new InteractionResourceConflictError("Network route update lost its fence");
565
+ const response = NetworkRouteMutationResponse.parse({
566
+ route: routeFromRow(row),
567
+ operationId: request.operationId,
568
+ replayed: false,
569
+ });
570
+ await insertCompletedOperation(scopedDb, {
571
+ operationId: request.operationId,
572
+ accountId: input.accountId,
573
+ workspaceId: input.workspaceId,
574
+ resourceKind: "network_route",
575
+ resourceId: input.routeId,
576
+ kind: "update",
577
+ requestDigest: digest,
578
+ resultVersion: row.version,
579
+ result: response,
580
+ actorSubjectId: input.actorSubjectId,
581
+ });
582
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
583
+ return response;
584
+ });
585
+ } catch (error) {
586
+ if (postgresConstraint(error) === "network_routes_workspace_active_name_uq") {
587
+ throw new InteractionResourceConflictError("An active network route already uses this name");
588
+ }
589
+ throw error;
590
+ }
591
+ }
592
+
593
+ export async function listSiteAuthConnections(
594
+ db: Database,
595
+ input: { accountId: string; workspaceId: string; includeArchived?: boolean },
596
+ ): Promise<SiteAuthConnectionListResponseValue> {
597
+ return await withRlsContext(
598
+ db,
599
+ input,
600
+ async (scopedDb) => {
601
+ const predicate = input.includeArchived
602
+ ? eq(schema.siteAuthConnections.workspaceId, input.workspaceId)
603
+ : and(
604
+ eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
605
+ eq(schema.siteAuthConnections.status, "active"),
606
+ );
607
+ const connections = await scopedDb
608
+ .select()
609
+ .from(schema.siteAuthConnections)
610
+ .where(predicate)
611
+ .orderBy(asc(schema.siteAuthConnections.name), asc(schema.siteAuthConnections.id));
612
+ return SiteAuthConnectionListResponse.parse({
613
+ revision: await readWorkspaceInteractionRevision(scopedDb, input.workspaceId),
614
+ connections: connections.map(siteAuthConnectionFromRow),
615
+ });
616
+ },
617
+ CONSISTENT_READ,
618
+ );
619
+ }
620
+
621
+ export async function getSiteAuthConnection(
622
+ db: Database,
623
+ input: { accountId: string; workspaceId: string; siteAuthConnectionId: string },
624
+ ): Promise<SiteAuthConnectionValue> {
625
+ return await withRlsContext(
626
+ db,
627
+ input,
628
+ async (scopedDb) => {
629
+ const [row] = await scopedDb
630
+ .select()
631
+ .from(schema.siteAuthConnections)
632
+ .where(
633
+ and(
634
+ eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
635
+ eq(schema.siteAuthConnections.id, input.siteAuthConnectionId),
636
+ ),
637
+ )
638
+ .limit(1);
639
+ if (!row) throw new InteractionResourceNotFoundError("Site auth connection not found");
640
+ return siteAuthConnectionFromRow(row);
641
+ },
642
+ CONSISTENT_READ,
643
+ );
644
+ }
645
+
646
+ async function validateSiteAuthReferences(
647
+ db: Database,
648
+ workspaceId: string,
649
+ connection: Pick<
650
+ SiteAuthConnectionValue,
651
+ "authorities" | "preferredIdentityId" | "preferredNetworkRouteId"
652
+ >,
653
+ ): Promise<void> {
654
+ await assertCredentialAuthorities(
655
+ db,
656
+ workspaceId,
657
+ credentialRefsFromAuthorities(connection.authorities),
658
+ );
659
+ await assertActiveBrowserIdentity(db, workspaceId, connection.preferredIdentityId);
660
+ await assertActiveNetworkRoute(db, workspaceId, connection.preferredNetworkRouteId);
661
+ }
662
+
663
+ async function validateSiteAuthUpdateReferences(
664
+ db: Database,
665
+ workspaceId: string,
666
+ current: SiteAuthConnectionValue,
667
+ candidate: SiteAuthConnectionValue,
668
+ ): Promise<void> {
669
+ await assertCredentialAuthorities(
670
+ db,
671
+ workspaceId,
672
+ introducedCredentialRefs(
673
+ credentialRefsFromAuthorities(current.authorities),
674
+ credentialRefsFromAuthorities(candidate.authorities),
675
+ ),
676
+ );
677
+ if (candidate.preferredIdentityId !== current.preferredIdentityId) {
678
+ await assertActiveBrowserIdentity(db, workspaceId, candidate.preferredIdentityId);
679
+ }
680
+ if (candidate.preferredNetworkRouteId !== current.preferredNetworkRouteId) {
681
+ await assertActiveNetworkRoute(db, workspaceId, candidate.preferredNetworkRouteId);
682
+ }
683
+ }
684
+
685
+ export async function createSiteAuthConnection(
686
+ db: Database,
687
+ input: InteractionMutationScope & CreateSiteAuthConnectionRequestValue,
688
+ ): Promise<SiteAuthConnectionMutationResponseValue> {
689
+ const request = CreateSiteAuthConnectionRequest.parse({
690
+ operationId: input.operationId,
691
+ name: input.name,
692
+ accountLabel: input.accountLabel,
693
+ origins: input.origins,
694
+ loginUrl: input.loginUrl,
695
+ verificationUrlPrefixes: input.verificationUrlPrefixes,
696
+ authorities: input.authorities,
697
+ methods: input.methods,
698
+ preferredIdentityId: input.preferredIdentityId,
699
+ preferredPlacement: input.preferredPlacement,
700
+ preferredNetworkRouteId: input.preferredNetworkRouteId,
701
+ healthPolicy: input.healthPolicy,
702
+ });
703
+ const { operationId: _operationId, ...digestRequest } = request;
704
+ const digest = operationDigest({
705
+ version: 1,
706
+ request: digestRequest,
707
+ actorSubjectId: input.actorSubjectId,
708
+ });
709
+ try {
710
+ return await withRlsContext(db, input, async (scopedDb) => {
711
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
712
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
713
+ await lockOperation(scopedDb, request.operationId);
714
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
715
+ if (existing) {
716
+ assertOperation(existing, {
717
+ resourceKind: "site_auth_connection",
718
+ kind: "create",
719
+ requestDigest: digest,
720
+ actorSubjectId: input.actorSubjectId,
721
+ });
722
+ const response = SiteAuthConnectionMutationResponse.parse(existing.result);
723
+ return { ...response, replayed: true };
724
+ }
725
+ await validateSiteAuthReferences(scopedDb, input.workspaceId, {
726
+ authorities: request.authorities,
727
+ preferredIdentityId: request.preferredIdentityId,
728
+ preferredNetworkRouteId: request.preferredNetworkRouteId,
729
+ });
730
+ const id = randomUUID();
731
+ const [row] = await scopedDb
732
+ .insert(schema.siteAuthConnections)
733
+ .values({
734
+ id,
735
+ accountId: input.accountId,
736
+ workspaceId: input.workspaceId,
737
+ name: request.name,
738
+ accountLabel: request.accountLabel,
739
+ origins: request.origins,
740
+ loginUrl: request.loginUrl,
741
+ verificationUrlPrefixes: request.verificationUrlPrefixes,
742
+ authorities: request.authorities,
743
+ methods: request.methods,
744
+ preferredIdentityId: request.preferredIdentityId,
745
+ preferredPlacement: request.preferredPlacement,
746
+ preferredNetworkRouteId: request.preferredNetworkRouteId,
747
+ healthPolicy: request.healthPolicy,
748
+ createOperationId: request.operationId,
749
+ createdBySubjectId: input.actorSubjectId,
750
+ updatedBySubjectId: input.actorSubjectId,
751
+ })
752
+ .returning();
753
+ if (!row) throw new Error("Site auth connection insert returned no row");
754
+ const response = SiteAuthConnectionMutationResponse.parse({
755
+ connection: siteAuthConnectionFromRow(row),
756
+ operationId: request.operationId,
757
+ replayed: false,
758
+ });
759
+ await insertCompletedOperation(scopedDb, {
760
+ operationId: request.operationId,
761
+ accountId: input.accountId,
762
+ workspaceId: input.workspaceId,
763
+ resourceKind: "site_auth_connection",
764
+ resourceId: id,
765
+ kind: "create",
766
+ requestDigest: digest,
767
+ resultVersion: row.version,
768
+ result: response,
769
+ actorSubjectId: input.actorSubjectId,
770
+ });
771
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
772
+ return response;
773
+ });
774
+ } catch (error) {
775
+ if (postgresConstraint(error) === "site_auth_connections_workspace_active_name_uq") {
776
+ throw new InteractionResourceConflictError(
777
+ "An active site auth connection already uses this name",
778
+ );
779
+ }
780
+ throw error;
781
+ }
782
+ }
783
+
784
+ export async function updateSiteAuthConnection(
785
+ db: Database,
786
+ input: InteractionMutationScope & {
787
+ siteAuthConnectionId: string;
788
+ } & UpdateSiteAuthConnectionRequestValue,
789
+ ): Promise<SiteAuthConnectionMutationResponseValue> {
790
+ const request = UpdateSiteAuthConnectionRequest.parse({
791
+ operationId: input.operationId,
792
+ expectedVersion: input.expectedVersion,
793
+ ...(input.name !== undefined ? { name: input.name } : {}),
794
+ ...(input.accountLabel !== undefined ? { accountLabel: input.accountLabel } : {}),
795
+ ...(input.origins !== undefined ? { origins: input.origins } : {}),
796
+ ...(input.loginUrl !== undefined ? { loginUrl: input.loginUrl } : {}),
797
+ ...(input.verificationUrlPrefixes !== undefined
798
+ ? { verificationUrlPrefixes: input.verificationUrlPrefixes }
799
+ : {}),
800
+ ...(input.authorities !== undefined ? { authorities: input.authorities } : {}),
801
+ ...(input.methods !== undefined ? { methods: input.methods } : {}),
802
+ ...(input.preferredIdentityId !== undefined
803
+ ? { preferredIdentityId: input.preferredIdentityId }
804
+ : {}),
805
+ ...(input.preferredPlacement !== undefined
806
+ ? { preferredPlacement: input.preferredPlacement }
807
+ : {}),
808
+ ...(input.preferredNetworkRouteId !== undefined
809
+ ? { preferredNetworkRouteId: input.preferredNetworkRouteId }
810
+ : {}),
811
+ ...(input.healthPolicy !== undefined ? { healthPolicy: input.healthPolicy } : {}),
812
+ ...(input.status !== undefined ? { status: input.status } : {}),
813
+ });
814
+ const {
815
+ operationId: _operationId,
816
+ expectedVersion: _expectedVersion,
817
+ ...candidatePatch
818
+ } = request;
819
+ const digest = operationDigest({
820
+ version: 1,
821
+ siteAuthConnectionId: input.siteAuthConnectionId,
822
+ request: { expectedVersion: request.expectedVersion, ...candidatePatch },
823
+ actorSubjectId: input.actorSubjectId,
824
+ });
825
+ try {
826
+ return await withRlsContext(db, input, async (scopedDb) => {
827
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
828
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
829
+ await lockOperation(scopedDb, request.operationId);
830
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
831
+ if (existing) {
832
+ assertOperation(existing, {
833
+ resourceKind: "site_auth_connection",
834
+ resourceId: input.siteAuthConnectionId,
835
+ kind: "update",
836
+ requestDigest: digest,
837
+ actorSubjectId: input.actorSubjectId,
838
+ });
839
+ const response = SiteAuthConnectionMutationResponse.parse(existing.result);
840
+ return { ...response, replayed: true };
841
+ }
842
+ const [current] = await scopedDb
843
+ .select()
844
+ .from(schema.siteAuthConnections)
845
+ .where(
846
+ and(
847
+ eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
848
+ eq(schema.siteAuthConnections.id, input.siteAuthConnectionId),
849
+ ),
850
+ )
851
+ .for("update")
852
+ .limit(1);
853
+ if (!current) throw new InteractionResourceNotFoundError("Site auth connection not found");
854
+ if (current.version !== request.expectedVersion) {
855
+ throw new InteractionResourceConflictError(
856
+ "Site auth connection changed before this update",
857
+ );
858
+ }
859
+ const currentValue = siteAuthConnectionFromRow(current);
860
+ const candidate = SiteAuthConnection.parse({
861
+ ...currentValue,
862
+ ...candidatePatch,
863
+ id: current.id,
864
+ accountId: current.accountId,
865
+ workspaceId: current.workspaceId,
866
+ version: current.version + 1,
867
+ createdBySubjectId: current.createdBySubjectId,
868
+ createdAt: iso(current.createdAt),
869
+ updatedAt: new Date().toISOString(),
870
+ });
871
+ await validateSiteAuthUpdateReferences(scopedDb, input.workspaceId, currentValue, candidate);
872
+ const [row] = await scopedDb
873
+ .update(schema.siteAuthConnections)
874
+ .set({
875
+ name: candidate.name,
876
+ accountLabel: candidate.accountLabel,
877
+ status: candidate.status,
878
+ origins: candidate.origins,
879
+ loginUrl: candidate.loginUrl,
880
+ verificationUrlPrefixes: candidate.verificationUrlPrefixes,
881
+ authorities: candidate.authorities,
882
+ methods: candidate.methods,
883
+ preferredIdentityId: candidate.preferredIdentityId,
884
+ preferredPlacement: candidate.preferredPlacement,
885
+ preferredNetworkRouteId: candidate.preferredNetworkRouteId,
886
+ healthPolicy: candidate.healthPolicy,
887
+ version: candidate.version,
888
+ updatedBySubjectId: input.actorSubjectId,
889
+ updatedAt: sql`now()`,
890
+ })
891
+ .where(
892
+ and(
893
+ eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
894
+ eq(schema.siteAuthConnections.id, input.siteAuthConnectionId),
895
+ eq(schema.siteAuthConnections.version, request.expectedVersion),
896
+ ),
897
+ )
898
+ .returning();
899
+ if (!row) {
900
+ throw new InteractionResourceConflictError("Site auth connection update lost its fence");
901
+ }
902
+ const response = SiteAuthConnectionMutationResponse.parse({
903
+ connection: siteAuthConnectionFromRow(row),
904
+ operationId: request.operationId,
905
+ replayed: false,
906
+ });
907
+ await insertCompletedOperation(scopedDb, {
908
+ operationId: request.operationId,
909
+ accountId: input.accountId,
910
+ workspaceId: input.workspaceId,
911
+ resourceKind: "site_auth_connection",
912
+ resourceId: input.siteAuthConnectionId,
913
+ kind: "update",
914
+ requestDigest: digest,
915
+ resultVersion: row.version,
916
+ result: response,
917
+ actorSubjectId: input.actorSubjectId,
918
+ });
919
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
920
+ return response;
921
+ });
922
+ } catch (error) {
923
+ if (postgresConstraint(error) === "site_auth_connections_workspace_active_name_uq") {
924
+ throw new InteractionResourceConflictError(
925
+ "An active site auth connection already uses this name",
926
+ );
927
+ }
928
+ throw error;
929
+ }
930
+ }
931
+
932
+ function authRunFromRow(row: AuthRunRow): AuthRunValue {
933
+ return AuthRun.parse({
934
+ id: row.id,
935
+ accountId: row.accountId,
936
+ workspaceId: row.workspaceId,
937
+ siteAuthConnectionId: row.siteAuthConnectionId,
938
+ browserSessionId: row.browserSessionId,
939
+ targetId: row.targetId,
940
+ controllerGeneration: row.controllerGeneration,
941
+ targetGeneration: row.targetGeneration,
942
+ documentGeneration: row.documentGeneration,
943
+ methodId: row.methodId,
944
+ authorityId: row.authorityId,
945
+ state: row.state,
946
+ choices: row.choices,
947
+ pendingFields: row.pendingFields,
948
+ externalAction: row.externalAction,
949
+ interventionId: row.interventionId,
950
+ verifiedUrl: row.verifiedUrl,
951
+ failureCode: row.failureCode,
952
+ version: row.version,
953
+ operationId: row.operationId,
954
+ createdBySubjectId: row.createdBySubjectId,
955
+ createdAt: iso(row.createdAt),
956
+ updatedAt: iso(row.updatedAt),
957
+ settledAt: row.settledAt ? iso(row.settledAt) : null,
958
+ });
959
+ }
960
+
961
+ async function loadAuthRunRow(
962
+ db: Database,
963
+ workspaceId: string,
964
+ authRunId: string,
965
+ ): Promise<AuthRunRow | null> {
966
+ const [row] = await db
967
+ .select()
968
+ .from(schema.authRuns)
969
+ .where(and(eq(schema.authRuns.workspaceId, workspaceId), eq(schema.authRuns.id, authRunId)))
970
+ .limit(1);
971
+ return row ?? null;
972
+ }
973
+
974
+ function assertAuthSelection(
975
+ connection: SiteAuthConnectionValue,
976
+ selection: { methodId?: string | null; authorityId?: string | null },
977
+ ): void {
978
+ const method = selection.methodId
979
+ ? connection.methods.find((candidate) => candidate.id === selection.methodId)
980
+ : null;
981
+ const authority = selection.authorityId
982
+ ? connection.authorities.find((candidate) => candidate.id === selection.authorityId)
983
+ : null;
984
+ if (selection.methodId && !method) {
985
+ throw new InteractionResourceStateError("Auth method is not configured");
986
+ }
987
+ if (selection.authorityId && !authority) {
988
+ throw new InteractionResourceStateError("Auth authority is not configured");
989
+ }
990
+ if (method && authority && !method.authorityIds.includes(authority.id)) {
991
+ throw new InteractionResourceStateError(
992
+ "Auth authority does not belong to the selected method",
993
+ );
994
+ }
995
+ }
996
+
997
+ const AUTH_RUN_TRANSITIONS: Readonly<
998
+ Record<AuthRunRow["state"], ReadonlySet<AuthRunRow["state"]>>
999
+ > = {
1000
+ discovering: new Set([
1001
+ "awaiting_choice",
1002
+ "awaiting_secret",
1003
+ "awaiting_external_action",
1004
+ "working",
1005
+ "failed",
1006
+ "cancelled",
1007
+ ]),
1008
+ awaiting_choice: new Set([
1009
+ "awaiting_choice",
1010
+ "awaiting_secret",
1011
+ "awaiting_external_action",
1012
+ "working",
1013
+ "failed",
1014
+ "cancelled",
1015
+ ]),
1016
+ awaiting_secret: new Set([
1017
+ "awaiting_secret",
1018
+ "awaiting_external_action",
1019
+ "working",
1020
+ "failed",
1021
+ "cancelled",
1022
+ ]),
1023
+ awaiting_external_action: new Set([
1024
+ "awaiting_external_action",
1025
+ "awaiting_secret",
1026
+ "working",
1027
+ "failed",
1028
+ "cancelled",
1029
+ ]),
1030
+ working: new Set([
1031
+ "working",
1032
+ "awaiting_choice",
1033
+ "awaiting_secret",
1034
+ "awaiting_external_action",
1035
+ "failed",
1036
+ "cancelled",
1037
+ ]),
1038
+ verified: new Set(),
1039
+ failed: new Set(),
1040
+ cancelled: new Set(),
1041
+ };
1042
+
1043
+ export async function listAuthRuns(
1044
+ db: Database,
1045
+ input: {
1046
+ accountId: string;
1047
+ workspaceId: string;
1048
+ browserSessionId?: string;
1049
+ siteAuthConnectionId?: string;
1050
+ includeSettled?: boolean;
1051
+ },
1052
+ ): Promise<AuthRunListResponseValue> {
1053
+ return await withRlsContext(
1054
+ db,
1055
+ input,
1056
+ async (scopedDb) => {
1057
+ const predicates = [eq(schema.authRuns.workspaceId, input.workspaceId)];
1058
+ if (input.browserSessionId) {
1059
+ predicates.push(eq(schema.authRuns.browserSessionId, input.browserSessionId));
1060
+ }
1061
+ if (input.siteAuthConnectionId) {
1062
+ predicates.push(eq(schema.authRuns.siteAuthConnectionId, input.siteAuthConnectionId));
1063
+ }
1064
+ if (!input.includeSettled) predicates.push(sql`${schema.authRuns.settledAt} is null`);
1065
+ const runs = await scopedDb
1066
+ .select()
1067
+ .from(schema.authRuns)
1068
+ .where(and(...predicates))
1069
+ .orderBy(asc(schema.authRuns.createdAt), asc(schema.authRuns.id));
1070
+ return AuthRunListResponse.parse({ runs: runs.map(authRunFromRow) });
1071
+ },
1072
+ CONSISTENT_READ,
1073
+ );
1074
+ }
1075
+
1076
+ export async function getAuthRun(
1077
+ db: Database,
1078
+ input: { accountId: string; workspaceId: string; authRunId: string },
1079
+ ): Promise<AuthRunValue> {
1080
+ return await withRlsContext(
1081
+ db,
1082
+ input,
1083
+ async (scopedDb) => {
1084
+ const row = await loadAuthRunRow(scopedDb, input.workspaceId, input.authRunId);
1085
+ if (!row) throw new InteractionResourceNotFoundError("Auth run not found");
1086
+ return authRunFromRow(row);
1087
+ },
1088
+ CONSISTENT_READ,
1089
+ );
1090
+ }
1091
+
1092
+ export async function startAuthRun(
1093
+ db: Database,
1094
+ input: InteractionMutationScope & { browserSessionId: string } & StartAuthRunRequestValue,
1095
+ ): Promise<AuthRunMutationResponseValue> {
1096
+ const request = StartAuthRunRequest.parse({
1097
+ operationId: input.operationId,
1098
+ siteAuthConnectionId: input.siteAuthConnectionId,
1099
+ targetId: input.targetId,
1100
+ expectedTargetGeneration: input.expectedTargetGeneration,
1101
+ expectedDocumentGeneration: input.expectedDocumentGeneration,
1102
+ ...(input.methodId !== undefined ? { methodId: input.methodId } : {}),
1103
+ ...(input.authorityId !== undefined ? { authorityId: input.authorityId } : {}),
1104
+ });
1105
+ const { operationId: _operationId, ...digestRequest } = request;
1106
+ const digest = operationDigest({
1107
+ version: 1,
1108
+ browserSessionId: input.browserSessionId,
1109
+ request: digestRequest,
1110
+ actorSubjectId: input.actorSubjectId,
1111
+ });
1112
+ try {
1113
+ return await withRlsContext(db, input, async (scopedDb) => {
1114
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
1115
+ await lockOperation(scopedDb, request.operationId);
1116
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
1117
+ if (existing) {
1118
+ assertOperation(existing, {
1119
+ resourceKind: "auth_run",
1120
+ kind: "start",
1121
+ requestDigest: digest,
1122
+ actorSubjectId: input.actorSubjectId,
1123
+ });
1124
+ const response = AuthRunMutationResponse.parse(existing.result);
1125
+ return { ...response, replayed: true };
1126
+ }
1127
+ const [browser] = await scopedDb
1128
+ .select()
1129
+ .from(schema.browserSessions)
1130
+ .where(
1131
+ and(
1132
+ eq(schema.browserSessions.workspaceId, input.workspaceId),
1133
+ eq(schema.browserSessions.id, input.browserSessionId),
1134
+ ),
1135
+ )
1136
+ .for("update")
1137
+ .limit(1);
1138
+ if (!browser) throw new InteractionResourceNotFoundError("Browser session not found");
1139
+ if (browser.lifecycle !== "active" || !browser.controllerGeneration) {
1140
+ throw new InteractionResourceStateError("Browser session is not active");
1141
+ }
1142
+ const authConnection = await getSiteAuthConnectionRow(
1143
+ scopedDb,
1144
+ input.workspaceId,
1145
+ request.siteAuthConnectionId,
1146
+ );
1147
+ if (authConnection.status !== "active") {
1148
+ throw new InteractionResourceStateError("Site auth connection is archived");
1149
+ }
1150
+ const connection = siteAuthConnectionFromRow(authConnection);
1151
+ assertAuthSelection(connection, {
1152
+ methodId: request.methodId ?? null,
1153
+ authorityId: request.authorityId ?? null,
1154
+ });
1155
+ const id = randomUUID();
1156
+ const [row] = await scopedDb
1157
+ .insert(schema.authRuns)
1158
+ .values({
1159
+ id,
1160
+ accountId: input.accountId,
1161
+ workspaceId: input.workspaceId,
1162
+ siteAuthConnectionId: request.siteAuthConnectionId,
1163
+ browserSessionId: input.browserSessionId,
1164
+ targetId: request.targetId,
1165
+ controllerGeneration: browser.controllerGeneration,
1166
+ targetGeneration: request.expectedTargetGeneration,
1167
+ documentGeneration: request.expectedDocumentGeneration,
1168
+ methodId: request.methodId ?? null,
1169
+ authorityId: request.authorityId ?? null,
1170
+ operationId: request.operationId,
1171
+ createdBySubjectId: input.actorSubjectId,
1172
+ })
1173
+ .returning();
1174
+ if (!row) throw new Error("Auth run insert returned no row");
1175
+ const response = AuthRunMutationResponse.parse({
1176
+ run: authRunFromRow(row),
1177
+ operationId: request.operationId,
1178
+ replayed: false,
1179
+ });
1180
+ await insertCompletedOperation(scopedDb, {
1181
+ operationId: request.operationId,
1182
+ accountId: input.accountId,
1183
+ workspaceId: input.workspaceId,
1184
+ resourceKind: "auth_run",
1185
+ resourceId: id,
1186
+ kind: "start",
1187
+ requestDigest: digest,
1188
+ resultVersion: row.version,
1189
+ result: response,
1190
+ actorSubjectId: input.actorSubjectId,
1191
+ });
1192
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
1193
+ return response;
1194
+ });
1195
+ } catch (error) {
1196
+ if (postgresConstraint(error) === "auth_runs_active_browser_target_uq") {
1197
+ throw new InteractionResourceConflictError(
1198
+ "This browser target already has an active auth run",
1199
+ );
1200
+ }
1201
+ throw error;
1202
+ }
1203
+ }
1204
+
1205
+ export async function reportAuthRun(
1206
+ db: Database,
1207
+ input: InteractionMutationScope & {
1208
+ authRunId: string;
1209
+ controllerGeneration: string;
1210
+ } & ReportAuthRunRequestValue,
1211
+ ): Promise<AuthRunMutationResponseValue> {
1212
+ const request = ReportAuthRunRequest.parse({
1213
+ operationId: input.operationId,
1214
+ expectedVersion: input.expectedVersion,
1215
+ ...(input.methodId !== undefined ? { methodId: input.methodId } : {}),
1216
+ ...(input.authorityId !== undefined ? { authorityId: input.authorityId } : {}),
1217
+ state: input.state,
1218
+ ...(input.choices !== undefined ? { choices: input.choices } : {}),
1219
+ ...(input.pendingFields !== undefined ? { pendingFields: input.pendingFields } : {}),
1220
+ ...(input.externalAction !== undefined ? { externalAction: input.externalAction } : {}),
1221
+ ...(input.failureCode !== undefined ? { failureCode: input.failureCode } : {}),
1222
+ });
1223
+ const { operationId: _operationId, ...digestRequest } = request;
1224
+ const digest = operationDigest({
1225
+ version: 1,
1226
+ authRunId: input.authRunId,
1227
+ controllerGeneration: input.controllerGeneration,
1228
+ request: digestRequest,
1229
+ actorSubjectId: input.actorSubjectId,
1230
+ });
1231
+ return await withRlsContext(db, input, async (scopedDb) => {
1232
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
1233
+ await lockOperation(scopedDb, request.operationId);
1234
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
1235
+ if (existing) {
1236
+ assertOperation(existing, {
1237
+ resourceKind: "auth_run",
1238
+ resourceId: input.authRunId,
1239
+ kind: "report",
1240
+ requestDigest: digest,
1241
+ actorSubjectId: input.actorSubjectId,
1242
+ });
1243
+ const response = AuthRunMutationResponse.parse(existing.result);
1244
+ return { ...response, replayed: true };
1245
+ }
1246
+ const [current] = await scopedDb
1247
+ .select()
1248
+ .from(schema.authRuns)
1249
+ .where(
1250
+ and(
1251
+ eq(schema.authRuns.workspaceId, input.workspaceId),
1252
+ eq(schema.authRuns.id, input.authRunId),
1253
+ ),
1254
+ )
1255
+ .for("update")
1256
+ .limit(1);
1257
+ if (!current) throw new InteractionResourceNotFoundError("Auth run not found");
1258
+ if (current.controllerGeneration !== input.controllerGeneration) {
1259
+ throw new InteractionResourceConflictError("Auth run belongs to a stale browser controller");
1260
+ }
1261
+ if (current.version !== request.expectedVersion) {
1262
+ throw new InteractionResourceConflictError("Auth run changed before this report");
1263
+ }
1264
+ if (!AUTH_RUN_TRANSITIONS[current.state].has(request.state)) {
1265
+ throw new InteractionResourceStateError(
1266
+ `Auth run cannot transition from ${current.state} to ${request.state}`,
1267
+ );
1268
+ }
1269
+ const authConnection = await getSiteAuthConnectionRow(
1270
+ scopedDb,
1271
+ input.workspaceId,
1272
+ current.siteAuthConnectionId,
1273
+ );
1274
+ assertAuthSelection(siteAuthConnectionFromRow(authConnection), {
1275
+ methodId: request.methodId ?? current.methodId,
1276
+ authorityId: request.authorityId ?? current.authorityId,
1277
+ });
1278
+ const settled = request.state === "failed" || request.state === "cancelled";
1279
+ const [row] = await scopedDb
1280
+ .update(schema.authRuns)
1281
+ .set({
1282
+ methodId: request.methodId ?? current.methodId,
1283
+ authorityId: request.authorityId ?? current.authorityId,
1284
+ state: request.state,
1285
+ choices: request.choices ?? [],
1286
+ pendingFields: request.pendingFields ?? [],
1287
+ externalAction: request.externalAction ?? null,
1288
+ failureCode: request.state === "failed" ? request.failureCode! : null,
1289
+ version: current.version + 1,
1290
+ updatedAt: sql`now()`,
1291
+ settledAt: settled ? sql`now()` : null,
1292
+ })
1293
+ .where(
1294
+ and(
1295
+ eq(schema.authRuns.workspaceId, input.workspaceId),
1296
+ eq(schema.authRuns.id, input.authRunId),
1297
+ eq(schema.authRuns.version, request.expectedVersion),
1298
+ eq(schema.authRuns.controllerGeneration, input.controllerGeneration),
1299
+ ),
1300
+ )
1301
+ .returning();
1302
+ if (!row) throw new InteractionResourceConflictError("Auth run report lost its fence");
1303
+ const response = AuthRunMutationResponse.parse({
1304
+ run: authRunFromRow(row),
1305
+ operationId: request.operationId,
1306
+ replayed: false,
1307
+ });
1308
+ await insertCompletedOperation(scopedDb, {
1309
+ operationId: request.operationId,
1310
+ accountId: input.accountId,
1311
+ workspaceId: input.workspaceId,
1312
+ resourceKind: "auth_run",
1313
+ resourceId: input.authRunId,
1314
+ kind: "report",
1315
+ requestDigest: digest,
1316
+ resultVersion: row.version,
1317
+ result: response,
1318
+ actorSubjectId: input.actorSubjectId,
1319
+ });
1320
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
1321
+ return response;
1322
+ });
1323
+ }
1324
+
1325
+ async function getSiteAuthConnectionRow(
1326
+ db: Database,
1327
+ workspaceId: string,
1328
+ siteAuthConnectionId: string,
1329
+ ): Promise<SiteAuthConnectionRow> {
1330
+ const [row] = await db
1331
+ .select()
1332
+ .from(schema.siteAuthConnections)
1333
+ .where(
1334
+ and(
1335
+ eq(schema.siteAuthConnections.workspaceId, workspaceId),
1336
+ eq(schema.siteAuthConnections.id, siteAuthConnectionId),
1337
+ ),
1338
+ )
1339
+ .limit(1);
1340
+ if (!row) throw new InteractionResourceNotFoundError("Site auth connection not found");
1341
+ return row;
1342
+ }
1343
+
1344
+ function interventionFromRow(row: InterventionRow): InteractionInterventionValue {
1345
+ return InteractionIntervention.parse({
1346
+ id: row.id,
1347
+ accountId: row.accountId,
1348
+ workspaceId: row.workspaceId,
1349
+ resourceKind: row.resourceKind,
1350
+ resourceId: row.resourceId,
1351
+ targetId: row.targetId,
1352
+ controllerGeneration: row.controllerGeneration,
1353
+ targetGeneration: row.targetGeneration,
1354
+ documentGeneration: row.documentGeneration,
1355
+ kind: row.kind,
1356
+ reason: row.reason,
1357
+ status: row.status,
1358
+ authRunId: row.authRunId,
1359
+ originatingSessionId: row.originatingSessionId,
1360
+ originatingTurnId: row.originatingTurnId,
1361
+ originatingAttemptId: row.originatingAttemptId,
1362
+ originatingToolOperationId: row.originatingToolOperationId,
1363
+ responseActorSubjectId: row.responseActorSubjectId,
1364
+ version: row.version,
1365
+ operationId: row.operationId,
1366
+ expiresAt: iso(row.expiresAt),
1367
+ createdAt: iso(row.createdAt),
1368
+ updatedAt: iso(row.updatedAt),
1369
+ settledAt: row.settledAt ? iso(row.settledAt) : null,
1370
+ });
1371
+ }
1372
+
1373
+ async function loadInterventionRow(
1374
+ db: Database,
1375
+ workspaceId: string,
1376
+ interventionId: string,
1377
+ ): Promise<InterventionRow | null> {
1378
+ const [row] = await db
1379
+ .select()
1380
+ .from(schema.interactionInterventions)
1381
+ .where(
1382
+ and(
1383
+ eq(schema.interactionInterventions.workspaceId, workspaceId),
1384
+ eq(schema.interactionInterventions.id, interventionId),
1385
+ ),
1386
+ )
1387
+ .limit(1);
1388
+ return row ?? null;
1389
+ }
1390
+
1391
+ async function assertActiveInteractionResource(
1392
+ db: Database,
1393
+ input: {
1394
+ workspaceId: string;
1395
+ resourceKind: "browser_session" | "computer_session";
1396
+ resourceId: string;
1397
+ expectedControllerGeneration: string;
1398
+ },
1399
+ ): Promise<void> {
1400
+ if (input.resourceKind === "browser_session") {
1401
+ const [resource] = await db
1402
+ .select({
1403
+ lifecycle: schema.browserSessions.lifecycle,
1404
+ controllerGeneration: schema.browserSessions.controllerGeneration,
1405
+ })
1406
+ .from(schema.browserSessions)
1407
+ .where(
1408
+ and(
1409
+ eq(schema.browserSessions.workspaceId, input.workspaceId),
1410
+ eq(schema.browserSessions.id, input.resourceId),
1411
+ ),
1412
+ )
1413
+ .for("update")
1414
+ .limit(1);
1415
+ if (!resource) throw new InteractionResourceNotFoundError("Browser session not found");
1416
+ if (resource.lifecycle !== "active" || !resource.controllerGeneration) {
1417
+ throw new InteractionResourceStateError("Browser session is not active");
1418
+ }
1419
+ if (resource.controllerGeneration !== input.expectedControllerGeneration) {
1420
+ throw new InteractionResourceConflictError(
1421
+ "Browser intervention belongs to a stale controller",
1422
+ );
1423
+ }
1424
+ return;
1425
+ }
1426
+ const [resource] = await db
1427
+ .select({
1428
+ lifecycle: schema.computerSessions.lifecycle,
1429
+ controllerGeneration: schema.computerSessions.controllerGeneration,
1430
+ })
1431
+ .from(schema.computerSessions)
1432
+ .where(
1433
+ and(
1434
+ eq(schema.computerSessions.workspaceId, input.workspaceId),
1435
+ eq(schema.computerSessions.id, input.resourceId),
1436
+ ),
1437
+ )
1438
+ .for("update")
1439
+ .limit(1);
1440
+ if (!resource) throw new InteractionResourceNotFoundError("Computer session not found");
1441
+ if (resource.lifecycle !== "active" || !resource.controllerGeneration) {
1442
+ throw new InteractionResourceStateError("Computer session is not active");
1443
+ }
1444
+ if (resource.controllerGeneration !== input.expectedControllerGeneration) {
1445
+ throw new InteractionResourceConflictError(
1446
+ "Computer intervention belongs to a stale controller",
1447
+ );
1448
+ }
1449
+ }
1450
+
1451
+ async function settleLinkedAuthRun(
1452
+ db: Database,
1453
+ input: {
1454
+ workspaceId: string;
1455
+ authRunId: string | null;
1456
+ interventionId: string;
1457
+ outcome: "completed" | "dismissed" | "expired" | "cancelled";
1458
+ },
1459
+ ): Promise<void> {
1460
+ if (!input.authRunId) return;
1461
+ const [run] = await db
1462
+ .select()
1463
+ .from(schema.authRuns)
1464
+ .where(
1465
+ and(
1466
+ eq(schema.authRuns.workspaceId, input.workspaceId),
1467
+ eq(schema.authRuns.id, input.authRunId),
1468
+ ),
1469
+ )
1470
+ .for("update")
1471
+ .limit(1);
1472
+ if (!run || run.interventionId !== input.interventionId || run.settledAt) return;
1473
+ const resumed = input.outcome === "completed";
1474
+ const [settled] = await db
1475
+ .update(schema.authRuns)
1476
+ .set({
1477
+ state: resumed ? "working" : input.outcome === "cancelled" ? "cancelled" : "failed",
1478
+ choices: [],
1479
+ pendingFields: [],
1480
+ externalAction: null,
1481
+ interventionId: null,
1482
+ failureCode:
1483
+ resumed || input.outcome === "cancelled" ? null : `intervention_${input.outcome}`,
1484
+ version: run.version + 1,
1485
+ updatedAt: sql`now()`,
1486
+ settledAt: resumed ? null : sql`now()`,
1487
+ })
1488
+ .where(
1489
+ and(
1490
+ eq(schema.authRuns.workspaceId, input.workspaceId),
1491
+ eq(schema.authRuns.id, input.authRunId),
1492
+ eq(schema.authRuns.version, run.version),
1493
+ eq(schema.authRuns.interventionId, input.interventionId),
1494
+ ),
1495
+ )
1496
+ .returning({ id: schema.authRuns.id });
1497
+ if (!settled) {
1498
+ throw new InteractionResourceConflictError(
1499
+ "Linked auth run changed before intervention settlement",
1500
+ );
1501
+ }
1502
+ }
1503
+
1504
+ async function expireInterventionsInScope(
1505
+ db: Database,
1506
+ input: { accountId: string; workspaceId: string },
1507
+ ): Promise<number> {
1508
+ const expired = await db
1509
+ .update(schema.interactionInterventions)
1510
+ .set({
1511
+ status: "expired",
1512
+ version: sql`${schema.interactionInterventions.version} + 1`,
1513
+ updatedAt: sql`now()`,
1514
+ settledAt: sql`now()`,
1515
+ })
1516
+ .where(
1517
+ and(
1518
+ eq(schema.interactionInterventions.workspaceId, input.workspaceId),
1519
+ eq(schema.interactionInterventions.status, "open"),
1520
+ sql`${schema.interactionInterventions.expiresAt} <= now()`,
1521
+ ),
1522
+ )
1523
+ .returning({
1524
+ id: schema.interactionInterventions.id,
1525
+ authRunId: schema.interactionInterventions.authRunId,
1526
+ });
1527
+ for (const intervention of expired) {
1528
+ await settleLinkedAuthRun(db, {
1529
+ workspaceId: input.workspaceId,
1530
+ authRunId: intervention.authRunId,
1531
+ interventionId: intervention.id,
1532
+ outcome: "expired",
1533
+ });
1534
+ }
1535
+ if (expired.length > 0) {
1536
+ await advanceWorkspaceInteractionRevision(db, input.accountId, input.workspaceId);
1537
+ }
1538
+ return expired.length;
1539
+ }
1540
+
1541
+ export async function listInteractionInterventions(
1542
+ db: Database,
1543
+ input: {
1544
+ accountId: string;
1545
+ workspaceId: string;
1546
+ resourceKind?: "browser_session" | "computer_session";
1547
+ resourceId?: string;
1548
+ includeSettled?: boolean;
1549
+ },
1550
+ ): Promise<InteractionInterventionListResponseValue> {
1551
+ return await withRlsContext(db, input, async (scopedDb) => {
1552
+ await expireInterventionsInScope(scopedDb, input);
1553
+ const predicates = [eq(schema.interactionInterventions.workspaceId, input.workspaceId)];
1554
+ if (input.resourceKind) {
1555
+ predicates.push(eq(schema.interactionInterventions.resourceKind, input.resourceKind));
1556
+ }
1557
+ if (input.resourceId) {
1558
+ predicates.push(eq(schema.interactionInterventions.resourceId, input.resourceId));
1559
+ }
1560
+ if (!input.includeSettled) {
1561
+ predicates.push(eq(schema.interactionInterventions.status, "open"));
1562
+ }
1563
+ const interventions = await scopedDb
1564
+ .select()
1565
+ .from(schema.interactionInterventions)
1566
+ .where(and(...predicates))
1567
+ .orderBy(
1568
+ asc(schema.interactionInterventions.createdAt),
1569
+ asc(schema.interactionInterventions.id),
1570
+ );
1571
+ return InteractionInterventionListResponse.parse({
1572
+ interventions: interventions.map(interventionFromRow),
1573
+ });
1574
+ });
1575
+ }
1576
+
1577
+ export async function getInteractionIntervention(
1578
+ db: Database,
1579
+ input: { accountId: string; workspaceId: string; interventionId: string },
1580
+ ): Promise<InteractionInterventionValue> {
1581
+ return await withRlsContext(db, input, async (scopedDb) => {
1582
+ await expireInterventionsInScope(scopedDb, input);
1583
+ const row = await loadInterventionRow(scopedDb, input.workspaceId, input.interventionId);
1584
+ if (!row) throw new InteractionResourceNotFoundError("Interaction intervention not found");
1585
+ return interventionFromRow(row);
1586
+ });
1587
+ }
1588
+
1589
+ export async function createInteractionIntervention(
1590
+ db: Database,
1591
+ input: InteractionMutationScope &
1592
+ CreateInteractionInterventionRequestValue & {
1593
+ originatingSessionId: string;
1594
+ originatingTurnId?: string | null;
1595
+ originatingAttemptId?: string | null;
1596
+ originatingToolOperationId?: string | null;
1597
+ },
1598
+ ): Promise<InteractionInterventionMutationResponseValue> {
1599
+ const request = CreateInteractionInterventionRequest.parse({
1600
+ operationId: input.operationId,
1601
+ resourceKind: input.resourceKind,
1602
+ resourceId: input.resourceId,
1603
+ targetId: input.targetId,
1604
+ expectedControllerGeneration: input.expectedControllerGeneration,
1605
+ expectedTargetGeneration: input.expectedTargetGeneration,
1606
+ expectedDocumentGeneration: input.expectedDocumentGeneration,
1607
+ kind: input.kind,
1608
+ reason: input.reason,
1609
+ ...(input.authRunId !== undefined ? { authRunId: input.authRunId } : {}),
1610
+ expiresInSeconds: input.expiresInSeconds,
1611
+ });
1612
+ const { operationId: _operationId, ...digestRequest } = request;
1613
+ const digest = operationDigest({
1614
+ version: 1,
1615
+ request: digestRequest,
1616
+ originatingSessionId: input.originatingSessionId,
1617
+ originatingTurnId: input.originatingTurnId ?? null,
1618
+ originatingAttemptId: input.originatingAttemptId ?? null,
1619
+ originatingToolOperationId: input.originatingToolOperationId ?? null,
1620
+ actorSubjectId: input.actorSubjectId,
1621
+ });
1622
+ try {
1623
+ return await withRlsContext(db, input, async (scopedDb) => {
1624
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
1625
+ await lockOperation(scopedDb, request.operationId);
1626
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
1627
+ if (existing) {
1628
+ assertOperation(existing, {
1629
+ resourceKind: "intervention",
1630
+ kind: "create",
1631
+ requestDigest: digest,
1632
+ actorSubjectId: input.actorSubjectId,
1633
+ });
1634
+ const response = InteractionInterventionMutationResponse.parse(existing.result);
1635
+ return { ...response, replayed: true };
1636
+ }
1637
+ await expireInterventionsInScope(scopedDb, input);
1638
+ await assertActiveInteractionResource(scopedDb, {
1639
+ workspaceId: input.workspaceId,
1640
+ resourceKind: request.resourceKind,
1641
+ resourceId: request.resourceId,
1642
+ expectedControllerGeneration: request.expectedControllerGeneration,
1643
+ });
1644
+ let authRun: AuthRunRow | null = null;
1645
+ if (request.authRunId) {
1646
+ if (request.resourceKind !== "browser_session") {
1647
+ throw new InteractionResourceStateError(
1648
+ "Only browser interventions can belong to an auth run",
1649
+ );
1650
+ }
1651
+ authRun = await loadAuthRunRow(scopedDb, input.workspaceId, request.authRunId);
1652
+ if (!authRun) throw new InteractionResourceNotFoundError("Auth run not found");
1653
+ if (
1654
+ authRun.browserSessionId !== request.resourceId ||
1655
+ authRun.targetId !== request.targetId ||
1656
+ authRun.controllerGeneration !== request.expectedControllerGeneration ||
1657
+ authRun.targetGeneration !== request.expectedTargetGeneration ||
1658
+ authRun.documentGeneration !== request.expectedDocumentGeneration
1659
+ ) {
1660
+ throw new InteractionResourceConflictError(
1661
+ "Intervention does not match the exact auth-run target",
1662
+ );
1663
+ }
1664
+ if (authRun.settledAt) {
1665
+ throw new InteractionResourceStateError("Auth run is already settled");
1666
+ }
1667
+ }
1668
+ const id = randomUUID();
1669
+ const [row] = await scopedDb
1670
+ .insert(schema.interactionInterventions)
1671
+ .values({
1672
+ id,
1673
+ accountId: input.accountId,
1674
+ workspaceId: input.workspaceId,
1675
+ resourceKind: request.resourceKind,
1676
+ resourceId: request.resourceId,
1677
+ targetId: request.targetId,
1678
+ controllerGeneration: request.expectedControllerGeneration,
1679
+ targetGeneration: request.expectedTargetGeneration,
1680
+ documentGeneration: request.expectedDocumentGeneration,
1681
+ kind: request.kind,
1682
+ reason: request.reason,
1683
+ authRunId: request.authRunId ?? null,
1684
+ originatingSessionId: input.originatingSessionId,
1685
+ originatingTurnId: input.originatingTurnId ?? null,
1686
+ originatingAttemptId: input.originatingAttemptId ?? null,
1687
+ originatingToolOperationId: input.originatingToolOperationId ?? null,
1688
+ operationId: request.operationId,
1689
+ expiresAt: sql`now() + (${request.expiresInSeconds} * interval '1 second')`,
1690
+ })
1691
+ .returning();
1692
+ if (!row) throw new Error("Interaction intervention insert returned no row");
1693
+ if (authRun) {
1694
+ const linked = await scopedDb
1695
+ .update(schema.authRuns)
1696
+ .set({
1697
+ interventionId: id,
1698
+ version: authRun.version + 1,
1699
+ updatedAt: sql`now()`,
1700
+ })
1701
+ .where(
1702
+ and(
1703
+ eq(schema.authRuns.workspaceId, input.workspaceId),
1704
+ eq(schema.authRuns.id, authRun.id),
1705
+ eq(schema.authRuns.version, authRun.version),
1706
+ sql`${schema.authRuns.settledAt} is null`,
1707
+ ),
1708
+ )
1709
+ .returning({ id: schema.authRuns.id });
1710
+ if (linked.length !== 1) {
1711
+ throw new InteractionResourceConflictError("Auth run changed before intervention link");
1712
+ }
1713
+ }
1714
+ const response = InteractionInterventionMutationResponse.parse({
1715
+ intervention: interventionFromRow(row),
1716
+ operationId: request.operationId,
1717
+ replayed: false,
1718
+ });
1719
+ await insertCompletedOperation(scopedDb, {
1720
+ operationId: request.operationId,
1721
+ accountId: input.accountId,
1722
+ workspaceId: input.workspaceId,
1723
+ resourceKind: "intervention",
1724
+ resourceId: id,
1725
+ kind: "create",
1726
+ requestDigest: digest,
1727
+ resultVersion: row.version,
1728
+ result: response,
1729
+ actorSubjectId: input.actorSubjectId,
1730
+ });
1731
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
1732
+ return response;
1733
+ });
1734
+ } catch (error) {
1735
+ if (
1736
+ postgresConstraint(error) === "interaction_interventions_open_target_kind_uq" ||
1737
+ postgresConstraint(error) === "interaction_interventions_open_auth_run_uq"
1738
+ ) {
1739
+ throw new InteractionResourceConflictError(
1740
+ "A matching interaction intervention is already open",
1741
+ );
1742
+ }
1743
+ throw error;
1744
+ }
1745
+ }
1746
+
1747
+ export async function resolveInteractionIntervention(
1748
+ db: Database,
1749
+ input: InteractionMutationScope & {
1750
+ interventionId: string;
1751
+ } & ResolveInteractionInterventionRequestValue,
1752
+ ): Promise<InteractionInterventionMutationResponseValue> {
1753
+ const request = ResolveInteractionInterventionRequest.parse({
1754
+ operationId: input.operationId,
1755
+ expectedVersion: input.expectedVersion,
1756
+ outcome: input.outcome,
1757
+ });
1758
+ const { operationId: _operationId, ...digestRequest } = request;
1759
+ const digest = operationDigest({
1760
+ version: 1,
1761
+ interventionId: input.interventionId,
1762
+ request: digestRequest,
1763
+ actorSubjectId: input.actorSubjectId,
1764
+ });
1765
+ return await withRlsContext(db, input, async (scopedDb) => {
1766
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
1767
+ await lockOperation(scopedDb, request.operationId);
1768
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
1769
+ if (existing) {
1770
+ assertOperation(existing, {
1771
+ resourceKind: "intervention",
1772
+ resourceId: input.interventionId,
1773
+ kind: "resolve",
1774
+ requestDigest: digest,
1775
+ actorSubjectId: input.actorSubjectId,
1776
+ });
1777
+ const response = InteractionInterventionMutationResponse.parse(existing.result);
1778
+ return { ...response, replayed: true };
1779
+ }
1780
+ const [current] = await scopedDb
1781
+ .select()
1782
+ .from(schema.interactionInterventions)
1783
+ .where(
1784
+ and(
1785
+ eq(schema.interactionInterventions.workspaceId, input.workspaceId),
1786
+ eq(schema.interactionInterventions.id, input.interventionId),
1787
+ ),
1788
+ )
1789
+ .for("update")
1790
+ .limit(1);
1791
+ if (!current) {
1792
+ throw new InteractionResourceNotFoundError("Interaction intervention not found");
1793
+ }
1794
+ if (current.version !== request.expectedVersion) {
1795
+ throw new InteractionResourceConflictError(
1796
+ "Interaction intervention changed before this response",
1797
+ );
1798
+ }
1799
+ if (current.status !== "open") {
1800
+ throw new InteractionResourceStateError("Interaction intervention is already settled");
1801
+ }
1802
+ const expired = current.expiresAt.getTime() <= Date.now();
1803
+ const status = expired ? "expired" : request.outcome;
1804
+ const [row] = await scopedDb
1805
+ .update(schema.interactionInterventions)
1806
+ .set({
1807
+ status,
1808
+ responseActorSubjectId: expired ? null : input.actorSubjectId,
1809
+ version: current.version + 1,
1810
+ updatedAt: sql`now()`,
1811
+ settledAt: sql`now()`,
1812
+ })
1813
+ .where(
1814
+ and(
1815
+ eq(schema.interactionInterventions.workspaceId, input.workspaceId),
1816
+ eq(schema.interactionInterventions.id, input.interventionId),
1817
+ eq(schema.interactionInterventions.version, request.expectedVersion),
1818
+ eq(schema.interactionInterventions.status, "open"),
1819
+ ),
1820
+ )
1821
+ .returning();
1822
+ if (!row) {
1823
+ throw new InteractionResourceConflictError("Intervention response lost its fence");
1824
+ }
1825
+ await settleLinkedAuthRun(scopedDb, {
1826
+ workspaceId: input.workspaceId,
1827
+ authRunId: row.authRunId,
1828
+ interventionId: row.id,
1829
+ outcome: status,
1830
+ });
1831
+ const response = InteractionInterventionMutationResponse.parse({
1832
+ intervention: interventionFromRow(row),
1833
+ operationId: request.operationId,
1834
+ replayed: false,
1835
+ });
1836
+ await insertCompletedOperation(scopedDb, {
1837
+ operationId: request.operationId,
1838
+ accountId: input.accountId,
1839
+ workspaceId: input.workspaceId,
1840
+ resourceKind: "intervention",
1841
+ resourceId: input.interventionId,
1842
+ kind: "resolve",
1843
+ requestDigest: digest,
1844
+ resultVersion: row.version,
1845
+ result: response,
1846
+ actorSubjectId: input.actorSubjectId,
1847
+ });
1848
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
1849
+ return response;
1850
+ });
1851
+ }
1852
+
1853
+ export async function cancelOpenInteractionInterventions(
1854
+ db: Database,
1855
+ input: {
1856
+ accountId: string;
1857
+ workspaceId: string;
1858
+ resourceKind: "browser_session" | "computer_session";
1859
+ resourceId: string;
1860
+ },
1861
+ ): Promise<number> {
1862
+ return await withRlsContext(db, input, async (scopedDb) => {
1863
+ const cancelled = await scopedDb
1864
+ .update(schema.interactionInterventions)
1865
+ .set({
1866
+ status: "cancelled",
1867
+ version: sql`${schema.interactionInterventions.version} + 1`,
1868
+ updatedAt: sql`now()`,
1869
+ settledAt: sql`now()`,
1870
+ })
1871
+ .where(
1872
+ and(
1873
+ eq(schema.interactionInterventions.workspaceId, input.workspaceId),
1874
+ eq(schema.interactionInterventions.resourceKind, input.resourceKind),
1875
+ eq(schema.interactionInterventions.resourceId, input.resourceId),
1876
+ eq(schema.interactionInterventions.status, "open"),
1877
+ ),
1878
+ )
1879
+ .returning({
1880
+ id: schema.interactionInterventions.id,
1881
+ authRunId: schema.interactionInterventions.authRunId,
1882
+ });
1883
+ for (const intervention of cancelled) {
1884
+ await settleLinkedAuthRun(scopedDb, {
1885
+ workspaceId: input.workspaceId,
1886
+ authRunId: intervention.authRunId,
1887
+ interventionId: intervention.id,
1888
+ outcome: "cancelled",
1889
+ });
1890
+ }
1891
+ if (cancelled.length > 0) {
1892
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
1893
+ }
1894
+ return cancelled.length;
1895
+ });
1896
+ }