enigma-memory 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/LICENSE +55 -0
  2. package/README.md +379 -0
  3. package/apps/cli/bin/enigma.mjs +1117 -0
  4. package/apps/desktop/src/app.js +1327 -0
  5. package/apps/desktop/src/index.html +1373 -0
  6. package/apps/desktop/src/styles.css +386 -0
  7. package/apps/gateway/bin/enigma-gateway.mjs +52 -0
  8. package/apps/gateway/src/server.mjs +1403 -0
  9. package/apps/native-host/README.md +265 -0
  10. package/apps/native-host/bin/enigma-native-host.mjs +499 -0
  11. package/apps/native-host/manifests/com.enigma.native_host.chrome.json +9 -0
  12. package/apps/native-host/manifests/com.enigma.native_host.edge.json +9 -0
  13. package/apps/native-host/manifests/com.enigma.native_host.firefox.json +9 -0
  14. package/apps/relay/bin/enigma-relay.mjs +52 -0
  15. package/apps/relay/src/server.mjs +1571 -0
  16. package/apps/verifier/bin/enigma-verify.mjs +240 -0
  17. package/deploy/docker-compose.production.example.yml +150 -0
  18. package/deploy/kubernetes/enigma-backend.example.yaml +580 -0
  19. package/package.json +196 -0
  20. package/packages/adapters/PACKAGE_CONTRACT.md +47 -0
  21. package/packages/adapters/src/index.js +183 -0
  22. package/packages/boundary/src/index.js +468 -0
  23. package/packages/connectors/src/index.js +533 -0
  24. package/packages/core/src/index.js +766 -0
  25. package/packages/enterprise/src/index.js +505 -0
  26. package/packages/importers/src/index.js +1083 -0
  27. package/packages/mcp-server/bin/enigma-mcp.mjs +19 -0
  28. package/packages/mcp-server/src/index.js +1185 -0
  29. package/packages/mesh/src/index.js +531 -0
  30. package/packages/metering/PACKAGE_CONTRACT.md +20 -0
  31. package/packages/metering/src/index.js +246 -0
  32. package/packages/optimizer/src/index.js +707 -0
  33. package/packages/passport/src/index.js +651 -0
  34. package/packages/settlement/PACKAGE_CONTRACT.md +28 -0
  35. package/packages/settlement/src/index.js +369 -0
  36. package/packages/storage/PACKAGE_CONTRACT.md +23 -0
  37. package/packages/storage/src/index.js +392 -0
  38. package/packages/vault/src/index.js +904 -0
  39. package/scripts/build-ai-orchestration-plan.mjs +248 -0
  40. package/scripts/build-cloudflare-pages-release-packet.mjs +287 -0
  41. package/scripts/build-cloudflare-token-policy.mjs +239 -0
  42. package/scripts/build-cloudflare-token-request.mjs +314 -0
  43. package/scripts/build-demo-assets.mjs +263 -0
  44. package/scripts/build-edge-backend-workers.mjs +353 -0
  45. package/scripts/build-goal-completion-audit.mjs +398 -0
  46. package/scripts/build-hosted-probe-worker.mjs +191 -0
  47. package/scripts/build-operator-acceptance-packet.mjs +254 -0
  48. package/scripts/build-operator-evidence-starter.mjs +525 -0
  49. package/scripts/build-production-backend-env-kit.mjs +384 -0
  50. package/scripts/build-production-dependency-report.mjs +369 -0
  51. package/scripts/build-production-handoff-packet.mjs +475 -0
  52. package/scripts/build-production-readiness-manifest.mjs +209 -0
  53. package/scripts/build-production-status-board.mjs +297 -0
  54. package/scripts/build-production-storage-migration.mjs +86 -0
  55. package/scripts/build-production-workplan.mjs +371 -0
  56. package/scripts/build-review-packet.mjs +594 -0
  57. package/scripts/check.mjs +419 -0
  58. package/scripts/cloudflare-ops.mjs +1672 -0
  59. package/scripts/cloudflare-secret-env.mjs +128 -0
  60. package/scripts/collect-hosted-backend-live-evidence.mjs +263 -0
  61. package/scripts/infrastructure-readiness.mjs +944 -0
  62. package/scripts/install-enigma-local.mjs +270 -0
  63. package/scripts/memory-optimization-benchmark.mjs +242 -0
  64. package/scripts/monitor-live-endpoints.mjs +358 -0
  65. package/scripts/release-audit.mjs +5588 -0
  66. package/scripts/release-provenance.mjs +250 -0
  67. package/scripts/run-backend-readiness-smoke.mjs +220 -0
  68. package/scripts/validate-backup-restore-drill.mjs +166 -0
  69. package/scripts/validate-cloudflare-credentials.mjs +112 -0
  70. package/scripts/validate-cloudflare-worker-inspect.mjs +179 -0
  71. package/scripts/validate-domain-tls.mjs +248 -0
  72. package/scripts/validate-hosted-backend-live.mjs +319 -0
  73. package/scripts/validate-incident-drill.mjs +221 -0
  74. package/scripts/validate-kms-custody.mjs +235 -0
  75. package/scripts/validate-legal-compliance-approval.mjs +274 -0
  76. package/scripts/validate-monitoring-alerting.mjs +263 -0
  77. package/scripts/validate-network-access-policy.mjs +285 -0
  78. package/scripts/validate-operator-acceptance.mjs +298 -0
  79. package/scripts/validate-production-manifests.mjs +325 -0
  80. package/scripts/validate-public-site-security.mjs +252 -0
  81. package/scripts/validate-security-threat-model.mjs +285 -0
  82. package/scripts/validate-service-settlement.mjs +256 -0
  83. package/scripts/validate-support-sla.mjs +190 -0
  84. package/scripts/validate-tenant-policy-approval.mjs +228 -0
  85. package/scripts/validate-usage-metering.mjs +224 -0
  86. package/scripts/validate-whitepaper-claims.mjs +166 -0
  87. package/specs/boundary-manifest-v1.schema.json +35 -0
  88. package/specs/capsule-v1.schema.json +55 -0
  89. package/specs/claim-boundary-manifest-v1.schema.json +22 -0
  90. package/specs/connector-profile-v1.schema.json +62 -0
  91. package/specs/deletion-tombstone-v1.schema.json +26 -0
  92. package/specs/enterprise-policy-v1.schema.json +59 -0
  93. package/specs/gateway-request-v1.schema.json +32 -0
  94. package/specs/goal-completion-audit-v1.schema.json +70 -0
  95. package/specs/hosted-backend-live-evidence-v1.schema.json +147 -0
  96. package/specs/import-report-v1.schema.json +214 -0
  97. package/specs/infrastructure-readiness-manifest-v1.schema.json +130 -0
  98. package/specs/memory-event-v1.schema.json +42 -0
  99. package/specs/mesh-node-v1.schema.json +126 -0
  100. package/specs/passport-v1.schema.json +50 -0
  101. package/specs/production-handoff-packet-v1.schema.json +149 -0
  102. package/specs/receipt-v1.schema.json +61 -0
  103. package/specs/relay-record-v1.schema.json +43 -0
  104. package/specs/state-checkpoint-v1.schema.json +37 -0
  105. package/specs/trust-bundle-v1.schema.json +56 -0
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
3
+ import { dirname, extname, join, relative, resolve, sep } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ export const PUBLIC_SITE_SECURITY_RESULT_SCHEMA = 'enigma.public_site_security_result.v1';
7
+
8
+ const TEXT_EXTENSIONS = new Set(['.css', '.html', '.js', '.json', '.md', '.svg', '.txt', '.xml', '.yml', '.yaml', '']);
9
+ const BLOCKED_DIR_OR_FILE_RE = /(?:^|[\\/])(?:\.git|\.github|\.env(?:\.|$)|node_modules|__pycache__|private|internal|secrets?|credentials?|api[-_]?keys?|private[-_]?keys?|launch-code|raw[-_]?memory)(?:[\\/]|$|[._-])/iu;
10
+ const BLOCKED_EXTENSION_RE = /\.(?:map|pem|key|p12|pfx|jks|keystore|sqlite|sqlite3|db|log)(?:$|\.)/iu;
11
+ const SECRET_VALUE_RE = /(?:Bearer\s+[A-Za-z0-9._~+/=-]{12,}|Basic\s+[A-Za-z0-9+/=-]{12,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|https?:\/\/[^\s/@]+:[^\s/@]+@|sk-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16}|"(?:raw_memory|plaintext|prompt|completion|transcript|provider_response|memory)"\s*:\s*"[^"]+")/iu;
12
+ const EMAIL_RE = /\b[A-Z0-9._%+-]+@([A-Z0-9.-]+\.[A-Z]{2,})\b/giu;
13
+ const PHONE_RE = /(?:\+?1[\s.-]?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}\b|\b(?:\(\d{3}\)|\d{3}[\s.-])[\s.-]?\d{3}[\s.-]\d{4}\b)/g;
14
+ const ADDRESS_RE = /\b\d{2,6}\s+[A-Z][A-Za-z0-9.'-]*(?:\s+[A-Z][A-Za-z0-9.'-]*){0,5}\s+(?:St|Street|Ave|Avenue|Rd|Road|Ln|Lane|Dr|Drive|Ct|Court|Blvd|Boulevard|Way|Trail|Trl|Pkwy|Parkway)\b/gu;
15
+ const SSN_RE = /\b\d{3}-\d{2}-\d{4}\b/g;
16
+ const SOURCE_MAP_RE = /sourceMappingURL\s*=|\.map(?:\?|#|$)/iu;
17
+ const EXTERNAL_SCRIPT_RE = /<script\b[^>]*\bsrc=["'](?:https?:)?\/\//giu;
18
+ const HREF_SRC_RE = /\b(?:href|src)=["']([^"'#?]+)(?:[?#][^"']*)?["']/giu;
19
+ const REQUIRED_HEADERS = Object.freeze({
20
+ 'x-content-type-options': 'nosniff',
21
+ 'x-frame-options': 'DENY',
22
+ 'referrer-policy': 'strict-origin-when-cross-origin',
23
+ 'permissions-policy': null,
24
+ 'content-security-policy': null,
25
+ });
26
+ const ALLOWED_EMAIL_DOMAINS = new Set(['example.com', 'example.org', 'example.net', 'example.invalid', 'enigma.ai', 'enigmamemory.com']);
27
+
28
+ function blocker(message, path = null) {
29
+ return path ? { message, path } : { message };
30
+ }
31
+
32
+ function toPosix(path) {
33
+ return path.split(sep).join('/');
34
+ }
35
+
36
+ function inside(base, candidate) {
37
+ const rel = relative(base, candidate);
38
+ return rel === '' || (!rel.startsWith('..') && !resolve(rel).startsWith('..'));
39
+ }
40
+
41
+ async function walk(root, dir = root, out = []) {
42
+ const entries = await readdir(dir, { withFileTypes: true });
43
+ for (const entry of entries) {
44
+ const full = join(dir, entry.name);
45
+ if (entry.isDirectory()) await walk(root, full, out);
46
+ else if (entry.isFile()) out.push(full);
47
+ }
48
+ return out;
49
+ }
50
+
51
+ async function readText(path) {
52
+ return readFile(path, 'utf8');
53
+ }
54
+
55
+ function parseHeaderFile(text) {
56
+ const headers = new Map();
57
+ let active = false;
58
+ for (const rawLine of text.split(/\r?\n/)) {
59
+ const line = rawLine.trim();
60
+ if (!line || line.startsWith('#')) continue;
61
+ if (!rawLine.startsWith(' ') && !rawLine.startsWith('\t')) {
62
+ active = line === '/*' || line === '/';
63
+ continue;
64
+ }
65
+ if (!active) continue;
66
+ const match = line.match(/^([^:]+):\s*(.+)$/);
67
+ if (match) headers.set(match[1].trim().toLowerCase(), match[2].trim());
68
+ }
69
+ return headers;
70
+ }
71
+
72
+ function addHeaderBlockers(headers, blockers) {
73
+ for (const [header, expected] of Object.entries(REQUIRED_HEADERS)) {
74
+ if (!headers.has(header)) {
75
+ blockers.push(blocker(`missing required security header: ${header}`, '_headers'));
76
+ continue;
77
+ }
78
+ if (expected !== null && headers.get(header) !== expected) blockers.push(blocker(`security header ${header} must be ${expected}`, '_headers'));
79
+ }
80
+ }
81
+
82
+ function scanEmail(text, rel, blockers) {
83
+ for (const match of text.matchAll(EMAIL_RE)) {
84
+ const domain = String(match[1] ?? '').toLowerCase();
85
+ if (!ALLOWED_EMAIL_DOMAINS.has(domain)) blockers.push(blocker('public site contains raw email address', rel));
86
+ }
87
+ }
88
+
89
+ function scanText(text, rel, blockers) {
90
+ if (SECRET_VALUE_RE.test(text)) blockers.push(blocker('public site contains secret-looking content', rel));
91
+ if (PHONE_RE.test(text)) blockers.push(blocker('public site contains phone-number-looking content', rel));
92
+ if (ADDRESS_RE.test(text)) blockers.push(blocker('public site contains street-address-looking content', rel));
93
+ if (SSN_RE.test(text)) blockers.push(blocker('public site contains SSN-looking content', rel));
94
+ if (SOURCE_MAP_RE.test(text)) blockers.push(blocker('public site references source maps or sourceMappingURL', rel));
95
+ scanEmail(text, rel, blockers);
96
+ if (EXTERNAL_SCRIPT_RE.test(text)) blockers.push(blocker('public site loads external script', rel));
97
+ }
98
+
99
+ function isIgnoredLink(value) {
100
+ return /^(?:https?:|mailto:|tel:|sms:|data:|blob:|javascript:|#)/iu.test(value) || value === '';
101
+ }
102
+
103
+ function normalizeLink(fromRel, value) {
104
+ if (isIgnoredLink(value)) return null;
105
+ if (value.startsWith('/')) return value.slice(1).replace(/\/+/g, '/');
106
+ const baseParts = fromRel.split('/');
107
+ baseParts.pop();
108
+ const parts = [...baseParts, ...value.split('/')];
109
+ const out = [];
110
+ for (const part of parts) {
111
+ if (!part || part === '.') continue;
112
+ if (part === '..') {
113
+ if (out.length === 0) return '__ESCAPE__';
114
+ out.pop();
115
+ } else out.push(part);
116
+ }
117
+ return out.join('/');
118
+ }
119
+
120
+ function existsStatic(rel, fileSet) {
121
+ if (rel === '' || rel === '.') return fileSet.has('index.html');
122
+ if (fileSet.has(rel)) return true;
123
+ if (fileSet.has(`${rel}/index.html`)) return true;
124
+ if (rel.endsWith('/') && fileSet.has(`${rel}index.html`)) return true;
125
+ return false;
126
+ }
127
+
128
+ function scanLinks(text, rel, fileSet, blockers) {
129
+ for (const match of text.matchAll(HREF_SRC_RE)) {
130
+ const normalized = normalizeLink(rel, match[1]);
131
+ if (normalized === null) continue;
132
+ if (normalized === '__ESCAPE__') {
133
+ blockers.push(blocker('public site link escapes site root', rel));
134
+ continue;
135
+ }
136
+ if (!existsStatic(normalized, fileSet)) blockers.push(blocker(`public site local link target is missing: ${match[1]}`, rel));
137
+ }
138
+ }
139
+
140
+ export async function validatePublicSiteSecurity(input = {}, options = {}) {
141
+ const site = resolve(String(input.site ?? input.site_dir ?? input.siteDir ?? '_public_site'));
142
+ const blockers = [];
143
+ let files = [];
144
+ try {
145
+ const stats = await stat(site);
146
+ if (!stats.isDirectory()) blockers.push(blocker('site path must be a directory', site));
147
+ else files = await walk(site);
148
+ } catch (error) {
149
+ blockers.push(blocker(`site path is not readable: ${error.message}`, site));
150
+ }
151
+
152
+ const relFiles = files.map((file) => toPosix(relative(site, file))).sort();
153
+ const fileSet = new Set(relFiles);
154
+ if (files.length === 0 && blockers.length === 0) blockers.push(blocker('site directory contains no files', site));
155
+ if (!fileSet.has('index.html')) blockers.push(blocker('public site must include index.html', 'index.html'));
156
+ if (!fileSet.has('_headers')) blockers.push(blocker('public site must include _headers', '_headers'));
157
+
158
+ for (const rel of relFiles) {
159
+ if (BLOCKED_DIR_OR_FILE_RE.test(rel) || BLOCKED_EXTENSION_RE.test(rel)) blockers.push(blocker('public site contains forbidden private/generated file path', rel));
160
+ const full = resolve(site, rel);
161
+ if (!inside(site, full)) blockers.push(blocker('public site file escapes site root', rel));
162
+ }
163
+
164
+ if (fileSet.has('_headers')) {
165
+ try {
166
+ addHeaderBlockers(parseHeaderFile(await readText(join(site, '_headers'))), blockers);
167
+ } catch (error) {
168
+ blockers.push(blocker(`cannot read _headers: ${error.message}`, '_headers'));
169
+ }
170
+ }
171
+
172
+ for (const file of files) {
173
+ const rel = toPosix(relative(site, file));
174
+ const ext = extname(file).toLowerCase();
175
+ if (!TEXT_EXTENSIONS.has(ext)) continue;
176
+ let text;
177
+ try {
178
+ text = await readText(file);
179
+ } catch (error) {
180
+ blockers.push(blocker(`cannot read text file: ${error.message}`, rel));
181
+ continue;
182
+ }
183
+ scanText(text, rel, blockers);
184
+ if (ext === '.html') scanLinks(text, rel, fileSet, blockers);
185
+ }
186
+
187
+ const result = {
188
+ schema: PUBLIC_SITE_SECURITY_RESULT_SCHEMA,
189
+ generated_at: options.generated_at ?? options.generatedAt ?? new Date(0).toISOString(),
190
+ ok: blockers.length === 0,
191
+ status: blockers.length === 0 ? 'accepted' : 'blocked',
192
+ site,
193
+ blockers,
194
+ checked: {
195
+ file_count: relFiles.length,
196
+ text_file_count: relFiles.filter((rel) => TEXT_EXTENSIONS.has(extname(rel).toLowerCase())).length,
197
+ has_index: fileSet.has('index.html'),
198
+ has_headers: fileSet.has('_headers'),
199
+ forbidden_path_count: blockers.filter((entry) => /forbidden private\/generated file path/.test(entry.message)).length,
200
+ },
201
+ claim_boundary: [
202
+ 'Public site security validation checks the supplied static artifact only; it does not deploy, configure DNS/TLS, or verify Cloudflare runtime headers.',
203
+ 'A pass result means this local artifact avoided configured PII, secret, source-map, private-collateral, local-link, and security-header blockers.',
204
+ 'Personal contact data, credentials, raw-memory values, prompt/transcript/provider-response JSON values, private launch collateral, and source maps must stay out of the public artifact.',
205
+ ],
206
+ };
207
+ return result;
208
+ }
209
+
210
+ function parseArgs(argv = process.argv.slice(2)) {
211
+ const flags = new Map();
212
+ for (let index = 0; index < argv.length; index += 1) {
213
+ const arg = argv[index];
214
+ if (!arg.startsWith('--')) continue;
215
+ const eq = arg.indexOf('=');
216
+ if (eq !== -1) flags.set(arg.slice(2, eq), arg.slice(eq + 1));
217
+ else if (!argv[index + 1] || argv[index + 1].startsWith('--')) flags.set(arg.slice(2), true);
218
+ else {
219
+ flags.set(arg.slice(2), argv[index + 1]);
220
+ index += 1;
221
+ }
222
+ }
223
+ return flags;
224
+ }
225
+
226
+ function getFlag(flags, names) {
227
+ for (const name of names) if (flags.has(name)) return flags.get(name);
228
+ return undefined;
229
+ }
230
+
231
+ async function main() {
232
+ const flags = parseArgs();
233
+ const site = getFlag(flags, ['site', 'site-dir', 'siteDir']) ?? '_public_site';
234
+ const result = await validatePublicSiteSecurity({ site }, { generated_at: new Date().toISOString() });
235
+ const out = getFlag(flags, ['out']);
236
+ const text = `${JSON.stringify(result, null, 2)}\n`;
237
+ if (out && out !== true) {
238
+ const outPath = resolve(String(out));
239
+ await mkdir(dirname(outPath), { recursive: true });
240
+ await writeFile(outPath, text, 'utf8');
241
+ }
242
+ process.stdout.write(text);
243
+ process.exitCode = result.ok ? 0 : 1;
244
+ }
245
+
246
+ const invokedPath = process.argv[1] ? resolve(process.argv[1]) : '';
247
+ if (invokedPath === fileURLToPath(import.meta.url)) {
248
+ main().catch((error) => {
249
+ process.stderr.write(`${error.name ?? 'Error'}: ${error.message}\n`);
250
+ process.exitCode = 1;
251
+ });
252
+ }
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ export const SECURITY_THREAT_MODEL_REVIEW_SCHEMA = 'enigma.security_threat_model_review.v1';
7
+ export const SECURITY_THREAT_MODEL_RESULT_SCHEMA = 'enigma.security_threat_model_result.v1';
8
+
9
+ export const REQUIRED_ASSET_IDS = Object.freeze([
10
+ 'local_vault',
11
+ 'mcp_server',
12
+ 'native_host',
13
+ 'relay',
14
+ 'gateway',
15
+ 'optimizer',
16
+ 'metering',
17
+ 'settlement',
18
+ 'public_site',
19
+ 'domain_tls',
20
+ 'kms_custody',
21
+ 'durable_storage',
22
+ 'siem_export',
23
+ 'backup_restore',
24
+ ]);
25
+
26
+ export const REQUIRED_BOUNDARY_IDS = Object.freeze([
27
+ 'local_device',
28
+ 'browser_extension',
29
+ 'mcp_client',
30
+ 'provider_page',
31
+ 'relay_api',
32
+ 'gateway_api',
33
+ 'operator_admin',
34
+ 'cloud_provider',
35
+ 'public_site',
36
+ ]);
37
+
38
+ export const REQUIRED_NON_CLAIM_IDS = Object.freeze([
39
+ 'provider_deletion',
40
+ 'model_forgetting',
41
+ 'token_roi_profit_equity',
42
+ 'compliance_certification',
43
+ 'tamper_proof_hardware',
44
+ 'provider_invoice_savings',
45
+ ]);
46
+
47
+ const ACCEPTED_STATUSES = new Set(['approved', 'accepted', 'go', 'verified']);
48
+ const ACCEPTED_RISK_STATUSES = new Set(['mitigated', 'accepted', 'transferred', 'blocked']);
49
+ const SECRET_VALUE_RE = /(?:Bearer\s+[A-Za-z0-9._~+/=-]{12,}|Basic\s+[A-Za-z0-9+/=-]{12,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|https?:\/\/[^\s/@]+:[^\s/@]+@|raw memory|private prompt|full transcript|decrypted capsule|sk-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16})/iu;
50
+ const FORBIDDEN_KEY_RE = /(?:password|passwd|pwd|token|api[_-]?key|private[_-]?key|secret|raw[_-]?memory|plaintext|plain[_-]?text|prompt|completion|transcript|provider[_-]?response|cookie|session)/iu;
51
+ const SAFE_FIELD_NAMES = new Set(['token_roi_profit_equity']);
52
+
53
+ function isPlainObject(value) {
54
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
55
+ }
56
+
57
+ function nonEmptyString(value) {
58
+ return typeof value === 'string' && value.trim().length > 0;
59
+ }
60
+
61
+ function isoLike(value) {
62
+ return nonEmptyString(value) && !Number.isNaN(Date.parse(value));
63
+ }
64
+
65
+ function blocker(message, path) {
66
+ return { message, path };
67
+ }
68
+
69
+ function assertNoSensitivePayload(value, path = 'threat_model') {
70
+ if (typeof value === 'string') {
71
+ if (!/\.(claim_boundary\[\d+\]|blockers\[\d+\]\.message)$/.test(path) && SECRET_VALUE_RE.test(value)) throw new Error(`${path} contains secret-looking data`);
72
+ return;
73
+ }
74
+ if (Array.isArray(value)) {
75
+ value.forEach((item, index) => assertNoSensitivePayload(item, `${path}[${index}]`));
76
+ return;
77
+ }
78
+ if (!isPlainObject(value)) return;
79
+ for (const [key, child] of Object.entries(value)) {
80
+ const childPath = `${path}.${key}`;
81
+ if (FORBIDDEN_KEY_RE.test(key) && !SAFE_FIELD_NAMES.has(key) && !/^result\.blockers\[\d+\]\.message$/.test(childPath)) throw new Error(`${childPath} is not allowed in security threat model evidence`);
82
+ assertNoSensitivePayload(child, childPath);
83
+ }
84
+ }
85
+
86
+ function statusAccepted(value) {
87
+ return typeof value === 'string' && ACCEPTED_STATUSES.has(value.trim().toLowerCase());
88
+ }
89
+
90
+ function validateMetadata(metadata, blockers) {
91
+ if (!isPlainObject(metadata)) {
92
+ blockers.push(blocker('metadata is required', 'metadata'));
93
+ return;
94
+ }
95
+ for (const field of ['review_id', 'environment', 'tenant', 'owner', 'reviewer', 'approved_at', 'approval_ref', 'status']) {
96
+ if (!nonEmptyString(metadata[field])) blockers.push(blocker(`metadata.${field} is required`, `metadata.${field}`));
97
+ }
98
+ if (!statusAccepted(metadata.status)) blockers.push(blocker('metadata.status must be approved/accepted/go/verified', 'metadata.status'));
99
+ if (!isoLike(metadata.approved_at)) blockers.push(blocker('metadata.approved_at must be ISO time', 'metadata.approved_at'));
100
+ }
101
+
102
+ function validateRefs(refs, blockers) {
103
+ if (!isPlainObject(refs)) {
104
+ blockers.push(blocker('source_refs object is required', 'source_refs'));
105
+ return false;
106
+ }
107
+ for (const field of ['security_policy_ref', 'threat_model_ref', 'public_api_ref', 'operator_acceptance_ref']) {
108
+ if (!nonEmptyString(refs[field])) blockers.push(blocker(`source_refs.${field} is required`, `source_refs.${field}`));
109
+ }
110
+ return true;
111
+ }
112
+
113
+ function validateIdCoverage(entries, requiredIds, kind, blockers) {
114
+ if (!Array.isArray(entries)) {
115
+ blockers.push(blocker(`${kind} must be an array`, kind));
116
+ return { checked: 0, covered: 0 };
117
+ }
118
+ const ids = new Set();
119
+ entries.forEach((entry, index) => {
120
+ const path = `${kind}[${index}]`;
121
+ if (!isPlainObject(entry)) {
122
+ blockers.push(blocker(`${kind} entry must be object`, path));
123
+ return;
124
+ }
125
+ if (!nonEmptyString(entry.id)) blockers.push(blocker(`${path}.id is required`, `${path}.id`));
126
+ else ids.add(entry.id);
127
+ for (const field of ['owner', 'evidence_ref', 'status']) {
128
+ if (!nonEmptyString(entry[field])) blockers.push(blocker(`${path}.${field} is required`, `${path}.${field}`));
129
+ }
130
+ if (!statusAccepted(entry.status)) blockers.push(blocker(`${path}.status must be approved/accepted/go/verified`, `${path}.status`));
131
+ });
132
+ for (const id of requiredIds) if (!ids.has(id)) blockers.push(blocker(`${kind} must include ${id}`, kind));
133
+ return { checked: entries.length, covered: requiredIds.filter((id) => ids.has(id)).length };
134
+ }
135
+
136
+ function validateRisks(risks, blockers) {
137
+ if (!Array.isArray(risks) || risks.length === 0) {
138
+ blockers.push(blocker('risks must be non-empty array', 'risks'));
139
+ return { checked: 0, mitigated: 0, accepted: 0, blocked: 0 };
140
+ }
141
+ let mitigated = 0;
142
+ let accepted = 0;
143
+ let blocked = 0;
144
+ risks.forEach((risk, index) => {
145
+ const path = `risks[${index}]`;
146
+ if (!isPlainObject(risk)) {
147
+ blockers.push(blocker('risk entry must be object', path));
148
+ return;
149
+ }
150
+ for (const field of ['id', 'asset_id', 'boundary_id', 'adversary', 'abuse_case', 'control_ref', 'evidence_ref', 'owner', 'status']) {
151
+ if (!nonEmptyString(risk[field])) blockers.push(blocker(`${path}.${field} is required`, `${path}.${field}`));
152
+ }
153
+ const status = String(risk.status ?? '').trim().toLowerCase();
154
+ if (!ACCEPTED_RISK_STATUSES.has(status)) blockers.push(blocker(`${path}.status must be mitigated/accepted/transferred/blocked`, `${path}.status`));
155
+ if (status === 'mitigated') mitigated += 1;
156
+ if (status === 'accepted' || status === 'transferred') accepted += 1;
157
+ if (status === 'blocked') blocked += 1;
158
+ if (!Array.isArray(risk.tests) || risk.tests.length === 0 || risk.tests.some((test) => !nonEmptyString(test))) blockers.push(blocker(`${path}.tests must be non-empty string array`, `${path}.tests`));
159
+ });
160
+ if (blocked > 0) blockers.push(blocker('threat model contains blocked risks', 'risks'));
161
+ return { checked: risks.length, mitigated, accepted, blocked };
162
+ }
163
+
164
+ function validateNonClaims(nonClaims, blockers) {
165
+ if (!Array.isArray(nonClaims)) {
166
+ blockers.push(blocker('non_claims must be an array', 'non_claims'));
167
+ return { checked: 0, covered: 0 };
168
+ }
169
+ const ids = new Set();
170
+ nonClaims.forEach((entry, index) => {
171
+ const path = `non_claims[${index}]`;
172
+ if (!isPlainObject(entry)) {
173
+ blockers.push(blocker('non-claim entry must be object', path));
174
+ return;
175
+ }
176
+ if (!nonEmptyString(entry.id)) blockers.push(blocker(`${path}.id is required`, `${path}.id`));
177
+ else ids.add(entry.id);
178
+ if (entry.claimed !== false) blockers.push(blocker(`${path}.claimed must be false`, `${path}.claimed`));
179
+ if (!nonEmptyString(entry.evidence_ref)) blockers.push(blocker(`${path}.evidence_ref is required`, `${path}.evidence_ref`));
180
+ });
181
+ for (const id of REQUIRED_NON_CLAIM_IDS) if (!ids.has(id)) blockers.push(blocker(`non_claims must include ${id}`, 'non_claims'));
182
+ return { checked: nonClaims.length, covered: REQUIRED_NON_CLAIM_IDS.filter((id) => ids.has(id)).length };
183
+ }
184
+
185
+ function validateReviewCadence(cadence, blockers) {
186
+ if (!isPlainObject(cadence)) {
187
+ blockers.push(blocker('review_cadence object is required', 'review_cadence'));
188
+ return false;
189
+ }
190
+ for (const field of ['next_review_at', 'trigger_ref', 'owner']) {
191
+ if (!nonEmptyString(cadence[field])) blockers.push(blocker(`review_cadence.${field} is required`, `review_cadence.${field}`));
192
+ }
193
+ if (!isoLike(cadence.next_review_at)) blockers.push(blocker('review_cadence.next_review_at must be ISO time', 'review_cadence.next_review_at'));
194
+ return true;
195
+ }
196
+
197
+ export function validateSecurityThreatModel(review, options = {}) {
198
+ if (!isPlainObject(review)) throw new Error('security threat model review must be an object');
199
+ assertNoSensitivePayload(review);
200
+ const blockers = [];
201
+ if (review.schema !== SECURITY_THREAT_MODEL_REVIEW_SCHEMA) blockers.push(blocker('schema mismatch', 'schema'));
202
+ validateMetadata(review.metadata, blockers);
203
+ const sourceRefs = validateRefs(review.source_refs, blockers);
204
+ const assets = validateIdCoverage(review.assets, REQUIRED_ASSET_IDS, 'assets', blockers);
205
+ const boundaries = validateIdCoverage(review.trust_boundaries, REQUIRED_BOUNDARY_IDS, 'trust_boundaries', blockers);
206
+ const risks = validateRisks(review.risks, blockers);
207
+ const nonClaims = validateNonClaims(review.non_claims, blockers);
208
+ const cadence = validateReviewCadence(review.review_cadence, blockers);
209
+ const result = {
210
+ schema: SECURITY_THREAT_MODEL_RESULT_SCHEMA,
211
+ generated_at: options.generated_at ?? options.generatedAt ?? new Date(0).toISOString(),
212
+ ok: blockers.length === 0,
213
+ status: blockers.length === 0 ? 'accepted' : 'blocked',
214
+ blockers,
215
+ checked: {
216
+ source_refs: sourceRefs,
217
+ required_assets: REQUIRED_ASSET_IDS.length,
218
+ assets_covered: assets.covered,
219
+ assets: assets.checked,
220
+ required_boundaries: REQUIRED_BOUNDARY_IDS.length,
221
+ boundaries_covered: boundaries.covered,
222
+ boundaries: boundaries.checked,
223
+ risks: risks.checked,
224
+ mitigated_risks: risks.mitigated,
225
+ accepted_risks: risks.accepted,
226
+ blocked_risks: risks.blocked,
227
+ required_non_claims: REQUIRED_NON_CLAIM_IDS.length,
228
+ non_claims_covered: nonClaims.covered,
229
+ review_cadence: cadence,
230
+ },
231
+ claim_boundary: [
232
+ 'Security threat model validation checks declared review evidence only; it is not SOC 2, HIPAA, GDPR, penetration-test, or compliance certification evidence.',
233
+ 'A pass result does not claim provider deletion, model forgetting, token ROI/profit/equity, tamper-proof hardware, or provider invoice savings.',
234
+ 'Threat model artifacts must not include raw memory, prompts, transcripts, provider responses, credentials, private keys, cookies, or sessions.',
235
+ ],
236
+ };
237
+ assertNoSensitivePayload(result, 'result');
238
+ return result;
239
+ }
240
+
241
+ function parseArgs(argv = process.argv.slice(2)) {
242
+ const flags = new Map();
243
+ for (let index = 0; index < argv.length; index += 1) {
244
+ const arg = argv[index];
245
+ if (!arg.startsWith('--')) continue;
246
+ const eq = arg.indexOf('=');
247
+ if (eq !== -1) flags.set(arg.slice(2, eq), arg.slice(eq + 1));
248
+ else if (!argv[index + 1] || argv[index + 1].startsWith('--')) flags.set(arg.slice(2), true);
249
+ else {
250
+ flags.set(arg.slice(2), argv[index + 1]);
251
+ index += 1;
252
+ }
253
+ }
254
+ return flags;
255
+ }
256
+
257
+ function getFlag(flags, names) {
258
+ for (const name of names) if (flags.has(name)) return flags.get(name);
259
+ return undefined;
260
+ }
261
+
262
+ async function main() {
263
+ const flags = parseArgs();
264
+ const reviewPath = getFlag(flags, ['review', 'threat-model', 'in']);
265
+ if (typeof reviewPath !== 'string' || reviewPath.trim() === '') throw new Error('--review <path> is required');
266
+ const review = JSON.parse(await readFile(resolve(reviewPath), 'utf8'));
267
+ const result = validateSecurityThreatModel(review, { generated_at: new Date().toISOString() });
268
+ const out = getFlag(flags, ['out']);
269
+ const text = `${JSON.stringify(result, null, 2)}\n`;
270
+ if (out && out !== true) {
271
+ const outPath = resolve(String(out));
272
+ await mkdir(dirname(outPath), { recursive: true });
273
+ await writeFile(outPath, text, 'utf8');
274
+ }
275
+ process.stdout.write(text);
276
+ process.exitCode = result.ok ? 0 : 1;
277
+ }
278
+
279
+ const invokedPath = process.argv[1] ? resolve(process.argv[1]) : '';
280
+ if (invokedPath === fileURLToPath(import.meta.url)) {
281
+ main().catch((error) => {
282
+ process.stderr.write(`${error.name ?? 'Error'}: ${error.message}\n`);
283
+ process.exitCode = 1;
284
+ });
285
+ }