@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
@@ -1,8 +1,19 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import {
3
+ ATLASSIAN_PROVIDER_DOMAIN,
4
+ AtlassianConnectionMetadata,
5
+ AtlassianDisconnectRequest,
6
+ AtlassianLifecycleActionRequest,
7
+ AtlassianOAuthStartRequest,
8
+ AtlassianOAuthStartResponse,
9
+ } from "@opengeni/contracts/atlassian";
10
+ import {
11
+ API_INTEGRATION_OAUTH_CREDENTIAL_ROLE,
12
+ ApiIntegrationOAuthStartRequest,
3
13
  ConnectionResponse,
4
14
  CreateConnectionRequest,
5
15
  IntegrationClientMetadata,
16
+ ListSlackInstallationBindingsResponse,
6
17
  ListConnectionsResponse,
7
18
  OpenGeniSlackBotInstallRequest,
8
19
  OpenGeniSlackBotInstallStart,
@@ -34,17 +45,18 @@ import {
34
45
  import {
35
46
  consumeIntegrationOAuthStateNonce,
36
47
  createConnection,
37
- createConnectionWithSlackBotSuccessAudit,
38
48
  encryptEnvironmentValue,
39
49
  getConnectionMetadata,
40
50
  getWorkspaceGrant,
41
51
  listConnectionsMetadata,
52
+ listSlackInstallationBindings,
53
+ persistSlackBotInstallationWithSuccessAudit,
42
54
  recordSlackBotInstallCallbackFailure,
43
55
  revokeConnection,
44
56
  revokeConnectionWithSlackBotSuccessAudit,
45
57
  SlackBotLifecycleSuccessAuditError,
58
+ SlackInstallationBindingConflictError,
46
59
  updateConnection,
47
- updateConnectionWithSlackBotSuccessAudit,
48
60
  updateSlackBotDocumentDestination,
49
61
  type SlackBotInstallCallbackFailureReason,
50
62
  type SlackBotInstallCallbackFailureStage,
@@ -60,6 +72,19 @@ import {
60
72
  startGoogleDriveOAuth,
61
73
  transitionGoogleDriveLifecycle,
62
74
  } from "../integrations/google-drive";
75
+ import {
76
+ completeApiIntegrationProviderOAuth,
77
+ isApiIntegrationProviderOAuthState,
78
+ startApiIntegrationProviderOAuth,
79
+ } from "../integrations/provider-oauth";
80
+ import {
81
+ browseAtlassianSources,
82
+ completeAtlassianOAuthCallback,
83
+ disconnectAtlassian,
84
+ saveAtlassianSources,
85
+ startAtlassianOAuth,
86
+ transitionAtlassianLifecycle,
87
+ } from "../integrations/atlassian";
63
88
  import {
64
89
  completeMcpOAuthCallback,
65
90
  integrationBaseUrl,
@@ -109,16 +134,31 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
109
134
  );
110
135
  });
111
136
 
137
+ app.get("/v1/workspaces/:workspaceId/connections/slack-bot/bindings", async (c) => {
138
+ const workspaceId = c.req.param("workspaceId");
139
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
140
+ return c.json(
141
+ ListSlackInstallationBindingsResponse.parse({
142
+ bindings: await listSlackInstallationBindings(db, {
143
+ accountId: grant.accountId,
144
+ workspaceId,
145
+ }),
146
+ }),
147
+ );
148
+ });
149
+
112
150
  app.post("/v1/workspaces/:workspaceId/connections", async (c) => {
113
151
  const workspaceId = c.req.param("workspaceId");
114
152
  const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
115
153
  const payload = CreateConnectionRequest.parse(await c.req.json());
116
154
  assertNotReservedSlackBotMetadata(payload.metadata);
155
+ assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
117
156
  const key = requireEnvironmentEncryption(settings);
118
157
  const subjectId = createConnectionSubjectId(payload, grant.subjectId);
119
158
  const providerDomain = canonicalProviderDomain(payload.providerDomain);
120
159
  assertNotDirectPersonalSlackOAuth(providerDomain, payload.kind);
121
160
  assertNotDirectGoogleDriveOAuth(providerDomain, payload.kind, payload.metadata);
161
+ assertNotDirectAtlassianOAuth(providerDomain, payload.kind, payload.metadata);
122
162
  const connection = await createConnection(db, {
123
163
  accountId: grant.accountId,
124
164
  workspaceId,
@@ -287,9 +327,30 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
287
327
  );
288
328
  });
289
329
 
290
- app.get("/v1/integrations/google-drive/callback", async (c) => {
330
+ app.post("/v1/workspaces/:workspaceId/connections/atlassian/install", async (c) => {
331
+ assertIntegrationsEnabled();
332
+ const workspaceId = c.req.param("workspaceId");
333
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
334
+ const parsed = AtlassianOAuthStartRequest.safeParse(await c.req.json());
335
+ if (!parsed.success) {
336
+ throw new HTTPException(400, { message: "invalid Atlassian install request" });
337
+ }
338
+ return c.json(
339
+ AtlassianOAuthStartResponse.parse(
340
+ await startAtlassianOAuth(deps, {
341
+ accountId: grant.accountId,
342
+ workspaceId,
343
+ subjectId: grant.subjectId,
344
+ requestUrl: c.req.url,
345
+ payload: parsed.data,
346
+ }),
347
+ ),
348
+ );
349
+ });
350
+
351
+ app.get("/v1/integrations/atlassian/callback", async (c) => {
291
352
  assertIntegrationsEnabled();
292
- const result = await completeGoogleDriveOAuthCallback(deps, {
353
+ const result = await completeAtlassianOAuthCallback(deps, {
293
354
  ...(c.req.query("code") ? { code: c.req.query("code") } : {}),
294
355
  ...(c.req.query("state") ? { state: c.req.query("state") } : {}),
295
356
  ...(c.req.query("error") ? { error: c.req.query("error") } : {}),
@@ -298,6 +359,82 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
298
359
  return c.redirect(result.redirectTo, 302);
299
360
  });
300
361
 
362
+ app.patch(
363
+ "/v1/workspaces/:workspaceId/connections/atlassian/:connectionId/lifecycle",
364
+ async (c) => {
365
+ assertIntegrationsEnabled();
366
+ const workspaceId = c.req.param("workspaceId");
367
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
368
+ const parsed = AtlassianLifecycleActionRequest.safeParse(await c.req.json());
369
+ if (!parsed.success) {
370
+ throw new HTTPException(400, { message: "invalid Atlassian lifecycle request" });
371
+ }
372
+ return c.json(
373
+ ConnectionResponse.parse({
374
+ connection: await transitionAtlassianLifecycle(deps, {
375
+ workspaceId,
376
+ subjectId: grant.subjectId,
377
+ connectionId: c.req.param("connectionId"),
378
+ payload: parsed.data,
379
+ }),
380
+ }),
381
+ );
382
+ },
383
+ );
384
+
385
+ app.get("/v1/workspaces/:workspaceId/connections/atlassian/:connectionId/browse", async (c) => {
386
+ assertIntegrationsEnabled();
387
+ const workspaceId = c.req.param("workspaceId");
388
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
389
+ return c.json(
390
+ await browseAtlassianSources(deps, {
391
+ workspaceId,
392
+ subjectId: grant.subjectId,
393
+ connectionId: c.req.param("connectionId"),
394
+ }),
395
+ );
396
+ });
397
+
398
+ app.post("/v1/workspaces/:workspaceId/connections/atlassian/:connectionId/source", async (c) => {
399
+ assertIntegrationsEnabled();
400
+ const workspaceId = c.req.param("workspaceId");
401
+ const authorization = await requireAccessGrantAuthorization(
402
+ c,
403
+ deps,
404
+ workspaceId,
405
+ "connections:write",
406
+ );
407
+ const { grant } = authorization;
408
+ const connection = await saveAtlassianSources(deps, {
409
+ accountId: grant.accountId,
410
+ workspaceId,
411
+ subjectId: grant.subjectId,
412
+ grant,
413
+ connectionId: c.req.param("connectionId"),
414
+ payload: await c.req.json(),
415
+ canManageOrganizationDestination:
416
+ authorization.accountGrant?.permissions.includes("account:admin") === true,
417
+ canManageWorkspaceDestination: hasPermission(grant.permissions, "workspace:admin"),
418
+ canManagePersonalDestination:
419
+ authorization.contextIntegrity && authorization.authenticatedSubjectId === grant.subjectId,
420
+ });
421
+ return c.json(ConnectionResponse.parse({ connection }));
422
+ });
423
+
424
+ app.get("/v1/integrations/google-drive/callback", async (c) => {
425
+ assertIntegrationsEnabled();
426
+ const input = {
427
+ ...(c.req.query("code") ? { code: c.req.query("code") } : {}),
428
+ ...(c.req.query("state") ? { state: c.req.query("state") } : {}),
429
+ ...(c.req.query("error") ? { error: c.req.query("error") } : {}),
430
+ requestUrl: c.req.url,
431
+ };
432
+ const result = isApiIntegrationProviderOAuthState(input.state, deps.settings)
433
+ ? await completeApiIntegrationProviderOAuth(deps, input)
434
+ : await completeGoogleDriveOAuthCallback(deps, input);
435
+ return c.redirect(result.redirectTo, 302);
436
+ });
437
+
301
438
  app.patch(
302
439
  "/v1/workspaces/:workspaceId/connections/google-drive/:connectionId/lifecycle",
303
440
  async (c) => {
@@ -355,6 +492,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
355
492
  accountId: grant.accountId,
356
493
  workspaceId,
357
494
  subjectId: grant.subjectId,
495
+ grant,
358
496
  connectionId: c.req.param("connectionId"),
359
497
  payload: await c.req.json(),
360
498
  canManageOrganizationDestination:
@@ -394,6 +532,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
394
532
  const { grant } = authorization;
395
533
  const payload = UpdateConnectionRequest.parse(await c.req.json());
396
534
  assertNotReservedSlackBotMetadata(payload.metadata);
535
+ assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
397
536
  const existing = await getConnectionMetadata(
398
537
  db,
399
538
  workspaceId,
@@ -470,6 +609,16 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
470
609
  message: "use the dedicated Google Drive reconnect or source-selection flow",
471
610
  });
472
611
  }
612
+ if (existing?.metadata.credentialRole === API_INTEGRATION_OAUTH_CREDENTIAL_ROLE) {
613
+ throw new HTTPException(422, {
614
+ message: "use the dedicated Integration provider OAuth flow to update this connection",
615
+ });
616
+ }
617
+ if (existing && AtlassianConnectionMetadata.safeParse(existing.metadata).success) {
618
+ throw new HTTPException(422, {
619
+ message: "use the dedicated Atlassian reconnect or source-selection flow",
620
+ });
621
+ }
473
622
  if (existing) {
474
623
  const providerDomain = canonicalProviderDomain(
475
624
  payload.providerDomain ?? existing.providerDomain,
@@ -477,6 +626,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
477
626
  const kind = payload.kind ?? existing.kind;
478
627
  assertNotDirectPersonalSlackOAuth(providerDomain, kind);
479
628
  assertNotDirectGoogleDriveOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
629
+ assertNotDirectAtlassianOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
480
630
  }
481
631
  // Status is not a free-form field: revocation goes through DELETE, and the
482
632
  // broker owns needs_reauth/error. Reactivating a connection is only
@@ -538,8 +688,14 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
538
688
  existing.providerDomain === GOOGLE_DRIVE_PROVIDER_DOMAIN &&
539
689
  existing.kind === "oauth2" &&
540
690
  GoogleDriveConnectionMetadata.safeParse(existing.metadata).success;
691
+ const isAtlassian =
692
+ existing.subjectId === grant.subjectId &&
693
+ existing.providerDomain === ATLASSIAN_PROVIDER_DOMAIN &&
694
+ existing.kind === "oauth2" &&
695
+ AtlassianConnectionMetadata.safeParse(existing.metadata).success;
696
+ const disconnectPayload = await c.req.json().catch(() => null);
541
697
  const googleDriveDisconnect = isGoogleDrive
542
- ? GoogleDriveDisconnectRequest.safeParse(await c.req.json().catch(() => null))
698
+ ? GoogleDriveDisconnectRequest.safeParse(disconnectPayload)
543
699
  : null;
544
700
  if (googleDriveDisconnect && !googleDriveDisconnect.success) {
545
701
  throw new HTTPException(400, {
@@ -548,7 +704,16 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
548
704
  "invalid Google Drive disconnect request",
549
705
  });
550
706
  }
551
- if (existing.status === "revoked" && !isGoogleDrive) {
707
+ const atlassianDisconnect = isAtlassian
708
+ ? AtlassianDisconnectRequest.safeParse(disconnectPayload)
709
+ : null;
710
+ if (atlassianDisconnect && !atlassianDisconnect.success) {
711
+ throw new HTTPException(400, {
712
+ message:
713
+ atlassianDisconnect.error.issues[0]?.message ?? "invalid Atlassian disconnect request",
714
+ });
715
+ }
716
+ if (existing.status === "revoked" && !isGoogleDrive && !isAtlassian) {
552
717
  return c.json(ConnectionResponse.parse({ connection: existing }));
553
718
  }
554
719
  const connection = isGoogleDrive
@@ -558,18 +723,31 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
558
723
  connection: existing,
559
724
  payload: googleDriveDisconnect!.data,
560
725
  })
561
- : isOpenGeniSlackBotConnection(existing)
562
- ? await revokeConnectionWithSlackBotSuccessAudit(db, {
563
- accountId: grant.accountId,
726
+ : isAtlassian
727
+ ? await disconnectAtlassian(deps, {
564
728
  workspaceId,
565
729
  subjectId: grant.subjectId,
566
- connectionId,
567
- expectedVersion: existing.version,
568
- credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
569
- credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
570
- slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
730
+ connection: existing,
731
+ payload: atlassianDisconnect!.data,
571
732
  })
572
- : await revokeConnection(db, workspaceId, connectionId, grant.subjectId, existing.version);
733
+ : isOpenGeniSlackBotConnection(existing)
734
+ ? await revokeConnectionWithSlackBotSuccessAudit(db, {
735
+ accountId: grant.accountId,
736
+ workspaceId,
737
+ subjectId: grant.subjectId,
738
+ connectionId,
739
+ expectedVersion: existing.version,
740
+ credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
741
+ credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
742
+ slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
743
+ })
744
+ : await revokeConnection(
745
+ db,
746
+ workspaceId,
747
+ connectionId,
748
+ grant.subjectId,
749
+ existing.version,
750
+ );
573
751
  if (!connection) {
574
752
  throw new HTTPException(409, { message: "connection changed during disconnect; try again" });
575
753
  }
@@ -600,6 +778,29 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
600
778
  return c.json(OAuthStartResponse.parse(result));
601
779
  });
602
780
 
781
+ app.post("/v1/workspaces/:workspaceId/integrations/oauth/start", async (c) => {
782
+ assertIntegrationsEnabled();
783
+ const workspaceId = c.req.param("workspaceId");
784
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
785
+ const parsed = ApiIntegrationOAuthStartRequest.safeParse(await c.req.json());
786
+ if (!parsed.success) {
787
+ throw new HTTPException(400, {
788
+ message: parsed.error.issues[0]?.message ?? "invalid Integration OAuth start request",
789
+ });
790
+ }
791
+ return c.json(
792
+ OAuthStartResponse.parse(
793
+ await startApiIntegrationProviderOAuth(deps, {
794
+ accountId: grant.accountId,
795
+ workspaceId,
796
+ subjectId: grant.subjectId,
797
+ requestUrl: c.req.url,
798
+ payload: parsed.data,
799
+ }),
800
+ ),
801
+ );
802
+ });
803
+
603
804
  app.get("/v1/integrations/oauth/callback", async (c) => {
604
805
  assertIntegrationsEnabled();
605
806
  const result = await completeMcpOAuthCallback(
@@ -613,6 +814,17 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
613
814
  return c.redirect(result.redirectTo, 302);
614
815
  });
615
816
 
817
+ app.get("/v1/integrations/provider-oauth/callback", async (c) => {
818
+ assertIntegrationsEnabled();
819
+ const result = await completeApiIntegrationProviderOAuth(deps, {
820
+ ...(c.req.query("code") ? { code: c.req.query("code") } : {}),
821
+ ...(c.req.query("state") ? { state: c.req.query("state") } : {}),
822
+ ...(c.req.query("error") ? { error: c.req.query("error") } : {}),
823
+ requestUrl: c.req.url,
824
+ });
825
+ return c.redirect(result.redirectTo, 302);
826
+ });
827
+
616
828
  app.get("/v1/integrations/oauth/client-metadata.json", (c) => {
617
829
  const baseUrl = integrationBaseUrl(settings.publicBaseUrl, c.req.url);
618
830
  const metadataUrl = `${baseUrl}/v1/integrations/oauth/client-metadata.json`;
@@ -670,14 +882,9 @@ async function persistOpenGeniSlackBotConnection(input: {
670
882
  "principal_validation",
671
883
  );
672
884
  }
673
- const existing =
674
- requestedExisting ??
675
- (await findMatchingOpenGeniSlackBotConnection(
676
- db,
677
- input.state.workspaceId,
678
- input.verified.metadata,
679
- ));
680
- const existingMetadata = existing ? openGeniSlackBotMetadata(existing.metadata) : null;
885
+ const existingMetadata = requestedExisting
886
+ ? openGeniSlackBotMetadata(requestedExisting.metadata)
887
+ : null;
681
888
  if (existingMetadata && existingMetadata.slackTeamId !== input.verified.metadata.slackTeamId) {
682
889
  throw new SlackInstallCallbackError(
683
890
  409,
@@ -699,83 +906,36 @@ async function persistOpenGeniSlackBotConnection(input: {
699
906
  );
700
907
  }
701
908
  const verifiedInstallAt = new Date(input.verified.metadata.verifiedAt);
702
- const lifecycleAudit = {
703
- accountId: input.state.accountId,
704
- workspaceId: input.state.workspaceId,
705
- subjectId: input.state.subjectId,
706
- credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
707
- credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
708
- slackTeamId: input.verified.metadata.slackTeamId,
709
- };
710
- const connection = existing
711
- ? await updateConnectionWithSlackBotSuccessAudit(db, {
712
- ...lifecycleAudit,
713
- connection: {
714
- workspaceId: input.state.workspaceId,
715
- connectionId: existing.id,
716
- visibleToSubjectId: input.state.subjectId,
717
- expectedVersion: existing.version,
718
- subjectId: null,
719
- providerDomain: "slack.com",
720
- kind: "app_install",
721
- status: "active",
722
- credentialEncrypted,
723
- grantedScopes: input.verified.grantedScopes,
724
- expiresAt: null,
725
- verifiedInstallAt,
726
- verifiedInstallVersion: existing.version + 1,
727
- metadata: input.verified.metadata,
728
- updatedBySubjectId: input.state.subjectId,
729
- },
730
- })
731
- : await createConnectionWithSlackBotSuccessAudit(db, {
732
- ...lifecycleAudit,
733
- connection: {
734
- accountId: input.state.accountId,
735
- workspaceId: input.state.workspaceId,
736
- subjectId: null,
737
- providerDomain: "slack.com",
738
- kind: "app_install",
739
- credentialEncrypted,
740
- grantedScopes: input.verified.grantedScopes,
741
- expiresAt: null,
742
- verifiedInstallAt,
743
- verifiedInstallVersion: 1,
744
- metadata: input.verified.metadata,
745
- createdBySubjectId: input.state.subjectId,
746
- },
747
- });
748
- if (!connection) {
749
- throw new SlackInstallCallbackError(
750
- 409,
751
- "connection_conflict",
752
- "Slack bot connection changed during reinstall; start again",
753
- "principal_validation",
754
- );
755
- }
756
- return connection;
757
- }
758
-
759
- async function findMatchingOpenGeniSlackBotConnection(
760
- db: ApiRouteDeps["db"],
761
- workspaceId: string,
762
- verified: Awaited<ReturnType<typeof verifyOpenGeniSlackBotCredential>>["metadata"],
763
- ) {
764
- const connections = await listConnectionsMetadata(db, workspaceId, null);
765
- return (
766
- connections.find((connection) => {
767
- const metadata = openGeniSlackBotMetadata(connection.metadata);
768
- return (
769
- connection.subjectId === null &&
770
- connection.providerDomain === "slack.com" &&
771
- connection.kind === "app_install" &&
772
- metadata?.credentialRole === OPENGENI_SLACK_BOT_CREDENTIAL_ROLE &&
773
- metadata.slackTeamId === verified.slackTeamId &&
774
- metadata.botId === verified.botId &&
775
- metadata.botUserId === verified.botUserId
909
+ try {
910
+ return await persistSlackBotInstallationWithSuccessAudit(db, {
911
+ accountId: input.state.accountId,
912
+ workspaceId: input.state.workspaceId,
913
+ subjectId: input.state.subjectId,
914
+ credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
915
+ credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
916
+ slackTeamId: input.verified.metadata.slackTeamId,
917
+ ...(input.state.connectionId
918
+ ? {
919
+ requestedConnectionId: input.state.connectionId,
920
+ requestedConnectionVersion: input.state.connectionVersion,
921
+ }
922
+ : {}),
923
+ credentialEncrypted,
924
+ grantedScopes: input.verified.grantedScopes,
925
+ verifiedInstallAt,
926
+ metadata: input.verified.metadata,
927
+ });
928
+ } catch (error) {
929
+ if (error instanceof SlackInstallationBindingConflictError) {
930
+ throw new SlackInstallCallbackError(
931
+ 409,
932
+ "connection_conflict",
933
+ error.message,
934
+ "principal_validation",
776
935
  );
777
- }) ?? null
778
- );
936
+ }
937
+ throw error;
938
+ }
779
939
  }
780
940
 
781
941
  function requireOpenGeniSlackOAuthSettings(settings: ApiRouteDeps["settings"]): {
@@ -952,6 +1112,21 @@ function assertNotDirectGoogleDriveOAuth(
952
1112
  }
953
1113
  }
954
1114
 
1115
+ function assertNotDirectAtlassianOAuth(
1116
+ providerDomain: string,
1117
+ kind: string,
1118
+ metadata: Record<string, unknown> | undefined,
1119
+ ): void {
1120
+ if (
1121
+ (providerDomain === ATLASSIAN_PROVIDER_DOMAIN && kind === "oauth2") ||
1122
+ AtlassianConnectionMetadata.safeParse(metadata).success
1123
+ ) {
1124
+ throw new HTTPException(422, {
1125
+ message: "Atlassian credentials must use the dedicated OAuth connection flow",
1126
+ });
1127
+ }
1128
+ }
1129
+
955
1130
  function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | undefined): void {
956
1131
  if (hasReservedOpenGeniSlackBotMetadata(metadata)) {
957
1132
  throw new HTTPException(422, {
@@ -960,6 +1135,16 @@ function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | u
960
1135
  }
961
1136
  }
962
1137
 
1138
+ function assertNotReservedApiIntegrationOAuthMetadata(
1139
+ metadata: Record<string, unknown> | undefined,
1140
+ ): void {
1141
+ if (metadata?.credentialRole === API_INTEGRATION_OAUTH_CREDENTIAL_ROLE) {
1142
+ throw new HTTPException(422, {
1143
+ message: "API Integration OAuth metadata is reserved for the dedicated provider flow",
1144
+ });
1145
+ }
1146
+ }
1147
+
963
1148
  function writableSubjectId(
964
1149
  requested: string | null | undefined,
965
1150
  grantSubjectId: string,
@@ -21,7 +21,6 @@ import {
21
21
  getKnowledgeMemory,
22
22
  listKnowledgeMemories,
23
23
  updateKnowledgeMemory,
24
- saveWorkspaceMemory,
25
24
  searchWorkspaceMemories,
26
25
  } from "@opengeni/db";
27
26
  import {
@@ -40,7 +39,11 @@ import {
40
39
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
41
40
  import type { Context, Hono } from "hono";
42
41
  import { HTTPException } from "hono/http-exception";
43
- import { requireAccessGrant, requireAccessGrantAuthorization } from "@opengeni/core";
42
+ import {
43
+ requireAccessGrant,
44
+ requireAccessGrantAuthorization,
45
+ saveWorkspaceMemoryWithSlackPublication,
46
+ } from "@opengeni/core";
44
47
  import { recordWorkspaceUsage, requireLimit } from "@opengeni/core";
45
48
  import type { ApiRouteDeps } from "@opengeni/core";
46
49
  import { buildDocumentsMcpServer } from "../mcp/documents";
@@ -561,12 +564,17 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
561
564
  throw new HTTPException(400, { message: "invalid knowledge memory request" });
562
565
  }
563
566
  const payload = parsedBody.data;
567
+ if (payload.status !== "active" && payload.slackPublication) {
568
+ throw new HTTPException(400, {
569
+ message: "Slack publication is available only for active Workspace Memory writes",
570
+ });
571
+ }
564
572
  // status `active` (the default) is a memory write → route through the single
565
573
  // gate (sanitize + embed + dedup). Explicit proposed/approved/rejected keeps
566
574
  // the legacy curated create.
567
575
  if (payload.status === "active") {
568
576
  try {
569
- const result = await saveWorkspaceMemory(
577
+ const result = await saveWorkspaceMemoryWithSlackPublication(
570
578
  db,
571
579
  {
572
580
  accountId: grant.accountId,
@@ -579,6 +587,17 @@ export function registerDocumentRoutes(app: Hono, deps: ApiRouteDeps): void {
579
587
  metadata: payload.metadata,
580
588
  origin: "human",
581
589
  },
590
+ payload.slackPublication
591
+ ? {
592
+ distribution: payload.slackPublication,
593
+ actor: {
594
+ kind: "human",
595
+ subjectId: grant.subjectId,
596
+ initiatingHumanSubjectId: grant.subjectId,
597
+ },
598
+ ownerLabel: grant.subjectLabel ?? null,
599
+ }
600
+ : null,
582
601
  getDocumentServices().embedder,
583
602
  );
584
603
  return c.json(KnowledgeMemory.parse(result.memory), 201);