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,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export const AI_ORCHESTRATION_PLAN_SCHEMA = 'enigma.ai_orchestration_plan.v1';
|
|
7
|
+
|
|
8
|
+
const SECRET_RE = /(?:Bearer\s+[A-Za-z0-9._~+/=-]{12,}|Basic\s+[A-Za-z0-9+/=-]{12,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|sk-[A-Za-z0-9_-]{16,}|cf-[A-Za-z0-9_-]{12,}|https?:\/\/[^\s/@]+:[^\s/@]+@|AKIA[0-9A-Z]{16}|\b[0-9a-f]{32}\b|(?:api[_-]?key|api[_-]?token|access[_-]?token|refresh[_-]?token|secret|password|private[_-]?key|token)\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{16,})/iu;
|
|
9
|
+
const LOCAL_PATH_RE = /(?<![A-Za-z])(?:[A-Za-z]:[\\/]|\\\\[^\\/]+[\\/][^\\/]+|\/(?:Users|home)\/[^\s"']+)/u;
|
|
10
|
+
|
|
11
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
12
|
+
const out = { statusBoard: null, out: null, help: false };
|
|
13
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
14
|
+
const token = argv[index];
|
|
15
|
+
const readValue = () => {
|
|
16
|
+
const value = argv[index + 1];
|
|
17
|
+
if (typeof value !== 'string' || value.length === 0 || value.startsWith('--')) throw new Error(`${token} requires a value`);
|
|
18
|
+
index += 1;
|
|
19
|
+
return value;
|
|
20
|
+
};
|
|
21
|
+
if (token === '--status-board' || token === '--statusBoard') out.statusBoard = readValue();
|
|
22
|
+
else if (token === '--out') out.out = readValue();
|
|
23
|
+
else if (token === '--help' || token === '-h') out.help = true;
|
|
24
|
+
else throw new Error(`Unknown AI orchestration option: ${token}`);
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function usage() {
|
|
30
|
+
return 'Usage: node scripts/build-ai-orchestration-plan.mjs --status-board <production-status-board.json> [--out <file>]\n\nBuilds a public-safe AI/operator orchestration plan from the current production status board.\n';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function readJson(path, label) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
36
|
+
} catch {
|
|
37
|
+
throw new Error(`Unable to read ${label}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function assertPublicSafe(value, label = 'AI orchestration plan') {
|
|
42
|
+
const text = JSON.stringify(value);
|
|
43
|
+
if (SECRET_RE.test(text)) throw new Error(`${label} contains secret-looking material`);
|
|
44
|
+
if (LOCAL_PATH_RE.test(text)) throw new Error(`${label} contains a local path`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function requireObject(value, label) {
|
|
48
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
49
|
+
assertPublicSafe(value, label);
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function compactCommands(values, limit = 4) {
|
|
54
|
+
const out = [];
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
for (const value of Array.isArray(values) ? values : []) {
|
|
57
|
+
if (typeof value !== 'string' || value.length === 0 || seen.has(value)) continue;
|
|
58
|
+
seen.add(value);
|
|
59
|
+
out.push(value);
|
|
60
|
+
if (out.length >= limit) break;
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function phaseById(statusBoard, id) {
|
|
66
|
+
const phases = Array.isArray(statusBoard.blocked_phases) ? statusBoard.blocked_phases : [];
|
|
67
|
+
return phases.find((phase) => phase.id === id) ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function groupByName(statusBoard, name) {
|
|
71
|
+
const groups = Array.isArray(statusBoard.blocked_groups) ? statusBoard.blocked_groups : [];
|
|
72
|
+
return groups.find((group) => group.name === name) ?? null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function blockersFor(item) {
|
|
76
|
+
return Array.isArray(item?.blockers) ? item.blockers.slice(0, 8) : [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function commandsFor(item) {
|
|
80
|
+
if (Array.isArray(item?.commands)) return compactCommands(item.commands, 5);
|
|
81
|
+
if (typeof item?.next_command === 'string') return [item.next_command];
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildLane({ id, role, model, charter, owns, waits_for = [], commands = [], blockers = [], acceptance = [] }) {
|
|
86
|
+
return {
|
|
87
|
+
id,
|
|
88
|
+
role,
|
|
89
|
+
model,
|
|
90
|
+
charter,
|
|
91
|
+
owns,
|
|
92
|
+
waits_for,
|
|
93
|
+
blocker_count: blockers.length,
|
|
94
|
+
blockers,
|
|
95
|
+
commands: compactCommands(commands, 6),
|
|
96
|
+
acceptance,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function buildRoleLanes(statusBoard) {
|
|
101
|
+
const credentialPhase = phaseById(statusBoard, 'cloudflare_credentials') ?? groupByName(statusBoard, 'cloudflare_credentials');
|
|
102
|
+
const workerPhase = phaseById(statusBoard, 'cloudflare_worker_permission') ?? groupByName(statusBoard, 'cloudflare_worker_permission');
|
|
103
|
+
const hostedPhase = phaseById(statusBoard, 'hosted_backend_refs') ?? groupByName(statusBoard, 'hosted_backend_live');
|
|
104
|
+
const operatorPhase = phaseById(statusBoard, 'operator_acceptance') ?? groupByName(statusBoard, 'operator_acceptance');
|
|
105
|
+
const finalPhase = phaseById(statusBoard, 'final_release_verification');
|
|
106
|
+
return [
|
|
107
|
+
buildLane({
|
|
108
|
+
id: 'human_operator_credentials',
|
|
109
|
+
role: 'Human operator',
|
|
110
|
+
model: 'human-controlled Cloudflare/account owner',
|
|
111
|
+
charter: 'Create or inject credentials out-of-band without pasting secrets into chat, then rerun credential and status evidence.',
|
|
112
|
+
owns: ['cloudflare_credentials'],
|
|
113
|
+
blockers: blockersFor(credentialPhase),
|
|
114
|
+
commands: commandsFor(credentialPhase),
|
|
115
|
+
acceptance: ['production:cloudflare-credentials reports credentials_present:true', 'No token values, account ids, local paths, or contact data appear in artifacts.'],
|
|
116
|
+
}),
|
|
117
|
+
buildLane({
|
|
118
|
+
id: 'gpt55_architecture',
|
|
119
|
+
role: 'Architecture and structure planner',
|
|
120
|
+
model: 'GPT-5.5',
|
|
121
|
+
charter: 'Keep the end-to-end production graph coherent, claim-bounded, and testable while external credentials and hosted refs are supplied.',
|
|
122
|
+
owns: ['execution_order', 'claim_boundaries', 'status_board', 'workplan'],
|
|
123
|
+
waits_for: ['human_operator_credentials'],
|
|
124
|
+
blockers: [...blockersFor(workerPhase), ...blockersFor(hostedPhase)].slice(0, 8),
|
|
125
|
+
commands: ['npm run production:status -- --goal-audit .enigma/goal-audit-current.json --dependencies .enigma/production-dependencies-current.json --workplan .enigma/production-workplan-current.json --out .enigma/production-status-board-current.json', 'npm run production:workplan -- --dependencies .enigma/production-dependencies-current.json --operator-acceptance .enigma/operator-acceptance-template-result.json --hosted-ref-catalog .enigma/operator-evidence-starter/hosted-ref-catalog.json --out .enigma/production-workplan-current.json'],
|
|
126
|
+
acceptance: ['Status board has fresh_input_evidence:true.', 'Workplan execution_order covers every phase and has no cycles.', 'No broad completion claim is emitted while external blockers remain.'],
|
|
127
|
+
}),
|
|
128
|
+
buildLane({
|
|
129
|
+
id: 'kimi_coding',
|
|
130
|
+
role: 'Fast implementation lane',
|
|
131
|
+
model: 'Kimi coding agent',
|
|
132
|
+
charter: 'Implement the exact next production code, validator, deploy-script, or artifact change selected by the architecture lane; no design drift or compatibility shims.',
|
|
133
|
+
owns: ['worker_permission_probe', 'hosted_backend_refs', 'operator_evidence_starter', 'validator_scripts'],
|
|
134
|
+
waits_for: ['gpt55_architecture', 'human_operator_credentials'],
|
|
135
|
+
blockers: [...blockersFor(workerPhase), ...blockersFor(hostedPhase)].slice(0, 8),
|
|
136
|
+
commands: [...commandsFor(workerPhase), ...commandsFor(hostedPhase)].slice(0, 6),
|
|
137
|
+
acceptance: ['Targeted tests for changed files pass.', 'Release audit optional gate for the changed artifact passes.', 'Generated artifacts remain public-safe.'],
|
|
138
|
+
}),
|
|
139
|
+
buildLane({
|
|
140
|
+
id: 'gpt55_review',
|
|
141
|
+
role: 'Production reviewer',
|
|
142
|
+
model: 'GPT-5.5 reviewer',
|
|
143
|
+
charter: 'Review Kimi-coded changes for security, completeness, claim boundaries, and operator usability before final gates are rerun.',
|
|
144
|
+
owns: ['security_review', 'release_gate_review', 'operator_acceptance_review'],
|
|
145
|
+
waits_for: ['kimi_coding'],
|
|
146
|
+
blockers: blockersFor(operatorPhase),
|
|
147
|
+
commands: commandsFor(operatorPhase),
|
|
148
|
+
acceptance: ['No secrets, raw memory, personal data, account IDs, or local paths in public artifacts.', 'Operator acceptance blocker breakdown is zero before go-live.', 'Review findings are resolved or explicitly represented as blockers.'],
|
|
149
|
+
}),
|
|
150
|
+
buildLane({
|
|
151
|
+
id: 'final_verification',
|
|
152
|
+
role: 'Release verification lane',
|
|
153
|
+
model: 'automation plus reviewer',
|
|
154
|
+
charter: 'Rerun all current gates and regenerate status artifacts after external blockers clear.',
|
|
155
|
+
owns: ['release_audit', 'goal_audit', 'dependency_report', 'status_board'],
|
|
156
|
+
waits_for: ['gpt55_review', 'human_operator_credentials'],
|
|
157
|
+
blockers: blockersFor(finalPhase),
|
|
158
|
+
commands: commandsFor(finalPhase),
|
|
159
|
+
acceptance: ['npm run check passes.', 'npm test passes.', 'npm run release:audit passes with required_failed:[]', 'production:status reports launch_ready:true only with fresh current inputs.'],
|
|
160
|
+
}),
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function buildWaves(statusBoard, lanes) {
|
|
165
|
+
const laneById = new Map(lanes.map((lane) => [lane.id, lane]));
|
|
166
|
+
return [
|
|
167
|
+
{
|
|
168
|
+
id: 'wave_1_external_access',
|
|
169
|
+
objective: 'Resolve external account and credential blockers without exposing secrets.',
|
|
170
|
+
lanes: ['human_operator_credentials'].filter((id) => laneById.has(id)),
|
|
171
|
+
acceptance: ['Cloudflare credentials are available only out-of-band and validated by production:cloudflare-credentials.'],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: 'wave_2_architecture_and_coding',
|
|
175
|
+
objective: 'Architect, implement, and verify the next production-hardening slice against current blockers.',
|
|
176
|
+
lanes: ['gpt55_architecture', 'kimi_coding'].filter((id) => laneById.has(id)),
|
|
177
|
+
acceptance: ['Changed scripts/tests/docs pass targeted checks before review.'],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'wave_3_review_and_acceptance',
|
|
181
|
+
objective: 'Review the implementation and complete operator acceptance evidence only with real hosted refs.',
|
|
182
|
+
lanes: ['gpt55_review'].filter((id) => laneById.has(id)),
|
|
183
|
+
acceptance: ['Operator acceptance remains blocked unless decision is go and blockers are zero.'],
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: 'wave_4_final_release',
|
|
187
|
+
objective: 'Rerun final gates and regenerate status after all external blockers clear.',
|
|
188
|
+
lanes: ['final_verification'].filter((id) => laneById.has(id)),
|
|
189
|
+
acceptance: ['production:status, production:dependencies, and goal audit all report launch_ready:true with fresh evidence.'],
|
|
190
|
+
},
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function buildAiOrchestrationPlan(inputs = {}, options = {}) {
|
|
195
|
+
const statusBoard = requireObject(inputs.statusBoard, 'production status board');
|
|
196
|
+
if (statusBoard.schema !== 'enigma.production_status_board.v1') throw new Error('production status board schema mismatch');
|
|
197
|
+
const lanes = buildRoleLanes(statusBoard);
|
|
198
|
+
const plan = {
|
|
199
|
+
schema: AI_ORCHESTRATION_PLAN_SCHEMA,
|
|
200
|
+
generated_at: options.generated_at ?? new Date().toISOString(),
|
|
201
|
+
status: statusBoard.launch_ready === true ? 'ready' : 'blocked',
|
|
202
|
+
launch_ready: statusBoard.launch_ready === true,
|
|
203
|
+
source_status_board_generated_at: statusBoard.generated_at ?? null,
|
|
204
|
+
source_status_next_phase_id: statusBoard.next_phase_id ?? null,
|
|
205
|
+
source_status_fresh_input_evidence: statusBoard.fresh_input_evidence === true,
|
|
206
|
+
next_phase_details: statusBoard.next_phase?.details ?? null,
|
|
207
|
+
orchestration_mode: 'human_secret_custody_plus_ai_architecture_coding_review',
|
|
208
|
+
role_lane_count: lanes.length,
|
|
209
|
+
wave_count: 4,
|
|
210
|
+
lanes,
|
|
211
|
+
waves: buildWaves(statusBoard, lanes),
|
|
212
|
+
non_delegable_controls: [
|
|
213
|
+
'Cloudflare token values, account ids, registrar purchases, payments, 2FA, CAPTCHAs, and legal approvals stay human-controlled unless explicitly and safely provided out-of-band.',
|
|
214
|
+
'No AI lane may mark the goal complete without fresh goal/dependency/workplan evidence and launch_ready:true.',
|
|
215
|
+
'No lane may paste secrets, personal information, raw memory, prompts, transcripts, provider responses, or private keys into public artifacts.',
|
|
216
|
+
],
|
|
217
|
+
claim_boundary: [
|
|
218
|
+
'This artifact orchestrates work from existing production status evidence; it does not invoke external AI systems, create accounts, deploy infrastructure, approve operators, or certify launch readiness.',
|
|
219
|
+
'Kimi/GPT role labels are execution lanes and review responsibilities, not proof that an external model has already performed the work.',
|
|
220
|
+
'Launch readiness remains false until status, dependency, goal, hosted, and operator evidence all prove readiness with fresh current inputs.',
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
assertPublicSafe(plan);
|
|
224
|
+
return plan;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
228
|
+
const args = parseArgs(argv);
|
|
229
|
+
if (args.help) return { text: usage(), status: 0 };
|
|
230
|
+
if (!args.statusBoard) throw new Error('--status-board is required');
|
|
231
|
+
const plan = buildAiOrchestrationPlan({ statusBoard: await readJson(args.statusBoard, 'production status board') });
|
|
232
|
+
const json = `${JSON.stringify(plan, null, 2)}\n`;
|
|
233
|
+
if (args.out) {
|
|
234
|
+
await mkdir(dirname(args.out), { recursive: true });
|
|
235
|
+
await writeFile(args.out, json, 'utf8');
|
|
236
|
+
}
|
|
237
|
+
return { text: json, status: plan.launch_ready ? 0 : 1 };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
241
|
+
main().then(({ text, status }) => {
|
|
242
|
+
process.stdout.write(text);
|
|
243
|
+
process.exitCode = status;
|
|
244
|
+
}).catch((error) => {
|
|
245
|
+
process.stdout.write(`${JSON.stringify({ schema: AI_ORCHESTRATION_PLAN_SCHEMA, status: 'blocked', launch_ready: false, blockers: [error.message] }, null, 2)}\n`);
|
|
246
|
+
process.exitCode = 1;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { lstat, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { resolve, relative, join, dirname, sep } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
import { buildWranglerPagesDeployPlan, CLOUDFLARE_OPS_SCHEMA } from './cloudflare-ops.mjs';
|
|
7
|
+
import { validatePublicSiteSecurity, PUBLIC_SITE_SECURITY_RESULT_SCHEMA } from './validate-public-site-security.mjs';
|
|
8
|
+
import { applyCloudflareSecretEnvFile } from './cloudflare-secret-env.mjs';
|
|
9
|
+
|
|
10
|
+
export const CLOUDFLARE_PAGES_RELEASE_PACKET_SCHEMA = 'enigma.cloudflare_pages_release_packet.v1';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_EXPECT_TITLE = 'Enigma';
|
|
13
|
+
const SECRET_OUTPUT_RE = /(?:Bearer\s+[A-Za-z0-9._~+/=-]{12,}|Basic\s+[A-Za-z0-9+/=-]{12,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|sk-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16})/iu;
|
|
14
|
+
const LOCAL_PATH_RE = /(?:(?:[A-Za-z]:[\\/][^\s"']*)|(?:\\\\[^\s"']+)|(?:\/(?:Users|home|tmp|var|private|mnt|Volumes)\/[^\s"']*))/iu;
|
|
15
|
+
const PUBLIC_SITE_PLACEHOLDER = '<public-site>';
|
|
16
|
+
const NODE_PLACEHOLDER = '<node>';
|
|
17
|
+
const NPM_CLI_PLACEHOLDER = '<npm-cli>';
|
|
18
|
+
|
|
19
|
+
class UsageError extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'UsageError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function requireString(name, value) {
|
|
27
|
+
if (typeof value !== 'string' || value.trim().length === 0) throw new UsageError(`${name} is required`);
|
|
28
|
+
return value.trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
const out = {
|
|
33
|
+
site: null,
|
|
34
|
+
projectName: null,
|
|
35
|
+
domain: null,
|
|
36
|
+
liveUrl: null,
|
|
37
|
+
expectTitle: DEFAULT_EXPECT_TITLE,
|
|
38
|
+
out: null,
|
|
39
|
+
envFile: null,
|
|
40
|
+
};
|
|
41
|
+
for (let index = 0; index < argv.length;) {
|
|
42
|
+
const token = argv[index];
|
|
43
|
+
if (token === '--help') return { help: true };
|
|
44
|
+
const readValue = (name) => {
|
|
45
|
+
if (index + 1 >= argv.length || argv[index + 1].startsWith('--')) throw new UsageError(`${name} requires a value`);
|
|
46
|
+
const value = argv[index + 1];
|
|
47
|
+
index += 2;
|
|
48
|
+
return value;
|
|
49
|
+
};
|
|
50
|
+
if (token === '--site') out.site = readValue(token);
|
|
51
|
+
else if (token === '--project-name') out.projectName = readValue(token);
|
|
52
|
+
else if (token === '--domain') out.domain = readValue(token);
|
|
53
|
+
else if (token === '--live-url') out.liveUrl = readValue(token);
|
|
54
|
+
else if (token === '--expect-title') out.expectTitle = readValue(token);
|
|
55
|
+
else if (token === '--cloudflare-env-file') out.envFile = readValue(token);
|
|
56
|
+
else if (token === '--out') out.out = readValue(token);
|
|
57
|
+
else throw new UsageError(`unknown option ${token}`);
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function usage() {
|
|
63
|
+
return `Usage: node scripts/build-cloudflare-pages-release-packet.mjs --site <dir> --project-name <name> [options]\n\nOptions:\n --domain <host> Public domain expected for deployment evidence.\n --live-url <https-url> Optional non-mutating fetch of the currently live page.\n --expect-title <text> Expected text in the live page title. Default: ${DEFAULT_EXPECT_TITLE}.\n --cloudflare-env-file <path> Optional local .env-style Cloudflare secret file; values are loaded but never printed.\n --out <file> Write packet JSON to a file and print the same JSON.\n\nThe packet is public-safe: it contains file hashes, counts, dry-run deploy command metadata,\nsecurity validation results, and optional live-page title observation. It never prints tokens or raw Cloudflare credential values.`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeRelativePath(root, absolutePath) {
|
|
67
|
+
return relative(root, absolutePath).split(sep).join('/');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function collectStaticFiles(root, current = root, acc = []) {
|
|
71
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
72
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
const absolute = join(current, entry.name);
|
|
75
|
+
const stat = await lstat(absolute);
|
|
76
|
+
if (stat.isSymbolicLink()) {
|
|
77
|
+
acc.push({ rel: normalizeRelativePath(root, absolute), symlink: true });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (entry.isDirectory()) {
|
|
81
|
+
await collectStaticFiles(root, absolute, acc);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!entry.isFile()) continue;
|
|
85
|
+
const bytes = await readFile(absolute);
|
|
86
|
+
acc.push({
|
|
87
|
+
rel: normalizeRelativePath(root, absolute),
|
|
88
|
+
bytes: bytes.length,
|
|
89
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return acc;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function artifactRootHash(files) {
|
|
96
|
+
const hash = createHash('sha256');
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
hash.update(file.rel);
|
|
99
|
+
hash.update('\0');
|
|
100
|
+
hash.update(String(file.bytes ?? 0));
|
|
101
|
+
hash.update('\0');
|
|
102
|
+
hash.update(file.sha256 ?? 'symlink');
|
|
103
|
+
hash.update('\n');
|
|
104
|
+
}
|
|
105
|
+
return `sha256:${hash.digest('hex')}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function extractTitle(html) {
|
|
109
|
+
const match = /<title[^>]*>([\s\S]*?)<\/title>/iu.exec(html);
|
|
110
|
+
if (!match) return null;
|
|
111
|
+
return match[1].replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizeHost(value) {
|
|
115
|
+
const text = String(value ?? '').trim().toLowerCase().replace(/\.$/, '');
|
|
116
|
+
return text.length === 0 ? null : text;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function observeLivePage({ liveUrl, domain, expectTitle, fetchImpl }) {
|
|
120
|
+
if (typeof liveUrl !== 'string' || liveUrl.trim().length === 0) return null;
|
|
121
|
+
const blockers = [];
|
|
122
|
+
let url;
|
|
123
|
+
try {
|
|
124
|
+
url = new URL(liveUrl);
|
|
125
|
+
} catch {
|
|
126
|
+
return { ok: false, live_url: null, blockers: ['--live-url is not a valid URL'] };
|
|
127
|
+
}
|
|
128
|
+
if (url.protocol !== 'https:') blockers.push('live URL must be HTTPS');
|
|
129
|
+
if (url.username || url.password) blockers.push('live URL must not contain userinfo');
|
|
130
|
+
const expectedDomain = normalizeHost(domain);
|
|
131
|
+
if (expectedDomain !== null && normalizeHost(url.hostname) !== expectedDomain) blockers.push('live URL host does not match --domain');
|
|
132
|
+
if (blockers.length > 0) return { ok: false, live_url: `${url.origin}${url.pathname}`, blockers };
|
|
133
|
+
|
|
134
|
+
const response = await fetchImpl(url.toString(), { method: 'GET', headers: { accept: 'text/html,application/xhtml+xml' } });
|
|
135
|
+
const body = await response.text();
|
|
136
|
+
if (SECRET_OUTPUT_RE.test(body)) blockers.push('live page response contains secret-looking text');
|
|
137
|
+
const title = extractTitle(body);
|
|
138
|
+
const status = Number(response.status ?? 0);
|
|
139
|
+
const contentType = typeof response.headers?.get === 'function' ? String(response.headers.get('content-type') ?? '') : '';
|
|
140
|
+
const titleMatched = typeof title === 'string' && title.toLowerCase().includes(String(expectTitle).toLowerCase());
|
|
141
|
+
if (status < 200 || status > 299) blockers.push(`live URL returned HTTP ${Number.isFinite(status) ? status : 'unknown'}`);
|
|
142
|
+
if (!/html/i.test(contentType) && !/^\s*<!doctype html|^\s*<html/i.test(body)) blockers.push('live URL did not return HTML-ish content');
|
|
143
|
+
if (!titleMatched) blockers.push(`live title did not include ${JSON.stringify(expectTitle)}`);
|
|
144
|
+
return {
|
|
145
|
+
ok: blockers.length === 0,
|
|
146
|
+
live_url: `${url.origin}${url.pathname}`,
|
|
147
|
+
status: Number.isFinite(status) ? status : null,
|
|
148
|
+
content_type: contentType || null,
|
|
149
|
+
title,
|
|
150
|
+
title_matched: titleMatched,
|
|
151
|
+
body_bytes_observed: Buffer.byteLength(body),
|
|
152
|
+
authorization_header_sent: false,
|
|
153
|
+
blockers,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function redactLocalPath(value, { site }) {
|
|
158
|
+
if (typeof value !== 'string') return value;
|
|
159
|
+
if (value === site) return PUBLIC_SITE_PLACEHOLDER;
|
|
160
|
+
if (!LOCAL_PATH_RE.test(value)) return value;
|
|
161
|
+
if (/[\\/]npm-cli\.js$/iu.test(value)) return NPM_CLI_PLACEHOLDER;
|
|
162
|
+
if (/[\\/]node(?:\.exe)?$/iu.test(value)) return NODE_PLACEHOLDER;
|
|
163
|
+
return '<local-path>';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function publicSiteIssuePath(value, { site }) {
|
|
167
|
+
if (typeof value !== 'string' || value.length === 0) return PUBLIC_SITE_PLACEHOLDER;
|
|
168
|
+
const normalized = value.replaceAll('\\', '/');
|
|
169
|
+
const normalizedSite = site.replaceAll('\\', '/');
|
|
170
|
+
if (normalized === normalizedSite) return PUBLIC_SITE_PLACEHOLDER;
|
|
171
|
+
if (normalized.startsWith(`${normalizedSite}/`)) return `${PUBLIC_SITE_PLACEHOLDER}/${normalized.slice(normalizedSite.length + 1)}`;
|
|
172
|
+
return redactLocalPath(value, { site });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function formatSecurityBlocker(entry, { site }) {
|
|
176
|
+
const safePath = publicSiteIssuePath(entry?.path, { site });
|
|
177
|
+
const message = redactCliMessage(entry?.message ?? 'security blocker');
|
|
178
|
+
return `${safePath}: ${message}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function publicDeployPlan(deployPlan, { site }) {
|
|
182
|
+
return {
|
|
183
|
+
schema: CLOUDFLARE_OPS_SCHEMA,
|
|
184
|
+
operation: deployPlan.operation,
|
|
185
|
+
dryRun: deployPlan.dryRun,
|
|
186
|
+
execute: deployPlan.execute,
|
|
187
|
+
command: redactLocalPath(deployPlan.command, { site }),
|
|
188
|
+
args: Array.isArray(deployPlan.args) ? deployPlan.args.map((arg) => redactLocalPath(arg, { site })) : [],
|
|
189
|
+
usesShell: deployPlan.usesShell,
|
|
190
|
+
destructive: deployPlan.destructive,
|
|
191
|
+
tokenPrinted: deployPlan.tokenPrinted,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
export async function buildCloudflarePagesReleasePacket(input = {}, options = {}) {
|
|
197
|
+
const env = options.env ?? process.env;
|
|
198
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
199
|
+
const generatedAt = options.generated_at ?? new Date().toISOString();
|
|
200
|
+
const site = resolve(requireString('--site', input.site));
|
|
201
|
+
const projectName = requireString('--project-name', input.projectName);
|
|
202
|
+
const domain = typeof input.domain === 'string' && input.domain.trim().length > 0 ? input.domain.trim().toLowerCase() : null;
|
|
203
|
+
const expectTitle = typeof input.expectTitle === 'string' && input.expectTitle.trim().length > 0 ? input.expectTitle.trim() : DEFAULT_EXPECT_TITLE;
|
|
204
|
+
const deployPlan = buildWranglerPagesDeployPlan({ site, projectName, execute: false });
|
|
205
|
+
const security = await validatePublicSiteSecurity({ site }, { generated_at: generatedAt });
|
|
206
|
+
const files = await collectStaticFiles(site);
|
|
207
|
+
const symlinks = files.filter((file) => file.symlink).map((file) => file.rel);
|
|
208
|
+
const index = files.find((file) => file.rel === 'index.html') ?? null;
|
|
209
|
+
const headers = files.find((file) => file.rel === '_headers') ?? null;
|
|
210
|
+
const rootHash = artifactRootHash(files);
|
|
211
|
+
const live = await observeLivePage({ liveUrl: input.liveUrl, domain, expectTitle, fetchImpl });
|
|
212
|
+
const localBlockers = [];
|
|
213
|
+
if (symlinks.length > 0) localBlockers.push('site artifact contains symlinks');
|
|
214
|
+
if (index === null) localBlockers.push('site artifact is missing index.html');
|
|
215
|
+
if (headers === null) localBlockers.push('site artifact is missing _headers');
|
|
216
|
+
if (security.ok !== true) localBlockers.push(...security.blockers.map((entry) => formatSecurityBlocker(entry, { site })));
|
|
217
|
+
const deploymentBlockers = [];
|
|
218
|
+
if (typeof env.CLOUDFLARE_API_TOKEN !== 'string' || env.CLOUDFLARE_API_TOKEN.length === 0) deploymentBlockers.push('CLOUDFLARE_API_TOKEN is absent; automated Pages deploy cannot execute from this environment');
|
|
219
|
+
if (live !== null && live.ok !== true) deploymentBlockers.push(...live.blockers.map((entry) => `live: ${entry}`));
|
|
220
|
+
return {
|
|
221
|
+
schema: CLOUDFLARE_PAGES_RELEASE_PACKET_SCHEMA,
|
|
222
|
+
generated_at: generatedAt,
|
|
223
|
+
site: PUBLIC_SITE_PLACEHOLDER,
|
|
224
|
+
project_name: projectName,
|
|
225
|
+
domain,
|
|
226
|
+
local_artifact_ready: localBlockers.length === 0,
|
|
227
|
+
automated_deploy_ready: localBlockers.length === 0 && deploymentBlockers.length === 0,
|
|
228
|
+
credential_present: typeof env.CLOUDFLARE_API_TOKEN === 'string' && env.CLOUDFLARE_API_TOKEN.length > 0,
|
|
229
|
+
deploy_plan: publicDeployPlan(deployPlan, { site }),
|
|
230
|
+
artifact: {
|
|
231
|
+
file_count: files.length,
|
|
232
|
+
byte_count: files.reduce((sum, file) => sum + (file.bytes ?? 0), 0),
|
|
233
|
+
root_hash: rootHash,
|
|
234
|
+
index_sha256: index?.sha256 ?? null,
|
|
235
|
+
headers_sha256: headers?.sha256 ?? null,
|
|
236
|
+
symlink_count: symlinks.length,
|
|
237
|
+
},
|
|
238
|
+
security: {
|
|
239
|
+
schema: PUBLIC_SITE_SECURITY_RESULT_SCHEMA,
|
|
240
|
+
ok: security.ok === true,
|
|
241
|
+
status: security.status,
|
|
242
|
+
blocker_count: security.blockers.length,
|
|
243
|
+
checked: security.checked,
|
|
244
|
+
},
|
|
245
|
+
live_observation: live,
|
|
246
|
+
blockers: localBlockers,
|
|
247
|
+
deployment_blockers: deploymentBlockers,
|
|
248
|
+
claim_boundary: [
|
|
249
|
+
'This packet validates a local Cloudflare Pages static artifact and dry-run deploy plan only.',
|
|
250
|
+
'It does not prove a Pages deployment occurred unless the deploy command is executed with credentials and re-verified afterward.',
|
|
251
|
+
'It does not prove hosted relay, gateway, durable storage, KMS, SIEM, backup, operator acceptance, token launch, or customer BYOC readiness.',
|
|
252
|
+
],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function redactCliMessage(value) {
|
|
257
|
+
return String(value)
|
|
258
|
+
.replace(SECRET_OUTPUT_RE, '[redacted]')
|
|
259
|
+
.replace(LOCAL_PATH_RE, '<local-path>');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
263
|
+
const parsed = parseArgs(argv);
|
|
264
|
+
if (parsed.help) return { text: usage() };
|
|
265
|
+
const secretEnv = await applyCloudflareSecretEnvFile(options.env ?? process.env, { path: parsed.envFile ?? undefined, includePath: false });
|
|
266
|
+
const packet = await buildCloudflarePagesReleasePacket(parsed, { ...options, env: secretEnv.env });
|
|
267
|
+
const json = `${JSON.stringify(packet, null, 2)}\n`;
|
|
268
|
+
if (SECRET_OUTPUT_RE.test(json)) throw new Error('release packet output appears to contain a secret');
|
|
269
|
+
if (parsed.out) {
|
|
270
|
+
const outPath = resolve(parsed.out);
|
|
271
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
272
|
+
await writeFile(outPath, json, 'utf8');
|
|
273
|
+
}
|
|
274
|
+
return { json: packet };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
278
|
+
try {
|
|
279
|
+
const result = await runCli();
|
|
280
|
+
if (result.text) process.stdout.write(result.text);
|
|
281
|
+
else process.stdout.write(`${JSON.stringify(result.json, null, 2)}\n`);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
const message = redactCliMessage(error instanceof UsageError || error instanceof Error ? error.message : String(error));
|
|
284
|
+
process.stdout.write(`${JSON.stringify({ schema: CLOUDFLARE_PAGES_RELEASE_PACKET_SCHEMA, ok: false, error: { code: error instanceof UsageError ? 'USAGE_ERROR' : 'PACKET_ERROR', message } }, null, 2)}\n`);
|
|
285
|
+
process.exitCode = error instanceof UsageError ? 2 : 1;
|
|
286
|
+
}
|
|
287
|
+
}
|