@opengeni/db 0.28.1 → 0.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attached-browser-devices.d.ts +42 -0
- package/dist/attempt-tool-catalogs.d.ts +16 -0
- package/dist/browser-auth.d.ts +91 -0
- package/dist/browser-identities.d.ts +128 -0
- package/dist/browser-sessions.d.ts +272 -0
- package/dist/browser-state-artifacts.d.ts +17 -0
- package/dist/capability-components.d.ts +37 -0
- package/dist/capability-integrations.d.ts +192 -0
- package/dist/chunk-F5FT7BEZ.js +3535 -0
- package/dist/chunk-F5FT7BEZ.js.map +1 -0
- package/dist/chunk-ISI2TIUG.js +639 -0
- package/dist/chunk-ISI2TIUG.js.map +1 -0
- package/dist/chunk-MWDVXQOL.js +11639 -0
- package/dist/chunk-MWDVXQOL.js.map +1 -0
- package/dist/chunk-NBE6CHRI.js +955 -0
- package/dist/chunk-NBE6CHRI.js.map +1 -0
- package/dist/{chunk-DBS5HPEW.js → chunk-YHZXQ7HP.js} +760 -11
- package/dist/chunk-YHZXQ7HP.js.map +1 -0
- package/dist/codemode-operations.d.ts +117 -0
- package/dist/codex-token-resolver.d.ts +2 -2
- package/dist/company-profile-schema.d.ts +798 -0
- package/dist/company-profile.d.ts +268 -0
- package/dist/computer-sessions.d.ts +109 -0
- package/dist/connection-token-resolver.d.ts +9 -4
- package/dist/database.d.ts +52 -3
- package/dist/editable-artifact-durable-export.d.ts +153 -0
- package/dist/editable-artifact-durable-export.js +788 -0
- package/dist/editable-artifact-durable-export.js.map +1 -0
- package/dist/editable-artifact-materialization.d.ts +123 -0
- package/dist/editable-artifacts-schema.d.ts +4152 -0
- package/dist/editable-artifacts.d.ts +610 -0
- package/dist/editable-artifacts.js +26 -0
- package/dist/editable-artifacts.js.map +1 -0
- package/dist/environment-crypto.d.ts +8 -4
- package/dist/generated-images.d.ts +139 -0
- package/dist/index.d.ts +958 -126
- package/dist/index.js +31204 -12172
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +51 -4
- package/dist/integration-bindings.d.ts +70 -0
- package/dist/integration-features.d.ts +99 -0
- package/dist/interaction-revisions.d.ts +3 -0
- package/dist/interaction-schema.d.ts +5372 -0
- package/dist/knowledge-source-sync-schema.d.ts +1679 -0
- package/dist/knowledge-source-sync.d.ts +274 -0
- package/dist/lossless-columns.d.ts +58 -0
- package/dist/lossless-json.d.ts +39 -0
- package/dist/memory-domain.d.ts +3 -6
- package/dist/memory-governance.d.ts +2 -2
- package/dist/memory-slack-delivery.d.ts +101 -0
- package/dist/new-session-drafts.d.ts +2 -2
- package/dist/pack-components.d.ts +76 -0
- package/dist/pack-installations.d.ts +86 -0
- package/dist/persistence-errors.d.ts +7 -14
- package/dist/plugin-packages.d.ts +126 -0
- package/dist/preference-registry.d.ts +1 -1
- package/dist/provision-roles.d.ts +9 -1
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +18 -6
- package/dist/schema.d.ts +19122 -8731
- package/dist/schema.js +173 -1
- package/dist/scoped-knowledge.d.ts +97 -1
- package/dist/session-control.d.ts +4 -4
- package/dist/session-queue-commands.d.ts +19 -11
- package/dist/session-realtime-context.d.ts +2 -2
- package/dist/session-realtime-ledger.d.ts +4 -4
- package/dist/session-realtime-mirror.d.ts +2 -2
- package/dist/session-realtime-state.d.ts +1 -1
- package/dist/session-realtime-terminal.d.ts +8 -1
- package/dist/session-realtime.d.ts +7 -7
- package/dist/session-tool-call-settlement.d.ts +1 -1
- package/dist/slack-user-link-access.d.ts +54 -0
- package/dist/transcription-recordings-schema.d.ts +44 -6
- package/dist/transcription-recordings.d.ts +3 -3
- package/dist/turn-initiator.d.ts +2 -2
- package/dist/video-generation.d.ts +216 -0
- package/dist/video-generation.js +66 -0
- package/dist/video-generation.js.map +1 -0
- package/dist/workspace-artifacts.d.ts +1 -1
- package/dist/workspace-instruction-policies.d.ts +1 -1
- package/dist/workspace-learning-policy-schema.d.ts +932 -0
- package/dist/workspace-learning-policy.d.ts +161 -0
- package/drizzle/0065_enrollment_credential_generation.sql +10 -0
- package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
- package/drizzle/0176_lossless_canonical_json.sql +975 -0
- package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
- package/drizzle/0178_permissioned_secret_reads.sql +14 -0
- package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
- package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
- package/drizzle/0181_connected_machine_removal.sql +52 -0
- package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
- package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
- package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
- package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
- package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
- package/drizzle/0187_generated_image_artifacts.sql +173 -0
- package/drizzle/0188_image_generation_retention_failure.sql +25 -0
- package/drizzle/0189_retained_session_image_formats.sql +13 -0
- package/drizzle/0190_timeline_annotations.sql +31 -0
- package/drizzle/0191_editable_artifact_engine.sql +3853 -0
- package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
- package/drizzle/0193_editable_artifact_authorization.sql +190 -0
- package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
- package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
- package/drizzle/0196_rig_provider_images.sql +152 -0
- package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
- package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
- package/drizzle/0199_workspace_learning_policy.sql +980 -0
- package/drizzle/0201_company_profile_authority.sql +849 -0
- package/drizzle/0202_document_index_checkpoints.sql +85 -0
- package/drizzle/0203_durable_video_generation.sql +353 -0
- package/drizzle/0204_video_generation_funding.sql +44 -0
- package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
- package/drizzle/0206_browser_sessions.sql +646 -0
- package/drizzle/0207_browser_identities.sql +379 -0
- package/drizzle/0208_attached_browser_devices.sql +165 -0
- package/drizzle/0209_computer_sessions.sql +575 -0
- package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
- package/drizzle/0211_editable_artifact_session_links.sql +58 -0
- package/drizzle/0212_slack_installation_bindings.sql +348 -0
- package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
- package/drizzle/0214_session_activity_commit_gate.sql +262 -0
- package/drizzle/0215_capabilities_platform.sql +980 -0
- package/drizzle/0216_pack_component_ownership.sql +235 -0
- package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
- package/package.json +18 -5
- package/src/attached-browser-devices.ts +371 -0
- package/src/attempt-tool-catalogs.ts +131 -0
- package/src/browser-auth.ts +1896 -0
- package/src/browser-identities.ts +1168 -0
- package/src/browser-sessions.ts +2005 -0
- package/src/browser-state-artifacts.ts +59 -0
- package/src/capability-components.ts +166 -0
- package/src/capability-integrations.ts +1663 -0
- package/src/codemode-operations.ts +674 -0
- package/src/company-profile-schema.ts +143 -0
- package/src/company-profile.ts +968 -0
- package/src/computer-sessions.ts +1130 -0
- package/src/connection-token-resolver.ts +270 -25
- package/src/database.ts +434 -24
- package/src/editable-artifact-durable-export.ts +1201 -0
- package/src/editable-artifact-materialization.ts +893 -0
- package/src/editable-artifacts-schema.ts +879 -0
- package/src/editable-artifacts.ts +5404 -0
- package/src/environment-crypto.ts +22 -9
- package/src/generated-images.ts +740 -0
- package/src/index.ts +10117 -2730
- package/src/insights.ts +160 -13
- package/src/integration-bindings.ts +455 -0
- package/src/integration-features.ts +859 -0
- package/src/interaction-revisions.ts +40 -0
- package/src/interaction-schema.ts +1303 -0
- package/src/knowledge-source-sync-schema.ts +192 -0
- package/src/knowledge-source-sync.ts +1922 -0
- package/src/lossless-columns.ts +35 -0
- package/src/lossless-json.ts +322 -0
- package/src/memory-domain.ts +5 -44
- package/src/memory-slack-delivery.ts +801 -0
- package/src/pack-components.ts +954 -0
- package/src/pack-installations.ts +929 -0
- package/src/persistence-errors.ts +26 -34
- package/src/plugin-packages.ts +1186 -0
- package/src/provision-roles.ts +302 -1
- package/src/runtime-posture-cli.ts +9 -0
- package/src/runtime-posture.ts +514 -5
- package/src/schema.ts +5177 -2957
- package/src/scoped-knowledge-schema.ts +1 -1
- package/src/scoped-knowledge.ts +655 -3
- package/src/session-control.ts +133 -81
- package/src/session-queue-commands.ts +511 -247
- package/src/session-realtime-context.ts +20 -7
- package/src/session-realtime-ledger.ts +32 -10
- package/src/session-realtime-mirror.ts +4 -2
- package/src/session-realtime-terminal.ts +89 -21
- package/src/session-realtime.ts +10 -9
- package/src/session-tool-call-settlement.ts +38 -15
- package/src/slack-user-link-access.ts +702 -0
- package/src/transcription-recordings-schema.ts +5 -2
- package/src/transcription-recordings.ts +70 -40
- package/src/video-generation.ts +1452 -0
- package/src/workspace-learning-policy-schema.ts +139 -0
- package/src/workspace-learning-policy.ts +595 -0
- package/dist/chunk-DBS5HPEW.js.map +0 -1
- package/dist/chunk-HT5T62CC.js +0 -6803
- package/dist/chunk-HT5T62CC.js.map +0 -1
- package/dist/event-payload-sanitizer.d.ts +0 -32
- package/src/event-payload-sanitizer.ts +0 -377
|
@@ -0,0 +1,1168 @@
|
|
|
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
|
+
type BrowserIdentity as BrowserIdentityValue,
|
|
13
|
+
type BrowserIdentityListResponse as BrowserIdentityListResponseValue,
|
|
14
|
+
type BrowserIdentityMutationResponse as BrowserIdentityMutationResponseValue,
|
|
15
|
+
type BrowserRevision as BrowserRevisionValue,
|
|
16
|
+
type BrowserRevisionListResponse as BrowserRevisionListResponseValue,
|
|
17
|
+
type BrowserRevisionMaterialization as BrowserRevisionMaterializationValue,
|
|
18
|
+
type InteractionError as InteractionErrorValue,
|
|
19
|
+
type PublishBrowserRevisionResponse as PublishBrowserRevisionResponseValue,
|
|
20
|
+
} from "@opengeni/contracts";
|
|
21
|
+
import { and, asc, desc, eq, inArray, sql } from "drizzle-orm";
|
|
22
|
+
import { type Database, withRlsContext } from "./database";
|
|
23
|
+
import {
|
|
24
|
+
advanceWorkspaceInteractionRevision,
|
|
25
|
+
readWorkspaceInteractionRevision,
|
|
26
|
+
} from "./interaction-revisions";
|
|
27
|
+
import { safeDatabaseErrorFacts } from "./persistence-errors";
|
|
28
|
+
import {
|
|
29
|
+
validateBrowserStateArtifactCommitInput,
|
|
30
|
+
type BrowserStateArtifactCommitInput,
|
|
31
|
+
} from "./browser-state-artifacts";
|
|
32
|
+
import * as schema from "./schema";
|
|
33
|
+
|
|
34
|
+
type BrowserIdentityRow = typeof schema.browserIdentities.$inferSelect;
|
|
35
|
+
type BrowserRevisionRow = typeof schema.browserRevisions.$inferSelect;
|
|
36
|
+
type BrowserRevisionComponentRow = typeof schema.browserRevisionComponents.$inferSelect;
|
|
37
|
+
type BrowserStateArtifactRow = typeof schema.browserStateArtifacts.$inferSelect;
|
|
38
|
+
type BrowserSessionRow = typeof schema.browserSessions.$inferSelect;
|
|
39
|
+
type InteractionOperationRow = typeof schema.interactionOperations.$inferSelect;
|
|
40
|
+
|
|
41
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
42
|
+
const MAX_COMPONENTS = 16;
|
|
43
|
+
const CONSISTENT_READ = {
|
|
44
|
+
isolationLevel: "repeatable read",
|
|
45
|
+
accessMode: "read only",
|
|
46
|
+
} as const;
|
|
47
|
+
|
|
48
|
+
export class BrowserIdentityNotFoundError extends Error {
|
|
49
|
+
readonly name = "BrowserIdentityNotFoundError";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class BrowserIdentityConflictError extends Error {
|
|
53
|
+
readonly name = "BrowserIdentityConflictError";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class BrowserIdentityStateError extends Error {
|
|
57
|
+
readonly name = "BrowserIdentityStateError";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type { BrowserStateArtifactCommitInput } from "./browser-state-artifacts";
|
|
61
|
+
|
|
62
|
+
export type BrowserRevisionPublicationPreparation =
|
|
63
|
+
| {
|
|
64
|
+
kind: "completed";
|
|
65
|
+
response: PublishBrowserRevisionResponseValue;
|
|
66
|
+
}
|
|
67
|
+
| {
|
|
68
|
+
kind: "pending";
|
|
69
|
+
operationState: "prepared" | "dispatched";
|
|
70
|
+
browserSessionId: string;
|
|
71
|
+
controllerGeneration: string;
|
|
72
|
+
identity: BrowserIdentityValue;
|
|
73
|
+
parentRevisionId: string | null;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type BrowserRevisionPublicationDispatch =
|
|
77
|
+
| {
|
|
78
|
+
kind: "completed";
|
|
79
|
+
response: PublishBrowserRevisionResponseValue;
|
|
80
|
+
}
|
|
81
|
+
| {
|
|
82
|
+
kind: "dispatched";
|
|
83
|
+
browserSessionId: string;
|
|
84
|
+
controllerGeneration: string;
|
|
85
|
+
replayed: boolean;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export type BrowserRevisionArtifactAuthority = {
|
|
89
|
+
revision: BrowserRevisionValue;
|
|
90
|
+
artifacts: Array<{
|
|
91
|
+
componentId: string;
|
|
92
|
+
artifactId: string;
|
|
93
|
+
objectKey: string;
|
|
94
|
+
encryptedDataKey: string;
|
|
95
|
+
contentDigest: string;
|
|
96
|
+
manifestDigest: string;
|
|
97
|
+
artifactDigest: string;
|
|
98
|
+
sizeBytes: number;
|
|
99
|
+
format: string;
|
|
100
|
+
materialization: BrowserRevisionMaterializationValue;
|
|
101
|
+
}>;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function iso(value: Date): string {
|
|
105
|
+
return value.toISOString();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function safeInteger(value: number, label: string): number {
|
|
109
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label} is outside safe range`);
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function identityFromRow(row: BrowserIdentityRow): BrowserIdentityValue {
|
|
114
|
+
return BrowserIdentity.parse({
|
|
115
|
+
id: row.id,
|
|
116
|
+
accountId: row.accountId,
|
|
117
|
+
workspaceId: row.workspaceId,
|
|
118
|
+
name: row.name,
|
|
119
|
+
status: row.status,
|
|
120
|
+
defaultRevisionId: row.defaultRevisionId,
|
|
121
|
+
headGeneration: safeInteger(row.headGeneration, "BrowserIdentity head generation"),
|
|
122
|
+
revisionCount: safeInteger(row.revisionCount, "BrowserIdentity revision count"),
|
|
123
|
+
createdBySubjectId: row.createdBySubjectId,
|
|
124
|
+
createdAt: iso(row.createdAt),
|
|
125
|
+
updatedAt: iso(row.updatedAt),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function revisionFromRows(
|
|
130
|
+
row: BrowserRevisionRow,
|
|
131
|
+
components: readonly BrowserRevisionComponentRow[],
|
|
132
|
+
artifacts: ReadonlyMap<string, BrowserStateArtifactRow>,
|
|
133
|
+
): BrowserRevisionValue {
|
|
134
|
+
return BrowserRevision.parse({
|
|
135
|
+
id: row.id,
|
|
136
|
+
accountId: row.accountId,
|
|
137
|
+
workspaceId: row.workspaceId,
|
|
138
|
+
identityId: row.identityId,
|
|
139
|
+
parentRevisionId: row.parentRevisionId,
|
|
140
|
+
ordinal: safeInteger(row.ordinal, "BrowserRevision ordinal"),
|
|
141
|
+
sourceBrowserSessionId: row.sourceBrowserSessionId,
|
|
142
|
+
manifestDigest: row.manifestDigest,
|
|
143
|
+
components: [...components]
|
|
144
|
+
.sort((left, right) => left.position - right.position)
|
|
145
|
+
.map((component) => {
|
|
146
|
+
const artifact = artifacts.get(component.artifactId);
|
|
147
|
+
if (
|
|
148
|
+
!artifact ||
|
|
149
|
+
artifact.state !== "available" ||
|
|
150
|
+
artifact.purpose !== component.artifactPurpose ||
|
|
151
|
+
artifact.kind !== component.kind ||
|
|
152
|
+
artifact.sourceBrowserSessionId !== component.sourceBrowserSessionId ||
|
|
153
|
+
component.sourceBrowserSessionId !== row.sourceBrowserSessionId
|
|
154
|
+
) {
|
|
155
|
+
throw new Error("BrowserRevision references invalid or unavailable state authority");
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
id: component.id,
|
|
159
|
+
kind: component.kind,
|
|
160
|
+
format: artifact.format,
|
|
161
|
+
artifactDigest: artifact.artifactDigest,
|
|
162
|
+
sizeBytes: safeInteger(artifact.sizeBytes, "browser artifact size"),
|
|
163
|
+
materialization: BrowserRevisionMaterialization.parse(artifact.materialization),
|
|
164
|
+
};
|
|
165
|
+
}),
|
|
166
|
+
createdBySubjectId: row.createdBySubjectId,
|
|
167
|
+
createdAt: iso(row.createdAt),
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function loadIdentity(
|
|
172
|
+
db: Database,
|
|
173
|
+
workspaceId: string,
|
|
174
|
+
identityId: string,
|
|
175
|
+
): Promise<BrowserIdentityRow | null> {
|
|
176
|
+
const [row] = await db
|
|
177
|
+
.select()
|
|
178
|
+
.from(schema.browserIdentities)
|
|
179
|
+
.where(
|
|
180
|
+
and(
|
|
181
|
+
eq(schema.browserIdentities.workspaceId, workspaceId),
|
|
182
|
+
eq(schema.browserIdentities.id, identityId),
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
.limit(1);
|
|
186
|
+
return row ?? null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function loadRevisionRows(
|
|
190
|
+
db: Database,
|
|
191
|
+
workspaceId: string,
|
|
192
|
+
filter: { identityId?: string; operationId?: string; revisionId?: string },
|
|
193
|
+
): Promise<BrowserRevisionRow[]> {
|
|
194
|
+
const predicates = [eq(schema.browserRevisions.workspaceId, workspaceId)];
|
|
195
|
+
if (filter.identityId) predicates.push(eq(schema.browserRevisions.identityId, filter.identityId));
|
|
196
|
+
if (filter.operationId) {
|
|
197
|
+
predicates.push(eq(schema.browserRevisions.publicationOperationId, filter.operationId));
|
|
198
|
+
}
|
|
199
|
+
if (filter.revisionId) predicates.push(eq(schema.browserRevisions.id, filter.revisionId));
|
|
200
|
+
return await db
|
|
201
|
+
.select()
|
|
202
|
+
.from(schema.browserRevisions)
|
|
203
|
+
.where(and(...predicates))
|
|
204
|
+
.orderBy(desc(schema.browserRevisions.ordinal), desc(schema.browserRevisions.id));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function materializeRevisions(
|
|
208
|
+
db: Database,
|
|
209
|
+
rows: readonly BrowserRevisionRow[],
|
|
210
|
+
): Promise<BrowserRevisionValue[]> {
|
|
211
|
+
if (rows.length === 0) return [];
|
|
212
|
+
const revisionIds = rows.map((row) => row.id);
|
|
213
|
+
const components = await db
|
|
214
|
+
.select()
|
|
215
|
+
.from(schema.browserRevisionComponents)
|
|
216
|
+
.where(inArray(schema.browserRevisionComponents.revisionId, revisionIds))
|
|
217
|
+
.orderBy(asc(schema.browserRevisionComponents.position));
|
|
218
|
+
const artifactIds = components.map((component) => component.artifactId);
|
|
219
|
+
const artifacts = artifactIds.length
|
|
220
|
+
? await db
|
|
221
|
+
.select()
|
|
222
|
+
.from(schema.browserStateArtifacts)
|
|
223
|
+
.where(inArray(schema.browserStateArtifacts.id, artifactIds))
|
|
224
|
+
: [];
|
|
225
|
+
const byRevision = new Map<string, BrowserRevisionComponentRow[]>();
|
|
226
|
+
for (const component of components) {
|
|
227
|
+
const current = byRevision.get(component.revisionId) ?? [];
|
|
228
|
+
current.push(component);
|
|
229
|
+
byRevision.set(component.revisionId, current);
|
|
230
|
+
}
|
|
231
|
+
const byArtifact = new Map(artifacts.map((artifact) => [artifact.id, artifact]));
|
|
232
|
+
return rows.map((row) => revisionFromRows(row, byRevision.get(row.id) ?? [], byArtifact));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function loadPublicationResponse(
|
|
236
|
+
db: Database,
|
|
237
|
+
workspaceId: string,
|
|
238
|
+
operationId: string,
|
|
239
|
+
replayed: boolean,
|
|
240
|
+
): Promise<PublishBrowserRevisionResponseValue | null> {
|
|
241
|
+
const rows = await loadRevisionRows(db, workspaceId, { operationId });
|
|
242
|
+
const row = rows[0];
|
|
243
|
+
if (!row) return null;
|
|
244
|
+
const identity = await loadIdentity(db, workspaceId, row.identityId);
|
|
245
|
+
if (!identity) throw new Error("BrowserRevision references a missing identity");
|
|
246
|
+
const [revision] = await materializeRevisions(db, [row]);
|
|
247
|
+
if (!revision) throw new Error("BrowserRevision could not be materialized");
|
|
248
|
+
return PublishBrowserRevisionResponse.parse({
|
|
249
|
+
identity: identityFromRow(identity),
|
|
250
|
+
revision,
|
|
251
|
+
outcome: row.defaultAdvanced ? "saved_as_default" : "saved_not_default",
|
|
252
|
+
replayed,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function listBrowserIdentities(
|
|
257
|
+
db: Database,
|
|
258
|
+
input: { accountId: string; workspaceId: string; includeArchived?: boolean },
|
|
259
|
+
): Promise<BrowserIdentityListResponseValue> {
|
|
260
|
+
return await withRlsContext(
|
|
261
|
+
db,
|
|
262
|
+
input,
|
|
263
|
+
async (scopedDb) => {
|
|
264
|
+
const rows = await scopedDb
|
|
265
|
+
.select()
|
|
266
|
+
.from(schema.browserIdentities)
|
|
267
|
+
.where(
|
|
268
|
+
input.includeArchived
|
|
269
|
+
? eq(schema.browserIdentities.workspaceId, input.workspaceId)
|
|
270
|
+
: and(
|
|
271
|
+
eq(schema.browserIdentities.workspaceId, input.workspaceId),
|
|
272
|
+
eq(schema.browserIdentities.status, "active"),
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
.orderBy(desc(schema.browserIdentities.updatedAt), asc(schema.browserIdentities.name));
|
|
276
|
+
return BrowserIdentityListResponse.parse({
|
|
277
|
+
revision: await readWorkspaceInteractionRevision(scopedDb, input.workspaceId),
|
|
278
|
+
identities: rows.map(identityFromRow),
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
CONSISTENT_READ,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function getBrowserIdentity(
|
|
286
|
+
db: Database,
|
|
287
|
+
input: { accountId: string; workspaceId: string; identityId: string },
|
|
288
|
+
): Promise<BrowserIdentityValue> {
|
|
289
|
+
return await withRlsContext(
|
|
290
|
+
db,
|
|
291
|
+
input,
|
|
292
|
+
async (scopedDb) => {
|
|
293
|
+
const row = await loadIdentity(scopedDb, input.workspaceId, input.identityId);
|
|
294
|
+
if (!row) throw new BrowserIdentityNotFoundError("BrowserIdentity not found");
|
|
295
|
+
return identityFromRow(row);
|
|
296
|
+
},
|
|
297
|
+
CONSISTENT_READ,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function listBrowserRevisions(
|
|
302
|
+
db: Database,
|
|
303
|
+
input: { accountId: string; workspaceId: string; identityId: string },
|
|
304
|
+
): Promise<BrowserRevisionListResponseValue> {
|
|
305
|
+
return await withRlsContext(
|
|
306
|
+
db,
|
|
307
|
+
input,
|
|
308
|
+
async (scopedDb) => {
|
|
309
|
+
const identity = await loadIdentity(scopedDb, input.workspaceId, input.identityId);
|
|
310
|
+
if (!identity) throw new BrowserIdentityNotFoundError("BrowserIdentity not found");
|
|
311
|
+
const revisions = await materializeRevisions(
|
|
312
|
+
scopedDb,
|
|
313
|
+
await loadRevisionRows(scopedDb, input.workspaceId, { identityId: input.identityId }),
|
|
314
|
+
);
|
|
315
|
+
return BrowserRevisionListResponse.parse({ identity: identityFromRow(identity), revisions });
|
|
316
|
+
},
|
|
317
|
+
CONSISTENT_READ,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export async function createBrowserIdentity(
|
|
322
|
+
db: Database,
|
|
323
|
+
input: {
|
|
324
|
+
accountId: string;
|
|
325
|
+
workspaceId: string;
|
|
326
|
+
operationId: string;
|
|
327
|
+
name: string;
|
|
328
|
+
actorSubjectId: string;
|
|
329
|
+
},
|
|
330
|
+
): Promise<BrowserIdentityMutationResponseValue> {
|
|
331
|
+
const name = input.name.trim();
|
|
332
|
+
if (!name || Buffer.byteLength(name) > 200) {
|
|
333
|
+
throw new BrowserIdentityStateError("BrowserIdentity name is invalid");
|
|
334
|
+
}
|
|
335
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
336
|
+
try {
|
|
337
|
+
return await scopedDb.transaction(async (txRaw) => {
|
|
338
|
+
const tx = txRaw as unknown as Database;
|
|
339
|
+
const id = randomUUID();
|
|
340
|
+
const [inserted] = await tx
|
|
341
|
+
.insert(schema.browserIdentities)
|
|
342
|
+
.values({
|
|
343
|
+
id,
|
|
344
|
+
accountId: input.accountId,
|
|
345
|
+
workspaceId: input.workspaceId,
|
|
346
|
+
name,
|
|
347
|
+
createOperationId: input.operationId,
|
|
348
|
+
createdBySubjectId: input.actorSubjectId,
|
|
349
|
+
})
|
|
350
|
+
.onConflictDoNothing({
|
|
351
|
+
target: [
|
|
352
|
+
schema.browserIdentities.workspaceId,
|
|
353
|
+
schema.browserIdentities.createOperationId,
|
|
354
|
+
],
|
|
355
|
+
})
|
|
356
|
+
.returning();
|
|
357
|
+
const row =
|
|
358
|
+
inserted ??
|
|
359
|
+
(
|
|
360
|
+
await tx
|
|
361
|
+
.select()
|
|
362
|
+
.from(schema.browserIdentities)
|
|
363
|
+
.where(
|
|
364
|
+
and(
|
|
365
|
+
eq(schema.browserIdentities.workspaceId, input.workspaceId),
|
|
366
|
+
eq(schema.browserIdentities.createOperationId, input.operationId),
|
|
367
|
+
),
|
|
368
|
+
)
|
|
369
|
+
.limit(1)
|
|
370
|
+
)[0];
|
|
371
|
+
if (!row) throw new Error("BrowserIdentity create operation was lost");
|
|
372
|
+
const replayed = inserted === undefined;
|
|
373
|
+
if (row.name !== name || row.createdBySubjectId !== input.actorSubjectId) {
|
|
374
|
+
throw new BrowserIdentityConflictError(
|
|
375
|
+
"BrowserIdentity operation id is already bound to another request",
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (!replayed) {
|
|
379
|
+
await advanceWorkspaceInteractionRevision(tx, input.accountId, input.workspaceId);
|
|
380
|
+
}
|
|
381
|
+
return BrowserIdentityMutationResponse.parse({
|
|
382
|
+
identity: identityFromRow(row),
|
|
383
|
+
operationId: input.operationId,
|
|
384
|
+
replayed,
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
} catch (error) {
|
|
388
|
+
if (postgresConstraint(error) === "browser_identities_workspace_active_name_uq") {
|
|
389
|
+
throw new BrowserIdentityConflictError("An active browser profile already uses this name");
|
|
390
|
+
}
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function browserRevisionPublicationRequestDigest(input: {
|
|
397
|
+
browserSessionId: string;
|
|
398
|
+
identityId: string;
|
|
399
|
+
expectedHeadGeneration: number;
|
|
400
|
+
advanceDefault: boolean;
|
|
401
|
+
actorSubjectId: string;
|
|
402
|
+
}): string {
|
|
403
|
+
return createHash("sha256")
|
|
404
|
+
.update(
|
|
405
|
+
JSON.stringify({
|
|
406
|
+
version: 1,
|
|
407
|
+
browserSessionId: input.browserSessionId,
|
|
408
|
+
identityId: input.identityId,
|
|
409
|
+
expectedHeadGeneration: input.expectedHeadGeneration,
|
|
410
|
+
advanceDefault: input.advanceDefault,
|
|
411
|
+
actorSubjectId: input.actorSubjectId,
|
|
412
|
+
}),
|
|
413
|
+
)
|
|
414
|
+
.digest("hex");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export async function prepareBrowserRevisionPublication(
|
|
418
|
+
db: Database,
|
|
419
|
+
input: {
|
|
420
|
+
accountId: string;
|
|
421
|
+
workspaceId: string;
|
|
422
|
+
operationId: string;
|
|
423
|
+
browserSessionId: string;
|
|
424
|
+
identityId: string;
|
|
425
|
+
expectedHeadGeneration: number;
|
|
426
|
+
advanceDefault: boolean;
|
|
427
|
+
actorSubjectId: string;
|
|
428
|
+
},
|
|
429
|
+
): Promise<BrowserRevisionPublicationPreparation> {
|
|
430
|
+
const digest = browserRevisionPublicationRequestDigest(input);
|
|
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
|
+
|
|
439
|
+
// Resolve a terminal idempotency replay before consulting mutable
|
|
440
|
+
// session/identity state. Ending a session cannot invalidate its
|
|
441
|
+
// already-completed publication receipt.
|
|
442
|
+
const existing = await loadInteractionOperation(tx, input.workspaceId, input.operationId);
|
|
443
|
+
if (existing) {
|
|
444
|
+
assertPublicationOperation(existing, input.browserSessionId, digest);
|
|
445
|
+
if (existing.state === "completed") {
|
|
446
|
+
const response = await loadPublicationResponse(
|
|
447
|
+
tx,
|
|
448
|
+
input.workspaceId,
|
|
449
|
+
input.operationId,
|
|
450
|
+
true,
|
|
451
|
+
);
|
|
452
|
+
if (!response) throw new Error("Completed publication has no BrowserRevision");
|
|
453
|
+
return { kind: "completed", response };
|
|
454
|
+
}
|
|
455
|
+
if (existing.state === "failed" || existing.state === "outcome_unknown") {
|
|
456
|
+
throw new BrowserIdentityStateError(
|
|
457
|
+
existing.errorMessage ?? "BrowserRevision publication did not complete",
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Every operation that competes for a BrowserSession first locks the
|
|
463
|
+
// resource row. Dispatch, settlement, and end therefore cannot form
|
|
464
|
+
// an operation-row/resource-row lock cycle.
|
|
465
|
+
await lockBrowserSession(tx, input.workspaceId, input.browserSessionId);
|
|
466
|
+
const session = await loadBrowserSessionRow(
|
|
467
|
+
tx,
|
|
468
|
+
input.workspaceId,
|
|
469
|
+
input.browserSessionId,
|
|
470
|
+
);
|
|
471
|
+
let operation = existing;
|
|
472
|
+
if (operation) {
|
|
473
|
+
await lockInteractionOperation(tx, input.workspaceId, input.operationId);
|
|
474
|
+
operation = await loadInteractionOperation(tx, input.workspaceId, input.operationId);
|
|
475
|
+
assertPublicationOperation(operation, input.browserSessionId, digest);
|
|
476
|
+
if (operation.state === "completed") {
|
|
477
|
+
const response = await loadPublicationResponse(
|
|
478
|
+
tx,
|
|
479
|
+
input.workspaceId,
|
|
480
|
+
input.operationId,
|
|
481
|
+
true,
|
|
482
|
+
);
|
|
483
|
+
if (!response) throw new Error("Completed publication has no BrowserRevision");
|
|
484
|
+
return { kind: "completed", response };
|
|
485
|
+
}
|
|
486
|
+
if (operation.state === "failed" || operation.state === "outcome_unknown") {
|
|
487
|
+
throw new BrowserIdentityStateError(
|
|
488
|
+
operation.errorMessage ?? "BrowserRevision publication did not complete",
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (!session) throw new BrowserIdentityNotFoundError("BrowserSession not found");
|
|
493
|
+
if (session.lifecycle !== "active" || !session.controllerGeneration) {
|
|
494
|
+
throw new BrowserIdentityStateError("BrowserSession is not active");
|
|
495
|
+
}
|
|
496
|
+
if (!BrowserSessionCapabilities.parse(session.capabilities).identityPublication) {
|
|
497
|
+
throw new BrowserIdentityStateError(
|
|
498
|
+
"BrowserSession does not support reusable profile publication",
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const identity = await loadIdentity(tx, input.workspaceId, input.identityId);
|
|
502
|
+
if (!identity) throw new BrowserIdentityNotFoundError("BrowserIdentity not found");
|
|
503
|
+
if (identity.status !== "active") {
|
|
504
|
+
throw new BrowserIdentityStateError("BrowserIdentity is archived");
|
|
505
|
+
}
|
|
506
|
+
if (!operation) {
|
|
507
|
+
const [inserted] = await tx
|
|
508
|
+
.insert(schema.interactionOperations)
|
|
509
|
+
.values({
|
|
510
|
+
operationId: input.operationId,
|
|
511
|
+
accountId: input.accountId,
|
|
512
|
+
workspaceId: input.workspaceId,
|
|
513
|
+
resourceKind: "browser_session",
|
|
514
|
+
resourceId: input.browserSessionId,
|
|
515
|
+
kind: "publish",
|
|
516
|
+
requestDigest: digest,
|
|
517
|
+
actorSubjectId: input.actorSubjectId,
|
|
518
|
+
})
|
|
519
|
+
.onConflictDoNothing({ target: schema.interactionOperations.operationId })
|
|
520
|
+
.returning();
|
|
521
|
+
operation =
|
|
522
|
+
inserted ??
|
|
523
|
+
(await loadInteractionOperation(tx, input.workspaceId, input.operationId));
|
|
524
|
+
}
|
|
525
|
+
if (!operation) {
|
|
526
|
+
throw new BrowserIdentityConflictError(
|
|
527
|
+
"Publication operation id belongs to another workspace",
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
assertPublicationOperation(operation, input.browserSessionId, digest);
|
|
531
|
+
if (
|
|
532
|
+
operation.controllerGeneration &&
|
|
533
|
+
operation.controllerGeneration !== session.controllerGeneration
|
|
534
|
+
) {
|
|
535
|
+
throw new BrowserIdentityConflictError("Publication belongs to a stale controller");
|
|
536
|
+
}
|
|
537
|
+
if (operation.state !== "prepared" && operation.state !== "dispatched") {
|
|
538
|
+
throw new BrowserIdentityStateError(
|
|
539
|
+
"BrowserRevision publication has an invalid active state",
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
kind: "pending",
|
|
544
|
+
operationState: operation.state,
|
|
545
|
+
browserSessionId: session.id,
|
|
546
|
+
controllerGeneration: session.controllerGeneration,
|
|
547
|
+
identity: identityFromRow(identity),
|
|
548
|
+
parentRevisionId:
|
|
549
|
+
session.identityId === input.identityId ? session.baseRevisionId : null,
|
|
550
|
+
};
|
|
551
|
+
}),
|
|
552
|
+
);
|
|
553
|
+
} catch (error) {
|
|
554
|
+
if (postgresConstraint(error) === "interaction_operations_active_resource_uq") {
|
|
555
|
+
throw new BrowserIdentityConflictError(
|
|
556
|
+
"BrowserSession already has an active lifecycle operation",
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
throw error;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export async function dispatchBrowserRevisionPublication(
|
|
564
|
+
db: Database,
|
|
565
|
+
input: {
|
|
566
|
+
accountId: string;
|
|
567
|
+
workspaceId: string;
|
|
568
|
+
operationId: string;
|
|
569
|
+
browserSessionId: string;
|
|
570
|
+
controllerGeneration: string;
|
|
571
|
+
identityId: string;
|
|
572
|
+
expectedHeadGeneration: number;
|
|
573
|
+
advanceDefault: boolean;
|
|
574
|
+
actorSubjectId: string;
|
|
575
|
+
},
|
|
576
|
+
): Promise<BrowserRevisionPublicationDispatch> {
|
|
577
|
+
const digest = browserRevisionPublicationRequestDigest(input);
|
|
578
|
+
return await withRlsContext(
|
|
579
|
+
db,
|
|
580
|
+
input,
|
|
581
|
+
async (scopedDb) =>
|
|
582
|
+
await scopedDb.transaction(async (txRaw) => {
|
|
583
|
+
const tx = txRaw as unknown as Database;
|
|
584
|
+
const beforeLock = await loadInteractionOperation(tx, input.workspaceId, input.operationId);
|
|
585
|
+
assertPublicationOperation(beforeLock, input.browserSessionId, digest);
|
|
586
|
+
if (beforeLock.state === "completed") {
|
|
587
|
+
const response = await loadPublicationResponse(
|
|
588
|
+
tx,
|
|
589
|
+
input.workspaceId,
|
|
590
|
+
input.operationId,
|
|
591
|
+
true,
|
|
592
|
+
);
|
|
593
|
+
if (!response) throw new Error("Completed publication has no BrowserRevision");
|
|
594
|
+
return { kind: "completed", response };
|
|
595
|
+
}
|
|
596
|
+
if (beforeLock.state === "failed" || beforeLock.state === "outcome_unknown") {
|
|
597
|
+
throw new BrowserIdentityStateError(
|
|
598
|
+
beforeLock.errorMessage ?? "BrowserRevision publication did not complete",
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
await lockBrowserSession(tx, input.workspaceId, input.browserSessionId);
|
|
603
|
+
const session = await loadBrowserSessionRow(tx, input.workspaceId, input.browserSessionId);
|
|
604
|
+
await lockInteractionOperation(tx, input.workspaceId, input.operationId);
|
|
605
|
+
const operation = await loadInteractionOperation(tx, input.workspaceId, input.operationId);
|
|
606
|
+
assertPublicationOperation(operation, input.browserSessionId, digest);
|
|
607
|
+
if (operation.state === "completed") {
|
|
608
|
+
const response = await loadPublicationResponse(
|
|
609
|
+
tx,
|
|
610
|
+
input.workspaceId,
|
|
611
|
+
input.operationId,
|
|
612
|
+
true,
|
|
613
|
+
);
|
|
614
|
+
if (!response) throw new Error("Completed publication has no BrowserRevision");
|
|
615
|
+
return { kind: "completed", response };
|
|
616
|
+
}
|
|
617
|
+
if (operation.state === "failed" || operation.state === "outcome_unknown") {
|
|
618
|
+
throw new BrowserIdentityStateError(
|
|
619
|
+
operation.errorMessage ?? "BrowserRevision publication did not complete",
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
if (
|
|
623
|
+
!session ||
|
|
624
|
+
session.lifecycle !== "active" ||
|
|
625
|
+
session.controllerGeneration !== input.controllerGeneration
|
|
626
|
+
) {
|
|
627
|
+
throw new BrowserIdentityConflictError(
|
|
628
|
+
"BrowserRevision publication has a stale BrowserSession controller",
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
if (operation.state === "dispatched") {
|
|
632
|
+
if (operation.controllerGeneration !== input.controllerGeneration) {
|
|
633
|
+
throw new BrowserIdentityConflictError(
|
|
634
|
+
"Publication belongs to another controller generation",
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
return {
|
|
638
|
+
kind: "dispatched",
|
|
639
|
+
browserSessionId: input.browserSessionId,
|
|
640
|
+
controllerGeneration: input.controllerGeneration,
|
|
641
|
+
replayed: true,
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const now = new Date();
|
|
646
|
+
const [updated] = await tx
|
|
647
|
+
.update(schema.interactionOperations)
|
|
648
|
+
.set({
|
|
649
|
+
state: "dispatched",
|
|
650
|
+
controllerGeneration: input.controllerGeneration,
|
|
651
|
+
dispatchedAt: now,
|
|
652
|
+
updatedAt: now,
|
|
653
|
+
})
|
|
654
|
+
.where(
|
|
655
|
+
and(
|
|
656
|
+
eq(schema.interactionOperations.workspaceId, input.workspaceId),
|
|
657
|
+
eq(schema.interactionOperations.operationId, input.operationId),
|
|
658
|
+
eq(schema.interactionOperations.state, "prepared"),
|
|
659
|
+
),
|
|
660
|
+
)
|
|
661
|
+
.returning({ operationId: schema.interactionOperations.operationId });
|
|
662
|
+
if (!updated) throw new Error("BrowserRevision publication dispatch was lost");
|
|
663
|
+
return {
|
|
664
|
+
kind: "dispatched",
|
|
665
|
+
browserSessionId: input.browserSessionId,
|
|
666
|
+
controllerGeneration: input.controllerGeneration,
|
|
667
|
+
replayed: false,
|
|
668
|
+
};
|
|
669
|
+
}),
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export async function commitBrowserRevisionPublication(
|
|
674
|
+
db: Database,
|
|
675
|
+
input: {
|
|
676
|
+
accountId: string;
|
|
677
|
+
workspaceId: string;
|
|
678
|
+
operationId: string;
|
|
679
|
+
browserSessionId: string;
|
|
680
|
+
controllerGeneration: string;
|
|
681
|
+
identityId: string;
|
|
682
|
+
expectedHeadGeneration: number;
|
|
683
|
+
advanceDefault: boolean;
|
|
684
|
+
actorSubjectId: string;
|
|
685
|
+
manifestDigest: string;
|
|
686
|
+
artifacts: readonly BrowserStateArtifactCommitInput[];
|
|
687
|
+
},
|
|
688
|
+
): Promise<PublishBrowserRevisionResponseValue> {
|
|
689
|
+
const digest = browserRevisionPublicationRequestDigest(input);
|
|
690
|
+
const artifacts = validateArtifacts(input.workspaceId, input.artifacts);
|
|
691
|
+
if (!SHA256_PATTERN.test(input.manifestDigest)) {
|
|
692
|
+
throw new BrowserIdentityStateError("BrowserRevision manifest digest is invalid");
|
|
693
|
+
}
|
|
694
|
+
return await withRlsContext(
|
|
695
|
+
db,
|
|
696
|
+
input,
|
|
697
|
+
async (scopedDb) =>
|
|
698
|
+
await scopedDb.transaction(async (txRaw) => {
|
|
699
|
+
const tx = txRaw as unknown as Database;
|
|
700
|
+
const beforeLock = await loadInteractionOperation(tx, input.workspaceId, input.operationId);
|
|
701
|
+
assertPublicationOperation(beforeLock, input.browserSessionId, digest);
|
|
702
|
+
if (beforeLock.state === "completed") {
|
|
703
|
+
const replayed = await loadPublicationResponse(
|
|
704
|
+
tx,
|
|
705
|
+
input.workspaceId,
|
|
706
|
+
input.operationId,
|
|
707
|
+
true,
|
|
708
|
+
);
|
|
709
|
+
if (!replayed) throw new Error("Completed publication has no BrowserRevision");
|
|
710
|
+
return replayed;
|
|
711
|
+
}
|
|
712
|
+
if (beforeLock.state === "failed" || beforeLock.state === "outcome_unknown") {
|
|
713
|
+
throw new BrowserIdentityStateError(
|
|
714
|
+
beforeLock.errorMessage ?? "BrowserRevision publication did not complete",
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
await lockBrowserSession(tx, input.workspaceId, input.browserSessionId);
|
|
719
|
+
const session = await loadBrowserSessionRow(tx, input.workspaceId, input.browserSessionId);
|
|
720
|
+
await lockInteractionOperation(tx, input.workspaceId, input.operationId);
|
|
721
|
+
const operation = await loadInteractionOperation(tx, input.workspaceId, input.operationId);
|
|
722
|
+
assertPublicationOperation(operation, input.browserSessionId, digest);
|
|
723
|
+
if (operation.state === "completed") {
|
|
724
|
+
const replayed = await loadPublicationResponse(
|
|
725
|
+
tx,
|
|
726
|
+
input.workspaceId,
|
|
727
|
+
input.operationId,
|
|
728
|
+
true,
|
|
729
|
+
);
|
|
730
|
+
if (!replayed) throw new Error("Completed publication has no BrowserRevision");
|
|
731
|
+
return replayed;
|
|
732
|
+
}
|
|
733
|
+
if (operation.state === "failed" || operation.state === "outcome_unknown") {
|
|
734
|
+
throw new BrowserIdentityStateError(
|
|
735
|
+
operation.errorMessage ?? "BrowserRevision publication did not complete",
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
if (
|
|
739
|
+
!session ||
|
|
740
|
+
session.lifecycle !== "active" ||
|
|
741
|
+
session.controllerGeneration !== input.controllerGeneration
|
|
742
|
+
) {
|
|
743
|
+
throw new BrowserIdentityStateError("BrowserSession controller is no longer active");
|
|
744
|
+
}
|
|
745
|
+
if (operation.state !== "dispatched") {
|
|
746
|
+
throw new BrowserIdentityStateError("BrowserRevision publication is not dispatched");
|
|
747
|
+
}
|
|
748
|
+
if (operation.controllerGeneration !== input.controllerGeneration) {
|
|
749
|
+
throw new BrowserIdentityConflictError("Publication controller generation changed");
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
await lockBrowserIdentity(tx, input.workspaceId, input.identityId);
|
|
753
|
+
const identity = await loadIdentity(tx, input.workspaceId, input.identityId);
|
|
754
|
+
if (!identity) throw new BrowserIdentityNotFoundError("BrowserIdentity not found");
|
|
755
|
+
if (identity.status !== "active") {
|
|
756
|
+
throw new BrowserIdentityStateError("BrowserIdentity is archived");
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const now = new Date();
|
|
760
|
+
const revisionId = randomUUID();
|
|
761
|
+
const ordinal = safeInteger(identity.revisionCount + 1, "next BrowserRevision ordinal");
|
|
762
|
+
const parentRevisionId =
|
|
763
|
+
session.identityId === input.identityId ? session.baseRevisionId : null;
|
|
764
|
+
const defaultAdvanced =
|
|
765
|
+
input.advanceDefault && identity.headGeneration === input.expectedHeadGeneration;
|
|
766
|
+
const resultHeadGeneration = defaultAdvanced
|
|
767
|
+
? safeInteger(identity.headGeneration + 1, "next BrowserIdentity head generation")
|
|
768
|
+
: identity.headGeneration;
|
|
769
|
+
|
|
770
|
+
const artifactRows: BrowserStateArtifactRow[] = [];
|
|
771
|
+
for (const artifact of artifacts) {
|
|
772
|
+
const [row] = await tx
|
|
773
|
+
.insert(schema.browserStateArtifacts)
|
|
774
|
+
.values({
|
|
775
|
+
id: randomUUID(),
|
|
776
|
+
accountId: input.accountId,
|
|
777
|
+
workspaceId: input.workspaceId,
|
|
778
|
+
sourceBrowserSessionId: input.browserSessionId,
|
|
779
|
+
purpose: "revision_component",
|
|
780
|
+
...artifact,
|
|
781
|
+
})
|
|
782
|
+
.returning();
|
|
783
|
+
if (!row) throw new Error("Browser state artifact insert was lost");
|
|
784
|
+
artifactRows.push(row);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const [revisionRow] = await tx
|
|
788
|
+
.insert(schema.browserRevisions)
|
|
789
|
+
.values({
|
|
790
|
+
id: revisionId,
|
|
791
|
+
accountId: input.accountId,
|
|
792
|
+
workspaceId: input.workspaceId,
|
|
793
|
+
identityId: input.identityId,
|
|
794
|
+
parentRevisionId,
|
|
795
|
+
ordinal,
|
|
796
|
+
sourceBrowserSessionId: input.browserSessionId,
|
|
797
|
+
publicationOperationId: input.operationId,
|
|
798
|
+
expectedHeadGeneration: input.expectedHeadGeneration,
|
|
799
|
+
advanceDefaultRequested: input.advanceDefault,
|
|
800
|
+
defaultAdvanced,
|
|
801
|
+
resultHeadGeneration,
|
|
802
|
+
manifestDigest: input.manifestDigest,
|
|
803
|
+
createdBySubjectId: input.actorSubjectId,
|
|
804
|
+
createdAt: now,
|
|
805
|
+
})
|
|
806
|
+
.returning();
|
|
807
|
+
if (!revisionRow) throw new Error("BrowserRevision insert was lost");
|
|
808
|
+
|
|
809
|
+
const componentRows: BrowserRevisionComponentRow[] = [];
|
|
810
|
+
for (const [position, artifact] of artifactRows.entries()) {
|
|
811
|
+
const [component] = await tx
|
|
812
|
+
.insert(schema.browserRevisionComponents)
|
|
813
|
+
.values({
|
|
814
|
+
id: randomUUID(),
|
|
815
|
+
accountId: input.accountId,
|
|
816
|
+
workspaceId: input.workspaceId,
|
|
817
|
+
identityId: input.identityId,
|
|
818
|
+
revisionId,
|
|
819
|
+
artifactId: artifact.id,
|
|
820
|
+
sourceBrowserSessionId: input.browserSessionId,
|
|
821
|
+
artifactPurpose: "revision_component",
|
|
822
|
+
kind: artifact.kind,
|
|
823
|
+
position,
|
|
824
|
+
createdAt: now,
|
|
825
|
+
})
|
|
826
|
+
.returning();
|
|
827
|
+
if (!component) throw new Error("BrowserRevision component insert was lost");
|
|
828
|
+
componentRows.push(component);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const [identityRow] = await tx
|
|
832
|
+
.update(schema.browserIdentities)
|
|
833
|
+
.set({
|
|
834
|
+
revisionCount: ordinal,
|
|
835
|
+
...(defaultAdvanced
|
|
836
|
+
? { defaultRevisionId: revisionId, headGeneration: resultHeadGeneration }
|
|
837
|
+
: {}),
|
|
838
|
+
updatedAt: now,
|
|
839
|
+
})
|
|
840
|
+
.where(
|
|
841
|
+
and(
|
|
842
|
+
eq(schema.browserIdentities.workspaceId, input.workspaceId),
|
|
843
|
+
eq(schema.browserIdentities.id, input.identityId),
|
|
844
|
+
),
|
|
845
|
+
)
|
|
846
|
+
.returning();
|
|
847
|
+
if (!identityRow) throw new Error("BrowserIdentity publication update was lost");
|
|
848
|
+
|
|
849
|
+
if (session.privateCheckpointArtifactId) {
|
|
850
|
+
const [retired] = await tx
|
|
851
|
+
.update(schema.browserStateArtifacts)
|
|
852
|
+
.set({ state: "delete_pending", retainedUntil: now })
|
|
853
|
+
.where(
|
|
854
|
+
and(
|
|
855
|
+
eq(schema.browserStateArtifacts.workspaceId, input.workspaceId),
|
|
856
|
+
eq(schema.browserStateArtifacts.id, session.privateCheckpointArtifactId),
|
|
857
|
+
eq(schema.browserStateArtifacts.sourceBrowserSessionId, input.browserSessionId),
|
|
858
|
+
eq(schema.browserStateArtifacts.purpose, "private_checkpoint"),
|
|
859
|
+
eq(schema.browserStateArtifacts.state, "available"),
|
|
860
|
+
),
|
|
861
|
+
)
|
|
862
|
+
.returning({ id: schema.browserStateArtifacts.id });
|
|
863
|
+
if (!retired) {
|
|
864
|
+
throw new BrowserIdentityStateError(
|
|
865
|
+
"BrowserSession private checkpoint authority is inconsistent",
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const [sessionRow] = await tx
|
|
871
|
+
.update(schema.browserSessions)
|
|
872
|
+
.set({
|
|
873
|
+
identityId: input.identityId,
|
|
874
|
+
baseRevisionId: revisionId,
|
|
875
|
+
privateCheckpointArtifactId: null,
|
|
876
|
+
lastUsedAt: now,
|
|
877
|
+
updatedAt: now,
|
|
878
|
+
})
|
|
879
|
+
.where(
|
|
880
|
+
and(
|
|
881
|
+
eq(schema.browserSessions.workspaceId, input.workspaceId),
|
|
882
|
+
eq(schema.browserSessions.id, input.browserSessionId),
|
|
883
|
+
eq(schema.browserSessions.controllerGeneration, input.controllerGeneration),
|
|
884
|
+
eq(schema.browserSessions.lifecycle, "active"),
|
|
885
|
+
),
|
|
886
|
+
)
|
|
887
|
+
.returning({ id: schema.browserSessions.id });
|
|
888
|
+
if (!sessionRow)
|
|
889
|
+
throw new BrowserIdentityStateError("BrowserSession publication fence changed");
|
|
890
|
+
|
|
891
|
+
const [settled] = await tx
|
|
892
|
+
.update(schema.interactionOperations)
|
|
893
|
+
.set({ state: "completed", settledAt: now, updatedAt: now })
|
|
894
|
+
.where(
|
|
895
|
+
and(
|
|
896
|
+
eq(schema.interactionOperations.workspaceId, input.workspaceId),
|
|
897
|
+
eq(schema.interactionOperations.operationId, input.operationId),
|
|
898
|
+
eq(schema.interactionOperations.state, "dispatched"),
|
|
899
|
+
),
|
|
900
|
+
)
|
|
901
|
+
.returning({ operationId: schema.interactionOperations.operationId });
|
|
902
|
+
if (!settled) throw new Error("BrowserRevision publication settlement was lost");
|
|
903
|
+
await advanceWorkspaceInteractionRevision(tx, input.accountId, input.workspaceId);
|
|
904
|
+
|
|
905
|
+
const artifactMap = new Map(artifactRows.map((artifact) => [artifact.id, artifact]));
|
|
906
|
+
return PublishBrowserRevisionResponse.parse({
|
|
907
|
+
identity: identityFromRow(identityRow),
|
|
908
|
+
revision: revisionFromRows(revisionRow, componentRows, artifactMap),
|
|
909
|
+
outcome: defaultAdvanced ? "saved_as_default" : "saved_not_default",
|
|
910
|
+
replayed: false,
|
|
911
|
+
});
|
|
912
|
+
}),
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
export async function failBrowserRevisionPublication(
|
|
917
|
+
db: Database,
|
|
918
|
+
input: {
|
|
919
|
+
accountId: string;
|
|
920
|
+
workspaceId: string;
|
|
921
|
+
operationId: string;
|
|
922
|
+
browserSessionId: string;
|
|
923
|
+
controllerGeneration: string;
|
|
924
|
+
state?: "failed" | "outcome_unknown";
|
|
925
|
+
error: InteractionErrorValue;
|
|
926
|
+
},
|
|
927
|
+
): Promise<void> {
|
|
928
|
+
const error = InteractionError.parse(input.error);
|
|
929
|
+
await withRlsContext(
|
|
930
|
+
db,
|
|
931
|
+
input,
|
|
932
|
+
async (scopedDb) =>
|
|
933
|
+
await scopedDb.transaction(async (txRaw) => {
|
|
934
|
+
const tx = txRaw as unknown as Database;
|
|
935
|
+
await lockInteractionOperation(tx, input.workspaceId, input.operationId);
|
|
936
|
+
const operation = await loadInteractionOperation(tx, input.workspaceId, input.operationId);
|
|
937
|
+
if (!operation) throw new BrowserIdentityNotFoundError("Publication operation not found");
|
|
938
|
+
if (
|
|
939
|
+
operation.resourceKind !== "browser_session" ||
|
|
940
|
+
operation.resourceId !== input.browserSessionId ||
|
|
941
|
+
operation.kind !== "publish"
|
|
942
|
+
) {
|
|
943
|
+
throw new BrowserIdentityConflictError("Operation belongs to another resource");
|
|
944
|
+
}
|
|
945
|
+
if (
|
|
946
|
+
operation.state === "completed" ||
|
|
947
|
+
operation.state === "failed" ||
|
|
948
|
+
operation.state === "outcome_unknown"
|
|
949
|
+
) {
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
if (
|
|
953
|
+
operation.state !== "dispatched" ||
|
|
954
|
+
operation.controllerGeneration !== input.controllerGeneration
|
|
955
|
+
) {
|
|
956
|
+
throw new BrowserIdentityConflictError(
|
|
957
|
+
"Publication failure belongs to another controller generation",
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
const state = input.state ?? "failed";
|
|
961
|
+
const now = new Date();
|
|
962
|
+
const [settled] = await tx
|
|
963
|
+
.update(schema.interactionOperations)
|
|
964
|
+
.set({
|
|
965
|
+
state,
|
|
966
|
+
errorCode: error.code,
|
|
967
|
+
errorMessage: error.message,
|
|
968
|
+
errorRetryable: error.retryable,
|
|
969
|
+
errorDetails: error.details ?? null,
|
|
970
|
+
settledAt: now,
|
|
971
|
+
updatedAt: now,
|
|
972
|
+
})
|
|
973
|
+
.where(
|
|
974
|
+
and(
|
|
975
|
+
eq(schema.interactionOperations.workspaceId, input.workspaceId),
|
|
976
|
+
eq(schema.interactionOperations.operationId, input.operationId),
|
|
977
|
+
eq(schema.interactionOperations.state, "dispatched"),
|
|
978
|
+
eq(schema.interactionOperations.controllerGeneration, input.controllerGeneration),
|
|
979
|
+
),
|
|
980
|
+
)
|
|
981
|
+
.returning({ operationId: schema.interactionOperations.operationId });
|
|
982
|
+
if (!settled) throw new Error("BrowserRevision publication failure settlement was lost");
|
|
983
|
+
}),
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/** Server-only storage authority used to restore a revision. Never project this
|
|
988
|
+
* result through HTTP, MCP, Codemode output, or the public SDK. */
|
|
989
|
+
export async function getBrowserRevisionArtifactAuthority(
|
|
990
|
+
db: Database,
|
|
991
|
+
input: {
|
|
992
|
+
accountId: string;
|
|
993
|
+
workspaceId: string;
|
|
994
|
+
identityId: string;
|
|
995
|
+
revisionId: string;
|
|
996
|
+
},
|
|
997
|
+
): Promise<BrowserRevisionArtifactAuthority> {
|
|
998
|
+
return await withRlsContext(
|
|
999
|
+
db,
|
|
1000
|
+
input,
|
|
1001
|
+
async (scopedDb) =>
|
|
1002
|
+
await loadBrowserRevisionArtifactAuthority(scopedDb, {
|
|
1003
|
+
workspaceId: input.workspaceId,
|
|
1004
|
+
identityId: input.identityId,
|
|
1005
|
+
revisionId: input.revisionId,
|
|
1006
|
+
}),
|
|
1007
|
+
CONSISTENT_READ,
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
async function loadBrowserRevisionArtifactAuthority(
|
|
1012
|
+
db: Database,
|
|
1013
|
+
input: { workspaceId: string; identityId: string; revisionId: string },
|
|
1014
|
+
): Promise<BrowserRevisionArtifactAuthority> {
|
|
1015
|
+
const rows = await loadRevisionRows(db, input.workspaceId, {
|
|
1016
|
+
identityId: input.identityId,
|
|
1017
|
+
revisionId: input.revisionId,
|
|
1018
|
+
});
|
|
1019
|
+
const row = rows[0];
|
|
1020
|
+
if (!row) throw new BrowserIdentityNotFoundError("BrowserRevision not found");
|
|
1021
|
+
const components = await db
|
|
1022
|
+
.select()
|
|
1023
|
+
.from(schema.browserRevisionComponents)
|
|
1024
|
+
.where(eq(schema.browserRevisionComponents.revisionId, row.id))
|
|
1025
|
+
.orderBy(asc(schema.browserRevisionComponents.position));
|
|
1026
|
+
if (components.length === 0) {
|
|
1027
|
+
throw new BrowserIdentityStateError("BrowserRevision has no state components");
|
|
1028
|
+
}
|
|
1029
|
+
const artifacts = await db
|
|
1030
|
+
.select()
|
|
1031
|
+
.from(schema.browserStateArtifacts)
|
|
1032
|
+
.where(
|
|
1033
|
+
inArray(
|
|
1034
|
+
schema.browserStateArtifacts.id,
|
|
1035
|
+
components.map((component) => component.artifactId),
|
|
1036
|
+
),
|
|
1037
|
+
);
|
|
1038
|
+
const byArtifact = new Map(artifacts.map((artifact) => [artifact.id, artifact]));
|
|
1039
|
+
const revision = revisionFromRows(row, components, byArtifact);
|
|
1040
|
+
return {
|
|
1041
|
+
revision,
|
|
1042
|
+
artifacts: components.map((component) => {
|
|
1043
|
+
const artifact = byArtifact.get(component.artifactId);
|
|
1044
|
+
if (!artifact || artifact.state !== "available") {
|
|
1045
|
+
throw new BrowserIdentityStateError("BrowserRevision artifact is unavailable");
|
|
1046
|
+
}
|
|
1047
|
+
return {
|
|
1048
|
+
componentId: component.id,
|
|
1049
|
+
artifactId: artifact.id,
|
|
1050
|
+
objectKey: artifact.objectKey,
|
|
1051
|
+
encryptedDataKey: artifact.encryptedDataKey,
|
|
1052
|
+
contentDigest: artifact.contentDigest,
|
|
1053
|
+
manifestDigest: artifact.manifestDigest,
|
|
1054
|
+
artifactDigest: artifact.artifactDigest,
|
|
1055
|
+
sizeBytes: safeInteger(artifact.sizeBytes, "browser artifact size"),
|
|
1056
|
+
format: artifact.format,
|
|
1057
|
+
materialization: BrowserRevisionMaterialization.parse(artifact.materialization),
|
|
1058
|
+
};
|
|
1059
|
+
}),
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function validateArtifacts(
|
|
1064
|
+
workspaceId: string,
|
|
1065
|
+
values: readonly BrowserStateArtifactCommitInput[],
|
|
1066
|
+
): BrowserStateArtifactCommitInput[] {
|
|
1067
|
+
if (values.length < 1 || values.length > MAX_COMPONENTS) {
|
|
1068
|
+
throw new BrowserIdentityStateError("BrowserRevision component count is invalid");
|
|
1069
|
+
}
|
|
1070
|
+
const seenKinds = new Set<BrowserStateArtifactCommitInput["kind"]>();
|
|
1071
|
+
return values.map((value) => {
|
|
1072
|
+
let validated: BrowserStateArtifactCommitInput;
|
|
1073
|
+
try {
|
|
1074
|
+
validated = validateBrowserStateArtifactCommitInput(workspaceId, value);
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
throw new BrowserIdentityStateError(
|
|
1077
|
+
error instanceof Error ? error.message : "BrowserRevision artifact metadata is invalid",
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
if (seenKinds.has(value.kind)) {
|
|
1081
|
+
throw new BrowserIdentityStateError("BrowserRevision component kinds must be unique");
|
|
1082
|
+
}
|
|
1083
|
+
seenKinds.add(value.kind);
|
|
1084
|
+
return validated;
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
async function loadBrowserSessionRow(
|
|
1089
|
+
db: Database,
|
|
1090
|
+
workspaceId: string,
|
|
1091
|
+
browserSessionId: string,
|
|
1092
|
+
): Promise<BrowserSessionRow | null> {
|
|
1093
|
+
const [row] = await db
|
|
1094
|
+
.select()
|
|
1095
|
+
.from(schema.browserSessions)
|
|
1096
|
+
.where(
|
|
1097
|
+
and(
|
|
1098
|
+
eq(schema.browserSessions.workspaceId, workspaceId),
|
|
1099
|
+
eq(schema.browserSessions.id, browserSessionId),
|
|
1100
|
+
),
|
|
1101
|
+
)
|
|
1102
|
+
.limit(1);
|
|
1103
|
+
return row ?? null;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
async function loadInteractionOperation(
|
|
1107
|
+
db: Database,
|
|
1108
|
+
workspaceId: string,
|
|
1109
|
+
operationId: string,
|
|
1110
|
+
): Promise<InteractionOperationRow | null> {
|
|
1111
|
+
const [row] = await db
|
|
1112
|
+
.select()
|
|
1113
|
+
.from(schema.interactionOperations)
|
|
1114
|
+
.where(
|
|
1115
|
+
and(
|
|
1116
|
+
eq(schema.interactionOperations.workspaceId, workspaceId),
|
|
1117
|
+
eq(schema.interactionOperations.operationId, operationId),
|
|
1118
|
+
),
|
|
1119
|
+
)
|
|
1120
|
+
.limit(1);
|
|
1121
|
+
return row ?? null;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
function assertPublicationOperation(
|
|
1125
|
+
operation: InteractionOperationRow | null,
|
|
1126
|
+
browserSessionId: string,
|
|
1127
|
+
digest: string,
|
|
1128
|
+
): asserts operation is InteractionOperationRow {
|
|
1129
|
+
if (!operation) throw new BrowserIdentityNotFoundError("Publication operation not found");
|
|
1130
|
+
if (
|
|
1131
|
+
operation.resourceKind !== "browser_session" ||
|
|
1132
|
+
operation.resourceId !== browserSessionId ||
|
|
1133
|
+
operation.kind !== "publish" ||
|
|
1134
|
+
operation.requestDigest !== digest
|
|
1135
|
+
) {
|
|
1136
|
+
throw new BrowserIdentityConflictError(
|
|
1137
|
+
"Publication operation id is already bound to another request",
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
async function lockBrowserIdentity(db: Database, workspaceId: string, identityId: string) {
|
|
1143
|
+
await db.execute(sql`
|
|
1144
|
+
select id from browser_identities
|
|
1145
|
+
where workspace_id = ${workspaceId} and id = ${identityId}
|
|
1146
|
+
for update
|
|
1147
|
+
`);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
async function lockBrowserSession(db: Database, workspaceId: string, browserSessionId: string) {
|
|
1151
|
+
await db.execute(sql`
|
|
1152
|
+
select id from browser_sessions
|
|
1153
|
+
where workspace_id = ${workspaceId} and id = ${browserSessionId}
|
|
1154
|
+
for update
|
|
1155
|
+
`);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
async function lockInteractionOperation(db: Database, workspaceId: string, operationId: string) {
|
|
1159
|
+
await db.execute(sql`
|
|
1160
|
+
select operation_id from interaction_operations
|
|
1161
|
+
where workspace_id = ${workspaceId} and operation_id = ${operationId}
|
|
1162
|
+
for update
|
|
1163
|
+
`);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
function postgresConstraint(error: unknown): string | null {
|
|
1167
|
+
return safeDatabaseErrorFacts(error).constraint ?? null;
|
|
1168
|
+
}
|