| 1 | /** |
| 2 | * Wraps sensitive values so normal output does not reveal them. |
| 3 | * |
| 4 | * A `Redacted<A>` shows a redacted placeholder in string, JSON, and inspection |
| 5 | * output, while still storing the original value for trusted code that needs to |
| 6 | * recover it. This helps reduce accidental leaks in logs and diagnostics. This |
| 7 | * module includes constructors, runtime checks, value recovery, wiping of stored |
| 8 | * values, and comparison helpers that avoid exposing the wrapped value at the |
| 9 | * call site. |
| 10 | * |
| 11 | * @since 3.3.0 |
| 12 | */ |
| 13 | import * as Equal from "./Equal.ts" |
| 14 | import * as Equivalence from "./Equivalence.ts" |
| 15 | import * as Hash from "./Hash.ts" |
| 16 | import { PipeInspectableProto } from "./internal/core.ts" |
| 17 | import * as redacted from "./internal/redacted.ts" |
| 18 | import type { Pipeable } from "./Pipeable.ts" |
| 19 | import { hasProperty, isString } from "./Predicate.ts" |
| 20 | import type { Covariant } from "./Types.ts" |
| 21 | |
| 22 | const TypeId = "~effect/Redacted" |
| 23 | |
| 24 | /** |
| 25 | * A wrapper for sensitive values whose string, JSON, and inspection output is |
| 26 | * redacted. |
| 27 | * |
| 28 | * **When to use** |
| 29 | * |
| 30 | * Use to carry sensitive values while reducing accidental exposure in string, |
| 31 | * JSON, and inspection output. |
| 32 | * |
| 33 | * **Gotchas** |
| 34 | * |
| 35 | * The underlying value is still stored in memory and can be recovered with |
| 36 | * `Redacted.value` until the wrapper is wiped or becomes unreachable. Use |
| 37 | * `Redacted` to reduce accidental disclosure in logs and diagnostics, not as a |
| 38 | * cryptographic protection mechanism. |
| 39 | * |
| 40 | * **Example** (Creating redacted values) |
| 41 | * |
| 42 | * ```ts import.meta.vitest |
| 43 | * import { Redacted } from "effect" |
| 44 | * |
| 45 | * // Create a redacted value to protect sensitive information |
| 46 | * const apiKey = Redacted.make("secret-key") |
| 47 | * const userPassword = Redacted.make("user-password") |
| 48 | * |
| 49 | * // TypeScript will infer the types as Redacted<string> |
| 50 | * Array.of(String(apiKey), String(userPassword)) // => ["<redacted>", "<redacted>"] |
| 51 | * ``` |
| 52 | * |
| 53 | * @category models |
| 54 | * @since 3.3.0 |
| 55 | */ |
| 56 | export interface Redacted<out A = string> extends Redacted.Variance<A>, Equal.Equal, Pipeable { |
| 57 | readonly label: string | undefined |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * Namespace containing type-level members associated with `Redacted` values. |
| 62 | * |
| 63 | * **When to use** |
| 64 | * |
| 65 | * Use to access type-level helpers associated with `Redacted`. |
| 66 | * |
| 67 | * **Example** (Using namespace utilities) |
| 68 | * |
| 69 | * ```ts import.meta.vitest |
| 70 | * import { Redacted } from "effect" |
| 71 | * |
| 72 | * // Use the Redacted namespace for type-level operations |
| 73 | * const secret = Redacted.make("my-secret") |
| 74 | * |
| 75 | * // The namespace contains utilities for working with Redacted values |
| 76 | * Redacted.isRedacted(secret) // => true |
| 77 | * ``` |
| 78 | * |
| 79 | * @since 3.3.0 |
| 80 | */ |
| 81 | export declare namespace Redacted { |
| 82 | /** |
| 83 | * Type-level variance marker for `Redacted`. |
| 84 | * |
| 85 | * **When to use** |
| 86 | * |
| 87 | * Use when defining internals that need to preserve the covariant value type |
| 88 | * carried by `Redacted`. |
| 89 | * |
| 90 | * **Details** |
| 91 | * |
| 92 | * This interface records the covariant value type carried by a `Redacted` |
| 93 | * value and is not normally referenced directly by users. |
| 94 | * |
| 95 | * @category models |
| 96 | * @since 3.3.0 |
| 97 | */ |
| 98 | export interface Variance<out A> { |
| 99 | readonly [TypeId]: { |
| 100 | readonly _A: Covariant<A> |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | /** |
| 105 | * Extracts the underlying value type from a `Redacted` type. |
| 106 | * |
| 107 | * **When to use** |
| 108 | * |
| 109 | * Use to infer the sensitive value type from an existing `Redacted` type. |
| 110 | * |
| 111 | * **Example** (Extracting the redacted value type) |
| 112 | * |
| 113 | * ```ts import.meta.vitest |
| 114 | * import { Redacted } from "effect" |
| 115 | * |
| 116 | * type ApiKey = Redacted.Redacted<{ readonly token: string }> |
| 117 | * type ApiKeyValue = Redacted.Redacted.Value<ApiKey> |
| 118 | * |
| 119 | * const rotate = (value: ApiKeyValue): ApiKeyValue => ({ |
| 120 | * token: `${value.token}:rotated` |
| 121 | * }) |
| 122 | * |
| 123 | * rotate({ token: "secret" }) // => { token: "secret:rotated" } |
| 124 | * ``` |
| 125 | * |
| 126 | * @category utility types |
| 127 | * @since 3.3.0 |
| 128 | */ |
| 129 | export type Value<T extends Redacted<any>> = [T] extends [Redacted<infer _A>] ? _A : never |
| 130 | } |
| 131 | |
| 132 | /** |
| 133 | * Returns `true` if a value is a `Redacted` wrapper. |
| 134 | * |
| 135 | * **When to use** |
| 136 | * |
| 137 | * Use to validate unknown input and narrow it to `Redacted`. |
| 138 | * |
| 139 | * **Details** |
| 140 | * |
| 141 | * When this function returns `true`, TypeScript narrows the value to |
| 142 | * `Redacted<unknown>`. |
| 143 | * |
| 144 | * **Example** (Checking for redacted values) |
| 145 | * |
| 146 | * ```ts import.meta.vitest |
| 147 | * import { Redacted } from "effect" |
| 148 | * |
| 149 | * const secret = Redacted.make("my-secret") |
| 150 | * const plainString = "not-secret" |
| 151 | * |
| 152 | * Redacted.isRedacted(secret) // => true |
| 153 | * Redacted.isRedacted(plainString) // => false |
| 154 | * ``` |
| 155 | * |
| 156 | * @category guards |
| 157 | * @since 3.3.0 |
| 158 | */ |
| 159 | export const isRedacted = (u: unknown): u is Redacted<unknown> => hasProperty(u, TypeId) |
| 160 | |
| 161 | /** |
| 162 | * Creates a `Redacted` wrapper for a sensitive value. |
| 163 | * |
| 164 | * **When to use** |
| 165 | * |
| 166 | * Use to wrap a sensitive value so normal string, JSON, and inspection output |
| 167 | * is redacted. |
| 168 | * |
| 169 | * **Details** |
| 170 | * |
| 171 | * The wrapper redacts string, JSON, and inspection output to reduce accidental |
| 172 | * disclosure. The original value remains retrievable with `Redacted.value` |
| 173 | * until the wrapper is wiped or becomes unreachable. |
| 174 | * |
| 175 | * **Example** (Creating a redacted value) |
| 176 | * |
| 177 | * ```ts import.meta.vitest |
| 178 | * import { Redacted } from "effect" |
| 179 | * |
| 180 | * const API_KEY = Redacted.make("1234567890") |
| 181 | * String(API_KEY) // => "<redacted>" |
| 182 | * ``` |
| 183 | * |
| 184 | * @category constructors |
| 185 | * @since 3.3.0 |
| 186 | */ |
| 187 | export const make = <T>(value: T, options?: { |
| 188 | readonly label?: string | undefined |
| 189 | }): Redacted<T> => { |
| 190 | const self = Object.create(Proto) |
| 191 | if (options?.label) { |
| 192 | self.label = options.label |
| 193 | } |
| 194 | redacted.redactedRegistry.set(self, value) |
| 195 | return self |
| 196 | } |
| 197 | |
| 198 | const Proto = { |
| 199 | [TypeId]: { |
| 200 | _A: (_: never) => _ |
| 201 | }, |
| 202 | label: undefined, |
| 203 | ...PipeInspectableProto, |
| 204 | toJSON() { |
| 205 | return this.toString() |
| 206 | }, |
| 207 | toString() { |
| 208 | return `<redacted${isString(this.label) ? ":" + this.label : ""}>` |
| 209 | }, |
| 210 | [Hash.symbol]<T>(this: Redacted<T>): number { |
| 211 | return Hash.hash(redacted.redactedRegistry.get(this)) |
| 212 | }, |
| 213 | [Equal.symbol]<T>(this: Redacted<T>, that: unknown): boolean { |
| 214 | return ( |
| 215 | isRedacted(that) && |
| 216 | Equal.equals( |
| 217 | redacted.redactedRegistry.get(this), |
| 218 | redacted.redactedRegistry.get(that) |
| 219 | ) |
| 220 | ) |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | /** |
| 225 | * Retrieves the original value from a `Redacted` instance. Use this function |
| 226 | * with caution, as it exposes the sensitive data. |
| 227 | * |
| 228 | * **When to use** |
| 229 | * |
| 230 | * Use when you need the underlying sensitive value at a trusted boundary. |
| 231 | * |
| 232 | * **Example** (Retrieving a redacted value) |
| 233 | * |
| 234 | * ```ts import.meta.vitest |
| 235 | * import { Redacted } from "effect" |
| 236 | * |
| 237 | * const API_KEY = Redacted.make("1234567890") |
| 238 | * |
| 239 | * Redacted.value(API_KEY) // => "1234567890" |
| 240 | * ``` |
| 241 | * |
| 242 | * @category getters |
| 243 | * @since 3.3.0 |
| 244 | */ |
| 245 | export const value: <T>(self: Redacted<T>) => T = redacted.value |
| 246 | |
| 247 | /** |
| 248 | * Deletes the stored value for a `Redacted` wrapper, making future |
| 249 | * `Redacted.value` calls on that wrapper fail. |
| 250 | * |
| 251 | * **When to use** |
| 252 | * |
| 253 | * Use when a `Redacted` wrapper should no longer be able to reveal its stored |
| 254 | * value. |
| 255 | * |
| 256 | * **Gotchas** |
| 257 | * |
| 258 | * This unsafe operation does not zero memory and does not affect other |
| 259 | * references to the original value. It only removes the value from the |
| 260 | * internal redacted registry. |
| 261 | * |
| 262 | * **Example** (Wiping a redacted value) |
| 263 | * |
| 264 | * ```ts import.meta.vitest |
| 265 | * import { Redacted, Result } from "effect" |
| 266 | * |
| 267 | * const API_KEY = Redacted.make("1234567890") |
| 268 | * |
| 269 | * Redacted.value(API_KEY) // => "1234567890" |
| 270 | * |
| 271 | * Redacted.wipeUnsafe(API_KEY) |
| 272 | * |
| 273 | * const failure = Result.try({ |
| 274 | * try: () => Redacted.value(API_KEY), |
| 275 | * catch: (error) => (error as Error).message |
| 276 | * }) |
| 277 | * failure // => Result.fail("Unable to get redacted value") |
| 278 | * ``` |
| 279 | * |
| 280 | * @category unsafe |
| 281 | * @since 4.0.0 |
| 282 | */ |
| 283 | export const wipeUnsafe = <T>(self: Redacted<T>): boolean => redacted.redactedRegistry.delete(self) |
| 284 | |
| 285 | /** |
| 286 | * Generates an equivalence relation for `Redacted<A>` values based on an |
| 287 | * equivalence relation for the underlying values `A`. This function is useful |
| 288 | * for comparing `Redacted` instances without exposing their contents. |
| 289 | * |
| 290 | * **When to use** |
| 291 | * |
| 292 | * Use when you need to compare wrapped secrets through an approved equality |
| 293 | * rule without exposing the underlying values at each comparison site. |
| 294 | * |
| 295 | * **Example** (Comparing redacted values) |
| 296 | * |
| 297 | * ```ts import.meta.vitest |
| 298 | * import { Equivalence, Redacted } from "effect" |
| 299 | * |
| 300 | * const API_KEY1 = Redacted.make("1234567890") |
| 301 | * const API_KEY2 = Redacted.make("1-34567890") |
| 302 | * const API_KEY3 = Redacted.make("1234567890") |
| 303 | * |
| 304 | * const equivalence = Redacted.makeEquivalence(Equivalence.strictEqual<string>()) |
| 305 | * |
| 306 | * equivalence(API_KEY1, API_KEY2) // => false |
| 307 | * equivalence(API_KEY1, API_KEY3) // => true |
| 308 | * ``` |
| 309 | * |
| 310 | * @category instances |
| 311 | * @since 4.0.0 |
| 312 | */ |
| 313 | export const makeEquivalence = <A>(isEquivalent: Equivalence.Equivalence<A>): Equivalence.Equivalence<Redacted<A>> => |
| 314 | Equivalence.make((x, y) => isEquivalent(value(x), value(y))) |