@cloudflare/workers-oauth-provider 0.5.0 → 0.7.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.
package/README.md CHANGED
@@ -117,6 +117,10 @@ export default new OAuthProvider({
117
117
  // Defaults to false.
118
118
  allowTokenExchangeGrant: false,
119
119
 
120
+ // Optional: Experimental MCP Enterprise-Managed Authorization support.
121
+ // When enabled, the token endpoint accepts ID-JAG JWTs with the JWT bearer grant.
122
+ enterpriseManagedAuthorization: undefined,
123
+
120
124
  // Optional: Explicitly enable Client ID Metadata Document (CIMD) support.
121
125
  // When true, URL-formatted client_ids will be fetched as metadata documents.
122
126
  // Requires the 'global_fetch_strictly_public' compatibility flag.
@@ -306,6 +310,88 @@ The `accessTokenTTL` override is particularly useful when the application is als
306
310
 
307
311
  The `props` values are end-to-end encrypted, so they can safely contain sensitive information.
308
312
 
313
+ ### Reporting errors from the callback
314
+
315
+ Throw `OAuthError` from `tokenExchangeCallback` to return a structured OAuth `/token` error (`{ error, error_description }`) instead of a generic 500:
316
+
317
+ ```ts
318
+ import { OAuthError, OAuthProvider } from '@cloudflare/workers-oauth-provider';
319
+
320
+ new OAuthProvider({
321
+ // …
322
+ tokenExchangeCallback: async (options) => {
323
+ if (options.grantType === 'refresh_token') {
324
+ return { newProps: await refreshUpstream(options.props) };
325
+ }
326
+ },
327
+ });
328
+
329
+ async function refreshUpstream(props) {
330
+ const res = await fetch(/* upstream token endpoint */);
331
+
332
+ if (res.status === 401) {
333
+ throw new OAuthError('invalid_grant', {
334
+ description: 'upstream refresh token is invalid',
335
+ });
336
+ }
337
+
338
+ if (res.status === 429) {
339
+ throw new OAuthError('temporarily_unavailable', {
340
+ description: 'upstream rate limited',
341
+ statusCode: 429,
342
+ headers: { 'Retry-After': res.headers.get('retry-after') ?? '60' },
343
+ });
344
+ }
345
+
346
+ return await res.json();
347
+ }
348
+ ```
349
+
350
+ `OAuthError(code, options)` takes:
351
+
352
+ - `code` — OAuth error code returned in the `error` field. This may be a standard code (`OAuthTokenErrorCode`) or an application-defined string.
353
+ - `options.description` — human-readable text returned in `error_description`.
354
+ - `options.statusCode` — HTTP status code (default `400`).
355
+ - `options.headers` — additional response headers, such as `Retry-After` for transient failures. There is no implicit `Retry-After` default for callback-thrown errors.
356
+
357
+ Only `OAuthError` from this package is converted into a structured `/token` response. Plain errors, plain objects with a `code` field, and app-local error classes continue to surface as 500s so unexpected failures stay visible. Import `OAuthError` from `@cloudflare/workers-oauth-provider` rather than copying or re-implementing it.
358
+
359
+ ## Enterprise-Managed Authorization (Experimental)
360
+
361
+ Accepts ID-JAG assertions at `/token` per the [MCP Enterprise-Managed Authorization extension](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization). The enterprise IdP issues an ID-JAG JWT and the MCP client exchanges it here for an opaque access token.
362
+
363
+ ```ts
364
+ new OAuthProvider({
365
+ // ... other options ...
366
+ resourceMetadata: { resource: 'https://mcp.example.com/mcp' },
367
+ enterpriseManagedAuthorization: {
368
+ trustedIssuers: async ({ iss }) =>
369
+ iss === 'https://idp.example.com'
370
+ ? { issuer: iss, jwksUri: 'https://idp.example.com/.well-known/jwks.json', algorithms: ['RS256'] }
371
+ : null,
372
+ async mapClaims({ claims, requestedScope }) {
373
+ return {
374
+ // Opaque tokens use ':' as a separator — encode subjects that may contain it.
375
+ userId: `enterprise-${claims.sub}`,
376
+ scope: requestedScope,
377
+ metadata: { enterpriseIssuer: claims.iss, enterpriseSubject: claims.sub },
378
+ props: { enterprise: true, subject: claims.sub, email: claims.email },
379
+ };
380
+ },
381
+ },
382
+ });
383
+ ```
384
+
385
+ Setup:
386
+
387
+ 1. Configure your IdP as an ID-JAG issuer with this worker's origin as the resource and a public JWKS endpoint.
388
+ 2. Set `resourceMetadata.resource` to the MCP endpoint URL (required when EMA is enabled).
389
+ 3. Implement `trustedIssuers` as a resolver — for multi-tenant deployments it can read `env` / `clientInfo` to look up per-tenant IdP config without redeploying.
390
+
391
+ The AS enforces `resolved.issuer === iss` (confused-deputy guard) and validates ID-JAG `typ`, signature, audience, client binding, resource, `exp` / `iat` / `nbf`, max lifetime, and `jti` replay. Refresh tokens are not issued for this grant — the ID-JAG itself is the renewable assertion.
392
+
393
+ Experimental — the MCP extension is still a draft.
394
+
309
395
  ## Custom Error Responses
310
396
 
311
397
  By using the `onError` option, you can emit notifications or take other actions when an error response was to be emitted:
@@ -388,6 +474,7 @@ This library implements the following OAuth and MCP specifications:
388
474
  - [OAuth 2.0 Protected Resource Metadata (RFC 9728)](https://datatracker.ietf.org/doc/html/rfc9728) — `/.well-known/oauth-protected-resource` discovery endpoint
389
475
  - [OAuth 2.0 Dynamic Client Registration (RFC 7591)](https://datatracker.ietf.org/doc/html/rfc7591) — Dynamic client registration endpoint
390
476
  - [OAuth Client ID Metadata Documents (draft-ietf-oauth-client-id-metadata-document)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-client-id-metadata-document) — HTTPS URLs as client IDs
477
+ - [MCP Enterprise-Managed Authorization extension](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization) — experimental ID-JAG JWT bearer grant support
391
478
 
392
479
  These are the specifications required by the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization).
393
480
 
@@ -1,7 +1,277 @@
1
1
  import { WorkerEntrypoint } from "cloudflare:workers";
2
2
 
3
- //#region src/oauth-provider.d.ts
3
+ //#region src/ema/result.d.ts
4
4
 
5
+ /**
6
+ * Tagged union of every validation failure that can occur on the EMA token
7
+ * endpoint path. Extend by adding a new arm; the exhaustive `switch` in
8
+ * `emaErrorToWire` will surface unhandled cases at compile time.
9
+ */
10
+ type EmaValidationError = {
11
+ reason: 'assertion_missing';
12
+ } | {
13
+ reason: 'assertion_too_large';
14
+ size: number;
15
+ max: number;
16
+ } | {
17
+ reason: 'assertion_malformed';
18
+ } | {
19
+ reason: 'invalid_typ';
20
+ got?: unknown;
21
+ } | {
22
+ reason: 'invalid_alg';
23
+ got?: unknown;
24
+ } | {
25
+ reason: 'issuer_not_trusted';
26
+ iss: string;
27
+ } | {
28
+ reason: 'no_matching_key';
29
+ kid?: string;
30
+ } | {
31
+ reason: 'signature_failed';
32
+ } | {
33
+ reason: 'jwks_fetch_failed';
34
+ status?: number;
35
+ } | {
36
+ reason: 'invalid_claim';
37
+ claim: string;
38
+ } | {
39
+ reason: 'aud_mismatch';
40
+ expected: string;
41
+ got: string | string[];
42
+ } | {
43
+ reason: 'expired';
44
+ exp: number;
45
+ now: number;
46
+ } | {
47
+ reason: 'iat_in_future';
48
+ iat: number;
49
+ now: number;
50
+ skew: number;
51
+ } | {
52
+ reason: 'nbf_in_future';
53
+ nbf: number;
54
+ now: number;
55
+ skew: number;
56
+ } | {
57
+ reason: 'lifetime_too_long';
58
+ lifetime: number;
59
+ max: number;
60
+ } | {
61
+ reason: 'replayed';
62
+ jti: string;
63
+ } | {
64
+ reason: 'client_id_mismatch';
65
+ expected: string;
66
+ got: string;
67
+ } | {
68
+ reason: 'resource_invalid';
69
+ resource: string;
70
+ } | {
71
+ reason: 'resource_mismatch';
72
+ expected: string;
73
+ got: string;
74
+ } | {
75
+ reason: 'invalid_scope_param';
76
+ } | {
77
+ reason: 'invalid_mapped_user';
78
+ } | {
79
+ reason: 'invalid_mapped_scope';
80
+ } | {
81
+ reason: 'invalid_mapped_props';
82
+ } | {
83
+ reason: 'invalid_mapped_ttl';
84
+ } | {
85
+ reason: 'mapper_denied';
86
+ } | {
87
+ reason: 'mapper_threw';
88
+ } | {
89
+ reason: 'assertion_expired_after_processing';
90
+ };
91
+ //#endregion
92
+ //#region src/ema/types.d.ts
93
+ /**
94
+ * Claims expected in an MCP Enterprise-Managed Authorization ID-JAG assertion.
95
+ * Additional issuer-specific claims (e.g. `email`) are preserved verbatim
96
+ * under the index signature.
97
+ */
98
+ interface EmaIdJagClaims {
99
+ /** Identity provider issuer URL. */
100
+ iss: string;
101
+ /** Enterprise subject identifier for the resource owner. */
102
+ sub: string;
103
+ /** Authorization server issuer URL or URLs for which this assertion is intended. */
104
+ aud: string | string[];
105
+ /** RFC 9728 resource identifier of the MCP server. */
106
+ resource: string;
107
+ /** OAuth client identifier this assertion was issued to. */
108
+ client_id: string;
109
+ /** Unique assertion identifier used for replay protection. */
110
+ jti: string;
111
+ /** Assertion expiration time as a Unix timestamp in seconds. */
112
+ exp: number;
113
+ /** Assertion issued-at time as a Unix timestamp in seconds. */
114
+ iat: number;
115
+ /** Optional space-separated OAuth scope string. */
116
+ scope?: string;
117
+ /** Optional email claim supplied by the enterprise IdP. */
118
+ email?: string;
119
+ /** Additional enterprise IdP claims. */
120
+ [claim: string]: unknown;
121
+ }
122
+ /**
123
+ * Trusted enterprise IdP configuration for ID-JAG validation.
124
+ */
125
+ interface EmaTrustedIssuer {
126
+ /** Issuer URL that must exactly match the assertion `iss` claim. */
127
+ issuer: string;
128
+ /** HTTPS JWKS endpoint used to validate assertion signatures. */
129
+ jwksUri: string;
130
+ /** Allowed JWT signing algorithms for this issuer. Defaults to `['RS256']`. */
131
+ algorithms?: string[];
132
+ /** Expected authorization server audience. Defaults to this provider's issuer URL. */
133
+ audience?: string;
134
+ }
135
+ /**
136
+ * Input passed to `enterpriseManagedAuthorization.mapClaims` after ID-JAG validation.
137
+ */
138
+ interface EmaClaimsMapperInput<Env = Cloudflare.Env> {
139
+ /** Validated ID-JAG claims. */
140
+ claims: EmaIdJagClaims;
141
+ /** Authenticated OAuth client that presented the assertion. */
142
+ clientInfo: ClientInfo;
143
+ /** Validated MCP resource identifier from the assertion. */
144
+ resource: string;
145
+ /** Requested scopes after downscoping to the assertion's scope claim, if present. */
146
+ requestedScope: string[];
147
+ /** The original HTTP token request, e.g. for inspecting Host header in multi-tenant routing. */
148
+ request: Request;
149
+ /** Cloudflare Worker environment variables. */
150
+ env: Env;
151
+ }
152
+ /**
153
+ * Result returned by `enterpriseManagedAuthorization.mapClaims`.
154
+ */
155
+ interface EmaClaimsMapperResult {
156
+ /**
157
+ * User ID to associate with the issued grant and access token.
158
+ *
159
+ * Must not contain `:` — the opaque access token format issued by this
160
+ * provider uses `:` as an internal separator, so a `userId` containing
161
+ * it will produce tokens that fail to parse on validation. If the IdP
162
+ * subject may contain `:` (e.g. an email), encode or hash it first
163
+ * (e.g. ``userId: `enterprise-${encodeURIComponent(claims.sub)}` ``).
164
+ */
165
+ userId: string;
166
+ /** Scopes to grant to the issued access token. */
167
+ scope: string[];
168
+ /**
169
+ * Optional grant metadata used for audit and grant listing. This is not
170
+ * encrypted. Stored on the grant in KV alongside the user ID — visible to
171
+ * server-side code via `OAuthHelpers` but never exposed to the MCP client.
172
+ * Use for audit logs, admin UIs, or "list active sessions" features.
173
+ */
174
+ metadata?: unknown;
175
+ /**
176
+ * Application props encrypted into the issued access token and exposed to
177
+ * API handlers on every authenticated request (via `getMcpAuthContext()`
178
+ * for MCP servers, or `ctx.props` in the underlying OAuth helpers). Use
179
+ * for per-request data the protected resource needs without hitting the
180
+ * IdP again — e.g. `enterprise: true`, subject, email, role claims.
181
+ */
182
+ props: unknown;
183
+ /**
184
+ * Optional access token TTL override in seconds. Overrides the AS's
185
+ * configured default. Not clamped to the assertion lifetime — the ID-JAG
186
+ * `exp` governs how long the assertion remains a usable grant, not the
187
+ * lifetime of the access token it mints (RFC 7523 §3).
188
+ */
189
+ accessTokenTTL?: number;
190
+ }
191
+ /**
192
+ * Maps validated enterprise ID-JAG claims to this provider's local user, scopes,
193
+ * metadata, and props. Return `null` to deny token issuance.
194
+ *
195
+ * Always async — mirrors `EmaTrustedIssuerResolver` so the two enterprise
196
+ * callbacks have the same shape and downstream lookups (KV, D1, IdP federation)
197
+ * don't require a later API change to add `Promise<…>` return support.
198
+ */
199
+ type EmaClaimsMapper<Env = Cloudflare.Env> = (input: EmaClaimsMapperInput<Env>) => Promise<EmaClaimsMapperResult | null>;
200
+ /**
201
+ * Input passed to a dynamic `EmaTrustedIssuerResolver`.
202
+ *
203
+ * The `iss` value comes from the assertion's unverified payload — it is
204
+ * used here only as a routing key to choose which IdP's JWKS to load.
205
+ * The cryptographic trust comes from signature verification later, so
206
+ * the resolver may safely treat `iss` as untrusted input.
207
+ */
208
+ interface EmaTrustedIssuerResolverInput<Env = Cloudflare.Env> {
209
+ /** Issuer URL from the assertion's `iss` claim (unverified). */
210
+ iss: string;
211
+ /** Cloudflare Worker environment variables (bindings, secrets, KV, D1). */
212
+ env: Env;
213
+ /** The original HTTP request, e.g. for inspecting the Host header in multi-tenant routing. */
214
+ request: Request;
215
+ /** Authenticated OAuth client that presented the assertion. */
216
+ clientInfo: ClientInfo;
217
+ }
218
+ /**
219
+ * Dynamic resolver for `EmaOptions.trustedIssuers`.
220
+ *
221
+ * Returns the trusted issuer configuration for an incoming `iss` claim,
222
+ * or `null` if the issuer is not trusted for this client / tenant. The
223
+ * returned `issuer` field must equal the input `iss` — the AS enforces
224
+ * this to prevent a resolver from being tricked into returning a config
225
+ * for a different IdP than the one the assertion claims to be from.
226
+ *
227
+ * Useful for B2B platforms where new tenants onboard their own IdPs
228
+ * dynamically and the AS cannot ship a static list at deploy time.
229
+ *
230
+ * **SSRF warning**: `jwksUri` is fetched outbound by the AS. If your
231
+ * resolver reads `jwksUri` from tenant-controlled storage (self-service
232
+ * tenant onboarding, etc.), an attacker who controls a tenant config
233
+ * can point the AS at arbitrary HTTPS endpoints — internal services,
234
+ * cloud metadata endpoints, victim hosts for DoS amplification. The
235
+ * library only enforces `https:`; deployers must validate `jwksUri`
236
+ * against their own allowlist (e.g. registered IdP vendor domains)
237
+ * before storing or returning it.
238
+ */
239
+ type EmaTrustedIssuerResolver<Env = Cloudflare.Env> = (input: EmaTrustedIssuerResolverInput<Env>) => Promise<EmaTrustedIssuer | null>;
240
+ /**
241
+ * MCP Enterprise-Managed Authorization configuration.
242
+ *
243
+ * Presence of this option on `OAuthProviderOptions` enables the EMA grant.
244
+ * There is intentionally no `enabled` flag — forgetting to set it would
245
+ * silently disable the feature despite full configuration.
246
+ */
247
+ interface EmaOptions<Env = Cloudflare.Env> {
248
+ /**
249
+ * Resolver that returns the trusted issuer configuration for an
250
+ * incoming `iss` claim, or `null` to reject the assertion.
251
+ *
252
+ * Always a function — for a fixed list of IdPs, write a one-line closure:
253
+ *
254
+ * ```ts
255
+ * const issuers = [{ issuer: 'https://idp.example.com', jwksUri: '...' }];
256
+ * trustedIssuers: async ({ iss }) => issuers.find((i) => i.issuer === iss) ?? null,
257
+ * ```
258
+ *
259
+ * For B2B / multi-tenant deployments, the resolver can consult `env`,
260
+ * `request`, or `clientInfo` to look up the issuer dynamically (e.g.
261
+ * a per-tenant config in KV / D1) without redeploying.
262
+ */
263
+ trustedIssuers: EmaTrustedIssuerResolver<Env>;
264
+ /** Maps validated enterprise claims to local token data. */
265
+ mapClaims: EmaClaimsMapper<Env>;
266
+ /** JWKS cache TTL in seconds. Defaults to 300 seconds. */
267
+ jwksCacheTtlSeconds?: number;
268
+ /** Allowed clock skew for `exp` and `iat` checks in seconds. Defaults to 60 seconds. */
269
+ clockSkewSeconds?: number;
270
+ /** Maximum accepted assertion lifetime in seconds. Defaults to 300 seconds. */
271
+ maxAssertionLifetimeSeconds?: number;
272
+ }
273
+ //#endregion
274
+ //#region src/oauth-provider.d.ts
5
275
  /**
6
276
  * Enum representing OAuth grant types
7
277
  */
@@ -9,6 +279,7 @@ declare enum GrantType {
9
279
  AUTHORIZATION_CODE = "authorization_code",
10
280
  REFRESH_TOKEN = "refresh_token",
11
281
  TOKEN_EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange",
282
+ JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer",
12
283
  }
13
284
  /**
14
285
  * Aliases for either type of Handler that makes .fetch required
@@ -20,6 +291,16 @@ type WorkerEntrypointWithFetch<Env = Cloudflare.Env> = WorkerEntrypoint<Env> & {
20
291
  /**
21
292
  * Configuration options for the OAuth Provider
22
293
  */
294
+ /**
295
+ * Registered OAuth 2.0 error codes that the `/token` endpoint may return.
296
+ *
297
+ * Union of:
298
+ * - RFC 6749 §5.2 (token endpoint)
299
+ * - RFC 6750 §3.1 (bearer / resource server) — included so callbacks
300
+ * doing audience validation can use them
301
+ * - RFC 8693 §2.2.2 (token exchange)
302
+ */
303
+ type OAuthTokenErrorCode = 'invalid_request' | 'invalid_client' | 'invalid_grant' | 'unauthorized_client' | 'unsupported_grant_type' | 'invalid_scope' | 'invalid_token' | 'insufficient_scope' | 'invalid_target' | 'server_error' | 'temporarily_unavailable';
23
304
  /**
24
305
  * Result of a token exchange callback function.
25
306
  * Allows updating the props stored in both the access token and the grant.
@@ -74,6 +355,14 @@ interface TokenExchangeCallbackOptions {
74
355
  * User who authorized this grant
75
356
  */
76
357
  userId: string;
358
+ /**
359
+ * Identifier of the grant record this callback is operating on. Stable across
360
+ * refreshes for the lifetime of the grant. Pass this together with `userId`
361
+ * to {@link OAuthHelpers.revokeGrant} when the callback decides the grant
362
+ * should be torn down (for example, after an upstream refresh fails with a
363
+ * terminal error code).
364
+ */
365
+ grantId: string;
77
366
  /**
78
367
  * List of scopes that were granted
79
368
  */
@@ -217,6 +506,15 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
217
506
  * Defaults to false.
218
507
  */
219
508
  allowTokenExchangeGrant?: boolean;
509
+ /**
510
+ * Experimental support for the MCP Enterprise-Managed Authorization extension.
511
+ * When enabled, the token endpoint accepts ID-JAG assertions using the JWT bearer
512
+ * grant type (`urn:ietf:params:oauth:grant-type:jwt-bearer`).
513
+ *
514
+ * This feature is opt-in because the MCP extension and underlying OAuth drafts are
515
+ * still evolving. Trusted issuers and a claim mapper are required.
516
+ */
517
+ enterpriseManagedAuthorization?: EmaOptions<Env>;
220
518
  /**
221
519
  * Controls whether public clients (clients without a secret, like SPAs) can register via the
222
520
  * dynamic client registration endpoint. When true, only confidential clients can register.
@@ -245,16 +543,26 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
245
543
  */
246
544
  resolveExternalToken?: (input: ResolveExternalTokenInput) => Promise<ResolveExternalTokenResult | null>;
247
545
  /**
248
- * Optional callback function that is called whenever the OAuthProvider returns an error response
546
+ * Optional callback function that is called whenever the OAuthProvider returns an error response.
249
547
  * This allows the client to emit notifications or perform other actions when an error occurs.
250
548
  *
251
549
  * If the function returns a Response, that will be used in place of the OAuthProvider's default one.
550
+ *
551
+ * `internal` (when present) carries a tagged, server-side-only reason that the library
552
+ * deliberately did NOT put on the wire — used for richer diagnostics where the public
553
+ * response must stay generic (e.g. JWT validation failures on the EMA path). Backwards
554
+ * compatible: existing callbacks ignoring this field continue to work unchanged.
252
555
  */
253
556
  onError?: (error: {
254
557
  code: string;
255
558
  description: string;
256
559
  status: number;
257
560
  headers: Record<string, string>;
561
+ internal?: {
562
+ category: string;
563
+ reason: string;
564
+ detail?: unknown;
565
+ };
258
566
  }) => Response | void;
259
567
  /**
260
568
  * Explicitly enable Client ID Metadata Document (CIMD) support.
@@ -889,5 +1197,114 @@ declare class OAuthProvider<Env = Cloudflare.Env> {
889
1197
  * @returns An instance of OAuthHelpers
890
1198
  */
891
1199
  declare function getOAuthApi<Env = Cloudflare.Env>(options: OAuthProviderOptions<Env>, env: Env): OAuthHelpers;
1200
+ /**
1201
+ * Error class for OAuth operations
1202
+ * Carries OAuth error code and description for proper error responses
1203
+ */
1204
+ /**
1205
+ * Options accepted by the {@link OAuthError} constructor.
1206
+ */
1207
+ interface OAuthErrorOptions {
1208
+ /**
1209
+ * Human-readable text returned in the `error_description` field.
1210
+ */
1211
+ description: string;
1212
+ /**
1213
+ * HTTP status code for the error response. Defaults to `400`.
1214
+ */
1215
+ statusCode?: number;
1216
+ /**
1217
+ * Additional response headers.
1218
+ *
1219
+ * For transient failures (e.g. upstream rate limits), set
1220
+ * `Retry-After` here so well-behaved clients back off instead of
1221
+ * retry-storming. Per RFC 7231 §7.1.3 the value may be either a
1222
+ * number of seconds or an HTTP-date.
1223
+ */
1224
+ headers?: Record<string, string>;
1225
+ }
1226
+ /**
1227
+ * Structured OAuth 2.0 error.
1228
+ *
1229
+ * Throw from a `tokenExchangeCallback` (or any code it calls — the error
1230
+ * propagates naturally up through deep call stacks) to surface a standard
1231
+ * `/token` error response (`{ error, error_description }`) instead of a
1232
+ * generic `500 Internal Server Error`.
1233
+ *
1234
+ * Anything thrown that is **not** an `OAuthError` continues to surface as
1235
+ * a 500 so unexpected failures remain visible — the provider does not
1236
+ * catch-everything-and-return-400.
1237
+ *
1238
+ * @example
1239
+ * ```ts
1240
+ * import { OAuthError } from '@cloudflare/workers-oauth-provider';
1241
+ *
1242
+ * tokenExchangeCallback: async (options) => {
1243
+ * if (options.grantType === 'refresh_token') {
1244
+ * // refreshUpstream() may throw OAuthError from any depth
1245
+ * return { newProps: await refreshUpstream(options.props) };
1246
+ * }
1247
+ * }
1248
+ *
1249
+ * async function refreshUpstream(props) {
1250
+ * const res = await fetch(...);
1251
+ * if (res.status === 401) {
1252
+ * throw new OAuthError('invalid_grant', { description: 'upstream refresh token is invalid' });
1253
+ * }
1254
+ * if (res.status === 429) {
1255
+ * // Mirror upstream's Retry-After if present, otherwise pick a default.
1256
+ * throw new OAuthError('temporarily_unavailable', {
1257
+ * description: 'upstream rate limited',
1258
+ * statusCode: 429,
1259
+ * headers: { 'Retry-After': res.headers.get('retry-after') ?? '60' },
1260
+ * });
1261
+ * }
1262
+ * return await res.json();
1263
+ * }
1264
+ * ```
1265
+ */
1266
+ declare class OAuthError extends Error {
1267
+ /** OAuth 2.0 error code. */
1268
+ readonly code: string;
1269
+ /** Options controlling the OAuth error response. */
1270
+ readonly options: OAuthErrorOptions & {
1271
+ statusCode: number;
1272
+ };
1273
+ /** Human-readable description sent in the `error_description` field. */
1274
+ readonly description: string;
1275
+ /** HTTP status code for the error response. */
1276
+ readonly statusCode: number;
1277
+ /** Additional response headers. */
1278
+ readonly headers?: Record<string, string>;
1279
+ constructor(code: string, options: OAuthErrorOptions);
1280
+ }
1281
+ /**
1282
+ * Validates a resource URI per RFC 8707 Section 2
1283
+ * @param uri - The URI string to validate
1284
+ * @returns true if valid, false otherwise
1285
+ */
1286
+ declare function validateResourceUri(uri: string): boolean;
1287
+ /**
1288
+ * Checks if a requested resource matches a granted resource.
1289
+ * When originOnly is true, compares only the origin (scheme + host + port),
1290
+ * allowing path-aware resources to match origin-only grants.
1291
+ */
1292
+ declare function resourceMatches(requested: string, granted: string, originOnly: boolean): boolean;
1293
+ /**
1294
+ * Decodes a base64url-encoded string to bytes.
1295
+ */
1296
+ declare function base64UrlToBytes(base64Url: string): Uint8Array;
1297
+ /**
1298
+ * Parses a base64url-encoded JWT JSON part into an object.
1299
+ */
1300
+ declare function parseJwtJsonPart(encoded: string): Record<string, unknown>;
1301
+ declare function isValidOAuthScopeToken(scopeToken: string): boolean;
1302
+ /**
1303
+ * Gets WebCrypto import and verify parameters for supported JOSE algorithms.
1304
+ */
1305
+ declare function getJwtCryptoAlgorithms(alg: string): {
1306
+ importAlgorithm: Parameters<SubtleCrypto['importKey']>[2];
1307
+ verifyAlgorithm: Parameters<SubtleCrypto['verify']>[0];
1308
+ };
892
1309
  //#endregion
893
- export { AuthRequest, ClientInfo, CompleteAuthorizationOptions, ExchangeTokenOptions, Grant, GrantSummary, GrantType, ListOptions, ListResult, OAuthHelpers, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, PurgeOptions, PurgeResult, ResolveExternalTokenInput, ResolveExternalTokenResult, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, getOAuthApi };
1310
+ export { AuthRequest, ClientInfo, CompleteAuthorizationOptions, type EmaClaimsMapper, type EmaClaimsMapperInput, type EmaClaimsMapperResult, type EmaIdJagClaims, type EmaOptions, type EmaTrustedIssuer, type EmaTrustedIssuerResolver, type EmaTrustedIssuerResolverInput, type EmaValidationError, ExchangeTokenOptions, Grant, GrantSummary, GrantType, ListOptions, ListResult, OAuthError, OAuthErrorOptions, OAuthHelpers, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, OAuthTokenErrorCode, PurgeOptions, PurgeResult, ResolveExternalTokenInput, ResolveExternalTokenResult, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, base64UrlToBytes, getJwtCryptoAlgorithms, getOAuthApi, isValidOAuthScopeToken, parseJwtJsonPart, resourceMatches, validateResourceUri };