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,250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const PROJECT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
+
const PACKAGE_ROOT_FILES = Object.freeze(['package.json', 'README.md', 'LICENSE']);
|
|
9
|
+
const SCHEMA = 'enigma.release_provenance.v1';
|
|
10
|
+
const SECRET_OR_LOCAL_BUNDLE_PATH = /(?:^|\/)(?:\.env(?:\.|$)|env\.local$|secrets?|credentials?|tokens?|api[-_]?keys?|private[-_]?keys?|private[-_]?local[-_]?memory|\.enigma|enigma[-_]?bundle|vault[-_]?bundle|local[-_]?bundle|bundle\.json|logs?|npm-debug\.log|yarn-error\.log|pnpm-debug\.log)(?:\/|$|[._-])|\.log(?:\.|$)/i;
|
|
11
|
+
const SECRET_EXTENSION = /\.(?:pem|key|p12|pfx|jks|keystore)(?:\.|$)/i;
|
|
12
|
+
const PRIVATE_PUBLIC_SITE_COLLATERAL = /(?:^|\/)(?:\d+[_-])?(?:private|internal|launch-code|executive|investor|partner|token(?:omics)?|sales|marketing|funnel|community|social|adoption|objections|faq|whitepaper|litepaper|pitch|demo[-_]?scripts?|content[-_]?calendar|brand[-_]?messaging)[^/]*\.(?:html|json|md|txt)$/i;
|
|
13
|
+
const PUBLIC_SITE_MANIFEST_CANDIDATES = Object.freeze([
|
|
14
|
+
'_public_site/public-site-manifest.json',
|
|
15
|
+
'_public_site/manifest.json',
|
|
16
|
+
'_public_site/launch/manifest.json',
|
|
17
|
+
'public/public-site-manifest.json',
|
|
18
|
+
'public/manifest.json',
|
|
19
|
+
'site/public/public-site-manifest.json',
|
|
20
|
+
'site/public/manifest.json'
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function normalizeRel(rel) {
|
|
24
|
+
return String(rel).replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+/g, '/');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function safeProjectPath(rel) {
|
|
28
|
+
const normalized = normalizeRel(rel);
|
|
29
|
+
if (normalized.length === 0 || path.posix.isAbsolute(normalized) || normalized.split('/').includes('..')) {
|
|
30
|
+
throw new Error(`Refusing unsafe package path: ${rel}`);
|
|
31
|
+
}
|
|
32
|
+
assertSafeEvidencePath(normalized);
|
|
33
|
+
const full = path.resolve(PROJECT_ROOT, ...normalized.split('/'));
|
|
34
|
+
const relative = path.relative(PROJECT_ROOT, full);
|
|
35
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) throw new Error(`Refusing path outside project root: ${rel}`);
|
|
36
|
+
return { normalized, full };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function assertSafeEvidencePath(rel) {
|
|
40
|
+
const normalized = normalizeRel(rel);
|
|
41
|
+
if (SECRET_OR_LOCAL_BUNDLE_PATH.test(normalized) || SECRET_EXTENSION.test(normalized) || PRIVATE_PUBLIC_SITE_COLLATERAL.test(normalized)) {
|
|
42
|
+
throw new Error(`Refusing release provenance over sensitive or private path: ${normalized}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sortedObject(value) {
|
|
47
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) return value;
|
|
48
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortedObject(value[key])]));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sha256File(full) {
|
|
52
|
+
const hash = createHash('sha256');
|
|
53
|
+
hash.update(fs.readFileSync(full));
|
|
54
|
+
return hash.digest('hex');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function collectFiles(rel, out) {
|
|
58
|
+
const { normalized, full } = safeProjectPath(rel);
|
|
59
|
+
if (!fs.existsSync(full)) throw new Error(`Package files entry does not exist: ${normalized}`);
|
|
60
|
+
const stat = fs.lstatSync(full);
|
|
61
|
+
if (stat.isSymbolicLink()) throw new Error(`Refusing symlink in package surface: ${normalized}`);
|
|
62
|
+
if (stat.isDirectory()) {
|
|
63
|
+
for (const entry of fs.readdirSync(full).sort()) collectFiles(`${normalized.replace(/\/$/, '')}/${entry}`, out);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
if (stat.isFile()) out.add(normalized);
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readPackageJson() {
|
|
71
|
+
const packagePath = path.join(PROJECT_ROOT, 'package.json');
|
|
72
|
+
return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function collectPackageSurface(pkg) {
|
|
76
|
+
if (!Array.isArray(pkg.files)) throw new Error('package.json files must be an array');
|
|
77
|
+
const rels = new Set();
|
|
78
|
+
for (const rel of PACKAGE_ROOT_FILES) {
|
|
79
|
+
const { full, normalized } = safeProjectPath(rel);
|
|
80
|
+
if (fs.existsSync(full)) rels.add(normalized);
|
|
81
|
+
}
|
|
82
|
+
for (const rel of pkg.files) {
|
|
83
|
+
if (typeof rel !== 'string' || rel.length === 0) throw new Error('package.json files entries must be non-empty strings');
|
|
84
|
+
collectFiles(rel, rels);
|
|
85
|
+
}
|
|
86
|
+
return [...rels].sort();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function fileRecord(rel) {
|
|
90
|
+
const { full } = safeProjectPath(rel);
|
|
91
|
+
const stat = fs.statSync(full);
|
|
92
|
+
return {
|
|
93
|
+
path: rel,
|
|
94
|
+
bytes: stat.size,
|
|
95
|
+
sha256: `sha256:${sha256File(full)}`
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function countFilesUnder(rel, predicate) {
|
|
100
|
+
const { full } = safeProjectPath(rel);
|
|
101
|
+
if (!fs.existsSync(full)) return 0;
|
|
102
|
+
let count = 0;
|
|
103
|
+
for (const entry of fs.readdirSync(full).sort()) {
|
|
104
|
+
const childRel = `${normalizeRel(rel).replace(/\/$/, '')}/${entry}`;
|
|
105
|
+
const childFull = path.join(full, entry);
|
|
106
|
+
const stat = fs.lstatSync(childFull);
|
|
107
|
+
if (stat.isSymbolicLink()) throw new Error(`Refusing symlink while counting: ${childRel}`);
|
|
108
|
+
if (stat.isDirectory()) count += countFilesUnder(childRel, predicate);
|
|
109
|
+
else if (stat.isFile() && predicate(childRel)) count += 1;
|
|
110
|
+
}
|
|
111
|
+
return count;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function optionalChecksum(rel) {
|
|
115
|
+
const { normalized, full } = safeProjectPath(rel);
|
|
116
|
+
if (!fs.existsSync(full)) return null;
|
|
117
|
+
const stat = fs.lstatSync(full);
|
|
118
|
+
if (stat.isSymbolicLink()) throw new Error(`Refusing symlink optional evidence: ${normalized}`);
|
|
119
|
+
if (!stat.isFile()) throw new Error(`Optional evidence is not a file: ${normalized}`);
|
|
120
|
+
return fileRecord(normalized);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function assertManifestPathStrings(value) {
|
|
124
|
+
if (typeof value === 'string') {
|
|
125
|
+
if (value.includes('/') || value.includes('\\') || /\.(?:css|html|js|json|md|mjs|png|svg|webp|log|pem|key)$/i.test(value)) {
|
|
126
|
+
assertSafeEvidencePath(value);
|
|
127
|
+
}
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (Array.isArray(value)) {
|
|
131
|
+
for (const item of value) assertManifestPathStrings(item);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (value !== null && typeof value === 'object') {
|
|
135
|
+
for (const item of Object.values(value)) assertManifestPathStrings(item);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function validatePublicSiteManifest(rel) {
|
|
140
|
+
const { full } = safeProjectPath(rel);
|
|
141
|
+
const manifest = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
142
|
+
assertManifestPathStrings(manifest);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
function publicSiteManifestChecksum() {
|
|
147
|
+
for (const rel of PUBLIC_SITE_MANIFEST_CANDIDATES) {
|
|
148
|
+
const record = optionalChecksum(rel);
|
|
149
|
+
if (record) {
|
|
150
|
+
validatePublicSiteManifest(record.path);
|
|
151
|
+
return record;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function rootHash(files) {
|
|
158
|
+
const hash = createHash('sha256');
|
|
159
|
+
for (const file of files) hash.update(`${file.sha256} ${file.path}\n`, 'utf8');
|
|
160
|
+
return `sha256:${hash.digest('hex')}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseArgs(argv) {
|
|
164
|
+
let out = null;
|
|
165
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
166
|
+
const arg = argv[index];
|
|
167
|
+
if (arg === '--out') {
|
|
168
|
+
if (out !== null) throw new Error('--out may only be provided once');
|
|
169
|
+
const value = argv[index + 1];
|
|
170
|
+
if (!value) throw new Error('--out requires a file path');
|
|
171
|
+
out = value;
|
|
172
|
+
index += 1;
|
|
173
|
+
} else {
|
|
174
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return { out };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function packageSummary(pkg) {
|
|
181
|
+
const bin = sortedObject(pkg.bin ?? {});
|
|
182
|
+
return {
|
|
183
|
+
name: pkg.name,
|
|
184
|
+
version: pkg.version,
|
|
185
|
+
type: pkg.type ?? null,
|
|
186
|
+
engines: sortedObject(pkg.engines ?? {}),
|
|
187
|
+
bin,
|
|
188
|
+
bins: bin,
|
|
189
|
+
exports: sortedObject(pkg.exports ?? {}),
|
|
190
|
+
scripts: sortedObject(pkg.scripts ?? {})
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function buildProvenance() {
|
|
195
|
+
const pkg = readPackageJson();
|
|
196
|
+
const files = collectPackageSurface(pkg).map(fileRecord);
|
|
197
|
+
const ciWorkflow = optionalChecksum('.github/workflows/ci.yml');
|
|
198
|
+
const publicSiteManifest = publicSiteManifestChecksum();
|
|
199
|
+
const computedRootHash = rootHash(files);
|
|
200
|
+
const specCount = countFilesUnder('specs', (rel) => rel.endsWith('.json'));
|
|
201
|
+
const testCount = countFilesUnder('test', (rel) => rel.endsWith('.test.mjs') || rel.endsWith('.test.js') || rel.endsWith('.test.ts'));
|
|
202
|
+
return {
|
|
203
|
+
schema: SCHEMA,
|
|
204
|
+
evidence: {
|
|
205
|
+
kind: 'local_checksum_provenance',
|
|
206
|
+
claim_boundary: 'Local checksum/SBOM-style package evidence only; unsigned and not a registry, cloud, git, or compliance attestation.',
|
|
207
|
+
external_credentials_required: false,
|
|
208
|
+
git_required: false,
|
|
209
|
+
signed: false,
|
|
210
|
+
registry_attestation: false
|
|
211
|
+
},
|
|
212
|
+
package: packageSummary(pkg),
|
|
213
|
+
counts: {
|
|
214
|
+
files: files.length,
|
|
215
|
+
file_count: files.length,
|
|
216
|
+
specs: specCount,
|
|
217
|
+
spec_count: specCount,
|
|
218
|
+
tests: testCount,
|
|
219
|
+
test_count: testCount
|
|
220
|
+
},
|
|
221
|
+
file_count: files.length,
|
|
222
|
+
spec_count: specCount,
|
|
223
|
+
test_count: testCount,
|
|
224
|
+
ci_workflow: ciWorkflow,
|
|
225
|
+
public_site_manifest: publicSiteManifest,
|
|
226
|
+
root_hash: computedRootHash,
|
|
227
|
+
files
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function writeJsonFile(outPath, value) {
|
|
232
|
+
const resolved = path.resolve(outPath);
|
|
233
|
+
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
|
234
|
+
fs.writeFileSync(resolved, `${JSON.stringify(value, null, 2)}\n`);
|
|
235
|
+
return resolved;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const { out } = parseArgs(process.argv.slice(2));
|
|
240
|
+
const provenance = buildProvenance();
|
|
241
|
+
if (out) {
|
|
242
|
+
const written = writeJsonFile(out, provenance);
|
|
243
|
+
process.stdout.write(`${JSON.stringify({ ok: true, path: written, file_count: provenance.counts.files, root_hash: provenance.root_hash })}\n`);
|
|
244
|
+
} else {
|
|
245
|
+
process.stdout.write(`${JSON.stringify(provenance, null, 2)}\n`);
|
|
246
|
+
}
|
|
247
|
+
} catch (error) {
|
|
248
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
249
|
+
process.exitCode = 1;
|
|
250
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { once } from 'node:events';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { createRelayServer } from '../apps/relay/src/server.mjs';
|
|
5
|
+
import { createGatewayServer } from '../apps/gateway/src/server.mjs';
|
|
6
|
+
|
|
7
|
+
export const BACKEND_READINESS_SMOKE_SCHEMA = 'enigma.backend_readiness_smoke.v1';
|
|
8
|
+
|
|
9
|
+
const SECRET_OUTPUT_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 SHARED_READINESS_REFS = Object.freeze({
|
|
11
|
+
network_access_policy_ref: 'network-policy-smoke-ref',
|
|
12
|
+
kms_custody_ref: 'kms-custody-smoke-ref',
|
|
13
|
+
tenant_policy_approval_ref: 'tenant-policy-smoke-ref',
|
|
14
|
+
usage_metering_ref: 'usage-metering-smoke-ref',
|
|
15
|
+
service_settlement_ref: 'service-settlement-smoke-ref',
|
|
16
|
+
monitoring_alerting_ref: 'monitoring-alerting-smoke-ref',
|
|
17
|
+
public_site_security_ref: 'public-site-security-smoke-ref',
|
|
18
|
+
security_threat_model_ref: 'security-threat-model-smoke-ref',
|
|
19
|
+
legal_compliance_ref: 'legal-compliance-smoke-ref',
|
|
20
|
+
support_sla_ref: 'support-sla-smoke-ref',
|
|
21
|
+
incident_drill_ref: 'incident-drill-smoke-ref',
|
|
22
|
+
backup_restore_drill_ref: 'backup-restore-drill-smoke-ref',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
26
|
+
const flags = new Map();
|
|
27
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
28
|
+
const arg = argv[index];
|
|
29
|
+
if (arg === '--help') return { help: true };
|
|
30
|
+
if (!arg.startsWith('--')) continue;
|
|
31
|
+
if (!argv[index + 1] || argv[index + 1].startsWith('--')) flags.set(arg.slice(2), true);
|
|
32
|
+
else {
|
|
33
|
+
flags.set(arg.slice(2), argv[index + 1]);
|
|
34
|
+
index += 1;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { help: false, flags };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function usage() {
|
|
41
|
+
return `Usage: node scripts/run-backend-readiness-smoke.mjs [--host 127.0.0.1]\n\nStarts in-process relay/gateway HTTP servers on loopback ephemeral ports, probes /livez and /readyz,\nand verifies fail-closed production readiness plus a fully referenced production-ready local fixture.\nIt does not expose admin/data routes, provision cloud resources, or prove hosted live infrastructure.\n`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function listen(server, host) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const onError = (error) => reject(error);
|
|
47
|
+
server.once('error', onError);
|
|
48
|
+
server.listen(0, host, () => {
|
|
49
|
+
server.off('error', onError);
|
|
50
|
+
resolve(server.address());
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function close(server) {
|
|
56
|
+
if (!server.listening) return;
|
|
57
|
+
server.close();
|
|
58
|
+
await once(server, 'close');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function getJson(baseUrl, path) {
|
|
62
|
+
const response = await fetch(`${baseUrl}${path}`, { method: 'GET', headers: { accept: 'application/json' } });
|
|
63
|
+
const text = await response.text();
|
|
64
|
+
if (SECRET_OUTPUT_RE.test(text)) throw new Error(`${baseUrl}${path} emitted secret-looking output`);
|
|
65
|
+
let body;
|
|
66
|
+
try {
|
|
67
|
+
body = JSON.parse(text);
|
|
68
|
+
} catch {
|
|
69
|
+
throw new Error(`${baseUrl}${path} did not return JSON`);
|
|
70
|
+
}
|
|
71
|
+
return { status: response.status, body, byte_count: Buffer.byteLength(text) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function missingCount(body) {
|
|
75
|
+
return Array.isArray(body?.missing_evidence_refs) ? body.missing_evidence_refs.length : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function allChecksOk(body) {
|
|
79
|
+
return Array.isArray(body?.checks) && body.checks.length > 0 && body.checks.every((check) => check.ok === true);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isLoopbackHost(host) {
|
|
83
|
+
return host === '127.0.0.1' || host === '::1' || host === 'localhost';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function relayReadyOptions() {
|
|
87
|
+
return {
|
|
88
|
+
mode: 'production',
|
|
89
|
+
now: '1970-01-01T00:00:00.000Z',
|
|
90
|
+
backend_host_ref: 'relay-backend-host-smoke-ref',
|
|
91
|
+
dns_tls_ref: 'relay-dns-tls-smoke-ref',
|
|
92
|
+
runtime_auth_ref: 'relay-runtime-auth-smoke-ref',
|
|
93
|
+
durable_storage: { kind: 'postgres', ref: 'relay-durable-storage-smoke-ref' },
|
|
94
|
+
kms_ref: 'relay-kms-smoke-ref',
|
|
95
|
+
siem_ref: 'relay-siem-smoke-ref',
|
|
96
|
+
backup_target: 'relay-backup-smoke-ref',
|
|
97
|
+
monitoring_ref: 'relay-monitoring-smoke-ref',
|
|
98
|
+
...SHARED_READINESS_REFS,
|
|
99
|
+
operator_acceptance: { decision: 'go', status: 'go', ref: 'relay-operator-acceptance-smoke-ref' },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function gatewayReadyOptions() {
|
|
104
|
+
return {
|
|
105
|
+
mode: 'production',
|
|
106
|
+
backend_host_ref: 'gateway-backend-host-smoke-ref',
|
|
107
|
+
dns_tls_ref: 'gateway-dns-tls-smoke-ref',
|
|
108
|
+
durable_storage: { kind: 'postgres', ref: 'gateway-durable-storage-smoke-ref' },
|
|
109
|
+
state_backend: { kind: 'postgres', ref: 'gateway-state-backend-smoke-ref' },
|
|
110
|
+
kms_ref: 'gateway-kms-smoke-ref',
|
|
111
|
+
siem_ref: 'gateway-siem-smoke-ref',
|
|
112
|
+
backup_target: 'gateway-backup-smoke-ref',
|
|
113
|
+
monitoring_ref: 'gateway-monitoring-smoke-ref',
|
|
114
|
+
admin_auth_ref: 'gateway-admin-auth-smoke-ref',
|
|
115
|
+
data_plane_auth_ref: 'gateway-data-plane-auth-smoke-ref',
|
|
116
|
+
...SHARED_READINESS_REFS,
|
|
117
|
+
operator_acceptance: { decision: 'go', status: 'go', ref: 'gateway-operator-acceptance-smoke-ref' },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function probeServer({ service, mode, serverFactory, host, expectedReadyStatus, expectedReadyOk }) {
|
|
122
|
+
const server = serverFactory();
|
|
123
|
+
try {
|
|
124
|
+
const address = await listen(server, host);
|
|
125
|
+
const baseUrl = `http://${address.address}:${address.port}`;
|
|
126
|
+
const livez = await getJson(baseUrl, '/livez');
|
|
127
|
+
const readyz = await getJson(baseUrl, '/readyz');
|
|
128
|
+
return {
|
|
129
|
+
service,
|
|
130
|
+
mode,
|
|
131
|
+
bind_host: address.address,
|
|
132
|
+
public_routes_probed: ['/livez', '/readyz'],
|
|
133
|
+
livez_status: livez.status,
|
|
134
|
+
livez_ok: livez.body?.ok === true,
|
|
135
|
+
readyz_status: readyz.status,
|
|
136
|
+
readyz_ok: readyz.body?.ok === true,
|
|
137
|
+
readyz_missing_evidence_ref_count: missingCount(readyz.body),
|
|
138
|
+
readyz_all_checks_ok: allChecksOk(readyz.body),
|
|
139
|
+
ok: livez.status === 200
|
|
140
|
+
&& livez.body?.ok === true
|
|
141
|
+
&& readyz.status === expectedReadyStatus
|
|
142
|
+
&& readyz.body?.ok === expectedReadyOk,
|
|
143
|
+
};
|
|
144
|
+
} finally {
|
|
145
|
+
await close(server);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function runBackendReadinessSmoke(options = {}) {
|
|
150
|
+
const host = options.host ?? '127.0.0.1';
|
|
151
|
+
if (!isLoopbackHost(host)) throw new Error(`backend readiness smoke host must be loopback-only; received ${host}`);
|
|
152
|
+
const generatedAt = options.generated_at ?? options.generatedAt ?? new Date().toISOString();
|
|
153
|
+
const checks = [];
|
|
154
|
+
checks.push(await probeServer({
|
|
155
|
+
service: 'relay',
|
|
156
|
+
mode: 'production-fail-closed',
|
|
157
|
+
host,
|
|
158
|
+
expectedReadyStatus: 503,
|
|
159
|
+
expectedReadyOk: false,
|
|
160
|
+
serverFactory: () => createRelayServer({ mode: 'production', now: '1970-01-01T00:00:00.000Z' }),
|
|
161
|
+
}));
|
|
162
|
+
checks.push(await probeServer({
|
|
163
|
+
service: 'gateway',
|
|
164
|
+
mode: 'production-fail-closed',
|
|
165
|
+
host,
|
|
166
|
+
expectedReadyStatus: 503,
|
|
167
|
+
expectedReadyOk: false,
|
|
168
|
+
serverFactory: () => createGatewayServer({ mode: 'production' }),
|
|
169
|
+
}));
|
|
170
|
+
checks.push(await probeServer({
|
|
171
|
+
service: 'relay',
|
|
172
|
+
mode: 'production-referenced-fixture',
|
|
173
|
+
host,
|
|
174
|
+
expectedReadyStatus: 200,
|
|
175
|
+
expectedReadyOk: true,
|
|
176
|
+
serverFactory: () => createRelayServer(relayReadyOptions()),
|
|
177
|
+
}));
|
|
178
|
+
checks.push(await probeServer({
|
|
179
|
+
service: 'gateway',
|
|
180
|
+
mode: 'production-referenced-fixture',
|
|
181
|
+
host,
|
|
182
|
+
expectedReadyStatus: 200,
|
|
183
|
+
expectedReadyOk: true,
|
|
184
|
+
serverFactory: () => createGatewayServer(gatewayReadyOptions()),
|
|
185
|
+
}));
|
|
186
|
+
const result = {
|
|
187
|
+
schema: BACKEND_READINESS_SMOKE_SCHEMA,
|
|
188
|
+
generated_at: generatedAt,
|
|
189
|
+
ok: checks.every((check) => check.ok === true) && isLoopbackHost(host),
|
|
190
|
+
check_count: checks.length,
|
|
191
|
+
loopback_only: isLoopbackHost(host),
|
|
192
|
+
checks,
|
|
193
|
+
claim_boundary: [
|
|
194
|
+
'This smoke starts local in-process HTTP servers on loopback ephemeral ports only.',
|
|
195
|
+
'It verifies /livez and /readyz behavior, fail-closed production defaults, and fully referenced production fixture readiness.',
|
|
196
|
+
'It does not prove hosted DNS, TLS, Cloudflare, durable cloud storage, KMS, SIEM, backup, operator acceptance, or customer BYOC infrastructure is live.',
|
|
197
|
+
],
|
|
198
|
+
};
|
|
199
|
+
if (SECRET_OUTPUT_RE.test(JSON.stringify(result))) throw new Error('backend readiness smoke output appears to contain a secret');
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
204
|
+
const parsed = parseArgs(argv);
|
|
205
|
+
if (parsed.help) return { text: usage() };
|
|
206
|
+
const host = parsed.flags?.get('host') ?? options.host ?? '127.0.0.1';
|
|
207
|
+
return { json: await runBackendReadinessSmoke({ ...options, host }) };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
211
|
+
try {
|
|
212
|
+
const result = await runCli();
|
|
213
|
+
if (result.text) process.stdout.write(result.text);
|
|
214
|
+
else process.stdout.write(`${JSON.stringify(result.json, null, 2)}\n`);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
217
|
+
process.stdout.write(`${JSON.stringify({ schema: BACKEND_READINESS_SMOKE_SCHEMA, ok: false, error: { code: 'BACKEND_READINESS_SMOKE_ERROR', message } }, null, 2)}\n`);
|
|
218
|
+
process.exitCode = 1;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
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 BACKUP_RESTORE_DRILL_SCHEMA = 'enigma.backup_restore_drill.v1';
|
|
7
|
+
export const BACKUP_RESTORE_DRILL_RESULT_SCHEMA = 'enigma.backup_restore_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 SAFE_SECRET_NAMED_KEYS = new Set(['kms_or_secret_custody_ref']);
|
|
12
|
+
const HASH_RE = /^sha256:[a-f0-9]{64}$/i;
|
|
13
|
+
const STATUS_PASS = new Set(['pass', 'passed', 'go', 'verified']);
|
|
14
|
+
|
|
15
|
+
function isPlainObject(value) {
|
|
16
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function nonEmptyString(value) {
|
|
20
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function assertNoSecrets(value, path = 'drill') {
|
|
24
|
+
if (typeof value === 'string') {
|
|
25
|
+
if (!/\.claim_boundary\[\d+\]$/.test(path) && SECRET_VALUE_RE.test(value)) throw new Error(`${path} contains secret or raw-memory-looking data`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
value.forEach((item, index) => assertNoSecrets(item, `${path}[${index}]`));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (!isPlainObject(value)) return;
|
|
33
|
+
for (const [key, child] of Object.entries(value)) {
|
|
34
|
+
if (SECRET_KEY_RE.test(key) && !SAFE_SECRET_NAMED_KEYS.has(key)) throw new Error(`${path}.${key} uses a forbidden field name`);
|
|
35
|
+
assertNoSecrets(child, `${path}.${key}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function blocker(message, path) {
|
|
40
|
+
return { message, path };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function positiveNumber(value) {
|
|
44
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hashOk(value) {
|
|
48
|
+
return nonEmptyString(value) && HASH_RE.test(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function validateBackupRestoreDrill(drill, options = {}) {
|
|
52
|
+
if (!isPlainObject(drill)) throw new Error('backup restore drill must be an object');
|
|
53
|
+
assertNoSecrets(drill);
|
|
54
|
+
const blockers = [];
|
|
55
|
+
if (drill.schema !== BACKUP_RESTORE_DRILL_SCHEMA) blockers.push(blocker('schema mismatch', 'schema'));
|
|
56
|
+
const metadata = drill.metadata;
|
|
57
|
+
if (!isPlainObject(metadata)) blockers.push(blocker('metadata is required', 'metadata'));
|
|
58
|
+
else {
|
|
59
|
+
for (const field of ['drill_id', 'environment', 'tenant', 'storage_engine', 'started_at', 'completed_at', 'operator', 'backup_owner', 'restore_owner']) {
|
|
60
|
+
if (!nonEmptyString(metadata[field])) blockers.push(blocker(`metadata.${field} is required`, `metadata.${field}`));
|
|
61
|
+
}
|
|
62
|
+
if (Number.isNaN(Date.parse(metadata.started_at ?? ''))) blockers.push(blocker('metadata.started_at must be ISO time', 'metadata.started_at'));
|
|
63
|
+
if (Number.isNaN(Date.parse(metadata.completed_at ?? ''))) blockers.push(blocker('metadata.completed_at must be ISO time', 'metadata.completed_at'));
|
|
64
|
+
}
|
|
65
|
+
const backup = drill.backup;
|
|
66
|
+
if (!isPlainObject(backup)) blockers.push(blocker('backup object is required', 'backup'));
|
|
67
|
+
else {
|
|
68
|
+
for (const field of ['backup_ref', 'scope_ref', 'storage_ref', 'kms_or_secret_custody_ref']) {
|
|
69
|
+
if (!nonEmptyString(backup[field])) blockers.push(blocker(`backup.${field} is required`, `backup.${field}`));
|
|
70
|
+
}
|
|
71
|
+
if (!hashOk(backup.source_snapshot_hash)) blockers.push(blocker('backup.source_snapshot_hash must be sha256', 'backup.source_snapshot_hash'));
|
|
72
|
+
if (!positiveNumber(backup.source_row_count)) blockers.push(blocker('backup.source_row_count must be non-negative number', 'backup.source_row_count'));
|
|
73
|
+
}
|
|
74
|
+
const restore = drill.restore;
|
|
75
|
+
if (!isPlainObject(restore)) blockers.push(blocker('restore object is required', 'restore'));
|
|
76
|
+
else {
|
|
77
|
+
for (const field of ['restore_ref', 'target_ref', 'restore_started_at', 'restore_completed_at']) {
|
|
78
|
+
if (!nonEmptyString(restore[field])) blockers.push(blocker(`restore.${field} is required`, `restore.${field}`));
|
|
79
|
+
}
|
|
80
|
+
if (!hashOk(restore.restored_snapshot_hash)) blockers.push(blocker('restore.restored_snapshot_hash must be sha256', 'restore.restored_snapshot_hash'));
|
|
81
|
+
if (!positiveNumber(restore.restored_row_count)) blockers.push(blocker('restore.restored_row_count must be non-negative number', 'restore.restored_row_count'));
|
|
82
|
+
}
|
|
83
|
+
const verification = drill.verification;
|
|
84
|
+
if (!isPlainObject(verification)) blockers.push(blocker('verification object is required', 'verification'));
|
|
85
|
+
else {
|
|
86
|
+
if (!STATUS_PASS.has(String(verification.status ?? '').toLowerCase())) blockers.push(blocker('verification.status must be pass', 'verification.status'));
|
|
87
|
+
if (!hashOk(verification.verifier_output_hash)) blockers.push(blocker('verification.verifier_output_hash must be sha256', 'verification.verifier_output_hash'));
|
|
88
|
+
if (!nonEmptyString(verification.verifier_ref)) blockers.push(blocker('verification.verifier_ref is required', 'verification.verifier_ref'));
|
|
89
|
+
}
|
|
90
|
+
const rpo = drill.rpo_rto;
|
|
91
|
+
if (!isPlainObject(rpo)) blockers.push(blocker('rpo_rto object is required', 'rpo_rto'));
|
|
92
|
+
else {
|
|
93
|
+
if (!positiveNumber(rpo.rpo_seconds)) blockers.push(blocker('rpo_rto.rpo_seconds must be non-negative number', 'rpo_rto.rpo_seconds'));
|
|
94
|
+
if (!positiveNumber(rpo.rto_seconds)) blockers.push(blocker('rpo_rto.rto_seconds must be non-negative number', 'rpo_rto.rto_seconds'));
|
|
95
|
+
if (!positiveNumber(rpo.max_rpo_seconds)) blockers.push(blocker('rpo_rto.max_rpo_seconds must be non-negative number', 'rpo_rto.max_rpo_seconds'));
|
|
96
|
+
if (!positiveNumber(rpo.max_rto_seconds)) blockers.push(blocker('rpo_rto.max_rto_seconds must be non-negative number', 'rpo_rto.max_rto_seconds'));
|
|
97
|
+
if (positiveNumber(rpo.rpo_seconds) && positiveNumber(rpo.max_rpo_seconds) && rpo.rpo_seconds > rpo.max_rpo_seconds) blockers.push(blocker('RPO exceeds maximum', 'rpo_rto.rpo_seconds'));
|
|
98
|
+
if (positiveNumber(rpo.rto_seconds) && positiveNumber(rpo.max_rto_seconds) && rpo.rto_seconds > rpo.max_rto_seconds) blockers.push(blocker('RTO exceeds maximum', 'rpo_rto.rto_seconds'));
|
|
99
|
+
}
|
|
100
|
+
if (isPlainObject(backup) && isPlainObject(restore) && hashOk(backup.source_snapshot_hash) && hashOk(restore.restored_snapshot_hash) && backup.source_snapshot_hash !== restore.restored_snapshot_hash) {
|
|
101
|
+
blockers.push(blocker('restored snapshot hash does not match source snapshot hash', 'restore.restored_snapshot_hash'));
|
|
102
|
+
}
|
|
103
|
+
if (isPlainObject(backup) && isPlainObject(restore) && positiveNumber(backup.source_row_count) && positiveNumber(restore.restored_row_count) && backup.source_row_count !== restore.restored_row_count) {
|
|
104
|
+
blockers.push(blocker('restored row count does not match source row count', 'restore.restored_row_count'));
|
|
105
|
+
}
|
|
106
|
+
const result = {
|
|
107
|
+
schema: BACKUP_RESTORE_DRILL_RESULT_SCHEMA,
|
|
108
|
+
generated_at: options.generated_at ?? options.generatedAt ?? new Date(0).toISOString(),
|
|
109
|
+
ok: blockers.length === 0,
|
|
110
|
+
status: blockers.length === 0 ? 'pass' : 'blocked',
|
|
111
|
+
blockers,
|
|
112
|
+
claim_boundary: [
|
|
113
|
+
'Backup restore drill validation checks declared evidence only; it does not access storage, secrets, or cloud resources.',
|
|
114
|
+
'A pass result is evidence for one named drill and does not prove all future restores.',
|
|
115
|
+
'Raw memory, credentials, private keys, prompts, transcripts, and decrypted content must remain outside drill artifacts.',
|
|
116
|
+
],
|
|
117
|
+
};
|
|
118
|
+
assertNoSecrets(result, 'result');
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
123
|
+
const flags = new Map();
|
|
124
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
125
|
+
const arg = argv[index];
|
|
126
|
+
if (!arg.startsWith('--')) continue;
|
|
127
|
+
const eq = arg.indexOf('=');
|
|
128
|
+
if (eq !== -1) flags.set(arg.slice(2, eq), arg.slice(eq + 1));
|
|
129
|
+
else if (!argv[index + 1] || argv[index + 1].startsWith('--')) flags.set(arg.slice(2), true);
|
|
130
|
+
else {
|
|
131
|
+
flags.set(arg.slice(2), argv[index + 1]);
|
|
132
|
+
index += 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return flags;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getFlag(flags, names) {
|
|
139
|
+
for (const name of names) if (flags.has(name)) return flags.get(name);
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function main() {
|
|
144
|
+
const flags = parseArgs();
|
|
145
|
+
const drillPath = getFlag(flags, ['drill', 'in']);
|
|
146
|
+
if (!nonEmptyString(drillPath)) throw new Error('--drill <path> is required');
|
|
147
|
+
const drill = JSON.parse(await readFile(resolve(String(drillPath)), 'utf8'));
|
|
148
|
+
const result = validateBackupRestoreDrill(drill, { generated_at: new Date().toISOString() });
|
|
149
|
+
const out = getFlag(flags, ['out']);
|
|
150
|
+
const text = `${JSON.stringify(result, null, 2)}\n`;
|
|
151
|
+
if (out && out !== true) {
|
|
152
|
+
const outPath = resolve(String(out));
|
|
153
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
154
|
+
await writeFile(outPath, text, 'utf8');
|
|
155
|
+
}
|
|
156
|
+
process.stdout.write(text);
|
|
157
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const invokedPath = process.argv[1] ? resolve(process.argv[1]) : '';
|
|
161
|
+
if (invokedPath === fileURLToPath(import.meta.url)) {
|
|
162
|
+
main().catch((error) => {
|
|
163
|
+
process.stderr.write(`${error.name ?? 'Error'}: ${error.message}\n`);
|
|
164
|
+
process.exitCode = 1;
|
|
165
|
+
});
|
|
166
|
+
}
|