@opengeni/api-router 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,13 +6,14 @@ import {
6
6
  } from "@opengeni/config";
7
7
  import { ClientConfig } from "@opengeni/contracts";
8
8
  import { createDocumentServices, indexDocumentNow } from "@opengeni/documents";
9
+ import { dbSql } from "@opengeni/db";
9
10
  import { createObservability } from "@opengeni/observability";
10
11
  import { createObjectStorage } from "@opengeni/storage";
11
12
  import { WebStandardStreamableHTTPServerTransport as WebStandardStreamableHTTPServerTransport2 } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
12
13
  import { Hono } from "hono";
13
14
  import { cors } from "hono/cors";
14
- import { HTTPException as HTTPException18 } from "hono/http-exception";
15
- 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";
16
17
 
17
18
  // src/auth/managed-auth.ts
18
19
  import { ensureManagedAccessForUser } from "@opengeni/db";
@@ -493,8 +494,9 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
493
494
  });
494
495
  const json = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
495
496
  const can = (permission) => hasPermission(grant.permissions, permission);
497
+ const toolspaceMode = options.toolspace != null;
496
498
  const sessionId = typeof grant.metadata?.["sessionId"] === "string" ? grant.metadata["sessionId"] : null;
497
- if (sessionId !== null) {
499
+ if (sessionId !== null && (!toolspaceMode || can("sessions:control"))) {
498
500
  server.registerTool("set_session_title", {
499
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.",
500
502
  inputSchema: { title: z4.string().min(1).max(200) }
@@ -506,7 +508,7 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
506
508
  if (sessionId !== null && can("goals:manage")) {
507
509
  registerGoalTools(server, deps, grant, sessionId, json);
508
510
  }
509
- if (sessionId !== null && deps.settings.sandboxSelfhostedEnabled) {
511
+ if (!toolspaceMode && sessionId !== null && deps.settings.sandboxSelfhostedEnabled) {
510
512
  registerFleetTools(server, deps, grant, sessionId, json);
511
513
  }
512
514
  registerWorkspaceOrchestrationTools(server, deps, grant, can, json);
@@ -517,225 +519,253 @@ function buildOpenGeniMcpServer(deps, grant, options = {}) {
517
519
  registerGitHubTokenTool(server, deps, grant, sessionId, json);
518
520
  }
519
521
  }
520
- server.registerTool("files_get_download_url", {
521
- description: "Create a short-lived download URL for a ready file asset.",
522
- inputSchema: { fileId: z4.string().uuid() }
523
- }, async ({ fileId }) => {
524
- if (!deps.objectStorage) {
525
- throw new Error("object storage is not configured");
526
- }
527
- const file = await requireFile(deps.db, grant.workspaceId, fileId);
528
- if (file.status !== "ready") {
529
- throw new Error(`file is ${file.status}`);
530
- }
531
- const signed = await deps.objectStorage.createGetUrl({ key: file.objectKey });
532
- return json({
533
- file: {
534
- id: file.id,
535
- filename: file.filename,
536
- safeFilename: file.safeFilename,
537
- contentType: file.contentType,
538
- sizeBytes: file.sizeBytes,
539
- sha256: file.sha256,
540
- status: file.status,
541
- createdAt: file.createdAt,
542
- updatedAt: file.updatedAt
543
- },
544
- downloadUrl: {
545
- url: signed.url,
546
- 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");
547
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
+ });
548
552
  });
549
- });
550
- server.registerTool("github_repositories_list", {
551
- description: "List GitHub App repositories available as scheduled task repository resources. Use the returned resource object in scheduled task agentConfig.resources.",
552
- inputSchema: { limit: z4.number().int().positive().optional() }
553
- }, async ({ limit }) => {
554
- try {
555
- const installationIds = await listGitHubInstallationIdsForWorkspace(deps.db, grant.workspaceId);
556
- const repositories = await listGitHubAppRepositories(deps.settings, { installationIds });
557
- const visible = typeof limit === "number" ? repositories.slice(0, limit) : repositories;
558
- return json({ repositories: visible.map((repository) => repositoryWithScheduledTaskResource(repository)) });
559
- } catch (error) {
560
- if (error instanceof GitHubAppConfigurationError) {
561
- 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;
562
569
  }
563
- throw error;
564
- }
565
- });
566
- server.registerTool("social_connections_list", {
567
- description: "List connected social media accounts available to social media analysis packs.",
568
- inputSchema: { limit: z4.number().int().positive().optional() }
569
- }, async ({ limit }) => json({ connections: await listSocialConnections(deps.db, grant.workspaceId, boundedMcpLimit(limit)) }));
570
- server.registerTool("social_posts_recent", {
571
- description: "List recent social media posts imported or synced into OpenGeni.",
572
- inputSchema: {
573
- connectionIds: z4.array(z4.string().uuid()).optional(),
574
- since: z4.string().optional(),
575
- windowHours: z4.number().int().positive().optional(),
576
- limit: z4.number().int().positive().optional()
577
- }
578
- }, async ({ connectionIds, since, windowHours, limit }) => {
579
- const sinceDate = since ? parseMcpDate(since, "since") : new Date(Date.now() - (windowHours ?? 24) * 60 * 60 * 1e3);
580
- return json({
581
- since: sinceDate.toISOString(),
582
- 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, {
583
619
  workspaceId: grant.workspaceId,
584
- ...connectionIds?.length ? { connectionIds } : {},
620
+ connectionIds: connections.map((connection) => connection.id),
585
621
  since: sinceDate,
586
622
  limit: boundedMcpLimit(limit)
587
- })
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
+ });
588
639
  });
589
- });
590
- server.registerTool("social_daily_analysis_context", {
591
- description: "Collect social account and recent post context for a daily marketing analysis run.",
592
- inputSchema: {
593
- connectionIds: z4.array(z4.string().uuid()).optional(),
594
- documentBaseIds: z4.array(z4.string().uuid()).optional(),
595
- since: z4.string().optional(),
596
- windowHours: z4.number().int().positive().optional(),
597
- limit: z4.number().int().positive().optional()
598
- }
599
- }, async ({ connectionIds, documentBaseIds, since, windowHours, limit }) => {
600
- const allConnections = await listSocialConnections(deps.db, grant.workspaceId, 500);
601
- const selectedIds = connectionIds && connectionIds.length > 0 ? new Set(connectionIds) : null;
602
- const connections = selectedIds ? allConnections.filter((connection) => selectedIds.has(connection.id)) : allConnections.filter((connection) => connection.status === "connected");
603
- if (selectedIds) {
604
- const foundIds = new Set(connections.map((connection) => connection.id));
605
- const missing = [...selectedIds].filter((id) => !foundIds.has(id));
606
- if (missing.length > 0) {
607
- 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()
608
661
  }
609
- }
610
- const sinceDate = since ? parseMcpDate(since, "since") : new Date(Date.now() - (windowHours ?? 24) * 60 * 60 * 1e3);
611
- const posts = connections.length > 0 ? await listSocialPosts(deps.db, {
612
- workspaceId: grant.workspaceId,
613
- connectionIds: connections.map((connection) => connection.id),
614
- since: sinceDate,
615
- limit: boundedMcpLimit(limit)
616
- }) : [];
617
- return json({
618
- generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
619
- window: {
620
- since: sinceDate.toISOString(),
621
- until: (/* @__PURE__ */ new Date()).toISOString()
622
- },
623
- documentBaseIds: documentBaseIds ?? [],
624
- connections,
625
- posts,
626
- instructions: [
627
- "Use docs MCP search tools for the supplied documentBaseIds when brand, campaign, or audience knowledge is needed.",
628
- "Report data gaps explicitly when posts or metrics are missing.",
629
- "Do not infer unpublished metrics or hidden platform data."
630
- ]
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);
631
669
  });
632
- });
633
- server.registerTool("scheduled_tasks_list", {
634
- description: "List scheduled tasks.",
635
- inputSchema: { limit: z4.number().int().positive().optional() }
636
- }, async ({ limit }) => json({ tasks: await listScheduledTasks(deps.db, grant.workspaceId, limit ?? 100) }));
637
- server.registerTool("scheduled_tasks_get", {
638
- description: "Get one scheduled task.",
639
- inputSchema: { id: z4.string().uuid() }
640
- }, async ({ id }) => json(await requireScheduledTask(deps.db, grant.workspaceId, id)));
641
- server.registerTool("scheduled_tasks_create", {
642
- description: "Create a scheduled task.",
643
- inputSchema: {
644
- name: z4.string(),
645
- schedule: z4.unknown(),
646
- runMode: z4.string().optional(),
647
- overlapPolicy: z4.string().optional(),
648
- agentConfig: z4.unknown(),
649
- status: z4.string().optional(),
650
- environmentId: z4.string().uuid().optional(),
651
- metadata: z4.record(z4.string(), z4.unknown()).optional()
652
- }
653
- }, async (args) => {
654
- const payload = CreateScheduledTaskRequest.parse(args);
655
- requireEnvironmentsUseForMcpAttachment(grant, payload.environmentId);
656
- await requireLimit(deps, { accountId: grant.accountId, workspaceId: grant.workspaceId, action: "schedule:create", quantity: 1 });
657
- const task = await createValidatedScheduledTask({ settings: deps.settings, db: deps.db, objectStorage: deps.objectStorage, grant, payload, toolsProvided: scheduledTaskToolsProvided(args) });
658
- await syncCreatedScheduledTask({ db: deps.db, workflowClient: deps.workflowClient, task });
659
- return json(task);
660
- });
661
- server.registerTool("scheduled_tasks_update", {
662
- description: "Update a scheduled task.",
663
- inputSchema: {
664
- id: z4.string().uuid(),
665
- name: z4.string().optional(),
666
- schedule: z4.unknown().optional(),
667
- runMode: z4.string().optional(),
668
- overlapPolicy: z4.string().optional(),
669
- agentConfig: z4.unknown().optional(),
670
- status: z4.string().optional(),
671
- environmentId: z4.string().uuid().nullable().optional(),
672
- metadata: z4.record(z4.string(), z4.unknown()).optional()
673
- }
674
- }, async ({ id, ...raw }) => {
675
- const existing = await requireScheduledTask(deps.db, grant.workspaceId, id);
676
- const payload = UpdateScheduledTaskRequest.parse(raw);
677
- requireEnvironmentsUseForMcpAttachment(grant, payload.environmentId);
678
- const update = await validatedScheduledTaskUpdate({ settings: deps.settings, db: deps.db, objectStorage: deps.objectStorage, grant, existing, payload, toolsProvided: scheduledTaskToolsProvided(raw) });
679
- const task = await updateScheduledTask(deps.db, grant.workspaceId, id, update);
680
- await syncUpdatedScheduledTask({ db: deps.db, workflowClient: deps.workflowClient, previous: existing, task });
681
- return json(task);
682
- });
683
- server.registerTool("scheduled_tasks_pause", {
684
- description: "Pause a scheduled task.",
685
- inputSchema: { id: z4.string().uuid() }
686
- }, async ({ id }) => {
687
- const existing = await requireScheduledTask(deps.db, grant.workspaceId, id);
688
- const task = await updateScheduledTask(deps.db, grant.workspaceId, id, { status: "paused" });
689
- await syncUpdatedScheduledTask({ db: deps.db, workflowClient: deps.workflowClient, previous: existing, task });
690
- return json(task);
691
- });
692
- server.registerTool("scheduled_tasks_resume", {
693
- description: "Resume 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: "active" });
698
- await syncUpdatedScheduledTask({ db: deps.db, workflowClient: deps.workflowClient, previous: existing, task });
699
- return json(task);
700
- });
701
- server.registerTool("scheduled_tasks_trigger", {
702
- description: "Trigger a scheduled task immediately. Pass a stable triggerId to make a retried trigger idempotent (one charge, one run).",
703
- inputSchema: { id: z4.string().uuid(), triggerId: z4.string().min(1).max(128).optional() }
704
- }, async ({ id, triggerId }) => {
705
- const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
706
- await requireLimit(deps, { accountId: grant.accountId, workspaceId: grant.workspaceId, action: "agent_run:create", quantity: 1, model: task.agentConfig.model ?? deps.settings.openaiModel });
707
- const triggerToken = scheduledTaskTriggerToken(triggerId);
708
- const agentRunUsageIdempotencyKey = manualScheduledTaskTriggerUsageKey(grant.workspaceId, task.id, triggerToken);
709
- const triggerWorkflowId = manualScheduledTaskTriggerWorkflowId(task.id, triggerToken);
710
- await deps.workflowClient.triggerScheduledTask({ task, agentRunUsageIdempotencyKey, triggerWorkflowId });
711
- await recordWorkspaceUsage(deps, {
712
- accountId: grant.accountId,
713
- workspaceId: grant.workspaceId,
714
- subjectId: grant.subjectId,
715
- eventType: "agent_run.created",
716
- quantity: 1,
717
- unit: "run",
718
- sourceResourceType: "scheduled_task",
719
- sourceResourceId: task.id,
720
- 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 });
721
741
  });
722
- return json(task);
723
- });
724
- server.registerTool("scheduled_tasks_delete", {
725
- description: "Delete a scheduled task.",
726
- inputSchema: { id: z4.string().uuid() }
727
- }, async ({ id }) => {
728
- const task = await requireScheduledTask(deps.db, grant.workspaceId, id);
729
- await deps.workflowClient.deleteScheduledTaskSchedule({ temporalScheduleId: task.temporalScheduleId });
730
- await deleteScheduledTask(deps.db, grant.workspaceId, id);
731
- return json({ ok: true });
732
- });
733
- server.registerTool("scheduled_task_runs_list", {
734
- description: "List runs for a scheduled task.",
735
- inputSchema: { taskId: z4.string().uuid(), limit: z4.number().int().positive().optional() }
736
- }, 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);
737
748
  return server;
738
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
+ }
739
769
  function registerGoalTools(server, deps, grant, sessionId, json) {
740
770
  server.registerTool("goal_set", {
741
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.",
@@ -1215,6 +1245,448 @@ function parseMcpDate(raw, label) {
1215
1245
  return date;
1216
1246
  }
1217
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
+
1218
1690
  // src/routes/install.ts
1219
1691
  import { readFile, stat } from "fs/promises";
1220
1692
  import { HTTPException } from "hono/http-exception";
@@ -1361,13 +1833,16 @@ function isAuthExempt(c, settings) {
1361
1833
  if (path === "/v1/github/setup" || path === "/v1/github/install/callback" || path === "/v1/github/oauth/callback" || path === "/v1/github/app-manifest/callback") {
1362
1834
  return true;
1363
1835
  }
1836
+ if (path === "/v1/integrations/oauth/callback" || path === "/v1/integrations/oauth/client-metadata.json") {
1837
+ return true;
1838
+ }
1364
1839
  if (githubConnectPathPattern.test(path)) {
1365
1840
  return true;
1366
1841
  }
1367
1842
  if (installExactPaths.has(path) || isInstallRedirectPath(path)) {
1368
1843
  return true;
1369
1844
  }
1370
- if (settings.authAllowHealth && path === "/healthz") {
1845
+ if (settings.authAllowHealth && (path === "/healthz" || path === "/readyz")) {
1371
1846
  return true;
1372
1847
  }
1373
1848
  if (settings.authAllowMetrics && path === "/metrics") {
@@ -1490,7 +1965,7 @@ function registerCapabilityRoutes(app, deps) {
1490
1965
  }
1491
1966
 
1492
1967
  // src/routes/codex.ts
1493
- import { environmentsEncryptionKeyBytes } from "@opengeni/config";
1968
+ import { environmentsEncryptionKeyBytes as environmentsEncryptionKeyBytes2 } from "@opengeni/config";
1494
1969
  import {
1495
1970
  accessTokenExpiry,
1496
1971
  buildCodexUsageWindowFromCache,
@@ -1603,7 +2078,7 @@ function registerCodexRoutes(app, deps) {
1603
2078
  throw new HTTPException3(502, { message: error instanceof CodexDeviceError ? error.message : "codex token exchange failed" });
1604
2079
  }
1605
2080
  const id = parseIdToken(tokens.idToken);
1606
- const key = environmentsEncryptionKeyBytes(settings);
2081
+ const key = environmentsEncryptionKeyBytes2(settings);
1607
2082
  if (!key) {
1608
2083
  throw new HTTPException3(500, { message: "OPENGENI_ENVIRONMENTS_ENCRYPTION_KEY is not configured" });
1609
2084
  }
@@ -1746,71 +2221,960 @@ function registerCodexRoutes(app, deps) {
1746
2221
  if (!row) {
1747
2222
  throw new HTTPException3(404, { message: "codex account not found" });
1748
2223
  }
1749
- return c.json(codexAccountJson(row));
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
+ }));
1750
3035
  });
1751
- app.delete("/v1/workspaces/:workspaceId/codex/accounts/:accountId", async (c) => {
3036
+ app.post("/v1/workspaces/:workspaceId/connections", async (c) => {
1752
3037
  const workspaceId = c.req.param("workspaceId");
1753
- await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
1754
- const accountId = c.req.param("accountId");
1755
- const result = await disconnectCodexAccount(db, workspaceId, accountId);
1756
- return c.json({ disconnected: result.removed, newActiveId: result.newActiveCredentialId });
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);
1757
3055
  });
1758
- app.delete("/v1/workspaces/:workspaceId/codex", async (c) => {
3056
+ app.get("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
1759
3057
  const workspaceId = c.req.param("workspaceId");
1760
- await requireAccessGrant2(c, deps, workspaceId, "workspace:admin");
1761
- const removed = await disconnectAllCodexAccounts(db, workspaceId);
1762
- return c.json({ disconnected: removed > 0 });
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" });
3062
+ }
3063
+ return c.json(ConnectionResponse.parse({ connection }));
1763
3064
  });
1764
- app.get("/v1/workspaces/:workspaceId/codex/usage", async (c) => {
3065
+ app.patch("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
1765
3066
  const workspaceId = c.req.param("workspaceId");
1766
- await requireAccessGrant2(c, deps, workspaceId, "workspace:read");
1767
- const status = await getCodexCredentialStatus(db, workspaceId);
1768
- if (!status?.credentialId) {
1769
- throw new HTTPException3(404, { message: "codex subscription is not connected" });
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" });
3075
+ }
1770
3076
  }
1771
- const payload = await fetchCodexUsageForAccount(db, settings, workspaceId, status.credentialId);
1772
- return c.json(codexUsageJson(payload));
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 } : {}
3092
+ });
3093
+ if (!connection) {
3094
+ throw new HTTPException5(404, { message: "connection not found" });
3095
+ }
3096
+ return c.json(ConnectionResponse.parse({ connection }));
1773
3097
  });
1774
- app.get("/v1/workspaces/:workspaceId/codex/accounts/:accountId/usage", async (c) => {
3098
+ app.delete("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
1775
3099
  const workspaceId = c.req.param("workspaceId");
1776
- await requireAccessGrant2(c, deps, workspaceId, "workspace:read");
1777
- const accountId = c.req.param("accountId");
1778
- const accounts = await listCodexAccountStatuses(db, workspaceId);
1779
- if (!accounts.some((account) => account.id === accountId)) {
1780
- throw new HTTPException3(404, { message: "codex account not found" });
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" });
1781
3104
  }
1782
- const payload = await fetchCodexUsageForAccount(db, settings, workspaceId, accountId);
1783
- return c.json(codexUsageJson(payload));
3105
+ return c.json(ConnectionResponse.parse({ connection }));
1784
3106
  });
1785
- app.post("/v1/workspaces/:workspaceId/codex/usage/refresh", async (c) => {
3107
+ app.post("/v1/workspaces/:workspaceId/connections/oauth/start", async (c) => {
3108
+ assertIntegrationsEnabled();
1786
3109
  const workspaceId = c.req.param("workspaceId");
1787
- await requireAccessGrant2(c, deps, workspaceId, "workspace:read");
1788
- const accounts = await listCodexAccountStatuses(db, workspaceId);
1789
- const usage = {};
1790
- const queue = [...accounts];
1791
- const CONCURRENCY = 4;
1792
- const worker = async () => {
1793
- for (; ; ) {
1794
- const account = queue.shift();
1795
- if (!account) return;
1796
- const settled = await Promise.allSettled([fetchCodexUsageForAccount(db, settings, workspaceId, account.id)]);
1797
- const result = settled[0];
1798
- 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() } };
1799
- }
1800
- };
1801
- await Promise.all(Array.from({ length: Math.min(CONCURRENCY, Math.max(1, accounts.length)) }, () => worker()));
1802
- return c.json({ usage });
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" });
3114
+ }
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
+ }));
1803
3145
  });
1804
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
+ }
1805
3159
 
1806
3160
  // src/routes/documents.ts
1807
3161
  import {
1808
3162
  AddDocumentRequest,
3163
+ CreateKnowledgeMemoryRequest,
1809
3164
  CreateDocumentBaseRequest,
1810
3165
  Document,
1811
3166
  DocumentBase,
1812
- DocumentSearchRequest
3167
+ DocumentSearchRequest,
3168
+ KnowledgeMemory,
3169
+ KnowledgeMemorySearchRequest,
3170
+ UpdateKnowledgeMemoryRequest
1813
3171
  } from "@opengeni/contracts";
3172
+ import {
3173
+ createKnowledgeMemory as createKnowledgeMemory2,
3174
+ getKnowledgeMemory,
3175
+ listKnowledgeMemories as listKnowledgeMemories2,
3176
+ updateKnowledgeMemory
3177
+ } from "@opengeni/db";
1814
3178
  import {
1815
3179
  addDocumentToBase,
1816
3180
  createDocumentBase,
@@ -1823,8 +3187,8 @@ import {
1823
3187
  searchDocuments as searchDocuments2
1824
3188
  } from "@opengeni/documents";
1825
3189
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
1826
- import { HTTPException as HTTPException4 } from "hono/http-exception";
1827
- import { requireAccessGrant as requireAccessGrant3 } from "@opengeni/core";
3190
+ import { HTTPException as HTTPException6 } from "hono/http-exception";
3191
+ import { requireAccessGrant as requireAccessGrant4 } from "@opengeni/core";
1828
3192
  import { recordWorkspaceUsage as recordWorkspaceUsage2, requireLimit as requireLimit2 } from "@opengeni/core";
1829
3193
 
1830
3194
  // src/mcp/documents.ts
@@ -1833,9 +3197,29 @@ import {
1833
3197
  listDocumentBases,
1834
3198
  searchDocuments
1835
3199
  } from "@opengeni/documents";
3200
+ import {
3201
+ createKnowledgeMemory,
3202
+ listKnowledgeMemories
3203
+ } from "@opengeni/db";
1836
3204
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
1837
3205
  import * as z from "zod/v4";
1838
- 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 = {}) {
1839
3223
  const server = new McpServer2({
1840
3224
  name: "opengeni-documents",
1841
3225
  version: "1.0.0"
@@ -1847,27 +3231,29 @@ function buildDocumentsMcpServer(db, workspaceId, documentServices) {
1847
3231
  content: [{ type: "text", text: JSON.stringify(await listDocumentBases(db, workspaceId)) }]
1848
3232
  }));
1849
3233
  server.registerTool("search_documents", {
1850
- description: "Search indexed documents.",
1851
- inputSchema: {
1852
- query: z.string(),
1853
- baseIds: z.array(z.string()).optional(),
1854
- limit: z.number().optional()
1855
- }
1856
- }, async ({ query, baseIds, limit }) => ({
1857
- content: [{
1858
- type: "text",
1859
- text: JSON.stringify(await searchDocuments(db, {
1860
- workspaceId,
1861
- query,
1862
- ...baseIds ? { baseIds } : {},
1863
- ...limit ? { limit } : {}
1864
- }, documentServices))
1865
- }]
1866
- }));
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));
1867
3241
  server.registerTool("fetch_document_chunk", {
1868
3242
  description: "Fetch one indexed document chunk by id.",
1869
3243
  inputSchema: {
1870
- 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()
1871
3257
  }
1872
3258
  }, async ({ chunkId }) => {
1873
3259
  const found = await getDocumentChunk(db, workspaceId, chunkId);
@@ -1876,42 +3262,99 @@ function buildDocumentsMcpServer(db, workspaceId, documentServices) {
1876
3262
  isError: !found
1877
3263
  };
1878
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
+ }));
1879
3306
  return server;
1880
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
+ }
1881
3324
 
1882
3325
  // src/routes/documents.ts
1883
3326
  function registerDocumentRoutes(app, deps) {
1884
3327
  const { db, objectStorage, documentIndexer, getDocumentServices } = deps;
1885
3328
  app.post("/v1/workspaces/:workspaceId/document-bases", async (c) => {
1886
3329
  const workspaceId = c.req.param("workspaceId");
1887
- const grant = await requireAccessGrant3(c, deps, workspaceId, "documents:manage");
3330
+ const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:manage");
1888
3331
  const payload = CreateDocumentBaseRequest.parse(await c.req.json());
1889
3332
  return c.json(DocumentBase.parse(await createDocumentBase(db, { ...payload, accountId: grant.accountId, workspaceId })), 201);
1890
3333
  });
1891
3334
  app.get("/v1/workspaces/:workspaceId/document-bases", async (c) => {
1892
3335
  const workspaceId = c.req.param("workspaceId");
1893
- await requireAccessGrant3(c, deps, workspaceId, "documents:search");
3336
+ await requireAccessGrant4(c, deps, workspaceId, "documents:search");
1894
3337
  return c.json((await listDocumentBases2(db, workspaceId)).map((base) => DocumentBase.parse(base)));
1895
3338
  });
1896
3339
  app.get("/v1/workspaces/:workspaceId/document-bases/:baseId", async (c) => {
1897
3340
  const workspaceId = c.req.param("workspaceId");
1898
- await requireAccessGrant3(c, deps, workspaceId, "documents:search");
3341
+ await requireAccessGrant4(c, deps, workspaceId, "documents:search");
1899
3342
  const base = await getDocumentBase(db, workspaceId, c.req.param("baseId"));
1900
3343
  if (!base) {
1901
- throw new HTTPException4(404, { message: "document base not found" });
3344
+ throw new HTTPException6(404, { message: "document base not found" });
1902
3345
  }
1903
3346
  return c.json(DocumentBase.parse(base));
1904
3347
  });
1905
3348
  app.post("/v1/workspaces/:workspaceId/document-bases/:baseId/documents", async (c) => {
1906
3349
  const workspaceId = c.req.param("workspaceId");
1907
- const grant = await requireAccessGrant3(c, deps, workspaceId, "documents:manage");
3350
+ const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:manage");
1908
3351
  if (!objectStorage) {
1909
- throw new HTTPException4(503, { message: "object storage is not configured" });
3352
+ throw new HTTPException6(503, { message: "object storage is not configured" });
1910
3353
  }
1911
3354
  await requireLimit2(deps, { accountId: grant.accountId, workspaceId, action: "document:index", quantity: 0 });
1912
3355
  const payload = AddDocumentRequest.parse(await c.req.json());
1913
3356
  try {
1914
- 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") });
1915
3358
  const wasCreated = document.status === "queued" && document.chunkCount === 0 && document.error === null;
1916
3359
  const indexed = document.status === "ready" ? document : await documentIndexer.indexDocument({ accountId: grant.accountId, workspaceId, documentId: document.id }) ?? document;
1917
3360
  if (indexed.status === "ready") {
@@ -1934,12 +3377,12 @@ function registerDocumentRoutes(app, deps) {
1934
3377
  });
1935
3378
  app.get("/v1/workspaces/:workspaceId/document-bases/:baseId/documents", async (c) => {
1936
3379
  const workspaceId = c.req.param("workspaceId");
1937
- await requireAccessGrant3(c, deps, workspaceId, "documents:search");
3380
+ await requireAccessGrant4(c, deps, workspaceId, "documents:search");
1938
3381
  return c.json((await listDocuments(db, workspaceId, c.req.param("baseId"))).map((document) => Document.parse(document)));
1939
3382
  });
1940
3383
  app.delete("/v1/workspaces/:workspaceId/document-bases/:baseId/documents/:documentId", async (c) => {
1941
3384
  const workspaceId = c.req.param("workspaceId");
1942
- const grant = await requireAccessGrant3(c, deps, workspaceId, "documents:manage");
3385
+ const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:manage");
1943
3386
  try {
1944
3387
  await deleteDocumentFromBase(db, {
1945
3388
  accountId: grant.accountId,
@@ -1954,21 +3397,21 @@ function registerDocumentRoutes(app, deps) {
1954
3397
  });
1955
3398
  app.post("/v1/workspaces/:workspaceId/document-bases/:baseId/documents/:documentId/reindex", async (c) => {
1956
3399
  const workspaceId = c.req.param("workspaceId");
1957
- const grant = await requireAccessGrant3(c, deps, workspaceId, "documents:manage");
3400
+ const grant = await requireAccessGrant4(c, deps, workspaceId, "documents:manage");
1958
3401
  if (!objectStorage) {
1959
- throw new HTTPException4(503, { message: "object storage is not configured" });
3402
+ throw new HTTPException6(503, { message: "object storage is not configured" });
1960
3403
  }
1961
3404
  await requireLimit2(deps, { accountId: grant.accountId, workspaceId, action: "document:index", quantity: 0 });
1962
3405
  try {
1963
3406
  const document = await getDocument(db, workspaceId, c.req.param("documentId"));
1964
3407
  if (!document) {
1965
- throw new HTTPException4(404, { message: "document not found" });
3408
+ throw new HTTPException6(404, { message: "document not found" });
1966
3409
  }
1967
3410
  if (document.status !== "failed") {
1968
- throw new HTTPException4(422, { message: "only failed documents can be retried" });
3411
+ throw new HTTPException6(422, { message: "only failed documents can be retried" });
1969
3412
  }
1970
3413
  if (document.baseId !== c.req.param("baseId")) {
1971
- throw new HTTPException4(404, { message: "document not found" });
3414
+ throw new HTTPException6(404, { message: "document not found" });
1972
3415
  }
1973
3416
  const queued = await queueDocumentForReindex(db, workspaceId, document.id);
1974
3417
  const indexed = await documentIndexer.indexDocument({ accountId: grant.accountId, workspaceId, documentId: document.id }) ?? queued;
@@ -1987,7 +3430,7 @@ function registerDocumentRoutes(app, deps) {
1987
3430
  }
1988
3431
  return c.json(Document.parse(indexed));
1989
3432
  } catch (error) {
1990
- if (error instanceof HTTPException4) {
3433
+ if (error instanceof HTTPException6) {
1991
3434
  throw error;
1992
3435
  }
1993
3436
  throw documentHttpException(error);
@@ -1995,26 +3438,94 @@ function registerDocumentRoutes(app, deps) {
1995
3438
  });
1996
3439
  app.post("/v1/workspaces/:workspaceId/document-bases/:baseId/search", async (c) => {
1997
3440
  const workspaceId = c.req.param("workspaceId");
1998
- await requireAccessGrant3(c, deps, workspaceId, "documents:search");
3441
+ await requireAccessGrant4(c, deps, workspaceId, "documents:search");
1999
3442
  const payload = DocumentSearchRequest.parse(await c.req.json());
2000
3443
  const base = await getDocumentBase(db, workspaceId, c.req.param("baseId"));
2001
3444
  if (!base) {
2002
- throw new HTTPException4(404, { message: "document base not found" });
3445
+ throw new HTTPException6(404, { message: "document base not found" });
2003
3446
  }
2004
3447
  return c.json({
2005
3448
  results: await searchDocuments2(db, {
2006
3449
  workspaceId,
2007
3450
  baseIds: [base.id],
2008
3451
  query: payload.query,
2009
- 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
2010
3472
  }, getDocumentServices())
2011
3473
  });
2012
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
+ });
2013
3523
  app.all("/v1/workspaces/:workspaceId/mcp/docs", async (c) => {
2014
3524
  const workspaceId = c.req.param("workspaceId");
2015
- 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;
2016
3527
  const transport = new WebStandardStreamableHTTPServerTransport({ enableJsonResponse: true });
2017
- const server = buildDocumentsMcpServer(db, workspaceId, getDocumentServices());
3528
+ const server = buildDocumentsMcpServer(db, grant.accountId, workspaceId, getDocumentServices(), { createdBySessionId: sessionId });
2018
3529
  await server.connect(transport);
2019
3530
  return await transport.handleRequest(c.req.raw);
2020
3531
  });
@@ -2022,12 +3533,12 @@ function registerDocumentRoutes(app, deps) {
2022
3533
  function documentHttpException(error) {
2023
3534
  const message = error instanceof Error ? error.message : String(error);
2024
3535
  if (message.includes("not found")) {
2025
- return new HTTPException4(404, { message });
3536
+ return new HTTPException6(404, { message });
2026
3537
  }
2027
3538
  if (message.includes("pending") || message.includes("failed") || message.includes("deleted")) {
2028
- return new HTTPException4(422, { message });
3539
+ return new HTTPException6(422, { message });
2029
3540
  }
2030
- return new HTTPException4(500, { message });
3541
+ return new HTTPException6(500, { message });
2031
3542
  }
2032
3543
 
2033
3544
  // src/routes/enrollments.ts
@@ -2053,11 +3564,11 @@ import {
2053
3564
  listEnrollments,
2054
3565
  revokeEnrollment
2055
3566
  } from "@opengeni/db";
2056
- import { HTTPException as HTTPException5 } from "hono/http-exception";
2057
- import { requireAccessGrant as requireAccessGrant4 } from "@opengeni/core";
3567
+ import { HTTPException as HTTPException7 } from "hono/http-exception";
3568
+ import { requireAccessGrant as requireAccessGrant5 } from "@opengeni/core";
2058
3569
 
2059
3570
  // src/sandbox/enrollment.ts
2060
- import { randomBytes } from "crypto";
3571
+ import { randomBytes as randomBytes2 } from "crypto";
2061
3572
  import {
2062
3573
  resolveEnrollmentSigningSecret,
2063
3574
  resolveRelayTokenSecret
@@ -2087,11 +3598,11 @@ var ENROLLMENT_BEARER_TTL_SECONDS = 30 * 24 * 3600;
2087
3598
  var RELAY_TOKEN_TTL_SECONDS = 30 * 24 * 3600;
2088
3599
  var ENROLL_TOKEN_TTL_SECONDS = 3600;
2089
3600
  function mintDeviceCode() {
2090
- return randomBytes(32).toString("base64url");
3601
+ return randomBytes2(32).toString("base64url");
2091
3602
  }
2092
3603
  var USER_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
2093
3604
  function mintUserCode() {
2094
- const bytes = randomBytes(8);
3605
+ const bytes = randomBytes2(8);
2095
3606
  let out = "";
2096
3607
  for (let i = 0; i < 8; i += 1) {
2097
3608
  out += USER_CODE_ALPHABET[bytes[i] % USER_CODE_ALPHABET.length];
@@ -2169,18 +3680,18 @@ async function lookupDeviceEnrollment(services, input) {
2169
3680
  const { db } = services;
2170
3681
  return await getPendingDeviceEnrollmentRequestByUserCodeGlobal(db, input.userCode);
2171
3682
  }
2172
- function toLookupResponse(record2) {
3683
+ function toLookupResponse(record3) {
2173
3684
  return {
2174
- workspaceId: record2.workspaceId,
2175
- userCode: record2.userCode,
3685
+ workspaceId: record3.workspaceId,
3686
+ userCode: record3.userCode,
2176
3687
  machine: {
2177
- machineName: record2.machineName,
2178
- os: record2.os,
2179
- arch: record2.arch,
2180
- canOfferDisplay: record2.canOfferDisplay,
2181
- requestsScreenControl: record2.requestsScreenControl
3688
+ machineName: record3.machineName,
3689
+ os: record3.os,
3690
+ arch: record3.arch,
3691
+ canOfferDisplay: record3.canOfferDisplay,
3692
+ requestsScreenControl: record3.requestsScreenControl
2182
3693
  },
2183
- expiresAt: record2.expiresAt
3694
+ expiresAt: record3.expiresAt
2184
3695
  };
2185
3696
  }
2186
3697
  async function denyDeviceEnrollment(services, input) {
@@ -2337,7 +3848,7 @@ function registerEnrollmentRoutes(app, deps) {
2337
3848
  const { settings, db } = deps;
2338
3849
  function assertSelfhostedEnabled() {
2339
3850
  if (!settings.sandboxSelfhostedEnabled) {
2340
- 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" });
2341
3852
  }
2342
3853
  }
2343
3854
  const startLimiter = new TokenBucket({ capacity: 10, refillPerSecond: 0.5 });
@@ -2347,7 +3858,7 @@ function registerEnrollmentRoutes(app, deps) {
2347
3858
  function rateLimit(c, limiter) {
2348
3859
  const ip = clientIp(c);
2349
3860
  if (!limiter.take(ip)) {
2350
- throw new HTTPException5(429, { message: "too many requests; slow down" });
3861
+ throw new HTTPException7(429, { message: "too many requests; slow down" });
2351
3862
  }
2352
3863
  }
2353
3864
  app.post("/v1/enrollments/device/start", async (c) => {
@@ -2355,12 +3866,12 @@ function registerEnrollmentRoutes(app, deps) {
2355
3866
  rateLimit(c, startLimiter);
2356
3867
  const parsed = DeviceEnrollmentStartRequest.safeParse(await c.req.json().catch(() => null));
2357
3868
  if (!parsed.success) {
2358
- throw new HTTPException5(400, { message: "invalid device-start request" });
3869
+ throw new HTTPException7(400, { message: "invalid device-start request" });
2359
3870
  }
2360
3871
  const body = parsed.data;
2361
3872
  const workspace = await getWorkspace(db, body.workspaceId);
2362
3873
  if (!workspace) {
2363
- throw new HTTPException5(404, { message: "workspace not found" });
3874
+ throw new HTTPException7(404, { message: "workspace not found" });
2364
3875
  }
2365
3876
  const result = await startDeviceEnrollment({ db, settings }, {
2366
3877
  accountId: workspace.accountId,
@@ -2381,7 +3892,7 @@ function registerEnrollmentRoutes(app, deps) {
2381
3892
  rateLimit(c, pollLimiter);
2382
3893
  const parsed = DeviceEnrollmentPollRequest.safeParse(await c.req.json().catch(() => null));
2383
3894
  if (!parsed.success) {
2384
- throw new HTTPException5(400, { message: "invalid device-poll request" });
3895
+ throw new HTTPException7(400, { message: "invalid device-poll request" });
2385
3896
  }
2386
3897
  const result = await pollDeviceEnrollment({ db, settings }, { deviceCode: parsed.data.deviceCode });
2387
3898
  return c.json(result, 200);
@@ -2391,25 +3902,25 @@ function registerEnrollmentRoutes(app, deps) {
2391
3902
  rateLimit(c, lookupLimiter);
2392
3903
  const parsed = DeviceEnrollmentLookupRequest.safeParse(await c.req.json().catch(() => null));
2393
3904
  if (!parsed.success) {
2394
- throw new HTTPException5(400, { message: "invalid device-lookup request" });
3905
+ throw new HTTPException7(400, { message: "invalid device-lookup request" });
2395
3906
  }
2396
- const record2 = await lookupDeviceEnrollment({ db, settings }, { userCode: parsed.data.userCode });
2397
- if (!record2) {
2398
- 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" });
2399
3910
  }
2400
3911
  try {
2401
- await requireAccessGrant4(c, deps, record2.workspaceId, "enrollments:read");
3912
+ await requireAccessGrant5(c, deps, record3.workspaceId, "enrollments:read");
2402
3913
  } catch {
2403
- throw new HTTPException5(404, { message: "no pending enrollment for that code" });
3914
+ throw new HTTPException7(404, { message: "no pending enrollment for that code" });
2404
3915
  }
2405
- return c.json(DeviceEnrollmentLookupResponse.parse(toLookupResponse(record2)), 200);
3916
+ return c.json(DeviceEnrollmentLookupResponse.parse(toLookupResponse(record3)), 200);
2406
3917
  });
2407
3918
  app.post("/v1/enrollments/token/exchange", async (c) => {
2408
3919
  assertSelfhostedEnabled();
2409
3920
  rateLimit(c, exchangeLimiter);
2410
3921
  const parsed = EnrollTokenExchangeRequest.safeParse(await c.req.json().catch(() => null));
2411
3922
  if (!parsed.success) {
2412
- throw new HTTPException5(400, { message: "invalid enroll-token-exchange request" });
3923
+ throw new HTTPException7(400, { message: "invalid enroll-token-exchange request" });
2413
3924
  }
2414
3925
  const body = parsed.data;
2415
3926
  const result = await exchangeEnrollToken({ db, settings }, {
@@ -2422,19 +3933,19 @@ function registerEnrollmentRoutes(app, deps) {
2422
3933
  });
2423
3934
  if (!result.ok) {
2424
3935
  if (result.reason === "disabled") {
2425
- throw new HTTPException5(503, { message: "enrollment credential plane is not configured" });
3936
+ throw new HTTPException7(503, { message: "enrollment credential plane is not configured" });
2426
3937
  }
2427
- throw new HTTPException5(401, { message: "invalid or expired enroll token" });
3938
+ throw new HTTPException7(401, { message: "invalid or expired enroll token" });
2428
3939
  }
2429
3940
  return c.json(EnrollTokenExchangeResponse.parse({ credentials: result.credentials }), 201);
2430
3941
  });
2431
3942
  app.post("/v1/workspaces/:workspaceId/enrollments/device/approve", async (c) => {
2432
3943
  const workspaceId = c.req.param("workspaceId");
2433
- const grant = await requireAccessGrant4(c, deps, workspaceId, "enrollments:manage");
3944
+ const grant = await requireAccessGrant5(c, deps, workspaceId, "enrollments:manage");
2434
3945
  assertSelfhostedEnabled();
2435
3946
  const parsed = DeviceEnrollmentApproveRequest.safeParse(await c.req.json().catch(() => null));
2436
3947
  if (!parsed.success) {
2437
- throw new HTTPException5(400, { message: "invalid device-approve request" });
3948
+ throw new HTTPException7(400, { message: "invalid device-approve request" });
2438
3949
  }
2439
3950
  const body = parsed.data;
2440
3951
  const approved = await approveDeviceEnrollment({ db, settings }, {
@@ -2447,7 +3958,7 @@ function registerEnrollmentRoutes(app, deps) {
2447
3958
  approvedBySubjectLabel: grant.subjectLabel ?? null
2448
3959
  });
2449
3960
  if (!approved) {
2450
- throw new HTTPException5(404, { message: "no pending enrollment for that code" });
3961
+ throw new HTTPException7(404, { message: "no pending enrollment for that code" });
2451
3962
  }
2452
3963
  return c.json(DeviceEnrollmentApproveResponse.parse({
2453
3964
  approved: true,
@@ -2458,11 +3969,11 @@ function registerEnrollmentRoutes(app, deps) {
2458
3969
  });
2459
3970
  app.post("/v1/workspaces/:workspaceId/enrollments/device/deny", async (c) => {
2460
3971
  const workspaceId = c.req.param("workspaceId");
2461
- const grant = await requireAccessGrant4(c, deps, workspaceId, "enrollments:manage");
3972
+ const grant = await requireAccessGrant5(c, deps, workspaceId, "enrollments:manage");
2462
3973
  assertSelfhostedEnabled();
2463
3974
  const parsed = DeviceEnrollmentDenyRequest.safeParse(await c.req.json().catch(() => null));
2464
3975
  if (!parsed.success) {
2465
- throw new HTTPException5(400, { message: "invalid device-deny request" });
3976
+ throw new HTTPException7(400, { message: "invalid device-deny request" });
2466
3977
  }
2467
3978
  const result = await denyDeviceEnrollment({ db, settings }, {
2468
3979
  accountId: grant.accountId,
@@ -2473,11 +3984,11 @@ function registerEnrollmentRoutes(app, deps) {
2473
3984
  });
2474
3985
  app.post("/v1/workspaces/:workspaceId/enrollments/token", async (c) => {
2475
3986
  const workspaceId = c.req.param("workspaceId");
2476
- const grant = await requireAccessGrant4(c, deps, workspaceId, "enrollments:manage");
3987
+ const grant = await requireAccessGrant5(c, deps, workspaceId, "enrollments:manage");
2477
3988
  assertSelfhostedEnabled();
2478
3989
  const parsed = MintEnrollTokenRequest.safeParse(await c.req.json().catch(() => ({})));
2479
3990
  if (!parsed.success) {
2480
- throw new HTTPException5(400, { message: "invalid mint-enroll-token request" });
3991
+ throw new HTTPException7(400, { message: "invalid mint-enroll-token request" });
2481
3992
  }
2482
3993
  const minted = await mintEnrollToken({ db, settings }, {
2483
3994
  accountId: grant.accountId,
@@ -2485,13 +3996,13 @@ function registerEnrollmentRoutes(app, deps) {
2485
3996
  allowScreenControl: parsed.data.allowScreenControl
2486
3997
  });
2487
3998
  if (!minted) {
2488
- throw new HTTPException5(503, { message: "enrollment credential plane is not configured" });
3999
+ throw new HTTPException7(503, { message: "enrollment credential plane is not configured" });
2489
4000
  }
2490
4001
  return c.json(MintEnrollTokenResponse.parse(minted), 201);
2491
4002
  });
2492
4003
  app.get("/v1/workspaces/:workspaceId/enrollments", async (c) => {
2493
4004
  const workspaceId = c.req.param("workspaceId");
2494
- await requireAccessGrant4(c, deps, workspaceId, "enrollments:read");
4005
+ await requireAccessGrant5(c, deps, workspaceId, "enrollments:read");
2495
4006
  assertSelfhostedEnabled();
2496
4007
  const statusFilter = c.req.query("status");
2497
4008
  const rows = await listEnrollments(db, workspaceId, statusFilter === "active" ? { status: "active" } : {});
@@ -2501,6 +4012,7 @@ function registerEnrollmentRoutes(app, deps) {
2501
4012
  pubkey: row.pubkey,
2502
4013
  exposure: row.exposure,
2503
4014
  hasDisplay: row.hasDisplay,
4015
+ desktopUnavailableReason: row.desktopUnavailableReason,
2504
4016
  allowScreenControl: row.allowScreenControl,
2505
4017
  status: row.status,
2506
4018
  os: row.os,
@@ -2513,7 +4025,7 @@ function registerEnrollmentRoutes(app, deps) {
2513
4025
  });
2514
4026
  app.post("/v1/workspaces/:workspaceId/enrollments/:enrollmentId/revoke", async (c) => {
2515
4027
  const workspaceId = c.req.param("workspaceId");
2516
- const grant = await requireAccessGrant4(c, deps, workspaceId, "enrollments:manage");
4028
+ const grant = await requireAccessGrant5(c, deps, workspaceId, "enrollments:manage");
2517
4029
  assertSelfhostedEnabled();
2518
4030
  const result = await revokeEnrollment(db, {
2519
4031
  accountId: grant.accountId,
@@ -2568,8 +4080,8 @@ import {
2568
4080
  getEnrollment as getEnrollment2,
2569
4081
  readMachineMetricsSeries
2570
4082
  } from "@opengeni/db";
2571
- import { HTTPException as HTTPException6 } from "hono/http-exception";
2572
- import { requireAccessGrant as requireAccessGrant5 } from "@opengeni/core";
4083
+ import { HTTPException as HTTPException8 } from "hono/http-exception";
4084
+ import { requireAccessGrant as requireAccessGrant6 } from "@opengeni/core";
2573
4085
  import { buildFleetContextForSession as buildFleetContextForSession2, swapActiveSandbox as swapActiveSandbox2 } from "@opengeni/core";
2574
4086
 
2575
4087
  // src/sandbox/machines.ts
@@ -2684,6 +4196,7 @@ async function listMachines(services, input) {
2684
4196
  os: "linux",
2685
4197
  arch: "x86_64",
2686
4198
  hasDisplay: false,
4199
+ desktopUnavailableReason: null,
2687
4200
  allowScreenControl: false,
2688
4201
  sharedSessionCount: 1,
2689
4202
  lastSeenAt: null,
@@ -2720,6 +4233,7 @@ async function listMachines(services, input) {
2720
4233
  os: enrollment.os,
2721
4234
  arch: enrollment.arch,
2722
4235
  hasDisplay: enrollment.hasDisplay,
4236
+ desktopUnavailableReason: enrollment.desktopUnavailableReason,
2723
4237
  allowScreenControl: enrollment.allowScreenControl,
2724
4238
  sharedSessionCount,
2725
4239
  lastSeenAt: enrollment.lastSeenAt,
@@ -2741,12 +4255,12 @@ function registerMachineRoutes(app, deps) {
2741
4255
  const { settings, db, bus } = deps;
2742
4256
  function assertSelfhostedEnabled() {
2743
4257
  if (!settings.sandboxSelfhostedEnabled) {
2744
- 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" });
2745
4259
  }
2746
4260
  }
2747
4261
  app.get("/v1/workspaces/:workspaceId/machines", async (c) => {
2748
4262
  const workspaceId = c.req.param("workspaceId");
2749
- await requireAccessGrant5(c, deps, workspaceId, "enrollments:read");
4263
+ await requireAccessGrant6(c, deps, workspaceId, "enrollments:read");
2750
4264
  assertSelfhostedEnabled();
2751
4265
  const sessionId = c.req.query("sessionId") ?? null;
2752
4266
  const response = await listMachines({ db, settings, bus }, { workspaceId, sessionId });
@@ -2754,12 +4268,12 @@ function registerMachineRoutes(app, deps) {
2754
4268
  });
2755
4269
  app.get("/v1/workspaces/:workspaceId/machines/:enrollmentId/metrics/series", async (c) => {
2756
4270
  const workspaceId = c.req.param("workspaceId");
2757
- await requireAccessGrant5(c, deps, workspaceId, "enrollments:read");
4271
+ await requireAccessGrant6(c, deps, workspaceId, "enrollments:read");
2758
4272
  assertSelfhostedEnabled();
2759
4273
  const enrollmentId = c.req.param("enrollmentId");
2760
4274
  const enrollment = await getEnrollment2(db, workspaceId, enrollmentId);
2761
4275
  if (!enrollment) {
2762
- throw new HTTPException6(404, { message: "machine not found in this workspace" });
4276
+ throw new HTTPException8(404, { message: "machine not found in this workspace" });
2763
4277
  }
2764
4278
  const windowMs = SERIES_WINDOWS_MS[c.req.query("window") ?? ""] ?? DEFAULT_SERIES_WINDOW_MS;
2765
4279
  const since = new Date(Date.now() - windowMs);
@@ -2770,7 +4284,7 @@ function registerMachineRoutes(app, deps) {
2770
4284
  });
2771
4285
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/active-sandbox", async (c) => {
2772
4286
  const workspaceId = c.req.param("workspaceId");
2773
- const grant = await requireAccessGrant5(c, deps, workspaceId, "sessions:control");
4287
+ const grant = await requireAccessGrant6(c, deps, workspaceId, "sessions:control");
2774
4288
  assertSelfhostedEnabled();
2775
4289
  const sessionId = c.req.param("sessionId");
2776
4290
  const body = SwapActiveSandboxRequest.parse(await c.req.json());
@@ -2798,51 +4312,51 @@ import {
2798
4312
  createWorkspaceEnvironment as createWorkspaceEnvironment2,
2799
4313
  deleteWorkspaceEnvironment,
2800
4314
  deleteWorkspaceEnvironmentVariable,
2801
- encryptEnvironmentValue as encryptEnvironmentValue3,
4315
+ encryptEnvironmentValue as encryptEnvironmentValue5,
2802
4316
  getWorkspaceEnvironmentByName as getWorkspaceEnvironmentByName2,
2803
4317
  listWorkspaceEnvironments as listWorkspaceEnvironments2,
2804
4318
  setWorkspaceEnvironmentVariable as setWorkspaceEnvironmentVariable2,
2805
4319
  updateWorkspaceEnvironment
2806
4320
  } from "@opengeni/db";
2807
- import { HTTPException as HTTPException7 } from "hono/http-exception";
2808
- import { requireAccessGrant as requireAccessGrant6 } from "@opengeni/core";
4321
+ import { HTTPException as HTTPException9 } from "hono/http-exception";
4322
+ import { requireAccessGrant as requireAccessGrant7 } from "@opengeni/core";
2809
4323
  import {
2810
4324
  assertAllowedEnvironmentVariableName as assertAllowedEnvironmentVariableName2,
2811
4325
  MAX_ENVIRONMENTS_PER_WORKSPACE as MAX_ENVIRONMENTS_PER_WORKSPACE2,
2812
4326
  MAX_VARIABLES_PER_ENVIRONMENT as MAX_VARIABLES_PER_ENVIRONMENT2,
2813
4327
  recordEnvironmentAuditEvent as recordEnvironmentAuditEvent2,
2814
- requireEnvironmentEncryption as requireEnvironmentEncryption2,
4328
+ requireEnvironmentEncryption as requireEnvironmentEncryption4,
2815
4329
  requireEnvironmentForApi
2816
4330
  } from "@opengeni/core";
2817
4331
  function registerEnvironmentRoutes(app, deps) {
2818
4332
  const { settings, db } = deps;
2819
4333
  app.get("/v1/workspaces/:workspaceId/environments", async (c) => {
2820
4334
  const workspaceId = c.req.param("workspaceId");
2821
- await requireAccessGrant6(c, deps, workspaceId, "environments:use");
4335
+ await requireAccessGrant7(c, deps, workspaceId, "environments:use");
2822
4336
  return c.json(await listWorkspaceEnvironments2(db, workspaceId));
2823
4337
  });
2824
4338
  app.post("/v1/workspaces/:workspaceId/environments", async (c) => {
2825
4339
  const workspaceId = c.req.param("workspaceId");
2826
- const grant = await requireAccessGrant6(c, deps, workspaceId, "environments:manage");
2827
- const key = requireEnvironmentEncryption2(settings);
4340
+ const grant = await requireAccessGrant7(c, deps, workspaceId, "environments:manage");
4341
+ const key = requireEnvironmentEncryption4(settings);
2828
4342
  const payload = CreateWorkspaceEnvironmentRequest.parse(await c.req.json());
2829
4343
  const name = trimmedEnvironmentName(payload.name);
2830
4344
  if (payload.variables.length > MAX_VARIABLES_PER_ENVIRONMENT2) {
2831
- 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` });
2832
4346
  }
2833
4347
  const variableNames = /* @__PURE__ */ new Set();
2834
4348
  for (const variable of payload.variables) {
2835
4349
  assertAllowedEnvironmentVariableName2(variable.name);
2836
4350
  if (variableNames.has(variable.name)) {
2837
- throw new HTTPException7(422, { message: `duplicate environment variable name: ${variable.name}` });
4351
+ throw new HTTPException9(422, { message: `duplicate environment variable name: ${variable.name}` });
2838
4352
  }
2839
4353
  variableNames.add(variable.name);
2840
4354
  }
2841
4355
  if (await countWorkspaceEnvironments2(db, workspaceId) >= MAX_ENVIRONMENTS_PER_WORKSPACE2) {
2842
- 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` });
2843
4357
  }
2844
4358
  if (await getWorkspaceEnvironmentByName2(db, workspaceId, name)) {
2845
- 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}` });
2846
4360
  }
2847
4361
  const created = await createWorkspaceEnvironment2(db, {
2848
4362
  accountId: grant.accountId,
@@ -2851,7 +4365,7 @@ function registerEnvironmentRoutes(app, deps) {
2851
4365
  description: payload.description ?? null,
2852
4366
  variables: payload.variables.map((variable) => ({
2853
4367
  name: variable.name,
2854
- valueEncrypted: encryptEnvironmentValue3(key, variable.value)
4368
+ valueEncrypted: encryptEnvironmentValue5(key, variable.value)
2855
4369
  }))
2856
4370
  });
2857
4371
  await recordEnvironmentAuditEvent2(db, { grant, action: "environment.created", environmentId: created.id });
@@ -2859,19 +4373,19 @@ function registerEnvironmentRoutes(app, deps) {
2859
4373
  });
2860
4374
  app.get("/v1/workspaces/:workspaceId/environments/:environmentId", async (c) => {
2861
4375
  const workspaceId = c.req.param("workspaceId");
2862
- await requireAccessGrant6(c, deps, workspaceId, "environments:use");
4376
+ await requireAccessGrant7(c, deps, workspaceId, "environments:use");
2863
4377
  return c.json(await requireEnvironmentForApi(db, workspaceId, c.req.param("environmentId")));
2864
4378
  });
2865
4379
  app.patch("/v1/workspaces/:workspaceId/environments/:environmentId", async (c) => {
2866
4380
  const workspaceId = c.req.param("workspaceId");
2867
- const grant = await requireAccessGrant6(c, deps, workspaceId, "environments:manage");
4381
+ const grant = await requireAccessGrant7(c, deps, workspaceId, "environments:manage");
2868
4382
  const environment = await requireEnvironmentForApi(db, workspaceId, c.req.param("environmentId"));
2869
4383
  const payload = UpdateWorkspaceEnvironmentRequest.parse(await c.req.json());
2870
4384
  const name = payload.name !== void 0 ? trimmedEnvironmentName(payload.name) : void 0;
2871
4385
  if (name !== void 0 && name !== environment.name) {
2872
4386
  const existing = await getWorkspaceEnvironmentByName2(db, workspaceId, name);
2873
4387
  if (existing && existing.id !== environment.id) {
2874
- 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}` });
2875
4389
  }
2876
4390
  }
2877
4391
  const updated = await updateWorkspaceEnvironment(db, workspaceId, environment.id, {
@@ -2883,15 +4397,15 @@ function registerEnvironmentRoutes(app, deps) {
2883
4397
  });
2884
4398
  app.delete("/v1/workspaces/:workspaceId/environments/:environmentId", async (c) => {
2885
4399
  const workspaceId = c.req.param("workspaceId");
2886
- const grant = await requireAccessGrant6(c, deps, workspaceId, "environments:manage");
4400
+ const grant = await requireAccessGrant7(c, deps, workspaceId, "environments:manage");
2887
4401
  const environment = await requireEnvironmentForApi(db, workspaceId, c.req.param("environmentId"));
2888
4402
  const attachedTasks = await countScheduledTasksUsingEnvironment(db, workspaceId, environment.id);
2889
4403
  if (attachedTasks > 0) {
2890
- 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` });
2891
4405
  }
2892
4406
  const activeSessions = await countActiveSessionsUsingEnvironment(db, workspaceId, environment.id);
2893
4407
  if (activeSessions > 0) {
2894
- 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` });
2895
4409
  }
2896
4410
  await deleteWorkspaceEnvironment(db, workspaceId, environment.id);
2897
4411
  await recordEnvironmentAuditEvent2(db, { grant, action: "environment.deleted", environmentId: environment.id });
@@ -2899,33 +4413,33 @@ function registerEnvironmentRoutes(app, deps) {
2899
4413
  });
2900
4414
  app.put("/v1/workspaces/:workspaceId/environments/:environmentId/variables/:name", async (c) => {
2901
4415
  const workspaceId = c.req.param("workspaceId");
2902
- const grant = await requireAccessGrant6(c, deps, workspaceId, "environments:manage");
2903
- const key = requireEnvironmentEncryption2(settings);
4416
+ const grant = await requireAccessGrant7(c, deps, workspaceId, "environments:manage");
4417
+ const key = requireEnvironmentEncryption4(settings);
2904
4418
  const name = parseVariableName(c.req.param("name"));
2905
4419
  const environment = await requireEnvironmentForApi(db, workspaceId, c.req.param("environmentId"));
2906
4420
  const payload = SetWorkspaceEnvironmentVariableRequest.parse(await c.req.json());
2907
4421
  const exists = environment.variables.some((variable) => variable.name === name);
2908
4422
  if (!exists && environment.variables.length >= MAX_VARIABLES_PER_ENVIRONMENT2) {
2909
- 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` });
2910
4424
  }
2911
4425
  const metadata = await setWorkspaceEnvironmentVariable2(db, {
2912
4426
  accountId: grant.accountId,
2913
4427
  workspaceId,
2914
4428
  environmentId: environment.id,
2915
4429
  name,
2916
- valueEncrypted: encryptEnvironmentValue3(key, payload.value)
4430
+ valueEncrypted: encryptEnvironmentValue5(key, payload.value)
2917
4431
  });
2918
4432
  await recordEnvironmentAuditEvent2(db, { grant, action: "environment.variable.set", environmentId: environment.id, variableName: name });
2919
4433
  return c.json(metadata);
2920
4434
  });
2921
4435
  app.delete("/v1/workspaces/:workspaceId/environments/:environmentId/variables/:name", async (c) => {
2922
4436
  const workspaceId = c.req.param("workspaceId");
2923
- const grant = await requireAccessGrant6(c, deps, workspaceId, "environments:manage");
4437
+ const grant = await requireAccessGrant7(c, deps, workspaceId, "environments:manage");
2924
4438
  const name = parseVariableName(c.req.param("name"));
2925
4439
  const environment = await requireEnvironmentForApi(db, workspaceId, c.req.param("environmentId"));
2926
4440
  const deleted = await deleteWorkspaceEnvironmentVariable(db, workspaceId, environment.id, name);
2927
4441
  if (!deleted) {
2928
- throw new HTTPException7(404, { message: "environment variable not found" });
4442
+ throw new HTTPException9(404, { message: "environment variable not found" });
2929
4443
  }
2930
4444
  await recordEnvironmentAuditEvent2(db, { grant, action: "environment.variable.deleted", environmentId: environment.id, variableName: name });
2931
4445
  return c.json({ ok: true });
@@ -2934,7 +4448,7 @@ function registerEnvironmentRoutes(app, deps) {
2934
4448
  function parseVariableName(raw) {
2935
4449
  const parsed = WorkspaceEnvironmentVariableName2.safeParse(raw);
2936
4450
  if (!parsed.success) {
2937
- 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_]*$" });
2938
4452
  }
2939
4453
  assertAllowedEnvironmentVariableName2(parsed.data);
2940
4454
  return parsed.data;
@@ -2942,7 +4456,7 @@ function parseVariableName(raw) {
2942
4456
  function trimmedEnvironmentName(name) {
2943
4457
  const trimmed = name.trim();
2944
4458
  if (!trimmed) {
2945
- throw new HTTPException7(422, { message: "environment name is required" });
4459
+ throw new HTTPException9(422, { message: "environment name is required" });
2946
4460
  }
2947
4461
  return trimmed;
2948
4462
  }
@@ -2962,21 +4476,21 @@ import {
2962
4476
  markFileUploadFailed,
2963
4477
  requireFile as requireFile2
2964
4478
  } from "@opengeni/db";
2965
- import { HTTPException as HTTPException8 } from "hono/http-exception";
2966
- import { requireAccessGrant as requireAccessGrant7 } from "@opengeni/core";
4479
+ import { HTTPException as HTTPException10 } from "hono/http-exception";
4480
+ import { requireAccessGrant as requireAccessGrant8 } from "@opengeni/core";
2967
4481
  import { recordWorkspaceUsage as recordWorkspaceUsage3, requireLimit as requireLimit3 } from "@opengeni/core";
2968
4482
  function registerFileRoutes(app, deps) {
2969
4483
  const { db, objectStorage } = deps;
2970
4484
  app.post("/v1/workspaces/:workspaceId/files/uploads", async (c) => {
2971
4485
  const workspaceId = c.req.param("workspaceId");
2972
- const grant = await requireAccessGrant7(c, deps, workspaceId, "files:upload");
4486
+ const grant = await requireAccessGrant8(c, deps, workspaceId, "files:upload");
2973
4487
  if (!objectStorage) {
2974
- throw new HTTPException8(503, { message: "object storage is not configured" });
4488
+ throw new HTTPException10(503, { message: "object storage is not configured" });
2975
4489
  }
2976
4490
  const payload = CreateFileUploadRequest.parse(await c.req.json());
2977
4491
  await requireLimit3(deps, { accountId: grant.accountId, workspaceId, action: "file:upload", quantity: payload.sizeBytes });
2978
4492
  if (payload.sizeBytes > objectStorage.maxSinglePutSizeBytes) {
2979
- 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` });
2980
4494
  }
2981
4495
  const fileId = crypto.randomUUID();
2982
4496
  const safeFilename = sanitizeFilename(payload.filename);
@@ -3010,35 +4524,35 @@ function registerFileRoutes(app, deps) {
3010
4524
  });
3011
4525
  app.post("/v1/workspaces/:workspaceId/files/uploads/:uploadId/complete", async (c) => {
3012
4526
  const workspaceId = c.req.param("workspaceId");
3013
- const grant = await requireAccessGrant7(c, deps, workspaceId, "files:upload");
4527
+ const grant = await requireAccessGrant8(c, deps, workspaceId, "files:upload");
3014
4528
  if (!objectStorage) {
3015
- throw new HTTPException8(503, { message: "object storage is not configured" });
4529
+ throw new HTTPException10(503, { message: "object storage is not configured" });
3016
4530
  }
3017
4531
  const upload = await getFileUpload(db, workspaceId, c.req.param("uploadId"));
3018
4532
  if (!upload) {
3019
- throw new HTTPException8(404, { message: "file upload not found" });
4533
+ throw new HTTPException10(404, { message: "file upload not found" });
3020
4534
  }
3021
4535
  if (upload.status !== "pending") {
3022
- throw new HTTPException8(409, { message: `file upload is ${upload.status}` });
4536
+ throw new HTTPException10(409, { message: `file upload is ${upload.status}` });
3023
4537
  }
3024
4538
  if (upload.expiresAt.getTime() < Date.now()) {
3025
4539
  await markFileUploadFailed(db, workspaceId, upload.id, upload.file.id);
3026
- throw new HTTPException8(409, { message: "file upload has expired" });
4540
+ throw new HTTPException10(409, { message: "file upload has expired" });
3027
4541
  }
3028
4542
  const head = await objectStorage.headFile(upload.file).catch((error) => {
3029
- 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)}` });
3030
4544
  });
3031
4545
  if (Number(head.ContentLength ?? -1) !== upload.file.sizeBytes) {
3032
4546
  await markFileUploadFailed(db, workspaceId, upload.id, upload.file.id);
3033
- 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" });
3034
4548
  }
3035
4549
  if (upload.file.contentType && head.ContentType && head.ContentType !== upload.file.contentType) {
3036
4550
  await markFileUploadFailed(db, workspaceId, upload.id, upload.file.id);
3037
- 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" });
3038
4552
  }
3039
4553
  if (upload.file.sha256 && head.Metadata?.sha256 !== upload.file.sha256) {
3040
4554
  await markFileUploadFailed(db, workspaceId, upload.id, upload.file.id);
3041
- 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" });
3042
4556
  }
3043
4557
  const file = await completeFileUpload(db, workspaceId, upload.id);
3044
4558
  await recordWorkspaceUsage3(deps, {
@@ -3056,25 +4570,25 @@ function registerFileRoutes(app, deps) {
3056
4570
  });
3057
4571
  app.get("/v1/workspaces/:workspaceId/files/:fileId", async (c) => {
3058
4572
  const workspaceId = c.req.param("workspaceId");
3059
- await requireAccessGrant7(c, deps, workspaceId, "files:read");
4573
+ await requireAccessGrant8(c, deps, workspaceId, "files:read");
3060
4574
  const file = await requireFile2(db, workspaceId, c.req.param("fileId")).catch(() => null);
3061
4575
  if (!file) {
3062
- throw new HTTPException8(404, { message: "file not found" });
4576
+ throw new HTTPException10(404, { message: "file not found" });
3063
4577
  }
3064
4578
  return c.json(FileAsset.parse(file));
3065
4579
  });
3066
4580
  app.post("/v1/workspaces/:workspaceId/files/:fileId/download-url", async (c) => {
3067
4581
  const workspaceId = c.req.param("workspaceId");
3068
- await requireAccessGrant7(c, deps, workspaceId, "files:read");
4582
+ await requireAccessGrant8(c, deps, workspaceId, "files:read");
3069
4583
  if (!objectStorage) {
3070
- throw new HTTPException8(503, { message: "object storage is not configured" });
4584
+ throw new HTTPException10(503, { message: "object storage is not configured" });
3071
4585
  }
3072
4586
  const file = await requireFile2(db, workspaceId, c.req.param("fileId")).catch(() => null);
3073
4587
  if (!file) {
3074
- throw new HTTPException8(404, { message: "file not found" });
4588
+ throw new HTTPException10(404, { message: "file not found" });
3075
4589
  }
3076
4590
  if (file.status !== "ready") {
3077
- throw new HTTPException8(409, { message: `file is ${file.status}` });
4591
+ throw new HTTPException10(409, { message: `file is ${file.status}` });
3078
4592
  }
3079
4593
  const signed = await objectStorage.createGetUrl({ key: file.objectKey });
3080
4594
  return c.json(FileDownloadUrlResponse.parse({
@@ -3093,18 +4607,18 @@ function sanitizeFilename(filename) {
3093
4607
  import { CreateApiKeyRequest, CreateApiKeyResponse } from "@opengeni/contracts";
3094
4608
  import { createApiKey, listApiKeys, revokeApiKey } from "@opengeni/db";
3095
4609
  import { zValidator } from "@hono/zod-validator";
3096
- import { HTTPException as HTTPException9 } from "hono/http-exception";
3097
- import { requireAccessGrant as requireAccessGrant8 } from "@opengeni/core";
4610
+ import { HTTPException as HTTPException11 } from "hono/http-exception";
4611
+ import { requireAccessGrant as requireAccessGrant9 } from "@opengeni/core";
3098
4612
  import { requireLimit as requireLimit4 } from "@opengeni/core";
3099
4613
  function registerApiKeyRoutes(app, deps) {
3100
4614
  app.get("/v1/workspaces/:workspaceId/api-keys", async (c) => {
3101
4615
  const workspaceId = c.req.param("workspaceId");
3102
- await requireAccessGrant8(c, deps, workspaceId, "api_keys:manage");
4616
+ await requireAccessGrant9(c, deps, workspaceId, "api_keys:manage");
3103
4617
  return c.json({ apiKeys: await listApiKeys(deps.db, workspaceId) });
3104
4618
  });
3105
4619
  app.post("/v1/workspaces/:workspaceId/api-keys", zValidator("json", CreateApiKeyRequest.omit({ workspaceId: true })), async (c) => {
3106
4620
  const workspaceId = c.req.param("workspaceId");
3107
- const grant = await requireAccessGrant8(c, deps, workspaceId, "api_keys:manage");
4621
+ const grant = await requireAccessGrant9(c, deps, workspaceId, "api_keys:manage");
3108
4622
  const body = c.req.valid("json");
3109
4623
  const permissions = body.permissions.length > 0 ? body.permissions : ["workspace:read"];
3110
4624
  ensureDelegablePermissions(grant.permissions, permissions);
@@ -3124,7 +4638,7 @@ function registerApiKeyRoutes(app, deps) {
3124
4638
  });
3125
4639
  app.delete("/v1/workspaces/:workspaceId/api-keys/:apiKeyId", async (c) => {
3126
4640
  const workspaceId = c.req.param("workspaceId");
3127
- await requireAccessGrant8(c, deps, workspaceId, "api_keys:manage");
4641
+ await requireAccessGrant9(c, deps, workspaceId, "api_keys:manage");
3128
4642
  return c.json(await revokeApiKey(deps.db, workspaceId, c.req.param("apiKeyId")));
3129
4643
  });
3130
4644
  }
@@ -3134,7 +4648,7 @@ function ensureDelegablePermissions(grantPermissions, requested) {
3134
4648
  }
3135
4649
  const missing = requested.filter((permission) => !grantPermissions.includes(permission));
3136
4650
  if (missing.length > 0) {
3137
- throw new HTTPException9(403, { message: `cannot delegate missing permissions: ${missing.join(", ")}` });
4651
+ throw new HTTPException11(403, { message: `cannot delegate missing permissions: ${missing.join(", ")}` });
3138
4652
  }
3139
4653
  }
3140
4654
  function generateApiKeyToken() {
@@ -3166,7 +4680,7 @@ import {
3166
4680
  recordStripeWebhookEvent,
3167
4681
  upsertBillingCustomer
3168
4682
  } from "@opengeni/db";
3169
- import { HTTPException as HTTPException10 } from "hono/http-exception";
4683
+ import { HTTPException as HTTPException12 } from "hono/http-exception";
3170
4684
  import Stripe from "stripe";
3171
4685
  import { requireAccessContext } from "@opengeni/core";
3172
4686
  function registerBillingRoutes(app, deps) {
@@ -3180,7 +4694,7 @@ function registerBillingRoutes(app, deps) {
3180
4694
  const accountId = requireSelectedAccount(context, c.req.query("accountId"), "billing:read");
3181
4695
  const workspaceId = c.req.query("workspaceId");
3182
4696
  if (workspaceId && !context.workspaceGrants.some((grant) => grant.accountId === accountId && grant.workspaceId === workspaceId)) {
3183
- throw new HTTPException10(403, { message: "missing workspace access for usage query" });
4697
+ throw new HTTPException12(403, { message: "missing workspace access for usage query" });
3184
4698
  }
3185
4699
  return c.json({
3186
4700
  balance: await getBillingBalance(deps.db, accountId),
@@ -3198,12 +4712,12 @@ function registerBillingRoutes(app, deps) {
3198
4712
  });
3199
4713
  app.post("/v1/billing/checkout", async (c) => {
3200
4714
  if (deps.settings.billingMode !== "stripe") {
3201
- throw new HTTPException10(404, { message: "stripe billing is not enabled" });
4715
+ throw new HTTPException12(404, { message: "stripe billing is not enabled" });
3202
4716
  }
3203
4717
  const context = await requireAccessContext(c, deps);
3204
4718
  const parsed = CreateCheckoutRequest.safeParse(await c.req.json());
3205
4719
  if (!parsed.success) {
3206
- 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" });
3207
4721
  }
3208
4722
  const body = parsed.data;
3209
4723
  const accountId = requireSelectedAccount(context, body.accountId, "billing:manage");
@@ -3224,7 +4738,7 @@ function registerBillingRoutes(app, deps) {
3224
4738
  idempotencyKey
3225
4739
  }), { idempotencyKey });
3226
4740
  if (!session.url) {
3227
- 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" });
3228
4742
  }
3229
4743
  return c.json(CreateCheckoutResponse.parse({
3230
4744
  checkoutSessionId: session.id,
@@ -3233,18 +4747,18 @@ function registerBillingRoutes(app, deps) {
3233
4747
  });
3234
4748
  app.post("/v1/webhooks/stripe", async (c) => {
3235
4749
  if (deps.settings.billingMode !== "stripe") {
3236
- throw new HTTPException10(404, { message: "stripe billing is not enabled" });
4750
+ throw new HTTPException12(404, { message: "stripe billing is not enabled" });
3237
4751
  }
3238
4752
  const signature = c.req.header("stripe-signature");
3239
4753
  if (!signature) {
3240
- throw new HTTPException10(400, { message: "missing stripe-signature" });
4754
+ throw new HTTPException12(400, { message: "missing stripe-signature" });
3241
4755
  }
3242
4756
  const payload = await c.req.text();
3243
4757
  let event;
3244
4758
  try {
3245
4759
  event = await stripeClient(deps).webhooks.constructEventAsync(payload, signature, deps.settings.stripeWebhookSecret);
3246
4760
  } catch (error) {
3247
- 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" });
3248
4762
  }
3249
4763
  const firstSeen = await recordStripeWebhookEvent(deps.db, {
3250
4764
  id: event.id,
@@ -3262,7 +4776,7 @@ function registerBillingRoutes(app, deps) {
3262
4776
  await markStripeWebhookProcessed(deps.db, event.id);
3263
4777
  return c.json({ received: true });
3264
4778
  } catch (error) {
3265
- 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) });
3266
4780
  }
3267
4781
  });
3268
4782
  }
@@ -3314,7 +4828,7 @@ function stripeCheckoutSessionCreateParams(input) {
3314
4828
  }
3315
4829
  function checkoutReturnUrl(publicBaseUrl, candidate, fallbackPath, field) {
3316
4830
  if (!publicBaseUrl) {
3317
- 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" });
3318
4832
  }
3319
4833
  const base = new URL(publicBaseUrl);
3320
4834
  const fallback = new URL(fallbackPath, base).toString();
@@ -3323,7 +4837,7 @@ function checkoutReturnUrl(publicBaseUrl, candidate, fallbackPath, field) {
3323
4837
  }
3324
4838
  const parsed = new URL(candidate);
3325
4839
  if (parsed.origin !== base.origin) {
3326
- 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` });
3327
4841
  }
3328
4842
  return parsed.toString();
3329
4843
  }
@@ -3394,6 +4908,7 @@ async function handleCheckoutSessionCompleted(deps, event) {
3394
4908
  stripeCreditAmountUsd: credit.amountUsd
3395
4909
  }
3396
4910
  });
4911
+ recordCreditMicrosMetric(deps, "topup", credit.amountMicros);
3397
4912
  }
3398
4913
  async function mirrorPaymentIntentCustomer(deps, event) {
3399
4914
  const intent = event.data.object;
@@ -3426,18 +4941,23 @@ async function applyRefundDebit(deps, stripe, refund) {
3426
4941
  if (!accountId) {
3427
4942
  return;
3428
4943
  }
4944
+ const idempotencyKey = `stripe:refund:${refund.id}`;
4945
+ if (await hasCreditLedgerEntry(deps.db, accountId, idempotencyKey)) {
4946
+ return;
4947
+ }
3429
4948
  await applyCreditLedgerEntry(deps.db, {
3430
4949
  accountId,
3431
4950
  type: "credit_refund",
3432
4951
  amountMicros: -centsToMicros(refund.amount),
3433
4952
  sourceType: "stripe_refund",
3434
4953
  sourceId: refund.id,
3435
- idempotencyKey: `stripe:refund:${refund.id}`,
4954
+ idempotencyKey,
3436
4955
  metadata: {
3437
4956
  stripeRefundId: refund.id,
3438
4957
  stripePaymentIntentId: paymentIntentId(refund.payment_intent)
3439
4958
  }
3440
4959
  });
4960
+ recordCreditMicrosMetric(deps, "refund", centsToMicros(refund.amount));
3441
4961
  }
3442
4962
  async function holdDisputedCredits(deps, stripe, event) {
3443
4963
  const dispute = event.data.object;
@@ -3492,6 +5012,17 @@ async function mirrorCustomer(deps, event, customer) {
3492
5012
  email: typeof customer.email === "string" ? customer.email : null
3493
5013
  });
3494
5014
  }
5015
+ function recordCreditMicrosMetric(deps, kind, amountMicros) {
5016
+ if (amountMicros <= 0) {
5017
+ return;
5018
+ }
5019
+ deps.observability?.incrementCounter({
5020
+ name: "opengeni_credit_micros_total",
5021
+ help: "Total credit micros recorded by kind.",
5022
+ labels: { kind },
5023
+ amount: amountMicros
5024
+ });
5025
+ }
3495
5026
  async function metadataForRefund(stripe, refund) {
3496
5027
  if (Object.keys(refund.metadata ?? {}).length > 0) {
3497
5028
  return refund.metadata;
@@ -3538,7 +5069,7 @@ async function getOrCreateStripeCustomer(deps, stripe, context, accountId) {
3538
5069
  }
3539
5070
  const account = await getManagedAccount(deps.db, accountId);
3540
5071
  if (!account) {
3541
- throw new HTTPException10(404, { message: "account not found" });
5072
+ throw new HTTPException12(404, { message: "account not found" });
3542
5073
  }
3543
5074
  const customer = await stripe.customers.create({
3544
5075
  name: account.name,
@@ -3564,17 +5095,17 @@ function stripeCustomerProvider(input) {
3564
5095
  function requireSelectedAccount(context, requested, permission) {
3565
5096
  const accountId = requested ?? context.defaultAccountId ?? void 0;
3566
5097
  if (!accountId) {
3567
- throw new HTTPException10(409, { message: "account selection is required" });
5098
+ throw new HTTPException12(409, { message: "account selection is required" });
3568
5099
  }
3569
5100
  const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
3570
5101
  if (!grant || !grant.permissions.includes(permission) && !grant.permissions.includes("account:admin")) {
3571
- throw new HTTPException10(403, { message: `missing permission: ${permission}` });
5102
+ throw new HTTPException12(403, { message: `missing permission: ${permission}` });
3572
5103
  }
3573
5104
  return accountId;
3574
5105
  }
3575
5106
  function stripeClient(deps) {
3576
5107
  if (!deps.settings.stripeSecretKey) {
3577
- throw new HTTPException10(500, { message: "Stripe secret key is not configured" });
5108
+ throw new HTTPException12(500, { message: "Stripe secret key is not configured" });
3578
5109
  }
3579
5110
  return new Stripe(deps.settings.stripeSecretKey);
3580
5111
  }
@@ -3603,7 +5134,7 @@ import {
3603
5134
  import {
3604
5135
  buildGitHubAppManifest,
3605
5136
  convertGitHubAppManifest,
3606
- createSignedState as createSignedState3,
5137
+ createSignedState as createSignedState4,
3607
5138
  envLinesFromGitHubManifestConversion,
3608
5139
  GitHubAppApiError,
3609
5140
  GitHubAppConfigurationError as GitHubAppConfigurationError2,
@@ -3612,23 +5143,23 @@ import {
3612
5143
  listGitHubAppRepositories as listGitHubAppRepositories2,
3613
5144
  organizationAppManifestUrl,
3614
5145
  personalAppManifestUrl,
3615
- readSignedState as readSignedState2,
5146
+ readSignedState as readSignedState3,
3616
5147
  stateMaxAgeSeconds as stateMaxAgeSeconds2,
3617
5148
  verifyGitHubInstallationAccessForUser,
3618
5149
  verifySignedState
3619
5150
  } from "@opengeni/github";
3620
5151
  import { deleteCookie, getCookie, setCookie } from "hono/cookie";
3621
- import { HTTPException as HTTPException11 } from "hono/http-exception";
3622
- import { requireAccessGrant as requireAccessGrant9 } from "@opengeni/core";
5152
+ import { HTTPException as HTTPException13 } from "hono/http-exception";
5153
+ import { requireAccessGrant as requireAccessGrant10 } from "@opengeni/core";
3623
5154
  var githubStateCookie = "opengeni_github_state";
3624
5155
  function registerGitHubRoutes(app, deps) {
3625
5156
  const { db, settings, githubStateSecret } = deps;
3626
5157
  app.get("/v1/workspaces/:workspaceId/github/app", async (c) => {
3627
5158
  const workspaceId = c.req.param("workspaceId");
3628
- const grant = await requireAccessGrant9(c, deps, workspaceId, "github:use");
5159
+ const grant = await requireAccessGrant10(c, deps, workspaceId, "github:use");
3629
5160
  const missing = githubAppMissingSettings2(settings);
3630
5161
  const slug = settings.githubAppSlug?.trim() || null;
3631
- const state = createSignedState3(githubStateSecret, {
5162
+ const state = createSignedState4(githubStateSecret, {
3632
5163
  accountId: grant.accountId,
3633
5164
  workspaceId: grant.workspaceId
3634
5165
  });
@@ -3646,49 +5177,49 @@ function registerGitHubRoutes(app, deps) {
3646
5177
  const workspaceId = c.req.param("workspaceId");
3647
5178
  const state = c.req.query("state");
3648
5179
  if (!state) {
3649
- throw new HTTPException11(400, { message: "missing GitHub installation state" });
5180
+ throw new HTTPException13(400, { message: "missing GitHub installation state" });
3650
5181
  }
3651
- const statePayload = readSignedState2(state, githubStateSecret);
5182
+ const statePayload = readSignedState3(state, githubStateSecret);
3652
5183
  if (!statePayload || statePayload.workspaceId !== workspaceId) {
3653
- throw new HTTPException11(400, { message: "invalid or expired GitHub installation state" });
5184
+ throw new HTTPException13(400, { message: "invalid or expired GitHub installation state" });
3654
5185
  }
3655
5186
  const slug = settings.githubAppSlug?.trim();
3656
5187
  if (!slug) {
3657
- 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) }) });
3658
5189
  }
3659
5190
  setGitHubStateCookie(c, deps, state);
3660
5191
  return c.redirect(`https://github.com/apps/${slug}/installations/new?state=${encodeURIComponent(state)}`);
3661
5192
  });
3662
5193
  app.get("/v1/workspaces/:workspaceId/github/repositories", async (c) => {
3663
5194
  const workspaceId = c.req.param("workspaceId");
3664
- await requireAccessGrant9(c, deps, workspaceId, "github:use");
5195
+ await requireAccessGrant10(c, deps, workspaceId, "github:use");
3665
5196
  try {
3666
5197
  return c.json({ repositories: await listWorkspaceGitHubRepositories(deps, workspaceId) });
3667
5198
  } catch (error) {
3668
5199
  if (error instanceof GitHubAppConfigurationError2) {
3669
- 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 }) });
3670
5201
  }
3671
- 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) });
3672
5203
  }
3673
5204
  });
3674
5205
  app.post("/v1/workspaces/:workspaceId/github/repositories/sync", async (c) => {
3675
5206
  const workspaceId = c.req.param("workspaceId");
3676
- await requireAccessGrant9(c, deps, workspaceId, "github:use");
5207
+ await requireAccessGrant10(c, deps, workspaceId, "github:use");
3677
5208
  try {
3678
5209
  return c.json({ repositories: await listWorkspaceGitHubRepositories(deps, workspaceId) });
3679
5210
  } catch (error) {
3680
5211
  if (error instanceof GitHubAppConfigurationError2) {
3681
- 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 }) });
3682
5213
  }
3683
- 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) });
3684
5215
  }
3685
5216
  });
3686
5217
  app.post("/v1/workspaces/:workspaceId/github/app-manifest", async (c) => {
3687
5218
  const workspaceId = c.req.param("workspaceId");
3688
- const grant = await requireAccessGrant9(c, deps, workspaceId, "github:manage");
5219
+ const grant = await requireAccessGrant10(c, deps, workspaceId, "github:manage");
3689
5220
  const payload = GitHubAppManifestCreate.parse(await c.req.json());
3690
5221
  const baseUrl = (settings.githubAppManifestBaseUrl ?? new URL(c.req.url).origin).replace(/\/+$/, "");
3691
- const state = createSignedState3(githubStateSecret, {
5222
+ const state = createSignedState4(githubStateSecret, {
3692
5223
  accountId: grant.accountId,
3693
5224
  workspaceId: grant.workspaceId
3694
5225
  });
@@ -3712,10 +5243,10 @@ function registerGitHubRoutes(app, deps) {
3712
5243
  const code = c.req.query("code");
3713
5244
  const state = c.req.query("state");
3714
5245
  if (!code) {
3715
- throw new HTTPException11(400, { message: "missing GitHub manifest code" });
5246
+ throw new HTTPException13(400, { message: "missing GitHub manifest code" });
3716
5247
  }
3717
5248
  if (!state || !verifySignedState(state, githubStateSecret)) {
3718
- throw new HTTPException11(400, { message: "invalid or expired GitHub manifest state" });
5249
+ throw new HTTPException13(400, { message: "invalid or expired GitHub manifest state" });
3719
5250
  }
3720
5251
  try {
3721
5252
  const conversion = await convertGitHubAppManifest(code);
@@ -3726,7 +5257,7 @@ function registerGitHubRoutes(app, deps) {
3726
5257
  return c.html(githubSuccessHtml(envLines, installUrl));
3727
5258
  } catch (error) {
3728
5259
  const message = error instanceof GitHubAppApiError ? error.message : String(error);
3729
- throw new HTTPException11(502, { message });
5260
+ throw new HTTPException13(502, { message });
3730
5261
  }
3731
5262
  });
3732
5263
  const handleGitHubInstallCallback = async (c) => {
@@ -3735,30 +5266,30 @@ function registerGitHubRoutes(app, deps) {
3735
5266
  const installationIdRaw = c.req.query("installation_id");
3736
5267
  const setupAction = c.req.query("setup_action") ?? null;
3737
5268
  if (!state) {
3738
- throw new HTTPException11(400, { message: "missing GitHub installation state" });
5269
+ throw new HTTPException13(400, { message: "missing GitHub installation state" });
3739
5270
  }
3740
- const statePayload = readSignedState2(state, githubStateSecret);
5271
+ const statePayload = readSignedState3(state, githubStateSecret);
3741
5272
  if (!statePayload || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string") {
3742
- throw new HTTPException11(400, { message: "invalid or expired GitHub installation state" });
5273
+ throw new HTTPException13(400, { message: "invalid or expired GitHub installation state" });
3743
5274
  }
3744
5275
  requireGitHubStateCookie(c, state);
3745
- const grant = await requireAccessGrant9(c, deps, statePayload.workspaceId, "github:manage");
5276
+ const grant = await requireAccessGrant10(c, deps, statePayload.workspaceId, "github:manage");
3746
5277
  if (grant.accountId !== statePayload.accountId) {
3747
- 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" });
3748
5279
  }
3749
5280
  if (setupAction === "request" && !installationIdRaw) {
3750
5281
  return c.html(githubSetupPendingHtml());
3751
5282
  }
3752
5283
  const installationId = parsePositiveInteger(installationIdRaw);
3753
5284
  if (installationId === null) {
3754
- throw new HTTPException11(400, { message: "missing or invalid GitHub installation_id" });
5285
+ throw new HTTPException13(400, { message: "missing or invalid GitHub installation_id" });
3755
5286
  }
3756
5287
  if (!code) {
3757
5288
  const clientId = settings.githubClientId?.trim();
3758
5289
  if (!clientId) {
3759
- 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"] }) });
3760
5291
  }
3761
- const oauthState = createSignedState3(githubStateSecret, {
5292
+ const oauthState = createSignedState4(githubStateSecret, {
3762
5293
  accountId: grant.accountId,
3763
5294
  workspaceId: grant.workspaceId,
3764
5295
  installationId
@@ -3783,15 +5314,15 @@ function registerGitHubRoutes(app, deps) {
3783
5314
  const code = c.req.query("code");
3784
5315
  const state = c.req.query("state");
3785
5316
  if (!code) {
3786
- throw new HTTPException11(400, { message: "missing GitHub OAuth code" });
5317
+ throw new HTTPException13(400, { message: "missing GitHub OAuth code" });
3787
5318
  }
3788
5319
  if (!state) {
3789
- throw new HTTPException11(400, { message: "missing GitHub OAuth state" });
5320
+ throw new HTTPException13(400, { message: "missing GitHub OAuth state" });
3790
5321
  }
3791
- const statePayload = readSignedState2(state, githubStateSecret);
5322
+ const statePayload = readSignedState3(state, githubStateSecret);
3792
5323
  const installationId = parsePositiveInteger(String(statePayload?.installationId ?? ""));
3793
5324
  if (!statePayload || typeof statePayload.accountId !== "string" || typeof statePayload.workspaceId !== "string" || installationId === null) {
3794
- throw new HTTPException11(400, { message: "invalid or expired GitHub OAuth state" });
5325
+ throw new HTTPException13(400, { message: "invalid or expired GitHub OAuth state" });
3795
5326
  }
3796
5327
  requireGitHubStateCookie(c, state);
3797
5328
  return await completeGitHubInstallationBinding(deps, c, {
@@ -3804,19 +5335,19 @@ function registerGitHubRoutes(app, deps) {
3804
5335
  async function completeGitHubInstallationBinding(deps, c, input) {
3805
5336
  const { db, settings } = deps;
3806
5337
  if (!input.statePayload.workspaceId || !input.statePayload.accountId) {
3807
- throw new HTTPException11(400, { message: "invalid or expired GitHub installation state" });
5338
+ throw new HTTPException13(400, { message: "invalid or expired GitHub installation state" });
3808
5339
  }
3809
- const grant = await requireAccessGrant9(c, deps, input.statePayload.workspaceId, "github:manage");
5340
+ const grant = await requireAccessGrant10(c, deps, input.statePayload.workspaceId, "github:manage");
3810
5341
  if (grant.accountId !== input.statePayload.accountId) {
3811
- 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" });
3812
5343
  }
3813
5344
  try {
3814
5345
  const installation = await verifyGitHubInstallationAccessForUser(settings, { code: input.code, installationId: input.installationId });
3815
5346
  if (!installation) {
3816
- 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" });
3817
5348
  }
3818
5349
  if (installation.suspended) {
3819
- throw new HTTPException11(409, { message: "GitHub App installation is suspended" });
5350
+ throw new HTTPException13(409, { message: "GitHub App installation is suspended" });
3820
5351
  }
3821
5352
  await upsertGitHubInstallation(db, {
3822
5353
  accountId: grant.accountId,
@@ -3829,13 +5360,13 @@ async function completeGitHubInstallationBinding(deps, c, input) {
3829
5360
  deleteCookie(c, githubStateCookie, { path: "/v1/github" });
3830
5361
  return c.html(githubSetupSuccessHtml(installation.accountLogin ?? `installation ${input.installationId}`, returnUrl));
3831
5362
  } catch (error) {
3832
- if (error instanceof HTTPException11) {
5363
+ if (error instanceof HTTPException13) {
3833
5364
  throw error;
3834
5365
  }
3835
5366
  if (error instanceof GitHubAppConfigurationError2) {
3836
- 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 }) });
3837
5368
  }
3838
- 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) });
3839
5370
  }
3840
5371
  }
3841
5372
  function setGitHubStateCookie(c, deps, state) {
@@ -3849,7 +5380,7 @@ function setGitHubStateCookie(c, deps, state) {
3849
5380
  }
3850
5381
  function requireGitHubStateCookie(c, state) {
3851
5382
  if (getCookie(c, githubStateCookie) !== state) {
3852
- 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" });
3853
5384
  }
3854
5385
  }
3855
5386
  function isSecureRequest(c, deps) {
@@ -3916,8 +5447,8 @@ import {
3916
5447
  updatePackInstallationStatus
3917
5448
  } from "@opengeni/db";
3918
5449
  import { getDocumentBase as getDocumentBase2 } from "@opengeni/documents";
3919
- import { HTTPException as HTTPException12 } from "hono/http-exception";
3920
- import { requireAccessGrant as requireAccessGrant10 } from "@opengeni/core";
5450
+ import { HTTPException as HTTPException14 } from "hono/http-exception";
5451
+ import { requireAccessGrant as requireAccessGrant11 } from "@opengeni/core";
3921
5452
  import { requireLimit as requireLimit5 } from "@opengeni/core";
3922
5453
  import { validateEnvironmentAttachment } from "@opengeni/core";
3923
5454
  import {
@@ -3936,7 +5467,7 @@ function registerPackRoutes(app, deps) {
3936
5467
  const { settings, db, objectStorage, workflowClient } = deps;
3937
5468
  app.get("/v1/workspaces/:workspaceId/packs", async (c) => {
3938
5469
  const workspaceId = c.req.param("workspaceId");
3939
- await requireAccessGrant10(c, deps, workspaceId, "workspace:read");
5470
+ await requireAccessGrant11(c, deps, workspaceId, "workspace:read");
3940
5471
  return c.json({
3941
5472
  packs: await listWorkspaceCapabilityPacks(db, workspaceId),
3942
5473
  installations: await listPackInstallations(db, workspaceId)
@@ -3944,10 +5475,10 @@ function registerPackRoutes(app, deps) {
3944
5475
  });
3945
5476
  app.post("/v1/workspaces/:workspaceId/packs", async (c) => {
3946
5477
  const workspaceId = c.req.param("workspaceId");
3947
- const grant = await requireAccessGrant10(c, deps, workspaceId, "workspace:admin");
5478
+ const grant = await requireAccessGrant11(c, deps, workspaceId, "workspace:admin");
3948
5479
  const manifest = RegisterCapabilityPackRequest.parse(await c.req.json());
3949
5480
  if (isBuiltInCapabilityPack(manifest.id)) {
3950
- 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` });
3951
5482
  }
3952
5483
  const { pack, created } = await registerWorkspacePack(db, {
3953
5484
  accountId: grant.accountId,
@@ -3958,13 +5489,13 @@ function registerPackRoutes(app, deps) {
3958
5489
  });
3959
5490
  app.delete("/v1/workspaces/:workspaceId/packs/:packId", async (c) => {
3960
5491
  const workspaceId = c.req.param("workspaceId");
3961
- await requireAccessGrant10(c, deps, workspaceId, "workspace:admin");
5492
+ await requireAccessGrant11(c, deps, workspaceId, "workspace:admin");
3962
5493
  const packId = c.req.param("packId");
3963
5494
  if (isBuiltInCapabilityPack(packId)) {
3964
- throw new HTTPException12(409, { message: "built-in packs cannot be unregistered" });
5495
+ throw new HTTPException14(409, { message: "built-in packs cannot be unregistered" });
3965
5496
  }
3966
5497
  if (!await getWorkspacePack(db, workspaceId, packId)) {
3967
- throw new HTTPException12(404, { message: "pack not found" });
5498
+ throw new HTTPException14(404, { message: "pack not found" });
3968
5499
  }
3969
5500
  const installation = await getPackInstallation(db, workspaceId, packId);
3970
5501
  if (installation && installation.status === "active") {
@@ -3979,12 +5510,12 @@ function registerPackRoutes(app, deps) {
3979
5510
  });
3980
5511
  app.get("/v1/workspaces/:workspaceId/packs/installations", async (c) => {
3981
5512
  const workspaceId = c.req.param("workspaceId");
3982
- await requireAccessGrant10(c, deps, workspaceId, "workspace:read");
5513
+ await requireAccessGrant11(c, deps, workspaceId, "workspace:read");
3983
5514
  return c.json(await listPackInstallations(db, workspaceId));
3984
5515
  });
3985
5516
  app.get("/v1/workspaces/:workspaceId/packs/:packId", async (c) => {
3986
5517
  const workspaceId = c.req.param("workspaceId");
3987
- await requireAccessGrant10(c, deps, workspaceId, "workspace:read");
5518
+ await requireAccessGrant11(c, deps, workspaceId, "workspace:read");
3988
5519
  const pack = await requirePack(db, workspaceId, c.req.param("packId"));
3989
5520
  return c.json({
3990
5521
  pack,
@@ -3993,7 +5524,7 @@ function registerPackRoutes(app, deps) {
3993
5524
  });
3994
5525
  app.post("/v1/workspaces/:workspaceId/packs/:packId/enable", async (c) => {
3995
5526
  const workspaceId = c.req.param("workspaceId");
3996
- const grant = await requireAccessGrant10(c, deps, workspaceId, "workspace:admin");
5527
+ const grant = await requireAccessGrant11(c, deps, workspaceId, "workspace:admin");
3997
5528
  const pack = await requirePack(db, workspaceId, c.req.param("packId"));
3998
5529
  await assertPackSandboxImageCompatible(db, workspaceId, pack);
3999
5530
  const existing = await getPackInstallation(db, workspaceId, pack.id);
@@ -4001,13 +5532,13 @@ function registerPackRoutes(app, deps) {
4001
5532
  const storedEnvironmentId = typeof existing?.metadata.environmentId === "string" ? existing.metadata.environmentId : void 0;
4002
5533
  const environmentId = payload.environmentId ?? storedEnvironmentId;
4003
5534
  if (pack.environment?.required && !environmentId) {
4004
- 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" });
4005
5536
  }
4006
5537
  if (environmentId) {
4007
5538
  const environment = await validateEnvironmentAttachment({ settings, db }, grant, workspaceId, environmentId, { preauthorized: !payload.environmentId });
4008
5539
  const missing = (pack.environment?.requiredVariables ?? []).filter((name) => !environment.variables.some((variable) => variable.name === name));
4009
5540
  if (missing.length > 0) {
4010
- 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(", ")}` });
4011
5542
  }
4012
5543
  }
4013
5544
  const installation = await enablePackInstallation(db, {
@@ -4024,17 +5555,17 @@ function registerPackRoutes(app, deps) {
4024
5555
  });
4025
5556
  app.post("/v1/workspaces/:workspaceId/packs/marketing-social-daily-analysis/scheduled-tasks", async (c) => {
4026
5557
  const workspaceId = c.req.param("workspaceId");
4027
- const grant = await requireAccessGrant10(c, deps, workspaceId, "scheduled_tasks:manage");
5558
+ const grant = await requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:manage");
4028
5559
  const pack = await requirePack(db, workspaceId, MARKETING_SOCIAL_PACK_ID);
4029
5560
  const installation = await getPackInstallation(db, workspaceId, pack.id);
4030
5561
  if (installation?.status !== "active") {
4031
- 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" });
4032
5563
  }
4033
5564
  const payload = MarketingDailyAnalysisTaskRequest.parse(await c.req.json());
4034
5565
  await requireLimit5(deps, { accountId: grant.accountId, workspaceId, action: "schedule:create", quantity: 1 });
4035
5566
  const connections = await resolveSocialConnections(db, workspaceId, payload.connectionIds);
4036
5567
  if (connections.length === 0) {
4037
- 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" });
4038
5569
  }
4039
5570
  await validateDocumentBaseIds(db, workspaceId, payload.documentBaseIds);
4040
5571
  const agentConfig = buildMarketingDailyAnalysisAgentConfig({
@@ -4078,7 +5609,7 @@ function registerPackRoutes(app, deps) {
4078
5609
  async function requirePack(db, workspaceId, packId) {
4079
5610
  const pack = await resolveCapabilityPack(db, workspaceId, packId);
4080
5611
  if (!pack) {
4081
- throw new HTTPException12(404, { message: "pack not found" });
5612
+ throw new HTTPException14(404, { message: "pack not found" });
4082
5613
  }
4083
5614
  return pack;
4084
5615
  }
@@ -4087,13 +5618,13 @@ async function resolveSocialConnections(db, workspaceId, connectionIds) {
4087
5618
  const connections = ids.length > 0 ? await Promise.all(ids.map(async (id) => {
4088
5619
  const connection = await getSocialConnection(db, workspaceId, id);
4089
5620
  if (!connection) {
4090
- throw new HTTPException12(422, { message: `unknown social connection: ${id}` });
5621
+ throw new HTTPException14(422, { message: `unknown social connection: ${id}` });
4091
5622
  }
4092
5623
  return connection;
4093
5624
  })) : (await listSocialConnections2(db, workspaceId, 500)).filter((connection) => connection.status === "connected");
4094
5625
  const inactive = connections.find((connection) => connection.status !== "connected");
4095
5626
  if (inactive) {
4096
- 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}` });
4097
5628
  }
4098
5629
  return connections;
4099
5630
  }
@@ -4101,7 +5632,7 @@ async function validateDocumentBaseIds(db, workspaceId, documentBaseIds) {
4101
5632
  for (const baseId of [...new Set(documentBaseIds)]) {
4102
5633
  const base = await getDocumentBase2(db, workspaceId, baseId);
4103
5634
  if (!base) {
4104
- throw new HTTPException12(422, { message: `unknown document base: ${baseId}` });
5635
+ throw new HTTPException14(422, { message: `unknown document base: ${baseId}` });
4105
5636
  }
4106
5637
  }
4107
5638
  }
@@ -4114,7 +5645,7 @@ import {
4114
5645
  listScheduledTasks as listScheduledTasks2,
4115
5646
  updateScheduledTask as updateScheduledTask2
4116
5647
  } from "@opengeni/db";
4117
- import { requireAccessGrant as requireAccessGrant11 } from "@opengeni/core";
5648
+ import { requireAccessGrant as requireAccessGrant12 } from "@opengeni/core";
4118
5649
  import { recordWorkspaceUsage as recordWorkspaceUsage4, requireLimit as requireLimit6 } from "@opengeni/core";
4119
5650
  import {
4120
5651
  createValidatedScheduledTask as createValidatedScheduledTask3,
@@ -4131,7 +5662,7 @@ function registerScheduledTaskRoutes(app, deps) {
4131
5662
  const { settings, db, workflowClient, objectStorage } = deps;
4132
5663
  app.post("/v1/workspaces/:workspaceId/scheduled-tasks", async (c) => {
4133
5664
  const workspaceId = c.req.param("workspaceId");
4134
- const grant = await requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:manage");
5665
+ const grant = await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:manage");
4135
5666
  const rawPayload = await c.req.json();
4136
5667
  const payload = CreateScheduledTaskRequest2.parse(rawPayload);
4137
5668
  await requireLimit6(deps, { accountId: grant.accountId, workspaceId, action: "schedule:create", quantity: 1 });
@@ -4141,17 +5672,17 @@ function registerScheduledTaskRoutes(app, deps) {
4141
5672
  });
4142
5673
  app.get("/v1/workspaces/:workspaceId/scheduled-tasks", async (c) => {
4143
5674
  const workspaceId = c.req.param("workspaceId");
4144
- await requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:run");
5675
+ await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:run");
4145
5676
  return c.json(await listScheduledTasks2(db, workspaceId, boundedLimit(c.req.query("limit"))));
4146
5677
  });
4147
5678
  app.get("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId", async (c) => {
4148
5679
  const workspaceId = c.req.param("workspaceId");
4149
- await requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:run");
5680
+ await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:run");
4150
5681
  return c.json(await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId")));
4151
5682
  });
4152
5683
  app.patch("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId", async (c) => {
4153
5684
  const workspaceId = c.req.param("workspaceId");
4154
- const grant = await requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:manage");
5685
+ const grant = await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:manage");
4155
5686
  const taskId = c.req.param("taskId");
4156
5687
  const existing = await requireScheduledTaskForApi(db, workspaceId, taskId);
4157
5688
  const rawPayload = await c.req.json();
@@ -4163,7 +5694,7 @@ function registerScheduledTaskRoutes(app, deps) {
4163
5694
  });
4164
5695
  app.post("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/pause", async (c) => {
4165
5696
  const workspaceId = c.req.param("workspaceId");
4166
- await requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:manage");
5697
+ await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:manage");
4167
5698
  const existing = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
4168
5699
  const task = await updateScheduledTask2(db, workspaceId, existing.id, { status: "paused" });
4169
5700
  await syncUpdatedScheduledTask2({ db, workflowClient, previous: existing, task });
@@ -4171,7 +5702,7 @@ function registerScheduledTaskRoutes(app, deps) {
4171
5702
  });
4172
5703
  app.post("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/resume", async (c) => {
4173
5704
  const workspaceId = c.req.param("workspaceId");
4174
- await requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:manage");
5705
+ await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:manage");
4175
5706
  const existing = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
4176
5707
  const task = await updateScheduledTask2(db, workspaceId, existing.id, { status: "active" });
4177
5708
  await syncUpdatedScheduledTask2({ db, workflowClient, previous: existing, task });
@@ -4179,7 +5710,7 @@ function registerScheduledTaskRoutes(app, deps) {
4179
5710
  });
4180
5711
  app.post("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/trigger", async (c) => {
4181
5712
  const workspaceId = c.req.param("workspaceId");
4182
- const grant = await requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:run");
5713
+ const grant = await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:run");
4183
5714
  const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
4184
5715
  await requireLimit6(deps, { accountId: grant.accountId, workspaceId, action: "agent_run:create", quantity: 1, model: task.agentConfig.model ?? deps.settings.openaiModel });
4185
5716
  const body = await c.req.json().catch(() => ({}));
@@ -4203,7 +5734,7 @@ function registerScheduledTaskRoutes(app, deps) {
4203
5734
  });
4204
5735
  app.delete("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId", async (c) => {
4205
5736
  const workspaceId = c.req.param("workspaceId");
4206
- await requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:manage");
5737
+ await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:manage");
4207
5738
  const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
4208
5739
  await workflowClient.deleteScheduledTaskSchedule({ temporalScheduleId: task.temporalScheduleId });
4209
5740
  await deleteScheduledTask2(db, workspaceId, task.id);
@@ -4211,7 +5742,7 @@ function registerScheduledTaskRoutes(app, deps) {
4211
5742
  });
4212
5743
  app.get("/v1/workspaces/:workspaceId/scheduled-tasks/:taskId/runs", async (c) => {
4213
5744
  const workspaceId = c.req.param("workspaceId");
4214
- await requireAccessGrant11(c, deps, workspaceId, "scheduled_tasks:run");
5745
+ await requireAccessGrant12(c, deps, workspaceId, "scheduled_tasks:run");
4215
5746
  const task = await requireScheduledTaskForApi(db, workspaceId, c.req.param("taskId"));
4216
5747
  return c.json(await listScheduledTaskRuns2(db, workspaceId, task.id, boundedLimit(c.req.query("limit"))));
4217
5748
  });
@@ -4263,14 +5794,14 @@ import {
4263
5794
  recordStreamAcknowledgment,
4264
5795
  reorderQueuedSessionTurns,
4265
5796
  requestSessionCompaction,
4266
- requireSession as requireSession2,
5797
+ requireSession as requireSession3,
4267
5798
  setSessionCodexPin,
4268
5799
  revokeViewer,
4269
5800
  setSessionGoalStatus as setSessionGoalStatus2,
4270
5801
  updatePtySessionActivity,
4271
5802
  updateQueuedSessionTurn
4272
5803
  } from "@opengeni/db";
4273
- import { appendAndPublishEvents as appendAndPublishEvents4, coalesceSessionEventDeltas } from "@opengeni/events";
5804
+ import { appendAndPublishEvents as appendAndPublishEvents5, coalesceSessionEventDeltas } from "@opengeni/events";
4274
5805
 
4275
5806
  // src/sandbox/channel-a.ts
4276
5807
  import { applyGitAuthPointerEnvironment, hasGitHubRepositorySelection, stableSandboxEnvironmentForRun } from "@opengeni/config";
@@ -4284,8 +5815,8 @@ import {
4284
5815
  readLease as readLease2,
4285
5816
  releaseLeaseHolder
4286
5817
  } from "@opengeni/db";
4287
- import { appendAndPublishEvents as appendAndPublishEvents2 } from "@opengeni/events";
4288
- 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";
4289
5820
  import {
4290
5821
  establishSandboxSessionFromEnvelope,
4291
5822
  serializeEstablishedSandboxEnvelope,
@@ -4300,7 +5831,7 @@ async function withChannelA(services, ctx, fn) {
4300
5831
  const { db, settings, bus } = services;
4301
5832
  const { accountId, workspaceId, session, subjectId } = ctx;
4302
5833
  if (session.sandboxBackend === "none") {
4303
- throw new HTTPException13(409, { message: "sandbox not available" });
5834
+ throw new HTTPException15(409, { message: "sandbox not available" });
4304
5835
  }
4305
5836
  const sandboxGroupId = session.sandboxGroupId;
4306
5837
  const viewerId = crypto.randomUUID();
@@ -4328,7 +5859,7 @@ async function withChannelA(services, ctx, fn) {
4328
5859
  });
4329
5860
  if (acquired.role === "fenced") {
4330
5861
  await release();
4331
- 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` });
4332
5863
  }
4333
5864
  let established;
4334
5865
  let leaseSnapshot = acquired.lease;
@@ -4336,7 +5867,7 @@ async function withChannelA(services, ctx, fn) {
4336
5867
  const envelope = await getSandboxSessionEnvelope(db, workspaceId, session.id);
4337
5868
  const workspaceEnvironment = await loadWorkspaceEnvironmentForRun(db, settings, workspaceId, session.environmentId);
4338
5869
  const settingsForSession = session.sandboxBackend !== settings.sandboxBackend ? { ...settings, sandboxBackend: session.sandboxBackend } : settings;
4339
- const environment = stableSandboxEnvironmentForRun(settingsForSession, workspaceEnvironment?.values ?? {});
5870
+ const environment = stableSandboxEnvironmentForRun(settingsForSession, workspaceEnvironment?.values ?? {}, { workspaceId });
4340
5871
  if (hasGitHubRepositorySelection(session.resources)) {
4341
5872
  applyGitAuthPointerEnvironment(environment, githubAppBotIdentity(settings));
4342
5873
  }
@@ -4351,7 +5882,7 @@ async function withChannelA(services, ctx, fn) {
4351
5882
  });
4352
5883
  } catch (error) {
4353
5884
  await failWarmingToCold(db, { accountId, workspaceId, sandboxGroupId, expectedEpoch });
4354
- 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"})` });
4355
5886
  }
4356
5887
  const resumeEnvelope = await serializeEstablishedSandboxEnvelope(established) ?? envelope ?? null;
4357
5888
  const committed = await commitWarmingToWarm(db, {
@@ -4366,7 +5897,7 @@ async function withChannelA(services, ctx, fn) {
4366
5897
  leaseTtlMs
4367
5898
  });
4368
5899
  if (!committed.committed || !committed.lease) {
4369
- throw new HTTPException13(409, { message: `sandbox lease superseded (epoch ${expectedEpoch}); retry` });
5900
+ throw new HTTPException15(409, { message: `sandbox lease superseded (epoch ${expectedEpoch}); retry` });
4370
5901
  }
4371
5902
  leaseSnapshot = committed.lease;
4372
5903
  } else {
@@ -4380,7 +5911,7 @@ async function withChannelA(services, ctx, fn) {
4380
5911
  });
4381
5912
  }
4382
5913
  const emit = async (events) => {
4383
- await appendAndPublishEvents2(
5914
+ await appendAndPublishEvents3(
4384
5915
  db,
4385
5916
  bus,
4386
5917
  workspaceId,
@@ -4405,11 +5936,11 @@ async function withChannelA(services, ctx, fn) {
4405
5936
  }
4406
5937
  }
4407
5938
  function mapChannelAError(error) {
4408
- if (error instanceof HTTPException13) return error;
4409
- if (error instanceof ChannelAValidationError) return new HTTPException13(400, { message: error.message });
4410
- if (error instanceof ChannelANotFoundError) return new HTTPException13(404, { message: error.message });
4411
- if (error instanceof ChannelAConflictError) return new HTTPException13(409, { message: error.message });
4412
- 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 });
4413
5944
  return error;
4414
5945
  }
4415
5946
  async function dropEstablishedHandle(established) {
@@ -4418,11 +5949,11 @@ async function dropEstablishedHandle(established) {
4418
5949
 
4419
5950
  // src/routes/sessions.ts
4420
5951
  import { negotiateCapabilities } from "@opengeni/runtime/sandbox";
4421
- import { HTTPException as HTTPException15 } from "hono/http-exception";
4422
- import { requireAccessGrant as requireAccessGrant12 } from "@opengeni/core";
5952
+ import { HTTPException as HTTPException17 } from "hono/http-exception";
5953
+ import { requireAccessGrant as requireAccessGrant13 } from "@opengeni/core";
4423
5954
 
4424
5955
  // src/sandbox/viewer.ts
4425
- import { createHash } from "crypto";
5956
+ import { createHash as createHash2 } from "crypto";
4426
5957
  import { applyGitAuthPointerEnvironment as applyGitAuthPointerEnvironment2, hasGitHubRepositorySelection as hasGitHubRepositorySelection2, resolveStreamTokenSecret, stableSandboxEnvironmentForRun as stableSandboxEnvironmentForRun2 } from "@opengeni/config";
4427
5958
  import { githubAppBotIdentity as githubAppBotIdentity2 } from "@opengeni/github";
4428
5959
  import {
@@ -4439,8 +5970,8 @@ import {
4439
5970
  releaseLeaseHolder as releaseLeaseHolder2,
4440
5971
  SandboxLeaseSupersededError as SandboxLeaseSupersededError2
4441
5972
  } from "@opengeni/db";
4442
- import { appendAndPublishEvents as appendAndPublishEvents3 } from "@opengeni/events";
4443
- 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";
4444
5975
  import {
4445
5976
  DESKTOP_STREAM_PORT,
4446
5977
  ensureDisplayStack,
@@ -4467,7 +5998,7 @@ async function sessionAttachEnvironment(services, workspaceId, session) {
4467
5998
  session.environmentId
4468
5999
  );
4469
6000
  const settingsForSession = session.sandboxBackend !== services.settings.sandboxBackend ? { ...services.settings, sandboxBackend: session.sandboxBackend } : services.settings;
4470
- const environment = stableSandboxEnvironmentForRun2(settingsForSession, workspaceEnvironment?.values ?? {});
6001
+ const environment = stableSandboxEnvironmentForRun2(settingsForSession, workspaceEnvironment?.values ?? {}, { workspaceId });
4471
6002
  if (hasGitHubRepositorySelection2(session.resources)) {
4472
6003
  applyGitAuthPointerEnvironment2(environment, githubAppBotIdentity2(services.settings));
4473
6004
  }
@@ -4502,7 +6033,7 @@ async function attachViewer(services, input) {
4502
6033
  });
4503
6034
  if (acquired.role === "fenced") {
4504
6035
  await release();
4505
- 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` });
4506
6037
  }
4507
6038
  if (acquired.role === "spawner") {
4508
6039
  const expectedEpoch = acquired.lease.leaseEpoch;
@@ -4543,12 +6074,12 @@ async function attachViewer(services, input) {
4543
6074
  };
4544
6075
  } catch (error) {
4545
6076
  if (error instanceof SandboxLeaseSupersededError2) {
4546
- 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` });
4547
6078
  }
4548
6079
  await failWarmingToCold2(db, { accountId, workspaceId, sandboxGroupId, expectedEpoch });
4549
6080
  await release();
4550
- if (error instanceof HTTPException14) throw error;
4551
- 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"})` });
4552
6083
  } finally {
4553
6084
  await dropEstablishedHandle2(established);
4554
6085
  }
@@ -4697,7 +6228,7 @@ async function mintDesktopStream(services, input) {
4697
6228
  viewerId
4698
6229
  };
4699
6230
  try {
4700
- await appendAndPublishEvents3(db, bus, workspaceId, session.id, [
6231
+ await appendAndPublishEvents4(db, bus, workspaceId, session.id, [
4701
6232
  { type: "stream.url.rotated", payload }
4702
6233
  ]);
4703
6234
  } catch {
@@ -4892,7 +6423,7 @@ function viewerIdAsUuid(rawViewerId) {
4892
6423
  if (UUID_RE.test(rawViewerId)) {
4893
6424
  return rawViewerId;
4894
6425
  }
4895
- const hex = createHash("sha256").update(`opengeni:stream-viewer:${rawViewerId}`).digest("hex");
6426
+ const hex = createHash2("sha256").update(`opengeni:stream-viewer:${rawViewerId}`).digest("hex");
4896
6427
  const b = hex.slice(0, 32).split("");
4897
6428
  b[12] = "5";
4898
6429
  const variantNibble = parseInt(b[16], 16) & 3 | 8;
@@ -4902,7 +6433,7 @@ function viewerIdAsUuid(rawViewerId) {
4902
6433
  }
4903
6434
 
4904
6435
  // src/routes/sessions.ts
4905
- import { settingsWithEnabledCapabilityMcpServers, settingsWithSessionMcpServerMetadata } from "@opengeni/core";
6436
+ import { settingsWithEnabledCapabilityMcpServers as settingsWithEnabledCapabilityMcpServers2, settingsWithSessionMcpServerMetadata } from "@opengeni/core";
4906
6437
  import {
4907
6438
  normalizeResources,
4908
6439
  validateFileResources,
@@ -5001,76 +6532,76 @@ function registerSessionRoutes(app, deps) {
5001
6532
  const { settings, db, bus, workflowClient, objectStorage } = deps;
5002
6533
  app.post("/v1/workspaces/:workspaceId/sessions", async (c) => {
5003
6534
  const workspaceId = c.req.param("workspaceId");
5004
- const grant = await requireAccessGrant12(c, deps, workspaceId, "sessions:create");
6535
+ const grant = await requireAccessGrant13(c, deps, workspaceId, "sessions:create");
5005
6536
  const session = await createSessionForRequest2(deps, grant, workspaceId, await c.req.json());
5006
6537
  return c.json(session, 202);
5007
6538
  });
5008
6539
  app.get("/v1/workspaces/:workspaceId/sessions", async (c) => {
5009
6540
  const workspaceId = c.req.param("workspaceId");
5010
- await requireAccessGrant12(c, deps, workspaceId, "sessions:read");
6541
+ await requireAccessGrant13(c, deps, workspaceId, "sessions:read");
5011
6542
  return c.json(await listSessions2(db, workspaceId, boundedLimit(c.req.query("limit"))));
5012
6543
  });
5013
6544
  app.get("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
5014
6545
  const workspaceId = c.req.param("workspaceId");
5015
- await requireAccessGrant12(c, deps, workspaceId, "sessions:read");
6546
+ await requireAccessGrant13(c, deps, workspaceId, "sessions:read");
5016
6547
  const session = await getSession4(db, workspaceId, c.req.param("sessionId"));
5017
6548
  if (!session) {
5018
- throw new HTTPException15(404, { message: "session not found" });
6549
+ throw new HTTPException17(404, { message: "session not found" });
5019
6550
  }
5020
6551
  return c.json(session);
5021
6552
  });
5022
6553
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/codex-account", async (c) => {
5023
6554
  const workspaceId = c.req.param("workspaceId");
5024
- await requireAccessGrant12(c, deps, workspaceId, "sessions:control");
6555
+ await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
5025
6556
  const sessionId = c.req.param("sessionId");
5026
6557
  const body = await c.req.json();
5027
6558
  const target = typeof body.target === "string" ? body.target : "";
5028
6559
  if (!target) {
5029
- 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)' });
5030
6561
  }
5031
6562
  const pinned = target === "auto" ? null : target;
5032
6563
  const ok = await setSessionCodexPin(db, workspaceId, sessionId, pinned);
5033
6564
  if (!ok) {
5034
- throw new HTTPException15(404, { message: "session or codex account not found" });
6565
+ throw new HTTPException17(404, { message: "session or codex account not found" });
5035
6566
  }
5036
6567
  return c.json({ pinned: target === "auto" ? "auto" : target });
5037
6568
  });
5038
6569
  app.patch("/v1/workspaces/:workspaceId/sessions/:sessionId", async (c) => {
5039
6570
  const workspaceId = c.req.param("workspaceId");
5040
- await requireAccessGrant12(c, deps, workspaceId, "sessions:control");
6571
+ await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
5041
6572
  const sessionId = c.req.param("sessionId");
5042
6573
  await assertSessionExists(db, workspaceId, sessionId);
5043
6574
  const payload = UpdateSessionRequest.parse(await c.req.json());
5044
6575
  await updateSessionTitle2({ db, bus }, workspaceId, sessionId, payload.title, "user");
5045
6576
  const session = await getSession4(db, workspaceId, sessionId);
5046
6577
  if (!session) {
5047
- throw new HTTPException15(404, { message: "session not found" });
6578
+ throw new HTTPException17(404, { message: "session not found" });
5048
6579
  }
5049
6580
  return c.json(session);
5050
6581
  });
5051
6582
  app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/goal", async (c) => {
5052
6583
  const workspaceId = c.req.param("workspaceId");
5053
- await requireAccessGrant12(c, deps, workspaceId, "sessions:read");
6584
+ await requireAccessGrant13(c, deps, workspaceId, "sessions:read");
5054
6585
  const sessionId = c.req.param("sessionId");
5055
6586
  await assertSessionExists(db, workspaceId, sessionId);
5056
6587
  const goal = await getSessionGoal2(db, workspaceId, sessionId);
5057
6588
  if (!goal) {
5058
- throw new HTTPException15(404, { message: "session goal not found" });
6589
+ throw new HTTPException17(404, { message: "session goal not found" });
5059
6590
  }
5060
6591
  return c.json(goal);
5061
6592
  });
5062
6593
  app.patch("/v1/workspaces/:workspaceId/sessions/:sessionId/goal", async (c) => {
5063
6594
  const workspaceId = c.req.param("workspaceId");
5064
- const grant = await requireAccessGrant12(c, deps, workspaceId, "sessions:control");
6595
+ const grant = await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
5065
6596
  const sessionId = c.req.param("sessionId");
5066
6597
  await assertSessionExists(db, workspaceId, sessionId);
5067
6598
  const payload = UpdateSessionGoalRequest.parse(await c.req.json());
5068
6599
  const existing = await getSessionGoal2(db, workspaceId, sessionId);
5069
6600
  if (!existing) {
5070
- throw new HTTPException15(404, { message: "session goal not found" });
6601
+ throw new HTTPException17(404, { message: "session goal not found" });
5071
6602
  }
5072
6603
  if (existing.status === "completed") {
5073
- 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" });
5074
6605
  }
5075
6606
  if (payload.status === "paused") {
5076
6607
  const { goal: goal2, changed: changed2 } = await setSessionGoalStatus2(db, workspaceId, sessionId, {
@@ -5079,7 +6610,7 @@ function registerSessionRoutes(app, deps) {
5079
6610
  pausedReason: "api"
5080
6611
  });
5081
6612
  if (changed2) {
5082
- await appendAndPublishEvents4(db, bus, workspaceId, sessionId, [{
6613
+ await appendAndPublishEvents5(db, bus, workspaceId, sessionId, [{
5083
6614
  type: "goal.paused",
5084
6615
  payload: {
5085
6616
  goalId: goal2.id,
@@ -5094,11 +6625,11 @@ function registerSessionRoutes(app, deps) {
5094
6625
  return c.json(goal2);
5095
6626
  }
5096
6627
  if (existing.status !== "paused") {
5097
- 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` });
5098
6629
  }
5099
6630
  const { goal, changed } = await setSessionGoalStatus2(db, workspaceId, sessionId, { status: "active" });
5100
6631
  if (changed) {
5101
- await appendAndPublishEvents4(db, bus, workspaceId, sessionId, [{
6632
+ await appendAndPublishEvents5(db, bus, workspaceId, sessionId, [{
5102
6633
  type: "goal.resumed",
5103
6634
  payload: {
5104
6635
  goalId: goal.id,
@@ -5114,19 +6645,19 @@ function registerSessionRoutes(app, deps) {
5114
6645
  });
5115
6646
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/context/clear", async (c) => {
5116
6647
  const workspaceId = c.req.param("workspaceId");
5117
- const grant = await requireAccessGrant12(c, deps, workspaceId, "sessions:control");
6648
+ const grant = await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
5118
6649
  const sessionId = c.req.param("sessionId");
5119
6650
  await assertSessionExists(db, workspaceId, sessionId);
5120
6651
  const clearBody = ClearSessionContextRequest.safeParse(await c.req.json().catch(() => ({})));
5121
6652
  if (!clearBody.success) {
5122
- 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 }" });
5123
6654
  }
5124
- const session = await requireSession2(db, workspaceId, sessionId);
6655
+ const session = await requireSession3(db, workspaceId, sessionId);
5125
6656
  if (session.status === "queued" || session.status === "running" || session.status === "requires_action") {
5126
- 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` });
5127
6658
  }
5128
6659
  const result = await clearSessionContext(db, { accountId: grant.accountId, workspaceId, sessionId });
5129
- await appendAndPublishEvents4(db, bus, workspaceId, sessionId, [{
6660
+ await appendAndPublishEvents5(db, bus, workspaceId, sessionId, [{
5130
6661
  type: "session.context.cleared",
5131
6662
  payload: {
5132
6663
  clearedBy: "api",
@@ -5138,7 +6669,7 @@ function registerSessionRoutes(app, deps) {
5138
6669
  });
5139
6670
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/context/compact", async (c) => {
5140
6671
  const workspaceId = c.req.param("workspaceId");
5141
- await requireAccessGrant12(c, deps, workspaceId, "sessions:control");
6672
+ await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
5142
6673
  const sessionId = c.req.param("sessionId");
5143
6674
  await assertSessionExists(db, workspaceId, sessionId);
5144
6675
  CompactSessionContextRequest.parse(await c.req.json().catch(() => ({})) ?? {});
@@ -5154,7 +6685,7 @@ function registerSessionRoutes(app, deps) {
5154
6685
  });
5155
6686
  app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/events", async (c) => {
5156
6687
  const workspaceId = c.req.param("workspaceId");
5157
- await requireAccessGrant12(c, deps, workspaceId, "sessions:read");
6688
+ await requireAccessGrant13(c, deps, workspaceId, "sessions:read");
5158
6689
  const sessionId = c.req.param("sessionId");
5159
6690
  await assertSessionExists(db, workspaceId, sessionId);
5160
6691
  const after = eventSequence(c.req.query("after"), 0);
@@ -5170,7 +6701,7 @@ function registerSessionRoutes(app, deps) {
5170
6701
  });
5171
6702
  app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/events/stream", async (c) => {
5172
6703
  const workspaceId = c.req.param("workspaceId");
5173
- await requireAccessGrant12(c, deps, workspaceId, "sessions:read");
6704
+ await requireAccessGrant13(c, deps, workspaceId, "sessions:read");
5174
6705
  const sessionId = c.req.param("sessionId");
5175
6706
  await assertSessionExists(db, workspaceId, sessionId);
5176
6707
  const after = Number(c.req.query("after") ?? c.req.header("Last-Event-ID") ?? 0);
@@ -5178,29 +6709,29 @@ function registerSessionRoutes(app, deps) {
5178
6709
  });
5179
6710
  app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/turns", async (c) => {
5180
6711
  const workspaceId = c.req.param("workspaceId");
5181
- await requireAccessGrant12(c, deps, workspaceId, "sessions:read");
6712
+ await requireAccessGrant13(c, deps, workspaceId, "sessions:read");
5182
6713
  const sessionId = c.req.param("sessionId");
5183
6714
  await assertSessionExists(db, workspaceId, sessionId);
5184
6715
  return c.json(await listSessionTurns(db, workspaceId, sessionId, boundedLimit(c.req.query("limit"))));
5185
6716
  });
5186
6717
  app.patch("/v1/workspaces/:workspaceId/sessions/:sessionId/turns/:turnId", async (c) => {
5187
6718
  const workspaceId = c.req.param("workspaceId");
5188
- await requireAccessGrant12(c, deps, workspaceId, "sessions:control");
6719
+ await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
5189
6720
  const sessionId = c.req.param("sessionId");
5190
6721
  const turnId = c.req.param("turnId");
5191
6722
  await assertSessionExists(db, workspaceId, sessionId);
5192
6723
  const existing = await requireQueuedTurnForApi(db, workspaceId, sessionId, turnId);
5193
6724
  const payload = UpdateSessionTurnRequest.parse(await c.req.json());
5194
6725
  assertConfiguredModel(settings, payload.model);
5195
- const session = await requireSession2(db, workspaceId, sessionId);
6726
+ const session = await requireSession3(db, workspaceId, sessionId);
5196
6727
  const runtimeSettings = settingsWithSessionMcpServerMetadata(
5197
- await settingsWithEnabledCapabilityMcpServers(db, workspaceId, settings),
6728
+ await settingsWithEnabledCapabilityMcpServers2(db, workspaceId, settings),
5198
6729
  session.mcpServers
5199
6730
  );
5200
6731
  const resources = payload.resources !== void 0 ? normalizeResources(payload.resources) : existing.resources;
5201
6732
  const tools = payload.tools !== void 0 ? validateToolRefs(payload.tools, runtimeSettings) : existing.tools;
5202
6733
  if (resources.some((resource) => resource.kind === "file") && !objectStorage) {
5203
- throw new HTTPException15(503, { message: "object storage is not configured" });
6734
+ throw new HTTPException17(503, { message: "object storage is not configured" });
5204
6735
  }
5205
6736
  await validateFileResources(db, workspaceId, resources);
5206
6737
  await validateGitHubRepositorySelection(db, workspaceId, [...session.resources, ...resources]);
@@ -5213,7 +6744,7 @@ function registerSessionRoutes(app, deps) {
5213
6744
  resources,
5214
6745
  tools
5215
6746
  });
5216
- await appendAndPublishEvents4(db, bus, workspaceId, sessionId, [{
6747
+ await appendAndPublishEvents5(db, bus, workspaceId, sessionId, [{
5217
6748
  type: "turn.updated",
5218
6749
  turnId: turn.id,
5219
6750
  payload: { turnId: turn.id }
@@ -5222,12 +6753,12 @@ function registerSessionRoutes(app, deps) {
5222
6753
  });
5223
6754
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/turns/reorder", async (c) => {
5224
6755
  const workspaceId = c.req.param("workspaceId");
5225
- const grant = await requireAccessGrant12(c, deps, workspaceId, "sessions:control");
6756
+ const grant = await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
5226
6757
  const sessionId = c.req.param("sessionId");
5227
6758
  await assertSessionExists(db, workspaceId, sessionId);
5228
6759
  const payload = ReorderSessionTurnsRequest.parse(await c.req.json());
5229
6760
  const turns = await reorderQueuedSessionTurns(db, workspaceId, sessionId, payload.turnIds);
5230
- await appendAndPublishEvents4(db, bus, workspaceId, sessionId, [{
6761
+ await appendAndPublishEvents5(db, bus, workspaceId, sessionId, [{
5231
6762
  type: "turn.updated",
5232
6763
  payload: { reorderedTurnIds: payload.turnIds }
5233
6764
  }]);
@@ -5236,13 +6767,13 @@ function registerSessionRoutes(app, deps) {
5236
6767
  });
5237
6768
  app.delete("/v1/workspaces/:workspaceId/sessions/:sessionId/turns/:turnId", async (c) => {
5238
6769
  const workspaceId = c.req.param("workspaceId");
5239
- await requireAccessGrant12(c, deps, workspaceId, "sessions:control");
6770
+ await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
5240
6771
  const sessionId = c.req.param("sessionId");
5241
6772
  const turnId = c.req.param("turnId");
5242
6773
  await assertSessionExists(db, workspaceId, sessionId);
5243
6774
  await requireQueuedTurnForApi(db, workspaceId, sessionId, turnId);
5244
6775
  const turn = await cancelQueuedSessionTurn(db, workspaceId, turnId);
5245
- await appendAndPublishEvents4(db, bus, workspaceId, sessionId, [{
6776
+ await appendAndPublishEvents5(db, bus, workspaceId, sessionId, [{
5246
6777
  type: "turn.cancelled",
5247
6778
  turnId: turn.id,
5248
6779
  payload: { turnId: turn.id, triggerEventId: turn.triggerEventId }
@@ -5251,7 +6782,7 @@ function registerSessionRoutes(app, deps) {
5251
6782
  });
5252
6783
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/events", async (c) => {
5253
6784
  const workspaceId = c.req.param("workspaceId");
5254
- const grant = await requireAccessGrant12(c, deps, workspaceId, "sessions:control");
6785
+ const grant = await requireAccessGrant13(c, deps, workspaceId, "sessions:control");
5255
6786
  const sessionId = c.req.param("sessionId");
5256
6787
  const rawEvent = await c.req.json();
5257
6788
  const event = ClientSessionEvent.parse(rawEvent);
@@ -5268,19 +6799,19 @@ function registerSessionRoutes(app, deps) {
5268
6799
  });
5269
6800
  return c.json(accepted2, 202);
5270
6801
  }
5271
- const session = await requireSession2(db, workspaceId, sessionId);
6802
+ const session = await requireSession3(db, workspaceId, sessionId);
5272
6803
  if (event.type === "user.approvalDecision" && session.status !== "requires_action") {
5273
- 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` });
5274
6805
  }
5275
6806
  const eventsToAppend = [{
5276
6807
  type: event.type,
5277
6808
  payload: event.payload,
5278
6809
  ...event.clientEventId ? { clientEventId: event.clientEventId } : {}
5279
6810
  }];
5280
- const appended = await appendAndPublishEvents4(db, bus, workspaceId, sessionId, eventsToAppend);
6811
+ const appended = await appendAndPublishEvents5(db, bus, workspaceId, sessionId, eventsToAppend);
5281
6812
  const accepted = appended[0];
5282
6813
  if (!accepted) {
5283
- throw new HTTPException15(500, { message: "failed to append client event" });
6814
+ throw new HTTPException17(500, { message: "failed to append client event" });
5284
6815
  }
5285
6816
  const workflowId = workflowIdForSession2(sessionId);
5286
6817
  if (event.type === "user.approvalDecision") {
@@ -5298,7 +6829,7 @@ function registerSessionRoutes(app, deps) {
5298
6829
  });
5299
6830
  function assertOwnershipEnabled() {
5300
6831
  if (!settings.sandboxOwnershipEnabled) {
5301
- 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" });
5302
6833
  }
5303
6834
  }
5304
6835
  async function resolveSharedExposure(workspaceId, session) {
@@ -5308,12 +6839,12 @@ function registerSessionRoutes(app, deps) {
5308
6839
  }
5309
6840
  app.get("/v1/workspaces/:workspaceId/sessions/:sessionId/stream-capabilities", async (c) => {
5310
6841
  const workspaceId = c.req.param("workspaceId");
5311
- const grant = await requireAccessGrant12(c, deps, workspaceId, "sessions:read");
6842
+ const grant = await requireAccessGrant13(c, deps, workspaceId, "sessions:read");
5312
6843
  assertOwnershipEnabled();
5313
6844
  const sessionId = c.req.param("sessionId");
5314
6845
  const session = await getSession4(db, workspaceId, sessionId);
5315
6846
  if (!session) {
5316
- throw new HTTPException15(404, { message: "session not found" });
6847
+ throw new HTTPException17(404, { message: "session not found" });
5317
6848
  }
5318
6849
  const lease = await readGroupLease({ db, settings }, { workspaceId, sandboxGroupId: session.sandboxGroupId });
5319
6850
  const { shared, sharedSessionIds } = await resolveSharedExposure(workspaceId, session);
@@ -5403,16 +6934,16 @@ function registerSessionRoutes(app, deps) {
5403
6934
  });
5404
6935
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/stream-capabilities/acknowledge", async (c) => {
5405
6936
  const workspaceId = c.req.param("workspaceId");
5406
- const grant = await requireAccessGrant12(c, deps, workspaceId, "stream:acknowledge");
6937
+ const grant = await requireAccessGrant13(c, deps, workspaceId, "stream:acknowledge");
5407
6938
  assertOwnershipEnabled();
5408
6939
  const sessionId = c.req.param("sessionId");
5409
6940
  const session = await getSession4(db, workspaceId, sessionId);
5410
6941
  if (!session) {
5411
- throw new HTTPException15(404, { message: "session not found" });
6942
+ throw new HTTPException17(404, { message: "session not found" });
5412
6943
  }
5413
6944
  const parsed = AcknowledgeStreamRequest.safeParse(await c.req.json().catch(() => ({})));
5414
6945
  if (!parsed.success) {
5415
- throw new HTTPException15(400, { message: "invalid stream acknowledgment request" });
6946
+ throw new HTTPException17(400, { message: "invalid stream acknowledgment request" });
5416
6947
  }
5417
6948
  const recorded = await recordStreamAcknowledgment(db, {
5418
6949
  accountId: grant.accountId,
@@ -5426,26 +6957,26 @@ function registerSessionRoutes(app, deps) {
5426
6957
  });
5427
6958
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/viewers", async (c) => {
5428
6959
  const workspaceId = c.req.param("workspaceId");
5429
- const grant = await requireAccessGrant12(c, deps, workspaceId, "stream:view");
6960
+ const grant = await requireAccessGrant13(c, deps, workspaceId, "stream:view");
5430
6961
  assertOwnershipEnabled();
5431
6962
  const sessionId = c.req.param("sessionId");
5432
6963
  const session = await getSession4(db, workspaceId, sessionId);
5433
6964
  if (!session) {
5434
- throw new HTTPException15(404, { message: "session not found" });
6965
+ throw new HTTPException17(404, { message: "session not found" });
5435
6966
  }
5436
6967
  const parsed = AttachViewerRequest.safeParse(await c.req.json().catch(() => ({})));
5437
6968
  if (!parsed.success) {
5438
- throw new HTTPException15(400, { message: "invalid viewer attach request" });
6969
+ throw new HTTPException17(400, { message: "invalid viewer attach request" });
5439
6970
  }
5440
6971
  const wantDesktop = parsed.data.desktop ?? false;
5441
6972
  const { shared } = await resolveSharedExposure(workspaceId, session);
5442
6973
  if (wantDesktop) {
5443
6974
  const ack = await getStreamAcknowledgment(db, { workspaceId, sandboxGroupId: session.sandboxGroupId, subjectId: grant.subjectId });
5444
6975
  if (!ack?.acknowledgedUnredacted) {
5445
- throw new HTTPException15(409, { message: "stream_acknowledgment_required" });
6976
+ throw new HTTPException17(409, { message: "stream_acknowledgment_required" });
5446
6977
  }
5447
6978
  if (shared && !ack.acknowledgedShared) {
5448
- throw new HTTPException15(409, { message: "shared_acknowledgment_required" });
6979
+ throw new HTTPException17(409, { message: "shared_acknowledgment_required" });
5449
6980
  }
5450
6981
  }
5451
6982
  const activeSandbox = session.activeSandboxId ? await getSandbox2(db, workspaceId, session.activeSandboxId) : null;
@@ -5539,16 +7070,16 @@ function registerSessionRoutes(app, deps) {
5539
7070
  });
5540
7071
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/viewers/:viewerId/heartbeat", async (c) => {
5541
7072
  const workspaceId = c.req.param("workspaceId");
5542
- const grant = await requireAccessGrant12(c, deps, workspaceId, "stream:view");
7073
+ const grant = await requireAccessGrant13(c, deps, workspaceId, "stream:view");
5543
7074
  assertOwnershipEnabled();
5544
7075
  const sessionId = c.req.param("sessionId");
5545
7076
  const session = await getSession4(db, workspaceId, sessionId);
5546
7077
  if (!session) {
5547
- throw new HTTPException15(404, { message: "session not found" });
7078
+ throw new HTTPException17(404, { message: "session not found" });
5548
7079
  }
5549
7080
  const parsed = ViewerHeartbeatRequest.safeParse(await c.req.json().catch(() => ({})));
5550
7081
  if (!parsed.success) {
5551
- throw new HTTPException15(400, { message: "viewer heartbeat requires { leaseEpoch }" });
7082
+ throw new HTTPException17(400, { message: "viewer heartbeat requires { leaseEpoch }" });
5552
7083
  }
5553
7084
  const alive = await heartbeatViewer({ db, settings }, {
5554
7085
  accountId: grant.accountId,
@@ -5561,12 +7092,12 @@ function registerSessionRoutes(app, deps) {
5561
7092
  });
5562
7093
  app.delete("/v1/workspaces/:workspaceId/sessions/:sessionId/viewers/:viewerId", async (c) => {
5563
7094
  const workspaceId = c.req.param("workspaceId");
5564
- const grant = await requireAccessGrant12(c, deps, workspaceId, "stream:view");
7095
+ const grant = await requireAccessGrant13(c, deps, workspaceId, "stream:view");
5565
7096
  assertOwnershipEnabled();
5566
7097
  const sessionId = c.req.param("sessionId");
5567
7098
  const session = await getSession4(db, workspaceId, sessionId);
5568
7099
  if (!session) {
5569
- throw new HTTPException15(404, { message: "session not found" });
7100
+ throw new HTTPException17(404, { message: "session not found" });
5570
7101
  }
5571
7102
  await detachViewer({ db, settings }, {
5572
7103
  accountId: grant.accountId,
@@ -5578,12 +7109,12 @@ function registerSessionRoutes(app, deps) {
5578
7109
  });
5579
7110
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/viewers/:viewerId/revoke", async (c) => {
5580
7111
  const workspaceId = c.req.param("workspaceId");
5581
- const grant = await requireAccessGrant12(c, deps, workspaceId, "stream:view");
7112
+ const grant = await requireAccessGrant13(c, deps, workspaceId, "stream:view");
5582
7113
  assertOwnershipEnabled();
5583
7114
  const sessionId = c.req.param("sessionId");
5584
7115
  const session = await getSession4(db, workspaceId, sessionId);
5585
7116
  if (!session) {
5586
- throw new HTTPException15(404, { message: "session not found" });
7117
+ throw new HTTPException17(404, { message: "session not found" });
5587
7118
  }
5588
7119
  const result = await revokeViewer(db, {
5589
7120
  accountId: grant.accountId,
@@ -5596,12 +7127,12 @@ function registerSessionRoutes(app, deps) {
5596
7127
  });
5597
7128
  async function channelAPreamble(c, permission) {
5598
7129
  const workspaceId = c.req.param("workspaceId") ?? "";
5599
- const grant = await requireAccessGrant12(c, deps, workspaceId, permission);
7130
+ const grant = await requireAccessGrant13(c, deps, workspaceId, permission);
5600
7131
  assertOwnershipEnabled();
5601
7132
  const sessionId = c.req.param("sessionId") ?? "";
5602
7133
  const session = await getSession4(db, workspaceId, sessionId);
5603
7134
  if (!session) {
5604
- throw new HTTPException15(404, { message: "session not found" });
7135
+ throw new HTTPException17(404, { message: "session not found" });
5605
7136
  }
5606
7137
  return { accountId: grant.accountId, workspaceId, session, subjectId: grant.subjectId };
5607
7138
  }
@@ -5609,7 +7140,7 @@ function registerSessionRoutes(app, deps) {
5609
7140
  const raw = await c.req.json().catch(() => void 0);
5610
7141
  const result = schema.safeParse(raw ?? {});
5611
7142
  if (!result.success) {
5612
- throw new HTTPException15(400, { message: "invalid request body" });
7143
+ throw new HTTPException17(400, { message: "invalid request body" });
5613
7144
  }
5614
7145
  return result.data;
5615
7146
  }
@@ -5704,7 +7235,7 @@ function registerSessionRoutes(app, deps) {
5704
7235
  const delta = { ptyId, stream: "stdout", chunk: opened.initialOutput, seq: 0 };
5705
7236
  events.push({ type: "terminal.pty.output.delta", payload: delta });
5706
7237
  }
5707
- await appendAndPublishEvents4(db, bus, ctx.workspaceId, ctx.session.id, events);
7238
+ await appendAndPublishEvents5(db, bus, ctx.workspaceId, ctx.session.id, events);
5708
7239
  return opened.response;
5709
7240
  });
5710
7241
  return c.json(out, 201);
@@ -5714,10 +7245,10 @@ function registerSessionRoutes(app, deps) {
5714
7245
  const req = await parseChannelABody(c, PtyWriteRequest);
5715
7246
  const pty = await getOpenPtySession(db, ctx.workspaceId, req.ptyId);
5716
7247
  if (!pty) {
5717
- throw new HTTPException15(404, { message: "pty not found or closed" });
7248
+ throw new HTTPException17(404, { message: "pty not found or closed" });
5718
7249
  }
5719
7250
  if (pty.execSessionId === null) {
5720
- throw new HTTPException15(409, { message: "interactive terminal unsupported on this backend" });
7251
+ throw new HTTPException17(409, { message: "interactive terminal unsupported on this backend" });
5721
7252
  }
5722
7253
  let seq = 1;
5723
7254
  await withChannelA({ db, settings, bus }, ctx, async ({ service }) => {
@@ -5725,7 +7256,7 @@ function registerSessionRoutes(app, deps) {
5725
7256
  await updatePtySessionActivity(db, { accountId: ctx.accountId, workspaceId: ctx.workspaceId, ptyId: req.ptyId, execSessionId: pty.execSessionId });
5726
7257
  if (output) {
5727
7258
  const delta = { ptyId: req.ptyId, stream: "stdout", chunk: output, seq: seq++ };
5728
- 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 }]);
5729
7260
  }
5730
7261
  });
5731
7262
  return c.body(null, 204);
@@ -5735,7 +7266,7 @@ function registerSessionRoutes(app, deps) {
5735
7266
  const req = await parseChannelABody(c, PtyResizeRequest);
5736
7267
  const pty = await getOpenPtySession(db, ctx.workspaceId, req.ptyId);
5737
7268
  if (!pty) {
5738
- throw new HTTPException15(404, { message: "pty not found or closed" });
7269
+ throw new HTTPException17(404, { message: "pty not found or closed" });
5739
7270
  }
5740
7271
  if (pty.execSessionId !== null) {
5741
7272
  await withChannelA({ db, settings, bus }, ctx, ({ service }) => service.ptyResize(req, pty.execSessionId));
@@ -5751,7 +7282,7 @@ function registerSessionRoutes(app, deps) {
5751
7282
  await withChannelA({ db, settings, bus }, ctx, ({ service }) => service.ptyClose(req, pty.execSessionId));
5752
7283
  await closePtySession(db, { accountId: ctx.accountId, workspaceId: ctx.workspaceId, ptyId: req.ptyId });
5753
7284
  const exited = { ptyId: req.ptyId, exitCode: 0, reason: "exit" };
5754
- 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 }]);
5755
7286
  }
5756
7287
  return c.body(null, 204);
5757
7288
  });
@@ -5805,19 +7336,19 @@ import {
5805
7336
  listSocialConnections as listSocialConnections3,
5806
7337
  listSocialPosts as listSocialPosts2
5807
7338
  } from "@opengeni/db";
5808
- import { HTTPException as HTTPException16 } from "hono/http-exception";
7339
+ import { HTTPException as HTTPException18 } from "hono/http-exception";
5809
7340
  import { z as z2 } from "zod";
5810
- import { requireAccessGrant as requireAccessGrant13 } from "@opengeni/core";
7341
+ import { requireAccessGrant as requireAccessGrant14 } from "@opengeni/core";
5811
7342
  function registerSocialRoutes(app, deps) {
5812
7343
  const { db } = deps;
5813
7344
  app.get("/v1/workspaces/:workspaceId/social/connections", async (c) => {
5814
7345
  const workspaceId = c.req.param("workspaceId");
5815
- await requireAccessGrant13(c, deps, workspaceId, "workspace:read");
7346
+ await requireAccessGrant14(c, deps, workspaceId, "workspace:read");
5816
7347
  return c.json(await listSocialConnections3(db, workspaceId, boundedLimit(c.req.query("limit"))));
5817
7348
  });
5818
7349
  app.post("/v1/workspaces/:workspaceId/social/connections", async (c) => {
5819
7350
  const workspaceId = c.req.param("workspaceId");
5820
- const grant = await requireAccessGrant13(c, deps, workspaceId, "workspace:admin");
7351
+ const grant = await requireAccessGrant14(c, deps, workspaceId, "workspace:admin");
5821
7352
  const payload = CreateSocialConnectionRequest.parse(await c.req.json());
5822
7353
  try {
5823
7354
  return c.json(await createSocialConnection(db, {
@@ -5839,7 +7370,7 @@ function registerSocialRoutes(app, deps) {
5839
7370
  });
5840
7371
  app.get("/v1/workspaces/:workspaceId/social/posts", async (c) => {
5841
7372
  const workspaceId = c.req.param("workspaceId");
5842
- await requireAccessGrant13(c, deps, workspaceId, "workspace:read");
7373
+ await requireAccessGrant14(c, deps, workspaceId, "workspace:read");
5843
7374
  const since = parseSince(c.req.query("since"));
5844
7375
  const connectionIds = parseConnectionIds(c.req.query("connectionIds") ?? c.req.query("connectionId"));
5845
7376
  return c.json(await listSocialPosts2(db, {
@@ -5851,7 +7382,7 @@ function registerSocialRoutes(app, deps) {
5851
7382
  });
5852
7383
  app.post("/v1/workspaces/:workspaceId/social/posts", async (c) => {
5853
7384
  const workspaceId = c.req.param("workspaceId");
5854
- const grant = await requireAccessGrant13(c, deps, workspaceId, "workspace:admin");
7385
+ const grant = await requireAccessGrant14(c, deps, workspaceId, "workspace:admin");
5855
7386
  const payload = CreateSocialPostRequest.parse(await c.req.json());
5856
7387
  try {
5857
7388
  return c.json(await createSocialPost(db, {
@@ -5877,7 +7408,7 @@ function parseSince(raw) {
5877
7408
  }
5878
7409
  const since = new Date(raw);
5879
7410
  if (Number.isNaN(since.getTime())) {
5880
- 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" });
5881
7412
  }
5882
7413
  return since;
5883
7414
  }
@@ -5888,7 +7419,7 @@ function parseConnectionIds(raw) {
5888
7419
  const values = raw.split(",").map((value) => value.trim()).filter(Boolean);
5889
7420
  const parsed = z2.array(z2.string().uuid()).safeParse(values);
5890
7421
  if (!parsed.success) {
5891
- 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" });
5892
7423
  }
5893
7424
  const ids = parsed.data;
5894
7425
  return [...new Set(ids)];
@@ -5896,12 +7427,12 @@ function parseConnectionIds(raw) {
5896
7427
  function socialHttpException(error) {
5897
7428
  const message = error instanceof Error ? error.message : String(error);
5898
7429
  if (message.includes("not found")) {
5899
- return new HTTPException16(404, { message });
7430
+ return new HTTPException18(404, { message });
5900
7431
  }
5901
7432
  if (message.includes("duplicate key")) {
5902
- return new HTTPException16(409, { message: "social connection or post already exists" });
7433
+ return new HTTPException18(409, { message: "social connection or post already exists" });
5903
7434
  }
5904
- return new HTTPException16(500, { message });
7435
+ return new HTTPException18(500, { message });
5905
7436
  }
5906
7437
 
5907
7438
  // src/routes/workspaces.ts
@@ -5929,8 +7460,8 @@ import {
5929
7460
  requireWorkspace,
5930
7461
  updateWorkspace
5931
7462
  } from "@opengeni/db";
5932
- import { HTTPException as HTTPException17 } from "hono/http-exception";
5933
- 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";
5934
7465
  import { requireLimit as requireLimit7 } from "@opengeni/core";
5935
7466
  import { assertWorkspaceDeletable, assertWorkspaceMemberRemovable, resolveMemberSubjectId } from "@opengeni/core";
5936
7467
  function registerWorkspaceRoutes(app, deps) {
@@ -5939,7 +7470,7 @@ function registerWorkspaceRoutes(app, deps) {
5939
7470
  });
5940
7471
  app.get("/v1/workspaces", async (c) => {
5941
7472
  const context = await requireAccessContext2(c, deps);
5942
- 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))];
5943
7474
  if (readableWorkspaceIds.length > 0) {
5944
7475
  const workspaces = await Promise.all(readableWorkspaceIds.map((workspaceId) => requireWorkspace(deps.db, workspaceId)));
5945
7476
  return c.json(workspaces.map((workspace) => Workspace.parse(workspace)));
@@ -5951,7 +7482,7 @@ function registerWorkspaceRoutes(app, deps) {
5951
7482
  const payload = CreateWorkspaceRequest.parse(await c.req.json());
5952
7483
  const accountId = payload.accountId ?? context.defaultAccountId;
5953
7484
  if (!accountId) {
5954
- throw new HTTPException17(409, { message: "account selection is required" });
7485
+ throw new HTTPException19(409, { message: "account selection is required" });
5955
7486
  }
5956
7487
  requireAccountPermission(context, accountId, "workspace:create");
5957
7488
  await requireLimit7(deps, { accountId, action: "workspace:create", quantity: 1 });
@@ -5975,12 +7506,12 @@ function registerWorkspaceRoutes(app, deps) {
5975
7506
  });
5976
7507
  app.get("/v1/workspaces/:workspaceId", async (c) => {
5977
7508
  const workspaceId = c.req.param("workspaceId");
5978
- await requireAccessGrant14(c, deps, workspaceId, "workspace:read");
7509
+ await requireAccessGrant15(c, deps, workspaceId, "workspace:read");
5979
7510
  return c.json(Workspace.parse(await requireWorkspace(deps.db, workspaceId)));
5980
7511
  });
5981
7512
  app.patch("/v1/workspaces/:workspaceId", async (c) => {
5982
7513
  const workspaceId = c.req.param("workspaceId");
5983
- await requireAccessGrant14(c, deps, workspaceId, "workspace:admin");
7514
+ await requireAccessGrant15(c, deps, workspaceId, "workspace:admin");
5984
7515
  const payload = UpdateWorkspaceRequest.parse(await c.req.json());
5985
7516
  const workspace = await updateWorkspace(deps.db, workspaceId, {
5986
7517
  ...payload.name !== void 0 ? { name: payload.name.trim() } : {},
@@ -5991,7 +7522,7 @@ function registerWorkspaceRoutes(app, deps) {
5991
7522
  });
5992
7523
  app.delete("/v1/workspaces/:workspaceId", async (c) => {
5993
7524
  const workspaceId = c.req.param("workspaceId");
5994
- const grant = await requireAccessGrant14(c, deps, workspaceId, "workspace:admin");
7525
+ const grant = await requireAccessGrant15(c, deps, workspaceId, "workspace:admin");
5995
7526
  const [workspaceCountForAccount, activeSessionCount] = await Promise.all([
5996
7527
  countWorkspacesForAccount(deps.db, grant.accountId),
5997
7528
  countActiveSessionsForWorkspace(deps.db, workspaceId)
@@ -6006,13 +7537,13 @@ function registerWorkspaceRoutes(app, deps) {
6006
7537
  });
6007
7538
  app.get("/v1/workspaces/:workspaceId/members", async (c) => {
6008
7539
  const workspaceId = c.req.param("workspaceId");
6009
- await requireAccessGrant14(c, deps, workspaceId, "workspace:read");
7540
+ await requireAccessGrant15(c, deps, workspaceId, "workspace:read");
6010
7541
  const members = await listWorkspaceMembers(deps.db, workspaceId);
6011
7542
  return c.json(ListWorkspaceMembersResponse.parse({ members }));
6012
7543
  });
6013
7544
  app.post("/v1/workspaces/:workspaceId/members", async (c) => {
6014
7545
  const workspaceId = c.req.param("workspaceId");
6015
- const grant = await requireAccessGrant14(c, deps, workspaceId, "members:manage");
7546
+ const grant = await requireAccessGrant15(c, deps, workspaceId, "members:manage");
6016
7547
  const payload = AddWorkspaceMemberRequest.parse(await c.req.json());
6017
7548
  const email = payload.email.trim();
6018
7549
  const subjectId = resolveMemberSubjectId(await getManagedUserByEmail(deps.db, email));
@@ -6027,19 +7558,19 @@ function registerWorkspaceRoutes(app, deps) {
6027
7558
  const members = await listWorkspaceMembers(deps.db, workspaceId);
6028
7559
  const member = members.find((candidate) => candidate.subjectId === subjectId);
6029
7560
  if (!member) {
6030
- throw new HTTPException17(500, { message: "failed to add member" });
7561
+ throw new HTTPException19(500, { message: "failed to add member" });
6031
7562
  }
6032
7563
  return c.json(WorkspaceMember.parse(member), 201);
6033
7564
  });
6034
7565
  app.patch("/v1/workspaces/:workspaceId/members/:subjectId", async (c) => {
6035
7566
  const workspaceId = c.req.param("workspaceId");
6036
- const grant = await requireAccessGrant14(c, deps, workspaceId, "members:manage");
7567
+ const grant = await requireAccessGrant15(c, deps, workspaceId, "members:manage");
6037
7568
  const subjectId = decodeURIComponent(c.req.param("subjectId"));
6038
7569
  const payload = UpdateWorkspaceMemberRequest.parse(await c.req.json());
6039
7570
  const existing = await listWorkspaceMembers(deps.db, workspaceId);
6040
7571
  const current = existing.find((member2) => member2.subjectId === subjectId);
6041
7572
  if (!current) {
6042
- throw new HTTPException17(404, { message: "member not found" });
7573
+ throw new HTTPException19(404, { message: "member not found" });
6043
7574
  }
6044
7575
  await grantWorkspaceAccess(deps.db, {
6045
7576
  accountId: grant.accountId,
@@ -6052,13 +7583,13 @@ function registerWorkspaceRoutes(app, deps) {
6052
7583
  const members = await listWorkspaceMembers(deps.db, workspaceId);
6053
7584
  const member = members.find((candidate) => candidate.subjectId === subjectId);
6054
7585
  if (!member) {
6055
- throw new HTTPException17(500, { message: "failed to update member" });
7586
+ throw new HTTPException19(500, { message: "failed to update member" });
6056
7587
  }
6057
7588
  return c.json(WorkspaceMember.parse(member));
6058
7589
  });
6059
7590
  app.delete("/v1/workspaces/:workspaceId/members/:subjectId", async (c) => {
6060
7591
  const workspaceId = c.req.param("workspaceId");
6061
- const grant = await requireAccessGrant14(c, deps, workspaceId, "members:manage");
7592
+ const grant = await requireAccessGrant15(c, deps, workspaceId, "members:manage");
6062
7593
  const subjectId = decodeURIComponent(c.req.param("subjectId"));
6063
7594
  const members = await listWorkspaceMembers(deps.db, workspaceId);
6064
7595
  assertWorkspaceMemberRemovable({ members, subjectId, callerSubjectId: grant.subjectId });
@@ -6076,7 +7607,7 @@ function normalizeAgentInstructions(value) {
6076
7607
  function requireAccountPermission(context, accountId, permission) {
6077
7608
  const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId);
6078
7609
  if (!grant || !grant.permissions.includes(permission) && !grant.permissions.includes("account:admin")) {
6079
- throw new HTTPException17(403, { message: `missing permission: ${permission}` });
7610
+ throw new HTTPException19(403, { message: `missing permission: ${permission}` });
6080
7611
  }
6081
7612
  }
6082
7613
 
@@ -6103,7 +7634,7 @@ function createApp(deps) {
6103
7634
  const documentIndexer = deps.documentIndexer ?? {
6104
7635
  indexDocument: async ({ accountId, workspaceId, documentId }) => {
6105
7636
  if (!objectStorage) {
6106
- throw new HTTPException18(503, { message: "object storage is not configured" });
7637
+ throw new HTTPException20(503, { message: "object storage is not configured" });
6107
7638
  }
6108
7639
  return await indexDocumentNow(deps.db, objectStorage, workspaceId, documentId, getDocumentServices(), {
6109
7640
  beforeEmbed: async ({ chunkCount }) => {
@@ -6194,13 +7725,19 @@ function createApp(deps) {
6194
7725
  service: deps.settings.serviceName,
6195
7726
  environment: deps.settings.environment,
6196
7727
  deploymentRevision: deps.settings.deploymentRevision,
7728
+ ...deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {},
6197
7729
  ok: true
6198
7730
  }));
6199
- app.get("/metrics", (c) => c.text(observability.prometheusMetrics(), 200, {
7731
+ app.get("/readyz", async (c) => {
7732
+ const result = await runReadinessChecks(readinessChecks(deps), 2e3);
7733
+ return c.json(result, result.ok ? 200 : 503);
7734
+ });
7735
+ app.get("/metrics", async (c) => c.text(await observability.prometheusMetrics(), 200, {
6200
7736
  "content-type": "text/plain; version=0.0.4; charset=utf-8"
6201
7737
  }));
6202
7738
  app.get("/v1/config/client", (c) => c.json(ClientConfig.parse({
6203
7739
  deploymentRevision: deps.settings.deploymentRevision,
7740
+ ...deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {},
6204
7741
  defaultModel: deps.settings.openaiModel,
6205
7742
  allowedModels: configuredAllowedModels(deps.settings),
6206
7743
  // Provider-grouped model list for the picker. configuredModels() carries the
@@ -6234,11 +7771,19 @@ function createApp(deps) {
6234
7771
  })));
6235
7772
  app.all("/v1/workspaces/:workspaceId/mcp", async (c) => {
6236
7773
  const workspaceId = c.req.param("workspaceId");
6237
- 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;
6238
7776
  const transport = new WebStandardStreamableHTTPServerTransport2({ enableJsonResponse: true });
6239
- const mcp = buildOpenGeniMcpServer(routeDeps, grant, { requestOrigin: new URL(c.req.url).origin });
6240
- await mcp.connect(transport);
6241
- 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
+ }
6242
7787
  });
6243
7788
  registerFileRoutes(app, routeDeps);
6244
7789
  registerApiKeyRoutes(app, routeDeps);
@@ -6248,6 +7793,7 @@ function createApp(deps) {
6248
7793
  registerInstallRoutes(app, routeDeps);
6249
7794
  registerWorkspaceRoutes(app, routeDeps);
6250
7795
  registerSocialRoutes(app, routeDeps);
7796
+ registerConnectionRoutes(app, routeDeps);
6251
7797
  registerCapabilityRoutes(app, routeDeps);
6252
7798
  registerEnrollmentRoutes(app, routeDeps);
6253
7799
  registerMachineRoutes(app, routeDeps);
@@ -6258,6 +7804,17 @@ function createApp(deps) {
6258
7804
  registerCodexRoutes(app, routeDeps);
6259
7805
  return app;
6260
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
+ }
6261
7818
  function clientAuthConfig(settings) {
6262
7819
  if (settings.productAccessMode === "managed") {
6263
7820
  return { mode: "managedSession", session: "cookie" };
@@ -6278,13 +7835,61 @@ function allowedCorsOrigin(pattern, origin) {
6278
7835
  return new RegExp(`^(?:${pattern})$`).test(origin);
6279
7836
  }
6280
7837
  function httpStatusForError(error) {
6281
- if (error instanceof HTTPException18) {
7838
+ if (error instanceof HTTPException20) {
6282
7839
  return error.status;
6283
7840
  }
6284
7841
  return 500;
6285
7842
  }
7843
+ function readinessChecks(deps) {
7844
+ return {
7845
+ db: deps.readinessChecks?.db ?? (async () => {
7846
+ await deps.db.execute(dbSql`select 1`);
7847
+ }),
7848
+ nats: deps.readinessChecks?.nats ?? (() => {
7849
+ if (deps.bus.isConnected && !deps.bus.isConnected()) {
7850
+ throw new Error("NATS is not connected");
7851
+ }
7852
+ }),
7853
+ temporal: deps.readinessChecks?.temporal ?? deps.workflowClient.check ?? (() => {
7854
+ throw new Error("Temporal readiness check unavailable");
7855
+ })
7856
+ };
7857
+ }
7858
+ async function runReadinessChecks(checks, timeoutMs) {
7859
+ const entries = await Promise.all(
7860
+ Object.entries(checks).map(async ([name, check]) => {
7861
+ try {
7862
+ await withTimeout(Promise.resolve().then(check), timeoutMs);
7863
+ return [name, { ok: true }];
7864
+ } catch (error) {
7865
+ return [name, { ok: false, error: error instanceof Error ? error.message : String(error) }];
7866
+ }
7867
+ })
7868
+ );
7869
+ const result = Object.fromEntries(entries);
7870
+ return {
7871
+ ok: Object.values(result).every((check) => check.ok),
7872
+ checks: result
7873
+ };
7874
+ }
7875
+ async function withTimeout(promise, timeoutMs) {
7876
+ let timer;
7877
+ try {
7878
+ return await Promise.race([
7879
+ promise,
7880
+ new Promise((_, reject) => {
7881
+ timer = setTimeout(() => reject(new Error(`readiness check timed out after ${timeoutMs}ms`)), timeoutMs);
7882
+ })
7883
+ ]);
7884
+ } finally {
7885
+ if (timer) {
7886
+ clearTimeout(timer);
7887
+ }
7888
+ }
7889
+ }
6286
7890
  var routeLabelPatterns = [
6287
7891
  { pattern: /^\/healthz$/, label: "/healthz" },
7892
+ { pattern: /^\/readyz$/, label: "/readyz" },
6288
7893
  { pattern: /^\/v1\/workspaces\/[^/]+\/codex\/connect\/start$/, label: "/v1/workspaces/:workspaceId/codex/connect/start" },
6289
7894
  { pattern: /^\/v1\/workspaces\/[^/]+\/codex\/connect\/poll$/, label: "/v1/workspaces/:workspaceId/codex/connect/poll" },
6290
7895
  { pattern: /^\/v1\/workspaces\/[^/]+\/codex\/status$/, label: "/v1/workspaces/:workspaceId/codex/status" },
@@ -6329,6 +7934,9 @@ var routeLabelPatterns = [
6329
7934
  { pattern: /^\/v1\/workspaces\/[^/]+\/document-bases\/[^/]+\/documents$/, label: "/v1/workspaces/:workspaceId/document-bases/:id/documents" },
6330
7935
  { pattern: /^\/v1\/workspaces\/[^/]+\/document-bases\/[^/]+\/search$/, label: "/v1/workspaces/:workspaceId/document-bases/:id/search" },
6331
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" },
6332
7940
  { pattern: /^\/v1\/workspaces\/[^/]+\/github\/app$/, label: "/v1/workspaces/:workspaceId/github/app" },
6333
7941
  { pattern: /^\/v1\/workspaces\/[^/]+\/github\/repositories$/, label: "/v1/workspaces/:workspaceId/github/repositories" },
6334
7942
  { pattern: /^\/v1\/workspaces\/[^/]+\/github\/repositories\/sync$/, label: "/v1/workspaces/:workspaceId/github/repositories/sync" },
@@ -6347,6 +7955,11 @@ var routeLabelPatterns = [
6347
7955
  { pattern: /^\/v1\/workspaces\/[^/]+\/packs\/[^/]+$/, label: "/v1/workspaces/:workspaceId/packs/:id" },
6348
7956
  { pattern: /^\/v1\/workspaces\/[^/]+\/social\/connections$/, label: "/v1/workspaces/:workspaceId/social/connections" },
6349
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" },
6350
7963
  { pattern: /^\/v1\/enrollments\/device\/start$/, label: "/v1/enrollments/device/start" },
6351
7964
  { pattern: /^\/v1\/enrollments\/device\/poll$/, label: "/v1/enrollments/device/poll" },
6352
7965
  { pattern: /^\/v1\/workspaces\/[^/]+\/enrollments\/device\/approve$/, label: "/v1/workspaces/:workspaceId/enrollments/device/approve" },
@@ -6384,4 +7997,4 @@ export {
6384
7997
  withDefaultEnabledCapabilityMcpTools,
6385
7998
  workflowIdForSession3 as workflowIdForSession
6386
7999
  };
6387
- //# sourceMappingURL=chunk-I2EDWHVF.js.map
8000
+ //# sourceMappingURL=chunk-DQ5TIRDZ.js.map