@opengeni/db 0.31.1 → 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.
- package/dist/browser-auth.d.ts +209 -1
- package/dist/browser-downloads.d.ts +51 -0
- package/dist/browser-identities.d.ts +11 -2
- package/dist/browser-sessions.d.ts +83 -1
- package/dist/browser-state-artifacts.d.ts +75 -1
- package/dist/canonical-human-identities.d.ts +48 -0
- package/dist/canonical-human-identities.js +28 -0
- package/dist/canonical-human-identities.js.map +1 -0
- package/dist/capability-integrations.d.ts +8 -6
- package/dist/{chunk-MWDVXQOL.js → chunk-5KTIEDX7.js} +5773 -4212
- package/dist/chunk-5KTIEDX7.js.map +1 -0
- package/dist/{chunk-NBE6CHRI.js → chunk-CQZ4QSP5.js} +49 -9
- package/dist/chunk-CQZ4QSP5.js.map +1 -0
- package/dist/chunk-F32YT7MP.js +117 -0
- package/dist/chunk-F32YT7MP.js.map +1 -0
- package/dist/{chunk-ISI2TIUG.js → chunk-JC5QPLUM.js} +30 -3
- package/dist/chunk-JC5QPLUM.js.map +1 -0
- package/dist/{chunk-F5FT7BEZ.js → chunk-P3RE4D2B.js} +2 -2
- package/dist/chunk-TAUTWOS5.js +259 -0
- package/dist/chunk-TAUTWOS5.js.map +1 -0
- package/dist/{chunk-YHZXQ7HP.js → chunk-VKLB2ZGI.js} +446 -19
- package/dist/chunk-VKLB2ZGI.js.map +1 -0
- package/dist/database.d.ts +5 -0
- package/dist/editable-artifact-durable-export.js +2 -2
- package/dist/editable-artifacts.js +3 -3
- package/dist/index.d.ts +438 -29
- package/dist/index.js +35640 -28969
- package/dist/index.js.map +1 -1
- package/dist/insights.d.ts +23 -7
- package/dist/integration-bindings.d.ts +24 -24
- package/dist/integration-facets.d.ts +99 -0
- package/dist/interaction-revisions.d.ts +11 -0
- package/dist/interaction-schema.d.ts +698 -34
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +8 -7
- package/dist/schema.d.ts +9464 -5412
- package/dist/schema.js +51 -9
- package/dist/session-control.d.ts +29 -3
- package/dist/session-tenancy.d.ts +44 -0
- package/dist/session-tenancy.js +16 -0
- package/dist/session-tenancy.js.map +1 -0
- package/dist/slack-task-policy-schema.d.ts +847 -0
- package/dist/slack-task-policy.d.ts +65 -0
- package/dist/task-notes-schema.d.ts +924 -0
- package/dist/task-notes.d.ts +85 -0
- package/dist/video-generation.d.ts +9 -0
- package/dist/video-generation.js +5 -3
- package/dist/xai-subscription.d.ts +326 -0
- package/drizzle/0212_browser_state_transfer_hardening.sql +560 -0
- package/drizzle/0213_browser_interaction_authority.sql +67 -0
- package/drizzle/0214_browser_download_saves.sql +34 -0
- package/drizzle/0215_browser_controller_host.sql +385 -0
- package/drizzle/0216_browser_auth_health_evidence.sql +137 -0
- package/drizzle/0217_external_browser_auth_operations.sql +22 -0
- package/drizzle/0218_organization_tenancy_foundation.sql +299 -0
- package/drizzle/0219_organization_tenancy_managed_human_provisioning.sql +317 -0
- package/drizzle/0219_site_auth_maintenance_sessions.sql +234 -0
- package/drizzle/0220_memory_slack_append_only_cascade.sql +38 -0
- package/drizzle/0220_session_channels.sql +54 -0
- package/drizzle/0221_sessions_channel_index.sql +5 -0
- package/drizzle/0222_session_visibility_authority_epochs.sql +158 -0
- package/drizzle/0222_sessions_channel_fk.sql +89 -0
- package/drizzle/0223_pending_tool_event_output.sql +55 -0
- package/drizzle/0223_sessions_channel_fk_validate.sql +118 -0
- package/drizzle/0224_slack_post_outcome_reconciliation.sql +123 -0
- package/drizzle/0225_session_visibility_fork_activation.sql +1363 -0
- package/drizzle/0226_personal_codex_authority_foundation.sql +365 -0
- package/drizzle/0227_slack_native_actions.sql +145 -0
- package/drizzle/0228_interaction_controller_data_plane.sql +14 -0
- package/drizzle/0228_slack_task_policy.sql +400 -0
- package/drizzle/0229_slack_inbox_file_fact.sql +10 -0
- package/drizzle/0230_user_scoped_variable_sets_rigs.sql +84 -0
- package/drizzle/0231_integration_definition_identity_cutover.sql +271 -0
- package/drizzle/0232_integration_facet_authority_cutover.sql +610 -0
- package/drizzle/0233_skill_and_integration_authority_cutover.sql +1162 -0
- package/drizzle/0234_xai_subscription_authority.sql +1139 -0
- package/drizzle/0235_canonical_human_login_bindings.sql +920 -0
- package/drizzle/0236_browser_identity_lifecycle.sql +39 -0
- package/drizzle/0236_session_visibility_slack_policy.sql +72 -0
- package/drizzle/0237_interaction_transition_reaper.sql +263 -0
- package/drizzle/0238_goal_persistence_policy.sql +471 -0
- package/drizzle/0238_supergrok_realtime_model.sql +29 -0
- package/drizzle/0239_supergrok_video_funding.sql +50 -0
- package/drizzle/0239_task_tree_notes.sql +990 -0
- package/package.json +13 -5
- package/src/browser-auth.ts +2600 -123
- package/src/browser-downloads.ts +517 -0
- package/src/browser-identities.ts +220 -3
- package/src/browser-sessions.ts +334 -45
- package/src/browser-state-artifacts.ts +402 -12
- package/src/canonical-human-identities.ts +317 -0
- package/src/capability-integrations.ts +121 -213
- package/src/database.ts +41 -1
- package/src/index.ts +4157 -394
- package/src/insights.ts +109 -28
- package/src/integration-bindings.ts +101 -101
- package/src/{integration-features.ts → integration-facets.ts} +187 -184
- package/src/interaction-revisions.ts +33 -3
- package/src/interaction-schema.ts +311 -8
- package/src/pack-components.ts +43 -43
- package/src/pack-installations.ts +0 -43
- package/src/plugin-packages.ts +9 -9
- package/src/provision-roles.ts +232 -0
- package/src/runtime-posture.ts +227 -14
- package/src/schema.ts +1188 -75
- package/src/session-control.ts +122 -37
- package/src/session-queue-commands.ts +78 -1
- package/src/session-tenancy.ts +188 -0
- package/src/session-tool-call-settlement.ts +9 -3
- package/src/slack-task-policy-schema.ts +130 -0
- package/src/slack-task-policy.ts +341 -0
- package/src/task-notes-schema.ts +135 -0
- package/src/task-notes.ts +178 -0
- package/src/video-generation.ts +90 -12
- package/src/workspace-artifacts.ts +1 -1
- package/src/xai-subscription.ts +1887 -0
- package/dist/chunk-ISI2TIUG.js.map +0 -1
- package/dist/chunk-MWDVXQOL.js.map +0 -1
- package/dist/chunk-NBE6CHRI.js.map +0 -1
- package/dist/chunk-YHZXQ7HP.js.map +0 -1
- package/dist/integration-features.d.ts +0 -99
- /package/dist/{chunk-F5FT7BEZ.js.map → chunk-P3RE4D2B.js.map} +0 -0
package/src/browser-sessions.ts
CHANGED
|
@@ -16,6 +16,11 @@ import {
|
|
|
16
16
|
type InteractionLifecycleOperationKind,
|
|
17
17
|
type InteractionLifecycleOperationReceipt as InteractionLifecycleOperationReceiptValue,
|
|
18
18
|
type InteractionPlacement,
|
|
19
|
+
NetworkRouteConfiguration,
|
|
20
|
+
NetworkRouteConsistency,
|
|
21
|
+
networkRoutePlacementCompatibilityIssue,
|
|
22
|
+
type NetworkRouteConfiguration as NetworkRouteConfigurationValue,
|
|
23
|
+
type NetworkRouteConsistency as NetworkRouteConsistencyValue,
|
|
19
24
|
} from "@opengeni/contracts";
|
|
20
25
|
import { and, desc, eq, inArray, isNull, sql } from "drizzle-orm";
|
|
21
26
|
import { type Database, withRlsContext } from "./database";
|
|
@@ -25,6 +30,9 @@ import {
|
|
|
25
30
|
} from "./interaction-revisions";
|
|
26
31
|
import { safeDatabaseErrorFacts } from "./persistence-errors";
|
|
27
32
|
import {
|
|
33
|
+
commitBrowserStateUploadInTransaction,
|
|
34
|
+
markBrowserStateUploadsDeletePendingInTransaction,
|
|
35
|
+
prepareBrowserStateUploadInTransaction,
|
|
28
36
|
validateBrowserStateArtifactCommitInput,
|
|
29
37
|
type BrowserStateArtifactCommitInput,
|
|
30
38
|
} from "./browser-state-artifacts";
|
|
@@ -45,9 +53,10 @@ export const MANAGED_BROWSER_SESSION_CAPABILITIES = BrowserSessionCapabilities.p
|
|
|
45
53
|
liveFrames: true,
|
|
46
54
|
humanInput: true,
|
|
47
55
|
tabs: true,
|
|
48
|
-
downloads:
|
|
49
|
-
uploads:
|
|
50
|
-
clipboard:
|
|
56
|
+
downloads: true,
|
|
57
|
+
uploads: true,
|
|
58
|
+
clipboard: true,
|
|
59
|
+
permissions: true,
|
|
51
60
|
diagnostics: true,
|
|
52
61
|
rawCdp: false,
|
|
53
62
|
linkedComputer: false,
|
|
@@ -64,7 +73,8 @@ export const ATTACHED_BROWSER_SESSION_CAPABILITIES = BrowserSessionCapabilities.
|
|
|
64
73
|
tabs: true,
|
|
65
74
|
downloads: false,
|
|
66
75
|
uploads: false,
|
|
67
|
-
clipboard:
|
|
76
|
+
clipboard: true,
|
|
77
|
+
permissions: false,
|
|
68
78
|
diagnostics: true,
|
|
69
79
|
rawCdp: false,
|
|
70
80
|
linkedComputer: false,
|
|
@@ -73,6 +83,46 @@ export const ATTACHED_BROWSER_SESSION_CAPABILITIES = BrowserSessionCapabilities.
|
|
|
73
83
|
parallelTargets: true,
|
|
74
84
|
});
|
|
75
85
|
|
|
86
|
+
/** Remote Chromium controlled through the same native CDP driver. Files and
|
|
87
|
+
* portable profile state remain false because their bytes live on another host. */
|
|
88
|
+
export const EXTERNAL_BROWSER_SESSION_CAPABILITIES = BrowserSessionCapabilities.parse({
|
|
89
|
+
semanticObservation: true,
|
|
90
|
+
screenshots: true,
|
|
91
|
+
liveFrames: true,
|
|
92
|
+
humanInput: true,
|
|
93
|
+
tabs: true,
|
|
94
|
+
downloads: false,
|
|
95
|
+
uploads: false,
|
|
96
|
+
clipboard: true,
|
|
97
|
+
permissions: true,
|
|
98
|
+
diagnostics: true,
|
|
99
|
+
rawCdp: false,
|
|
100
|
+
linkedComputer: false,
|
|
101
|
+
privateCheckpoint: false,
|
|
102
|
+
identityPublication: false,
|
|
103
|
+
parallelTargets: true,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/** Measured against Lightpanda 0.3.5. Unsupported Chromium-only affordances
|
|
107
|
+
* remain false instead of being emulated or silently routed elsewhere. */
|
|
108
|
+
export const LIGHTPANDA_BROWSER_SESSION_CAPABILITIES = BrowserSessionCapabilities.parse({
|
|
109
|
+
semanticObservation: true,
|
|
110
|
+
screenshots: true,
|
|
111
|
+
liveFrames: false,
|
|
112
|
+
humanInput: false,
|
|
113
|
+
tabs: false,
|
|
114
|
+
downloads: false,
|
|
115
|
+
uploads: true,
|
|
116
|
+
clipboard: false,
|
|
117
|
+
permissions: false,
|
|
118
|
+
diagnostics: true,
|
|
119
|
+
rawCdp: false,
|
|
120
|
+
linkedComputer: false,
|
|
121
|
+
privateCheckpoint: false,
|
|
122
|
+
identityPublication: false,
|
|
123
|
+
parallelTargets: false,
|
|
124
|
+
});
|
|
125
|
+
|
|
76
126
|
export class BrowserSessionNotFoundError extends Error {
|
|
77
127
|
readonly name = "BrowserSessionNotFoundError";
|
|
78
128
|
}
|
|
@@ -135,7 +185,11 @@ export type BrowserSessionControlRecord = {
|
|
|
135
185
|
session: BrowserSession;
|
|
136
186
|
tokenGeneration: number;
|
|
137
187
|
sourceSessionId: string;
|
|
188
|
+
/** Placement hosting browserd. It equals the logical sandbox placement for
|
|
189
|
+
* managed browsers and remains the source home sandbox for remote browsers. */
|
|
190
|
+
controllerHostSandboxGroupId: string | null;
|
|
138
191
|
createOperationId: string;
|
|
192
|
+
networkRouteAuthority: BrowserSessionNetworkRouteAuthority | null;
|
|
139
193
|
operation: null | {
|
|
140
194
|
operationId: string;
|
|
141
195
|
kind: InteractionLifecycleOperationKind;
|
|
@@ -145,6 +199,15 @@ export type BrowserSessionControlRecord = {
|
|
|
145
199
|
};
|
|
146
200
|
};
|
|
147
201
|
|
|
202
|
+
export type BrowserSessionNetworkRouteAuthority = {
|
|
203
|
+
routeId: string;
|
|
204
|
+
routeVersion: number;
|
|
205
|
+
credentialVersion: number | null;
|
|
206
|
+
authorityDigest: string | null;
|
|
207
|
+
configuration: NetworkRouteConfigurationValue;
|
|
208
|
+
consistency: NetworkRouteConsistencyValue;
|
|
209
|
+
};
|
|
210
|
+
|
|
148
211
|
function iso(value: Date): string {
|
|
149
212
|
return value.toISOString();
|
|
150
213
|
}
|
|
@@ -329,7 +392,7 @@ export function browserSessionCreateRequestDigest(input: PrepareBrowserSessionCr
|
|
|
329
392
|
: { kind: "blank_identity" as const }
|
|
330
393
|
: { kind: "none" as const };
|
|
331
394
|
return requestDigest({
|
|
332
|
-
version:
|
|
395
|
+
version: 4,
|
|
333
396
|
associatedSessionId: input.associatedSessionId,
|
|
334
397
|
name: input.name,
|
|
335
398
|
initialUrl: input.initialUrl,
|
|
@@ -502,7 +565,8 @@ export async function getBrowserPrivateCheckpointAuthority(
|
|
|
502
565
|
!artifact ||
|
|
503
566
|
artifact.sourceBrowserSessionId !== session.id ||
|
|
504
567
|
artifact.purpose !== "private_checkpoint" ||
|
|
505
|
-
artifact.state !== "available"
|
|
568
|
+
artifact.state !== "available" ||
|
|
569
|
+
artifact.encryptedDataKey === null
|
|
506
570
|
) {
|
|
507
571
|
throw new BrowserSessionStateError(
|
|
508
572
|
"BrowserSession private checkpoint authority is inconsistent",
|
|
@@ -578,7 +642,9 @@ async function controlRecordFromRows(
|
|
|
578
642
|
session: browserSessionFromRows(row, associations),
|
|
579
643
|
tokenGeneration: row.tokenGeneration,
|
|
580
644
|
sourceSessionId: createdAssociations[0]!.sessionId,
|
|
645
|
+
controllerHostSandboxGroupId: row.controllerHostSandboxGroupId,
|
|
581
646
|
createOperationId: row.createOperationId,
|
|
647
|
+
networkRouteAuthority: networkRouteAuthorityFromRow(row),
|
|
582
648
|
operation: operation
|
|
583
649
|
? {
|
|
584
650
|
operationId: operation.operationId,
|
|
@@ -591,6 +657,23 @@ async function controlRecordFromRows(
|
|
|
591
657
|
};
|
|
592
658
|
}
|
|
593
659
|
|
|
660
|
+
function networkRouteAuthorityFromRow(
|
|
661
|
+
row: BrowserSessionRow,
|
|
662
|
+
): BrowserSessionNetworkRouteAuthority | null {
|
|
663
|
+
if (!row.networkRouteId) return null;
|
|
664
|
+
if (!row.networkRouteVersion || !row.networkRouteConfiguration || !row.networkRouteConsistency) {
|
|
665
|
+
throw new Error("BrowserSession network route is missing its pinned authority");
|
|
666
|
+
}
|
|
667
|
+
return {
|
|
668
|
+
routeId: row.networkRouteId,
|
|
669
|
+
routeVersion: row.networkRouteVersion,
|
|
670
|
+
credentialVersion: row.networkRouteCredentialVersion,
|
|
671
|
+
authorityDigest: row.networkRouteAuthorityDigest,
|
|
672
|
+
configuration: NetworkRouteConfiguration.parse(row.networkRouteConfiguration),
|
|
673
|
+
consistency: NetworkRouteConsistency.parse(row.networkRouteConsistency),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
594
677
|
export async function prepareBrowserSessionCreate(
|
|
595
678
|
db: Database,
|
|
596
679
|
input: PrepareBrowserSessionCreateInput,
|
|
@@ -617,7 +700,7 @@ export async function prepareBrowserSessionCreate(
|
|
|
617
700
|
});
|
|
618
701
|
}
|
|
619
702
|
const [sourceSession] = await tx
|
|
620
|
-
.select({ id: schema.sessions.id })
|
|
703
|
+
.select({ id: schema.sessions.id, sandboxGroupId: schema.sessions.sandboxGroupId })
|
|
621
704
|
.from(schema.sessions)
|
|
622
705
|
.where(
|
|
623
706
|
and(
|
|
@@ -628,7 +711,7 @@ export async function prepareBrowserSessionCreate(
|
|
|
628
711
|
.limit(1);
|
|
629
712
|
if (!sourceSession) throw new BrowserSessionNotFoundError("Associated session not found");
|
|
630
713
|
await assertLinkedComputerAvailable(tx, input);
|
|
631
|
-
await assertNetworkRouteAvailable(tx, input);
|
|
714
|
+
const networkRoute = await assertNetworkRouteAvailable(tx, input);
|
|
632
715
|
const identitySelection = await resolveCreateIdentitySelection(tx, input);
|
|
633
716
|
|
|
634
717
|
const [insertedOperation] = await tx
|
|
@@ -644,7 +727,9 @@ export async function prepareBrowserSessionCreate(
|
|
|
644
727
|
state: "prepared",
|
|
645
728
|
actorSubjectId: input.actorSubjectId,
|
|
646
729
|
})
|
|
647
|
-
.onConflictDoNothing({
|
|
730
|
+
.onConflictDoNothing({
|
|
731
|
+
target: schema.interactionOperations.operationId,
|
|
732
|
+
})
|
|
648
733
|
.returning();
|
|
649
734
|
if (!insertedOperation) {
|
|
650
735
|
const existing = await loadOperation(tx, input.workspaceId, input.operationId);
|
|
@@ -660,6 +745,12 @@ export async function prepareBrowserSessionCreate(
|
|
|
660
745
|
}
|
|
661
746
|
|
|
662
747
|
const placementColumns = placementToColumns(input.placement);
|
|
748
|
+
const controllerHostSandboxGroupId =
|
|
749
|
+
input.placement.kind === "sandbox_group"
|
|
750
|
+
? input.placement.sandboxGroupId
|
|
751
|
+
: input.placement.kind === "external_provider"
|
|
752
|
+
? sourceSession.sandboxGroupId
|
|
753
|
+
: null;
|
|
663
754
|
const [insertedSession] = await tx
|
|
664
755
|
.insert(schema.browserSessions)
|
|
665
756
|
.values({
|
|
@@ -669,12 +760,16 @@ export async function prepareBrowserSessionCreate(
|
|
|
669
760
|
name: input.name,
|
|
670
761
|
lifecycle: "starting",
|
|
671
762
|
...placementColumns,
|
|
763
|
+
controllerHostSandboxGroupId,
|
|
672
764
|
driverId: input.driverId,
|
|
673
765
|
engine: input.engine,
|
|
674
766
|
headless: input.headless,
|
|
675
767
|
identityId: identitySelection.identityId,
|
|
676
768
|
baseRevisionId: identitySelection.baseRevisionId,
|
|
677
769
|
networkRouteId: input.networkRouteId ?? null,
|
|
770
|
+
networkRouteVersion: networkRoute?.version ?? null,
|
|
771
|
+
networkRouteConfiguration: networkRoute?.configuration ?? null,
|
|
772
|
+
networkRouteConsistency: networkRoute?.consistency ?? null,
|
|
678
773
|
linkedComputerSessionId: input.linkedComputerSessionId ?? null,
|
|
679
774
|
capabilities,
|
|
680
775
|
createOperationId: input.operationId,
|
|
@@ -705,6 +800,120 @@ export async function prepareBrowserSessionCreate(
|
|
|
705
800
|
);
|
|
706
801
|
}
|
|
707
802
|
|
|
803
|
+
/**
|
|
804
|
+
* Bind the broker-resolved, secret-free launch authority before controller
|
|
805
|
+
* dispatch. A prepared operation may refresh this binding while no physical
|
|
806
|
+
* controller owns it; after dispatch it is immutable and replay-fenced.
|
|
807
|
+
*/
|
|
808
|
+
export async function bindBrowserSessionNetworkRouteAuthority(
|
|
809
|
+
db: Database,
|
|
810
|
+
input: {
|
|
811
|
+
accountId: string;
|
|
812
|
+
workspaceId: string;
|
|
813
|
+
browserSessionId: string;
|
|
814
|
+
operationId: string;
|
|
815
|
+
routeVersion: number;
|
|
816
|
+
credentialVersion: number | null;
|
|
817
|
+
authorityDigest: string;
|
|
818
|
+
},
|
|
819
|
+
): Promise<BrowserSessionNetworkRouteAuthority> {
|
|
820
|
+
if (!Number.isSafeInteger(input.routeVersion) || input.routeVersion < 1) {
|
|
821
|
+
throw new BrowserSessionStateError("Network route version is invalid");
|
|
822
|
+
}
|
|
823
|
+
if (
|
|
824
|
+
input.credentialVersion !== null &&
|
|
825
|
+
(!Number.isSafeInteger(input.credentialVersion) || input.credentialVersion < 1)
|
|
826
|
+
) {
|
|
827
|
+
throw new BrowserSessionStateError("Network route credential version is invalid");
|
|
828
|
+
}
|
|
829
|
+
if (!/^[A-Za-z0-9._~-]{16,256}$/u.test(input.authorityDigest)) {
|
|
830
|
+
throw new BrowserSessionStateError("Network route authority digest is invalid");
|
|
831
|
+
}
|
|
832
|
+
return await withRlsContext(
|
|
833
|
+
db,
|
|
834
|
+
input,
|
|
835
|
+
async (scopedDb) =>
|
|
836
|
+
await scopedDb.transaction(async (txRaw) => {
|
|
837
|
+
const tx = txRaw as unknown as Database;
|
|
838
|
+
await lockOperation(tx, input.workspaceId, input.operationId);
|
|
839
|
+
const operation = await loadOperation(tx, input.workspaceId, input.operationId);
|
|
840
|
+
assertOperationResource(operation, input.browserSessionId);
|
|
841
|
+
if (operation!.kind !== "create" && operation!.kind !== "resume") {
|
|
842
|
+
throw new BrowserSessionOperationConflictError(
|
|
843
|
+
"Network route authority belongs to another browser operation",
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
const [current] = await tx
|
|
847
|
+
.select()
|
|
848
|
+
.from(schema.browserSessions)
|
|
849
|
+
.where(
|
|
850
|
+
and(
|
|
851
|
+
eq(schema.browserSessions.accountId, input.accountId),
|
|
852
|
+
eq(schema.browserSessions.workspaceId, input.workspaceId),
|
|
853
|
+
eq(schema.browserSessions.id, input.browserSessionId),
|
|
854
|
+
),
|
|
855
|
+
)
|
|
856
|
+
.for("update")
|
|
857
|
+
.limit(1);
|
|
858
|
+
if (!current) throw new BrowserSessionNotFoundError("BrowserSession not found");
|
|
859
|
+
if (
|
|
860
|
+
!current.networkRouteId ||
|
|
861
|
+
current.networkRouteVersion !== input.routeVersion ||
|
|
862
|
+
!current.networkRouteConfiguration ||
|
|
863
|
+
!current.networkRouteConsistency
|
|
864
|
+
) {
|
|
865
|
+
throw new BrowserSessionOperationConflictError(
|
|
866
|
+
"BrowserSession route does not match this launch authority",
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
const configuration = NetworkRouteConfiguration.parse(current.networkRouteConfiguration);
|
|
870
|
+
const hasCredential = "credential" in configuration && configuration.credential !== null;
|
|
871
|
+
if (hasCredential !== (input.credentialVersion !== null)) {
|
|
872
|
+
throw new BrowserSessionStateError(
|
|
873
|
+
"Network route credential authority does not match its configuration",
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
if (
|
|
877
|
+
current.networkRouteAuthorityDigest === input.authorityDigest &&
|
|
878
|
+
current.networkRouteCredentialVersion === input.credentialVersion
|
|
879
|
+
) {
|
|
880
|
+
return networkRouteAuthorityFromRow(current)!;
|
|
881
|
+
}
|
|
882
|
+
if (
|
|
883
|
+
operation!.state !== "prepared" ||
|
|
884
|
+
operation!.controllerGeneration !== null ||
|
|
885
|
+
current.controllerGeneration !== null
|
|
886
|
+
) {
|
|
887
|
+
throw new BrowserSessionOperationConflictError(
|
|
888
|
+
"Dispatched BrowserSession route authority is immutable",
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
const [updated] = await tx
|
|
892
|
+
.update(schema.browserSessions)
|
|
893
|
+
.set({
|
|
894
|
+
networkRouteCredentialVersion: input.credentialVersion,
|
|
895
|
+
networkRouteAuthorityDigest: input.authorityDigest,
|
|
896
|
+
updatedAt: new Date(),
|
|
897
|
+
})
|
|
898
|
+
.where(
|
|
899
|
+
and(
|
|
900
|
+
eq(schema.browserSessions.workspaceId, input.workspaceId),
|
|
901
|
+
eq(schema.browserSessions.id, input.browserSessionId),
|
|
902
|
+
eq(schema.browserSessions.networkRouteVersion, input.routeVersion),
|
|
903
|
+
isNull(schema.browserSessions.controllerGeneration),
|
|
904
|
+
),
|
|
905
|
+
)
|
|
906
|
+
.returning();
|
|
907
|
+
if (!updated) {
|
|
908
|
+
throw new BrowserSessionOperationConflictError(
|
|
909
|
+
"Network route authority binding lost its fence",
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
return networkRouteAuthorityFromRow(updated)!;
|
|
913
|
+
}),
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
708
917
|
function normalizedBrowserCapabilities(
|
|
709
918
|
input: PrepareBrowserSessionCreateInput,
|
|
710
919
|
): BrowserSessionCapabilitiesValue {
|
|
@@ -717,9 +926,13 @@ function normalizedBrowserCapabilities(
|
|
|
717
926
|
async function assertNetworkRouteAvailable(
|
|
718
927
|
db: Database,
|
|
719
928
|
input: PrepareBrowserSessionCreateInput,
|
|
720
|
-
): Promise<
|
|
929
|
+
): Promise<{
|
|
930
|
+
version: number;
|
|
931
|
+
configuration: NetworkRouteConfigurationValue;
|
|
932
|
+
consistency: NetworkRouteConsistencyValue;
|
|
933
|
+
} | null> {
|
|
721
934
|
const networkRouteId = input.networkRouteId ?? null;
|
|
722
|
-
if (!networkRouteId) return;
|
|
935
|
+
if (!networkRouteId) return null;
|
|
723
936
|
const [route] = await db
|
|
724
937
|
.select()
|
|
725
938
|
.from(schema.networkRoutes)
|
|
@@ -731,35 +944,24 @@ async function assertNetworkRouteAvailable(
|
|
|
731
944
|
),
|
|
732
945
|
)
|
|
733
946
|
.limit(1)
|
|
734
|
-
.for("
|
|
947
|
+
.for("update");
|
|
735
948
|
if (!route) throw new BrowserSessionNotFoundError("Network route not found");
|
|
736
949
|
if (route.status !== "active") {
|
|
737
950
|
throw new BrowserSessionStateError("Network route is archived");
|
|
738
951
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
case "connected_machine":
|
|
753
|
-
return right.kind === "connected_machine" && left.sandboxId === right.sandboxId;
|
|
754
|
-
case "attached_device":
|
|
755
|
-
return right.kind === "attached_device" && left.deviceId === right.deviceId;
|
|
756
|
-
case "external_provider":
|
|
757
|
-
return (
|
|
758
|
-
right.kind === "external_provider" &&
|
|
759
|
-
left.providerId === right.providerId &&
|
|
760
|
-
left.placementId === right.placementId
|
|
761
|
-
);
|
|
762
|
-
}
|
|
952
|
+
const configuration = NetworkRouteConfiguration.parse(route.configuration);
|
|
953
|
+
const consistency = NetworkRouteConsistency.parse(route.consistency);
|
|
954
|
+
const compatibilityIssue = networkRoutePlacementCompatibilityIssue(
|
|
955
|
+
configuration,
|
|
956
|
+
consistency,
|
|
957
|
+
input.placement,
|
|
958
|
+
);
|
|
959
|
+
if (compatibilityIssue) throw new BrowserSessionStateError(compatibilityIssue);
|
|
960
|
+
return {
|
|
961
|
+
version: safeInteger(route.version, "network route version"),
|
|
962
|
+
configuration,
|
|
963
|
+
consistency,
|
|
964
|
+
};
|
|
763
965
|
}
|
|
764
966
|
|
|
765
967
|
async function assertLinkedComputerAvailable(
|
|
@@ -771,11 +973,6 @@ async function assertLinkedComputerAvailable(
|
|
|
771
973
|
if (input.headless) {
|
|
772
974
|
throw new BrowserSessionStateError("A linked ComputerSession requires a headed browser");
|
|
773
975
|
}
|
|
774
|
-
if (input.placement.kind === "attached_device") {
|
|
775
|
-
throw new BrowserSessionStateError(
|
|
776
|
-
"Attached Chrome does not expose an exact linked ComputerSession yet",
|
|
777
|
-
);
|
|
778
|
-
}
|
|
779
976
|
const [computer] = await db
|
|
780
977
|
.select()
|
|
781
978
|
.from(schema.computerSessions)
|
|
@@ -911,6 +1108,7 @@ export async function dispatchBrowserSessionOperation(
|
|
|
911
1108
|
browserSessionId: string;
|
|
912
1109
|
controllerGeneration: string;
|
|
913
1110
|
controller?: InteractionControllerBinding;
|
|
1111
|
+
stateUpload?: { objectKey: string; cleanupAfter: Date };
|
|
914
1112
|
},
|
|
915
1113
|
): Promise<InteractionLifecycleOperationReceiptValue> {
|
|
916
1114
|
const controller = input.controller ? InteractionControllerBinding.parse(input.controller) : null;
|
|
@@ -929,6 +1127,16 @@ export async function dispatchBrowserSessionOperation(
|
|
|
929
1127
|
const operation = await loadOperation(tx, input.workspaceId, input.operationId);
|
|
930
1128
|
assertOperationResource(operation, input.browserSessionId);
|
|
931
1129
|
if (operation!.state === "prepared") {
|
|
1130
|
+
if (input.stateUpload) {
|
|
1131
|
+
await prepareBrowserStateUploadInTransaction(tx, {
|
|
1132
|
+
accountId: input.accountId,
|
|
1133
|
+
workspaceId: input.workspaceId,
|
|
1134
|
+
operationId: input.operationId,
|
|
1135
|
+
sourceBrowserSessionId: input.browserSessionId,
|
|
1136
|
+
purpose: "private_checkpoint",
|
|
1137
|
+
...input.stateUpload,
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
932
1140
|
if (controller) {
|
|
933
1141
|
await lockBrowserSession(tx, input.workspaceId, input.browserSessionId);
|
|
934
1142
|
const now = new Date();
|
|
@@ -999,11 +1207,54 @@ export async function dispatchBrowserSessionOperation(
|
|
|
999
1207
|
);
|
|
1000
1208
|
}
|
|
1001
1209
|
}
|
|
1210
|
+
if (input.stateUpload && operation!.state === "dispatched") {
|
|
1211
|
+
await prepareBrowserStateUploadInTransaction(tx, {
|
|
1212
|
+
accountId: input.accountId,
|
|
1213
|
+
workspaceId: input.workspaceId,
|
|
1214
|
+
operationId: input.operationId,
|
|
1215
|
+
sourceBrowserSessionId: input.browserSessionId,
|
|
1216
|
+
purpose: "private_checkpoint",
|
|
1217
|
+
...input.stateUpload,
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1002
1220
|
return operationReceipt(operation!, true);
|
|
1003
1221
|
}),
|
|
1004
1222
|
);
|
|
1005
1223
|
}
|
|
1006
1224
|
|
|
1225
|
+
/** Pulse one exact dispatched lifecycle operation while its controller mutation
|
|
1226
|
+
* is still in flight. This is not viewer or resource activity: it exists only
|
|
1227
|
+
* so the global reaper can distinguish a long profile transfer from an API
|
|
1228
|
+
* caller that disappeared after dispatch. */
|
|
1229
|
+
export async function touchInteractionOperation(
|
|
1230
|
+
db: Database,
|
|
1231
|
+
input: {
|
|
1232
|
+
accountId: string;
|
|
1233
|
+
workspaceId: string;
|
|
1234
|
+
operationId: string;
|
|
1235
|
+
resourceId: string;
|
|
1236
|
+
controllerGeneration: string;
|
|
1237
|
+
},
|
|
1238
|
+
): Promise<boolean> {
|
|
1239
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
1240
|
+
const [row] = await scopedDb
|
|
1241
|
+
.update(schema.interactionOperations)
|
|
1242
|
+
.set({ updatedAt: new Date() })
|
|
1243
|
+
.where(
|
|
1244
|
+
and(
|
|
1245
|
+
eq(schema.interactionOperations.accountId, input.accountId),
|
|
1246
|
+
eq(schema.interactionOperations.workspaceId, input.workspaceId),
|
|
1247
|
+
eq(schema.interactionOperations.operationId, input.operationId),
|
|
1248
|
+
eq(schema.interactionOperations.resourceId, input.resourceId),
|
|
1249
|
+
eq(schema.interactionOperations.state, "dispatched"),
|
|
1250
|
+
eq(schema.interactionOperations.controllerGeneration, input.controllerGeneration),
|
|
1251
|
+
),
|
|
1252
|
+
)
|
|
1253
|
+
.returning({ operationId: schema.interactionOperations.operationId });
|
|
1254
|
+
return Boolean(row);
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1007
1258
|
export async function activateBrowserSession(
|
|
1008
1259
|
db: Database,
|
|
1009
1260
|
input: {
|
|
@@ -1042,6 +1293,25 @@ export async function activateBrowserSession(
|
|
|
1042
1293
|
);
|
|
1043
1294
|
}
|
|
1044
1295
|
await lockBrowserSession(tx, input.workspaceId, input.browserSessionId);
|
|
1296
|
+
const [currentSession] = await tx
|
|
1297
|
+
.select({
|
|
1298
|
+
networkRouteId: schema.browserSessions.networkRouteId,
|
|
1299
|
+
networkRouteAuthorityDigest: schema.browserSessions.networkRouteAuthorityDigest,
|
|
1300
|
+
})
|
|
1301
|
+
.from(schema.browserSessions)
|
|
1302
|
+
.where(
|
|
1303
|
+
and(
|
|
1304
|
+
eq(schema.browserSessions.workspaceId, input.workspaceId),
|
|
1305
|
+
eq(schema.browserSessions.id, input.browserSessionId),
|
|
1306
|
+
),
|
|
1307
|
+
)
|
|
1308
|
+
.limit(1);
|
|
1309
|
+
if (!currentSession) throw new BrowserSessionNotFoundError("BrowserSession not found");
|
|
1310
|
+
if (currentSession.networkRouteId && !currentSession.networkRouteAuthorityDigest) {
|
|
1311
|
+
throw new BrowserSessionStateError(
|
|
1312
|
+
"BrowserSession network route authority was not bound before activation",
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1045
1315
|
const now = new Date();
|
|
1046
1316
|
const [sessionRow] = await tx
|
|
1047
1317
|
.update(schema.browserSessions)
|
|
@@ -1287,6 +1557,12 @@ async function prepareBrowserSessionLifecycleTransition(
|
|
|
1287
1557
|
.set({
|
|
1288
1558
|
lifecycle: transition.targetLifecycle,
|
|
1289
1559
|
failureCode: null,
|
|
1560
|
+
...(transition.kind === "resume"
|
|
1561
|
+
? {
|
|
1562
|
+
networkRouteCredentialVersion: null,
|
|
1563
|
+
networkRouteAuthorityDigest: null,
|
|
1564
|
+
}
|
|
1565
|
+
: {}),
|
|
1290
1566
|
updatedAt: now,
|
|
1291
1567
|
})
|
|
1292
1568
|
.where(
|
|
@@ -1398,6 +1674,14 @@ export async function commitBrowserSessionSuspension(
|
|
|
1398
1674
|
})
|
|
1399
1675
|
.returning({ id: schema.browserStateArtifacts.id });
|
|
1400
1676
|
if (!artifactRow) throw new Error("Browser checkpoint artifact insert was lost");
|
|
1677
|
+
await commitBrowserStateUploadInTransaction(tx, {
|
|
1678
|
+
workspaceId: input.workspaceId,
|
|
1679
|
+
operationId: input.operationId,
|
|
1680
|
+
sourceBrowserSessionId: input.browserSessionId,
|
|
1681
|
+
purpose: "private_checkpoint",
|
|
1682
|
+
objectKey: artifact.objectKey,
|
|
1683
|
+
artifactId,
|
|
1684
|
+
});
|
|
1401
1685
|
|
|
1402
1686
|
if (current.privateCheckpointArtifactId) {
|
|
1403
1687
|
const [retired] = await tx
|
|
@@ -1669,6 +1953,9 @@ async function failBrowserSessionTransition(
|
|
|
1669
1953
|
if (!sessionRow || !operationRow) {
|
|
1670
1954
|
throw new Error("BrowserSession lifecycle failure settlement was lost");
|
|
1671
1955
|
}
|
|
1956
|
+
if (input.kind === "suspend") {
|
|
1957
|
+
await markBrowserStateUploadsDeletePendingInTransaction(tx, input);
|
|
1958
|
+
}
|
|
1672
1959
|
const associations = await loadAssociations(tx, input.workspaceId, [
|
|
1673
1960
|
input.browserSessionId,
|
|
1674
1961
|
]);
|
|
@@ -1712,7 +1999,9 @@ export async function prepareBrowserSessionEnd(
|
|
|
1712
1999
|
actorSubjectId: input.actorSubjectId,
|
|
1713
2000
|
...(terminal ? { dispatchedAt: now, settledAt: now } : {}),
|
|
1714
2001
|
})
|
|
1715
|
-
.onConflictDoNothing({
|
|
2002
|
+
.onConflictDoNothing({
|
|
2003
|
+
target: schema.interactionOperations.operationId,
|
|
2004
|
+
})
|
|
1716
2005
|
.returning();
|
|
1717
2006
|
if (!insertedOperation) {
|
|
1718
2007
|
const existing = await loadOperation(tx, input.workspaceId, input.operationId);
|
|
@@ -1905,7 +2194,7 @@ export async function touchBrowserSessionController(
|
|
|
1905
2194
|
)
|
|
1906
2195
|
.limit(1);
|
|
1907
2196
|
if (!observed) return false;
|
|
1908
|
-
if (observed.
|
|
2197
|
+
if (observed.controllerHostSandboxGroupId) {
|
|
1909
2198
|
// Match the reaper's exact lease -> holder -> BrowserSession lock
|
|
1910
2199
|
// order. The pre-lock observation is only a locator; every authority
|
|
1911
2200
|
// predicate is repeated below while the corresponding row is locked.
|
|
@@ -1915,7 +2204,7 @@ export async function touchBrowserSessionController(
|
|
|
1915
2204
|
join browser_sessions browser
|
|
1916
2205
|
on browser.account_id = lease.account_id
|
|
1917
2206
|
and browser.workspace_id = lease.workspace_id
|
|
1918
|
-
and browser.
|
|
2207
|
+
and browser.controller_host_sandbox_group_id = lease.sandbox_group_id
|
|
1919
2208
|
where browser.account_id = ${input.accountId}
|
|
1920
2209
|
and browser.workspace_id = ${input.workspaceId}
|
|
1921
2210
|
and browser.id = ${input.browserSessionId}
|
|
@@ -1934,7 +2223,7 @@ export async function touchBrowserSessionController(
|
|
|
1934
2223
|
and holder.kind = 'interaction'
|
|
1935
2224
|
and holder.holder_id = ${`browser-session:${input.browserSessionId}`}
|
|
1936
2225
|
and lease.workspace_id = ${input.workspaceId}
|
|
1937
|
-
and lease.sandbox_group_id = ${observed.
|
|
2226
|
+
and lease.sandbox_group_id = ${observed.controllerHostSandboxGroupId}
|
|
1938
2227
|
and lease.instance_id = ${observed.placementInstanceId}
|
|
1939
2228
|
returning holder.id
|
|
1940
2229
|
`);
|