@opengeni/api-router 3.0.1 → 4.0.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 (96) hide show
  1. package/dist/admission-trace.d.ts +8 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth-attempt-context.d.ts +2 -0
  4. package/dist/auth/managed-auth-session-adapter.d.ts +2 -0
  5. package/dist/auth/managed-auth.d.ts +1 -0
  6. package/dist/auth/managed-sign-in-notifications.d.ts +4 -0
  7. package/dist/{chunk-AJ62T5QN.js → chunk-E7TZFJ65.js} +22300 -21113
  8. package/dist/{chunk-AJ62T5QN.js.map → chunk-E7TZFJ65.js.map} +1 -1
  9. package/dist/connection-ownership.d.ts +1 -2
  10. package/dist/fatal-process-boundary.d.ts +1 -1
  11. package/dist/index.js +24 -6
  12. package/dist/index.js.map +1 -1
  13. package/dist/integrations/atlassian.d.ts +8 -3
  14. package/dist/integrations/fiken.d.ts +3 -1
  15. package/dist/integrations/github-lens-connect.d.ts +5 -65
  16. package/dist/integrations/google-drive.d.ts +2 -0
  17. package/dist/integrations/integration-commit-authority.d.ts +13 -0
  18. package/dist/integrations/oauth-client.d.ts +119 -11
  19. package/dist/integrations/oauth-profiles.d.ts +8 -13
  20. package/dist/integrations/slack-interactions.d.ts +1 -1
  21. package/dist/mcp/capability-account-readiness.d.ts +7 -0
  22. package/dist/mcp/first-party-tool-permissions.d.ts +4 -0
  23. package/dist/mcp-oauth.d.ts +22 -1
  24. package/dist/routes/api-integrations.d.ts +1 -0
  25. package/dist/routes/codex.d.ts +28 -0
  26. package/dist/routes/managed-sign-in-methods.d.ts +6 -0
  27. package/dist/routes/organization-integration-policy.d.ts +3 -0
  28. package/dist/routes/sessions.d.ts +24 -2
  29. package/dist/scheduled-task-deletion.d.ts +3 -4
  30. package/dist/workspace-tool-gateway.d.ts +3 -1
  31. package/package.json +15 -15
  32. package/src/admission-trace.ts +14 -0
  33. package/src/app.ts +141 -94
  34. package/src/auth/managed-auth-attempt-context.ts +9 -0
  35. package/src/auth/managed-auth-session-adapter.ts +11 -0
  36. package/src/auth/managed-auth.ts +39 -2
  37. package/src/auth/managed-sign-in-notifications.ts +81 -0
  38. package/src/connection-ownership.ts +2 -9
  39. package/src/editable-artifact-production.ts +6 -0
  40. package/src/fatal-process-boundary.ts +17 -2
  41. package/src/index.ts +3 -4
  42. package/src/integrations/atlassian.ts +184 -76
  43. package/src/integrations/fiken.ts +57 -35
  44. package/src/integrations/github-app-connect.ts +51 -15
  45. package/src/integrations/github-lens-connect.ts +7 -28
  46. package/src/integrations/google-drive.ts +296 -125
  47. package/src/integrations/integration-commit-authority.ts +69 -0
  48. package/src/integrations/oauth-client.ts +231 -132
  49. package/src/integrations/oauth-profiles.ts +10 -45
  50. package/src/integrations/personal-github.ts +55 -31
  51. package/src/integrations/provider-oauth.ts +57 -82
  52. package/src/integrations/social-oauth.ts +83 -56
  53. package/src/mcp/capability-account-readiness.ts +17 -0
  54. package/src/mcp/editable-artifacts.ts +7 -2
  55. package/src/mcp/first-party-tool-permissions.ts +1 -0
  56. package/src/mcp/knowledge-entries.ts +25 -8
  57. package/src/mcp/server.ts +135 -43
  58. package/src/mcp-oauth.ts +336 -34
  59. package/src/routes/api-integrations.ts +116 -4
  60. package/src/routes/automations.ts +5 -13
  61. package/src/routes/billing.ts +52 -0
  62. package/src/routes/canonical-human-identities.ts +61 -0
  63. package/src/routes/capabilities.ts +51 -1
  64. package/src/routes/codex.ts +4 -2
  65. package/src/routes/connect.ts +90 -31
  66. package/src/routes/connections.ts +487 -307
  67. package/src/routes/files.ts +21 -12
  68. package/src/routes/github.ts +48 -19
  69. package/src/routes/insights.ts +17 -7
  70. package/src/routes/integration-facets.ts +66 -25
  71. package/src/routes/knowledge.ts +48 -1
  72. package/src/routes/managed-sign-in-methods.ts +433 -0
  73. package/src/routes/organization-integration-policy.ts +113 -0
  74. package/src/routes/organization-memberships.ts +22 -2
  75. package/src/routes/plugins.ts +42 -4
  76. package/src/routes/pr-review-github.ts +67 -57
  77. package/src/routes/pr-review.ts +74 -71
  78. package/src/routes/rigs.ts +32 -13
  79. package/src/routes/scheduled-tasks.ts +6 -6
  80. package/src/routes/sessions.ts +233 -11
  81. package/src/routes/skill-content.ts +19 -3
  82. package/src/routes/social.ts +24 -13
  83. package/src/sandbox/channel-a.ts +7 -1
  84. package/src/sandbox/rematerialize.ts +2 -1
  85. package/src/scheduled-task-deletion.ts +13 -3
  86. package/src/workspace-tool-gateway.ts +31 -31
  87. package/dist/connection-authority-owner.d.ts +0 -59
  88. package/dist/routes/connection-authorities.d.ts +0 -3
  89. package/dist/routes/host-mcp-bindings.d.ts +0 -5
  90. package/dist/routes/host-mcp-resolvers.d.ts +0 -3
  91. package/dist/routes/packs.d.ts +0 -3
  92. package/src/connection-authority-owner.ts +0 -61
  93. package/src/routes/connection-authorities.ts +0 -139
  94. package/src/routes/host-mcp-bindings.ts +0 -122
  95. package/src/routes/host-mcp-resolvers.ts +0 -41
  96. package/src/routes/packs.ts +0 -784
@@ -1,7 +1,12 @@
1
1
  import pluginSnapshot from "../../../../data/catalog/plugins-snapshot.json";
2
2
  import { marketplacePlugin } from "../integrations/marketplace-plugin";
3
3
  import { mcpEndpointIdentity } from "@opengeni/contracts";
4
- import { buildCapabilityCatalog, createCatalogItem } from "@opengeni/core";
4
+ import {
5
+ buildCapabilityCatalog,
6
+ createCatalogItem,
7
+ integrationSourceForOrganizationPolicy,
8
+ } from "@opengeni/core";
9
+ import { withOrganizationIntegrationPolicyFence } from "@opengeni/db/organization-integration-policy";
5
10
  import { createHash } from "node:crypto";
6
11
 
7
12
  import {
@@ -43,6 +48,7 @@ import {
43
48
  getConnectionMetadata,
44
49
  getInstalledPluginPackage,
45
50
  getPluginPackageUninstallPreview,
51
+ PluginUninstallPreviewChangedError,
46
52
  integrationBindingKey,
47
53
  installApiIntegration,
48
54
  installPluginMcpReference,
@@ -344,9 +350,14 @@ export function registerPluginRoutes(
344
350
 
345
351
  app.get("/v1/workspaces/:workspaceId/plugins/:pluginKey/uninstall-preview", async (c) => {
346
352
  const workspaceId = c.req.param("workspaceId");
347
- await requireAccessGrant(c, deps, workspaceId, "workspace:read");
353
+ const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
348
354
  const pluginKey = decodeURIComponent(c.req.param("pluginKey"));
349
- const preview = await getPluginPackageUninstallPreview(deps.db, workspaceId, pluginKey);
355
+ const preview = await getPluginPackageUninstallPreview(
356
+ deps.db,
357
+ workspaceId,
358
+ pluginKey,
359
+ grant.subjectId,
360
+ );
350
361
  return c.json(PluginUninstallPreview.parse({ pluginKey, ...preview }));
351
362
  });
352
363
 
@@ -373,11 +384,33 @@ export function registerPluginRoutes(
373
384
  subjectId: grant.subjectId,
374
385
  pluginKey,
375
386
  expectedInstallationVersion: payload.expectedInstallationVersion,
387
+ ...(payload.expectedPreviewToken
388
+ ? { expectedPreviewToken: payload.expectedPreviewToken }
389
+ : {}),
376
390
  idempotencyKey: payload.idempotencyKey,
377
391
  })),
378
392
  }),
379
393
  );
380
394
  } catch (error) {
395
+ if (error instanceof PluginUninstallPreviewChangedError) {
396
+ const preview = error.preview ?? {
397
+ pluginKey,
398
+ ...(await getPluginPackageUninstallPreview(
399
+ deps.db,
400
+ workspaceId,
401
+ pluginKey,
402
+ grant.subjectId,
403
+ )),
404
+ };
405
+ return c.json(
406
+ {
407
+ code: error.code,
408
+ message: error.message,
409
+ preview: PluginUninstallPreview.parse(preview),
410
+ },
411
+ 409,
412
+ );
413
+ }
381
414
  throw pluginMutationHttpError(error);
382
415
  }
383
416
  });
@@ -499,6 +532,11 @@ async function resolvePluginPackage(input: {
499
532
  continue;
500
533
  }
501
534
  if (component.kind === "integration") {
535
+ const source = await withOrganizationIntegrationPolicyFence(
536
+ input.deps.db,
537
+ input,
538
+ async (_tx, policy) => integrationSourceForOrganizationPolicy(policy, component.source),
539
+ );
502
540
  const requestedBinding = input.bindings[component.key];
503
541
  const connection = await optionalConnection(
504
542
  input.deps,
@@ -507,7 +545,7 @@ async function resolvePluginPackage(input: {
507
545
  requestedBinding?.connectionId,
508
546
  );
509
547
  const resolved = await resolveApiIntegrationPreview({
510
- source: component.source,
548
+ source,
511
549
  connection: connectionDescriptor(connection),
512
550
  transport: input.transport,
513
551
  authority: {
@@ -1,7 +1,7 @@
1
1
  import { environmentsEncryptionKeyBytes } from "@opengeni/config";
2
+ import { withOrganizationIntegrationAcquisition } from "@opengeni/db/organization-integration-policy";
2
3
  import {
3
4
  AUTOMATION_WEBHOOK_MAX_BYTES,
4
- OPENGENI_PR_REVIEW_PACK_ID,
5
5
  PrReviewManagedGitHubSetup,
6
6
  type AccessGrant,
7
7
  type GitHubInstallationBindingCandidate,
@@ -9,22 +9,19 @@ import {
9
9
  } from "@opengeni/contracts";
10
10
  import {
11
11
  automationRequestDigest,
12
- getCapabilityPack,
13
12
  hasPermission,
14
- PR_REVIEW_AUTOMATION_TEMPLATE_ID,
15
- prReviewPackConnectorId,
16
13
  requireAutomationAdapter,
17
14
  requireAccessGrantAuthorization,
18
15
  externalActorContinuationForAuthorization,
19
16
  requirePermission,
20
17
  verifyPrReviewWebhook,
21
18
  type ApiRouteDeps,
19
+ PR_REVIEW_AUTOMATION_SETUP,
22
20
  } from "@opengeni/core";
23
21
  import {
24
22
  AutomationDeliveryConflictError,
25
23
  encryptVariableSetValue,
26
24
  getAutomationSourceSecret,
27
- getPackInstallation,
28
25
  listPrReviewAppRegistrations,
29
26
  listPrReviewRepositoryBindings,
30
27
  nestedPostgresSqlState,
@@ -49,6 +46,10 @@ import {
49
46
  } from "@opengeni/github";
50
47
  import type { Context, Hono } from "hono";
51
48
  import { requireLegacyOAuthActor } from "../connection-ownership";
49
+ import {
50
+ integrationCommitGrant,
51
+ type IntegrationCommitGrant,
52
+ } from "../integrations/integration-commit-authority";
52
53
  import { deleteCookie, setCookie } from "hono/cookie";
53
54
  import { HTTPException } from "hono/http-exception";
54
55
  import { githubBrowserBaseUrl } from "../github-browser-flow";
@@ -131,7 +132,7 @@ export function registerPrReviewGitHubRoutes(app: Hono, deps: ApiRouteDeps): voi
131
132
  const workspaceId = c.req.param("workspaceId");
132
133
  const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "workspace:read");
133
134
  const grant = access.grant;
134
- await requireActivePack(deps, workspaceId);
135
+
135
136
  const missing = prReviewGitHubAppMissingSettings(deps.settings);
136
137
  const configured = missing.length === 0;
137
138
  const registrations = (
@@ -203,7 +204,7 @@ export function registerPrReviewGitHubRoutes(app: Hono, deps: ApiRouteDeps): voi
203
204
  const state = requireStateQuery(c, "missing OpenGeni Lens installation state");
204
205
  const payload = requireFreshState(state, deps, "pr_review_github_authority", workspaceId);
205
206
  await requirePrReviewManageGrant(c, deps, workspaceId, payload);
206
- await requireActivePack(deps, workspaceId);
207
+
207
208
  assertManagedCompute(deps);
208
209
  requireConfiguredApp(deps);
209
210
  const discoveryState = createSignedState(deps.githubStateSecret, {
@@ -233,7 +234,7 @@ export function registerPrReviewGitHubRoutes(app: Hono, deps: ApiRouteDeps): voi
233
234
  throw new HTTPException(400, { message: "invalid OpenGeni Lens installation" });
234
235
  }
235
236
  await requirePrReviewManageGrant(c, deps, workspaceId, payload);
236
- await requireActivePack(deps, workspaceId);
237
+
237
238
  assertManagedCompute(deps);
238
239
  const registrations = await listPrReviewAppRegistrations(
239
240
  deps.db,
@@ -279,7 +280,7 @@ export function registerPrReviewGitHubRoutes(app: Hono, deps: ApiRouteDeps): voi
279
280
  const payload = requireFreshState(state, deps, "pr_review_github_install");
280
281
  requireStateCookie(c, state);
281
282
  await requirePrReviewManageGrant(c, deps, payload.workspaceId!, payload);
282
- await requireActivePack(deps, payload.workspaceId!);
283
+
283
284
  assertManagedCompute(deps);
284
285
  const setupAction = c.req.query("setup_action");
285
286
  if (setupAction === "request") return c.html(setupPendingHtml());
@@ -336,10 +337,11 @@ export function registerPrReviewGitHubRoutes(app: Hono, deps: ApiRouteDeps): voi
336
337
  const payload = requireFreshState(state, deps);
337
338
  requireStateCookie(c, state);
338
339
  const grant = await requirePrReviewManageGrant(c, deps, payload.workspaceId!, payload);
339
- const packInstallation = await requireActivePack(deps, grant.workspaceId);
340
+
340
341
  assertManagedCompute(deps);
341
342
  requireConfiguredApp(deps);
342
343
 
344
+ await withOrganizationIntegrationAcquisition(deps.db, grant, ["github-lens"], async () => {});
343
345
  if (payload.intent === "pr_review_github_discovery") {
344
346
  let candidates: GitHubInstallationBindingCandidate[] | null;
345
347
  try {
@@ -410,12 +412,8 @@ export function registerPrReviewGitHubRoutes(app: Hono, deps: ApiRouteDeps): voi
410
412
  if (repositoryIds.size !== proof.repositories.length) {
411
413
  throw new HTTPException(409, { message: "GitHub returned duplicate repository identities" });
412
414
  }
413
- const template = getCapabilityPack(OPENGENI_PR_REVIEW_PACK_ID)?.automationTemplates?.find(
414
- (candidate) => candidate.id === PR_REVIEW_AUTOMATION_TEMPLATE_ID,
415
- );
416
- if (!template) {
417
- throw new HTTPException(503, { message: "PR Review automation template is unavailable" });
418
- }
415
+ const template = PR_REVIEW_AUTOMATION_SETUP;
416
+
419
417
  const encryptionKey = environmentsEncryptionKeyBytes(deps.settings);
420
418
  if (!encryptionKey) {
421
419
  throw new HTTPException(503, {
@@ -424,32 +422,38 @@ export function registerPrReviewGitHubRoutes(app: Hono, deps: ApiRouteDeps): voi
424
422
  }
425
423
  let synchronized;
426
424
  try {
427
- synchronized = await syncManagedGitHubPrReviewInstallation(deps.db, {
428
- accountId: grant.accountId,
429
- workspaceId: grant.workspaceId,
430
- installationId,
431
- providerAccountLogin: proof.installation.accountLogin,
432
- providerAccountType: proof.installation.accountType as "User" | "Organization",
433
- githubActorId: proof.actorId,
434
- authorityKind: proof.authorityKind,
435
- authorityCheckedAt: new Date(),
436
- authorityExpiresAt: new Date((payload.iat + bindingStateMaxAgeSeconds) * 1_000),
437
- authorityNonce: payload.nonce,
438
- appId: deps.settings.prReviewGithubAppId!,
439
- webhookSecretEncrypted: encryptVariableSetValue(
440
- encryptionKey,
441
- deps.settings.prReviewGithubWebhookSecret!,
442
- ),
443
- repositories: proof.repositories,
444
- createdBySubjectId: grant.subjectId,
445
- packInstallationId: packInstallation.id,
446
- packConnectorId: prReviewPackConnectorId("github"),
447
- packTemplateId: template.id,
448
- adapterId: template.adapterId,
449
- eventTypes: template.eventTypes,
450
- configuration: template.configuration,
451
- sessionTemplate: template.sessionTemplate,
452
- });
425
+ synchronized = await withOrganizationIntegrationAcquisition(
426
+ deps.db,
427
+ grant,
428
+ ["github-lens"],
429
+ async (tx) => {
430
+ await grant.authorizeCommit(tx);
431
+ return syncManagedGitHubPrReviewInstallation(tx, {
432
+ accountId: grant.accountId,
433
+ workspaceId: grant.workspaceId,
434
+ installationId,
435
+ providerAccountLogin: proof.installation.accountLogin,
436
+ providerAccountType: proof.installation.accountType as "User" | "Organization",
437
+ githubActorId: proof.actorId,
438
+ authorityKind: proof.authorityKind,
439
+ authorityCheckedAt: new Date(),
440
+ authorityExpiresAt: new Date((payload.iat + bindingStateMaxAgeSeconds) * 1_000),
441
+ authorityNonce: payload.nonce,
442
+ appId: deps.settings.prReviewGithubAppId!,
443
+ webhookSecretEncrypted: encryptVariableSetValue(
444
+ encryptionKey,
445
+ deps.settings.prReviewGithubWebhookSecret!,
446
+ ),
447
+ repositories: proof.repositories,
448
+ createdBySubjectId: grant.subjectId,
449
+
450
+ adapterId: template.adapterId,
451
+ eventTypes: template.eventTypes,
452
+ configuration: template.configuration,
453
+ sessionTemplate: template.sessionTemplate,
454
+ });
455
+ },
456
+ );
453
457
  } catch (error) {
454
458
  if (error instanceof PrReviewDispatchAuthorityError) {
455
459
  throw new HTTPException(409, { message: error.message });
@@ -492,7 +496,7 @@ export function registerPrReviewGitHubRoutes(app: Hono, deps: ApiRouteDeps): voi
492
496
  const payload = requireFreshState(state, deps, "pr_review_github_selection", workspaceId);
493
497
  requireStateCookie(c, state);
494
498
  await requirePrReviewManageGrant(c, deps, workspaceId, payload);
495
- await requireActivePack(deps, workspaceId);
499
+
496
500
  const selected = c.req.query("installation_id");
497
501
  if (selected === "new") return redirectToInstallation(c, deps, state);
498
502
  const installationId = positiveInteger(selected);
@@ -518,16 +522,6 @@ function requireConfiguredApp(deps: ApiRouteDeps): void {
518
522
  }
519
523
  }
520
524
 
521
- async function requireActivePack(deps: ApiRouteDeps, workspaceId: string) {
522
- const installation = await getPackInstallation(deps.db, workspaceId, OPENGENI_PR_REVIEW_PACK_ID);
523
- if (installation?.status !== "active") {
524
- throw new HTTPException(409, {
525
- message: "Install and enable the OpenGeni Review Bot Pack first",
526
- });
527
- }
528
- return installation;
529
- }
530
-
531
525
  function assertManagedCompute(deps: ApiRouteDeps): void {
532
526
  if (deps.settings.sandboxBackend === "selfhosted") {
533
527
  throw new HTTPException(409, { message: "OpenGeni Lens requires managed compute" });
@@ -539,17 +533,33 @@ async function requirePrReviewManageGrant(
539
533
  deps: ApiRouteDeps,
540
534
  workspaceId: string,
541
535
  state: GitHubSignedStatePayload,
542
- ): Promise<AccessGrant> {
543
- let grant: AccessGrant;
536
+ ): Promise<IntegrationCommitGrant> {
537
+ let grant: IntegrationCommitGrant;
544
538
  try {
545
539
  const access = await requireAccessGrantAuthorization(c, deps, workspaceId, "workspace:admin");
546
540
  requireLegacyOAuthActor(access);
547
- grant = access.grant;
541
+ grant = await integrationCommitGrant(access, ["workspace:admin", "secrets:write"], {
542
+ settings: deps.settings,
543
+ authorizationHeader: c.req.header("authorization"),
544
+ });
548
545
  } catch (error) {
549
546
  if (!(error instanceof HTTPException) || error.status !== 401) throw error;
550
547
  const handedOff = prReviewBrowserGrantFromState(deps, state, workspaceId);
551
548
  if (!handedOff) throw error;
552
- grant = handedOff;
549
+ grant = {
550
+ ...handedOff,
551
+ authorizeCommit: async () => {
552
+ const current = prReviewBrowserGrantFromState(deps, state, workspaceId);
553
+ if (
554
+ !current ||
555
+ current.accountId !== handedOff.accountId ||
556
+ current.subjectId !== handedOff.subjectId
557
+ )
558
+ throw new HTTPException(403, {
559
+ message: "OpenGeni Lens browser handoff expired or changed",
560
+ });
561
+ },
562
+ };
553
563
  }
554
564
  requirePermission(grant, "secrets:write");
555
565
  if (grant.accountId !== state.accountId) {
@@ -1,10 +1,10 @@
1
1
  import { environmentsEncryptionKeyBytes } from "@opengeni/config";
2
+ import { withOrganizationIntegrationAcquisition } from "@opengeni/db/organization-integration-policy";
2
3
  import {
3
4
  CreatePrReviewAppRegistrationRequest,
4
5
  CreatePrReviewRepositoryBindingRequest,
5
6
  PrReviewAppRegistration,
6
7
  PrReviewRepositoryBinding,
7
- OPENGENI_PR_REVIEW_PACK_ID,
8
8
  UpdatePrReviewAppRegistrationRequest,
9
9
  UpdatePrReviewRepositoryBindingRequest,
10
10
  } from "@opengeni/contracts";
@@ -12,17 +12,16 @@ import {
12
12
  assertWorkspaceModelPolicyAllows,
13
13
  canonicalConfiguredModel,
14
14
  defaultPrReviewProviderBaseUrl,
15
- getCapabilityPack,
16
15
  workspaceCustomModelReference,
17
16
  lockActiveCustomModelForAdmission,
18
17
  prReviewWebhookAuthKind,
19
18
  normalizePrReviewProviderBaseUrl,
20
- prReviewPackConnectorId,
21
- PR_REVIEW_AUTOMATION_TEMPLATE_ID,
22
19
  requireAccessGrant,
20
+ requireAccessGrantAuthorization,
23
21
  requirePermission,
24
22
  resolveWorkspaceCatalogSettings,
25
23
  type ApiRouteDeps,
24
+ PR_REVIEW_AUTOMATION_SETUP,
26
25
  } from "@opengeni/core";
27
26
  import {
28
27
  createPrReviewAppRegistration,
@@ -32,7 +31,6 @@ import {
32
31
  decryptVariableSetValue,
33
32
  encryptVariableSetValue,
34
33
  getPrReviewAppRegistrationSecret,
35
- getPackInstallation,
36
34
  listPrReviewAppRegistrations,
37
35
  listPrReviewRepositoryBindings,
38
36
  nestedPostgresSqlState,
@@ -48,6 +46,7 @@ import {
48
46
  verifyPrReviewProviderRepository,
49
47
  } from "../integrations/pr-review-provider";
50
48
  import { registerPrReviewGitHubRoutes } from "./pr-review-github";
49
+ import { integrationCommitGrant } from "../integrations/integration-commit-authority";
51
50
 
52
51
  export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
53
52
  const { db, settings } = deps;
@@ -56,7 +55,7 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
56
55
  app.get("/v1/workspaces/:workspaceId/pr-review/registrations", async (c) => {
57
56
  const workspaceId = c.req.param("workspaceId");
58
57
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read");
59
- await requirePrReviewPackActive(db, workspaceId);
58
+
60
59
  return c.json({
61
60
  registrations: await listPrReviewAppRegistrations(db, grant.accountId, workspaceId),
62
61
  repositories: await listPrReviewRepositoryBindings(db, grant.accountId, workspaceId),
@@ -67,7 +66,7 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
67
66
  const workspaceId = c.req.param("workspaceId");
68
67
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
69
68
  requirePermission(grant, "secrets:write");
70
- const packInstallation = await requirePrReviewPackActive(db, workspaceId);
69
+
71
70
  assertPrReviewSandboxBackend(deps);
72
71
  const payload = CreatePrReviewAppRegistrationRequest.parse(await c.req.json());
73
72
  const encryptionKey = requirePrReviewEncryptionKey(deps);
@@ -94,28 +93,32 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
94
93
  });
95
94
  }
96
95
  const registration = await mapPrReviewUniqueConflict(
97
- createPrReviewAppRegistration(db, {
98
- accountId: grant.accountId,
99
- workspaceId,
100
- name: payload.name,
101
- provider: payload.provider,
102
- providerBaseUrl,
103
- appId: payload.appId ?? null,
104
- credentialKind: payload.credentialKind,
105
- credentialEncrypted: encryptVariableSetValue(
106
- encryptionKey,
107
- payload.privateKey ?? payload.accessToken!,
108
- ),
109
- accessTokenExpiresAt: payload.accessTokenExpiresAt
110
- ? new Date(payload.accessTokenExpiresAt)
111
- : null,
112
- webhookAuthKind: prReviewWebhookAuthKind(payload.provider),
113
- webhookSecretEncrypted: encryptVariableSetValue(encryptionKey, payload.webhookSecret),
114
- webhookUsername: payload.webhookUsername ?? null,
115
- createdBySubjectId: grant.subjectId,
116
- packInstallationId: packInstallation.id,
117
- packConnectorId: prReviewPackConnectorId(payload.provider),
118
- }),
96
+ withOrganizationIntegrationAcquisition(
97
+ db,
98
+ grant,
99
+ [prReviewIntegrationKey(payload.provider)],
100
+ (tx) =>
101
+ createPrReviewAppRegistration(tx, {
102
+ accountId: grant.accountId,
103
+ workspaceId,
104
+ name: payload.name,
105
+ provider: payload.provider,
106
+ providerBaseUrl,
107
+ appId: payload.appId ?? null,
108
+ credentialKind: payload.credentialKind,
109
+ credentialEncrypted: encryptVariableSetValue(
110
+ encryptionKey,
111
+ payload.privateKey ?? payload.accessToken!,
112
+ ),
113
+ accessTokenExpiresAt: payload.accessTokenExpiresAt
114
+ ? new Date(payload.accessTokenExpiresAt)
115
+ : null,
116
+ webhookAuthKind: prReviewWebhookAuthKind(payload.provider),
117
+ webhookSecretEncrypted: encryptVariableSetValue(encryptionKey, payload.webhookSecret),
118
+ webhookUsername: payload.webhookUsername ?? null,
119
+ createdBySubjectId: grant.subjectId,
120
+ }),
121
+ ),
119
122
  "A PR Review registration with this provider and name already exists",
120
123
  );
121
124
  await recordAuditEvent(db, {
@@ -138,7 +141,7 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
138
141
  const workspaceId = c.req.param("workspaceId");
139
142
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
140
143
  requirePermission(grant, "secrets:write");
141
- await requirePrReviewPackActive(db, workspaceId);
144
+
142
145
  const payload = UpdatePrReviewAppRegistrationRequest.parse(await c.req.json());
143
146
  const existing = await getPrReviewAppRegistrationSecret(db, {
144
147
  accountId: grant.accountId,
@@ -272,8 +275,12 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
272
275
 
273
276
  app.post("/v1/workspaces/:workspaceId/pr-review/repositories", async (c) => {
274
277
  const workspaceId = c.req.param("workspaceId");
275
- const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
276
- const packInstallation = await requirePrReviewPackActive(db, workspaceId);
278
+ const grant = await integrationCommitGrant(
279
+ await requireAccessGrantAuthorization(c, deps, workspaceId, "workspace:admin"),
280
+ ["workspace:admin"],
281
+ { settings: deps.settings, authorizationHeader: c.req.header("authorization") },
282
+ );
283
+
277
284
  const payload = CreatePrReviewRepositoryBindingRequest.parse(await c.req.json());
278
285
  const registration = await getPrReviewAppRegistrationSecret(db, {
279
286
  accountId: grant.accountId,
@@ -284,6 +291,8 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
284
291
  throw new HTTPException(404, {
285
292
  message: "PR Review app registration not found",
286
293
  });
294
+ const integrationKey = prReviewIntegrationKey(registration.provider);
295
+ await withOrganizationIntegrationAcquisition(db, grant, [integrationKey], async () => {});
287
296
  const repositoryUrl = new URL(payload.repositoryUri);
288
297
  let canonicalRepositoryUri = repositoryUrl.href;
289
298
  let canonicalRepositoryFullName = payload.repositoryFullName;
@@ -406,35 +415,33 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
406
415
  modelId: model,
407
416
  })
408
417
  : undefined;
409
- const template = getCapabilityPack(OPENGENI_PR_REVIEW_PACK_ID)?.automationTemplates?.find(
410
- (candidate) => candidate.id === PR_REVIEW_AUTOMATION_TEMPLATE_ID,
411
- );
412
- if (!template) {
413
- throw new HTTPException(503, { message: "PR Review automation template is unavailable" });
414
- }
418
+ const template = PR_REVIEW_AUTOMATION_SETUP;
419
+
415
420
  const binding = await mapPrReviewUniqueConflict(
416
- createPrReviewRepositoryBinding(db, {
417
- accountId: grant.accountId,
418
- workspaceId,
419
- registrationId: registration.id,
420
- provider: registration.provider,
421
- repositoryUri: canonicalRepositoryUri,
422
- repositoryFullName: canonicalRepositoryFullName,
423
- providerRepositoryId: canonicalProviderRepositoryId,
424
- installationId:
425
- payload.installationId === undefined ? null : String(payload.installationId),
426
- projectId: canonicalProjectId,
427
- model,
428
- additionalInstructions: payload.additionalInstructions ?? null,
429
- status: payload.status,
430
- createdBySubjectId: grant.subjectId,
431
- packInstallationId: packInstallation.id,
432
- packTemplateId: template.id,
433
- adapterId: template.adapterId,
434
- eventTypes: template.eventTypes,
435
- configuration: template.configuration,
436
- sessionTemplate: template.sessionTemplate,
437
- ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
421
+ withOrganizationIntegrationAcquisition(db, grant, [integrationKey], async (tx) => {
422
+ await grant.authorizeCommit(tx);
423
+ return createPrReviewRepositoryBinding(tx, {
424
+ accountId: grant.accountId,
425
+ workspaceId,
426
+ registrationId: registration.id,
427
+ provider: registration.provider,
428
+ repositoryUri: canonicalRepositoryUri,
429
+ repositoryFullName: canonicalRepositoryFullName,
430
+ providerRepositoryId: canonicalProviderRepositoryId,
431
+ installationId:
432
+ payload.installationId === undefined ? null : String(payload.installationId),
433
+ projectId: canonicalProjectId,
434
+ model,
435
+ additionalInstructions: payload.additionalInstructions ?? null,
436
+ status: payload.status,
437
+ createdBySubjectId: grant.subjectId,
438
+
439
+ adapterId: template.adapterId,
440
+ eventTypes: template.eventTypes,
441
+ configuration: template.configuration,
442
+ sessionTemplate: template.sessionTemplate,
443
+ ...(beforeCreateCommit ? { beforeCreateCommit } : {}),
444
+ });
438
445
  }),
439
446
  "This repository is already bound to the selected PR Review registration",
440
447
  );
@@ -457,7 +464,7 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
457
464
  app.patch("/v1/workspaces/:workspaceId/pr-review/repositories/:bindingId", async (c) => {
458
465
  const workspaceId = c.req.param("workspaceId");
459
466
  const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
460
- await requirePrReviewPackActive(db, workspaceId);
467
+
461
468
  const payload = UpdatePrReviewRepositoryBindingRequest.parse(await c.req.json());
462
469
  const catalogSettings = (
463
470
  await resolveWorkspaceCatalogSettings(db, deps.settings, {
@@ -547,6 +554,12 @@ export function registerPrReviewRoutes(app: Hono, deps: ApiRouteDeps): void {
547
554
  });
548
555
  }
549
556
 
557
+ /** PR Review's server-owned provider, not a repository URL or user label,
558
+ * establishes Lens identity. Other providers have no curated acquisition key. */
559
+ function prReviewIntegrationKey(provider: string): "github-lens" | null {
560
+ return provider === "github" ? "github-lens" : null;
561
+ }
562
+
550
563
  function prReviewCustomModelCommitGuard(input: {
551
564
  settings: ApiRouteDeps["settings"];
552
565
  accountId: string;
@@ -578,16 +591,6 @@ function assertPrReviewSandboxBackend(deps: ApiRouteDeps): void {
578
591
  }
579
592
  }
580
593
 
581
- async function requirePrReviewPackActive(db: ApiRouteDeps["db"], workspaceId: string) {
582
- const installation = await getPackInstallation(db, workspaceId, OPENGENI_PR_REVIEW_PACK_ID);
583
- if (installation?.status !== "active") {
584
- throw new HTTPException(409, {
585
- message: "Install and enable the OpenGeni Review Bot Pack first",
586
- });
587
- }
588
- return installation;
589
- }
590
-
591
594
  function requirePrReviewEncryptionKey(deps: ApiRouteDeps): Uint8Array {
592
595
  const key = environmentsEncryptionKeyBytes(deps.settings);
593
596
  if (!key) {