@opengeni/core 0.28.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/access/external-actor-authority.d.ts +47 -0
- package/dist/access/index.d.ts +182 -3
- package/dist/access/service-initiator.d.ts +3 -0
- package/dist/application/api-integration-servers.d.ts +20 -0
- package/dist/application/archived-session-imports.d.ts +6 -0
- package/dist/application/composer-submit.d.ts +24 -0
- package/dist/application/connect-authority.d.ts +13 -0
- package/dist/application/connect-operation.d.ts +31 -0
- package/dist/application/external-continuation.d.ts +15 -0
- package/dist/application/external-identity-lifecycle.d.ts +20 -0
- package/dist/application/external-link-work-admission.d.ts +12 -0
- package/dist/application/external-workspace-members.d.ts +34 -0
- package/dist/application/modal-native-worker-host-transport.d.ts +297 -0
- package/dist/application/new-session-drafts.d.ts +39 -2
- package/dist/application/sandbox-recovery.d.ts +58 -0
- package/dist/application/session-commands.d.ts +26 -13
- package/dist/application/session-mcp-credential-rotation.d.ts +29 -0
- package/dist/application/session-tenancy.d.ts +19 -0
- package/dist/application/user-resource-grants.d.ts +20 -0
- package/dist/billing/limits.d.ts +11 -0
- package/dist/canonical-human-identities.d.ts +7 -2
- package/dist/canonical-human-identities.js +17 -4
- package/dist/canonical-human-identities.js.map +1 -1
- package/dist/{chunk-6WKPWE5S.js → chunk-2MKMRGSY.js} +108 -24
- package/dist/chunk-2MKMRGSY.js.map +1 -0
- package/dist/{chunk-NYPMQ7NO.js → chunk-GVO3N37G.js} +205 -37
- package/dist/chunk-GVO3N37G.js.map +1 -0
- package/dist/{chunk-JJU6I5XD.js → chunk-LCQXNR62.js} +36 -10
- package/dist/chunk-LCQXNR62.js.map +1 -0
- package/dist/chunk-MM6N7CTO.js +387 -0
- package/dist/chunk-MM6N7CTO.js.map +1 -0
- package/dist/chunk-QO5GVFFO.js +235 -0
- package/dist/chunk-QO5GVFFO.js.map +1 -0
- package/dist/codex-model-availability.d.ts +14 -0
- package/dist/default-session-model.d.ts +139 -0
- package/dist/dependencies.d.ts +87 -4
- package/dist/developer-setup-credential-ceiling.d.ts +10 -0
- package/dist/domain/agent-config-resolution.d.ts +49 -0
- package/dist/domain/automations.d.ts +58 -0
- package/dist/domain/capabilities.d.ts +22 -4
- package/dist/domain/company-profile-agent-admin.d.ts +25 -0
- package/dist/domain/connection-account-selection-error.d.ts +8 -0
- package/dist/domain/connector-tool-permissions.d.ts +36 -0
- package/dist/domain/direct-model-provider.d.ts +3 -0
- package/dist/domain/editable-artifacts/agent-application.d.ts +10 -1
- package/dist/domain/editable-artifacts/durable-export.d.ts +22 -1
- package/dist/domain/editable-artifacts/ports.d.ts +10 -0
- package/dist/domain/editable-artifacts/service.d.ts +4 -3
- package/dist/domain/environments.d.ts +5 -9
- package/dist/domain/external-creation-attribution.d.ts +6 -0
- package/dist/domain/file-owner.d.ts +8 -0
- package/dist/domain/github-action-policies.d.ts +36 -0
- package/dist/domain/github-repository-bindings.d.ts +117 -0
- package/dist/domain/github-skill-source.d.ts +5 -0
- package/dist/domain/governed-learning-slack-publication.d.ts +45 -0
- package/dist/domain/insights-timing.d.ts +16 -0
- package/dist/domain/insights-usage.d.ts +27 -0
- package/dist/domain/insights.d.ts +28 -1
- package/dist/domain/knowledge-files.d.ts +20 -0
- package/dist/domain/knowledge-messages.d.ts +39 -0
- package/dist/domain/knowledge-messages.test.d.ts +1 -0
- package/dist/domain/knowledge-preparation.d.ts +10 -0
- package/dist/domain/knowledge-preparation.test.d.ts +1 -0
- package/dist/domain/knowledge-search.d.ts +15 -0
- package/dist/domain/knowledge-search.test.d.ts +1 -0
- package/dist/domain/knowledge.d.ts +9 -0
- package/dist/domain/mcp-account-bindings.d.ts +19 -0
- package/dist/domain/mcp-account-routes.d.ts +21 -0
- package/dist/domain/native-mcp-connection-admission.d.ts +3 -0
- package/dist/domain/organization-integration-catalog.d.ts +15 -0
- package/dist/domain/organization-membership-lifecycle.d.ts +1 -0
- package/dist/domain/personal-connection-delegations.d.ts +87 -4
- package/dist/domain/pr-review.d.ts +249 -0
- package/dist/domain/resources.d.ts +24 -3
- package/dist/domain/scheduled-task-access.d.ts +247 -0
- package/dist/domain/scheduled-tasks.d.ts +103 -6
- package/dist/domain/session-connection-accounts.d.ts +16 -0
- package/dist/domain/session-file-access.d.ts +4 -0
- package/dist/domain/session-native-connection-replacement.d.ts +14 -0
- package/dist/domain/session-start-timing.d.ts +7 -0
- package/dist/domain/session-tool-policy.d.ts +93 -4
- package/dist/domain/sessions.d.ts +275 -28
- package/dist/domain/skill-imports.d.ts +1 -0
- package/dist/domain/skill-search.d.ts +30 -0
- package/dist/domain/skills.d.ts +117 -0
- package/dist/domain/slack-bot.d.ts +46 -1
- package/dist/domain/workspace-members.d.ts +8 -1
- package/dist/editable-artifact-live/application.d.ts +12 -3
- package/dist/editable-artifact-live/ports.d.ts +0 -1
- package/dist/editable-artifact-live/server.d.ts +5 -2
- package/dist/editable-artifact-live/ticket.d.ts +2 -2
- package/dist/editable-artifact-live/types.d.ts +14 -2
- package/dist/editable-artifact-live.js +2 -2
- package/dist/editable-artifacts.js +10 -4
- package/dist/index.d.ts +49 -3
- package/dist/index.js +18812 -6833
- package/dist/index.js.map +1 -1
- package/dist/managed-auth-session-sets.d.ts +131 -0
- package/dist/managed-auth-session-sets.js +51 -0
- package/dist/managed-auth-session-sets.js.map +1 -0
- package/dist/managed-session.d.ts +98 -11
- package/dist/model-catalog.d.ts +117 -0
- package/dist/product-usage-metrics.d.ts +30 -0
- package/dist/rigs/index.d.ts +22 -5
- package/dist/rigs/provider-images.d.ts +1 -1
- package/dist/sandbox/fleet.d.ts +59 -10
- package/dist/sandbox/routing.d.ts +22 -2
- package/dist/sandbox/runtime-settings.d.ts +56 -0
- package/dist/session-authorization.d.ts +38 -1
- package/dist/site-session-origin.d.ts +9 -0
- package/dist/transcription.d.ts +12 -1
- package/dist/turn-surface.d.ts +18 -0
- package/dist/user-presence.d.ts +32 -0
- package/package.json +18 -11
- package/src/access/external-actor-authority.ts +94 -0
- package/src/access/index.ts +1386 -44
- package/src/access/service-initiator.ts +50 -0
- package/src/application/api-integration-servers.ts +215 -0
- package/src/application/archived-session-imports.ts +161 -0
- package/src/application/composer-submit.ts +77 -0
- package/src/application/connect-authority.ts +77 -0
- package/src/application/connect-operation.ts +80 -0
- package/src/application/external-continuation.ts +129 -0
- package/src/application/external-identity-lifecycle.ts +48 -0
- package/src/application/external-link-work-admission.ts +90 -0
- package/src/application/external-workspace-members.ts +232 -0
- package/src/application/modal-native-worker-host-transport.ts +263 -0
- package/src/application/new-session-drafts.ts +222 -29
- package/src/application/sandbox-recovery.ts +78 -0
- package/src/application/session-commands.ts +430 -73
- package/src/application/session-mcp-credential-rotation.ts +217 -0
- package/src/application/session-tenancy.ts +445 -0
- package/src/application/user-resource-grants.ts +154 -0
- package/src/billing/limits.ts +103 -24
- package/src/canonical-human-identities.ts +22 -3
- package/src/codex-model-availability.ts +136 -0
- package/src/default-session-model.ts +656 -0
- package/src/dependencies.ts +97 -5
- package/src/developer-setup-credential-ceiling.ts +60 -0
- package/src/domain/agent-config-resolution.ts +148 -0
- package/src/domain/automations.ts +270 -0
- package/src/domain/capabilities.ts +244 -151
- package/src/domain/company-profile-agent-admin.ts +56 -0
- package/src/domain/connection-account-selection-error.ts +19 -0
- package/src/domain/connector-tool-permissions.ts +371 -0
- package/src/domain/direct-model-provider.ts +62 -0
- package/src/domain/editable-artifacts/agent-application.ts +61 -15
- package/src/domain/editable-artifacts/durable-export.ts +100 -5
- package/src/domain/editable-artifacts/in-memory.ts +6 -0
- package/src/domain/editable-artifacts/ports.ts +11 -0
- package/src/domain/editable-artifacts/service.ts +32 -1
- package/src/domain/editable-artifacts/snapshot-compaction.ts +9 -5
- package/src/domain/editable-artifacts/snapshot-verifier-genesis.ts +10 -8
- package/src/domain/editable-artifacts/snapshot-verifier.ts +10 -3
- package/src/domain/environments.ts +25 -45
- package/src/domain/external-creation-attribution.ts +22 -0
- package/src/domain/file-owner.ts +67 -0
- package/src/domain/github-action-policies.ts +169 -0
- package/src/domain/github-repository-bindings.ts +295 -0
- package/src/domain/github-skill-source.ts +233 -0
- package/src/domain/governed-learning-slack-publication.ts +157 -0
- package/src/domain/insights-timing.ts +95 -0
- package/src/domain/insights-usage.ts +67 -0
- package/src/domain/insights.ts +270 -169
- package/src/domain/knowledge-files.ts +46 -0
- package/src/domain/knowledge-messages.test.ts +91 -0
- package/src/domain/knowledge-messages.ts +106 -0
- package/src/domain/knowledge-preparation.test.ts +276 -0
- package/src/domain/knowledge-preparation.ts +147 -0
- package/src/domain/knowledge-search.test.ts +561 -0
- package/src/domain/knowledge-search.ts +256 -0
- package/src/domain/knowledge.ts +133 -0
- package/src/domain/mcp-account-bindings.ts +171 -0
- package/src/domain/mcp-account-routes.ts +121 -0
- package/src/domain/native-mcp-connection-admission.ts +14 -0
- package/src/domain/organization-integration-catalog.ts +88 -0
- package/src/domain/organization-membership-lifecycle.ts +10 -0
- package/src/domain/personal-connection-delegations.ts +758 -115
- package/src/domain/pr-review.ts +687 -0
- package/src/domain/resources.ts +182 -25
- package/src/domain/scheduled-task-access.ts +1052 -0
- package/src/domain/scheduled-tasks.ts +1952 -215
- package/src/domain/session-connection-accounts.ts +67 -0
- package/src/domain/session-file-access.ts +47 -0
- package/src/domain/session-native-connection-replacement.ts +64 -0
- package/src/domain/session-start-timing.ts +55 -0
- package/src/domain/session-tool-policy.ts +443 -19
- package/src/domain/sessions.ts +3417 -619
- package/src/domain/skill-imports.ts +137 -17
- package/src/domain/skill-search.ts +221 -0
- package/src/domain/skills.ts +104 -0
- package/src/domain/slack-bot.ts +128 -3
- package/src/domain/workspace-members.ts +34 -2
- package/src/editable-artifact-live/application.ts +29 -4
- package/src/editable-artifact-live/ports.ts +0 -1
- package/src/editable-artifact-live/server.ts +167 -13
- package/src/editable-artifact-live/ticket.ts +87 -6
- package/src/editable-artifact-live/types.ts +16 -2
- package/src/editable-artifact-live/wire.ts +0 -10
- package/src/index.ts +69 -4
- package/src/managed-auth-session-sets.ts +371 -0
- package/src/managed-session.ts +520 -2
- package/src/model-catalog.ts +873 -0
- package/src/product-usage-metrics.ts +101 -0
- package/src/rigs/index.ts +179 -63
- package/src/rigs/provider-images.ts +2 -3
- package/src/sandbox/fleet.ts +471 -59
- package/src/sandbox/routing.ts +160 -33
- package/src/sandbox/runtime-settings.ts +281 -0
- package/src/session-authorization.ts +262 -78
- package/src/site-session-origin.ts +22 -0
- package/src/transcription.ts +13 -1
- package/src/turn-surface.ts +45 -0
- package/src/user-presence.ts +134 -0
- package/dist/chunk-6WKPWE5S.js.map +0 -1
- package/dist/chunk-IBOEYG6N.js +0 -34
- package/dist/chunk-IBOEYG6N.js.map +0 -1
- package/dist/chunk-JJU6I5XD.js.map +0 -1
- package/dist/chunk-NYPMQ7NO.js.map +0 -1
- package/dist/domain/durable-learning-slack-publication.d.ts +0 -72
- package/dist/domain/packs.d.ts +0 -55
- package/src/domain/durable-learning-slack-publication.ts +0 -202
- package/src/domain/packs.ts +0 -556
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { RotateSessionMcpCredentialsRequest, type AccessGrant } from "@opengeni/contracts";
|
|
3
|
+
import {
|
|
4
|
+
encryptVariableSetValue,
|
|
5
|
+
getWorkspaceGrant,
|
|
6
|
+
subjectHasLiveWorkspaceAuthorityInScope,
|
|
7
|
+
type Database,
|
|
8
|
+
} from "@opengeni/db";
|
|
9
|
+
import {
|
|
10
|
+
rotateSessionMcpCredentialsAtomically,
|
|
11
|
+
SessionMcpCredentialRotationError,
|
|
12
|
+
} from "@opengeni/db/session-mcp-credential-rotation";
|
|
13
|
+
import { HTTPException } from "hono/http-exception";
|
|
14
|
+
import type { Settings } from "@opengeni/config";
|
|
15
|
+
import {
|
|
16
|
+
requirePermission,
|
|
17
|
+
hasVerifiedOwningUserAuthorization,
|
|
18
|
+
requireResolvedAccessGrantAuthorization,
|
|
19
|
+
type AccessGrantAuthorization,
|
|
20
|
+
} from "../access";
|
|
21
|
+
import {
|
|
22
|
+
grantHasAgentAttemptAuthority,
|
|
23
|
+
requireSessionAuthorization,
|
|
24
|
+
SessionAuthorizationDeniedError,
|
|
25
|
+
SessionAuthorizationUnavailableError,
|
|
26
|
+
type SessionAuthorizationDependencies,
|
|
27
|
+
} from "../session-authorization";
|
|
28
|
+
import { requireVariableSetEncryption } from "../domain/environments";
|
|
29
|
+
import { normalizedSessionMcpCredentialHeaders } from "../domain/sessions";
|
|
30
|
+
import { externalContinuationCommitAuthorizer } from "./external-continuation";
|
|
31
|
+
import {
|
|
32
|
+
listOwnConnectionAccountsForGrant,
|
|
33
|
+
personalConnectionDelegationSourceForGrant,
|
|
34
|
+
} from "../domain/personal-connection-delegations";
|
|
35
|
+
import { nativeSessionConnectionReplacement } from "../domain/session-native-connection-replacement";
|
|
36
|
+
|
|
37
|
+
/** HMAC domains separate request identity from key availability. The key tag
|
|
38
|
+
* carries no request material and neither fingerprint is returned publicly. */
|
|
39
|
+
export function sessionMcpRotationFingerprint(
|
|
40
|
+
key: Uint8Array,
|
|
41
|
+
scope: { accountId: string; workspaceId: string; sessionId: string; subjectId: string },
|
|
42
|
+
request: RotateSessionMcpCredentialsRequest,
|
|
43
|
+
) {
|
|
44
|
+
return {
|
|
45
|
+
digestKeyTag: createHmac("sha256", key)
|
|
46
|
+
.update("opengeni:session-mcp-rotation:key:v1")
|
|
47
|
+
.digest("hex"),
|
|
48
|
+
requestDigest: createHmac("sha256", key)
|
|
49
|
+
.update("opengeni:session-mcp-rotation:request:v1\0")
|
|
50
|
+
.update(JSON.stringify({ ...scope, request }))
|
|
51
|
+
.digest("hex"),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function requireRotationGrant(grant: AccessGrant) {
|
|
56
|
+
if (grantHasAgentAttemptAuthority(grant))
|
|
57
|
+
throw new HTTPException(403, { message: "host authorization required" });
|
|
58
|
+
requirePermission(grant, "sessions:control");
|
|
59
|
+
requirePermission(grant, "mcp_servers:attach");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** HTTP/SDK host boundary only. The mandatory callback re-resolves request
|
|
63
|
+
* authentication with the transaction handle, not a cached route-time grant. */
|
|
64
|
+
export async function rotateSessionMcpCredentialsForRequest(
|
|
65
|
+
deps: SessionAuthorizationDependencies & { settings: Settings },
|
|
66
|
+
authorization: AccessGrantAuthorization,
|
|
67
|
+
sessionId: string,
|
|
68
|
+
raw: unknown,
|
|
69
|
+
reauthorize: (tx: Database) => Promise<AccessGrant>,
|
|
70
|
+
) {
|
|
71
|
+
const grant = requireResolvedAccessGrantAuthorization(
|
|
72
|
+
authorization,
|
|
73
|
+
authorization.grant.workspaceId,
|
|
74
|
+
);
|
|
75
|
+
requireRotationGrant(grant);
|
|
76
|
+
const parsed = RotateSessionMcpCredentialsRequest.safeParse(raw);
|
|
77
|
+
if (!parsed.success)
|
|
78
|
+
throw new HTTPException(422, { message: "invalid credential rotation request" });
|
|
79
|
+
const encryptionKey = requireVariableSetEncryption(deps.settings);
|
|
80
|
+
let request: RotateSessionMcpCredentialsRequest;
|
|
81
|
+
try {
|
|
82
|
+
request = {
|
|
83
|
+
operationKey: parsed.data.operationKey,
|
|
84
|
+
updates: parsed.data.updates
|
|
85
|
+
.map((update) => ({
|
|
86
|
+
id: update.id,
|
|
87
|
+
expectedCredentialVersion: update.expectedCredentialVersion,
|
|
88
|
+
expectedServerUrl: update.expectedServerUrl,
|
|
89
|
+
...("nativeConnectionId" in update
|
|
90
|
+
? {
|
|
91
|
+
nativeConnectionId: update.nativeConnectionId,
|
|
92
|
+
...(update.replacementServerUrl
|
|
93
|
+
? { replacementServerUrl: update.replacementServerUrl }
|
|
94
|
+
: {}),
|
|
95
|
+
}
|
|
96
|
+
: {
|
|
97
|
+
headers: Object.fromEntries(
|
|
98
|
+
Object.entries(normalizedSessionMcpCredentialHeaders(update.headers))
|
|
99
|
+
.map(([name, value]) => [name.toLowerCase(), value] as const)
|
|
100
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),
|
|
101
|
+
),
|
|
102
|
+
}),
|
|
103
|
+
}))
|
|
104
|
+
.sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0)),
|
|
105
|
+
};
|
|
106
|
+
if (new Set(request.updates.map((update) => update.id)).size !== request.updates.length)
|
|
107
|
+
throw new Error();
|
|
108
|
+
} catch {
|
|
109
|
+
// Header names and validation paths are caller-controlled secret channels.
|
|
110
|
+
throw new HTTPException(422, { message: "invalid credential rotation headers or server IDs" });
|
|
111
|
+
}
|
|
112
|
+
const scope = {
|
|
113
|
+
accountId: grant.accountId,
|
|
114
|
+
workspaceId: grant.workspaceId,
|
|
115
|
+
sessionId,
|
|
116
|
+
subjectId: grant.subjectId,
|
|
117
|
+
actorType: grant.principalKind === "service" ? ("service" as const) : ("human" as const),
|
|
118
|
+
};
|
|
119
|
+
const externalAuthorize = externalContinuationCommitAuthorizer(authorization);
|
|
120
|
+
let authorizedGrant = grant;
|
|
121
|
+
try {
|
|
122
|
+
return await rotateSessionMcpCredentialsAtomically(deps.db, {
|
|
123
|
+
...scope,
|
|
124
|
+
operationKey: request.operationKey,
|
|
125
|
+
...sessionMcpRotationFingerprint(encryptionKey, scope, request),
|
|
126
|
+
updates: request.updates.map((update) =>
|
|
127
|
+
"nativeConnectionId" in update
|
|
128
|
+
? update
|
|
129
|
+
: {
|
|
130
|
+
id: update.id,
|
|
131
|
+
expectedCredentialVersion: update.expectedCredentialVersion,
|
|
132
|
+
expectedServerUrl: update.expectedServerUrl,
|
|
133
|
+
headersEncrypted: Object.fromEntries(
|
|
134
|
+
Object.entries(update.headers).map(([name, value]) => [
|
|
135
|
+
name,
|
|
136
|
+
encryptVariableSetValue(encryptionKey, value),
|
|
137
|
+
]),
|
|
138
|
+
),
|
|
139
|
+
},
|
|
140
|
+
),
|
|
141
|
+
resolveNativeConnection: async (tx, server, nativeConnectionId, replacementServerUrl) => {
|
|
142
|
+
const connections = await listOwnConnectionAccountsForGrant(tx, authorizedGrant);
|
|
143
|
+
const source = personalConnectionDelegationSourceForGrant(authorizedGrant);
|
|
144
|
+
try {
|
|
145
|
+
return nativeSessionConnectionReplacement({
|
|
146
|
+
accountId: grant.accountId,
|
|
147
|
+
workspaceId: grant.workspaceId,
|
|
148
|
+
subjectId: source.kind === "subject" ? source.subjectId : null,
|
|
149
|
+
serverUrl: server.url,
|
|
150
|
+
...(replacementServerUrl ? { replacementServerUrl } : {}),
|
|
151
|
+
currentRef: server.connectionRef,
|
|
152
|
+
nativeConnectionId,
|
|
153
|
+
connections,
|
|
154
|
+
});
|
|
155
|
+
} catch {
|
|
156
|
+
throw new SessionMcpCredentialRotationError("connection_unavailable");
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
authorize: async (tx) => {
|
|
160
|
+
await externalAuthorize?.(tx);
|
|
161
|
+
const fresh = await reauthorize(tx);
|
|
162
|
+
if (
|
|
163
|
+
fresh.accountId !== grant.accountId ||
|
|
164
|
+
fresh.workspaceId !== grant.workspaceId ||
|
|
165
|
+
fresh.subjectId !== grant.subjectId ||
|
|
166
|
+
fresh.principalKind !== grant.principalKind
|
|
167
|
+
)
|
|
168
|
+
throw new HTTPException(403, { message: "credential rotation authority changed" });
|
|
169
|
+
requireRotationGrant(fresh);
|
|
170
|
+
if (fresh.principalKind === "human_session" && !externalAuthorize) {
|
|
171
|
+
// Signed host grants may narrow a named human's permissions, never
|
|
172
|
+
// outlive removal or expand that human's current workspace grant.
|
|
173
|
+
const live = await getWorkspaceGrant(tx, fresh.subjectId, fresh.workspaceId);
|
|
174
|
+
if (live) {
|
|
175
|
+
requireRotationGrant(live);
|
|
176
|
+
} else if (!hasVerifiedOwningUserAuthorization(authorization)) {
|
|
177
|
+
throw new HTTPException(403, { message: "credential rotation authority changed" });
|
|
178
|
+
}
|
|
179
|
+
if (!(await subjectHasLiveWorkspaceAuthorityInScope(tx, scope))) {
|
|
180
|
+
throw new HTTPException(403, { message: "credential rotation authority changed" });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
await requireSessionAuthorization({ ...deps, db: tx }, fresh, {
|
|
184
|
+
sessionId,
|
|
185
|
+
operation: "session.mcp.credentials.rotate",
|
|
186
|
+
surface: "http",
|
|
187
|
+
});
|
|
188
|
+
authorizedGrant = fresh;
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (error instanceof HTTPException)
|
|
193
|
+
throw new HTTPException(error.status, { message: "credential rotation access denied" });
|
|
194
|
+
if (error instanceof SessionAuthorizationDeniedError)
|
|
195
|
+
throw new HTTPException(403, { message: "credential rotation access denied" });
|
|
196
|
+
if (error instanceof SessionAuthorizationUnavailableError)
|
|
197
|
+
throw new HTTPException(503, { message: "credential rotation authorization unavailable" });
|
|
198
|
+
if (error instanceof SessionMcpCredentialRotationError) {
|
|
199
|
+
const status =
|
|
200
|
+
error.code === "receipt_key_unavailable"
|
|
201
|
+
? 503
|
|
202
|
+
: error.code === "authority_revoked"
|
|
203
|
+
? 403
|
|
204
|
+
: error.code === "not_found"
|
|
205
|
+
? 404
|
|
206
|
+
: error.code === "invalid_request" ||
|
|
207
|
+
error.code === "brokered_server" ||
|
|
208
|
+
error.code === "connection_unavailable"
|
|
209
|
+
? 422
|
|
210
|
+
: 409;
|
|
211
|
+
throw new HTTPException(status, { message: `credential_rotation_${error.code}` });
|
|
212
|
+
}
|
|
213
|
+
// Drizzle failures can include query parameters. Never attach their cause,
|
|
214
|
+
// ciphertext, fingerprints, header names, or raw input to API telemetry.
|
|
215
|
+
throw new HTTPException(503, { message: "credential rotation unavailable" });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ForkSessionResponse,
|
|
3
|
+
SessionTenancyCreateCapabilities,
|
|
4
|
+
UpdateSessionVisibilityResponse,
|
|
5
|
+
sessionVisibilityFromPublic,
|
|
6
|
+
sessionVisibilityToPublic,
|
|
7
|
+
type ForkSessionRequest,
|
|
8
|
+
type SessionAuthorizationSurface,
|
|
9
|
+
type UpdateSessionVisibilityRequest,
|
|
10
|
+
type VariableSet,
|
|
11
|
+
} from "@opengeni/contracts";
|
|
12
|
+
import {
|
|
13
|
+
assertSessionIsNotImported,
|
|
14
|
+
forkSessionContent,
|
|
15
|
+
getSessionForSubject,
|
|
16
|
+
getRig,
|
|
17
|
+
getPrivateSessionCreatePolicy,
|
|
18
|
+
getSessionEventForSubject,
|
|
19
|
+
isRetryableDatabaseTransportFailure,
|
|
20
|
+
nestedPostgresSqlState,
|
|
21
|
+
replayAppliedSessionFork,
|
|
22
|
+
sessionTenancyProductActivated,
|
|
23
|
+
SessionTenancyAccessError,
|
|
24
|
+
SessionTenancyInvalidRequestError,
|
|
25
|
+
SessionTenancyNotActivatedError,
|
|
26
|
+
transitionSessionVisibility,
|
|
27
|
+
type Database,
|
|
28
|
+
type ForkSessionContentResult,
|
|
29
|
+
} from "@opengeni/db";
|
|
30
|
+
import { publishDurableSessionEvents, type EventBus } from "@opengeni/events";
|
|
31
|
+
import {
|
|
32
|
+
requirePermission,
|
|
33
|
+
hasVerifiedOwningUserAuthorization,
|
|
34
|
+
type AccessGrantAuthorization,
|
|
35
|
+
} from "../access";
|
|
36
|
+
import { requireSessionAuthorization } from "../session-authorization";
|
|
37
|
+
import type { AppDependencies } from "../dependencies";
|
|
38
|
+
import { validateVariableSetAttachment } from "../domain/environments";
|
|
39
|
+
import { externalContinuationCommitAuthorizer } from "./external-continuation";
|
|
40
|
+
|
|
41
|
+
type SessionTenancyDependencies = Pick<
|
|
42
|
+
AppDependencies,
|
|
43
|
+
"db" | "bus" | "sessionAuthorization" | "settings"
|
|
44
|
+
>;
|
|
45
|
+
|
|
46
|
+
export class SessionTenancyManagedHumanRequiredError extends Error {
|
|
47
|
+
readonly name = "SessionTenancyManagedHumanRequiredError";
|
|
48
|
+
constructor() {
|
|
49
|
+
super("Session tenancy mutations require the owning managed-human session");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class SessionTenancyPersistenceOutcomeUnknownError extends Error {
|
|
54
|
+
readonly name = "SessionTenancyPersistenceOutcomeUnknownError";
|
|
55
|
+
constructor(options?: ErrorOptions) {
|
|
56
|
+
super("The session tenancy mutation outcome is unknown", options);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function requireCanonicalManagedHuman(
|
|
61
|
+
authorization: AccessGrantAuthorization,
|
|
62
|
+
workspaceId: string,
|
|
63
|
+
): void {
|
|
64
|
+
if (
|
|
65
|
+
!authorization.canonicalManagedHumanSession ||
|
|
66
|
+
!authorization.contextIntegrity ||
|
|
67
|
+
authorization.authenticatedSubjectId !== authorization.grant.subjectId ||
|
|
68
|
+
authorization.grant.workspaceId !== workspaceId
|
|
69
|
+
) {
|
|
70
|
+
throw new SessionTenancyManagedHumanRequiredError();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function requireVerifiedOwningUser(
|
|
75
|
+
authorization: AccessGrantAuthorization,
|
|
76
|
+
workspaceId: string,
|
|
77
|
+
): void {
|
|
78
|
+
if (
|
|
79
|
+
!hasVerifiedOwningUserAuthorization(authorization) ||
|
|
80
|
+
authorization.grant.workspaceId !== workspaceId
|
|
81
|
+
) {
|
|
82
|
+
throw new SessionTenancyManagedHumanRequiredError();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function getManagedHumanSessionCreateCapabilities(
|
|
87
|
+
deps: Pick<SessionTenancyDependencies, "db">,
|
|
88
|
+
authorization: AccessGrantAuthorization,
|
|
89
|
+
workspaceId: string,
|
|
90
|
+
): Promise<SessionTenancyCreateCapabilities> {
|
|
91
|
+
requirePermission(authorization.grant, "sessions:create");
|
|
92
|
+
try {
|
|
93
|
+
requireVerifiedOwningUser(authorization, workspaceId);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error instanceof SessionTenancyManagedHumanRequiredError) {
|
|
96
|
+
return SessionTenancyCreateCapabilities.parse({
|
|
97
|
+
activated: false,
|
|
98
|
+
canCreatePrivate: false,
|
|
99
|
+
reason: "managed_session_required",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
// Mirror the database create fence exactly: a managed human's own Personal
|
|
105
|
+
// workspace needs only the operator readiness receipt, while a shared
|
|
106
|
+
// organization workspace additionally needs the owner/admin product setting.
|
|
107
|
+
let activated = false;
|
|
108
|
+
try {
|
|
109
|
+
const policy = await getPrivateSessionCreatePolicy(deps.db, {
|
|
110
|
+
workspaceId,
|
|
111
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
112
|
+
});
|
|
113
|
+
activated = policy.personalWorkspace ? policy.platformAvailable : policy.organizationEnabled;
|
|
114
|
+
} catch (error) {
|
|
115
|
+
// A canonical managed human without an active organization membership or
|
|
116
|
+
// stated workspace authority (legacy managed access) is simply not
|
|
117
|
+
// private-ready; the definer's 42501 is a capability answer here, not a
|
|
118
|
+
// failure. Every other error still propagates.
|
|
119
|
+
if (
|
|
120
|
+
!(error instanceof SessionTenancyAccessError) &&
|
|
121
|
+
nestedPostgresSqlState(error) !== "42501"
|
|
122
|
+
) {
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return SessionTenancyCreateCapabilities.parse({
|
|
127
|
+
activated,
|
|
128
|
+
canCreatePrivate: activated,
|
|
129
|
+
reason: activated ? "available" : "not_activated",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function requireManagedHumanPrivateSessionCreate(
|
|
134
|
+
deps: Pick<SessionTenancyDependencies, "db">,
|
|
135
|
+
authorization: AccessGrantAuthorization,
|
|
136
|
+
workspaceId: string,
|
|
137
|
+
): Promise<void> {
|
|
138
|
+
// Target-free readiness preflight only. The owner/admin organization setting
|
|
139
|
+
// is enforced by the create transaction itself (under the organization
|
|
140
|
+
// fence, after keyed replay resolution), so a committed keyed success still
|
|
141
|
+
// replays after the setting is disabled while a fresh key fails closed.
|
|
142
|
+
await requireSessionTenancyMutationGate(deps, authorization, workspaceId, ["sessions:create"]);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function requireSessionTenancyMutationGate(
|
|
146
|
+
deps: Pick<SessionTenancyDependencies, "db">,
|
|
147
|
+
authorization: AccessGrantAuthorization,
|
|
148
|
+
workspaceId: string,
|
|
149
|
+
permissions: ReadonlyArray<"sessions:read" | "sessions:create" | "sessions:control">,
|
|
150
|
+
): Promise<void> {
|
|
151
|
+
// These checks are deliberately target-free. A rejected principal must not
|
|
152
|
+
// cause a session lookup or embedding-host callback that distinguishes a
|
|
153
|
+
// missing, shared, or another owner's private session.
|
|
154
|
+
requireVerifiedOwningUser(authorization, workspaceId);
|
|
155
|
+
for (const permission of permissions) requirePermission(authorization.grant, permission);
|
|
156
|
+
if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
|
|
157
|
+
throw new SessionTenancyNotActivatedError();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function publishExactCommittedEvent(
|
|
162
|
+
deps: { db: Database; bus: EventBus },
|
|
163
|
+
input: {
|
|
164
|
+
workspaceId: string;
|
|
165
|
+
sessionId: string;
|
|
166
|
+
subjectId: string;
|
|
167
|
+
eventId: string | null;
|
|
168
|
+
eventSequence: number | null;
|
|
169
|
+
},
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
if (input.eventId === null && input.eventSequence === null) return;
|
|
172
|
+
if (input.eventId === null || input.eventSequence === null) {
|
|
173
|
+
console.error("[session-tenancy] committed event receipt is incomplete", {
|
|
174
|
+
errorClass: "SessionTenancyEventInvariantError",
|
|
175
|
+
errorCode: "session_tenancy_event_receipt_incomplete",
|
|
176
|
+
origin: "core",
|
|
177
|
+
sessionId: input.sessionId,
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const event = await getSessionEventForSubject(
|
|
183
|
+
deps.db,
|
|
184
|
+
input.workspaceId,
|
|
185
|
+
input.subjectId,
|
|
186
|
+
input.eventId,
|
|
187
|
+
);
|
|
188
|
+
if (!event || event.sessionId !== input.sessionId || event.sequence !== input.eventSequence) {
|
|
189
|
+
console.error("[session-tenancy] committed event receipt did not resolve exactly", {
|
|
190
|
+
errorClass: "SessionTenancyEventInvariantError",
|
|
191
|
+
errorCode: "session_tenancy_event_receipt_mismatch",
|
|
192
|
+
origin: "core",
|
|
193
|
+
sessionId: input.sessionId,
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
await publishDurableSessionEvents(deps.bus, input.workspaceId, input.sessionId, [event]);
|
|
198
|
+
} catch {
|
|
199
|
+
// The mutation and event committed atomically before this read. Live fanout
|
|
200
|
+
// is repairable from the durable stream, so a publication-side failure must
|
|
201
|
+
// not turn a known successful mutation into an unknown client outcome.
|
|
202
|
+
console.warn("[session-tenancy] committed event live publication was deferred to replay", {
|
|
203
|
+
errorClass: "SessionTenancyEventPublishOperationError",
|
|
204
|
+
errorCode: "session_tenancy_event_publish_deferred",
|
|
205
|
+
origin: "core",
|
|
206
|
+
sessionId: input.sessionId,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function runTenancyMutation<T>(operation: () => Promise<T>): Promise<T> {
|
|
212
|
+
try {
|
|
213
|
+
return await operation();
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (isRetryableDatabaseTransportFailure(error)) {
|
|
216
|
+
throw new SessionTenancyPersistenceOutcomeUnknownError({ cause: error });
|
|
217
|
+
}
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function projectAndPublishSessionFork(
|
|
223
|
+
deps: SessionTenancyDependencies,
|
|
224
|
+
actorSubjectId: string,
|
|
225
|
+
result: ForkSessionContentResult,
|
|
226
|
+
): Promise<ForkSessionResponse> {
|
|
227
|
+
const response = ForkSessionResponse.parse({
|
|
228
|
+
operationId: result.operationId,
|
|
229
|
+
eventId: result.eventId,
|
|
230
|
+
eventSequence: result.eventSequence,
|
|
231
|
+
sessionId: result.sessionId,
|
|
232
|
+
workspaceId: result.workspaceId,
|
|
233
|
+
visibility: sessionVisibilityToPublic(result.visibility),
|
|
234
|
+
authorityEpoch: result.authorityEpoch,
|
|
235
|
+
copiedHistoryItemCount: result.copiedHistoryItemCount,
|
|
236
|
+
replay: result.replay,
|
|
237
|
+
});
|
|
238
|
+
await publishExactCommittedEvent(deps, {
|
|
239
|
+
workspaceId: response.workspaceId,
|
|
240
|
+
sessionId: response.sessionId,
|
|
241
|
+
subjectId: actorSubjectId,
|
|
242
|
+
eventId: response.eventId,
|
|
243
|
+
eventSequence: response.eventSequence,
|
|
244
|
+
});
|
|
245
|
+
return response;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function updateManagedHumanSessionVisibility(
|
|
249
|
+
deps: SessionTenancyDependencies,
|
|
250
|
+
authorization: AccessGrantAuthorization,
|
|
251
|
+
workspaceId: string,
|
|
252
|
+
sessionId: string,
|
|
253
|
+
request: UpdateSessionVisibilityRequest,
|
|
254
|
+
authorizationSurface: SessionAuthorizationSurface = "core",
|
|
255
|
+
): Promise<UpdateSessionVisibilityResponse> {
|
|
256
|
+
const beforeCommit = externalContinuationCommitAuthorizer(authorization);
|
|
257
|
+
await requireSessionTenancyMutationGate(deps, authorization, workspaceId, ["sessions:control"]);
|
|
258
|
+
await requireSessionAuthorization(deps, authorization.grant, {
|
|
259
|
+
sessionId,
|
|
260
|
+
operation: "session.visibility.write",
|
|
261
|
+
surface: authorizationSurface,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const result = await runTenancyMutation(
|
|
265
|
+
async () =>
|
|
266
|
+
await transitionSessionVisibility(deps.db, {
|
|
267
|
+
workspaceId,
|
|
268
|
+
sessionId,
|
|
269
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
270
|
+
targetVisibility: sessionVisibilityFromPublic(request.visibility),
|
|
271
|
+
expectedAuthorityEpoch: request.expectedAuthorityEpoch,
|
|
272
|
+
operationKey: request.idempotencyKey,
|
|
273
|
+
...(beforeCommit ? { beforeCommit } : {}),
|
|
274
|
+
}),
|
|
275
|
+
);
|
|
276
|
+
const response = UpdateSessionVisibilityResponse.parse({
|
|
277
|
+
operationId: result.operationId,
|
|
278
|
+
eventId: result.eventId,
|
|
279
|
+
eventSequence: result.eventSequence,
|
|
280
|
+
visibility: sessionVisibilityToPublic(result.visibility),
|
|
281
|
+
authorityEpoch: result.authorityEpoch,
|
|
282
|
+
changed: result.changed,
|
|
283
|
+
replay: result.replay,
|
|
284
|
+
revokedGrantCount: result.revokedGrantCount,
|
|
285
|
+
});
|
|
286
|
+
await publishExactCommittedEvent(deps, {
|
|
287
|
+
workspaceId,
|
|
288
|
+
sessionId,
|
|
289
|
+
subjectId: authorization.grant.subjectId,
|
|
290
|
+
eventId: response.eventId,
|
|
291
|
+
eventSequence: response.eventSequence,
|
|
292
|
+
});
|
|
293
|
+
return response;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export async function forkManagedHumanSession(
|
|
297
|
+
deps: SessionTenancyDependencies,
|
|
298
|
+
authorization: AccessGrantAuthorization,
|
|
299
|
+
workspaceId: string,
|
|
300
|
+
sourceSessionId: string,
|
|
301
|
+
request: ForkSessionRequest,
|
|
302
|
+
authorizationSurface: SessionAuthorizationSurface = "core",
|
|
303
|
+
): Promise<ForkSessionResponse> {
|
|
304
|
+
await requireSessionTenancyMutationGate(deps, authorization, workspaceId, [
|
|
305
|
+
"sessions:read",
|
|
306
|
+
"sessions:create",
|
|
307
|
+
]);
|
|
308
|
+
const beforeCommit = externalContinuationCommitAuthorizer(authorization);
|
|
309
|
+
const forkInput = {
|
|
310
|
+
sourceWorkspaceId: workspaceId,
|
|
311
|
+
sourceSessionId,
|
|
312
|
+
...(request.sourceEventId ? { sourceEventId: request.sourceEventId } : {}),
|
|
313
|
+
actorSubjectId: authorization.grant.subjectId,
|
|
314
|
+
destinationWorkspaceId: workspaceId,
|
|
315
|
+
destinationVisibility:
|
|
316
|
+
request.visibility === "private" ? ("user_private" as const) : ("workspace_shared" as const),
|
|
317
|
+
workspaceSharedAcknowledged: request.workspaceSharedAcknowledged,
|
|
318
|
+
operationKey: request.idempotencyKey,
|
|
319
|
+
...(beforeCommit ? { beforeCommit } : {}),
|
|
320
|
+
...(request.rigId !== undefined || request.variableSetIds !== undefined
|
|
321
|
+
? {
|
|
322
|
+
runtimeRequest: {
|
|
323
|
+
rigId: request.rigId ?? null,
|
|
324
|
+
variableSetIds: request.variableSetIds ?? [],
|
|
325
|
+
},
|
|
326
|
+
}
|
|
327
|
+
: {}),
|
|
328
|
+
};
|
|
329
|
+
const recoverAppliedReceipt = async () =>
|
|
330
|
+
await runTenancyMutation(async () => await replayAppliedSessionFork(deps.db, forkInput));
|
|
331
|
+
const runtimeSetupRequested = request.rigId !== undefined || request.variableSetIds !== undefined;
|
|
332
|
+
let runtimeConfigurationPromise: Promise<{
|
|
333
|
+
variableSetIds: string[];
|
|
334
|
+
rigId: string | null;
|
|
335
|
+
rigVersionId: string | null;
|
|
336
|
+
}> | null = null;
|
|
337
|
+
const resolveRuntimeConfiguration = async () => {
|
|
338
|
+
if (runtimeConfigurationPromise) return await runtimeConfigurationPromise;
|
|
339
|
+
runtimeConfigurationPromise = (async () => {
|
|
340
|
+
const variableSets: VariableSet[] = [];
|
|
341
|
+
for (const variableSetId of request.variableSetIds ?? []) {
|
|
342
|
+
variableSets.push(
|
|
343
|
+
await validateVariableSetAttachment(
|
|
344
|
+
{ settings: deps.settings, db: deps.db },
|
|
345
|
+
authorization.grant,
|
|
346
|
+
workspaceId,
|
|
347
|
+
variableSetId,
|
|
348
|
+
),
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
let rigVersionId: string | null = null;
|
|
352
|
+
if (request.rigId) {
|
|
353
|
+
requirePermission(authorization.grant, "rigs:use");
|
|
354
|
+
const rig = await getRig(deps.db, authorization.grant, request.rigId);
|
|
355
|
+
if (!rig?.activeVersion) {
|
|
356
|
+
throw new SessionTenancyInvalidRequestError();
|
|
357
|
+
}
|
|
358
|
+
for (const defaultVariableSetId of new Set(rig.activeVersion.defaultVariableSetIds)) {
|
|
359
|
+
await validateVariableSetAttachment(
|
|
360
|
+
{ settings: deps.settings, db: deps.db },
|
|
361
|
+
authorization.grant,
|
|
362
|
+
workspaceId,
|
|
363
|
+
defaultVariableSetId,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
rigVersionId = rig.activeVersion.id;
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
variableSetIds: variableSets.map((variableSet) => variableSet.id),
|
|
370
|
+
rigId: request.rigId ?? null,
|
|
371
|
+
rigVersionId,
|
|
372
|
+
};
|
|
373
|
+
})();
|
|
374
|
+
return await runtimeConfigurationPromise;
|
|
375
|
+
};
|
|
376
|
+
const publishRuntimeConfiguration = async (result: ForkSessionContentResult) => {
|
|
377
|
+
if (!result.runtimeEventId || !result.runtimeEventSequence) return;
|
|
378
|
+
await publishExactCommittedEvent(deps, {
|
|
379
|
+
workspaceId,
|
|
380
|
+
sessionId: result.sessionId,
|
|
381
|
+
subjectId: authorization.grant.subjectId,
|
|
382
|
+
eventId: result.runtimeEventId,
|
|
383
|
+
eventSequence: result.runtimeEventSequence,
|
|
384
|
+
});
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// A committed response is owned by the exact actor/key/request tuple and no
|
|
388
|
+
// longer depends on mutable source visibility. Resolve it before source/host
|
|
389
|
+
// authorization so a lost response remains recoverable after the source
|
|
390
|
+
// owner makes a formerly shared session private.
|
|
391
|
+
const applied = await recoverAppliedReceipt();
|
|
392
|
+
if (applied) {
|
|
393
|
+
const response = await projectAndPublishSessionFork(
|
|
394
|
+
deps,
|
|
395
|
+
authorization.grant.subjectId,
|
|
396
|
+
applied,
|
|
397
|
+
);
|
|
398
|
+
await publishRuntimeConfiguration(applied);
|
|
399
|
+
return response;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
await requireSessionAuthorization(deps, authorization.grant, {
|
|
404
|
+
sessionId: sourceSessionId,
|
|
405
|
+
operation: "session.fork.create",
|
|
406
|
+
surface: authorizationSurface,
|
|
407
|
+
});
|
|
408
|
+
} catch (authorizationError) {
|
|
409
|
+
// Close the race where a duplicate request checked before the first commit
|
|
410
|
+
// but mutable source/host authority changed immediately after it. Only an
|
|
411
|
+
// exact applied receipt can convert the authorization failure into replay.
|
|
412
|
+
const racedApplied = await recoverAppliedReceipt();
|
|
413
|
+
if (racedApplied) {
|
|
414
|
+
const response = await projectAndPublishSessionFork(
|
|
415
|
+
deps,
|
|
416
|
+
authorization.grant.subjectId,
|
|
417
|
+
racedApplied,
|
|
418
|
+
);
|
|
419
|
+
await publishRuntimeConfiguration(racedApplied);
|
|
420
|
+
return response;
|
|
421
|
+
}
|
|
422
|
+
throw authorizationError;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Validate all requested runtime resources before committing the independent
|
|
426
|
+
// fork so ordinary authorization failures cannot leave an unusable copy.
|
|
427
|
+
const source = await getSessionForSubject(
|
|
428
|
+
deps.db,
|
|
429
|
+
workspaceId,
|
|
430
|
+
sourceSessionId,
|
|
431
|
+
authorization.grant.subjectId,
|
|
432
|
+
);
|
|
433
|
+
if (source) assertSessionIsNotImported(source);
|
|
434
|
+
const runtimeConfiguration = runtimeSetupRequested ? await resolveRuntimeConfiguration() : null;
|
|
435
|
+
const result = await runTenancyMutation(
|
|
436
|
+
async () =>
|
|
437
|
+
await forkSessionContent(deps.db, {
|
|
438
|
+
...forkInput,
|
|
439
|
+
...(runtimeConfiguration ? { runtimeConfiguration } : {}),
|
|
440
|
+
}),
|
|
441
|
+
);
|
|
442
|
+
const response = await projectAndPublishSessionFork(deps, authorization.grant.subjectId, result);
|
|
443
|
+
await publishRuntimeConfiguration(result);
|
|
444
|
+
return response;
|
|
445
|
+
}
|