@opengeni/api-router 0.23.1 → 0.30.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.
Files changed (152) hide show
  1. package/dist/app.d.ts +3 -2
  2. package/dist/app.js +5 -3
  3. package/dist/auth/canonical-human-session-admission.d.ts +27 -0
  4. package/dist/browser-auth-broker.d.ts +20 -0
  5. package/dist/browser-controller-authority.d.ts +61 -0
  6. package/dist/browser-network-route.d.ts +8 -0
  7. package/dist/browser-state-authority.d.ts +27 -0
  8. package/dist/{chunk-T4T2PGU4.js → chunk-2PQMSRCB.js} +37891 -17463
  9. package/dist/chunk-2PQMSRCB.js.map +1 -0
  10. package/dist/codemode.d.ts +23 -0
  11. package/dist/controller-data-plane.d.ts +12 -0
  12. package/dist/editable-artifact-live-hints.d.ts +11 -0
  13. package/dist/editable-artifact-native-kernel.d.ts +37 -0
  14. package/dist/editable-artifact-office-import.d.ts +22 -0
  15. package/dist/editable-artifact-production.d.ts +29 -0
  16. package/dist/editable-artifact-websocket.d.ts +49 -0
  17. package/dist/editable-artifact-workspace-files.d.ts +23 -0
  18. package/dist/github-browser-flow.d.ts +6 -0
  19. package/dist/http/auth.d.ts +1 -1
  20. package/dist/http/cors.d.ts +1 -0
  21. package/dist/http/sse.d.ts +18 -1
  22. package/dist/index.d.ts +3 -2
  23. package/dist/index.js +1796 -22
  24. package/dist/index.js.map +1 -1
  25. package/dist/integrations/api-integrations.d.ts +24 -0
  26. package/dist/integrations/atlassian.d.ts +176 -0
  27. package/dist/integrations/fiken.d.ts +233 -0
  28. package/dist/integrations/github-skill-source.d.ts +5 -0
  29. package/dist/integrations/google-drive.d.ts +85 -0
  30. package/dist/integrations/oauth-client.d.ts +22 -1
  31. package/dist/integrations/provider-oauth.d.ts +17 -0
  32. package/dist/integrations/slack-bot.d.ts +175 -1
  33. package/dist/integrations/slack-interactions.d.ts +24 -6
  34. package/dist/integrations/social-api.d.ts +2 -1
  35. package/dist/interaction-holder-heartbeat.d.ts +17 -0
  36. package/dist/interaction-metrics.d.ts +11 -0
  37. package/dist/mcp/editable-artifact-query-schema.d.ts +4 -0
  38. package/dist/mcp/editable-artifacts.d.ts +13 -0
  39. package/dist/mcp/scheduled-task-view.d.ts +168 -0
  40. package/dist/mcp/server.d.ts +16 -4
  41. package/dist/memory-slack-delivery.d.ts +9 -0
  42. package/dist/model-catalog.d.ts +1 -0
  43. package/dist/routes/api-integrations.d.ts +8 -0
  44. package/dist/routes/browser-identities.d.ts +5 -0
  45. package/dist/routes/browser-sessions.d.ts +6 -0
  46. package/dist/routes/canonical-human-identities.d.ts +3 -0
  47. package/dist/routes/channels.d.ts +3 -0
  48. package/dist/routes/company-profile.d.ts +3 -0
  49. package/dist/routes/computer-sessions.d.ts +6 -0
  50. package/dist/routes/editable-artifacts.d.ts +44 -0
  51. package/dist/routes/integration-facets.d.ts +3 -0
  52. package/dist/routes/interaction-resources.d.ts +5 -0
  53. package/dist/routes/memory-slack-publications.d.ts +6 -0
  54. package/dist/routes/organization-memberships.d.ts +3 -0
  55. package/dist/routes/plugins.d.ts +8 -0
  56. package/dist/routes/sessions.d.ts +18 -2
  57. package/dist/routes/skills.d.ts +6 -0
  58. package/dist/routes/slack-task-policy.d.ts +3 -0
  59. package/dist/routes/supergrok.d.ts +3 -0
  60. package/dist/routes/video-generation.d.ts +3 -0
  61. package/dist/sandbox/channel-a.d.ts +62 -2
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +6 -1
  64. package/dist/sandbox/viewer.d.ts +7 -3
  65. package/dist/slack-reaction-files.d.ts +27 -0
  66. package/dist/temporal-schedule-cleanup.d.ts +26 -0
  67. package/dist/transcription/providers/xai-subscription.d.ts +8 -0
  68. package/dist/xai-realtime.d.ts +26 -0
  69. package/dist/xai-subscription-auth.d.ts +23 -0
  70. package/package.json +20 -14
  71. package/src/app.ts +604 -64
  72. package/src/auth/canonical-human-session-admission.ts +116 -0
  73. package/src/auth/managed-auth.ts +101 -0
  74. package/src/browser-auth-broker.ts +155 -0
  75. package/src/browser-controller-authority.ts +186 -0
  76. package/src/browser-network-route.ts +34 -0
  77. package/src/browser-state-authority.ts +236 -0
  78. package/src/codemode.ts +186 -0
  79. package/src/controller-data-plane.ts +35 -0
  80. package/src/editable-artifact-live-hints.ts +64 -0
  81. package/src/editable-artifact-native-kernel.ts +659 -0
  82. package/src/editable-artifact-office-import.ts +230 -0
  83. package/src/editable-artifact-production.ts +419 -0
  84. package/src/editable-artifact-websocket.ts +311 -0
  85. package/src/editable-artifact-workspace-files.ts +186 -0
  86. package/src/github-browser-flow.ts +35 -6
  87. package/src/http/auth.ts +29 -9
  88. package/src/http/cors.ts +3 -0
  89. package/src/http/sse.ts +229 -5
  90. package/src/index.ts +119 -20
  91. package/src/integrations/api-integrations.ts +365 -0
  92. package/src/integrations/atlassian.ts +1621 -0
  93. package/src/integrations/fiken.ts +1230 -0
  94. package/src/integrations/github-skill-source.ts +142 -0
  95. package/src/integrations/google-drive.ts +1005 -64
  96. package/src/integrations/oauth-client.ts +123 -33
  97. package/src/integrations/provider-oauth.ts +756 -0
  98. package/src/integrations/slack-bot.ts +669 -11
  99. package/src/integrations/slack-interactions.ts +2063 -93
  100. package/src/integrations/social-api.ts +11 -0
  101. package/src/interaction-holder-heartbeat.ts +95 -0
  102. package/src/interaction-metrics.ts +159 -0
  103. package/src/mcp/documents.ts +146 -6
  104. package/src/mcp/editable-artifact-query-schema.ts +236 -0
  105. package/src/mcp/editable-artifacts.ts +448 -0
  106. package/src/mcp/scheduled-task-view.ts +34 -0
  107. package/src/mcp/server.ts +1756 -355
  108. package/src/memory-slack-delivery.ts +209 -0
  109. package/src/model-catalog.ts +19 -6
  110. package/src/routes/api-integrations.ts +410 -0
  111. package/src/routes/browser-identities.ts +160 -0
  112. package/src/routes/browser-sessions.ts +4482 -0
  113. package/src/routes/canonical-human-identities.ts +156 -0
  114. package/src/routes/channels.ts +90 -0
  115. package/src/routes/company-profile.ts +255 -0
  116. package/src/routes/computer-sessions.ts +1602 -0
  117. package/src/routes/connections.ts +421 -99
  118. package/src/routes/documents.ts +22 -3
  119. package/src/routes/editable-artifacts.ts +1159 -0
  120. package/src/routes/files.ts +106 -4
  121. package/src/routes/github.ts +18 -2
  122. package/src/routes/install.ts +7 -1
  123. package/src/routes/integration-facets.ts +258 -0
  124. package/src/routes/interaction-resources.ts +595 -0
  125. package/src/routes/memory-slack-publications.ts +216 -0
  126. package/src/routes/organization-memberships.ts +53 -0
  127. package/src/routes/packs.ts +437 -7
  128. package/src/routes/plugins.ts +752 -0
  129. package/src/routes/rigs.ts +77 -20
  130. package/src/routes/scheduled-tasks.ts +67 -40
  131. package/src/routes/sessions.ts +830 -278
  132. package/src/routes/skills.ts +256 -0
  133. package/src/routes/slack-task-policy.ts +115 -0
  134. package/src/routes/supergrok.ts +717 -0
  135. package/src/routes/transcription-recordings.ts +9 -2
  136. package/src/routes/transcriptions.ts +2 -1
  137. package/src/routes/video-generation.ts +162 -0
  138. package/src/routes/workspaces.ts +68 -28
  139. package/src/sandbox/channel-a.ts +750 -116
  140. package/src/sandbox/machines.ts +5 -5
  141. package/src/sandbox/metrics-ingestion.ts +121 -3
  142. package/src/sandbox/rematerialize.ts +35 -47
  143. package/src/sandbox/viewer.ts +39 -12
  144. package/src/slack-reaction-files.ts +181 -0
  145. package/src/temporal-schedule-cleanup.ts +135 -0
  146. package/src/transcription/providers/xai-subscription.ts +110 -0
  147. package/src/transcription/service.ts +17 -2
  148. package/src/xai-realtime.ts +216 -0
  149. package/src/xai-subscription-auth.ts +132 -0
  150. package/dist/chunk-T4T2PGU4.js.map +0 -1
  151. package/dist/mcp/toolspace.d.ts +0 -71
  152. package/src/mcp/toolspace.ts +0 -1190
@@ -8,7 +8,10 @@ import {
8
8
  RETAINED_OUTPUT_MAX_PAGE_BYTES,
9
9
  RetainedArtifactMetadataSchema,
10
10
  retainedArtifactReferenceFromFile,
11
+ retainedGeneratedImageReferenceFromFile,
12
+ retainedGeneratedVideoReferenceFromFile,
11
13
  retainedScreenshotReferenceFromFile,
14
+ VideoArtifactPlaybackSource,
12
15
  resolveRetainedOutputRange,
13
16
  type RetainedArtifactMetadata,
14
17
  type RetainedOutputUnavailableReason,
@@ -19,10 +22,14 @@ import {
19
22
  completeFileUpload,
20
23
  createFileUpload,
21
24
  getFileUpload,
25
+ getGeneratedImageArtifact,
26
+ getGeneratedVideoArtifact,
22
27
  getRetainedFileArtifact,
23
28
  getRetainedScreenshotArtifact,
24
29
  requireFile,
25
30
  type RetainedFileArtifact,
31
+ type GeneratedImageArtifact,
32
+ type GeneratedVideoArtifact,
26
33
  type RetainedScreenshotArtifact,
27
34
  } from "@opengeni/db";
28
35
  import type { Context, Hono } from "hono";
@@ -270,18 +277,18 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
270
277
  const workspaceId = c.req.param("workspaceId");
271
278
  await requireAccessGrant(c, deps, workspaceId, "files:read");
272
279
  const artifactId = retainedArtifactId(c.req.param("artifactId"));
273
- const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId);
280
+ const artifact = await getWorkspaceArtifact(db, workspaceId, artifactId);
274
281
  if (!artifact) {
275
282
  return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
276
283
  }
277
- return c.json(retainedArtifactMetadata(artifact));
284
+ return c.json(artifact.metadata);
278
285
  });
279
286
 
280
287
  app.get("/v1/workspaces/:workspaceId/artifacts/:artifactId/content", async (c) => {
281
288
  const workspaceId = c.req.param("workspaceId");
282
289
  await requireAccessGrant(c, deps, workspaceId, "files:read");
283
290
  const artifactId = retainedArtifactId(c.req.param("artifactId"));
284
- const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId);
291
+ const artifact = await getWorkspaceArtifact(db, workspaceId, artifactId);
285
292
  if (!artifact) {
286
293
  return c.json(retainedArtifactUnavailable(artifactId, "deleted"), 404);
287
294
  }
@@ -291,7 +298,40 @@ export function registerFileRoutes(app: Hono, deps: ApiRouteDeps): void {
291
298
  objectStorage,
292
299
  artifactId,
293
300
  artifact.file,
294
- retainedArtifactMetadata(artifact),
301
+ artifact.metadata,
302
+ );
303
+ });
304
+
305
+ app.post("/v1/workspaces/:workspaceId/artifacts/:artifactId/playback-source", async (c) => {
306
+ const workspaceId = c.req.param("workspaceId");
307
+ await requireAccessGrant(c, deps, workspaceId, "files:read");
308
+ if (!objectStorage) {
309
+ throw new HTTPException(503, { message: "object storage is not configured" });
310
+ }
311
+ const artifactId = retainedArtifactId(c.req.param("artifactId"));
312
+ const retained = await getGeneratedVideoArtifact(db, workspaceId, artifactId);
313
+ if (!retained || retained.artifact.deletedAt) {
314
+ throw new HTTPException(404, { message: "generated video not found" });
315
+ }
316
+ if (retained.file.status !== "ready") {
317
+ throw new HTTPException(409, { message: `generated video is ${retained.file.status}` });
318
+ }
319
+ const file = generatedVideoFileAsset(retained.file);
320
+ if (!(await objectStorage.fileExists(file))) {
321
+ throw new HTTPException(410, { message: "generated video bytes are unavailable" });
322
+ }
323
+ const signed = await objectStorage.createGetUrl({ key: file.objectKey });
324
+ return c.json(
325
+ VideoArtifactPlaybackSource.parse({
326
+ schemaVersion: 1,
327
+ artifactId,
328
+ url: signed.url,
329
+ expiresAt: signed.expiresAt.toISOString(),
330
+ contentType: "video/mp4",
331
+ sizeBytes: file.sizeBytes,
332
+ sha256: file.sha256,
333
+ acceptRanges: "bytes",
334
+ }),
295
335
  );
296
336
  });
297
337
 
@@ -495,6 +535,68 @@ function retainedArtifactMetadata(artifact: RetainedFileArtifact): RetainedArtif
495
535
  return retainedArtifactUnavailable(file.id, "unsupported");
496
536
  }
497
537
 
538
+ async function getWorkspaceArtifact(
539
+ db: ApiRouteDeps["db"],
540
+ workspaceId: string,
541
+ artifactId: string,
542
+ ): Promise<{ file: RetainedFileArtifact["file"]; metadata: RetainedArtifactMetadata } | null> {
543
+ const generated = await getGeneratedImageArtifact(db, workspaceId, artifactId);
544
+ if (generated) {
545
+ return { file: generated.file, metadata: retainedGeneratedImageMetadata(generated) };
546
+ }
547
+ const generatedVideo = await getGeneratedVideoArtifact(db, workspaceId, artifactId);
548
+ if (generatedVideo && !generatedVideo.artifact.deletedAt) {
549
+ const file = generatedVideoFileAsset(generatedVideo.file);
550
+ return {
551
+ file,
552
+ metadata: retainedGeneratedVideoMetadata({
553
+ artifact: generatedVideo.artifact,
554
+ file,
555
+ }),
556
+ };
557
+ }
558
+ const artifact = await getRetainedFileArtifact(db, workspaceId, artifactId);
559
+ return artifact ? { file: artifact.file, metadata: retainedArtifactMetadata(artifact) } : null;
560
+ }
561
+
562
+ function generatedVideoFileAsset(
563
+ file: NonNullable<Awaited<ReturnType<typeof getGeneratedVideoArtifact>>>["file"],
564
+ ): RetainedFileArtifact["file"] {
565
+ return FileAsset.parse({
566
+ ...file,
567
+ createdAt: file.createdAt.toISOString(),
568
+ updatedAt: file.updatedAt.toISOString(),
569
+ });
570
+ }
571
+
572
+ function retainedGeneratedVideoMetadata(artifact: {
573
+ artifact: GeneratedVideoArtifact;
574
+ file: RetainedFileArtifact["file"];
575
+ }): RetainedArtifactMetadata {
576
+ const reference = retainedGeneratedVideoReferenceFromFile({
577
+ ...artifact.file,
578
+ id: artifact.artifact.id,
579
+ width: artifact.artifact.width,
580
+ height: artifact.artifact.height,
581
+ updatedAt: artifact.artifact.readyAt.toISOString(),
582
+ });
583
+ return reference ?? retainedArtifactUnavailable(artifact.artifact.id, "failed");
584
+ }
585
+
586
+ function retainedGeneratedImageMetadata(
587
+ artifact: GeneratedImageArtifact,
588
+ ): RetainedArtifactMetadata {
589
+ if (artifact.status === "ready") {
590
+ const reference = retainedGeneratedImageReferenceFromFile({
591
+ ...artifact.file,
592
+ width: artifact.width,
593
+ height: artifact.height,
594
+ });
595
+ if (reference) return reference;
596
+ }
597
+ return retainedArtifactUnavailable(artifact.artifactId, "pending");
598
+ }
599
+
498
600
  function retainedScreenshotMetadata(
499
601
  artifact: RetainedScreenshotArtifact,
500
602
  ): RetainedArtifactMetadata {
@@ -38,6 +38,7 @@ import {
38
38
  githubBrowserBaseUrl,
39
39
  githubBrowserGrantClaims,
40
40
  githubBrowserGrantFromState,
41
+ githubSessionReturnPath,
41
42
  } from "../github-browser-flow";
42
43
  import {
43
44
  githubBindingStatus,
@@ -64,12 +65,14 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
64
65
  const status = githubBindingStatus(missing.length === 0, installations);
65
66
  const setupMode = settings.productAccessMode === "managed" ? "platform" : "operator";
66
67
  const canManage = hasPermission(grant.permissions, "github:manage");
68
+ const returnPath = githubSessionReturnPath(c.req.query("returnPath"), grant.workspaceId);
67
69
  const connectState =
68
70
  missing.length === 0 && slug && canManage
69
71
  ? createSignedState(githubStateSecret, {
70
72
  accountId: grant.accountId,
71
73
  workspaceId: grant.workspaceId,
72
74
  intent: "installation_authority",
75
+ ...(returnPath ? { returnPath } : {}),
73
76
  ...githubBrowserGrantClaims(settings, grant),
74
77
  })
75
78
  : null;
@@ -517,7 +520,12 @@ export function registerGitHubRoutes(app: Hono, deps: ApiRouteDeps): void {
517
520
  return c.html(
518
521
  githubSetupSuccessHtml(
519
522
  proof.installation.accountLogin ?? `installation ${installationId}`,
520
- openGeniReturnUrl(settings, c, grant.workspaceId),
523
+ openGeniReturnUrl(
524
+ settings,
525
+ c,
526
+ grant.workspaceId,
527
+ typeof statePayload.returnPath === "string" ? statePayload.returnPath : null,
528
+ ),
521
529
  ),
522
530
  );
523
531
  });
@@ -879,8 +887,16 @@ function openGeniReturnUrl(
879
887
  settings: ApiRouteDeps["settings"],
880
888
  c: Context,
881
889
  workspaceId: string,
890
+ returnPath: string | null = null,
882
891
  ): string {
883
- const url = new URL(openGeniBaseUrl(settings, c) || new URL(c.req.url).origin);
892
+ const baseUrl = openGeniBaseUrl(settings, c) || new URL(c.req.url).origin;
893
+ const safeReturnPath = githubSessionReturnPath(returnPath, workspaceId);
894
+ const url = safeReturnPath ? new URL(safeReturnPath, `${baseUrl}/`) : new URL(baseUrl);
895
+ if (safeReturnPath) {
896
+ url.searchParams.delete("capability_auth");
897
+ url.searchParams.set("github", "connected");
898
+ return url.toString();
899
+ }
884
900
  url.searchParams.set("workspaceId", workspaceId);
885
901
  return url.toString();
886
902
  }
@@ -1,3 +1,4 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { readFile, stat } from "node:fs/promises";
2
3
  import type { Hono } from "hono";
3
4
  import { HTTPException } from "hono/http-exception";
@@ -27,7 +28,12 @@ import type { ApiRouteDeps } from "@opengeni/core";
27
28
  // The committed install artifacts, resolved relative to this module so the API
28
29
  // (run from source under /app via bun) locates the sibling agent/install/ dir at
29
30
  // runtime. apps/api/src/routes -> ../../../../agent/install.
30
- const INSTALL_DIR = new URL("../../../../agent/install/", import.meta.url);
31
+ const INSTALL_DIR =
32
+ [
33
+ new URL("./assets/agent-install/", import.meta.url),
34
+ new URL("../../../../agent/install/", import.meta.url),
35
+ ].find((candidate) => existsSync(candidate)) ??
36
+ new URL("../../../../agent/install/", import.meta.url);
31
37
 
32
38
  // The baked release-binary dir (a sibling of the committed scripts). The build's
33
39
  // signing step writes the per-SHA Linux musl binaries + their `.sha256`/`.minisig`
@@ -0,0 +1,258 @@
1
+ import {
2
+ IntegrationFacetMutationResult,
3
+ IntegrationFacetRemovalResult,
4
+ IntegrationInstanceFacetsResponse,
5
+ MutateIntegrationFacetRequest,
6
+ UpsertIntegrationFacetRequest,
7
+ } from "@opengeni/contracts";
8
+ import {
9
+ hasPermission,
10
+ requireAccessGrant,
11
+ requireAccessGrantAuthorization,
12
+ type ApiRouteDeps,
13
+ } from "@opengeni/core";
14
+ import {
15
+ configureIntegrationFacet,
16
+ IntegrationFacetBindingOwnershipConflictError,
17
+ IntegrationFacetBindingVersionConflictError,
18
+ IntegrationFacetBindingVersionRequiredError,
19
+ IntegrationFacetConfigError,
20
+ IntegrationFacetConnectionError,
21
+ IntegrationFacetNotFoundError,
22
+ IntegrationFacetOperationIdempotencyError,
23
+ listIntegrationInstanceFacets,
24
+ removeIntegrationFacet,
25
+ setIntegrationFacetLifecycle,
26
+ } from "@opengeni/db";
27
+ import type { Hono } from "hono";
28
+ import { HTTPException } from "hono/http-exception";
29
+
30
+ import {
31
+ browseGoogleDriveFacetSource,
32
+ saveGoogleDriveFacetSource,
33
+ } from "../integrations/google-drive";
34
+
35
+ export function registerIntegrationFacetRoutes(app: Hono, deps: ApiRouteDeps): void {
36
+ app.get(
37
+ "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/facets/:facetKey/browse",
38
+ async (c) => {
39
+ const workspaceId = c.req.param("workspaceId");
40
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
41
+ try {
42
+ return c.json(
43
+ await browseGoogleDriveFacetSource(deps, {
44
+ workspaceId,
45
+ subjectId: grant.subjectId,
46
+ capabilityId: decoded(c.req.param("capabilityId")),
47
+ instanceKey: decoded(c.req.param("instanceKey")),
48
+ facetKey: decoded(c.req.param("facetKey")),
49
+ parentId: c.req.query("parentId") ?? "root",
50
+ ...(c.req.query("pageToken") ? { pageToken: c.req.query("pageToken") } : {}),
51
+ }),
52
+ );
53
+ } catch (error) {
54
+ throw facetHttpError(error);
55
+ }
56
+ },
57
+ );
58
+
59
+ app.put(
60
+ "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/facets/:facetKey/source",
61
+ async (c) => {
62
+ const workspaceId = c.req.param("workspaceId");
63
+ const authorization = await requireAccessGrantAuthorization(
64
+ c,
65
+ deps,
66
+ workspaceId,
67
+ "workspace:admin",
68
+ );
69
+ const { grant } = authorization;
70
+ try {
71
+ return c.json(
72
+ IntegrationFacetMutationResult.parse(
73
+ await saveGoogleDriveFacetSource(deps, {
74
+ accountId: grant.accountId,
75
+ workspaceId,
76
+ subjectId: grant.subjectId,
77
+ capabilityId: decoded(c.req.param("capabilityId")),
78
+ instanceKey: decoded(c.req.param("instanceKey")),
79
+ facetKey: decoded(c.req.param("facetKey")),
80
+ payload: await c.req.json(),
81
+ canManageOrganizationDestination:
82
+ authorization.accountGrant?.permissions.includes("account:admin") === true,
83
+ canManageWorkspaceDestination: hasPermission(grant.permissions, "workspace:admin"),
84
+ canManagePersonalDestination:
85
+ authorization.contextIntegrity &&
86
+ authorization.authenticatedSubjectId === grant.subjectId,
87
+ }),
88
+ ),
89
+ );
90
+ } catch (error) {
91
+ throw facetHttpError(error);
92
+ }
93
+ },
94
+ );
95
+
96
+ app.get(
97
+ "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/facets",
98
+ async (c) => {
99
+ const workspaceId = c.req.param("workspaceId");
100
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
101
+ try {
102
+ return c.json(
103
+ IntegrationInstanceFacetsResponse.parse(
104
+ await listIntegrationInstanceFacets(
105
+ deps.db,
106
+ workspaceId,
107
+ grant.subjectId,
108
+ decoded(c.req.param("capabilityId")),
109
+ decoded(c.req.param("instanceKey")),
110
+ ),
111
+ ),
112
+ );
113
+ } catch (error) {
114
+ throw facetHttpError(error);
115
+ }
116
+ },
117
+ );
118
+
119
+ app.put(
120
+ "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/facets/:facetKey",
121
+ async (c) => {
122
+ const workspaceId = c.req.param("workspaceId");
123
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
124
+ const payload = UpsertIntegrationFacetRequest.parse(await c.req.json());
125
+ const capabilityId = decoded(c.req.param("capabilityId"));
126
+ const instanceKey = decoded(c.req.param("instanceKey"));
127
+ const facetKey = decoded(c.req.param("facetKey"));
128
+ try {
129
+ const instance = await listIntegrationInstanceFacets(
130
+ deps.db,
131
+ workspaceId,
132
+ grant.subjectId,
133
+ capabilityId,
134
+ instanceKey,
135
+ );
136
+ const definition = instance.facets.find(
137
+ (facet) => facet.definition.facetKey === facetKey,
138
+ )?.definition;
139
+ if (
140
+ definition?.kind === "knowledge_source" &&
141
+ definition.capabilities.provider === "google-drive"
142
+ ) {
143
+ throw new HTTPException(422, {
144
+ message:
145
+ "Google Drive source configuration requires the provider-specific source route",
146
+ });
147
+ }
148
+ return c.json(
149
+ IntegrationFacetMutationResult.parse(
150
+ await configureIntegrationFacet(deps.db, {
151
+ accountId: grant.accountId,
152
+ workspaceId,
153
+ subjectId: grant.subjectId,
154
+ capabilityId,
155
+ instanceKey,
156
+ facetKey,
157
+ displayName: payload.displayName,
158
+ config: payload.config,
159
+ ...(payload.expectedVersion !== undefined
160
+ ? { expectedVersion: payload.expectedVersion }
161
+ : {}),
162
+ idempotencyKey: payload.idempotencyKey,
163
+ }),
164
+ ),
165
+ payload.expectedVersion === undefined ? 201 : 200,
166
+ );
167
+ } catch (error) {
168
+ throw facetHttpError(error);
169
+ }
170
+ },
171
+ );
172
+
173
+ for (const action of ["pause", "resume"] as const) {
174
+ app.post(
175
+ `/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/facets/:facetKey/${action}`,
176
+ async (c) => {
177
+ const workspaceId = c.req.param("workspaceId");
178
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
179
+ const payload = MutateIntegrationFacetRequest.parse(await c.req.json());
180
+ try {
181
+ return c.json(
182
+ IntegrationFacetMutationResult.parse(
183
+ await setIntegrationFacetLifecycle(deps.db, {
184
+ accountId: grant.accountId,
185
+ workspaceId,
186
+ subjectId: grant.subjectId,
187
+ capabilityId: decoded(c.req.param("capabilityId")),
188
+ instanceKey: decoded(c.req.param("instanceKey")),
189
+ facetKey: decoded(c.req.param("facetKey")),
190
+ action,
191
+ expectedVersion: payload.expectedVersion,
192
+ idempotencyKey: payload.idempotencyKey,
193
+ }),
194
+ ),
195
+ );
196
+ } catch (error) {
197
+ throw facetHttpError(error);
198
+ }
199
+ },
200
+ );
201
+ }
202
+
203
+ app.delete(
204
+ "/v1/workspaces/:workspaceId/integrations/:capabilityId/instances/:instanceKey/facets/:facetKey",
205
+ async (c) => {
206
+ const workspaceId = c.req.param("workspaceId");
207
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
208
+ const payload = MutateIntegrationFacetRequest.parse(await c.req.json());
209
+ try {
210
+ return c.json(
211
+ IntegrationFacetRemovalResult.parse(
212
+ await removeIntegrationFacet(deps.db, {
213
+ accountId: grant.accountId,
214
+ workspaceId,
215
+ subjectId: grant.subjectId,
216
+ capabilityId: decoded(c.req.param("capabilityId")),
217
+ instanceKey: decoded(c.req.param("instanceKey")),
218
+ facetKey: decoded(c.req.param("facetKey")),
219
+ expectedVersion: payload.expectedVersion,
220
+ idempotencyKey: payload.idempotencyKey,
221
+ }),
222
+ ),
223
+ );
224
+ } catch (error) {
225
+ throw facetHttpError(error);
226
+ }
227
+ },
228
+ );
229
+ }
230
+
231
+ function decoded(value: string): string {
232
+ return decodeURIComponent(value);
233
+ }
234
+
235
+ function facetHttpError(error: unknown): HTTPException {
236
+ if (error instanceof HTTPException) return error;
237
+ if (error instanceof IntegrationFacetNotFoundError) {
238
+ return new HTTPException(404, { message: error.message });
239
+ }
240
+ if (
241
+ error instanceof IntegrationFacetBindingVersionConflictError ||
242
+ error instanceof IntegrationFacetBindingVersionRequiredError ||
243
+ error instanceof IntegrationFacetBindingOwnershipConflictError ||
244
+ error instanceof IntegrationFacetOperationIdempotencyError
245
+ ) {
246
+ return new HTTPException(409, {
247
+ message:
248
+ "The Integration facet changed, is shared, or reused an idempotency key. Refresh before retrying.",
249
+ });
250
+ }
251
+ if (
252
+ error instanceof IntegrationFacetConfigError ||
253
+ error instanceof IntegrationFacetConnectionError
254
+ ) {
255
+ return new HTTPException(422, { message: error.message });
256
+ }
257
+ throw error;
258
+ }