1/**
2 * Context-aware redaction for sensitive values.
3 *
4 * The `Redactable` module provides a protocol for objects that need to present
5 * alternative representations of themselves depending on the runtime context.
6 * Typical use cases include masking secrets, tokens, or personal data in logs, traces,
7 * and serialized output.
8 *
9 * @since 4.0.0
10 */
11import type * as Context from "./Context.ts"
12import { pipeArguments } from "./Pipeable.ts"
13import { hasProperty } from "./Predicate.ts"
14
15/**
16 * Defines the symbol used to identify objects that implement the {@link Redactable}
17 * protocol.
18 *
19 * **When to use**
20 *
21 * Use as the property key when implementing the `Redactable` protocol.
22 *
23 * **Details**
24 *
25 * Add a method under this key to make an object redactable. The method receives
26 * the current `Context` and must return the replacement value. The symbol is
27 * registered globally via `Symbol.for("~effect/Redactable")`, so it is
28 * identical across multiple copies of the library at runtime.
29 *
30 * **Example** (Masking an API key)
31 *
32 * ```ts import.meta.vitest
33 * import { Context, Redactable } from "effect"
34 *
35 * class ApiKey {
36 * constructor(readonly raw: string) {}
37 *
38 * [Redactable.symbolRedactable](_ctx: Context.Context<never>) {
39 * return this.raw.slice(0, 4) + "..."
40 * }
41 * }
42 *
43 * Redactable.redact(new ApiKey("secret-key")) // => "secr..."
44 * ```
45 *
46 * @see {@link Redactable} for the interface this symbol belongs to
47 * @see {@link isRedactable} to check whether a value has this symbol
48 * @category symbols
49 * @since 3.10.0
50 */
51export const symbolRedactable: unique symbol = Symbol.for("~effect/Redactable")
52
53/**
54 * Interface for objects that provide context-aware redacted representations.
55 *
56 * **When to use**
57 *
58 * Use to define classes or objects that hold sensitive data and should present
59 * a sanitized form when inspected or logged.
60 *
61 * **Details**
62 *
63 * The `[symbolRedactable]` method receives the current fiber's `Context`. If no
64 * fiber is active, an empty `Context` is provided.
65 *
66 * **Example** (Masking an API key)
67 *
68 * ```ts import.meta.vitest
69 * import { Context, Redactable } from "effect"
70 *
71 * class ApiKey {
72 * constructor(readonly raw: string) {}
73 *
74 * [Redactable.symbolRedactable](_ctx: Context.Context<never>) {
75 * return this.raw.slice(0, 4) + "..."
76 * }
77 * }
78 *
79 * Redactable.redact(new ApiKey("secret-key")) // => "secr..."
80 * ```
81 *
82 * @see {@link symbolRedactable} for the symbol key to implement
83 * @see {@link redact} to apply redaction to any value
84 * @see {@link isRedactable} for the type guard for this interface
85 * @category models
86 * @since 3.10.0
87 */
88export interface Redactable {
89 readonly [symbolRedactable]: (context: Context.Context<never>) => unknown
90}
91
92/**
93 * Type guard that checks whether a value implements the {@link Redactable}
94 * interface.
95 *
96 * **When to use**
97 *
98 * Use to narrow an unknown value before calling redaction-specific helpers.
99 *
100 * @see {@link Redactable} for the interface being checked
101 * @see {@link redact} to apply redaction if the value is redactable
102 * @category guards
103 * @since 3.10.0
104 */
105export const isRedactable = (u: unknown): u is Redactable => hasProperty(u, symbolRedactable)
106
107/**
108 * Returns a redacted value if it implements {@link Redactable}, otherwise returns it
109 * unchanged.
110 *
111 * **When to use**
112 *
113 * Use as the general-purpose entry point for redaction when the input may
114 * or may not implement the redaction protocol.
115 *
116 * **Details**
117 *
118 * This function calls {@link isRedactable} and, when it returns `true`,
119 * delegates to {@link getRedacted}.
120 *
121 * **Gotchas**
122 *
123 * Redaction is not recursive. Nested redactable values inside the returned
124 * object are not automatically redacted.
125 *
126 * @see {@link isRedactable} to check before redacting
127 * @see {@link getRedacted} for the lower-level variant for known redactables
128 * @category destructors
129 * @since 3.10.0
130 */
131export function redact(u: unknown): unknown {
132 if (isRedactable(u)) return getRedacted(u)
133 return u
134}
135
136/**
137 * Returns the result of calling `[symbolRedactable]` on a value that is
138 * already known to be {@link Redactable}.
139 *
140 * **When to use**
141 *
142 * Use when you need to read the redacted representation from a value already
143 * verified as `Redactable`.
144 *
145 * **Details**
146 *
147 * This function reads the current fiber's `Context` from the global fiber
148 * reference and passes it to the redaction method.
149 *
150 * **Gotchas**
151 *
152 * If no fiber is active, an empty `Context` is passed to the redaction method.
153 *
154 * @see {@link redact} for the higher-level variant that handles non-redactable values
155 * @see {@link isRedactable} for the type guard to verify before calling this
156 * @category destructors
157 * @since 4.0.0
158 */
159export function getRedacted(redactable: Redactable): unknown {
160 return redactable[symbolRedactable]((globalThis as any)[currentFiberTypeId]?.context ?? emptyContext)
161}
162
163/** @internal */
164export const currentFiberTypeId = "~effect/Fiber/currentFiber"
165
166const emptyMap = new Map()
167const emptyContext: Context.Context<never> = {
168 "~effect/Context": {} as any,
169 base: emptyMap,
170 depth: 0,
171 mapUnsafe: emptyMap,
172 pipe() {
173 return pipeArguments(this, arguments)
174 }
175} as any