import { isPlainObject } from "./lib/utils" import type { Encodable } from "./index" /** Runtime representation of a whole-value or structural sensitivity policy. */ export type SensitivityMask = | true | readonly SensitivityMask[] | { readonly [key: string]: SensitivityMask | undefined } /** Codec object types whose internal properties are not maskable structure. */ type AtomicEncodableObject = | ArrayBuffer | ArrayBufferView | Blob | Date | File | Headers | Map | RegExp | Request | Response | Set | URL | URLSearchParams /** * Mirrors an output value with `true` at each subtree that must not be exposed. * * A one-element array mask applies to every array element. Non-structural codec * values can only be marked wholly sensitive. */ export type StructuralSensitivityMask = | true | (T extends unknown ? NonNullable extends AtomicEncodableObject ? never : NonNullable extends readonly (infer TItem)[] ? readonly [StructuralSensitivityMask] : NonNullable extends object ? { readonly [TKey in keyof NonNullable]?: StructuralSensitivityMask< NonNullable[TKey] > } : never : never) /** * Returns whether a value is a valid runtime sensitivity mask. * * @param value - Candidate mask. */ export function isSensitivityMask(value: unknown): value is SensitivityMask { if (value === true) return true if (Array.isArray(value)) { return value.length === 1 && isSensitivityMask(value[0]) } return ( typeof value === "object" && value !== null && isPlainObject(value) && Object.keys(value).length > 0 && Object.values(value).every(isSensitivityMask) ) } /** * Replaces structurally sensitive subtrees with `undefined` for persistence in * author-visible execution data. * * @param value - Validated codec value being projected. * @param mask - Structural policy declared by the value's producer. * @throws {TypeError} When the mask does not match the value structure. */ export function redactSensitiveValue( value: T, mask: SensitivityMask, ): T | undefined export function redactSensitiveValue( value: Encodable, mask: SensitivityMask, ): Encodable { if (mask === true) return undefined if (isSensitivityArray(mask)) { if (!Array.isArray(value)) { throw new TypeError("Array sensitivity masks require array values.") } return value.map((item) => redactSensitiveValue(item, mask[0]!)) } if (typeof value !== "object" || value === null || !isPlainObject(value)) { throw new TypeError("Object sensitivity masks require plain-object values.") } if (Object.keys(mask).some((key) => !Object.hasOwn(value, key))) { throw new TypeError( "Object sensitivity masks may only name properties present in the value.", ) } return Object.fromEntries( Object.entries(value).map(([key, item]) => [ key, mask[key] ? redactSensitiveValue(item, mask[key]) : item, ]), ) } /** * Projects a structural mask through one property access. * * @param mask - Parent sensitivity mask. * @param property - Accessed property name or array index. */ export function projectSensitivityMask( mask: SensitivityMask | undefined, property: string, ): SensitivityMask | undefined { if (mask === true) return true if (!mask) return if (isSensitivityArray(mask)) { const index = Number(property) if (!Number.isInteger(index) || index < 0) return return mask[0]! } return mask[property] } /** * Narrows the readonly array branch that `Array.isArray` cannot express. * * @param mask - Runtime sensitivity mask. */ function isSensitivityArray( mask: SensitivityMask, ): mask is readonly SensitivityMask[] { return Array.isArray(mask) }