@opengeni/api-router 2.11.3 → 2.12.1-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +1 -1
- package/dist/{chunk-5X3XM6CE.js → chunk-Y3EPAIEQ.js} +17643 -14398
- package/dist/chunk-Y3EPAIEQ.js.map +1 -0
- package/dist/connection-ownership.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +4 -0
- package/dist/integrations/connect-authority.d.ts +1 -0
- package/dist/integrations/connect-callback-return.d.ts +6 -0
- package/dist/integrations/fiken.d.ts +7 -0
- package/dist/integrations/github-app-connect.d.ts +47 -0
- package/dist/integrations/github-installation-proof.d.ts +3 -0
- package/dist/integrations/github-lens-connect.d.ts +153 -0
- package/dist/integrations/google-drive.d.ts +4 -0
- package/dist/integrations/oauth-client.d.ts +4 -0
- package/dist/integrations/oauth-return-path.d.ts +1 -0
- package/dist/integrations/personal-github.d.ts +2 -0
- package/dist/integrations/provider-oauth.d.ts +11 -1
- package/dist/integrations/slack-install.d.ts +20 -0
- package/dist/integrations/social-oauth.d.ts +2 -0
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/routes/api-integrations.d.ts +14 -0
- package/dist/routes/connect.d.ts +5 -0
- package/dist/routes/external-identity-links.d.ts +6 -0
- package/dist/routes/host-mcp-bindings.d.ts +5 -0
- package/dist/workspace-tool-gateway.d.ts +1 -0
- package/package.json +19 -19
- package/src/app.ts +22 -2
- package/src/connection-ownership.ts +14 -1
- package/src/index.ts +2 -0
- package/src/integrations/atlassian.ts +143 -39
- package/src/integrations/connect-authority.ts +2 -0
- package/src/integrations/connect-callback-return.ts +86 -0
- package/src/integrations/fiken.ts +222 -40
- package/src/integrations/github-app-connect.ts +389 -0
- package/src/integrations/github-installation-proof.ts +60 -0
- package/src/integrations/github-lens-connect.ts +97 -0
- package/src/integrations/google-drive.ts +152 -47
- package/src/integrations/oauth-client.ts +180 -84
- package/src/integrations/oauth-return-path.ts +18 -0
- package/src/integrations/personal-github.ts +146 -52
- package/src/integrations/provider-oauth.ts +132 -21
- package/src/integrations/slack-install.ts +83 -0
- package/src/integrations/social-oauth.ts +142 -1
- package/src/mcp/documents.ts +1 -1
- package/src/mcp/server.ts +4 -4
- package/src/routes/api-integrations.ts +74 -64
- package/src/routes/connect.ts +1869 -0
- package/src/routes/connections.ts +263 -221
- package/src/routes/documents.ts +2 -2
- package/src/routes/external-identity-links.ts +200 -0
- package/src/routes/github.ts +39 -64
- package/src/routes/host-mcp-bindings.ts +122 -0
- package/src/routes/organization-memberships.ts +23 -0
- package/src/routes/organization-sessions.ts +2 -2
- package/src/routes/personal-github.ts +8 -1
- package/src/routes/pr-review-github.ts +30 -3
- package/src/routes/scheduled-tasks.ts +29 -4
- package/src/routes/sessions.ts +61 -32
- package/src/routes/social.ts +5 -1
- package/src/routes/supergrok.ts +90 -3
- package/src/routes/workspaces.ts +16 -0
- package/src/workspace-tool-gateway.ts +136 -24
- package/dist/chunk-5X3XM6CE.js.map +0 -1
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
loadSocialConnectionCredential,
|
|
16
16
|
updateSocialConnectionCredential,
|
|
17
17
|
upsertSocialOAuthConnection,
|
|
18
|
+
claimConnectOperation,
|
|
19
|
+
finishConnectOperation,
|
|
20
|
+
getConnectAttempt,
|
|
18
21
|
type Database,
|
|
19
22
|
} from "@opengeni/db";
|
|
20
23
|
import { createSignedState, readSignedState } from "@opengeni/github";
|
|
@@ -29,6 +32,7 @@ import {
|
|
|
29
32
|
PERSONAL_OWNER_VERIFIED_STATE_CLAIM,
|
|
30
33
|
} from "../connection-ownership";
|
|
31
34
|
import { ApiHttpError } from "../http/api-error";
|
|
35
|
+
import { requireConnectOwnerAuthority } from "./connect-authority";
|
|
32
36
|
import {
|
|
33
37
|
integrationBaseUrl,
|
|
34
38
|
oauthStateTtlMs,
|
|
@@ -135,6 +139,7 @@ function socialProviderFetch(
|
|
|
135
139
|
}
|
|
136
140
|
|
|
137
141
|
export type SocialOAuthStartContext = {
|
|
142
|
+
connectAttemptId?: string;
|
|
138
143
|
accountId: string;
|
|
139
144
|
workspaceId: string;
|
|
140
145
|
subjectId: string;
|
|
@@ -148,6 +153,7 @@ export type SocialOAuthStartContext = {
|
|
|
148
153
|
};
|
|
149
154
|
|
|
150
155
|
type SocialOAuthStatePayload = {
|
|
156
|
+
connectAttemptId?: string;
|
|
151
157
|
kind: "social_oauth";
|
|
152
158
|
accountId: string;
|
|
153
159
|
workspaceId: string;
|
|
@@ -201,6 +207,7 @@ export async function startSocialOAuth(
|
|
|
201
207
|
const key = requireEnvironmentEncryption(settings);
|
|
202
208
|
const state = createSignedState(requireIntegrationsStateSecret(settings), {
|
|
203
209
|
kind: "social_oauth",
|
|
210
|
+
...(context.connectAttemptId ? { connectAttemptId: context.connectAttemptId } : {}),
|
|
204
211
|
accountId: context.accountId,
|
|
205
212
|
workspaceId: context.workspaceId,
|
|
206
213
|
subjectId: context.subjectId,
|
|
@@ -244,7 +251,7 @@ export async function completeSocialOAuthCallback(
|
|
|
244
251
|
error?: string | undefined;
|
|
245
252
|
requestUrl: string;
|
|
246
253
|
},
|
|
247
|
-
): Promise<{ redirectTo: string }> {
|
|
254
|
+
): Promise<{ redirectTo: string; exactReturn?: boolean }> {
|
|
248
255
|
const { db, settings, observability } = deps;
|
|
249
256
|
let state: SocialOAuthStatePayload | null = null;
|
|
250
257
|
try {
|
|
@@ -255,6 +262,7 @@ export async function completeSocialOAuthCallback(
|
|
|
255
262
|
redirectTo: callbackReturnPath("/integrations", "error", { reason: "state_invalid" }),
|
|
256
263
|
};
|
|
257
264
|
}
|
|
265
|
+
if (state.connectAttemptId) return completeSocialConnect(deps, state, input);
|
|
258
266
|
// Provider denial / missing code: report before burning the single-use
|
|
259
267
|
// nonce so a user who cancelled at the provider can retry the same flow
|
|
260
268
|
// within the state TTL.
|
|
@@ -365,6 +373,136 @@ export async function completeSocialOAuthCallback(
|
|
|
365
373
|
}
|
|
366
374
|
}
|
|
367
375
|
|
|
376
|
+
/** Durable host setup keeps social credentials in their original domain. The
|
|
377
|
+
* stored origin, not the unauthenticated callback, supplies current authority. */
|
|
378
|
+
async function completeSocialConnect(
|
|
379
|
+
deps: SocialOAuthDeps,
|
|
380
|
+
state: SocialOAuthStatePayload,
|
|
381
|
+
input: {
|
|
382
|
+
state?: string | undefined;
|
|
383
|
+
code?: string | undefined;
|
|
384
|
+
error?: string | undefined;
|
|
385
|
+
requestUrl: string;
|
|
386
|
+
},
|
|
387
|
+
): Promise<{ redirectTo: string; exactReturn: true }> {
|
|
388
|
+
const stored = await getConnectAttempt(deps.db, state, state.connectAttemptId!);
|
|
389
|
+
if (stored.attempt.providerId !== state.provider || stored.attempt.ownership !== state.ownership)
|
|
390
|
+
throw new HTTPException(400, { message: "Social connection attempt mismatch" });
|
|
391
|
+
const authorize: import("@opengeni/db").ConnectOperationAuthorization = async (
|
|
392
|
+
tx,
|
|
393
|
+
_attempt,
|
|
394
|
+
origin,
|
|
395
|
+
) => {
|
|
396
|
+
await requireConnectOwnerAuthority(
|
|
397
|
+
tx,
|
|
398
|
+
{ ...state, ...(origin ? { externalContinuation: origin } : {}) },
|
|
399
|
+
state.ownership === "workspace" ? "workspace:admin" : "workspace:read",
|
|
400
|
+
origin,
|
|
401
|
+
);
|
|
402
|
+
};
|
|
403
|
+
const operation = {
|
|
404
|
+
attemptId: state.connectAttemptId!,
|
|
405
|
+
operationId: `social:${state.nonce}`,
|
|
406
|
+
inputDigest: createHash("sha256").update(input.state!).digest("hex"),
|
|
407
|
+
authorize,
|
|
408
|
+
};
|
|
409
|
+
try {
|
|
410
|
+
const claim = await claimConnectOperation(deps.db, state, {
|
|
411
|
+
...operation,
|
|
412
|
+
expectedRevision: stored.attempt.revision,
|
|
413
|
+
});
|
|
414
|
+
if (claim.status === "replayed") return { redirectTo: stored.returnUrl, exactReturn: true };
|
|
415
|
+
if (!personalOwnerStateAccepted(state))
|
|
416
|
+
throw new HTTPException(403, { message: PERSONAL_CONNECTION_PRINCIPAL_MESSAGE });
|
|
417
|
+
if (input.error || !input.code) {
|
|
418
|
+
await finishConnectOperation(deps.db, state, {
|
|
419
|
+
...operation,
|
|
420
|
+
commit: async (_tx, current) => ({
|
|
421
|
+
...current,
|
|
422
|
+
revision: current.revision + 1,
|
|
423
|
+
state: input.error === "access_denied" ? "cancelled" : "failed",
|
|
424
|
+
nextAction: { type: "none" },
|
|
425
|
+
error: {
|
|
426
|
+
code: "authorization_not_completed",
|
|
427
|
+
message: "Authorization was not completed. Start a new attempt.",
|
|
428
|
+
retryable: false,
|
|
429
|
+
},
|
|
430
|
+
}),
|
|
431
|
+
});
|
|
432
|
+
} else {
|
|
433
|
+
const provider = SOCIAL_OAUTH_PROVIDERS[state.provider];
|
|
434
|
+
const key = requireEnvironmentEncryption(deps.settings);
|
|
435
|
+
const verifier = state.encryptedPkceVerifier
|
|
436
|
+
? decryptEnvironmentValue(key, state.encryptedPkceVerifier)
|
|
437
|
+
: null;
|
|
438
|
+
const token = await socialTokenRequest(
|
|
439
|
+
deps,
|
|
440
|
+
provider,
|
|
441
|
+
socialOAuthClientFor(deps.settings, provider.id),
|
|
442
|
+
{
|
|
443
|
+
grant_type: "authorization_code",
|
|
444
|
+
code: input.code,
|
|
445
|
+
redirect_uri: socialOAuthRedirectUri(deps.settings, input.requestUrl),
|
|
446
|
+
...(verifier ? { code_verifier: verifier } : {}),
|
|
447
|
+
},
|
|
448
|
+
);
|
|
449
|
+
const identity = await fetchSocialIdentity(deps, provider.id, token.accessToken);
|
|
450
|
+
const bundle: SocialCredentialBundle = {
|
|
451
|
+
provider: provider.id,
|
|
452
|
+
accessToken: token.accessToken,
|
|
453
|
+
tokenType: token.tokenType,
|
|
454
|
+
...(token.refreshToken ? { refreshToken: token.refreshToken } : {}),
|
|
455
|
+
...(token.expiresAt ? { expiresAt: token.expiresAt } : {}),
|
|
456
|
+
...(token.scope ? { scope: token.scope } : {}),
|
|
457
|
+
};
|
|
458
|
+
await finishConnectOperation(deps.db, state, {
|
|
459
|
+
...operation,
|
|
460
|
+
commit: async (tx, current) => {
|
|
461
|
+
const connection = await upsertSocialOAuthConnection(tx, {
|
|
462
|
+
accountId: state.accountId,
|
|
463
|
+
workspaceId: state.workspaceId,
|
|
464
|
+
...(current.account
|
|
465
|
+
? {
|
|
466
|
+
expectedConnection: {
|
|
467
|
+
id: current.account.id.slice("social:".length),
|
|
468
|
+
version: current.account.version!,
|
|
469
|
+
},
|
|
470
|
+
}
|
|
471
|
+
: {}),
|
|
472
|
+
subjectId: state.ownership === "personal" ? state.subjectId : null,
|
|
473
|
+
provider: provider.id,
|
|
474
|
+
accountHandle: identity.handle,
|
|
475
|
+
accountName: identity.name ?? null,
|
|
476
|
+
externalAccountId: identity.externalAccountId,
|
|
477
|
+
scopes: token.scope ? token.scope.split(/[\s,]+/).filter(Boolean) : state.scopes,
|
|
478
|
+
credentialEncrypted: encryptEnvironmentValue(key, JSON.stringify(bundle)),
|
|
479
|
+
tokenMetadata: publicTokenMetadata(bundle),
|
|
480
|
+
});
|
|
481
|
+
return {
|
|
482
|
+
...current,
|
|
483
|
+
revision: current.revision + 1,
|
|
484
|
+
state: "complete",
|
|
485
|
+
credentialsCommitted: true,
|
|
486
|
+
nextAction: { type: "none" },
|
|
487
|
+
account: {
|
|
488
|
+
id: `social:${connection.id}`,
|
|
489
|
+
providerId: provider.id,
|
|
490
|
+
version: connection.version!,
|
|
491
|
+
label: identity.handle,
|
|
492
|
+
ownership: state.ownership,
|
|
493
|
+
status: "connected",
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
} catch (error) {
|
|
500
|
+
// An uncertain exchange stays occupied: do not replay a consumed OAuth code.
|
|
501
|
+
logSocialOAuthFailure(deps.observability, "connect_callback", state, error);
|
|
502
|
+
}
|
|
503
|
+
return { redirectTo: stored.returnUrl, exactReturn: true };
|
|
504
|
+
}
|
|
505
|
+
|
|
368
506
|
/**
|
|
369
507
|
* Resolves a usable access token for a social connection, refreshing (and
|
|
370
508
|
* persisting the rotated bundle) when the stored token is near expiry. Marks
|
|
@@ -706,6 +844,9 @@ function readSocialOAuthState(
|
|
|
706
844
|
}
|
|
707
845
|
return {
|
|
708
846
|
kind: "social_oauth",
|
|
847
|
+
...(typeof payload.connectAttemptId === "string"
|
|
848
|
+
? { connectAttemptId: payload.connectAttemptId }
|
|
849
|
+
: {}),
|
|
709
850
|
accountId: requiredStateString(payload.accountId, "accountId"),
|
|
710
851
|
workspaceId: requiredStateString(payload.workspaceId, "workspaceId"),
|
|
711
852
|
subjectId: requiredStateString(payload.subjectId, "subjectId"),
|
package/src/mcp/documents.ts
CHANGED
|
@@ -297,7 +297,7 @@ export function buildDocumentsMcpServer(
|
|
|
297
297
|
if (memoryScope?.mode === "off") return server;
|
|
298
298
|
const memoryReadScope: MemoryAgentScope = memoryScope ?? {
|
|
299
299
|
mode: "workspace",
|
|
300
|
-
|
|
300
|
+
userSubjectId: null,
|
|
301
301
|
rootSessionId: null,
|
|
302
302
|
};
|
|
303
303
|
|
package/src/mcp/server.ts
CHANGED
|
@@ -3613,7 +3613,7 @@ function registerMemoryTools(
|
|
|
3613
3613
|
// session's narrowest layer. A null scope is the pre-0426 workspace path.
|
|
3614
3614
|
const agentScope: MemoryAgentScope = sessionMemory ?? {
|
|
3615
3615
|
mode: "workspace",
|
|
3616
|
-
|
|
3616
|
+
userSubjectId: null,
|
|
3617
3617
|
rootSessionId: null,
|
|
3618
3618
|
};
|
|
3619
3619
|
const writeScope = memoryWriteScopeForAgentScope(agentScope);
|
|
@@ -5157,12 +5157,12 @@ function registerWorkspaceOrchestrationTools(
|
|
|
5157
5157
|
),
|
|
5158
5158
|
// The child's agent-access scope and end-user label are never model
|
|
5159
5159
|
// choices: it inherits this session's exactly. Only the Memory
|
|
5160
|
-
// selector may be narrowed here (workspace > user >
|
|
5160
|
+
// selector may be narrowed here (workspace > user > off).
|
|
5161
5161
|
memoryScope: z4
|
|
5162
|
-
.enum(["workspace", "user", "
|
|
5162
|
+
.enum(["workspace", "user", "off"])
|
|
5163
5163
|
.optional()
|
|
5164
5164
|
.describe(
|
|
5165
|
-
"Optional Memory selector for the child. Omit to inherit this session's selector. An explicit value may only narrow it (workspace > user >
|
|
5165
|
+
"Optional Memory selector for the child. Omit to inherit this session's selector. An explicit value may only narrow it (workspace > user > off); off gives the child no Memory tools. Use task notes for task-local findings.",
|
|
5166
5166
|
),
|
|
5167
5167
|
// Omission is the ordinary safe sharing path. Literal "shared" remains
|
|
5168
5168
|
// available to advanced REST/SDK callers but is intentionally absent
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
PreviewApiIntegrationRequest,
|
|
16
16
|
UninstallApiIntegrationRequest,
|
|
17
17
|
UninstallApiIntegrationResult,
|
|
18
|
+
type AccessGrant,
|
|
18
19
|
} from "@opengeni/contracts";
|
|
19
20
|
import { requireAccessGrant, type ApiRouteDeps } from "@opengeni/core";
|
|
20
21
|
import {
|
|
@@ -37,6 +38,7 @@ import { HTTPException } from "hono/http-exception";
|
|
|
37
38
|
import {
|
|
38
39
|
resolveApiIntegrationPreview,
|
|
39
40
|
type ApiIntegrationConnectionDescriptor,
|
|
41
|
+
type ResolvedApiIntegrationPreview,
|
|
40
42
|
} from "../integrations/api-integrations";
|
|
41
43
|
|
|
42
44
|
export type ApiIntegrationRouteOverrides = Readonly<{ fetchImpl?: FetchLike }>;
|
|
@@ -46,6 +48,72 @@ export function apiIntegrationRequiresConnection(authScheme: Record<string, unkn
|
|
|
46
48
|
return typeof authKind === "string" && authKind !== "none";
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
/** Shared native/embedded install validation. The caller must still authorize
|
|
52
|
+
* capabilities:manage and commit under its own live-authority boundary. */
|
|
53
|
+
export function validatedIntegrationInstallInput(
|
|
54
|
+
grant: AccessGrant,
|
|
55
|
+
workspaceId: string,
|
|
56
|
+
payload: InstallApiIntegrationRequest,
|
|
57
|
+
resolved: ResolvedApiIntegrationPreview,
|
|
58
|
+
): Parameters<typeof installApiIntegration>[1] {
|
|
59
|
+
if (
|
|
60
|
+
resolved.preview.revisionId !== payload.expectedRevisionId ||
|
|
61
|
+
resolved.preview.contentSha256 !== payload.expectedContentSha256
|
|
62
|
+
) {
|
|
63
|
+
throw new HTTPException(409, {
|
|
64
|
+
message:
|
|
65
|
+
"The Integration source changed after preview. Review the new tools and permissions before installing.",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (resolved.preview.auth.kind !== "none" && !payload.connectionId)
|
|
69
|
+
throw new HTTPException(422, {
|
|
70
|
+
message: "Connect an account before installing this Integration.",
|
|
71
|
+
});
|
|
72
|
+
if (
|
|
73
|
+
payload.ownership &&
|
|
74
|
+
resolved.preview.connectionOwnership &&
|
|
75
|
+
payload.ownership !== resolved.preview.connectionOwnership
|
|
76
|
+
)
|
|
77
|
+
throw new HTTPException(422, {
|
|
78
|
+
message: "The selected Connection ownership does not match this install request.",
|
|
79
|
+
});
|
|
80
|
+
if (payload.ownership === "personal" && !resolved.preview.connectionOwnership)
|
|
81
|
+
throw new HTTPException(422, {
|
|
82
|
+
message: "Choose a Personal Connection before installing for yourself.",
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
accountId: grant.accountId,
|
|
86
|
+
workspaceId,
|
|
87
|
+
subjectId: grant.subjectId,
|
|
88
|
+
capabilityId: resolved.preview.capabilityId,
|
|
89
|
+
pluginKey: resolved.preview.pluginKey,
|
|
90
|
+
serverId: resolved.preview.serverId,
|
|
91
|
+
name: resolved.preview.name,
|
|
92
|
+
description: resolved.preview.description,
|
|
93
|
+
category: "integrations",
|
|
94
|
+
tags: [resolved.preview.protocol, resolved.preview.provider ?? "custom"],
|
|
95
|
+
definitionId: resolved.preview.definitionId,
|
|
96
|
+
definitionProvenance: resolved.preview.definitionProvenance,
|
|
97
|
+
...(resolved.provider ? { provider: resolved.provider } : {}),
|
|
98
|
+
providerDomain: resolved.preview.providerDomain,
|
|
99
|
+
protocol: resolved.preview.protocol,
|
|
100
|
+
baseUrl: resolved.preview.baseUrl,
|
|
101
|
+
sourceUrl: resolved.preview.sourceUrl,
|
|
102
|
+
authScheme: resolved.authScheme,
|
|
103
|
+
...(payload.connectionId ? { connectionId: payload.connectionId } : {}),
|
|
104
|
+
...(payload.instanceKey ? { instanceKey: payload.instanceKey } : {}),
|
|
105
|
+
...(payload.displayName ? { displayName: payload.displayName } : {}),
|
|
106
|
+
...(payload.expectedInstanceVersion !== undefined
|
|
107
|
+
? { expectedInstanceVersion: payload.expectedInstanceVersion }
|
|
108
|
+
: {}),
|
|
109
|
+
requiredScopes: resolved.requiredScopes,
|
|
110
|
+
ownership: resolved.preview.connectionOwnership === "personal" ? "subject" : "workspace",
|
|
111
|
+
...(payload.allowedTools ? { allowedTools: payload.allowedTools } : {}),
|
|
112
|
+
facetDefinitions: integrationFacetDefinitions(resolved.preview.definitionId),
|
|
113
|
+
revision: resolved.revision,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
49
117
|
export function registerApiIntegrationRoutes(
|
|
50
118
|
app: Hono,
|
|
51
119
|
deps: ApiRouteDeps,
|
|
@@ -53,7 +121,9 @@ export function registerApiIntegrationRoutes(
|
|
|
53
121
|
): void {
|
|
54
122
|
const transport = createPinnedIntegrationTransport({
|
|
55
123
|
network: deps.settings,
|
|
56
|
-
...(overrides.fetchImpl
|
|
124
|
+
...((overrides.fetchImpl ?? deps.apiIntegrationSourceFetch)
|
|
125
|
+
? { fetchImpl: overrides.fetchImpl ?? deps.apiIntegrationSourceFetch! }
|
|
126
|
+
: {}),
|
|
57
127
|
});
|
|
58
128
|
|
|
59
129
|
app.get("/v1/workspaces/:workspaceId/integrations/definitions", async (c) => {
|
|
@@ -157,70 +227,10 @@ export function registerApiIntegrationRoutes(
|
|
|
157
227
|
subjectId: grant.subjectId,
|
|
158
228
|
payload,
|
|
159
229
|
});
|
|
160
|
-
|
|
161
|
-
resolved.preview.revisionId !== payload.expectedRevisionId ||
|
|
162
|
-
resolved.preview.contentSha256 !== payload.expectedContentSha256
|
|
163
|
-
) {
|
|
164
|
-
throw new HTTPException(409, {
|
|
165
|
-
message:
|
|
166
|
-
"The Integration source changed after preview. Review the new tools and permissions before installing.",
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
if (resolved.preview.auth.kind !== "none" && !payload.connectionId) {
|
|
170
|
-
throw new HTTPException(422, {
|
|
171
|
-
message: "Connect an account before installing this Integration.",
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
if (
|
|
175
|
-
payload.ownership &&
|
|
176
|
-
resolved.preview.connectionOwnership &&
|
|
177
|
-
payload.ownership !== resolved.preview.connectionOwnership
|
|
178
|
-
) {
|
|
179
|
-
throw new HTTPException(422, {
|
|
180
|
-
message: "The selected Connection ownership does not match this install request.",
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
if (payload.ownership === "personal" && !resolved.preview.connectionOwnership) {
|
|
184
|
-
throw new HTTPException(422, {
|
|
185
|
-
message: "Choose a Personal Connection before installing for yourself.",
|
|
186
|
-
});
|
|
187
|
-
}
|
|
230
|
+
const installation = validatedIntegrationInstallInput(grant, workspaceId, payload, resolved);
|
|
188
231
|
try {
|
|
189
232
|
return c.json(
|
|
190
|
-
InstalledApiIntegration.parse(
|
|
191
|
-
await installApiIntegration(deps.db, {
|
|
192
|
-
accountId: grant.accountId,
|
|
193
|
-
workspaceId,
|
|
194
|
-
subjectId: grant.subjectId,
|
|
195
|
-
capabilityId: resolved.preview.capabilityId,
|
|
196
|
-
pluginKey: resolved.preview.pluginKey,
|
|
197
|
-
serverId: resolved.preview.serverId,
|
|
198
|
-
name: resolved.preview.name,
|
|
199
|
-
description: resolved.preview.description,
|
|
200
|
-
category: "integrations",
|
|
201
|
-
tags: [resolved.preview.protocol, resolved.preview.provider ?? "custom"],
|
|
202
|
-
definitionId: resolved.preview.definitionId,
|
|
203
|
-
definitionProvenance: resolved.preview.definitionProvenance,
|
|
204
|
-
...(resolved.provider ? { provider: resolved.provider } : {}),
|
|
205
|
-
providerDomain: resolved.preview.providerDomain,
|
|
206
|
-
protocol: resolved.preview.protocol,
|
|
207
|
-
baseUrl: resolved.preview.baseUrl,
|
|
208
|
-
sourceUrl: resolved.preview.sourceUrl,
|
|
209
|
-
authScheme: resolved.authScheme,
|
|
210
|
-
...(payload.connectionId ? { connectionId: payload.connectionId } : {}),
|
|
211
|
-
...(payload.instanceKey ? { instanceKey: payload.instanceKey } : {}),
|
|
212
|
-
...(payload.displayName ? { displayName: payload.displayName } : {}),
|
|
213
|
-
...(payload.expectedInstanceVersion !== undefined
|
|
214
|
-
? { expectedInstanceVersion: payload.expectedInstanceVersion }
|
|
215
|
-
: {}),
|
|
216
|
-
requiredScopes: resolved.requiredScopes,
|
|
217
|
-
ownership:
|
|
218
|
-
resolved.preview.connectionOwnership === "personal" ? "subject" : "workspace",
|
|
219
|
-
...(payload.allowedTools ? { allowedTools: payload.allowedTools } : {}),
|
|
220
|
-
facetDefinitions: integrationFacetDefinitions(resolved.preview.definitionId),
|
|
221
|
-
revision: resolved.revision,
|
|
222
|
-
}),
|
|
223
|
-
),
|
|
233
|
+
InstalledApiIntegration.parse(await installApiIntegration(deps.db, installation)),
|
|
224
234
|
payload.expectedInstanceVersion === undefined ? 201 : 200,
|
|
225
235
|
);
|
|
226
236
|
} catch (error) {
|
|
@@ -295,7 +305,7 @@ export function registerApiIntegrationRoutes(
|
|
|
295
305
|
);
|
|
296
306
|
}
|
|
297
307
|
|
|
298
|
-
async function resolveForRoute(input: {
|
|
308
|
+
export async function resolveForRoute(input: {
|
|
299
309
|
deps: ApiRouteDeps;
|
|
300
310
|
transport: ReturnType<typeof createPinnedIntegrationTransport>;
|
|
301
311
|
workspaceId: string;
|