scripts/diagnose-request.mjs
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env node
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { parseArgs } from 'node:util';
import { outsideGit } from './lib/private-evidence.mjs';
import { isQaLane } from './lib/qa-lanes.mjs';
import { readEnvironmentRegistry } from './lib/environment-registry.mjs';
import { cloudflareDiagnosticScript, localDiagnosticQuery, projectDiagnosticSession, resolveDiagnosticRequest } from './lib/request-diagnostics.mjs';
const HELP = `Usage: npm run diagnose -- <prepare|query> [options]
prepare --target amber|cobalt|violet --account-id ID --admin-origin https://HOST
--slack-url URL [--run-id ID] [--from UTC --to UTC] [--trace-id ID]
[--output-root PRIVATE_DIRECTORY]
Or use explicit --worker NAME --workspace ID for another resolved target.
Local: --local --worker NAME --workspace ID --admin-origin http://127.0.0.1:PORT
Run-ID-only requests require --from and --to (UTC ending Z, <=30 minutes).
This only creates a private record. It does not contact Slack or the Worker.
query --record DIRECTORY --session PRIVATE_JSON_FILE
Read the prepare command's Admin URL through an authenticated browser first.
Saves a content-free snapshot and a bounded Cloudflare MCP script or Local
Explorer query. It does not fetch credentials or execute the query itself.
If Sessions is unavailable: --no-session (the gap stays in the evidence).
Each attempt uses a new directory so the first failure cannot be overwritten.
`;
const readJson = (file) => {
if (statSync(file).size > 5 * 1024 * 1024) throw new Error('Evidence input exceeds 5 MiB.');
try { return JSON.parse(readFileSync(file, 'utf8')); }
catch { throw new Error('Evidence input could not be read as JSON.'); }
};
function privateDirectory(root, prefix) {
outsideGit(root);
mkdirSync(root, { recursive: true, mode: 0o700 });
const directory = mkdtempSync(join(realpathSync(root), prefix));
chmodSync(directory, 0o700);
return directory;
}
function writePrivate(file, value) {
writeFileSync(file, typeof value === 'string' ? value : `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
}
export async function runDiagnosticCli(argv, io = {}) {
const output = io.stdout ?? ((value) => process.stdout.write(value));
const error = io.stderr ?? ((value) => process.stderr.write(value));
try {
const { values: flags, positionals } = parseArgs({ args: argv, allowPositionals: true, options: {
...Object.fromEntries(['target', 'worker', 'workspace', 'account-id', 'admin-origin', 'slack-url',
'run-id', 'from', 'to', 'trace-id', 'output-root', 'record', 'session'].map((name) => [name, { type: 'string' }])),
local: { type: 'boolean' }, 'no-session': { type: 'boolean' }, help: { type: 'boolean' },
} });
if (flags.help) { output(HELP); return 0; }
if (positionals.length !== 1) throw new Error('Choose prepare or query. Use --help for usage.');
const command = positionals[0];
let result;
if (command === 'prepare') {
if (flags.record || flags.session || flags['no-session']) throw new Error('Evidence inputs belong to query.');
let registration;
if (flags.target) {
if (!isQaLane(flags.target) || flags.local) throw new Error('Use a registered hosted target, or explicit local coordinates.');
registration = (io.readRegistry ?? readEnvironmentRegistry)().targets[flags.target];
if ((flags.worker && flags.worker !== registration.workerName) ||
(flags.workspace && flags.workspace !== registration.workspaceId)) throw new Error('Coordinates disagree with the registered target.');
}
const request = resolveDiagnosticRequest({
target: flags.target, workerName: registration?.workerName ?? flags.worker,
workspaceId: registration?.workspaceId ?? flags.workspace, accountId: flags['account-id'],
adminOrigin: flags['admin-origin'], slackUrl: flags['slack-url'], runId: flags['run-id'],
from: flags.from, to: flags.to, traceId: flags['trace-id'], local: flags.local,
});
const record = privateDirectory(flags['output-root'] ?? join(homedir(), '.chickpea', 'diagnostics'), 'request-');
writePrivate(join(record, 'request.json'), request);
result = { record, runId: request.runId, adminUrl: request.adminUrl,
next: 'Read adminUrl using existing authenticated Admin access, then run query --record DIRECTORY --session PRIVATE_JSON_FILE.' };
} else if (command === 'query') {
if (Object.keys(flags).some((name) => !['record', 'session', 'no-session'].includes(name))) throw new Error('Query uses the coordinates saved by prepare.');
if (!flags.record || Boolean(flags.session) === Boolean(flags['no-session'])) throw new Error('Query needs --record and either --session or --no-session.');
const record = outsideGit(flags.record);
const saved = readJson(join(record, 'request.json'));
// Revalidate record coordinates rather than trusting an arbitrary URL in a file.
const request = resolveDiagnosticRequest({ ...saved, local: saved.runtime === 'local',
adminOrigin: new URL(saved.adminUrl).origin, traceId: saved.suppliedTraceId,
from: new Date(saved.timeframe.from).toISOString(), to: new Date(saved.timeframe.to).toISOString() });
if (flags.session) outsideGit(flags.session);
const session = flags.session ? projectDiagnosticSession(readJson(flags.session), request) : null;
const attempt = privateDirectory(record, 'attempt-');
writePrivate(join(attempt, 'session.json'), session);
const local = request.runtime === 'local';
const queryFile = join(attempt, local ? 'local-query.json' : 'cloudflare-query.js');
writePrivate(queryFile, local ? localDiagnosticQuery(request, session) : cloudflareDiagnosticScript(request, session));
writePrivate(join(attempt, 'evidence.json'), {
createdAt: new Date().toISOString(), runId: request.runId, runtime: request.runtime,
ledgerAvailable: session !== null, correlation: request.suppliedTraceId ? 'operator_supplied_trace' : 'requires_ledger_submission_bridge',
queryFile, reportFile: join(attempt, 'report.json'),
acceptance: 'unverified',
});
result = { attempt, queryFile, reportFile: join(attempt, 'report.json'),
...(local ? { url: request.explorerUrl } : { accountId: request.accountId }),
next: local
? 'POST local-query.json to url; preserve the response as reportFile. Use the saved session timeline alongside it. No rows or a 300-row result means coverage is incomplete.'
: 'Discover the current telemetry API schema with cloudflare-api search, then execute this script with the explicit accountId. Save its projected result as reportFile. Record Slack/provider acceptance separately.' };
} else throw new Error('Choose prepare or query. Use --help for usage.');
output(`${JSON.stringify(result, null, 2)}\n`);
return 0;
} catch (cause) {
// Do not echo parser/input errors that can contain raw credentials or bodies.
const message = cause instanceof Error && !cause.code?.startsWith('ERR_PARSE_ARGS') &&
!cause.message.includes('\n') ? cause.message : 'Invalid diagnostic input. Use --help for usage.';
error(`Diagnosis command failed: ${message}\n`);
return 1;
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
process.exitCode = await runDiagnosticCli(process.argv.slice(2));
}