@opengeni/api-router 0.4.1 → 0.5.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/LICENSE +190 -0
- package/dist/app.js +1 -1
- package/dist/{chunk-2JL5OXRE.js → chunk-DQ5TIRDZ.js} +2205 -664
- package/dist/chunk-DQ5TIRDZ.js.map +1 -0
- package/dist/index.js +16 -6
- package/dist/index.js.map +1 -1
- package/package.json +12 -18
- package/src/app.ts +39 -6
- package/src/http/auth.ts +6 -0
- package/src/integrations/oauth-client.ts +899 -0
- package/src/mcp/documents.ts +124 -20
- package/src/mcp/server.ts +37 -2
- package/src/mcp/toolspace.ts +627 -0
- package/src/routes/connections.ts +179 -0
- package/src/routes/documents.ts +87 -3
- package/src/routes/enrollments.ts +1 -0
- package/src/sandbox/channel-a.ts +1 -1
- package/src/sandbox/machines.ts +2 -0
- package/src/sandbox/metrics-ingestion.ts +41 -10
- package/src/sandbox/viewer.ts +1 -1
- package/dist/chunk-2JL5OXRE.js.map +0 -1
|
@@ -12,8 +12,8 @@ import { createObjectStorage } from "@opengeni/storage";
|
|
|
12
12
|
import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
13
13
|
import { Hono } from "hono";
|
|
14
14
|
import { cors } from "hono/cors";
|
|
15
|
-
import { HTTPException as
|
|
16
|
-
import { requireAccessGrant as
|
|
15
|
+
import { HTTPException as HTTPException20 } from "hono/http-exception";
|
|
16
|
+
import { hasPermission as hasPermission4, requireAccessGrant as requireAccessGrant16, requirePermission } from "@opengeni/core";
|
|
17
17
|
|
|
18
18
|
// src/auth/managed-auth.ts
|
|
19
19
|
import { ensureManagedAccessForUser } from "@opengeni/db";
|
|
@@ -494,8 +494,9 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
|
|
|
494
494
|
});
|
|
495
495
|
const json = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
|
|
496
496
|
const can = (permission) => hasPermission(grant.permissions, permission);
|
|
497
|
+
const toolspaceMode = options.toolspace != null;
|
|
497
498
|
const sessionId = typeof grant.metadata?.["sessionId"] === "string" ? grant.metadata["sessionId"] : null;
|
|
498
|
-
if (sessionId !== null) {
|
|
499
|
+
if (sessionId !== null && (!toolspaceMode || can("sessions:control"))) {
|
|
499
500
|
server.registerTool("set_session_title", {
|
|
500
501
|
description: "Set this session's display title to a concise 3-7 word summary. Call once early to name the session; calling again replaces it unless a human has manually set the title.",
|
|
501
502
|
inputSchema: { title: z4.string().min(1).max(200) }
|
|
@@ -507,7 +508,7 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
|
|
|
507
508
|
if (sessionId !== null && can("goals:manage")) {
|
|
508
509
|
registerGoalTools(server, deps, grant, sessionId, json);
|
|
509
510
|
}
|
|
510
|
-
if (sessionId !== null && deps.settings.sandboxSelfhostedEnabled) {
|
|
511
|
+
if (!toolspaceMode && sessionId !== null && deps.settings.sandboxSelfhostedEnabled) {
|
|
511
512
|
registerFleetTools(server, deps, grant, sessionId, json);
|
|
512
513
|
}
|
|
513
514
|
registerWorkspaceOrchestrationTools(server, deps, grant, can, json);
|
|
@@ -518,225 +519,253 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
|
|
|
518
519
|
registerGitHubTokenTool(server, deps, grant, sessionId, json);
|
|
519
520
|
}
|
|
520
521
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const file = await requireFile(deps.db, grant.workspaceId, fileId);
|
|
529
|
-
if (file.status !== "ready") {
|
|
530
|
-
throw new Error(`file is ${file.status}`);
|
|
531
|
-
}
|
|
532
|
-
const signed = await deps.objectStorage.createGetUrl({ key: file.objectKey });
|
|
533
|
-
return json({
|
|
534
|
-
file: {
|
|
535
|
-
id: file.id,
|
|
536
|
-
filename: file.filename,
|
|
537
|
-
safeFilename: file.safeFilename,
|
|
538
|
-
contentType: file.contentType,
|
|
539
|
-
sizeBytes: file.sizeBytes,
|
|
540
|
-
sha256: file.sha256,
|
|
541
|
-
status: file.status,
|
|
542
|
-
createdAt: file.createdAt,
|
|
543
|
-
updatedAt: file.updatedAt
|
|
544
|
-
},
|
|
545
|
-
downloadUrl: {
|
|
546
|
-
url: signed.url,
|
|
547
|
-
expiresAt: signed.expiresAt.toISOString()
|
|
522
|
+
if (!toolspaceMode || can("files:read")) {
|
|
523
|
+
server.registerTool("files_get_download_url", {
|
|
524
|
+
description: "Create a short-lived download URL for a ready file asset.",
|
|
525
|
+
inputSchema: { fileId: z4.string().uuid() }
|
|
526
|
+
}, async ({ fileId }) => {
|
|
527
|
+
if (!deps.objectStorage) {
|
|
528
|
+
throw new Error("object storage is not configured");
|
|
548
529
|
}
|
|
530
|
+
const file = await requireFile(deps.db, grant.workspaceId, fileId);
|
|
531
|
+
if (file.status !== "ready") {
|
|
532
|
+
throw new Error(`file is ${file.status}`);
|
|
533
|
+
}
|
|
534
|
+
const signed = await deps.objectStorage.createGetUrl({ key: file.objectKey });
|
|
535
|
+
return json({
|
|
536
|
+
file: {
|
|
537
|
+
id: file.id,
|
|
538
|
+
filename: file.filename,
|
|
539
|
+
safeFilename: file.safeFilename,
|
|
540
|
+
contentType: file.contentType,
|
|
541
|
+
sizeBytes: file.sizeBytes,
|
|
542
|
+
sha256: file.sha256,
|
|
543
|
+
status: file.status,
|
|
544
|
+
createdAt: file.createdAt,
|
|
545
|
+
updatedAt: file.updatedAt
|
|
546
|
+
},
|
|
547
|
+
downloadUrl: {
|
|
548
|
+
url: signed.url,
|
|
549
|
+
expiresAt: signed.expiresAt.toISOString()
|
|
550
|
+
}
|
|
551
|
+
});
|
|
549
552
|
});
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
553
|
+
}
|
|
554
|
+
if (!toolspaceMode || can("github:use")) {
|
|
555
|
+
server.registerTool("github_repositories_list", {
|
|
556
|
+
description: "List GitHub App repositories available as scheduled task repository resources. Use the returned resource object in scheduled task agentConfig.resources.",
|
|
557
|
+
inputSchema: { limit: z4.number().int().positive().optional() }
|
|
558
|
+
}, async ({ limit }) => {
|
|
559
|
+
try {
|
|
560
|
+
const installationIds = await listGitHubInstallationIdsForWorkspace(deps.db, grant.workspaceId);
|
|
561
|
+
const repositories = await listGitHubAppRepositories(deps.settings, { installationIds });
|
|
562
|
+
const visible = typeof limit === "number" ? repositories.slice(0, limit) : repositories;
|
|
563
|
+
return json({ repositories: visible.map((repository) => repositoryWithScheduledTaskResource(repository)) });
|
|
564
|
+
} catch (error) {
|
|
565
|
+
if (error instanceof GitHubAppConfigurationError) {
|
|
566
|
+
throw new Error(`GitHub App is not configured: ${error.missing.join(", ")}`);
|
|
567
|
+
}
|
|
568
|
+
throw error;
|
|
563
569
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
if (!toolspaceMode || can("connections:read")) {
|
|
573
|
+
server.registerTool("social_connections_list", {
|
|
574
|
+
description: "List connected social media accounts available to social media analysis packs.",
|
|
575
|
+
inputSchema: { limit: z4.number().int().positive().optional() }
|
|
576
|
+
}, async ({ limit }) => json({ connections: await listSocialConnections(deps.db, grant.workspaceId, boundedMcpLimit(limit)) }));
|
|
577
|
+
server.registerTool("social_posts_recent", {
|
|
578
|
+
description: "List recent social media posts imported or synced into OpenGeni.",
|
|
579
|
+
inputSchema: {
|
|
580
|
+
connectionIds: z4.array(z4.string().uuid()).optional(),
|
|
581
|
+
since: z4.string().optional(),
|
|
582
|
+
windowHours: z4.number().int().positive().optional(),
|
|
583
|
+
limit: z4.number().int().positive().optional()
|
|
584
|
+
}
|
|
585
|
+
}, async ({ connectionIds, since, windowHours, limit }) => {
|
|
586
|
+
const sinceDate = since ? parseMcpDate(since, "since") : new Date(Date.now() - (windowHours ?? 24) * 60 * 60 * 1e3);
|
|
587
|
+
return json({
|
|
588
|
+
since: sinceDate.toISOString(),
|
|
589
|
+
posts: await listSocialPosts(deps.db, {
|
|
590
|
+
workspaceId: grant.workspaceId,
|
|
591
|
+
...connectionIds?.length ? { connectionIds } : {},
|
|
592
|
+
since: sinceDate,
|
|
593
|
+
limit: boundedMcpLimit(limit)
|
|
594
|
+
})
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
server.registerTool("social_daily_analysis_context", {
|
|
598
|
+
description: "Collect social account and recent post context for a daily marketing analysis run.",
|
|
599
|
+
inputSchema: {
|
|
600
|
+
connectionIds: z4.array(z4.string().uuid()).optional(),
|
|
601
|
+
documentBaseIds: z4.array(z4.string().uuid()).optional(),
|
|
602
|
+
since: z4.string().optional(),
|
|
603
|
+
windowHours: z4.number().int().positive().optional(),
|
|
604
|
+
limit: z4.number().int().positive().optional()
|
|
605
|
+
}
|
|
606
|
+
}, async ({ connectionIds, documentBaseIds, since, windowHours, limit }) => {
|
|
607
|
+
const allConnections = await listSocialConnections(deps.db, grant.workspaceId, 500);
|
|
608
|
+
const selectedIds = connectionIds && connectionIds.length > 0 ? new Set(connectionIds) : null;
|
|
609
|
+
const connections = selectedIds ? allConnections.filter((connection) => selectedIds.has(connection.id)) : allConnections.filter((connection) => connection.status === "connected");
|
|
610
|
+
if (selectedIds) {
|
|
611
|
+
const foundIds = new Set(connections.map((connection) => connection.id));
|
|
612
|
+
const missing = [...selectedIds].filter((id) => !foundIds.has(id));
|
|
613
|
+
if (missing.length > 0) {
|
|
614
|
+
throw new Error(`Unknown social connection IDs: ${missing.join(", ")}`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const sinceDate = since ? parseMcpDate(since, "since") : new Date(Date.now() - (windowHours ?? 24) * 60 * 60 * 1e3);
|
|
618
|
+
const posts = connections.length > 0 ? await listSocialPosts(deps.db, {
|
|
584
619
|
workspaceId: grant.workspaceId,
|
|
585
|
-
|
|
620
|
+
connectionIds: connections.map((connection) => connection.id),
|
|
586
621
|
since: sinceDate,
|
|
587
622
|
limit: boundedMcpLimit(limit)
|
|
588
|
-
})
|
|
623
|
+
}) : [];
|
|
624
|
+
return json({
|
|
625
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
626
|
+
window: {
|
|
627
|
+
since: sinceDate.toISOString(),
|
|
628
|
+
until: (/* @__PURE__ */ new Date()).toISOString()
|
|
629
|
+
},
|
|
630
|
+
documentBaseIds: documentBaseIds ?? [],
|
|
631
|
+
connections,
|
|
632
|
+
posts,
|
|
633
|
+
instructions: [
|
|
634
|
+
"Use docs MCP search tools for the supplied documentBaseIds when brand, campaign, or audience knowledge is needed.",
|
|
635
|
+
"Report data gaps explicitly when posts or metrics are missing.",
|
|
636
|
+
"Do not infer unpublished metrics or hidden platform data."
|
|
637
|
+
]
|
|
638
|
+
});
|
|
589
639
|
});
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
640
|
+
}
|
|
641
|
+
if (!toolspaceMode || can("scheduled_tasks:manage") || can("scheduled_tasks:run")) {
|
|
642
|
+
server.registerTool("scheduled_tasks_list", {
|
|
643
|
+
description: "List scheduled tasks.",
|
|
644
|
+
inputSchema: { limit: z4.number().int().positive().optional() }
|
|
645
|
+
}, async ({ limit }) => json({ tasks: await listScheduledTasks(deps.db, grant.workspaceId, limit ?? 100) }));
|
|
646
|
+
server.registerTool("scheduled_tasks_get", {
|
|
647
|
+
description: "Get one scheduled task.",
|
|
648
|
+
inputSchema: { id: z4.string().uuid() }
|
|
649
|
+
}, async ({ id }) => json(await requireScheduledTask(deps.db, grant.workspaceId, id)));
|
|
650
|
+
server.registerTool("scheduled_tasks_create", {
|
|
651
|
+
description: "Create a scheduled task.",
|
|
652
|
+
inputSchema: {
|
|
653
|
+
name: z4.string(),
|
|
654
|
+
schedule: z4.unknown(),
|
|
655
|
+
runMode: z4.string().optional(),
|
|
656
|
+
overlapPolicy: z4.string().optional(),
|
|
657
|
+
agentConfig: z4.unknown(),
|
|
658
|
+
status: z4.string().optional(),
|
|
659
|
+
environmentId: z4.string().uuid().optional(),
|
|
660
|
+
metadata: z4.record(z4.string(), z4.unknown()).optional()
|
|
609
661
|
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
workspaceId: grant.workspaceId,
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
}) : [];
|
|
618
|
-
return json({
|
|
619
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
620
|
-
window: {
|
|
621
|
-
since: sinceDate.toISOString(),
|
|
622
|
-
until: (/* @__PURE__ */ new Date()).toISOString()
|
|
623
|
-
},
|
|
624
|
-
documentBaseIds: documentBaseIds ?? [],
|
|
625
|
-
connections,
|
|
626
|
-
posts,
|
|
627
|
-
instructions: [
|
|
628
|
-
"Use docs MCP search tools for the supplied documentBaseIds when brand, campaign, or audience knowledge is needed.",
|
|
629
|
-
"Report data gaps explicitly when posts or metrics are missing.",
|
|
630
|
-
"Do not infer unpublished metrics or hidden platform data."
|
|
631
|
-
]
|
|
662
|
+
}, async (args) => {
|
|
663
|
+
const payload = CreateScheduledTaskRequest.parse(args);
|
|
664
|
+
requireEnvironmentsUseForMcpAttachment(grant, payload.environmentId);
|
|
665
|
+
await requireLimit(deps, { accountId: grant.accountId, workspaceId: grant.workspaceId, action: "schedule:create", quantity: 1 });
|
|
666
|
+
const task = await createValidatedScheduledTask({ settings: deps.settings, db: deps.db, objectStorage: deps.objectStorage, grant, payload, toolsProvided: scheduledTaskToolsProvided(args) });
|
|
667
|
+
await syncCreatedScheduledTask({ db: deps.db, workflowClient: deps.workflowClient, task });
|
|
668
|
+
return json(task);
|
|
632
669
|
});
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
inputSchema: { id: z4.string().uuid(), triggerId: z4.string().min(1).max(128).optional() }
|
|
705
|
-
}, async ({ id, triggerId }) => {
|
|
706
|
-
const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
707
|
-
await requireLimit(deps, { accountId: grant.accountId, workspaceId: grant.workspaceId, action: "agent_run:create", quantity: 1, model: task.agentConfig.model ?? deps.settings.openaiModel });
|
|
708
|
-
const triggerToken = scheduledTaskTriggerToken(triggerId);
|
|
709
|
-
const agentRunUsageIdempotencyKey = manualScheduledTaskTriggerUsageKey(grant.workspaceId, task.id, triggerToken);
|
|
710
|
-
const triggerWorkflowId = manualScheduledTaskTriggerWorkflowId(task.id, triggerToken);
|
|
711
|
-
await deps.workflowClient.triggerScheduledTask({ task, agentRunUsageIdempotencyKey, triggerWorkflowId });
|
|
712
|
-
await recordWorkspaceUsage(deps, {
|
|
713
|
-
accountId: grant.accountId,
|
|
714
|
-
workspaceId: grant.workspaceId,
|
|
715
|
-
subjectId: grant.subjectId,
|
|
716
|
-
eventType: "agent_run.created",
|
|
717
|
-
quantity: 1,
|
|
718
|
-
unit: "run",
|
|
719
|
-
sourceResourceType: "scheduled_task",
|
|
720
|
-
sourceResourceId: task.id,
|
|
721
|
-
idempotencyKey: agentRunUsageIdempotencyKey
|
|
670
|
+
server.registerTool("scheduled_tasks_update", {
|
|
671
|
+
description: "Update a scheduled task.",
|
|
672
|
+
inputSchema: {
|
|
673
|
+
id: z4.string().uuid(),
|
|
674
|
+
name: z4.string().optional(),
|
|
675
|
+
schedule: z4.unknown().optional(),
|
|
676
|
+
runMode: z4.string().optional(),
|
|
677
|
+
overlapPolicy: z4.string().optional(),
|
|
678
|
+
agentConfig: z4.unknown().optional(),
|
|
679
|
+
status: z4.string().optional(),
|
|
680
|
+
environmentId: z4.string().uuid().nullable().optional(),
|
|
681
|
+
metadata: z4.record(z4.string(), z4.unknown()).optional()
|
|
682
|
+
}
|
|
683
|
+
}, async ({ id, ...raw }) => {
|
|
684
|
+
const existing = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
685
|
+
const payload = UpdateScheduledTaskRequest.parse(raw);
|
|
686
|
+
requireEnvironmentsUseForMcpAttachment(grant, payload.environmentId);
|
|
687
|
+
const update = await validatedScheduledTaskUpdate({ settings: deps.settings, db: deps.db, objectStorage: deps.objectStorage, grant, existing, payload, toolsProvided: scheduledTaskToolsProvided(raw) });
|
|
688
|
+
const task = await updateScheduledTask(deps.db, grant.workspaceId, id, update);
|
|
689
|
+
await syncUpdatedScheduledTask({ db: deps.db, workflowClient: deps.workflowClient, previous: existing, task });
|
|
690
|
+
return json(task);
|
|
691
|
+
});
|
|
692
|
+
server.registerTool("scheduled_tasks_pause", {
|
|
693
|
+
description: "Pause a scheduled task.",
|
|
694
|
+
inputSchema: { id: z4.string().uuid() }
|
|
695
|
+
}, async ({ id }) => {
|
|
696
|
+
const existing = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
697
|
+
const task = await updateScheduledTask(deps.db, grant.workspaceId, id, { status: "paused" });
|
|
698
|
+
await syncUpdatedScheduledTask({ db: deps.db, workflowClient: deps.workflowClient, previous: existing, task });
|
|
699
|
+
return json(task);
|
|
700
|
+
});
|
|
701
|
+
server.registerTool("scheduled_tasks_resume", {
|
|
702
|
+
description: "Resume a scheduled task.",
|
|
703
|
+
inputSchema: { id: z4.string().uuid() }
|
|
704
|
+
}, async ({ id }) => {
|
|
705
|
+
const existing = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
706
|
+
const task = await updateScheduledTask(deps.db, grant.workspaceId, id, { status: "active" });
|
|
707
|
+
await syncUpdatedScheduledTask({ db: deps.db, workflowClient: deps.workflowClient, previous: existing, task });
|
|
708
|
+
return json(task);
|
|
709
|
+
});
|
|
710
|
+
server.registerTool("scheduled_tasks_trigger", {
|
|
711
|
+
description: "Trigger a scheduled task immediately. Pass a stable triggerId to make a retried trigger idempotent (one charge, one run).",
|
|
712
|
+
inputSchema: { id: z4.string().uuid(), triggerId: z4.string().min(1).max(128).optional() }
|
|
713
|
+
}, async ({ id, triggerId }) => {
|
|
714
|
+
const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
715
|
+
await requireLimit(deps, { accountId: grant.accountId, workspaceId: grant.workspaceId, action: "agent_run:create", quantity: 1, model: task.agentConfig.model ?? deps.settings.openaiModel });
|
|
716
|
+
const triggerToken = scheduledTaskTriggerToken(triggerId);
|
|
717
|
+
const agentRunUsageIdempotencyKey = manualScheduledTaskTriggerUsageKey(grant.workspaceId, task.id, triggerToken);
|
|
718
|
+
const triggerWorkflowId = manualScheduledTaskTriggerWorkflowId(task.id, triggerToken);
|
|
719
|
+
await deps.workflowClient.triggerScheduledTask({ task, agentRunUsageIdempotencyKey, triggerWorkflowId });
|
|
720
|
+
await recordWorkspaceUsage(deps, {
|
|
721
|
+
accountId: grant.accountId,
|
|
722
|
+
workspaceId: grant.workspaceId,
|
|
723
|
+
subjectId: grant.subjectId,
|
|
724
|
+
eventType: "agent_run.created",
|
|
725
|
+
quantity: 1,
|
|
726
|
+
unit: "run",
|
|
727
|
+
sourceResourceType: "scheduled_task",
|
|
728
|
+
sourceResourceId: task.id,
|
|
729
|
+
idempotencyKey: agentRunUsageIdempotencyKey
|
|
730
|
+
});
|
|
731
|
+
return json(task);
|
|
732
|
+
});
|
|
733
|
+
server.registerTool("scheduled_tasks_delete", {
|
|
734
|
+
description: "Delete a scheduled task.",
|
|
735
|
+
inputSchema: { id: z4.string().uuid() }
|
|
736
|
+
}, async ({ id }) => {
|
|
737
|
+
const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
738
|
+
await deps.workflowClient.deleteScheduledTaskSchedule({ temporalScheduleId: task.temporalScheduleId });
|
|
739
|
+
await deleteScheduledTask(deps.db, grant.workspaceId, id);
|
|
740
|
+
return json({ ok: true });
|
|
722
741
|
});
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
|
|
730
|
-
await deps.workflowClient.deleteScheduledTaskSchedule({ temporalScheduleId: task.temporalScheduleId });
|
|
731
|
-
await deleteScheduledTask(deps.db, grant.workspaceId, id);
|
|
732
|
-
return json({ ok: true });
|
|
733
|
-
});
|
|
734
|
-
server.registerTool("scheduled_task_runs_list", {
|
|
735
|
-
description: "List runs for a scheduled task.",
|
|
736
|
-
inputSchema: { taskId: z4.string().uuid(), limit: z4.number().int().positive().optional() }
|
|
737
|
-
}, async ({ taskId, limit }) => json({ runs: await listScheduledTaskRuns(deps.db, grant.workspaceId, taskId, limit ?? 100) }));
|
|
742
|
+
server.registerTool("scheduled_task_runs_list", {
|
|
743
|
+
description: "List runs for a scheduled task.",
|
|
744
|
+
inputSchema: { taskId: z4.string().uuid(), limit: z4.number().int().positive().optional() }
|
|
745
|
+
}, async ({ taskId, limit }) => json({ runs: await listScheduledTaskRuns(deps.db, grant.workspaceId, taskId, limit ?? 100) }));
|
|
746
|
+
}
|
|
747
|
+
registerToolspaceProxyTools(server, options.toolspace ?? null);
|
|
738
748
|
return server;
|
|
739
749
|
}
|
|
750
|
+
function registerToolspaceProxyTools(server, surface) {
|
|
751
|
+
if (!surface) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
for (const tool of surface.tools) {
|
|
755
|
+
server.registerTool(tool.name, {
|
|
756
|
+
...tool.description ? { description: tool.description } : {},
|
|
757
|
+
inputSchema: z4.object({}).passthrough(),
|
|
758
|
+
_meta: {
|
|
759
|
+
opengeni: {
|
|
760
|
+
origin: "toolspace",
|
|
761
|
+
subjectId: surface.subjectId,
|
|
762
|
+
sessionId: surface.sessionId,
|
|
763
|
+
...tool.inputSchema ? { inputSchema: tool.inputSchema } : {}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}, async (args) => await tool.call(args));
|
|
767
|
+
}
|
|
768
|
+
}
|
|
740
769
|
function registerGoalTools(server, deps, grant, sessionId, json) {
|
|
741
770
|
server.registerTool("goal_set", {
|
|
742
771
|
description: "Set or replace this session's goal. While a goal is active the session keeps working: idle moments synthesize continuation turns until goal_complete or goal_pause is called. Replacing a goal reactivates it and resets the continuation budget.",
|
|
@@ -1216,6 +1245,448 @@ function parseMcpDate(raw, label) {
|
|
|
1216
1245
|
return date;
|
|
1217
1246
|
}
|
|
1218
1247
|
|
|
1248
|
+
// src/mcp/toolspace.ts
|
|
1249
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1250
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
1251
|
+
import { environmentsEncryptionKeyBytes } from "@opengeni/config";
|
|
1252
|
+
import { prefixedMcpToolName } from "@opengeni/contracts";
|
|
1253
|
+
import { hasPermission as hasPermission2, settingsWithEnabledCapabilityMcpServers } from "@opengeni/core";
|
|
1254
|
+
import {
|
|
1255
|
+
buildConnectionTokenResolver,
|
|
1256
|
+
listSessionMcpServerMetadata,
|
|
1257
|
+
listSessionMcpServersForRun,
|
|
1258
|
+
requireSession as requireSession2,
|
|
1259
|
+
reserveToolspaceCallForTurn
|
|
1260
|
+
} from "@opengeni/db";
|
|
1261
|
+
import { appendAndPublishEvents as appendAndPublishEvents2 } from "@opengeni/events";
|
|
1262
|
+
var APPROVAL_REQUIRED_MESSAGE = "requires approval - invoke via the agent";
|
|
1263
|
+
var TOOLSPACE_AUTH_NEEDED_ERROR_CODE = -32001;
|
|
1264
|
+
var TOOLSPACE_AUTH_NEEDED_MESSAGE = "Authentication required - a connection link was posted to the session.";
|
|
1265
|
+
var TOOLSPACE_NO_ACTIVE_TURN_MESSAGE = "no active turn - toolspace calls require an in-flight turn";
|
|
1266
|
+
var FIRST_PARTY_PROXY_IDS = /* @__PURE__ */ new Set(["files", "docs"]);
|
|
1267
|
+
var TOOLSPACE_TOOL_LIST_TTL_MS = 3e4;
|
|
1268
|
+
var TOOLSPACE_TOOL_LIST_CACHE_MAX_ENTRIES = 2e3;
|
|
1269
|
+
var toolListCache = /* @__PURE__ */ new Map();
|
|
1270
|
+
function isToolspaceGrant(settings, grant) {
|
|
1271
|
+
return settings.toolspaceEnabled && hasPermission2(grant.permissions, "toolspace:call") && typeof grant.metadata?.sessionId === "string";
|
|
1272
|
+
}
|
|
1273
|
+
async function prepareToolspaceMcpSurface(input) {
|
|
1274
|
+
const { deps, grant } = input;
|
|
1275
|
+
if (!isToolspaceGrant(deps.settings, grant)) {
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
const sessionId = grant.metadata.sessionId;
|
|
1279
|
+
const session = await requireSession2(deps.db, grant.workspaceId, sessionId);
|
|
1280
|
+
const selectedIds = selectedMcpServerIds(session.tools, session.mcpServers.map((server) => server.id));
|
|
1281
|
+
const proxyableIds = [...selectedIds].filter((id) => toolspaceCanProxyServerId(id));
|
|
1282
|
+
if (proxyableIds.length === 0) {
|
|
1283
|
+
return emptyToolspaceSurface(sessionId, grant.subjectId);
|
|
1284
|
+
}
|
|
1285
|
+
let registryPromise = null;
|
|
1286
|
+
const getRegistry = () => registryPromise ??= buildToolspaceRegistry(deps, grant.workspaceId, sessionId);
|
|
1287
|
+
const listing = await resolveToolListing({
|
|
1288
|
+
deps,
|
|
1289
|
+
grant,
|
|
1290
|
+
sessionId,
|
|
1291
|
+
proxyableIds,
|
|
1292
|
+
activeTurnId: session.activeTurnId ?? null,
|
|
1293
|
+
getRegistry
|
|
1294
|
+
});
|
|
1295
|
+
const tools = listing.map((entry) => toolspaceToolFor({ deps, grant, sessionId, entry, getRegistry }));
|
|
1296
|
+
return {
|
|
1297
|
+
sessionId,
|
|
1298
|
+
subjectId: grant.subjectId,
|
|
1299
|
+
tools,
|
|
1300
|
+
// Connections are opened lazily and closed inline (per listing pass, per
|
|
1301
|
+
// call), so there is nothing persistent to tear down here.
|
|
1302
|
+
close: async () => {
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
function emptyToolspaceSurface(sessionId, subjectId) {
|
|
1307
|
+
return { sessionId, subjectId, tools: [], close: async () => {
|
|
1308
|
+
} };
|
|
1309
|
+
}
|
|
1310
|
+
async function buildToolspaceRegistry(deps, workspaceId, sessionId) {
|
|
1311
|
+
const runtimeSettings = await settingsWithEnabledCapabilityMcpServers(deps.db, workspaceId, deps.settings);
|
|
1312
|
+
const withSessionServers = await settingsWithSessionMcpServersForToolspace(deps, workspaceId, sessionId, runtimeSettings);
|
|
1313
|
+
return new Map(withSessionServers.mcpServers.map((server) => [server.id, server]));
|
|
1314
|
+
}
|
|
1315
|
+
async function resolveToolListing(input) {
|
|
1316
|
+
const { deps, grant, sessionId, proxyableIds, activeTurnId, getRegistry } = input;
|
|
1317
|
+
const cacheKey = await toolListCacheKey(deps, grant.workspaceId, sessionId, proxyableIds);
|
|
1318
|
+
const cached = readToolListCache(cacheKey);
|
|
1319
|
+
if (cached) {
|
|
1320
|
+
return cached;
|
|
1321
|
+
}
|
|
1322
|
+
if (!activeTurnId) {
|
|
1323
|
+
return [];
|
|
1324
|
+
}
|
|
1325
|
+
const registry = await getRegistry();
|
|
1326
|
+
const entries = [];
|
|
1327
|
+
for (const serverId of proxyableIds) {
|
|
1328
|
+
const config = registry.get(serverId);
|
|
1329
|
+
if (!config || !toolspaceCanProxyServer(config)) {
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
const connection = await connectToolspaceServer({ deps, grant, config, sessionId }).catch(() => null);
|
|
1333
|
+
if (!connection) {
|
|
1334
|
+
continue;
|
|
1335
|
+
}
|
|
1336
|
+
try {
|
|
1337
|
+
const listed = await connection.client.listTools(void 0, toolspaceRequestOptions(config)).catch(() => ({ tools: [] }));
|
|
1338
|
+
for (const tool of listed.tools) {
|
|
1339
|
+
if (!tool?.name || !allowedByConfig(config, tool.name)) {
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
entries.push({ serverId, tool, requireApproval: config.requireApproval });
|
|
1343
|
+
}
|
|
1344
|
+
} finally {
|
|
1345
|
+
await connection.close();
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
writeToolListCache(cacheKey, entries);
|
|
1349
|
+
return entries;
|
|
1350
|
+
}
|
|
1351
|
+
async function toolListCacheKey(deps, workspaceId, sessionId, proxyableIds) {
|
|
1352
|
+
const metadata = await listSessionMcpServerMetadata(deps.db, workspaceId, sessionId);
|
|
1353
|
+
const versions = new Map(metadata.map((server) => [server.id, server.credentialVersion]));
|
|
1354
|
+
const signature = proxyableIds.slice().sort().map((id) => `${id}@${versions.get(id) ?? 0}`).join(",");
|
|
1355
|
+
return `${workspaceId}:${sessionId}:${signature}`;
|
|
1356
|
+
}
|
|
1357
|
+
function readToolListCache(key) {
|
|
1358
|
+
const hit = toolListCache.get(key);
|
|
1359
|
+
if (!hit) {
|
|
1360
|
+
return null;
|
|
1361
|
+
}
|
|
1362
|
+
if (hit.expiresAt <= Date.now()) {
|
|
1363
|
+
toolListCache.delete(key);
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1366
|
+
return hit.entries;
|
|
1367
|
+
}
|
|
1368
|
+
function writeToolListCache(key, entries) {
|
|
1369
|
+
if (toolListCache.size >= TOOLSPACE_TOOL_LIST_CACHE_MAX_ENTRIES) {
|
|
1370
|
+
const now = Date.now();
|
|
1371
|
+
for (const [existingKey, value] of toolListCache) {
|
|
1372
|
+
if (value.expiresAt <= now) {
|
|
1373
|
+
toolListCache.delete(existingKey);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
if (toolListCache.size >= TOOLSPACE_TOOL_LIST_CACHE_MAX_ENTRIES) {
|
|
1377
|
+
toolListCache.clear();
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
toolListCache.set(key, { expiresAt: Date.now() + TOOLSPACE_TOOL_LIST_TTL_MS, entries });
|
|
1381
|
+
}
|
|
1382
|
+
async function settingsWithSessionMcpServersForToolspace(deps, workspaceId, sessionId, settings) {
|
|
1383
|
+
const encryptionKey = environmentsEncryptionKeyBytes(settings);
|
|
1384
|
+
if (!encryptionKey) {
|
|
1385
|
+
const metadata = await listSessionMcpServerMetadata(deps.db, workspaceId, sessionId);
|
|
1386
|
+
if (metadata.length === 0) {
|
|
1387
|
+
return settings;
|
|
1388
|
+
}
|
|
1389
|
+
throw new Error("session MCP server credentials require OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY");
|
|
1390
|
+
}
|
|
1391
|
+
const servers = await listSessionMcpServersForRun(deps.db, workspaceId, sessionId, encryptionKey);
|
|
1392
|
+
if (servers.length === 0) {
|
|
1393
|
+
return settings;
|
|
1394
|
+
}
|
|
1395
|
+
const sessionIds = new Set(servers.map((server) => server.id));
|
|
1396
|
+
return {
|
|
1397
|
+
...settings,
|
|
1398
|
+
mcpServers: [
|
|
1399
|
+
...settings.mcpServers.filter((server) => !sessionIds.has(server.id)),
|
|
1400
|
+
...servers.map((server) => ({
|
|
1401
|
+
id: server.id,
|
|
1402
|
+
...server.name ? { name: server.name } : {},
|
|
1403
|
+
url: server.url,
|
|
1404
|
+
...server.allowedTools ? { allowedTools: server.allowedTools } : {},
|
|
1405
|
+
...server.timeoutMs ? { timeoutMs: server.timeoutMs } : {},
|
|
1406
|
+
cacheToolsList: server.cacheToolsList ?? false,
|
|
1407
|
+
...server.requireApproval !== void 0 ? { requireApproval: server.requireApproval } : {},
|
|
1408
|
+
headers: server.headers
|
|
1409
|
+
}))
|
|
1410
|
+
]
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
async function connectToolspaceServer(input) {
|
|
1414
|
+
const baseFetch = input.config.connectionRef ? connectionBrokerFetch(globalThis.fetch, input) : globalThis.fetch;
|
|
1415
|
+
const client = new Client({ name: `opengeni-toolspace-${input.config.id}`, version: "1.0.0" }, { capabilities: {} });
|
|
1416
|
+
const transport = new StreamableHTTPClientTransport(new URL(input.config.url), {
|
|
1417
|
+
...baseFetch !== globalThis.fetch ? { fetch: baseFetch } : {},
|
|
1418
|
+
requestInit: {
|
|
1419
|
+
headers: toolspaceServerHeaders(input.config)
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
await client.connect(transport, toolspaceRequestOptions(input.config));
|
|
1423
|
+
return {
|
|
1424
|
+
config: input.config,
|
|
1425
|
+
client,
|
|
1426
|
+
close: async () => {
|
|
1427
|
+
await client.close().catch(() => void 0);
|
|
1428
|
+
}
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
function toolspaceToolFor(input) {
|
|
1432
|
+
const { deps, grant, sessionId, entry, getRegistry } = input;
|
|
1433
|
+
const { serverId, tool } = entry;
|
|
1434
|
+
const name = prefixedMcpToolName(serverId, tool.name);
|
|
1435
|
+
const approvalRequired = mcpToolRequiresApproval(entry.requireApproval, tool.name);
|
|
1436
|
+
const description = approvalRequired ? `${tool.description ?? tool.name} (unavailable: ${APPROVAL_REQUIRED_MESSAGE})` : tool.description;
|
|
1437
|
+
return {
|
|
1438
|
+
name,
|
|
1439
|
+
...description ? { description } : {},
|
|
1440
|
+
...tool.inputSchema ? { inputSchema: tool.inputSchema } : {},
|
|
1441
|
+
call: async (args) => {
|
|
1442
|
+
if (approvalRequired) {
|
|
1443
|
+
return mcpError(APPROVAL_REQUIRED_MESSAGE);
|
|
1444
|
+
}
|
|
1445
|
+
const reservation = await reserveActiveTurnCall(deps, grant.workspaceId, sessionId);
|
|
1446
|
+
if (reservation.status === "no_active_turn") {
|
|
1447
|
+
return mcpError(TOOLSPACE_NO_ACTIVE_TURN_MESSAGE);
|
|
1448
|
+
}
|
|
1449
|
+
if (reservation.status === "budget_exhausted") {
|
|
1450
|
+
return mcpError(`toolspace call budget exhausted (${deps.settings.toolspaceMaxCallsPerTurn}/turn)`);
|
|
1451
|
+
}
|
|
1452
|
+
const turnId = reservation.turnId;
|
|
1453
|
+
const registry = await getRegistry();
|
|
1454
|
+
const config = registry.get(serverId);
|
|
1455
|
+
if (!config || !toolspaceCanProxyServer(config) || !allowedByConfig(config, tool.name)) {
|
|
1456
|
+
return mcpError(`upstream tool failed: ${name}`);
|
|
1457
|
+
}
|
|
1458
|
+
if (mcpToolRequiresApproval(config.requireApproval, tool.name)) {
|
|
1459
|
+
return mcpError(APPROVAL_REQUIRED_MESSAGE);
|
|
1460
|
+
}
|
|
1461
|
+
const connection = await connectToolspaceServer({ deps, grant, config, sessionId }).catch(() => null);
|
|
1462
|
+
if (!connection) {
|
|
1463
|
+
return mcpError(`upstream tool failed: ${name}`);
|
|
1464
|
+
}
|
|
1465
|
+
try {
|
|
1466
|
+
const callId = crypto.randomUUID();
|
|
1467
|
+
await appendAndPublishEvents2(deps.db, deps.bus, grant.workspaceId, sessionId, [{
|
|
1468
|
+
type: "agent.toolCall.created",
|
|
1469
|
+
turnId,
|
|
1470
|
+
producerId: grant.subjectId,
|
|
1471
|
+
payload: {
|
|
1472
|
+
id: callId,
|
|
1473
|
+
name,
|
|
1474
|
+
arguments: args,
|
|
1475
|
+
origin: "toolspace",
|
|
1476
|
+
subjectId: grant.subjectId,
|
|
1477
|
+
raw: {
|
|
1478
|
+
type: "toolspace_call",
|
|
1479
|
+
serverId,
|
|
1480
|
+
toolName: tool.name
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}]);
|
|
1484
|
+
const output = await callRemoteTool(deps, connection, tool.name, args);
|
|
1485
|
+
await appendAndPublishEvents2(deps.db, deps.bus, grant.workspaceId, sessionId, [{
|
|
1486
|
+
type: "agent.toolCall.output",
|
|
1487
|
+
turnId,
|
|
1488
|
+
producerId: grant.subjectId,
|
|
1489
|
+
payload: {
|
|
1490
|
+
id: callId,
|
|
1491
|
+
output,
|
|
1492
|
+
origin: "toolspace",
|
|
1493
|
+
subjectId: grant.subjectId
|
|
1494
|
+
}
|
|
1495
|
+
}]);
|
|
1496
|
+
return output;
|
|
1497
|
+
} finally {
|
|
1498
|
+
await connection.close();
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
async function callRemoteTool(deps, server, toolName, args) {
|
|
1504
|
+
try {
|
|
1505
|
+
return await server.client.callTool({
|
|
1506
|
+
name: toolName,
|
|
1507
|
+
arguments: args
|
|
1508
|
+
}, void 0, toolspaceRequestOptions(server.config));
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
if (isToolspaceAuthNeededError(error)) {
|
|
1511
|
+
return mcpError(TOOLSPACE_AUTH_NEEDED_MESSAGE);
|
|
1512
|
+
}
|
|
1513
|
+
deps.observability?.warn("toolspace upstream tool call failed", {
|
|
1514
|
+
serverId: server.config.id,
|
|
1515
|
+
toolName,
|
|
1516
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1517
|
+
});
|
|
1518
|
+
return mcpError(`upstream tool failed: ${prefixedMcpToolName(server.config.id, toolName)}`);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
async function reserveActiveTurnCall(deps, workspaceId, sessionId) {
|
|
1522
|
+
const session = await requireSession2(deps.db, workspaceId, sessionId);
|
|
1523
|
+
if (!session.activeTurnId) {
|
|
1524
|
+
return { status: "no_active_turn" };
|
|
1525
|
+
}
|
|
1526
|
+
const reservation = await reserveToolspaceCallForTurn(
|
|
1527
|
+
deps.db,
|
|
1528
|
+
workspaceId,
|
|
1529
|
+
sessionId,
|
|
1530
|
+
session.activeTurnId,
|
|
1531
|
+
deps.settings.toolspaceMaxCallsPerTurn
|
|
1532
|
+
);
|
|
1533
|
+
return reservation.reserved ? { status: "ok", turnId: session.activeTurnId } : { status: "budget_exhausted" };
|
|
1534
|
+
}
|
|
1535
|
+
function selectedMcpServerIds(tools, sessionServerIds) {
|
|
1536
|
+
const out = new Set(sessionServerIds);
|
|
1537
|
+
for (const tool of tools) {
|
|
1538
|
+
if (tool.kind === "mcp") {
|
|
1539
|
+
out.add(tool.id);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
return out;
|
|
1543
|
+
}
|
|
1544
|
+
function toolspaceCanProxyServerId(serverId) {
|
|
1545
|
+
return serverId !== "opengeni" && !FIRST_PARTY_PROXY_IDS.has(serverId);
|
|
1546
|
+
}
|
|
1547
|
+
function toolspaceCanProxyServer(config) {
|
|
1548
|
+
return toolspaceCanProxyServerId(config.id);
|
|
1549
|
+
}
|
|
1550
|
+
function toolspaceServerHeaders(config) {
|
|
1551
|
+
const headers = {};
|
|
1552
|
+
for (const [name, value] of Object.entries(config.headers ?? {})) {
|
|
1553
|
+
headers[name] = value;
|
|
1554
|
+
}
|
|
1555
|
+
return headers;
|
|
1556
|
+
}
|
|
1557
|
+
function allowedByConfig(config, toolName) {
|
|
1558
|
+
return !config.allowedTools || config.allowedTools.includes(toolName);
|
|
1559
|
+
}
|
|
1560
|
+
function mcpToolRequiresApproval(policy, unprefixedName) {
|
|
1561
|
+
if (policy === true) {
|
|
1562
|
+
return true;
|
|
1563
|
+
}
|
|
1564
|
+
return Array.isArray(policy) && policy.includes(unprefixedName);
|
|
1565
|
+
}
|
|
1566
|
+
function mcpError(message) {
|
|
1567
|
+
return {
|
|
1568
|
+
isError: true,
|
|
1569
|
+
content: [{ type: "text", text: message }]
|
|
1570
|
+
};
|
|
1571
|
+
}
|
|
1572
|
+
function toolspaceRequestOptions(config) {
|
|
1573
|
+
return config.timeoutMs ? { timeout: config.timeoutMs, maxTotalTimeout: config.timeoutMs } : {};
|
|
1574
|
+
}
|
|
1575
|
+
function connectionBrokerFetch(baseFetch, input) {
|
|
1576
|
+
const connectionRef = input.config.connectionRef;
|
|
1577
|
+
if (!connectionRef) {
|
|
1578
|
+
return baseFetch;
|
|
1579
|
+
}
|
|
1580
|
+
const resolveCredential = buildConnectionTokenResolver(input.deps.db, input.deps.settings);
|
|
1581
|
+
return async (requestInput, init) => {
|
|
1582
|
+
const request = await mcpRequestInfo(requestInput, init);
|
|
1583
|
+
const first = await resolveCredential({
|
|
1584
|
+
workspaceId: input.grant.workspaceId,
|
|
1585
|
+
serverId: input.config.id,
|
|
1586
|
+
connectionRef,
|
|
1587
|
+
forceRefresh: false,
|
|
1588
|
+
...request.toolName ? { toolId: request.toolName } : {},
|
|
1589
|
+
subjectId: input.grant.subjectId
|
|
1590
|
+
});
|
|
1591
|
+
if (first.status === "auth_needed") {
|
|
1592
|
+
return await authNeededFetchResponse(input, request, first);
|
|
1593
|
+
}
|
|
1594
|
+
const response = await baseFetch(fetchInputForAttempt(requestInput), withConnectionHeaders(requestInput, init, first.headers));
|
|
1595
|
+
if (response.status === 401) {
|
|
1596
|
+
const refreshed = await resolveCredential({
|
|
1597
|
+
workspaceId: input.grant.workspaceId,
|
|
1598
|
+
serverId: input.config.id,
|
|
1599
|
+
connectionRef,
|
|
1600
|
+
forceRefresh: true,
|
|
1601
|
+
...request.toolName ? { toolId: request.toolName } : {},
|
|
1602
|
+
subjectId: input.grant.subjectId
|
|
1603
|
+
});
|
|
1604
|
+
if (refreshed.status === "auth_needed") {
|
|
1605
|
+
return await authNeededFetchResponse(input, request, refreshed);
|
|
1606
|
+
}
|
|
1607
|
+
return await baseFetch(fetchInputForAttempt(requestInput), withConnectionHeaders(requestInput, init, refreshed.headers));
|
|
1608
|
+
}
|
|
1609
|
+
if (response.status === 403) {
|
|
1610
|
+
return await authNeededFetchResponse(input, request, authNeededFromStatus(input.config, first, "insufficient_scope"));
|
|
1611
|
+
}
|
|
1612
|
+
return response;
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
function authNeededFromStatus(config, first, reason) {
|
|
1616
|
+
const connectionRef = config.connectionRef;
|
|
1617
|
+
return {
|
|
1618
|
+
status: "auth_needed",
|
|
1619
|
+
reason,
|
|
1620
|
+
providerDomain: connectionRef.providerDomain,
|
|
1621
|
+
connectionId: first.connectionId,
|
|
1622
|
+
...connectionRef.scopes ? { scopes: connectionRef.scopes } : {},
|
|
1623
|
+
...connectionRef.resource ? { resource: connectionRef.resource } : {}
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
async function authNeededFetchResponse(input, request, auth) {
|
|
1627
|
+
await appendAndPublishEvents2(input.deps.db, input.deps.bus, input.grant.workspaceId, input.sessionId, [{
|
|
1628
|
+
type: "tool.auth_needed",
|
|
1629
|
+
producerId: input.grant.subjectId,
|
|
1630
|
+
payload: {
|
|
1631
|
+
serverId: input.config.id,
|
|
1632
|
+
toolName: request.toolName ?? null,
|
|
1633
|
+
providerDomain: auth.providerDomain,
|
|
1634
|
+
reason: auth.reason,
|
|
1635
|
+
...auth.connectionId ? { connectionId: auth.connectionId } : {},
|
|
1636
|
+
...auth.scopes ? { scopes: auth.scopes } : {},
|
|
1637
|
+
...auth.resource ? { resource: auth.resource } : {},
|
|
1638
|
+
...auth.authorizationUrl ? { authorizationUrl: auth.authorizationUrl } : {},
|
|
1639
|
+
subjectId: input.grant.subjectId
|
|
1640
|
+
}
|
|
1641
|
+
}]).catch(() => void 0);
|
|
1642
|
+
if (request.method === "tools/call") {
|
|
1643
|
+
return new Response(JSON.stringify({
|
|
1644
|
+
jsonrpc: "2.0",
|
|
1645
|
+
id: request.id ?? null,
|
|
1646
|
+
error: {
|
|
1647
|
+
code: TOOLSPACE_AUTH_NEEDED_ERROR_CODE,
|
|
1648
|
+
message: TOOLSPACE_AUTH_NEEDED_MESSAGE
|
|
1649
|
+
}
|
|
1650
|
+
}), {
|
|
1651
|
+
status: 200,
|
|
1652
|
+
headers: { "content-type": "application/json" }
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
return new Response("Authentication required for MCP server connection", { status: 401 });
|
|
1656
|
+
}
|
|
1657
|
+
async function mcpRequestInfo(_input, init) {
|
|
1658
|
+
const body = typeof init?.body === "string" ? init.body : "";
|
|
1659
|
+
if (!body) {
|
|
1660
|
+
return {};
|
|
1661
|
+
}
|
|
1662
|
+
try {
|
|
1663
|
+
const parsed = JSON.parse(body);
|
|
1664
|
+
const method = typeof parsed.method === "string" ? parsed.method : void 0;
|
|
1665
|
+
const id = typeof parsed.id === "string" || typeof parsed.id === "number" || parsed.id === null ? parsed.id : void 0;
|
|
1666
|
+
const toolName = method === "tools/call" && typeof parsed.params?.name === "string" ? parsed.params.name : void 0;
|
|
1667
|
+
return {
|
|
1668
|
+
...method ? { method } : {},
|
|
1669
|
+
...id !== void 0 ? { id } : {},
|
|
1670
|
+
...toolName ? { toolName } : {}
|
|
1671
|
+
};
|
|
1672
|
+
} catch {
|
|
1673
|
+
return {};
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
function withConnectionHeaders(_input, init, authHeaders) {
|
|
1677
|
+
const headers = new Headers(init?.headers);
|
|
1678
|
+
for (const [name, value] of Object.entries(authHeaders)) {
|
|
1679
|
+
headers.set(name, value);
|
|
1680
|
+
}
|
|
1681
|
+
return { ...init, headers };
|
|
1682
|
+
}
|
|
1683
|
+
function fetchInputForAttempt(input) {
|
|
1684
|
+
return input;
|
|
1685
|
+
}
|
|
1686
|
+
function isToolspaceAuthNeededError(error) {
|
|
1687
|
+
return error instanceof Error && (error.code === TOOLSPACE_AUTH_NEEDED_ERROR_CODE || error.message.includes(TOOLSPACE_AUTH_NEEDED_MESSAGE));
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1219
1690
|
// src/routes/install.ts
|
|
1220
1691
|
import { readFile, stat } from "fs/promises";
|
|
1221
1692
|
import { HTTPException } from "hono/http-exception";
|
|
@@ -1362,6 +1833,9 @@ function isAuthExempt(c, settings) {
|
|
|
1362
1833
|
if (path === "/v1/github/setup" || path === "/v1/github/install/callback" || path === "/v1/github/oauth/callback" || path === "/v1/github/app-manifest/callback") {
|
|
1363
1834
|
return true;
|
|
1364
1835
|
}
|
|
1836
|
+
if (path === "/v1/integrations/oauth/callback" || path === "/v1/integrations/oauth/client-metadata.json") {
|
|
1837
|
+
return true;
|
|
1838
|
+
}
|
|
1365
1839
|
if (githubConnectPathPattern.test(path)) {
|
|
1366
1840
|
return true;
|
|
1367
1841
|
}
|
|
@@ -1491,7 +1965,7 @@ function registerCapabilityRoutes(app, deps) {
|
|
|
1491
1965
|
}
|
|
1492
1966
|
|
|
1493
1967
|
// src/routes/codex.ts
|
|
1494
|
-
import { environmentsEncryptionKeyBytes } from "@opengeni/config";
|
|
1968
|
+
import { environmentsEncryptionKeyBytes as environmentsEncryptionKeyBytes2 } from "@opengeni/config";
|
|
1495
1969
|
import {
|
|
1496
1970
|
accessTokenExpiry,
|
|
1497
1971
|
buildCodexUsageWindowFromCache,
|
|
@@ -1604,7 +2078,7 @@ function registerCodexRoutes(app, deps) {
|
|
|
1604
2078
|
throw new HTTPException3(502, { message: error instanceof CodexDeviceError ? error.message : "codex token exchange failed" });
|
|
1605
2079
|
}
|
|
1606
2080
|
const id = parseIdToken(tokens.idToken);
|
|
1607
|
-
const key =
|
|
2081
|
+
const key = environmentsEncryptionKeyBytes2(settings);
|
|
1608
2082
|
if (!key) {
|
|
1609
2083
|
throw new HTTPException3(500, { message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured" });
|
|
1610
2084
|
}
|
|
@@ -1694,124 +2168,1013 @@ function registerCodexRoutes(app, deps) {
|
|
|
1694
2168
|
}
|
|
1695
2169
|
});
|
|
1696
2170
|
});
|
|
1697
|
-
app.post("/v1/workspaces/:workspaceId/codex/accounts/:accountId/activate", async (c) => {
|
|
2171
|
+
app.post("/v1/workspaces/:workspaceId/codex/accounts/:accountId/activate", async (c) => {
|
|
2172
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2173
|
+
await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
|
|
2174
|
+
const accountId = c.req.param("accountId");
|
|
2175
|
+
const activated = await setActiveCodexCredential(db, workspaceId, accountId);
|
|
2176
|
+
if (!activated) {
|
|
2177
|
+
throw new HTTPException3(404, { message: "codex account not found" });
|
|
2178
|
+
}
|
|
2179
|
+
return c.json({ activated: true, accountId });
|
|
2180
|
+
});
|
|
2181
|
+
app.patch("/v1/workspaces/:workspaceId/codex/settings", async (c) => {
|
|
2182
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2183
|
+
const grant = await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
|
|
2184
|
+
const body = await c.req.json().catch(() => ({}));
|
|
2185
|
+
const patch = {};
|
|
2186
|
+
if (typeof body.rotationEnabled === "boolean") {
|
|
2187
|
+
patch.rotationEnabled = body.rotationEnabled;
|
|
2188
|
+
}
|
|
2189
|
+
if (typeof body.rotationStrategy === "string") {
|
|
2190
|
+
if (!CODEX_ROTATION_STRATEGIES.includes(body.rotationStrategy)) {
|
|
2191
|
+
throw new HTTPException3(400, { message: "invalid rotation strategy" });
|
|
2192
|
+
}
|
|
2193
|
+
patch.rotationStrategy = body.rotationStrategy;
|
|
2194
|
+
}
|
|
2195
|
+
if (patch.rotationEnabled === void 0 && patch.rotationStrategy === void 0) {
|
|
2196
|
+
throw new HTTPException3(400, { message: "no settings to update" });
|
|
2197
|
+
}
|
|
2198
|
+
await ensureCodexRotationSettings(db, grant.accountId, workspaceId);
|
|
2199
|
+
const updated = await updateCodexRotationSettings(db, workspaceId, patch);
|
|
2200
|
+
if (!updated) {
|
|
2201
|
+
throw new HTTPException3(404, { message: "codex rotation settings not found" });
|
|
2202
|
+
}
|
|
2203
|
+
return c.json({
|
|
2204
|
+
rotationEnabled: updated.rotationEnabled,
|
|
2205
|
+
rotationStrategy: updated.rotationStrategy,
|
|
2206
|
+
activeCredentialId: updated.activeCredentialId
|
|
2207
|
+
});
|
|
2208
|
+
});
|
|
2209
|
+
app.patch("/v1/workspaces/:workspaceId/codex/accounts/:accountId", async (c) => {
|
|
2210
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2211
|
+
await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
|
|
2212
|
+
const accountId = c.req.param("accountId");
|
|
2213
|
+
const body = await c.req.json();
|
|
2214
|
+
const label = typeof body.label === "string" ? body.label : null;
|
|
2215
|
+
const renamed = await renameCodexAccount(db, workspaceId, accountId, label);
|
|
2216
|
+
if (!renamed) {
|
|
2217
|
+
throw new HTTPException3(404, { message: "codex account not found" });
|
|
2218
|
+
}
|
|
2219
|
+
const accounts = await listCodexAccountStatuses(db, workspaceId);
|
|
2220
|
+
const row = accounts.find((account) => account.id === accountId);
|
|
2221
|
+
if (!row) {
|
|
2222
|
+
throw new HTTPException3(404, { message: "codex account not found" });
|
|
2223
|
+
}
|
|
2224
|
+
return c.json(codexAccountJson(row));
|
|
2225
|
+
});
|
|
2226
|
+
app.delete("/v1/workspaces/:workspaceId/codex/accounts/:accountId", async (c) => {
|
|
2227
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2228
|
+
await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
|
|
2229
|
+
const accountId = c.req.param("accountId");
|
|
2230
|
+
const result = await disconnectCodexAccount(db, workspaceId, accountId);
|
|
2231
|
+
return c.json({ disconnected: result.removed, newActiveId: result.newActiveCredentialId });
|
|
2232
|
+
});
|
|
2233
|
+
app.delete("/v1/workspaces/:workspaceId/codex", async (c) => {
|
|
2234
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2235
|
+
await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
|
|
2236
|
+
const removed = await disconnectAllCodexAccounts(db, workspaceId);
|
|
2237
|
+
return c.json({ disconnected: removed > 0 });
|
|
2238
|
+
});
|
|
2239
|
+
app.get("/v1/workspaces/:workspaceId/codex/usage", async (c) => {
|
|
2240
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2241
|
+
await requireAccessGrant2(c, deps, workspaceId, "workspace:read");
|
|
2242
|
+
const status = await getCodexCredentialStatus(db, workspaceId);
|
|
2243
|
+
if (!status?.credentialId) {
|
|
2244
|
+
throw new HTTPException3(404, { message: "codex subscription is not connected" });
|
|
2245
|
+
}
|
|
2246
|
+
const payload = await fetchCodexUsageForAccount(db, settings, workspaceId, status.credentialId);
|
|
2247
|
+
return c.json(codexUsageJson(payload));
|
|
2248
|
+
});
|
|
2249
|
+
app.get("/v1/workspaces/:workspaceId/codex/accounts/:accountId/usage", async (c) => {
|
|
2250
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2251
|
+
await requireAccessGrant2(c, deps, workspaceId, "workspace:read");
|
|
2252
|
+
const accountId = c.req.param("accountId");
|
|
2253
|
+
const accounts = await listCodexAccountStatuses(db, workspaceId);
|
|
2254
|
+
if (!accounts.some((account) => account.id === accountId)) {
|
|
2255
|
+
throw new HTTPException3(404, { message: "codex account not found" });
|
|
2256
|
+
}
|
|
2257
|
+
const payload = await fetchCodexUsageForAccount(db, settings, workspaceId, accountId);
|
|
2258
|
+
return c.json(codexUsageJson(payload));
|
|
2259
|
+
});
|
|
2260
|
+
app.post("/v1/workspaces/:workspaceId/codex/usage/refresh", async (c) => {
|
|
2261
|
+
const workspaceId = c.req.param("workspaceId");
|
|
2262
|
+
await requireAccessGrant2(c, deps, workspaceId, "workspace:read");
|
|
2263
|
+
const accounts = await listCodexAccountStatuses(db, workspaceId);
|
|
2264
|
+
const usage = {};
|
|
2265
|
+
const queue = [...accounts];
|
|
2266
|
+
const CONCURRENCY = 4;
|
|
2267
|
+
const worker = async () => {
|
|
2268
|
+
for (; ; ) {
|
|
2269
|
+
const account = queue.shift();
|
|
2270
|
+
if (!account) return;
|
|
2271
|
+
const settled = await Promise.allSettled([fetchCodexUsageForAccount(db, settings, workspaceId, account.id)]);
|
|
2272
|
+
const result = settled[0];
|
|
2273
|
+
usage[account.id] = result.status === "fulfilled" ? codexUsageJson(result.value) : { status: "error", usage: { status: "error", planType: null, fiveHour: null, weekly: null, limitReached: false, fetchedAt: (/* @__PURE__ */ new Date()).toISOString() } };
|
|
2274
|
+
}
|
|
2275
|
+
};
|
|
2276
|
+
await Promise.all(Array.from({ length: Math.min(CONCURRENCY, Math.max(1, accounts.length)) }, () => worker()));
|
|
2277
|
+
return c.json({ usage });
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
// src/routes/connections.ts
|
|
2282
|
+
import {
|
|
2283
|
+
ConnectionResponse,
|
|
2284
|
+
CreateConnectionRequest,
|
|
2285
|
+
IntegrationClientMetadata,
|
|
2286
|
+
ListConnectionsResponse,
|
|
2287
|
+
OAuthStartRequest,
|
|
2288
|
+
OAuthStartResponse as OAuthStartResponse2,
|
|
2289
|
+
UpdateConnectionRequest
|
|
2290
|
+
} from "@opengeni/contracts";
|
|
2291
|
+
import { requireAccessGrant as requireAccessGrant3, requireEnvironmentEncryption as requireEnvironmentEncryption3 } from "@opengeni/core";
|
|
2292
|
+
import {
|
|
2293
|
+
createConnection as createConnection2,
|
|
2294
|
+
encryptEnvironmentValue as encryptEnvironmentValue4,
|
|
2295
|
+
getConnectionMetadata as getConnectionMetadata2,
|
|
2296
|
+
listConnectionsMetadata,
|
|
2297
|
+
revokeConnection,
|
|
2298
|
+
updateConnection as updateConnection2
|
|
2299
|
+
} from "@opengeni/db";
|
|
2300
|
+
import { HTTPException as HTTPException5 } from "hono/http-exception";
|
|
2301
|
+
|
|
2302
|
+
// src/integrations/oauth-client.ts
|
|
2303
|
+
import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2304
|
+
import { StreamableHTTPClientTransport as StreamableHTTPClientTransport2 } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
2305
|
+
import { parseIntegrationsOauthClientsJson } from "@opengeni/config";
|
|
2306
|
+
import { OAuthStartResponse } from "@opengeni/contracts";
|
|
2307
|
+
import { requireEnvironmentEncryption as requireEnvironmentEncryption2 } from "@opengeni/core";
|
|
2308
|
+
import {
|
|
2309
|
+
consumeIntegrationOAuthStateNonce,
|
|
2310
|
+
createConnection,
|
|
2311
|
+
decryptEnvironmentValue,
|
|
2312
|
+
encryptEnvironmentValue as encryptEnvironmentValue3,
|
|
2313
|
+
getConnectionMetadata,
|
|
2314
|
+
isPrivateAddress,
|
|
2315
|
+
loadIntegrationOAuthClient,
|
|
2316
|
+
storeIntegrationOAuthClient,
|
|
2317
|
+
updateConnection
|
|
2318
|
+
} from "@opengeni/db";
|
|
2319
|
+
import { createSignedState as createSignedState3, readSignedState as readSignedState2 } from "@opengeni/github";
|
|
2320
|
+
import { Buffer } from "buffer";
|
|
2321
|
+
import { createHash, randomBytes } from "crypto";
|
|
2322
|
+
import { lookup } from "dns/promises";
|
|
2323
|
+
import { isIP } from "net";
|
|
2324
|
+
import { HTTPException as HTTPException4 } from "hono/http-exception";
|
|
2325
|
+
var oauthStateTtlMs = 10 * 60 * 1e3;
|
|
2326
|
+
async function startMcpOAuth(deps, context) {
|
|
2327
|
+
const { db, settings } = deps;
|
|
2328
|
+
const resource = canonicalMcpResource(context.payload.mcpUrl ?? context.payload.resource);
|
|
2329
|
+
const providerDomain = canonicalProviderDomain(context.payload.providerDomain ?? new URL(resource).hostname);
|
|
2330
|
+
const returnPath = safeReturnPath(context.payload.returnPath ?? "/integrations");
|
|
2331
|
+
const baseUrl = integrationBaseUrl(settings.publicBaseUrl, context.requestUrl);
|
|
2332
|
+
const redirectUri = `${baseUrl}/v1/integrations/oauth/callback`;
|
|
2333
|
+
const metadataUrl = `${baseUrl}/v1/integrations/oauth/client-metadata.json`;
|
|
2334
|
+
const existing = context.payload.connectionId ? await getConnectionMetadata(db, context.workspaceId, context.payload.connectionId, context.subjectId) : null;
|
|
2335
|
+
if (context.payload.connectionId && !existing) {
|
|
2336
|
+
throw new HTTPException4(404, { message: "connection not found" });
|
|
2337
|
+
}
|
|
2338
|
+
const discovery = await discoverMcpOAuth(resource, settings);
|
|
2339
|
+
const client = await registerOAuthClient(db, settings, discovery.as, metadataUrl, redirectUri);
|
|
2340
|
+
const verifier = randomPkceVerifier();
|
|
2341
|
+
const authorizeScopes = chooseAuthorizeScopes(context.payload.requestedScopes, discovery.challenge.scope, discovery.prm.scopesSupported);
|
|
2342
|
+
const key = requireEnvironmentEncryption2(settings);
|
|
2343
|
+
const state = createSignedState3(requireIntegrationsStateSecret(settings), {
|
|
2344
|
+
accountId: context.accountId,
|
|
2345
|
+
workspaceId: context.workspaceId,
|
|
2346
|
+
subjectId: context.subjectId,
|
|
2347
|
+
providerDomain,
|
|
2348
|
+
resource,
|
|
2349
|
+
requestedScopes: uniqueStrings(context.payload.requestedScopes ?? []),
|
|
2350
|
+
authorizeScopes,
|
|
2351
|
+
encryptedPkceVerifier: encryptEnvironmentValue3(key, verifier),
|
|
2352
|
+
clientId: client.clientId,
|
|
2353
|
+
tokenEndpoint: discovery.as.tokenEndpoint,
|
|
2354
|
+
authorizationServer: client.authorizationServer,
|
|
2355
|
+
issuer: client.issuer,
|
|
2356
|
+
clientRegistrationMethod: client.method,
|
|
2357
|
+
tokenEndpointAuthMethod: client.tokenEndpointAuthMethod,
|
|
2358
|
+
returnPath,
|
|
2359
|
+
...existing ? { connectionId: existing.id, connectionVersion: existing.version } : {}
|
|
2360
|
+
});
|
|
2361
|
+
const authorizationUrl = buildAuthorizationUrl({
|
|
2362
|
+
endpoint: discovery.as.authorizationEndpoint,
|
|
2363
|
+
clientId: client.clientId,
|
|
2364
|
+
redirectUri,
|
|
2365
|
+
state,
|
|
2366
|
+
resource,
|
|
2367
|
+
verifier,
|
|
2368
|
+
scopes: authorizeScopes
|
|
2369
|
+
});
|
|
2370
|
+
return OAuthStartResponse.parse({
|
|
2371
|
+
state,
|
|
2372
|
+
authorizationUrl,
|
|
2373
|
+
expiresAt: new Date(Date.now() + oauthStateTtlMs).toISOString()
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
async function completeMcpOAuthCallback(deps, input) {
|
|
2377
|
+
const { db, settings } = deps;
|
|
2378
|
+
if (!input.state) {
|
|
2379
|
+
throw new HTTPException4(400, { message: "missing OAuth state" });
|
|
2380
|
+
}
|
|
2381
|
+
const state = readOAuthState(input.state, settings);
|
|
2382
|
+
if (!input.code) {
|
|
2383
|
+
return { redirectTo: callbackReturnPath(state.returnPath, "error", { reason: "missing_code" }) };
|
|
2384
|
+
}
|
|
2385
|
+
const consumed = await consumeIntegrationOAuthStateNonce(db, {
|
|
2386
|
+
accountId: state.accountId,
|
|
2387
|
+
workspaceId: state.workspaceId,
|
|
2388
|
+
subjectId: state.subjectId,
|
|
2389
|
+
nonce: state.nonce,
|
|
2390
|
+
expiresAt: new Date(state.iat * 1e3 + oauthStateTtlMs),
|
|
2391
|
+
now: /* @__PURE__ */ new Date()
|
|
2392
|
+
});
|
|
2393
|
+
if (!consumed) {
|
|
2394
|
+
throw new HTTPException4(400, { message: "OAuth state has already been used" });
|
|
2395
|
+
}
|
|
2396
|
+
try {
|
|
2397
|
+
const baseUrl = integrationBaseUrl(settings.publicBaseUrl, input.requestUrl);
|
|
2398
|
+
const redirectUri = `${baseUrl}/v1/integrations/oauth/callback`;
|
|
2399
|
+
const key = requireEnvironmentEncryption2(settings);
|
|
2400
|
+
const verifier = decryptEnvironmentValue(key, state.encryptedPkceVerifier);
|
|
2401
|
+
const client = await clientForState(db, settings, state);
|
|
2402
|
+
const token = await exchangeAuthorizationCode(settings, {
|
|
2403
|
+
code: input.code,
|
|
2404
|
+
verifier,
|
|
2405
|
+
redirectUri,
|
|
2406
|
+
resource: state.resource,
|
|
2407
|
+
tokenEndpoint: state.tokenEndpoint,
|
|
2408
|
+
client
|
|
2409
|
+
});
|
|
2410
|
+
const tools = await verifyMcpToolsList(settings, state.resource, token);
|
|
2411
|
+
const scopes = grantedScopes(token.scopeText, state.authorizeScopes);
|
|
2412
|
+
const credential = credentialBundle(token, state, client);
|
|
2413
|
+
const metadata = {
|
|
2414
|
+
resource: state.resource,
|
|
2415
|
+
authorizationServer: state.authorizationServer,
|
|
2416
|
+
authorizationServerIssuer: state.issuer,
|
|
2417
|
+
tokenEndpoint: state.tokenEndpoint,
|
|
2418
|
+
clientId: client.clientId,
|
|
2419
|
+
clientRegistrationMethod: state.clientRegistrationMethod,
|
|
2420
|
+
mcpTools: tools
|
|
2421
|
+
};
|
|
2422
|
+
const credentialEncrypted = encryptEnvironmentValue3(key, JSON.stringify(credential));
|
|
2423
|
+
const connection = state.connectionId ? await updateConnection(db, {
|
|
2424
|
+
workspaceId: state.workspaceId,
|
|
2425
|
+
connectionId: state.connectionId,
|
|
2426
|
+
visibleToSubjectId: state.subjectId,
|
|
2427
|
+
expectedVersion: state.connectionVersion,
|
|
2428
|
+
providerDomain: state.providerDomain,
|
|
2429
|
+
kind: "oauth2",
|
|
2430
|
+
status: "active",
|
|
2431
|
+
credentialEncrypted,
|
|
2432
|
+
grantedScopes: scopes,
|
|
2433
|
+
expiresAt: token.expiresAt,
|
|
2434
|
+
metadata,
|
|
2435
|
+
updatedBySubjectId: state.subjectId
|
|
2436
|
+
}) : await createConnection(db, {
|
|
2437
|
+
accountId: state.accountId,
|
|
2438
|
+
workspaceId: state.workspaceId,
|
|
2439
|
+
subjectId: null,
|
|
2440
|
+
providerDomain: state.providerDomain,
|
|
2441
|
+
kind: "oauth2",
|
|
2442
|
+
credentialEncrypted,
|
|
2443
|
+
grantedScopes: scopes,
|
|
2444
|
+
expiresAt: token.expiresAt,
|
|
2445
|
+
metadata,
|
|
2446
|
+
createdBySubjectId: state.subjectId
|
|
2447
|
+
});
|
|
2448
|
+
if (!connection) {
|
|
2449
|
+
throw new HTTPException4(409, { message: "connection changed during OAuth reconnect; start again" });
|
|
2450
|
+
}
|
|
2451
|
+
return { redirectTo: callbackReturnPath(state.returnPath, "success", { connectionId: connection.id }) };
|
|
2452
|
+
} catch (error) {
|
|
2453
|
+
if (error instanceof HTTPException4 && error.status >= 400 && error.status < 500) {
|
|
2454
|
+
throw error;
|
|
2455
|
+
}
|
|
2456
|
+
return { redirectTo: callbackReturnPath(state.returnPath, "error", { reason: "oauth_callback_failed" }) };
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
function integrationBaseUrl(publicBaseUrl, requestUrl) {
|
|
2460
|
+
return (publicBaseUrl ?? new URL(requestUrl).origin).replace(/\/+$/, "");
|
|
2461
|
+
}
|
|
2462
|
+
function requireIntegrationsStateSecret(settings) {
|
|
2463
|
+
const secret = settings.integrationsStateSecret?.trim();
|
|
2464
|
+
if (!secret) {
|
|
2465
|
+
throw new HTTPException4(503, { message: "integrations OAuth requires OPENGENI_INTEGRATIONS_STATE_SECRET" });
|
|
2466
|
+
}
|
|
2467
|
+
return secret;
|
|
2468
|
+
}
|
|
2469
|
+
async function discoverMcpOAuth(resource, settings) {
|
|
2470
|
+
const challenge = await probeMcpChallenge(resource, settings);
|
|
2471
|
+
const prm = await discoverProtectedResourceMetadata(resource, settings, challenge.resourceMetadata);
|
|
2472
|
+
const authorizationServer = prm.authorizationServers[0];
|
|
2473
|
+
if (!authorizationServer) {
|
|
2474
|
+
throw new HTTPException4(422, { message: "MCP protected resource metadata did not advertise an authorization server" });
|
|
2475
|
+
}
|
|
2476
|
+
const as = await discoverAuthorizationServerMetadata(authorizationServer, settings);
|
|
2477
|
+
if (!as.codeChallengeMethodsSupported.includes("S256")) {
|
|
2478
|
+
throw new HTTPException4(422, { message: "authorization server does not support required PKCE S256" });
|
|
2479
|
+
}
|
|
2480
|
+
return { challenge, prm, as };
|
|
2481
|
+
}
|
|
2482
|
+
async function probeMcpChallenge(resource, settings) {
|
|
2483
|
+
const response = await fetchOAuth(resource, settings, {
|
|
2484
|
+
method: "GET",
|
|
2485
|
+
headers: { accept: "application/json" }
|
|
2486
|
+
});
|
|
2487
|
+
if (response.status !== 401) {
|
|
2488
|
+
return {};
|
|
2489
|
+
}
|
|
2490
|
+
return parseWwwAuthenticate(response.headers.get("www-authenticate"));
|
|
2491
|
+
}
|
|
2492
|
+
async function discoverProtectedResourceMetadata(resource, settings, advertisedUrl) {
|
|
2493
|
+
const candidates = uniqueStrings([
|
|
2494
|
+
...advertisedUrl ? [advertisedUrl] : [],
|
|
2495
|
+
...wellKnownCandidates(resource, "oauth-protected-resource")
|
|
2496
|
+
]);
|
|
2497
|
+
for (const candidate of candidates) {
|
|
2498
|
+
const payload = await fetchJsonObject(candidate, settings).catch((error) => {
|
|
2499
|
+
if (error instanceof HTTPException4) {
|
|
2500
|
+
throw error;
|
|
2501
|
+
}
|
|
2502
|
+
return null;
|
|
2503
|
+
});
|
|
2504
|
+
if (!payload) {
|
|
2505
|
+
continue;
|
|
2506
|
+
}
|
|
2507
|
+
const authorizationServers = stringArray(payload.authorization_servers);
|
|
2508
|
+
if (authorizationServers.length === 0) {
|
|
2509
|
+
continue;
|
|
2510
|
+
}
|
|
2511
|
+
return {
|
|
2512
|
+
authorizationServers,
|
|
2513
|
+
scopesSupported: stringArray(payload.scopes_supported),
|
|
2514
|
+
raw: payload,
|
|
2515
|
+
...stringValue(payload.resource) ? { resource: stringValue(payload.resource) } : {}
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
throw new HTTPException4(422, { message: "could not discover MCP protected resource metadata" });
|
|
2519
|
+
}
|
|
2520
|
+
async function discoverAuthorizationServerMetadata(authorizationServer, settings) {
|
|
2521
|
+
const candidates = uniqueStrings([
|
|
2522
|
+
authorizationServer,
|
|
2523
|
+
...wellKnownCandidates(authorizationServer, "oauth-authorization-server"),
|
|
2524
|
+
...wellKnownCandidates(authorizationServer, "openid-configuration")
|
|
2525
|
+
]);
|
|
2526
|
+
for (const candidate of candidates) {
|
|
2527
|
+
const payload = await fetchJsonObject(candidate, settings).catch((error) => {
|
|
2528
|
+
if (error instanceof HTTPException4) {
|
|
2529
|
+
throw error;
|
|
2530
|
+
}
|
|
2531
|
+
return null;
|
|
2532
|
+
});
|
|
2533
|
+
if (!payload) {
|
|
2534
|
+
continue;
|
|
2535
|
+
}
|
|
2536
|
+
const authorizationEndpoint = stringValue(payload.authorization_endpoint);
|
|
2537
|
+
const tokenEndpoint = stringValue(payload.token_endpoint);
|
|
2538
|
+
if (!authorizationEndpoint || !tokenEndpoint) {
|
|
2539
|
+
continue;
|
|
2540
|
+
}
|
|
2541
|
+
return {
|
|
2542
|
+
issuer: stringValue(payload.issuer) ?? authorizationServer.replace(/\/+$/, ""),
|
|
2543
|
+
authorizationServer: authorizationServer.replace(/\/+$/, ""),
|
|
2544
|
+
authorizationEndpoint,
|
|
2545
|
+
tokenEndpoint,
|
|
2546
|
+
clientIdMetadataDocumentSupported: payload.client_id_metadata_document_supported === true,
|
|
2547
|
+
codeChallengeMethodsSupported: stringArray(payload.code_challenge_methods_supported),
|
|
2548
|
+
raw: payload,
|
|
2549
|
+
...stringValue(payload.registration_endpoint) ? { registrationEndpoint: stringValue(payload.registration_endpoint) } : {}
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
throw new HTTPException4(422, { message: "could not discover OAuth authorization server metadata" });
|
|
2553
|
+
}
|
|
2554
|
+
async function registerOAuthClient(db, settings, as, metadataUrl, redirectUri) {
|
|
2555
|
+
const operator = operatorClientForAs(settings, as);
|
|
2556
|
+
if (operator) {
|
|
2557
|
+
return operator;
|
|
2558
|
+
}
|
|
2559
|
+
if (as.clientIdMetadataDocumentSupported) {
|
|
2560
|
+
return {
|
|
2561
|
+
method: "cimd",
|
|
2562
|
+
issuer: as.issuer,
|
|
2563
|
+
authorizationServer: as.authorizationServer,
|
|
2564
|
+
clientId: metadataUrl,
|
|
2565
|
+
tokenEndpointAuthMethod: "none"
|
|
2566
|
+
};
|
|
2567
|
+
}
|
|
2568
|
+
const storedClient = await loadIntegrationOAuthClient(db, settings, as.issuer);
|
|
2569
|
+
if (storedClient) {
|
|
2570
|
+
return {
|
|
2571
|
+
method: "dcr",
|
|
2572
|
+
issuer: storedClient.issuer,
|
|
2573
|
+
authorizationServer: storedClient.authorizationServer,
|
|
2574
|
+
clientId: storedClient.clientId,
|
|
2575
|
+
...storedClient.clientSecret ? { clientSecret: storedClient.clientSecret } : {},
|
|
2576
|
+
tokenEndpointAuthMethod: tokenAuthMethod(storedClient.tokenEndpointAuthMethod, Boolean(storedClient.clientSecret))
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
if (!as.registrationEndpoint) {
|
|
2580
|
+
throw new HTTPException4(422, {
|
|
2581
|
+
message: "manual OAuth client credentials are required for this authorization server"
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
const dcr = await dynamicClientRegistration(settings, as, redirectUri);
|
|
2585
|
+
const key = dcr.clientSecret ? requireEnvironmentEncryption2(settings) : null;
|
|
2586
|
+
const storedWinner = await storeIntegrationOAuthClient(db, {
|
|
2587
|
+
issuer: as.issuer,
|
|
2588
|
+
authorizationServer: as.authorizationServer,
|
|
2589
|
+
clientId: dcr.clientId,
|
|
2590
|
+
clientSecretEncrypted: dcr.clientSecret && key ? encryptEnvironmentValue3(key, dcr.clientSecret) : null,
|
|
2591
|
+
tokenEndpointAuthMethod: dcr.tokenEndpointAuthMethod,
|
|
2592
|
+
metadata: {
|
|
2593
|
+
registrationEndpoint: as.registrationEndpoint,
|
|
2594
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2595
|
+
}
|
|
2596
|
+
});
|
|
2597
|
+
if (storedWinner.clientId !== dcr.clientId) {
|
|
2598
|
+
const winner = await loadIntegrationOAuthClient(db, settings, as.issuer);
|
|
2599
|
+
if (!winner) {
|
|
2600
|
+
throw new HTTPException4(422, { message: "OAuth client registration could not be loaded after a registration race" });
|
|
2601
|
+
}
|
|
2602
|
+
return dcrRegistrationFromStored(winner);
|
|
2603
|
+
}
|
|
2604
|
+
return dcr;
|
|
2605
|
+
}
|
|
2606
|
+
function dcrRegistrationFromStored(stored) {
|
|
2607
|
+
return {
|
|
2608
|
+
method: "dcr",
|
|
2609
|
+
issuer: stored.issuer,
|
|
2610
|
+
authorizationServer: stored.authorizationServer,
|
|
2611
|
+
clientId: stored.clientId,
|
|
2612
|
+
...stored.clientSecret ? { clientSecret: stored.clientSecret } : {},
|
|
2613
|
+
tokenEndpointAuthMethod: tokenAuthMethod(stored.tokenEndpointAuthMethod, Boolean(stored.clientSecret))
|
|
2614
|
+
};
|
|
2615
|
+
}
|
|
2616
|
+
function operatorClientForAs(settings, as) {
|
|
2617
|
+
const entry = operatorClientEntryFor(settings, [as.issuer, as.authorizationServer]);
|
|
2618
|
+
if (!entry) {
|
|
2619
|
+
return null;
|
|
2620
|
+
}
|
|
2621
|
+
return {
|
|
2622
|
+
method: "operator",
|
|
2623
|
+
issuer: as.issuer,
|
|
2624
|
+
authorizationServer: as.authorizationServer,
|
|
2625
|
+
clientId: entry.clientId,
|
|
2626
|
+
...entry.clientSecret ? { clientSecret: entry.clientSecret } : {},
|
|
2627
|
+
tokenEndpointAuthMethod: tokenAuthMethod(entry.tokenEndpointAuthMethod, Boolean(entry.clientSecret))
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
function operatorClientEntryFor(settings, candidates) {
|
|
2631
|
+
const configured = parseIntegrationsOauthClientsJson(settings.integrationsOauthClientsJson);
|
|
2632
|
+
const exactKeys = uniqueStrings(candidates.flatMap((candidate) => [candidate, normalizedIssuerKey(candidate)]));
|
|
2633
|
+
for (const key of exactKeys) {
|
|
2634
|
+
const entry = configured[key];
|
|
2635
|
+
if (entry) {
|
|
2636
|
+
return entry;
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
const normalizedCandidates = new Set(candidates.map(normalizedIssuerKey));
|
|
2640
|
+
for (const [key, entry] of Object.entries(configured)) {
|
|
2641
|
+
if (normalizedCandidates.has(normalizedIssuerKey(key))) {
|
|
2642
|
+
return entry;
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
return null;
|
|
2646
|
+
}
|
|
2647
|
+
function normalizedIssuerKey(value) {
|
|
2648
|
+
return value.replace(/\/+$/, "");
|
|
2649
|
+
}
|
|
2650
|
+
async function dynamicClientRegistration(settings, as, redirectUri) {
|
|
2651
|
+
if (!as.registrationEndpoint) {
|
|
2652
|
+
throw new HTTPException4(422, { message: "authorization server does not support dynamic client registration" });
|
|
2653
|
+
}
|
|
2654
|
+
await assertOAuthFetchAllowed(as.registrationEndpoint, settings);
|
|
2655
|
+
const response = await fetchOAuth(as.registrationEndpoint, settings, {
|
|
2656
|
+
method: "POST",
|
|
2657
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
2658
|
+
body: JSON.stringify({
|
|
2659
|
+
client_name: "OpenGeni",
|
|
2660
|
+
redirect_uris: [redirectUri],
|
|
2661
|
+
token_endpoint_auth_method: "none",
|
|
2662
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
2663
|
+
response_types: ["code"]
|
|
2664
|
+
})
|
|
2665
|
+
});
|
|
2666
|
+
if (!response.ok) {
|
|
2667
|
+
throw new HTTPException4(422, { message: `dynamic client registration failed with HTTP ${response.status}` });
|
|
2668
|
+
}
|
|
2669
|
+
const payload = await response.json();
|
|
2670
|
+
const clientId = stringValue(payload.client_id);
|
|
2671
|
+
if (!clientId) {
|
|
2672
|
+
throw new HTTPException4(422, { message: "dynamic client registration response did not include client_id" });
|
|
2673
|
+
}
|
|
2674
|
+
const clientSecret = stringValue(payload.client_secret);
|
|
2675
|
+
return {
|
|
2676
|
+
method: "dcr",
|
|
2677
|
+
issuer: as.issuer,
|
|
2678
|
+
authorizationServer: as.authorizationServer,
|
|
2679
|
+
clientId,
|
|
2680
|
+
...clientSecret ? { clientSecret } : {},
|
|
2681
|
+
tokenEndpointAuthMethod: tokenAuthMethod(stringValue(payload.token_endpoint_auth_method), Boolean(clientSecret))
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
function buildAuthorizationUrl(input) {
|
|
2685
|
+
const url = new URL(input.endpoint);
|
|
2686
|
+
url.searchParams.set("response_type", "code");
|
|
2687
|
+
url.searchParams.set("client_id", input.clientId);
|
|
2688
|
+
url.searchParams.set("redirect_uri", input.redirectUri);
|
|
2689
|
+
url.searchParams.set("state", input.state);
|
|
2690
|
+
url.searchParams.set("resource", input.resource);
|
|
2691
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
2692
|
+
url.searchParams.set("code_challenge", pkceChallenge(input.verifier));
|
|
2693
|
+
if (input.scopes.length > 0) {
|
|
2694
|
+
url.searchParams.set("scope", input.scopes.join(" "));
|
|
2695
|
+
}
|
|
2696
|
+
return url.toString();
|
|
2697
|
+
}
|
|
2698
|
+
function readOAuthState(state, settings) {
|
|
2699
|
+
const payload = readSignedState2(state, requireIntegrationsStateSecret(settings));
|
|
2700
|
+
if (!payload) {
|
|
2701
|
+
throw new HTTPException4(400, { message: "invalid or expired OAuth state" });
|
|
2702
|
+
}
|
|
2703
|
+
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
2704
|
+
const iat = numberValue(payload.iat);
|
|
2705
|
+
if (iat === void 0 || nowSeconds - iat > oauthStateTtlMs / 1e3 || nowSeconds < iat) {
|
|
2706
|
+
throw new HTTPException4(400, { message: "invalid or expired OAuth state" });
|
|
2707
|
+
}
|
|
2708
|
+
const parsed = {
|
|
2709
|
+
accountId: requiredString(payload.accountId, "state.accountId"),
|
|
2710
|
+
workspaceId: requiredString(payload.workspaceId, "state.workspaceId"),
|
|
2711
|
+
subjectId: requiredString(payload.subjectId, "state.subjectId"),
|
|
2712
|
+
providerDomain: requiredString(payload.providerDomain, "state.providerDomain"),
|
|
2713
|
+
resource: requiredString(payload.resource, "state.resource"),
|
|
2714
|
+
requestedScopes: stringArray(payload.requestedScopes),
|
|
2715
|
+
authorizeScopes: stringArray(payload.authorizeScopes),
|
|
2716
|
+
encryptedPkceVerifier: requiredString(payload.encryptedPkceVerifier, "state.encryptedPkceVerifier"),
|
|
2717
|
+
clientId: requiredString(payload.clientId, "state.clientId"),
|
|
2718
|
+
tokenEndpoint: requiredString(payload.tokenEndpoint, "state.tokenEndpoint"),
|
|
2719
|
+
authorizationServer: requiredString(payload.authorizationServer, "state.authorizationServer"),
|
|
2720
|
+
issuer: requiredString(payload.issuer, "state.issuer"),
|
|
2721
|
+
clientRegistrationMethod: registrationMethod(payload.clientRegistrationMethod),
|
|
2722
|
+
tokenEndpointAuthMethod: tokenAuthMethod(stringValue(payload.tokenEndpointAuthMethod), false),
|
|
2723
|
+
returnPath: safeReturnPath(stringValue(payload.returnPath) ?? "/integrations"),
|
|
2724
|
+
nonce: requiredString(payload.nonce, "state.nonce"),
|
|
2725
|
+
iat
|
|
2726
|
+
};
|
|
2727
|
+
const connectionId = stringValue(payload.connectionId);
|
|
2728
|
+
const connectionVersion = numberValue(payload.connectionVersion);
|
|
2729
|
+
return {
|
|
2730
|
+
...parsed,
|
|
2731
|
+
...connectionId ? { connectionId } : {},
|
|
2732
|
+
...connectionVersion !== void 0 ? { connectionVersion } : {}
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
async function clientForState(db, settings, state) {
|
|
2736
|
+
if (state.clientRegistrationMethod === "cimd") {
|
|
2737
|
+
return {
|
|
2738
|
+
method: "cimd",
|
|
2739
|
+
issuer: state.issuer,
|
|
2740
|
+
authorizationServer: state.authorizationServer,
|
|
2741
|
+
clientId: state.clientId,
|
|
2742
|
+
tokenEndpointAuthMethod: "none"
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
if (state.clientRegistrationMethod === "dcr") {
|
|
2746
|
+
const stored = await loadIntegrationOAuthClient(db, settings, state.issuer);
|
|
2747
|
+
if (!stored || stored.clientId !== state.clientId) {
|
|
2748
|
+
throw new HTTPException4(400, { message: "OAuth client registration is no longer available" });
|
|
2749
|
+
}
|
|
2750
|
+
return {
|
|
2751
|
+
method: "dcr",
|
|
2752
|
+
issuer: stored.issuer,
|
|
2753
|
+
authorizationServer: stored.authorizationServer,
|
|
2754
|
+
clientId: stored.clientId,
|
|
2755
|
+
...stored.clientSecret ? { clientSecret: stored.clientSecret } : {},
|
|
2756
|
+
tokenEndpointAuthMethod: tokenAuthMethod(stored.tokenEndpointAuthMethod, Boolean(stored.clientSecret))
|
|
2757
|
+
};
|
|
2758
|
+
}
|
|
2759
|
+
const entry = operatorClientEntryFor(settings, [state.issuer, state.authorizationServer]);
|
|
2760
|
+
if (!entry || entry.clientId !== state.clientId) {
|
|
2761
|
+
throw new HTTPException4(400, { message: "operator OAuth client credentials are no longer available" });
|
|
2762
|
+
}
|
|
2763
|
+
return {
|
|
2764
|
+
method: "operator",
|
|
2765
|
+
issuer: state.issuer,
|
|
2766
|
+
authorizationServer: state.authorizationServer,
|
|
2767
|
+
clientId: entry.clientId,
|
|
2768
|
+
...entry.clientSecret ? { clientSecret: entry.clientSecret } : {},
|
|
2769
|
+
tokenEndpointAuthMethod: tokenAuthMethod(entry.tokenEndpointAuthMethod, Boolean(entry.clientSecret))
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
async function exchangeAuthorizationCode(settings, input) {
|
|
2773
|
+
await assertOAuthFetchAllowed(input.tokenEndpoint, settings);
|
|
2774
|
+
const body = new URLSearchParams();
|
|
2775
|
+
body.set("grant_type", "authorization_code");
|
|
2776
|
+
body.set("code", input.code);
|
|
2777
|
+
body.set("redirect_uri", input.redirectUri);
|
|
2778
|
+
body.set("code_verifier", input.verifier);
|
|
2779
|
+
body.set("resource", input.resource);
|
|
2780
|
+
body.set("client_id", input.client.clientId);
|
|
2781
|
+
const headers = { "content-type": "application/x-www-form-urlencoded", accept: "application/json" };
|
|
2782
|
+
if (input.client.clientSecret && input.client.tokenEndpointAuthMethod === "client_secret_post") {
|
|
2783
|
+
body.set("client_secret", input.client.clientSecret);
|
|
2784
|
+
} else if (input.client.clientSecret && input.client.tokenEndpointAuthMethod === "client_secret_basic") {
|
|
2785
|
+
headers.authorization = `Basic ${Buffer.from(`${input.client.clientId}:${input.client.clientSecret}`).toString("base64")}`;
|
|
2786
|
+
}
|
|
2787
|
+
const response = await fetchOAuth(input.tokenEndpoint, settings, { method: "POST", headers, body });
|
|
2788
|
+
if (!response.ok) {
|
|
2789
|
+
throw new Error(`OAuth token endpoint returned HTTP ${response.status}`);
|
|
2790
|
+
}
|
|
2791
|
+
const payload = await response.json();
|
|
2792
|
+
const accessToken = stringValue(payload.access_token);
|
|
2793
|
+
if (!accessToken) {
|
|
2794
|
+
throw new Error("OAuth token response did not include access_token");
|
|
2795
|
+
}
|
|
2796
|
+
return {
|
|
2797
|
+
accessToken,
|
|
2798
|
+
tokenType: stringValue(payload.token_type) ?? "Bearer",
|
|
2799
|
+
expiresAt: expiresAtFromTokenResponse(payload),
|
|
2800
|
+
raw: payload,
|
|
2801
|
+
...stringValue(payload.refresh_token) ? { refreshToken: stringValue(payload.refresh_token) } : {},
|
|
2802
|
+
...stringValue(payload.scope) ? { scopeText: stringValue(payload.scope) } : {}
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2805
|
+
async function verifyMcpToolsList(settings, resource, token) {
|
|
2806
|
+
await assertOAuthFetchAllowed(resource, settings);
|
|
2807
|
+
const client = new Client2({ name: "opengeni-integration-verify", version: "0.1.0" }, { capabilities: {} });
|
|
2808
|
+
try {
|
|
2809
|
+
const transport = new StreamableHTTPClientTransport2(new URL(resource), {
|
|
2810
|
+
requestInit: {
|
|
2811
|
+
headers: { authorization: `${token.tokenType} ${token.accessToken}` }
|
|
2812
|
+
},
|
|
2813
|
+
fetch: (url, init) => fetchOAuth(url.toString(), settings, init)
|
|
2814
|
+
});
|
|
2815
|
+
await client.connect(transport, { timeout: 1e4, maxTotalTimeout: 1e4 });
|
|
2816
|
+
const listed = await client.listTools(void 0, { timeout: 1e4, maxTotalTimeout: 1e4 });
|
|
2817
|
+
return listed.tools.map((tool) => ({
|
|
2818
|
+
name: tool.name,
|
|
2819
|
+
...tool.description ? { description: tool.description } : {}
|
|
2820
|
+
}));
|
|
2821
|
+
} finally {
|
|
2822
|
+
await client.close().catch(() => void 0);
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
function credentialBundle(token, state, client) {
|
|
2826
|
+
return {
|
|
2827
|
+
access_token: token.accessToken,
|
|
2828
|
+
...token.refreshToken ? { refresh_token: token.refreshToken } : {},
|
|
2829
|
+
token_type: token.tokenType,
|
|
2830
|
+
...token.expiresAt ? { expires_at: token.expiresAt.toISOString() } : {},
|
|
2831
|
+
resource: state.resource,
|
|
2832
|
+
...token.scopeText ? { scope: token.scopeText } : state.authorizeScopes.length ? { scope: state.authorizeScopes.join(" ") } : {},
|
|
2833
|
+
token_endpoint: state.tokenEndpoint,
|
|
2834
|
+
client_id: client.clientId,
|
|
2835
|
+
...client.clientSecret ? { client_secret: client.clientSecret, token_endpoint_auth_method: client.tokenEndpointAuthMethod } : {}
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
function callbackReturnPath(returnPath, status, params) {
|
|
2839
|
+
const url = new URL(returnPath, "https://opengeni.local");
|
|
2840
|
+
url.searchParams.set("integration_oauth", status);
|
|
2841
|
+
for (const [key, value] of Object.entries(params)) {
|
|
2842
|
+
url.searchParams.set(key, value);
|
|
2843
|
+
}
|
|
2844
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
2845
|
+
}
|
|
2846
|
+
function canonicalMcpResource(value) {
|
|
2847
|
+
if (!value) {
|
|
2848
|
+
throw new HTTPException4(400, { message: "mcpUrl is required" });
|
|
2849
|
+
}
|
|
2850
|
+
let url;
|
|
2851
|
+
try {
|
|
2852
|
+
url = new URL(value);
|
|
2853
|
+
} catch {
|
|
2854
|
+
throw new HTTPException4(422, { message: "MCP resource URL is invalid" });
|
|
2855
|
+
}
|
|
2856
|
+
url.hash = "";
|
|
2857
|
+
return url.toString();
|
|
2858
|
+
}
|
|
2859
|
+
function canonicalProviderDomain(value) {
|
|
2860
|
+
return value.trim().toLowerCase().replace(/^www\./, "");
|
|
2861
|
+
}
|
|
2862
|
+
function safeReturnPath(value) {
|
|
2863
|
+
if (!value.startsWith("/") || value.startsWith("//")) {
|
|
2864
|
+
throw new HTTPException4(400, { message: "OAuth returnPath must be a relative path" });
|
|
2865
|
+
}
|
|
2866
|
+
const parsed = new URL(value, "https://opengeni.local");
|
|
2867
|
+
if (parsed.origin !== "https://opengeni.local") {
|
|
2868
|
+
throw new HTTPException4(400, { message: "OAuth returnPath must be a relative path" });
|
|
2869
|
+
}
|
|
2870
|
+
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
|
|
2871
|
+
}
|
|
2872
|
+
async function fetchJsonObject(url, settings) {
|
|
2873
|
+
const response = await fetchOAuth(url, settings, { headers: { accept: "application/json" } });
|
|
2874
|
+
if (!response.ok) {
|
|
2875
|
+
throw new Error(`HTTP ${response.status}`);
|
|
2876
|
+
}
|
|
2877
|
+
const payload = await response.json();
|
|
2878
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
2879
|
+
throw new Error("metadata response was not a JSON object");
|
|
2880
|
+
}
|
|
2881
|
+
return payload;
|
|
2882
|
+
}
|
|
2883
|
+
async function fetchOAuth(rawUrl, settings, init = {}, hop = 0) {
|
|
2884
|
+
await assertOAuthFetchAllowed(rawUrl, settings);
|
|
2885
|
+
const response = await fetch(rawUrl, { ...init, redirect: "manual" });
|
|
2886
|
+
if (response.status < 300 || response.status >= 400) {
|
|
2887
|
+
return response;
|
|
2888
|
+
}
|
|
2889
|
+
if (hop >= 3) {
|
|
2890
|
+
throw new HTTPException4(422, { message: "OAuth fetch exceeded maximum redirect hops" });
|
|
2891
|
+
}
|
|
2892
|
+
const location = response.headers.get("location");
|
|
2893
|
+
if (!location) {
|
|
2894
|
+
throw new HTTPException4(422, { message: "OAuth fetch redirect was missing Location" });
|
|
2895
|
+
}
|
|
2896
|
+
let nextUrl;
|
|
2897
|
+
try {
|
|
2898
|
+
nextUrl = new URL(location, rawUrl).toString();
|
|
2899
|
+
} catch {
|
|
2900
|
+
throw new HTTPException4(422, { message: "OAuth fetch redirect Location was invalid" });
|
|
2901
|
+
}
|
|
2902
|
+
return await fetchOAuth(nextUrl, settings, init, hop + 1);
|
|
2903
|
+
}
|
|
2904
|
+
async function assertOAuthFetchAllowed(rawUrl, settings) {
|
|
2905
|
+
const url = new URL(rawUrl);
|
|
2906
|
+
if (!["https:", "http:"].includes(url.protocol)) {
|
|
2907
|
+
throw new HTTPException4(422, { message: "OAuth discovery only supports http and https URLs" });
|
|
2908
|
+
}
|
|
2909
|
+
if (settings.integrationsAllowPrivateNetworkTargets || ["local", "test"].includes(settings.environment)) {
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
if (url.protocol !== "https:") {
|
|
2913
|
+
throw new HTTPException4(422, { message: "OAuth discovery targets must use https outside local/test" });
|
|
2914
|
+
}
|
|
2915
|
+
const hostname = url.hostname.toLowerCase();
|
|
2916
|
+
if (hostname === "localhost" || hostname.endsWith(".localhost")) {
|
|
2917
|
+
throw new HTTPException4(422, { message: "OAuth discovery may not target localhost" });
|
|
2918
|
+
}
|
|
2919
|
+
const literal2 = isIP(hostname);
|
|
2920
|
+
const addresses = literal2 ? [hostname] : (await lookup(hostname, { all: true })).map((entry) => entry.address);
|
|
2921
|
+
if (addresses.some(isPrivateAddress)) {
|
|
2922
|
+
throw new HTTPException4(422, { message: "OAuth discovery may not target private network addresses" });
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
function parseWwwAuthenticate(header) {
|
|
2926
|
+
if (!header) {
|
|
2927
|
+
return {};
|
|
2928
|
+
}
|
|
2929
|
+
const bearerIndex = header.toLowerCase().indexOf("bearer");
|
|
2930
|
+
if (bearerIndex < 0) {
|
|
2931
|
+
return {};
|
|
2932
|
+
}
|
|
2933
|
+
const paramsText = header.slice(bearerIndex + "bearer".length);
|
|
2934
|
+
const params = {};
|
|
2935
|
+
const re = /([a-zA-Z_][a-zA-Z0-9_-]*)\s*=\s*("(?:[^"\\]|\\.)*"|[^,\s]+)/g;
|
|
2936
|
+
let match;
|
|
2937
|
+
while ((match = re.exec(paramsText)) !== null) {
|
|
2938
|
+
const raw = match[2];
|
|
2939
|
+
params[match[1].toLowerCase()] = raw.startsWith('"') ? raw.slice(1, -1).replace(/\\"/g, '"') : raw;
|
|
2940
|
+
}
|
|
2941
|
+
return {
|
|
2942
|
+
...params.resource_metadata ? { resourceMetadata: params.resource_metadata } : {},
|
|
2943
|
+
...params.scope ? { scope: params.scope.split(/\s+/).filter(Boolean) } : {},
|
|
2944
|
+
...params.error ? { error: params.error } : {}
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
function wellKnownCandidates(rawUrl, name) {
|
|
2948
|
+
const url = new URL(rawUrl);
|
|
2949
|
+
const path = url.pathname.replace(/^\/+|\/+$/g, "");
|
|
2950
|
+
return uniqueStrings([
|
|
2951
|
+
`${url.origin}/.well-known/${name}${path ? `/${path}` : ""}`,
|
|
2952
|
+
`${url.origin}${path ? `/${path}` : ""}/.well-known/${name}`,
|
|
2953
|
+
`${url.origin}/.well-known/${name}`
|
|
2954
|
+
]);
|
|
2955
|
+
}
|
|
2956
|
+
function chooseAuthorizeScopes(requested, challenged, supported) {
|
|
2957
|
+
if (requested?.length) {
|
|
2958
|
+
return uniqueStrings(requested);
|
|
2959
|
+
}
|
|
2960
|
+
if (challenged?.length) {
|
|
2961
|
+
return uniqueStrings(challenged);
|
|
2962
|
+
}
|
|
2963
|
+
return uniqueStrings(supported);
|
|
2964
|
+
}
|
|
2965
|
+
function grantedScopes(scopeText, fallback) {
|
|
2966
|
+
if (scopeText) {
|
|
2967
|
+
return uniqueStrings(scopeText.split(/\s+/).filter(Boolean));
|
|
2968
|
+
}
|
|
2969
|
+
return fallback;
|
|
2970
|
+
}
|
|
2971
|
+
function tokenAuthMethod(raw, hasSecret) {
|
|
2972
|
+
if (raw === "client_secret_post" || raw === "client_secret_basic") {
|
|
2973
|
+
return raw;
|
|
2974
|
+
}
|
|
2975
|
+
return hasSecret ? "client_secret_post" : "none";
|
|
2976
|
+
}
|
|
2977
|
+
function registrationMethod(value) {
|
|
2978
|
+
if (value === "operator" || value === "cimd" || value === "dcr") {
|
|
2979
|
+
return value;
|
|
2980
|
+
}
|
|
2981
|
+
throw new HTTPException4(400, { message: "invalid OAuth state" });
|
|
2982
|
+
}
|
|
2983
|
+
function expiresAtFromTokenResponse(payload) {
|
|
2984
|
+
const expiresAt = stringValue(payload.expires_at);
|
|
2985
|
+
if (expiresAt) {
|
|
2986
|
+
const parsed = new Date(expiresAt);
|
|
2987
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
2988
|
+
}
|
|
2989
|
+
const expiresIn = typeof payload.expires_in === "number" ? payload.expires_in : Number(payload.expires_in);
|
|
2990
|
+
if (Number.isFinite(expiresIn) && expiresIn > 0) {
|
|
2991
|
+
return new Date(Date.now() + expiresIn * 1e3);
|
|
2992
|
+
}
|
|
2993
|
+
return null;
|
|
2994
|
+
}
|
|
2995
|
+
function pkceChallenge(verifier) {
|
|
2996
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
2997
|
+
}
|
|
2998
|
+
function randomPkceVerifier() {
|
|
2999
|
+
return randomBytes(32).toString("base64url");
|
|
3000
|
+
}
|
|
3001
|
+
function uniqueStrings(values) {
|
|
3002
|
+
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
3003
|
+
}
|
|
3004
|
+
function stringArray(value) {
|
|
3005
|
+
return Array.isArray(value) ? uniqueStrings(value.filter((entry) => typeof entry === "string")) : [];
|
|
3006
|
+
}
|
|
3007
|
+
function stringValue(value) {
|
|
3008
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
3009
|
+
}
|
|
3010
|
+
function numberValue(value) {
|
|
3011
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3012
|
+
}
|
|
3013
|
+
function requiredString(value, field) {
|
|
3014
|
+
const result = stringValue(value);
|
|
3015
|
+
if (!result) {
|
|
3016
|
+
throw new HTTPException4(400, { message: `invalid OAuth state: missing ${field}` });
|
|
3017
|
+
}
|
|
3018
|
+
return result;
|
|
3019
|
+
}
|
|
3020
|
+
|
|
3021
|
+
// src/routes/connections.ts
|
|
3022
|
+
function registerConnectionRoutes(app, deps) {
|
|
3023
|
+
const { db, settings } = deps;
|
|
3024
|
+
function assertIntegrationsEnabled() {
|
|
3025
|
+
if (!settings.integrationsEnabled) {
|
|
3026
|
+
throw new HTTPException5(404, { message: "integrations are not enabled for this deployment" });
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
app.get("/v1/workspaces/:workspaceId/connections", async (c) => {
|
|
3030
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3031
|
+
const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:read");
|
|
3032
|
+
return c.json(ListConnectionsResponse.parse({
|
|
3033
|
+
connections: await listConnectionsMetadata(db, workspaceId, grant.subjectId)
|
|
3034
|
+
}));
|
|
3035
|
+
});
|
|
3036
|
+
app.post("/v1/workspaces/:workspaceId/connections", async (c) => {
|
|
3037
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3038
|
+
const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:write");
|
|
3039
|
+
const payload = CreateConnectionRequest.parse(await c.req.json());
|
|
3040
|
+
const key = requireEnvironmentEncryption3(settings);
|
|
3041
|
+
const subjectId = writableSubjectId(payload.subjectId, grant.subjectId);
|
|
3042
|
+
const connection = await createConnection2(db, {
|
|
3043
|
+
accountId: grant.accountId,
|
|
3044
|
+
workspaceId,
|
|
3045
|
+
subjectId,
|
|
3046
|
+
providerDomain: payload.providerDomain,
|
|
3047
|
+
kind: payload.kind,
|
|
3048
|
+
credentialEncrypted: encryptCredentialBundle(key, payload.credential),
|
|
3049
|
+
grantedScopes: payload.grantedScopes,
|
|
3050
|
+
expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null,
|
|
3051
|
+
metadata: payload.metadata,
|
|
3052
|
+
createdBySubjectId: grant.subjectId
|
|
3053
|
+
});
|
|
3054
|
+
return c.json(ConnectionResponse.parse({ connection }), 201);
|
|
3055
|
+
});
|
|
3056
|
+
app.get("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
|
|
1698
3057
|
const workspaceId = c.req.param("workspaceId");
|
|
1699
|
-
await
|
|
1700
|
-
const
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
throw new HTTPException3(404, { message: "codex account not found" });
|
|
3058
|
+
const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:read");
|
|
3059
|
+
const connection = await getConnectionMetadata2(db, workspaceId, c.req.param("connectionId"), grant.subjectId);
|
|
3060
|
+
if (!connection) {
|
|
3061
|
+
throw new HTTPException5(404, { message: "connection not found" });
|
|
1704
3062
|
}
|
|
1705
|
-
return c.json({
|
|
3063
|
+
return c.json(ConnectionResponse.parse({ connection }));
|
|
1706
3064
|
});
|
|
1707
|
-
app.patch("/v1/workspaces/:workspaceId/
|
|
3065
|
+
app.patch("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
|
|
1708
3066
|
const workspaceId = c.req.param("workspaceId");
|
|
1709
|
-
const grant = await
|
|
1710
|
-
const
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
throw new HTTPException3(400, { message: "invalid rotation strategy" });
|
|
3067
|
+
const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:write");
|
|
3068
|
+
const payload = UpdateConnectionRequest.parse(await c.req.json());
|
|
3069
|
+
if (payload.status !== void 0) {
|
|
3070
|
+
if (payload.status !== "active") {
|
|
3071
|
+
throw new HTTPException5(400, { message: 'status can only be set to "active"; use DELETE to revoke' });
|
|
3072
|
+
}
|
|
3073
|
+
if (payload.credential === void 0) {
|
|
3074
|
+
throw new HTTPException5(400, { message: "reactivating a connection requires a new credential" });
|
|
1718
3075
|
}
|
|
1719
|
-
patch.rotationStrategy = body.rotationStrategy;
|
|
1720
|
-
}
|
|
1721
|
-
if (patch.rotationEnabled === void 0 && patch.rotationStrategy === void 0) {
|
|
1722
|
-
throw new HTTPException3(400, { message: "no settings to update" });
|
|
1723
|
-
}
|
|
1724
|
-
await ensureCodexRotationSettings(db, grant.accountId, workspaceId);
|
|
1725
|
-
const updated = await updateCodexRotationSettings(db, workspaceId, patch);
|
|
1726
|
-
if (!updated) {
|
|
1727
|
-
throw new HTTPException3(404, { message: "codex rotation settings not found" });
|
|
1728
3076
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
3077
|
+
const key = payload.credential === void 0 ? null : requireEnvironmentEncryption3(settings);
|
|
3078
|
+
const subjectId = payload.subjectId === void 0 ? void 0 : writableSubjectId(payload.subjectId, grant.subjectId);
|
|
3079
|
+
const connection = await updateConnection2(db, {
|
|
3080
|
+
workspaceId,
|
|
3081
|
+
connectionId: c.req.param("connectionId"),
|
|
3082
|
+
visibleToSubjectId: grant.subjectId,
|
|
3083
|
+
updatedBySubjectId: grant.subjectId,
|
|
3084
|
+
...payload.providerDomain !== void 0 ? { providerDomain: payload.providerDomain } : {},
|
|
3085
|
+
...subjectId !== void 0 ? { subjectId } : {},
|
|
3086
|
+
...payload.kind !== void 0 ? { kind: payload.kind } : {},
|
|
3087
|
+
...payload.status !== void 0 ? { status: payload.status } : {},
|
|
3088
|
+
...payload.credential !== void 0 && key ? { credentialEncrypted: encryptCredentialBundle(key, payload.credential) } : {},
|
|
3089
|
+
...payload.grantedScopes !== void 0 ? { grantedScopes: payload.grantedScopes } : {},
|
|
3090
|
+
...payload.expiresAt !== void 0 ? { expiresAt: payload.expiresAt ? new Date(payload.expiresAt) : null } : {},
|
|
3091
|
+
...payload.metadata !== void 0 ? { metadata: payload.metadata } : {}
|
|
1733
3092
|
});
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
const workspaceId = c.req.param("workspaceId");
|
|
1737
|
-
await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
|
|
1738
|
-
const accountId = c.req.param("accountId");
|
|
1739
|
-
const body = await c.req.json();
|
|
1740
|
-
const label = typeof body.label === "string" ? body.label : null;
|
|
1741
|
-
const renamed = await renameCodexAccount(db, workspaceId, accountId, label);
|
|
1742
|
-
if (!renamed) {
|
|
1743
|
-
throw new HTTPException3(404, { message: "codex account not found" });
|
|
1744
|
-
}
|
|
1745
|
-
const accounts = await listCodexAccountStatuses(db, workspaceId);
|
|
1746
|
-
const row = accounts.find((account) => account.id === accountId);
|
|
1747
|
-
if (!row) {
|
|
1748
|
-
throw new HTTPException3(404, { message: "codex account not found" });
|
|
3093
|
+
if (!connection) {
|
|
3094
|
+
throw new HTTPException5(404, { message: "connection not found" });
|
|
1749
3095
|
}
|
|
1750
|
-
return c.json(
|
|
1751
|
-
});
|
|
1752
|
-
app.delete("/v1/workspaces/:workspaceId/codex/accounts/:accountId", async (c) => {
|
|
1753
|
-
const workspaceId = c.req.param("workspaceId");
|
|
1754
|
-
await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
|
|
1755
|
-
const accountId = c.req.param("accountId");
|
|
1756
|
-
const result = await disconnectCodexAccount(db, workspaceId, accountId);
|
|
1757
|
-
return c.json({ disconnected: result.removed, newActiveId: result.newActiveCredentialId });
|
|
1758
|
-
});
|
|
1759
|
-
app.delete("/v1/workspaces/:workspaceId/codex", async (c) => {
|
|
1760
|
-
const workspaceId = c.req.param("workspaceId");
|
|
1761
|
-
await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
|
|
1762
|
-
const removed = await disconnectAllCodexAccounts(db, workspaceId);
|
|
1763
|
-
return c.json({ disconnected: removed > 0 });
|
|
3096
|
+
return c.json(ConnectionResponse.parse({ connection }));
|
|
1764
3097
|
});
|
|
1765
|
-
app.
|
|
3098
|
+
app.delete("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
|
|
1766
3099
|
const workspaceId = c.req.param("workspaceId");
|
|
1767
|
-
await
|
|
1768
|
-
const
|
|
1769
|
-
if (!
|
|
1770
|
-
throw new
|
|
3100
|
+
const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:write");
|
|
3101
|
+
const connection = await revokeConnection(db, workspaceId, c.req.param("connectionId"), grant.subjectId);
|
|
3102
|
+
if (!connection) {
|
|
3103
|
+
throw new HTTPException5(404, { message: "connection not found" });
|
|
1771
3104
|
}
|
|
1772
|
-
|
|
1773
|
-
return c.json(codexUsageJson(payload));
|
|
3105
|
+
return c.json(ConnectionResponse.parse({ connection }));
|
|
1774
3106
|
});
|
|
1775
|
-
app.
|
|
3107
|
+
app.post("/v1/workspaces/:workspaceId/connections/oauth/start", async (c) => {
|
|
3108
|
+
assertIntegrationsEnabled();
|
|
1776
3109
|
const workspaceId = c.req.param("workspaceId");
|
|
1777
|
-
await
|
|
1778
|
-
const
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
throw new HTTPException3(404, { message: "codex account not found" });
|
|
3110
|
+
const grant = await requireAccessGrant3(c, deps, workspaceId, "connections:write");
|
|
3111
|
+
const parsed = OAuthStartRequest.safeParse(await c.req.json());
|
|
3112
|
+
if (!parsed.success) {
|
|
3113
|
+
throw new HTTPException5(400, { message: parsed.error.issues[0]?.message ?? "invalid OAuth start request" });
|
|
1782
3114
|
}
|
|
1783
|
-
const payload =
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
3115
|
+
const payload = parsed.data;
|
|
3116
|
+
const result = await startMcpOAuth({ db, settings }, {
|
|
3117
|
+
accountId: grant.accountId,
|
|
3118
|
+
workspaceId,
|
|
3119
|
+
subjectId: grant.subjectId,
|
|
3120
|
+
requestUrl: c.req.url,
|
|
3121
|
+
payload
|
|
3122
|
+
});
|
|
3123
|
+
return c.json(OAuthStartResponse2.parse(result));
|
|
3124
|
+
});
|
|
3125
|
+
app.get("/v1/integrations/oauth/callback", async (c) => {
|
|
3126
|
+
assertIntegrationsEnabled();
|
|
3127
|
+
const result = await completeMcpOAuthCallback({ db, settings }, {
|
|
3128
|
+
code: c.req.query("code"),
|
|
3129
|
+
state: c.req.query("state"),
|
|
3130
|
+
requestUrl: c.req.url
|
|
3131
|
+
});
|
|
3132
|
+
return c.redirect(result.redirectTo, 302);
|
|
3133
|
+
});
|
|
3134
|
+
app.get("/v1/integrations/oauth/client-metadata.json", (c) => {
|
|
3135
|
+
const baseUrl = integrationBaseUrl(settings.publicBaseUrl, c.req.url);
|
|
3136
|
+
const metadataUrl = `${baseUrl}/v1/integrations/oauth/client-metadata.json`;
|
|
3137
|
+
return c.json(IntegrationClientMetadata.parse({
|
|
3138
|
+
client_id: metadataUrl,
|
|
3139
|
+
client_name: "OpenGeni",
|
|
3140
|
+
redirect_uris: [`${baseUrl}/v1/integrations/oauth/callback`],
|
|
3141
|
+
token_endpoint_auth_method: "none",
|
|
3142
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
3143
|
+
response_types: ["code"]
|
|
3144
|
+
}));
|
|
1804
3145
|
});
|
|
1805
3146
|
}
|
|
3147
|
+
function writableSubjectId(requested, grantSubjectId) {
|
|
3148
|
+
if (requested == null) {
|
|
3149
|
+
return null;
|
|
3150
|
+
}
|
|
3151
|
+
if (requested !== grantSubjectId) {
|
|
3152
|
+
throw new HTTPException5(403, { message: "cannot write a connection for another subject" });
|
|
3153
|
+
}
|
|
3154
|
+
return requested;
|
|
3155
|
+
}
|
|
3156
|
+
function encryptCredentialBundle(key, credential) {
|
|
3157
|
+
return encryptEnvironmentValue4(key, JSON.stringify(credential));
|
|
3158
|
+
}
|
|
1806
3159
|
|
|
1807
3160
|
// src/routes/documents.ts
|
|
1808
3161
|
import {
|
|
1809
3162
|
AddDocumentRequest,
|
|
3163
|
+
CreateKnowledgeMemoryRequest,
|
|
1810
3164
|
CreateDocumentBaseRequest,
|
|
1811
3165
|
Document,
|
|
1812
3166
|
DocumentBase,
|
|
1813
|
-
DocumentSearchRequest
|
|
3167
|
+
DocumentSearchRequest,
|
|
3168
|
+
KnowledgeMemory,
|
|
3169
|
+
KnowledgeMemorySearchRequest,
|
|
3170
|
+
UpdateKnowledgeMemoryRequest
|
|
1814
3171
|
} from "@opengeni/contracts";
|
|
3172
|
+
import {
|
|
3173
|
+
createKnowledgeMemory as createKnowledgeMemory2,
|
|
3174
|
+
getKnowledgeMemory,
|
|
3175
|
+
listKnowledgeMemories as listKnowledgeMemories2,
|
|
3176
|
+
updateKnowledgeMemory
|
|
3177
|
+
} from "@opengeni/db";
|
|
1815
3178
|
import {
|
|
1816
3179
|
addDocumentToBase,
|
|
1817
3180
|
createDocumentBase,
|
|
@@ -1824,8 +3187,8 @@ import {
|
|
|
1824
3187
|
searchDocuments as searchDocuments2
|
|
1825
3188
|
} from "@opengeni/documents";
|
|
1826
3189
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
1827
|
-
import { HTTPException as
|
|
1828
|
-
import { requireAccessGrant as
|
|
3190
|
+
import { HTTPException as HTTPException6 } from "hono/http-exception";
|
|
3191
|
+
import { requireAccessGrant as requireAccessGrant4 } from "@opengeni/core";
|
|
1829
3192
|
import { recordWorkspaceUsage as recordWorkspaceUsage2, requireLimit as requireLimit2 } from "@opengeni/core";
|
|
1830
3193
|
|
|
1831
3194
|
// src/mcp/documents.ts
|
|
@@ -1834,9 +3197,29 @@ import {
|
|
|
1834
3197
|
listDocumentBases,
|
|
1835
3198
|
searchDocuments
|
|
1836
3199
|
} from "@opengeni/documents";
|
|
3200
|
+
import {
|
|
3201
|
+
createKnowledgeMemory,
|
|
3202
|
+
listKnowledgeMemories
|
|
3203
|
+
} from "@opengeni/db";
|
|
1837
3204
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1838
3205
|
import * as z from "zod/v4";
|
|
1839
|
-
|
|
3206
|
+
var SearchInputSchema = {
|
|
3207
|
+
query: z.string().min(1),
|
|
3208
|
+
baseIds: z.array(z.string().uuid()).optional(),
|
|
3209
|
+
limit: z.number().int().positive().max(50).optional(),
|
|
3210
|
+
mode: z.enum(["hybrid", "vector", "keyword"]).optional(),
|
|
3211
|
+
sourceKinds: z.array(z.enum(["manual_upload", "meeting_transcript", "repository", "email", "chat", "document", "web", "other"])).optional(),
|
|
3212
|
+
aclTags: z.array(z.string().min(1)).optional()
|
|
3213
|
+
};
|
|
3214
|
+
var MemoryKindSchema = z.enum(["semantic", "episodic", "procedural", "decision", "preference"]);
|
|
3215
|
+
var SourceRefSchema = z.object({
|
|
3216
|
+
kind: z.enum(["document_chunk", "document", "session_event", "memory", "external"]),
|
|
3217
|
+
id: z.string().min(1),
|
|
3218
|
+
uri: z.string().min(1).optional(),
|
|
3219
|
+
title: z.string().min(1).optional(),
|
|
3220
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
3221
|
+
});
|
|
3222
|
+
function buildDocumentsMcpServer(db, accountId, workspaceId, documentServices, options = {}) {
|
|
1840
3223
|
const server = new McpServer2({
|
|
1841
3224
|
name: "opengeni-documents",
|
|
1842
3225
|
version: "1.0.0"
|
|
@@ -1848,27 +3231,29 @@ function buildDocumentsMcpServer(db, workspaceId, documentServices) {
|
|
|
1848
3231
|
content: [{ type: "text", text: JSON.stringify(await listDocumentBases(db, workspaceId)) }]
|
|
1849
3232
|
}));
|
|
1850
3233
|
server.registerTool("search_documents", {
|
|
1851
|
-
description: "Search indexed documents.",
|
|
1852
|
-
inputSchema:
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
}, async (
|
|
1858
|
-
content: [{
|
|
1859
|
-
type: "text",
|
|
1860
|
-
text: JSON.stringify(await searchDocuments(db, {
|
|
1861
|
-
workspaceId,
|
|
1862
|
-
query,
|
|
1863
|
-
...baseIds ? { baseIds } : {},
|
|
1864
|
-
...limit ? { limit } : {}
|
|
1865
|
-
}, documentServices))
|
|
1866
|
-
}]
|
|
1867
|
-
}));
|
|
3234
|
+
description: "Search indexed documents with hybrid, vector, or keyword retrieval.",
|
|
3235
|
+
inputSchema: SearchInputSchema
|
|
3236
|
+
}, async (input) => searchContent(db, workspaceId, documentServices, input));
|
|
3237
|
+
server.registerTool("knowledge_search", {
|
|
3238
|
+
description: "Search company knowledge sources with optional base, source-kind, ACL, and retrieval-mode filters.",
|
|
3239
|
+
inputSchema: SearchInputSchema
|
|
3240
|
+
}, async (input) => searchContent(db, workspaceId, documentServices, input));
|
|
1868
3241
|
server.registerTool("fetch_document_chunk", {
|
|
1869
3242
|
description: "Fetch one indexed document chunk by id.",
|
|
1870
3243
|
inputSchema: {
|
|
1871
|
-
chunkId: z.string()
|
|
3244
|
+
chunkId: z.string().uuid()
|
|
3245
|
+
}
|
|
3246
|
+
}, async ({ chunkId }) => {
|
|
3247
|
+
const found = await getDocumentChunk(db, workspaceId, chunkId);
|
|
3248
|
+
return {
|
|
3249
|
+
content: [{ type: "text", text: found ? JSON.stringify(found) : `chunk not found: ${chunkId}` }],
|
|
3250
|
+
isError: !found
|
|
3251
|
+
};
|
|
3252
|
+
});
|
|
3253
|
+
server.registerTool("knowledge_fetch", {
|
|
3254
|
+
description: "Fetch one knowledge source chunk by id.",
|
|
3255
|
+
inputSchema: {
|
|
3256
|
+
chunkId: z.string().uuid()
|
|
1872
3257
|
}
|
|
1873
3258
|
}, async ({ chunkId }) => {
|
|
1874
3259
|
const found = await getDocumentChunk(db, workspaceId, chunkId);
|
|
@@ -1877,42 +3262,99 @@ function buildDocumentsMcpServer(db, workspaceId, documentServices) {
|
|
|
1877
3262
|
isError: !found
|
|
1878
3263
|
};
|
|
1879
3264
|
});
|
|
3265
|
+
server.registerTool("memory_search", {
|
|
3266
|
+
description: "Search approved company memory records.",
|
|
3267
|
+
inputSchema: {
|
|
3268
|
+
query: z.string().min(1).optional(),
|
|
3269
|
+
kind: MemoryKindSchema.optional(),
|
|
3270
|
+
scope: z.string().min(1).optional(),
|
|
3271
|
+
limit: z.number().int().positive().max(100).optional()
|
|
3272
|
+
}
|
|
3273
|
+
}, async ({ query, kind, scope, limit }) => ({
|
|
3274
|
+
content: [{ type: "text", text: JSON.stringify(await listKnowledgeMemories(db, workspaceId, {
|
|
3275
|
+
...query ? { query } : {},
|
|
3276
|
+
status: "approved",
|
|
3277
|
+
...kind ? { kind } : {},
|
|
3278
|
+
...scope ? { scope } : {},
|
|
3279
|
+
...limit ? { limit } : {}
|
|
3280
|
+
})) }]
|
|
3281
|
+
}));
|
|
3282
|
+
server.registerTool("memory_propose", {
|
|
3283
|
+
description: "Propose a company memory record for human review.",
|
|
3284
|
+
inputSchema: {
|
|
3285
|
+
text: z.string().min(1),
|
|
3286
|
+
kind: MemoryKindSchema.optional(),
|
|
3287
|
+
scope: z.string().min(1).optional(),
|
|
3288
|
+
sourceRefs: z.array(SourceRefSchema).optional(),
|
|
3289
|
+
confidence: z.number().min(0).max(1).optional(),
|
|
3290
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
3291
|
+
}
|
|
3292
|
+
}, async ({ text, kind, scope, sourceRefs, confidence, metadata }) => ({
|
|
3293
|
+
content: [{ type: "text", text: JSON.stringify(await createKnowledgeMemory(db, {
|
|
3294
|
+
accountId,
|
|
3295
|
+
workspaceId,
|
|
3296
|
+
status: "proposed",
|
|
3297
|
+
kind: kind ?? "semantic",
|
|
3298
|
+
scope: scope ?? "workspace",
|
|
3299
|
+
text,
|
|
3300
|
+
sourceRefs: sourceRefs?.map((sourceRef) => ({ ...sourceRef, metadata: sourceRef.metadata ?? {} })) ?? [],
|
|
3301
|
+
confidence: confidence ?? 0.5,
|
|
3302
|
+
metadata: metadata ?? {},
|
|
3303
|
+
createdBySessionId: options.createdBySessionId
|
|
3304
|
+
})) }]
|
|
3305
|
+
}));
|
|
1880
3306
|
return server;
|
|
1881
3307
|
}
|
|
3308
|
+
async function searchContent(db, workspaceId, documentServices, input) {
|
|
3309
|
+
return {
|
|
3310
|
+
content: [{
|
|
3311
|
+
type: "text",
|
|
3312
|
+
text: JSON.stringify(await searchDocuments(db, {
|
|
3313
|
+
workspaceId,
|
|
3314
|
+
query: input.query,
|
|
3315
|
+
...input.baseIds ? { baseIds: input.baseIds } : {},
|
|
3316
|
+
...input.limit ? { limit: input.limit } : {},
|
|
3317
|
+
...input.mode ? { mode: input.mode } : {},
|
|
3318
|
+
...input.sourceKinds ? { sourceKinds: input.sourceKinds } : {},
|
|
3319
|
+
...input.aclTags ? { aclTags: input.aclTags } : {}
|
|
3320
|
+
}, documentServices))
|
|
3321
|
+
}]
|
|
3322
|
+
};
|
|
3323
|
+
}
|
|
1882
3324
|
|
|
1883
3325
|
// src/routes/documents.ts
|
|
1884
3326
|
function registerDocumentRoutes(app, deps) {
|
|
1885
3327
|
const { db, objectStorage, documentIndexer, getDocumentServices } = deps;
|
|
1886
3328
|
app.post("/v1/workspaces/:workspaceId/document-bases", async (c) => {
|
|
1887
3329
|
const workspaceId = c.req.param("workspaceId");
|
|
1888
|
-
const grant = await
|
|
3330
|
+
const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:manage");
|
|
1889
3331
|
const payload = CreateDocumentBaseRequest.parse(await c.req.json());
|
|
1890
3332
|
return c.json(DocumentBase.parse(await createDocumentBase(db, { ...payload, accountId: grant.accountId, workspaceId })), 201);
|
|
1891
3333
|
});
|
|
1892
3334
|
app.get("/v1/workspaces/:workspaceId/document-bases", async (c) => {
|
|
1893
3335
|
const workspaceId = c.req.param("workspaceId");
|
|
1894
|
-
await
|
|
3336
|
+
await requireAccessGrant4(c, deps, workspaceId, "documents:search");
|
|
1895
3337
|
return c.json((await listDocumentBases2(db, workspaceId)).map((base) => DocumentBase.parse(base)));
|
|
1896
3338
|
});
|
|
1897
3339
|
app.get("/v1/workspaces/:workspaceId/document-bases/:baseId", async (c) => {
|
|
1898
3340
|
const workspaceId = c.req.param("workspaceId");
|
|
1899
|
-
await
|
|
3341
|
+
await requireAccessGrant4(c, deps, workspaceId, "documents:search");
|
|
1900
3342
|
const base = await getDocumentBase(db, workspaceId, c.req.param("baseId"));
|
|
1901
3343
|
if (!base) {
|
|
1902
|
-
throw new
|
|
3344
|
+
throw new HTTPException6(404, { message: "document base not found" });
|
|
1903
3345
|
}
|
|
1904
3346
|
return c.json(DocumentBase.parse(base));
|
|
1905
3347
|
});
|
|
1906
3348
|
app.post("/v1/workspaces/:workspaceId/document-bases/:baseId/documents", async (c) => {
|
|
1907
3349
|
const workspaceId = c.req.param("workspaceId");
|
|
1908
|
-
const grant = await
|
|
3350
|
+
const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:manage");
|
|
1909
3351
|
if (!objectStorage) {
|
|
1910
|
-
throw new
|
|
3352
|
+
throw new HTTPException6(503, { message: "object storage is not configured" });
|
|
1911
3353
|
}
|
|
1912
3354
|
await requireLimit2(deps, { accountId: grant.accountId, workspaceId, action: "document:index", quantity: 0 });
|
|
1913
3355
|
const payload = AddDocumentRequest.parse(await c.req.json());
|
|
1914
3356
|
try {
|
|
1915
|
-
const document = await addDocumentToBase(db, { accountId: grant.accountId, workspaceId, baseId: c.req.param("baseId")
|
|
3357
|
+
const document = await addDocumentToBase(db, { ...payload, accountId: grant.accountId, workspaceId, baseId: c.req.param("baseId") });
|
|
1916
3358
|
const wasCreated = document.status === "queued" && document.chunkCount === 0 && document.error === null;
|
|
1917
3359
|
const indexed = document.status === "ready" ? document : await documentIndexer.indexDocument({ accountId: grant.accountId, workspaceId, documentId: document.id }) ?? document;
|
|
1918
3360
|
if (indexed.status === "ready") {
|
|
@@ -1935,12 +3377,12 @@ function registerDocumentRoutes(app, deps) {
|
|
|
1935
3377
|
});
|
|
1936
3378
|
app.get("/v1/workspaces/:workspaceId/document-bases/:baseId/documents", async (c) => {
|
|
1937
3379
|
const workspaceId = c.req.param("workspaceId");
|
|
1938
|
-
await
|
|
3380
|
+
await requireAccessGrant4(c, deps, workspaceId, "documents:search");
|
|
1939
3381
|
return c.json((await listDocuments(db, workspaceId, c.req.param("baseId"))).map((document) => Document.parse(document)));
|
|
1940
3382
|
});
|
|
1941
3383
|
app.delete("/v1/workspaces/:workspaceId/document-bases/:baseId/documents/:documentId", async (c) => {
|
|
1942
3384
|
const workspaceId = c.req.param("workspaceId");
|
|
1943
|
-
const grant = await
|
|
3385
|
+
const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:manage");
|
|
1944
3386
|
try {
|
|
1945
3387
|
await deleteDocumentFromBase(db, {
|
|
1946
3388
|
accountId: grant.accountId,
|
|
@@ -1955,21 +3397,21 @@ function registerDocumentRoutes(app, deps) {
|
|
|
1955
3397
|
});
|
|
1956
3398
|
app.post("/v1/workspaces/:workspaceId/document-bases/:baseId/documents/:documentId/reindex", async (c) => {
|
|
1957
3399
|
const workspaceId = c.req.param("workspaceId");
|
|
1958
|
-
const grant = await
|
|
3400
|
+
const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:manage");
|
|
1959
3401
|
if (!objectStorage) {
|
|
1960
|
-
throw new
|
|
3402
|
+
throw new HTTPException6(503, { message: "object storage is not configured" });
|
|
1961
3403
|
}
|
|
1962
3404
|
await requireLimit2(deps, { accountId: grant.accountId, workspaceId, action: "document:index", quantity: 0 });
|
|
1963
3405
|
try {
|
|
1964
3406
|
const document = await getDocument(db, workspaceId, c.req.param("documentId"));
|
|
1965
3407
|
if (!document) {
|
|
1966
|
-
throw new
|
|
3408
|
+
throw new HTTPException6(404, { message: "document not found" });
|
|
1967
3409
|
}
|
|
1968
3410
|
if (document.status !== "failed") {
|
|
1969
|
-
throw new
|
|
3411
|
+
throw new HTTPException6(422, { message: "only failed documents can be retried" });
|
|
1970
3412
|
}
|
|
1971
3413
|
if (document.baseId !== c.req.param("baseId")) {
|
|
1972
|
-
throw new
|
|
3414
|
+
throw new HTTPException6(404, { message: "document not found" });
|
|
1973
3415
|
}
|
|
1974
3416
|
const queued = await queueDocumentForReindex(db, workspaceId, document.id);
|
|
1975
3417
|
const indexed = await documentIndexer.indexDocument({ accountId: grant.accountId, workspaceId, documentId: document.id }) ?? queued;
|
|
@@ -1988,7 +3430,7 @@ function registerDocumentRoutes(app, deps) {
|
|
|
1988
3430
|
}
|
|
1989
3431
|
return c.json(Document.parse(indexed));
|
|
1990
3432
|
} catch (error) {
|
|
1991
|
-
if (error instanceof
|
|
3433
|
+
if (error instanceof HTTPException6) {
|
|
1992
3434
|
throw error;
|
|
1993
3435
|
}
|
|
1994
3436
|
throw documentHttpException(error);
|
|
@@ -1996,26 +3438,94 @@ function registerDocumentRoutes(app, deps) {
|
|
|
1996
3438
|
});
|
|
1997
3439
|
app.post("/v1/workspaces/:workspaceId/document-bases/:baseId/search", async (c) => {
|
|
1998
3440
|
const workspaceId = c.req.param("workspaceId");
|
|
1999
|
-
await
|
|
3441
|
+
await requireAccessGrant4(c, deps, workspaceId, "documents:search");
|
|
2000
3442
|
const payload = DocumentSearchRequest.parse(await c.req.json());
|
|
2001
3443
|
const base = await getDocumentBase(db, workspaceId, c.req.param("baseId"));
|
|
2002
3444
|
if (!base) {
|
|
2003
|
-
throw new
|
|
3445
|
+
throw new HTTPException6(404, { message: "document base not found" });
|
|
2004
3446
|
}
|
|
2005
3447
|
return c.json({
|
|
2006
3448
|
results: await searchDocuments2(db, {
|
|
2007
3449
|
workspaceId,
|
|
2008
3450
|
baseIds: [base.id],
|
|
2009
3451
|
query: payload.query,
|
|
2010
|
-
limit: payload.limit
|
|
3452
|
+
limit: payload.limit,
|
|
3453
|
+
mode: payload.mode,
|
|
3454
|
+
sourceKinds: payload.sourceKinds,
|
|
3455
|
+
aclTags: payload.aclTags
|
|
3456
|
+
}, getDocumentServices())
|
|
3457
|
+
});
|
|
3458
|
+
});
|
|
3459
|
+
app.post("/v1/workspaces/:workspaceId/knowledge/search", async (c) => {
|
|
3460
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3461
|
+
await requireAccessGrant4(c, deps, workspaceId, "documents:search");
|
|
3462
|
+
const payload = DocumentSearchRequest.parse(await c.req.json());
|
|
3463
|
+
return c.json({
|
|
3464
|
+
results: await searchDocuments2(db, {
|
|
3465
|
+
workspaceId,
|
|
3466
|
+
query: payload.query,
|
|
3467
|
+
baseIds: payload.baseIds,
|
|
3468
|
+
limit: payload.limit,
|
|
3469
|
+
mode: payload.mode,
|
|
3470
|
+
sourceKinds: payload.sourceKinds,
|
|
3471
|
+
aclTags: payload.aclTags
|
|
2011
3472
|
}, getDocumentServices())
|
|
2012
3473
|
});
|
|
2013
3474
|
});
|
|
3475
|
+
app.get("/v1/workspaces/:workspaceId/knowledge/memories", async (c) => {
|
|
3476
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3477
|
+
await requireAccessGrant4(c, deps, workspaceId, "documents:search");
|
|
3478
|
+
const parsed = KnowledgeMemorySearchRequest.safeParse({
|
|
3479
|
+
query: c.req.query("query") || void 0,
|
|
3480
|
+
status: c.req.query("status") || void 0,
|
|
3481
|
+
kind: c.req.query("kind") || void 0,
|
|
3482
|
+
scope: c.req.query("scope") || void 0,
|
|
3483
|
+
limit: c.req.query("limit") ? Number(c.req.query("limit")) : void 0
|
|
3484
|
+
});
|
|
3485
|
+
if (!parsed.success) {
|
|
3486
|
+
throw new HTTPException6(400, { message: "invalid knowledge memory query parameters" });
|
|
3487
|
+
}
|
|
3488
|
+
return c.json((await listKnowledgeMemories2(db, workspaceId, parsed.data)).map((memory) => KnowledgeMemory.parse(memory)));
|
|
3489
|
+
});
|
|
3490
|
+
app.get("/v1/workspaces/:workspaceId/knowledge/memories/:memoryId", async (c) => {
|
|
3491
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3492
|
+
await requireAccessGrant4(c, deps, workspaceId, "documents:search");
|
|
3493
|
+
const memory = await getKnowledgeMemory(db, workspaceId, c.req.param("memoryId"));
|
|
3494
|
+
if (!memory) {
|
|
3495
|
+
throw new HTTPException6(404, { message: "knowledge memory not found" });
|
|
3496
|
+
}
|
|
3497
|
+
return c.json(KnowledgeMemory.parse(memory));
|
|
3498
|
+
});
|
|
3499
|
+
app.post("/v1/workspaces/:workspaceId/knowledge/memories", async (c) => {
|
|
3500
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3501
|
+
const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:manage");
|
|
3502
|
+
const payload = CreateKnowledgeMemoryRequest.parse(await c.req.json());
|
|
3503
|
+
return c.json(KnowledgeMemory.parse(await createKnowledgeMemory2(db, {
|
|
3504
|
+
...payload,
|
|
3505
|
+
accountId: grant.accountId,
|
|
3506
|
+
workspaceId
|
|
3507
|
+
})), 201);
|
|
3508
|
+
});
|
|
3509
|
+
app.patch("/v1/workspaces/:workspaceId/knowledge/memories/:memoryId", async (c) => {
|
|
3510
|
+
const workspaceId = c.req.param("workspaceId");
|
|
3511
|
+
const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:manage");
|
|
3512
|
+
const payload = UpdateKnowledgeMemoryRequest.parse(await c.req.json());
|
|
3513
|
+
const reviewedBy = payload.reviewedBy ?? (payload.status === "approved" || payload.status === "rejected" ? grant.subjectLabel ?? grant.subjectId : void 0);
|
|
3514
|
+
try {
|
|
3515
|
+
return c.json(KnowledgeMemory.parse(await updateKnowledgeMemory(db, workspaceId, c.req.param("memoryId"), {
|
|
3516
|
+
...payload,
|
|
3517
|
+
...reviewedBy ? { reviewedBy } : {}
|
|
3518
|
+
})));
|
|
3519
|
+
} catch (error) {
|
|
3520
|
+
throw documentHttpException(error);
|
|
3521
|
+
}
|
|
3522
|
+
});
|
|
2014
3523
|
app.all("/v1/workspaces/:workspaceId/mcp/docs", async (c) => {
|
|
2015
3524
|
const workspaceId = c.req.param("workspaceId");
|
|
2016
|
-
await
|
|
3525
|
+
const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:search");
|
|
3526
|
+
const sessionId = typeof grant.metadata?.sessionId === "string" ? grant.metadata.sessionId : void 0;
|
|
2017
3527
|
const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
|
|
2018
|
-
const server = buildDocumentsMcpServer(db, workspaceId, getDocumentServices());
|
|
3528
|
+
const server = buildDocumentsMcpServer(db, grant.accountId, workspaceId, getDocumentServices(), { createdBySessionId: sessionId });
|
|
2019
3529
|
await server.connect(transport);
|
|
2020
3530
|
return await transport.handleRequest(c.req.raw);
|
|
2021
3531
|
});
|
|
@@ -2023,12 +3533,12 @@ function registerDocumentRoutes(app, deps) {
|
|
|
2023
3533
|
function documentHttpException(error) {
|
|
2024
3534
|
const message = error instanceof Error ? error.message : String(error);
|
|
2025
3535
|
if (message.includes("not found")) {
|
|
2026
|
-
return new
|
|
3536
|
+
return new HTTPException6(404, { message });
|
|
2027
3537
|
}
|
|
2028
3538
|
if (message.includes("pending") || message.includes("failed") || message.includes("deleted")) {
|
|
2029
|
-
return new
|
|
3539
|
+
return new HTTPException6(422, { message });
|
|
2030
3540
|
}
|
|
2031
|
-
return new
|
|
3541
|
+
return new HTTPException6(500, { message });
|
|
2032
3542
|
}
|
|
2033
3543
|
|
|
2034
3544
|
// src/routes/enrollments.ts
|
|
@@ -2054,11 +3564,11 @@ import {
|
|
|
2054
3564
|
listEnrollments,
|
|
2055
3565
|
revokeEnrollment
|
|
2056
3566
|
} from "@opengeni/db";
|
|
2057
|
-
import { HTTPException as
|
|
2058
|
-
import { requireAccessGrant as
|
|
3567
|
+
import { HTTPException as HTTPException7 } from "hono/http-exception";
|
|
3568
|
+
import { requireAccessGrant as requireAccessGrant5 } from "@opengeni/core";
|
|
2059
3569
|
|
|
2060
3570
|
// src/sandbox/enrollment.ts
|
|
2061
|
-
import { randomBytes } from "crypto";
|
|
3571
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
2062
3572
|
import {
|
|
2063
3573
|
resolveEnrollmentSigningSecret,
|
|
2064
3574
|
resolveRelayTokenSecret
|
|
@@ -2088,11 +3598,11 @@ var ENROLLMENT_BEARER_TTL_SECONDS = 30 * 24 * 3600;
|
|
|
2088
3598
|
var RELAY_TOKEN_TTL_SECONDS = 30 * 24 * 3600;
|
|
2089
3599
|
var ENROLL_TOKEN_TTL_SECONDS = 3600;
|
|
2090
3600
|
function mintDeviceCode() {
|
|
2091
|
-
return
|
|
3601
|
+
return randomBytes2(32).toString("base64url");
|
|
2092
3602
|
}
|
|
2093
3603
|
var USER_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
2094
3604
|
function mintUserCode() {
|
|
2095
|
-
const bytes =
|
|
3605
|
+
const bytes = randomBytes2(8);
|
|
2096
3606
|
let out = "";
|
|
2097
3607
|
for (let i = 0; i < 8; i += 1) {
|
|
2098
3608
|
out += USER_CODE_ALPHABET[bytes[i] % USER_CODE_ALPHABET.length];
|
|
@@ -2170,18 +3680,18 @@ async function lookupDeviceEnrollment(services, input) {
|
|
|
2170
3680
|
const { db } = services;
|
|
2171
3681
|
return await getPendingDeviceEnrollmentRequestByUserCodeGlobal(db, input.userCode);
|
|
2172
3682
|
}
|
|
2173
|
-
function toLookupResponse(
|
|
3683
|
+
function toLookupResponse(record3) {
|
|
2174
3684
|
return {
|
|
2175
|
-
workspaceId:
|
|
2176
|
-
userCode:
|
|
3685
|
+
workspaceId: record3.workspaceId,
|
|
3686
|
+
userCode: record3.userCode,
|
|
2177
3687
|
machine: {
|
|
2178
|
-
machineName:
|
|
2179
|
-
os:
|
|
2180
|
-
arch:
|
|
2181
|
-
canOfferDisplay:
|
|
2182
|
-
requestsScreenControl:
|
|
3688
|
+
machineName: record3.machineName,
|
|
3689
|
+
os: record3.os,
|
|
3690
|
+
arch: record3.arch,
|
|
3691
|
+
canOfferDisplay: record3.canOfferDisplay,
|
|
3692
|
+
requestsScreenControl: record3.requestsScreenControl
|
|
2183
3693
|
},
|
|
2184
|
-
expiresAt:
|
|
3694
|
+
expiresAt: record3.expiresAt
|
|
2185
3695
|
};
|
|
2186
3696
|
}
|
|
2187
3697
|
async function denyDeviceEnrollment(services, input) {
|
|
@@ -2338,7 +3848,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
2338
3848
|
const { settings, db } = deps;
|
|
2339
3849
|
function assertSelfhostedEnabled() {
|
|
2340
3850
|
if (!settings.sandboxSelfhostedEnabled) {
|
|
2341
|
-
throw new
|
|
3851
|
+
throw new HTTPException7(404, { message: "selfhosted enrollment is not enabled for this deployment" });
|
|
2342
3852
|
}
|
|
2343
3853
|
}
|
|
2344
3854
|
const startLimiter = new TokenBucket({ capacity: 10, refillPerSecond: 0.5 });
|
|
@@ -2348,7 +3858,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
2348
3858
|
function rateLimit(c, limiter) {
|
|
2349
3859
|
const ip = clientIp(c);
|
|
2350
3860
|
if (!limiter.take(ip)) {
|
|
2351
|
-
throw new
|
|
3861
|
+
throw new HTTPException7(429, { message: "too many requests; slow down" });
|
|
2352
3862
|
}
|
|
2353
3863
|
}
|
|
2354
3864
|
app.post("/v1/enrollments/device/start", async (c) => {
|
|
@@ -2356,12 +3866,12 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
2356
3866
|
rateLimit(c, startLimiter);
|
|
2357
3867
|
const parsed = DeviceEnrollmentStartRequest.safeParse(await c.req.json().catch(() => null));
|
|
2358
3868
|
if (!parsed.success) {
|
|
2359
|
-
throw new
|
|
3869
|
+
throw new HTTPException7(400, { message: "invalid device-start request" });
|
|
2360
3870
|
}
|
|
2361
3871
|
const body = parsed.data;
|
|
2362
3872
|
const workspace = await getWorkspace(db, body.workspaceId);
|
|
2363
3873
|
if (!workspace) {
|
|
2364
|
-
throw new
|
|
3874
|
+
throw new HTTPException7(404, { message: "workspace not found" });
|
|
2365
3875
|
}
|
|
2366
3876
|
const result = await startDeviceEnrollment({ db, settings }, {
|
|
2367
3877
|
accountId: workspace.accountId,
|
|
@@ -2382,7 +3892,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
2382
3892
|
rateLimit(c, pollLimiter);
|
|
2383
3893
|
const parsed = DeviceEnrollmentPollRequest.safeParse(await c.req.json().catch(() => null));
|
|
2384
3894
|
if (!parsed.success) {
|
|
2385
|
-
throw new
|
|
3895
|
+
throw new HTTPException7(400, { message: "invalid device-poll request" });
|
|
2386
3896
|
}
|
|
2387
3897
|
const result = await pollDeviceEnrollment({ db, settings }, { deviceCode: parsed.data.deviceCode });
|
|
2388
3898
|
return c.json(result, 200);
|
|
@@ -2392,25 +3902,25 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
2392
3902
|
rateLimit(c, lookupLimiter);
|
|
2393
3903
|
const parsed = DeviceEnrollmentLookupRequest.safeParse(await c.req.json().catch(() => null));
|
|
2394
3904
|
if (!parsed.success) {
|
|
2395
|
-
throw new
|
|
3905
|
+
throw new HTTPException7(400, { message: "invalid device-lookup request" });
|
|
2396
3906
|
}
|
|
2397
|
-
const
|
|
2398
|
-
if (!
|
|
2399
|
-
throw new
|
|
3907
|
+
const record3 = await lookupDeviceEnrollment({ db, settings }, { userCode: parsed.data.userCode });
|
|
3908
|
+
if (!record3) {
|
|
3909
|
+
throw new HTTPException7(404, { message: "no pending enrollment for that code" });
|
|
2400
3910
|
}
|
|
2401
3911
|
try {
|
|
2402
|
-
await
|
|
3912
|
+
await requireAccessGrant5(c, deps, record3.workspaceId, "enrollments:read");
|
|
2403
3913
|
} catch {
|
|
2404
|
-
throw new
|
|
3914
|
+
throw new HTTPException7(404, { message: "no pending enrollment for that code" });
|
|
2405
3915
|
}
|
|
2406
|
-
return c.json(DeviceEnrollmentLookupResponse.parse(toLookupResponse(
|
|
3916
|
+
return c.json(DeviceEnrollmentLookupResponse.parse(toLookupResponse(record3)), 200);
|
|
2407
3917
|
});
|
|
2408
3918
|
app.post("/v1/enrollments/token/exchange", async (c) => {
|
|
2409
3919
|
assertSelfhostedEnabled();
|
|
2410
3920
|
rateLimit(c, exchangeLimiter);
|
|
2411
3921
|
const parsed = EnrollTokenExchangeRequest.safeParse(await c.req.json().catch(() => null));
|
|
2412
3922
|
if (!parsed.success) {
|
|
2413
|
-
throw new
|
|
3923
|
+
throw new HTTPException7(400, { message: "invalid enroll-token-exchange request" });
|
|
2414
3924
|
}
|
|
2415
3925
|
const body = parsed.data;
|
|
2416
3926
|
const result = await exchangeEnrollToken({ db, settings }, {
|
|
@@ -2423,19 +3933,19 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
2423
3933
|
});
|
|
2424
3934
|
if (!result.ok) {
|
|
2425
3935
|
if (result.reason === "disabled") {
|
|
2426
|
-
throw new
|
|
3936
|
+
throw new HTTPException7(503, { message: "enrollment credential plane is not configured" });
|
|
2427
3937
|
}
|
|
2428
|
-
throw new
|
|
3938
|
+
throw new HTTPException7(401, { message: "invalid or expired enroll token" });
|
|
2429
3939
|
}
|
|
2430
3940
|
return c.json(EnrollTokenExchangeResponse.parse({ credentials: result.credentials }), 201);
|
|
2431
3941
|
});
|
|
2432
3942
|
app.post("/v1/workspaces/:workspaceId/enrollments/device/approve", async (c) => {
|
|
2433
3943
|
const workspaceId = c.req.param("workspaceId");
|
|
2434
|
-
const grant = await
|
|
3944
|
+
const grant = await requireAccessGrant5(c, deps, workspaceId, "enrollments:manage");
|
|
2435
3945
|
assertSelfhostedEnabled();
|
|
2436
3946
|
const parsed = DeviceEnrollmentApproveRequest.safeParse(await c.req.json().catch(() => null));
|
|
2437
3947
|
if (!parsed.success) {
|
|
2438
|
-
throw new
|
|
3948
|
+
throw new HTTPException7(400, { message: "invalid device-approve request" });
|
|
2439
3949
|
}
|
|
2440
3950
|
const body = parsed.data;
|
|
2441
3951
|
const approved = await approveDeviceEnrollment({ db, settings }, {
|
|
@@ -2448,7 +3958,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
2448
3958
|
approvedBySubjectLabel: grant.subjectLabel ?? null
|
|
2449
3959
|
});
|
|
2450
3960
|
if (!approved) {
|
|
2451
|
-
throw new
|
|
3961
|
+
throw new HTTPException7(404, { message: "no pending enrollment for that code" });
|
|
2452
3962
|
}
|
|
2453
3963
|
return c.json(DeviceEnrollmentApproveResponse.parse({
|
|
2454
3964
|
approved: true,
|
|
@@ -2459,11 +3969,11 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
2459
3969
|
});
|
|
2460
3970
|
app.post("/v1/workspaces/:workspaceId/enrollments/device/deny", async (c) => {
|
|
2461
3971
|
const workspaceId = c.req.param("workspaceId");
|
|
2462
|
-
const grant = await
|
|
3972
|
+
const grant = await requireAccessGrant5(c, deps, workspaceId, "enrollments:manage");
|
|
2463
3973
|
assertSelfhostedEnabled();
|
|
2464
3974
|
const parsed = DeviceEnrollmentDenyRequest.safeParse(await c.req.json().catch(() => null));
|
|
2465
3975
|
if (!parsed.success) {
|
|
2466
|
-
throw new
|
|
3976
|
+
throw new HTTPException7(400, { message: "invalid device-deny request" });
|
|
2467
3977
|
}
|
|
2468
3978
|
const result = await denyDeviceEnrollment({ db, settings }, {
|
|
2469
3979
|
accountId: grant.accountId,
|
|
@@ -2474,11 +3984,11 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
2474
3984
|
});
|
|
2475
3985
|
app.post("/v1/workspaces/:workspaceId/enrollments/token", async (c) => {
|
|
2476
3986
|
const workspaceId = c.req.param("workspaceId");
|
|
2477
|
-
const grant = await
|
|
3987
|
+
const grant = await requireAccessGrant5(c, deps, workspaceId, "enrollments:manage");
|
|
2478
3988
|
assertSelfhostedEnabled();
|
|
2479
3989
|
const parsed = MintEnrollTokenRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
2480
3990
|
if (!parsed.success) {
|
|
2481
|
-
throw new
|
|
3991
|
+
throw new HTTPException7(400, { message: "invalid mint-enroll-token request" });
|
|
2482
3992
|
}
|
|
2483
3993
|
const minted = await mintEnrollToken({ db, settings }, {
|
|
2484
3994
|
accountId: grant.accountId,
|
|
@@ -2486,13 +3996,13 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
2486
3996
|
allowScreenControl: parsed.data.allowScreenControl
|
|
2487
3997
|
});
|
|
2488
3998
|
if (!minted) {
|
|
2489
|
-
throw new
|
|
3999
|
+
throw new HTTPException7(503, { message: "enrollment credential plane is not configured" });
|
|
2490
4000
|
}
|
|
2491
4001
|
return c.json(MintEnrollTokenResponse.parse(minted), 201);
|
|
2492
4002
|
});
|
|
2493
4003
|
app.get("/v1/workspaces/:workspaceId/enrollments", async (c) => {
|
|
2494
4004
|
const workspaceId = c.req.param("workspaceId");
|
|
2495
|
-
await
|
|
4005
|
+
await requireAccessGrant5(c, deps, workspaceId, "enrollments:read");
|
|
2496
4006
|
assertSelfhostedEnabled();
|
|
2497
4007
|
const statusFilter = c.req.query("status");
|
|
2498
4008
|
const rows = await listEnrollments(db, workspaceId, statusFilter === "active" ? { status: "active" } : {});
|
|
@@ -2502,6 +4012,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
2502
4012
|
pubkey: row.pubkey,
|
|
2503
4013
|
exposure: row.exposure,
|
|
2504
4014
|
hasDisplay: row.hasDisplay,
|
|
4015
|
+
desktopUnavailableReason: row.desktopUnavailableReason,
|
|
2505
4016
|
allowScreenControl: row.allowScreenControl,
|
|
2506
4017
|
status: row.status,
|
|
2507
4018
|
os: row.os,
|
|
@@ -2514,7 +4025,7 @@ function registerEnrollmentRoutes(app, deps) {
|
|
|
2514
4025
|
});
|
|
2515
4026
|
app.post("/v1/workspaces/:workspaceId/enrollments/:enrollmentId/revoke", async (c) => {
|
|
2516
4027
|
const workspaceId = c.req.param("workspaceId");
|
|
2517
|
-
const grant = await
|
|
4028
|
+
const grant = await requireAccessGrant5(c, deps, workspaceId, "enrollments:manage");
|
|
2518
4029
|
assertSelfhostedEnabled();
|
|
2519
4030
|
const result = await revokeEnrollment(db, {
|
|
2520
4031
|
accountId: grant.accountId,
|
|
@@ -2569,8 +4080,8 @@ import {
|
|
|
2569
4080
|
getEnrollment as getEnrollment2,
|
|
2570
4081
|
readMachineMetricsSeries
|
|
2571
4082
|
} from "@opengeni/db";
|
|
2572
|
-
import { HTTPException as
|
|
2573
|
-
import { requireAccessGrant as
|
|
4083
|
+
import { HTTPException as HTTPException8 } from "hono/http-exception";
|
|
4084
|
+
import { requireAccessGrant as requireAccessGrant6 } from "@opengeni/core";
|
|
2574
4085
|
import { buildFleetContextForSession as buildFleetContextForSession2, swapActiveSandbox as swapActiveSandbox2 } from "@opengeni/core";
|
|
2575
4086
|
|
|
2576
4087
|
// src/sandbox/machines.ts
|
|
@@ -2685,6 +4196,7 @@ async function listMachines(services, input) {
|
|
|
2685
4196
|
os: "linux",
|
|
2686
4197
|
arch: "x86_64",
|
|
2687
4198
|
hasDisplay: false,
|
|
4199
|
+
desktopUnavailableReason: null,
|
|
2688
4200
|
allowScreenControl: false,
|
|
2689
4201
|
sharedSessionCount: 1,
|
|
2690
4202
|
lastSeenAt: null,
|
|
@@ -2721,6 +4233,7 @@ async function listMachines(services, input) {
|
|
|
2721
4233
|
os: enrollment.os,
|
|
2722
4234
|
arch: enrollment.arch,
|
|
2723
4235
|
hasDisplay: enrollment.hasDisplay,
|
|
4236
|
+
desktopUnavailableReason: enrollment.desktopUnavailableReason,
|
|
2724
4237
|
allowScreenControl: enrollment.allowScreenControl,
|
|
2725
4238
|
sharedSessionCount,
|
|
2726
4239
|
lastSeenAt: enrollment.lastSeenAt,
|
|
@@ -2742,12 +4255,12 @@ function registerMachineRoutes(app, deps) {
|
|
|
2742
4255
|
const { settings, db, bus } = deps;
|
|
2743
4256
|
function assertSelfhostedEnabled() {
|
|
2744
4257
|
if (!settings.sandboxSelfhostedEnabled) {
|
|
2745
|
-
throw new
|
|
4258
|
+
throw new HTTPException8(404, { message: "selfhosted machines are not enabled for this deployment" });
|
|
2746
4259
|
}
|
|
2747
4260
|
}
|
|
2748
4261
|
app.get("/v1/workspaces/:workspaceId/machines", async (c) => {
|
|
2749
4262
|
const workspaceId = c.req.param("workspaceId");
|
|
2750
|
-
await
|
|
4263
|
+
await requireAccessGrant6(c, deps, workspaceId, "enrollments:read");
|
|
2751
4264
|
assertSelfhostedEnabled();
|
|
2752
4265
|
const sessionId = c.req.query("sessionId") ?? null;
|
|
2753
4266
|
const response = await listMachines({ db, settings, bus }, { workspaceId, sessionId });
|
|
@@ -2755,12 +4268,12 @@ function registerMachineRoutes(app, deps) {
|
|
|
2755
4268
|
});
|
|
2756
4269
|
app.get("/v1/workspaces/:workspaceId/machines/:enrollmentId/metrics/series", async (c) => {
|
|
2757
4270
|
const workspaceId = c.req.param("workspaceId");
|
|
2758
|
-
await
|
|
4271
|
+
await requireAccessGrant6(c, deps, workspaceId, "enrollments:read");
|
|
2759
4272
|
assertSelfhostedEnabled();
|
|
2760
4273
|
const enrollmentId = c.req.param("enrollmentId");
|
|
2761
4274
|
const enrollment = await getEnrollment2(db, workspaceId, enrollmentId);
|
|
2762
4275
|
if (!enrollment) {
|
|
2763
|
-
throw new
|
|
4276
|
+
throw new HTTPException8(404, { message: "machine not found in this workspace" });
|
|
2764
4277
|
}
|
|
2765
4278
|
const windowMs = SERIES_WINDOWS_MS[c.req.query("window") ?? ""] ?? DEFAULT_SERIES_WINDOW_MS;
|
|
2766
4279
|
const since = new Date(Date.now() - windowMs);
|
|
@@ -2771,7 +4284,7 @@ function registerMachineRoutes(app, deps) {
|
|
|
2771
4284
|
});
|
|
2772
4285
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/active-sandbox", async (c) => {
|
|
2773
4286
|
const workspaceId = c.req.param("workspaceId");
|
|
2774
|
-
const grant = await
|
|
4287
|
+
const grant = await requireAccessGrant6(c, deps, workspaceId, "sessions:control");
|
|
2775
4288
|
assertSelfhostedEnabled();
|
|
2776
4289
|
const sessionId = c.req.param("sessionId");
|
|
2777
4290
|
const body = SwapActiveSandboxRequest.parse(await c.req.json());
|
|
@@ -2799,51 +4312,51 @@ import {
|
|
|
2799
4312
|
createWorkspaceEnvironment as createWorkspaceEnvironment2,
|
|
2800
4313
|
deleteWorkspaceEnvironment,
|
|
2801
4314
|
deleteWorkspaceEnvironmentVariable,
|
|
2802
|
-
encryptEnvironmentValue as
|
|
4315
|
+
encryptEnvironmentValue as encryptEnvironmentValue5,
|
|
2803
4316
|
getWorkspaceEnvironmentByName as getWorkspaceEnvironmentByName2,
|
|
2804
4317
|
listWorkspaceEnvironments as listWorkspaceEnvironments2,
|
|
2805
4318
|
setWorkspaceEnvironmentVariable as setWorkspaceEnvironmentVariable2,
|
|
2806
4319
|
updateWorkspaceEnvironment
|
|
2807
4320
|
} from "@opengeni/db";
|
|
2808
|
-
import { HTTPException as
|
|
2809
|
-
import { requireAccessGrant as
|
|
4321
|
+
import { HTTPException as HTTPException9 } from "hono/http-exception";
|
|
4322
|
+
import { requireAccessGrant as requireAccessGrant7 } from "@opengeni/core";
|
|
2810
4323
|
import {
|
|
2811
4324
|
assertAllowedEnvironmentVariableName as assertAllowedEnvironmentVariableName2,
|
|
2812
4325
|
MAX_ENVIRONMENTS_PER_WORKSPACE as MAX_ENVIRONMENTS_PER_WORKSPACE2,
|
|
2813
4326
|
MAX_VARIABLES_PER_ENVIRONMENT as MAX_VARIABLES_PER_ENVIRONMENT2,
|
|
2814
4327
|
recordEnvironmentAuditEvent as recordEnvironmentAuditEvent2,
|
|
2815
|
-
requireEnvironmentEncryption as
|
|
4328
|
+
requireEnvironmentEncryption as requireEnvironmentEncryption4,
|
|
2816
4329
|
requireEnvironmentForApi
|
|
2817
4330
|
} from "@opengeni/core";
|
|
2818
4331
|
function registerEnvironmentRoutes(app, deps) {
|
|
2819
4332
|
const { settings, db } = deps;
|
|
2820
4333
|
app.get("/v1/workspaces/:workspaceId/environments", async (c) => {
|
|
2821
4334
|
const workspaceId = c.req.param("workspaceId");
|
|
2822
|
-
await
|
|
4335
|
+
await requireAccessGrant7(c, deps, workspaceId, "environments:use");
|
|
2823
4336
|
return c.json(await listWorkspaceEnvironments2(db, workspaceId));
|
|
2824
4337
|
});
|
|
2825
4338
|
app.post("/v1/workspaces/:workspaceId/environments", async (c) => {
|
|
2826
4339
|
const workspaceId = c.req.param("workspaceId");
|
|
2827
|
-
const grant = await
|
|
2828
|
-
const key =
|
|
4340
|
+
const grant = await requireAccessGrant7(c, deps, workspaceId, "environments:manage");
|
|
4341
|
+
const key = requireEnvironmentEncryption4(settings);
|
|
2829
4342
|
const payload = CreateWorkspaceEnvironmentRequest.parse(await c.req.json());
|
|
2830
4343
|
const name = trimmedEnvironmentName(payload.name);
|
|
2831
4344
|
if (payload.variables.length > MAX_VARIABLES_PER_ENVIRONMENT2) {
|
|
2832
|
-
throw new
|
|
4345
|
+
throw new HTTPException9(422, { message: `an environment supports at most ${MAX_VARIABLES_PER_ENVIRONMENT2} variables` });
|
|
2833
4346
|
}
|
|
2834
4347
|
const variableNames = /* @__PURE__ */ new Set();
|
|
2835
4348
|
for (const variable of payload.variables) {
|
|
2836
4349
|
assertAllowedEnvironmentVariableName2(variable.name);
|
|
2837
4350
|
if (variableNames.has(variable.name)) {
|
|
2838
|
-
throw new
|
|
4351
|
+
throw new HTTPException9(422, { message: `duplicate environment variable name: ${variable.name}` });
|
|
2839
4352
|
}
|
|
2840
4353
|
variableNames.add(variable.name);
|
|
2841
4354
|
}
|
|
2842
4355
|
if (await countWorkspaceEnvironments2(db, workspaceId) >= MAX_ENVIRONMENTS_PER_WORKSPACE2) {
|
|
2843
|
-
throw new
|
|
4356
|
+
throw new HTTPException9(422, { message: `a workspace supports at most ${MAX_ENVIRONMENTS_PER_WORKSPACE2} environments` });
|
|
2844
4357
|
}
|
|
2845
4358
|
if (await getWorkspaceEnvironmentByName2(db, workspaceId, name)) {
|
|
2846
|
-
throw new
|
|
4359
|
+
throw new HTTPException9(409, { message: `environment name is already in use: ${name}` });
|
|
2847
4360
|
}
|
|
2848
4361
|
const created = await createWorkspaceEnvironment2(db, {
|
|
2849
4362
|
accountId: grant.accountId,
|
|
@@ -2852,7 +4365,7 @@ function registerEnvironmentRoutes(app, deps) {
|
|
|
2852
4365
|
description: payload.description ?? null,
|
|
2853
4366
|
variables: payload.variables.map((variable) => ({
|
|
2854
4367
|
name: variable.name,
|
|
2855
|
-
valueEncrypted:
|
|
4368
|
+
valueEncrypted: encryptEnvironmentValue5(key, variable.value)
|
|
2856
4369
|
}))
|
|
2857
4370
|
});
|
|
2858
4371
|
await recordEnvironmentAuditEvent2(db, { grant, action: "environment.created", environmentId: created.id });
|
|
@@ -2860,19 +4373,19 @@ function registerEnvironmentRoutes(app, deps) {
|
|
|
2860
4373
|
});
|
|
2861
4374
|
app.get("/v1/workspaces/:workspaceId/environments/:environmentId", async (c) => {
|
|
2862
4375
|
const workspaceId = c.req.param("workspaceId");
|
|
2863
|
-
await
|
|
4376
|
+
await requireAccessGrant7(c, deps, workspaceId, "environments:use");
|
|
2864
4377
|
return c.json(await requireEnvironmentForApi(db, workspaceId, c.req.param("environmentId")));
|
|
2865
4378
|
});
|
|
2866
4379
|
app.patch("/v1/workspaces/:workspaceId/environments/:environmentId", async (c) => {
|
|
2867
4380
|
const workspaceId = c.req.param("workspaceId");
|
|
2868
|
-
const grant = await
|
|
4381
|
+
const grant = await requireAccessGrant7(c, deps, workspaceId, "environments:manage");
|
|
2869
4382
|
const environment = await requireEnvironmentForApi(db, workspaceId, c.req.param("environmentId"));
|
|
2870
4383
|
const payload = UpdateWorkspaceEnvironmentRequest.parse(await c.req.json());
|
|
2871
4384
|
const name = payload.name !== void 0 ? trimmedEnvironmentName(payload.name) : void 0;
|
|
2872
4385
|
if (name !== void 0 && name !== environment.name) {
|
|
2873
4386
|
const existing = await getWorkspaceEnvironmentByName2(db, workspaceId, name);
|
|
2874
4387
|
if (existing && existing.id !== environment.id) {
|
|
2875
|
-
throw new
|
|
4388
|
+
throw new HTTPException9(409, { message: `environment name is already in use: ${name}` });
|
|
2876
4389
|
}
|
|
2877
4390
|
}
|
|
2878
4391
|
const updated = await updateWorkspaceEnvironment(db, workspaceId, environment.id, {
|
|
@@ -2884,15 +4397,15 @@ function registerEnvironmentRoutes(app, deps) {
|
|
|
2884
4397
|
});
|
|
2885
4398
|
app.delete("/v1/workspaces/:workspaceId/environments/:environmentId", async (c) => {
|
|
2886
4399
|
const workspaceId = c.req.param("workspaceId");
|
|
2887
|
-
const grant = await
|
|
4400
|
+
const grant = await requireAccessGrant7(c, deps, workspaceId, "environments:manage");
|
|
2888
4401
|
const environment = await requireEnvironmentForApi(db, workspaceId, c.req.param("environmentId"));
|
|
2889
4402
|
const attachedTasks = await countScheduledTasksUsingEnvironment(db, workspaceId, environment.id);
|
|
2890
4403
|
if (attachedTasks > 0) {
|
|
2891
|
-
throw new
|
|
4404
|
+
throw new HTTPException9(409, { message: `environment is attached to ${attachedTasks} scheduled task(s); detach first` });
|
|
2892
4405
|
}
|
|
2893
4406
|
const activeSessions = await countActiveSessionsUsingEnvironment(db, workspaceId, environment.id);
|
|
2894
4407
|
if (activeSessions > 0) {
|
|
2895
|
-
throw new
|
|
4408
|
+
throw new HTTPException9(409, { message: `environment is attached to ${activeSessions} active session(s); wait for them to finish or cancel them first` });
|
|
2896
4409
|
}
|
|
2897
4410
|
await deleteWorkspaceEnvironment(db, workspaceId, environment.id);
|
|
2898
4411
|
await recordEnvironmentAuditEvent2(db, { grant, action: "environment.deleted", environmentId: environment.id });
|
|
@@ -2900,33 +4413,33 @@ function registerEnvironmentRoutes(app, deps) {
|
|
|
2900
4413
|
});
|
|
2901
4414
|
app.put("/v1/workspaces/:workspaceId/environments/:environmentId/variables/:name", async (c) => {
|
|
2902
4415
|
const workspaceId = c.req.param("workspaceId");
|
|
2903
|
-
const grant = await
|
|
2904
|
-
const key =
|
|
4416
|
+
const grant = await requireAccessGrant7(c, deps, workspaceId, "environments:manage");
|
|
4417
|
+
const key = requireEnvironmentEncryption4(settings);
|
|
2905
4418
|
const name = parseVariableName(c.req.param("name"));
|
|
2906
4419
|
const environment = await requireEnvironmentForApi(db, workspaceId, c.req.param("environmentId"));
|
|
2907
4420
|
const payload = SetWorkspaceEnvironmentVariableRequest.parse(await c.req.json());
|
|
2908
4421
|
const exists = environment.variables.some((variable) => variable.name === name);
|
|
2909
4422
|
if (!exists && environment.variables.length >= MAX_VARIABLES_PER_ENVIRONMENT2) {
|
|
2910
|
-
throw new
|
|
4423
|
+
throw new HTTPException9(422, { message: `an environment supports at most ${MAX_VARIABLES_PER_ENVIRONMENT2} variables` });
|
|
2911
4424
|
}
|
|
2912
4425
|
const metadata = await setWorkspaceEnvironmentVariable2(db, {
|
|
2913
4426
|
accountId: grant.accountId,
|
|
2914
4427
|
workspaceId,
|
|
2915
4428
|
environmentId: environment.id,
|
|
2916
4429
|
name,
|
|
2917
|
-
valueEncrypted:
|
|
4430
|
+
valueEncrypted: encryptEnvironmentValue5(key, payload.value)
|
|
2918
4431
|
});
|
|
2919
4432
|
await recordEnvironmentAuditEvent2(db, { grant, action: "environment.variable.set", environmentId: environment.id, variableName: name });
|
|
2920
4433
|
return c.json(metadata);
|
|
2921
4434
|
});
|
|
2922
4435
|
app.delete("/v1/workspaces/:workspaceId/environments/:environmentId/variables/:name", async (c) => {
|
|
2923
4436
|
const workspaceId = c.req.param("workspaceId");
|
|
2924
|
-
const grant = await
|
|
4437
|
+
const grant = await requireAccessGrant7(c, deps, workspaceId, "environments:manage");
|
|
2925
4438
|
const name = parseVariableName(c.req.param("name"));
|
|
2926
4439
|
const environment = await requireEnvironmentForApi(db, workspaceId, c.req.param("environmentId"));
|
|
2927
4440
|
const deleted = await deleteWorkspaceEnvironmentVariable(db, workspaceId, environment.id, name);
|
|
2928
4441
|
if (!deleted) {
|
|
2929
|
-
throw new
|
|
4442
|
+
throw new HTTPException9(404, { message: "environment variable not found" });
|
|
2930
4443
|
}
|
|
2931
4444
|
await recordEnvironmentAuditEvent2(db, { grant, action: "environment.variable.deleted", environmentId: environment.id, variableName: name });
|
|
2932
4445
|
return c.json({ ok: true });
|
|
@@ -2935,7 +4448,7 @@ function registerEnvironmentRoutes(app, deps) {
|
|
|
2935
4448
|
function parseVariableName(raw) {
|
|
2936
4449
|
const parsed = WorkspaceEnvironmentVariableName2.safeParse(raw);
|
|
2937
4450
|
if (!parsed.success) {
|
|
2938
|
-
throw new
|
|
4451
|
+
throw new HTTPException9(422, { message: "environment variable names must match ^[A-Z][A-Z0-9_]*$" });
|
|
2939
4452
|
}
|
|
2940
4453
|
assertAllowedEnvironmentVariableName2(parsed.data);
|
|
2941
4454
|
return parsed.data;
|
|
@@ -2943,7 +4456,7 @@ function parseVariableName(raw) {
|
|
|
2943
4456
|
function trimmedEnvironmentName(name) {
|
|
2944
4457
|
const trimmed = name.trim();
|
|
2945
4458
|
if (!trimmed) {
|
|
2946
|
-
throw new
|
|
4459
|
+
throw new HTTPException9(422, { message: "environment name is required" });
|
|
2947
4460
|
}
|
|
2948
4461
|
return trimmed;
|
|
2949
4462
|
}
|
|
@@ -2963,21 +4476,21 @@ import {
|
|
|
2963
4476
|
markFileUploadFailed,
|
|
2964
4477
|
requireFile as requireFile2
|
|
2965
4478
|
} from "@opengeni/db";
|
|
2966
|
-
import { HTTPException as
|
|
2967
|
-
import { requireAccessGrant as
|
|
4479
|
+
import { HTTPException as HTTPException10 } from "hono/http-exception";
|
|
4480
|
+
import { requireAccessGrant as requireAccessGrant8 } from "@opengeni/core";
|
|
2968
4481
|
import { recordWorkspaceUsage as recordWorkspaceUsage3, requireLimit as requireLimit3 } from "@opengeni/core";
|
|
2969
4482
|
function registerFileRoutes(app, deps) {
|
|
2970
4483
|
const { db, objectStorage } = deps;
|
|
2971
4484
|
app.post("/v1/workspaces/:workspaceId/files/uploads", async (c) => {
|
|
2972
4485
|
const workspaceId = c.req.param("workspaceId");
|
|
2973
|
-
const grant = await
|
|
4486
|
+
const grant = await requireAccessGrant8(c, deps, workspaceId, "files:upload");
|
|
2974
4487
|
if (!objectStorage) {
|
|
2975
|
-
throw new
|
|
4488
|
+
throw new HTTPException10(503, { message: "object storage is not configured" });
|
|
2976
4489
|
}
|
|
2977
4490
|
const payload = CreateFileUploadRequest.parse(await c.req.json());
|
|
2978
4491
|
await requireLimit3(deps, { accountId: grant.accountId, workspaceId, action: "file:upload", quantity: payload.sizeBytes });
|
|
2979
4492
|
if (payload.sizeBytes > objectStorage.maxSinglePutSizeBytes) {
|
|
2980
|
-
throw new
|
|
4493
|
+
throw new HTTPException10(413, { message: `file exceeds single PUT limit of ${objectStorage.maxSinglePutSizeBytes} bytes` });
|
|
2981
4494
|
}
|
|
2982
4495
|
const fileId = crypto.randomUUID();
|
|
2983
4496
|
const safeFilename = sanitizeFilename(payload.filename);
|
|
@@ -3011,35 +4524,35 @@ function registerFileRoutes(app, deps) {
|
|
|
3011
4524
|
});
|
|
3012
4525
|
app.post("/v1/workspaces/:workspaceId/files/uploads/:uploadId/complete", async (c) => {
|
|
3013
4526
|
const workspaceId = c.req.param("workspaceId");
|
|
3014
|
-
const grant = await
|
|
4527
|
+
const grant = await requireAccessGrant8(c, deps, workspaceId, "files:upload");
|
|
3015
4528
|
if (!objectStorage) {
|
|
3016
|
-
throw new
|
|
4529
|
+
throw new HTTPException10(503, { message: "object storage is not configured" });
|
|
3017
4530
|
}
|
|
3018
4531
|
const upload = await getFileUpload(db, workspaceId, c.req.param("uploadId"));
|
|
3019
4532
|
if (!upload) {
|
|
3020
|
-
throw new
|
|
4533
|
+
throw new HTTPException10(404, { message: "file upload not found" });
|
|
3021
4534
|
}
|
|
3022
4535
|
if (upload.status !== "pending") {
|
|
3023
|
-
throw new
|
|
4536
|
+
throw new HTTPException10(409, { message: `file upload is ${upload.status}` });
|
|
3024
4537
|
}
|
|
3025
4538
|
if (upload.expiresAt.getTime() < Date.now()) {
|
|
3026
4539
|
await markFileUploadFailed(db, workspaceId, upload.id, upload.file.id);
|
|
3027
|
-
throw new
|
|
4540
|
+
throw new HTTPException10(409, { message: "file upload has expired" });
|
|
3028
4541
|
}
|
|
3029
4542
|
const head = await objectStorage.headFile(upload.file).catch((error) => {
|
|
3030
|
-
throw new
|
|
4543
|
+
throw new HTTPException10(409, { message: `uploaded object is not available: ${error instanceof Error ? error.message : String(error)}` });
|
|
3031
4544
|
});
|
|
3032
4545
|
if (Number(head.ContentLength ?? -1) !== upload.file.sizeBytes) {
|
|
3033
4546
|
await markFileUploadFailed(db, workspaceId, upload.id, upload.file.id);
|
|
3034
|
-
throw new
|
|
4547
|
+
throw new HTTPException10(422, { message: "uploaded object size does not match file metadata" });
|
|
3035
4548
|
}
|
|
3036
4549
|
if (upload.file.contentType && head.ContentType && head.ContentType !== upload.file.contentType) {
|
|
3037
4550
|
await markFileUploadFailed(db, workspaceId, upload.id, upload.file.id);
|
|
3038
|
-
throw new
|
|
4551
|
+
throw new HTTPException10(422, { message: "uploaded object content type does not match file metadata" });
|
|
3039
4552
|
}
|
|
3040
4553
|
if (upload.file.sha256 && head.Metadata?.sha256 !== upload.file.sha256) {
|
|
3041
4554
|
await markFileUploadFailed(db, workspaceId, upload.id, upload.file.id);
|
|
3042
|
-
throw new
|
|
4555
|
+
throw new HTTPException10(422, { message: "uploaded object checksum metadata does not match file metadata" });
|
|
3043
4556
|
}
|
|
3044
4557
|
const file = await completeFileUpload(db, workspaceId, upload.id);
|
|
3045
4558
|
await recordWorkspaceUsage3(deps, {
|
|
@@ -3057,25 +4570,25 @@ function registerFileRoutes(app, deps) {
|
|
|
3057
4570
|
});
|
|
3058
4571
|
app.get("/v1/workspaces/:workspaceId/files/:fileId", async (c) => {
|
|
3059
4572
|
const workspaceId = c.req.param("workspaceId");
|
|
3060
|
-
await
|
|
4573
|
+
await requireAccessGrant8(c, deps, workspaceId, "files:read");
|
|
3061
4574
|
const file = await requireFile2(db, workspaceId, c.req.param("fileId")).catch(() => null);
|
|
3062
4575
|
if (!file) {
|
|
3063
|
-
throw new
|
|
4576
|
+
throw new HTTPException10(404, { message: "file not found" });
|
|
3064
4577
|
}
|
|
3065
4578
|
return c.json(FileAsset.parse(file));
|
|
3066
4579
|
});
|
|
3067
4580
|
app.post("/v1/workspaces/:workspaceId/files/:fileId/download-url", async (c) => {
|
|
3068
4581
|
const workspaceId = c.req.param("workspaceId");
|
|
3069
|
-
await
|
|
4582
|
+
await requireAccessGrant8(c, deps, workspaceId, "files:read");
|
|
3070
4583
|
if (!objectStorage) {
|
|
3071
|
-
throw new
|
|
4584
|
+
throw new HTTPException10(503, { message: "object storage is not configured" });
|
|
3072
4585
|
}
|
|
3073
4586
|
const file = await requireFile2(db, workspaceId, c.req.param("fileId")).catch(() => null);
|
|
3074
4587
|
if (!file) {
|
|
3075
|
-
throw new
|
|
4588
|
+
throw new HTTPException10(404, { message: "file not found" });
|
|
3076
4589
|
}
|
|
3077
4590
|
if (file.status !== "ready") {
|
|
3078
|
-
throw new
|
|
4591
|
+
throw new HTTPException10(409, { message: `file is ${file.status}` });
|
|
3079
4592
|
}
|
|
3080
4593
|
const signed = await objectStorage.createGetUrl({ key: file.objectKey });
|
|
3081
4594
|
return c.json(FileDownloadUrlResponse.parse({
|
|
@@ -3094,18 +4607,18 @@ function sanitizeFilename(filename) {
|
|
|
3094
4607
|
import { CreateApiKeyRequest, CreateApiKeyResponse } from "@opengeni/contracts";
|
|
3095
4608
|
import { createApiKey, listApiKeys, revokeApiKey } from "@opengeni/db";
|
|
3096
4609
|
import { zValidator } from "@hono/zod-validator";
|
|
3097
|
-
import { HTTPException as
|
|
3098
|
-
import { requireAccessGrant as
|
|
4610
|
+
import { HTTPException as HTTPException11 } from "hono/http-exception";
|
|
4611
|
+
import { requireAccessGrant as requireAccessGrant9 } from "@opengeni/core";
|
|
3099
4612
|
import { requireLimit as requireLimit4 } from "@opengeni/core";
|
|
3100
4613
|
function registerApiKeyRoutes(app, deps) {
|
|
3101
4614
|
app.get("/v1/workspaces/:workspaceId/api-keys", async (c) => {
|
|
3102
4615
|
const workspaceId = c.req.param("workspaceId");
|
|
3103
|
-
await
|
|
4616
|
+
await requireAccessGrant9(c, deps, workspaceId, "api_keys:manage");
|
|
3104
4617
|
return c.json({ apiKeys: await listApiKeys(deps.db, workspaceId) });
|
|
3105
4618
|
});
|
|
3106
4619
|
app.post("/v1/workspaces/:workspaceId/api-keys", zValidator("json", CreateApiKeyRequest.omit({ workspaceId: true })), async (c) => {
|
|
3107
4620
|
const workspaceId = c.req.param("workspaceId");
|
|
3108
|
-
const grant = await
|
|
4621
|
+
const grant = await requireAccessGrant9(c, deps, workspaceId, "api_keys:manage");
|
|
3109
4622
|
const body = c.req.valid("json");
|
|
3110
4623
|
const permissions = body.permissions.length > 0 ? body.permissions : ["workspace:read"];
|
|
3111
4624
|
ensureDelegablePermissions(grant.permissions, permissions);
|
|
@@ -3125,7 +4638,7 @@ function registerApiKeyRoutes(app, deps) {
|
|
|
3125
4638
|
});
|
|
3126
4639
|
app.delete("/v1/workspaces/:workspaceId/api-keys/:apiKeyId", async (c) => {
|
|
3127
4640
|
const workspaceId = c.req.param("workspaceId");
|
|
3128
|
-
await
|
|
4641
|
+
await requireAccessGrant9(c, deps, workspaceId, "api_keys:manage");
|
|
3129
4642
|
return c.json(await revokeApiKey(deps.db, workspaceId, c.req.param("apiKeyId")));
|
|
3130
4643
|
});
|
|
3131
4644
|
}
|
|
@@ -3135,7 +4648,7 @@ function ensureDelegablePermissions(grantPermissions, requested) {
|
|
|
3135
4648
|
}
|
|
3136
4649
|
const missing = requested.filter((permission) => !grantPermissions.includes(permission));
|
|
3137
4650
|
if (missing.length > 0) {
|
|
3138
|
-
throw new
|
|
4651
|
+
throw new HTTPException11(403, { message: `cannot delegate missing permissions: ${missing.join(", ")}` });
|
|
3139
4652
|
}
|
|
3140
4653
|
}
|
|
3141
4654
|
function generateApiKeyToken() {
|
|
@@ -3167,7 +4680,7 @@ import {
|
|
|
3167
4680
|
recordStripeWebhookEvent,
|
|
3168
4681
|
upsertBillingCustomer
|
|
3169
4682
|
} from "@opengeni/db";
|
|
3170
|
-
import { HTTPException as
|
|
4683
|
+
import { HTTPException as HTTPException12 } from "hono/http-exception";
|
|
3171
4684
|
import Stripe from "stripe";
|
|
3172
4685
|
import { requireAccessContext } from "@opengeni/core";
|
|
3173
4686
|
function registerBillingRoutes(app, deps) {
|
|
@@ -3181,7 +4694,7 @@ function registerBillingRoutes(app, deps) {
|
|
|
3181
4694
|
const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
|
|
3182
4695
|
const workspaceId = c.req.query("workspaceId");
|
|
3183
4696
|
if (workspaceId && !context.workspaceGrants.some((grant) => grant.accountId === accountId && grant.workspaceId === workspaceId)) {
|
|
3184
|
-
throw new
|
|
4697
|
+
throw new HTTPException12(403, { message: "missing workspace access for usage query" });
|
|
3185
4698
|
}
|
|
3186
4699
|
return c.json({
|
|
3187
4700
|
balance: await getBillingBalance(deps.db, accountId),
|
|
@@ -3199,12 +4712,12 @@ function registerBillingRoutes(app, deps) {
|
|
|
3199
4712
|
});
|
|
3200
4713
|
app.post("/v1/billing/checkout", async (c) => {
|
|
3201
4714
|
if (deps.settings.billingMode !== "stripe") {
|
|
3202
|
-
throw new
|
|
4715
|
+
throw new HTTPException12(404, { message: "stripe billing is not enabled" });
|
|
3203
4716
|
}
|
|
3204
4717
|
const context = await requireAccessContext(c, deps);
|
|
3205
4718
|
const parsed = CreateCheckoutRequest.safeParse(await c.req.json());
|
|
3206
4719
|
if (!parsed.success) {
|
|
3207
|
-
throw new
|
|
4720
|
+
throw new HTTPException12(400, { message: parsed.error.issues[0]?.message ?? "invalid checkout request" });
|
|
3208
4721
|
}
|
|
3209
4722
|
const body = parsed.data;
|
|
3210
4723
|
const accountId = requireSelectedAccount(context, body.accountId, "billing:manage");
|
|
@@ -3225,7 +4738,7 @@ function registerBillingRoutes(app, deps) {
|
|
|
3225
4738
|
idempotencyKey
|
|
3226
4739
|
}), { idempotencyKey });
|
|
3227
4740
|
if (!session.url) {
|
|
3228
|
-
throw new
|
|
4741
|
+
throw new HTTPException12(502, { message: "Stripe did not return a checkout URL" });
|
|
3229
4742
|
}
|
|
3230
4743
|
return c.json(CreateCheckoutResponse.parse({
|
|
3231
4744
|
checkoutSessionId: session.id,
|
|
@@ -3234,18 +4747,18 @@ function registerBillingRoutes(app, deps) {
|
|
|
3234
4747
|
});
|
|
3235
4748
|
app.post("/v1/webhooks/stripe", async (c) => {
|
|
3236
4749
|
if (deps.settings.billingMode !== "stripe") {
|
|
3237
|
-
throw new
|
|
4750
|
+
throw new HTTPException12(404, { message: "stripe billing is not enabled" });
|
|
3238
4751
|
}
|
|
3239
4752
|
const signature = c.req.header("stripe-signature");
|
|
3240
4753
|
if (!signature) {
|
|
3241
|
-
throw new
|
|
4754
|
+
throw new HTTPException12(400, { message: "missing stripe-signature" });
|
|
3242
4755
|
}
|
|
3243
4756
|
const payload = await c.req.text();
|
|
3244
4757
|
let event;
|
|
3245
4758
|
try {
|
|
3246
4759
|
event = await stripeClient(deps).webhooks.constructEventAsync(payload, signature, deps.settings.stripeWebhookSecret);
|
|
3247
4760
|
} catch (error) {
|
|
3248
|
-
throw new
|
|
4761
|
+
throw new HTTPException12(400, { message: error instanceof Error ? error.message : "invalid stripe signature" });
|
|
3249
4762
|
}
|
|
3250
4763
|
const firstSeen = await recordStripeWebhookEvent(deps.db, {
|
|
3251
4764
|
id: event.id,
|
|
@@ -3263,7 +4776,7 @@ function registerBillingRoutes(app, deps) {
|
|
|
3263
4776
|
await markStripeWebhookProcessed(deps.db, event.id);
|
|
3264
4777
|
return c.json({ received: true });
|
|
3265
4778
|
} catch (error) {
|
|
3266
|
-
throw new
|
|
4779
|
+
throw new HTTPException12(500, { message: error instanceof Error ? error.message : String(error) });
|
|
3267
4780
|
}
|
|
3268
4781
|
});
|
|
3269
4782
|
}
|
|
@@ -3315,7 +4828,7 @@ function stripeCheckoutSessionCreateParams(input) {
|
|
|
3315
4828
|
}
|
|
3316
4829
|
function checkoutReturnUrl(publicBaseUrl, candidate, fallbackPath, field) {
|
|
3317
4830
|
if (!publicBaseUrl) {
|
|
3318
|
-
throw new
|
|
4831
|
+
throw new HTTPException12(500, { message: "OPENGENI_PUBLIC_BASE_URL is required for Stripe checkout" });
|
|
3319
4832
|
}
|
|
3320
4833
|
const base = new URL(publicBaseUrl);
|
|
3321
4834
|
const fallback = new URL(fallbackPath, base).toString();
|
|
@@ -3324,7 +4837,7 @@ function checkoutReturnUrl(publicBaseUrl, candidate, fallbackPath, field) {
|
|
|
3324
4837
|
}
|
|
3325
4838
|
const parsed = new URL(candidate);
|
|
3326
4839
|
if (parsed.origin !== base.origin) {
|
|
3327
|
-
throw new
|
|
4840
|
+
throw new HTTPException12(400, { message: `${field} must use the OpenGeni public origin` });
|
|
3328
4841
|
}
|
|
3329
4842
|
return parsed.toString();
|
|
3330
4843
|
}
|
|
@@ -3556,7 +5069,7 @@ async function getOrCreateStripeCustomer(deps, stripe, context, accountId) {
|
|
|
3556
5069
|
}
|
|
3557
5070
|
const account = await getManagedAccount(deps.db, accountId);
|
|
3558
5071
|
if (!account) {
|
|
3559
|
-
throw new
|
|
5072
|
+
throw new HTTPException12(404, { message: "account not found" });
|
|
3560
5073
|
}
|
|
3561
5074
|
const customer = await stripe.customers.create({
|
|
3562
5075
|
name: account.name,
|
|
@@ -3582,17 +5095,17 @@ function stripeCustomerProvider(input) {
|
|
|
3582
5095
|
function requireSelectedAccount(context, requested, permission) {
|
|
3583
5096
|
const accountId = requested ?? context.defaultAccountId ?? void 0;
|
|
3584
5097
|
if (!accountId) {
|
|
3585
|
-
throw new
|
|
5098
|
+
throw new HTTPException12(409, { message: "account selection is required" });
|
|
3586
5099
|
}
|
|
3587
5100
|
const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
|
|
3588
5101
|
if (!grant || !grant.permissions.includes(permission) && !grant.permissions.includes("account:admin")) {
|
|
3589
|
-
throw new
|
|
5102
|
+
throw new HTTPException12(403, { message: `missing permission: ${permission}` });
|
|
3590
5103
|
}
|
|
3591
5104
|
return accountId;
|
|
3592
5105
|
}
|
|
3593
5106
|
function stripeClient(deps) {
|
|
3594
5107
|
if (!deps.settings.stripeSecretKey) {
|
|
3595
|
-
throw new
|
|
5108
|
+
throw new HTTPException12(500, { message: "Stripe secret key is not configured" });
|
|
3596
5109
|
}
|
|
3597
5110
|
return new Stripe(deps.settings.stripeSecretKey);
|
|
3598
5111
|
}
|
|
@@ -3621,7 +5134,7 @@ import {
|
|
|
3621
5134
|
import {
|
|
3622
5135
|
buildGitHubAppManifest,
|
|
3623
5136
|
convertGitHubAppManifest,
|
|
3624
|
-
createSignedState as
|
|
5137
|
+
createSignedState as createSignedState4,
|
|
3625
5138
|
envLinesFromGitHubManifestConversion,
|
|
3626
5139
|
GitHubAppApiError,
|
|
3627
5140
|
GitHubAppConfigurationError as GitHubAppConfigurationError2,
|
|
@@ -3630,23 +5143,23 @@ import {
|
|
|
3630
5143
|
listGitHubAppRepositories as listGitHubAppRepositories2,
|
|
3631
5144
|
organizationAppManifestUrl,
|
|
3632
5145
|
personalAppManifestUrl,
|
|
3633
|
-
readSignedState as
|
|
5146
|
+
readSignedState as readSignedState3,
|
|
3634
5147
|
stateMaxAgeSeconds as stateMaxAgeSeconds2,
|
|
3635
5148
|
verifyGitHubInstallationAccessForUser,
|
|
3636
5149
|
verifySignedState
|
|
3637
5150
|
} from "@opengeni/github";
|
|
3638
5151
|
import { deleteCookie, getCookie, setCookie } from "hono/cookie";
|
|
3639
|
-
import { HTTPException as
|
|
3640
|
-
import { requireAccessGrant as
|
|
5152
|
+
import { HTTPException as HTTPException13 } from "hono/http-exception";
|
|
5153
|
+
import { requireAccessGrant as requireAccessGrant10 } from "@opengeni/core";
|
|
3641
5154
|
var githubStateCookie = "opengeni_github_state";
|
|
3642
5155
|
function registerGitHubRoutes(app, deps) {
|
|
3643
5156
|
const { db, settings, githubStateSecret } = deps;
|
|
3644
5157
|
app.get("/v1/workspaces/:workspaceId/github/app", async (c) => {
|
|
3645
5158
|
const workspaceId = c.req.param("workspaceId");
|
|
3646
|
-
const grant = await
|
|
5159
|
+
const grant = await requireAccessGrant10(c, deps, workspaceId, "github:use");
|
|
3647
5160
|
const missing = githubAppMissingSettings2(settings);
|
|
3648
5161
|
const slug = settings.githubAppSlug?.trim() || null;
|
|
3649
|
-
const state =
|
|
5162
|
+
const state = createSignedState4(githubStateSecret, {
|
|
3650
5163
|
accountId: grant.accountId,
|
|
3651
5164
|
workspaceId: grant.workspaceId
|
|
3652
5165
|
});
|
|
@@ -3664,49 +5177,49 @@ function registerGitHubRoutes(app, deps) {
|
|
|
3664
5177
|
const workspaceId = c.req.param("workspaceId");
|
|
3665
5178
|
const state = c.req.query("state");
|
|
3666
5179
|
if (!state) {
|
|
3667
|
-
throw new
|
|
5180
|
+
throw new HTTPException13(400, { message: "missing GitHub installation state" });
|
|
3668
5181
|
}
|
|
3669
|
-
const statePayload =
|
|
5182
|
+
const statePayload = readSignedState3(state, githubStateSecret);
|
|
3670
5183
|
if (!statePayload || statePayload.workspaceId !== workspaceId) {
|
|
3671
|
-
throw new
|
|
5184
|
+
throw new HTTPException13(400, { message: "invalid or expired GitHub installation state" });
|
|
3672
5185
|
}
|
|
3673
5186
|
const slug = settings.githubAppSlug?.trim();
|
|
3674
5187
|
if (!slug) {
|
|
3675
|
-
throw new
|
|
5188
|
+
throw new HTTPException13(409, { message: JSON.stringify({ message: "GitHub App is not configured", missing: githubAppMissingSettings2(settings) }) });
|
|
3676
5189
|
}
|
|
3677
5190
|
setGitHubStateCookie(c, deps, state);
|
|
3678
5191
|
return c.redirect(`https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}`);
|
|
3679
5192
|
});
|
|
3680
5193
|
app.get("/v1/workspaces/:workspaceId/github/repositories", async (c) => {
|
|
3681
5194
|
const workspaceId = c.req.param("workspaceId");
|
|
3682
|
-
await
|
|
5195
|
+
await requireAccessGrant10(c, deps, workspaceId, "github:use");
|
|
3683
5196
|
try {
|
|
3684
5197
|
return c.json({ repositories: await listWorkspaceGitHubRepositories(deps, workspaceId) });
|
|
3685
5198
|
} catch (error) {
|
|
3686
5199
|
if (error instanceof GitHubAppConfigurationError2) {
|
|
3687
|
-
throw new
|
|
5200
|
+
throw new HTTPException13(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
|
|
3688
5201
|
}
|
|
3689
|
-
throw new
|
|
5202
|
+
throw new HTTPException13(502, { message: error instanceof Error ? error.message : String(error) });
|
|
3690
5203
|
}
|
|
3691
5204
|
});
|
|
3692
5205
|
app.post("/v1/workspaces/:workspaceId/github/repositories/sync", async (c) => {
|
|
3693
5206
|
const workspaceId = c.req.param("workspaceId");
|
|
3694
|
-
await
|
|
5207
|
+
await requireAccessGrant10(c, deps, workspaceId, "github:use");
|
|
3695
5208
|
try {
|
|
3696
5209
|
return c.json({ repositories: await listWorkspaceGitHubRepositories(deps, workspaceId) });
|
|
3697
5210
|
} catch (error) {
|
|
3698
5211
|
if (error instanceof GitHubAppConfigurationError2) {
|
|
3699
|
-
throw new
|
|
5212
|
+
throw new HTTPException13(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
|
|
3700
5213
|
}
|
|
3701
|
-
throw new
|
|
5214
|
+
throw new HTTPException13(502, { message: error instanceof Error ? error.message : String(error) });
|
|
3702
5215
|
}
|
|
3703
5216
|
});
|
|
3704
5217
|
app.post("/v1/workspaces/:workspaceId/github/app-manifest", async (c) => {
|
|
3705
5218
|
const workspaceId = c.req.param("workspaceId");
|
|
3706
|
-
const grant = await
|
|
5219
|
+
const grant = await requireAccessGrant10(c, deps, workspaceId, "github:manage");
|
|
3707
5220
|
const payload = GitHubAppManifestCreate.parse(await c.req.json());
|
|
3708
5221
|
const baseUrl = (settings.githubAppManifestBaseUrl ?? new URL(c.req.url).origin).replace(/\/+$/, "");
|
|
3709
|
-
const state =
|
|
5222
|
+
const state = createSignedState4(githubStateSecret, {
|
|
3710
5223
|
accountId: grant.accountId,
|
|
3711
5224
|
workspaceId: grant.workspaceId
|
|
3712
5225
|
});
|
|
@@ -3730,10 +5243,10 @@ function registerGitHubRoutes(app, deps) {
|
|
|
3730
5243
|
const code = c.req.query("code");
|
|
3731
5244
|
const state = c.req.query("state");
|
|
3732
5245
|
if (!code) {
|
|
3733
|
-
throw new
|
|
5246
|
+
throw new HTTPException13(400, { message: "missing GitHub manifest code" });
|
|
3734
5247
|
}
|
|
3735
5248
|
if (!state || !verifySignedState(state, githubStateSecret)) {
|
|
3736
|
-
throw new
|
|
5249
|
+
throw new HTTPException13(400, { message: "invalid or expired GitHub manifest state" });
|
|
3737
5250
|
}
|
|
3738
5251
|
try {
|
|
3739
5252
|
const conversion = await convertGitHubAppManifest(code);
|
|
@@ -3744,7 +5257,7 @@ function registerGitHubRoutes(app, deps) {
|
|
|
3744
5257
|
return c.html(githubSuccessHtml(envLines, installUrl));
|
|
3745
5258
|
} catch (error) {
|
|
3746
5259
|
const message = error instanceof GitHubAppApiError ? error.message : String(error);
|
|
3747
|
-
throw new
|
|
5260
|
+
throw new HTTPException13(502, { message });
|
|
3748
5261
|
}
|
|
3749
5262
|
});
|
|
3750
5263
|
const handleGitHubInstallCallback = async (c) => {
|
|
@@ -3753,30 +5266,30 @@ function registerGitHubRoutes(app, deps) {
|
|
|
3753
5266
|
const installationIdRaw = c.req.query("installation_id");
|
|
3754
5267
|
const setupAction = c.req.query("setup_action") ?? null;
|
|
3755
5268
|
if (!state) {
|
|
3756
|
-
throw new
|
|
5269
|
+
throw new HTTPException13(400, { message: "missing GitHub installation state" });
|
|
3757
5270
|
}
|
|
3758
|
-
const statePayload =
|
|
5271
|
+
const statePayload = readSignedState3(state, githubStateSecret);
|
|
3759
5272
|
if (!statePayload || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string") {
|
|
3760
|
-
throw new
|
|
5273
|
+
throw new HTTPException13(400, { message: "invalid or expired GitHub installation state" });
|
|
3761
5274
|
}
|
|
3762
5275
|
requireGitHubStateCookie(c, state);
|
|
3763
|
-
const grant = await
|
|
5276
|
+
const grant = await requireAccessGrant10(c, deps, statePayload.workspaceId, "github:manage");
|
|
3764
5277
|
if (grant.accountId !== statePayload.accountId) {
|
|
3765
|
-
throw new
|
|
5278
|
+
throw new HTTPException13(403, { message: "GitHub installation state does not match this workspace" });
|
|
3766
5279
|
}
|
|
3767
5280
|
if (setupAction === "request" && !installationIdRaw) {
|
|
3768
5281
|
return c.html(githubSetupPendingHtml());
|
|
3769
5282
|
}
|
|
3770
5283
|
const installationId = parsePositiveInteger(installationIdRaw);
|
|
3771
5284
|
if (installationId === null) {
|
|
3772
|
-
throw new
|
|
5285
|
+
throw new HTTPException13(400, { message: "missing or invalid GitHub installation_id" });
|
|
3773
5286
|
}
|
|
3774
5287
|
if (!code) {
|
|
3775
5288
|
const clientId = settings.githubClientId?.trim();
|
|
3776
5289
|
if (!clientId) {
|
|
3777
|
-
throw new
|
|
5290
|
+
throw new HTTPException13(409, { message: JSON.stringify({ message: "GitHub App is not configured", missing: ["OPENGENI_GITHUB_CLIENT_ID"] }) });
|
|
3778
5291
|
}
|
|
3779
|
-
const oauthState =
|
|
5292
|
+
const oauthState = createSignedState4(githubStateSecret, {
|
|
3780
5293
|
accountId: grant.accountId,
|
|
3781
5294
|
workspaceId: grant.workspaceId,
|
|
3782
5295
|
installationId
|
|
@@ -3801,15 +5314,15 @@ function registerGitHubRoutes(app, deps) {
|
|
|
3801
5314
|
const code = c.req.query("code");
|
|
3802
5315
|
const state = c.req.query("state");
|
|
3803
5316
|
if (!code) {
|
|
3804
|
-
throw new
|
|
5317
|
+
throw new HTTPException13(400, { message: "missing GitHub OAuth code" });
|
|
3805
5318
|
}
|
|
3806
5319
|
if (!state) {
|
|
3807
|
-
throw new
|
|
5320
|
+
throw new HTTPException13(400, { message: "missing GitHub OAuth state" });
|
|
3808
5321
|
}
|
|
3809
|
-
const statePayload =
|
|
5322
|
+
const statePayload = readSignedState3(state, githubStateSecret);
|
|
3810
5323
|
const installationId = parsePositiveInteger(String(statePayload?.installationId ?? ""));
|
|
3811
5324
|
if (!statePayload || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string" || installationId === null) {
|
|
3812
|
-
throw new
|
|
5325
|
+
throw new HTTPException13(400, { message: "invalid or expired GitHub OAuth state" });
|
|
3813
5326
|
}
|
|
3814
5327
|
requireGitHubStateCookie(c, state);
|
|
3815
5328
|
return await completeGitHubInstallationBinding(deps, c, {
|
|
@@ -3822,19 +5335,19 @@ function registerGitHubRoutes(app, deps) {
|
|
|
3822
5335
|
async function completeGitHubInstallationBinding(deps, c, input) {
|
|
3823
5336
|
const { db, settings } = deps;
|
|
3824
5337
|
if (!input.statePayload.workspaceId || !input.statePayload.accountId) {
|
|
3825
|
-
throw new
|
|
5338
|
+
throw new HTTPException13(400, { message: "invalid or expired GitHub installation state" });
|
|
3826
5339
|
}
|
|
3827
|
-
const grant = await
|
|
5340
|
+
const grant = await requireAccessGrant10(c, deps, input.statePayload.workspaceId, "github:manage");
|
|
3828
5341
|
if (grant.accountId !== input.statePayload.accountId) {
|
|
3829
|
-
throw new
|
|
5342
|
+
throw new HTTPException13(403, { message: "GitHub installation state does not match this workspace" });
|
|
3830
5343
|
}
|
|
3831
5344
|
try {
|
|
3832
5345
|
const installation = await verifyGitHubInstallationAccessForUser(settings, { code: input.code, installationId: input.installationId });
|
|
3833
5346
|
if (!installation) {
|
|
3834
|
-
throw new
|
|
5347
|
+
throw new HTTPException13(404, { message: "GitHub App installation was not found for this app" });
|
|
3835
5348
|
}
|
|
3836
5349
|
if (installation.suspended) {
|
|
3837
|
-
throw new
|
|
5350
|
+
throw new HTTPException13(409, { message: "GitHub App installation is suspended" });
|
|
3838
5351
|
}
|
|
3839
5352
|
await upsertGitHubInstallation(db, {
|
|
3840
5353
|
accountId: grant.accountId,
|
|
@@ -3847,13 +5360,13 @@ async function completeGitHubInstallationBinding(deps, c, input) {
|
|
|
3847
5360
|
deleteCookie(c, githubStateCookie, { path: "/v1/github" });
|
|
3848
5361
|
return c.html(githubSetupSuccessHtml(installation.accountLogin ?? `installation ${input.installationId}`, returnUrl));
|
|
3849
5362
|
} catch (error) {
|
|
3850
|
-
if (error instanceof
|
|
5363
|
+
if (error instanceof HTTPException13) {
|
|
3851
5364
|
throw error;
|
|
3852
5365
|
}
|
|
3853
5366
|
if (error instanceof GitHubAppConfigurationError2) {
|
|
3854
|
-
throw new
|
|
5367
|
+
throw new HTTPException13(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
|
|
3855
5368
|
}
|
|
3856
|
-
throw new
|
|
5369
|
+
throw new HTTPException13(502, { message: error instanceof Error ? error.message : String(error) });
|
|
3857
5370
|
}
|
|
3858
5371
|
}
|
|
3859
5372
|
function setGitHubStateCookie(c, deps, state) {
|
|
@@ -3867,7 +5380,7 @@ function setGitHubStateCookie(c, deps, state) {
|
|
|
3867
5380
|
}
|
|
3868
5381
|
function requireGitHubStateCookie(c, state) {
|
|
3869
5382
|
if (getCookie(c, githubStateCookie) !== state) {
|
|
3870
|
-
throw new
|
|
5383
|
+
throw new HTTPException13(400, { message: "invalid or expired GitHub installation browser state" });
|
|
3871
5384
|
}
|
|
3872
5385
|
}
|
|
3873
5386
|
function isSecureRequest(c, deps) {
|
|
@@ -3934,8 +5447,8 @@ import {
|
|
|
3934
5447
|
updatePackInstallationStatus
|
|
3935
5448
|
} from "@opengeni/db";
|
|
3936
5449
|
import { getDocumentBase as getDocumentBase2 } from "@opengeni/documents";
|
|
3937
|
-
import { HTTPException as
|
|
3938
|
-
import { requireAccessGrant as
|
|
5450
|
+
import { HTTPException as HTTPException14 } from "hono/http-exception";
|
|
5451
|
+
import { requireAccessGrant as requireAccessGrant11 } from "@opengeni/core";
|
|
3939
5452
|
import { requireLimit as requireLimit5 } from "@opengeni/core";
|
|
3940
5453
|
import { validateEnvironmentAttachment } from "@opengeni/core";
|
|
3941
5454
|
import {
|
|
@@ -3954,7 +5467,7 @@ function registerPackRoutes(app, deps) {
|
|
|
3954
5467
|
const { settings, db, objectStorage, workflowClient } = deps;
|
|
3955
5468
|
app.get("/v1/workspaces/:workspaceId/packs", async (c) => {
|
|
3956
5469
|
const workspaceId = c.req.param("workspaceId");
|
|
3957
|
-
await
|
|
5470
|
+
await requireAccessGrant11(c, deps, workspaceId, "workspace:read");
|
|
3958
5471
|
return c.json({
|
|
3959
5472
|
packs: await listWorkspaceCapabilityPacks(db, workspaceId),
|
|
3960
5473
|
installations: await listPackInstallations(db, workspaceId)
|
|
@@ -3962,10 +5475,10 @@ function registerPackRoutes(app, deps) {
|
|
|
3962
5475
|
});
|
|
3963
5476
|
app.post("/v1/workspaces/:workspaceId/packs", async (c) => {
|
|
3964
5477
|
const workspaceId = c.req.param("workspaceId");
|
|
3965
|
-
const grant = await
|
|
5478
|
+
const grant = await requireAccessGrant11(c, deps, workspaceId, "workspace:admin");
|
|
3966
5479
|
const manifest = RegisterCapabilityPackRequest.parse(await c.req.json());
|
|
3967
5480
|
if (isBuiltInCapabilityPack(manifest.id)) {
|
|
3968
|
-
throw new
|
|
5481
|
+
throw new HTTPException14(409, { message: `pack id ${manifest.id} is a built-in pack and cannot be replaced` });
|
|
3969
5482
|
}
|
|
3970
5483
|
const { pack, created } = await registerWorkspacePack(db, {
|
|
3971
5484
|
accountId: grant.accountId,
|
|
@@ -3976,13 +5489,13 @@ function registerPackRoutes(app, deps) {
|
|
|
3976
5489
|
});
|
|
3977
5490
|
app.delete("/v1/workspaces/:workspaceId/packs/:packId", async (c) => {
|
|
3978
5491
|
const workspaceId = c.req.param("workspaceId");
|
|
3979
|
-
await
|
|
5492
|
+
await requireAccessGrant11(c, deps, workspaceId, "workspace:admin");
|
|
3980
5493
|
const packId = c.req.param("packId");
|
|
3981
5494
|
if (isBuiltInCapabilityPack(packId)) {
|
|
3982
|
-
throw new
|
|
5495
|
+
throw new HTTPException14(409, { message: "built-in packs cannot be unregistered" });
|
|
3983
5496
|
}
|
|
3984
5497
|
if (!await getWorkspacePack(db, workspaceId, packId)) {
|
|
3985
|
-
throw new
|
|
5498
|
+
throw new HTTPException14(404, { message: "pack not found" });
|
|
3986
5499
|
}
|
|
3987
5500
|
const installation = await getPackInstallation(db, workspaceId, packId);
|
|
3988
5501
|
if (installation && installation.status === "active") {
|
|
@@ -3997,12 +5510,12 @@ function registerPackRoutes(app, deps) {
|
|
|
3997
5510
|
});
|
|
3998
5511
|
app.get("/v1/workspaces/:workspaceId/packs/installations", async (c) => {
|
|
3999
5512
|
const workspaceId = c.req.param("workspaceId");
|
|
4000
|
-
await
|
|
5513
|
+
await requireAccessGrant11(c, deps, workspaceId, "workspace:read");
|
|
4001
5514
|
return c.json(await listPackInstallations(db, workspaceId));
|
|
4002
5515
|
});
|
|
4003
5516
|
app.get("/v1/workspaces/:workspaceId/packs/:packId", async (c) => {
|
|
4004
5517
|
const workspaceId = c.req.param("workspaceId");
|
|
4005
|
-
await
|
|
5518
|
+
await requireAccessGrant11(c, deps, workspaceId, "workspace:read");
|
|
4006
5519
|
const pack = await requirePack(db, workspaceId, c.req.param("packId"));
|
|
4007
5520
|
return c.json({
|
|
4008
5521
|
pack,
|
|
@@ -4011,7 +5524,7 @@ function registerPackRoutes(app, deps) {
|
|
|
4011
5524
|
});
|
|
4012
5525
|
app.post("/v1/workspaces/:workspaceId/packs/:packId/enable", async (c) => {
|
|
4013
5526
|
const workspaceId = c.req.param("workspaceId");
|
|
4014
|
-
const grant = await
|
|
5527
|
+
const grant = await requireAccessGrant11(c, deps, workspaceId, "workspace:admin");
|
|
4015
5528
|
const pack = await requirePack(db, workspaceId, c.req.param("packId"));
|
|
4016
5529
|
await assertPackSandboxImageCompatible(db, workspaceId, pack);
|
|
4017
5530
|
const existing = await getPackInstallation(db, workspaceId, pack.id);
|
|
@@ -4019,13 +5532,13 @@ function registerPackRoutes(app, deps) {
|
|
|
4019
5532
|
const storedEnvironmentId = typeof existing?.metadata.environmentId === "string" ? existing.metadata.environmentId : void 0;
|
|
4020
5533
|
const environmentId = payload.environmentId ?? storedEnvironmentId;
|
|
4021
5534
|
if (pack.environment?.required && !environmentId) {
|
|
4022
|
-
throw new
|
|
5535
|
+
throw new HTTPException14(422, { message: "this pack requires an environment attachment; pass environmentId" });
|
|
4023
5536
|
}
|
|
4024
5537
|
if (environmentId) {
|
|
4025
5538
|
const environment = await validateEnvironmentAttachment({ settings, db }, grant, workspaceId, environmentId, { preauthorized: !payload.environmentId });
|
|
4026
5539
|
const missing = (pack.environment?.requiredVariables ?? []).filter((name) => !environment.variables.some((variable) => variable.name === name));
|
|
4027
5540
|
if (missing.length > 0) {
|
|
4028
|
-
throw new
|
|
5541
|
+
throw new HTTPException14(422, { message: `environment is missing required variable(s): ${missing.join(", ")}` });
|
|
4029
5542
|
}
|
|
4030
5543
|
}
|
|
4031
5544
|
const installation = await enablePackInstallation(db, {
|
|
@@ -4042,17 +5555,17 @@ function registerPackRoutes(app, deps) {
|
|
|
4042
5555
|
});
|
|
4043
5556
|
app.post("/v1/workspaces/:workspaceId/packs/marketing-social-daily-analysis/scheduled-tasks", async (c) => {
|
|
4044
5557
|
const workspaceId = c.req.param("workspaceId");
|
|
4045
|
-
const grant = await
|
|
5558
|
+
const grant = await requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
4046
5559
|
const pack = await requirePack(db, workspaceId, MARKETING_SOCIAL_PACK_ID);
|
|
4047
5560
|
const installation = await getPackInstallation(db, workspaceId, pack.id);
|
|
4048
5561
|
if (installation?.status !== "active") {
|
|
4049
|
-
throw new
|
|
5562
|
+
throw new HTTPException14(409, { message: "enable the marketing social pack before creating its scheduled tasks" });
|
|
4050
5563
|
}
|
|
4051
5564
|
const payload = MarketingDailyAnalysisTaskRequest.parse(await c.req.json());
|
|
4052
5565
|
await requireLimit5(deps, { accountId: grant.accountId, workspaceId, action: "schedule:create", quantity: 1 });
|
|
4053
5566
|
const connections = await resolveSocialConnections(db, workspaceId, payload.connectionIds);
|
|
4054
5567
|
if (connections.length === 0) {
|
|
4055
|
-
throw new
|
|
5568
|
+
throw new HTTPException14(422, { message: "at least one connected social account is required" });
|
|
4056
5569
|
}
|
|
4057
5570
|
await validateDocumentBaseIds(db, workspaceId, payload.documentBaseIds);
|
|
4058
5571
|
const agentConfig = buildMarketingDailyAnalysisAgentConfig({
|
|
@@ -4096,7 +5609,7 @@ function registerPackRoutes(app, deps) {
|
|
|
4096
5609
|
async function requirePack(db, workspaceId, packId) {
|
|
4097
5610
|
const pack = await resolveCapabilityPack(db, workspaceId, packId);
|
|
4098
5611
|
if (!pack) {
|
|
4099
|
-
throw new
|
|
5612
|
+
throw new HTTPException14(404, { message: "pack not found" });
|
|
4100
5613
|
}
|
|
4101
5614
|
return pack;
|
|
4102
5615
|
}
|
|
@@ -4105,13 +5618,13 @@ async function resolveSocialConnections(db, workspaceId, connectionIds) {
|
|
|
4105
5618
|
const connections = ids.length > 0 ? await Promise.all(ids.map(async (id) => {
|
|
4106
5619
|
const connection = await getSocialConnection(db, workspaceId, id);
|
|
4107
5620
|
if (!connection) {
|
|
4108
|
-
throw new
|
|
5621
|
+
throw new HTTPException14(422, { message: `unknown social connection: ${id}` });
|
|
4109
5622
|
}
|
|
4110
5623
|
return connection;
|
|
4111
5624
|
})) : (await listSocialConnections2(db, workspaceId, 500)).filter((connection) => connection.status === "connected");
|
|
4112
5625
|
const inactive = connections.find((connection) => connection.status !== "connected");
|
|
4113
5626
|
if (inactive) {
|
|
4114
|
-
throw new
|
|
5627
|
+
throw new HTTPException14(422, { message: `social connection ${inactive.id} is ${inactive.status}` });
|
|
4115
5628
|
}
|
|
4116
5629
|
return connections;
|
|
4117
5630
|
}
|
|
@@ -4119,7 +5632,7 @@ async function validateDocumentBaseIds(db, workspaceId, documentBaseIds) {
|
|
|
4119
5632
|
for (const baseId of [...new Set(documentBaseIds)]) {
|
|
4120
5633
|
const base = await getDocumentBase2(db, workspaceId, baseId);
|
|
4121
5634
|
if (!base) {
|
|
4122
|
-
throw new
|
|
5635
|
+
throw new HTTPException14(422, { message: `unknown document base: ${baseId}` });
|
|
4123
5636
|
}
|
|
4124
5637
|
}
|
|
4125
5638
|
}
|
|
@@ -4132,7 +5645,7 @@ import {
|
|
|
4132
5645
|
listScheduledTasks as listScheduledTasks2,
|
|
4133
5646
|
updateScheduledTask as updateScheduledTask2
|
|
4134
5647
|
} from "@opengeni/db";
|
|
4135
|
-
import { requireAccessGrant as
|
|
5648
|
+
import { requireAccessGrant as requireAccessGrant12 } from "@opengeni/core";
|
|
4136
5649
|
import { recordWorkspaceUsage as recordWorkspaceUsage4, requireLimit as requireLimit6 } from "@opengeni/core";
|
|
4137
5650
|
import {
|
|
4138
5651
|
createValidatedScheduledTask as createValidatedScheduledTask3,
|
|
@@ -4149,7 +5662,7 @@ function registerScheduledTaskRoutes(app, deps) {
|
|
|
4149
5662
|
const { settings, db, workflowClient, objectStorage } = deps;
|
|
4150
5663
|
app.post("/v1/workspaces/:workspaceId/scheduled-tasks", async (c) => {
|
|
4151
5664
|
const workspaceId = c.req.param("workspaceId");
|
|
4152
|
-
const grant = await
|
|
5665
|
+
const grant = await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
4153
5666
|
const rawPayload = await c.req.json();
|
|
4154
5667
|
const payload = CreateScheduledTaskRequest2.parse(rawPayload);
|
|
4155
5668
|
await requireLimit6(deps, { accountId: grant.accountId, workspaceId, action: "schedule:create", quantity: 1 });
|
|
@@ -4159,17 +5672,17 @@ function registerScheduledTaskRoutes(app, deps) {
|
|
|
4159
5672
|
});
|
|
4160
5673
|
app.get("/v1/workspaces/:workspaceId/scheduled-tasks", async (c) => {
|
|
4161
5674
|
const workspaceId = c.req.param("workspaceId");
|
|
4162
|
-
await
|
|
5675
|
+
await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:run");
|
|
4163
5676
|
return c.json(await listScheduledTasks2(db, workspaceId, boundedLimit(c.req.query("limit"))));
|
|
4164
5677
|
});
|
|
4165
5678
|
app.get("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId", async (c) => {
|
|
4166
5679
|
const workspaceId = c.req.param("workspaceId");
|
|
4167
|
-
await
|
|
5680
|
+
await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:run");
|
|
4168
5681
|
return c.json(await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId")));
|
|
4169
5682
|
});
|
|
4170
5683
|
app.patch("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId", async (c) => {
|
|
4171
5684
|
const workspaceId = c.req.param("workspaceId");
|
|
4172
|
-
const grant = await
|
|
5685
|
+
const grant = await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
4173
5686
|
const taskId = c.req.param("taskId");
|
|
4174
5687
|
const existing = await requireScheduledTaskForApi(db, workspaceId, taskId);
|
|
4175
5688
|
const rawPayload = await c.req.json();
|
|
@@ -4181,7 +5694,7 @@ function registerScheduledTaskRoutes(app, deps) {
|
|
|
4181
5694
|
});
|
|
4182
5695
|
app.post("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/pause", async (c) => {
|
|
4183
5696
|
const workspaceId = c.req.param("workspaceId");
|
|
4184
|
-
await
|
|
5697
|
+
await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
4185
5698
|
const existing = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
4186
5699
|
const task = await updateScheduledTask2(db, workspaceId, existing.id, { status: "paused" });
|
|
4187
5700
|
await syncUpdatedScheduledTask2({ db, workflowClient, previous: existing, task });
|
|
@@ -4189,7 +5702,7 @@ function registerScheduledTaskRoutes(app, deps) {
|
|
|
4189
5702
|
});
|
|
4190
5703
|
app.post("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/resume", async (c) => {
|
|
4191
5704
|
const workspaceId = c.req.param("workspaceId");
|
|
4192
|
-
await
|
|
5705
|
+
await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
4193
5706
|
const existing = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
4194
5707
|
const task = await updateScheduledTask2(db, workspaceId, existing.id, { status: "active" });
|
|
4195
5708
|
await syncUpdatedScheduledTask2({ db, workflowClient, previous: existing, task });
|
|
@@ -4197,7 +5710,7 @@ function registerScheduledTaskRoutes(app, deps) {
|
|
|
4197
5710
|
});
|
|
4198
5711
|
app.post("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/trigger", async (c) => {
|
|
4199
5712
|
const workspaceId = c.req.param("workspaceId");
|
|
4200
|
-
const grant = await
|
|
5713
|
+
const grant = await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:run");
|
|
4201
5714
|
const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
4202
5715
|
await requireLimit6(deps, { accountId: grant.accountId, workspaceId, action: "agent_run:create", quantity: 1, model: task.agentConfig.model ?? deps.settings.openaiModel });
|
|
4203
5716
|
const body = await c.req.json().catch(() => ({}));
|
|
@@ -4221,7 +5734,7 @@ function registerScheduledTaskRoutes(app, deps) {
|
|
|
4221
5734
|
});
|
|
4222
5735
|
app.delete("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId", async (c) => {
|
|
4223
5736
|
const workspaceId = c.req.param("workspaceId");
|
|
4224
|
-
await
|
|
5737
|
+
await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:manage");
|
|
4225
5738
|
const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
4226
5739
|
await workflowClient.deleteScheduledTaskSchedule({ temporalScheduleId: task.temporalScheduleId });
|
|
4227
5740
|
await deleteScheduledTask2(db, workspaceId, task.id);
|
|
@@ -4229,7 +5742,7 @@ function registerScheduledTaskRoutes(app, deps) {
|
|
|
4229
5742
|
});
|
|
4230
5743
|
app.get("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/runs", async (c) => {
|
|
4231
5744
|
const workspaceId = c.req.param("workspaceId");
|
|
4232
|
-
await
|
|
5745
|
+
await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:run");
|
|
4233
5746
|
const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
|
|
4234
5747
|
return c.json(await listScheduledTaskRuns2(db, workspaceId, task.id, boundedLimit(c.req.query("limit"))));
|
|
4235
5748
|
});
|
|
@@ -4281,14 +5794,14 @@ import {
|
|
|
4281
5794
|
recordStreamAcknowledgment,
|
|
4282
5795
|
reorderQueuedSessionTurns,
|
|
4283
5796
|
requestSessionCompaction,
|
|
4284
|
-
requireSession as
|
|
5797
|
+
requireSession as requireSession3,
|
|
4285
5798
|
setSessionCodexPin,
|
|
4286
5799
|
revokeViewer,
|
|
4287
5800
|
setSessionGoalStatus as setSessionGoalStatus2,
|
|
4288
5801
|
updatePtySessionActivity,
|
|
4289
5802
|
updateQueuedSessionTurn
|
|
4290
5803
|
} from "@opengeni/db";
|
|
4291
|
-
import { appendAndPublishEvents as
|
|
5804
|
+
import { appendAndPublishEvents as appendAndPublishEvents5, coalesceSessionEventDeltas } from "@opengeni/events";
|
|
4292
5805
|
|
|
4293
5806
|
// src/sandbox/channel-a.ts
|
|
4294
5807
|
import { applyGitAuthPointerEnvironment, hasGitHubRepositorySelection, stableSandboxEnvironmentForRun } from "@opengeni/config";
|
|
@@ -4302,8 +5815,8 @@ import {
|
|
|
4302
5815
|
readLease as readLease2,
|
|
4303
5816
|
releaseLeaseHolder
|
|
4304
5817
|
} from "@opengeni/db";
|
|
4305
|
-
import { appendAndPublishEvents as
|
|
4306
|
-
import { HTTPException as
|
|
5818
|
+
import { appendAndPublishEvents as appendAndPublishEvents3 } from "@opengeni/events";
|
|
5819
|
+
import { HTTPException as HTTPException15 } from "hono/http-exception";
|
|
4307
5820
|
import {
|
|
4308
5821
|
establishSandboxSessionFromEnvelope,
|
|
4309
5822
|
serializeEstablishedSandboxEnvelope,
|
|
@@ -4318,7 +5831,7 @@ async function withChannelA(services, ctx, fn) {
|
|
|
4318
5831
|
const { db, settings, bus } = services;
|
|
4319
5832
|
const { accountId, workspaceId, session, subjectId } = ctx;
|
|
4320
5833
|
if (session.sandboxBackend === "none") {
|
|
4321
|
-
throw new
|
|
5834
|
+
throw new HTTPException15(409, { message: "sandbox not available" });
|
|
4322
5835
|
}
|
|
4323
5836
|
const sandboxGroupId = session.sandboxGroupId;
|
|
4324
5837
|
const viewerId = crypto.randomUUID();
|
|
@@ -4346,7 +5859,7 @@ async function withChannelA(services, ctx, fn) {
|
|
|
4346
5859
|
});
|
|
4347
5860
|
if (acquired.role === "fenced") {
|
|
4348
5861
|
await release();
|
|
4349
|
-
throw new
|
|
5862
|
+
throw new HTTPException15(409, { message: `sandbox lease superseded (epoch ${acquired.lease.leaseEpoch}); retry` });
|
|
4350
5863
|
}
|
|
4351
5864
|
let established;
|
|
4352
5865
|
let leaseSnapshot = acquired.lease;
|
|
@@ -4354,7 +5867,7 @@ async function withChannelA(services, ctx, fn) {
|
|
|
4354
5867
|
const envelope = await getSandboxSessionEnvelope(db, workspaceId, session.id);
|
|
4355
5868
|
const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(db, settings, workspaceId, session.environmentId);
|
|
4356
5869
|
const settingsForSession = session.sandboxBackend !== settings.sandboxBackend ? { ...settings, sandboxBackend: session.sandboxBackend } : settings;
|
|
4357
|
-
const environment = stableSandboxEnvironmentForRun(settingsForSession, workspaceEnvironment?.values ?? {});
|
|
5870
|
+
const environment = stableSandboxEnvironmentForRun(settingsForSession, workspaceEnvironment?.values ?? {}, { workspaceId });
|
|
4358
5871
|
if (hasGitHubRepositorySelection(session.resources)) {
|
|
4359
5872
|
applyGitAuthPointerEnvironment(environment, githubAppBotIdentity(settings));
|
|
4360
5873
|
}
|
|
@@ -4369,7 +5882,7 @@ async function withChannelA(services, ctx, fn) {
|
|
|
4369
5882
|
});
|
|
4370
5883
|
} catch (error) {
|
|
4371
5884
|
await failWarmingToCold(db, { accountId, workspaceId, sandboxGroupId, expectedEpoch });
|
|
4372
|
-
throw new
|
|
5885
|
+
throw new HTTPException15(409, { message: `sandbox not available (${error instanceof Error ? error.message : "spawn failed"})` });
|
|
4373
5886
|
}
|
|
4374
5887
|
const resumeEnvelope = await serializeEstablishedSandboxEnvelope(established) ?? envelope ?? null;
|
|
4375
5888
|
const committed = await commitWarmingToWarm(db, {
|
|
@@ -4384,7 +5897,7 @@ async function withChannelA(services, ctx, fn) {
|
|
|
4384
5897
|
leaseTtlMs
|
|
4385
5898
|
});
|
|
4386
5899
|
if (!committed.committed || !committed.lease) {
|
|
4387
|
-
throw new
|
|
5900
|
+
throw new HTTPException15(409, { message: `sandbox lease superseded (epoch ${expectedEpoch}); retry` });
|
|
4388
5901
|
}
|
|
4389
5902
|
leaseSnapshot = committed.lease;
|
|
4390
5903
|
} else {
|
|
@@ -4398,7 +5911,7 @@ async function withChannelA(services, ctx, fn) {
|
|
|
4398
5911
|
});
|
|
4399
5912
|
}
|
|
4400
5913
|
const emit = async (events) => {
|
|
4401
|
-
await
|
|
5914
|
+
await appendAndPublishEvents3(
|
|
4402
5915
|
db,
|
|
4403
5916
|
bus,
|
|
4404
5917
|
workspaceId,
|
|
@@ -4423,11 +5936,11 @@ async function withChannelA(services, ctx, fn) {
|
|
|
4423
5936
|
}
|
|
4424
5937
|
}
|
|
4425
5938
|
function mapChannelAError(error) {
|
|
4426
|
-
if (error instanceof
|
|
4427
|
-
if (error instanceof ChannelAValidationError) return new
|
|
4428
|
-
if (error instanceof ChannelANotFoundError) return new
|
|
4429
|
-
if (error instanceof ChannelAConflictError) return new
|
|
4430
|
-
if (error instanceof ChannelAUnsupportedError) return new
|
|
5939
|
+
if (error instanceof HTTPException15) return error;
|
|
5940
|
+
if (error instanceof ChannelAValidationError) return new HTTPException15(400, { message: error.message });
|
|
5941
|
+
if (error instanceof ChannelANotFoundError) return new HTTPException15(404, { message: error.message });
|
|
5942
|
+
if (error instanceof ChannelAConflictError) return new HTTPException15(409, { message: error.message });
|
|
5943
|
+
if (error instanceof ChannelAUnsupportedError) return new HTTPException15(409, { message: error.message });
|
|
4431
5944
|
return error;
|
|
4432
5945
|
}
|
|
4433
5946
|
async function dropEstablishedHandle(established) {
|
|
@@ -4436,11 +5949,11 @@ async function dropEstablishedHandle(established) {
|
|
|
4436
5949
|
|
|
4437
5950
|
// src/routes/sessions.ts
|
|
4438
5951
|
import { negotiateCapabilities } from "@opengeni/runtime/sandbox";
|
|
4439
|
-
import { HTTPException as
|
|
4440
|
-
import { requireAccessGrant as
|
|
5952
|
+
import { HTTPException as HTTPException17 } from "hono/http-exception";
|
|
5953
|
+
import { requireAccessGrant as requireAccessGrant13 } from "@opengeni/core";
|
|
4441
5954
|
|
|
4442
5955
|
// src/sandbox/viewer.ts
|
|
4443
|
-
import { createHash } from "crypto";
|
|
5956
|
+
import { createHash as createHash2 } from "crypto";
|
|
4444
5957
|
import { applyGitAuthPointerEnvironment as applyGitAuthPointerEnvironment2, hasGitHubRepositorySelection as hasGitHubRepositorySelection2, resolveStreamTokenSecret, stableSandboxEnvironmentForRun as stableSandboxEnvironmentForRun2 } from "@opengeni/config";
|
|
4445
5958
|
import { githubAppBotIdentity as githubAppBotIdentity2 } from "@opengeni/github";
|
|
4446
5959
|
import {
|
|
@@ -4457,8 +5970,8 @@ import {
|
|
|
4457
5970
|
releaseLeaseHolder as releaseLeaseHolder2,
|
|
4458
5971
|
SandboxLeaseSupersededError as SandboxLeaseSupersededError2
|
|
4459
5972
|
} from "@opengeni/db";
|
|
4460
|
-
import { appendAndPublishEvents as
|
|
4461
|
-
import { HTTPException as
|
|
5973
|
+
import { appendAndPublishEvents as appendAndPublishEvents4 } from "@opengeni/events";
|
|
5974
|
+
import { HTTPException as HTTPException16 } from "hono/http-exception";
|
|
4462
5975
|
import {
|
|
4463
5976
|
DESKTOP_STREAM_PORT,
|
|
4464
5977
|
ensureDisplayStack,
|
|
@@ -4485,7 +5998,7 @@ async function sessionAttachEnvironment(services, workspaceId, session) {
|
|
|
4485
5998
|
session.environmentId
|
|
4486
5999
|
);
|
|
4487
6000
|
const settingsForSession = session.sandboxBackend !== services.settings.sandboxBackend ? { ...services.settings, sandboxBackend: session.sandboxBackend } : services.settings;
|
|
4488
|
-
const environment = stableSandboxEnvironmentForRun2(settingsForSession, workspaceEnvironment?.values ?? {});
|
|
6001
|
+
const environment = stableSandboxEnvironmentForRun2(settingsForSession, workspaceEnvironment?.values ?? {}, { workspaceId });
|
|
4489
6002
|
if (hasGitHubRepositorySelection2(session.resources)) {
|
|
4490
6003
|
applyGitAuthPointerEnvironment2(environment, githubAppBotIdentity2(services.settings));
|
|
4491
6004
|
}
|
|
@@ -4520,7 +6033,7 @@ async function attachViewer(services, input) {
|
|
|
4520
6033
|
});
|
|
4521
6034
|
if (acquired.role === "fenced") {
|
|
4522
6035
|
await release();
|
|
4523
|
-
throw new
|
|
6036
|
+
throw new HTTPException16(409, { message: `sandbox lease superseded (epoch ${acquired.lease.leaseEpoch}); re-read capabilities and re-attach` });
|
|
4524
6037
|
}
|
|
4525
6038
|
if (acquired.role === "spawner") {
|
|
4526
6039
|
const expectedEpoch = acquired.lease.leaseEpoch;
|
|
@@ -4561,12 +6074,12 @@ async function attachViewer(services, input) {
|
|
|
4561
6074
|
};
|
|
4562
6075
|
} catch (error) {
|
|
4563
6076
|
if (error instanceof SandboxLeaseSupersededError2) {
|
|
4564
|
-
throw new
|
|
6077
|
+
throw new HTTPException16(409, { message: `sandbox lease superseded (epoch ${error.leaseEpoch}); re-read capabilities and re-attach` });
|
|
4565
6078
|
}
|
|
4566
6079
|
await failWarmingToCold2(db, { accountId, workspaceId, sandboxGroupId, expectedEpoch });
|
|
4567
6080
|
await release();
|
|
4568
|
-
if (error instanceof
|
|
4569
|
-
throw new
|
|
6081
|
+
if (error instanceof HTTPException16) throw error;
|
|
6082
|
+
throw new HTTPException16(409, { message: `sandbox not available (${error instanceof Error ? error.message : "spawn failed"})` });
|
|
4570
6083
|
} finally {
|
|
4571
6084
|
await dropEstablishedHandle2(established);
|
|
4572
6085
|
}
|
|
@@ -4715,7 +6228,7 @@ async function mintDesktopStream(services, input) {
|
|
|
4715
6228
|
viewerId
|
|
4716
6229
|
};
|
|
4717
6230
|
try {
|
|
4718
|
-
await
|
|
6231
|
+
await appendAndPublishEvents4(db, bus, workspaceId, session.id, [
|
|
4719
6232
|
{ type: "stream.url.rotated", payload }
|
|
4720
6233
|
]);
|
|
4721
6234
|
} catch {
|
|
@@ -4910,7 +6423,7 @@ function viewerIdAsUuid(rawViewerId) {
|
|
|
4910
6423
|
if (UUID_RE.test(rawViewerId)) {
|
|
4911
6424
|
return rawViewerId;
|
|
4912
6425
|
}
|
|
4913
|
-
const hex =
|
|
6426
|
+
const hex = createHash2("sha256").update(`opengeni:stream-viewer:${rawViewerId}`).digest("hex");
|
|
4914
6427
|
const b = hex.slice(0, 32).split("");
|
|
4915
6428
|
b[12] = "5";
|
|
4916
6429
|
const variantNibble = parseInt(b[16], 16) & 3 | 8;
|
|
@@ -4920,7 +6433,7 @@ function viewerIdAsUuid(rawViewerId) {
|
|
|
4920
6433
|
}
|
|
4921
6434
|
|
|
4922
6435
|
// src/routes/sessions.ts
|
|
4923
|
-
import { settingsWithEnabledCapabilityMcpServers, settingsWithSessionMcpServerMetadata } from "@opengeni/core";
|
|
6436
|
+
import { settingsWithEnabledCapabilityMcpServers as settingsWithEnabledCapabilityMcpServers2, settingsWithSessionMcpServerMetadata } from "@opengeni/core";
|
|
4924
6437
|
import {
|
|
4925
6438
|
normalizeResources,
|
|
4926
6439
|
validateFileResources,
|
|
@@ -5019,76 +6532,76 @@ function registerSessionRoutes(app, deps) {
|
|
|
5019
6532
|
const { settings, db, bus, workflowClient, objectStorage } = deps;
|
|
5020
6533
|
app.post("/v1/workspaces/:workspaceId/sessions", async (c) => {
|
|
5021
6534
|
const workspaceId = c.req.param("workspaceId");
|
|
5022
|
-
const grant = await
|
|
6535
|
+
const grant = await requireAccessGrant13(c, deps, workspaceId, "sessions:create");
|
|
5023
6536
|
const session = await createSessionForRequest2(deps, grant, workspaceId, await c.req.json());
|
|
5024
6537
|
return c.json(session, 202);
|
|
5025
6538
|
});
|
|
5026
6539
|
app.get("/v1/workspaces/:workspaceId/sessions", async (c) => {
|
|
5027
6540
|
const workspaceId = c.req.param("workspaceId");
|
|
5028
|
-
await
|
|
6541
|
+
await requireAccessGrant13(c, deps, workspaceId, "sessions:read");
|
|
5029
6542
|
return c.json(await listSessions2(db, workspaceId, boundedLimit(c.req.query("limit"))));
|
|
5030
6543
|
});
|
|
5031
6544
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
|
|
5032
6545
|
const workspaceId = c.req.param("workspaceId");
|
|
5033
|
-
await
|
|
6546
|
+
await requireAccessGrant13(c, deps, workspaceId, "sessions:read");
|
|
5034
6547
|
const session = await getSession4(db, workspaceId, c.req.param("sessionId"));
|
|
5035
6548
|
if (!session) {
|
|
5036
|
-
throw new
|
|
6549
|
+
throw new HTTPException17(404, { message: "session not found" });
|
|
5037
6550
|
}
|
|
5038
6551
|
return c.json(session);
|
|
5039
6552
|
});
|
|
5040
6553
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/codex-account", async (c) => {
|
|
5041
6554
|
const workspaceId = c.req.param("workspaceId");
|
|
5042
|
-
await
|
|
6555
|
+
await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
|
|
5043
6556
|
const sessionId = c.req.param("sessionId");
|
|
5044
6557
|
const body = await c.req.json();
|
|
5045
6558
|
const target = typeof body.target === "string" ? body.target : "";
|
|
5046
6559
|
if (!target) {
|
|
5047
|
-
throw new
|
|
6560
|
+
throw new HTTPException17(400, { message: 'target is required ("auto" or an account id)' });
|
|
5048
6561
|
}
|
|
5049
6562
|
const pinned = target === "auto" ? null : target;
|
|
5050
6563
|
const ok = await setSessionCodexPin(db, workspaceId, sessionId, pinned);
|
|
5051
6564
|
if (!ok) {
|
|
5052
|
-
throw new
|
|
6565
|
+
throw new HTTPException17(404, { message: "session or codex account not found" });
|
|
5053
6566
|
}
|
|
5054
6567
|
return c.json({ pinned: target === "auto" ? "auto" : target });
|
|
5055
6568
|
});
|
|
5056
6569
|
app.patch("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
|
|
5057
6570
|
const workspaceId = c.req.param("workspaceId");
|
|
5058
|
-
await
|
|
6571
|
+
await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
|
|
5059
6572
|
const sessionId = c.req.param("sessionId");
|
|
5060
6573
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
5061
6574
|
const payload = UpdateSessionRequest.parse(await c.req.json());
|
|
5062
6575
|
await updateSessionTitle2({ db, bus }, workspaceId, sessionId, payload.title, "user");
|
|
5063
6576
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
5064
6577
|
if (!session) {
|
|
5065
|
-
throw new
|
|
6578
|
+
throw new HTTPException17(404, { message: "session not found" });
|
|
5066
6579
|
}
|
|
5067
6580
|
return c.json(session);
|
|
5068
6581
|
});
|
|
5069
6582
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/goal", async (c) => {
|
|
5070
6583
|
const workspaceId = c.req.param("workspaceId");
|
|
5071
|
-
await
|
|
6584
|
+
await requireAccessGrant13(c, deps, workspaceId, "sessions:read");
|
|
5072
6585
|
const sessionId = c.req.param("sessionId");
|
|
5073
6586
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
5074
6587
|
const goal = await getSessionGoal2(db, workspaceId, sessionId);
|
|
5075
6588
|
if (!goal) {
|
|
5076
|
-
throw new
|
|
6589
|
+
throw new HTTPException17(404, { message: "session goal not found" });
|
|
5077
6590
|
}
|
|
5078
6591
|
return c.json(goal);
|
|
5079
6592
|
});
|
|
5080
6593
|
app.patch("/v1/workspaces/:workspaceId/sessions/:sessionId/goal", async (c) => {
|
|
5081
6594
|
const workspaceId = c.req.param("workspaceId");
|
|
5082
|
-
const grant = await
|
|
6595
|
+
const grant = await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
|
|
5083
6596
|
const sessionId = c.req.param("sessionId");
|
|
5084
6597
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
5085
6598
|
const payload = UpdateSessionGoalRequest.parse(await c.req.json());
|
|
5086
6599
|
const existing = await getSessionGoal2(db, workspaceId, sessionId);
|
|
5087
6600
|
if (!existing) {
|
|
5088
|
-
throw new
|
|
6601
|
+
throw new HTTPException17(404, { message: "session goal not found" });
|
|
5089
6602
|
}
|
|
5090
6603
|
if (existing.status === "completed") {
|
|
5091
|
-
throw new
|
|
6604
|
+
throw new HTTPException17(409, { message: "session goal is completed; set a new goal instead" });
|
|
5092
6605
|
}
|
|
5093
6606
|
if (payload.status === "paused") {
|
|
5094
6607
|
const { goal: goal2, changed: changed2 } = await setSessionGoalStatus2(db, workspaceId, sessionId, {
|
|
@@ -5097,7 +6610,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
5097
6610
|
pausedReason: "api"
|
|
5098
6611
|
});
|
|
5099
6612
|
if (changed2) {
|
|
5100
|
-
await
|
|
6613
|
+
await appendAndPublishEvents5(db, bus, workspaceId, sessionId, [{
|
|
5101
6614
|
type: "goal.paused",
|
|
5102
6615
|
payload: {
|
|
5103
6616
|
goalId: goal2.id,
|
|
@@ -5112,11 +6625,11 @@ function registerSessionRoutes(app, deps) {
|
|
|
5112
6625
|
return c.json(goal2);
|
|
5113
6626
|
}
|
|
5114
6627
|
if (existing.status !== "paused") {
|
|
5115
|
-
throw new
|
|
6628
|
+
throw new HTTPException17(409, { message: `session goal is ${existing.status}; only paused goals can be resumed` });
|
|
5116
6629
|
}
|
|
5117
6630
|
const { goal, changed } = await setSessionGoalStatus2(db, workspaceId, sessionId, { status: "active" });
|
|
5118
6631
|
if (changed) {
|
|
5119
|
-
await
|
|
6632
|
+
await appendAndPublishEvents5(db, bus, workspaceId, sessionId, [{
|
|
5120
6633
|
type: "goal.resumed",
|
|
5121
6634
|
payload: {
|
|
5122
6635
|
goalId: goal.id,
|
|
@@ -5132,19 +6645,19 @@ function registerSessionRoutes(app, deps) {
|
|
|
5132
6645
|
});
|
|
5133
6646
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/context/clear", async (c) => {
|
|
5134
6647
|
const workspaceId = c.req.param("workspaceId");
|
|
5135
|
-
const grant = await
|
|
6648
|
+
const grant = await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
|
|
5136
6649
|
const sessionId = c.req.param("sessionId");
|
|
5137
6650
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
5138
6651
|
const clearBody = ClearSessionContextRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
5139
6652
|
if (!clearBody.success) {
|
|
5140
|
-
throw new
|
|
6653
|
+
throw new HTTPException17(400, { message: "context clear requires an explicit { confirm: true }" });
|
|
5141
6654
|
}
|
|
5142
|
-
const session = await
|
|
6655
|
+
const session = await requireSession3(db, workspaceId, sessionId);
|
|
5143
6656
|
if (session.status === "queued" || session.status === "running" || session.status === "requires_action") {
|
|
5144
|
-
throw new
|
|
6657
|
+
throw new HTTPException17(409, { message: `session is ${session.status}; cannot clear context mid-turn \u2014 stop the turn first` });
|
|
5145
6658
|
}
|
|
5146
6659
|
const result = await clearSessionContext(db, { accountId: grant.accountId, workspaceId, sessionId });
|
|
5147
|
-
await
|
|
6660
|
+
await appendAndPublishEvents5(db, bus, workspaceId, sessionId, [{
|
|
5148
6661
|
type: "session.context.cleared",
|
|
5149
6662
|
payload: {
|
|
5150
6663
|
clearedBy: "api",
|
|
@@ -5156,7 +6669,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
5156
6669
|
});
|
|
5157
6670
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/context/compact", async (c) => {
|
|
5158
6671
|
const workspaceId = c.req.param("workspaceId");
|
|
5159
|
-
await
|
|
6672
|
+
await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
|
|
5160
6673
|
const sessionId = c.req.param("sessionId");
|
|
5161
6674
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
5162
6675
|
CompactSessionContextRequest.parse(await c.req.json().catch(() => ({})) ?? {});
|
|
@@ -5172,7 +6685,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
5172
6685
|
});
|
|
5173
6686
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/events", async (c) => {
|
|
5174
6687
|
const workspaceId = c.req.param("workspaceId");
|
|
5175
|
-
await
|
|
6688
|
+
await requireAccessGrant13(c, deps, workspaceId, "sessions:read");
|
|
5176
6689
|
const sessionId = c.req.param("sessionId");
|
|
5177
6690
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
5178
6691
|
const after = eventSequence(c.req.query("after"), 0);
|
|
@@ -5188,7 +6701,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
5188
6701
|
});
|
|
5189
6702
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/events/stream", async (c) => {
|
|
5190
6703
|
const workspaceId = c.req.param("workspaceId");
|
|
5191
|
-
await
|
|
6704
|
+
await requireAccessGrant13(c, deps, workspaceId, "sessions:read");
|
|
5192
6705
|
const sessionId = c.req.param("sessionId");
|
|
5193
6706
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
5194
6707
|
const after = Number(c.req.query("after") ?? c.req.header("Last-Event-ID") ?? 0);
|
|
@@ -5196,29 +6709,29 @@ function registerSessionRoutes(app, deps) {
|
|
|
5196
6709
|
});
|
|
5197
6710
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/turns", async (c) => {
|
|
5198
6711
|
const workspaceId = c.req.param("workspaceId");
|
|
5199
|
-
await
|
|
6712
|
+
await requireAccessGrant13(c, deps, workspaceId, "sessions:read");
|
|
5200
6713
|
const sessionId = c.req.param("sessionId");
|
|
5201
6714
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
5202
6715
|
return c.json(await listSessionTurns(db, workspaceId, sessionId, boundedLimit(c.req.query("limit"))));
|
|
5203
6716
|
});
|
|
5204
6717
|
app.patch("/v1/workspaces/:workspaceId/sessions/:sessionId/turns/:turnId", async (c) => {
|
|
5205
6718
|
const workspaceId = c.req.param("workspaceId");
|
|
5206
|
-
await
|
|
6719
|
+
await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
|
|
5207
6720
|
const sessionId = c.req.param("sessionId");
|
|
5208
6721
|
const turnId = c.req.param("turnId");
|
|
5209
6722
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
5210
6723
|
const existing = await requireQueuedTurnForApi(db, workspaceId, sessionId, turnId);
|
|
5211
6724
|
const payload = UpdateSessionTurnRequest.parse(await c.req.json());
|
|
5212
6725
|
assertConfiguredModel(settings, payload.model);
|
|
5213
|
-
const session = await
|
|
6726
|
+
const session = await requireSession3(db, workspaceId, sessionId);
|
|
5214
6727
|
const runtimeSettings = settingsWithSessionMcpServerMetadata(
|
|
5215
|
-
await
|
|
6728
|
+
await settingsWithEnabledCapabilityMcpServers2(db, workspaceId, settings),
|
|
5216
6729
|
session.mcpServers
|
|
5217
6730
|
);
|
|
5218
6731
|
const resources = payload.resources !== void 0 ? normalizeResources(payload.resources) : existing.resources;
|
|
5219
6732
|
const tools = payload.tools !== void 0 ? validateToolRefs(payload.tools, runtimeSettings) : existing.tools;
|
|
5220
6733
|
if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
|
|
5221
|
-
throw new
|
|
6734
|
+
throw new HTTPException17(503, { message: "object storage is not configured" });
|
|
5222
6735
|
}
|
|
5223
6736
|
await validateFileResources(db, workspaceId, resources);
|
|
5224
6737
|
await validateGitHubRepositorySelection(db, workspaceId, [...session.resources, ...resources]);
|
|
@@ -5231,7 +6744,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
5231
6744
|
resources,
|
|
5232
6745
|
tools
|
|
5233
6746
|
});
|
|
5234
|
-
await
|
|
6747
|
+
await appendAndPublishEvents5(db, bus, workspaceId, sessionId, [{
|
|
5235
6748
|
type: "turn.updated",
|
|
5236
6749
|
turnId: turn.id,
|
|
5237
6750
|
payload: { turnId: turn.id }
|
|
@@ -5240,12 +6753,12 @@ function registerSessionRoutes(app, deps) {
|
|
|
5240
6753
|
});
|
|
5241
6754
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/turns/reorder", async (c) => {
|
|
5242
6755
|
const workspaceId = c.req.param("workspaceId");
|
|
5243
|
-
const grant = await
|
|
6756
|
+
const grant = await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
|
|
5244
6757
|
const sessionId = c.req.param("sessionId");
|
|
5245
6758
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
5246
6759
|
const payload = ReorderSessionTurnsRequest.parse(await c.req.json());
|
|
5247
6760
|
const turns = await reorderQueuedSessionTurns(db, workspaceId, sessionId, payload.turnIds);
|
|
5248
|
-
await
|
|
6761
|
+
await appendAndPublishEvents5(db, bus, workspaceId, sessionId, [{
|
|
5249
6762
|
type: "turn.updated",
|
|
5250
6763
|
payload: { reorderedTurnIds: payload.turnIds }
|
|
5251
6764
|
}]);
|
|
@@ -5254,13 +6767,13 @@ function registerSessionRoutes(app, deps) {
|
|
|
5254
6767
|
});
|
|
5255
6768
|
app.delete("/v1/workspaces/:workspaceId/sessions/:sessionId/turns/:turnId", async (c) => {
|
|
5256
6769
|
const workspaceId = c.req.param("workspaceId");
|
|
5257
|
-
await
|
|
6770
|
+
await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
|
|
5258
6771
|
const sessionId = c.req.param("sessionId");
|
|
5259
6772
|
const turnId = c.req.param("turnId");
|
|
5260
6773
|
await assertSessionExists(db, workspaceId, sessionId);
|
|
5261
6774
|
await requireQueuedTurnForApi(db, workspaceId, sessionId, turnId);
|
|
5262
6775
|
const turn = await cancelQueuedSessionTurn(db, workspaceId, turnId);
|
|
5263
|
-
await
|
|
6776
|
+
await appendAndPublishEvents5(db, bus, workspaceId, sessionId, [{
|
|
5264
6777
|
type: "turn.cancelled",
|
|
5265
6778
|
turnId: turn.id,
|
|
5266
6779
|
payload: { turnId: turn.id, triggerEventId: turn.triggerEventId }
|
|
@@ -5269,7 +6782,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
5269
6782
|
});
|
|
5270
6783
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/events", async (c) => {
|
|
5271
6784
|
const workspaceId = c.req.param("workspaceId");
|
|
5272
|
-
const grant = await
|
|
6785
|
+
const grant = await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
|
|
5273
6786
|
const sessionId = c.req.param("sessionId");
|
|
5274
6787
|
const rawEvent = await c.req.json();
|
|
5275
6788
|
const event = ClientSessionEvent.parse(rawEvent);
|
|
@@ -5286,19 +6799,19 @@ function registerSessionRoutes(app, deps) {
|
|
|
5286
6799
|
});
|
|
5287
6800
|
return c.json(accepted2, 202);
|
|
5288
6801
|
}
|
|
5289
|
-
const session = await
|
|
6802
|
+
const session = await requireSession3(db, workspaceId, sessionId);
|
|
5290
6803
|
if (event.type === "user.approvalDecision" && session.status !== "requires_action") {
|
|
5291
|
-
throw new
|
|
6804
|
+
throw new HTTPException17(409, { message: `session is ${session.status}; no approval is pending` });
|
|
5292
6805
|
}
|
|
5293
6806
|
const eventsToAppend = [{
|
|
5294
6807
|
type: event.type,
|
|
5295
6808
|
payload: event.payload,
|
|
5296
6809
|
...event.clientEventId ? { clientEventId: event.clientEventId } : {}
|
|
5297
6810
|
}];
|
|
5298
|
-
const appended = await
|
|
6811
|
+
const appended = await appendAndPublishEvents5(db, bus, workspaceId, sessionId, eventsToAppend);
|
|
5299
6812
|
const accepted = appended[0];
|
|
5300
6813
|
if (!accepted) {
|
|
5301
|
-
throw new
|
|
6814
|
+
throw new HTTPException17(500, { message: "failed to append client event" });
|
|
5302
6815
|
}
|
|
5303
6816
|
const workflowId = workflowIdForSession2(sessionId);
|
|
5304
6817
|
if (event.type === "user.approvalDecision") {
|
|
@@ -5316,7 +6829,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
5316
6829
|
});
|
|
5317
6830
|
function assertOwnershipEnabled() {
|
|
5318
6831
|
if (!settings.sandboxOwnershipEnabled) {
|
|
5319
|
-
throw new
|
|
6832
|
+
throw new HTTPException17(404, { message: "sandbox ownership is not enabled for this deployment" });
|
|
5320
6833
|
}
|
|
5321
6834
|
}
|
|
5322
6835
|
async function resolveSharedExposure(workspaceId, session) {
|
|
@@ -5326,12 +6839,12 @@ function registerSessionRoutes(app, deps) {
|
|
|
5326
6839
|
}
|
|
5327
6840
|
app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/stream-capabilities", async (c) => {
|
|
5328
6841
|
const workspaceId = c.req.param("workspaceId");
|
|
5329
|
-
const grant = await
|
|
6842
|
+
const grant = await requireAccessGrant13(c, deps, workspaceId, "sessions:read");
|
|
5330
6843
|
assertOwnershipEnabled();
|
|
5331
6844
|
const sessionId = c.req.param("sessionId");
|
|
5332
6845
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
5333
6846
|
if (!session) {
|
|
5334
|
-
throw new
|
|
6847
|
+
throw new HTTPException17(404, { message: "session not found" });
|
|
5335
6848
|
}
|
|
5336
6849
|
const lease = await readGroupLease({ db, settings }, { workspaceId, sandboxGroupId: session.sandboxGroupId });
|
|
5337
6850
|
const { shared, sharedSessionIds } = await resolveSharedExposure(workspaceId, session);
|
|
@@ -5421,16 +6934,16 @@ function registerSessionRoutes(app, deps) {
|
|
|
5421
6934
|
});
|
|
5422
6935
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/stream-capabilities/acknowledge", async (c) => {
|
|
5423
6936
|
const workspaceId = c.req.param("workspaceId");
|
|
5424
|
-
const grant = await
|
|
6937
|
+
const grant = await requireAccessGrant13(c, deps, workspaceId, "stream:acknowledge");
|
|
5425
6938
|
assertOwnershipEnabled();
|
|
5426
6939
|
const sessionId = c.req.param("sessionId");
|
|
5427
6940
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
5428
6941
|
if (!session) {
|
|
5429
|
-
throw new
|
|
6942
|
+
throw new HTTPException17(404, { message: "session not found" });
|
|
5430
6943
|
}
|
|
5431
6944
|
const parsed = AcknowledgeStreamRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
5432
6945
|
if (!parsed.success) {
|
|
5433
|
-
throw new
|
|
6946
|
+
throw new HTTPException17(400, { message: "invalid stream acknowledgment request" });
|
|
5434
6947
|
}
|
|
5435
6948
|
const recorded = await recordStreamAcknowledgment(db, {
|
|
5436
6949
|
accountId: grant.accountId,
|
|
@@ -5444,26 +6957,26 @@ function registerSessionRoutes(app, deps) {
|
|
|
5444
6957
|
});
|
|
5445
6958
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/viewers", async (c) => {
|
|
5446
6959
|
const workspaceId = c.req.param("workspaceId");
|
|
5447
|
-
const grant = await
|
|
6960
|
+
const grant = await requireAccessGrant13(c, deps, workspaceId, "stream:view");
|
|
5448
6961
|
assertOwnershipEnabled();
|
|
5449
6962
|
const sessionId = c.req.param("sessionId");
|
|
5450
6963
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
5451
6964
|
if (!session) {
|
|
5452
|
-
throw new
|
|
6965
|
+
throw new HTTPException17(404, { message: "session not found" });
|
|
5453
6966
|
}
|
|
5454
6967
|
const parsed = AttachViewerRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
5455
6968
|
if (!parsed.success) {
|
|
5456
|
-
throw new
|
|
6969
|
+
throw new HTTPException17(400, { message: "invalid viewer attach request" });
|
|
5457
6970
|
}
|
|
5458
6971
|
const wantDesktop = parsed.data.desktop ?? false;
|
|
5459
6972
|
const { shared } = await resolveSharedExposure(workspaceId, session);
|
|
5460
6973
|
if (wantDesktop) {
|
|
5461
6974
|
const ack = await getStreamAcknowledgment(db, { workspaceId, sandboxGroupId: session.sandboxGroupId, subjectId: grant.subjectId });
|
|
5462
6975
|
if (!ack?.acknowledgedUnredacted) {
|
|
5463
|
-
throw new
|
|
6976
|
+
throw new HTTPException17(409, { message: "stream_acknowledgment_required" });
|
|
5464
6977
|
}
|
|
5465
6978
|
if (shared && !ack.acknowledgedShared) {
|
|
5466
|
-
throw new
|
|
6979
|
+
throw new HTTPException17(409, { message: "shared_acknowledgment_required" });
|
|
5467
6980
|
}
|
|
5468
6981
|
}
|
|
5469
6982
|
const activeSandbox = session.activeSandboxId ? await getSandbox2(db, workspaceId, session.activeSandboxId) : null;
|
|
@@ -5557,16 +7070,16 @@ function registerSessionRoutes(app, deps) {
|
|
|
5557
7070
|
});
|
|
5558
7071
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/viewers/:viewerId/heartbeat", async (c) => {
|
|
5559
7072
|
const workspaceId = c.req.param("workspaceId");
|
|
5560
|
-
const grant = await
|
|
7073
|
+
const grant = await requireAccessGrant13(c, deps, workspaceId, "stream:view");
|
|
5561
7074
|
assertOwnershipEnabled();
|
|
5562
7075
|
const sessionId = c.req.param("sessionId");
|
|
5563
7076
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
5564
7077
|
if (!session) {
|
|
5565
|
-
throw new
|
|
7078
|
+
throw new HTTPException17(404, { message: "session not found" });
|
|
5566
7079
|
}
|
|
5567
7080
|
const parsed = ViewerHeartbeatRequest.safeParse(await c.req.json().catch(() => ({})));
|
|
5568
7081
|
if (!parsed.success) {
|
|
5569
|
-
throw new
|
|
7082
|
+
throw new HTTPException17(400, { message: "viewer heartbeat requires { leaseEpoch }" });
|
|
5570
7083
|
}
|
|
5571
7084
|
const alive = await heartbeatViewer({ db, settings }, {
|
|
5572
7085
|
accountId: grant.accountId,
|
|
@@ -5579,12 +7092,12 @@ function registerSessionRoutes(app, deps) {
|
|
|
5579
7092
|
});
|
|
5580
7093
|
app.delete("/v1/workspaces/:workspaceId/sessions/:sessionId/viewers/:viewerId", async (c) => {
|
|
5581
7094
|
const workspaceId = c.req.param("workspaceId");
|
|
5582
|
-
const grant = await
|
|
7095
|
+
const grant = await requireAccessGrant13(c, deps, workspaceId, "stream:view");
|
|
5583
7096
|
assertOwnershipEnabled();
|
|
5584
7097
|
const sessionId = c.req.param("sessionId");
|
|
5585
7098
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
5586
7099
|
if (!session) {
|
|
5587
|
-
throw new
|
|
7100
|
+
throw new HTTPException17(404, { message: "session not found" });
|
|
5588
7101
|
}
|
|
5589
7102
|
await detachViewer({ db, settings }, {
|
|
5590
7103
|
accountId: grant.accountId,
|
|
@@ -5596,12 +7109,12 @@ function registerSessionRoutes(app, deps) {
|
|
|
5596
7109
|
});
|
|
5597
7110
|
app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/viewers/:viewerId/revoke", async (c) => {
|
|
5598
7111
|
const workspaceId = c.req.param("workspaceId");
|
|
5599
|
-
const grant = await
|
|
7112
|
+
const grant = await requireAccessGrant13(c, deps, workspaceId, "stream:view");
|
|
5600
7113
|
assertOwnershipEnabled();
|
|
5601
7114
|
const sessionId = c.req.param("sessionId");
|
|
5602
7115
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
5603
7116
|
if (!session) {
|
|
5604
|
-
throw new
|
|
7117
|
+
throw new HTTPException17(404, { message: "session not found" });
|
|
5605
7118
|
}
|
|
5606
7119
|
const result = await revokeViewer(db, {
|
|
5607
7120
|
accountId: grant.accountId,
|
|
@@ -5614,12 +7127,12 @@ function registerSessionRoutes(app, deps) {
|
|
|
5614
7127
|
});
|
|
5615
7128
|
async function channelAPreamble(c, permission) {
|
|
5616
7129
|
const workspaceId = c.req.param("workspaceId") ?? "";
|
|
5617
|
-
const grant = await
|
|
7130
|
+
const grant = await requireAccessGrant13(c, deps, workspaceId, permission);
|
|
5618
7131
|
assertOwnershipEnabled();
|
|
5619
7132
|
const sessionId = c.req.param("sessionId") ?? "";
|
|
5620
7133
|
const session = await getSession4(db, workspaceId, sessionId);
|
|
5621
7134
|
if (!session) {
|
|
5622
|
-
throw new
|
|
7135
|
+
throw new HTTPException17(404, { message: "session not found" });
|
|
5623
7136
|
}
|
|
5624
7137
|
return { accountId: grant.accountId, workspaceId, session, subjectId: grant.subjectId };
|
|
5625
7138
|
}
|
|
@@ -5627,7 +7140,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
5627
7140
|
const raw = await c.req.json().catch(() => void 0);
|
|
5628
7141
|
const result = schema.safeParse(raw ?? {});
|
|
5629
7142
|
if (!result.success) {
|
|
5630
|
-
throw new
|
|
7143
|
+
throw new HTTPException17(400, { message: "invalid request body" });
|
|
5631
7144
|
}
|
|
5632
7145
|
return result.data;
|
|
5633
7146
|
}
|
|
@@ -5722,7 +7235,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
5722
7235
|
const delta = { ptyId, stream: "stdout", chunk: opened.initialOutput, seq: 0 };
|
|
5723
7236
|
events.push({ type: "terminal.pty.output.delta", payload: delta });
|
|
5724
7237
|
}
|
|
5725
|
-
await
|
|
7238
|
+
await appendAndPublishEvents5(db, bus, ctx.workspaceId, ctx.session.id, events);
|
|
5726
7239
|
return opened.response;
|
|
5727
7240
|
});
|
|
5728
7241
|
return c.json(out, 201);
|
|
@@ -5732,10 +7245,10 @@ function registerSessionRoutes(app, deps) {
|
|
|
5732
7245
|
const req = await parseChannelABody(c, PtyWriteRequest);
|
|
5733
7246
|
const pty = await getOpenPtySession(db, ctx.workspaceId, req.ptyId);
|
|
5734
7247
|
if (!pty) {
|
|
5735
|
-
throw new
|
|
7248
|
+
throw new HTTPException17(404, { message: "pty not found or closed" });
|
|
5736
7249
|
}
|
|
5737
7250
|
if (pty.execSessionId === null) {
|
|
5738
|
-
throw new
|
|
7251
|
+
throw new HTTPException17(409, { message: "interactive terminal unsupported on this backend" });
|
|
5739
7252
|
}
|
|
5740
7253
|
let seq = 1;
|
|
5741
7254
|
await withChannelA({ db, settings, bus }, ctx, async ({ service }) => {
|
|
@@ -5743,7 +7256,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
5743
7256
|
await updatePtySessionActivity(db, { accountId: ctx.accountId, workspaceId: ctx.workspaceId, ptyId: req.ptyId, execSessionId: pty.execSessionId });
|
|
5744
7257
|
if (output) {
|
|
5745
7258
|
const delta = { ptyId: req.ptyId, stream: "stdout", chunk: output, seq: seq++ };
|
|
5746
|
-
await
|
|
7259
|
+
await appendAndPublishEvents5(db, bus, ctx.workspaceId, ctx.session.id, [{ type: "terminal.pty.output.delta", payload: delta }]);
|
|
5747
7260
|
}
|
|
5748
7261
|
});
|
|
5749
7262
|
return c.body(null, 204);
|
|
@@ -5753,7 +7266,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
5753
7266
|
const req = await parseChannelABody(c, PtyResizeRequest);
|
|
5754
7267
|
const pty = await getOpenPtySession(db, ctx.workspaceId, req.ptyId);
|
|
5755
7268
|
if (!pty) {
|
|
5756
|
-
throw new
|
|
7269
|
+
throw new HTTPException17(404, { message: "pty not found or closed" });
|
|
5757
7270
|
}
|
|
5758
7271
|
if (pty.execSessionId !== null) {
|
|
5759
7272
|
await withChannelA({ db, settings, bus }, ctx, ({ service }) => service.ptyResize(req, pty.execSessionId));
|
|
@@ -5769,7 +7282,7 @@ function registerSessionRoutes(app, deps) {
|
|
|
5769
7282
|
await withChannelA({ db, settings, bus }, ctx, ({ service }) => service.ptyClose(req, pty.execSessionId));
|
|
5770
7283
|
await closePtySession(db, { accountId: ctx.accountId, workspaceId: ctx.workspaceId, ptyId: req.ptyId });
|
|
5771
7284
|
const exited = { ptyId: req.ptyId, exitCode: 0, reason: "exit" };
|
|
5772
|
-
await
|
|
7285
|
+
await appendAndPublishEvents5(db, bus, ctx.workspaceId, ctx.session.id, [{ type: "terminal.pty.exited", payload: exited }]);
|
|
5773
7286
|
}
|
|
5774
7287
|
return c.body(null, 204);
|
|
5775
7288
|
});
|
|
@@ -5823,19 +7336,19 @@ import {
|
|
|
5823
7336
|
listSocialConnections as listSocialConnections3,
|
|
5824
7337
|
listSocialPosts as listSocialPosts2
|
|
5825
7338
|
} from "@opengeni/db";
|
|
5826
|
-
import { HTTPException as
|
|
7339
|
+
import { HTTPException as HTTPException18 } from "hono/http-exception";
|
|
5827
7340
|
import { z as z2 } from "zod";
|
|
5828
|
-
import { requireAccessGrant as
|
|
7341
|
+
import { requireAccessGrant as requireAccessGrant14 } from "@opengeni/core";
|
|
5829
7342
|
function registerSocialRoutes(app, deps) {
|
|
5830
7343
|
const { db } = deps;
|
|
5831
7344
|
app.get("/v1/workspaces/:workspaceId/social/connections", async (c) => {
|
|
5832
7345
|
const workspaceId = c.req.param("workspaceId");
|
|
5833
|
-
await
|
|
7346
|
+
await requireAccessGrant14(c, deps, workspaceId, "workspace:read");
|
|
5834
7347
|
return c.json(await listSocialConnections3(db, workspaceId, boundedLimit(c.req.query("limit"))));
|
|
5835
7348
|
});
|
|
5836
7349
|
app.post("/v1/workspaces/:workspaceId/social/connections", async (c) => {
|
|
5837
7350
|
const workspaceId = c.req.param("workspaceId");
|
|
5838
|
-
const grant = await
|
|
7351
|
+
const grant = await requireAccessGrant14(c, deps, workspaceId, "workspace:admin");
|
|
5839
7352
|
const payload = CreateSocialConnectionRequest.parse(await c.req.json());
|
|
5840
7353
|
try {
|
|
5841
7354
|
return c.json(await createSocialConnection(db, {
|
|
@@ -5857,7 +7370,7 @@ function registerSocialRoutes(app, deps) {
|
|
|
5857
7370
|
});
|
|
5858
7371
|
app.get("/v1/workspaces/:workspaceId/social/posts", async (c) => {
|
|
5859
7372
|
const workspaceId = c.req.param("workspaceId");
|
|
5860
|
-
await
|
|
7373
|
+
await requireAccessGrant14(c, deps, workspaceId, "workspace:read");
|
|
5861
7374
|
const since = parseSince(c.req.query("since"));
|
|
5862
7375
|
const connectionIds = parseConnectionIds(c.req.query("connectionIds") ?? c.req.query("connectionId"));
|
|
5863
7376
|
return c.json(await listSocialPosts2(db, {
|
|
@@ -5869,7 +7382,7 @@ function registerSocialRoutes(app, deps) {
|
|
|
5869
7382
|
});
|
|
5870
7383
|
app.post("/v1/workspaces/:workspaceId/social/posts", async (c) => {
|
|
5871
7384
|
const workspaceId = c.req.param("workspaceId");
|
|
5872
|
-
const grant = await
|
|
7385
|
+
const grant = await requireAccessGrant14(c, deps, workspaceId, "workspace:admin");
|
|
5873
7386
|
const payload = CreateSocialPostRequest.parse(await c.req.json());
|
|
5874
7387
|
try {
|
|
5875
7388
|
return c.json(await createSocialPost(db, {
|
|
@@ -5895,7 +7408,7 @@ function parseSince(raw) {
|
|
|
5895
7408
|
}
|
|
5896
7409
|
const since = new Date(raw);
|
|
5897
7410
|
if (Number.isNaN(since.getTime())) {
|
|
5898
|
-
throw new
|
|
7411
|
+
throw new HTTPException18(422, { message: "since must be an ISO date-time" });
|
|
5899
7412
|
}
|
|
5900
7413
|
return since;
|
|
5901
7414
|
}
|
|
@@ -5906,7 +7419,7 @@ function parseConnectionIds(raw) {
|
|
|
5906
7419
|
const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
5907
7420
|
const parsed = z2.array(z2.string().uuid()).safeParse(values);
|
|
5908
7421
|
if (!parsed.success) {
|
|
5909
|
-
throw new
|
|
7422
|
+
throw new HTTPException18(422, { message: "connectionIds must be a comma-separated list of UUIDs" });
|
|
5910
7423
|
}
|
|
5911
7424
|
const ids = parsed.data;
|
|
5912
7425
|
return [...new Set(ids)];
|
|
@@ -5914,12 +7427,12 @@ function parseConnectionIds(raw) {
|
|
|
5914
7427
|
function socialHttpException(error) {
|
|
5915
7428
|
const message = error instanceof Error ? error.message : String(error);
|
|
5916
7429
|
if (message.includes("not found")) {
|
|
5917
|
-
return new
|
|
7430
|
+
return new HTTPException18(404, { message });
|
|
5918
7431
|
}
|
|
5919
7432
|
if (message.includes("duplicate key")) {
|
|
5920
|
-
return new
|
|
7433
|
+
return new HTTPException18(409, { message: "social connection or post already exists" });
|
|
5921
7434
|
}
|
|
5922
|
-
return new
|
|
7435
|
+
return new HTTPException18(500, { message });
|
|
5923
7436
|
}
|
|
5924
7437
|
|
|
5925
7438
|
// src/routes/workspaces.ts
|
|
@@ -5947,8 +7460,8 @@ import {
|
|
|
5947
7460
|
requireWorkspace,
|
|
5948
7461
|
updateWorkspace
|
|
5949
7462
|
} from "@opengeni/db";
|
|
5950
|
-
import { HTTPException as
|
|
5951
|
-
import { hasPermission as
|
|
7463
|
+
import { HTTPException as HTTPException19 } from "hono/http-exception";
|
|
7464
|
+
import { hasPermission as hasPermission3, requireAccessContext as requireAccessContext2, requireAccessGrant as requireAccessGrant15 } from "@opengeni/core";
|
|
5952
7465
|
import { requireLimit as requireLimit7 } from "@opengeni/core";
|
|
5953
7466
|
import { assertWorkspaceDeletable, assertWorkspaceMemberRemovable, resolveMemberSubjectId } from "@opengeni/core";
|
|
5954
7467
|
function registerWorkspaceRoutes(app, deps) {
|
|
@@ -5957,7 +7470,7 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
5957
7470
|
});
|
|
5958
7471
|
app.get("/v1/workspaces", async (c) => {
|
|
5959
7472
|
const context = await requireAccessContext2(c, deps);
|
|
5960
|
-
const readableWorkspaceIds = [...new Set(context.workspaceGrants.filter((grant) =>
|
|
7473
|
+
const readableWorkspaceIds = [...new Set(context.workspaceGrants.filter((grant) => hasPermission3(grant.permissions, "workspace:read")).map((grant) => grant.workspaceId))];
|
|
5961
7474
|
if (readableWorkspaceIds.length > 0) {
|
|
5962
7475
|
const workspaces = await Promise.all(readableWorkspaceIds.map((workspaceId) => requireWorkspace(deps.db, workspaceId)));
|
|
5963
7476
|
return c.json(workspaces.map((workspace) => Workspace.parse(workspace)));
|
|
@@ -5969,7 +7482,7 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
5969
7482
|
const payload = CreateWorkspaceRequest.parse(await c.req.json());
|
|
5970
7483
|
const accountId = payload.accountId ?? context.defaultAccountId;
|
|
5971
7484
|
if (!accountId) {
|
|
5972
|
-
throw new
|
|
7485
|
+
throw new HTTPException19(409, { message: "account selection is required" });
|
|
5973
7486
|
}
|
|
5974
7487
|
requireAccountPermission(context, accountId, "workspace:create");
|
|
5975
7488
|
await requireLimit7(deps, { accountId, action: "workspace:create", quantity: 1 });
|
|
@@ -5993,12 +7506,12 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
5993
7506
|
});
|
|
5994
7507
|
app.get("/v1/workspaces/:workspaceId", async (c) => {
|
|
5995
7508
|
const workspaceId = c.req.param("workspaceId");
|
|
5996
|
-
await
|
|
7509
|
+
await requireAccessGrant15(c, deps, workspaceId, "workspace:read");
|
|
5997
7510
|
return c.json(Workspace.parse(await requireWorkspace(deps.db, workspaceId)));
|
|
5998
7511
|
});
|
|
5999
7512
|
app.patch("/v1/workspaces/:workspaceId", async (c) => {
|
|
6000
7513
|
const workspaceId = c.req.param("workspaceId");
|
|
6001
|
-
await
|
|
7514
|
+
await requireAccessGrant15(c, deps, workspaceId, "workspace:admin");
|
|
6002
7515
|
const payload = UpdateWorkspaceRequest.parse(await c.req.json());
|
|
6003
7516
|
const workspace = await updateWorkspace(deps.db, workspaceId, {
|
|
6004
7517
|
...payload.name !== void 0 ? { name: payload.name.trim() } : {},
|
|
@@ -6009,7 +7522,7 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
6009
7522
|
});
|
|
6010
7523
|
app.delete("/v1/workspaces/:workspaceId", async (c) => {
|
|
6011
7524
|
const workspaceId = c.req.param("workspaceId");
|
|
6012
|
-
const grant = await
|
|
7525
|
+
const grant = await requireAccessGrant15(c, deps, workspaceId, "workspace:admin");
|
|
6013
7526
|
const [workspaceCountForAccount, activeSessionCount] = await Promise.all([
|
|
6014
7527
|
countWorkspacesForAccount(deps.db, grant.accountId),
|
|
6015
7528
|
countActiveSessionsForWorkspace(deps.db, workspaceId)
|
|
@@ -6024,13 +7537,13 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
6024
7537
|
});
|
|
6025
7538
|
app.get("/v1/workspaces/:workspaceId/members", async (c) => {
|
|
6026
7539
|
const workspaceId = c.req.param("workspaceId");
|
|
6027
|
-
await
|
|
7540
|
+
await requireAccessGrant15(c, deps, workspaceId, "workspace:read");
|
|
6028
7541
|
const members = await listWorkspaceMembers(deps.db, workspaceId);
|
|
6029
7542
|
return c.json(ListWorkspaceMembersResponse.parse({ members }));
|
|
6030
7543
|
});
|
|
6031
7544
|
app.post("/v1/workspaces/:workspaceId/members", async (c) => {
|
|
6032
7545
|
const workspaceId = c.req.param("workspaceId");
|
|
6033
|
-
const grant = await
|
|
7546
|
+
const grant = await requireAccessGrant15(c, deps, workspaceId, "members:manage");
|
|
6034
7547
|
const payload = AddWorkspaceMemberRequest.parse(await c.req.json());
|
|
6035
7548
|
const email = payload.email.trim();
|
|
6036
7549
|
const subjectId = resolveMemberSubjectId(await getManagedUserByEmail(deps.db, email));
|
|
@@ -6045,19 +7558,19 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
6045
7558
|
const members = await listWorkspaceMembers(deps.db, workspaceId);
|
|
6046
7559
|
const member = members.find((candidate) => candidate.subjectId === subjectId);
|
|
6047
7560
|
if (!member) {
|
|
6048
|
-
throw new
|
|
7561
|
+
throw new HTTPException19(500, { message: "failed to add member" });
|
|
6049
7562
|
}
|
|
6050
7563
|
return c.json(WorkspaceMember.parse(member), 201);
|
|
6051
7564
|
});
|
|
6052
7565
|
app.patch("/v1/workspaces/:workspaceId/members/:subjectId", async (c) => {
|
|
6053
7566
|
const workspaceId = c.req.param("workspaceId");
|
|
6054
|
-
const grant = await
|
|
7567
|
+
const grant = await requireAccessGrant15(c, deps, workspaceId, "members:manage");
|
|
6055
7568
|
const subjectId = decodeURIComponent(c.req.param("subjectId"));
|
|
6056
7569
|
const payload = UpdateWorkspaceMemberRequest.parse(await c.req.json());
|
|
6057
7570
|
const existing = await listWorkspaceMembers(deps.db, workspaceId);
|
|
6058
7571
|
const current = existing.find((member2) => member2.subjectId === subjectId);
|
|
6059
7572
|
if (!current) {
|
|
6060
|
-
throw new
|
|
7573
|
+
throw new HTTPException19(404, { message: "member not found" });
|
|
6061
7574
|
}
|
|
6062
7575
|
await grantWorkspaceAccess(deps.db, {
|
|
6063
7576
|
accountId: grant.accountId,
|
|
@@ -6070,13 +7583,13 @@ function registerWorkspaceRoutes(app, deps) {
|
|
|
6070
7583
|
const members = await listWorkspaceMembers(deps.db, workspaceId);
|
|
6071
7584
|
const member = members.find((candidate) => candidate.subjectId === subjectId);
|
|
6072
7585
|
if (!member) {
|
|
6073
|
-
throw new
|
|
7586
|
+
throw new HTTPException19(500, { message: "failed to update member" });
|
|
6074
7587
|
}
|
|
6075
7588
|
return c.json(WorkspaceMember.parse(member));
|
|
6076
7589
|
});
|
|
6077
7590
|
app.delete("/v1/workspaces/:workspaceId/members/:subjectId", async (c) => {
|
|
6078
7591
|
const workspaceId = c.req.param("workspaceId");
|
|
6079
|
-
const grant = await
|
|
7592
|
+
const grant = await requireAccessGrant15(c, deps, workspaceId, "members:manage");
|
|
6080
7593
|
const subjectId = decodeURIComponent(c.req.param("subjectId"));
|
|
6081
7594
|
const members = await listWorkspaceMembers(deps.db, workspaceId);
|
|
6082
7595
|
assertWorkspaceMemberRemovable({ members, subjectId, callerSubjectId: grant.subjectId });
|
|
@@ -6094,7 +7607,7 @@ function normalizeAgentInstructions(value) {
|
|
|
6094
7607
|
function requireAccountPermission(context, accountId, permission) {
|
|
6095
7608
|
const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
|
|
6096
7609
|
if (!grant || !grant.permissions.includes(permission) && !grant.permissions.includes("account:admin")) {
|
|
6097
|
-
throw new
|
|
7610
|
+
throw new HTTPException19(403, { message: `missing permission: ${permission}` });
|
|
6098
7611
|
}
|
|
6099
7612
|
}
|
|
6100
7613
|
|
|
@@ -6121,7 +7634,7 @@ function createApp(deps) {
|
|
|
6121
7634
|
const documentIndexer = deps.documentIndexer ?? {
|
|
6122
7635
|
indexDocument: async ({ accountId, workspaceId, documentId }) => {
|
|
6123
7636
|
if (!objectStorage) {
|
|
6124
|
-
throw new
|
|
7637
|
+
throw new HTTPException20(503, { message: "object storage is not configured" });
|
|
6125
7638
|
}
|
|
6126
7639
|
return await indexDocumentNow(deps.db, objectStorage, workspaceId, documentId, getDocumentServices(), {
|
|
6127
7640
|
beforeEmbed: async ({ chunkCount }) => {
|
|
@@ -6258,11 +7771,19 @@ function createApp(deps) {
|
|
|
6258
7771
|
})));
|
|
6259
7772
|
app.all("/v1/workspaces/:workspaceId/mcp", async (c) => {
|
|
6260
7773
|
const workspaceId = c.req.param("workspaceId");
|
|
6261
|
-
const grant = await
|
|
7774
|
+
const grant = await requireMcpAccessGrant(c, routeDeps, workspaceId);
|
|
7775
|
+
const toolspace = isToolspaceGrant(routeDeps.settings, grant) ? await prepareToolspaceMcpSurface({ deps: routeDeps, grant }) : null;
|
|
6262
7776
|
const transport = new WebStandardStreamableHTTPServerTransport2({ enableJsonResponse: true });
|
|
6263
|
-
const mcp = buildOpenGeniMcpServer(routeDeps, grant, {
|
|
6264
|
-
|
|
6265
|
-
|
|
7777
|
+
const mcp = buildOpenGeniMcpServer(routeDeps, grant, {
|
|
7778
|
+
requestOrigin: new URL(c.req.url).origin,
|
|
7779
|
+
toolspace
|
|
7780
|
+
});
|
|
7781
|
+
try {
|
|
7782
|
+
await mcp.connect(transport);
|
|
7783
|
+
return await transport.handleRequest(c.req.raw);
|
|
7784
|
+
} finally {
|
|
7785
|
+
await toolspace?.close().catch(() => void 0);
|
|
7786
|
+
}
|
|
6266
7787
|
});
|
|
6267
7788
|
registerFileRoutes(app, routeDeps);
|
|
6268
7789
|
registerApiKeyRoutes(app, routeDeps);
|
|
@@ -6272,6 +7793,7 @@ function createApp(deps) {
|
|
|
6272
7793
|
registerInstallRoutes(app, routeDeps);
|
|
6273
7794
|
registerWorkspaceRoutes(app, routeDeps);
|
|
6274
7795
|
registerSocialRoutes(app, routeDeps);
|
|
7796
|
+
registerConnectionRoutes(app, routeDeps);
|
|
6275
7797
|
registerCapabilityRoutes(app, routeDeps);
|
|
6276
7798
|
registerEnrollmentRoutes(app, routeDeps);
|
|
6277
7799
|
registerMachineRoutes(app, routeDeps);
|
|
@@ -6282,6 +7804,17 @@ function createApp(deps) {
|
|
|
6282
7804
|
registerCodexRoutes(app, routeDeps);
|
|
6283
7805
|
return app;
|
|
6284
7806
|
}
|
|
7807
|
+
async function requireMcpAccessGrant(c, deps, workspaceId) {
|
|
7808
|
+
const grant = await requireAccessGrant16(c, deps, workspaceId);
|
|
7809
|
+
if (hasPermission4(grant.permissions, "workspace:read")) {
|
|
7810
|
+
return grant;
|
|
7811
|
+
}
|
|
7812
|
+
if (isToolspaceGrant(deps.settings, grant)) {
|
|
7813
|
+
return grant;
|
|
7814
|
+
}
|
|
7815
|
+
requirePermission(grant, "workspace:read");
|
|
7816
|
+
return grant;
|
|
7817
|
+
}
|
|
6285
7818
|
function clientAuthConfig(settings) {
|
|
6286
7819
|
if (settings.productAccessMode === "managed") {
|
|
6287
7820
|
return { mode: "managedSession", session: "cookie" };
|
|
@@ -6302,7 +7835,7 @@ function allowedCorsOrigin(pattern, origin) {
|
|
|
6302
7835
|
return new RegExp(`^(?:${pattern})$`).test(origin);
|
|
6303
7836
|
}
|
|
6304
7837
|
function httpStatusForError(error) {
|
|
6305
|
-
if (error instanceof
|
|
7838
|
+
if (error instanceof HTTPException20) {
|
|
6306
7839
|
return error.status;
|
|
6307
7840
|
}
|
|
6308
7841
|
return 500;
|
|
@@ -6401,6 +7934,9 @@ var routeLabelPatterns = [
|
|
|
6401
7934
|
{ pattern: /^\/v1\/workspaces\/[^/]+\/document-bases\/[^/]+\/documents$/, label: "/v1/workspaces/:workspaceId/document-bases/:id/documents" },
|
|
6402
7935
|
{ pattern: /^\/v1\/workspaces\/[^/]+\/document-bases\/[^/]+\/search$/, label: "/v1/workspaces/:workspaceId/document-bases/:id/search" },
|
|
6403
7936
|
{ pattern: /^\/v1\/workspaces\/[^/]+\/document-bases\/[^/]+$/, label: "/v1/workspaces/:workspaceId/document-bases/:id" },
|
|
7937
|
+
{ pattern: /^\/v1\/workspaces\/[^/]+\/knowledge\/search$/, label: "/v1/workspaces/:workspaceId/knowledge/search" },
|
|
7938
|
+
{ pattern: /^\/v1\/workspaces\/[^/]+\/knowledge\/memories\/[^/]+$/, label: "/v1/workspaces/:workspaceId/knowledge/memories/:id" },
|
|
7939
|
+
{ pattern: /^\/v1\/workspaces\/[^/]+\/knowledge\/memories$/, label: "/v1/workspaces/:workspaceId/knowledge/memories" },
|
|
6404
7940
|
{ pattern: /^\/v1\/workspaces\/[^/]+\/github\/app$/, label: "/v1/workspaces/:workspaceId/github/app" },
|
|
6405
7941
|
{ pattern: /^\/v1\/workspaces\/[^/]+\/github\/repositories$/, label: "/v1/workspaces/:workspaceId/github/repositories" },
|
|
6406
7942
|
{ pattern: /^\/v1\/workspaces\/[^/]+\/github\/repositories\/sync$/, label: "/v1/workspaces/:workspaceId/github/repositories/sync" },
|
|
@@ -6419,6 +7955,11 @@ var routeLabelPatterns = [
|
|
|
6419
7955
|
{ pattern: /^\/v1\/workspaces\/[^/]+\/packs\/[^/]+$/, label: "/v1/workspaces/:workspaceId/packs/:id" },
|
|
6420
7956
|
{ pattern: /^\/v1\/workspaces\/[^/]+\/social\/connections$/, label: "/v1/workspaces/:workspaceId/social/connections" },
|
|
6421
7957
|
{ pattern: /^\/v1\/workspaces\/[^/]+\/social\/posts$/, label: "/v1/workspaces/:workspaceId/social/posts" },
|
|
7958
|
+
{ pattern: /^\/v1\/workspaces\/[^/]+\/connections$/, label: "/v1/workspaces/:workspaceId/connections" },
|
|
7959
|
+
{ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/oauth\/start$/, label: "/v1/workspaces/:workspaceId/connections/oauth/start" },
|
|
7960
|
+
{ pattern: /^\/v1\/workspaces\/[^/]+\/connections\/[^/]+$/, label: "/v1/workspaces/:workspaceId/connections/:connectionId" },
|
|
7961
|
+
{ pattern: /^\/v1\/integrations\/oauth\/callback$/, label: "/v1/integrations/oauth/callback" },
|
|
7962
|
+
{ pattern: /^\/v1\/integrations\/oauth\/client-metadata\.json$/, label: "/v1/integrations/oauth/client-metadata.json" },
|
|
6422
7963
|
{ pattern: /^\/v1\/enrollments\/device\/start$/, label: "/v1/enrollments/device/start" },
|
|
6423
7964
|
{ pattern: /^\/v1\/enrollments\/device\/poll$/, label: "/v1/enrollments/device/poll" },
|
|
6424
7965
|
{ pattern: /^\/v1\/workspaces\/[^/]+\/enrollments\/device\/approve$/, label: "/v1/workspaces/:workspaceId/enrollments/device/approve" },
|
|
@@ -6456,4 +7997,4 @@ export {
|
|
|
6456
7997
|
withDefaultEnabledCapabilityMcpTools,
|
|
6457
7998
|
workflowIdForSession3 as workflowIdForSession
|
|
6458
7999
|
};
|
|
6459
|
-
//# sourceMappingURL=chunk-
|
|
8000
|
+
//# sourceMappingURL=chunk-DQ5TIRDZ.js.map
|