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,1672 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFile as execFileCallback } from 'node:child_process';
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
import { applyCloudflareSecretEnvFileFromArgv, CloudflareSecretEnvError } from './cloudflare-secret-env.mjs';
|
|
9
|
+
|
|
10
|
+
const execFile = promisify(execFileCallback);
|
|
11
|
+
|
|
12
|
+
export const CLOUDFLARE_API_BASE = 'https://api.cloudflare.com/client/v4';
|
|
13
|
+
export const CLOUDFLARE_OPS_SCHEMA = 'enigma.cloudflare_ops.v1';
|
|
14
|
+
export const INFRASTRUCTURE_READINESS_SCHEMA = 'enigma.infrastructure_readiness.v1';
|
|
15
|
+
export const INFRASTRUCTURE_READINESS_MANIFEST_SCHEMA = 'enigma.infrastructure_readiness_manifest.v1';
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
const REGISTRATION_CHARGE_FLAG = '--i-understand-this-charges-my-payment-method';
|
|
19
|
+
const HELP_TEXT = `Usage: node scripts/cloudflare-ops.mjs <command> [options]
|
|
20
|
+
|
|
21
|
+
Safe Cloudflare operations helper. Read-only Cloudflare API commands call Cloudflare
|
|
22
|
+
only when a token is present or explicitly required.
|
|
23
|
+
Billable registration, Pages deployment, and Worker probe deployment are dry-run plans unless --execute is provided.
|
|
24
|
+
|
|
25
|
+
Commands:
|
|
26
|
+
token verify [--account-id <id>]
|
|
27
|
+
GET /accounts/:account_id/tokens/verify when an account id is present;
|
|
28
|
+
otherwise GET /user/tokens/verify.
|
|
29
|
+
accounts list
|
|
30
|
+
GET /accounts using CLOUDFLARE_API_TOKEN.
|
|
31
|
+
|
|
32
|
+
registrar search --query <q> [--limit n] [--account-id <id>]
|
|
33
|
+
GET /accounts/:account_id/registrar/domain-search.
|
|
34
|
+
|
|
35
|
+
registrar check --domain <domain> [--account-id <id>]
|
|
36
|
+
POST /accounts/:account_id/registrar/domain-check.
|
|
37
|
+
|
|
38
|
+
registrar register --domain <domain> --max-price-usd <amount> \\
|
|
39
|
+
--confirm-domain <domain> --confirm-registration-cost <amount> \\
|
|
40
|
+
[--registrant-contact-json <path>] ${REGISTRATION_CHARGE_FLAG} [--account-id <id>] [--execute]
|
|
41
|
+
Without --execute, prints the check/register request plan only.
|
|
42
|
+
With --execute, checks current availability and USD price first, requires exact
|
|
43
|
+
domain and price confirmations, then starts registration.
|
|
44
|
+
Optional --registrant-contact-json sends a one-use inline registrant contact;
|
|
45
|
+
command output redacts contact body fields.
|
|
46
|
+
|
|
47
|
+
pages deploy --site <dir> --project-name <name> [--execute]
|
|
48
|
+
Without --execute, prints the exact Wrangler deploy plan only.
|
|
49
|
+
With --execute, runs Wrangler through npm exec/npx without printing the token.
|
|
50
|
+
|
|
51
|
+
pages verify --url <https-url> --project-name <name> [--domain <host>] \\
|
|
52
|
+
[--account-id <id>] [--cloudflare-live off|auto|required]
|
|
53
|
+
Non-mutating readiness check. Fetches the public HTTPS URL without
|
|
54
|
+
Authorization and verifies 2xx HTML-ish reachability.
|
|
55
|
+
--cloudflare-live off skips credentials; auto uses CLOUDFLARE_API_TOKEN when
|
|
56
|
+
present; required reports not-ready when absent. With credentials plus account
|
|
57
|
+
id, it fetches the declared Pages project and matches project/domain/URL.
|
|
58
|
+
Claim boundary: public Pages reachability plus optional Cloudflare
|
|
59
|
+
token/account/Pages project observation only; no hosted relay/gateway/KMS/storage/SIEM/
|
|
60
|
+
backup/operator/token readiness.
|
|
61
|
+
|
|
62
|
+
workers deploy-probe --script <worker.mjs> [--name enigma-hosted-probe] \\
|
|
63
|
+
[--compatibility-date YYYY-MM-DD] [--execute]
|
|
64
|
+
Without --execute, prints the exact Wrangler Worker deploy plan only.
|
|
65
|
+
With --execute, deploys the hosted /livez and fail-closed /readyz probe Worker.
|
|
66
|
+
workers deploy-edge --script <worker.mjs> --name <enigma-relay|enigma-gateway> \\
|
|
67
|
+
--domain <relay.enigmamemory.com|gateway.enigmamemory.com> [--compatibility-date YYYY-MM-DD] [--execute]
|
|
68
|
+
Without --execute, prints the exact Wrangler Worker Custom Domain deploy plan only.
|
|
69
|
+
With --execute, deploys the relay/gateway edge Worker to the named Custom Domain.
|
|
70
|
+
workers inspect-probe [--name enigma-hosted-probe] [--account-id <id>] [--out <file>]
|
|
71
|
+
Non-mutating Cloudflare API check for Worker service visibility and token/account
|
|
72
|
+
permission before a deploy attempt. Output redacts account ids and local paths;
|
|
73
|
+
--out writes the same public-safe JSON for release evidence.
|
|
74
|
+
|
|
75
|
+
workers verify-probe --url <https-url> [--expect fail-closed|ready]
|
|
76
|
+
Non-mutating check for a hosted probe Worker. Verifies /livez and /readyz,
|
|
77
|
+
defaulting to fail-closed /readyz until operator evidence refs are configured.
|
|
78
|
+
|
|
79
|
+
Global options:
|
|
80
|
+
--account-id <id> Overrides CLOUDFLARE_ACCOUNT_ID for account-scoped API calls.
|
|
81
|
+
--cloudflare-env-file <path>
|
|
82
|
+
Loads CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, and
|
|
83
|
+
CLOUDFLARE_PROJECT_NAME from a local .env-style file without
|
|
84
|
+
printing values. Equivalent env var: CLOUDFLARE_ENV_FILE.
|
|
85
|
+
--help Show this help.
|
|
86
|
+
|
|
87
|
+
Environment:
|
|
88
|
+
CLOUDFLARE_ACCOUNT_ID Account default for registrar commands and Pages verify observation.
|
|
89
|
+
CLOUDFLARE_API_TOKEN Bearer token for Cloudflare API calls. The token is never printed.
|
|
90
|
+
CLOUDFLARE_ENV_FILE Optional local .env-style secret file path for Cloudflare keys.
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
class UsageError extends Error {
|
|
94
|
+
constructor(message) {
|
|
95
|
+
super(message);
|
|
96
|
+
this.name = 'UsageError';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeDomainName(domain) {
|
|
101
|
+
return String(domain ?? '').trim().toLowerCase().replace(/\.$/, '');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeUsdText(cents) {
|
|
105
|
+
return `${Math.trunc(cents / 100)}.${String(cents % 100).padStart(2, '0')}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function requireNonEmptyString(name, value) {
|
|
109
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
110
|
+
throw new UsageError(`${name} is required`);
|
|
111
|
+
}
|
|
112
|
+
return value.trim();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function requireObject(name, value) {
|
|
116
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
117
|
+
throw new UsageError(`${name} must be an object`);
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function normalizeRegistrantContact(value) {
|
|
123
|
+
const root = requireObject('registrant contact', value);
|
|
124
|
+
const raw = root.contacts?.registrant ?? root.registrant ?? root;
|
|
125
|
+
const contact = requireObject('registrant contact', raw);
|
|
126
|
+
const postalInfo = requireObject('registrant contact postal_info', contact.postal_info);
|
|
127
|
+
const address = requireObject('registrant contact postal_info.address', postalInfo.address);
|
|
128
|
+
const email = requireNonEmptyString('registrant contact email', contact.email);
|
|
129
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) throw new UsageError('registrant contact email must be a valid email address');
|
|
130
|
+
const phone = requireNonEmptyString('registrant contact phone', contact.phone);
|
|
131
|
+
if (!/^\+[0-9][0-9 .-]{5,30}[0-9]$/.test(phone)) {
|
|
132
|
+
throw new UsageError('registrant contact phone must use international format, for example +1.5555555555');
|
|
133
|
+
}
|
|
134
|
+
const name = requireNonEmptyString('registrant contact postal_info.name', postalInfo.name);
|
|
135
|
+
const street = requireNonEmptyString('registrant contact address.street', address.street);
|
|
136
|
+
const city = requireNonEmptyString('registrant contact address.city', address.city);
|
|
137
|
+
const postalCode = requireNonEmptyString('registrant contact address.postal_code', address.postal_code);
|
|
138
|
+
const countryCode = requireNonEmptyString('registrant contact address.country_code', address.country_code).toUpperCase();
|
|
139
|
+
if (!/^[A-Z]{2}$/.test(countryCode)) throw new UsageError('registrant contact address.country_code must be an ISO 3166-1 alpha-2 code');
|
|
140
|
+
const state = typeof address.state === 'string' ? address.state.trim() : '';
|
|
141
|
+
if (countryCode === 'US' && state.length === 0) throw new UsageError('registrant contact address.state is required for US contacts');
|
|
142
|
+
const organization = typeof postalInfo.organization === 'string' ? postalInfo.organization.trim() : '';
|
|
143
|
+
return {
|
|
144
|
+
email,
|
|
145
|
+
phone,
|
|
146
|
+
postal_info: {
|
|
147
|
+
name,
|
|
148
|
+
...(organization.length === 0 ? {} : { organization }),
|
|
149
|
+
address: {
|
|
150
|
+
street,
|
|
151
|
+
city,
|
|
152
|
+
...(state.length === 0 ? {} : { state }),
|
|
153
|
+
postal_code: postalCode,
|
|
154
|
+
country_code: countryCode,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function loadRegistrantContactJson(path, readFileImpl = readFileSync) {
|
|
161
|
+
const filePath = requireNonEmptyString('--registrant-contact-json', path);
|
|
162
|
+
try {
|
|
163
|
+
return normalizeRegistrantContact(JSON.parse(readFileImpl(filePath, 'utf8')));
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (error instanceof UsageError) throw error;
|
|
166
|
+
throw new UsageError(`failed to read --registrant-contact-json: ${error?.message ?? String(error)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function summarizeRegistrantContact(contact) {
|
|
171
|
+
return {
|
|
172
|
+
provided: true,
|
|
173
|
+
emailProvided: typeof contact.email === 'string' && contact.email.length > 0,
|
|
174
|
+
phoneProvided: typeof contact.phone === 'string' && contact.phone.length > 0,
|
|
175
|
+
postalInfo: {
|
|
176
|
+
nameProvided: typeof contact.postal_info?.name === 'string' && contact.postal_info.name.length > 0,
|
|
177
|
+
organizationProvided: typeof contact.postal_info?.organization === 'string' && contact.postal_info.organization.length > 0,
|
|
178
|
+
countryCode: contact.postal_info?.address?.country_code ?? null,
|
|
179
|
+
stateProvided: typeof contact.postal_info?.address?.state === 'string' && contact.postal_info.address.state.length > 0,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const REGISTRAR_CONTACT_OUTPUT_FIELD = /^(?:email|phone|fax|name|street|street1|street2|address_line_1|address_line_2|city|postal_code|postalCode|postcode|zip|zip_code)$/iu;
|
|
185
|
+
const SECRET_OUTPUT_VALUE = /(?: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;
|
|
186
|
+
const EMAIL_OUTPUT_VALUE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/u;
|
|
187
|
+
const PHONE_OUTPUT_VALUE = /^\+?[0-9][0-9 .()-]{5,30}[0-9]$/u;
|
|
188
|
+
const PROBE_FORBIDDEN_FIELD = /(?:authorization|bearer|cookie|credential|password|passwd|token|secret|api[_-]?key|private[_-]?key|client[_-]?secret|raw[_-]?memory|plaintext|plain[_-]?text|prompt|completion|transcript|provider[_-]?response|email|phone|address|street)/iu;
|
|
189
|
+
const PROBE_RAW_VALUE = /(?:raw memory|private prompt|full transcript|decrypted capsule|provider response|customer note|launch-code phrase)/iu;
|
|
190
|
+
|
|
191
|
+
function assertProbePayloadSafe(value, path = 'payload') {
|
|
192
|
+
if (Array.isArray(value)) {
|
|
193
|
+
value.forEach((item, index) => assertProbePayloadSafe(item, `${path}[${index}]`));
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
if (value !== null && typeof value === 'object') {
|
|
197
|
+
for (const [key, child] of Object.entries(value)) {
|
|
198
|
+
if (PROBE_FORBIDDEN_FIELD.test(key)) throw new UsageError(`hosted probe response field ${path}.${key} is not allowed`);
|
|
199
|
+
assertProbePayloadSafe(child, `${path}.${key}`);
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
if (typeof value === 'string' && (SECRET_OUTPUT_VALUE.test(value) || EMAIL_OUTPUT_VALUE.test(value) || PHONE_OUTPUT_VALUE.test(value) || PROBE_RAW_VALUE.test(value))) {
|
|
204
|
+
throw new UsageError(`hosted probe response value ${path} is not public-safe`);
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function redactScalarOutput(value) {
|
|
210
|
+
if (typeof value !== 'string') return '[redacted]';
|
|
211
|
+
if (value.length === 0) return value;
|
|
212
|
+
return '[redacted]';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function redactCloudflareOutputPayload(value) {
|
|
216
|
+
if (Array.isArray(value)) return value.map((item) => redactCloudflareOutputPayload(item));
|
|
217
|
+
if (value === null || typeof value !== 'object') {
|
|
218
|
+
if (typeof value === 'string' && (SECRET_OUTPUT_VALUE.test(value) || EMAIL_OUTPUT_VALUE.test(value) || PHONE_OUTPUT_VALUE.test(value))) {
|
|
219
|
+
return '[redacted]';
|
|
220
|
+
}
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
const out = {};
|
|
224
|
+
for (const [key, child] of Object.entries(value)) {
|
|
225
|
+
out[key] = REGISTRAR_CONTACT_OUTPUT_FIELD.test(key)
|
|
226
|
+
? redactScalarOutput(child)
|
|
227
|
+
: redactCloudflareOutputPayload(child);
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function redactCloudflareRequestPlan(plan) {
|
|
233
|
+
if (plan.body?.contacts?.registrant === undefined) return plan;
|
|
234
|
+
return {
|
|
235
|
+
...plan,
|
|
236
|
+
body: {
|
|
237
|
+
...plan.body,
|
|
238
|
+
contacts: {
|
|
239
|
+
registrant: summarizeRegistrantContact(plan.body.contacts.registrant),
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function redactOperationalPayload(value) {
|
|
246
|
+
if (Array.isArray(value)) return value.map((item) => redactOperationalPayload(item));
|
|
247
|
+
if (value === null || typeof value !== 'object') {
|
|
248
|
+
if (typeof value === 'string') {
|
|
249
|
+
if (SECRET_OUTPUT_VALUE.test(value) || EMAIL_OUTPUT_VALUE.test(value) || PHONE_OUTPUT_VALUE.test(value)) return '[redacted]';
|
|
250
|
+
return redactOperationalText(value);
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
const out = {};
|
|
255
|
+
for (const [key, child] of Object.entries(value)) {
|
|
256
|
+
out[key] = REGISTRAR_CONTACT_OUTPUT_FIELD.test(key)
|
|
257
|
+
? redactScalarOutput(child)
|
|
258
|
+
: redactOperationalPayload(child);
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function redactPlanOutput(plan) {
|
|
264
|
+
return redactOperationalPayload(plan);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function parsePositiveInteger(value, name) {
|
|
268
|
+
const text = requireNonEmptyString(name, value);
|
|
269
|
+
if (!/^\d+$/.test(text)) throw new UsageError(`${name} must be a positive integer`);
|
|
270
|
+
const parsed = Number(text);
|
|
271
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) throw new UsageError(`${name} must be a positive integer`);
|
|
272
|
+
return parsed;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function accountIdFrom(command, env) {
|
|
276
|
+
return command.accountId ?? env.CLOUDFLARE_ACCOUNT_ID ?? null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function requireAccountId(command, env) {
|
|
280
|
+
const accountId = accountIdFrom(command, env);
|
|
281
|
+
if (typeof accountId !== 'string' || accountId.trim().length === 0) {
|
|
282
|
+
throw new UsageError('Cloudflare account id is required via --account-id or CLOUDFLARE_ACCOUNT_ID');
|
|
283
|
+
}
|
|
284
|
+
return accountId.trim();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function requireApiToken(env) {
|
|
288
|
+
const token = env.CLOUDFLARE_API_TOKEN;
|
|
289
|
+
if (typeof token !== 'string' || token.length === 0) {
|
|
290
|
+
throw new UsageError('CLOUDFLARE_API_TOKEN is required for this Cloudflare API call');
|
|
291
|
+
}
|
|
292
|
+
return token;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function isValidDomainName(domain) {
|
|
296
|
+
const normalized = normalizeDomainName(domain);
|
|
297
|
+
if (normalized.length < 4 || normalized.length > 253) return false;
|
|
298
|
+
if (normalized.includes('..')) return false;
|
|
299
|
+
const labels = normalized.split('.');
|
|
300
|
+
if (labels.length < 2) return false;
|
|
301
|
+
for (const label of labels) {
|
|
302
|
+
if (label.length < 1 || label.length > 63) return false;
|
|
303
|
+
if (!/^[a-z0-9-]+$/.test(label)) return false;
|
|
304
|
+
if (label.startsWith('-') || label.endsWith('-')) return false;
|
|
305
|
+
}
|
|
306
|
+
const tld = labels.at(-1);
|
|
307
|
+
return /^[a-z][a-z0-9-]{1,62}$/.test(tld);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function parseUsdAmount(value, name = 'amount') {
|
|
311
|
+
const text = requireNonEmptyString(name, value);
|
|
312
|
+
if (!/^(?:0|[1-9]\d*)(?:\.\d{1,2})?$/.test(text)) {
|
|
313
|
+
throw new UsageError(`${name} must be a USD amount with at most two decimal places`);
|
|
314
|
+
}
|
|
315
|
+
const [whole, fraction = ''] = text.split('.');
|
|
316
|
+
const cents = (Number(whole) * 100) + Number(fraction.padEnd(2, '0'));
|
|
317
|
+
if (!Number.isSafeInteger(cents) || cents < 0) throw new UsageError(`${name} is outside the supported USD range`);
|
|
318
|
+
return { cents, usd: cents / 100, text: normalizeUsdText(cents) };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function assertRegistrationPriceGuard({
|
|
322
|
+
domain,
|
|
323
|
+
maxPriceUsd,
|
|
324
|
+
confirmationDomain,
|
|
325
|
+
confirmationCostUsd,
|
|
326
|
+
iUnderstandThisChargesMyPaymentMethod,
|
|
327
|
+
availability,
|
|
328
|
+
}) {
|
|
329
|
+
const normalizedDomain = normalizeDomainName(domain);
|
|
330
|
+
if (!isValidDomainName(normalizedDomain)) throw new UsageError('domain must be a valid DNS domain name');
|
|
331
|
+
if (normalizeDomainName(confirmationDomain) !== normalizedDomain) {
|
|
332
|
+
throw new UsageError('--confirm-domain must exactly match --domain after DNS lowercase normalization');
|
|
333
|
+
}
|
|
334
|
+
if (iUnderstandThisChargesMyPaymentMethod !== true) {
|
|
335
|
+
throw new UsageError(`${REGISTRATION_CHARGE_FLAG} is required for domain registration`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const maxPrice = parseUsdAmount(maxPriceUsd, '--max-price-usd');
|
|
339
|
+
const confirmedPrice = parseUsdAmount(confirmationCostUsd, '--confirm-registration-cost');
|
|
340
|
+
|
|
341
|
+
if (!availability || typeof availability !== 'object') throw new UsageError('current availability result is required before registration');
|
|
342
|
+
const returnedDomain = normalizeDomainName(availability.name ?? availability.domain_name);
|
|
343
|
+
if (returnedDomain !== normalizedDomain) throw new UsageError('availability result domain does not match requested domain');
|
|
344
|
+
if (availability.registrable !== true) {
|
|
345
|
+
const reason = availability.reason ? `: ${availability.reason}` : '';
|
|
346
|
+
throw new UsageError(`domain is not registrable${reason}`);
|
|
347
|
+
}
|
|
348
|
+
const pricing = availability.pricing;
|
|
349
|
+
if (!pricing || pricing.currency !== 'USD') throw new UsageError('registration price must be returned in USD');
|
|
350
|
+
const livePrice = parseUsdAmount(pricing.registration_cost, 'current registration_cost');
|
|
351
|
+
if (livePrice.cents > maxPrice.cents) {
|
|
352
|
+
throw new UsageError(`current registration cost ${livePrice.text} exceeds --max-price-usd ${maxPrice.text}`);
|
|
353
|
+
}
|
|
354
|
+
if (confirmedPrice.cents !== livePrice.cents) {
|
|
355
|
+
throw new UsageError(`--confirm-registration-cost must exactly match current registration cost ${livePrice.text}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
domain: normalizedDomain,
|
|
360
|
+
registrable: true,
|
|
361
|
+
currency: 'USD',
|
|
362
|
+
registrationCostUsd: livePrice.text,
|
|
363
|
+
maxPriceUsd: maxPrice.text,
|
|
364
|
+
confirmedRegistrationCostUsd: confirmedPrice.text,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function encodePathSegment(value) {
|
|
369
|
+
return encodeURIComponent(String(value));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function makeCloudflareUrl(path, query = null) {
|
|
373
|
+
const url = new URL(path, `${CLOUDFLARE_API_BASE}/`);
|
|
374
|
+
if (query) {
|
|
375
|
+
for (const [key, value] of Object.entries(query)) {
|
|
376
|
+
if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return url.toString();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function buildCloudflareRequestPlan({ operation, accountId, query, limit, domain, projectName, registrantContact }) {
|
|
383
|
+
switch (operation) {
|
|
384
|
+
case 'token.verify': {
|
|
385
|
+
const scopedAccountId = accountId ?? null;
|
|
386
|
+
return Object.freeze({
|
|
387
|
+
operation,
|
|
388
|
+
method: 'GET',
|
|
389
|
+
url: scopedAccountId
|
|
390
|
+
? makeCloudflareUrl(`accounts/${encodePathSegment(scopedAccountId)}/tokens/verify`)
|
|
391
|
+
: makeCloudflareUrl('user/tokens/verify'),
|
|
392
|
+
requiresToken: true,
|
|
393
|
+
tokenPrinted: false,
|
|
394
|
+
tokenScope: scopedAccountId ? 'account' : 'user',
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
case 'accounts.list':
|
|
398
|
+
return Object.freeze({
|
|
399
|
+
operation,
|
|
400
|
+
method: 'GET',
|
|
401
|
+
url: makeCloudflareUrl('accounts'),
|
|
402
|
+
requiresToken: true,
|
|
403
|
+
tokenPrinted: false,
|
|
404
|
+
});
|
|
405
|
+
case 'registrar.search': {
|
|
406
|
+
const scopedAccountId = requireNonEmptyString('accountId', accountId);
|
|
407
|
+
const trimmedQuery = requireNonEmptyString('--query', query);
|
|
408
|
+
const parsedLimit = limit === undefined || limit === null ? undefined : parsePositiveInteger(String(limit), '--limit');
|
|
409
|
+
return Object.freeze({
|
|
410
|
+
operation,
|
|
411
|
+
method: 'GET',
|
|
412
|
+
url: makeCloudflareUrl(`accounts/${encodePathSegment(scopedAccountId)}/registrar/domain-search`, {
|
|
413
|
+
q: trimmedQuery,
|
|
414
|
+
...(parsedLimit === undefined ? {} : { limit: parsedLimit }),
|
|
415
|
+
}),
|
|
416
|
+
requiresToken: true,
|
|
417
|
+
tokenPrinted: false,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
case 'registrar.check': {
|
|
421
|
+
const scopedAccountId = requireNonEmptyString('accountId', accountId);
|
|
422
|
+
const normalizedDomain = normalizeDomainName(domain);
|
|
423
|
+
if (!isValidDomainName(normalizedDomain)) throw new UsageError('--domain must be a valid DNS domain name');
|
|
424
|
+
return Object.freeze({
|
|
425
|
+
operation,
|
|
426
|
+
method: 'POST',
|
|
427
|
+
url: makeCloudflareUrl(`accounts/${encodePathSegment(scopedAccountId)}/registrar/domain-check`),
|
|
428
|
+
body: { domains: [normalizedDomain] },
|
|
429
|
+
requiresToken: true,
|
|
430
|
+
tokenPrinted: false,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
case 'registrar.register': {
|
|
434
|
+
const scopedAccountId = requireNonEmptyString('accountId', accountId);
|
|
435
|
+
const normalizedDomain = normalizeDomainName(domain);
|
|
436
|
+
if (!isValidDomainName(normalizedDomain)) throw new UsageError('--domain must be a valid DNS domain name');
|
|
437
|
+
const normalizedContact = registrantContact === undefined || registrantContact === null
|
|
438
|
+
? null
|
|
439
|
+
: normalizeRegistrantContact(registrantContact);
|
|
440
|
+
return Object.freeze({
|
|
441
|
+
operation,
|
|
442
|
+
method: 'POST',
|
|
443
|
+
url: makeCloudflareUrl(`accounts/${encodePathSegment(scopedAccountId)}/registrar/registrations`),
|
|
444
|
+
body: {
|
|
445
|
+
domain_name: normalizedDomain,
|
|
446
|
+
...(normalizedContact === null ? {} : { contacts: { registrant: normalizedContact } }),
|
|
447
|
+
},
|
|
448
|
+
requiresToken: true,
|
|
449
|
+
tokenPrinted: false,
|
|
450
|
+
billable: true,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
case 'registrar.registration-status': {
|
|
454
|
+
const scopedAccountId = requireNonEmptyString('accountId', accountId);
|
|
455
|
+
const normalizedDomain = normalizeDomainName(domain);
|
|
456
|
+
if (!isValidDomainName(normalizedDomain)) throw new UsageError('--domain must be a valid DNS domain name');
|
|
457
|
+
return Object.freeze({
|
|
458
|
+
operation,
|
|
459
|
+
method: 'GET',
|
|
460
|
+
url: makeCloudflareUrl(`accounts/${encodePathSegment(scopedAccountId)}/registrar/registrations/${encodePathSegment(normalizedDomain)}/registration-status`),
|
|
461
|
+
requiresToken: true,
|
|
462
|
+
tokenPrinted: false,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
case 'workers.service': {
|
|
466
|
+
const scopedAccountId = requireNonEmptyString('accountId', accountId);
|
|
467
|
+
const scopedName = requireNonEmptyString('--name', projectName);
|
|
468
|
+
return Object.freeze({
|
|
469
|
+
operation,
|
|
470
|
+
method: 'GET',
|
|
471
|
+
url: makeCloudflareUrl(`accounts/${encodePathSegment(scopedAccountId)}/workers/services/${encodePathSegment(scopedName)}`),
|
|
472
|
+
requiresToken: true,
|
|
473
|
+
tokenPrinted: false,
|
|
474
|
+
tokenScope: 'account',
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
case 'pages.project': {
|
|
478
|
+
const scopedAccountId = requireNonEmptyString('accountId', accountId);
|
|
479
|
+
const scopedProjectName = requireNonEmptyString('--project-name', projectName);
|
|
480
|
+
return Object.freeze({
|
|
481
|
+
operation,
|
|
482
|
+
method: 'GET',
|
|
483
|
+
url: makeCloudflareUrl(`accounts/${encodePathSegment(scopedAccountId)}/pages/projects/${encodePathSegment(scopedProjectName)}`),
|
|
484
|
+
requiresToken: true,
|
|
485
|
+
tokenPrinted: false,
|
|
486
|
+
tokenScope: 'account',
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
default:
|
|
490
|
+
throw new UsageError(`unsupported Cloudflare operation: ${operation}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function buildNpmExecInvocation() {
|
|
495
|
+
const envNpmExecPath = process.env.npm_execpath || null;
|
|
496
|
+
const nodeDirNpmExecPath = join(dirname(process.execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js');
|
|
497
|
+
const npmExecPath = [envNpmExecPath, nodeDirNpmExecPath].find((candidate) => candidate && existsSync(candidate));
|
|
498
|
+
if (npmExecPath) {
|
|
499
|
+
return {
|
|
500
|
+
command: process.execPath,
|
|
501
|
+
argsPrefix: [npmExecPath, 'exec', '--'],
|
|
502
|
+
usesShell: false,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
return {
|
|
506
|
+
command: process.platform === 'win32' ? 'npx.cmd' : 'npx',
|
|
507
|
+
argsPrefix: [],
|
|
508
|
+
usesShell: process.platform === 'win32',
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export function buildWranglerPagesDeployPlan({ site, projectName, execute = false }) {
|
|
513
|
+
const safeSite = requireNonEmptyString('--site', site);
|
|
514
|
+
const safeProjectName = requireNonEmptyString('--project-name', projectName);
|
|
515
|
+
const npmExec = buildNpmExecInvocation();
|
|
516
|
+
return Object.freeze({
|
|
517
|
+
operation: 'pages.deploy',
|
|
518
|
+
dryRun: execute !== true,
|
|
519
|
+
execute: execute === true,
|
|
520
|
+
command: npmExec.command,
|
|
521
|
+
args: [...npmExec.argsPrefix, 'wrangler', 'pages', 'deploy', safeSite, '--project-name', safeProjectName],
|
|
522
|
+
usesShell: npmExec.usesShell,
|
|
523
|
+
destructive: true,
|
|
524
|
+
tokenPrinted: false,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function buildWranglerWorkerDeployPlan({ script, name = 'enigma-hosted-probe', compatibilityDate = '2026-06-24', execute = false, domain = null, kind = null, operation = kind ?? 'workers.deploy-probe' }) {
|
|
529
|
+
const safeScript = requireNonEmptyString('--script', script);
|
|
530
|
+
const safeName = requireNonEmptyString('--name', name);
|
|
531
|
+
const safeCompatibilityDate = requireNonEmptyString('--compatibility-date', compatibilityDate);
|
|
532
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(safeCompatibilityDate)) throw new UsageError('--compatibility-date must use YYYY-MM-DD');
|
|
533
|
+
const normalizedDomain = domain === null || domain === undefined ? null : normalizeDomainName(domain);
|
|
534
|
+
if (normalizedDomain !== null && !isValidDomainName(normalizedDomain)) throw new UsageError('--domain must be a valid DNS domain name');
|
|
535
|
+
const npmExec = buildNpmExecInvocation();
|
|
536
|
+
const args = [...npmExec.argsPrefix, 'wrangler', 'deploy', safeScript, '--name', safeName, '--compatibility-date', safeCompatibilityDate];
|
|
537
|
+
if (normalizedDomain !== null) args.push('--domain', normalizedDomain);
|
|
538
|
+
return Object.freeze({
|
|
539
|
+
operation,
|
|
540
|
+
dryRun: execute !== true,
|
|
541
|
+
execute: execute === true,
|
|
542
|
+
command: npmExec.command,
|
|
543
|
+
args,
|
|
544
|
+
usesShell: npmExec.usesShell,
|
|
545
|
+
destructive: true,
|
|
546
|
+
tokenPrinted: false,
|
|
547
|
+
claimBoundary: normalizedDomain === null
|
|
548
|
+
? 'Cloudflare Worker deploys an edge probe only; it is not hosted relay/gateway readiness.'
|
|
549
|
+
: 'Cloudflare Worker Custom Domain deploy proves edge reachability only; it is not hosted relay/gateway production readiness.',
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function readOption(tokens, index, name) {
|
|
554
|
+
if (index + 1 >= tokens.length || tokens[index + 1].startsWith('--')) throw new UsageError(`${name} requires a value`);
|
|
555
|
+
return [tokens[index + 1], index + 2];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function parseOptions(tokens, allowedFlags) {
|
|
559
|
+
const options = {};
|
|
560
|
+
const positionals = [];
|
|
561
|
+
for (let index = 0; index < tokens.length;) {
|
|
562
|
+
const token = tokens[index];
|
|
563
|
+
if (!token.startsWith('--')) {
|
|
564
|
+
positionals.push(token);
|
|
565
|
+
index += 1;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const equalIndex = token.indexOf('=');
|
|
569
|
+
const name = equalIndex === -1 ? token : token.slice(0, equalIndex);
|
|
570
|
+
if (!allowedFlags.has(name)) throw new UsageError(`unknown option ${name}`);
|
|
571
|
+
if (name === '--execute' || name === REGISTRATION_CHARGE_FLAG || name === '--help') {
|
|
572
|
+
if (equalIndex !== -1) throw new UsageError(`${name} does not take a value`);
|
|
573
|
+
options[name] = true;
|
|
574
|
+
index += 1;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (equalIndex !== -1) {
|
|
578
|
+
const value = token.slice(equalIndex + 1);
|
|
579
|
+
if (value.length === 0) throw new UsageError(`${name} requires a value`);
|
|
580
|
+
options[name] = value;
|
|
581
|
+
index += 1;
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
const [value, next] = readOption(tokens, index, name);
|
|
585
|
+
options[name] = value;
|
|
586
|
+
index = next;
|
|
587
|
+
}
|
|
588
|
+
return { options, positionals };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const GLOBAL_FLAGS = new Set(['--account-id', '--help']);
|
|
592
|
+
const REGISTRAR_FLAGS = new Set(['--account-id', '--query', '--limit', '--domain', '--max-price-usd', '--confirm-domain', '--confirm-registration-cost', '--registrant-contact-json', REGISTRATION_CHARGE_FLAG, '--execute', '--help']);
|
|
593
|
+
const PAGES_DEPLOY_FLAGS = new Set(['--site', '--project-name', '--execute', '--help']);
|
|
594
|
+
const PAGES_VERIFY_FLAGS = new Set(['--url', '--project-name', '--domain', '--account-id', '--cloudflare-live', '--help']);
|
|
595
|
+
const WORKERS_DEPLOY_FLAGS = new Set(['--script', '--name', '--domain', '--compatibility-date', '--execute', '--help']);
|
|
596
|
+
const WORKERS_VERIFY_FLAGS = new Set(['--url', '--expect', '--help']);
|
|
597
|
+
const WORKERS_INSPECT_FLAGS = new Set(['--name', '--account-id', '--out', '--help']);
|
|
598
|
+
function stripLeadingGlobalOptions(tokens, env) {
|
|
599
|
+
let accountId = env.CLOUDFLARE_ACCOUNT_ID ?? null;
|
|
600
|
+
let index = 0;
|
|
601
|
+
while (index < tokens.length && tokens[index].startsWith('--')) {
|
|
602
|
+
const token = tokens[index];
|
|
603
|
+
if (token === '--help') return { tokens: ['--help'], env };
|
|
604
|
+
if (token === '--account-id') {
|
|
605
|
+
const [value, next] = readOption(tokens, index, '--account-id');
|
|
606
|
+
accountId = value;
|
|
607
|
+
index = next;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (token.startsWith('--account-id=')) {
|
|
611
|
+
accountId = token.slice('--account-id='.length);
|
|
612
|
+
if (accountId.length === 0) throw new UsageError('--account-id requires a value');
|
|
613
|
+
index += 1;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
throw new UsageError(`unknown global option ${token}`);
|
|
617
|
+
}
|
|
618
|
+
return {
|
|
619
|
+
tokens: tokens.slice(index),
|
|
620
|
+
env: accountId === (env.CLOUDFLARE_ACCOUNT_ID ?? null) ? env : { ...env, CLOUDFLARE_ACCOUNT_ID: accountId },
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
function parseCloudflareLiveMode(value) {
|
|
626
|
+
const mode = value === undefined || value === null ? 'auto' : requireNonEmptyString('--cloudflare-live', value).toLowerCase();
|
|
627
|
+
if (mode !== 'off' && mode !== 'auto' && mode !== 'required') {
|
|
628
|
+
throw new UsageError('--cloudflare-live must be one of: off, auto, required');
|
|
629
|
+
}
|
|
630
|
+
return mode;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function rawUrlAuthorityHasUserinfo(rawUrl) {
|
|
634
|
+
const scheme = /^[A-Za-z][A-Za-z\d+.-]*:\/\//.exec(rawUrl);
|
|
635
|
+
if (scheme === null) return false;
|
|
636
|
+
const authorityStart = scheme[0].length;
|
|
637
|
+
let authorityEnd = rawUrl.length;
|
|
638
|
+
const pathStart = rawUrl.indexOf('/', authorityStart);
|
|
639
|
+
if (pathStart !== -1) authorityEnd = pathStart;
|
|
640
|
+
const queryStart = rawUrl.indexOf('?', authorityStart);
|
|
641
|
+
if (queryStart !== -1 && queryStart < authorityEnd) authorityEnd = queryStart;
|
|
642
|
+
const hashStart = rawUrl.indexOf('#', authorityStart);
|
|
643
|
+
if (hashStart !== -1 && hashStart < authorityEnd) authorityEnd = hashStart;
|
|
644
|
+
return rawUrl.lastIndexOf('@', authorityEnd - 1) >= authorityStart;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function parsedUrlHasUserinfo(url) {
|
|
648
|
+
return url.username !== '' || url.password !== '';
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function stripUrlUserinfoFromText(value) {
|
|
652
|
+
let text = String(value);
|
|
653
|
+
for (let pass = 0; pass < 8; pass += 1) {
|
|
654
|
+
const redacted = text.replace(/\b([A-Za-z][A-Za-z\d+.-]*:\/\/)([^/?#\s]*@)([^/?#\s]+)/g, '$1$3');
|
|
655
|
+
if (redacted === text) return text;
|
|
656
|
+
text = redacted;
|
|
657
|
+
}
|
|
658
|
+
return text;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function redactOperationalText(value) {
|
|
662
|
+
let text = stripUrlUserinfoFromText(value ?? '');
|
|
663
|
+
text = text.replace(/\/accounts\/[0-9a-f]{32}\b/gi, '/accounts/<account-id>');
|
|
664
|
+
text = text.replace(/\b[0-9a-f]{32}\b/gi, '<hex32>');
|
|
665
|
+
text = text.replace(/[A-Z]:\\Program Files\\nodejs\\[^\r\n\"]+/gi, '<node-runtime-path>');
|
|
666
|
+
text = text.replace(/[A-Z]:\\[^\r\n\"']+/gi, '<local-path>');
|
|
667
|
+
text = text.replace(/\\\\[^\s\"']+/g, '<unc-local-path>');
|
|
668
|
+
text = text.replace(/\/(?:Users|home)\/[^\s\"\']+/gi, '<user-local-path>');
|
|
669
|
+
text = text.replace(/\/(?:tmp|private\/tmp|var\/folders|mnt|Volumes)\/[^\s\"\']+/gi, '<workspace-local-path>');
|
|
670
|
+
text = text.replace(/\bhttps:\/\/([a-z0-9-]+)\.[a-z0-9-]+\.workers\.dev\b/gi, 'https://$1.<workers-subdomain>.workers.dev');
|
|
671
|
+
text = text.replace(/(?:\.\.[\\/])?github-upload[\\/]enigma-memory-site[\\/]_public_site/gi, '<public-site>');
|
|
672
|
+
text = text.replace(/\"[^\"]*wrangler-[^\"]*\\.log\"/gi, '\"<wrangler-log>\"');
|
|
673
|
+
return text;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function urlTextHasUserinfo(value) {
|
|
677
|
+
return stripUrlUserinfoFromText(value) !== String(value);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function parsePublicHttpsUrl(value) {
|
|
681
|
+
const rawUrl = requireNonEmptyString('--url', value);
|
|
682
|
+
if (rawUrlAuthorityHasUserinfo(rawUrl) || urlTextHasUserinfo(rawUrl)) throw new UsageError('--url must not include username, password, or userinfo');
|
|
683
|
+
let url;
|
|
684
|
+
try {
|
|
685
|
+
url = new URL(rawUrl);
|
|
686
|
+
} catch {
|
|
687
|
+
throw new UsageError('--url must be a valid HTTPS URL');
|
|
688
|
+
}
|
|
689
|
+
if (parsedUrlHasUserinfo(url)) throw new UsageError('--url must not include username, password, or userinfo');
|
|
690
|
+
if (url.protocol !== 'https:') throw new UsageError('--url must use https://');
|
|
691
|
+
const host = normalizeDomainName(url.hostname);
|
|
692
|
+
if (!isValidDomainName(host)) throw new UsageError('--url must include a valid DNS host');
|
|
693
|
+
url.hash = '';
|
|
694
|
+
return url.toString();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function parseOptionalHost(value) {
|
|
698
|
+
if (value === undefined || value === null) return null;
|
|
699
|
+
const host = normalizeDomainName(value);
|
|
700
|
+
if (!isValidDomainName(host)) throw new UsageError('--domain must be a valid DNS host');
|
|
701
|
+
return host;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export function parseCloudflareOpsCommand(argv, env = {}) {
|
|
705
|
+
const rawTokens = Array.from(argv ?? []);
|
|
706
|
+
if (rawTokens.length === 0 || rawTokens.includes('--help')) {
|
|
707
|
+
return { kind: 'help', help: HELP_TEXT };
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const leading = stripLeadingGlobalOptions(rawTokens, env);
|
|
711
|
+
const tokens = leading.tokens;
|
|
712
|
+
const commandEnv = leading.env;
|
|
713
|
+
if (tokens.length === 0) return { kind: 'help', help: HELP_TEXT };
|
|
714
|
+
|
|
715
|
+
const [group, subcommand, ...rest] = tokens;
|
|
716
|
+
if (group === 'token' && subcommand === 'verify') {
|
|
717
|
+
const { options, positionals } = parseOptions(rest, GLOBAL_FLAGS);
|
|
718
|
+
if (positionals.length > 0) throw new UsageError(`unexpected argument ${positionals[0]}`);
|
|
719
|
+
return { kind: 'token.verify', accountId: options['--account-id'] ?? commandEnv.CLOUDFLARE_ACCOUNT_ID ?? null };
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
if (group === 'accounts' && subcommand === 'list') {
|
|
723
|
+
const { options, positionals } = parseOptions(rest, GLOBAL_FLAGS);
|
|
724
|
+
if (positionals.length > 0) throw new UsageError(`unexpected argument ${positionals[0]}`);
|
|
725
|
+
return { kind: 'accounts.list', accountId: options['--account-id'] ?? commandEnv.CLOUDFLARE_ACCOUNT_ID ?? null };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (group === 'registrar' && subcommand === 'search') {
|
|
729
|
+
const { options, positionals } = parseOptions(rest, REGISTRAR_FLAGS);
|
|
730
|
+
if (positionals.length > 0) throw new UsageError(`unexpected argument ${positionals[0]}`);
|
|
731
|
+
return {
|
|
732
|
+
kind: 'registrar.search',
|
|
733
|
+
accountId: options['--account-id'] ?? commandEnv.CLOUDFLARE_ACCOUNT_ID ?? null,
|
|
734
|
+
query: requireNonEmptyString('--query', options['--query']),
|
|
735
|
+
limit: options['--limit'] === undefined ? undefined : parsePositiveInteger(options['--limit'], '--limit'),
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (group === 'registrar' && subcommand === 'check') {
|
|
740
|
+
const { options, positionals } = parseOptions(rest, REGISTRAR_FLAGS);
|
|
741
|
+
if (positionals.length > 0) throw new UsageError(`unexpected argument ${positionals[0]}`);
|
|
742
|
+
const domain = normalizeDomainName(options['--domain']);
|
|
743
|
+
if (!isValidDomainName(domain)) throw new UsageError('--domain must be a valid DNS domain name');
|
|
744
|
+
return {
|
|
745
|
+
kind: 'registrar.check',
|
|
746
|
+
accountId: options['--account-id'] ?? commandEnv.CLOUDFLARE_ACCOUNT_ID ?? null,
|
|
747
|
+
domain,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (group === 'registrar' && subcommand === 'register') {
|
|
752
|
+
const { options, positionals } = parseOptions(rest, REGISTRAR_FLAGS);
|
|
753
|
+
if (positionals.length > 0) throw new UsageError(`unexpected argument ${positionals[0]}`);
|
|
754
|
+
const domain = normalizeDomainName(options['--domain']);
|
|
755
|
+
if (!isValidDomainName(domain)) throw new UsageError('--domain must be a valid DNS domain name');
|
|
756
|
+
const confirmationDomain = normalizeDomainName(options['--confirm-domain']);
|
|
757
|
+
if (confirmationDomain !== domain) throw new UsageError('--confirm-domain must exactly match --domain after DNS lowercase normalization');
|
|
758
|
+
if (options[REGISTRATION_CHARGE_FLAG] !== true) {
|
|
759
|
+
throw new UsageError(`${REGISTRATION_CHARGE_FLAG} is required for domain registration`);
|
|
760
|
+
}
|
|
761
|
+
parseUsdAmount(options['--max-price-usd'], '--max-price-usd');
|
|
762
|
+
parseUsdAmount(options['--confirm-registration-cost'], '--confirm-registration-cost');
|
|
763
|
+
return {
|
|
764
|
+
kind: 'registrar.register',
|
|
765
|
+
accountId: options['--account-id'] ?? commandEnv.CLOUDFLARE_ACCOUNT_ID ?? null,
|
|
766
|
+
domain,
|
|
767
|
+
maxPriceUsd: options['--max-price-usd'],
|
|
768
|
+
confirmationDomain,
|
|
769
|
+
confirmationCostUsd: options['--confirm-registration-cost'],
|
|
770
|
+
registrantContactJson: options['--registrant-contact-json'] ?? null,
|
|
771
|
+
iUnderstandThisChargesMyPaymentMethod: true,
|
|
772
|
+
execute: options['--execute'] === true,
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (group === 'pages' && subcommand === 'deploy') {
|
|
777
|
+
const { options, positionals } = parseOptions(rest, PAGES_DEPLOY_FLAGS);
|
|
778
|
+
if (positionals.length > 0) throw new UsageError(`unexpected argument ${positionals[0]}`);
|
|
779
|
+
return {
|
|
780
|
+
kind: 'pages.deploy',
|
|
781
|
+
site: requireNonEmptyString('--site', options['--site']),
|
|
782
|
+
projectName: requireNonEmptyString('--project-name', options['--project-name']),
|
|
783
|
+
execute: options['--execute'] === true,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (group === 'pages' && subcommand === 'verify') {
|
|
788
|
+
const { options, positionals } = parseOptions(rest, PAGES_VERIFY_FLAGS);
|
|
789
|
+
if (positionals.length > 0) throw new UsageError(`unexpected argument ${positionals[0]}`);
|
|
790
|
+
const url = parsePublicHttpsUrl(options['--url']);
|
|
791
|
+
const domain = parseOptionalHost(options['--domain']);
|
|
792
|
+
const urlHost = normalizeDomainName(new URL(url).hostname);
|
|
793
|
+
if (domain !== null && domain !== urlHost) {
|
|
794
|
+
throw new UsageError('--domain must exactly match --url host after DNS lowercase normalization');
|
|
795
|
+
}
|
|
796
|
+
return {
|
|
797
|
+
kind: 'pages.verify',
|
|
798
|
+
url,
|
|
799
|
+
projectName: requireNonEmptyString('--project-name', options['--project-name']),
|
|
800
|
+
domain,
|
|
801
|
+
accountId: options['--account-id'] ?? commandEnv.CLOUDFLARE_ACCOUNT_ID ?? null,
|
|
802
|
+
cloudflareLive: parseCloudflareLiveMode(options['--cloudflare-live']),
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (group === 'workers' && subcommand === 'deploy-probe') {
|
|
807
|
+
const { options, positionals } = parseOptions(rest, WORKERS_DEPLOY_FLAGS);
|
|
808
|
+
if (positionals.length > 0) throw new UsageError(`unexpected argument ${positionals[0]}`);
|
|
809
|
+
return {
|
|
810
|
+
kind: 'workers.deploy-probe',
|
|
811
|
+
script: requireNonEmptyString('--script', options['--script']),
|
|
812
|
+
name: options['--name'] ?? 'enigma-hosted-probe',
|
|
813
|
+
compatibilityDate: options['--compatibility-date'] ?? '2026-06-24',
|
|
814
|
+
execute: options['--execute'] === true,
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (group === 'workers' && subcommand === 'deploy-edge') {
|
|
819
|
+
const { options, positionals } = parseOptions(rest, WORKERS_DEPLOY_FLAGS);
|
|
820
|
+
if (positionals.length > 0) throw new UsageError(`unexpected argument ${positionals[0]}`);
|
|
821
|
+
const name = requireNonEmptyString('--name', options['--name']);
|
|
822
|
+
if (name !== 'enigma-relay' && name !== 'enigma-gateway') throw new UsageError('--name must be enigma-relay or enigma-gateway');
|
|
823
|
+
const domain = normalizeDomainName(requireNonEmptyString('--domain', options['--domain']));
|
|
824
|
+
const expectedDomain = name === 'enigma-relay' ? 'relay.enigmamemory.com' : 'gateway.enigmamemory.com';
|
|
825
|
+
if (domain !== expectedDomain) throw new UsageError(`--domain must be ${expectedDomain} for ${name}`);
|
|
826
|
+
return {
|
|
827
|
+
kind: 'workers.deploy-edge',
|
|
828
|
+
script: requireNonEmptyString('--script', options['--script']),
|
|
829
|
+
name,
|
|
830
|
+
domain,
|
|
831
|
+
compatibilityDate: options['--compatibility-date'] ?? '2026-06-24',
|
|
832
|
+
execute: options['--execute'] === true,
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (group === 'workers' && subcommand === 'inspect-probe') {
|
|
837
|
+
const { options, positionals } = parseOptions(rest, WORKERS_INSPECT_FLAGS);
|
|
838
|
+
if (positionals.length > 0) throw new UsageError(`unexpected argument ${positionals[0]}`);
|
|
839
|
+
return {
|
|
840
|
+
kind: 'workers.inspect-probe',
|
|
841
|
+
name: options['--name'] ?? 'enigma-hosted-probe',
|
|
842
|
+
accountId: options['--account-id'] ?? commandEnv.CLOUDFLARE_ACCOUNT_ID ?? null,
|
|
843
|
+
out: options['--out'] ?? null,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (group === 'workers' && subcommand === 'verify-probe') {
|
|
848
|
+
const { options, positionals } = parseOptions(rest, WORKERS_VERIFY_FLAGS);
|
|
849
|
+
if (positionals.length > 0) throw new UsageError(`unexpected argument ${positionals[0]}`);
|
|
850
|
+
const expect = (options['--expect'] ?? 'fail-closed').toLowerCase();
|
|
851
|
+
if (expect !== 'fail-closed' && expect !== 'ready') throw new UsageError('--expect must be fail-closed or ready');
|
|
852
|
+
return {
|
|
853
|
+
kind: 'workers.verify-probe',
|
|
854
|
+
url: parsePublicHttpsUrl(options['--url']),
|
|
855
|
+
expect,
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
throw new UsageError(`unknown command: ${tokens.slice(0, 2).join(' ')}`);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function safeJson(value) {
|
|
863
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function writeJsonOut(outPath, value) {
|
|
867
|
+
if (typeof outPath !== 'string' || outPath.length === 0) return;
|
|
868
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
869
|
+
await writeFile(outPath, safeJson(value), 'utf8');
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
async function cloudflareRequest(plan, token, fetchImpl = globalThis.fetch) {
|
|
873
|
+
if (typeof fetchImpl !== 'function') throw new UsageError('global fetch is not available in this Node runtime');
|
|
874
|
+
const headers = {
|
|
875
|
+
Authorization: `Bearer ${token}`,
|
|
876
|
+
...(plan.body === undefined ? {} : { 'Content-Type': 'application/json' }),
|
|
877
|
+
};
|
|
878
|
+
const response = await fetchImpl(plan.url, {
|
|
879
|
+
method: plan.method,
|
|
880
|
+
headers,
|
|
881
|
+
...(plan.body === undefined ? {} : { body: JSON.stringify(plan.body) }),
|
|
882
|
+
});
|
|
883
|
+
const text = await response.text();
|
|
884
|
+
let payload = null;
|
|
885
|
+
if (text.trim().length > 0) payload = JSON.parse(text);
|
|
886
|
+
if (!response.ok || payload?.success === false) {
|
|
887
|
+
const error = new Error(`Cloudflare API request failed with HTTP ${response.status}`);
|
|
888
|
+
error.payload = payload;
|
|
889
|
+
error.status = response.status;
|
|
890
|
+
throw error;
|
|
891
|
+
}
|
|
892
|
+
return { status: response.status, payload };
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const PAGES_READINESS_CLAIM_BOUNDARY = 'This proves public Cloudflare Pages reachability and optional Cloudflare token/account/Pages project observation only; it does not prove hosted relay, gateway, KMS, storage, SIEM, backup, operator, or token readiness.';
|
|
896
|
+
|
|
897
|
+
function responseHeader(headers, name) {
|
|
898
|
+
if (!headers) return '';
|
|
899
|
+
if (typeof headers.get === 'function') return headers.get(name) ?? '';
|
|
900
|
+
const lowerName = name.toLowerCase();
|
|
901
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
902
|
+
if (String(key).toLowerCase() === lowerName) return Array.isArray(value) ? value.join(', ') : String(value);
|
|
903
|
+
}
|
|
904
|
+
return '';
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function isHtmlishResponse(contentType, bodyText) {
|
|
908
|
+
const normalizedContentType = String(contentType ?? '').toLowerCase();
|
|
909
|
+
if (normalizedContentType.includes('text/html') || normalizedContentType.includes('application/xhtml+xml')) return true;
|
|
910
|
+
const trimmed = String(bodyText ?? '').trimStart();
|
|
911
|
+
return /^<!doctype\s+html\b/i.test(trimmed) || /^<html(?:\s|>)/i.test(trimmed);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function errorMessage(error) {
|
|
915
|
+
return stripUrlUserinfoFromText(error?.message ?? String(error));
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function credentialErrorMessage(error, token) {
|
|
919
|
+
const message = errorMessage(error);
|
|
920
|
+
return typeof token === 'string' && token.length > 0 ? message.split(token).join('[redacted]') : message;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
async function publicPagesReachabilityCheck({ url, domain, fetchImpl }) {
|
|
924
|
+
const publicUrl = parsePublicHttpsUrl(url);
|
|
925
|
+
if (typeof fetchImpl !== 'function') throw new UsageError('global fetch is not available in this Node runtime');
|
|
926
|
+
const expectedHost = parseOptionalHost(domain);
|
|
927
|
+
const initialHost = normalizeDomainName(new URL(publicUrl).hostname);
|
|
928
|
+
const blockers = [];
|
|
929
|
+
if (expectedHost !== null && expectedHost !== initialHost) {
|
|
930
|
+
blockers.push('--domain did not match the public URL host');
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
let response;
|
|
934
|
+
try {
|
|
935
|
+
response = await fetchImpl(publicUrl, {
|
|
936
|
+
method: 'GET',
|
|
937
|
+
headers: {
|
|
938
|
+
Accept: 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.1',
|
|
939
|
+
},
|
|
940
|
+
redirect: 'follow',
|
|
941
|
+
});
|
|
942
|
+
} catch (error) {
|
|
943
|
+
return {
|
|
944
|
+
ok: false,
|
|
945
|
+
method: 'GET',
|
|
946
|
+
url: publicUrl,
|
|
947
|
+
final_url: null,
|
|
948
|
+
status: null,
|
|
949
|
+
https: true,
|
|
950
|
+
final_https: null,
|
|
951
|
+
authorization_header_sent: false,
|
|
952
|
+
content_type: null,
|
|
953
|
+
htmlish: false,
|
|
954
|
+
host: initialHost,
|
|
955
|
+
domain_expected: expectedHost,
|
|
956
|
+
domain_matched: blockers.length === 0,
|
|
957
|
+
blockers: [...blockers, `public Pages URL fetch failed: ${errorMessage(error)}`],
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
let bodyText = '';
|
|
962
|
+
try {
|
|
963
|
+
bodyText = typeof response.text === 'function' ? await response.text() : '';
|
|
964
|
+
} catch (error) {
|
|
965
|
+
blockers.push(`public Pages URL body read failed: ${errorMessage(error)}`);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
let finalUrl = typeof response.url === 'string' && response.url.length > 0 ? response.url : publicUrl;
|
|
969
|
+
let reportedFinalUrl = finalUrl;
|
|
970
|
+
let finalHost = initialHost;
|
|
971
|
+
let finalHttps = true;
|
|
972
|
+
try {
|
|
973
|
+
const parsedFinalUrl = new URL(finalUrl);
|
|
974
|
+
const redactedFinalUrl = stripUrlUserinfoFromText(finalUrl);
|
|
975
|
+
if (redactedFinalUrl !== finalUrl || rawUrlAuthorityHasUserinfo(finalUrl) || parsedUrlHasUserinfo(parsedFinalUrl)) {
|
|
976
|
+
blockers.push('public Pages URL final URL included userinfo');
|
|
977
|
+
parsedFinalUrl.username = '';
|
|
978
|
+
parsedFinalUrl.password = '';
|
|
979
|
+
reportedFinalUrl = redactedFinalUrl;
|
|
980
|
+
}
|
|
981
|
+
finalHost = normalizeDomainName(parsedFinalUrl.hostname);
|
|
982
|
+
finalHttps = parsedFinalUrl.protocol === 'https:';
|
|
983
|
+
} catch {
|
|
984
|
+
reportedFinalUrl = null;
|
|
985
|
+
finalHttps = false;
|
|
986
|
+
blockers.push('public Pages URL response reported an invalid final URL');
|
|
987
|
+
}
|
|
988
|
+
const status = Number(response.status ?? 0);
|
|
989
|
+
const statusOk = status >= 200 && status <= 299;
|
|
990
|
+
const contentType = responseHeader(response.headers, 'content-type');
|
|
991
|
+
const htmlish = isHtmlishResponse(contentType, bodyText);
|
|
992
|
+
const domainMatched = expectedHost === null || finalHost === expectedHost;
|
|
993
|
+
if (!statusOk) blockers.push(`public Pages URL returned HTTP ${Number.isFinite(status) ? status : 'unknown'}`);
|
|
994
|
+
if (!finalHttps) blockers.push('public Pages URL final URL did not remain HTTPS');
|
|
995
|
+
if (!htmlish) blockers.push('public Pages URL did not return HTML-ish content');
|
|
996
|
+
if (!domainMatched) blockers.push('public Pages URL final host did not match --domain');
|
|
997
|
+
|
|
998
|
+
return {
|
|
999
|
+
ok: blockers.length === 0,
|
|
1000
|
+
method: 'GET',
|
|
1001
|
+
url: publicUrl,
|
|
1002
|
+
final_url: reportedFinalUrl,
|
|
1003
|
+
status: Number.isFinite(status) ? status : null,
|
|
1004
|
+
https: true,
|
|
1005
|
+
final_https: finalHttps,
|
|
1006
|
+
authorization_header_sent: false,
|
|
1007
|
+
content_type: contentType || null,
|
|
1008
|
+
htmlish,
|
|
1009
|
+
host: initialHost,
|
|
1010
|
+
final_host: finalHost,
|
|
1011
|
+
domain_expected: expectedHost,
|
|
1012
|
+
domain_matched: domainMatched,
|
|
1013
|
+
blockers,
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function hostedProbeEndpointUrl(baseUrl, endpointPath) {
|
|
1018
|
+
const url = new URL(parsePublicHttpsUrl(baseUrl));
|
|
1019
|
+
url.pathname = endpointPath;
|
|
1020
|
+
url.search = '';
|
|
1021
|
+
url.hash = '';
|
|
1022
|
+
return url.toString();
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function publicJsonEndpointErrorMessage(error) {
|
|
1026
|
+
const message = errorMessage(error);
|
|
1027
|
+
return /not allowed|secret-looking|public-safe|probe body field/i.test(message)
|
|
1028
|
+
? 'public JSON endpoint returned non-public-safe payload'
|
|
1029
|
+
: 'public JSON endpoint fetch failed';
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
async function fetchJsonEndpoint({ url, fetchImpl }) {
|
|
1033
|
+
if (typeof fetchImpl !== 'function') throw new UsageError('global fetch is not available in this Node runtime');
|
|
1034
|
+
try {
|
|
1035
|
+
const response = await fetchImpl(url, {
|
|
1036
|
+
method: 'GET',
|
|
1037
|
+
headers: { Accept: 'application/json' },
|
|
1038
|
+
redirect: 'manual',
|
|
1039
|
+
});
|
|
1040
|
+
const status = Number(response.status ?? 0);
|
|
1041
|
+
const responseUrl = typeof response.url === 'string' && response.url.length > 0 ? stripUrlUserinfoFromText(response.url) : url;
|
|
1042
|
+
const contentType = responseHeader(response.headers, 'content-type');
|
|
1043
|
+
if (response.redirected === true || responseUrl !== url || (status >= 300 && status <= 399)) {
|
|
1044
|
+
return {
|
|
1045
|
+
ok: false,
|
|
1046
|
+
status: Number.isFinite(status) ? status : null,
|
|
1047
|
+
url: responseUrl,
|
|
1048
|
+
content_type: contentType || null,
|
|
1049
|
+
json: false,
|
|
1050
|
+
payload: { error: 'public JSON endpoint must not redirect' },
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
const text = typeof response.text === 'function' ? await response.text() : '';
|
|
1054
|
+
let payload = null;
|
|
1055
|
+
try {
|
|
1056
|
+
payload = text.trim().length === 0 ? null : JSON.parse(text);
|
|
1057
|
+
} catch {
|
|
1058
|
+
payload = null;
|
|
1059
|
+
}
|
|
1060
|
+
if (payload !== null) assertProbePayloadSafe(payload, 'hosted_probe');
|
|
1061
|
+
return {
|
|
1062
|
+
ok: status >= 200 && status <= 299,
|
|
1063
|
+
status: Number.isFinite(status) ? status : null,
|
|
1064
|
+
url: responseUrl,
|
|
1065
|
+
content_type: contentType || null,
|
|
1066
|
+
json: payload !== null,
|
|
1067
|
+
payload,
|
|
1068
|
+
};
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
return {
|
|
1071
|
+
ok: false,
|
|
1072
|
+
status: null,
|
|
1073
|
+
url: '<hosted-probe-worker-url>',
|
|
1074
|
+
content_type: null,
|
|
1075
|
+
json: false,
|
|
1076
|
+
payload: null,
|
|
1077
|
+
error: publicJsonEndpointErrorMessage(error),
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function summarizeHostedProbePayload(payload) {
|
|
1083
|
+
if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return null;
|
|
1084
|
+
const checks = Array.isArray(payload.checks) ? payload.checks : null;
|
|
1085
|
+
const error = typeof payload.error === 'string' && /redirect/i.test(payload.error)
|
|
1086
|
+
? 'public JSON endpoint must not redirect'
|
|
1087
|
+
: null;
|
|
1088
|
+
return {
|
|
1089
|
+
ok: payload.ok === true ? true : payload.ok === false ? false : null,
|
|
1090
|
+
hosted_probe_only: payload.hosted_probe_only === true,
|
|
1091
|
+
missing_evidence_ref_count: Array.isArray(payload.missing_evidence_refs) ? payload.missing_evidence_refs.length : null,
|
|
1092
|
+
checks_ok: checks === null ? null : checks.every((check) => check?.ok === true),
|
|
1093
|
+
error,
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
function redactHostedProbeEndpointResult(result, endpointPath) {
|
|
1099
|
+
return {
|
|
1100
|
+
...result,
|
|
1101
|
+
url: `<hosted-probe-worker-url>${endpointPath}`,
|
|
1102
|
+
payload: summarizeHostedProbePayload(result.payload),
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
export async function verifyHostedProbeWorker(command, { fetchImpl = globalThis.fetch } = {}) {
|
|
1107
|
+
const baseUrl = parsePublicHttpsUrl(command.url);
|
|
1108
|
+
const expected = command.expect === 'ready' ? 'ready' : 'fail-closed';
|
|
1109
|
+
const livez = await fetchJsonEndpoint({ url: hostedProbeEndpointUrl(baseUrl, '/livez'), fetchImpl });
|
|
1110
|
+
const readyz = await fetchJsonEndpoint({ url: hostedProbeEndpointUrl(baseUrl, '/readyz'), fetchImpl });
|
|
1111
|
+
const blockers = [];
|
|
1112
|
+
if (livez.status !== 200 || livez.payload?.ok !== true || livez.payload?.hosted_probe_only !== true) {
|
|
1113
|
+
blockers.push('/livez did not return hosted probe ok:true with HTTP 200');
|
|
1114
|
+
}
|
|
1115
|
+
if (expected === 'ready') {
|
|
1116
|
+
const missingRefs = readyz.payload?.missing_evidence_refs;
|
|
1117
|
+
const checks = readyz.payload?.checks;
|
|
1118
|
+
const checksOk = checks === undefined || (Array.isArray(checks) && checks.every((check) => check?.ok === true));
|
|
1119
|
+
if (readyz.status !== 200 || readyz.payload?.ok !== true || readyz.payload?.hosted_probe_only !== true || !Array.isArray(missingRefs) || missingRefs.length !== 0 || !checksOk) {
|
|
1120
|
+
blockers.push('/readyz did not return ready ok:true with HTTP 200, hosted_probe_only:true, missing_evidence_refs:[], and all checks ok');
|
|
1121
|
+
}
|
|
1122
|
+
} else if (readyz.status !== 503 || readyz.payload?.ok !== false || readyz.payload?.hosted_probe_only !== true || !Array.isArray(readyz.payload?.missing_evidence_refs)) {
|
|
1123
|
+
blockers.push('/readyz did not fail closed with HTTP 503, ok:false, hosted_probe_only:true, and missing_evidence_refs');
|
|
1124
|
+
}
|
|
1125
|
+
return redactOperationalPayload({
|
|
1126
|
+
schema: CLOUDFLARE_OPS_SCHEMA,
|
|
1127
|
+
operation: 'workers.verify-probe',
|
|
1128
|
+
ok: blockers.length === 0,
|
|
1129
|
+
expected,
|
|
1130
|
+
base_url: '<hosted-probe-worker-url>',
|
|
1131
|
+
authorization_header_sent: false,
|
|
1132
|
+
tokenPrinted: false,
|
|
1133
|
+
livez: redactHostedProbeEndpointResult(livez, '/livez'),
|
|
1134
|
+
readyz: redactHostedProbeEndpointResult(readyz, '/readyz'),
|
|
1135
|
+
blockers,
|
|
1136
|
+
claimBoundary: 'Hosted probe Worker verification proves only public /livez and /readyz edge-probe behavior, not relay/gateway hosted readiness.',
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
export async function inspectHostedProbeWorkerService(command, { env = process.env, fetchImpl = globalThis.fetch } = {}) {
|
|
1141
|
+
const token = requireApiToken(env);
|
|
1142
|
+
const accountId = requireAccountId(command, env);
|
|
1143
|
+
const name = requireNonEmptyString('--name', command.name ?? 'enigma-hosted-probe');
|
|
1144
|
+
const plan = buildCloudflareRequestPlan({ operation: 'workers.service', accountId, projectName: name });
|
|
1145
|
+
const blockers = [];
|
|
1146
|
+
let status = null;
|
|
1147
|
+
let serviceObserved = false;
|
|
1148
|
+
let payload = null;
|
|
1149
|
+
try {
|
|
1150
|
+
const response = await cloudflareRequest(plan, token, fetchImpl);
|
|
1151
|
+
status = response.status;
|
|
1152
|
+
payload = redactOperationalPayload(response.payload);
|
|
1153
|
+
serviceObserved = true;
|
|
1154
|
+
} catch (error) {
|
|
1155
|
+
status = Number.isInteger(error?.status) ? error.status : null;
|
|
1156
|
+
payload = error?.payload ? redactOperationalPayload(error.payload) : null;
|
|
1157
|
+
blockers.push(`Worker service observation failed: ${credentialErrorMessage(error, token)}`);
|
|
1158
|
+
}
|
|
1159
|
+
return {
|
|
1160
|
+
schema: CLOUDFLARE_OPS_SCHEMA,
|
|
1161
|
+
operation: 'workers.inspect-probe',
|
|
1162
|
+
ok: blockers.length === 0,
|
|
1163
|
+
execute: true,
|
|
1164
|
+
mutates_cloudflare: false,
|
|
1165
|
+
service_name: name,
|
|
1166
|
+
service_observed: serviceObserved,
|
|
1167
|
+
status,
|
|
1168
|
+
plan: redactPlanOutput(plan),
|
|
1169
|
+
response: payload === null ? null : { status, payload },
|
|
1170
|
+
tokenPrinted: false,
|
|
1171
|
+
blockers,
|
|
1172
|
+
claimBoundary: 'Worker service inspection is a non-mutating Cloudflare API visibility and permission check only; it does not deploy the Worker and is not hosted relay/gateway readiness.',
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function tokenFromEnv(env) {
|
|
1177
|
+
const token = env?.CLOUDFLARE_API_TOKEN;
|
|
1178
|
+
return typeof token === 'string' && token.trim().length > 0 ? token : null;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function normalizeCloudflareProjectUrl(value) {
|
|
1182
|
+
if (typeof value !== 'string' || value.trim().length === 0) return null;
|
|
1183
|
+
if (rawUrlAuthorityHasUserinfo(value) || urlTextHasUserinfo(value)) return null;
|
|
1184
|
+
let url;
|
|
1185
|
+
try {
|
|
1186
|
+
url = new URL(value);
|
|
1187
|
+
} catch {
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
if (parsedUrlHasUserinfo(url) || url.protocol !== 'https:') return null;
|
|
1191
|
+
const host = normalizeDomainName(url.hostname);
|
|
1192
|
+
if (!isValidDomainName(host)) return null;
|
|
1193
|
+
url.hash = '';
|
|
1194
|
+
return url.toString();
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function cloudflareProjectHostValue(value) {
|
|
1198
|
+
const normalizedUrl = normalizeCloudflareProjectUrl(value);
|
|
1199
|
+
if (normalizedUrl !== null) return normalizeDomainName(new URL(normalizedUrl).hostname);
|
|
1200
|
+
if (typeof value !== 'string' || value.trim().length === 0) return null;
|
|
1201
|
+
const host = normalizeDomainName(value);
|
|
1202
|
+
return isValidDomainName(host) ? host : null;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function addCloudflareProjectHost(hosts, value) {
|
|
1206
|
+
const host = cloudflareProjectHostValue(value);
|
|
1207
|
+
if (host !== null) hosts.add(host);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function addCloudflareProjectUrl(urls, hosts, value) {
|
|
1211
|
+
const normalizedUrl = normalizeCloudflareProjectUrl(value);
|
|
1212
|
+
if (normalizedUrl === null) {
|
|
1213
|
+
addCloudflareProjectHost(hosts, value);
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
urls.add(normalizedUrl);
|
|
1217
|
+
hosts.add(normalizeDomainName(new URL(normalizedUrl).hostname));
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
function addCloudflareProjectDomainEntry(hosts, urls, entry) {
|
|
1221
|
+
if (typeof entry === 'string') {
|
|
1222
|
+
addCloudflareProjectUrl(urls, hosts, entry);
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) return;
|
|
1226
|
+
for (const field of ['name', 'hostname', 'host', 'domain', 'domain_name', 'url']) {
|
|
1227
|
+
addCloudflareProjectUrl(urls, hosts, entry[field]);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function collectCloudflarePagesProjectObservation(payload) {
|
|
1232
|
+
const result = payload?.result;
|
|
1233
|
+
const hosts = new Set();
|
|
1234
|
+
const urls = new Set();
|
|
1235
|
+
addCloudflareProjectHost(hosts, result?.subdomain);
|
|
1236
|
+
for (const domainEntry of Array.isArray(result?.domains) ? result.domains : []) {
|
|
1237
|
+
addCloudflareProjectDomainEntry(hosts, urls, domainEntry);
|
|
1238
|
+
}
|
|
1239
|
+
for (const deployment of [result?.latest_deployment, result?.canonical_deployment, result?.latest, result?.latestDeployment, result?.canonicalDeployment]) {
|
|
1240
|
+
if (deployment === null || typeof deployment !== 'object' || Array.isArray(deployment)) continue;
|
|
1241
|
+
addCloudflareProjectUrl(urls, hosts, deployment.url);
|
|
1242
|
+
for (const alias of Array.isArray(deployment.aliases) ? deployment.aliases : []) {
|
|
1243
|
+
addCloudflareProjectUrl(urls, hosts, alias);
|
|
1244
|
+
}
|
|
1245
|
+
for (const domainEntry of Array.isArray(deployment.domains) ? deployment.domains : []) {
|
|
1246
|
+
addCloudflareProjectDomainEntry(hosts, urls, domainEntry);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
return {
|
|
1250
|
+
name: typeof result?.name === 'string' ? result.name : null,
|
|
1251
|
+
hosts: [...hosts].sort(),
|
|
1252
|
+
urls: [...urls].sort(),
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function evaluateCloudflarePagesProjectObservation({ payload, projectName, url, domain, status }) {
|
|
1257
|
+
const expectedProjectName = requireNonEmptyString('--project-name', projectName);
|
|
1258
|
+
const expectedUrl = parsePublicHttpsUrl(url);
|
|
1259
|
+
const expectedUrlHost = normalizeDomainName(new URL(expectedUrl).hostname);
|
|
1260
|
+
const expectedDomain = parseOptionalHost(domain);
|
|
1261
|
+
const observed = collectCloudflarePagesProjectObservation(payload);
|
|
1262
|
+
const observedHosts = new Set(observed.hosts);
|
|
1263
|
+
const observedUrls = new Set(observed.urls);
|
|
1264
|
+
const projectNameMatched = observed.name === expectedProjectName;
|
|
1265
|
+
const urlMatched = observedUrls.has(expectedUrl) || observedHosts.has(expectedUrlHost);
|
|
1266
|
+
const domainMatched = expectedDomain === null || observedHosts.has(expectedDomain);
|
|
1267
|
+
const blockers = [];
|
|
1268
|
+
if (!projectNameMatched) blockers.push('Cloudflare Pages project name did not match --project-name');
|
|
1269
|
+
if (!urlMatched) blockers.push('Cloudflare Pages project URL/domains did not match --url');
|
|
1270
|
+
if (!domainMatched) blockers.push('Cloudflare Pages project domains did not include --domain');
|
|
1271
|
+
return {
|
|
1272
|
+
ok: blockers.length === 0,
|
|
1273
|
+
operation: 'pages.project',
|
|
1274
|
+
lookup_performed: true,
|
|
1275
|
+
status,
|
|
1276
|
+
project_name_matched: projectNameMatched,
|
|
1277
|
+
url_matched: urlMatched,
|
|
1278
|
+
domain_matched: domainMatched,
|
|
1279
|
+
observed_project_name: observed.name,
|
|
1280
|
+
observed_hosts: observed.hosts,
|
|
1281
|
+
observed_urls: observed.urls,
|
|
1282
|
+
blockers,
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
async function cloudflareLiveObservationCheck(command, env, fetchImpl) {
|
|
1287
|
+
const mode = parseCloudflareLiveMode(command.cloudflareLive);
|
|
1288
|
+
const accountId = accountIdFrom(command, env);
|
|
1289
|
+
const tokenScope = accountId ? 'account' : 'user';
|
|
1290
|
+
if (mode === 'off') {
|
|
1291
|
+
return {
|
|
1292
|
+
ok: true,
|
|
1293
|
+
mode,
|
|
1294
|
+
skipped: true,
|
|
1295
|
+
credentials_used: false,
|
|
1296
|
+
credentials_required_by_mode: false,
|
|
1297
|
+
token_present: false,
|
|
1298
|
+
account_id_present: accountId !== null,
|
|
1299
|
+
token_scope: tokenScope,
|
|
1300
|
+
token_printed: false,
|
|
1301
|
+
project: null,
|
|
1302
|
+
blockers: [],
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
const token = tokenFromEnv(env);
|
|
1307
|
+
if (token === null) {
|
|
1308
|
+
const blockers = mode === 'required' ? ['--cloudflare-live required but CLOUDFLARE_API_TOKEN is absent'] : [];
|
|
1309
|
+
return {
|
|
1310
|
+
ok: blockers.length === 0,
|
|
1311
|
+
mode,
|
|
1312
|
+
skipped: true,
|
|
1313
|
+
credentials_used: false,
|
|
1314
|
+
credentials_required_by_mode: mode === 'required',
|
|
1315
|
+
token_present: false,
|
|
1316
|
+
account_id_present: accountId !== null,
|
|
1317
|
+
token_scope: tokenScope,
|
|
1318
|
+
token_printed: false,
|
|
1319
|
+
project: null,
|
|
1320
|
+
blockers,
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
if (accountId !== null && typeof command.projectName === 'string' && command.projectName.trim().length > 0) {
|
|
1325
|
+
const projectPlan = buildCloudflareRequestPlan({ operation: 'pages.project', accountId, projectName: command.projectName });
|
|
1326
|
+
try {
|
|
1327
|
+
const projectResponse = await cloudflareRequest(projectPlan, token, fetchImpl);
|
|
1328
|
+
const project = evaluateCloudflarePagesProjectObservation({
|
|
1329
|
+
payload: projectResponse.payload,
|
|
1330
|
+
projectName: command.projectName,
|
|
1331
|
+
url: command.url,
|
|
1332
|
+
domain: command.domain,
|
|
1333
|
+
status: projectResponse.status,
|
|
1334
|
+
});
|
|
1335
|
+
return {
|
|
1336
|
+
ok: project.ok,
|
|
1337
|
+
mode,
|
|
1338
|
+
skipped: false,
|
|
1339
|
+
credentials_used: true,
|
|
1340
|
+
credentials_required_by_mode: mode === 'required',
|
|
1341
|
+
token_present: true,
|
|
1342
|
+
account_id_present: true,
|
|
1343
|
+
token_scope: projectPlan.tokenScope,
|
|
1344
|
+
token_printed: false,
|
|
1345
|
+
status: projectResponse.status,
|
|
1346
|
+
project,
|
|
1347
|
+
blockers: project.blockers,
|
|
1348
|
+
};
|
|
1349
|
+
} catch (error) {
|
|
1350
|
+
const project = {
|
|
1351
|
+
ok: false,
|
|
1352
|
+
operation: 'pages.project',
|
|
1353
|
+
lookup_performed: true,
|
|
1354
|
+
status: error?.status ?? null,
|
|
1355
|
+
project_name_matched: false,
|
|
1356
|
+
url_matched: false,
|
|
1357
|
+
domain_matched: false,
|
|
1358
|
+
observed_project_name: null,
|
|
1359
|
+
observed_hosts: [],
|
|
1360
|
+
observed_urls: [],
|
|
1361
|
+
blockers: [`Cloudflare Pages project observation failed: ${credentialErrorMessage(error, token)}`],
|
|
1362
|
+
};
|
|
1363
|
+
return {
|
|
1364
|
+
ok: false,
|
|
1365
|
+
mode,
|
|
1366
|
+
skipped: false,
|
|
1367
|
+
credentials_used: true,
|
|
1368
|
+
credentials_required_by_mode: mode === 'required',
|
|
1369
|
+
token_present: true,
|
|
1370
|
+
account_id_present: true,
|
|
1371
|
+
token_scope: projectPlan.tokenScope,
|
|
1372
|
+
token_printed: false,
|
|
1373
|
+
status: error?.status ?? null,
|
|
1374
|
+
project,
|
|
1375
|
+
blockers: project.blockers,
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
const blockers = ['Cloudflare Pages project observation requires --account-id with CLOUDFLARE_API_TOKEN'];
|
|
1381
|
+
return {
|
|
1382
|
+
ok: mode !== 'required',
|
|
1383
|
+
mode,
|
|
1384
|
+
skipped: true,
|
|
1385
|
+
credentials_used: false,
|
|
1386
|
+
credentials_required_by_mode: mode === 'required',
|
|
1387
|
+
token_present: true,
|
|
1388
|
+
account_id_present: false,
|
|
1389
|
+
token_scope: tokenScope,
|
|
1390
|
+
token_printed: false,
|
|
1391
|
+
status: null,
|
|
1392
|
+
project: null,
|
|
1393
|
+
blockers: mode === 'required' ? blockers : [],
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
export async function verifyCloudflarePagesReadiness(command, {
|
|
1398
|
+
env = process.env,
|
|
1399
|
+
fetchImpl = globalThis.fetch,
|
|
1400
|
+
} = {}) {
|
|
1401
|
+
if (command?.kind !== 'pages.verify') throw new UsageError('verifyCloudflarePagesReadiness requires a pages.verify command');
|
|
1402
|
+
const publicUrl = parsePublicHttpsUrl(command.url);
|
|
1403
|
+
const domain = parseOptionalHost(command.domain);
|
|
1404
|
+
const cloudflareLive = parseCloudflareLiveMode(command.cloudflareLive);
|
|
1405
|
+
const projectName = requireNonEmptyString('--project-name', command.projectName);
|
|
1406
|
+
const publicUrlCheck = await publicPagesReachabilityCheck({ url: publicUrl, domain, fetchImpl });
|
|
1407
|
+
const cloudflareLiveCheck = await cloudflareLiveObservationCheck({ ...command, url: publicUrl, domain, projectName, cloudflareLive }, env, fetchImpl);
|
|
1408
|
+
const externalBlockers = [...publicUrlCheck.blockers, ...cloudflareLiveCheck.blockers];
|
|
1409
|
+
const contractReady = true;
|
|
1410
|
+
const requestedLiveChecksPass = publicUrlCheck.ok && cloudflareLiveCheck.ok;
|
|
1411
|
+
const operatorAcceptanceDecision = 'not_assessed';
|
|
1412
|
+
const hostedLiveReady = contractReady
|
|
1413
|
+
&& requestedLiveChecksPass
|
|
1414
|
+
&& operatorAcceptanceDecision === 'go'
|
|
1415
|
+
&& externalBlockers.length === 0;
|
|
1416
|
+
|
|
1417
|
+
return {
|
|
1418
|
+
schema: INFRASTRUCTURE_READINESS_SCHEMA,
|
|
1419
|
+
ok: contractReady && requestedLiveChecksPass && externalBlockers.length === 0,
|
|
1420
|
+
generated_at: new Date().toISOString(),
|
|
1421
|
+
mode: cloudflareLive,
|
|
1422
|
+
operation: 'pages.verify',
|
|
1423
|
+
cloudflare_live: cloudflareLive,
|
|
1424
|
+
project_name: projectName,
|
|
1425
|
+
credentials_required: false,
|
|
1426
|
+
credentials_used: cloudflareLiveCheck.credentials_used === true,
|
|
1427
|
+
readiness: {
|
|
1428
|
+
contract_ready: contractReady,
|
|
1429
|
+
public_live_ready: publicUrlCheck.ok,
|
|
1430
|
+
cloudflare_observed: cloudflareLiveCheck.credentials_used === true && cloudflareLiveCheck.ok,
|
|
1431
|
+
hosted_live_ready: hostedLiveReady,
|
|
1432
|
+
},
|
|
1433
|
+
checks: {
|
|
1434
|
+
command: {
|
|
1435
|
+
ok: true,
|
|
1436
|
+
project_name_present: true,
|
|
1437
|
+
url: publicUrl,
|
|
1438
|
+
domain,
|
|
1439
|
+
cloudflare_live: cloudflareLive,
|
|
1440
|
+
},
|
|
1441
|
+
public_url: publicUrlCheck,
|
|
1442
|
+
cloudflare_live: cloudflareLiveCheck,
|
|
1443
|
+
operator_acceptance: {
|
|
1444
|
+
ok: true,
|
|
1445
|
+
decision: operatorAcceptanceDecision,
|
|
1446
|
+
required_for_hosted_live_ready: true,
|
|
1447
|
+
},
|
|
1448
|
+
},
|
|
1449
|
+
external_blockers: externalBlockers,
|
|
1450
|
+
claim_boundary: PAGES_READINESS_CLAIM_BOUNDARY,
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
function firstCheckedDomain(payload, domain) {
|
|
1456
|
+
const normalizedDomain = normalizeDomainName(domain);
|
|
1457
|
+
const domains = payload?.result?.domains;
|
|
1458
|
+
if (!Array.isArray(domains)) throw new UsageError('Cloudflare check response did not include result.domains');
|
|
1459
|
+
const match = domains.find((item) => normalizeDomainName(item?.name ?? item?.domain_name) === normalizedDomain);
|
|
1460
|
+
if (!match) throw new UsageError('Cloudflare check response did not include the requested domain');
|
|
1461
|
+
return match;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function assertRegistrationWorkflowState(payload) {
|
|
1465
|
+
const state = payload?.result?.state;
|
|
1466
|
+
if (state === 'action_required') throw new UsageError('registration stopped: Cloudflare returned action_required');
|
|
1467
|
+
if (state === 'failed') throw new UsageError('registration stopped: Cloudflare returned failed');
|
|
1468
|
+
if (state === 'blocked') throw new UsageError('registration stopped: Cloudflare returned blocked');
|
|
1469
|
+
return state ?? null;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
async function followInProgressRegistrationOnce({ accountId, domain, token, fetchImpl }) {
|
|
1473
|
+
const statusPlan = buildCloudflareRequestPlan({
|
|
1474
|
+
operation: 'registrar.registration-status',
|
|
1475
|
+
accountId,
|
|
1476
|
+
domain,
|
|
1477
|
+
});
|
|
1478
|
+
const statusResponse = await cloudflareRequest(statusPlan, token, fetchImpl);
|
|
1479
|
+
const state = assertRegistrationWorkflowState(statusResponse.payload);
|
|
1480
|
+
return {
|
|
1481
|
+
plan: statusPlan,
|
|
1482
|
+
status: statusResponse.status,
|
|
1483
|
+
state,
|
|
1484
|
+
payload: statusResponse.payload,
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
function apiPlanOutput(command, env) {
|
|
1490
|
+
const accountId = command.kind.startsWith('registrar.') ? requireAccountId(command, env) : accountIdFrom(command, env);
|
|
1491
|
+
return buildCloudflareRequestPlan({
|
|
1492
|
+
operation: command.kind,
|
|
1493
|
+
accountId,
|
|
1494
|
+
query: command.query,
|
|
1495
|
+
limit: command.limit,
|
|
1496
|
+
domain: command.domain,
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
export async function runCloudflareOpsCommand(command, {
|
|
1501
|
+
env = process.env,
|
|
1502
|
+
fetchImpl = globalThis.fetch,
|
|
1503
|
+
execFileImpl = execFile,
|
|
1504
|
+
} = {}) {
|
|
1505
|
+
if (command.kind === 'help') return { text: command.help };
|
|
1506
|
+
|
|
1507
|
+
if (command.kind === 'pages.verify') {
|
|
1508
|
+
return { json: await verifyCloudflarePagesReadiness(command, { env, fetchImpl }) };
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
if (command.kind === 'workers.verify-probe') {
|
|
1512
|
+
return { json: await verifyHostedProbeWorker(command, { fetchImpl }) };
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
if (command.kind === 'workers.inspect-probe') {
|
|
1516
|
+
return { json: await inspectHostedProbeWorkerService(command, { env, fetchImpl }) };
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
if (command.kind === 'pages.deploy') {
|
|
1520
|
+
const plan = buildWranglerPagesDeployPlan(command);
|
|
1521
|
+
if (!command.execute) {
|
|
1522
|
+
return {
|
|
1523
|
+
json: {
|
|
1524
|
+
schema: CLOUDFLARE_OPS_SCHEMA,
|
|
1525
|
+
operation: command.kind,
|
|
1526
|
+
dryRun: true,
|
|
1527
|
+
execute: false,
|
|
1528
|
+
plan: redactPlanOutput(plan),
|
|
1529
|
+
claimBoundary: 'Plan only; no Cloudflare Pages deployment was executed.',
|
|
1530
|
+
},
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
const result = await execFileImpl(plan.command, plan.args, { shell: plan.usesShell === true, windowsHide: true, maxBuffer: 10 * 1024 * 1024, env });
|
|
1534
|
+
return {
|
|
1535
|
+
json: {
|
|
1536
|
+
schema: CLOUDFLARE_OPS_SCHEMA,
|
|
1537
|
+
operation: command.kind,
|
|
1538
|
+
dryRun: false,
|
|
1539
|
+
execute: true,
|
|
1540
|
+
plan: redactPlanOutput(plan),
|
|
1541
|
+
stdout: redactOperationalText(result.stdout ?? ''),
|
|
1542
|
+
stderr: redactOperationalText(result.stderr ?? ''),
|
|
1543
|
+
tokenPrinted: false,
|
|
1544
|
+
},
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
if (command.kind === 'workers.deploy-probe' || command.kind === 'workers.deploy-edge') {
|
|
1549
|
+
const plan = buildWranglerWorkerDeployPlan(command);
|
|
1550
|
+
if (!command.execute) {
|
|
1551
|
+
return {
|
|
1552
|
+
json: {
|
|
1553
|
+
schema: CLOUDFLARE_OPS_SCHEMA,
|
|
1554
|
+
operation: command.kind,
|
|
1555
|
+
dryRun: true,
|
|
1556
|
+
execute: false,
|
|
1557
|
+
plan: redactPlanOutput(plan),
|
|
1558
|
+
claimBoundary: command.kind === 'workers.deploy-edge' ? 'Plan only; no relay/gateway edge Worker deployment was executed.' : 'Plan only; no hosted probe Worker deployment was executed.',
|
|
1559
|
+
},
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
const result = await execFileImpl(plan.command, plan.args, { shell: plan.usesShell === true, windowsHide: true, maxBuffer: 10 * 1024 * 1024, env });
|
|
1563
|
+
const safeStdout = redactOperationalText(result.stdout ?? '');
|
|
1564
|
+
const safeStderr = redactOperationalText(result.stderr ?? '');
|
|
1565
|
+
return {
|
|
1566
|
+
json: {
|
|
1567
|
+
schema: CLOUDFLARE_OPS_SCHEMA,
|
|
1568
|
+
operation: command.kind,
|
|
1569
|
+
dryRun: false,
|
|
1570
|
+
execute: true,
|
|
1571
|
+
plan: redactPlanOutput(plan),
|
|
1572
|
+
stdout: safeStdout,
|
|
1573
|
+
stderr: safeStderr,
|
|
1574
|
+
tokenPrinted: false,
|
|
1575
|
+
claimBoundary: plan.claimBoundary,
|
|
1576
|
+
},
|
|
1577
|
+
};
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
if (command.kind === 'registrar.register') {
|
|
1581
|
+
const accountId = requireAccountId(command, env);
|
|
1582
|
+
const registrantContact = command.registrantContactJson == null ? null : loadRegistrantContactJson(command.registrantContactJson);
|
|
1583
|
+
const checkPlan = buildCloudflareRequestPlan({ operation: 'registrar.check', accountId, domain: command.domain });
|
|
1584
|
+
const registerPlan = buildCloudflareRequestPlan({ operation: 'registrar.register', accountId, domain: command.domain, registrantContact });
|
|
1585
|
+
if (!command.execute) {
|
|
1586
|
+
return {
|
|
1587
|
+
json: {
|
|
1588
|
+
schema: CLOUDFLARE_OPS_SCHEMA,
|
|
1589
|
+
operation: command.kind,
|
|
1590
|
+
dryRun: true,
|
|
1591
|
+
execute: false,
|
|
1592
|
+
safety: {
|
|
1593
|
+
billable: true,
|
|
1594
|
+
requiresExecute: true,
|
|
1595
|
+
requiresFreshAvailabilityCheckBeforeExecute: true,
|
|
1596
|
+
requiresExactDomainConfirmation: true,
|
|
1597
|
+
requiresExactPriceConfirmation: true,
|
|
1598
|
+
chargeAcknowledgementFlag: REGISTRATION_CHARGE_FLAG,
|
|
1599
|
+
},
|
|
1600
|
+
checkPlan: redactPlanOutput(checkPlan),
|
|
1601
|
+
registerPlan: redactPlanOutput(redactCloudflareRequestPlan(registerPlan)),
|
|
1602
|
+
claimBoundary: 'Plan only; no availability check or domain registration was executed.',
|
|
1603
|
+
},
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
const token = requireApiToken(env);
|
|
1608
|
+
const checkResponse = await cloudflareRequest(checkPlan, token, fetchImpl);
|
|
1609
|
+
const availability = firstCheckedDomain(checkResponse.payload, command.domain);
|
|
1610
|
+
const guard = assertRegistrationPriceGuard({ ...command, availability });
|
|
1611
|
+
const registerResponse = await cloudflareRequest(registerPlan, token, fetchImpl);
|
|
1612
|
+
const state = assertRegistrationWorkflowState(registerResponse.payload);
|
|
1613
|
+
const statusFollowup = state === 'in_progress'
|
|
1614
|
+
? await followInProgressRegistrationOnce({ accountId, domain: command.domain, token, fetchImpl })
|
|
1615
|
+
: null;
|
|
1616
|
+
return {
|
|
1617
|
+
json: {
|
|
1618
|
+
schema: CLOUDFLARE_OPS_SCHEMA,
|
|
1619
|
+
operation: command.kind,
|
|
1620
|
+
dryRun: false,
|
|
1621
|
+
execute: true,
|
|
1622
|
+
guard,
|
|
1623
|
+
check: { status: checkResponse.status, result: availability },
|
|
1624
|
+
registration: { status: registerResponse.status, state, payload: redactOperationalPayload(registerResponse.payload) },
|
|
1625
|
+
statusFollowup: statusFollowup === null ? null : { ...statusFollowup, payload: redactOperationalPayload(statusFollowup.payload) },
|
|
1626
|
+
},
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
const token = requireApiToken(env);
|
|
1631
|
+
const plan = apiPlanOutput(command, env);
|
|
1632
|
+
const response = await cloudflareRequest(plan, token, fetchImpl);
|
|
1633
|
+
return {
|
|
1634
|
+
json: {
|
|
1635
|
+
schema: CLOUDFLARE_OPS_SCHEMA,
|
|
1636
|
+
operation: command.kind,
|
|
1637
|
+
dryRun: false,
|
|
1638
|
+
execute: true,
|
|
1639
|
+
plan: redactPlanOutput(plan),
|
|
1640
|
+
response: redactOperationalPayload(response),
|
|
1641
|
+
},
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
export function cloudflareOpsHelpText() {
|
|
1646
|
+
return HELP_TEXT;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
async function main(argv = process.argv.slice(2), env = process.env) {
|
|
1650
|
+
try {
|
|
1651
|
+
const secretEnv = await applyCloudflareSecretEnvFileFromArgv(argv, env, { includePath: false });
|
|
1652
|
+
const command = parseCloudflareOpsCommand(secretEnv.argv, secretEnv.env);
|
|
1653
|
+
const result = await runCloudflareOpsCommand(command, { env: secretEnv.env });
|
|
1654
|
+
if (result.text !== undefined) {
|
|
1655
|
+
process.stdout.write(result.text);
|
|
1656
|
+
return 0;
|
|
1657
|
+
}
|
|
1658
|
+
await writeJsonOut(command.out, result.json);
|
|
1659
|
+
process.stdout.write(safeJson(result.json));
|
|
1660
|
+
return 0;
|
|
1661
|
+
} catch (error) {
|
|
1662
|
+
const rawMessage = error instanceof UsageError || error instanceof CloudflareSecretEnvError ? error.message : (error?.message ?? String(error));
|
|
1663
|
+
const message = redactOperationalText(rawMessage);
|
|
1664
|
+
process.stderr.write(`${message}\n`);
|
|
1665
|
+
if (error?.payload) process.stderr.write(safeJson({ cloudflare: redactOperationalPayload(error.payload) }));
|
|
1666
|
+
return 1;
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
1671
|
+
process.exitCode = await main();
|
|
1672
|
+
}
|