@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,1896 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
AuthRun,
|
|
4
|
+
AuthRunListResponse,
|
|
5
|
+
AuthRunMutationResponse,
|
|
6
|
+
CreateInteractionInterventionRequest,
|
|
7
|
+
CreateNetworkRouteRequest,
|
|
8
|
+
CreateSiteAuthConnectionRequest,
|
|
9
|
+
InteractionIntervention,
|
|
10
|
+
InteractionInterventionListResponse,
|
|
11
|
+
InteractionInterventionMutationResponse,
|
|
12
|
+
NetworkRoute,
|
|
13
|
+
NetworkRouteConfiguration,
|
|
14
|
+
NetworkRouteConsistency,
|
|
15
|
+
NetworkRouteListResponse,
|
|
16
|
+
NetworkRouteMutationResponse,
|
|
17
|
+
ReportAuthRunRequest,
|
|
18
|
+
ResolveInteractionInterventionRequest,
|
|
19
|
+
SiteAuthConnection,
|
|
20
|
+
SiteAuthConnectionListResponse,
|
|
21
|
+
SiteAuthConnectionMutationResponse,
|
|
22
|
+
StartAuthRunRequest,
|
|
23
|
+
UpdateNetworkRouteRequest,
|
|
24
|
+
UpdateSiteAuthConnectionRequest,
|
|
25
|
+
type AuthRun as AuthRunValue,
|
|
26
|
+
type AuthRunListResponse as AuthRunListResponseValue,
|
|
27
|
+
type AuthRunMutationResponse as AuthRunMutationResponseValue,
|
|
28
|
+
type CreateInteractionInterventionRequest as CreateInteractionInterventionRequestValue,
|
|
29
|
+
type CreateNetworkRouteRequest as CreateNetworkRouteRequestValue,
|
|
30
|
+
type CreateSiteAuthConnectionRequest as CreateSiteAuthConnectionRequestValue,
|
|
31
|
+
type InteractionCredentialAuthorityRef,
|
|
32
|
+
type InteractionIntervention as InteractionInterventionValue,
|
|
33
|
+
type InteractionInterventionListResponse as InteractionInterventionListResponseValue,
|
|
34
|
+
type InteractionInterventionMutationResponse as InteractionInterventionMutationResponseValue,
|
|
35
|
+
type NetworkRoute as NetworkRouteValue,
|
|
36
|
+
type NetworkRouteConfiguration as NetworkRouteConfigurationValue,
|
|
37
|
+
type NetworkRouteListResponse as NetworkRouteListResponseValue,
|
|
38
|
+
type NetworkRouteMutationResponse as NetworkRouteMutationResponseValue,
|
|
39
|
+
type ReportAuthRunRequest as ReportAuthRunRequestValue,
|
|
40
|
+
type ResolveInteractionInterventionRequest as ResolveInteractionInterventionRequestValue,
|
|
41
|
+
type SiteAuthAuthority,
|
|
42
|
+
type SiteAuthConnection as SiteAuthConnectionValue,
|
|
43
|
+
type SiteAuthConnectionListResponse as SiteAuthConnectionListResponseValue,
|
|
44
|
+
type SiteAuthConnectionMutationResponse as SiteAuthConnectionMutationResponseValue,
|
|
45
|
+
type StartAuthRunRequest as StartAuthRunRequestValue,
|
|
46
|
+
type UpdateNetworkRouteRequest as UpdateNetworkRouteRequestValue,
|
|
47
|
+
type UpdateSiteAuthConnectionRequest as UpdateSiteAuthConnectionRequestValue,
|
|
48
|
+
} from "@opengeni/contracts";
|
|
49
|
+
import { and, asc, eq, inArray, sql } from "drizzle-orm";
|
|
50
|
+
import { type Database, setSubjectRlsContext, withRlsContext } from "./database";
|
|
51
|
+
import {
|
|
52
|
+
advanceWorkspaceInteractionRevision,
|
|
53
|
+
readWorkspaceInteractionRevision,
|
|
54
|
+
} from "./interaction-revisions";
|
|
55
|
+
import { safeDatabaseErrorFacts } from "./persistence-errors";
|
|
56
|
+
import * as schema from "./schema";
|
|
57
|
+
|
|
58
|
+
type NetworkRouteRow = typeof schema.networkRoutes.$inferSelect;
|
|
59
|
+
type SiteAuthConnectionRow = typeof schema.siteAuthConnections.$inferSelect;
|
|
60
|
+
type AuthRunRow = typeof schema.authRuns.$inferSelect;
|
|
61
|
+
type InterventionRow = typeof schema.interactionInterventions.$inferSelect;
|
|
62
|
+
type ResourceOperationRow = typeof schema.interactionResourceOperations.$inferSelect;
|
|
63
|
+
|
|
64
|
+
type InteractionMutationScope = {
|
|
65
|
+
accountId: string;
|
|
66
|
+
workspaceId: string;
|
|
67
|
+
actorSubjectId: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type ResourceKind = ResourceOperationRow["resourceKind"];
|
|
71
|
+
type ResourceOperationKind = ResourceOperationRow["kind"];
|
|
72
|
+
|
|
73
|
+
const CONSISTENT_READ = {
|
|
74
|
+
isolationLevel: "repeatable read",
|
|
75
|
+
accessMode: "read only",
|
|
76
|
+
} as const;
|
|
77
|
+
|
|
78
|
+
export class InteractionResourceNotFoundError extends Error {
|
|
79
|
+
readonly name = "InteractionResourceNotFoundError";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class InteractionResourceConflictError extends Error {
|
|
83
|
+
readonly name = "InteractionResourceConflictError";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class InteractionResourceStateError extends Error {
|
|
87
|
+
readonly name = "InteractionResourceStateError";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function iso(value: Date): string {
|
|
91
|
+
return value.toISOString();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function canonicalJsonValue(value: unknown): unknown {
|
|
95
|
+
if (Array.isArray(value)) return value.map(canonicalJsonValue);
|
|
96
|
+
if (!value || typeof value !== "object") return value;
|
|
97
|
+
return Object.fromEntries(
|
|
98
|
+
Object.entries(value as Record<string, unknown>)
|
|
99
|
+
.filter(([, entry]) => entry !== undefined)
|
|
100
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
101
|
+
.map(([key, entry]) => [key, canonicalJsonValue(entry)]),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function operationDigest(value: Record<string, unknown>): string {
|
|
106
|
+
return createHash("sha256")
|
|
107
|
+
.update(JSON.stringify(canonicalJsonValue(value)))
|
|
108
|
+
.digest("hex");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function postgresConstraint(error: unknown): string | null {
|
|
112
|
+
return safeDatabaseErrorFacts(error).constraint ?? null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function routeFromRow(row: NetworkRouteRow): NetworkRouteValue {
|
|
116
|
+
return NetworkRoute.parse({
|
|
117
|
+
id: row.id,
|
|
118
|
+
accountId: row.accountId,
|
|
119
|
+
workspaceId: row.workspaceId,
|
|
120
|
+
name: row.name,
|
|
121
|
+
status: row.status,
|
|
122
|
+
configuration: row.configuration,
|
|
123
|
+
consistency: row.consistency,
|
|
124
|
+
version: row.version,
|
|
125
|
+
createdBySubjectId: row.createdBySubjectId,
|
|
126
|
+
createdAt: iso(row.createdAt),
|
|
127
|
+
updatedAt: iso(row.updatedAt),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function siteAuthConnectionFromRow(row: SiteAuthConnectionRow): SiteAuthConnectionValue {
|
|
132
|
+
return SiteAuthConnection.parse({
|
|
133
|
+
id: row.id,
|
|
134
|
+
accountId: row.accountId,
|
|
135
|
+
workspaceId: row.workspaceId,
|
|
136
|
+
name: row.name,
|
|
137
|
+
accountLabel: row.accountLabel,
|
|
138
|
+
status: row.status,
|
|
139
|
+
origins: row.origins,
|
|
140
|
+
loginUrl: row.loginUrl,
|
|
141
|
+
verificationUrlPrefixes: row.verificationUrlPrefixes,
|
|
142
|
+
authorities: row.authorities,
|
|
143
|
+
methods: row.methods,
|
|
144
|
+
preferredIdentityId: row.preferredIdentityId,
|
|
145
|
+
preferredPlacement: row.preferredPlacement,
|
|
146
|
+
preferredNetworkRouteId: row.preferredNetworkRouteId,
|
|
147
|
+
healthPolicy: row.healthPolicy,
|
|
148
|
+
verificationState: row.verificationState,
|
|
149
|
+
lastVerifiedAt: row.lastVerifiedAt ? iso(row.lastVerifiedAt) : null,
|
|
150
|
+
lastVerifiedUrl: row.lastVerifiedUrl,
|
|
151
|
+
repairCode: row.repairCode,
|
|
152
|
+
version: row.version,
|
|
153
|
+
createdBySubjectId: row.createdBySubjectId,
|
|
154
|
+
createdAt: iso(row.createdAt),
|
|
155
|
+
updatedAt: iso(row.updatedAt),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function lockOperation(db: Database, operationId: string): Promise<void> {
|
|
160
|
+
await db.execute(
|
|
161
|
+
sql`select pg_advisory_xact_lock(hashtextextended(${`interaction-resource:${operationId}`}, 0))`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function loadOperation(
|
|
166
|
+
db: Database,
|
|
167
|
+
workspaceId: string,
|
|
168
|
+
operationId: string,
|
|
169
|
+
): Promise<ResourceOperationRow | null> {
|
|
170
|
+
const [row] = await db
|
|
171
|
+
.select()
|
|
172
|
+
.from(schema.interactionResourceOperations)
|
|
173
|
+
.where(
|
|
174
|
+
and(
|
|
175
|
+
eq(schema.interactionResourceOperations.workspaceId, workspaceId),
|
|
176
|
+
eq(schema.interactionResourceOperations.operationId, operationId),
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
.limit(1);
|
|
180
|
+
return row ?? null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function assertOperation(
|
|
184
|
+
operation: ResourceOperationRow,
|
|
185
|
+
expected: {
|
|
186
|
+
resourceKind: ResourceKind;
|
|
187
|
+
resourceId?: string;
|
|
188
|
+
kind: ResourceOperationKind;
|
|
189
|
+
requestDigest: string;
|
|
190
|
+
actorSubjectId: string;
|
|
191
|
+
},
|
|
192
|
+
): void {
|
|
193
|
+
if (
|
|
194
|
+
operation.resourceKind !== expected.resourceKind ||
|
|
195
|
+
(expected.resourceId !== undefined && operation.resourceId !== expected.resourceId) ||
|
|
196
|
+
operation.kind !== expected.kind ||
|
|
197
|
+
operation.requestDigest !== expected.requestDigest ||
|
|
198
|
+
operation.actorSubjectId !== expected.actorSubjectId
|
|
199
|
+
) {
|
|
200
|
+
throw new InteractionResourceConflictError(
|
|
201
|
+
"Operation id is already bound to another interaction request",
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (operation.state !== "completed" || !operation.result || !operation.resultVersion) {
|
|
205
|
+
throw new InteractionResourceStateError("Interaction operation is not complete");
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function insertCompletedOperation(
|
|
210
|
+
db: Database,
|
|
211
|
+
input: {
|
|
212
|
+
operationId: string;
|
|
213
|
+
accountId: string;
|
|
214
|
+
workspaceId: string;
|
|
215
|
+
resourceKind: ResourceKind;
|
|
216
|
+
resourceId: string;
|
|
217
|
+
kind: ResourceOperationKind;
|
|
218
|
+
requestDigest: string;
|
|
219
|
+
resultVersion: number;
|
|
220
|
+
result: Record<string, unknown>;
|
|
221
|
+
actorSubjectId: string;
|
|
222
|
+
},
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
await db.insert(schema.interactionResourceOperations).values({
|
|
225
|
+
...input,
|
|
226
|
+
state: "completed",
|
|
227
|
+
settledAt: sql`now()`,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function assertWorkspaceAccount(
|
|
232
|
+
db: Database,
|
|
233
|
+
accountId: string,
|
|
234
|
+
workspaceId: string,
|
|
235
|
+
): Promise<void> {
|
|
236
|
+
const [workspace] = await db
|
|
237
|
+
.select({ id: schema.workspaces.id })
|
|
238
|
+
.from(schema.workspaces)
|
|
239
|
+
.where(and(eq(schema.workspaces.id, workspaceId), eq(schema.workspaces.accountId, accountId)))
|
|
240
|
+
.limit(1);
|
|
241
|
+
if (!workspace) throw new InteractionResourceNotFoundError("Workspace not found");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function credentialRefsFromRoute(
|
|
245
|
+
configuration: NetworkRouteConfigurationValue,
|
|
246
|
+
): InteractionCredentialAuthorityRef[] {
|
|
247
|
+
return "credential" in configuration && configuration.credential
|
|
248
|
+
? [configuration.credential]
|
|
249
|
+
: [];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function credentialRefsFromAuthorities(
|
|
253
|
+
authorities: readonly SiteAuthAuthority[],
|
|
254
|
+
): InteractionCredentialAuthorityRef[] {
|
|
255
|
+
return authorities.flatMap((authority) =>
|
|
256
|
+
"credential" in authority && authority.credential ? [authority.credential] : [],
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function credentialRefKey(reference: InteractionCredentialAuthorityRef): string {
|
|
261
|
+
return `${reference.connectionId}\u0000${reference.connectionSubjectId ?? ""}\u0000${reference.providerDomain}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function introducedCredentialRefs(
|
|
265
|
+
current: readonly InteractionCredentialAuthorityRef[],
|
|
266
|
+
candidate: readonly InteractionCredentialAuthorityRef[],
|
|
267
|
+
): InteractionCredentialAuthorityRef[] {
|
|
268
|
+
const trusted = new Set(current.map(credentialRefKey));
|
|
269
|
+
return candidate.filter((reference) => !trusted.has(credentialRefKey(reference)));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function assertCredentialAuthorities(
|
|
273
|
+
db: Database,
|
|
274
|
+
workspaceId: string,
|
|
275
|
+
refs: readonly InteractionCredentialAuthorityRef[],
|
|
276
|
+
): Promise<void> {
|
|
277
|
+
if (refs.length === 0) return;
|
|
278
|
+
const ids = [...new Set(refs.map((reference) => reference.connectionId))];
|
|
279
|
+
const rows = await db
|
|
280
|
+
.select({
|
|
281
|
+
id: schema.connections.id,
|
|
282
|
+
subjectId: schema.connections.subjectId,
|
|
283
|
+
providerDomain: schema.connections.providerDomain,
|
|
284
|
+
status: schema.connections.status,
|
|
285
|
+
})
|
|
286
|
+
.from(schema.connections)
|
|
287
|
+
.where(
|
|
288
|
+
and(eq(schema.connections.workspaceId, workspaceId), inArray(schema.connections.id, ids)),
|
|
289
|
+
);
|
|
290
|
+
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
291
|
+
for (const reference of refs) {
|
|
292
|
+
const row = byId.get(reference.connectionId);
|
|
293
|
+
if (
|
|
294
|
+
!row ||
|
|
295
|
+
row.subjectId !== reference.connectionSubjectId ||
|
|
296
|
+
row.providerDomain !== reference.providerDomain
|
|
297
|
+
) {
|
|
298
|
+
throw new InteractionResourceNotFoundError("Credential authority not found");
|
|
299
|
+
}
|
|
300
|
+
if (row.status !== "active") {
|
|
301
|
+
throw new InteractionResourceStateError("Credential authority is not active");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function assertActiveBrowserIdentity(
|
|
307
|
+
db: Database,
|
|
308
|
+
workspaceId: string,
|
|
309
|
+
identityId: string | null,
|
|
310
|
+
): Promise<void> {
|
|
311
|
+
if (!identityId) return;
|
|
312
|
+
const [identity] = await db
|
|
313
|
+
.select({ status: schema.browserIdentities.status })
|
|
314
|
+
.from(schema.browserIdentities)
|
|
315
|
+
.where(
|
|
316
|
+
and(
|
|
317
|
+
eq(schema.browserIdentities.workspaceId, workspaceId),
|
|
318
|
+
eq(schema.browserIdentities.id, identityId),
|
|
319
|
+
),
|
|
320
|
+
)
|
|
321
|
+
.limit(1);
|
|
322
|
+
if (!identity) throw new InteractionResourceNotFoundError("Preferred browser identity not found");
|
|
323
|
+
if (identity.status !== "active") {
|
|
324
|
+
throw new InteractionResourceStateError("Preferred browser identity is archived");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function assertActiveNetworkRoute(
|
|
329
|
+
db: Database,
|
|
330
|
+
workspaceId: string,
|
|
331
|
+
routeId: string | null,
|
|
332
|
+
): Promise<void> {
|
|
333
|
+
if (!routeId) return;
|
|
334
|
+
const [route] = await db
|
|
335
|
+
.select({ status: schema.networkRoutes.status })
|
|
336
|
+
.from(schema.networkRoutes)
|
|
337
|
+
.where(
|
|
338
|
+
and(eq(schema.networkRoutes.workspaceId, workspaceId), eq(schema.networkRoutes.id, routeId)),
|
|
339
|
+
)
|
|
340
|
+
.limit(1);
|
|
341
|
+
if (!route) throw new InteractionResourceNotFoundError("Preferred network route not found");
|
|
342
|
+
if (route.status !== "active") {
|
|
343
|
+
throw new InteractionResourceStateError("Preferred network route is archived");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export async function listNetworkRoutes(
|
|
348
|
+
db: Database,
|
|
349
|
+
input: { accountId: string; workspaceId: string; includeArchived?: boolean },
|
|
350
|
+
): Promise<NetworkRouteListResponseValue> {
|
|
351
|
+
return await withRlsContext(
|
|
352
|
+
db,
|
|
353
|
+
input,
|
|
354
|
+
async (scopedDb) => {
|
|
355
|
+
const predicate = input.includeArchived
|
|
356
|
+
? eq(schema.networkRoutes.workspaceId, input.workspaceId)
|
|
357
|
+
: and(
|
|
358
|
+
eq(schema.networkRoutes.workspaceId, input.workspaceId),
|
|
359
|
+
eq(schema.networkRoutes.status, "active"),
|
|
360
|
+
);
|
|
361
|
+
const routes = await scopedDb
|
|
362
|
+
.select()
|
|
363
|
+
.from(schema.networkRoutes)
|
|
364
|
+
.where(predicate)
|
|
365
|
+
.orderBy(asc(schema.networkRoutes.name), asc(schema.networkRoutes.id));
|
|
366
|
+
return NetworkRouteListResponse.parse({
|
|
367
|
+
revision: await readWorkspaceInteractionRevision(scopedDb, input.workspaceId),
|
|
368
|
+
routes: routes.map(routeFromRow),
|
|
369
|
+
});
|
|
370
|
+
},
|
|
371
|
+
CONSISTENT_READ,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export async function getNetworkRoute(
|
|
376
|
+
db: Database,
|
|
377
|
+
input: { accountId: string; workspaceId: string; routeId: string },
|
|
378
|
+
): Promise<NetworkRouteValue> {
|
|
379
|
+
return await withRlsContext(
|
|
380
|
+
db,
|
|
381
|
+
input,
|
|
382
|
+
async (scopedDb) => {
|
|
383
|
+
const [row] = await scopedDb
|
|
384
|
+
.select()
|
|
385
|
+
.from(schema.networkRoutes)
|
|
386
|
+
.where(
|
|
387
|
+
and(
|
|
388
|
+
eq(schema.networkRoutes.workspaceId, input.workspaceId),
|
|
389
|
+
eq(schema.networkRoutes.id, input.routeId),
|
|
390
|
+
),
|
|
391
|
+
)
|
|
392
|
+
.limit(1);
|
|
393
|
+
if (!row) throw new InteractionResourceNotFoundError("Network route not found");
|
|
394
|
+
return routeFromRow(row);
|
|
395
|
+
},
|
|
396
|
+
CONSISTENT_READ,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export async function createNetworkRoute(
|
|
401
|
+
db: Database,
|
|
402
|
+
input: InteractionMutationScope & CreateNetworkRouteRequestValue,
|
|
403
|
+
): Promise<NetworkRouteMutationResponseValue> {
|
|
404
|
+
const request = CreateNetworkRouteRequest.parse({
|
|
405
|
+
operationId: input.operationId,
|
|
406
|
+
name: input.name,
|
|
407
|
+
configuration: input.configuration,
|
|
408
|
+
consistency: input.consistency,
|
|
409
|
+
});
|
|
410
|
+
const digest = operationDigest({
|
|
411
|
+
version: 1,
|
|
412
|
+
name: request.name,
|
|
413
|
+
configuration: request.configuration,
|
|
414
|
+
consistency: request.consistency,
|
|
415
|
+
actorSubjectId: input.actorSubjectId,
|
|
416
|
+
});
|
|
417
|
+
try {
|
|
418
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
419
|
+
await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
|
|
420
|
+
await setSubjectRlsContext(scopedDb, input.actorSubjectId);
|
|
421
|
+
await lockOperation(scopedDb, request.operationId);
|
|
422
|
+
const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
|
|
423
|
+
if (existing) {
|
|
424
|
+
assertOperation(existing, {
|
|
425
|
+
resourceKind: "network_route",
|
|
426
|
+
kind: "create",
|
|
427
|
+
requestDigest: digest,
|
|
428
|
+
actorSubjectId: input.actorSubjectId,
|
|
429
|
+
});
|
|
430
|
+
const response = NetworkRouteMutationResponse.parse(existing.result);
|
|
431
|
+
return { ...response, replayed: true };
|
|
432
|
+
}
|
|
433
|
+
await assertCredentialAuthorities(
|
|
434
|
+
scopedDb,
|
|
435
|
+
input.workspaceId,
|
|
436
|
+
credentialRefsFromRoute(request.configuration),
|
|
437
|
+
);
|
|
438
|
+
const id = randomUUID();
|
|
439
|
+
const [row] = await scopedDb
|
|
440
|
+
.insert(schema.networkRoutes)
|
|
441
|
+
.values({
|
|
442
|
+
id,
|
|
443
|
+
accountId: input.accountId,
|
|
444
|
+
workspaceId: input.workspaceId,
|
|
445
|
+
name: request.name,
|
|
446
|
+
configuration: request.configuration,
|
|
447
|
+
consistency: request.consistency,
|
|
448
|
+
createOperationId: request.operationId,
|
|
449
|
+
createdBySubjectId: input.actorSubjectId,
|
|
450
|
+
updatedBySubjectId: input.actorSubjectId,
|
|
451
|
+
})
|
|
452
|
+
.returning();
|
|
453
|
+
if (!row) throw new Error("Network route insert returned no row");
|
|
454
|
+
const response = NetworkRouteMutationResponse.parse({
|
|
455
|
+
route: routeFromRow(row),
|
|
456
|
+
operationId: request.operationId,
|
|
457
|
+
replayed: false,
|
|
458
|
+
});
|
|
459
|
+
await insertCompletedOperation(scopedDb, {
|
|
460
|
+
operationId: request.operationId,
|
|
461
|
+
accountId: input.accountId,
|
|
462
|
+
workspaceId: input.workspaceId,
|
|
463
|
+
resourceKind: "network_route",
|
|
464
|
+
resourceId: id,
|
|
465
|
+
kind: "create",
|
|
466
|
+
requestDigest: digest,
|
|
467
|
+
resultVersion: row.version,
|
|
468
|
+
result: response,
|
|
469
|
+
actorSubjectId: input.actorSubjectId,
|
|
470
|
+
});
|
|
471
|
+
await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
|
|
472
|
+
return response;
|
|
473
|
+
});
|
|
474
|
+
} catch (error) {
|
|
475
|
+
if (postgresConstraint(error) === "network_routes_workspace_active_name_uq") {
|
|
476
|
+
throw new InteractionResourceConflictError("An active network route already uses this name");
|
|
477
|
+
}
|
|
478
|
+
throw error;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export async function updateNetworkRoute(
|
|
483
|
+
db: Database,
|
|
484
|
+
input: InteractionMutationScope & { routeId: string } & UpdateNetworkRouteRequestValue,
|
|
485
|
+
): Promise<NetworkRouteMutationResponseValue> {
|
|
486
|
+
const request = UpdateNetworkRouteRequest.parse({
|
|
487
|
+
operationId: input.operationId,
|
|
488
|
+
expectedVersion: input.expectedVersion,
|
|
489
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
490
|
+
...(input.status !== undefined ? { status: input.status } : {}),
|
|
491
|
+
...(input.configuration !== undefined ? { configuration: input.configuration } : {}),
|
|
492
|
+
...(input.consistency !== undefined ? { consistency: input.consistency } : {}),
|
|
493
|
+
});
|
|
494
|
+
const { operationId: _operationId, ...digestRequest } = request;
|
|
495
|
+
const digest = operationDigest({
|
|
496
|
+
version: 1,
|
|
497
|
+
routeId: input.routeId,
|
|
498
|
+
request: digestRequest,
|
|
499
|
+
actorSubjectId: input.actorSubjectId,
|
|
500
|
+
});
|
|
501
|
+
try {
|
|
502
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
503
|
+
await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
|
|
504
|
+
await setSubjectRlsContext(scopedDb, input.actorSubjectId);
|
|
505
|
+
await lockOperation(scopedDb, request.operationId);
|
|
506
|
+
const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
|
|
507
|
+
if (existing) {
|
|
508
|
+
assertOperation(existing, {
|
|
509
|
+
resourceKind: "network_route",
|
|
510
|
+
resourceId: input.routeId,
|
|
511
|
+
kind: "update",
|
|
512
|
+
requestDigest: digest,
|
|
513
|
+
actorSubjectId: input.actorSubjectId,
|
|
514
|
+
});
|
|
515
|
+
const response = NetworkRouteMutationResponse.parse(existing.result);
|
|
516
|
+
return { ...response, replayed: true };
|
|
517
|
+
}
|
|
518
|
+
const [current] = await scopedDb
|
|
519
|
+
.select()
|
|
520
|
+
.from(schema.networkRoutes)
|
|
521
|
+
.where(
|
|
522
|
+
and(
|
|
523
|
+
eq(schema.networkRoutes.workspaceId, input.workspaceId),
|
|
524
|
+
eq(schema.networkRoutes.id, input.routeId),
|
|
525
|
+
),
|
|
526
|
+
)
|
|
527
|
+
.for("update")
|
|
528
|
+
.limit(1);
|
|
529
|
+
if (!current) throw new InteractionResourceNotFoundError("Network route not found");
|
|
530
|
+
if (current.version !== request.expectedVersion) {
|
|
531
|
+
throw new InteractionResourceConflictError("Network route changed before this update");
|
|
532
|
+
}
|
|
533
|
+
const configuration = NetworkRouteConfiguration.parse(
|
|
534
|
+
request.configuration ?? current.configuration,
|
|
535
|
+
);
|
|
536
|
+
const consistency = NetworkRouteConsistency.parse(request.consistency ?? current.consistency);
|
|
537
|
+
await assertCredentialAuthorities(
|
|
538
|
+
scopedDb,
|
|
539
|
+
input.workspaceId,
|
|
540
|
+
introducedCredentialRefs(
|
|
541
|
+
credentialRefsFromRoute(NetworkRouteConfiguration.parse(current.configuration)),
|
|
542
|
+
credentialRefsFromRoute(configuration),
|
|
543
|
+
),
|
|
544
|
+
);
|
|
545
|
+
const [row] = await scopedDb
|
|
546
|
+
.update(schema.networkRoutes)
|
|
547
|
+
.set({
|
|
548
|
+
name: request.name ?? current.name,
|
|
549
|
+
status: request.status ?? current.status,
|
|
550
|
+
configuration,
|
|
551
|
+
consistency,
|
|
552
|
+
version: current.version + 1,
|
|
553
|
+
updatedBySubjectId: input.actorSubjectId,
|
|
554
|
+
updatedAt: sql`now()`,
|
|
555
|
+
})
|
|
556
|
+
.where(
|
|
557
|
+
and(
|
|
558
|
+
eq(schema.networkRoutes.workspaceId, input.workspaceId),
|
|
559
|
+
eq(schema.networkRoutes.id, input.routeId),
|
|
560
|
+
eq(schema.networkRoutes.version, request.expectedVersion),
|
|
561
|
+
),
|
|
562
|
+
)
|
|
563
|
+
.returning();
|
|
564
|
+
if (!row) throw new InteractionResourceConflictError("Network route update lost its fence");
|
|
565
|
+
const response = NetworkRouteMutationResponse.parse({
|
|
566
|
+
route: routeFromRow(row),
|
|
567
|
+
operationId: request.operationId,
|
|
568
|
+
replayed: false,
|
|
569
|
+
});
|
|
570
|
+
await insertCompletedOperation(scopedDb, {
|
|
571
|
+
operationId: request.operationId,
|
|
572
|
+
accountId: input.accountId,
|
|
573
|
+
workspaceId: input.workspaceId,
|
|
574
|
+
resourceKind: "network_route",
|
|
575
|
+
resourceId: input.routeId,
|
|
576
|
+
kind: "update",
|
|
577
|
+
requestDigest: digest,
|
|
578
|
+
resultVersion: row.version,
|
|
579
|
+
result: response,
|
|
580
|
+
actorSubjectId: input.actorSubjectId,
|
|
581
|
+
});
|
|
582
|
+
await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
|
|
583
|
+
return response;
|
|
584
|
+
});
|
|
585
|
+
} catch (error) {
|
|
586
|
+
if (postgresConstraint(error) === "network_routes_workspace_active_name_uq") {
|
|
587
|
+
throw new InteractionResourceConflictError("An active network route already uses this name");
|
|
588
|
+
}
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export async function listSiteAuthConnections(
|
|
594
|
+
db: Database,
|
|
595
|
+
input: { accountId: string; workspaceId: string; includeArchived?: boolean },
|
|
596
|
+
): Promise<SiteAuthConnectionListResponseValue> {
|
|
597
|
+
return await withRlsContext(
|
|
598
|
+
db,
|
|
599
|
+
input,
|
|
600
|
+
async (scopedDb) => {
|
|
601
|
+
const predicate = input.includeArchived
|
|
602
|
+
? eq(schema.siteAuthConnections.workspaceId, input.workspaceId)
|
|
603
|
+
: and(
|
|
604
|
+
eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
|
|
605
|
+
eq(schema.siteAuthConnections.status, "active"),
|
|
606
|
+
);
|
|
607
|
+
const connections = await scopedDb
|
|
608
|
+
.select()
|
|
609
|
+
.from(schema.siteAuthConnections)
|
|
610
|
+
.where(predicate)
|
|
611
|
+
.orderBy(asc(schema.siteAuthConnections.name), asc(schema.siteAuthConnections.id));
|
|
612
|
+
return SiteAuthConnectionListResponse.parse({
|
|
613
|
+
revision: await readWorkspaceInteractionRevision(scopedDb, input.workspaceId),
|
|
614
|
+
connections: connections.map(siteAuthConnectionFromRow),
|
|
615
|
+
});
|
|
616
|
+
},
|
|
617
|
+
CONSISTENT_READ,
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export async function getSiteAuthConnection(
|
|
622
|
+
db: Database,
|
|
623
|
+
input: { accountId: string; workspaceId: string; siteAuthConnectionId: string },
|
|
624
|
+
): Promise<SiteAuthConnectionValue> {
|
|
625
|
+
return await withRlsContext(
|
|
626
|
+
db,
|
|
627
|
+
input,
|
|
628
|
+
async (scopedDb) => {
|
|
629
|
+
const [row] = await scopedDb
|
|
630
|
+
.select()
|
|
631
|
+
.from(schema.siteAuthConnections)
|
|
632
|
+
.where(
|
|
633
|
+
and(
|
|
634
|
+
eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
|
|
635
|
+
eq(schema.siteAuthConnections.id, input.siteAuthConnectionId),
|
|
636
|
+
),
|
|
637
|
+
)
|
|
638
|
+
.limit(1);
|
|
639
|
+
if (!row) throw new InteractionResourceNotFoundError("Site auth connection not found");
|
|
640
|
+
return siteAuthConnectionFromRow(row);
|
|
641
|
+
},
|
|
642
|
+
CONSISTENT_READ,
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async function validateSiteAuthReferences(
|
|
647
|
+
db: Database,
|
|
648
|
+
workspaceId: string,
|
|
649
|
+
connection: Pick<
|
|
650
|
+
SiteAuthConnectionValue,
|
|
651
|
+
"authorities" | "preferredIdentityId" | "preferredNetworkRouteId"
|
|
652
|
+
>,
|
|
653
|
+
): Promise<void> {
|
|
654
|
+
await assertCredentialAuthorities(
|
|
655
|
+
db,
|
|
656
|
+
workspaceId,
|
|
657
|
+
credentialRefsFromAuthorities(connection.authorities),
|
|
658
|
+
);
|
|
659
|
+
await assertActiveBrowserIdentity(db, workspaceId, connection.preferredIdentityId);
|
|
660
|
+
await assertActiveNetworkRoute(db, workspaceId, connection.preferredNetworkRouteId);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function validateSiteAuthUpdateReferences(
|
|
664
|
+
db: Database,
|
|
665
|
+
workspaceId: string,
|
|
666
|
+
current: SiteAuthConnectionValue,
|
|
667
|
+
candidate: SiteAuthConnectionValue,
|
|
668
|
+
): Promise<void> {
|
|
669
|
+
await assertCredentialAuthorities(
|
|
670
|
+
db,
|
|
671
|
+
workspaceId,
|
|
672
|
+
introducedCredentialRefs(
|
|
673
|
+
credentialRefsFromAuthorities(current.authorities),
|
|
674
|
+
credentialRefsFromAuthorities(candidate.authorities),
|
|
675
|
+
),
|
|
676
|
+
);
|
|
677
|
+
if (candidate.preferredIdentityId !== current.preferredIdentityId) {
|
|
678
|
+
await assertActiveBrowserIdentity(db, workspaceId, candidate.preferredIdentityId);
|
|
679
|
+
}
|
|
680
|
+
if (candidate.preferredNetworkRouteId !== current.preferredNetworkRouteId) {
|
|
681
|
+
await assertActiveNetworkRoute(db, workspaceId, candidate.preferredNetworkRouteId);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
export async function createSiteAuthConnection(
|
|
686
|
+
db: Database,
|
|
687
|
+
input: InteractionMutationScope & CreateSiteAuthConnectionRequestValue,
|
|
688
|
+
): Promise<SiteAuthConnectionMutationResponseValue> {
|
|
689
|
+
const request = CreateSiteAuthConnectionRequest.parse({
|
|
690
|
+
operationId: input.operationId,
|
|
691
|
+
name: input.name,
|
|
692
|
+
accountLabel: input.accountLabel,
|
|
693
|
+
origins: input.origins,
|
|
694
|
+
loginUrl: input.loginUrl,
|
|
695
|
+
verificationUrlPrefixes: input.verificationUrlPrefixes,
|
|
696
|
+
authorities: input.authorities,
|
|
697
|
+
methods: input.methods,
|
|
698
|
+
preferredIdentityId: input.preferredIdentityId,
|
|
699
|
+
preferredPlacement: input.preferredPlacement,
|
|
700
|
+
preferredNetworkRouteId: input.preferredNetworkRouteId,
|
|
701
|
+
healthPolicy: input.healthPolicy,
|
|
702
|
+
});
|
|
703
|
+
const { operationId: _operationId, ...digestRequest } = request;
|
|
704
|
+
const digest = operationDigest({
|
|
705
|
+
version: 1,
|
|
706
|
+
request: digestRequest,
|
|
707
|
+
actorSubjectId: input.actorSubjectId,
|
|
708
|
+
});
|
|
709
|
+
try {
|
|
710
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
711
|
+
await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
|
|
712
|
+
await setSubjectRlsContext(scopedDb, input.actorSubjectId);
|
|
713
|
+
await lockOperation(scopedDb, request.operationId);
|
|
714
|
+
const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
|
|
715
|
+
if (existing) {
|
|
716
|
+
assertOperation(existing, {
|
|
717
|
+
resourceKind: "site_auth_connection",
|
|
718
|
+
kind: "create",
|
|
719
|
+
requestDigest: digest,
|
|
720
|
+
actorSubjectId: input.actorSubjectId,
|
|
721
|
+
});
|
|
722
|
+
const response = SiteAuthConnectionMutationResponse.parse(existing.result);
|
|
723
|
+
return { ...response, replayed: true };
|
|
724
|
+
}
|
|
725
|
+
await validateSiteAuthReferences(scopedDb, input.workspaceId, {
|
|
726
|
+
authorities: request.authorities,
|
|
727
|
+
preferredIdentityId: request.preferredIdentityId,
|
|
728
|
+
preferredNetworkRouteId: request.preferredNetworkRouteId,
|
|
729
|
+
});
|
|
730
|
+
const id = randomUUID();
|
|
731
|
+
const [row] = await scopedDb
|
|
732
|
+
.insert(schema.siteAuthConnections)
|
|
733
|
+
.values({
|
|
734
|
+
id,
|
|
735
|
+
accountId: input.accountId,
|
|
736
|
+
workspaceId: input.workspaceId,
|
|
737
|
+
name: request.name,
|
|
738
|
+
accountLabel: request.accountLabel,
|
|
739
|
+
origins: request.origins,
|
|
740
|
+
loginUrl: request.loginUrl,
|
|
741
|
+
verificationUrlPrefixes: request.verificationUrlPrefixes,
|
|
742
|
+
authorities: request.authorities,
|
|
743
|
+
methods: request.methods,
|
|
744
|
+
preferredIdentityId: request.preferredIdentityId,
|
|
745
|
+
preferredPlacement: request.preferredPlacement,
|
|
746
|
+
preferredNetworkRouteId: request.preferredNetworkRouteId,
|
|
747
|
+
healthPolicy: request.healthPolicy,
|
|
748
|
+
createOperationId: request.operationId,
|
|
749
|
+
createdBySubjectId: input.actorSubjectId,
|
|
750
|
+
updatedBySubjectId: input.actorSubjectId,
|
|
751
|
+
})
|
|
752
|
+
.returning();
|
|
753
|
+
if (!row) throw new Error("Site auth connection insert returned no row");
|
|
754
|
+
const response = SiteAuthConnectionMutationResponse.parse({
|
|
755
|
+
connection: siteAuthConnectionFromRow(row),
|
|
756
|
+
operationId: request.operationId,
|
|
757
|
+
replayed: false,
|
|
758
|
+
});
|
|
759
|
+
await insertCompletedOperation(scopedDb, {
|
|
760
|
+
operationId: request.operationId,
|
|
761
|
+
accountId: input.accountId,
|
|
762
|
+
workspaceId: input.workspaceId,
|
|
763
|
+
resourceKind: "site_auth_connection",
|
|
764
|
+
resourceId: id,
|
|
765
|
+
kind: "create",
|
|
766
|
+
requestDigest: digest,
|
|
767
|
+
resultVersion: row.version,
|
|
768
|
+
result: response,
|
|
769
|
+
actorSubjectId: input.actorSubjectId,
|
|
770
|
+
});
|
|
771
|
+
await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
|
|
772
|
+
return response;
|
|
773
|
+
});
|
|
774
|
+
} catch (error) {
|
|
775
|
+
if (postgresConstraint(error) === "site_auth_connections_workspace_active_name_uq") {
|
|
776
|
+
throw new InteractionResourceConflictError(
|
|
777
|
+
"An active site auth connection already uses this name",
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
throw error;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
export async function updateSiteAuthConnection(
|
|
785
|
+
db: Database,
|
|
786
|
+
input: InteractionMutationScope & {
|
|
787
|
+
siteAuthConnectionId: string;
|
|
788
|
+
} & UpdateSiteAuthConnectionRequestValue,
|
|
789
|
+
): Promise<SiteAuthConnectionMutationResponseValue> {
|
|
790
|
+
const request = UpdateSiteAuthConnectionRequest.parse({
|
|
791
|
+
operationId: input.operationId,
|
|
792
|
+
expectedVersion: input.expectedVersion,
|
|
793
|
+
...(input.name !== undefined ? { name: input.name } : {}),
|
|
794
|
+
...(input.accountLabel !== undefined ? { accountLabel: input.accountLabel } : {}),
|
|
795
|
+
...(input.origins !== undefined ? { origins: input.origins } : {}),
|
|
796
|
+
...(input.loginUrl !== undefined ? { loginUrl: input.loginUrl } : {}),
|
|
797
|
+
...(input.verificationUrlPrefixes !== undefined
|
|
798
|
+
? { verificationUrlPrefixes: input.verificationUrlPrefixes }
|
|
799
|
+
: {}),
|
|
800
|
+
...(input.authorities !== undefined ? { authorities: input.authorities } : {}),
|
|
801
|
+
...(input.methods !== undefined ? { methods: input.methods } : {}),
|
|
802
|
+
...(input.preferredIdentityId !== undefined
|
|
803
|
+
? { preferredIdentityId: input.preferredIdentityId }
|
|
804
|
+
: {}),
|
|
805
|
+
...(input.preferredPlacement !== undefined
|
|
806
|
+
? { preferredPlacement: input.preferredPlacement }
|
|
807
|
+
: {}),
|
|
808
|
+
...(input.preferredNetworkRouteId !== undefined
|
|
809
|
+
? { preferredNetworkRouteId: input.preferredNetworkRouteId }
|
|
810
|
+
: {}),
|
|
811
|
+
...(input.healthPolicy !== undefined ? { healthPolicy: input.healthPolicy } : {}),
|
|
812
|
+
...(input.status !== undefined ? { status: input.status } : {}),
|
|
813
|
+
});
|
|
814
|
+
const {
|
|
815
|
+
operationId: _operationId,
|
|
816
|
+
expectedVersion: _expectedVersion,
|
|
817
|
+
...candidatePatch
|
|
818
|
+
} = request;
|
|
819
|
+
const digest = operationDigest({
|
|
820
|
+
version: 1,
|
|
821
|
+
siteAuthConnectionId: input.siteAuthConnectionId,
|
|
822
|
+
request: { expectedVersion: request.expectedVersion, ...candidatePatch },
|
|
823
|
+
actorSubjectId: input.actorSubjectId,
|
|
824
|
+
});
|
|
825
|
+
try {
|
|
826
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
827
|
+
await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
|
|
828
|
+
await setSubjectRlsContext(scopedDb, input.actorSubjectId);
|
|
829
|
+
await lockOperation(scopedDb, request.operationId);
|
|
830
|
+
const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
|
|
831
|
+
if (existing) {
|
|
832
|
+
assertOperation(existing, {
|
|
833
|
+
resourceKind: "site_auth_connection",
|
|
834
|
+
resourceId: input.siteAuthConnectionId,
|
|
835
|
+
kind: "update",
|
|
836
|
+
requestDigest: digest,
|
|
837
|
+
actorSubjectId: input.actorSubjectId,
|
|
838
|
+
});
|
|
839
|
+
const response = SiteAuthConnectionMutationResponse.parse(existing.result);
|
|
840
|
+
return { ...response, replayed: true };
|
|
841
|
+
}
|
|
842
|
+
const [current] = await scopedDb
|
|
843
|
+
.select()
|
|
844
|
+
.from(schema.siteAuthConnections)
|
|
845
|
+
.where(
|
|
846
|
+
and(
|
|
847
|
+
eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
|
|
848
|
+
eq(schema.siteAuthConnections.id, input.siteAuthConnectionId),
|
|
849
|
+
),
|
|
850
|
+
)
|
|
851
|
+
.for("update")
|
|
852
|
+
.limit(1);
|
|
853
|
+
if (!current) throw new InteractionResourceNotFoundError("Site auth connection not found");
|
|
854
|
+
if (current.version !== request.expectedVersion) {
|
|
855
|
+
throw new InteractionResourceConflictError(
|
|
856
|
+
"Site auth connection changed before this update",
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
const currentValue = siteAuthConnectionFromRow(current);
|
|
860
|
+
const candidate = SiteAuthConnection.parse({
|
|
861
|
+
...currentValue,
|
|
862
|
+
...candidatePatch,
|
|
863
|
+
id: current.id,
|
|
864
|
+
accountId: current.accountId,
|
|
865
|
+
workspaceId: current.workspaceId,
|
|
866
|
+
version: current.version + 1,
|
|
867
|
+
createdBySubjectId: current.createdBySubjectId,
|
|
868
|
+
createdAt: iso(current.createdAt),
|
|
869
|
+
updatedAt: new Date().toISOString(),
|
|
870
|
+
});
|
|
871
|
+
await validateSiteAuthUpdateReferences(scopedDb, input.workspaceId, currentValue, candidate);
|
|
872
|
+
const [row] = await scopedDb
|
|
873
|
+
.update(schema.siteAuthConnections)
|
|
874
|
+
.set({
|
|
875
|
+
name: candidate.name,
|
|
876
|
+
accountLabel: candidate.accountLabel,
|
|
877
|
+
status: candidate.status,
|
|
878
|
+
origins: candidate.origins,
|
|
879
|
+
loginUrl: candidate.loginUrl,
|
|
880
|
+
verificationUrlPrefixes: candidate.verificationUrlPrefixes,
|
|
881
|
+
authorities: candidate.authorities,
|
|
882
|
+
methods: candidate.methods,
|
|
883
|
+
preferredIdentityId: candidate.preferredIdentityId,
|
|
884
|
+
preferredPlacement: candidate.preferredPlacement,
|
|
885
|
+
preferredNetworkRouteId: candidate.preferredNetworkRouteId,
|
|
886
|
+
healthPolicy: candidate.healthPolicy,
|
|
887
|
+
version: candidate.version,
|
|
888
|
+
updatedBySubjectId: input.actorSubjectId,
|
|
889
|
+
updatedAt: sql`now()`,
|
|
890
|
+
})
|
|
891
|
+
.where(
|
|
892
|
+
and(
|
|
893
|
+
eq(schema.siteAuthConnections.workspaceId, input.workspaceId),
|
|
894
|
+
eq(schema.siteAuthConnections.id, input.siteAuthConnectionId),
|
|
895
|
+
eq(schema.siteAuthConnections.version, request.expectedVersion),
|
|
896
|
+
),
|
|
897
|
+
)
|
|
898
|
+
.returning();
|
|
899
|
+
if (!row) {
|
|
900
|
+
throw new InteractionResourceConflictError("Site auth connection update lost its fence");
|
|
901
|
+
}
|
|
902
|
+
const response = SiteAuthConnectionMutationResponse.parse({
|
|
903
|
+
connection: siteAuthConnectionFromRow(row),
|
|
904
|
+
operationId: request.operationId,
|
|
905
|
+
replayed: false,
|
|
906
|
+
});
|
|
907
|
+
await insertCompletedOperation(scopedDb, {
|
|
908
|
+
operationId: request.operationId,
|
|
909
|
+
accountId: input.accountId,
|
|
910
|
+
workspaceId: input.workspaceId,
|
|
911
|
+
resourceKind: "site_auth_connection",
|
|
912
|
+
resourceId: input.siteAuthConnectionId,
|
|
913
|
+
kind: "update",
|
|
914
|
+
requestDigest: digest,
|
|
915
|
+
resultVersion: row.version,
|
|
916
|
+
result: response,
|
|
917
|
+
actorSubjectId: input.actorSubjectId,
|
|
918
|
+
});
|
|
919
|
+
await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
|
|
920
|
+
return response;
|
|
921
|
+
});
|
|
922
|
+
} catch (error) {
|
|
923
|
+
if (postgresConstraint(error) === "site_auth_connections_workspace_active_name_uq") {
|
|
924
|
+
throw new InteractionResourceConflictError(
|
|
925
|
+
"An active site auth connection already uses this name",
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
throw error;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function authRunFromRow(row: AuthRunRow): AuthRunValue {
|
|
933
|
+
return AuthRun.parse({
|
|
934
|
+
id: row.id,
|
|
935
|
+
accountId: row.accountId,
|
|
936
|
+
workspaceId: row.workspaceId,
|
|
937
|
+
siteAuthConnectionId: row.siteAuthConnectionId,
|
|
938
|
+
browserSessionId: row.browserSessionId,
|
|
939
|
+
targetId: row.targetId,
|
|
940
|
+
controllerGeneration: row.controllerGeneration,
|
|
941
|
+
targetGeneration: row.targetGeneration,
|
|
942
|
+
documentGeneration: row.documentGeneration,
|
|
943
|
+
methodId: row.methodId,
|
|
944
|
+
authorityId: row.authorityId,
|
|
945
|
+
state: row.state,
|
|
946
|
+
choices: row.choices,
|
|
947
|
+
pendingFields: row.pendingFields,
|
|
948
|
+
externalAction: row.externalAction,
|
|
949
|
+
interventionId: row.interventionId,
|
|
950
|
+
verifiedUrl: row.verifiedUrl,
|
|
951
|
+
failureCode: row.failureCode,
|
|
952
|
+
version: row.version,
|
|
953
|
+
operationId: row.operationId,
|
|
954
|
+
createdBySubjectId: row.createdBySubjectId,
|
|
955
|
+
createdAt: iso(row.createdAt),
|
|
956
|
+
updatedAt: iso(row.updatedAt),
|
|
957
|
+
settledAt: row.settledAt ? iso(row.settledAt) : null,
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
async function loadAuthRunRow(
|
|
962
|
+
db: Database,
|
|
963
|
+
workspaceId: string,
|
|
964
|
+
authRunId: string,
|
|
965
|
+
): Promise<AuthRunRow | null> {
|
|
966
|
+
const [row] = await db
|
|
967
|
+
.select()
|
|
968
|
+
.from(schema.authRuns)
|
|
969
|
+
.where(and(eq(schema.authRuns.workspaceId, workspaceId), eq(schema.authRuns.id, authRunId)))
|
|
970
|
+
.limit(1);
|
|
971
|
+
return row ?? null;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function assertAuthSelection(
|
|
975
|
+
connection: SiteAuthConnectionValue,
|
|
976
|
+
selection: { methodId?: string | null; authorityId?: string | null },
|
|
977
|
+
): void {
|
|
978
|
+
const method = selection.methodId
|
|
979
|
+
? connection.methods.find((candidate) => candidate.id === selection.methodId)
|
|
980
|
+
: null;
|
|
981
|
+
const authority = selection.authorityId
|
|
982
|
+
? connection.authorities.find((candidate) => candidate.id === selection.authorityId)
|
|
983
|
+
: null;
|
|
984
|
+
if (selection.methodId && !method) {
|
|
985
|
+
throw new InteractionResourceStateError("Auth method is not configured");
|
|
986
|
+
}
|
|
987
|
+
if (selection.authorityId && !authority) {
|
|
988
|
+
throw new InteractionResourceStateError("Auth authority is not configured");
|
|
989
|
+
}
|
|
990
|
+
if (method && authority && !method.authorityIds.includes(authority.id)) {
|
|
991
|
+
throw new InteractionResourceStateError(
|
|
992
|
+
"Auth authority does not belong to the selected method",
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const AUTH_RUN_TRANSITIONS: Readonly<
|
|
998
|
+
Record<AuthRunRow["state"], ReadonlySet<AuthRunRow["state"]>>
|
|
999
|
+
> = {
|
|
1000
|
+
discovering: new Set([
|
|
1001
|
+
"awaiting_choice",
|
|
1002
|
+
"awaiting_secret",
|
|
1003
|
+
"awaiting_external_action",
|
|
1004
|
+
"working",
|
|
1005
|
+
"failed",
|
|
1006
|
+
"cancelled",
|
|
1007
|
+
]),
|
|
1008
|
+
awaiting_choice: new Set([
|
|
1009
|
+
"awaiting_choice",
|
|
1010
|
+
"awaiting_secret",
|
|
1011
|
+
"awaiting_external_action",
|
|
1012
|
+
"working",
|
|
1013
|
+
"failed",
|
|
1014
|
+
"cancelled",
|
|
1015
|
+
]),
|
|
1016
|
+
awaiting_secret: new Set([
|
|
1017
|
+
"awaiting_secret",
|
|
1018
|
+
"awaiting_external_action",
|
|
1019
|
+
"working",
|
|
1020
|
+
"failed",
|
|
1021
|
+
"cancelled",
|
|
1022
|
+
]),
|
|
1023
|
+
awaiting_external_action: new Set([
|
|
1024
|
+
"awaiting_external_action",
|
|
1025
|
+
"awaiting_secret",
|
|
1026
|
+
"working",
|
|
1027
|
+
"failed",
|
|
1028
|
+
"cancelled",
|
|
1029
|
+
]),
|
|
1030
|
+
working: new Set([
|
|
1031
|
+
"working",
|
|
1032
|
+
"awaiting_choice",
|
|
1033
|
+
"awaiting_secret",
|
|
1034
|
+
"awaiting_external_action",
|
|
1035
|
+
"failed",
|
|
1036
|
+
"cancelled",
|
|
1037
|
+
]),
|
|
1038
|
+
verified: new Set(),
|
|
1039
|
+
failed: new Set(),
|
|
1040
|
+
cancelled: new Set(),
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
export async function listAuthRuns(
|
|
1044
|
+
db: Database,
|
|
1045
|
+
input: {
|
|
1046
|
+
accountId: string;
|
|
1047
|
+
workspaceId: string;
|
|
1048
|
+
browserSessionId?: string;
|
|
1049
|
+
siteAuthConnectionId?: string;
|
|
1050
|
+
includeSettled?: boolean;
|
|
1051
|
+
},
|
|
1052
|
+
): Promise<AuthRunListResponseValue> {
|
|
1053
|
+
return await withRlsContext(
|
|
1054
|
+
db,
|
|
1055
|
+
input,
|
|
1056
|
+
async (scopedDb) => {
|
|
1057
|
+
const predicates = [eq(schema.authRuns.workspaceId, input.workspaceId)];
|
|
1058
|
+
if (input.browserSessionId) {
|
|
1059
|
+
predicates.push(eq(schema.authRuns.browserSessionId, input.browserSessionId));
|
|
1060
|
+
}
|
|
1061
|
+
if (input.siteAuthConnectionId) {
|
|
1062
|
+
predicates.push(eq(schema.authRuns.siteAuthConnectionId, input.siteAuthConnectionId));
|
|
1063
|
+
}
|
|
1064
|
+
if (!input.includeSettled) predicates.push(sql`${schema.authRuns.settledAt} is null`);
|
|
1065
|
+
const runs = await scopedDb
|
|
1066
|
+
.select()
|
|
1067
|
+
.from(schema.authRuns)
|
|
1068
|
+
.where(and(...predicates))
|
|
1069
|
+
.orderBy(asc(schema.authRuns.createdAt), asc(schema.authRuns.id));
|
|
1070
|
+
return AuthRunListResponse.parse({ runs: runs.map(authRunFromRow) });
|
|
1071
|
+
},
|
|
1072
|
+
CONSISTENT_READ,
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
export async function getAuthRun(
|
|
1077
|
+
db: Database,
|
|
1078
|
+
input: { accountId: string; workspaceId: string; authRunId: string },
|
|
1079
|
+
): Promise<AuthRunValue> {
|
|
1080
|
+
return await withRlsContext(
|
|
1081
|
+
db,
|
|
1082
|
+
input,
|
|
1083
|
+
async (scopedDb) => {
|
|
1084
|
+
const row = await loadAuthRunRow(scopedDb, input.workspaceId, input.authRunId);
|
|
1085
|
+
if (!row) throw new InteractionResourceNotFoundError("Auth run not found");
|
|
1086
|
+
return authRunFromRow(row);
|
|
1087
|
+
},
|
|
1088
|
+
CONSISTENT_READ,
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
export async function startAuthRun(
|
|
1093
|
+
db: Database,
|
|
1094
|
+
input: InteractionMutationScope & { browserSessionId: string } & StartAuthRunRequestValue,
|
|
1095
|
+
): Promise<AuthRunMutationResponseValue> {
|
|
1096
|
+
const request = StartAuthRunRequest.parse({
|
|
1097
|
+
operationId: input.operationId,
|
|
1098
|
+
siteAuthConnectionId: input.siteAuthConnectionId,
|
|
1099
|
+
targetId: input.targetId,
|
|
1100
|
+
expectedTargetGeneration: input.expectedTargetGeneration,
|
|
1101
|
+
expectedDocumentGeneration: input.expectedDocumentGeneration,
|
|
1102
|
+
...(input.methodId !== undefined ? { methodId: input.methodId } : {}),
|
|
1103
|
+
...(input.authorityId !== undefined ? { authorityId: input.authorityId } : {}),
|
|
1104
|
+
});
|
|
1105
|
+
const { operationId: _operationId, ...digestRequest } = request;
|
|
1106
|
+
const digest = operationDigest({
|
|
1107
|
+
version: 1,
|
|
1108
|
+
browserSessionId: input.browserSessionId,
|
|
1109
|
+
request: digestRequest,
|
|
1110
|
+
actorSubjectId: input.actorSubjectId,
|
|
1111
|
+
});
|
|
1112
|
+
try {
|
|
1113
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
1114
|
+
await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
|
|
1115
|
+
await lockOperation(scopedDb, request.operationId);
|
|
1116
|
+
const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
|
|
1117
|
+
if (existing) {
|
|
1118
|
+
assertOperation(existing, {
|
|
1119
|
+
resourceKind: "auth_run",
|
|
1120
|
+
kind: "start",
|
|
1121
|
+
requestDigest: digest,
|
|
1122
|
+
actorSubjectId: input.actorSubjectId,
|
|
1123
|
+
});
|
|
1124
|
+
const response = AuthRunMutationResponse.parse(existing.result);
|
|
1125
|
+
return { ...response, replayed: true };
|
|
1126
|
+
}
|
|
1127
|
+
const [browser] = await scopedDb
|
|
1128
|
+
.select()
|
|
1129
|
+
.from(schema.browserSessions)
|
|
1130
|
+
.where(
|
|
1131
|
+
and(
|
|
1132
|
+
eq(schema.browserSessions.workspaceId, input.workspaceId),
|
|
1133
|
+
eq(schema.browserSessions.id, input.browserSessionId),
|
|
1134
|
+
),
|
|
1135
|
+
)
|
|
1136
|
+
.for("update")
|
|
1137
|
+
.limit(1);
|
|
1138
|
+
if (!browser) throw new InteractionResourceNotFoundError("Browser session not found");
|
|
1139
|
+
if (browser.lifecycle !== "active" || !browser.controllerGeneration) {
|
|
1140
|
+
throw new InteractionResourceStateError("Browser session is not active");
|
|
1141
|
+
}
|
|
1142
|
+
const authConnection = await getSiteAuthConnectionRow(
|
|
1143
|
+
scopedDb,
|
|
1144
|
+
input.workspaceId,
|
|
1145
|
+
request.siteAuthConnectionId,
|
|
1146
|
+
);
|
|
1147
|
+
if (authConnection.status !== "active") {
|
|
1148
|
+
throw new InteractionResourceStateError("Site auth connection is archived");
|
|
1149
|
+
}
|
|
1150
|
+
const connection = siteAuthConnectionFromRow(authConnection);
|
|
1151
|
+
assertAuthSelection(connection, {
|
|
1152
|
+
methodId: request.methodId ?? null,
|
|
1153
|
+
authorityId: request.authorityId ?? null,
|
|
1154
|
+
});
|
|
1155
|
+
const id = randomUUID();
|
|
1156
|
+
const [row] = await scopedDb
|
|
1157
|
+
.insert(schema.authRuns)
|
|
1158
|
+
.values({
|
|
1159
|
+
id,
|
|
1160
|
+
accountId: input.accountId,
|
|
1161
|
+
workspaceId: input.workspaceId,
|
|
1162
|
+
siteAuthConnectionId: request.siteAuthConnectionId,
|
|
1163
|
+
browserSessionId: input.browserSessionId,
|
|
1164
|
+
targetId: request.targetId,
|
|
1165
|
+
controllerGeneration: browser.controllerGeneration,
|
|
1166
|
+
targetGeneration: request.expectedTargetGeneration,
|
|
1167
|
+
documentGeneration: request.expectedDocumentGeneration,
|
|
1168
|
+
methodId: request.methodId ?? null,
|
|
1169
|
+
authorityId: request.authorityId ?? null,
|
|
1170
|
+
operationId: request.operationId,
|
|
1171
|
+
createdBySubjectId: input.actorSubjectId,
|
|
1172
|
+
})
|
|
1173
|
+
.returning();
|
|
1174
|
+
if (!row) throw new Error("Auth run insert returned no row");
|
|
1175
|
+
const response = AuthRunMutationResponse.parse({
|
|
1176
|
+
run: authRunFromRow(row),
|
|
1177
|
+
operationId: request.operationId,
|
|
1178
|
+
replayed: false,
|
|
1179
|
+
});
|
|
1180
|
+
await insertCompletedOperation(scopedDb, {
|
|
1181
|
+
operationId: request.operationId,
|
|
1182
|
+
accountId: input.accountId,
|
|
1183
|
+
workspaceId: input.workspaceId,
|
|
1184
|
+
resourceKind: "auth_run",
|
|
1185
|
+
resourceId: id,
|
|
1186
|
+
kind: "start",
|
|
1187
|
+
requestDigest: digest,
|
|
1188
|
+
resultVersion: row.version,
|
|
1189
|
+
result: response,
|
|
1190
|
+
actorSubjectId: input.actorSubjectId,
|
|
1191
|
+
});
|
|
1192
|
+
await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
|
|
1193
|
+
return response;
|
|
1194
|
+
});
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
if (postgresConstraint(error) === "auth_runs_active_browser_target_uq") {
|
|
1197
|
+
throw new InteractionResourceConflictError(
|
|
1198
|
+
"This browser target already has an active auth run",
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
throw error;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
export async function reportAuthRun(
|
|
1206
|
+
db: Database,
|
|
1207
|
+
input: InteractionMutationScope & {
|
|
1208
|
+
authRunId: string;
|
|
1209
|
+
controllerGeneration: string;
|
|
1210
|
+
} & ReportAuthRunRequestValue,
|
|
1211
|
+
): Promise<AuthRunMutationResponseValue> {
|
|
1212
|
+
const request = ReportAuthRunRequest.parse({
|
|
1213
|
+
operationId: input.operationId,
|
|
1214
|
+
expectedVersion: input.expectedVersion,
|
|
1215
|
+
...(input.methodId !== undefined ? { methodId: input.methodId } : {}),
|
|
1216
|
+
...(input.authorityId !== undefined ? { authorityId: input.authorityId } : {}),
|
|
1217
|
+
state: input.state,
|
|
1218
|
+
...(input.choices !== undefined ? { choices: input.choices } : {}),
|
|
1219
|
+
...(input.pendingFields !== undefined ? { pendingFields: input.pendingFields } : {}),
|
|
1220
|
+
...(input.externalAction !== undefined ? { externalAction: input.externalAction } : {}),
|
|
1221
|
+
...(input.failureCode !== undefined ? { failureCode: input.failureCode } : {}),
|
|
1222
|
+
});
|
|
1223
|
+
const { operationId: _operationId, ...digestRequest } = request;
|
|
1224
|
+
const digest = operationDigest({
|
|
1225
|
+
version: 1,
|
|
1226
|
+
authRunId: input.authRunId,
|
|
1227
|
+
controllerGeneration: input.controllerGeneration,
|
|
1228
|
+
request: digestRequest,
|
|
1229
|
+
actorSubjectId: input.actorSubjectId,
|
|
1230
|
+
});
|
|
1231
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
1232
|
+
await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
|
|
1233
|
+
await lockOperation(scopedDb, request.operationId);
|
|
1234
|
+
const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
|
|
1235
|
+
if (existing) {
|
|
1236
|
+
assertOperation(existing, {
|
|
1237
|
+
resourceKind: "auth_run",
|
|
1238
|
+
resourceId: input.authRunId,
|
|
1239
|
+
kind: "report",
|
|
1240
|
+
requestDigest: digest,
|
|
1241
|
+
actorSubjectId: input.actorSubjectId,
|
|
1242
|
+
});
|
|
1243
|
+
const response = AuthRunMutationResponse.parse(existing.result);
|
|
1244
|
+
return { ...response, replayed: true };
|
|
1245
|
+
}
|
|
1246
|
+
const [current] = await scopedDb
|
|
1247
|
+
.select()
|
|
1248
|
+
.from(schema.authRuns)
|
|
1249
|
+
.where(
|
|
1250
|
+
and(
|
|
1251
|
+
eq(schema.authRuns.workspaceId, input.workspaceId),
|
|
1252
|
+
eq(schema.authRuns.id, input.authRunId),
|
|
1253
|
+
),
|
|
1254
|
+
)
|
|
1255
|
+
.for("update")
|
|
1256
|
+
.limit(1);
|
|
1257
|
+
if (!current) throw new InteractionResourceNotFoundError("Auth run not found");
|
|
1258
|
+
if (current.controllerGeneration !== input.controllerGeneration) {
|
|
1259
|
+
throw new InteractionResourceConflictError("Auth run belongs to a stale browser controller");
|
|
1260
|
+
}
|
|
1261
|
+
if (current.version !== request.expectedVersion) {
|
|
1262
|
+
throw new InteractionResourceConflictError("Auth run changed before this report");
|
|
1263
|
+
}
|
|
1264
|
+
if (!AUTH_RUN_TRANSITIONS[current.state].has(request.state)) {
|
|
1265
|
+
throw new InteractionResourceStateError(
|
|
1266
|
+
`Auth run cannot transition from ${current.state} to ${request.state}`,
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
const authConnection = await getSiteAuthConnectionRow(
|
|
1270
|
+
scopedDb,
|
|
1271
|
+
input.workspaceId,
|
|
1272
|
+
current.siteAuthConnectionId,
|
|
1273
|
+
);
|
|
1274
|
+
assertAuthSelection(siteAuthConnectionFromRow(authConnection), {
|
|
1275
|
+
methodId: request.methodId ?? current.methodId,
|
|
1276
|
+
authorityId: request.authorityId ?? current.authorityId,
|
|
1277
|
+
});
|
|
1278
|
+
const settled = request.state === "failed" || request.state === "cancelled";
|
|
1279
|
+
const [row] = await scopedDb
|
|
1280
|
+
.update(schema.authRuns)
|
|
1281
|
+
.set({
|
|
1282
|
+
methodId: request.methodId ?? current.methodId,
|
|
1283
|
+
authorityId: request.authorityId ?? current.authorityId,
|
|
1284
|
+
state: request.state,
|
|
1285
|
+
choices: request.choices ?? [],
|
|
1286
|
+
pendingFields: request.pendingFields ?? [],
|
|
1287
|
+
externalAction: request.externalAction ?? null,
|
|
1288
|
+
failureCode: request.state === "failed" ? request.failureCode! : null,
|
|
1289
|
+
version: current.version + 1,
|
|
1290
|
+
updatedAt: sql`now()`,
|
|
1291
|
+
settledAt: settled ? sql`now()` : null,
|
|
1292
|
+
})
|
|
1293
|
+
.where(
|
|
1294
|
+
and(
|
|
1295
|
+
eq(schema.authRuns.workspaceId, input.workspaceId),
|
|
1296
|
+
eq(schema.authRuns.id, input.authRunId),
|
|
1297
|
+
eq(schema.authRuns.version, request.expectedVersion),
|
|
1298
|
+
eq(schema.authRuns.controllerGeneration, input.controllerGeneration),
|
|
1299
|
+
),
|
|
1300
|
+
)
|
|
1301
|
+
.returning();
|
|
1302
|
+
if (!row) throw new InteractionResourceConflictError("Auth run report lost its fence");
|
|
1303
|
+
const response = AuthRunMutationResponse.parse({
|
|
1304
|
+
run: authRunFromRow(row),
|
|
1305
|
+
operationId: request.operationId,
|
|
1306
|
+
replayed: false,
|
|
1307
|
+
});
|
|
1308
|
+
await insertCompletedOperation(scopedDb, {
|
|
1309
|
+
operationId: request.operationId,
|
|
1310
|
+
accountId: input.accountId,
|
|
1311
|
+
workspaceId: input.workspaceId,
|
|
1312
|
+
resourceKind: "auth_run",
|
|
1313
|
+
resourceId: input.authRunId,
|
|
1314
|
+
kind: "report",
|
|
1315
|
+
requestDigest: digest,
|
|
1316
|
+
resultVersion: row.version,
|
|
1317
|
+
result: response,
|
|
1318
|
+
actorSubjectId: input.actorSubjectId,
|
|
1319
|
+
});
|
|
1320
|
+
await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
|
|
1321
|
+
return response;
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
async function getSiteAuthConnectionRow(
|
|
1326
|
+
db: Database,
|
|
1327
|
+
workspaceId: string,
|
|
1328
|
+
siteAuthConnectionId: string,
|
|
1329
|
+
): Promise<SiteAuthConnectionRow> {
|
|
1330
|
+
const [row] = await db
|
|
1331
|
+
.select()
|
|
1332
|
+
.from(schema.siteAuthConnections)
|
|
1333
|
+
.where(
|
|
1334
|
+
and(
|
|
1335
|
+
eq(schema.siteAuthConnections.workspaceId, workspaceId),
|
|
1336
|
+
eq(schema.siteAuthConnections.id, siteAuthConnectionId),
|
|
1337
|
+
),
|
|
1338
|
+
)
|
|
1339
|
+
.limit(1);
|
|
1340
|
+
if (!row) throw new InteractionResourceNotFoundError("Site auth connection not found");
|
|
1341
|
+
return row;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function interventionFromRow(row: InterventionRow): InteractionInterventionValue {
|
|
1345
|
+
return InteractionIntervention.parse({
|
|
1346
|
+
id: row.id,
|
|
1347
|
+
accountId: row.accountId,
|
|
1348
|
+
workspaceId: row.workspaceId,
|
|
1349
|
+
resourceKind: row.resourceKind,
|
|
1350
|
+
resourceId: row.resourceId,
|
|
1351
|
+
targetId: row.targetId,
|
|
1352
|
+
controllerGeneration: row.controllerGeneration,
|
|
1353
|
+
targetGeneration: row.targetGeneration,
|
|
1354
|
+
documentGeneration: row.documentGeneration,
|
|
1355
|
+
kind: row.kind,
|
|
1356
|
+
reason: row.reason,
|
|
1357
|
+
status: row.status,
|
|
1358
|
+
authRunId: row.authRunId,
|
|
1359
|
+
originatingSessionId: row.originatingSessionId,
|
|
1360
|
+
originatingTurnId: row.originatingTurnId,
|
|
1361
|
+
originatingAttemptId: row.originatingAttemptId,
|
|
1362
|
+
originatingToolOperationId: row.originatingToolOperationId,
|
|
1363
|
+
responseActorSubjectId: row.responseActorSubjectId,
|
|
1364
|
+
version: row.version,
|
|
1365
|
+
operationId: row.operationId,
|
|
1366
|
+
expiresAt: iso(row.expiresAt),
|
|
1367
|
+
createdAt: iso(row.createdAt),
|
|
1368
|
+
updatedAt: iso(row.updatedAt),
|
|
1369
|
+
settledAt: row.settledAt ? iso(row.settledAt) : null,
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
async function loadInterventionRow(
|
|
1374
|
+
db: Database,
|
|
1375
|
+
workspaceId: string,
|
|
1376
|
+
interventionId: string,
|
|
1377
|
+
): Promise<InterventionRow | null> {
|
|
1378
|
+
const [row] = await db
|
|
1379
|
+
.select()
|
|
1380
|
+
.from(schema.interactionInterventions)
|
|
1381
|
+
.where(
|
|
1382
|
+
and(
|
|
1383
|
+
eq(schema.interactionInterventions.workspaceId, workspaceId),
|
|
1384
|
+
eq(schema.interactionInterventions.id, interventionId),
|
|
1385
|
+
),
|
|
1386
|
+
)
|
|
1387
|
+
.limit(1);
|
|
1388
|
+
return row ?? null;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
async function assertActiveInteractionResource(
|
|
1392
|
+
db: Database,
|
|
1393
|
+
input: {
|
|
1394
|
+
workspaceId: string;
|
|
1395
|
+
resourceKind: "browser_session" | "computer_session";
|
|
1396
|
+
resourceId: string;
|
|
1397
|
+
expectedControllerGeneration: string;
|
|
1398
|
+
},
|
|
1399
|
+
): Promise<void> {
|
|
1400
|
+
if (input.resourceKind === "browser_session") {
|
|
1401
|
+
const [resource] = await db
|
|
1402
|
+
.select({
|
|
1403
|
+
lifecycle: schema.browserSessions.lifecycle,
|
|
1404
|
+
controllerGeneration: schema.browserSessions.controllerGeneration,
|
|
1405
|
+
})
|
|
1406
|
+
.from(schema.browserSessions)
|
|
1407
|
+
.where(
|
|
1408
|
+
and(
|
|
1409
|
+
eq(schema.browserSessions.workspaceId, input.workspaceId),
|
|
1410
|
+
eq(schema.browserSessions.id, input.resourceId),
|
|
1411
|
+
),
|
|
1412
|
+
)
|
|
1413
|
+
.for("update")
|
|
1414
|
+
.limit(1);
|
|
1415
|
+
if (!resource) throw new InteractionResourceNotFoundError("Browser session not found");
|
|
1416
|
+
if (resource.lifecycle !== "active" || !resource.controllerGeneration) {
|
|
1417
|
+
throw new InteractionResourceStateError("Browser session is not active");
|
|
1418
|
+
}
|
|
1419
|
+
if (resource.controllerGeneration !== input.expectedControllerGeneration) {
|
|
1420
|
+
throw new InteractionResourceConflictError(
|
|
1421
|
+
"Browser intervention belongs to a stale controller",
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
const [resource] = await db
|
|
1427
|
+
.select({
|
|
1428
|
+
lifecycle: schema.computerSessions.lifecycle,
|
|
1429
|
+
controllerGeneration: schema.computerSessions.controllerGeneration,
|
|
1430
|
+
})
|
|
1431
|
+
.from(schema.computerSessions)
|
|
1432
|
+
.where(
|
|
1433
|
+
and(
|
|
1434
|
+
eq(schema.computerSessions.workspaceId, input.workspaceId),
|
|
1435
|
+
eq(schema.computerSessions.id, input.resourceId),
|
|
1436
|
+
),
|
|
1437
|
+
)
|
|
1438
|
+
.for("update")
|
|
1439
|
+
.limit(1);
|
|
1440
|
+
if (!resource) throw new InteractionResourceNotFoundError("Computer session not found");
|
|
1441
|
+
if (resource.lifecycle !== "active" || !resource.controllerGeneration) {
|
|
1442
|
+
throw new InteractionResourceStateError("Computer session is not active");
|
|
1443
|
+
}
|
|
1444
|
+
if (resource.controllerGeneration !== input.expectedControllerGeneration) {
|
|
1445
|
+
throw new InteractionResourceConflictError(
|
|
1446
|
+
"Computer intervention belongs to a stale controller",
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
async function settleLinkedAuthRun(
|
|
1452
|
+
db: Database,
|
|
1453
|
+
input: {
|
|
1454
|
+
workspaceId: string;
|
|
1455
|
+
authRunId: string | null;
|
|
1456
|
+
interventionId: string;
|
|
1457
|
+
outcome: "completed" | "dismissed" | "expired" | "cancelled";
|
|
1458
|
+
},
|
|
1459
|
+
): Promise<void> {
|
|
1460
|
+
if (!input.authRunId) return;
|
|
1461
|
+
const [run] = await db
|
|
1462
|
+
.select()
|
|
1463
|
+
.from(schema.authRuns)
|
|
1464
|
+
.where(
|
|
1465
|
+
and(
|
|
1466
|
+
eq(schema.authRuns.workspaceId, input.workspaceId),
|
|
1467
|
+
eq(schema.authRuns.id, input.authRunId),
|
|
1468
|
+
),
|
|
1469
|
+
)
|
|
1470
|
+
.for("update")
|
|
1471
|
+
.limit(1);
|
|
1472
|
+
if (!run || run.interventionId !== input.interventionId || run.settledAt) return;
|
|
1473
|
+
const resumed = input.outcome === "completed";
|
|
1474
|
+
const [settled] = await db
|
|
1475
|
+
.update(schema.authRuns)
|
|
1476
|
+
.set({
|
|
1477
|
+
state: resumed ? "working" : input.outcome === "cancelled" ? "cancelled" : "failed",
|
|
1478
|
+
choices: [],
|
|
1479
|
+
pendingFields: [],
|
|
1480
|
+
externalAction: null,
|
|
1481
|
+
interventionId: null,
|
|
1482
|
+
failureCode:
|
|
1483
|
+
resumed || input.outcome === "cancelled" ? null : `intervention_${input.outcome}`,
|
|
1484
|
+
version: run.version + 1,
|
|
1485
|
+
updatedAt: sql`now()`,
|
|
1486
|
+
settledAt: resumed ? null : sql`now()`,
|
|
1487
|
+
})
|
|
1488
|
+
.where(
|
|
1489
|
+
and(
|
|
1490
|
+
eq(schema.authRuns.workspaceId, input.workspaceId),
|
|
1491
|
+
eq(schema.authRuns.id, input.authRunId),
|
|
1492
|
+
eq(schema.authRuns.version, run.version),
|
|
1493
|
+
eq(schema.authRuns.interventionId, input.interventionId),
|
|
1494
|
+
),
|
|
1495
|
+
)
|
|
1496
|
+
.returning({ id: schema.authRuns.id });
|
|
1497
|
+
if (!settled) {
|
|
1498
|
+
throw new InteractionResourceConflictError(
|
|
1499
|
+
"Linked auth run changed before intervention settlement",
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
async function expireInterventionsInScope(
|
|
1505
|
+
db: Database,
|
|
1506
|
+
input: { accountId: string; workspaceId: string },
|
|
1507
|
+
): Promise<number> {
|
|
1508
|
+
const expired = await db
|
|
1509
|
+
.update(schema.interactionInterventions)
|
|
1510
|
+
.set({
|
|
1511
|
+
status: "expired",
|
|
1512
|
+
version: sql`${schema.interactionInterventions.version} + 1`,
|
|
1513
|
+
updatedAt: sql`now()`,
|
|
1514
|
+
settledAt: sql`now()`,
|
|
1515
|
+
})
|
|
1516
|
+
.where(
|
|
1517
|
+
and(
|
|
1518
|
+
eq(schema.interactionInterventions.workspaceId, input.workspaceId),
|
|
1519
|
+
eq(schema.interactionInterventions.status, "open"),
|
|
1520
|
+
sql`${schema.interactionInterventions.expiresAt} <= now()`,
|
|
1521
|
+
),
|
|
1522
|
+
)
|
|
1523
|
+
.returning({
|
|
1524
|
+
id: schema.interactionInterventions.id,
|
|
1525
|
+
authRunId: schema.interactionInterventions.authRunId,
|
|
1526
|
+
});
|
|
1527
|
+
for (const intervention of expired) {
|
|
1528
|
+
await settleLinkedAuthRun(db, {
|
|
1529
|
+
workspaceId: input.workspaceId,
|
|
1530
|
+
authRunId: intervention.authRunId,
|
|
1531
|
+
interventionId: intervention.id,
|
|
1532
|
+
outcome: "expired",
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
if (expired.length > 0) {
|
|
1536
|
+
await advanceWorkspaceInteractionRevision(db, input.accountId, input.workspaceId);
|
|
1537
|
+
}
|
|
1538
|
+
return expired.length;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
export async function listInteractionInterventions(
|
|
1542
|
+
db: Database,
|
|
1543
|
+
input: {
|
|
1544
|
+
accountId: string;
|
|
1545
|
+
workspaceId: string;
|
|
1546
|
+
resourceKind?: "browser_session" | "computer_session";
|
|
1547
|
+
resourceId?: string;
|
|
1548
|
+
includeSettled?: boolean;
|
|
1549
|
+
},
|
|
1550
|
+
): Promise<InteractionInterventionListResponseValue> {
|
|
1551
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
1552
|
+
await expireInterventionsInScope(scopedDb, input);
|
|
1553
|
+
const predicates = [eq(schema.interactionInterventions.workspaceId, input.workspaceId)];
|
|
1554
|
+
if (input.resourceKind) {
|
|
1555
|
+
predicates.push(eq(schema.interactionInterventions.resourceKind, input.resourceKind));
|
|
1556
|
+
}
|
|
1557
|
+
if (input.resourceId) {
|
|
1558
|
+
predicates.push(eq(schema.interactionInterventions.resourceId, input.resourceId));
|
|
1559
|
+
}
|
|
1560
|
+
if (!input.includeSettled) {
|
|
1561
|
+
predicates.push(eq(schema.interactionInterventions.status, "open"));
|
|
1562
|
+
}
|
|
1563
|
+
const interventions = await scopedDb
|
|
1564
|
+
.select()
|
|
1565
|
+
.from(schema.interactionInterventions)
|
|
1566
|
+
.where(and(...predicates))
|
|
1567
|
+
.orderBy(
|
|
1568
|
+
asc(schema.interactionInterventions.createdAt),
|
|
1569
|
+
asc(schema.interactionInterventions.id),
|
|
1570
|
+
);
|
|
1571
|
+
return InteractionInterventionListResponse.parse({
|
|
1572
|
+
interventions: interventions.map(interventionFromRow),
|
|
1573
|
+
});
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
export async function getInteractionIntervention(
|
|
1578
|
+
db: Database,
|
|
1579
|
+
input: { accountId: string; workspaceId: string; interventionId: string },
|
|
1580
|
+
): Promise<InteractionInterventionValue> {
|
|
1581
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
1582
|
+
await expireInterventionsInScope(scopedDb, input);
|
|
1583
|
+
const row = await loadInterventionRow(scopedDb, input.workspaceId, input.interventionId);
|
|
1584
|
+
if (!row) throw new InteractionResourceNotFoundError("Interaction intervention not found");
|
|
1585
|
+
return interventionFromRow(row);
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
export async function createInteractionIntervention(
|
|
1590
|
+
db: Database,
|
|
1591
|
+
input: InteractionMutationScope &
|
|
1592
|
+
CreateInteractionInterventionRequestValue & {
|
|
1593
|
+
originatingSessionId: string;
|
|
1594
|
+
originatingTurnId?: string | null;
|
|
1595
|
+
originatingAttemptId?: string | null;
|
|
1596
|
+
originatingToolOperationId?: string | null;
|
|
1597
|
+
},
|
|
1598
|
+
): Promise<InteractionInterventionMutationResponseValue> {
|
|
1599
|
+
const request = CreateInteractionInterventionRequest.parse({
|
|
1600
|
+
operationId: input.operationId,
|
|
1601
|
+
resourceKind: input.resourceKind,
|
|
1602
|
+
resourceId: input.resourceId,
|
|
1603
|
+
targetId: input.targetId,
|
|
1604
|
+
expectedControllerGeneration: input.expectedControllerGeneration,
|
|
1605
|
+
expectedTargetGeneration: input.expectedTargetGeneration,
|
|
1606
|
+
expectedDocumentGeneration: input.expectedDocumentGeneration,
|
|
1607
|
+
kind: input.kind,
|
|
1608
|
+
reason: input.reason,
|
|
1609
|
+
...(input.authRunId !== undefined ? { authRunId: input.authRunId } : {}),
|
|
1610
|
+
expiresInSeconds: input.expiresInSeconds,
|
|
1611
|
+
});
|
|
1612
|
+
const { operationId: _operationId, ...digestRequest } = request;
|
|
1613
|
+
const digest = operationDigest({
|
|
1614
|
+
version: 1,
|
|
1615
|
+
request: digestRequest,
|
|
1616
|
+
originatingSessionId: input.originatingSessionId,
|
|
1617
|
+
originatingTurnId: input.originatingTurnId ?? null,
|
|
1618
|
+
originatingAttemptId: input.originatingAttemptId ?? null,
|
|
1619
|
+
originatingToolOperationId: input.originatingToolOperationId ?? null,
|
|
1620
|
+
actorSubjectId: input.actorSubjectId,
|
|
1621
|
+
});
|
|
1622
|
+
try {
|
|
1623
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
1624
|
+
await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
|
|
1625
|
+
await lockOperation(scopedDb, request.operationId);
|
|
1626
|
+
const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
|
|
1627
|
+
if (existing) {
|
|
1628
|
+
assertOperation(existing, {
|
|
1629
|
+
resourceKind: "intervention",
|
|
1630
|
+
kind: "create",
|
|
1631
|
+
requestDigest: digest,
|
|
1632
|
+
actorSubjectId: input.actorSubjectId,
|
|
1633
|
+
});
|
|
1634
|
+
const response = InteractionInterventionMutationResponse.parse(existing.result);
|
|
1635
|
+
return { ...response, replayed: true };
|
|
1636
|
+
}
|
|
1637
|
+
await expireInterventionsInScope(scopedDb, input);
|
|
1638
|
+
await assertActiveInteractionResource(scopedDb, {
|
|
1639
|
+
workspaceId: input.workspaceId,
|
|
1640
|
+
resourceKind: request.resourceKind,
|
|
1641
|
+
resourceId: request.resourceId,
|
|
1642
|
+
expectedControllerGeneration: request.expectedControllerGeneration,
|
|
1643
|
+
});
|
|
1644
|
+
let authRun: AuthRunRow | null = null;
|
|
1645
|
+
if (request.authRunId) {
|
|
1646
|
+
if (request.resourceKind !== "browser_session") {
|
|
1647
|
+
throw new InteractionResourceStateError(
|
|
1648
|
+
"Only browser interventions can belong to an auth run",
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
authRun = await loadAuthRunRow(scopedDb, input.workspaceId, request.authRunId);
|
|
1652
|
+
if (!authRun) throw new InteractionResourceNotFoundError("Auth run not found");
|
|
1653
|
+
if (
|
|
1654
|
+
authRun.browserSessionId !== request.resourceId ||
|
|
1655
|
+
authRun.targetId !== request.targetId ||
|
|
1656
|
+
authRun.controllerGeneration !== request.expectedControllerGeneration ||
|
|
1657
|
+
authRun.targetGeneration !== request.expectedTargetGeneration ||
|
|
1658
|
+
authRun.documentGeneration !== request.expectedDocumentGeneration
|
|
1659
|
+
) {
|
|
1660
|
+
throw new InteractionResourceConflictError(
|
|
1661
|
+
"Intervention does not match the exact auth-run target",
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
if (authRun.settledAt) {
|
|
1665
|
+
throw new InteractionResourceStateError("Auth run is already settled");
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
const id = randomUUID();
|
|
1669
|
+
const [row] = await scopedDb
|
|
1670
|
+
.insert(schema.interactionInterventions)
|
|
1671
|
+
.values({
|
|
1672
|
+
id,
|
|
1673
|
+
accountId: input.accountId,
|
|
1674
|
+
workspaceId: input.workspaceId,
|
|
1675
|
+
resourceKind: request.resourceKind,
|
|
1676
|
+
resourceId: request.resourceId,
|
|
1677
|
+
targetId: request.targetId,
|
|
1678
|
+
controllerGeneration: request.expectedControllerGeneration,
|
|
1679
|
+
targetGeneration: request.expectedTargetGeneration,
|
|
1680
|
+
documentGeneration: request.expectedDocumentGeneration,
|
|
1681
|
+
kind: request.kind,
|
|
1682
|
+
reason: request.reason,
|
|
1683
|
+
authRunId: request.authRunId ?? null,
|
|
1684
|
+
originatingSessionId: input.originatingSessionId,
|
|
1685
|
+
originatingTurnId: input.originatingTurnId ?? null,
|
|
1686
|
+
originatingAttemptId: input.originatingAttemptId ?? null,
|
|
1687
|
+
originatingToolOperationId: input.originatingToolOperationId ?? null,
|
|
1688
|
+
operationId: request.operationId,
|
|
1689
|
+
expiresAt: sql`now() + (${request.expiresInSeconds} * interval '1 second')`,
|
|
1690
|
+
})
|
|
1691
|
+
.returning();
|
|
1692
|
+
if (!row) throw new Error("Interaction intervention insert returned no row");
|
|
1693
|
+
if (authRun) {
|
|
1694
|
+
const linked = await scopedDb
|
|
1695
|
+
.update(schema.authRuns)
|
|
1696
|
+
.set({
|
|
1697
|
+
interventionId: id,
|
|
1698
|
+
version: authRun.version + 1,
|
|
1699
|
+
updatedAt: sql`now()`,
|
|
1700
|
+
})
|
|
1701
|
+
.where(
|
|
1702
|
+
and(
|
|
1703
|
+
eq(schema.authRuns.workspaceId, input.workspaceId),
|
|
1704
|
+
eq(schema.authRuns.id, authRun.id),
|
|
1705
|
+
eq(schema.authRuns.version, authRun.version),
|
|
1706
|
+
sql`${schema.authRuns.settledAt} is null`,
|
|
1707
|
+
),
|
|
1708
|
+
)
|
|
1709
|
+
.returning({ id: schema.authRuns.id });
|
|
1710
|
+
if (linked.length !== 1) {
|
|
1711
|
+
throw new InteractionResourceConflictError("Auth run changed before intervention link");
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
const response = InteractionInterventionMutationResponse.parse({
|
|
1715
|
+
intervention: interventionFromRow(row),
|
|
1716
|
+
operationId: request.operationId,
|
|
1717
|
+
replayed: false,
|
|
1718
|
+
});
|
|
1719
|
+
await insertCompletedOperation(scopedDb, {
|
|
1720
|
+
operationId: request.operationId,
|
|
1721
|
+
accountId: input.accountId,
|
|
1722
|
+
workspaceId: input.workspaceId,
|
|
1723
|
+
resourceKind: "intervention",
|
|
1724
|
+
resourceId: id,
|
|
1725
|
+
kind: "create",
|
|
1726
|
+
requestDigest: digest,
|
|
1727
|
+
resultVersion: row.version,
|
|
1728
|
+
result: response,
|
|
1729
|
+
actorSubjectId: input.actorSubjectId,
|
|
1730
|
+
});
|
|
1731
|
+
await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
|
|
1732
|
+
return response;
|
|
1733
|
+
});
|
|
1734
|
+
} catch (error) {
|
|
1735
|
+
if (
|
|
1736
|
+
postgresConstraint(error) === "interaction_interventions_open_target_kind_uq" ||
|
|
1737
|
+
postgresConstraint(error) === "interaction_interventions_open_auth_run_uq"
|
|
1738
|
+
) {
|
|
1739
|
+
throw new InteractionResourceConflictError(
|
|
1740
|
+
"A matching interaction intervention is already open",
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
throw error;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
export async function resolveInteractionIntervention(
|
|
1748
|
+
db: Database,
|
|
1749
|
+
input: InteractionMutationScope & {
|
|
1750
|
+
interventionId: string;
|
|
1751
|
+
} & ResolveInteractionInterventionRequestValue,
|
|
1752
|
+
): Promise<InteractionInterventionMutationResponseValue> {
|
|
1753
|
+
const request = ResolveInteractionInterventionRequest.parse({
|
|
1754
|
+
operationId: input.operationId,
|
|
1755
|
+
expectedVersion: input.expectedVersion,
|
|
1756
|
+
outcome: input.outcome,
|
|
1757
|
+
});
|
|
1758
|
+
const { operationId: _operationId, ...digestRequest } = request;
|
|
1759
|
+
const digest = operationDigest({
|
|
1760
|
+
version: 1,
|
|
1761
|
+
interventionId: input.interventionId,
|
|
1762
|
+
request: digestRequest,
|
|
1763
|
+
actorSubjectId: input.actorSubjectId,
|
|
1764
|
+
});
|
|
1765
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
1766
|
+
await assertWorkspaceAccount(scopedDb, input.accountId, input.workspaceId);
|
|
1767
|
+
await lockOperation(scopedDb, request.operationId);
|
|
1768
|
+
const existing = await loadOperation(scopedDb, input.workspaceId, request.operationId);
|
|
1769
|
+
if (existing) {
|
|
1770
|
+
assertOperation(existing, {
|
|
1771
|
+
resourceKind: "intervention",
|
|
1772
|
+
resourceId: input.interventionId,
|
|
1773
|
+
kind: "resolve",
|
|
1774
|
+
requestDigest: digest,
|
|
1775
|
+
actorSubjectId: input.actorSubjectId,
|
|
1776
|
+
});
|
|
1777
|
+
const response = InteractionInterventionMutationResponse.parse(existing.result);
|
|
1778
|
+
return { ...response, replayed: true };
|
|
1779
|
+
}
|
|
1780
|
+
const [current] = await scopedDb
|
|
1781
|
+
.select()
|
|
1782
|
+
.from(schema.interactionInterventions)
|
|
1783
|
+
.where(
|
|
1784
|
+
and(
|
|
1785
|
+
eq(schema.interactionInterventions.workspaceId, input.workspaceId),
|
|
1786
|
+
eq(schema.interactionInterventions.id, input.interventionId),
|
|
1787
|
+
),
|
|
1788
|
+
)
|
|
1789
|
+
.for("update")
|
|
1790
|
+
.limit(1);
|
|
1791
|
+
if (!current) {
|
|
1792
|
+
throw new InteractionResourceNotFoundError("Interaction intervention not found");
|
|
1793
|
+
}
|
|
1794
|
+
if (current.version !== request.expectedVersion) {
|
|
1795
|
+
throw new InteractionResourceConflictError(
|
|
1796
|
+
"Interaction intervention changed before this response",
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
if (current.status !== "open") {
|
|
1800
|
+
throw new InteractionResourceStateError("Interaction intervention is already settled");
|
|
1801
|
+
}
|
|
1802
|
+
const expired = current.expiresAt.getTime() <= Date.now();
|
|
1803
|
+
const status = expired ? "expired" : request.outcome;
|
|
1804
|
+
const [row] = await scopedDb
|
|
1805
|
+
.update(schema.interactionInterventions)
|
|
1806
|
+
.set({
|
|
1807
|
+
status,
|
|
1808
|
+
responseActorSubjectId: expired ? null : input.actorSubjectId,
|
|
1809
|
+
version: current.version + 1,
|
|
1810
|
+
updatedAt: sql`now()`,
|
|
1811
|
+
settledAt: sql`now()`,
|
|
1812
|
+
})
|
|
1813
|
+
.where(
|
|
1814
|
+
and(
|
|
1815
|
+
eq(schema.interactionInterventions.workspaceId, input.workspaceId),
|
|
1816
|
+
eq(schema.interactionInterventions.id, input.interventionId),
|
|
1817
|
+
eq(schema.interactionInterventions.version, request.expectedVersion),
|
|
1818
|
+
eq(schema.interactionInterventions.status, "open"),
|
|
1819
|
+
),
|
|
1820
|
+
)
|
|
1821
|
+
.returning();
|
|
1822
|
+
if (!row) {
|
|
1823
|
+
throw new InteractionResourceConflictError("Intervention response lost its fence");
|
|
1824
|
+
}
|
|
1825
|
+
await settleLinkedAuthRun(scopedDb, {
|
|
1826
|
+
workspaceId: input.workspaceId,
|
|
1827
|
+
authRunId: row.authRunId,
|
|
1828
|
+
interventionId: row.id,
|
|
1829
|
+
outcome: status,
|
|
1830
|
+
});
|
|
1831
|
+
const response = InteractionInterventionMutationResponse.parse({
|
|
1832
|
+
intervention: interventionFromRow(row),
|
|
1833
|
+
operationId: request.operationId,
|
|
1834
|
+
replayed: false,
|
|
1835
|
+
});
|
|
1836
|
+
await insertCompletedOperation(scopedDb, {
|
|
1837
|
+
operationId: request.operationId,
|
|
1838
|
+
accountId: input.accountId,
|
|
1839
|
+
workspaceId: input.workspaceId,
|
|
1840
|
+
resourceKind: "intervention",
|
|
1841
|
+
resourceId: input.interventionId,
|
|
1842
|
+
kind: "resolve",
|
|
1843
|
+
requestDigest: digest,
|
|
1844
|
+
resultVersion: row.version,
|
|
1845
|
+
result: response,
|
|
1846
|
+
actorSubjectId: input.actorSubjectId,
|
|
1847
|
+
});
|
|
1848
|
+
await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
|
|
1849
|
+
return response;
|
|
1850
|
+
});
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
export async function cancelOpenInteractionInterventions(
|
|
1854
|
+
db: Database,
|
|
1855
|
+
input: {
|
|
1856
|
+
accountId: string;
|
|
1857
|
+
workspaceId: string;
|
|
1858
|
+
resourceKind: "browser_session" | "computer_session";
|
|
1859
|
+
resourceId: string;
|
|
1860
|
+
},
|
|
1861
|
+
): Promise<number> {
|
|
1862
|
+
return await withRlsContext(db, input, async (scopedDb) => {
|
|
1863
|
+
const cancelled = await scopedDb
|
|
1864
|
+
.update(schema.interactionInterventions)
|
|
1865
|
+
.set({
|
|
1866
|
+
status: "cancelled",
|
|
1867
|
+
version: sql`${schema.interactionInterventions.version} + 1`,
|
|
1868
|
+
updatedAt: sql`now()`,
|
|
1869
|
+
settledAt: sql`now()`,
|
|
1870
|
+
})
|
|
1871
|
+
.where(
|
|
1872
|
+
and(
|
|
1873
|
+
eq(schema.interactionInterventions.workspaceId, input.workspaceId),
|
|
1874
|
+
eq(schema.interactionInterventions.resourceKind, input.resourceKind),
|
|
1875
|
+
eq(schema.interactionInterventions.resourceId, input.resourceId),
|
|
1876
|
+
eq(schema.interactionInterventions.status, "open"),
|
|
1877
|
+
),
|
|
1878
|
+
)
|
|
1879
|
+
.returning({
|
|
1880
|
+
id: schema.interactionInterventions.id,
|
|
1881
|
+
authRunId: schema.interactionInterventions.authRunId,
|
|
1882
|
+
});
|
|
1883
|
+
for (const intervention of cancelled) {
|
|
1884
|
+
await settleLinkedAuthRun(scopedDb, {
|
|
1885
|
+
workspaceId: input.workspaceId,
|
|
1886
|
+
authRunId: intervention.authRunId,
|
|
1887
|
+
interventionId: intervention.id,
|
|
1888
|
+
outcome: "cancelled",
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
if (cancelled.length > 0) {
|
|
1892
|
+
await advanceWorkspaceInteractionRevision(scopedDb, input.accountId, input.workspaceId);
|
|
1893
|
+
}
|
|
1894
|
+
return cancelled.length;
|
|
1895
|
+
});
|
|
1896
|
+
}
|