@opengeni/core 0.28.0 → 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 +18826 -6847
- 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,50 @@
|
|
|
1
|
+
import { ServiceTurnInitiatorContext, type AccessGrant } from "@opengeni/contracts";
|
|
2
|
+
import { HTTPException } from "hono/http-exception";
|
|
3
|
+
|
|
4
|
+
/** Validated provenance only. The API key remains the authenticated principal. */
|
|
5
|
+
export function serviceInitiatorFromHeaders(
|
|
6
|
+
headers: Headers,
|
|
7
|
+
): Pick<AccessGrant, "serviceInitiator" | "serviceInitiatorContext"> | null {
|
|
8
|
+
const name = headers.get("x-opengeni-service-initiator");
|
|
9
|
+
const rawContext = headers.get("x-opengeni-service-context");
|
|
10
|
+
if (name === null && rawContext === null) return null;
|
|
11
|
+
if (headers.has("x-opengeni-external-actor")) {
|
|
12
|
+
throw new HTTPException(422, {
|
|
13
|
+
message: "service initiator and external actor (asUser) are mutually exclusive",
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
if (name === null || !/^[a-z0-9][a-z0-9:._-]{0,63}$/.test(name)) {
|
|
17
|
+
throw new HTTPException(422, {
|
|
18
|
+
message: "x-opengeni-service-initiator must match ^[a-z0-9][a-z0-9:._-]{0,63}$",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
let context: Record<string, string | number | boolean> = {};
|
|
22
|
+
if (rawContext !== null) {
|
|
23
|
+
try {
|
|
24
|
+
if (new TextEncoder().encode(rawContext).byteLength > 2048) throw new Error("oversize");
|
|
25
|
+
const parsed: unknown = JSON.parse(rawContext);
|
|
26
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("object");
|
|
27
|
+
if (
|
|
28
|
+
!Object.values(parsed).every(
|
|
29
|
+
(value) =>
|
|
30
|
+
typeof value === "string" ||
|
|
31
|
+
typeof value === "boolean" ||
|
|
32
|
+
(typeof value === "number" && Number.isFinite(value)),
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
throw new Error("flat");
|
|
36
|
+
context = parsed as typeof context;
|
|
37
|
+
// Keep delegated provenance's reserved, OpenGeni-owned lineage fields.
|
|
38
|
+
if (!ServiceTurnInitiatorContext.safeParse(context).success) throw new Error("reserved");
|
|
39
|
+
} catch {
|
|
40
|
+
throw new HTTPException(422, {
|
|
41
|
+
message:
|
|
42
|
+
"x-opengeni-service-context must be a JSON object of flat string/number/boolean values, at most 2048 UTF-8 bytes, without reserved Opengeni provenance fields",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
serviceInitiator: { kind: "service", subjectId: name },
|
|
48
|
+
serviceInitiatorContext: context,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createGraphqlMcpServer,
|
|
3
|
+
createOpenApiMcpServer,
|
|
4
|
+
createPinnedIntegrationTransport,
|
|
5
|
+
directIntegrationTransport,
|
|
6
|
+
IntegrationInvocationError,
|
|
7
|
+
type IntegrationCredentialResolver,
|
|
8
|
+
type IntegrationInvocationAuthority,
|
|
9
|
+
type IntegrationTransport,
|
|
10
|
+
} from "@opengeni/capabilities";
|
|
11
|
+
import type { Settings } from "@opengeni/config";
|
|
12
|
+
import type { ToolAuthNeededPayload } from "@opengeni/contracts";
|
|
13
|
+
import type {
|
|
14
|
+
ApiIntegrationRuntime,
|
|
15
|
+
ResolveConnectionCredentialInput,
|
|
16
|
+
ResolveConnectionCredentialResult,
|
|
17
|
+
} from "@opengeni/db";
|
|
18
|
+
import type { FetchLike } from "@opengeni/network";
|
|
19
|
+
import type { LocalMcpServerRegistration } from "@opengeni/runtime";
|
|
20
|
+
|
|
21
|
+
export type BuildApiIntegrationServersInput = {
|
|
22
|
+
settings: Settings;
|
|
23
|
+
integrations: readonly ApiIntegrationRuntime[];
|
|
24
|
+
authority: Omit<IntegrationInvocationAuthority, "connectionRef">;
|
|
25
|
+
resolveCredential: (
|
|
26
|
+
input: ResolveConnectionCredentialInput,
|
|
27
|
+
) => Promise<ResolveConnectionCredentialResult>;
|
|
28
|
+
onAuthNeeded?: (payload: ToolAuthNeededPayload) => Promise<void> | void;
|
|
29
|
+
fetchImpl?: FetchLike;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Compile active persisted API facets into ordinary in-process MCP providers.
|
|
34
|
+
* The caller supplies its authority resolver, so agent attempts and current
|
|
35
|
+
* humans use the same provider assembly without sharing credentials or policy.
|
|
36
|
+
*/
|
|
37
|
+
export function buildApiIntegrationMcpServers(
|
|
38
|
+
input: BuildApiIntegrationServersInput,
|
|
39
|
+
): LocalMcpServerRegistration[] {
|
|
40
|
+
const transport = createPinnedIntegrationTransport({
|
|
41
|
+
network: input.settings,
|
|
42
|
+
...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}),
|
|
43
|
+
});
|
|
44
|
+
return input.integrations.map((integration) => {
|
|
45
|
+
const authority: IntegrationInvocationAuthority = {
|
|
46
|
+
...input.authority,
|
|
47
|
+
...(integration.connectionRef?.connectionId
|
|
48
|
+
? { connectionRef: integration.connectionRef.connectionId }
|
|
49
|
+
: {}),
|
|
50
|
+
};
|
|
51
|
+
const credentialResolver = integration.connectionRef
|
|
52
|
+
? integrationCredentialResolver(input, integration, "execution")
|
|
53
|
+
: undefined;
|
|
54
|
+
const buildServer = (
|
|
55
|
+
serverTransport: IntegrationTransport,
|
|
56
|
+
serverCredentialResolver = credentialResolver,
|
|
57
|
+
) =>
|
|
58
|
+
integration.revision.protocol === "openapi"
|
|
59
|
+
? createOpenApiMcpServer({
|
|
60
|
+
revision: integration.revision,
|
|
61
|
+
transport: serverTransport,
|
|
62
|
+
authority,
|
|
63
|
+
...(serverCredentialResolver ? { credentialResolver: serverCredentialResolver } : {}),
|
|
64
|
+
})
|
|
65
|
+
: createGraphqlMcpServer({
|
|
66
|
+
revision: integration.revision,
|
|
67
|
+
endpoint: integration.baseUrl,
|
|
68
|
+
transport: serverTransport,
|
|
69
|
+
authority,
|
|
70
|
+
...(serverCredentialResolver ? { credentialResolver: serverCredentialResolver } : {}),
|
|
71
|
+
});
|
|
72
|
+
const server = buildServer(transport);
|
|
73
|
+
const preflightCredentialResolver = integration.connectionRef
|
|
74
|
+
? integrationCredentialResolver(input, integration, "preflight")
|
|
75
|
+
: undefined;
|
|
76
|
+
const preflightServer = preflightCredentialResolver
|
|
77
|
+
? buildServer(providerBlockingPreflightTransport, preflightCredentialResolver)
|
|
78
|
+
: null;
|
|
79
|
+
return {
|
|
80
|
+
id: integration.serverId,
|
|
81
|
+
server,
|
|
82
|
+
approvalAuthority: {
|
|
83
|
+
kind: "api_integration",
|
|
84
|
+
capabilityId: integration.capabilityId,
|
|
85
|
+
pluginKey: integration.pluginKey,
|
|
86
|
+
pluginInstallationId: integration.pluginInstallationId,
|
|
87
|
+
installationVersion: integration.installationVersion,
|
|
88
|
+
instanceId: integration.instanceId,
|
|
89
|
+
instanceKey: integration.instanceKey,
|
|
90
|
+
instanceVersion: integration.instanceVersion,
|
|
91
|
+
definitionId: integration.definitionId,
|
|
92
|
+
definitionProvenance: integration.definitionProvenance,
|
|
93
|
+
revisionId: integration.revision.id,
|
|
94
|
+
baseUrl: integration.baseUrl,
|
|
95
|
+
providerDomain: integration.providerDomain,
|
|
96
|
+
connectionRef: integration.connectionRef,
|
|
97
|
+
connectionAuthorityGeneration: integration.connectionAuthorityGeneration,
|
|
98
|
+
},
|
|
99
|
+
...(integration.connectionRef?.connectionId
|
|
100
|
+
? { resolvedConnectionId: integration.connectionRef.connectionId }
|
|
101
|
+
: {}),
|
|
102
|
+
...(credentialResolver
|
|
103
|
+
? {
|
|
104
|
+
preflightCall: async (
|
|
105
|
+
toolName: string,
|
|
106
|
+
args: Record<string, unknown>,
|
|
107
|
+
options?: { signal?: AbortSignal },
|
|
108
|
+
) => await preflightApiIntegrationCall(preflightServer!, toolName, args, options),
|
|
109
|
+
}
|
|
110
|
+
: {}),
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const providerBlockingPreflightTransport = directIntegrationTransport(async () => {
|
|
116
|
+
throw new Error("integration provider request blocked by preflight");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
async function preflightApiIntegrationCall(
|
|
120
|
+
server: LocalMcpServerRegistration["server"],
|
|
121
|
+
toolName: string,
|
|
122
|
+
args: Record<string, unknown>,
|
|
123
|
+
options?: { signal?: AbortSignal },
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
try {
|
|
126
|
+
await server.callTool(toolName, args, null, options);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error instanceof IntegrationInvocationError && error.code === "request_failed") return;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
throw new Error("integration preflight unexpectedly crossed the provider request boundary");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function integrationCredentialResolver(
|
|
135
|
+
input: BuildApiIntegrationServersInput,
|
|
136
|
+
integration: ApiIntegrationRuntime,
|
|
137
|
+
credentialResolutionMode: "execution" | "preflight",
|
|
138
|
+
): IntegrationCredentialResolver {
|
|
139
|
+
const connectionRef = integration.connectionRef;
|
|
140
|
+
if (!connectionRef) throw new Error("Integration credential resolver requires a connection");
|
|
141
|
+
const expectedAuthorityGeneration = integration.connectionAuthorityGeneration;
|
|
142
|
+
if (expectedAuthorityGeneration === null) {
|
|
143
|
+
throw new Error("Integration credential resolver requires a connection authority generation");
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
resolve: async (request) => {
|
|
147
|
+
const result = await input.resolveCredential({
|
|
148
|
+
workspaceId: input.authority.workspaceId,
|
|
149
|
+
serverId: integration.serverId,
|
|
150
|
+
toolName: request.operationKey,
|
|
151
|
+
connectionRef,
|
|
152
|
+
destinationUrl: request.destinationUrl,
|
|
153
|
+
credentialTarget: "http_api",
|
|
154
|
+
forceRefresh: request.forceRefresh === true,
|
|
155
|
+
credentialResolutionMode,
|
|
156
|
+
expectedAuthorityGeneration,
|
|
157
|
+
});
|
|
158
|
+
if (result.status === "auth_needed") {
|
|
159
|
+
await publishAuthNeeded(
|
|
160
|
+
input,
|
|
161
|
+
integration.serverId,
|
|
162
|
+
request.operationKey,
|
|
163
|
+
result,
|
|
164
|
+
connectionRef,
|
|
165
|
+
);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const destination = new URL(request.destinationUrl);
|
|
169
|
+
return {
|
|
170
|
+
audience: { origin: destination.origin, pathPrefix: "/" },
|
|
171
|
+
placements:
|
|
172
|
+
result.placements ??
|
|
173
|
+
Object.entries(result.headers).map(([name, value]) => ({
|
|
174
|
+
carrier: "header" as const,
|
|
175
|
+
name,
|
|
176
|
+
value,
|
|
177
|
+
})),
|
|
178
|
+
...(result.authorizeProviderRequest
|
|
179
|
+
? { authorizeProviderRequest: result.authorizeProviderRequest }
|
|
180
|
+
: {}),
|
|
181
|
+
...(result.expiresAt ? { expiresAt: result.expiresAt.toISOString() } : {}),
|
|
182
|
+
...(connectionRef.scopes ? { scope: [...connectionRef.scopes] } : {}),
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function publishAuthNeeded(
|
|
189
|
+
input: BuildApiIntegrationServersInput,
|
|
190
|
+
serverId: string,
|
|
191
|
+
toolName: string,
|
|
192
|
+
result: Extract<ResolveConnectionCredentialResult, { status: "auth_needed" }>,
|
|
193
|
+
connectionRef: NonNullable<ApiIntegrationRuntime["connectionRef"]>,
|
|
194
|
+
): Promise<void> {
|
|
195
|
+
try {
|
|
196
|
+
await input.onAuthNeeded?.({
|
|
197
|
+
serverId,
|
|
198
|
+
toolName,
|
|
199
|
+
providerDomain: result.providerDomain,
|
|
200
|
+
...(result.provider ? { provider: result.provider } : {}),
|
|
201
|
+
reason: result.reason,
|
|
202
|
+
...(result.connectionId ? { connectionId: result.connectionId } : {}),
|
|
203
|
+
...(result.authoritySource === "host" || connectionRef.authoritySource === "host"
|
|
204
|
+
? { authoritySource: "host" as const }
|
|
205
|
+
: {}),
|
|
206
|
+
...(result.scopes ? { scopes: result.scopes } : {}),
|
|
207
|
+
...(result.resource ? { resource: result.resource } : {}),
|
|
208
|
+
...(result.selectedResources ? { selectedResources: result.selectedResources } : {}),
|
|
209
|
+
...(result.authorizationUrl ? { authorizationUrl: result.authorizationUrl } : {}),
|
|
210
|
+
});
|
|
211
|
+
} catch {
|
|
212
|
+
// Authentication notices are advisory UI/audit signals. The local tool
|
|
213
|
+
// still returns the fixed connection-required result when publication fails.
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AppendArchivedSessionEventsRequest,
|
|
3
|
+
AppendArchivedSessionEventsResponse,
|
|
4
|
+
ImportArchivedSessionRequest,
|
|
5
|
+
ImportArchivedSessionResponse,
|
|
6
|
+
type Permission,
|
|
7
|
+
type SessionEvent,
|
|
8
|
+
} from "@opengeni/contracts";
|
|
9
|
+
import {
|
|
10
|
+
ArchivedSessionImportError,
|
|
11
|
+
appendArchivedSessionEvents,
|
|
12
|
+
getArchivedSessionImportId,
|
|
13
|
+
importArchivedSession,
|
|
14
|
+
withSessionRlsActorContext,
|
|
15
|
+
} from "@opengeni/db";
|
|
16
|
+
import { publishDurableSessionEvents } from "@opengeni/events";
|
|
17
|
+
import { HTTPException } from "hono/http-exception";
|
|
18
|
+
import {
|
|
19
|
+
hasVerifiedOwningUserAuthorization,
|
|
20
|
+
requirePermission,
|
|
21
|
+
requireResolvedAccessGrantAuthorization,
|
|
22
|
+
type AccessGrantAuthorization,
|
|
23
|
+
} from "../access";
|
|
24
|
+
import type { ApiRouteDeps } from "../dependencies";
|
|
25
|
+
import { creationInitiatorForGrant } from "../domain/sessions";
|
|
26
|
+
import { fileOwnerContextForAccess } from "../domain/file-owner";
|
|
27
|
+
import {
|
|
28
|
+
grantHasAgentAttemptAuthority,
|
|
29
|
+
requireSessionAuthorization,
|
|
30
|
+
} from "../session-authorization";
|
|
31
|
+
import { externalContinuationCommitAuthorizer } from "./external-continuation";
|
|
32
|
+
import { requireManagedHumanPrivateSessionCreate } from "./session-tenancy";
|
|
33
|
+
|
|
34
|
+
export { ArchivedSessionImportError } from "@opengeni/db";
|
|
35
|
+
|
|
36
|
+
function requireImportAuthorization(
|
|
37
|
+
authorization: AccessGrantAuthorization,
|
|
38
|
+
workspaceId: string,
|
|
39
|
+
permission: Permission,
|
|
40
|
+
) {
|
|
41
|
+
const grant = requireResolvedAccessGrantAuthorization(authorization, workspaceId);
|
|
42
|
+
requirePermission(grant, permission);
|
|
43
|
+
if (grantHasAgentAttemptAuthority(grant)) {
|
|
44
|
+
throw new HTTPException(403, {
|
|
45
|
+
message: "Session history imports require a user or service request",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return grant;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function importerScope(
|
|
52
|
+
authorization: AccessGrantAuthorization,
|
|
53
|
+
workspaceId: string,
|
|
54
|
+
permission: Permission,
|
|
55
|
+
fileOwnerSubjectId: string | null,
|
|
56
|
+
) {
|
|
57
|
+
const grant = authorization.grant;
|
|
58
|
+
const apiKeyId =
|
|
59
|
+
grant.principalKind === "api_key" && grant.subjectId.startsWith("api_key:")
|
|
60
|
+
? grant.subjectId.slice("api_key:".length)
|
|
61
|
+
: undefined;
|
|
62
|
+
const beforeCommit = externalContinuationCommitAuthorizer(authorization);
|
|
63
|
+
return {
|
|
64
|
+
accountId: grant.accountId,
|
|
65
|
+
workspaceId,
|
|
66
|
+
subjectId: grant.subjectId,
|
|
67
|
+
fileOwnerSubjectId,
|
|
68
|
+
requiredPermission: permission,
|
|
69
|
+
requireLiveSubject:
|
|
70
|
+
hasVerifiedOwningUserAuthorization(authorization) || authorization.canonicalLocalHumanSession,
|
|
71
|
+
...(apiKeyId ? { apiKeyId } : {}),
|
|
72
|
+
...(beforeCommit ? { beforeCommit } : {}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Committed import truth survives unavailable fanout; SSE catch-up repairs it.
|
|
77
|
+
* Never make a known committed append look failed and invite an unsafe retry. */
|
|
78
|
+
async function publishImportedEvents(
|
|
79
|
+
deps: ApiRouteDeps,
|
|
80
|
+
workspaceId: string,
|
|
81
|
+
sessionId: string,
|
|
82
|
+
events: SessionEvent[],
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
if (!events.length) return;
|
|
85
|
+
try {
|
|
86
|
+
await publishDurableSessionEvents(deps.bus, workspaceId, sessionId, events);
|
|
87
|
+
} catch {
|
|
88
|
+
console.warn("[session-import] committed timeline fanout deferred to replay", { sessionId });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function importArchivedSessionForRequest(
|
|
93
|
+
deps: ApiRouteDeps,
|
|
94
|
+
authorization: AccessGrantAuthorization,
|
|
95
|
+
workspaceId: string,
|
|
96
|
+
rawPayload: unknown,
|
|
97
|
+
): Promise<ImportArchivedSessionResponse> {
|
|
98
|
+
const grant = requireImportAuthorization(authorization, workspaceId, "sessions:create");
|
|
99
|
+
const payload = ImportArchivedSessionRequest.parse(rawPayload);
|
|
100
|
+
if (payload.visibility === "user_private") {
|
|
101
|
+
await requireManagedHumanPrivateSessionCreate(deps, authorization, workspaceId);
|
|
102
|
+
}
|
|
103
|
+
const creator = creationInitiatorForGrant(grant);
|
|
104
|
+
if (!creator.initiator)
|
|
105
|
+
throw new HTTPException(403, { message: "Import creator is unavailable" });
|
|
106
|
+
const actor = await fileOwnerContextForAccess(deps, authorization, "sessions:create");
|
|
107
|
+
const result = await withSessionRlsActorContext(actor, () =>
|
|
108
|
+
importArchivedSession(deps.db, {
|
|
109
|
+
...importerScope(
|
|
110
|
+
authorization,
|
|
111
|
+
workspaceId,
|
|
112
|
+
"sessions:create",
|
|
113
|
+
actor.privateFileOwnerSubjectId ?? null,
|
|
114
|
+
),
|
|
115
|
+
payload,
|
|
116
|
+
createdBy: creator.initiator!,
|
|
117
|
+
createdByContext: creator.context ?? {},
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
await publishImportedEvents(deps, workspaceId, result.session.id, result.events);
|
|
121
|
+
return ImportArchivedSessionResponse.parse(result);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function appendArchivedSessionEventsForRequest(
|
|
125
|
+
deps: ApiRouteDeps,
|
|
126
|
+
authorization: AccessGrantAuthorization,
|
|
127
|
+
workspaceId: string,
|
|
128
|
+
importId: string,
|
|
129
|
+
rawPayload: unknown,
|
|
130
|
+
): Promise<AppendArchivedSessionEventsResponse> {
|
|
131
|
+
const grant = requireImportAuthorization(authorization, workspaceId, "sessions:control");
|
|
132
|
+
const payload = AppendArchivedSessionEventsRequest.parse(rawPayload);
|
|
133
|
+
const actor = await fileOwnerContextForAccess(deps, authorization, "sessions:control");
|
|
134
|
+
const result = await withSessionRlsActorContext(actor, async () => {
|
|
135
|
+
const sessionId = await getArchivedSessionImportId(
|
|
136
|
+
deps.db,
|
|
137
|
+
workspaceId,
|
|
138
|
+
grant.subjectId,
|
|
139
|
+
importId,
|
|
140
|
+
);
|
|
141
|
+
if (!sessionId) throw new ArchivedSessionImportError("SESSION_IMPORT_NOT_FOUND");
|
|
142
|
+
await requireSessionAuthorization(deps, grant, {
|
|
143
|
+
sessionId,
|
|
144
|
+
operation: "session.append",
|
|
145
|
+
surface: "core",
|
|
146
|
+
});
|
|
147
|
+
return appendArchivedSessionEvents(deps.db, {
|
|
148
|
+
...importerScope(
|
|
149
|
+
authorization,
|
|
150
|
+
workspaceId,
|
|
151
|
+
"sessions:control",
|
|
152
|
+
actor.privateFileOwnerSubjectId ?? null,
|
|
153
|
+
),
|
|
154
|
+
importId,
|
|
155
|
+
sessionId,
|
|
156
|
+
payload,
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
await publishImportedEvents(deps, workspaceId, result.sessionId, result.events);
|
|
160
|
+
return AppendArchivedSessionEventsResponse.parse(result);
|
|
161
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AccessGrant,
|
|
3
|
+
ResourceRef,
|
|
4
|
+
SubmitComposerDraftRequest,
|
|
5
|
+
SubmitComposerDraftResponse,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
7
|
+
import type { AccessGrantAuthorization } from "../access";
|
|
8
|
+
import type { AcceptSessionUserMessageDependencies } from "../dependencies";
|
|
9
|
+
import { acceptSessionUserMessageWithOutcome } from "../domain/sessions";
|
|
10
|
+
|
|
11
|
+
export type SubmitComposerDraftForRequestOptions = {
|
|
12
|
+
authorization?: AccessGrantAuthorization | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Trusted host-owned resources admitted with this command without writing
|
|
15
|
+
* them into the actor's browser-visible draft first. The saved draft remains
|
|
16
|
+
* the exact content fence for actor-owned text, annotations, resources, and
|
|
17
|
+
* execution policy; these resources participate in the accepted command's
|
|
18
|
+
* idempotency hash, validation, turn, and durable session resource set.
|
|
19
|
+
*/
|
|
20
|
+
additionalResources?: ResourceRef[] | undefined;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Atomically accept one established-session composer draft.
|
|
25
|
+
*
|
|
26
|
+
* This is the application boundary shared by the stock HTTP adapter and an
|
|
27
|
+
* in-process embedding host. A host may prepare its own durable business state
|
|
28
|
+
* before calling this function and project the returned receipt afterward, but
|
|
29
|
+
* OpenGeni remains the sole authority for draft validation/rotation, event
|
|
30
|
+
* append, turn creation, routing, and idempotent replay.
|
|
31
|
+
*/
|
|
32
|
+
export async function submitComposerDraftForRequest(
|
|
33
|
+
deps: AcceptSessionUserMessageDependencies,
|
|
34
|
+
grant: AccessGrant,
|
|
35
|
+
workspaceId: string,
|
|
36
|
+
sessionId: string,
|
|
37
|
+
input: SubmitComposerDraftRequest,
|
|
38
|
+
options: SubmitComposerDraftForRequestOptions = {},
|
|
39
|
+
): Promise<SubmitComposerDraftResponse> {
|
|
40
|
+
const additionalResources = options.additionalResources ?? [];
|
|
41
|
+
const result = await acceptSessionUserMessageWithOutcome(deps, grant, workspaceId, sessionId, {
|
|
42
|
+
text: input.text,
|
|
43
|
+
annotations: input.annotations,
|
|
44
|
+
modelContext: input.modelContext ?? null,
|
|
45
|
+
resources: [...input.resources, ...additionalResources],
|
|
46
|
+
...(additionalResources.length > 0 ? { composerDraftResources: input.resources } : {}),
|
|
47
|
+
model: input.model,
|
|
48
|
+
reasoningEffort: input.reasoningEffort,
|
|
49
|
+
latencyMode: input.latencyMode,
|
|
50
|
+
mcpCredentialUpdates: input.mcpCredentialUpdates ?? [],
|
|
51
|
+
connectionAccounts: input.connectionAccounts,
|
|
52
|
+
|
|
53
|
+
...(input.personalResourceAttachment
|
|
54
|
+
? { personalResourceAttachment: input.personalResourceAttachment }
|
|
55
|
+
: {}),
|
|
56
|
+
...(options.authorization ? { authorization: options.authorization } : {}),
|
|
57
|
+
delivery: input.delivery,
|
|
58
|
+
origin: "human",
|
|
59
|
+
expectedDraftRevision: input.expectedDraftRevision,
|
|
60
|
+
clientEventId: input.clientEventId,
|
|
61
|
+
...(input.controlEtag ? { controlEtag: input.controlEtag } : {}),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!result.draft) {
|
|
65
|
+
throw new Error("Accepted composer draft submission did not return its next draft");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
accepted: result.accepted,
|
|
70
|
+
turn: result.turn,
|
|
71
|
+
draft: result.draft,
|
|
72
|
+
receipt: result.receipt,
|
|
73
|
+
routing: result.routing,
|
|
74
|
+
interruptionCount: result.interruptionCount,
|
|
75
|
+
replay: result.replay,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Permission } from "@opengeni/contracts";
|
|
2
|
+
import type { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
|
|
3
|
+
import { hasPermission } from "../access";
|
|
4
|
+
import { requireExternalContinuationAuthority } from "./external-continuation";
|
|
5
|
+
import {
|
|
6
|
+
getWorkspaceGrant,
|
|
7
|
+
resolveNamedManagedPersonalWorkspaceGrant,
|
|
8
|
+
lockExternalWorkspaceMembershipLifecycle,
|
|
9
|
+
withWorkspaceSubjectRls,
|
|
10
|
+
type Database,
|
|
11
|
+
lockConnectionSetupKeyAuthority,
|
|
12
|
+
nestedPostgresSqlState,
|
|
13
|
+
} from "@opengeni/db";
|
|
14
|
+
import { HTTPException } from "hono/http-exception";
|
|
15
|
+
|
|
16
|
+
/** Accept only a verified request owner or authenticated server-minted OAuth
|
|
17
|
+
* state. The callback does not need a browser cookie: it rechecks the stored
|
|
18
|
+
* principal against current authority before claiming and committing effects. */
|
|
19
|
+
export async function requireConnectOwnerAuthority(
|
|
20
|
+
db: Database,
|
|
21
|
+
state: {
|
|
22
|
+
accountId: string;
|
|
23
|
+
workspaceId: string;
|
|
24
|
+
subjectId: string;
|
|
25
|
+
personalOwnerVerified?: boolean;
|
|
26
|
+
externalContinuation?: ExternalActorContinuation;
|
|
27
|
+
},
|
|
28
|
+
permission: Permission = "connections:write",
|
|
29
|
+
origin: ExternalActorContinuation | null = null,
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
async function requireExternal(continuation: ExternalActorContinuation) {
|
|
32
|
+
try {
|
|
33
|
+
await requireExternalContinuationAuthority(db, continuation, state, permission);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
const denied =
|
|
36
|
+
nestedPostgresSqlState(error) === "42501" ||
|
|
37
|
+
(error instanceof Error && error.message === "External continuation authority unavailable");
|
|
38
|
+
throw new HTTPException(denied ? 403 : 503, {
|
|
39
|
+
message: denied
|
|
40
|
+
? "Connection origin authority changed"
|
|
41
|
+
: "Connection origin authority unavailable",
|
|
42
|
+
cause: error,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Current request authority cannot replace the original restriction. Native
|
|
47
|
+
// owners may resume their work, but an expired host/link origin still denies.
|
|
48
|
+
if (origin) await requireExternal(origin);
|
|
49
|
+
if (state.externalContinuation) {
|
|
50
|
+
await requireExternal(state.externalContinuation);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (state.subjectId.startsWith("external_user:"))
|
|
54
|
+
throw new HTTPException(403, { message: "External Connect continuation required" });
|
|
55
|
+
if (state.subjectId.startsWith("api_key:")) {
|
|
56
|
+
const authority = await lockConnectionSetupKeyAuthority(db, state);
|
|
57
|
+
if (!authority || !hasPermission(authority.permissions, permission, authority.permissionMode))
|
|
58
|
+
throw new HTTPException(403, { message: "Connection API key authority changed" });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await withWorkspaceSubjectRls(db, state.workspaceId, state.subjectId, async (tx) => {
|
|
62
|
+
await lockExternalWorkspaceMembershipLifecycle(tx, state.accountId);
|
|
63
|
+
const membership = await getWorkspaceGrant(tx, state.subjectId, state.workspaceId);
|
|
64
|
+
const grant =
|
|
65
|
+
membership?.accountId === state.accountId
|
|
66
|
+
? membership
|
|
67
|
+
: state.personalOwnerVerified
|
|
68
|
+
? await resolveNamedManagedPersonalWorkspaceGrant(tx, state)
|
|
69
|
+
: null;
|
|
70
|
+
if (
|
|
71
|
+
!grant ||
|
|
72
|
+
grant.accountId !== state.accountId ||
|
|
73
|
+
!hasPermission(grant.permissions, permission)
|
|
74
|
+
)
|
|
75
|
+
throw new HTTPException(403, { message: "Connection owner authority changed" });
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ConnectAttempt } from "@opengeni/contracts/connect";
|
|
2
|
+
import { assertOrganizationIntegrationAllowed } from "@opengeni/contracts";
|
|
3
|
+
import { withOrganizationIntegrationPolicyFence } from "@opengeni/db/organization-integration-policy";
|
|
4
|
+
import { integrationKeyForConnectProvider } from "../domain/organization-integration-catalog";
|
|
5
|
+
import {
|
|
6
|
+
claimConnectOperation,
|
|
7
|
+
finishConnectOperation,
|
|
8
|
+
type ConnectActorScope,
|
|
9
|
+
type ConnectOperationAuthorization,
|
|
10
|
+
type Database,
|
|
11
|
+
} from "@opengeni/db";
|
|
12
|
+
|
|
13
|
+
/** Provider result is deliberately not serializable: secrets can remain in its
|
|
14
|
+
* closure until encrypted credential persistence. Do not retain/log this value. */
|
|
15
|
+
export type PreparedConnectOperation = {
|
|
16
|
+
commit: (tx: Database, current: ConnectAttempt) => Promise<ConnectAttempt>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Internal provider-adapter boundary, not an HTTP authentication adapter.
|
|
20
|
+
*
|
|
21
|
+
* authorize is mandatory, including for a receipt replay. It must validate the
|
|
22
|
+
* exact saved key/actor/link authority and current permission under database
|
|
23
|
+
* locks. execute owns provider-specific proof, destination and cancellation.
|
|
24
|
+
* No remote effects may occur inside authorize or commit.
|
|
25
|
+
*
|
|
26
|
+
* A failed execute/authorization/commit keeps the durable claim occupied. It
|
|
27
|
+
* does not release or retry work whose provider outcome may be unknown.
|
|
28
|
+
*/
|
|
29
|
+
export async function executeConnectOperation(input: {
|
|
30
|
+
db: Database;
|
|
31
|
+
scope: ConnectActorScope;
|
|
32
|
+
attemptId: string;
|
|
33
|
+
expectedRevision: number;
|
|
34
|
+
operationId: string;
|
|
35
|
+
/** Caller computes a stable keyed digest if input contains low-entropy secrets. */
|
|
36
|
+
inputDigest: string;
|
|
37
|
+
/** Trusted server adapter classification, never derived from request data.
|
|
38
|
+
* Cancellation releases pending setup; it acquires no integration authority. */
|
|
39
|
+
purpose?: "acquisition" | "cancellation";
|
|
40
|
+
authorize: ConnectOperationAuthorization;
|
|
41
|
+
execute: (attempt: ConnectAttempt) => Promise<PreparedConnectOperation>;
|
|
42
|
+
}): Promise<ConnectAttempt> {
|
|
43
|
+
// Snapshot caller-controlled identity before the first asynchronous boundary.
|
|
44
|
+
const { db, authorize, execute, purpose = "acquisition" } = input;
|
|
45
|
+
const scope = { ...input.scope };
|
|
46
|
+
const operation = {
|
|
47
|
+
attemptId: input.attemptId,
|
|
48
|
+
expectedRevision: input.expectedRevision,
|
|
49
|
+
operationId: input.operationId,
|
|
50
|
+
inputDigest: input.inputDigest,
|
|
51
|
+
authorize,
|
|
52
|
+
};
|
|
53
|
+
const claim = await withOrganizationIntegrationPolicyFence(db, scope, async (tx, policy) =>
|
|
54
|
+
claimConnectOperation(tx, scope, {
|
|
55
|
+
...operation,
|
|
56
|
+
authorizeAcquisition: async (_tx, attempt) => {
|
|
57
|
+
if (purpose === "cancellation") return;
|
|
58
|
+
assertOrganizationIntegrationAllowed(
|
|
59
|
+
policy,
|
|
60
|
+
integrationKeyForConnectProvider(attempt.providerId),
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
if (claim.status === "replayed") return claim.attempt;
|
|
66
|
+
const prepared = await execute(structuredClone(claim.attempt));
|
|
67
|
+
return withOrganizationIntegrationPolicyFence(db, scope, async (tx, policy) =>
|
|
68
|
+
finishConnectOperation(tx, scope, {
|
|
69
|
+
...operation,
|
|
70
|
+
commit: prepared.commit,
|
|
71
|
+
authorizeAcquisition: async (_tx, attempt) => {
|
|
72
|
+
if (purpose === "cancellation") return;
|
|
73
|
+
assertOrganizationIntegrationAllowed(
|
|
74
|
+
policy,
|
|
75
|
+
integrationKeyForConnectProvider(attempt.providerId),
|
|
76
|
+
);
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
}
|