react-context-compressor 0.1.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/LICENSE +21 -0
- package/README.md +169 -0
- package/dist/chunk-VBOCWPM5.mjs +208 -0
- package/dist/chunk-VBOCWPM5.mjs.map +1 -0
- package/dist/index.cjs +210 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +59 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -0
- package/dist/react/index.cjs +239 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +13 -0
- package/dist/react/index.d.ts +13 -0
- package/dist/react/index.mjs +31 -0
- package/dist/react/index.mjs.map +1 -0
- package/package.json +103 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Muhammad Umair Ali
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# react-context-compressor
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/react-context-compressor)
|
|
4
|
+
[](https://bundlephobia.com/package/react-context-compressor)
|
|
5
|
+
[](https://www.npmjs.com/package/react-context-compressor?activeTab=dependencies)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
|
|
8
|
+
A lightweight, **zero-dependency** JS/TS utility that mechanically strips
|
|
9
|
+
non-essential UI data and deep nesting from your application state **and**
|
|
10
|
+
sanitizes sensitive fields (tokens, credentials, private IDs) — producing a
|
|
11
|
+
minimal, safe payload to send to an LLM. It cuts token costs and avoids
|
|
12
|
+
context-window overflows.
|
|
13
|
+
|
|
14
|
+
It is **100% mechanical**: no network calls, no models, no AI summarization
|
|
15
|
+
(see [Non-goals](#non-goals)). A framework-agnostic core (`.`) plus a thin React
|
|
16
|
+
bindings layer (`./react`).
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
Before: 17,763 chars → After: 402 chars (97.7% smaller, secrets redacted)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
_(from [`examples/demo.mjs`](./examples/demo.mjs) — run it yourself.)_
|
|
23
|
+
|
|
24
|
+
## Why
|
|
25
|
+
|
|
26
|
+
Sending application state to an LLM is expensive (you pay per token) and fragile
|
|
27
|
+
(oversized blobs overflow the context window). State is also full of data the
|
|
28
|
+
model doesn't need — UI flags, caches, deep view-models — and sometimes carries
|
|
29
|
+
**secrets that must never leave the client**. `react-context-compressor` does the
|
|
30
|
+
shrinking and the sanitizing in one mechanical, deterministic pass.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm install react-context-compressor
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`react` is an optional **peer dependency** (>= 17) — only needed for the
|
|
39
|
+
`./react` entry. The core has zero runtime dependencies.
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
### Core (any JS/TS — React not required)
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { compress } from "react-context-compressor";
|
|
47
|
+
|
|
48
|
+
const payload = compress(appState, {
|
|
49
|
+
maxDepth: 3,
|
|
50
|
+
maxArrayLength: 5,
|
|
51
|
+
dropEmpty: true,
|
|
52
|
+
strip: [/^_/], // drop internal/private keys
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// `payload` is a minimal, secret-free plain object — JSON.stringify and send it.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### React
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { useCompressedContext } from "react-context-compressor/react";
|
|
62
|
+
|
|
63
|
+
function useLlmContext(state: AppState) {
|
|
64
|
+
// Memoized: recomputes only when `state` or the options content changes.
|
|
65
|
+
return useCompressedContext(state, {
|
|
66
|
+
maxDepth: 3,
|
|
67
|
+
maxArrayLength: 5,
|
|
68
|
+
dropEmpty: true,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Works with any state source — Redux, Zustand, Context, `useState`. The hook is
|
|
74
|
+
pure (no DOM, no side effects), so it's safe under SSR and React Server
|
|
75
|
+
Components, on React 17 / 18 / 19.
|
|
76
|
+
|
|
77
|
+
## What it does
|
|
78
|
+
|
|
79
|
+
| Transform | Option | Behavior |
|
|
80
|
+
| ------------ | -------------------------------- | --------------------------------------------------------------------------------------- |
|
|
81
|
+
| Cap depth | `maxDepth` | Nodes deeper than the cap become `"[Object]"` / `"[Array]"`. Default **100**. |
|
|
82
|
+
| Cap arrays | `maxArrayLength` | Longer arrays are truncated and a `"[+N more]"` marker appended. Default **unlimited**. |
|
|
83
|
+
| Strip keys | `strip` | Remove keys by exact string or `RegExp`, at any depth. |
|
|
84
|
+
| Drop empties | `dropEmpty` | Drop `null` / `undefined` / `""` / `[]` / `{}`. Default `false`. |
|
|
85
|
+
| Sanitize | `sanitize`, `defaultSanitize`, … | Redact/remove sensitive fields (see below). |
|
|
86
|
+
|
|
87
|
+
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**.
|
|
91
|
+
|
|
92
|
+
## Sanitization (data safety)
|
|
93
|
+
|
|
94
|
+
By default, common sensitive field **names** are redacted to `"[REDACTED]"`
|
|
95
|
+
before their value is ever read:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
compress({ user: "ada", apiKey: "sk-live-123", nested: { password: "p@ss" } });
|
|
99
|
+
// → { user: "ada", apiKey: "[REDACTED]", nested: { password: "[REDACTED]" } }
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The built-in deny-list covers (case-insensitive): `password`, `secret`, `token`
|
|
103
|
+
(and `accessToken`/`authToken`/…), `apiKey`/`accessKey`/`privateKey`/`signingKey`,
|
|
104
|
+
`jwt`, `authorization`, `bearer`, `credentials`, `cookie`, `sessionId`,
|
|
105
|
+
`otp`/`totp`/`mfa`, `mnemonic`/`seedPhrase`, recovery/backup codes, `hmac`,
|
|
106
|
+
`signature`, `connectionString`/`dsn`/db URLs, `ssn`, `creditCard`/`cardNumber`,
|
|
107
|
+
`cvv`, `pin`, `iban`, routing/account numbers, `passport`, `taxId`. It's tuned to
|
|
108
|
+
avoid false positives like `author`, `dashboard`, `secretary`, `tokenCount`, or
|
|
109
|
+
`promptTokens`.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// Extend the deny-list, or remove instead of redact:
|
|
113
|
+
compress(state, {
|
|
114
|
+
sanitize: ["employeeId", /internal/i],
|
|
115
|
+
sanitizeMode: "remove",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Replace the built-ins entirely:
|
|
119
|
+
compress(state, { defaultSanitize: false, sanitize: [/^secret_/] });
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Security note & limits
|
|
123
|
+
|
|
124
|
+
Sanitization is **key-name-driven** and mechanical. It is intentionally **not**:
|
|
125
|
+
|
|
126
|
+
- **Value-based** — a secret stored under an innocuous key, or as a bare array /
|
|
127
|
+
`Set` element (no key), is not detected. Match it explicitly via `sanitize`.
|
|
128
|
+
- A defense against deliberate **homoglyph** keys — keys are NFKC-normalized and
|
|
129
|
+
zero-width-stripped (defeating fullwidth/zero-width evasion), but not Cyrillic
|
|
130
|
+
look-alikes.
|
|
131
|
+
|
|
132
|
+
Only own **enumerable string** keys are processed; Symbol-keyed and non-enumerable
|
|
133
|
+
properties are dropped (never emitted). Treat the deny-list as a strong default,
|
|
134
|
+
and extend it for your domain.
|
|
135
|
+
|
|
136
|
+
## API
|
|
137
|
+
|
|
138
|
+
### `compress(state, options?) → unknown`
|
|
139
|
+
|
|
140
|
+
Mechanically compress + sanitize `state`. Pure, deterministic, never mutates input.
|
|
141
|
+
|
|
142
|
+
### `useCompressedContext(state, options?) → unknown` (`./react`)
|
|
143
|
+
|
|
144
|
+
Memoized React hook wrapping `compress`. Recomputes only when the `state`
|
|
145
|
+
reference or the options **content** changes (an inline options literal is fine).
|
|
146
|
+
|
|
147
|
+
### `CompressOptions`
|
|
148
|
+
|
|
149
|
+
| Option | Type | Default | Notes |
|
|
150
|
+
| ----------------- | ------------------------- | -------------- | --------------------------------------------------------------------------- |
|
|
151
|
+
| `maxDepth` | `number` | `100` | `Infinity` to disable. |
|
|
152
|
+
| `maxArrayLength` | `number` | `Infinity` | |
|
|
153
|
+
| `strip` | `Array<string \| RegExp>` | `[]` | String = exact (case-sensitive) key; `RegExp` = pattern. |
|
|
154
|
+
| `dropEmpty` | `boolean` | `false` | |
|
|
155
|
+
| `sanitize` | `Array<string \| RegExp>` | `[]` | String = case-insensitive exact; `RegExp` = pattern. Adds to the deny-list. |
|
|
156
|
+
| `defaultSanitize` | `boolean` | `true` | Toggle the built-in deny-list. |
|
|
157
|
+
| `sanitizeMode` | `"redact" \| "remove"` | `"redact"` | |
|
|
158
|
+
| `redactedValue` | `string` | `"[REDACTED]"` | Used when redacting. |
|
|
159
|
+
|
|
160
|
+
## Non-goals
|
|
161
|
+
|
|
162
|
+
This library will **never** perform semantic / AI-powered summarization, make
|
|
163
|
+
network calls, or run a local model. It is a mechanical, zero-cost, client-side
|
|
164
|
+
object parser — that's the whole point (it saves you money _before_ the network
|
|
165
|
+
layer). See [SPLIT-PLAN §2 (out of scope)](./SPLIT-PLAN.md).
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// src/core/sanitize.ts
|
|
2
|
+
var REDACTED = "[REDACTED]";
|
|
3
|
+
var DEFAULT_DENY_LIST = [
|
|
4
|
+
// Passwords / pass-phrases
|
|
5
|
+
/pass(?:word|wd|phrase)/i,
|
|
6
|
+
// Secrets ("secret", "clientSecret", "secretKey") but not "secretary"
|
|
7
|
+
/secret(?!ary)/i,
|
|
8
|
+
// Tokens — standalone or with an auth-ish prefix; NOT tokenCount/tokenize/promptTokens
|
|
9
|
+
/\btoken\b/i,
|
|
10
|
+
/(?:access|refresh|id|auth|bearer|api|csrf|xsrf|session|sso|oauth|reset|verification|activation)[-_]?token/i,
|
|
11
|
+
// Keys ("apiKey", "accessKey", "signingKey") but not "accessKeyword"/"keyboard"
|
|
12
|
+
/(?:api|access|private|signing|encryption)[-_]?keys?(?![a-z])/i,
|
|
13
|
+
/\bjwt\b/i,
|
|
14
|
+
/authorization/i,
|
|
15
|
+
/bearer/i,
|
|
16
|
+
/credentials?/i,
|
|
17
|
+
/cookie/i,
|
|
18
|
+
/session[-_]?(?:id|token|key)/i,
|
|
19
|
+
// 2FA / recovery
|
|
20
|
+
/\b(?:otp|totp|mfa|2fa)\b/i,
|
|
21
|
+
/(?:recovery|backup)[-_]?codes?/i,
|
|
22
|
+
/\bmnemonic\b/i,
|
|
23
|
+
/seed[-_]?phrase/i,
|
|
24
|
+
// Crypto / signing
|
|
25
|
+
/\bhmac\b/i,
|
|
26
|
+
/\bsignature\b/i,
|
|
27
|
+
/\b[cx]srf\b/i,
|
|
28
|
+
// Connection strings / DB credentials
|
|
29
|
+
/connection[-_]?string/i,
|
|
30
|
+
/\bdsn\b/i,
|
|
31
|
+
/(?:database|db)[-_]?(?:url|uri)/i,
|
|
32
|
+
// PII / financial
|
|
33
|
+
/ssn/i,
|
|
34
|
+
/social[-_]?security[-_]?(?:number|no)?/i,
|
|
35
|
+
/credit[-_]?card/i,
|
|
36
|
+
/card[-_]?number/i,
|
|
37
|
+
/cvv2?/i,
|
|
38
|
+
/\bpin\b/i,
|
|
39
|
+
/\biban\b/i,
|
|
40
|
+
/routing[-_]?number/i,
|
|
41
|
+
/account[-_]?number/i,
|
|
42
|
+
/\bpassport\b/i,
|
|
43
|
+
/tax[-_]?id/i
|
|
44
|
+
];
|
|
45
|
+
var ZERO_WIDTH = /[\u00AD\u200B-\u200D\u2060\uFEFF]/g;
|
|
46
|
+
function normalizeKey(key) {
|
|
47
|
+
return key.normalize("NFKC").replace(ZERO_WIDTH, "");
|
|
48
|
+
}
|
|
49
|
+
function regexTest(re, value) {
|
|
50
|
+
if (re.global || re.sticky) re.lastIndex = 0;
|
|
51
|
+
return re.test(value);
|
|
52
|
+
}
|
|
53
|
+
function isSensitiveKey(key, userMatchers, useDefaults) {
|
|
54
|
+
const normalized = normalizeKey(key);
|
|
55
|
+
const lower = normalized.toLowerCase();
|
|
56
|
+
for (const matcher of userMatchers) {
|
|
57
|
+
if (typeof matcher === "string") {
|
|
58
|
+
if (normalizeKey(matcher).toLowerCase() === lower) return true;
|
|
59
|
+
} else if (regexTest(matcher, normalized)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (useDefaults) {
|
|
64
|
+
for (const re of DEFAULT_DENY_LIST) {
|
|
65
|
+
if (regexTest(re, normalized)) return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/core/compress.ts
|
|
72
|
+
var TRUNCATED_OBJECT = "[Object]";
|
|
73
|
+
var TRUNCATED_ARRAY = "[Array]";
|
|
74
|
+
var CIRCULAR = "[Circular]";
|
|
75
|
+
var GETTER_ERROR = "[Getter]";
|
|
76
|
+
var DEFAULT_MAX_DEPTH = 100;
|
|
77
|
+
var DEFAULTS = {
|
|
78
|
+
maxDepth: DEFAULT_MAX_DEPTH,
|
|
79
|
+
maxArrayLength: Number.POSITIVE_INFINITY,
|
|
80
|
+
strip: [],
|
|
81
|
+
dropEmpty: false,
|
|
82
|
+
sanitize: [],
|
|
83
|
+
defaultSanitize: true,
|
|
84
|
+
sanitizeMode: "redact",
|
|
85
|
+
redactedValue: REDACTED
|
|
86
|
+
};
|
|
87
|
+
var OMIT = /* @__PURE__ */ Symbol("omit");
|
|
88
|
+
function isPlainObject(value) {
|
|
89
|
+
if (typeof value !== "object" || value === null) return false;
|
|
90
|
+
const proto = Object.getPrototypeOf(value);
|
|
91
|
+
return proto === Object.prototype || proto === null;
|
|
92
|
+
}
|
|
93
|
+
function safeAssign(target, key, value) {
|
|
94
|
+
Object.defineProperty(target, key, {
|
|
95
|
+
value,
|
|
96
|
+
writable: true,
|
|
97
|
+
enumerable: true,
|
|
98
|
+
configurable: true
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function keyMatches(key, matchers) {
|
|
102
|
+
for (const matcher of matchers) {
|
|
103
|
+
if (typeof matcher === "string") {
|
|
104
|
+
if (matcher === key) return true;
|
|
105
|
+
} else if (regexTest(matcher, key)) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
function isEmptyValue(value) {
|
|
112
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
113
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
114
|
+
if (isPlainObject(value)) return Object.keys(value).length === 0;
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
function resolveOptions(options) {
|
|
118
|
+
return {
|
|
119
|
+
maxDepth: options.maxDepth ?? DEFAULTS.maxDepth,
|
|
120
|
+
maxArrayLength: options.maxArrayLength ?? DEFAULTS.maxArrayLength,
|
|
121
|
+
strip: options.strip ?? DEFAULTS.strip,
|
|
122
|
+
dropEmpty: options.dropEmpty ?? DEFAULTS.dropEmpty,
|
|
123
|
+
sanitize: options.sanitize ?? DEFAULTS.sanitize,
|
|
124
|
+
defaultSanitize: options.defaultSanitize ?? DEFAULTS.defaultSanitize,
|
|
125
|
+
sanitizeMode: options.sanitizeMode ?? DEFAULTS.sanitizeMode,
|
|
126
|
+
redactedValue: options.redactedValue ?? DEFAULTS.redactedValue
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function walkObjectInto(out, source, depth, opts, seen) {
|
|
130
|
+
for (const key of Object.keys(source)) {
|
|
131
|
+
if (keyMatches(key, opts.strip)) continue;
|
|
132
|
+
if (isSensitiveKey(key, opts.sanitize, opts.defaultSanitize)) {
|
|
133
|
+
if (opts.sanitizeMode === "remove") continue;
|
|
134
|
+
safeAssign(out, key, opts.redactedValue);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
let raw;
|
|
138
|
+
try {
|
|
139
|
+
raw = source[key];
|
|
140
|
+
} catch {
|
|
141
|
+
safeAssign(out, key, GETTER_ERROR);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const walked = walk(raw, depth + 1, opts, seen);
|
|
145
|
+
if (walked === OMIT) continue;
|
|
146
|
+
if (opts.dropEmpty && isEmptyValue(walked)) continue;
|
|
147
|
+
safeAssign(out, key, walked);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function walk(value, depth, opts, seen) {
|
|
151
|
+
if (value === null) return null;
|
|
152
|
+
const type = typeof value;
|
|
153
|
+
if (type === "function" || type === "symbol") return OMIT;
|
|
154
|
+
if (type === "bigint") return value.toString();
|
|
155
|
+
if (type !== "object") return value;
|
|
156
|
+
const ref = value;
|
|
157
|
+
if (seen.has(ref)) return CIRCULAR;
|
|
158
|
+
if (value instanceof Date) return value;
|
|
159
|
+
if (value instanceof Map) {
|
|
160
|
+
seen.add(ref);
|
|
161
|
+
const obj = {};
|
|
162
|
+
for (const [k, v] of value) safeAssign(obj, String(k), v);
|
|
163
|
+
const result = walk(obj, depth, opts, seen);
|
|
164
|
+
seen.delete(ref);
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
if (value instanceof Set) {
|
|
168
|
+
seen.add(ref);
|
|
169
|
+
const result = walk(Array.from(value), depth, opts, seen);
|
|
170
|
+
seen.delete(ref);
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
if (Array.isArray(value)) {
|
|
174
|
+
if (depth >= opts.maxDepth) return TRUNCATED_ARRAY;
|
|
175
|
+
seen.add(ref);
|
|
176
|
+
const limited = value.length > opts.maxArrayLength ? value.slice(0, opts.maxArrayLength) : value;
|
|
177
|
+
const out2 = [];
|
|
178
|
+
for (const item of limited) {
|
|
179
|
+
const walked = walk(item, depth + 1, opts, seen);
|
|
180
|
+
out2.push(walked === OMIT ? null : walked);
|
|
181
|
+
}
|
|
182
|
+
if (value.length > opts.maxArrayLength) {
|
|
183
|
+
out2.push(`[+${value.length - opts.maxArrayLength} more]`);
|
|
184
|
+
}
|
|
185
|
+
seen.delete(ref);
|
|
186
|
+
return out2;
|
|
187
|
+
}
|
|
188
|
+
if (depth >= opts.maxDepth) return TRUNCATED_OBJECT;
|
|
189
|
+
seen.add(ref);
|
|
190
|
+
const out = {};
|
|
191
|
+
walkObjectInto(out, value, depth, opts, seen);
|
|
192
|
+
seen.delete(ref);
|
|
193
|
+
return out;
|
|
194
|
+
}
|
|
195
|
+
function compressCore(state, options = {}) {
|
|
196
|
+
const opts = resolveOptions(options);
|
|
197
|
+
const walked = walk(state, 0, opts, /* @__PURE__ */ new WeakSet());
|
|
198
|
+
return walked === OMIT ? void 0 : walked;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/index.ts
|
|
202
|
+
function compress(state, options = {}) {
|
|
203
|
+
return compressCore(state, options);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export { compress };
|
|
207
|
+
//# sourceMappingURL=chunk-VBOCWPM5.mjs.map
|
|
208
|
+
//# sourceMappingURL=chunk-VBOCWPM5.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/sanitize.ts
|
|
4
|
+
var REDACTED = "[REDACTED]";
|
|
5
|
+
var DEFAULT_DENY_LIST = [
|
|
6
|
+
// Passwords / pass-phrases
|
|
7
|
+
/pass(?:word|wd|phrase)/i,
|
|
8
|
+
// Secrets ("secret", "clientSecret", "secretKey") but not "secretary"
|
|
9
|
+
/secret(?!ary)/i,
|
|
10
|
+
// Tokens — standalone or with an auth-ish prefix; NOT tokenCount/tokenize/promptTokens
|
|
11
|
+
/\btoken\b/i,
|
|
12
|
+
/(?:access|refresh|id|auth|bearer|api|csrf|xsrf|session|sso|oauth|reset|verification|activation)[-_]?token/i,
|
|
13
|
+
// Keys ("apiKey", "accessKey", "signingKey") but not "accessKeyword"/"keyboard"
|
|
14
|
+
/(?:api|access|private|signing|encryption)[-_]?keys?(?![a-z])/i,
|
|
15
|
+
/\bjwt\b/i,
|
|
16
|
+
/authorization/i,
|
|
17
|
+
/bearer/i,
|
|
18
|
+
/credentials?/i,
|
|
19
|
+
/cookie/i,
|
|
20
|
+
/session[-_]?(?:id|token|key)/i,
|
|
21
|
+
// 2FA / recovery
|
|
22
|
+
/\b(?:otp|totp|mfa|2fa)\b/i,
|
|
23
|
+
/(?:recovery|backup)[-_]?codes?/i,
|
|
24
|
+
/\bmnemonic\b/i,
|
|
25
|
+
/seed[-_]?phrase/i,
|
|
26
|
+
// Crypto / signing
|
|
27
|
+
/\bhmac\b/i,
|
|
28
|
+
/\bsignature\b/i,
|
|
29
|
+
/\b[cx]srf\b/i,
|
|
30
|
+
// Connection strings / DB credentials
|
|
31
|
+
/connection[-_]?string/i,
|
|
32
|
+
/\bdsn\b/i,
|
|
33
|
+
/(?:database|db)[-_]?(?:url|uri)/i,
|
|
34
|
+
// PII / financial
|
|
35
|
+
/ssn/i,
|
|
36
|
+
/social[-_]?security[-_]?(?:number|no)?/i,
|
|
37
|
+
/credit[-_]?card/i,
|
|
38
|
+
/card[-_]?number/i,
|
|
39
|
+
/cvv2?/i,
|
|
40
|
+
/\bpin\b/i,
|
|
41
|
+
/\biban\b/i,
|
|
42
|
+
/routing[-_]?number/i,
|
|
43
|
+
/account[-_]?number/i,
|
|
44
|
+
/\bpassport\b/i,
|
|
45
|
+
/tax[-_]?id/i
|
|
46
|
+
];
|
|
47
|
+
var ZERO_WIDTH = /[\u00AD\u200B-\u200D\u2060\uFEFF]/g;
|
|
48
|
+
function normalizeKey(key) {
|
|
49
|
+
return key.normalize("NFKC").replace(ZERO_WIDTH, "");
|
|
50
|
+
}
|
|
51
|
+
function regexTest(re, value) {
|
|
52
|
+
if (re.global || re.sticky) re.lastIndex = 0;
|
|
53
|
+
return re.test(value);
|
|
54
|
+
}
|
|
55
|
+
function isSensitiveKey(key, userMatchers, useDefaults) {
|
|
56
|
+
const normalized = normalizeKey(key);
|
|
57
|
+
const lower = normalized.toLowerCase();
|
|
58
|
+
for (const matcher of userMatchers) {
|
|
59
|
+
if (typeof matcher === "string") {
|
|
60
|
+
if (normalizeKey(matcher).toLowerCase() === lower) return true;
|
|
61
|
+
} else if (regexTest(matcher, normalized)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (useDefaults) {
|
|
66
|
+
for (const re of DEFAULT_DENY_LIST) {
|
|
67
|
+
if (regexTest(re, normalized)) return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/core/compress.ts
|
|
74
|
+
var TRUNCATED_OBJECT = "[Object]";
|
|
75
|
+
var TRUNCATED_ARRAY = "[Array]";
|
|
76
|
+
var CIRCULAR = "[Circular]";
|
|
77
|
+
var GETTER_ERROR = "[Getter]";
|
|
78
|
+
var DEFAULT_MAX_DEPTH = 100;
|
|
79
|
+
var DEFAULTS = {
|
|
80
|
+
maxDepth: DEFAULT_MAX_DEPTH,
|
|
81
|
+
maxArrayLength: Number.POSITIVE_INFINITY,
|
|
82
|
+
strip: [],
|
|
83
|
+
dropEmpty: false,
|
|
84
|
+
sanitize: [],
|
|
85
|
+
defaultSanitize: true,
|
|
86
|
+
sanitizeMode: "redact",
|
|
87
|
+
redactedValue: REDACTED
|
|
88
|
+
};
|
|
89
|
+
var OMIT = /* @__PURE__ */ Symbol("omit");
|
|
90
|
+
function isPlainObject(value) {
|
|
91
|
+
if (typeof value !== "object" || value === null) return false;
|
|
92
|
+
const proto = Object.getPrototypeOf(value);
|
|
93
|
+
return proto === Object.prototype || proto === null;
|
|
94
|
+
}
|
|
95
|
+
function safeAssign(target, key, value) {
|
|
96
|
+
Object.defineProperty(target, key, {
|
|
97
|
+
value,
|
|
98
|
+
writable: true,
|
|
99
|
+
enumerable: true,
|
|
100
|
+
configurable: true
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function keyMatches(key, matchers) {
|
|
104
|
+
for (const matcher of matchers) {
|
|
105
|
+
if (typeof matcher === "string") {
|
|
106
|
+
if (matcher === key) return true;
|
|
107
|
+
} else if (regexTest(matcher, key)) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
function isEmptyValue(value) {
|
|
114
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
115
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
116
|
+
if (isPlainObject(value)) return Object.keys(value).length === 0;
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
function resolveOptions(options) {
|
|
120
|
+
return {
|
|
121
|
+
maxDepth: options.maxDepth ?? DEFAULTS.maxDepth,
|
|
122
|
+
maxArrayLength: options.maxArrayLength ?? DEFAULTS.maxArrayLength,
|
|
123
|
+
strip: options.strip ?? DEFAULTS.strip,
|
|
124
|
+
dropEmpty: options.dropEmpty ?? DEFAULTS.dropEmpty,
|
|
125
|
+
sanitize: options.sanitize ?? DEFAULTS.sanitize,
|
|
126
|
+
defaultSanitize: options.defaultSanitize ?? DEFAULTS.defaultSanitize,
|
|
127
|
+
sanitizeMode: options.sanitizeMode ?? DEFAULTS.sanitizeMode,
|
|
128
|
+
redactedValue: options.redactedValue ?? DEFAULTS.redactedValue
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function walkObjectInto(out, source, depth, opts, seen) {
|
|
132
|
+
for (const key of Object.keys(source)) {
|
|
133
|
+
if (keyMatches(key, opts.strip)) continue;
|
|
134
|
+
if (isSensitiveKey(key, opts.sanitize, opts.defaultSanitize)) {
|
|
135
|
+
if (opts.sanitizeMode === "remove") continue;
|
|
136
|
+
safeAssign(out, key, opts.redactedValue);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
let raw;
|
|
140
|
+
try {
|
|
141
|
+
raw = source[key];
|
|
142
|
+
} catch {
|
|
143
|
+
safeAssign(out, key, GETTER_ERROR);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const walked = walk(raw, depth + 1, opts, seen);
|
|
147
|
+
if (walked === OMIT) continue;
|
|
148
|
+
if (opts.dropEmpty && isEmptyValue(walked)) continue;
|
|
149
|
+
safeAssign(out, key, walked);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function walk(value, depth, opts, seen) {
|
|
153
|
+
if (value === null) return null;
|
|
154
|
+
const type = typeof value;
|
|
155
|
+
if (type === "function" || type === "symbol") return OMIT;
|
|
156
|
+
if (type === "bigint") return value.toString();
|
|
157
|
+
if (type !== "object") return value;
|
|
158
|
+
const ref = value;
|
|
159
|
+
if (seen.has(ref)) return CIRCULAR;
|
|
160
|
+
if (value instanceof Date) return value;
|
|
161
|
+
if (value instanceof Map) {
|
|
162
|
+
seen.add(ref);
|
|
163
|
+
const obj = {};
|
|
164
|
+
for (const [k, v] of value) safeAssign(obj, String(k), v);
|
|
165
|
+
const result = walk(obj, depth, opts, seen);
|
|
166
|
+
seen.delete(ref);
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
if (value instanceof Set) {
|
|
170
|
+
seen.add(ref);
|
|
171
|
+
const result = walk(Array.from(value), depth, opts, seen);
|
|
172
|
+
seen.delete(ref);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
if (Array.isArray(value)) {
|
|
176
|
+
if (depth >= opts.maxDepth) return TRUNCATED_ARRAY;
|
|
177
|
+
seen.add(ref);
|
|
178
|
+
const limited = value.length > opts.maxArrayLength ? value.slice(0, opts.maxArrayLength) : value;
|
|
179
|
+
const out2 = [];
|
|
180
|
+
for (const item of limited) {
|
|
181
|
+
const walked = walk(item, depth + 1, opts, seen);
|
|
182
|
+
out2.push(walked === OMIT ? null : walked);
|
|
183
|
+
}
|
|
184
|
+
if (value.length > opts.maxArrayLength) {
|
|
185
|
+
out2.push(`[+${value.length - opts.maxArrayLength} more]`);
|
|
186
|
+
}
|
|
187
|
+
seen.delete(ref);
|
|
188
|
+
return out2;
|
|
189
|
+
}
|
|
190
|
+
if (depth >= opts.maxDepth) return TRUNCATED_OBJECT;
|
|
191
|
+
seen.add(ref);
|
|
192
|
+
const out = {};
|
|
193
|
+
walkObjectInto(out, value, depth, opts, seen);
|
|
194
|
+
seen.delete(ref);
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
function compressCore(state, options = {}) {
|
|
198
|
+
const opts = resolveOptions(options);
|
|
199
|
+
const walked = walk(state, 0, opts, /* @__PURE__ */ new WeakSet());
|
|
200
|
+
return walked === OMIT ? void 0 : walked;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/index.ts
|
|
204
|
+
function compress(state, options = {}) {
|
|
205
|
+
return compressCore(state, options);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
exports.compress = compress;
|
|
209
|
+
//# sourceMappingURL=index.cjs.map
|
|
210
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* react-context-compressor — core entry (framework-agnostic, zero dependencies).
|
|
3
|
+
*
|
|
4
|
+
* Mechanical compression lands here (task 002); sanitization composes into the
|
|
5
|
+
* same walk in task 003.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Options controlling how a state value is mechanically compressed and
|
|
9
|
+
* sanitized into a minimal, LLM-ready payload. All fields are optional;
|
|
10
|
+
* `compress(state)` with no options returns a safe deep copy.
|
|
11
|
+
*/
|
|
12
|
+
interface CompressOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Maximum object/array depth to retain. Nodes deeper than this are replaced
|
|
15
|
+
* with a `"[Object]"` / `"[Array]"` marker. Default: `100` (a safe cap that
|
|
16
|
+
* keeps payloads minimal and prevents stack overflow on pathologically deep
|
|
17
|
+
* input). Set to `Infinity` to disable depth capping.
|
|
18
|
+
*/
|
|
19
|
+
maxDepth?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Maximum array length to retain. Longer arrays are truncated and a
|
|
22
|
+
* `"[+N more]"` marker is appended. Default: unlimited.
|
|
23
|
+
*/
|
|
24
|
+
maxArrayLength?: number;
|
|
25
|
+
/** Keys (exact strings or patterns) to strip from the output. */
|
|
26
|
+
strip?: Array<string | RegExp>;
|
|
27
|
+
/** When true, drop `null` / `undefined` / `""` / `[]` / `{}` values. */
|
|
28
|
+
dropEmpty?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Extra sensitive field-name matchers to redact/remove, IN ADDITION to the
|
|
31
|
+
* built-in deny-list (unless {@link CompressOptions.defaultSanitize} is
|
|
32
|
+
* `false`). A `string` matches a key case-insensitively and exactly; a
|
|
33
|
+
* `RegExp` matches by pattern. Matching is by field NAME, not value.
|
|
34
|
+
*/
|
|
35
|
+
sanitize?: Array<string | RegExp>;
|
|
36
|
+
/**
|
|
37
|
+
* Apply the built-in sensitive-field deny-list (password, token, secret,
|
|
38
|
+
* apiKey, authorization, cookie, ssn, creditCard, …). Default: `true`.
|
|
39
|
+
* Set `false` to rely solely on {@link CompressOptions.sanitize}.
|
|
40
|
+
*/
|
|
41
|
+
defaultSanitize?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* How to treat a sensitive field: `"redact"` replaces its value with
|
|
44
|
+
* {@link CompressOptions.redactedValue}; `"remove"` drops the key entirely.
|
|
45
|
+
* Either way the value is never read or emitted. Default: `"redact"`.
|
|
46
|
+
*/
|
|
47
|
+
sanitizeMode?: "redact" | "remove";
|
|
48
|
+
/** Replacement used when `sanitizeMode` is `"redact"`. Default: `"[REDACTED]"`. */
|
|
49
|
+
redactedValue?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Mechanically compress and sanitize a state value into a minimal, safe payload.
|
|
53
|
+
*
|
|
54
|
+
* Pure and deterministic: the same input and options always produce the same
|
|
55
|
+
* output, and the input is never mutated. Performs no I/O — purely structural.
|
|
56
|
+
*/
|
|
57
|
+
declare function compress<T>(state: T, options?: CompressOptions): unknown;
|
|
58
|
+
|
|
59
|
+
export { type CompressOptions, compress };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* react-context-compressor — core entry (framework-agnostic, zero dependencies).
|
|
3
|
+
*
|
|
4
|
+
* Mechanical compression lands here (task 002); sanitization composes into the
|
|
5
|
+
* same walk in task 003.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Options controlling how a state value is mechanically compressed and
|
|
9
|
+
* sanitized into a minimal, LLM-ready payload. All fields are optional;
|
|
10
|
+
* `compress(state)` with no options returns a safe deep copy.
|
|
11
|
+
*/
|
|
12
|
+
interface CompressOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Maximum object/array depth to retain. Nodes deeper than this are replaced
|
|
15
|
+
* with a `"[Object]"` / `"[Array]"` marker. Default: `100` (a safe cap that
|
|
16
|
+
* keeps payloads minimal and prevents stack overflow on pathologically deep
|
|
17
|
+
* input). Set to `Infinity` to disable depth capping.
|
|
18
|
+
*/
|
|
19
|
+
maxDepth?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Maximum array length to retain. Longer arrays are truncated and a
|
|
22
|
+
* `"[+N more]"` marker is appended. Default: unlimited.
|
|
23
|
+
*/
|
|
24
|
+
maxArrayLength?: number;
|
|
25
|
+
/** Keys (exact strings or patterns) to strip from the output. */
|
|
26
|
+
strip?: Array<string | RegExp>;
|
|
27
|
+
/** When true, drop `null` / `undefined` / `""` / `[]` / `{}` values. */
|
|
28
|
+
dropEmpty?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Extra sensitive field-name matchers to redact/remove, IN ADDITION to the
|
|
31
|
+
* built-in deny-list (unless {@link CompressOptions.defaultSanitize} is
|
|
32
|
+
* `false`). A `string` matches a key case-insensitively and exactly; a
|
|
33
|
+
* `RegExp` matches by pattern. Matching is by field NAME, not value.
|
|
34
|
+
*/
|
|
35
|
+
sanitize?: Array<string | RegExp>;
|
|
36
|
+
/**
|
|
37
|
+
* Apply the built-in sensitive-field deny-list (password, token, secret,
|
|
38
|
+
* apiKey, authorization, cookie, ssn, creditCard, …). Default: `true`.
|
|
39
|
+
* Set `false` to rely solely on {@link CompressOptions.sanitize}.
|
|
40
|
+
*/
|
|
41
|
+
defaultSanitize?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* How to treat a sensitive field: `"redact"` replaces its value with
|
|
44
|
+
* {@link CompressOptions.redactedValue}; `"remove"` drops the key entirely.
|
|
45
|
+
* Either way the value is never read or emitted. Default: `"redact"`.
|
|
46
|
+
*/
|
|
47
|
+
sanitizeMode?: "redact" | "remove";
|
|
48
|
+
/** Replacement used when `sanitizeMode` is `"redact"`. Default: `"[REDACTED]"`. */
|
|
49
|
+
redactedValue?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Mechanically compress and sanitize a state value into a minimal, safe payload.
|
|
53
|
+
*
|
|
54
|
+
* Pure and deterministic: the same input and options always produce the same
|
|
55
|
+
* output, and the input is never mutated. Performs no I/O — purely structural.
|
|
56
|
+
*/
|
|
57
|
+
declare function compress<T>(state: T, options?: CompressOptions): unknown;
|
|
58
|
+
|
|
59
|
+
export { type CompressOptions, compress };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.mjs"}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
|
|
5
|
+
// src/react/index.ts
|
|
6
|
+
|
|
7
|
+
// src/core/sanitize.ts
|
|
8
|
+
var REDACTED = "[REDACTED]";
|
|
9
|
+
var DEFAULT_DENY_LIST = [
|
|
10
|
+
// Passwords / pass-phrases
|
|
11
|
+
/pass(?:word|wd|phrase)/i,
|
|
12
|
+
// Secrets ("secret", "clientSecret", "secretKey") but not "secretary"
|
|
13
|
+
/secret(?!ary)/i,
|
|
14
|
+
// Tokens — standalone or with an auth-ish prefix; NOT tokenCount/tokenize/promptTokens
|
|
15
|
+
/\btoken\b/i,
|
|
16
|
+
/(?:access|refresh|id|auth|bearer|api|csrf|xsrf|session|sso|oauth|reset|verification|activation)[-_]?token/i,
|
|
17
|
+
// Keys ("apiKey", "accessKey", "signingKey") but not "accessKeyword"/"keyboard"
|
|
18
|
+
/(?:api|access|private|signing|encryption)[-_]?keys?(?![a-z])/i,
|
|
19
|
+
/\bjwt\b/i,
|
|
20
|
+
/authorization/i,
|
|
21
|
+
/bearer/i,
|
|
22
|
+
/credentials?/i,
|
|
23
|
+
/cookie/i,
|
|
24
|
+
/session[-_]?(?:id|token|key)/i,
|
|
25
|
+
// 2FA / recovery
|
|
26
|
+
/\b(?:otp|totp|mfa|2fa)\b/i,
|
|
27
|
+
/(?:recovery|backup)[-_]?codes?/i,
|
|
28
|
+
/\bmnemonic\b/i,
|
|
29
|
+
/seed[-_]?phrase/i,
|
|
30
|
+
// Crypto / signing
|
|
31
|
+
/\bhmac\b/i,
|
|
32
|
+
/\bsignature\b/i,
|
|
33
|
+
/\b[cx]srf\b/i,
|
|
34
|
+
// Connection strings / DB credentials
|
|
35
|
+
/connection[-_]?string/i,
|
|
36
|
+
/\bdsn\b/i,
|
|
37
|
+
/(?:database|db)[-_]?(?:url|uri)/i,
|
|
38
|
+
// PII / financial
|
|
39
|
+
/ssn/i,
|
|
40
|
+
/social[-_]?security[-_]?(?:number|no)?/i,
|
|
41
|
+
/credit[-_]?card/i,
|
|
42
|
+
/card[-_]?number/i,
|
|
43
|
+
/cvv2?/i,
|
|
44
|
+
/\bpin\b/i,
|
|
45
|
+
/\biban\b/i,
|
|
46
|
+
/routing[-_]?number/i,
|
|
47
|
+
/account[-_]?number/i,
|
|
48
|
+
/\bpassport\b/i,
|
|
49
|
+
/tax[-_]?id/i
|
|
50
|
+
];
|
|
51
|
+
var ZERO_WIDTH = /[\u00AD\u200B-\u200D\u2060\uFEFF]/g;
|
|
52
|
+
function normalizeKey(key) {
|
|
53
|
+
return key.normalize("NFKC").replace(ZERO_WIDTH, "");
|
|
54
|
+
}
|
|
55
|
+
function regexTest(re, value) {
|
|
56
|
+
if (re.global || re.sticky) re.lastIndex = 0;
|
|
57
|
+
return re.test(value);
|
|
58
|
+
}
|
|
59
|
+
function isSensitiveKey(key, userMatchers, useDefaults) {
|
|
60
|
+
const normalized = normalizeKey(key);
|
|
61
|
+
const lower = normalized.toLowerCase();
|
|
62
|
+
for (const matcher of userMatchers) {
|
|
63
|
+
if (typeof matcher === "string") {
|
|
64
|
+
if (normalizeKey(matcher).toLowerCase() === lower) return true;
|
|
65
|
+
} else if (regexTest(matcher, normalized)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (useDefaults) {
|
|
70
|
+
for (const re of DEFAULT_DENY_LIST) {
|
|
71
|
+
if (regexTest(re, normalized)) return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/core/compress.ts
|
|
78
|
+
var TRUNCATED_OBJECT = "[Object]";
|
|
79
|
+
var TRUNCATED_ARRAY = "[Array]";
|
|
80
|
+
var CIRCULAR = "[Circular]";
|
|
81
|
+
var GETTER_ERROR = "[Getter]";
|
|
82
|
+
var DEFAULT_MAX_DEPTH = 100;
|
|
83
|
+
var DEFAULTS = {
|
|
84
|
+
maxDepth: DEFAULT_MAX_DEPTH,
|
|
85
|
+
maxArrayLength: Number.POSITIVE_INFINITY,
|
|
86
|
+
strip: [],
|
|
87
|
+
dropEmpty: false,
|
|
88
|
+
sanitize: [],
|
|
89
|
+
defaultSanitize: true,
|
|
90
|
+
sanitizeMode: "redact",
|
|
91
|
+
redactedValue: REDACTED
|
|
92
|
+
};
|
|
93
|
+
var OMIT = /* @__PURE__ */ Symbol("omit");
|
|
94
|
+
function isPlainObject(value) {
|
|
95
|
+
if (typeof value !== "object" || value === null) return false;
|
|
96
|
+
const proto = Object.getPrototypeOf(value);
|
|
97
|
+
return proto === Object.prototype || proto === null;
|
|
98
|
+
}
|
|
99
|
+
function safeAssign(target, key, value) {
|
|
100
|
+
Object.defineProperty(target, key, {
|
|
101
|
+
value,
|
|
102
|
+
writable: true,
|
|
103
|
+
enumerable: true,
|
|
104
|
+
configurable: true
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function keyMatches(key, matchers) {
|
|
108
|
+
for (const matcher of matchers) {
|
|
109
|
+
if (typeof matcher === "string") {
|
|
110
|
+
if (matcher === key) return true;
|
|
111
|
+
} else if (regexTest(matcher, key)) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
function isEmptyValue(value) {
|
|
118
|
+
if (value === null || value === void 0 || value === "") return true;
|
|
119
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
120
|
+
if (isPlainObject(value)) return Object.keys(value).length === 0;
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
function resolveOptions(options) {
|
|
124
|
+
return {
|
|
125
|
+
maxDepth: options.maxDepth ?? DEFAULTS.maxDepth,
|
|
126
|
+
maxArrayLength: options.maxArrayLength ?? DEFAULTS.maxArrayLength,
|
|
127
|
+
strip: options.strip ?? DEFAULTS.strip,
|
|
128
|
+
dropEmpty: options.dropEmpty ?? DEFAULTS.dropEmpty,
|
|
129
|
+
sanitize: options.sanitize ?? DEFAULTS.sanitize,
|
|
130
|
+
defaultSanitize: options.defaultSanitize ?? DEFAULTS.defaultSanitize,
|
|
131
|
+
sanitizeMode: options.sanitizeMode ?? DEFAULTS.sanitizeMode,
|
|
132
|
+
redactedValue: options.redactedValue ?? DEFAULTS.redactedValue
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function walkObjectInto(out, source, depth, opts, seen) {
|
|
136
|
+
for (const key of Object.keys(source)) {
|
|
137
|
+
if (keyMatches(key, opts.strip)) continue;
|
|
138
|
+
if (isSensitiveKey(key, opts.sanitize, opts.defaultSanitize)) {
|
|
139
|
+
if (opts.sanitizeMode === "remove") continue;
|
|
140
|
+
safeAssign(out, key, opts.redactedValue);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
let raw;
|
|
144
|
+
try {
|
|
145
|
+
raw = source[key];
|
|
146
|
+
} catch {
|
|
147
|
+
safeAssign(out, key, GETTER_ERROR);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const walked = walk(raw, depth + 1, opts, seen);
|
|
151
|
+
if (walked === OMIT) continue;
|
|
152
|
+
if (opts.dropEmpty && isEmptyValue(walked)) continue;
|
|
153
|
+
safeAssign(out, key, walked);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function walk(value, depth, opts, seen) {
|
|
157
|
+
if (value === null) return null;
|
|
158
|
+
const type = typeof value;
|
|
159
|
+
if (type === "function" || type === "symbol") return OMIT;
|
|
160
|
+
if (type === "bigint") return value.toString();
|
|
161
|
+
if (type !== "object") return value;
|
|
162
|
+
const ref = value;
|
|
163
|
+
if (seen.has(ref)) return CIRCULAR;
|
|
164
|
+
if (value instanceof Date) return value;
|
|
165
|
+
if (value instanceof Map) {
|
|
166
|
+
seen.add(ref);
|
|
167
|
+
const obj = {};
|
|
168
|
+
for (const [k, v] of value) safeAssign(obj, String(k), v);
|
|
169
|
+
const result = walk(obj, depth, opts, seen);
|
|
170
|
+
seen.delete(ref);
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
if (value instanceof Set) {
|
|
174
|
+
seen.add(ref);
|
|
175
|
+
const result = walk(Array.from(value), depth, opts, seen);
|
|
176
|
+
seen.delete(ref);
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
if (Array.isArray(value)) {
|
|
180
|
+
if (depth >= opts.maxDepth) return TRUNCATED_ARRAY;
|
|
181
|
+
seen.add(ref);
|
|
182
|
+
const limited = value.length > opts.maxArrayLength ? value.slice(0, opts.maxArrayLength) : value;
|
|
183
|
+
const out2 = [];
|
|
184
|
+
for (const item of limited) {
|
|
185
|
+
const walked = walk(item, depth + 1, opts, seen);
|
|
186
|
+
out2.push(walked === OMIT ? null : walked);
|
|
187
|
+
}
|
|
188
|
+
if (value.length > opts.maxArrayLength) {
|
|
189
|
+
out2.push(`[+${value.length - opts.maxArrayLength} more]`);
|
|
190
|
+
}
|
|
191
|
+
seen.delete(ref);
|
|
192
|
+
return out2;
|
|
193
|
+
}
|
|
194
|
+
if (depth >= opts.maxDepth) return TRUNCATED_OBJECT;
|
|
195
|
+
seen.add(ref);
|
|
196
|
+
const out = {};
|
|
197
|
+
walkObjectInto(out, value, depth, opts, seen);
|
|
198
|
+
seen.delete(ref);
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
function compressCore(state, options = {}) {
|
|
202
|
+
const opts = resolveOptions(options);
|
|
203
|
+
const walked = walk(state, 0, opts, /* @__PURE__ */ new WeakSet());
|
|
204
|
+
return walked === OMIT ? void 0 : walked;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/index.ts
|
|
208
|
+
function compress(state, options = {}) {
|
|
209
|
+
return compressCore(state, options);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/react/signature.ts
|
|
213
|
+
var FIELD = "";
|
|
214
|
+
var ITEM = "\0";
|
|
215
|
+
function matcherKey(m) {
|
|
216
|
+
return typeof m === "string" ? `s:${JSON.stringify(m)}` : `r:${JSON.stringify(m.source)}:${m.flags}`;
|
|
217
|
+
}
|
|
218
|
+
function optionsSignature(o) {
|
|
219
|
+
return [
|
|
220
|
+
`d=${o.maxDepth ?? ""}`,
|
|
221
|
+
`a=${o.maxArrayLength ?? ""}`,
|
|
222
|
+
`e=${o.dropEmpty ?? ""}`,
|
|
223
|
+
`ds=${o.defaultSanitize ?? ""}`,
|
|
224
|
+
`sm=${o.sanitizeMode ?? ""}`,
|
|
225
|
+
`rv=${JSON.stringify(o.redactedValue ?? "")}`,
|
|
226
|
+
`st=${(o.strip ?? []).map(matcherKey).join(ITEM)}`,
|
|
227
|
+
`sn=${(o.sanitize ?? []).map(matcherKey).join(ITEM)}`
|
|
228
|
+
].join(FIELD);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/react/index.ts
|
|
232
|
+
function useCompressedContext(state, options = {}) {
|
|
233
|
+
const signature = optionsSignature(options);
|
|
234
|
+
return react.useMemo(() => compress(state, options), [state, signature]);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
exports.useCompressedContext = useCompressedContext;
|
|
238
|
+
//# sourceMappingURL=index.cjs.map
|
|
239
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CompressOptions } from '../index.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* React hook returning a memoized, compressed + sanitized view of `state`.
|
|
5
|
+
*
|
|
6
|
+
* Recomputes only when the `state` reference changes or the options content
|
|
7
|
+
* changes (compared by a stable signature, so an inline options literal is
|
|
8
|
+
* fine). Pure: no DOM access, no side effects — safe under SSR and React
|
|
9
|
+
* Server Components. Works on React 17 / 18 / 19.
|
|
10
|
+
*/
|
|
11
|
+
declare function useCompressedContext<T>(state: T, options?: CompressOptions): unknown;
|
|
12
|
+
|
|
13
|
+
export { CompressOptions, useCompressedContext };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { CompressOptions } from '../index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* React hook returning a memoized, compressed + sanitized view of `state`.
|
|
5
|
+
*
|
|
6
|
+
* Recomputes only when the `state` reference changes or the options content
|
|
7
|
+
* changes (compared by a stable signature, so an inline options literal is
|
|
8
|
+
* fine). Pure: no DOM access, no side effects — safe under SSR and React
|
|
9
|
+
* Server Components. Works on React 17 / 18 / 19.
|
|
10
|
+
*/
|
|
11
|
+
declare function useCompressedContext<T>(state: T, options?: CompressOptions): unknown;
|
|
12
|
+
|
|
13
|
+
export { CompressOptions, useCompressedContext };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { compress } from '../chunk-VBOCWPM5.mjs';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
|
|
4
|
+
// src/react/signature.ts
|
|
5
|
+
var FIELD = "";
|
|
6
|
+
var ITEM = "\0";
|
|
7
|
+
function matcherKey(m) {
|
|
8
|
+
return typeof m === "string" ? `s:${JSON.stringify(m)}` : `r:${JSON.stringify(m.source)}:${m.flags}`;
|
|
9
|
+
}
|
|
10
|
+
function optionsSignature(o) {
|
|
11
|
+
return [
|
|
12
|
+
`d=${o.maxDepth ?? ""}`,
|
|
13
|
+
`a=${o.maxArrayLength ?? ""}`,
|
|
14
|
+
`e=${o.dropEmpty ?? ""}`,
|
|
15
|
+
`ds=${o.defaultSanitize ?? ""}`,
|
|
16
|
+
`sm=${o.sanitizeMode ?? ""}`,
|
|
17
|
+
`rv=${JSON.stringify(o.redactedValue ?? "")}`,
|
|
18
|
+
`st=${(o.strip ?? []).map(matcherKey).join(ITEM)}`,
|
|
19
|
+
`sn=${(o.sanitize ?? []).map(matcherKey).join(ITEM)}`
|
|
20
|
+
].join(FIELD);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/react/index.ts
|
|
24
|
+
function useCompressedContext(state, options = {}) {
|
|
25
|
+
const signature = optionsSignature(options);
|
|
26
|
+
return useMemo(() => compress(state, options), [state, signature]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { useCompressedContext };
|
|
30
|
+
//# sourceMappingURL=index.mjs.map
|
|
31
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-context-compressor",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Mechanical, zero-dependency client-side compressor + sanitizer that turns React/JS app state into a minimal, safe payload for LLMs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Muhammad Umair Ali <claude@oxym.tech> (https://github.com/Muhammad-UmairAli)",
|
|
8
|
+
"contributors": [
|
|
9
|
+
"Muhammad Umair Ali <claude@oxym.tech> (https://github.com/Muhammad-UmairAli)"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/Muhammad-UmairAli/react-context-compressor.git"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/Muhammad-UmairAli/react-context-compressor#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/Muhammad-UmairAli/react-context-compressor/issues"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"react",
|
|
21
|
+
"llm",
|
|
22
|
+
"ai",
|
|
23
|
+
"context",
|
|
24
|
+
"compression",
|
|
25
|
+
"sanitize",
|
|
26
|
+
"tokens",
|
|
27
|
+
"state",
|
|
28
|
+
"prompt"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"main": "./dist/index.cjs",
|
|
35
|
+
"module": "./dist/index.mjs",
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"exports": {
|
|
38
|
+
".": {
|
|
39
|
+
"import": {
|
|
40
|
+
"types": "./dist/index.d.ts",
|
|
41
|
+
"default": "./dist/index.mjs"
|
|
42
|
+
},
|
|
43
|
+
"require": {
|
|
44
|
+
"types": "./dist/index.d.cts",
|
|
45
|
+
"default": "./dist/index.cjs"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"./react": {
|
|
49
|
+
"import": {
|
|
50
|
+
"types": "./dist/react/index.d.ts",
|
|
51
|
+
"default": "./dist/react/index.mjs"
|
|
52
|
+
},
|
|
53
|
+
"require": {
|
|
54
|
+
"types": "./dist/react/index.d.cts",
|
|
55
|
+
"default": "./dist/react/index.cjs"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"./package.json": "./package.json"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsup",
|
|
62
|
+
"dev": "tsup --watch",
|
|
63
|
+
"typecheck": "tsc --noEmit",
|
|
64
|
+
"lint": "biome check .",
|
|
65
|
+
"format": "biome format --write .",
|
|
66
|
+
"test": "vitest run",
|
|
67
|
+
"test:watch": "vitest",
|
|
68
|
+
"test:cov": "vitest run --coverage",
|
|
69
|
+
"size": "size-limit",
|
|
70
|
+
"changeset": "changeset",
|
|
71
|
+
"prepublishOnly": "npm run build"
|
|
72
|
+
},
|
|
73
|
+
"peerDependencies": {
|
|
74
|
+
"react": ">=17"
|
|
75
|
+
},
|
|
76
|
+
"peerDependenciesMeta": {
|
|
77
|
+
"react": {
|
|
78
|
+
"optional": true
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"publishConfig": {
|
|
82
|
+
"access": "public"
|
|
83
|
+
},
|
|
84
|
+
"engines": {
|
|
85
|
+
"node": ">=18"
|
|
86
|
+
},
|
|
87
|
+
"devDependencies": {
|
|
88
|
+
"@biomejs/biome": "^2.5.0",
|
|
89
|
+
"@changesets/cli": "^2.31.0",
|
|
90
|
+
"@size-limit/preset-small-lib": "^12.1.0",
|
|
91
|
+
"@testing-library/react": "^16.3.2",
|
|
92
|
+
"@types/react": "^19.2.17",
|
|
93
|
+
"@types/react-dom": "^19.2.3",
|
|
94
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
95
|
+
"jsdom": "^29.1.1",
|
|
96
|
+
"react": "^19.2.7",
|
|
97
|
+
"react-dom": "^19.2.7",
|
|
98
|
+
"size-limit": "^12.1.0",
|
|
99
|
+
"tsup": "^8.5.1",
|
|
100
|
+
"typescript": "^6.0.3",
|
|
101
|
+
"vitest": "^4.1.9"
|
|
102
|
+
}
|
|
103
|
+
}
|