Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 | 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 31x 31x 31x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 19x 19x 19x 38x 3x 3x 38x 38x 19x 19x 1x 1x 18x 19x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 360x 22x 22x 360x 285x 285x 285x 285x 285x 285x 285x 22x 2x 2x 20x 22x 1x 1x 19x 19x 19x 22x 285x 285x 285x 285x 285x 285x 285x 12388x 12388x 285x 285x 285x 285x 285x 285x 285x 55x 1x 1x 55x 74x 27x 27x 74x 27x 55x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 19x 19x 19x 19x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 21x 21x 18x 11x 11x 18x 18x 21x 1x 1x 2x 21x 21x 2x 2x 13x 21x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 126x 126x 126x 316x 11x 11x 316x 126x 126x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x 285x | /**
* @file redact.js
* @description Property-path redaction for traces.
*
* A trace records real application state, which means it records whatever the
* user typed. Redaction happens at *record* time, not at export time: a value
* a rule matches is never written into the buffer at all, so a trace cannot
* leak a secret that a later export step forgot to strip.
*
* Patterns are matched against the same dotted property paths the reactive
* system already produces (see `getPropertyPath` in `reactive/watcher.js`), so
* `auth.token` and `cart.items.2.cardNumber` are both addressable.
* @module lib/core/trace/redact
*/
import { REDACTED } from './schema.js';
/**
* The shortest withheld value that is worth scrubbing out of source text.
* @type {number}
*/
const MIN_SCRUBBABLE_LENGTH = 6;
/**
* How deep a withheld object is walked when collecting scrubbable strings.
* @type {number}
*/
const REMEMBER_MAX_DEPTH = 6;
/**
* How many strings are remembered from a single withheld value.
*
* A rule that matches a large object should not turn the redactor into a
* copy of that object.
* @type {number}
*/
const REMEMBER_MAX_STRINGS = 200;
/**
* Escapes the regular-expression metacharacters in a literal path segment.
* @param {string} segment - A literal segment.
* @returns {string} The escaped segment.
*/
function escapeSegment(segment) {
return segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Compiles one redaction pattern into a matcher.
*
* Supported syntax, deliberately small:
*
* - `auth.token` — that exact path.
* - `auth.*` — any single segment under `auth`.
* - `*.password` — `password` under any single segment.
* - `auth.**` — `auth` and everything beneath it, at any depth.
*
* A pattern that matches a path also redacts everything nested below it: a
* rule for `auth.token` must not be defeated by the value happening to be an
* object. That also makes `auth` and `auth.**` equivalent, which is why `**`
* needs no special handling beyond ending the pattern.
* @param {string} pattern - The pattern source.
* @returns {RegExp} A matcher anchored to the whole path.
*/
function compilePattern(pattern) {
const parts = [];
for (const segment of String(pattern).split('.')) {
if (segment === '**') {
break;
}
parts.push(segment === '*' ? '[^.]+' : escapeSegment(segment));
}
if (parts.length === 0) {
return /^.*$/;
}
return new RegExp(`^${parts.join('\\.')}(?:\\..*)?$`);
}
/**
* A compiled set of redaction rules.
*
* Held per recorder rather than globally, so a test can record with different
* rules than the dev server without leaking configuration between them.
*/
export class Redactor {
/**
* @param {string[]} [patterns] - Redaction patterns from `avenx.config.json` or the runtime API.
*/
constructor(patterns = []) {
/**
* The patterns as written, kept so an exported trace can declare what was
* withheld from it.
* @type {string[]}
*/
this.patterns = [];
/** @type {RegExp[]} */
this.matchers = [];
/**
* True once a value has actually been withheld. A trace that declares
* rules but never matched one is not a redacted trace.
* @type {boolean}
*/
this.applied = false;
/** @type {Set<string>} */
this.matchedPaths = new Set();
/**
* String values a rule actually withheld.
*
* Kept so the same value can be scrubbed out of recorded *source text*.
* A trace records the verbatim body of every action it ran — that is what
* lets it name the code responsible — and an action with a literal in it
* would otherwise carry a value the path rules just withheld. These are
* values already resident in the application's own memory; nothing new is
* retained, and the set dies with the recorder.
* @type {Set<string>}
*/
this.withheldValues = new Set();
for (const pattern of patterns) {
this.add(pattern);
}
}
/**
* Registers an additional pattern.
* @param {string} pattern - The pattern source.
* @returns {Redactor} This redactor, for chaining.
*/
add(pattern) {
if (typeof pattern !== 'string' || pattern.trim() === '') {
return this;
}
const trimmed = pattern.trim();
if (this.patterns.includes(trimmed)) {
return this;
}
this.patterns.push(trimmed);
this.matchers.push(compilePattern(trimmed));
return this;
}
/**
* Whether this redactor has any rules at all. Hot paths check this first, so
* an unconfigured recorder pays nothing for the feature.
* @returns {boolean}
*/
get isEmpty() {
return this.matchers.length === 0;
}
/**
* Whether a property path must be withheld.
* @param {string} path - A dotted property path, e.g. `auth.token`.
* @returns {boolean}
*/
matches(path) {
if (this.matchers.length === 0 || typeof path !== 'string' || path === '') {
return false;
}
for (const matcher of this.matchers) {
if (matcher.test(path)) {
return true;
}
}
return false;
}
/**
* Notes that a rule fired for a path.
*
* Separate from {@link Redactor#matches} because capture cannot detect a
* redaction by comparing values: `NaN !== NaN` would make every NaN look
* like a withheld value.
* @param {string} path - The path a rule matched.
* @param {any} [value] - The withheld value, remembered so it can also be
* scrubbed out of recorded source text.
*/
markApplied(path, value) {
this.applied = true;
this.matchedPaths.add(path);
this.#remember(value, REMEMBER_MAX_DEPTH);
}
/**
* Collects the strings inside a withheld value.
*
* Withholding `{ email, name }` withholds the email, so the email string is
* a secret wherever else it appears — in a recorded call's arguments, for
* instance, which no path rule covers.
*
* Short strings are excluded: scrubbing a two-character value out of every
* recorded expression would mangle unrelated source for no benefit.
* @param {any} value - A withheld value.
* @param {number} depth - Remaining depth budget.
* @private
*/
#remember(value, depth) {
if (this.withheldValues.size >= REMEMBER_MAX_STRINGS) {
return;
}
if (typeof value === 'string') {
if (value.length >= MIN_SCRUBBABLE_LENGTH) {
this.withheldValues.add(value);
}
return;
}
if (depth <= 0 || value === null || typeof value !== 'object') {
return;
}
try {
const entries = Array.isArray(value) ? value : Object.values(value);
for (const entry of entries) {
this.#remember(entry, depth - 1);
}
} catch {
// A throwing getter costs one value's worth of scrubbing, not the trace.
}
}
/**
* Removes any withheld value from a piece of recorded source text.
*
* Applied when a trace is serialized rather than when a node is recorded: an
* action's source is captured before its writes run, so the value it
* contains is not yet known to be a secret at that point.
* @param {string} text - Recorded source text.
* @returns {string} The text, with withheld values replaced.
*/
scrub(text) {
if (typeof text !== 'string' || this.withheldValues.size === 0) {
return text;
}
let scrubbed = text;
for (const secret of this.withheldValues) {
if (scrubbed.includes(secret)) {
scrubbed = scrubbed.split(secret).join(REDACTED);
}
}
return scrubbed;
}
/**
* Returns the value to record for a path: the value itself, or the redaction
* placeholder when a rule matched.
* @param {string} path - The property path the value sits at.
* @param {any} value - The candidate value.
* @returns {any} What may be recorded.
*/
guard(path, value) {
if (this.matches(path)) {
this.markApplied(path, value);
return REDACTED;
}
return value;
}
}
/**
* A redactor with no rules, shared by callers that have not configured any.
* @type {Redactor}
*/
export const NO_REDACTION = new Redactor();
|