@loomwork/classify 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +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
package/dist/external.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.externalized = externalized;
|
|
4
|
+
exports.isExternalized = isExternalized;
|
|
5
|
+
exports.externalizedPaths = externalizedPaths;
|
|
6
|
+
const classify_1 = require("./classify");
|
|
7
|
+
// ── ADR 0046 — the `externalized()` marker ────────────────────────────────────
|
|
8
|
+
//
|
|
9
|
+
// Orthogonal to the secret/sensitive/internal taxonomy: that axis says HOW sensitive a field
|
|
10
|
+
// is; this one says WHERE its value is stored — externalized to a content store, with the log
|
|
11
|
+
// keeping only a reference. A field can be both (a large AND sensitive document: externalize
|
|
12
|
+
// the bytes, and encrypt them in the content store). Because it is a separate axis, it has its
|
|
13
|
+
// own registry rather than a fourth DataClass.
|
|
14
|
+
//
|
|
15
|
+
// A non-invasive WeakMap keeps the marker from mutating the Zod schema (same instance returned,
|
|
16
|
+
// validation untouched), exactly like the classification registry.
|
|
17
|
+
const externalRegistry = new WeakMap();
|
|
18
|
+
/**
|
|
19
|
+
* Mark a field `externalized` — its value is moved to a content store on append and the log
|
|
20
|
+
* keeps a content-addressed reference (ADR 0046). Returns the SAME schema (validation
|
|
21
|
+
* unchanged). Marking an object externalizes its whole subtree as one blob.
|
|
22
|
+
*/
|
|
23
|
+
function externalized(schema) {
|
|
24
|
+
externalRegistry.set(schema, true);
|
|
25
|
+
return schema;
|
|
26
|
+
}
|
|
27
|
+
/** Whether a schema was marked {@link externalized} (through transparent wrappers is handled by the walker). */
|
|
28
|
+
function isExternalized(schema) {
|
|
29
|
+
return typeof schema === "object" && schema !== null && externalRegistry.get(schema) === true;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The dot-paths of every `externalized()`-marked field in a schema (declaration order,
|
|
33
|
+
* deterministic). A marked object is reported at its own path as a unit.
|
|
34
|
+
*/
|
|
35
|
+
function externalizedPaths(schema) {
|
|
36
|
+
return (0, classify_1.collectMarkedPaths)(schema, (node) => externalRegistry.get(node) === true ? true : undefined).map((m) => m.path);
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=external.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external.js","sourceRoot":"","sources":["../src/external.ts"],"names":[],"mappings":";;AAqBA,oCAGC;AAGD,wCAEC;AAMD,8CAIC;AAtCD,yCAAgD;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,SAAgB,YAAY,CAAyB,MAAS;IAC5D,gBAAgB,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACnC,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,gHAAgH;AAChH,SAAgB,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,SAAgB,iBAAiB,CAAC,MAAoB;IACpD,OAAO,IAAA,6BAAkB,EAAC,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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { classify, classOf, collectClassifiedPaths, collectMarkedPaths, internal, pathsOfClass, secret, sensitive, strictest, } from "./classify";
|
|
2
|
+
export type { ClassifiedPath, DataClass } from "./classify";
|
|
3
|
+
export { externalized, externalizedPaths, isExternalized } from "./external";
|
|
4
|
+
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";
|
|
5
|
+
export type { Taint, TaintScan, TaintScanOptions, TaintedValue } from "./taint";
|
|
6
|
+
export { REDACTED, maskPaths } from "./redact";
|
|
7
|
+
export { expandPaths, hasWildcard } from "./expand";
|
|
8
|
+
export { collectUnreachableMarks } from "./unreachable";
|
|
9
|
+
export type { UnreachableMark } from "./unreachable";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","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;AACpB,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5D,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;AACjB,YAAY,EAAE,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAChF,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;AACxD,YAAY,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.collectUnreachableMarks = exports.hasWildcard = exports.expandPaths = exports.maskPaths = exports.REDACTED = exports.unwrapTainted = exports.taintOf = exports.tainted = exports.scanTaint = exports.TAINT_ORIGIN_LABEL_MAX = exports.taintOriginLabel = exports.nestedTaintOrigin = exports.isTainted = exports.TAINT_SCAN_MAX_NODES = exports.TAINT_SCAN_MAX_DEPTH = exports.TAINT_SCAN_EXCEEDED_ORIGIN = exports.isExternalized = exports.externalizedPaths = exports.externalized = exports.strictest = exports.sensitive = exports.secret = exports.pathsOfClass = exports.internal = exports.collectMarkedPaths = exports.collectClassifiedPaths = exports.classOf = exports.classify = void 0;
|
|
4
|
+
var classify_1 = require("./classify");
|
|
5
|
+
Object.defineProperty(exports, "classify", { enumerable: true, get: function () { return classify_1.classify; } });
|
|
6
|
+
Object.defineProperty(exports, "classOf", { enumerable: true, get: function () { return classify_1.classOf; } });
|
|
7
|
+
Object.defineProperty(exports, "collectClassifiedPaths", { enumerable: true, get: function () { return classify_1.collectClassifiedPaths; } });
|
|
8
|
+
Object.defineProperty(exports, "collectMarkedPaths", { enumerable: true, get: function () { return classify_1.collectMarkedPaths; } });
|
|
9
|
+
Object.defineProperty(exports, "internal", { enumerable: true, get: function () { return classify_1.internal; } });
|
|
10
|
+
Object.defineProperty(exports, "pathsOfClass", { enumerable: true, get: function () { return classify_1.pathsOfClass; } });
|
|
11
|
+
Object.defineProperty(exports, "secret", { enumerable: true, get: function () { return classify_1.secret; } });
|
|
12
|
+
Object.defineProperty(exports, "sensitive", { enumerable: true, get: function () { return classify_1.sensitive; } });
|
|
13
|
+
Object.defineProperty(exports, "strictest", { enumerable: true, get: function () { return classify_1.strictest; } });
|
|
14
|
+
var external_1 = require("./external");
|
|
15
|
+
Object.defineProperty(exports, "externalized", { enumerable: true, get: function () { return external_1.externalized; } });
|
|
16
|
+
Object.defineProperty(exports, "externalizedPaths", { enumerable: true, get: function () { return external_1.externalizedPaths; } });
|
|
17
|
+
Object.defineProperty(exports, "isExternalized", { enumerable: true, get: function () { return external_1.isExternalized; } });
|
|
18
|
+
var taint_1 = require("./taint");
|
|
19
|
+
Object.defineProperty(exports, "TAINT_SCAN_EXCEEDED_ORIGIN", { enumerable: true, get: function () { return taint_1.TAINT_SCAN_EXCEEDED_ORIGIN; } });
|
|
20
|
+
Object.defineProperty(exports, "TAINT_SCAN_MAX_DEPTH", { enumerable: true, get: function () { return taint_1.TAINT_SCAN_MAX_DEPTH; } });
|
|
21
|
+
Object.defineProperty(exports, "TAINT_SCAN_MAX_NODES", { enumerable: true, get: function () { return taint_1.TAINT_SCAN_MAX_NODES; } });
|
|
22
|
+
Object.defineProperty(exports, "isTainted", { enumerable: true, get: function () { return taint_1.isTainted; } });
|
|
23
|
+
Object.defineProperty(exports, "nestedTaintOrigin", { enumerable: true, get: function () { return taint_1.nestedTaintOrigin; } });
|
|
24
|
+
Object.defineProperty(exports, "taintOriginLabel", { enumerable: true, get: function () { return taint_1.taintOriginLabel; } });
|
|
25
|
+
Object.defineProperty(exports, "TAINT_ORIGIN_LABEL_MAX", { enumerable: true, get: function () { return taint_1.TAINT_ORIGIN_LABEL_MAX; } });
|
|
26
|
+
Object.defineProperty(exports, "scanTaint", { enumerable: true, get: function () { return taint_1.scanTaint; } });
|
|
27
|
+
Object.defineProperty(exports, "tainted", { enumerable: true, get: function () { return taint_1.tainted; } });
|
|
28
|
+
Object.defineProperty(exports, "taintOf", { enumerable: true, get: function () { return taint_1.taintOf; } });
|
|
29
|
+
Object.defineProperty(exports, "unwrapTainted", { enumerable: true, get: function () { return taint_1.unwrapTainted; } });
|
|
30
|
+
var redact_1 = require("./redact");
|
|
31
|
+
Object.defineProperty(exports, "REDACTED", { enumerable: true, get: function () { return redact_1.REDACTED; } });
|
|
32
|
+
Object.defineProperty(exports, "maskPaths", { enumerable: true, get: function () { return redact_1.maskPaths; } });
|
|
33
|
+
var expand_1 = require("./expand");
|
|
34
|
+
Object.defineProperty(exports, "expandPaths", { enumerable: true, get: function () { return expand_1.expandPaths; } });
|
|
35
|
+
Object.defineProperty(exports, "hasWildcard", { enumerable: true, get: function () { return expand_1.hasWildcard; } });
|
|
36
|
+
var unreachable_1 = require("./unreachable");
|
|
37
|
+
Object.defineProperty(exports, "collectUnreachableMarks", { enumerable: true, get: function () { return unreachable_1.collectUnreachableMarks; } });
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,uCAUoB;AATlB,oGAAA,QAAQ,OAAA;AACR,mGAAA,OAAO,OAAA;AACP,kHAAA,sBAAsB,OAAA;AACtB,8GAAA,kBAAkB,OAAA;AAClB,oGAAA,QAAQ,OAAA;AACR,wGAAA,YAAY,OAAA;AACZ,kGAAA,MAAM,OAAA;AACN,qGAAA,SAAS,OAAA;AACT,qGAAA,SAAS,OAAA;AAGX,uCAA6E;AAApE,wGAAA,YAAY,OAAA;AAAE,6GAAA,iBAAiB,OAAA;AAAE,0GAAA,cAAc,OAAA;AACxD,iCAYiB;AAXf,mHAAA,0BAA0B,OAAA;AAC1B,6GAAA,oBAAoB,OAAA;AACpB,6GAAA,oBAAoB,OAAA;AACpB,kGAAA,SAAS,OAAA;AACT,0GAAA,iBAAiB,OAAA;AACjB,yGAAA,gBAAgB,OAAA;AAChB,+GAAA,sBAAsB,OAAA;AACtB,kGAAA,SAAS,OAAA;AACT,gGAAA,OAAO,OAAA;AACP,gGAAA,OAAO,OAAA;AACP,sGAAA,aAAa,OAAA;AAGf,mCAA+C;AAAtC,kGAAA,QAAQ,OAAA;AAAE,mGAAA,SAAS,OAAA;AAC5B,mCAAoD;AAA3C,qGAAA,WAAW,OAAA;AAAE,qGAAA,WAAW,OAAA;AACjC,6CAAwD;AAA/C,sHAAA,uBAAuB,OAAA"}
|
package/dist/redact.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":"AAUA,oFAAoF;AACpF,eAAO,MAAM,QAAQ,EAAG,YAAqB,CAAC;AAc9C,wBAAgB,SAAS,CACvB,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,IAAI,GAAE,OAAkB,GACvB,OAAO,CA6BT"}
|
package/dist/redact.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REDACTED = void 0;
|
|
4
|
+
exports.maskPaths = maskPaths;
|
|
5
|
+
const expand_1 = require("./expand");
|
|
6
|
+
// ── K10-R6 — the shared egress-masking primitive ──────────────────────────────
|
|
7
|
+
//
|
|
8
|
+
// One structural path-masker, used by every egress surface (telemetry adapters, agent-surface
|
|
9
|
+
// surfaces, the CLI). It replaces the value at each dot-path with a mask, copying only the
|
|
10
|
+
// spine it touches (structural sharing elsewhere) and never mutating the caller's object.
|
|
11
|
+
// Absent paths are skipped, so masking a payload that doesn't carry a marked field is a
|
|
12
|
+
// no-op on that field. Lives here in `@loomwork/classify` — the neutral home the classification
|
|
13
|
+
// consumers already depend on — so no surface package depends on another.
|
|
14
|
+
/** The value an egress-masked field is replaced by (structure kept, value gone). */
|
|
15
|
+
exports.REDACTED = "[redacted]";
|
|
16
|
+
/**
|
|
17
|
+
* Return a copy of `root` with each dot-path's value replaced by `mask`. A dot-path always
|
|
18
|
+
* roots at an object (an array/primitive root has nothing to address), so those are returned
|
|
19
|
+
* as-is. Paths are applied independently; a shared prefix is copied once.
|
|
20
|
+
*/
|
|
21
|
+
/** Copy one spine node, preserving arrays. */
|
|
22
|
+
function shallowCopy(value) {
|
|
23
|
+
return Array.isArray(value)
|
|
24
|
+
? [...value]
|
|
25
|
+
: { ...value };
|
|
26
|
+
}
|
|
27
|
+
function maskPaths(root, paths, mask = exports.REDACTED) {
|
|
28
|
+
if (typeof root !== "object" || root === null || Array.isArray(root))
|
|
29
|
+
return root;
|
|
30
|
+
const out = { ...root };
|
|
31
|
+
// `[]` segments are resolved against the concrete value first (#260); a path without one
|
|
32
|
+
// passes through untouched, so pre-existing behaviour is unchanged.
|
|
33
|
+
for (const path of (0, expand_1.hasWildcard)(paths) ? (0, expand_1.expandPaths)(root, paths) : paths) {
|
|
34
|
+
const segs = path.split(".");
|
|
35
|
+
let src = root;
|
|
36
|
+
let dst = out;
|
|
37
|
+
let reachable = true;
|
|
38
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
39
|
+
const seg = segs[i];
|
|
40
|
+
const child = src[seg];
|
|
41
|
+
if (typeof child !== "object" || child === null) {
|
|
42
|
+
reachable = false;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
// Copy this spine node once (first path to reach it); later paths sharing the prefix
|
|
46
|
+
// descend into the already-copied node.
|
|
47
|
+
// Copy arrays AS arrays. `{ ...array }` would yield an object with numeric keys —
|
|
48
|
+
// harmless while paths never crossed arrays, corrupting now that they do.
|
|
49
|
+
if (dst[seg] === child)
|
|
50
|
+
dst[seg] = shallowCopy(child);
|
|
51
|
+
src = child;
|
|
52
|
+
dst = dst[seg];
|
|
53
|
+
}
|
|
54
|
+
const leaf = segs[segs.length - 1];
|
|
55
|
+
if (reachable && Object.hasOwn(src, leaf))
|
|
56
|
+
dst[leaf] = mask;
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=redact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.js","sourceRoot":"","sources":["../src/redact.ts"],"names":[],"mappings":";;;AAyBA,8BAiCC;AA1DD,qCAAoD;AACpD,iFAAiF;AACjF,EAAE;AACF,8FAA8F;AAC9F,2FAA2F;AAC3F,0FAA0F;AAC1F,wFAAwF;AACxF,gGAAgG;AAChG,0EAA0E;AAE1E,oFAAoF;AACvE,QAAA,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,SAAgB,SAAS,CACvB,IAAa,EACb,KAAwB,EACxB,OAAgB,gBAAQ;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,IAAA,oBAAW,EAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAA,oBAAW,EAAC,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"}
|
package/dist/taint.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Where a tainted value came from — an origin label, e.g. `mcp:acme/chart`. */
|
|
2
|
+
export interface Taint {
|
|
3
|
+
/**
|
|
4
|
+
* Human-meaningful provenance, chosen by the producer. It is rendered into
|
|
5
|
+
* the fence around the content, so treat it as untrusted display text: it
|
|
6
|
+
* frequently contains a third-party-controlled fragment (a tool name, a peer
|
|
7
|
+
* id) and is escaped at the point of use, never trusted as markup.
|
|
8
|
+
*/
|
|
9
|
+
origin: string;
|
|
10
|
+
}
|
|
11
|
+
/** A value carrying its own provenance. Plain JSON — survives the log unchanged. */
|
|
12
|
+
export interface TaintedValue<T = unknown> {
|
|
13
|
+
__loomTainted: Taint;
|
|
14
|
+
value: T;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Wrap a value as untrusted, recording where it came from.
|
|
18
|
+
*
|
|
19
|
+
* Apply this at INGRESS — the moment content crosses into the system from a
|
|
20
|
+
* party that is not the operator. Tainting later is guesswork, and tainting
|
|
21
|
+
* never is how third-party text ends up in a system prompt.
|
|
22
|
+
*
|
|
23
|
+
* Re-tainting an already-tainted value keeps the ORIGINAL origin: provenance
|
|
24
|
+
* records where content entered, and a re-wrap would let a later, more
|
|
25
|
+
* flattering label overwrite the true one.
|
|
26
|
+
*/
|
|
27
|
+
export declare function tainted<T>(value: T, origin: string): TaintedValue<T>;
|
|
28
|
+
/** Whether a value carries taint. Structural, so it survives a JSON round-trip. */
|
|
29
|
+
export declare function isTainted(value: unknown): value is TaintedValue;
|
|
30
|
+
/** The provenance on a value, or `undefined` when it carries none. */
|
|
31
|
+
export declare function taintOf(value: unknown): Taint | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* The inner value, discarding provenance. Non-tainted values pass through.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately loud. Unwrapping is legitimate and often necessary — parsing a
|
|
36
|
+
* tainted JSON payload, indexing a tainted record — but every call is a place
|
|
37
|
+
* where provenance stops travelling. Unwrap to COMPUTE with content; never
|
|
38
|
+
* unwrap on the way to a prompt, which is the one path the taint exists to
|
|
39
|
+
* govern. Nothing can stop you; the point is that the call site is visible.
|
|
40
|
+
*/
|
|
41
|
+
export declare function unwrapTainted<T>(value: TaintedValue<T> | T): T;
|
|
42
|
+
/** Bounds of a {@link scanTaint} walk. */
|
|
43
|
+
export interface TaintScanOptions {
|
|
44
|
+
/** Deepest nesting visited (the root is depth 0). Default {@link TAINT_SCAN_MAX_DEPTH}. */
|
|
45
|
+
maxDepth?: number;
|
|
46
|
+
/** Most object/array nodes visited. Default {@link TAINT_SCAN_MAX_NODES}. */
|
|
47
|
+
maxNodes?: number;
|
|
48
|
+
}
|
|
49
|
+
/** Default nesting bound of {@link scanTaint}. */
|
|
50
|
+
export declare const TAINT_SCAN_MAX_DEPTH = 64;
|
|
51
|
+
/** Default node bound of {@link scanTaint}. */
|
|
52
|
+
export declare const TAINT_SCAN_MAX_NODES = 100000;
|
|
53
|
+
/** The origin reported when a scan hit its bound and could not prove the value clean. */
|
|
54
|
+
export declare const TAINT_SCAN_EXCEEDED_ORIGIN = "unknown:taint-scan-bound-exceeded";
|
|
55
|
+
/** What {@link scanTaint} found. */
|
|
56
|
+
export interface TaintScan {
|
|
57
|
+
/** Distinct origins of every wrapper found, in discovery order. */
|
|
58
|
+
readonly origins: readonly string[];
|
|
59
|
+
/** The walk hit a bound before it finished: the value is NOT proven clean. */
|
|
60
|
+
readonly exceeded: boolean;
|
|
61
|
+
/** `origins` is non-empty or `exceeded` - the value must be treated as untrusted. */
|
|
62
|
+
readonly tainted: boolean;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Walk `value` for taint wrappers at ANY depth. Iterative (no recursion, so a 100k-deep JSON cannot
|
|
66
|
+
* blow the stack), cycle-safe, and bounded by depth and node count. FAILS CLOSED: when a bound is hit
|
|
67
|
+
* the result is `exceeded: true` and `tainted: true`, because "too big to look at" is not "clean".
|
|
68
|
+
* Binary views are skipped (they cannot hold a wrapper). Pure: the value is never modified.
|
|
69
|
+
*/
|
|
70
|
+
export declare function scanTaint(value: unknown, options?: TaintScanOptions): TaintScan;
|
|
71
|
+
/** Most distinct origins named in a joined label; the rest are counted (`+N more`). */
|
|
72
|
+
export declare const TAINT_ORIGIN_LABEL_MAX = 8;
|
|
73
|
+
/**
|
|
74
|
+
* The label for a {@link TaintScan}, or `undefined` when it is clean. The first
|
|
75
|
+
* {@link TAINT_ORIGIN_LABEL_MAX} distinct origins are joined (`a, b`) and the rest counted
|
|
76
|
+
* (`+N more`), so a value stuffed with forged wrappers cannot make a megabyte label that is then
|
|
77
|
+
* journaled and fenced. A bound hit adds {@link TAINT_SCAN_EXCEEDED_ORIGIN}.
|
|
78
|
+
*/
|
|
79
|
+
export declare function taintOriginLabel(scan: TaintScan): string | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* The origin label for a value that may hold nested taint, or `undefined` when it is provably clean
|
|
82
|
+
* (see {@link taintOriginLabel}).
|
|
83
|
+
*/
|
|
84
|
+
export declare function nestedTaintOrigin(value: unknown, options?: TaintScanOptions): string | undefined;
|
|
85
|
+
//# sourceMappingURL=taint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"taint.d.ts","sourceRoot":"","sources":["../src/taint.ts"],"names":[],"mappings":"AA0BA,gFAAgF;AAChF,MAAM,WAAW,KAAK;IACpB;;;;;OAKG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,oFAAoF;AACpF,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO;IACvC,aAAa,EAAE,KAAK,CAAC;IACrB,KAAK,EAAE,CAAC,CAAC;CACV;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,YAAY,CAAC,CAAC,CAAC,CAGpE;AAED,mFAAmF;AACnF,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAS/D;AAED,sEAAsE;AACtE,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,GAAG,SAAS,CAEzD;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAE9D;AASD,0CAA0C;AAC1C,MAAM,WAAW,gBAAgB;IAC/B,2FAA2F;IAC3F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,kDAAkD;AAClD,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,+CAA+C;AAC/C,eAAO,MAAM,oBAAoB,SAAU,CAAC;AAC5C,yFAAyF;AACzF,eAAO,MAAM,0BAA0B,sCAAsC,CAAC;AAE9E,oCAAoC;AACpC,MAAM,WAAW,SAAS;IACxB,mEAAmE;IACnE,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,qFAAqF;IACrF,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS,CA6BnF;AAED,uFAAuF;AACvF,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAIxC;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAWpE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAEhG"}
|
package/dist/taint.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ── The integrity axis — provenance for content the system did not author ────
|
|
3
|
+
//
|
|
4
|
+
// The existing markers are a CONFIDENTIALITY axis (`secret`/`sensitive`/
|
|
5
|
+
// `internal` — what may LEAVE) plus a where-stored axis (`externalized`). This
|
|
6
|
+
// is the third and opposite direction: what may ENTER an instruction position.
|
|
7
|
+
// A third-party MCP tool result, an A2A peer's response, a fetched document —
|
|
8
|
+
// all arrive as ordinary strings, and an ordinary string interpolated into a
|
|
9
|
+
// system prompt is indistinguishable from an instruction the operator wrote.
|
|
10
|
+
//
|
|
11
|
+
// WHY A VALUE WRAPPER AND NOT A SCHEMA MARKER. Confidentiality is a property of
|
|
12
|
+
// a FIELD: `ssn` is sensitive in every execution, forever, so declaring it once
|
|
13
|
+
// at the schema is exactly right. Integrity is a property of a VALUE: the same
|
|
14
|
+
// `summary: string` field is trustworthy when this flow computed it and
|
|
15
|
+
// untrusted when a third-party server returned it, and which one it is cannot be
|
|
16
|
+
// known until runtime. A schema marker would have to guess, and a marker that
|
|
17
|
+
// guesses is worse than none — it reads as coverage. So taint is attached where
|
|
18
|
+
// it is acquired, to the value, by the code that acquired it.
|
|
19
|
+
//
|
|
20
|
+
// The wrapper is plain JSON (`{ __loomTainted, value }`), the same structural
|
|
21
|
+
// shape K10.3's `secretRef` uses, so it survives the log verbatim and a replay
|
|
22
|
+
// hands the effect back a value that is still tainted. The corollary is the
|
|
23
|
+
// honest limit on retrofitting: taint applies to content ingested AFTER a
|
|
24
|
+
// producer starts applying it. An existing log replays exactly as it did, with
|
|
25
|
+
// no taint, because the fact was never recorded — the same argument ADR 0063 D3
|
|
26
|
+
// made about principals.
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.TAINT_ORIGIN_LABEL_MAX = exports.TAINT_SCAN_EXCEEDED_ORIGIN = exports.TAINT_SCAN_MAX_NODES = exports.TAINT_SCAN_MAX_DEPTH = void 0;
|
|
29
|
+
exports.tainted = tainted;
|
|
30
|
+
exports.isTainted = isTainted;
|
|
31
|
+
exports.taintOf = taintOf;
|
|
32
|
+
exports.unwrapTainted = unwrapTainted;
|
|
33
|
+
exports.scanTaint = scanTaint;
|
|
34
|
+
exports.taintOriginLabel = taintOriginLabel;
|
|
35
|
+
exports.nestedTaintOrigin = nestedTaintOrigin;
|
|
36
|
+
/**
|
|
37
|
+
* Wrap a value as untrusted, recording where it came from.
|
|
38
|
+
*
|
|
39
|
+
* Apply this at INGRESS — the moment content crosses into the system from a
|
|
40
|
+
* party that is not the operator. Tainting later is guesswork, and tainting
|
|
41
|
+
* never is how third-party text ends up in a system prompt.
|
|
42
|
+
*
|
|
43
|
+
* Re-tainting an already-tainted value keeps the ORIGINAL origin: provenance
|
|
44
|
+
* records where content entered, and a re-wrap would let a later, more
|
|
45
|
+
* flattering label overwrite the true one.
|
|
46
|
+
*/
|
|
47
|
+
function tainted(value, origin) {
|
|
48
|
+
if (isTainted(value))
|
|
49
|
+
return value;
|
|
50
|
+
return { __loomTainted: { origin }, value };
|
|
51
|
+
}
|
|
52
|
+
/** Whether a value carries taint. Structural, so it survives a JSON round-trip. */
|
|
53
|
+
function isTainted(value) {
|
|
54
|
+
if (typeof value !== "object" || value === null)
|
|
55
|
+
return false;
|
|
56
|
+
const mark = value.__loomTainted;
|
|
57
|
+
return (typeof mark === "object" &&
|
|
58
|
+
mark !== null &&
|
|
59
|
+
typeof mark.origin === "string" &&
|
|
60
|
+
"value" in value);
|
|
61
|
+
}
|
|
62
|
+
/** The provenance on a value, or `undefined` when it carries none. */
|
|
63
|
+
function taintOf(value) {
|
|
64
|
+
return isTainted(value) ? value.__loomTainted : undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The inner value, discarding provenance. Non-tainted values pass through.
|
|
68
|
+
*
|
|
69
|
+
* Deliberately loud. Unwrapping is legitimate and often necessary — parsing a
|
|
70
|
+
* tainted JSON payload, indexing a tainted record — but every call is a place
|
|
71
|
+
* where provenance stops travelling. Unwrap to COMPUTE with content; never
|
|
72
|
+
* unwrap on the way to a prompt, which is the one path the taint exists to
|
|
73
|
+
* govern. Nothing can stop you; the point is that the call site is visible.
|
|
74
|
+
*/
|
|
75
|
+
function unwrapTainted(value) {
|
|
76
|
+
return isTainted(value) ? value.value : value;
|
|
77
|
+
}
|
|
78
|
+
/** Default nesting bound of {@link scanTaint}. */
|
|
79
|
+
exports.TAINT_SCAN_MAX_DEPTH = 64;
|
|
80
|
+
/** Default node bound of {@link scanTaint}. */
|
|
81
|
+
exports.TAINT_SCAN_MAX_NODES = 100_000;
|
|
82
|
+
/** The origin reported when a scan hit its bound and could not prove the value clean. */
|
|
83
|
+
exports.TAINT_SCAN_EXCEEDED_ORIGIN = "unknown:taint-scan-bound-exceeded";
|
|
84
|
+
/**
|
|
85
|
+
* Walk `value` for taint wrappers at ANY depth. Iterative (no recursion, so a 100k-deep JSON cannot
|
|
86
|
+
* blow the stack), cycle-safe, and bounded by depth and node count. FAILS CLOSED: when a bound is hit
|
|
87
|
+
* the result is `exceeded: true` and `tainted: true`, because "too big to look at" is not "clean".
|
|
88
|
+
* Binary views are skipped (they cannot hold a wrapper). Pure: the value is never modified.
|
|
89
|
+
*/
|
|
90
|
+
function scanTaint(value, options = {}) {
|
|
91
|
+
const maxDepth = options.maxDepth ?? exports.TAINT_SCAN_MAX_DEPTH;
|
|
92
|
+
const maxNodes = options.maxNodes ?? exports.TAINT_SCAN_MAX_NODES;
|
|
93
|
+
const origins = [];
|
|
94
|
+
const found = new Set();
|
|
95
|
+
let exceeded = false;
|
|
96
|
+
let nodes = 0;
|
|
97
|
+
const seen = new WeakSet();
|
|
98
|
+
const stack = [{ v: value, d: 0 }];
|
|
99
|
+
while (stack.length > 0) {
|
|
100
|
+
const { v, d } = stack.pop();
|
|
101
|
+
if (typeof v !== "object" || v === null || seen.has(v) || ArrayBuffer.isView(v))
|
|
102
|
+
continue;
|
|
103
|
+
seen.add(v);
|
|
104
|
+
if (++nodes > maxNodes || d > maxDepth) {
|
|
105
|
+
exceeded = true;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
if (isTainted(v)) {
|
|
109
|
+
const origin = v.__loomTainted.origin;
|
|
110
|
+
if (!found.has(origin)) {
|
|
111
|
+
found.add(origin);
|
|
112
|
+
origins.push(origin);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const child of Array.isArray(v) ? v : Object.values(v)) {
|
|
116
|
+
if (typeof child === "object" && child !== null)
|
|
117
|
+
stack.push({ v: child, d: d + 1 });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { origins, exceeded, tainted: exceeded || origins.length > 0 };
|
|
121
|
+
}
|
|
122
|
+
/** Most distinct origins named in a joined label; the rest are counted (`+N more`). */
|
|
123
|
+
exports.TAINT_ORIGIN_LABEL_MAX = 8;
|
|
124
|
+
/** Longest single origin kept in a joined label (a forged wrapper can carry a megabyte origin). */
|
|
125
|
+
const TAINT_ORIGIN_MAX_CHARS = 200;
|
|
126
|
+
/**
|
|
127
|
+
* The label for a {@link TaintScan}, or `undefined` when it is clean. The first
|
|
128
|
+
* {@link TAINT_ORIGIN_LABEL_MAX} distinct origins are joined (`a, b`) and the rest counted
|
|
129
|
+
* (`+N more`), so a value stuffed with forged wrappers cannot make a megabyte label that is then
|
|
130
|
+
* journaled and fenced. A bound hit adds {@link TAINT_SCAN_EXCEEDED_ORIGIN}.
|
|
131
|
+
*/
|
|
132
|
+
function taintOriginLabel(scan) {
|
|
133
|
+
if (!scan.tainted)
|
|
134
|
+
return undefined;
|
|
135
|
+
const shown = scan.origins
|
|
136
|
+
.slice(0, exports.TAINT_ORIGIN_LABEL_MAX)
|
|
137
|
+
.map((o) => o.length > TAINT_ORIGIN_MAX_CHARS ? `${o.slice(0, TAINT_ORIGIN_MAX_CHARS)}...` : o);
|
|
138
|
+
const more = scan.origins.length - shown.length;
|
|
139
|
+
if (more > 0)
|
|
140
|
+
shown.push(`+${more} more`);
|
|
141
|
+
if (scan.exceeded)
|
|
142
|
+
shown.push(exports.TAINT_SCAN_EXCEEDED_ORIGIN);
|
|
143
|
+
return shown.join(", ");
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The origin label for a value that may hold nested taint, or `undefined` when it is provably clean
|
|
147
|
+
* (see {@link taintOriginLabel}).
|
|
148
|
+
*/
|
|
149
|
+
function nestedTaintOrigin(value, options) {
|
|
150
|
+
return taintOriginLabel(scanTaint(value, options));
|
|
151
|
+
}
|
|
152
|
+
//# 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,20 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import { type DataClass } from "./classify";
|
|
3
|
+
/** A classification mark that no dot-path sink can address. */
|
|
4
|
+
export interface UnreachableMark {
|
|
5
|
+
/**
|
|
6
|
+
* Dot-path to the mark, with `[]` per array crossing and `<Type>` where an untraversable
|
|
7
|
+
* container sits — e.g. `"byId.<record>.ssn"`. Not an addressable path; a location.
|
|
8
|
+
*/
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly dataClass: DataClass;
|
|
11
|
+
/** The container that blocks it, lowercased without the `Zod` prefix (`record`, `union`, …). */
|
|
12
|
+
readonly container: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Every classification mark stranded under a container the dot-path sinks cannot address.
|
|
16
|
+
*
|
|
17
|
+
* Returns `[]` for marks on objects and arrays — both are traversable, so those marks work.
|
|
18
|
+
*/
|
|
19
|
+
export declare function collectUnreachableMarks(schema: z.ZodTypeAny, prefix?: string): UnreachableMark[];
|
|
20
|
+
//# sourceMappingURL=unreachable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unreachable.d.ts","sourceRoot":"","sources":["../src/unreachable.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EAAE,KAAK,SAAS,EAAW,MAAM,YAAY,CAAC;AAgBrD,+DAA+D;AAC/D,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,gGAAgG;IAChG,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAwFD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,EAAE,MAAM,SAAK,GAAG,eAAe,EAAE,CAE5F"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.collectUnreachableMarks = collectUnreachableMarks;
|
|
4
|
+
const classify_1 = require("./classify");
|
|
5
|
+
/** Unwrap optional/nullable/default/effects, as the classification walker does. */
|
|
6
|
+
function chain(schema) {
|
|
7
|
+
const nodes = [];
|
|
8
|
+
let cur = schema;
|
|
9
|
+
for (let i = 0; i < 100; i++) {
|
|
10
|
+
nodes.push(cur);
|
|
11
|
+
const def = cur._def;
|
|
12
|
+
const typeName = def?.typeName;
|
|
13
|
+
let next;
|
|
14
|
+
if (typeName === "ZodOptional" || typeName === "ZodNullable" || typeName === "ZodDefault") {
|
|
15
|
+
next = def?.innerType;
|
|
16
|
+
}
|
|
17
|
+
else if (typeName === "ZodEffects") {
|
|
18
|
+
next = def?.schema;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
if (next === undefined)
|
|
24
|
+
break;
|
|
25
|
+
cur = next;
|
|
26
|
+
}
|
|
27
|
+
return { nodes, inner: cur };
|
|
28
|
+
}
|
|
29
|
+
function typeNameOf(node) {
|
|
30
|
+
return node._def?.typeName;
|
|
31
|
+
}
|
|
32
|
+
function objectShape(node) {
|
|
33
|
+
const def = node._def;
|
|
34
|
+
if (def?.typeName !== "ZodObject" || typeof def.shape !== "function")
|
|
35
|
+
return undefined;
|
|
36
|
+
return def.shape();
|
|
37
|
+
}
|
|
38
|
+
function arrayElement(node) {
|
|
39
|
+
const def = node._def;
|
|
40
|
+
if (def?.typeName !== "ZodArray")
|
|
41
|
+
return undefined;
|
|
42
|
+
return def.type;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Schemas that hold inner schemas the classification walker never descends into. Returns the
|
|
46
|
+
* children to search for stranded marks, plus the label for the location string.
|
|
47
|
+
*
|
|
48
|
+
* `lazy` is deliberately absent: descending a recursive schema to hunt for marks risks
|
|
49
|
+
* non-termination, and the payoff is small.
|
|
50
|
+
*/
|
|
51
|
+
function opaqueChildren(node) {
|
|
52
|
+
const def = node._def;
|
|
53
|
+
const typeName = typeNameOf(node);
|
|
54
|
+
switch (typeName) {
|
|
55
|
+
case "ZodRecord":
|
|
56
|
+
case "ZodMap":
|
|
57
|
+
case "ZodSet":
|
|
58
|
+
return {
|
|
59
|
+
label: typeName.slice(3).toLowerCase(),
|
|
60
|
+
children: [def?.valueType].filter(Boolean),
|
|
61
|
+
};
|
|
62
|
+
case "ZodUnion":
|
|
63
|
+
case "ZodDiscriminatedUnion": {
|
|
64
|
+
const options = def?.options;
|
|
65
|
+
const list = Array.isArray(options)
|
|
66
|
+
? options
|
|
67
|
+
: [...(options?.values?.() ?? [])];
|
|
68
|
+
return { label: "union", children: list };
|
|
69
|
+
}
|
|
70
|
+
case "ZodTuple":
|
|
71
|
+
return { label: "tuple", children: def?.items ?? [] };
|
|
72
|
+
case "ZodIntersection":
|
|
73
|
+
return {
|
|
74
|
+
label: "intersection",
|
|
75
|
+
children: [def?.left, def?.right].filter(Boolean),
|
|
76
|
+
};
|
|
77
|
+
default:
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function effectiveClass(schema) {
|
|
82
|
+
for (const node of chain(schema).nodes) {
|
|
83
|
+
const mark = (0, classify_1.classOf)(node);
|
|
84
|
+
if (mark !== undefined)
|
|
85
|
+
return mark;
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Every classification mark stranded under a container the dot-path sinks cannot address.
|
|
91
|
+
*
|
|
92
|
+
* Returns `[]` for marks on objects and arrays — both are traversable, so those marks work.
|
|
93
|
+
*/
|
|
94
|
+
function collectUnreachableMarks(schema, prefix = "") {
|
|
95
|
+
return walk(schema, prefix, undefined);
|
|
96
|
+
}
|
|
97
|
+
function walk(schema, path, blockedBy) {
|
|
98
|
+
const mark = effectiveClass(schema);
|
|
99
|
+
if (mark !== undefined && path !== "") {
|
|
100
|
+
// A mark terminates the walk exactly as it does in `collectMarkedPaths` — a marked object
|
|
101
|
+
// is a unit — so a mark nested inside a marked one is not double-reported.
|
|
102
|
+
return blockedBy === undefined ? [] : [{ path, dataClass: mark, container: blockedBy }];
|
|
103
|
+
}
|
|
104
|
+
const inner = chain(schema).inner;
|
|
105
|
+
const element = arrayElement(inner);
|
|
106
|
+
if (element !== undefined)
|
|
107
|
+
return walk(element, `${path}[]`, blockedBy);
|
|
108
|
+
const shape = objectShape(inner);
|
|
109
|
+
if (shape !== undefined) {
|
|
110
|
+
const out = [];
|
|
111
|
+
for (const [key, child] of Object.entries(shape)) {
|
|
112
|
+
out.push(...walk(child, path === "" ? key : `${path}.${key}`, blockedBy));
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
const opaque = opaqueChildren(inner);
|
|
117
|
+
if (opaque !== undefined) {
|
|
118
|
+
const out = [];
|
|
119
|
+
// Everything below here is unreachable — but keep the OUTERMOST blocker, since that is
|
|
120
|
+
// the one the developer has to restructure.
|
|
121
|
+
const blocker = blockedBy ?? opaque.label;
|
|
122
|
+
for (const child of opaque.children) {
|
|
123
|
+
out.push(...walk(child, `${path}.<${opaque.label}>`, blocker));
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=unreachable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unreachable.js","sourceRoot":"","sources":["../src/unreachable.ts"],"names":[],"mappings":"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"}
|