@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
@@ -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,
@@ -10,6 +21,10 @@ import {
10
21
  OAuthStartResponse,
11
22
  UpdateConnectionRequest,
12
23
  } from "@opengeni/contracts";
24
+ import {
25
+ bindConnectorDocumentDestination,
26
+ ConnectorDocumentDestinationSelection,
27
+ } from "@opengeni/contracts/connector-destinations";
13
28
  import {
14
29
  GOOGLE_DRIVE_PROVIDER_DOMAIN,
15
30
  GoogleDriveConnectionMetadata,
@@ -30,17 +45,19 @@ import {
30
45
  import {
31
46
  consumeIntegrationOAuthStateNonce,
32
47
  createConnection,
33
- createConnectionWithSlackBotSuccessAudit,
34
48
  encryptEnvironmentValue,
35
49
  getConnectionMetadata,
36
50
  getWorkspaceGrant,
37
51
  listConnectionsMetadata,
52
+ listSlackInstallationBindings,
53
+ persistSlackBotInstallationWithSuccessAudit,
38
54
  recordSlackBotInstallCallbackFailure,
39
55
  revokeConnection,
40
56
  revokeConnectionWithSlackBotSuccessAudit,
41
57
  SlackBotLifecycleSuccessAuditError,
58
+ SlackInstallationBindingConflictError,
42
59
  updateConnection,
43
- updateConnectionWithSlackBotSuccessAudit,
60
+ updateSlackBotDocumentDestination,
44
61
  type SlackBotInstallCallbackFailureReason,
45
62
  type SlackBotInstallCallbackFailureStage,
46
63
  } from "@opengeni/db";
@@ -55,6 +72,19 @@ import {
55
72
  startGoogleDriveOAuth,
56
73
  transitionGoogleDriveLifecycle,
57
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";
58
88
  import {
59
89
  completeMcpOAuthCallback,
60
90
  integrationBaseUrl,
@@ -104,16 +134,31 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
104
134
  );
105
135
  });
106
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
+
107
150
  app.post("/v1/workspaces/:workspaceId/connections", async (c) => {
108
151
  const workspaceId = c.req.param("workspaceId");
109
152
  const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
110
153
  const payload = CreateConnectionRequest.parse(await c.req.json());
111
154
  assertNotReservedSlackBotMetadata(payload.metadata);
155
+ assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
112
156
  const key = requireEnvironmentEncryption(settings);
113
157
  const subjectId = createConnectionSubjectId(payload, grant.subjectId);
114
158
  const providerDomain = canonicalProviderDomain(payload.providerDomain);
115
159
  assertNotDirectPersonalSlackOAuth(providerDomain, payload.kind);
116
160
  assertNotDirectGoogleDriveOAuth(providerDomain, payload.kind, payload.metadata);
161
+ assertNotDirectAtlassianOAuth(providerDomain, payload.kind, payload.metadata);
117
162
  const connection = await createConnection(db, {
118
163
  accountId: grant.accountId,
119
164
  workspaceId,
@@ -282,9 +327,30 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
282
327
  );
283
328
  });
284
329
 
285
- app.get("/v1/integrations/google-drive/callback", async (c) => {
330
+ app.post("/v1/workspaces/:workspaceId/connections/atlassian/install", async (c) => {
286
331
  assertIntegrationsEnabled();
287
- const result = await completeGoogleDriveOAuthCallback(deps, {
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) => {
352
+ assertIntegrationsEnabled();
353
+ const result = await completeAtlassianOAuthCallback(deps, {
288
354
  ...(c.req.query("code") ? { code: c.req.query("code") } : {}),
289
355
  ...(c.req.query("state") ? { state: c.req.query("state") } : {}),
290
356
  ...(c.req.query("error") ? { error: c.req.query("error") } : {}),
@@ -293,6 +359,82 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
293
359
  return c.redirect(result.redirectTo, 302);
294
360
  });
295
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
+
296
438
  app.patch(
297
439
  "/v1/workspaces/:workspaceId/connections/google-drive/:connectionId/lifecycle",
298
440
  async (c) => {
@@ -350,6 +492,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
350
492
  accountId: grant.accountId,
351
493
  workspaceId,
352
494
  subjectId: grant.subjectId,
495
+ grant,
353
496
  connectionId: c.req.param("connectionId"),
354
497
  payload: await c.req.json(),
355
498
  canManageOrganizationDestination:
@@ -380,9 +523,16 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
380
523
 
381
524
  app.patch("/v1/workspaces/:workspaceId/connections/:connectionId", async (c) => {
382
525
  const workspaceId = c.req.param("workspaceId");
383
- const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
526
+ const authorization = await requireAccessGrantAuthorization(
527
+ c,
528
+ deps,
529
+ workspaceId,
530
+ "connections:write",
531
+ );
532
+ const { grant } = authorization;
384
533
  const payload = UpdateConnectionRequest.parse(await c.req.json());
385
534
  assertNotReservedSlackBotMetadata(payload.metadata);
535
+ assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
386
536
  const existing = await getConnectionMetadata(
387
537
  db,
388
538
  workspaceId,
@@ -390,15 +540,85 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
390
540
  grant.subjectId,
391
541
  );
392
542
  if (existing && isOpenGeniSlackBotConnection(existing)) {
393
- throw new HTTPException(422, {
394
- message: "use the dedicated OpenGeni Slack bot reinstall flow to update this connection",
543
+ const destination = ConnectorDocumentDestinationSelection.safeParse(
544
+ payload.metadata?.documentDestination,
545
+ );
546
+ const destinationOnlyUpdate =
547
+ payload.metadata !== undefined &&
548
+ Object.keys(payload.metadata).length === 1 &&
549
+ payload.providerDomain === undefined &&
550
+ payload.subjectId === undefined &&
551
+ payload.kind === undefined &&
552
+ payload.status === undefined &&
553
+ payload.credential === undefined &&
554
+ payload.grantedScopes === undefined &&
555
+ payload.expiresAt === undefined;
556
+ if (!destination.success || !destinationOnlyUpdate) {
557
+ throw new HTTPException(422, {
558
+ message: "use the dedicated OpenGeni Slack bot reinstall flow to update this connection",
559
+ });
560
+ }
561
+ const destinationSelection = destination.data;
562
+ if (
563
+ destinationSelection.authorityKind === "organization" &&
564
+ authorization.accountGrant?.permissions.includes("account:admin") !== true
565
+ ) {
566
+ throw new HTTPException(403, { message: "missing permission: account:admin" });
567
+ }
568
+ if (
569
+ destinationSelection.authorityKind === "workspace" &&
570
+ !hasPermission(grant.permissions, "workspace:admin")
571
+ ) {
572
+ throw new HTTPException(403, { message: "missing permission: workspace:admin" });
573
+ }
574
+ if (
575
+ destinationSelection.authorityKind === "personal" &&
576
+ (!authorization.contextIntegrity ||
577
+ authorization.authenticatedSubjectId !== grant.subjectId)
578
+ ) {
579
+ throw new HTTPException(403, {
580
+ message: "personal destination requires the exact actor",
581
+ });
582
+ }
583
+ const metadata = {
584
+ ...existing.metadata,
585
+ documentDestination: bindConnectorDocumentDestination(destinationSelection, {
586
+ accountId: grant.accountId,
587
+ workspaceId,
588
+ initiatingSubjectId: grant.subjectId,
589
+ }),
590
+ };
591
+ const connection = await updateSlackBotDocumentDestination(db, {
592
+ accountId: grant.accountId,
593
+ workspaceId,
594
+ connectionId: existing.id,
595
+ visibleToSubjectId: grant.subjectId,
596
+ expectedVersion: existing.version,
597
+ metadata,
598
+ updatedBySubjectId: grant.subjectId,
395
599
  });
600
+ if (!connection) {
601
+ throw new HTTPException(409, {
602
+ message: "Slack bot connection changed; reload before saving its destination",
603
+ });
604
+ }
605
+ return c.json(ConnectionResponse.parse({ connection }));
396
606
  }
397
607
  if (existing && GoogleDriveConnectionMetadata.safeParse(existing.metadata).success) {
398
608
  throw new HTTPException(422, {
399
609
  message: "use the dedicated Google Drive reconnect or source-selection flow",
400
610
  });
401
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
+ }
402
622
  if (existing) {
403
623
  const providerDomain = canonicalProviderDomain(
404
624
  payload.providerDomain ?? existing.providerDomain,
@@ -406,6 +626,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
406
626
  const kind = payload.kind ?? existing.kind;
407
627
  assertNotDirectPersonalSlackOAuth(providerDomain, kind);
408
628
  assertNotDirectGoogleDriveOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
629
+ assertNotDirectAtlassianOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
409
630
  }
410
631
  // Status is not a free-form field: revocation goes through DELETE, and the
411
632
  // broker owns needs_reauth/error. Reactivating a connection is only
@@ -467,8 +688,14 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
467
688
  existing.providerDomain === GOOGLE_DRIVE_PROVIDER_DOMAIN &&
468
689
  existing.kind === "oauth2" &&
469
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);
470
697
  const googleDriveDisconnect = isGoogleDrive
471
- ? GoogleDriveDisconnectRequest.safeParse(await c.req.json().catch(() => null))
698
+ ? GoogleDriveDisconnectRequest.safeParse(disconnectPayload)
472
699
  : null;
473
700
  if (googleDriveDisconnect && !googleDriveDisconnect.success) {
474
701
  throw new HTTPException(400, {
@@ -477,7 +704,16 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
477
704
  "invalid Google Drive disconnect request",
478
705
  });
479
706
  }
480
- 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) {
481
717
  return c.json(ConnectionResponse.parse({ connection: existing }));
482
718
  }
483
719
  const connection = isGoogleDrive
@@ -487,18 +723,31 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
487
723
  connection: existing,
488
724
  payload: googleDriveDisconnect!.data,
489
725
  })
490
- : isOpenGeniSlackBotConnection(existing)
491
- ? await revokeConnectionWithSlackBotSuccessAudit(db, {
492
- accountId: grant.accountId,
726
+ : isAtlassian
727
+ ? await disconnectAtlassian(deps, {
493
728
  workspaceId,
494
729
  subjectId: grant.subjectId,
495
- connectionId,
496
- expectedVersion: existing.version,
497
- credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
498
- credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
499
- slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
730
+ connection: existing,
731
+ payload: atlassianDisconnect!.data,
500
732
  })
501
- : 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
+ );
502
751
  if (!connection) {
503
752
  throw new HTTPException(409, { message: "connection changed during disconnect; try again" });
504
753
  }
@@ -529,6 +778,29 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
529
778
  return c.json(OAuthStartResponse.parse(result));
530
779
  });
531
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
+
532
804
  app.get("/v1/integrations/oauth/callback", async (c) => {
533
805
  assertIntegrationsEnabled();
534
806
  const result = await completeMcpOAuthCallback(
@@ -542,6 +814,17 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
542
814
  return c.redirect(result.redirectTo, 302);
543
815
  });
544
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
+
545
828
  app.get("/v1/integrations/oauth/client-metadata.json", (c) => {
546
829
  const baseUrl = integrationBaseUrl(settings.publicBaseUrl, c.req.url);
547
830
  const metadataUrl = `${baseUrl}/v1/integrations/oauth/client-metadata.json`;
@@ -599,14 +882,9 @@ async function persistOpenGeniSlackBotConnection(input: {
599
882
  "principal_validation",
600
883
  );
601
884
  }
602
- const existing =
603
- requestedExisting ??
604
- (await findMatchingOpenGeniSlackBotConnection(
605
- db,
606
- input.state.workspaceId,
607
- input.verified.metadata,
608
- ));
609
- const existingMetadata = existing ? openGeniSlackBotMetadata(existing.metadata) : null;
885
+ const existingMetadata = requestedExisting
886
+ ? openGeniSlackBotMetadata(requestedExisting.metadata)
887
+ : null;
610
888
  if (existingMetadata && existingMetadata.slackTeamId !== input.verified.metadata.slackTeamId) {
611
889
  throw new SlackInstallCallbackError(
612
890
  409,
@@ -628,83 +906,36 @@ async function persistOpenGeniSlackBotConnection(input: {
628
906
  );
629
907
  }
630
908
  const verifiedInstallAt = new Date(input.verified.metadata.verifiedAt);
631
- const lifecycleAudit = {
632
- accountId: input.state.accountId,
633
- workspaceId: input.state.workspaceId,
634
- subjectId: input.state.subjectId,
635
- credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
636
- credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
637
- slackTeamId: input.verified.metadata.slackTeamId,
638
- };
639
- const connection = existing
640
- ? await updateConnectionWithSlackBotSuccessAudit(db, {
641
- ...lifecycleAudit,
642
- connection: {
643
- workspaceId: input.state.workspaceId,
644
- connectionId: existing.id,
645
- visibleToSubjectId: input.state.subjectId,
646
- expectedVersion: existing.version,
647
- subjectId: null,
648
- providerDomain: "slack.com",
649
- kind: "app_install",
650
- status: "active",
651
- credentialEncrypted,
652
- grantedScopes: input.verified.grantedScopes,
653
- expiresAt: null,
654
- verifiedInstallAt,
655
- verifiedInstallVersion: existing.version + 1,
656
- metadata: input.verified.metadata,
657
- updatedBySubjectId: input.state.subjectId,
658
- },
659
- })
660
- : await createConnectionWithSlackBotSuccessAudit(db, {
661
- ...lifecycleAudit,
662
- connection: {
663
- accountId: input.state.accountId,
664
- workspaceId: input.state.workspaceId,
665
- subjectId: null,
666
- providerDomain: "slack.com",
667
- kind: "app_install",
668
- credentialEncrypted,
669
- grantedScopes: input.verified.grantedScopes,
670
- expiresAt: null,
671
- verifiedInstallAt,
672
- verifiedInstallVersion: 1,
673
- metadata: input.verified.metadata,
674
- createdBySubjectId: input.state.subjectId,
675
- },
676
- });
677
- if (!connection) {
678
- throw new SlackInstallCallbackError(
679
- 409,
680
- "connection_conflict",
681
- "Slack bot connection changed during reinstall; start again",
682
- "principal_validation",
683
- );
684
- }
685
- return connection;
686
- }
687
-
688
- async function findMatchingOpenGeniSlackBotConnection(
689
- db: ApiRouteDeps["db"],
690
- workspaceId: string,
691
- verified: Awaited<ReturnType<typeof verifyOpenGeniSlackBotCredential>>["metadata"],
692
- ) {
693
- const connections = await listConnectionsMetadata(db, workspaceId, null);
694
- return (
695
- connections.find((connection) => {
696
- const metadata = openGeniSlackBotMetadata(connection.metadata);
697
- return (
698
- connection.subjectId === null &&
699
- connection.providerDomain === "slack.com" &&
700
- connection.kind === "app_install" &&
701
- metadata?.credentialRole === OPENGENI_SLACK_BOT_CREDENTIAL_ROLE &&
702
- metadata.slackTeamId === verified.slackTeamId &&
703
- metadata.botId === verified.botId &&
704
- 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",
705
935
  );
706
- }) ?? null
707
- );
936
+ }
937
+ throw error;
938
+ }
708
939
  }
709
940
 
710
941
  function requireOpenGeniSlackOAuthSettings(settings: ApiRouteDeps["settings"]): {
@@ -881,6 +1112,21 @@ function assertNotDirectGoogleDriveOAuth(
881
1112
  }
882
1113
  }
883
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
+
884
1130
  function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | undefined): void {
885
1131
  if (hasReservedOpenGeniSlackBotMetadata(metadata)) {
886
1132
  throw new HTTPException(422, {
@@ -889,6 +1135,16 @@ function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | u
889
1135
  }
890
1136
  }
891
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
+
892
1148
  function writableSubjectId(
893
1149
  requested: string | null | undefined,
894
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);