@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
@@ -1,8 +1,25 @@
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,
15
+ FIKEN_CREDENTIAL_LABEL,
16
+ FIKEN_CREDENTIAL_ROLE,
17
+ FIKEN_PROVIDER_DOMAIN,
18
+ FikenInstallRequest,
19
+ FikenOAuthStartRequest,
20
+ FikenOAuthStartResponse,
5
21
  IntegrationClientMetadata,
22
+ ListSlackInstallationBindingsResponse,
6
23
  ListConnectionsResponse,
7
24
  OpenGeniSlackBotInstallRequest,
8
25
  OpenGeniSlackBotInstallStart,
@@ -23,28 +40,33 @@ import {
23
40
  GoogleDriveOAuthStartResponse,
24
41
  } from "@opengeni/contracts/google-drive";
25
42
  import {
43
+ fikenConnectionMetadata,
26
44
  hasPermission,
45
+ hasReservedFikenMetadata,
27
46
  hasReservedOpenGeniSlackBotMetadata,
47
+ isFikenConnection,
28
48
  isOpenGeniSlackBotConnection,
29
49
  openGeniSlackBotMetadata,
30
50
  requireAccessGrant,
31
51
  requireAccessGrantAuthorization,
32
52
  requireEnvironmentEncryption,
53
+ resolveFikenDefaultCompanySlug,
33
54
  } from "@opengeni/core";
34
55
  import {
35
56
  consumeIntegrationOAuthStateNonce,
36
57
  createConnection,
37
- createConnectionWithSlackBotSuccessAudit,
38
58
  encryptEnvironmentValue,
39
59
  getConnectionMetadata,
40
60
  getWorkspaceGrant,
41
61
  listConnectionsMetadata,
62
+ listSlackInstallationBindings,
63
+ persistSlackBotInstallationWithSuccessAudit,
42
64
  recordSlackBotInstallCallbackFailure,
43
65
  revokeConnection,
44
66
  revokeConnectionWithSlackBotSuccessAudit,
45
67
  SlackBotLifecycleSuccessAuditError,
68
+ SlackInstallationBindingConflictError,
46
69
  updateConnection,
47
- updateConnectionWithSlackBotSuccessAudit,
48
70
  updateSlackBotDocumentDestination,
49
71
  type SlackBotInstallCallbackFailureReason,
50
72
  type SlackBotInstallCallbackFailureStage,
@@ -60,6 +82,18 @@ import {
60
82
  startGoogleDriveOAuth,
61
83
  transitionGoogleDriveLifecycle,
62
84
  } from "../integrations/google-drive";
85
+ import {
86
+ completeApiIntegrationProviderOAuth,
87
+ startApiIntegrationProviderOAuth,
88
+ } from "../integrations/provider-oauth";
89
+ import {
90
+ browseAtlassianSources,
91
+ completeAtlassianOAuthCallback,
92
+ disconnectAtlassian,
93
+ saveAtlassianSources,
94
+ startAtlassianOAuth,
95
+ transitionAtlassianLifecycle,
96
+ } from "../integrations/atlassian";
63
97
  import {
64
98
  completeMcpOAuthCallback,
65
99
  integrationBaseUrl,
@@ -71,6 +105,12 @@ import {
71
105
  SlackBotCredentialVerificationError,
72
106
  verifyOpenGeniSlackBotCredential,
73
107
  } from "../integrations/slack-bot";
108
+ import {
109
+ completeFikenOAuthCallback,
110
+ fikenCredentialBundle,
111
+ startFikenOAuth,
112
+ verifyFikenApiToken,
113
+ } from "../integrations/fiken";
74
114
  import {
75
115
  OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
76
116
  OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
@@ -109,16 +149,32 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
109
149
  );
110
150
  });
111
151
 
152
+ app.get("/v1/workspaces/:workspaceId/connections/slack-bot/bindings", async (c) => {
153
+ const workspaceId = c.req.param("workspaceId");
154
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
155
+ return c.json(
156
+ ListSlackInstallationBindingsResponse.parse({
157
+ bindings: await listSlackInstallationBindings(db, {
158
+ accountId: grant.accountId,
159
+ workspaceId,
160
+ }),
161
+ }),
162
+ );
163
+ });
164
+
112
165
  app.post("/v1/workspaces/:workspaceId/connections", async (c) => {
113
166
  const workspaceId = c.req.param("workspaceId");
114
167
  const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
115
168
  const payload = CreateConnectionRequest.parse(await c.req.json());
116
169
  assertNotReservedSlackBotMetadata(payload.metadata);
170
+ assertNotReservedFikenMetadata(payload.metadata);
171
+ assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
117
172
  const key = requireEnvironmentEncryption(settings);
118
173
  const subjectId = createConnectionSubjectId(payload, grant.subjectId);
119
174
  const providerDomain = canonicalProviderDomain(payload.providerDomain);
120
175
  assertNotDirectPersonalSlackOAuth(providerDomain, payload.kind);
121
176
  assertNotDirectGoogleDriveOAuth(providerDomain, payload.kind, payload.metadata);
177
+ assertNotDirectAtlassianOAuth(providerDomain, payload.kind, payload.metadata);
122
178
  const connection = await createConnection(db, {
123
179
  accountId: grant.accountId,
124
180
  workspaceId,
@@ -266,6 +322,120 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
266
322
  }
267
323
  });
268
324
 
325
+ // Verified paste-a-token install for the first-party Fiken connector. The
326
+ // token is validated against Fiken (and its accessible companies discovered)
327
+ // before it enters encrypted storage. Workspace-owned only.
328
+ app.post("/v1/workspaces/:workspaceId/connections/fiken/install", async (c) => {
329
+ assertIntegrationsEnabled();
330
+ const workspaceId = c.req.param("workspaceId");
331
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
332
+ const payload = FikenInstallRequest.parse(await c.req.json());
333
+ const key = requireEnvironmentEncryption(settings);
334
+ const existing = payload.connectionId
335
+ ? await getConnectionMetadata(db, workspaceId, payload.connectionId, null)
336
+ : null;
337
+ if (payload.connectionId && !existing) {
338
+ throw new HTTPException(404, { message: "connection not found" });
339
+ }
340
+ if (existing && !isFikenConnection(existing)) {
341
+ throw new HTTPException(422, { message: "connectionId is not a Fiken connection" });
342
+ }
343
+ const verified = await verifyFikenApiToken(payload.apiToken, deps.fikenFetch ?? fetch);
344
+ const previousDefault = existing
345
+ ? (fikenConnectionMetadata(existing.metadata)?.defaultCompanySlug ?? null)
346
+ : null;
347
+ const defaultCompanySlug = resolveFikenDefaultCompanySlug({
348
+ requested: payload.defaultCompanySlug ?? null,
349
+ previous: previousDefault,
350
+ companies: verified.companies,
351
+ });
352
+ if (payload.defaultCompanySlug && defaultCompanySlug !== payload.defaultCompanySlug) {
353
+ throw new HTTPException(422, {
354
+ message: `defaultCompanySlug is not among the companies this token can access: ${verified.companies
355
+ .map((company) => company.slug)
356
+ .join(", ")}`,
357
+ });
358
+ }
359
+ const metadata = {
360
+ credentialRole: FIKEN_CREDENTIAL_ROLE,
361
+ credentialLabel: FIKEN_CREDENTIAL_LABEL,
362
+ companies: verified.companies,
363
+ defaultCompanySlug,
364
+ verifiedAt: new Date().toISOString(),
365
+ };
366
+ const credentialEncrypted = encryptCredentialBundle(
367
+ key,
368
+ fikenCredentialBundle(payload.apiToken),
369
+ );
370
+ if (existing) {
371
+ // Rewrites the whole credential identity: a token pasted over an OAuth
372
+ // row must also flip kind and clear the OAuth expiry, or the broker
373
+ // keeps treating the api_key bundle as a refreshable oauth2 credential.
374
+ const updated = await updateConnection(db, {
375
+ workspaceId,
376
+ connectionId: existing.id,
377
+ visibleToSubjectId: null,
378
+ expectedVersion: existing.version,
379
+ kind: "api_key",
380
+ status: "active",
381
+ credentialEncrypted,
382
+ grantedScopes: [],
383
+ expiresAt: null,
384
+ metadata,
385
+ updatedBySubjectId: grant.subjectId,
386
+ });
387
+ if (!updated) {
388
+ throw new HTTPException(409, { message: "the Fiken connection changed; retry" });
389
+ }
390
+ return c.json(ConnectionResponse.parse({ connection: updated }));
391
+ }
392
+ const connection = await createConnection(db, {
393
+ accountId: grant.accountId,
394
+ workspaceId,
395
+ subjectId: null,
396
+ providerDomain: FIKEN_PROVIDER_DOMAIN,
397
+ kind: "api_key",
398
+ credentialEncrypted,
399
+ grantedScopes: [],
400
+ expiresAt: null,
401
+ metadata,
402
+ createdBySubjectId: grant.subjectId,
403
+ });
404
+ return c.json(ConnectionResponse.parse({ connection }), 201);
405
+ });
406
+
407
+ // Fiken OAuth (registered app) start. Both Fiken lanes produce the same
408
+ // workspace-owned connection shape; this one refreshes through the broker.
409
+ app.post("/v1/workspaces/:workspaceId/connections/fiken/oauth/start", async (c) => {
410
+ assertIntegrationsEnabled();
411
+ const workspaceId = c.req.param("workspaceId");
412
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
413
+ const payload = FikenOAuthStartRequest.parse(await c.req.json());
414
+ requireEnvironmentEncryption(settings);
415
+ return c.json(
416
+ FikenOAuthStartResponse.parse(
417
+ await startFikenOAuth(deps, {
418
+ accountId: grant.accountId,
419
+ workspaceId,
420
+ subjectId: grant.subjectId,
421
+ requestUrl: c.req.url,
422
+ payload,
423
+ }),
424
+ ),
425
+ );
426
+ });
427
+
428
+ app.get("/v1/integrations/fiken/callback", async (c) => {
429
+ assertIntegrationsEnabled();
430
+ const result = await completeFikenOAuthCallback(deps, {
431
+ ...(c.req.query("code") ? { code: c.req.query("code") } : {}),
432
+ ...(c.req.query("state") ? { state: c.req.query("state") } : {}),
433
+ ...(c.req.query("error") ? { error: c.req.query("error") } : {}),
434
+ requestUrl: c.req.url,
435
+ });
436
+ return c.redirect(result.redirectTo, 302);
437
+ });
438
+
269
439
  app.post("/v1/workspaces/:workspaceId/connections/google-drive/install", async (c) => {
270
440
  assertIntegrationsEnabled();
271
441
  const workspaceId = c.req.param("workspaceId");
@@ -287,9 +457,30 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
287
457
  );
288
458
  });
289
459
 
290
- app.get("/v1/integrations/google-drive/callback", async (c) => {
460
+ app.post("/v1/workspaces/:workspaceId/connections/atlassian/install", async (c) => {
461
+ assertIntegrationsEnabled();
462
+ const workspaceId = c.req.param("workspaceId");
463
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
464
+ const parsed = AtlassianOAuthStartRequest.safeParse(await c.req.json());
465
+ if (!parsed.success) {
466
+ throw new HTTPException(400, { message: "invalid Atlassian install request" });
467
+ }
468
+ return c.json(
469
+ AtlassianOAuthStartResponse.parse(
470
+ await startAtlassianOAuth(deps, {
471
+ accountId: grant.accountId,
472
+ workspaceId,
473
+ subjectId: grant.subjectId,
474
+ requestUrl: c.req.url,
475
+ payload: parsed.data,
476
+ }),
477
+ ),
478
+ );
479
+ });
480
+
481
+ app.get("/v1/integrations/atlassian/callback", async (c) => {
291
482
  assertIntegrationsEnabled();
292
- const result = await completeGoogleDriveOAuthCallback(deps, {
483
+ const result = await completeAtlassianOAuthCallback(deps, {
293
484
  ...(c.req.query("code") ? { code: c.req.query("code") } : {}),
294
485
  ...(c.req.query("state") ? { state: c.req.query("state") } : {}),
295
486
  ...(c.req.query("error") ? { error: c.req.query("error") } : {}),
@@ -298,6 +489,80 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
298
489
  return c.redirect(result.redirectTo, 302);
299
490
  });
300
491
 
492
+ app.patch(
493
+ "/v1/workspaces/:workspaceId/connections/atlassian/:connectionId/lifecycle",
494
+ async (c) => {
495
+ assertIntegrationsEnabled();
496
+ const workspaceId = c.req.param("workspaceId");
497
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
498
+ const parsed = AtlassianLifecycleActionRequest.safeParse(await c.req.json());
499
+ if (!parsed.success) {
500
+ throw new HTTPException(400, { message: "invalid Atlassian lifecycle request" });
501
+ }
502
+ return c.json(
503
+ ConnectionResponse.parse({
504
+ connection: await transitionAtlassianLifecycle(deps, {
505
+ workspaceId,
506
+ subjectId: grant.subjectId,
507
+ connectionId: c.req.param("connectionId"),
508
+ payload: parsed.data,
509
+ }),
510
+ }),
511
+ );
512
+ },
513
+ );
514
+
515
+ app.get("/v1/workspaces/:workspaceId/connections/atlassian/:connectionId/browse", async (c) => {
516
+ assertIntegrationsEnabled();
517
+ const workspaceId = c.req.param("workspaceId");
518
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:read");
519
+ return c.json(
520
+ await browseAtlassianSources(deps, {
521
+ workspaceId,
522
+ subjectId: grant.subjectId,
523
+ connectionId: c.req.param("connectionId"),
524
+ }),
525
+ );
526
+ });
527
+
528
+ app.post("/v1/workspaces/:workspaceId/connections/atlassian/:connectionId/source", async (c) => {
529
+ assertIntegrationsEnabled();
530
+ const workspaceId = c.req.param("workspaceId");
531
+ const authorization = await requireAccessGrantAuthorization(
532
+ c,
533
+ deps,
534
+ workspaceId,
535
+ "connections:write",
536
+ );
537
+ const { grant } = authorization;
538
+ const connection = await saveAtlassianSources(deps, {
539
+ accountId: grant.accountId,
540
+ workspaceId,
541
+ subjectId: grant.subjectId,
542
+ grant,
543
+ connectionId: c.req.param("connectionId"),
544
+ payload: await c.req.json(),
545
+ canManageOrganizationDestination:
546
+ authorization.accountGrant?.permissions.includes("account:admin") === true,
547
+ canManageWorkspaceDestination: hasPermission(grant.permissions, "workspace:admin"),
548
+ canManagePersonalDestination:
549
+ authorization.contextIntegrity && authorization.authenticatedSubjectId === grant.subjectId,
550
+ });
551
+ return c.json(ConnectionResponse.parse({ connection }));
552
+ });
553
+
554
+ app.get("/v1/integrations/google-drive/callback", async (c) => {
555
+ assertIntegrationsEnabled();
556
+ const input = {
557
+ ...(c.req.query("code") ? { code: c.req.query("code") } : {}),
558
+ ...(c.req.query("state") ? { state: c.req.query("state") } : {}),
559
+ ...(c.req.query("error") ? { error: c.req.query("error") } : {}),
560
+ requestUrl: c.req.url,
561
+ };
562
+ const result = await completeGoogleDriveOAuthCallback(deps, input);
563
+ return c.redirect(result.redirectTo, 302);
564
+ });
565
+
301
566
  app.patch(
302
567
  "/v1/workspaces/:workspaceId/connections/google-drive/:connectionId/lifecycle",
303
568
  async (c) => {
@@ -355,6 +620,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
355
620
  accountId: grant.accountId,
356
621
  workspaceId,
357
622
  subjectId: grant.subjectId,
623
+ grant,
358
624
  connectionId: c.req.param("connectionId"),
359
625
  payload: await c.req.json(),
360
626
  canManageOrganizationDestination:
@@ -394,6 +660,8 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
394
660
  const { grant } = authorization;
395
661
  const payload = UpdateConnectionRequest.parse(await c.req.json());
396
662
  assertNotReservedSlackBotMetadata(payload.metadata);
663
+ assertNotReservedFikenMetadata(payload.metadata);
664
+ assertNotReservedApiIntegrationOAuthMetadata(payload.metadata);
397
665
  const existing = await getConnectionMetadata(
398
666
  db,
399
667
  workspaceId,
@@ -470,6 +738,16 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
470
738
  message: "use the dedicated Google Drive reconnect or source-selection flow",
471
739
  });
472
740
  }
741
+ if (existing?.metadata.credentialRole === API_INTEGRATION_OAUTH_CREDENTIAL_ROLE) {
742
+ throw new HTTPException(422, {
743
+ message: "use the dedicated Integration provider OAuth flow to update this connection",
744
+ });
745
+ }
746
+ if (existing && AtlassianConnectionMetadata.safeParse(existing.metadata).success) {
747
+ throw new HTTPException(422, {
748
+ message: "use the dedicated Atlassian reconnect or source-selection flow",
749
+ });
750
+ }
473
751
  if (existing) {
474
752
  const providerDomain = canonicalProviderDomain(
475
753
  payload.providerDomain ?? existing.providerDomain,
@@ -477,6 +755,7 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
477
755
  const kind = payload.kind ?? existing.kind;
478
756
  assertNotDirectPersonalSlackOAuth(providerDomain, kind);
479
757
  assertNotDirectGoogleDriveOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
758
+ assertNotDirectAtlassianOAuth(providerDomain, kind, payload.metadata ?? existing.metadata);
480
759
  }
481
760
  // Status is not a free-form field: revocation goes through DELETE, and the
482
761
  // broker owns needs_reauth/error. Reactivating a connection is only
@@ -538,8 +817,14 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
538
817
  existing.providerDomain === GOOGLE_DRIVE_PROVIDER_DOMAIN &&
539
818
  existing.kind === "oauth2" &&
540
819
  GoogleDriveConnectionMetadata.safeParse(existing.metadata).success;
820
+ const isAtlassian =
821
+ existing.subjectId === grant.subjectId &&
822
+ existing.providerDomain === ATLASSIAN_PROVIDER_DOMAIN &&
823
+ existing.kind === "oauth2" &&
824
+ AtlassianConnectionMetadata.safeParse(existing.metadata).success;
825
+ const disconnectPayload = await c.req.json().catch(() => null);
541
826
  const googleDriveDisconnect = isGoogleDrive
542
- ? GoogleDriveDisconnectRequest.safeParse(await c.req.json().catch(() => null))
827
+ ? GoogleDriveDisconnectRequest.safeParse(disconnectPayload)
543
828
  : null;
544
829
  if (googleDriveDisconnect && !googleDriveDisconnect.success) {
545
830
  throw new HTTPException(400, {
@@ -548,7 +833,16 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
548
833
  "invalid Google Drive disconnect request",
549
834
  });
550
835
  }
551
- if (existing.status === "revoked" && !isGoogleDrive) {
836
+ const atlassianDisconnect = isAtlassian
837
+ ? AtlassianDisconnectRequest.safeParse(disconnectPayload)
838
+ : null;
839
+ if (atlassianDisconnect && !atlassianDisconnect.success) {
840
+ throw new HTTPException(400, {
841
+ message:
842
+ atlassianDisconnect.error.issues[0]?.message ?? "invalid Atlassian disconnect request",
843
+ });
844
+ }
845
+ if (existing.status === "revoked" && !isGoogleDrive && !isAtlassian) {
552
846
  return c.json(ConnectionResponse.parse({ connection: existing }));
553
847
  }
554
848
  const connection = isGoogleDrive
@@ -558,18 +852,31 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
558
852
  connection: existing,
559
853
  payload: googleDriveDisconnect!.data,
560
854
  })
561
- : isOpenGeniSlackBotConnection(existing)
562
- ? await revokeConnectionWithSlackBotSuccessAudit(db, {
563
- accountId: grant.accountId,
855
+ : isAtlassian
856
+ ? await disconnectAtlassian(deps, {
564
857
  workspaceId,
565
858
  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,
859
+ connection: existing,
860
+ payload: atlassianDisconnect!.data,
571
861
  })
572
- : await revokeConnection(db, workspaceId, connectionId, grant.subjectId, existing.version);
862
+ : isOpenGeniSlackBotConnection(existing)
863
+ ? await revokeConnectionWithSlackBotSuccessAudit(db, {
864
+ accountId: grant.accountId,
865
+ workspaceId,
866
+ subjectId: grant.subjectId,
867
+ connectionId,
868
+ expectedVersion: existing.version,
869
+ credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
870
+ credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
871
+ slackTeamId: openGeniSlackBotMetadata(existing.metadata)!.slackTeamId,
872
+ })
873
+ : await revokeConnection(
874
+ db,
875
+ workspaceId,
876
+ connectionId,
877
+ grant.subjectId,
878
+ existing.version,
879
+ );
573
880
  if (!connection) {
574
881
  throw new HTTPException(409, { message: "connection changed during disconnect; try again" });
575
882
  }
@@ -600,6 +907,29 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
600
907
  return c.json(OAuthStartResponse.parse(result));
601
908
  });
602
909
 
910
+ app.post("/v1/workspaces/:workspaceId/integrations/oauth/start", async (c) => {
911
+ assertIntegrationsEnabled();
912
+ const workspaceId = c.req.param("workspaceId");
913
+ const grant = await requireAccessGrant(c, deps, workspaceId, "connections:write");
914
+ const parsed = ApiIntegrationOAuthStartRequest.safeParse(await c.req.json());
915
+ if (!parsed.success) {
916
+ throw new HTTPException(400, {
917
+ message: parsed.error.issues[0]?.message ?? "invalid Integration OAuth start request",
918
+ });
919
+ }
920
+ return c.json(
921
+ OAuthStartResponse.parse(
922
+ await startApiIntegrationProviderOAuth(deps, {
923
+ accountId: grant.accountId,
924
+ workspaceId,
925
+ subjectId: grant.subjectId,
926
+ requestUrl: c.req.url,
927
+ payload: parsed.data,
928
+ }),
929
+ ),
930
+ );
931
+ });
932
+
603
933
  app.get("/v1/integrations/oauth/callback", async (c) => {
604
934
  assertIntegrationsEnabled();
605
935
  const result = await completeMcpOAuthCallback(
@@ -613,6 +943,17 @@ export function registerConnectionRoutes(app: Hono, deps: ApiRouteDeps): void {
613
943
  return c.redirect(result.redirectTo, 302);
614
944
  });
615
945
 
946
+ app.get("/v1/integrations/provider-oauth/callback", async (c) => {
947
+ assertIntegrationsEnabled();
948
+ const result = await completeApiIntegrationProviderOAuth(deps, {
949
+ ...(c.req.query("code") ? { code: c.req.query("code") } : {}),
950
+ ...(c.req.query("state") ? { state: c.req.query("state") } : {}),
951
+ ...(c.req.query("error") ? { error: c.req.query("error") } : {}),
952
+ requestUrl: c.req.url,
953
+ });
954
+ return c.redirect(result.redirectTo, 302);
955
+ });
956
+
616
957
  app.get("/v1/integrations/oauth/client-metadata.json", (c) => {
617
958
  const baseUrl = integrationBaseUrl(settings.publicBaseUrl, c.req.url);
618
959
  const metadataUrl = `${baseUrl}/v1/integrations/oauth/client-metadata.json`;
@@ -670,14 +1011,9 @@ async function persistOpenGeniSlackBotConnection(input: {
670
1011
  "principal_validation",
671
1012
  );
672
1013
  }
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;
1014
+ const existingMetadata = requestedExisting
1015
+ ? openGeniSlackBotMetadata(requestedExisting.metadata)
1016
+ : null;
681
1017
  if (existingMetadata && existingMetadata.slackTeamId !== input.verified.metadata.slackTeamId) {
682
1018
  throw new SlackInstallCallbackError(
683
1019
  409,
@@ -699,83 +1035,36 @@ async function persistOpenGeniSlackBotConnection(input: {
699
1035
  );
700
1036
  }
701
1037
  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
1038
+ try {
1039
+ return await persistSlackBotInstallationWithSuccessAudit(db, {
1040
+ accountId: input.state.accountId,
1041
+ workspaceId: input.state.workspaceId,
1042
+ subjectId: input.state.subjectId,
1043
+ credentialRole: OPENGENI_SLACK_BOT_CREDENTIAL_ROLE,
1044
+ credentialLabel: OPENGENI_SLACK_BOT_CREDENTIAL_LABEL,
1045
+ slackTeamId: input.verified.metadata.slackTeamId,
1046
+ ...(input.state.connectionId
1047
+ ? {
1048
+ requestedConnectionId: input.state.connectionId,
1049
+ requestedConnectionVersion: input.state.connectionVersion,
1050
+ }
1051
+ : {}),
1052
+ credentialEncrypted,
1053
+ grantedScopes: input.verified.grantedScopes,
1054
+ verifiedInstallAt,
1055
+ metadata: input.verified.metadata,
1056
+ });
1057
+ } catch (error) {
1058
+ if (error instanceof SlackInstallationBindingConflictError) {
1059
+ throw new SlackInstallCallbackError(
1060
+ 409,
1061
+ "connection_conflict",
1062
+ error.message,
1063
+ "principal_validation",
776
1064
  );
777
- }) ?? null
778
- );
1065
+ }
1066
+ throw error;
1067
+ }
779
1068
  }
780
1069
 
781
1070
  function requireOpenGeniSlackOAuthSettings(settings: ApiRouteDeps["settings"]): {
@@ -952,6 +1241,21 @@ function assertNotDirectGoogleDriveOAuth(
952
1241
  }
953
1242
  }
954
1243
 
1244
+ function assertNotDirectAtlassianOAuth(
1245
+ providerDomain: string,
1246
+ kind: string,
1247
+ metadata: Record<string, unknown> | undefined,
1248
+ ): void {
1249
+ if (
1250
+ (providerDomain === ATLASSIAN_PROVIDER_DOMAIN && kind === "oauth2") ||
1251
+ AtlassianConnectionMetadata.safeParse(metadata).success
1252
+ ) {
1253
+ throw new HTTPException(422, {
1254
+ message: "Atlassian credentials must use the dedicated OAuth connection flow",
1255
+ });
1256
+ }
1257
+ }
1258
+
955
1259
  function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | undefined): void {
956
1260
  if (hasReservedOpenGeniSlackBotMetadata(metadata)) {
957
1261
  throw new HTTPException(422, {
@@ -960,6 +1264,24 @@ function assertNotReservedSlackBotMetadata(metadata: Record<string, unknown> | u
960
1264
  }
961
1265
  }
962
1266
 
1267
+ function assertNotReservedFikenMetadata(metadata: Record<string, unknown> | undefined): void {
1268
+ if (hasReservedFikenMetadata(metadata)) {
1269
+ throw new HTTPException(422, {
1270
+ message: "Fiken connection metadata is reserved for the verified Fiken connect flows",
1271
+ });
1272
+ }
1273
+ }
1274
+
1275
+ function assertNotReservedApiIntegrationOAuthMetadata(
1276
+ metadata: Record<string, unknown> | undefined,
1277
+ ): void {
1278
+ if (metadata?.credentialRole === API_INTEGRATION_OAUTH_CREDENTIAL_ROLE) {
1279
+ throw new HTTPException(422, {
1280
+ message: "API Integration OAuth metadata is reserved for the dedicated provider flow",
1281
+ });
1282
+ }
1283
+ }
1284
+
963
1285
  function writableSubjectId(
964
1286
  requested: string | null | undefined,
965
1287
  grantSubjectId: string,