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,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { buildCloudflareRequestPlan } from './cloudflare-ops.mjs';
|
|
6
|
+
|
|
7
|
+
export const CLOUDFLARE_TOKEN_POLICY_SCHEMA = 'enigma.cloudflare_token_policy.v1';
|
|
8
|
+
|
|
9
|
+
const SECRET_OUTPUT_RE = /(?:Bearer\s+[A-Za-z0-9._~+/=-]{12,}|Basic\s+[A-Za-z0-9+/=-]{12,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|sk-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16}|cf-[A-Za-z0-9_-]{12,})/iu;
|
|
10
|
+
const MODES = Object.freeze(['pages-deploy', 'pages-observe', 'domain-registrar', 'hosted-probe', 'all']);
|
|
11
|
+
|
|
12
|
+
class UsageError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'UsageError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function requireText(name, value) {
|
|
20
|
+
if (typeof value !== 'string' || value.trim().length === 0) throw new UsageError(`${name} is required`);
|
|
21
|
+
return value.trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function optionalText(value, fallback = null) {
|
|
25
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : fallback;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const out = {
|
|
30
|
+
mode: 'pages-deploy',
|
|
31
|
+
accountId: '<cloudflare-account-id>',
|
|
32
|
+
projectName: 'enigma-memory',
|
|
33
|
+
domain: 'enigmamemory.com',
|
|
34
|
+
out: null,
|
|
35
|
+
};
|
|
36
|
+
for (let index = 0; index < argv.length;) {
|
|
37
|
+
const token = argv[index];
|
|
38
|
+
if (token === '--help') return { help: true };
|
|
39
|
+
const readValue = (name) => {
|
|
40
|
+
if (index + 1 >= argv.length || argv[index + 1].startsWith('--')) throw new UsageError(`${name} requires a value`);
|
|
41
|
+
const value = argv[index + 1];
|
|
42
|
+
index += 2;
|
|
43
|
+
return value;
|
|
44
|
+
};
|
|
45
|
+
if (token === '--mode') out.mode = readValue(token);
|
|
46
|
+
else if (token === '--account-id') out.accountId = readValue(token);
|
|
47
|
+
else if (token === '--project-name') out.projectName = readValue(token);
|
|
48
|
+
else if (token === '--domain') out.domain = readValue(token);
|
|
49
|
+
else if (token === '--out') out.out = readValue(token);
|
|
50
|
+
else throw new UsageError(`unknown option ${token}`);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function usage() {
|
|
56
|
+
return `Usage: node scripts/build-cloudflare-token-policy.mjs [options]\n\nOptions:\n --mode <pages-deploy|pages-observe|domain-registrar|hosted-probe|all> Default: pages-deploy.\n --account-id <id> Account scope placeholder/default.\n --project-name <name> Pages project. Default: enigma-memory.\n --domain <host> Domain. Default: enigmamemory.com.\n --out <file> Write JSON and print the same JSON.\n\nThis prints a public-safe API-token policy packet for the Cloudflare dashboard.\nIt lists intended permission groups and endpoints only; it never creates or prints a token.\n`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function permission(id, product, permission_name, access, resource, reason) {
|
|
60
|
+
return { id, product, permission_name, access, resource, reason, operator_must_map_to_permission_group_id: true };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function includeMode(mode, candidate) {
|
|
64
|
+
return mode === 'all' || mode === candidate;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function plannedApiCalls({ mode, accountId, projectName, domain }) {
|
|
68
|
+
const calls = [];
|
|
69
|
+
calls.push(buildCloudflareRequestPlan({ operation: 'token.verify', accountId }));
|
|
70
|
+
if (includeMode(mode, 'pages-observe') || includeMode(mode, 'pages-deploy')) {
|
|
71
|
+
calls.push(buildCloudflareRequestPlan({ operation: 'pages.project', accountId, projectName }));
|
|
72
|
+
}
|
|
73
|
+
if (includeMode(mode, 'domain-registrar')) {
|
|
74
|
+
calls.push(buildCloudflareRequestPlan({ operation: 'registrar.search', accountId, query: domain, limit: 5 }));
|
|
75
|
+
calls.push(buildCloudflareRequestPlan({ operation: 'registrar.check', accountId, domain }));
|
|
76
|
+
calls.push(buildCloudflareRequestPlan({ operation: 'registrar.register', accountId, domain }));
|
|
77
|
+
calls.push(buildCloudflareRequestPlan({ operation: 'registrar.registration-status', accountId, domain }));
|
|
78
|
+
}
|
|
79
|
+
if (includeMode(mode, 'hosted-probe')) {
|
|
80
|
+
for (const service of ['enigma-hosted-probe', 'enigma-relay', 'enigma-gateway']) {
|
|
81
|
+
calls.push({
|
|
82
|
+
operation: 'workers.service.upsert',
|
|
83
|
+
method: 'PUT',
|
|
84
|
+
path: `/client/v4/accounts/${accountId}/workers/services/${service}`,
|
|
85
|
+
requires_token: true,
|
|
86
|
+
token_printed: false,
|
|
87
|
+
billable: false,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
calls.push({
|
|
91
|
+
operation: 'workers.custom-domains.attach',
|
|
92
|
+
method: 'PUT',
|
|
93
|
+
path: `/client/v4/accounts/${accountId}/workers/domains`,
|
|
94
|
+
requires_token: true,
|
|
95
|
+
token_printed: false,
|
|
96
|
+
billable: false,
|
|
97
|
+
});
|
|
98
|
+
calls.push({
|
|
99
|
+
operation: 'workers.custom-domains.list',
|
|
100
|
+
method: 'GET',
|
|
101
|
+
path: `/client/v4/accounts/${accountId}/workers/domains`,
|
|
102
|
+
requires_token: true,
|
|
103
|
+
token_printed: false,
|
|
104
|
+
billable: false,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return calls.map((call) => ({
|
|
108
|
+
operation: call.operation,
|
|
109
|
+
method: call.method,
|
|
110
|
+
path: call.path ?? new URL(call.url).pathname,
|
|
111
|
+
requires_token: call.requires_token ?? call.requiresToken === true,
|
|
112
|
+
token_printed: call.token_printed ?? call.tokenPrinted === true,
|
|
113
|
+
billable: call.billable === true,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function permissionPolicy({ mode, accountId, projectName, domain }) {
|
|
118
|
+
const permissions = [
|
|
119
|
+
permission('token_verify', 'User/API Tokens', 'API Tokens Read', 'read', 'own token', 'Verify the token without exposing its value.'),
|
|
120
|
+
];
|
|
121
|
+
if (includeMode(mode, 'pages-observe')) {
|
|
122
|
+
permissions.push(
|
|
123
|
+
permission('account_read', 'Account', 'Account Settings Read', 'read', `account:${accountId}`, 'Confirm account/project ownership.'),
|
|
124
|
+
permission('pages_read', 'Cloudflare Pages', 'Cloudflare Pages Read', 'read', `account:${accountId}/pages/${projectName}`, 'Observe Pages project and custom-domain state.')
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (includeMode(mode, 'pages-deploy')) {
|
|
128
|
+
permissions.push(
|
|
129
|
+
permission('account_read', 'Account', 'Account Settings Read', 'read', `account:${accountId}`, 'Confirm account/project ownership.'),
|
|
130
|
+
permission('pages_edit', 'Cloudflare Pages', 'Cloudflare Pages Edit', 'edit', `account:${accountId}/pages/${projectName}`, 'Deploy the preflighted static artifact to Pages.')
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (includeMode(mode, 'domain-registrar')) {
|
|
134
|
+
permissions.push(
|
|
135
|
+
permission('registrar_read', 'Registrar', 'Registrar Read', 'read', `account:${accountId}/registrar/${domain}`, 'Search/check domain and read registration status.'),
|
|
136
|
+
permission('registrar_edit', 'Registrar', 'Registrar Edit', 'edit', `account:${accountId}/registrar/${domain}`, 'Register the exact confirmed domain after explicit charge acknowledgement.')
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (includeMode(mode, 'hosted-probe')) {
|
|
140
|
+
permissions.push(
|
|
141
|
+
permission('workers_scripts_read', 'Workers Scripts', 'Workers Scripts Read', 'read', `account:${accountId}/workers/enigma-hosted-probe,enigma-relay,enigma-gateway`, 'Observe hosted probe and relay/gateway Worker services before/after deploy.'),
|
|
142
|
+
permission('workers_scripts_edit', 'Workers Scripts', 'Workers Scripts Edit', 'edit', `account:${accountId}/workers/enigma-hosted-probe,enigma-relay,enigma-gateway`, 'Deploy the hosted probe plus relay/gateway /livez and fail-closed /readyz Workers.'),
|
|
143
|
+
permission('workers_routes_edit', 'Workers Routes', 'Workers Routes Edit', 'edit', `zone:${domain}/workers/custom-domains`, 'Attach relay.enigmamemory.com and gateway.enigmamemory.com as Workers Custom Domains without printing token values.')
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const unique = new Map();
|
|
147
|
+
for (const item of permissions) unique.set(`${item.permission_name}:${item.access}:${item.resource}`, item);
|
|
148
|
+
return [...unique.values()];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function environmentContract({ mode }) {
|
|
152
|
+
const env = [
|
|
153
|
+
{ name: 'CLOUDFLARE_API_TOKEN', secret: true, required: true, handling: 'Store in a secret manager or process environment only; never paste into chat, docs, or repo files.' },
|
|
154
|
+
{ name: 'CLOUDFLARE_ACCOUNT_ID', secret: false, required: true, handling: 'Account id is not a token but still belongs in operator config, not public marketing collateral.' },
|
|
155
|
+
];
|
|
156
|
+
if (includeMode(mode, 'domain-registrar')) {
|
|
157
|
+
env.push({ name: 'REGISTRANT_CONTACT_JSON', secret: true, required: 'only for registration execution', handling: 'One-use local file; never commit. Command output redacts contact body fields.' });
|
|
158
|
+
}
|
|
159
|
+
return env;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function buildCloudflareTokenPolicy(input = {}) {
|
|
163
|
+
const mode = requireText('--mode', input.mode ?? 'pages-deploy');
|
|
164
|
+
if (!MODES.includes(mode)) throw new UsageError(`--mode must be one of ${MODES.join(', ')}`);
|
|
165
|
+
const accountId = optionalText(input.accountId, '<cloudflare-account-id>');
|
|
166
|
+
const projectName = optionalText(input.projectName, 'enigma-memory');
|
|
167
|
+
const domain = optionalText(input.domain, 'enigmamemory.com');
|
|
168
|
+
const verificationCommands = [
|
|
169
|
+
'npm run cloudflare:ops -- token verify --account-id <account-id>',
|
|
170
|
+
'npm run cloudflare:pages:packet -- --site ../github-upload/enigma-memory-site/_public_site --project-name enigma-memory --domain enigmamemory.com --live-url https://enigmamemory.com/ --expect-title Enigma',
|
|
171
|
+
'npm run production:handoff -- --site ../github-upload/enigma-memory-site/_public_site --project-name enigma-memory --domain enigmamemory.com --live-url https://enigmamemory.com/ --expect-title Enigma',
|
|
172
|
+
];
|
|
173
|
+
if (includeMode(mode, 'hosted-probe')) {
|
|
174
|
+
verificationCommands.splice(1, 0, 'npm run cloudflare:ops -- workers inspect-probe --name enigma-hosted-probe');
|
|
175
|
+
verificationCommands.splice(2, 0, 'npm run production:edge-backend -- --out-dir .enigma/edge-backend-workers --domain enigmamemory.com');
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
schema: CLOUDFLARE_TOKEN_POLICY_SCHEMA,
|
|
179
|
+
generated_at: input.generated_at ?? input.generatedAt ?? new Date().toISOString(),
|
|
180
|
+
mode,
|
|
181
|
+
target: {
|
|
182
|
+
account_id: accountId,
|
|
183
|
+
pages_project: projectName,
|
|
184
|
+
domain,
|
|
185
|
+
},
|
|
186
|
+
credential_contract: environmentContract({ mode }),
|
|
187
|
+
permission_groups: permissionPolicy({ mode, accountId, projectName, domain }),
|
|
188
|
+
planned_api_calls: plannedApiCalls({ mode, accountId, projectName, domain }),
|
|
189
|
+
mutation_boundaries: {
|
|
190
|
+
pages_deploy_requires_execute: true,
|
|
191
|
+
registrar_register_requires_execute: true,
|
|
192
|
+
workers_deploy_probe_requires_execute: true,
|
|
193
|
+
registrar_register_requires_charge_acknowledgement: true,
|
|
194
|
+
registrar_register_requires_fresh_price_check: true,
|
|
195
|
+
token_value_printed: false,
|
|
196
|
+
personal_contact_printed: false,
|
|
197
|
+
},
|
|
198
|
+
dashboard_steps: [
|
|
199
|
+
'Cloudflare Dashboard -> My Profile -> API Tokens -> Create Token -> Custom token.',
|
|
200
|
+
'Set account scope to the target account only; do not grant all accounts unless an operator explicitly approves it.',
|
|
201
|
+
'Map each listed permission name to Cloudflare permission-group IDs shown by the dashboard or /user/tokens/permission_groups.',
|
|
202
|
+
'Copy the token directly into a secret manager or process environment as CLOUDFLARE_API_TOKEN; do not paste it into chat.',
|
|
203
|
+
'Run token verify, then the dry-run packet, then execute deploy only after local site preflight/security gates pass.',
|
|
204
|
+
],
|
|
205
|
+
verification_commands: verificationCommands,
|
|
206
|
+
claim_boundary: [
|
|
207
|
+
'This packet is a least-privilege Cloudflare token policy, not an API token and not proof a token exists.',
|
|
208
|
+
'Permission display names must be mapped to Cloudflare permission_group IDs in the logged-in dashboard or API response.',
|
|
209
|
+
'Pages deployment and domain registration remain dry-run unless --execute and all explicit safety confirmations are supplied.',
|
|
210
|
+
'Worker probe deployment remains dry-run unless --execute is supplied; token policy generation never deploys a Worker.',
|
|
211
|
+
],
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
216
|
+
const parsed = parseArgs(argv);
|
|
217
|
+
if (parsed.help) return { text: usage() };
|
|
218
|
+
const packet = buildCloudflareTokenPolicy({ ...parsed, generated_at: options.generated_at ?? options.generatedAt });
|
|
219
|
+
const json = `${JSON.stringify(packet, null, 2)}\n`;
|
|
220
|
+
if (SECRET_OUTPUT_RE.test(json)) throw new Error('Cloudflare token policy output appears to contain a secret');
|
|
221
|
+
if (parsed.out) {
|
|
222
|
+
const outPath = resolve(parsed.out);
|
|
223
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
224
|
+
await writeFile(outPath, json, 'utf8');
|
|
225
|
+
}
|
|
226
|
+
return { json: packet };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
230
|
+
try {
|
|
231
|
+
const result = await runCli();
|
|
232
|
+
if (result.text) process.stdout.write(result.text);
|
|
233
|
+
else process.stdout.write(`${JSON.stringify(result.json, null, 2)}\n`);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
236
|
+
process.stdout.write(`${JSON.stringify({ schema: CLOUDFLARE_TOKEN_POLICY_SCHEMA, ok: false, error: { code: error instanceof UsageError ? 'USAGE_ERROR' : 'TOKEN_POLICY_ERROR', message } }, null, 2)}\n`);
|
|
237
|
+
process.exitCode = error instanceof UsageError ? 2 : 1;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export const CLOUDFLARE_TOKEN_REQUEST_SCHEMA = 'enigma.cloudflare_token_request.v1';
|
|
7
|
+
|
|
8
|
+
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}|cf-[A-Za-z0-9_-]{12,})/iu;
|
|
9
|
+
const MODES = Object.freeze(['pages-deploy', 'pages-observe', 'domain-registrar', 'hosted-probe', 'all']);
|
|
10
|
+
const ACCOUNT_SCOPE = 'com.cloudflare.api.account';
|
|
11
|
+
const ZONE_SCOPE = 'com.cloudflare.api.account.zone';
|
|
12
|
+
const USER_SCOPE = 'com.cloudflare.api.user';
|
|
13
|
+
|
|
14
|
+
class UsageError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'UsageError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
22
|
+
const out = {
|
|
23
|
+
mode: 'pages-deploy',
|
|
24
|
+
accountId: null,
|
|
25
|
+
userId: null,
|
|
26
|
+
permissionGroups: null,
|
|
27
|
+
projectName: 'enigma-memory',
|
|
28
|
+
domain: 'enigmamemory.com',
|
|
29
|
+
tokenName: 'enigma-memory-pages-deploy',
|
|
30
|
+
out: null,
|
|
31
|
+
};
|
|
32
|
+
for (let index = 0; index < argv.length;) {
|
|
33
|
+
const token = argv[index];
|
|
34
|
+
if (token === '--help') return { help: true };
|
|
35
|
+
const readValue = (name) => {
|
|
36
|
+
if (index + 1 >= argv.length || argv[index + 1].startsWith('--')) throw new UsageError(`${name} requires a value`);
|
|
37
|
+
const value = argv[index + 1];
|
|
38
|
+
index += 2;
|
|
39
|
+
return value;
|
|
40
|
+
};
|
|
41
|
+
if (token === '--mode') out.mode = readValue(token);
|
|
42
|
+
else if (token === '--account-id') out.accountId = readValue(token);
|
|
43
|
+
else if (token === '--user-id') out.userId = readValue(token);
|
|
44
|
+
else if (token === '--permission-groups') out.permissionGroups = readValue(token);
|
|
45
|
+
else if (token === '--project-name') out.projectName = readValue(token);
|
|
46
|
+
else if (token === '--domain') out.domain = readValue(token);
|
|
47
|
+
else if (token === '--token-name') out.tokenName = readValue(token);
|
|
48
|
+
else if (token === '--out') out.out = readValue(token);
|
|
49
|
+
else throw new UsageError(`unknown option ${token}`);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function usage() {
|
|
55
|
+
return `Usage: node scripts/build-cloudflare-token-request.mjs --permission-groups <json> --account-id <id> [options]\n\nOptions:\n --mode <pages-deploy|pages-observe|domain-registrar|hosted-probe|all> Default: pages-deploy.\n --project-name <name> Default: enigma-memory.\n --domain <host> Default: enigmamemory.com.\n --token-name <name> Default: enigma-memory-pages-deploy.\n --user-id <id> Only needed if adding user-scoped permissions.\n --out <file> Write JSON and print the same JSON.\n\nInput should be the JSON returned by Cloudflare /user/tokens/permission_groups. The output is a request-body skeleton; it does not create or print a token.\n`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function requiredText(name, value) {
|
|
59
|
+
if (typeof value !== 'string' || value.trim().length === 0) throw new UsageError(`${name} is required`);
|
|
60
|
+
return value.trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalize(value) {
|
|
64
|
+
return String(value ?? '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().replace(/\s+/g, ' ');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function includeMode(mode, candidate) {
|
|
68
|
+
return mode === 'all' || mode === candidate;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function permissionSpecs(mode) {
|
|
72
|
+
const specs = [];
|
|
73
|
+
if (includeMode(mode, 'pages-observe') || includeMode(mode, 'pages-deploy')) {
|
|
74
|
+
specs.push({
|
|
75
|
+
key: 'account_read',
|
|
76
|
+
access: 'read',
|
|
77
|
+
product: 'Account',
|
|
78
|
+
aliases: ['Account Settings Read', 'Account:Read', 'Account Read'],
|
|
79
|
+
preferred_scope: ACCOUNT_SCOPE,
|
|
80
|
+
reason: 'Confirm account/project ownership before deploy or verify.',
|
|
81
|
+
});
|
|
82
|
+
specs.push({
|
|
83
|
+
key: 'pages_read',
|
|
84
|
+
access: 'read',
|
|
85
|
+
product: 'Cloudflare Pages',
|
|
86
|
+
aliases: ['Cloudflare Pages Read', 'Pages Read'],
|
|
87
|
+
preferred_scope: ACCOUNT_SCOPE,
|
|
88
|
+
reason: 'Read Pages project/custom-domain state during verification.',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (includeMode(mode, 'pages-deploy')) {
|
|
92
|
+
specs.push({
|
|
93
|
+
key: 'pages_edit',
|
|
94
|
+
access: 'edit',
|
|
95
|
+
product: 'Cloudflare Pages',
|
|
96
|
+
aliases: ['Cloudflare Pages Edit', 'Cloudflare Pages Write', 'Cloudflare Pages:Edit', 'Pages Edit', 'Pages Write'],
|
|
97
|
+
preferred_scope: ACCOUNT_SCOPE,
|
|
98
|
+
reason: 'Deploy the preflighted static artifact to Cloudflare Pages.',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (includeMode(mode, 'domain-registrar')) {
|
|
102
|
+
specs.push({
|
|
103
|
+
key: 'registrar_read',
|
|
104
|
+
access: 'read',
|
|
105
|
+
product: 'Registrar',
|
|
106
|
+
aliases: ['Registrar Read', 'Domain Registration Read', 'Registrar Domains Read'],
|
|
107
|
+
preferred_scope: ACCOUNT_SCOPE,
|
|
108
|
+
reason: 'Search/check domain registration and read registration status.',
|
|
109
|
+
});
|
|
110
|
+
specs.push({
|
|
111
|
+
key: 'registrar_edit',
|
|
112
|
+
access: 'edit',
|
|
113
|
+
product: 'Registrar',
|
|
114
|
+
aliases: ['Registrar Edit', 'Registrar Write', 'Domain Registration Edit', 'Domain Registration Write', 'Registrar Domains Edit', 'Registrar Domains Write'],
|
|
115
|
+
preferred_scope: ACCOUNT_SCOPE,
|
|
116
|
+
reason: 'Register the exact confirmed domain only after charge acknowledgement.',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (includeMode(mode, 'hosted-probe')) {
|
|
120
|
+
specs.push({
|
|
121
|
+
key: 'workers_scripts_read',
|
|
122
|
+
access: 'read',
|
|
123
|
+
product: 'Workers Scripts',
|
|
124
|
+
aliases: ['Workers Scripts Read', 'Workers Read', 'Cloudflare Workers Read', 'Workers Script Read'],
|
|
125
|
+
preferred_scope: ACCOUNT_SCOPE,
|
|
126
|
+
reason: 'Observe hosted probe and relay/gateway Worker services before/after deploy.',
|
|
127
|
+
});
|
|
128
|
+
specs.push({
|
|
129
|
+
key: 'workers_scripts_edit',
|
|
130
|
+
access: 'edit',
|
|
131
|
+
product: 'Workers Scripts',
|
|
132
|
+
aliases: ['Workers Scripts Edit', 'Workers Scripts Write', 'Workers Edit', 'Workers Write', 'Cloudflare Workers Edit', 'Cloudflare Workers Write', 'Workers Script Edit'],
|
|
133
|
+
preferred_scope: ACCOUNT_SCOPE,
|
|
134
|
+
reason: 'Deploy the hosted /livez and fail-closed /readyz probe plus relay/gateway edge Workers.',
|
|
135
|
+
});
|
|
136
|
+
specs.push({
|
|
137
|
+
key: 'workers_routes_edit',
|
|
138
|
+
access: 'edit',
|
|
139
|
+
product: 'Workers Routes',
|
|
140
|
+
aliases: ['Workers Routes Edit', 'Workers Routes Write', 'Cloudflare Workers Routes Edit', 'Workers Route Edit'],
|
|
141
|
+
preferred_scope: ZONE_SCOPE,
|
|
142
|
+
reason: 'Attach relay.enigmamemory.com and gateway.enigmamemory.com as Workers Custom Domains without exposing token values.',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return specs;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function extractPermissionGroups(raw) {
|
|
149
|
+
const parsed = Array.isArray(raw) ? raw : (Array.isArray(raw?.result) ? raw.result : raw?.result?.permission_groups);
|
|
150
|
+
if (!Array.isArray(parsed)) throw new UsageError('permission group JSON must be an array or contain result[]');
|
|
151
|
+
return parsed.map((item, index) => {
|
|
152
|
+
if (typeof item?.id !== 'string' || item.id.trim().length === 0) throw new UsageError(`permission group at index ${index} missing id`);
|
|
153
|
+
if (typeof item?.name !== 'string' || item.name.trim().length === 0) throw new UsageError(`permission group at index ${index} missing name`);
|
|
154
|
+
const scopes = Array.isArray(item.scopes) ? item.scopes.filter((scope) => typeof scope === 'string') : [];
|
|
155
|
+
return {
|
|
156
|
+
id: item.id.trim(),
|
|
157
|
+
name: item.name.trim(),
|
|
158
|
+
description: typeof item.description === 'string' ? item.description.trim() : '',
|
|
159
|
+
scopes,
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function chooseCandidate(candidates, preferredScope) {
|
|
165
|
+
const scoped = candidates.find((candidate) => candidate.scopes.includes(preferredScope));
|
|
166
|
+
return scoped ?? candidates[0] ?? null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function resolvePermissions(groups, specs) {
|
|
170
|
+
const byName = new Map();
|
|
171
|
+
for (const group of groups) {
|
|
172
|
+
const key = normalize(group.name);
|
|
173
|
+
if (!byName.has(key)) byName.set(key, []);
|
|
174
|
+
byName.get(key).push(group);
|
|
175
|
+
}
|
|
176
|
+
const resolved = [];
|
|
177
|
+
const unresolved = [];
|
|
178
|
+
for (const spec of specs) {
|
|
179
|
+
let match = null;
|
|
180
|
+
for (const alias of spec.aliases) {
|
|
181
|
+
const candidates = byName.get(normalize(alias));
|
|
182
|
+
if (candidates?.length) {
|
|
183
|
+
match = chooseCandidate(candidates, spec.preferred_scope);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (match === null) {
|
|
188
|
+
unresolved.push({ key: spec.key, aliases: spec.aliases, preferred_scope: spec.preferred_scope, reason: spec.reason });
|
|
189
|
+
} else {
|
|
190
|
+
resolved.push({
|
|
191
|
+
key: spec.key,
|
|
192
|
+
id: match.id,
|
|
193
|
+
name: match.name,
|
|
194
|
+
scopes: match.scopes,
|
|
195
|
+
access: spec.access,
|
|
196
|
+
product: spec.product,
|
|
197
|
+
reason: spec.reason,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return { resolved, unresolved };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function resourceForScope(scope, { accountId, userId }) {
|
|
205
|
+
if (scope === ACCOUNT_SCOPE) return { resource: `com.cloudflare.api.account.${accountId}`, value: '*' };
|
|
206
|
+
if (scope === ZONE_SCOPE) return { resource: `com.cloudflare.api.account.${accountId}`, value: { 'com.cloudflare.api.account.zone.*': '*' } };
|
|
207
|
+
if (scope === USER_SCOPE) return userId ? { resource: `com.cloudflare.api.user.${userId}`, value: '*' } : null;
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function policiesForResolved(resolved, context) {
|
|
212
|
+
const buckets = new Map();
|
|
213
|
+
const missingResources = [];
|
|
214
|
+
for (const item of resolved) {
|
|
215
|
+
const scope = item.scopes.includes(ACCOUNT_SCOPE) ? ACCOUNT_SCOPE
|
|
216
|
+
: item.scopes.includes(ZONE_SCOPE) ? ZONE_SCOPE
|
|
217
|
+
: item.scopes.includes(USER_SCOPE) ? USER_SCOPE
|
|
218
|
+
: item.scopes[0];
|
|
219
|
+
const resource = resourceForScope(scope, context);
|
|
220
|
+
if (resource === null) {
|
|
221
|
+
missingResources.push({ key: item.key, id: item.id, name: item.name, scope });
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const bucketKey = `${scope}:${JSON.stringify(resource)}`;
|
|
225
|
+
if (!buckets.has(bucketKey)) buckets.set(bucketKey, { scope, resources: { [resource.resource]: resource.value }, permission_groups: [] });
|
|
226
|
+
buckets.get(bucketKey).permission_groups.push({ id: item.id, name: item.name });
|
|
227
|
+
}
|
|
228
|
+
const policies = [...buckets.values()].map((bucket) => ({
|
|
229
|
+
effect: 'allow',
|
|
230
|
+
resources: bucket.resources,
|
|
231
|
+
permission_groups: bucket.permission_groups.sort((a, b) => a.name.localeCompare(b.name)),
|
|
232
|
+
}));
|
|
233
|
+
return { policies, missingResources };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function readPermissionGroups(path) {
|
|
237
|
+
return JSON.parse(await readFile(resolve(path), 'utf8'));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function buildCloudflareTokenRequest(input = {}) {
|
|
241
|
+
const mode = requiredText('--mode', input.mode ?? 'pages-deploy');
|
|
242
|
+
if (!MODES.includes(mode)) throw new UsageError(`--mode must be one of ${MODES.join(', ')}`);
|
|
243
|
+
const accountId = requiredText('--account-id', input.accountId);
|
|
244
|
+
const tokenName = requiredText('--token-name', input.tokenName ?? 'enigma-memory-pages-deploy');
|
|
245
|
+
const rawGroups = input.permissionGroupsJson ?? await readPermissionGroups(requiredText('--permission-groups', input.permissionGroups));
|
|
246
|
+
const groups = extractPermissionGroups(rawGroups);
|
|
247
|
+
const specs = permissionSpecs(mode);
|
|
248
|
+
const { resolved, unresolved } = resolvePermissions(groups, specs);
|
|
249
|
+
const { policies, missingResources } = policiesForResolved(resolved, { accountId, userId: input.userId });
|
|
250
|
+
const tokenRequest = {
|
|
251
|
+
name: tokenName,
|
|
252
|
+
policies,
|
|
253
|
+
};
|
|
254
|
+
const ok = unresolved.length === 0 && missingResources.length === 0 && policies.length > 0;
|
|
255
|
+
const output = {
|
|
256
|
+
schema: CLOUDFLARE_TOKEN_REQUEST_SCHEMA,
|
|
257
|
+
generated_at: input.generated_at ?? input.generatedAt ?? new Date().toISOString(),
|
|
258
|
+
ok,
|
|
259
|
+
mode,
|
|
260
|
+
account_id: accountId,
|
|
261
|
+
token_name: tokenName,
|
|
262
|
+
required_permission_count: specs.length,
|
|
263
|
+
resolved_permission_count: resolved.length,
|
|
264
|
+
unresolved_permission_count: unresolved.length,
|
|
265
|
+
resolved_permission_groups: resolved,
|
|
266
|
+
unresolved_permission_groups: unresolved,
|
|
267
|
+
missing_resource_bindings: missingResources,
|
|
268
|
+
token_request: tokenRequest,
|
|
269
|
+
create_endpoints: {
|
|
270
|
+
account_owned_token: `/client/v4/accounts/${accountId}/tokens`,
|
|
271
|
+
user_owned_token: '/client/v4/user/tokens',
|
|
272
|
+
},
|
|
273
|
+
mutation_boundaries: {
|
|
274
|
+
token_created: false,
|
|
275
|
+
token_value_printed: false,
|
|
276
|
+
requires_api_tokens_edit_bootstrap_token_for_api_creation: true,
|
|
277
|
+
dashboard_creation_supported: true,
|
|
278
|
+
registrar_register_requires_charge_acknowledgement: includeMode(mode, 'domain-registrar'),
|
|
279
|
+
},
|
|
280
|
+
source_notes: [
|
|
281
|
+
'Cloudflare token policies use permission group ids; permission names are cosmetic and may change.',
|
|
282
|
+
'Account resources use com.cloudflare.api.account.<ACCOUNT_ID>: "*" for account-scoped permissions.',
|
|
283
|
+
'This packet is a request body skeleton only; it does not create a token or prove credentials exist.',
|
|
284
|
+
],
|
|
285
|
+
};
|
|
286
|
+
const json = JSON.stringify(output);
|
|
287
|
+
if (SECRET_OUTPUT_RE.test(json)) throw new Error('Cloudflare token request output appears to contain a secret');
|
|
288
|
+
return output;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function runCli(argv = process.argv.slice(2), options = {}) {
|
|
292
|
+
const parsed = parseArgs(argv);
|
|
293
|
+
if (parsed.help) return { text: usage() };
|
|
294
|
+
const packet = await buildCloudflareTokenRequest({ ...parsed, generated_at: options.generated_at ?? options.generatedAt });
|
|
295
|
+
const json = `${JSON.stringify(packet, null, 2)}\n`;
|
|
296
|
+
if (parsed.out) {
|
|
297
|
+
const outPath = resolve(parsed.out);
|
|
298
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
299
|
+
await writeFile(outPath, json, 'utf8');
|
|
300
|
+
}
|
|
301
|
+
return { json: packet };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
305
|
+
try {
|
|
306
|
+
const result = await runCli();
|
|
307
|
+
if (result.text) process.stdout.write(result.text);
|
|
308
|
+
else process.stdout.write(`${JSON.stringify(result.json, null, 2)}\n`);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
311
|
+
process.stdout.write(`${JSON.stringify({ schema: CLOUDFLARE_TOKEN_REQUEST_SCHEMA, ok: false, error: { code: error instanceof UsageError ? 'USAGE_ERROR' : 'TOKEN_REQUEST_ERROR', message } }, null, 2)}\n`);
|
|
312
|
+
process.exitCode = error instanceof UsageError ? 2 : 1;
|
|
313
|
+
}
|
|
314
|
+
}
|