@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.
Files changed (53) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +73 -2
  4. package/dist/classify.d.ts +47 -0
  5. package/dist/classify.d.ts.map +1 -0
  6. package/dist/classify.js +147 -0
  7. package/dist/classify.js.map +1 -0
  8. package/dist/esm/classify.js +135 -0
  9. package/dist/esm/classify.js.map +1 -0
  10. package/dist/esm/expand.js +87 -0
  11. package/dist/esm/expand.js.map +1 -0
  12. package/dist/esm/external.js +33 -0
  13. package/dist/esm/external.js.map +1 -0
  14. package/dist/esm/index.js +7 -0
  15. package/dist/esm/index.js.map +1 -0
  16. package/dist/esm/redact.js +56 -0
  17. package/dist/esm/redact.js.map +1 -0
  18. package/dist/esm/taint.js +142 -0
  19. package/dist/esm/taint.js.map +1 -0
  20. package/dist/esm/unreachable.js +126 -0
  21. package/dist/esm/unreachable.js.map +1 -0
  22. package/dist/expand.d.ts +16 -0
  23. package/dist/expand.d.ts.map +1 -0
  24. package/dist/expand.js +91 -0
  25. package/dist/expand.js.map +1 -0
  26. package/dist/external.d.ts +15 -0
  27. package/dist/external.d.ts.map +1 -0
  28. package/dist/external.js +38 -0
  29. package/dist/external.js.map +1 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +38 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/redact.d.ts +4 -0
  35. package/dist/redact.d.ts.map +1 -0
  36. package/dist/redact.js +60 -0
  37. package/dist/redact.js.map +1 -0
  38. package/dist/taint.d.ts +85 -0
  39. package/dist/taint.d.ts.map +1 -0
  40. package/dist/taint.js +152 -0
  41. package/dist/taint.js.map +1 -0
  42. package/dist/unreachable.d.ts +20 -0
  43. package/dist/unreachable.d.ts.map +1 -0
  44. package/dist/unreachable.js +129 -0
  45. package/dist/unreachable.js.map +1 -0
  46. package/package.json +55 -3
  47. package/src/classify.ts +175 -0
  48. package/src/expand.ts +97 -0
  49. package/src/external.ts +40 -0
  50. package/src/index.ts +31 -0
  51. package/src/redact.ts +59 -0
  52. package/src/taint.ts +189 -0
  53. package/src/unreachable.ts +164 -0
@@ -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"}
@@ -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"}
@@ -0,0 +1,4 @@
1
+ /** The value an egress-masked field is replaced by (structure kept, value gone). */
2
+ export declare const REDACTED: "[redacted]";
3
+ export declare function maskPaths(root: unknown, paths: readonly string[], mask?: unknown): unknown;
4
+ //# sourceMappingURL=redact.d.ts.map
@@ -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"}
@@ -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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"}