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,944 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { pathToFileURL } from 'node:url';
4
+
5
+ export const INFRASTRUCTURE_READINESS_SCHEMA = 'enigma.infrastructure_readiness.v1';
6
+ export const INFRASTRUCTURE_READINESS_MANIFEST_SCHEMA = 'enigma.infrastructure_readiness_manifest.v1';
7
+
8
+ const CLOUDFLARE_API_BASE = 'https://api.cloudflare.com/client/v4';
9
+
10
+ const HELP_TEXT = `Usage: node scripts/infrastructure-readiness.mjs --manifest <path> [--live] [--cloudflare-live off|auto|required] [--allow-localhost]
11
+
12
+ Emits one JSON document describing Enigma infrastructure readiness.
13
+ Default contract-only mode performs no network checks and never claims hosted live readiness.
14
+
15
+ Options:
16
+ --manifest <path> Readiness manifest JSON.
17
+ --live Run manifest-requested public/relay/gateway live checks using fetch.
18
+ --cloudflare-live <mode> off, auto, or required. Defaults to off.
19
+ --allow-localhost Permit localhost/private URLs in manifests and live checks.
20
+ --help Emit JSON containing this help text.
21
+
22
+ Environment for optional Cloudflare observation:
23
+ CLOUDFLARE_API_TOKEN Used only for Cloudflare API endpoints; never printed.
24
+ CLOUDFLARE_ACCOUNT_ID Optional account id when manifest omits cloudflare.account_id.
25
+ `;
26
+
27
+ class UsageError extends Error {
28
+ constructor(message) {
29
+ super(message);
30
+ this.name = 'UsageError';
31
+ }
32
+ }
33
+
34
+ const SECRET_KEY_PATTERN = /(?:^(?:token|pat)$|authorization|bearer|cookie|credential|password|passwd|private[_-]?key|client[_-]?secret|secret|raw[_-]?memory|plaintext|plain[_-]?text|prompt|completion|transcript|provider[_-]?response|(?:api|access|auth|bearer|refresh|id|session|oauth|jwt|github|gitlab|cloudflare|cloudflare[_-]?api|cloudflare[_-]?access)[_-]?(?:key|token))/i;
35
+ const SAFE_SECRET_NAMED_KEYS = new Set(['kms_or_secret_custody']);
36
+ const SECRET_VALUE_PATTERNS = [
37
+ /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/,
38
+ /\b(?:sk|rk)-[A-Za-z0-9_-]{20,}\b/,
39
+ /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/,
40
+ /\bgithub_pat_[A-Za-z0-9_]{20,}\b/,
41
+ /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/,
42
+ /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
43
+ /\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/-]+=*\b/i,
44
+ /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/,
45
+ ];
46
+ const URL_CANDIDATE_PATTERN = /\b[A-Za-z][A-Za-z0-9+.-]*:\/\/[^\s"'<>]+/g;
47
+ const ALLOWED_REF_KEYS = new Set([
48
+ 'relay',
49
+ 'gateway',
50
+ 'relay_deployment',
51
+ 'relay_ref',
52
+ 'gateway_deployment',
53
+ 'gateway_ref',
54
+ 'backend_host',
55
+ 'dns_tls',
56
+ 'durable_storage',
57
+ 'kms',
58
+ 'kms_or_secret_custody',
59
+ 'siem_or_log_sink',
60
+ 'backup_restore',
61
+ 'monitoring',
62
+ 'runtime_auth',
63
+ 'admin_auth',
64
+ 'data_plane_auth',
65
+ 'siem',
66
+ 'backup',
67
+ 'operator_acceptance',
68
+ 'network_access_policy',
69
+ 'kms_custody',
70
+ 'tenant_policy_approval',
71
+ 'usage_metering',
72
+ 'service_settlement',
73
+ 'monitoring_alerting',
74
+ 'public_site_security',
75
+ 'security_threat_model',
76
+ 'legal_compliance_approval',
77
+ 'support_sla',
78
+ 'incident_drill',
79
+ 'backup_restore_drill',
80
+ 'token_approval',
81
+ ]);
82
+ const HOSTED_REQUIRED_REF_GROUPS = Object.freeze([
83
+ ['backend_host'],
84
+ ['dns_tls'],
85
+ ['durable_storage'],
86
+ ['kms_or_secret_custody', 'kms'],
87
+ ['siem_or_log_sink', 'siem'],
88
+ ['backup_restore', 'backup'],
89
+ ['monitoring'],
90
+ ['runtime_auth'],
91
+ ['admin_auth'],
92
+ ['data_plane_auth'],
93
+ ['network_access_policy'],
94
+ ['kms_custody'],
95
+ ['tenant_policy_approval'],
96
+ ['usage_metering'],
97
+ ['service_settlement'],
98
+ ['monitoring_alerting'],
99
+ ['public_site_security'],
100
+ ['security_threat_model'],
101
+ ['legal_compliance_approval'],
102
+ ['support_sla'],
103
+ ['incident_drill'],
104
+ ['backup_restore_drill'],
105
+ ]);
106
+
107
+
108
+ function safeJson(value) {
109
+ return `${JSON.stringify(value, null, 2)}\n`;
110
+ }
111
+
112
+ function requireObject(name, value) {
113
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
114
+ throw new UsageError(`${name} must be an object`);
115
+ }
116
+ return value;
117
+ }
118
+
119
+ function asOptionalObject(name, value) {
120
+ if (value === undefined || value === null) return null;
121
+ return requireObject(name, value);
122
+ }
123
+
124
+ function nonEmptyString(value) {
125
+ return typeof value === 'string' && value.trim().length > 0;
126
+ }
127
+
128
+ function normalizedString(value) {
129
+ return nonEmptyString(value) ? value.trim() : null;
130
+ }
131
+
132
+ function stringArray(value, name, { defaultValue = [] } = {}) {
133
+ if (value === undefined || value === null) return defaultValue;
134
+ if (!Array.isArray(value)) throw new UsageError(`${name} must be an array of strings`);
135
+ const result = [];
136
+ for (const [index, item] of value.entries()) {
137
+ if (!nonEmptyString(item)) throw new UsageError(`${name}[${index}] must be a non-empty string`);
138
+ result.push(item.trim());
139
+ }
140
+ return result;
141
+ }
142
+
143
+ function boolValue(value, name, defaultValue = false) {
144
+ if (value === undefined || value === null) return defaultValue;
145
+ if (typeof value !== 'boolean') throw new UsageError(`${name} must be a boolean`);
146
+ return value;
147
+ }
148
+
149
+ function intValue(value, name, defaultValue = null) {
150
+ if (value === undefined || value === null) return defaultValue;
151
+ if (!Number.isInteger(value)) throw new UsageError(`${name} must be an integer`);
152
+ return value;
153
+ }
154
+
155
+ function looksLikeSecretValue(value) {
156
+ if (typeof value !== 'string') return false;
157
+ const text = value.trim();
158
+ if (text.length === 0) return false;
159
+ return SECRET_VALUE_PATTERNS.some((pattern) => pattern.test(text));
160
+ }
161
+
162
+ function looksLikeCredentialBearingUrl(value) {
163
+ if (typeof value !== 'string') return false;
164
+ const text = value.trim();
165
+ if (text.length === 0) return false;
166
+ URL_CANDIDATE_PATTERN.lastIndex = 0;
167
+ for (let match = URL_CANDIDATE_PATTERN.exec(text); match !== null; match = URL_CANDIDATE_PATTERN.exec(text)) {
168
+ try {
169
+ const parsed = new URL(match[0]);
170
+ if (parsed.username !== '' || parsed.password !== '') return true;
171
+ } catch {
172
+ return true;
173
+ }
174
+ }
175
+ return false;
176
+ }
177
+
178
+ export function assertNoSecretMaterial(value, { path = '$' } = {}) {
179
+ if (Array.isArray(value)) {
180
+ for (const [index, item] of value.entries()) {
181
+ assertNoSecretMaterial(item, { path: `${path}[${index}]` });
182
+ }
183
+ return true;
184
+ }
185
+ if (value !== null && typeof value === 'object') {
186
+ for (const [key, item] of Object.entries(value)) {
187
+ const childPath = `${path}.${key}`;
188
+ if (SECRET_KEY_PATTERN.test(key) && !SAFE_SECRET_NAMED_KEYS.has(key)) {
189
+ throw new UsageError(`secret-looking manifest key rejected at ${childPath}`);
190
+ }
191
+ assertNoSecretMaterial(item, { path: childPath });
192
+ }
193
+ return true;
194
+ }
195
+ if (looksLikeCredentialBearingUrl(value)) {
196
+ throw new UsageError(`secret-looking URL credentials rejected at ${path}`);
197
+ }
198
+ if (looksLikeSecretValue(value)) {
199
+ throw new UsageError(`secret-looking manifest value rejected at ${path}`);
200
+ }
201
+ return true;
202
+ }
203
+
204
+ function parseUrlField(value, name, { allowLocalhost }) {
205
+ if (value === undefined || value === null) return null;
206
+ if (!nonEmptyString(value)) throw new UsageError(`${name} must be a non-empty URL string`);
207
+ let parsed;
208
+ try {
209
+ parsed = new URL(value.trim());
210
+ } catch {
211
+ throw new UsageError(`${name} must be a valid URL`);
212
+ }
213
+ if (parsed.username !== '' || parsed.password !== '') {
214
+ throw new UsageError(`${name} must not include URL userinfo`);
215
+ }
216
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
217
+ throw new UsageError(`${name} must use http or https`);
218
+ }
219
+ const localOrPrivate = isLocalOrPrivateHostname(parsed.hostname);
220
+ if (localOrPrivate && !allowLocalhost) {
221
+ throw new UsageError(`${name} points to localhost or private infrastructure; pass --allow-localhost only for local demos`);
222
+ }
223
+ if (parsed.protocol !== 'https:' && !(allowLocalhost && localOrPrivate)) {
224
+ throw new UsageError(`${name} must use https unless --allow-localhost is explicitly permitting a localhost/private demo URL`);
225
+ }
226
+ return parsed.href;
227
+ }
228
+
229
+ function isLocalOrPrivateHostname(hostname) {
230
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, '');
231
+ if (host === 'localhost' || host === '::1' || host === '::' || host === '0.0.0.0') return true;
232
+ if (host.endsWith('.localhost') || host.endsWith('.local')) return true;
233
+ if (/^(?:fc|fd)[0-9a-f]{2}:/.test(host) || /^fe[89ab][0-9a-f]:/.test(host)) return true;
234
+ const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
235
+ if (!match) return false;
236
+ const octets = match.slice(1).map(Number);
237
+ if (octets.some((octet) => octet < 0 || octet > 255)) return true;
238
+ const [a, b] = octets;
239
+ return a === 10 || a === 127 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 169 && b === 254) || (a === 100 && b >= 64 && b <= 127);
240
+ }
241
+
242
+ function urlHostname(value) {
243
+ if (!nonEmptyString(value)) return null;
244
+ try {
245
+ return new URL(value).hostname.toLowerCase().replace(/^\[|\]$/g, '');
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
250
+
251
+ function isLocalOrPrivateUrl(value) {
252
+ const hostname = urlHostname(value);
253
+ return hostname !== null && isLocalOrPrivateHostname(hostname);
254
+ }
255
+
256
+ function assertPublicProbePath(value, name) {
257
+ const parsed = new URL(value);
258
+ if (isLocalOrPrivateHostname(parsed.hostname)) return;
259
+ if (parsed.search || parsed.hash) {
260
+ throw new UsageError(`${name} public live check URL must not include query strings or fragments`);
261
+ }
262
+ if (parsed.pathname !== '/readyz' && parsed.pathname !== '/livez') {
263
+ throw new UsageError(`${name} public live check URL must use /readyz or /livez`);
264
+ }
265
+ }
266
+
267
+ function normalizeEndpoint(root, name, options) {
268
+ const value = asOptionalObject(name, root);
269
+ if (value === null) return null;
270
+ const url = parseUrlField(value.url ?? value.ready_url ?? value.health_url ?? value.live_url, `${name}.url`, options);
271
+ const readyUrl = parseUrlField(value.ready_url, `${name}.ready_url`, options);
272
+ const healthUrl = parseUrlField(value.health_url, `${name}.health_url`, options);
273
+ const liveUrl = parseUrlField(value.live_url, `${name}.live_url`, options);
274
+ const fetchUrl = liveUrl ?? readyUrl ?? healthUrl ?? url;
275
+ const expectedStatus = intValue(value.expected_status, `${name}.expected_status`, 200);
276
+ if (expectedStatus < 100 || expectedStatus > 599) throw new UsageError(`${name}.expected_status must be an HTTP status code`);
277
+ if ((name === 'relay' || name === 'gateway') && fetchUrl !== null) assertPublicProbePath(fetchUrl, name);
278
+ return {
279
+ url,
280
+ ready_url: readyUrl,
281
+ health_url: healthUrl,
282
+ live_url: liveUrl,
283
+ fetch_url: fetchUrl,
284
+ expected_status: expectedStatus,
285
+ expected_text: stringArray(value.expected_text, `${name}.expected_text`),
286
+ required: boolValue(value.required, `${name}.required`, false),
287
+ ref: normalizedString(value.ref ?? value.reference ?? value.deployment_ref),
288
+ unauthenticated_local_demo: boolValue(value.unauthenticated_local_demo, `${name}.unauthenticated_local_demo`, false),
289
+ internal: boolValue(value.internal, `${name}.internal`, false),
290
+ };
291
+ }
292
+
293
+ function normalizeGithub(root, options) {
294
+ const value = asOptionalObject('github', root);
295
+ if (value === null) return null;
296
+ const repository = normalizedString(value.repository ?? value.repo ?? value.repository_full_name);
297
+ const url = parseUrlField(value.url ?? value.repository_url, 'github.url', options);
298
+ if (repository !== null && !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) {
299
+ throw new UsageError('github.repository must use owner/name form');
300
+ }
301
+ return {
302
+ repository,
303
+ url,
304
+ ref: normalizedString(value.ref ?? value.commit ?? value.release_ref),
305
+ pages_branch: normalizedString(value.pages_branch),
306
+ workflow_ref: normalizedString(value.workflow_ref),
307
+ };
308
+ }
309
+
310
+ function normalizeCloudflare(root, options) {
311
+ const value = asOptionalObject('cloudflare', root);
312
+ if (value === null) return null;
313
+ return {
314
+ account_id: normalizedString(value.account_id),
315
+ project_name: normalizedString(value.project_name ?? value.pages_project ?? value.pages_project_name),
316
+ pages_url: parseUrlField(value.pages_url ?? value.project_url, 'cloudflare.pages_url', options),
317
+ custom_domains: stringArray(value.custom_domains, 'cloudflare.custom_domains'),
318
+ ref: normalizedString(value.ref ?? value.deployment_ref),
319
+ };
320
+ }
321
+
322
+ function normalizeOperatorAcceptance(root) {
323
+ const value = asOptionalObject('operator_acceptance', root);
324
+ if (value === null) return { decision: 'pending', ref: null, owner: null };
325
+ const rawDecision = normalizedString(value.decision ?? value.status) ?? 'pending';
326
+ const decision = rawDecision === 'no_go' ? 'no-go' : rawDecision;
327
+ if (!['go', 'no-go', 'pending'].includes(decision)) {
328
+ throw new UsageError('operator_acceptance.decision must be go, no-go, or pending');
329
+ }
330
+ return {
331
+ decision,
332
+ ref: normalizedString(value.ref ?? value.packet_ref ?? value.evidence_ref),
333
+ owner: normalizedString(value.owner),
334
+ };
335
+ }
336
+
337
+ function normalizeRefs(value) {
338
+ if (value === undefined || value === null) return {};
339
+ const refs = requireObject('refs', value);
340
+ const result = {};
341
+ for (const [key, item] of Object.entries(refs)) {
342
+ if (!ALLOWED_REF_KEYS.has(key)) throw new UsageError(`refs.${key} is not supported by the readiness manifest schema`);
343
+ if (item === undefined || item === null) continue;
344
+ if (!nonEmptyString(item)) throw new UsageError(`refs.${key} must be a non-empty string`);
345
+ const ref = item.trim();
346
+ assertNoSecretMaterial(ref, { path: `refs.${key}` });
347
+ result[key] = ref;
348
+ }
349
+ return result;
350
+ }
351
+
352
+ function normalizeClaimBoundary(value) {
353
+ if (value === undefined || value === null) throw new UsageError('claim_boundary must be a non-empty string or array of strings');
354
+ if (Array.isArray(value)) {
355
+ const items = stringArray(value, 'claim_boundary');
356
+ if (items.length === 0) throw new UsageError('claim_boundary must not be empty');
357
+ return items;
358
+ }
359
+ if (!nonEmptyString(value)) throw new UsageError('claim_boundary must be a non-empty string or array of strings');
360
+ return [value.trim()];
361
+ }
362
+
363
+ export function validateInfrastructureReadinessManifest(value, { allowLocalhost = false } = {}) {
364
+ const parsed = typeof value === 'string' ? JSON.parse(value) : value;
365
+ assertNoSecretMaterial(parsed);
366
+ const root = requireObject('manifest', parsed);
367
+ if (root.schema !== INFRASTRUCTURE_READINESS_MANIFEST_SCHEMA) {
368
+ throw new UsageError(`manifest.schema must be ${INFRASTRUCTURE_READINESS_MANIFEST_SCHEMA}`);
369
+ }
370
+ const options = { allowLocalhost };
371
+ const externalBlockers = stringArray(root.external_blockers, 'external_blockers');
372
+ const cloudflareSource = root.cloudflare ?? root.cloudflare_pages;
373
+ return {
374
+ schema: INFRASTRUCTURE_READINESS_MANIFEST_SCHEMA,
375
+ name: normalizedString(root.name),
376
+ public_site: normalizeEndpoint(root.public_site, 'public_site', options),
377
+ github: normalizeGithub(root.github, options),
378
+ cloudflare: normalizeCloudflare(cloudflareSource, options),
379
+ relay: normalizeEndpoint(root.relay, 'relay', options),
380
+ gateway: normalizeEndpoint(root.gateway, 'gateway', options),
381
+ refs: normalizeRefs(root.refs),
382
+ operator_acceptance: normalizeOperatorAcceptance(root.operator_acceptance),
383
+ external_blockers: externalBlockers,
384
+ claim_boundary: normalizeClaimBoundary(root.claim_boundary),
385
+ };
386
+ }
387
+
388
+ export function parseInfrastructureReadinessArgs(argv = []) {
389
+ const command = {
390
+ manifestPath: null,
391
+ live: false,
392
+ cloudflareLive: 'off',
393
+ allowLocalhost: false,
394
+ help: false,
395
+ liveChecks: null,
396
+ operatorDecision: null,
397
+ };
398
+ for (let index = 0; index < argv.length; index += 1) {
399
+ const arg = argv[index];
400
+ if (arg === '--help' || arg === '-h') {
401
+ command.help = true;
402
+ continue;
403
+ }
404
+ if (arg === '--live') {
405
+ command.live = true;
406
+ continue;
407
+ }
408
+ if (arg === '--mode') {
409
+ const value = argv[index + 1];
410
+ if (value === 'hosted-live') command.live = true;
411
+ else if (value === 'contract-only') command.live = false;
412
+ else throw new UsageError('--mode must be contract-only or hosted-live');
413
+ index += 1;
414
+ continue;
415
+ }
416
+ if (arg === '--live-checks') {
417
+ const value = argv[index + 1];
418
+ if (!nonEmptyString(value)) throw new UsageError('--live-checks requires a comma-separated list');
419
+ command.liveChecks = value.split(',').map((item) => item.trim()).filter(Boolean);
420
+ index += 1;
421
+ continue;
422
+ }
423
+ if (arg === '--operator-decision') {
424
+ const value = argv[index + 1];
425
+ if (!['go', 'no-go', 'no_go', 'pending'].includes(value)) throw new UsageError('--operator-decision must be go, no-go, no_go, or pending');
426
+ command.operatorDecision = value === 'no_go' ? 'no-go' : value;
427
+ index += 1;
428
+ continue;
429
+ }
430
+ if (arg === '--allow-localhost') {
431
+ command.allowLocalhost = true;
432
+ continue;
433
+ }
434
+ if (arg === '--manifest') {
435
+ const value = argv[index + 1];
436
+ if (!nonEmptyString(value)) throw new UsageError('--manifest requires a path');
437
+ command.manifestPath = value;
438
+ index += 1;
439
+ continue;
440
+ }
441
+ if (arg === '--cloudflare-live') {
442
+ const value = argv[index + 1];
443
+ if (!['off', 'auto', 'required'].includes(value)) throw new UsageError('--cloudflare-live must be off, auto, or required');
444
+ command.cloudflareLive = value;
445
+ index += 1;
446
+ continue;
447
+ }
448
+ throw new UsageError(`unknown option: ${arg}`);
449
+ }
450
+ if (!command.help && command.manifestPath === null) throw new UsageError('--manifest is required');
451
+ return command;
452
+ }
453
+
454
+ function hasRef(value) {
455
+ return nonEmptyString(value);
456
+ }
457
+
458
+ function endpointRef(endpoint, refs, endpointName) {
459
+ return endpoint?.ref ?? refs[endpointName] ?? refs[`${endpointName}_deployment`] ?? refs[`${endpointName}_ref`] ?? null;
460
+ }
461
+
462
+ function anyRef(refs, names) {
463
+ for (const name of names) {
464
+ if (hasRef(refs[name])) return true;
465
+ }
466
+ return false;
467
+ }
468
+
469
+ function contractFindings(manifest) {
470
+ const missing = [];
471
+ if (!hasRef(manifest.public_site?.url)) missing.push('public_site.url');
472
+ if (manifest.cloudflare === null || (!hasRef(manifest.cloudflare.project_name) && !hasRef(manifest.cloudflare.pages_url))) {
473
+ missing.push('cloudflare.project_name_or_pages_url');
474
+ }
475
+ if (manifest.claim_boundary.length === 0) missing.push('claim_boundary');
476
+ return missing;
477
+ }
478
+
479
+ function hostedFindings(manifest) {
480
+ const missing = [];
481
+ if (!hasRef(endpointRef(manifest.relay, manifest.refs, 'relay'))) missing.push('relay.ref');
482
+ if (!hasRef(endpointRef(manifest.gateway, manifest.refs, 'gateway'))) missing.push('gateway.ref');
483
+ const requiredRefGroups = HOSTED_REQUIRED_REF_GROUPS;
484
+ for (const names of requiredRefGroups) {
485
+ if (!anyRef(manifest.refs, names)) missing.push(`refs.${names[0]}`);
486
+ }
487
+ if (!hasRef(manifest.operator_acceptance.ref ?? manifest.refs.operator_acceptance)) missing.push('operator_acceptance.ref');
488
+ return missing;
489
+ }
490
+
491
+ function addCheck(checks, name, ok, details = {}) {
492
+ const check = { name, ok: ok === true, ...details };
493
+ checks.push(check);
494
+ return check.ok;
495
+ }
496
+
497
+ function buildFetchError(error) {
498
+ return error?.message ?? String(error);
499
+ }
500
+
501
+ async function fetchPublicText(url, { fetchImpl }) {
502
+ if (typeof fetchImpl !== 'function') throw new UsageError('fetch implementation is required for --live');
503
+ const response = await fetchImpl(url, {
504
+ method: 'GET',
505
+ headers: { accept: 'text/html,application/xhtml+xml,text/plain;q=0.9,*/*;q=0.1' },
506
+ redirect: 'follow',
507
+ });
508
+ const text = typeof response.text === 'function' ? await response.text() : '';
509
+ return { status: response.status, ok: response.ok === true || (response.status >= 200 && response.status < 400), text };
510
+ }
511
+
512
+ async function fetchJsonEndpoint(url, { fetchImpl }) {
513
+ if (typeof fetchImpl !== 'function') throw new UsageError('fetch implementation is required for --live');
514
+ const response = await fetchImpl(url, {
515
+ method: 'GET',
516
+ headers: { accept: 'application/json' },
517
+ redirect: 'manual',
518
+ });
519
+ const statusCode = Number(response.status ?? 0);
520
+ const responseUrl = typeof response.url === 'string' && response.url.length > 0 ? response.url : url;
521
+ if (response.redirected === true || responseUrl !== url || (statusCode >= 300 && statusCode <= 399)) {
522
+ throw new UsageError(`public JSON endpoint ${url} must not redirect`);
523
+ }
524
+ let payload = null;
525
+ const contentType = typeof response.headers?.get === 'function' ? response.headers.get('content-type') ?? '' : '';
526
+ if (typeof response.json === 'function' && (contentType.includes('json') || statusCode !== 204)) {
527
+ try {
528
+ payload = await response.json();
529
+ } catch {
530
+ payload = null;
531
+ }
532
+ }
533
+ return { status: statusCode, ok: response.ok === true || (statusCode >= 200 && statusCode < 300), payload };
534
+ }
535
+
536
+ function responseHasRefs(payload) {
537
+ if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return false;
538
+ if (hasRef(payload.ref) || hasRef(payload.version) || hasRef(payload.build_ref) || hasRef(payload.deployment_ref) || hasRef(payload.release_ref) || hasRef(payload.provenance_ref)) return true;
539
+ const refs = payload.refs;
540
+ if (refs !== null && typeof refs === 'object' && !Array.isArray(refs)) {
541
+ return Object.values(refs).some((value) => hasRef(value));
542
+ }
543
+ return false;
544
+ }
545
+
546
+ function refsCoverHostedDependencies(refs) {
547
+ if (refs === null || typeof refs !== 'object' || Array.isArray(refs)) return false;
548
+ return HOSTED_REQUIRED_REF_GROUPS.every((group) => group.some((key) => hasRef(refs[key])));
549
+ }
550
+
551
+ function checksAllOk(payload) {
552
+ return Array.isArray(payload?.checks) && payload.checks.length > 0 && payload.checks.every((check) => check?.ok === true);
553
+ }
554
+
555
+ function hostedReadyShapeOk(payload, expectedService) {
556
+ if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return false;
557
+ assertNoSecretMaterial(payload, { path: `${expectedService}.readyz` });
558
+ if (payload.unauthenticated_local_demo === true || payload.internal === true) return false;
559
+ if (payload.service !== expectedService) return false;
560
+ if (payload.ok !== true || payload.status !== 'ready') return false;
561
+ if (!checksAllOk(payload)) return false;
562
+ if (!Array.isArray(payload.missing_evidence_refs) || payload.missing_evidence_refs.length !== 0) return false;
563
+ return refsCoverHostedDependencies(payload.evidence_refs);
564
+ }
565
+
566
+ function relayShapeOk(payload) {
567
+ return hostedReadyShapeOk(payload, 'enigma-relay');
568
+ }
569
+
570
+ function gatewayShapeOk(payload) {
571
+ return hostedReadyShapeOk(payload, 'enigma-gateway');
572
+ }
573
+
574
+ function cloudflareHostValue(value) {
575
+ if (!nonEmptyString(value)) return null;
576
+ const hostname = value.includes('://') ? urlHostname(value) : value.trim().toLowerCase();
577
+ return hostname === null || hostname === '' ? null : hostname.replace(/^\[|\]$/g, '');
578
+ }
579
+
580
+ function collectCloudflareProjectHosts(projectPayload) {
581
+ const result = projectPayload?.result;
582
+ const hosts = new Set();
583
+ const add = (value) => {
584
+ const hostname = cloudflareHostValue(value);
585
+ if (hostname !== null) hosts.add(hostname);
586
+ };
587
+ add(result?.subdomain);
588
+ for (const domain of Array.isArray(result?.domains) ? result.domains : []) add(domain);
589
+ for (const deployment of [result?.latest_deployment, result?.canonical_deployment]) {
590
+ add(deployment?.url);
591
+ for (const alias of Array.isArray(deployment?.aliases) ? deployment.aliases : []) add(alias);
592
+ }
593
+ return hosts;
594
+ }
595
+
596
+ function cloudflareProjectMatchesDeclared(manifest, projectPayload) {
597
+ const hosts = collectCloudflareProjectHosts(projectPayload);
598
+ const expectedPagesHost = urlHostname(manifest.cloudflare?.pages_url);
599
+ const expectedDomains = manifest.cloudflare?.custom_domains ?? [];
600
+ const pagesUrlOk = expectedPagesHost === null || hosts.has(expectedPagesHost);
601
+ const domainsOk = expectedDomains.every((domain) => {
602
+ const hostname = cloudflareHostValue(domain);
603
+ return hostname !== null && hosts.has(hostname);
604
+ });
605
+ return { pagesUrlOk, domainsOk, host_count: hosts.size };
606
+ }
607
+
608
+ async function cloudflareJson(fetchImpl, path, headers) {
609
+ const response = await fetchImpl(`${CLOUDFLARE_API_BASE}${path}`, { method: 'GET', headers });
610
+ let payload = null;
611
+ try {
612
+ payload = typeof response.json === 'function' ? await response.json() : null;
613
+ } catch {
614
+ payload = null;
615
+ }
616
+ const ok = (response.ok === true || (response.status >= 200 && response.status < 300)) && payload?.success !== false;
617
+ return { response, payload, ok };
618
+ }
619
+
620
+ async function observeCloudflare(manifest, { env, fetchImpl, mode }) {
621
+ if (mode === 'off') return { observed: false, required: false, ok: true, credentialsUsed: false, skipped: true, reason: 'cloudflare-live off' };
622
+ const token = env.CLOUDFLARE_API_TOKEN;
623
+ const accountId = manifest.cloudflare?.account_id ?? env.CLOUDFLARE_ACCOUNT_ID ?? null;
624
+ const projectName = manifest.cloudflare?.project_name ?? null;
625
+ if (!nonEmptyString(token)) {
626
+ return { observed: false, required: mode === 'required', ok: mode !== 'required', credentialsUsed: false, skipped: true, reason: 'CLOUDFLARE_API_TOKEN not provided' };
627
+ }
628
+ if (typeof fetchImpl !== 'function') throw new UsageError('fetch implementation is required for Cloudflare live observation');
629
+ const headers = { authorization: `Bearer ${token}`, accept: 'application/json' };
630
+ const accountScopedToken = nonEmptyString(accountId);
631
+ const tokenScope = accountScopedToken ? 'account' : 'user';
632
+ const tokenVerifyPath = accountScopedToken
633
+ ? `/accounts/${encodeURIComponent(accountId)}/tokens/verify`
634
+ : '/user/tokens/verify';
635
+ const tokenCheck = await cloudflareJson(fetchImpl, tokenVerifyPath, headers);
636
+ const tokenOk = tokenCheck.ok;
637
+ let accountOk = true;
638
+ let accountStatus = null;
639
+ if (nonEmptyString(accountId)) {
640
+ const accountCheck = await cloudflareJson(fetchImpl, `/accounts/${encodeURIComponent(accountId)}`, headers);
641
+ accountStatus = accountCheck.response.status;
642
+ accountOk = accountCheck.ok;
643
+ }
644
+ let projectOk = true;
645
+ let projectStatus = null;
646
+ let pagesUrlOk = true;
647
+ let domainsOk = true;
648
+ let projectHostCount = 0;
649
+ let domainsStatus = null;
650
+ let projectReason = null;
651
+ const declaredPagesEvidence = nonEmptyString(manifest.cloudflare?.pages_url) || (manifest.cloudflare?.custom_domains ?? []).length > 0;
652
+ const canCheckProject = nonEmptyString(accountId) && nonEmptyString(projectName);
653
+ if (canCheckProject) {
654
+ const projectCheck = await cloudflareJson(fetchImpl, `/accounts/${encodeURIComponent(accountId)}/pages/projects/${encodeURIComponent(projectName)}`, headers);
655
+ projectStatus = projectCheck.response.status;
656
+ projectOk = projectCheck.ok && projectCheck.payload?.result?.name === projectName;
657
+ const declared = cloudflareProjectMatchesDeclared(manifest, projectCheck.payload);
658
+ pagesUrlOk = declared.pagesUrlOk;
659
+ domainsOk = declared.domainsOk;
660
+ projectHostCount = declared.host_count;
661
+ const expectedDomains = manifest.cloudflare?.custom_domains ?? [];
662
+ if (expectedDomains.length > 0 && !domainsOk) {
663
+ const domainsCheck = await cloudflareJson(fetchImpl, `/accounts/${encodeURIComponent(accountId)}/pages/projects/${encodeURIComponent(projectName)}/domains`, headers);
664
+ domainsStatus = domainsCheck.response.status;
665
+ const domainHosts = new Set();
666
+ for (const domain of Array.isArray(domainsCheck.payload?.result) ? domainsCheck.payload.result : []) {
667
+ for (const value of [domain?.name, domain?.hostname, domain?.domain]) {
668
+ const hostname = cloudflareHostValue(value);
669
+ if (hostname !== null) domainHosts.add(hostname);
670
+ }
671
+ }
672
+ domainsOk = domainsCheck.ok && expectedDomains.every((domain) => {
673
+ const hostname = cloudflareHostValue(domain);
674
+ return hostname !== null && domainHosts.has(hostname);
675
+ });
676
+ projectHostCount += domainHosts.size;
677
+ }
678
+ } else if (declaredPagesEvidence) {
679
+ projectOk = false;
680
+ projectReason = 'Cloudflare Pages project observation requires account_id and project_name';
681
+ }
682
+ return {
683
+ observed: tokenOk && accountOk && projectOk && pagesUrlOk && domainsOk,
684
+ required: mode === 'required',
685
+ ok: tokenOk && accountOk && projectOk && pagesUrlOk && domainsOk,
686
+ credentialsUsed: true,
687
+ token_status: tokenCheck.response.status,
688
+ token_scope: tokenScope,
689
+ account_checked: nonEmptyString(accountId),
690
+ account_status: accountStatus,
691
+ project_status: projectStatus,
692
+ domains_status: domainsStatus,
693
+ project_checked: canCheckProject,
694
+ pages_url_matched: pagesUrlOk,
695
+ custom_domains_matched: domainsOk,
696
+ project_host_count: projectHostCount,
697
+ reason: projectReason,
698
+ };
699
+ }
700
+
701
+ async function runPublicSiteCheck(manifest, checks, options) {
702
+ const site = manifest.public_site;
703
+ if (site?.fetch_url === null || site === null) {
704
+ addCheck(checks, 'public_site.live', false, { skipped: true, reason: 'public_site.url missing' });
705
+ return false;
706
+ }
707
+ try {
708
+ const response = await fetchPublicText(site.fetch_url, options);
709
+ const statusOk = response.status === site.expected_status || (site.expected_status === 200 && response.status >= 200 && response.status < 400);
710
+ const textOk = site.expected_text.every((needle) => response.text.includes(needle));
711
+ return addCheck(checks, 'public_site.live', response.ok && statusOk && textOk, {
712
+ status: response.status,
713
+ expected_status: site.expected_status,
714
+ expected_text_matched: textOk,
715
+ });
716
+ } catch (error) {
717
+ addCheck(checks, 'public_site.live', false, { error: buildFetchError(error) });
718
+ return false;
719
+ }
720
+ }
721
+
722
+ async function runEndpointShapeCheck(name, endpoint, checks, options, shapeOk) {
723
+ if (endpoint === null || endpoint.fetch_url === null) {
724
+ addCheck(checks, `${name}.live`, false, { skipped: true, reason: `${name}.url missing` });
725
+ return false;
726
+ }
727
+ if (endpoint.unauthenticated_local_demo === true) {
728
+ addCheck(checks, `${name}.live`, false, { reason: 'unauthenticated_local_demo is not hosted readiness evidence' });
729
+ return false;
730
+ }
731
+ if (endpoint.internal === true) {
732
+ addCheck(checks, `${name}.live`, false, { reason: 'internal endpoint is not public hosted readiness evidence' });
733
+ return false;
734
+ }
735
+ try {
736
+ const response = await fetchJsonEndpoint(endpoint.fetch_url, options);
737
+ const ok = response.ok && shapeOk(response.payload);
738
+ return addCheck(checks, `${name}.live`, ok, {
739
+ status: response.status,
740
+ has_refs: responseHasRefs(response.payload),
741
+ local_demo: response.payload?.unauthenticated_local_demo === true,
742
+ internal: response.payload?.internal === true,
743
+ });
744
+ } catch (error) {
745
+ addCheck(checks, `${name}.live`, false, { error: buildFetchError(error) });
746
+ return false;
747
+ }
748
+ }
749
+
750
+ function isReadinessCommand(value) {
751
+ return value !== null
752
+ && typeof value === 'object'
753
+ && !Array.isArray(value)
754
+ && (Object.hasOwn(value, 'manifestPath') || Object.hasOwn(value, 'manifest'));
755
+ }
756
+
757
+ function manifestFromCommand(command, readFileImpl) {
758
+ if (command.manifest !== undefined) return command.manifest;
759
+ if (!nonEmptyString(command.manifestPath)) throw new UsageError('--manifest is required');
760
+ return JSON.parse(readFileImpl(command.manifestPath, 'utf8'));
761
+ }
762
+
763
+ function applyCommandOverrides(manifest, command) {
764
+ if (!nonEmptyString(command.operatorDecision)) return manifest;
765
+ return {
766
+ ...manifest,
767
+ operator_acceptance: {
768
+ ...(manifest.operator_acceptance ?? {}),
769
+ decision: command.operatorDecision,
770
+ },
771
+ };
772
+ }
773
+
774
+ export async function runInfrastructureReadiness(manifestInput, options = {}) {
775
+ const commandInput = isReadinessCommand(manifestInput) ? manifestInput : null;
776
+ const {
777
+ live: optionLive,
778
+ cloudflareLive: optionCloudflareLive,
779
+ allowLocalhost: optionAllowLocalhost,
780
+ env = process.env,
781
+ fetchImpl = globalThis.fetch,
782
+ readFileImpl = readFileSync,
783
+ now = new Date(),
784
+ } = options;
785
+ const live = optionLive ?? commandInput?.live ?? false;
786
+ const cloudflareLive = optionCloudflareLive ?? commandInput?.cloudflareLive ?? 'off';
787
+ const allowLocalhost = optionAllowLocalhost ?? commandInput?.allowLocalhost ?? false;
788
+ if (!['off', 'auto', 'required'].includes(cloudflareLive)) throw new UsageError('cloudflareLive must be off, auto, or required');
789
+
790
+ const rawManifest = commandInput === null ? manifestInput : applyCommandOverrides(manifestFromCommand(commandInput, readFileImpl), commandInput);
791
+ const manifest = validateInfrastructureReadinessManifest(rawManifest, { allowLocalhost });
792
+ const checks = [];
793
+ addCheck(checks, 'manifest.secret_scan', true);
794
+ addCheck(checks, 'manifest.schema', true, { manifest_schema: INFRASTRUCTURE_READINESS_MANIFEST_SCHEMA });
795
+
796
+ const missingContractRefs = contractFindings(manifest);
797
+ const missingHostedRefs = hostedFindings(manifest);
798
+ const contractReady = addCheck(checks, 'readiness.contract', missingContractRefs.length === 0, { missing: missingContractRefs });
799
+ addCheck(checks, 'hosted.required_refs', missingHostedRefs.length === 0, { missing: missingHostedRefs, required_count: HOSTED_REQUIRED_REF_GROUPS.length + 3, missing_count: missingHostedRefs.length });
800
+ const operatorGo = addCheck(checks, 'operator_acceptance.decision', manifest.operator_acceptance.decision === 'go', { decision: manifest.operator_acceptance.decision });
801
+ const manifestBlockersClear = addCheck(checks, 'external_blockers.manifest', manifest.external_blockers.length === 0, { count: manifest.external_blockers.length });
802
+ const localhostAllowed = addCheck(checks, 'hosted.allow_localhost_boundary', allowLocalhost !== true, {
803
+ allow_localhost: allowLocalhost === true,
804
+ hosted_live_ready: false,
805
+ reason: allowLocalhost === true ? '--allow-localhost is local/demo evidence only' : null,
806
+ });
807
+
808
+ let publicLiveReady = false;
809
+ let relayLiveReady = false;
810
+ let gatewayLiveReady = false;
811
+ let cloudflareObserved = false;
812
+ let credentialsUsedCloudflare = false;
813
+
814
+ if (live === true) {
815
+ publicLiveReady = await runPublicSiteCheck(manifest, checks, { fetchImpl });
816
+ relayLiveReady = await runEndpointShapeCheck('relay', manifest.relay, checks, { fetchImpl }, relayShapeOk);
817
+ gatewayLiveReady = await runEndpointShapeCheck('gateway', manifest.gateway, checks, { fetchImpl }, gatewayShapeOk);
818
+ const cloudflare = await observeCloudflare(manifest, { env, fetchImpl, mode: cloudflareLive });
819
+ credentialsUsedCloudflare = cloudflare.credentialsUsed;
820
+ cloudflareObserved = cloudflare.observed;
821
+ const cloudflareReadinessOk = cloudflare.ok === true && cloudflare.observed === true;
822
+ addCheck(checks, 'cloudflare.observation', cloudflareReadinessOk, {
823
+ mode: cloudflareLive,
824
+ observed: cloudflare.observed,
825
+ skipped: cloudflare.skipped === true,
826
+ required: cloudflare.required,
827
+ hosted_readiness_required: true,
828
+ account_checked: cloudflare.account_checked === true,
829
+ project_checked: cloudflare.project_checked === true,
830
+ pages_url_matched: cloudflare.pages_url_matched !== false,
831
+ custom_domains_matched: cloudflare.custom_domains_matched !== false,
832
+ token_status: cloudflare.token_status ?? null,
833
+ token_scope: cloudflare.token_scope ?? null,
834
+ account_status: cloudflare.account_status ?? null,
835
+ project_status: cloudflare.project_status ?? null,
836
+ domains_status: cloudflare.domains_status ?? null,
837
+ reason: cloudflare.reason ?? null,
838
+ });
839
+ } else {
840
+ addCheck(checks, 'network.live_checks', true, { skipped: true, reason: 'contract-only mode performs no network checks' });
841
+ addCheck(checks, 'hosted.live_boundary', true, { hosted_live_ready: false, reason: 'contract-only mode cannot prove hosted live readiness' });
842
+ }
843
+
844
+ const externalBlockers = [...manifest.external_blockers];
845
+ for (const missing of missingContractRefs) externalBlockers.push(`missing ${missing}`);
846
+ for (const missing of missingHostedRefs) externalBlockers.push(`missing ${missing}`);
847
+ if (!operatorGo) externalBlockers.push(`operator acceptance decision is ${manifest.operator_acceptance.decision}`);
848
+ if (manifest.relay?.unauthenticated_local_demo === true) externalBlockers.push('relay unauthenticated_local_demo is not hosted readiness evidence');
849
+ if (manifest.gateway?.internal === true) externalBlockers.push('gateway internal:true is not hosted readiness evidence');
850
+ if (manifest.relay?.internal === true) externalBlockers.push('relay internal:true is not hosted readiness evidence');
851
+ if (manifest.gateway?.unauthenticated_local_demo === true) externalBlockers.push('gateway unauthenticated_local_demo is not hosted readiness evidence');
852
+ if (allowLocalhost === true) externalBlockers.push('--allow-localhost was used; localhost/private demo URLs cannot prove hosted live readiness');
853
+ if ([manifest.public_site, manifest.relay, manifest.gateway].some((endpoint) => isLocalOrPrivateUrl(endpoint?.fetch_url)) || isLocalOrPrivateUrl(manifest.github?.url) || isLocalOrPrivateUrl(manifest.cloudflare?.pages_url)) {
854
+ externalBlockers.push('localhost/private manifest URL cannot prove hosted live readiness');
855
+ }
856
+ if (live === true && !relayLiveReady) externalBlockers.push('relay live readiness check did not pass');
857
+ if (live === true && !gatewayLiveReady) externalBlockers.push('gateway live readiness check did not pass');
858
+ const cloudflareObservationCheck = checks.find((check) => check.name === 'cloudflare.observation');
859
+ const cloudflareObservationOk = cloudflareObservationCheck?.ok === true;
860
+ const cloudflareObservedOk = live !== true || cloudflareObserved === true;
861
+ const cloudflareCredentialedOk = !(live === true && cloudflareLive !== 'off' && credentialsUsedCloudflare === true && !cloudflareObservationOk);
862
+ const cloudflareRequiredOk = cloudflareLive !== 'required' || cloudflareObservationOk;
863
+ if (live === true && !cloudflareObservedOk) externalBlockers.push('Cloudflare live observation did not complete');
864
+ if (!cloudflareCredentialedOk) externalBlockers.push('credentialed Cloudflare observation did not pass');
865
+ if (cloudflareLive === 'required' && !cloudflareRequiredOk) externalBlockers.push('required Cloudflare observation did not pass');
866
+
867
+ const hostedLiveReady = allowLocalhost !== true
868
+ && localhostAllowed
869
+ && live === true
870
+ && contractReady
871
+ && publicLiveReady
872
+ && relayLiveReady
873
+ && gatewayLiveReady
874
+ && cloudflareObservedOk
875
+ && cloudflareCredentialedOk
876
+ && cloudflareRequiredOk
877
+ && operatorGo
878
+ && manifestBlockersClear
879
+ && externalBlockers.length === 0;
880
+
881
+ const ok = live === true
882
+ ? contractReady && publicLiveReady && relayLiveReady && gatewayLiveReady && cloudflareObservedOk && cloudflareCredentialedOk && cloudflareRequiredOk && operatorGo && manifestBlockersClear && externalBlockers.length === 0
883
+ : contractReady;
884
+
885
+ return {
886
+ schema: INFRASTRUCTURE_READINESS_SCHEMA,
887
+ ok,
888
+ generated_at: now.toISOString(),
889
+ mode: live === true ? 'live' : 'contract',
890
+ credentials_required: false,
891
+ credentials_used: {
892
+ cloudflare_api_token: credentialsUsedCloudflare,
893
+ },
894
+ readiness: {
895
+ contract_ready: contractReady,
896
+ public_live_ready: live === true ? publicLiveReady : false,
897
+ cloudflare_observed: cloudflareObserved,
898
+ hosted_live_ready: hostedLiveReady,
899
+ },
900
+ checks,
901
+ external_blockers: [...new Set(externalBlockers)],
902
+ claim_boundary: manifest.claim_boundary,
903
+ };
904
+ }
905
+
906
+ export function infrastructureReadinessHelpText() {
907
+ return HELP_TEXT;
908
+ }
909
+
910
+ async function main(argv = process.argv.slice(2), env = process.env) {
911
+ try {
912
+ const command = parseInfrastructureReadinessArgs(argv);
913
+ if (command.help) {
914
+ process.stdout.write(safeJson({
915
+ schema: INFRASTRUCTURE_READINESS_SCHEMA,
916
+ ok: true,
917
+ mode: 'help',
918
+ credentials_required: false,
919
+ credentials_used: { cloudflare_api_token: false },
920
+ help: infrastructureReadinessHelpText(),
921
+ }));
922
+ return 0;
923
+ }
924
+ const result = await runInfrastructureReadiness(command, {
925
+ env,
926
+ });
927
+ process.stdout.write(safeJson(result));
928
+ return result.ok ? 0 : 1;
929
+ } catch (error) {
930
+ const message = error instanceof UsageError ? error.message : (error?.message ?? String(error));
931
+ process.stderr.write(safeJson({
932
+ schema: INFRASTRUCTURE_READINESS_SCHEMA,
933
+ ok: false,
934
+ error: message,
935
+ credentials_required: false,
936
+ credentials_used: { cloudflare_api_token: false },
937
+ }));
938
+ return 1;
939
+ }
940
+ }
941
+
942
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
943
+ process.exitCode = await main();
944
+ }