safeseed 0.2.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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -0
  3. package/dist/catalog.d.ts +77 -0
  4. package/dist/catalog.d.ts.map +1 -0
  5. package/dist/catalog.js +203 -0
  6. package/dist/catalog.js.map +1 -0
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +228 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/csv.d.ts +21 -0
  12. package/dist/csv.d.ts.map +1 -0
  13. package/dist/csv.js +85 -0
  14. package/dist/csv.js.map +1 -0
  15. package/dist/generate.d.ts +28 -0
  16. package/dist/generate.d.ts.map +1 -0
  17. package/dist/generate.js +101 -0
  18. package/dist/generate.js.map +1 -0
  19. package/dist/hash.d.ts +10 -0
  20. package/dist/hash.d.ts.map +1 -0
  21. package/dist/hash.js +16 -0
  22. package/dist/hash.js.map +1 -0
  23. package/dist/index.d.ts +18 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +9 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/luhn.d.ts +8 -0
  28. package/dist/luhn.d.ts.map +1 -0
  29. package/dist/luhn.js +24 -0
  30. package/dist/luhn.js.map +1 -0
  31. package/dist/net.d.ts +16 -0
  32. package/dist/net.d.ts.map +1 -0
  33. package/dist/net.js +95 -0
  34. package/dist/net.js.map +1 -0
  35. package/dist/record.d.ts +36 -0
  36. package/dist/record.d.ts.map +1 -0
  37. package/dist/record.js +53 -0
  38. package/dist/record.js.map +1 -0
  39. package/dist/rng.d.ts +13 -0
  40. package/dist/rng.d.ts.map +1 -0
  41. package/dist/rng.js +26 -0
  42. package/dist/rng.js.map +1 -0
  43. package/dist/scan.d.ts +24 -0
  44. package/dist/scan.d.ts.map +1 -0
  45. package/dist/scan.js +49 -0
  46. package/dist/scan.js.map +1 -0
  47. package/dist/types.d.ts +30 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +9 -0
  50. package/dist/types.js.map +1 -0
  51. package/dist/verify.d.ts +33 -0
  52. package/dist/verify.d.ts.map +1 -0
  53. package/dist/verify.js +177 -0
  54. package/dist/verify.js.map +1 -0
  55. package/package.json +60 -0
package/dist/net.js ADDED
@@ -0,0 +1,95 @@
1
+ /**
2
+ * IP address membership helpers, so the catalog can say "this value is inside a
3
+ * standards-reserved block" precisely rather than by string prefix.
4
+ */
5
+ /** Parse a dotted-quad IPv4 string to an unsigned 32-bit int, or null if invalid. */
6
+ export function ipv4ToInt(ip) {
7
+ const parts = ip.split(".");
8
+ if (parts.length !== 4)
9
+ return null;
10
+ let n = 0;
11
+ for (const p of parts) {
12
+ if (!/^\d{1,3}$/.test(p))
13
+ return null;
14
+ const octet = Number(p);
15
+ if (octet > 255)
16
+ return null;
17
+ n = n * 256 + octet;
18
+ }
19
+ return n >>> 0;
20
+ }
21
+ /** True if `ip` falls inside the given IPv4 CIDR block (e.g. "192.0.2.0/24"). */
22
+ export function ipv4InCidr(ip, cidr) {
23
+ const [base, bitsStr] = cidr.split("/");
24
+ const bits = Number(bitsStr);
25
+ const ipInt = ipv4ToInt(ip);
26
+ const baseInt = base === undefined ? null : ipv4ToInt(base);
27
+ if (ipInt === null || baseInt === null || Number.isNaN(bits))
28
+ return false;
29
+ if (bits <= 0)
30
+ return true;
31
+ const mask = (0xffffffff << (32 - bits)) >>> 0;
32
+ return (ipInt & mask) === (baseInt & mask);
33
+ }
34
+ /**
35
+ * Expand an IPv6 string to its 8 hextets (numbers), handling a single "::"
36
+ * abbreviation and an optional zone id. Returns null if malformed.
37
+ */
38
+ export function expandIpv6(ip) {
39
+ let addr = ip.trim().toLowerCase();
40
+ const zone = addr.indexOf("%");
41
+ if (zone >= 0)
42
+ addr = addr.slice(0, zone);
43
+ if (addr === "")
44
+ return null;
45
+ const parseGroups = (s) => {
46
+ if (s === "")
47
+ return [];
48
+ const groups = s.split(":");
49
+ const out = [];
50
+ for (const g of groups) {
51
+ if (!/^[0-9a-f]{1,4}$/.test(g))
52
+ return null;
53
+ out.push(parseInt(g, 16));
54
+ }
55
+ return out;
56
+ };
57
+ const halves = addr.split("::");
58
+ if (halves.length > 2)
59
+ return null;
60
+ if (halves.length === 2) {
61
+ const head = parseGroups(halves[0]);
62
+ const tail = parseGroups(halves[1]);
63
+ if (head === null || tail === null)
64
+ return null;
65
+ const missing = 8 - head.length - tail.length;
66
+ if (missing < 0)
67
+ return null;
68
+ return [...head, ...new Array(missing).fill(0), ...tail];
69
+ }
70
+ const groups = parseGroups(addr);
71
+ if (groups === null || groups.length !== 8)
72
+ return null;
73
+ return groups;
74
+ }
75
+ /** True if `ip` falls inside the given IPv6 prefix (e.g. "2001:db8::/32"). */
76
+ export function ipv6InPrefix(ip, prefixCidr) {
77
+ const [base, bitsStr] = prefixCidr.split("/");
78
+ const bits = Number(bitsStr);
79
+ const a = expandIpv6(ip);
80
+ const b = base === undefined ? null : expandIpv6(base);
81
+ if (a === null || b === null || Number.isNaN(bits))
82
+ return false;
83
+ let remaining = bits;
84
+ for (let h = 0; h < 8; h++) {
85
+ if (remaining <= 0)
86
+ break;
87
+ const take = Math.min(16, remaining);
88
+ const mask = take === 16 ? 0xffff : (0xffff << (16 - take)) & 0xffff;
89
+ if ((a[h] & mask) !== (b[h] & mask))
90
+ return false;
91
+ remaining -= 16;
92
+ }
93
+ return true;
94
+ }
95
+ //# sourceMappingURL=net.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"net.js","sourceRoot":"","sources":["../src/net.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { GeneratedDataset } from "./generate.js";
2
+ import type { FieldType, Tier } from "./types.js";
3
+ export declare const SAFESEED_VERSION = "0.2.0";
4
+ export interface FieldRecord {
5
+ name: string;
6
+ type: FieldType;
7
+ tier: Tier;
8
+ citation: string;
9
+ claim: string;
10
+ /**
11
+ * SHA-256 over a canonical serialization of this column's values (see
12
+ * `canonicalColumn`). Lets column-scoped verify re-check one declared column
13
+ * independently of the rest of the file. Optional only for backward compat with
14
+ * 0.1.0 records that predate it; `makeRunRecord` always emits it.
15
+ */
16
+ sha256?: string;
17
+ }
18
+ export interface RunRecord {
19
+ safeseedVersion: string;
20
+ catalogVersion: string;
21
+ seed: number;
22
+ rowCount: number;
23
+ columns: string[];
24
+ fields: FieldRecord[];
25
+ /** SHA-256 of the exact emitted file content. */
26
+ contentSha256: string;
27
+ /** Honest statement of what this record attests and what it does NOT. */
28
+ attestation: string;
29
+ /** Optional caller-supplied timestamp; omitted by default to keep records deterministic. */
30
+ generatedAt?: string;
31
+ }
32
+ export declare const ATTESTATION: string;
33
+ export declare function makeRunRecord(dataset: GeneratedDataset, csv: string, opts?: {
34
+ generatedAt?: string;
35
+ }): Promise<RunRecord>;
36
+ //# sourceMappingURL=record.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record.d.ts","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAKlD,eAAO,MAAM,gBAAgB,UAAU,CAAC;AAExC,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;IAChB,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,SAAS;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,iDAAiD;IACjD,aAAa,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,WAAW,EAAE,MAAM,CAAC;IACpB,4FAA4F;IAC5F,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,WAAW,QASb,CAAC;AAEZ,wBAAsB,aAAa,CACjC,OAAO,EAAE,gBAAgB,EACzB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9B,OAAO,CAAC,SAAS,CAAC,CA2BpB"}
package/dist/record.js ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The tamper-evident run record.
3
+ *
4
+ * A run record binds to the exact bytes of the file SafeSeed emitted (a SHA-256
5
+ * content hash) and states, per field, the reserved range and honesty tier used.
6
+ * It is deliberately a *tamper-evident record*, not "cryptographic proof that the
7
+ * file contains no PII" — the battle-test panel killed the stronger claim, because
8
+ * a signature proves the tool ran, not that the file is free of personal data.
9
+ */
10
+ import { CATALOG_VERSION, getEntry } from "./catalog.js";
11
+ import { sha256Hex } from "./hash.js";
12
+ import { canonicalColumn } from "./csv.js";
13
+ // 0.2.0 adds a per-column `sha256` to every FieldRecord (the basis for opt-in
14
+ // column-scoped verify). It is purely additive — strict whole-file verify is
15
+ // unchanged, and a 0.1.0 record without per-column hashes still verifies strictly.
16
+ export const SAFESEED_VERSION = "0.2.0";
17
+ export const ATTESTATION = [
18
+ "This run record attests that the named dataset was generated by SafeSeed from",
19
+ "standards-reserved ranges, with no real input data. It is a tamper-evident",
20
+ "record, not a cryptographic proof that the file contains no personal data. It",
21
+ "attests the generator and binds to this file's content hash; it says nothing",
22
+ "about edits made after generation.",
23
+ '"Not derived from production data" is not the same claim as "not personal data."',
24
+ "Re-verify against the actual artifact in your pipeline (GDPR Art. 25/32 control",
25
+ "for non-production environments; not a scope-out from privacy law).",
26
+ ].join(" ");
27
+ export async function makeRunRecord(dataset, csv, opts) {
28
+ const contentSha256 = await sha256Hex(csv);
29
+ const fields = await Promise.all(dataset.schema.map(async (f, c) => {
30
+ const entry = getEntry(f.type);
31
+ const column = dataset.rows.map((row) => row[c] ?? "");
32
+ return {
33
+ name: f.name,
34
+ type: f.type,
35
+ tier: entry.tier,
36
+ citation: entry.citation,
37
+ claim: entry.claim,
38
+ sha256: await sha256Hex(canonicalColumn(column)),
39
+ };
40
+ }));
41
+ return {
42
+ safeseedVersion: SAFESEED_VERSION,
43
+ catalogVersion: dataset.catalogVersion ?? CATALOG_VERSION,
44
+ seed: dataset.seed,
45
+ rowCount: dataset.rows.length,
46
+ columns: dataset.columns,
47
+ fields,
48
+ contentSha256,
49
+ attestation: ATTESTATION,
50
+ ...(opts?.generatedAt !== undefined ? { generatedAt: opts.generatedAt } : {}),
51
+ };
52
+ }
53
+ //# sourceMappingURL=record.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"record.js","sourceRoot":"","sources":["../src/record.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAI3C,8EAA8E;AAC9E,6EAA6E;AAC7E,mFAAmF;AACnF,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAC;AAgCxC,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,+EAA+E;IAC/E,4EAA4E;IAC5E,+EAA+E;IAC/E,8EAA8E;IAC9E,oCAAoC;IACpC,kFAAkF;IAClF,iFAAiF;IACjF,qEAAqE;CACtE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAEZ,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAyB,EACzB,GAAW,EACX,IAA+B;IAE/B,MAAM,aAAa,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAkB,MAAM,OAAO,CAAC,GAAG,CAC7C,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE;QAChC,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAC/B,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvD,OAAO;YACL,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,KAAK,EAAE,KAAK,CAAC,KAAK;YAClB,MAAM,EAAE,MAAM,SAAS,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;SACjD,CAAC;IACJ,CAAC,CAAC,CACH,CAAC;IACF,OAAO;QACL,eAAe,EAAE,gBAAgB;QACjC,cAAc,EAAE,OAAO,CAAC,cAAc,IAAI,eAAe;QACzD,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,MAAM;QAC7B,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,MAAM;QACN,aAAa;QACb,WAAW,EAAE,WAAW;QACxB,GAAG,CAAC,IAAI,EAAE,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC9E,CAAC;AACJ,CAAC"}
package/dist/rng.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Tiny seeded PRNG so generation is deterministic: the same seed always produces
3
+ * the same dataset, which makes output a committable, reviewable fixture.
4
+ *
5
+ * mulberry32 — a well-known, public-domain 32-bit generator. Not cryptographic;
6
+ * it never needs to be. Determinism, not unpredictability, is the requirement.
7
+ */
8
+ export declare function mulberry32(seed: number): () => number;
9
+ /** Deterministically pick one element from a non-empty array. */
10
+ export declare function pick<T>(rng: () => number, arr: readonly T[]): T;
11
+ /** Deterministic integer in [min, max] inclusive. */
12
+ export declare function intBetween(rng: () => number, min: number, max: number): number;
13
+ //# sourceMappingURL=rng.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rng.d.ts","sourceRoot":"","sources":["../src/rng.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,MAAM,CASrD;AAED,iEAAiE;AACjE,wBAAgB,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,MAAM,EAAE,GAAG,EAAE,SAAS,CAAC,EAAE,GAAG,CAAC,CAE/D;AAED,qDAAqD;AACrD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE9E"}
package/dist/rng.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Tiny seeded PRNG so generation is deterministic: the same seed always produces
3
+ * the same dataset, which makes output a committable, reviewable fixture.
4
+ *
5
+ * mulberry32 — a well-known, public-domain 32-bit generator. Not cryptographic;
6
+ * it never needs to be. Determinism, not unpredictability, is the requirement.
7
+ */
8
+ export function mulberry32(seed) {
9
+ let a = seed >>> 0;
10
+ return function next() {
11
+ a |= 0;
12
+ a = (a + 0x6d2b79f5) | 0;
13
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
14
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
15
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
16
+ };
17
+ }
18
+ /** Deterministically pick one element from a non-empty array. */
19
+ export function pick(rng, arr) {
20
+ return arr[Math.floor(rng() * arr.length)];
21
+ }
22
+ /** Deterministic integer in [min, max] inclusive. */
23
+ export function intBetween(rng, min, max) {
24
+ return min + Math.floor(rng() * (max - min + 1));
25
+ }
26
+ //# sourceMappingURL=rng.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rng.js","sourceRoot":"","sources":["../src/rng.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,IAAI,CAAC,GAAG,IAAI,KAAK,CAAC,CAAC;IACnB,OAAO,SAAS,IAAI;QAClB,CAAC,IAAI,CAAC,CAAC;QACP,CAAC,GAAG,CAAC,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;QACzB,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACzC,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC/C,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,UAAU,CAAC;IAC/C,CAAC,CAAC;AACJ,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,IAAI,CAAI,GAAiB,EAAE,GAAiB;IAC1D,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CAAE,CAAC;AAC9C,CAAC;AAED,qDAAqD;AACrD,MAAM,UAAU,UAAU,CAAC,GAAiB,EAAE,GAAW,EAAE,GAAW;IACpE,OAAO,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC;AACnD,CAAC"}
package/dist/scan.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { FieldType } from "./types.js";
2
+ export interface ScanColumn {
3
+ name: string;
4
+ type: FieldType;
5
+ }
6
+ export interface ScanOptions {
7
+ csv: string;
8
+ columns: ScanColumn[];
9
+ }
10
+ export interface ScanFinding {
11
+ field: string;
12
+ type: FieldType;
13
+ row: number;
14
+ value: string;
15
+ reason: string;
16
+ }
17
+ export interface ScanResult {
18
+ ok: boolean;
19
+ findings: ScanFinding[];
20
+ perField: Record<string, number>;
21
+ scannedRows: number;
22
+ }
23
+ export declare function scan(opts: ScanOptions): ScanResult;
24
+ //# sourceMappingURL=scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,UAAU,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,SAAS,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,IAAI,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,CAmClD"}
package/dist/scan.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `scan` — reverse mode. Point it at an *existing* CSV / seed file and tell it the
3
+ * expected type of each column; it flags every value that is NOT in the reserved
4
+ * range as a candidate piece of real PII.
5
+ *
6
+ * The panel called this the thing they'd deploy first: it addresses the prod dump
7
+ * already sitting in staging, not just freshly generated data. It is a detector of
8
+ * candidates, not a classifier — a finding means "this is not provably safe, look
9
+ * at it", not "this is definitely a real person".
10
+ */
11
+ import { getEntry, isReserved } from "./catalog.js";
12
+ import { parseCsv } from "./csv.js";
13
+ export function scan(opts) {
14
+ const { columns: dataColumns, rows } = parseCsv(opts.csv);
15
+ const findings = [];
16
+ const perField = {};
17
+ for (const col of opts.columns)
18
+ perField[col.name] = 0;
19
+ const indexByName = new Map();
20
+ dataColumns.forEach((name, i) => indexByName.set(name, i));
21
+ rows.forEach((row, r) => {
22
+ for (const col of opts.columns) {
23
+ const idx = indexByName.get(col.name);
24
+ if (idx === undefined)
25
+ continue;
26
+ const value = row[idx];
27
+ if (value === undefined || value === "")
28
+ continue;
29
+ const entry = getEntry(col.type);
30
+ if (!isReserved(entry, value)) {
31
+ findings.push({
32
+ field: col.name,
33
+ type: col.type,
34
+ row: r,
35
+ value,
36
+ reason: `not in reserved range for ${col.type}`,
37
+ });
38
+ perField[col.name] = (perField[col.name] ?? 0) + 1;
39
+ }
40
+ }
41
+ });
42
+ return {
43
+ ok: findings.length === 0,
44
+ findings,
45
+ perField,
46
+ scannedRows: rows.length,
47
+ };
48
+ }
49
+ //# sourceMappingURL=scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan.js","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AA4BpC,MAAM,UAAU,IAAI,CAAC,IAAiB;IACpC,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1D,MAAM,QAAQ,GAAkB,EAAE,CAAC;IACnC,MAAM,QAAQ,GAA2B,EAAE,CAAC;IAC5C,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,OAAO;QAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEvD,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC9C,WAAW,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAE3D,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE;QACtB,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAC/B,MAAM,GAAG,GAAG,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACtC,IAAI,GAAG,KAAK,SAAS;gBAAE,SAAS;YAChC,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;YACvB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE;gBAAE,SAAS;YAClD,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YACjC,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC;gBAC9B,QAAQ,CAAC,IAAI,CAAC;oBACZ,KAAK,EAAE,GAAG,CAAC,IAAI;oBACf,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,GAAG,EAAE,CAAC;oBACN,KAAK;oBACL,MAAM,EAAE,6BAA6B,GAAG,CAAC,IAAI,EAAE;iBAChD,CAAC,CAAC;gBACH,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YACrD,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,EAAE,EAAE,QAAQ,CAAC,MAAM,KAAK,CAAC;QACzB,QAAQ;QACR,QAAQ;QACR,WAAW,EAAE,IAAI,CAAC,MAAM;KACzB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Core type vocabulary for SafeSeed.
3
+ *
4
+ * The honesty tier is the load-bearing concept: every value SafeSeed touches is
5
+ * classified by *how* it is known to be non-real, and the language allowed about
6
+ * it follows from the tier (see `record.ts`).
7
+ */
8
+ /**
9
+ * How a value is known to be non-real. Ordered from strongest to weakest claim.
10
+ *
11
+ * - `provably-non-real` reserved by a published *standard / protocol*; cannot
12
+ * belong to a real person or system (RFC 2606 domains,
13
+ * RFC 5737 / 3849 documentation IPs). The standard itself
14
+ * makes them non-routable / non-registrable.
15
+ * - `reserved-not-issued` reserved by the *issuing authority* and never assigned,
16
+ * so no real holder has one (NANPA 555-01xx fictitious
17
+ * phones, SSA never-issued SSN ranges). Strong, but it
18
+ * rests on administrative policy, NOT protocol — so it is
19
+ * held apart from the protocol-provable tier above.
20
+ * - `designated-test-only` a valid-looking value that networks/sandboxes have
21
+ * *designated* for testing (e.g. card test PANs). It
22
+ * passes validation, so it is non-real by designation,
23
+ * NOT by impossibility.
24
+ * - `structurally-fake` no standard reserves it (names, addresses, free text),
25
+ * so it is made self-evidently fake instead of plausible.
26
+ */
27
+ export type Tier = "provably-non-real" | "reserved-not-issued" | "designated-test-only" | "structurally-fake";
28
+ /** The PII-shaped field types SafeSeed knows how to generate, verify, and scan. */
29
+ export type FieldType = "email" | "domain" | "ipv4" | "ipv6" | "phone" | "ssn" | "creditCard" | "firstName" | "lastName" | "fullName" | "streetAddress" | "freeText";
30
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,IAAI,GACZ,mBAAmB,GACnB,qBAAqB,GACrB,sBAAsB,GACtB,mBAAmB,CAAC;AAExB,mFAAmF;AACnF,MAAM,MAAM,SAAS,GACjB,OAAO,GACP,QAAQ,GACR,MAAM,GACN,MAAM,GACN,OAAO,GACP,KAAK,GACL,YAAY,GACZ,WAAW,GACX,UAAU,GACV,UAAU,GACV,eAAe,GACf,UAAU,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Core type vocabulary for SafeSeed.
3
+ *
4
+ * The honesty tier is the load-bearing concept: every value SafeSeed touches is
5
+ * classified by *how* it is known to be non-real, and the language allowed about
6
+ * it follows from the tier (see `record.ts`).
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
@@ -0,0 +1,33 @@
1
+ import type { RunRecord } from "./record.js";
2
+ export type VerifyFailureKind = "content-hash-mismatch" | "out-of-range-value" | "schema-mismatch" | "row-arity-mismatch" | "missing-column" | "column-hash-mismatch";
3
+ export interface VerifyFailure {
4
+ kind: VerifyFailureKind;
5
+ message: string;
6
+ field?: string;
7
+ row?: number;
8
+ value?: string;
9
+ }
10
+ export interface VerifyResult {
11
+ ok: boolean;
12
+ failures: VerifyFailure[];
13
+ checked: {
14
+ rows: number;
15
+ fields: number;
16
+ };
17
+ /** Columns present in the file but not declared in the record (column-scoped mode). */
18
+ unattestedColumns: string[];
19
+ /** Non-fatal notes, e.g. a 0.1.0 record with no per-column hash → range-only fallback. */
20
+ warnings: string[];
21
+ }
22
+ export interface VerifyOptions {
23
+ /**
24
+ * Opt-in column-scoped mode. Attest only the declared synthetic columns (by name)
25
+ * and report — rather than fail on — columns the team added. Off by default, so
26
+ * the strict whole-file guarantee is the one you get unless you ask otherwise.
27
+ */
28
+ allowAddedColumns?: boolean;
29
+ }
30
+ export declare function verify(csv: string, record: RunRecord, opts?: VerifyOptions): Promise<VerifyResult>;
31
+ /** CI helper: 0 when clean, 1 on any drift. */
32
+ export declare function exitCode(result: VerifyResult): number;
33
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE7C,MAAM,MAAM,iBAAiB,GACzB,uBAAuB,GACvB,oBAAoB,GACpB,iBAAiB,GACjB,oBAAoB,GACpB,gBAAgB,GAChB,sBAAsB,CAAC;AAE3B,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,iBAAiB,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,OAAO,CAAC;IACZ,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,uFAAuF;IACvF,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,0FAA0F;IAC1F,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,wBAAsB,MAAM,CAC1B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,SAAS,EACjB,IAAI,CAAC,EAAE,aAAa,GACnB,OAAO,CAAC,YAAY,CAAC,CAEvB;AA0KD,+CAA+C;AAC/C,wBAAgB,QAAQ,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAErD"}
package/dist/verify.js ADDED
@@ -0,0 +1,177 @@
1
+ /**
2
+ * `verify` — the enforcement gate. Given a file and its run record, it:
3
+ * 1. re-hashes the file and compares to the recorded content hash (tamper-evidence), and
4
+ * 2. independently re-checks every value against its declared reserved range.
5
+ *
6
+ * Both checks run and all failures are reported. The range check is independent of
7
+ * the hash on purpose: even if someone edits the file *and* recomputes the hash,
8
+ * an out-of-range (candidate real) value still fails. Wire `exitCode` into CI to
9
+ * fail the build on any drift.
10
+ *
11
+ * Strict whole-file verify is the DEFAULT. Pass `{ allowAddedColumns: true }` for
12
+ * column-scoped verify: it attests only the declared synthetic columns (matched by
13
+ * header name, order-independent) via per-column hash + range, and REPORTS added
14
+ * business columns as unattested rather than failing on them. The relaxed
15
+ * guarantee is never silent — it only happens when the caller opts in. Column-scoped
16
+ * verify vouches for the synthetic columns; the columns a team adds are out of
17
+ * scope here and must be checked with `scan`.
18
+ */
19
+ import { getEntry, isReserved } from "./catalog.js";
20
+ import { sha256Hex } from "./hash.js";
21
+ import { parseCsv, canonicalColumn } from "./csv.js";
22
+ export async function verify(csv, record, opts) {
23
+ return opts?.allowAddedColumns ? verifyColumnScoped(csv, record) : verifyStrict(csv, record);
24
+ }
25
+ async function verifyStrict(csv, record) {
26
+ const failures = [];
27
+ const actualHash = await sha256Hex(csv);
28
+ if (actualHash !== record.contentSha256) {
29
+ failures.push({
30
+ kind: "content-hash-mismatch",
31
+ message: `content hash ${actualHash} does not match recorded ${record.contentSha256}`,
32
+ });
33
+ }
34
+ const { columns, rows } = parseCsv(csv);
35
+ const columnsMatch = columns.length === record.columns.length &&
36
+ columns.every((c, i) => c === record.columns[i]);
37
+ if (!columnsMatch) {
38
+ failures.push({
39
+ kind: "schema-mismatch",
40
+ message: `columns ${JSON.stringify(columns)} do not match recorded ${JSON.stringify(record.columns)}`,
41
+ });
42
+ }
43
+ rows.forEach((row, r) => {
44
+ // The verifier must be authoritative over the WHOLE row, not just the declared
45
+ // columns — otherwise a tampered file could append a trailing column of real PII
46
+ // (and recompute the hash) and pass. Any arity mismatch is a failure.
47
+ if (row.length !== record.fields.length) {
48
+ failures.push({
49
+ kind: "row-arity-mismatch",
50
+ row: r,
51
+ message: `row ${r}: expected ${record.fields.length} columns, found ${row.length}`,
52
+ });
53
+ }
54
+ record.fields.forEach((field, c) => {
55
+ const value = row[c];
56
+ if (value === undefined) {
57
+ failures.push({
58
+ kind: "out-of-range-value",
59
+ field: field.name,
60
+ row: r,
61
+ message: `${field.name} row ${r}: missing value`,
62
+ });
63
+ return;
64
+ }
65
+ const entry = getEntry(field.type);
66
+ if (!isReserved(entry, value)) {
67
+ failures.push({
68
+ kind: "out-of-range-value",
69
+ field: field.name,
70
+ row: r,
71
+ value,
72
+ message: `${field.name} row ${r}: "${value}" is not in the reserved range for ${field.type}`,
73
+ });
74
+ }
75
+ });
76
+ });
77
+ return {
78
+ ok: failures.length === 0,
79
+ failures,
80
+ checked: { rows: rows.length, fields: record.fields.length },
81
+ unattestedColumns: [],
82
+ warnings: [],
83
+ };
84
+ }
85
+ async function verifyColumnScoped(csv, record) {
86
+ const failures = [];
87
+ const warnings = [];
88
+ const { columns, rows } = parseCsv(csv);
89
+ // The file must stay rectangular: every row matches the header width. This closes
90
+ // the same hole strict mode closes — a trailing unheadered cell can't smuggle in
91
+ // real PII, because it would make a row wider than the header and fail here.
92
+ rows.forEach((row, r) => {
93
+ if (row.length !== columns.length) {
94
+ failures.push({
95
+ kind: "row-arity-mismatch",
96
+ row: r,
97
+ message: `row ${r}: expected ${columns.length} columns, found ${row.length}`,
98
+ });
99
+ }
100
+ });
101
+ // Count header occurrences so a duplicated declared name is caught as ambiguous
102
+ // rather than silently resolving to the first match.
103
+ const occurrences = new Map();
104
+ for (const h of columns)
105
+ occurrences.set(h, (occurrences.get(h) ?? 0) + 1);
106
+ const declaredNames = new Set(record.fields.map((f) => f.name));
107
+ for (const field of record.fields) {
108
+ const count = occurrences.get(field.name) ?? 0;
109
+ if (count === 0) {
110
+ failures.push({
111
+ kind: "missing-column",
112
+ field: field.name,
113
+ message: `declared column "${field.name}" is missing from the file`,
114
+ });
115
+ continue;
116
+ }
117
+ if (count > 1) {
118
+ failures.push({
119
+ kind: "schema-mismatch",
120
+ field: field.name,
121
+ message: `declared column "${field.name}" is ambiguous: it appears ${count} times`,
122
+ });
123
+ continue;
124
+ }
125
+ const idx = columns.indexOf(field.name);
126
+ const values = rows.map((row) => row[idx] ?? "");
127
+ const entry = getEntry(field.type);
128
+ // Independent range check: a real value smuggled into a synthetic column fails
129
+ // even if an attacker recomputes the column hash to match.
130
+ values.forEach((value, r) => {
131
+ if (!isReserved(entry, value)) {
132
+ failures.push({
133
+ kind: "out-of-range-value",
134
+ field: field.name,
135
+ row: r,
136
+ value,
137
+ message: `${field.name} row ${r}: "${value}" is not in the reserved range for ${field.type}`,
138
+ });
139
+ }
140
+ });
141
+ // Per-column hash: catches an in-range swap (one reserved value for another)
142
+ // that the range check alone would wave through. Falls back to range-only with a
143
+ // warning for 0.1.0 records that predate per-column hashes.
144
+ if (field.sha256 === undefined) {
145
+ warnings.push(`column "${field.name}": run record predates per-column hashes; verified by range only`);
146
+ }
147
+ else {
148
+ const actual = await sha256Hex(canonicalColumn(values));
149
+ if (actual !== field.sha256) {
150
+ failures.push({
151
+ kind: "column-hash-mismatch",
152
+ field: field.name,
153
+ message: `column "${field.name}" hash ${actual} does not match recorded ${field.sha256}`,
154
+ });
155
+ }
156
+ }
157
+ }
158
+ const unattestedColumns = columns.filter((c) => !declaredNames.has(c));
159
+ // A blank-headed added column is surfaced (it's in unattestedColumns as ""), but a "" in
160
+ // a long list is easy to miss — and a team told to "scan the columns you added" can't
161
+ // easily name it. Warn so it can't be overlooked. Not a failure: added columns are scan's job.
162
+ if (unattestedColumns.some((c) => c.trim() === "")) {
163
+ warnings.push("an added (unattested) column has a blank header; make sure your scan covers it");
164
+ }
165
+ return {
166
+ ok: failures.length === 0,
167
+ failures,
168
+ checked: { rows: rows.length, fields: record.fields.length },
169
+ unattestedColumns,
170
+ warnings,
171
+ };
172
+ }
173
+ /** CI helper: 0 when clean, 1 on any drift. */
174
+ export function exitCode(result) {
175
+ return result.ok ? 0 : 1;
176
+ }
177
+ //# sourceMappingURL=verify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.js","sourceRoot":"","sources":["../src/verify.ts"],"names":[],"mappings":"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"}