@cloudflare/workers-oauth-provider 0.4.0 → 0.5.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 +39 -3
- package/dist/oauth-provider.d.ts +86 -2
- package/dist/oauth-provider.js +135 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,15 +96,21 @@ export default new OAuthProvider({
|
|
|
96
96
|
disallowPublicClientRegistration: false,
|
|
97
97
|
|
|
98
98
|
// Optional: Time-to-live for refresh tokens in seconds.
|
|
99
|
-
//
|
|
99
|
+
// Defaults to 30 days (2,592,000 seconds).
|
|
100
100
|
// Set to 0 to disable refresh tokens (only access tokens will be issued).
|
|
101
|
-
//
|
|
102
|
-
refreshTokenTTL: 2592000, // 30 days
|
|
101
|
+
// Set to `undefined` explicitly for refresh tokens that never expire.
|
|
102
|
+
refreshTokenTTL: 2592000, // 30 days (the default)
|
|
103
103
|
|
|
104
104
|
// Optional: Time-to-live for access tokens in seconds.
|
|
105
105
|
// Defaults to 1 hour (3600 seconds) if not specified.
|
|
106
106
|
accessTokenTTL: 3600,
|
|
107
107
|
|
|
108
|
+
// Optional: Time-to-live for dynamically registered clients in seconds.
|
|
109
|
+
// Defaults to 90 days (7,776,000 seconds).
|
|
110
|
+
// Clients created via OAuthHelpers.createClient() are not affected.
|
|
111
|
+
// Set to `undefined` explicitly for clients that never expire.
|
|
112
|
+
clientRegistrationTTL: 7776000, // 90 days (the default)
|
|
113
|
+
|
|
108
114
|
// Optional: Controls whether OAuth 2.0 Token Exchange (RFC 8693) is allowed.
|
|
109
115
|
// When false, the token exchange grant type will not be advertised in metadata
|
|
110
116
|
// and token exchange requests will be rejected.
|
|
@@ -226,6 +232,9 @@ The `env.OAUTH_PROVIDER` object available to the fetch handlers provides some me
|
|
|
226
232
|
- Create, list, modify, and delete client_id registrations (in addition to `lookupClient()`, already shown in the example code).
|
|
227
233
|
- List all active authorization grants for a particular user.
|
|
228
234
|
- Revoke (delete) an authorization grant.
|
|
235
|
+
- Purge expired and orphaned data from the KV namespace.
|
|
236
|
+
|
|
237
|
+
Note that `deleteClient()` cascades: it revokes all grants (and their associated tokens) for the deleted client across all users.
|
|
229
238
|
|
|
230
239
|
See the `OAuthHelpers` interface definition for full API details.
|
|
231
240
|
|
|
@@ -326,6 +335,33 @@ new OAuthProvider({
|
|
|
326
335
|
|
|
327
336
|
By default, the `onError` callback is set to ``({ status, code, description }) => console.warn(`OAuth error response: ${status} ${code} - ${description}`)``.
|
|
328
337
|
|
|
338
|
+
## KV Namespace Cleanup
|
|
339
|
+
|
|
340
|
+
The library uses KV TTLs to automatically expire access tokens, refresh tokens (grants), and dynamically registered clients. As defense-in-depth, the library also provides a `purgeExpiredData()` method that cleans up orphaned and expired records. This is designed to be called from a [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/) (scheduled handler):
|
|
341
|
+
|
|
342
|
+
```ts
|
|
343
|
+
const oauthProvider = new OAuthProvider({
|
|
344
|
+
// ... options ...
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
export default {
|
|
348
|
+
fetch(request, env, ctx) {
|
|
349
|
+
return oauthProvider.fetch(request, env, ctx);
|
|
350
|
+
},
|
|
351
|
+
async scheduled(event, env, ctx) {
|
|
352
|
+
const result = await oauthProvider.purgeExpiredData(env, { batchSize: 100 });
|
|
353
|
+
console.log(`Checked ${result.grantsChecked} grants, purged ${result.grantsPurged}`);
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
The method processes records in configurable batches (default: 50) to stay within Cloudflare's subrequest limits. It performs two sweep phases:
|
|
359
|
+
|
|
360
|
+
1. **Grant sweep**: Removes orphaned grants (whose client no longer exists) and expired grants.
|
|
361
|
+
2. **Token sweep**: Removes orphaned tokens (whose grant no longer exists).
|
|
362
|
+
|
|
363
|
+
Call it repeatedly via a cron trigger — deleted records disappear from KV, so subsequent invocations naturally process fresh records without needing a persisted cursor. The `result.done` field indicates whether the full key space was scanned in this invocation.
|
|
364
|
+
|
|
329
365
|
## Protected Resource Metadata (RFC 9728)
|
|
330
366
|
|
|
331
367
|
The library automatically serves a `/.well-known/oauth-protected-resource` endpoint. By default, it uses the request origin as the resource identifier and the token endpoint's origin as the authorization server. You can customize this with the `resourceMetadata` option:
|
package/dist/oauth-provider.d.ts
CHANGED
|
@@ -178,10 +178,20 @@ interface OAuthProviderOptions<Env = Cloudflare.Env> {
|
|
|
178
178
|
accessTokenTTL?: number;
|
|
179
179
|
/**
|
|
180
180
|
* Time-to-live for refresh tokens in seconds.
|
|
181
|
-
*
|
|
181
|
+
* Defaults to 30 days (2,592,000 seconds).
|
|
182
|
+
* Set to 0 to disable refresh tokens entirely.
|
|
183
|
+
* Set to `undefined` explicitly for refresh tokens that never expire.
|
|
182
184
|
* For example: 3600 = 1 hour, 2592000 = 30 days
|
|
183
185
|
*/
|
|
184
186
|
refreshTokenTTL?: number;
|
|
187
|
+
/**
|
|
188
|
+
* Time-to-live for dynamically registered clients in seconds.
|
|
189
|
+
* Defaults to 90 days (7,776,000 seconds).
|
|
190
|
+
* Clients created via the DCR endpoint will automatically expire after this duration.
|
|
191
|
+
* Clients created via `OAuthHelpers.createClient()` are not affected by this setting.
|
|
192
|
+
* Set to `undefined` explicitly for clients that never expire.
|
|
193
|
+
*/
|
|
194
|
+
clientRegistrationTTL?: number;
|
|
185
195
|
/**
|
|
186
196
|
* List of scopes supported by this OAuth provider.
|
|
187
197
|
* If not provided, the 'scopes_supported' field will be omitted from the OAuth metadata.
|
|
@@ -375,6 +385,22 @@ interface OAuthHelpers {
|
|
|
375
385
|
* @returns Promise resolving to token response with new access token
|
|
376
386
|
*/
|
|
377
387
|
exchangeToken(options: ExchangeTokenOptions): Promise<TokenResponse>;
|
|
388
|
+
/**
|
|
389
|
+
* Purges expired and orphaned data from the KV namespace.
|
|
390
|
+
* Designed to be called from a scheduled handler (Cron Trigger) for periodic cleanup.
|
|
391
|
+
* Processes records in configurable batches to stay within Cloudflare's subrequest limits.
|
|
392
|
+
*
|
|
393
|
+
* Performs two sweep phases:
|
|
394
|
+
* 1. Grant sweep: removes orphaned grants (client deleted) and expired grants (defense-in-depth for KV TTL)
|
|
395
|
+
* 2. Token sweep: removes orphaned tokens (grant deleted) as defense-in-depth
|
|
396
|
+
*
|
|
397
|
+
* Safe to call repeatedly — deleted records disappear from KV, so subsequent invocations
|
|
398
|
+
* naturally process fresh records without needing a persisted cursor.
|
|
399
|
+
*
|
|
400
|
+
* @param options - Optional configuration for batch size and which purge types to enable
|
|
401
|
+
* @returns Statistics about what was checked and purged, and whether the full scan completed
|
|
402
|
+
*/
|
|
403
|
+
purgeExpiredData(options?: PurgeOptions): Promise<PurgeResult>;
|
|
378
404
|
}
|
|
379
405
|
/**
|
|
380
406
|
* Options for token exchange operations (RFC 8693)
|
|
@@ -742,6 +768,55 @@ interface ListResult<T> {
|
|
|
742
768
|
*/
|
|
743
769
|
cursor?: string;
|
|
744
770
|
}
|
|
771
|
+
/**
|
|
772
|
+
* Options for the purgeExpiredData garbage collection method
|
|
773
|
+
*/
|
|
774
|
+
interface PurgeOptions {
|
|
775
|
+
/**
|
|
776
|
+
* Maximum number of KV keys to check per phase (grants and tokens) per invocation.
|
|
777
|
+
* Each phase (grant sweep, token sweep) gets its own budget of this size.
|
|
778
|
+
* Keep this conservative to stay within Cloudflare's 1000 subrequest limit per invocation,
|
|
779
|
+
* since each checked key requires at least one KV read, and orphaned grants trigger
|
|
780
|
+
* additional KV operations via revokeGrant().
|
|
781
|
+
* Defaults to 50.
|
|
782
|
+
*/
|
|
783
|
+
batchSize?: number;
|
|
784
|
+
/**
|
|
785
|
+
* Whether to purge orphaned grants whose client no longer exists in KV.
|
|
786
|
+
* Grants for CIMD (Client ID Metadata Document) clients are always skipped
|
|
787
|
+
* since those clients are not stored in KV.
|
|
788
|
+
* Defaults to true.
|
|
789
|
+
*/
|
|
790
|
+
purgeOrphanedGrants?: boolean;
|
|
791
|
+
/**
|
|
792
|
+
* Whether to purge expired grants as defense-in-depth for KV TTL.
|
|
793
|
+
* Normally KV auto-deletes expired entries, but this catches any stragglers.
|
|
794
|
+
* Defaults to true.
|
|
795
|
+
*/
|
|
796
|
+
purgeExpiredGrants?: boolean;
|
|
797
|
+
/**
|
|
798
|
+
* Whether to purge orphaned tokens whose grant no longer exists.
|
|
799
|
+
* Tokens already auto-expire via KV TTL (default 1 hour), so this is
|
|
800
|
+
* defense-in-depth for partial revokeGrant() failures.
|
|
801
|
+
* Defaults to true.
|
|
802
|
+
*/
|
|
803
|
+
purgeOrphanedTokens?: boolean;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Result of a purgeExpiredData garbage collection invocation
|
|
807
|
+
*/
|
|
808
|
+
interface PurgeResult {
|
|
809
|
+
/** Number of grant records checked in this invocation */
|
|
810
|
+
grantsChecked: number;
|
|
811
|
+
/** Number of grant records purged (orphaned or expired) */
|
|
812
|
+
grantsPurged: number;
|
|
813
|
+
/** Number of token records checked in this invocation */
|
|
814
|
+
tokensChecked: number;
|
|
815
|
+
/** Number of token records purged (orphaned) */
|
|
816
|
+
tokensPurged: number;
|
|
817
|
+
/** True if the full key space was scanned in this invocation (both grants and tokens) */
|
|
818
|
+
done: boolean;
|
|
819
|
+
}
|
|
745
820
|
/**
|
|
746
821
|
* Public representation of a grant, with sensitive data removed
|
|
747
822
|
* Used for list operations where the complete grant data isn't needed
|
|
@@ -797,6 +872,15 @@ declare class OAuthProvider<Env = Cloudflare.Env> {
|
|
|
797
872
|
* @returns A Promise resolving to an HTTP Response
|
|
798
873
|
*/
|
|
799
874
|
fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response>;
|
|
875
|
+
/**
|
|
876
|
+
* Purges expired and orphaned data from the KV namespace.
|
|
877
|
+
* Can be called directly from a scheduled handler without needing a request context.
|
|
878
|
+
*
|
|
879
|
+
* @param env - Cloudflare Worker environment variables (must include OAUTH_KV binding)
|
|
880
|
+
* @param options - Optional configuration for batch size and which purge types to enable
|
|
881
|
+
* @returns Statistics about what was checked and purged
|
|
882
|
+
*/
|
|
883
|
+
purgeExpiredData(env: Env, options?: PurgeOptions): Promise<PurgeResult>;
|
|
800
884
|
}
|
|
801
885
|
/**
|
|
802
886
|
* Gets OAuthHelpers for the given environment
|
|
@@ -806,4 +890,4 @@ declare class OAuthProvider<Env = Cloudflare.Env> {
|
|
|
806
890
|
*/
|
|
807
891
|
declare function getOAuthApi<Env = Cloudflare.Env>(options: OAuthProviderOptions<Env>, env: Env): OAuthHelpers;
|
|
808
892
|
//#endregion
|
|
809
|
-
export { AuthRequest, ClientInfo, CompleteAuthorizationOptions, ExchangeTokenOptions, Grant, GrantSummary, GrantType, ListOptions, ListResult, OAuthHelpers, OAuthProvider, OAuthProvider as default, OAuthProviderOptions, ResolveExternalTokenInput, ResolveExternalTokenResult, Token, TokenBase, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, TokenSummary, getOAuthApi };
|
|
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 };
|
package/dist/oauth-provider.js
CHANGED
|
@@ -45,6 +45,17 @@ var OAuthProvider = class {
|
|
|
45
45
|
fetch(request, env, ctx) {
|
|
46
46
|
return this.#impl.fetch(request, env, ctx);
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Purges expired and orphaned data from the KV namespace.
|
|
50
|
+
* Can be called directly from a scheduled handler without needing a request context.
|
|
51
|
+
*
|
|
52
|
+
* @param env - Cloudflare Worker environment variables (must include OAUTH_KV binding)
|
|
53
|
+
* @param options - Optional configuration for batch size and which purge types to enable
|
|
54
|
+
* @returns Statistics about what was checked and purged
|
|
55
|
+
*/
|
|
56
|
+
purgeExpiredData(env, options) {
|
|
57
|
+
return this.#impl.createOAuthHelpers(env).purgeExpiredData(options);
|
|
58
|
+
}
|
|
48
59
|
};
|
|
49
60
|
/**
|
|
50
61
|
* Gets OAuthHelpers for the given environment
|
|
@@ -94,6 +105,8 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
94
105
|
if (options.clientRegistrationEndpoint) this.validateEndpoint(options.clientRegistrationEndpoint, "clientRegistrationEndpoint");
|
|
95
106
|
this.options = {
|
|
96
107
|
accessTokenTTL: DEFAULT_ACCESS_TOKEN_TTL,
|
|
108
|
+
refreshTokenTTL: DEFAULT_REFRESH_TOKEN_TTL,
|
|
109
|
+
clientRegistrationTTL: DEFAULT_CLIENT_REGISTRATION_TTL,
|
|
97
110
|
onError: ({ status, code, description }) => console.warn(`OAuth error response: ${status} ${code} - ${description}`),
|
|
98
111
|
...options
|
|
99
112
|
};
|
|
@@ -945,7 +958,9 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
945
958
|
} catch (error) {
|
|
946
959
|
return this.createErrorResponse("invalid_client_metadata", error instanceof Error ? error.message : "Invalid client metadata");
|
|
947
960
|
}
|
|
948
|
-
|
|
961
|
+
const clientKvOptions = {};
|
|
962
|
+
if (this.options.clientRegistrationTTL !== void 0) clientKvOptions.expirationTtl = this.options.clientRegistrationTTL;
|
|
963
|
+
await env.OAUTH_KV.put(`client:${clientId}`, JSON.stringify(clientInfo), clientKvOptions);
|
|
949
964
|
const response = {
|
|
950
965
|
client_id: clientInfo.clientId,
|
|
951
966
|
redirect_uris: clientInfo.redirectUris,
|
|
@@ -964,7 +979,7 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
964
979
|
};
|
|
965
980
|
if (clientSecret) {
|
|
966
981
|
response.client_secret = clientSecret;
|
|
967
|
-
response.client_secret_expires_at = 0;
|
|
982
|
+
response.client_secret_expires_at = this.options.clientRegistrationTTL && clientInfo.registrationDate ? clientInfo.registrationDate + this.options.clientRegistrationTTL : 0;
|
|
968
983
|
response.client_secret_issued_at = clientInfo.registrationDate;
|
|
969
984
|
}
|
|
970
985
|
return new Response(JSON.stringify(response), {
|
|
@@ -1123,7 +1138,8 @@ var OAuthProviderImpl = class OAuthProviderImpl {
|
|
|
1123
1138
|
return !!(typeof Cloudflare !== "undefined" && Cloudflare.compatibilityFlags ? Cloudflare.compatibilityFlags : null)?.global_fetch_strictly_public;
|
|
1124
1139
|
}
|
|
1125
1140
|
/**
|
|
1126
|
-
* Checks if a client_id is a CIMD URL (HTTPS with non-root path)
|
|
1141
|
+
* Checks if a client_id is a CIMD URL (HTTPS with non-root path).
|
|
1142
|
+
* Not private because OAuthHelpersImpl needs access for purgeExpiredData.
|
|
1127
1143
|
*/
|
|
1128
1144
|
isClientMetadataUrl(clientId) {
|
|
1129
1145
|
try {
|
|
@@ -1304,6 +1320,19 @@ var OAuthError = class extends Error {
|
|
|
1304
1320
|
*/
|
|
1305
1321
|
const DEFAULT_ACCESS_TOKEN_TTL = 3600;
|
|
1306
1322
|
/**
|
|
1323
|
+
* Default expiration time for refresh tokens (30 days in seconds)
|
|
1324
|
+
*/
|
|
1325
|
+
const DEFAULT_REFRESH_TOKEN_TTL = 720 * 60 * 60;
|
|
1326
|
+
/**
|
|
1327
|
+
* Default expiration time for dynamically registered clients (90 days in seconds)
|
|
1328
|
+
*/
|
|
1329
|
+
const DEFAULT_CLIENT_REGISTRATION_TTL = 2160 * 60 * 60;
|
|
1330
|
+
/**
|
|
1331
|
+
* Default batch size for purgeExpiredData. Conservative to stay within
|
|
1332
|
+
* Cloudflare's 1000 subrequest limit per invocation.
|
|
1333
|
+
*/
|
|
1334
|
+
const DEFAULT_PURGE_BATCH_SIZE = 50;
|
|
1335
|
+
/**
|
|
1307
1336
|
* Length of generated token strings
|
|
1308
1337
|
*/
|
|
1309
1338
|
const TOKEN_LENGTH = 32;
|
|
@@ -1846,17 +1875,32 @@ var OAuthHelpersImpl = class {
|
|
|
1846
1875
|
};
|
|
1847
1876
|
if (!isPublicClient && secretToStore) updatedClient.clientSecret = secretToStore;
|
|
1848
1877
|
else delete updatedClient.clientSecret;
|
|
1849
|
-
|
|
1878
|
+
const clientKvOptions = {};
|
|
1879
|
+
if (this.provider.options.clientRegistrationTTL !== void 0) clientKvOptions.expirationTtl = this.provider.options.clientRegistrationTTL;
|
|
1880
|
+
await this.env.OAUTH_KV.put(`client:${clientId}`, JSON.stringify(updatedClient), clientKvOptions);
|
|
1850
1881
|
const response = { ...updatedClient };
|
|
1851
1882
|
if (!isPublicClient && originalSecret) response.clientSecret = originalSecret;
|
|
1852
1883
|
return response;
|
|
1853
1884
|
}
|
|
1854
1885
|
/**
|
|
1855
|
-
* Deletes an OAuth client
|
|
1886
|
+
* Deletes an OAuth client and revokes all associated grants across all users.
|
|
1856
1887
|
* @param clientId - The ID of the client to delete
|
|
1857
1888
|
* @returns A Promise resolving when the deletion is confirmed.
|
|
1858
1889
|
*/
|
|
1859
1890
|
async deleteClient(clientId) {
|
|
1891
|
+
let cursor;
|
|
1892
|
+
let allProcessed = false;
|
|
1893
|
+
while (!allProcessed) {
|
|
1894
|
+
const listOptions = { prefix: "grant:" };
|
|
1895
|
+
if (cursor) listOptions.cursor = cursor;
|
|
1896
|
+
const result = await this.env.OAUTH_KV.list(listOptions);
|
|
1897
|
+
for (const key of result.keys) {
|
|
1898
|
+
const grantData = await this.env.OAUTH_KV.get(key.name, { type: "json" });
|
|
1899
|
+
if (grantData && grantData.clientId === clientId) await this.revokeGrant(grantData.id, grantData.userId);
|
|
1900
|
+
}
|
|
1901
|
+
if (result.list_complete) allProcessed = true;
|
|
1902
|
+
else cursor = result.cursor;
|
|
1903
|
+
}
|
|
1860
1904
|
await this.env.OAUTH_KV.delete(`client:${clientId}`);
|
|
1861
1905
|
}
|
|
1862
1906
|
/**
|
|
@@ -1937,6 +1981,92 @@ var OAuthHelpersImpl = class {
|
|
|
1937
1981
|
if (!clientInfo) throw new Error("Client not found");
|
|
1938
1982
|
return await this.provider.exchangeToken(options.subjectToken, options.scope, options.aud, options.expiresIn, clientInfo, this.env);
|
|
1939
1983
|
}
|
|
1984
|
+
async purgeExpiredData(options) {
|
|
1985
|
+
const batchSize = options?.batchSize ?? DEFAULT_PURGE_BATCH_SIZE;
|
|
1986
|
+
const purgeOrphanedGrants = options?.purgeOrphanedGrants !== false;
|
|
1987
|
+
const purgeExpiredGrants = options?.purgeExpiredGrants !== false;
|
|
1988
|
+
const purgeOrphanedTokens = options?.purgeOrphanedTokens !== false;
|
|
1989
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1990
|
+
const result = {
|
|
1991
|
+
grantsChecked: 0,
|
|
1992
|
+
grantsPurged: 0,
|
|
1993
|
+
tokensChecked: 0,
|
|
1994
|
+
tokensPurged: 0,
|
|
1995
|
+
done: false
|
|
1996
|
+
};
|
|
1997
|
+
if (purgeOrphanedGrants || purgeExpiredGrants) {
|
|
1998
|
+
const knownGoodClients = /* @__PURE__ */ new Set();
|
|
1999
|
+
const knownMissingClients = /* @__PURE__ */ new Set();
|
|
2000
|
+
let grantCursor;
|
|
2001
|
+
let grantsDone = false;
|
|
2002
|
+
while (!grantsDone && result.grantsChecked < batchSize) {
|
|
2003
|
+
const listOptions = {
|
|
2004
|
+
prefix: "grant:",
|
|
2005
|
+
limit: Math.min(1e3, batchSize - result.grantsChecked)
|
|
2006
|
+
};
|
|
2007
|
+
if (grantCursor) listOptions.cursor = grantCursor;
|
|
2008
|
+
const page = await this.env.OAUTH_KV.list(listOptions);
|
|
2009
|
+
for (const key of page.keys) {
|
|
2010
|
+
if (result.grantsChecked >= batchSize) break;
|
|
2011
|
+
result.grantsChecked++;
|
|
2012
|
+
const grantData = await this.env.OAUTH_KV.get(key.name, { type: "json" });
|
|
2013
|
+
if (!grantData) continue;
|
|
2014
|
+
let shouldPurge = false;
|
|
2015
|
+
if (purgeExpiredGrants && grantData.expiresAt !== void 0 && now >= grantData.expiresAt) shouldPurge = true;
|
|
2016
|
+
if (!shouldPurge && purgeOrphanedGrants && !this.provider.isClientMetadataUrl(grantData.clientId)) {
|
|
2017
|
+
if (knownMissingClients.has(grantData.clientId)) shouldPurge = true;
|
|
2018
|
+
else if (!knownGoodClients.has(grantData.clientId)) if (await this.env.OAUTH_KV.get(`client:${grantData.clientId}`, { type: "json" })) knownGoodClients.add(grantData.clientId);
|
|
2019
|
+
else {
|
|
2020
|
+
knownMissingClients.add(grantData.clientId);
|
|
2021
|
+
shouldPurge = true;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
if (shouldPurge) {
|
|
2025
|
+
await this.revokeGrant(grantData.id, grantData.userId);
|
|
2026
|
+
result.grantsPurged++;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
if (page.list_complete) grantsDone = true;
|
|
2030
|
+
else grantCursor = page.cursor;
|
|
2031
|
+
}
|
|
2032
|
+
if (!grantsDone) return result;
|
|
2033
|
+
}
|
|
2034
|
+
if (purgeOrphanedTokens) {
|
|
2035
|
+
const knownGoodGrants = /* @__PURE__ */ new Set();
|
|
2036
|
+
const knownMissingGrants = /* @__PURE__ */ new Set();
|
|
2037
|
+
let tokenCursor;
|
|
2038
|
+
let tokensDone = false;
|
|
2039
|
+
while (!tokensDone && result.tokensChecked < batchSize) {
|
|
2040
|
+
const listOptions = {
|
|
2041
|
+
prefix: "token:",
|
|
2042
|
+
limit: Math.min(1e3, batchSize - result.tokensChecked)
|
|
2043
|
+
};
|
|
2044
|
+
if (tokenCursor) listOptions.cursor = tokenCursor;
|
|
2045
|
+
const page = await this.env.OAUTH_KV.list(listOptions);
|
|
2046
|
+
for (const key of page.keys) {
|
|
2047
|
+
if (result.tokensChecked >= batchSize) break;
|
|
2048
|
+
result.tokensChecked++;
|
|
2049
|
+
const tokenData = await this.env.OAUTH_KV.get(key.name, { type: "json" });
|
|
2050
|
+
if (!tokenData) continue;
|
|
2051
|
+
const grantKey = `grant:${tokenData.userId}:${tokenData.grantId}`;
|
|
2052
|
+
if (knownMissingGrants.has(grantKey)) {
|
|
2053
|
+
await this.env.OAUTH_KV.delete(key.name);
|
|
2054
|
+
result.tokensPurged++;
|
|
2055
|
+
} else if (!knownGoodGrants.has(grantKey)) if (await this.env.OAUTH_KV.get(grantKey)) knownGoodGrants.add(grantKey);
|
|
2056
|
+
else {
|
|
2057
|
+
knownMissingGrants.add(grantKey);
|
|
2058
|
+
await this.env.OAUTH_KV.delete(key.name);
|
|
2059
|
+
result.tokensPurged++;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
if (page.list_complete) tokensDone = true;
|
|
2063
|
+
else tokenCursor = page.cursor;
|
|
2064
|
+
}
|
|
2065
|
+
if (!tokensDone) return result;
|
|
2066
|
+
}
|
|
2067
|
+
result.done = true;
|
|
2068
|
+
return result;
|
|
2069
|
+
}
|
|
1940
2070
|
};
|
|
1941
2071
|
/**
|
|
1942
2072
|
* Default export of the OAuth provider
|