@opengeni/api-router 3.0.1 → 4.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/dist/admission-trace.d.ts +8 -0
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +2 -0
- package/dist/auth/managed-auth.d.ts +1 -0
- package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
- package/dist/{chunk-AJ62T5QN.js → chunk-E7TZFJ65.js} +22300 -21113
- package/dist/{chunk-AJ62T5QN.js.map → chunk-E7TZFJ65.js.map} +1 -1
- package/dist/connection-ownership.d.ts +1 -2
- package/dist/fatal-process-boundary.d.ts +1 -1
- package/dist/index.js +24 -6
- package/dist/index.js.map +1 -1
- package/dist/integrations/atlassian.d.ts +8 -3
- package/dist/integrations/fiken.d.ts +3 -1
- package/dist/integrations/github-lens-connect.d.ts +5 -65
- package/dist/integrations/google-drive.d.ts +2 -0
- package/dist/integrations/integration-commit-authority.d.ts +13 -0
- package/dist/integrations/oauth-client.d.ts +119 -11
- package/dist/integrations/oauth-profiles.d.ts +8 -13
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/mcp/capability-account-readiness.d.ts +7 -0
- package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
- package/dist/mcp-oauth.d.ts +22 -1
- package/dist/routes/api-integrations.d.ts +1 -0
- package/dist/routes/codex.d.ts +28 -0
- package/dist/routes/managed-sign-in-methods.d.ts +6 -0
- package/dist/routes/organization-integration-policy.d.ts +3 -0
- package/dist/routes/sessions.d.ts +24 -2
- package/dist/scheduled-task-deletion.d.ts +3 -4
- package/dist/workspace-tool-gateway.d.ts +3 -1
- package/package.json +15 -15
- package/src/admission-trace.ts +14 -0
- package/src/app.ts +141 -94
- package/src/auth/managed-auth-attempt-context.ts +9 -0
- package/src/auth/managed-auth-session-adapter.ts +11 -0
- package/src/auth/managed-auth.ts +39 -2
- package/src/auth/managed-sign-in-notifications.ts +81 -0
- package/src/connection-ownership.ts +2 -9
- package/src/editable-artifact-production.ts +6 -0
- package/src/fatal-process-boundary.ts +17 -2
- package/src/index.ts +3 -4
- package/src/integrations/atlassian.ts +184 -76
- package/src/integrations/fiken.ts +57 -35
- package/src/integrations/github-app-connect.ts +51 -15
- package/src/integrations/github-lens-connect.ts +7 -28
- package/src/integrations/google-drive.ts +296 -125
- package/src/integrations/integration-commit-authority.ts +69 -0
- package/src/integrations/oauth-client.ts +231 -132
- package/src/integrations/oauth-profiles.ts +10 -45
- package/src/integrations/personal-github.ts +55 -31
- package/src/integrations/provider-oauth.ts +57 -82
- package/src/integrations/social-oauth.ts +83 -56
- package/src/mcp/capability-account-readiness.ts +17 -0
- package/src/mcp/editable-artifacts.ts +7 -2
- package/src/mcp/first-party-tool-permissions.ts +1 -0
- package/src/mcp/knowledge-entries.ts +25 -8
- package/src/mcp/server.ts +135 -43
- package/src/mcp-oauth.ts +336 -34
- package/src/routes/api-integrations.ts +116 -4
- package/src/routes/automations.ts +5 -13
- package/src/routes/billing.ts +52 -0
- package/src/routes/canonical-human-identities.ts +61 -0
- package/src/routes/capabilities.ts +51 -1
- package/src/routes/codex.ts +4 -2
- package/src/routes/connect.ts +90 -31
- package/src/routes/connections.ts +487 -307
- package/src/routes/files.ts +21 -12
- package/src/routes/github.ts +48 -19
- package/src/routes/insights.ts +17 -7
- package/src/routes/integration-facets.ts +66 -25
- package/src/routes/knowledge.ts +48 -1
- package/src/routes/managed-sign-in-methods.ts +433 -0
- package/src/routes/organization-integration-policy.ts +113 -0
- package/src/routes/organization-memberships.ts +22 -2
- package/src/routes/plugins.ts +42 -4
- package/src/routes/pr-review-github.ts +67 -57
- package/src/routes/pr-review.ts +74 -71
- package/src/routes/rigs.ts +32 -13
- package/src/routes/scheduled-tasks.ts +6 -6
- package/src/routes/sessions.ts +233 -11
- package/src/routes/skill-content.ts +19 -3
- package/src/routes/social.ts +24 -13
- package/src/sandbox/channel-a.ts +7 -1
- package/src/sandbox/rematerialize.ts +2 -1
- package/src/scheduled-task-deletion.ts +13 -3
- package/src/workspace-tool-gateway.ts +31 -31
- package/dist/connection-authority-owner.d.ts +0 -59
- package/dist/routes/connection-authorities.d.ts +0 -3
- package/dist/routes/host-mcp-bindings.d.ts +0 -5
- package/dist/routes/host-mcp-resolvers.d.ts +0 -3
- package/dist/routes/packs.d.ts +0 -3
- package/src/connection-authority-owner.ts +0 -61
- package/src/routes/connection-authorities.ts +0 -139
- package/src/routes/host-mcp-bindings.ts +0 -122
- package/src/routes/host-mcp-resolvers.ts +0 -41
- package/src/routes/packs.ts +0 -784
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { createHash, createHmac } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
createConnectionIdempotently,
|
|
4
|
+
getConnectionCreationResult,
|
|
5
|
+
ConnectionCreateIdempotencyError,
|
|
6
|
+
} from "@opengeni/db";
|
|
7
|
+
import { sql } from "drizzle-orm";
|
|
8
|
+
import { assertOrganizationIntegrationAllowed } from "@opengeni/contracts";
|
|
9
|
+
import {
|
|
10
|
+
withOrganizationIntegrationAcquisition,
|
|
11
|
+
withOrganizationIntegrationPolicyFence,
|
|
12
|
+
} from "@opengeni/db/organization-integration-policy";
|
|
2
13
|
import { ExternalActorContinuation } from "@opengeni/contracts/external-identities";
|
|
3
14
|
import {
|
|
4
15
|
startSlackBotInstall,
|
|
@@ -59,6 +70,7 @@ import {
|
|
|
59
70
|
} from "@opengeni/contracts/personal-github";
|
|
60
71
|
import {
|
|
61
72
|
hasPermission,
|
|
73
|
+
listOwnConnectionAccountsForGrant,
|
|
62
74
|
hasReservedFikenMetadata,
|
|
63
75
|
hasReservedOpenGeniSlackBotMetadata,
|
|
64
76
|
isOpenGeniSlackBotConnection,
|
|
@@ -74,7 +86,6 @@ import {
|
|
|
74
86
|
finishConnectOperation,
|
|
75
87
|
getConnectAttempt,
|
|
76
88
|
decryptEnvironmentValue,
|
|
77
|
-
createConnection,
|
|
78
89
|
withWorkspaceSubjectRls,
|
|
79
90
|
type Database,
|
|
80
91
|
encryptEnvironmentValue,
|
|
@@ -185,6 +196,16 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
185
196
|
);
|
|
186
197
|
});
|
|
187
198
|
|
|
199
|
+
app.get("/v1/workspaces/:workspaceId/connections/accounts", async (c) => {
|
|
200
|
+
const workspaceId = c.req.param("workspaceId");
|
|
201
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
|
|
202
|
+
return c.json(
|
|
203
|
+
ListConnectionsResponse.parse({
|
|
204
|
+
connections: await listOwnConnectionAccountsForGrant(db, grant),
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
|
|
188
209
|
app.get("/v1/workspaces/:workspaceId/connections/slack-bot/bindings", async (c) => {
|
|
189
210
|
const workspaceId = c.req.param("workspaceId");
|
|
190
211
|
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
|
|
@@ -198,6 +219,22 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
198
219
|
);
|
|
199
220
|
});
|
|
200
221
|
|
|
222
|
+
app.get("/v1/workspaces/:workspaceId/connections/operations/:operationId", async (c) => {
|
|
223
|
+
const workspaceId = c.req.param("workspaceId");
|
|
224
|
+
const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
|
|
225
|
+
const operationId = CreateConnectionRequest.shape.operationId
|
|
226
|
+
.unwrap()
|
|
227
|
+
.parse(c.req.param("operationId"));
|
|
228
|
+
const connection = await getConnectionCreationResult(db, {
|
|
229
|
+
accountId: grant.accountId,
|
|
230
|
+
workspaceId,
|
|
231
|
+
subjectId: grant.subjectId,
|
|
232
|
+
operationId,
|
|
233
|
+
});
|
|
234
|
+
if (!connection) throw new HTTPException(404, { message: "Connection operation not found" });
|
|
235
|
+
return c.json(ConnectionResponse.parse({ connection }));
|
|
236
|
+
});
|
|
237
|
+
|
|
201
238
|
app.post("/v1/workspaces/:workspaceId/connections", async (c) => {
|
|
202
239
|
const workspaceId = c.req.param("workspaceId");
|
|
203
240
|
const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "connections:write");
|
|
@@ -272,7 +309,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
272
309
|
}
|
|
273
310
|
return created;
|
|
274
311
|
})()
|
|
275
|
-
: await
|
|
312
|
+
: await createConnectionIdempotently(db, {
|
|
276
313
|
accountId: grant.accountId,
|
|
277
314
|
workspaceId,
|
|
278
315
|
subjectId,
|
|
@@ -283,15 +320,56 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
283
320
|
expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null,
|
|
284
321
|
metadata: payload.metadata,
|
|
285
322
|
createdBySubjectId: grant.subjectId,
|
|
323
|
+
...(payload.operationId
|
|
324
|
+
? {
|
|
325
|
+
operation: {
|
|
326
|
+
id: payload.operationId,
|
|
327
|
+
requestDigest: workspaceProviderCredentialRequestDigest(key, {
|
|
328
|
+
action: "create",
|
|
329
|
+
providerDomain,
|
|
330
|
+
kind: payload.kind,
|
|
331
|
+
subjectId,
|
|
332
|
+
credential: payload.credential,
|
|
333
|
+
grantedScopes: payload.grantedScopes,
|
|
334
|
+
expiresAt: payload.expiresAt
|
|
335
|
+
? new Date(payload.expiresAt).toISOString()
|
|
336
|
+
: null,
|
|
337
|
+
metadata: payload.metadata,
|
|
338
|
+
}),
|
|
339
|
+
},
|
|
340
|
+
}
|
|
341
|
+
: {}),
|
|
342
|
+
}).catch((error: unknown) => {
|
|
343
|
+
if (error instanceof ConnectionCreateIdempotencyError) {
|
|
344
|
+
throw new HTTPException(409, { message: error.message });
|
|
345
|
+
}
|
|
346
|
+
throw error;
|
|
286
347
|
});
|
|
287
348
|
return c.json(ConnectionResponse.parse({ connection }), 201);
|
|
288
349
|
};
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
350
|
+
const commit = (database: Database) =>
|
|
351
|
+
beforeCommit
|
|
352
|
+
? withWorkspaceSubjectRls(database, workspaceId, grant.subjectId, async (tx) => {
|
|
353
|
+
await beforeCommit(tx);
|
|
354
|
+
return persist(tx);
|
|
355
|
+
})
|
|
356
|
+
: persist(database);
|
|
357
|
+
// Only the canonical workspace model-key lane is outside integration policy.
|
|
358
|
+
// Generic credentials have no validated protocol/curated target in this contract.
|
|
359
|
+
const modelKey = workspaceProviderApiKeyConnectionKind({
|
|
360
|
+
subjectId: createConnectionSubjectId(payload, grant.subjectId),
|
|
361
|
+
providerDomain: canonicalProviderDomain(payload.providerDomain),
|
|
362
|
+
kind: payload.kind,
|
|
363
|
+
metadata: payload.metadata,
|
|
364
|
+
});
|
|
365
|
+
return modelKey
|
|
366
|
+
? commit(db)
|
|
367
|
+
: withOrganizationIntegrationAcquisition(
|
|
368
|
+
db,
|
|
369
|
+
{ accountId: grant.accountId, workspaceId },
|
|
370
|
+
[null],
|
|
371
|
+
commit,
|
|
372
|
+
);
|
|
295
373
|
});
|
|
296
374
|
|
|
297
375
|
app.post("/v1/workspaces/:workspaceId/connections/slack-bot/install", async (c) => {
|
|
@@ -301,13 +379,22 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
301
379
|
const grant = access.grant;
|
|
302
380
|
const payload = OpenGeniSlackBotInstallRequest.parse(await c.req.json());
|
|
303
381
|
return c.json(
|
|
304
|
-
await
|
|
305
|
-
|
|
306
|
-
workspaceId,
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
382
|
+
await withOrganizationIntegrationAcquisition(
|
|
383
|
+
db,
|
|
384
|
+
{ accountId: grant.accountId, workspaceId },
|
|
385
|
+
["slack-bot"],
|
|
386
|
+
(tx) =>
|
|
387
|
+
startSlackBotInstall(
|
|
388
|
+
{ ...deps, db: tx },
|
|
389
|
+
{
|
|
390
|
+
accountId: grant.accountId,
|
|
391
|
+
workspaceId,
|
|
392
|
+
subjectId: grant.subjectId,
|
|
393
|
+
requestUrl: c.req.url,
|
|
394
|
+
...(payload.connectionId ? { connectionId: payload.connectionId } : {}),
|
|
395
|
+
},
|
|
396
|
+
),
|
|
397
|
+
),
|
|
311
398
|
);
|
|
312
399
|
});
|
|
313
400
|
|
|
@@ -329,12 +416,25 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
329
416
|
operationId: `oauth:${state.nonce}`,
|
|
330
417
|
inputDigest: createHash("sha256").update(c.req.query("state")!).digest("hex"),
|
|
331
418
|
};
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
419
|
+
const callbackState = state;
|
|
420
|
+
const callbackOperation = operation;
|
|
421
|
+
const claim = await withOrganizationIntegrationPolicyFence(
|
|
422
|
+
db,
|
|
423
|
+
callbackState,
|
|
424
|
+
(policyTx, policy) =>
|
|
425
|
+
claimConnectOperation(policyTx, callbackState, {
|
|
426
|
+
...callbackOperation,
|
|
427
|
+
expectedRevision: stored.attempt.revision,
|
|
428
|
+
authorize: (tx, _attempt, origin) =>
|
|
429
|
+
requireConnectOwnerAuthority(tx, callbackState, "connections:write", origin),
|
|
430
|
+
authorizeAcquisition: async () => {
|
|
431
|
+
// Provider denial/cancellation acquires nothing; completed exact
|
|
432
|
+
// receipts replay before this callback is invoked.
|
|
433
|
+
if (!c.req.query("error") && c.req.query("code"))
|
|
434
|
+
assertOrganizationIntegrationAllowed(policy, "slack-bot");
|
|
435
|
+
},
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
338
438
|
if (claim.status === "replayed")
|
|
339
439
|
return new Response(null, { status: 302, headers: { Location: exactReturnUrl } });
|
|
340
440
|
}
|
|
@@ -391,6 +491,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
391
491
|
"Slack installation callback is missing code",
|
|
392
492
|
);
|
|
393
493
|
}
|
|
494
|
+
await withOrganizationIntegrationAcquisition(db, state, ["slack-bot"], async () => {});
|
|
394
495
|
const slack = requireOpenGeniSlackOAuthSettings(settings);
|
|
395
496
|
const redirectUri = `${baseUrl}/v1/integrations/slack/callback`;
|
|
396
497
|
const authorization = await exchangeOpenGeniSlackAuthorizationCode(
|
|
@@ -424,35 +525,44 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
424
525
|
verified,
|
|
425
526
|
});
|
|
426
527
|
if (operation) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
528
|
+
const acceptedOperation = operation;
|
|
529
|
+
await withOrganizationIntegrationPolicyFence(db, acceptedState, (policyTx, policy) =>
|
|
530
|
+
finishConnectOperation(policyTx, acceptedState, {
|
|
531
|
+
...acceptedOperation,
|
|
532
|
+
authorize: (tx, _attempt, origin) =>
|
|
533
|
+
requireConnectOwnerAuthority(tx, acceptedState, "connections:write", origin),
|
|
534
|
+
commit: async (tx, current) => {
|
|
535
|
+
assertOrganizationIntegrationAllowed(policy, "slack-bot");
|
|
536
|
+
const connection = await persist(tx);
|
|
537
|
+
return {
|
|
538
|
+
...current,
|
|
539
|
+
revision: current.revision + 1,
|
|
540
|
+
state: "complete",
|
|
541
|
+
credentialsCommitted: true,
|
|
542
|
+
nextAction: { type: "none" },
|
|
543
|
+
account: {
|
|
544
|
+
id: connection.id,
|
|
545
|
+
version: connection.version,
|
|
546
|
+
providerId: "slack-bot",
|
|
547
|
+
label: "Slack workspace bot",
|
|
548
|
+
ownership: "workspace",
|
|
549
|
+
status: "connected",
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
},
|
|
553
|
+
}),
|
|
554
|
+
);
|
|
450
555
|
return new Response(null, { status: 302, headers: { Location: exactReturnUrl! } });
|
|
451
556
|
}
|
|
452
|
-
const connection = await
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
557
|
+
const connection = await withOrganizationIntegrationAcquisition(
|
|
558
|
+
db,
|
|
559
|
+
acceptedState,
|
|
560
|
+
["slack-bot"],
|
|
561
|
+
async (tx) => {
|
|
562
|
+
await requireSlackInstallCallbackGrant(tx, acceptedState);
|
|
563
|
+
return persist(tx);
|
|
564
|
+
},
|
|
565
|
+
);
|
|
456
566
|
return c.redirect(
|
|
457
567
|
slackInstallReturnUrl(baseUrl, state.returnPath, "connected", connection.id),
|
|
458
568
|
302,
|
|
@@ -502,16 +612,21 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
502
612
|
const payload = FikenInstallRequest.parse(await c.req.json());
|
|
503
613
|
const persist = await prepareFikenTokenInstall(deps, grant, payload);
|
|
504
614
|
const continuation = externalActorContinuationForAuthorization(authorization);
|
|
505
|
-
const connection = await
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
615
|
+
const connection = await withOrganizationIntegrationAcquisition(
|
|
616
|
+
db,
|
|
617
|
+
{ accountId: grant.accountId, workspaceId },
|
|
618
|
+
["fiken"],
|
|
619
|
+
async (tx) => {
|
|
620
|
+
await requireConnectOwnerAuthority(tx, {
|
|
621
|
+
accountId: grant.accountId,
|
|
622
|
+
workspaceId,
|
|
623
|
+
subjectId: grant.subjectId,
|
|
624
|
+
personalOwnerVerified: isPersonalConnectionOwnerPrincipal(authorization),
|
|
625
|
+
...(continuation ? { externalContinuation: continuation } : {}),
|
|
626
|
+
});
|
|
627
|
+
return persist(tx);
|
|
628
|
+
},
|
|
629
|
+
);
|
|
515
630
|
return c.json(ConnectionResponse.parse({ connection }), payload.connectionId ? 200 : 201);
|
|
516
631
|
});
|
|
517
632
|
|
|
@@ -789,258 +904,323 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
|
|
|
789
904
|
);
|
|
790
905
|
const { grant } = authorization;
|
|
791
906
|
const payload = UpdateConnectionRequest.parse(await c.req.json());
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const existing = await getConnectionMetadata(
|
|
907
|
+
// Policy first, then the existing tenancy prefix and exact credential row.
|
|
908
|
+
// The locked live identity makes unchanged/reducing updates safe against a
|
|
909
|
+
// concurrent replacement; all final writes below use this same transaction.
|
|
910
|
+
return withOrganizationIntegrationPolicyFence(
|
|
797
911
|
db,
|
|
798
|
-
workspaceId,
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
: existing.expiresAt
|
|
977
|
-
? new Date(existing.expiresAt)
|
|
912
|
+
{ accountId: grant.accountId, workspaceId },
|
|
913
|
+
(policyTx, policy) =>
|
|
914
|
+
// eslint-disable-next-line no-shadow -- every existing final writer must use this exact scoped transaction
|
|
915
|
+
withWorkspaceSubjectRls(policyTx, workspaceId, grant.subjectId, async (db) => {
|
|
916
|
+
const initial = await getConnectionMetadata(
|
|
917
|
+
db,
|
|
918
|
+
workspaceId,
|
|
919
|
+
c.req.param("connectionId"),
|
|
920
|
+
grant.subjectId,
|
|
921
|
+
);
|
|
922
|
+
// Model-key rotation owns an advisory -> credential-row prefix. Do
|
|
923
|
+
// not take its row first or change that excluded lane's lock order.
|
|
924
|
+
if (!initial || !workspaceProviderApiKeyConnectionKind(initial)) {
|
|
925
|
+
await db.execute(
|
|
926
|
+
sql`select id from connections where workspace_id = ${workspaceId}::uuid and id = ${c.req.param("connectionId")}::uuid and (subject_id is null or subject_id = ${grant.subjectId}) for update`,
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
assertNotReservedSlackBotMetadata(payload.metadata);
|
|
930
|
+
assertNotReservedFikenMetadata(payload.metadata);
|
|
931
|
+
assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
|
|
932
|
+
assertNotReservedPersonalGitHubMetadata(payload.metadata);
|
|
933
|
+
const existing = await getConnectionMetadata(
|
|
934
|
+
db,
|
|
935
|
+
workspaceId,
|
|
936
|
+
c.req.param("connectionId"),
|
|
937
|
+
grant.subjectId,
|
|
938
|
+
);
|
|
939
|
+
if (existing && isOpenGeniSlackBotConnection(existing)) {
|
|
940
|
+
const destination = ConnectorDocumentDestinationSelection.safeParse(
|
|
941
|
+
payload.metadata?.documentDestination,
|
|
942
|
+
);
|
|
943
|
+
const destinationOnlyUpdate =
|
|
944
|
+
payload.metadata !== undefined &&
|
|
945
|
+
Object.keys(payload.metadata).length === 1 &&
|
|
946
|
+
payload.providerDomain === undefined &&
|
|
947
|
+
payload.subjectId === undefined &&
|
|
948
|
+
payload.kind === undefined &&
|
|
949
|
+
payload.status === undefined &&
|
|
950
|
+
payload.credential === undefined &&
|
|
951
|
+
payload.grantedScopes === undefined &&
|
|
952
|
+
payload.expiresAt === undefined;
|
|
953
|
+
if (!destination.success || !destinationOnlyUpdate) {
|
|
954
|
+
throw new HTTPException(422, {
|
|
955
|
+
message:
|
|
956
|
+
"use the dedicated OpenGeni Slack bot reinstall flow to update this connection",
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
const destinationSelection = destination.data;
|
|
960
|
+
if (
|
|
961
|
+
destinationSelection.authorityKind === "organization" &&
|
|
962
|
+
authorization.accountGrant?.permissions.includes("account:admin") !== true
|
|
963
|
+
) {
|
|
964
|
+
throw new HTTPException(403, { message: "missing permission: account:admin" });
|
|
965
|
+
}
|
|
966
|
+
if (
|
|
967
|
+
destinationSelection.authorityKind === "workspace" &&
|
|
968
|
+
!hasPermission(grant.permissions, "workspace:admin")
|
|
969
|
+
) {
|
|
970
|
+
throw new HTTPException(403, { message: "missing permission: workspace:admin" });
|
|
971
|
+
}
|
|
972
|
+
if (
|
|
973
|
+
destinationSelection.authorityKind === "personal" &&
|
|
974
|
+
(!authorization.contextIntegrity ||
|
|
975
|
+
authorization.authenticatedSubjectId !== grant.subjectId)
|
|
976
|
+
) {
|
|
977
|
+
throw new HTTPException(403, {
|
|
978
|
+
message: "personal destination requires the exact actor",
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
const metadata = {
|
|
982
|
+
...existing.metadata,
|
|
983
|
+
documentDestination: bindConnectorDocumentDestination(destinationSelection, {
|
|
984
|
+
accountId: grant.accountId,
|
|
985
|
+
workspaceId,
|
|
986
|
+
initiatingSubjectId: grant.subjectId,
|
|
987
|
+
}),
|
|
988
|
+
};
|
|
989
|
+
if (
|
|
990
|
+
stableJson(metadata.documentDestination) !==
|
|
991
|
+
stableJson(existing.metadata.documentDestination)
|
|
992
|
+
) {
|
|
993
|
+
assertOrganizationIntegrationAllowed(policy, "slack-bot");
|
|
994
|
+
}
|
|
995
|
+
const connection = await updateSlackBotDocumentDestination(db, {
|
|
996
|
+
accountId: grant.accountId,
|
|
997
|
+
workspaceId,
|
|
998
|
+
connectionId: existing.id,
|
|
999
|
+
visibleToSubjectId: grant.subjectId,
|
|
1000
|
+
expectedVersion: existing.version,
|
|
1001
|
+
metadata,
|
|
1002
|
+
updatedBySubjectId: grant.subjectId,
|
|
1003
|
+
});
|
|
1004
|
+
if (!connection) {
|
|
1005
|
+
throw new HTTPException(409, {
|
|
1006
|
+
message: "Slack bot connection changed; reload before saving its destination",
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
return c.json(ConnectionResponse.parse({ connection }));
|
|
1010
|
+
}
|
|
1011
|
+
if (existing && GoogleDriveConnectionMetadata.safeParse(existing.metadata).success) {
|
|
1012
|
+
throw new HTTPException(422, {
|
|
1013
|
+
message: "use the dedicated Google Drive reconnect or source-selection flow",
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
if (existing?.metadata.credentialRole === API_INTEGRATION_OAUTH_CREDENTIAL_ROLE) {
|
|
1017
|
+
throw new HTTPException(422, {
|
|
1018
|
+
message:
|
|
1019
|
+
"use the dedicated Integration provider OAuth flow to update this connection",
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
if (existing && AtlassianConnectionMetadata.safeParse(existing.metadata).success) {
|
|
1023
|
+
throw new HTTPException(422, {
|
|
1024
|
+
message: "use the dedicated Atlassian reconnect or source-selection flow",
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
if (existing && isPersonalGitHubConnection(existing)) {
|
|
1028
|
+
throw new HTTPException(422, {
|
|
1029
|
+
message: "use the dedicated personal GitHub reconnect flow to update this connection",
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
if (existing) {
|
|
1033
|
+
const providerDomain = canonicalProviderDomain(
|
|
1034
|
+
payload.providerDomain ?? existing.providerDomain,
|
|
1035
|
+
);
|
|
1036
|
+
const kind = payload.kind ?? existing.kind;
|
|
1037
|
+
const subjectId =
|
|
1038
|
+
payload.subjectId === undefined
|
|
1039
|
+
? existing.subjectId
|
|
1040
|
+
: writableSubjectId(payload.subjectId, grant.subjectId);
|
|
1041
|
+
const targetWorkspaceProviderKind = workspaceProviderApiKeyConnectionKind({
|
|
1042
|
+
subjectId,
|
|
1043
|
+
providerDomain,
|
|
1044
|
+
kind,
|
|
1045
|
+
metadata: payload.metadata ?? existing.metadata,
|
|
1046
|
+
});
|
|
1047
|
+
if (!workspaceProviderApiKeyConnectionKind(existing) && targetWorkspaceProviderKind) {
|
|
1048
|
+
throw new HTTPException(422, {
|
|
1049
|
+
message:
|
|
1050
|
+
targetWorkspaceProviderKind === "vercel_gateway"
|
|
1051
|
+
? "use the Vercel AI Gateway connect flow to create this connection"
|
|
1052
|
+
: "use the OpenRouter connect flow to create this connection",
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
assertNotDirectPersonalSlackOAuth(providerDomain, kind);
|
|
1056
|
+
assertNotDirectGoogleDriveOAuth(
|
|
1057
|
+
providerDomain,
|
|
1058
|
+
kind,
|
|
1059
|
+
payload.metadata ?? existing.metadata,
|
|
1060
|
+
);
|
|
1061
|
+
assertNotDirectAtlassianOAuth(
|
|
1062
|
+
providerDomain,
|
|
1063
|
+
kind,
|
|
1064
|
+
payload.metadata ?? existing.metadata,
|
|
1065
|
+
);
|
|
1066
|
+
assertNotDirectPersonalGitHubOAuth(
|
|
1067
|
+
providerDomain,
|
|
1068
|
+
kind,
|
|
1069
|
+
payload.metadata ?? existing.metadata,
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
// Status is not a free-form field: revocation goes through DELETE, and the
|
|
1073
|
+
// broker owns needs_reauth/error. Reactivating a connection is only
|
|
1074
|
+
// meaningful together with a fresh credential bundle — otherwise a PATCH
|
|
1075
|
+
// could clear the broker's re-auth signal while stale tokens stay in place.
|
|
1076
|
+
if (payload.status !== undefined) {
|
|
1077
|
+
if (payload.status !== "active") {
|
|
1078
|
+
throw new HTTPException(400, {
|
|
1079
|
+
message: 'status can only be set to "active"; use DELETE to revoke',
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
if (payload.credential === undefined) {
|
|
1083
|
+
throw new HTTPException(400, {
|
|
1084
|
+
message: "reactivating a connection requires a new credential",
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
const existingWorkspaceProviderKind = existing
|
|
1089
|
+
? workspaceProviderApiKeyConnectionKind(existing)
|
|
978
1090
|
: null;
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1091
|
+
if (existing && existingWorkspaceProviderKind) {
|
|
1092
|
+
const provider = workspaceProviderApiKeyConnectionSpec(existingWorkspaceProviderKind);
|
|
1093
|
+
const providerDomain = canonicalProviderDomain(
|
|
1094
|
+
payload.providerDomain ?? existing.providerDomain,
|
|
1095
|
+
);
|
|
1096
|
+
const subjectId =
|
|
1097
|
+
payload.subjectId === undefined
|
|
1098
|
+
? existing.subjectId
|
|
1099
|
+
: writableSubjectId(payload.subjectId, grant.subjectId);
|
|
1100
|
+
const kind = payload.kind ?? existing.kind;
|
|
1101
|
+
if (
|
|
1102
|
+
subjectId !== null ||
|
|
1103
|
+
providerDomain !== provider.providerDomain ||
|
|
1104
|
+
kind !== "api_key" ||
|
|
1105
|
+
(payload.metadata?.credentialRole !== undefined &&
|
|
1106
|
+
payload.metadata.credentialRole !== provider.credentialRole)
|
|
1107
|
+
) {
|
|
1108
|
+
throw new HTTPException(422, {
|
|
1109
|
+
message: `${provider.label} connection identity cannot be changed`,
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
if (payload.credential === undefined) {
|
|
1113
|
+
throw new HTTPException(400, {
|
|
1114
|
+
message: `updating a ${provider.label} connection requires a new credential`,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
if (payload.expectedVersion === undefined || payload.operationId === undefined) {
|
|
1118
|
+
throw new HTTPException(400, {
|
|
1119
|
+
message: `updating a ${provider.label} connection requires expectedVersion and operationId`,
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
const key = requireEnvironmentEncryption(settings);
|
|
1123
|
+
const grantedScopes = payload.grantedScopes ?? existing.grantedScopes;
|
|
1124
|
+
const expiresAt =
|
|
1125
|
+
payload.expiresAt !== undefined
|
|
1126
|
+
? payload.expiresAt
|
|
1127
|
+
? new Date(payload.expiresAt)
|
|
1128
|
+
: null
|
|
1129
|
+
: existing.expiresAt
|
|
1130
|
+
? new Date(existing.expiresAt)
|
|
1131
|
+
: null;
|
|
1132
|
+
const metadata = workspaceProviderCredentialMetadata(existingWorkspaceProviderKind, {
|
|
1133
|
+
...existing.metadata,
|
|
1134
|
+
...(payload.metadata ?? {}),
|
|
1135
|
+
});
|
|
1136
|
+
const input = {
|
|
1137
|
+
accountId: grant.accountId,
|
|
1138
|
+
workspaceId,
|
|
1139
|
+
connectionId: existing.id,
|
|
1140
|
+
expectedVersion: payload.expectedVersion,
|
|
1141
|
+
operationId: payload.operationId,
|
|
1142
|
+
requestDigest: workspaceProviderCredentialRequestDigest(key, {
|
|
1143
|
+
action: "rotate",
|
|
1144
|
+
connectionId: existing.id,
|
|
1145
|
+
expectedVersion: payload.expectedVersion,
|
|
1146
|
+
credential: payload.credential,
|
|
1147
|
+
grantedScopes,
|
|
1148
|
+
expiresAt: expiresAt?.toISOString() ?? null,
|
|
1149
|
+
metadata,
|
|
1150
|
+
}),
|
|
1151
|
+
credentialEncrypted: encryptCredentialBundle(key, payload.credential),
|
|
1152
|
+
grantedScopes,
|
|
1153
|
+
expiresAt,
|
|
1154
|
+
metadata,
|
|
1155
|
+
updatedBySubjectId: grant.subjectId,
|
|
1156
|
+
};
|
|
1157
|
+
const connection =
|
|
1158
|
+
existingWorkspaceProviderKind === "vercel_gateway"
|
|
1159
|
+
? await rotateWorkspaceVercelAiGatewayConnection(db, input)
|
|
1160
|
+
: await rotateWorkspaceOpenRouterConnection(db, input);
|
|
1161
|
+
if (!connection) {
|
|
1162
|
+
throw new HTTPException(409, {
|
|
1163
|
+
message: `${provider.label} connection changed; reload before replacing its key`,
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
return c.json(ConnectionResponse.parse({ connection }));
|
|
1167
|
+
}
|
|
1168
|
+
const key =
|
|
1169
|
+
payload.credential === undefined ? null : requireEnvironmentEncryption(settings);
|
|
1170
|
+
const subjectId =
|
|
1171
|
+
payload.subjectId === undefined
|
|
1172
|
+
? undefined
|
|
1173
|
+
: writableSubjectId(payload.subjectId, grant.subjectId);
|
|
1174
|
+
if (
|
|
1175
|
+
existing &&
|
|
1176
|
+
(payload.credential !== undefined ||
|
|
1177
|
+
(payload.providerDomain !== undefined &&
|
|
1178
|
+
canonicalProviderDomain(payload.providerDomain) !== existing.providerDomain) ||
|
|
1179
|
+
(payload.kind !== undefined && payload.kind !== existing.kind) ||
|
|
1180
|
+
(subjectId !== undefined && subjectId !== existing.subjectId) ||
|
|
1181
|
+
payload.grantedScopes?.some((scope) => !existing.grantedScopes.includes(scope)) ||
|
|
1182
|
+
(payload.metadata !== undefined &&
|
|
1183
|
+
["mcpUrl", "resource", "credentialRole"].some(
|
|
1184
|
+
(field) =>
|
|
1185
|
+
stableJson(payload.metadata?.[field] ?? null) !==
|
|
1186
|
+
stableJson(existing.metadata[field] ?? null),
|
|
1187
|
+
)) ||
|
|
1188
|
+
(payload.expiresAt !== undefined &&
|
|
1189
|
+
existing.expiresAt !== null &&
|
|
1190
|
+
(payload.expiresAt === null ||
|
|
1191
|
+
Date.parse(payload.expiresAt) > Date.parse(existing.expiresAt))) ||
|
|
1192
|
+
(payload.status === "active" && existing.status !== "active"))
|
|
1193
|
+
) {
|
|
1194
|
+
assertOrganizationIntegrationAllowed(policy, null);
|
|
1195
|
+
}
|
|
1196
|
+
const connection = await updateConnection(db, {
|
|
1197
|
+
workspaceId,
|
|
1198
|
+
connectionId: c.req.param("connectionId"),
|
|
1199
|
+
visibleToSubjectId: grant.subjectId,
|
|
1200
|
+
updatedBySubjectId: grant.subjectId,
|
|
1201
|
+
...(payload.providerDomain !== undefined
|
|
1202
|
+
? { providerDomain: canonicalProviderDomain(payload.providerDomain) }
|
|
1203
|
+
: {}),
|
|
1204
|
+
...(subjectId !== undefined ? { subjectId } : {}),
|
|
1205
|
+
...(payload.kind !== undefined ? { kind: payload.kind } : {}),
|
|
1206
|
+
...(payload.status !== undefined ? { status: payload.status } : {}),
|
|
1207
|
+
...(payload.credential !== undefined && key
|
|
1208
|
+
? { credentialEncrypted: encryptCredentialBundle(key, payload.credential) }
|
|
1209
|
+
: {}),
|
|
1210
|
+
...(payload.grantedScopes !== undefined
|
|
1211
|
+
? { grantedScopes: payload.grantedScopes }
|
|
1212
|
+
: {}),
|
|
1213
|
+
...(payload.expiresAt !== undefined
|
|
1214
|
+
? { expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null }
|
|
1215
|
+
: {}),
|
|
1216
|
+
...(payload.metadata !== undefined ? { metadata: payload.metadata } : {}),
|
|
1217
|
+
});
|
|
1218
|
+
if (!connection) {
|
|
1219
|
+
throw new HTTPException(404, { message: "connection not found" });
|
|
1220
|
+
}
|
|
1221
|
+
return c.json(ConnectionResponse.parse({ connection }));
|
|
997
1222
|
}),
|
|
998
|
-
|
|
999
|
-
grantedScopes,
|
|
1000
|
-
expiresAt,
|
|
1001
|
-
metadata,
|
|
1002
|
-
updatedBySubjectId: grant.subjectId,
|
|
1003
|
-
};
|
|
1004
|
-
const connection =
|
|
1005
|
-
existingWorkspaceProviderKind === "vercel_gateway"
|
|
1006
|
-
? await rotateWorkspaceVercelAiGatewayConnection(db, input)
|
|
1007
|
-
: await rotateWorkspaceOpenRouterConnection(db, input);
|
|
1008
|
-
if (!connection) {
|
|
1009
|
-
throw new HTTPException(409, {
|
|
1010
|
-
message: `${provider.label} connection changed; reload before replacing its key`,
|
|
1011
|
-
});
|
|
1012
|
-
}
|
|
1013
|
-
return c.json(ConnectionResponse.parse({ connection }));
|
|
1014
|
-
}
|
|
1015
|
-
const key = payload.credential === undefined ? null : requireEnvironmentEncryption(settings);
|
|
1016
|
-
const subjectId =
|
|
1017
|
-
payload.subjectId === undefined
|
|
1018
|
-
? undefined
|
|
1019
|
-
: writableSubjectId(payload.subjectId, grant.subjectId);
|
|
1020
|
-
const connection = await updateConnection(db, {
|
|
1021
|
-
workspaceId,
|
|
1022
|
-
connectionId: c.req.param("connectionId"),
|
|
1023
|
-
visibleToSubjectId: grant.subjectId,
|
|
1024
|
-
updatedBySubjectId: grant.subjectId,
|
|
1025
|
-
...(payload.providerDomain !== undefined
|
|
1026
|
-
? { providerDomain: canonicalProviderDomain(payload.providerDomain) }
|
|
1027
|
-
: {}),
|
|
1028
|
-
...(subjectId !== undefined ? { subjectId } : {}),
|
|
1029
|
-
...(payload.kind !== undefined ? { kind: payload.kind } : {}),
|
|
1030
|
-
...(payload.status !== undefined ? { status: payload.status } : {}),
|
|
1031
|
-
...(payload.credential !== undefined && key
|
|
1032
|
-
? { credentialEncrypted: encryptCredentialBundle(key, payload.credential) }
|
|
1033
|
-
: {}),
|
|
1034
|
-
...(payload.grantedScopes !== undefined ? { grantedScopes: payload.grantedScopes } : {}),
|
|
1035
|
-
...(payload.expiresAt !== undefined
|
|
1036
|
-
? { expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null }
|
|
1037
|
-
: {}),
|
|
1038
|
-
...(payload.metadata !== undefined ? { metadata: payload.metadata } : {}),
|
|
1039
|
-
});
|
|
1040
|
-
if (!connection) {
|
|
1041
|
-
throw new HTTPException(404, { message: "connection not found" });
|
|
1042
|
-
}
|
|
1043
|
-
return c.json(ConnectionResponse.parse({ connection }));
|
|
1223
|
+
);
|
|
1044
1224
|
});
|
|
1045
1225
|
|
|
1046
1226
|
app.delete("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
|