@opengeni/api-router 2.5.0 → 2.6.4-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.d.ts +2 -1
- package/dist/app.js +3 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +5 -1
- package/dist/auth/managed-auth.d.ts +24 -1
- package/dist/{chunk-QESX7HDK.js → chunk-UOI7KAP3.js} +3240 -791
- package/dist/chunk-UOI7KAP3.js.map +1 -0
- package/dist/http/sse.d.ts +9 -0
- package/dist/index.js +117 -5
- package/dist/index.js.map +1 -1
- package/dist/interaction-metrics.d.ts +2 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +1 -1
- package/dist/mcp/company-profile-agent-admin.d.ts +6 -6
- package/dist/mcp/remember.d.ts +2 -2
- package/dist/mcp/scheduled-task-view.d.ts +3 -3
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/session-view.d.ts +1 -0
- package/dist/mcp/session-wait.d.ts +19 -0
- package/dist/model-catalog.d.ts +5 -31
- package/dist/routes/api-keys.d.ts +2 -0
- package/dist/routes/browser-sessions.d.ts +1 -0
- package/dist/routes/computer-sessions.d.ts +12 -0
- package/dist/routes/managed-auth-session-sets.d.ts +7 -0
- package/dist/routes/workspaces.d.ts +1 -1
- package/dist/sandbox/metrics-ingestion.d.ts +16 -0
- package/dist/workspace-delete-observability.d.ts +10 -0
- package/package.json +19 -19
- package/src/app.ts +206 -29
- package/src/auth/managed-auth-attempt-context.ts +40 -3
- package/src/auth/managed-auth-session-adapter.ts +1 -0
- package/src/auth/managed-auth.ts +164 -4
- package/src/http/sse.ts +279 -45
- package/src/index.ts +13 -1
- package/src/integrations/oauth-client.ts +8 -1
- package/src/integrations/provider-oauth.ts +12 -2
- package/src/integrations/slack-interactions.ts +77 -37
- package/src/interaction-metrics.ts +30 -0
- package/src/mcp/company-brain-governed-writes.ts +38 -23
- package/src/mcp/company-profile-agent-admin.ts +7 -7
- package/src/mcp/remember.ts +19 -8
- package/src/mcp/server.ts +328 -27
- package/src/mcp/session-wait.ts +56 -8
- package/src/model-catalog.ts +45 -337
- package/src/routes/api-integrations.ts +2 -2
- package/src/routes/api-keys.ts +149 -7
- package/src/routes/automations.ts +186 -19
- package/src/routes/browser-sessions.ts +10 -2
- package/src/routes/capabilities.ts +3 -3
- package/src/routes/codex.ts +483 -74
- package/src/routes/company-profile.ts +64 -0
- package/src/routes/computer-sessions.ts +103 -1
- package/src/routes/connections.ts +271 -16
- package/src/routes/github.ts +0 -8
- package/src/routes/integration-facets.ts +8 -5
- package/src/routes/interaction-resources.ts +7 -1
- package/src/routes/managed-auth-session-sets.ts +199 -2
- package/src/routes/organization-memberships.ts +28 -8
- package/src/routes/packs.ts +5 -5
- package/src/routes/plugins.ts +2 -2
- package/src/routes/pr-review.ts +80 -4
- package/src/routes/scheduled-tasks.ts +47 -11
- package/src/routes/sessions.ts +75 -53
- package/src/routes/skills.ts +3 -3
- package/src/routes/supergrok.ts +3 -2
- package/src/routes/workspaces.ts +665 -56
- package/src/sandbox/channel-a.ts +10 -4
- package/src/sandbox/machines.ts +13 -6
- package/src/sandbox/metrics-ingestion.ts +157 -3
- package/src/sandbox/viewer.ts +20 -1
- package/src/workspace-delete-observability.ts +75 -0
- package/dist/chunk-QESX7HDK.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import {
|
|
3
3
|
ActivateCompanyProfileRevisionRequest,
|
|
4
|
+
CompanyProfileAgentPolicy,
|
|
4
5
|
CompanyProfileConflictResponse,
|
|
5
6
|
CompanyProfileDiffRequest,
|
|
6
7
|
CompanyProfileDiffResponse,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
CompanyProfileRevision,
|
|
12
13
|
RollbackCompanyProfileRequest,
|
|
13
14
|
UpdateCompanyProfileRequest,
|
|
15
|
+
UpdateCompanyProfileAgentPolicyRequest,
|
|
14
16
|
} from "@opengeni/contracts";
|
|
15
17
|
import {
|
|
16
18
|
requireAccessGrant,
|
|
@@ -20,15 +22,18 @@ import {
|
|
|
20
22
|
} from "@opengeni/core";
|
|
21
23
|
import {
|
|
22
24
|
activateCompanyProfileRevision,
|
|
25
|
+
CompanyProfileAgentPolicyError,
|
|
23
26
|
CompanyProfileConflictError,
|
|
24
27
|
CompanyProfileInvalidOperationError,
|
|
25
28
|
CompanyProfileNotFoundError,
|
|
26
29
|
CompanyProfileOperationReuseError,
|
|
27
30
|
diffCompanyProfileRevisions,
|
|
28
31
|
getCompanyProfileRevision,
|
|
32
|
+
getCompanyProfileAgentPolicy,
|
|
29
33
|
listCompanyProfile,
|
|
30
34
|
rollbackCompanyProfileRevision,
|
|
31
35
|
updateCompanyProfile,
|
|
36
|
+
updateCompanyProfileAgentPolicy,
|
|
32
37
|
} from "@opengeni/db";
|
|
33
38
|
import type { Context, Hono } from "hono";
|
|
34
39
|
import { HTTPException } from "hono/http-exception";
|
|
@@ -84,6 +89,15 @@ function profileError(context: Context, error: unknown): Response {
|
|
|
84
89
|
if (error instanceof CompanyProfileInvalidOperationError) {
|
|
85
90
|
return context.json({ code: "INVALID_COMPANY_PROFILE_OPERATION", message: error.message }, 422);
|
|
86
91
|
}
|
|
92
|
+
if (error instanceof CompanyProfileAgentPolicyError) {
|
|
93
|
+
if (error.code === "authority_unavailable") {
|
|
94
|
+
throw new HTTPException(403, { message: error.message });
|
|
95
|
+
}
|
|
96
|
+
if (error.code === "policy_conflict" || error.code === "operation_reused") {
|
|
97
|
+
throw new HTTPException(409, { message: error.message });
|
|
98
|
+
}
|
|
99
|
+
throw new HTTPException(422, { message: error.message });
|
|
100
|
+
}
|
|
87
101
|
throw error;
|
|
88
102
|
}
|
|
89
103
|
|
|
@@ -97,6 +111,56 @@ function revisionId(context: Context): string {
|
|
|
97
111
|
export function registerCompanyProfileRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
98
112
|
const base = "/v1/workspaces/:workspaceId/company-profile";
|
|
99
113
|
|
|
114
|
+
app.get(`${base}/agent-policy`, async (context) => {
|
|
115
|
+
const workspaceId = context.req.param("workspaceId");
|
|
116
|
+
const access = await requireAccessGrantAuthorization(
|
|
117
|
+
context,
|
|
118
|
+
deps,
|
|
119
|
+
workspaceId,
|
|
120
|
+
"workspace:read",
|
|
121
|
+
);
|
|
122
|
+
requireDirectAccountAdmin(access);
|
|
123
|
+
try {
|
|
124
|
+
return context.json(
|
|
125
|
+
CompanyProfileAgentPolicy.parse(
|
|
126
|
+
await getCompanyProfileAgentPolicy(deps.db, {
|
|
127
|
+
accountId: access.grant.accountId,
|
|
128
|
+
workspaceId,
|
|
129
|
+
actorSubjectId: access.grant.subjectId,
|
|
130
|
+
}),
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return profileError(context, error);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
app.patch(`${base}/agent-policy`, async (context) => {
|
|
139
|
+
const workspaceId = context.req.param("workspaceId");
|
|
140
|
+
const access = await requireAccessGrantAuthorization(
|
|
141
|
+
context,
|
|
142
|
+
deps,
|
|
143
|
+
workspaceId,
|
|
144
|
+
"workspace:read",
|
|
145
|
+
);
|
|
146
|
+
requireDirectAccountAdmin(access);
|
|
147
|
+
const request = await parseBody(context, UpdateCompanyProfileAgentPolicyRequest);
|
|
148
|
+
try {
|
|
149
|
+
return context.json(
|
|
150
|
+
CompanyProfileAgentPolicy.parse(
|
|
151
|
+
await updateCompanyProfileAgentPolicy(deps.db, {
|
|
152
|
+
accountId: access.grant.accountId,
|
|
153
|
+
workspaceId,
|
|
154
|
+
actorSubjectId: access.grant.subjectId,
|
|
155
|
+
...request,
|
|
156
|
+
}),
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return profileError(context, error);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
100
164
|
app.get(base, async (context) => {
|
|
101
165
|
const workspaceId = context.req.param("workspaceId");
|
|
102
166
|
const grant = await requireAccessGrant(context, deps, workspaceId, "workspace:read");
|
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
BrowserControlServerUnsupportedError,
|
|
69
69
|
BrowserControlTransportError,
|
|
70
70
|
BrowserControlUnsupportedError,
|
|
71
|
+
ComputerFrameEvidenceMismatchError,
|
|
71
72
|
buildSelfhostedBackendSession,
|
|
72
73
|
buildStreamUrl,
|
|
73
74
|
exposedPortEndpointFromUrl,
|
|
@@ -75,8 +76,11 @@ import {
|
|
|
75
76
|
NatsControlRpc,
|
|
76
77
|
NatsOpStreamTransport,
|
|
77
78
|
provisionBrowserControlClient,
|
|
79
|
+
validateComputerControlFrameEvidence,
|
|
78
80
|
renewSandboxProviderExpiration,
|
|
79
81
|
type BrowserControlPlacementSession,
|
|
82
|
+
type ComputerControlFrame,
|
|
83
|
+
type ExpectedComputerFrameEvidence,
|
|
80
84
|
} from "@opengeni/runtime/sandbox";
|
|
81
85
|
import type { Context, Hono } from "hono";
|
|
82
86
|
import { HTTPException } from "hono/http-exception";
|
|
@@ -101,7 +105,11 @@ import {
|
|
|
101
105
|
createInteractionFrameProxyAttachment,
|
|
102
106
|
placementUsesInteractionFrameProxy,
|
|
103
107
|
} from "../interaction-frame-proxy";
|
|
104
|
-
import {
|
|
108
|
+
import {
|
|
109
|
+
observeComputerActionResult,
|
|
110
|
+
observeComputerFrameEvidenceMismatch,
|
|
111
|
+
observeLifecycleResult,
|
|
112
|
+
} from "../interaction-metrics";
|
|
105
113
|
import { withChannelA, withChannelARead, type ChannelAOperation } from "../sandbox/channel-a";
|
|
106
114
|
|
|
107
115
|
type ComputerPlacement = {
|
|
@@ -111,6 +119,59 @@ type ComputerPlacement = {
|
|
|
111
119
|
lease: LeaseSnapshot | null;
|
|
112
120
|
};
|
|
113
121
|
|
|
122
|
+
const MODEL_COMPUTER_FRAME_MAX_BYTES = 256 * 1024;
|
|
123
|
+
|
|
124
|
+
type ComputerFrameCaptureClient = {
|
|
125
|
+
capture(
|
|
126
|
+
targetId: string,
|
|
127
|
+
options: {
|
|
128
|
+
format?: "jpeg" | "png";
|
|
129
|
+
quality?: number;
|
|
130
|
+
maxWidth?: number;
|
|
131
|
+
maxHeight?: number;
|
|
132
|
+
},
|
|
133
|
+
): Promise<ComputerControlFrame>;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export function validateComputerFrameForApi(
|
|
137
|
+
frame: Pick<ComputerControlFrame, "data" | "mediaType" | "metadataHeader">,
|
|
138
|
+
expected: ExpectedComputerFrameEvidence,
|
|
139
|
+
): ComputerControlFrame {
|
|
140
|
+
return validateComputerControlFrameEvidence(frame, expected);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function captureModelComputerFrame(
|
|
144
|
+
sessionClient: ComputerFrameCaptureClient,
|
|
145
|
+
expected: ExpectedComputerFrameEvidence,
|
|
146
|
+
): Promise<ComputerControlFrame> {
|
|
147
|
+
let captured = validateComputerFrameForApi(
|
|
148
|
+
await sessionClient.capture(expected.targetId, {
|
|
149
|
+
format: "jpeg",
|
|
150
|
+
quality: 55,
|
|
151
|
+
maxWidth: 1_024,
|
|
152
|
+
maxHeight: 768,
|
|
153
|
+
}),
|
|
154
|
+
expected,
|
|
155
|
+
);
|
|
156
|
+
if (captured.data.byteLength > MODEL_COMPUTER_FRAME_MAX_BYTES) {
|
|
157
|
+
captured = validateComputerFrameForApi(
|
|
158
|
+
await sessionClient.capture(expected.targetId, {
|
|
159
|
+
format: "jpeg",
|
|
160
|
+
quality: 30,
|
|
161
|
+
maxWidth: 640,
|
|
162
|
+
maxHeight: 480,
|
|
163
|
+
}),
|
|
164
|
+
expected,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (captured.data.byteLength > MODEL_COMPUTER_FRAME_MAX_BYTES) {
|
|
168
|
+
throw new BrowserControlProtocolError(
|
|
169
|
+
"computer screenshot could not honor the model image byte bound",
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
return captured;
|
|
173
|
+
}
|
|
174
|
+
|
|
114
175
|
/** Public ComputerSession resource surface. Physical app/window authority stays
|
|
115
176
|
* in the same placement controller used by BrowserSession; this route owns only
|
|
116
177
|
* durable authorization, placement fencing, lifecycle receipts, and routing. */
|
|
@@ -389,6 +450,47 @@ export function registerComputerSessionRoutes(app: Hono, deps: ApiRouteDeps): vo
|
|
|
389
450
|
},
|
|
390
451
|
);
|
|
391
452
|
|
|
453
|
+
app.get(
|
|
454
|
+
"/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets/:targetId/screenshot",
|
|
455
|
+
async (context) => {
|
|
456
|
+
const { workspaceId, grant, computerSessionId } = await routePreamble(
|
|
457
|
+
context,
|
|
458
|
+
"sessions:read",
|
|
459
|
+
);
|
|
460
|
+
const targetId = requireOpaqueParam(context, "targetId");
|
|
461
|
+
const frame = await withActiveComputerController(
|
|
462
|
+
context,
|
|
463
|
+
grant,
|
|
464
|
+
workspaceId,
|
|
465
|
+
computerSessionId,
|
|
466
|
+
"session.read",
|
|
467
|
+
"computer.read",
|
|
468
|
+
async ({ sessionClient, binding }) => {
|
|
469
|
+
try {
|
|
470
|
+
return await captureModelComputerFrame(sessionClient, {
|
|
471
|
+
computerSessionId,
|
|
472
|
+
controllerGeneration: binding.controllerGeneration,
|
|
473
|
+
targetId,
|
|
474
|
+
});
|
|
475
|
+
} catch (error) {
|
|
476
|
+
if (error instanceof ComputerFrameEvidenceMismatchError) {
|
|
477
|
+
observeComputerFrameEvidenceMismatch(deps.observability, error.reason);
|
|
478
|
+
}
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
);
|
|
483
|
+
return new Response(frame.data.slice().buffer, {
|
|
484
|
+
status: 200,
|
|
485
|
+
headers: {
|
|
486
|
+
"cache-control": "no-store",
|
|
487
|
+
"content-type": frame.mediaType,
|
|
488
|
+
"x-opengeni-computer-frame": frame.metadataHeader,
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
},
|
|
492
|
+
);
|
|
493
|
+
|
|
392
494
|
app.get(
|
|
393
495
|
"/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/clipboard",
|
|
394
496
|
async (context) => {
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, createHmac } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
WORKSPACE_OPENROUTER_CONNECTION_DOMAIN,
|
|
4
|
+
WORKSPACE_OPENROUTER_CONNECTION_ROLE,
|
|
5
|
+
VERCEL_AI_GATEWAY_CONNECTION_DOMAIN,
|
|
6
|
+
VERCEL_AI_GATEWAY_CONNECTION_ROLE,
|
|
7
|
+
} from "@opengeni/config";
|
|
2
8
|
import {
|
|
3
9
|
ATLASSIAN_PROVIDER_DOMAIN,
|
|
4
10
|
AtlassianConnectionMetadata,
|
|
@@ -21,11 +27,16 @@ import {
|
|
|
21
27
|
IntegrationClientMetadata,
|
|
22
28
|
ListSlackInstallationBindingsResponse,
|
|
23
29
|
ListConnectionsResponse,
|
|
30
|
+
OPENROUTER_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY,
|
|
31
|
+
OPENROUTER_CREDENTIAL_OPERATION_ID_METADATA_KEY,
|
|
24
32
|
OpenGeniSlackBotInstallRequest,
|
|
25
33
|
OpenGeniSlackBotInstallStart,
|
|
26
34
|
OAuthStartRequest,
|
|
27
35
|
OAuthStartResponse,
|
|
28
36
|
UpdateConnectionRequest,
|
|
37
|
+
VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY,
|
|
38
|
+
VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_ID_METADATA_KEY,
|
|
39
|
+
stableJson,
|
|
29
40
|
} from "@opengeni/contracts";
|
|
30
41
|
import {
|
|
31
42
|
bindConnectorDocumentDestination,
|
|
@@ -69,11 +80,17 @@ import {
|
|
|
69
80
|
persistSlackBotInstallationWithSuccessAudit,
|
|
70
81
|
recordSlackBotInstallCallbackFailure,
|
|
71
82
|
revokeConnection,
|
|
83
|
+
revokeWorkspaceOpenRouterConnections,
|
|
84
|
+
revokeWorkspaceVercelAiGatewayConnections,
|
|
72
85
|
revokeConnectionWithSlackBotSuccessAudit,
|
|
86
|
+
rotateWorkspaceVercelAiGatewayConnection,
|
|
87
|
+
rotateWorkspaceOpenRouterConnection,
|
|
73
88
|
SlackBotLifecycleSuccessAuditError,
|
|
74
89
|
SlackInstallationBindingConflictError,
|
|
75
90
|
updateConnection,
|
|
76
91
|
updateSlackBotDocumentDestination,
|
|
92
|
+
upsertWorkspaceVercelAiGatewayConnection,
|
|
93
|
+
upsertWorkspaceOpenRouterConnection,
|
|
77
94
|
type SlackBotInstallCallbackFailureReason,
|
|
78
95
|
type SlackBotInstallCallbackFailureStage,
|
|
79
96
|
} from "@opengeni/db";
|
|
@@ -193,18 +210,69 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
193
210
|
assertNotDirectGoogleDriveOAuth(providerDomain, payload.kind, payload.metadata);
|
|
194
211
|
assertNotDirectAtlassianOAuth(providerDomain, payload.kind, payload.metadata);
|
|
195
212
|
assertNotDirectPersonalGitHubOAuth(providerDomain, payload.kind, payload.metadata);
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
workspaceId,
|
|
213
|
+
const credentialEncrypted = encryptCredentialBundle(key, payload.credential);
|
|
214
|
+
const workspaceProviderKind = workspaceProviderApiKeyConnectionKind({
|
|
199
215
|
subjectId,
|
|
200
216
|
providerDomain,
|
|
201
217
|
kind: payload.kind,
|
|
202
|
-
credentialEncrypted: encryptCredentialBundle(key, payload.credential),
|
|
203
|
-
grantedScopes: payload.grantedScopes,
|
|
204
|
-
expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null,
|
|
205
218
|
metadata: payload.metadata,
|
|
206
|
-
createdBySubjectId: grant.subjectId,
|
|
207
219
|
});
|
|
220
|
+
const connection = workspaceProviderKind
|
|
221
|
+
? await (async () => {
|
|
222
|
+
const provider = workspaceProviderApiKeyConnectionSpec(workspaceProviderKind);
|
|
223
|
+
if (!payload.operationId) {
|
|
224
|
+
throw new HTTPException(400, {
|
|
225
|
+
message: `connecting ${provider.label} requires an operationId`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const expiresAt = payload.expiresAt ? new Date(payload.expiresAt) : null;
|
|
229
|
+
const metadata = workspaceProviderCredentialMetadata(
|
|
230
|
+
workspaceProviderKind,
|
|
231
|
+
payload.metadata,
|
|
232
|
+
);
|
|
233
|
+
const input = {
|
|
234
|
+
accountId: grant.accountId,
|
|
235
|
+
workspaceId,
|
|
236
|
+
operationId: payload.operationId,
|
|
237
|
+
requestDigest: workspaceProviderCredentialRequestDigest(key, {
|
|
238
|
+
action: "create",
|
|
239
|
+
providerDomain,
|
|
240
|
+
kind: payload.kind,
|
|
241
|
+
subjectId,
|
|
242
|
+
credential: payload.credential,
|
|
243
|
+
grantedScopes: payload.grantedScopes,
|
|
244
|
+
expiresAt: expiresAt?.toISOString() ?? null,
|
|
245
|
+
metadata,
|
|
246
|
+
}),
|
|
247
|
+
credentialEncrypted,
|
|
248
|
+
grantedScopes: payload.grantedScopes,
|
|
249
|
+
expiresAt,
|
|
250
|
+
metadata,
|
|
251
|
+
updatedBySubjectId: grant.subjectId,
|
|
252
|
+
};
|
|
253
|
+
const created =
|
|
254
|
+
workspaceProviderKind === "vercel_gateway"
|
|
255
|
+
? await upsertWorkspaceVercelAiGatewayConnection(db, input)
|
|
256
|
+
: await upsertWorkspaceOpenRouterConnection(db, input);
|
|
257
|
+
if (!created || created.status === "revoked") {
|
|
258
|
+
throw new HTTPException(409, {
|
|
259
|
+
message: `${provider.label} is already connected; reload before replacing its key`,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return created;
|
|
263
|
+
})()
|
|
264
|
+
: await createConnection(db, {
|
|
265
|
+
accountId: grant.accountId,
|
|
266
|
+
workspaceId,
|
|
267
|
+
subjectId,
|
|
268
|
+
providerDomain,
|
|
269
|
+
kind: payload.kind,
|
|
270
|
+
credentialEncrypted,
|
|
271
|
+
grantedScopes: payload.grantedScopes,
|
|
272
|
+
expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null,
|
|
273
|
+
metadata: payload.metadata,
|
|
274
|
+
createdBySubjectId: grant.subjectId,
|
|
275
|
+
});
|
|
208
276
|
return c.json(ConnectionResponse.parse({ connection }), 201);
|
|
209
277
|
});
|
|
210
278
|
|
|
@@ -794,6 +862,24 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
794
862
|
payload.providerDomain ?? existing.providerDomain,
|
|
795
863
|
);
|
|
796
864
|
const kind = payload.kind ?? existing.kind;
|
|
865
|
+
const subjectId =
|
|
866
|
+
payload.subjectId === undefined
|
|
867
|
+
? existing.subjectId
|
|
868
|
+
: writableSubjectId(payload.subjectId, grant.subjectId);
|
|
869
|
+
const targetWorkspaceProviderKind = workspaceProviderApiKeyConnectionKind({
|
|
870
|
+
subjectId,
|
|
871
|
+
providerDomain,
|
|
872
|
+
kind,
|
|
873
|
+
metadata: payload.metadata ?? existing.metadata,
|
|
874
|
+
});
|
|
875
|
+
if (!workspaceProviderApiKeyConnectionKind(existing) && targetWorkspaceProviderKind) {
|
|
876
|
+
throw new HTTPException(422, {
|
|
877
|
+
message:
|
|
878
|
+
targetWorkspaceProviderKind === "vercel_gateway"
|
|
879
|
+
? "use the Vercel AI Gateway connect flow to create this connection"
|
|
880
|
+
: "use the OpenRouter connect flow to create this connection",
|
|
881
|
+
});
|
|
882
|
+
}
|
|
797
883
|
assertNotDirectPersonalSlackOAuth(providerDomain, kind);
|
|
798
884
|
assertNotDirectGoogleDriveOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
|
|
799
885
|
assertNotDirectAtlassianOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
|
|
@@ -819,6 +905,86 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
819
905
|
});
|
|
820
906
|
}
|
|
821
907
|
}
|
|
908
|
+
const existingWorkspaceProviderKind = existing
|
|
909
|
+
? workspaceProviderApiKeyConnectionKind(existing)
|
|
910
|
+
: null;
|
|
911
|
+
if (existing && existingWorkspaceProviderKind) {
|
|
912
|
+
const provider = workspaceProviderApiKeyConnectionSpec(existingWorkspaceProviderKind);
|
|
913
|
+
const providerDomain = canonicalProviderDomain(
|
|
914
|
+
payload.providerDomain ?? existing.providerDomain,
|
|
915
|
+
);
|
|
916
|
+
const subjectId =
|
|
917
|
+
payload.subjectId === undefined
|
|
918
|
+
? existing.subjectId
|
|
919
|
+
: writableSubjectId(payload.subjectId, grant.subjectId);
|
|
920
|
+
const kind = payload.kind ?? existing.kind;
|
|
921
|
+
if (
|
|
922
|
+
subjectId !== null ||
|
|
923
|
+
providerDomain !== provider.providerDomain ||
|
|
924
|
+
kind !== "api_key" ||
|
|
925
|
+
(payload.metadata?.credentialRole !== undefined &&
|
|
926
|
+
payload.metadata.credentialRole !== provider.credentialRole)
|
|
927
|
+
) {
|
|
928
|
+
throw new HTTPException(422, {
|
|
929
|
+
message: `${provider.label} connection identity cannot be changed`,
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
if (payload.credential === undefined) {
|
|
933
|
+
throw new HTTPException(400, {
|
|
934
|
+
message: `updating a ${provider.label} connection requires a new credential`,
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
if (payload.expectedVersion === undefined || payload.operationId === undefined) {
|
|
938
|
+
throw new HTTPException(400, {
|
|
939
|
+
message: `updating a ${provider.label} connection requires expectedVersion and operationId`,
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
const key = requireEnvironmentEncryption(settings);
|
|
943
|
+
const grantedScopes = payload.grantedScopes ?? existing.grantedScopes;
|
|
944
|
+
const expiresAt =
|
|
945
|
+
payload.expiresAt !== undefined
|
|
946
|
+
? payload.expiresAt
|
|
947
|
+
? new Date(payload.expiresAt)
|
|
948
|
+
: null
|
|
949
|
+
: existing.expiresAt
|
|
950
|
+
? new Date(existing.expiresAt)
|
|
951
|
+
: null;
|
|
952
|
+
const metadata = workspaceProviderCredentialMetadata(existingWorkspaceProviderKind, {
|
|
953
|
+
...existing.metadata,
|
|
954
|
+
...(payload.metadata ?? {}),
|
|
955
|
+
});
|
|
956
|
+
const input = {
|
|
957
|
+
accountId: grant.accountId,
|
|
958
|
+
workspaceId,
|
|
959
|
+
connectionId: existing.id,
|
|
960
|
+
expectedVersion: payload.expectedVersion,
|
|
961
|
+
operationId: payload.operationId,
|
|
962
|
+
requestDigest: workspaceProviderCredentialRequestDigest(key, {
|
|
963
|
+
action: "rotate",
|
|
964
|
+
connectionId: existing.id,
|
|
965
|
+
expectedVersion: payload.expectedVersion,
|
|
966
|
+
credential: payload.credential,
|
|
967
|
+
grantedScopes,
|
|
968
|
+
expiresAt: expiresAt?.toISOString() ?? null,
|
|
969
|
+
metadata,
|
|
970
|
+
}),
|
|
971
|
+
credentialEncrypted: encryptCredentialBundle(key, payload.credential),
|
|
972
|
+
grantedScopes,
|
|
973
|
+
expiresAt,
|
|
974
|
+
metadata,
|
|
975
|
+
updatedBySubjectId: grant.subjectId,
|
|
976
|
+
};
|
|
977
|
+
const connection =
|
|
978
|
+
existingWorkspaceProviderKind === "vercel_gateway"
|
|
979
|
+
? await rotateWorkspaceVercelAiGatewayConnection(db, input)
|
|
980
|
+
: await rotateWorkspaceOpenRouterConnection(db, input);
|
|
981
|
+
if (!connection) {
|
|
982
|
+
throw new HTTPException(409, {
|
|
983
|
+
message: `${provider.label} connection changed; reload before replacing its key`,
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
return c.json(ConnectionResponse.parse({ connection }));
|
|
987
|
+
}
|
|
822
988
|
const key = payload.credential === undefined ? null : requireEnvironmentEncryption(settings);
|
|
823
989
|
const subjectId =
|
|
824
990
|
payload.subjectId === undefined
|
|
@@ -879,6 +1045,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
879
1045
|
existing.providerDomain === ATLASSIAN_PROVIDER_DOMAIN &&
|
|
880
1046
|
existing.kind === "oauth2" &&
|
|
881
1047
|
AtlassianConnectionMetadata.safeParse(existing.metadata).success;
|
|
1048
|
+
const workspaceProviderKind = workspaceProviderApiKeyConnectionKind(existing);
|
|
882
1049
|
const disconnectPayload = await c.req.json().catch(() => null);
|
|
883
1050
|
const googleDriveDisconnect = isGoogleDrive
|
|
884
1051
|
? GoogleDriveDisconnectRequest.safeParse(disconnectPayload)
|
|
@@ -909,7 +1076,13 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
909
1076
|
"invalid personal GitHub disconnect request",
|
|
910
1077
|
});
|
|
911
1078
|
}
|
|
912
|
-
if (
|
|
1079
|
+
if (
|
|
1080
|
+
existing.status === "revoked" &&
|
|
1081
|
+
!isGoogleDrive &&
|
|
1082
|
+
!isAtlassian &&
|
|
1083
|
+
!isPersonalGitHub &&
|
|
1084
|
+
!workspaceProviderKind
|
|
1085
|
+
) {
|
|
913
1086
|
return c.json(ConnectionResponse.parse({ connection: existing }));
|
|
914
1087
|
}
|
|
915
1088
|
const connection = isGoogleDrive
|
|
@@ -944,13 +1117,29 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
944
1117
|
credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
|
|
945
1118
|
slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
|
|
946
1119
|
})
|
|
947
|
-
:
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1120
|
+
: workspaceProviderKind
|
|
1121
|
+
? workspaceProviderKind === "vercel_gateway"
|
|
1122
|
+
? await revokeWorkspaceVercelAiGatewayConnections(db, {
|
|
1123
|
+
accountId: grant.accountId,
|
|
1124
|
+
workspaceId,
|
|
1125
|
+
connectionId,
|
|
1126
|
+
expectedVersion: existing.version,
|
|
1127
|
+
updatedBySubjectId: grant.subjectId,
|
|
1128
|
+
})
|
|
1129
|
+
: await revokeWorkspaceOpenRouterConnections(db, {
|
|
1130
|
+
accountId: grant.accountId,
|
|
1131
|
+
workspaceId,
|
|
1132
|
+
connectionId,
|
|
1133
|
+
expectedVersion: existing.version,
|
|
1134
|
+
updatedBySubjectId: grant.subjectId,
|
|
1135
|
+
})
|
|
1136
|
+
: await revokeConnection(
|
|
1137
|
+
db,
|
|
1138
|
+
workspaceId,
|
|
1139
|
+
connectionId,
|
|
1140
|
+
grant.subjectId,
|
|
1141
|
+
existing.version,
|
|
1142
|
+
);
|
|
954
1143
|
if (!connection) {
|
|
955
1144
|
throw new HTTPException(409, { message: "connection changed during disconnect; try again" });
|
|
956
1145
|
}
|
|
@@ -1367,6 +1556,51 @@ function assertNotReservedPersonalGitHubMetadata(
|
|
|
1367
1556
|
}
|
|
1368
1557
|
}
|
|
1369
1558
|
|
|
1559
|
+
type WorkspaceProviderApiKeyConnectionKind = "vercel_gateway" | "openrouter";
|
|
1560
|
+
|
|
1561
|
+
function workspaceProviderApiKeyConnectionSpec(
|
|
1562
|
+
providerKind: WorkspaceProviderApiKeyConnectionKind,
|
|
1563
|
+
): {
|
|
1564
|
+
providerDomain: string;
|
|
1565
|
+
credentialRole: string;
|
|
1566
|
+
label: string;
|
|
1567
|
+
} {
|
|
1568
|
+
return providerKind === "vercel_gateway"
|
|
1569
|
+
? {
|
|
1570
|
+
providerDomain: VERCEL_AI_GATEWAY_CONNECTION_DOMAIN,
|
|
1571
|
+
credentialRole: VERCEL_AI_GATEWAY_CONNECTION_ROLE,
|
|
1572
|
+
label: "Vercel AI Gateway",
|
|
1573
|
+
}
|
|
1574
|
+
: {
|
|
1575
|
+
providerDomain: WORKSPACE_OPENROUTER_CONNECTION_DOMAIN,
|
|
1576
|
+
credentialRole: WORKSPACE_OPENROUTER_CONNECTION_ROLE,
|
|
1577
|
+
label: "OpenRouter",
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function workspaceProviderApiKeyConnectionKind(input: {
|
|
1582
|
+
subjectId?: string | null;
|
|
1583
|
+
providerDomain: string;
|
|
1584
|
+
kind: string;
|
|
1585
|
+
metadata?: Record<string, unknown>;
|
|
1586
|
+
}): WorkspaceProviderApiKeyConnectionKind | null {
|
|
1587
|
+
if (input.subjectId != null || input.kind !== "api_key") return null;
|
|
1588
|
+
const providerDomain = input.providerDomain.toLowerCase();
|
|
1589
|
+
if (
|
|
1590
|
+
providerDomain === VERCEL_AI_GATEWAY_CONNECTION_DOMAIN &&
|
|
1591
|
+
input.metadata?.credentialRole === VERCEL_AI_GATEWAY_CONNECTION_ROLE
|
|
1592
|
+
) {
|
|
1593
|
+
return "vercel_gateway";
|
|
1594
|
+
}
|
|
1595
|
+
if (
|
|
1596
|
+
providerDomain === WORKSPACE_OPENROUTER_CONNECTION_DOMAIN &&
|
|
1597
|
+
input.metadata?.credentialRole === WORKSPACE_OPENROUTER_CONNECTION_ROLE
|
|
1598
|
+
) {
|
|
1599
|
+
return "openrouter";
|
|
1600
|
+
}
|
|
1601
|
+
return null;
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1370
1604
|
function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | undefined): void {
|
|
1371
1605
|
if (hasReservedOpenGeniSlackBotMetadata(metadata)) {
|
|
1372
1606
|
throw new HTTPException(422, {
|
|
@@ -1426,6 +1660,27 @@ function encryptCredentialBundle(key: Uint8Array, credential: Record<string, unk
|
|
|
1426
1660
|
return encryptEnvironmentValue(key, JSON.stringify(credential));
|
|
1427
1661
|
}
|
|
1428
1662
|
|
|
1663
|
+
function workspaceProviderCredentialMetadata(
|
|
1664
|
+
providerKind: WorkspaceProviderApiKeyConnectionKind,
|
|
1665
|
+
metadata: Record<string, unknown> | undefined,
|
|
1666
|
+
): Record<string, unknown> {
|
|
1667
|
+
const {
|
|
1668
|
+
[OPENROUTER_CREDENTIAL_OPERATION_ID_METADATA_KEY]: _openRouterOperationId,
|
|
1669
|
+
[OPENROUTER_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY]: _openRouterOperationDigest,
|
|
1670
|
+
[VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_ID_METADATA_KEY]: _operationId,
|
|
1671
|
+
[VERCEL_AI_GATEWAY_CREDENTIAL_OPERATION_DIGEST_METADATA_KEY]: _operationDigest,
|
|
1672
|
+
...effectiveMetadata
|
|
1673
|
+
} = metadata ?? {};
|
|
1674
|
+
return {
|
|
1675
|
+
...effectiveMetadata,
|
|
1676
|
+
credentialRole: workspaceProviderApiKeyConnectionSpec(providerKind).credentialRole,
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function workspaceProviderCredentialRequestDigest(key: Uint8Array, value: unknown): string {
|
|
1681
|
+
return createHmac("sha256", key).update(stableJson(value), "utf8").digest("hex");
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1429
1684
|
function slackBotCredentialBundle(token: string): Record<string, unknown> {
|
|
1430
1685
|
const headerName = ["author", "ization"].join("");
|
|
1431
1686
|
const scheme = ["Bear", "er"].join("");
|
package/src/routes/github.ts
CHANGED
|
@@ -428,14 +428,6 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
428
428
|
if (candidates.length === 0) {
|
|
429
429
|
return redirectToGitHubInstallation(c, deps, selectionState);
|
|
430
430
|
}
|
|
431
|
-
if (candidates.length === 1) {
|
|
432
|
-
return redirectToExactGitHubAuthorization(
|
|
433
|
-
c,
|
|
434
|
-
deps,
|
|
435
|
-
selectionState,
|
|
436
|
-
candidates[0]!.installation.installationId,
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
431
|
setGitHubStateCookie(c, deps, selectionState);
|
|
440
432
|
return c.html(
|
|
441
433
|
githubInstallationChooserHtml(
|
|
@@ -66,7 +66,7 @@ export function registerIntegrationFacetRoutes(app: Hono, deps: ApiRouteDeps): v
|
|
|
66
66
|
c,
|
|
67
67
|
deps,
|
|
68
68
|
workspaceId,
|
|
69
|
-
"
|
|
69
|
+
"capabilities:manage",
|
|
70
70
|
);
|
|
71
71
|
const { grant } = authorization;
|
|
72
72
|
try {
|
|
@@ -82,7 +82,10 @@ export function registerIntegrationFacetRoutes(app: Hono, deps: ApiRouteDeps): v
|
|
|
82
82
|
payload: await c.req.json(),
|
|
83
83
|
canManageOrganizationDestination:
|
|
84
84
|
authorization.accountGrant?.permissions.includes("account:admin") === true,
|
|
85
|
-
canManageWorkspaceDestination: hasPermission(
|
|
85
|
+
canManageWorkspaceDestination: hasPermission(
|
|
86
|
+
grant.permissions,
|
|
87
|
+
"capabilities:manage",
|
|
88
|
+
),
|
|
86
89
|
canManagePersonalDestination:
|
|
87
90
|
authorization.contextIntegrity &&
|
|
88
91
|
authorization.authenticatedSubjectId === grant.subjectId,
|
|
@@ -122,7 +125,7 @@ export function registerIntegrationFacetRoutes(app: Hono, deps: ApiRouteDeps): v
|
|
|
122
125
|
"/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/facets/:facetKey",
|
|
123
126
|
async (c) => {
|
|
124
127
|
const workspaceId = c.req.param("workspaceId");
|
|
125
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
128
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
126
129
|
const payload = UpsertIntegrationFacetRequest.parse(await c.req.json());
|
|
127
130
|
const capabilityId = decoded(c.req.param("capabilityId"));
|
|
128
131
|
const instanceKey = decoded(c.req.param("instanceKey"));
|
|
@@ -204,7 +207,7 @@ export function registerIntegrationFacetRoutes(app: Hono, deps: ApiRouteDeps): v
|
|
|
204
207
|
`/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/facets/:facetKey/${action}`,
|
|
205
208
|
async (c) => {
|
|
206
209
|
const workspaceId = c.req.param("workspaceId");
|
|
207
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
210
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
208
211
|
const payload = MutateIntegrationFacetRequest.parse(await c.req.json());
|
|
209
212
|
try {
|
|
210
213
|
return c.json(
|
|
@@ -233,7 +236,7 @@ export function registerIntegrationFacetRoutes(app: Hono, deps: ApiRouteDeps): v
|
|
|
233
236
|
"/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/facets/:facetKey",
|
|
234
237
|
async (c) => {
|
|
235
238
|
const workspaceId = c.req.param("workspaceId");
|
|
236
|
-
const grant = await requireAccessGrant(c, deps, workspaceId, "
|
|
239
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "capabilities:manage");
|
|
237
240
|
const payload = MutateIntegrationFacetRequest.parse(await c.req.json());
|
|
238
241
|
try {
|
|
239
242
|
return c.json(
|