@loomwork/classify 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +73 -2
- package/dist/classify.d.ts +47 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/classify.js +147 -0
- package/dist/classify.js.map +1 -0
- package/dist/esm/classify.js +135 -0
- package/dist/esm/classify.js.map +1 -0
- package/dist/esm/expand.js +87 -0
- package/dist/esm/expand.js.map +1 -0
- package/dist/esm/external.js +33 -0
- package/dist/esm/external.js.map +1 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/redact.js +56 -0
- package/dist/esm/redact.js.map +1 -0
- package/dist/esm/taint.js +142 -0
- package/dist/esm/taint.js.map +1 -0
- package/dist/esm/unreachable.js +126 -0
- package/dist/esm/unreachable.js.map +1 -0
- package/dist/expand.d.ts +16 -0
- package/dist/expand.d.ts.map +1 -0
- package/dist/expand.js +91 -0
- package/dist/expand.js.map +1 -0
- package/dist/external.d.ts +15 -0
- package/dist/external.d.ts.map +1 -0
- package/dist/external.js +38 -0
- package/dist/external.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/redact.d.ts +4 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +60 -0
- package/dist/redact.js.map +1 -0
- package/dist/taint.d.ts +85 -0
- package/dist/taint.d.ts.map +1 -0
- package/dist/taint.js +152 -0
- package/dist/taint.js.map +1 -0
- package/dist/unreachable.d.ts +20 -0
- package/dist/unreachable.d.ts.map +1 -0
- package/dist/unreachable.js +129 -0
- package/dist/unreachable.js.map +1 -0
- package/package.json +55 -3
- package/src/classify.ts +175 -0
- package/src/expand.ts +97 -0
- package/src/external.ts +40 -0
- package/src/index.ts +31 -0
- package/src/redact.ts +59 -0
- package/src/taint.ts +189 -0
- package/src/unreachable.ts +164 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// ── Wildcard path expansion (#260 option 2) ───────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// The dot-path sinks — telemetry masking, field-level encryption, content externalization —
|
|
4
|
+
// address scalar paths and originally could not descend into array elements, so a mark
|
|
5
|
+
// placed inside one did nothing at all. `[]` is the array-crossing segment that fixes it:
|
|
6
|
+
// `people[].ssn` means "the `ssn` of every element of `people`".
|
|
7
|
+
//
|
|
8
|
+
// Rather than teach three near-identical spine-walkers about arrays, expansion happens ONCE
|
|
9
|
+
// against the concrete value and yields ordinary indexed paths (`people.0.ssn`,
|
|
10
|
+
// `people.1.ssn`) that every existing applier already handles — `array["0"]` is a normal
|
|
11
|
+
// property access, and each applier's spine copy already preserves arrays. So the walkers
|
|
12
|
+
// stay as they were, and there is one place where array semantics live.
|
|
13
|
+
//
|
|
14
|
+
// A path with no `[]` passes through untouched, so every pre-existing path behaves exactly
|
|
15
|
+
// as before (G2).
|
|
16
|
+
/** Split a segment into its key and how many array crossings follow it (`rows[][]` ⇒ 2). */
|
|
17
|
+
function parseSegment(segment) {
|
|
18
|
+
let depth = 0;
|
|
19
|
+
let key = segment;
|
|
20
|
+
while (key.endsWith("[]")) {
|
|
21
|
+
key = key.slice(0, -2);
|
|
22
|
+
depth++;
|
|
23
|
+
}
|
|
24
|
+
return { key, depth };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Expand `paths` against `root`, replacing every `[]` with the concrete indices present in
|
|
28
|
+
* the value.
|
|
29
|
+
*
|
|
30
|
+
* - `"a.b"` (no wildcard) → `["a.b"]`, unchanged and unchecked; the appliers already skip
|
|
31
|
+
* absent paths, and pre-existing behaviour must not shift.
|
|
32
|
+
* - `"people[].ssn"` over two people → `["people.0.ssn", "people.1.ssn"]`.
|
|
33
|
+
* - `"tags[]"` (a marked array ELEMENT type) → one path per element.
|
|
34
|
+
* - A wildcard that does not resolve — absent, or `[]` over a non-array — expands to
|
|
35
|
+
* nothing, so nothing is masked/encrypted for it. Resolving against the value is the whole
|
|
36
|
+
* point of expanding late.
|
|
37
|
+
*/
|
|
38
|
+
export function expandPaths(root, paths) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const path of paths) {
|
|
41
|
+
if (!path.includes("[]")) {
|
|
42
|
+
out.push(path);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
expandInto(root, path.split("."), 0, [], out);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/** True if any path uses a wildcard — lets callers skip expansion entirely (G2 fast path). */
|
|
50
|
+
export function hasWildcard(paths) {
|
|
51
|
+
for (const p of paths)
|
|
52
|
+
if (p.includes("[]"))
|
|
53
|
+
return true;
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
function expandInto(value, segments, index, acc, out) {
|
|
57
|
+
if (index >= segments.length) {
|
|
58
|
+
out.push(acc.join("."));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (typeof value !== "object" || value === null)
|
|
62
|
+
return;
|
|
63
|
+
const { key, depth } = parseSegment(segments[index]);
|
|
64
|
+
if (!Object.hasOwn(value, key))
|
|
65
|
+
return;
|
|
66
|
+
const child = value[key];
|
|
67
|
+
descend(child, depth, [...acc, key], segments, index, out);
|
|
68
|
+
}
|
|
69
|
+
/** Walk `depth` array levels, emitting an index per level, then continue with the next segment. */
|
|
70
|
+
function descend(value, depth, acc, segments, index, out) {
|
|
71
|
+
if (depth === 0) {
|
|
72
|
+
// Reached the value this segment names. If it is the last segment it IS the target;
|
|
73
|
+
// otherwise keep walking.
|
|
74
|
+
if (index === segments.length - 1)
|
|
75
|
+
out.push(acc.join("."));
|
|
76
|
+
else
|
|
77
|
+
expandInto(value, segments, index + 1, acc, out);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// A `[]` over a non-array resolves to nothing — the mark does not apply to this value.
|
|
81
|
+
if (!Array.isArray(value))
|
|
82
|
+
return;
|
|
83
|
+
for (const [i, element] of value.entries()) {
|
|
84
|
+
descend(element, depth - 1, [...acc, String(i)], segments, index, out);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=expand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expand.js","sourceRoot":"","sources":["../../src/expand.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,EAAE;AACF,4FAA4F;AAC5F,uFAAuF;AACvF,0FAA0F;AAC1F,iEAAiE;AACjE,EAAE;AACF,4FAA4F;AAC5F,gFAAgF;AAChF,yFAAyF;AACzF,0FAA0F;AAC1F,wEAAwE;AACxE,EAAE;AACF,2FAA2F;AAC3F,kBAAkB;AAElB,4FAA4F;AAC5F,SAAS,YAAY,CAAC,OAAe;IACnC,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,GAAG,GAAG,OAAO,CAAC;IAClB,OAAO,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1B,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACvB,KAAK,EAAE,CAAC;IACV,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;AACxB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,WAAW,CAAC,IAAa,EAAE,KAAwB;IACjE,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACf,SAAS;QACX,CAAC;QACD,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,WAAW,CAAC,KAAwB;IAClD,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IACzD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,UAAU,CACjB,KAAc,EACd,QAA2B,EAC3B,KAAa,EACb,GAAsB,EACtB,GAAa;IAEb,IAAI,KAAK,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;QAC7B,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QACxB,OAAO;IACT,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO;IACxD,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAW,CAAC,CAAC;IAC/D,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,GAAG,CAAC;QAAE,OAAO;IACvC,MAAM,KAAK,GAAI,KAAiC,CAAC,GAAG,CAAC,CAAC;IACtD,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,CAAC,GAAG,GAAG,EAAE,GAAG,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED,mGAAmG;AACnG,SAAS,OAAO,CACd,KAAc,EACd,KAAa,EACb,GAAsB,EACtB,QAA2B,EAC3B,KAAa,EACb,GAAa;IAEb,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;QAChB,oFAAoF;QACpF,0BAA0B;QAC1B,IAAI,KAAK,KAAK,QAAQ,CAAC,MAAM,GAAG,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;;YACtD,UAAU,CAAC,KAAK,EAAE,QAAQ,EAAE,KAAK,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;QACtD,OAAO;IACT,CAAC;IACD,uFAAuF;IACvF,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO;IAClC,KAAK,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAC3C,OAAO,CAAC,OAAO,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,GAAG,CAAC,CAAC;IACzE,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { collectMarkedPaths } from "./classify";
|
|
2
|
+
// ── ADR 0046 — the `externalized()` marker ────────────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// Orthogonal to the secret/sensitive/internal taxonomy: that axis says HOW sensitive a field
|
|
5
|
+
// is; this one says WHERE its value is stored — externalized to a content store, with the log
|
|
6
|
+
// keeping only a reference. A field can be both (a large AND sensitive document: externalize
|
|
7
|
+
// the bytes, and encrypt them in the content store). Because it is a separate axis, it has its
|
|
8
|
+
// own registry rather than a fourth DataClass.
|
|
9
|
+
//
|
|
10
|
+
// A non-invasive WeakMap keeps the marker from mutating the Zod schema (same instance returned,
|
|
11
|
+
// validation untouched), exactly like the classification registry.
|
|
12
|
+
const externalRegistry = new WeakMap();
|
|
13
|
+
/**
|
|
14
|
+
* Mark a field `externalized` — its value is moved to a content store on append and the log
|
|
15
|
+
* keeps a content-addressed reference (ADR 0046). Returns the SAME schema (validation
|
|
16
|
+
* unchanged). Marking an object externalizes its whole subtree as one blob.
|
|
17
|
+
*/
|
|
18
|
+
export function externalized(schema) {
|
|
19
|
+
externalRegistry.set(schema, true);
|
|
20
|
+
return schema;
|
|
21
|
+
}
|
|
22
|
+
/** Whether a schema was marked {@link externalized} (through transparent wrappers is handled by the walker). */
|
|
23
|
+
export function isExternalized(schema) {
|
|
24
|
+
return typeof schema === "object" && schema !== null && externalRegistry.get(schema) === true;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The dot-paths of every `externalized()`-marked field in a schema (declaration order,
|
|
28
|
+
* deterministic). A marked object is reported at its own path as a unit.
|
|
29
|
+
*/
|
|
30
|
+
export function externalizedPaths(schema) {
|
|
31
|
+
return collectMarkedPaths(schema, (node) => externalRegistry.get(node) === true ? true : undefined).map((m) => m.path);
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=external.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external.js","sourceRoot":"","sources":["../../src/external.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhD,iFAAiF;AACjF,EAAE;AACF,6FAA6F;AAC7F,8FAA8F;AAC9F,6FAA6F;AAC7F,+FAA+F;AAC/F,+CAA+C;AAC/C,EAAE;AACF,gGAAgG;AAChG,mEAAmE;AAEnE,MAAM,gBAAgB,GAAG,IAAI,OAAO,EAAgB,CAAC;AAErD;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAyB,MAAS;IAC5D,gBAAgB,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACnC,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,gHAAgH;AAChH,MAAM,UAAU,cAAc,CAAC,MAAe;IAC5C,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC;AAChG,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAoB;IACpD,OAAO,kBAAkB,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CACzC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CACvD,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACvB,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { classify, classOf, collectClassifiedPaths, collectMarkedPaths, internal, pathsOfClass, secret, sensitive, strictest, } from "./classify";
|
|
2
|
+
export { externalized, externalizedPaths, isExternalized } from "./external";
|
|
3
|
+
export { TAINT_SCAN_EXCEEDED_ORIGIN, TAINT_SCAN_MAX_DEPTH, TAINT_SCAN_MAX_NODES, isTainted, nestedTaintOrigin, taintOriginLabel, TAINT_ORIGIN_LABEL_MAX, scanTaint, tainted, taintOf, unwrapTainted, } from "./taint";
|
|
4
|
+
export { REDACTED, maskPaths } from "./redact";
|
|
5
|
+
export { expandPaths, hasWildcard } from "./expand";
|
|
6
|
+
export { collectUnreachableMarks } from "./unreachable";
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,QAAQ,EACR,OAAO,EACP,sBAAsB,EACtB,kBAAkB,EAClB,QAAQ,EACR,YAAY,EACZ,MAAM,EACN,SAAS,EACT,SAAS,GACV,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7E,OAAO,EACL,0BAA0B,EAC1B,oBAAoB,EACpB,oBAAoB,EACpB,SAAS,EACT,iBAAiB,EACjB,gBAAgB,EAChB,sBAAsB,EACtB,SAAS,EACT,OAAO,EACP,OAAO,EACP,aAAa,GACd,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { expandPaths, hasWildcard } from "./expand";
|
|
2
|
+
// ── K10-R6 — the shared egress-masking primitive ──────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// One structural path-masker, used by every egress surface (telemetry adapters, agent-surface
|
|
5
|
+
// surfaces, the CLI). It replaces the value at each dot-path with a mask, copying only the
|
|
6
|
+
// spine it touches (structural sharing elsewhere) and never mutating the caller's object.
|
|
7
|
+
// Absent paths are skipped, so masking a payload that doesn't carry a marked field is a
|
|
8
|
+
// no-op on that field. Lives here in `@loomwork/classify` — the neutral home the classification
|
|
9
|
+
// consumers already depend on — so no surface package depends on another.
|
|
10
|
+
/** The value an egress-masked field is replaced by (structure kept, value gone). */
|
|
11
|
+
export const REDACTED = "[redacted]";
|
|
12
|
+
/**
|
|
13
|
+
* Return a copy of `root` with each dot-path's value replaced by `mask`. A dot-path always
|
|
14
|
+
* roots at an object (an array/primitive root has nothing to address), so those are returned
|
|
15
|
+
* as-is. Paths are applied independently; a shared prefix is copied once.
|
|
16
|
+
*/
|
|
17
|
+
/** Copy one spine node, preserving arrays. */
|
|
18
|
+
function shallowCopy(value) {
|
|
19
|
+
return Array.isArray(value)
|
|
20
|
+
? [...value]
|
|
21
|
+
: { ...value };
|
|
22
|
+
}
|
|
23
|
+
export function maskPaths(root, paths, mask = REDACTED) {
|
|
24
|
+
if (typeof root !== "object" || root === null || Array.isArray(root))
|
|
25
|
+
return root;
|
|
26
|
+
const out = { ...root };
|
|
27
|
+
// `[]` segments are resolved against the concrete value first (#260); a path without one
|
|
28
|
+
// passes through untouched, so pre-existing behaviour is unchanged.
|
|
29
|
+
for (const path of hasWildcard(paths) ? expandPaths(root, paths) : paths) {
|
|
30
|
+
const segs = path.split(".");
|
|
31
|
+
let src = root;
|
|
32
|
+
let dst = out;
|
|
33
|
+
let reachable = true;
|
|
34
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
35
|
+
const seg = segs[i];
|
|
36
|
+
const child = src[seg];
|
|
37
|
+
if (typeof child !== "object" || child === null) {
|
|
38
|
+
reachable = false;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
// Copy this spine node once (first path to reach it); later paths sharing the prefix
|
|
42
|
+
// descend into the already-copied node.
|
|
43
|
+
// Copy arrays AS arrays. `{ ...array }` would yield an object with numeric keys —
|
|
44
|
+
// harmless while paths never crossed arrays, corrupting now that they do.
|
|
45
|
+
if (dst[seg] === child)
|
|
46
|
+
dst[seg] = shallowCopy(child);
|
|
47
|
+
src = child;
|
|
48
|
+
dst = dst[seg];
|
|
49
|
+
}
|
|
50
|
+
const leaf = segs[segs.length - 1];
|
|
51
|
+
if (reachable && Object.hasOwn(src, leaf))
|
|
52
|
+
dst[leaf] = mask;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sourceRoot":"","sources":["../../src/redact.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACpD,iFAAiF;AACjF,EAAE;AACF,8FAA8F;AAC9F,2FAA2F;AAC3F,0FAA0F;AAC1F,wFAAwF;AACxF,gGAAgG;AAChG,0EAA0E;AAE1E,oFAAoF;AACpF,MAAM,CAAC,MAAM,QAAQ,GAAG,YAAqB,CAAC;AAE9C;;;;GAIG;AACH,8CAA8C;AAC9C,SAAS,WAAW,CAAC,KAAa;IAChC,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACzB,CAAC,CAAE,CAAC,GAAG,KAAK,CAAwC;QACpD,CAAC,CAAC,EAAE,GAAI,KAAiC,EAAE,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,SAAS,CACvB,IAAa,EACb,KAAwB,EACxB,OAAgB,QAAQ;IAExB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAClF,MAAM,GAAG,GAA4B,EAAE,GAAI,IAAgC,EAAE,CAAC;IAC9E,yFAAyF;IACzF,oEAAoE;IACpE,KAAK,MAAM,IAAI,IAAI,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;QACzE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,GAAG,GAAG,IAA+B,CAAC;QAC1C,IAAI,GAAG,GAAG,GAAG,CAAC;QACd,IAAI,SAAS,GAAG,IAAI,CAAC;QACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YACzC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAW,CAAC;YAC9B,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;YACvB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBAChD,SAAS,GAAG,KAAK,CAAC;gBAClB,MAAM;YACR,CAAC;YACD,qFAAqF;YACrF,wCAAwC;YACxC,kFAAkF;YAClF,0EAA0E;YAC1E,IAAI,GAAG,CAAC,GAAG,CAAC,KAAK,KAAK;gBAAE,GAAG,CAAC,GAAG,CAAC,GAAG,WAAW,CAAC,KAAe,CAAC,CAAC;YAChE,GAAG,GAAG,KAAgC,CAAC;YACvC,GAAG,GAAG,GAAG,CAAC,GAAG,CAA4B,CAAC;QAC5C,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAW,CAAC;QAC7C,IAAI,SAAS,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9D,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// ── The integrity axis — provenance for content the system did not author ────
|
|
2
|
+
//
|
|
3
|
+
// The existing markers are a CONFIDENTIALITY axis (`secret`/`sensitive`/
|
|
4
|
+
// `internal` — what may LEAVE) plus a where-stored axis (`externalized`). This
|
|
5
|
+
// is the third and opposite direction: what may ENTER an instruction position.
|
|
6
|
+
// A third-party MCP tool result, an A2A peer's response, a fetched document —
|
|
7
|
+
// all arrive as ordinary strings, and an ordinary string interpolated into a
|
|
8
|
+
// system prompt is indistinguishable from an instruction the operator wrote.
|
|
9
|
+
//
|
|
10
|
+
// WHY A VALUE WRAPPER AND NOT A SCHEMA MARKER. Confidentiality is a property of
|
|
11
|
+
// a FIELD: `ssn` is sensitive in every execution, forever, so declaring it once
|
|
12
|
+
// at the schema is exactly right. Integrity is a property of a VALUE: the same
|
|
13
|
+
// `summary: string` field is trustworthy when this flow computed it and
|
|
14
|
+
// untrusted when a third-party server returned it, and which one it is cannot be
|
|
15
|
+
// known until runtime. A schema marker would have to guess, and a marker that
|
|
16
|
+
// guesses is worse than none — it reads as coverage. So taint is attached where
|
|
17
|
+
// it is acquired, to the value, by the code that acquired it.
|
|
18
|
+
//
|
|
19
|
+
// The wrapper is plain JSON (`{ __loomTainted, value }`), the same structural
|
|
20
|
+
// shape K10.3's `secretRef` uses, so it survives the log verbatim and a replay
|
|
21
|
+
// hands the effect back a value that is still tainted. The corollary is the
|
|
22
|
+
// honest limit on retrofitting: taint applies to content ingested AFTER a
|
|
23
|
+
// producer starts applying it. An existing log replays exactly as it did, with
|
|
24
|
+
// no taint, because the fact was never recorded — the same argument ADR 0063 D3
|
|
25
|
+
// made about principals.
|
|
26
|
+
/**
|
|
27
|
+
* Wrap a value as untrusted, recording where it came from.
|
|
28
|
+
*
|
|
29
|
+
* Apply this at INGRESS — the moment content crosses into the system from a
|
|
30
|
+
* party that is not the operator. Tainting later is guesswork, and tainting
|
|
31
|
+
* never is how third-party text ends up in a system prompt.
|
|
32
|
+
*
|
|
33
|
+
* Re-tainting an already-tainted value keeps the ORIGINAL origin: provenance
|
|
34
|
+
* records where content entered, and a re-wrap would let a later, more
|
|
35
|
+
* flattering label overwrite the true one.
|
|
36
|
+
*/
|
|
37
|
+
export function tainted(value, origin) {
|
|
38
|
+
if (isTainted(value))
|
|
39
|
+
return value;
|
|
40
|
+
return { __loomTainted: { origin }, value };
|
|
41
|
+
}
|
|
42
|
+
/** Whether a value carries taint. Structural, so it survives a JSON round-trip. */
|
|
43
|
+
export function isTainted(value) {
|
|
44
|
+
if (typeof value !== "object" || value === null)
|
|
45
|
+
return false;
|
|
46
|
+
const mark = value.__loomTainted;
|
|
47
|
+
return (typeof mark === "object" &&
|
|
48
|
+
mark !== null &&
|
|
49
|
+
typeof mark.origin === "string" &&
|
|
50
|
+
"value" in value);
|
|
51
|
+
}
|
|
52
|
+
/** The provenance on a value, or `undefined` when it carries none. */
|
|
53
|
+
export function taintOf(value) {
|
|
54
|
+
return isTainted(value) ? value.__loomTainted : undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The inner value, discarding provenance. Non-tainted values pass through.
|
|
58
|
+
*
|
|
59
|
+
* Deliberately loud. Unwrapping is legitimate and often necessary — parsing a
|
|
60
|
+
* tainted JSON payload, indexing a tainted record — but every call is a place
|
|
61
|
+
* where provenance stops travelling. Unwrap to COMPUTE with content; never
|
|
62
|
+
* unwrap on the way to a prompt, which is the one path the taint exists to
|
|
63
|
+
* govern. Nothing can stop you; the point is that the call site is visible.
|
|
64
|
+
*/
|
|
65
|
+
export function unwrapTainted(value) {
|
|
66
|
+
return isTainted(value) ? value.value : value;
|
|
67
|
+
}
|
|
68
|
+
/** Default nesting bound of {@link scanTaint}. */
|
|
69
|
+
export const TAINT_SCAN_MAX_DEPTH = 64;
|
|
70
|
+
/** Default node bound of {@link scanTaint}. */
|
|
71
|
+
export const TAINT_SCAN_MAX_NODES = 100_000;
|
|
72
|
+
/** The origin reported when a scan hit its bound and could not prove the value clean. */
|
|
73
|
+
export const TAINT_SCAN_EXCEEDED_ORIGIN = "unknown:taint-scan-bound-exceeded";
|
|
74
|
+
/**
|
|
75
|
+
* Walk `value` for taint wrappers at ANY depth. Iterative (no recursion, so a 100k-deep JSON cannot
|
|
76
|
+
* blow the stack), cycle-safe, and bounded by depth and node count. FAILS CLOSED: when a bound is hit
|
|
77
|
+
* the result is `exceeded: true` and `tainted: true`, because "too big to look at" is not "clean".
|
|
78
|
+
* Binary views are skipped (they cannot hold a wrapper). Pure: the value is never modified.
|
|
79
|
+
*/
|
|
80
|
+
export function scanTaint(value, options = {}) {
|
|
81
|
+
const maxDepth = options.maxDepth ?? TAINT_SCAN_MAX_DEPTH;
|
|
82
|
+
const maxNodes = options.maxNodes ?? TAINT_SCAN_MAX_NODES;
|
|
83
|
+
const origins = [];
|
|
84
|
+
const found = new Set();
|
|
85
|
+
let exceeded = false;
|
|
86
|
+
let nodes = 0;
|
|
87
|
+
const seen = new WeakSet();
|
|
88
|
+
const stack = [{ v: value, d: 0 }];
|
|
89
|
+
while (stack.length > 0) {
|
|
90
|
+
const { v, d } = stack.pop();
|
|
91
|
+
if (typeof v !== "object" || v === null || seen.has(v) || ArrayBuffer.isView(v))
|
|
92
|
+
continue;
|
|
93
|
+
seen.add(v);
|
|
94
|
+
if (++nodes > maxNodes || d > maxDepth) {
|
|
95
|
+
exceeded = true;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
if (isTainted(v)) {
|
|
99
|
+
const origin = v.__loomTainted.origin;
|
|
100
|
+
if (!found.has(origin)) {
|
|
101
|
+
found.add(origin);
|
|
102
|
+
origins.push(origin);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const child of Array.isArray(v) ? v : Object.values(v)) {
|
|
106
|
+
if (typeof child === "object" && child !== null)
|
|
107
|
+
stack.push({ v: child, d: d + 1 });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { origins, exceeded, tainted: exceeded || origins.length > 0 };
|
|
111
|
+
}
|
|
112
|
+
/** Most distinct origins named in a joined label; the rest are counted (`+N more`). */
|
|
113
|
+
export const TAINT_ORIGIN_LABEL_MAX = 8;
|
|
114
|
+
/** Longest single origin kept in a joined label (a forged wrapper can carry a megabyte origin). */
|
|
115
|
+
const TAINT_ORIGIN_MAX_CHARS = 200;
|
|
116
|
+
/**
|
|
117
|
+
* The label for a {@link TaintScan}, or `undefined` when it is clean. The first
|
|
118
|
+
* {@link TAINT_ORIGIN_LABEL_MAX} distinct origins are joined (`a, b`) and the rest counted
|
|
119
|
+
* (`+N more`), so a value stuffed with forged wrappers cannot make a megabyte label that is then
|
|
120
|
+
* journaled and fenced. A bound hit adds {@link TAINT_SCAN_EXCEEDED_ORIGIN}.
|
|
121
|
+
*/
|
|
122
|
+
export function taintOriginLabel(scan) {
|
|
123
|
+
if (!scan.tainted)
|
|
124
|
+
return undefined;
|
|
125
|
+
const shown = scan.origins
|
|
126
|
+
.slice(0, TAINT_ORIGIN_LABEL_MAX)
|
|
127
|
+
.map((o) => o.length > TAINT_ORIGIN_MAX_CHARS ? `${o.slice(0, TAINT_ORIGIN_MAX_CHARS)}...` : o);
|
|
128
|
+
const more = scan.origins.length - shown.length;
|
|
129
|
+
if (more > 0)
|
|
130
|
+
shown.push(`+${more} more`);
|
|
131
|
+
if (scan.exceeded)
|
|
132
|
+
shown.push(TAINT_SCAN_EXCEEDED_ORIGIN);
|
|
133
|
+
return shown.join(", ");
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The origin label for a value that may hold nested taint, or `undefined` when it is provably clean
|
|
137
|
+
* (see {@link taintOriginLabel}).
|
|
138
|
+
*/
|
|
139
|
+
export function nestedTaintOrigin(value, options) {
|
|
140
|
+
return taintOriginLabel(scanTaint(value, options));
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=taint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"taint.js","sourceRoot":"","sources":["../../src/taint.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { classOf } from "./classify";
|
|
2
|
+
/** Unwrap optional/nullable/default/effects, as the classification walker does. */
|
|
3
|
+
function chain(schema) {
|
|
4
|
+
const nodes = [];
|
|
5
|
+
let cur = schema;
|
|
6
|
+
for (let i = 0; i < 100; i++) {
|
|
7
|
+
nodes.push(cur);
|
|
8
|
+
const def = cur._def;
|
|
9
|
+
const typeName = def?.typeName;
|
|
10
|
+
let next;
|
|
11
|
+
if (typeName === "ZodOptional" || typeName === "ZodNullable" || typeName === "ZodDefault") {
|
|
12
|
+
next = def?.innerType;
|
|
13
|
+
}
|
|
14
|
+
else if (typeName === "ZodEffects") {
|
|
15
|
+
next = def?.schema;
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
if (next === undefined)
|
|
21
|
+
break;
|
|
22
|
+
cur = next;
|
|
23
|
+
}
|
|
24
|
+
return { nodes, inner: cur };
|
|
25
|
+
}
|
|
26
|
+
function typeNameOf(node) {
|
|
27
|
+
return node._def?.typeName;
|
|
28
|
+
}
|
|
29
|
+
function objectShape(node) {
|
|
30
|
+
const def = node._def;
|
|
31
|
+
if (def?.typeName !== "ZodObject" || typeof def.shape !== "function")
|
|
32
|
+
return undefined;
|
|
33
|
+
return def.shape();
|
|
34
|
+
}
|
|
35
|
+
function arrayElement(node) {
|
|
36
|
+
const def = node._def;
|
|
37
|
+
if (def?.typeName !== "ZodArray")
|
|
38
|
+
return undefined;
|
|
39
|
+
return def.type;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Schemas that hold inner schemas the classification walker never descends into. Returns the
|
|
43
|
+
* children to search for stranded marks, plus the label for the location string.
|
|
44
|
+
*
|
|
45
|
+
* `lazy` is deliberately absent: descending a recursive schema to hunt for marks risks
|
|
46
|
+
* non-termination, and the payoff is small.
|
|
47
|
+
*/
|
|
48
|
+
function opaqueChildren(node) {
|
|
49
|
+
const def = node._def;
|
|
50
|
+
const typeName = typeNameOf(node);
|
|
51
|
+
switch (typeName) {
|
|
52
|
+
case "ZodRecord":
|
|
53
|
+
case "ZodMap":
|
|
54
|
+
case "ZodSet":
|
|
55
|
+
return {
|
|
56
|
+
label: typeName.slice(3).toLowerCase(),
|
|
57
|
+
children: [def?.valueType].filter(Boolean),
|
|
58
|
+
};
|
|
59
|
+
case "ZodUnion":
|
|
60
|
+
case "ZodDiscriminatedUnion": {
|
|
61
|
+
const options = def?.options;
|
|
62
|
+
const list = Array.isArray(options)
|
|
63
|
+
? options
|
|
64
|
+
: [...(options?.values?.() ?? [])];
|
|
65
|
+
return { label: "union", children: list };
|
|
66
|
+
}
|
|
67
|
+
case "ZodTuple":
|
|
68
|
+
return { label: "tuple", children: def?.items ?? [] };
|
|
69
|
+
case "ZodIntersection":
|
|
70
|
+
return {
|
|
71
|
+
label: "intersection",
|
|
72
|
+
children: [def?.left, def?.right].filter(Boolean),
|
|
73
|
+
};
|
|
74
|
+
default:
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function effectiveClass(schema) {
|
|
79
|
+
for (const node of chain(schema).nodes) {
|
|
80
|
+
const mark = classOf(node);
|
|
81
|
+
if (mark !== undefined)
|
|
82
|
+
return mark;
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Every classification mark stranded under a container the dot-path sinks cannot address.
|
|
88
|
+
*
|
|
89
|
+
* Returns `[]` for marks on objects and arrays — both are traversable, so those marks work.
|
|
90
|
+
*/
|
|
91
|
+
export function collectUnreachableMarks(schema, prefix = "") {
|
|
92
|
+
return walk(schema, prefix, undefined);
|
|
93
|
+
}
|
|
94
|
+
function walk(schema, path, blockedBy) {
|
|
95
|
+
const mark = effectiveClass(schema);
|
|
96
|
+
if (mark !== undefined && path !== "") {
|
|
97
|
+
// A mark terminates the walk exactly as it does in `collectMarkedPaths` — a marked object
|
|
98
|
+
// is a unit — so a mark nested inside a marked one is not double-reported.
|
|
99
|
+
return blockedBy === undefined ? [] : [{ path, dataClass: mark, container: blockedBy }];
|
|
100
|
+
}
|
|
101
|
+
const inner = chain(schema).inner;
|
|
102
|
+
const element = arrayElement(inner);
|
|
103
|
+
if (element !== undefined)
|
|
104
|
+
return walk(element, `${path}[]`, blockedBy);
|
|
105
|
+
const shape = objectShape(inner);
|
|
106
|
+
if (shape !== undefined) {
|
|
107
|
+
const out = [];
|
|
108
|
+
for (const [key, child] of Object.entries(shape)) {
|
|
109
|
+
out.push(...walk(child, path === "" ? key : `${path}.${key}`, blockedBy));
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
const opaque = opaqueChildren(inner);
|
|
114
|
+
if (opaque !== undefined) {
|
|
115
|
+
const out = [];
|
|
116
|
+
// Everything below here is unreachable — but keep the OUTERMOST blocker, since that is
|
|
117
|
+
// the one the developer has to restructure.
|
|
118
|
+
const blocker = blockedBy ?? opaque.label;
|
|
119
|
+
for (const child of opaque.children) {
|
|
120
|
+
out.push(...walk(child, `${path}.<${opaque.label}>`, blocker));
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=unreachable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unreachable.js","sourceRoot":"","sources":["../../src/unreachable.ts"],"names":[],"mappings":"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"}
|
package/dist/expand.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expand `paths` against `root`, replacing every `[]` with the concrete indices present in
|
|
3
|
+
* the value.
|
|
4
|
+
*
|
|
5
|
+
* - `"a.b"` (no wildcard) → `["a.b"]`, unchanged and unchecked; the appliers already skip
|
|
6
|
+
* absent paths, and pre-existing behaviour must not shift.
|
|
7
|
+
* - `"people[].ssn"` over two people → `["people.0.ssn", "people.1.ssn"]`.
|
|
8
|
+
* - `"tags[]"` (a marked array ELEMENT type) → one path per element.
|
|
9
|
+
* - A wildcard that does not resolve — absent, or `[]` over a non-array — expands to
|
|
10
|
+
* nothing, so nothing is masked/encrypted for it. Resolving against the value is the whole
|
|
11
|
+
* point of expanding late.
|
|
12
|
+
*/
|
|
13
|
+
export declare function expandPaths(root: unknown, paths: readonly string[]): string[];
|
|
14
|
+
/** True if any path uses a wildcard — lets callers skip expansion entirely (G2 fast path). */
|
|
15
|
+
export declare function hasWildcard(paths: readonly string[]): boolean;
|
|
16
|
+
//# sourceMappingURL=expand.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expand.d.ts","sourceRoot":"","sources":["../src/expand.ts"],"names":[],"mappings":"AA2BA;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAU7E;AAED,8FAA8F;AAC9F,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAG7D"}
|
package/dist/expand.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ── Wildcard path expansion (#260 option 2) ───────────────────────────────────
|
|
3
|
+
//
|
|
4
|
+
// The dot-path sinks — telemetry masking, field-level encryption, content externalization —
|
|
5
|
+
// address scalar paths and originally could not descend into array elements, so a mark
|
|
6
|
+
// placed inside one did nothing at all. `[]` is the array-crossing segment that fixes it:
|
|
7
|
+
// `people[].ssn` means "the `ssn` of every element of `people`".
|
|
8
|
+
//
|
|
9
|
+
// Rather than teach three near-identical spine-walkers about arrays, expansion happens ONCE
|
|
10
|
+
// against the concrete value and yields ordinary indexed paths (`people.0.ssn`,
|
|
11
|
+
// `people.1.ssn`) that every existing applier already handles — `array["0"]` is a normal
|
|
12
|
+
// property access, and each applier's spine copy already preserves arrays. So the walkers
|
|
13
|
+
// stay as they were, and there is one place where array semantics live.
|
|
14
|
+
//
|
|
15
|
+
// A path with no `[]` passes through untouched, so every pre-existing path behaves exactly
|
|
16
|
+
// as before (G2).
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.expandPaths = expandPaths;
|
|
19
|
+
exports.hasWildcard = hasWildcard;
|
|
20
|
+
/** Split a segment into its key and how many array crossings follow it (`rows[][]` ⇒ 2). */
|
|
21
|
+
function parseSegment(segment) {
|
|
22
|
+
let depth = 0;
|
|
23
|
+
let key = segment;
|
|
24
|
+
while (key.endsWith("[]")) {
|
|
25
|
+
key = key.slice(0, -2);
|
|
26
|
+
depth++;
|
|
27
|
+
}
|
|
28
|
+
return { key, depth };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Expand `paths` against `root`, replacing every `[]` with the concrete indices present in
|
|
32
|
+
* the value.
|
|
33
|
+
*
|
|
34
|
+
* - `"a.b"` (no wildcard) → `["a.b"]`, unchanged and unchecked; the appliers already skip
|
|
35
|
+
* absent paths, and pre-existing behaviour must not shift.
|
|
36
|
+
* - `"people[].ssn"` over two people → `["people.0.ssn", "people.1.ssn"]`.
|
|
37
|
+
* - `"tags[]"` (a marked array ELEMENT type) → one path per element.
|
|
38
|
+
* - A wildcard that does not resolve — absent, or `[]` over a non-array — expands to
|
|
39
|
+
* nothing, so nothing is masked/encrypted for it. Resolving against the value is the whole
|
|
40
|
+
* point of expanding late.
|
|
41
|
+
*/
|
|
42
|
+
function expandPaths(root, paths) {
|
|
43
|
+
const out = [];
|
|
44
|
+
for (const path of paths) {
|
|
45
|
+
if (!path.includes("[]")) {
|
|
46
|
+
out.push(path);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
expandInto(root, path.split("."), 0, [], out);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/** True if any path uses a wildcard — lets callers skip expansion entirely (G2 fast path). */
|
|
54
|
+
function hasWildcard(paths) {
|
|
55
|
+
for (const p of paths)
|
|
56
|
+
if (p.includes("[]"))
|
|
57
|
+
return true;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
function expandInto(value, segments, index, acc, out) {
|
|
61
|
+
if (index >= segments.length) {
|
|
62
|
+
out.push(acc.join("."));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (typeof value !== "object" || value === null)
|
|
66
|
+
return;
|
|
67
|
+
const { key, depth } = parseSegment(segments[index]);
|
|
68
|
+
if (!Object.hasOwn(value, key))
|
|
69
|
+
return;
|
|
70
|
+
const child = value[key];
|
|
71
|
+
descend(child, depth, [...acc, key], segments, index, out);
|
|
72
|
+
}
|
|
73
|
+
/** Walk `depth` array levels, emitting an index per level, then continue with the next segment. */
|
|
74
|
+
function descend(value, depth, acc, segments, index, out) {
|
|
75
|
+
if (depth === 0) {
|
|
76
|
+
// Reached the value this segment names. If it is the last segment it IS the target;
|
|
77
|
+
// otherwise keep walking.
|
|
78
|
+
if (index === segments.length - 1)
|
|
79
|
+
out.push(acc.join("."));
|
|
80
|
+
else
|
|
81
|
+
expandInto(value, segments, index + 1, acc, out);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// A `[]` over a non-array resolves to nothing — the mark does not apply to this value.
|
|
85
|
+
if (!Array.isArray(value))
|
|
86
|
+
return;
|
|
87
|
+
for (const [i, element] of value.entries()) {
|
|
88
|
+
descend(element, depth - 1, [...acc, String(i)], segments, index, out);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=expand.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expand.js","sourceRoot":"","sources":["../src/expand.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Mark a field `externalized` — its value is moved to a content store on append and the log
|
|
4
|
+
* keeps a content-addressed reference (ADR 0046). Returns the SAME schema (validation
|
|
5
|
+
* unchanged). Marking an object externalizes its whole subtree as one blob.
|
|
6
|
+
*/
|
|
7
|
+
export declare function externalized<T extends z.ZodTypeAny>(schema: T): T;
|
|
8
|
+
/** Whether a schema was marked {@link externalized} (through transparent wrappers is handled by the walker). */
|
|
9
|
+
export declare function isExternalized(schema: unknown): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The dot-paths of every `externalized()`-marked field in a schema (declaration order,
|
|
12
|
+
* deterministic). A marked object is reported at its own path as a unit.
|
|
13
|
+
*/
|
|
14
|
+
export declare function externalizedPaths(schema: z.ZodTypeAny): string[];
|
|
15
|
+
//# sourceMappingURL=external.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external.d.ts","sourceRoot":"","sources":["../src/external.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAgB7B;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAGjE;AAED,gHAAgH;AAChH,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO,CAEvD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,GAAG,MAAM,EAAE,CAIhE"}
|