@cloudflare/workers-oauth-provider 1.1.0 → 1.2.1

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,14 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
916
1097
  internal: OAuthErrorInternal;
917
1098
  request?: Request;
918
1099
  }) => Response | void;
1100
+ /**
1101
+ * Let authorization requests use RFC 8252 private-use URI scheme redirect URIs (for example
1102
+ * `com.example.app:/oauth`), for native apps. By default a request's redirect URI must use
1103
+ * `https`, or `http` on a loopback host, as MCP and OAuth 2.1 require; remote `http` is never
1104
+ * accepted. Without this option a client can still register private-use URIs next to a compliant
1105
+ * one (Cursor does), but no request can use them.
1106
+ */
1107
+ allowPrivateUseRedirectUris?: boolean;
919
1108
  /**
920
1109
  * Explicitly enable Client ID Metadata Document (CIMD) support.
921
1110
  * When true, URL-formatted client_ids will be fetched as metadata documents.
@@ -934,6 +1123,18 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
934
1123
  * Controls the response served at /.well-known/oauth-protected-resource.
935
1124
  */
936
1125
  resourceMetadata: OAuthProtectedResourceMetadata;
1126
+ /**
1127
+ * The scopes required for accessing this resource (MCP: the minimal set for basic use). Published
1128
+ * as its protected resource metadata's `scopes_supported` and named in the `401` challenge, so
1129
+ * clients request them first; not the authorization server's catalogue, `scopesSupported`.
1130
+ * Operations that need more ask for it with `insufficientScope()` (step-up). `offline_access` is
1131
+ * removed automatically. Leave unset when your consent page picks the scopes.
1132
+ *
1133
+ * Advertised, not enforced: the handler decides whether a token is sufficient (`ctx.auth.scope`),
1134
+ * because only it knows the deployment's scope hierarchy (a broader scope implying a narrower
1135
+ * one), which MCP requires servers to honour.
1136
+ */
1137
+ requiredScopes?: string[];
937
1138
  }
938
1139
  /**
939
1140
  * Functional authorization-server surface for the role-based API.
@@ -941,9 +1142,20 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
941
1142
  * `getOAuthApi()` to parse and complete it; `fetch()` serves protocol-owned AS
942
1143
  * endpoints such as metadata, token, revocation, and optional registration.
943
1144
  */
944
- type OAuthAuthorizationServerOptions<Env = Cloudflare.Env> = Omit<OAuthProviderOptions<Env>, 'apiRoute' | 'apiHandler' | 'apiHandlers' | 'defaultHandler' | 'resourceMetadata' | 'resolveExternalToken'> & {
1145
+ type OAuthAuthorizationServerOptions<Env = Cloudflare.Env> = Omit<OAuthProviderOptions<Env>, 'apiRoute' | 'apiHandler' | 'apiHandlers' | 'defaultHandler' | 'authorizeEndpoint' | 'tokenEndpoint' | 'resourceMetadata' | 'requiredScopes' | 'resolveExternalToken'> & {
945
1146
  /** Canonical RFC 8414 issuer. */
946
1147
  issuer: string;
1148
+ /**
1149
+ * Your authorization endpoint, advertised in metadata. Route it before calling `fetch()`;
1150
+ * `parseAuthRequest()` rejects requests that arrive anywhere else, so a mismatch fails on the
1151
+ * first request. Defaults to `${issuer}/authorize`. A path is on the issuer's origin.
1152
+ */
1153
+ authorizeEndpoint?: string;
1154
+ /**
1155
+ * The token endpoint, which also handles revocation, served by `fetch()` and advertised in
1156
+ * metadata. Defaults to `${issuer}/oauth/token`. A path is on the issuer's origin.
1157
+ */
1158
+ tokenEndpoint?: string;
947
1159
  /**
948
1160
  * Canonical identifiers of every protected resource this authorization server issues
949
1161
  * tokens for, whether hosted in this Worker through `OAuthResourceServer` or by another
@@ -1006,6 +1218,13 @@ interface OAuthHelpers {
1006
1218
  completeAuthorization(options: CompleteAuthorizationOptions): Promise<{
1007
1219
  redirectTo: string;
1008
1220
  }>;
1221
+ /**
1222
+ * The facts a consent page must show for a request: the client's name (and, for a Client ID
1223
+ * Metadata Document client, its verified domain), the redirect URI's hostname, whether it goes
1224
+ * to a local app, and the scopes. Every string may come from the client: escape it.
1225
+ * @throws CimdFetchError when the client's metadata document cannot be resolved
1226
+ */
1227
+ describeConsent(authRequest: AuthRequest): Promise<ConsentDescription>;
1009
1228
  /**
1010
1229
  * Whether an approval remembered by `approveConsent(…, { remember })` covers this request: the
1011
1230
  * same client, redirect URI and resource, asking for a subset of the approved scopes. Pass the
@@ -1066,16 +1285,28 @@ interface OAuthHelpers {
1066
1285
  */
1067
1286
  listClients(options?: ListOptions): Promise<ListResult<ClientInfo>>;
1068
1287
  /**
1069
- * Updates an existing OAuth client
1288
+ * Updates an existing OAuth client. Redirect URIs, grant types and response types are held to
1289
+ * the same rules as registration. Client ID Metadata Document clients can't be updated here:
1290
+ * their document owns their metadata.
1070
1291
  * @param clientId - The ID of the client to update
1071
1292
  * @param updates - Partial client information with fields to update
1072
1293
  * @returns A Promise resolving to the updated client info, or null if not found
1294
+ * @throws Error when an update breaks the redirect URI policy or names an unsupported grant or response type
1295
+ * @throws TypeError for a Client ID Metadata Document client while CIMD is enabled
1073
1296
  */
1074
1297
  updateClient(clientId: string, updates: Partial<ClientInfo>): Promise<ClientInfo | null>;
1075
1298
  /**
1076
- * Deletes an OAuth client
1299
+ * Deletes an OAuth client, then revokes its grants and tokens across all users. The client
1300
+ * can no longer authenticate, authorize or refresh as soon as the call starts. Access tokens
1301
+ * it already holds stay valid until revocation reaches them or they expire (`accessTokenTTL`):
1302
+ * validating an access token reads only the token record, never the client. If revocation is
1303
+ * cut short (for example by a subrequest limit on a very large namespace), the call throws;
1304
+ * call it again to continue, and `purgeExpiredData()` also removes the leftovers, which are
1305
+ * orphaned grants by then.
1306
+ * For a Client ID Metadata Document client there is no stored record: this revokes its grants,
1307
+ * and the client can authorize again as long as its document resolves.
1077
1308
  * @param clientId - The ID of the client to delete
1078
- * @returns A Promise resolving when the deletion is confirmed.
1309
+ * @returns A Promise resolving when the client and all its grants are gone.
1079
1310
  */
1080
1311
  deleteClient(clientId: string): Promise<void>;
1081
1312
  /**
@@ -1279,7 +1510,9 @@ interface CompleteAuthorizationOptions {
1279
1510
  */
1280
1511
  request: AuthRequest;
1281
1512
  /**
1282
- * Identifier for the user granting the authorization
1513
+ * Identifier for the user granting the authorization. Must be non-empty and must not contain
1514
+ * `:`, which separates the parts of issued tokens and storage keys; encode namespaced or
1515
+ * composite IDs first (for example `encodeURIComponent('tenant:user')`).
1283
1516
  */
1284
1517
  userId: string;
1285
1518
  /**
@@ -1302,15 +1535,6 @@ interface CompleteAuthorizationOptions {
1302
1535
  * Set to false to allow multiple concurrent grants per user+client.
1303
1536
  */
1304
1537
  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
1538
  }
1315
1539
  /**
1316
1540
  * Authorization grant record
@@ -1467,7 +1691,8 @@ interface Token extends TokenBase {
1467
1691
  */
1468
1692
  grant: {
1469
1693
  /**
1470
- * Client that received this grant
1694
+ * Client this token was issued to: the grant's client, or, for a token from an allowed
1695
+ * cross-client token exchange, the client that requested it (RFC 8693).
1471
1696
  */
1472
1697
  clientId: string;
1473
1698
  /**
@@ -1490,7 +1715,8 @@ interface TokenSummary<T = any> extends TokenBase {
1490
1715
  */
1491
1716
  grant: {
1492
1717
  /**
1493
- * Client that received this grant
1718
+ * Client this token was issued to: the grant's client, or, for a token from an allowed
1719
+ * cross-client token exchange, the client that requested it (RFC 8693).
1494
1720
  */
1495
1721
  clientId: string;
1496
1722
  /**
@@ -1562,6 +1788,12 @@ interface PurgeOptions {
1562
1788
  * Defaults to true.
1563
1789
  */
1564
1790
  purgeOrphanedTokens?: boolean;
1791
+ /**
1792
+ * Where to resume: the `cursor` from the previous invocation's result. Omit it to start
1793
+ * a new sweep. Persist the returned cursor between invocations (for example in KV), or
1794
+ * every invocation re-checks the same first `batchSize` records.
1795
+ */
1796
+ cursor?: string;
1565
1797
  }
1566
1798
  /**
1567
1799
  * Result of a purgeExpiredData garbage collection invocation
@@ -1575,8 +1807,10 @@ interface PurgeResult {
1575
1807
  tokensChecked: number;
1576
1808
  /** Number of token records purged (orphaned) */
1577
1809
  tokensPurged: number;
1578
- /** True if the full key space was scanned in this invocation (both grants and tokens) */
1810
+ /** True once the sweep has covered both key spaces; there is no `cursor` then. */
1579
1811
  done: boolean;
1812
+ /** Pass as `PurgeOptions.cursor` to the next invocation to continue this sweep. Absent when `done`. */
1813
+ cursor?: string;
1580
1814
  }
1581
1815
  /**
1582
1816
  * Public representation of a grant, with sensitive data removed
@@ -1700,108 +1934,6 @@ declare function getOAuthApi<Env = Cloudflare.Env>(options: OAuthProviderOptions
1700
1934
  * Error class for OAuth operations
1701
1935
  * Carries OAuth error code and description for proper error responses
1702
1936
  */
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
1937
  /** Options accepted by the {@link ExternalTokenError} constructor. */
1806
1938
  interface ExternalTokenErrorOptions {
1807
1939
  /**
@@ -1878,20 +2010,5 @@ declare class CimdFetchError extends Error {
1878
2010
  */
1879
2011
  constructor(metadataUrl: string, cause: unknown);
1880
2012
  }
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
2013
  //#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 };
2014
+ 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 };