@opengeni/api-router 2.3.2-canary.2 → 2.4.2-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 +4 -0
- package/dist/auth/managed-auth-session-adapter.d.ts +4 -0
- package/dist/{chunk-IBV7Z6F4.js → chunk-L7GVVSSQ.js} +2996 -373
- package/dist/chunk-L7GVVSSQ.js.map +1 -0
- package/dist/fatal-process-boundary.d.ts +25 -0
- package/dist/http/sse.d.ts +2 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +168 -6
- package/dist/index.js.map +1 -1
- package/dist/mcp/receipts.d.ts +9 -0
- package/dist/organization-recovery-notifications.d.ts +41 -0
- package/dist/routes/managed-auth-session-sets.d.ts +19 -0
- package/dist/routes/organization-recovery.d.ts +19 -0
- package/dist/routes/workspaces.d.ts +1 -0
- package/package.json +18 -18
- package/src/app.ts +133 -1
- package/src/auth/managed-auth-attempt-context.ts +24 -0
- package/src/auth/managed-auth-session-adapter.ts +205 -0
- package/src/auth/managed-auth.ts +52 -2
- package/src/fatal-process-boundary.ts +231 -0
- package/src/http/sse.ts +7 -0
- package/src/index.ts +25 -5
- package/src/integrations/slack-interactions.ts +30 -17
- package/src/mcp/receipts.ts +34 -0
- package/src/mcp/server.ts +45 -39
- package/src/organization-recovery-notifications.ts +103 -0
- package/src/routes/canonical-human-identities.ts +29 -14
- package/src/routes/codex.ts +5 -1
- package/src/routes/environments.ts +23 -0
- package/src/routes/interaction-resources.ts +3 -0
- package/src/routes/managed-auth-session-sets.ts +994 -0
- package/src/routes/managed-onboarding.ts +2 -0
- package/src/routes/organization-memberships.ts +2 -0
- package/src/routes/organization-recovery.ts +325 -0
- package/src/routes/sessions.ts +24 -39
- package/src/routes/supergrok.ts +5 -1
- package/src/routes/workspaces.ts +23 -1
- package/dist/chunk-IBV7Z6F4.js.map +0 -1
package/dist/mcp/receipts.d.ts
CHANGED
|
@@ -9,6 +9,15 @@ export type McpMutationReceiptInput = Omit<McpMutationReceiptType, "receiptVersi
|
|
|
9
9
|
* accidentally adding an unbounded entity or a copy of request fields.
|
|
10
10
|
*/
|
|
11
11
|
export declare function mcpMutationReceipt(input: McpMutationReceiptInput): McpMutationReceiptType;
|
|
12
|
+
export declare function sessionControlMutationReceipt(input: {
|
|
13
|
+
operation: "session_pause" | "session_resume";
|
|
14
|
+
sessionId: string;
|
|
15
|
+
state: string;
|
|
16
|
+
receiptId: string;
|
|
17
|
+
timestamp: string;
|
|
18
|
+
outcome: "changed" | "unchanged" | "replayed";
|
|
19
|
+
interruptionCount: number;
|
|
20
|
+
}): McpMutationReceiptType;
|
|
12
21
|
export type SessionCreateReceiptResult = {
|
|
13
22
|
session: {
|
|
14
23
|
id: string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Database } from "@opengeni/db";
|
|
2
|
+
import { prepareOrganizationRecoveryNotifications, settleOrganizationRecoveryNotification, type OrganizationRecoveryNotificationClaim, type OrganizationRecoveryNotificationSettlement } from "@opengeni/db";
|
|
3
|
+
export type OrganizationRecoveryNotificationDeliveryResult = {
|
|
4
|
+
status: "sent";
|
|
5
|
+
providerMessageId: string | null;
|
|
6
|
+
} | {
|
|
7
|
+
status: "failed";
|
|
8
|
+
errorClass: string;
|
|
9
|
+
} | {
|
|
10
|
+
status: "outcome_unknown";
|
|
11
|
+
};
|
|
12
|
+
export interface OrganizationRecoveryNotificationTransport {
|
|
13
|
+
readonly provider: string;
|
|
14
|
+
send(claim: OrganizationRecoveryNotificationClaim): Promise<OrganizationRecoveryNotificationDeliveryResult>;
|
|
15
|
+
}
|
|
16
|
+
export type OrganizationRecoveryNotificationLifecycle = {
|
|
17
|
+
prepare: typeof prepareOrganizationRecoveryNotifications;
|
|
18
|
+
settle: typeof settleOrganizationRecoveryNotification;
|
|
19
|
+
};
|
|
20
|
+
export declare function dispatchOrganizationRecoveryNotifications(input: {
|
|
21
|
+
db: Database;
|
|
22
|
+
transport: OrganizationRecoveryNotificationTransport;
|
|
23
|
+
claimOwner: string;
|
|
24
|
+
limit?: number;
|
|
25
|
+
leaseSeconds?: number;
|
|
26
|
+
lifecycle?: OrganizationRecoveryNotificationLifecycle;
|
|
27
|
+
}): Promise<OrganizationRecoveryNotificationSettlement[]>;
|
|
28
|
+
export declare class InMemoryOrganizationRecoveryNotificationTransport implements OrganizationRecoveryNotificationTransport {
|
|
29
|
+
readonly provider = "fake";
|
|
30
|
+
readonly attempts: Array<{
|
|
31
|
+
idempotencyKey: string;
|
|
32
|
+
payloadDigest: string;
|
|
33
|
+
recipientCanonicalIdentityId: string;
|
|
34
|
+
providerMessageId: string;
|
|
35
|
+
}>;
|
|
36
|
+
private readonly deliveries;
|
|
37
|
+
private readonly scripted;
|
|
38
|
+
enqueue(...results: OrganizationRecoveryNotificationDeliveryResult[]): void;
|
|
39
|
+
logicalDeliveryCount(): number;
|
|
40
|
+
send(claim: OrganizationRecoveryNotificationClaim): Promise<OrganizationRecoveryNotificationDeliveryResult>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ApiRouteDeps } from "@opengeni/core";
|
|
2
|
+
import type { Hono } from "hono";
|
|
3
|
+
export declare function registerManagedAuthSessionSetRoutes(app: Hono, deps: ApiRouteDeps): void;
|
|
4
|
+
/**
|
|
5
|
+
* Better Auth 1.6.26 provider lifecycle routes retained while session-set mode
|
|
6
|
+
* owns all selected-session reads, enumeration, revocation, and user mutation.
|
|
7
|
+
* The list is intentionally exact so a dependency upgrade cannot silently add
|
|
8
|
+
* a selected-session capability under the wildcard.
|
|
9
|
+
*/
|
|
10
|
+
export declare function requireManagedAuthProviderRouteAllowed(method: string, pathname: string): void;
|
|
11
|
+
/** Remove provider bearer/session material and enforce browser-auth cache/cookie policy. */
|
|
12
|
+
export declare function scrubManagedAuthProviderResponse(response: Response, options?: {
|
|
13
|
+
replacementCookies?: readonly string[] | undefined;
|
|
14
|
+
}): Promise<Response>;
|
|
15
|
+
export declare function parseSupportedDeepLink(path: string): {
|
|
16
|
+
workspaceId: string | null;
|
|
17
|
+
sessionId: string | null;
|
|
18
|
+
permission: "workspace:read" | "sessions:read";
|
|
19
|
+
} | null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getManagedAuthRequestActorAdmissionStamp, getManagedAuthRequestActorLeaseStamp, requireCanonicalHumanRequestIdentity } from "@opengeni/core/canonical-human-identities";
|
|
2
|
+
import type { ApiRouteDeps } from "@opengeni/core";
|
|
3
|
+
import { acceptOrganizationRecoveryCustody, approveOrganizationRecoveryOperation, cancelOrganizationRecoveryOperation, configureOrganizationRecoveryPolicy, disableOrganizationRecoveryPolicy, executeOrganizationRecoveryOperation, getOrganizationRecoveryOverview, startOrganizationRecoveryOperation } from "@opengeni/db";
|
|
4
|
+
import type { Hono } from "hono";
|
|
5
|
+
export type OrganizationRecoveryRouteServices = {
|
|
6
|
+
requireCanonicalHumanRequestIdentity: typeof requireCanonicalHumanRequestIdentity;
|
|
7
|
+
getManagedAuthRequestActorAdmissionStamp: typeof getManagedAuthRequestActorAdmissionStamp;
|
|
8
|
+
getManagedAuthRequestActorLeaseStamp: typeof getManagedAuthRequestActorLeaseStamp;
|
|
9
|
+
getOrganizationRecoveryOverview: typeof getOrganizationRecoveryOverview;
|
|
10
|
+
configureOrganizationRecoveryPolicy: typeof configureOrganizationRecoveryPolicy;
|
|
11
|
+
acceptOrganizationRecoveryCustody: typeof acceptOrganizationRecoveryCustody;
|
|
12
|
+
disableOrganizationRecoveryPolicy: typeof disableOrganizationRecoveryPolicy;
|
|
13
|
+
startOrganizationRecoveryOperation: typeof startOrganizationRecoveryOperation;
|
|
14
|
+
approveOrganizationRecoveryOperation: typeof approveOrganizationRecoveryOperation;
|
|
15
|
+
cancelOrganizationRecoveryOperation: typeof cancelOrganizationRecoveryOperation;
|
|
16
|
+
executeOrganizationRecoveryOperation: typeof executeOrganizationRecoveryOperation;
|
|
17
|
+
};
|
|
18
|
+
export declare function organizationRecoveryHttpError(error: unknown): Error;
|
|
19
|
+
export declare function registerOrganizationRecoveryRoutes(app: Hono, deps: ApiRouteDeps, services?: OrganizationRecoveryRouteServices): void;
|
|
@@ -20,5 +20,6 @@ export declare function workspaceMembersResponse(members: readonly WorkspaceMemb
|
|
|
20
20
|
createdAt: string;
|
|
21
21
|
}[];
|
|
22
22
|
};
|
|
23
|
+
export declare function workspaceUpdateRequestsAccountTransfer(value: unknown): boolean;
|
|
23
24
|
export declare function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void;
|
|
24
25
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/api-router",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.2-canary.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": {
|
|
@@ -43,23 +43,23 @@
|
|
|
43
43
|
"@hono/zod-validator": "^0.7.6",
|
|
44
44
|
"@llamaindex/liteparse": "^1.5.3",
|
|
45
45
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
46
|
-
"@opengeni/agent-proto": "^0.5.1-canary.
|
|
47
|
-
"@opengeni/artifact-tool": "^0.3.
|
|
48
|
-
"@opengeni/capabilities": "^0.3.1-canary.
|
|
49
|
-
"@opengeni/codemode": "^0.4.
|
|
50
|
-
"@opengeni/codex": "^0.2.19-canary.
|
|
51
|
-
"@opengeni/config": "^0.
|
|
52
|
-
"@opengeni/contracts": "^2.
|
|
53
|
-
"@opengeni/core": "^2.
|
|
54
|
-
"@opengeni/db": "^3.
|
|
55
|
-
"@opengeni/documents": "^0.8.
|
|
56
|
-
"@opengeni/events": "^0.4.
|
|
57
|
-
"@opengeni/github": "^0.
|
|
58
|
-
"@opengeni/network": "^0.2.3-canary.
|
|
59
|
-
"@opengeni/observability": "^0.8.
|
|
60
|
-
"@opengeni/runtime": "^1.4.
|
|
61
|
-
"@opengeni/storage": "^0.2.
|
|
62
|
-
"@opengeni/xai-subscription": "^0.1.2-canary.
|
|
46
|
+
"@opengeni/agent-proto": "^0.5.1-canary.5",
|
|
47
|
+
"@opengeni/artifact-tool": "^0.3.8-canary.0",
|
|
48
|
+
"@opengeni/capabilities": "^0.3.1-canary.4",
|
|
49
|
+
"@opengeni/codemode": "^0.4.16-canary.0",
|
|
50
|
+
"@opengeni/codex": "^0.2.19-canary.4",
|
|
51
|
+
"@opengeni/config": "^0.21.0-canary.0",
|
|
52
|
+
"@opengeni/contracts": "^2.6.0-canary.0",
|
|
53
|
+
"@opengeni/core": "^2.5.2-canary.0",
|
|
54
|
+
"@opengeni/db": "^3.5.2-canary.0",
|
|
55
|
+
"@opengeni/documents": "^0.8.5-canary.0",
|
|
56
|
+
"@opengeni/events": "^0.4.3-canary.0",
|
|
57
|
+
"@opengeni/github": "^0.6.0-canary.0",
|
|
58
|
+
"@opengeni/network": "^0.2.3-canary.4",
|
|
59
|
+
"@opengeni/observability": "^0.8.8-canary.0",
|
|
60
|
+
"@opengeni/runtime": "^1.4.1-canary.0",
|
|
61
|
+
"@opengeni/storage": "^0.2.109-canary.0",
|
|
62
|
+
"@opengeni/xai-subscription": "^0.1.2-canary.4",
|
|
63
63
|
"@temporalio/client": "^1.17.0",
|
|
64
64
|
"better-auth": "^1.6.14",
|
|
65
65
|
"hono": "^4.12.18",
|
package/src/app.ts
CHANGED
|
@@ -36,7 +36,10 @@ import {
|
|
|
36
36
|
configureChildLifecycleNotices,
|
|
37
37
|
configureWorkspaceControlRequestLockTimeoutMs,
|
|
38
38
|
dbSql,
|
|
39
|
+
getManagedAuthSessionSetSnapshot,
|
|
39
40
|
getWorkspace,
|
|
41
|
+
reapManagedAuthIsolatedSessions,
|
|
42
|
+
reapExpiredManagedAuthSessionSets,
|
|
40
43
|
rlsContextForWorkspace,
|
|
41
44
|
withSessionRlsActorContext,
|
|
42
45
|
} from "@opengeni/db";
|
|
@@ -49,21 +52,32 @@ import { Hono } from "hono";
|
|
|
49
52
|
import { bodyLimit } from "hono/body-limit";
|
|
50
53
|
import { compress } from "hono/compress";
|
|
51
54
|
import { cors } from "hono/cors";
|
|
55
|
+
import { getCookie } from "hono/cookie";
|
|
52
56
|
import { HTTPException } from "hono/http-exception";
|
|
53
57
|
import { ApiHttpError, workspaceControlBusyHttpError } from "./http/api-error";
|
|
54
58
|
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
|
55
59
|
import type { ApiRouteDeps, AppDependencies } from "@opengeni/core";
|
|
56
60
|
import {
|
|
57
61
|
CodexCompactionV2ProviderLockedError,
|
|
62
|
+
ManagedAuthActorLeaseOutcomeUnknownError,
|
|
58
63
|
hasPermission,
|
|
59
64
|
requireAccessGrant,
|
|
60
65
|
requireLiveAgentAttemptAuthorization,
|
|
61
66
|
requirePermission,
|
|
67
|
+
releaseManagedAuthRequestActorLease,
|
|
68
|
+
validateManagedAuthRequestActorLease,
|
|
62
69
|
requireSessionAuthorization,
|
|
63
70
|
SessionAuthorizationDeniedError,
|
|
64
71
|
SessionAuthorizationUnavailableError,
|
|
65
72
|
} from "@opengeni/core";
|
|
66
73
|
import { createManagedAuth } from "./auth/managed-auth";
|
|
74
|
+
import {
|
|
75
|
+
MANAGED_AUTH_SESSION_SET_COOKIE,
|
|
76
|
+
ManagedAuthActorChangeError,
|
|
77
|
+
managedAuthSha256,
|
|
78
|
+
} from "@opengeni/core/managed-auth-session-sets";
|
|
79
|
+
import { createBetterAuthSessionAdapter } from "./auth/managed-auth-session-adapter";
|
|
80
|
+
import { runManagedAuthDiscardedProviderSession } from "./auth/managed-auth-attempt-context";
|
|
67
81
|
import { createManagedEmailTransport } from "./auth/managed-email";
|
|
68
82
|
import { assertManagedEmailTransportMetadata } from "./auth/organization-user-setup";
|
|
69
83
|
import { createApiSandboxClient, makeResumeBoxById } from "./sandbox/access";
|
|
@@ -135,7 +149,13 @@ import { registerEditableArtifactRoutes } from "./routes/editable-artifacts";
|
|
|
135
149
|
import { registerVideoGenerationRoutes } from "./routes/video-generation";
|
|
136
150
|
import { registerCanonicalHumanIdentityRoutes } from "./routes/canonical-human-identities";
|
|
137
151
|
import { registerOrganizationMembershipRoutes } from "./routes/organization-memberships";
|
|
152
|
+
import { registerOrganizationRecoveryRoutes } from "./routes/organization-recovery";
|
|
138
153
|
import { registerManagedOnboardingRoutes } from "./routes/managed-onboarding";
|
|
154
|
+
import {
|
|
155
|
+
registerManagedAuthSessionSetRoutes,
|
|
156
|
+
requireManagedAuthProviderRouteAllowed,
|
|
157
|
+
scrubManagedAuthProviderResponse,
|
|
158
|
+
} from "./routes/managed-auth-session-sets";
|
|
139
159
|
import { registerUserResourceAuthorityRoutes } from "./routes/user-resource-authorities";
|
|
140
160
|
import { registerConnectionAuthorityRoutes } from "./routes/connection-authorities";
|
|
141
161
|
import { projectClientModel } from "./model-catalog";
|
|
@@ -167,6 +187,8 @@ export { workflowIdForSession } from "@opengeni/core";
|
|
|
167
187
|
export { replaySessionEvents, sseSessionStream, sseWorkspaceControlStream } from "./http/sse";
|
|
168
188
|
|
|
169
189
|
export const API_MAX_REQUEST_BODY_BYTES = 32 * 1024 * 1024;
|
|
190
|
+
const managedAuthReaperDatabases = new WeakSet<object>();
|
|
191
|
+
const MANAGED_AUTH_REAPER_INTERVAL_MS = 60_000;
|
|
170
192
|
|
|
171
193
|
/** Effective Hono bodyLimit — API JSON ceiling or voice multipart + multipart overhead. */
|
|
172
194
|
export function apiRequestBodyLimitBytes(settings: {
|
|
@@ -202,6 +224,9 @@ export function createAppComposition(deps: AppDependencies): {
|
|
|
202
224
|
assertManagedEmailTransportMetadata(managedEmailTransport);
|
|
203
225
|
const managedAuth =
|
|
204
226
|
deps.managedAuth ?? createManagedAuth(deps.settings, deps.db, managedEmailTransport);
|
|
227
|
+
const managedAuthSessionAdapter =
|
|
228
|
+
deps.managedAuthSessionAdapter ??
|
|
229
|
+
(managedAuth ? createBetterAuthSessionAdapter(managedAuth, deps.db) : null);
|
|
205
230
|
const objectStorage =
|
|
206
231
|
deps.objectStorage === undefined ? createObjectStorage(deps.settings) : deps.objectStorage;
|
|
207
232
|
let documentServices: DocumentServices | null = deps.documentServices ?? null;
|
|
@@ -284,6 +309,24 @@ export function createAppComposition(deps: AppDependencies): {
|
|
|
284
309
|
const resumeBoxById = deps.resumeBoxById ?? makeResumeBoxById(sandboxClient);
|
|
285
310
|
const observability =
|
|
286
311
|
deps.observability ?? createObservability(deps.settings, { component: "api" });
|
|
312
|
+
if (
|
|
313
|
+
managedAuth &&
|
|
314
|
+
deps.settings.managedAuthSessionSetMode !== "legacy" &&
|
|
315
|
+
!managedAuthReaperDatabases.has(deps.db as object)
|
|
316
|
+
) {
|
|
317
|
+
managedAuthReaperDatabases.add(deps.db as object);
|
|
318
|
+
const timer = setInterval(() => {
|
|
319
|
+
void Promise.all([
|
|
320
|
+
reapManagedAuthIsolatedSessions(deps.db, 100),
|
|
321
|
+
reapExpiredManagedAuthSessionSets(deps.db, 100),
|
|
322
|
+
]).catch((error) => {
|
|
323
|
+
observability.error("Managed isolated-auth orphan reap failed", {
|
|
324
|
+
errorClass: error instanceof Error ? error.name : "UnknownError",
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
}, MANAGED_AUTH_REAPER_INTERVAL_MS);
|
|
328
|
+
(timer as ReturnType<typeof setInterval> & { unref?: () => void }).unref?.();
|
|
329
|
+
}
|
|
287
330
|
const transcription =
|
|
288
331
|
deps.transcription === undefined
|
|
289
332
|
? createTranscriptionService({
|
|
@@ -304,6 +347,7 @@ export function createAppComposition(deps: AppDependencies): {
|
|
|
304
347
|
githubStateSecret:
|
|
305
348
|
deps.githubStateSecret ?? deps.settings.githubAppManifestStateSecret ?? crypto.randomUUID(),
|
|
306
349
|
managedAuth,
|
|
350
|
+
managedAuthSessionAdapter,
|
|
307
351
|
managedEmailTransport,
|
|
308
352
|
objectStorage,
|
|
309
353
|
documentIndexer,
|
|
@@ -332,13 +376,17 @@ export function createAppComposition(deps: AppDependencies): {
|
|
|
332
376
|
"Range",
|
|
333
377
|
"X-OpenGeni-Access-Key",
|
|
334
378
|
"X-OpenGeni-Api-Contract",
|
|
379
|
+
"X-OpenGeni-Actor-Epoch",
|
|
335
380
|
"X-OpenGeni-Correlation-Id",
|
|
381
|
+
"X-OpenGeni-Session-Csrf",
|
|
336
382
|
"X-OpenGeni-Subject",
|
|
337
383
|
],
|
|
338
384
|
exposeHeaders: [
|
|
339
385
|
"Accept-Ranges",
|
|
340
386
|
"Content-Range",
|
|
341
387
|
"X-OpenGeni-Api-Contract",
|
|
388
|
+
"X-OpenGeni-Actor-Epoch",
|
|
389
|
+
"X-OpenGeni-Actor-State",
|
|
342
390
|
"X-OpenGeni-Correlation-Id",
|
|
343
391
|
],
|
|
344
392
|
};
|
|
@@ -495,11 +543,93 @@ export function createAppComposition(deps: AppDependencies): {
|
|
|
495
543
|
await next();
|
|
496
544
|
});
|
|
497
545
|
|
|
546
|
+
app.use("/v1/*", async (c, next) => {
|
|
547
|
+
try {
|
|
548
|
+
await next();
|
|
549
|
+
try {
|
|
550
|
+
await validateManagedAuthRequestActorLease(c.req.raw);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if (error instanceof ManagedAuthActorChangeError) {
|
|
553
|
+
c.header("x-opengeni-actor-state", "changed");
|
|
554
|
+
throw new HTTPException(409, { message: error.code, cause: error });
|
|
555
|
+
}
|
|
556
|
+
if (error instanceof ManagedAuthActorLeaseOutcomeUnknownError) {
|
|
557
|
+
throw new ApiHttpError(503, {
|
|
558
|
+
code: "upstream_unavailable",
|
|
559
|
+
message: error.code,
|
|
560
|
+
retryable: true,
|
|
561
|
+
outcomeUnknown: true,
|
|
562
|
+
details: { managedAuthCode: error.code },
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
} finally {
|
|
568
|
+
await releaseManagedAuthRequestActorLease(c.req.raw).catch((error) => {
|
|
569
|
+
observability.error("Managed actor mutation lease release failed", {
|
|
570
|
+
errorClass: error instanceof Error ? error.name : "UnknownError",
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
498
576
|
// These product-owned auth routes must be registered before Better Auth's
|
|
499
577
|
// wildcard handler or the provider returns its own 404 first.
|
|
500
578
|
registerManagedOnboardingRoutes(app, routeDeps);
|
|
579
|
+
registerManagedAuthSessionSetRoutes(app, routeDeps);
|
|
501
580
|
if (managedAuth) {
|
|
502
|
-
app.on(["GET", "POST"], "/v1/auth/*", (c) =>
|
|
581
|
+
app.on(["GET", "POST"], "/v1/auth/*", async (c) => {
|
|
582
|
+
if (deps.settings.managedAuthSessionSetMode === "legacy") {
|
|
583
|
+
return await managedAuth.handler(c.req.raw);
|
|
584
|
+
}
|
|
585
|
+
requireManagedAuthProviderRouteAllowed(c.req.method, new URL(c.req.url).pathname);
|
|
586
|
+
// Provider authentication/recovery is isolated from whichever actor the
|
|
587
|
+
// browser currently renders. Selected-session capabilities are all
|
|
588
|
+
// 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
|
+
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),
|
|
600
|
+
)
|
|
601
|
+
: await managedAuth.handler(providerRequest);
|
|
602
|
+
let replacementCookies: readonly string[] | undefined;
|
|
603
|
+
if (deps.settings.managedAuthSessionSetMode === "broker") {
|
|
604
|
+
replacementCookies = await managedAuthSessionAdapter!.createLegacySelectedSessionCookies(
|
|
605
|
+
null,
|
|
606
|
+
c.req.header("cookie") ?? null,
|
|
607
|
+
);
|
|
608
|
+
} else {
|
|
609
|
+
if (authority !== undefined) {
|
|
610
|
+
const snapshot = await getManagedAuthSessionSetSnapshot(deps.db, {
|
|
611
|
+
authorityHash: managedAuthSha256(authority),
|
|
612
|
+
mode: "dual",
|
|
613
|
+
includeInternal: true,
|
|
614
|
+
readOnly: true,
|
|
615
|
+
});
|
|
616
|
+
if (snapshot) {
|
|
617
|
+
replacementCookies =
|
|
618
|
+
await managedAuthSessionAdapter!.createLegacySelectedSessionCookies(
|
|
619
|
+
snapshot.selected,
|
|
620
|
+
c.req.header("cookie") ?? null,
|
|
621
|
+
);
|
|
622
|
+
} else {
|
|
623
|
+
replacementCookies =
|
|
624
|
+
await managedAuthSessionAdapter!.createLegacySelectedSessionCookies(
|
|
625
|
+
null,
|
|
626
|
+
c.req.header("cookie") ?? null,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return await scrubManagedAuthProviderResponse(providerResponse, { replacementCookies });
|
|
632
|
+
});
|
|
503
633
|
}
|
|
504
634
|
|
|
505
635
|
app.get("/healthz", (c) =>
|
|
@@ -585,6 +715,7 @@ export function createAppComposition(deps: AppDependencies): {
|
|
|
585
715
|
: {}),
|
|
586
716
|
},
|
|
587
717
|
productAccessMode: deps.settings.productAccessMode,
|
|
718
|
+
managedAuthSessionSetMode: deps.settings.managedAuthSessionSetMode,
|
|
588
719
|
auth: clientAuthConfig(deps.settings),
|
|
589
720
|
analytics: clientAnalyticsConfig(deps.settings),
|
|
590
721
|
// Channel-A structured services (P4.4) ride exec/readFile/createEditor,
|
|
@@ -765,6 +896,7 @@ export function createAppComposition(deps: AppDependencies): {
|
|
|
765
896
|
registerVideoGenerationRoutes(app, routeDeps);
|
|
766
897
|
registerCanonicalHumanIdentityRoutes(app, routeDeps);
|
|
767
898
|
registerOrganizationMembershipRoutes(app, routeDeps);
|
|
899
|
+
registerOrganizationRecoveryRoutes(app, routeDeps);
|
|
768
900
|
registerUserResourceAuthorityRoutes(app, routeDeps);
|
|
769
901
|
registerConnectionAuthorityRoutes(app, routeDeps);
|
|
770
902
|
registerSlackInteractionRoutes(app, routeDeps);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
|
|
3
|
+
type ManagedAuthAttemptContext =
|
|
4
|
+
| { kind: "isolated_transaction"; transactionId: string }
|
|
5
|
+
| { kind: "discard_provider_session" };
|
|
6
|
+
|
|
7
|
+
const managedAuthAttemptStorage = new AsyncLocalStorage<ManagedAuthAttemptContext>();
|
|
8
|
+
|
|
9
|
+
export function runManagedAuthAttempt<T>(transactionId: string, action: () => T): T {
|
|
10
|
+
return managedAuthAttemptStorage.run({ kind: "isolated_transaction", transactionId }, action);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function runManagedAuthDiscardedProviderSession<T>(action: () => T): T {
|
|
14
|
+
return managedAuthAttemptStorage.run({ kind: "discard_provider_session" }, action);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function currentManagedAuthAttemptId(): string | null {
|
|
18
|
+
const context = managedAuthAttemptStorage.getStore();
|
|
19
|
+
return context?.kind === "isolated_transaction" ? context.transactionId : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function shouldDiscardCurrentManagedAuthProviderSession(): boolean {
|
|
23
|
+
return managedAuthAttemptStorage.getStore()?.kind === "discard_provider_session";
|
|
24
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ManagedAuthResolvedSession,
|
|
3
|
+
ManagedAuthSessionAdapter,
|
|
4
|
+
} from "@opengeni/core/managed-auth-session-sets";
|
|
5
|
+
import { sql } from "drizzle-orm";
|
|
6
|
+
import type { Database } from "@opengeni/db";
|
|
7
|
+
import type { ManagedAuth } from "@opengeni/core";
|
|
8
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
9
|
+
import { runManagedAuthAttempt } from "./managed-auth-attempt-context";
|
|
10
|
+
|
|
11
|
+
export function createBetterAuthSessionAdapter(
|
|
12
|
+
auth: ManagedAuth,
|
|
13
|
+
db: Database,
|
|
14
|
+
): ManagedAuthSessionAdapter {
|
|
15
|
+
return {
|
|
16
|
+
async authenticate(input) {
|
|
17
|
+
if (input.provider !== "email_password") {
|
|
18
|
+
throw new Error("unsupported managed authentication provider");
|
|
19
|
+
}
|
|
20
|
+
const result = await runManagedAuthAttempt(
|
|
21
|
+
input.transactionId,
|
|
22
|
+
async () =>
|
|
23
|
+
await auth.api.signInEmail({
|
|
24
|
+
body: {
|
|
25
|
+
email: input.credentials.email,
|
|
26
|
+
password: input.credentials.password,
|
|
27
|
+
rememberMe: true,
|
|
28
|
+
},
|
|
29
|
+
headers: input.headers,
|
|
30
|
+
returnHeaders: true,
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
const token = result.response?.token;
|
|
34
|
+
if (typeof token !== "string") {
|
|
35
|
+
throw new Error("managed authentication did not create an isolated session");
|
|
36
|
+
}
|
|
37
|
+
const resolved = await (await auth.$context).internalAdapter.findSession(token);
|
|
38
|
+
if (!resolved?.session?.id) {
|
|
39
|
+
throw new Error("managed authentication session could not be resolved");
|
|
40
|
+
}
|
|
41
|
+
return { authSessionId: resolved.session.id };
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
async resolveSelectedSession(input): Promise<ManagedAuthResolvedSession | null> {
|
|
45
|
+
const resolved = await (await auth.$context).internalAdapter.findSession(input.token);
|
|
46
|
+
return liveResolvedSession(resolved);
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async resolveAmbientSession(headers): Promise<ManagedAuthResolvedSession | null> {
|
|
50
|
+
const context = await auth.$context;
|
|
51
|
+
const signed = cookieValue(headers.get("cookie"), context.authCookies.sessionToken.name);
|
|
52
|
+
const token = signed ? verifiedSignedCookieValue(signed, context.secret) : null;
|
|
53
|
+
if (!token) return null;
|
|
54
|
+
const resolved = await context.internalAdapter.findSession(token);
|
|
55
|
+
return liveResolvedSession(resolved);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
async refreshSelectedSession(input): Promise<ManagedAuthResolvedSession | null> {
|
|
59
|
+
const context = await auth.$context;
|
|
60
|
+
const cookie = context.authCookies.sessionToken;
|
|
61
|
+
const headers = new Headers({
|
|
62
|
+
cookie: `${cookie.name}=${signedCookieValue(input.token, context.secret)}`,
|
|
63
|
+
});
|
|
64
|
+
const resolved = await auth.api.getSession({ headers, returnHeaders: true });
|
|
65
|
+
if (!resolved.response) return null;
|
|
66
|
+
// The provider may renew its durable expiry and emit token/cache cookies;
|
|
67
|
+
// this server-side selected-slot resolution intentionally discards them.
|
|
68
|
+
return resolved.response as ManagedAuthResolvedSession;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
async revokeSession(input) {
|
|
72
|
+
await db.execute(sql`delete from auth_sessions where id = ${input.authSessionId}`);
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
async createLegacySelectedSessionCookies(input, currentCookieHeader) {
|
|
76
|
+
const context = await auth.$context;
|
|
77
|
+
const cookie = context.authCookies.sessionToken;
|
|
78
|
+
const value = input ? signedCookieValue(input.token, context.secret) : "";
|
|
79
|
+
const headers = [
|
|
80
|
+
serializeCookieHeader(cookie.name, value, {
|
|
81
|
+
...cookie.attributes,
|
|
82
|
+
...(input ? {} : { maxAge: 0, expires: new Date(0) }),
|
|
83
|
+
}),
|
|
84
|
+
];
|
|
85
|
+
for (const cacheCookie of [
|
|
86
|
+
context.authCookies.sessionData,
|
|
87
|
+
context.authCookies.accountData,
|
|
88
|
+
context.authCookies.dontRememberToken,
|
|
89
|
+
]) {
|
|
90
|
+
for (const name of cacheCookieNames(cacheCookie.name, currentCookieHeader)) {
|
|
91
|
+
headers.push(
|
|
92
|
+
serializeCookieHeader(name, "", {
|
|
93
|
+
...cacheCookie.attributes,
|
|
94
|
+
maxAge: 0,
|
|
95
|
+
expires: new Date(0),
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return headers;
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function liveResolvedSession(value: unknown): ManagedAuthResolvedSession | null {
|
|
106
|
+
if (!value || typeof value !== "object") return null;
|
|
107
|
+
const session = (value as { session?: { expiresAt?: unknown } }).session;
|
|
108
|
+
const expiresAt = session?.expiresAt;
|
|
109
|
+
const expiryMillis =
|
|
110
|
+
expiresAt instanceof Date
|
|
111
|
+
? expiresAt.getTime()
|
|
112
|
+
: typeof expiresAt === "string" || typeof expiresAt === "number"
|
|
113
|
+
? new Date(expiresAt).getTime()
|
|
114
|
+
: Number.NaN;
|
|
115
|
+
if (!Number.isFinite(expiryMillis) || expiryMillis <= Date.now()) return null;
|
|
116
|
+
return value as ManagedAuthResolvedSession;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function cookieValue(header: string | null, name: string): string | null {
|
|
120
|
+
if (!header) return null;
|
|
121
|
+
for (const part of header.split(";")) {
|
|
122
|
+
const separator = part.indexOf("=");
|
|
123
|
+
if (separator < 0 || part.slice(0, separator).trim() !== name) continue;
|
|
124
|
+
return part.slice(separator + 1).trim() || null;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function verifiedSignedCookieValue(value: string, secret: string): string | null {
|
|
130
|
+
let decoded: string;
|
|
131
|
+
try {
|
|
132
|
+
decoded = decodeURIComponent(value);
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const separator = decoded.lastIndexOf(".");
|
|
137
|
+
if (separator <= 0) return null;
|
|
138
|
+
const token = decoded.slice(0, separator);
|
|
139
|
+
const signature = decoded.slice(separator + 1);
|
|
140
|
+
const expected = createHmac("sha256", secret).update(token, "utf8").digest("base64");
|
|
141
|
+
const actualBytes = Buffer.from(signature, "utf8");
|
|
142
|
+
const expectedBytes = Buffer.from(expected, "utf8");
|
|
143
|
+
return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes)
|
|
144
|
+
? token
|
|
145
|
+
: null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function signedCookieValue(value: string, secret: string): string {
|
|
149
|
+
return encodeURIComponent(
|
|
150
|
+
`${value}.${createHmac("sha256", secret).update(value, "utf8").digest("base64")}`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function serializeCookieHeader(
|
|
155
|
+
name: string,
|
|
156
|
+
value: string,
|
|
157
|
+
attributes: {
|
|
158
|
+
domain?: string;
|
|
159
|
+
expires?: Date;
|
|
160
|
+
httpOnly?: boolean;
|
|
161
|
+
maxAge?: number;
|
|
162
|
+
path?: string;
|
|
163
|
+
partitioned?: boolean;
|
|
164
|
+
prefix?: "secure" | "host";
|
|
165
|
+
sameSite?: string;
|
|
166
|
+
secure?: boolean;
|
|
167
|
+
},
|
|
168
|
+
): string {
|
|
169
|
+
let cookieName = name;
|
|
170
|
+
if (attributes.prefix === "secure" && !cookieName.startsWith("__Secure-")) {
|
|
171
|
+
cookieName = `__Secure-${cookieName}`;
|
|
172
|
+
} else if (attributes.prefix === "host" && !cookieName.startsWith("__Host-")) {
|
|
173
|
+
cookieName = `__Host-${cookieName}`;
|
|
174
|
+
}
|
|
175
|
+
if (cookieName.startsWith("__Secure-")) attributes.secure = true;
|
|
176
|
+
if (cookieName.startsWith("__Host-")) {
|
|
177
|
+
attributes.secure = true;
|
|
178
|
+
attributes.path = "/";
|
|
179
|
+
delete attributes.domain;
|
|
180
|
+
}
|
|
181
|
+
let header = `${cookieName}=${value}`;
|
|
182
|
+
if (attributes.maxAge !== undefined)
|
|
183
|
+
header += `; Max-Age=${Math.max(0, Math.floor(attributes.maxAge))}`;
|
|
184
|
+
if (attributes.domain) header += `; Domain=${attributes.domain}`;
|
|
185
|
+
if (attributes.path) header += `; Path=${attributes.path}`;
|
|
186
|
+
if (attributes.expires) header += `; Expires=${attributes.expires.toUTCString()}`;
|
|
187
|
+
if (attributes.httpOnly) header += "; HttpOnly";
|
|
188
|
+
if (attributes.secure) header += "; Secure";
|
|
189
|
+
if (attributes.sameSite) {
|
|
190
|
+
header += `; SameSite=${attributes.sameSite[0]?.toUpperCase()}${attributes.sameSite.slice(1)}`;
|
|
191
|
+
}
|
|
192
|
+
if (attributes.partitioned) header += "; Partitioned";
|
|
193
|
+
return header;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function cacheCookieNames(baseName: string, cookieHeader?: string | null): string[] {
|
|
197
|
+
const names = new Set([baseName]);
|
|
198
|
+
for (const part of cookieHeader?.split(";") ?? []) {
|
|
199
|
+
const separator = part.indexOf("=");
|
|
200
|
+
if (separator < 0) continue;
|
|
201
|
+
const name = part.slice(0, separator).trim();
|
|
202
|
+
if (name === baseName || name.startsWith(`${baseName}.`)) names.add(name);
|
|
203
|
+
}
|
|
204
|
+
return [...names].sort();
|
|
205
|
+
}
|