@cloudflare/workers-oauth-provider 0.0.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.
@@ -0,0 +1,1339 @@
1
+ var __typeError = (msg) => {
2
+ throw TypeError(msg);
3
+ };
4
+ var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
5
+ var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
6
+ var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
7
+ var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
8
+
9
+ // src/oauth-provider.ts
10
+ import { WorkerEntrypoint } from "cloudflare:workers";
11
+ var _impl;
12
+ var OAuthProvider = class {
13
+ /**
14
+ * Creates a new OAuth provider instance
15
+ * @param options - Configuration options for the provider
16
+ */
17
+ constructor(options) {
18
+ __privateAdd(this, _impl);
19
+ __privateSet(this, _impl, new OAuthProviderImpl(options));
20
+ }
21
+ /**
22
+ * Main fetch handler for the Worker
23
+ * Routes requests to the appropriate handler based on the URL
24
+ * @param request - The HTTP request
25
+ * @param env - Cloudflare Worker environment variables
26
+ * @param ctx - Cloudflare Worker execution context
27
+ * @returns A Promise resolving to an HTTP Response
28
+ */
29
+ fetch(request, env, ctx) {
30
+ return __privateGet(this, _impl).fetch(request, env, ctx);
31
+ }
32
+ };
33
+ _impl = new WeakMap();
34
+ var OAuthProviderImpl = class {
35
+ /**
36
+ * Creates a new OAuth provider instance
37
+ * @param options - Configuration options for the provider
38
+ */
39
+ constructor(options) {
40
+ this.typedApiHandler = this.validateHandler(options.apiHandler, "apiHandler");
41
+ 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
+ });
46
+ } else {
47
+ this.validateEndpoint(options.apiRoute, "apiRoute");
48
+ }
49
+ this.validateEndpoint(options.authorizeEndpoint, "authorizeEndpoint");
50
+ this.validateEndpoint(options.tokenEndpoint, "tokenEndpoint");
51
+ if (options.clientRegistrationEndpoint) {
52
+ this.validateEndpoint(options.clientRegistrationEndpoint, "clientRegistrationEndpoint");
53
+ }
54
+ this.options = {
55
+ ...options,
56
+ accessTokenTTL: options.accessTokenTTL || DEFAULT_ACCESS_TOKEN_TTL
57
+ };
58
+ }
59
+ /**
60
+ * Validates that an endpoint is either an absolute path or a full URL
61
+ * @param endpoint - The endpoint to validate
62
+ * @param name - The name of the endpoint property for error messages
63
+ * @throws TypeError if the endpoint is invalid
64
+ */
65
+ validateEndpoint(endpoint, name) {
66
+ if (this.isPath(endpoint)) {
67
+ if (!endpoint.startsWith("/")) {
68
+ throw new TypeError(`${name} path must be an absolute path starting with /`);
69
+ }
70
+ } else {
71
+ try {
72
+ new URL(endpoint);
73
+ } catch (e) {
74
+ throw new TypeError(`${name} must be either an absolute path starting with / or a valid URL`);
75
+ }
76
+ }
77
+ }
78
+ /**
79
+ * Validates that a handler is either an ExportedHandler or a class extending WorkerEntrypoint
80
+ * @param handler - The handler to validate
81
+ * @param name - The name of the handler property for error messages
82
+ * @returns The type of the handler (EXPORTED_HANDLER or WORKER_ENTRYPOINT)
83
+ * @throws TypeError if the handler is invalid
84
+ */
85
+ validateHandler(handler, name) {
86
+ if (typeof handler === "object" && handler !== null && typeof handler.fetch === "function") {
87
+ return { type: 0 /* EXPORTED_HANDLER */, handler };
88
+ }
89
+ if (typeof handler === "function" && handler.prototype instanceof WorkerEntrypoint) {
90
+ return { type: 1 /* WORKER_ENTRYPOINT */, handler };
91
+ }
92
+ throw new TypeError(`${name} must be either an ExportedHandler object with a fetch method or a class extending WorkerEntrypoint`);
93
+ }
94
+ /**
95
+ * Main fetch handler for the Worker
96
+ * Routes requests to the appropriate handler based on the URL
97
+ * @param request - The HTTP request
98
+ * @param env - Cloudflare Worker environment variables
99
+ * @param ctx - Cloudflare Worker execution context
100
+ * @returns A Promise resolving to an HTTP Response
101
+ */
102
+ async fetch(request, env, ctx) {
103
+ const url = new URL(request.url);
104
+ if (request.method === "OPTIONS") {
105
+ if (this.isApiRequest(url) || url.pathname === "/.well-known/oauth-authorization-server" || this.isTokenEndpoint(url) || this.options.clientRegistrationEndpoint && this.isClientRegistrationEndpoint(url)) {
106
+ return this.addCorsHeaders(
107
+ new Response(null, {
108
+ status: 204,
109
+ headers: { "Content-Length": "0" }
110
+ }),
111
+ request
112
+ );
113
+ }
114
+ }
115
+ if (url.pathname === "/.well-known/oauth-authorization-server") {
116
+ const response = await this.handleMetadataDiscovery(url);
117
+ return this.addCorsHeaders(response, request);
118
+ }
119
+ if (this.isTokenEndpoint(url)) {
120
+ const response = await this.handleTokenRequest(request, env);
121
+ return this.addCorsHeaders(response, request);
122
+ }
123
+ if (this.options.clientRegistrationEndpoint && this.isClientRegistrationEndpoint(url)) {
124
+ const response = await this.handleClientRegistration(request, env);
125
+ return this.addCorsHeaders(response, request);
126
+ }
127
+ if (this.isApiRequest(url)) {
128
+ const response = await this.handleApiRequest(request, env, ctx);
129
+ return this.addCorsHeaders(response, request);
130
+ }
131
+ if (!env.OAUTH_PROVIDER) {
132
+ env.OAUTH_PROVIDER = this.createOAuthHelpers(env);
133
+ }
134
+ if (this.typedDefaultHandler.type === 0 /* EXPORTED_HANDLER */) {
135
+ return this.typedDefaultHandler.handler.fetch(request, env, ctx);
136
+ } else {
137
+ const handler = new this.typedDefaultHandler.handler(ctx, env);
138
+ return handler.fetch(request);
139
+ }
140
+ }
141
+ /**
142
+ * Determines if an endpoint configuration is a path or a full URL
143
+ * @param endpoint - The endpoint configuration
144
+ * @returns True if the endpoint is a path (starts with /), false if it's a full URL
145
+ */
146
+ isPath(endpoint) {
147
+ return endpoint.startsWith("/");
148
+ }
149
+ /**
150
+ * Matches a URL against an endpoint pattern that can be a full URL or just a path
151
+ * @param url - The URL to check
152
+ * @param endpoint - The endpoint pattern (full URL or path)
153
+ * @returns True if the URL matches the endpoint pattern
154
+ */
155
+ matchEndpoint(url, endpoint) {
156
+ if (this.isPath(endpoint)) {
157
+ return url.pathname === endpoint;
158
+ } else {
159
+ const endpointUrl = new URL(endpoint);
160
+ return url.hostname === endpointUrl.hostname && url.pathname === endpointUrl.pathname;
161
+ }
162
+ }
163
+ /**
164
+ * Checks if a URL matches the configured token endpoint
165
+ * @param url - The URL to check
166
+ * @returns True if the URL matches the token endpoint
167
+ */
168
+ isTokenEndpoint(url) {
169
+ return this.matchEndpoint(url, this.options.tokenEndpoint);
170
+ }
171
+ /**
172
+ * Checks if a URL matches the configured client registration endpoint
173
+ * @param url - The URL to check
174
+ * @returns True if the URL matches the client registration endpoint
175
+ */
176
+ isClientRegistrationEndpoint(url) {
177
+ if (!this.options.clientRegistrationEndpoint) return false;
178
+ return this.matchEndpoint(url, this.options.clientRegistrationEndpoint);
179
+ }
180
+ /**
181
+ * Checks if a URL matches a specific API route
182
+ * @param url - The URL to check
183
+ * @param route - The API route to check against
184
+ * @returns True if the URL matches the API route
185
+ */
186
+ matchApiRoute(url, route) {
187
+ if (this.isPath(route)) {
188
+ return url.pathname.startsWith(route);
189
+ } else {
190
+ const apiUrl = new URL(route);
191
+ return url.hostname === apiUrl.hostname && url.pathname.startsWith(apiUrl.pathname);
192
+ }
193
+ }
194
+ /**
195
+ * Checks if a URL is an API request based on the configured API route(s)
196
+ * @param url - The URL to check
197
+ * @returns True if the URL matches any of the API routes
198
+ */
199
+ isApiRequest(url) {
200
+ if (Array.isArray(this.options.apiRoute)) {
201
+ return this.options.apiRoute.some((route) => this.matchApiRoute(url, route));
202
+ } else {
203
+ return this.matchApiRoute(url, this.options.apiRoute);
204
+ }
205
+ }
206
+ /**
207
+ * Gets the full URL for an endpoint, using the provided request URL's
208
+ * origin for endpoints specified as just paths
209
+ * @param endpoint - The endpoint configuration (path or full URL)
210
+ * @param requestUrl - The URL of the incoming request
211
+ * @returns The full URL for the endpoint
212
+ */
213
+ getFullEndpointUrl(endpoint, requestUrl) {
214
+ if (this.isPath(endpoint)) {
215
+ return `${requestUrl.origin}${endpoint}`;
216
+ } else {
217
+ return endpoint;
218
+ }
219
+ }
220
+ /**
221
+ * Adds CORS headers to a response
222
+ * @param response - The response to add CORS headers to
223
+ * @param request - The original request
224
+ * @returns A new Response with CORS headers added
225
+ */
226
+ addCorsHeaders(response, request) {
227
+ const origin = request.headers.get("Origin");
228
+ if (!origin) {
229
+ return response;
230
+ }
231
+ const newResponse = new Response(response.body, response);
232
+ newResponse.headers.set("Access-Control-Allow-Origin", origin);
233
+ newResponse.headers.set("Access-Control-Allow-Methods", "*");
234
+ newResponse.headers.set("Access-Control-Allow-Headers", "Authorization, *");
235
+ newResponse.headers.set("Access-Control-Max-Age", "86400");
236
+ return newResponse;
237
+ }
238
+ /**
239
+ * Handles the OAuth metadata discovery endpoint
240
+ * Implements RFC 8414 for OAuth Server Metadata
241
+ * @param requestUrl - The URL of the incoming request
242
+ * @returns Response with OAuth server metadata
243
+ */
244
+ async handleMetadataDiscovery(requestUrl) {
245
+ const tokenEndpoint = this.getFullEndpointUrl(this.options.tokenEndpoint, requestUrl);
246
+ const authorizeEndpoint = this.getFullEndpointUrl(this.options.authorizeEndpoint, requestUrl);
247
+ let registrationEndpoint = void 0;
248
+ if (this.options.clientRegistrationEndpoint) {
249
+ registrationEndpoint = this.getFullEndpointUrl(this.options.clientRegistrationEndpoint, requestUrl);
250
+ }
251
+ const responseTypesSupported = ["code"];
252
+ if (this.options.allowImplicitFlow) {
253
+ responseTypesSupported.push("token");
254
+ }
255
+ const metadata = {
256
+ issuer: new URL(tokenEndpoint).origin,
257
+ authorization_endpoint: authorizeEndpoint,
258
+ token_endpoint: tokenEndpoint,
259
+ // not implemented: jwks_uri
260
+ registration_endpoint: registrationEndpoint,
261
+ scopes_supported: this.options.scopesSupported,
262
+ response_types_supported: responseTypesSupported,
263
+ response_modes_supported: ["query"],
264
+ grant_types_supported: ["authorization_code", "refresh_token"],
265
+ // Support "none" auth method for public clients
266
+ token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post", "none"],
267
+ // not implemented: token_endpoint_auth_signing_alg_values_supported
268
+ // not implemented: service_documentation
269
+ // not implemented: ui_locales_supported
270
+ // not implemented: op_policy_uri
271
+ // not implemented: op_tos_uri
272
+ revocation_endpoint: tokenEndpoint,
273
+ // Reusing token endpoint for revocation
274
+ // not implemented: revocation_endpoint_auth_methods_supported
275
+ // not implemented: revocation_endpoint_auth_signing_alg_values_supported
276
+ // not implemented: introspection_endpoint
277
+ // not implemented: introspection_endpoint_auth_methods_supported
278
+ // not implemented: introspection_endpoint_auth_signing_alg_values_supported
279
+ code_challenge_methods_supported: ["plain", "S256"]
280
+ // PKCE support
281
+ };
282
+ return new Response(JSON.stringify(metadata), {
283
+ headers: { "Content-Type": "application/json" }
284
+ });
285
+ }
286
+ /**
287
+ * Handles client authentication and token issuance via the token endpoint
288
+ * Supports authorization_code and refresh_token grant types
289
+ * @param request - The HTTP request
290
+ * @param env - Cloudflare Worker environment variables
291
+ * @returns Response with token data or error
292
+ */
293
+ async handleTokenRequest(request, env) {
294
+ if (request.method !== "POST") {
295
+ return createErrorResponse(
296
+ "invalid_request",
297
+ "Method not allowed",
298
+ 405
299
+ );
300
+ }
301
+ let contentType = request.headers.get("Content-Type") || "";
302
+ let body = {};
303
+ if (!contentType.includes("application/x-www-form-urlencoded")) {
304
+ return createErrorResponse(
305
+ "invalid_request",
306
+ "Content-Type must be application/x-www-form-urlencoded",
307
+ 400
308
+ );
309
+ }
310
+ const formData = await request.formData();
311
+ for (const [key, value] of formData.entries()) {
312
+ body[key] = value;
313
+ }
314
+ const authHeader = request.headers.get("Authorization");
315
+ let clientId = "";
316
+ let clientSecret = "";
317
+ if (authHeader && authHeader.startsWith("Basic ")) {
318
+ const credentials = atob(authHeader.substring(6));
319
+ const [id, secret] = credentials.split(":");
320
+ clientId = id;
321
+ clientSecret = secret || "";
322
+ } else {
323
+ clientId = body.client_id;
324
+ clientSecret = body.client_secret || "";
325
+ }
326
+ if (!clientId) {
327
+ return createErrorResponse(
328
+ "invalid_client",
329
+ "Client ID is required",
330
+ 401
331
+ );
332
+ }
333
+ const clientInfo = await this.getClient(env, clientId);
334
+ if (!clientInfo) {
335
+ return createErrorResponse(
336
+ "invalid_client",
337
+ "Client not found",
338
+ 401
339
+ );
340
+ }
341
+ const isPublicClient = clientInfo.tokenEndpointAuthMethod === "none";
342
+ if (!isPublicClient) {
343
+ if (!clientSecret) {
344
+ return createErrorResponse(
345
+ "invalid_client",
346
+ "Client authentication failed: missing client_secret",
347
+ 401
348
+ );
349
+ }
350
+ if (!clientInfo.clientSecret) {
351
+ return createErrorResponse(
352
+ "invalid_client",
353
+ "Client authentication failed: client has no registered secret",
354
+ 401
355
+ );
356
+ }
357
+ const providedSecretHash = await hashSecret(clientSecret);
358
+ if (providedSecretHash !== clientInfo.clientSecret) {
359
+ return createErrorResponse(
360
+ "invalid_client",
361
+ "Client authentication failed: invalid client_secret",
362
+ 401
363
+ );
364
+ }
365
+ }
366
+ const grantType = body.grant_type;
367
+ if (grantType === "authorization_code") {
368
+ return this.handleAuthorizationCodeGrant(body, clientInfo, env);
369
+ } else if (grantType === "refresh_token") {
370
+ return this.handleRefreshTokenGrant(body, clientInfo, env);
371
+ } else {
372
+ return createErrorResponse(
373
+ "unsupported_grant_type",
374
+ "Grant type not supported"
375
+ );
376
+ }
377
+ }
378
+ /**
379
+ * Handles the authorization code grant type
380
+ * Exchanges an authorization code for access and refresh tokens
381
+ * @param body - The parsed request body
382
+ * @param clientInfo - The authenticated client information
383
+ * @param env - Cloudflare Worker environment variables
384
+ * @returns Response with token data or error
385
+ */
386
+ async handleAuthorizationCodeGrant(body, clientInfo, env) {
387
+ const code = body.code;
388
+ const redirectUri = body.redirect_uri;
389
+ const codeVerifier = body.code_verifier;
390
+ if (!code) {
391
+ return createErrorResponse(
392
+ "invalid_request",
393
+ "Authorization code is required"
394
+ );
395
+ }
396
+ const codeParts = code.split(":");
397
+ if (codeParts.length !== 3) {
398
+ return createErrorResponse(
399
+ "invalid_grant",
400
+ "Invalid authorization code format"
401
+ );
402
+ }
403
+ const [userId, grantId, _] = codeParts;
404
+ const grantKey = `grant:${userId}:${grantId}`;
405
+ const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
406
+ if (!grantData) {
407
+ return createErrorResponse(
408
+ "invalid_grant",
409
+ "Grant not found or authorization code expired"
410
+ );
411
+ }
412
+ if (!grantData.authCodeId) {
413
+ return createErrorResponse(
414
+ "invalid_grant",
415
+ "Authorization code already used"
416
+ );
417
+ }
418
+ const codeHash = await hashSecret(code);
419
+ if (codeHash !== grantData.authCodeId) {
420
+ return createErrorResponse(
421
+ "invalid_grant",
422
+ "Invalid authorization code"
423
+ );
424
+ }
425
+ if (grantData.clientId !== clientInfo.clientId) {
426
+ return createErrorResponse(
427
+ "invalid_grant",
428
+ "Client ID mismatch"
429
+ );
430
+ }
431
+ const isPkceEnabled = !!grantData.codeChallenge;
432
+ if (!redirectUri && !isPkceEnabled) {
433
+ return createErrorResponse(
434
+ "invalid_request",
435
+ "redirect_uri is required when not using PKCE"
436
+ );
437
+ }
438
+ if (redirectUri && !clientInfo.redirectUris.includes(redirectUri)) {
439
+ return createErrorResponse(
440
+ "invalid_grant",
441
+ "Invalid redirect URI"
442
+ );
443
+ }
444
+ if (isPkceEnabled) {
445
+ if (!codeVerifier) {
446
+ return createErrorResponse(
447
+ "invalid_request",
448
+ "code_verifier is required for PKCE"
449
+ );
450
+ }
451
+ let calculatedChallenge;
452
+ if (grantData.codeChallengeMethod === "S256") {
453
+ const encoder = new TextEncoder();
454
+ const data = encoder.encode(codeVerifier);
455
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
456
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
457
+ calculatedChallenge = base64UrlEncode(String.fromCharCode(...hashArray));
458
+ } else {
459
+ calculatedChallenge = codeVerifier;
460
+ }
461
+ if (calculatedChallenge !== grantData.codeChallenge) {
462
+ return createErrorResponse(
463
+ "invalid_grant",
464
+ "Invalid PKCE code_verifier"
465
+ );
466
+ }
467
+ }
468
+ const accessTokenSecret = generateRandomString(TOKEN_LENGTH);
469
+ const refreshTokenSecret = generateRandomString(TOKEN_LENGTH);
470
+ const accessToken = `${userId}:${grantId}:${accessTokenSecret}`;
471
+ const refreshToken = `${userId}:${grantId}:${refreshTokenSecret}`;
472
+ const accessTokenId = await generateTokenId(accessToken);
473
+ const refreshTokenId = await generateTokenId(refreshToken);
474
+ const now = Math.floor(Date.now() / 1e3);
475
+ const accessTokenExpiresAt = now + this.options.accessTokenTTL;
476
+ const encryptionKey = await unwrapKeyWithToken(code, grantData.authCodeWrappedKey);
477
+ const accessTokenWrappedKey = await wrapKeyWithToken(accessToken, encryptionKey);
478
+ const refreshTokenWrappedKey = await wrapKeyWithToken(refreshToken, encryptionKey);
479
+ delete grantData.authCodeId;
480
+ delete grantData.codeChallenge;
481
+ delete grantData.codeChallengeMethod;
482
+ delete grantData.authCodeWrappedKey;
483
+ grantData.refreshTokenId = refreshTokenId;
484
+ grantData.refreshTokenWrappedKey = refreshTokenWrappedKey;
485
+ grantData.previousRefreshTokenId = void 0;
486
+ grantData.previousRefreshTokenWrappedKey = void 0;
487
+ await env.OAUTH_KV.put(grantKey, JSON.stringify(grantData));
488
+ const accessTokenData = {
489
+ id: accessTokenId,
490
+ grantId,
491
+ userId,
492
+ createdAt: now,
493
+ expiresAt: accessTokenExpiresAt,
494
+ wrappedEncryptionKey: accessTokenWrappedKey,
495
+ grant: {
496
+ clientId: grantData.clientId,
497
+ scope: grantData.scope,
498
+ encryptedProps: grantData.encryptedProps
499
+ }
500
+ };
501
+ await env.OAUTH_KV.put(
502
+ `token:${userId}:${grantId}:${accessTokenId}`,
503
+ JSON.stringify(accessTokenData),
504
+ { expirationTtl: this.options.accessTokenTTL }
505
+ );
506
+ return new Response(JSON.stringify({
507
+ access_token: accessToken,
508
+ token_type: "bearer",
509
+ expires_in: this.options.accessTokenTTL,
510
+ refresh_token: refreshToken,
511
+ scope: grantData.scope.join(" ")
512
+ }), {
513
+ headers: { "Content-Type": "application/json" }
514
+ });
515
+ }
516
+ /**
517
+ * Handles the refresh token grant type
518
+ * Issues a new access token using a refresh token
519
+ * @param body - The parsed request body
520
+ * @param clientInfo - The authenticated client information
521
+ * @param env - Cloudflare Worker environment variables
522
+ * @returns Response with token data or error
523
+ */
524
+ async handleRefreshTokenGrant(body, clientInfo, env) {
525
+ const refreshToken = body.refresh_token;
526
+ if (!refreshToken) {
527
+ return createErrorResponse(
528
+ "invalid_request",
529
+ "Refresh token is required"
530
+ );
531
+ }
532
+ const tokenParts = refreshToken.split(":");
533
+ if (tokenParts.length !== 3) {
534
+ return createErrorResponse(
535
+ "invalid_grant",
536
+ "Invalid token format"
537
+ );
538
+ }
539
+ const [userId, grantId, _] = tokenParts;
540
+ const providedTokenHash = await generateTokenId(refreshToken);
541
+ const grantKey = `grant:${userId}:${grantId}`;
542
+ const grantData = await env.OAUTH_KV.get(grantKey, { type: "json" });
543
+ if (!grantData) {
544
+ return createErrorResponse(
545
+ "invalid_grant",
546
+ "Grant not found"
547
+ );
548
+ }
549
+ const isCurrentToken = grantData.refreshTokenId === providedTokenHash;
550
+ const isPreviousToken = grantData.previousRefreshTokenId === providedTokenHash;
551
+ if (!isCurrentToken && !isPreviousToken) {
552
+ return createErrorResponse(
553
+ "invalid_grant",
554
+ "Invalid refresh token"
555
+ );
556
+ }
557
+ if (grantData.clientId !== clientInfo.clientId) {
558
+ return createErrorResponse(
559
+ "invalid_grant",
560
+ "Client ID mismatch"
561
+ );
562
+ }
563
+ const accessTokenSecret = generateRandomString(TOKEN_LENGTH);
564
+ const newAccessToken = `${userId}:${grantId}:${accessTokenSecret}`;
565
+ const accessTokenId = await generateTokenId(newAccessToken);
566
+ const refreshTokenSecret = generateRandomString(TOKEN_LENGTH);
567
+ const newRefreshToken = `${userId}:${grantId}:${refreshTokenSecret}`;
568
+ const newRefreshTokenId = await generateTokenId(newRefreshToken);
569
+ const now = Math.floor(Date.now() / 1e3);
570
+ const accessTokenExpiresAt = now + this.options.accessTokenTTL;
571
+ let wrappedKeyToUse;
572
+ if (isCurrentToken) {
573
+ wrappedKeyToUse = grantData.refreshTokenWrappedKey;
574
+ } else {
575
+ wrappedKeyToUse = grantData.previousRefreshTokenWrappedKey;
576
+ }
577
+ const encryptionKey = await unwrapKeyWithToken(refreshToken, wrappedKeyToUse);
578
+ const accessTokenWrappedKey = await wrapKeyWithToken(newAccessToken, encryptionKey);
579
+ const newRefreshTokenWrappedKey = await wrapKeyWithToken(newRefreshToken, encryptionKey);
580
+ grantData.previousRefreshTokenId = providedTokenHash;
581
+ grantData.previousRefreshTokenWrappedKey = wrappedKeyToUse;
582
+ grantData.refreshTokenId = newRefreshTokenId;
583
+ grantData.refreshTokenWrappedKey = newRefreshTokenWrappedKey;
584
+ await env.OAUTH_KV.put(grantKey, JSON.stringify(grantData));
585
+ const accessTokenData = {
586
+ id: accessTokenId,
587
+ grantId,
588
+ userId,
589
+ createdAt: now,
590
+ expiresAt: accessTokenExpiresAt,
591
+ wrappedEncryptionKey: accessTokenWrappedKey,
592
+ grant: {
593
+ clientId: grantData.clientId,
594
+ scope: grantData.scope,
595
+ encryptedProps: grantData.encryptedProps
596
+ }
597
+ };
598
+ await env.OAUTH_KV.put(
599
+ `token:${userId}:${grantId}:${accessTokenId}`,
600
+ JSON.stringify(accessTokenData),
601
+ { expirationTtl: this.options.accessTokenTTL }
602
+ );
603
+ return new Response(JSON.stringify({
604
+ access_token: newAccessToken,
605
+ token_type: "bearer",
606
+ expires_in: this.options.accessTokenTTL,
607
+ refresh_token: newRefreshToken,
608
+ scope: grantData.scope.join(" ")
609
+ }), {
610
+ headers: { "Content-Type": "application/json" }
611
+ });
612
+ }
613
+ /**
614
+ * Handles the dynamic client registration endpoint (RFC 7591)
615
+ * @param request - The HTTP request
616
+ * @param env - Cloudflare Worker environment variables
617
+ * @returns Response with client registration data or error
618
+ */
619
+ async handleClientRegistration(request, env) {
620
+ if (!this.options.clientRegistrationEndpoint) {
621
+ return createErrorResponse(
622
+ "not_implemented",
623
+ "Client registration is not enabled",
624
+ 501
625
+ );
626
+ }
627
+ if (request.method !== "POST") {
628
+ return createErrorResponse(
629
+ "invalid_request",
630
+ "Method not allowed",
631
+ 405
632
+ );
633
+ }
634
+ const contentLength = parseInt(request.headers.get("Content-Length") || "0", 10);
635
+ if (contentLength > 1048576) {
636
+ return createErrorResponse(
637
+ "invalid_request",
638
+ "Request payload too large, must be under 1 MiB",
639
+ 413
640
+ );
641
+ }
642
+ let clientMetadata;
643
+ try {
644
+ const text = await request.text();
645
+ if (text.length > 1048576) {
646
+ return createErrorResponse(
647
+ "invalid_request",
648
+ "Request payload too large, must be under 1 MiB",
649
+ 413
650
+ );
651
+ }
652
+ clientMetadata = JSON.parse(text);
653
+ } catch (error) {
654
+ return createErrorResponse(
655
+ "invalid_request",
656
+ "Invalid JSON payload",
657
+ 400
658
+ );
659
+ }
660
+ const validateStringField = (field) => {
661
+ if (field === void 0) {
662
+ return void 0;
663
+ }
664
+ if (typeof field !== "string") {
665
+ throw new Error("Field must be a string");
666
+ }
667
+ return field;
668
+ };
669
+ const validateStringArray = (arr) => {
670
+ if (arr === void 0) {
671
+ return void 0;
672
+ }
673
+ if (!Array.isArray(arr)) {
674
+ throw new Error("Field must be an array");
675
+ }
676
+ for (const item of arr) {
677
+ if (typeof item !== "string") {
678
+ throw new Error("All array elements must be strings");
679
+ }
680
+ }
681
+ return arr;
682
+ };
683
+ const authMethod = validateStringField(clientMetadata.token_endpoint_auth_method) || "client_secret_basic";
684
+ const isPublicClient = authMethod === "none";
685
+ if (isPublicClient && this.options.disallowPublicClientRegistration) {
686
+ return createErrorResponse(
687
+ "invalid_client_metadata",
688
+ "Public client registration is not allowed"
689
+ );
690
+ }
691
+ const clientId = generateRandomString(16);
692
+ let clientSecret;
693
+ let hashedSecret;
694
+ if (!isPublicClient) {
695
+ clientSecret = generateRandomString(32);
696
+ hashedSecret = await hashSecret(clientSecret);
697
+ }
698
+ let clientInfo;
699
+ try {
700
+ const redirectUris = validateStringArray(clientMetadata.redirect_uris);
701
+ if (!redirectUris || redirectUris.length === 0) {
702
+ throw new Error("At least one redirect URI is required");
703
+ }
704
+ clientInfo = {
705
+ clientId,
706
+ redirectUris,
707
+ clientName: validateStringField(clientMetadata.client_name),
708
+ logoUri: validateStringField(clientMetadata.logo_uri),
709
+ clientUri: validateStringField(clientMetadata.client_uri),
710
+ policyUri: validateStringField(clientMetadata.policy_uri),
711
+ tosUri: validateStringField(clientMetadata.tos_uri),
712
+ jwksUri: validateStringField(clientMetadata.jwks_uri),
713
+ contacts: validateStringArray(clientMetadata.contacts),
714
+ grantTypes: validateStringArray(clientMetadata.grant_types) || ["authorization_code", "refresh_token"],
715
+ responseTypes: validateStringArray(clientMetadata.response_types) || ["code"],
716
+ registrationDate: Math.floor(Date.now() / 1e3),
717
+ tokenEndpointAuthMethod: authMethod
718
+ };
719
+ if (!isPublicClient && hashedSecret) {
720
+ clientInfo.clientSecret = hashedSecret;
721
+ }
722
+ } catch (error) {
723
+ return createErrorResponse(
724
+ "invalid_client_metadata",
725
+ error instanceof Error ? error.message : "Invalid client metadata"
726
+ );
727
+ }
728
+ await env.OAUTH_KV.put(`client:${clientId}`, JSON.stringify(clientInfo));
729
+ const response = {
730
+ client_id: clientInfo.clientId,
731
+ redirect_uris: clientInfo.redirectUris,
732
+ client_name: clientInfo.clientName,
733
+ logo_uri: clientInfo.logoUri,
734
+ client_uri: clientInfo.clientUri,
735
+ policy_uri: clientInfo.policyUri,
736
+ tos_uri: clientInfo.tosUri,
737
+ jwks_uri: clientInfo.jwksUri,
738
+ contacts: clientInfo.contacts,
739
+ grant_types: clientInfo.grantTypes,
740
+ response_types: clientInfo.responseTypes,
741
+ token_endpoint_auth_method: clientInfo.tokenEndpointAuthMethod,
742
+ registration_client_uri: `${this.options.clientRegistrationEndpoint}/${clientId}`,
743
+ client_id_issued_at: clientInfo.registrationDate
744
+ };
745
+ if (clientSecret) {
746
+ response.client_secret = clientSecret;
747
+ }
748
+ return new Response(JSON.stringify(response), {
749
+ status: 201,
750
+ headers: { "Content-Type": "application/json" }
751
+ });
752
+ }
753
+ /**
754
+ * Handles API requests by validating the access token and calling the API handler
755
+ * @param request - The HTTP request
756
+ * @param env - Cloudflare Worker environment variables
757
+ * @param ctx - Cloudflare Worker execution context
758
+ * @returns Response from the API handler or error
759
+ */
760
+ async handleApiRequest(request, env, ctx) {
761
+ const authHeader = request.headers.get("Authorization");
762
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
763
+ return createErrorResponse(
764
+ "invalid_token",
765
+ "Missing or invalid access token",
766
+ 401,
767
+ { "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token", error_description="Missing or invalid access token"' }
768
+ );
769
+ }
770
+ const accessToken = authHeader.substring(7);
771
+ const tokenParts = accessToken.split(":");
772
+ if (tokenParts.length !== 3) {
773
+ return createErrorResponse(
774
+ "invalid_token",
775
+ "Invalid token format",
776
+ 401,
777
+ { "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"' }
778
+ );
779
+ }
780
+ const [userId, grantId, _] = tokenParts;
781
+ const accessTokenId = await generateTokenId(accessToken);
782
+ const tokenKey = `token:${userId}:${grantId}:${accessTokenId}`;
783
+ const tokenData = await env.OAUTH_KV.get(tokenKey, { type: "json" });
784
+ if (!tokenData) {
785
+ return createErrorResponse(
786
+ "invalid_token",
787
+ "Invalid access token",
788
+ 401,
789
+ { "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"' }
790
+ );
791
+ }
792
+ const now = Math.floor(Date.now() / 1e3);
793
+ if (tokenData.expiresAt < now) {
794
+ return createErrorResponse(
795
+ "invalid_token",
796
+ "Access token expired",
797
+ 401,
798
+ { "WWW-Authenticate": 'Bearer realm="OAuth", error="invalid_token"' }
799
+ );
800
+ }
801
+ const encryptionKey = await unwrapKeyWithToken(accessToken, tokenData.wrappedEncryptionKey);
802
+ const decryptedProps = await decryptProps(
803
+ encryptionKey,
804
+ tokenData.grant.encryptedProps
805
+ );
806
+ ctx.props = decryptedProps;
807
+ if (!env.OAUTH_PROVIDER) {
808
+ env.OAUTH_PROVIDER = this.createOAuthHelpers(env);
809
+ }
810
+ if (this.typedApiHandler.type === 0 /* EXPORTED_HANDLER */) {
811
+ return this.typedApiHandler.handler.fetch(request, env, ctx);
812
+ } else {
813
+ const handler = new this.typedApiHandler.handler(ctx, env);
814
+ return handler.fetch(request);
815
+ }
816
+ }
817
+ /**
818
+ * Creates the helper methods object for OAuth operations
819
+ * This is passed to the handler functions to allow them to interact with the OAuth system
820
+ * @param env - Cloudflare Worker environment variables
821
+ * @returns An instance of OAuthHelpers
822
+ */
823
+ createOAuthHelpers(env) {
824
+ return new OAuthHelpersImpl(env, this);
825
+ }
826
+ /**
827
+ * Fetches client information from KV storage
828
+ * This method is not private because `OAuthHelpers` needs to call it. Note that since
829
+ * `OAuthProviderImpl` is not exposed outside this module, this is still effectively
830
+ * module-private.
831
+ * @param env - Cloudflare Worker environment variables
832
+ * @param clientId - The client ID to look up
833
+ * @returns The client information, or null if not found
834
+ */
835
+ getClient(env, clientId) {
836
+ const clientKey = `client:${clientId}`;
837
+ return env.OAUTH_KV.get(clientKey, { type: "json" });
838
+ }
839
+ };
840
+ var DEFAULT_ACCESS_TOKEN_TTL = 60 * 60;
841
+ var TOKEN_LENGTH = 32;
842
+ function createErrorResponse(code, description, status = 400, headers = {}) {
843
+ const body = JSON.stringify({
844
+ error: code,
845
+ error_description: description
846
+ });
847
+ return new Response(body, {
848
+ status,
849
+ headers: {
850
+ "Content-Type": "application/json",
851
+ ...headers
852
+ }
853
+ });
854
+ }
855
+ async function hashSecret(secret) {
856
+ return generateTokenId(secret);
857
+ }
858
+ function generateRandomString(length) {
859
+ const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
860
+ let result = "";
861
+ const values = new Uint8Array(length);
862
+ crypto.getRandomValues(values);
863
+ for (let i = 0; i < length; i++) {
864
+ result += characters.charAt(values[i] % characters.length);
865
+ }
866
+ return result;
867
+ }
868
+ async function generateTokenId(token) {
869
+ const encoder = new TextEncoder();
870
+ const data = encoder.encode(token);
871
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
872
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
873
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
874
+ return hashHex;
875
+ }
876
+ function base64UrlEncode(str) {
877
+ return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
878
+ }
879
+ function arrayBufferToBase64(buffer) {
880
+ return btoa(String.fromCharCode(...new Uint8Array(buffer)));
881
+ }
882
+ function base64ToArrayBuffer(base64) {
883
+ const binaryString = atob(base64);
884
+ const bytes = new Uint8Array(binaryString.length);
885
+ for (let i = 0; i < binaryString.length; i++) {
886
+ bytes[i] = binaryString.charCodeAt(i);
887
+ }
888
+ return bytes.buffer;
889
+ }
890
+ async function encryptProps(data) {
891
+ const key = await crypto.subtle.generateKey(
892
+ {
893
+ name: "AES-GCM",
894
+ length: 256
895
+ },
896
+ true,
897
+ // extractable
898
+ ["encrypt", "decrypt"]
899
+ );
900
+ const iv = new Uint8Array(12);
901
+ const jsonData = JSON.stringify(data);
902
+ const encoder = new TextEncoder();
903
+ const encodedData = encoder.encode(jsonData);
904
+ const encryptedBuffer = await crypto.subtle.encrypt(
905
+ {
906
+ name: "AES-GCM",
907
+ iv
908
+ },
909
+ key,
910
+ encodedData
911
+ );
912
+ return {
913
+ encryptedData: arrayBufferToBase64(encryptedBuffer),
914
+ key
915
+ };
916
+ }
917
+ async function decryptProps(key, encryptedData) {
918
+ const encryptedBuffer = base64ToArrayBuffer(encryptedData);
919
+ const iv = new Uint8Array(12);
920
+ const decryptedBuffer = await crypto.subtle.decrypt(
921
+ {
922
+ name: "AES-GCM",
923
+ iv
924
+ },
925
+ key,
926
+ encryptedBuffer
927
+ );
928
+ const decoder = new TextDecoder();
929
+ const jsonData = decoder.decode(decryptedBuffer);
930
+ return JSON.parse(jsonData);
931
+ }
932
+ var WRAPPING_KEY_HMAC_KEY = new Uint8Array([
933
+ 34,
934
+ 126,
935
+ 38,
936
+ 134,
937
+ 141,
938
+ 241,
939
+ 225,
940
+ 109,
941
+ 128,
942
+ 112,
943
+ 234,
944
+ 23,
945
+ 151,
946
+ 91,
947
+ 71,
948
+ 166,
949
+ 130,
950
+ 24,
951
+ 250,
952
+ 135,
953
+ 40,
954
+ 174,
955
+ 222,
956
+ 133,
957
+ 181,
958
+ 29,
959
+ 74,
960
+ 217,
961
+ 150,
962
+ 202,
963
+ 202,
964
+ 67
965
+ ]);
966
+ async function deriveKeyFromToken(tokenStr) {
967
+ const encoder = new TextEncoder();
968
+ const hmacKey = await crypto.subtle.importKey(
969
+ "raw",
970
+ WRAPPING_KEY_HMAC_KEY,
971
+ { name: "HMAC", hash: "SHA-256" },
972
+ false,
973
+ ["sign"]
974
+ );
975
+ const hmacResult = await crypto.subtle.sign(
976
+ "HMAC",
977
+ hmacKey,
978
+ encoder.encode(tokenStr)
979
+ );
980
+ return await crypto.subtle.importKey(
981
+ "raw",
982
+ hmacResult,
983
+ { name: "AES-KW" },
984
+ false,
985
+ // not extractable
986
+ ["wrapKey", "unwrapKey"]
987
+ );
988
+ }
989
+ async function wrapKeyWithToken(tokenStr, keyToWrap) {
990
+ const wrappingKey = await deriveKeyFromToken(tokenStr);
991
+ const wrappedKeyBuffer = await crypto.subtle.wrapKey(
992
+ "raw",
993
+ keyToWrap,
994
+ wrappingKey,
995
+ { name: "AES-KW" }
996
+ );
997
+ return arrayBufferToBase64(wrappedKeyBuffer);
998
+ }
999
+ async function unwrapKeyWithToken(tokenStr, wrappedKeyBase64) {
1000
+ const wrappingKey = await deriveKeyFromToken(tokenStr);
1001
+ const wrappedKeyBuffer = base64ToArrayBuffer(wrappedKeyBase64);
1002
+ return await crypto.subtle.unwrapKey(
1003
+ "raw",
1004
+ wrappedKeyBuffer,
1005
+ wrappingKey,
1006
+ { name: "AES-KW" },
1007
+ { name: "AES-GCM" },
1008
+ true,
1009
+ // extractable
1010
+ ["encrypt", "decrypt"]
1011
+ );
1012
+ }
1013
+ var OAuthHelpersImpl = class {
1014
+ /**
1015
+ * Creates a new OAuthHelpers instance
1016
+ * @param env - Cloudflare Worker environment variables
1017
+ * @param provider - Reference to the parent provider instance
1018
+ */
1019
+ constructor(env, provider) {
1020
+ this.env = env;
1021
+ this.provider = provider;
1022
+ }
1023
+ /**
1024
+ * Parses an OAuth authorization request from the HTTP request
1025
+ * @param request - The HTTP request containing OAuth parameters
1026
+ * @returns The parsed authorization request parameters
1027
+ */
1028
+ async parseAuthRequest(request) {
1029
+ const url = new URL(request.url);
1030
+ const responseType = url.searchParams.get("response_type") || "";
1031
+ const clientId = url.searchParams.get("client_id") || "";
1032
+ const redirectUri = url.searchParams.get("redirect_uri") || "";
1033
+ const scope = (url.searchParams.get("scope") || "").split(" ").filter(Boolean);
1034
+ const state = url.searchParams.get("state") || "";
1035
+ const codeChallenge = url.searchParams.get("code_challenge") || void 0;
1036
+ const codeChallengeMethod = url.searchParams.get("code_challenge_method") || "plain";
1037
+ if (responseType === "token" && !this.provider.options.allowImplicitFlow) {
1038
+ throw new Error("The implicit grant flow is not enabled for this provider");
1039
+ }
1040
+ return {
1041
+ responseType,
1042
+ clientId,
1043
+ redirectUri,
1044
+ scope,
1045
+ state,
1046
+ codeChallenge,
1047
+ codeChallengeMethod
1048
+ };
1049
+ }
1050
+ /**
1051
+ * Looks up a client by its client ID
1052
+ * @param clientId - The client ID to look up
1053
+ * @returns A Promise resolving to the client info, or null if not found
1054
+ */
1055
+ async lookupClient(clientId) {
1056
+ return await this.provider.getClient(this.env, clientId);
1057
+ }
1058
+ /**
1059
+ * Completes an authorization request by creating a grant and either:
1060
+ * - For authorization code flow: generating an authorization code
1061
+ * - For implicit flow: generating an access token directly
1062
+ * @param options - Options specifying the grant details
1063
+ * @returns A Promise resolving to an object containing the redirect URL
1064
+ */
1065
+ async completeAuthorization(options) {
1066
+ const grantId = generateRandomString(16);
1067
+ const { encryptedData, key: encryptionKey } = await encryptProps(options.props);
1068
+ const now = Math.floor(Date.now() / 1e3);
1069
+ if (options.request.responseType === "token") {
1070
+ const accessTokenSecret = generateRandomString(TOKEN_LENGTH);
1071
+ const accessToken = `${options.userId}:${grantId}:${accessTokenSecret}`;
1072
+ const accessTokenId = await generateTokenId(accessToken);
1073
+ const accessTokenTTL = this.provider.options.accessTokenTTL || DEFAULT_ACCESS_TOKEN_TTL;
1074
+ const accessTokenExpiresAt = now + accessTokenTTL;
1075
+ const accessTokenWrappedKey = await wrapKeyWithToken(accessToken, encryptionKey);
1076
+ const grant = {
1077
+ id: grantId,
1078
+ clientId: options.request.clientId,
1079
+ userId: options.userId,
1080
+ scope: options.scope,
1081
+ metadata: options.metadata,
1082
+ encryptedProps: encryptedData,
1083
+ createdAt: now
1084
+ };
1085
+ const grantKey = `grant:${options.userId}:${grantId}`;
1086
+ await this.env.OAUTH_KV.put(grantKey, JSON.stringify(grant));
1087
+ const accessTokenData = {
1088
+ id: accessTokenId,
1089
+ grantId,
1090
+ userId: options.userId,
1091
+ createdAt: now,
1092
+ expiresAt: accessTokenExpiresAt,
1093
+ wrappedEncryptionKey: accessTokenWrappedKey,
1094
+ grant: {
1095
+ clientId: options.request.clientId,
1096
+ scope: options.scope,
1097
+ encryptedProps: encryptedData
1098
+ }
1099
+ };
1100
+ await this.env.OAUTH_KV.put(
1101
+ `token:${options.userId}:${grantId}:${accessTokenId}`,
1102
+ JSON.stringify(accessTokenData),
1103
+ { expirationTtl: accessTokenTTL }
1104
+ );
1105
+ const redirectUrl = new URL(options.request.redirectUri);
1106
+ const fragment = new URLSearchParams();
1107
+ fragment.set("access_token", accessToken);
1108
+ fragment.set("token_type", "bearer");
1109
+ fragment.set("expires_in", accessTokenTTL.toString());
1110
+ fragment.set("scope", options.scope.join(" "));
1111
+ if (options.request.state) {
1112
+ fragment.set("state", options.request.state);
1113
+ }
1114
+ redirectUrl.hash = fragment.toString();
1115
+ return { redirectTo: redirectUrl.toString() };
1116
+ } else {
1117
+ const authCodeSecret = generateRandomString(32);
1118
+ const authCode = `${options.userId}:${grantId}:${authCodeSecret}`;
1119
+ const authCodeId = await hashSecret(authCode);
1120
+ const authCodeWrappedKey = await wrapKeyWithToken(authCode, encryptionKey);
1121
+ const grant = {
1122
+ id: grantId,
1123
+ clientId: options.request.clientId,
1124
+ userId: options.userId,
1125
+ scope: options.scope,
1126
+ metadata: options.metadata,
1127
+ encryptedProps: encryptedData,
1128
+ createdAt: now,
1129
+ authCodeId,
1130
+ // Store the auth code hash in the grant
1131
+ authCodeWrappedKey,
1132
+ // Store the wrapped key
1133
+ // Store PKCE parameters if provided
1134
+ codeChallenge: options.request.codeChallenge,
1135
+ codeChallengeMethod: options.request.codeChallengeMethod
1136
+ };
1137
+ const grantKey = `grant:${options.userId}:${grantId}`;
1138
+ const codeExpiresIn = 600;
1139
+ await this.env.OAUTH_KV.put(grantKey, JSON.stringify(grant), { expirationTtl: codeExpiresIn });
1140
+ const redirectUrl = new URL(options.request.redirectUri);
1141
+ redirectUrl.searchParams.set("code", authCode);
1142
+ if (options.request.state) {
1143
+ redirectUrl.searchParams.set("state", options.request.state);
1144
+ }
1145
+ return { redirectTo: redirectUrl.toString() };
1146
+ }
1147
+ }
1148
+ /**
1149
+ * Creates a new OAuth client
1150
+ * @param clientInfo - Partial client information to create the client with
1151
+ * @returns A Promise resolving to the created client info
1152
+ */
1153
+ async createClient(clientInfo) {
1154
+ const clientId = generateRandomString(16);
1155
+ const tokenEndpointAuthMethod = clientInfo.tokenEndpointAuthMethod || "client_secret_basic";
1156
+ const isPublicClient = tokenEndpointAuthMethod === "none";
1157
+ const newClient = {
1158
+ clientId,
1159
+ redirectUris: clientInfo.redirectUris || [],
1160
+ clientName: clientInfo.clientName,
1161
+ logoUri: clientInfo.logoUri,
1162
+ clientUri: clientInfo.clientUri,
1163
+ policyUri: clientInfo.policyUri,
1164
+ tosUri: clientInfo.tosUri,
1165
+ jwksUri: clientInfo.jwksUri,
1166
+ contacts: clientInfo.contacts,
1167
+ grantTypes: clientInfo.grantTypes || ["authorization_code", "refresh_token"],
1168
+ responseTypes: clientInfo.responseTypes || ["code"],
1169
+ registrationDate: Math.floor(Date.now() / 1e3),
1170
+ tokenEndpointAuthMethod
1171
+ };
1172
+ let clientSecret;
1173
+ if (!isPublicClient) {
1174
+ clientSecret = generateRandomString(32);
1175
+ newClient.clientSecret = await hashSecret(clientSecret);
1176
+ }
1177
+ await this.env.OAUTH_KV.put(`client:${clientId}`, JSON.stringify(newClient));
1178
+ const clientResponse = { ...newClient };
1179
+ if (!isPublicClient && clientSecret) {
1180
+ clientResponse.clientSecret = clientSecret;
1181
+ }
1182
+ return clientResponse;
1183
+ }
1184
+ /**
1185
+ * Lists all registered OAuth clients with pagination support
1186
+ * @param options - Optional pagination parameters (limit and cursor)
1187
+ * @returns A Promise resolving to the list result with items and optional cursor
1188
+ */
1189
+ async listClients(options) {
1190
+ const listOptions = {
1191
+ prefix: "client:"
1192
+ };
1193
+ if (options?.limit !== void 0) {
1194
+ listOptions.limit = options.limit;
1195
+ }
1196
+ if (options?.cursor !== void 0) {
1197
+ listOptions.cursor = options.cursor;
1198
+ }
1199
+ const response = await this.env.OAUTH_KV.list(listOptions);
1200
+ const clients = [];
1201
+ const promises = response.keys.map(async (key) => {
1202
+ const clientId = key.name.substring("client:".length);
1203
+ const client = await this.provider.getClient(this.env, clientId);
1204
+ if (client) {
1205
+ clients.push(client);
1206
+ }
1207
+ });
1208
+ await Promise.all(promises);
1209
+ return {
1210
+ items: clients,
1211
+ cursor: response.list_complete ? void 0 : response.cursor
1212
+ };
1213
+ }
1214
+ /**
1215
+ * Updates an existing OAuth client
1216
+ * @param clientId - The ID of the client to update
1217
+ * @param updates - Partial client information with fields to update
1218
+ * @returns A Promise resolving to the updated client info, or null if not found
1219
+ */
1220
+ async updateClient(clientId, updates) {
1221
+ const client = await this.provider.getClient(this.env, clientId);
1222
+ if (!client) {
1223
+ return null;
1224
+ }
1225
+ let authMethod = updates.tokenEndpointAuthMethod || client.tokenEndpointAuthMethod || "client_secret_basic";
1226
+ const isPublicClient = authMethod === "none";
1227
+ let secretToStore = client.clientSecret;
1228
+ let originalSecret = void 0;
1229
+ if (isPublicClient) {
1230
+ secretToStore = void 0;
1231
+ } else if (updates.clientSecret) {
1232
+ originalSecret = updates.clientSecret;
1233
+ secretToStore = await hashSecret(updates.clientSecret);
1234
+ }
1235
+ const updatedClient = {
1236
+ ...client,
1237
+ ...updates,
1238
+ clientId: client.clientId,
1239
+ // Ensure clientId doesn't change
1240
+ tokenEndpointAuthMethod: authMethod
1241
+ // Use determined auth method
1242
+ };
1243
+ if (!isPublicClient && secretToStore) {
1244
+ updatedClient.clientSecret = secretToStore;
1245
+ } else {
1246
+ delete updatedClient.clientSecret;
1247
+ }
1248
+ await this.env.OAUTH_KV.put(`client:${clientId}`, JSON.stringify(updatedClient));
1249
+ const response = { ...updatedClient };
1250
+ if (!isPublicClient && originalSecret) {
1251
+ response.clientSecret = originalSecret;
1252
+ }
1253
+ return response;
1254
+ }
1255
+ /**
1256
+ * Deletes an OAuth client
1257
+ * @param clientId - The ID of the client to delete
1258
+ * @returns A Promise resolving when the deletion is confirmed.
1259
+ */
1260
+ async deleteClient(clientId) {
1261
+ await this.env.OAUTH_KV.delete(`client:${clientId}`);
1262
+ }
1263
+ /**
1264
+ * Lists all authorization grants for a specific user with pagination support
1265
+ * Returns a summary of each grant without sensitive information
1266
+ * @param userId - The ID of the user whose grants to list
1267
+ * @param options - Optional pagination parameters (limit and cursor)
1268
+ * @returns A Promise resolving to the list result with grant summaries and optional cursor
1269
+ */
1270
+ async listUserGrants(userId, options) {
1271
+ const listOptions = {
1272
+ prefix: `grant:${userId}:`
1273
+ };
1274
+ if (options?.limit !== void 0) {
1275
+ listOptions.limit = options.limit;
1276
+ }
1277
+ if (options?.cursor !== void 0) {
1278
+ listOptions.cursor = options.cursor;
1279
+ }
1280
+ const response = await this.env.OAUTH_KV.list(listOptions);
1281
+ const grantSummaries = [];
1282
+ const promises = response.keys.map(async (key) => {
1283
+ const grantData = await this.env.OAUTH_KV.get(key.name, { type: "json" });
1284
+ if (grantData) {
1285
+ const summary = {
1286
+ id: grantData.id,
1287
+ clientId: grantData.clientId,
1288
+ userId: grantData.userId,
1289
+ scope: grantData.scope,
1290
+ metadata: grantData.metadata,
1291
+ createdAt: grantData.createdAt
1292
+ };
1293
+ grantSummaries.push(summary);
1294
+ }
1295
+ });
1296
+ await Promise.all(promises);
1297
+ return {
1298
+ items: grantSummaries,
1299
+ cursor: response.list_complete ? void 0 : response.cursor
1300
+ };
1301
+ }
1302
+ /**
1303
+ * Revokes an authorization grant and all its associated access tokens
1304
+ * @param grantId - The ID of the grant to revoke
1305
+ * @param userId - The ID of the user who owns the grant
1306
+ * @returns A Promise resolving when the revocation is confirmed.
1307
+ */
1308
+ async revokeGrant(grantId, userId) {
1309
+ const grantKey = `grant:${userId}:${grantId}`;
1310
+ const tokenPrefix = `token:${userId}:${grantId}:`;
1311
+ let cursor;
1312
+ let allTokensDeleted = false;
1313
+ while (!allTokensDeleted) {
1314
+ const listOptions = {
1315
+ prefix: tokenPrefix
1316
+ };
1317
+ if (cursor) {
1318
+ listOptions.cursor = cursor;
1319
+ }
1320
+ const result = await this.env.OAUTH_KV.list(listOptions);
1321
+ if (result.keys.length > 0) {
1322
+ await Promise.all(result.keys.map((key) => {
1323
+ return this.env.OAUTH_KV.delete(key.name);
1324
+ }));
1325
+ }
1326
+ if (result.list_complete) {
1327
+ allTokensDeleted = true;
1328
+ } else {
1329
+ cursor = result.cursor;
1330
+ }
1331
+ }
1332
+ await this.env.OAUTH_KV.delete(grantKey);
1333
+ }
1334
+ };
1335
+ var oauth_provider_default = OAuthProvider;
1336
+ export {
1337
+ OAuthProvider,
1338
+ oauth_provider_default as default
1339
+ };