diff --git a/packages/http-transport/src/server.ts b/packages/http-transport/src/server.ts index eec67317..89842295 100644 --- a/packages/http-transport/src/server.ts +++ b/packages/http-transport/src/server.ts @@ -438,13 +438,17 @@ export class HttpTransportServer implements TransportWorker< this.#corsOptions.origin === true ? { ...DEFAULT_CORS_PARAMS } : { ...EXPLICIT_ORIGIN_CORS_PARAMS } - // Iterating base params also drops allowCredentials for origin: true for (const key in params) { const value = this.#corsOptions[key] if (value !== undefined) { params[key] = value } } + // This explicit opt-in restores credentialed origin reflection without + // weakening the safe `cors: true` default. + if (this.#corsOptions.allowCredentials !== undefined) { + params.allowCredentials = this.#corsOptions.allowCredentials + } } } else if (typeof this.#corsOptions === 'function') { const result = this.#corsOptions(origin, request) @@ -466,6 +470,9 @@ export class HttpTransportServer implements TransportWorker< params[key] = value } } + if (result.allowCredentials !== undefined) { + params.allowCredentials = result.allowCredentials + } } } } diff --git a/packages/http-transport/src/types.ts b/packages/http-transport/src/types.ts index 7c9aaef5..ef309a7c 100644 --- a/packages/http-transport/src/types.ts +++ b/packages/http-transport/src/types.ts @@ -22,31 +22,23 @@ export type HttpTransportOptions< } export type HttpTransportCorsCustomParams = { + /** + * `true` reflects any request origin, an array is an explicit allowlist. + * Credentials default on only for allowlisted origins. + */ + origin: true | string[] allowMethods?: string[] allowHeaders?: string[] + /** + * Explicitly enables credentials when reflecting request origins. Only use + * this when the API should accept credentialed requests from any website. + */ + allowCredentials?: string maxAge?: string exposeHeaders?: string[] requestHeaders?: string[] requestMethod?: string -} & ( - | { - /** - * `true` reflects any request origin, an array is an explicit - * allowlist. Credentials default on for allowlisted origins only. - */ - origin: true | string[] - allowCredentials?: never - } - | { - /** - * Explicit `allowCredentials` requires an origin allowlist: combining - * credentials with a reflected origin (`origin: true`) would let any - * website make credentialed (cookie-authed) requests. - */ - origin: string[] - allowCredentials?: string - } -) +} export type HttpTransportCorsOptions = | true diff --git a/packages/http-transport/tests/cors.spec.ts b/packages/http-transport/tests/cors.spec.ts index 7ab27e22..e022fbdf 100644 --- a/packages/http-transport/tests/cors.spec.ts +++ b/packages/http-transport/tests/cors.spec.ts @@ -46,13 +46,23 @@ describe('CORS credentials defaults', () => { expect(response.headers.get(ALLOW_CREDENTIALS)).toBeNull() }) - it('omits credentials for custom params with origin: true', async () => { + it('omits credentials by default for custom params with origin: true', async () => { const response = await preflight({ origin: true }) expect(response.headers.get(ALLOW_ORIGIN)).toBe(ORIGIN) expect(response.headers.get(ALLOW_CREDENTIALS)).toBeNull() }) + it('allows explicit credentials with reflected origins', async () => { + const response = await preflight({ + origin: true, + allowCredentials: 'true', + }) + + expect(response.headers.get(ALLOW_ORIGIN)).toBe(ORIGIN) + expect(response.headers.get(ALLOW_CREDENTIALS)).toBe('true') + }) + it('enables credentials for custom params with explicit origins', async () => { const response = await preflight({ origin: [ORIGIN] }) @@ -139,7 +149,7 @@ describe('CORS Vary header', () => { }) describe('CORS config type surface', () => { - it('accepts safe configs and rejects credentials with reflected origin', () => { + it('accepts serializable explicit credentials with reflected origins', () => { const allowAll = Math.random() >= 0 const allowedOrigins = [ORIGIN] @@ -151,14 +161,13 @@ describe('CORS config type surface', () => { origin: allowedOrigins, allowCredentials: 'true', } - // @ts-expect-error credentials cannot be combined with origin reflection - const unsafe: HttpTransportCorsOptions = { + const reflectedCredentials: HttpTransportCorsOptions = { origin: true, allowCredentials: 'true', } expect(composed).toBeDefined() expect(credentialed).toBeDefined() - expect(unsafe).toBeDefined() + expect(reflectedCredentials).toBeDefined() }) })