@cloudflare/workers-oauth-provider 1.1.0 → 1.2.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.
@@ -1,5 +1,126 @@
1
1
  import { WorkerEntrypoint } from "cloudflare:workers";
2
2
 
3
+ //#region src/oauth-error.d.ts
4
+
5
+ /**
6
+ * The error your code throws to the library: from a `tokenExchangeCallback`, or from an
7
+ * `OAuthResourceServer`'s `validateToken`, to answer with a standard OAuth error response instead
8
+ * of a generic failure. Its own module so both hosts can use it without importing the package entry.
9
+ */
10
+ /**
11
+ * The internal reason behind an error response, forwarded to the `onError` hook and never
12
+ * placed on the wire. `category` is a stable kebab-case subsystem (`client-authentication`,
13
+ * `authorization-code-grant`, `refresh-token-grant`, `token-exchange-grant`,
14
+ * `token-endpoint-request`, `token-revocation`, `token-issuance`, `resource-indicator`,
15
+ * `client-registration`, `client-id-metadata-document`, `protected-resource`,
16
+ * `enterprise-managed-authorization`, `token-exchange-callback`); `reason` is a stable
17
+ * snake_case slug naming the exact check that failed. Treat both like enum members in semver.
18
+ * `detail` may carry structured context such as a caught error; it is never a secret.
19
+ */
20
+ interface OAuthErrorInternal {
21
+ /** Stable kebab-case subsystem that produced the error. */
22
+ category: string;
23
+ /** Stable snake_case slug naming the failed check, often more specific than the wire description. */
24
+ reason: string;
25
+ /** Optional structured context, e.g. the caught error or the offending parameter. */
26
+ detail?: unknown;
27
+ }
28
+ /**
29
+ * Options accepted by the {@link OAuthError} constructor.
30
+ */
31
+ interface OAuthErrorOptions {
32
+ /**
33
+ * Human-readable text returned in the `error_description` field.
34
+ */
35
+ description: string;
36
+ /**
37
+ * HTTP status code for the error response. Defaults to `400`.
38
+ */
39
+ statusCode?: number;
40
+ /**
41
+ * Additional response headers.
42
+ *
43
+ * For transient failures (e.g. upstream rate limits), set
44
+ * `Retry-After` here so well-behaved clients back off instead of
45
+ * retry-storming. Per RFC 7231 §7.1.3 the value may be either a
46
+ * number of seconds or an HTTP-date.
47
+ */
48
+ headers?: Record<string, string>;
49
+ /**
50
+ * Internal reason forwarded to the `onError` hook and never sent on the wire. The library
51
+ * sets it on every error it originates; a `tokenExchangeCallback` may set its own. An
52
+ * `OAuthError` thrown without one reaches `onError` as
53
+ * `{ category: 'token-exchange-callback', reason: 'callback_error', detail: error }`.
54
+ */
55
+ internal?: OAuthErrorInternal;
56
+ /**
57
+ * For `insufficient_scope` from an `OAuthResourceServer`'s `validateToken`: every scope the
58
+ * operation needs, named in the `403` challenge. Defaults to the resource's `requiredScopes`.
59
+ */
60
+ requiredScopes?: string[];
61
+ }
62
+ /**
63
+ * The OAuth error your code throws to the library, to answer with a standard OAuth error instead
64
+ * of a generic failure. It is honoured in two places:
65
+ *
66
+ * - **`tokenExchangeCallback`** (token endpoint): the response is `{ error, error_description }`
67
+ * with `statusCode` (default `400`) and `headers`. `invalid_grant` also revokes the grant.
68
+ * Anything else thrown is a `500`.
69
+ * - **`OAuthResourceServer`'s `validateToken`**: `invalid_token` is a `401` and
70
+ * `insufficient_scope` a `403`, each with a Bearer challenge (the latter naming
71
+ * `requiredScopes`); any other code keeps `statusCode` and `headers`, such as `429` with
72
+ * `Retry-After`. Anything else thrown is a `503`.
73
+ *
74
+ * Unexpected failures stay visible that way: the library doesn't catch everything and return 400.
75
+ * (`OAuthProvider`'s `resolveExternalToken` uses `ExternalTokenError` instead.)
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * import { OAuthError } from '@cloudflare/workers-oauth-provider';
80
+ *
81
+ * tokenExchangeCallback: async (options) => {
82
+ * if (options.grantType === 'refresh_token') {
83
+ * // refreshUpstream() may throw OAuthError from any depth
84
+ * return { newProps: await refreshUpstream(options.props) };
85
+ * }
86
+ * }
87
+ *
88
+ * async function refreshUpstream(props) {
89
+ * const res = await fetch(...);
90
+ * if (res.status === 401) {
91
+ * // invalid_grant can never recover: the provider also revokes this grant and its tokens.
92
+ * throw new OAuthError('invalid_grant', { description: 'upstream refresh token is invalid' });
93
+ * }
94
+ * if (res.status === 429) {
95
+ * // Mirror upstream's Retry-After if present, otherwise pick a default.
96
+ * throw new OAuthError('temporarily_unavailable', {
97
+ * description: 'upstream rate limited',
98
+ * statusCode: 429,
99
+ * headers: { 'Retry-After': res.headers.get('retry-after') ?? '60' },
100
+ * });
101
+ * }
102
+ * return await res.json();
103
+ * }
104
+ * ```
105
+ */
106
+ declare class OAuthError extends Error {
107
+ /** OAuth 2.0 error code. */
108
+ readonly code: string;
109
+ /** Options controlling the OAuth error response. */
110
+ readonly options: OAuthErrorOptions & {
111
+ statusCode: number;
112
+ };
113
+ /** Human-readable description sent in the `error_description` field. */
114
+ readonly description: string;
115
+ /** HTTP status code for the error response. */
116
+ readonly statusCode: number;
117
+ /** Additional response headers. */
118
+ readonly headers?: Record<string, string>;
119
+ /** Scopes an `insufficient_scope` challenge names. */
120
+ readonly requiredScopes?: string[];
121
+ constructor(code: string, options: OAuthErrorOptions);
122
+ }
123
+ //#endregion
3
124
  //#region src/ema/result.d.ts
4
125
 
5
126
  /**
@@ -289,7 +410,7 @@ interface EmaOptions<Env = Cloudflare.Env> {
289
410
  * Set to `true` to also accept public clients on this grant — for example
290
411
  * clients registered via a Client ID Metadata Document (CIMD), which are
291
412
  * always public (`none`) and therefore cannot present a client secret. The
292
- * security trade-off is documented in the README: the trust then rests on
413
+ * security trade-off is documented in docs/advanced-configuration.md: the trust then rests on
293
414
  * the IdP-issued, signature-verified, short-lived, single-use ID-JAG
294
415
  * assertion (audience- and client-bound), together with the provider's
295
416
  * configured resource pinning, rather than on a separately presented client
@@ -310,6 +431,23 @@ interface AuthorizationErrorOptions {
310
431
  /** Authorization server issuer for RFC 9207 error responses. */
311
432
  issuer?: string;
312
433
  }
434
+ /**
435
+ * The OAuth error redirect back to a client (RFC 6749 §4.1.2.1): `error`, an optional
436
+ * `error_description`, the client's `state`, and `iss` (RFC 9207). Pass only a request the
437
+ * library validated, from `parseAuthRequest()`, `finishUpstream()` or `approveConsent()`,
438
+ * never one rebuilt from user input.
439
+ *
440
+ * ```ts
441
+ * if (new URL(req.url).searchParams.get('error')) {
442
+ * return Response.redirect(authorizationErrorRedirect(original, 'access_denied'), 302);
443
+ * }
444
+ * ```
445
+ */
446
+ declare function authorizationErrorRedirect(request: {
447
+ redirectUri: string;
448
+ state?: string;
449
+ issuer?: string;
450
+ }, code: AuthorizationErrorCode, description?: string): string;
313
451
  /**
314
452
  * Expected authorization-request validation failure. Absence of `redirectUri`
315
453
  * means a caller MUST render locally and MUST NOT redirect.
@@ -320,9 +458,13 @@ declare class AuthorizationError extends Error {
320
458
  readonly redirectUri?: string;
321
459
  readonly state?: string;
322
460
  readonly issuer?: string;
461
+ /**
462
+ * The ready-made error redirect back to the client, set only when a redirect is safe
463
+ * (`redirectUri` was validated). Without it, render the error locally.
464
+ */
465
+ readonly redirectTo?: string;
323
466
  constructor(code: AuthorizationErrorCode, options: AuthorizationErrorOptions);
324
467
  }
325
- declare function isValidOAuthScopeToken(scopeToken: string): boolean;
326
468
  //#endregion
327
469
  //#region src/oauth-resource-server.d.ts
328
470
  /** RFC 9728 metadata published by a standalone OAuth resource server. */
@@ -331,7 +473,10 @@ interface OAuthResourceMetadata {
331
473
  resource: string;
332
474
  /** Authorization server issuers that can issue tokens for this resource. */
333
475
  authorization_servers: string[];
334
- /** Minimal scopes used to access the protected resource. */
476
+ /**
477
+ * Minimal scopes used to access the protected resource.
478
+ * @deprecated Use `requiredScopes`, which this becomes on the wire. Setting both is refused.
479
+ */
335
480
  scopes_supported?: string[];
336
481
  /** Bearer-token presentation methods. This implementation supports only `header`. */
337
482
  bearer_methods_supported?: string[];
@@ -398,6 +543,18 @@ type OAuthResourceHandler<Env, Props> = {
398
543
  interface OAuthResourceServerOptions<Env = Cloudflare.Env, Props = unknown> {
399
544
  /** RFC 9728 metadata, including this server's one canonical resource. */
400
545
  resourceMetadata: OAuthResourceMetadata;
546
+ /**
547
+ * The scopes required for accessing this resource (MCP: the minimal set for basic use). Published
548
+ * as its protected resource metadata's `scopes_supported` and named in the `401` challenge, so
549
+ * clients request them first; not the authorization server's catalogue, `scopesSupported`.
550
+ * Operations that need more ask for it with `insufficientScope()` (step-up). `offline_access` is
551
+ * removed automatically. Leave unset when your consent page picks the scopes.
552
+ *
553
+ * Advertised, not enforced: the handler decides whether a token is sufficient (`ctx.auth.scope`),
554
+ * because only it knows the deployment's scope hierarchy (a broader scope implying a narrower
555
+ * one), which MCP requires servers to honour.
556
+ */
557
+ requiredScopes?: string[];
401
558
  /** Application handler for the canonical resource URL and its path descendants. */
402
559
  handler: OAuthResourceHandler<Env, Props>;
403
560
  /**
@@ -409,7 +566,15 @@ interface OAuthResourceServerOptions<Env = Cloudflare.Env, Props = unknown> {
409
566
  * `(env) => env.AUTH_SERVER.validateToken`
410
567
  * - The authorization server in this Worker:
411
568
  * `(env) => (resource, token) => authorizationServer.validateToken(resource, token, env)`
412
- * - Anything else, at your own risk: a function that validates the token for `resource`.
569
+ * - Anything else, at your own risk: a function that validates the token for `resource`,
570
+ * such as one that recognises an upstream API's credentials and validates them itself
571
+ * before falling back to the authorization server.
572
+ *
573
+ * Return `null` for a token that isn't valid here (a `401` challenge). Throw an `OAuthError`
574
+ * for a specific answer: `temporarily_unavailable` with `statusCode: 429` and a `Retry-After`
575
+ * header, `insufficient_scope` with `requiredScopes`, or `invalid_token` with a description.
576
+ * Anything else thrown is a validator failure (`503`). An `OAuthError` thrown in another
577
+ * Worker arrives over RPC as a plain `Error`, so throw it in this Worker's validator.
413
578
  */
414
579
  validateToken(env: Env, request: Request): OAuthResourceTokenValidator<Props>;
415
580
  }
@@ -454,6 +619,37 @@ interface RememberConsentOptions {
454
619
  */
455
620
  subject?: string;
456
621
  }
622
+ /**
623
+ * What a consent page must show, per the MCP authorization spec and security best practices.
624
+ * Every string here may come from the client (dynamic registration or a CIMD document), so
625
+ * escape all of it before rendering.
626
+ */
627
+ interface ConsentDescription {
628
+ clientId: string;
629
+ /** The client's name, or its client ID when it gave none. Self-asserted unless `clientDomain` is set. */
630
+ clientName: string;
631
+ /**
632
+ * For a Client ID Metadata Document client: the hostname of its HTTPS client ID, a domain the
633
+ * client controls. Show it prominently. Absent for registered clients, whose names are unverified.
634
+ */
635
+ clientDomain?: string;
636
+ clientUri?: string;
637
+ logoUri?: string;
638
+ /** Where the tokens will be sent. */
639
+ redirectUri: string;
640
+ /**
641
+ * The redirect URI's hostname, which the page MUST display. A native app's private-use URI
642
+ * (`com.example.app:/callback`) has no host, so it's the whole URI instead.
643
+ */
644
+ redirectHost: string;
645
+ /**
646
+ * The redirect goes to a local app (`localhost`, `127.0.0.0/8`, `::1`). The page SHOULD warn:
647
+ * any local process could be listening, whatever name the client claims.
648
+ */
649
+ redirectIsLoopback: boolean;
650
+ /** The scopes being requested. */
651
+ scope: string[];
652
+ }
457
653
  /** A consent page to render: post `handle` back to {@link approveConsent}; send `headers` with the page. */
458
654
  interface ConsentTransaction {
459
655
  handle: string;
@@ -483,19 +679,6 @@ interface ResumedUpstream<Data = unknown> {
483
679
  headers: Headers;
484
680
  }
485
681
  //#endregion
486
- //#region src/oauth-resource.d.ts
487
- /** Validate an RFC 3986-safe HTTP(S) resource identifier for RFC 8707. */
488
- declare function validateResourceUri(uri: string): boolean;
489
- /**
490
- * Whether a requested resource identifies a granted or configured resource.
491
- * Only ASCII case in the URI scheme and host is ignored, and an empty path is
492
- * equivalent to `/` (RFC 3986 §6.2.3), so a client that round-trips
493
- * `https://example.com` through a URL parser as `https://example.com/` still
494
- * names it. Port, path, query, a trailing slash after a path segment, user
495
- * information, and every other byte remain significant.
496
- */
497
- declare function resourceMatches(requested: string, granted: string): boolean;
498
- //#endregion
499
682
  //#region src/oauth-provider.d.ts
500
683
  /**
501
684
  * Enum representing OAuth grant types
@@ -552,19 +735,19 @@ interface TokenExchangeCallbackResult {
552
735
  */
553
736
  accessTokenTTL?: number;
554
737
  /**
555
- * Override the default refresh token TTL (time-to-live) for this specific grant.
556
- * Value should be in seconds.
557
- * Note: This is only honored during authorization code exchange. Returning it during
558
- * refresh token exchange is rejected with `invalid_request`; to extend a grant on
559
- * refresh, return `refreshTokenIdleTTL`.
738
+ * Override the default refresh token TTL (time-to-live) for this specific grant, in seconds:
739
+ * `0` issues no refresh token, anything else must be an integer of at least 60. `undefined`
740
+ * keeps the provider's `refreshTokenTTL`; it never means "no expiry".
741
+ * Applies at authorization code exchange only and is ignored for other grant types, so one
742
+ * callback can return it for every grant; to extend a grant on refresh, return
743
+ * `refreshTokenIdleTTL`.
560
744
  */
561
745
  refreshTokenTTL?: number;
562
746
  /**
563
747
  * Idle lifetime for this grant's refresh token, in seconds from this refresh. The
564
- * grant then expires that long after this refresh unless it is refreshed again. Only
565
- * honored during refresh token exchange; returning it for another grant type is
566
- * rejected with `invalid_request`. Overrides the provider's `refreshTokenIdleTTL`
567
- * for this refresh.
748
+ * grant then expires that long after this refresh unless it is refreshed again. Applies to
749
+ * refresh token exchange only and is ignored for other grant types. Overrides the provider's
750
+ * `refreshTokenIdleTTL` for this refresh.
568
751
  *
569
752
  * Useful when the Worker is itself an OAuth client and has just rotated an upstream
570
753
  * refresh token: return the upstream lifetime and the grant lives exactly as long as
@@ -588,7 +771,7 @@ interface TokenExchangeCallbackResult {
588
771
  /**
589
772
  * Options for token exchange callback functions
590
773
  */
591
- interface TokenExchangeCallbackOptions {
774
+ interface TokenExchangeCallbackOptions<Env = Cloudflare.Env> {
592
775
  /**
593
776
  * The type of grant being processed.
594
777
  */
@@ -631,6 +814,11 @@ interface TokenExchangeCallbackOptions {
631
814
  * Application-specific properties currently associated with this grant
632
815
  */
633
816
  props: any;
817
+ /**
818
+ * The Worker's environment for this request, so the callback can reach secrets and bindings
819
+ * (an upstream provider's client secret, say) without the provider being rebuilt per request.
820
+ */
821
+ env: Env;
634
822
  }
635
823
  /**
636
824
  * Options for the client registration callback (RFC 7591).
@@ -728,7 +916,10 @@ interface OAuthProtectedResourceMetadata {
728
916
  * issuer.
729
917
  */
730
918
  authorization_servers?: string[];
731
- /** Minimal scopes required for basic protected-resource functionality. */
919
+ /**
920
+ * Minimal scopes required for basic protected-resource functionality.
921
+ * @deprecated Use `requiredScopes`, which this becomes on the wire. Setting both is refused.
922
+ */
732
923
  scopes_supported?: string[];
733
924
  /** Methods by which bearer tokens can be presented. Defaults to `["header"]`. */
734
925
  bearer_methods_supported?: string[];
@@ -800,6 +991,7 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
800
991
  * Defaults to 30 days (2,592,000 seconds).
801
992
  * Set to 0 to disable refresh tokens entirely.
802
993
  * Set to `undefined` explicitly for refresh tokens that never expire.
994
+ * Anything else must be an integer of at least 60 seconds (Cloudflare KV's minimum expiration window).
803
995
  * For example: 3600 = 1 hour, 2592000 = 30 days
804
996
  */
805
997
  refreshTokenTTL?: number;
@@ -820,27 +1012,16 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
820
1012
  * Defaults to 90 days (7,776,000 seconds).
821
1013
  * Clients created via the DCR endpoint will automatically expire after this duration.
822
1014
  * Clients created via `OAuthHelpers.createClient()` are not affected by this setting.
823
- * Set to `undefined` explicitly for clients that never expire.
1015
+ * Set to `undefined` explicitly for clients that never expire; otherwise an integer of at
1016
+ * least 60 seconds (Cloudflare KV's minimum expiration window).
824
1017
  */
825
1018
  clientRegistrationTTL?: number;
826
1019
  /**
827
1020
  * Scopes supported by the authorization server.
828
- * These are advertised only in authorization server metadata; configure
829
- * `resourceMetadata.scopes_supported` separately for protected resource requirements.
1021
+ * These are advertised only in authorization server metadata; configure a resource's
1022
+ * required scopes separately, with `requiredScopes`.
830
1023
  */
831
1024
  scopesSupported?: string[];
832
- /**
833
- * Controls whether the OAuth implicit flow is allowed.
834
- * This flow is discouraged in OAuth 2.1 due to security concerns.
835
- * Defaults to false.
836
- */
837
- allowImplicitFlow?: boolean;
838
- /**
839
- * Controls whether the legacy plain PKCE method is allowed.
840
- * Defaults to false so PKCE challenges use S256 exclusively.
841
- * Set to true only for compatibility with clients that cannot use S256.
842
- */
843
- allowPlainPKCE?: boolean;
844
1025
  /**
845
1026
  * Controls whether OAuth 2.0 Token Exchange (RFC 8693) is allowed.
846
1027
  * When false, the token exchange grant type will not be advertised in metadata
@@ -878,7 +1059,7 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
878
1059
  * The callback can return new props values that will be stored with the token or grant.
879
1060
  * If the callback returns nothing or undefined for a props field, the original props will be used.
880
1061
  */
881
- tokenExchangeCallback?: (options: TokenExchangeCallbackOptions) => Promise<TokenExchangeCallbackResult | void> | TokenExchangeCallbackResult | void;
1062
+ tokenExchangeCallback?: (options: TokenExchangeCallbackOptions<Env>) => Promise<TokenExchangeCallbackResult | void> | TokenExchangeCallbackResult | void;
882
1063
  /**
883
1064
  * Optional callback called when a provided bearer credential was not found
884
1065
  * in the internal KV. It can validate an external OAuth access token, opaque
@@ -916,6 +1097,12 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
916
1097
  internal: OAuthErrorInternal;
917
1098
  request?: Request;
918
1099
  }) => Response | void;
1100
+ /**
1101
+ * Accept RFC 8252 private-use URI scheme redirect URIs (for example `com.example.app:/oauth`)
1102
+ * for native apps. By default redirect URIs must use `https`, or `http` on a loopback host, as
1103
+ * MCP and OAuth 2.1 require; remote `http` is never accepted. Leave this off for MCP servers.
1104
+ */
1105
+ allowPrivateUseRedirectUris?: boolean;
919
1106
  /**
920
1107
  * Explicitly enable Client ID Metadata Document (CIMD) support.
921
1108
  * When true, URL-formatted client_ids will be fetched as metadata documents.
@@ -934,6 +1121,18 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
934
1121
  * Controls the response served at /.well-known/oauth-protected-resource.
935
1122
  */
936
1123
  resourceMetadata: OAuthProtectedResourceMetadata;
1124
+ /**
1125
+ * The scopes required for accessing this resource (MCP: the minimal set for basic use). Published
1126
+ * as its protected resource metadata's `scopes_supported` and named in the `401` challenge, so
1127
+ * clients request them first; not the authorization server's catalogue, `scopesSupported`.
1128
+ * Operations that need more ask for it with `insufficientScope()` (step-up). `offline_access` is
1129
+ * removed automatically. Leave unset when your consent page picks the scopes.
1130
+ *
1131
+ * Advertised, not enforced: the handler decides whether a token is sufficient (`ctx.auth.scope`),
1132
+ * because only it knows the deployment's scope hierarchy (a broader scope implying a narrower
1133
+ * one), which MCP requires servers to honour.
1134
+ */
1135
+ requiredScopes?: string[];
937
1136
  }
938
1137
  /**
939
1138
  * Functional authorization-server surface for the role-based API.
@@ -941,9 +1140,20 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
941
1140
  * `getOAuthApi()` to parse and complete it; `fetch()` serves protocol-owned AS
942
1141
  * endpoints such as metadata, token, revocation, and optional registration.
943
1142
  */
944
- type OAuthAuthorizationServerOptions<Env = Cloudflare.Env> = Omit<OAuthProviderOptions<Env>, 'apiRoute' | 'apiHandler' | 'apiHandlers' | 'defaultHandler' | 'resourceMetadata' | 'resolveExternalToken'> & {
1143
+ type OAuthAuthorizationServerOptions<Env = Cloudflare.Env> = Omit<OAuthProviderOptions<Env>, 'apiRoute' | 'apiHandler' | 'apiHandlers' | 'defaultHandler' | 'authorizeEndpoint' | 'tokenEndpoint' | 'resourceMetadata' | 'requiredScopes' | 'resolveExternalToken'> & {
945
1144
  /** Canonical RFC 8414 issuer. */
946
1145
  issuer: string;
1146
+ /**
1147
+ * Your authorization endpoint, advertised in metadata. Route it before calling `fetch()`;
1148
+ * `parseAuthRequest()` rejects requests that arrive anywhere else, so a mismatch fails on the
1149
+ * first request. Defaults to `${issuer}/authorize`. A path is on the issuer's origin.
1150
+ */
1151
+ authorizeEndpoint?: string;
1152
+ /**
1153
+ * The token endpoint, which also handles revocation, served by `fetch()` and advertised in
1154
+ * metadata. Defaults to `${issuer}/oauth/token`. A path is on the issuer's origin.
1155
+ */
1156
+ tokenEndpoint?: string;
947
1157
  /**
948
1158
  * Canonical identifiers of every protected resource this authorization server issues
949
1159
  * tokens for, whether hosted in this Worker through `OAuthResourceServer` or by another
@@ -1006,6 +1216,13 @@ interface OAuthHelpers {
1006
1216
  completeAuthorization(options: CompleteAuthorizationOptions): Promise<{
1007
1217
  redirectTo: string;
1008
1218
  }>;
1219
+ /**
1220
+ * The facts a consent page must show for a request: the client's name (and, for a Client ID
1221
+ * Metadata Document client, its verified domain), the redirect URI's hostname, whether it goes
1222
+ * to a local app, and the scopes. Every string may come from the client: escape it.
1223
+ * @throws CimdFetchError when the client's metadata document cannot be resolved
1224
+ */
1225
+ describeConsent(authRequest: AuthRequest): Promise<ConsentDescription>;
1009
1226
  /**
1010
1227
  * Whether an approval remembered by `approveConsent(…, { remember })` covers this request: the
1011
1228
  * same client, redirect URI and resource, asking for a subset of the approved scopes. Pass the
@@ -1066,16 +1283,28 @@ interface OAuthHelpers {
1066
1283
  */
1067
1284
  listClients(options?: ListOptions): Promise<ListResult<ClientInfo>>;
1068
1285
  /**
1069
- * Updates an existing OAuth client
1286
+ * Updates an existing OAuth client. Redirect URIs, grant types and response types are held to
1287
+ * the same rules as registration. Client ID Metadata Document clients can't be updated here:
1288
+ * their document owns their metadata.
1070
1289
  * @param clientId - The ID of the client to update
1071
1290
  * @param updates - Partial client information with fields to update
1072
1291
  * @returns A Promise resolving to the updated client info, or null if not found
1292
+ * @throws Error when an update breaks the redirect URI policy or names an unsupported grant or response type
1293
+ * @throws TypeError for a Client ID Metadata Document client while CIMD is enabled
1073
1294
  */
1074
1295
  updateClient(clientId: string, updates: Partial<ClientInfo>): Promise<ClientInfo | null>;
1075
1296
  /**
1076
- * Deletes an OAuth client
1297
+ * Deletes an OAuth client, then revokes its grants and tokens across all users. The client
1298
+ * can no longer authenticate, authorize or refresh as soon as the call starts. Access tokens
1299
+ * it already holds stay valid until revocation reaches them or they expire (`accessTokenTTL`):
1300
+ * validating an access token reads only the token record, never the client. If revocation is
1301
+ * cut short (for example by a subrequest limit on a very large namespace), the call throws;
1302
+ * call it again to continue, and `purgeExpiredData()` also removes the leftovers, which are
1303
+ * orphaned grants by then.
1304
+ * For a Client ID Metadata Document client there is no stored record: this revokes its grants,
1305
+ * and the client can authorize again as long as its document resolves.
1077
1306
  * @param clientId - The ID of the client to delete
1078
- * @returns A Promise resolving when the deletion is confirmed.
1307
+ * @returns A Promise resolving when the client and all its grants are gone.
1079
1308
  */
1080
1309
  deleteClient(clientId: string): Promise<void>;
1081
1310
  /**
@@ -1279,7 +1508,9 @@ interface CompleteAuthorizationOptions {
1279
1508
  */
1280
1509
  request: AuthRequest;
1281
1510
  /**
1282
- * Identifier for the user granting the authorization
1511
+ * Identifier for the user granting the authorization. Must be non-empty and must not contain
1512
+ * `:`, which separates the parts of issued tokens and storage keys; encode namespaced or
1513
+ * composite IDs first (for example `encodeURIComponent('tenant:user')`).
1283
1514
  */
1284
1515
  userId: string;
1285
1516
  /**
@@ -1302,15 +1533,6 @@ interface CompleteAuthorizationOptions {
1302
1533
  * Set to false to allow multiple concurrent grants per user+client.
1303
1534
  */
1304
1535
  revokeExistingGrants?: boolean;
1305
- /**
1306
- * How many grants written by a version before 1.0 are read at once while
1307
- * revoking existing grants. Grants written by 1.0 or later carry their client,
1308
- * resource and redirect URI as KV key metadata and are matched from `list()`
1309
- * without being read, so this only bounds the fan-out for older grants. Only
1310
- * used when revokeExistingGrants is not false. Must be a positive integer;
1311
- * values above 1000 are clamped. Defaults to 50.
1312
- */
1313
- revokeExistingGrantsBatchSize?: number;
1314
1536
  }
1315
1537
  /**
1316
1538
  * Authorization grant record
@@ -1467,7 +1689,8 @@ interface Token extends TokenBase {
1467
1689
  */
1468
1690
  grant: {
1469
1691
  /**
1470
- * Client that received this grant
1692
+ * Client this token was issued to: the grant's client, or, for a token from an allowed
1693
+ * cross-client token exchange, the client that requested it (RFC 8693).
1471
1694
  */
1472
1695
  clientId: string;
1473
1696
  /**
@@ -1490,7 +1713,8 @@ interface TokenSummary<T = any> extends TokenBase {
1490
1713
  */
1491
1714
  grant: {
1492
1715
  /**
1493
- * Client that received this grant
1716
+ * Client this token was issued to: the grant's client, or, for a token from an allowed
1717
+ * cross-client token exchange, the client that requested it (RFC 8693).
1494
1718
  */
1495
1719
  clientId: string;
1496
1720
  /**
@@ -1562,6 +1786,12 @@ interface PurgeOptions {
1562
1786
  * Defaults to true.
1563
1787
  */
1564
1788
  purgeOrphanedTokens?: boolean;
1789
+ /**
1790
+ * Where to resume: the `cursor` from the previous invocation's result. Omit it to start
1791
+ * a new sweep. Persist the returned cursor between invocations (for example in KV), or
1792
+ * every invocation re-checks the same first `batchSize` records.
1793
+ */
1794
+ cursor?: string;
1565
1795
  }
1566
1796
  /**
1567
1797
  * Result of a purgeExpiredData garbage collection invocation
@@ -1575,8 +1805,10 @@ interface PurgeResult {
1575
1805
  tokensChecked: number;
1576
1806
  /** Number of token records purged (orphaned) */
1577
1807
  tokensPurged: number;
1578
- /** True if the full key space was scanned in this invocation (both grants and tokens) */
1808
+ /** True once the sweep has covered both key spaces; there is no `cursor` then. */
1579
1809
  done: boolean;
1810
+ /** Pass as `PurgeOptions.cursor` to the next invocation to continue this sweep. Absent when `done`. */
1811
+ cursor?: string;
1580
1812
  }
1581
1813
  /**
1582
1814
  * Public representation of a grant, with sensitive data removed
@@ -1700,108 +1932,6 @@ declare function getOAuthApi<Env = Cloudflare.Env>(options: OAuthProviderOptions
1700
1932
  * Error class for OAuth operations
1701
1933
  * Carries OAuth error code and description for proper error responses
1702
1934
  */
1703
- /**
1704
- * The internal reason behind an error response, forwarded to the `onError` hook and never
1705
- * placed on the wire. `category` is a stable kebab-case subsystem (`client-authentication`,
1706
- * `authorization-code-grant`, `refresh-token-grant`, `token-exchange-grant`,
1707
- * `token-endpoint-request`, `token-revocation`, `token-issuance`, `resource-indicator`,
1708
- * `client-registration`, `client-id-metadata-document`, `protected-resource`,
1709
- * `enterprise-managed-authorization`, `token-exchange-callback`); `reason` is a stable
1710
- * snake_case slug naming the exact check that failed. Treat both like enum members in semver.
1711
- * `detail` may carry structured context such as a caught error; it is never a secret.
1712
- */
1713
- interface OAuthErrorInternal {
1714
- /** Stable kebab-case subsystem that produced the error. */
1715
- category: string;
1716
- /** Stable snake_case slug naming the failed check, often more specific than the wire description. */
1717
- reason: string;
1718
- /** Optional structured context, e.g. the caught error or the offending parameter. */
1719
- detail?: unknown;
1720
- }
1721
- /**
1722
- * Options accepted by the {@link OAuthError} constructor.
1723
- */
1724
- interface OAuthErrorOptions {
1725
- /**
1726
- * Human-readable text returned in the `error_description` field.
1727
- */
1728
- description: string;
1729
- /**
1730
- * HTTP status code for the error response. Defaults to `400`.
1731
- */
1732
- statusCode?: number;
1733
- /**
1734
- * Additional response headers.
1735
- *
1736
- * For transient failures (e.g. upstream rate limits), set
1737
- * `Retry-After` here so well-behaved clients back off instead of
1738
- * retry-storming. Per RFC 7231 §7.1.3 the value may be either a
1739
- * number of seconds or an HTTP-date.
1740
- */
1741
- headers?: Record<string, string>;
1742
- /**
1743
- * Internal reason forwarded to the `onError` hook and never sent on the wire. The library
1744
- * sets it on every error it originates; a `tokenExchangeCallback` may set its own. An
1745
- * `OAuthError` thrown without one reaches `onError` as
1746
- * `{ category: 'token-exchange-callback', reason: 'callback_error', detail: error }`.
1747
- */
1748
- internal?: OAuthErrorInternal;
1749
- }
1750
- /**
1751
- * Structured OAuth 2.0 token-endpoint error.
1752
- *
1753
- * Throw from a `tokenExchangeCallback` or any code it calls to surface a
1754
- * standard OAuth token response (`{ error, error_description }`) instead of a
1755
- * generic `500 Internal Server Error`.
1756
- *
1757
- * Anything thrown that is **not** an `OAuthError` continues to surface as
1758
- * a 500 so unexpected failures remain visible — the provider does not
1759
- * catch-everything-and-return-400.
1760
- *
1761
- * @example
1762
- * ```ts
1763
- * import { OAuthError } from '@cloudflare/workers-oauth-provider';
1764
- *
1765
- * tokenExchangeCallback: async (options) => {
1766
- * if (options.grantType === 'refresh_token') {
1767
- * // refreshUpstream() may throw OAuthError from any depth
1768
- * return { newProps: await refreshUpstream(options.props) };
1769
- * }
1770
- * }
1771
- *
1772
- * async function refreshUpstream(props) {
1773
- * const res = await fetch(...);
1774
- * if (res.status === 401) {
1775
- * // invalid_grant can never recover: the provider also revokes this grant and its tokens.
1776
- * throw new OAuthError('invalid_grant', { description: 'upstream refresh token is invalid' });
1777
- * }
1778
- * if (res.status === 429) {
1779
- * // Mirror upstream's Retry-After if present, otherwise pick a default.
1780
- * throw new OAuthError('temporarily_unavailable', {
1781
- * description: 'upstream rate limited',
1782
- * statusCode: 429,
1783
- * headers: { 'Retry-After': res.headers.get('retry-after') ?? '60' },
1784
- * });
1785
- * }
1786
- * return await res.json();
1787
- * }
1788
- * ```
1789
- */
1790
- declare class OAuthError extends Error {
1791
- /** OAuth 2.0 error code. */
1792
- readonly code: string;
1793
- /** Options controlling the OAuth error response. */
1794
- readonly options: OAuthErrorOptions & {
1795
- statusCode: number;
1796
- };
1797
- /** Human-readable description sent in the `error_description` field. */
1798
- readonly description: string;
1799
- /** HTTP status code for the error response. */
1800
- readonly statusCode: number;
1801
- /** Additional response headers. */
1802
- readonly headers?: Record<string, string>;
1803
- constructor(code: string, options: OAuthErrorOptions);
1804
- }
1805
1935
  /** Options accepted by the {@link ExternalTokenError} constructor. */
1806
1936
  interface ExternalTokenErrorOptions {
1807
1937
  /**
@@ -1878,20 +2008,5 @@ declare class CimdFetchError extends Error {
1878
2008
  */
1879
2009
  constructor(metadataUrl: string, cause: unknown);
1880
2010
  }
1881
- /**
1882
- * Decodes a base64url-encoded string to bytes.
1883
- */
1884
- declare function base64UrlToBytes(base64Url: string): Uint8Array;
1885
- /**
1886
- * Parses a base64url-encoded JWT JSON part into an object.
1887
- */
1888
- declare function parseJwtJsonPart(encoded: string): Record<string, unknown>;
1889
- /**
1890
- * Gets WebCrypto import and verify parameters for supported JOSE algorithms.
1891
- */
1892
- declare function getJwtCryptoAlgorithms(alg: string): {
1893
- importAlgorithm: Parameters<SubtleCrypto['importKey']>[2];
1894
- verifyAlgorithm: Parameters<SubtleCrypto['verify']>[0];
1895
- };
1896
2011
  //#endregion
1897
- export { type ApprovedConsent, AuthRequest, AuthorizationError, type AuthorizationErrorCode, type AuthorizationErrorOptions, AuthorizationServerBinding, CimdFetchError, ClientInfo, ClientRegistrationCallbackOptions, ClientRegistrationCallbackResult, CompleteAuthorizationOptions, type ConsentTransaction, type DeniedConsent, type EmaClaimsMapper, type EmaClaimsMapperInput, type EmaClaimsMapperResult, type EmaIdJagClaims, type EmaOptions, type EmaTrustedIssuer, type EmaTrustedIssuerResolver, type EmaTrustedIssuerResolverInput, type EmaValidationError, ExchangeTokenOptions, ExternalTokenError, ExternalTokenErrorOptions, Grant, GrantSummary, GrantType, ListOptions, ListResult, OAuthAuthorizationServer, OAuthAuthorizationServerOptions, OAuthError, OAuthErrorInternal, OAuthErrorOptions, OAuthHelpers, OAuthProtectedResourceMetadata, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, OAuthResourceAuth, OAuthResourceContext, OAuthResourceHandler, OAuthResourceMetadata, OAuthResourceServer, OAuthResourceServerOptions, OAuthResourceTokenValidation, OAuthResourceTokenValidator, OAuthTokenErrorCode, PurgeOptions, PurgeResult, type RememberConsentOptions, ResolveExternalTokenInput, ResolveExternalTokenResult, type ResumedUpstream, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, type UpstreamTransaction, ValidatedAccessToken, base64UrlToBytes, getJwtCryptoAlgorithms, getOAuthApi, insufficientScope, isValidOAuthScopeToken, parseJwtJsonPart, resourceMatches, validateResourceUri };
2012
+ export { type ApprovedConsent, AuthRequest, AuthorizationError, type AuthorizationErrorCode, type AuthorizationErrorOptions, AuthorizationServerBinding, CimdFetchError, ClientInfo, ClientRegistrationCallbackOptions, ClientRegistrationCallbackResult, CompleteAuthorizationOptions, type ConsentDescription, type ConsentTransaction, type DeniedConsent, type EmaClaimsMapper, type EmaClaimsMapperInput, type EmaClaimsMapperResult, type EmaIdJagClaims, type EmaOptions, type EmaTrustedIssuer, type EmaTrustedIssuerResolver, type EmaTrustedIssuerResolverInput, type EmaValidationError, ExchangeTokenOptions, ExternalTokenError, ExternalTokenErrorOptions, Grant, GrantSummary, GrantType, ListOptions, ListResult, OAuthAuthorizationServer, OAuthAuthorizationServerOptions, OAuthError, type OAuthErrorInternal, type OAuthErrorOptions, OAuthHelpers, OAuthProtectedResourceMetadata, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, OAuthResourceAuth, OAuthResourceContext, OAuthResourceHandler, OAuthResourceMetadata, OAuthResourceServer, OAuthResourceServerOptions, OAuthResourceTokenValidation, OAuthResourceTokenValidator, OAuthTokenErrorCode, PurgeOptions, PurgeResult, type RememberConsentOptions, ResolveExternalTokenInput, ResolveExternalTokenResult, type ResumedUpstream, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, type UpstreamTransaction, ValidatedAccessToken, authorizationErrorRedirect, getOAuthApi, insufficientScope };