@cloudflare/workers-oauth-provider 0.0.2 → 0.0.4-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
@@ -2,9 +2,9 @@
2
2
 
3
3
  This is a TypeScript library that implements the provider side of the OAuth 2.1 protocol with PKCE support. The library is intended to be used on Cloudflare Workers.
4
4
 
5
- ## EXPERIMENTAL
5
+ ## Beta
6
6
 
7
- As of March, 2025, this library is very new. Please use with caution as additional security reviews are still in progress.
7
+ As of March, 2025, this library is very new, prerelease software. The API is still subject to change.
8
8
 
9
9
  ## Benefits of this library
10
10
 
@@ -43,6 +43,16 @@ export default new OAuthProvider({
43
43
  // You can provide either an object with a fetch method (ExportedHandler)
44
44
  // or a class extending WorkerEntrypoint.
45
45
  apiHandler: ApiHandler, // Using a WorkerEntrypoint class
46
+
47
+ // For multi-handler setups, you can use apiHandlers instead of apiRoute+apiHandler.
48
+ // This allows you to use different handlers for different API routes.
49
+ // Note: You must use either apiRoute+apiHandler (single-handler) OR apiHandlers (multi-handler), not both.
50
+ // Example:
51
+ // apiHandlers: {
52
+ // "/api/users/": UsersApiHandler,
53
+ // "/api/documents/": DocumentsApiHandler,
54
+ // "https://api.example.com/": ExternalApiHandler,
55
+ // },
46
56
 
47
57
  // Any requests which aren't API request will be passed to the default handler instead.
48
58
  // Again, this can be either an object or a WorkerEntrypoint.
@@ -262,11 +272,34 @@ The `accessTokenTTL` override is particularly useful when the application is als
262
272
 
263
273
  The `props` values are end-to-end encrypted, so they can safely contain sensitive information.
264
274
 
265
- ## Written by Claude
275
+ ## Custom Error Responses
266
276
 
267
- This library (including the schema documentation) was largely written by [Claude](https://claude.ai), the AI model by Anthropic. Claude's output was thoroughly reviewed by Cloudflare engineers with careful attention paid to security and compliance with standards. Many improvements were made on the initial output, mostly again by prompting Claude (and reviewing the results). Check out the commit history to see how Claude was prompted and what code it produced.
277
+ By using the `onError` option, you can emit notifications or take other actions when an error response was to be emitted:
268
278
 
269
- (@kentonv, the lead engineer, was actually an AI skeptic, and started this project with the intent to prove that LLMs cannot code. He ended up deciding he had proven himself wrong.)
279
+ ```ts
280
+ new OAuthProvider({
281
+ // ... other options ...
282
+ onError({ code, description, status, headers }) {
283
+ Sentry.captureMessage(/* ... */)
284
+ }
285
+ })
286
+ ```
287
+
288
+ By returning a `Response` you can also override what the OAuthProvider returns to your users:
289
+
290
+ ```ts
291
+ new OAuthProvider({
292
+ // ... other options ...
293
+ onError({ code, description, status, headers }) {
294
+ if (code === 'unsupported_grant_type') {
295
+ return new Response('...', { status, headers })
296
+ }
297
+ // returning undefined (i.e. void) uses the default Response generation
298
+ }
299
+ })
300
+ ```
301
+
302
+ By default, the `onError` callback is set to ``({ status, code, description }) => console.warn(`OAuth error response: ${status} ${code} - ${description}`)``.
270
303
 
271
304
  ## Implementation Notes
272
305
 
@@ -286,3 +319,17 @@ OAuth 2.1 requires that refresh tokens are either "cryptographically bound" to t
286
319
  This requirement is seemingly fundamentally flawed as it assumes that every refresh request will complete with no errors. In the real world, a transient network error, machine failure, or software fault could mean that the client fails to store the new refresh token after a refresh request. In this case, the client would be permanently unable to make any further requests, as the only token it has is no longer valid.
287
320
 
288
321
  This library implements a compromise: At any particular time, a grant may have two valid refresh tokens. When the client uses one of them, the other one is invalidated, and a new one is generated and returned. Thus, if the client correctly uses the new refresh token each time, then older refresh tokens are continuously invalidated. But if a transient failure prevents the client from updating its token, it can always retry the request with the token it used previously.
322
+
323
+ ## Written using Claude
324
+
325
+ This library (including the schema documentation) was largely written with the help of [Claude](https://claude.ai), the AI model by Anthropic. Claude's output was thoroughly reviewed by Cloudflare engineers with careful attention paid to security and compliance with standards. Many improvements were made on the initial output, mostly again by prompting Claude (and reviewing the results). Check out the commit history to see how Claude was prompted and what code it produced.
326
+
327
+ **"NOOOOOOOO!!!! You can't just use an LLM to write an auth library!"**
328
+
329
+ "haha gpus go brrr"
330
+
331
+ In all seriousness, two months ago (January 2025), I ([@kentonv](https://github.com/kentonv)) would have agreed. I was an AI skeptic. I thoughts LLMs were glorified Markov chain generators that didn't actually understand code and couldn't produce anything novel. I started this project on a lark, fully expecting the AI to produce terrible code for me to laugh at. And then, uh... the code actually looked pretty good. Not perfect, but I just told the AI to fix things, and it did. I was shocked.
332
+
333
+ To emphasize, **this is not "vibe coded"**. Every line was thoroughly reviewed and cross-referenced with relevant RFCs, by security experts with previous experience with those RFCs. I was *trying* to validate my skepticism. I ended up proving myself wrong.
334
+
335
+ Again, please check out the commit history -- especially early commits -- to understand how this went.
@@ -67,14 +67,31 @@ interface OAuthProviderOptions {
67
67
  * URL(s) for API routes. Requests with URLs starting with any of these prefixes
68
68
  * will be treated as API requests and require a valid access token.
69
69
  * Can be a single route or an array of routes. Each route can be a full URL or just a path.
70
+ *
71
+ * Used with `apiHandler` for the single-handler configuration. This is incompatible with
72
+ * the `apiHandlers` property. You must use either `apiRoute` + `apiHandler` OR `apiHandlers`, not both.
70
73
  */
71
- apiRoute: string | string[];
74
+ apiRoute?: string | string[];
72
75
  /**
73
76
  * Handler for API requests that have a valid access token.
74
77
  * This handler will receive the authenticated user properties in ctx.props.
75
78
  * Can be either an ExportedHandler object with a fetch method or a class extending WorkerEntrypoint.
79
+ *
80
+ * Used with `apiRoute` for the single-handler configuration. This is incompatible with
81
+ * the `apiHandlers` property. You must use either `apiRoute` + `apiHandler` OR `apiHandlers`, not both.
76
82
  */
77
- apiHandler: ExportedHandlerWithFetch | (new (ctx: ExecutionContext, env: any) => WorkerEntrypointWithFetch);
83
+ apiHandler?: ExportedHandlerWithFetch | (new (ctx: ExecutionContext, env: any) => WorkerEntrypointWithFetch);
84
+ /**
85
+ * Map of API routes to their corresponding handlers for the multi-handler configuration.
86
+ * The keys are the API routes (strings only, not arrays), and the values are the handlers.
87
+ * Each route can be a full URL or just a path, and each handler can be either an ExportedHandler
88
+ * object with a fetch method or a class extending WorkerEntrypoint.
89
+ *
90
+ * This is incompatible with the `apiRoute` and `apiHandler` properties. You must use either
91
+ * `apiRoute` + `apiHandler` (single-handler configuration) OR `apiHandlers` (multi-handler
92
+ * configuration), not both.
93
+ */
94
+ apiHandlers?: Record<string, ExportedHandlerWithFetch | (new (ctx: ExecutionContext, env: any) => WorkerEntrypointWithFetch)>;
78
95
  /**
79
96
  * Handler for all non-API requests or API requests without a valid token.
80
97
  * Can be either an ExportedHandler object with a fetch method or a class extending WorkerEntrypoint.
@@ -128,6 +145,18 @@ interface OAuthProviderOptions {
128
145
  * If the callback returns nothing or undefined for a props field, the original props will be used.
129
146
  */
130
147
  tokenExchangeCallback?: (options: TokenExchangeCallbackOptions) => Promise<TokenExchangeCallbackResult | void> | TokenExchangeCallbackResult | void;
148
+ /**
149
+ * Optional callback function that is called whenever the OAuthProvider returns an error response
150
+ * This allows the client to emit notifications or perform other actions when an error occurs.
151
+ *
152
+ * If the function returns a Response, that will be used in place of the OAuthProvider's default one.
153
+ */
154
+ onError?: (error: {
155
+ code: string;
156
+ description: string;
157
+ status: number;
158
+ headers: Record<string, string>;
159
+ }) => Response | void;
131
160
  }
132
161
  /**
133
162
  * Helper methods for OAuth operations provided to handler functions
@@ -37,14 +37,36 @@ var OAuthProviderImpl = class {
37
37
  * @param options - Configuration options for the provider
38
38
  */
39
39
  constructor(options) {
40
- this.typedApiHandler = this.validateHandler(options.apiHandler, "apiHandler");
40
+ this.typedApiHandlers = /* @__PURE__ */ new Map();
41
+ const hasSingleHandlerConfig = !!(options.apiRoute && options.apiHandler);
42
+ const hasMultiHandlerConfig = !!options.apiHandlers;
43
+ if (hasSingleHandlerConfig && hasMultiHandlerConfig) {
44
+ throw new TypeError(
45
+ "Cannot use both apiRoute/apiHandler and apiHandlers. Use either apiRoute + apiHandler OR apiHandlers, not both."
46
+ );
47
+ }
48
+ if (!hasSingleHandlerConfig && !hasMultiHandlerConfig) {
49
+ throw new TypeError(
50
+ "Must provide either apiRoute + apiHandler OR apiHandlers. No API route configuration provided."
51
+ );
52
+ }
41
53
  this.typedDefaultHandler = this.validateHandler(options.defaultHandler, "defaultHandler");
42
- if (Array.isArray(options.apiRoute)) {
43
- options.apiRoute.forEach((route, index) => {
44
- this.validateEndpoint(route, `apiRoute[${index}]`);
45
- });
54
+ if (hasSingleHandlerConfig) {
55
+ const apiHandler = this.validateHandler(options.apiHandler, "apiHandler");
56
+ if (Array.isArray(options.apiRoute)) {
57
+ options.apiRoute.forEach((route, index) => {
58
+ this.validateEndpoint(route, `apiRoute[${index}]`);
59
+ this.typedApiHandlers.set(route, apiHandler);
60
+ });
61
+ } else {
62
+ this.validateEndpoint(options.apiRoute, "apiRoute");
63
+ this.typedApiHandlers.set(options.apiRoute, apiHandler);
64
+ }
46
65
  } else {
47
- this.validateEndpoint(options.apiRoute, "apiRoute");
66
+ for (const [route, handler] of Object.entries(options.apiHandlers)) {
67
+ this.validateEndpoint(route, `apiHandlers key: ${route}`);
68
+ this.typedApiHandlers.set(route, this.validateHandler(handler, `apiHandlers[${route}]`));
69
+ }
48
70
  }
49
71
  this.validateEndpoint(options.authorizeEndpoint, "authorizeEndpoint");
50
72
  this.validateEndpoint(options.tokenEndpoint, "tokenEndpoint");
@@ -52,8 +74,9 @@ var OAuthProviderImpl = class {
52
74
  this.validateEndpoint(options.clientRegistrationEndpoint, "clientRegistrationEndpoint");
53
75
  }
54
76
  this.options = {
55
- ...options,
56
- accessTokenTTL: options.accessTokenTTL || DEFAULT_ACCESS_TOKEN_TTL
77
+ accessTokenTTL: DEFAULT_ACCESS_TOKEN_TTL,
78
+ onError: ({ status, code, description }) => console.warn(`OAuth error response: ${status} ${code} - ${description}`),
79
+ ...options
57
80
  };
58
81
  }
59
82
  /**
@@ -203,11 +226,25 @@ var OAuthProviderImpl = class {
203
226
  * @returns True if the URL matches any of the API routes
204
227
  */
205
228
  isApiRequest(url) {
206
- if (Array.isArray(this.options.apiRoute)) {
207
- return this.options.apiRoute.some((route) => this.matchApiRoute(url, route));
208
- } else {
209
- return this.matchApiRoute(url, this.options.apiRoute);
229
+ for (const route of this.typedApiHandlers.keys()) {
230
+ if (this.matchApiRoute(url, route)) {
231
+ return true;
232
+ }
233
+ }
234
+ return false;
235
+ }
236
+ /**
237
+ * Finds the appropriate API handler for a URL
238
+ * @param url - The URL to find a handler for
239
+ * @returns The TypedHandler for the URL, or undefined if no handler matches
240
+ */
241
+ findApiHandlerForUrl(url) {
242
+ for (const [route, handler] of this.typedApiHandlers.entries()) {
243
+ if (this.matchApiRoute(url, route)) {
244
+ return handler;
245
+ }
210
246
  }
247
+ return void 0;
211
248
  }
212
249
  /**
213
250
  * Gets the full URL for an endpoint, using the provided request URL's
@@ -298,12 +335,12 @@ var OAuthProviderImpl = class {
298
335
  */
299
336
  async handleTokenRequest(request, env) {
300
337
  if (request.method !== "POST") {
301
- return createErrorResponse("invalid_request", "Method not allowed", 405);
338
+ return this.createErrorResponse("invalid_request", "Method not allowed", 405);
302
339
  }
303
340
  let contentType = request.headers.get("Content-Type") || "";
304
341
  let body = {};
305
342
  if (!contentType.includes("application/x-www-form-urlencoded")) {
306
- return createErrorResponse("invalid_request", "Content-Type must be application/x-www-form-urlencoded", 400);
343
+ return this.createErrorResponse("invalid_request", "Content-Type must be application/x-www-form-urlencoded", 400);
307
344
  }
308
345
  const formData = await request.formData();
309
346
  for (const [key, value] of formData.entries()) {
@@ -322,19 +359,19 @@ var OAuthProviderImpl = class {
322
359
  clientSecret = body.client_secret || "";
323
360
  }
324
361
  if (!clientId) {
325
- return createErrorResponse("invalid_client", "Client ID is required", 401);
362
+ return this.createErrorResponse("invalid_client", "Client ID is required", 401);
326
363
  }
327
364
  const clientInfo = await this.getClient(env, clientId);
328
365
  if (!clientInfo) {
329
- return createErrorResponse("invalid_client", "Client not found", 401);
366
+ return this.createErrorResponse("invalid_client", "Client not found", 401);
330
367
  }
331
368
  const isPublicClient = clientInfo.tokenEndpointAuthMethod === "none";
332
369
  if (!isPublicClient) {
333
370
  if (!clientSecret) {
334
- return createErrorResponse("invalid_client", "Client authentication failed: missing client_secret", 401);
371
+ return this.createErrorResponse("invalid_client", "Client authentication failed: missing client_secret", 401);
335
372
  }
336
373
  if (!clientInfo.clientSecret) {
337
- return createErrorResponse(
374
+ return this.createErrorResponse(
338
375
  "invalid_client",
339
376
  "Client authentication failed: client has no registered secret",
340
377
  401
@@ -342,7 +379,7 @@ var OAuthProviderImpl = class {
342
379
  }
343
380
  const providedSecretHash = await hashSecret(clientSecret);
344
381
  if (providedSecretHash !== clientInfo.clientSecret) {
345
- return createErrorResponse("invalid_client", "Client authentication failed: invalid client_secret", 401);
382
+ return this.createErrorResponse("invalid_client", "Client authentication failed: invalid client_secret", 401);
346
383
  }
347
384
  }
348
385
  const grantType = body.grant_type;
@@ -351,7 +388,7 @@ var OAuthProviderImpl = class {
351
388
  } else if (grantType === "refresh_token") {
352
389
  return this.handleRefreshTokenGrant(body, clientInfo, env);
353
390
  } else {
354
- return createErrorResponse("unsupported_grant_type", "Grant type not supported");
391
+ return this.createErrorResponse("unsupported_grant_type", "Grant type not supported");
355
392
  }
356
393
  }
357
394
  /**
@@ -367,38 +404,38 @@ var OAuthProviderImpl = class {
367
404
  const redirectUri = body.redirect_uri;
368
405
  const codeVerifier = body.code_verifier;
369
406
  if (!code) {
370
- return createErrorResponse("invalid_request", "Authorization code is required");
407
+ return this.createErrorResponse("invalid_request", "Authorization code is required");
371
408
  }
372
409
  const codeParts = code.split(":");
373
410
  if (codeParts.length !== 3) {
374
- return createErrorResponse("invalid_grant", "Invalid authorization code format");
411
+ return this.createErrorResponse("invalid_grant", "Invalid authorization code format");
375
412
  }
376
413
  const [userId, grantId, _] = codeParts;
377
414
  const grantKey = `grant:${userId}:${grantId}`;
378
415
  const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
379
416
  if (!grantData) {
380
- return createErrorResponse("invalid_grant", "Grant not found or authorization code expired");
417
+ return this.createErrorResponse("invalid_grant", "Grant not found or authorization code expired");
381
418
  }
382
419
  if (!grantData.authCodeId) {
383
- return createErrorResponse("invalid_grant", "Authorization code already used");
420
+ return this.createErrorResponse("invalid_grant", "Authorization code already used");
384
421
  }
385
422
  const codeHash = await hashSecret(code);
386
423
  if (codeHash !== grantData.authCodeId) {
387
- return createErrorResponse("invalid_grant", "Invalid authorization code");
424
+ return this.createErrorResponse("invalid_grant", "Invalid authorization code");
388
425
  }
389
426
  if (grantData.clientId !== clientInfo.clientId) {
390
- return createErrorResponse("invalid_grant", "Client ID mismatch");
427
+ return this.createErrorResponse("invalid_grant", "Client ID mismatch");
391
428
  }
392
429
  const isPkceEnabled = !!grantData.codeChallenge;
393
430
  if (!redirectUri && !isPkceEnabled) {
394
- return createErrorResponse("invalid_request", "redirect_uri is required when not using PKCE");
431
+ return this.createErrorResponse("invalid_request", "redirect_uri is required when not using PKCE");
395
432
  }
396
433
  if (redirectUri && !clientInfo.redirectUris.includes(redirectUri)) {
397
- return createErrorResponse("invalid_grant", "Invalid redirect URI");
434
+ return this.createErrorResponse("invalid_grant", "Invalid redirect URI");
398
435
  }
399
436
  if (isPkceEnabled) {
400
437
  if (!codeVerifier) {
401
- return createErrorResponse("invalid_request", "code_verifier is required for PKCE");
438
+ return this.createErrorResponse("invalid_request", "code_verifier is required for PKCE");
402
439
  }
403
440
  let calculatedChallenge;
404
441
  if (grantData.codeChallengeMethod === "S256") {
@@ -411,7 +448,7 @@ var OAuthProviderImpl = class {
411
448
  calculatedChallenge = codeVerifier;
412
449
  }
413
450
  if (calculatedChallenge !== grantData.codeChallenge) {
414
- return createErrorResponse("invalid_grant", "Invalid PKCE code_verifier");
451
+ return this.createErrorResponse("invalid_grant", "Invalid PKCE code_verifier");
415
452
  }
416
453
  }
417
454
  const accessTokenSecret = generateRandomString(TOKEN_LENGTH);
@@ -516,26 +553,26 @@ var OAuthProviderImpl = class {
516
553
  async handleRefreshTokenGrant(body, clientInfo, env) {
517
554
  const refreshToken = body.refresh_token;
518
555
  if (!refreshToken) {
519
- return createErrorResponse("invalid_request", "Refresh token is required");
556
+ return this.createErrorResponse("invalid_request", "Refresh token is required");
520
557
  }
521
558
  const tokenParts = refreshToken.split(":");
522
559
  if (tokenParts.length !== 3) {
523
- return createErrorResponse("invalid_grant", "Invalid token format");
560
+ return this.createErrorResponse("invalid_grant", "Invalid token format");
524
561
  }
525
562
  const [userId, grantId, _] = tokenParts;
526
563
  const providedTokenHash = await generateTokenId(refreshToken);
527
564
  const grantKey = `grant:${userId}:${grantId}`;
528
565
  const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
529
566
  if (!grantData) {
530
- return createErrorResponse("invalid_grant", "Grant not found");
567
+ return this.createErrorResponse("invalid_grant", "Grant not found");
531
568
  }
532
569
  const isCurrentToken = grantData.refreshTokenId === providedTokenHash;
533
570
  const isPreviousToken = grantData.previousRefreshTokenId === providedTokenHash;
534
571
  if (!isCurrentToken && !isPreviousToken) {
535
- return createErrorResponse("invalid_grant", "Invalid refresh token");
572
+ return this.createErrorResponse("invalid_grant", "Invalid refresh token");
536
573
  }
537
574
  if (grantData.clientId !== clientInfo.clientId) {
538
- return createErrorResponse("invalid_grant", "Client ID mismatch");
575
+ return this.createErrorResponse("invalid_grant", "Client ID mismatch");
539
576
  }
540
577
  const accessTokenSecret = generateRandomString(TOKEN_LENGTH);
541
578
  const newAccessToken = `${userId}:${grantId}:${accessTokenSecret}`;
@@ -647,24 +684,24 @@ var OAuthProviderImpl = class {
647
684
  */
648
685
  async handleClientRegistration(request, env) {
649
686
  if (!this.options.clientRegistrationEndpoint) {
650
- return createErrorResponse("not_implemented", "Client registration is not enabled", 501);
687
+ return this.createErrorResponse("not_implemented", "Client registration is not enabled", 501);
651
688
  }
652
689
  if (request.method !== "POST") {
653
- return createErrorResponse("invalid_request", "Method not allowed", 405);
690
+ return this.createErrorResponse("invalid_request", "Method not allowed", 405);
654
691
  }
655
692
  const contentLength = parseInt(request.headers.get("Content-Length") || "0", 10);
656
693
  if (contentLength > 1048576) {
657
- return createErrorResponse("invalid_request", "Request payload too large, must be under 1 MiB", 413);
694
+ return this.createErrorResponse("invalid_request", "Request payload too large, must be under 1 MiB", 413);
658
695
  }
659
696
  let clientMetadata;
660
697
  try {
661
698
  const text = await request.text();
662
699
  if (text.length > 1048576) {
663
- return createErrorResponse("invalid_request", "Request payload too large, must be under 1 MiB", 413);
700
+ return this.createErrorResponse("invalid_request", "Request payload too large, must be under 1 MiB", 413);
664
701
  }
665
702
  clientMetadata = JSON.parse(text);
666
703
  } catch (error) {
667
- return createErrorResponse("invalid_request", "Invalid JSON payload", 400);
704
+ return this.createErrorResponse("invalid_request", "Invalid JSON payload", 400);
668
705
  }
669
706
  const validateStringField = (field) => {
670
707
  if (field === void 0) {
@@ -692,7 +729,7 @@ var OAuthProviderImpl = class {
692
729
  const authMethod = validateStringField(clientMetadata.token_endpoint_auth_method) || "client_secret_basic";
693
730
  const isPublicClient = authMethod === "none";
694
731
  if (isPublicClient && this.options.disallowPublicClientRegistration) {
695
- return createErrorResponse("invalid_client_metadata", "Public client registration is not allowed");
732
+ return this.createErrorResponse("invalid_client_metadata", "Public client registration is not allowed");
696
733
  }
697
734
  const clientId = generateRandomString(16);
698
735
  let clientSecret;
@@ -726,7 +763,7 @@ var OAuthProviderImpl = class {
726
763
  clientInfo.clientSecret = hashedSecret;
727
764
  }
728
765
  } catch (error) {
729
- return createErrorResponse(
766
+ return this.createErrorResponse(
730
767
  "invalid_client_metadata",
731
768
  error instanceof Error ? error.message : "Invalid client metadata"
732
769
  );
@@ -766,14 +803,14 @@ var OAuthProviderImpl = class {
766
803
  async handleApiRequest(request, env, ctx) {
767
804
  const authHeader = request.headers.get("Authorization");
768
805
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
769
- return createErrorResponse("invalid_token", "Missing or invalid access token", 401, {
806
+ return this.createErrorResponse("invalid_token", "Missing or invalid access token", 401, {
770
807
  "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token", error_description="Missing or invalid access token"'
771
808
  });
772
809
  }
773
810
  const accessToken = authHeader.substring(7);
774
811
  const tokenParts = accessToken.split(":");
775
812
  if (tokenParts.length !== 3) {
776
- return createErrorResponse("invalid_token", "Invalid token format", 401, {
813
+ return this.createErrorResponse("invalid_token", "Invalid token format", 401, {
777
814
  "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
778
815
  });
779
816
  }
@@ -782,13 +819,13 @@ var OAuthProviderImpl = class {
782
819
  const tokenKey = `token:${userId}:${grantId}:${accessTokenId}`;
783
820
  const tokenData = await env.OAUTH_KV.get(tokenKey, { type: "json" });
784
821
  if (!tokenData) {
785
- return createErrorResponse("invalid_token", "Invalid access token", 401, {
822
+ return this.createErrorResponse("invalid_token", "Invalid access token", 401, {
786
823
  "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
787
824
  });
788
825
  }
789
826
  const now = Math.floor(Date.now() / 1e3);
790
827
  if (tokenData.expiresAt < now) {
791
- return createErrorResponse("invalid_token", "Access token expired", 401, {
828
+ return this.createErrorResponse("invalid_token", "Access token expired", 401, {
792
829
  "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"'
793
830
  });
794
831
  }
@@ -798,10 +835,15 @@ var OAuthProviderImpl = class {
798
835
  if (!env.OAUTH_PROVIDER) {
799
836
  env.OAUTH_PROVIDER = this.createOAuthHelpers(env);
800
837
  }
801
- if (this.typedApiHandler.type === 0 /* EXPORTED_HANDLER */) {
802
- return this.typedApiHandler.handler.fetch(request, env, ctx);
838
+ const url = new URL(request.url);
839
+ const apiHandler = this.findApiHandlerForUrl(url);
840
+ if (!apiHandler) {
841
+ return this.createErrorResponse("invalid_request", "No handler found for API route", 404);
842
+ }
843
+ if (apiHandler.type === 0 /* EXPORTED_HANDLER */) {
844
+ return apiHandler.handler.fetch(request, env, ctx);
803
845
  } else {
804
- const handler = new this.typedApiHandler.handler(ctx, env);
846
+ const handler = new apiHandler.handler(ctx, env);
805
847
  return handler.fetch(request);
806
848
  }
807
849
  }
@@ -827,22 +869,32 @@ var OAuthProviderImpl = class {
827
869
  const clientKey = `client:${clientId}`;
828
870
  return env.OAUTH_KV.get(clientKey, { type: "json" });
829
871
  }
872
+ /**
873
+ * Helper function to create OAuth error responses
874
+ * @param code - OAuth error code (e.g., 'invalid_request', 'invalid_token')
875
+ * @param description - Human-readable error description
876
+ * @param status - HTTP status code (default: 400)
877
+ * @param headers - Additional headers to include
878
+ * @returns A Response object with the error
879
+ */
880
+ createErrorResponse(code, description, status = 400, headers = {}) {
881
+ const customErrorResponse = this.options.onError?.({ code, description, status, headers });
882
+ if (customErrorResponse) return customErrorResponse;
883
+ const body = JSON.stringify({
884
+ error: code,
885
+ error_description: description
886
+ });
887
+ return new Response(body, {
888
+ status,
889
+ headers: {
890
+ "Content-Type": "application/json",
891
+ ...headers
892
+ }
893
+ });
894
+ }
830
895
  };
831
896
  var DEFAULT_ACCESS_TOKEN_TTL = 60 * 60;
832
897
  var TOKEN_LENGTH = 32;
833
- function createErrorResponse(code, description, status = 400, headers = {}) {
834
- const body = JSON.stringify({
835
- error: code,
836
- error_description: description
837
- });
838
- return new Response(body, {
839
- status,
840
- headers: {
841
- "Content-Type": "application/json",
842
- ...headers
843
- }
844
- });
845
- }
846
898
  async function hashSecret(secret) {
847
899
  return generateTokenId(secret);
848
900
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudflare/workers-oauth-provider",
3
- "version": "0.0.2",
3
+ "version": "0.0.4-0",
4
4
  "description": "OAuth provider for Cloudflare Workers",
5
5
  "main": "dist/oauth-provider.js",
6
6
  "types": "dist/oauth-provider.d.ts",
@@ -15,7 +15,9 @@
15
15
  },
16
16
  "scripts": {
17
17
  "build": "tsup",
18
+ "build:watch": "tsup --watch",
18
19
  "test": "vitest run",
20
+ "test:watch": "vitest",
19
21
  "prepublishOnly": "npm run build",
20
22
  "prettier": "prettier -w ."
21
23
  },