@jini-ai/core 0.3.1 → 0.4.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/CHANGELOG.md +136 -0
- package/LICENSE +201 -0
- package/README.md +154 -14
- package/dist/agent-tools.d.ts +29 -0
- package/dist/agent-tools.js +2 -0
- package/dist/api-token-auth.d.ts +12 -5
- package/dist/api-token-auth.js +7 -7
- package/dist/bindings.d.ts +16 -5
- package/dist/bindings.js +7 -6
- package/dist/{internal.d.ts → composition.d.ts} +9 -9
- package/dist/{internal.js → composition.js} +9 -9
- package/dist/contribution-registry.d.ts +16 -0
- package/dist/contribution-registry.js +12 -0
- package/dist/daemon.d.ts +12 -7
- package/dist/daemon.js +21 -9
- package/dist/gated-mutations/actor-identity.d.ts +20 -0
- package/dist/gated-mutations/actor-identity.js +31 -0
- package/dist/gated-mutations/gateway.d.ts +115 -0
- package/dist/gated-mutations/gateway.js +123 -0
- package/dist/gated-mutations/ports.d.ts +27 -0
- package/dist/gated-mutations/ports.js +2 -0
- package/dist/gated-mutations/token.d.ts +95 -0
- package/dist/gated-mutations/token.js +87 -0
- package/dist/gated-mutations.d.ts +5 -0
- package/dist/gated-mutations.js +5 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +5 -0
- package/dist/model-facing-tool-errors.d.ts +37 -0
- package/dist/model-facing-tool-errors.js +71 -0
- package/dist/naming.d.ts +31 -0
- package/dist/naming.js +33 -0
- package/dist/origin-validation.d.ts +64 -19
- package/dist/origin-validation.js +43 -33
- package/dist/pack-lifecycle.d.ts +19 -12
- package/dist/pack-lifecycle.js +14 -14
- package/dist/pack.d.ts +41 -13
- package/dist/pack.js +2 -2
- package/dist/primitives/http.d.ts +57 -0
- package/dist/primitives/http.js +2 -0
- package/dist/primitives/ids.d.ts +8 -0
- package/dist/primitives/ids.js +5 -0
- package/dist/primitives/index.d.ts +8 -0
- package/dist/primitives/index.js +8 -0
- package/dist/primitives/json.d.ts +10 -0
- package/dist/primitives/json.js +2 -0
- package/dist/primitives/logger.d.ts +26 -0
- package/dist/primitives/logger.js +17 -0
- package/dist/primitives/paths.d.ts +16 -0
- package/dist/primitives/paths.js +38 -0
- package/dist/primitives/result.d.ts +9 -0
- package/dist/primitives/result.js +2 -0
- package/dist/primitives/time.d.ts +17 -0
- package/dist/primitives/time.js +13 -0
- package/dist/redact.d.ts +43 -8
- package/dist/redact.js +121 -50
- package/dist/registration-kit.d.ts +357 -0
- package/dist/registration-kit.js +439 -0
- package/dist/text/index.d.ts +3 -0
- package/dist/text/index.js +2 -0
- package/dist/text/sanitize-untrusted-text.d.ts +22 -0
- package/dist/text/sanitize-untrusted-text.js +108 -0
- package/dist/token.d.ts +29 -2
- package/dist/token.js +22 -2
- package/dist/tool-registry.d.ts +32 -16
- package/dist/tool-registry.js +11 -11
- package/dist/tool-tokens.js +1 -1
- package/package.json +56 -11
- package/dist/api-token-auth.d.ts.map +0 -1
- package/dist/api-token-auth.js.map +0 -1
- package/dist/bindings.d.ts.map +0 -1
- package/dist/bindings.js.map +0 -1
- package/dist/compose.typecheck.d.ts +0 -2
- package/dist/compose.typecheck.d.ts.map +0 -1
- package/dist/compose.typecheck.js +0 -29
- package/dist/compose.typecheck.js.map +0 -1
- package/dist/daemon.d.ts.map +0 -1
- package/dist/daemon.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal.d.ts.map +0 -1
- package/dist/internal.js.map +0 -1
- package/dist/origin-validation.d.ts.map +0 -1
- package/dist/origin-validation.js.map +0 -1
- package/dist/pack-lifecycle.d.ts.map +0 -1
- package/dist/pack-lifecycle.js.map +0 -1
- package/dist/pack.d.ts.map +0 -1
- package/dist/pack.js.map +0 -1
- package/dist/principal.d.ts.map +0 -1
- package/dist/principal.js.map +0 -1
- package/dist/redact.d.ts.map +0 -1
- package/dist/redact.js.map +0 -1
- package/dist/token.d.ts.map +0 -1
- package/dist/token.js.map +0 -1
- package/dist/tool-registry.d.ts.map +0 -1
- package/dist/tool-registry.js.map +0 -1
- package/dist/tool-tokens.d.ts.map +0 -1
- package/dist/tool-tokens.js.map +0 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Pure lexical normalization of an already absolute path; no cwd, filesystem or symlink access. */
|
|
2
|
+
function normalizeAbsolutePath(value, separator) {
|
|
3
|
+
const path = separator === '\\' ? value.replace(/\\/g, '/') : value;
|
|
4
|
+
const drive = separator === '\\' ? /^[A-Za-z]:\//.exec(path)?.[0] : undefined;
|
|
5
|
+
const unc = separator === '\\' ? /^\/\/[^/]+\/[^/]+(?:\/|$)/.exec(path)?.[0] : undefined;
|
|
6
|
+
const prefix = drive ?? unc ?? (path.startsWith('/') ? '/' : undefined);
|
|
7
|
+
if (!prefix)
|
|
8
|
+
throw new RangeError('pathContains requires resolved absolute paths');
|
|
9
|
+
const segments = [];
|
|
10
|
+
for (const segment of path.slice(prefix.length).split('/')) {
|
|
11
|
+
if (!segment || segment === '.')
|
|
12
|
+
continue;
|
|
13
|
+
if (segment === '..')
|
|
14
|
+
segments.pop();
|
|
15
|
+
else
|
|
16
|
+
segments.push(segment);
|
|
17
|
+
}
|
|
18
|
+
return prefix.replace(/\/+$/, '') + '/' + segments.join('/');
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Check lexical containment using whole directory segments, including equality.
|
|
22
|
+
* Hosts resolve cwd-relative paths and symlinks before calling: checking a textual
|
|
23
|
+
* prefix alone can permit traversal or a symlink escape. Keeping that effect outside
|
|
24
|
+
* this helper makes the kernel browser-safe. Windows hosts select case-insensitive
|
|
25
|
+
* comparison and backslash separators; drive roots and UNC share roots remain distinct.
|
|
26
|
+
* POSIX backslashes remain literal characters, so distinct directories cannot alias.
|
|
27
|
+
*/
|
|
28
|
+
export function pathContains({ root, target }, { caseSensitive = true, separator = '/' } = {}) {
|
|
29
|
+
const normalize = (value) => {
|
|
30
|
+
const path = normalizeAbsolutePath(value, separator);
|
|
31
|
+
return caseSensitive ? path : path.toLowerCase();
|
|
32
|
+
};
|
|
33
|
+
const normalizedRoot = normalize(root);
|
|
34
|
+
const normalizedTarget = normalize(target);
|
|
35
|
+
const boundary = normalizedRoot.endsWith('/') ? normalizedRoot : normalizedRoot + '/';
|
|
36
|
+
return normalizedTarget === normalizedRoot || normalizedTarget.startsWith(boundary);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Unbranded wire timestamp: hosts can share clocks without conversion adapters. */
|
|
2
|
+
export type ISODateTime = string;
|
|
3
|
+
/** Milliseconds since the Unix epoch. Zero-argument getters need no empty argument object. */
|
|
4
|
+
export interface Clock {
|
|
5
|
+
nowMs(): number;
|
|
6
|
+
}
|
|
7
|
+
/** Default wall clock; inject a Clock to control time in application code. */
|
|
8
|
+
export declare function createSystemClock(): Clock;
|
|
9
|
+
/** Converts epoch milliseconds using the standard UTC ISO representation. Invalid time throws. */
|
|
10
|
+
export declare function toIsoDateTime({ epochMs }: {
|
|
11
|
+
epochMs: number;
|
|
12
|
+
}): ISODateTime;
|
|
13
|
+
/** Samples the supplied clock once and formats its time in UTC. */
|
|
14
|
+
export declare function nowIso({ clock }: {
|
|
15
|
+
clock: Clock;
|
|
16
|
+
}): ISODateTime;
|
|
17
|
+
//# sourceMappingURL=time.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Default wall clock; inject a Clock to control time in application code. */
|
|
2
|
+
export function createSystemClock() {
|
|
3
|
+
return { nowMs: () => Date.now() };
|
|
4
|
+
}
|
|
5
|
+
/** Converts epoch milliseconds using the standard UTC ISO representation. Invalid time throws. */
|
|
6
|
+
export function toIsoDateTime({ epochMs }) {
|
|
7
|
+
return new Date(epochMs).toISOString();
|
|
8
|
+
}
|
|
9
|
+
/** Samples the supplied clock once and formats its time in UTC. */
|
|
10
|
+
export function nowIso({ clock }) {
|
|
11
|
+
return toIsoDateTime({ epochMs: clock.nowMs() });
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=time.js.map
|
package/dist/redact.d.ts
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module redact
|
|
3
3
|
*
|
|
4
|
-
* Lexical secret / PII scrubber for telemetry payloads.
|
|
5
|
-
* OD's
|
|
6
|
-
*
|
|
4
|
+
* Lexical secret / PII scrubber for telemetry payloads. Originally a verbatim port of
|
|
5
|
+
* OD's daemon redactor — no product-identity strings or hardcoded env vars in the original,
|
|
6
|
+
* so no genericization was needed. The shared kernel now also covers the CLI/provider rule
|
|
7
|
+
* union and accepts explicit host secrets; the original categories and rationale stay here.
|
|
7
8
|
*
|
|
8
9
|
* Runs before any prompt or assistant text is sent to an external
|
|
9
|
-
* observability sink. The
|
|
10
|
+
* observability sink. The default policy is intentionally conservative:
|
|
10
11
|
* each one matches a well-defined token shape with extremely low
|
|
11
12
|
* false-positive rate (API keys have a fixed prefix, JWTs have the
|
|
12
13
|
* "header.payload.signature" triple, credit-card matches go through a Luhn
|
|
13
14
|
* check). What this file does NOT do — and any caller-facing copy must
|
|
14
15
|
* reflect that — is detect names, addresses, business secrets, or anything
|
|
15
16
|
* that requires understanding the meaning of the surrounding text. That's
|
|
16
|
-
* an ML / LLM problem this module doesn't take on.
|
|
17
|
+
* an ML / LLM problem this module doesn't take on. Callers can explicitly opt
|
|
18
|
+
* into `policy: 'aggressive'` to also mask bare 20+ character opaque runs;
|
|
19
|
+
* that CLI-style heuristic trades identifier/path readability for broader masking
|
|
20
|
+
* and must never be used by diagnostics or secret-scan guards.
|
|
17
21
|
*
|
|
18
22
|
* Output format: every match is replaced by `[REDACTED:<kind>]` so a
|
|
19
23
|
* reviewer reading a trace can see exactly which category fired without
|
|
@@ -27,21 +31,52 @@
|
|
|
27
31
|
* - AWS access key shape: 'AKIA' + 16 uppercase alphanumerics.
|
|
28
32
|
* - Luhn algorithm (credit cards): https://en.wikipedia.org/wiki/Luhn_algorithm
|
|
29
33
|
*/
|
|
34
|
+
/** Credential-shape vocabulary for secret-only redaction; no I/O or host dependencies. */
|
|
35
|
+
export interface SecretShapePattern {
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly pattern: RegExp;
|
|
38
|
+
}
|
|
39
|
+
export declare const PEM_PATTERN_NAME = "PEM private key block";
|
|
40
|
+
export declare const SECRET_SHAPE_PATTERNS: readonly SecretShapePattern[];
|
|
41
|
+
export interface SecretRedactionPattern {
|
|
42
|
+
name: string;
|
|
43
|
+
regex: RegExp;
|
|
44
|
+
}
|
|
45
|
+
/** Optional caller-owned secrets and named lexical rules, without ambient credential lookup. */
|
|
46
|
+
export interface SecretRedactionOptions {
|
|
47
|
+
/** Conservative by default; aggressive also masks bare 20+ character opaque runs. */
|
|
48
|
+
policy?: 'conservative' | 'aggressive';
|
|
49
|
+
exactSecrets?: ReadonlyArray<string | undefined | null>;
|
|
50
|
+
extraPatterns?: readonly SecretRedactionPattern[];
|
|
51
|
+
}
|
|
52
|
+
export declare const LABELED_SECRET_RE: RegExp;
|
|
53
|
+
export declare const OPAQUE_TOKEN_RE: RegExp;
|
|
54
|
+
export declare const NON_SECRET_TOKEN_RE: RegExp;
|
|
30
55
|
/**
|
|
31
56
|
* Returns `input` with every recognised secret / PII pattern replaced by a
|
|
32
57
|
* `[REDACTED:<kind>]` marker. Idempotent — re-running on already redacted
|
|
33
|
-
* text only matches new tokens.
|
|
58
|
+
* text only matches new tokens. Defaults to conservative; `{ policy: 'aggressive' }`
|
|
59
|
+
* additionally masks bare 20+ character opaque runs. Exact secrets and extra patterns
|
|
60
|
+
* are honored under either policy.
|
|
34
61
|
*
|
|
35
62
|
* Empty / non-string input passes through unchanged so the caller can use
|
|
36
63
|
* this as a no-op on optional fields.
|
|
37
64
|
*/
|
|
38
|
-
export declare function redactSecrets(input
|
|
65
|
+
export declare function redactSecrets({ input }: {
|
|
66
|
+
input: string;
|
|
67
|
+
}, options?: SecretRedactionOptions): string;
|
|
39
68
|
/**
|
|
40
69
|
* Same as {@link redactSecrets} but also returns per-category counts so the
|
|
41
70
|
* caller can attach an audit summary to the trace metadata ('we stripped 2
|
|
42
71
|
* keys + 1 email before send').
|
|
72
|
+
*
|
|
73
|
+
* Exact secrets run first, longest first: lexical rules or a shorter supplied secret must not
|
|
74
|
+
* split a known credential and leave a suffix behind. Opt-in aggressive opaque rules precede
|
|
75
|
+
* PII rules for the same reason (a numeric token can also contain phone/card candidates).
|
|
43
76
|
*/
|
|
44
|
-
export declare function redactSecretsWithCounts(input
|
|
77
|
+
export declare function redactSecretsWithCounts({ input }: {
|
|
78
|
+
input: string;
|
|
79
|
+
}, options?: SecretRedactionOptions): {
|
|
45
80
|
redacted: string;
|
|
46
81
|
counts: Record<string, number>;
|
|
47
82
|
};
|
package/dist/redact.js
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module redact
|
|
3
3
|
*
|
|
4
|
-
* Lexical secret / PII scrubber for telemetry payloads.
|
|
5
|
-
* OD's
|
|
6
|
-
*
|
|
4
|
+
* Lexical secret / PII scrubber for telemetry payloads. Originally a verbatim port of
|
|
5
|
+
* OD's daemon redactor — no product-identity strings or hardcoded env vars in the original,
|
|
6
|
+
* so no genericization was needed. The shared kernel now also covers the CLI/provider rule
|
|
7
|
+
* union and accepts explicit host secrets; the original categories and rationale stay here.
|
|
7
8
|
*
|
|
8
9
|
* Runs before any prompt or assistant text is sent to an external
|
|
9
|
-
* observability sink. The
|
|
10
|
+
* observability sink. The default policy is intentionally conservative:
|
|
10
11
|
* each one matches a well-defined token shape with extremely low
|
|
11
12
|
* false-positive rate (API keys have a fixed prefix, JWTs have the
|
|
12
13
|
* "header.payload.signature" triple, credit-card matches go through a Luhn
|
|
13
14
|
* check). What this file does NOT do — and any caller-facing copy must
|
|
14
15
|
* reflect that — is detect names, addresses, business secrets, or anything
|
|
15
16
|
* that requires understanding the meaning of the surrounding text. That's
|
|
16
|
-
* an ML / LLM problem this module doesn't take on.
|
|
17
|
+
* an ML / LLM problem this module doesn't take on. Callers can explicitly opt
|
|
18
|
+
* into `policy: 'aggressive'` to also mask bare 20+ character opaque runs;
|
|
19
|
+
* that CLI-style heuristic trades identifier/path readability for broader masking
|
|
20
|
+
* and must never be used by diagnostics or secret-scan guards.
|
|
17
21
|
*
|
|
18
22
|
* Output format: every match is replaced by `[REDACTED:<kind>]` so a
|
|
19
23
|
* reviewer reading a trace can see exactly which category fired without
|
|
@@ -27,6 +31,27 @@
|
|
|
27
31
|
* - AWS access key shape: 'AKIA' + 16 uppercase alphanumerics.
|
|
28
32
|
* - Luhn algorithm (credit cards): https://en.wikipedia.org/wiki/Luhn_algorithm
|
|
29
33
|
*/
|
|
34
|
+
// Named once and reused (by SECRET_SHAPE_PATTERNS here and by the guard's ALLOWLIST) rather than repeated
|
|
35
|
+
// as a literal in multiple places.
|
|
36
|
+
export const PEM_PATTERN_NAME = "PEM private key block";
|
|
37
|
+
// Share data between static scans and runtime redactors without importing a CLI entry point:
|
|
38
|
+
// top-level argv/path handling in a scanner can crash a daemon that imports it. Keeping the
|
|
39
|
+
// vocabulary dependency-free also avoids making core redaction depend on feature code.
|
|
40
|
+
// Match each vendor's actual alphabet. Fixed-length patterns need lookaround boundaries so a
|
|
41
|
+
// credential-shaped substring of a longer inert value is not mistaken for a complete secret.
|
|
42
|
+
export const SECRET_SHAPE_PATTERNS = [
|
|
43
|
+
{ name: "Google API key (AIza)", pattern: /(?<![A-Za-z0-9_-])AIza[0-9A-Za-z_-]{35}(?![A-Za-z0-9_-])/ },
|
|
44
|
+
{ name: "Anthropic API key (sk-ant-)", pattern: /(?<![A-Za-z0-9_-])sk-ant-[A-Za-z0-9_-]{90,}/ },
|
|
45
|
+
{ name: "generic sk- secret key (OpenAI-shaped)", pattern: /(?<![A-Za-z0-9_-])sk-[A-Za-z0-9]{40,}/ },
|
|
46
|
+
{ name: "GitHub personal access token (classic, ghp_)", pattern: /(?<!\w)ghp_\w{36}(?!\w)/ },
|
|
47
|
+
{ name: "GitHub OAuth token (gho_)", pattern: /(?<!\w)gho_\w{36}(?!\w)/ },
|
|
48
|
+
{ name: "GitHub fine-grained PAT", pattern: /(?<!\w)github_pat_\w{80,}/ },
|
|
49
|
+
{ name: "npm access token", pattern: /(?<!\w)npm_\w{36}(?!\w)/ },
|
|
50
|
+
{ name: "Slack token", pattern: /(?<![A-Za-z0-9-])xox[baprs]-[A-Za-z0-9-]{12,}/ },
|
|
51
|
+
{ name: "AWS access key ID", pattern: /(?<![0-9A-Z])AKIA[0-9A-Z]{16}(?![0-9A-Z])/ },
|
|
52
|
+
{ name: "Fastmail app password (fm2_)", pattern: /(?<![A-Za-z0-9+/=])fm2_[A-Za-z0-9+/=]{20,}/ },
|
|
53
|
+
{ name: PEM_PATTERN_NAME, pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/ },
|
|
54
|
+
];
|
|
30
55
|
// Order matters: list specific rules before more general ones. Langfuse
|
|
31
56
|
// keys (`sk-lf-...`) would otherwise be eaten by the generic `sk-...`
|
|
32
57
|
// rule and labeled as a generic OpenAI-style key.
|
|
@@ -40,7 +65,7 @@ const PATTERNS = [
|
|
|
40
65
|
// covers most "sk-..." secrets a user might paste into a prompt.
|
|
41
66
|
{ name: 'sk_key', regex: /\bsk-(?:proj-|live-|test-|ant-)?[A-Za-z0-9_-]{20,}\b/g },
|
|
42
67
|
// GitHub personal / OAuth / server / user-server / refresh tokens.
|
|
43
|
-
{ name: 'github_token', regex: /\bgh[opsur]_[A-Za-z0-9]{36,
|
|
68
|
+
{ name: 'github_token', regex: /\bgh[opsur]_[A-Za-z0-9]{36,}\b/g },
|
|
44
69
|
// GitHub legacy 40-hex personal access tokens that ship with no prefix
|
|
45
70
|
// are indistinguishable from a sha1 hash, so we don't try to match them
|
|
46
71
|
// here — false positives would be brutal in commit logs / artifact slugs.
|
|
@@ -92,7 +117,7 @@ const API_KEY_HEADER = /(^|[^?&\w-])("?)(x-api-key|api-key|x-goog-api-key)\2(\s*
|
|
|
92
117
|
const API_KEY_QUERY = /([?&](?:key|api_key|api-key)=)[^&#\s,;"']+/gi;
|
|
93
118
|
// The length guard and the per-digit `d < 0 || d > 9` guard from the origin
|
|
94
119
|
// were both dropped here (a documented deviation from the otherwise-verbatim
|
|
95
|
-
// port — see
|
|
120
|
+
// port — see archived provenance ledger): `isLuhnValid` is private with exactly one call
|
|
96
121
|
// site (the `CARD_CANDIDATE.replace` callbacks below), and `CARD_CANDIDATE`'s
|
|
97
122
|
// own regex (`\b(?:\d[ -]?){12,18}\d\b`) already guarantees any string
|
|
98
123
|
// reaching here is 13-19 characters, and the preceding `match.replace(/\D/g,
|
|
@@ -117,74 +142,120 @@ function redactApiKeyHeaderValue(prefix, quote, name, separator, value) {
|
|
|
117
142
|
const redactedValue = value.startsWith('"') ? '"[REDACTED:api_key_header]"' : '[REDACTED:api_key_header]';
|
|
118
143
|
return `${prefix}${quote}${name}${quote}${separator}${redactedValue}`;
|
|
119
144
|
}
|
|
145
|
+
// A labeled credential has its value masked even when too short to have a vendor token shape.
|
|
146
|
+
// Bare long opaque runs are opt-in CLI heuristics: UUIDs, hashes, model IDs and path segments
|
|
147
|
+
// share that alphabet, so the default telemetry policy must leave them readable. Whole
|
|
148
|
+
// correlation IDs and the delegated-tool route stay readable even in aggressive mode;
|
|
149
|
+
// appending secret characters defeats that exception. The host's ERR-XXXX-XXXX-XXXX-XXXX
|
|
150
|
+
// correlation ID uses uppercase hex and is meant to reach the model and be quoted to an
|
|
151
|
+
// operator; delegated-tool-calls is the route segment named in delegated tool failures.
|
|
152
|
+
// Matching the ENTIRE run keeps a secret glued onto either spelling redactable.
|
|
153
|
+
export const LABELED_SECRET_RE = /\b(authorization|bearer|api[-_]?key|access[-_]?token|refresh[-_]?token|client[-_]?secret|password|secret|cookie)\b\s*[:=]\s*\S+/gi;
|
|
154
|
+
export const OPAQUE_TOKEN_RE = /[A-Za-z0-9_-]{20,}/g;
|
|
155
|
+
export const NON_SECRET_TOKEN_RE = /^(?:ERR-[0-9A-F]{4}(?:-[0-9A-F]{4}){3}|delegated-tool-calls)$/;
|
|
156
|
+
// Runtime markers must survive a second pass; the bounded sanitizer deliberately retains
|
|
157
|
+
// its historical lowercase marker contract and uses the unmodified lexical vocabulary.
|
|
158
|
+
// Reject a masked header/query value before matching, so a following field's credential
|
|
159
|
+
// remains available to the global scan rather than being consumed beside the marker.
|
|
160
|
+
const MASK_AWARE_LABELED_SECRET_RE = new RegExp(LABELED_SECRET_RE.source.replace(/\\S\+$/, "(?!Bearer\\s+\\[REDACTED:[^\\]]+\\](?:\\s|$))(?!\\[REDACTED:[^\\]]+\\][;&])(\\S+)"), LABELED_SECRET_RE.flags);
|
|
161
|
+
// Provider failures also echo short bearer values. Exclude our own marker on subsequent passes.
|
|
162
|
+
const SHORT_BEARER_RE = /(?<=Bearer\s+)[A-Za-z0-9_\-.+/=]+/gi;
|
|
163
|
+
const PROVIDER_API_KEY_HEADER = /(x-api-key|api-key|x-goog-api-key)\s*[:=]\s*([^\s,;"']+)/gi;
|
|
164
|
+
const PROVIDER_API_KEY_QUERY = /([?&]key=)([^&\s]+)/gi;
|
|
165
|
+
// URL userinfo and signed query values have explicit credential context; unlike opaque runs,
|
|
166
|
+
// they are safe to recognize in the conservative policy. Keep scheme/host and query names
|
|
167
|
+
// readable for debugging without exposing credentials that authorize access to the resource.
|
|
168
|
+
const URL_CREDENTIALS_RE = /(\b[a-z][a-z0-9+.-]*:\/\/)([^/\s?#@]+)@/gi;
|
|
169
|
+
const SIGNED_QUERY_RE = /([?&](?:signature|sig|x-amz-signature|x-goog-signature|access_token|refresh_token)=)([^&#\s,;"']+)/gi;
|
|
170
|
+
// Only an entire marker is already masked. A secret glued onto marker-like text still needs
|
|
171
|
+
// masking; a substring check would preserve credential suffixes in echoed header/query values.
|
|
172
|
+
function isMaskedValue(value) {
|
|
173
|
+
return /^["']?\[REDACTED:[^\]]+\]["']?$/.test(value);
|
|
174
|
+
}
|
|
120
175
|
/**
|
|
121
176
|
* Returns `input` with every recognised secret / PII pattern replaced by a
|
|
122
177
|
* `[REDACTED:<kind>]` marker. Idempotent — re-running on already redacted
|
|
123
|
-
* text only matches new tokens.
|
|
178
|
+
* text only matches new tokens. Defaults to conservative; `{ policy: 'aggressive' }`
|
|
179
|
+
* additionally masks bare 20+ character opaque runs. Exact secrets and extra patterns
|
|
180
|
+
* are honored under either policy.
|
|
124
181
|
*
|
|
125
182
|
* Empty / non-string input passes through unchanged so the caller can use
|
|
126
183
|
* this as a no-op on optional fields.
|
|
127
184
|
*/
|
|
128
|
-
export function redactSecrets(input) {
|
|
129
|
-
|
|
130
|
-
return input;
|
|
131
|
-
let out = input;
|
|
132
|
-
for (const { name, regex } of PATTERNS) {
|
|
133
|
-
out = out.replace(regex, `[REDACTED:${name}]`);
|
|
134
|
-
}
|
|
135
|
-
out = out
|
|
136
|
-
.replace(API_KEY_HEADER, (_match, prefix, quote, name, separator, value) => redactApiKeyHeaderValue(prefix, quote, name, separator, value))
|
|
137
|
-
.replace(API_KEY_QUERY, '$1[REDACTED:api_key_query]');
|
|
138
|
-
out = out.replace(CARD_CANDIDATE, (match) => {
|
|
139
|
-
const digits = match.replace(/\D/g, '');
|
|
140
|
-
return isLuhnValid(digits) ? '[REDACTED:credit_card]' : match;
|
|
141
|
-
});
|
|
142
|
-
return out;
|
|
185
|
+
export function redactSecrets({ input }, options = {}) {
|
|
186
|
+
return redactSecretsWithCounts({ input }, options).redacted;
|
|
143
187
|
}
|
|
144
188
|
/**
|
|
145
189
|
* Same as {@link redactSecrets} but also returns per-category counts so the
|
|
146
190
|
* caller can attach an audit summary to the trace metadata ('we stripped 2
|
|
147
191
|
* keys + 1 email before send').
|
|
192
|
+
*
|
|
193
|
+
* Exact secrets run first, longest first: lexical rules or a shorter supplied secret must not
|
|
194
|
+
* split a known credential and leave a suffix behind. Opt-in aggressive opaque rules precede
|
|
195
|
+
* PII rules for the same reason (a numeric token can also contain phone/card candidates).
|
|
148
196
|
*/
|
|
149
|
-
export function redactSecretsWithCounts(input) {
|
|
197
|
+
export function redactSecretsWithCounts({ input }, options = {}) {
|
|
150
198
|
const counts = {};
|
|
151
199
|
if (!input)
|
|
152
200
|
return { redacted: input, counts };
|
|
153
201
|
let out = input;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
202
|
+
const marker = (name) => {
|
|
203
|
+
counts[name] = (counts[name] ?? 0) + 1;
|
|
204
|
+
return `[REDACTED:${name}]`;
|
|
205
|
+
};
|
|
206
|
+
const applyPattern = ({ name, regex }) => {
|
|
207
|
+
// Clone host/global regexes so lastIndex state never leaks across requests. Named extra
|
|
208
|
+
// patterns use the same global replacement contract as the built-in vocabulary.
|
|
209
|
+
const flags = regex.flags.includes('g') ? regex.flags : `${regex.flags}g`;
|
|
210
|
+
out = out.replace(new RegExp(regex.source, flags), () => marker(name));
|
|
211
|
+
};
|
|
212
|
+
const secrets = [...new Set((options.exactSecrets ?? []).filter((secret) => typeof secret === 'string' && secret.length > 0))].sort((a, b) => b.length - a.length);
|
|
213
|
+
for (const secret of secrets) {
|
|
214
|
+
const escaped = secret.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
215
|
+
out = out.replace(new RegExp(escaped, 'g'), () => marker('exact_secret'));
|
|
216
|
+
}
|
|
217
|
+
out = out.replace(URL_CREDENTIALS_RE, (match, scheme, userinfo) => isMaskedValue(userinfo) ? match : `${scheme}${marker('url_credentials')}@`);
|
|
218
|
+
out = out.replace(SIGNED_QUERY_RE, (match, prefix, value) => isMaskedValue(value) ? match : `${prefix}${marker('signed_query')}`);
|
|
219
|
+
const piiNames = new Set(['email', 'ipv4', 'phone']);
|
|
220
|
+
for (const pattern of PATTERNS) {
|
|
221
|
+
if (!piiNames.has(pattern.name))
|
|
222
|
+
applyPattern(pattern);
|
|
162
223
|
}
|
|
163
|
-
|
|
224
|
+
for (const shape of SECRET_SHAPE_PATTERNS)
|
|
225
|
+
applyPattern({ name: 'credential', regex: shape.pattern });
|
|
226
|
+
for (const pattern of options.extraPatterns ?? [])
|
|
227
|
+
applyPattern(pattern);
|
|
164
228
|
out = out.replace(API_KEY_HEADER, (_match, prefix, quote, name, separator, value) => {
|
|
165
|
-
|
|
229
|
+
// Already masked values are not new credentials and should not inflate the audit count.
|
|
230
|
+
if (isMaskedValue(value))
|
|
231
|
+
return `${prefix}${quote}${name}${quote}${separator}${value}`;
|
|
232
|
+
marker('api_key_header');
|
|
166
233
|
return redactApiKeyHeaderValue(prefix, quote, name, separator, value);
|
|
167
234
|
});
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
out = out.replace(API_KEY_QUERY, (
|
|
172
|
-
|
|
173
|
-
|
|
235
|
+
// The provider rule includes a query fragment in an echoed key; mask that whole value
|
|
236
|
+
// before the narrower core query rule can split it and leave a credential suffix behind.
|
|
237
|
+
out = out.replace(PROVIDER_API_KEY_QUERY, (match, prefix, value) => isMaskedValue(value) ? match : `${prefix}${marker('api_key_query')}`);
|
|
238
|
+
out = out.replace(API_KEY_QUERY, (match, prefix) => isMaskedValue(match.slice(prefix.length)) ? match : `${prefix}${marker('api_key_query')}`);
|
|
239
|
+
out = out.replace(SHORT_BEARER_RE, () => marker('bearer_token'));
|
|
240
|
+
out = out.replace(PROVIDER_API_KEY_HEADER, (match, name, value) => isMaskedValue(value) ? match : `${name}: ${marker('api_key_header')}`);
|
|
241
|
+
out = out.replace(MASK_AWARE_LABELED_SECRET_RE, (match, _label, value) => {
|
|
242
|
+
// Replace only the captured value, retaining labels even when no whitespace separates
|
|
243
|
+
// them. Glued credential suffixes fail the complete-marker check and remain maskable.
|
|
244
|
+
if (isMaskedValue(value))
|
|
245
|
+
return match;
|
|
246
|
+
return `${match.slice(0, -value.length)}${marker('labeled_secret')}`;
|
|
174
247
|
});
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
248
|
+
if (options.policy === 'aggressive') {
|
|
249
|
+
out = out.replace(OPAQUE_TOKEN_RE, (match) => NON_SECRET_TOKEN_RE.test(match) ? match : marker('opaque_token'));
|
|
250
|
+
}
|
|
251
|
+
for (const pattern of PATTERNS) {
|
|
252
|
+
if (piiNames.has(pattern.name))
|
|
253
|
+
applyPattern(pattern);
|
|
254
|
+
}
|
|
178
255
|
out = out.replace(CARD_CANDIDATE, (match) => {
|
|
179
256
|
const digits = match.replace(/\D/g, '');
|
|
180
|
-
|
|
181
|
-
cardCount += 1;
|
|
182
|
-
return '[REDACTED:credit_card]';
|
|
183
|
-
}
|
|
184
|
-
return match;
|
|
257
|
+
return isLuhnValid(digits) ? marker('credit_card') : match;
|
|
185
258
|
});
|
|
186
|
-
if (cardCount > 0)
|
|
187
|
-
counts.credit_card = cardCount;
|
|
188
259
|
return { redacted: out, counts };
|
|
189
260
|
}
|
|
190
261
|
//# sourceMappingURL=redact.js.map
|