@clossys/writer 0.3.2
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 +196 -0
- package/LICENSE +21 -0
- package/README.md +593 -0
- package/dist/addressability.d.ts +245 -0
- package/dist/addressability.d.ts.map +1 -0
- package/dist/addressability.js +567 -0
- package/dist/addressability.js.map +1 -0
- package/dist/checker.d.ts +159 -0
- package/dist/checker.d.ts.map +1 -0
- package/dist/checker.js +177 -0
- package/dist/checker.js.map +1 -0
- package/dist/cli.d.ts +149 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1049 -0
- package/dist/cli.js.map +1 -0
- package/dist/copy-gate.d.ts +107 -0
- package/dist/copy-gate.d.ts.map +1 -0
- package/dist/copy-gate.js +132 -0
- package/dist/copy-gate.js.map +1 -0
- package/dist/fingerprint.d.ts +61 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +64 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/locale-coverage.d.ts +227 -0
- package/dist/locale-coverage.d.ts.map +1 -0
- package/dist/locale-coverage.js +414 -0
- package/dist/locale-coverage.js.map +1 -0
- package/dist/passage.d.ts +294 -0
- package/dist/passage.d.ts.map +1 -0
- package/dist/passage.js +463 -0
- package/dist/passage.js.map +1 -0
- package/dist/path-exclusions.d.ts +114 -0
- package/dist/path-exclusions.d.ts.map +1 -0
- package/dist/path-exclusions.js +213 -0
- package/dist/path-exclusions.js.map +1 -0
- package/dist/registry.d.ts +61 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +64 -0
- package/dist/registry.js.map +1 -0
- package/dist/resolve.d.ts +33 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +133 -0
- package/dist/resolve.js.map +1 -0
- package/dist/scan.d.ts +457 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +1698 -0
- package/dist/scan.js.map +1 -0
- package/dist/schema.d.ts +81 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +482 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +268 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +40 -0
- package/dist/types.js.map +1 -0
- package/dist/voice/checker.d.ts +186 -0
- package/dist/voice/checker.d.ts.map +1 -0
- package/dist/voice/checker.js +483 -0
- package/dist/voice/checker.js.map +1 -0
- package/dist/voice/derivation-coverage.d.ts +168 -0
- package/dist/voice/derivation-coverage.d.ts.map +1 -0
- package/dist/voice/derivation-coverage.js +181 -0
- package/dist/voice/derivation-coverage.js.map +1 -0
- package/dist/voice/fields.d.ts +107 -0
- package/dist/voice/fields.d.ts.map +1 -0
- package/dist/voice/fields.js +144 -0
- package/dist/voice/fields.js.map +1 -0
- package/dist/voice/index.d.ts +19 -0
- package/dist/voice/index.d.ts.map +1 -0
- package/dist/voice/index.js +24 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/internal/parse-template.d.ts +27 -0
- package/dist/voice/internal/parse-template.d.ts.map +1 -0
- package/dist/voice/internal/parse-template.js +111 -0
- package/dist/voice/internal/parse-template.js.map +1 -0
- package/dist/voice/internal/pattern-safety.d.ts +111 -0
- package/dist/voice/internal/pattern-safety.d.ts.map +1 -0
- package/dist/voice/internal/pattern-safety.js +278 -0
- package/dist/voice/internal/pattern-safety.js.map +1 -0
- package/dist/voice/schema.d.ts +65 -0
- package/dist/voice/schema.d.ts.map +1 -0
- package/dist/voice/schema.js +535 -0
- package/dist/voice/schema.js.map +1 -0
- package/dist/voice/types.d.ts +282 -0
- package/dist/voice/types.d.ts.map +1 -0
- package/dist/voice/types.js +37 -0
- package/dist/voice/types.js.map +1 -0
- package/package.json +72 -0
- package/src/addressability.ts +664 -0
- package/src/checker.ts +293 -0
- package/src/cli.ts +1166 -0
- package/src/copy-gate.ts +197 -0
- package/src/fingerprint.ts +66 -0
- package/src/index.ts +163 -0
- package/src/locale-coverage.ts +512 -0
- package/src/passage.ts +644 -0
- package/src/path-exclusions.ts +265 -0
- package/src/registry.ts +103 -0
- package/src/resolve.ts +172 -0
- package/src/scan.ts +1959 -0
- package/src/schema.ts +528 -0
- package/src/types.ts +301 -0
- package/src/voice/checker.ts +590 -0
- package/src/voice/derivation-coverage.ts +217 -0
- package/src/voice/fields.ts +160 -0
- package/src/voice/index.ts +55 -0
- package/src/voice/internal/parse-template.ts +114 -0
- package/src/voice/internal/pattern-safety.ts +336 -0
- package/src/voice/schema.ts +585 -0
- package/src/voice/types.ts +329 -0
- package/templates/voice-record.template.jsonc +135 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clossys/writer/voice — public entry point. See README.md for the full
|
|
3
|
+
* picture: the machinery-vs-values split this package mirrors from
|
|
4
|
+
* @example/ui/tokens, exactly what `checkCopy` does and does not
|
|
5
|
+
* attempt, and the `factRef` seam into a separate `strategy` package this
|
|
6
|
+
* package deliberately does not import from.
|
|
7
|
+
*/
|
|
8
|
+
export { FORMALITY_LEVELS, GLOSSARY_STATUSES, VOICE_SEVERITIES } from "./types.js";
|
|
9
|
+
export { VOICE_FIELDS, TEMPLATE_PLACEHOLDER } from "./fields.js";
|
|
10
|
+
export { validateVoiceRecordShape, parseVoiceRecord } from "./schema.js";
|
|
11
|
+
export { checkCopy, auditClaimsRegister, isCiBlockingSeverity } from "./checker.js";
|
|
12
|
+
// The other half of the seam `@example/strategy`'s
|
|
13
|
+
// `checkBrandCoverage` documents but cannot itself close (it does not, and
|
|
14
|
+
// cannot, import this package) — see `derivation-coverage.ts`'s top-of-file
|
|
15
|
+
// doc comment for the full account of the gap this closes.
|
|
16
|
+
export { checkVoiceDerivationCoverage } from "./derivation-coverage.js";
|
|
17
|
+
// The regex-safety gate itself — exported so a consumer can validate a
|
|
18
|
+
// pattern (e.g. in an editor/CI step, before it ever reaches a VoiceRecord)
|
|
19
|
+
// using the exact same logic schema.ts/checker.ts already run internally,
|
|
20
|
+
// rather than reimplementing or second-guessing it. See
|
|
21
|
+
// `internal/pattern-safety.ts`'s top doc comment for the full, documented
|
|
22
|
+
// regex-safety position this package takes.
|
|
23
|
+
export { checkPatternSafety } from "./internal/pattern-safety.js";
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/voice/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAiBnF,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGjE,OAAO,EAAE,wBAAwB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEzE,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAUpF,mDAAmD;AACnD,2EAA2E;AAC3E,4EAA4E;AAC5E,2DAA2D;AAC3D,OAAO,EAAE,4BAA4B,EAAE,MAAM,0BAA0B,CAAC;AAGxE,uEAAuE;AACvE,4EAA4E;AAC5E,0EAA0E;AAC1E,wDAAwD;AACxD,0EAA0E;AAC1E,4CAA4C;AAC5C,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal, test-only JSONC reader for `templates/voice-record.template.jsonc`
|
|
3
|
+
* — NOT part of this package's public API (nothing under `src/internal/` is
|
|
4
|
+
* reachable from `src/index.ts`, and `package.json#exports` never points
|
|
5
|
+
* here). It exists solely so `src/field-coverage.test.ts` can parse the
|
|
6
|
+
* template as data and check it against `src/fields.ts`'s `VOICE_FIELDS`,
|
|
7
|
+
* without pulling in a real JSONC parser as a dependency this package would
|
|
8
|
+
* otherwise have zero reason to carry — the same call `@example/ui/tokens`
|
|
9
|
+
* makes for `src/internal/parse-css.ts`.
|
|
10
|
+
*/
|
|
11
|
+
/** Parses `text` as JSONC (JSON plus `//` and `/* *\/` comments). Throws the underlying `JSON.parse` error, with position information, if the stripped result still isn't valid JSON. */
|
|
12
|
+
export declare function parseTemplate(text: string): unknown;
|
|
13
|
+
/**
|
|
14
|
+
* Flattens `value` into the set of dot-paths `src/fields.ts`'s `VOICE_FIELDS`
|
|
15
|
+
* uses, matching its own "a leaf, not a tree" rule: a plain object's keys
|
|
16
|
+
* are walked recursively, but an ARRAY is recorded as a single path (its
|
|
17
|
+
* own) and never descended into — `glossary` and `claims` are each one
|
|
18
|
+
* path, not `glossary.0.term`, `glossary.1.term`, ..., because an array's
|
|
19
|
+
* LENGTH and CONTENTS are exactly the bindable data a consumer supplies, not
|
|
20
|
+
* more structure for this function to enumerate. See `fields.ts`'s header
|
|
21
|
+
* comment, "A leaf, not a tree", for the full reasoning — this function is
|
|
22
|
+
* that rule's mechanical mirror image, the same relationship
|
|
23
|
+
* `findAllCustomPropertyNames` in `@example/ui/tokens`' own
|
|
24
|
+
* `internal/parse-css.ts` has to that package's `brand-template.css`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function extractFieldPaths(value: unknown, prefix?: string): Set<string>;
|
|
27
|
+
//# sourceMappingURL=parse-template.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-template.d.ts","sourceRoot":"","sources":["../../../src/voice/internal/parse-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA2DH,yLAAyL;AACzL,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,SAAK,GAAG,GAAG,CAAC,MAAM,CAAC,CA2B1E"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal, test-only JSONC reader for `templates/voice-record.template.jsonc`
|
|
3
|
+
* — NOT part of this package's public API (nothing under `src/internal/` is
|
|
4
|
+
* reachable from `src/index.ts`, and `package.json#exports` never points
|
|
5
|
+
* here). It exists solely so `src/field-coverage.test.ts` can parse the
|
|
6
|
+
* template as data and check it against `src/fields.ts`'s `VOICE_FIELDS`,
|
|
7
|
+
* without pulling in a real JSONC parser as a dependency this package would
|
|
8
|
+
* otherwise have zero reason to carry — the same call `@example/ui/tokens`
|
|
9
|
+
* makes for `src/internal/parse-css.ts`.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Strips `//` line comments and `/* *\/` block comments from `text`,
|
|
13
|
+
* WITHOUT touching either kind of sequence when it appears inside a JSON
|
|
14
|
+
* string literal (`"a // not a comment"`, `"a /* not one either *\/"`) —
|
|
15
|
+
* the one thing a naive regex-replace over the whole file gets wrong. Walks
|
|
16
|
+
* the text one character at a time, tracking exactly one piece of state
|
|
17
|
+
* (whether the cursor is inside a string, honoring `\"` escapes) so a `//`
|
|
18
|
+
* or `/*` that is really just part of a quoted value is copied through
|
|
19
|
+
* unchanged rather than eaten.
|
|
20
|
+
*/
|
|
21
|
+
function stripJsonComments(text) {
|
|
22
|
+
let out = "";
|
|
23
|
+
let inString = false;
|
|
24
|
+
for (let i = 0; i < text.length; i++) {
|
|
25
|
+
const ch = text[i];
|
|
26
|
+
const next = text[i + 1];
|
|
27
|
+
if (inString) {
|
|
28
|
+
out += ch;
|
|
29
|
+
if (ch === "\\") {
|
|
30
|
+
// Copy the escaped character too, unexamined — this is what keeps
|
|
31
|
+
// `\"` from prematurely ending the string and `\\` from being
|
|
32
|
+
// mistaken for the start of a second escape.
|
|
33
|
+
if (i + 1 < text.length) {
|
|
34
|
+
out += text[i + 1];
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (ch === '"')
|
|
40
|
+
inString = false;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (ch === '"') {
|
|
44
|
+
inString = true;
|
|
45
|
+
out += ch;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (ch === "/" && next === "/") {
|
|
49
|
+
while (i < text.length && text[i] !== "\n")
|
|
50
|
+
i++;
|
|
51
|
+
out += "\n"; // preserve line numbering for anyone debugging a parse error
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (ch === "/" && next === "*") {
|
|
55
|
+
i += 2;
|
|
56
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/"))
|
|
57
|
+
i++;
|
|
58
|
+
i++; // land on the trailing '/'
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
out += ch;
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
/** Parses `text` as JSONC (JSON plus `//` and `/* *\/` comments). Throws the underlying `JSON.parse` error, with position information, if the stripped result still isn't valid JSON. */
|
|
66
|
+
export function parseTemplate(text) {
|
|
67
|
+
return JSON.parse(stripJsonComments(text));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Flattens `value` into the set of dot-paths `src/fields.ts`'s `VOICE_FIELDS`
|
|
71
|
+
* uses, matching its own "a leaf, not a tree" rule: a plain object's keys
|
|
72
|
+
* are walked recursively, but an ARRAY is recorded as a single path (its
|
|
73
|
+
* own) and never descended into — `glossary` and `claims` are each one
|
|
74
|
+
* path, not `glossary.0.term`, `glossary.1.term`, ..., because an array's
|
|
75
|
+
* LENGTH and CONTENTS are exactly the bindable data a consumer supplies, not
|
|
76
|
+
* more structure for this function to enumerate. See `fields.ts`'s header
|
|
77
|
+
* comment, "A leaf, not a tree", for the full reasoning — this function is
|
|
78
|
+
* that rule's mechanical mirror image, the same relationship
|
|
79
|
+
* `findAllCustomPropertyNames` in `@example/ui/tokens`' own
|
|
80
|
+
* `internal/parse-css.ts` has to that package's `brand-template.css`.
|
|
81
|
+
*/
|
|
82
|
+
export function extractFieldPaths(value, prefix = "") {
|
|
83
|
+
const paths = new Set();
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
if (prefix)
|
|
86
|
+
paths.add(prefix);
|
|
87
|
+
return paths;
|
|
88
|
+
}
|
|
89
|
+
if (value !== null && typeof value === "object") {
|
|
90
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
91
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
92
|
+
const nestedPaths = extractFieldPaths(nestedValue, path);
|
|
93
|
+
if (nestedPaths.size === 0) {
|
|
94
|
+
// A leaf with no nested paths of its own (a string, number, boolean,
|
|
95
|
+
// null, or an empty object) is still a real field — record it directly.
|
|
96
|
+
paths.add(path);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
for (const nestedPath of nestedPaths)
|
|
100
|
+
paths.add(nestedPath);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return paths;
|
|
104
|
+
}
|
|
105
|
+
// A scalar reached with no prefix (extractFieldPaths called on a bare
|
|
106
|
+
// string/number/etc. at the root) has nothing to name — nothing to add.
|
|
107
|
+
if (prefix)
|
|
108
|
+
paths.add(prefix);
|
|
109
|
+
return paths;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=parse-template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-template.js","sourceRoot":"","sources":["../../../src/voice/internal/parse-template.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The regex-safety gate for `PatternRule.pattern` (see `../types.ts`) —
|
|
3
|
+
* shared by `../schema.ts` (registration-time validation, when a
|
|
4
|
+
* `VoiceRecord` is parsed from raw data) and `../checker.ts`
|
|
5
|
+
* (defense-in-depth at check time, since `checkCopy` deliberately does not
|
|
6
|
+
* trust its `VoiceRecord` argument — see that file's own doc comment).
|
|
7
|
+
*
|
|
8
|
+
* THE POSITION THIS PACKAGE TAKES ON REGEX SAFETY, STATED ONCE, IN FULL:
|
|
9
|
+
* bound the pattern at REGISTRATION TIME, never at run time. There is no
|
|
10
|
+
* runtime timeout, no worker thread, no execution sandbox anywhere in this
|
|
11
|
+
* package — Node has no synchronous way to cancel a regex mid-match, and a
|
|
12
|
+
* background-thread-plus-timeout scheme is real infrastructure this
|
|
13
|
+
* package's zero-dependency, synchronous, pure-function design (see
|
|
14
|
+
* `../types.ts`'s header comment) has no room for. Instead, `checkPatternSafety`
|
|
15
|
+
* below performs a STATIC analysis of the pattern's SOURCE TEXT and refuses
|
|
16
|
+
* to compile — reports as a finding, never silently drops (see `../schema.ts`
|
|
17
|
+
* and `../checker.ts`) — any pattern this analysis cannot bound:
|
|
18
|
+
*
|
|
19
|
+
* 1. Disallowed flags. Only `i` (case-insensitive), `u` (unicode), and `s`
|
|
20
|
+
* (dotAll) are accepted. `g`/`y` (global/sticky) are rejected because
|
|
21
|
+
* this package always does its own counting pass (see
|
|
22
|
+
* `countPatternMatches`) — accepting a caller's own `g`/`y` would only
|
|
23
|
+
* invite `lastIndex`-statefulness bugs for zero benefit. `m`
|
|
24
|
+
* (multiline) is rejected too: this package matches a pattern against a
|
|
25
|
+
* single piece of copy, never multi-line source text where `^`/`$`
|
|
26
|
+
* anchors are meant to bind per-line — accepting `m` would silently
|
|
27
|
+
* change what `^`/`$` mean in exactly the case most likely to surprise
|
|
28
|
+
* an author.
|
|
29
|
+
* 2. Pattern length. `source.length > MAX_PATTERN_SOURCE_LENGTH` (200) is
|
|
30
|
+
* rejected outright — a voice rule is a short, auditable phrase or
|
|
31
|
+
* character ban, never a novel-length expression, and a hard length
|
|
32
|
+
* cap bounds every other check's own work too.
|
|
33
|
+
* 3. Backreferences (`\1`-`\9`, `\k<name>`). A well-known, independent
|
|
34
|
+
* source of catastrophic backtracking, and one a voice/glossary rule
|
|
35
|
+
* has essentially no legitimate use for — rejected outright, no
|
|
36
|
+
* attempt to bound them.
|
|
37
|
+
* 4. Oversized bounded repetition (`{m,n}` or `{m}` whose effective upper
|
|
38
|
+
* bound exceeds `MAX_QUANTIFIER_BOUND`, 50). A large-but-bounded repeat
|
|
39
|
+
* count is still a real cost multiplier, especially combined with
|
|
40
|
+
* other repeats elsewhere in the same pattern.
|
|
41
|
+
* 5. NESTED QUANTIFIERS — the classic `(a+)+` / `(a*)*` / `(.*)+` shape
|
|
42
|
+
* that causes real exponential-time catastrophic backtracking.
|
|
43
|
+
* Detected via a hand-rolled structural scan (`analyzePatternSource`
|
|
44
|
+
* below): any quantifier (bounded or unbounded — `*`, `+`, `{m,}`, or
|
|
45
|
+
* `{m,n}`) applied to a GROUP whose own top-level content already
|
|
46
|
+
* contains an UNBOUNDED quantifier (`*`, `+`, or `{m,}` with no upper
|
|
47
|
+
* bound) is rejected. Deliberately conservative in the direction of
|
|
48
|
+
* REJECTING more than strictly necessary: a false positive (a pattern
|
|
49
|
+
* that would actually have run fine) is merely annoying to its author;
|
|
50
|
+
* a false negative here is a hung scanner, which this package will
|
|
51
|
+
* never accept as the trade.
|
|
52
|
+
*
|
|
53
|
+
* WHAT THIS DOES NOT CATCH — stated honestly, not silently:
|
|
54
|
+
*
|
|
55
|
+
* - Overlapping-alternation blowup (`(a|a)+`, `(a|ab)*`) that involves no
|
|
56
|
+
* nested quantifier at all. A real, known ReDoS shape this static scan
|
|
57
|
+
* does not detect — a full fix requires real regex-AST analysis (what
|
|
58
|
+
* tools like `safe-regex` do), which this package's zero-dependency,
|
|
59
|
+
* hand-rolled-scanner design does not attempt. Bounded in practice by
|
|
60
|
+
* the length cap (rule 2) and the fact that this package only ever
|
|
61
|
+
* matches a pattern against one short piece of product/marketing copy,
|
|
62
|
+
* never arbitrary-length adversarial input — but this is a mitigation,
|
|
63
|
+
* not a guarantee.
|
|
64
|
+
* - Aggregate cost across many pattern rules evaluated against the same
|
|
65
|
+
* `copy` string. Each pattern is bounded individually; the total cost of
|
|
66
|
+
* a large `patterns` list is a volume concern for the record's author,
|
|
67
|
+
* not something this gate polices.
|
|
68
|
+
*
|
|
69
|
+
* This is a real, bounded, DOCUMENTED static gate — not a general ReDoS
|
|
70
|
+
* detector, and this file does not claim to be one.
|
|
71
|
+
*/
|
|
72
|
+
/** Regex flags this package will construct a `RegExp` with. Anything else is rejected — see this file's top doc comment, point 1. */
|
|
73
|
+
export declare const PATTERN_ALLOWED_FLAGS: ReadonlySet<string>;
|
|
74
|
+
/** Hard cap on `pattern.source.length` — see this file's top doc comment, point 2. */
|
|
75
|
+
export declare const MAX_PATTERN_SOURCE_LENGTH = 200;
|
|
76
|
+
/** Hard cap on a bounded quantifier's effective upper bound (`{m,n}`/`{m}`) — see this file's top doc comment, point 4. */
|
|
77
|
+
export declare const MAX_QUANTIFIER_BOUND = 50;
|
|
78
|
+
export type PatternSafetyIssue = "pattern-source-shape" | "pattern-flags-shape" | "pattern-too-long" | "pattern-backreference" | "pattern-quantifier-bound-too-large" | "pattern-nested-quantifier" | "pattern-does-not-compile";
|
|
79
|
+
export interface PatternSafetyOk {
|
|
80
|
+
ok: true;
|
|
81
|
+
regex: RegExp;
|
|
82
|
+
}
|
|
83
|
+
export interface PatternSafetyFailure {
|
|
84
|
+
ok: false;
|
|
85
|
+
issue: PatternSafetyIssue;
|
|
86
|
+
detail: string;
|
|
87
|
+
}
|
|
88
|
+
export type PatternSafetyResult = PatternSafetyOk | PatternSafetyFailure;
|
|
89
|
+
/**
|
|
90
|
+
* The full registration-time (and defense-in-depth check-time) safety gate
|
|
91
|
+
* for one `VoicePattern`. `pattern` is `unknown`, not the `{ source, flags
|
|
92
|
+
* }` shape it will usually already be — this is called from both
|
|
93
|
+
* `../schema.ts` (validating raw, untrusted JSON, where `pattern` may be
|
|
94
|
+
* anything) and `../checker.ts` (re-checking a typed `VoiceRecord` this
|
|
95
|
+
* package's own `checkCopy` does not otherwise trust — see that file's doc
|
|
96
|
+
* comment; a hostile or malformed in-memory record could still hand this a
|
|
97
|
+
* non-object). Never throws, for any input.
|
|
98
|
+
*/
|
|
99
|
+
export declare function checkPatternSafety(pattern: unknown): PatternSafetyResult;
|
|
100
|
+
/**
|
|
101
|
+
* Runs an ALREADY-SAFE (passed `checkPatternSafety`) regex against `copy`,
|
|
102
|
+
* counting non-overlapping matches — the pattern-rule analog of
|
|
103
|
+
* `../checker.ts`'s own `countMatches`, except this never wraps the source
|
|
104
|
+
* in `\b...\b`: a pattern rule's whole point is that the caller's regex IS
|
|
105
|
+
* the expression, unmodified. Always adds "g" for counting, regardless of
|
|
106
|
+
* what flags were validated — "g" is never an allowed caller-supplied flag
|
|
107
|
+
* (see `checkPatternSafety`), so this is the one place it is ever added,
|
|
108
|
+
* and only for this internal counting pass.
|
|
109
|
+
*/
|
|
110
|
+
export declare function countPatternMatches(copy: string, regex: RegExp): number;
|
|
111
|
+
//# sourceMappingURL=pattern-safety.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pattern-safety.d.ts","sourceRoot":"","sources":["../../../src/voice/internal/pattern-safety.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAEH,qIAAqI;AACrI,eAAO,MAAM,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAA4B,CAAC;AAEnF,sFAAsF;AACtF,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C,2HAA2H;AAC3H,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAEvC,MAAM,MAAM,kBAAkB,GAC1B,sBAAsB,GACtB,qBAAqB,GACrB,kBAAkB,GAClB,uBAAuB,GACvB,oCAAoC,GACpC,2BAA2B,GAC3B,0BAA0B,CAAC;AAE/B,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,IAAI,CAAC;IACT,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,kBAAkB,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,mBAAmB,GAAG,eAAe,GAAG,oBAAoB,CAAC;AAyKzE;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,mBAAmB,CAuCxE;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIvE"}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The regex-safety gate for `PatternRule.pattern` (see `../types.ts`) —
|
|
3
|
+
* shared by `../schema.ts` (registration-time validation, when a
|
|
4
|
+
* `VoiceRecord` is parsed from raw data) and `../checker.ts`
|
|
5
|
+
* (defense-in-depth at check time, since `checkCopy` deliberately does not
|
|
6
|
+
* trust its `VoiceRecord` argument — see that file's own doc comment).
|
|
7
|
+
*
|
|
8
|
+
* THE POSITION THIS PACKAGE TAKES ON REGEX SAFETY, STATED ONCE, IN FULL:
|
|
9
|
+
* bound the pattern at REGISTRATION TIME, never at run time. There is no
|
|
10
|
+
* runtime timeout, no worker thread, no execution sandbox anywhere in this
|
|
11
|
+
* package — Node has no synchronous way to cancel a regex mid-match, and a
|
|
12
|
+
* background-thread-plus-timeout scheme is real infrastructure this
|
|
13
|
+
* package's zero-dependency, synchronous, pure-function design (see
|
|
14
|
+
* `../types.ts`'s header comment) has no room for. Instead, `checkPatternSafety`
|
|
15
|
+
* below performs a STATIC analysis of the pattern's SOURCE TEXT and refuses
|
|
16
|
+
* to compile — reports as a finding, never silently drops (see `../schema.ts`
|
|
17
|
+
* and `../checker.ts`) — any pattern this analysis cannot bound:
|
|
18
|
+
*
|
|
19
|
+
* 1. Disallowed flags. Only `i` (case-insensitive), `u` (unicode), and `s`
|
|
20
|
+
* (dotAll) are accepted. `g`/`y` (global/sticky) are rejected because
|
|
21
|
+
* this package always does its own counting pass (see
|
|
22
|
+
* `countPatternMatches`) — accepting a caller's own `g`/`y` would only
|
|
23
|
+
* invite `lastIndex`-statefulness bugs for zero benefit. `m`
|
|
24
|
+
* (multiline) is rejected too: this package matches a pattern against a
|
|
25
|
+
* single piece of copy, never multi-line source text where `^`/`$`
|
|
26
|
+
* anchors are meant to bind per-line — accepting `m` would silently
|
|
27
|
+
* change what `^`/`$` mean in exactly the case most likely to surprise
|
|
28
|
+
* an author.
|
|
29
|
+
* 2. Pattern length. `source.length > MAX_PATTERN_SOURCE_LENGTH` (200) is
|
|
30
|
+
* rejected outright — a voice rule is a short, auditable phrase or
|
|
31
|
+
* character ban, never a novel-length expression, and a hard length
|
|
32
|
+
* cap bounds every other check's own work too.
|
|
33
|
+
* 3. Backreferences (`\1`-`\9`, `\k<name>`). A well-known, independent
|
|
34
|
+
* source of catastrophic backtracking, and one a voice/glossary rule
|
|
35
|
+
* has essentially no legitimate use for — rejected outright, no
|
|
36
|
+
* attempt to bound them.
|
|
37
|
+
* 4. Oversized bounded repetition (`{m,n}` or `{m}` whose effective upper
|
|
38
|
+
* bound exceeds `MAX_QUANTIFIER_BOUND`, 50). A large-but-bounded repeat
|
|
39
|
+
* count is still a real cost multiplier, especially combined with
|
|
40
|
+
* other repeats elsewhere in the same pattern.
|
|
41
|
+
* 5. NESTED QUANTIFIERS — the classic `(a+)+` / `(a*)*` / `(.*)+` shape
|
|
42
|
+
* that causes real exponential-time catastrophic backtracking.
|
|
43
|
+
* Detected via a hand-rolled structural scan (`analyzePatternSource`
|
|
44
|
+
* below): any quantifier (bounded or unbounded — `*`, `+`, `{m,}`, or
|
|
45
|
+
* `{m,n}`) applied to a GROUP whose own top-level content already
|
|
46
|
+
* contains an UNBOUNDED quantifier (`*`, `+`, or `{m,}` with no upper
|
|
47
|
+
* bound) is rejected. Deliberately conservative in the direction of
|
|
48
|
+
* REJECTING more than strictly necessary: a false positive (a pattern
|
|
49
|
+
* that would actually have run fine) is merely annoying to its author;
|
|
50
|
+
* a false negative here is a hung scanner, which this package will
|
|
51
|
+
* never accept as the trade.
|
|
52
|
+
*
|
|
53
|
+
* WHAT THIS DOES NOT CATCH — stated honestly, not silently:
|
|
54
|
+
*
|
|
55
|
+
* - Overlapping-alternation blowup (`(a|a)+`, `(a|ab)*`) that involves no
|
|
56
|
+
* nested quantifier at all. A real, known ReDoS shape this static scan
|
|
57
|
+
* does not detect — a full fix requires real regex-AST analysis (what
|
|
58
|
+
* tools like `safe-regex` do), which this package's zero-dependency,
|
|
59
|
+
* hand-rolled-scanner design does not attempt. Bounded in practice by
|
|
60
|
+
* the length cap (rule 2) and the fact that this package only ever
|
|
61
|
+
* matches a pattern against one short piece of product/marketing copy,
|
|
62
|
+
* never arbitrary-length adversarial input — but this is a mitigation,
|
|
63
|
+
* not a guarantee.
|
|
64
|
+
* - Aggregate cost across many pattern rules evaluated against the same
|
|
65
|
+
* `copy` string. Each pattern is bounded individually; the total cost of
|
|
66
|
+
* a large `patterns` list is a volume concern for the record's author,
|
|
67
|
+
* not something this gate polices.
|
|
68
|
+
*
|
|
69
|
+
* This is a real, bounded, DOCUMENTED static gate — not a general ReDoS
|
|
70
|
+
* detector, and this file does not claim to be one.
|
|
71
|
+
*/
|
|
72
|
+
/** Regex flags this package will construct a `RegExp` with. Anything else is rejected — see this file's top doc comment, point 1. */
|
|
73
|
+
export const PATTERN_ALLOWED_FLAGS = new Set(["i", "u", "s"]);
|
|
74
|
+
/** Hard cap on `pattern.source.length` — see this file's top doc comment, point 2. */
|
|
75
|
+
export const MAX_PATTERN_SOURCE_LENGTH = 200;
|
|
76
|
+
/** Hard cap on a bounded quantifier's effective upper bound (`{m,n}`/`{m}`) — see this file's top doc comment, point 4. */
|
|
77
|
+
export const MAX_QUANTIFIER_BOUND = 50;
|
|
78
|
+
function fail(issue, detail) {
|
|
79
|
+
return { ok: false, issue, detail };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Structural scan for backreferences and nested/oversized quantifiers — see
|
|
83
|
+
* this file's top doc comment for exactly what is and is not caught. Walks
|
|
84
|
+
* `source` one character at a time, tracking character-class state (`[...]`)
|
|
85
|
+
* and a group-nesting stack, exactly the same bounded, hand-rolled style
|
|
86
|
+
* `../../scan.ts`'s own tokenizer uses for JS/JSX (see that file's top doc
|
|
87
|
+
* comment) — no regex-of-regexes, no parser library. Returns the FIRST
|
|
88
|
+
* issue found, or `undefined` if the source passes this scan (compilation
|
|
89
|
+
* itself is checked separately, by the caller, via `new RegExp`).
|
|
90
|
+
*/
|
|
91
|
+
function analyzePatternSource(source) {
|
|
92
|
+
const n = source.length;
|
|
93
|
+
let i = 0;
|
|
94
|
+
let inClass = false;
|
|
95
|
+
const stack = [{ hasUnboundedQuantifier: false }];
|
|
96
|
+
// The frame belonging to the group that was JUST closed by the immediately
|
|
97
|
+
// preceding ")" — cleared by every other kind of token, since only a
|
|
98
|
+
// quantifier reading this value RIGHT AFTER a ")" means "applies to that
|
|
99
|
+
// group".
|
|
100
|
+
let lastClosedGroupFrame;
|
|
101
|
+
const applyUnboundedQuantifier = () => {
|
|
102
|
+
if (lastClosedGroupFrame?.hasUnboundedQuantifier) {
|
|
103
|
+
return fail("pattern-nested-quantifier", `a quantifier at offset ${i} applies to a group that itself contains an unbounded quantifier — this is the classic catastrophic-backtracking shape (e.g. "(a+)+"); rejected without attempting to run it`);
|
|
104
|
+
}
|
|
105
|
+
const top = stack[stack.length - 1];
|
|
106
|
+
if (top)
|
|
107
|
+
top.hasUnboundedQuantifier = true;
|
|
108
|
+
return undefined;
|
|
109
|
+
};
|
|
110
|
+
while (i < n) {
|
|
111
|
+
const c = source[i];
|
|
112
|
+
if (inClass) {
|
|
113
|
+
if (c === "\\") {
|
|
114
|
+
i += 2;
|
|
115
|
+
}
|
|
116
|
+
else if (c === "]") {
|
|
117
|
+
inClass = false;
|
|
118
|
+
i++;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
i++;
|
|
122
|
+
}
|
|
123
|
+
lastClosedGroupFrame = undefined;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (c === "\\") {
|
|
127
|
+
const next = source[i + 1];
|
|
128
|
+
if (next !== undefined && next >= "1" && next <= "9") {
|
|
129
|
+
return fail("pattern-backreference", `backreference "\\${next}" at offset ${i} — a known independent source of catastrophic backtracking, rejected outright`);
|
|
130
|
+
}
|
|
131
|
+
if (next === "k" && source[i + 2] === "<") {
|
|
132
|
+
return fail("pattern-backreference", `named backreference "\\k<...>" at offset ${i} — a known independent source of catastrophic backtracking, rejected outright`);
|
|
133
|
+
}
|
|
134
|
+
i += 2;
|
|
135
|
+
lastClosedGroupFrame = undefined;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (c === "[") {
|
|
139
|
+
inClass = true;
|
|
140
|
+
i++;
|
|
141
|
+
lastClosedGroupFrame = undefined;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (c === "(") {
|
|
145
|
+
stack.push({ hasUnboundedQuantifier: false });
|
|
146
|
+
i++;
|
|
147
|
+
lastClosedGroupFrame = undefined;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (c === ")") {
|
|
151
|
+
const closed = stack.pop();
|
|
152
|
+
if (!closed)
|
|
153
|
+
return fail("pattern-does-not-compile", `unmatched ")" at offset ${i}`);
|
|
154
|
+
i++;
|
|
155
|
+
lastClosedGroupFrame = closed;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (c === "*" || c === "+") {
|
|
159
|
+
const issue = applyUnboundedQuantifier();
|
|
160
|
+
if (issue)
|
|
161
|
+
return issue;
|
|
162
|
+
i++;
|
|
163
|
+
if (source[i] === "?")
|
|
164
|
+
i++; // lazy modifier — still unbounded, does not change the analysis
|
|
165
|
+
lastClosedGroupFrame = undefined;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (c === "?") {
|
|
169
|
+
// A bounded (0-1) quantifier on the previous atom/group. Never marks
|
|
170
|
+
// the enclosing frame as unbounded, and a group with an inner
|
|
171
|
+
// unbounded quantifier wrapped as "(...)?" (applied at most once) is
|
|
172
|
+
// NOT the nested-quantifier shape this scan exists to catch.
|
|
173
|
+
i++;
|
|
174
|
+
lastClosedGroupFrame = undefined;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (c === "{") {
|
|
178
|
+
const close = source.indexOf("}", i);
|
|
179
|
+
const body = close === -1 ? undefined : source.slice(i + 1, close);
|
|
180
|
+
const match = body === undefined ? null : /^(\d+)(,(\d*))?$/.exec(body);
|
|
181
|
+
if (!match) {
|
|
182
|
+
// Not valid `{n}`/`{n,}`/`{n,m}` quantifier syntax — JS treats a
|
|
183
|
+
// lone "{" that doesn't form one of these as a literal character,
|
|
184
|
+
// not an error, so this scan does the same.
|
|
185
|
+
i++;
|
|
186
|
+
lastClosedGroupFrame = undefined;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const hasComma = match[2] !== undefined;
|
|
190
|
+
const upperText = match[3];
|
|
191
|
+
const unbounded = hasComma && (upperText === undefined || upperText === "");
|
|
192
|
+
if (unbounded) {
|
|
193
|
+
const issue = applyUnboundedQuantifier();
|
|
194
|
+
if (issue)
|
|
195
|
+
return issue;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const upper = hasComma ? Number(upperText) : Number(match[1]);
|
|
199
|
+
if (upper > MAX_QUANTIFIER_BOUND) {
|
|
200
|
+
return fail("pattern-quantifier-bound-too-large", `quantifier "{${body}}" at offset ${i} has an upper bound of ${upper}, exceeding the maximum of ${MAX_QUANTIFIER_BOUND}`);
|
|
201
|
+
}
|
|
202
|
+
if (lastClosedGroupFrame?.hasUnboundedQuantifier) {
|
|
203
|
+
return fail("pattern-nested-quantifier", `quantifier "{${body}}" at offset ${i} applies to a group that itself contains an unbounded quantifier`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
i = close + 1;
|
|
207
|
+
if (source[i] === "?")
|
|
208
|
+
i++;
|
|
209
|
+
lastClosedGroupFrame = undefined;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
i++;
|
|
213
|
+
lastClosedGroupFrame = undefined;
|
|
214
|
+
}
|
|
215
|
+
if (stack.length !== 1) {
|
|
216
|
+
return fail("pattern-does-not-compile", `unbalanced "(" — ${stack.length - 1} group(s) never closed`);
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* The full registration-time (and defense-in-depth check-time) safety gate
|
|
222
|
+
* for one `VoicePattern`. `pattern` is `unknown`, not the `{ source, flags
|
|
223
|
+
* }` shape it will usually already be — this is called from both
|
|
224
|
+
* `../schema.ts` (validating raw, untrusted JSON, where `pattern` may be
|
|
225
|
+
* anything) and `../checker.ts` (re-checking a typed `VoiceRecord` this
|
|
226
|
+
* package's own `checkCopy` does not otherwise trust — see that file's doc
|
|
227
|
+
* comment; a hostile or malformed in-memory record could still hand this a
|
|
228
|
+
* non-object). Never throws, for any input.
|
|
229
|
+
*/
|
|
230
|
+
export function checkPatternSafety(pattern) {
|
|
231
|
+
if (typeof pattern !== "object" || pattern === null || Array.isArray(pattern)) {
|
|
232
|
+
return fail("pattern-source-shape", `pattern must be an object with a "source" string, got ${JSON.stringify(pattern)}`);
|
|
233
|
+
}
|
|
234
|
+
const source = pattern.source;
|
|
235
|
+
if (typeof source !== "string" || source.length === 0) {
|
|
236
|
+
return fail("pattern-source-shape", `pattern.source must be a non-empty string, got ${JSON.stringify(source)}`);
|
|
237
|
+
}
|
|
238
|
+
if (source.length > MAX_PATTERN_SOURCE_LENGTH) {
|
|
239
|
+
return fail("pattern-too-long", `pattern.source is ${source.length} character(s), exceeding the maximum of ${MAX_PATTERN_SOURCE_LENGTH}`);
|
|
240
|
+
}
|
|
241
|
+
const flags = pattern.flags;
|
|
242
|
+
if (flags !== undefined) {
|
|
243
|
+
if (typeof flags !== "string") {
|
|
244
|
+
return fail("pattern-flags-shape", `pattern.flags must be a string when present, got ${JSON.stringify(flags)}`);
|
|
245
|
+
}
|
|
246
|
+
for (const flag of flags) {
|
|
247
|
+
if (!PATTERN_ALLOWED_FLAGS.has(flag)) {
|
|
248
|
+
return fail("pattern-flags-shape", `pattern.flags "${flags}" contains "${flag}", which is not one of the allowed flags (${[...PATTERN_ALLOWED_FLAGS].join(", ")}) — see this file's top doc comment for why "g"/"y"/"m" are rejected`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const structuralIssue = analyzePatternSource(source);
|
|
253
|
+
if (structuralIssue)
|
|
254
|
+
return structuralIssue;
|
|
255
|
+
try {
|
|
256
|
+
const regex = new RegExp(source, flags ?? "");
|
|
257
|
+
return { ok: true, regex };
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
return fail("pattern-does-not-compile", error instanceof Error ? error.message : String(error));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Runs an ALREADY-SAFE (passed `checkPatternSafety`) regex against `copy`,
|
|
265
|
+
* counting non-overlapping matches — the pattern-rule analog of
|
|
266
|
+
* `../checker.ts`'s own `countMatches`, except this never wraps the source
|
|
267
|
+
* in `\b...\b`: a pattern rule's whole point is that the caller's regex IS
|
|
268
|
+
* the expression, unmodified. Always adds "g" for counting, regardless of
|
|
269
|
+
* what flags were validated — "g" is never an allowed caller-supplied flag
|
|
270
|
+
* (see `checkPatternSafety`), so this is the one place it is ever added,
|
|
271
|
+
* and only for this internal counting pass.
|
|
272
|
+
*/
|
|
273
|
+
export function countPatternMatches(copy, regex) {
|
|
274
|
+
const counting = new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
|
|
275
|
+
const matches = copy.match(counting);
|
|
276
|
+
return matches ? matches.length : 0;
|
|
277
|
+
}
|
|
278
|
+
//# sourceMappingURL=pattern-safety.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pattern-safety.js","sourceRoot":"","sources":["../../../src/voice/internal/pattern-safety.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAEH,qIAAqI;AACrI,MAAM,CAAC,MAAM,qBAAqB,GAAwB,IAAI,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAEnF,sFAAsF;AACtF,MAAM,CAAC,MAAM,yBAAyB,GAAG,GAAG,CAAC;AAE7C,2HAA2H;AAC3H,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAwBvC,SAAS,IAAI,CAAC,KAAyB,EAAE,MAAc;IACrD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AACtC,CAAC;AAOD;;;;;;;;;GASG;AACH,SAAS,oBAAoB,CAAC,MAAc;IAC1C,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;IACxB,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,KAAK,GAAiB,CAAC,EAAE,sBAAsB,EAAE,KAAK,EAAE,CAAC,CAAC;IAChE,2EAA2E;IAC3E,qEAAqE;IACrE,yEAAyE;IACzE,UAAU;IACV,IAAI,oBAA4C,CAAC;IAEjD,MAAM,wBAAwB,GAAG,GAAqC,EAAE;QACtE,IAAI,oBAAoB,EAAE,sBAAsB,EAAE,CAAC;YACjD,OAAO,IAAI,CACT,2BAA2B,EAC3B,0BAA0B,CAAC,8KAA8K,CAC1M,CAAC;QACJ,CAAC;QACD,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QACpC,IAAI,GAAG;YAAE,GAAG,CAAC,sBAAsB,GAAG,IAAI,CAAC;QAC3C,OAAO,SAAS,CAAC;IACnB,CAAC,CAAC;IAEF,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACb,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC,CAAW,CAAC;QAE9B,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;gBACf,CAAC,IAAI,CAAC,CAAC;YACT,CAAC;iBAAM,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;gBACrB,OAAO,GAAG,KAAK,CAAC;gBAChB,CAAC,EAAE,CAAC;YACN,CAAC;iBAAM,CAAC;gBACN,CAAC,EAAE,CAAC;YACN,CAAC;YACD,oBAAoB,GAAG,SAAS,CAAC;YACjC,SAAS;QACX,CAAC;QAED,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC;YACf,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAC3B,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,EAAE,CAAC;gBACrD,OAAO,IAAI,CACT,uBAAuB,EACvB,oBAAoB,IAAI,eAAe,CAAC,+EAA+E,CACxH,CAAC;YACJ,CAAC;YACD,IAAI,IAAI,KAAK,GAAG,IAAI,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBAC1C,OAAO,IAAI,CACT,uBAAuB,EACvB,4CAA4C,CAAC,+EAA+E,CAC7H,CAAC;YACJ,CAAC;YACD,CAAC,IAAI,CAAC,CAAC;YACP,oBAAoB,GAAG,SAAS,CAAC;YACjC,SAAS;QACX,CAAC;QAED,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACd,OAAO,GAAG,IAAI,CAAC;YACf,CAAC,EAAE,CAAC;YACJ,oBAAoB,GAAG,SAAS,CAAC;YACjC,SAAS;QACX,CAAC;QAED,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACd,KAAK,CAAC,IAAI,CAAC,EAAE,sBAAsB,EAAE,KAAK,EAAE,CAAC,CAAC;YAC9C,CAAC,EAAE,CAAC;YACJ,oBAAoB,GAAG,SAAS,CAAC;YACjC,SAAS;QACX,CAAC;QAED,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACd,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC;YAC3B,IAAI,CAAC,MAAM;gBAAE,OAAO,IAAI,CAAC,0BAA0B,EAAE,2BAA2B,CAAC,EAAE,CAAC,CAAC;YACrF,CAAC,EAAE,CAAC;YACJ,oBAAoB,GAAG,MAAM,CAAC;YAC9B,SAAS;QACX,CAAC;QAED,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YAC3B,MAAM,KAAK,GAAG,wBAAwB,EAAE,CAAC;YACzC,IAAI,KAAK;gBAAE,OAAO,KAAK,CAAC;YACxB,CAAC,EAAE,CAAC;YACJ,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG;gBAAE,CAAC,EAAE,CAAC,CAAC,gEAAgE;YAC5F,oBAAoB,GAAG,SAAS,CAAC;YACjC,SAAS;QACX,CAAC;QAED,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACd,qEAAqE;YACrE,8DAA8D;YAC9D,qEAAqE;YACrE,6DAA6D;YAC7D,CAAC,EAAE,CAAC;YACJ,oBAAoB,GAAG,SAAS,CAAC;YACjC,SAAS;QACX,CAAC;QAED,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACd,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;YACrC,MAAM,IAAI,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC;YACnE,MAAM,KAAK,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACxE,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,iEAAiE;gBACjE,kEAAkE;gBAClE,4CAA4C;gBAC5C,CAAC,EAAE,CAAC;gBACJ,oBAAoB,GAAG,SAAS,CAAC;gBACjC,SAAS;YACX,CAAC;YACD,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC;YACxC,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;YAC3B,MAAM,SAAS,GAAG,QAAQ,IAAI,CAAC,SAAS,KAAK,SAAS,IAAI,SAAS,KAAK,EAAE,CAAC,CAAC;YAC5E,IAAI,SAAS,EAAE,CAAC;gBACd,MAAM,KAAK,GAAG,wBAAwB,EAAE,CAAC;gBACzC,IAAI,KAAK;oBAAE,OAAO,KAAK,CAAC;YAC1B,CAAC;iBAAM,CAAC;gBACN,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;gBAC9D,IAAI,KAAK,GAAG,oBAAoB,EAAE,CAAC;oBACjC,OAAO,IAAI,CACT,oCAAoC,EACpC,gBAAgB,IAAI,gBAAgB,CAAC,0BAA0B,KAAK,8BAA8B,oBAAoB,EAAE,CACzH,CAAC;gBACJ,CAAC;gBACD,IAAI,oBAAoB,EAAE,sBAAsB,EAAE,CAAC;oBACjD,OAAO,IAAI,CACT,2BAA2B,EAC3B,gBAAgB,IAAI,gBAAgB,CAAC,kEAAkE,CACxG,CAAC;gBACJ,CAAC;YACH,CAAC;YACD,CAAC,GAAI,KAAgB,GAAG,CAAC,CAAC;YAC1B,IAAI,MAAM,CAAC,CAAC,CAAC,KAAK,GAAG;gBAAE,CAAC,EAAE,CAAC;YAC3B,oBAAoB,GAAG,SAAS,CAAC;YACjC,SAAS;QACX,CAAC;QAED,CAAC,EAAE,CAAC;QACJ,oBAAoB,GAAG,SAAS,CAAC;IACnC,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,IAAI,CAAC,0BAA0B,EAAE,oBAAoB,KAAK,CAAC,MAAM,GAAG,CAAC,wBAAwB,CAAC,CAAC;IACxG,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAgB;IACjD,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC9E,OAAO,IAAI,CAAC,sBAAsB,EAAE,yDAAyD,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC1H,CAAC;IACD,MAAM,MAAM,GAAI,OAAgC,CAAC,MAAM,CAAC;IACxD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtD,OAAO,IAAI,CAAC,sBAAsB,EAAE,kDAAkD,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAClH,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,GAAG,yBAAyB,EAAE,CAAC;QAC9C,OAAO,IAAI,CACT,kBAAkB,EAClB,qBAAqB,MAAM,CAAC,MAAM,2CAA2C,yBAAyB,EAAE,CACzG,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAI,OAA+B,CAAC,KAAK,CAAC;IACrD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9B,OAAO,IAAI,CAAC,qBAAqB,EAAE,oDAAoD,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAClH,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACrC,OAAO,IAAI,CACT,qBAAqB,EACrB,kBAAkB,KAAK,eAAe,IAAI,6CAA6C,CAAC,GAAG,qBAAqB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,sEAAsE,CACnM,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,eAAe,GAAG,oBAAoB,CAAC,MAAM,CAAC,CAAC;IACrD,IAAI,eAAe;QAAE,OAAO,eAAe,CAAC;IAE5C,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;QAC9C,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAC7B,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,IAAI,CAAC,0BAA0B,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAClG,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY,EAAE,KAAa;IAC7D,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC;IACvG,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACrC,OAAO,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AACtC,CAAC"}
|