@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.
Files changed (93) hide show
  1. package/README.md +15 -8
  2. package/dist/auth/conventionalAuth.d.ts +3 -0
  3. package/dist/auth/conventionalAuth.d.ts.map +1 -1
  4. package/dist/auth/conventionalAuth.js +23 -6
  5. package/dist/auth/conventionalAuth.js.map +1 -1
  6. package/dist/auth/createAuth.d.ts +32 -13
  7. package/dist/auth/createAuth.d.ts.map +1 -1
  8. package/dist/auth/createAuth.js +182 -154
  9. package/dist/auth/createAuth.js.map +1 -1
  10. package/dist/bindings/AssetsAssetServer.d.ts +4 -4
  11. package/dist/bindings/AssetsAssetServer.d.ts.map +1 -1
  12. package/dist/bindings/AssetsAssetServer.js +2 -2
  13. package/dist/bindings/D1DatabaseDriver.d.ts +3 -4
  14. package/dist/bindings/D1DatabaseDriver.d.ts.map +1 -1
  15. package/dist/bindings/D1DatabaseDriver.js +4 -91
  16. package/dist/bindings/D1DatabaseDriver.js.map +1 -1
  17. package/dist/bindings/WorkersQueueHookDispatcher.d.ts +2 -2
  18. package/dist/bindings/WorkersQueueHookDispatcher.d.ts.map +1 -1
  19. package/dist/bindings/WorkersQueueHookDispatcher.js +1 -1
  20. package/dist/bindings/WorkersQueueHookDispatcher.js.map +1 -1
  21. package/dist/bindings/conventionalBindings.d.ts +2 -3
  22. package/dist/bindings/conventionalBindings.d.ts.map +1 -1
  23. package/dist/bindings/conventionalBindings.js +1 -3
  24. package/dist/bindings/conventionalBindings.js.map +1 -1
  25. package/dist/handlers/turnstile.d.ts +1 -1
  26. package/dist/handlers/turnstile.d.ts.map +1 -1
  27. package/dist/handlers/turnstile.js +3 -1
  28. package/dist/handlers/turnstile.js.map +1 -1
  29. package/dist/mount/bootRuntimeOnce.d.ts +15 -8
  30. package/dist/mount/bootRuntimeOnce.d.ts.map +1 -1
  31. package/dist/mount/bootRuntimeOnce.js +36 -21
  32. package/dist/mount/bootRuntimeOnce.js.map +1 -1
  33. package/dist/mount/cmsConfig.d.ts +17 -9
  34. package/dist/mount/cmsConfig.d.ts.map +1 -1
  35. package/dist/mount/index.d.ts +4 -4
  36. package/dist/mount/index.d.ts.map +1 -1
  37. package/dist/mount/index.js +3 -3
  38. package/dist/mount/index.js.map +1 -1
  39. package/dist/mount/mountAdmin.d.ts +6 -0
  40. package/dist/mount/mountAdmin.d.ts.map +1 -0
  41. package/dist/mount/mountAdmin.js +16 -0
  42. package/dist/mount/mountAdmin.js.map +1 -0
  43. package/dist/mount/mountMcp.d.ts +7 -10
  44. package/dist/mount/mountMcp.d.ts.map +1 -1
  45. package/dist/mount/mountMcp.js +57 -49
  46. package/dist/mount/mountMcp.js.map +1 -1
  47. package/dist/mount/mountPublicRoutes.d.ts +4 -4
  48. package/dist/mount/mountPublicRoutes.d.ts.map +1 -1
  49. package/dist/mount/mountPublicRoutes.js +31 -24
  50. package/dist/mount/mountPublicRoutes.js.map +1 -1
  51. package/dist/mount/mountRuntimeEndpoints.d.ts +7 -0
  52. package/dist/mount/mountRuntimeEndpoints.d.ts.map +1 -0
  53. package/dist/mount/mountRuntimeEndpoints.js +49 -0
  54. package/dist/mount/mountRuntimeEndpoints.js.map +1 -0
  55. package/dist/mount/resolveCaller.d.ts.map +1 -1
  56. package/dist/mount/resolveCaller.js +0 -33
  57. package/dist/mount/resolveCaller.js.map +1 -1
  58. package/dist/oauth/cachePolicy.d.ts.map +1 -1
  59. package/dist/oauth/cachePolicy.js +2 -4
  60. package/dist/oauth/cachePolicy.js.map +1 -1
  61. package/dist/oauth/consentHtml.d.ts +2 -2
  62. package/dist/oauth/consentHtml.d.ts.map +1 -1
  63. package/dist/oauth/consentHtml.js +5 -5
  64. package/dist/oauth/consentHtml.js.map +1 -1
  65. package/dist/oauth/index.d.ts +0 -2
  66. package/dist/oauth/index.d.ts.map +1 -1
  67. package/dist/oauth/index.js +0 -2
  68. package/dist/oauth/index.js.map +1 -1
  69. package/dist/oauth/mountOAuth.d.ts +2 -16
  70. package/dist/oauth/mountOAuth.d.ts.map +1 -1
  71. package/dist/oauth/mountOAuth.js +24 -99
  72. package/dist/oauth/mountOAuth.js.map +1 -1
  73. package/dist/worker/createMantleWorker.d.ts +12 -13
  74. package/dist/worker/createMantleWorker.d.ts.map +1 -1
  75. package/dist/worker/createMantleWorker.js +31 -24
  76. package/dist/worker/createMantleWorker.js.map +1 -1
  77. package/package.json +12 -11
  78. package/dist/mount/mountServerEndpoints.d.ts +0 -9
  79. package/dist/mount/mountServerEndpoints.d.ts.map +0 -1
  80. package/dist/mount/mountServerEndpoints.js +0 -1625
  81. package/dist/mount/mountServerEndpoints.js.map +0 -1
  82. package/dist/mount/runMantleUseCase.d.ts +0 -3
  83. package/dist/mount/runMantleUseCase.d.ts.map +0 -1
  84. package/dist/mount/runMantleUseCase.js +0 -41
  85. package/dist/mount/runMantleUseCase.js.map +0 -1
  86. package/dist/oauth/oauthConstants.d.ts +0 -15
  87. package/dist/oauth/oauthConstants.d.ts.map +0 -1
  88. package/dist/oauth/oauthConstants.js +0 -15
  89. package/dist/oauth/oauthConstants.js.map +0 -1
  90. package/dist/oauth/oauthSingleton.d.ts +0 -33
  91. package/dist/oauth/oauthSingleton.d.ts.map +0 -1
  92. package/dist/oauth/oauthSingleton.js +0 -39
  93. package/dist/oauth/oauthSingleton.js.map +0 -1
@@ -1,12 +1,16 @@
1
1
  import { betterAuth } from "better-auth";
2
- import { applyDefaultAccessTokenExpiry, refreshAccessToken, validateAuthorizationCode, verifyJwsAccessToken, } from "better-auth/oauth2";
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 === "/oauth2/callback/:providerId" &&
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.validAudiences
451
- ? { validAudiences: [...config.validAudiences] }
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 resourceLifecyclePlugin = buildOAuthResourceLifecyclePlugin(config.methods);
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.map(({ displayName: _displayName, ...provider }) => provider),
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
- oauthProvider(buildOAuthProviderOptions(config.oauthProvider)),
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
- handler: async (request) => normalizeAuthResponseCookies(await auth.handler(request)),
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) => getProviderAccessTokenForRequest(api, 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.type ? { type: input.type } : {}),
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: { providerId },
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 token = bearerTokenFrom(tokenOrRequest);
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.public !== true && row.token_endpoint_auth_method !== "none"
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.type ? { type: row.type } : {}),
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