@opengeni/api-router 0.22.2 → 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 (124) hide show
  1. package/dist/app.d.ts +2 -2
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth.d.ts +0 -30
  4. package/dist/browser-controller-authority.d.ts +43 -0
  5. package/dist/browser-state-authority.d.ts +27 -0
  6. package/dist/{chunk-HWXJW5C7.js → chunk-JIKNR5YL.js} +27993 -14546
  7. package/dist/chunk-JIKNR5YL.js.map +1 -0
  8. package/dist/codemode.d.ts +23 -0
  9. package/dist/editable-artifact-live-hints.d.ts +11 -0
  10. package/dist/editable-artifact-native-kernel.d.ts +37 -0
  11. package/dist/editable-artifact-office-import.d.ts +22 -0
  12. package/dist/editable-artifact-production.d.ts +29 -0
  13. package/dist/editable-artifact-websocket.d.ts +49 -0
  14. package/dist/editable-artifact-workspace-files.d.ts +23 -0
  15. package/dist/github-browser-flow.d.ts +6 -0
  16. package/dist/http/cors.d.ts +1 -0
  17. package/dist/http/sse.d.ts +2 -0
  18. package/dist/index.d.ts +3 -2
  19. package/dist/index.js +1824 -30
  20. package/dist/index.js.map +1 -1
  21. package/dist/integrations/api-integrations.d.ts +24 -0
  22. package/dist/integrations/atlassian.d.ts +176 -0
  23. package/dist/integrations/github-skill-source.d.ts +5 -0
  24. package/dist/integrations/google-drive.d.ts +85 -0
  25. package/dist/integrations/oauth-client.d.ts +30 -1
  26. package/dist/integrations/provider-oauth.d.ts +19 -0
  27. package/dist/integrations/slack-bot.d.ts +4 -0
  28. package/dist/integrations/slack-interactions.d.ts +14 -2
  29. package/dist/integrations/social-api.d.ts +2 -1
  30. package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
  31. package/dist/mcp/editable-artifacts.d.ts +13 -0
  32. package/dist/mcp/receipts.d.ts +28 -0
  33. package/dist/mcp/scheduled-task-view.d.ts +518 -0
  34. package/dist/mcp/server.d.ts +14 -3
  35. package/dist/memory-slack-delivery.d.ts +9 -0
  36. package/dist/routes/api-integrations.d.ts +8 -0
  37. package/dist/routes/browser-identities.d.ts +5 -0
  38. package/dist/routes/browser-sessions.d.ts +6 -0
  39. package/dist/routes/company-profile.d.ts +3 -0
  40. package/dist/routes/computer-sessions.d.ts +6 -0
  41. package/dist/routes/editable-artifacts.d.ts +44 -0
  42. package/dist/routes/integration-features.d.ts +3 -0
  43. package/dist/routes/memory-slack-publications.d.ts +6 -0
  44. package/dist/routes/plugins.d.ts +8 -0
  45. package/dist/routes/sessions.d.ts +17 -2
  46. package/dist/routes/skills.d.ts +6 -0
  47. package/dist/routes/video-generation.d.ts +3 -0
  48. package/dist/sandbox/auth-callout.d.ts +2 -0
  49. package/dist/sandbox/channel-a.d.ts +59 -2
  50. package/dist/sandbox/metrics-ingestion.d.ts +6 -1
  51. package/dist/sandbox/viewer.d.ts +4 -2
  52. package/dist/temporal-schedule-cleanup.d.ts +26 -0
  53. package/package.json +19 -14
  54. package/src/app.ts +277 -41
  55. package/src/auth/managed-auth.ts +0 -16
  56. package/src/browser-controller-authority.ts +137 -0
  57. package/src/browser-state-authority.ts +236 -0
  58. package/src/codemode.ts +186 -0
  59. package/src/editable-artifact-live-hints.ts +64 -0
  60. package/src/editable-artifact-native-kernel.ts +659 -0
  61. package/src/editable-artifact-office-import.ts +230 -0
  62. package/src/editable-artifact-production.ts +419 -0
  63. package/src/editable-artifact-websocket.ts +311 -0
  64. package/src/editable-artifact-workspace-files.ts +186 -0
  65. package/src/github-browser-flow.ts +35 -6
  66. package/src/http/auth.ts +2 -0
  67. package/src/http/cors.ts +3 -0
  68. package/src/http/sse.ts +101 -6
  69. package/src/index.ts +147 -23
  70. package/src/integrations/api-integrations.ts +350 -0
  71. package/src/integrations/atlassian.ts +1621 -0
  72. package/src/integrations/github-skill-source.ts +142 -0
  73. package/src/integrations/google-drive.ts +1000 -64
  74. package/src/integrations/oauth-client.ts +159 -89
  75. package/src/integrations/provider-oauth.ts +777 -0
  76. package/src/integrations/slack-bot.ts +31 -2
  77. package/src/integrations/slack-interactions.ts +610 -42
  78. package/src/integrations/social-api.ts +11 -0
  79. package/src/mcp/documents.ts +74 -26
  80. package/src/mcp/editable-artifact-query-schema.ts +236 -0
  81. package/src/mcp/editable-artifacts.ts +448 -0
  82. package/src/mcp/receipts.ts +95 -0
  83. package/src/mcp/scheduled-task-view.ts +642 -0
  84. package/src/mcp/server.ts +1718 -310
  85. package/src/memory-slack-delivery.ts +209 -0
  86. package/src/observability.ts +3 -3
  87. package/src/routes/api-integrations.ts +407 -0
  88. package/src/routes/api-keys.ts +7 -1
  89. package/src/routes/browser-identities.ts +136 -0
  90. package/src/routes/browser-sessions.ts +2543 -0
  91. package/src/routes/codex.ts +7 -4
  92. package/src/routes/company-profile.ts +255 -0
  93. package/src/routes/computer-sessions.ts +1247 -0
  94. package/src/routes/connections.ts +358 -102
  95. package/src/routes/documents.ts +22 -3
  96. package/src/routes/editable-artifacts.ts +1159 -0
  97. package/src/routes/enrollments.ts +54 -12
  98. package/src/routes/environments.ts +60 -11
  99. package/src/routes/files.ts +277 -65
  100. package/src/routes/github.ts +18 -2
  101. package/src/routes/install.ts +38 -2
  102. package/src/routes/integration-features.ts +258 -0
  103. package/src/routes/machines.ts +1 -1
  104. package/src/routes/memory-slack-publications.ts +216 -0
  105. package/src/routes/packs.ts +437 -7
  106. package/src/routes/plugins.ts +751 -0
  107. package/src/routes/rigs.ts +77 -20
  108. package/src/routes/scheduled-tasks.ts +94 -42
  109. package/src/routes/sessions.ts +475 -234
  110. package/src/routes/skills.ts +174 -0
  111. package/src/routes/transcription-recordings.ts +65 -33
  112. package/src/routes/video-generation.ts +132 -0
  113. package/src/routes/workspaces.ts +46 -24
  114. package/src/sandbox/auth-callout.ts +16 -4
  115. package/src/sandbox/channel-a.ts +809 -85
  116. package/src/sandbox/enrollment.ts +13 -3
  117. package/src/sandbox/machines.ts +1 -1
  118. package/src/sandbox/metrics-ingestion.ts +121 -3
  119. package/src/sandbox/rematerialize.ts +35 -47
  120. package/src/sandbox/viewer.ts +58 -29
  121. package/src/temporal-schedule-cleanup.ts +135 -0
  122. package/dist/chunk-HWXJW5C7.js.map +0 -1
  123. package/dist/mcp/toolspace.d.ts +0 -62
  124. package/src/mcp/toolspace.ts +0 -1186
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,
@@ -307,7 +344,6 @@ export function createAppComposition(deps: AppDependencies): {
307
344
  const start = performance.now();
308
345
  const span = observability.startSpan(`HTTP ${c.req.method} ${route}`, {
309
346
  "http.request.method": c.req.method,
310
- "url.path": url.pathname,
311
347
  "opengeni.route": route,
312
348
  });
313
349
  try {
@@ -366,7 +402,7 @@ export function createAppComposition(deps: AppDependencies): {
366
402
  spanId: span.spanId,
367
403
  correlationId,
368
404
  errorCode,
369
- errorClass: error instanceof Error ? error.name : "NonErrorThrown",
405
+ errorClass: "HttpOperationError",
370
406
  });
371
407
  throw error;
372
408
  }
@@ -500,17 +536,13 @@ export function createAppComposition(deps: AppDependencies): {
500
536
  throw error;
501
537
  }
502
538
  const grant = await requireMcpAccessGrant(c, routeDeps, workspaceId);
503
- const toolspaceGrant = isToolspaceGrant(routeDeps.settings, grant);
504
539
  const boundSessionId = grant.metadata?.sessionId;
505
- if (toolspaceGrant || typeof boundSessionId === "string") {
506
- if (typeof boundSessionId !== "string") {
507
- throw new HTTPException(404, { message: "session not found" });
508
- }
540
+ if (typeof boundSessionId === "string") {
509
541
  try {
510
542
  await requireSessionAuthorization(routeDeps, grant, {
511
543
  sessionId: boundSessionId,
512
- operation: toolspaceGrant ? "session.toolspace.call" : "session.first_party_mcp.call",
513
- surface: toolspaceGrant ? "toolspace" : "first_party_mcp",
544
+ operation: "session.first_party_mcp.call",
545
+ surface: "first_party_mcp",
514
546
  });
515
547
  } catch (error) {
516
548
  if (error instanceof SessionAuthorizationDeniedError) {
@@ -524,22 +556,6 @@ export function createAppComposition(deps: AppDependencies): {
524
556
  throw error;
525
557
  }
526
558
  }
527
- let toolspace: Awaited<ReturnType<typeof prepareToolspaceMcpSurface>> = null;
528
- if (toolspaceGrant) {
529
- try {
530
- toolspace = await prepareToolspaceMcpSurface({
531
- deps: routeDeps,
532
- grant,
533
- });
534
- } catch (error) {
535
- if (error instanceof McpPayloadTooLargeError) {
536
- throw new HTTPException(413, {
537
- message: "MCP tool list exceeds the safety limit",
538
- });
539
- }
540
- throw error;
541
- }
542
- }
543
559
  const workspace = await getWorkspace(routeDeps.db, workspaceId);
544
560
  const workspaceMemoryEnabled = resolveWorkspaceMemoryEnabled(workspace?.settings);
545
561
  const transport = new WebStandardStreamableHTTPServerTransport({
@@ -547,42 +563,101 @@ export function createAppComposition(deps: AppDependencies): {
547
563
  });
548
564
  const mcp = buildOpenGeniMcpServer(routeDeps, grant, {
549
565
  requestOrigin: new URL(c.req.url).origin,
550
- toolspace,
551
566
  workspaceMemoryEnabled,
552
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
+ }
553
612
  try {
554
- await mcp.connect(transport);
555
- return await transport.handleRequest(boundedRequest);
556
- } finally {
557
- 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);
558
622
  }
559
623
  });
560
624
 
561
625
  registerFileRoutes(app, routeDeps);
562
626
  registerApiKeyRoutes(app, routeDeps);
563
627
  registerBillingRoutes(app, routeDeps);
628
+ registerBrowserIdentityRoutes(app, routeDeps);
629
+ registerBrowserSessionRoutes(app, routeDeps);
630
+ registerComputerSessionRoutes(app, routeDeps);
564
631
  registerDocumentRoutes(app, routeDeps);
565
632
  registerGitHubRoutes(app, routeDeps);
566
633
  registerInstallRoutes(app, routeDeps);
567
634
  registerWorkspaceRoutes(app, routeDeps);
568
635
  registerInsightsRoutes(app, routeDeps);
569
636
  registerWorkspaceInstructionPolicyRoutes(app, routeDeps);
637
+ registerCompanyProfileRoutes(app, routeDeps);
570
638
  registerWorkspaceStateRoutes(app, routeDeps);
639
+ registerMemorySlackPublicationRoutes(app, routeDeps);
571
640
  registerWorkspaceArtifactRoutes(app, routeDeps);
572
641
  registerPreferenceRegistryRoutes(app, routeDeps);
573
642
  registerSocialRoutes(app, routeDeps);
574
643
  registerConnectionRoutes(app, routeDeps);
575
644
  registerCapabilityRoutes(app, routeDeps);
645
+ registerApiIntegrationRoutes(app, routeDeps);
646
+ registerIntegrationFeatureRoutes(app, routeDeps);
576
647
  registerCatalogAssetRoutes(app, routeDeps);
577
648
  registerEnrollmentRoutes(app, routeDeps);
578
649
  registerMachineRoutes(app, routeDeps);
579
650
  registerEnvironmentRoutes(app, routeDeps);
580
651
  registerRigRoutes(app, routeDeps);
581
652
  registerPackRoutes(app, routeDeps);
653
+ registerPluginRoutes(app, routeDeps);
654
+ registerSkillRoutes(app, routeDeps);
582
655
  registerSessionRoutes(app, routeDeps);
583
656
  registerScheduledTaskRoutes(app, routeDeps);
584
657
  registerCodexRoutes(app, routeDeps);
585
658
  registerTranscriptionRoutes(app, routeDeps);
659
+ registerEditableArtifactRoutes(app, routeDeps);
660
+ registerVideoGenerationRoutes(app, routeDeps);
586
661
  registerSlackInteractionRoutes(app, routeDeps);
587
662
 
588
663
  app.notFound((c) => {
@@ -654,8 +729,8 @@ async function requireMcpAccessGrant(
654
729
  if (hasPermission(grant.permissions, "workspace:read")) {
655
730
  return grant;
656
731
  }
657
- if (isToolspaceGrant(deps.settings, grant)) {
658
- return grant;
732
+ if (isCodemodeGrant(grant)) {
733
+ requirePermission(grant, "workspace:read");
659
734
  }
660
735
  // A worker-signed session-bound grant is allowed to reach the transport
661
736
  // without inheriting broad workspace read access. The exact session
@@ -688,6 +763,48 @@ function clientAuthConfig(settings: AppDependencies["settings"]) {
688
763
  return { mode: "none" as const };
689
764
  }
690
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
+
691
808
  function clientAnalyticsConfig(settings: AppDependencies["settings"]) {
692
809
  if (!settings.analyticsEnabled) {
693
810
  return { consentRequired: true, providers: {} };
@@ -722,10 +839,6 @@ function structuredServicesHint(backend: string): {
722
839
  return { fileSystem: hasBox, git: hasBox, terminalEvents: hasBox };
723
840
  }
724
841
 
725
- export function allowedCorsOrigin(pattern: string, origin: string): boolean {
726
- return new RegExp(`^(?:${pattern})$`).test(origin);
727
- }
728
-
729
842
  function codexCompactionV2ProviderLockedError(
730
843
  error: unknown,
731
844
  ): CodexCompactionV2ProviderLockedError | null {
@@ -837,12 +950,12 @@ async function runReadinessChecks<const Checks extends Readonly<Record<string, R
837
950
  try {
838
951
  await withTimeout(Promise.resolve().then(check), timeoutMs);
839
952
  return [name, { ok: true }] as const;
840
- } catch (error) {
953
+ } catch {
841
954
  return [
842
955
  name,
843
956
  {
844
957
  ok: false,
845
- error: error instanceof Error ? error.message : String(error),
958
+ error: "dependency_unavailable",
846
959
  },
847
960
  ] as const;
848
961
  }
@@ -946,6 +1059,23 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
946
1059
  pattern: /^\/v1\/workspaces\/[^/]+\/inference-control$/,
947
1060
  label: "/v1/workspaces/:workspaceId/inference-control",
948
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
+ },
949
1079
  {
950
1080
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions\/[^/]+\/events\/stream$/,
951
1081
  label: "/v1/workspaces/:workspaceId/sessions/:id/events/stream",
@@ -1142,6 +1272,80 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
1142
1272
  pattern: /^\/v1\/workspaces\/[^/]+\/capabilities\/[^/]+\/disable$/,
1143
1273
  label: "/v1/workspaces/:workspaceId/capabilities/:id/disable",
1144
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
+ },
1145
1349
  {
1146
1350
  pattern: /^\/v1\/workspaces\/[^/]+\/environments$/,
1147
1351
  label: "/v1/workspaces/:workspaceId/environments",
@@ -1174,6 +1378,26 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
1174
1378
  pattern: /^\/v1\/workspaces\/[^/]+\/packs\/[^/]+$/,
1175
1379
  label: "/v1/workspaces/:workspaceId/packs/:id",
1176
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
+ },
1177
1401
  {
1178
1402
  pattern: /^\/v1\/workspaces\/[^/]+\/social\/connections$/,
1179
1403
  label: "/v1/workspaces/:workspaceId/social/connections",
@@ -1190,6 +1414,10 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
1190
1414
  pattern: /^\/v1\/workspaces\/[^/]+\/connections\/oauth\/start$/,
1191
1415
  label: "/v1/workspaces/:workspaceId/connections/oauth/start",
1192
1416
  },
1417
+ {
1418
+ pattern: /^\/v1\/workspaces\/[^/]+\/integrations\/oauth\/start$/,
1419
+ label: "/v1/workspaces/:workspaceId/integrations/oauth/start",
1420
+ },
1193
1421
  {
1194
1422
  pattern: /^\/v1\/workspaces\/[^/]+\/connections\/slack-bot\/install$/,
1195
1423
  label: "/v1/workspaces/:workspaceId/connections/slack-bot/install",
@@ -1203,6 +1431,14 @@ const routeLabelPatterns: Array<{ pattern: RegExp; label: string }> = [
1203
1431
  pattern: /^\/v1\/integrations\/oauth\/callback$/,
1204
1432
  label: "/v1/integrations/oauth/callback",
1205
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
+ },
1206
1442
  {
1207
1443
  pattern: /^\/v1\/integrations\/oauth\/client-metadata\.json$/,
1208
1444
  label: "/v1/integrations/oauth/client-metadata.json",
@@ -151,22 +151,6 @@ export function createManagedAuth(settings: Settings, db: Database): ManagedAuth
151
151
  }) as ManagedAuth;
152
152
  }
153
153
 
154
- export async function managedSessionAccessContext(
155
- auth: ManagedAuth,
156
- db: Database,
157
- headers: Headers,
158
- ) {
159
- const session = await auth.api.getSession({ headers });
160
- if (!session?.user) {
161
- return null;
162
- }
163
- return await ensureManagedAccessForUser(db, {
164
- userId: session.user.id,
165
- email: session.user.email,
166
- name: session.user.name,
167
- });
168
- }
169
-
170
154
  function betterAuthBaseUrl(settings: Settings) {
171
155
  const allowedHosts = splitCsv(settings.betterAuthAllowedHosts);
172
156
  if (allowedHosts.length === 0) {
@@ -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
+ }