@mandujs/core 0.41.2 → 0.43.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/package.json +21 -4
- package/src/auth/__tests__/login.test.ts +420 -419
- package/src/auth/__tests__/reset.test.ts +296 -296
- package/src/brain/adapters/anthropic-oauth.ts +421 -420
- package/src/brain/adapters/index.ts +2 -1
- package/src/brain/adapters/ollama.ts +1 -1
- package/src/brain/adapters/openai-oauth.ts +534 -533
- package/src/brain/brain.ts +2 -1
- package/src/brain/redactor.ts +196 -196
- package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -149
- package/src/bundler/__tests__/cold-start.test.ts +504 -504
- package/src/bundler/__tests__/fast-refresh.test.ts +607 -606
- package/src/bundler/__tests__/hdr.test.ts +1 -1
- package/src/bundler/analyzer.ts +958 -958
- package/src/bundler/build.ts +104 -14
- package/src/bundler/dev.ts +125 -0
- package/src/bundler/hmr-types.ts +1 -0
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -0
- package/src/bundler/plugins/index.ts +14 -0
- package/src/bundler/plugins/react-compiler-lint.ts +253 -0
- package/src/bundler/plugins/react-compiler.ts +162 -0
- package/src/bundler/types.ts +12 -0
- package/src/change/integrity.ts +2 -1
- package/src/client/index.ts +10 -0
- package/src/client/island.ts +38 -11
- package/src/client/router.ts +6 -1
- package/src/config/mandu.ts +57 -0
- package/src/config/validate.ts +42 -0
- package/src/content/collection.ts +844 -809
- package/src/content/content-layer.ts +316 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/digest.ts +133 -133
- package/src/content/generate-types.ts +168 -168
- package/src/content/index.ts +6 -1
- package/src/content/llms-txt.ts +277 -277
- package/src/contract/define.ts +474 -474
- package/src/contract/route-helpers.ts +2 -1
- package/src/contract/zod-utils.ts +158 -155
- package/src/db/index.ts +513 -513
- package/src/desktop/__tests__/smoke.test.ts +100 -100
- package/src/desktop/webview-fallback.ts +583 -583
- package/src/desktop/window.ts +3 -1
- package/src/dev-error-overlay/overlay-client.ts +300 -300
- package/src/devtools/ai/mcp-connector.ts +499 -498
- package/src/devtools/client/components/kitchen-root.tsx +7 -2
- package/src/email/resend.ts +163 -163
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -0
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/graph.ts +898 -898
- package/src/guard/index.ts +16 -0
- package/src/guard/statistics.ts +578 -578
- package/src/guard/tsgolint-bridge.ts +512 -0
- package/src/i18n/locale-resolver.ts +214 -214
- package/src/id/__tests__/id.test.ts +120 -120
- package/src/intent/index.ts +321 -321
- package/src/island/index.ts +39 -23
- package/src/kitchen/api/contract-api.ts +15 -8
- package/src/kitchen/kitchen-ui.ts +2137 -2137
- package/src/lockfile/index.ts +3 -2
- package/src/middleware/oauth/__tests__/oauth.test.ts +575 -574
- package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -642
- package/src/middleware/secure/index.ts +417 -417
- package/src/observability/event-bus.ts +2 -2
- package/src/observability/metrics.ts +334 -334
- package/src/observability/tracing.ts +694 -694
- package/src/openapi/generator.ts +1 -1
- package/src/perf/user-marks.ts +553 -553
- package/src/plugins/registry.ts +387 -387
- package/src/resource/ddl/diff.ts +392 -392
- package/src/resource/ddl/snapshot.ts +448 -447
- package/src/resource/generator-schema.ts +477 -476
- package/src/resource/parser.ts +4 -2
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.ts +422 -422
- package/src/runtime/fast-refresh-types.ts +126 -128
- package/src/runtime/image-handler.ts +206 -195
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +36 -19
- package/src/runtime/session-key.ts +328 -328
- package/src/scheduler/__tests__/scheduler.test.ts +514 -514
- package/src/seo/resolve/index.ts +353 -353
- package/src/spec/load.ts +1 -1
- package/src/testing/reporter.ts +676 -676
- package/src/testing/server.ts +196 -196
- package/src/testing/snapshot.ts +444 -444
- package/src/utils/__tests__/lru-cache.test.ts +186 -186
- package/src/utils/bun.ts +8 -8
package/src/brain/brain.ts
CHANGED
|
@@ -21,7 +21,8 @@ import type {
|
|
|
21
21
|
import { DEFAULT_BRAIN_POLICY } from "./types";
|
|
22
22
|
import { type LLMAdapter, NoopAdapter } from "./adapters/base";
|
|
23
23
|
import { createOllamaAdapter } from "./adapters/ollama";
|
|
24
|
-
import { SessionMemory
|
|
24
|
+
import type { SessionMemory} from "./memory";
|
|
25
|
+
import { getSessionMemory } from "./memory";
|
|
25
26
|
import {
|
|
26
27
|
detectEnvironment,
|
|
27
28
|
shouldEnableBrain,
|
package/src/brain/redactor.ts
CHANGED
|
@@ -1,196 +1,196 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Brain — pre-transmission secret redactor (Issue #235)
|
|
3
|
-
*
|
|
4
|
-
* Every payload destined for a cloud adapter is passed through
|
|
5
|
-
* `redactSecrets()` FIRST. The function returns:
|
|
6
|
-
*
|
|
7
|
-
* - `{ redacted, hits }` — redacted payload + list of what matched, so
|
|
8
|
-
* the CLI / adapter can print an audit line and append to
|
|
9
|
-
* `.mandu/brain-redactions.jsonl` for user inspection.
|
|
10
|
-
*
|
|
11
|
-
* Design principles (user feedback — Mandu as connector, not owner):
|
|
12
|
-
* - Mandu NEVER transmits a prompt without passing it through here.
|
|
13
|
-
* - Patterns are conservative (false positives preferred over leaks).
|
|
14
|
-
* - No regex is anchored; we scan the full text so embedded secrets
|
|
15
|
-
* inside a diff block are still caught.
|
|
16
|
-
* - All replacements collapse to a single token `[[REDACTED:<kind>]]`
|
|
17
|
-
* so the cloud model sees a stable shape rather than a mangled
|
|
18
|
-
* partial key that could still be reconstructed.
|
|
19
|
-
*
|
|
20
|
-
* @see docs/brain/oauth-adapters.md (if that ever ships — for now this
|
|
21
|
-
* file + its tests are the source of truth).
|
|
22
|
-
*/
|
|
23
|
-
export type RedactionKind =
|
|
24
|
-
| "openai-key"
|
|
25
|
-
| "stripe-key"
|
|
26
|
-
| "github-token"
|
|
27
|
-
| "slack-token"
|
|
28
|
-
| "aws-key"
|
|
29
|
-
| "bearer-token"
|
|
30
|
-
| "env-ref"
|
|
31
|
-
| "api-key-assignment"
|
|
32
|
-
| "jwt"
|
|
33
|
-
| "long-base64";
|
|
34
|
-
|
|
35
|
-
export interface RedactionHit {
|
|
36
|
-
/** The kind of secret matched (for audit logging). */
|
|
37
|
-
kind: RedactionKind;
|
|
38
|
-
/** Byte offset into the original input where the match started. */
|
|
39
|
-
start: number;
|
|
40
|
-
/** Byte offset into the original input where the match ended. */
|
|
41
|
-
end: number;
|
|
42
|
-
/**
|
|
43
|
-
* Short sample of the redacted material — always prefix + ellipsis,
|
|
44
|
-
* never the full secret. Safe to log.
|
|
45
|
-
*/
|
|
46
|
-
sample: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface RedactionResult {
|
|
50
|
-
/** Input with every match replaced by `[[REDACTED:<kind>]]`. */
|
|
51
|
-
redacted: string;
|
|
52
|
-
/** One entry per match, in scan order. */
|
|
53
|
-
hits: RedactionHit[];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Ordered pattern list. Earlier patterns win — specific formats (e.g.
|
|
58
|
-
* `sk-...`) are checked before the generic "long base64" fallback so
|
|
59
|
-
* their `kind` tag is accurate.
|
|
60
|
-
*
|
|
61
|
-
* Every pattern is intentionally over-conservative — we would rather
|
|
62
|
-
* redact a false-positive hash than leak a real key. Users can inspect
|
|
63
|
-
* `.mandu/brain-redactions.jsonl` to spot over-redaction.
|
|
64
|
-
*/
|
|
65
|
-
const PATTERNS: Array<{ kind: RedactionKind; regex: RegExp }> = [
|
|
66
|
-
// OpenAI-style keys: sk-XXXX, sk-proj-XXXX, pk-XXXX.
|
|
67
|
-
{ kind: "openai-key", regex: /\b(?:sk|pk)-(?:proj-)?[A-Za-z0-9_\-]{16,}\b/g },
|
|
68
|
-
// Stripe live/test keys.
|
|
69
|
-
{ kind: "stripe-key", regex: /\brk_(?:live|test)_[A-Za-z0-9]{16,}\b/g },
|
|
70
|
-
// GitHub PATs — ghp_, gho_, ghu_, ghs_, ghr_.
|
|
71
|
-
{ kind: "github-token", regex: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g },
|
|
72
|
-
// Slack bot / user tokens.
|
|
73
|
-
{ kind: "slack-token", regex: /\bxox[abpors]-[A-Za-z0-9-]{10,}\b/g },
|
|
74
|
-
// AWS access key id (fixed-width 20 char uppercase starting w/ AKIA / ASIA).
|
|
75
|
-
{ kind: "aws-key", regex: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
|
|
76
|
-
// Authorization: Bearer ...
|
|
77
|
-
{
|
|
78
|
-
kind: "bearer-token",
|
|
79
|
-
regex: /(?:Authorization:\s*)?\bBearer\s+[A-Za-z0-9._\-]{16,}/gi,
|
|
80
|
-
},
|
|
81
|
-
// `.env` path references — we redact the path itself, not the
|
|
82
|
-
// surrounding sentence. "see .env" → "see [[REDACTED:env-ref]]".
|
|
83
|
-
// Trailing lookahead matches sentence punctuation / whitespace /
|
|
84
|
-
// end-of-string so "see .env." / ".env.production." both redact.
|
|
85
|
-
{ kind: "env-ref", regex: /\.env(?:\.[a-z]+)?(?=[\s,.;:!?'")`]|$)/g },
|
|
86
|
-
// KEY="value" / KEY=value assignments, where KEY contains
|
|
87
|
-
// API_KEY/SECRET/TOKEN/PASSWORD. Matches the whole assignment.
|
|
88
|
-
{
|
|
89
|
-
kind: "api-key-assignment",
|
|
90
|
-
regex:
|
|
91
|
-
/\b[A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|PASSWD)[A-Z0-9_]*\s*=\s*["']?[^\s"'\n]+["']?/g,
|
|
92
|
-
},
|
|
93
|
-
// JWTs (three base64url segments separated by dots).
|
|
94
|
-
{
|
|
95
|
-
kind: "jwt",
|
|
96
|
-
regex: /\beyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\b/g,
|
|
97
|
-
},
|
|
98
|
-
// Generic long base64/hex blob (20+ chars, mixed case or digit-heavy).
|
|
99
|
-
// Runs last so more specific kinds claim first.
|
|
100
|
-
{
|
|
101
|
-
kind: "long-base64",
|
|
102
|
-
regex: /\b[A-Za-z0-9+/=_\-]{24,}\b/g,
|
|
103
|
-
},
|
|
104
|
-
];
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Sample a match for audit logging — never return the full secret.
|
|
108
|
-
*
|
|
109
|
-
* Returns `"<first 4 chars>...<last 2 chars>"` for entries longer
|
|
110
|
-
* than 8 characters, and a hard-coded stub otherwise. The sample is
|
|
111
|
-
* deliberately short so it cannot be used to reconstruct the key.
|
|
112
|
-
*/
|
|
113
|
-
function sampleOf(match: string): string {
|
|
114
|
-
if (match.length <= 8) return "***";
|
|
115
|
-
return `${match.slice(0, 4)}...${match.slice(-2)}`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Scan `input` for secrets and return a redacted copy + hit list.
|
|
120
|
-
*
|
|
121
|
-
* The scan is non-overlapping: once a region is claimed by an earlier
|
|
122
|
-
* pattern, later patterns cannot match inside it. This is enforced by
|
|
123
|
-
* walking the hit set and rebuilding the string in one pass rather than
|
|
124
|
-
* running `.replace()` per-pattern (which would allow the generic
|
|
125
|
-
* "long-base64" to fire on the already-inserted `[[REDACTED:...]]`
|
|
126
|
-
* marker).
|
|
127
|
-
*
|
|
128
|
-
* @param input Raw text about to be transmitted to a cloud adapter.
|
|
129
|
-
* @returns Redacted text plus audit-safe hit list.
|
|
130
|
-
*/
|
|
131
|
-
export function redactSecrets(input: string): RedactionResult {
|
|
132
|
-
if (!input || input.length === 0) {
|
|
133
|
-
return { redacted: input ?? "", hits: [] };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// 1. Collect every candidate hit across every pattern.
|
|
137
|
-
const raw: RedactionHit[] = [];
|
|
138
|
-
for (const { kind, regex } of PATTERNS) {
|
|
139
|
-
regex.lastIndex = 0;
|
|
140
|
-
let m: RegExpExecArray | null;
|
|
141
|
-
while ((m = regex.exec(input)) !== null) {
|
|
142
|
-
if (m[0].length === 0) {
|
|
143
|
-
regex.lastIndex += 1;
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
raw.push({
|
|
147
|
-
kind,
|
|
148
|
-
start: m.index,
|
|
149
|
-
end: m.index + m[0].length,
|
|
150
|
-
sample: sampleOf(m[0]),
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (raw.length === 0) {
|
|
156
|
-
return { redacted: input, hits: [] };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// 2. Sort by start offset; on tie prefer the earlier pattern (which
|
|
160
|
-
// means the more specific kind since PATTERNS is ordered).
|
|
161
|
-
raw.sort((a, b) => (a.start - b.start) || (a.end - b.end));
|
|
162
|
-
|
|
163
|
-
// 3. Greedy non-overlap: keep the first match; skip anything that
|
|
164
|
-
// starts before the last accepted match ended.
|
|
165
|
-
const accepted: RedactionHit[] = [];
|
|
166
|
-
let cursor = -1;
|
|
167
|
-
for (const h of raw) {
|
|
168
|
-
if (h.start < cursor) continue;
|
|
169
|
-
accepted.push(h);
|
|
170
|
-
cursor = h.end;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 4. Rebuild the output in one pass.
|
|
174
|
-
let out = "";
|
|
175
|
-
let i = 0;
|
|
176
|
-
for (const h of accepted) {
|
|
177
|
-
out += input.slice(i, h.start);
|
|
178
|
-
out += `[[REDACTED:${h.kind}]]`;
|
|
179
|
-
i = h.end;
|
|
180
|
-
}
|
|
181
|
-
out += input.slice(i);
|
|
182
|
-
|
|
183
|
-
return { redacted: out, hits: accepted };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Convenience — returns only the redacted string.
|
|
188
|
-
*
|
|
189
|
-
* Use `redactSecrets()` directly when you also need the hit list (for
|
|
190
|
-
* audit logging). Use this when you just want "scrub and forward".
|
|
191
|
-
*/
|
|
192
|
-
export function redact(input: string): string {
|
|
193
|
-
return redactSecrets(input).hits.length === 0
|
|
194
|
-
? input
|
|
195
|
-
: redactSecrets(input).redacted;
|
|
196
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Brain — pre-transmission secret redactor (Issue #235)
|
|
3
|
+
*
|
|
4
|
+
* Every payload destined for a cloud adapter is passed through
|
|
5
|
+
* `redactSecrets()` FIRST. The function returns:
|
|
6
|
+
*
|
|
7
|
+
* - `{ redacted, hits }` — redacted payload + list of what matched, so
|
|
8
|
+
* the CLI / adapter can print an audit line and append to
|
|
9
|
+
* `.mandu/brain-redactions.jsonl` for user inspection.
|
|
10
|
+
*
|
|
11
|
+
* Design principles (user feedback — Mandu as connector, not owner):
|
|
12
|
+
* - Mandu NEVER transmits a prompt without passing it through here.
|
|
13
|
+
* - Patterns are conservative (false positives preferred over leaks).
|
|
14
|
+
* - No regex is anchored; we scan the full text so embedded secrets
|
|
15
|
+
* inside a diff block are still caught.
|
|
16
|
+
* - All replacements collapse to a single token `[[REDACTED:<kind>]]`
|
|
17
|
+
* so the cloud model sees a stable shape rather than a mangled
|
|
18
|
+
* partial key that could still be reconstructed.
|
|
19
|
+
*
|
|
20
|
+
* @see docs/brain/oauth-adapters.md (if that ever ships — for now this
|
|
21
|
+
* file + its tests are the source of truth).
|
|
22
|
+
*/
|
|
23
|
+
export type RedactionKind =
|
|
24
|
+
| "openai-key"
|
|
25
|
+
| "stripe-key"
|
|
26
|
+
| "github-token"
|
|
27
|
+
| "slack-token"
|
|
28
|
+
| "aws-key"
|
|
29
|
+
| "bearer-token"
|
|
30
|
+
| "env-ref"
|
|
31
|
+
| "api-key-assignment"
|
|
32
|
+
| "jwt"
|
|
33
|
+
| "long-base64";
|
|
34
|
+
|
|
35
|
+
export interface RedactionHit {
|
|
36
|
+
/** The kind of secret matched (for audit logging). */
|
|
37
|
+
kind: RedactionKind;
|
|
38
|
+
/** Byte offset into the original input where the match started. */
|
|
39
|
+
start: number;
|
|
40
|
+
/** Byte offset into the original input where the match ended. */
|
|
41
|
+
end: number;
|
|
42
|
+
/**
|
|
43
|
+
* Short sample of the redacted material — always prefix + ellipsis,
|
|
44
|
+
* never the full secret. Safe to log.
|
|
45
|
+
*/
|
|
46
|
+
sample: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RedactionResult {
|
|
50
|
+
/** Input with every match replaced by `[[REDACTED:<kind>]]`. */
|
|
51
|
+
redacted: string;
|
|
52
|
+
/** One entry per match, in scan order. */
|
|
53
|
+
hits: RedactionHit[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Ordered pattern list. Earlier patterns win — specific formats (e.g.
|
|
58
|
+
* `sk-...`) are checked before the generic "long base64" fallback so
|
|
59
|
+
* their `kind` tag is accurate.
|
|
60
|
+
*
|
|
61
|
+
* Every pattern is intentionally over-conservative — we would rather
|
|
62
|
+
* redact a false-positive hash than leak a real key. Users can inspect
|
|
63
|
+
* `.mandu/brain-redactions.jsonl` to spot over-redaction.
|
|
64
|
+
*/
|
|
65
|
+
const PATTERNS: Array<{ kind: RedactionKind; regex: RegExp }> = [
|
|
66
|
+
// OpenAI-style keys: sk-XXXX, sk-proj-XXXX, pk-XXXX.
|
|
67
|
+
{ kind: "openai-key", regex: /\b(?:sk|pk)-(?:proj-)?[A-Za-z0-9_\-]{16,}\b/g },
|
|
68
|
+
// Stripe live/test keys.
|
|
69
|
+
{ kind: "stripe-key", regex: /\brk_(?:live|test)_[A-Za-z0-9]{16,}\b/g },
|
|
70
|
+
// GitHub PATs — ghp_, gho_, ghu_, ghs_, ghr_.
|
|
71
|
+
{ kind: "github-token", regex: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g },
|
|
72
|
+
// Slack bot / user tokens.
|
|
73
|
+
{ kind: "slack-token", regex: /\bxox[abpors]-[A-Za-z0-9-]{10,}\b/g },
|
|
74
|
+
// AWS access key id (fixed-width 20 char uppercase starting w/ AKIA / ASIA).
|
|
75
|
+
{ kind: "aws-key", regex: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g },
|
|
76
|
+
// Authorization: Bearer ...
|
|
77
|
+
{
|
|
78
|
+
kind: "bearer-token",
|
|
79
|
+
regex: /(?:Authorization:\s*)?\bBearer\s+[A-Za-z0-9._\-]{16,}/gi,
|
|
80
|
+
},
|
|
81
|
+
// `.env` path references — we redact the path itself, not the
|
|
82
|
+
// surrounding sentence. "see .env" → "see [[REDACTED:env-ref]]".
|
|
83
|
+
// Trailing lookahead matches sentence punctuation / whitespace /
|
|
84
|
+
// end-of-string so "see .env." / ".env.production." both redact.
|
|
85
|
+
{ kind: "env-ref", regex: /\.env(?:\.[a-z]+)?(?=[\s,.;:!?'")`]|$)/g },
|
|
86
|
+
// KEY="value" / KEY=value assignments, where KEY contains
|
|
87
|
+
// API_KEY/SECRET/TOKEN/PASSWORD. Matches the whole assignment.
|
|
88
|
+
{
|
|
89
|
+
kind: "api-key-assignment",
|
|
90
|
+
regex:
|
|
91
|
+
/\b[A-Z][A-Z0-9_]*(?:API_KEY|SECRET|TOKEN|PASSWORD|PASSWD)[A-Z0-9_]*\s*=\s*["']?[^\s"'\n]+["']?/g,
|
|
92
|
+
},
|
|
93
|
+
// JWTs (three base64url segments separated by dots).
|
|
94
|
+
{
|
|
95
|
+
kind: "jwt",
|
|
96
|
+
regex: /\beyJ[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\b/g,
|
|
97
|
+
},
|
|
98
|
+
// Generic long base64/hex blob (20+ chars, mixed case or digit-heavy).
|
|
99
|
+
// Runs last so more specific kinds claim first.
|
|
100
|
+
{
|
|
101
|
+
kind: "long-base64",
|
|
102
|
+
regex: /\b[A-Za-z0-9+/=_\-]{24,}\b/g,
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Sample a match for audit logging — never return the full secret.
|
|
108
|
+
*
|
|
109
|
+
* Returns `"<first 4 chars>...<last 2 chars>"` for entries longer
|
|
110
|
+
* than 8 characters, and a hard-coded stub otherwise. The sample is
|
|
111
|
+
* deliberately short so it cannot be used to reconstruct the key.
|
|
112
|
+
*/
|
|
113
|
+
function sampleOf(match: string): string {
|
|
114
|
+
if (match.length <= 8) return "***";
|
|
115
|
+
return `${match.slice(0, 4)}...${match.slice(-2)}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Scan `input` for secrets and return a redacted copy + hit list.
|
|
120
|
+
*
|
|
121
|
+
* The scan is non-overlapping: once a region is claimed by an earlier
|
|
122
|
+
* pattern, later patterns cannot match inside it. This is enforced by
|
|
123
|
+
* walking the hit set and rebuilding the string in one pass rather than
|
|
124
|
+
* running `.replace()` per-pattern (which would allow the generic
|
|
125
|
+
* "long-base64" to fire on the already-inserted `[[REDACTED:...]]`
|
|
126
|
+
* marker).
|
|
127
|
+
*
|
|
128
|
+
* @param input Raw text about to be transmitted to a cloud adapter.
|
|
129
|
+
* @returns Redacted text plus audit-safe hit list.
|
|
130
|
+
*/
|
|
131
|
+
export function redactSecrets(input: string): RedactionResult {
|
|
132
|
+
if (!input || input.length === 0) {
|
|
133
|
+
return { redacted: input ?? "", hits: [] };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 1. Collect every candidate hit across every pattern.
|
|
137
|
+
const raw: RedactionHit[] = [];
|
|
138
|
+
for (const { kind, regex } of PATTERNS) {
|
|
139
|
+
regex.lastIndex = 0;
|
|
140
|
+
let m: RegExpExecArray | null;
|
|
141
|
+
while ((m = regex.exec(input)) !== null) {
|
|
142
|
+
if (m[0].length === 0) {
|
|
143
|
+
regex.lastIndex += 1;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
raw.push({
|
|
147
|
+
kind,
|
|
148
|
+
start: m.index,
|
|
149
|
+
end: m.index + m[0].length,
|
|
150
|
+
sample: sampleOf(m[0]),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (raw.length === 0) {
|
|
156
|
+
return { redacted: input, hits: [] };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 2. Sort by start offset; on tie prefer the earlier pattern (which
|
|
160
|
+
// means the more specific kind since PATTERNS is ordered).
|
|
161
|
+
raw.sort((a, b) => (a.start - b.start) || (a.end - b.end));
|
|
162
|
+
|
|
163
|
+
// 3. Greedy non-overlap: keep the first match; skip anything that
|
|
164
|
+
// starts before the last accepted match ended.
|
|
165
|
+
const accepted: RedactionHit[] = [];
|
|
166
|
+
let cursor = -1;
|
|
167
|
+
for (const h of raw) {
|
|
168
|
+
if (h.start < cursor) continue;
|
|
169
|
+
accepted.push(h);
|
|
170
|
+
cursor = h.end;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 4. Rebuild the output in one pass.
|
|
174
|
+
let out = "";
|
|
175
|
+
let i = 0;
|
|
176
|
+
for (const h of accepted) {
|
|
177
|
+
out += input.slice(i, h.start);
|
|
178
|
+
out += `[[REDACTED:${h.kind}]]`;
|
|
179
|
+
i = h.end;
|
|
180
|
+
}
|
|
181
|
+
out += input.slice(i);
|
|
182
|
+
|
|
183
|
+
return { redacted: out, hits: accepted };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Convenience — returns only the redacted string.
|
|
188
|
+
*
|
|
189
|
+
* Use `redactSecrets()` directly when you also need the hit list (for
|
|
190
|
+
* audit logging). Use this when you just want "scrub and forward".
|
|
191
|
+
*/
|
|
192
|
+
export function redact(input: string): string {
|
|
193
|
+
return redactSecrets(input).hits.length === 0
|
|
194
|
+
? input
|
|
195
|
+
: redactSecrets(input).redacted;
|
|
196
|
+
}
|