@opengeni/api-router 0.23.1 → 0.26.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/app.d.ts +2 -2
  2. package/dist/app.js +1 -1
  3. package/dist/browser-controller-authority.d.ts +43 -0
  4. package/dist/browser-state-authority.d.ts +27 -0
  5. package/dist/{chunk-T4T2PGU4.js → chunk-JIKNR5YL.js} +26181 -14408
  6. package/dist/chunk-JIKNR5YL.js.map +1 -0
  7. package/dist/codemode.d.ts +23 -0
  8. package/dist/editable-artifact-live-hints.d.ts +11 -0
  9. package/dist/editable-artifact-native-kernel.d.ts +37 -0
  10. package/dist/editable-artifact-office-import.d.ts +22 -0
  11. package/dist/editable-artifact-production.d.ts +29 -0
  12. package/dist/editable-artifact-websocket.d.ts +49 -0
  13. package/dist/editable-artifact-workspace-files.d.ts +23 -0
  14. package/dist/github-browser-flow.d.ts +6 -0
  15. package/dist/http/cors.d.ts +1 -0
  16. package/dist/index.d.ts +3 -2
  17. package/dist/index.js +1796 -22
  18. package/dist/index.js.map +1 -1
  19. package/dist/integrations/api-integrations.d.ts +24 -0
  20. package/dist/integrations/atlassian.d.ts +176 -0
  21. package/dist/integrations/github-skill-source.d.ts +5 -0
  22. package/dist/integrations/google-drive.d.ts +85 -0
  23. package/dist/integrations/oauth-client.d.ts +22 -1
  24. package/dist/integrations/provider-oauth.d.ts +19 -0
  25. package/dist/integrations/slack-bot.d.ts +4 -0
  26. package/dist/integrations/slack-interactions.d.ts +8 -2
  27. package/dist/integrations/social-api.d.ts +2 -1
  28. package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
  29. package/dist/mcp/editable-artifacts.d.ts +13 -0
  30. package/dist/mcp/scheduled-task-view.d.ts +168 -0
  31. package/dist/mcp/server.d.ts +14 -3
  32. package/dist/memory-slack-delivery.d.ts +9 -0
  33. package/dist/routes/api-integrations.d.ts +8 -0
  34. package/dist/routes/browser-identities.d.ts +5 -0
  35. package/dist/routes/browser-sessions.d.ts +6 -0
  36. package/dist/routes/company-profile.d.ts +3 -0
  37. package/dist/routes/computer-sessions.d.ts +6 -0
  38. package/dist/routes/editable-artifacts.d.ts +44 -0
  39. package/dist/routes/integration-features.d.ts +3 -0
  40. package/dist/routes/memory-slack-publications.d.ts +6 -0
  41. package/dist/routes/plugins.d.ts +8 -0
  42. package/dist/routes/sessions.d.ts +17 -2
  43. package/dist/routes/skills.d.ts +6 -0
  44. package/dist/routes/video-generation.d.ts +3 -0
  45. package/dist/sandbox/channel-a.d.ts +55 -2
  46. package/dist/sandbox/metrics-ingestion.d.ts +6 -1
  47. package/dist/sandbox/viewer.d.ts +4 -2
  48. package/dist/temporal-schedule-cleanup.d.ts +26 -0
  49. package/package.json +19 -14
  50. package/src/app.ts +274 -37
  51. package/src/browser-controller-authority.ts +137 -0
  52. package/src/browser-state-authority.ts +236 -0
  53. package/src/codemode.ts +186 -0
  54. package/src/editable-artifact-live-hints.ts +64 -0
  55. package/src/editable-artifact-native-kernel.ts +659 -0
  56. package/src/editable-artifact-office-import.ts +230 -0
  57. package/src/editable-artifact-production.ts +419 -0
  58. package/src/editable-artifact-websocket.ts +311 -0
  59. package/src/editable-artifact-workspace-files.ts +186 -0
  60. package/src/github-browser-flow.ts +35 -6
  61. package/src/http/auth.ts +2 -0
  62. package/src/http/cors.ts +3 -0
  63. package/src/index.ts +119 -20
  64. package/src/integrations/api-integrations.ts +350 -0
  65. package/src/integrations/atlassian.ts +1621 -0
  66. package/src/integrations/github-skill-source.ts +142 -0
  67. package/src/integrations/google-drive.ts +1000 -64
  68. package/src/integrations/oauth-client.ts +123 -33
  69. package/src/integrations/provider-oauth.ts +777 -0
  70. package/src/integrations/slack-bot.ts +31 -2
  71. package/src/integrations/slack-interactions.ts +487 -27
  72. package/src/integrations/social-api.ts +11 -0
  73. package/src/mcp/documents.ts +32 -1
  74. package/src/mcp/editable-artifact-query-schema.ts +236 -0
  75. package/src/mcp/editable-artifacts.ts +448 -0
  76. package/src/mcp/scheduled-task-view.ts +34 -0
  77. package/src/mcp/server.ts +923 -145
  78. package/src/memory-slack-delivery.ts +209 -0
  79. package/src/routes/api-integrations.ts +407 -0
  80. package/src/routes/browser-identities.ts +136 -0
  81. package/src/routes/browser-sessions.ts +2543 -0
  82. package/src/routes/company-profile.ts +255 -0
  83. package/src/routes/computer-sessions.ts +1247 -0
  84. package/src/routes/connections.ts +284 -99
  85. package/src/routes/documents.ts +22 -3
  86. package/src/routes/editable-artifacts.ts +1159 -0
  87. package/src/routes/files.ts +106 -4
  88. package/src/routes/github.ts +18 -2
  89. package/src/routes/install.ts +7 -1
  90. package/src/routes/integration-features.ts +258 -0
  91. package/src/routes/memory-slack-publications.ts +216 -0
  92. package/src/routes/packs.ts +437 -7
  93. package/src/routes/plugins.ts +751 -0
  94. package/src/routes/rigs.ts +77 -20
  95. package/src/routes/scheduled-tasks.ts +67 -40
  96. package/src/routes/sessions.ts +421 -245
  97. package/src/routes/skills.ts +174 -0
  98. package/src/routes/video-generation.ts +132 -0
  99. package/src/routes/workspaces.ts +46 -24
  100. package/src/sandbox/channel-a.ts +723 -116
  101. package/src/sandbox/metrics-ingestion.ts +121 -3
  102. package/src/sandbox/rematerialize.ts +35 -47
  103. package/src/sandbox/viewer.ts +29 -9
  104. package/src/temporal-schedule-cleanup.ts +135 -0
  105. package/dist/chunk-T4T2PGU4.js.map +0 -1
  106. package/dist/mcp/toolspace.d.ts +0 -71
  107. package/src/mcp/toolspace.ts +0 -1190
package/src/app.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  } from "@opengeni/config";
8
8
  import {
9
9
  ClientConfig,
10
+ CodemodeCallRequest,
10
11
  ErrorEnvelope,
11
12
  OPENGENI_API_CONTRACT_HEADER,
12
13
  OPENGENI_API_CONTRACT_REVISION,
@@ -23,7 +24,17 @@ import {
23
24
  indexDocumentNow,
24
25
  type DocumentServices,
25
26
  } from "@opengeni/documents";
26
- import { dbSql, getWorkspace, rlsContextForWorkspace } from "@opengeni/db";
27
+ import {
28
+ CodemodeCallBudgetExceededError,
29
+ CodemodeOperationConflictError,
30
+ CodemodeOperationNotExecutableError,
31
+ CodemodePayloadTooLargeError,
32
+ CodemodeToolApprovalRequiredError,
33
+ CodemodeToolNotInCatalogError,
34
+ dbSql,
35
+ getWorkspace,
36
+ rlsContextForWorkspace,
37
+ } from "@opengeni/db";
27
38
  import { createObservability } from "@opengeni/observability";
28
39
  import { createObjectStorage } from "@opengeni/storage";
29
40
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
@@ -48,9 +59,16 @@ import { createManagedAuth } from "./auth/managed-auth";
48
59
  import { createApiSandboxClient, makeResumeBoxById } from "./sandbox/access";
49
60
  import { requireLimit } from "@opengeni/core";
50
61
  import { buildOpenGeniMcpServer } from "./mcp/server";
51
- import { isToolspaceGrant, prepareToolspaceMcpSurface } from "./mcp/toolspace";
62
+ import {
63
+ CodemodeAuthorityError,
64
+ isCodemodeGrant,
65
+ readCodemodeOperation,
66
+ requireActiveCodemodeCatalog,
67
+ submitAndDispatchCodemodeCall,
68
+ } from "./codemode";
52
69
  import { boundedMcpRequest, McpPayloadTooLargeError } from "@opengeni/runtime/mcp-network";
53
70
  import { requireAccessKey } from "./http/auth";
71
+ import { allowedCorsOrigin } from "./http/cors";
54
72
  import { registerCapabilityRoutes } from "./routes/capabilities";
55
73
  import { registerCatalogAssetRoutes } from "./routes/catalog-assets";
56
74
  import { registerCodexRoutes } from "./routes/codex";
@@ -58,29 +76,42 @@ import { registerConnectionRoutes } from "./routes/connections";
58
76
  import { registerDocumentRoutes } from "./routes/documents";
59
77
  import { registerEnrollmentRoutes } from "./routes/enrollments";
60
78
  import { registerMachineRoutes } from "./routes/machines";
79
+ import { registerMemorySlackPublicationRoutes } from "./routes/memory-slack-publications";
61
80
  import { registerEnvironmentRoutes } from "./routes/environments";
62
81
  import { registerFileRoutes } from "./routes/files";
63
82
  import { registerApiKeyRoutes } from "./routes/api-keys";
64
83
  import { registerBillingRoutes } from "./routes/billing";
84
+ import { registerBrowserIdentityRoutes } from "./routes/browser-identities";
85
+ import { registerBrowserSessionRoutes } from "./routes/browser-sessions";
86
+ import { registerComputerSessionRoutes } from "./routes/computer-sessions";
65
87
  import { registerGitHubRoutes } from "./routes/github";
66
88
  import { registerInstallRoutes } from "./routes/install";
89
+ import { registerApiIntegrationRoutes } from "./routes/api-integrations";
90
+ import { registerIntegrationFeatureRoutes } from "./routes/integration-features";
67
91
  import { registerPackRoutes } from "./routes/packs";
92
+ import { registerPluginRoutes } from "./routes/plugins";
93
+ import { registerSkillRoutes } from "./routes/skills";
68
94
  import { registerRigRoutes } from "./routes/rigs";
69
95
  import { registerScheduledTaskRoutes } from "./routes/scheduled-tasks";
70
96
  import { registerSessionRoutes } from "./routes/sessions";
71
97
  import { registerSocialRoutes } from "./routes/social";
72
98
  import { registerWorkspaceRoutes } from "./routes/workspaces";
73
99
  import { registerWorkspaceInstructionPolicyRoutes } from "./routes/workspace-instruction-policies";
100
+ import { registerCompanyProfileRoutes } from "./routes/company-profile";
74
101
  import { registerWorkspaceStateRoutes } from "./routes/workspace-state";
75
102
  import { registerWorkspaceArtifactRoutes } from "./routes/workspace-artifacts";
76
103
  import { registerPreferenceRegistryRoutes } from "./routes/preference-registry";
77
104
  import { registerInsightsRoutes } from "./routes/insights";
78
105
  import { registerTranscriptionRoutes } from "./routes/transcriptions";
106
+ import { registerEditableArtifactRoutes } from "./routes/editable-artifacts";
107
+ import { registerVideoGenerationRoutes } from "./routes/video-generation";
79
108
  import { projectClientModel } from "./model-catalog";
80
109
  import { createTranscriptionService } from "./transcription/service";
81
110
  import { createFfmpegTranscriptionSegmenter } from "./transcription/segmenter";
82
111
  import { registerSlackInteractionRoutes } from "./integrations/slack-interactions";
83
112
 
113
+ export { allowedCorsOrigin } from "./http/cors";
114
+
84
115
  export type {
85
116
  ApiRouteDeps,
86
117
  AppDependencies,
@@ -249,12 +280,18 @@ export function createAppComposition(deps: AppDependencies): {
249
280
  "Accept",
250
281
  "Authorization",
251
282
  "Content-Type",
283
+ "Range",
252
284
  "X-OpenGeni-Access-Key",
253
285
  "X-OpenGeni-Api-Contract",
254
286
  "X-OpenGeni-Correlation-Id",
255
287
  "X-OpenGeni-Subject",
256
288
  ],
257
- exposeHeaders: ["X-OpenGeni-Api-Contract", "X-OpenGeni-Correlation-Id"],
289
+ exposeHeaders: [
290
+ "Accept-Ranges",
291
+ "Content-Range",
292
+ "X-OpenGeni-Api-Contract",
293
+ "X-OpenGeni-Correlation-Id",
294
+ ],
258
295
  };
259
296
  const publicApiCors = cors({
260
297
  ...corsHeaders,
@@ -499,17 +536,13 @@ export function createAppComposition(deps: AppDependencies): {
499
536
  throw error;
500
537
  }
501
538
  const grant = await requireMcpAccessGrant(c, routeDeps, workspaceId);
502
- const toolspaceGrant = isToolspaceGrant(routeDeps.settings, grant);
503
539
  const boundSessionId = grant.metadata?.sessionId;
504
- if (toolspaceGrant || typeof boundSessionId === "string") {
505
- if (typeof boundSessionId !== "string") {
506
- throw new HTTPException(404, { message: "session not found" });
507
- }
540
+ if (typeof boundSessionId === "string") {
508
541
  try {
509
542
  await requireSessionAuthorization(routeDeps, grant, {
510
543
  sessionId: boundSessionId,
511
- operation: toolspaceGrant ? "session.toolspace.call" : "session.first_party_mcp.call",
512
- surface: toolspaceGrant ? "toolspace" : "first_party_mcp",
544
+ operation: "session.first_party_mcp.call",
545
+ surface: "first_party_mcp",
513
546
  });
514
547
  } catch (error) {
515
548
  if (error instanceof SessionAuthorizationDeniedError) {
@@ -523,22 +556,6 @@ export function createAppComposition(deps: AppDependencies): {
523
556
  throw error;
524
557
  }
525
558
  }
526
- let toolspace: Awaited<ReturnType<typeof prepareToolspaceMcpSurface>> = null;
527
- if (toolspaceGrant) {
528
- try {
529
- toolspace = await prepareToolspaceMcpSurface({
530
- deps: routeDeps,
531
- grant,
532
- });
533
- } catch (error) {
534
- if (error instanceof McpPayloadTooLargeError) {
535
- throw new HTTPException(413, {
536
- message: "MCP tool list exceeds the safety limit",
537
- });
538
- }
539
- throw error;
540
- }
541
- }
542
559
  const workspace = await getWorkspace(routeDeps.db, workspaceId);
543
560
  const workspaceMemoryEnabled = resolveWorkspaceMemoryEnabled(workspace?.settings);
544
561
  const transport = new WebStandardStreamableHTTPServerTransport({
@@ -546,42 +563,101 @@ export function createAppComposition(deps: AppDependencies): {
546
563
  });
547
564
  const mcp = buildOpenGeniMcpServer(routeDeps, grant, {
548
565
  requestOrigin: new URL(c.req.url).origin,
549
- toolspace,
550
566
  workspaceMemoryEnabled,
551
567
  });
568
+ await mcp.connect(transport);
569
+ return await transport.handleRequest(boundedRequest);
570
+ });
571
+
572
+ app.get("/v1/workspaces/:workspaceId/codemode/catalog", async (c) => {
573
+ const workspaceId = c.req.param("workspaceId");
574
+ const grant = await requireAccessGrant(c, routeDeps, workspaceId);
575
+ if (!isCodemodeGrant(grant)) {
576
+ throw new HTTPException(403, { message: "Codemode access denied" });
577
+ }
578
+ try {
579
+ return c.json((await requireActiveCodemodeCatalog(routeDeps, grant)).catalog);
580
+ } catch (error) {
581
+ throw codemodeHttpError(error);
582
+ }
583
+ });
584
+
585
+ app.post("/v1/workspaces/:workspaceId/codemode/calls", async (c) => {
586
+ const workspaceId = c.req.param("workspaceId");
587
+ const grant = await requireAccessGrant(c, routeDeps, workspaceId);
588
+ if (!isCodemodeGrant(grant)) {
589
+ throw new HTTPException(403, { message: "Codemode access denied" });
590
+ }
591
+ const parsed = CodemodeCallRequest.safeParse(await c.req.json().catch(() => null));
592
+ if (!parsed.success) {
593
+ throw new HTTPException(400, { message: "Invalid Codemode call" });
594
+ }
595
+ try {
596
+ const submission = await submitAndDispatchCodemodeCall(routeDeps, grant, parsed.data);
597
+ const terminal = ["completed", "failed", "outcome_unknown", "cancelled"].includes(
598
+ submission.operation.state,
599
+ );
600
+ return c.json(submission, terminal ? 200 : 202);
601
+ } catch (error) {
602
+ throw codemodeHttpError(error);
603
+ }
604
+ });
605
+
606
+ app.get("/v1/workspaces/:workspaceId/codemode/calls/:operationId", async (c) => {
607
+ const workspaceId = c.req.param("workspaceId");
608
+ const grant = await requireAccessGrant(c, routeDeps, workspaceId);
609
+ if (!isCodemodeGrant(grant)) {
610
+ throw new HTTPException(403, { message: "Codemode access denied" });
611
+ }
552
612
  try {
553
- await mcp.connect(transport);
554
- return await transport.handleRequest(boundedRequest);
555
- } finally {
556
- await toolspace?.close().catch(() => undefined);
613
+ const operation = await readCodemodeOperation(routeDeps, grant, c.req.param("operationId"));
614
+ if (!operation)
615
+ throw new HTTPException(404, {
616
+ message: "Codemode operation not found",
617
+ });
618
+ return c.json(operation);
619
+ } catch (error) {
620
+ if (error instanceof HTTPException) throw error;
621
+ throw codemodeHttpError(error);
557
622
  }
558
623
  });
559
624
 
560
625
  registerFileRoutes(app, routeDeps);
561
626
  registerApiKeyRoutes(app, routeDeps);
562
627
  registerBillingRoutes(app, routeDeps);
628
+ registerBrowserIdentityRoutes(app, routeDeps);
629
+ registerBrowserSessionRoutes(app, routeDeps);
630
+ registerComputerSessionRoutes(app, routeDeps);
563
631
  registerDocumentRoutes(app, routeDeps);
564
632
  registerGitHubRoutes(app, routeDeps);
565
633
  registerInstallRoutes(app, routeDeps);
566
634
  registerWorkspaceRoutes(app, routeDeps);
567
635
  registerInsightsRoutes(app, routeDeps);
568
636
  registerWorkspaceInstructionPolicyRoutes(app, routeDeps);
637
+ registerCompanyProfileRoutes(app, routeDeps);
569
638
  registerWorkspaceStateRoutes(app, routeDeps);
639
+ registerMemorySlackPublicationRoutes(app, routeDeps);
570
640
  registerWorkspaceArtifactRoutes(app, routeDeps);
571
641
  registerPreferenceRegistryRoutes(app, routeDeps);
572
642
  registerSocialRoutes(app, routeDeps);
573
643
  registerConnectionRoutes(app, routeDeps);
574
644
  registerCapabilityRoutes(app, routeDeps);
645
+ registerApiIntegrationRoutes(app, routeDeps);
646
+ registerIntegrationFeatureRoutes(app, routeDeps);
575
647
  registerCatalogAssetRoutes(app, routeDeps);
576
648
  registerEnrollmentRoutes(app, routeDeps);
577
649
  registerMachineRoutes(app, routeDeps);
578
650
  registerEnvironmentRoutes(app, routeDeps);
579
651
  registerRigRoutes(app, routeDeps);
580
652
  registerPackRoutes(app, routeDeps);
653
+ registerPluginRoutes(app, routeDeps);
654
+ registerSkillRoutes(app, routeDeps);
581
655
  registerSessionRoutes(app, routeDeps);
582
656
  registerScheduledTaskRoutes(app, routeDeps);
583
657
  registerCodexRoutes(app, routeDeps);
584
658
  registerTranscriptionRoutes(app, routeDeps);
659
+ registerEditableArtifactRoutes(app, routeDeps);
660
+ registerVideoGenerationRoutes(app, routeDeps);
585
661
  registerSlackInteractionRoutes(app, routeDeps);
586
662
 
587
663
  app.notFound((c) => {
@@ -653,8 +729,8 @@ async function requireMcpAccessGrant(
653
729
  if (hasPermission(grant.permissions, "workspace:read")) {
654
730
  return grant;
655
731
  }
656
- if (isToolspaceGrant(deps.settings, grant)) {
657
- return grant;
732
+ if (isCodemodeGrant(grant)) {
733
+ requirePermission(grant, "workspace:read");
658
734
  }
659
735
  // A worker-signed session-bound grant is allowed to reach the transport
660
736
  // without inheriting broad workspace read access. The exact session
@@ -687,6 +763,48 @@ function clientAuthConfig(settings: AppDependencies["settings"]) {
687
763
  return { mode: "none" as const };
688
764
  }
689
765
 
766
+ function codemodeHttpError(error: unknown): HTTPException {
767
+ if (error instanceof SessionAuthorizationDeniedError) {
768
+ return new HTTPException(404, {
769
+ message: "session not found",
770
+ cause: error,
771
+ });
772
+ }
773
+ if (error instanceof SessionAuthorizationUnavailableError) {
774
+ return new HTTPException(503, {
775
+ message: "session authorization is unavailable",
776
+ cause: error,
777
+ });
778
+ }
779
+ if (
780
+ error instanceof CodemodeAuthorityError ||
781
+ error instanceof CodemodeOperationNotExecutableError
782
+ ) {
783
+ return new HTTPException(409, { message: error.message, cause: error });
784
+ }
785
+ if (error instanceof CodemodeOperationConflictError) {
786
+ return new HTTPException(409, { message: error.message, cause: error });
787
+ }
788
+ if (error instanceof CodemodeCallBudgetExceededError) {
789
+ return new HTTPException(429, { message: error.message, cause: error });
790
+ }
791
+ if (error instanceof CodemodeToolNotInCatalogError) {
792
+ return new HTTPException(404, { message: error.message, cause: error });
793
+ }
794
+ if (error instanceof CodemodeToolApprovalRequiredError) {
795
+ return new HTTPException(409, { message: error.message, cause: error });
796
+ }
797
+ if (error instanceof CodemodePayloadTooLargeError) {
798
+ return new HTTPException(413, { message: error.message, cause: error });
799
+ }
800
+ return error instanceof HTTPException
801
+ ? error
802
+ : new HTTPException(500, {
803
+ message: "Codemode request failed",
804
+ cause: error,
805
+ });
806
+ }
807
+
690
808
  function clientAnalyticsConfig(settings: AppDependencies["settings"]) {
691
809
  if (!settings.analyticsEnabled) {
692
810
  return { consentRequired: true, providers: {} };
@@ -721,10 +839,6 @@ function structuredServicesHint(backend: string): {
721
839
  return { fileSystem: hasBox, git: hasBox, terminalEvents: hasBox };
722
840
  }
723
841
 
724
- export function allowedCorsOrigin(pattern: string, origin: string): boolean {
725
- return new RegExp(`^(?:${pattern})$`).test(origin);
726
- }
727
-
728
842
  function codexCompactionV2ProviderLockedError(
729
843
  error: unknown,
730
844
  ): CodexCompactionV2ProviderLockedError | null {
@@ -945,6 +1059,23 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
945
1059
  pattern: /^\/v1\/workspaces\/[^/]+\/inference-control$/,
946
1060
  label: "/v1/workspaces/:workspaceId/inference-control",
947
1061
  },
1062
+ {
1063
+ pattern:
1064
+ /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/fs\/(list|list-batch|read|write|delete|move|mkdir)$/,
1065
+ label: "/v1/workspaces/:workspaceId/sessions/:id/fs/:operation",
1066
+ },
1067
+ {
1068
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/git\/(status|diff|read-batch|log|show)$/,
1069
+ label: "/v1/workspaces/:workspaceId/sessions/:id/git/:operation",
1070
+ },
1071
+ {
1072
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/terminal\/(exec|pty)$/,
1073
+ label: "/v1/workspaces/:workspaceId/sessions/:id/terminal/:operation",
1074
+ },
1075
+ {
1076
+ pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/terminal\/pty\/(write|resize|close)$/,
1077
+ label: "/v1/workspaces/:workspaceId/sessions/:id/terminal/pty/:action",
1078
+ },
948
1079
  {
949
1080
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/events\/stream$/,
950
1081
  label: "/v1/workspaces/:workspaceId/sessions/:id/events/stream",
@@ -1141,6 +1272,80 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
1141
1272
  pattern: /^\/v1\/workspaces\/[^/]+\/capabilities\/[^/]+\/disable$/,
1142
1273
  label: "/v1/workspaces/:workspaceId/capabilities/:id/disable",
1143
1274
  },
1275
+ {
1276
+ pattern: /^\/v1\/workspaces\/[^/]+\/integrations\/preview$/,
1277
+ label: "/v1/workspaces/:workspaceId/integrations/preview",
1278
+ },
1279
+ {
1280
+ pattern: /^\/v1\/workspaces\/[^/]+\/integrations\/install$/,
1281
+ label: "/v1/workspaces/:workspaceId/integrations/install",
1282
+ },
1283
+ {
1284
+ pattern: /^\/v1\/workspaces\/[^/]+\/integrations\/presets$/,
1285
+ label: "/v1/workspaces/:workspaceId/integrations/presets",
1286
+ },
1287
+ {
1288
+ pattern: /^\/v1\/workspaces\/[^/]+\/integrations\/[^/]+\/instances\/[^/]+\/uninstall-preview$/,
1289
+ label:
1290
+ "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/uninstall-preview",
1291
+ },
1292
+ {
1293
+ pattern:
1294
+ /^\/v1\/workspaces\/[^/]+\/integrations\/[^/]+\/instances\/[^/]+\/features\/[^/]+\/browse$/,
1295
+ label:
1296
+ "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/features/:featureKey/browse",
1297
+ },
1298
+ {
1299
+ pattern:
1300
+ /^\/v1\/workspaces\/[^/]+\/integrations\/[^/]+\/instances\/[^/]+\/features\/[^/]+\/source$/,
1301
+ label:
1302
+ "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/features/:featureKey/source",
1303
+ },
1304
+ {
1305
+ pattern:
1306
+ /^\/v1\/workspaces\/[^/]+\/integrations\/[^/]+\/instances\/[^/]+\/features\/[^/]+\/pause$/,
1307
+ label:
1308
+ "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/features/:featureKey/pause",
1309
+ },
1310
+ {
1311
+ pattern:
1312
+ /^\/v1\/workspaces\/[^/]+\/integrations\/[^/]+\/instances\/[^/]+\/features\/[^/]+\/resume$/,
1313
+ label:
1314
+ "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/features/:featureKey/resume",
1315
+ },
1316
+ {
1317
+ pattern: /^\/v1\/workspaces\/[^/]+\/integrations\/[^/]+\/instances\/[^/]+\/features\/[^/]+$/,
1318
+ label:
1319
+ "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/features/:featureKey",
1320
+ },
1321
+ {
1322
+ pattern: /^\/v1\/workspaces\/[^/]+\/integrations\/[^/]+\/instances\/[^/]+\/features$/,
1323
+ label: "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/features",
1324
+ },
1325
+ {
1326
+ pattern: /^\/v1\/workspaces\/[^/]+\/integrations\/[^/]+\/instances\/[^/]+$/,
1327
+ label: "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey",
1328
+ },
1329
+ {
1330
+ pattern: /^\/v1\/workspaces\/[^/]+\/integrations$/,
1331
+ label: "/v1/workspaces/:workspaceId/integrations",
1332
+ },
1333
+ {
1334
+ pattern: /^\/v1\/workspaces\/[^/]+\/skills\/preview$/,
1335
+ label: "/v1/workspaces/:workspaceId/skills/preview",
1336
+ },
1337
+ {
1338
+ pattern: /^\/v1\/workspaces\/[^/]+\/skills\/install$/,
1339
+ label: "/v1/workspaces/:workspaceId/skills/install",
1340
+ },
1341
+ {
1342
+ pattern: /^\/v1\/workspaces\/[^/]+\/skills\/[^/]+\/uninstall-preview$/,
1343
+ label: "/v1/workspaces/:workspaceId/skills/:id/uninstall-preview",
1344
+ },
1345
+ {
1346
+ pattern: /^\/v1\/workspaces\/[^/]+\/skills\/[^/]+$/,
1347
+ label: "/v1/workspaces/:workspaceId/skills/:id",
1348
+ },
1144
1349
  {
1145
1350
  pattern: /^\/v1\/workspaces\/[^/]+\/environments$/,
1146
1351
  label: "/v1/workspaces/:workspaceId/environments",
@@ -1173,6 +1378,26 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
1173
1378
  pattern: /^\/v1\/workspaces\/[^/]+\/packs\/[^/]+$/,
1174
1379
  label: "/v1/workspaces/:workspaceId/packs/:id",
1175
1380
  },
1381
+ {
1382
+ pattern: /^\/v1\/workspaces\/[^/]+\/plugins\/preview$/,
1383
+ label: "/v1/workspaces/:workspaceId/plugins/preview",
1384
+ },
1385
+ {
1386
+ pattern: /^\/v1\/workspaces\/[^/]+\/plugins$/,
1387
+ label: "/v1/workspaces/:workspaceId/plugins",
1388
+ },
1389
+ {
1390
+ pattern: /^\/v1\/workspaces\/[^/]+\/plugins\/install$/,
1391
+ label: "/v1/workspaces/:workspaceId/plugins/install",
1392
+ },
1393
+ {
1394
+ pattern: /^\/v1\/workspaces\/[^/]+\/plugins\/[^/]+\/uninstall-preview$/,
1395
+ label: "/v1/workspaces/:workspaceId/plugins/:pluginKey/uninstall-preview",
1396
+ },
1397
+ {
1398
+ pattern: /^\/v1\/workspaces\/[^/]+\/plugins\/[^/]+$/,
1399
+ label: "/v1/workspaces/:workspaceId/plugins/:pluginKey",
1400
+ },
1176
1401
  {
1177
1402
  pattern: /^\/v1\/workspaces\/[^/]+\/social\/connections$/,
1178
1403
  label: "/v1/workspaces/:workspaceId/social/connections",
@@ -1189,6 +1414,10 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
1189
1414
  pattern: /^\/v1\/workspaces\/[^/]+\/connections\/oauth\/start$/,
1190
1415
  label: "/v1/workspaces/:workspaceId/connections/oauth/start",
1191
1416
  },
1417
+ {
1418
+ pattern: /^\/v1\/workspaces\/[^/]+\/integrations\/oauth\/start$/,
1419
+ label: "/v1/workspaces/:workspaceId/integrations/oauth/start",
1420
+ },
1192
1421
  {
1193
1422
  pattern: /^\/v1\/workspaces\/[^/]+\/connections\/slack-bot\/install$/,
1194
1423
  label: "/v1/workspaces/:workspaceId/connections/slack-bot/install",
@@ -1202,6 +1431,14 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
1202
1431
  pattern: /^\/v1\/integrations\/oauth\/callback$/,
1203
1432
  label: "/v1/integrations/oauth/callback",
1204
1433
  },
1434
+ {
1435
+ pattern: /^\/v1\/integrations\/provider-oauth\/callback$/,
1436
+ label: "/v1/integrations/provider-oauth/callback",
1437
+ },
1438
+ {
1439
+ pattern: /^\/v1\/integrations\/google-drive\/callback$/,
1440
+ label: "/v1/integrations/google-drive/callback",
1441
+ },
1205
1442
  {
1206
1443
  pattern: /^\/v1\/integrations\/oauth\/client-metadata\.json$/,
1207
1444
  label: "/v1/integrations/oauth/client-metadata.json",
@@ -0,0 +1,137 @@
1
+ import { createHmac } from "node:crypto";
2
+ import type { InteractionPlacement } from "@opengeni/contracts";
3
+
4
+ type BrowserControllerAuthorityScope = {
5
+ rootSecret: string;
6
+ accountId: string;
7
+ workspaceId: string;
8
+ placement: InteractionPlacement;
9
+ placementInstanceId: string;
10
+ };
11
+
12
+ type BrowserSessionAuthorityScope = BrowserControllerAuthorityScope & {
13
+ browserSessionId: string;
14
+ controllerGeneration: string;
15
+ tokenGeneration: number;
16
+ };
17
+
18
+ type ComputerSessionAuthorityScope = BrowserControllerAuthorityScope & {
19
+ computerSessionId: string;
20
+ controllerGeneration: string;
21
+ tokenGeneration: number;
22
+ };
23
+
24
+ /** Stable placement-admin authority. This token is installed owner-only inside
25
+ * the exact placement and is never persisted or returned by the public API. */
26
+ export function deriveBrowserControllerAdminToken(scope: BrowserControllerAuthorityScope): string {
27
+ return derive(scope.rootSecret, "placement-admin.v1", [
28
+ scope.accountId,
29
+ scope.workspaceId,
30
+ placementKey(scope.placement),
31
+ scope.placementInstanceId,
32
+ ]);
33
+ }
34
+
35
+ /** Deterministic controller credentials. The durable generations are the only
36
+ * stored authority; plaintext tokens can be reconstructed only by API replicas
37
+ * holding the deployment HMAC root. */
38
+ export function deriveBrowserSessionControllerTokens(scope: BrowserSessionAuthorityScope): {
39
+ controlToken: string;
40
+ viewToken: string;
41
+ } {
42
+ const fields = [
43
+ scope.accountId,
44
+ scope.workspaceId,
45
+ placementKey(scope.placement),
46
+ scope.placementInstanceId,
47
+ scope.browserSessionId,
48
+ scope.controllerGeneration,
49
+ scope.tokenGeneration,
50
+ ] as const;
51
+ return {
52
+ controlToken: derive(scope.rootSecret, "session-control.v1", fields),
53
+ viewToken: derive(scope.rootSecret, "session-view.v1", fields),
54
+ };
55
+ }
56
+
57
+ export function deriveBrowserViewGrantToken(
58
+ scope: BrowserSessionAuthorityScope & { grantId: string; expiresAt: string },
59
+ ): string {
60
+ return derive(scope.rootSecret, "frame-view-grant.v1", [
61
+ scope.accountId,
62
+ scope.workspaceId,
63
+ placementKey(scope.placement),
64
+ scope.placementInstanceId,
65
+ scope.browserSessionId,
66
+ scope.controllerGeneration,
67
+ scope.tokenGeneration,
68
+ scope.grantId,
69
+ scope.expiresAt,
70
+ ]);
71
+ }
72
+
73
+ /** ComputerSession credentials share the placement controller root while using
74
+ * resource-specific domains. Browser and computer authority cannot collide. */
75
+ export function deriveComputerSessionControllerTokens(scope: ComputerSessionAuthorityScope): {
76
+ controlToken: string;
77
+ viewToken: string;
78
+ } {
79
+ const fields = [
80
+ scope.accountId,
81
+ scope.workspaceId,
82
+ placementKey(scope.placement),
83
+ scope.placementInstanceId,
84
+ scope.computerSessionId,
85
+ scope.controllerGeneration,
86
+ scope.tokenGeneration,
87
+ ] as const;
88
+ return {
89
+ controlToken: derive(scope.rootSecret, "computer-session-control.v1", fields),
90
+ viewToken: derive(scope.rootSecret, "computer-session-view.v1", fields),
91
+ };
92
+ }
93
+
94
+ export function deriveComputerViewGrantToken(
95
+ scope: ComputerSessionAuthorityScope & { grantId: string; expiresAt: string },
96
+ ): string {
97
+ return derive(scope.rootSecret, "computer-frame-view-grant.v1", [
98
+ scope.accountId,
99
+ scope.workspaceId,
100
+ placementKey(scope.placement),
101
+ scope.placementInstanceId,
102
+ scope.computerSessionId,
103
+ scope.controllerGeneration,
104
+ scope.tokenGeneration,
105
+ scope.grantId,
106
+ scope.expiresAt,
107
+ ]);
108
+ }
109
+
110
+ function derive(rootSecret: string, domain: string, fields: readonly (string | number)[]): string {
111
+ if (!rootSecret) throw new Error("browser controller authority root is unavailable");
112
+ const hmac = createHmac("sha256", rootSecret);
113
+ hmac.update("opengeni.browser-controller-authority\0", "utf8");
114
+ writeField(hmac, domain);
115
+ for (const field of fields) writeField(hmac, String(field));
116
+ return `ogb.${hmac.digest("base64url")}`;
117
+ }
118
+
119
+ function writeField(hmac: ReturnType<typeof createHmac>, value: string): void {
120
+ const bytes = Buffer.from(value, "utf8");
121
+ hmac.update(String(bytes.byteLength), "utf8");
122
+ hmac.update(":", "utf8");
123
+ hmac.update(bytes);
124
+ }
125
+
126
+ function placementKey(placement: InteractionPlacement): string {
127
+ switch (placement.kind) {
128
+ case "sandbox_group":
129
+ return `sandbox_group:${placement.sandboxGroupId}`;
130
+ case "connected_machine":
131
+ return `connected_machine:${placement.sandboxId}`;
132
+ case "attached_device":
133
+ return `attached_device:${placement.deviceId}`;
134
+ case "external_provider":
135
+ return `external_provider:${placement.providerId}:${placement.placementId}`;
136
+ }
137
+ }