@opengeni/api-router 2.11.3 → 2.12.3-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-4AQMKVQM.js} +17727 -14427
- package/dist/chunk-4AQMKVQM.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/sandbox/rematerialize.d.ts +1 -1
- 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 +64 -17
- 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/sandbox/rematerialize.ts +33 -1
- 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
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
} from "../site-uploads";
|
|
16
16
|
import {
|
|
17
17
|
CreateScheduledTaskRequest,
|
|
18
|
+
CreateSessionRequest,
|
|
19
|
+
GoalSpec,
|
|
18
20
|
boundSessionMcpText as capSessionDiscoveryText,
|
|
19
21
|
compactSessionMcpListRow,
|
|
20
22
|
sessionMcpIncludesRelatedWork,
|
|
@@ -376,6 +378,45 @@ function orchestrationFailureCode(tool: OrchestrationToolName, error: HTTPExcept
|
|
|
376
378
|
return `${tool}_${suffix}`.slice(0, ORCHESTRATION_FAILURE_CODE_MAX_LENGTH);
|
|
377
379
|
}
|
|
378
380
|
|
|
381
|
+
function sessionCreateValidationFailureResult(error: z4.ZodError) {
|
|
382
|
+
// Only call with issues from the raw request preflight, never downstream errors.
|
|
383
|
+
// Paths may contain caller-controlled record keys, and custom issue messages
|
|
384
|
+
// may contain values. Only publish canonical field names and fixed type labels.
|
|
385
|
+
const details = error.issues.slice(0, 5).map((issue) => {
|
|
386
|
+
const [field, goalField] = issue.path;
|
|
387
|
+
let path =
|
|
388
|
+
typeof field === "string" && Object.hasOwn(CreateSessionRequest.out.shape, field)
|
|
389
|
+
? field
|
|
390
|
+
: "request";
|
|
391
|
+
if (
|
|
392
|
+
field === "goal" &&
|
|
393
|
+
typeof goalField === "string" &&
|
|
394
|
+
Object.hasOwn(GoalSpec.shape, goalField)
|
|
395
|
+
) {
|
|
396
|
+
path += `.${goalField}`;
|
|
397
|
+
}
|
|
398
|
+
const expected =
|
|
399
|
+
issue.code === "invalid_type" &&
|
|
400
|
+
["string", "number", "boolean", "object", "array", "int"].includes(issue.expected)
|
|
401
|
+
? `expected ${issue.expected === "int" ? "integer" : issue.expected}`
|
|
402
|
+
: "failed schema validation";
|
|
403
|
+
return `${path} ${expected}`;
|
|
404
|
+
});
|
|
405
|
+
const envelope = {
|
|
406
|
+
error: {
|
|
407
|
+
code: "session_create_invalid_request",
|
|
408
|
+
message: boundedOrchestrationFailureMessage(
|
|
409
|
+
`Invalid session create request: ${details.join("; ") || "request failed schema validation"}${error.issues.length > 5 ? "; additional fields failed validation" : ""}.`,
|
|
410
|
+
),
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
return {
|
|
414
|
+
content: [{ type: "text" as const, text: JSON.stringify(envelope, null, 2) }],
|
|
415
|
+
structuredContent: envelope,
|
|
416
|
+
isError: true as const,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
379
420
|
function orchestrationFailureEnvelope(tool: OrchestrationToolName, error: unknown) {
|
|
380
421
|
if (error instanceof SessionSpawnDeniedError) {
|
|
381
422
|
const denial = sessionSpawnDenialEnvelope(error);
|
|
@@ -3613,7 +3654,7 @@ function registerMemoryTools(
|
|
|
3613
3654
|
// session's narrowest layer. A null scope is the pre-0426 workspace path.
|
|
3614
3655
|
const agentScope: MemoryAgentScope = sessionMemory ?? {
|
|
3615
3656
|
mode: "workspace",
|
|
3616
|
-
|
|
3657
|
+
userSubjectId: null,
|
|
3617
3658
|
rootSessionId: null,
|
|
3618
3659
|
};
|
|
3619
3660
|
const writeScope = memoryWriteScopeForAgentScope(agentScope);
|
|
@@ -5102,7 +5143,7 @@ function registerWorkspaceOrchestrationTools(
|
|
|
5102
5143
|
"Concise semantic title for the child session. Omit only when the delegated goal or initial message already provides a suitable title; OpenGeni derives a sensitive-safe bounded fallback from that text.",
|
|
5103
5144
|
),
|
|
5104
5145
|
instructions: z4.string().min(1).max(SESSION_INSTRUCTIONS_MAX_CHARACTERS).optional(),
|
|
5105
|
-
goal:
|
|
5146
|
+
goal: GoalSpec.optional(),
|
|
5106
5147
|
resources: z4
|
|
5107
5148
|
.array(z4.unknown())
|
|
5108
5149
|
.optional()
|
|
@@ -5157,12 +5198,12 @@ function registerWorkspaceOrchestrationTools(
|
|
|
5157
5198
|
),
|
|
5158
5199
|
// The child's agent-access scope and end-user label are never model
|
|
5159
5200
|
// choices: it inherits this session's exactly. Only the Memory
|
|
5160
|
-
// selector may be narrowed here (workspace > user >
|
|
5201
|
+
// selector may be narrowed here (workspace > user > off).
|
|
5161
5202
|
memoryScope: z4
|
|
5162
|
-
.enum(["workspace", "user", "
|
|
5203
|
+
.enum(["workspace", "user", "off"])
|
|
5163
5204
|
.optional()
|
|
5164
5205
|
.describe(
|
|
5165
|
-
"Optional Memory selector for the child. Omit to inherit this session's selector. An explicit value may only narrow it (workspace > user >
|
|
5206
|
+
"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
5207
|
),
|
|
5167
5208
|
// Omission is the ordinary safe sharing path. Literal "shared" remains
|
|
5168
5209
|
// available to advanced REST/SDK callers but is intentionally absent
|
|
@@ -5211,22 +5252,28 @@ function registerWorkspaceOrchestrationTools(
|
|
|
5211
5252
|
await authorizeFirstPartySession(deps, grant, callerSessionId, "session.child.create");
|
|
5212
5253
|
}
|
|
5213
5254
|
const { machineTarget, title, projectId, ...request } = args;
|
|
5255
|
+
const rawRequest = {
|
|
5256
|
+
...request,
|
|
5257
|
+
...(projectId !== undefined ? { channelId: projectId } : {}),
|
|
5258
|
+
...(machineTarget
|
|
5259
|
+
? {
|
|
5260
|
+
targetSandboxId: machineTarget.targetSandboxId,
|
|
5261
|
+
...(machineTarget.workingDir !== undefined
|
|
5262
|
+
? { workingDir: machineTarget.workingDir }
|
|
5263
|
+
: {}),
|
|
5264
|
+
}
|
|
5265
|
+
: {}),
|
|
5266
|
+
};
|
|
5267
|
+
// Keep authorization above validation and let core parse the original
|
|
5268
|
+
// request. Only this preflight's failures are attributable to input;
|
|
5269
|
+
// arbitrary downstream schema failures retain the private fallback.
|
|
5270
|
+
const validation = CreateSessionRequest.safeParse(rawRequest);
|
|
5271
|
+
if (!validation.success) return sessionCreateValidationFailureResult(validation.error);
|
|
5214
5272
|
const result = await createSessionForRequestWithOutcome(
|
|
5215
5273
|
deps,
|
|
5216
5274
|
grant,
|
|
5217
5275
|
grant.workspaceId,
|
|
5218
|
-
|
|
5219
|
-
...request,
|
|
5220
|
-
...(projectId !== undefined ? { channelId: projectId } : {}),
|
|
5221
|
-
...(machineTarget
|
|
5222
|
-
? {
|
|
5223
|
-
targetSandboxId: machineTarget.targetSandboxId,
|
|
5224
|
-
...(machineTarget.workingDir !== undefined
|
|
5225
|
-
? { workingDir: machineTarget.workingDir }
|
|
5226
|
-
: {}),
|
|
5227
|
-
}
|
|
5228
|
-
: {}),
|
|
5229
|
-
},
|
|
5276
|
+
rawRequest,
|
|
5230
5277
|
undefined,
|
|
5231
5278
|
title === undefined ? {} : { automaticTitleCandidate: title },
|
|
5232
5279
|
);
|
|
@@ -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;
|