@opengeni/db 0.28.9 → 0.36.0

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 (225) 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 +299 -0
  4. package/dist/browser-downloads.d.ts +51 -0
  5. package/dist/browser-identities.d.ts +137 -0
  6. package/dist/browser-sessions.d.ts +354 -0
  7. package/dist/browser-state-artifacts.d.ts +91 -0
  8. package/dist/canonical-human-identities.d.ts +48 -0
  9. package/dist/canonical-human-identities.js +28 -0
  10. package/dist/canonical-human-identities.js.map +1 -0
  11. package/dist/capability-components.d.ts +37 -0
  12. package/dist/capability-integrations.d.ts +194 -0
  13. package/dist/chunk-5KTIEDX7.js +13200 -0
  14. package/dist/chunk-5KTIEDX7.js.map +1 -0
  15. package/dist/chunk-CQZ4QSP5.js +995 -0
  16. package/dist/chunk-CQZ4QSP5.js.map +1 -0
  17. package/dist/chunk-F32YT7MP.js +117 -0
  18. package/dist/chunk-F32YT7MP.js.map +1 -0
  19. package/dist/chunk-JC5QPLUM.js +666 -0
  20. package/dist/chunk-JC5QPLUM.js.map +1 -0
  21. package/dist/chunk-P3RE4D2B.js +3535 -0
  22. package/dist/chunk-P3RE4D2B.js.map +1 -0
  23. package/dist/chunk-TAUTWOS5.js +259 -0
  24. package/dist/chunk-TAUTWOS5.js.map +1 -0
  25. package/dist/chunk-VKLB2ZGI.js +2389 -0
  26. package/dist/chunk-VKLB2ZGI.js.map +1 -0
  27. package/dist/codemode-operations.d.ts +117 -0
  28. package/dist/codex-token-resolver.d.ts +2 -2
  29. package/dist/company-profile-schema.d.ts +798 -0
  30. package/dist/company-profile.d.ts +268 -0
  31. package/dist/computer-sessions.d.ts +109 -0
  32. package/dist/connection-token-resolver.d.ts +9 -4
  33. package/dist/database.d.ts +57 -3
  34. package/dist/editable-artifact-durable-export.d.ts +153 -0
  35. package/dist/editable-artifact-durable-export.js +788 -0
  36. package/dist/editable-artifact-durable-export.js.map +1 -0
  37. package/dist/editable-artifact-materialization.d.ts +123 -0
  38. package/dist/editable-artifacts-schema.d.ts +4152 -0
  39. package/dist/editable-artifacts.d.ts +610 -0
  40. package/dist/editable-artifacts.js +26 -0
  41. package/dist/editable-artifacts.js.map +1 -0
  42. package/dist/generated-images.d.ts +139 -0
  43. package/dist/index.d.ts +1122 -126
  44. package/dist/index.js +42329 -18407
  45. package/dist/index.js.map +1 -1
  46. package/dist/insights.d.ts +73 -10
  47. package/dist/integration-bindings.d.ts +70 -0
  48. package/dist/integration-facets.d.ts +99 -0
  49. package/dist/interaction-revisions.d.ts +14 -0
  50. package/dist/interaction-schema.d.ts +6036 -0
  51. package/dist/knowledge-source-sync-schema.d.ts +1679 -0
  52. package/dist/knowledge-source-sync.d.ts +274 -0
  53. package/dist/memory-governance.d.ts +2 -2
  54. package/dist/memory-slack-delivery.d.ts +101 -0
  55. package/dist/new-session-drafts.d.ts +2 -2
  56. package/dist/pack-components.d.ts +76 -0
  57. package/dist/pack-installations.d.ts +86 -0
  58. package/dist/persistence-errors.d.ts +3 -3
  59. package/dist/plugin-packages.d.ts +126 -0
  60. package/dist/preference-registry.d.ts +1 -1
  61. package/dist/provision-roles.d.ts +9 -1
  62. package/dist/provision-roles.js +1 -1
  63. package/dist/runtime-posture.d.ts +19 -6
  64. package/dist/schema.d.ts +22654 -9359
  65. package/dist/schema.js +209 -3
  66. package/dist/scoped-knowledge.d.ts +97 -1
  67. package/dist/session-control.d.ts +33 -7
  68. package/dist/session-queue-commands.d.ts +19 -11
  69. package/dist/session-realtime-context.d.ts +2 -2
  70. package/dist/session-realtime-ledger.d.ts +4 -4
  71. package/dist/session-realtime-mirror.d.ts +2 -2
  72. package/dist/session-realtime-state.d.ts +1 -1
  73. package/dist/session-realtime-terminal.d.ts +1 -1
  74. package/dist/session-realtime.d.ts +7 -7
  75. package/dist/session-tenancy.d.ts +44 -0
  76. package/dist/session-tenancy.js +16 -0
  77. package/dist/session-tenancy.js.map +1 -0
  78. package/dist/session-tool-call-settlement.d.ts +1 -1
  79. package/dist/slack-task-policy-schema.d.ts +847 -0
  80. package/dist/slack-task-policy.d.ts +65 -0
  81. package/dist/slack-user-link-access.d.ts +54 -0
  82. package/dist/task-notes-schema.d.ts +924 -0
  83. package/dist/task-notes.d.ts +85 -0
  84. package/dist/transcription-recordings.d.ts +2 -2
  85. package/dist/turn-initiator.d.ts +2 -2
  86. package/dist/video-generation.d.ts +225 -0
  87. package/dist/video-generation.js +68 -0
  88. package/dist/video-generation.js.map +1 -0
  89. package/dist/workspace-artifacts.d.ts +1 -1
  90. package/dist/workspace-instruction-policies.d.ts +1 -1
  91. package/dist/workspace-learning-policy-schema.d.ts +932 -0
  92. package/dist/workspace-learning-policy.d.ts +161 -0
  93. package/dist/xai-subscription.d.ts +326 -0
  94. package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
  95. package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
  96. package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
  97. package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
  98. package/drizzle/0187_generated_image_artifacts.sql +173 -0
  99. package/drizzle/0188_image_generation_retention_failure.sql +25 -0
  100. package/drizzle/0189_retained_session_image_formats.sql +13 -0
  101. package/drizzle/0190_timeline_annotations.sql +31 -0
  102. package/drizzle/0191_editable_artifact_engine.sql +3853 -0
  103. package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
  104. package/drizzle/0193_editable_artifact_authorization.sql +190 -0
  105. package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
  106. package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
  107. package/drizzle/0196_rig_provider_images.sql +152 -0
  108. package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
  109. package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
  110. package/drizzle/0199_workspace_learning_policy.sql +980 -0
  111. package/drizzle/0201_company_profile_authority.sql +849 -0
  112. package/drizzle/0202_document_index_checkpoints.sql +85 -0
  113. package/drizzle/0203_durable_video_generation.sql +353 -0
  114. package/drizzle/0204_video_generation_funding.sql +44 -0
  115. package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
  116. package/drizzle/0206_browser_sessions.sql +646 -0
  117. package/drizzle/0207_browser_identities.sql +379 -0
  118. package/drizzle/0208_attached_browser_devices.sql +165 -0
  119. package/drizzle/0209_computer_sessions.sql +575 -0
  120. package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
  121. package/drizzle/0211_editable_artifact_session_links.sql +58 -0
  122. package/drizzle/0212_browser_state_transfer_hardening.sql +560 -0
  123. package/drizzle/0212_slack_installation_bindings.sql +348 -0
  124. package/drizzle/0213_browser_interaction_authority.sql +67 -0
  125. package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
  126. package/drizzle/0214_browser_download_saves.sql +34 -0
  127. package/drizzle/0214_session_activity_commit_gate.sql +262 -0
  128. package/drizzle/0215_browser_controller_host.sql +385 -0
  129. package/drizzle/0215_capabilities_platform.sql +980 -0
  130. package/drizzle/0216_browser_auth_health_evidence.sql +137 -0
  131. package/drizzle/0216_pack_component_ownership.sql +235 -0
  132. package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
  133. package/drizzle/0217_external_browser_auth_operations.sql +22 -0
  134. package/drizzle/0218_organization_tenancy_foundation.sql +299 -0
  135. package/drizzle/0219_organization_tenancy_managed_human_provisioning.sql +317 -0
  136. package/drizzle/0219_site_auth_maintenance_sessions.sql +234 -0
  137. package/drizzle/0220_memory_slack_append_only_cascade.sql +38 -0
  138. package/drizzle/0220_session_channels.sql +54 -0
  139. package/drizzle/0221_sessions_channel_index.sql +5 -0
  140. package/drizzle/0222_session_visibility_authority_epochs.sql +158 -0
  141. package/drizzle/0222_sessions_channel_fk.sql +89 -0
  142. package/drizzle/0223_pending_tool_event_output.sql +55 -0
  143. package/drizzle/0223_sessions_channel_fk_validate.sql +118 -0
  144. package/drizzle/0224_slack_post_outcome_reconciliation.sql +123 -0
  145. package/drizzle/0225_session_visibility_fork_activation.sql +1363 -0
  146. package/drizzle/0226_personal_codex_authority_foundation.sql +365 -0
  147. package/drizzle/0227_slack_native_actions.sql +145 -0
  148. package/drizzle/0228_interaction_controller_data_plane.sql +14 -0
  149. package/drizzle/0228_slack_task_policy.sql +400 -0
  150. package/drizzle/0229_slack_inbox_file_fact.sql +10 -0
  151. package/drizzle/0230_user_scoped_variable_sets_rigs.sql +84 -0
  152. package/drizzle/0231_integration_definition_identity_cutover.sql +271 -0
  153. package/drizzle/0232_integration_facet_authority_cutover.sql +610 -0
  154. package/drizzle/0233_skill_and_integration_authority_cutover.sql +1162 -0
  155. package/drizzle/0234_xai_subscription_authority.sql +1139 -0
  156. package/drizzle/0235_canonical_human_login_bindings.sql +920 -0
  157. package/drizzle/0236_browser_identity_lifecycle.sql +39 -0
  158. package/drizzle/0236_session_visibility_slack_policy.sql +72 -0
  159. package/drizzle/0237_interaction_transition_reaper.sql +263 -0
  160. package/drizzle/0238_goal_persistence_policy.sql +471 -0
  161. package/drizzle/0238_supergrok_realtime_model.sql +29 -0
  162. package/drizzle/0239_supergrok_video_funding.sql +50 -0
  163. package/drizzle/0239_task_tree_notes.sql +990 -0
  164. package/package.json +26 -5
  165. package/src/attached-browser-devices.ts +371 -0
  166. package/src/attempt-tool-catalogs.ts +131 -0
  167. package/src/browser-auth.ts +4373 -0
  168. package/src/browser-downloads.ts +517 -0
  169. package/src/browser-identities.ts +1385 -0
  170. package/src/browser-sessions.ts +2294 -0
  171. package/src/browser-state-artifacts.ts +449 -0
  172. package/src/canonical-human-identities.ts +317 -0
  173. package/src/capability-components.ts +166 -0
  174. package/src/capability-integrations.ts +1571 -0
  175. package/src/codemode-operations.ts +674 -0
  176. package/src/company-profile-schema.ts +143 -0
  177. package/src/company-profile.ts +968 -0
  178. package/src/computer-sessions.ts +1130 -0
  179. package/src/connection-token-resolver.ts +270 -25
  180. package/src/database.ts +468 -24
  181. package/src/editable-artifact-durable-export.ts +1201 -0
  182. package/src/editable-artifact-materialization.ts +893 -0
  183. package/src/editable-artifacts-schema.ts +879 -0
  184. package/src/editable-artifacts.ts +5404 -0
  185. package/src/generated-images.ts +740 -0
  186. package/src/index.ts +9963 -1465
  187. package/src/insights.ts +251 -23
  188. package/src/integration-bindings.ts +455 -0
  189. package/src/integration-facets.ts +862 -0
  190. package/src/interaction-revisions.ts +70 -0
  191. package/src/interaction-schema.ts +1606 -0
  192. package/src/knowledge-source-sync-schema.ts +192 -0
  193. package/src/knowledge-source-sync.ts +1922 -0
  194. package/src/memory-slack-delivery.ts +801 -0
  195. package/src/pack-components.ts +954 -0
  196. package/src/pack-installations.ts +886 -0
  197. package/src/persistence-errors.ts +4 -7
  198. package/src/plugin-packages.ts +1186 -0
  199. package/src/provision-roles.ts +534 -1
  200. package/src/runtime-posture-cli.ts +9 -0
  201. package/src/runtime-posture.ts +721 -5
  202. package/src/schema.ts +6270 -3159
  203. package/src/scoped-knowledge-schema.ts +1 -1
  204. package/src/scoped-knowledge.ts +655 -3
  205. package/src/session-control.ts +130 -42
  206. package/src/session-queue-commands.ts +265 -15
  207. package/src/session-realtime-context.ts +2 -2
  208. package/src/session-realtime-ledger.ts +3 -3
  209. package/src/session-realtime.ts +7 -7
  210. package/src/session-tenancy.ts +188 -0
  211. package/src/session-tool-call-settlement.ts +18 -3
  212. package/src/slack-task-policy-schema.ts +130 -0
  213. package/src/slack-task-policy.ts +341 -0
  214. package/src/slack-user-link-access.ts +702 -0
  215. package/src/task-notes-schema.ts +135 -0
  216. package/src/task-notes.ts +178 -0
  217. package/src/video-generation.ts +1530 -0
  218. package/src/workspace-artifacts.ts +1 -1
  219. package/src/workspace-learning-policy-schema.ts +139 -0
  220. package/src/workspace-learning-policy.ts +595 -0
  221. package/src/xai-subscription.ts +1887 -0
  222. package/dist/chunk-P6CUHXQZ.js +0 -7248
  223. package/dist/chunk-P6CUHXQZ.js.map +0 -1
  224. package/dist/chunk-VHBFHMPC.js +0 -1219
  225. package/dist/chunk-VHBFHMPC.js.map +0 -1
@@ -0,0 +1,4373 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ AuthRun,
4
+ AuthRunListResponse,
5
+ AuthRunMutationResponse,
6
+ BrowserExternalAuthResult,
7
+ CreateInteractionInterventionRequest,
8
+ CreateNetworkRouteRequest,
9
+ CreateSiteAuthConnectionRequest,
10
+ ExternalAuthInteractiveRequest,
11
+ ExternalAuthRunRequest,
12
+ ExternalAuthRunResponse,
13
+ InteractionIntervention,
14
+ InteractionInterventionListResponse,
15
+ InteractionInterventionMutationResponse,
16
+ NetworkRoute,
17
+ NetworkRouteConfiguration,
18
+ NetworkRouteConsistency,
19
+ NetworkRouteListResponse,
20
+ NetworkRouteMutationResponse,
21
+ interactionPlacementsEqual,
22
+ networkRoutePlacementCompatibilityIssue,
23
+ ProtectedAuthFillRequest,
24
+ ProtectedAuthFillResponse,
25
+ ReportAuthRunRequest,
26
+ RequestHumanInteractionToolInput,
27
+ SITE_AUTH_MAINTENANCE_CONNECTION_CONTEXT_KEY,
28
+ SITE_AUTH_MAINTENANCE_OPERATION_CONTEXT_KEY,
29
+ SiteAuthAuthority,
30
+ ResolveInteractionInterventionRequest,
31
+ SiteAuthConnection,
32
+ SiteAuthConnectionListResponse,
33
+ SiteAuthConnectionMutationResponse,
34
+ StartAuthRunRequest,
35
+ UpdateNetworkRouteRequest,
36
+ UpdateSiteAuthConnectionRequest,
37
+ VerifyAuthRunRequest,
38
+ type AuthRun as AuthRunValue,
39
+ type AuthRunListResponse as AuthRunListResponseValue,
40
+ type AuthRunMutationResponse as AuthRunMutationResponseValue,
41
+ type CreateInteractionInterventionRequest as CreateInteractionInterventionRequestValue,
42
+ type CreateNetworkRouteRequest as CreateNetworkRouteRequestValue,
43
+ type CreateSiteAuthConnectionRequest as CreateSiteAuthConnectionRequestValue,
44
+ type BrowserExternalAuthResult as BrowserExternalAuthResultValue,
45
+ type ExternalAuthInteractiveRequest as ExternalAuthInteractiveRequestValue,
46
+ type ExternalAuthRunRequest as ExternalAuthRunRequestValue,
47
+ type ExternalAuthRunResponse as ExternalAuthRunResponseValue,
48
+ type InteractionCredentialAuthorityRef,
49
+ type InteractionIntervention as InteractionInterventionValue,
50
+ type InteractionInterventionListResponse as InteractionInterventionListResponseValue,
51
+ type InteractionInterventionMutationResponse as InteractionInterventionMutationResponseValue,
52
+ type NetworkRoute as NetworkRouteValue,
53
+ type NetworkRouteConfiguration as NetworkRouteConfigurationValue,
54
+ type NetworkRouteListResponse as NetworkRouteListResponseValue,
55
+ type NetworkRouteMutationResponse as NetworkRouteMutationResponseValue,
56
+ type ProtectedAuthFillRequest as ProtectedAuthFillRequestValue,
57
+ type ProtectedAuthFillResponse as ProtectedAuthFillResponseValue,
58
+ type ReportAuthRunRequest as ReportAuthRunRequestValue,
59
+ type ResolveInteractionInterventionRequest as ResolveInteractionInterventionRequestValue,
60
+ type RequestHumanInteractionToolInput as RequestHumanInteractionToolInputValue,
61
+ type SessionEvent,
62
+ type SiteAuthAuthority as SiteAuthAuthorityValue,
63
+ type SiteAuthConnection as SiteAuthConnectionValue,
64
+ type SiteAuthConnectionListResponse as SiteAuthConnectionListResponseValue,
65
+ type SiteAuthConnectionMutationResponse as SiteAuthConnectionMutationResponseValue,
66
+ type StartAuthRunRequest as StartAuthRunRequestValue,
67
+ type UpdateNetworkRouteRequest as UpdateNetworkRouteRequestValue,
68
+ type UpdateSiteAuthConnectionRequest as UpdateSiteAuthConnectionRequestValue,
69
+ type VerifyAuthRunRequest as VerifyAuthRunRequestValue,
70
+ } from "@opengeni/contracts";
71
+ import { and, asc, eq, inArray, sql } from "drizzle-orm";
72
+ import { type Database, rawRows, setSubjectRlsContext, withRlsContext } from "./database";
73
+ import {
74
+ advanceWorkspaceInteractionRevision,
75
+ readWorkspaceInteractionRevision,
76
+ } from "./interaction-revisions";
77
+ import { safeDatabaseErrorFacts } from "./persistence-errors";
78
+ import * as schema from "./schema";
79
+
80
+ type NetworkRouteRow = typeof schema.networkRoutes.$inferSelect;
81
+ type SiteAuthConnectionRow = typeof schema.siteAuthConnections.$inferSelect;
82
+ type AuthRunRow = typeof schema.authRuns.$inferSelect;
83
+ type InterventionRow = typeof schema.interactionInterventions.$inferSelect;
84
+ type ResourceOperationRow = typeof schema.interactionResourceOperations.$inferSelect;
85
+
86
+ type InteractionMutationScope = {
87
+ accountId: string;
88
+ workspaceId: string;
89
+ actorSubjectId: string;
90
+ };
91
+
92
+ type ResourceKind = ResourceOperationRow["resourceKind"];
93
+ type ResourceOperationKind = ResourceOperationRow["kind"];
94
+
95
+ const CONSISTENT_READ = {
96
+ isolationLevel: "repeatable read",
97
+ accessMode: "read only",
98
+ } as const;
99
+ const MAX_SITE_AUTH_MAINTENANCE_RETRY_MS = 15 * 60 * 1_000;
100
+
101
+ export class InteractionResourceNotFoundError extends Error {
102
+ readonly name = "InteractionResourceNotFoundError";
103
+ }
104
+
105
+ export class InteractionResourceConflictError extends Error {
106
+ readonly name = "InteractionResourceConflictError";
107
+ }
108
+
109
+ export class InteractionResourceStateError extends Error {
110
+ readonly name = "InteractionResourceStateError";
111
+ }
112
+
113
+ function iso(value: Date): string {
114
+ return value.toISOString();
115
+ }
116
+
117
+ function canonicalJsonValue(value: unknown): unknown {
118
+ if (Array.isArray(value)) return value.map(canonicalJsonValue);
119
+ if (!value || typeof value !== "object") return value;
120
+ return Object.fromEntries(
121
+ Object.entries(value as Record<string, unknown>)
122
+ .filter(([, entry]) => entry !== undefined)
123
+ .sort(([left], [right]) => left.localeCompare(right))
124
+ .map(([key, entry]) => [key, canonicalJsonValue(entry)]),
125
+ );
126
+ }
127
+
128
+ function operationDigest(value: Record<string, unknown>): string {
129
+ return createHash("sha256")
130
+ .update(JSON.stringify(canonicalJsonValue(value)))
131
+ .digest("hex");
132
+ }
133
+
134
+ function postgresConstraint(error: unknown): string | null {
135
+ return safeDatabaseErrorFacts(error).constraint ?? null;
136
+ }
137
+
138
+ function routeFromRow(row: NetworkRouteRow): NetworkRouteValue {
139
+ return NetworkRoute.parse({
140
+ id: row.id,
141
+ accountId: row.accountId,
142
+ workspaceId: row.workspaceId,
143
+ name: row.name,
144
+ status: row.status,
145
+ configuration: row.configuration,
146
+ consistency: row.consistency,
147
+ version: row.version,
148
+ createdBySubjectId: row.createdBySubjectId,
149
+ createdAt: iso(row.createdAt),
150
+ updatedAt: iso(row.updatedAt),
151
+ });
152
+ }
153
+
154
+ function siteAuthConnectionFromRow(row: SiteAuthConnectionRow): SiteAuthConnectionValue {
155
+ return SiteAuthConnection.parse({
156
+ id: row.id,
157
+ accountId: row.accountId,
158
+ workspaceId: row.workspaceId,
159
+ name: row.name,
160
+ accountLabel: row.accountLabel,
161
+ status: row.status,
162
+ origins: row.origins,
163
+ loginUrl: row.loginUrl,
164
+ verificationUrlPrefixes: row.verificationUrlPrefixes,
165
+ authorities: row.authorities,
166
+ methods: row.methods,
167
+ preferredIdentityId: row.preferredIdentityId,
168
+ preferredPlacement: row.preferredPlacement,
169
+ preferredNetworkRouteId: row.preferredNetworkRouteId,
170
+ healthPolicy: row.healthPolicy,
171
+ verificationState: row.verificationState,
172
+ lastVerifiedAt: row.lastVerifiedAt ? iso(row.lastVerifiedAt) : null,
173
+ lastVerifiedUrl: row.lastVerifiedUrl,
174
+ lastCheckedAt: row.lastCheckedAt ? iso(row.lastCheckedAt) : null,
175
+ nextCheckAt: row.nextCheckAt ? iso(row.nextCheckAt) : null,
176
+ maintenance: row.maintenanceOperationId
177
+ ? {
178
+ action: row.maintenanceAction,
179
+ sessionId: row.maintenanceStartedAt ? row.maintenanceSessionId : null,
180
+ dueAt: iso(row.maintenanceDueAt!),
181
+ startedAt: row.maintenanceStartedAt ? iso(row.maintenanceStartedAt) : null,
182
+ }
183
+ : null,
184
+ repairCode: row.repairCode,
185
+ version: row.version,
186
+ createdBySubjectId: row.createdBySubjectId,
187
+ createdAt: iso(row.createdAt),
188
+ updatedAt: iso(row.updatedAt),
189
+ });
190
+ }
191
+
192
+ function nextMaintainedAuthCheck(
193
+ status: SiteAuthConnectionRow["status"],
194
+ policy: SiteAuthConnectionRow["healthPolicy"],
195
+ lastCheckedAt: Date | null,
196
+ ): Date | null {
197
+ if (status !== "active" || policy.mode !== "maintained" || policy.intervalSeconds === null) {
198
+ return null;
199
+ }
200
+ return lastCheckedAt
201
+ ? new Date(lastCheckedAt.getTime() + policy.intervalSeconds * 1_000)
202
+ : new Date();
203
+ }
204
+
205
+ export type SiteAuthMaintenanceClaim = {
206
+ operationId: string;
207
+ sessionId: string;
208
+ accountId: string;
209
+ workspaceId: string;
210
+ siteAuthConnectionId: string;
211
+ connectionVersion: number;
212
+ action: "health_check" | "repair";
213
+ dueAt: Date;
214
+ claimedAt: Date;
215
+ name: string;
216
+ accountLabel: string;
217
+ loginUrl: string | null;
218
+ verificationUrlPrefixes: string[];
219
+ preferredIdentityId: string | null;
220
+ preferredPlacement: SiteAuthConnectionValue["preferredPlacement"];
221
+ preferredNetworkRouteId: string | null;
222
+ healthPolicy: SiteAuthConnectionValue["healthPolicy"];
223
+ verificationState: SiteAuthConnectionValue["verificationState"];
224
+ };
225
+
226
+ /** Claim a bounded cross-workspace batch. Stale pre-start claims keep the same
227
+ * operation/session ids, so worker loss repairs one durable dispatch. */
228
+ export async function claimSiteAuthMaintenance(
229
+ db: Database,
230
+ input: { claimTimeoutMs: number; limit: number },
231
+ ): Promise<SiteAuthMaintenanceClaim[]> {
232
+ if (!Number.isSafeInteger(input.claimTimeoutMs) || input.claimTimeoutMs < 0) {
233
+ throw new Error("site auth maintenance claim timeout must be a non-negative safe integer");
234
+ }
235
+ if (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 1_000) {
236
+ throw new Error("site auth maintenance claim limit must be between 1 and 1000");
237
+ }
238
+ const rows = await rawRows<{
239
+ operation_id: string;
240
+ session_id: string;
241
+ account_id: string;
242
+ workspace_id: string;
243
+ site_auth_connection_id: string;
244
+ connection_version: number;
245
+ action: "health_check" | "repair";
246
+ due_at: Date;
247
+ claimed_at: Date;
248
+ name: string;
249
+ account_label: string;
250
+ login_url: string | null;
251
+ verification_url_prefixes: string[];
252
+ preferred_identity_id: string | null;
253
+ preferred_placement: SiteAuthConnectionValue["preferredPlacement"];
254
+ preferred_network_route_id: string | null;
255
+ health_policy: SiteAuthConnectionValue["healthPolicy"];
256
+ verification_state: SiteAuthConnectionValue["verificationState"];
257
+ }>(
258
+ db,
259
+ sql`
260
+ select *
261
+ from opengeni_private.claim_site_auth_maintenance(
262
+ ${input.claimTimeoutMs},
263
+ ${input.limit}
264
+ )
265
+ `,
266
+ );
267
+ return rows.map((row) => ({
268
+ operationId: row.operation_id,
269
+ sessionId: row.session_id,
270
+ accountId: row.account_id,
271
+ workspaceId: row.workspace_id,
272
+ siteAuthConnectionId: row.site_auth_connection_id,
273
+ connectionVersion: Number(row.connection_version),
274
+ action: row.action,
275
+ dueAt: new Date(row.due_at),
276
+ claimedAt: new Date(row.claimed_at),
277
+ name: row.name,
278
+ accountLabel: row.account_label,
279
+ loginUrl: row.login_url,
280
+ verificationUrlPrefixes: row.verification_url_prefixes,
281
+ preferredIdentityId: row.preferred_identity_id,
282
+ preferredPlacement: row.preferred_placement,
283
+ preferredNetworkRouteId: row.preferred_network_route_id,
284
+ healthPolicy: row.health_policy,
285
+ verificationState: row.verification_state,
286
+ }));
287
+ }
288
+
289
+ /** Link the exact hidden maintenance claim to its service-created session in
290
+ * the caller's create transaction. A false result must abort that transaction. */
291
+ export async function confirmSiteAuthMaintenanceSessionInTransaction(
292
+ tx: Database,
293
+ input: {
294
+ accountId: string;
295
+ workspaceId: string;
296
+ siteAuthConnectionId: string;
297
+ operationId: string;
298
+ sessionId: string;
299
+ },
300
+ ): Promise<boolean> {
301
+ const [session] = await tx
302
+ .select({
303
+ createdByKind: schema.sessions.createdByKind,
304
+ createdBySubjectId: schema.sessions.createdBySubjectId,
305
+ createdByContext: schema.sessions.createdByContext,
306
+ })
307
+ .from(schema.sessions)
308
+ .where(
309
+ and(
310
+ eq(schema.sessions.workspaceId, input.workspaceId),
311
+ eq(schema.sessions.id, input.sessionId),
312
+ ),
313
+ )
314
+ .limit(1);
315
+ if (
316
+ !session ||
317
+ session.createdByKind !== "service" ||
318
+ session.createdBySubjectId !== "site-auth-maintenance" ||
319
+ session.createdByContext[SITE_AUTH_MAINTENANCE_OPERATION_CONTEXT_KEY] !== input.operationId ||
320
+ session.createdByContext[SITE_AUTH_MAINTENANCE_CONNECTION_CONTEXT_KEY] !==
321
+ input.siteAuthConnectionId
322
+ ) {
323
+ return false;
324
+ }
325
+ const startedAt = new Date();
326
+ const [confirmed] = await tx
327
+ .update(schema.siteAuthConnections)
328
+ .set({
329
+ maintenanceStartedAt: startedAt,
330
+ version: sql`${schema.siteAuthConnections.version} + 1`,
331
+ updatedBySubjectId: "site-auth-maintenance",
332
+ updatedAt: startedAt,
333
+ })
334
+ .where(
335
+ and(
336
+ eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
337
+ eq(schema.siteAuthConnections.id, input.siteAuthConnectionId),
338
+ eq(schema.siteAuthConnections.maintenanceOperationId, input.operationId),
339
+ eq(schema.siteAuthConnections.maintenanceSessionId, input.sessionId),
340
+ sql`${schema.siteAuthConnections.maintenanceStartedAt} is null`,
341
+ ),
342
+ )
343
+ .returning({ id: schema.siteAuthConnections.id });
344
+ if (!confirmed) {
345
+ const [existing] = await tx
346
+ .select({ startedAt: schema.siteAuthConnections.maintenanceStartedAt })
347
+ .from(schema.siteAuthConnections)
348
+ .where(
349
+ and(
350
+ eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
351
+ eq(schema.siteAuthConnections.id, input.siteAuthConnectionId),
352
+ eq(schema.siteAuthConnections.maintenanceOperationId, input.operationId),
353
+ eq(schema.siteAuthConnections.maintenanceSessionId, input.sessionId),
354
+ ),
355
+ )
356
+ .limit(1);
357
+ return existing?.startedAt !== null && existing?.startedAt !== undefined;
358
+ }
359
+ await advanceWorkspaceInteractionRevision(tx, input.accountId, input.workspaceId);
360
+ return true;
361
+ }
362
+
363
+ /** Standalone wrapper retained for exact DB callers and tests. Session creation
364
+ * uses the in-transaction form so a changed claim cannot leave an empty shell. */
365
+ export async function confirmSiteAuthMaintenanceSession(
366
+ db: Database,
367
+ input: Parameters<typeof confirmSiteAuthMaintenanceSessionInTransaction>[1],
368
+ ): Promise<boolean> {
369
+ return await withRlsContext(db, input, async (scopedDb) =>
370
+ confirmSiteAuthMaintenanceSessionInTransaction(scopedDb, input),
371
+ );
372
+ }
373
+
374
+ /** Release an exact claim that failed before a maintenance session started. */
375
+ export async function deferSiteAuthMaintenance(
376
+ db: Database,
377
+ input: {
378
+ accountId: string;
379
+ workspaceId: string;
380
+ siteAuthConnectionId: string;
381
+ operationId: string;
382
+ sessionId: string;
383
+ retryAt: Date;
384
+ },
385
+ ): Promise<boolean> {
386
+ return await withRlsContext(db, input, async (scopedDb) =>
387
+ scopedDb.transaction(async (tx) => {
388
+ const [deferred] = await tx
389
+ .update(schema.siteAuthConnections)
390
+ .set({
391
+ maintenanceOperationId: null,
392
+ maintenanceAction: null,
393
+ maintenanceDueAt: null,
394
+ maintenanceClaimedAt: null,
395
+ maintenanceSessionId: null,
396
+ maintenanceStartedAt: null,
397
+ nextCheckAt: input.retryAt,
398
+ version: sql`${schema.siteAuthConnections.version} + 1`,
399
+ updatedBySubjectId: "site-auth-maintenance",
400
+ updatedAt: new Date(),
401
+ })
402
+ .where(
403
+ and(
404
+ eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
405
+ eq(schema.siteAuthConnections.id, input.siteAuthConnectionId),
406
+ eq(schema.siteAuthConnections.maintenanceOperationId, input.operationId),
407
+ eq(schema.siteAuthConnections.maintenanceSessionId, input.sessionId),
408
+ sql`${schema.siteAuthConnections.maintenanceStartedAt} is null`,
409
+ ),
410
+ )
411
+ .returning({ id: schema.siteAuthConnections.id });
412
+ if (!deferred) return false;
413
+ await advanceWorkspaceInteractionRevision(
414
+ tx as unknown as Database,
415
+ input.accountId,
416
+ input.workspaceId,
417
+ );
418
+ return true;
419
+ }),
420
+ );
421
+ }
422
+
423
+ async function lockOperation(db: Database, operationId: string): Promise<void> {
424
+ await db.execute(
425
+ sql`select pg_advisory_xact_lock(hashtextextended(${`interaction-resource:${operationId}`}, 0))`,
426
+ );
427
+ }
428
+
429
+ async function loadOperation(
430
+ db: Database,
431
+ workspaceId: string,
432
+ operationId: string,
433
+ ): Promise<ResourceOperationRow | null> {
434
+ const [row] = await db
435
+ .select()
436
+ .from(schema.interactionResourceOperations)
437
+ .where(
438
+ and(
439
+ eq(schema.interactionResourceOperations.workspaceId, workspaceId),
440
+ eq(schema.interactionResourceOperations.operationId, operationId),
441
+ ),
442
+ )
443
+ .limit(1);
444
+ return row ?? null;
445
+ }
446
+
447
+ function assertOperationIdentity(
448
+ operation: ResourceOperationRow,
449
+ expected: {
450
+ resourceKind: ResourceKind;
451
+ resourceId?: string;
452
+ kind: ResourceOperationKind;
453
+ requestDigest: string;
454
+ actorSubjectId: string;
455
+ },
456
+ ): void {
457
+ if (
458
+ operation.resourceKind !== expected.resourceKind ||
459
+ (expected.resourceId !== undefined && operation.resourceId !== expected.resourceId) ||
460
+ operation.kind !== expected.kind ||
461
+ operation.requestDigest !== expected.requestDigest ||
462
+ operation.actorSubjectId !== expected.actorSubjectId
463
+ ) {
464
+ throw new InteractionResourceConflictError(
465
+ "Operation id is already bound to another interaction request",
466
+ );
467
+ }
468
+ }
469
+
470
+ function assertOperation(
471
+ operation: ResourceOperationRow,
472
+ expected: {
473
+ resourceKind: ResourceKind;
474
+ resourceId?: string;
475
+ kind: ResourceOperationKind;
476
+ requestDigest: string;
477
+ actorSubjectId: string;
478
+ },
479
+ ): void {
480
+ assertOperationIdentity(operation, expected);
481
+ if (operation.state !== "completed" || !operation.result || !operation.resultVersion) {
482
+ throw new InteractionResourceStateError("Interaction operation is not complete");
483
+ }
484
+ }
485
+
486
+ async function insertCompletedOperation(
487
+ db: Database,
488
+ input: {
489
+ operationId: string;
490
+ accountId: string;
491
+ workspaceId: string;
492
+ resourceKind: ResourceKind;
493
+ resourceId: string;
494
+ kind: ResourceOperationKind;
495
+ requestDigest: string;
496
+ resultVersion: number;
497
+ result: Record<string, unknown>;
498
+ actorSubjectId: string;
499
+ },
500
+ ): Promise<void> {
501
+ await db.insert(schema.interactionResourceOperations).values({
502
+ ...input,
503
+ state: "completed",
504
+ settledAt: sql`now()`,
505
+ });
506
+ }
507
+
508
+ async function assertWorkspaceAccount(
509
+ db: Database,
510
+ accountId: string,
511
+ workspaceId: string,
512
+ ): Promise<void> {
513
+ const [workspace] = await db
514
+ .select({ id: schema.workspaces.id })
515
+ .from(schema.workspaces)
516
+ .where(and(eq(schema.workspaces.id, workspaceId), eq(schema.workspaces.accountId, accountId)))
517
+ .limit(1);
518
+ if (!workspace) throw new InteractionResourceNotFoundError("Workspace not found");
519
+ }
520
+
521
+ function credentialRefsFromRoute(
522
+ configuration: NetworkRouteConfigurationValue,
523
+ ): InteractionCredentialAuthorityRef[] {
524
+ return "credential" in configuration && configuration.credential
525
+ ? [configuration.credential]
526
+ : [];
527
+ }
528
+
529
+ function credentialRefsFromAuthorities(
530
+ authorities: readonly SiteAuthAuthorityValue[],
531
+ ): InteractionCredentialAuthorityRef[] {
532
+ return authorities.flatMap((authority) =>
533
+ "credential" in authority && authority.credential ? [authority.credential] : [],
534
+ );
535
+ }
536
+
537
+ function credentialRefKey(reference: InteractionCredentialAuthorityRef): string {
538
+ return `${reference.connectionId}\u0000${reference.connectionSubjectId ?? ""}\u0000${reference.providerDomain}`;
539
+ }
540
+
541
+ function introducedCredentialRefs(
542
+ current: readonly InteractionCredentialAuthorityRef[],
543
+ candidate: readonly InteractionCredentialAuthorityRef[],
544
+ ): InteractionCredentialAuthorityRef[] {
545
+ const trusted = new Set(current.map(credentialRefKey));
546
+ return candidate.filter((reference) => !trusted.has(credentialRefKey(reference)));
547
+ }
548
+
549
+ async function assertCredentialAuthorities(
550
+ db: Database,
551
+ workspaceId: string,
552
+ refs: readonly InteractionCredentialAuthorityRef[],
553
+ ): Promise<void> {
554
+ if (refs.length === 0) return;
555
+ const ids = [...new Set(refs.map((reference) => reference.connectionId))];
556
+ const rows = await db
557
+ .select({
558
+ id: schema.connections.id,
559
+ subjectId: schema.connections.subjectId,
560
+ providerDomain: schema.connections.providerDomain,
561
+ status: schema.connections.status,
562
+ })
563
+ .from(schema.connections)
564
+ .where(
565
+ and(eq(schema.connections.workspaceId, workspaceId), inArray(schema.connections.id, ids)),
566
+ );
567
+ const byId = new Map(rows.map((row) => [row.id, row]));
568
+ for (const reference of refs) {
569
+ const row = byId.get(reference.connectionId);
570
+ if (
571
+ !row ||
572
+ row.subjectId !== reference.connectionSubjectId ||
573
+ row.providerDomain !== reference.providerDomain
574
+ ) {
575
+ throw new InteractionResourceNotFoundError("Credential authority not found");
576
+ }
577
+ if (row.status !== "active") {
578
+ throw new InteractionResourceStateError("Credential authority is not active");
579
+ }
580
+ }
581
+ }
582
+
583
+ async function assertActiveBrowserIdentity(
584
+ db: Database,
585
+ workspaceId: string,
586
+ identityId: string | null,
587
+ ): Promise<void> {
588
+ if (!identityId) return;
589
+ const [identity] = await db
590
+ .select({ status: schema.browserIdentities.status })
591
+ .from(schema.browserIdentities)
592
+ .where(
593
+ and(
594
+ eq(schema.browserIdentities.workspaceId, workspaceId),
595
+ eq(schema.browserIdentities.id, identityId),
596
+ ),
597
+ )
598
+ .limit(1);
599
+ if (!identity) throw new InteractionResourceNotFoundError("Preferred browser identity not found");
600
+ if (identity.status !== "active") {
601
+ throw new InteractionResourceStateError("Preferred browser identity is archived");
602
+ }
603
+ }
604
+
605
+ async function assertActiveNetworkRoute(
606
+ db: Database,
607
+ workspaceId: string,
608
+ routeId: string | null,
609
+ ): Promise<NetworkRouteValue | null> {
610
+ if (!routeId) return null;
611
+ const [route] = await db
612
+ .select()
613
+ .from(schema.networkRoutes)
614
+ .where(
615
+ and(eq(schema.networkRoutes.workspaceId, workspaceId), eq(schema.networkRoutes.id, routeId)),
616
+ )
617
+ .for("share")
618
+ .limit(1);
619
+ if (!route) throw new InteractionResourceNotFoundError("Preferred network route not found");
620
+ if (route.status !== "active") {
621
+ throw new InteractionResourceStateError("Preferred network route is archived");
622
+ }
623
+ return routeFromRow(route);
624
+ }
625
+
626
+ async function assertNetworkRouteUpdateCompatibleWithSiteAuthConnections(
627
+ db: Database,
628
+ input: {
629
+ workspaceId: string;
630
+ routeId: string;
631
+ status: NetworkRouteValue["status"];
632
+ configuration: NetworkRouteValue["configuration"];
633
+ consistency: NetworkRouteValue["consistency"];
634
+ },
635
+ ): Promise<void> {
636
+ const references = await db
637
+ .select({ preferredPlacement: schema.siteAuthConnections.preferredPlacement })
638
+ .from(schema.siteAuthConnections)
639
+ .where(
640
+ and(
641
+ eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
642
+ eq(schema.siteAuthConnections.preferredNetworkRouteId, input.routeId),
643
+ eq(schema.siteAuthConnections.status, "active"),
644
+ ),
645
+ );
646
+ if (references.length === 0) return;
647
+ if (input.status !== "active") {
648
+ throw new InteractionResourceStateError(
649
+ "Network route is selected by an active site auth connection",
650
+ );
651
+ }
652
+ for (const reference of references) {
653
+ const issue = networkRoutePlacementCompatibilityIssue(
654
+ input.configuration,
655
+ input.consistency,
656
+ reference.preferredPlacement,
657
+ );
658
+ if (issue) {
659
+ throw new InteractionResourceStateError(
660
+ `Network route update is incompatible with an active site auth connection: ${issue}`,
661
+ );
662
+ }
663
+ }
664
+ }
665
+
666
+ function assertSiteAuthRouteCompatibility(
667
+ route: NetworkRouteValue | null,
668
+ placement: SiteAuthConnectionValue["preferredPlacement"],
669
+ ): void {
670
+ if (!route) return;
671
+ const issue = networkRoutePlacementCompatibilityIssue(
672
+ route.configuration,
673
+ route.consistency,
674
+ placement,
675
+ );
676
+ if (issue) throw new InteractionResourceStateError(`Preferred network route: ${issue}`);
677
+ }
678
+
679
+ export async function listNetworkRoutes(
680
+ db: Database,
681
+ input: { accountId: string; workspaceId: string; includeArchived?: boolean },
682
+ ): Promise<NetworkRouteListResponseValue> {
683
+ return await withRlsContext(
684
+ db,
685
+ input,
686
+ async (scopedDb) => {
687
+ const predicate = input.includeArchived
688
+ ? eq(schema.networkRoutes.workspaceId, input.workspaceId)
689
+ : and(
690
+ eq(schema.networkRoutes.workspaceId, input.workspaceId),
691
+ eq(schema.networkRoutes.status, "active"),
692
+ );
693
+ const routes = await scopedDb
694
+ .select()
695
+ .from(schema.networkRoutes)
696
+ .where(predicate)
697
+ .orderBy(asc(schema.networkRoutes.name), asc(schema.networkRoutes.id));
698
+ return NetworkRouteListResponse.parse({
699
+ revision: await readWorkspaceInteractionRevision(scopedDb, input.workspaceId),
700
+ routes: routes.map(routeFromRow),
701
+ });
702
+ },
703
+ CONSISTENT_READ,
704
+ );
705
+ }
706
+
707
+ export async function getNetworkRoute(
708
+ db: Database,
709
+ input: { accountId: string; workspaceId: string; routeId: string },
710
+ ): Promise<NetworkRouteValue> {
711
+ return await withRlsContext(
712
+ db,
713
+ input,
714
+ async (scopedDb) => {
715
+ const [row] = await scopedDb
716
+ .select()
717
+ .from(schema.networkRoutes)
718
+ .where(
719
+ and(
720
+ eq(schema.networkRoutes.workspaceId, input.workspaceId),
721
+ eq(schema.networkRoutes.id, input.routeId),
722
+ ),
723
+ )
724
+ .limit(1);
725
+ if (!row) throw new InteractionResourceNotFoundError("Network route not found");
726
+ return routeFromRow(row);
727
+ },
728
+ CONSISTENT_READ,
729
+ );
730
+ }
731
+
732
+ export async function createNetworkRoute(
733
+ db: Database,
734
+ input: InteractionMutationScope & CreateNetworkRouteRequestValue,
735
+ ): Promise<NetworkRouteMutationResponseValue> {
736
+ const request = CreateNetworkRouteRequest.parse({
737
+ operationId: input.operationId,
738
+ name: input.name,
739
+ configuration: input.configuration,
740
+ consistency: input.consistency,
741
+ });
742
+ const digest = operationDigest({
743
+ version: 1,
744
+ name: request.name,
745
+ configuration: request.configuration,
746
+ consistency: request.consistency,
747
+ actorSubjectId: input.actorSubjectId,
748
+ });
749
+ try {
750
+ return await withRlsContext(db, input, async (scopedDb) => {
751
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
752
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
753
+ await lockOperation(scopedDb, request.operationId);
754
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
755
+ if (existing) {
756
+ assertOperation(existing, {
757
+ resourceKind: "network_route",
758
+ kind: "create",
759
+ requestDigest: digest,
760
+ actorSubjectId: input.actorSubjectId,
761
+ });
762
+ const response = NetworkRouteMutationResponse.parse(existing.result);
763
+ return { ...response, replayed: true };
764
+ }
765
+ await assertCredentialAuthorities(
766
+ scopedDb,
767
+ input.workspaceId,
768
+ credentialRefsFromRoute(request.configuration),
769
+ );
770
+ const id = randomUUID();
771
+ const [row] = await scopedDb
772
+ .insert(schema.networkRoutes)
773
+ .values({
774
+ id,
775
+ accountId: input.accountId,
776
+ workspaceId: input.workspaceId,
777
+ name: request.name,
778
+ configuration: request.configuration,
779
+ consistency: request.consistency,
780
+ createOperationId: request.operationId,
781
+ createdBySubjectId: input.actorSubjectId,
782
+ updatedBySubjectId: input.actorSubjectId,
783
+ })
784
+ .returning();
785
+ if (!row) throw new Error("Network route insert returned no row");
786
+ const response = NetworkRouteMutationResponse.parse({
787
+ route: routeFromRow(row),
788
+ operationId: request.operationId,
789
+ replayed: false,
790
+ });
791
+ await insertCompletedOperation(scopedDb, {
792
+ operationId: request.operationId,
793
+ accountId: input.accountId,
794
+ workspaceId: input.workspaceId,
795
+ resourceKind: "network_route",
796
+ resourceId: id,
797
+ kind: "create",
798
+ requestDigest: digest,
799
+ resultVersion: row.version,
800
+ result: response,
801
+ actorSubjectId: input.actorSubjectId,
802
+ });
803
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
804
+ return response;
805
+ });
806
+ } catch (error) {
807
+ if (postgresConstraint(error) === "network_routes_workspace_active_name_uq") {
808
+ throw new InteractionResourceConflictError("An active network route already uses this name");
809
+ }
810
+ throw error;
811
+ }
812
+ }
813
+
814
+ export async function updateNetworkRoute(
815
+ db: Database,
816
+ input: InteractionMutationScope & {
817
+ routeId: string;
818
+ } & UpdateNetworkRouteRequestValue,
819
+ ): Promise<NetworkRouteMutationResponseValue> {
820
+ const request = UpdateNetworkRouteRequest.parse({
821
+ operationId: input.operationId,
822
+ expectedVersion: input.expectedVersion,
823
+ ...(input.name !== undefined ? { name: input.name } : {}),
824
+ ...(input.status !== undefined ? { status: input.status } : {}),
825
+ ...(input.configuration !== undefined ? { configuration: input.configuration } : {}),
826
+ ...(input.consistency !== undefined ? { consistency: input.consistency } : {}),
827
+ });
828
+ const { operationId: _operationId, ...digestRequest } = request;
829
+ const digest = operationDigest({
830
+ version: 1,
831
+ routeId: input.routeId,
832
+ request: digestRequest,
833
+ actorSubjectId: input.actorSubjectId,
834
+ });
835
+ try {
836
+ return await withRlsContext(db, input, async (scopedDb) => {
837
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
838
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
839
+ await lockOperation(scopedDb, request.operationId);
840
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
841
+ if (existing) {
842
+ assertOperation(existing, {
843
+ resourceKind: "network_route",
844
+ resourceId: input.routeId,
845
+ kind: "update",
846
+ requestDigest: digest,
847
+ actorSubjectId: input.actorSubjectId,
848
+ });
849
+ const response = NetworkRouteMutationResponse.parse(existing.result);
850
+ return { ...response, replayed: true };
851
+ }
852
+ const [current] = await scopedDb
853
+ .select()
854
+ .from(schema.networkRoutes)
855
+ .where(
856
+ and(
857
+ eq(schema.networkRoutes.workspaceId, input.workspaceId),
858
+ eq(schema.networkRoutes.id, input.routeId),
859
+ ),
860
+ )
861
+ .for("update")
862
+ .limit(1);
863
+ if (!current) throw new InteractionResourceNotFoundError("Network route not found");
864
+ if (current.version !== request.expectedVersion) {
865
+ throw new InteractionResourceConflictError("Network route changed before this update");
866
+ }
867
+ const configuration = NetworkRouteConfiguration.parse(
868
+ request.configuration ?? current.configuration,
869
+ );
870
+ const consistency = NetworkRouteConsistency.parse(request.consistency ?? current.consistency);
871
+ await assertCredentialAuthorities(
872
+ scopedDb,
873
+ input.workspaceId,
874
+ introducedCredentialRefs(
875
+ credentialRefsFromRoute(NetworkRouteConfiguration.parse(current.configuration)),
876
+ credentialRefsFromRoute(configuration),
877
+ ),
878
+ );
879
+ if (
880
+ request.configuration !== undefined ||
881
+ request.consistency !== undefined ||
882
+ (request.status !== undefined && request.status !== current.status)
883
+ ) {
884
+ await assertNetworkRouteUpdateCompatibleWithSiteAuthConnections(scopedDb, {
885
+ workspaceId: input.workspaceId,
886
+ routeId: input.routeId,
887
+ status: request.status ?? current.status,
888
+ configuration,
889
+ consistency,
890
+ });
891
+ }
892
+ const [row] = await scopedDb
893
+ .update(schema.networkRoutes)
894
+ .set({
895
+ name: request.name ?? current.name,
896
+ status: request.status ?? current.status,
897
+ configuration,
898
+ consistency,
899
+ version: current.version + 1,
900
+ updatedBySubjectId: input.actorSubjectId,
901
+ updatedAt: sql`now()`,
902
+ })
903
+ .where(
904
+ and(
905
+ eq(schema.networkRoutes.workspaceId, input.workspaceId),
906
+ eq(schema.networkRoutes.id, input.routeId),
907
+ eq(schema.networkRoutes.version, request.expectedVersion),
908
+ ),
909
+ )
910
+ .returning();
911
+ if (!row) throw new InteractionResourceConflictError("Network route update lost its fence");
912
+ const response = NetworkRouteMutationResponse.parse({
913
+ route: routeFromRow(row),
914
+ operationId: request.operationId,
915
+ replayed: false,
916
+ });
917
+ await insertCompletedOperation(scopedDb, {
918
+ operationId: request.operationId,
919
+ accountId: input.accountId,
920
+ workspaceId: input.workspaceId,
921
+ resourceKind: "network_route",
922
+ resourceId: input.routeId,
923
+ kind: "update",
924
+ requestDigest: digest,
925
+ resultVersion: row.version,
926
+ result: response,
927
+ actorSubjectId: input.actorSubjectId,
928
+ });
929
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
930
+ return response;
931
+ });
932
+ } catch (error) {
933
+ if (postgresConstraint(error) === "network_routes_workspace_active_name_uq") {
934
+ throw new InteractionResourceConflictError("An active network route already uses this name");
935
+ }
936
+ throw error;
937
+ }
938
+ }
939
+
940
+ export async function listSiteAuthConnections(
941
+ db: Database,
942
+ input: { accountId: string; workspaceId: string; includeArchived?: boolean },
943
+ ): Promise<SiteAuthConnectionListResponseValue> {
944
+ return await withRlsContext(
945
+ db,
946
+ input,
947
+ async (scopedDb) => {
948
+ const predicate = input.includeArchived
949
+ ? eq(schema.siteAuthConnections.workspaceId, input.workspaceId)
950
+ : and(
951
+ eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
952
+ eq(schema.siteAuthConnections.status, "active"),
953
+ );
954
+ const connections = await scopedDb
955
+ .select()
956
+ .from(schema.siteAuthConnections)
957
+ .where(predicate)
958
+ .orderBy(asc(schema.siteAuthConnections.name), asc(schema.siteAuthConnections.id));
959
+ return SiteAuthConnectionListResponse.parse({
960
+ revision: await readWorkspaceInteractionRevision(scopedDb, input.workspaceId),
961
+ connections: connections.map(siteAuthConnectionFromRow),
962
+ });
963
+ },
964
+ CONSISTENT_READ,
965
+ );
966
+ }
967
+
968
+ export async function getSiteAuthConnection(
969
+ db: Database,
970
+ input: {
971
+ accountId: string;
972
+ workspaceId: string;
973
+ siteAuthConnectionId: string;
974
+ },
975
+ ): Promise<SiteAuthConnectionValue> {
976
+ return await withRlsContext(
977
+ db,
978
+ input,
979
+ async (scopedDb) => {
980
+ const [row] = await scopedDb
981
+ .select()
982
+ .from(schema.siteAuthConnections)
983
+ .where(
984
+ and(
985
+ eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
986
+ eq(schema.siteAuthConnections.id, input.siteAuthConnectionId),
987
+ ),
988
+ )
989
+ .limit(1);
990
+ if (!row) throw new InteractionResourceNotFoundError("Site auth connection not found");
991
+ return siteAuthConnectionFromRow(row);
992
+ },
993
+ CONSISTENT_READ,
994
+ );
995
+ }
996
+
997
+ async function validateSiteAuthReferences(
998
+ db: Database,
999
+ workspaceId: string,
1000
+ connection: Pick<
1001
+ SiteAuthConnectionValue,
1002
+ "authorities" | "preferredIdentityId" | "preferredNetworkRouteId" | "preferredPlacement"
1003
+ >,
1004
+ ): Promise<void> {
1005
+ await assertCredentialAuthorities(
1006
+ db,
1007
+ workspaceId,
1008
+ credentialRefsFromAuthorities(connection.authorities),
1009
+ );
1010
+ await assertActiveBrowserIdentity(db, workspaceId, connection.preferredIdentityId);
1011
+ const route = await assertActiveNetworkRoute(db, workspaceId, connection.preferredNetworkRouteId);
1012
+ assertSiteAuthRouteCompatibility(route, connection.preferredPlacement);
1013
+ }
1014
+
1015
+ async function validateSiteAuthUpdateReferences(
1016
+ db: Database,
1017
+ workspaceId: string,
1018
+ current: SiteAuthConnectionValue,
1019
+ candidate: SiteAuthConnectionValue,
1020
+ ): Promise<void> {
1021
+ await assertCredentialAuthorities(
1022
+ db,
1023
+ workspaceId,
1024
+ introducedCredentialRefs(
1025
+ credentialRefsFromAuthorities(current.authorities),
1026
+ credentialRefsFromAuthorities(candidate.authorities),
1027
+ ),
1028
+ );
1029
+ if (candidate.preferredIdentityId !== current.preferredIdentityId) {
1030
+ await assertActiveBrowserIdentity(db, workspaceId, candidate.preferredIdentityId);
1031
+ }
1032
+ if (
1033
+ candidate.status === "active" &&
1034
+ (current.status !== "active" ||
1035
+ candidate.preferredNetworkRouteId !== current.preferredNetworkRouteId ||
1036
+ (candidate.preferredPlacement === null) !== (current.preferredPlacement === null) ||
1037
+ (candidate.preferredPlacement !== null &&
1038
+ current.preferredPlacement !== null &&
1039
+ !interactionPlacementsEqual(candidate.preferredPlacement, current.preferredPlacement)))
1040
+ ) {
1041
+ const route = await assertActiveNetworkRoute(
1042
+ db,
1043
+ workspaceId,
1044
+ candidate.preferredNetworkRouteId,
1045
+ );
1046
+ assertSiteAuthRouteCompatibility(route, candidate.preferredPlacement);
1047
+ }
1048
+ }
1049
+
1050
+ export async function createSiteAuthConnection(
1051
+ db: Database,
1052
+ input: InteractionMutationScope & CreateSiteAuthConnectionRequestValue,
1053
+ ): Promise<SiteAuthConnectionMutationResponseValue> {
1054
+ const request = CreateSiteAuthConnectionRequest.parse({
1055
+ operationId: input.operationId,
1056
+ name: input.name,
1057
+ accountLabel: input.accountLabel,
1058
+ origins: input.origins,
1059
+ loginUrl: input.loginUrl,
1060
+ verificationUrlPrefixes: input.verificationUrlPrefixes,
1061
+ authorities: input.authorities,
1062
+ methods: input.methods,
1063
+ preferredIdentityId: input.preferredIdentityId,
1064
+ preferredPlacement: input.preferredPlacement,
1065
+ preferredNetworkRouteId: input.preferredNetworkRouteId,
1066
+ healthPolicy: input.healthPolicy,
1067
+ });
1068
+ const { operationId: _operationId, ...digestRequest } = request;
1069
+ const digest = operationDigest({
1070
+ version: 1,
1071
+ request: digestRequest,
1072
+ actorSubjectId: input.actorSubjectId,
1073
+ });
1074
+ try {
1075
+ return await withRlsContext(db, input, async (scopedDb) => {
1076
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
1077
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
1078
+ await lockOperation(scopedDb, request.operationId);
1079
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
1080
+ if (existing) {
1081
+ assertOperation(existing, {
1082
+ resourceKind: "site_auth_connection",
1083
+ kind: "create",
1084
+ requestDigest: digest,
1085
+ actorSubjectId: input.actorSubjectId,
1086
+ });
1087
+ const response = SiteAuthConnectionMutationResponse.parse(existing.result);
1088
+ return { ...response, replayed: true };
1089
+ }
1090
+ await validateSiteAuthReferences(scopedDb, input.workspaceId, {
1091
+ authorities: request.authorities,
1092
+ preferredIdentityId: request.preferredIdentityId,
1093
+ preferredPlacement: request.preferredPlacement,
1094
+ preferredNetworkRouteId: request.preferredNetworkRouteId,
1095
+ });
1096
+ const id = randomUUID();
1097
+ const [row] = await scopedDb
1098
+ .insert(schema.siteAuthConnections)
1099
+ .values({
1100
+ id,
1101
+ accountId: input.accountId,
1102
+ workspaceId: input.workspaceId,
1103
+ name: request.name,
1104
+ accountLabel: request.accountLabel,
1105
+ origins: request.origins,
1106
+ loginUrl: request.loginUrl,
1107
+ verificationUrlPrefixes: request.verificationUrlPrefixes,
1108
+ authorities: request.authorities,
1109
+ methods: request.methods,
1110
+ preferredIdentityId: request.preferredIdentityId,
1111
+ preferredPlacement: request.preferredPlacement,
1112
+ preferredNetworkRouteId: request.preferredNetworkRouteId,
1113
+ healthPolicy: request.healthPolicy,
1114
+ nextCheckAt: nextMaintainedAuthCheck("active", request.healthPolicy, null),
1115
+ createOperationId: request.operationId,
1116
+ createdBySubjectId: input.actorSubjectId,
1117
+ updatedBySubjectId: input.actorSubjectId,
1118
+ })
1119
+ .returning();
1120
+ if (!row) throw new Error("Site auth connection insert returned no row");
1121
+ const response = SiteAuthConnectionMutationResponse.parse({
1122
+ connection: siteAuthConnectionFromRow(row),
1123
+ operationId: request.operationId,
1124
+ replayed: false,
1125
+ });
1126
+ await insertCompletedOperation(scopedDb, {
1127
+ operationId: request.operationId,
1128
+ accountId: input.accountId,
1129
+ workspaceId: input.workspaceId,
1130
+ resourceKind: "site_auth_connection",
1131
+ resourceId: id,
1132
+ kind: "create",
1133
+ requestDigest: digest,
1134
+ resultVersion: row.version,
1135
+ result: response,
1136
+ actorSubjectId: input.actorSubjectId,
1137
+ });
1138
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
1139
+ return response;
1140
+ });
1141
+ } catch (error) {
1142
+ if (postgresConstraint(error) === "site_auth_connections_workspace_active_name_uq") {
1143
+ throw new InteractionResourceConflictError(
1144
+ "An active site auth connection already uses this name",
1145
+ );
1146
+ }
1147
+ throw error;
1148
+ }
1149
+ }
1150
+
1151
+ export async function updateSiteAuthConnection(
1152
+ db: Database,
1153
+ input: InteractionMutationScope & {
1154
+ siteAuthConnectionId: string;
1155
+ } & UpdateSiteAuthConnectionRequestValue,
1156
+ ): Promise<SiteAuthConnectionMutationResponseValue> {
1157
+ const request = UpdateSiteAuthConnectionRequest.parse({
1158
+ operationId: input.operationId,
1159
+ expectedVersion: input.expectedVersion,
1160
+ ...(input.name !== undefined ? { name: input.name } : {}),
1161
+ ...(input.accountLabel !== undefined ? { accountLabel: input.accountLabel } : {}),
1162
+ ...(input.origins !== undefined ? { origins: input.origins } : {}),
1163
+ ...(input.loginUrl !== undefined ? { loginUrl: input.loginUrl } : {}),
1164
+ ...(input.verificationUrlPrefixes !== undefined
1165
+ ? { verificationUrlPrefixes: input.verificationUrlPrefixes }
1166
+ : {}),
1167
+ ...(input.authorities !== undefined ? { authorities: input.authorities } : {}),
1168
+ ...(input.methods !== undefined ? { methods: input.methods } : {}),
1169
+ ...(input.preferredIdentityId !== undefined
1170
+ ? { preferredIdentityId: input.preferredIdentityId }
1171
+ : {}),
1172
+ ...(input.preferredPlacement !== undefined
1173
+ ? { preferredPlacement: input.preferredPlacement }
1174
+ : {}),
1175
+ ...(input.preferredNetworkRouteId !== undefined
1176
+ ? { preferredNetworkRouteId: input.preferredNetworkRouteId }
1177
+ : {}),
1178
+ ...(input.healthPolicy !== undefined ? { healthPolicy: input.healthPolicy } : {}),
1179
+ ...(input.status !== undefined ? { status: input.status } : {}),
1180
+ });
1181
+ const {
1182
+ operationId: _operationId,
1183
+ expectedVersion: _expectedVersion,
1184
+ ...candidatePatch
1185
+ } = request;
1186
+ const digest = operationDigest({
1187
+ version: 1,
1188
+ siteAuthConnectionId: input.siteAuthConnectionId,
1189
+ request: { expectedVersion: request.expectedVersion, ...candidatePatch },
1190
+ actorSubjectId: input.actorSubjectId,
1191
+ });
1192
+ try {
1193
+ return await withRlsContext(db, input, async (scopedDb) => {
1194
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
1195
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
1196
+ await lockOperation(scopedDb, request.operationId);
1197
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
1198
+ if (existing) {
1199
+ assertOperation(existing, {
1200
+ resourceKind: "site_auth_connection",
1201
+ resourceId: input.siteAuthConnectionId,
1202
+ kind: "update",
1203
+ requestDigest: digest,
1204
+ actorSubjectId: input.actorSubjectId,
1205
+ });
1206
+ const response = SiteAuthConnectionMutationResponse.parse(existing.result);
1207
+ return { ...response, replayed: true };
1208
+ }
1209
+ const [current] = await scopedDb
1210
+ .select()
1211
+ .from(schema.siteAuthConnections)
1212
+ .where(
1213
+ and(
1214
+ eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
1215
+ eq(schema.siteAuthConnections.id, input.siteAuthConnectionId),
1216
+ ),
1217
+ )
1218
+ .for("update")
1219
+ .limit(1);
1220
+ if (!current) throw new InteractionResourceNotFoundError("Site auth connection not found");
1221
+ if (current.version !== request.expectedVersion) {
1222
+ throw new InteractionResourceConflictError(
1223
+ "Site auth connection changed before this update",
1224
+ );
1225
+ }
1226
+ const currentValue = siteAuthConnectionFromRow(current);
1227
+ const candidate = SiteAuthConnection.parse({
1228
+ ...currentValue,
1229
+ ...candidatePatch,
1230
+ // Any configuration change invalidates the scheduler's frozen prompt.
1231
+ // The already-created session remains auditable but can no longer bind
1232
+ // a maintenance AuthRun to this connection.
1233
+ maintenance: null,
1234
+ id: current.id,
1235
+ accountId: current.accountId,
1236
+ workspaceId: current.workspaceId,
1237
+ version: current.version + 1,
1238
+ createdBySubjectId: current.createdBySubjectId,
1239
+ createdAt: iso(current.createdAt),
1240
+ updatedAt: new Date().toISOString(),
1241
+ });
1242
+ await validateSiteAuthUpdateReferences(scopedDb, input.workspaceId, currentValue, candidate);
1243
+ const [row] = await scopedDb
1244
+ .update(schema.siteAuthConnections)
1245
+ .set({
1246
+ name: candidate.name,
1247
+ accountLabel: candidate.accountLabel,
1248
+ status: candidate.status,
1249
+ origins: candidate.origins,
1250
+ loginUrl: candidate.loginUrl,
1251
+ verificationUrlPrefixes: candidate.verificationUrlPrefixes,
1252
+ authorities: candidate.authorities,
1253
+ methods: candidate.methods,
1254
+ preferredIdentityId: candidate.preferredIdentityId,
1255
+ preferredPlacement: candidate.preferredPlacement,
1256
+ preferredNetworkRouteId: candidate.preferredNetworkRouteId,
1257
+ healthPolicy: candidate.healthPolicy,
1258
+ nextCheckAt: nextMaintainedAuthCheck(
1259
+ candidate.status,
1260
+ candidate.healthPolicy,
1261
+ current.lastCheckedAt,
1262
+ ),
1263
+ maintenanceOperationId: null,
1264
+ maintenanceAction: null,
1265
+ maintenanceDueAt: null,
1266
+ maintenanceClaimedAt: null,
1267
+ maintenanceSessionId: null,
1268
+ maintenanceStartedAt: null,
1269
+ version: candidate.version,
1270
+ updatedBySubjectId: input.actorSubjectId,
1271
+ updatedAt: sql`now()`,
1272
+ })
1273
+ .where(
1274
+ and(
1275
+ eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
1276
+ eq(schema.siteAuthConnections.id, input.siteAuthConnectionId),
1277
+ eq(schema.siteAuthConnections.version, request.expectedVersion),
1278
+ ),
1279
+ )
1280
+ .returning();
1281
+ if (!row) {
1282
+ throw new InteractionResourceConflictError("Site auth connection update lost its fence");
1283
+ }
1284
+ const response = SiteAuthConnectionMutationResponse.parse({
1285
+ connection: siteAuthConnectionFromRow(row),
1286
+ operationId: request.operationId,
1287
+ replayed: false,
1288
+ });
1289
+ await insertCompletedOperation(scopedDb, {
1290
+ operationId: request.operationId,
1291
+ accountId: input.accountId,
1292
+ workspaceId: input.workspaceId,
1293
+ resourceKind: "site_auth_connection",
1294
+ resourceId: input.siteAuthConnectionId,
1295
+ kind: "update",
1296
+ requestDigest: digest,
1297
+ resultVersion: row.version,
1298
+ result: response,
1299
+ actorSubjectId: input.actorSubjectId,
1300
+ });
1301
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
1302
+ return response;
1303
+ });
1304
+ } catch (error) {
1305
+ if (postgresConstraint(error) === "site_auth_connections_workspace_active_name_uq") {
1306
+ throw new InteractionResourceConflictError(
1307
+ "An active site auth connection already uses this name",
1308
+ );
1309
+ }
1310
+ throw error;
1311
+ }
1312
+ }
1313
+
1314
+ function authRunFromRow(row: AuthRunRow): AuthRunValue {
1315
+ return AuthRun.parse({
1316
+ id: row.id,
1317
+ accountId: row.accountId,
1318
+ workspaceId: row.workspaceId,
1319
+ siteAuthConnectionId: row.siteAuthConnectionId,
1320
+ browserSessionId: row.browserSessionId,
1321
+ targetId: row.targetId,
1322
+ controllerGeneration: row.controllerGeneration,
1323
+ targetGeneration: row.targetGeneration,
1324
+ documentGeneration: row.documentGeneration,
1325
+ purpose: row.purpose,
1326
+ methodId: row.methodId,
1327
+ authorityId: row.authorityId,
1328
+ state: row.state,
1329
+ choices: row.choices,
1330
+ pendingFields: row.pendingFields,
1331
+ externalAction: row.externalAction,
1332
+ interventionId: row.interventionId,
1333
+ verifiedUrl: row.verifiedUrl,
1334
+ failureCode: row.failureCode,
1335
+ version: row.version,
1336
+ operationId: row.operationId,
1337
+ createdBySubjectId: row.createdBySubjectId,
1338
+ createdAt: iso(row.createdAt),
1339
+ updatedAt: iso(row.updatedAt),
1340
+ settledAt: row.settledAt ? iso(row.settledAt) : null,
1341
+ });
1342
+ }
1343
+
1344
+ /**
1345
+ * Project only terminal authentication evidence. `healthSequence` is allocated
1346
+ * by PostgreSQL when a run starts, so an older browser may finish later without
1347
+ * overwriting evidence from a newer run. A cancelled run carries no evidence.
1348
+ */
1349
+ async function projectSettledAuthRunHealth(
1350
+ db: Database,
1351
+ input: { run: AuthRunRow; actorSubjectId: string },
1352
+ ): Promise<boolean> {
1353
+ const { run } = input;
1354
+ if (!run.settledAt) return false;
1355
+ const carriesEvidence = run.state === "verified" || run.state === "failed";
1356
+ if (!carriesEvidence && (run.state !== "cancelled" || !run.maintenanceOperationId)) {
1357
+ return false;
1358
+ }
1359
+ const [connection] = await db
1360
+ .select()
1361
+ .from(schema.siteAuthConnections)
1362
+ .where(
1363
+ and(
1364
+ eq(schema.siteAuthConnections.workspaceId, run.workspaceId),
1365
+ eq(schema.siteAuthConnections.id, run.siteAuthConnectionId),
1366
+ ),
1367
+ )
1368
+ .for("update")
1369
+ .limit(1);
1370
+ if (!connection) throw new InteractionResourceNotFoundError("Site auth connection not found");
1371
+ const checkedAt = run.settledAt;
1372
+ const maintenanceMatches =
1373
+ run.maintenanceOperationId !== null &&
1374
+ connection.maintenanceOperationId === run.maintenanceOperationId;
1375
+ const evidenceAdvances =
1376
+ carriesEvidence &&
1377
+ connection.healthSequence < run.healthSequence &&
1378
+ (run.maintenanceOperationId === null || maintenanceMatches);
1379
+ if (!maintenanceMatches && !evidenceAdvances) return false;
1380
+ const intervalSeconds = connection.healthPolicy.intervalSeconds;
1381
+ const nextEvidenceCheckAt =
1382
+ connection.status === "active" &&
1383
+ connection.healthPolicy.mode === "maintained" &&
1384
+ intervalSeconds !== null
1385
+ ? run.state === "failed" &&
1386
+ run.purpose === "health_check" &&
1387
+ connection.healthPolicy.automaticRepair
1388
+ ? checkedAt
1389
+ : new Date(checkedAt.getTime() + intervalSeconds * 1_000)
1390
+ : null;
1391
+ const failureState = run.purpose === "repair" ? "failed" : "needs_repair";
1392
+ const cancelledMaintenanceCheckAt =
1393
+ connection.status === "active" &&
1394
+ connection.healthPolicy.mode === "maintained" &&
1395
+ intervalSeconds !== null
1396
+ ? new Date(
1397
+ checkedAt.getTime() +
1398
+ Math.min(intervalSeconds * 1_000, MAX_SITE_AUTH_MAINTENANCE_RETRY_MS),
1399
+ )
1400
+ : null;
1401
+ const [projected] = await db
1402
+ .update(schema.siteAuthConnections)
1403
+ .set({
1404
+ ...(evidenceAdvances
1405
+ ? {
1406
+ verificationState: run.state === "verified" ? "verified" : failureState,
1407
+ ...(run.state === "verified"
1408
+ ? {
1409
+ lastVerifiedAt: checkedAt,
1410
+ lastVerifiedUrl: run.verifiedUrl,
1411
+ repairCode: null,
1412
+ }
1413
+ : { repairCode: run.failureCode }),
1414
+ lastCheckedAt: checkedAt,
1415
+ nextCheckAt: nextEvidenceCheckAt,
1416
+ healthSequence: run.healthSequence,
1417
+ }
1418
+ : maintenanceMatches && run.state === "cancelled"
1419
+ ? { nextCheckAt: cancelledMaintenanceCheckAt }
1420
+ : {}),
1421
+ ...(maintenanceMatches
1422
+ ? {
1423
+ maintenanceOperationId: null,
1424
+ maintenanceAction: null,
1425
+ maintenanceDueAt: null,
1426
+ maintenanceClaimedAt: null,
1427
+ maintenanceSessionId: null,
1428
+ maintenanceStartedAt: null,
1429
+ }
1430
+ : {}),
1431
+ version: sql`${schema.siteAuthConnections.version} + 1`,
1432
+ updatedBySubjectId: input.actorSubjectId,
1433
+ updatedAt: checkedAt,
1434
+ })
1435
+ .where(
1436
+ and(
1437
+ eq(schema.siteAuthConnections.workspaceId, run.workspaceId),
1438
+ eq(schema.siteAuthConnections.id, run.siteAuthConnectionId),
1439
+ sql`(
1440
+ ${schema.siteAuthConnections.healthSequence} < ${run.healthSequence}
1441
+ or ${schema.siteAuthConnections.maintenanceOperationId} = ${run.maintenanceOperationId}
1442
+ )`,
1443
+ ),
1444
+ )
1445
+ .returning({ id: schema.siteAuthConnections.id });
1446
+ return Boolean(projected);
1447
+ }
1448
+
1449
+ async function loadAuthRunRow(
1450
+ db: Database,
1451
+ workspaceId: string,
1452
+ authRunId: string,
1453
+ ): Promise<AuthRunRow | null> {
1454
+ const [row] = await db
1455
+ .select()
1456
+ .from(schema.authRuns)
1457
+ .where(and(eq(schema.authRuns.workspaceId, workspaceId), eq(schema.authRuns.id, authRunId)))
1458
+ .limit(1);
1459
+ return row ?? null;
1460
+ }
1461
+
1462
+ function assertAuthSelection(
1463
+ connection: SiteAuthConnectionValue,
1464
+ selection: { methodId?: string | null; authorityId?: string | null },
1465
+ ): void {
1466
+ const method = selection.methodId
1467
+ ? connection.methods.find((candidate) => candidate.id === selection.methodId)
1468
+ : null;
1469
+ const authority = selection.authorityId
1470
+ ? connection.authorities.find((candidate) => candidate.id === selection.authorityId)
1471
+ : null;
1472
+ if (selection.methodId && !method) {
1473
+ throw new InteractionResourceStateError("Auth method is not configured");
1474
+ }
1475
+ if (selection.authorityId && !authority) {
1476
+ throw new InteractionResourceStateError("Auth authority is not configured");
1477
+ }
1478
+ if (method && authority && !method.authorityIds.includes(authority.id)) {
1479
+ throw new InteractionResourceStateError(
1480
+ "Auth authority does not belong to the selected method",
1481
+ );
1482
+ }
1483
+ }
1484
+
1485
+ const AUTH_RUN_TRANSITIONS: Readonly<
1486
+ Record<AuthRunRow["state"], ReadonlySet<AuthRunRow["state"]>>
1487
+ > = {
1488
+ discovering: new Set([
1489
+ "awaiting_choice",
1490
+ "awaiting_secret",
1491
+ "awaiting_external_action",
1492
+ "working",
1493
+ "failed",
1494
+ "cancelled",
1495
+ ]),
1496
+ awaiting_choice: new Set([
1497
+ "awaiting_choice",
1498
+ "awaiting_secret",
1499
+ "awaiting_external_action",
1500
+ "working",
1501
+ "failed",
1502
+ "cancelled",
1503
+ ]),
1504
+ awaiting_secret: new Set([
1505
+ "awaiting_secret",
1506
+ "awaiting_external_action",
1507
+ "working",
1508
+ "failed",
1509
+ "cancelled",
1510
+ ]),
1511
+ awaiting_external_action: new Set([
1512
+ "awaiting_external_action",
1513
+ "awaiting_secret",
1514
+ "working",
1515
+ "failed",
1516
+ "cancelled",
1517
+ ]),
1518
+ working: new Set([
1519
+ "working",
1520
+ "awaiting_choice",
1521
+ "awaiting_secret",
1522
+ "awaiting_external_action",
1523
+ "failed",
1524
+ "cancelled",
1525
+ ]),
1526
+ verified: new Set(),
1527
+ failed: new Set(),
1528
+ cancelled: new Set(),
1529
+ };
1530
+
1531
+ type ProtectedAuthOperationMetadata = {
1532
+ schemaVersion: 1;
1533
+ authRunVersion: number;
1534
+ authority: SiteAuthAuthorityValue;
1535
+ origins: string[];
1536
+ fields: Array<{
1537
+ id: string;
1538
+ purpose: "identifier" | "password" | "secret" | "totp";
1539
+ }>;
1540
+ credentialVersion: number | null;
1541
+ };
1542
+
1543
+ type ExternalAuthOperationMetadata = {
1544
+ schemaVersion: 1;
1545
+ authRunVersion: number;
1546
+ authority: Extract<SiteAuthAuthorityValue, { kind: "external_provider" }>;
1547
+ };
1548
+
1549
+ export type ExternalAuthPreparation = {
1550
+ run: AuthRunValue;
1551
+ authority: ExternalAuthOperationMetadata["authority"];
1552
+ operationState: ResourceOperationRow["state"];
1553
+ response: ExternalAuthRunResponseValue | null;
1554
+ replayed: boolean;
1555
+ };
1556
+
1557
+ export type ProtectedAuthFillPreparation = {
1558
+ run: AuthRunValue;
1559
+ authority: SiteAuthAuthorityValue;
1560
+ origins: string[];
1561
+ credentialVersion: number | null;
1562
+ operationState: ResourceOperationRow["state"];
1563
+ response: ProtectedAuthFillResponseValue | null;
1564
+ replayed: boolean;
1565
+ };
1566
+
1567
+ function protectedAuthFillDigest(
1568
+ authRunId: string,
1569
+ request: ProtectedAuthFillRequestValue,
1570
+ actorSubjectId: string,
1571
+ ): string {
1572
+ const { operationId: _operationId, ...digestRequest } = request;
1573
+ return operationDigest({
1574
+ version: 1,
1575
+ authRunId,
1576
+ request: digestRequest,
1577
+ actorSubjectId,
1578
+ });
1579
+ }
1580
+
1581
+ function parseExternalAuthRunRequest(
1582
+ input: ExternalAuthRunRequestValue,
1583
+ ): ExternalAuthRunRequestValue {
1584
+ return ExternalAuthRunRequest.parse({
1585
+ operationId: input.operationId,
1586
+ expectedVersion: input.expectedVersion,
1587
+ action: input.action,
1588
+ });
1589
+ }
1590
+
1591
+ function externalAuthDigest(
1592
+ authRunId: string,
1593
+ request: ExternalAuthRunRequestValue,
1594
+ actorSubjectId: string,
1595
+ ): string {
1596
+ const { operationId: _operationId, ...digestRequest } = request;
1597
+ return operationDigest({
1598
+ version: 1,
1599
+ authRunId,
1600
+ request: digestRequest,
1601
+ actorSubjectId,
1602
+ });
1603
+ }
1604
+
1605
+ function externalAuthOperationMetadata(value: unknown): ExternalAuthOperationMetadata {
1606
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1607
+ throw new InteractionResourceStateError("External-auth operation metadata is invalid");
1608
+ }
1609
+ const record = value as Record<string, unknown>;
1610
+ const authority = SiteAuthAuthority.parse(record.authority);
1611
+ if (
1612
+ record.schemaVersion !== 1 ||
1613
+ !Number.isSafeInteger(record.authRunVersion) ||
1614
+ (record.authRunVersion as number) < 1 ||
1615
+ authority.kind !== "external_provider"
1616
+ ) {
1617
+ throw new InteractionResourceStateError("External-auth operation metadata is invalid");
1618
+ }
1619
+ return {
1620
+ schemaVersion: 1,
1621
+ authRunVersion: record.authRunVersion as number,
1622
+ authority,
1623
+ };
1624
+ }
1625
+
1626
+ function externalAuthPreparationFromOperation(
1627
+ operation: ResourceOperationRow,
1628
+ run: AuthRunRow,
1629
+ ): ExternalAuthPreparation {
1630
+ const metadata = externalAuthOperationMetadata(operation.metadata);
1631
+ return {
1632
+ run: authRunFromRow(run),
1633
+ authority: metadata.authority,
1634
+ operationState: operation.state,
1635
+ response:
1636
+ operation.state === "completed" && operation.result
1637
+ ? {
1638
+ ...ExternalAuthRunResponse.parse(operation.result),
1639
+ replayed: true,
1640
+ }
1641
+ : null,
1642
+ replayed: operation.state === "completed",
1643
+ };
1644
+ }
1645
+
1646
+ function parseProtectedAuthFillRequest(
1647
+ input: ProtectedAuthFillRequestValue,
1648
+ ): ProtectedAuthFillRequestValue {
1649
+ return ProtectedAuthFillRequest.parse({
1650
+ operationId: input.operationId,
1651
+ expectedVersion: input.expectedVersion,
1652
+ expectedTargetGeneration: input.expectedTargetGeneration,
1653
+ expectedDocumentGeneration: input.expectedDocumentGeneration,
1654
+ expectedFrameId: input.expectedFrameId,
1655
+ authorityId: input.authorityId,
1656
+ fields: input.fields,
1657
+ submit: input.submit,
1658
+ });
1659
+ }
1660
+
1661
+ function protectedAuthPreparationFromOperation(
1662
+ operation: ResourceOperationRow,
1663
+ run: AuthRunRow,
1664
+ ): ProtectedAuthFillPreparation {
1665
+ const metadata = protectedAuthOperationMetadata(operation.metadata);
1666
+ return {
1667
+ run: authRunFromRow(run),
1668
+ authority: metadata.authority,
1669
+ origins: metadata.origins,
1670
+ credentialVersion: metadata.credentialVersion,
1671
+ operationState: operation.state,
1672
+ response:
1673
+ operation.state === "completed" && operation.result
1674
+ ? {
1675
+ ...ProtectedAuthFillResponse.parse(operation.result),
1676
+ replayed: true,
1677
+ }
1678
+ : null,
1679
+ replayed: operation.state === "completed",
1680
+ };
1681
+ }
1682
+
1683
+ export async function getProtectedAuthFillPreparation(
1684
+ db: Database,
1685
+ input: InteractionMutationScope & {
1686
+ authRunId: string;
1687
+ } & ProtectedAuthFillRequestValue,
1688
+ ): Promise<ProtectedAuthFillPreparation | null> {
1689
+ const request = parseProtectedAuthFillRequest(input);
1690
+ const digest = protectedAuthFillDigest(input.authRunId, request, input.actorSubjectId);
1691
+ return await withRlsContext(
1692
+ db,
1693
+ input,
1694
+ async (scopedDb) => {
1695
+ const operation = await loadOperation(scopedDb, input.workspaceId, request.operationId);
1696
+ if (!operation) return null;
1697
+ assertOperationIdentity(operation, {
1698
+ resourceKind: "auth_run",
1699
+ resourceId: input.authRunId,
1700
+ kind: "protected_fill",
1701
+ requestDigest: digest,
1702
+ actorSubjectId: input.actorSubjectId,
1703
+ });
1704
+ const run = await loadAuthRunRow(scopedDb, input.workspaceId, input.authRunId);
1705
+ if (!run) throw new InteractionResourceNotFoundError("Auth run not found");
1706
+ return protectedAuthPreparationFromOperation(operation, run);
1707
+ },
1708
+ CONSISTENT_READ,
1709
+ );
1710
+ }
1711
+
1712
+ function protectedAuthOperationMetadata(value: unknown): ProtectedAuthOperationMetadata {
1713
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1714
+ throw new InteractionResourceStateError("Protected-fill operation metadata is invalid");
1715
+ }
1716
+ const record = value as Record<string, unknown>;
1717
+ const authority = SiteAuthAuthority.parse(record.authority);
1718
+ if (
1719
+ record.schemaVersion !== 1 ||
1720
+ !Number.isSafeInteger(record.authRunVersion) ||
1721
+ (record.authRunVersion as number) < 1 ||
1722
+ !Array.isArray(record.origins) ||
1723
+ record.origins.some(
1724
+ (origin) =>
1725
+ typeof origin !== "string" ||
1726
+ (() => {
1727
+ try {
1728
+ return new URL(origin).origin !== origin;
1729
+ } catch {
1730
+ return true;
1731
+ }
1732
+ })(),
1733
+ ) ||
1734
+ !Array.isArray(record.fields) ||
1735
+ record.fields.length < 1 ||
1736
+ record.fields.length > 32 ||
1737
+ !(
1738
+ record.credentialVersion === null ||
1739
+ (Number.isSafeInteger(record.credentialVersion) && (record.credentialVersion as number) > 0)
1740
+ )
1741
+ ) {
1742
+ throw new InteractionResourceStateError("Protected-fill operation metadata is invalid");
1743
+ }
1744
+ const purposes = new Set(["identifier", "password", "secret", "totp"]);
1745
+ const fields = record.fields.map((field) => {
1746
+ if (!field || typeof field !== "object" || Array.isArray(field)) {
1747
+ throw new InteractionResourceStateError("Protected-fill operation metadata is invalid");
1748
+ }
1749
+ const candidate = field as Record<string, unknown>;
1750
+ if (
1751
+ typeof candidate.id !== "string" ||
1752
+ candidate.id.length < 1 ||
1753
+ candidate.id.length > 512 ||
1754
+ typeof candidate.purpose !== "string" ||
1755
+ !purposes.has(candidate.purpose)
1756
+ ) {
1757
+ throw new InteractionResourceStateError("Protected-fill operation metadata is invalid");
1758
+ }
1759
+ return {
1760
+ id: candidate.id,
1761
+ purpose: candidate.purpose as ProtectedAuthOperationMetadata["fields"][number]["purpose"],
1762
+ };
1763
+ });
1764
+ if (new Set(fields.map((field) => field.id)).size !== fields.length) {
1765
+ throw new InteractionResourceStateError("Protected-fill operation metadata is invalid");
1766
+ }
1767
+ if ((authority.kind === "connection_fields") !== (record.credentialVersion !== null)) {
1768
+ throw new InteractionResourceStateError("Protected-fill credential metadata is invalid");
1769
+ }
1770
+ return {
1771
+ schemaVersion: 1,
1772
+ authRunVersion: record.authRunVersion as number,
1773
+ authority,
1774
+ origins: [...(record.origins as string[])],
1775
+ fields,
1776
+ credentialVersion: record.credentialVersion as number | null,
1777
+ };
1778
+ }
1779
+
1780
+ function protectedAuthFields(
1781
+ authority: SiteAuthAuthorityValue,
1782
+ requested: readonly ProtectedAuthFillRequestValue["fields"][number][],
1783
+ ): ProtectedAuthOperationMetadata["fields"] {
1784
+ if (authority.kind === "external_provider") {
1785
+ throw new InteractionResourceStateError(
1786
+ "External auth providers cannot use protected field fill",
1787
+ );
1788
+ }
1789
+ const configured = new Map(authority.fields.map((field) => [field.id, field]));
1790
+ return requested.map((field) => {
1791
+ const match = configured.get(field.fieldId);
1792
+ if (!match) {
1793
+ throw new InteractionResourceStateError("Protected-fill field is not configured");
1794
+ }
1795
+ return { id: match.id, purpose: match.purpose };
1796
+ });
1797
+ }
1798
+
1799
+ export async function listAuthRuns(
1800
+ db: Database,
1801
+ input: {
1802
+ accountId: string;
1803
+ workspaceId: string;
1804
+ browserSessionId?: string;
1805
+ siteAuthConnectionId?: string;
1806
+ includeSettled?: boolean;
1807
+ },
1808
+ ): Promise<AuthRunListResponseValue> {
1809
+ return await withRlsContext(
1810
+ db,
1811
+ input,
1812
+ async (scopedDb) => {
1813
+ const predicates = [eq(schema.authRuns.workspaceId, input.workspaceId)];
1814
+ if (input.browserSessionId) {
1815
+ predicates.push(eq(schema.authRuns.browserSessionId, input.browserSessionId));
1816
+ }
1817
+ if (input.siteAuthConnectionId) {
1818
+ predicates.push(eq(schema.authRuns.siteAuthConnectionId, input.siteAuthConnectionId));
1819
+ }
1820
+ if (!input.includeSettled) predicates.push(sql`${schema.authRuns.settledAt} is null`);
1821
+ const runs = await scopedDb
1822
+ .select()
1823
+ .from(schema.authRuns)
1824
+ .where(and(...predicates))
1825
+ .orderBy(asc(schema.authRuns.createdAt), asc(schema.authRuns.id));
1826
+ return AuthRunListResponse.parse({ runs: runs.map(authRunFromRow) });
1827
+ },
1828
+ CONSISTENT_READ,
1829
+ );
1830
+ }
1831
+
1832
+ export async function getAuthRun(
1833
+ db: Database,
1834
+ input: { accountId: string; workspaceId: string; authRunId: string },
1835
+ ): Promise<AuthRunValue> {
1836
+ return await withRlsContext(
1837
+ db,
1838
+ input,
1839
+ async (scopedDb) => {
1840
+ const row = await loadAuthRunRow(scopedDb, input.workspaceId, input.authRunId);
1841
+ if (!row) throw new InteractionResourceNotFoundError("Auth run not found");
1842
+ return authRunFromRow(row);
1843
+ },
1844
+ CONSISTENT_READ,
1845
+ );
1846
+ }
1847
+
1848
+ export type ExternalAuthInteractiveContext = {
1849
+ run: AuthRunValue;
1850
+ authority: Extract<SiteAuthAuthorityValue, { kind: "external_provider" }>;
1851
+ };
1852
+
1853
+ /** Resolve only the durable authority needed to reveal an external provider's
1854
+ * hosted login UI. The hosted URL itself is deliberately never persisted. */
1855
+ export async function getExternalAuthInteractiveContext(
1856
+ db: Database,
1857
+ input: InteractionMutationScope & {
1858
+ authRunId: string;
1859
+ } & ExternalAuthInteractiveRequestValue,
1860
+ ): Promise<ExternalAuthInteractiveContext> {
1861
+ const request = ExternalAuthInteractiveRequest.parse({
1862
+ operationId: input.operationId,
1863
+ expectedVersion: input.expectedVersion,
1864
+ });
1865
+ return await withRlsContext(
1866
+ db,
1867
+ input,
1868
+ async (scopedDb) => {
1869
+ const run = await loadAuthRunRow(scopedDb, input.workspaceId, input.authRunId);
1870
+ if (!run) throw new InteractionResourceNotFoundError("Auth run not found");
1871
+ if (run.settledAt || run.version !== request.expectedVersion) {
1872
+ throw new InteractionResourceConflictError(
1873
+ "Auth run changed before its hosted login flow was opened",
1874
+ );
1875
+ }
1876
+ if (run.state !== "awaiting_external_action" || !run.externalAction || !run.interventionId) {
1877
+ throw new InteractionResourceStateError("Auth run is not waiting for a hosted login flow");
1878
+ }
1879
+ const [browser] = await scopedDb
1880
+ .select({
1881
+ lifecycle: schema.browserSessions.lifecycle,
1882
+ controllerGeneration: schema.browserSessions.controllerGeneration,
1883
+ })
1884
+ .from(schema.browserSessions)
1885
+ .where(
1886
+ and(
1887
+ eq(schema.browserSessions.workspaceId, input.workspaceId),
1888
+ eq(schema.browserSessions.id, run.browserSessionId),
1889
+ ),
1890
+ )
1891
+ .limit(1);
1892
+ if (
1893
+ !browser ||
1894
+ browser.lifecycle !== "active" ||
1895
+ browser.controllerGeneration !== run.controllerGeneration
1896
+ ) {
1897
+ throw new InteractionResourceConflictError(
1898
+ "Hosted login belongs to a stale browser controller",
1899
+ );
1900
+ }
1901
+ const connectionRow = await getSiteAuthConnectionRow(
1902
+ scopedDb,
1903
+ input.workspaceId,
1904
+ run.siteAuthConnectionId,
1905
+ );
1906
+ if (connectionRow.status !== "active") {
1907
+ throw new InteractionResourceStateError("Site auth connection is archived");
1908
+ }
1909
+ const connection = siteAuthConnectionFromRow(connectionRow);
1910
+ const authority = connection.authorities.find(
1911
+ (candidate) => candidate.id === run.authorityId,
1912
+ );
1913
+ if (!authority || authority.kind !== "external_provider") {
1914
+ throw new InteractionResourceStateError("Auth run has no external provider authority");
1915
+ }
1916
+ return { run: authRunFromRow(run), authority };
1917
+ },
1918
+ CONSISTENT_READ,
1919
+ );
1920
+ }
1921
+
1922
+ export async function startAuthRun(
1923
+ db: Database,
1924
+ input: InteractionMutationScope & {
1925
+ browserSessionId: string;
1926
+ controllerGeneration: string;
1927
+ /** Trusted attempt provenance supplied by the API grant. */
1928
+ originatingSessionId?: string | null;
1929
+ } & StartAuthRunRequestValue,
1930
+ ): Promise<AuthRunMutationResponseValue> {
1931
+ const request = StartAuthRunRequest.parse({
1932
+ operationId: input.operationId,
1933
+ siteAuthConnectionId: input.siteAuthConnectionId,
1934
+ targetId: input.targetId,
1935
+ expectedTargetGeneration: input.expectedTargetGeneration,
1936
+ expectedDocumentGeneration: input.expectedDocumentGeneration,
1937
+ ...(input.purpose !== undefined ? { purpose: input.purpose } : {}),
1938
+ ...(input.methodId !== undefined ? { methodId: input.methodId } : {}),
1939
+ ...(input.authorityId !== undefined ? { authorityId: input.authorityId } : {}),
1940
+ });
1941
+ const { operationId: _operationId, ...digestRequest } = request;
1942
+ const digest = operationDigest({
1943
+ version: 1,
1944
+ browserSessionId: input.browserSessionId,
1945
+ controllerGeneration: input.controllerGeneration,
1946
+ request: digestRequest,
1947
+ actorSubjectId: input.actorSubjectId,
1948
+ });
1949
+ try {
1950
+ return await withRlsContext(db, input, async (scopedDb) => {
1951
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
1952
+ await lockOperation(scopedDb, request.operationId);
1953
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
1954
+ if (existing) {
1955
+ assertOperation(existing, {
1956
+ resourceKind: "auth_run",
1957
+ kind: "start",
1958
+ requestDigest: digest,
1959
+ actorSubjectId: input.actorSubjectId,
1960
+ });
1961
+ const response = AuthRunMutationResponse.parse(existing.result);
1962
+ return { ...response, replayed: true };
1963
+ }
1964
+ const [browser] = await scopedDb
1965
+ .select()
1966
+ .from(schema.browserSessions)
1967
+ .where(
1968
+ and(
1969
+ eq(schema.browserSessions.workspaceId, input.workspaceId),
1970
+ eq(schema.browserSessions.id, input.browserSessionId),
1971
+ ),
1972
+ )
1973
+ .for("update")
1974
+ .limit(1);
1975
+ if (!browser) throw new InteractionResourceNotFoundError("Browser session not found");
1976
+ if (
1977
+ browser.lifecycle !== "active" ||
1978
+ !browser.controllerGeneration ||
1979
+ browser.controllerGeneration !== input.controllerGeneration
1980
+ ) {
1981
+ throw new InteractionResourceStateError("Browser session is not active");
1982
+ }
1983
+ const authConnection = await getSiteAuthConnectionRow(
1984
+ scopedDb,
1985
+ input.workspaceId,
1986
+ request.siteAuthConnectionId,
1987
+ );
1988
+ if (authConnection.status !== "active") {
1989
+ throw new InteractionResourceStateError("Site auth connection is archived");
1990
+ }
1991
+ const connection = siteAuthConnectionFromRow(authConnection);
1992
+ assertAuthSelection(connection, {
1993
+ methodId: request.methodId ?? null,
1994
+ authorityId: request.authorityId ?? null,
1995
+ });
1996
+ let originatingMaintenanceOperationId: string | null = null;
1997
+ if (input.originatingSessionId) {
1998
+ const [originatingSession] = await scopedDb
1999
+ .select({
2000
+ createdByKind: schema.sessions.createdByKind,
2001
+ createdBySubjectId: schema.sessions.createdBySubjectId,
2002
+ createdByContext: schema.sessions.createdByContext,
2003
+ })
2004
+ .from(schema.sessions)
2005
+ .where(
2006
+ and(
2007
+ eq(schema.sessions.workspaceId, input.workspaceId),
2008
+ eq(schema.sessions.id, input.originatingSessionId),
2009
+ ),
2010
+ )
2011
+ .limit(1);
2012
+ const contextOperationId =
2013
+ originatingSession?.createdByContext[SITE_AUTH_MAINTENANCE_OPERATION_CONTEXT_KEY];
2014
+ const contextConnectionId =
2015
+ originatingSession?.createdByContext[SITE_AUTH_MAINTENANCE_CONNECTION_CONTEXT_KEY];
2016
+ if (
2017
+ originatingSession?.createdByKind === "service" &&
2018
+ originatingSession.createdBySubjectId === "site-auth-maintenance" &&
2019
+ typeof contextOperationId === "string"
2020
+ ) {
2021
+ if (contextConnectionId !== request.siteAuthConnectionId) {
2022
+ throw new InteractionResourceStateError(
2023
+ "Site auth maintenance session belongs to another connection",
2024
+ );
2025
+ }
2026
+ originatingMaintenanceOperationId = contextOperationId;
2027
+ }
2028
+ }
2029
+ if (
2030
+ originatingMaintenanceOperationId &&
2031
+ (input.originatingSessionId !== authConnection.maintenanceSessionId ||
2032
+ originatingMaintenanceOperationId !== authConnection.maintenanceOperationId)
2033
+ ) {
2034
+ throw new InteractionResourceStateError("Site auth maintenance claim is no longer active");
2035
+ }
2036
+ const maintenanceOperationId = originatingMaintenanceOperationId;
2037
+ if (
2038
+ maintenanceOperationId &&
2039
+ (request.purpose ?? "authenticate") !== authConnection.maintenanceAction
2040
+ ) {
2041
+ throw new InteractionResourceStateError(
2042
+ "Maintenance auth run purpose does not match its durable claim",
2043
+ );
2044
+ }
2045
+ const id = randomUUID();
2046
+ const [row] = await scopedDb
2047
+ .insert(schema.authRuns)
2048
+ .values({
2049
+ id,
2050
+ accountId: input.accountId,
2051
+ workspaceId: input.workspaceId,
2052
+ siteAuthConnectionId: request.siteAuthConnectionId,
2053
+ browserSessionId: input.browserSessionId,
2054
+ targetId: request.targetId,
2055
+ controllerGeneration: browser.controllerGeneration,
2056
+ targetGeneration: request.expectedTargetGeneration,
2057
+ documentGeneration: request.expectedDocumentGeneration,
2058
+ purpose: request.purpose ?? "authenticate",
2059
+ maintenanceOperationId,
2060
+ methodId: request.methodId ?? null,
2061
+ authorityId: request.authorityId ?? null,
2062
+ operationId: request.operationId,
2063
+ createdBySubjectId: input.actorSubjectId,
2064
+ })
2065
+ .returning();
2066
+ if (!row) throw new Error("Auth run insert returned no row");
2067
+ const response = AuthRunMutationResponse.parse({
2068
+ run: authRunFromRow(row),
2069
+ operationId: request.operationId,
2070
+ replayed: false,
2071
+ });
2072
+ await insertCompletedOperation(scopedDb, {
2073
+ operationId: request.operationId,
2074
+ accountId: input.accountId,
2075
+ workspaceId: input.workspaceId,
2076
+ resourceKind: "auth_run",
2077
+ resourceId: id,
2078
+ kind: "start",
2079
+ requestDigest: digest,
2080
+ resultVersion: row.version,
2081
+ result: response,
2082
+ actorSubjectId: input.actorSubjectId,
2083
+ });
2084
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
2085
+ return response;
2086
+ });
2087
+ } catch (error) {
2088
+ if (postgresConstraint(error) === "auth_runs_workspace_maintenance_operation_uq") {
2089
+ throw new InteractionResourceConflictError(
2090
+ "This maintenance session already has an auth run",
2091
+ );
2092
+ }
2093
+ if (postgresConstraint(error) === "auth_runs_active_browser_target_uq") {
2094
+ throw new InteractionResourceConflictError(
2095
+ "This browser target already has an active auth run",
2096
+ );
2097
+ }
2098
+ throw error;
2099
+ }
2100
+ }
2101
+
2102
+ export type SettleSessionMaintenanceResult = {
2103
+ cancelledAuthRuns: number;
2104
+ releasedClaim: boolean;
2105
+ };
2106
+
2107
+ /** A terminal-for-now maintenance session cannot leave an AuthRun or its claim
2108
+ * pinned. Interventions settle linked runs first; this covers every other path. */
2109
+ export async function settleSessionMaintenanceInTransaction(
2110
+ db: Database,
2111
+ input: {
2112
+ accountId: string;
2113
+ workspaceId: string;
2114
+ sessionId: string;
2115
+ },
2116
+ ): Promise<SettleSessionMaintenanceResult> {
2117
+ const [session] = await db
2118
+ .select({
2119
+ createdByKind: schema.sessions.createdByKind,
2120
+ createdBySubjectId: schema.sessions.createdBySubjectId,
2121
+ createdByContext: schema.sessions.createdByContext,
2122
+ })
2123
+ .from(schema.sessions)
2124
+ .where(
2125
+ and(
2126
+ eq(schema.sessions.workspaceId, input.workspaceId),
2127
+ eq(schema.sessions.id, input.sessionId),
2128
+ ),
2129
+ )
2130
+ .limit(1);
2131
+ const maintenanceOperationId =
2132
+ session?.createdByContext[SITE_AUTH_MAINTENANCE_OPERATION_CONTEXT_KEY];
2133
+ const siteAuthConnectionId =
2134
+ session?.createdByContext[SITE_AUTH_MAINTENANCE_CONNECTION_CONTEXT_KEY];
2135
+ if (
2136
+ session?.createdByKind !== "service" ||
2137
+ session.createdBySubjectId !== "site-auth-maintenance" ||
2138
+ typeof maintenanceOperationId !== "string" ||
2139
+ typeof siteAuthConnectionId !== "string"
2140
+ ) {
2141
+ return { cancelledAuthRuns: 0, releasedClaim: false };
2142
+ }
2143
+ const settledAt = new Date();
2144
+ const cancelled = await db
2145
+ .update(schema.authRuns)
2146
+ .set({
2147
+ state: "cancelled",
2148
+ choices: [],
2149
+ pendingFields: [],
2150
+ externalAction: null,
2151
+ verifiedUrl: null,
2152
+ failureCode: null,
2153
+ version: sql`${schema.authRuns.version} + 1`,
2154
+ updatedAt: settledAt,
2155
+ settledAt,
2156
+ })
2157
+ .where(
2158
+ and(
2159
+ eq(schema.authRuns.workspaceId, input.workspaceId),
2160
+ eq(schema.authRuns.siteAuthConnectionId, siteAuthConnectionId),
2161
+ eq(schema.authRuns.maintenanceOperationId, maintenanceOperationId),
2162
+ sql`${schema.authRuns.settledAt} is null`,
2163
+ ),
2164
+ )
2165
+ .returning();
2166
+ for (const run of cancelled) {
2167
+ await projectSettledAuthRunHealth(db, {
2168
+ run,
2169
+ actorSubjectId: "site-auth-maintenance",
2170
+ });
2171
+ }
2172
+ const [orphanedClaim] = await db
2173
+ .select()
2174
+ .from(schema.siteAuthConnections)
2175
+ .where(
2176
+ and(
2177
+ eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
2178
+ eq(schema.siteAuthConnections.id, siteAuthConnectionId),
2179
+ eq(schema.siteAuthConnections.maintenanceOperationId, maintenanceOperationId),
2180
+ eq(schema.siteAuthConnections.maintenanceSessionId, input.sessionId),
2181
+ ),
2182
+ )
2183
+ .for("update")
2184
+ .limit(1);
2185
+ let releasedClaim = false;
2186
+ if (orphanedClaim) {
2187
+ const intervalSeconds = orphanedClaim.healthPolicy.intervalSeconds;
2188
+ const retryAt =
2189
+ orphanedClaim.status === "active" &&
2190
+ orphanedClaim.healthPolicy.mode === "maintained" &&
2191
+ intervalSeconds !== null
2192
+ ? new Date(
2193
+ settledAt.getTime() +
2194
+ Math.min(intervalSeconds * 1_000, MAX_SITE_AUTH_MAINTENANCE_RETRY_MS),
2195
+ )
2196
+ : null;
2197
+ const [released] = await db
2198
+ .update(schema.siteAuthConnections)
2199
+ .set({
2200
+ maintenanceOperationId: null,
2201
+ maintenanceAction: null,
2202
+ maintenanceDueAt: null,
2203
+ maintenanceClaimedAt: null,
2204
+ maintenanceSessionId: null,
2205
+ maintenanceStartedAt: null,
2206
+ nextCheckAt: retryAt,
2207
+ version: sql`${schema.siteAuthConnections.version} + 1`,
2208
+ updatedBySubjectId: "site-auth-maintenance",
2209
+ updatedAt: settledAt,
2210
+ })
2211
+ .where(
2212
+ and(
2213
+ eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
2214
+ eq(schema.siteAuthConnections.id, orphanedClaim.id),
2215
+ eq(schema.siteAuthConnections.maintenanceOperationId, maintenanceOperationId),
2216
+ ),
2217
+ )
2218
+ .returning({ id: schema.siteAuthConnections.id });
2219
+ releasedClaim = released !== undefined;
2220
+ }
2221
+ if (cancelled.length > 0 || releasedClaim) {
2222
+ await advanceWorkspaceInteractionRevision(db, input.accountId, input.workspaceId);
2223
+ }
2224
+ return { cancelledAuthRuns: cancelled.length, releasedClaim };
2225
+ }
2226
+
2227
+ export async function reportAuthRun(
2228
+ db: Database,
2229
+ input: InteractionMutationScope & {
2230
+ authRunId: string;
2231
+ controllerGeneration: string;
2232
+ } & ReportAuthRunRequestValue,
2233
+ ): Promise<AuthRunMutationResponseValue> {
2234
+ const request = ReportAuthRunRequest.parse({
2235
+ operationId: input.operationId,
2236
+ expectedVersion: input.expectedVersion,
2237
+ ...(input.methodId !== undefined ? { methodId: input.methodId } : {}),
2238
+ ...(input.authorityId !== undefined ? { authorityId: input.authorityId } : {}),
2239
+ state: input.state,
2240
+ ...(input.choices !== undefined ? { choices: input.choices } : {}),
2241
+ ...(input.pendingFields !== undefined ? { pendingFields: input.pendingFields } : {}),
2242
+ ...(input.externalAction !== undefined ? { externalAction: input.externalAction } : {}),
2243
+ ...(input.failureCode !== undefined ? { failureCode: input.failureCode } : {}),
2244
+ });
2245
+ const { operationId: _operationId, ...digestRequest } = request;
2246
+ const digest = operationDigest({
2247
+ version: 1,
2248
+ authRunId: input.authRunId,
2249
+ controllerGeneration: input.controllerGeneration,
2250
+ request: digestRequest,
2251
+ actorSubjectId: input.actorSubjectId,
2252
+ });
2253
+ return await withRlsContext(db, input, async (scopedDb) => {
2254
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
2255
+ await lockOperation(scopedDb, request.operationId);
2256
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
2257
+ if (existing) {
2258
+ assertOperation(existing, {
2259
+ resourceKind: "auth_run",
2260
+ resourceId: input.authRunId,
2261
+ kind: "report",
2262
+ requestDigest: digest,
2263
+ actorSubjectId: input.actorSubjectId,
2264
+ });
2265
+ const response = AuthRunMutationResponse.parse(existing.result);
2266
+ return { ...response, replayed: true };
2267
+ }
2268
+ const [current] = await scopedDb
2269
+ .select()
2270
+ .from(schema.authRuns)
2271
+ .where(
2272
+ and(
2273
+ eq(schema.authRuns.workspaceId, input.workspaceId),
2274
+ eq(schema.authRuns.id, input.authRunId),
2275
+ ),
2276
+ )
2277
+ .for("update")
2278
+ .limit(1);
2279
+ if (!current) throw new InteractionResourceNotFoundError("Auth run not found");
2280
+ if (current.controllerGeneration !== input.controllerGeneration) {
2281
+ throw new InteractionResourceConflictError("Auth run belongs to a stale browser controller");
2282
+ }
2283
+ if (current.version !== request.expectedVersion) {
2284
+ throw new InteractionResourceConflictError("Auth run changed before this report");
2285
+ }
2286
+ if (!AUTH_RUN_TRANSITIONS[current.state].has(request.state)) {
2287
+ throw new InteractionResourceStateError(
2288
+ `Auth run cannot transition from ${current.state} to ${request.state}`,
2289
+ );
2290
+ }
2291
+ const authConnection = await getSiteAuthConnectionRow(
2292
+ scopedDb,
2293
+ input.workspaceId,
2294
+ current.siteAuthConnectionId,
2295
+ );
2296
+ assertAuthSelection(siteAuthConnectionFromRow(authConnection), {
2297
+ methodId: request.methodId ?? current.methodId,
2298
+ authorityId: request.authorityId ?? current.authorityId,
2299
+ });
2300
+ const settled = request.state === "failed" || request.state === "cancelled";
2301
+ const [row] = await scopedDb
2302
+ .update(schema.authRuns)
2303
+ .set({
2304
+ methodId: request.methodId ?? current.methodId,
2305
+ authorityId: request.authorityId ?? current.authorityId,
2306
+ state: request.state,
2307
+ choices: request.choices ?? [],
2308
+ pendingFields: request.pendingFields ?? [],
2309
+ externalAction: request.externalAction ?? null,
2310
+ failureCode: request.state === "failed" ? request.failureCode! : null,
2311
+ version: current.version + 1,
2312
+ updatedAt: sql`now()`,
2313
+ settledAt: settled ? sql`now()` : null,
2314
+ })
2315
+ .where(
2316
+ and(
2317
+ eq(schema.authRuns.workspaceId, input.workspaceId),
2318
+ eq(schema.authRuns.id, input.authRunId),
2319
+ eq(schema.authRuns.version, request.expectedVersion),
2320
+ eq(schema.authRuns.controllerGeneration, input.controllerGeneration),
2321
+ ),
2322
+ )
2323
+ .returning();
2324
+ if (!row) throw new InteractionResourceConflictError("Auth run report lost its fence");
2325
+ await projectSettledAuthRunHealth(scopedDb, {
2326
+ run: row,
2327
+ actorSubjectId: input.actorSubjectId,
2328
+ });
2329
+ const response = AuthRunMutationResponse.parse({
2330
+ run: authRunFromRow(row),
2331
+ operationId: request.operationId,
2332
+ replayed: false,
2333
+ });
2334
+ await insertCompletedOperation(scopedDb, {
2335
+ operationId: request.operationId,
2336
+ accountId: input.accountId,
2337
+ workspaceId: input.workspaceId,
2338
+ resourceKind: "auth_run",
2339
+ resourceId: input.authRunId,
2340
+ kind: "report",
2341
+ requestDigest: digest,
2342
+ resultVersion: row.version,
2343
+ result: response,
2344
+ actorSubjectId: input.actorSubjectId,
2345
+ });
2346
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
2347
+ return response;
2348
+ });
2349
+ }
2350
+
2351
+ export async function getExternalAuthPreparation(
2352
+ db: Database,
2353
+ input: InteractionMutationScope & {
2354
+ authRunId: string;
2355
+ } & ExternalAuthRunRequestValue,
2356
+ ): Promise<ExternalAuthPreparation | null> {
2357
+ const request = parseExternalAuthRunRequest(input);
2358
+ const digest = externalAuthDigest(input.authRunId, request, input.actorSubjectId);
2359
+ return await withRlsContext(
2360
+ db,
2361
+ input,
2362
+ async (scopedDb) => {
2363
+ const operation = await loadOperation(scopedDb, input.workspaceId, request.operationId);
2364
+ if (!operation) return null;
2365
+ assertOperationIdentity(operation, {
2366
+ resourceKind: "auth_run",
2367
+ resourceId: input.authRunId,
2368
+ kind: "external_auth",
2369
+ requestDigest: digest,
2370
+ actorSubjectId: input.actorSubjectId,
2371
+ });
2372
+ const run = await loadAuthRunRow(scopedDb, input.workspaceId, input.authRunId);
2373
+ if (!run) throw new InteractionResourceNotFoundError("Auth run not found");
2374
+ return externalAuthPreparationFromOperation(operation, run);
2375
+ },
2376
+ CONSISTENT_READ,
2377
+ );
2378
+ }
2379
+
2380
+ export async function prepareExternalAuth(
2381
+ db: Database,
2382
+ input: InteractionMutationScope & {
2383
+ authRunId: string;
2384
+ } & ExternalAuthRunRequestValue,
2385
+ ): Promise<ExternalAuthPreparation> {
2386
+ const request = parseExternalAuthRunRequest(input);
2387
+ const digest = externalAuthDigest(input.authRunId, request, input.actorSubjectId);
2388
+ return await withRlsContext(db, input, async (scopedDb) => {
2389
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
2390
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
2391
+ await lockOperation(scopedDb, request.operationId);
2392
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
2393
+ if (existing) {
2394
+ assertOperationIdentity(existing, {
2395
+ resourceKind: "auth_run",
2396
+ resourceId: input.authRunId,
2397
+ kind: "external_auth",
2398
+ requestDigest: digest,
2399
+ actorSubjectId: input.actorSubjectId,
2400
+ });
2401
+ const run = await loadAuthRunRow(scopedDb, input.workspaceId, input.authRunId);
2402
+ if (!run) throw new InteractionResourceNotFoundError("Auth run not found");
2403
+ return externalAuthPreparationFromOperation(existing, run);
2404
+ }
2405
+
2406
+ const [run] = await scopedDb
2407
+ .select()
2408
+ .from(schema.authRuns)
2409
+ .where(
2410
+ and(
2411
+ eq(schema.authRuns.workspaceId, input.workspaceId),
2412
+ eq(schema.authRuns.id, input.authRunId),
2413
+ ),
2414
+ )
2415
+ .for("update")
2416
+ .limit(1);
2417
+ if (!run) throw new InteractionResourceNotFoundError("Auth run not found");
2418
+ if (run.settledAt) throw new InteractionResourceStateError("Auth run is already settled");
2419
+ if (run.version !== request.expectedVersion) {
2420
+ throw new InteractionResourceConflictError("Auth run changed before external authentication");
2421
+ }
2422
+ const [browser] = await scopedDb
2423
+ .select({
2424
+ lifecycle: schema.browserSessions.lifecycle,
2425
+ controllerGeneration: schema.browserSessions.controllerGeneration,
2426
+ })
2427
+ .from(schema.browserSessions)
2428
+ .where(
2429
+ and(
2430
+ eq(schema.browserSessions.workspaceId, input.workspaceId),
2431
+ eq(schema.browserSessions.id, run.browserSessionId),
2432
+ ),
2433
+ )
2434
+ .for("update")
2435
+ .limit(1);
2436
+ if (
2437
+ !browser ||
2438
+ browser.lifecycle !== "active" ||
2439
+ browser.controllerGeneration !== run.controllerGeneration
2440
+ ) {
2441
+ throw new InteractionResourceConflictError(
2442
+ "External authentication belongs to a stale browser controller",
2443
+ );
2444
+ }
2445
+ const connectionRow = await getSiteAuthConnectionRow(
2446
+ scopedDb,
2447
+ input.workspaceId,
2448
+ run.siteAuthConnectionId,
2449
+ );
2450
+ if (connectionRow.status !== "active") {
2451
+ throw new InteractionResourceStateError("Site auth connection is archived");
2452
+ }
2453
+ const connection = siteAuthConnectionFromRow(connectionRow);
2454
+ if (!run.authorityId) {
2455
+ throw new InteractionResourceStateError(
2456
+ "Auth run must select an external authority before it can start",
2457
+ );
2458
+ }
2459
+ assertAuthSelection(connection, {
2460
+ methodId: run.methodId,
2461
+ authorityId: run.authorityId,
2462
+ });
2463
+ const selected = connection.authorities.find((authority) => authority.id === run.authorityId);
2464
+ if (!selected || selected.kind !== "external_provider") {
2465
+ throw new InteractionResourceStateError(
2466
+ "Selected auth authority is not an external provider",
2467
+ );
2468
+ }
2469
+ const metadata: ExternalAuthOperationMetadata = {
2470
+ schemaVersion: 1,
2471
+ authRunVersion: run.version,
2472
+ authority: selected,
2473
+ };
2474
+ await scopedDb.insert(schema.interactionResourceOperations).values({
2475
+ operationId: request.operationId,
2476
+ accountId: input.accountId,
2477
+ workspaceId: input.workspaceId,
2478
+ resourceKind: "auth_run",
2479
+ resourceId: input.authRunId,
2480
+ kind: "external_auth",
2481
+ requestDigest: digest,
2482
+ metadata,
2483
+ state: "prepared",
2484
+ actorSubjectId: input.actorSubjectId,
2485
+ });
2486
+ return {
2487
+ run: authRunFromRow(run),
2488
+ authority: selected,
2489
+ operationState: "prepared",
2490
+ response: null,
2491
+ replayed: false,
2492
+ };
2493
+ });
2494
+ }
2495
+
2496
+ export async function dispatchExternalAuth(
2497
+ db: Database,
2498
+ input: InteractionMutationScope & { authRunId: string; operationId: string },
2499
+ ): Promise<ResourceOperationRow["state"]> {
2500
+ return await withRlsContext(db, input, async (scopedDb) => {
2501
+ await lockOperation(scopedDb, input.operationId);
2502
+ const operation = await loadOperation(scopedDb, input.workspaceId, input.operationId);
2503
+ if (!operation) throw new InteractionResourceNotFoundError("External-auth operation not found");
2504
+ assertExternalAuthOperationBinding(operation, input);
2505
+ if (operation.state !== "prepared") return operation.state;
2506
+ const [dispatched] = await scopedDb
2507
+ .update(schema.interactionResourceOperations)
2508
+ .set({ state: "dispatched" })
2509
+ .where(
2510
+ and(
2511
+ eq(schema.interactionResourceOperations.operationId, input.operationId),
2512
+ eq(schema.interactionResourceOperations.state, "prepared"),
2513
+ ),
2514
+ )
2515
+ .returning({ state: schema.interactionResourceOperations.state });
2516
+ if (!dispatched) {
2517
+ throw new InteractionResourceConflictError("External-auth dispatch lost its fence");
2518
+ }
2519
+ return dispatched.state;
2520
+ });
2521
+ }
2522
+
2523
+ export async function completeExternalAuth(
2524
+ db: Database,
2525
+ input: InteractionMutationScope & {
2526
+ authRunId: string;
2527
+ operationId: string;
2528
+ result: BrowserExternalAuthResultValue;
2529
+ target?: {
2530
+ id: string;
2531
+ targetGeneration: string;
2532
+ documentGeneration: string | null;
2533
+ };
2534
+ intervention?: {
2535
+ originatingSessionId: string;
2536
+ originatingTurnId?: string | null;
2537
+ originatingAttemptId?: string | null;
2538
+ originatingToolOperationId?: string | null;
2539
+ expiresInSeconds: number;
2540
+ };
2541
+ },
2542
+ ): Promise<ExternalAuthRunResponseValue> {
2543
+ const providerResult = BrowserExternalAuthResult.parse(input.result);
2544
+ try {
2545
+ return await withRlsContext(db, input, async (scopedDb) => {
2546
+ await lockOperation(scopedDb, input.operationId);
2547
+ const operation = await loadOperation(scopedDb, input.workspaceId, input.operationId);
2548
+ if (!operation)
2549
+ throw new InteractionResourceNotFoundError("External-auth operation not found");
2550
+ assertExternalAuthOperationBinding(operation, input);
2551
+ if (operation.state === "completed" && operation.result) {
2552
+ return {
2553
+ ...ExternalAuthRunResponse.parse(operation.result),
2554
+ replayed: true,
2555
+ };
2556
+ }
2557
+ if (operation.state !== "dispatched") {
2558
+ throw new InteractionResourceStateError(
2559
+ `External-auth operation is ${operation.state.replace("_", " ")}`,
2560
+ );
2561
+ }
2562
+ const metadata = externalAuthOperationMetadata(operation.metadata);
2563
+ const [run] = await scopedDb
2564
+ .select()
2565
+ .from(schema.authRuns)
2566
+ .where(
2567
+ and(
2568
+ eq(schema.authRuns.workspaceId, input.workspaceId),
2569
+ eq(schema.authRuns.id, input.authRunId),
2570
+ ),
2571
+ )
2572
+ .for("update")
2573
+ .limit(1);
2574
+ if (!run) throw new InteractionResourceNotFoundError("Auth run not found");
2575
+ if (run.version !== metadata.authRunVersion || run.settledAt) {
2576
+ throw new InteractionResourceConflictError("Auth run changed after external auth began");
2577
+ }
2578
+ if ((providerResult.state === "authenticated") !== Boolean(input.target)) {
2579
+ throw new InteractionResourceStateError(
2580
+ "Authenticated external auth requires the current browser target",
2581
+ );
2582
+ }
2583
+ if ((providerResult.state === "needs_human") !== Boolean(input.intervention)) {
2584
+ throw new InteractionResourceStateError(
2585
+ "Human external auth requires intervention provenance",
2586
+ );
2587
+ }
2588
+ const nextState =
2589
+ providerResult.state === "needs_human"
2590
+ ? "awaiting_external_action"
2591
+ : providerResult.state === "failed"
2592
+ ? "failed"
2593
+ : "working";
2594
+ if (!AUTH_RUN_TRANSITIONS[run.state].has(nextState)) {
2595
+ throw new InteractionResourceStateError(
2596
+ `Auth run cannot transition from ${run.state} to ${nextState}`,
2597
+ );
2598
+ }
2599
+
2600
+ if (providerResult.profileLoaded) {
2601
+ const otherRuns = await scopedDb
2602
+ .update(schema.authRuns)
2603
+ .set({
2604
+ state: "failed",
2605
+ choices: [],
2606
+ pendingFields: [],
2607
+ externalAction: null,
2608
+ interventionId: null,
2609
+ verifiedUrl: null,
2610
+ failureCode: "browser_profile_reconfigured",
2611
+ version: sql`${schema.authRuns.version} + 1`,
2612
+ updatedAt: sql`now()`,
2613
+ settledAt: sql`now()`,
2614
+ })
2615
+ .where(
2616
+ and(
2617
+ eq(schema.authRuns.workspaceId, input.workspaceId),
2618
+ eq(schema.authRuns.browserSessionId, run.browserSessionId),
2619
+ sql`${schema.authRuns.id} <> ${run.id}`,
2620
+ sql`${schema.authRuns.settledAt} is null`,
2621
+ ),
2622
+ )
2623
+ .returning();
2624
+ if (otherRuns.length > 0) {
2625
+ const otherIds = otherRuns.map((entry) => entry.id);
2626
+ await scopedDb
2627
+ .update(schema.interactionResourceOperations)
2628
+ .set({
2629
+ state: "failed",
2630
+ errorCode: "browser_profile_reconfigured",
2631
+ settledAt: sql`now()`,
2632
+ })
2633
+ .where(
2634
+ and(
2635
+ eq(schema.interactionResourceOperations.workspaceId, input.workspaceId),
2636
+ eq(schema.interactionResourceOperations.resourceKind, "auth_run"),
2637
+ inArray(schema.interactionResourceOperations.resourceId, otherIds),
2638
+ inArray(schema.interactionResourceOperations.state, ["prepared", "dispatched"]),
2639
+ ),
2640
+ );
2641
+ await scopedDb
2642
+ .update(schema.interactionInterventions)
2643
+ .set({
2644
+ status: "cancelled",
2645
+ responseActorSubjectId: input.actorSubjectId,
2646
+ version: sql`${schema.interactionInterventions.version} + 1`,
2647
+ updatedAt: sql`now()`,
2648
+ settledAt: sql`now()`,
2649
+ })
2650
+ .where(
2651
+ and(
2652
+ eq(schema.interactionInterventions.workspaceId, input.workspaceId),
2653
+ inArray(schema.interactionInterventions.authRunId, otherIds),
2654
+ eq(schema.interactionInterventions.status, "open"),
2655
+ ),
2656
+ );
2657
+ for (const other of otherRuns) {
2658
+ await projectSettledAuthRunHealth(scopedDb, {
2659
+ run: other,
2660
+ actorSubjectId: input.actorSubjectId,
2661
+ });
2662
+ }
2663
+ }
2664
+ }
2665
+
2666
+ let interventionId: string | null = null;
2667
+ if (providerResult.state === "needs_human") {
2668
+ const currentIntervention = run.interventionId
2669
+ ? (
2670
+ await scopedDb
2671
+ .select()
2672
+ .from(schema.interactionInterventions)
2673
+ .where(
2674
+ and(
2675
+ eq(schema.interactionInterventions.workspaceId, input.workspaceId),
2676
+ eq(schema.interactionInterventions.id, run.interventionId),
2677
+ ),
2678
+ )
2679
+ .for("update")
2680
+ .limit(1)
2681
+ )[0]
2682
+ : null;
2683
+ if (currentIntervention?.status === "open" && currentIntervention.expiresAt > new Date()) {
2684
+ interventionId = currentIntervention.id;
2685
+ } else {
2686
+ if (currentIntervention?.status === "open") {
2687
+ await scopedDb
2688
+ .update(schema.interactionInterventions)
2689
+ .set({
2690
+ status: "expired",
2691
+ version: currentIntervention.version + 1,
2692
+ updatedAt: sql`now()`,
2693
+ settledAt: sql`now()`,
2694
+ })
2695
+ .where(
2696
+ and(
2697
+ eq(schema.interactionInterventions.id, currentIntervention.id),
2698
+ eq(schema.interactionInterventions.version, currentIntervention.version),
2699
+ eq(schema.interactionInterventions.status, "open"),
2700
+ ),
2701
+ );
2702
+ }
2703
+ const intervention = input.intervention!;
2704
+ if (
2705
+ (intervention.originatingAttemptId && !intervention.originatingTurnId) ||
2706
+ (intervention.originatingToolOperationId && !intervention.originatingAttemptId)
2707
+ ) {
2708
+ throw new InteractionResourceStateError(
2709
+ "External-auth intervention provenance is incomplete",
2710
+ );
2711
+ }
2712
+ interventionId = randomUUID();
2713
+ const [created] = await scopedDb
2714
+ .insert(schema.interactionInterventions)
2715
+ .values({
2716
+ id: interventionId,
2717
+ accountId: input.accountId,
2718
+ workspaceId: input.workspaceId,
2719
+ resourceKind: "browser_session",
2720
+ resourceId: run.browserSessionId,
2721
+ targetId: run.targetId,
2722
+ controllerGeneration: run.controllerGeneration,
2723
+ targetGeneration: run.targetGeneration,
2724
+ documentGeneration: run.documentGeneration,
2725
+ kind: "external_action",
2726
+ reason: providerResult.externalAction!.label,
2727
+ authRunId: run.id,
2728
+ originatingSessionId: intervention.originatingSessionId,
2729
+ originatingTurnId: intervention.originatingTurnId ?? null,
2730
+ originatingAttemptId: intervention.originatingAttemptId ?? null,
2731
+ originatingToolOperationId: intervention.originatingToolOperationId ?? null,
2732
+ operationId: input.operationId,
2733
+ expiresAt: providerResult.externalAction!.expiresAt
2734
+ ? new Date(providerResult.externalAction!.expiresAt)
2735
+ : sql`now() + (${intervention.expiresInSeconds} * interval '1 second')`,
2736
+ })
2737
+ .returning({ id: schema.interactionInterventions.id });
2738
+ if (!created) throw new Error("External-auth intervention insert returned no row");
2739
+ }
2740
+ } else if (run.interventionId) {
2741
+ await scopedDb
2742
+ .update(schema.interactionInterventions)
2743
+ .set({
2744
+ status: "completed",
2745
+ responseActorSubjectId: input.actorSubjectId,
2746
+ version: sql`${schema.interactionInterventions.version} + 1`,
2747
+ updatedAt: sql`now()`,
2748
+ settledAt: sql`now()`,
2749
+ })
2750
+ .where(
2751
+ and(
2752
+ eq(schema.interactionInterventions.workspaceId, input.workspaceId),
2753
+ eq(schema.interactionInterventions.id, run.interventionId),
2754
+ eq(schema.interactionInterventions.status, "open"),
2755
+ ),
2756
+ );
2757
+ }
2758
+
2759
+ const [updated] = await scopedDb
2760
+ .update(schema.authRuns)
2761
+ .set({
2762
+ authorityId: metadata.authority.id,
2763
+ state: nextState,
2764
+ targetId: input.target?.id ?? run.targetId,
2765
+ targetGeneration: input.target?.targetGeneration ?? run.targetGeneration,
2766
+ documentGeneration: input.target?.documentGeneration ?? run.documentGeneration,
2767
+ choices: [],
2768
+ pendingFields: [],
2769
+ externalAction:
2770
+ providerResult.state === "needs_human" ? providerResult.externalAction : null,
2771
+ interventionId,
2772
+ verifiedUrl: null,
2773
+ failureCode: providerResult.state === "failed" ? providerResult.failureCode : null,
2774
+ version: run.version + 1,
2775
+ updatedAt: sql`now()`,
2776
+ settledAt: providerResult.state === "failed" ? sql`now()` : null,
2777
+ })
2778
+ .where(
2779
+ and(
2780
+ eq(schema.authRuns.workspaceId, input.workspaceId),
2781
+ eq(schema.authRuns.id, input.authRunId),
2782
+ eq(schema.authRuns.version, metadata.authRunVersion),
2783
+ sql`${schema.authRuns.settledAt} is null`,
2784
+ ),
2785
+ )
2786
+ .returning();
2787
+ if (!updated) {
2788
+ throw new InteractionResourceConflictError("External-auth settlement lost its auth fence");
2789
+ }
2790
+ await projectSettledAuthRunHealth(scopedDb, {
2791
+ run: updated,
2792
+ actorSubjectId: input.actorSubjectId,
2793
+ });
2794
+ const status =
2795
+ providerResult.state === "authenticated"
2796
+ ? "ready_to_verify"
2797
+ : providerResult.state === "needs_human"
2798
+ ? "needs_human"
2799
+ : providerResult.state === "failed"
2800
+ ? "failed"
2801
+ : "working";
2802
+ const response = ExternalAuthRunResponse.parse({
2803
+ run: authRunFromRow(updated),
2804
+ status,
2805
+ operationId: input.operationId,
2806
+ replayed: false,
2807
+ });
2808
+ const [completed] = await scopedDb
2809
+ .update(schema.interactionResourceOperations)
2810
+ .set({
2811
+ state: "completed",
2812
+ resultVersion: updated.version,
2813
+ result: response,
2814
+ settledAt: sql`now()`,
2815
+ })
2816
+ .where(
2817
+ and(
2818
+ eq(schema.interactionResourceOperations.operationId, input.operationId),
2819
+ eq(schema.interactionResourceOperations.state, "dispatched"),
2820
+ ),
2821
+ )
2822
+ .returning({ operationId: schema.interactionResourceOperations.operationId });
2823
+ if (!completed) {
2824
+ throw new InteractionResourceConflictError(
2825
+ "External-auth settlement lost its operation fence",
2826
+ );
2827
+ }
2828
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
2829
+ return response;
2830
+ });
2831
+ } catch (error) {
2832
+ if (
2833
+ postgresConstraint(error) === "interaction_interventions_workspace_operation_uq" ||
2834
+ postgresConstraint(error) === "interaction_interventions_open_target_kind_uq" ||
2835
+ postgresConstraint(error) === "interaction_interventions_open_auth_run_uq"
2836
+ ) {
2837
+ throw new InteractionResourceConflictError(
2838
+ "A matching interaction intervention is already open",
2839
+ );
2840
+ }
2841
+ throw error;
2842
+ }
2843
+ }
2844
+
2845
+ function assertExternalAuthOperationBinding(
2846
+ operation: ResourceOperationRow,
2847
+ input: { authRunId: string; actorSubjectId: string },
2848
+ ): void {
2849
+ if (
2850
+ operation.resourceKind !== "auth_run" ||
2851
+ operation.resourceId !== input.authRunId ||
2852
+ operation.kind !== "external_auth" ||
2853
+ operation.actorSubjectId !== input.actorSubjectId
2854
+ ) {
2855
+ throw new InteractionResourceConflictError(
2856
+ "Operation id is bound to another external-auth request",
2857
+ );
2858
+ }
2859
+ }
2860
+
2861
+ export async function prepareProtectedAuthFill(
2862
+ db: Database,
2863
+ input: InteractionMutationScope & {
2864
+ authRunId: string;
2865
+ credentialVersion: number | null;
2866
+ } & ProtectedAuthFillRequestValue,
2867
+ ): Promise<ProtectedAuthFillPreparation> {
2868
+ const request = parseProtectedAuthFillRequest(input);
2869
+ const digest = protectedAuthFillDigest(input.authRunId, request, input.actorSubjectId);
2870
+ return await withRlsContext(db, input, async (scopedDb) => {
2871
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
2872
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
2873
+ await lockOperation(scopedDb, request.operationId);
2874
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
2875
+ if (existing) {
2876
+ assertOperationIdentity(existing, {
2877
+ resourceKind: "auth_run",
2878
+ resourceId: input.authRunId,
2879
+ kind: "protected_fill",
2880
+ requestDigest: digest,
2881
+ actorSubjectId: input.actorSubjectId,
2882
+ });
2883
+ const run = await loadAuthRunRow(scopedDb, input.workspaceId, input.authRunId);
2884
+ if (!run) throw new InteractionResourceNotFoundError("Auth run not found");
2885
+ return protectedAuthPreparationFromOperation(existing, run);
2886
+ }
2887
+
2888
+ const [run] = await scopedDb
2889
+ .select()
2890
+ .from(schema.authRuns)
2891
+ .where(
2892
+ and(
2893
+ eq(schema.authRuns.workspaceId, input.workspaceId),
2894
+ eq(schema.authRuns.id, input.authRunId),
2895
+ ),
2896
+ )
2897
+ .for("update")
2898
+ .limit(1);
2899
+ if (!run) throw new InteractionResourceNotFoundError("Auth run not found");
2900
+ if (run.settledAt) throw new InteractionResourceStateError("Auth run is already settled");
2901
+ if (run.version !== request.expectedVersion) {
2902
+ throw new InteractionResourceConflictError("Auth run changed before protected fill");
2903
+ }
2904
+ if (
2905
+ run.targetGeneration !== request.expectedTargetGeneration ||
2906
+ run.documentGeneration !== request.expectedDocumentGeneration ||
2907
+ !request.expectedDocumentGeneration ||
2908
+ !request.expectedFrameId
2909
+ ) {
2910
+ throw new InteractionResourceConflictError(
2911
+ "Protected fill does not match the exact auth-run document",
2912
+ );
2913
+ }
2914
+ const [browser] = await scopedDb
2915
+ .select({
2916
+ lifecycle: schema.browserSessions.lifecycle,
2917
+ controllerGeneration: schema.browserSessions.controllerGeneration,
2918
+ })
2919
+ .from(schema.browserSessions)
2920
+ .where(
2921
+ and(
2922
+ eq(schema.browserSessions.workspaceId, input.workspaceId),
2923
+ eq(schema.browserSessions.id, run.browserSessionId),
2924
+ ),
2925
+ )
2926
+ .for("update")
2927
+ .limit(1);
2928
+ if (
2929
+ !browser ||
2930
+ browser.lifecycle !== "active" ||
2931
+ browser.controllerGeneration !== run.controllerGeneration
2932
+ ) {
2933
+ throw new InteractionResourceConflictError(
2934
+ "Protected fill belongs to a stale browser controller",
2935
+ );
2936
+ }
2937
+ const connectionRow = await getSiteAuthConnectionRow(
2938
+ scopedDb,
2939
+ input.workspaceId,
2940
+ run.siteAuthConnectionId,
2941
+ );
2942
+ if (connectionRow.status !== "active") {
2943
+ throw new InteractionResourceStateError("Site auth connection is archived");
2944
+ }
2945
+ const connection = siteAuthConnectionFromRow(connectionRow);
2946
+ assertAuthSelection(connection, {
2947
+ methodId: run.methodId,
2948
+ authorityId: request.authorityId,
2949
+ });
2950
+ if (run.authorityId && run.authorityId !== request.authorityId) {
2951
+ throw new InteractionResourceConflictError("Auth run selected another authority");
2952
+ }
2953
+ const authority = connection.authorities.find(
2954
+ (candidate) => candidate.id === request.authorityId,
2955
+ );
2956
+ if (!authority) throw new InteractionResourceStateError("Auth authority is not configured");
2957
+ const fields = protectedAuthFields(authority, request.fields);
2958
+ if (
2959
+ (authority.kind === "connection_fields" &&
2960
+ (!Number.isSafeInteger(input.credentialVersion) || input.credentialVersion! < 1)) ||
2961
+ (authority.kind !== "connection_fields" && input.credentialVersion !== null)
2962
+ ) {
2963
+ throw new InteractionResourceStateError("Protected-fill credential version is invalid");
2964
+ }
2965
+ const metadata: ProtectedAuthOperationMetadata = {
2966
+ schemaVersion: 1,
2967
+ authRunVersion: run.version,
2968
+ authority,
2969
+ origins: [...connection.origins],
2970
+ fields,
2971
+ credentialVersion: input.credentialVersion,
2972
+ };
2973
+ await scopedDb.insert(schema.interactionResourceOperations).values({
2974
+ operationId: request.operationId,
2975
+ accountId: input.accountId,
2976
+ workspaceId: input.workspaceId,
2977
+ resourceKind: "auth_run",
2978
+ resourceId: input.authRunId,
2979
+ kind: "protected_fill",
2980
+ requestDigest: digest,
2981
+ metadata,
2982
+ state: "prepared",
2983
+ actorSubjectId: input.actorSubjectId,
2984
+ });
2985
+ return {
2986
+ run: authRunFromRow(run),
2987
+ authority,
2988
+ origins: [...connection.origins],
2989
+ credentialVersion: input.credentialVersion,
2990
+ operationState: "prepared",
2991
+ response: null,
2992
+ replayed: false,
2993
+ };
2994
+ });
2995
+ }
2996
+
2997
+ export async function dispatchProtectedAuthFill(
2998
+ db: Database,
2999
+ input: InteractionMutationScope & { authRunId: string; operationId: string },
3000
+ ): Promise<ResourceOperationRow["state"]> {
3001
+ return await withRlsContext(db, input, async (scopedDb) => {
3002
+ await lockOperation(scopedDb, input.operationId);
3003
+ const [operation] = await scopedDb
3004
+ .select()
3005
+ .from(schema.interactionResourceOperations)
3006
+ .where(
3007
+ and(
3008
+ eq(schema.interactionResourceOperations.workspaceId, input.workspaceId),
3009
+ eq(schema.interactionResourceOperations.operationId, input.operationId),
3010
+ ),
3011
+ )
3012
+ .for("update")
3013
+ .limit(1);
3014
+ if (!operation)
3015
+ throw new InteractionResourceNotFoundError("Protected-fill operation not found");
3016
+ assertProtectedFillOperationBinding(operation, input);
3017
+ if (operation.state === "prepared") {
3018
+ const [dispatched] = await scopedDb
3019
+ .update(schema.interactionResourceOperations)
3020
+ .set({ state: "dispatched" })
3021
+ .where(
3022
+ and(
3023
+ eq(schema.interactionResourceOperations.operationId, input.operationId),
3024
+ eq(schema.interactionResourceOperations.state, "prepared"),
3025
+ ),
3026
+ )
3027
+ .returning({ state: schema.interactionResourceOperations.state });
3028
+ if (!dispatched) {
3029
+ throw new InteractionResourceConflictError("Protected-fill dispatch lost its fence");
3030
+ }
3031
+ return dispatched.state;
3032
+ }
3033
+ return operation.state;
3034
+ });
3035
+ }
3036
+
3037
+ export async function completeProtectedAuthFill(
3038
+ db: Database,
3039
+ input: InteractionMutationScope & {
3040
+ authRunId: string;
3041
+ operationId: string;
3042
+ status: ProtectedAuthFillResponseValue["status"];
3043
+ targetGeneration?: string;
3044
+ documentGeneration?: string | null;
3045
+ failureCode?: string;
3046
+ intervention?: {
3047
+ originatingSessionId: string;
3048
+ originatingTurnId?: string | null;
3049
+ originatingAttemptId?: string | null;
3050
+ originatingToolOperationId?: string | null;
3051
+ kind: "manual_login" | "mfa" | "external_action" | "confirmation" | "other";
3052
+ reason: string;
3053
+ expiresInSeconds: number;
3054
+ };
3055
+ },
3056
+ ): Promise<ProtectedAuthFillResponseValue> {
3057
+ try {
3058
+ return await withRlsContext(db, input, async (scopedDb) => {
3059
+ await lockOperation(scopedDb, input.operationId);
3060
+ const [operation] = await scopedDb
3061
+ .select()
3062
+ .from(schema.interactionResourceOperations)
3063
+ .where(
3064
+ and(
3065
+ eq(schema.interactionResourceOperations.workspaceId, input.workspaceId),
3066
+ eq(schema.interactionResourceOperations.operationId, input.operationId),
3067
+ ),
3068
+ )
3069
+ .for("update")
3070
+ .limit(1);
3071
+ if (!operation)
3072
+ throw new InteractionResourceNotFoundError("Protected-fill operation not found");
3073
+ assertProtectedFillOperationBinding(operation, input);
3074
+ if (operation.state === "completed" && operation.result) {
3075
+ return {
3076
+ ...ProtectedAuthFillResponse.parse(operation.result),
3077
+ replayed: true,
3078
+ };
3079
+ }
3080
+ if (operation.state === "failed" || operation.state === "outcome_unknown") {
3081
+ throw new InteractionResourceStateError(
3082
+ `Protected-fill operation is ${operation.state.replace("_", " ")}`,
3083
+ );
3084
+ }
3085
+ if (
3086
+ (input.status === "submitted" || input.status === "working") &&
3087
+ operation.state !== "dispatched"
3088
+ ) {
3089
+ throw new InteractionResourceStateError("Protected fill was not dispatched");
3090
+ }
3091
+ const metadata = protectedAuthOperationMetadata(operation.metadata);
3092
+ const [run] = await scopedDb
3093
+ .select()
3094
+ .from(schema.authRuns)
3095
+ .where(
3096
+ and(
3097
+ eq(schema.authRuns.workspaceId, input.workspaceId),
3098
+ eq(schema.authRuns.id, input.authRunId),
3099
+ ),
3100
+ )
3101
+ .for("update")
3102
+ .limit(1);
3103
+ if (!run) throw new InteractionResourceNotFoundError("Auth run not found");
3104
+ if (run.version !== metadata.authRunVersion || run.settledAt) {
3105
+ throw new InteractionResourceConflictError("Auth run changed after protected fill began");
3106
+ }
3107
+ if (input.targetGeneration && input.targetGeneration !== run.targetGeneration) {
3108
+ throw new InteractionResourceConflictError("Protected-fill result targets another tab");
3109
+ }
3110
+ const submitted = input.status === "submitted" || input.status === "working";
3111
+ const needsHuman = input.status === "needs_human";
3112
+ if (needsHuman !== Boolean(input.intervention)) {
3113
+ throw new InteractionResourceStateError(
3114
+ "Human protected fill requires one durable intervention",
3115
+ );
3116
+ }
3117
+ if (needsHuman && metadata.authority.kind !== "human") {
3118
+ throw new InteractionResourceStateError(
3119
+ "Only a human auth authority can request human intervention",
3120
+ );
3121
+ }
3122
+ let interventionId: string | null = null;
3123
+ if (needsHuman) {
3124
+ const intervention = input.intervention!;
3125
+ if (
3126
+ (intervention.originatingAttemptId && !intervention.originatingTurnId) ||
3127
+ (intervention.originatingToolOperationId && !intervention.originatingAttemptId)
3128
+ ) {
3129
+ throw new InteractionResourceStateError(
3130
+ "Protected-fill intervention provenance is incomplete",
3131
+ );
3132
+ }
3133
+ const request = CreateInteractionInterventionRequest.parse({
3134
+ operationId: input.operationId,
3135
+ resourceKind: "browser_session",
3136
+ resourceId: run.browserSessionId,
3137
+ targetId: run.targetId,
3138
+ expectedControllerGeneration: run.controllerGeneration,
3139
+ expectedTargetGeneration: run.targetGeneration,
3140
+ expectedDocumentGeneration: run.documentGeneration,
3141
+ kind: intervention.kind,
3142
+ reason: intervention.reason,
3143
+ authRunId: run.id,
3144
+ expiresInSeconds: intervention.expiresInSeconds,
3145
+ });
3146
+ interventionId = randomUUID();
3147
+ const [created] = await scopedDb
3148
+ .insert(schema.interactionInterventions)
3149
+ .values({
3150
+ id: interventionId,
3151
+ accountId: input.accountId,
3152
+ workspaceId: input.workspaceId,
3153
+ resourceKind: request.resourceKind,
3154
+ resourceId: request.resourceId,
3155
+ targetId: request.targetId,
3156
+ controllerGeneration: request.expectedControllerGeneration,
3157
+ targetGeneration: request.expectedTargetGeneration,
3158
+ documentGeneration: request.expectedDocumentGeneration,
3159
+ kind: request.kind,
3160
+ reason: request.reason,
3161
+ authRunId: run.id,
3162
+ originatingSessionId: intervention.originatingSessionId,
3163
+ originatingTurnId: intervention.originatingTurnId ?? null,
3164
+ originatingAttemptId: intervention.originatingAttemptId ?? null,
3165
+ originatingToolOperationId: intervention.originatingToolOperationId ?? null,
3166
+ operationId: input.operationId,
3167
+ expiresAt: sql`now() + (${request.expiresInSeconds} * interval '1 second')`,
3168
+ })
3169
+ .returning({ id: schema.interactionInterventions.id });
3170
+ if (!created) {
3171
+ throw new Error("Protected-fill intervention insert returned no row");
3172
+ }
3173
+ }
3174
+ const failureCode =
3175
+ submitted || needsHuman ? null : (input.failureCode ?? "protected_fill_failed");
3176
+ const [updated] = await scopedDb
3177
+ .update(schema.authRuns)
3178
+ .set({
3179
+ authorityId: metadata.authority.id,
3180
+ state: submitted ? "working" : needsHuman ? "awaiting_secret" : "failed",
3181
+ choices: [],
3182
+ pendingFields: needsHuman
3183
+ ? metadata.fields.map((field) => ({
3184
+ id: field.id,
3185
+ label: field.id,
3186
+ purpose: field.purpose,
3187
+ }))
3188
+ : [],
3189
+ externalAction: null,
3190
+ interventionId,
3191
+ verifiedUrl: null,
3192
+ failureCode,
3193
+ documentGeneration:
3194
+ submitted && input.documentGeneration !== undefined
3195
+ ? input.documentGeneration
3196
+ : run.documentGeneration,
3197
+ version: run.version + 1,
3198
+ updatedAt: sql`now()`,
3199
+ settledAt: failureCode ? sql`now()` : null,
3200
+ })
3201
+ .where(
3202
+ and(
3203
+ eq(schema.authRuns.workspaceId, input.workspaceId),
3204
+ eq(schema.authRuns.id, input.authRunId),
3205
+ eq(schema.authRuns.version, metadata.authRunVersion),
3206
+ sql`${schema.authRuns.settledAt} is null`,
3207
+ ),
3208
+ )
3209
+ .returning();
3210
+ if (!updated) {
3211
+ throw new InteractionResourceConflictError("Protected-fill settlement lost its auth fence");
3212
+ }
3213
+ await projectSettledAuthRunHealth(scopedDb, {
3214
+ run: updated,
3215
+ actorSubjectId: input.actorSubjectId,
3216
+ });
3217
+ const response = ProtectedAuthFillResponse.parse({
3218
+ run: authRunFromRow(updated),
3219
+ status: input.status,
3220
+ operationId: input.operationId,
3221
+ replayed: false,
3222
+ });
3223
+ const [completed] = await scopedDb
3224
+ .update(schema.interactionResourceOperations)
3225
+ .set({
3226
+ state: "completed",
3227
+ resultVersion: updated.version,
3228
+ result: response,
3229
+ settledAt: sql`now()`,
3230
+ })
3231
+ .where(
3232
+ and(
3233
+ eq(schema.interactionResourceOperations.operationId, input.operationId),
3234
+ inArray(schema.interactionResourceOperations.state, ["prepared", "dispatched"]),
3235
+ ),
3236
+ )
3237
+ .returning({
3238
+ operationId: schema.interactionResourceOperations.operationId,
3239
+ });
3240
+ if (!completed) {
3241
+ throw new InteractionResourceConflictError(
3242
+ "Protected-fill settlement lost its operation fence",
3243
+ );
3244
+ }
3245
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
3246
+ return response;
3247
+ });
3248
+ } catch (error) {
3249
+ if (
3250
+ postgresConstraint(error) === "interaction_interventions_workspace_operation_uq" ||
3251
+ postgresConstraint(error) === "interaction_interventions_open_target_kind_uq" ||
3252
+ postgresConstraint(error) === "interaction_interventions_open_auth_run_uq"
3253
+ ) {
3254
+ throw new InteractionResourceConflictError(
3255
+ "A matching interaction intervention is already open",
3256
+ );
3257
+ }
3258
+ throw error;
3259
+ }
3260
+ }
3261
+
3262
+ export async function markProtectedAuthFillOutcomeUnknown(
3263
+ db: Database,
3264
+ input: InteractionMutationScope & {
3265
+ authRunId: string;
3266
+ operationId: string;
3267
+ errorCode: string;
3268
+ },
3269
+ ): Promise<void> {
3270
+ await withRlsContext(db, input, async (scopedDb) => {
3271
+ await lockOperation(scopedDb, input.operationId);
3272
+ const [operation] = await scopedDb
3273
+ .select()
3274
+ .from(schema.interactionResourceOperations)
3275
+ .where(
3276
+ and(
3277
+ eq(schema.interactionResourceOperations.workspaceId, input.workspaceId),
3278
+ eq(schema.interactionResourceOperations.operationId, input.operationId),
3279
+ ),
3280
+ )
3281
+ .for("update")
3282
+ .limit(1);
3283
+ if (!operation)
3284
+ throw new InteractionResourceNotFoundError("Protected-fill operation not found");
3285
+ assertProtectedFillOperationBinding(operation, input);
3286
+ if (
3287
+ operation.state === "completed" ||
3288
+ operation.state === "failed" ||
3289
+ operation.state === "outcome_unknown"
3290
+ ) {
3291
+ return;
3292
+ }
3293
+ const [marked] = await scopedDb
3294
+ .update(schema.interactionResourceOperations)
3295
+ .set({
3296
+ state: "outcome_unknown",
3297
+ errorCode: input.errorCode,
3298
+ settledAt: sql`now()`,
3299
+ })
3300
+ .where(
3301
+ and(
3302
+ eq(schema.interactionResourceOperations.operationId, input.operationId),
3303
+ inArray(schema.interactionResourceOperations.state, ["prepared", "dispatched"]),
3304
+ ),
3305
+ )
3306
+ .returning({
3307
+ operationId: schema.interactionResourceOperations.operationId,
3308
+ });
3309
+ if (!marked) {
3310
+ throw new InteractionResourceConflictError(
3311
+ "Protected-fill unknown outcome lost its operation fence",
3312
+ );
3313
+ }
3314
+ const metadata = protectedAuthOperationMetadata(operation.metadata);
3315
+ const [failedRun] = await scopedDb
3316
+ .update(schema.authRuns)
3317
+ .set({
3318
+ state: "failed",
3319
+ choices: [],
3320
+ pendingFields: [],
3321
+ externalAction: null,
3322
+ verifiedUrl: null,
3323
+ failureCode: input.errorCode,
3324
+ version: sql`${schema.authRuns.version} + 1`,
3325
+ updatedAt: sql`now()`,
3326
+ settledAt: sql`now()`,
3327
+ })
3328
+ .where(
3329
+ and(
3330
+ eq(schema.authRuns.workspaceId, input.workspaceId),
3331
+ eq(schema.authRuns.id, input.authRunId),
3332
+ eq(schema.authRuns.version, metadata.authRunVersion),
3333
+ sql`${schema.authRuns.settledAt} is null`,
3334
+ ),
3335
+ )
3336
+ .returning();
3337
+ if (failedRun) {
3338
+ await projectSettledAuthRunHealth(scopedDb, {
3339
+ run: failedRun,
3340
+ actorSubjectId: input.actorSubjectId,
3341
+ });
3342
+ }
3343
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
3344
+ });
3345
+ }
3346
+
3347
+ function assertProtectedFillOperationBinding(
3348
+ operation: ResourceOperationRow,
3349
+ input: { authRunId: string; actorSubjectId: string },
3350
+ ): void {
3351
+ if (
3352
+ operation.resourceKind !== "auth_run" ||
3353
+ operation.resourceId !== input.authRunId ||
3354
+ operation.kind !== "protected_fill" ||
3355
+ operation.actorSubjectId !== input.actorSubjectId
3356
+ ) {
3357
+ throw new InteractionResourceConflictError(
3358
+ "Operation id is bound to another protected-fill request",
3359
+ );
3360
+ }
3361
+ }
3362
+
3363
+ export async function verifyAuthRun(
3364
+ db: Database,
3365
+ input: InteractionMutationScope & {
3366
+ authRunId: string;
3367
+ controllerGeneration: string;
3368
+ targetId: string;
3369
+ targetGeneration: string;
3370
+ documentGeneration: string | null;
3371
+ url: string;
3372
+ } & VerifyAuthRunRequestValue,
3373
+ ): Promise<AuthRunMutationResponseValue> {
3374
+ const request = VerifyAuthRunRequest.parse({
3375
+ operationId: input.operationId,
3376
+ expectedVersion: input.expectedVersion,
3377
+ });
3378
+ const digest = operationDigest({
3379
+ version: 1,
3380
+ authRunId: input.authRunId,
3381
+ controllerGeneration: input.controllerGeneration,
3382
+ targetId: input.targetId,
3383
+ targetGeneration: input.targetGeneration,
3384
+ documentGeneration: input.documentGeneration,
3385
+ url: input.url,
3386
+ expectedVersion: request.expectedVersion,
3387
+ actorSubjectId: input.actorSubjectId,
3388
+ });
3389
+ return await withRlsContext(db, input, async (scopedDb) => {
3390
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
3391
+ await lockOperation(scopedDb, request.operationId);
3392
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
3393
+ if (existing) {
3394
+ assertOperation(existing, {
3395
+ resourceKind: "auth_run",
3396
+ resourceId: input.authRunId,
3397
+ kind: "verify",
3398
+ requestDigest: digest,
3399
+ actorSubjectId: input.actorSubjectId,
3400
+ });
3401
+ return {
3402
+ ...AuthRunMutationResponse.parse(existing.result),
3403
+ replayed: true,
3404
+ };
3405
+ }
3406
+ const [run] = await scopedDb
3407
+ .select()
3408
+ .from(schema.authRuns)
3409
+ .where(
3410
+ and(
3411
+ eq(schema.authRuns.workspaceId, input.workspaceId),
3412
+ eq(schema.authRuns.id, input.authRunId),
3413
+ ),
3414
+ )
3415
+ .for("update")
3416
+ .limit(1);
3417
+ if (!run) throw new InteractionResourceNotFoundError("Auth run not found");
3418
+ if (run.version !== request.expectedVersion) {
3419
+ throw new InteractionResourceConflictError("Auth run changed before verification");
3420
+ }
3421
+ if (
3422
+ run.controllerGeneration !== input.controllerGeneration ||
3423
+ run.targetId !== input.targetId ||
3424
+ run.targetGeneration !== input.targetGeneration
3425
+ ) {
3426
+ throw new InteractionResourceConflictError("Verification targets another browser tab");
3427
+ }
3428
+ const connection = await getSiteAuthConnectionRow(
3429
+ scopedDb,
3430
+ input.workspaceId,
3431
+ run.siteAuthConnectionId,
3432
+ );
3433
+ const verified = connection.verificationUrlPrefixes.some((prefix) =>
3434
+ input.url.startsWith(prefix),
3435
+ );
3436
+ let resultRun = run;
3437
+ if (verified) {
3438
+ if (run.settledAt && run.state !== "verified") {
3439
+ throw new InteractionResourceStateError("Auth run is already settled");
3440
+ }
3441
+ if (!run.settledAt) {
3442
+ const [updated] = await scopedDb
3443
+ .update(schema.authRuns)
3444
+ .set({
3445
+ state: "verified",
3446
+ choices: [],
3447
+ pendingFields: [],
3448
+ externalAction: null,
3449
+ interventionId: null,
3450
+ documentGeneration: input.documentGeneration,
3451
+ verifiedUrl: input.url,
3452
+ failureCode: null,
3453
+ version: run.version + 1,
3454
+ updatedAt: sql`now()`,
3455
+ settledAt: sql`now()`,
3456
+ })
3457
+ .where(
3458
+ and(
3459
+ eq(schema.authRuns.workspaceId, input.workspaceId),
3460
+ eq(schema.authRuns.id, input.authRunId),
3461
+ eq(schema.authRuns.version, request.expectedVersion),
3462
+ ),
3463
+ )
3464
+ .returning();
3465
+ if (!updated)
3466
+ throw new InteractionResourceConflictError("Auth verification lost its fence");
3467
+ resultRun = updated;
3468
+ await projectSettledAuthRunHealth(scopedDb, {
3469
+ run: updated,
3470
+ actorSubjectId: input.actorSubjectId,
3471
+ });
3472
+ }
3473
+ }
3474
+ const response = AuthRunMutationResponse.parse({
3475
+ run: authRunFromRow(resultRun),
3476
+ operationId: request.operationId,
3477
+ replayed: false,
3478
+ });
3479
+ await insertCompletedOperation(scopedDb, {
3480
+ operationId: request.operationId,
3481
+ accountId: input.accountId,
3482
+ workspaceId: input.workspaceId,
3483
+ resourceKind: "auth_run",
3484
+ resourceId: input.authRunId,
3485
+ kind: "verify",
3486
+ requestDigest: digest,
3487
+ resultVersion: resultRun.version,
3488
+ result: response,
3489
+ actorSubjectId: input.actorSubjectId,
3490
+ });
3491
+ if (verified && !run.settledAt) {
3492
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
3493
+ }
3494
+ return response;
3495
+ });
3496
+ }
3497
+
3498
+ async function getSiteAuthConnectionRow(
3499
+ db: Database,
3500
+ workspaceId: string,
3501
+ siteAuthConnectionId: string,
3502
+ ): Promise<SiteAuthConnectionRow> {
3503
+ const [row] = await db
3504
+ .select()
3505
+ .from(schema.siteAuthConnections)
3506
+ .where(
3507
+ and(
3508
+ eq(schema.siteAuthConnections.workspaceId, workspaceId),
3509
+ eq(schema.siteAuthConnections.id, siteAuthConnectionId),
3510
+ ),
3511
+ )
3512
+ .limit(1);
3513
+ if (!row) throw new InteractionResourceNotFoundError("Site auth connection not found");
3514
+ return row;
3515
+ }
3516
+
3517
+ function interventionFromRow(row: InterventionRow): InteractionInterventionValue {
3518
+ return InteractionIntervention.parse({
3519
+ id: row.id,
3520
+ accountId: row.accountId,
3521
+ workspaceId: row.workspaceId,
3522
+ resourceKind: row.resourceKind,
3523
+ resourceId: row.resourceId,
3524
+ targetId: row.targetId,
3525
+ controllerGeneration: row.controllerGeneration,
3526
+ targetGeneration: row.targetGeneration,
3527
+ documentGeneration: row.documentGeneration,
3528
+ kind: row.kind,
3529
+ reason: row.reason,
3530
+ status: row.status,
3531
+ authRunId: row.authRunId,
3532
+ originatingSessionId: row.originatingSessionId,
3533
+ originatingTurnId: row.originatingTurnId,
3534
+ originatingAttemptId: row.originatingAttemptId,
3535
+ originatingToolOperationId: row.originatingToolOperationId,
3536
+ responseActorSubjectId: row.responseActorSubjectId,
3537
+ version: row.version,
3538
+ operationId: row.operationId,
3539
+ expiresAt: iso(row.expiresAt),
3540
+ createdAt: iso(row.createdAt),
3541
+ updatedAt: iso(row.updatedAt),
3542
+ settledAt: row.settledAt ? iso(row.settledAt) : null,
3543
+ });
3544
+ }
3545
+
3546
+ async function loadInterventionRow(
3547
+ db: Database,
3548
+ workspaceId: string,
3549
+ interventionId: string,
3550
+ ): Promise<InterventionRow | null> {
3551
+ const [row] = await db
3552
+ .select()
3553
+ .from(schema.interactionInterventions)
3554
+ .where(
3555
+ and(
3556
+ eq(schema.interactionInterventions.workspaceId, workspaceId),
3557
+ eq(schema.interactionInterventions.id, interventionId),
3558
+ ),
3559
+ )
3560
+ .limit(1);
3561
+ return row ?? null;
3562
+ }
3563
+
3564
+ async function assertActiveInteractionResource(
3565
+ db: Database,
3566
+ input: {
3567
+ workspaceId: string;
3568
+ resourceKind: "browser_session" | "computer_session";
3569
+ resourceId: string;
3570
+ expectedControllerGeneration: string;
3571
+ },
3572
+ ): Promise<void> {
3573
+ if (input.resourceKind === "browser_session") {
3574
+ const [resource] = await db
3575
+ .select({
3576
+ lifecycle: schema.browserSessions.lifecycle,
3577
+ controllerGeneration: schema.browserSessions.controllerGeneration,
3578
+ })
3579
+ .from(schema.browserSessions)
3580
+ .where(
3581
+ and(
3582
+ eq(schema.browserSessions.workspaceId, input.workspaceId),
3583
+ eq(schema.browserSessions.id, input.resourceId),
3584
+ ),
3585
+ )
3586
+ .for("update")
3587
+ .limit(1);
3588
+ if (!resource) throw new InteractionResourceNotFoundError("Browser session not found");
3589
+ if (resource.lifecycle !== "active" || !resource.controllerGeneration) {
3590
+ throw new InteractionResourceStateError("Browser session is not active");
3591
+ }
3592
+ if (resource.controllerGeneration !== input.expectedControllerGeneration) {
3593
+ throw new InteractionResourceConflictError(
3594
+ "Browser intervention belongs to a stale controller",
3595
+ );
3596
+ }
3597
+ return;
3598
+ }
3599
+ const [resource] = await db
3600
+ .select({
3601
+ lifecycle: schema.computerSessions.lifecycle,
3602
+ controllerGeneration: schema.computerSessions.controllerGeneration,
3603
+ })
3604
+ .from(schema.computerSessions)
3605
+ .where(
3606
+ and(
3607
+ eq(schema.computerSessions.workspaceId, input.workspaceId),
3608
+ eq(schema.computerSessions.id, input.resourceId),
3609
+ ),
3610
+ )
3611
+ .for("update")
3612
+ .limit(1);
3613
+ if (!resource) throw new InteractionResourceNotFoundError("Computer session not found");
3614
+ if (resource.lifecycle !== "active" || !resource.controllerGeneration) {
3615
+ throw new InteractionResourceStateError("Computer session is not active");
3616
+ }
3617
+ if (resource.controllerGeneration !== input.expectedControllerGeneration) {
3618
+ throw new InteractionResourceConflictError(
3619
+ "Computer intervention belongs to a stale controller",
3620
+ );
3621
+ }
3622
+ }
3623
+
3624
+ async function settleLinkedAuthRun(
3625
+ db: Database,
3626
+ input: {
3627
+ workspaceId: string;
3628
+ authRunId: string | null;
3629
+ interventionId: string;
3630
+ outcome: "completed" | "dismissed" | "expired" | "cancelled";
3631
+ },
3632
+ ): Promise<void> {
3633
+ if (!input.authRunId) return;
3634
+ const [run] = await db
3635
+ .select()
3636
+ .from(schema.authRuns)
3637
+ .where(
3638
+ and(
3639
+ eq(schema.authRuns.workspaceId, input.workspaceId),
3640
+ eq(schema.authRuns.id, input.authRunId),
3641
+ ),
3642
+ )
3643
+ .for("update")
3644
+ .limit(1);
3645
+ if (!run || run.interventionId !== input.interventionId || run.settledAt) return;
3646
+ const resumed = input.outcome === "completed";
3647
+ const [settled] = await db
3648
+ .update(schema.authRuns)
3649
+ .set({
3650
+ state: resumed ? "working" : input.outcome === "cancelled" ? "cancelled" : "failed",
3651
+ choices: [],
3652
+ pendingFields: [],
3653
+ externalAction: null,
3654
+ interventionId: null,
3655
+ failureCode:
3656
+ resumed || input.outcome === "cancelled" ? null : `intervention_${input.outcome}`,
3657
+ version: run.version + 1,
3658
+ updatedAt: sql`now()`,
3659
+ settledAt: resumed ? null : sql`now()`,
3660
+ })
3661
+ .where(
3662
+ and(
3663
+ eq(schema.authRuns.workspaceId, input.workspaceId),
3664
+ eq(schema.authRuns.id, input.authRunId),
3665
+ eq(schema.authRuns.version, run.version),
3666
+ eq(schema.authRuns.interventionId, input.interventionId),
3667
+ ),
3668
+ )
3669
+ .returning();
3670
+ if (!settled) {
3671
+ throw new InteractionResourceConflictError(
3672
+ "Linked auth run changed before intervention settlement",
3673
+ );
3674
+ }
3675
+ await projectSettledAuthRunHealth(db, {
3676
+ run: settled,
3677
+ actorSubjectId: run.createdBySubjectId,
3678
+ });
3679
+ }
3680
+
3681
+ async function expireInterventionsInScope(
3682
+ db: Database,
3683
+ input: { accountId: string; workspaceId: string },
3684
+ ): Promise<number> {
3685
+ const expired = await db
3686
+ .update(schema.interactionInterventions)
3687
+ .set({
3688
+ status: "expired",
3689
+ version: sql`${schema.interactionInterventions.version} + 1`,
3690
+ updatedAt: sql`now()`,
3691
+ settledAt: sql`now()`,
3692
+ })
3693
+ .where(
3694
+ and(
3695
+ eq(schema.interactionInterventions.workspaceId, input.workspaceId),
3696
+ eq(schema.interactionInterventions.status, "open"),
3697
+ // Model-owned interventions are also pending approvals. Only the
3698
+ // owning session workflow may expire those rows, because it must append
3699
+ // the exact rejection that resumes the frozen RunState atomically.
3700
+ sql`${schema.interactionInterventions.originatingToolCallId} is null`,
3701
+ sql`${schema.interactionInterventions.expiresAt} <= now()`,
3702
+ ),
3703
+ )
3704
+ .returning({
3705
+ id: schema.interactionInterventions.id,
3706
+ authRunId: schema.interactionInterventions.authRunId,
3707
+ });
3708
+ for (const intervention of expired) {
3709
+ await settleLinkedAuthRun(db, {
3710
+ workspaceId: input.workspaceId,
3711
+ authRunId: intervention.authRunId,
3712
+ interventionId: intervention.id,
3713
+ outcome: "expired",
3714
+ });
3715
+ }
3716
+ if (expired.length > 0) {
3717
+ await advanceWorkspaceInteractionRevision(db, input.accountId, input.workspaceId);
3718
+ }
3719
+ return expired.length;
3720
+ }
3721
+
3722
+ export async function listInteractionInterventions(
3723
+ db: Database,
3724
+ input: {
3725
+ accountId: string;
3726
+ workspaceId: string;
3727
+ resourceKind?: "browser_session" | "computer_session";
3728
+ resourceId?: string;
3729
+ includeSettled?: boolean;
3730
+ },
3731
+ ): Promise<InteractionInterventionListResponseValue> {
3732
+ return await withRlsContext(db, input, async (scopedDb) => {
3733
+ await expireInterventionsInScope(scopedDb, input);
3734
+ const predicates = [eq(schema.interactionInterventions.workspaceId, input.workspaceId)];
3735
+ if (input.resourceKind) {
3736
+ predicates.push(eq(schema.interactionInterventions.resourceKind, input.resourceKind));
3737
+ }
3738
+ if (input.resourceId) {
3739
+ predicates.push(eq(schema.interactionInterventions.resourceId, input.resourceId));
3740
+ }
3741
+ if (!input.includeSettled) {
3742
+ predicates.push(eq(schema.interactionInterventions.status, "open"));
3743
+ }
3744
+ const interventions = await scopedDb
3745
+ .select()
3746
+ .from(schema.interactionInterventions)
3747
+ .where(and(...predicates))
3748
+ .orderBy(
3749
+ asc(schema.interactionInterventions.createdAt),
3750
+ asc(schema.interactionInterventions.id),
3751
+ );
3752
+ return InteractionInterventionListResponse.parse({
3753
+ interventions: interventions.map(interventionFromRow),
3754
+ });
3755
+ });
3756
+ }
3757
+
3758
+ export async function getInteractionIntervention(
3759
+ db: Database,
3760
+ input: { accountId: string; workspaceId: string; interventionId: string },
3761
+ ): Promise<InteractionInterventionValue> {
3762
+ return await withRlsContext(db, input, async (scopedDb) => {
3763
+ await expireInterventionsInScope(scopedDb, input);
3764
+ const row = await loadInterventionRow(scopedDb, input.workspaceId, input.interventionId);
3765
+ if (!row) throw new InteractionResourceNotFoundError("Interaction intervention not found");
3766
+ return interventionFromRow(row);
3767
+ });
3768
+ }
3769
+
3770
+ export type AttemptInteractionInterventionRequest = {
3771
+ id: string;
3772
+ operationId: string;
3773
+ accountId: string;
3774
+ workspaceId: string;
3775
+ originatingSessionId: string;
3776
+ originatingTurnId: string;
3777
+ originatingAttemptId: string;
3778
+ toolCallId: string;
3779
+ input: RequestHumanInteractionToolInputValue;
3780
+ };
3781
+
3782
+ /**
3783
+ * Persist one interaction interruption inside the same transaction as its
3784
+ * frozen Agent RunState. This is deliberately transaction-local: callers must
3785
+ * already hold the session/turn/attempt settlement locks and RLS context.
3786
+ */
3787
+ export async function persistAttemptInteractionInterventionInTransaction(
3788
+ db: Database,
3789
+ raw: AttemptInteractionInterventionRequest,
3790
+ ): Promise<InteractionInterventionValue> {
3791
+ const input = RequestHumanInteractionToolInput.parse(raw.input);
3792
+ if (Buffer.byteLength(raw.toolCallId) < 1 || Buffer.byteLength(raw.toolCallId) > 1_024) {
3793
+ throw new InteractionResourceStateError("Interaction tool call id is invalid");
3794
+ }
3795
+ if (input.operation === "wait") {
3796
+ const [current] = await db
3797
+ .select()
3798
+ .from(schema.interactionInterventions)
3799
+ .where(
3800
+ and(
3801
+ eq(schema.interactionInterventions.workspaceId, raw.workspaceId),
3802
+ eq(schema.interactionInterventions.id, input.interventionId),
3803
+ ),
3804
+ )
3805
+ .for("update")
3806
+ .limit(1);
3807
+ if (!current) throw new InteractionResourceNotFoundError("Interaction intervention not found");
3808
+ if (current.status !== "open" || current.expiresAt.getTime() <= Date.now()) {
3809
+ throw new InteractionResourceStateError("Interaction intervention is no longer open");
3810
+ }
3811
+ if (
3812
+ current.originatingSessionId !== raw.originatingSessionId ||
3813
+ current.originatingTurnId !== raw.originatingTurnId
3814
+ ) {
3815
+ throw new InteractionResourceConflictError(
3816
+ "Interaction intervention belongs to another agent operation",
3817
+ );
3818
+ }
3819
+ if (
3820
+ current.originatingToolCallId !== null &&
3821
+ current.originatingToolCallId !== raw.toolCallId
3822
+ ) {
3823
+ throw new InteractionResourceConflictError(
3824
+ "Interaction intervention already has another waiting tool call",
3825
+ );
3826
+ }
3827
+ const row =
3828
+ current.originatingToolCallId === raw.toolCallId
3829
+ ? current
3830
+ : (
3831
+ await db
3832
+ .update(schema.interactionInterventions)
3833
+ .set({
3834
+ originatingToolCallId: raw.toolCallId,
3835
+ updatedAt: sql`now()`,
3836
+ })
3837
+ .where(
3838
+ and(
3839
+ eq(schema.interactionInterventions.workspaceId, raw.workspaceId),
3840
+ eq(schema.interactionInterventions.id, current.id),
3841
+ eq(schema.interactionInterventions.version, current.version),
3842
+ eq(schema.interactionInterventions.status, "open"),
3843
+ sql`${schema.interactionInterventions.originatingToolCallId} is null`,
3844
+ ),
3845
+ )
3846
+ .returning()
3847
+ )[0];
3848
+ if (!row) throw new InteractionResourceConflictError("Intervention wait lost its fence");
3849
+ return interventionFromRow(row);
3850
+ }
3851
+
3852
+ await assertActiveInteractionResource(db, {
3853
+ workspaceId: raw.workspaceId,
3854
+ resourceKind: input.resourceKind,
3855
+ resourceId: input.resourceId,
3856
+ expectedControllerGeneration: input.expectedControllerGeneration,
3857
+ });
3858
+ let authRun: AuthRunRow | null = null;
3859
+ if (input.authRunId) {
3860
+ if (input.resourceKind !== "browser_session") {
3861
+ throw new InteractionResourceStateError("Only browser interventions can belong to auth runs");
3862
+ }
3863
+ authRun = await loadAuthRunRow(db, raw.workspaceId, input.authRunId);
3864
+ if (!authRun) throw new InteractionResourceNotFoundError("Auth run not found");
3865
+ if (
3866
+ authRun.browserSessionId !== input.resourceId ||
3867
+ authRun.targetId !== input.targetId ||
3868
+ authRun.controllerGeneration !== input.expectedControllerGeneration ||
3869
+ authRun.targetGeneration !== input.expectedTargetGeneration ||
3870
+ authRun.documentGeneration !== input.expectedDocumentGeneration ||
3871
+ authRun.settledAt
3872
+ ) {
3873
+ throw new InteractionResourceConflictError(
3874
+ "Interaction intervention does not match the active auth run",
3875
+ );
3876
+ }
3877
+ }
3878
+ const [existing] = await db
3879
+ .select()
3880
+ .from(schema.interactionInterventions)
3881
+ .where(
3882
+ and(
3883
+ eq(schema.interactionInterventions.workspaceId, raw.workspaceId),
3884
+ eq(schema.interactionInterventions.id, raw.id),
3885
+ ),
3886
+ )
3887
+ .for("update")
3888
+ .limit(1);
3889
+ if (existing) {
3890
+ if (
3891
+ existing.operationId !== raw.operationId ||
3892
+ existing.originatingSessionId !== raw.originatingSessionId ||
3893
+ existing.originatingTurnId !== raw.originatingTurnId ||
3894
+ existing.originatingToolCallId !== raw.toolCallId ||
3895
+ existing.resourceKind !== input.resourceKind ||
3896
+ existing.resourceId !== input.resourceId ||
3897
+ existing.targetId !== input.targetId ||
3898
+ existing.controllerGeneration !== input.expectedControllerGeneration ||
3899
+ existing.targetGeneration !== input.expectedTargetGeneration ||
3900
+ existing.documentGeneration !== input.expectedDocumentGeneration ||
3901
+ existing.kind !== input.kind ||
3902
+ existing.reason !== input.reason ||
3903
+ existing.authRunId !== (input.authRunId ?? null) ||
3904
+ existing.status !== "open"
3905
+ ) {
3906
+ throw new InteractionResourceConflictError("Interaction interruption replay changed");
3907
+ }
3908
+ return interventionFromRow(existing);
3909
+ }
3910
+ const [row] = await db
3911
+ .insert(schema.interactionInterventions)
3912
+ .values({
3913
+ id: raw.id,
3914
+ accountId: raw.accountId,
3915
+ workspaceId: raw.workspaceId,
3916
+ resourceKind: input.resourceKind,
3917
+ resourceId: input.resourceId,
3918
+ targetId: input.targetId,
3919
+ controllerGeneration: input.expectedControllerGeneration,
3920
+ targetGeneration: input.expectedTargetGeneration,
3921
+ documentGeneration: input.expectedDocumentGeneration,
3922
+ kind: input.kind,
3923
+ reason: input.reason,
3924
+ authRunId: input.authRunId ?? null,
3925
+ originatingSessionId: raw.originatingSessionId,
3926
+ originatingTurnId: raw.originatingTurnId,
3927
+ originatingAttemptId: raw.originatingAttemptId,
3928
+ originatingToolOperationId: raw.operationId,
3929
+ originatingToolCallId: raw.toolCallId,
3930
+ operationId: raw.operationId,
3931
+ expiresAt: sql`now() + (${input.expiresInSeconds} * interval '1 second')`,
3932
+ })
3933
+ .returning();
3934
+ if (!row) throw new Error("Interaction interruption insert returned no row");
3935
+ if (authRun) {
3936
+ const linked = await db
3937
+ .update(schema.authRuns)
3938
+ .set({
3939
+ interventionId: row.id,
3940
+ version: authRun.version + 1,
3941
+ updatedAt: sql`now()`,
3942
+ })
3943
+ .where(
3944
+ and(
3945
+ eq(schema.authRuns.workspaceId, raw.workspaceId),
3946
+ eq(schema.authRuns.id, authRun.id),
3947
+ eq(schema.authRuns.version, authRun.version),
3948
+ sql`${schema.authRuns.settledAt} is null`,
3949
+ ),
3950
+ )
3951
+ .returning({ id: schema.authRuns.id });
3952
+ if (linked.length !== 1) {
3953
+ throw new InteractionResourceConflictError("Auth run changed before intervention link");
3954
+ }
3955
+ }
3956
+ await advanceWorkspaceInteractionRevision(db, raw.accountId, raw.workspaceId);
3957
+ return interventionFromRow(row);
3958
+ }
3959
+
3960
+ export async function getInteractionInterventionApprovalTarget(
3961
+ db: Database,
3962
+ input: { accountId: string; workspaceId: string; interventionId: string },
3963
+ ): Promise<{ sessionId: string; toolCallId: string } | null> {
3964
+ return await withRlsContext(db, input, async (scopedDb) => {
3965
+ const row = await loadInterventionRow(scopedDb, input.workspaceId, input.interventionId);
3966
+ return row?.originatingToolCallId
3967
+ ? {
3968
+ sessionId: row.originatingSessionId,
3969
+ toolCallId: row.originatingToolCallId,
3970
+ }
3971
+ : null;
3972
+ });
3973
+ }
3974
+
3975
+ export async function getInteractionInterventionResumeForEvent(
3976
+ db: Database,
3977
+ workspaceId: string,
3978
+ sessionId: string,
3979
+ event: Pick<SessionEvent, "type" | "payload">,
3980
+ ): Promise<{
3981
+ toolCallId: string;
3982
+ intervention: InteractionInterventionValue;
3983
+ } | null> {
3984
+ if (event.type !== "user.approvalDecision") return null;
3985
+ const payload = event.payload as { approvalId?: unknown; decision?: unknown };
3986
+ if (typeof payload.approvalId !== "string" || payload.decision !== "approve") return null;
3987
+ const [row] = await db
3988
+ .select()
3989
+ .from(schema.interactionInterventions)
3990
+ .where(
3991
+ and(
3992
+ eq(schema.interactionInterventions.workspaceId, workspaceId),
3993
+ eq(schema.interactionInterventions.originatingSessionId, sessionId),
3994
+ eq(schema.interactionInterventions.originatingToolCallId, payload.approvalId),
3995
+ ),
3996
+ )
3997
+ .orderBy(asc(schema.interactionInterventions.createdAt))
3998
+ .limit(1);
3999
+ if (!row || row.status !== "completed") return null;
4000
+ return {
4001
+ toolCallId: payload.approvalId,
4002
+ intervention: interventionFromRow(row),
4003
+ };
4004
+ }
4005
+
4006
+ export async function createInteractionIntervention(
4007
+ db: Database,
4008
+ input: InteractionMutationScope &
4009
+ CreateInteractionInterventionRequestValue & {
4010
+ originatingSessionId: string;
4011
+ originatingTurnId?: string | null;
4012
+ originatingAttemptId?: string | null;
4013
+ originatingToolOperationId?: string | null;
4014
+ },
4015
+ ): Promise<InteractionInterventionMutationResponseValue> {
4016
+ const request = CreateInteractionInterventionRequest.parse({
4017
+ operationId: input.operationId,
4018
+ resourceKind: input.resourceKind,
4019
+ resourceId: input.resourceId,
4020
+ targetId: input.targetId,
4021
+ expectedControllerGeneration: input.expectedControllerGeneration,
4022
+ expectedTargetGeneration: input.expectedTargetGeneration,
4023
+ expectedDocumentGeneration: input.expectedDocumentGeneration,
4024
+ kind: input.kind,
4025
+ reason: input.reason,
4026
+ ...(input.authRunId !== undefined ? { authRunId: input.authRunId } : {}),
4027
+ expiresInSeconds: input.expiresInSeconds,
4028
+ });
4029
+ const { operationId: _operationId, ...digestRequest } = request;
4030
+ const digest = operationDigest({
4031
+ version: 1,
4032
+ request: digestRequest,
4033
+ originatingSessionId: input.originatingSessionId,
4034
+ originatingTurnId: input.originatingTurnId ?? null,
4035
+ originatingAttemptId: input.originatingAttemptId ?? null,
4036
+ originatingToolOperationId: input.originatingToolOperationId ?? null,
4037
+ actorSubjectId: input.actorSubjectId,
4038
+ });
4039
+ try {
4040
+ return await withRlsContext(db, input, async (scopedDb) => {
4041
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
4042
+ await lockOperation(scopedDb, request.operationId);
4043
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
4044
+ if (existing) {
4045
+ assertOperation(existing, {
4046
+ resourceKind: "intervention",
4047
+ kind: "create",
4048
+ requestDigest: digest,
4049
+ actorSubjectId: input.actorSubjectId,
4050
+ });
4051
+ const response = InteractionInterventionMutationResponse.parse(existing.result);
4052
+ return { ...response, replayed: true };
4053
+ }
4054
+ await expireInterventionsInScope(scopedDb, input);
4055
+ await assertActiveInteractionResource(scopedDb, {
4056
+ workspaceId: input.workspaceId,
4057
+ resourceKind: request.resourceKind,
4058
+ resourceId: request.resourceId,
4059
+ expectedControllerGeneration: request.expectedControllerGeneration,
4060
+ });
4061
+ let authRun: AuthRunRow | null = null;
4062
+ if (request.authRunId) {
4063
+ if (request.resourceKind !== "browser_session") {
4064
+ throw new InteractionResourceStateError(
4065
+ "Only browser interventions can belong to an auth run",
4066
+ );
4067
+ }
4068
+ authRun = await loadAuthRunRow(scopedDb, input.workspaceId, request.authRunId);
4069
+ if (!authRun) throw new InteractionResourceNotFoundError("Auth run not found");
4070
+ if (
4071
+ authRun.browserSessionId !== request.resourceId ||
4072
+ authRun.targetId !== request.targetId ||
4073
+ authRun.controllerGeneration !== request.expectedControllerGeneration ||
4074
+ authRun.targetGeneration !== request.expectedTargetGeneration ||
4075
+ authRun.documentGeneration !== request.expectedDocumentGeneration
4076
+ ) {
4077
+ throw new InteractionResourceConflictError(
4078
+ "Intervention does not match the exact auth-run target",
4079
+ );
4080
+ }
4081
+ if (authRun.settledAt) {
4082
+ throw new InteractionResourceStateError("Auth run is already settled");
4083
+ }
4084
+ }
4085
+ const id = randomUUID();
4086
+ const [row] = await scopedDb
4087
+ .insert(schema.interactionInterventions)
4088
+ .values({
4089
+ id,
4090
+ accountId: input.accountId,
4091
+ workspaceId: input.workspaceId,
4092
+ resourceKind: request.resourceKind,
4093
+ resourceId: request.resourceId,
4094
+ targetId: request.targetId,
4095
+ controllerGeneration: request.expectedControllerGeneration,
4096
+ targetGeneration: request.expectedTargetGeneration,
4097
+ documentGeneration: request.expectedDocumentGeneration,
4098
+ kind: request.kind,
4099
+ reason: request.reason,
4100
+ authRunId: request.authRunId ?? null,
4101
+ originatingSessionId: input.originatingSessionId,
4102
+ originatingTurnId: input.originatingTurnId ?? null,
4103
+ originatingAttemptId: input.originatingAttemptId ?? null,
4104
+ originatingToolOperationId: input.originatingToolOperationId ?? null,
4105
+ operationId: request.operationId,
4106
+ expiresAt: sql`now() + (${request.expiresInSeconds} * interval '1 second')`,
4107
+ })
4108
+ .returning();
4109
+ if (!row) throw new Error("Interaction intervention insert returned no row");
4110
+ if (authRun) {
4111
+ const linked = await scopedDb
4112
+ .update(schema.authRuns)
4113
+ .set({
4114
+ interventionId: id,
4115
+ version: authRun.version + 1,
4116
+ updatedAt: sql`now()`,
4117
+ })
4118
+ .where(
4119
+ and(
4120
+ eq(schema.authRuns.workspaceId, input.workspaceId),
4121
+ eq(schema.authRuns.id, authRun.id),
4122
+ eq(schema.authRuns.version, authRun.version),
4123
+ sql`${schema.authRuns.settledAt} is null`,
4124
+ ),
4125
+ )
4126
+ .returning({ id: schema.authRuns.id });
4127
+ if (linked.length !== 1) {
4128
+ throw new InteractionResourceConflictError("Auth run changed before intervention link");
4129
+ }
4130
+ }
4131
+ const response = InteractionInterventionMutationResponse.parse({
4132
+ intervention: interventionFromRow(row),
4133
+ operationId: request.operationId,
4134
+ replayed: false,
4135
+ });
4136
+ await insertCompletedOperation(scopedDb, {
4137
+ operationId: request.operationId,
4138
+ accountId: input.accountId,
4139
+ workspaceId: input.workspaceId,
4140
+ resourceKind: "intervention",
4141
+ resourceId: id,
4142
+ kind: "create",
4143
+ requestDigest: digest,
4144
+ resultVersion: row.version,
4145
+ result: response,
4146
+ actorSubjectId: input.actorSubjectId,
4147
+ });
4148
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
4149
+ return response;
4150
+ });
4151
+ } catch (error) {
4152
+ if (
4153
+ postgresConstraint(error) === "interaction_interventions_open_target_kind_uq" ||
4154
+ postgresConstraint(error) === "interaction_interventions_open_auth_run_uq"
4155
+ ) {
4156
+ throw new InteractionResourceConflictError(
4157
+ "A matching interaction intervention is already open",
4158
+ );
4159
+ }
4160
+ throw error;
4161
+ }
4162
+ }
4163
+
4164
+ export async function resolveInteractionInterventionInTransaction(
4165
+ db: Database,
4166
+ input: InteractionMutationScope & {
4167
+ interventionId: string;
4168
+ } & ResolveInteractionInterventionRequestValue,
4169
+ ): Promise<InteractionInterventionMutationResponseValue> {
4170
+ const request = ResolveInteractionInterventionRequest.parse({
4171
+ operationId: input.operationId,
4172
+ expectedVersion: input.expectedVersion,
4173
+ outcome: input.outcome,
4174
+ });
4175
+ const { operationId: _operationId, ...digestRequest } = request;
4176
+ const digest = operationDigest({
4177
+ version: 1,
4178
+ interventionId: input.interventionId,
4179
+ request: digestRequest,
4180
+ actorSubjectId: input.actorSubjectId,
4181
+ });
4182
+ const scopedDb = db;
4183
+ await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
4184
+ await lockOperation(scopedDb, request.operationId);
4185
+ const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
4186
+ if (existing) {
4187
+ assertOperation(existing, {
4188
+ resourceKind: "intervention",
4189
+ resourceId: input.interventionId,
4190
+ kind: "resolve",
4191
+ requestDigest: digest,
4192
+ actorSubjectId: input.actorSubjectId,
4193
+ });
4194
+ const response = InteractionInterventionMutationResponse.parse(existing.result);
4195
+ return { ...response, replayed: true };
4196
+ }
4197
+ const [current] = await scopedDb
4198
+ .select()
4199
+ .from(schema.interactionInterventions)
4200
+ .where(
4201
+ and(
4202
+ eq(schema.interactionInterventions.workspaceId, input.workspaceId),
4203
+ eq(schema.interactionInterventions.id, input.interventionId),
4204
+ ),
4205
+ )
4206
+ .for("update")
4207
+ .limit(1);
4208
+ if (!current) {
4209
+ throw new InteractionResourceNotFoundError("Interaction intervention not found");
4210
+ }
4211
+ if (current.version !== request.expectedVersion) {
4212
+ throw new InteractionResourceConflictError(
4213
+ "Interaction intervention changed before this response",
4214
+ );
4215
+ }
4216
+ if (current.status !== "open") {
4217
+ throw new InteractionResourceStateError("Interaction intervention is already settled");
4218
+ }
4219
+ const expired = current.expiresAt.getTime() <= Date.now();
4220
+ const status = expired ? "expired" : request.outcome;
4221
+ const [row] = await scopedDb
4222
+ .update(schema.interactionInterventions)
4223
+ .set({
4224
+ status,
4225
+ responseActorSubjectId: expired ? null : input.actorSubjectId,
4226
+ version: current.version + 1,
4227
+ updatedAt: sql`now()`,
4228
+ settledAt: sql`now()`,
4229
+ })
4230
+ .where(
4231
+ and(
4232
+ eq(schema.interactionInterventions.workspaceId, input.workspaceId),
4233
+ eq(schema.interactionInterventions.id, input.interventionId),
4234
+ eq(schema.interactionInterventions.version, request.expectedVersion),
4235
+ eq(schema.interactionInterventions.status, "open"),
4236
+ ),
4237
+ )
4238
+ .returning();
4239
+ if (!row) {
4240
+ throw new InteractionResourceConflictError("Intervention response lost its fence");
4241
+ }
4242
+ await settleLinkedAuthRun(scopedDb, {
4243
+ workspaceId: input.workspaceId,
4244
+ authRunId: row.authRunId,
4245
+ interventionId: row.id,
4246
+ outcome: status,
4247
+ });
4248
+ const response = InteractionInterventionMutationResponse.parse({
4249
+ intervention: interventionFromRow(row),
4250
+ operationId: request.operationId,
4251
+ replayed: false,
4252
+ });
4253
+ await insertCompletedOperation(scopedDb, {
4254
+ operationId: request.operationId,
4255
+ accountId: input.accountId,
4256
+ workspaceId: input.workspaceId,
4257
+ resourceKind: "intervention",
4258
+ resourceId: input.interventionId,
4259
+ kind: "resolve",
4260
+ requestDigest: digest,
4261
+ resultVersion: row.version,
4262
+ result: response,
4263
+ actorSubjectId: input.actorSubjectId,
4264
+ });
4265
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
4266
+ return response;
4267
+ }
4268
+
4269
+ export async function resolveInteractionIntervention(
4270
+ db: Database,
4271
+ input: InteractionMutationScope & {
4272
+ interventionId: string;
4273
+ } & ResolveInteractionInterventionRequestValue,
4274
+ ): Promise<InteractionInterventionMutationResponseValue> {
4275
+ return await withRlsContext(
4276
+ db,
4277
+ input,
4278
+ async (scopedDb) => await resolveInteractionInterventionInTransaction(scopedDb, input),
4279
+ );
4280
+ }
4281
+
4282
+ /**
4283
+ * Cancel every still-open intervention created by a logical agent turn when
4284
+ * that turn becomes terminal. The caller already owns the turn-settlement
4285
+ * transaction, so no resource can outlive the terminal session truth.
4286
+ */
4287
+ export async function cancelTurnInteractionInterventionsInTransaction(
4288
+ db: Database,
4289
+ input: {
4290
+ accountId: string;
4291
+ workspaceId: string;
4292
+ sessionId: string;
4293
+ turnId: string;
4294
+ },
4295
+ ): Promise<number> {
4296
+ const cancelled = await db
4297
+ .update(schema.interactionInterventions)
4298
+ .set({
4299
+ status: "cancelled",
4300
+ version: sql`${schema.interactionInterventions.version} + 1`,
4301
+ updatedAt: sql`now()`,
4302
+ settledAt: sql`now()`,
4303
+ })
4304
+ .where(
4305
+ and(
4306
+ eq(schema.interactionInterventions.workspaceId, input.workspaceId),
4307
+ eq(schema.interactionInterventions.originatingSessionId, input.sessionId),
4308
+ eq(schema.interactionInterventions.originatingTurnId, input.turnId),
4309
+ eq(schema.interactionInterventions.status, "open"),
4310
+ ),
4311
+ )
4312
+ .returning({
4313
+ id: schema.interactionInterventions.id,
4314
+ authRunId: schema.interactionInterventions.authRunId,
4315
+ });
4316
+ for (const intervention of cancelled) {
4317
+ await settleLinkedAuthRun(db, {
4318
+ workspaceId: input.workspaceId,
4319
+ authRunId: intervention.authRunId,
4320
+ interventionId: intervention.id,
4321
+ outcome: "cancelled",
4322
+ });
4323
+ }
4324
+ if (cancelled.length > 0) {
4325
+ await advanceWorkspaceInteractionRevision(db, input.accountId, input.workspaceId);
4326
+ }
4327
+ return cancelled.length;
4328
+ }
4329
+
4330
+ export async function cancelOpenInteractionInterventions(
4331
+ db: Database,
4332
+ input: {
4333
+ accountId: string;
4334
+ workspaceId: string;
4335
+ resourceKind: "browser_session" | "computer_session";
4336
+ resourceId: string;
4337
+ },
4338
+ ): Promise<number> {
4339
+ return await withRlsContext(db, input, async (scopedDb) => {
4340
+ const cancelled = await scopedDb
4341
+ .update(schema.interactionInterventions)
4342
+ .set({
4343
+ status: "cancelled",
4344
+ version: sql`${schema.interactionInterventions.version} + 1`,
4345
+ updatedAt: sql`now()`,
4346
+ settledAt: sql`now()`,
4347
+ })
4348
+ .where(
4349
+ and(
4350
+ eq(schema.interactionInterventions.workspaceId, input.workspaceId),
4351
+ eq(schema.interactionInterventions.resourceKind, input.resourceKind),
4352
+ eq(schema.interactionInterventions.resourceId, input.resourceId),
4353
+ eq(schema.interactionInterventions.status, "open"),
4354
+ ),
4355
+ )
4356
+ .returning({
4357
+ id: schema.interactionInterventions.id,
4358
+ authRunId: schema.interactionInterventions.authRunId,
4359
+ });
4360
+ for (const intervention of cancelled) {
4361
+ await settleLinkedAuthRun(scopedDb, {
4362
+ workspaceId: input.workspaceId,
4363
+ authRunId: intervention.authRunId,
4364
+ interventionId: intervention.id,
4365
+ outcome: "cancelled",
4366
+ });
4367
+ }
4368
+ if (cancelled.length > 0) {
4369
+ await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
4370
+ }
4371
+ return cancelled.length;
4372
+ });
4373
+ }