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.
- package/LICENSE +55 -0
- package/README.md +379 -0
- package/apps/cli/bin/enigma.mjs +1117 -0
- package/apps/desktop/src/app.js +1327 -0
- package/apps/desktop/src/index.html +1373 -0
- package/apps/desktop/src/styles.css +386 -0
- package/apps/gateway/bin/enigma-gateway.mjs +52 -0
- package/apps/gateway/src/server.mjs +1403 -0
- package/apps/native-host/README.md +265 -0
- package/apps/native-host/bin/enigma-native-host.mjs +499 -0
- package/apps/native-host/manifests/com.enigma.native_host.chrome.json +9 -0
- package/apps/native-host/manifests/com.enigma.native_host.edge.json +9 -0
- package/apps/native-host/manifests/com.enigma.native_host.firefox.json +9 -0
- package/apps/relay/bin/enigma-relay.mjs +52 -0
- package/apps/relay/src/server.mjs +1571 -0
- package/apps/verifier/bin/enigma-verify.mjs +240 -0
- package/deploy/docker-compose.production.example.yml +150 -0
- package/deploy/kubernetes/enigma-backend.example.yaml +580 -0
- package/package.json +196 -0
- package/packages/adapters/PACKAGE_CONTRACT.md +47 -0
- package/packages/adapters/src/index.js +183 -0
- package/packages/boundary/src/index.js +468 -0
- package/packages/connectors/src/index.js +533 -0
- package/packages/core/src/index.js +766 -0
- package/packages/enterprise/src/index.js +505 -0
- package/packages/importers/src/index.js +1083 -0
- package/packages/mcp-server/bin/enigma-mcp.mjs +19 -0
- package/packages/mcp-server/src/index.js +1185 -0
- package/packages/mesh/src/index.js +531 -0
- package/packages/metering/PACKAGE_CONTRACT.md +20 -0
- package/packages/metering/src/index.js +246 -0
- package/packages/optimizer/src/index.js +707 -0
- package/packages/passport/src/index.js +651 -0
- package/packages/settlement/PACKAGE_CONTRACT.md +28 -0
- package/packages/settlement/src/index.js +369 -0
- package/packages/storage/PACKAGE_CONTRACT.md +23 -0
- package/packages/storage/src/index.js +392 -0
- package/packages/vault/src/index.js +904 -0
- package/scripts/build-ai-orchestration-plan.mjs +248 -0
- package/scripts/build-cloudflare-pages-release-packet.mjs +287 -0
- package/scripts/build-cloudflare-token-policy.mjs +239 -0
- package/scripts/build-cloudflare-token-request.mjs +314 -0
- package/scripts/build-demo-assets.mjs +263 -0
- package/scripts/build-edge-backend-workers.mjs +353 -0
- package/scripts/build-goal-completion-audit.mjs +398 -0
- package/scripts/build-hosted-probe-worker.mjs +191 -0
- package/scripts/build-operator-acceptance-packet.mjs +254 -0
- package/scripts/build-operator-evidence-starter.mjs +525 -0
- package/scripts/build-production-backend-env-kit.mjs +384 -0
- package/scripts/build-production-dependency-report.mjs +369 -0
- package/scripts/build-production-handoff-packet.mjs +475 -0
- package/scripts/build-production-readiness-manifest.mjs +209 -0
- package/scripts/build-production-status-board.mjs +297 -0
- package/scripts/build-production-storage-migration.mjs +86 -0
- package/scripts/build-production-workplan.mjs +371 -0
- package/scripts/build-review-packet.mjs +594 -0
- package/scripts/check.mjs +419 -0
- package/scripts/cloudflare-ops.mjs +1672 -0
- package/scripts/cloudflare-secret-env.mjs +128 -0
- package/scripts/collect-hosted-backend-live-evidence.mjs +263 -0
- package/scripts/infrastructure-readiness.mjs +944 -0
- package/scripts/install-enigma-local.mjs +270 -0
- package/scripts/memory-optimization-benchmark.mjs +242 -0
- package/scripts/monitor-live-endpoints.mjs +358 -0
- package/scripts/release-audit.mjs +5588 -0
- package/scripts/release-provenance.mjs +250 -0
- package/scripts/run-backend-readiness-smoke.mjs +220 -0
- package/scripts/validate-backup-restore-drill.mjs +166 -0
- package/scripts/validate-cloudflare-credentials.mjs +112 -0
- package/scripts/validate-cloudflare-worker-inspect.mjs +179 -0
- package/scripts/validate-domain-tls.mjs +248 -0
- package/scripts/validate-hosted-backend-live.mjs +319 -0
- package/scripts/validate-incident-drill.mjs +221 -0
- package/scripts/validate-kms-custody.mjs +235 -0
- package/scripts/validate-legal-compliance-approval.mjs +274 -0
- package/scripts/validate-monitoring-alerting.mjs +263 -0
- package/scripts/validate-network-access-policy.mjs +285 -0
- package/scripts/validate-operator-acceptance.mjs +298 -0
- package/scripts/validate-production-manifests.mjs +325 -0
- package/scripts/validate-public-site-security.mjs +252 -0
- package/scripts/validate-security-threat-model.mjs +285 -0
- package/scripts/validate-service-settlement.mjs +256 -0
- package/scripts/validate-support-sla.mjs +190 -0
- package/scripts/validate-tenant-policy-approval.mjs +228 -0
- package/scripts/validate-usage-metering.mjs +224 -0
- package/scripts/validate-whitepaper-claims.mjs +166 -0
- package/specs/boundary-manifest-v1.schema.json +35 -0
- package/specs/capsule-v1.schema.json +55 -0
- package/specs/claim-boundary-manifest-v1.schema.json +22 -0
- package/specs/connector-profile-v1.schema.json +62 -0
- package/specs/deletion-tombstone-v1.schema.json +26 -0
- package/specs/enterprise-policy-v1.schema.json +59 -0
- package/specs/gateway-request-v1.schema.json +32 -0
- package/specs/goal-completion-audit-v1.schema.json +70 -0
- package/specs/hosted-backend-live-evidence-v1.schema.json +147 -0
- package/specs/import-report-v1.schema.json +214 -0
- package/specs/infrastructure-readiness-manifest-v1.schema.json +130 -0
- package/specs/memory-event-v1.schema.json +42 -0
- package/specs/mesh-node-v1.schema.json +126 -0
- package/specs/passport-v1.schema.json +50 -0
- package/specs/production-handoff-packet-v1.schema.json +149 -0
- package/specs/receipt-v1.schema.json +61 -0
- package/specs/relay-record-v1.schema.json +43 -0
- package/specs/state-checkpoint-v1.schema.json +37 -0
- package/specs/trust-bundle-v1.schema.json +56 -0
|
@@ -0,0 +1,319 @@
|
|
|
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 HOSTED_BACKEND_LIVE_EVIDENCE_SCHEMA = 'enigma.hosted_backend_live_evidence.v1';
|
|
7
|
+
export const HOSTED_BACKEND_LIVE_RESULT_SCHEMA = 'enigma.hosted_backend_live_result.v1';
|
|
8
|
+
|
|
9
|
+
export const REQUIRED_REF_KEYS = Object.freeze([
|
|
10
|
+
'backend_host',
|
|
11
|
+
'dns_tls',
|
|
12
|
+
'durable_storage',
|
|
13
|
+
'kms_or_secret_custody',
|
|
14
|
+
'backup_restore',
|
|
15
|
+
'monitoring',
|
|
16
|
+
'siem_or_log_sink',
|
|
17
|
+
'operator_acceptance',
|
|
18
|
+
'runtime_auth',
|
|
19
|
+
'admin_auth',
|
|
20
|
+
'data_plane_auth',
|
|
21
|
+
'network_access_policy',
|
|
22
|
+
'kms_custody',
|
|
23
|
+
'tenant_policy_approval',
|
|
24
|
+
'usage_metering',
|
|
25
|
+
'service_settlement',
|
|
26
|
+
'monitoring_alerting',
|
|
27
|
+
'public_site_security',
|
|
28
|
+
'security_threat_model',
|
|
29
|
+
'legal_compliance_approval',
|
|
30
|
+
'support_sla',
|
|
31
|
+
'incident_drill',
|
|
32
|
+
'backup_restore_drill',
|
|
33
|
+
'relay_deployment',
|
|
34
|
+
'gateway_deployment',
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const REQUIRED_PROBES = Object.freeze(['relay_livez', 'relay_readyz', 'gateway_livez', 'gateway_readyz']);
|
|
38
|
+
const ACCEPTED_STATUSES = new Set(['observed', 'verified', 'go', 'accepted']);
|
|
39
|
+
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;
|
|
40
|
+
const FORBIDDEN_KEY_RE = /(?:password|passwd|pwd|token|api[_-]?key|private[_-]?key|secret|raw[_-]?memory|plaintext|plain[_-]?text|prompt|completion|transcript|embedding|provider[_-]?response|cookie|session)/iu;
|
|
41
|
+
const TEMPLATE_PLACEHOLDER_RE = /<[^<>\r\n]+>/u;
|
|
42
|
+
const SAFE_FIELD_NAMES = new Set(['cloudflare_token_policy_ref', 'token_policy_ref', 'token_value_printed', 'kms_or_secret_custody', 'token_roi_claim', 'cloudflare_credentials_claim', 'provider_deletion_claim']);
|
|
43
|
+
|
|
44
|
+
function isPlainObject(value) {
|
|
45
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function nonEmptyString(value) {
|
|
49
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isoLike(value) {
|
|
53
|
+
return nonEmptyString(value) && !Number.isNaN(Date.parse(value));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function blocker(message, path) {
|
|
57
|
+
return { message, path };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function assertNoSensitivePayload(value, path = 'hosted_backend_live') {
|
|
61
|
+
if (typeof value === 'string') {
|
|
62
|
+
if (!/\.(claim_boundary\[\d+\]|blockers\[\d+\]\.message)$/.test(path) && SECRET_VALUE_RE.test(value)) throw new Error(`${path} contains secret-looking data`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
value.forEach((item, index) => assertNoSensitivePayload(item, `${path}[${index}]`));
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!isPlainObject(value)) return;
|
|
70
|
+
for (const [key, child] of Object.entries(value)) {
|
|
71
|
+
const childPath = `${path}.${key}`;
|
|
72
|
+
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 hosted backend live evidence`);
|
|
73
|
+
assertNoSensitivePayload(child, childPath);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function collectTemplatePlaceholderBlockers(value, blockers, path = 'hosted_backend_live') {
|
|
78
|
+
if (typeof value === 'string') {
|
|
79
|
+
if (TEMPLATE_PLACEHOLDER_RE.test(value)) blockers.push(blocker(`${path} must not contain unresolved template placeholders`, path));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
value.forEach((item, index) => collectTemplatePlaceholderBlockers(item, blockers, `${path}[${index}]`));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!isPlainObject(value)) return;
|
|
87
|
+
for (const [key, child] of Object.entries(value)) collectTemplatePlaceholderBlockers(child, blockers, `${path}.${key}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeDomain(value) {
|
|
91
|
+
if (!nonEmptyString(value)) return null;
|
|
92
|
+
const domain = value.trim().toLowerCase().replace(/\.$/, '');
|
|
93
|
+
return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+$/.test(domain) ? domain : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isPrivateHost(hostname) {
|
|
97
|
+
const host = String(hostname ?? '').toLowerCase();
|
|
98
|
+
return host === 'localhost'
|
|
99
|
+
|| host.endsWith('.localhost')
|
|
100
|
+
|| host === '127.0.0.1'
|
|
101
|
+
|| host === '0.0.0.0'
|
|
102
|
+
|| host === '::1'
|
|
103
|
+
|| /^10\./.test(host)
|
|
104
|
+
|| /^192\.168\./.test(host)
|
|
105
|
+
|| /^172\.(1[6-9]|2\d|3[0-1])\./.test(host);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function httpsHealthUrl(value, path, blockers) {
|
|
109
|
+
if (!nonEmptyString(value)) {
|
|
110
|
+
blockers.push(blocker(`${path} is required`, path));
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const url = new URL(value);
|
|
115
|
+
if (url.protocol !== 'https:') blockers.push(blocker(`${path} must use https`, path));
|
|
116
|
+
if (url.username || url.password) blockers.push(blocker(`${path} must not include credentials`, path));
|
|
117
|
+
if (url.search || url.hash) blockers.push(blocker(`${path} must not include query strings or fragments`, path));
|
|
118
|
+
if (isPrivateHost(url.hostname)) blockers.push(blocker(`${path} must not target localhost or private network host`, path));
|
|
119
|
+
if (url.pathname !== '/livez' && url.pathname !== '/readyz') blockers.push(blocker(`${path} must use /livez or /readyz`, path));
|
|
120
|
+
return url;
|
|
121
|
+
} catch {
|
|
122
|
+
blockers.push(blocker(`${path} must be a valid URL`, path));
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function statusAccepted(value) {
|
|
128
|
+
return typeof value === 'string' && ACCEPTED_STATUSES.has(value.trim().toLowerCase());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function validateRefs(refs, blockers) {
|
|
132
|
+
if (!isPlainObject(refs)) {
|
|
133
|
+
blockers.push(blocker('refs object is required', 'refs'));
|
|
134
|
+
return { present: 0, missing: REQUIRED_REF_KEYS.length };
|
|
135
|
+
}
|
|
136
|
+
let present = 0;
|
|
137
|
+
for (const key of REQUIRED_REF_KEYS) {
|
|
138
|
+
if (!nonEmptyString(refs[key])) blockers.push(blocker(`refs.${key} is required`, `refs.${key}`));
|
|
139
|
+
else present += 1;
|
|
140
|
+
}
|
|
141
|
+
return { present, missing: REQUIRED_REF_KEYS.length - present };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function validateEnvironment(environment, blockers) {
|
|
145
|
+
if (!isPlainObject(environment)) {
|
|
146
|
+
blockers.push(blocker('environment object is required', 'environment'));
|
|
147
|
+
return { domain: null };
|
|
148
|
+
}
|
|
149
|
+
const domain = normalizeDomain(environment.domain);
|
|
150
|
+
if (!domain) blockers.push(blocker('environment.domain must be a valid domain', 'environment.domain'));
|
|
151
|
+
for (const field of ['environment_id', 'cloud_provider', 'region', 'owner', 'status']) {
|
|
152
|
+
if (!nonEmptyString(environment[field])) blockers.push(blocker(`environment.${field} is required`, `environment.${field}`));
|
|
153
|
+
}
|
|
154
|
+
if (!statusAccepted(environment.status)) blockers.push(blocker('environment.status must be observed/verified/go/accepted', 'environment.status'));
|
|
155
|
+
return { domain };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function expectedServiceForProbe(expectedName) {
|
|
159
|
+
return expectedName.startsWith('relay_') ? 'enigma-relay' : 'enigma-gateway';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function validateProbe(probe, expectedName, domain, blockers) {
|
|
163
|
+
const path = `probes.${expectedName}`;
|
|
164
|
+
if (!isPlainObject(probe)) {
|
|
165
|
+
blockers.push(blocker(`${path} object is required`, path));
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
const url = httpsHealthUrl(probe.url, `${path}.url`, blockers);
|
|
169
|
+
if (url && domain) {
|
|
170
|
+
const hostname = url.hostname.toLowerCase();
|
|
171
|
+
if (hostname !== domain && !hostname.endsWith(`.${domain}`)) blockers.push(blocker(`${path}.url host must be the domain or a subdomain`, `${path}.url`));
|
|
172
|
+
if (expectedName.endsWith('livez') && url.pathname !== '/livez') blockers.push(blocker(`${path}.url must end in /livez`, `${path}.url`));
|
|
173
|
+
if (expectedName.endsWith('readyz') && url.pathname !== '/readyz') blockers.push(blocker(`${path}.url must end in /readyz`, `${path}.url`));
|
|
174
|
+
}
|
|
175
|
+
if (!Number.isSafeInteger(probe.status_code) || probe.status_code !== 200) blockers.push(blocker(`${path}.status_code must be 200`, `${path}.status_code`));
|
|
176
|
+
if (!isPlainObject(probe.body)) blockers.push(blocker(`${path}.body object is required`, `${path}.body`));
|
|
177
|
+
else {
|
|
178
|
+
const expectedService = expectedServiceForProbe(expectedName);
|
|
179
|
+
if (probe.body.hosted_probe_only === true || probe.body.pages_edge_probe_only === true) {
|
|
180
|
+
blockers.push(blocker(`${path}.body must not be an edge-probe-only payload`, `${path}.body`));
|
|
181
|
+
}
|
|
182
|
+
if (probe.body.service !== expectedService) blockers.push(blocker(`${path}.body.service must be ${expectedService}`, `${path}.body.service`));
|
|
183
|
+
if (probe.body.ok !== true) blockers.push(blocker(`${path}.body.ok must be true`, `${path}.body.ok`));
|
|
184
|
+
if (expectedName.endsWith('readyz')) {
|
|
185
|
+
if (Array.isArray(probe.body.missing_evidence_refs) && probe.body.missing_evidence_refs.length !== 0) blockers.push(blocker(`${path}.body.missing_evidence_refs must be empty`, `${path}.body.missing_evidence_refs`));
|
|
186
|
+
if (Array.isArray(probe.body.checks) && probe.body.checks.some((check) => check?.ok !== true)) blockers.push(blocker(`${path}.body.checks must all be ok`, `${path}.body.checks`));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (!isoLike(probe.observed_at)) blockers.push(blocker(`${path}.observed_at must be ISO time`, `${path}.observed_at`));
|
|
190
|
+
if (!nonEmptyString(probe.response_hash) || !/^sha256:[a-f0-9]{64}$/i.test(probe.response_hash)) blockers.push(blocker(`${path}.response_hash must be sha256:<64 hex>`, `${path}.response_hash`));
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function validateProbes(probes, domain, blockers) {
|
|
195
|
+
if (!isPlainObject(probes)) {
|
|
196
|
+
blockers.push(blocker('probes object is required', 'probes'));
|
|
197
|
+
return { covered: 0 };
|
|
198
|
+
}
|
|
199
|
+
let covered = 0;
|
|
200
|
+
for (const name of REQUIRED_PROBES) {
|
|
201
|
+
if (validateProbe(probes[name], name, domain, blockers)) covered += 1;
|
|
202
|
+
}
|
|
203
|
+
return { covered };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function validateOperator(operatorAcceptance, blockers) {
|
|
207
|
+
if (!isPlainObject(operatorAcceptance)) {
|
|
208
|
+
blockers.push(blocker('operator_acceptance object is required', 'operator_acceptance'));
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
if (operatorAcceptance.decision !== 'go') blockers.push(blocker('operator_acceptance.decision must be go', 'operator_acceptance.decision'));
|
|
212
|
+
if (!nonEmptyString(operatorAcceptance.packet_ref)) blockers.push(blocker('operator_acceptance.packet_ref is required', 'operator_acceptance.packet_ref'));
|
|
213
|
+
if (!isoLike(operatorAcceptance.approved_at)) blockers.push(blocker('operator_acceptance.approved_at must be ISO time', 'operator_acceptance.approved_at'));
|
|
214
|
+
if (!nonEmptyString(operatorAcceptance.approved_by)) blockers.push(blocker('operator_acceptance.approved_by is required', 'operator_acceptance.approved_by'));
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function validateClaimBoundary(boundary, blockers) {
|
|
219
|
+
if (!isPlainObject(boundary)) {
|
|
220
|
+
blockers.push(blocker('claim_boundary object is required', 'claim_boundary'));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const required = ['hosted_backend_live', 'public_site_live', 'cloudflare_credentials_claim', 'token_roi_claim', 'provider_deletion_claim', 'model_forgetting_claim'];
|
|
224
|
+
for (const field of required) if (typeof boundary[field] !== 'boolean') blockers.push(blocker(`claim_boundary.${field} must be boolean`, `claim_boundary.${field}`));
|
|
225
|
+
if (boundary.hosted_backend_live !== true) blockers.push(blocker('claim_boundary.hosted_backend_live must be true', 'claim_boundary.hosted_backend_live'));
|
|
226
|
+
for (const field of required.filter((item) => item !== 'hosted_backend_live')) {
|
|
227
|
+
if (boundary[field] !== false) blockers.push(blocker(`claim_boundary.${field} must be false`, `claim_boundary.${field}`));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function validateHostedBackendLiveEvidence(evidence, options = {}) {
|
|
232
|
+
if (!isPlainObject(evidence)) throw new Error('hosted backend live evidence must be an object');
|
|
233
|
+
assertNoSensitivePayload(evidence);
|
|
234
|
+
const blockers = [];
|
|
235
|
+
collectTemplatePlaceholderBlockers(evidence, blockers);
|
|
236
|
+
if (evidence.schema !== HOSTED_BACKEND_LIVE_EVIDENCE_SCHEMA) blockers.push(blocker('schema mismatch', 'schema'));
|
|
237
|
+
if (!isoLike(evidence.observed_at)) blockers.push(blocker('observed_at must be ISO time', 'observed_at'));
|
|
238
|
+
const env = validateEnvironment(evidence.environment, blockers);
|
|
239
|
+
const refs = validateRefs(evidence.refs, blockers);
|
|
240
|
+
const probes = validateProbes(evidence.probes, env.domain, blockers);
|
|
241
|
+
validateOperator(evidence.operator_acceptance, blockers);
|
|
242
|
+
validateClaimBoundary(evidence.claim_boundary, blockers);
|
|
243
|
+
const result = {
|
|
244
|
+
schema: HOSTED_BACKEND_LIVE_RESULT_SCHEMA,
|
|
245
|
+
generated_at: options.generated_at ?? options.generatedAt ?? new Date(0).toISOString(),
|
|
246
|
+
ok: blockers.length === 0,
|
|
247
|
+
status: blockers.length === 0 ? 'accepted' : 'blocked',
|
|
248
|
+
blockers,
|
|
249
|
+
checked: {
|
|
250
|
+
domain: env.domain,
|
|
251
|
+
required_refs: REQUIRED_REF_KEYS.length,
|
|
252
|
+
refs_present: refs.present,
|
|
253
|
+
refs_missing: refs.missing,
|
|
254
|
+
required_probes: REQUIRED_PROBES.length,
|
|
255
|
+
probes_covered: probes.covered,
|
|
256
|
+
operator_decision: evidence.operator_acceptance?.decision ?? null,
|
|
257
|
+
},
|
|
258
|
+
claim_boundary: [
|
|
259
|
+
'Hosted backend live validation checks supplied evidence only; it does not deploy infrastructure, mutate Cloudflare, create DNS records, or generate credentials.',
|
|
260
|
+
'A pass result requires public HTTPS /livez and /readyz probe evidence for relay and gateway plus all required production refs and operator acceptance go.',
|
|
261
|
+
'Credentials, bearer tokens, API tokens, private memory payloads, prompts, transcripts, provider responses, and personal contact data must remain outside hosted backend live evidence.',
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
assertNoSensitivePayload(result, 'result');
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
269
|
+
const flags = new Map();
|
|
270
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
271
|
+
const arg = argv[index];
|
|
272
|
+
if (!arg.startsWith('--')) continue;
|
|
273
|
+
const eq = arg.indexOf('=');
|
|
274
|
+
if (eq !== -1) flags.set(arg.slice(2), arg.slice(eq + 1));
|
|
275
|
+
else if (!argv[index + 1] || argv[index + 1].startsWith('--')) flags.set(arg.slice(2), true);
|
|
276
|
+
else {
|
|
277
|
+
flags.set(arg.slice(2), argv[index + 1]);
|
|
278
|
+
index += 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return flags;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function readJson(path) {
|
|
285
|
+
return JSON.parse(await readFile(resolve(path), 'utf8'));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function runCli(argv = process.argv.slice(2)) {
|
|
289
|
+
const flags = parseArgs(argv);
|
|
290
|
+
if (flags.has('help') || !flags.has('evidence')) {
|
|
291
|
+
const help = 'Usage: node scripts/validate-hosted-backend-live.mjs --evidence <hosted-backend-live.json> [--out <result.json>]\n';
|
|
292
|
+
if (flags.has('help')) {
|
|
293
|
+
process.stdout.write(help);
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
process.stderr.write(help);
|
|
297
|
+
return 2;
|
|
298
|
+
}
|
|
299
|
+
const evidence = await readJson(flags.get('evidence'));
|
|
300
|
+
const result = validateHostedBackendLiveEvidence(evidence, { generated_at: new Date().toISOString() });
|
|
301
|
+
const json = `${JSON.stringify(result, null, 2)}\n`;
|
|
302
|
+
if (flags.has('out')) {
|
|
303
|
+
const outPath = resolve(flags.get('out'));
|
|
304
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
305
|
+
await writeFile(outPath, json, 'utf8');
|
|
306
|
+
}
|
|
307
|
+
process.stdout.write(json);
|
|
308
|
+
return result.ok ? 0 : 1;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
|
|
312
|
+
try {
|
|
313
|
+
process.exitCode = await runCli();
|
|
314
|
+
} catch (error) {
|
|
315
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
316
|
+
process.stdout.write(`${JSON.stringify({ schema: HOSTED_BACKEND_LIVE_RESULT_SCHEMA, ok: false, status: 'error', error: { message } }, null, 2)}\n`);
|
|
317
|
+
process.exitCode = 1;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
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 INCIDENT_DRILL_SCHEMA = 'enigma.incident_drill.v1';
|
|
7
|
+
export const INCIDENT_DRILL_RESULT_SCHEMA = 'enigma.incident_drill_result.v1';
|
|
8
|
+
|
|
9
|
+
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;
|
|
10
|
+
const SECRET_KEY_RE = /(?:password|passwd|pwd|token|api[_-]?key|private[_-]?key|secret|raw[_-]?memory|plaintext|plain[_-]?text|prompt|completion|transcript|embedding|provider[_-]?response)/iu;
|
|
11
|
+
const STATUS_PASS = new Set(['pass', 'passed', 'go', 'verified']);
|
|
12
|
+
const REQUIRED_CONTACT_ROLES = Object.freeze(['incident_commander', 'security', 'infrastructure', 'legal_privacy', 'customer_support']);
|
|
13
|
+
const REQUIRED_TIMELINE_EVENTS = Object.freeze(['detect', 'triage', 'contain', 'preserve_evidence', 'notify', 'recover', 'postmortem']);
|
|
14
|
+
const HASH_RE = /^sha256:[a-f0-9]{64}$/i;
|
|
15
|
+
|
|
16
|
+
function isPlainObject(value) {
|
|
17
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function nonEmptyString(value) {
|
|
21
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function assertNoSecrets(value, path = 'incident') {
|
|
25
|
+
if (typeof value === 'string') {
|
|
26
|
+
if (!/\.claim_boundary\[\d+\]$/.test(path) && SECRET_VALUE_RE.test(value)) throw new Error(`${path} contains secret or raw-memory-looking data`);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
value.forEach((item, index) => assertNoSecrets(item, `${path}[${index}]`));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!isPlainObject(value)) return;
|
|
34
|
+
for (const [key, child] of Object.entries(value)) {
|
|
35
|
+
if (SECRET_KEY_RE.test(key)) throw new Error(`${path}.${key} uses a forbidden field name`);
|
|
36
|
+
assertNoSecrets(child, `${path}.${key}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function blocker(message, path) {
|
|
41
|
+
return { message, path };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function iso(value) {
|
|
45
|
+
return nonEmptyString(value) && !Number.isNaN(Date.parse(value));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function hashOk(value) {
|
|
49
|
+
return nonEmptyString(value) && HASH_RE.test(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function nonNegative(value) {
|
|
53
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function roleSet(contacts) {
|
|
57
|
+
return new Set(Array.isArray(contacts) ? contacts.map((contact) => String(contact?.role ?? '')) : []);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function eventSet(events) {
|
|
61
|
+
return new Set(Array.isArray(events) ? events.map((event) => String(event?.event ?? '')) : []);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function validateIncidentDrill(drill, options = {}) {
|
|
65
|
+
if (!isPlainObject(drill)) throw new Error('incident drill must be an object');
|
|
66
|
+
assertNoSecrets(drill);
|
|
67
|
+
const blockers = [];
|
|
68
|
+
if (drill.schema !== INCIDENT_DRILL_SCHEMA) blockers.push(blocker('schema mismatch', 'schema'));
|
|
69
|
+
|
|
70
|
+
const metadata = drill.metadata;
|
|
71
|
+
if (!isPlainObject(metadata)) blockers.push(blocker('metadata is required', 'metadata'));
|
|
72
|
+
else {
|
|
73
|
+
for (const field of ['drill_id', 'environment', 'tenant', 'severity', 'started_at', 'completed_at', 'incident_commander']) {
|
|
74
|
+
if (!nonEmptyString(metadata[field])) blockers.push(blocker(`metadata.${field} is required`, `metadata.${field}`));
|
|
75
|
+
}
|
|
76
|
+
if (!iso(metadata.started_at)) blockers.push(blocker('metadata.started_at must be ISO time', 'metadata.started_at'));
|
|
77
|
+
if (!iso(metadata.completed_at)) blockers.push(blocker('metadata.completed_at must be ISO time', 'metadata.completed_at'));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const contacts = drill.contacts;
|
|
81
|
+
if (!Array.isArray(contacts) || contacts.length === 0) blockers.push(blocker('contacts array is required', 'contacts'));
|
|
82
|
+
else {
|
|
83
|
+
const roles = roleSet(contacts);
|
|
84
|
+
for (const role of REQUIRED_CONTACT_ROLES) if (!roles.has(role)) blockers.push(blocker(`contact role ${role} is required`, `contacts.${role}`));
|
|
85
|
+
contacts.forEach((contact, index) => {
|
|
86
|
+
if (!isPlainObject(contact)) blockers.push(blocker('contact must be object', `contacts[${index}]`));
|
|
87
|
+
else {
|
|
88
|
+
for (const field of ['role', 'name', 'organization', 'contact_ref', 'escalation_ref']) {
|
|
89
|
+
if (!nonEmptyString(contact[field])) blockers.push(blocker(`contacts[${index}].${field} is required`, `contacts[${index}].${field}`));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const preservation = drill.evidence_preservation;
|
|
96
|
+
if (!isPlainObject(preservation)) blockers.push(blocker('evidence_preservation object is required', 'evidence_preservation'));
|
|
97
|
+
else {
|
|
98
|
+
for (const field of ['incident_ticket_ref', 'log_snapshot_ref', 'forensic_bundle_ref', 'retention_policy_ref']) {
|
|
99
|
+
if (!nonEmptyString(preservation[field])) blockers.push(blocker(`evidence_preservation.${field} is required`, `evidence_preservation.${field}`));
|
|
100
|
+
}
|
|
101
|
+
if (!hashOk(preservation.forensic_bundle_hash)) blockers.push(blocker('evidence_preservation.forensic_bundle_hash must be sha256', 'evidence_preservation.forensic_bundle_hash'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const communications = drill.communications;
|
|
105
|
+
if (!isPlainObject(communications)) blockers.push(blocker('communications object is required', 'communications'));
|
|
106
|
+
else {
|
|
107
|
+
for (const field of ['internal_channel_ref', 'customer_notification_ref', 'status_page_ref', 'executive_update_ref']) {
|
|
108
|
+
if (!nonEmptyString(communications[field])) blockers.push(blocker(`communications.${field} is required`, `communications.${field}`));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const timeline = drill.timeline;
|
|
113
|
+
if (!Array.isArray(timeline) || timeline.length === 0) blockers.push(blocker('timeline array is required', 'timeline'));
|
|
114
|
+
else {
|
|
115
|
+
const events = eventSet(timeline);
|
|
116
|
+
for (const event of REQUIRED_TIMELINE_EVENTS) if (!events.has(event)) blockers.push(blocker(`timeline event ${event} is required`, `timeline.${event}`));
|
|
117
|
+
timeline.forEach((event, index) => {
|
|
118
|
+
if (!isPlainObject(event)) blockers.push(blocker('timeline event must be object', `timeline[${index}]`));
|
|
119
|
+
else {
|
|
120
|
+
if (!nonEmptyString(event.event)) blockers.push(blocker(`timeline[${index}].event is required`, `timeline[${index}].event`));
|
|
121
|
+
if (!iso(event.at)) blockers.push(blocker(`timeline[${index}].at must be ISO time`, `timeline[${index}].at`));
|
|
122
|
+
if (!nonEmptyString(event.evidence_ref)) blockers.push(blocker(`timeline[${index}].evidence_ref is required`, `timeline[${index}].evidence_ref`));
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const targets = drill.response_targets;
|
|
128
|
+
if (!isPlainObject(targets)) blockers.push(blocker('response_targets object is required', 'response_targets'));
|
|
129
|
+
else {
|
|
130
|
+
for (const field of ['detect_seconds', 'triage_seconds', 'notify_seconds', 'recover_seconds', 'max_detect_seconds', 'max_triage_seconds', 'max_notify_seconds', 'max_recover_seconds']) {
|
|
131
|
+
if (!nonNegative(targets[field])) blockers.push(blocker(`response_targets.${field} must be non-negative number`, `response_targets.${field}`));
|
|
132
|
+
}
|
|
133
|
+
const pairs = [
|
|
134
|
+
['detect_seconds', 'max_detect_seconds', 'detection time exceeds maximum'],
|
|
135
|
+
['triage_seconds', 'max_triage_seconds', 'triage time exceeds maximum'],
|
|
136
|
+
['notify_seconds', 'max_notify_seconds', 'notification time exceeds maximum'],
|
|
137
|
+
['recover_seconds', 'max_recover_seconds', 'recovery time exceeds maximum'],
|
|
138
|
+
];
|
|
139
|
+
for (const [actual, max, message] of pairs) {
|
|
140
|
+
if (nonNegative(targets[actual]) && nonNegative(targets[max]) && targets[actual] > targets[max]) blockers.push(blocker(message, `response_targets.${actual}`));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const result = drill.result;
|
|
145
|
+
if (!isPlainObject(result)) blockers.push(blocker('result object is required', 'result'));
|
|
146
|
+
else {
|
|
147
|
+
if (!STATUS_PASS.has(String(result.status ?? '').toLowerCase())) blockers.push(blocker('result.status must be pass', 'result.status'));
|
|
148
|
+
for (const field of ['postmortem_ref', 'lessons_learned_ref', 'followup_owner', 'followup_due_at']) {
|
|
149
|
+
if (!nonEmptyString(result[field])) blockers.push(blocker(`result.${field} is required`, `result.${field}`));
|
|
150
|
+
}
|
|
151
|
+
if (!iso(result.followup_due_at)) blockers.push(blocker('result.followup_due_at must be ISO time', 'result.followup_due_at'));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const validation = {
|
|
155
|
+
schema: INCIDENT_DRILL_RESULT_SCHEMA,
|
|
156
|
+
generated_at: options.generated_at ?? options.generatedAt ?? new Date(0).toISOString(),
|
|
157
|
+
ok: blockers.length === 0,
|
|
158
|
+
status: blockers.length === 0 ? 'pass' : 'blocked',
|
|
159
|
+
blockers,
|
|
160
|
+
checked: {
|
|
161
|
+
contact_roles: REQUIRED_CONTACT_ROLES.length,
|
|
162
|
+
timeline_events: REQUIRED_TIMELINE_EVENTS.length,
|
|
163
|
+
response_targets: true,
|
|
164
|
+
evidence_preservation: true,
|
|
165
|
+
communications: true,
|
|
166
|
+
},
|
|
167
|
+
claim_boundary: [
|
|
168
|
+
'Incident drill validation checks declared evidence only; it does not page people, access systems, or verify external ticket contents.',
|
|
169
|
+
'A pass result is evidence for one named drill and does not prove future incident performance.',
|
|
170
|
+
'Secret values, raw memory, prompts, transcripts, provider responses, and decrypted content must remain outside drill artifacts.',
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
assertNoSecrets(validation, 'result');
|
|
174
|
+
return validation;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
178
|
+
const flags = new Map();
|
|
179
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
180
|
+
const arg = argv[index];
|
|
181
|
+
if (!arg.startsWith('--')) continue;
|
|
182
|
+
const eq = arg.indexOf('=');
|
|
183
|
+
if (eq !== -1) flags.set(arg.slice(2, eq), arg.slice(eq + 1));
|
|
184
|
+
else if (!argv[index + 1] || argv[index + 1].startsWith('--')) flags.set(arg.slice(2), true);
|
|
185
|
+
else {
|
|
186
|
+
flags.set(arg.slice(2), argv[index + 1]);
|
|
187
|
+
index += 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return flags;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function getFlag(flags, names) {
|
|
194
|
+
for (const name of names) if (flags.has(name)) return flags.get(name);
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function main() {
|
|
199
|
+
const flags = parseArgs();
|
|
200
|
+
const drillPath = getFlag(flags, ['drill', 'in']);
|
|
201
|
+
if (!nonEmptyString(drillPath)) throw new Error('--drill <path> is required');
|
|
202
|
+
const drill = JSON.parse(await readFile(resolve(String(drillPath)), 'utf8'));
|
|
203
|
+
const result = validateIncidentDrill(drill, { generated_at: new Date().toISOString() });
|
|
204
|
+
const out = getFlag(flags, ['out']);
|
|
205
|
+
const text = `${JSON.stringify(result, null, 2)}\n`;
|
|
206
|
+
if (out && out !== true) {
|
|
207
|
+
const outPath = resolve(String(out));
|
|
208
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
209
|
+
await writeFile(outPath, text, 'utf8');
|
|
210
|
+
}
|
|
211
|
+
process.stdout.write(text);
|
|
212
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const invokedPath = process.argv[1] ? resolve(process.argv[1]) : '';
|
|
216
|
+
if (invokedPath === fileURLToPath(import.meta.url)) {
|
|
217
|
+
main().catch((error) => {
|
|
218
|
+
process.stderr.write(`${error.name ?? 'Error'}: ${error.message}\n`);
|
|
219
|
+
process.exitCode = 1;
|
|
220
|
+
});
|
|
221
|
+
}
|