@spool-lab/redact 0.0.1

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 ADDED
@@ -0,0 +1,32 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Spool Contributors
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.
22
+
23
+ ---
24
+
25
+ TRADEMARK NOTICE
26
+
27
+ "Spool" and the Spool logo are trademarks of TypeSafe Limited. The MIT
28
+ License above governs the source code only; it does not grant any right or
29
+ license to use the "Spool" name, the Spool logo, or any other TypeSafe
30
+ Limited trademark. Forks and redistributions must not use the "Spool" name
31
+ or logo in a way that suggests endorsement by, or affiliation with,
32
+ TypeSafe Limited.
@@ -0,0 +1,8 @@
1
+ import type { SensitiveMatch } from './types.js';
2
+ /** Synchronous regex-driven scan. Rules earlier in the priority list
3
+ * claim their regions first; later, broader rules whose match
4
+ * overlaps a claimed region are dropped. This is what makes a JWT
5
+ * inside a `Bearer` header surface as a JWT, and a Stripe key inside
6
+ * `STRIPE_SECRET_KEY=…` surface as the whole assignment. */
7
+ export declare function detectWithRegex(text: string, providerName?: string): SensitiveMatch[];
8
+ //# sourceMappingURL=detectors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detectors.d.ts","sourceRoot":"","sources":["../src/detectors.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAiB,cAAc,EAAE,MAAM,YAAY,CAAA;AAoL/D;;;;6DAI6D;AAC7D,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,SAAU,GAAG,cAAc,EAAE,CAsCtF"}
@@ -0,0 +1,205 @@
1
+ // Regex detection pipeline for @spool-lab/redact.
2
+ //
3
+ // Primary scenario: Spool sessions captured from local coding agents
4
+ // (Claude Code, codex, gemini) — terminal output, tool results, files
5
+ // the agent read, error logs. The patterns favour structured leaks
6
+ // that *appear in this kind of content*: a stray `cat ~/.aws/creds`
7
+ // line, `gh auth status` dumping a token, a `kubectl config view`
8
+ // pasted into the conversation, a `psql "postgresql://…"` invocation.
9
+ //
10
+ // Rule ordering matters. Wider patterns whose prefix LEAKS context
11
+ // (env-var name reveals the vendor; URL host + creds leak the host;
12
+ // connection-string scheme reveals the database type) run before
13
+ // bare vendor api-keys so the whole assignment is masked, not just
14
+ // the secret value. JWT runs before `Bearer` so a JWT-shaped bearer
15
+ // surfaces as a JWT rather than a generic bearer wrapping a JWT.
16
+ import { hasQuotedEntropy, luhnOk } from './validators.js';
17
+ // ── Credential blocks (multi-line, highest specificity) ───────────
18
+ // PEM-armoured key: header + base64 body + footer. Match runs to the
19
+ // END footer (non-greedy across newlines) so the whole block — armor
20
+ // headers and all — is masked as a single unit.
21
+ const PEM_RX = /-----BEGIN (?:RSA |EC |DSA |OPENSSH |ENCRYPTED |PGP |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |ENCRYPTED |PGP |)PRIVATE KEY-----/g;
22
+ // Raw OpenSSH private key body when a user pasted only the body
23
+ // without the BEGIN/END armour. Conservative: requires the canonical
24
+ // `b3BlbnNzaC1rZXk` magic prefix (base64 of "openssh-key") to keep
25
+ // the FP rate near zero.
26
+ const SSH_KEY_BODY_RX = /b3BlbnNzaC1rZXk[A-Za-z0-9+/=\n\r]{60,}/g;
27
+ // ~/.aws/credentials INI block: starts at a `[profile]` section
28
+ // header that immediately precedes one of the canonical AWS keys,
29
+ // runs through whatever AWS settings follow until a blank line.
30
+ // Also catches `aws_access_key_id = …` lines outside an INI section
31
+ // (one-off pasted line from `aws configure`).
32
+ const AWS_INI_RX = /(?:\[[^\]\n]+\]\s*\n)?(?:aws_(?:access_key_id|secret_access_key|session_token)\s*=\s*\S+\s*\n?){1,4}/g;
33
+ // gcloud INI line. `gcloud auth print-access-token` output is just a
34
+ // bare `ya29.…` token — covered by the api-key vendor list. The INI
35
+ // rule here catches `~/.config/gcloud/application_default_credentials.json`
36
+ // JSON paste where keys leak as fielded values.
37
+ const GCLOUD_JSON_RX = /"(?:refresh_token|client_secret|access_token)"\s*:\s*"[A-Za-z0-9_\-./]{20,}"/g;
38
+ // kubeconfig token / cert-data fields. Either YAML or JSON shape.
39
+ // Catches `token:`, `client-certificate-data:`, `client-key-data:`,
40
+ // `id-token:`, `refresh-token:`. The value is base64 PEM or an
41
+ // opaque bearer — either way it must not ship.
42
+ const KUBECONFIG_RX = /\b(?:token|client-certificate-data|client-key-data|certificate-authority-data|id-token|refresh-token)\s*:\s*[A-Za-z0-9+/=_\-.]{20,}\b/g;
43
+ // .netrc machine/login/password line — single-line shape since that's
44
+ // how it's usually pasted from a terminal.
45
+ const NETRC_RX = /\bmachine\s+\S+\s+login\s+\S+\s+password\s+\S+/g;
46
+ // Database connection strings — scheme-aware, capture the whole URI
47
+ // so the host/db/user/password are all masked.
48
+ const CONN_STRING_RX = /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis(?:s)?|amqps?|mssql|sqlserver|jdbc:[a-z]+|cassandra|clickhouse|kafka|nats):\/\/[^\s"'`<>]+/gi;
49
+ // ── Single-token credentials ─────────────────────────────────────
50
+ const JWT_RX = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
51
+ // Vendor-prefixed credentials. Order each alt so the most specific
52
+ // pattern is unambiguous — `sk-ant-` before `sk-` (with a lookahead
53
+ // on the latter to avoid double-matching Anthropic keys as OpenAI).
54
+ const VENDOR_API_KEY_RX = new RegExp('\\b(?:' + [
55
+ // Payment / SaaS
56
+ 'sk_(?:live|test)_[A-Za-z0-9]{16,}',
57
+ 'rk_(?:live|test)_[A-Za-z0-9]{16,}',
58
+ 'pk_(?:live|test)_[A-Za-z0-9]{16,}',
59
+ // AI vendors
60
+ 'sk-ant-[A-Za-z0-9_-]{32,}',
61
+ 'sk-proj-[A-Za-z0-9_-]{32,}',
62
+ 'sk-(?!ant-|proj-)[A-Za-z0-9]{32,}',
63
+ 'hf_[A-Za-z0-9]{30,}',
64
+ // Source forges
65
+ 'gh[pousr]_[A-Za-z0-9]{36}',
66
+ 'glpat-[A-Za-z0-9_-]{20}',
67
+ // Cloud providers
68
+ 'AKIA[0-9A-Z]{16}',
69
+ 'ASIA[0-9A-Z]{16}',
70
+ 'AIza[0-9A-Za-z_-]{35}',
71
+ 'ya29\\.[A-Za-z0-9_-]{40,}',
72
+ 'AccDB[A-Za-z0-9+/=]{40,}',
73
+ 'dop_v1_[A-Fa-f0-9]{64}',
74
+ 'vc_[A-Za-z0-9]{24,}',
75
+ 'vercel_[A-Za-z0-9]{24,}',
76
+ // CDN / PaaS
77
+ 'CFPAT-[A-Za-z0-9_-]{40,}',
78
+ 'dckr_pat_[A-Za-z0-9_-]{27,}',
79
+ // Comms / mail
80
+ 'xox[abprs]-[A-Za-z0-9-]{10,}',
81
+ 'xapp-[A-Za-z0-9-]{10,}',
82
+ 'SG\\.[A-Za-z0-9_-]{20,24}\\.[A-Za-z0-9_-]{39,50}',
83
+ 'key-[a-f0-9]{32}',
84
+ 'AC[a-f0-9]{32}',
85
+ 'SK[a-f0-9]{32}',
86
+ 'sq0csp-[A-Za-z0-9_-]{43}',
87
+ // Package managers
88
+ 'npm_[A-Za-z0-9]{36}',
89
+ 'pypi-AgEIc[A-Za-z0-9_-]{50,}',
90
+ // Data platforms
91
+ 'dapi[a-f0-9]{32}',
92
+ // Telemetry / monitoring
93
+ 'datadog_api_key_[a-f0-9]{32}',
94
+ ].map((p) => `(?:${p})`).join('|') + ')\\b', 'g');
95
+ const BEARER_RX = /\b[Bb]earer\s+[A-Za-z0-9_\-.=+/]{16,}\b/g;
96
+ const BASIC_AUTH_RX = /\b[Bb]asic\s+[A-Za-z0-9+/=]{12,}={0,2}\b/g;
97
+ const URL_CREDS_RX = /\b[a-z][a-z0-9+\-.]*:\/\/[^\s/@:"'`<>]+:[^\s/@:"'`<>]+@[A-Za-z0-9.\-]+(?::\d+)?(?:\/[^\s"'`<>]*)?/g;
98
+ // NAME=VALUE assignment with a credential-shaped suffix. Captures
99
+ // the whole assignment so the name (which itself leaks vendor
100
+ // intent) is masked alongside the value.
101
+ const ENV_VAR_RX = /\b[A-Z][A-Z0-9_]{2,}(?:_KEY|_SECRET|_TOKEN|_PASSWORD|_PASSWD|_PWD|_API_?KEY|_DSN|_URL)\s*=\s*\S+/g;
102
+ const GENERIC_SECRET_RX = /\b(?:api[_-]?key|secret|token|password|passwd|auth|access[_-]?key|client[_-]?secret|webhook)\b\s*[:=]\s*["']([A-Za-z0-9+/=_\-]{20,})["']/gi;
103
+ // ── Identity ─────────────────────────────────────────────────────
104
+ const CC_RX = /\b(?:4\d{3}|5[1-5]\d{2}|6(?:011|5\d{2})|3[47]\d{2}|3(?:0[0-5]|[68]\d)\d|(?:2131|1800|35\d{3}))[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{3,4}\b/g;
105
+ const SSN_RX = /\b(?!000|666|9\d{2})\d{3}-(?!00)\d{2}-(?!0000)\d{4}\b/g;
106
+ const EMAIL_RX = /\b[A-Za-z0-9](?:[A-Za-z0-9._+-]*[A-Za-z0-9])?@[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?\.[A-Za-z]{2,}\b/g;
107
+ const PHONE_RX = /(?:\+\d[\d .\-()]{7,16}\d|\(\d{2,4}\)[\d .\-]{6,14}\d)/g;
108
+ const IPV4_RX = /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\b/g;
109
+ // Permissive match; `validIPv6` below rejects look-alikes (timestamps,
110
+ // short colon-separated number strings). Real IPv6 either uses the
111
+ // compressed `::` marker or contains the full 8 groups.
112
+ const IPV6_RX = /\b(?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}\b|\b(?:[0-9a-fA-F]{1,4}:){1,7}:|\b(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}\b/g;
113
+ /** Distinguishes real IPv6 addresses from look-alikes the broad
114
+ * regex above also accepts. Timestamps like `12:22:57` and other
115
+ * short colon-separated decimal sequences must be rejected:
116
+ *
117
+ * - A canonical IPv6 has 8 groups (`2001:0db8:…:7334`).
118
+ * - The `::` compression marker is unambiguous when present.
119
+ *
120
+ * Anything else (e.g. 3-group decimal time-of-day) we drop. */
121
+ function validIPv6(value) {
122
+ if (value.includes('::'))
123
+ return true;
124
+ return value.split(':').length === 8;
125
+ }
126
+ // ── Location & infra ─────────────────────────────────────────────
127
+ const ABSOLUTE_PATH_RX = /(?:\/Users\/|\/home\/|\/var\/|\/etc\/|\/opt\/|[A-Z]:\\Users\\)[A-Za-z0-9._\-/\\À-￿]+/g;
128
+ // Internal-only hostnames that aren't routable on the public DNS
129
+ // but reveal an org's network shape.
130
+ //
131
+ // Note `.local` is intentionally OMITTED: while it's the mDNS TLD,
132
+ // it's also the dominant filename suffix (`.env.local`,
133
+ // `settings.local`, `next.config.local`, …) — and we saw ~50%
134
+ // false-positive rate from a real-world scan with `.local` enabled.
135
+ // The remaining TLDs (`.internal`, `.corp`, `.lan`, `.intra`,
136
+ // `.home`, `.prod.*`, `.stg.*`) are unambiguous internal-DNS markers.
137
+ const INTERNAL_HOST_RX = /\b[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*\.(?:internal|corp|lan|intra|home|prod\.[a-z0-9]+|stg\.[a-z0-9]+)\b/gi;
138
+ const RULES = [
139
+ { kind: 'private-key', rx: PEM_RX, confidence: 1.0 },
140
+ { kind: 'ssh-key', rx: SSH_KEY_BODY_RX, confidence: 0.95 },
141
+ { kind: 'cloud-cred-ini', rx: AWS_INI_RX, confidence: 0.95 },
142
+ { kind: 'cloud-cred-ini', rx: GCLOUD_JSON_RX, confidence: 0.9 },
143
+ { kind: 'kubeconfig-token', rx: KUBECONFIG_RX, confidence: 0.85 },
144
+ { kind: 'netrc', rx: NETRC_RX, confidence: 0.95 },
145
+ { kind: 'connection-string', rx: CONN_STRING_RX, confidence: 0.9 },
146
+ { kind: 'url-creds', rx: URL_CREDS_RX, confidence: 0.95 },
147
+ { kind: 'basic-auth', rx: BASIC_AUTH_RX, confidence: 0.9 },
148
+ { kind: 'env-var', rx: ENV_VAR_RX, confidence: 0.9 },
149
+ { kind: 'generic-secret', rx: GENERIC_SECRET_RX, confidence: 0.6, validate: hasQuotedEntropy(4.0) },
150
+ { kind: 'jwt', rx: JWT_RX, confidence: 0.95 },
151
+ { kind: 'api-key', rx: VENDOR_API_KEY_RX, confidence: 0.98 },
152
+ { kind: 'bearer', rx: BEARER_RX, confidence: 0.85 },
153
+ { kind: 'credit-card', rx: CC_RX, confidence: 0.95, validate: luhnOk },
154
+ { kind: 'ssn', rx: SSN_RX, confidence: 0.85 },
155
+ { kind: 'email', rx: EMAIL_RX, confidence: 0.85 },
156
+ { kind: 'phone', rx: PHONE_RX, confidence: 0.7 },
157
+ { kind: 'ip', rx: IPV4_RX, confidence: 0.55 },
158
+ { kind: 'ip', rx: IPV6_RX, confidence: 0.6, validate: validIPv6 },
159
+ { kind: 'internal-host', rx: INTERNAL_HOST_RX, confidence: 0.55 },
160
+ { kind: 'absolute-path', rx: ABSOLUTE_PATH_RX, confidence: 0.75 },
161
+ ];
162
+ /** Synchronous regex-driven scan. Rules earlier in the priority list
163
+ * claim their regions first; later, broader rules whose match
164
+ * overlaps a claimed region are dropped. This is what makes a JWT
165
+ * inside a `Bearer` header surface as a JWT, and a Stripe key inside
166
+ * `STRIPE_SECRET_KEY=…` surface as the whole assignment. */
167
+ export function detectWithRegex(text, providerName = 'regex') {
168
+ const matches = [];
169
+ const claimed = [];
170
+ const overlaps = (s, e) => claimed.some((c) => s < c.end && e > c.start);
171
+ for (const rule of RULES) {
172
+ rule.rx.lastIndex = 0;
173
+ let m;
174
+ while ((m = rule.rx.exec(text)) !== null) {
175
+ const value = m[0];
176
+ const start = m.index;
177
+ const end = start + value.length;
178
+ const advance = () => {
179
+ if (m && m.index === rule.rx.lastIndex)
180
+ rule.rx.lastIndex++;
181
+ };
182
+ if (rule.validate && !rule.validate(value)) {
183
+ advance();
184
+ continue;
185
+ }
186
+ if (overlaps(start, end)) {
187
+ advance();
188
+ continue;
189
+ }
190
+ matches.push({
191
+ kind: rule.kind,
192
+ value,
193
+ start,
194
+ end,
195
+ confidence: rule.confidence,
196
+ provider: providerName,
197
+ });
198
+ claimed.push({ start, end });
199
+ advance();
200
+ }
201
+ }
202
+ matches.sort((a, b) => a.start - b.start);
203
+ return matches;
204
+ }
205
+ //# sourceMappingURL=detectors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detectors.js","sourceRoot":"","sources":["../src/detectors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ export type { SensitiveKind, SensitiveMatch, SensitiveGroup, SensitiveValue, } from './types.js';
2
+ export { SENSITIVE_KIND_LABEL, SENSITIVE_KIND_ORDER, } from './types.js';
3
+ export { detectWithRegex } from './detectors.js';
4
+ export { regexProvider, analyzeWith, } from './providers.js';
5
+ export type { RedactProvider } from './providers.js';
6
+ export { luhnOk, shannon, hashValueForRedactExclude } from './validators.js';
7
+ export { maskValueByKind } from './mask.js';
8
+ export { HIGH_SEVERITY_KINDS, INFO_SEVERITY_KINDS, severityOf } from './severity.js';
9
+ export type { Severity } from './severity.js';
10
+ import type { SensitiveGroup, SensitiveMatch } from './types.js';
11
+ /** Convenience wrapper for the common "scan one string" call. Equivalent
12
+ * to `regexProvider.analyze(text)` minus the Promise — synchronous so
13
+ * it's safe to use during React render. */
14
+ export declare function detectSensitiveSpans(text: string): SensitiveMatch[];
15
+ /** Group matches by kind, deduplicating identical literals so the
16
+ * editor's expanded list shows one row per decision (not one row
17
+ * per occurrence). `group.count` keeps the total occurrence sum so
18
+ * the header `×N` still reflects how often a value appears; each
19
+ * per-value entry carries its own occurrence count for callers that
20
+ * want to surface duplicates explicitly. */
21
+ export declare function groupBySensitiveKind(matches: SensitiveMatch[]): SensitiveGroup[];
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAWA,YAAY,EACV,aAAa,EACb,cAAc,EACd,cAAc,EACd,cAAc,GACf,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAEhD,OAAO,EACL,aAAa,EACb,WAAW,GACZ,MAAM,gBAAgB,CAAA;AAEvB,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AAEpD,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,yBAAyB,EAAE,MAAM,iBAAiB,CAAA;AAE5E,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAE3C,OAAO,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AACpF,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAG7C,OAAO,KAAK,EAAE,cAAc,EAAiB,cAAc,EAAE,MAAM,YAAY,CAAA;AAG/E;;4CAE4C;AAC5C,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,EAAE,CAEnE;AAED;;;;;6CAK6C;AAC7C,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,cAAc,EAAE,GAAG,cAAc,EAAE,CA6BhF"}
package/dist/index.js ADDED
@@ -0,0 +1,62 @@
1
+ // @spool-lab/redact — sensitive-data detection for Spool sessions.
2
+ //
3
+ // Surfaces:
4
+ // • Share editor (pre-publish review of artifact content)
5
+ // • Security Scan (planned) — background sweep of every local
6
+ // `.spool` session, surfaces token/credential leaks in a report
7
+ // • CLI `spool doctor` (planned) — same pipeline, headless
8
+ //
9
+ // Everything here runs locally, with no network access of any kind.
10
+ // See `providers.ts` for the pluggable boundary.
11
+ export { SENSITIVE_KIND_LABEL, SENSITIVE_KIND_ORDER, } from './types.js';
12
+ export { detectWithRegex } from './detectors.js';
13
+ export { regexProvider, analyzeWith, } from './providers.js';
14
+ export { luhnOk, shannon, hashValueForRedactExclude } from './validators.js';
15
+ export { maskValueByKind } from './mask.js';
16
+ export { HIGH_SEVERITY_KINDS, INFO_SEVERITY_KINDS, severityOf } from './severity.js';
17
+ import { detectWithRegex } from './detectors.js';
18
+ import { SENSITIVE_KIND_ORDER } from './types.js';
19
+ /** Convenience wrapper for the common "scan one string" call. Equivalent
20
+ * to `regexProvider.analyze(text)` minus the Promise — synchronous so
21
+ * it's safe to use during React render. */
22
+ export function detectSensitiveSpans(text) {
23
+ return detectWithRegex(text);
24
+ }
25
+ /** Group matches by kind, deduplicating identical literals so the
26
+ * editor's expanded list shows one row per decision (not one row
27
+ * per occurrence). `group.count` keeps the total occurrence sum so
28
+ * the header `×N` still reflects how often a value appears; each
29
+ * per-value entry carries its own occurrence count for callers that
30
+ * want to surface duplicates explicitly. */
31
+ export function groupBySensitiveKind(matches) {
32
+ const byKind = new Map();
33
+ for (const m of matches) {
34
+ const list = byKind.get(m.kind) ?? [];
35
+ list.push(m);
36
+ byKind.set(m.kind, list);
37
+ }
38
+ return Array.from(byKind.entries())
39
+ .map(([kind, list]) => {
40
+ // Preserve first-seen order while counting duplicates.
41
+ const order = [];
42
+ const counts = new Map();
43
+ for (const m of list) {
44
+ const n = counts.get(m.value);
45
+ if (n === undefined) {
46
+ order.push(m.value);
47
+ counts.set(m.value, 1);
48
+ }
49
+ else {
50
+ counts.set(m.value, n + 1);
51
+ }
52
+ }
53
+ return {
54
+ kind,
55
+ count: list.length,
56
+ values: order.map((v) => ({ value: v, count: counts.get(v) })),
57
+ minConfidence: list.reduce((acc, m) => Math.min(acc, m.confidence), 1),
58
+ };
59
+ })
60
+ .sort((a, b) => SENSITIVE_KIND_ORDER.indexOf(a.kind) - SENSITIVE_KIND_ORDER.indexOf(b.kind));
61
+ }
62
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/mask.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { SensitiveKind } from './types.js';
2
+ /** Compute the per-kind replacement string for a detected match.
3
+ * `kind` is loosely typed so synthetic categories (author names,
4
+ * manual entries) also get a sensible default. */
5
+ export declare function maskValueByKind(value: string, kind: SensitiveKind | string): string;
6
+ //# sourceMappingURL=mask.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mask.d.ts","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C;;mDAEmD;AACnD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,MAAM,GAAG,MAAM,CA0GnF"}
package/dist/mask.js ADDED
@@ -0,0 +1,184 @@
1
+ // Per-kind redaction masks.
2
+ //
3
+ // The naive substitution — every match → `[redacted]` — works but
4
+ // throws away too much context. A reader of the published artifact
5
+ // sees a wall of identical placeholders with no idea which were
6
+ // credentials, which were emails, which were paths. Per-kind masks
7
+ // keep just enough structure for the artifact to remain readable:
8
+ //
9
+ // STRIPE_SECRET_KEY=sk_live_abc… → STRIPE_SECRET_KEY=[redacted]
10
+ // maya@example.com → m***@example.com
11
+ // 4111 1111 1111 1111 → **** **** **** 1111
12
+ // postgres://u:p@host/db → postgres://[redacted]
13
+ // ghp_abcdef… → [redacted: GitHub key]
14
+ //
15
+ // Trade-off: a mask that retains structure (scheme, domain, last-4,
16
+ // vendor name) leaks slightly more than `[redacted]`. The judgement
17
+ // call is that those fragments are NOT secrets themselves — the
18
+ // scheme of a connection string is public, the domain of an email
19
+ // is usually public, vendor token prefixes are documented. What's
20
+ // secret is the random tail.
21
+ /** Compute the per-kind replacement string for a detected match.
22
+ * `kind` is loosely typed so synthetic categories (author names,
23
+ * manual entries) also get a sensible default. */
24
+ export function maskValueByKind(value, kind) {
25
+ switch (kind) {
26
+ case 'private-key':
27
+ return `[redacted private key · ${formatBytes(value.length)}]`;
28
+ case 'ssh-key':
29
+ return `[redacted SSH key · ${formatBytes(value.length)}]`;
30
+ case 'api-key': {
31
+ const vendor = detectVendor(value);
32
+ return vendor ? `[redacted: ${vendor} key]` : '[redacted API key]';
33
+ }
34
+ case 'jwt':
35
+ return '[redacted JWT]';
36
+ case 'cloud-cred-ini': {
37
+ if (/aws_(access|secret|session)_/i.test(value))
38
+ return '[redacted AWS credentials]';
39
+ if (/refresh_token|client_secret|access_token/i.test(value))
40
+ return '[redacted Google credentials]';
41
+ return '[redacted cloud credentials]';
42
+ }
43
+ case 'kubeconfig-token': {
44
+ const m = value.match(/^([a-z-]+)\s*:/i);
45
+ return m ? `${m[1]}: [redacted]` : '[redacted kubeconfig token]';
46
+ }
47
+ case 'netrc':
48
+ return 'machine [redacted] login [redacted] password [redacted]';
49
+ case 'connection-string': {
50
+ const m = value.match(/^([a-z][a-z0-9+\-.]*):\/\//i);
51
+ return m ? `${m[1]}://[redacted]` : '[redacted connection string]';
52
+ }
53
+ case 'url-creds': {
54
+ const m = value.match(/^([a-z][a-z0-9+\-.]*):\/\/[^@]+@([^\s/]+)(\/[^\s]*)?$/i);
55
+ return m ? `${m[1]}://[redacted:redacted]@${m[2]}${m[3] ?? ''}` : '[redacted URL]';
56
+ }
57
+ case 'bearer':
58
+ return 'Bearer [redacted]';
59
+ case 'basic-auth':
60
+ return 'Basic [redacted]';
61
+ case 'env-var': {
62
+ // NAME=VALUE — preserve NAME, mask VALUE. The user has already
63
+ // consented to redact by toggling, so the assignment NAME is
64
+ // surfaced to keep the line readable.
65
+ const m = value.match(/^([A-Z][A-Z0-9_]*)\s*=/);
66
+ return m ? `${m[1]}=[redacted]` : '[redacted env-var]';
67
+ }
68
+ case 'generic-secret': {
69
+ // The match capture is "keyword : value" or "keyword = value".
70
+ // Preserve the keyword prefix verbatim so the assignment shape
71
+ // survives.
72
+ const m = value.match(/^([^"'\s:=]+\s*[:=]\s*)["']/);
73
+ return m ? `${m[1]}"[redacted]"` : '[redacted secret]';
74
+ }
75
+ case 'credit-card': {
76
+ const digits = value.replace(/\D/g, '');
77
+ const last4 = digits.slice(-4) || '****';
78
+ return `**** **** **** ${last4}`;
79
+ }
80
+ case 'ssn': {
81
+ const last4 = value.slice(-4);
82
+ return `***-**-${last4}`;
83
+ }
84
+ case 'email': {
85
+ const at = value.indexOf('@');
86
+ if (at <= 0)
87
+ return '[redacted email]';
88
+ const first = value[0] ?? '';
89
+ const domain = value.slice(at + 1);
90
+ // Don't reveal more than the first char of the local part. For
91
+ // single-char local parts ("a@x.com"), use a literal asterisk.
92
+ return `${first}***@${domain}`;
93
+ }
94
+ case 'phone':
95
+ return '[redacted phone]';
96
+ case 'person-name':
97
+ return '[redacted name]';
98
+ case 'street-address':
99
+ return '[redacted address]';
100
+ case 'date-of-birth':
101
+ return '[redacted DOB]';
102
+ case 'ip':
103
+ // Preserve shape so readers know it was a network address; the
104
+ // four-octet pattern matters more than the digits.
105
+ return value.includes(':') ? '[redacted IPv6]' : '[redacted IPv4]';
106
+ case 'internal-host': {
107
+ const m = value.match(/\.([a-z0-9-]+(?:\.[a-z0-9-]+)*)$/i);
108
+ return m ? `[redacted].${m[1]}` : '[redacted internal host]';
109
+ }
110
+ case 'absolute-path': {
111
+ const m = value.match(/^(\/Users\/|\/home\/|\/var\/|\/etc\/|\/opt\/|[A-Z]:\\Users\\)/);
112
+ return m ? `${m[1]}[redacted]` : '[redacted path]';
113
+ }
114
+ // Synthetic kinds emitted by the Share editor's PII layer.
115
+ case 'synthetic:author':
116
+ return '[redacted name]';
117
+ case 'synthetic:manual':
118
+ return '[redacted]';
119
+ default:
120
+ return '[redacted]';
121
+ }
122
+ }
123
+ /** Recognise the vendor behind a token by its prefix. Returns null
124
+ * when the prefix isn't one we've codified — the caller falls
125
+ * back to a generic `[redacted API key]` mask. */
126
+ function detectVendor(token) {
127
+ if (/^sk_(live|test)_/.test(token))
128
+ return 'Stripe';
129
+ if (/^(rk|pk)_(live|test)_/.test(token))
130
+ return 'Stripe';
131
+ if (/^sk-ant-/.test(token))
132
+ return 'Anthropic';
133
+ if (/^sk-proj-/.test(token))
134
+ return 'OpenAI';
135
+ if (/^sk-/.test(token))
136
+ return 'OpenAI';
137
+ if (/^hf_/.test(token))
138
+ return 'Hugging Face';
139
+ if (/^gh[pousr]_/.test(token))
140
+ return 'GitHub';
141
+ if (/^glpat-/.test(token))
142
+ return 'GitLab';
143
+ if (/^(AKIA|ASIA)/.test(token))
144
+ return 'AWS';
145
+ if (/^AIza/.test(token))
146
+ return 'Google';
147
+ if (/^ya29\./.test(token))
148
+ return 'Google';
149
+ if (/^xox[abprs]-|^xapp-/.test(token))
150
+ return 'Slack';
151
+ if (/^SG\./.test(token))
152
+ return 'SendGrid';
153
+ if (/^key-[a-f0-9]/.test(token))
154
+ return 'Mailgun';
155
+ if (/^AC[a-f0-9]/.test(token))
156
+ return 'Twilio';
157
+ if (/^SK[a-f0-9]/.test(token))
158
+ return 'Twilio';
159
+ if (/^sq0csp-/.test(token))
160
+ return 'Square';
161
+ if (/^npm_/.test(token))
162
+ return 'npm';
163
+ if (/^pypi-/.test(token))
164
+ return 'PyPI';
165
+ if (/^dop_v1_/.test(token))
166
+ return 'DigitalOcean';
167
+ if (/^vc_|^vercel_/.test(token))
168
+ return 'Vercel';
169
+ if (/^CFPAT-/.test(token))
170
+ return 'Cloudflare';
171
+ if (/^dckr_pat_/.test(token))
172
+ return 'Docker Hub';
173
+ if (/^dapi/.test(token))
174
+ return 'Databricks';
175
+ return null;
176
+ }
177
+ function formatBytes(n) {
178
+ if (n < 1024)
179
+ return `${n} B`;
180
+ if (n < 1024 * 1024)
181
+ return `${(n / 1024).toFixed(1)} KB`;
182
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
183
+ }
184
+ //# sourceMappingURL=mask.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mask.js","sourceRoot":"","sources":["../src/mask.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { SensitiveMatch } from './types.js';
2
+ export interface RedactProvider {
3
+ /** Stable identifier used in `SensitiveMatch.provider` and shown
4
+ * to the user when explaining where a flag came from. */
5
+ readonly name: string;
6
+ /** Human-readable label for settings UI. */
7
+ readonly displayName: string;
8
+ /** True when the provider is loaded and ready to call `analyze`.
9
+ * Always true for regex; future ML providers return false until
10
+ * the model bundle is on disk. */
11
+ available(): boolean;
12
+ /** Run detection on the given text. Even synchronous providers
13
+ * return a Promise so callers can compose providers uniformly. */
14
+ analyze(text: string): Promise<SensitiveMatch[]>;
15
+ }
16
+ /** The default, always-available provider. */
17
+ export declare const regexProvider: RedactProvider;
18
+ /** Merge results from multiple providers and de-overlap by priority.
19
+ * When two providers flag overlapping spans, the higher-priority
20
+ * provider wins. Higher priority = earlier in the array. */
21
+ export declare function analyzeWith(providers: RedactProvider[], text: string): Promise<SensitiveMatch[]>;
22
+ //# sourceMappingURL=providers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"providers.d.ts","sourceRoot":"","sources":["../src/providers.ts"],"names":[],"mappings":"AA8CA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAGhD,MAAM,WAAW,cAAc;IAC7B;8DAC0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,4CAA4C;IAC5C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B;;uCAEmC;IACnC,SAAS,IAAI,OAAO,CAAA;IACpB;uEACmE;IACnE,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAAA;CACjD;AAED,8CAA8C;AAC9C,eAAO,MAAM,aAAa,EAAE,cAK3B,CAAA;AAED;;6DAE6D;AAC7D,wBAAsB,WAAW,CAC/B,SAAS,EAAE,cAAc,EAAE,EAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,cAAc,EAAE,CAAC,CAgB3B"}
@@ -0,0 +1,74 @@
1
+ // Pluggable provider boundary for sensitive-data detection.
2
+ //
3
+ // `RedactProvider` is the single seam through which the Share editor,
4
+ // the future Security Scan feature, and the CLI all consume detection.
5
+ // Today the only implementation is `regexProvider` — deterministic,
6
+ // zero-dep, <1ms on a typical session body. Tomorrow another provider
7
+ // (e.g. OpenAI Privacy Filter served via transformers.js + WebGPU)
8
+ // can plug in *without touching call sites*.
9
+ //
10
+ // Local-first invariant: every provider must run entirely on-device.
11
+ // Network round-trips for detection are out of scope by policy —
12
+ // Spool data does not leave the user's machine.
13
+ //
14
+ // Why we are NOT shipping OpenAI Privacy Filter today
15
+ // (Apache-2.0, 1.5B/50M-active, 96% F1 on PII-Masking-300k):
16
+ //
17
+ // 1. The model bundle (Q4-quantised ONNX) is ~800 MB. Downloading
18
+ // that the first time a user opens the Share editor is a poor
19
+ // experience for an interactive surface; it makes more sense
20
+ // behind an explicit "enable enhanced detection" toggle, paired
21
+ // with the planned background Security Scan feature.
22
+ // 2. Privacy Filter's `secret` category is broad but does not
23
+ // reliably recognise the long tail of vendor token formats
24
+ // (`ghp_…`, `sk-ant-…`, `dop_v1_…`). Regex remains the right
25
+ // mechanism for structurally-distinctive credentials.
26
+ // 3. The marginal win is on prose-style PII — sentences like "my
27
+ // SSN is 123-45-6789", names embedded in free text, addresses
28
+ // in error messages — where regex fundamentally can't help.
29
+ // That win matters for the Security Scan report; for the
30
+ // Share editor (where the user is also reviewing the artifact
31
+ // visually) it is much less critical.
32
+ //
33
+ // Concretely, the integration path when we do enable it:
34
+ // • Add `@huggingface/transformers` as an optional dependency
35
+ // • Implement `privacyFilterProvider` in this file (or a sibling)
36
+ // that lazy-imports the package, downloads the ONNX bundle to
37
+ // userData/, and exposes `analyze(text): Promise<SensitiveMatch[]>`
38
+ // emitting matches with `provider: 'privacy-filter'`
39
+ // • Map its 8 entity types (`private_person`, `private_email`,
40
+ // `private_phone`, `private_url`, `private_address`,
41
+ // `private_date`, `account_number`, `secret`) to our existing
42
+ // SensitiveKind values, adding new kinds for what's net-new
43
+ // (`person-name`, `postal-address`, `date-of-birth`)
44
+ // • In the Settings panel, gate behind a toggle showing model
45
+ // size, where the file lives, and how to delete it
46
+ import { detectWithRegex } from './detectors.js';
47
+ /** The default, always-available provider. */
48
+ export const regexProvider = {
49
+ name: 'regex',
50
+ displayName: 'Pattern matcher (built-in)',
51
+ available: () => true,
52
+ analyze: async (text) => detectWithRegex(text, 'regex'),
53
+ };
54
+ /** Merge results from multiple providers and de-overlap by priority.
55
+ * When two providers flag overlapping spans, the higher-priority
56
+ * provider wins. Higher priority = earlier in the array. */
57
+ export async function analyzeWith(providers, text) {
58
+ if (providers.length === 0)
59
+ return [];
60
+ const lists = await Promise.all(providers.map((p) => (p.available() ? p.analyze(text) : Promise.resolve([]))));
61
+ const claimed = [];
62
+ const out = [];
63
+ // Priority = order in `providers`; lower index wins.
64
+ for (const matches of lists) {
65
+ for (const m of matches) {
66
+ if (claimed.some((c) => m.start < c.end && m.end > c.start))
67
+ continue;
68
+ out.push(m);
69
+ claimed.push({ start: m.start, end: m.end });
70
+ }
71
+ }
72
+ return out.sort((a, b) => a.start - b.start);
73
+ }
74
+ //# sourceMappingURL=providers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"providers.js","sourceRoot":"","sources":["../src/providers.ts"],"names":[],"mappings":"AAAA,4DAA4D;AAC5D,EAAE;AACF,sEAAsE;AACtE,uEAAuE;AACvE,oEAAoE;AACpE,sEAAsE;AACtE,mEAAmE;AACnE,6CAA6C;AAC7C,EAAE;AACF,qEAAqE;AACrE,iEAAiE;AACjE,gDAAgD;AAChD,EAAE;AACF,sDAAsD;AACtD,6DAA6D;AAC7D,EAAE;AACF,oEAAoE;AACpE,mEAAmE;AACnE,kEAAkE;AAClE,qEAAqE;AACrE,0DAA0D;AAC1D,gEAAgE;AAChE,gEAAgE;AAChE,kEAAkE;AAClE,2DAA2D;AAC3D,mEAAmE;AACnE,mEAAmE;AACnE,iEAAiE;AACjE,8DAA8D;AAC9D,mEAAmE;AACnE,2CAA2C;AAC3C,EAAE;AACF,yDAAyD;AACzD,gEAAgE;AAChE,oEAAoE;AACpE,kEAAkE;AAClE,wEAAwE;AACxE,yDAAyD;AACzD,iEAAiE;AACjE,yDAAyD;AACzD,kEAAkE;AAClE,gEAAgE;AAChE,yDAAyD;AACzD,gEAAgE;AAChE,uDAAuD;AAGvD,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA;AAiBhD,8CAA8C;AAC9C,MAAM,CAAC,MAAM,aAAa,GAAmB;IAC3C,IAAI,EAAE,OAAO;IACb,WAAW,EAAE,4BAA4B;IACzC,SAAS,EAAE,GAAG,EAAE,CAAC,IAAI;IACrB,OAAO,EAAE,KAAK,EAAE,IAAY,EAAE,EAAE,CAAC,eAAe,CAAC,IAAI,EAAE,OAAO,CAAC;CAChE,CAAA;AAED;;6DAE6D;AAC7D,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,SAA2B,EAC3B,IAAY;IAEZ,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACrC,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,CAC7B,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,EAAsB,CAAC,CAAC,CAAC,CAClG,CAAA;IACD,MAAM,OAAO,GAAqC,EAAE,CAAA;IACpD,MAAM,GAAG,GAAqB,EAAE,CAAA;IAChC,qDAAqD;IACrD,KAAK,MAAM,OAAO,IAAI,KAAK,EAAE,CAAC;QAC5B,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACxB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC;gBAAE,SAAQ;YACrE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACX,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAA;QAC9C,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAA;AAC9C,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { SensitiveKind } from './types.js';
2
+ export declare const HIGH_SEVERITY_KINDS: ReadonlySet<SensitiveKind>;
3
+ /** Pattern hits that are useful as signal but are almost never an
4
+ * actual sensitive-data leak. Real-world audit on a 317-session
5
+ * archive showed ~100% noise from these kinds (file paths flagged
6
+ * every cwd reference, timestamps misclassified as IPv6, mDNS
7
+ * hostnames overlapping with `.env.local` filenames). Keeping them
8
+ * off the default Security view is the only way the surface stays
9
+ * signal-dense. */
10
+ export declare const INFO_SEVERITY_KINDS: ReadonlySet<SensitiveKind>;
11
+ export type Severity = 'high' | 'low' | 'info';
12
+ export declare function severityOf(kind: SensitiveKind): Severity;
13
+ //# sourceMappingURL=severity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"severity.d.ts","sourceRoot":"","sources":["../src/severity.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,aAAa,CAczD,CAAA;AAEF;;;;;;oBAMoB;AACpB,eAAO,MAAM,mBAAmB,EAAE,WAAW,CAAC,aAAa,CAIzD,CAAA;AAEF,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,CAAA;AAE9C,wBAAgB,UAAU,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,CAIxD"}
@@ -0,0 +1,51 @@
1
+ // Severity tiers for the Security Scan surfaces.
2
+ //
3
+ // high — a single leak is potentially catastrophic. Credentials,
4
+ // cred files, structured tokens.
5
+ // low — annoying but recoverable. Identity-level (email / phone /
6
+ // person-name / address / DOB / credit-card / SSN).
7
+ // info — signal-only, not a credential leak. File paths, IP
8
+ // addresses, internal-DNS hostnames. Audit-positive to
9
+ // collect, but heavy false-positive rate when surfaced as
10
+ // "findings" — the Security page hides this tier by default
11
+ // and offers an opt-in toggle.
12
+ //
13
+ // The Library badge colour, the Security page Risk-panel grouping,
14
+ // and the inline-confirm policy for Dismiss vs Purge all derive from
15
+ // this three-way split. `scan_high_count` only counts `high`; the
16
+ // session badge only fires when a finding is `high` or `low`.
17
+ export const HIGH_SEVERITY_KINDS = new Set([
18
+ 'private-key',
19
+ 'ssh-key',
20
+ 'cloud-cred-ini',
21
+ 'kubeconfig-token',
22
+ 'netrc',
23
+ 'connection-string',
24
+ 'url-creds',
25
+ 'api-key',
26
+ 'jwt',
27
+ 'bearer',
28
+ 'basic-auth',
29
+ 'env-var',
30
+ 'generic-secret',
31
+ ]);
32
+ /** Pattern hits that are useful as signal but are almost never an
33
+ * actual sensitive-data leak. Real-world audit on a 317-session
34
+ * archive showed ~100% noise from these kinds (file paths flagged
35
+ * every cwd reference, timestamps misclassified as IPv6, mDNS
36
+ * hostnames overlapping with `.env.local` filenames). Keeping them
37
+ * off the default Security view is the only way the surface stays
38
+ * signal-dense. */
39
+ export const INFO_SEVERITY_KINDS = new Set([
40
+ 'absolute-path',
41
+ 'ip',
42
+ 'internal-host',
43
+ ]);
44
+ export function severityOf(kind) {
45
+ if (HIGH_SEVERITY_KINDS.has(kind))
46
+ return 'high';
47
+ if (INFO_SEVERITY_KINDS.has(kind))
48
+ return 'info';
49
+ return 'low';
50
+ }
51
+ //# sourceMappingURL=severity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"severity.js","sourceRoot":"","sources":["../src/severity.ts"],"names":[],"mappings":"AAAA,iDAAiD;AACjD,EAAE;AACF,mEAAmE;AACnE,0CAA0C;AAC1C,qEAAqE;AACrE,6DAA6D;AAC7D,8DAA8D;AAC9D,gEAAgE;AAChE,mEAAmE;AACnE,qEAAqE;AACrE,wCAAwC;AACxC,EAAE;AACF,mEAAmE;AACnE,qEAAqE;AACrE,kEAAkE;AAClE,8DAA8D;AAI9D,MAAM,CAAC,MAAM,mBAAmB,GAA+B,IAAI,GAAG,CAAC;IACrE,aAAa;IACb,SAAS;IACT,gBAAgB;IAChB,kBAAkB;IAClB,OAAO;IACP,mBAAmB;IACnB,WAAW;IACX,SAAS;IACT,KAAK;IACL,QAAQ;IACR,YAAY;IACZ,SAAS;IACT,gBAAgB;CACjB,CAAC,CAAA;AAEF;;;;;;oBAMoB;AACpB,MAAM,CAAC,MAAM,mBAAmB,GAA+B,IAAI,GAAG,CAAC;IACrE,eAAe;IACf,IAAI;IACJ,eAAe;CAChB,CAAC,CAAA;AAIF,MAAM,UAAU,UAAU,CAAC,IAAmB;IAC5C,IAAI,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,MAAM,CAAA;IAChD,IAAI,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,MAAM,CAAA;IAChD,OAAO,KAAK,CAAA;AACd,CAAC"}
@@ -0,0 +1,57 @@
1
+ /** Every category the detector can emit. New kinds get appended; the
2
+ * set is exhaustive so consumers can pattern-match it safely. */
3
+ export type SensitiveKind = 'private-key' | 'ssh-key' | 'api-key' | 'cloud-cred-ini' | 'kubeconfig-token' | 'connection-string' | 'jwt' | 'bearer' | 'basic-auth' | 'env-var' | 'generic-secret' | 'netrc' | 'email' | 'phone' | 'person-name' | 'street-address' | 'date-of-birth' | 'credit-card' | 'ssn' | 'ip' | 'url-creds' | 'absolute-path' | 'internal-host';
4
+ export interface SensitiveMatch {
5
+ kind: SensitiveKind;
6
+ /** The literal substring detected, suitable to pass to a redact
7
+ * substituter. For multi-line kinds (private-key, cloud-cred-ini)
8
+ * this is the entire block so the whole thing gets masked. */
9
+ value: string;
10
+ /** Character offset within the input string. */
11
+ start: number;
12
+ /** Exclusive end offset. */
13
+ end: number;
14
+ /** Heuristic 0–1.
15
+ * 1.0 Structural certainty — PEM block, Luhn-valid card.
16
+ * ≥0.9 Strong vendor prefix or composite signal.
17
+ * ≥0.7 Format match without checksum.
18
+ * <0.7 Context-only signal, surface for user review.
19
+ */
20
+ confidence: number;
21
+ /** Which provider produced this match (`'regex'` today; future
22
+ * values: `'privacy-filter'`, `'gliner'`, …). Lets the editor UI
23
+ * show "model-detected" vs "rule-detected" badges and lets a
24
+ * future security-scan report attribute matches. */
25
+ provider: string;
26
+ }
27
+ export interface SensitiveValue {
28
+ value: string;
29
+ /** Number of times this exact literal appeared in the source.
30
+ * Always ≥ 1. Useful for surfacing "same token printed twice"
31
+ * without rendering duplicate rows. */
32
+ count: number;
33
+ }
34
+ export interface SensitiveGroup {
35
+ kind: SensitiveKind;
36
+ /** Total number of matches in this group (sum of every value's
37
+ * occurrence count). Drives the `×N` header label. */
38
+ count: number;
39
+ /** Distinct values for this kind, in first-seen detection order,
40
+ * each paired with its occurrence count. Editor renders one row
41
+ * per entry — so a value that appears 3× is one decision, not
42
+ * three. */
43
+ values: SensitiveValue[];
44
+ /** Worst-case (lowest) confidence in this group — drives the
45
+ * "review" badge in the editor. */
46
+ minConfidence: number;
47
+ }
48
+ /** Display order — strict risk descent. Credentials (a single leak
49
+ * is potentially catastrophic) come first; identity / location
50
+ * signals (annoying but recoverable) come last. Items that travel
51
+ * together stay adjacent: cred files (PEM / SSH / cloud INI /
52
+ * kubeconfig / netrc); URL-form creds (conn-string / url-creds);
53
+ * token-form creds (api-key / jwt / bearer / basic-auth /
54
+ * env-var / generic-secret); finally financial / identity / infra. */
55
+ export declare const SENSITIVE_KIND_ORDER: SensitiveKind[];
56
+ export declare const SENSITIVE_KIND_LABEL: Record<SensitiveKind, string>;
57
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAOA;kEACkE;AAClE,MAAM,MAAM,aAAa,GAErB,aAAa,GACb,SAAS,GACT,SAAS,GACT,gBAAgB,GAChB,kBAAkB,GAClB,mBAAmB,GACnB,KAAK,GACL,QAAQ,GACR,YAAY,GACZ,SAAS,GACT,gBAAgB,GAChB,OAAO,GAEP,OAAO,GACP,OAAO,GACP,aAAa,GACb,gBAAgB,GAChB,eAAe,GACf,aAAa,GACb,KAAK,GACL,IAAI,GAEJ,WAAW,GACX,eAAe,GACf,eAAe,CAAA;AAEnB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,aAAa,CAAA;IACnB;;mEAE+D;IAC/D,KAAK,EAAE,MAAM,CAAA;IACb,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAA;IACb,4BAA4B;IAC5B,GAAG,EAAE,MAAM,CAAA;IACX;;;;;OAKG;IACH,UAAU,EAAE,MAAM,CAAA;IAClB;;;yDAGqD;IACrD,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb;;4CAEwC;IACxC,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,aAAa,CAAA;IACnB;2DACuD;IACvD,KAAK,EAAE,MAAM,CAAA;IACb;;;iBAGa;IACb,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB;wCACoC;IACpC,aAAa,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;uEAMuE;AACvE,eAAO,MAAM,oBAAoB,EAAE,aAAa,EAwB/C,CAAA;AAED,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,aAAa,EAAE,MAAM,CAwB9D,CAAA"}
package/dist/types.js ADDED
@@ -0,0 +1,64 @@
1
+ // Public types for @spool-lab/redact.
2
+ //
3
+ // Stable interface so callers (Share editor, future Security Scan,
4
+ // CLI doctor) don't break when we add detector kinds or swap the
5
+ // internal provider. New `SensitiveKind` values are additive — never
6
+ // rename or repurpose existing ones.
7
+ /** Display order — strict risk descent. Credentials (a single leak
8
+ * is potentially catastrophic) come first; identity / location
9
+ * signals (annoying but recoverable) come last. Items that travel
10
+ * together stay adjacent: cred files (PEM / SSH / cloud INI /
11
+ * kubeconfig / netrc); URL-form creds (conn-string / url-creds);
12
+ * token-form creds (api-key / jwt / bearer / basic-auth /
13
+ * env-var / generic-secret); finally financial / identity / infra. */
14
+ export const SENSITIVE_KIND_ORDER = [
15
+ 'private-key',
16
+ 'ssh-key',
17
+ 'cloud-cred-ini',
18
+ 'kubeconfig-token',
19
+ 'netrc',
20
+ 'connection-string',
21
+ 'url-creds',
22
+ 'api-key',
23
+ 'jwt',
24
+ 'bearer',
25
+ 'basic-auth',
26
+ 'env-var',
27
+ 'generic-secret',
28
+ 'credit-card',
29
+ 'ssn',
30
+ 'email',
31
+ 'phone',
32
+ 'person-name',
33
+ 'street-address',
34
+ 'date-of-birth',
35
+ 'ip',
36
+ 'absolute-path',
37
+ 'internal-host',
38
+ ];
39
+ export const SENSITIVE_KIND_LABEL = {
40
+ 'private-key': 'Private key',
41
+ 'ssh-key': 'SSH private key',
42
+ 'cloud-cred-ini': 'Cloud credentials',
43
+ 'kubeconfig-token': 'kubeconfig token',
44
+ 'connection-string': 'Connection string',
45
+ 'api-key': 'API key',
46
+ 'netrc': '.netrc entry',
47
+ 'jwt': 'JWT',
48
+ 'bearer': 'Bearer token',
49
+ 'basic-auth': 'Basic auth',
50
+ 'env-var': 'Env-var secret',
51
+ 'generic-secret': 'Generic secret',
52
+ 'url-creds': 'URL credential',
53
+ 'credit-card': 'Credit card',
54
+ 'ssn': 'SSN',
55
+ 'email': 'Email',
56
+ 'phone': 'Phone',
57
+ 'person-name': 'Person name',
58
+ 'street-address': 'Street address',
59
+ 'date-of-birth': 'Date of birth',
60
+ 'ip': 'IP address',
61
+ 'absolute-path': 'Absolute path',
62
+ 'internal-host': 'Internal hostname',
63
+ };
64
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,EAAE;AACF,mEAAmE;AACnE,iEAAiE;AACjE,qEAAqE;AACrE,qCAAqC;AA+ErC;;;;;;uEAMuE;AACvE,MAAM,CAAC,MAAM,oBAAoB,GAAoB;IACnD,aAAa;IACb,SAAS;IACT,gBAAgB;IAChB,kBAAkB;IAClB,OAAO;IACP,mBAAmB;IACnB,WAAW;IACX,SAAS;IACT,KAAK;IACL,QAAQ;IACR,YAAY;IACZ,SAAS;IACT,gBAAgB;IAChB,aAAa;IACb,KAAK;IACL,OAAO;IACP,OAAO;IACP,aAAa;IACb,gBAAgB;IAChB,eAAe;IACf,IAAI;IACJ,eAAe;IACf,eAAe;CAChB,CAAA;AAED,MAAM,CAAC,MAAM,oBAAoB,GAAkC;IACjE,aAAa,EAAE,aAAa;IAC5B,SAAS,EAAE,iBAAiB;IAC5B,gBAAgB,EAAE,mBAAmB;IACrC,kBAAkB,EAAE,kBAAkB;IACtC,mBAAmB,EAAE,mBAAmB;IACxC,SAAS,EAAE,SAAS;IACpB,OAAO,EAAE,cAAc;IACvB,KAAK,EAAE,KAAK;IACZ,QAAQ,EAAE,cAAc;IACxB,YAAY,EAAE,YAAY;IAC1B,SAAS,EAAE,gBAAgB;IAC3B,gBAAgB,EAAE,gBAAgB;IAClC,WAAW,EAAE,gBAAgB;IAC7B,aAAa,EAAE,aAAa;IAC5B,KAAK,EAAE,KAAK;IACZ,OAAO,EAAE,OAAO;IAChB,OAAO,EAAE,OAAO;IAChB,aAAa,EAAE,aAAa;IAC5B,gBAAgB,EAAE,gBAAgB;IAClC,eAAe,EAAE,eAAe;IAChC,IAAI,EAAE,YAAY;IAClB,eAAe,EAAE,eAAe;IAChC,eAAe,EAAE,mBAAmB;CACrC,CAAA"}
@@ -0,0 +1,25 @@
1
+ /** Luhn check for credit-card-shaped digit runs. Operates on the
2
+ * raw match (separators allowed); returns true if the embedded
3
+ * digit sequence is a valid mod-10 checksum. */
4
+ export declare function luhnOk(value: string): boolean;
5
+ /** Stable 32-bit non-cryptographic hash of `value`, hex-encoded as
6
+ * 8 lowercase characters. Used by the Share editor to persist per-
7
+ * item redact opt-outs in `RedactExclude.valueHashes` *without*
8
+ * storing the literal value.
9
+ *
10
+ * Threat model: an attacker who can read the persisted draft can
11
+ * also read the conversation body in the same file, so a stronger
12
+ * hash would not raise the bar. The goal here is to ensure the
13
+ * Share editor itself doesn't produce a NEW on-disk artifact that
14
+ * names a sensitive literal. FNV-1a is deterministic, sync, and
15
+ * fast enough to call inside a React render. */
16
+ export declare function hashValueForRedactExclude(value: string): string;
17
+ /** Shannon entropy of a string. Used to gate the generic-secret rule
18
+ * so that `password = "letmeinletmeinletmein"` (low-entropy) doesn't
19
+ * trigger but `password = "j82H1xK9pQrSt7VwYzA3"` does. */
20
+ export declare function shannon(s: string): number;
21
+ /** Curried entropy floor for use as a rule validator. The match
22
+ * string is expected to be a "keyword = value" capture; we pull the
23
+ * quoted body so we don't include the key name in the entropy. */
24
+ export declare function hasQuotedEntropy(min: number): (value: string) => boolean;
25
+ //# sourceMappingURL=validators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validators.d.ts","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"AASA;;iDAEiD;AACjD,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAe7C;AAED;;;;;;;;;;iDAUiD;AACjD,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAU/D;AAED;;4DAE4D;AAC5D,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAWzC;AAED;;mEAEmE;AACnE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAKxE"}
@@ -0,0 +1,78 @@
1
+ // Small validators used by the detector rules to suppress false
2
+ // positives that pure regex can't. Each is side-effect-free and
3
+ // returns boolean — discard-on-false at the detector level.
4
+ //
5
+ // Also home to `hashValueForRedactExclude`: the non-crypto hash
6
+ // used by the Share editor to record per-item opt-outs WITHOUT
7
+ // writing the literal value back to disk. See `RedactExclude` in
8
+ // `@spool/share-kit` for the threat model and rationale.
9
+ /** Luhn check for credit-card-shaped digit runs. Operates on the
10
+ * raw match (separators allowed); returns true if the embedded
11
+ * digit sequence is a valid mod-10 checksum. */
12
+ export function luhnOk(value) {
13
+ const digits = value.replace(/\D/g, '');
14
+ if (digits.length < 13 || digits.length > 19)
15
+ return false;
16
+ let sum = 0;
17
+ let alt = false;
18
+ for (let i = digits.length - 1; i >= 0; i--) {
19
+ let d = digits.charCodeAt(i) - 48;
20
+ if (alt) {
21
+ d *= 2;
22
+ if (d > 9)
23
+ d -= 9;
24
+ }
25
+ sum += d;
26
+ alt = !alt;
27
+ }
28
+ return sum % 10 === 0;
29
+ }
30
+ /** Stable 32-bit non-cryptographic hash of `value`, hex-encoded as
31
+ * 8 lowercase characters. Used by the Share editor to persist per-
32
+ * item redact opt-outs in `RedactExclude.valueHashes` *without*
33
+ * storing the literal value.
34
+ *
35
+ * Threat model: an attacker who can read the persisted draft can
36
+ * also read the conversation body in the same file, so a stronger
37
+ * hash would not raise the bar. The goal here is to ensure the
38
+ * Share editor itself doesn't produce a NEW on-disk artifact that
39
+ * names a sensitive literal. FNV-1a is deterministic, sync, and
40
+ * fast enough to call inside a React render. */
41
+ export function hashValueForRedactExclude(value) {
42
+ // FNV-1a 32-bit. Offset basis 0x811c9dc5, prime 0x01000193.
43
+ let h = 0x811c9dc5;
44
+ for (let i = 0; i < value.length; i++) {
45
+ h ^= value.charCodeAt(i);
46
+ // Force back to unsigned 32-bit after each multiply so JS's
47
+ // 53-bit-mantissa Number doesn't drift into floating-point.
48
+ h = Math.imul(h, 0x01000193) >>> 0;
49
+ }
50
+ return h.toString(16).padStart(8, '0');
51
+ }
52
+ /** Shannon entropy of a string. Used to gate the generic-secret rule
53
+ * so that `password = "letmeinletmeinletmein"` (low-entropy) doesn't
54
+ * trigger but `password = "j82H1xK9pQrSt7VwYzA3"` does. */
55
+ export function shannon(s) {
56
+ if (!s)
57
+ return 0;
58
+ const counts = new Map();
59
+ for (const c of s)
60
+ counts.set(c, (counts.get(c) ?? 0) + 1);
61
+ const len = s.length;
62
+ let h = 0;
63
+ for (const n of counts.values()) {
64
+ const p = n / len;
65
+ h -= p * Math.log2(p);
66
+ }
67
+ return h;
68
+ }
69
+ /** Curried entropy floor for use as a rule validator. The match
70
+ * string is expected to be a "keyword = value" capture; we pull the
71
+ * quoted body so we don't include the key name in the entropy. */
72
+ export function hasQuotedEntropy(min) {
73
+ return (value) => {
74
+ const inner = value.match(/["']([^"']{6,})["']/)?.[1] ?? value;
75
+ return shannon(inner) >= min;
76
+ };
77
+ }
78
+ //# sourceMappingURL=validators.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validators.js","sourceRoot":"","sources":["../src/validators.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,gEAAgE;AAChE,4DAA4D;AAC5D,EAAE;AACF,gEAAgE;AAChE,+DAA+D;AAC/D,iEAAiE;AACjE,yDAAyD;AAEzD;;iDAEiD;AACjD,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;IACvC,IAAI,MAAM,CAAC,MAAM,GAAG,EAAE,IAAI,MAAM,CAAC,MAAM,GAAG,EAAE;QAAE,OAAO,KAAK,CAAA;IAC1D,IAAI,GAAG,GAAG,CAAC,CAAA;IACX,IAAI,GAAG,GAAG,KAAK,CAAA;IACf,KAAK,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC5C,IAAI,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,EAAE,CAAA;QACjC,IAAI,GAAG,EAAE,CAAC;YACR,CAAC,IAAI,CAAC,CAAA;YACN,IAAI,CAAC,GAAG,CAAC;gBAAE,CAAC,IAAI,CAAC,CAAA;QACnB,CAAC;QACD,GAAG,IAAI,CAAC,CAAA;QACR,GAAG,GAAG,CAAC,GAAG,CAAA;IACZ,CAAC;IACD,OAAO,GAAG,GAAG,EAAE,KAAK,CAAC,CAAA;AACvB,CAAC;AAED;;;;;;;;;;iDAUiD;AACjD,MAAM,UAAU,yBAAyB,CAAC,KAAa;IACrD,4DAA4D;IAC5D,IAAI,CAAC,GAAG,UAAU,CAAA;IAClB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,CAAC,IAAI,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;QACxB,4DAA4D;QAC5D,4DAA4D;QAC5D,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAA;IACpC,CAAC;IACD,OAAO,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;AACxC,CAAC;AAED;;4DAE4D;AAC5D,MAAM,UAAU,OAAO,CAAC,CAAS;IAC/B,IAAI,CAAC,CAAC;QAAE,OAAO,CAAC,CAAA;IAChB,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAA;IACxC,KAAK,MAAM,CAAC,IAAI,CAAC;QAAE,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IAC1D,MAAM,GAAG,GAAG,CAAC,CAAC,MAAM,CAAA;IACpB,IAAI,CAAC,GAAG,CAAC,CAAA;IACT,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;QAChC,MAAM,CAAC,GAAG,CAAC,GAAG,GAAG,CAAA;QACjB,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACvB,CAAC;IACD,OAAO,CAAC,CAAA;AACV,CAAC;AAED;;mEAEmE;AACnE,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,OAAO,CAAC,KAAa,EAAE,EAAE;QACvB,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,CAAA;QAC9D,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,GAAG,CAAA;IAC9B,CAAC,CAAA;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@spool-lab/redact",
3
+ "version": "0.0.1",
4
+ "description": "Local, pure-TS sensitive-data detection for Spool sessions. Pattern + checksum + entropy pipeline today; pluggable provider boundary for future on-device ML (e.g. OpenAI Privacy Filter via transformers.js).",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.19.17",
22
+ "vitest": "^3.2.4"
23
+ },
24
+ "scripts": {
25
+ "build": "pnpm run clean && tsc",
26
+ "dev": "tsc --watch",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "typecheck": "tsc --noEmit",
30
+ "clean": "rm -rf dist"
31
+ }
32
+ }