@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,1385 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ BrowserIdentity,
4
+ BrowserIdentityListResponse,
5
+ BrowserIdentityMutationResponse,
6
+ BrowserRevision,
7
+ BrowserRevisionListResponse,
8
+ BrowserRevisionMaterialization,
9
+ BrowserSessionCapabilities,
10
+ InteractionError,
11
+ PublishBrowserRevisionResponse,
12
+ UpdateBrowserIdentityRequest,
13
+ type BrowserIdentity as BrowserIdentityValue,
14
+ type BrowserIdentityListResponse as BrowserIdentityListResponseValue,
15
+ type BrowserIdentityMutationResponse as BrowserIdentityMutationResponseValue,
16
+ type BrowserRevision as BrowserRevisionValue,
17
+ type BrowserRevisionListResponse as BrowserRevisionListResponseValue,
18
+ type BrowserRevisionMaterialization as BrowserRevisionMaterializationValue,
19
+ type InteractionError as InteractionErrorValue,
20
+ type PublishBrowserRevisionResponse as PublishBrowserRevisionResponseValue,
21
+ type UpdateBrowserIdentityRequest as UpdateBrowserIdentityRequestValue,
22
+ } from "@opengeni/contracts";
23
+ import { and, asc, desc, eq, inArray, sql } from "drizzle-orm";
24
+ import { type Database, withRlsContext } from "./database";
25
+ import {
26
+ advanceWorkspaceInteractionRevision,
27
+ readWorkspaceInteractionRevision,
28
+ } from "./interaction-revisions";
29
+ import { safeDatabaseErrorFacts } from "./persistence-errors";
30
+ import {
31
+ commitBrowserStateUploadInTransaction,
32
+ markBrowserStateUploadsDeletePendingInTransaction,
33
+ prepareBrowserStateUploadInTransaction,
34
+ validateBrowserStateArtifactCommitInput,
35
+ type BrowserStateArtifactCommitInput,
36
+ } from "./browser-state-artifacts";
37
+ import * as schema from "./schema";
38
+
39
+ type BrowserIdentityRow = typeof schema.browserIdentities.$inferSelect;
40
+ type BrowserRevisionRow = typeof schema.browserRevisions.$inferSelect;
41
+ type BrowserRevisionComponentRow = typeof schema.browserRevisionComponents.$inferSelect;
42
+ type BrowserStateArtifactRow = typeof schema.browserStateArtifacts.$inferSelect;
43
+ type BrowserSessionRow = typeof schema.browserSessions.$inferSelect;
44
+ type InteractionOperationRow = typeof schema.interactionOperations.$inferSelect;
45
+ type InteractionResourceOperationRow = typeof schema.interactionResourceOperations.$inferSelect;
46
+
47
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
48
+ const MAX_COMPONENTS = 16;
49
+ const CONSISTENT_READ = {
50
+ isolationLevel: "repeatable read",
51
+ accessMode: "read only",
52
+ } as const;
53
+
54
+ export class BrowserIdentityNotFoundError extends Error {
55
+ readonly name = "BrowserIdentityNotFoundError";
56
+ }
57
+
58
+ export class BrowserIdentityConflictError extends Error {
59
+ readonly name = "BrowserIdentityConflictError";
60
+ }
61
+
62
+ export class BrowserIdentityStateError extends Error {
63
+ readonly name = "BrowserIdentityStateError";
64
+ }
65
+
66
+ export type BrowserRevisionPublicationPreparation =
67
+ | {
68
+ kind: "completed";
69
+ response: PublishBrowserRevisionResponseValue;
70
+ }
71
+ | {
72
+ kind: "pending";
73
+ operationState: "prepared" | "dispatched";
74
+ browserSessionId: string;
75
+ controllerGeneration: string;
76
+ identity: BrowserIdentityValue;
77
+ parentRevisionId: string | null;
78
+ };
79
+
80
+ export type BrowserRevisionPublicationDispatch =
81
+ | {
82
+ kind: "completed";
83
+ response: PublishBrowserRevisionResponseValue;
84
+ }
85
+ | {
86
+ kind: "dispatched";
87
+ browserSessionId: string;
88
+ controllerGeneration: string;
89
+ replayed: boolean;
90
+ };
91
+
92
+ export type BrowserRevisionArtifactAuthority = {
93
+ revision: BrowserRevisionValue;
94
+ artifacts: Array<{
95
+ componentId: string;
96
+ artifactId: string;
97
+ objectKey: string;
98
+ encryptedDataKey: string;
99
+ contentDigest: string;
100
+ manifestDigest: string;
101
+ artifactDigest: string;
102
+ sizeBytes: number;
103
+ format: string;
104
+ materialization: BrowserRevisionMaterializationValue;
105
+ }>;
106
+ };
107
+
108
+ function iso(value: Date): string {
109
+ return value.toISOString();
110
+ }
111
+
112
+ function safeInteger(value: number, label: string): number {
113
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label} is outside safe range`);
114
+ return value;
115
+ }
116
+
117
+ function identityFromRow(row: BrowserIdentityRow): BrowserIdentityValue {
118
+ return BrowserIdentity.parse({
119
+ id: row.id,
120
+ accountId: row.accountId,
121
+ workspaceId: row.workspaceId,
122
+ name: row.name,
123
+ status: row.status,
124
+ version: safeInteger(row.version, "BrowserIdentity version"),
125
+ defaultRevisionId: row.defaultRevisionId,
126
+ headGeneration: safeInteger(row.headGeneration, "BrowserIdentity head generation"),
127
+ revisionCount: safeInteger(row.revisionCount, "BrowserIdentity revision count"),
128
+ createdBySubjectId: row.createdBySubjectId,
129
+ createdAt: iso(row.createdAt),
130
+ updatedAt: iso(row.updatedAt),
131
+ });
132
+ }
133
+
134
+ function revisionFromRows(
135
+ row: BrowserRevisionRow,
136
+ components: readonly BrowserRevisionComponentRow[],
137
+ artifacts: ReadonlyMap<string, BrowserStateArtifactRow>,
138
+ ): BrowserRevisionValue {
139
+ return BrowserRevision.parse({
140
+ id: row.id,
141
+ accountId: row.accountId,
142
+ workspaceId: row.workspaceId,
143
+ identityId: row.identityId,
144
+ parentRevisionId: row.parentRevisionId,
145
+ ordinal: safeInteger(row.ordinal, "BrowserRevision ordinal"),
146
+ sourceBrowserSessionId: row.sourceBrowserSessionId,
147
+ manifestDigest: row.manifestDigest,
148
+ components: [...components]
149
+ .sort((left, right) => left.position - right.position)
150
+ .map((component) => {
151
+ const artifact = artifacts.get(component.artifactId);
152
+ if (
153
+ !artifact ||
154
+ artifact.state !== "available" ||
155
+ artifact.purpose !== component.artifactPurpose ||
156
+ artifact.kind !== component.kind ||
157
+ artifact.sourceBrowserSessionId !== component.sourceBrowserSessionId ||
158
+ component.sourceBrowserSessionId !== row.sourceBrowserSessionId
159
+ ) {
160
+ throw new Error("BrowserRevision references invalid or unavailable state authority");
161
+ }
162
+ return {
163
+ id: component.id,
164
+ kind: component.kind,
165
+ format: artifact.format,
166
+ artifactDigest: artifact.artifactDigest,
167
+ sizeBytes: safeInteger(artifact.sizeBytes, "browser artifact size"),
168
+ materialization: BrowserRevisionMaterialization.parse(artifact.materialization),
169
+ };
170
+ }),
171
+ createdBySubjectId: row.createdBySubjectId,
172
+ createdAt: iso(row.createdAt),
173
+ });
174
+ }
175
+
176
+ async function loadIdentity(
177
+ db: Database,
178
+ workspaceId: string,
179
+ identityId: string,
180
+ ): Promise<BrowserIdentityRow | null> {
181
+ const [row] = await db
182
+ .select()
183
+ .from(schema.browserIdentities)
184
+ .where(
185
+ and(
186
+ eq(schema.browserIdentities.workspaceId, workspaceId),
187
+ eq(schema.browserIdentities.id, identityId),
188
+ ),
189
+ )
190
+ .limit(1);
191
+ return row ?? null;
192
+ }
193
+
194
+ async function loadRevisionRows(
195
+ db: Database,
196
+ workspaceId: string,
197
+ filter: { identityId?: string; operationId?: string; revisionId?: string },
198
+ ): Promise<BrowserRevisionRow[]> {
199
+ const predicates = [eq(schema.browserRevisions.workspaceId, workspaceId)];
200
+ if (filter.identityId) predicates.push(eq(schema.browserRevisions.identityId, filter.identityId));
201
+ if (filter.operationId) {
202
+ predicates.push(eq(schema.browserRevisions.publicationOperationId, filter.operationId));
203
+ }
204
+ if (filter.revisionId) predicates.push(eq(schema.browserRevisions.id, filter.revisionId));
205
+ return await db
206
+ .select()
207
+ .from(schema.browserRevisions)
208
+ .where(and(...predicates))
209
+ .orderBy(desc(schema.browserRevisions.ordinal), desc(schema.browserRevisions.id));
210
+ }
211
+
212
+ async function materializeRevisions(
213
+ db: Database,
214
+ rows: readonly BrowserRevisionRow[],
215
+ ): Promise<BrowserRevisionValue[]> {
216
+ if (rows.length === 0) return [];
217
+ const revisionIds = rows.map((row) => row.id);
218
+ const components = await db
219
+ .select()
220
+ .from(schema.browserRevisionComponents)
221
+ .where(inArray(schema.browserRevisionComponents.revisionId, revisionIds))
222
+ .orderBy(asc(schema.browserRevisionComponents.position));
223
+ const artifactIds = components.map((component) => component.artifactId);
224
+ const artifacts = artifactIds.length
225
+ ? await db
226
+ .select()
227
+ .from(schema.browserStateArtifacts)
228
+ .where(inArray(schema.browserStateArtifacts.id, artifactIds))
229
+ : [];
230
+ const byRevision = new Map<string, BrowserRevisionComponentRow[]>();
231
+ for (const component of components) {
232
+ const current = byRevision.get(component.revisionId) ?? [];
233
+ current.push(component);
234
+ byRevision.set(component.revisionId, current);
235
+ }
236
+ const byArtifact = new Map(artifacts.map((artifact) => [artifact.id, artifact]));
237
+ return rows.map((row) => revisionFromRows(row, byRevision.get(row.id) ?? [], byArtifact));
238
+ }
239
+
240
+ async function loadPublicationResponse(
241
+ db: Database,
242
+ workspaceId: string,
243
+ operationId: string,
244
+ replayed: boolean,
245
+ ): Promise<PublishBrowserRevisionResponseValue | null> {
246
+ const rows = await loadRevisionRows(db, workspaceId, { operationId });
247
+ const row = rows[0];
248
+ if (!row) return null;
249
+ const identity = await loadIdentity(db, workspaceId, row.identityId);
250
+ if (!identity) throw new Error("BrowserRevision references a missing identity");
251
+ const [revision] = await materializeRevisions(db, [row]);
252
+ if (!revision) throw new Error("BrowserRevision could not be materialized");
253
+ return PublishBrowserRevisionResponse.parse({
254
+ identity: identityFromRow(identity),
255
+ revision,
256
+ outcome: row.defaultAdvanced ? "saved_as_default" : "saved_not_default",
257
+ replayed,
258
+ });
259
+ }
260
+
261
+ export async function listBrowserIdentities(
262
+ db: Database,
263
+ input: { accountId: string; workspaceId: string; includeArchived?: boolean },
264
+ ): Promise<BrowserIdentityListResponseValue> {
265
+ return await withRlsContext(
266
+ db,
267
+ input,
268
+ async (scopedDb) => {
269
+ const rows = await scopedDb
270
+ .select()
271
+ .from(schema.browserIdentities)
272
+ .where(
273
+ input.includeArchived
274
+ ? eq(schema.browserIdentities.workspaceId, input.workspaceId)
275
+ : and(
276
+ eq(schema.browserIdentities.workspaceId, input.workspaceId),
277
+ eq(schema.browserIdentities.status, "active"),
278
+ ),
279
+ )
280
+ .orderBy(desc(schema.browserIdentities.updatedAt), asc(schema.browserIdentities.name));
281
+ return BrowserIdentityListResponse.parse({
282
+ revision: await readWorkspaceInteractionRevision(scopedDb, input.workspaceId),
283
+ identities: rows.map(identityFromRow),
284
+ });
285
+ },
286
+ CONSISTENT_READ,
287
+ );
288
+ }
289
+
290
+ export async function getBrowserIdentity(
291
+ db: Database,
292
+ input: { accountId: string; workspaceId: string; identityId: string },
293
+ ): Promise<BrowserIdentityValue> {
294
+ return await withRlsContext(
295
+ db,
296
+ input,
297
+ async (scopedDb) => {
298
+ const row = await loadIdentity(scopedDb, input.workspaceId, input.identityId);
299
+ if (!row) throw new BrowserIdentityNotFoundError("BrowserIdentity not found");
300
+ return identityFromRow(row);
301
+ },
302
+ CONSISTENT_READ,
303
+ );
304
+ }
305
+
306
+ export async function listBrowserRevisions(
307
+ db: Database,
308
+ input: { accountId: string; workspaceId: string; identityId: string },
309
+ ): Promise<BrowserRevisionListResponseValue> {
310
+ return await withRlsContext(
311
+ db,
312
+ input,
313
+ async (scopedDb) => {
314
+ const identity = await loadIdentity(scopedDb, input.workspaceId, input.identityId);
315
+ if (!identity) throw new BrowserIdentityNotFoundError("BrowserIdentity not found");
316
+ const revisions = await materializeRevisions(
317
+ scopedDb,
318
+ await loadRevisionRows(scopedDb, input.workspaceId, { identityId: input.identityId }),
319
+ );
320
+ return BrowserRevisionListResponse.parse({ identity: identityFromRow(identity), revisions });
321
+ },
322
+ CONSISTENT_READ,
323
+ );
324
+ }
325
+
326
+ export async function createBrowserIdentity(
327
+ db: Database,
328
+ input: {
329
+ accountId: string;
330
+ workspaceId: string;
331
+ operationId: string;
332
+ name: string;
333
+ actorSubjectId: string;
334
+ },
335
+ ): Promise<BrowserIdentityMutationResponseValue> {
336
+ const name = input.name.trim();
337
+ if (!name || Buffer.byteLength(name) > 200) {
338
+ throw new BrowserIdentityStateError("BrowserIdentity name is invalid");
339
+ }
340
+ return await withRlsContext(db, input, async (scopedDb) => {
341
+ try {
342
+ return await scopedDb.transaction(async (txRaw) => {
343
+ const tx = txRaw as unknown as Database;
344
+ const id = randomUUID();
345
+ const [inserted] = await tx
346
+ .insert(schema.browserIdentities)
347
+ .values({
348
+ id,
349
+ accountId: input.accountId,
350
+ workspaceId: input.workspaceId,
351
+ name,
352
+ createOperationId: input.operationId,
353
+ createdBySubjectId: input.actorSubjectId,
354
+ })
355
+ .onConflictDoNothing({
356
+ target: [
357
+ schema.browserIdentities.workspaceId,
358
+ schema.browserIdentities.createOperationId,
359
+ ],
360
+ })
361
+ .returning();
362
+ const row =
363
+ inserted ??
364
+ (
365
+ await tx
366
+ .select()
367
+ .from(schema.browserIdentities)
368
+ .where(
369
+ and(
370
+ eq(schema.browserIdentities.workspaceId, input.workspaceId),
371
+ eq(schema.browserIdentities.createOperationId, input.operationId),
372
+ ),
373
+ )
374
+ .limit(1)
375
+ )[0];
376
+ if (!row) throw new Error("BrowserIdentity create operation was lost");
377
+ const replayed = inserted === undefined;
378
+ if (row.name !== name || row.createdBySubjectId !== input.actorSubjectId) {
379
+ throw new BrowserIdentityConflictError(
380
+ "BrowserIdentity operation id is already bound to another request",
381
+ );
382
+ }
383
+ if (!replayed) {
384
+ await advanceWorkspaceInteractionRevision(tx, input.accountId, input.workspaceId);
385
+ }
386
+ return BrowserIdentityMutationResponse.parse({
387
+ identity: identityFromRow(row),
388
+ operationId: input.operationId,
389
+ replayed,
390
+ });
391
+ });
392
+ } catch (error) {
393
+ if (postgresConstraint(error) === "browser_identities_workspace_active_name_uq") {
394
+ throw new BrowserIdentityConflictError("An active browser profile already uses this name");
395
+ }
396
+ throw error;
397
+ }
398
+ });
399
+ }
400
+
401
+ export async function updateBrowserIdentity(
402
+ db: Database,
403
+ input: {
404
+ accountId: string;
405
+ workspaceId: string;
406
+ identityId: string;
407
+ actorSubjectId: string;
408
+ } & UpdateBrowserIdentityRequestValue,
409
+ ): Promise<BrowserIdentityMutationResponseValue> {
410
+ const request = UpdateBrowserIdentityRequest.parse({
411
+ operationId: input.operationId,
412
+ expectedVersion: input.expectedVersion,
413
+ ...(input.name !== undefined ? { name: input.name } : {}),
414
+ ...(input.status !== undefined ? { status: input.status } : {}),
415
+ ...(input.defaultRevisionId !== undefined
416
+ ? { defaultRevisionId: input.defaultRevisionId }
417
+ : {}),
418
+ });
419
+ const { operationId: _operationId, ...digestRequest } = request;
420
+ const requestDigest = createHash("sha256")
421
+ .update(
422
+ JSON.stringify({
423
+ version: 1,
424
+ identityId: input.identityId,
425
+ request: digestRequest,
426
+ actorSubjectId: input.actorSubjectId,
427
+ }),
428
+ )
429
+ .digest("hex");
430
+
431
+ try {
432
+ return await withRlsContext(
433
+ db,
434
+ input,
435
+ async (scopedDb) =>
436
+ await scopedDb.transaction(async (txRaw) => {
437
+ const tx = txRaw as unknown as Database;
438
+ await lockBrowserIdentityMutationOperation(tx, request.operationId);
439
+ const existing = await loadBrowserIdentityMutationOperation(
440
+ tx,
441
+ input.workspaceId,
442
+ request.operationId,
443
+ );
444
+ if (existing) {
445
+ if (
446
+ existing.resourceKind !== "browser_identity" ||
447
+ existing.resourceId !== input.identityId ||
448
+ existing.kind !== "update" ||
449
+ existing.requestDigest !== requestDigest ||
450
+ existing.actorSubjectId !== input.actorSubjectId
451
+ ) {
452
+ throw new BrowserIdentityConflictError(
453
+ "Operation id is already bound to another browser identity request",
454
+ );
455
+ }
456
+ if (existing.state !== "completed" || !existing.result || !existing.resultVersion) {
457
+ throw new BrowserIdentityStateError("Browser identity operation is not complete");
458
+ }
459
+ const response = BrowserIdentityMutationResponse.parse(existing.result);
460
+ return { ...response, replayed: true };
461
+ }
462
+
463
+ await lockBrowserIdentity(tx, input.workspaceId, input.identityId);
464
+ const current = await loadIdentity(tx, input.workspaceId, input.identityId);
465
+ if (!current) throw new BrowserIdentityNotFoundError("BrowserIdentity not found");
466
+ if (current.version !== request.expectedVersion) {
467
+ throw new BrowserIdentityConflictError("BrowserIdentity changed before this update");
468
+ }
469
+
470
+ let defaultRevisionChanged = false;
471
+ if (request.defaultRevisionId !== undefined) {
472
+ if ((request.status ?? current.status) !== "active") {
473
+ throw new BrowserIdentityStateError(
474
+ "An archived browser profile cannot select a default version",
475
+ );
476
+ }
477
+ const [revision] = await tx
478
+ .select({ id: schema.browserRevisions.id })
479
+ .from(schema.browserRevisions)
480
+ .where(
481
+ and(
482
+ eq(schema.browserRevisions.workspaceId, input.workspaceId),
483
+ eq(schema.browserRevisions.identityId, input.identityId),
484
+ eq(schema.browserRevisions.id, request.defaultRevisionId),
485
+ ),
486
+ )
487
+ .limit(1);
488
+ if (!revision) {
489
+ throw new BrowserIdentityStateError(
490
+ "Default browser version does not belong to this profile",
491
+ );
492
+ }
493
+ defaultRevisionChanged = request.defaultRevisionId !== current.defaultRevisionId;
494
+ }
495
+
496
+ const now = new Date();
497
+ const [row] = await tx
498
+ .update(schema.browserIdentities)
499
+ .set({
500
+ name: request.name ?? current.name,
501
+ status: request.status ?? current.status,
502
+ ...(defaultRevisionChanged
503
+ ? {
504
+ defaultRevisionId: request.defaultRevisionId,
505
+ headGeneration: safeInteger(
506
+ current.headGeneration + 1,
507
+ "next BrowserIdentity head generation",
508
+ ),
509
+ }
510
+ : {}),
511
+ version: safeInteger(current.version + 1, "next BrowserIdentity version"),
512
+ updatedAt: now,
513
+ })
514
+ .where(
515
+ and(
516
+ eq(schema.browserIdentities.workspaceId, input.workspaceId),
517
+ eq(schema.browserIdentities.id, input.identityId),
518
+ eq(schema.browserIdentities.version, request.expectedVersion),
519
+ ),
520
+ )
521
+ .returning();
522
+ if (!row) throw new BrowserIdentityConflictError("BrowserIdentity update lost its fence");
523
+
524
+ const response = BrowserIdentityMutationResponse.parse({
525
+ identity: identityFromRow(row),
526
+ operationId: request.operationId,
527
+ replayed: false,
528
+ });
529
+ await tx.insert(schema.interactionResourceOperations).values({
530
+ operationId: request.operationId,
531
+ accountId: input.accountId,
532
+ workspaceId: input.workspaceId,
533
+ resourceKind: "browser_identity",
534
+ resourceId: input.identityId,
535
+ kind: "update",
536
+ requestDigest,
537
+ resultVersion: row.version,
538
+ result: response,
539
+ actorSubjectId: input.actorSubjectId,
540
+ state: "completed",
541
+ settledAt: now,
542
+ });
543
+ await advanceWorkspaceInteractionRevision(tx, input.accountId, input.workspaceId);
544
+ return response;
545
+ }),
546
+ );
547
+ } catch (error) {
548
+ if (postgresConstraint(error) === "browser_identities_workspace_active_name_uq") {
549
+ throw new BrowserIdentityConflictError("An active browser profile already uses this name");
550
+ }
551
+ throw error;
552
+ }
553
+ }
554
+
555
+ export function browserRevisionPublicationRequestDigest(input: {
556
+ browserSessionId: string;
557
+ identityId: string;
558
+ expectedHeadGeneration: number;
559
+ advanceDefault: boolean;
560
+ actorSubjectId: string;
561
+ }): string {
562
+ return createHash("sha256")
563
+ .update(
564
+ JSON.stringify({
565
+ version: 1,
566
+ browserSessionId: input.browserSessionId,
567
+ identityId: input.identityId,
568
+ expectedHeadGeneration: input.expectedHeadGeneration,
569
+ advanceDefault: input.advanceDefault,
570
+ actorSubjectId: input.actorSubjectId,
571
+ }),
572
+ )
573
+ .digest("hex");
574
+ }
575
+
576
+ export async function prepareBrowserRevisionPublication(
577
+ db: Database,
578
+ input: {
579
+ accountId: string;
580
+ workspaceId: string;
581
+ operationId: string;
582
+ browserSessionId: string;
583
+ identityId: string;
584
+ expectedHeadGeneration: number;
585
+ advanceDefault: boolean;
586
+ actorSubjectId: string;
587
+ },
588
+ ): Promise<BrowserRevisionPublicationPreparation> {
589
+ const digest = browserRevisionPublicationRequestDigest(input);
590
+ try {
591
+ return await withRlsContext(
592
+ db,
593
+ input,
594
+ async (scopedDb) =>
595
+ await scopedDb.transaction(async (txRaw) => {
596
+ const tx = txRaw as unknown as Database;
597
+
598
+ // Resolve a terminal idempotency replay before consulting mutable
599
+ // session/identity state. Ending a session cannot invalidate its
600
+ // already-completed publication receipt.
601
+ const existing = await loadInteractionOperation(tx, input.workspaceId, input.operationId);
602
+ if (existing) {
603
+ assertPublicationOperation(existing, input.browserSessionId, digest);
604
+ if (existing.state === "completed") {
605
+ const response = await loadPublicationResponse(
606
+ tx,
607
+ input.workspaceId,
608
+ input.operationId,
609
+ true,
610
+ );
611
+ if (!response) throw new Error("Completed publication has no BrowserRevision");
612
+ return { kind: "completed", response };
613
+ }
614
+ if (existing.state === "failed" || existing.state === "outcome_unknown") {
615
+ throw new BrowserIdentityStateError(
616
+ existing.errorMessage ?? "BrowserRevision publication did not complete",
617
+ );
618
+ }
619
+ }
620
+
621
+ // Every operation that competes for a BrowserSession first locks the
622
+ // resource row. Dispatch, settlement, and end therefore cannot form
623
+ // an operation-row/resource-row lock cycle.
624
+ await lockBrowserSession(tx, input.workspaceId, input.browserSessionId);
625
+ const session = await loadBrowserSessionRow(
626
+ tx,
627
+ input.workspaceId,
628
+ input.browserSessionId,
629
+ );
630
+ let operation = existing;
631
+ if (operation) {
632
+ await lockInteractionOperation(tx, input.workspaceId, input.operationId);
633
+ operation = await loadInteractionOperation(tx, input.workspaceId, input.operationId);
634
+ assertPublicationOperation(operation, input.browserSessionId, digest);
635
+ if (operation.state === "completed") {
636
+ const response = await loadPublicationResponse(
637
+ tx,
638
+ input.workspaceId,
639
+ input.operationId,
640
+ true,
641
+ );
642
+ if (!response) throw new Error("Completed publication has no BrowserRevision");
643
+ return { kind: "completed", response };
644
+ }
645
+ if (operation.state === "failed" || operation.state === "outcome_unknown") {
646
+ throw new BrowserIdentityStateError(
647
+ operation.errorMessage ?? "BrowserRevision publication did not complete",
648
+ );
649
+ }
650
+ }
651
+ if (!session) throw new BrowserIdentityNotFoundError("BrowserSession not found");
652
+ if (session.lifecycle !== "active" || !session.controllerGeneration) {
653
+ throw new BrowserIdentityStateError("BrowserSession is not active");
654
+ }
655
+ if (!BrowserSessionCapabilities.parse(session.capabilities).identityPublication) {
656
+ throw new BrowserIdentityStateError(
657
+ "BrowserSession does not support reusable profile publication",
658
+ );
659
+ }
660
+ const identity = await loadIdentity(tx, input.workspaceId, input.identityId);
661
+ if (!identity) throw new BrowserIdentityNotFoundError("BrowserIdentity not found");
662
+ if (identity.status !== "active") {
663
+ throw new BrowserIdentityStateError("BrowserIdentity is archived");
664
+ }
665
+ if (!operation) {
666
+ const [inserted] = await tx
667
+ .insert(schema.interactionOperations)
668
+ .values({
669
+ operationId: input.operationId,
670
+ accountId: input.accountId,
671
+ workspaceId: input.workspaceId,
672
+ resourceKind: "browser_session",
673
+ resourceId: input.browserSessionId,
674
+ kind: "publish",
675
+ requestDigest: digest,
676
+ actorSubjectId: input.actorSubjectId,
677
+ })
678
+ .onConflictDoNothing({ target: schema.interactionOperations.operationId })
679
+ .returning();
680
+ operation =
681
+ inserted ??
682
+ (await loadInteractionOperation(tx, input.workspaceId, input.operationId));
683
+ }
684
+ if (!operation) {
685
+ throw new BrowserIdentityConflictError(
686
+ "Publication operation id belongs to another workspace",
687
+ );
688
+ }
689
+ assertPublicationOperation(operation, input.browserSessionId, digest);
690
+ if (
691
+ operation.controllerGeneration &&
692
+ operation.controllerGeneration !== session.controllerGeneration
693
+ ) {
694
+ throw new BrowserIdentityConflictError("Publication belongs to a stale controller");
695
+ }
696
+ if (operation.state !== "prepared" && operation.state !== "dispatched") {
697
+ throw new BrowserIdentityStateError(
698
+ "BrowserRevision publication has an invalid active state",
699
+ );
700
+ }
701
+ return {
702
+ kind: "pending",
703
+ operationState: operation.state,
704
+ browserSessionId: session.id,
705
+ controllerGeneration: session.controllerGeneration,
706
+ identity: identityFromRow(identity),
707
+ parentRevisionId:
708
+ session.identityId === input.identityId ? session.baseRevisionId : null,
709
+ };
710
+ }),
711
+ );
712
+ } catch (error) {
713
+ if (postgresConstraint(error) === "interaction_operations_active_resource_uq") {
714
+ throw new BrowserIdentityConflictError(
715
+ "BrowserSession already has an active lifecycle operation",
716
+ );
717
+ }
718
+ throw error;
719
+ }
720
+ }
721
+
722
+ export async function dispatchBrowserRevisionPublication(
723
+ db: Database,
724
+ input: {
725
+ accountId: string;
726
+ workspaceId: string;
727
+ operationId: string;
728
+ browserSessionId: string;
729
+ controllerGeneration: string;
730
+ identityId: string;
731
+ expectedHeadGeneration: number;
732
+ advanceDefault: boolean;
733
+ actorSubjectId: string;
734
+ stateUpload?: { objectKey: string; cleanupAfter: Date };
735
+ },
736
+ ): Promise<BrowserRevisionPublicationDispatch> {
737
+ const digest = browserRevisionPublicationRequestDigest(input);
738
+ return await withRlsContext(
739
+ db,
740
+ input,
741
+ async (scopedDb) =>
742
+ await scopedDb.transaction(async (txRaw) => {
743
+ const tx = txRaw as unknown as Database;
744
+ const beforeLock = await loadInteractionOperation(tx, input.workspaceId, input.operationId);
745
+ assertPublicationOperation(beforeLock, input.browserSessionId, digest);
746
+ if (beforeLock.state === "completed") {
747
+ const response = await loadPublicationResponse(
748
+ tx,
749
+ input.workspaceId,
750
+ input.operationId,
751
+ true,
752
+ );
753
+ if (!response) throw new Error("Completed publication has no BrowserRevision");
754
+ return { kind: "completed", response };
755
+ }
756
+ if (beforeLock.state === "failed" || beforeLock.state === "outcome_unknown") {
757
+ throw new BrowserIdentityStateError(
758
+ beforeLock.errorMessage ?? "BrowserRevision publication did not complete",
759
+ );
760
+ }
761
+
762
+ await lockBrowserSession(tx, input.workspaceId, input.browserSessionId);
763
+ const session = await loadBrowserSessionRow(tx, input.workspaceId, input.browserSessionId);
764
+ await lockInteractionOperation(tx, input.workspaceId, input.operationId);
765
+ const operation = await loadInteractionOperation(tx, input.workspaceId, input.operationId);
766
+ assertPublicationOperation(operation, input.browserSessionId, digest);
767
+ if (operation.state === "completed") {
768
+ const response = await loadPublicationResponse(
769
+ tx,
770
+ input.workspaceId,
771
+ input.operationId,
772
+ true,
773
+ );
774
+ if (!response) throw new Error("Completed publication has no BrowserRevision");
775
+ return { kind: "completed", response };
776
+ }
777
+ if (operation.state === "failed" || operation.state === "outcome_unknown") {
778
+ throw new BrowserIdentityStateError(
779
+ operation.errorMessage ?? "BrowserRevision publication did not complete",
780
+ );
781
+ }
782
+ if (
783
+ !session ||
784
+ session.lifecycle !== "active" ||
785
+ session.controllerGeneration !== input.controllerGeneration
786
+ ) {
787
+ throw new BrowserIdentityConflictError(
788
+ "BrowserRevision publication has a stale BrowserSession controller",
789
+ );
790
+ }
791
+ if (operation.state === "dispatched") {
792
+ if (operation.controllerGeneration !== input.controllerGeneration) {
793
+ throw new BrowserIdentityConflictError(
794
+ "Publication belongs to another controller generation",
795
+ );
796
+ }
797
+ if (input.stateUpload) {
798
+ await prepareBrowserStateUploadInTransaction(tx, {
799
+ accountId: input.accountId,
800
+ workspaceId: input.workspaceId,
801
+ operationId: input.operationId,
802
+ sourceBrowserSessionId: input.browserSessionId,
803
+ purpose: "revision_component",
804
+ ...input.stateUpload,
805
+ });
806
+ }
807
+ return {
808
+ kind: "dispatched",
809
+ browserSessionId: input.browserSessionId,
810
+ controllerGeneration: input.controllerGeneration,
811
+ replayed: true,
812
+ };
813
+ }
814
+
815
+ if (input.stateUpload) {
816
+ await prepareBrowserStateUploadInTransaction(tx, {
817
+ accountId: input.accountId,
818
+ workspaceId: input.workspaceId,
819
+ operationId: input.operationId,
820
+ sourceBrowserSessionId: input.browserSessionId,
821
+ purpose: "revision_component",
822
+ ...input.stateUpload,
823
+ });
824
+ }
825
+ const now = new Date();
826
+ const [updated] = await tx
827
+ .update(schema.interactionOperations)
828
+ .set({
829
+ state: "dispatched",
830
+ controllerGeneration: input.controllerGeneration,
831
+ dispatchedAt: now,
832
+ updatedAt: now,
833
+ })
834
+ .where(
835
+ and(
836
+ eq(schema.interactionOperations.workspaceId, input.workspaceId),
837
+ eq(schema.interactionOperations.operationId, input.operationId),
838
+ eq(schema.interactionOperations.state, "prepared"),
839
+ ),
840
+ )
841
+ .returning({ operationId: schema.interactionOperations.operationId });
842
+ if (!updated) throw new Error("BrowserRevision publication dispatch was lost");
843
+ return {
844
+ kind: "dispatched",
845
+ browserSessionId: input.browserSessionId,
846
+ controllerGeneration: input.controllerGeneration,
847
+ replayed: false,
848
+ };
849
+ }),
850
+ );
851
+ }
852
+
853
+ export async function commitBrowserRevisionPublication(
854
+ db: Database,
855
+ input: {
856
+ accountId: string;
857
+ workspaceId: string;
858
+ operationId: string;
859
+ browserSessionId: string;
860
+ controllerGeneration: string;
861
+ identityId: string;
862
+ expectedHeadGeneration: number;
863
+ advanceDefault: boolean;
864
+ actorSubjectId: string;
865
+ manifestDigest: string;
866
+ artifacts: readonly BrowserStateArtifactCommitInput[];
867
+ },
868
+ ): Promise<PublishBrowserRevisionResponseValue> {
869
+ const digest = browserRevisionPublicationRequestDigest(input);
870
+ const artifacts = validateArtifacts(input.workspaceId, input.artifacts);
871
+ if (!SHA256_PATTERN.test(input.manifestDigest)) {
872
+ throw new BrowserIdentityStateError("BrowserRevision manifest digest is invalid");
873
+ }
874
+ return await withRlsContext(
875
+ db,
876
+ input,
877
+ async (scopedDb) =>
878
+ await scopedDb.transaction(async (txRaw) => {
879
+ const tx = txRaw as unknown as Database;
880
+ const beforeLock = await loadInteractionOperation(tx, input.workspaceId, input.operationId);
881
+ assertPublicationOperation(beforeLock, input.browserSessionId, digest);
882
+ if (beforeLock.state === "completed") {
883
+ const replayed = await loadPublicationResponse(
884
+ tx,
885
+ input.workspaceId,
886
+ input.operationId,
887
+ true,
888
+ );
889
+ if (!replayed) throw new Error("Completed publication has no BrowserRevision");
890
+ return replayed;
891
+ }
892
+ if (beforeLock.state === "failed" || beforeLock.state === "outcome_unknown") {
893
+ throw new BrowserIdentityStateError(
894
+ beforeLock.errorMessage ?? "BrowserRevision publication did not complete",
895
+ );
896
+ }
897
+
898
+ await lockBrowserSession(tx, input.workspaceId, input.browserSessionId);
899
+ const session = await loadBrowserSessionRow(tx, input.workspaceId, input.browserSessionId);
900
+ await lockInteractionOperation(tx, input.workspaceId, input.operationId);
901
+ const operation = await loadInteractionOperation(tx, input.workspaceId, input.operationId);
902
+ assertPublicationOperation(operation, input.browserSessionId, digest);
903
+ if (operation.state === "completed") {
904
+ const replayed = await loadPublicationResponse(
905
+ tx,
906
+ input.workspaceId,
907
+ input.operationId,
908
+ true,
909
+ );
910
+ if (!replayed) throw new Error("Completed publication has no BrowserRevision");
911
+ return replayed;
912
+ }
913
+ if (operation.state === "failed" || operation.state === "outcome_unknown") {
914
+ throw new BrowserIdentityStateError(
915
+ operation.errorMessage ?? "BrowserRevision publication did not complete",
916
+ );
917
+ }
918
+ if (
919
+ !session ||
920
+ session.lifecycle !== "active" ||
921
+ session.controllerGeneration !== input.controllerGeneration
922
+ ) {
923
+ throw new BrowserIdentityStateError("BrowserSession controller is no longer active");
924
+ }
925
+ if (operation.state !== "dispatched") {
926
+ throw new BrowserIdentityStateError("BrowserRevision publication is not dispatched");
927
+ }
928
+ if (operation.controllerGeneration !== input.controllerGeneration) {
929
+ throw new BrowserIdentityConflictError("Publication controller generation changed");
930
+ }
931
+
932
+ await lockBrowserIdentity(tx, input.workspaceId, input.identityId);
933
+ const identity = await loadIdentity(tx, input.workspaceId, input.identityId);
934
+ if (!identity) throw new BrowserIdentityNotFoundError("BrowserIdentity not found");
935
+ if (identity.status !== "active") {
936
+ throw new BrowserIdentityStateError("BrowserIdentity is archived");
937
+ }
938
+
939
+ const now = new Date();
940
+ const revisionId = randomUUID();
941
+ const ordinal = safeInteger(identity.revisionCount + 1, "next BrowserRevision ordinal");
942
+ const parentRevisionId =
943
+ session.identityId === input.identityId ? session.baseRevisionId : null;
944
+ const defaultAdvanced =
945
+ input.advanceDefault && identity.headGeneration === input.expectedHeadGeneration;
946
+ const resultHeadGeneration = defaultAdvanced
947
+ ? safeInteger(identity.headGeneration + 1, "next BrowserIdentity head generation")
948
+ : identity.headGeneration;
949
+
950
+ const artifactRows: BrowserStateArtifactRow[] = [];
951
+ for (const artifact of artifacts) {
952
+ const [row] = await tx
953
+ .insert(schema.browserStateArtifacts)
954
+ .values({
955
+ id: randomUUID(),
956
+ accountId: input.accountId,
957
+ workspaceId: input.workspaceId,
958
+ sourceBrowserSessionId: input.browserSessionId,
959
+ purpose: "revision_component",
960
+ ...artifact,
961
+ })
962
+ .returning();
963
+ if (!row) throw new Error("Browser state artifact insert was lost");
964
+ await commitBrowserStateUploadInTransaction(tx, {
965
+ workspaceId: input.workspaceId,
966
+ operationId: input.operationId,
967
+ sourceBrowserSessionId: input.browserSessionId,
968
+ purpose: "revision_component",
969
+ objectKey: artifact.objectKey,
970
+ artifactId: row.id,
971
+ });
972
+ artifactRows.push(row);
973
+ }
974
+
975
+ const [revisionRow] = await tx
976
+ .insert(schema.browserRevisions)
977
+ .values({
978
+ id: revisionId,
979
+ accountId: input.accountId,
980
+ workspaceId: input.workspaceId,
981
+ identityId: input.identityId,
982
+ parentRevisionId,
983
+ ordinal,
984
+ sourceBrowserSessionId: input.browserSessionId,
985
+ publicationOperationId: input.operationId,
986
+ expectedHeadGeneration: input.expectedHeadGeneration,
987
+ advanceDefaultRequested: input.advanceDefault,
988
+ defaultAdvanced,
989
+ resultHeadGeneration,
990
+ manifestDigest: input.manifestDigest,
991
+ createdBySubjectId: input.actorSubjectId,
992
+ createdAt: now,
993
+ })
994
+ .returning();
995
+ if (!revisionRow) throw new Error("BrowserRevision insert was lost");
996
+
997
+ const componentRows: BrowserRevisionComponentRow[] = [];
998
+ for (const [position, artifact] of artifactRows.entries()) {
999
+ const [component] = await tx
1000
+ .insert(schema.browserRevisionComponents)
1001
+ .values({
1002
+ id: randomUUID(),
1003
+ accountId: input.accountId,
1004
+ workspaceId: input.workspaceId,
1005
+ identityId: input.identityId,
1006
+ revisionId,
1007
+ artifactId: artifact.id,
1008
+ sourceBrowserSessionId: input.browserSessionId,
1009
+ artifactPurpose: "revision_component",
1010
+ kind: artifact.kind,
1011
+ position,
1012
+ createdAt: now,
1013
+ })
1014
+ .returning();
1015
+ if (!component) throw new Error("BrowserRevision component insert was lost");
1016
+ componentRows.push(component);
1017
+ }
1018
+
1019
+ const [identityRow] = await tx
1020
+ .update(schema.browserIdentities)
1021
+ .set({
1022
+ revisionCount: ordinal,
1023
+ version: safeInteger(identity.version + 1, "next BrowserIdentity version"),
1024
+ ...(defaultAdvanced
1025
+ ? { defaultRevisionId: revisionId, headGeneration: resultHeadGeneration }
1026
+ : {}),
1027
+ updatedAt: now,
1028
+ })
1029
+ .where(
1030
+ and(
1031
+ eq(schema.browserIdentities.workspaceId, input.workspaceId),
1032
+ eq(schema.browserIdentities.id, input.identityId),
1033
+ ),
1034
+ )
1035
+ .returning();
1036
+ if (!identityRow) throw new Error("BrowserIdentity publication update was lost");
1037
+
1038
+ if (session.privateCheckpointArtifactId) {
1039
+ const [retired] = await tx
1040
+ .update(schema.browserStateArtifacts)
1041
+ .set({ state: "delete_pending", retainedUntil: now })
1042
+ .where(
1043
+ and(
1044
+ eq(schema.browserStateArtifacts.workspaceId, input.workspaceId),
1045
+ eq(schema.browserStateArtifacts.id, session.privateCheckpointArtifactId),
1046
+ eq(schema.browserStateArtifacts.sourceBrowserSessionId, input.browserSessionId),
1047
+ eq(schema.browserStateArtifacts.purpose, "private_checkpoint"),
1048
+ eq(schema.browserStateArtifacts.state, "available"),
1049
+ ),
1050
+ )
1051
+ .returning({ id: schema.browserStateArtifacts.id });
1052
+ if (!retired) {
1053
+ throw new BrowserIdentityStateError(
1054
+ "BrowserSession private checkpoint authority is inconsistent",
1055
+ );
1056
+ }
1057
+ }
1058
+
1059
+ const [sessionRow] = await tx
1060
+ .update(schema.browserSessions)
1061
+ .set({
1062
+ identityId: input.identityId,
1063
+ baseRevisionId: revisionId,
1064
+ privateCheckpointArtifactId: null,
1065
+ lastUsedAt: now,
1066
+ updatedAt: now,
1067
+ })
1068
+ .where(
1069
+ and(
1070
+ eq(schema.browserSessions.workspaceId, input.workspaceId),
1071
+ eq(schema.browserSessions.id, input.browserSessionId),
1072
+ eq(schema.browserSessions.controllerGeneration, input.controllerGeneration),
1073
+ eq(schema.browserSessions.lifecycle, "active"),
1074
+ ),
1075
+ )
1076
+ .returning({ id: schema.browserSessions.id });
1077
+ if (!sessionRow)
1078
+ throw new BrowserIdentityStateError("BrowserSession publication fence changed");
1079
+
1080
+ const [settled] = await tx
1081
+ .update(schema.interactionOperations)
1082
+ .set({ state: "completed", settledAt: now, updatedAt: now })
1083
+ .where(
1084
+ and(
1085
+ eq(schema.interactionOperations.workspaceId, input.workspaceId),
1086
+ eq(schema.interactionOperations.operationId, input.operationId),
1087
+ eq(schema.interactionOperations.state, "dispatched"),
1088
+ ),
1089
+ )
1090
+ .returning({ operationId: schema.interactionOperations.operationId });
1091
+ if (!settled) throw new Error("BrowserRevision publication settlement was lost");
1092
+ await advanceWorkspaceInteractionRevision(tx, input.accountId, input.workspaceId);
1093
+
1094
+ const artifactMap = new Map(artifactRows.map((artifact) => [artifact.id, artifact]));
1095
+ return PublishBrowserRevisionResponse.parse({
1096
+ identity: identityFromRow(identityRow),
1097
+ revision: revisionFromRows(revisionRow, componentRows, artifactMap),
1098
+ outcome: defaultAdvanced ? "saved_as_default" : "saved_not_default",
1099
+ replayed: false,
1100
+ });
1101
+ }),
1102
+ );
1103
+ }
1104
+
1105
+ export async function failBrowserRevisionPublication(
1106
+ db: Database,
1107
+ input: {
1108
+ accountId: string;
1109
+ workspaceId: string;
1110
+ operationId: string;
1111
+ browserSessionId: string;
1112
+ controllerGeneration: string;
1113
+ state?: "failed" | "outcome_unknown";
1114
+ error: InteractionErrorValue;
1115
+ },
1116
+ ): Promise<void> {
1117
+ const error = InteractionError.parse(input.error);
1118
+ await withRlsContext(
1119
+ db,
1120
+ input,
1121
+ async (scopedDb) =>
1122
+ await scopedDb.transaction(async (txRaw) => {
1123
+ const tx = txRaw as unknown as Database;
1124
+ await lockInteractionOperation(tx, input.workspaceId, input.operationId);
1125
+ const operation = await loadInteractionOperation(tx, input.workspaceId, input.operationId);
1126
+ if (!operation) throw new BrowserIdentityNotFoundError("Publication operation not found");
1127
+ if (
1128
+ operation.resourceKind !== "browser_session" ||
1129
+ operation.resourceId !== input.browserSessionId ||
1130
+ operation.kind !== "publish"
1131
+ ) {
1132
+ throw new BrowserIdentityConflictError("Operation belongs to another resource");
1133
+ }
1134
+ if (
1135
+ operation.state === "completed" ||
1136
+ operation.state === "failed" ||
1137
+ operation.state === "outcome_unknown"
1138
+ ) {
1139
+ return;
1140
+ }
1141
+ if (
1142
+ operation.state !== "dispatched" ||
1143
+ operation.controllerGeneration !== input.controllerGeneration
1144
+ ) {
1145
+ throw new BrowserIdentityConflictError(
1146
+ "Publication failure belongs to another controller generation",
1147
+ );
1148
+ }
1149
+ const state = input.state ?? "failed";
1150
+ const now = new Date();
1151
+ const [settled] = await tx
1152
+ .update(schema.interactionOperations)
1153
+ .set({
1154
+ state,
1155
+ errorCode: error.code,
1156
+ errorMessage: error.message,
1157
+ errorRetryable: error.retryable,
1158
+ errorDetails: error.details ?? null,
1159
+ settledAt: now,
1160
+ updatedAt: now,
1161
+ })
1162
+ .where(
1163
+ and(
1164
+ eq(schema.interactionOperations.workspaceId, input.workspaceId),
1165
+ eq(schema.interactionOperations.operationId, input.operationId),
1166
+ eq(schema.interactionOperations.state, "dispatched"),
1167
+ eq(schema.interactionOperations.controllerGeneration, input.controllerGeneration),
1168
+ ),
1169
+ )
1170
+ .returning({ operationId: schema.interactionOperations.operationId });
1171
+ if (!settled) throw new Error("BrowserRevision publication failure settlement was lost");
1172
+ await markBrowserStateUploadsDeletePendingInTransaction(tx, input);
1173
+ }),
1174
+ );
1175
+ }
1176
+
1177
+ /** Server-only storage authority used to restore a revision. Never project this
1178
+ * result through HTTP, MCP, Codemode output, or the public SDK. */
1179
+ export async function getBrowserRevisionArtifactAuthority(
1180
+ db: Database,
1181
+ input: {
1182
+ accountId: string;
1183
+ workspaceId: string;
1184
+ identityId: string;
1185
+ revisionId: string;
1186
+ },
1187
+ ): Promise<BrowserRevisionArtifactAuthority> {
1188
+ return await withRlsContext(
1189
+ db,
1190
+ input,
1191
+ async (scopedDb) =>
1192
+ await loadBrowserRevisionArtifactAuthority(scopedDb, {
1193
+ workspaceId: input.workspaceId,
1194
+ identityId: input.identityId,
1195
+ revisionId: input.revisionId,
1196
+ }),
1197
+ CONSISTENT_READ,
1198
+ );
1199
+ }
1200
+
1201
+ async function loadBrowserRevisionArtifactAuthority(
1202
+ db: Database,
1203
+ input: { workspaceId: string; identityId: string; revisionId: string },
1204
+ ): Promise<BrowserRevisionArtifactAuthority> {
1205
+ const rows = await loadRevisionRows(db, input.workspaceId, {
1206
+ identityId: input.identityId,
1207
+ revisionId: input.revisionId,
1208
+ });
1209
+ const row = rows[0];
1210
+ if (!row) throw new BrowserIdentityNotFoundError("BrowserRevision not found");
1211
+ const components = await db
1212
+ .select()
1213
+ .from(schema.browserRevisionComponents)
1214
+ .where(eq(schema.browserRevisionComponents.revisionId, row.id))
1215
+ .orderBy(asc(schema.browserRevisionComponents.position));
1216
+ if (components.length === 0) {
1217
+ throw new BrowserIdentityStateError("BrowserRevision has no state components");
1218
+ }
1219
+ const artifacts = await db
1220
+ .select()
1221
+ .from(schema.browserStateArtifacts)
1222
+ .where(
1223
+ inArray(
1224
+ schema.browserStateArtifacts.id,
1225
+ components.map((component) => component.artifactId),
1226
+ ),
1227
+ );
1228
+ const byArtifact = new Map(artifacts.map((artifact) => [artifact.id, artifact]));
1229
+ const revision = revisionFromRows(row, components, byArtifact);
1230
+ return {
1231
+ revision,
1232
+ artifacts: components.map((component) => {
1233
+ const artifact = byArtifact.get(component.artifactId);
1234
+ if (!artifact || artifact.state !== "available" || artifact.encryptedDataKey === null) {
1235
+ throw new BrowserIdentityStateError("BrowserRevision artifact is unavailable");
1236
+ }
1237
+ return {
1238
+ componentId: component.id,
1239
+ artifactId: artifact.id,
1240
+ objectKey: artifact.objectKey,
1241
+ encryptedDataKey: artifact.encryptedDataKey,
1242
+ contentDigest: artifact.contentDigest,
1243
+ manifestDigest: artifact.manifestDigest,
1244
+ artifactDigest: artifact.artifactDigest,
1245
+ sizeBytes: safeInteger(artifact.sizeBytes, "browser artifact size"),
1246
+ format: artifact.format,
1247
+ materialization: BrowserRevisionMaterialization.parse(artifact.materialization),
1248
+ };
1249
+ }),
1250
+ };
1251
+ }
1252
+
1253
+ function validateArtifacts(
1254
+ workspaceId: string,
1255
+ values: readonly BrowserStateArtifactCommitInput[],
1256
+ ): BrowserStateArtifactCommitInput[] {
1257
+ if (values.length < 1 || values.length > MAX_COMPONENTS) {
1258
+ throw new BrowserIdentityStateError("BrowserRevision component count is invalid");
1259
+ }
1260
+ const seenKinds = new Set<BrowserStateArtifactCommitInput["kind"]>();
1261
+ return values.map((value) => {
1262
+ let validated: BrowserStateArtifactCommitInput;
1263
+ try {
1264
+ validated = validateBrowserStateArtifactCommitInput(workspaceId, value);
1265
+ } catch (error) {
1266
+ throw new BrowserIdentityStateError(
1267
+ error instanceof Error ? error.message : "BrowserRevision artifact metadata is invalid",
1268
+ );
1269
+ }
1270
+ if (seenKinds.has(value.kind)) {
1271
+ throw new BrowserIdentityStateError("BrowserRevision component kinds must be unique");
1272
+ }
1273
+ seenKinds.add(value.kind);
1274
+ return validated;
1275
+ });
1276
+ }
1277
+
1278
+ async function loadBrowserSessionRow(
1279
+ db: Database,
1280
+ workspaceId: string,
1281
+ browserSessionId: string,
1282
+ ): Promise<BrowserSessionRow | null> {
1283
+ const [row] = await db
1284
+ .select()
1285
+ .from(schema.browserSessions)
1286
+ .where(
1287
+ and(
1288
+ eq(schema.browserSessions.workspaceId, workspaceId),
1289
+ eq(schema.browserSessions.id, browserSessionId),
1290
+ ),
1291
+ )
1292
+ .limit(1);
1293
+ return row ?? null;
1294
+ }
1295
+
1296
+ async function loadInteractionOperation(
1297
+ db: Database,
1298
+ workspaceId: string,
1299
+ operationId: string,
1300
+ ): Promise<InteractionOperationRow | null> {
1301
+ const [row] = await db
1302
+ .select()
1303
+ .from(schema.interactionOperations)
1304
+ .where(
1305
+ and(
1306
+ eq(schema.interactionOperations.workspaceId, workspaceId),
1307
+ eq(schema.interactionOperations.operationId, operationId),
1308
+ ),
1309
+ )
1310
+ .limit(1);
1311
+ return row ?? null;
1312
+ }
1313
+
1314
+ function assertPublicationOperation(
1315
+ operation: InteractionOperationRow | null,
1316
+ browserSessionId: string,
1317
+ digest: string,
1318
+ ): asserts operation is InteractionOperationRow {
1319
+ if (!operation) throw new BrowserIdentityNotFoundError("Publication operation not found");
1320
+ if (
1321
+ operation.resourceKind !== "browser_session" ||
1322
+ operation.resourceId !== browserSessionId ||
1323
+ operation.kind !== "publish" ||
1324
+ operation.requestDigest !== digest
1325
+ ) {
1326
+ throw new BrowserIdentityConflictError(
1327
+ "Publication operation id is already bound to another request",
1328
+ );
1329
+ }
1330
+ }
1331
+
1332
+ async function lockBrowserIdentity(db: Database, workspaceId: string, identityId: string) {
1333
+ await db.execute(sql`
1334
+ select id from browser_identities
1335
+ where workspace_id = ${workspaceId} and id = ${identityId}
1336
+ for update
1337
+ `);
1338
+ }
1339
+
1340
+ async function lockBrowserIdentityMutationOperation(
1341
+ db: Database,
1342
+ operationId: string,
1343
+ ): Promise<void> {
1344
+ await db.execute(
1345
+ sql`select pg_advisory_xact_lock(hashtextextended(${`interaction-resource:${operationId}`}, 0))`,
1346
+ );
1347
+ }
1348
+
1349
+ async function loadBrowserIdentityMutationOperation(
1350
+ db: Database,
1351
+ workspaceId: string,
1352
+ operationId: string,
1353
+ ): Promise<InteractionResourceOperationRow | null> {
1354
+ const [row] = await db
1355
+ .select()
1356
+ .from(schema.interactionResourceOperations)
1357
+ .where(
1358
+ and(
1359
+ eq(schema.interactionResourceOperations.workspaceId, workspaceId),
1360
+ eq(schema.interactionResourceOperations.operationId, operationId),
1361
+ ),
1362
+ )
1363
+ .limit(1);
1364
+ return row ?? null;
1365
+ }
1366
+
1367
+ async function lockBrowserSession(db: Database, workspaceId: string, browserSessionId: string) {
1368
+ await db.execute(sql`
1369
+ select id from browser_sessions
1370
+ where workspace_id = ${workspaceId} and id = ${browserSessionId}
1371
+ for update
1372
+ `);
1373
+ }
1374
+
1375
+ async function lockInteractionOperation(db: Database, workspaceId: string, operationId: string) {
1376
+ await db.execute(sql`
1377
+ select operation_id from interaction_operations
1378
+ where workspace_id = ${workspaceId} and operation_id = ${operationId}
1379
+ for update
1380
+ `);
1381
+ }
1382
+
1383
+ function postgresConstraint(error: unknown): string | null {
1384
+ return safeDatabaseErrorFacts(error).constraint ?? null;
1385
+ }