@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.
- package/dist/app.js +1 -1
- package/dist/auth/managed-auth-attempt-context.d.ts +5 -1
- package/dist/auth/managed-auth.d.ts +24 -1
- package/dist/{chunk-L7GVVSSQ.js → chunk-XTLI3CBH.js} +2884 -483
- package/dist/chunk-XTLI3CBH.js.map +1 -0
- package/dist/http/sse.d.ts +9 -0
- package/dist/index.js +104 -4
- package/dist/index.js.map +1 -1
- package/dist/integrations/slack-interactions.d.ts +1 -1
- package/dist/interaction-metrics.d.ts +2 -0
- package/dist/mcp/company-brain-governed-writes.d.ts +1 -1
- package/dist/mcp/company-profile-agent-admin.d.ts +6 -6
- package/dist/mcp/remember.d.ts +2 -2
- package/dist/mcp/server.d.ts +34 -0
- package/dist/mcp/session-view.d.ts +1 -0
- package/dist/mcp/session-wait.d.ts +19 -0
- package/dist/routes/api-keys.d.ts +2 -0
- package/dist/routes/browser-sessions.d.ts +1 -0
- package/dist/routes/computer-sessions.d.ts +12 -0
- package/dist/routes/managed-auth-session-sets.d.ts +7 -0
- package/dist/routes/sessions.d.ts +17 -5
- package/dist/routes/workspaces.d.ts +1 -1
- package/dist/sandbox/metrics-ingestion.d.ts +16 -0
- package/dist/work-discovery-observability.d.ts +33 -0
- package/dist/workspace-delete-observability.d.ts +10 -0
- package/package.json +19 -19
- package/src/app.ts +172 -20
- package/src/auth/managed-auth-attempt-context.ts +40 -3
- package/src/auth/managed-auth-session-adapter.ts +1 -0
- package/src/auth/managed-auth.ts +164 -4
- package/src/http/sse.ts +279 -45
- package/src/integrations/oauth-client.ts +8 -1
- package/src/integrations/provider-oauth.ts +12 -2
- package/src/interaction-metrics.ts +30 -0
- package/src/mcp/company-brain-governed-writes.ts +38 -23
- package/src/mcp/company-profile-agent-admin.ts +7 -7
- package/src/mcp/remember.ts +19 -8
- package/src/mcp/server.ts +621 -54
- package/src/mcp/session-wait.ts +56 -8
- package/src/routes/api-integrations.ts +2 -2
- package/src/routes/api-keys.ts +149 -7
- package/src/routes/browser-sessions.ts +10 -2
- package/src/routes/capabilities.ts +3 -3
- package/src/routes/codex.ts +483 -74
- package/src/routes/company-profile.ts +64 -0
- package/src/routes/computer-sessions.ts +103 -1
- package/src/routes/integration-facets.ts +8 -5
- package/src/routes/interaction-resources.ts +7 -1
- package/src/routes/managed-auth-session-sets.ts +199 -2
- package/src/routes/organization-memberships.ts +28 -8
- package/src/routes/packs.ts +5 -5
- package/src/routes/plugins.ts +2 -2
- package/src/routes/scheduled-tasks.ts +9 -0
- package/src/routes/sessions.ts +380 -87
- package/src/routes/skills.ts +3 -3
- package/src/routes/workspaces.ts +293 -54
- package/src/sandbox/channel-a.ts +10 -4
- package/src/sandbox/machines.ts +13 -6
- package/src/sandbox/metrics-ingestion.ts +157 -3
- package/src/sandbox/viewer.ts +20 -1
- package/src/work-discovery-observability.ts +121 -0
- package/src/workspace-delete-observability.ts +75 -0
- 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 {
|
|
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
|
|
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,
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
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, {
|
|
731
|
+
return await scrubManagedAuthProviderResponse(providerResponse, {
|
|
732
|
+
replacementCookies,
|
|
733
|
+
preserveCookieNames,
|
|
734
|
+
});
|
|
632
735
|
});
|
|
633
736
|
}
|
|
634
737
|
|
|
635
|
-
app.get("/healthz", (c) =>
|
|
636
|
-
|
|
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
|
-
| {
|
|
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>(
|
|
10
|
-
|
|
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
|
}
|
package/src/auth/managed-auth.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
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
|
|
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
|
|
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) =>
|
|
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) {
|