@opengeni/api-router 2.4.2-canary.0 → 2.6.3-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 (63) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/auth/managed-auth-attempt-context.d.ts +5 -1
  3. package/dist/auth/managed-auth.d.ts +24 -1
  4. package/dist/{chunk-L7GVVSSQ.js → chunk-XTLI3CBH.js} +2884 -483
  5. package/dist/chunk-XTLI3CBH.js.map +1 -0
  6. package/dist/http/sse.d.ts +9 -0
  7. package/dist/index.js +104 -4
  8. package/dist/index.js.map +1 -1
  9. package/dist/integrations/slack-interactions.d.ts +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/server.d.ts +34 -0
  15. package/dist/mcp/session-view.d.ts +1 -0
  16. package/dist/mcp/session-wait.d.ts +19 -0
  17. package/dist/routes/api-keys.d.ts +2 -0
  18. package/dist/routes/browser-sessions.d.ts +1 -0
  19. package/dist/routes/computer-sessions.d.ts +12 -0
  20. package/dist/routes/managed-auth-session-sets.d.ts +7 -0
  21. package/dist/routes/sessions.d.ts +17 -5
  22. package/dist/routes/workspaces.d.ts +1 -1
  23. package/dist/sandbox/metrics-ingestion.d.ts +16 -0
  24. package/dist/work-discovery-observability.d.ts +33 -0
  25. package/dist/workspace-delete-observability.d.ts +10 -0
  26. package/package.json +19 -19
  27. package/src/app.ts +172 -20
  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/integrations/oauth-client.ts +8 -1
  33. package/src/integrations/provider-oauth.ts +12 -2
  34. package/src/interaction-metrics.ts +30 -0
  35. package/src/mcp/company-brain-governed-writes.ts +38 -23
  36. package/src/mcp/company-profile-agent-admin.ts +7 -7
  37. package/src/mcp/remember.ts +19 -8
  38. package/src/mcp/server.ts +621 -54
  39. package/src/mcp/session-wait.ts +56 -8
  40. package/src/routes/api-integrations.ts +2 -2
  41. package/src/routes/api-keys.ts +149 -7
  42. package/src/routes/browser-sessions.ts +10 -2
  43. package/src/routes/capabilities.ts +3 -3
  44. package/src/routes/codex.ts +483 -74
  45. package/src/routes/company-profile.ts +64 -0
  46. package/src/routes/computer-sessions.ts +103 -1
  47. package/src/routes/integration-facets.ts +8 -5
  48. package/src/routes/interaction-resources.ts +7 -1
  49. package/src/routes/managed-auth-session-sets.ts +199 -2
  50. package/src/routes/organization-memberships.ts +28 -8
  51. package/src/routes/packs.ts +5 -5
  52. package/src/routes/plugins.ts +2 -2
  53. package/src/routes/scheduled-tasks.ts +9 -0
  54. package/src/routes/sessions.ts +380 -87
  55. package/src/routes/skills.ts +3 -3
  56. package/src/routes/workspaces.ts +293 -54
  57. package/src/sandbox/channel-a.ts +10 -4
  58. package/src/sandbox/machines.ts +13 -6
  59. package/src/sandbox/metrics-ingestion.ts +157 -3
  60. package/src/sandbox/viewer.ts +20 -1
  61. package/src/work-discovery-observability.ts +121 -0
  62. package/src/workspace-delete-observability.ts +75 -0
  63. package/dist/chunk-L7GVVSSQ.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";
@@ -70,14 +71,28 @@ import {
70
71
  SessionAuthorizationDeniedError,
71
72
  SessionAuthorizationUnavailableError,
72
73
  } from "@opengeni/core";
73
- import { createManagedAuth } from "./auth/managed-auth";
74
74
  import {
75
+ createManagedAuth,
76
+ isolatedManagedAuthOAuthCallbackRequest,
77
+ managedAuthOAuthReturnMatches,
78
+ resolveManagedAuthOAuthAttempt,
79
+ } from "./auth/managed-auth";
80
+ import {
81
+ adoptManagedAuthSession,
75
82
  MANAGED_AUTH_SESSION_SET_COOKIE,
76
83
  ManagedAuthActorChangeError,
84
+ managedAuthCsrfHash,
85
+ managedAuthDerivedUuid,
86
+ managedAuthSecretRequestDigest,
77
87
  managedAuthSha256,
78
88
  } from "@opengeni/core/managed-auth-session-sets";
79
89
  import { createBetterAuthSessionAdapter } from "./auth/managed-auth-session-adapter";
80
- import { runManagedAuthDiscardedProviderSession } from "./auth/managed-auth-attempt-context";
90
+ import {
91
+ currentManagedAuthCreatedSessionId,
92
+ runManagedAuthAttempt,
93
+ runManagedAuthDiscardedProviderSession,
94
+ runManagedAuthProvider,
95
+ } from "./auth/managed-auth-attempt-context";
81
96
  import { createManagedEmailTransport } from "./auth/managed-email";
82
97
  import { assertManagedEmailTransportMetadata } from "./auth/organization-user-setup";
83
98
  import { createApiSandboxClient, makeResumeBoxById } from "./sandbox/access";
@@ -579,26 +594,111 @@ export function createAppComposition(deps: AppDependencies): {
579
594
  registerManagedAuthSessionSetRoutes(app, routeDeps);
580
595
  if (managedAuth) {
581
596
  app.on(["GET", "POST"], "/v1/auth/*", async (c) => {
597
+ const pathname = new URL(c.req.url).pathname;
598
+ const oauthCallbackProvider = managedAuthOAuthCallbackProvider(pathname);
582
599
  if (deps.settings.managedAuthSessionSetMode === "legacy") {
583
- return await managedAuth.handler(c.req.raw);
600
+ return oauthCallbackProvider
601
+ ? await runManagedAuthProvider(
602
+ oauthCallbackProvider,
603
+ async () => await managedAuth.handler(c.req.raw),
604
+ )
605
+ : await managedAuth.handler(c.req.raw);
584
606
  }
585
- requireManagedAuthProviderRouteAllowed(c.req.method, new URL(c.req.url).pathname);
607
+ requireManagedAuthProviderRouteAllowed(c.req.method, pathname);
586
608
  // Provider authentication/recovery is isolated from whichever actor the
587
609
  // browser currently renders. Selected-session capabilities are all
588
610
  // 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
611
  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),
612
+ let providerResponse: Response;
613
+ let preserveCookieNames: readonly string[] | undefined;
614
+ if (oauthCallbackProvider) {
615
+ const attempt = await resolveManagedAuthOAuthAttempt(
616
+ managedAuth,
617
+ c.req.raw,
618
+ oauthCallbackProvider,
619
+ deps.settings.publicBaseUrl!,
620
+ );
621
+ if (!attempt || !authority || attempt.authorityHash !== managedAuthSha256(authority)) {
622
+ throw new HTTPException(409, { message: "provider_route_blocked" });
623
+ }
624
+ const isolated = await isolatedManagedAuthOAuthCallbackRequest(managedAuth, c.req.raw);
625
+ preserveCookieNames = [isolated.stateCookieName];
626
+ const handled = await runManagedAuthAttempt(
627
+ attempt.transactionId,
628
+ oauthCallbackProvider,
629
+ async () => {
630
+ const response = await managedAuth.handler(isolated.request);
631
+ return {
632
+ response,
633
+ authSessionId: currentManagedAuthCreatedSessionId(),
634
+ };
635
+ },
636
+ );
637
+ providerResponse = handled.response;
638
+ if (
639
+ !handled.authSessionId &&
640
+ managedAuthOAuthReturnMatches(
641
+ providerResponse.headers.get("location") ?? "",
642
+ new URL(deps.settings.publicBaseUrl!).origin,
643
+ attempt.transactionId,
644
+ "complete",
600
645
  )
601
- : await managedAuth.handler(providerRequest);
646
+ ) {
647
+ const location = new URL("/account-auth", deps.settings.publicBaseUrl!);
648
+ location.searchParams.set("transaction", attempt.transactionId);
649
+ location.searchParams.set("social", "error");
650
+ providerResponse = Response.redirect(location, 302);
651
+ } else if (handled.authSessionId) {
652
+ try {
653
+ await adoptManagedAuthSession({
654
+ db: deps.db,
655
+ adapter: managedAuthSessionAdapter!,
656
+ authority,
657
+ authorityHash: attempt.authorityHash,
658
+ csrfHash: managedAuthCsrfHash(authority),
659
+ operationId: managedAuthDerivedUuid(
660
+ "opengeni:managed-auth:social-completion",
661
+ `${attempt.transactionId}:${attempt.provider}`,
662
+ ),
663
+ requestDigest: managedAuthSecretRequestDigest(deps.settings.betterAuthSecret!, {
664
+ operation: "social_completion",
665
+ transactionId: attempt.transactionId,
666
+ provider: attempt.provider,
667
+ expectedGeneration: attempt.expectedGeneration,
668
+ expectedActorEpoch: attempt.expectedActorEpoch,
669
+ }),
670
+ expectedGeneration: attempt.expectedGeneration,
671
+ expectedActorEpoch: attempt.expectedActorEpoch,
672
+ transactionId: attempt.transactionId,
673
+ transactionSecretHash: attempt.transactionSecretHash,
674
+ authSessionId: handled.authSessionId,
675
+ mode: deps.settings.managedAuthSessionSetMode,
676
+ });
677
+ } catch {
678
+ // adoptManagedAuthSession revokes only when durable reconciliation
679
+ // proves that completion did not commit. An uncertain outcome may
680
+ // already point the selected slot at this session, so the callback
681
+ // must not independently revoke it.
682
+ const location = new URL("/account-auth", deps.settings.publicBaseUrl!);
683
+ location.searchParams.set("transaction", attempt.transactionId);
684
+ location.searchParams.set("social", "error");
685
+ providerResponse = Response.redirect(location, 302);
686
+ }
687
+ }
688
+ } else {
689
+ const headers = new Headers(c.req.raw.headers);
690
+ headers.delete("cookie");
691
+ headers.delete("authorization");
692
+ headers.delete("x-forwarded-user");
693
+ const providerRequest = new Request(c.req.raw, { headers });
694
+ const discardProviderSession =
695
+ deps.settings.managedAuthSessionSetMode === "broker" || authority !== undefined;
696
+ providerResponse = discardProviderSession
697
+ ? await runManagedAuthDiscardedProviderSession(
698
+ async () => await managedAuth.handler(providerRequest),
699
+ )
700
+ : await managedAuth.handler(providerRequest);
701
+ }
602
702
  let replacementCookies: readonly string[] | undefined;
603
703
  if (deps.settings.managedAuthSessionSetMode === "broker") {
604
704
  replacementCookies = await managedAuthSessionAdapter!.createLegacySelectedSessionCookies(
@@ -628,19 +728,24 @@ export function createAppComposition(deps: AppDependencies): {
628
728
  }
629
729
  }
630
730
  }
631
- return await scrubManagedAuthProviderResponse(providerResponse, { replacementCookies });
731
+ return await scrubManagedAuthProviderResponse(providerResponse, {
732
+ replacementCookies,
733
+ preserveCookieNames,
734
+ });
632
735
  });
633
736
  }
634
737
 
635
- app.get("/healthz", (c) =>
636
- c.json({
738
+ app.get("/healthz", (c) => {
739
+ const warnings = githubAppBotIdentityWarnings(deps.settings);
740
+ return c.json({
637
741
  service: deps.settings.serviceName,
638
742
  environment: deps.settings.environment,
639
743
  deploymentRevision: deps.settings.deploymentRevision,
640
744
  ...(deps.settings.serverVersion ? { serverVersion: deps.settings.serverVersion } : {}),
745
+ ...(warnings.length > 0 ? { warnings } : {}),
641
746
  ok: true,
642
- }),
643
- );
747
+ });
748
+ });
644
749
 
645
750
  app.get("/readyz", async (c) => {
646
751
  const result = await runReadinessChecks(readinessChecks(deps), 2_000);
@@ -681,6 +786,7 @@ export function createAppComposition(deps: AppDependencies): {
681
786
  models: configuredModels(catalogSettings).map(projectClientModel),
682
787
  defaultReasoningEffort: deps.settings.openaiReasoningEffort,
683
788
  allowedReasoningEfforts: configuredAllowedReasoningEfforts(deps.settings),
789
+ defaultSandboxBackend: deps.settings.sandboxBackend,
684
790
  mcpServers: deps.settings.mcpServers.map((server) => ({
685
791
  id: server.id,
686
792
  name: server.name ?? server.id,
@@ -956,6 +1062,11 @@ export function createAppComposition(deps: AppDependencies): {
956
1062
  return { app, routeDeps };
957
1063
  }
958
1064
 
1065
+ function managedAuthOAuthCallbackProvider(pathname: string): "google" | "github" | null {
1066
+ const match = pathname.match(/^\/v1\/auth\/callback\/(google|github)$/u);
1067
+ return match?.[1] === "google" || match?.[1] === "github" ? match[1] : null;
1068
+ }
1069
+
959
1070
  function mutationOutcomeUnknown(error: unknown, method: string): boolean {
960
1071
  if (method === "GET" || method === "HEAD" || method === "OPTIONS") return false;
961
1072
  const cause = error instanceof HTTPException ? error.cause : error;
@@ -1072,6 +1183,14 @@ function clientAuthConfig(settings: AppDependencies["settings"]) {
1072
1183
  mode: "managedSession" as const,
1073
1184
  session: "cookie" as const,
1074
1185
  emailVerificationRequired: settings.environment !== "local",
1186
+ socialProviders: [
1187
+ ...(settings.managedAuthGoogleClientId && settings.managedAuthGoogleClientSecret
1188
+ ? (["google"] as const)
1189
+ : []),
1190
+ ...(settings.managedAuthGithubClientId && settings.managedAuthGithubClientSecret
1191
+ ? (["github"] as const)
1192
+ : []),
1193
+ ],
1075
1194
  };
1076
1195
  }
1077
1196
  if (settings.productAccessMode === "configured") {
@@ -1354,6 +1473,22 @@ const routeLabelPatterns: Array<{
1354
1473
  pattern: /^\/v1\/workspaces\/[^/]+\/codex\/status$/,
1355
1474
  label: "/v1/workspaces/:workspaceId/codex/status",
1356
1475
  },
1476
+ {
1477
+ pattern: /^\/v1\/workspaces\/[^/]+\/codex\/source$/,
1478
+ label: "/v1/workspaces/:workspaceId/codex/source",
1479
+ },
1480
+ {
1481
+ pattern: /^\/v1\/organizations\/[^/]+\/codex\/(accounts|settings)$/,
1482
+ label: (match) => `/v1/organizations/:organizationId/codex/${match[1]}`,
1483
+ },
1484
+ {
1485
+ pattern: /^\/v1\/organizations\/[^/]+\/codex\/connect\/(start|poll)$/,
1486
+ label: (match) => `/v1/organizations/:organizationId/codex/connect/${match[1]}`,
1487
+ },
1488
+ {
1489
+ pattern: /^\/v1\/organizations\/[^/]+\/codex\/accounts\/[^/]+(?:\/activate)?$/,
1490
+ label: "/v1/organizations/:organizationId/codex/accounts/:accountId",
1491
+ },
1357
1492
  {
1358
1493
  pattern: /^\/v1\/workspaces\/[^/]+\/supergrok\/connect\/(start|poll)$/,
1359
1494
  label: (match) => `/v1/workspaces/:workspaceId/supergrok/connect/${match[1]}`,
@@ -1513,6 +1648,11 @@ const routeLabelPatterns: Array<{
1513
1648
  label:
1514
1649
  "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets/:targetId/observation",
1515
1650
  },
1651
+ {
1652
+ pattern: /^\/v1\/workspaces\/[^/]+\/computer-sessions\/[^/]+\/targets\/[^/]+\/screenshot$/,
1653
+ label:
1654
+ "/v1/workspaces/:workspaceId/computer-sessions/:computerSessionId/targets/:targetId/screenshot",
1655
+ },
1516
1656
  {
1517
1657
  pattern: /^\/v1\/workspaces\/[^/]+\/computer-sessions\/[^/]+\/operations\/[^/]+$/,
1518
1658
  label:
@@ -1689,6 +1829,18 @@ const routeLabelPatterns: Array<{
1689
1829
  pattern: /^\/v1\/workspaces\/[^/]+\/api-keys\/[^/]+$/,
1690
1830
  label: "/v1/workspaces/:workspaceId/api-keys/:id",
1691
1831
  },
1832
+ {
1833
+ pattern: /^\/v1\/organizations\/[^/]+\/api-keys$/,
1834
+ label: "/v1/organizations/:organizationId/api-keys",
1835
+ },
1836
+ {
1837
+ pattern: /^\/v1\/organizations\/[^/]+\/api-keys\/[^/]+$/,
1838
+ label: "/v1/organizations/:organizationId/api-keys/:id",
1839
+ },
1840
+ {
1841
+ pattern: /^\/v1\/workspaces\/external$/,
1842
+ label: "/v1/workspaces/external",
1843
+ },
1692
1844
  {
1693
1845
  pattern: /^\/v1\/workspaces\/[^/]+\/scheduled-tasks$/,
1694
1846
  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) {