react-context-compressor 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # react-context-compressor
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/react-context-compressor.svg)](https://www.npmjs.com/package/react-context-compressor)
4
+ [![CI](https://github.com/Muhammad-UmairAli/react-context-compressor/actions/workflows/ci.yml/badge.svg)](https://github.com/Muhammad-UmairAli/react-context-compressor/actions/workflows/ci.yml)
4
5
  [![minzipped size](https://img.shields.io/bundlephobia/minzip/react-context-compressor)](https://bundlephobia.com/package/react-context-compressor)
5
6
  [![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](https://www.npmjs.com/package/react-context-compressor?activeTab=dependencies)
6
7
  [![license](https://img.shields.io/npm/l/react-context-compressor.svg)](./LICENSE)
@@ -85,9 +86,10 @@ Components, on React 17 / 18 / 19.
85
86
  | Sanitize | `sanitize`, `defaultSanitize`, … | Redact/remove sensitive fields (see below). |
86
87
 
87
88
  It also handles awkward inputs predictably: **circular references** → `"[Circular]"`,
88
- a **throwing getter** → `"[Getter]"` (never crashes), `Date` kept as-is, `Map` →
89
- plain object, `Set` → array, functions/symbols dropped, `BigInt` → string. The
90
- input object is **never mutated**, and output is **deterministic**.
89
+ a **throwing getter** → `"[Getter]"` (never crashes), `Date` a copy, `Map` → plain
90
+ object, `Set` / `TypedArray` → array, `RegExp` → its source string, `Error` →
91
+ `{ name, message }`, functions/symbols dropped, `BigInt` string. The input object
92
+ is **never mutated**, and output is **deterministic**.
91
93
 
92
94
  ## Sanitization (data safety)
93
95
 
@@ -164,6 +166,24 @@ network calls, or run a local model. It is a mechanical, zero-cost, client-side
164
166
  object parser — that's the whole point (it saves you money _before_ the network
165
167
  layer). See [SPLIT-PLAN §2 (out of scope)](./SPLIT-PLAN.md).
166
168
 
169
+ ## Contributing & releasing
170
+
171
+ Development uses Git Flow: feature branches → `develop` (PR required), releases
172
+ promote `develop` → `main`.
173
+
174
+ ```sh
175
+ npm install
176
+ npm test # vitest
177
+ npm run typecheck && npm run lint && npm run build && npm run size
178
+ ```
179
+
180
+ Each change adds a [changeset](https://github.com/changesets/changesets)
181
+ (`npm run changeset`). To cut a release: branch `release/X.Y.Z` off `develop`,
182
+ run `npx changeset version` (bumps the version + updates the changelog), open a
183
+ PR to `main`, merge, then tag `vX.Y.Z`. Pushing the tag triggers
184
+ [`.github/workflows/release.yml`](./.github/workflows/release.yml), which
185
+ publishes to npm **with provenance** (once an `NPM_TOKEN` secret is configured).
186
+
167
187
  ## License
168
188
 
169
189
  MIT
@@ -114,6 +114,14 @@ function isEmptyValue(value) {
114
114
  if (isPlainObject(value)) return Object.keys(value).length === 0;
115
115
  return false;
116
116
  }
117
+ function coerceRedactedValue(value) {
118
+ if (value === null || value === void 0) return DEFAULTS.redactedValue;
119
+ try {
120
+ return String(value);
121
+ } catch {
122
+ return DEFAULTS.redactedValue;
123
+ }
124
+ }
117
125
  function resolveOptions(options) {
118
126
  return {
119
127
  maxDepth: options.maxDepth ?? DEFAULTS.maxDepth,
@@ -123,7 +131,7 @@ function resolveOptions(options) {
123
131
  sanitize: options.sanitize ?? DEFAULTS.sanitize,
124
132
  defaultSanitize: options.defaultSanitize ?? DEFAULTS.defaultSanitize,
125
133
  sanitizeMode: options.sanitizeMode ?? DEFAULTS.sanitizeMode,
126
- redactedValue: options.redactedValue ?? DEFAULTS.redactedValue
134
+ redactedValue: coerceRedactedValue(options.redactedValue)
127
135
  };
128
136
  }
129
137
  function walkObjectInto(out, source, depth, opts, seen) {
@@ -155,27 +163,67 @@ function walk(value, depth, opts, seen) {
155
163
  if (type !== "object") return value;
156
164
  const ref = value;
157
165
  if (seen.has(ref)) return CIRCULAR;
158
- if (value instanceof Date) return value;
166
+ if (value instanceof Date) return new Date(value.getTime());
167
+ if (value instanceof RegExp) {
168
+ try {
169
+ return value.toString();
170
+ } catch {
171
+ return GETTER_ERROR;
172
+ }
173
+ }
174
+ if (value instanceof Error) {
175
+ try {
176
+ return walk({ name: value.name, message: value.message }, depth, opts, seen);
177
+ } catch {
178
+ return GETTER_ERROR;
179
+ }
180
+ }
181
+ if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
182
+ try {
183
+ return walk(Array.from(value), depth, opts, seen);
184
+ } catch {
185
+ return GETTER_ERROR;
186
+ }
187
+ }
159
188
  if (value instanceof Map) {
160
189
  seen.add(ref);
161
190
  const obj = {};
162
- for (const [k, v] of value) safeAssign(obj, String(k), v);
191
+ try {
192
+ for (const [k, v] of value) safeAssign(obj, String(k), v);
193
+ } catch {
194
+ seen.delete(ref);
195
+ return GETTER_ERROR;
196
+ }
163
197
  const result = walk(obj, depth, opts, seen);
164
198
  seen.delete(ref);
165
199
  return result;
166
200
  }
167
201
  if (value instanceof Set) {
168
202
  seen.add(ref);
169
- const result = walk(Array.from(value), depth, opts, seen);
203
+ let items;
204
+ try {
205
+ items = Array.from(value);
206
+ } catch {
207
+ seen.delete(ref);
208
+ return GETTER_ERROR;
209
+ }
210
+ const result = walk(items, depth, opts, seen);
170
211
  seen.delete(ref);
171
212
  return result;
172
213
  }
173
214
  if (Array.isArray(value)) {
174
215
  if (depth >= opts.maxDepth) return TRUNCATED_ARRAY;
175
216
  seen.add(ref);
176
- const limited = value.length > opts.maxArrayLength ? value.slice(0, opts.maxArrayLength) : value;
217
+ const cap = Math.min(value.length, opts.maxArrayLength);
177
218
  const out2 = [];
178
- for (const item of limited) {
219
+ for (let i = 0; i < cap; i++) {
220
+ let item;
221
+ try {
222
+ item = value[i];
223
+ } catch {
224
+ out2.push(GETTER_ERROR);
225
+ continue;
226
+ }
179
227
  const walked = walk(item, depth + 1, opts, seen);
180
228
  out2.push(walked === OMIT ? null : walked);
181
229
  }
@@ -192,8 +240,18 @@ function walk(value, depth, opts, seen) {
192
240
  seen.delete(ref);
193
241
  return out;
194
242
  }
243
+ function warnIfRedactionDisabled(opts) {
244
+ if (opts.defaultSanitize || opts.sanitize.length > 0) return;
245
+ const proc = globalThis.process;
246
+ if (typeof proc !== "undefined" && proc.env?.NODE_ENV !== "production") {
247
+ console.warn(
248
+ "react-context-compressor: redaction is fully disabled (defaultSanitize:false and no sanitize matchers); sensitive fields will NOT be redacted."
249
+ );
250
+ }
251
+ }
195
252
  function compressCore(state, options = {}) {
196
253
  const opts = resolveOptions(options);
254
+ warnIfRedactionDisabled(opts);
197
255
  const walked = walk(state, 0, opts, /* @__PURE__ */ new WeakSet());
198
256
  return walked === OMIT ? void 0 : walked;
199
257
  }
@@ -204,5 +262,5 @@ function compress(state, options = {}) {
204
262
  }
205
263
 
206
264
  export { compress };
207
- //# sourceMappingURL=chunk-VBOCWPM5.mjs.map
208
- //# sourceMappingURL=chunk-VBOCWPM5.mjs.map
265
+ //# sourceMappingURL=chunk-X2A2RYP4.mjs.map
266
+ //# sourceMappingURL=chunk-X2A2RYP4.mjs.map
package/dist/index.cjs CHANGED
@@ -116,6 +116,14 @@ function isEmptyValue(value) {
116
116
  if (isPlainObject(value)) return Object.keys(value).length === 0;
117
117
  return false;
118
118
  }
119
+ function coerceRedactedValue(value) {
120
+ if (value === null || value === void 0) return DEFAULTS.redactedValue;
121
+ try {
122
+ return String(value);
123
+ } catch {
124
+ return DEFAULTS.redactedValue;
125
+ }
126
+ }
119
127
  function resolveOptions(options) {
120
128
  return {
121
129
  maxDepth: options.maxDepth ?? DEFAULTS.maxDepth,
@@ -125,7 +133,7 @@ function resolveOptions(options) {
125
133
  sanitize: options.sanitize ?? DEFAULTS.sanitize,
126
134
  defaultSanitize: options.defaultSanitize ?? DEFAULTS.defaultSanitize,
127
135
  sanitizeMode: options.sanitizeMode ?? DEFAULTS.sanitizeMode,
128
- redactedValue: options.redactedValue ?? DEFAULTS.redactedValue
136
+ redactedValue: coerceRedactedValue(options.redactedValue)
129
137
  };
130
138
  }
131
139
  function walkObjectInto(out, source, depth, opts, seen) {
@@ -157,27 +165,67 @@ function walk(value, depth, opts, seen) {
157
165
  if (type !== "object") return value;
158
166
  const ref = value;
159
167
  if (seen.has(ref)) return CIRCULAR;
160
- if (value instanceof Date) return value;
168
+ if (value instanceof Date) return new Date(value.getTime());
169
+ if (value instanceof RegExp) {
170
+ try {
171
+ return value.toString();
172
+ } catch {
173
+ return GETTER_ERROR;
174
+ }
175
+ }
176
+ if (value instanceof Error) {
177
+ try {
178
+ return walk({ name: value.name, message: value.message }, depth, opts, seen);
179
+ } catch {
180
+ return GETTER_ERROR;
181
+ }
182
+ }
183
+ if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
184
+ try {
185
+ return walk(Array.from(value), depth, opts, seen);
186
+ } catch {
187
+ return GETTER_ERROR;
188
+ }
189
+ }
161
190
  if (value instanceof Map) {
162
191
  seen.add(ref);
163
192
  const obj = {};
164
- for (const [k, v] of value) safeAssign(obj, String(k), v);
193
+ try {
194
+ for (const [k, v] of value) safeAssign(obj, String(k), v);
195
+ } catch {
196
+ seen.delete(ref);
197
+ return GETTER_ERROR;
198
+ }
165
199
  const result = walk(obj, depth, opts, seen);
166
200
  seen.delete(ref);
167
201
  return result;
168
202
  }
169
203
  if (value instanceof Set) {
170
204
  seen.add(ref);
171
- const result = walk(Array.from(value), depth, opts, seen);
205
+ let items;
206
+ try {
207
+ items = Array.from(value);
208
+ } catch {
209
+ seen.delete(ref);
210
+ return GETTER_ERROR;
211
+ }
212
+ const result = walk(items, depth, opts, seen);
172
213
  seen.delete(ref);
173
214
  return result;
174
215
  }
175
216
  if (Array.isArray(value)) {
176
217
  if (depth >= opts.maxDepth) return TRUNCATED_ARRAY;
177
218
  seen.add(ref);
178
- const limited = value.length > opts.maxArrayLength ? value.slice(0, opts.maxArrayLength) : value;
219
+ const cap = Math.min(value.length, opts.maxArrayLength);
179
220
  const out2 = [];
180
- for (const item of limited) {
221
+ for (let i = 0; i < cap; i++) {
222
+ let item;
223
+ try {
224
+ item = value[i];
225
+ } catch {
226
+ out2.push(GETTER_ERROR);
227
+ continue;
228
+ }
181
229
  const walked = walk(item, depth + 1, opts, seen);
182
230
  out2.push(walked === OMIT ? null : walked);
183
231
  }
@@ -194,8 +242,18 @@ function walk(value, depth, opts, seen) {
194
242
  seen.delete(ref);
195
243
  return out;
196
244
  }
245
+ function warnIfRedactionDisabled(opts) {
246
+ if (opts.defaultSanitize || opts.sanitize.length > 0) return;
247
+ const proc = globalThis.process;
248
+ if (typeof proc !== "undefined" && proc.env?.NODE_ENV !== "production") {
249
+ console.warn(
250
+ "react-context-compressor: redaction is fully disabled (defaultSanitize:false and no sanitize matchers); sensitive fields will NOT be redacted."
251
+ );
252
+ }
253
+ }
197
254
  function compressCore(state, options = {}) {
198
255
  const opts = resolveOptions(options);
256
+ warnIfRedactionDisabled(opts);
199
257
  const walked = walk(state, 0, opts, /* @__PURE__ */ new WeakSet());
200
258
  return walked === OMIT ? void 0 : walked;
201
259
  }
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- export { compress } from './chunk-VBOCWPM5.mjs';
1
+ export { compress } from './chunk-X2A2RYP4.mjs';
2
2
  //# sourceMappingURL=index.mjs.map
3
3
  //# sourceMappingURL=index.mjs.map
@@ -120,6 +120,14 @@ function isEmptyValue(value) {
120
120
  if (isPlainObject(value)) return Object.keys(value).length === 0;
121
121
  return false;
122
122
  }
123
+ function coerceRedactedValue(value) {
124
+ if (value === null || value === void 0) return DEFAULTS.redactedValue;
125
+ try {
126
+ return String(value);
127
+ } catch {
128
+ return DEFAULTS.redactedValue;
129
+ }
130
+ }
123
131
  function resolveOptions(options) {
124
132
  return {
125
133
  maxDepth: options.maxDepth ?? DEFAULTS.maxDepth,
@@ -129,7 +137,7 @@ function resolveOptions(options) {
129
137
  sanitize: options.sanitize ?? DEFAULTS.sanitize,
130
138
  defaultSanitize: options.defaultSanitize ?? DEFAULTS.defaultSanitize,
131
139
  sanitizeMode: options.sanitizeMode ?? DEFAULTS.sanitizeMode,
132
- redactedValue: options.redactedValue ?? DEFAULTS.redactedValue
140
+ redactedValue: coerceRedactedValue(options.redactedValue)
133
141
  };
134
142
  }
135
143
  function walkObjectInto(out, source, depth, opts, seen) {
@@ -161,27 +169,67 @@ function walk(value, depth, opts, seen) {
161
169
  if (type !== "object") return value;
162
170
  const ref = value;
163
171
  if (seen.has(ref)) return CIRCULAR;
164
- if (value instanceof Date) return value;
172
+ if (value instanceof Date) return new Date(value.getTime());
173
+ if (value instanceof RegExp) {
174
+ try {
175
+ return value.toString();
176
+ } catch {
177
+ return GETTER_ERROR;
178
+ }
179
+ }
180
+ if (value instanceof Error) {
181
+ try {
182
+ return walk({ name: value.name, message: value.message }, depth, opts, seen);
183
+ } catch {
184
+ return GETTER_ERROR;
185
+ }
186
+ }
187
+ if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
188
+ try {
189
+ return walk(Array.from(value), depth, opts, seen);
190
+ } catch {
191
+ return GETTER_ERROR;
192
+ }
193
+ }
165
194
  if (value instanceof Map) {
166
195
  seen.add(ref);
167
196
  const obj = {};
168
- for (const [k, v] of value) safeAssign(obj, String(k), v);
197
+ try {
198
+ for (const [k, v] of value) safeAssign(obj, String(k), v);
199
+ } catch {
200
+ seen.delete(ref);
201
+ return GETTER_ERROR;
202
+ }
169
203
  const result = walk(obj, depth, opts, seen);
170
204
  seen.delete(ref);
171
205
  return result;
172
206
  }
173
207
  if (value instanceof Set) {
174
208
  seen.add(ref);
175
- const result = walk(Array.from(value), depth, opts, seen);
209
+ let items;
210
+ try {
211
+ items = Array.from(value);
212
+ } catch {
213
+ seen.delete(ref);
214
+ return GETTER_ERROR;
215
+ }
216
+ const result = walk(items, depth, opts, seen);
176
217
  seen.delete(ref);
177
218
  return result;
178
219
  }
179
220
  if (Array.isArray(value)) {
180
221
  if (depth >= opts.maxDepth) return TRUNCATED_ARRAY;
181
222
  seen.add(ref);
182
- const limited = value.length > opts.maxArrayLength ? value.slice(0, opts.maxArrayLength) : value;
223
+ const cap = Math.min(value.length, opts.maxArrayLength);
183
224
  const out2 = [];
184
- for (const item of limited) {
225
+ for (let i = 0; i < cap; i++) {
226
+ let item;
227
+ try {
228
+ item = value[i];
229
+ } catch {
230
+ out2.push(GETTER_ERROR);
231
+ continue;
232
+ }
185
233
  const walked = walk(item, depth + 1, opts, seen);
186
234
  out2.push(walked === OMIT ? null : walked);
187
235
  }
@@ -198,8 +246,18 @@ function walk(value, depth, opts, seen) {
198
246
  seen.delete(ref);
199
247
  return out;
200
248
  }
249
+ function warnIfRedactionDisabled(opts) {
250
+ if (opts.defaultSanitize || opts.sanitize.length > 0) return;
251
+ const proc = globalThis.process;
252
+ if (typeof proc !== "undefined" && proc.env?.NODE_ENV !== "production") {
253
+ console.warn(
254
+ "react-context-compressor: redaction is fully disabled (defaultSanitize:false and no sanitize matchers); sensitive fields will NOT be redacted."
255
+ );
256
+ }
257
+ }
201
258
  function compressCore(state, options = {}) {
202
259
  const opts = resolveOptions(options);
260
+ warnIfRedactionDisabled(opts);
203
261
  const walked = walk(state, 0, opts, /* @__PURE__ */ new WeakSet());
204
262
  return walked === OMIT ? void 0 : walked;
205
263
  }
@@ -1,4 +1,4 @@
1
- import { compress } from '../chunk-VBOCWPM5.mjs';
1
+ import { compress } from '../chunk-X2A2RYP4.mjs';
2
2
  import { useMemo } from 'react';
3
3
 
4
4
  // src/react/signature.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-context-compressor",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Mechanical, zero-dependency client-side compressor + sanitizer that turns React/JS app state into a minimal, safe payload for LLMs.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -29,7 +29,8 @@
29
29
  ],
30
30
  "sideEffects": false,
31
31
  "files": [
32
- "dist"
32
+ "dist",
33
+ "!dist/**/*.map"
33
34
  ],
34
35
  "main": "./dist/index.cjs",
35
36
  "module": "./dist/index.mjs",
@@ -67,6 +68,8 @@
67
68
  "test:watch": "vitest",
68
69
  "test:cov": "vitest run --coverage",
69
70
  "size": "size-limit",
71
+ "attw": "attw --pack",
72
+ "publint": "publint",
70
73
  "changeset": "changeset",
71
74
  "prepublishOnly": "npm run build"
72
75
  },
@@ -85,6 +88,7 @@
85
88
  "node": ">=18"
86
89
  },
87
90
  "devDependencies": {
91
+ "@arethetypeswrong/cli": "^0.18.3",
88
92
  "@biomejs/biome": "^2.5.0",
89
93
  "@changesets/cli": "^2.31.0",
90
94
  "@size-limit/preset-small-lib": "^12.1.0",
@@ -93,6 +97,7 @@
93
97
  "@types/react-dom": "^19.2.3",
94
98
  "@vitest/coverage-v8": "^4.1.9",
95
99
  "jsdom": "^29.1.1",
100
+ "publint": "^0.3.21",
96
101
  "react": "^19.2.7",
97
102
  "react-dom": "^19.2.7",
98
103
  "size-limit": "^12.1.0",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/sanitize.ts","../src/core/compress.ts","../src/index.ts"],"names":["out"],"mappings":"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","file":"chunk-VBOCWPM5.mjs","sourcesContent":["/**\n * Client-side data-safety layer — detect sensitive field names so the walker\n * can redact or remove them BEFORE their values are ever read. Mechanical and\n * key-name-driven: no value inspection, no network, no models.\n *\n * Limitations (key-name-driven by design):\n * - Matches field NAMES, not values: a secret under an innocuous key, or as a\n * bare array/Set element (no key), is NOT detected.\n * - Only own enumerable string keys are processed; Symbol-keyed / non-enumerable\n * properties are dropped by the walker, never scanned.\n * - Homoglyph attacks (e.g. Cyrillic look-alikes) are not fully closed; keys are\n * NFKC-normalized and zero-width-stripped before matching, which defeats the\n * common fullwidth/zero-width evasions but not deliberate confusables.\n */\n\n/** Default replacement value used when a sensitive field is redacted. */\nexport const REDACTED = \"[REDACTED]\";\n\n/**\n * Built-in deny-list of sensitive field-name patterns (case-insensitive).\n * Patterns are anchored to avoid false positives on common keys — e.g. it does\n * NOT redact `author`, `dashboard`, `secretary`, `tokenCount`, `promptTokens`,\n * or `accessKeyboard`.\n */\nexport const DEFAULT_DENY_LIST: readonly RegExp[] = [\n // Passwords / pass-phrases\n /pass(?:word|wd|phrase)/i,\n // Secrets (\"secret\", \"clientSecret\", \"secretKey\") but not \"secretary\"\n /secret(?!ary)/i,\n // Tokens — standalone or with an auth-ish prefix; NOT tokenCount/tokenize/promptTokens\n /\\btoken\\b/i,\n /(?:access|refresh|id|auth|bearer|api|csrf|xsrf|session|sso|oauth|reset|verification|activation)[-_]?token/i,\n // Keys (\"apiKey\", \"accessKey\", \"signingKey\") but not \"accessKeyword\"/\"keyboard\"\n /(?:api|access|private|signing|encryption)[-_]?keys?(?![a-z])/i,\n /\\bjwt\\b/i,\n /authorization/i,\n /bearer/i,\n /credentials?/i,\n /cookie/i,\n /session[-_]?(?:id|token|key)/i,\n // 2FA / recovery\n /\\b(?:otp|totp|mfa|2fa)\\b/i,\n /(?:recovery|backup)[-_]?codes?/i,\n /\\bmnemonic\\b/i,\n /seed[-_]?phrase/i,\n // Crypto / signing\n /\\bhmac\\b/i,\n /\\bsignature\\b/i,\n /\\b[cx]srf\\b/i,\n // Connection strings / DB credentials\n /connection[-_]?string/i,\n /\\bdsn\\b/i,\n /(?:database|db)[-_]?(?:url|uri)/i,\n // PII / financial\n /ssn/i,\n /social[-_]?security[-_]?(?:number|no)?/i,\n /credit[-_]?card/i,\n /card[-_]?number/i,\n /cvv2?/i,\n /\\bpin\\b/i,\n /\\biban\\b/i,\n /routing[-_]?number/i,\n /account[-_]?number/i,\n /\\bpassport\\b/i,\n /tax[-_]?id/i,\n];\n\n/** Zero-width / default-ignorable code points used to evade name matching. */\nconst ZERO_WIDTH = /[\\u00AD\\u200B-\\u200D\\u2060\\uFEFF]/g;\n\n/**\n * Normalize a key before matching: NFKC folds fullwidth/compatibility forms to\n * their ASCII equivalents, and zero-width characters are stripped. This defeats\n * the common `\"password\"` / zero-width-injected evasions.\n */\nexport function normalizeKey(key: string): string {\n return key.normalize(\"NFKC\").replace(ZERO_WIDTH, \"\");\n}\n\n/**\n * Stateless RegExp test. A user-supplied pattern with the `g`/`y` flag carries\n * a mutable `lastIndex`; resetting it keeps matching deterministic across keys.\n */\nexport function regexTest(re: RegExp, value: string): boolean {\n if (re.global || re.sticky) re.lastIndex = 0;\n return re.test(value);\n}\n\n/**\n * Is `key` a sensitive field name? Checks user matchers (string = exact,\n * case-insensitive; RegExp = pattern) and, when `useDefaults`, the built-in\n * deny-list. Keys are NFKC-normalized and zero-width-stripped first.\n */\nexport function isSensitiveKey(\n key: string,\n userMatchers: ReadonlyArray<string | RegExp>,\n useDefaults: boolean,\n): boolean {\n const normalized = normalizeKey(key);\n const lower = normalized.toLowerCase();\n for (const matcher of userMatchers) {\n if (typeof matcher === \"string\") {\n if (normalizeKey(matcher).toLowerCase() === lower) return true;\n } else if (regexTest(matcher, normalized)) {\n return true;\n }\n }\n if (useDefaults) {\n for (const re of DEFAULT_DENY_LIST) {\n if (regexTest(re, normalized)) return true;\n }\n }\n return false;\n}\n","/**\n * Core compression walker — mechanical, deterministic, zero-dependency.\n *\n * A single recursive pass applies: key stripping, depth capping, array-length\n * capping, and empty-value dropping, with circular-reference protection.\n * Sanitization (task 003) composes into this same walk via {@link CompressOptions.sanitize}.\n */\n\nimport type { CompressOptions } from \"../index\";\nimport { isSensitiveKey, REDACTED, regexTest } from \"./sanitize\";\n\n/** Marker substituted for a node that exceeds {@link CompressOptions.maxDepth}. */\nexport const TRUNCATED_OBJECT = \"[Object]\";\n/** Marker substituted for an array that exceeds {@link CompressOptions.maxDepth}. */\nexport const TRUNCATED_ARRAY = \"[Array]\";\n/** Marker substituted for a circular back-reference. */\nexport const CIRCULAR = \"[Circular]\";\n/** Marker substituted for a property whose getter threw when read. */\nexport const GETTER_ERROR = \"[Getter]\";\n\n/**\n * Safe default depth cap. Real application state is rarely deeper than a few\n * dozen levels; capping by default keeps payloads minimal and prevents a\n * stack-overflow DoS on pathologically deep (untrusted) input. Opt out with\n * `maxDepth: Infinity`.\n */\nexport const DEFAULT_MAX_DEPTH = 100;\n\n/** Fully-resolved options after defaults are applied. */\ninterface ResolvedOptions {\n maxDepth: number;\n maxArrayLength: number;\n strip: Array<string | RegExp>;\n dropEmpty: boolean;\n sanitize: Array<string | RegExp>;\n defaultSanitize: boolean;\n sanitizeMode: \"redact\" | \"remove\";\n redactedValue: string;\n}\n\nconst DEFAULTS: ResolvedOptions = {\n maxDepth: DEFAULT_MAX_DEPTH,\n maxArrayLength: Number.POSITIVE_INFINITY,\n strip: [],\n dropEmpty: false,\n sanitize: [],\n defaultSanitize: true,\n sanitizeMode: \"redact\",\n redactedValue: REDACTED,\n};\n\n/** A unique sentinel meaning \"this value should be omitted from the output\". */\nconst OMIT = Symbol(\"omit\");\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Assign an own, enumerable data property — even for dangerous keys like\n * `__proto__` (a plain `out[key] = v` would reassign the prototype instead of\n * creating a property, corrupting the output and silently dropping the value).\n */\nfunction safeAssign(target: Record<string, unknown>, key: string, value: unknown): void {\n Object.defineProperty(target, key, {\n value,\n writable: true,\n enumerable: true,\n configurable: true,\n });\n}\n\n/** Does `key` match any matcher (exact string or RegExp test)? */\nexport function keyMatches(key: string, matchers: ReadonlyArray<string | RegExp>): boolean {\n for (const matcher of matchers) {\n if (typeof matcher === \"string\") {\n if (matcher === key) return true;\n } else if (regexTest(matcher, key)) {\n return true;\n }\n }\n return false;\n}\n\nfunction isEmptyValue(value: unknown): boolean {\n if (value === null || value === undefined || value === \"\") return true;\n if (Array.isArray(value)) return value.length === 0;\n if (isPlainObject(value)) return Object.keys(value).length === 0;\n return false;\n}\n\n/**\n * Resolve user options against defaults. Exposed so the React layer and\n * sanitization can share one normalization path.\n */\nexport function resolveOptions(options: CompressOptions): ResolvedOptions {\n return {\n maxDepth: options.maxDepth ?? DEFAULTS.maxDepth,\n maxArrayLength: options.maxArrayLength ?? DEFAULTS.maxArrayLength,\n strip: options.strip ?? DEFAULTS.strip,\n dropEmpty: options.dropEmpty ?? DEFAULTS.dropEmpty,\n sanitize: options.sanitize ?? DEFAULTS.sanitize,\n defaultSanitize: options.defaultSanitize ?? DEFAULTS.defaultSanitize,\n sanitizeMode: options.sanitizeMode ?? DEFAULTS.sanitizeMode,\n redactedValue: options.redactedValue ?? DEFAULTS.redactedValue,\n };\n}\n\n/** Walk own enumerable string keys of an object-like value into `out`. */\nfunction walkObjectInto(\n out: Record<string, unknown>,\n source: Record<string, unknown>,\n depth: number,\n opts: ResolvedOptions,\n seen: WeakSet<object>,\n): void {\n for (const key of Object.keys(source)) {\n if (keyMatches(key, opts.strip)) continue;\n // Sanitize BEFORE reading the value: a sensitive value is never read\n // (no getter fired), never walked, and never reaches the output.\n if (isSensitiveKey(key, opts.sanitize, opts.defaultSanitize)) {\n if (opts.sanitizeMode === \"remove\") continue;\n safeAssign(out, key, opts.redactedValue);\n continue;\n }\n let raw: unknown;\n try {\n raw = source[key];\n } catch {\n // A getter threw — degrade to a marker rather than crashing the walk.\n safeAssign(out, key, GETTER_ERROR);\n continue;\n }\n const walked = walk(raw, depth + 1, opts, seen);\n if (walked === OMIT) continue;\n if (opts.dropEmpty && isEmptyValue(walked)) continue;\n safeAssign(out, key, walked);\n }\n}\n\nfunction walk(\n value: unknown,\n depth: number,\n opts: ResolvedOptions,\n seen: WeakSet<object>,\n): unknown {\n if (value === null) return null;\n const type = typeof value;\n if (type === \"function\" || type === \"symbol\") return OMIT;\n // BigInt is not JSON-serializable; coerce to string for an LLM-ready payload.\n if (type === \"bigint\") return (value as bigint).toString();\n if (type !== \"object\") return value;\n\n // Circular-reference guard sits above all object normalization so a cycle\n // through a Map/Set is caught instead of overflowing the stack.\n const ref = value as object;\n if (seen.has(ref)) return CIRCULAR;\n\n // Non-plain objects we deliberately normalize for a predictable payload.\n if (value instanceof Date) return value;\n if (value instanceof Map) {\n seen.add(ref);\n const obj: Record<string, unknown> = {};\n for (const [k, v] of value) safeAssign(obj, String(k), v);\n const result = walk(obj, depth, opts, seen);\n seen.delete(ref);\n return result;\n }\n if (value instanceof Set) {\n seen.add(ref);\n const result = walk(Array.from(value), depth, opts, seen);\n seen.delete(ref);\n return result;\n }\n\n if (Array.isArray(value)) {\n if (depth >= opts.maxDepth) return TRUNCATED_ARRAY;\n seen.add(ref);\n const limited =\n value.length > opts.maxArrayLength ? value.slice(0, opts.maxArrayLength) : value;\n const out: unknown[] = [];\n for (const item of limited) {\n const walked = walk(item, depth + 1, opts, seen);\n // In arrays, an omitted item collapses to null to preserve index intent.\n out.push(walked === OMIT ? null : walked);\n }\n if (value.length > opts.maxArrayLength) {\n out.push(`[+${value.length - opts.maxArrayLength} more]`);\n }\n seen.delete(ref);\n return out;\n }\n\n // Plain objects and unknown object kinds (class instances) both rebuild as a\n // plain object from their own enumerable string keys.\n if (depth >= opts.maxDepth) return TRUNCATED_OBJECT;\n seen.add(ref);\n const out: Record<string, unknown> = {};\n walkObjectInto(out, value as Record<string, unknown>, depth, opts, seen);\n seen.delete(ref);\n return out;\n}\n\n/**\n * Mechanically compress a state value into a minimal payload, applying the\n * structural transforms in {@link CompressOptions}. Pure and deterministic;\n * the input is never mutated.\n */\nexport function compressCore(state: unknown, options: CompressOptions = {}): unknown {\n const opts = resolveOptions(options);\n const walked = walk(state, 0, opts, new WeakSet());\n // A top-level function/symbol compresses to undefined rather than a sentinel.\n return walked === OMIT ? undefined : walked;\n}\n","/**\n * react-context-compressor — core entry (framework-agnostic, zero dependencies).\n *\n * Mechanical compression lands here (task 002); sanitization composes into the\n * same walk in task 003.\n */\n\nimport { compressCore } from \"./core/compress\";\n\n/**\n * Options controlling how a state value is mechanically compressed and\n * sanitized into a minimal, LLM-ready payload. All fields are optional;\n * `compress(state)` with no options returns a safe deep copy.\n */\nexport interface CompressOptions {\n /**\n * Maximum object/array depth to retain. Nodes deeper than this are replaced\n * with a `\"[Object]\"` / `\"[Array]\"` marker. Default: `100` (a safe cap that\n * keeps payloads minimal and prevents stack overflow on pathologically deep\n * input). Set to `Infinity` to disable depth capping.\n */\n maxDepth?: number;\n /**\n * Maximum array length to retain. Longer arrays are truncated and a\n * `\"[+N more]\"` marker is appended. Default: unlimited.\n */\n maxArrayLength?: number;\n /** Keys (exact strings or patterns) to strip from the output. */\n strip?: Array<string | RegExp>;\n /** When true, drop `null` / `undefined` / `\"\"` / `[]` / `{}` values. */\n dropEmpty?: boolean;\n /**\n * Extra sensitive field-name matchers to redact/remove, IN ADDITION to the\n * built-in deny-list (unless {@link CompressOptions.defaultSanitize} is\n * `false`). A `string` matches a key case-insensitively and exactly; a\n * `RegExp` matches by pattern. Matching is by field NAME, not value.\n */\n sanitize?: Array<string | RegExp>;\n /**\n * Apply the built-in sensitive-field deny-list (password, token, secret,\n * apiKey, authorization, cookie, ssn, creditCard, …). Default: `true`.\n * Set `false` to rely solely on {@link CompressOptions.sanitize}.\n */\n defaultSanitize?: boolean;\n /**\n * How to treat a sensitive field: `\"redact\"` replaces its value with\n * {@link CompressOptions.redactedValue}; `\"remove\"` drops the key entirely.\n * Either way the value is never read or emitted. Default: `\"redact\"`.\n */\n sanitizeMode?: \"redact\" | \"remove\";\n /** Replacement used when `sanitizeMode` is `\"redact\"`. Default: `\"[REDACTED]\"`. */\n redactedValue?: string;\n}\n\n/**\n * Mechanically compress and sanitize a state value into a minimal, safe payload.\n *\n * Pure and deterministic: the same input and options always produce the same\n * output, and the input is never mutated. Performs no I/O — purely structural.\n */\nexport function compress<T>(state: T, options: CompressOptions = {}): unknown {\n return compressCore(state, options);\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/core/sanitize.ts","../src/core/compress.ts","../src/index.ts"],"names":["out"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Client-side data-safety layer — detect sensitive field names so the walker\n * can redact or remove them BEFORE their values are ever read. Mechanical and\n * key-name-driven: no value inspection, no network, no models.\n *\n * Limitations (key-name-driven by design):\n * - Matches field NAMES, not values: a secret under an innocuous key, or as a\n * bare array/Set element (no key), is NOT detected.\n * - Only own enumerable string keys are processed; Symbol-keyed / non-enumerable\n * properties are dropped by the walker, never scanned.\n * - Homoglyph attacks (e.g. Cyrillic look-alikes) are not fully closed; keys are\n * NFKC-normalized and zero-width-stripped before matching, which defeats the\n * common fullwidth/zero-width evasions but not deliberate confusables.\n */\n\n/** Default replacement value used when a sensitive field is redacted. */\nexport const REDACTED = \"[REDACTED]\";\n\n/**\n * Built-in deny-list of sensitive field-name patterns (case-insensitive).\n * Patterns are anchored to avoid false positives on common keys — e.g. it does\n * NOT redact `author`, `dashboard`, `secretary`, `tokenCount`, `promptTokens`,\n * or `accessKeyboard`.\n */\nexport const DEFAULT_DENY_LIST: readonly RegExp[] = [\n // Passwords / pass-phrases\n /pass(?:word|wd|phrase)/i,\n // Secrets (\"secret\", \"clientSecret\", \"secretKey\") but not \"secretary\"\n /secret(?!ary)/i,\n // Tokens — standalone or with an auth-ish prefix; NOT tokenCount/tokenize/promptTokens\n /\\btoken\\b/i,\n /(?:access|refresh|id|auth|bearer|api|csrf|xsrf|session|sso|oauth|reset|verification|activation)[-_]?token/i,\n // Keys (\"apiKey\", \"accessKey\", \"signingKey\") but not \"accessKeyword\"/\"keyboard\"\n /(?:api|access|private|signing|encryption)[-_]?keys?(?![a-z])/i,\n /\\bjwt\\b/i,\n /authorization/i,\n /bearer/i,\n /credentials?/i,\n /cookie/i,\n /session[-_]?(?:id|token|key)/i,\n // 2FA / recovery\n /\\b(?:otp|totp|mfa|2fa)\\b/i,\n /(?:recovery|backup)[-_]?codes?/i,\n /\\bmnemonic\\b/i,\n /seed[-_]?phrase/i,\n // Crypto / signing\n /\\bhmac\\b/i,\n /\\bsignature\\b/i,\n /\\b[cx]srf\\b/i,\n // Connection strings / DB credentials\n /connection[-_]?string/i,\n /\\bdsn\\b/i,\n /(?:database|db)[-_]?(?:url|uri)/i,\n // PII / financial\n /ssn/i,\n /social[-_]?security[-_]?(?:number|no)?/i,\n /credit[-_]?card/i,\n /card[-_]?number/i,\n /cvv2?/i,\n /\\bpin\\b/i,\n /\\biban\\b/i,\n /routing[-_]?number/i,\n /account[-_]?number/i,\n /\\bpassport\\b/i,\n /tax[-_]?id/i,\n];\n\n/** Zero-width / default-ignorable code points used to evade name matching. */\nconst ZERO_WIDTH = /[\\u00AD\\u200B-\\u200D\\u2060\\uFEFF]/g;\n\n/**\n * Normalize a key before matching: NFKC folds fullwidth/compatibility forms to\n * their ASCII equivalents, and zero-width characters are stripped. This defeats\n * the common `\"password\"` / zero-width-injected evasions.\n */\nexport function normalizeKey(key: string): string {\n return key.normalize(\"NFKC\").replace(ZERO_WIDTH, \"\");\n}\n\n/**\n * Stateless RegExp test. A user-supplied pattern with the `g`/`y` flag carries\n * a mutable `lastIndex`; resetting it keeps matching deterministic across keys.\n */\nexport function regexTest(re: RegExp, value: string): boolean {\n if (re.global || re.sticky) re.lastIndex = 0;\n return re.test(value);\n}\n\n/**\n * Is `key` a sensitive field name? Checks user matchers (string = exact,\n * case-insensitive; RegExp = pattern) and, when `useDefaults`, the built-in\n * deny-list. Keys are NFKC-normalized and zero-width-stripped first.\n */\nexport function isSensitiveKey(\n key: string,\n userMatchers: ReadonlyArray<string | RegExp>,\n useDefaults: boolean,\n): boolean {\n const normalized = normalizeKey(key);\n const lower = normalized.toLowerCase();\n for (const matcher of userMatchers) {\n if (typeof matcher === \"string\") {\n if (normalizeKey(matcher).toLowerCase() === lower) return true;\n } else if (regexTest(matcher, normalized)) {\n return true;\n }\n }\n if (useDefaults) {\n for (const re of DEFAULT_DENY_LIST) {\n if (regexTest(re, normalized)) return true;\n }\n }\n return false;\n}\n","/**\n * Core compression walker — mechanical, deterministic, zero-dependency.\n *\n * A single recursive pass applies: key stripping, depth capping, array-length\n * capping, and empty-value dropping, with circular-reference protection.\n * Sanitization (task 003) composes into this same walk via {@link CompressOptions.sanitize}.\n */\n\nimport type { CompressOptions } from \"../index\";\nimport { isSensitiveKey, REDACTED, regexTest } from \"./sanitize\";\n\n/** Marker substituted for a node that exceeds {@link CompressOptions.maxDepth}. */\nexport const TRUNCATED_OBJECT = \"[Object]\";\n/** Marker substituted for an array that exceeds {@link CompressOptions.maxDepth}. */\nexport const TRUNCATED_ARRAY = \"[Array]\";\n/** Marker substituted for a circular back-reference. */\nexport const CIRCULAR = \"[Circular]\";\n/** Marker substituted for a property whose getter threw when read. */\nexport const GETTER_ERROR = \"[Getter]\";\n\n/**\n * Safe default depth cap. Real application state is rarely deeper than a few\n * dozen levels; capping by default keeps payloads minimal and prevents a\n * stack-overflow DoS on pathologically deep (untrusted) input. Opt out with\n * `maxDepth: Infinity`.\n */\nexport const DEFAULT_MAX_DEPTH = 100;\n\n/** Fully-resolved options after defaults are applied. */\ninterface ResolvedOptions {\n maxDepth: number;\n maxArrayLength: number;\n strip: Array<string | RegExp>;\n dropEmpty: boolean;\n sanitize: Array<string | RegExp>;\n defaultSanitize: boolean;\n sanitizeMode: \"redact\" | \"remove\";\n redactedValue: string;\n}\n\nconst DEFAULTS: ResolvedOptions = {\n maxDepth: DEFAULT_MAX_DEPTH,\n maxArrayLength: Number.POSITIVE_INFINITY,\n strip: [],\n dropEmpty: false,\n sanitize: [],\n defaultSanitize: true,\n sanitizeMode: \"redact\",\n redactedValue: REDACTED,\n};\n\n/** A unique sentinel meaning \"this value should be omitted from the output\". */\nconst OMIT = Symbol(\"omit\");\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Assign an own, enumerable data property — even for dangerous keys like\n * `__proto__` (a plain `out[key] = v` would reassign the prototype instead of\n * creating a property, corrupting the output and silently dropping the value).\n */\nfunction safeAssign(target: Record<string, unknown>, key: string, value: unknown): void {\n Object.defineProperty(target, key, {\n value,\n writable: true,\n enumerable: true,\n configurable: true,\n });\n}\n\n/** Does `key` match any matcher (exact string or RegExp test)? */\nexport function keyMatches(key: string, matchers: ReadonlyArray<string | RegExp>): boolean {\n for (const matcher of matchers) {\n if (typeof matcher === \"string\") {\n if (matcher === key) return true;\n } else if (regexTest(matcher, key)) {\n return true;\n }\n }\n return false;\n}\n\nfunction isEmptyValue(value: unknown): boolean {\n if (value === null || value === undefined || value === \"\") return true;\n if (Array.isArray(value)) return value.length === 0;\n if (isPlainObject(value)) return Object.keys(value).length === 0;\n return false;\n}\n\n/**\n * Resolve user options against defaults. Exposed so the React layer and\n * sanitization can share one normalization path.\n */\nexport function resolveOptions(options: CompressOptions): ResolvedOptions {\n return {\n maxDepth: options.maxDepth ?? DEFAULTS.maxDepth,\n maxArrayLength: options.maxArrayLength ?? DEFAULTS.maxArrayLength,\n strip: options.strip ?? DEFAULTS.strip,\n dropEmpty: options.dropEmpty ?? DEFAULTS.dropEmpty,\n sanitize: options.sanitize ?? DEFAULTS.sanitize,\n defaultSanitize: options.defaultSanitize ?? DEFAULTS.defaultSanitize,\n sanitizeMode: options.sanitizeMode ?? DEFAULTS.sanitizeMode,\n redactedValue: options.redactedValue ?? DEFAULTS.redactedValue,\n };\n}\n\n/** Walk own enumerable string keys of an object-like value into `out`. */\nfunction walkObjectInto(\n out: Record<string, unknown>,\n source: Record<string, unknown>,\n depth: number,\n opts: ResolvedOptions,\n seen: WeakSet<object>,\n): void {\n for (const key of Object.keys(source)) {\n if (keyMatches(key, opts.strip)) continue;\n // Sanitize BEFORE reading the value: a sensitive value is never read\n // (no getter fired), never walked, and never reaches the output.\n if (isSensitiveKey(key, opts.sanitize, opts.defaultSanitize)) {\n if (opts.sanitizeMode === \"remove\") continue;\n safeAssign(out, key, opts.redactedValue);\n continue;\n }\n let raw: unknown;\n try {\n raw = source[key];\n } catch {\n // A getter threw — degrade to a marker rather than crashing the walk.\n safeAssign(out, key, GETTER_ERROR);\n continue;\n }\n const walked = walk(raw, depth + 1, opts, seen);\n if (walked === OMIT) continue;\n if (opts.dropEmpty && isEmptyValue(walked)) continue;\n safeAssign(out, key, walked);\n }\n}\n\nfunction walk(\n value: unknown,\n depth: number,\n opts: ResolvedOptions,\n seen: WeakSet<object>,\n): unknown {\n if (value === null) return null;\n const type = typeof value;\n if (type === \"function\" || type === \"symbol\") return OMIT;\n // BigInt is not JSON-serializable; coerce to string for an LLM-ready payload.\n if (type === \"bigint\") return (value as bigint).toString();\n if (type !== \"object\") return value;\n\n // Circular-reference guard sits above all object normalization so a cycle\n // through a Map/Set is caught instead of overflowing the stack.\n const ref = value as object;\n if (seen.has(ref)) return CIRCULAR;\n\n // Non-plain objects we deliberately normalize for a predictable payload.\n if (value instanceof Date) return value;\n if (value instanceof Map) {\n seen.add(ref);\n const obj: Record<string, unknown> = {};\n for (const [k, v] of value) safeAssign(obj, String(k), v);\n const result = walk(obj, depth, opts, seen);\n seen.delete(ref);\n return result;\n }\n if (value instanceof Set) {\n seen.add(ref);\n const result = walk(Array.from(value), depth, opts, seen);\n seen.delete(ref);\n return result;\n }\n\n if (Array.isArray(value)) {\n if (depth >= opts.maxDepth) return TRUNCATED_ARRAY;\n seen.add(ref);\n const limited =\n value.length > opts.maxArrayLength ? value.slice(0, opts.maxArrayLength) : value;\n const out: unknown[] = [];\n for (const item of limited) {\n const walked = walk(item, depth + 1, opts, seen);\n // In arrays, an omitted item collapses to null to preserve index intent.\n out.push(walked === OMIT ? null : walked);\n }\n if (value.length > opts.maxArrayLength) {\n out.push(`[+${value.length - opts.maxArrayLength} more]`);\n }\n seen.delete(ref);\n return out;\n }\n\n // Plain objects and unknown object kinds (class instances) both rebuild as a\n // plain object from their own enumerable string keys.\n if (depth >= opts.maxDepth) return TRUNCATED_OBJECT;\n seen.add(ref);\n const out: Record<string, unknown> = {};\n walkObjectInto(out, value as Record<string, unknown>, depth, opts, seen);\n seen.delete(ref);\n return out;\n}\n\n/**\n * Mechanically compress a state value into a minimal payload, applying the\n * structural transforms in {@link CompressOptions}. Pure and deterministic;\n * the input is never mutated.\n */\nexport function compressCore(state: unknown, options: CompressOptions = {}): unknown {\n const opts = resolveOptions(options);\n const walked = walk(state, 0, opts, new WeakSet());\n // A top-level function/symbol compresses to undefined rather than a sentinel.\n return walked === OMIT ? undefined : walked;\n}\n","/**\n * react-context-compressor — core entry (framework-agnostic, zero dependencies).\n *\n * Mechanical compression lands here (task 002); sanitization composes into the\n * same walk in task 003.\n */\n\nimport { compressCore } from \"./core/compress\";\n\n/**\n * Options controlling how a state value is mechanically compressed and\n * sanitized into a minimal, LLM-ready payload. All fields are optional;\n * `compress(state)` with no options returns a safe deep copy.\n */\nexport interface CompressOptions {\n /**\n * Maximum object/array depth to retain. Nodes deeper than this are replaced\n * with a `\"[Object]\"` / `\"[Array]\"` marker. Default: `100` (a safe cap that\n * keeps payloads minimal and prevents stack overflow on pathologically deep\n * input). Set to `Infinity` to disable depth capping.\n */\n maxDepth?: number;\n /**\n * Maximum array length to retain. Longer arrays are truncated and a\n * `\"[+N more]\"` marker is appended. Default: unlimited.\n */\n maxArrayLength?: number;\n /** Keys (exact strings or patterns) to strip from the output. */\n strip?: Array<string | RegExp>;\n /** When true, drop `null` / `undefined` / `\"\"` / `[]` / `{}` values. */\n dropEmpty?: boolean;\n /**\n * Extra sensitive field-name matchers to redact/remove, IN ADDITION to the\n * built-in deny-list (unless {@link CompressOptions.defaultSanitize} is\n * `false`). A `string` matches a key case-insensitively and exactly; a\n * `RegExp` matches by pattern. Matching is by field NAME, not value.\n */\n sanitize?: Array<string | RegExp>;\n /**\n * Apply the built-in sensitive-field deny-list (password, token, secret,\n * apiKey, authorization, cookie, ssn, creditCard, …). Default: `true`.\n * Set `false` to rely solely on {@link CompressOptions.sanitize}.\n */\n defaultSanitize?: boolean;\n /**\n * How to treat a sensitive field: `\"redact\"` replaces its value with\n * {@link CompressOptions.redactedValue}; `\"remove\"` drops the key entirely.\n * Either way the value is never read or emitted. Default: `\"redact\"`.\n */\n sanitizeMode?: \"redact\" | \"remove\";\n /** Replacement used when `sanitizeMode` is `\"redact\"`. Default: `\"[REDACTED]\"`. */\n redactedValue?: string;\n}\n\n/**\n * Mechanically compress and sanitize a state value into a minimal, safe payload.\n *\n * Pure and deterministic: the same input and options always produce the same\n * output, and the input is never mutated. Performs no I/O — purely structural.\n */\nexport function compress<T>(state: T, options: CompressOptions = {}): unknown {\n return compressCore(state, options);\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"index.mjs"}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/core/sanitize.ts","../../src/core/compress.ts","../../src/index.ts","../../src/react/signature.ts","../../src/react/index.ts"],"names":["out","useMemo"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Client-side data-safety layer — detect sensitive field names so the walker\n * can redact or remove them BEFORE their values are ever read. Mechanical and\n * key-name-driven: no value inspection, no network, no models.\n *\n * Limitations (key-name-driven by design):\n * - Matches field NAMES, not values: a secret under an innocuous key, or as a\n * bare array/Set element (no key), is NOT detected.\n * - Only own enumerable string keys are processed; Symbol-keyed / non-enumerable\n * properties are dropped by the walker, never scanned.\n * - Homoglyph attacks (e.g. Cyrillic look-alikes) are not fully closed; keys are\n * NFKC-normalized and zero-width-stripped before matching, which defeats the\n * common fullwidth/zero-width evasions but not deliberate confusables.\n */\n\n/** Default replacement value used when a sensitive field is redacted. */\nexport const REDACTED = \"[REDACTED]\";\n\n/**\n * Built-in deny-list of sensitive field-name patterns (case-insensitive).\n * Patterns are anchored to avoid false positives on common keys — e.g. it does\n * NOT redact `author`, `dashboard`, `secretary`, `tokenCount`, `promptTokens`,\n * or `accessKeyboard`.\n */\nexport const DEFAULT_DENY_LIST: readonly RegExp[] = [\n // Passwords / pass-phrases\n /pass(?:word|wd|phrase)/i,\n // Secrets (\"secret\", \"clientSecret\", \"secretKey\") but not \"secretary\"\n /secret(?!ary)/i,\n // Tokens — standalone or with an auth-ish prefix; NOT tokenCount/tokenize/promptTokens\n /\\btoken\\b/i,\n /(?:access|refresh|id|auth|bearer|api|csrf|xsrf|session|sso|oauth|reset|verification|activation)[-_]?token/i,\n // Keys (\"apiKey\", \"accessKey\", \"signingKey\") but not \"accessKeyword\"/\"keyboard\"\n /(?:api|access|private|signing|encryption)[-_]?keys?(?![a-z])/i,\n /\\bjwt\\b/i,\n /authorization/i,\n /bearer/i,\n /credentials?/i,\n /cookie/i,\n /session[-_]?(?:id|token|key)/i,\n // 2FA / recovery\n /\\b(?:otp|totp|mfa|2fa)\\b/i,\n /(?:recovery|backup)[-_]?codes?/i,\n /\\bmnemonic\\b/i,\n /seed[-_]?phrase/i,\n // Crypto / signing\n /\\bhmac\\b/i,\n /\\bsignature\\b/i,\n /\\b[cx]srf\\b/i,\n // Connection strings / DB credentials\n /connection[-_]?string/i,\n /\\bdsn\\b/i,\n /(?:database|db)[-_]?(?:url|uri)/i,\n // PII / financial\n /ssn/i,\n /social[-_]?security[-_]?(?:number|no)?/i,\n /credit[-_]?card/i,\n /card[-_]?number/i,\n /cvv2?/i,\n /\\bpin\\b/i,\n /\\biban\\b/i,\n /routing[-_]?number/i,\n /account[-_]?number/i,\n /\\bpassport\\b/i,\n /tax[-_]?id/i,\n];\n\n/** Zero-width / default-ignorable code points used to evade name matching. */\nconst ZERO_WIDTH = /[\\u00AD\\u200B-\\u200D\\u2060\\uFEFF]/g;\n\n/**\n * Normalize a key before matching: NFKC folds fullwidth/compatibility forms to\n * their ASCII equivalents, and zero-width characters are stripped. This defeats\n * the common `\"password\"` / zero-width-injected evasions.\n */\nexport function normalizeKey(key: string): string {\n return key.normalize(\"NFKC\").replace(ZERO_WIDTH, \"\");\n}\n\n/**\n * Stateless RegExp test. A user-supplied pattern with the `g`/`y` flag carries\n * a mutable `lastIndex`; resetting it keeps matching deterministic across keys.\n */\nexport function regexTest(re: RegExp, value: string): boolean {\n if (re.global || re.sticky) re.lastIndex = 0;\n return re.test(value);\n}\n\n/**\n * Is `key` a sensitive field name? Checks user matchers (string = exact,\n * case-insensitive; RegExp = pattern) and, when `useDefaults`, the built-in\n * deny-list. Keys are NFKC-normalized and zero-width-stripped first.\n */\nexport function isSensitiveKey(\n key: string,\n userMatchers: ReadonlyArray<string | RegExp>,\n useDefaults: boolean,\n): boolean {\n const normalized = normalizeKey(key);\n const lower = normalized.toLowerCase();\n for (const matcher of userMatchers) {\n if (typeof matcher === \"string\") {\n if (normalizeKey(matcher).toLowerCase() === lower) return true;\n } else if (regexTest(matcher, normalized)) {\n return true;\n }\n }\n if (useDefaults) {\n for (const re of DEFAULT_DENY_LIST) {\n if (regexTest(re, normalized)) return true;\n }\n }\n return false;\n}\n","/**\n * Core compression walker — mechanical, deterministic, zero-dependency.\n *\n * A single recursive pass applies: key stripping, depth capping, array-length\n * capping, and empty-value dropping, with circular-reference protection.\n * Sanitization (task 003) composes into this same walk via {@link CompressOptions.sanitize}.\n */\n\nimport type { CompressOptions } from \"../index\";\nimport { isSensitiveKey, REDACTED, regexTest } from \"./sanitize\";\n\n/** Marker substituted for a node that exceeds {@link CompressOptions.maxDepth}. */\nexport const TRUNCATED_OBJECT = \"[Object]\";\n/** Marker substituted for an array that exceeds {@link CompressOptions.maxDepth}. */\nexport const TRUNCATED_ARRAY = \"[Array]\";\n/** Marker substituted for a circular back-reference. */\nexport const CIRCULAR = \"[Circular]\";\n/** Marker substituted for a property whose getter threw when read. */\nexport const GETTER_ERROR = \"[Getter]\";\n\n/**\n * Safe default depth cap. Real application state is rarely deeper than a few\n * dozen levels; capping by default keeps payloads minimal and prevents a\n * stack-overflow DoS on pathologically deep (untrusted) input. Opt out with\n * `maxDepth: Infinity`.\n */\nexport const DEFAULT_MAX_DEPTH = 100;\n\n/** Fully-resolved options after defaults are applied. */\ninterface ResolvedOptions {\n maxDepth: number;\n maxArrayLength: number;\n strip: Array<string | RegExp>;\n dropEmpty: boolean;\n sanitize: Array<string | RegExp>;\n defaultSanitize: boolean;\n sanitizeMode: \"redact\" | \"remove\";\n redactedValue: string;\n}\n\nconst DEFAULTS: ResolvedOptions = {\n maxDepth: DEFAULT_MAX_DEPTH,\n maxArrayLength: Number.POSITIVE_INFINITY,\n strip: [],\n dropEmpty: false,\n sanitize: [],\n defaultSanitize: true,\n sanitizeMode: \"redact\",\n redactedValue: REDACTED,\n};\n\n/** A unique sentinel meaning \"this value should be omitted from the output\". */\nconst OMIT = Symbol(\"omit\");\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n\n/**\n * Assign an own, enumerable data property — even for dangerous keys like\n * `__proto__` (a plain `out[key] = v` would reassign the prototype instead of\n * creating a property, corrupting the output and silently dropping the value).\n */\nfunction safeAssign(target: Record<string, unknown>, key: string, value: unknown): void {\n Object.defineProperty(target, key, {\n value,\n writable: true,\n enumerable: true,\n configurable: true,\n });\n}\n\n/** Does `key` match any matcher (exact string or RegExp test)? */\nexport function keyMatches(key: string, matchers: ReadonlyArray<string | RegExp>): boolean {\n for (const matcher of matchers) {\n if (typeof matcher === \"string\") {\n if (matcher === key) return true;\n } else if (regexTest(matcher, key)) {\n return true;\n }\n }\n return false;\n}\n\nfunction isEmptyValue(value: unknown): boolean {\n if (value === null || value === undefined || value === \"\") return true;\n if (Array.isArray(value)) return value.length === 0;\n if (isPlainObject(value)) return Object.keys(value).length === 0;\n return false;\n}\n\n/**\n * Resolve user options against defaults. Exposed so the React layer and\n * sanitization can share one normalization path.\n */\nexport function resolveOptions(options: CompressOptions): ResolvedOptions {\n return {\n maxDepth: options.maxDepth ?? DEFAULTS.maxDepth,\n maxArrayLength: options.maxArrayLength ?? DEFAULTS.maxArrayLength,\n strip: options.strip ?? DEFAULTS.strip,\n dropEmpty: options.dropEmpty ?? DEFAULTS.dropEmpty,\n sanitize: options.sanitize ?? DEFAULTS.sanitize,\n defaultSanitize: options.defaultSanitize ?? DEFAULTS.defaultSanitize,\n sanitizeMode: options.sanitizeMode ?? DEFAULTS.sanitizeMode,\n redactedValue: options.redactedValue ?? DEFAULTS.redactedValue,\n };\n}\n\n/** Walk own enumerable string keys of an object-like value into `out`. */\nfunction walkObjectInto(\n out: Record<string, unknown>,\n source: Record<string, unknown>,\n depth: number,\n opts: ResolvedOptions,\n seen: WeakSet<object>,\n): void {\n for (const key of Object.keys(source)) {\n if (keyMatches(key, opts.strip)) continue;\n // Sanitize BEFORE reading the value: a sensitive value is never read\n // (no getter fired), never walked, and never reaches the output.\n if (isSensitiveKey(key, opts.sanitize, opts.defaultSanitize)) {\n if (opts.sanitizeMode === \"remove\") continue;\n safeAssign(out, key, opts.redactedValue);\n continue;\n }\n let raw: unknown;\n try {\n raw = source[key];\n } catch {\n // A getter threw — degrade to a marker rather than crashing the walk.\n safeAssign(out, key, GETTER_ERROR);\n continue;\n }\n const walked = walk(raw, depth + 1, opts, seen);\n if (walked === OMIT) continue;\n if (opts.dropEmpty && isEmptyValue(walked)) continue;\n safeAssign(out, key, walked);\n }\n}\n\nfunction walk(\n value: unknown,\n depth: number,\n opts: ResolvedOptions,\n seen: WeakSet<object>,\n): unknown {\n if (value === null) return null;\n const type = typeof value;\n if (type === \"function\" || type === \"symbol\") return OMIT;\n // BigInt is not JSON-serializable; coerce to string for an LLM-ready payload.\n if (type === \"bigint\") return (value as bigint).toString();\n if (type !== \"object\") return value;\n\n // Circular-reference guard sits above all object normalization so a cycle\n // through a Map/Set is caught instead of overflowing the stack.\n const ref = value as object;\n if (seen.has(ref)) return CIRCULAR;\n\n // Non-plain objects we deliberately normalize for a predictable payload.\n if (value instanceof Date) return value;\n if (value instanceof Map) {\n seen.add(ref);\n const obj: Record<string, unknown> = {};\n for (const [k, v] of value) safeAssign(obj, String(k), v);\n const result = walk(obj, depth, opts, seen);\n seen.delete(ref);\n return result;\n }\n if (value instanceof Set) {\n seen.add(ref);\n const result = walk(Array.from(value), depth, opts, seen);\n seen.delete(ref);\n return result;\n }\n\n if (Array.isArray(value)) {\n if (depth >= opts.maxDepth) return TRUNCATED_ARRAY;\n seen.add(ref);\n const limited =\n value.length > opts.maxArrayLength ? value.slice(0, opts.maxArrayLength) : value;\n const out: unknown[] = [];\n for (const item of limited) {\n const walked = walk(item, depth + 1, opts, seen);\n // In arrays, an omitted item collapses to null to preserve index intent.\n out.push(walked === OMIT ? null : walked);\n }\n if (value.length > opts.maxArrayLength) {\n out.push(`[+${value.length - opts.maxArrayLength} more]`);\n }\n seen.delete(ref);\n return out;\n }\n\n // Plain objects and unknown object kinds (class instances) both rebuild as a\n // plain object from their own enumerable string keys.\n if (depth >= opts.maxDepth) return TRUNCATED_OBJECT;\n seen.add(ref);\n const out: Record<string, unknown> = {};\n walkObjectInto(out, value as Record<string, unknown>, depth, opts, seen);\n seen.delete(ref);\n return out;\n}\n\n/**\n * Mechanically compress a state value into a minimal payload, applying the\n * structural transforms in {@link CompressOptions}. Pure and deterministic;\n * the input is never mutated.\n */\nexport function compressCore(state: unknown, options: CompressOptions = {}): unknown {\n const opts = resolveOptions(options);\n const walked = walk(state, 0, opts, new WeakSet());\n // A top-level function/symbol compresses to undefined rather than a sentinel.\n return walked === OMIT ? undefined : walked;\n}\n","/**\n * react-context-compressor — core entry (framework-agnostic, zero dependencies).\n *\n * Mechanical compression lands here (task 002); sanitization composes into the\n * same walk in task 003.\n */\n\nimport { compressCore } from \"./core/compress\";\n\n/**\n * Options controlling how a state value is mechanically compressed and\n * sanitized into a minimal, LLM-ready payload. All fields are optional;\n * `compress(state)` with no options returns a safe deep copy.\n */\nexport interface CompressOptions {\n /**\n * Maximum object/array depth to retain. Nodes deeper than this are replaced\n * with a `\"[Object]\"` / `\"[Array]\"` marker. Default: `100` (a safe cap that\n * keeps payloads minimal and prevents stack overflow on pathologically deep\n * input). Set to `Infinity` to disable depth capping.\n */\n maxDepth?: number;\n /**\n * Maximum array length to retain. Longer arrays are truncated and a\n * `\"[+N more]\"` marker is appended. Default: unlimited.\n */\n maxArrayLength?: number;\n /** Keys (exact strings or patterns) to strip from the output. */\n strip?: Array<string | RegExp>;\n /** When true, drop `null` / `undefined` / `\"\"` / `[]` / `{}` values. */\n dropEmpty?: boolean;\n /**\n * Extra sensitive field-name matchers to redact/remove, IN ADDITION to the\n * built-in deny-list (unless {@link CompressOptions.defaultSanitize} is\n * `false`). A `string` matches a key case-insensitively and exactly; a\n * `RegExp` matches by pattern. Matching is by field NAME, not value.\n */\n sanitize?: Array<string | RegExp>;\n /**\n * Apply the built-in sensitive-field deny-list (password, token, secret,\n * apiKey, authorization, cookie, ssn, creditCard, …). Default: `true`.\n * Set `false` to rely solely on {@link CompressOptions.sanitize}.\n */\n defaultSanitize?: boolean;\n /**\n * How to treat a sensitive field: `\"redact\"` replaces its value with\n * {@link CompressOptions.redactedValue}; `\"remove\"` drops the key entirely.\n * Either way the value is never read or emitted. Default: `\"redact\"`.\n */\n sanitizeMode?: \"redact\" | \"remove\";\n /** Replacement used when `sanitizeMode` is `\"redact\"`. Default: `\"[REDACTED]\"`. */\n redactedValue?: string;\n}\n\n/**\n * Mechanically compress and sanitize a state value into a minimal, safe payload.\n *\n * Pure and deterministic: the same input and options always produce the same\n * output, and the input is never mutated. Performs no I/O — purely structural.\n */\nexport function compress<T>(state: T, options: CompressOptions = {}): unknown {\n return compressCore(state, options);\n}\n","/**\n * Internal helper for the React hook: turn a {@link CompressOptions} object into\n * a stable string signature so an inline options literal (new reference every\n * render) doesn't thrash `useMemo`. Not part of the public `./react` API.\n */\nimport type { CompressOptions } from \"../index\";\n\n// Control-char delimiters that cannot appear unescaped in a JSON-encoded token,\n// so neither matcher contents nor free-text option values can forge a boundary.\nconst FIELD = \"\\u0001\";\nconst ITEM = \"\\u0000\";\n\n/** Unambiguous, collision-free key for one matcher (string or RegExp). */\nfunction matcherKey(m: string | RegExp): string {\n return typeof m === \"string\"\n ? `s:${JSON.stringify(m)}`\n : `r:${JSON.stringify(m.source)}:${m.flags}`;\n}\n\n/**\n * Build a stable signature from an options object. Two options objects with\n * equal content produce the same signature; any content difference (including\n * RegExp flags, matcher order, or `Infinity` vs unset) produces a different one.\n */\nexport function optionsSignature(o: CompressOptions): string {\n return [\n `d=${o.maxDepth ?? \"\"}`,\n `a=${o.maxArrayLength ?? \"\"}`,\n `e=${o.dropEmpty ?? \"\"}`,\n `ds=${o.defaultSanitize ?? \"\"}`,\n `sm=${o.sanitizeMode ?? \"\"}`,\n `rv=${JSON.stringify(o.redactedValue ?? \"\")}`,\n `st=${(o.strip ?? []).map(matcherKey).join(ITEM)}`,\n `sn=${(o.sanitize ?? []).map(matcherKey).join(ITEM)}`,\n ].join(FIELD);\n}\n","/**\n * react-context-compressor — React bindings entry.\n *\n * Imports only from the core entry and the `react` peer dependency, so the\n * core (`.`) entry stays React-free and the React layer tree-shakes away for\n * consumers who import only `.`.\n */\nimport { useMemo } from \"react\";\nimport { type CompressOptions, compress } from \"../index\";\nimport { optionsSignature } from \"./signature\";\n\n/**\n * React hook returning a memoized, compressed + sanitized view of `state`.\n *\n * Recomputes only when the `state` reference changes or the options content\n * changes (compared by a stable signature, so an inline options literal is\n * fine). Pure: no DOM access, no side effects — safe under SSR and React\n * Server Components. Works on React 17 / 18 / 19.\n */\nexport function useCompressedContext<T>(state: T, options: CompressOptions = {}): unknown {\n const signature = optionsSignature(options);\n // biome-ignore lint/correctness/useExhaustiveDependencies: `options` is keyed by its stable `signature`\n return useMemo(() => compress(state, options), [state, signature]);\n}\n\nexport type { CompressOptions };\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/react/signature.ts","../../src/react/index.ts"],"names":[],"mappings":";;;;AASA,IAAM,KAAA,GAAQ,GAAA;AACd,IAAM,IAAA,GAAO,IAAA;AAGb,SAAS,WAAW,CAAA,EAA4B;AAC9C,EAAA,OAAO,OAAO,CAAA,KAAM,QAAA,GAChB,CAAA,EAAA,EAAK,IAAA,CAAK,UAAU,CAAC,CAAC,CAAA,CAAA,GACtB,CAAA,EAAA,EAAK,KAAK,SAAA,CAAU,CAAA,CAAE,MAAM,CAAC,CAAA,CAAA,EAAI,EAAE,KAAK,CAAA,CAAA;AAC9C;AAOO,SAAS,iBAAiB,CAAA,EAA4B;AAC3D,EAAA,OAAO;AAAA,IACL,CAAA,EAAA,EAAK,CAAA,CAAE,QAAA,IAAY,EAAE,CAAA,CAAA;AAAA,IACrB,CAAA,EAAA,EAAK,CAAA,CAAE,cAAA,IAAkB,EAAE,CAAA,CAAA;AAAA,IAC3B,CAAA,EAAA,EAAK,CAAA,CAAE,SAAA,IAAa,EAAE,CAAA,CAAA;AAAA,IACtB,CAAA,GAAA,EAAM,CAAA,CAAE,eAAA,IAAmB,EAAE,CAAA,CAAA;AAAA,IAC7B,CAAA,GAAA,EAAM,CAAA,CAAE,YAAA,IAAgB,EAAE,CAAA,CAAA;AAAA,IAC1B,MAAM,IAAA,CAAK,SAAA,CAAU,CAAA,CAAE,aAAA,IAAiB,EAAE,CAAC,CAAA,CAAA;AAAA,IAC3C,CAAA,GAAA,EAAA,CAAO,CAAA,CAAE,KAAA,IAAS,EAAC,EAAG,IAAI,UAAU,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC,CAAA,CAAA;AAAA,IAChD,CAAA,GAAA,EAAA,CAAO,CAAA,CAAE,QAAA,IAAY,EAAC,EAAG,IAAI,UAAU,CAAA,CAAE,IAAA,CAAK,IAAI,CAAC,CAAA;AAAA,GACrD,CAAE,KAAK,KAAK,CAAA;AACd;;;AChBO,SAAS,oBAAA,CAAwB,KAAA,EAAU,OAAA,GAA2B,EAAC,EAAY;AACxF,EAAA,MAAM,SAAA,GAAY,iBAAiB,OAAO,CAAA;AAE1C,EAAA,OAAO,OAAA,CAAQ,MAAM,QAAA,CAAS,KAAA,EAAO,OAAO,CAAA,EAAG,CAAC,KAAA,EAAO,SAAS,CAAC,CAAA;AACnE","file":"index.mjs","sourcesContent":["/**\n * Internal helper for the React hook: turn a {@link CompressOptions} object into\n * a stable string signature so an inline options literal (new reference every\n * render) doesn't thrash `useMemo`. Not part of the public `./react` API.\n */\nimport type { CompressOptions } from \"../index\";\n\n// Control-char delimiters that cannot appear unescaped in a JSON-encoded token,\n// so neither matcher contents nor free-text option values can forge a boundary.\nconst FIELD = \"\\u0001\";\nconst ITEM = \"\\u0000\";\n\n/** Unambiguous, collision-free key for one matcher (string or RegExp). */\nfunction matcherKey(m: string | RegExp): string {\n return typeof m === \"string\"\n ? `s:${JSON.stringify(m)}`\n : `r:${JSON.stringify(m.source)}:${m.flags}`;\n}\n\n/**\n * Build a stable signature from an options object. Two options objects with\n * equal content produce the same signature; any content difference (including\n * RegExp flags, matcher order, or `Infinity` vs unset) produces a different one.\n */\nexport function optionsSignature(o: CompressOptions): string {\n return [\n `d=${o.maxDepth ?? \"\"}`,\n `a=${o.maxArrayLength ?? \"\"}`,\n `e=${o.dropEmpty ?? \"\"}`,\n `ds=${o.defaultSanitize ?? \"\"}`,\n `sm=${o.sanitizeMode ?? \"\"}`,\n `rv=${JSON.stringify(o.redactedValue ?? \"\")}`,\n `st=${(o.strip ?? []).map(matcherKey).join(ITEM)}`,\n `sn=${(o.sanitize ?? []).map(matcherKey).join(ITEM)}`,\n ].join(FIELD);\n}\n","/**\n * react-context-compressor — React bindings entry.\n *\n * Imports only from the core entry and the `react` peer dependency, so the\n * core (`.`) entry stays React-free and the React layer tree-shakes away for\n * consumers who import only `.`.\n */\nimport { useMemo } from \"react\";\nimport { type CompressOptions, compress } from \"../index\";\nimport { optionsSignature } from \"./signature\";\n\n/**\n * React hook returning a memoized, compressed + sanitized view of `state`.\n *\n * Recomputes only when the `state` reference changes or the options content\n * changes (compared by a stable signature, so an inline options literal is\n * fine). Pure: no DOM access, no side effects — safe under SSR and React\n * Server Components. Works on React 17 / 18 / 19.\n */\nexport function useCompressedContext<T>(state: T, options: CompressOptions = {}): unknown {\n const signature = optionsSignature(options);\n // biome-ignore lint/correctness/useExhaustiveDependencies: `options` is keyed by its stable `signature`\n return useMemo(() => compress(state, options), [state, signature]);\n}\n\nexport type { CompressOptions };\n"]}