diff --git a/.changeset/dry-toys-count.md b/.changeset/dry-toys-count.md new file mode 100644 index 0000000..62ccdab --- /dev/null +++ b/.changeset/dry-toys-count.md @@ -0,0 +1,5 @@ +--- +'@cloudflare/workers-oauth-provider': patch +--- + +fix types, add linting diff --git a/__tests__/mocks/cloudflare-workers.ts b/__tests__/mocks/cloudflare-workers.ts index 7e41855..6abce7c 100644 --- a/__tests__/mocks/cloudflare-workers.ts +++ b/__tests__/mocks/cloudflare-workers.ts @@ -2,6 +2,7 @@ * Mock for cloudflare:workers module * Provides a minimal implementation of WorkerEntrypoint for testing */ +/** biome-ignore-all lint/suspicious/noExplicitAny: it's fine */ export class WorkerEntrypoint { ctx: any; @@ -12,7 +13,7 @@ export class WorkerEntrypoint { this.env = env; } - fetch(request: Request): Response | Promise { + fetch(_request: Request): Response | Promise { throw new Error('Method not implemented. This should be overridden by subclasses.'); } } diff --git a/__tests__/oauth-provider.test.ts b/__tests__/oauth-provider.test.ts index 50f96da..f0fe78d 100644 --- a/__tests__/oauth-provider.test.ts +++ b/__tests__/oauth-provider.test.ts @@ -1,6 +1,19 @@ -import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import { OAuthProvider, type OAuthHelpers } from '../src/oauth-provider'; +/** biome-ignore-all lint/style/noNonNullAssertion: it's fine */ +/** biome-ignore-all lint/style/noUnusedTemplateLiteral: it's fine */ +/** biome-ignore-all lint/correctness/noUnusedFunctionParameters: it's fine */ + import type { ExecutionContext } from '@cloudflare/workers-types'; + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + OAuthProvider, + type TokenExchangeCallbackOptions, + type ClientInfo, + type Grant, + type RequiredEnv, +} from '../src/oauth-provider'; + // We're importing WorkerEntrypoint from our mock implementation // The actual import is mocked in setup.ts import { WorkerEntrypoint } from 'cloudflare:workers'; @@ -9,10 +22,10 @@ import { WorkerEntrypoint } from 'cloudflare:workers'; * Mock KV namespace implementation that stores data in memory */ class MockKV { - private storage: Map = new Map(); + private storage: Map = new Map(); async put(key: string, value: string | ArrayBuffer, options?: { expirationTtl?: number }): Promise { - let expirationTime: number | undefined = undefined; + let expirationTime: number | undefined; if (options?.expirationTtl) { expirationTime = Date.now() + options.expirationTtl * 1000; @@ -50,7 +63,7 @@ class MockKV { cursor?: string; }> { const { prefix, limit = 1000 } = options; - let keys: { name: string }[] = []; + const keys: { name: string }[] = []; for (const key of this.storage.keys()) { if (key.startsWith(prefix)) { @@ -80,9 +93,9 @@ class MockKV { * Mock execution context for Cloudflare Workers */ class MockExecutionContext implements ExecutionContext { - props: any = {}; + props: Record = {}; - waitUntil(promise: Promise): void { + waitUntil(promise: Promise): void { // In tests, we can just ignore waitUntil } @@ -121,6 +134,7 @@ const testDefaultHandler = { if (url.pathname === '/authorize') { // Mock authorize endpoint const oauthReqInfo = await env.OAUTH_PROVIDER.parseAuthRequest(request); + // biome-ignore lint/correctness/noUnusedVariables: just keeping this pattern consistent const clientInfo = await env.OAUTH_PROVIDER.lookupClient(oauthReqInfo.clientId); // Mock user consent flow - automatically grant consent @@ -142,7 +156,7 @@ const testDefaultHandler = { // Helper function to create mock requests function createMockRequest( url: string, - method: string = 'GET', + method = 'GET', headers: Record = {}, body?: string | FormData ): Request { @@ -163,10 +177,7 @@ function createMockEnv() { return { OAUTH_KV: new MockKV(), OAUTH_PROVIDER: null, // Will be populated by the OAuthProvider - } as { - OAUTH_KV: MockKV; - OAUTH_PROVIDER: OAuthHelpers | null; - }; + } as unknown as RequiredEnv; } describe('OAuthProvider', () => { @@ -198,6 +209,7 @@ describe('OAuthProvider', () => { afterEach(() => { // Clean up KV storage after each test + // @ts-expect-error: .clear is only in the mocked kv namespace in tests mockEnv.OAUTH_KV.clear(); }); @@ -244,7 +256,7 @@ describe('OAuthProvider', () => { ); const registerResponse = await providerWithMultiHandler.fetch(registerRequest, mockEnv, mockCtx); - const client = await registerResponse.json(); + const client = await registerResponse.json>(); const clientId = client.client_id; const clientSecret = client.client_secret; const redirectUri = 'https://client.example.com/callback'; @@ -265,8 +277,8 @@ describe('OAuthProvider', () => { params.append('grant_type', 'authorization_code'); params.append('code', code); params.append('redirect_uri', redirectUri); - params.append('client_id', clientId); - params.append('client_secret', clientSecret); + params.append('client_id', clientId as string); + params.append('client_secret', clientSecret as string); const tokenRequest = createMockRequest( 'https://example.com/oauth/token', @@ -276,7 +288,7 @@ describe('OAuthProvider', () => { ); const tokenResponse = await providerWithMultiHandler.fetch(tokenRequest, mockEnv, mockCtx); - const tokens = await tokenResponse.json(); + const tokens = await tokenResponse.json>(); const accessToken = tokens.access_token; // Make requests to different API routes @@ -337,7 +349,7 @@ describe('OAuthProvider', () => { expect(response.status).toBe(200); - const metadata = await response.json(); + const metadata = await response.json>(); expect(metadata.issuer).toBe('https://example.com'); expect(metadata.authorization_endpoint).toBe('https://example.com/authorize'); expect(metadata.token_endpoint).toBe('https://example.com/oauth/token'); @@ -366,7 +378,7 @@ describe('OAuthProvider', () => { expect(response.status).toBe(200); - const metadata = await response.json(); + const metadata = await response.json>(); expect(metadata.response_types_supported).toContain('code'); expect(metadata.response_types_supported).not.toContain('token'); }); @@ -391,18 +403,20 @@ describe('OAuthProvider', () => { expect(response.status).toBe(201); - const registeredClient = await response.json(); + const registeredClient = await response.json>(); expect(registeredClient.client_id).toBeDefined(); expect(registeredClient.client_secret).toBeDefined(); expect(registeredClient.redirect_uris).toEqual(['https://client.example.com/callback']); expect(registeredClient.client_name).toBe('Test Client'); // Verify the client was saved to KV - const savedClient = await mockEnv.OAUTH_KV.get(`client:${registeredClient.client_id}`, { type: 'json' }); + const savedClient = await mockEnv.OAUTH_KV.get(`client:${registeredClient.client_id}`, { + type: 'json', + }); expect(savedClient).not.toBeNull(); - expect(savedClient.clientId).toBe(registeredClient.client_id); + expect(savedClient!.clientId).toBe(registeredClient.client_id); // Secret should be stored as a hash - expect(savedClient.clientSecret).not.toBe(registeredClient.client_secret); + expect(savedClient!.clientSecret).not.toBe(registeredClient.client_secret); }); it('should register a public client', async () => { @@ -423,20 +437,23 @@ describe('OAuthProvider', () => { expect(response.status).toBe(201); - const registeredClient = await response.json(); + const registeredClient = await response.json>(); expect(registeredClient.client_id).toBeDefined(); expect(registeredClient.client_secret).toBeUndefined(); // Public client should not have a secret expect(registeredClient.token_endpoint_auth_method).toBe('none'); // Verify the client was saved to KV - const savedClient = await mockEnv.OAUTH_KV.get(`client:${registeredClient.client_id}`, { type: 'json' }); + const savedClient = await mockEnv.OAUTH_KV.get(`client:${registeredClient.client_id}`, { + type: 'json', + }); expect(savedClient).not.toBeNull(); - expect(savedClient.clientSecret).toBeUndefined(); // No secret stored + expect(savedClient!.clientSecret).toBeUndefined(); // No secret stored }); }); describe('Authorization Code Flow', () => { let clientId: string; + // biome-ignore lint/correctness/noUnusedVariables: just keeping this pattern consistent let clientSecret: string; let redirectUri: string; @@ -456,10 +473,10 @@ describe('OAuthProvider', () => { ); const response = await oauthProvider.fetch(request, mockEnv, mockCtx); - const client = await response.json(); + const client = await response.json>(); - clientId = client.client_id; - clientSecret = client.client_secret; + clientId = client.client_id as string; + clientSecret = client.client_secret as string; redirectUri = 'https://client.example.com/callback'; } @@ -572,9 +589,9 @@ describe('OAuthProvider', () => { ); const response = await oauthProvider.fetch(request, mockEnv, mockCtx); - const client = await response.json(); + const client = await response.json>(); - clientId = client.client_id; + clientId = client.client_id as string; redirectUri = 'https://spa-client.example.com/callback'; } @@ -683,7 +700,7 @@ describe('OAuthProvider', () => { expect(apiResponse.status).toBe(200); - const apiData = await apiResponse.json(); + const apiData = await apiResponse.json>(); expect(apiData.success).toBe(true); expect(apiData.user).toEqual({ userId: 'test-user-123', username: 'TestUser' }); }); @@ -710,10 +727,10 @@ describe('OAuthProvider', () => { ); const response = await oauthProvider.fetch(request, mockEnv, mockCtx); - const client = await response.json(); + const client = await response.json>(); - clientId = client.client_id; - clientSecret = client.client_secret; + clientId = client.client_id as string; + clientSecret = client.client_secret as string; redirectUri = 'https://client.example.com/callback'; } @@ -755,7 +772,7 @@ describe('OAuthProvider', () => { expect(tokenResponse.status).toBe(200); - const tokens = await tokenResponse.json(); + const tokens = await tokenResponse.json>(); expect(tokens.access_token).toBeDefined(); expect(tokens.refresh_token).toBeDefined(); expect(tokens.token_type).toBe('bearer'); @@ -768,10 +785,12 @@ describe('OAuthProvider', () => { // Verify grant was updated (auth code removed, refresh token added) const grantEntries = await mockEnv.OAUTH_KV.list({ prefix: 'grant:' }); const grantKey = grantEntries.keys[0].name; - const grant = await mockEnv.OAUTH_KV.get(grantKey, { type: 'json' }); + const grant = await mockEnv.OAUTH_KV.get(grantKey, { type: 'json' }); - expect(grant.authCodeId).toBeUndefined(); // Auth code should be removed - expect(grant.refreshTokenId).toBeDefined(); // Refresh token should be added + expect(grant).not.toBeNull(); + + expect(grant!.authCodeId).toBeUndefined(); // Auth code should be removed + expect(grant!.refreshTokenId).toBeDefined(); // Refresh token should be added }); it('should reject token exchange without redirect_uri when not using PKCE', async () => { @@ -806,7 +825,7 @@ describe('OAuthProvider', () => { // Should fail because redirect_uri is required when not using PKCE expect(tokenResponse.status).toBe(400); - const error = await tokenResponse.json(); + const error = await tokenResponse.json>(); expect(error.error).toBe('invalid_request'); expect(error.error_description).toBe('redirect_uri is required when not using PKCE'); }); @@ -844,7 +863,7 @@ describe('OAuthProvider', () => { // Should fail because code_verifier is provided but PKCE wasn't used in authorization expect(tokenResponse.status).toBe(400); - const error = await tokenResponse.json(); + const error = await tokenResponse.json>(); expect(error.error).toBe('invalid_request'); expect(error.error_description).toBe('code_verifier provided for a flow that did not use PKCE'); }); @@ -909,7 +928,7 @@ describe('OAuthProvider', () => { // Should succeed because redirect_uri is optional when using PKCE expect(tokenResponse.status).toBe(200); - const tokens = await tokenResponse.json(); + const tokens = await tokenResponse.json>(); expect(tokens.access_token).toBeDefined(); expect(tokens.refresh_token).toBeDefined(); expect(tokens.token_type).toBe('bearer'); @@ -944,7 +963,7 @@ describe('OAuthProvider', () => { ); const tokenResponse = await oauthProvider.fetch(tokenRequest, mockEnv, mockCtx); - const tokens = await tokenResponse.json(); + const tokens = await tokenResponse.json>(); // Now use the access token for an API request const apiRequest = createMockRequest('https://example.com/api/test', 'GET', { @@ -955,7 +974,7 @@ describe('OAuthProvider', () => { expect(apiResponse.status).toBe(200); - const apiData = await apiResponse.json(); + const apiData = await apiResponse.json>(); expect(apiData.success).toBe(true); expect(apiData.user).toEqual({ userId: 'test-user-123', username: 'TestUser' }); }); @@ -983,9 +1002,9 @@ describe('OAuthProvider', () => { ); const registerResponse = await oauthProvider.fetch(registerRequest, mockEnv, mockCtx); - const client = await registerResponse.json(); - clientId = client.client_id; - clientSecret = client.client_secret; + const client = await registerResponse.json>(); + clientId = client.client_id as string; + clientSecret = client.client_secret as string; const redirectUri = 'https://client.example.com/callback'; // Get an auth code @@ -1015,8 +1034,8 @@ describe('OAuthProvider', () => { ); const tokenResponse = await oauthProvider.fetch(tokenRequest, mockEnv, mockCtx); - const tokens = await tokenResponse.json(); - refreshToken = tokens.refresh_token; + const tokens = await tokenResponse.json>(); + refreshToken = tokens.refresh_token as string; } beforeEach(async () => { @@ -1042,7 +1061,7 @@ describe('OAuthProvider', () => { expect(refreshResponse.status).toBe(200); - const newTokens = await refreshResponse.json(); + const newTokens = await refreshResponse.json>(); expect(newTokens.access_token).toBeDefined(); expect(newTokens.refresh_token).toBeDefined(); expect(newTokens.refresh_token).not.toBe(refreshToken); // Should get a new refresh token @@ -1054,10 +1073,12 @@ describe('OAuthProvider', () => { // Verify the grant was updated const grantEntries = await mockEnv.OAUTH_KV.list({ prefix: 'grant:' }); const grantKey = grantEntries.keys[0].name; - const grant = await mockEnv.OAUTH_KV.get(grantKey, { type: 'json' }); + const grant = await mockEnv.OAUTH_KV.get(grantKey, { type: 'json' }); + + expect(grant).not.toBeNull(); - expect(grant.previousRefreshTokenId).toBeDefined(); // Old refresh token should be tracked - expect(grant.refreshTokenId).toBeDefined(); // New refresh token should be set + expect(grant!.previousRefreshTokenId).toBeDefined(); // Old refresh token should be tracked + expect(grant!.refreshTokenId).toBeDefined(); // New refresh token should be set }); it('should allow using the previous refresh token once', async () => { @@ -1076,8 +1097,8 @@ describe('OAuthProvider', () => { ); const refreshResponse1 = await oauthProvider.fetch(refreshRequest1, mockEnv, mockCtx); - const newTokens1 = await refreshResponse1.json(); - const newRefreshToken = newTokens1.refresh_token; + const _newTokens1 = await refreshResponse1.json>(); + const _newRefreshToken = _newTokens1.refresh_token; // Now try to use the original refresh token again (simulating a retry after failure) const params2 = new URLSearchParams(); @@ -1098,7 +1119,7 @@ describe('OAuthProvider', () => { // The request should succeed expect(refreshResponse2.status).toBe(200); - const newTokens2 = await refreshResponse2.json(); + const newTokens2 = await refreshResponse2.json>(); expect(newTokens2.access_token).toBeDefined(); expect(newTokens2.refresh_token).toBeDefined(); @@ -1106,10 +1127,12 @@ describe('OAuthProvider', () => { // as the previous token const grantEntries = await mockEnv.OAUTH_KV.list({ prefix: 'grant:' }); const grantKey = grantEntries.keys[0].name; - const grant = await mockEnv.OAUTH_KV.get(grantKey, { type: 'json' }); + const grant = await mockEnv.OAUTH_KV.get(grantKey, { type: 'json' }); // The previousRefreshTokenId should now be from the first refresh, not the original - expect(grant.previousRefreshTokenId).toBeDefined(); + expect(grant).not.toBeNull(); + + expect(grant!.previousRefreshTokenId).toBeDefined(); }); }); @@ -1133,7 +1156,7 @@ describe('OAuthProvider', () => { ); const registerResponse = await oauthProvider.fetch(registerRequest, mockEnv, mockCtx); - const client = await registerResponse.json(); + const client = await registerResponse.json>(); const clientId = client.client_id; const clientSecret = client.client_secret; const redirectUri = 'https://client.example.com/callback'; @@ -1154,8 +1177,8 @@ describe('OAuthProvider', () => { params.append('grant_type', 'authorization_code'); params.append('code', code); params.append('redirect_uri', redirectUri); - params.append('client_id', clientId); - params.append('client_secret', clientSecret); + params.append('client_id', clientId as string); + params.append('client_secret', clientSecret as string); const tokenRequest = createMockRequest( 'https://example.com/oauth/token', @@ -1165,8 +1188,8 @@ describe('OAuthProvider', () => { ); const tokenResponse = await oauthProvider.fetch(tokenRequest, mockEnv, mockCtx); - const tokens = await tokenResponse.json(); - accessToken = tokens.access_token; + const tokens = await tokenResponse.json>(); + accessToken = tokens.access_token as string; } beforeEach(async () => { @@ -1180,7 +1203,7 @@ describe('OAuthProvider', () => { expect(apiResponse.status).toBe(401); - const error = await apiResponse.json(); + const error = await apiResponse.json>(); expect(error.error).toBe('invalid_token'); }); @@ -1193,7 +1216,7 @@ describe('OAuthProvider', () => { expect(apiResponse.status).toBe(401); - const error = await apiResponse.json(); + const error = await apiResponse.json>(); expect(error.error).toBe('invalid_token'); }); @@ -1206,7 +1229,7 @@ describe('OAuthProvider', () => { expect(apiResponse.status).toBe(200); - const data = await apiResponse.json(); + const data = await apiResponse.json>(); expect(data.success).toBe(true); expect(data.user).toEqual({ userId: 'test-user-123', username: 'TestUser' }); }); @@ -1243,10 +1266,14 @@ describe('OAuthProvider', () => { }); it('should handle OPTIONS preflight for metadata discovery endpoint', async () => { - const preflightRequest = createMockRequest('https://example.com/.well-known/oauth-authorization-server', 'OPTIONS', { - Origin: 'https://spa.example.com', - 'Access-Control-Request-Method': 'GET', - }); + const preflightRequest = createMockRequest( + 'https://example.com/.well-known/oauth-authorization-server', + 'OPTIONS', + { + Origin: 'https://spa.example.com', + 'Access-Control-Request-Method': 'GET', + } + ); const response = await oauthProvider.fetch(preflightRequest, mockEnv, mockCtx); @@ -1274,9 +1301,9 @@ describe('OAuthProvider', () => { ); const registerResponse = await oauthProvider.fetch(registerRequest, mockEnv, mockCtx); - const client = await registerResponse.json(); - const clientId = client.client_id; - const clientSecret = client.client_secret; + const client = await registerResponse.json>(); + const clientId = client.client_id as string; + const clientSecret = client.client_secret as string; const redirectUri = 'https://client.example.com/callback'; const authRequest = createMockRequest( @@ -1300,9 +1327,9 @@ describe('OAuthProvider', () => { const tokenRequest = createMockRequest( 'https://example.com/oauth/token', 'POST', - { + { 'Content-Type': 'application/x-www-form-urlencoded', - 'Origin': 'https://webapp.example.com' + Origin: 'https://webapp.example.com', }, params.toString() ); @@ -1342,9 +1369,9 @@ describe('OAuthProvider', () => { const request = createMockRequest( 'https://example.com/oauth/register', 'POST', - { + { 'Content-Type': 'application/json', - 'Origin': 'https://admin.example.com' + Origin: 'https://admin.example.com', }, JSON.stringify(clientData) ); @@ -1395,7 +1422,7 @@ describe('OAuthProvider', () => { expect(response.headers.get('Access-Control-Allow-Methods')).toBe('*'); expect(response.headers.get('Access-Control-Allow-Headers')).toBe('Authorization, *'); - const error = await response.json(); + const error = await response.json>(); expect(error.error).toBe('invalid_token'); }); @@ -1408,9 +1435,9 @@ describe('OAuthProvider', () => { const tokenRequest = createMockRequest( 'https://example.com/oauth/token', 'POST', - { + { 'Content-Type': 'application/x-www-form-urlencoded', - 'Origin': 'https://evil.example.com' + Origin: 'https://evil.example.com', }, params.toString() ); @@ -1441,7 +1468,7 @@ describe('OAuthProvider', () => { describe('Token Exchange Callback', () => { // Test with provider that has token exchange callback let oauthProviderWithCallback: OAuthProvider; - let callbackInvocations: any[] = []; + let callbackInvocations: TokenExchangeCallbackOptions[] = []; let mockEnv: ReturnType; let mockCtx: MockExecutionContext; @@ -1449,7 +1476,7 @@ describe('OAuthProvider', () => { function createProviderWithCallback() { callbackInvocations = []; - const tokenExchangeCallback = async (options: any) => { + const tokenExchangeCallback = async (options: TokenExchangeCallbackOptions) => { // Record that the callback was called and with what arguments callbackInvocations.push({ ...options }); @@ -1476,7 +1503,7 @@ describe('OAuthProvider', () => { newProps: { ...options.props, grantUpdated: true, - refreshCount: (options.props.refreshCount || 0) + 1, + refreshCount: ((options.props.refreshCount as number) ?? 0) + 1, }, }; } @@ -1516,10 +1543,10 @@ describe('OAuthProvider', () => { ); const response = await oauthProviderWithCallback.fetch(request, mockEnv, mockCtx); - const client = await response.json(); + const client = await response.json>(); - clientId = client.client_id; - clientSecret = client.client_secret; + clientId = client.client_id as string; + clientSecret = client.client_secret as string; redirectUri = 'https://client.example.com/callback'; } @@ -1540,6 +1567,7 @@ describe('OAuthProvider', () => { afterEach(() => { // Clean up KV storage after each test + // @ts-expect-error: .clear is only in the mocked kv namespace in tests mockEnv.OAUTH_KV.clear(); }); @@ -1577,7 +1605,7 @@ describe('OAuthProvider', () => { // Check that the token exchange was successful expect(tokenResponse.status).toBe(200); - const tokens = await tokenResponse.json(); + const tokens = await tokenResponse.json>(); expect(tokens.access_token).toBeDefined(); // Check that the callback was called once @@ -1598,7 +1626,7 @@ describe('OAuthProvider', () => { expect(apiResponse.status).toBe(200); // Check that the API received the token-specific props from the callback - const apiData = await apiResponse.json(); + const apiData = await apiResponse.json>(); expect(apiData.user).toEqual({ userId: 'test-user-123', username: 'TestUser', @@ -1635,7 +1663,7 @@ describe('OAuthProvider', () => { ); const tokenResponse = await oauthProviderWithCallback.fetch(tokenRequest, mockEnv, mockCtx); - const tokens = await tokenResponse.json(); + const tokens = await tokenResponse.json>(); // Reset the callback invocations tracking before refresh callbackInvocations = []; @@ -1643,9 +1671,9 @@ describe('OAuthProvider', () => { // Now use the refresh token const refreshParams = new URLSearchParams(); refreshParams.append('grant_type', 'refresh_token'); - refreshParams.append('refresh_token', tokens.refresh_token); - refreshParams.append('client_id', clientId); - refreshParams.append('client_secret', clientSecret); + refreshParams.append('refresh_token', tokens.refresh_token as string); + refreshParams.append('client_id', clientId as string); + refreshParams.append('client_secret', clientSecret as string); const refreshRequest = createMockRequest( 'https://example.com/oauth/token', @@ -1658,7 +1686,7 @@ describe('OAuthProvider', () => { // Check that the refresh was successful expect(refreshResponse.status).toBe(200); - const newTokens = await refreshResponse.json(); + const newTokens = await refreshResponse.json>(); expect(newTokens.access_token).toBeDefined(); // Check that the callback was called once @@ -1685,7 +1713,7 @@ describe('OAuthProvider', () => { expect(apiResponse.status).toBe(200); // Check that the API received the token-specific props from the refresh callback - const apiData = await apiResponse.json(); + const apiData = await apiResponse.json>(); expect(apiData.user).toEqual({ userId: 'test-user-123', username: 'TestUser', @@ -1697,7 +1725,7 @@ describe('OAuthProvider', () => { // Do a second refresh to verify that grant props are properly updated const refresh2Params = new URLSearchParams(); refresh2Params.append('grant_type', 'refresh_token'); - refresh2Params.append('refresh_token', newTokens.refresh_token); + refresh2Params.append('refresh_token', newTokens.refresh_token as string); refresh2Params.append('client_id', clientId); refresh2Params.append('client_secret', clientSecret); @@ -1712,7 +1740,7 @@ describe('OAuthProvider', () => { ); const refresh2Response = await oauthProviderWithCallback.fetch(refresh2Request, mockEnv, mockCtx); - const newerTokens = await refresh2Response.json(); + const _newerTokens = await refresh2Response.json(); // Check that the refresh count was incremented in the grant props expect(callbackInvocations.length).toBe(1); @@ -1722,7 +1750,7 @@ describe('OAuthProvider', () => { it('should update token props during refresh when explicitly provided', async () => { // Create a provider with a callback that returns both accessTokenProps and newProps // but with different values for each - const differentPropsCallback = async (options: any) => { + const differentPropsCallback = async (options: TokenExchangeCallbackOptions) => { if (options.grantType === 'refresh_token') { return { accessTokenProps: { @@ -1765,7 +1793,7 @@ describe('OAuthProvider', () => { ); const registerResponse = await refreshPropsProvider.fetch(registerRequest, mockEnv, mockCtx); - const client = await registerResponse.json(); + const client = await registerResponse.json>(); const testClientId = client.client_id; const testClientSecret = client.client_secret; const testRedirectUri = 'https://client.example.com/callback'; @@ -1785,8 +1813,8 @@ describe('OAuthProvider', () => { params.append('grant_type', 'authorization_code'); params.append('code', code); params.append('redirect_uri', testRedirectUri); - params.append('client_id', testClientId); - params.append('client_secret', testClientSecret); + params.append('client_id', testClientId as string); + params.append('client_secret', testClientSecret as string); const tokenRequest = createMockRequest( 'https://example.com/oauth/token', @@ -1796,14 +1824,14 @@ describe('OAuthProvider', () => { ); const tokenResponse = await refreshPropsProvider.fetch(tokenRequest, mockEnv, mockCtx); - const tokens = await tokenResponse.json(); + const tokens = await tokenResponse.json>(); // Now do a refresh token exchange const refreshParams = new URLSearchParams(); refreshParams.append('grant_type', 'refresh_token'); - refreshParams.append('refresh_token', tokens.refresh_token); - refreshParams.append('client_id', testClientId); - refreshParams.append('client_secret', testClientSecret); + refreshParams.append('refresh_token', tokens.refresh_token as string); + refreshParams.append('client_id', testClientId as string); + refreshParams.append('client_secret', testClientSecret as string); const refreshRequest = createMockRequest( 'https://example.com/oauth/token', @@ -1813,7 +1841,7 @@ describe('OAuthProvider', () => { ); const refreshResponse = await refreshPropsProvider.fetch(refreshRequest, mockEnv, mockCtx); - const newTokens = await refreshResponse.json(); + const newTokens = await refreshResponse.json>(); // Use the new token to access API const apiRequest = createMockRequest('https://example.com/api/test', 'GET', { @@ -1821,7 +1849,7 @@ describe('OAuthProvider', () => { }); const apiResponse = await refreshPropsProvider.fetch(apiRequest, mockEnv, mockCtx); - const apiData = await apiResponse.json(); + const apiData = await apiResponse.json>(); // The access token should contain the token-specific props from the refresh callback expect(apiData.user).toHaveProperty('refreshed', true); @@ -1834,7 +1862,7 @@ describe('OAuthProvider', () => { // and only newProps for refresh token // Note: With the enhanced implementation, when only newProps is returned // without accessTokenProps, the token props will inherit from newProps - const propsCallback = async (options: any) => { + const propsCallback = async (options: TokenExchangeCallbackOptions) => { if (options.grantType === 'authorization_code') { return { accessTokenProps: { ...options.props, tokenOnly: true }, @@ -1872,7 +1900,7 @@ describe('OAuthProvider', () => { ); const registerResponse = await specialProvider.fetch(registerRequest, mockEnv, mockCtx); - const client = await registerResponse.json(); + const client = await registerResponse.json>(); const testClientId = client.client_id; const testClientSecret = client.client_secret; const testRedirectUri = 'https://client.example.com/callback'; @@ -1892,8 +1920,8 @@ describe('OAuthProvider', () => { params.append('grant_type', 'authorization_code'); params.append('code', code); params.append('redirect_uri', testRedirectUri); - params.append('client_id', testClientId); - params.append('client_secret', testClientSecret); + params.append('client_id', testClientId as string); + params.append('client_secret', testClientSecret as string); const tokenRequest = createMockRequest( 'https://example.com/oauth/token', @@ -1903,7 +1931,7 @@ describe('OAuthProvider', () => { ); const tokenResponse = await specialProvider.fetch(tokenRequest, mockEnv, mockCtx); - const tokens = await tokenResponse.json(); + const tokens = await tokenResponse.json>(); // Verify the token has the tokenOnly property when used for API access const apiRequest = createMockRequest('https://example.com/api/test', 'GET', { @@ -1911,15 +1939,15 @@ describe('OAuthProvider', () => { }); const apiResponse = await specialProvider.fetch(apiRequest, mockEnv, mockCtx); - const apiData = await apiResponse.json(); - expect(apiData.user.tokenOnly).toBe(true); + const apiData = await apiResponse.json>(); + expect((apiData.user as Record).tokenOnly).toBe(true); // Now do a refresh token exchange const refreshParams = new URLSearchParams(); refreshParams.append('grant_type', 'refresh_token'); - refreshParams.append('refresh_token', tokens.refresh_token); - refreshParams.append('client_id', testClientId); - refreshParams.append('client_secret', testClientSecret); + refreshParams.append('refresh_token', tokens.refresh_token as string); + refreshParams.append('client_id', testClientId as string); + refreshParams.append('client_secret', testClientSecret as string); const refreshRequest = createMockRequest( 'https://example.com/oauth/token', @@ -1929,7 +1957,7 @@ describe('OAuthProvider', () => { ); const refreshResponse = await specialProvider.fetch(refreshRequest, mockEnv, mockCtx); - const newTokens = await refreshResponse.json(); + const newTokens = await refreshResponse.json>(); // Use the new token to access API const api2Request = createMockRequest('https://example.com/api/test', 'GET', { @@ -1937,7 +1965,7 @@ describe('OAuthProvider', () => { }); const api2Response = await specialProvider.fetch(api2Request, mockEnv, mockCtx); - const api2Data = await api2Response.json(); + const api2Data = await api2Response.json>(); // With the enhanced implementation, the token props now inherit from grant props // when only newProps is returned but accessTokenProps is not specified @@ -1950,7 +1978,7 @@ describe('OAuthProvider', () => { it('should allow customizing access token TTL via callback', async () => { // Create a provider with a callback that customizes TTL - const customTtlCallback = async (options: any) => { + const customTtlCallback = async (options: TokenExchangeCallbackOptions) => { if (options.grantType === 'refresh_token') { // Return custom TTL for the access token return { @@ -1988,7 +2016,7 @@ describe('OAuthProvider', () => { ); const registerResponse = await customTtlProvider.fetch(registerRequest, mockEnv, mockCtx); - const client = await registerResponse.json(); + const client = await registerResponse.json>(); const testClientId = client.client_id; const testClientSecret = client.client_secret; const testRedirectUri = 'https://client.example.com/callback'; @@ -2008,8 +2036,8 @@ describe('OAuthProvider', () => { params.append('grant_type', 'authorization_code'); params.append('code', code); params.append('redirect_uri', testRedirectUri); - params.append('client_id', testClientId); - params.append('client_secret', testClientSecret); + params.append('client_id', testClientId as string); + params.append('client_secret', testClientSecret as string); const tokenRequest = createMockRequest( 'https://example.com/oauth/token', @@ -2019,14 +2047,14 @@ describe('OAuthProvider', () => { ); const tokenResponse = await customTtlProvider.fetch(tokenRequest, mockEnv, mockCtx); - const tokens = await tokenResponse.json(); + const tokens = await tokenResponse.json>(); // Now do a refresh const refreshParams = new URLSearchParams(); refreshParams.append('grant_type', 'refresh_token'); - refreshParams.append('refresh_token', tokens.refresh_token); - refreshParams.append('client_id', testClientId); - refreshParams.append('client_secret', testClientSecret); + refreshParams.append('refresh_token', tokens.refresh_token as string); + refreshParams.append('client_id', testClientId as string); + refreshParams.append('client_secret', testClientSecret as string); const refreshRequest = createMockRequest( 'https://example.com/oauth/token', @@ -2036,7 +2064,7 @@ describe('OAuthProvider', () => { ); const refreshResponse = await customTtlProvider.fetch(refreshRequest, mockEnv, mockCtx); - const newTokens = await refreshResponse.json(); + const newTokens = await refreshResponse.json>(); // Verify that the TTL is from the callback, not the default expect(newTokens.expires_in).toBe(7200); @@ -2047,13 +2075,13 @@ describe('OAuthProvider', () => { }); const apiResponse = await customTtlProvider.fetch(apiRequest, mockEnv, mockCtx); - const apiData = await apiResponse.json(); - expect(apiData.user.customTtl).toBe(true); + const apiData = await apiResponse.json>(); + expect((apiData.user as Record).customTtl).toBe(true); }); it('should handle callback that returns undefined (keeping original props)', async () => { // Create a provider with a callback that returns undefined - const noopCallback = async (options: any) => { + const noopCallback = async (options: TokenExchangeCallbackOptions) => { // Don't return anything, which should keep the original props return undefined; }; @@ -2084,7 +2112,7 @@ describe('OAuthProvider', () => { ); const registerResponse = await noopProvider.fetch(registerRequest, mockEnv, mockCtx); - const client = await registerResponse.json(); + const client = await registerResponse.json>(); const testClientId = client.client_id; const testClientSecret = client.client_secret; const testRedirectUri = 'https://client.example.com/callback'; @@ -2104,8 +2132,8 @@ describe('OAuthProvider', () => { params.append('grant_type', 'authorization_code'); params.append('code', code); params.append('redirect_uri', testRedirectUri); - params.append('client_id', testClientId); - params.append('client_secret', testClientSecret); + params.append('client_id', testClientId as string); + params.append('client_secret', testClientSecret as string); const tokenRequest = createMockRequest( 'https://example.com/oauth/token', @@ -2115,7 +2143,7 @@ describe('OAuthProvider', () => { ); const tokenResponse = await noopProvider.fetch(tokenRequest, mockEnv, mockCtx); - const tokens = await tokenResponse.json(); + const tokens = await tokenResponse.json>(); // Verify the token has the original props when used for API access const apiRequest = createMockRequest('https://example.com/api/test', 'GET', { @@ -2123,7 +2151,7 @@ describe('OAuthProvider', () => { }); const apiResponse = await noopProvider.fetch(apiRequest, mockEnv, mockCtx); - const apiData = await apiResponse.json(); + const apiData = await apiResponse.json>(); // The props should be the original ones (no change) expect(apiData.user).toEqual({ userId: 'test-user-123', username: 'TestUser' }); @@ -2134,12 +2162,12 @@ describe('OAuthProvider', () => { // 1. previousRefreshTokenWrappedKey not being re-wrapped when grant props change // 2. accessTokenProps not inheriting from newProps when only newProps is returned let callCount = 0; - const propUpdatingCallback = async (options: any) => { + const propUpdatingCallback = async (options: TokenExchangeCallbackOptions) => { callCount++; if (options.grantType === 'refresh_token') { const updatedProps = { ...options.props, - updatedCount: (options.props.updatedCount || 0) + 1, + updatedCount: ((options.props.updatedCount as number) ?? 0) + 1, }; // Only return newProps to test that accessTokenProps will inherit from it @@ -2178,7 +2206,7 @@ describe('OAuthProvider', () => { ); const registerResponse = await testProvider.fetch(registerRequest, mockEnv, mockCtx); - const client = await registerResponse.json(); + const client = await registerResponse.json>(); const testClientId = client.client_id; const testClientSecret = client.client_secret; const testRedirectUri = 'https://client.example.com/callback'; @@ -2198,8 +2226,8 @@ describe('OAuthProvider', () => { params.append('grant_type', 'authorization_code'); params.append('code', code); params.append('redirect_uri', testRedirectUri); - params.append('client_id', testClientId); - params.append('client_secret', testClientSecret); + params.append('client_id', testClientId as string); + params.append('client_secret', testClientSecret as string); const tokenRequest = createMockRequest( 'https://example.com/oauth/token', @@ -2209,7 +2237,7 @@ describe('OAuthProvider', () => { ); const tokenResponse = await testProvider.fetch(tokenRequest, mockEnv, mockCtx); - const tokens = await tokenResponse.json(); + const tokens = await tokenResponse.json>(); const refreshToken = tokens.refresh_token; // Reset the callback invocations before refresh @@ -2218,9 +2246,9 @@ describe('OAuthProvider', () => { // First refresh - this will update the grant props and re-encrypt them with a new key const refreshParams = new URLSearchParams(); refreshParams.append('grant_type', 'refresh_token'); - refreshParams.append('refresh_token', refreshToken); - refreshParams.append('client_id', testClientId); - refreshParams.append('client_secret', testClientSecret); + refreshParams.append('refresh_token', refreshToken as string); + refreshParams.append('client_id', testClientId as string); + refreshParams.append('client_secret', testClientSecret as string); const refreshRequest = createMockRequest( 'https://example.com/oauth/token', @@ -2236,7 +2264,7 @@ describe('OAuthProvider', () => { expect(callCount).toBe(1); // Get the new tokens from the first refresh - const newTokens = await refreshResponse.json(); + const newTokens = await refreshResponse.json>(); // Get the refresh token's corresponding token data to verify it has the updated props const apiRequest1 = createMockRequest('https://example.com/api/test', 'GET', { @@ -2244,13 +2272,13 @@ describe('OAuthProvider', () => { }); const apiResponse1 = await testProvider.fetch(apiRequest1, mockEnv, mockCtx); - const apiData1 = await apiResponse1.json(); + const apiData1 = await apiResponse1.json>(); // Print the actual API response to debug console.log('First API response:', JSON.stringify(apiData1)); // Verify that the token has the updated props (updatedCount should be 1) - expect(apiData1.user.updatedCount).toBe(1); + expect((apiData1.user as Record).updatedCount).toBe(1); // Reset callCount before the second refresh callCount = 0; @@ -2270,7 +2298,7 @@ describe('OAuthProvider', () => { // When fixed, it should succeed because the previous refresh token is still valid once. expect(secondRefreshResponse.status).toBe(200); - const secondTokens = await secondRefreshResponse.json(); + const secondTokens = await secondRefreshResponse.json>(); expect(secondTokens.access_token).toBeDefined(); // The callback should have been called again @@ -2282,10 +2310,10 @@ describe('OAuthProvider', () => { }); const apiResponse2 = await testProvider.fetch(apiRequest2, mockEnv, mockCtx); - const apiData2 = await apiResponse2.json(); + const apiData2 = await apiResponse2.json>(); // The updatedCount should be 2 now (incremented again during the second refresh) - expect(apiData2.user.updatedCount).toBe(2); + expect((apiData2.user as Record).updatedCount).toBe(2); }); }); @@ -2303,7 +2331,7 @@ describe('OAuthProvider', () => { // Verify the error response expect(response.status).toBe(401); - const error = await response.json(); + const error = await response.json>(); expect(error.error).toBe('invalid_token'); // Verify the default onError callback was triggered and logged a warning @@ -2352,7 +2380,7 @@ describe('OAuthProvider', () => { expect(response.status).toBe(401); // Status should be preserved expect(response.headers.get('X-Custom-Error')).toBe('true'); - const error = await response.json(); + const error = await response.json>(); expect(error.custom_error).toBe(true); expect(error.original_code).toBe('invalid_token'); expect(error.custom_message).toContain('Custom error handler'); @@ -2370,7 +2398,6 @@ describe('OAuthProvider', () => { scopesSupported: ['read', 'write'], onError: () => { callbackInvoked = true; - // No return - should use standard error response }, }); @@ -2383,7 +2410,7 @@ describe('OAuthProvider', () => { // Verify the standard error response expect(response.status).toBe(401); - const error = await response.json(); + const error = await response.json>(); expect(error.error).toBe('invalid_client'); // Verify callback was invoked @@ -2507,9 +2534,9 @@ describe('OAuthProvider', () => { ); expect(clientResponse.status).toBe(201); - const client = await clientResponse.json(); - clientId = client.client_id; - clientSecret = client.client_secret; + const client = await clientResponse.json>(); + clientId = client.client_id as string; + clientSecret = client.client_secret as string; }); it('should connect revokeGrant to token endpoint ', async () => { @@ -2532,7 +2559,7 @@ describe('OAuthProvider', () => { const tokenResponse = await oauthProvider.fetch(tokenRequest, mockEnv, mockCtx); expect(tokenResponse.status).toBe(200); - const tokens = await tokenResponse.json(); + const tokens = await tokenResponse.json>(); // Step 2:this should successfully revoke the token const revokeRequest = createMockRequest( @@ -2557,7 +2584,7 @@ describe('OAuthProvider', () => { const apiResponse = await oauthProvider.fetch(apiRequest, mockEnv, mockCtx); expect(apiResponse.status).toBe(401); // Access token should no longer work - // Step 4: Verify refresh token still works + // Step 4: Verify refresh token still works const refreshRequest = createMockRequest( 'https://example.com/oauth/token', 'POST', @@ -2570,7 +2597,7 @@ describe('OAuthProvider', () => { const refreshResponse = await oauthProvider.fetch(refreshRequest, mockEnv, mockCtx); expect(refreshResponse.status).toBe(200); // Refresh token should still work - const newTokens = await refreshResponse.json(); + const newTokens = await refreshResponse.json>(); expect(newTokens.access_token).toBeDefined(); expect(newTokens.refresh_token).toBeDefined(); }); diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..de3c0ba --- /dev/null +++ b/biome.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.1.4/schema.json", + "assist": { + "actions": { + "source": { + "useSortedKeys": "off", + "organizeImports": "off" + } + }, + "enabled": true + }, + "files": { + "ignoreUnknown": false, + "includes": [ + "src/**", + "__tests__/**", + ".github/**", + "!node_modules", + "!dist", + "!.wrangler", + "!wrangler.jsonc", + "!./tsconfig.json", + "!package.json", + "!package-lock.json" + ] + }, + "formatter": { + "enabled": false, + "indentStyle": "tab" + }, + "linter": { + "enabled": true, + "rules": { + "a11y": { + "useKeyWithClickEvents": "off" + }, + "complexity": { + "noBannedTypes": "off" + }, + "recommended": true, + "style": { + "noInferrableTypes": "error", + "noNonNullAssertion": "off", + "noParameterAssign": "error", + "noUnusedTemplateLiteral": "error", + "noUselessElse": "off", + "useAsConstAssertion": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useNumberNamespace": "error", + "useSelfClosingElements": "error", + "useSingleVarDeclarator": "error" + } + } + }, + "vcs": { + "clientKind": "git", + "enabled": false, + "useIgnoreFile": false + } +} diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..e69de29 diff --git a/example/src/client.tsx b/example/src/client.tsx new file mode 100644 index 0000000..e69de29 diff --git a/example/src/server.ts b/example/src/server.ts new file mode 100644 index 0000000..e69de29 diff --git a/example/vite.config.ts b/example/vite.config.ts new file mode 100644 index 0000000..e69de29 diff --git a/example/wrangler.jsonc b/example/wrangler.jsonc new file mode 100644 index 0000000..e69de29 diff --git a/package-lock.json b/package-lock.json index bfa0d79..ca96d05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.6", "license": "MIT", "devDependencies": { + "@biomejs/biome": "^2.1.4", "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "^2.29.5", "@cloudflare/workers-types": "^4.20250807.0", @@ -29,6 +30,169 @@ "node": ">=6.9.0" } }, + "node_modules/@biomejs/biome": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.1.4.tgz", + "integrity": "sha512-QWlrqyxsU0FCebuMnkvBIkxvPqH89afiJzjMl+z67ybutse590jgeaFdDurE9XYtzpjRGTI1tlUZPGWmbKsElA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.1.4", + "@biomejs/cli-darwin-x64": "2.1.4", + "@biomejs/cli-linux-arm64": "2.1.4", + "@biomejs/cli-linux-arm64-musl": "2.1.4", + "@biomejs/cli-linux-x64": "2.1.4", + "@biomejs/cli-linux-x64-musl": "2.1.4", + "@biomejs/cli-win32-arm64": "2.1.4", + "@biomejs/cli-win32-x64": "2.1.4" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.1.4.tgz", + "integrity": "sha512-sCrNENE74I9MV090Wq/9Dg7EhPudx3+5OiSoQOkIe3DLPzFARuL1dOwCWhKCpA3I5RHmbrsbNSRfZwCabwd8Qg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.1.4.tgz", + "integrity": "sha512-gOEICJbTCy6iruBywBDcG4X5rHMbqCPs3clh3UQ+hRKlgvJTk4NHWQAyHOXvaLe+AxD1/TNX1jbZeffBJzcrOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.1.4.tgz", + "integrity": "sha512-juhEkdkKR4nbUi5k/KRp1ocGPNWLgFRD4NrHZSveYrD6i98pyvuzmS9yFYgOZa5JhaVqo0HPnci0+YuzSwT2fw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.1.4.tgz", + "integrity": "sha512-nYr7H0CyAJPaLupFE2cH16KZmRC5Z9PEftiA2vWxk+CsFkPZQ6dBRdcC6RuS+zJlPc/JOd8xw3uCCt9Pv41WvQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.1.4.tgz", + "integrity": "sha512-Eoy9ycbhpJVYuR+LskV9s3uyaIkp89+qqgqhGQsWnp/I02Uqg2fXFblHJOpGZR8AxdB9ADy87oFVxn9MpFKUrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.1.4.tgz", + "integrity": "sha512-lvwvb2SQQHctHUKvBKptR6PLFCM7JfRjpCCrDaTmvB7EeZ5/dQJPhTYBf36BE/B4CRWR2ZiBLRYhK7hhXBCZAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.1.4.tgz", + "integrity": "sha512-3WRYte7orvyi6TRfIZkDN9Jzoogbv+gSvR+b9VOXUg1We1XrjBg6WljADeVEaKTvOcpVdH0a90TwyOQ6ue4fGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.1.4.tgz", + "integrity": "sha512-tBc+W7anBPSFXGAoQW+f/+svkpt8/uXfRwDzN1DvnatkRMt16KIYpEi/iw8u9GahJlFv98kgHcIrSsZHZTR0sw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@changesets/apply-release-plan": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.12.tgz", diff --git a/package.json b/package.json index 9e75051..287968c 100644 --- a/package.json +++ b/package.json @@ -17,14 +17,14 @@ "scripts": { "build": "tsup", "build:watch": "tsup --watch", - "check": "npm run typecheck && npm run test", - "typecheck": "tsc", + "check": "biome check && tsc && npm run test", "test": "vitest run", "test:watch": "vitest", "prepublishOnly": "npm run build", "prettier": "prettier -w ." }, "devDependencies": { + "@biomejs/biome": "^2.1.4", "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "^2.29.5", "@cloudflare/workers-types": "^4.20250807.0", diff --git a/src/oauth-provider.ts b/src/oauth-provider.ts index 978ccd6..e7b0ae7 100644 --- a/src/oauth-provider.ts +++ b/src/oauth-provider.ts @@ -1,33 +1,47 @@ -import { WorkerEntrypoint } from 'cloudflare:workers'; +/** biome-ignore-all lint/style/noNonNullAssertion: it's fine */ +import { type env, WorkerEntrypoint } from 'cloudflare:workers'; // Types +/** + * Required environment bindings + */ +export type RequiredEnv = { + OAUTH_KV: KVNamespace; + OAUTH_PROVIDER: OAuthHelpers; +}; + +/** + * Default environment bindings + */ +export type DefaultEnv = typeof env & RequiredEnv; + /** * Enum representing the type of handler (ExportedHandler or WorkerEntrypoint) */ enum HandlerType { - EXPORTED_HANDLER, - WORKER_ENTRYPOINT, + EXPORTED_HANDLER = 0, + WORKER_ENTRYPOINT = 1, } /** * Discriminated union type for handlers */ -type TypedHandler = +type TypedHandler = | { type: HandlerType.EXPORTED_HANDLER; handler: ExportedHandlerWithFetch; } | { type: HandlerType.WORKER_ENTRYPOINT; - handler: new (ctx: ExecutionContext, env: any) => WorkerEntrypointWithFetch; + handler: new (ctx: ExecutionContext, env: Env) => WorkerEntrypointWithFetch; }; /** * Aliases for either type of Handler that makes .fetch required */ -type ExportedHandlerWithFetch = ExportedHandler & Pick, 'fetch'>; -type WorkerEntrypointWithFetch = WorkerEntrypoint & Pick, 'fetch'>; +export type ExportedHandlerWithFetch = ExportedHandler & Pick, 'fetch'>; +export type WorkerEntrypointWithFetch = WorkerEntrypoint & Pick, 'fetch'>; /** * Configuration options for the OAuth Provider @@ -42,7 +56,7 @@ export interface TokenExchangeCallbackResult { * If not provided but newProps is, the access token will use newProps. * If neither is provided, the original props will be used. */ - accessTokenProps?: any; + accessTokenProps?: Record; /** * New props to replace the props stored in the grant itself. @@ -50,7 +64,7 @@ export interface TokenExchangeCallbackResult { * If accessTokenProps is not provided, these props will also be used for the current access token. * If not provided, the original props will be used. */ - newProps?: any; + newProps?: Record; /** * Override the default access token TTL (time-to-live) for this specific token. @@ -90,10 +104,10 @@ export interface TokenExchangeCallbackOptions { /** * Application-specific properties currently associated with this grant */ - props: any; + props: Record; } -export interface OAuthProviderOptions { +export interface OAuthProviderOptions { /** * URL(s) for API routes. Requests with URLs starting with any of these prefixes * will be treated as API requests and require a valid access token. @@ -112,7 +126,7 @@ export interface OAuthProviderOptions { * Used with `apiRoute` for the single-handler configuration. This is incompatible with * the `apiHandlers` property. You must use either `apiRoute` + `apiHandler` OR `apiHandlers`, not both. */ - apiHandler?: ExportedHandlerWithFetch | (new (ctx: ExecutionContext, env: any) => WorkerEntrypointWithFetch); + apiHandler?: ExportedHandlerWithFetch | (new (ctx: ExecutionContext, env: Env) => WorkerEntrypointWithFetch); /** * Map of API routes to their corresponding handlers for the multi-handler configuration. @@ -126,14 +140,14 @@ export interface OAuthProviderOptions { */ apiHandlers?: Record< string, - ExportedHandlerWithFetch | (new (ctx: ExecutionContext, env: any) => WorkerEntrypointWithFetch) + ExportedHandlerWithFetch | (new (ctx: ExecutionContext, env: Env) => WorkerEntrypointWithFetch) >; /** * Handler for all non-API requests or API requests without a valid token. * Can be either an ExportedHandler object with a fetch method or a class extending WorkerEntrypoint. */ - defaultHandler: ExportedHandler | (new (ctx: ExecutionContext, env: any) => WorkerEntrypointWithFetch); + defaultHandler: ExportedHandler | (new (ctx: ExecutionContext, env: Env) => WorkerEntrypointWithFetch); /** * URL of the OAuth authorization endpoint where users can grant permissions. @@ -191,6 +205,7 @@ export interface OAuthProviderOptions { */ tokenExchangeCallback?: ( options: TokenExchangeCallbackOptions + // biome-ignore lint/suspicious/noConfusingVoidType: this could've beeen undefined, but would be a breaking change if we changed it now, so it's fine ) => Promise | TokenExchangeCallbackResult | void; /** @@ -204,6 +219,7 @@ export interface OAuthProviderOptions { description: string; status: number; headers: Record; + // biome-ignore lint/suspicious/noConfusingVoidType: this could've beeen undefined, but would be a breaking change if we changed it now, so it's fine }) => Response | void; } @@ -422,7 +438,7 @@ export interface CompleteAuthorizationOptions { /** * Application-specific metadata to associate with this grant */ - metadata: any; + metadata: Record; /** * List of scopes that were actually granted (may differ from requested scopes) @@ -433,7 +449,7 @@ export interface CompleteAuthorizationOptions { * Application-specific properties to include with API requests * authorized by this grant */ - props: any; + props: Record; } /** @@ -463,7 +479,7 @@ export interface Grant { /** * Application-specific metadata associated with this grant */ - metadata: any; + metadata: Record; /** * Encrypted application-specific properties @@ -637,7 +653,7 @@ export interface GrantSummary { /** * Application-specific metadata associated with this grant */ - metadata: any; + metadata: Record; /** * Unix timestamp when the grant was created @@ -650,14 +666,14 @@ export interface GrantSummary { * Implements authorization code flow with support for refresh tokens * and dynamic client registration. */ -export class OAuthProvider { - #impl: OAuthProviderImpl; +export class OAuthProvider { + #impl: OAuthProviderImpl; /** * Creates a new OAuth provider instance * @param options - Configuration options for the provider */ - constructor(options: OAuthProviderOptions) { + constructor(options: OAuthProviderOptions) { this.#impl = new OAuthProviderImpl(options); } @@ -669,7 +685,7 @@ export class OAuthProvider { * @param ctx - Cloudflare Worker execution context * @returns A Promise resolving to an HTTP Response */ - fetch(request: Request, env: any, ctx: ExecutionContext): Promise { + fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { return this.#impl.fetch(request, env, ctx); } } @@ -682,29 +698,29 @@ export class OAuthProvider { * annotation, and does not actually prevent the method from being called from outside the class, * including over RPC. */ -class OAuthProviderImpl { +class OAuthProviderImpl { /** * Configuration options for the provider */ - options: OAuthProviderOptions; + options: OAuthProviderOptions; /** * Represents the validated type of a handler (ExportedHandler or WorkerEntrypoint) */ - private typedDefaultHandler: TypedHandler; + private typedDefaultHandler: TypedHandler; /** * Array of tuples of API routes and their validated handlers * In the simple case, this will be a single entry with the route and handler from options.apiRoute/apiHandler * In the advanced case, this will contain entries from options.apiHandlers */ - private typedApiHandlers: Array<[string, TypedHandler]>; + private typedApiHandlers: Array<[string, TypedHandler]>; /** * Creates a new OAuth provider instance * @param options - Configuration options for the provider */ - constructor(options: OAuthProviderOptions) { + constructor(options: OAuthProviderOptions) { // Initialize typedApiHandlers as an array this.typedApiHandlers = []; @@ -783,6 +799,7 @@ class OAuthProviderImpl { try { new URL(endpoint); } catch (e) { + console.error('Error validating endpoint', e); throw new TypeError(`${name} must be either an absolute path starting with / or a valid URL`); } } @@ -795,15 +812,18 @@ class OAuthProviderImpl { * @returns The type of the handler (EXPORTED_HANDLER or WORKER_ENTRYPOINT) * @throws TypeError if the handler is invalid */ - private validateHandler(handler: any, name: string): TypedHandler { - if (typeof handler === 'object' && handler !== null && typeof handler.fetch === 'function') { + private validateHandler(handler: unknown, name: string): TypedHandler { + if (typeof handler === 'object' && handler !== null && 'fetch' in handler && typeof handler.fetch === 'function') { // It's an ExportedHandler object - return { type: HandlerType.EXPORTED_HANDLER, handler }; + return { type: HandlerType.EXPORTED_HANDLER, handler: handler as ExportedHandlerWithFetch }; } // Check if it's a class constructor extending WorkerEntrypoint if (typeof handler === 'function' && handler.prototype instanceof WorkerEntrypoint) { - return { type: HandlerType.WORKER_ENTRYPOINT, handler }; + return { + type: HandlerType.WORKER_ENTRYPOINT, + handler: handler as new (ctx: ExecutionContext, env: Env) => WorkerEntrypointWithFetch, + }; } throw new TypeError( @@ -819,7 +839,7 @@ class OAuthProviderImpl { * @param ctx - Cloudflare Worker execution context * @returns A Promise resolving to an HTTP Response */ - async fetch(request: Request, env: any, ctx: ExecutionContext): Promise { + async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { const url = new URL(request.url); // Special handling for OPTIONS requests (CORS preflight) @@ -954,10 +974,10 @@ class OAuthProviderImpl { */ private async parseTokenEndpointRequest( request: Request, - env: any + env: Env ): Promise< | { - body: any; + body: Record; clientInfo: ClientInfo; isRevocationRequest: boolean; } @@ -968,8 +988,8 @@ class OAuthProviderImpl { return this.createErrorResponse('invalid_request', 'Method not allowed', 405); } - let contentType = request.headers.get('Content-Type') || ''; - let body: any = {}; + const contentType = request.headers.get('Content-Type') || ''; + const body: Record = {}; // According to OAuth 2.0 RFC 6749/7009, requests MUST use application/x-www-form-urlencoded if (!contentType.includes('application/x-www-form-urlencoded')) { @@ -987,7 +1007,7 @@ class OAuthProviderImpl { let clientId = ''; let clientSecret = ''; - if (authHeader && authHeader.startsWith('Basic ')) { + if (authHeader?.startsWith('Basic ')) { // Basic auth const credentials = atob(authHeader.substring(6)); const [id, secret] = credentials.split(':', 2); @@ -1081,7 +1101,7 @@ class OAuthProviderImpl { * @param url - The URL to find a handler for * @returns The TypedHandler for the URL, or undefined if no handler matches */ - private findApiHandlerForUrl(url: URL): TypedHandler | undefined { + private findApiHandlerForUrl(url: URL): TypedHandler | undefined { // Check each route in our array of validated API handlers for (const [route, handler] of this.typedApiHandlers) { if (this.matchApiRoute(url, route)) { @@ -1148,7 +1168,7 @@ class OAuthProviderImpl { const tokenEndpoint = this.getFullEndpointUrl(this.options.tokenEndpoint, requestUrl); const authorizeEndpoint = this.getFullEndpointUrl(this.options.authorizeEndpoint, requestUrl); - let registrationEndpoint: string | undefined = undefined; + let registrationEndpoint: string | undefined; if (this.options.clientRegistrationEndpoint) { registrationEndpoint = this.getFullEndpointUrl(this.options.clientRegistrationEndpoint, requestUrl); } @@ -1200,7 +1220,7 @@ class OAuthProviderImpl { * @param env - Cloudflare Worker environment variables * @returns Response with token data or error */ - private async handleTokenRequest(body: any, clientInfo: ClientInfo, env: any): Promise { + private async handleTokenRequest(body: Record, clientInfo: ClientInfo, env: Env): Promise { // Handle different grant types const grantType = body.grant_type; @@ -1221,7 +1241,11 @@ class OAuthProviderImpl { * @param env - Cloudflare Worker environment variables * @returns Response with token data or error */ - private async handleAuthorizationCodeGrant(body: any, clientInfo: ClientInfo, env: any): Promise { + private async handleAuthorizationCodeGrant( + body: Record, + clientInfo: ClientInfo, + env: Env + ): Promise { const code = body.code; const redirectUri = body.redirect_uri; const codeVerifier = body.code_verifier; @@ -1240,7 +1264,7 @@ class OAuthProviderImpl { // Get the grant const grantKey = `grant:${userId}:${grantId}`; - const grantData: Grant | null = await env.OAUTH_KV.get(grantKey, { type: 'json' }); + const grantData = await env.OAUTH_KV.get(grantKey, { type: 'json' }); if (!grantData) { return this.createErrorResponse('invalid_grant', 'Grant not found or authorization code expired'); @@ -1456,7 +1480,11 @@ class OAuthProviderImpl { * @param env - Cloudflare Worker environment variables * @returns Response with token data or error */ - private async handleRefreshTokenGrant(body: any, clientInfo: ClientInfo, env: any): Promise { + private async handleRefreshTokenGrant( + body: Record, + clientInfo: ClientInfo, + env: Env + ): Promise { const refreshToken = body.refresh_token; if (!refreshToken) { @@ -1476,7 +1504,7 @@ class OAuthProviderImpl { // Get the associated grant using userId in the key const grantKey = `grant:${userId}:${grantId}`; - const grantData: Grant | null = await env.OAUTH_KV.get(grantKey, { type: 'json' }); + const grantData = await env.OAUTH_KV.get(grantKey, { type: 'json' }); if (!grantData) { return this.createErrorResponse('invalid_grant', 'Grant not found'); @@ -1666,7 +1694,7 @@ class OAuthProviderImpl { * @param env - Cloudflare Worker environment variables * @returns Response confirming revocation or error */ - private async handleRevocationRequest(body: any, env: any): Promise { + private async handleRevocationRequest(body: Record, env: Env): Promise { // Handle the revocation request return this.revokeToken(body, env); } @@ -1678,7 +1706,7 @@ class OAuthProviderImpl { * @param env - Cloudflare Worker environment variables * @returns Response confirming revocation or error */ - private async revokeToken(body: any, env: any): Promise { + private async revokeToken(body: Record, env: Env): Promise { const token = body.token; if (!token) { @@ -1710,7 +1738,7 @@ class OAuthProviderImpl { * @param grantId - The grant ID extracted from the token * @param env - Cloudflare Worker environment variables */ - private async revokeSpecificAccessToken(tokenId: string, userId: string, grantId: string, env: any): Promise { + private async revokeSpecificAccessToken(tokenId: string, userId: string, grantId: string, env: Env): Promise { const tokenKey = `token:${userId}:${grantId}:${tokenId}`; await env.OAUTH_KV.delete(tokenKey); } @@ -1723,9 +1751,9 @@ class OAuthProviderImpl { * @param env - Cloudflare Worker environment variables * @returns Promise indicating if the token is valid */ - private async validateAccessToken(tokenId: string, userId: string, grantId: string, env: any): Promise { + private async validateAccessToken(tokenId: string, userId: string, grantId: string, env: Env): Promise { const tokenKey = `token:${userId}:${grantId}:${tokenId}`; - const tokenData = await env.OAUTH_KV.get(tokenKey, { type: 'json' }); + const tokenData = await env.OAUTH_KV.get(tokenKey, { type: 'json' }); if (!tokenData) { return false; @@ -1744,9 +1772,9 @@ class OAuthProviderImpl { * @param env - Cloudflare Worker environment variables * @returns Promise indicating if the token is valid */ - private async validateRefreshToken(tokenId: string, userId: string, grantId: string, env: any): Promise { + private async validateRefreshToken(tokenId: string, userId: string, grantId: string, env: Env): Promise { const grantKey = `grant:${userId}:${grantId}`; - const grantData = await env.OAUTH_KV.get(grantKey, { type: 'json' }); + const grantData = await env.OAUTH_KV.get(grantKey, { type: 'json' }); if (!grantData) { return false; @@ -1762,7 +1790,7 @@ class OAuthProviderImpl { * @param env - Cloudflare Worker environment variables * @returns Response with client registration data or error */ - private async handleClientRegistration(request: Request, env: any): Promise { + private async handleClientRegistration(request: Request, env: Env): Promise { if (!this.options.clientRegistrationEndpoint) { return this.createErrorResponse('not_implemented', 'Client registration is not enabled', 501); } @@ -1773,14 +1801,14 @@ class OAuthProviderImpl { } // Check content length to ensure it's not too large (1 MiB limit) - const contentLength = parseInt(request.headers.get('Content-Length') || '0', 10); + const contentLength = Number.parseInt(request.headers.get('Content-Length') || '0', 10); if (contentLength > 1048576) { // 1 MiB = 1048576 bytes return this.createErrorResponse('invalid_request', 'Request payload too large, must be under 1 MiB', 413); } // Parse client metadata with a size limitation - let clientMetadata; + let clientMetadata: Record; try { const text = await request.text(); if (text.length > 1048576) { @@ -1789,11 +1817,12 @@ class OAuthProviderImpl { } clientMetadata = JSON.parse(text); } catch (error) { + console.error('Error parsing client metadata', error); return this.createErrorResponse('invalid_request', 'Invalid JSON payload', 400); } // Basic type validation functions - const validateStringField = (field: any): string | undefined => { + const validateStringField = (field: unknown): string | undefined => { if (field === undefined) { return undefined; } @@ -1803,7 +1832,7 @@ class OAuthProviderImpl { return field; }; - const validateStringArray = (arr: any): string[] | undefined => { + const validateStringArray = (arr: unknown): string[] | undefined => { if (arr === undefined) { return undefined; } @@ -1881,7 +1910,7 @@ class OAuthProviderImpl { await env.OAUTH_KV.put(`client:${clientId}`, JSON.stringify(clientInfo)); // Return client information with the original unhashed secret - const response: Record = { + const response: Record = { client_id: clientInfo.clientId, redirect_uris: clientInfo.redirectUris, client_name: clientInfo.clientName, @@ -1916,7 +1945,7 @@ class OAuthProviderImpl { * @param ctx - Cloudflare Worker execution context * @returns Response from the API handler or error */ - private async handleApiRequest(request: Request, env: any, ctx: ExecutionContext): Promise { + private async handleApiRequest(request: Request, env: Env, ctx: ExecutionContext): Promise { // Get access token from Authorization header const authHeader = request.headers.get('Authorization'); @@ -1944,7 +1973,7 @@ class OAuthProviderImpl { // Look up the token record, which now contains the denormalized grant information const tokenKey = `token:${userId}:${grantId}:${accessTokenId}`; - const tokenData: Token | null = await env.OAUTH_KV.get(tokenKey, { type: 'json' }); + const tokenData = await env.OAUTH_KV.get(tokenKey, { type: 'json' }); // Verify token if (!tokenData) { @@ -2001,7 +2030,7 @@ class OAuthProviderImpl { * @param env - Cloudflare Worker environment variables * @returns An instance of OAuthHelpers */ - private createOAuthHelpers(env: any): OAuthHelpers { + private createOAuthHelpers(env: Env): OAuthHelpers { return new OAuthHelpersImpl(env, this); } @@ -2014,9 +2043,9 @@ class OAuthProviderImpl { * @param clientId - The client ID to look up * @returns The client information, or null if not found */ - getClient(env: any, clientId: string): Promise { + getClient(env: Env, clientId: string): Promise { const clientKey = `client:${clientId}`; - return env.OAUTH_KV.get(clientKey, { type: 'json' }); + return env.OAUTH_KV.get(clientKey, { type: 'json' }); } /** @@ -2030,7 +2059,7 @@ class OAuthProviderImpl { private createErrorResponse( code: string, description: string, - status: number = 400, + status = 400, headers: Record = {} ): Response { // Notify the user of the error and allow them to override the response @@ -2147,7 +2176,7 @@ function base64ToArrayBuffer(base64: string): ArrayBuffer { * @param data - The data to encrypt * @returns An object containing the encrypted data and the generated key */ -async function encryptProps(data: any): Promise<{ encryptedData: string; key: CryptoKey }> { +async function encryptProps(data: Record): Promise<{ encryptedData: string; key: CryptoKey }> { // Generate a new encryption key for this specific props data // @ts-ignore const key: CryptoKey = await crypto.subtle.generateKey( @@ -2190,7 +2219,7 @@ async function encryptProps(data: any): Promise<{ encryptedData: string; key: Cr * @param encryptedData - The encrypted data as a base64 string * @returns The decrypted data object */ -async function decryptProps(key: CryptoKey, encryptedData: string): Promise { +async function decryptProps(key: CryptoKey, encryptedData: string): Promise> { // Convert base64 string back to ArrayBuffer const encryptedBuffer = base64ToArrayBuffer(encryptedData); @@ -2299,16 +2328,16 @@ async function unwrapKeyWithToken(tokenStr: string, wrappedKeyBase64: string): P * Class that implements the OAuth helper methods * Provides methods for OAuth operations needed by handlers */ -class OAuthHelpersImpl implements OAuthHelpers { - private env: any; - private provider: OAuthProviderImpl; +class OAuthHelpersImpl implements OAuthHelpers { + private env: Env; + private provider: OAuthProviderImpl; /** * Creates a new OAuthHelpers instance * @param env - Cloudflare Worker environment variables * @param provider - Reference to the parent provider instance */ - constructor(env: any, provider: OAuthProviderImpl) { + constructor(env: Env, provider: OAuthProviderImpl) { this.env = env; this.provider = provider; } @@ -2338,15 +2367,13 @@ class OAuthHelpersImpl implements OAuthHelpers { const clientInfo = await this.lookupClient(clientId); if (!clientInfo) { - throw new Error( - `Invalid client. The clientId provided does not match to this client.` - ); + throw new Error('Invalid client. The clientId provided does not match to this client.'); } // If client exists, validate the redirect URI against registered URIs if (clientInfo && redirectUri) { if (!clientInfo.redirectUris.includes(redirectUri)) { throw new Error( - `Invalid redirect URI. The redirect URI provided does not match any registered URI for this client.` + 'Invalid redirect URI. The redirect URI provided does not match any registered URI for this client.' ); } } @@ -2608,12 +2635,12 @@ class OAuthHelpersImpl implements OAuthHelpers { } // Determine token endpoint auth method - let authMethod = updates.tokenEndpointAuthMethod || client.tokenEndpointAuthMethod || 'client_secret_basic'; + const authMethod = updates.tokenEndpointAuthMethod || client.tokenEndpointAuthMethod || 'client_secret_basic'; const isPublicClient = authMethod === 'none'; // Handle changes in auth method let secretToStore = client.clientSecret; - let originalSecret: string | undefined = undefined; + let originalSecret: string | undefined; if (isPublicClient) { // Public clients don't have secrets @@ -2688,7 +2715,7 @@ class OAuthHelpersImpl implements OAuthHelpers { // Fetch all grants in parallel and convert to grant summaries const grantSummaries: GrantSummary[] = []; const promises = response.keys.map(async (key: { name: string }) => { - const grantData: Grant | null = await this.env.OAUTH_KV.get(key.name, { type: 'json' }); + const grantData = await this.env.OAUTH_KV.get(key.name, { type: 'json' }); if (grantData) { // Create a summary with only the public fields const summary: GrantSummary = {