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,128 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+
4
+ export const CLOUDFLARE_SECRET_ENV_SCHEMA = 'enigma.cloudflare_secret_env.v1';
5
+
6
+ export const CLOUDFLARE_SECRET_ENV_KEYS = Object.freeze([
7
+ 'CLOUDFLARE_API_TOKEN',
8
+ 'CLOUDFLARE_ACCOUNT_ID',
9
+ 'CLOUDFLARE_PROJECT_NAME',
10
+ ]);
11
+
12
+ const ALLOWED = new Set(CLOUDFLARE_SECRET_ENV_KEYS);
13
+ 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/@]+@|AKIA[0-9A-Z]{16})/iu;
14
+
15
+ export class CloudflareSecretEnvError extends Error {
16
+ constructor(message) {
17
+ super(message);
18
+ this.name = 'CloudflareSecretEnvError';
19
+ }
20
+ }
21
+
22
+ function unquote(value) {
23
+ const trimmed = value.trim();
24
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) return trimmed.slice(1, -1);
25
+ return trimmed;
26
+ }
27
+
28
+ export function parseCloudflareSecretEnvText(text) {
29
+ const parsed = {};
30
+ const ignored = [];
31
+ const lines = String(text ?? '').split(/\r?\n/);
32
+ lines.forEach((line, index) => {
33
+ const trimmed = line.trim();
34
+ if (trimmed.length === 0 || trimmed.startsWith('#')) return;
35
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/.exec(line);
36
+ if (!match) throw new CloudflareSecretEnvError(`invalid env-file line ${index + 1}`);
37
+ const key = match[1];
38
+ if (!ALLOWED.has(key)) {
39
+ ignored.push(key);
40
+ return;
41
+ }
42
+ const value = unquote(match[2]);
43
+ if (value.length === 0) throw new CloudflareSecretEnvError(`${key} must not be empty`);
44
+ if (key !== 'CLOUDFLARE_API_TOKEN' && SECRET_VALUE_RE.test(value)) throw new CloudflareSecretEnvError(`${key} contains secret-looking data`);
45
+ parsed[key] = value;
46
+ });
47
+ return {
48
+ schema: CLOUDFLARE_SECRET_ENV_SCHEMA,
49
+ values: parsed,
50
+ present_keys: Object.keys(parsed).sort(),
51
+ ignored_keys: [...new Set(ignored)].sort(),
52
+ };
53
+ }
54
+
55
+ export async function loadCloudflareSecretEnvFile(path, options = {}) {
56
+ if (typeof path !== 'string' || path.trim().length === 0) throw new CloudflareSecretEnvError('Cloudflare secret env file path is required');
57
+ const file = resolve(path);
58
+ let text;
59
+ try {
60
+ text = await readFile(file, 'utf8');
61
+ } catch {
62
+ throw new CloudflareSecretEnvError('Cloudflare secret env file could not be read');
63
+ }
64
+ const parsed = parseCloudflareSecretEnvText(text);
65
+ return {
66
+ ...parsed,
67
+ path: options.includePath === false ? null : file,
68
+ byte_count: Buffer.byteLength(text),
69
+ };
70
+ }
71
+
72
+ export async function applyCloudflareSecretEnvFile(env = process.env, options = {}) {
73
+ const envFile = options.path ?? env.CLOUDFLARE_ENV_FILE ?? env.ENIGMA_CLOUDFLARE_ENV_FILE ?? null;
74
+ if (typeof envFile !== 'string' || envFile.trim().length === 0) {
75
+ return {
76
+ env,
77
+ source: null,
78
+ present_keys: CLOUDFLARE_SECRET_ENV_KEYS.filter((key) => typeof env[key] === 'string' && env[key].length > 0),
79
+ loaded_keys: [],
80
+ };
81
+ }
82
+ const loaded = await loadCloudflareSecretEnvFile(envFile, { includePath: options.includePath });
83
+ return {
84
+ env: { ...env, ...loaded.values },
85
+ source: {
86
+ schema: loaded.schema,
87
+ path: loaded.path,
88
+ byte_count: loaded.byte_count,
89
+ present_keys: loaded.present_keys,
90
+ ignored_keys: loaded.ignored_keys,
91
+ },
92
+ present_keys: CLOUDFLARE_SECRET_ENV_KEYS.filter((key) => typeof (loaded.values[key] ?? env[key]) === 'string' && (loaded.values[key] ?? env[key]).length > 0),
93
+ loaded_keys: loaded.present_keys,
94
+ };
95
+ }
96
+
97
+ function readValue(argv, index, name) {
98
+ if (index + 1 >= argv.length || String(argv[index + 1]).startsWith('--')) throw new CloudflareSecretEnvError(`${name} requires a value`);
99
+ return [argv[index + 1], index + 2];
100
+ }
101
+
102
+ export async function applyCloudflareSecretEnvFileFromArgv(argv = [], env = process.env, options = {}) {
103
+ const tokens = Array.from(argv ?? []);
104
+ const stripped = [];
105
+ let envFile = null;
106
+ for (let index = 0; index < tokens.length;) {
107
+ const token = tokens[index];
108
+ if (token === '--cloudflare-env-file') {
109
+ const [value, next] = readValue(tokens, index, token);
110
+ envFile = value;
111
+ index = next;
112
+ continue;
113
+ }
114
+ if (typeof token === 'string' && token.startsWith('--cloudflare-env-file=')) {
115
+ envFile = token.slice('--cloudflare-env-file='.length);
116
+ if (envFile.length === 0) throw new CloudflareSecretEnvError('--cloudflare-env-file requires a value');
117
+ index += 1;
118
+ continue;
119
+ }
120
+ stripped.push(token);
121
+ index += 1;
122
+ }
123
+ const applied = await applyCloudflareSecretEnvFile(env, { ...options, path: envFile ?? undefined });
124
+ return {
125
+ argv: stripped,
126
+ ...applied,
127
+ };
128
+ }
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from 'node:crypto';
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import {
7
+ HOSTED_BACKEND_LIVE_EVIDENCE_SCHEMA,
8
+ HOSTED_BACKEND_LIVE_RESULT_SCHEMA,
9
+ REQUIRED_REF_KEYS,
10
+ validateHostedBackendLiveEvidence,
11
+ } from './validate-hosted-backend-live.mjs';
12
+
13
+ export const HOSTED_BACKEND_LIVE_COLLECTION_SCHEMA = 'enigma.hosted_backend_live_collection.v1';
14
+
15
+ const REQUIRED_PROBES = Object.freeze(['relay_livez', 'relay_readyz', 'gateway_livez', 'gateway_readyz']);
16
+ const CLAIM_BOUNDARY = Object.freeze([
17
+ 'This collector performs public HTTPS health probes and assembles evidence; it does not deploy infrastructure, mutate DNS, create credentials, or approve operator acceptance.',
18
+ 'Collected evidence is accepted only if validate-hosted-backend-live also accepts it.',
19
+ 'Probe response bodies must be public-safe readiness JSON and must not contain tokens, prompts, transcripts, provider responses, raw memory, or personal contact data.',
20
+ ]);
21
+ const SAFE_PROBE_FIELD_NAMES = new Set(['evidence_refs', 'kms_or_secret_custody']);
22
+
23
+ function sha256(text) {
24
+ return `sha256:${createHash('sha256').update(String(text)).digest('hex')}`;
25
+ }
26
+
27
+ function isPlainObject(value) {
28
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
29
+ }
30
+
31
+ function nonEmptyString(value) {
32
+ return typeof value === 'string' && value.trim().length > 0;
33
+ }
34
+
35
+ function readFlag(flags, key, fallback = null) {
36
+ return flags.has(key) ? flags.get(key) : fallback;
37
+ }
38
+
39
+ function parsePublicHealthUrl(value, label) {
40
+ if (!nonEmptyString(value)) throw new Error(`${label} is required`);
41
+ const url = new URL(value);
42
+ if (url.protocol !== 'https:') throw new Error(`${label} must use https`);
43
+ if (url.username || url.password) throw new Error(`${label} must not include credentials`);
44
+ if (url.search || url.hash) throw new Error(`${label} must not include query strings or fragments`);
45
+ if (url.pathname !== '/livez' && url.pathname !== '/readyz') throw new Error(`${label} must end in /livez or /readyz`);
46
+ return url.toString();
47
+ }
48
+
49
+ function endpointFromBase(value, endpoint, label) {
50
+ if (!nonEmptyString(value)) throw new Error(`${label} is required`);
51
+ const url = new URL(value);
52
+ if (url.protocol !== 'https:') throw new Error(`${label} must use https`);
53
+ if (url.username || url.password) throw new Error(`${label} must not include credentials`);
54
+ url.pathname = endpoint;
55
+ url.search = '';
56
+ url.hash = '';
57
+ return url.toString();
58
+ }
59
+
60
+ async function readJsonFile(path) {
61
+ return JSON.parse(await readFile(resolve(path), 'utf8'));
62
+ }
63
+
64
+ async function maybeReadJsonFile(path) {
65
+ return nonEmptyString(path) ? readJsonFile(path) : null;
66
+ }
67
+
68
+ function redactProbeBody(value, path = 'probe.body') {
69
+ const forbidden = /(?:token|api[_-]?key|secret|password|passwd|pwd|private[_-]?key|prompt|completion|transcript|embedding|provider[_-]?response|raw[_-]?memory|plaintext|cookie|session)/iu;
70
+ const secretValue = /(?: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})/iu;
71
+ if (typeof value === 'string') {
72
+ if (secretValue.test(value)) throw new Error(`${path} contains secret-looking data`);
73
+ return value;
74
+ }
75
+ if (Array.isArray(value)) return value.map((item, index) => redactProbeBody(item, `${path}[${index}]`));
76
+ if (!isPlainObject(value)) return value;
77
+ for (const [key, child] of Object.entries(value)) {
78
+ if (forbidden.test(key) && !SAFE_PROBE_FIELD_NAMES.has(key) && !REQUIRED_REF_KEYS.includes(key)) throw new Error(`probe body field ${key} is not allowed in hosted backend evidence`);
79
+ redactProbeBody(child, `${path}.${key}`);
80
+ }
81
+ return value;
82
+ }
83
+
84
+ async function fetchProbe(url, { fetchImpl = globalThis.fetch, observedAt }) {
85
+ if (typeof fetchImpl !== 'function') throw new Error('global fetch is not available in this Node runtime');
86
+ const response = await fetchImpl(url, {
87
+ method: 'GET',
88
+ headers: { Accept: 'application/json' },
89
+ redirect: 'manual',
90
+ });
91
+ const statusCode = Number(response.status ?? 0);
92
+ const responseUrl = typeof response.url === 'string' && response.url.length > 0 ? response.url : url;
93
+ if (response.redirected === true || responseUrl !== url || (statusCode >= 300 && statusCode <= 399)) {
94
+ throw new Error(`hosted backend probe ${url} must not redirect`);
95
+ }
96
+ const text = typeof response.text === 'function' ? await response.text() : '';
97
+ let body = null;
98
+ try {
99
+ body = text.trim().length === 0 ? {} : JSON.parse(text);
100
+ } catch {
101
+ body = { ok: false, parse_error: 'response was not JSON' };
102
+ }
103
+ return {
104
+ url,
105
+ status_code: Number(response.status ?? 0),
106
+ body: redactProbeBody(body),
107
+ observed_at: observedAt,
108
+ response_hash: sha256(text),
109
+ };
110
+ }
111
+
112
+ function buildProbeUrls(options) {
113
+ const relayBaseUrl = options.relayBaseUrl ?? options.relayUrl ?? null;
114
+ const gatewayBaseUrl = options.gatewayBaseUrl ?? options.gatewayUrl ?? null;
115
+ return {
116
+ relay_livez: options.relayLivezUrl ? parsePublicHealthUrl(options.relayLivezUrl, 'relay_livez_url') : endpointFromBase(relayBaseUrl, '/livez', 'relay_url'),
117
+ relay_readyz: options.relayReadyzUrl ? parsePublicHealthUrl(options.relayReadyzUrl, 'relay_readyz_url') : endpointFromBase(relayBaseUrl, '/readyz', 'relay_url'),
118
+ gateway_livez: options.gatewayLivezUrl ? parsePublicHealthUrl(options.gatewayLivezUrl, 'gateway_livez_url') : endpointFromBase(gatewayBaseUrl, '/livez', 'gateway_url'),
119
+ gateway_readyz: options.gatewayReadyzUrl ? parsePublicHealthUrl(options.gatewayReadyzUrl, 'gateway_readyz_url') : endpointFromBase(gatewayBaseUrl, '/readyz', 'gateway_url'),
120
+ };
121
+ }
122
+
123
+ function buildEnvironment(options) {
124
+ if (isPlainObject(options.environment)) return options.environment;
125
+ return {
126
+ environment_id: options.environmentId,
127
+ domain: options.domain,
128
+ cloud_provider: options.cloudProvider,
129
+ region: options.region,
130
+ owner: options.owner,
131
+ status: options.environmentStatus ?? 'observed',
132
+ };
133
+ }
134
+
135
+ function buildOperatorAcceptance(options) {
136
+ if (isPlainObject(options.operatorAcceptance)) return options.operatorAcceptance;
137
+ return {
138
+ decision: options.operatorDecision ?? 'blocked',
139
+ packet_ref: options.operatorPacketRef,
140
+ approved_at: options.operatorApprovedAt,
141
+ approved_by: options.operatorApprovedBy,
142
+ };
143
+ }
144
+
145
+ function validateRefsShape(refs) {
146
+ if (!isPlainObject(refs)) throw new Error('--refs-json must contain an object');
147
+ return Object.fromEntries(REQUIRED_REF_KEYS.map((key) => [key, refs[key] ?? '']));
148
+ }
149
+
150
+ export async function collectHostedBackendLiveEvidence(options = {}) {
151
+ const observedAt = options.observed_at ?? options.observedAt ?? new Date().toISOString();
152
+ const probeUrls = buildProbeUrls(options);
153
+ const probeEntries = await Promise.all(REQUIRED_PROBES.map(async (key) => [key, await fetchProbe(probeUrls[key], { fetchImpl: options.fetchImpl, observedAt })]));
154
+ const evidence = {
155
+ schema: HOSTED_BACKEND_LIVE_EVIDENCE_SCHEMA,
156
+ observed_at: observedAt,
157
+ environment: buildEnvironment(options),
158
+ refs: validateRefsShape(options.refs),
159
+ probes: Object.fromEntries(probeEntries),
160
+ operator_acceptance: buildOperatorAcceptance(options),
161
+ claim_boundary: {
162
+ hosted_backend_live: true,
163
+ public_site_live: false,
164
+ cloudflare_credentials_claim: false,
165
+ token_roi_claim: false,
166
+ provider_deletion_claim: false,
167
+ model_forgetting_claim: false,
168
+ },
169
+ };
170
+ const validation = validateHostedBackendLiveEvidence(evidence, { generated_at: observedAt });
171
+ return {
172
+ schema: HOSTED_BACKEND_LIVE_COLLECTION_SCHEMA,
173
+ generated_at: observedAt,
174
+ ok: validation.ok,
175
+ evidence,
176
+ validation,
177
+ probe_summary: Object.fromEntries(Object.entries(evidence.probes).map(([key, probe]) => [key, { url: probe.url, status_code: probe.status_code, response_hash: probe.response_hash }])),
178
+ claim_boundary: [...CLAIM_BOUNDARY],
179
+ };
180
+ }
181
+
182
+ function parseArgs(argv) {
183
+ const flags = new Map();
184
+ for (let index = 0; index < argv.length; index += 1) {
185
+ const arg = argv[index];
186
+ if (arg === '--help' || arg === '-h') {
187
+ flags.set('help', true);
188
+ continue;
189
+ }
190
+ if (!arg.startsWith('--')) throw new Error(`Unexpected argument: ${arg}`);
191
+ const name = arg.slice(2);
192
+ const value = argv[index + 1];
193
+ if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value`);
194
+ flags.set(name, value);
195
+ index += 1;
196
+ }
197
+ return flags;
198
+ }
199
+
200
+ function usage() {
201
+ return `Usage: node scripts/collect-hosted-backend-live-evidence.mjs --relay-url <https-base> --gateway-url <https-base> --refs-json <refs.json> --domain <domain> --environment-id <id> --cloud-provider <provider> --region <region> --owner <owner> --operator-decision go --operator-packet-ref <ref> --operator-approved-at <iso> --operator-approved-by <name> [--out <collection.json>] [--evidence-out <evidence.json>]\n\nCollects public HTTPS /livez and /readyz evidence for relay and gateway, then validates it with validate-hosted-backend-live. It never sends credentials and does not deploy infrastructure.\n`;
202
+ }
203
+
204
+ async function runCli(argv = process.argv.slice(2), { fetchImpl = globalThis.fetch } = {}) {
205
+ const flags = parseArgs(argv);
206
+ if (flags.get('help')) {
207
+ process.stdout.write(usage());
208
+ return 0;
209
+ }
210
+ const refsPath = readFlag(flags, 'refs-json');
211
+ if (!nonEmptyString(refsPath)) throw new Error('--refs-json is required');
212
+ const refs = await readJsonFile(refsPath);
213
+ const environment = await maybeReadJsonFile(readFlag(flags, 'environment-json'));
214
+ const operatorAcceptance = await maybeReadJsonFile(readFlag(flags, 'operator-acceptance-json'));
215
+ const collection = await collectHostedBackendLiveEvidence({
216
+ relayBaseUrl: readFlag(flags, 'relay-url'),
217
+ gatewayBaseUrl: readFlag(flags, 'gateway-url'),
218
+ relayLivezUrl: readFlag(flags, 'relay-livez-url'),
219
+ relayReadyzUrl: readFlag(flags, 'relay-readyz-url'),
220
+ gatewayLivezUrl: readFlag(flags, 'gateway-livez-url'),
221
+ gatewayReadyzUrl: readFlag(flags, 'gateway-readyz-url'),
222
+ refs,
223
+ environment,
224
+ environmentId: readFlag(flags, 'environment-id'),
225
+ domain: readFlag(flags, 'domain'),
226
+ cloudProvider: readFlag(flags, 'cloud-provider'),
227
+ region: readFlag(flags, 'region'),
228
+ owner: readFlag(flags, 'owner'),
229
+ environmentStatus: readFlag(flags, 'environment-status', 'observed'),
230
+ operatorAcceptance,
231
+ operatorDecision: readFlag(flags, 'operator-decision', 'blocked'),
232
+ operatorPacketRef: readFlag(flags, 'operator-packet-ref'),
233
+ operatorApprovedAt: readFlag(flags, 'operator-approved-at'),
234
+ operatorApprovedBy: readFlag(flags, 'operator-approved-by'),
235
+ observed_at: readFlag(flags, 'observed-at') ?? new Date().toISOString(),
236
+ fetchImpl,
237
+ });
238
+ const collectionJson = `${JSON.stringify(collection, null, 2)}\n`;
239
+ const evidenceJson = `${JSON.stringify(collection.evidence, null, 2)}\n`;
240
+ const out = readFlag(flags, 'out');
241
+ if (out) {
242
+ const outPath = resolve(out);
243
+ await mkdir(dirname(outPath), { recursive: true });
244
+ await writeFile(outPath, collectionJson, 'utf8');
245
+ }
246
+ const evidenceOut = readFlag(flags, 'evidence-out');
247
+ if (evidenceOut) {
248
+ const evidencePath = resolve(evidenceOut);
249
+ await mkdir(dirname(evidencePath), { recursive: true });
250
+ await writeFile(evidencePath, evidenceJson, 'utf8');
251
+ }
252
+ process.stdout.write(collectionJson);
253
+ return collection.ok ? 0 : 1;
254
+ }
255
+
256
+ if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
257
+ runCli().then((code) => {
258
+ process.exitCode = code;
259
+ }).catch((error) => {
260
+ process.stderr.write(`${error.stack ?? error.message}\n`);
261
+ process.exitCode = 1;
262
+ });
263
+ }