From ceb85bdcda3d60f8f6e11ccdd838ecc49032f5c6 Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Fri, 9 Feb 2024 10:17:08 +0100 Subject: [PATCH 1/6] Add cache for issuer directory Insatead of computing the issuer directory whenever requested, cache it with the same setting used by clients. This cache is clearer upon key rotation. This commit also allows cache time to be configured per deployment. --- src/bindings.ts | 1 + src/index.ts | 32 ++++++++++++++++++++++++++++++-- test/index.test.ts | 38 +++++++++++++++++++++++++++++++++++++- test/mocks.ts | 25 +++++++++++++++++++++++++ wrangler.toml | 1 + 5 files changed, 94 insertions(+), 3 deletions(-) diff --git a/src/bindings.ts b/src/bindings.ts index 1f2a4b8..dc3009c 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -2,6 +2,7 @@ import type { R2Bucket } from '@cloudflare/workers-types/2023-07-01'; export interface Bindings { // variables and secrets + DIRECTORY_CACHE_MAX_AGE_SECONDS: string; ENVIRONMENT: string; LOGGING_SHIM_TOKEN: string; SENTRY_ACCESS_CLIENT_ID: string; diff --git a/src/index.ts b/src/index.ts index 43b2c8d..4bdc7e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -79,7 +79,22 @@ export const handleTokenRequest = async (ctx: Context, request: Request) => { }); }; +const getDirectoryCache = async (): Promise => { + return caches.open('response/issuer-directory'); +}; + +const FAKE_DOMAIN_CACHE = 'cache.local'; +const DIRECTORY_CACHE_REQUEST = new Request( + `https://${FAKE_DOMAIN_CACHE}${PRIVATE_TOKEN_ISSUER_DIRECTORY}` +); + export const handleTokenDirectory = async (ctx: Context, request?: Request) => { + const cache = await getDirectoryCache(); + const cachedResponse = await cache.match(DIRECTORY_CACHE_REQUEST); + if (cachedResponse) { + return cachedResponse; + } + const keys = await ctx.env.ISSUANCE_KEYS.list({ include: ['customMetadata'] }); if (keys.objects.length === 0) { @@ -95,12 +110,20 @@ export const handleTokenDirectory = async (ctx: Context, request?: Request) => { })), }; - return new Response(JSON.stringify(directory), { + const response = new Response(JSON.stringify(directory), { headers: { 'content-type': MediaType.PRIVATE_TOKEN_ISSUER_DIRECTORY, - 'cache-control': 'public, max-age=86400', + 'cache-control': `public, max-age=${ctx.env.DIRECTORY_CACHE_MAX_AGE_SECONDS}`, }, }); + ctx.waitUntil(cache.put(DIRECTORY_CACHE_REQUEST, response.clone())); + + return response; +}; + +const clearDirectoryCache = async (): Promise => { + const cache = await getDirectoryCache(); + return cache.delete(DIRECTORY_CACHE_REQUEST); }; export const handleRotateKey = async (ctx: Context, request?: Request) => { @@ -148,6 +171,8 @@ export const handleRotateKey = async (ctx: Context, request?: Request) => { customMetadata: metadata, }); + ctx.waitUntil(clearDirectoryCache()); + return new Response(`New key ${publicKeyEnc}`, { status: 201 }); }; @@ -169,6 +194,9 @@ const handleClearKey = async (ctx: Context, request?: Request) => { } const toDeleteArray = [...toDelete]; await ctx.env.ISSUANCE_KEYS.delete(toDeleteArray); + + ctx.waitUntil(clearDirectoryCache()); + return new Response(`Keys cleared: ${toDeleteArray.join('\n')}`, { status: 201 }); }; diff --git a/test/index.test.ts b/test/index.test.ts index a881a38..5045e1e 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1,8 +1,10 @@ +import { jest } from '@jest/globals'; + import { Context } from '../src/context'; import { handleTokenRequest, default as workerObject } from '../src/index'; import { IssuerConfigurationResponse } from '../src/types'; import { b64ToB64URL, u8ToB64 } from '../src/utils/base64'; -import { ExecutionContextMock, getContext, getEnv } from './mocks'; +import { ExecutionContextMock, MockCache, getContext, getEnv } from './mocks'; import { RSABSSA } from '@cloudflare/blindrsa-ts'; import { MediaType, @@ -147,3 +149,37 @@ describe('rotate and clear key', () => { expect(directory['token-keys']).toHaveLength(1); }); }); + +describe('cache directory response', () => { + const rotateURL = `${sampleURL}/admin/rotate`; + const directoryURL = `${sampleURL}${PRIVATE_TOKEN_ISSUER_DIRECTORY}`; + + const initializeKeys = async (numberOfKeys = 1): Promise => { + const rotateRequest = new Request(rotateURL, { method: 'POST' }); + + for (let i = 0; i < numberOfKeys; i += 1) { + await workerObject.fetch(rotateRequest, getEnv(), new ExecutionContextMock()); + } + }; + + it('should cache the directory response', async () => { + await initializeKeys(); + + const mockCache = new MockCache(); + const spy = jest.spyOn(caches, 'open').mockResolvedValue(mockCache); + const directoryRequest = new Request(directoryURL); + + let response = await workerObject.fetch(directoryRequest, getEnv(), new ExecutionContextMock()); + expect(response.ok).toBe(true); + expect(Object.entries(mockCache.cache)).toHaveLength(1); + + const [cachedURL, _] = Object.entries(mockCache.cache)[0]; + const cachedResponse = new Response('cached response'); + mockCache.cache[cachedURL] = cachedResponse; + + response = await workerObject.fetch(directoryRequest, getEnv(), new ExecutionContextMock()); + expect(response.ok).toBe(true); + expect(response).toBe(cachedResponse); + spy.mockClear(); + }); +}); diff --git a/test/mocks.ts b/test/mocks.ts index 289b6fa..a76ceb1 100644 --- a/test/mocks.ts +++ b/test/mocks.ts @@ -3,6 +3,31 @@ import { Context, WaitUntilFunc } from '../src/context'; import { ConsoleLogger, Logger } from '../src/context/logging'; import { MetricsRegistry } from '../src/context/metrics'; +export class MockCache implements Cache { + public cache: Record = {}; + + async match(info: RequestInfo, options?: CacheQueryOptions): Promise { + if (options) { + throw new Error('CacheQueryOptions not supported'); + } + const url = new URL(info instanceof Request ? info.url : info).href; + return this.cache[url]; + } + + async delete(info: RequestInfo, options?: CacheQueryOptions): Promise { + if (options) { + throw new Error('CacheQueryOptions not supported'); + } + const url = new URL(info instanceof Request ? info.url : info).href; + return delete this.cache[url]; + } + + async put(info: RequestInfo, response: Response): Promise { + const url = new URL(info instanceof Request ? info.url : info).href; + this.cache[url] = response; + } +} + export class ExecutionContextMock implements ExecutionContext { waitUntils: Promise[] = []; passThrough = false; diff --git a/wrangler.toml b/wrangler.toml index e6aad47..2f53360 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -14,6 +14,7 @@ route = { pattern = "pp-issuer.example.test", custom_domain=true } crons = ["0 0 * * *"] [env.production.vars] +DIRECTORY_CACHE_MAX_AGE_SECONDS = "86400" ENVIRONMENT = "production" SENTRY_SAMPLE_RATE = "0" # Between 0-1 if you log errors on Sentry. 0 disables Sentry logging. Configuration is done through Workers Secrets From 1f927bd445ace57f1996b0e68b3c1209597c4719 Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Fri, 9 Feb 2024 10:18:28 +0100 Subject: [PATCH 2/6] Fix unused variable warning Disable eslint no-unused-variable for handler. `request` does not have to be used, and it's good to have a consistent signature for the handlers. --- src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/index.ts b/src/index.ts index 4bdc7e4..d75a3a1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -88,6 +88,7 @@ const DIRECTORY_CACHE_REQUEST = new Request( `https://${FAKE_DOMAIN_CACHE}${PRIVATE_TOKEN_ISSUER_DIRECTORY}` ); +// eslint-disable-next-line @typescript-eslint/no-unused-vars export const handleTokenDirectory = async (ctx: Context, request?: Request) => { const cache = await getDirectoryCache(); const cachedResponse = await cache.match(DIRECTORY_CACHE_REQUEST); @@ -126,6 +127,7 @@ const clearDirectoryCache = async (): Promise => { return cache.delete(DIRECTORY_CACHE_REQUEST); }; +// eslint-disable-next-line @typescript-eslint/no-unused-vars export const handleRotateKey = async (ctx: Context, request?: Request) => { ctx.metrics.keyRotationTotal.inc({ env: ctx.env.ENVIRONMENT }); @@ -176,6 +178,7 @@ export const handleRotateKey = async (ctx: Context, request?: Request) => { return new Response(`New key ${publicKeyEnc}`, { status: 201 }); }; +// eslint-disable-next-line @typescript-eslint/no-unused-vars const handleClearKey = async (ctx: Context, request?: Request) => { ctx.metrics.keyClearTotal.inc({ env: ctx.env.ENVIRONMENT }); const keys = await ctx.env.ISSUANCE_KEYS.list(); From 92ef2a9a362f70cc0853fa1a378415f628f06ed5 Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Fri, 9 Feb 2024 10:23:34 +0100 Subject: [PATCH 3/6] Add metrics for directory cache miss To enable monitoring of cache miss, a dedicated prometheus metric is added. --- src/context/metrics.ts | 6 ++++++ src/index.ts | 1 + 2 files changed, 7 insertions(+) diff --git a/src/context/metrics.ts b/src/context/metrics.ts index d9a956c..98cab39 100644 --- a/src/context/metrics.ts +++ b/src/context/metrics.ts @@ -19,6 +19,7 @@ export class MetricsRegistry { keyClearTotal: CounterType; issuanceRequestTotal: CounterType; signedTokenTotal: CounterType; + directoryCacheMissTotal: CounterType; constructor(options: RegistryOptions) { this.options = options; @@ -50,6 +51,11 @@ export class MetricsRegistry { 'signed_token_total', 'Number of issued signed private tokens.' ); + this.directoryCacheMissTotal = this.registry.create( + 'counter', + 'directory_cache_miss_total', + 'Number of requests for private token issuer directory which are not served by the cache.' + ); } /** diff --git a/src/index.ts b/src/index.ts index d75a3a1..b5fd31e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -95,6 +95,7 @@ export const handleTokenDirectory = async (ctx: Context, request?: Request) => { if (cachedResponse) { return cachedResponse; } + ctx.metrics.directoryCacheMissTotal.inc({ env: ctx.env.ENVIRONMENT }); const keys = await ctx.env.ISSUANCE_KEYS.list({ include: ['customMetadata'] }); From e193ae56e60fb2bf47c021e8661a8fd9db03a38d Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Fri, 9 Feb 2024 10:25:19 +0100 Subject: [PATCH 4/6] Fix metrics variable declaration to use alphabetical order With the multiplication of metrics, use alphabetical ordering to ease searches. --- src/context/metrics.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/context/metrics.ts b/src/context/metrics.ts index 98cab39..325303f 100644 --- a/src/context/metrics.ts +++ b/src/context/metrics.ts @@ -13,24 +13,33 @@ export class MetricsRegistry { registry: RegistryType; options: RegistryOptions; - requestsTotal: CounterType; + directoryCacheMissTotal: CounterType; erroredRequestsTotal: CounterType; + issuanceRequestTotal: CounterType; keyRotationTotal: CounterType; keyClearTotal: CounterType; - issuanceRequestTotal: CounterType; + requestsTotal: CounterType; signedTokenTotal: CounterType; - directoryCacheMissTotal: CounterType; constructor(options: RegistryOptions) { this.options = options; this.registry = new Registry(); - this.requestsTotal = this.registry.create('counter', 'requests_total', 'total requests'); + this.directoryCacheMissTotal = this.registry.create( + 'counter', + 'directory_cache_miss_total', + 'Number of requests for private token issuer directory which are not served by the cache.' + ); this.erroredRequestsTotal = this.registry.create( 'counter', 'errored_requests_total', 'Errored requests served to eyeball' ); + this.issuanceRequestTotal = this.registry.create( + 'counter', + 'issuance_request_total', + 'Number of requests for private token issuance.' + ); this.keyRotationTotal = this.registry.create( 'counter', 'key_rotation_total', @@ -41,21 +50,12 @@ export class MetricsRegistry { 'key_clear_total', 'Number of key clear performed.' ); - this.issuanceRequestTotal = this.registry.create( - 'counter', - 'issuance_request_total', - 'Number of requests for private token issuance.' - ); + this.requestsTotal = this.registry.create('counter', 'requests_total', 'total requests'); this.signedTokenTotal = this.registry.create( 'counter', 'signed_token_total', 'Number of issued signed private tokens.' ); - this.directoryCacheMissTotal = this.registry.create( - 'counter', - 'directory_cache_miss_total', - 'Number of requests for private token issuer directory which are not served by the cache.' - ); } /** From 6482ab38d56bf10c5e3533fab492ef834824447a Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Fri, 9 Feb 2024 16:05:21 +0100 Subject: [PATCH 5/6] Add HEAD request handling and etag to issuer directory Browsers might send `HEAD` request to understand if they should do a `GET` request. This commit adds an ETag on response, allowing `If-None-Match` to be handled, as well as `HEAD` request to be correctly processed. --- src/index.ts | 30 ++++++++++++++++++++++++++++-- src/utils/hex.ts | 12 ++++++++++++ test/index.test.ts | 25 ++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 src/utils/hex.ts diff --git a/src/index.ts b/src/index.ts index b5fd31e..4ef62b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ import { } from '@cloudflare/privacypass-ts'; import { ConsoleLogger } from './context/logging'; import { MetricsRegistry } from './context/metrics'; +import { hexEncode } from './utils/hex'; const { BlindRSAMode, Issuer, TokenRequest } = publicVerif; const keyToTokenKeyID = async (key: Uint8Array): Promise => { @@ -88,11 +89,26 @@ const DIRECTORY_CACHE_REQUEST = new Request( `https://${FAKE_DOMAIN_CACHE}${PRIVATE_TOKEN_ISSUER_DIRECTORY}` ); +export const handleHeadTokenDirectory = async (ctx: Context, request: Request) => { + const getResponse = await handleTokenDirectory(ctx, request); + + return new Response(undefined, { + status: getResponse.status, + headers: getResponse.headers, + }); +}; + // eslint-disable-next-line @typescript-eslint/no-unused-vars -export const handleTokenDirectory = async (ctx: Context, request?: Request) => { +export const handleTokenDirectory = async (ctx: Context, request: Request) => { const cache = await getDirectoryCache(); const cachedResponse = await cache.match(DIRECTORY_CACHE_REQUEST); if (cachedResponse) { + if (request.headers.get('if-none-match') === cachedResponse.headers.get('etag')) { + return new Response(undefined, { + status: 304, + headers: cachedResponse.headers, + }); + } return cachedResponse; } ctx.metrics.directoryCacheMissTotal.inc({ env: ctx.env.ENVIRONMENT }); @@ -112,10 +128,19 @@ export const handleTokenDirectory = async (ctx: Context, request?: Request) => { })), }; - const response = new Response(JSON.stringify(directory), { + const body = JSON.stringify(directory); + const digest = new Uint8Array( + await crypto.subtle.digest('SHA-256', new TextEncoder().encode(body)) + ); + const etag = `"${hexEncode(digest)}"`; + + const response = new Response(body, { headers: { 'content-type': MediaType.PRIVATE_TOKEN_ISSUER_DIRECTORY, 'cache-control': `public, max-age=${ctx.env.DIRECTORY_CACHE_MAX_AGE_SECONDS}`, + 'content-length': body.length.toString(), + 'date': new Date().toUTCString(), + etag, }, }); ctx.waitUntil(cache.put(DIRECTORY_CACHE_REQUEST, response.clone())); @@ -211,6 +236,7 @@ export default { const router = new Router(); router + .head(PRIVATE_TOKEN_ISSUER_DIRECTORY, handleHeadTokenDirectory) .get(PRIVATE_TOKEN_ISSUER_DIRECTORY, handleTokenDirectory) .post('/token-request', handleTokenRequest) .post('/admin/rotate', handleRotateKey) diff --git a/src/utils/hex.ts b/src/utils/hex.ts new file mode 100644 index 0000000..4b7d22e --- /dev/null +++ b/src/utils/hex.ts @@ -0,0 +1,12 @@ +export const hexDecode = (s: string) => { + const bytes = s.match(/.{1,2}/g); + if (!bytes) { + return new Uint8Array(0); + } + return Uint8Array.from(bytes.map(b => parseInt(b, 16))); +}; + +export const hexEncode = (u: Uint8Array) => + Array.from(u) + .map(b => b.toString(16).padStart(2, '0')) + .join(''); diff --git a/test/index.test.ts b/test/index.test.ts index 5045e1e..efb0cad 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -174,12 +174,35 @@ describe('cache directory response', () => { expect(Object.entries(mockCache.cache)).toHaveLength(1); const [cachedURL, _] = Object.entries(mockCache.cache)[0]; - const cachedResponse = new Response('cached response'); + const sampleEtag = '"sampleEtag"'; + const cachedResponse = new Response('cached response', { headers: { etag: sampleEtag } }); mockCache.cache[cachedURL] = cachedResponse; response = await workerObject.fetch(directoryRequest, getEnv(), new ExecutionContextMock()); expect(response.ok).toBe(true); expect(response).toBe(cachedResponse); + + const cachedDirectoryRequest = new Request(directoryURL, { + headers: { 'if-none-match': sampleEtag }, + }); + response = await workerObject.fetch( + cachedDirectoryRequest, + getEnv(), + new ExecutionContextMock() + ); + expect(response.status).toBe(304); + + const headCachedDirectoryRequest = new Request(directoryURL, { + method: 'HEAD', + headers: { 'if-none-match': sampleEtag }, + }); + response = await workerObject.fetch( + headCachedDirectoryRequest, + getEnv(), + new ExecutionContextMock() + ); + expect(response.status).toBe(304); + spy.mockClear(); }); }); From 6249350a74c28fa06775252b5014379a701a94b7 Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Mon, 12 Feb 2024 10:51:53 +0100 Subject: [PATCH 6/6] Update eslint-config-typescript to @typescript-eslint/eslint-plugin The former has not been updated in 5 years. --- package-lock.json | 274 ++++++++++++++++++++-------------------------- package.json | 18 ++- src/index.ts | 8 +- src/router.ts | 13 +++ 4 files changed, 150 insertions(+), 163 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca61c4b..cfabe0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,8 @@ "@cloudflare/blindrsa-ts": "0.3.2", "@cloudflare/workers-types": "4.20240117.0", "@types/jest": "29.5.11", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", "dotenv": "16.4.0", "esbuild": "0.19.12", "eslint": "8.56.0", @@ -2412,11 +2414,10 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true, - "peer": true + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true }, "node_modules/@types/node": { "version": "18.15.12", @@ -2439,11 +2440,10 @@ "peer": true }, "node_modules/@types/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", - "dev": true, - "peer": true + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", + "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", + "dev": true }, "node_modules/@types/stack-utils": { "version": "2.0.1", @@ -2467,33 +2467,33 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.5.tgz", - "integrity": "sha512-feA9xbVRWJZor+AnLNAr7A8JRWeZqHUf4T9tlP+TN04b05pFVhO5eN7/O93Y/1OUlLMHKbnJisgDURs/qvtqdg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dev": true, - "peer": true, "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/type-utils": "5.59.5", - "@typescript-eslint/utils": "5.59.5", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2502,26 +2502,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.5.tgz", - "integrity": "sha512-NJXQC4MRnF9N9yWqQE2/KLRSOLvrrlZb48NGVfBa+RuPMN6B7ZcK5jZOvhuygv4D64fRKnZI4L4p8+M+rfeQuw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, - "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2530,17 +2530,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.59.5.tgz", - "integrity": "sha512-jVecWwnkX6ZgutF+DovbBJirZcAxgxC0EOHYt/niMROf8p4PwxxG32Qdhj/iIQQIuOflLjNkxoXyArkcIP7C3A==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, - "peer": true, "dependencies": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5" + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -2548,26 +2547,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.59.5.tgz", - "integrity": "sha512-4eyhS7oGym67/pSxA2mmNq7X164oqDYNnZCUayBwJZIRVvKpBCMBzFnFxjeoDeShjtO6RQBHBuwybuX3POnDqg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", "dev": true, - "peer": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.59.5", - "@typescript-eslint/utils": "5.59.5", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -2576,13 +2574,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.59.5.tgz", - "integrity": "sha512-xkfRPHbqSH4Ggx4eHRIO/eGL8XL4Ysb4woL8c87YuAo8Md7AUjyWKa9YMwTL519SyDPrfEgKdewjkxNCVeJW7w==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true, - "peer": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -2590,22 +2587,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.5.tgz", - "integrity": "sha512-+XXdLN2CZLZcD/mO7mQtJMvCkzRfmODbeSKuMY/yXbGkzvA9rJyDY5qDYNoiz2kP/dmyAxXquL2BvLQLJFPQIg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, - "peer": true, "dependencies": { - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/visitor-keys": "5.59.5", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -2617,45 +2614,66 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/utils": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.59.5.tgz", - "integrity": "sha512-sCEHOiw+RbyTii9c3/qN74hYDPNORb8yWCoPLmB7BIflhplJ65u2PBpdRla12e3SSTJ2erRkPjz7ngLHhUegxA==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, - "peer": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.59.5", - "@typescript-eslint/types": "5.59.5", - "@typescript-eslint/typescript-estree": "5.59.5", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.59.5", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.5.tgz", - "integrity": "sha512-qL+Oz+dbeBRTeyJTIy0eniD3uvqU7x+y1QceBismZ41hd4aBSRh8UAw4pZP0+XzLuPZmx4raNMq/I+59W2lXKA==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dev": true, - "peer": true, "dependencies": { - "@typescript-eslint/types": "5.59.5", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -2791,7 +2809,6 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, - "peer": true, "engines": { "node": ">=8" } @@ -3359,7 +3376,6 @@ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, - "peer": true, "dependencies": { "path-type": "^4.0.0" }, @@ -3577,20 +3593,6 @@ "typescript": "*" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -3803,16 +3805,6 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "peer": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/estree-walker": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", @@ -3895,11 +3887,10 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, - "peer": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -4153,7 +4144,6 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, - "peer": true, "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -4175,13 +4165,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "node_modules/grapheme-splitter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", - "dev": true, - "peer": true - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -6065,7 +6048,6 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, - "peer": true, "engines": { "node": ">= 8" } @@ -6189,13 +6171,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true, - "peer": true - }, "node_modules/node-fetch": { "version": "2.6.11", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", @@ -7289,6 +7264,18 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, + "node_modules/ts-api-utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", + "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-jest": { "version": "29.1.2", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", @@ -7332,29 +7319,6 @@ } } }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true, - "peer": true - }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "peer": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index c650cb4..d108705 100644 --- a/package.json +++ b/package.json @@ -15,14 +15,28 @@ "eslintConfig": { "root": true, "extends": [ - "typescript", + "plugin:@typescript-eslint/recommended", "prettier" - ] + ], + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_" + } + ] + } }, "devDependencies": { "@cloudflare/blindrsa-ts": "0.3.2", "@cloudflare/workers-types": "4.20240117.0", "@types/jest": "29.5.11", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", "dotenv": "16.4.0", "esbuild": "0.19.12", "eslint": "8.56.0", diff --git a/src/index.ts b/src/index.ts index 4ef62b8..3c6d339 100644 --- a/src/index.ts +++ b/src/index.ts @@ -98,7 +98,6 @@ export const handleHeadTokenDirectory = async (ctx: Context, request: Request) = }); }; -// eslint-disable-next-line @typescript-eslint/no-unused-vars export const handleTokenDirectory = async (ctx: Context, request: Request) => { const cache = await getDirectoryCache(); const cachedResponse = await cache.match(DIRECTORY_CACHE_REQUEST); @@ -153,8 +152,7 @@ const clearDirectoryCache = async (): Promise => { return cache.delete(DIRECTORY_CACHE_REQUEST); }; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const handleRotateKey = async (ctx: Context, request?: Request) => { +export const handleRotateKey = async (ctx: Context, _request?: Request) => { ctx.metrics.keyRotationTotal.inc({ env: ctx.env.ENVIRONMENT }); // Generate a new type 2 Issuer key @@ -204,8 +202,7 @@ export const handleRotateKey = async (ctx: Context, request?: Request) => { return new Response(`New key ${publicKeyEnc}`, { status: 201 }); }; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const handleClearKey = async (ctx: Context, request?: Request) => { +const handleClearKey = async (ctx: Context, _request?: Request) => { ctx.metrics.keyClearTotal.inc({ env: ctx.env.ENVIRONMENT }); const keys = await ctx.env.ISSUANCE_KEYS.list(); @@ -236,7 +233,6 @@ export default { const router = new Router(); router - .head(PRIVATE_TOKEN_ISSUER_DIRECTORY, handleHeadTokenDirectory) .get(PRIVATE_TOKEN_ISSUER_DIRECTORY, handleTokenDirectory) .post('/token-request', handleTokenRequest) .post('/admin/rotate', handleRotateKey) diff --git a/src/router.ts b/src/router.ts index 5cf2ccd..5da90f5 100644 --- a/src/router.ts +++ b/src/router.ts @@ -44,6 +44,19 @@ export class Router { // normalise path, so that they never end with a trailing '/' path = this.normalisePath(path); this.handlers[method][path] = handler; + if (method === HttpMethod.GET) { + this.handlers[HttpMethod.HEAD] ??= {}; + this.handlers[HttpMethod.HEAD][path] = async ( + ctx: Context, + request: Request + ): Promise => { + const response = await handler(ctx, request); + if (response.ok) { + return new Response(null, response); + } + return response; + }; + } return this; }