@opengeni/api-router 0.4.1 → 0.5.1

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.
@@ -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 HTTPException18 } from "hono/http-exception";
16
- import { requireAccessGrant as requireAccessGrant15 } from "@opengeni/core";
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
- server.registerTool("files_get_download_url", {
522
- description: "Create a short-lived download URL for a ready file asset.",
523
- inputSchema: { fileId: z4.string().uuid() }
524
- }, async ({ fileId }) => {
525
- if (!deps.objectStorage) {
526
- throw new Error("object storage is not configured");
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
- server.registerTool("github_repositories_list", {
552
- description: "List GitHub App repositories available as scheduled task repository resources. Use the returned resource object in scheduled task agentConfig.resources.",
553
- inputSchema: { limit: z4.number().int().positive().optional() }
554
- }, async ({ limit }) => {
555
- try {
556
- const installationIds = await listGitHubInstallationIdsForWorkspace(deps.db, grant.workspaceId);
557
- const repositories = await listGitHubAppRepositories(deps.settings, { installationIds });
558
- const visible = typeof limit === "number" ? repositories.slice(0, limit) : repositories;
559
- return json({ repositories: visible.map((repository) => repositoryWithScheduledTaskResource(repository)) });
560
- } catch (error) {
561
- if (error instanceof GitHubAppConfigurationError) {
562
- throw new Error(`GitHub App is not configured: ${error.missing.join(", ")}`);
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
- throw error;
565
- }
566
- });
567
- server.registerTool("social_connections_list", {
568
- description: "List connected social media accounts available to social media analysis packs.",
569
- inputSchema: { limit: z4.number().int().positive().optional() }
570
- }, async ({ limit }) => json({ connections: await listSocialConnections(deps.db, grant.workspaceId, boundedMcpLimit(limit)) }));
571
- server.registerTool("social_posts_recent", {
572
- description: "List recent social media posts imported or synced into OpenGeni.",
573
- inputSchema: {
574
- connectionIds: z4.array(z4.string().uuid()).optional(),
575
- since: z4.string().optional(),
576
- windowHours: z4.number().int().positive().optional(),
577
- limit: z4.number().int().positive().optional()
578
- }
579
- }, async ({ connectionIds, since, windowHours, limit }) => {
580
- const sinceDate = since ? parseMcpDate(since, "since") : new Date(Date.now() - (windowHours ?? 24) * 60 * 60 * 1e3);
581
- return json({
582
- since: sinceDate.toISOString(),
583
- posts: await listSocialPosts(deps.db, {
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
- ...connectionIds?.length ? { connectionIds } : {},
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
- server.registerTool("social_daily_analysis_context", {
592
- description: "Collect social account and recent post context for a daily marketing analysis run.",
593
- inputSchema: {
594
- connectionIds: z4.array(z4.string().uuid()).optional(),
595
- documentBaseIds: z4.array(z4.string().uuid()).optional(),
596
- since: z4.string().optional(),
597
- windowHours: z4.number().int().positive().optional(),
598
- limit: z4.number().int().positive().optional()
599
- }
600
- }, async ({ connectionIds, documentBaseIds, since, windowHours, limit }) => {
601
- const allConnections = await listSocialConnections(deps.db, grant.workspaceId, 500);
602
- const selectedIds = connectionIds && connectionIds.length > 0 ? new Set(connectionIds) : null;
603
- const connections = selectedIds ? allConnections.filter((connection) => selectedIds.has(connection.id)) : allConnections.filter((connection) => connection.status === "connected");
604
- if (selectedIds) {
605
- const foundIds = new Set(connections.map((connection) => connection.id));
606
- const missing = [...selectedIds].filter((id) => !foundIds.has(id));
607
- if (missing.length > 0) {
608
- throw new Error(`Unknown social connection IDs: ${missing.join(", ")}`);
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
- const sinceDate = since ? parseMcpDate(since, "since") : new Date(Date.now() - (windowHours ?? 24) * 60 * 60 * 1e3);
612
- const posts = connections.length > 0 ? await listSocialPosts(deps.db, {
613
- workspaceId: grant.workspaceId,
614
- connectionIds: connections.map((connection) => connection.id),
615
- since: sinceDate,
616
- limit: boundedMcpLimit(limit)
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
- server.registerTool("scheduled_tasks_list", {
635
- description: "List scheduled tasks.",
636
- inputSchema: { limit: z4.number().int().positive().optional() }
637
- }, async ({ limit }) => json({ tasks: await listScheduledTasks(deps.db, grant.workspaceId, limit ?? 100) }));
638
- server.registerTool("scheduled_tasks_get", {
639
- description: "Get one scheduled task.",
640
- inputSchema: { id: z4.string().uuid() }
641
- }, async ({ id }) => json(await requireScheduledTask(deps.db, grant.workspaceId, id)));
642
- server.registerTool("scheduled_tasks_create", {
643
- description: "Create a scheduled task.",
644
- inputSchema: {
645
- name: z4.string(),
646
- schedule: z4.unknown(),
647
- runMode: z4.string().optional(),
648
- overlapPolicy: z4.string().optional(),
649
- agentConfig: z4.unknown(),
650
- status: z4.string().optional(),
651
- environmentId: z4.string().uuid().optional(),
652
- metadata: z4.record(z4.string(), z4.unknown()).optional()
653
- }
654
- }, async (args) => {
655
- const payload = CreateScheduledTaskRequest.parse(args);
656
- requireEnvironmentsUseForMcpAttachment(grant, payload.environmentId);
657
- await requireLimit(deps, { accountId: grant.accountId, workspaceId: grant.workspaceId, action: "schedule:create", quantity: 1 });
658
- const task = await createValidatedScheduledTask({ settings: deps.settings, db: deps.db, objectStorage: deps.objectStorage, grant, payload, toolsProvided: scheduledTaskToolsProvided(args) });
659
- await syncCreatedScheduledTask({ db: deps.db, workflowClient: deps.workflowClient, task });
660
- return json(task);
661
- });
662
- server.registerTool("scheduled_tasks_update", {
663
- description: "Update a scheduled task.",
664
- inputSchema: {
665
- id: z4.string().uuid(),
666
- name: z4.string().optional(),
667
- schedule: z4.unknown().optional(),
668
- runMode: z4.string().optional(),
669
- overlapPolicy: z4.string().optional(),
670
- agentConfig: z4.unknown().optional(),
671
- status: z4.string().optional(),
672
- environmentId: z4.string().uuid().nullable().optional(),
673
- metadata: z4.record(z4.string(), z4.unknown()).optional()
674
- }
675
- }, async ({ id, ...raw }) => {
676
- const existing = await requireScheduledTask(deps.db, grant.workspaceId, id);
677
- const payload = UpdateScheduledTaskRequest.parse(raw);
678
- requireEnvironmentsUseForMcpAttachment(grant, payload.environmentId);
679
- const update = await validatedScheduledTaskUpdate({ settings: deps.settings, db: deps.db, objectStorage: deps.objectStorage, grant, existing, payload, toolsProvided: scheduledTaskToolsProvided(raw) });
680
- const task = await updateScheduledTask(deps.db, grant.workspaceId, id, update);
681
- await syncUpdatedScheduledTask({ db: deps.db, workflowClient: deps.workflowClient, previous: existing, task });
682
- return json(task);
683
- });
684
- server.registerTool("scheduled_tasks_pause", {
685
- description: "Pause a scheduled task.",
686
- inputSchema: { id: z4.string().uuid() }
687
- }, async ({ id }) => {
688
- const existing = await requireScheduledTask(deps.db, grant.workspaceId, id);
689
- const task = await updateScheduledTask(deps.db, grant.workspaceId, id, { status: "paused" });
690
- await syncUpdatedScheduledTask({ db: deps.db, workflowClient: deps.workflowClient, previous: existing, task });
691
- return json(task);
692
- });
693
- server.registerTool("scheduled_tasks_resume", {
694
- description: "Resume a scheduled task.",
695
- inputSchema: { id: z4.string().uuid() }
696
- }, async ({ id }) => {
697
- const existing = await requireScheduledTask(deps.db, grant.workspaceId, id);
698
- const task = await updateScheduledTask(deps.db, grant.workspaceId, id, { status: "active" });
699
- await syncUpdatedScheduledTask({ db: deps.db, workflowClient: deps.workflowClient, previous: existing, task });
700
- return json(task);
701
- });
702
- server.registerTool("scheduled_tasks_trigger", {
703
- description: "Trigger a scheduled task immediately. Pass a stable triggerId to make a retried trigger idempotent (one charge, one run).",
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
- return json(task);
724
- });
725
- server.registerTool("scheduled_tasks_delete", {
726
- description: "Delete a scheduled task.",
727
- inputSchema: { id: z4.string().uuid() }
728
- }, async ({ id }) => {
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 = environmentsEncryptionKeyBytes(settings);
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 requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
1700
- const accountId = c.req.param("accountId");
1701
- const activated = await setActiveCodexCredential(db, workspaceId, accountId);
1702
- if (!activated) {
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({ activated: true, accountId });
3063
+ return c.json(ConnectionResponse.parse({ connection }));
1706
3064
  });
1707
- app.patch("/v1/workspaces/:workspaceId/codex/settings", async (c) => {
3065
+ app.patch("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
1708
3066
  const workspaceId = c.req.param("workspaceId");
1709
- const grant = await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
1710
- const body = await c.req.json().catch(() => ({}));
1711
- const patch = {};
1712
- if (typeof body.rotationEnabled === "boolean") {
1713
- patch.rotationEnabled = body.rotationEnabled;
1714
- }
1715
- if (typeof body.rotationStrategy === "string") {
1716
- if (!CODEX_ROTATION_STRATEGIES.includes(body.rotationStrategy)) {
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
- return c.json({
1730
- rotationEnabled: updated.rotationEnabled,
1731
- rotationStrategy: updated.rotationStrategy,
1732
- activeCredentialId: updated.activeCredentialId
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
- app.patch("/v1/workspaces/:workspaceId/codex/accounts/:accountId", async (c) => {
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(codexAccountJson(row));
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.get("/v1/workspaces/:workspaceId/codex/usage", async (c) => {
3098
+ app.delete("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
1766
3099
  const workspaceId = c.req.param("workspaceId");
1767
- await requireAccessGrant2(c, deps, workspaceId, "workspace:read");
1768
- const status = await getCodexCredentialStatus(db, workspaceId);
1769
- if (!status?.credentialId) {
1770
- throw new HTTPException3(404, { message: "codex subscription is not connected" });
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
- const payload = await fetchCodexUsageForAccount(db, settings, workspaceId, status.credentialId);
1773
- return c.json(codexUsageJson(payload));
3105
+ return c.json(ConnectionResponse.parse({ connection }));
1774
3106
  });
1775
- app.get("/v1/workspaces/:workspaceId/codex/accounts/:accountId/usage", async (c) => {
3107
+ app.post("/v1/workspaces/:workspaceId/connections/oauth/start", async (c) => {
3108
+ assertIntegrationsEnabled();
1776
3109
  const workspaceId = c.req.param("workspaceId");
1777
- await requireAccessGrant2(c, deps, workspaceId, "workspace:read");
1778
- const accountId = c.req.param("accountId");
1779
- const accounts = await listCodexAccountStatuses(db, workspaceId);
1780
- if (!accounts.some((account) => account.id === accountId)) {
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 = await fetchCodexUsageForAccount(db, settings, workspaceId, accountId);
1784
- return c.json(codexUsageJson(payload));
1785
- });
1786
- app.post("/v1/workspaces/:workspaceId/codex/usage/refresh", async (c) => {
1787
- const workspaceId = c.req.param("workspaceId");
1788
- await requireAccessGrant2(c, deps, workspaceId, "workspace:read");
1789
- const accounts = await listCodexAccountStatuses(db, workspaceId);
1790
- const usage = {};
1791
- const queue = [...accounts];
1792
- const CONCURRENCY = 4;
1793
- const worker = async () => {
1794
- for (; ; ) {
1795
- const account = queue.shift();
1796
- if (!account) return;
1797
- const settled = await Promise.allSettled([fetchCodexUsageForAccount(db, settings, workspaceId, account.id)]);
1798
- const result = settled[0];
1799
- 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() } };
1800
- }
1801
- };
1802
- await Promise.all(Array.from({ length: Math.min(CONCURRENCY, Math.max(1, accounts.length)) }, () => worker()));
1803
- return c.json({ usage });
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 HTTPException4 } from "hono/http-exception";
1828
- import { requireAccessGrant as requireAccessGrant3 } from "@opengeni/core";
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
- function buildDocumentsMcpServer(db, workspaceId, documentServices) {
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
- query: z.string(),
1854
- baseIds: z.array(z.string()).optional(),
1855
- limit: z.number().optional()
1856
- }
1857
- }, async ({ query, baseIds, limit }) => ({
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 requireAccessGrant3(c, deps, workspaceId, "documents:manage");
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 requireAccessGrant3(c, deps, workspaceId, "documents:search");
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 requireAccessGrant3(c, deps, workspaceId, "documents:search");
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 HTTPException4(404, { message: "document base not found" });
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 requireAccessGrant3(c, deps, workspaceId, "documents:manage");
3350
+ const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:manage");
1909
3351
  if (!objectStorage) {
1910
- throw new HTTPException4(503, { message: "object storage is not configured" });
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"), fileId: payload.fileId });
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 requireAccessGrant3(c, deps, workspaceId, "documents:search");
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 requireAccessGrant3(c, deps, workspaceId, "documents:manage");
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 requireAccessGrant3(c, deps, workspaceId, "documents:manage");
3400
+ const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:manage");
1959
3401
  if (!objectStorage) {
1960
- throw new HTTPException4(503, { message: "object storage is not configured" });
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 HTTPException4(404, { message: "document not found" });
3408
+ throw new HTTPException6(404, { message: "document not found" });
1967
3409
  }
1968
3410
  if (document.status !== "failed") {
1969
- throw new HTTPException4(422, { message: "only failed documents can be retried" });
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 HTTPException4(404, { message: "document not found" });
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 HTTPException4) {
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 requireAccessGrant3(c, deps, workspaceId, "documents:search");
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 HTTPException4(404, { message: "document base not found" });
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 requireAccessGrant3(c, deps, workspaceId, "documents:search");
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 HTTPException4(404, { message });
3536
+ return new HTTPException6(404, { message });
2027
3537
  }
2028
3538
  if (message.includes("pending") || message.includes("failed") || message.includes("deleted")) {
2029
- return new HTTPException4(422, { message });
3539
+ return new HTTPException6(422, { message });
2030
3540
  }
2031
- return new HTTPException4(500, { message });
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 HTTPException5 } from "hono/http-exception";
2058
- import { requireAccessGrant as requireAccessGrant4 } from "@opengeni/core";
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 randomBytes(32).toString("base64url");
3601
+ return randomBytes2(32).toString("base64url");
2092
3602
  }
2093
3603
  var USER_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
2094
3604
  function mintUserCode() {
2095
- const bytes = randomBytes(8);
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(record2) {
3683
+ function toLookupResponse(record3) {
2174
3684
  return {
2175
- workspaceId: record2.workspaceId,
2176
- userCode: record2.userCode,
3685
+ workspaceId: record3.workspaceId,
3686
+ userCode: record3.userCode,
2177
3687
  machine: {
2178
- machineName: record2.machineName,
2179
- os: record2.os,
2180
- arch: record2.arch,
2181
- canOfferDisplay: record2.canOfferDisplay,
2182
- requestsScreenControl: record2.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: record2.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 HTTPException5(404, { message: "selfhosted enrollment is not enabled for this deployment" });
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 HTTPException5(429, { message: "too many requests; slow down" });
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 HTTPException5(400, { message: "invalid device-start request" });
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 HTTPException5(404, { message: "workspace not found" });
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 HTTPException5(400, { message: "invalid device-poll request" });
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 HTTPException5(400, { message: "invalid device-lookup request" });
3905
+ throw new HTTPException7(400, { message: "invalid device-lookup request" });
2396
3906
  }
2397
- const record2 = await lookupDeviceEnrollment({ db, settings }, { userCode: parsed.data.userCode });
2398
- if (!record2) {
2399
- throw new HTTPException5(404, { message: "no pending enrollment for that code" });
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 requireAccessGrant4(c, deps, record2.workspaceId, "enrollments:read");
3912
+ await requireAccessGrant5(c, deps, record3.workspaceId, "enrollments:read");
2403
3913
  } catch {
2404
- throw new HTTPException5(404, { message: "no pending enrollment for that code" });
3914
+ throw new HTTPException7(404, { message: "no pending enrollment for that code" });
2405
3915
  }
2406
- return c.json(DeviceEnrollmentLookupResponse.parse(toLookupResponse(record2)), 200);
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 HTTPException5(400, { message: "invalid enroll-token-exchange request" });
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 HTTPException5(503, { message: "enrollment credential plane is not configured" });
3936
+ throw new HTTPException7(503, { message: "enrollment credential plane is not configured" });
2427
3937
  }
2428
- throw new HTTPException5(401, { message: "invalid or expired enroll token" });
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 requireAccessGrant4(c, deps, workspaceId, "enrollments:manage");
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 HTTPException5(400, { message: "invalid device-approve request" });
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 HTTPException5(404, { message: "no pending enrollment for that code" });
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 requireAccessGrant4(c, deps, workspaceId, "enrollments:manage");
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 HTTPException5(400, { message: "invalid device-deny request" });
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 requireAccessGrant4(c, deps, workspaceId, "enrollments:manage");
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 HTTPException5(400, { message: "invalid mint-enroll-token request" });
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 HTTPException5(503, { message: "enrollment credential plane is not configured" });
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 requireAccessGrant4(c, deps, workspaceId, "enrollments:read");
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 requireAccessGrant4(c, deps, workspaceId, "enrollments:manage");
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 HTTPException6 } from "hono/http-exception";
2573
- import { requireAccessGrant as requireAccessGrant5 } from "@opengeni/core";
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 HTTPException6(404, { message: "selfhosted machines are not enabled for this deployment" });
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 requireAccessGrant5(c, deps, workspaceId, "enrollments:read");
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 requireAccessGrant5(c, deps, workspaceId, "enrollments:read");
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 HTTPException6(404, { message: "machine not found in this workspace" });
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 requireAccessGrant5(c, deps, workspaceId, "sessions:control");
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 encryptEnvironmentValue3,
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 HTTPException7 } from "hono/http-exception";
2809
- import { requireAccessGrant as requireAccessGrant6 } from "@opengeni/core";
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 requireEnvironmentEncryption2,
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 requireAccessGrant6(c, deps, workspaceId, "environments:use");
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 requireAccessGrant6(c, deps, workspaceId, "environments:manage");
2828
- const key = requireEnvironmentEncryption2(settings);
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 HTTPException7(422, { message: `an environment supports at most ${MAX_VARIABLES_PER_ENVIRONMENT2} variables` });
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 HTTPException7(422, { message: `duplicate environment variable name: ${variable.name}` });
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 HTTPException7(422, { message: `a workspace supports at most ${MAX_ENVIRONMENTS_PER_WORKSPACE2} environments` });
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 HTTPException7(409, { message: `environment name is already in use: ${name}` });
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: encryptEnvironmentValue3(key, variable.value)
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 requireAccessGrant6(c, deps, workspaceId, "environments:use");
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 requireAccessGrant6(c, deps, workspaceId, "environments:manage");
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 HTTPException7(409, { message: `environment name is already in use: ${name}` });
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 requireAccessGrant6(c, deps, workspaceId, "environments:manage");
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 HTTPException7(409, { message: `environment is attached to ${attachedTasks} scheduled task(s); detach first` });
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 HTTPException7(409, { message: `environment is attached to ${activeSessions} active session(s); wait for them to finish or cancel them first` });
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 requireAccessGrant6(c, deps, workspaceId, "environments:manage");
2904
- const key = requireEnvironmentEncryption2(settings);
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 HTTPException7(422, { message: `an environment supports at most ${MAX_VARIABLES_PER_ENVIRONMENT2} variables` });
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: encryptEnvironmentValue3(key, payload.value)
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 requireAccessGrant6(c, deps, workspaceId, "environments:manage");
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 HTTPException7(404, { message: "environment variable not found" });
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 HTTPException7(422, { message: "environment variable names must match ^[A-Z][A-Z0-9_]*$" });
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 HTTPException7(422, { message: "environment name is required" });
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 HTTPException8 } from "hono/http-exception";
2967
- import { requireAccessGrant as requireAccessGrant7 } from "@opengeni/core";
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 requireAccessGrant7(c, deps, workspaceId, "files:upload");
4486
+ const grant = await requireAccessGrant8(c, deps, workspaceId, "files:upload");
2974
4487
  if (!objectStorage) {
2975
- throw new HTTPException8(503, { message: "object storage is not configured" });
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 HTTPException8(413, { message: `file exceeds single PUT limit of ${objectStorage.maxSinglePutSizeBytes} bytes` });
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 requireAccessGrant7(c, deps, workspaceId, "files:upload");
4527
+ const grant = await requireAccessGrant8(c, deps, workspaceId, "files:upload");
3015
4528
  if (!objectStorage) {
3016
- throw new HTTPException8(503, { message: "object storage is not configured" });
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 HTTPException8(404, { message: "file upload not found" });
4533
+ throw new HTTPException10(404, { message: "file upload not found" });
3021
4534
  }
3022
4535
  if (upload.status !== "pending") {
3023
- throw new HTTPException8(409, { message: `file upload is ${upload.status}` });
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 HTTPException8(409, { message: "file upload has expired" });
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 HTTPException8(409, { message: `uploaded object is not available: ${error instanceof Error ? error.message : String(error)}` });
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 HTTPException8(422, { message: "uploaded object size does not match file metadata" });
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 HTTPException8(422, { message: "uploaded object content type does not match file metadata" });
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 HTTPException8(422, { message: "uploaded object checksum metadata does not match file metadata" });
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 requireAccessGrant7(c, deps, workspaceId, "files:read");
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 HTTPException8(404, { message: "file not found" });
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 requireAccessGrant7(c, deps, workspaceId, "files:read");
4582
+ await requireAccessGrant8(c, deps, workspaceId, "files:read");
3070
4583
  if (!objectStorage) {
3071
- throw new HTTPException8(503, { message: "object storage is not configured" });
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 HTTPException8(404, { message: "file not found" });
4588
+ throw new HTTPException10(404, { message: "file not found" });
3076
4589
  }
3077
4590
  if (file.status !== "ready") {
3078
- throw new HTTPException8(409, { message: `file is ${file.status}` });
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 HTTPException9 } from "hono/http-exception";
3098
- import { requireAccessGrant as requireAccessGrant8 } from "@opengeni/core";
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 requireAccessGrant8(c, deps, workspaceId, "api_keys:manage");
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 requireAccessGrant8(c, deps, workspaceId, "api_keys:manage");
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 requireAccessGrant8(c, deps, workspaceId, "api_keys:manage");
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 HTTPException9(403, { message: `cannot delegate missing permissions: ${missing.join(", ")}` });
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 HTTPException10 } from "hono/http-exception";
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 HTTPException10(403, { message: "missing workspace access for usage query" });
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 HTTPException10(404, { message: "stripe billing is not enabled" });
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 HTTPException10(400, { message: parsed.error.issues[0]?.message ?? "invalid checkout request" });
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 HTTPException10(502, { message: "Stripe did not return a checkout URL" });
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 HTTPException10(404, { message: "stripe billing is not enabled" });
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 HTTPException10(400, { message: "missing stripe-signature" });
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 HTTPException10(400, { message: error instanceof Error ? error.message : "invalid stripe signature" });
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 HTTPException10(500, { message: error instanceof Error ? error.message : String(error) });
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 HTTPException10(500, { message: "OPENGENI_PUBLIC_BASE_URL is required for Stripe checkout" });
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 HTTPException10(400, { message: `${field} must use the OpenGeni public origin` });
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 HTTPException10(404, { message: "account not found" });
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 HTTPException10(409, { message: "account selection is required" });
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 HTTPException10(403, { message: `missing permission: ${permission}` });
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 HTTPException10(500, { message: "Stripe secret key is not configured" });
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 createSignedState3,
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 readSignedState2,
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 HTTPException11 } from "hono/http-exception";
3640
- import { requireAccessGrant as requireAccessGrant9 } from "@opengeni/core";
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 requireAccessGrant9(c, deps, workspaceId, "github:use");
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 = createSignedState3(githubStateSecret, {
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 HTTPException11(400, { message: "missing GitHub installation state" });
5180
+ throw new HTTPException13(400, { message: "missing GitHub installation state" });
3668
5181
  }
3669
- const statePayload = readSignedState2(state, githubStateSecret);
5182
+ const statePayload = readSignedState3(state, githubStateSecret);
3670
5183
  if (!statePayload || statePayload.workspaceId !== workspaceId) {
3671
- throw new HTTPException11(400, { message: "invalid or expired GitHub installation state" });
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 HTTPException11(409, { message: JSON.stringify({ message: "GitHub App is not configured", missing: githubAppMissingSettings2(settings) }) });
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 requireAccessGrant9(c, deps, workspaceId, "github:use");
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 HTTPException11(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
5200
+ throw new HTTPException13(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
3688
5201
  }
3689
- throw new HTTPException11(502, { message: error instanceof Error ? error.message : String(error) });
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 requireAccessGrant9(c, deps, workspaceId, "github:use");
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 HTTPException11(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
5212
+ throw new HTTPException13(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
3700
5213
  }
3701
- throw new HTTPException11(502, { message: error instanceof Error ? error.message : String(error) });
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 requireAccessGrant9(c, deps, workspaceId, "github:manage");
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 = createSignedState3(githubStateSecret, {
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 HTTPException11(400, { message: "missing GitHub manifest code" });
5246
+ throw new HTTPException13(400, { message: "missing GitHub manifest code" });
3734
5247
  }
3735
5248
  if (!state || !verifySignedState(state, githubStateSecret)) {
3736
- throw new HTTPException11(400, { message: "invalid or expired GitHub manifest state" });
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 HTTPException11(502, { message });
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 HTTPException11(400, { message: "missing GitHub installation state" });
5269
+ throw new HTTPException13(400, { message: "missing GitHub installation state" });
3757
5270
  }
3758
- const statePayload = readSignedState2(state, githubStateSecret);
5271
+ const statePayload = readSignedState3(state, githubStateSecret);
3759
5272
  if (!statePayload || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string") {
3760
- throw new HTTPException11(400, { message: "invalid or expired GitHub installation state" });
5273
+ throw new HTTPException13(400, { message: "invalid or expired GitHub installation state" });
3761
5274
  }
3762
5275
  requireGitHubStateCookie(c, state);
3763
- const grant = await requireAccessGrant9(c, deps, statePayload.workspaceId, "github:manage");
5276
+ const grant = await requireAccessGrant10(c, deps, statePayload.workspaceId, "github:manage");
3764
5277
  if (grant.accountId !== statePayload.accountId) {
3765
- throw new HTTPException11(403, { message: "GitHub installation state does not match this workspace" });
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 HTTPException11(400, { message: "missing or invalid GitHub installation_id" });
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 HTTPException11(409, { message: JSON.stringify({ message: "GitHub App is not configured", missing: ["OPENGENI_GITHUB_CLIENT_ID"] }) });
5290
+ throw new HTTPException13(409, { message: JSON.stringify({ message: "GitHub App is not configured", missing: ["OPENGENI_GITHUB_CLIENT_ID"] }) });
3778
5291
  }
3779
- const oauthState = createSignedState3(githubStateSecret, {
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 HTTPException11(400, { message: "missing GitHub OAuth code" });
5317
+ throw new HTTPException13(400, { message: "missing GitHub OAuth code" });
3805
5318
  }
3806
5319
  if (!state) {
3807
- throw new HTTPException11(400, { message: "missing GitHub OAuth state" });
5320
+ throw new HTTPException13(400, { message: "missing GitHub OAuth state" });
3808
5321
  }
3809
- const statePayload = readSignedState2(state, githubStateSecret);
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 HTTPException11(400, { message: "invalid or expired GitHub OAuth state" });
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 HTTPException11(400, { message: "invalid or expired GitHub installation state" });
5338
+ throw new HTTPException13(400, { message: "invalid or expired GitHub installation state" });
3826
5339
  }
3827
- const grant = await requireAccessGrant9(c, deps, input.statePayload.workspaceId, "github:manage");
5340
+ const grant = await requireAccessGrant10(c, deps, input.statePayload.workspaceId, "github:manage");
3828
5341
  if (grant.accountId !== input.statePayload.accountId) {
3829
- throw new HTTPException11(403, { message: "GitHub installation state does not match this workspace" });
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 HTTPException11(404, { message: "GitHub App installation was not found for this app" });
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 HTTPException11(409, { message: "GitHub App installation is suspended" });
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 HTTPException11) {
5363
+ if (error instanceof HTTPException13) {
3851
5364
  throw error;
3852
5365
  }
3853
5366
  if (error instanceof GitHubAppConfigurationError2) {
3854
- throw new HTTPException11(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
5367
+ throw new HTTPException13(409, { message: JSON.stringify({ message: error.message, missing: error.missing }) });
3855
5368
  }
3856
- throw new HTTPException11(502, { message: error instanceof Error ? error.message : String(error) });
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 HTTPException11(400, { message: "invalid or expired GitHub installation browser state" });
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 HTTPException12 } from "hono/http-exception";
3938
- import { requireAccessGrant as requireAccessGrant10 } from "@opengeni/core";
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 requireAccessGrant10(c, deps, workspaceId, "workspace:read");
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 requireAccessGrant10(c, deps, workspaceId, "workspace:admin");
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 HTTPException12(409, { message: `pack id ${manifest.id} is a built-in pack and cannot be replaced` });
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 requireAccessGrant10(c, deps, workspaceId, "workspace:admin");
5492
+ await requireAccessGrant11(c, deps, workspaceId, "workspace:admin");
3980
5493
  const packId = c.req.param("packId");
3981
5494
  if (isBuiltInCapabilityPack(packId)) {
3982
- throw new HTTPException12(409, { message: "built-in packs cannot be unregistered" });
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 HTTPException12(404, { message: "pack not found" });
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 requireAccessGrant10(c, deps, workspaceId, "workspace:read");
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 requireAccessGrant10(c, deps, workspaceId, "workspace:read");
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 requireAccessGrant10(c, deps, workspaceId, "workspace:admin");
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 HTTPException12(422, { message: "this pack requires an environment attachment; pass environmentId" });
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 HTTPException12(422, { message: `environment is missing required variable(s): ${missing.join(", ")}` });
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 requireAccessGrant10(c, deps, workspaceId, "scheduled_tasks:manage");
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 HTTPException12(409, { message: "enable the marketing social pack before creating its scheduled tasks" });
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 HTTPException12(422, { message: "at least one connected social account is required" });
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 HTTPException12(404, { message: "pack not found" });
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 HTTPException12(422, { message: `unknown social connection: ${id}` });
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 HTTPException12(422, { message: `social connection ${inactive.id} is ${inactive.status}` });
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 HTTPException12(422, { message: `unknown document base: ${baseId}` });
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 requireAccessGrant11 } from "@opengeni/core";
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 requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:manage");
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 requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:run");
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 requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:run");
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 requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:manage");
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 requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:manage");
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 requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:manage");
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 requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:run");
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 requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:manage");
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 requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:run");
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 requireSession2,
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 appendAndPublishEvents4, coalesceSessionEventDeltas } from "@opengeni/events";
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 appendAndPublishEvents2 } from "@opengeni/events";
4306
- import { HTTPException as HTTPException13 } from "hono/http-exception";
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 HTTPException13(409, { message: "sandbox not available" });
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 HTTPException13(409, { message: `sandbox lease superseded (epoch ${acquired.lease.leaseEpoch}); retry` });
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 HTTPException13(409, { message: `sandbox not available (${error instanceof Error ? error.message : "spawn failed"})` });
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 HTTPException13(409, { message: `sandbox lease superseded (epoch ${expectedEpoch}); retry` });
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 appendAndPublishEvents2(
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 HTTPException13) return error;
4427
- if (error instanceof ChannelAValidationError) return new HTTPException13(400, { message: error.message });
4428
- if (error instanceof ChannelANotFoundError) return new HTTPException13(404, { message: error.message });
4429
- if (error instanceof ChannelAConflictError) return new HTTPException13(409, { message: error.message });
4430
- if (error instanceof ChannelAUnsupportedError) return new HTTPException13(409, { message: error.message });
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 HTTPException15 } from "hono/http-exception";
4440
- import { requireAccessGrant as requireAccessGrant12 } from "@opengeni/core";
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 appendAndPublishEvents3 } from "@opengeni/events";
4461
- import { HTTPException as HTTPException14 } from "hono/http-exception";
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 HTTPException14(409, { message: `sandbox lease superseded (epoch ${acquired.lease.leaseEpoch}); re-read capabilities and re-attach` });
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 HTTPException14(409, { message: `sandbox lease superseded (epoch ${error.leaseEpoch}); re-read capabilities and re-attach` });
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 HTTPException14) throw error;
4569
- throw new HTTPException14(409, { message: `sandbox not available (${error instanceof Error ? error.message : "spawn failed"})` });
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 appendAndPublishEvents3(db, bus, workspaceId, session.id, [
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 = createHash("sha256").update(`opengeni:stream-viewer:${rawViewerId}`).digest("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 requireAccessGrant12(c, deps, workspaceId, "sessions:create");
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 requireAccessGrant12(c, deps, workspaceId, "sessions:read");
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 requireAccessGrant12(c, deps, workspaceId, "sessions:read");
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 HTTPException15(404, { message: "session not found" });
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 requireAccessGrant12(c, deps, workspaceId, "sessions:control");
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 HTTPException15(400, { message: 'target is required ("auto" or an account id)' });
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 HTTPException15(404, { message: "session or codex account not found" });
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 requireAccessGrant12(c, deps, workspaceId, "sessions:control");
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 HTTPException15(404, { message: "session not found" });
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 requireAccessGrant12(c, deps, workspaceId, "sessions:read");
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 HTTPException15(404, { message: "session goal not found" });
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 requireAccessGrant12(c, deps, workspaceId, "sessions:control");
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 HTTPException15(404, { message: "session goal not found" });
6601
+ throw new HTTPException17(404, { message: "session goal not found" });
5089
6602
  }
5090
6603
  if (existing.status === "completed") {
5091
- throw new HTTPException15(409, { message: "session goal is completed; set a new goal instead" });
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 appendAndPublishEvents4(db, bus, workspaceId, sessionId, [{
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 HTTPException15(409, { message: `session goal is ${existing.status}; only paused goals can be resumed` });
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 appendAndPublishEvents4(db, bus, workspaceId, sessionId, [{
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 requireAccessGrant12(c, deps, workspaceId, "sessions:control");
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 HTTPException15(400, { message: "context clear requires an explicit { confirm: true }" });
6653
+ throw new HTTPException17(400, { message: "context clear requires an explicit { confirm: true }" });
5141
6654
  }
5142
- const session = await requireSession2(db, workspaceId, sessionId);
6655
+ const session = await requireSession3(db, workspaceId, sessionId);
5143
6656
  if (session.status === "queued" || session.status === "running" || session.status === "requires_action") {
5144
- throw new HTTPException15(409, { message: `session is ${session.status}; cannot clear context mid-turn \u2014 stop the turn first` });
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 appendAndPublishEvents4(db, bus, workspaceId, sessionId, [{
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 requireAccessGrant12(c, deps, workspaceId, "sessions:control");
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 requireAccessGrant12(c, deps, workspaceId, "sessions:read");
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 requireAccessGrant12(c, deps, workspaceId, "sessions:read");
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 requireAccessGrant12(c, deps, workspaceId, "sessions:read");
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 requireAccessGrant12(c, deps, workspaceId, "sessions:control");
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 requireSession2(db, workspaceId, sessionId);
6726
+ const session = await requireSession3(db, workspaceId, sessionId);
5214
6727
  const runtimeSettings = settingsWithSessionMcpServerMetadata(
5215
- await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings),
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 HTTPException15(503, { message: "object storage is not configured" });
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 appendAndPublishEvents4(db, bus, workspaceId, sessionId, [{
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 requireAccessGrant12(c, deps, workspaceId, "sessions:control");
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 appendAndPublishEvents4(db, bus, workspaceId, sessionId, [{
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 requireAccessGrant12(c, deps, workspaceId, "sessions:control");
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 appendAndPublishEvents4(db, bus, workspaceId, sessionId, [{
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 requireAccessGrant12(c, deps, workspaceId, "sessions:control");
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 requireSession2(db, workspaceId, sessionId);
6802
+ const session = await requireSession3(db, workspaceId, sessionId);
5290
6803
  if (event.type === "user.approvalDecision" && session.status !== "requires_action") {
5291
- throw new HTTPException15(409, { message: `session is ${session.status}; no approval is pending` });
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 appendAndPublishEvents4(db, bus, workspaceId, sessionId, eventsToAppend);
6811
+ const appended = await appendAndPublishEvents5(db, bus, workspaceId, sessionId, eventsToAppend);
5299
6812
  const accepted = appended[0];
5300
6813
  if (!accepted) {
5301
- throw new HTTPException15(500, { message: "failed to append client event" });
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 HTTPException15(404, { message: "sandbox ownership is not enabled for this deployment" });
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 requireAccessGrant12(c, deps, workspaceId, "sessions:read");
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 HTTPException15(404, { message: "session not found" });
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 requireAccessGrant12(c, deps, workspaceId, "stream:acknowledge");
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 HTTPException15(404, { message: "session not found" });
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 HTTPException15(400, { message: "invalid stream acknowledgment request" });
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 requireAccessGrant12(c, deps, workspaceId, "stream:view");
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 HTTPException15(404, { message: "session not found" });
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 HTTPException15(400, { message: "invalid viewer attach request" });
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 HTTPException15(409, { message: "stream_acknowledgment_required" });
6976
+ throw new HTTPException17(409, { message: "stream_acknowledgment_required" });
5464
6977
  }
5465
6978
  if (shared && !ack.acknowledgedShared) {
5466
- throw new HTTPException15(409, { message: "shared_acknowledgment_required" });
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 requireAccessGrant12(c, deps, workspaceId, "stream:view");
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 HTTPException15(404, { message: "session not found" });
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 HTTPException15(400, { message: "viewer heartbeat requires { leaseEpoch }" });
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 requireAccessGrant12(c, deps, workspaceId, "stream:view");
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 HTTPException15(404, { message: "session not found" });
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 requireAccessGrant12(c, deps, workspaceId, "stream:view");
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 HTTPException15(404, { message: "session not found" });
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 requireAccessGrant12(c, deps, workspaceId, permission);
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 HTTPException15(404, { message: "session not found" });
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 HTTPException15(400, { message: "invalid request body" });
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 appendAndPublishEvents4(db, bus, ctx.workspaceId, ctx.session.id, events);
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 HTTPException15(404, { message: "pty not found or closed" });
7248
+ throw new HTTPException17(404, { message: "pty not found or closed" });
5736
7249
  }
5737
7250
  if (pty.execSessionId === null) {
5738
- throw new HTTPException15(409, { message: "interactive terminal unsupported on this backend" });
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 appendAndPublishEvents4(db, bus, ctx.workspaceId, ctx.session.id, [{ type: "terminal.pty.output.delta", payload: delta }]);
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 HTTPException15(404, { message: "pty not found or closed" });
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 appendAndPublishEvents4(db, bus, ctx.workspaceId, ctx.session.id, [{ type: "terminal.pty.exited", payload: exited }]);
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 HTTPException16 } from "hono/http-exception";
7339
+ import { HTTPException as HTTPException18 } from "hono/http-exception";
5827
7340
  import { z as z2 } from "zod";
5828
- import { requireAccessGrant as requireAccessGrant13 } from "@opengeni/core";
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 requireAccessGrant13(c, deps, workspaceId, "workspace:read");
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 requireAccessGrant13(c, deps, workspaceId, "workspace:admin");
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 requireAccessGrant13(c, deps, workspaceId, "workspace:read");
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 requireAccessGrant13(c, deps, workspaceId, "workspace:admin");
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 HTTPException16(422, { message: "since must be an ISO date-time" });
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 HTTPException16(422, { message: "connectionIds must be a comma-separated list of UUIDs" });
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 HTTPException16(404, { message });
7430
+ return new HTTPException18(404, { message });
5918
7431
  }
5919
7432
  if (message.includes("duplicate key")) {
5920
- return new HTTPException16(409, { message: "social connection or post already exists" });
7433
+ return new HTTPException18(409, { message: "social connection or post already exists" });
5921
7434
  }
5922
- return new HTTPException16(500, { message });
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 HTTPException17 } from "hono/http-exception";
5951
- import { hasPermission as hasPermission2, requireAccessContext as requireAccessContext2, requireAccessGrant as requireAccessGrant14 } from "@opengeni/core";
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) => hasPermission2(grant.permissions, "workspace:read")).map((grant) => grant.workspaceId))];
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 HTTPException17(409, { message: "account selection is required" });
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 requireAccessGrant14(c, deps, workspaceId, "workspace:read");
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 requireAccessGrant14(c, deps, workspaceId, "workspace:admin");
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 requireAccessGrant14(c, deps, workspaceId, "workspace:admin");
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 requireAccessGrant14(c, deps, workspaceId, "workspace:read");
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 requireAccessGrant14(c, deps, workspaceId, "members:manage");
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 HTTPException17(500, { message: "failed to add member" });
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 requireAccessGrant14(c, deps, workspaceId, "members:manage");
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 HTTPException17(404, { message: "member not found" });
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 HTTPException17(500, { message: "failed to update member" });
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 requireAccessGrant14(c, deps, workspaceId, "members:manage");
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 HTTPException17(403, { message: `missing permission: ${permission}` });
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 HTTPException18(503, { message: "object storage is not configured" });
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 requireAccessGrant15(c, routeDeps, workspaceId, "workspace:read");
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, { requestOrigin: new URL(c.req.url).origin });
6264
- await mcp.connect(transport);
6265
- return await transport.handleRequest(c.req.raw);
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 HTTPException18) {
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-2JL5OXRE.js.map
8000
+ //# sourceMappingURL=chunk-DQ5TIRDZ.js.map