@aotter/mantle-cloudflare 0.1.0-alpha.6 → 0.1.0-alpha.8
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/README.md +15 -8
- package/dist/auth/conventionalAuth.d.ts +3 -0
- package/dist/auth/conventionalAuth.d.ts.map +1 -1
- package/dist/auth/conventionalAuth.js +23 -6
- package/dist/auth/conventionalAuth.js.map +1 -1
- package/dist/auth/createAuth.d.ts +32 -13
- package/dist/auth/createAuth.d.ts.map +1 -1
- package/dist/auth/createAuth.js +182 -154
- package/dist/auth/createAuth.js.map +1 -1
- package/dist/bindings/AssetsAssetServer.d.ts +4 -4
- package/dist/bindings/AssetsAssetServer.d.ts.map +1 -1
- package/dist/bindings/AssetsAssetServer.js +2 -2
- package/dist/bindings/D1DatabaseDriver.d.ts +3 -4
- package/dist/bindings/D1DatabaseDriver.d.ts.map +1 -1
- package/dist/bindings/D1DatabaseDriver.js +4 -91
- package/dist/bindings/D1DatabaseDriver.js.map +1 -1
- package/dist/bindings/WorkersQueueHookDispatcher.d.ts +2 -2
- package/dist/bindings/WorkersQueueHookDispatcher.d.ts.map +1 -1
- package/dist/bindings/WorkersQueueHookDispatcher.js +1 -1
- package/dist/bindings/WorkersQueueHookDispatcher.js.map +1 -1
- package/dist/bindings/conventionalBindings.d.ts +2 -3
- package/dist/bindings/conventionalBindings.d.ts.map +1 -1
- package/dist/bindings/conventionalBindings.js +1 -3
- package/dist/bindings/conventionalBindings.js.map +1 -1
- package/dist/handlers/turnstile.d.ts +1 -1
- package/dist/handlers/turnstile.d.ts.map +1 -1
- package/dist/handlers/turnstile.js +3 -1
- package/dist/handlers/turnstile.js.map +1 -1
- package/dist/mount/bootRuntimeOnce.d.ts +15 -8
- package/dist/mount/bootRuntimeOnce.d.ts.map +1 -1
- package/dist/mount/bootRuntimeOnce.js +36 -21
- package/dist/mount/bootRuntimeOnce.js.map +1 -1
- package/dist/mount/cmsConfig.d.ts +17 -9
- package/dist/mount/cmsConfig.d.ts.map +1 -1
- package/dist/mount/index.d.ts +4 -4
- package/dist/mount/index.d.ts.map +1 -1
- package/dist/mount/index.js +3 -3
- package/dist/mount/index.js.map +1 -1
- package/dist/mount/mountAdmin.d.ts +6 -0
- package/dist/mount/mountAdmin.d.ts.map +1 -0
- package/dist/mount/mountAdmin.js +16 -0
- package/dist/mount/mountAdmin.js.map +1 -0
- package/dist/mount/mountMcp.d.ts +7 -10
- package/dist/mount/mountMcp.d.ts.map +1 -1
- package/dist/mount/mountMcp.js +57 -49
- package/dist/mount/mountMcp.js.map +1 -1
- package/dist/mount/mountPublicRoutes.d.ts +4 -4
- package/dist/mount/mountPublicRoutes.d.ts.map +1 -1
- package/dist/mount/mountPublicRoutes.js +31 -24
- package/dist/mount/mountPublicRoutes.js.map +1 -1
- package/dist/mount/mountRuntimeEndpoints.d.ts +7 -0
- package/dist/mount/mountRuntimeEndpoints.d.ts.map +1 -0
- package/dist/mount/mountRuntimeEndpoints.js +49 -0
- package/dist/mount/mountRuntimeEndpoints.js.map +1 -0
- package/dist/mount/resolveCaller.d.ts.map +1 -1
- package/dist/mount/resolveCaller.js +0 -33
- package/dist/mount/resolveCaller.js.map +1 -1
- package/dist/oauth/cachePolicy.d.ts.map +1 -1
- package/dist/oauth/cachePolicy.js +2 -4
- package/dist/oauth/cachePolicy.js.map +1 -1
- package/dist/oauth/consentHtml.d.ts +2 -2
- package/dist/oauth/consentHtml.d.ts.map +1 -1
- package/dist/oauth/consentHtml.js +5 -5
- package/dist/oauth/consentHtml.js.map +1 -1
- package/dist/oauth/index.d.ts +0 -2
- package/dist/oauth/index.d.ts.map +1 -1
- package/dist/oauth/index.js +0 -2
- package/dist/oauth/index.js.map +1 -1
- package/dist/oauth/mountOAuth.d.ts +2 -16
- package/dist/oauth/mountOAuth.d.ts.map +1 -1
- package/dist/oauth/mountOAuth.js +24 -99
- package/dist/oauth/mountOAuth.js.map +1 -1
- package/dist/worker/createMantleWorker.d.ts +12 -13
- package/dist/worker/createMantleWorker.d.ts.map +1 -1
- package/dist/worker/createMantleWorker.js +31 -24
- package/dist/worker/createMantleWorker.js.map +1 -1
- package/package.json +12 -11
- package/dist/mount/mountServerEndpoints.d.ts +0 -9
- package/dist/mount/mountServerEndpoints.d.ts.map +0 -1
- package/dist/mount/mountServerEndpoints.js +0 -1625
- package/dist/mount/mountServerEndpoints.js.map +0 -1
- package/dist/mount/runMantleUseCase.d.ts +0 -3
- package/dist/mount/runMantleUseCase.d.ts.map +0 -1
- package/dist/mount/runMantleUseCase.js +0 -41
- package/dist/mount/runMantleUseCase.js.map +0 -1
- package/dist/oauth/oauthConstants.d.ts +0 -15
- package/dist/oauth/oauthConstants.d.ts.map +0 -1
- package/dist/oauth/oauthConstants.js +0 -15
- package/dist/oauth/oauthConstants.js.map +0 -1
- package/dist/oauth/oauthSingleton.d.ts +0 -33
- package/dist/oauth/oauthSingleton.d.ts.map +0 -1
- package/dist/oauth/oauthSingleton.js +0 -39
- package/dist/oauth/oauthSingleton.js.map +0 -1
package/dist/auth/createAuth.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { betterAuth } from "better-auth";
|
|
2
|
-
import {
|
|
2
|
+
import { createDpopReplayStore, enforceDpopBinding, isDpopBindingError, parseAccessTokenAuthorization, verifyJwsAccessToken, } from "better-auth/oauth2";
|
|
3
3
|
import { admin, emailOTP, jwt, magicLink } from "better-auth/plugins";
|
|
4
4
|
import { createAccessControl } from "better-auth/plugins/access";
|
|
5
5
|
import { defaultStatements } from "better-auth/plugins/admin/access";
|
|
6
6
|
import { genericOAuth, } from "better-auth/plugins/generic-oauth";
|
|
7
7
|
import { splitSetCookieHeader } from "better-auth/cookies";
|
|
8
8
|
import { oauthProvider } from "@better-auth/oauth-provider";
|
|
9
|
+
import { mcp } from "@better-auth/mcp";
|
|
10
|
+
import { cimd } from "@better-auth/cimd";
|
|
11
|
+
import { decodeMemberCursor, encodeMemberCursor } from "@aotter/mantle-admin";
|
|
9
12
|
import { STAFF_ROLES } from "@aotter/mantle-spec";
|
|
13
|
+
export { decodeMemberCursor, encodeMemberCursor };
|
|
10
14
|
export { STAFF_ROLES };
|
|
11
15
|
/**
|
|
12
16
|
* Set lookup for "is this role string a staff role?" — handlers/MCP
|
|
@@ -31,6 +35,14 @@ function normalizeAuthBasePath(basePath) {
|
|
|
31
35
|
}
|
|
32
36
|
return trimmed;
|
|
33
37
|
}
|
|
38
|
+
function normalizeAuthErrorURL(errorURL, baseURL) {
|
|
39
|
+
const base = new URL(baseURL);
|
|
40
|
+
const resolved = new URL(errorURL ?? "/", base);
|
|
41
|
+
if (resolved.origin !== base.origin) {
|
|
42
|
+
throw new Error("createAuth: errorURL must be same-origin with baseURL.");
|
|
43
|
+
}
|
|
44
|
+
return `${resolved.pathname}${resolved.search}`;
|
|
45
|
+
}
|
|
34
46
|
/** @internal Exported for regression tests. */
|
|
35
47
|
export function normalizeAuthResponseCookies(response) {
|
|
36
48
|
const setCookie = response.headers.get("set-cookie");
|
|
@@ -156,14 +168,9 @@ export function buildGenericOAuthProviders(methods) {
|
|
|
156
168
|
}
|
|
157
169
|
out.push({
|
|
158
170
|
providerId: method.providerId,
|
|
159
|
-
...(method.displayName ? { displayName: method.displayName } : {}),
|
|
160
171
|
clientId: method.clientId,
|
|
161
172
|
...(method.clientSecret ? { clientSecret: method.clientSecret } : {}),
|
|
162
173
|
...(method.discoveryUrl ? { discoveryUrl: method.discoveryUrl } : {}),
|
|
163
|
-
...(method.issuer ? { issuer: method.issuer } : {}),
|
|
164
|
-
...(method.requireIssuerValidation !== undefined
|
|
165
|
-
? { requireIssuerValidation: method.requireIssuerValidation }
|
|
166
|
-
: {}),
|
|
167
174
|
...(method.authorizationUrl ? { authorizationUrl: method.authorizationUrl } : {}),
|
|
168
175
|
...(method.tokenUrl ? { tokenUrl: method.tokenUrl } : {}),
|
|
169
176
|
...(method.userInfoUrl ? { userInfoUrl: method.userInfoUrl } : {}),
|
|
@@ -180,6 +187,7 @@ export function buildGenericOAuthProviders(methods) {
|
|
|
180
187
|
resource: method.resource,
|
|
181
188
|
authorizationUrlParams: { resource: method.resource },
|
|
182
189
|
tokenUrlParams: { resource: method.resource },
|
|
190
|
+
refreshTokenParams: { resource: method.resource },
|
|
183
191
|
}
|
|
184
192
|
: {}),
|
|
185
193
|
});
|
|
@@ -296,15 +304,11 @@ export function shouldPromoteToOwner(rule, user) {
|
|
|
296
304
|
export function guardGithubLoginProfile(user, context, methods) {
|
|
297
305
|
if (!("githubLogin" in user))
|
|
298
306
|
return undefined;
|
|
299
|
-
const providerId = context?.path === "/callback/:id"
|
|
300
|
-
? context.params?.id
|
|
301
|
-
: context?.path === "/oauth2/callback/:providerId"
|
|
302
|
-
? context.params?.providerId
|
|
303
|
-
: null;
|
|
307
|
+
const providerId = context?.path === "/callback/:id" ? context.params?.id : null;
|
|
304
308
|
const trusted = typeof providerId === "string" && methods.some((method) => method.kind === "social"
|
|
305
309
|
? context?.path === "/callback/:id" && method.provider === "github" && providerId === "github"
|
|
306
310
|
: method.kind === "oauth" &&
|
|
307
|
-
context?.path === "/
|
|
311
|
+
context?.path === "/callback/:id" &&
|
|
308
312
|
method.providerId === providerId &&
|
|
309
313
|
Boolean(method.mapProfileToUser));
|
|
310
314
|
return trusted ? undefined : { data: { githubLogin: null } };
|
|
@@ -348,107 +352,20 @@ export function buildTrustedOriginsFor(methods, configured = []) {
|
|
|
348
352
|
function methodsRequireSameSiteNone(methods) {
|
|
349
353
|
return methods.some((m) => m.kind === "social" && m.provider === "apple");
|
|
350
354
|
}
|
|
351
|
-
/**
|
|
352
|
-
* Better Auth 1.6.x declares generic-OAuth URL parameter options but
|
|
353
|
-
* its provider implementation does not replay them across every token
|
|
354
|
-
* lifecycle step. Patch only resource-configured generic providers at
|
|
355
|
-
* the provider boundary, using Better Auth's public OAuth helpers.
|
|
356
|
-
*/
|
|
357
|
-
/** @internal exported for lifecycle regression tests. */
|
|
358
|
-
export function buildOAuthResourceLifecyclePlugin(methods) {
|
|
359
|
-
const configured = new Map(methods
|
|
360
|
-
.filter((method) => method.kind === "oauth" && typeof method.resource === "string")
|
|
361
|
-
.map((method) => [method.providerId, method]));
|
|
362
|
-
if (configured.size === 0)
|
|
363
|
-
return null;
|
|
364
|
-
return {
|
|
365
|
-
id: "mantle-oauth-resource-lifecycle",
|
|
366
|
-
init(ctx) {
|
|
367
|
-
const tokenEndpoints = new Map();
|
|
368
|
-
const resolveTokenEndpoint = (method) => {
|
|
369
|
-
if (method.tokenUrl)
|
|
370
|
-
return Promise.resolve(method.tokenUrl);
|
|
371
|
-
const existing = tokenEndpoints.get(method.providerId);
|
|
372
|
-
if (existing)
|
|
373
|
-
return existing;
|
|
374
|
-
const pending = (async () => {
|
|
375
|
-
if (!method.discoveryUrl) {
|
|
376
|
-
throw new Error(`OAuth provider '${method.providerId}' has no token endpoint.`);
|
|
377
|
-
}
|
|
378
|
-
const response = await fetch(method.discoveryUrl, {
|
|
379
|
-
headers: { accept: "application/json" },
|
|
380
|
-
redirect: "error",
|
|
381
|
-
});
|
|
382
|
-
if (!response.ok) {
|
|
383
|
-
throw new Error(`OAuth discovery for '${method.providerId}' returned ${response.status}.`);
|
|
384
|
-
}
|
|
385
|
-
const discovery = (await response.json());
|
|
386
|
-
if (typeof discovery.token_endpoint !== "string") {
|
|
387
|
-
throw new Error(`OAuth discovery for '${method.providerId}' omitted token_endpoint.`);
|
|
388
|
-
}
|
|
389
|
-
return discovery.token_endpoint;
|
|
390
|
-
})();
|
|
391
|
-
tokenEndpoints.set(method.providerId, pending);
|
|
392
|
-
return pending;
|
|
393
|
-
};
|
|
394
|
-
return {
|
|
395
|
-
context: {
|
|
396
|
-
socialProviders: ctx.socialProviders.map((provider) => {
|
|
397
|
-
const method = configured.get(provider.id);
|
|
398
|
-
if (!method?.resource)
|
|
399
|
-
return provider;
|
|
400
|
-
const createAuthorizationURL = provider.createAuthorizationURL.bind(provider);
|
|
401
|
-
return {
|
|
402
|
-
...provider,
|
|
403
|
-
async createAuthorizationURL(data) {
|
|
404
|
-
const url = await createAuthorizationURL(data);
|
|
405
|
-
url.searchParams.set("resource", method.resource);
|
|
406
|
-
return url;
|
|
407
|
-
},
|
|
408
|
-
async validateAuthorizationCode(data) {
|
|
409
|
-
const tokens = await validateAuthorizationCode({
|
|
410
|
-
code: data.code,
|
|
411
|
-
codeVerifier: data.codeVerifier,
|
|
412
|
-
redirectURI: data.redirectURI,
|
|
413
|
-
options: {
|
|
414
|
-
clientId: method.clientId,
|
|
415
|
-
clientSecret: method.clientSecret,
|
|
416
|
-
redirectURI: method.redirectURI,
|
|
417
|
-
},
|
|
418
|
-
tokenEndpoint: await resolveTokenEndpoint(method),
|
|
419
|
-
authentication: method.authentication,
|
|
420
|
-
resource: method.resource,
|
|
421
|
-
});
|
|
422
|
-
return applyDefaultAccessTokenExpiry(tokens, undefined);
|
|
423
|
-
},
|
|
424
|
-
async refreshAccessToken(refreshTokenValue) {
|
|
425
|
-
const tokens = await refreshAccessToken({
|
|
426
|
-
refreshToken: refreshTokenValue,
|
|
427
|
-
options: {
|
|
428
|
-
clientId: method.clientId,
|
|
429
|
-
clientSecret: method.clientSecret,
|
|
430
|
-
},
|
|
431
|
-
tokenEndpoint: await resolveTokenEndpoint(method),
|
|
432
|
-
authentication: method.authentication,
|
|
433
|
-
extraParams: { resource: method.resource },
|
|
434
|
-
});
|
|
435
|
-
return applyDefaultAccessTokenExpiry(tokens, undefined);
|
|
436
|
-
},
|
|
437
|
-
};
|
|
438
|
-
}),
|
|
439
|
-
},
|
|
440
|
-
};
|
|
441
|
-
},
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
355
|
/** @internal exported for provider-option mapping tests. */
|
|
445
356
|
export function buildOAuthProviderOptions(config) {
|
|
446
357
|
return {
|
|
447
358
|
loginPage: config.loginPage,
|
|
448
359
|
consentPage: config.consentPage,
|
|
449
360
|
...(config.scopes ? { scopes: [...config.scopes] } : {}),
|
|
450
|
-
...(config.
|
|
451
|
-
? {
|
|
361
|
+
...(config.resources
|
|
362
|
+
? { resources: [...config.resources] }
|
|
363
|
+
: {}),
|
|
364
|
+
...(config.mcpResource
|
|
365
|
+
? {
|
|
366
|
+
clientRegistrationClientSecretExpiration: "90d",
|
|
367
|
+
allowPublicClientPrelogin: true,
|
|
368
|
+
}
|
|
452
369
|
: {}),
|
|
453
370
|
...(config.allowDynamicClientRegistration !== undefined
|
|
454
371
|
? { allowDynamicClientRegistration: config.allowDynamicClientRegistration }
|
|
@@ -492,7 +409,9 @@ function buildAuth(config) {
|
|
|
492
409
|
const bootstrap = config.bootstrapOwner;
|
|
493
410
|
const emailOtpMethod = pickSingleton(config.methods, "email-otp");
|
|
494
411
|
const magicLinkMethod = pickSingleton(config.methods, "magic-link");
|
|
495
|
-
const
|
|
412
|
+
const providerOptions = config.oauthProvider
|
|
413
|
+
? buildOAuthProviderOptions(config.oauthProvider)
|
|
414
|
+
: null;
|
|
496
415
|
// Rate limit: Better Auth's per-route limits gate on
|
|
497
416
|
// `process.env.NODE_ENV === "production"`, which is unset on
|
|
498
417
|
// Cloudflare Workers — leaving the limits silently off. When any
|
|
@@ -525,17 +444,33 @@ function buildAuth(config) {
|
|
|
525
444
|
...(genericOAuthProviders.length > 0
|
|
526
445
|
? [
|
|
527
446
|
genericOAuth({
|
|
528
|
-
config: genericOAuthProviders
|
|
447
|
+
config: genericOAuthProviders,
|
|
529
448
|
}),
|
|
530
449
|
]
|
|
531
450
|
: []),
|
|
532
|
-
...(resourceLifecyclePlugin ? [resourceLifecyclePlugin] : []),
|
|
533
451
|
...(emailOtpMethod ? [buildEmailOTPPlugin(emailOtpMethod)] : []),
|
|
534
452
|
...(magicLinkMethod ? [buildMagicLinkPlugin(magicLinkMethod)] : []),
|
|
535
|
-
...(config.oauthProvider
|
|
453
|
+
...(config.oauthProvider && providerOptions
|
|
536
454
|
? [
|
|
537
455
|
jwt(),
|
|
538
|
-
|
|
456
|
+
config.oauthProvider.mcpResource
|
|
457
|
+
? mcp({
|
|
458
|
+
...providerOptions,
|
|
459
|
+
resource: config.oauthProvider.mcpResource,
|
|
460
|
+
})
|
|
461
|
+
: oauthProvider(providerOptions),
|
|
462
|
+
...(config.oauthProvider.mcpResource
|
|
463
|
+
? [
|
|
464
|
+
cimd({
|
|
465
|
+
// Cloudflare's `global_fetch_strictly_public` flag is the
|
|
466
|
+
// runtime network boundary: resolution and connection stay
|
|
467
|
+
// on the public Internet. Better Auth owns timeout, limits,
|
|
468
|
+
// validation, caching, and redirect rejection above it.
|
|
469
|
+
fetchClientMetadataResource: (input, init) => fetch(input, { ...init, redirect: "error" }),
|
|
470
|
+
metadataProfile: "mcp-2026-07-28",
|
|
471
|
+
}),
|
|
472
|
+
]
|
|
473
|
+
: []),
|
|
539
474
|
]
|
|
540
475
|
: []),
|
|
541
476
|
];
|
|
@@ -620,6 +555,7 @@ function buildAuth(config) {
|
|
|
620
555
|
secret: config.secret,
|
|
621
556
|
baseURL: config.baseURL,
|
|
622
557
|
basePath: normalizeAuthBasePath(config.basePath),
|
|
558
|
+
onAPIError: { errorURL: normalizeAuthErrorURL(config.errorURL, config.baseURL) },
|
|
623
559
|
socialProviders,
|
|
624
560
|
user: userConfig,
|
|
625
561
|
...(rateLimit ? { rateLimit } : {}),
|
|
@@ -629,47 +565,63 @@ function buildAuth(config) {
|
|
|
629
565
|
databaseHooks,
|
|
630
566
|
});
|
|
631
567
|
}
|
|
632
|
-
const MEMBER_CURSOR_PREFIX = "m:";
|
|
633
|
-
/** @internal Exported for endpoint and regression-test validation. */
|
|
634
|
-
export function encodeMemberCursor(createdAt, id) {
|
|
635
|
-
return `${MEMBER_CURSOR_PREFIX}${encodeURIComponent(JSON.stringify([createdAt, id]))}`;
|
|
636
|
-
}
|
|
637
|
-
/** @internal Exported for endpoint and regression-test validation. */
|
|
638
|
-
export function decodeMemberCursor(cursor) {
|
|
639
|
-
if (!cursor.startsWith(MEMBER_CURSOR_PREFIX))
|
|
640
|
-
return null;
|
|
641
|
-
try {
|
|
642
|
-
const value = JSON.parse(decodeURIComponent(cursor.slice(MEMBER_CURSOR_PREFIX.length)));
|
|
643
|
-
return Array.isArray(value) && value.length === 2 &&
|
|
644
|
-
typeof value[0] === "string" && !Number.isNaN(Date.parse(value[0])) &&
|
|
645
|
-
typeof value[1] === "string" && value[1]
|
|
646
|
-
? [value[0], value[1]]
|
|
647
|
-
: null;
|
|
648
|
-
}
|
|
649
|
-
catch {
|
|
650
|
-
return null;
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
568
|
const SETUP_INCOMPLETE_AUTHS = new WeakSet();
|
|
654
569
|
/** True only for the fail-closed facade returned by createSetupIncompleteAuth. */
|
|
655
570
|
export function isSetupIncompleteAuth(auth) {
|
|
656
571
|
return SETUP_INCOMPLETE_AUTHS.has(auth);
|
|
657
572
|
}
|
|
573
|
+
const LEGACY_DCR_TTL_MS = 90 * 24 * 60 * 60 * 1_000;
|
|
574
|
+
const LEGACY_DCR_CLEANUP_INTERVAL_MS = 60 * 60 * 1_000;
|
|
658
575
|
export function createAuth(config) {
|
|
659
576
|
const auth = buildAuth(config);
|
|
660
577
|
const basePath = normalizeAuthBasePath(config.basePath);
|
|
661
578
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
662
579
|
const api = auth.api;
|
|
663
580
|
const localJwksCacheKey = {};
|
|
581
|
+
let dpopReplayStore = null;
|
|
582
|
+
const getDpopReplayStore = async () => {
|
|
583
|
+
if (dpopReplayStore)
|
|
584
|
+
return dpopReplayStore;
|
|
585
|
+
const context = await auth.$context;
|
|
586
|
+
dpopReplayStore = createDpopReplayStore(context.internalAdapter);
|
|
587
|
+
return dpopReplayStore;
|
|
588
|
+
};
|
|
664
589
|
const verifyAccessToken = config.oauthProvider
|
|
665
590
|
? async (token, audience) => {
|
|
666
591
|
const context = await auth.$context;
|
|
667
592
|
return verifyOAuthJwtWithLocalJwks(token, audience, context.baseURL, async () => api.getJwks(), localJwksCacheKey);
|
|
668
593
|
}
|
|
669
594
|
: null;
|
|
595
|
+
let nextDcrCleanupAt = 0;
|
|
596
|
+
const pruneExpiredDynamicClients = async () => {
|
|
597
|
+
const now = Date.now();
|
|
598
|
+
if (!config.oauthProvider?.mcpResource || now < nextDcrCleanupAt)
|
|
599
|
+
return;
|
|
600
|
+
// Set before awaiting so concurrent OAuth requests do not fan out writes.
|
|
601
|
+
nextDcrCleanupAt = now + LEGACY_DCR_CLEANUP_INTERVAL_MS;
|
|
602
|
+
try {
|
|
603
|
+
await config.database
|
|
604
|
+
.prepare("DELETE FROM oauthClient WHERE clientDiscoveryId IS NULL AND userId IS NULL AND referenceId IS NULL AND createdAt < ?")
|
|
605
|
+
.bind(new Date(now - LEGACY_DCR_TTL_MS).toISOString())
|
|
606
|
+
.run();
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
// Cleanup is bounded storage hygiene, not an authorization decision.
|
|
610
|
+
console.error("[better-auth] legacy DCR cleanup failed", error);
|
|
611
|
+
}
|
|
612
|
+
};
|
|
670
613
|
return {
|
|
671
614
|
basePath,
|
|
672
|
-
|
|
615
|
+
...(config.oauthProvider?.mcpResource
|
|
616
|
+
? { mcpResource: config.oauthProvider.mcpResource }
|
|
617
|
+
: {}),
|
|
618
|
+
handler: async (request) => {
|
|
619
|
+
const pathname = new URL(request.url).pathname;
|
|
620
|
+
if (pathname.startsWith(`${basePath}/oauth2/`)) {
|
|
621
|
+
await pruneExpiredDynamicClients();
|
|
622
|
+
}
|
|
623
|
+
return normalizeAuthResponseCookies(await auth.handler(request));
|
|
624
|
+
},
|
|
673
625
|
getSession: (request) => api.getSession({ headers: request.headers }).then((r) => r ?? null),
|
|
674
626
|
getUserRole: async (userId) => {
|
|
675
627
|
const row = await config.database
|
|
@@ -700,9 +652,76 @@ export function createAuth(config) {
|
|
|
700
652
|
createdAt,
|
|
701
653
|
};
|
|
702
654
|
},
|
|
703
|
-
getProviderAccessToken: (request, providerId) =>
|
|
655
|
+
getProviderAccessToken: async (request, providerId) => {
|
|
656
|
+
const session = await api.getSession({ headers: request.headers });
|
|
657
|
+
const userId = session?.user?.id;
|
|
658
|
+
const account = userId
|
|
659
|
+
? await config.database
|
|
660
|
+
.prepare("SELECT id FROM account WHERE userId = ? AND providerId = ? LIMIT 1")
|
|
661
|
+
.bind(userId, providerId)
|
|
662
|
+
.first()
|
|
663
|
+
: null;
|
|
664
|
+
if (!account) {
|
|
665
|
+
throw new Error(`getProviderAccessToken: provider '${providerId}' is not linked to the current user.`);
|
|
666
|
+
}
|
|
667
|
+
return getProviderAccessTokenForRequest(api, request, account.id, providerId);
|
|
668
|
+
},
|
|
704
669
|
verifyOAuthAccessToken: async (tokenOrRequest, options) => {
|
|
705
|
-
return verifyOAuthJwt(tokenOrRequest, options, verifyAccessToken);
|
|
670
|
+
return verifyOAuthJwt(tokenOrRequest, options, verifyAccessToken, getDpopReplayStore);
|
|
671
|
+
},
|
|
672
|
+
getOAuthConsentRequest: async (request) => {
|
|
673
|
+
if (!config.oauthProvider)
|
|
674
|
+
return null;
|
|
675
|
+
const url = new URL(request.url);
|
|
676
|
+
const clientId = url.searchParams.get("client_id");
|
|
677
|
+
if (!clientId || !url.search)
|
|
678
|
+
return null;
|
|
679
|
+
const oauthQuery = url.search.slice(1);
|
|
680
|
+
const client = await api.getOAuthClientPublicPrelogin({
|
|
681
|
+
headers: request.headers,
|
|
682
|
+
body: { client_id: clientId, oauth_query: oauthQuery },
|
|
683
|
+
});
|
|
684
|
+
const redirectUri = url.searchParams.get("redirect_uri") ??
|
|
685
|
+
(Array.isArray(client?.redirect_uris) &&
|
|
686
|
+
typeof client.redirect_uris[0] === "string"
|
|
687
|
+
? client.redirect_uris[0]
|
|
688
|
+
: "");
|
|
689
|
+
return {
|
|
690
|
+
clientName: typeof client?.client_name === "string"
|
|
691
|
+
? client.client_name
|
|
692
|
+
: clientId,
|
|
693
|
+
redirectUri,
|
|
694
|
+
scopes: (url.searchParams.get("scope") ?? "")
|
|
695
|
+
.split(/\s+/u)
|
|
696
|
+
.filter(Boolean),
|
|
697
|
+
oauthQuery,
|
|
698
|
+
};
|
|
699
|
+
},
|
|
700
|
+
completeOAuthConsent: async (request, accept) => {
|
|
701
|
+
if (!config.oauthProvider) {
|
|
702
|
+
throw new Error("completeOAuthConsent: oauthProvider is not configured.");
|
|
703
|
+
}
|
|
704
|
+
const form = await request.formData();
|
|
705
|
+
const oauthQuery = form.get("oauth_query");
|
|
706
|
+
if (typeof oauthQuery !== "string" || oauthQuery.length === 0) {
|
|
707
|
+
throw new Error("completeOAuthConsent: oauth_query is missing.");
|
|
708
|
+
}
|
|
709
|
+
const headers = new Headers(request.headers);
|
|
710
|
+
headers.set("content-type", "application/json");
|
|
711
|
+
headers.delete("content-length");
|
|
712
|
+
const response = await auth.handler(new Request(new URL(`${basePath}/oauth2/consent`, request.url), {
|
|
713
|
+
method: "POST",
|
|
714
|
+
headers,
|
|
715
|
+
body: JSON.stringify({ accept, oauth_query: oauthQuery }),
|
|
716
|
+
}));
|
|
717
|
+
if (!response.ok) {
|
|
718
|
+
throw new Error(`completeOAuthConsent: Better Auth returned ${response.status}.`);
|
|
719
|
+
}
|
|
720
|
+
const result = await response.json();
|
|
721
|
+
if (!result || typeof result.url !== "string") {
|
|
722
|
+
throw new Error("completeOAuthConsent: Better Auth omitted the redirect URL.");
|
|
723
|
+
}
|
|
724
|
+
return result.url;
|
|
706
725
|
},
|
|
707
726
|
methods: config.methods.map((m) => {
|
|
708
727
|
switch (m.kind) {
|
|
@@ -861,7 +880,7 @@ export function createAuth(config) {
|
|
|
861
880
|
: {}),
|
|
862
881
|
...(input.grantTypes ? { grant_types: [...input.grantTypes] } : {}),
|
|
863
882
|
...(input.responseTypes ? { response_types: [...input.responseTypes] } : {}),
|
|
864
|
-
...(input.
|
|
883
|
+
...(input.applicationType ? { application_type: input.applicationType } : {}),
|
|
865
884
|
...(input.skipConsent !== undefined ? { skip_consent: input.skipConsent } : {}),
|
|
866
885
|
...(input.enableEndSession !== undefined
|
|
867
886
|
? { enable_end_session: input.enableEndSession }
|
|
@@ -900,6 +919,10 @@ export function createSetupIncompleteAuth(options = {}) {
|
|
|
900
919
|
status: 401,
|
|
901
920
|
reason: "invalid-token",
|
|
902
921
|
}),
|
|
922
|
+
getOAuthConsentRequest: async () => null,
|
|
923
|
+
completeOAuthConsent: async () => {
|
|
924
|
+
throw new Error(message);
|
|
925
|
+
},
|
|
903
926
|
methods: [],
|
|
904
927
|
listLinkedAccounts: async () => [],
|
|
905
928
|
unlinkAccount: async () => false,
|
|
@@ -917,21 +940,12 @@ export function createSetupIncompleteAuth(options = {}) {
|
|
|
917
940
|
SETUP_INCOMPLETE_AUTHS.add(auth);
|
|
918
941
|
return auth;
|
|
919
942
|
}
|
|
920
|
-
function bearerTokenFrom(tokenOrRequest) {
|
|
921
|
-
if (typeof tokenOrRequest === "string")
|
|
922
|
-
return tokenOrRequest.trim() || null;
|
|
923
|
-
const header = tokenOrRequest.headers.get("authorization");
|
|
924
|
-
if (!header)
|
|
925
|
-
return null;
|
|
926
|
-
const match = /^Bearer ([^\s]+)$/i.exec(header);
|
|
927
|
-
return match?.[1] ?? null;
|
|
928
|
-
}
|
|
929
943
|
/** @internal exported to pin the session-bound Better Auth request and
|
|
930
944
|
* secret-minimizing response mapping. */
|
|
931
|
-
export async function getProviderAccessTokenForRequest(api, request, providerId) {
|
|
945
|
+
export async function getProviderAccessTokenForRequest(api, request, accountId, providerId) {
|
|
932
946
|
const value = (await api.getAccessToken({
|
|
933
947
|
headers: request.headers,
|
|
934
|
-
body: {
|
|
948
|
+
body: { accountId },
|
|
935
949
|
}));
|
|
936
950
|
if (typeof value?.accessToken !== "string") {
|
|
937
951
|
throw new Error(`getProviderAccessToken: provider '${providerId}' returned no access token.`);
|
|
@@ -964,8 +978,10 @@ export function verifyOAuthJwtWithLocalJwks(token, audience, issuer, jwksFetch,
|
|
|
964
978
|
}
|
|
965
979
|
/** @internal exported to pin the stable facade's normalization and
|
|
966
980
|
* 401/403 contract independently of Better Auth network/JWKS I/O. */
|
|
967
|
-
export async function verifyOAuthJwt(tokenOrRequest, options, verify) {
|
|
968
|
-
const
|
|
981
|
+
export async function verifyOAuthJwt(tokenOrRequest, options, verify, getDpopReplayStore) {
|
|
982
|
+
const request = typeof tokenOrRequest === "string" ? null : tokenOrRequest;
|
|
983
|
+
const authorization = parseAccessTokenAuthorization(request?.headers.get("authorization") ?? `Bearer ${tokenOrRequest}`);
|
|
984
|
+
const token = authorization?.token;
|
|
969
985
|
// JWT compact serialization has exactly three non-empty parts.
|
|
970
986
|
// Reject opaque tokens before any network/JWKS work.
|
|
971
987
|
if (!verify ||
|
|
@@ -976,6 +992,16 @@ export async function verifyOAuthJwt(tokenOrRequest, options, verify) {
|
|
|
976
992
|
}
|
|
977
993
|
try {
|
|
978
994
|
const claims = await verify(token, options.audience);
|
|
995
|
+
await enforceDpopBinding({
|
|
996
|
+
payload: claims,
|
|
997
|
+
authorization,
|
|
998
|
+
proofJwt: request?.headers.get("dpop"),
|
|
999
|
+
method: request?.method ?? "GET",
|
|
1000
|
+
url: request?.url ?? options.audience,
|
|
1001
|
+
...(request && authorization.scheme === "DPoP" && getDpopReplayStore
|
|
1002
|
+
? { replayStore: await getDpopReplayStore() }
|
|
1003
|
+
: {}),
|
|
1004
|
+
});
|
|
979
1005
|
if (typeof claims["sub"] !== "string" || claims["sub"].length === 0) {
|
|
980
1006
|
return { ok: false, status: 401, reason: "invalid-token" };
|
|
981
1007
|
}
|
|
@@ -997,7 +1023,10 @@ export async function verifyOAuthJwt(tokenOrRequest, options, verify) {
|
|
|
997
1023
|
scopes,
|
|
998
1024
|
};
|
|
999
1025
|
}
|
|
1000
|
-
catch {
|
|
1026
|
+
catch (error) {
|
|
1027
|
+
if (isDpopBindingError(error)) {
|
|
1028
|
+
return { ok: false, status: 401, reason: "invalid-dpop-proof" };
|
|
1029
|
+
}
|
|
1001
1030
|
return { ok: false, status: 401, reason: "invalid-token" };
|
|
1002
1031
|
}
|
|
1003
1032
|
}
|
|
@@ -1009,7 +1038,7 @@ export function mapRegisteredOAuthClient(value) {
|
|
|
1009
1038
|
}
|
|
1010
1039
|
return {
|
|
1011
1040
|
clientId: row.client_id,
|
|
1012
|
-
...(row.client_secret && row.
|
|
1041
|
+
...(row.client_secret && row.token_endpoint_auth_method !== "none"
|
|
1013
1042
|
? { clientSecret: row.client_secret }
|
|
1014
1043
|
: {}),
|
|
1015
1044
|
redirectUris: row.redirect_uris,
|
|
@@ -1019,8 +1048,7 @@ export function mapRegisteredOAuthClient(value) {
|
|
|
1019
1048
|
...(row.token_endpoint_auth_method
|
|
1020
1049
|
? { tokenEndpointAuthMethod: row.token_endpoint_auth_method }
|
|
1021
1050
|
: {}),
|
|
1022
|
-
...(row.
|
|
1023
|
-
...(row.public !== undefined ? { public: row.public } : {}),
|
|
1051
|
+
...(row.application_type ? { applicationType: row.application_type } : {}),
|
|
1024
1052
|
};
|
|
1025
1053
|
}
|
|
1026
1054
|
/** Random 32-char alphanumeric id, shaped like Better Auth's own user
|