@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/api-router",
3
- "version": "3.0.1",
3
+ "version": "4.0.0",
4
4
  "description": "OpenGeni HTTP surface: the Hono adapter/router (createApp), routes, MCP HTTP transport, and HTTP access adapters over @opengeni/core. An engine-distribution surface — its runtime closure includes engine-internal packages.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -44,22 +44,22 @@
44
44
  "@llamaindex/liteparse": "2.14.2",
45
45
  "@modelcontextprotocol/sdk": "^1.29.0",
46
46
  "@opengeni/agent-proto": "^0.6.0",
47
- "@opengeni/artifact-tool": "^0.3.29",
47
+ "@opengeni/artifact-tool": "^0.3.30",
48
48
  "@opengeni/capabilities": "^0.3.4",
49
- "@opengeni/codemode": "^0.5.9",
50
- "@opengeni/codex": "^0.2.23",
51
- "@opengeni/config": "^1.2.2",
52
- "@opengeni/contracts": "^4.1.0",
53
- "@opengeni/core": "^3.0.1",
54
- "@opengeni/db": "^5.0.1",
55
- "@opengeni/documents": "^0.8.30",
56
- "@opengeni/events": "^0.4.28",
57
- "@opengeni/github": "^0.7.13",
49
+ "@opengeni/codemode": "^0.6.0",
50
+ "@opengeni/codex": "^0.2.24",
51
+ "@opengeni/config": "^2.0.0",
52
+ "@opengeni/contracts": "^5.0.0",
53
+ "@opengeni/core": "^4.0.0",
54
+ "@opengeni/db": "^6.0.0",
55
+ "@opengeni/documents": "^0.8.31",
56
+ "@opengeni/events": "^0.4.29",
57
+ "@opengeni/github": "^0.7.14",
58
58
  "@opengeni/network": "^0.3.1",
59
- "@opengeni/observability": "^0.8.29",
60
- "@opengeni/runtime": "^3.0.1",
61
- "@opengeni/storage": "^0.2.130",
62
- "@opengeni/tool-gateway": "^0.1.10",
59
+ "@opengeni/observability": "^0.8.30",
60
+ "@opengeni/runtime": "^4.0.0",
61
+ "@opengeni/storage": "^0.2.131",
62
+ "@opengeni/tool-gateway": "^0.1.11",
63
63
  "@opengeni/xai-subscription": "^0.1.4",
64
64
  "@temporalio/client": "^1.17.0",
65
65
  "better-auth": "^1.6.14",
@@ -0,0 +1,14 @@
1
+ import type { Observability } from "@opengeni/observability";
2
+
3
+ /** Only committed, non-replayed server event identities mint admission anchors. */
4
+ export function recordAcceptedApiAdmission(
5
+ observability: Observability | undefined,
6
+ result: { accepted: { id: string }; replay: boolean },
7
+ ): void {
8
+ if (result.replay) return;
9
+ try {
10
+ observability?.recordAdmissionTrace(result.accepted.id);
11
+ } catch {
12
+ // Telemetry must never turn a committed admission into an HTTP failure.
13
+ }
14
+ }
package/src/app.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { registerConnectCallbackReturns } from "./integrations/connect-callback-return";
2
2
  import { registerFeedbackRoutes } from "./routes/feedback";
3
3
  import { codemodeSessionRequest } from "./codemode";
4
- import { SiteSessionPathError } from "@opengeni/contracts";
4
+ import { SiteSessionPathError, OrganizationIntegrationDeniedError } from "@opengeni/contracts";
5
5
  import { registerModelConnectionAccessRoutes } from "./routes/model-connection-access";
6
6
  import {
7
7
  canonicalizeConfiguredModelId,
@@ -55,7 +55,7 @@ import {
55
55
  } from "@opengeni/db";
56
56
  import { requireSessionEventDurableFanoutCapability } from "@opengeni/events";
57
57
  import { githubAppBotIdentityWarnings } from "@opengeni/github";
58
- import { createObservability } from "@opengeni/observability";
58
+ import { createObservability, withTraceContext } from "@opengeni/observability";
59
59
  import { createObjectStorage } from "@opengeni/storage";
60
60
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
61
61
  import { handleMcpRequestWithClientAbort } from "./mcp/request-abort";
@@ -106,6 +106,7 @@ import {
106
106
  runManagedAuthProvider,
107
107
  } from "./auth/managed-auth-attempt-context";
108
108
  import { createManagedEmailTransport } from "./auth/managed-email";
109
+ import { startManagedSignInNotificationDelivery } from "./auth/managed-sign-in-notifications";
109
110
  import {
110
111
  assertManagedEmailTransportMetadata,
111
112
  assertOrganizationUserSetupQueryTransportConfigured,
@@ -149,11 +150,11 @@ import { registerCapabilityRoutes } from "./routes/capabilities";
149
150
  import { registerCatalogAssetRoutes } from "./routes/catalog-assets";
150
151
  import { registerCodexRoutes } from "./routes/codex";
151
152
  import { registerOrganizationModelProviderRoutes } from "./routes/organization-model-providers";
153
+ import { registerOrganizationIntegrationPolicyRoutes } from "./routes/organization-integration-policy";
152
154
  import { registerSuperGrokRoutes } from "./routes/supergrok";
153
155
  import { registerConnectionRoutes } from "./routes/connections";
154
156
  import { registerConnectRoutes } from "./routes/connect";
155
- import { registerHostMcpBindingRoutes } from "./routes/host-mcp-bindings";
156
- import { registerHostMcpResolverRoutes } from "./routes/host-mcp-resolvers";
157
+
157
158
  import { registerExternalIdentityLinkRoutes } from "./routes/external-identity-links";
158
159
  import { registerDocumentRoutes } from "./routes/documents";
159
160
  import { registerKnowledgeRoutes } from "./routes/knowledge";
@@ -178,7 +179,6 @@ import { registerApiIntegrationRoutes } from "./routes/api-integrations";
178
179
  import { registerIntegrationFacetRoutes } from "./routes/integration-facets";
179
180
  import { registerInteractionResourceRoutes } from "./routes/interaction-resources";
180
181
  import { registerPrReviewRoutes } from "./routes/pr-review";
181
- import { registerPackRoutes } from "./routes/packs";
182
182
  import { registerAutomationRoutes } from "./routes/automations";
183
183
  import { registerPluginRoutes } from "./routes/plugins";
184
184
  import { registerSkillRoutes } from "./routes/skills";
@@ -206,13 +206,16 @@ import { registerOrganizationMembershipRoutes } from "./routes/organization-memb
206
206
  import { registerOrganizationSessionRoutes } from "./routes/organization-sessions";
207
207
  import { registerOrganizationRecoveryRoutes } from "./routes/organization-recovery";
208
208
  import { registerManagedOnboardingRoutes } from "./routes/managed-onboarding";
209
+ import {
210
+ registerManagedSignInMethodRoutes,
211
+ handleManagedSignInConnectCallback,
212
+ } from "./routes/managed-sign-in-methods";
209
213
  import {
210
214
  registerManagedAuthSessionSetRoutes,
211
215
  requireManagedAuthProviderRouteAllowed,
212
216
  scrubManagedAuthProviderResponse,
213
217
  } from "./routes/managed-auth-session-sets";
214
218
  import { registerUserResourceAuthorityRoutes } from "./routes/user-resource-authorities";
215
- import { registerConnectionAuthorityRoutes } from "./routes/connection-authorities";
216
219
  import { projectClientModel } from "./model-catalog";
217
220
  import { createTranscriptionService } from "./transcription/service";
218
221
  import { createFfmpegTranscriptionSegmenter } from "./transcription/segmenter";
@@ -299,6 +302,7 @@ export function createAppComposition(deps: AppDependencies): {
299
302
  const managedAuthSessionAdapter =
300
303
  deps.managedAuthSessionAdapter ??
301
304
  (managedAuth ? createBetterAuthSessionAdapter(managedAuth, deps.db) : null);
305
+ if (managedAuth) startManagedSignInNotificationDelivery(deps.db, managedEmailTransport);
302
306
  const objectStorage =
303
307
  deps.objectStorage === undefined ? createObjectStorage(deps.settings) : deps.objectStorage;
304
308
  let documentServices: DocumentServices | null = deps.documentServices ?? null;
@@ -538,70 +542,76 @@ export function createAppComposition(deps: AppDependencies): {
538
542
  const route = routeLabel(url.pathname);
539
543
  const correlationId = correlationIds.get(c.req.raw) ?? crypto.randomUUID();
540
544
  const start = performance.now();
541
- const span = observability.startSpan(`HTTP ${c.req.method} ${route}`, {
542
- "http.request.method": c.req.method,
543
- "opengeni.route": route,
545
+ const span = observability.startSpan(
546
+ `HTTP ${c.req.method} ${route}`,
547
+ {
548
+ "http.request.method": c.req.method,
549
+ "opengeni.route": route,
550
+ },
551
+ { parent: null },
552
+ );
553
+ return await withTraceContext(span, async () => {
554
+ try {
555
+ await next();
556
+ const status = c.res.status || 200;
557
+ const durationSeconds = (performance.now() - start) / 1000;
558
+ observability.recordHttpRequest({
559
+ method: c.req.method,
560
+ route,
561
+ status,
562
+ durationSeconds,
563
+ });
564
+ span.end({
565
+ attributes: {
566
+ "http.response.status_code": status,
567
+ "opengeni.duration_ms": Math.round(durationSeconds * 1000),
568
+ },
569
+ });
570
+ observability.info("HTTP request completed", {
571
+ method: c.req.method,
572
+ route,
573
+ status,
574
+ durationMs: Math.round(durationSeconds * 1000),
575
+ traceId: span.traceId,
576
+ spanId: span.spanId,
577
+ correlationId,
578
+ });
579
+ } catch (error) {
580
+ const status = httpStatusForError(error);
581
+ const errorCode = errorCodeForStatus(status);
582
+ const durationSeconds = (performance.now() - start) / 1000;
583
+ observability.recordHttpRequest({
584
+ method: c.req.method,
585
+ route,
586
+ status,
587
+ durationSeconds,
588
+ });
589
+ observability.incrementCounter({
590
+ name: "opengeni_http_errors_total",
591
+ help: "Total OpenGeni HTTP request failures by bounded route, status, and stable code.",
592
+ labels: { route, status: String(status), code: errorCode },
593
+ });
594
+ span.end({
595
+ attributes: {
596
+ "http.response.status_code": status,
597
+ "opengeni.duration_ms": Math.round(durationSeconds * 1000),
598
+ },
599
+ error,
600
+ });
601
+ observability.error("HTTP request failed", {
602
+ method: c.req.method,
603
+ route,
604
+ status,
605
+ durationMs: Math.round(durationSeconds * 1000),
606
+ traceId: span.traceId,
607
+ spanId: span.spanId,
608
+ correlationId,
609
+ errorCode,
610
+ errorClass: "HttpOperationError",
611
+ });
612
+ throw error;
613
+ }
544
614
  });
545
- try {
546
- await next();
547
- const status = c.res.status || 200;
548
- const durationSeconds = (performance.now() - start) / 1000;
549
- observability.recordHttpRequest({
550
- method: c.req.method,
551
- route,
552
- status,
553
- durationSeconds,
554
- });
555
- span.end({
556
- attributes: {
557
- "http.response.status_code": status,
558
- "opengeni.duration_ms": Math.round(durationSeconds * 1000),
559
- },
560
- });
561
- observability.info("HTTP request completed", {
562
- method: c.req.method,
563
- route,
564
- status,
565
- durationMs: Math.round(durationSeconds * 1000),
566
- traceId: span.traceId,
567
- spanId: span.spanId,
568
- correlationId,
569
- });
570
- } catch (error) {
571
- const status = httpStatusForError(error);
572
- const errorCode = errorCodeForStatus(status);
573
- const durationSeconds = (performance.now() - start) / 1000;
574
- observability.recordHttpRequest({
575
- method: c.req.method,
576
- route,
577
- status,
578
- durationSeconds,
579
- });
580
- observability.incrementCounter({
581
- name: "opengeni_http_errors_total",
582
- help: "Total OpenGeni HTTP request failures by bounded route, status, and stable code.",
583
- labels: { route, status: String(status), code: errorCode },
584
- });
585
- span.end({
586
- attributes: {
587
- "http.response.status_code": status,
588
- "opengeni.duration_ms": Math.round(durationSeconds * 1000),
589
- },
590
- error,
591
- });
592
- observability.error("HTTP request failed", {
593
- method: c.req.method,
594
- route,
595
- status,
596
- durationMs: Math.round(durationSeconds * 1000),
597
- traceId: span.traceId,
598
- spanId: span.spanId,
599
- correlationId,
600
- errorCode,
601
- errorClass: "HttpOperationError",
602
- });
603
- throw error;
604
- }
605
615
  });
606
616
 
607
617
  const accessKeyBoundary = requireAccessKey(deps.settings);
@@ -669,10 +679,61 @@ export function createAppComposition(deps: AppDependencies): {
669
679
  // wildcard handler or the provider returns its own 404 first.
670
680
  registerManagedOnboardingRoutes(app, routeDeps);
671
681
  registerManagedAuthSessionSetRoutes(app, routeDeps);
682
+ registerManagedSignInMethodRoutes(app, routeDeps);
672
683
  if (managedAuth) {
673
684
  app.on(["GET", "POST"], "/v1/auth/*", async (c) => {
674
685
  const pathname = new URL(c.req.url).pathname;
675
686
  const oauthCallbackProvider = managedAuthOAuthCallbackProvider(pathname);
687
+ if (pathname === "/v1/auth/sign-in/social" && c.req.method === "POST") {
688
+ const body = await c.req.raw
689
+ .clone()
690
+ .json()
691
+ .catch(() => null);
692
+ if (
693
+ !body ||
694
+ !["google", "github"].includes(body.provider) ||
695
+ Object.prototype.hasOwnProperty.call(body, "idToken")
696
+ ) {
697
+ return c.json(
698
+ {
699
+ code: "SIGN_IN_METHOD_OAUTH_REDIRECT_REQUIRED",
700
+ message: "Use the browser OAuth sign-in redirect",
701
+ },
702
+ 403,
703
+ );
704
+ }
705
+ }
706
+ if (oauthCallbackProvider) {
707
+ const connectResponse = await handleManagedSignInConnectCallback(
708
+ c,
709
+ routeDeps,
710
+ oauthCallbackProvider,
711
+ );
712
+ if (connectResponse) return connectResponse;
713
+ }
714
+ if (
715
+ new Set([
716
+ "link-social",
717
+ "unlink-account",
718
+ "list-accounts",
719
+ "set-password",
720
+ "change-password",
721
+ "change-email",
722
+ "update-user",
723
+ "delete-user",
724
+ "get-access-token",
725
+ "refresh-token",
726
+ "account-info",
727
+ ]).has(pathname.slice("/v1/auth/".length))
728
+ ) {
729
+ return c.json(
730
+ {
731
+ code: "SIGN_IN_METHOD_PRODUCT_ROUTE_REQUIRED",
732
+ message: "Use personal sign-in method settings",
733
+ },
734
+ 403,
735
+ );
736
+ }
676
737
  if (deps.settings.managedAuthSessionSetMode === "legacy") {
677
738
  return oauthCallbackProvider
678
739
  ? await runManagedAuthProvider(
@@ -1256,8 +1317,7 @@ export function createAppComposition(deps: AppDependencies): {
1256
1317
  registerPersonalGitHubGitBrokerRoutes(app, routeDeps);
1257
1318
  registerConnectionRoutes(app, routeDeps);
1258
1319
  registerConnectRoutes(app, routeDeps);
1259
- registerHostMcpBindingRoutes(app, routeDeps);
1260
- registerHostMcpResolverRoutes(app, routeDeps);
1320
+
1261
1321
  registerExternalIdentityLinkRoutes(app, routeDeps);
1262
1322
  registerCapabilityRoutes(app, routeDeps);
1263
1323
  registerApiIntegrationRoutes(app, routeDeps);
@@ -1268,7 +1328,6 @@ export function createAppComposition(deps: AppDependencies): {
1268
1328
  registerEnvironmentRoutes(app, routeDeps);
1269
1329
  registerChannelRoutes(app, routeDeps);
1270
1330
  registerRigRoutes(app, routeDeps);
1271
- registerPackRoutes(app, routeDeps);
1272
1331
  registerAutomationRoutes(app, routeDeps);
1273
1332
  registerPrReviewRoutes(app, routeDeps);
1274
1333
  registerPluginRoutes(app, routeDeps);
@@ -1278,6 +1337,7 @@ export function createAppComposition(deps: AppDependencies): {
1278
1337
  registerScheduledTaskRoutes(app, routeDeps);
1279
1338
  registerCodexRoutes(app, routeDeps);
1280
1339
  registerOrganizationModelProviderRoutes(app, routeDeps);
1340
+ registerOrganizationIntegrationPolicyRoutes(app, routeDeps);
1281
1341
  registerModelConnectionAccessRoutes(app, routeDeps);
1282
1342
  registerSuperGrokRoutes(app, routeDeps);
1283
1343
  registerTranscriptionRoutes(app, routeDeps);
@@ -1288,7 +1348,6 @@ export function createAppComposition(deps: AppDependencies): {
1288
1348
  registerOrganizationSessionRoutes(app, routeDeps);
1289
1349
  registerOrganizationRecoveryRoutes(app, routeDeps);
1290
1350
  registerUserResourceAuthorityRoutes(app, routeDeps);
1291
- registerConnectionAuthorityRoutes(app, routeDeps);
1292
1351
  registerSlackInteractionRoutes(app, routeDeps);
1293
1352
 
1294
1353
  app.notFound((c) => {
@@ -1311,7 +1370,10 @@ export function createAppComposition(deps: AppDependencies): {
1311
1370
  app.onError((rawError, c) => {
1312
1371
  // One central mapping for every Send/Steer/control route: a bounded
1313
1372
  // control-prefix wait that expired is a known, retryable, not-applied 503.
1314
- const error = workspaceControlBusyHttpError(rawError) ?? rawError;
1373
+ const error =
1374
+ rawError instanceof OrganizationIntegrationDeniedError
1375
+ ? new HTTPException(403, { message: rawError.message })
1376
+ : (workspaceControlBusyHttpError(rawError) ?? rawError);
1315
1377
  const compactionLock = codexCompactionV2ProviderLockedError(error);
1316
1378
  const apiError = error instanceof ApiHttpError ? error : null;
1317
1379
  const status = compactionLock ? 422 : httpStatusForError(error);
@@ -1861,6 +1923,10 @@ const routeLabelPatterns: Array<{
1861
1923
  pattern: /^\/v1\/workspaces\/[^/]+\/sessions$/,
1862
1924
  label: "/v1/workspaces/:workspaceId/sessions",
1863
1925
  },
1926
+ {
1927
+ pattern: /^\/v1\/workspaces\/[^/]+\/session-message-search$/,
1928
+ label: "/v1/workspaces/:workspaceId/session-message-search",
1929
+ },
1864
1930
  {
1865
1931
  pattern: /^\/v1\/workspaces\/[^/]+\/control-events\/stream$/,
1866
1932
  label: "/v1/workspaces/:workspaceId/control-events/stream",
@@ -2373,26 +2439,7 @@ const routeLabelPatterns: Array<{
2373
2439
  pattern: /^\/v1\/workspaces\/[^/]+\/environments\/[^/]+$/,
2374
2440
  label: "/v1/workspaces/:workspaceId/environments/:id",
2375
2441
  },
2376
- {
2377
- pattern: /^\/v1\/workspaces\/[^/]+\/packs$/,
2378
- label: "/v1/workspaces/:workspaceId/packs",
2379
- },
2380
- {
2381
- pattern: /^\/v1\/workspaces\/[^/]+\/packs\/installations$/,
2382
- label: "/v1/workspaces/:workspaceId/packs/installations",
2383
- },
2384
- {
2385
- pattern: /^\/v1\/workspaces\/[^/]+\/packs\/marketing-social-daily-analysis\/scheduled-tasks$/,
2386
- label: "/v1/workspaces/:workspaceId/packs/marketing-social-daily-analysis/scheduled-tasks",
2387
- },
2388
- {
2389
- pattern: /^\/v1\/workspaces\/[^/]+\/packs\/[^/]+\/enable$/,
2390
- label: "/v1/workspaces/:workspaceId/packs/:id/enable",
2391
- },
2392
- {
2393
- pattern: /^\/v1\/workspaces\/[^/]+\/packs\/[^/]+$/,
2394
- label: "/v1/workspaces/:workspaceId/packs/:id",
2395
- },
2442
+
2396
2443
  {
2397
2444
  pattern: /^\/v1\/workspaces\/[^/]+\/plugins\/preview$/,
2398
2445
  label: "/v1/workspaces/:workspaceId/plugins/preview",
@@ -11,6 +11,15 @@ type ManagedAuthAttemptContext =
11
11
  | { kind: "discard_provider_session" };
12
12
 
13
13
  const managedAuthAttemptStorage = new AsyncLocalStorage<ManagedAuthAttemptContext>();
14
+ const managedLinkIntentStorage = new AsyncLocalStorage<string>();
15
+
16
+ export function runManagedSignInConnect<T>(intentId: string, action: () => T): T {
17
+ return managedLinkIntentStorage.run(intentId, action);
18
+ }
19
+
20
+ export function currentManagedSignInConnectIntent(): string | undefined {
21
+ return managedLinkIntentStorage.getStore();
22
+ }
14
23
 
15
24
  export function runManagedAuthAttempt<T>(
16
25
  transactionId: string,
@@ -8,6 +8,17 @@ import type { ManagedAuth } from "@opengeni/core";
8
8
  import { createHmac, timingSafeEqual } from "node:crypto";
9
9
  import { runManagedAuthAttempt } from "./managed-auth-attempt-context";
10
10
 
11
+ /** Server-only proof for a product-owned endpoint; never returned to a browser. */
12
+ export async function managedAuthSelectedProofHeaders(
13
+ auth: ManagedAuth,
14
+ token: string,
15
+ ): Promise<Headers> {
16
+ const context = await auth.$context;
17
+ return new Headers({
18
+ cookie: `${context.authCookies.sessionToken.name}=${signedCookieValue(token, context.secret)}`,
19
+ });
20
+ }
21
+
11
22
  export function createBetterAuthSessionAdapter(
12
23
  auth: ManagedAuth,
13
24
  db: Database,
@@ -14,16 +14,18 @@ import {
14
14
  } from "@opengeni/db/canonical-human-identities";
15
15
  import { betterAuth } from "better-auth";
16
16
  import { createEmailVerificationToken } from "better-auth/api";
17
- import { hashPassword } from "better-auth/crypto";
17
+ import { hashPassword, verifyPassword } from "better-auth/crypto";
18
18
  import { sql } from "drizzle-orm";
19
19
  import { Pool } from "pg";
20
20
 
21
21
  import { decideCanonicalHumanSessionAdmission } from "./canonical-human-session-admission";
22
+ import { deliverManagedSignInNotification } from "./managed-sign-in-notifications";
22
23
  import {
23
24
  currentManagedAuthProviderId,
24
25
  currentManagedAuthAttemptId,
25
26
  recordCurrentManagedAuthSession,
26
27
  shouldDiscardCurrentManagedAuthProviderSession,
28
+ currentManagedSignInConnectIntent,
27
29
  } from "./managed-auth-attempt-context";
28
30
 
29
31
  // `ManagedAuth` (the Better Auth `Auth<any>` alias) is owned by @opengeni/core
@@ -66,6 +68,10 @@ export async function hashManagedAuthPassword(password: string): Promise<string>
66
68
  return await hashPassword(password);
67
69
  }
68
70
 
71
+ export async function verifyManagedAuthPassword(password: string, hash: string): Promise<boolean> {
72
+ return await verifyPassword({ password, hash });
73
+ }
74
+
69
75
  export function createManagedAuth(
70
76
  settings: Settings,
71
77
  db: Database,
@@ -180,7 +186,18 @@ export function createManagedAuth(
180
186
  updatedAt: "updated_at",
181
187
  },
182
188
  accountLinking: {
183
- enabled: false,
189
+ enabled: true,
190
+ requireLocalEmailVerified: true,
191
+ trustedProviders: [],
192
+ allowDifferentEmails: false,
193
+ },
194
+ additionalFields: {
195
+ managedLinkIntentId: {
196
+ type: "string",
197
+ fieldName: "managed_link_intent_id",
198
+ input: false,
199
+ returned: false,
200
+ },
184
201
  },
185
202
  encryptOAuthTokens: true,
186
203
  storeStateStrategy: "database",
@@ -264,6 +281,26 @@ export function createManagedAuth(
264
281
  },
265
282
  },
266
283
  databaseHooks: {
284
+ account: {
285
+ create: {
286
+ before: async (account) => ({
287
+ data: {
288
+ ...account,
289
+ ...(currentManagedSignInConnectIntent()
290
+ ? { managedLinkIntentId: currentManagedSignInConnectIntent() }
291
+ : {}),
292
+ },
293
+ }),
294
+ after: async (account) => {
295
+ if (
296
+ !["google", "github"].includes(account.providerId) ||
297
+ currentManagedSignInConnectIntent()
298
+ )
299
+ return;
300
+ await deliverManagedSignInNotification(db, managedEmailTransport, account.id);
301
+ },
302
+ },
303
+ },
267
304
  session: {
268
305
  create: {
269
306
  before: async (session) => {
@@ -0,0 +1,81 @@
1
+ import type { ManagedEmailTransport } from "@opengeni/core";
2
+ import type { Database } from "@opengeni/db";
3
+ import { sql } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ const Claim = z.object({
7
+ operationId: z.string().uuid(),
8
+ leaseId: z.string().uuid(),
9
+ email: z.string().email(),
10
+ provider: z.enum(["password", "google", "github"]),
11
+ sender: z.string(),
12
+ });
13
+ const started = new WeakSet<object>();
14
+
15
+ export async function deliverManagedSignInNotification(
16
+ db: Database,
17
+ transport: ManagedEmailTransport,
18
+ operationId: string | null = null,
19
+ ): Promise<"sent" | "failed" | "outcome_unknown"> {
20
+ try {
21
+ const result = await db.execute(
22
+ sql`select claim_managed_sign_in_notification(${operationId}::uuid,${transport.sender},${transport.idempotency.scope},${transport.idempotency.retentionSeconds}::integer) as claim`,
23
+ );
24
+ const row = (Array.isArray(result) ? result : result.rows)[0] as { claim: unknown } | undefined;
25
+ if (!row?.claim) return "outcome_unknown";
26
+ const claim = Claim.parse(row.claim);
27
+ let status: "sent" | "failed" | "outcome_unknown" = "outcome_unknown";
28
+ let timer: ReturnType<typeof setTimeout> | undefined;
29
+ try {
30
+ const response = await Promise.race([
31
+ transport.send({
32
+ kind: "sign_in_method_changed",
33
+ from: claim.sender,
34
+ to: claim.email,
35
+ idempotencyKey: `sign-in-method:${claim.operationId}`,
36
+ subject: "Your OpenGeni sign-in methods changed",
37
+ text: `Your OpenGeni ${claim.provider} sign-in method changed. If this was not you, reset your password and contact your administrator.`,
38
+ html: `<p>Your OpenGeni ${claim.provider} sign-in method changed. If this was not you, reset your password and contact your administrator.</p>`,
39
+ }),
40
+ new Promise<{ status: "outcome_unknown" }>((resolve) => {
41
+ timer = setTimeout(() => resolve({ status: "outcome_unknown" }), 5_000);
42
+ }),
43
+ ]);
44
+ status = response.status;
45
+ } catch {
46
+ /* Retain the durable unknown claim for idempotent retry. */
47
+ } finally {
48
+ if (timer) clearTimeout(timer);
49
+ }
50
+ await db.execute(
51
+ sql`select settle_managed_sign_in_notification(${claim.operationId}::uuid,${claim.leaseId}::uuid,${status})`,
52
+ );
53
+ return status;
54
+ } catch {
55
+ // Security state is already committed. A failed claim/ack must never be
56
+ // reported as a failed method change; its durable obligation remains live.
57
+ return "outcome_unknown";
58
+ }
59
+ }
60
+
61
+ export function startManagedSignInNotificationDelivery(
62
+ db: Database,
63
+ transport: ManagedEmailTransport,
64
+ ): void {
65
+ if (started.has(db as object)) return;
66
+ started.add(db as object);
67
+ let active = false;
68
+ const drain = async () => {
69
+ if (active) return;
70
+ active = true;
71
+ try {
72
+ for (let count = 0; count < 10; count++)
73
+ if ((await deliverManagedSignInNotification(db, transport)) === "outcome_unknown") break;
74
+ } finally {
75
+ active = false;
76
+ }
77
+ };
78
+ const timer = setInterval(() => void drain(), 60_000);
79
+ (timer as ReturnType<typeof setInterval> & { unref?: () => void }).unref?.();
80
+ void drain();
81
+ }
@@ -85,13 +85,7 @@ const RESERVED_MACHINE_SUBJECT_NAMESPACES = [
85
85
  export function isPersonalConnectionOwnerPrincipal(access: AccessGrantAuthorization): boolean {
86
86
  const { grant } = access;
87
87
  return (
88
- // The anti-substitution invariant `requireConnectionAuthorityOwner` uses for
89
- // the same question: every context grant agrees on principal kind, delegated
90
- // flag and service flags, and the selected grant's account resolves to
91
- // exactly one matching account grant. Without it a grant whose account has
92
- // no matching account grant (a surviving membership row in an organization
93
- // where the membership is no longer active) could mint a personal
94
- // Connection that the sibling helper would refuse.
88
+ // Every context grant must agree on the authenticated principal and account.
95
89
  access.contextIntegrity &&
96
90
  access.authenticatedSubjectId === grant.subjectId &&
97
91
  grant.principalKind === "human_session" &&
@@ -132,8 +126,7 @@ export function personalOnlyConnectionPrincipalMessage(label: string): string {
132
126
  * rejects every reserved machine-subject namespace as defence-in-depth. The
133
127
  * sibling guards a self-service authority surface where the caller claims to
134
128
  * *be* the owner (403 "not you"), while this one rejects an ownership *value*
135
- * that is unavailable to the caller, alongside `assertOwnershipAllowed`'s
136
- * existing 422 convention.
129
+ * that is unavailable to the caller (422).
137
130
  */
138
131
  export function assertPersonalConnectionOwnerPrincipal(
139
132
  access: AccessGrantAuthorization,
@@ -47,6 +47,7 @@ import {
47
47
  PostgresEditableArtifactStore,
48
48
  listEditableArtifactIdsForSession,
49
49
  touchEditableArtifactSessionLink,
50
+ recordNativeDocumentInspection,
50
51
  dbSql,
51
52
  withRlsContext,
52
53
  type Database,
@@ -167,6 +168,11 @@ export async function createStandaloneEditableArtifactApplication(input: {
167
168
  await touchEditableArtifactSessionLink(input.db, scope, sessionId, artifactId),
168
169
  } satisfies EditableArtifactAgentAssociationPort),
169
170
  inspector: kernel,
171
+ recordInspection: async (inspection) => {
172
+ if (inspection.actor.kind !== "agent")
173
+ throw new Error("Inspection proof requires an agent attempt");
174
+ return recordNativeDocumentInspection(input.db, { ...inspection, actor: inspection.actor });
175
+ },
170
176
  officeImports,
171
177
  workspaceFiles: new EditableArtifactWorkspaceFileAdapter({
172
178
  db: input.db,