diff --git a/.changeset/strong-taxis-tell.md b/.changeset/strong-taxis-tell.md new file mode 100644 index 00000000000..df487381696 --- /dev/null +++ b/.changeset/strong-taxis-tell.md @@ -0,0 +1,5 @@ +--- +"@effect/platform": patch +--- + +feat: Add Redacted support to headers diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000000..bda5ef7b5c0 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,30 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Watch", + "type": "shell", + "command": "pnpm check -w", + "options": { + "cwd": "${workspaceRoot}" + }, + "group": { + "kind": "build", + "isDefault": true + }, + "isBackground": true, + "presentation": { + "group": "watch-build" + }, + "problemMatcher": [ + { + "base": "$tsc-watch", + "fileLocation": [ + "relative", + "${workspaceRoot}", + ], + } + ] + } + ] +} diff --git a/packages/platform-browser/src/internal/httpClient.ts b/packages/platform-browser/src/internal/httpClient.ts index d6f20e12e02..510c3cbf127 100644 --- a/packages/platform-browser/src/internal/httpClient.ts +++ b/packages/platform-browser/src/internal/httpClient.ts @@ -50,7 +50,7 @@ const makeXMLHttpRequest = Client.make((request, url, signal, fiber) => }, { once: true }) xhr.open(request.method, url.toString(), true) xhr.responseType = fiber.getFiberRef(currentXHRResponseType) - Object.entries(request.headers).forEach(([k, v]) => { + Object.entries(Headers.unredact(request.headers)).forEach(([k, v]) => { xhr.setRequestHeader(k, v) }) return Effect.zipRight( @@ -60,7 +60,9 @@ const makeXMLHttpRequest = Client.make((request, url, signal, fiber) => const onChange = () => { if (!sent && xhr.readyState >= 2) { sent = true - resume(Effect.succeed(new ClientResponseImpl(request, xhr))) + resume( + Effect.succeed(new ClientResponseImpl(request, xhr, fiber.getFiberRef(Headers.currentRedactedNames))) + ) } } xhr.onreadystatechange = onChange @@ -127,7 +129,8 @@ export abstract class IncomingMessageImpl extends Inspectable.Class constructor( readonly source: XMLHttpRequest, - readonly onError: (error: unknown) => E + readonly onError: (error: unknown) => E, + private readonly redactedKeys: string | RegExp | ReadonlyArray ) { super() this[IncomingMessage.TypeId] = IncomingMessage.TypeId @@ -147,7 +150,7 @@ export abstract class IncomingMessageImpl extends Inspectable.Class const parser = HeaderParser.make() const result = parser(encoder.encode(this._rawHeaderString + "\r\n"), 0) this._rawHeaders = result._tag === "Headers" ? result.headers : undefined - const parsed = result._tag === "Headers" ? Headers.fromInput(result.headers) : Headers.empty + const parsed = result._tag === "Headers" ? Headers.fromInput(result.headers, this.redactedKeys) : Headers.empty return this._headers = parsed } @@ -289,7 +292,8 @@ class ClientResponseImpl extends IncomingMessageImpl implem constructor( readonly request: ClientRequest.HttpClientRequest, - source: XMLHttpRequest + source: XMLHttpRequest, + redactedKeys: string | RegExp | ReadonlyArray ) { super(source, (cause) => new Error.ResponseError({ @@ -297,7 +301,7 @@ class ClientResponseImpl extends IncomingMessageImpl implem response: this, reason: "Decode", cause - })) + }), redactedKeys) this[ClientResponse.TypeId] = ClientResponse.TypeId } diff --git a/packages/platform-bun/src/internal/httpServer.ts b/packages/platform-bun/src/internal/httpServer.ts index eb10b0b9d76..a9b1a19d168 100644 --- a/packages/platform-bun/src/internal/httpServer.ts +++ b/packages/platform-bun/src/internal/httpServer.ts @@ -21,6 +21,7 @@ import * as Config from "effect/Config" import * as Deferred from "effect/Deferred" import * as Effect from "effect/Effect" import * as Exit from "effect/Exit" +import * as FiberRef from "effect/FiberRef" import * as FiberSet from "effect/FiberSet" import { pipe } from "effect/Function" import * as Inspectable from "effect/Inspectable" @@ -88,11 +89,17 @@ export const make = ( Effect.async((_) => { function handler(request: Request, server: BunServer) { return new Promise((resolve, _reject) => { - const fiber = runFork(Effect.provideService( - app, - ServerRequest.HttpServerRequest, - new ServerRequestImpl(request, resolve, removeHost(request.url), server) - )) + const fiber = runFork( + FiberRef.get(Headers.currentRedactedNames).pipe( + Effect.flatMap((keys) => + Effect.provideService( + app, + ServerRequest.HttpServerRequest, + new ServerRequestImpl(request, resolve, removeHost(request.url), server, keys) + ) + ) + ) + ) request.signal.addEventListener("abort", () => { runFork(fiber.interruptAsFork(Error.clientAbortFiberId)) }, { once: true }) @@ -124,7 +131,7 @@ const makeResponse = ( status?: number statusText?: string } = { - headers: new globalThis.Headers(response.headers), + headers: new globalThis.Headers(Headers.unredact(response.headers)), status: response.status } @@ -216,6 +223,7 @@ class ServerRequestImpl extends Inspectable.Class implements ServerRequest.HttpS public resolve: (response: Response) => void, readonly url: string, private bunServer: BunServer, + private redactedKeys: string | RegExp | ReadonlyArray, public headersOverride?: Headers.Headers, private remoteAddressOverride?: string ) { @@ -242,6 +250,7 @@ class ServerRequestImpl extends Inspectable.Class implements ServerRequest.HttpS this.resolve, options.url ?? this.url, this.bunServer, + this.redactedKeys, options.headers ?? this.headersOverride, options.remoteAddress ?? this.remoteAddressOverride ) @@ -256,7 +265,7 @@ class ServerRequestImpl extends Inspectable.Class implements ServerRequest.HttpS return this.remoteAddressOverride ? Option.some(this.remoteAddressOverride) : Option.none() } get headers(): Headers.Headers { - this.headersOverride ??= Headers.fromInput(this.source.headers) + this.headersOverride ??= Headers.fromInput(this.source.headers, this.redactedKeys) return this.headersOverride } @@ -265,7 +274,7 @@ class ServerRequestImpl extends Inspectable.Class implements ServerRequest.HttpS if (this.cachedCookies) { return this.cachedCookies } - return this.cachedCookies = Cookies.parseHeader(this.headers.cookie ?? "") + return this.cachedCookies = Cookies.parseHeader(Headers.unredactHeader(this.headers.cookie) ?? "") } get stream(): Stream.Stream { @@ -345,13 +354,13 @@ class ServerRequestImpl extends Inspectable.Class implements ServerRequest.HttpS return this.multipartEffect } this.multipartEffect = Effect.runSync(Effect.cached( - MultipartNode.persisted(Readable.fromWeb(this.source.body! as any), this.headers) + MultipartNode.persisted(Readable.fromWeb(this.source.body! as any), Headers.unredact(this.headers)) )) return this.multipartEffect } get multipartStream(): Stream.Stream { - return MultipartNode.stream(Readable.fromWeb(this.source.body! as any), this.headers) + return MultipartNode.stream(Readable.fromWeb(this.source.body! as any), Headers.unredact(this.headers)) } private arrayBufferEffect: Effect.Effect | undefined diff --git a/packages/platform-node/src/internal/httpClient.ts b/packages/platform-node/src/internal/httpClient.ts index b9da0d7b874..61d921798e9 100644 --- a/packages/platform-node/src/internal/httpClient.ts +++ b/packages/platform-node/src/internal/httpClient.ts @@ -1,4 +1,5 @@ import * as Cookies from "@effect/platform/Cookies" +import * as Headers from "@effect/platform/Headers" import type * as Body from "@effect/platform/HttpBody" import * as Client from "@effect/platform/HttpClient" import * as Error from "@effect/platform/HttpClientError" @@ -7,6 +8,7 @@ import * as ClientResponse from "@effect/platform/HttpClientResponse" import * as IncomingMessage from "@effect/platform/HttpIncomingMessage" import * as Context from "effect/Context" import * as Effect from "effect/Effect" +import * as FiberRef from "effect/FiberRef" import { pipe } from "effect/Function" import * as Layer from "effect/Layer" import type * as Scope from "effect/Scope" @@ -60,20 +62,24 @@ const fromAgent = (agent: NodeClient.HttpAgent): Client.HttpClient => Https.request(url, { agent: agent.https, method: request.method, - headers: request.headers, + headers: Headers.unredact(request.headers), signal }) : Http.request(url, { agent: agent.http, method: request.method, - headers: request.headers, + headers: Headers.unredact(request.headers), signal }) return pipe( Effect.zipRight(sendBody(nodeRequest, request, request.body), waitForResponse(nodeRequest, request), { concurrent: true }), - Effect.map((_) => new ClientResponseImpl(request, _)) + Effect.flatMap((_) => + FiberRef.get(Headers.currentRedactedNames).pipe( + Effect.map((redactedKeys) => new ClientResponseImpl(request, _, redactedKeys)) + ) + ) ) }) @@ -188,7 +194,8 @@ class ClientResponseImpl extends HttpIncomingMessageImpl constructor( readonly request: ClientRequest.HttpClientRequest, - source: Http.IncomingMessage + source: Http.IncomingMessage, + redactedKeys: string | RegExp | ReadonlyArray ) { super(source, (cause) => new Error.ResponseError({ @@ -196,7 +203,7 @@ class ClientResponseImpl extends HttpIncomingMessageImpl response: this, reason: "Decode", cause - })) + }), redactedKeys) this[ClientResponse.TypeId] = ClientResponse.TypeId } diff --git a/packages/platform-node/src/internal/httpClientUndici.ts b/packages/platform-node/src/internal/httpClientUndici.ts index 2a86be633ab..a80faf5f74e 100644 --- a/packages/platform-node/src/internal/httpClientUndici.ts +++ b/packages/platform-node/src/internal/httpClientUndici.ts @@ -52,7 +52,7 @@ export const make = (dispatcher: Undici.Dispatcher): Client.HttpClient => ...options, signal, method: request.method, - headers: request.headers, + headers: Headers.unredact(request.headers), origin: url.origin, path: url.pathname + url.search + url.hash, body, @@ -69,7 +69,11 @@ export const make = (dispatcher: Undici.Dispatcher): Client.HttpClient => }) }) ), - Effect.map((response) => new ClientResponseImpl(request, response)) + Effect.flatMap((response) => + FiberRef.get(Headers.currentRedactedNames).pipe( + Effect.map((redactedKeys) => new ClientResponseImpl(request, response, redactedKeys)) + ) + ) ) }) @@ -101,7 +105,8 @@ class ClientResponseImpl extends Inspectable.Class implements ClientResponse.Htt constructor( readonly request: ClientRequest.HttpClientRequest, - readonly source: Undici.Dispatcher.ResponseData + readonly source: Undici.Dispatcher.ResponseData, + protected redactedKeys: string | RegExp | ReadonlyArray ) { super() this[IncomingMessage.TypeId] = IncomingMessage.TypeId @@ -130,7 +135,7 @@ class ClientResponseImpl extends Inspectable.Class implements ClientResponse.Htt } get headers(): Headers.Headers { - return Headers.fromInput(this.source.headers) + return Headers.fromInput(this.source.headers, this.redactedKeys) } get remoteAddress(): Option.Option { diff --git a/packages/platform-node/src/internal/httpIncomingMessage.ts b/packages/platform-node/src/internal/httpIncomingMessage.ts index e89a78032aa..4ed8140181f 100644 --- a/packages/platform-node/src/internal/httpIncomingMessage.ts +++ b/packages/platform-node/src/internal/httpIncomingMessage.ts @@ -18,6 +18,7 @@ export abstract class HttpIncomingMessageImpl extends Inspectable.Class constructor( readonly source: Http.IncomingMessage, readonly onError: (error: unknown) => E, + protected readonly redactedKeys: string | RegExp | ReadonlyArray, readonly remoteAddressOverride?: string ) { super() @@ -25,7 +26,7 @@ export abstract class HttpIncomingMessageImpl extends Inspectable.Class } get headers() { - return Headers.fromInput(this.source.headers as any) + return Headers.fromInput(this.source.headers as any, this.redactedKeys) } get remoteAddress() { diff --git a/packages/platform-node/src/internal/httpPlatform.ts b/packages/platform-node/src/internal/httpPlatform.ts index 58321bdc5c4..46a0a76eeba 100644 --- a/packages/platform-node/src/internal/httpPlatform.ts +++ b/packages/platform-node/src/internal/httpPlatform.ts @@ -28,7 +28,8 @@ export const make = Platform.make({ headers: Headers.merge( headers, Headers.unsafeFromRecord({ - "content-type": headers["content-type"] ?? Mime.getType(file.name) ?? "application/octet-stream", + "content-type": Headers.unredactHeader(headers["content-type"]) ?? Mime.getType(file.name) ?? + "application/octet-stream", "content-length": file.size.toString() }) ), diff --git a/packages/platform-node/src/internal/httpServer.ts b/packages/platform-node/src/internal/httpServer.ts index 13a966397f6..106350faf52 100644 --- a/packages/platform-node/src/internal/httpServer.ts +++ b/packages/platform-node/src/internal/httpServer.ts @@ -2,7 +2,7 @@ import * as MultipartNode from "@effect/platform-node-shared/NodeMultipart" import * as Cookies from "@effect/platform/Cookies" import * as Etag from "@effect/platform/Etag" import * as FileSystem from "@effect/platform/FileSystem" -import type * as Headers from "@effect/platform/Headers" +import * as Headers from "@effect/platform/Headers" import * as App from "@effect/platform/HttpApp" import * as IncomingMessage from "@effect/platform/HttpIncomingMessage" import type { HttpMethod } from "@effect/platform/HttpMethod" @@ -17,6 +17,7 @@ import * as Socket from "@effect/platform/Socket" import type * as Cause from "effect/Cause" import * as Config from "effect/Config" import * as Effect from "effect/Effect" +import * as FiberRef from "effect/FiberRef" import * as FiberSet from "effect/FiberSet" import { type LazyArg } from "effect/Function" import * as Layer from "effect/Layer" @@ -140,10 +141,14 @@ export const makeHandler: { nodeResponse: Http.ServerResponse ) { const fiber = runFork( - Effect.provideService( - handledApp, - ServerRequest.HttpServerRequest, - new ServerRequestImpl(nodeRequest, nodeResponse) + FiberRef.get(Headers.currentRedactedNames).pipe( + Effect.flatMap((keys) => + Effect.provideService( + handledApp, + ServerRequest.HttpServerRequest, + new ServerRequestImpl(nodeRequest, nodeResponse, keys) + ) + ) ) ) nodeResponse.on("close", () => { @@ -188,10 +193,14 @@ export const makeUpgradeHandler = ( ) )) const fiber = runFork( - Effect.provideService( - handledApp, - ServerRequest.HttpServerRequest, - new ServerRequestImpl(nodeRequest, nodeResponse, upgradeEffect) + FiberRef.get(Headers.currentRedactedNames).pipe( + Effect.flatMap((keys) => + Effect.provideService( + handledApp, + ServerRequest.HttpServerRequest, + new ServerRequestImpl(nodeRequest, nodeResponse, keys, upgradeEffect) + ) + ) ) ) socket.on("close", () => { @@ -208,17 +217,23 @@ class ServerRequestImpl extends HttpIncomingMessageImpl impl constructor( readonly source: Http.IncomingMessage, readonly response: Http.ServerResponse | LazyArg, + redactedKeys: string | RegExp | ReadonlyArray, private upgradeEffect?: Effect.Effect, readonly url = source.url!, private headersOverride?: Headers.Headers, remoteAddressOverride?: string ) { - super(source, (cause) => - new Error.RequestError({ - request: this, - reason: "Decode", - cause - }), remoteAddressOverride) + super( + source, + (cause) => + new Error.RequestError({ + request: this, + reason: "Decode", + cause + }), + redactedKeys, + remoteAddressOverride + ) this[ServerRequest.TypeId] = ServerRequest.TypeId } @@ -227,7 +242,7 @@ class ServerRequestImpl extends HttpIncomingMessageImpl impl if (this.cachedCookies) { return this.cachedCookies } - return this.cachedCookies = Cookies.parseHeader(this.headers.cookie ?? "") + return this.cachedCookies = Cookies.parseHeader(Headers.unredactHeader(this.headers.cookie) ?? "") } get resolvedResponse(): Http.ServerResponse { @@ -244,6 +259,7 @@ class ServerRequestImpl extends HttpIncomingMessageImpl impl return new ServerRequestImpl( this.source, this.response, + this.redactedKeys, this.upgradeEffect, options.url ?? this.url, options.headers ?? this.headersOverride, @@ -359,7 +375,7 @@ const handleResponse = (request: ServerRequest.HttpServerRequest, response: Serv return Effect.void } - let headers: Record> = response.headers + let headers: Record> = Headers.unredact(response.headers) if (!Cookies.isEmpty(response.cookies)) { headers = { ...headers } const toSet = Cookies.toSetCookieHeaders(response.cookies) diff --git a/packages/platform/src/Headers.ts b/packages/platform/src/Headers.ts index 844c751552c..fc61f476f81 100644 --- a/packages/platform/src/Headers.ts +++ b/packages/platform/src/Headers.ts @@ -2,6 +2,8 @@ * @since 1.0.0 */ import * as Schema from "@effect/schema/Schema" +import * as Effect from "effect/Effect" +import * as Equivalence from "effect/Equivalence" import * as FiberRef from "effect/FiberRef" import { dual, identity } from "effect/Function" import { globalValue } from "effect/GlobalValue" @@ -30,40 +32,69 @@ export type HeadersTypeId = typeof HeadersTypeId */ export const isHeaders = (u: unknown): u is Headers => Predicate.hasProperty(u, HeadersTypeId) +/** + * @since 1.0.0 + * @category models + */ +export type RedactedKeys = string | RegExp | ReadonlyArray + /** * @since 1.0.0 * @category models */ export interface Headers { readonly [HeadersTypeId]: HeadersTypeId - readonly [key: string]: string + readonly [key: string]: HeaderValues } const Proto = Object.assign(Object.create(null), { [HeadersTypeId]: HeadersTypeId }) -const make = (input: Record.ReadonlyRecord): Mutable => +const make = (input: Record.ReadonlyRecord): Mutable => Object.assign(Object.create(Proto), input) as Headers +const equiv = Redacted.getEquivalence(String.Equivalence) + /** * @since 1.0.0 * @category schemas */ export const schemaFromSelf: Schema.Schema = Schema.declare(isHeaders, { identifier: "Headers", - equivalence: () => Record.getEquivalence(String.Equivalence) + equivalence: () => + Record.getEquivalence( + Equivalence.make>((self, that) => + Redacted.isRedacted(self) + ? Redacted.isRedacted(that) && equiv(self, that) + : String.isString(that) && String.Equivalence(self, that) + ) + ) }) +const HeaderValues = Schema.Union(Schema.String, Schema.Redacted(Schema.String)) +type HeaderValues = typeof HeaderValues.Type + /** * @since 1.0.0 * @category schemas */ -export const schema: Schema.Schema>> = Schema - .transform( +export const schema: Schema.Schema< + Headers, + Record.ReadonlyRecord< + string, + string | ReadonlyArray + > +> = Schema + .transformOrFail( Schema.Record({ key: Schema.String, value: Schema.Union(Schema.String, Schema.Array(Schema.String)) }), schemaFromSelf, - { strict: true, decode: (record) => fromInput(record), encode: identity } + { + strict: true, + decode: (record) => + FiberRef.get(currentRedactedNames).pipe(Effect.map((redactedKeys) => fromInput(record, redactedKeys))), + encode: (_) => Effect.succeed(unredact(_)) + } ) /** @@ -71,8 +102,8 @@ export const schema: Schema.Schema | undefined> - | Iterable + | Record.ReadonlyRecord | undefined> + | Iterable /** * @since 1.0.0 @@ -80,29 +111,42 @@ export type Input = */ export const empty: Headers = Object.create(Proto) +// export const fromInputEffect: (input: Input | undefined) => Effect.Effect = (input) => +// currentRedactedNames.pipe(Effect.map((redactedKeys) => fromInput(input, redactedKeys))) + /** * @since 1.0.0 * @category constructors */ -export const fromInput: (input?: Input) => Headers = (input) => { +export const fromInput: ( + input: Input | undefined, + redactedKeys: RedactedKeys +) => Headers = ( + input, + redactedKey +) => { + const redact_ = redactedKey ? redact(redactedKey) : identity if (input === undefined) { return empty } else if (Symbol.iterator in input) { - const out: Record = Object.create(Proto) + const out: Record = Object.create(Proto) for (const [k, v] of input) { out[k.toLowerCase()] = v } - return out as Headers + return redact_(out) as Headers } - const out: Record = Object.create(Proto) + const out: Record = Object.create(Proto) for (const [k, v] of Object.entries(input)) { if (Array.isArray(v)) { - out[k.toLowerCase()] = v.join(", ") + const redacted = v.some((_) => Redacted.isRedacted(_)) + out[k.toLowerCase()] = redacted + ? Redacted.make(v.map((_) => Redacted.isRedacted(_) ? Redacted.value(_) : _).join(", ")) + : v.join(", ") } else if (v !== undefined) { out[k.toLowerCase()] = v as string } } - return out as Headers + return redact_(out) as Headers } /** @@ -141,11 +185,11 @@ export const get: { * @category combinators */ export const set: { - (key: string, value: string): (self: Headers) => Headers - (self: Headers, key: string, value: string): Headers + (key: string, value: HeaderValues): (self: Headers) => Headers + (self: Headers, key: string, value: HeaderValues): Headers } = dual< - (key: string, value: string) => (self: Headers) => Headers, - (self: Headers, key: string, value: string) => Headers + (key: string, value: HeaderValues) => (self: Headers) => Headers, + (self: Headers, key: string, value: HeaderValues) => Headers >(3, (self, key, value) => { const out = make(self) out[key.toLowerCase()] = value @@ -157,15 +201,15 @@ export const set: { * @category combinators */ export const setAll: { - (headers: Input): (self: Headers) => Headers - (self: Headers, headers: Input): Headers + (headers: Input, redactedKeys: RedactedKeys): (self: Headers) => Headers + (self: Headers, headers: Input, redactedKeys: RedactedKeys): Headers } = dual< - (headers: Input) => (self: Headers) => Headers, - (self: Headers, headers: Input) => Headers ->(2, (self, headers) => + (headers: Input, redactedKeys: RedactedKeys) => (self: Headers) => Headers, + (self: Headers, headers: Input, redactedKeys: RedactedKeys) => Headers +>(3, (self, headers, redactedKeys) => make({ ...self, - ...fromInput(headers) + ...fromInput(headers, redactedKeys) })) /** @@ -200,35 +244,53 @@ export const remove: { return out }) +export const unredactHeader: { + (self: string | Redacted.Redacted): string + (self: string | Redacted.Redacted | undefined): string | undefined +} = (self) => (self !== undefined ? Redacted.isRedacted(self) ? Redacted.value(self) : self : undefined) as any + +/** + * @since 1.0.0 + * @category combinators + */ +export const unredact = (self: Headers): Record => { + const out: Record = {} + for (const name in self) { + out[name] = unredactHeader(self[name]) + } + + return out +} + /** * @since 1.0.0 * @category combinators */ export const redact: { ( - key: string | RegExp | ReadonlyArray - ): (self: Headers) => Record + key: RedactedKeys + ): (self: Record>) => Record ( - self: Headers, - key: string | RegExp | ReadonlyArray + self: Record>, + key: RedactedKeys ): Record } = dual( 2, ( - self: Headers, - key: string | RegExp | ReadonlyArray + self: Record>, + key: RedactedKeys ): Record => { const out: Record = { ...self } const modify = (key: string | RegExp) => { if (typeof key === "string") { const k = key.toLowerCase() if (k in self) { - out[k] = Redacted.make(self[k]) + out[k] = Redacted.isRedacted(self[k]) ? self[k] : Redacted.make(self[k]) } } else { for (const name in self) { if (key.test(name)) { - out[name] = Redacted.make(self[name]) + out[name] = Redacted.isRedacted(self[name]) ? self[name] : Redacted.make(self[name]) } } } diff --git a/packages/platform/src/HttpApiBuilder.ts b/packages/platform/src/HttpApiBuilder.ts index 1ead6c5cad8..d51022cba8a 100644 --- a/packages/platform/src/HttpApiBuilder.ts +++ b/packages/platform/src/HttpApiBuilder.ts @@ -22,6 +22,7 @@ import type { Covariant, Mutable, NoInfer } from "effect/Types" import { unify } from "effect/Unify" import type { Cookie } from "./Cookies.js" import type { FileSystem } from "./FileSystem.js" +import { unredactHeader } from "./Headers.js" import * as HttpApi from "./HttpApi.js" import * as HttpApiEndpoint from "./HttpApiEndpoint.js" import { HttpApiDecodeError } from "./HttpApiError.js" @@ -606,7 +607,7 @@ export const securityDecode = case "Bearer": { return Effect.map( HttpServerRequest.HttpServerRequest, - (request) => Redacted.make((request.headers.authorization ?? "").slice(bearerLen)) as any + (request) => Redacted.make((unredactHeader(request.headers.authorization) ?? "").slice(bearerLen)) as any ) } case "ApiKey": { @@ -631,7 +632,7 @@ export const securityDecode = password: Redacted.make("") } as any return HttpServerRequest.HttpServerRequest.pipe( - Effect.flatMap((request) => Encoding.decodeBase64String(request.headers.authorization ?? "")), + Effect.flatMap((request) => Encoding.decodeBase64String(unredactHeader(request.headers.authorization) ?? "")), Effect.match({ onFailure: () => empty, onSuccess: (header) => { diff --git a/packages/platform/src/HttpApiClient.ts b/packages/platform/src/HttpApiClient.ts index ee98f97d48d..41b36500262 100644 --- a/packages/platform/src/HttpApiClient.ts +++ b/packages/platform/src/HttpApiClient.ts @@ -8,6 +8,7 @@ import * as Effect from "effect/Effect" import { identity } from "effect/Function" import * as Option from "effect/Option" import type { Simplify } from "effect/Types" +import { currentRedactedNames } from "./Headers.js" import * as HttpApi from "./HttpApi.js" import type { HttpApiEndpoint } from "./HttpApiEndpoint.js" import type { HttpApiGroup } from "./HttpApiGroup.js" @@ -181,7 +182,11 @@ export const make = ( ? Effect.flatMap((httpRequest) => encodeHeaders.value(request.headers).pipe( Effect.orDie, - Effect.map((headers) => HttpClientRequest.setHeaders(httpRequest, headers as any)) + Effect.flatMap((headers) => + currentRedactedNames.pipe(Effect.map((redactedKeys) => + HttpClientRequest.setHeaders(httpRequest, headers as any, redactedKeys) + )) + ) ) ) : identity, diff --git a/packages/platform/src/HttpApp.ts b/packages/platform/src/HttpApp.ts index 2951d196fd1..36464d2904c 100644 --- a/packages/platform/src/HttpApp.ts +++ b/packages/platform/src/HttpApp.ts @@ -11,6 +11,7 @@ import * as Layer from "effect/Layer" import * as Option from "effect/Option" import * as Runtime from "effect/Runtime" import * as Scope from "effect/Scope" +import { currentRedactedNames } from "./Headers.js" import type { HttpMiddleware } from "./HttpMiddleware.js" import * as ServerError from "./HttpServerError.js" import * as ServerRequest from "./HttpServerRequest.js" @@ -160,13 +161,13 @@ export const toWebHandlerRuntime = (runtime: Runtime.Runtime) => { return (self: Default, middleware?: HttpMiddleware | undefined) => (request: Request): Promise => new Promise((resolve) => { - const fiber = run(Effect.provideService( + const fiber = run(Effect.provideServiceEffect( toHandled(self, (request, response) => { resolve(ServerResponse.toWeb(response, { withoutBody: request.method === "HEAD", runtime })) return Effect.void }, middleware), ServerRequest.HttpServerRequest, - ServerRequest.fromWeb(request) + currentRedactedNames.pipe(Effect.map((redactedKeys) => ServerRequest.fromWeb(request, redactedKeys))) )) request.signal.addEventListener("abort", () => { fiber.unsafeInterruptAsFork(ServerError.clientAbortFiberId) diff --git a/packages/platform/src/HttpClientRequest.ts b/packages/platform/src/HttpClientRequest.ts index 68d511f51a9..aacdbbd7d7b 100644 --- a/packages/platform/src/HttpClientRequest.ts +++ b/packages/platform/src/HttpClientRequest.ts @@ -56,6 +56,7 @@ export interface Options { readonly body?: Body.HttpBody | undefined readonly accept?: string | undefined readonly acceptJson?: boolean | undefined + readonly redactedKeys?: Headers.RedactedKeys | undefined } /** @@ -160,8 +161,8 @@ export const setHeader: { * @category combinators */ export const setHeaders: { - (input: Headers.Input): (self: HttpClientRequest) => HttpClientRequest - (self: HttpClientRequest, input: Headers.Input): HttpClientRequest + (input: Headers.Input, redactedKeys: Headers.RedactedKeys): (self: HttpClientRequest) => HttpClientRequest + (self: HttpClientRequest, input: Headers.Input, redactedKeys: Headers.RedactedKeys): HttpClientRequest } = internal.setHeaders /** diff --git a/packages/platform/src/HttpClientResponse.ts b/packages/platform/src/HttpClientResponse.ts index 8455e36a824..12b18efa8fc 100644 --- a/packages/platform/src/HttpClientResponse.ts +++ b/packages/platform/src/HttpClientResponse.ts @@ -9,6 +9,7 @@ import type * as Scope from "effect/Scope" import type * as Stream from "effect/Stream" import type { Unify } from "effect/Unify" import type * as Cookies from "./Cookies.js" +import type { RedactedKeys } from "./Headers.js" import type * as Error from "./HttpClientError.js" import type * as ClientRequest from "./HttpClientRequest.js" import type * as IncomingMessage from "./HttpIncomingMessage.js" @@ -60,8 +61,11 @@ export interface HttpClientResponse extends IncomingMessage.HttpIncomingMessage< * @since 1.0.0 * @category constructors */ -export const fromWeb: (request: ClientRequest.HttpClientRequest, source: Response) => HttpClientResponse = - internal.fromWeb +export const fromWeb: ( + request: ClientRequest.HttpClientRequest, + source: Response, + redactedKeys: RedactedKeys +) => HttpClientResponse = internal.fromWeb /** * @since 1.0.0 diff --git a/packages/platform/src/HttpIncomingMessage.ts b/packages/platform/src/HttpIncomingMessage.ts index e217a94ebc0..d081c877a2f 100644 --- a/packages/platform/src/HttpIncomingMessage.ts +++ b/packages/platform/src/HttpIncomingMessage.ts @@ -12,7 +12,7 @@ import type { Inspectable } from "effect/Inspectable" import * as Option from "effect/Option" import type * as Stream from "effect/Stream" import * as FileSystem from "./FileSystem.js" -import type * as Headers from "./Headers.js" +import * as Headers from "./Headers.js" import type * as UrlParams from "./UrlParams.js" /** @@ -99,7 +99,7 @@ export const withMaxBodySize = dual< * @since 1.0.0 */ export const inspect = (self: HttpIncomingMessage, that: object): object => { - const contentType = self.headers["content-type"] ?? "" + const contentType = Headers.unredactHeader(self.headers["content-type"]) ?? "" let body: unknown if (contentType.includes("application/json")) { try { diff --git a/packages/platform/src/HttpServerRequest.ts b/packages/platform/src/HttpServerRequest.ts index 3944778ca25..a22ff1c4e22 100644 --- a/packages/platform/src/HttpServerRequest.ts +++ b/packages/platform/src/HttpServerRequest.ts @@ -220,7 +220,7 @@ export const schemaBodyFormJson: ( * @since 1.0.0 * @category conversions */ -export const fromWeb: (request: Request) => HttpServerRequest = internal.fromWeb +export const fromWeb: (request: Request, redactedKeys: Headers.RedactedKeys) => HttpServerRequest = internal.fromWeb /** * @since 1.0.0 diff --git a/packages/platform/src/HttpServerResponse.ts b/packages/platform/src/HttpServerResponse.ts index 3a2ed00f8f2..ead50de82d2 100644 --- a/packages/platform/src/HttpServerResponse.ts +++ b/packages/platform/src/HttpServerResponse.ts @@ -205,8 +205,8 @@ export const setHeader: { * @category combinators */ export const setHeaders: { - (input: Headers.Input): (self: HttpServerResponse) => HttpServerResponse - (self: HttpServerResponse, input: Headers.Input): HttpServerResponse + (input: Headers.Input, redactedKeys: Headers.RedactedKeys): (self: HttpServerResponse) => HttpServerResponse + (self: HttpServerResponse, input: Headers.Input, redactedKeys: Headers.RedactedKeys): HttpServerResponse } = internal.setHeaders /** diff --git a/packages/platform/src/HttpTraceContext.ts b/packages/platform/src/HttpTraceContext.ts index c041d9f19f0..18f0758df80 100644 --- a/packages/platform/src/HttpTraceContext.ts +++ b/packages/platform/src/HttpTraceContext.ts @@ -49,7 +49,7 @@ export const b3: FromHeaders = (headers) => { if (!("b3" in headers)) { return Option.none() } - const parts = headers["b3"].split("-") + const parts = Headers.unredactHeader(headers["b3"])!.split("-") if (parts.length < 2) { return Option.none() } @@ -69,9 +69,9 @@ export const xb3: FromHeaders = (headers) => { return Option.none() } return Option.some(Tracer.externalSpan({ - traceId: headers["x-b3-traceid"], - spanId: headers["x-b3-spanid"], - sampled: headers["x-b3-sampled"] ? headers["x-b3-sampled"] === "1" : true + traceId: Headers.unredactHeader(headers["x-b3-traceid"]), + spanId: Headers.unredactHeader(headers["x-b3-spanid"]), + sampled: headers["x-b3-sampled"] ? Headers.unredactHeader(headers["x-b3-sampled"]) === "1" : true })) } @@ -86,7 +86,7 @@ export const w3c: FromHeaders = (headers) => { if (!(headers["traceparent"])) { return Option.none() } - const parts = headers["traceparent"].split("-") + const parts = Headers.unredactHeader(headers["traceparent"]).split("-") if (parts.length !== 4) { return Option.none() } diff --git a/packages/platform/src/internal/fetchHttpClient.ts b/packages/platform/src/internal/fetchHttpClient.ts index 1438c3f51a6..3f1107ef77a 100644 --- a/packages/platform/src/internal/fetchHttpClient.ts +++ b/packages/platform/src/internal/fetchHttpClient.ts @@ -1,6 +1,7 @@ import * as Effect from "effect/Effect" import * as FiberRef from "effect/FiberRef" import * as Stream from "effect/Stream" +import * as Headers from "../Headers.js" import type * as Client from "../HttpClient.js" import * as Error from "../HttpClientError.js" import * as client from "./httpClient.js" @@ -15,9 +16,9 @@ const fetch: Client.HttpClient = client.make((request, url, signal, fiber) => { const context = fiber.getFiberRef(FiberRef.currentContext) const fetch: typeof globalThis.fetch = context.unsafeMap.get(fetchTagKey) ?? globalThis.fetch const options: RequestInit = context.unsafeMap.get(requestInitTagKey) ?? {} - const headers = new globalThis.Headers(request.headers) + const headers = new globalThis.Headers(Headers.unredact(request.headers)) const send = (body: BodyInit | undefined) => - Effect.map( + Effect.flatMap( Effect.tryPromise({ try: () => fetch(url, { @@ -35,7 +36,10 @@ const fetch: Client.HttpClient = client.make((request, url, signal, fiber) => { cause }) }), - (response) => internalResponse.fromWeb(request, response) + (response) => + FiberRef.get(Headers.currentRedactedNames).pipe( + Effect.map((redactedKeys) => internalResponse.fromWeb(request, response, redactedKeys)) + ) ) switch (request.body._tag) { case "Raw": diff --git a/packages/platform/src/internal/httpClient.ts b/packages/platform/src/internal/httpClient.ts index b171986c19c..f0ced40bc05 100644 --- a/packages/platform/src/internal/httpClient.ts +++ b/packages/platform/src/internal/httpClient.ts @@ -1,3 +1,4 @@ +import { Redacted } from "effect" import * as Context from "effect/Context" import * as Effect from "effect/Effect" import type * as Fiber from "effect/Fiber" @@ -173,7 +174,7 @@ export const make = ( span.attribute(`http.request.header.${name}`, String(redactedHeaders[name])) } request = fiber.getFiberRef(currentTracerPropagation) - ? internalRequest.setHeaders(request, TraceContext.toHeaders(span)) + ? internalRequest.setHeaders(request, TraceContext.toHeaders(span), redactedHeaderNames) : request return Effect.tap( Effect.withParentSpan( @@ -688,7 +689,9 @@ export const followRedirects = dual< ? loop( internalRequest.setUrl( request, - response.headers.location + Redacted.isRedacted(response.headers.location) + ? Redacted.value(response.headers.location) + : response.headers.location ), redirects + 1 ) diff --git a/packages/platform/src/internal/httpClientRequest.ts b/packages/platform/src/internal/httpClientRequest.ts index 7f5aa36565b..925691c39ac 100644 --- a/packages/platform/src/internal/httpClientRequest.ts +++ b/packages/platform/src/internal/httpClientRequest.ts @@ -117,7 +117,7 @@ export const modify = dual< result = setUrl(result, options.url) } if (options.headers) { - result = setHeaders(result, options.headers) + result = setHeaders(result, options.headers, options.redactedKeys ?? []) } if (options.urlParams) { result = setUrlParams(result, options.urlParams) @@ -154,15 +154,22 @@ export const setHeader = dual< /** @internal */ export const setHeaders = dual< - (input: Headers.Input) => (self: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest, - (self: ClientRequest.HttpClientRequest, input: Headers.Input) => ClientRequest.HttpClientRequest ->(2, (self, input) => + ( + input: Headers.Input, + redactedKeys: Headers.RedactedKeys + ) => (self: ClientRequest.HttpClientRequest) => ClientRequest.HttpClientRequest, + ( + self: ClientRequest.HttpClientRequest, + input: Headers.Input, + redactedKeys: Headers.RedactedKeys + ) => ClientRequest.HttpClientRequest +>(3, (self, input, redactedKeys) => makeInternal( self.method, self.url, self.urlParams, self.hash, - Headers.setAll(self.headers, input), + Headers.setAll(self.headers, input, redactedKeys), self.body )) diff --git a/packages/platform/src/internal/httpClientResponse.ts b/packages/platform/src/internal/httpClientResponse.ts index 540ffd90e69..88d9fcaa215 100644 --- a/packages/platform/src/internal/httpClientResponse.ts +++ b/packages/platform/src/internal/httpClientResponse.ts @@ -21,8 +21,9 @@ export const TypeId: ClientResponse.TypeId = Symbol.for("@effect/platform/HttpCl /** @internal */ export const fromWeb = ( request: ClientRequest.HttpClientRequest, - source: globalThis.Response -): ClientResponse.HttpClientResponse => new ClientResponseImpl(request, source) + source: globalThis.Response, + redactedKeys: string | RegExp | ReadonlyArray +): ClientResponse.HttpClientResponse => new ClientResponseImpl(request, source, redactedKeys) class ClientResponseImpl extends Inspectable.Class implements ClientResponse.HttpClientResponse { readonly [IncomingMessage.TypeId]: IncomingMessage.TypeId @@ -30,7 +31,8 @@ class ClientResponseImpl extends Inspectable.Class implements ClientResponse.Htt constructor( readonly request: ClientRequest.HttpClientRequest, - private readonly source: globalThis.Response + private readonly source: globalThis.Response, + protected redactedKeys: string | RegExp | ReadonlyArray ) { super() this[IncomingMessage.TypeId] = IncomingMessage.TypeId @@ -50,7 +52,7 @@ class ClientResponseImpl extends Inspectable.Class implements ClientResponse.Htt } get headers(): Headers.Headers { - return Headers.fromInput(this.source.headers) + return Headers.fromInput(this.source.headers, this.redactedKeys) } cachedCookies?: Cookies.Cookies diff --git a/packages/platform/src/internal/httpMiddleware.ts b/packages/platform/src/internal/httpMiddleware.ts index 00282e0dae3..ec22adc73eb 100644 --- a/packages/platform/src/internal/httpMiddleware.ts +++ b/packages/platform/src/internal/httpMiddleware.ts @@ -177,7 +177,7 @@ export const xForwardedHeaders = make((httpApp) => "host", request.headers["x-forwarded-host"] ), - remoteAddress: request.headers["x-forwarded-for"]?.split(",")[0].trim() + remoteAddress: Headers.unredactHeader(request.headers["x-forwarded-for"])?.split(",")[0].trim() }) : request) ) @@ -283,8 +283,9 @@ export const cors = (options?: { context, ServerRequest.HttpServerRequest ) - const origin = request.headers["origin"] - const accessControlRequestHeaders = request.headers["access-control-request-headers"] + const redactedKeys = fiber.getFiberRef(Headers.currentRedactedNames) + const origin = Headers.unredactHeader(request.headers["origin"])! + const accessControlRequestHeaders = Headers.unredactHeader(request.headers["access-control-request-headers"]) const corsHeaders = Headers.unsafeFromRecord({ ...allowOrigin(origin), ...allowCredentials, @@ -298,6 +299,6 @@ export const cors = (options?: { }) return Effect.succeed(ServerResponse.empty({ status: 204, headers: corsHeaders })) } - return Effect.map(httpApp, ServerResponse.setHeaders(corsHeaders)) + return Effect.map(httpApp, ServerResponse.setHeaders(corsHeaders, redactedKeys)) }) } diff --git a/packages/platform/src/internal/httpMultiplex.ts b/packages/platform/src/internal/httpMultiplex.ts index 3c082766a99..7c5d9c9e03a 100644 --- a/packages/platform/src/internal/httpMultiplex.ts +++ b/packages/platform/src/internal/httpMultiplex.ts @@ -3,6 +3,7 @@ import * as Effect from "effect/Effect" import * as Effectable from "effect/Effectable" import { dual } from "effect/Function" import * as Inspectable from "effect/Inspectable" +import * as Headers from "../Headers.js" import type * as App from "../HttpApp.js" import type * as Multiplex from "../HttpMultiplex.js" import * as Error from "../HttpServerError.js" @@ -138,7 +139,7 @@ export const headerRegex = dual< (self, header, regex, app) => add(self, (req) => req.headers[header] !== undefined - ? Effect.succeed(regex.test(req.headers[header])) + ? Effect.succeed(regex.test(Headers.unredactHeader(req.headers[header]))) : Effect.succeed(false), app) ) @@ -160,7 +161,7 @@ export const headerStartsWith = dual< (self, header, prefix, app) => add(self, (req) => req.headers[header] !== undefined - ? Effect.succeed(req.headers[header].startsWith(prefix)) + ? Effect.succeed(Headers.unredactHeader(req.headers[header]).startsWith(prefix)) : Effect.succeed(false), app) ) @@ -182,7 +183,7 @@ export const headerEndsWith = dual< (self, header, suffix, app) => add(self, (req) => req.headers[header] !== undefined - ? Effect.succeed(req.headers[header].endsWith(suffix)) + ? Effect.succeed(Headers.unredactHeader(req.headers[header])!.endsWith(suffix)) : Effect.succeed(false), app) ) diff --git a/packages/platform/src/internal/httpServerRequest.ts b/packages/platform/src/internal/httpServerRequest.ts index 78e25531c17..6e632a814b6 100644 --- a/packages/platform/src/internal/httpServerRequest.ts +++ b/packages/platform/src/internal/httpServerRequest.ts @@ -100,7 +100,7 @@ export const schemaBodyJson = (schema: Schema.Schema, options? } const isMultipart = (request: ServerRequest.HttpServerRequest) => - request.headers["content-type"]?.toLowerCase().includes("multipart/form-data") + Headers.unredactHeader(request.headers["content-type"])?.toLowerCase().includes("multipart/form-data") /** @internal */ export const schemaBodyForm = , R>( @@ -170,8 +170,10 @@ export const schemaBodyFormJson = (schema: Schema.Schema, opti } /** @internal */ -export const fromWeb = (request: globalThis.Request): ServerRequest.HttpServerRequest => - new ServerRequestImpl(request, removeHost(request.url)) +export const fromWeb = ( + request: globalThis.Request, + redactedKeys: string | RegExp | ReadonlyArray +): ServerRequest.HttpServerRequest => new ServerRequestImpl(request, removeHost(request.url), redactedKeys) const removeHost = (url: string) => { if (url[0] === "/") { @@ -187,6 +189,7 @@ class ServerRequestImpl extends Inspectable.Class implements ServerRequest.HttpS constructor( readonly source: Request, readonly url: string, + private readonly redactedKeys: string | RegExp | ReadonlyArray, public headersOverride?: Headers.Headers, private remoteAddressOverride?: string ) { @@ -211,6 +214,7 @@ class ServerRequestImpl extends Inspectable.Class implements ServerRequest.HttpS return new ServerRequestImpl( this.source, options.url ?? this.url, + this.redactedKeys, options.headers ?? this.headersOverride, options.remoteAddress ?? this.remoteAddressOverride ) @@ -225,7 +229,7 @@ class ServerRequestImpl extends Inspectable.Class implements ServerRequest.HttpS return this.remoteAddressOverride ? Option.some(this.remoteAddressOverride) : Option.none() } get headers(): Headers.Headers { - this.headersOverride ??= Headers.fromInput(this.source.headers) + this.headersOverride ??= Headers.fromInput(this.source.headers, this.redactedKeys) return this.headersOverride } @@ -234,7 +238,9 @@ class ServerRequestImpl extends Inspectable.Class implements ServerRequest.HttpS if (this.cachedCookies) { return this.cachedCookies } - return this.cachedCookies = Cookies.parseHeader(this.headers.cookie ?? "") + return this.cachedCookies = Cookies.parseHeader( + Headers.unredactHeader(this.headers.cookie) ?? "" + ) } get stream(): Stream.Stream { @@ -322,7 +328,7 @@ class ServerRequestImpl extends Inspectable.Class implements ServerRequest.HttpS get multipartStream(): Stream.Stream { return Stream.pipeThroughChannel( Stream.mapError(this.stream, (cause) => new Multipart.MultipartError({ reason: "InternalError", cause })), - Multipart.makeChannel(this.headers) + Multipart.makeChannel(Headers.unredact(this.headers)) ) } diff --git a/packages/platform/src/internal/httpServerResponse.ts b/packages/platform/src/internal/httpServerResponse.ts index 6648e665559..cb1ff805e46 100644 --- a/packages/platform/src/internal/httpServerResponse.ts +++ b/packages/platform/src/internal/httpServerResponse.ts @@ -291,7 +291,7 @@ export const getContentType = (options?: ServerResponse.Options | undefined): st if (options?.contentType) { return options.contentType } else if (options?.headers) { - return options.headers["content-type"] + return Headers.unredactHeader(options.headers["content-type"]) } else { return } @@ -462,13 +462,20 @@ export const removeCookie = dual< /** @internal */ export const setHeaders = dual< - (input: Headers.Input) => (self: ServerResponse.HttpServerResponse) => ServerResponse.HttpServerResponse, - (self: ServerResponse.HttpServerResponse, input: Headers.Input) => ServerResponse.HttpServerResponse ->(2, (self, input) => + ( + input: Headers.Input, + redactedKeys: Headers.RedactedKeys + ) => (self: ServerResponse.HttpServerResponse) => ServerResponse.HttpServerResponse, + ( + self: ServerResponse.HttpServerResponse, + input: Headers.Input, + redactedKeys: Headers.RedactedKeys + ) => ServerResponse.HttpServerResponse +>(3, (self, input, redactedKeys) => new ServerResponseImpl( self.status, self.statusText, - Headers.setAll(self.headers, input), + Headers.setAll(self.headers, input, redactedKeys), self.cookies, self.body )) @@ -512,7 +519,7 @@ export const toWeb = (response: ServerResponse.HttpServerResponse, options?: { readonly withoutBody?: boolean | undefined readonly runtime?: Runtime.Runtime | undefined }): Response => { - const headers = new globalThis.Headers(response.headers) + const headers = new globalThis.Headers(Headers.unredact(response.headers)) if (!Cookies.isEmpty(response.cookies)) { const toAdd = Cookies.toSetCookieHeaders(response.cookies) for (const header of toAdd) { diff --git a/packages/platform/test/Headers.test.ts b/packages/platform/test/Headers.test.ts index 46d0e7e3290..591ebb79907 100644 --- a/packages/platform/test/Headers.test.ts +++ b/packages/platform/test/Headers.test.ts @@ -9,7 +9,7 @@ describe("Headers", () => { "Content-Type": "application/json", "Authorization": "Bearer some-token", "X-Api-Key": "some-key" - }) + }, []) const redacted = Headers.redact(headers, "Authorization") @@ -26,7 +26,7 @@ describe("Headers", () => { "Content-Type": "application/json", "Authorization": "Bearer some-token", "X-Api-Key": "some-key" - }) + }, []) const redacted = Headers.redact(headers, ["Authorization", "authorization", "X-Api-Token", "x-api-key"]) @@ -44,7 +44,7 @@ describe("Headers", () => { "Authorization": "Bearer some-token", "sec-ret": "some", "sec-ret-2": "some" - }) + }, []) const redacted = Headers.redact(headers, [/^sec-/]) diff --git a/packages/rpc/src/Rpc.ts b/packages/rpc/src/Rpc.ts index 36ffbb4ad40..e1504e3f1f6 100644 --- a/packages/rpc/src/Rpc.ts +++ b/packages/rpc/src/Rpc.ts @@ -314,10 +314,17 @@ export const currentHeaders: FiberRef.FiberRef = globalValue( * @category headers */ export const annotateHeaders: { - (headers: Headers.Input): (self: Effect.Effect) => Effect.Effect - (self: Effect.Effect, headers: Headers.Input): Effect.Effect -} = dual(2, (self, headers) => { - const resolved = Headers.fromInput(headers) + ( + headers: Headers.Input, + redactedKeys: Headers.RedactedKeys + ): (self: Effect.Effect) => Effect.Effect + ( + self: Effect.Effect, + headers: Headers.Input, + redactedKeys: Headers.RedactedKeys + ): Effect.Effect +} = dual(3, (self, headers, redactedKeys) => { + const resolved = Headers.fromInput(headers, redactedKeys) return Effect.locallyWith(self, currentHeaders, (prev) => ({ ...prev, ...resolved })) }) diff --git a/packages/rpc/src/RpcResolver.ts b/packages/rpc/src/RpcResolver.ts index 3c38c8e6a5d..e13e9979b8f 100644 --- a/packages/rpc/src/RpcResolver.ts +++ b/packages/rpc/src/RpcResolver.ts @@ -111,19 +111,22 @@ export const make = ( */ export const annotateHeaders: { ( - headers: Headers.Input + headers: Headers.Input, + redactedKeys: Headers.RedactedKeys ): ( self: RequestResolver.RequestResolver, R> ) => RequestResolver.RequestResolver, R> ( self: RequestResolver.RequestResolver, R>, - headers: Headers.Input + headers: Headers.Input, + redactedKeys: Headers.RedactedKeys ): RequestResolver.RequestResolver, R> -} = dual(2, ( +} = dual(3, ( self: RequestResolver.RequestResolver, R>, - headers: Headers.Input + headers: Headers.Input, + redactedKeys: Headers.RedactedKeys ): RequestResolver.RequestResolver, R> => { - const resolved = Headers.fromInput(headers) + const resolved = Headers.fromInput(headers, redactedKeys) return RequestResolver.makeWithEntry((requests) => { requests.forEach((entries) => entries.forEach((entry) => { @@ -154,7 +157,9 @@ export const annotateHeadersEffect: { ): RequestResolver.RequestResolver, R | R2> => RequestResolver.makeWithEntry((requests) => headers.pipe( - Effect.map(Headers.fromInput), + Effect.flatMap((headers) => + Headers.currentRedactedNames.pipe(Effect.map((redactedKeys) => Headers.fromInput(headers, redactedKeys))) + ), Effect.orDie, Effect.matchCauseEffect({ onFailure: (cause) => diff --git a/packages/rpc/test/Router.test.ts b/packages/rpc/test/Router.test.ts index 8bc0354000f..f5061d5ca57 100644 --- a/packages/rpc/test/Router.test.ts +++ b/packages/rpc/test/Router.test.ts @@ -351,7 +351,7 @@ describe.each([{ Effect.gen(function*(_) { const headers = yield* _( Rpc.call(new EchoHeaders(), resolver), - Rpc.annotateHeaders({ FOO: "bar" }) + Rpc.annotateHeaders({ FOO: "bar" }, []) ) assert.deepStrictEqual(headers, { foo: "bar" }) }).pipe(Effect.runPromise)) @@ -360,7 +360,7 @@ describe.each([{ Effect.gen(function*(_) { const headers = yield* _( Rpc.call(new EchoHeaders(), resolverWithHeaders), - Rpc.annotateHeaders({ FOO: "bar" }) + Rpc.annotateHeaders({ FOO: "bar" }, []) ) assert.deepStrictEqual(headers, { foo: "bar", baz: "qux" }) }).pipe(Effect.tapErrorCause(Effect.logError), Effect.runPromise))