@aotter/mantle-cloudflare 0.1.0-alpha.7 → 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 +30 -13
- package/dist/auth/createAuth.d.ts.map +1 -1
- package/dist/auth/createAuth.js +173 -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
|
|
@@ -164,14 +168,9 @@ export function buildGenericOAuthProviders(methods) {
|
|
|
164
168
|
}
|
|
165
169
|
out.push({
|
|
166
170
|
providerId: method.providerId,
|
|
167
|
-
...(method.displayName ? { displayName: method.displayName } : {}),
|
|
168
171
|
clientId: method.clientId,
|
|
169
172
|
...(method.clientSecret ? { clientSecret: method.clientSecret } : {}),
|
|
170
173
|
...(method.discoveryUrl ? { discoveryUrl: method.discoveryUrl } : {}),
|
|
171
|
-
...(method.issuer ? { issuer: method.issuer } : {}),
|
|
172
|
-
...(method.requireIssuerValidation !== undefined
|
|
173
|
-
? { requireIssuerValidation: method.requireIssuerValidation }
|
|
174
|
-
: {}),
|
|
175
174
|
...(method.authorizationUrl ? { authorizationUrl: method.authorizationUrl } : {}),
|
|
176
175
|
...(method.tokenUrl ? { tokenUrl: method.tokenUrl } : {}),
|
|
177
176
|
...(method.userInfoUrl ? { userInfoUrl: method.userInfoUrl } : {}),
|
|
@@ -188,6 +187,7 @@ export function buildGenericOAuthProviders(methods) {
|
|
|
188
187
|
resource: method.resource,
|
|
189
188
|
authorizationUrlParams: { resource: method.resource },
|
|
190
189
|
tokenUrlParams: { resource: method.resource },
|
|
190
|
+
refreshTokenParams: { resource: method.resource },
|
|
191
191
|
}
|
|
192
192
|
: {}),
|
|
193
193
|
});
|
|
@@ -304,15 +304,11 @@ export function shouldPromoteToOwner(rule, user) {
|
|
|
304
304
|
export function guardGithubLoginProfile(user, context, methods) {
|
|
305
305
|
if (!("githubLogin" in user))
|
|
306
306
|
return undefined;
|
|
307
|
-
const providerId = context?.path === "/callback/:id"
|
|
308
|
-
? context.params?.id
|
|
309
|
-
: context?.path === "/oauth2/callback/:providerId"
|
|
310
|
-
? context.params?.providerId
|
|
311
|
-
: null;
|
|
307
|
+
const providerId = context?.path === "/callback/:id" ? context.params?.id : null;
|
|
312
308
|
const trusted = typeof providerId === "string" && methods.some((method) => method.kind === "social"
|
|
313
309
|
? context?.path === "/callback/:id" && method.provider === "github" && providerId === "github"
|
|
314
310
|
: method.kind === "oauth" &&
|
|
315
|
-
context?.path === "/
|
|
311
|
+
context?.path === "/callback/:id" &&
|
|
316
312
|
method.providerId === providerId &&
|
|
317
313
|
Boolean(method.mapProfileToUser));
|
|
318
314
|
return trusted ? undefined : { data: { githubLogin: null } };
|
|
@@ -356,107 +352,20 @@ export function buildTrustedOriginsFor(methods, configured = []) {
|
|
|
356
352
|
function methodsRequireSameSiteNone(methods) {
|
|
357
353
|
return methods.some((m) => m.kind === "social" && m.provider === "apple");
|
|
358
354
|
}
|
|
359
|
-
/**
|
|
360
|
-
* Better Auth 1.6.x declares generic-OAuth URL parameter options but
|
|
361
|
-
* its provider implementation does not replay them across every token
|
|
362
|
-
* lifecycle step. Patch only resource-configured generic providers at
|
|
363
|
-
* the provider boundary, using Better Auth's public OAuth helpers.
|
|
364
|
-
*/
|
|
365
|
-
/** @internal exported for lifecycle regression tests. */
|
|
366
|
-
export function buildOAuthResourceLifecyclePlugin(methods) {
|
|
367
|
-
const configured = new Map(methods
|
|
368
|
-
.filter((method) => method.kind === "oauth" && typeof method.resource === "string")
|
|
369
|
-
.map((method) => [method.providerId, method]));
|
|
370
|
-
if (configured.size === 0)
|
|
371
|
-
return null;
|
|
372
|
-
return {
|
|
373
|
-
id: "mantle-oauth-resource-lifecycle",
|
|
374
|
-
init(ctx) {
|
|
375
|
-
const tokenEndpoints = new Map();
|
|
376
|
-
const resolveTokenEndpoint = (method) => {
|
|
377
|
-
if (method.tokenUrl)
|
|
378
|
-
return Promise.resolve(method.tokenUrl);
|
|
379
|
-
const existing = tokenEndpoints.get(method.providerId);
|
|
380
|
-
if (existing)
|
|
381
|
-
return existing;
|
|
382
|
-
const pending = (async () => {
|
|
383
|
-
if (!method.discoveryUrl) {
|
|
384
|
-
throw new Error(`OAuth provider '${method.providerId}' has no token endpoint.`);
|
|
385
|
-
}
|
|
386
|
-
const response = await fetch(method.discoveryUrl, {
|
|
387
|
-
headers: { accept: "application/json" },
|
|
388
|
-
redirect: "error",
|
|
389
|
-
});
|
|
390
|
-
if (!response.ok) {
|
|
391
|
-
throw new Error(`OAuth discovery for '${method.providerId}' returned ${response.status}.`);
|
|
392
|
-
}
|
|
393
|
-
const discovery = (await response.json());
|
|
394
|
-
if (typeof discovery.token_endpoint !== "string") {
|
|
395
|
-
throw new Error(`OAuth discovery for '${method.providerId}' omitted token_endpoint.`);
|
|
396
|
-
}
|
|
397
|
-
return discovery.token_endpoint;
|
|
398
|
-
})();
|
|
399
|
-
tokenEndpoints.set(method.providerId, pending);
|
|
400
|
-
return pending;
|
|
401
|
-
};
|
|
402
|
-
return {
|
|
403
|
-
context: {
|
|
404
|
-
socialProviders: ctx.socialProviders.map((provider) => {
|
|
405
|
-
const method = configured.get(provider.id);
|
|
406
|
-
if (!method?.resource)
|
|
407
|
-
return provider;
|
|
408
|
-
const createAuthorizationURL = provider.createAuthorizationURL.bind(provider);
|
|
409
|
-
return {
|
|
410
|
-
...provider,
|
|
411
|
-
async createAuthorizationURL(data) {
|
|
412
|
-
const url = await createAuthorizationURL(data);
|
|
413
|
-
url.searchParams.set("resource", method.resource);
|
|
414
|
-
return url;
|
|
415
|
-
},
|
|
416
|
-
async validateAuthorizationCode(data) {
|
|
417
|
-
const tokens = await validateAuthorizationCode({
|
|
418
|
-
code: data.code,
|
|
419
|
-
codeVerifier: data.codeVerifier,
|
|
420
|
-
redirectURI: data.redirectURI,
|
|
421
|
-
options: {
|
|
422
|
-
clientId: method.clientId,
|
|
423
|
-
clientSecret: method.clientSecret,
|
|
424
|
-
redirectURI: method.redirectURI,
|
|
425
|
-
},
|
|
426
|
-
tokenEndpoint: await resolveTokenEndpoint(method),
|
|
427
|
-
authentication: method.authentication,
|
|
428
|
-
resource: method.resource,
|
|
429
|
-
});
|
|
430
|
-
return applyDefaultAccessTokenExpiry(tokens, undefined);
|
|
431
|
-
},
|
|
432
|
-
async refreshAccessToken(refreshTokenValue) {
|
|
433
|
-
const tokens = await refreshAccessToken({
|
|
434
|
-
refreshToken: refreshTokenValue,
|
|
435
|
-
options: {
|
|
436
|
-
clientId: method.clientId,
|
|
437
|
-
clientSecret: method.clientSecret,
|
|
438
|
-
},
|
|
439
|
-
tokenEndpoint: await resolveTokenEndpoint(method),
|
|
440
|
-
authentication: method.authentication,
|
|
441
|
-
extraParams: { resource: method.resource },
|
|
442
|
-
});
|
|
443
|
-
return applyDefaultAccessTokenExpiry(tokens, undefined);
|
|
444
|
-
},
|
|
445
|
-
};
|
|
446
|
-
}),
|
|
447
|
-
},
|
|
448
|
-
};
|
|
449
|
-
},
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
355
|
/** @internal exported for provider-option mapping tests. */
|
|
453
356
|
export function buildOAuthProviderOptions(config) {
|
|
454
357
|
return {
|
|
455
358
|
loginPage: config.loginPage,
|
|
456
359
|
consentPage: config.consentPage,
|
|
457
360
|
...(config.scopes ? { scopes: [...config.scopes] } : {}),
|
|
458
|
-
...(config.
|
|
459
|
-
? {
|
|
361
|
+
...(config.resources
|
|
362
|
+
? { resources: [...config.resources] }
|
|
363
|
+
: {}),
|
|
364
|
+
...(config.mcpResource
|
|
365
|
+
? {
|
|
366
|
+
clientRegistrationClientSecretExpiration: "90d",
|
|
367
|
+
allowPublicClientPrelogin: true,
|
|
368
|
+
}
|
|
460
369
|
: {}),
|
|
461
370
|
...(config.allowDynamicClientRegistration !== undefined
|
|
462
371
|
? { allowDynamicClientRegistration: config.allowDynamicClientRegistration }
|
|
@@ -500,7 +409,9 @@ function buildAuth(config) {
|
|
|
500
409
|
const bootstrap = config.bootstrapOwner;
|
|
501
410
|
const emailOtpMethod = pickSingleton(config.methods, "email-otp");
|
|
502
411
|
const magicLinkMethod = pickSingleton(config.methods, "magic-link");
|
|
503
|
-
const
|
|
412
|
+
const providerOptions = config.oauthProvider
|
|
413
|
+
? buildOAuthProviderOptions(config.oauthProvider)
|
|
414
|
+
: null;
|
|
504
415
|
// Rate limit: Better Auth's per-route limits gate on
|
|
505
416
|
// `process.env.NODE_ENV === "production"`, which is unset on
|
|
506
417
|
// Cloudflare Workers — leaving the limits silently off. When any
|
|
@@ -533,17 +444,33 @@ function buildAuth(config) {
|
|
|
533
444
|
...(genericOAuthProviders.length > 0
|
|
534
445
|
? [
|
|
535
446
|
genericOAuth({
|
|
536
|
-
config: genericOAuthProviders
|
|
447
|
+
config: genericOAuthProviders,
|
|
537
448
|
}),
|
|
538
449
|
]
|
|
539
450
|
: []),
|
|
540
|
-
...(resourceLifecyclePlugin ? [resourceLifecyclePlugin] : []),
|
|
541
451
|
...(emailOtpMethod ? [buildEmailOTPPlugin(emailOtpMethod)] : []),
|
|
542
452
|
...(magicLinkMethod ? [buildMagicLinkPlugin(magicLinkMethod)] : []),
|
|
543
|
-
...(config.oauthProvider
|
|
453
|
+
...(config.oauthProvider && providerOptions
|
|
544
454
|
? [
|
|
545
455
|
jwt(),
|
|
546
|
-
|
|
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
|
+
: []),
|
|
547
474
|
]
|
|
548
475
|
: []),
|
|
549
476
|
];
|
|
@@ -638,47 +565,63 @@ function buildAuth(config) {
|
|
|
638
565
|
databaseHooks,
|
|
639
566
|
});
|
|
640
567
|
}
|
|
641
|
-
const MEMBER_CURSOR_PREFIX = "m:";
|
|
642
|
-
/** @internal Exported for endpoint and regression-test validation. */
|
|
643
|
-
export function encodeMemberCursor(createdAt, id) {
|
|
644
|
-
return `${MEMBER_CURSOR_PREFIX}${encodeURIComponent(JSON.stringify([createdAt, id]))}`;
|
|
645
|
-
}
|
|
646
|
-
/** @internal Exported for endpoint and regression-test validation. */
|
|
647
|
-
export function decodeMemberCursor(cursor) {
|
|
648
|
-
if (!cursor.startsWith(MEMBER_CURSOR_PREFIX))
|
|
649
|
-
return null;
|
|
650
|
-
try {
|
|
651
|
-
const value = JSON.parse(decodeURIComponent(cursor.slice(MEMBER_CURSOR_PREFIX.length)));
|
|
652
|
-
return Array.isArray(value) && value.length === 2 &&
|
|
653
|
-
typeof value[0] === "string" && !Number.isNaN(Date.parse(value[0])) &&
|
|
654
|
-
typeof value[1] === "string" && value[1]
|
|
655
|
-
? [value[0], value[1]]
|
|
656
|
-
: null;
|
|
657
|
-
}
|
|
658
|
-
catch {
|
|
659
|
-
return null;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
568
|
const SETUP_INCOMPLETE_AUTHS = new WeakSet();
|
|
663
569
|
/** True only for the fail-closed facade returned by createSetupIncompleteAuth. */
|
|
664
570
|
export function isSetupIncompleteAuth(auth) {
|
|
665
571
|
return SETUP_INCOMPLETE_AUTHS.has(auth);
|
|
666
572
|
}
|
|
573
|
+
const LEGACY_DCR_TTL_MS = 90 * 24 * 60 * 60 * 1_000;
|
|
574
|
+
const LEGACY_DCR_CLEANUP_INTERVAL_MS = 60 * 60 * 1_000;
|
|
667
575
|
export function createAuth(config) {
|
|
668
576
|
const auth = buildAuth(config);
|
|
669
577
|
const basePath = normalizeAuthBasePath(config.basePath);
|
|
670
578
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
671
579
|
const api = auth.api;
|
|
672
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
|
+
};
|
|
673
589
|
const verifyAccessToken = config.oauthProvider
|
|
674
590
|
? async (token, audience) => {
|
|
675
591
|
const context = await auth.$context;
|
|
676
592
|
return verifyOAuthJwtWithLocalJwks(token, audience, context.baseURL, async () => api.getJwks(), localJwksCacheKey);
|
|
677
593
|
}
|
|
678
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
|
+
};
|
|
679
613
|
return {
|
|
680
614
|
basePath,
|
|
681
|
-
|
|
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
|
+
},
|
|
682
625
|
getSession: (request) => api.getSession({ headers: request.headers }).then((r) => r ?? null),
|
|
683
626
|
getUserRole: async (userId) => {
|
|
684
627
|
const row = await config.database
|
|
@@ -709,9 +652,76 @@ export function createAuth(config) {
|
|
|
709
652
|
createdAt,
|
|
710
653
|
};
|
|
711
654
|
},
|
|
712
|
-
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
|
+
},
|
|
713
669
|
verifyOAuthAccessToken: async (tokenOrRequest, options) => {
|
|
714
|
-
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;
|
|
715
725
|
},
|
|
716
726
|
methods: config.methods.map((m) => {
|
|
717
727
|
switch (m.kind) {
|
|
@@ -870,7 +880,7 @@ export function createAuth(config) {
|
|
|
870
880
|
: {}),
|
|
871
881
|
...(input.grantTypes ? { grant_types: [...input.grantTypes] } : {}),
|
|
872
882
|
...(input.responseTypes ? { response_types: [...input.responseTypes] } : {}),
|
|
873
|
-
...(input.
|
|
883
|
+
...(input.applicationType ? { application_type: input.applicationType } : {}),
|
|
874
884
|
...(input.skipConsent !== undefined ? { skip_consent: input.skipConsent } : {}),
|
|
875
885
|
...(input.enableEndSession !== undefined
|
|
876
886
|
? { enable_end_session: input.enableEndSession }
|
|
@@ -909,6 +919,10 @@ export function createSetupIncompleteAuth(options = {}) {
|
|
|
909
919
|
status: 401,
|
|
910
920
|
reason: "invalid-token",
|
|
911
921
|
}),
|
|
922
|
+
getOAuthConsentRequest: async () => null,
|
|
923
|
+
completeOAuthConsent: async () => {
|
|
924
|
+
throw new Error(message);
|
|
925
|
+
},
|
|
912
926
|
methods: [],
|
|
913
927
|
listLinkedAccounts: async () => [],
|
|
914
928
|
unlinkAccount: async () => false,
|
|
@@ -926,21 +940,12 @@ export function createSetupIncompleteAuth(options = {}) {
|
|
|
926
940
|
SETUP_INCOMPLETE_AUTHS.add(auth);
|
|
927
941
|
return auth;
|
|
928
942
|
}
|
|
929
|
-
function bearerTokenFrom(tokenOrRequest) {
|
|
930
|
-
if (typeof tokenOrRequest === "string")
|
|
931
|
-
return tokenOrRequest.trim() || null;
|
|
932
|
-
const header = tokenOrRequest.headers.get("authorization");
|
|
933
|
-
if (!header)
|
|
934
|
-
return null;
|
|
935
|
-
const match = /^Bearer ([^\s]+)$/i.exec(header);
|
|
936
|
-
return match?.[1] ?? null;
|
|
937
|
-
}
|
|
938
943
|
/** @internal exported to pin the session-bound Better Auth request and
|
|
939
944
|
* secret-minimizing response mapping. */
|
|
940
|
-
export async function getProviderAccessTokenForRequest(api, request, providerId) {
|
|
945
|
+
export async function getProviderAccessTokenForRequest(api, request, accountId, providerId) {
|
|
941
946
|
const value = (await api.getAccessToken({
|
|
942
947
|
headers: request.headers,
|
|
943
|
-
body: {
|
|
948
|
+
body: { accountId },
|
|
944
949
|
}));
|
|
945
950
|
if (typeof value?.accessToken !== "string") {
|
|
946
951
|
throw new Error(`getProviderAccessToken: provider '${providerId}' returned no access token.`);
|
|
@@ -973,8 +978,10 @@ export function verifyOAuthJwtWithLocalJwks(token, audience, issuer, jwksFetch,
|
|
|
973
978
|
}
|
|
974
979
|
/** @internal exported to pin the stable facade's normalization and
|
|
975
980
|
* 401/403 contract independently of Better Auth network/JWKS I/O. */
|
|
976
|
-
export async function verifyOAuthJwt(tokenOrRequest, options, verify) {
|
|
977
|
-
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;
|
|
978
985
|
// JWT compact serialization has exactly three non-empty parts.
|
|
979
986
|
// Reject opaque tokens before any network/JWKS work.
|
|
980
987
|
if (!verify ||
|
|
@@ -985,6 +992,16 @@ export async function verifyOAuthJwt(tokenOrRequest, options, verify) {
|
|
|
985
992
|
}
|
|
986
993
|
try {
|
|
987
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
|
+
});
|
|
988
1005
|
if (typeof claims["sub"] !== "string" || claims["sub"].length === 0) {
|
|
989
1006
|
return { ok: false, status: 401, reason: "invalid-token" };
|
|
990
1007
|
}
|
|
@@ -1006,7 +1023,10 @@ export async function verifyOAuthJwt(tokenOrRequest, options, verify) {
|
|
|
1006
1023
|
scopes,
|
|
1007
1024
|
};
|
|
1008
1025
|
}
|
|
1009
|
-
catch {
|
|
1026
|
+
catch (error) {
|
|
1027
|
+
if (isDpopBindingError(error)) {
|
|
1028
|
+
return { ok: false, status: 401, reason: "invalid-dpop-proof" };
|
|
1029
|
+
}
|
|
1010
1030
|
return { ok: false, status: 401, reason: "invalid-token" };
|
|
1011
1031
|
}
|
|
1012
1032
|
}
|
|
@@ -1018,7 +1038,7 @@ export function mapRegisteredOAuthClient(value) {
|
|
|
1018
1038
|
}
|
|
1019
1039
|
return {
|
|
1020
1040
|
clientId: row.client_id,
|
|
1021
|
-
...(row.client_secret && row.
|
|
1041
|
+
...(row.client_secret && row.token_endpoint_auth_method !== "none"
|
|
1022
1042
|
? { clientSecret: row.client_secret }
|
|
1023
1043
|
: {}),
|
|
1024
1044
|
redirectUris: row.redirect_uris,
|
|
@@ -1028,8 +1048,7 @@ export function mapRegisteredOAuthClient(value) {
|
|
|
1028
1048
|
...(row.token_endpoint_auth_method
|
|
1029
1049
|
? { tokenEndpointAuthMethod: row.token_endpoint_auth_method }
|
|
1030
1050
|
: {}),
|
|
1031
|
-
...(row.
|
|
1032
|
-
...(row.public !== undefined ? { public: row.public } : {}),
|
|
1051
|
+
...(row.application_type ? { applicationType: row.application_type } : {}),
|
|
1033
1052
|
};
|
|
1034
1053
|
}
|
|
1035
1054
|
/** Random 32-char alphanumeric id, shaped like Better Auth's own user
|