@opengeni/api-router 2.5.0 → 2.6.4-canary.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 (70) hide show
  1. package/dist/app.d.ts +2 -1
  2. package/dist/app.js +3 -1
  3. package/dist/auth/managed-auth-attempt-context.d.ts +5 -1
  4. package/dist/auth/managed-auth.d.ts +24 -1
  5. package/dist/{chunk-QESX7HDK.js → chunk-UOI7KAP3.js} +3240 -791
  6. package/dist/chunk-UOI7KAP3.js.map +1 -0
  7. package/dist/http/sse.d.ts +9 -0
  8. package/dist/index.js +117 -5
  9. package/dist/index.js.map +1 -1
  10. package/dist/interaction-metrics.d.ts +2 -0
  11. package/dist/mcp/company-brain-governed-writes.d.ts +1 -1
  12. package/dist/mcp/company-profile-agent-admin.d.ts +6 -6
  13. package/dist/mcp/remember.d.ts +2 -2
  14. package/dist/mcp/scheduled-task-view.d.ts +3 -3
  15. package/dist/mcp/server.d.ts +1 -0
  16. package/dist/mcp/session-view.d.ts +1 -0
  17. package/dist/mcp/session-wait.d.ts +19 -0
  18. package/dist/model-catalog.d.ts +5 -31
  19. package/dist/routes/api-keys.d.ts +2 -0
  20. package/dist/routes/browser-sessions.d.ts +1 -0
  21. package/dist/routes/computer-sessions.d.ts +12 -0
  22. package/dist/routes/managed-auth-session-sets.d.ts +7 -0
  23. package/dist/routes/workspaces.d.ts +1 -1
  24. package/dist/sandbox/metrics-ingestion.d.ts +16 -0
  25. package/dist/workspace-delete-observability.d.ts +10 -0
  26. package/package.json +19 -19
  27. package/src/app.ts +206 -29
  28. package/src/auth/managed-auth-attempt-context.ts +40 -3
  29. package/src/auth/managed-auth-session-adapter.ts +1 -0
  30. package/src/auth/managed-auth.ts +164 -4
  31. package/src/http/sse.ts +279 -45
  32. package/src/index.ts +13 -1
  33. package/src/integrations/oauth-client.ts +8 -1
  34. package/src/integrations/provider-oauth.ts +12 -2
  35. package/src/integrations/slack-interactions.ts +77 -37
  36. package/src/interaction-metrics.ts +30 -0
  37. package/src/mcp/company-brain-governed-writes.ts +38 -23
  38. package/src/mcp/company-profile-agent-admin.ts +7 -7
  39. package/src/mcp/remember.ts +19 -8
  40. package/src/mcp/server.ts +328 -27
  41. package/src/mcp/session-wait.ts +56 -8
  42. package/src/model-catalog.ts +45 -337
  43. package/src/routes/api-integrations.ts +2 -2
  44. package/src/routes/api-keys.ts +149 -7
  45. package/src/routes/automations.ts +186 -19
  46. package/src/routes/browser-sessions.ts +10 -2
  47. package/src/routes/capabilities.ts +3 -3
  48. package/src/routes/codex.ts +483 -74
  49. package/src/routes/company-profile.ts +64 -0
  50. package/src/routes/computer-sessions.ts +103 -1
  51. package/src/routes/connections.ts +271 -16
  52. package/src/routes/github.ts +0 -8
  53. package/src/routes/integration-facets.ts +8 -5
  54. package/src/routes/interaction-resources.ts +7 -1
  55. package/src/routes/managed-auth-session-sets.ts +199 -2
  56. package/src/routes/organization-memberships.ts +28 -8
  57. package/src/routes/packs.ts +5 -5
  58. package/src/routes/plugins.ts +2 -2
  59. package/src/routes/pr-review.ts +80 -4
  60. package/src/routes/scheduled-tasks.ts +47 -11
  61. package/src/routes/sessions.ts +75 -53
  62. package/src/routes/skills.ts +3 -3
  63. package/src/routes/supergrok.ts +3 -2
  64. package/src/routes/workspaces.ts +665 -56
  65. package/src/sandbox/channel-a.ts +10 -4
  66. package/src/sandbox/machines.ts +13 -6
  67. package/src/sandbox/metrics-ingestion.ts +157 -3
  68. package/src/sandbox/viewer.ts +20 -1
  69. package/src/workspace-delete-observability.ts +75 -0
  70. package/dist/chunk-QESX7HDK.js.map +0 -1
package/src/app.ts CHANGED
@@ -44,6 +44,7 @@ import {
44
44
  withSessionRlsActorContext,
45
45
  } from "@opengeni/db";
46
46
  import { requireSessionEventDurableFanoutCapability } from "@opengeni/events";
47
+ import { githubAppBotIdentityWarnings } from "@opengeni/github";
47
48
  import { createObservability } from "@opengeni/observability";
48
49
  import { createObjectStorage } from "@opengeni/storage";
49
50
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
@@ -65,19 +66,35 @@ import {
65
66
  requireLiveAgentAttemptAuthorization,
66
67
  requirePermission,
67
68
  releaseManagedAuthRequestActorLease,
69
+ resolveCatalogSettings,
70
+ resolveWorkspaceCatalogSettings,
68
71
  validateManagedAuthRequestActorLease,
69
72
  requireSessionAuthorization,
70
73
  SessionAuthorizationDeniedError,
71
74
  SessionAuthorizationUnavailableError,
72
75
  } from "@opengeni/core";
73
- import { createManagedAuth } from "./auth/managed-auth";
74
76
  import {
77
+ createManagedAuth,
78
+ isolatedManagedAuthOAuthCallbackRequest,
79
+ managedAuthOAuthReturnMatches,
80
+ resolveManagedAuthOAuthAttempt,
81
+ } from "./auth/managed-auth";
82
+ import {
83
+ adoptManagedAuthSession,
75
84
  MANAGED_AUTH_SESSION_SET_COOKIE,
76
85
  ManagedAuthActorChangeError,
86
+ managedAuthCsrfHash,
87
+ managedAuthDerivedUuid,
88
+ managedAuthSecretRequestDigest,
77
89
  managedAuthSha256,
78
90
  } from "@opengeni/core/managed-auth-session-sets";
79
91
  import { createBetterAuthSessionAdapter } from "./auth/managed-auth-session-adapter";
80
- import { runManagedAuthDiscardedProviderSession } from "./auth/managed-auth-attempt-context";
92
+ import {
93
+ currentManagedAuthCreatedSessionId,
94
+ runManagedAuthAttempt,
95
+ runManagedAuthDiscardedProviderSession,
96
+ runManagedAuthProvider,
97
+ } from "./auth/managed-auth-attempt-context";
81
98
  import { createManagedEmailTransport } from "./auth/managed-email";
82
99
  import { assertManagedEmailTransportMetadata } from "./auth/organization-user-setup";
83
100
  import { createApiSandboxClient, makeResumeBoxById } from "./sandbox/access";
@@ -207,6 +224,22 @@ export function createApp(deps: AppDependencies): Hono {
207
224
  return createAppComposition(deps).app;
208
225
  }
209
226
 
227
+ export async function resolveWorkspaceMcpRouteDeps(
228
+ routeDeps: ApiRouteDeps,
229
+ grant: AccessGrant,
230
+ ): Promise<ApiRouteDeps> {
231
+ const catalogSourceSettings = routeDeps.catalogSourceSettings ?? routeDeps.settings;
232
+ const resolvedCatalog = await resolveWorkspaceCatalogSettings(
233
+ routeDeps.db,
234
+ catalogSourceSettings,
235
+ {
236
+ accountId: grant.accountId,
237
+ workspaceId: grant.workspaceId,
238
+ },
239
+ );
240
+ return { ...routeDeps, catalogSourceSettings, settings: resolvedCatalog.settings };
241
+ }
242
+
210
243
  export function createAppComposition(deps: AppDependencies): {
211
244
  app: Hono;
212
245
  routeDeps: ApiRouteDeps;
@@ -343,6 +376,7 @@ export function createAppComposition(deps: AppDependencies): {
343
376
  : deps.transcriptionSegmenter;
344
377
  const routeDeps: ApiRouteDeps = {
345
378
  ...deps,
379
+ resolveCatalogSettings: () => resolveCatalogSettings(deps.db, deps.settings),
346
380
  observability,
347
381
  githubStateSecret:
348
382
  deps.githubStateSecret ?? deps.settings.githubAppManifestStateSecret ?? crypto.randomUUID(),
@@ -579,26 +613,111 @@ export function createAppComposition(deps: AppDependencies): {
579
613
  registerManagedAuthSessionSetRoutes(app, routeDeps);
580
614
  if (managedAuth) {
581
615
  app.on(["GET", "POST"], "/v1/auth/*", async (c) => {
616
+ const pathname = new URL(c.req.url).pathname;
617
+ const oauthCallbackProvider = managedAuthOAuthCallbackProvider(pathname);
582
618
  if (deps.settings.managedAuthSessionSetMode === "legacy") {
583
- return await managedAuth.handler(c.req.raw);
619
+ return oauthCallbackProvider
620
+ ? await runManagedAuthProvider(
621
+ oauthCallbackProvider,
622
+ async () => await managedAuth.handler(c.req.raw),
623
+ )
624
+ : await managedAuth.handler(c.req.raw);
584
625
  }
585
- requireManagedAuthProviderRouteAllowed(c.req.method, new URL(c.req.url).pathname);
626
+ requireManagedAuthProviderRouteAllowed(c.req.method, pathname);
586
627
  // Provider authentication/recovery is isolated from whichever actor the
587
628
  // browser currently renders. Selected-session capabilities are all
588
629
  // product-owned above this wildcard and generation/epoch fenced.
589
- const headers = new Headers(c.req.raw.headers);
590
- headers.delete("cookie");
591
- headers.delete("authorization");
592
- headers.delete("x-forwarded-user");
593
- const providerRequest = new Request(c.req.raw, { headers });
594
630
  const authority = getCookie(c, MANAGED_AUTH_SESSION_SET_COOKIE);
595
- const discardProviderSession =
596
- deps.settings.managedAuthSessionSetMode === "broker" || authority !== undefined;
597
- const providerResponse = discardProviderSession
598
- ? await runManagedAuthDiscardedProviderSession(
599
- async () => await managedAuth.handler(providerRequest),
631
+ let providerResponse: Response;
632
+ let preserveCookieNames: readonly string[] | undefined;
633
+ if (oauthCallbackProvider) {
634
+ const attempt = await resolveManagedAuthOAuthAttempt(
635
+ managedAuth,
636
+ c.req.raw,
637
+ oauthCallbackProvider,
638
+ deps.settings.publicBaseUrl!,
639
+ );
640
+ if (!attempt || !authority || attempt.authorityHash !== managedAuthSha256(authority)) {
641
+ throw new HTTPException(409, { message: "provider_route_blocked" });
642
+ }
643
+ const isolated = await isolatedManagedAuthOAuthCallbackRequest(managedAuth, c.req.raw);
644
+ preserveCookieNames = [isolated.stateCookieName];
645
+ const handled = await runManagedAuthAttempt(
646
+ attempt.transactionId,
647
+ oauthCallbackProvider,
648
+ async () => {
649
+ const response = await managedAuth.handler(isolated.request);
650
+ return {
651
+ response,
652
+ authSessionId: currentManagedAuthCreatedSessionId(),
653
+ };
654
+ },
655
+ );
656
+ providerResponse = handled.response;
657
+ if (
658
+ !handled.authSessionId &&
659
+ managedAuthOAuthReturnMatches(
660
+ providerResponse.headers.get("location") ?? "",
661
+ new URL(deps.settings.publicBaseUrl!).origin,
662
+ attempt.transactionId,
663
+ "complete",
600
664
  )
601
- : await managedAuth.handler(providerRequest);
665
+ ) {
666
+ const location = new URL("/account-auth", deps.settings.publicBaseUrl!);
667
+ location.searchParams.set("transaction", attempt.transactionId);
668
+ location.searchParams.set("social", "error");
669
+ providerResponse = Response.redirect(location, 302);
670
+ } else if (handled.authSessionId) {
671
+ try {
672
+ await adoptManagedAuthSession({
673
+ db: deps.db,
674
+ adapter: managedAuthSessionAdapter!,
675
+ authority,
676
+ authorityHash: attempt.authorityHash,
677
+ csrfHash: managedAuthCsrfHash(authority),
678
+ operationId: managedAuthDerivedUuid(
679
+ "opengeni:managed-auth:social-completion",
680
+ `${attempt.transactionId}:${attempt.provider}`,
681
+ ),
682
+ requestDigest: managedAuthSecretRequestDigest(deps.settings.betterAuthSecret!, {
683
+ operation: "social_completion",
684
+ transactionId: attempt.transactionId,
685
+ provider: attempt.provider,
686
+ expectedGeneration: attempt.expectedGeneration,
687
+ expectedActorEpoch: attempt.expectedActorEpoch,
688
+ }),
689
+ expectedGeneration: attempt.expectedGeneration,
690
+ expectedActorEpoch: attempt.expectedActorEpoch,
691
+ transactionId: attempt.transactionId,
692
+ transactionSecretHash: attempt.transactionSecretHash,
693
+ authSessionId: handled.authSessionId,
694
+ mode: deps.settings.managedAuthSessionSetMode,
695
+ });
696
+ } catch {
697
+ // adoptManagedAuthSession revokes only when durable reconciliation
698
+ // proves that completion did not commit. An uncertain outcome may
699
+ // already point the selected slot at this session, so the callback
700
+ // must not independently revoke it.
701
+ const location = new URL("/account-auth", deps.settings.publicBaseUrl!);
702
+ location.searchParams.set("transaction", attempt.transactionId);
703
+ location.searchParams.set("social", "error");
704
+ providerResponse = Response.redirect(location, 302);
705
+ }
706
+ }
707
+ } else {
708
+ const headers = new Headers(c.req.raw.headers);
709
+ headers.delete("cookie");
710
+ headers.delete("authorization");
711
+ headers.delete("x-forwarded-user");
712
+ const providerRequest = new Request(c.req.raw, { headers });
713
+ const discardProviderSession =
714
+ deps.settings.managedAuthSessionSetMode === "broker" || authority !== undefined;
715
+ providerResponse = discardProviderSession
716
+ ? await runManagedAuthDiscardedProviderSession(
717
+ async () => await managedAuth.handler(providerRequest),
718
+ )
719
+ : await managedAuth.handler(providerRequest);
720
+ }
602
721
  let replacementCookies: readonly string[] | undefined;
603
722
  if (deps.settings.managedAuthSessionSetMode === "broker") {
604
723
  replacementCookies = await managedAuthSessionAdapter!.createLegacySelectedSessionCookies(
@@ -628,19 +747,24 @@ export function createAppComposition(deps: AppDependencies): {
628
747
  }
629
748
  }
630
749
  }
631
- return await scrubManagedAuthProviderResponse(providerResponse, { replacementCookies });
750
+ return await scrubManagedAuthProviderResponse(providerResponse, {
751
+ replacementCookies,
752
+ preserveCookieNames,
753
+ });
632
754
  });
633
755
  }
634
756
 
635
- app.get("/healthz", (c) =>
636
- c.json({
757
+ app.get("/healthz", (c) => {
758
+ const warnings = githubAppBotIdentityWarnings(deps.settings);
759
+ return c.json({
637
760
  service: deps.settings.serviceName,
638
761
  environment: deps.settings.environment,
639
762
  deploymentRevision: deps.settings.deploymentRevision,
640
763
  ...(deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {}),
764
+ ...(warnings.length > 0 ? { warnings } : {}),
641
765
  ok: true,
642
- }),
643
- );
766
+ });
767
+ });
644
768
 
645
769
  app.get("/readyz", async (c) => {
646
770
  const result = await runReadinessChecks(readinessChecks(deps), 2_000);
@@ -661,10 +785,12 @@ export function createAppComposition(deps: AppDependencies): {
661
785
 
662
786
  app.get("/v1/config/client", async (c) => {
663
787
  c.header("cache-control", "no-store");
664
- const codexCatalogSettings = deps.settings.codexSubscriptionEnabled
665
- ? withCodexCatalogProvider(deps.settings)
666
- : deps.settings;
667
- const catalogSettings = deps.settings.supergrokSubscriptionEnabled
788
+ const resolvedCatalog = await resolveCatalogSettings(deps.db, deps.settings);
789
+ const baseCatalogSettings = resolvedCatalog.settings;
790
+ const codexCatalogSettings = baseCatalogSettings.codexSubscriptionEnabled
791
+ ? withCodexCatalogProvider(baseCatalogSettings)
792
+ : baseCatalogSettings;
793
+ const catalogSettings = baseCatalogSettings.supergrokSubscriptionEnabled
668
794
  ? withXaiSubscriptionCatalogProvider(codexCatalogSettings)
669
795
  : codexCatalogSettings;
670
796
  return c.json(
@@ -681,6 +807,7 @@ export function createAppComposition(deps: AppDependencies): {
681
807
  models: configuredModels(catalogSettings).map(projectClientModel),
682
808
  defaultReasoningEffort: deps.settings.openaiReasoningEffort,
683
809
  allowedReasoningEfforts: configuredAllowedReasoningEfforts(deps.settings),
810
+ defaultSandboxBackend: deps.settings.sandboxBackend,
684
811
  mcpServers: deps.settings.mcpServers.map((server) => ({
685
812
  id: server.id,
686
813
  name: server.name ?? server.id,
@@ -783,7 +910,8 @@ export function createAppComposition(deps: AppDependencies): {
783
910
  const transport = new WebStandardStreamableHTTPServerTransport({
784
911
  enableJsonResponse: true,
785
912
  });
786
- const mcp = buildOpenGeniMcpServer(routeDeps, grant, {
913
+ const mcpDeps = await resolveWorkspaceMcpRouteDeps(routeDeps, grant);
914
+ const mcp = buildOpenGeniMcpServer(mcpDeps, grant, {
787
915
  requestOrigin: new URL(c.req.url).origin,
788
916
  workspaceMemoryEnabled,
789
917
  workspaceMemoryPromptMode,
@@ -956,6 +1084,11 @@ export function createAppComposition(deps: AppDependencies): {
956
1084
  return { app, routeDeps };
957
1085
  }
958
1086
 
1087
+ function managedAuthOAuthCallbackProvider(pathname: string): "google" | "github" | null {
1088
+ const match = pathname.match(/^\/v1\/auth\/callback\/(google|github)$/u);
1089
+ return match?.[1] === "google" || match?.[1] === "github" ? match[1] : null;
1090
+ }
1091
+
959
1092
  function mutationOutcomeUnknown(error: unknown, method: string): boolean {
960
1093
  if (method === "GET" || method === "HEAD" || method === "OPTIONS") return false;
961
1094
  const cause = error instanceof HTTPException ? error.cause : error;
@@ -1072,6 +1205,14 @@ function clientAuthConfig(settings: AppDependencies["settings"]) {
1072
1205
  mode: "managedSession" as const,
1073
1206
  session: "cookie" as const,
1074
1207
  emailVerificationRequired: settings.environment !== "local",
1208
+ socialProviders: [
1209
+ ...(settings.managedAuthGoogleClientId && settings.managedAuthGoogleClientSecret
1210
+ ? (["google"] as const)
1211
+ : []),
1212
+ ...(settings.managedAuthGithubClientId && settings.managedAuthGithubClientSecret
1213
+ ? (["github"] as const)
1214
+ : []),
1215
+ ],
1075
1216
  };
1076
1217
  }
1077
1218
  if (settings.productAccessMode === "configured") {
@@ -1260,11 +1401,14 @@ type ReadinessCheckResult = { ok: boolean; error?: string };
1260
1401
  function readinessChecks(deps: AppDependencies): ReadinessChecks {
1261
1402
  const configuredNatsCheck = deps.readinessChecks?.nats;
1262
1403
  return {
1263
- db:
1264
- deps.readinessChecks?.db ??
1265
- (async () => {
1404
+ db: async () => {
1405
+ if (deps.readinessChecks?.db) {
1406
+ await deps.readinessChecks.db();
1407
+ } else {
1266
1408
  await deps.db.execute(dbSql`select 1`);
1267
- }),
1409
+ }
1410
+ await resolveCatalogSettings(deps.db, deps.settings);
1411
+ },
1268
1412
  nats: async () => {
1269
1413
  requireSessionEventDurableFanoutCapability(deps.bus);
1270
1414
  if (configuredNatsCheck) {
@@ -1354,6 +1498,22 @@ const routeLabelPatterns: Array<{
1354
1498
  pattern: /^\/v1\/workspaces\/[^/]+\/codex\/status$/,
1355
1499
  label: "/v1/workspaces/:workspaceId/codex/status",
1356
1500
  },
1501
+ {
1502
+ pattern: /^\/v1\/workspaces\/[^/]+\/codex\/source$/,
1503
+ label: "/v1/workspaces/:workspaceId/codex/source",
1504
+ },
1505
+ {
1506
+ pattern: /^\/v1\/organizations\/[^/]+\/codex\/(accounts|settings)$/,
1507
+ label: (match) => `/v1/organizations/:organizationId/codex/${match[1]}`,
1508
+ },
1509
+ {
1510
+ pattern: /^\/v1\/organizations\/[^/]+\/codex\/connect\/(start|poll)$/,
1511
+ label: (match) => `/v1/organizations/:organizationId/codex/connect/${match[1]}`,
1512
+ },
1513
+ {
1514
+ pattern: /^\/v1\/organizations\/[^/]+\/codex\/accounts\/[^/]+(?:\/activate)?$/,
1515
+ label: "/v1/organizations/:organizationId/codex/accounts/:accountId",
1516
+ },
1357
1517
  {
1358
1518
  pattern: /^\/v1\/workspaces\/[^/]+\/supergrok\/connect\/(start|poll)$/,
1359
1519
  label: (match) => `/v1/workspaces/:workspaceId/supergrok/connect/${match[1]}`,
@@ -1513,6 +1673,11 @@ const routeLabelPatterns: Array<{
1513
1673
  label:
1514
1674
  "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets/:targetId/observation",
1515
1675
  },
1676
+ {
1677
+ pattern: /^\/v1\/workspaces\/[^/]+\/computer-sessions\/[^/]+\/targets\/[^/]+\/screenshot$/,
1678
+ label:
1679
+ "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets/:targetId/screenshot",
1680
+ },
1516
1681
  {
1517
1682
  pattern: /^\/v1\/workspaces\/[^/]+\/computer-sessions\/[^/]+\/operations\/[^/]+$/,
1518
1683
  label:
@@ -1689,6 +1854,18 @@ const routeLabelPatterns: Array<{
1689
1854
  pattern: /^\/v1\/workspaces\/[^/]+\/api-keys\/[^/]+$/,
1690
1855
  label: "/v1/workspaces/:workspaceId/api-keys/:id",
1691
1856
  },
1857
+ {
1858
+ pattern: /^\/v1\/organizations\/[^/]+\/api-keys$/,
1859
+ label: "/v1/organizations/:organizationId/api-keys",
1860
+ },
1861
+ {
1862
+ pattern: /^\/v1\/organizations\/[^/]+\/api-keys\/[^/]+$/,
1863
+ label: "/v1/organizations/:organizationId/api-keys/:id",
1864
+ },
1865
+ {
1866
+ pattern: /^\/v1\/workspaces\/external$/,
1867
+ label: "/v1/workspaces/external",
1868
+ },
1692
1869
  {
1693
1870
  pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks$/,
1694
1871
  label: "/v1/workspaces/:workspaceId/scheduled-tasks",
@@ -1,13 +1,33 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
 
3
3
  type ManagedAuthAttemptContext =
4
- | { kind: "isolated_transaction"; transactionId: string }
4
+ | {
5
+ kind: "isolated_transaction";
6
+ transactionId: string;
7
+ providerId: "credential" | "google" | "github";
8
+ createdAuthSessionId: string | null;
9
+ }
10
+ | { kind: "provider"; providerId: "credential" | "google" | "github" }
5
11
  | { kind: "discard_provider_session" };
6
12
 
7
13
  const managedAuthAttemptStorage = new AsyncLocalStorage<ManagedAuthAttemptContext>();
8
14
 
9
- export function runManagedAuthAttempt<T>(transactionId: string, action: () => T): T {
10
- return managedAuthAttemptStorage.run({ kind: "isolated_transaction", transactionId }, action);
15
+ export function runManagedAuthAttempt<T>(
16
+ transactionId: string,
17
+ providerId: "credential" | "google" | "github",
18
+ action: () => T,
19
+ ): T {
20
+ return managedAuthAttemptStorage.run(
21
+ { kind: "isolated_transaction", transactionId, providerId, createdAuthSessionId: null },
22
+ action,
23
+ );
24
+ }
25
+
26
+ export function runManagedAuthProvider<T>(
27
+ providerId: "credential" | "google" | "github",
28
+ action: () => T,
29
+ ): T {
30
+ return managedAuthAttemptStorage.run({ kind: "provider", providerId }, action);
11
31
  }
12
32
 
13
33
  export function runManagedAuthDiscardedProviderSession<T>(action: () => T): T {
@@ -19,6 +39,23 @@ export function currentManagedAuthAttemptId(): string | null {
19
39
  return context?.kind === "isolated_transaction" ? context.transactionId : null;
20
40
  }
21
41
 
42
+ export function currentManagedAuthProviderId(): "credential" | "google" | "github" {
43
+ const context = managedAuthAttemptStorage.getStore();
44
+ return context?.kind === "isolated_transaction" || context?.kind === "provider"
45
+ ? context.providerId
46
+ : "credential";
47
+ }
48
+
49
+ export function recordCurrentManagedAuthSession(authSessionId: string): void {
50
+ const context = managedAuthAttemptStorage.getStore();
51
+ if (context?.kind === "isolated_transaction") context.createdAuthSessionId = authSessionId;
52
+ }
53
+
54
+ export function currentManagedAuthCreatedSessionId(): string | null {
55
+ const context = managedAuthAttemptStorage.getStore();
56
+ return context?.kind === "isolated_transaction" ? context.createdAuthSessionId : null;
57
+ }
58
+
22
59
  export function shouldDiscardCurrentManagedAuthProviderSession(): boolean {
23
60
  return managedAuthAttemptStorage.getStore()?.kind === "discard_provider_session";
24
61
  }
@@ -19,6 +19,7 @@ export function createBetterAuthSessionAdapter(
19
19
  }
20
20
  const result = await runManagedAuthAttempt(
21
21
  input.transactionId,
22
+ "credential",
22
23
  async () =>
23
24
  await auth.api.signInEmail({
24
25
  body: {
@@ -1,4 +1,4 @@
1
- import type { Settings } from "@opengeni/config";
1
+ import { canonicalPublicOrigin, type Settings } from "@opengeni/config";
2
2
  import {
3
3
  type ManagedAuth,
4
4
  type ManagedEmailMessage,
@@ -20,7 +20,9 @@ import { Pool } from "pg";
20
20
 
21
21
  import { decideCanonicalHumanSessionAdmission } from "./canonical-human-session-admission";
22
22
  import {
23
+ currentManagedAuthProviderId,
23
24
  currentManagedAuthAttemptId,
25
+ recordCurrentManagedAuthSession,
24
26
  shouldDiscardCurrentManagedAuthProviderSession,
25
27
  } from "./managed-auth-attempt-context";
26
28
 
@@ -44,6 +46,21 @@ export function managedAuthUserCreateOverride(
44
46
  return { data: { ...user, emailVerified: true } };
45
47
  }
46
48
 
49
+ export function managedAuthUserCreateAdmission(
50
+ settings: Pick<Settings, "environment">,
51
+ user: { emailVerified: boolean } & Record<string, unknown>,
52
+ providerId: string,
53
+ ): { data: typeof user } | false | undefined {
54
+ if (
55
+ managedAuthRequiresEmailVerification(settings) &&
56
+ providerId !== "credential" &&
57
+ !user.emailVerified
58
+ ) {
59
+ return false;
60
+ }
61
+ return managedAuthUserCreateOverride(settings, user);
62
+ }
63
+
47
64
  /** Keep Better Auth password policy and storage format behind this boundary. */
48
65
  export async function hashManagedAuthPassword(password: string): Promise<string> {
49
66
  return await hashPassword(password);
@@ -165,6 +182,27 @@ export function createManagedAuth(
165
182
  accountLinking: {
166
183
  enabled: false,
167
184
  },
185
+ encryptOAuthTokens: true,
186
+ storeStateStrategy: "database",
187
+ },
188
+ socialProviders: {
189
+ ...(settings.managedAuthGoogleClientId && settings.managedAuthGoogleClientSecret
190
+ ? {
191
+ google: {
192
+ clientId: settings.managedAuthGoogleClientId,
193
+ clientSecret: settings.managedAuthGoogleClientSecret,
194
+ prompt: "select_account" as const,
195
+ },
196
+ }
197
+ : {}),
198
+ ...(settings.managedAuthGithubClientId && settings.managedAuthGithubClientSecret
199
+ ? {
200
+ github: {
201
+ clientId: settings.managedAuthGithubClientId,
202
+ clientSecret: settings.managedAuthGithubClientSecret,
203
+ },
204
+ }
205
+ : {}),
168
206
  },
169
207
  verification: {
170
208
  modelName: "auth_verifications",
@@ -229,6 +267,7 @@ export function createManagedAuth(
229
267
  session: {
230
268
  create: {
231
269
  before: async (session) => {
270
+ const providerId = currentManagedAuthProviderId();
232
271
  await ensureCanonicalHumanIdentityForAuthUser(db, session.userId);
233
272
  const preflightProjection = await getCanonicalHumanIdentityProjection(
234
273
  db,
@@ -242,7 +281,7 @@ export function createManagedAuth(
242
281
  if (!preflight.allowed) {
243
282
  const exactRecoveryBinding = await getCanonicalHumanExactLoginBindingForAuthUser(db, {
244
283
  authUserId: session.userId,
245
- providerId: "credential",
284
+ providerId,
246
285
  });
247
286
  const recoveryBinding = preflightProjection.loginBindings.find(
248
287
  (binding) =>
@@ -282,7 +321,7 @@ export function createManagedAuth(
282
321
  const projection = await getCanonicalHumanIdentityProjection(db, session.userId);
283
322
  const exactBinding = await getCanonicalHumanExactLoginBindingForAuthUser(db, {
284
323
  authUserId: session.userId,
285
- providerId: "credential",
324
+ providerId,
286
325
  });
287
326
  const activeBinding = projection.loginBindings.find(
288
327
  (binding) => binding.id === exactBinding.id,
@@ -318,6 +357,7 @@ export function createManagedAuth(
318
357
  };
319
358
  },
320
359
  after: async (session) => {
360
+ recordCurrentManagedAuthSession(session.id);
321
361
  if (!shouldDiscardCurrentManagedAuthProviderSession()) return;
322
362
  await db.execute(sql`delete from auth_sessions where id = ${session.id}`);
323
363
  },
@@ -325,7 +365,8 @@ export function createManagedAuth(
325
365
  },
326
366
  user: {
327
367
  create: {
328
- before: async (user) => managedAuthUserCreateOverride(settings, user),
368
+ before: async (user) =>
369
+ managedAuthUserCreateAdmission(settings, user, currentManagedAuthProviderId()),
329
370
  after: async (user) => {
330
371
  if (!user.emailVerified) return;
331
372
  await ensureManagedAccessForUser(db, {
@@ -342,6 +383,125 @@ export function createManagedAuth(
342
383
  }) as ManagedAuth;
343
384
  }
344
385
 
386
+ export type ManagedAuthOAuthAttempt = {
387
+ transactionId: string;
388
+ provider: "google" | "github";
389
+ authorityHash: string;
390
+ transactionSecretHash: string;
391
+ expectedGeneration: string;
392
+ expectedActorEpoch: string;
393
+ };
394
+
395
+ /**
396
+ * Resolve OpenGeni's server-only login transaction proof from Better Auth's
397
+ * database-backed OAuth state before the provider callback consumes it.
398
+ */
399
+ export async function resolveManagedAuthOAuthAttempt(
400
+ auth: ManagedAuth,
401
+ request: Request,
402
+ provider: "google" | "github",
403
+ publicBaseUrl: string,
404
+ ): Promise<ManagedAuthOAuthAttempt | null> {
405
+ const expectedOrigin = canonicalPublicOrigin(publicBaseUrl);
406
+ if (!expectedOrigin) return null;
407
+ const state = new URL(request.url).searchParams.get("state");
408
+ if (!state) return null;
409
+ const verification = await (await auth.$context).internalAdapter.findVerificationValue(state);
410
+ if (!verification?.value) return null;
411
+ let parsed: unknown;
412
+ try {
413
+ parsed = JSON.parse(verification.value);
414
+ } catch {
415
+ return null;
416
+ }
417
+ if (!parsed || typeof parsed !== "object") return null;
418
+ const stateData = parsed as Record<string, unknown>;
419
+ const proof = stateData.opengeniManagedAuth;
420
+ if (!proof || typeof proof !== "object") return null;
421
+ const value = proof as Record<string, unknown>;
422
+ if (
423
+ value.version !== 1 ||
424
+ value.provider !== provider ||
425
+ typeof value.transactionId !== "string" ||
426
+ typeof value.authorityHash !== "string" ||
427
+ typeof value.transactionSecretHash !== "string" ||
428
+ typeof value.expectedGeneration !== "string" ||
429
+ typeof value.expectedActorEpoch !== "string" ||
430
+ !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(
431
+ value.transactionId,
432
+ ) ||
433
+ !/^[0-9a-f]{64}$/u.test(value.authorityHash) ||
434
+ !/^[0-9a-f]{64}$/u.test(value.transactionSecretHash) ||
435
+ !/^[1-9][0-9]*$/u.test(value.expectedGeneration) ||
436
+ !/^[1-9][0-9]*$/u.test(value.expectedActorEpoch)
437
+ ) {
438
+ return null;
439
+ }
440
+ const callbackURL = typeof stateData.callbackURL === "string" ? stateData.callbackURL : null;
441
+ const errorURL = typeof stateData.errorURL === "string" ? stateData.errorURL : null;
442
+ if (
443
+ !callbackURL ||
444
+ !errorURL ||
445
+ !managedAuthOAuthReturnMatches(callbackURL, expectedOrigin, value.transactionId, "complete") ||
446
+ !managedAuthOAuthReturnMatches(errorURL, expectedOrigin, value.transactionId, "error")
447
+ ) {
448
+ return null;
449
+ }
450
+ return {
451
+ transactionId: value.transactionId,
452
+ provider,
453
+ authorityHash: value.authorityHash,
454
+ transactionSecretHash: value.transactionSecretHash,
455
+ expectedGeneration: value.expectedGeneration,
456
+ expectedActorEpoch: value.expectedActorEpoch,
457
+ };
458
+ }
459
+
460
+ export async function isolatedManagedAuthOAuthCallbackRequest(
461
+ auth: ManagedAuth,
462
+ request: Request,
463
+ ): Promise<{ request: Request; stateCookieName: string }> {
464
+ const context = await auth.$context;
465
+ const stateCookieName = context.createAuthCookie("state").name;
466
+ const headers = new Headers(request.headers);
467
+ const stateCookie = cookiePair(headers.get("cookie"), stateCookieName);
468
+ if (stateCookie) headers.set("cookie", stateCookie);
469
+ else headers.delete("cookie");
470
+ headers.delete("authorization");
471
+ headers.delete("x-forwarded-user");
472
+ return { request: new Request(request, { headers }), stateCookieName };
473
+ }
474
+
475
+ export function managedAuthOAuthReturnMatches(
476
+ raw: string,
477
+ expectedOrigin: string,
478
+ transactionId: string,
479
+ outcome: "complete" | "error",
480
+ ): boolean {
481
+ try {
482
+ const url = new URL(raw);
483
+ return (
484
+ url.origin === expectedOrigin &&
485
+ url.pathname === "/account-auth" &&
486
+ url.searchParams.size === 2 &&
487
+ url.searchParams.get("transaction") === transactionId &&
488
+ url.searchParams.get("social") === outcome &&
489
+ !url.hash
490
+ );
491
+ } catch {
492
+ return false;
493
+ }
494
+ }
495
+
496
+ function cookiePair(header: string | null, name: string): string | null {
497
+ for (const part of header?.split(";") ?? []) {
498
+ const separator = part.indexOf("=");
499
+ if (separator < 0 || part.slice(0, separator).trim() !== name) continue;
500
+ return `${name}=${part.slice(separator + 1).trim()}`;
501
+ }
502
+ return null;
503
+ }
504
+
345
505
  function betterAuthBaseUrl(settings: Settings) {
346
506
  const allowedHosts = splitCsv(settings.betterAuthAllowedHosts);
347
507
  if (allowedHosts.length === 0) {