@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,517 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ BrowserDownload,
4
+ BrowserDownloadSaveRequest,
5
+ BrowserDownloadSaveResponse,
6
+ type BrowserDownload as BrowserDownloadValue,
7
+ type BrowserDownloadSaveRequest as BrowserDownloadSaveRequestValue,
8
+ type BrowserDownloadSaveResponse as BrowserDownloadSaveResponseValue,
9
+ } from "@opengeni/contracts";
10
+ import { and, eq, sql } from "drizzle-orm";
11
+ import { type Database, withRlsContext, withWorkspaceRls } from "./database";
12
+ import {
13
+ InteractionResourceConflictError,
14
+ InteractionResourceNotFoundError,
15
+ InteractionResourceStateError,
16
+ } from "./browser-auth";
17
+ import * as schema from "./schema";
18
+
19
+ type OperationRow = typeof schema.interactionResourceOperations.$inferSelect;
20
+
21
+ type SaveMetadata = {
22
+ browserSessionId: string;
23
+ controllerGeneration: string;
24
+ sourceSessionId: string;
25
+ destinationPath: string;
26
+ overwrite: boolean;
27
+ fileId: string;
28
+ uploadId: string;
29
+ objectKey: string;
30
+ safeFilename: string;
31
+ contentType: string;
32
+ download: BrowserDownloadValue;
33
+ };
34
+
35
+ export type BrowserDownloadSavePreparation = SaveMetadata & {
36
+ operationId: string;
37
+ state: OperationRow["state"];
38
+ errorCode: string | null;
39
+ replayed: boolean;
40
+ dispatchedNow: boolean;
41
+ response: BrowserDownloadSaveResponseValue | null;
42
+ };
43
+
44
+ type SaveIdentity = {
45
+ accountId: string;
46
+ workspaceId: string;
47
+ actorSubjectId: string;
48
+ browserSessionId: string;
49
+ downloadId: string;
50
+ request: BrowserDownloadSaveRequestValue;
51
+ };
52
+
53
+ export async function findBrowserDownloadSave(
54
+ db: Database,
55
+ inputValue: SaveIdentity,
56
+ ): Promise<BrowserDownloadSavePreparation | null> {
57
+ const input = parseIdentity(inputValue);
58
+ return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
59
+ const [operation] = await scopedDb
60
+ .select()
61
+ .from(schema.interactionResourceOperations)
62
+ .where(
63
+ and(
64
+ eq(schema.interactionResourceOperations.workspaceId, input.workspaceId),
65
+ eq(schema.interactionResourceOperations.operationId, input.request.operationId),
66
+ ),
67
+ )
68
+ .limit(1);
69
+ if (!operation) return null;
70
+ return preparation(operation, input, true);
71
+ });
72
+ }
73
+
74
+ export async function prepareBrowserDownloadSave(
75
+ db: Database,
76
+ inputValue: SaveIdentity & {
77
+ sourceSessionId: string;
78
+ download: BrowserDownloadValue;
79
+ fileId: string;
80
+ safeFilename: string;
81
+ contentType: string;
82
+ bucket: string;
83
+ objectKey: string;
84
+ uploadExpiresAt: Date;
85
+ },
86
+ ): Promise<BrowserDownloadSavePreparation> {
87
+ const input = parseIdentity(inputValue);
88
+ const download = BrowserDownload.parse(inputValue.download);
89
+ if (
90
+ download.id !== input.downloadId ||
91
+ download.browserSessionId !== input.browserSessionId ||
92
+ download.status !== "completed" ||
93
+ !download.sha256
94
+ ) {
95
+ throw new InteractionResourceStateError("Browser download is not ready to save");
96
+ }
97
+ const sourceSessionId = requireUuid(inputValue.sourceSessionId, "sourceSessionId");
98
+ const fileId = requireUuid(inputValue.fileId, "fileId");
99
+ const safeFilename = requireBoundedString(inputValue.safeFilename, "safeFilename", 4_096);
100
+ const contentType = requireBoundedString(inputValue.contentType, "contentType", 512);
101
+ const bucket = requireBoundedString(inputValue.bucket, "bucket", 1_024);
102
+ const objectKey = requireBoundedString(inputValue.objectKey, "objectKey", 2_048);
103
+ const expectedObjectKey = `workspaces/${input.workspaceId}/files/${fileId}/original/${safeFilename}`;
104
+ if (objectKey !== expectedObjectKey) {
105
+ throw new InteractionResourceConflictError("Browser download object key is not canonical");
106
+ }
107
+ if (!Number.isFinite(inputValue.uploadExpiresAt.valueOf())) {
108
+ throw new InteractionResourceStateError("Browser download upload expiry is invalid");
109
+ }
110
+
111
+ return await withRlsContext(
112
+ db,
113
+ input,
114
+ async (scopedDb) =>
115
+ await scopedDb.transaction(async (tx) => {
116
+ await lockOperation(tx, input.request.operationId);
117
+ const [existing] = await tx
118
+ .select()
119
+ .from(schema.interactionResourceOperations)
120
+ .where(
121
+ and(
122
+ eq(schema.interactionResourceOperations.workspaceId, input.workspaceId),
123
+ eq(schema.interactionResourceOperations.operationId, input.request.operationId),
124
+ ),
125
+ )
126
+ .for("update")
127
+ .limit(1);
128
+ if (existing) {
129
+ const replay = preparation(existing, input, true);
130
+ if (replay.state === "prepared" || replay.state === "dispatched") {
131
+ await tx
132
+ .update(schema.fileUploads)
133
+ .set({ expiresAt: inputValue.uploadExpiresAt, updatedAt: sql`now()` })
134
+ .where(
135
+ and(
136
+ eq(schema.fileUploads.workspaceId, input.workspaceId),
137
+ eq(schema.fileUploads.id, replay.uploadId),
138
+ eq(schema.fileUploads.status, "pending"),
139
+ ),
140
+ );
141
+ }
142
+ return { ...replay, dispatchedNow: false };
143
+ }
144
+
145
+ const [session] = await tx
146
+ .select({
147
+ lifecycle: schema.browserSessions.lifecycle,
148
+ controllerGeneration: schema.browserSessions.controllerGeneration,
149
+ })
150
+ .from(schema.browserSessions)
151
+ .where(
152
+ and(
153
+ eq(schema.browserSessions.accountId, input.accountId),
154
+ eq(schema.browserSessions.workspaceId, input.workspaceId),
155
+ eq(schema.browserSessions.id, input.browserSessionId),
156
+ ),
157
+ )
158
+ .limit(1);
159
+ if (!session) throw new InteractionResourceNotFoundError("BrowserSession not found");
160
+ if (
161
+ session.lifecycle !== "active" ||
162
+ session.controllerGeneration !== download.controllerGeneration
163
+ ) {
164
+ throw new InteractionResourceStateError(
165
+ "Browser download controller is no longer active",
166
+ );
167
+ }
168
+ const [association] = await tx
169
+ .select({ id: schema.browserSessionAssociations.id })
170
+ .from(schema.browserSessionAssociations)
171
+ .where(
172
+ and(
173
+ eq(schema.browserSessionAssociations.workspaceId, input.workspaceId),
174
+ eq(schema.browserSessionAssociations.browserSessionId, input.browserSessionId),
175
+ eq(schema.browserSessionAssociations.sessionId, sourceSessionId),
176
+ eq(schema.browserSessionAssociations.relationship, "created"),
177
+ ),
178
+ )
179
+ .limit(1);
180
+ if (!association) {
181
+ throw new InteractionResourceConflictError(
182
+ "Browser download source session lost its creation binding",
183
+ );
184
+ }
185
+
186
+ await tx.insert(schema.files).values({
187
+ id: fileId,
188
+ accountId: input.accountId,
189
+ workspaceId: input.workspaceId,
190
+ status: "pending_upload",
191
+ filename: download.filename,
192
+ safeFilename,
193
+ contentType,
194
+ sizeBytes: download.receivedBytes,
195
+ sha256: download.sha256,
196
+ bucket,
197
+ objectKey,
198
+ });
199
+ const [upload] = await tx
200
+ .insert(schema.fileUploads)
201
+ .values({
202
+ accountId: input.accountId,
203
+ workspaceId: input.workspaceId,
204
+ fileId,
205
+ status: "pending",
206
+ expiresAt: inputValue.uploadExpiresAt,
207
+ })
208
+ .returning({ id: schema.fileUploads.id });
209
+ if (!upload) throw new Error("Browser download upload insert returned no row");
210
+ const metadata: SaveMetadata = {
211
+ browserSessionId: input.browserSessionId,
212
+ controllerGeneration: download.controllerGeneration,
213
+ sourceSessionId,
214
+ destinationPath: input.request.destinationPath,
215
+ overwrite: input.request.overwrite,
216
+ fileId,
217
+ uploadId: upload.id,
218
+ objectKey,
219
+ safeFilename,
220
+ contentType,
221
+ download,
222
+ };
223
+ const [operation] = await tx
224
+ .insert(schema.interactionResourceOperations)
225
+ .values({
226
+ operationId: input.request.operationId,
227
+ accountId: input.accountId,
228
+ workspaceId: input.workspaceId,
229
+ resourceKind: "browser_download",
230
+ resourceId: input.downloadId,
231
+ kind: "save",
232
+ requestDigest: requestDigest(input),
233
+ metadata,
234
+ state: "prepared",
235
+ actorSubjectId: input.actorSubjectId,
236
+ })
237
+ .returning();
238
+ if (!operation) throw new Error("Browser download save insert returned no row");
239
+ return { ...preparation(operation, input, false), dispatchedNow: false };
240
+ }),
241
+ );
242
+ }
243
+
244
+ export async function dispatchBrowserDownloadSave(
245
+ db: Database,
246
+ inputValue: SaveIdentity,
247
+ ): Promise<BrowserDownloadSavePreparation> {
248
+ const input = parseIdentity(inputValue);
249
+ return await withRlsContext(
250
+ db,
251
+ input,
252
+ async (scopedDb) =>
253
+ await scopedDb.transaction(async (tx) => {
254
+ await lockOperation(tx, input.request.operationId);
255
+ const operation = await requireOperation(tx, input.workspaceId, input.request.operationId);
256
+ const current = preparation(operation, input, true);
257
+ if (current.state === "prepared") {
258
+ const [updated] = await tx
259
+ .update(schema.interactionResourceOperations)
260
+ .set({ state: "dispatched" })
261
+ .where(
262
+ and(
263
+ eq(schema.interactionResourceOperations.operationId, input.request.operationId),
264
+ eq(schema.interactionResourceOperations.state, "prepared"),
265
+ ),
266
+ )
267
+ .returning();
268
+ if (!updated) throw new InteractionResourceConflictError("Save dispatch lost its fence");
269
+ return { ...preparation(updated, input, current.replayed), dispatchedNow: true };
270
+ }
271
+ return { ...current, dispatchedNow: false };
272
+ }),
273
+ );
274
+ }
275
+
276
+ export async function completeBrowserDownloadSave(
277
+ db: Database,
278
+ inputValue: SaveIdentity,
279
+ ): Promise<BrowserDownloadSaveResponseValue> {
280
+ const input = parseIdentity(inputValue);
281
+ return await withRlsContext(
282
+ db,
283
+ input,
284
+ async (scopedDb) =>
285
+ await scopedDb.transaction(async (tx) => {
286
+ await lockOperation(tx, input.request.operationId);
287
+ const operation = await requireOperation(tx, input.workspaceId, input.request.operationId);
288
+ const current = preparation(operation, input, true);
289
+ if (current.response) return { ...current.response, replayed: true };
290
+ if (current.state !== "dispatched") {
291
+ throw terminalStateError(current);
292
+ }
293
+ const response = BrowserDownloadSaveResponse.parse({
294
+ download: current.download,
295
+ destinationPath: current.destinationPath,
296
+ fileId: current.fileId,
297
+ operationId: input.request.operationId,
298
+ replayed: false,
299
+ });
300
+ const [completed] = await tx
301
+ .update(schema.interactionResourceOperations)
302
+ .set({
303
+ state: "completed",
304
+ resultVersion: current.download.version,
305
+ result: response,
306
+ settledAt: sql`now()`,
307
+ })
308
+ .where(
309
+ and(
310
+ eq(schema.interactionResourceOperations.operationId, input.request.operationId),
311
+ eq(schema.interactionResourceOperations.workspaceId, input.workspaceId),
312
+ eq(schema.interactionResourceOperations.state, "dispatched"),
313
+ ),
314
+ )
315
+ .returning({ operationId: schema.interactionResourceOperations.operationId });
316
+ if (!completed)
317
+ throw new InteractionResourceConflictError("Save completion lost its fence");
318
+ return response;
319
+ }),
320
+ );
321
+ }
322
+
323
+ export async function settleBrowserDownloadSaveFailure(
324
+ db: Database,
325
+ inputValue: SaveIdentity & {
326
+ state: "failed" | "outcome_unknown";
327
+ errorCode: string;
328
+ },
329
+ ): Promise<void> {
330
+ const input = parseIdentity(inputValue);
331
+ const errorCode = requireBoundedString(inputValue.errorCode, "errorCode", 512);
332
+ await withRlsContext(db, input, async (scopedDb) => {
333
+ await scopedDb.transaction(async (tx) => {
334
+ await lockOperation(tx, input.request.operationId);
335
+ const operation = await requireOperation(tx, input.workspaceId, input.request.operationId);
336
+ const current = preparation(operation, input, true);
337
+ if (
338
+ current.state === "completed" ||
339
+ current.state === "failed" ||
340
+ current.state === "outcome_unknown"
341
+ ) {
342
+ return;
343
+ }
344
+ const [settled] = await tx
345
+ .update(schema.interactionResourceOperations)
346
+ .set({ state: inputValue.state, errorCode, settledAt: sql`now()` })
347
+ .where(
348
+ and(
349
+ eq(schema.interactionResourceOperations.operationId, input.request.operationId),
350
+ eq(schema.interactionResourceOperations.workspaceId, input.workspaceId),
351
+ sql`${schema.interactionResourceOperations.state} in ('prepared', 'dispatched')`,
352
+ ),
353
+ )
354
+ .returning({ operationId: schema.interactionResourceOperations.operationId });
355
+ if (!settled) throw new InteractionResourceConflictError("Save failure lost its fence");
356
+ });
357
+ });
358
+ }
359
+
360
+ function parseIdentity(input: SaveIdentity): SaveIdentity {
361
+ const request = BrowserDownloadSaveRequest.parse(input.request);
362
+ return {
363
+ accountId: requireUuid(input.accountId, "accountId"),
364
+ workspaceId: requireUuid(input.workspaceId, "workspaceId"),
365
+ actorSubjectId: requireBoundedString(input.actorSubjectId, "actorSubjectId", 1_024),
366
+ browserSessionId: requireUuid(input.browserSessionId, "browserSessionId"),
367
+ downloadId: requireUuid(input.downloadId, "downloadId"),
368
+ request,
369
+ };
370
+ }
371
+
372
+ function requestDigest(input: SaveIdentity): string {
373
+ return createHash("sha256")
374
+ .update(
375
+ JSON.stringify({
376
+ browserSessionId: input.browserSessionId,
377
+ destinationPath: input.request.destinationPath,
378
+ downloadId: input.downloadId,
379
+ overwrite: input.request.overwrite,
380
+ }),
381
+ )
382
+ .digest("hex");
383
+ }
384
+
385
+ function preparation(
386
+ operation: OperationRow,
387
+ input: SaveIdentity,
388
+ replayed: boolean,
389
+ ): BrowserDownloadSavePreparation {
390
+ if (
391
+ operation.accountId !== input.accountId ||
392
+ operation.workspaceId !== input.workspaceId ||
393
+ operation.resourceKind !== "browser_download" ||
394
+ operation.resourceId !== input.downloadId ||
395
+ operation.kind !== "save" ||
396
+ operation.requestDigest !== requestDigest(input) ||
397
+ operation.actorSubjectId !== input.actorSubjectId
398
+ ) {
399
+ throw new InteractionResourceConflictError(
400
+ "Operation id is already bound to another browser download save",
401
+ );
402
+ }
403
+ const metadata = saveMetadata(operation.metadata);
404
+ if (
405
+ metadata.browserSessionId !== input.browserSessionId ||
406
+ metadata.download.id !== input.downloadId
407
+ ) {
408
+ throw new InteractionResourceConflictError("Browser download save metadata lost its binding");
409
+ }
410
+ let response: BrowserDownloadSaveResponseValue | null = null;
411
+ if (operation.state === "completed") {
412
+ response = BrowserDownloadSaveResponse.parse(operation.result);
413
+ if (
414
+ response.operationId !== operation.operationId ||
415
+ response.download.id !== input.downloadId ||
416
+ response.fileId !== metadata.fileId ||
417
+ response.destinationPath !== metadata.destinationPath
418
+ ) {
419
+ throw new InteractionResourceStateError("Browser download save result is corrupted");
420
+ }
421
+ response = { ...response, replayed: true };
422
+ }
423
+ return {
424
+ ...metadata,
425
+ operationId: operation.operationId,
426
+ state: operation.state,
427
+ errorCode: operation.errorCode,
428
+ replayed,
429
+ dispatchedNow: false,
430
+ response,
431
+ };
432
+ }
433
+
434
+ function saveMetadata(value: unknown): SaveMetadata {
435
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
436
+ throw new InteractionResourceStateError("Browser download save metadata is invalid");
437
+ }
438
+ const record = value as Record<string, unknown>;
439
+ return {
440
+ browserSessionId: requireUuid(record.browserSessionId, "metadata.browserSessionId"),
441
+ controllerGeneration: requireBoundedString(
442
+ record.controllerGeneration,
443
+ "metadata.controllerGeneration",
444
+ 256,
445
+ ),
446
+ sourceSessionId: requireUuid(record.sourceSessionId, "metadata.sourceSessionId"),
447
+ destinationPath: BrowserDownloadSaveRequest.shape.destinationPath.parse(record.destinationPath),
448
+ overwrite: requireBoolean(record.overwrite, "metadata.overwrite"),
449
+ fileId: requireUuid(record.fileId, "metadata.fileId"),
450
+ uploadId: requireUuid(record.uploadId, "metadata.uploadId"),
451
+ objectKey: requireBoundedString(record.objectKey, "metadata.objectKey", 2_048),
452
+ safeFilename: requireBoundedString(record.safeFilename, "metadata.safeFilename", 4_096),
453
+ contentType: requireBoundedString(record.contentType, "metadata.contentType", 512),
454
+ download: BrowserDownload.parse(record.download),
455
+ };
456
+ }
457
+
458
+ async function lockOperation(db: Database, operationId: string): Promise<void> {
459
+ await db.execute(
460
+ sql`select pg_advisory_xact_lock(hashtextextended(${`browser-download-save:${operationId}`}, 0))`,
461
+ );
462
+ }
463
+
464
+ async function requireOperation(
465
+ db: Database,
466
+ workspaceId: string,
467
+ operationId: string,
468
+ ): Promise<OperationRow> {
469
+ const [operation] = await db
470
+ .select()
471
+ .from(schema.interactionResourceOperations)
472
+ .where(
473
+ and(
474
+ eq(schema.interactionResourceOperations.workspaceId, workspaceId),
475
+ eq(schema.interactionResourceOperations.operationId, operationId),
476
+ ),
477
+ )
478
+ .for("update")
479
+ .limit(1);
480
+ if (!operation) throw new InteractionResourceNotFoundError("Browser download save not found");
481
+ return operation;
482
+ }
483
+
484
+ function terminalStateError(
485
+ prepared: BrowserDownloadSavePreparation,
486
+ ): InteractionResourceStateError {
487
+ return new InteractionResourceStateError(
488
+ `Browser download save is ${prepared.state.replace("_", " ")}${prepared.errorCode ? ` (${prepared.errorCode})` : ""}`,
489
+ );
490
+ }
491
+
492
+ function requireUuid(value: unknown, label: string): string {
493
+ if (
494
+ typeof value !== "string" ||
495
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(value)
496
+ ) {
497
+ throw new InteractionResourceStateError(`${label} is invalid`);
498
+ }
499
+ return value;
500
+ }
501
+
502
+ function requireBoundedString(value: unknown, label: string, maxBytes: number): string {
503
+ if (
504
+ typeof value !== "string" ||
505
+ value.length < 1 ||
506
+ Buffer.byteLength(value) > maxBytes ||
507
+ /[\u0000-\u001f\u007f]/u.test(value)
508
+ ) {
509
+ throw new InteractionResourceStateError(`${label} is invalid`);
510
+ }
511
+ return value;
512
+ }
513
+
514
+ function requireBoolean(value: unknown, label: string): boolean {
515
+ if (typeof value !== "boolean") throw new InteractionResourceStateError(`${label} is invalid`);
516
+ return value;
517
+ }