#!/usr/bin/env node import { createHash } from 'node:crypto'; import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; const DEFAULT_PORT = process.env.LWLT_CDP_PORT || '9223'; const APPROVAL_TOKEN = 'APPROVE-LTJT-DOINFOJH-SUBMIT'; function parseArgs(argv) { const args = { _: [] }; for (let i = 0; i < argv.length; i += 1) { const arg = argv[i]; if (!arg.startsWith('--')) { args._.push(arg); continue; } const key = arg.slice(2); const next = argv[i + 1]; if (!next || next.startsWith('--')) args[key] = true; else { args[key] = next; i += 1; } } return args; } function usage() { return [ 'Usage:', ' node tools/browser_order_add_submit_approved.mjs --preflight-report reports/real-preflight.json --submit-intercept-report reports/real-intercept.json --execute-live-submit --approval-token APPROVE-LTJT-DOINFOJH-SUBMIT --out reports/live-submit.json', '', 'Options:', ` --port Chrome DevTools port. Default: ${DEFAULT_PORT}`, ' --preflight-report Required browser_order_add_preflight report with status browser_preflight_passed.', ' --submit-intercept-report Required browser_order_add_submit_intercept report with matching payload_sha256.', ' --execute-live-submit Required. Without this flag the tool refuses to submit.', ` --approval-token Required exact token: ${APPROVAL_TOKEN}`, ' --out Write redacted live submit report.', ' --help Show this help.', '', 'This is the only tool intended to allow a real DoInfoJH submit, and only after preflight and intercept hashes match.' ].join('\n'); } function readJson(path) { return JSON.parse(readFileSync(path, 'utf8')); } function writeJson(path, data) { mkdirSync(dirname(resolve(path)), { recursive: true }); writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`); } async function getJson(url) { const res = await fetch(url); if (!res.ok) throw new Error(`${res.status} ${res.statusText}: ${url}`); return res.json(); } async function getTarget(port) { const targets = await getJson(`http://127.0.0.1:${port}/json`); const target = targets.find((item) => item.type === 'page' && item.url.includes('ltjt.yunzhi.run')); if (!target) throw new Error(`No ltjt.yunzhi.run page target on port ${port}`); return target; } class CDP { constructor(wsUrl) { this.wsUrl = wsUrl; this.id = 0; this.pending = new Map(); } connect() { return new Promise((resolve, reject) => { this.ws = new WebSocket(this.wsUrl); this.ws.onopen = resolve; this.ws.onerror = (event) => reject(new Error(event.message || event.type || 'WebSocket error')); this.ws.onmessage = (event) => { const msg = JSON.parse(event.data); if (!msg.id || !this.pending.has(msg.id)) return; const { resolve: ok, reject: fail, timer } = this.pending.get(msg.id); clearTimeout(timer); this.pending.delete(msg.id); msg.error ? fail(new Error(msg.error.message)) : ok(msg.result); }; }); } send(method, params = {}, timeoutMs = 30000) { const id = ++this.id; this.ws.send(JSON.stringify({ id, method, params })); return new Promise((resolve, reject) => { const timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`Timeout: ${method}`)); }, timeoutMs); this.pending.set(id, { resolve, reject, timer }); }); } close() { this.ws?.close(); } } async function evaluate(cdp, expression, timeoutMs = 30000) { const result = await cdp.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true }, timeoutMs); if (result.exceptionDetails) { const detail = result.exceptionDetails.exception?.description || result.exceptionDetails.exception?.value || result.exceptionDetails.text || 'Runtime.evaluate failed'; throw new Error(detail); } return result.result.value; } function validateReports(preflightReport, interceptReport) { const blockers = []; if (preflightReport.status !== 'browser_preflight_passed') { blockers.push(`preflight report status must be browser_preflight_passed, got ${preflightReport.status || ''}`); } if (preflightReport.submit_safety?.live_submit_attempted !== false) { blockers.push('preflight report must prove live_submit_attempted=false'); } if (preflightReport.final_form?.serialized_form_redacted !== true) { blockers.push('preflight report must be redacted by default'); } const preflightSerializedHash = preflightReport.final_form?.serialized_form_sha256 || ''; const preflightSubmitHash = preflightReport.final_form?.submit_payload_sha256 || ''; const approvedPreflightHash = preflightSubmitHash || preflightSerializedHash; if (!approvedPreflightHash) { blockers.push('preflight report is missing final_form.serialized_form_sha256 or final_form.submit_payload_sha256'); } if (preflightReport.final_form?.required_missing?.length) { blockers.push('preflight report has required_missing fields'); } if (interceptReport.status !== 'submit_intercept_captured') { blockers.push(`submit intercept report status must be submit_intercept_captured, got ${interceptReport.status || ''}`); } if (interceptReport.submit_safety?.DoInfoJH_network_prevented !== true) { blockers.push('submit intercept report must prove DoInfoJH_network_prevented=true'); } if (interceptReport.intercepted_submit_count !== 1) { blockers.push(`submit intercept report must contain exactly one intercepted submit, got ${interceptReport.intercepted_submit_count || 0}`); } if (interceptReport.alerts?.length) { blockers.push('submit intercept report contains validation alerts'); } const intercepted = interceptReport.intercepted_submits?.[0]; if (!intercepted?.payload_sha256) { blockers.push('submit intercept report is missing intercepted payload_sha256'); } if (intercepted?.prevented_from_network !== true) { blockers.push('submit intercept report must prove intercepted submit was prevented from network'); } if (intercepted?.payload_redacted !== true) { blockers.push('submit intercept report must be redacted by default'); } const interceptHash = intercepted?.payload_sha256 || ''; if (approvedPreflightHash && interceptHash && approvedPreflightHash !== interceptHash) { blockers.push('preflight approved payload hash does not match submit-intercept payload_sha256'); } return { blockers, expectedPayloadSha256: approvedPreflightHash, expectedFieldCount: preflightReport.final_form?.serialized_field_count || intercepted?.payload_summary?.field_count || 0 }; } function submitExpression({ expectedPayloadSha256, expectedFieldCount }) { return `(async () => { const expectedPayloadSha256 = ${JSON.stringify(expectedPayloadSha256)}; const expectedFieldCount = ${JSON.stringify(expectedFieldCount || 0)}; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); function sha256Sync(ascii) { function rightRotate(value, amount) { return (value >>> amount) | (value << (32 - amount)); } const mathPow = Math.pow; const maxWord = mathPow(2, 32); const lengthProperty = 'length'; let i; let j; const result = []; const words = []; const asciiBitLength = ascii[lengthProperty] * 8; let hash = sha256Sync.h = sha256Sync.h || []; let k = sha256Sync.k = sha256Sync.k || []; let primeCounter = k[lengthProperty]; const isComposite = {}; for (let candidate = 2; primeCounter < 64; candidate += 1) { if (!isComposite[candidate]) { for (i = 0; i < 313; i += candidate) isComposite[i] = candidate; hash[primeCounter] = (mathPow(candidate, 0.5) * maxWord) | 0; k[primeCounter] = (mathPow(candidate, 1 / 3) * maxWord) | 0; primeCounter += 1; } } ascii += String.fromCharCode(0x80); while (ascii[lengthProperty] % 64 - 56) ascii += String.fromCharCode(0); for (i = 0; i < ascii[lengthProperty]; i += 1) { j = ascii.charCodeAt(i); if (j >> 8) throw new Error('sha256Sync only supports 8-bit input'); words[i >> 2] |= j << ((3 - i) % 4) * 8; } words[words[lengthProperty]] = ((asciiBitLength / maxWord) | 0); words[words[lengthProperty]] = (asciiBitLength); for (j = 0; j < words[lengthProperty];) { const w = words.slice(j, j += 16); const oldHash = hash; hash = hash.slice(0, 8); for (i = 0; i < 64; i += 1) { const w15 = w[i - 15]; const w2 = w[i - 2]; const a = hash[0]; const e = hash[4]; const temp1 = hash[7] + (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25)) + ((e & hash[5]) ^ ((~e) & hash[6])) + k[i] + (w[i] = (i < 16) ? w[i] : ( w[i - 16] + (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15 >>> 3)) + w[i - 7] + (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2 >>> 10)) ) | 0); const temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22)) + ((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2])); hash = [(temp1 + temp2) | 0].concat(hash); hash[4] = (hash[4] + temp1) | 0; } for (i = 0; i < 8; i += 1) hash[i] = (hash[i] + oldHash[i]) | 0; } for (i = 0; i < 8; i += 1) { for (j = 3; j + 1; j -= 1) { const b = (hash[i] >> (j * 8)) & 255; result.push((b < 16 ? '0' : '') + b.toString(16)); } } return result.join(''); } async function sha256(text) { const buffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)); return Array.from(new Uint8Array(buffer)).map((byte) => byte.toString(16).padStart(2, '0')).join(''); } function findOrderWindow() { const queue = [window]; while (queue.length) { const win = queue.shift(); let doc; try { doc = win.document; } catch (err) { continue; } if (doc.querySelector('#ListForm') && doc.location.href.includes('/orders_add.asp')) return win; Array.from(win.frames).forEach((frame) => queue.push(frame)); } return null; } const orderWin = findOrderWindow(); if (!orderWin) return { ok: false, status: 'live_submit_blocked', blockers: ['orders_add_form_not_found'] }; const doc = orderWin.document; const form = doc.querySelector('#ListForm'); if (!form || form.elements.length < 800) { return { ok: false, status: 'live_submit_blocked', blockers: ['orders_add_form_not_ready'], form_element_count: form?.elements?.length || 0 }; } const $ = orderWin.jQuery || orderWin.$; if (!$ || typeof $.ajax !== 'function') { return { ok: false, status: 'live_submit_blocked', blockers: ['jquery_ajax_not_found'] }; } if (typeof orderWin.SubmitInfoForm !== 'function') { return { ok: false, status: 'live_submit_blocked', blockers: ['SubmitInfoForm_not_found'] }; } const serializedForm = orderWin.jQuery ? orderWin.jQuery(form).serialize() : Array.from(new FormData(form).entries()).map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value)).join('&'); const requestBody = 'Act=DoInfoJH&' + serializedForm; const currentHash = await sha256(requestBody); const params = new URLSearchParams(serializedForm); const blockers = []; if (expectedFieldCount && Array.from(params.keys()).length !== expectedFieldCount) blockers.push('current browser field count does not match approved report field count'); if (blockers.length) { return { ok: true, status: 'live_submit_blocked', current_payload: { payload_sha256: currentHash, field_count: Array.from(params.keys()).length, value_redacted: true }, blockers, submit_safety: { live_submit_attempted: false } }; } const originalAjax = $.ajax; const ajaxRecords = []; const alerts = []; const originalAlert = orderWin.alert; const originalDialogAlert = $.dialog?.alert; orderWin.alert = function liveSubmitAlert(message) { alerts.push({ type: 'alert', message_length: String(message || '').length, message_redacted: true }); return originalAlert ? originalAlert.apply(this, arguments) : undefined; }; if ($.dialog && typeof $.dialog.alert === 'function') { $.dialog.alert = function liveSubmitDialogAlert(message) { alerts.push({ type: 'dialog.alert', message_length: String(message || '').length, message_redacted: true }); return originalDialogAlert ? originalDialogAlert.apply(this, arguments) : undefined; }; } $.ajax = function liveSubmitAjaxWrapper(options, ...rest) { const config = typeof options === 'string' ? { url: options } : { ...(options || {}) }; const url = String(config.url || ''); const data = String(config.data || ''); const isSubmit = /\\/System\\/DAT\\/orders\\.asp/i.test(url) && data.startsWith('Act=DoInfoJH&'); if (!isSubmit) return originalAjax.call(this, options, ...rest); const record = { url_redacted: '/System/DAT/orders.asp', method: config.type || config.method || 'GET', dataType: config.dataType || '', request_payload_sha256: sha256Sync(data), request_field_count: expectedFieldCount, live_submit_attempted: true, prevented_from_network: false, completed: false, ok: false, text_status: '', response_byte_length: 0, response_contains_login_timeout: false, response_contains_permission_text: false, response_contains_success_hint: false, response_contains_error_hint: false, response_redacted: true }; ajaxRecords.push(record); if (record.request_payload_sha256 !== expectedPayloadSha256) { record.prevented_from_network = true; record.completed = true; record.ok = false; record.text_status = 'blocked_hash_mismatch_before_network'; return { readyState: 4, status: 0, statusText: 'blocked_hash_mismatch_before_network', abort() {} }; } const originalSuccess = config.success; const originalError = config.error; const originalComplete = config.complete; config.success = function successWrapper(responseText, textStatus) { const text = String(responseText || ''); record.ok = true; record.text_status = textStatus || ''; record.response_byte_length = new Blob([text]).size; record.response_contains_login_timeout = /登陆|登录|login/i.test(text); record.response_contains_permission_text = /权限|permission/i.test(text); record.response_contains_success_hint = /成功|保存|添加|完成|ok|success/i.test(text); record.response_contains_error_hint = /失败|错误|异常|error|alert\\(/i.test(text) && !record.response_contains_success_hint; return originalSuccess ? originalSuccess.apply(this, arguments) : undefined; }; config.error = function errorWrapper(jqXHR, textStatus, errorThrown) { record.ok = false; record.text_status = textStatus || ''; record.error_text_redacted = errorThrown ? String(errorThrown).slice(0, 80) : ''; return originalError ? originalError.apply(this, arguments) : undefined; }; config.complete = function completeWrapper() { record.completed = true; return originalComplete ? originalComplete.apply(this, arguments) : undefined; }; return originalAjax.call(this, config, ...rest); }; let thrown = ''; try { orderWin.SubmitInfoForm(); } catch (err) { thrown = err.message || String(err); } const started = Date.now(); while (Date.now() - started < 30000) { if (ajaxRecords.some((record) => record.completed)) break; await sleep(250); } $.ajax = originalAjax; orderWin.alert = originalAlert; if ($.dialog && typeof originalDialogAlert === 'function') $.dialog.alert = originalDialogAlert; const submitBlockers = []; if (thrown) submitBlockers.push('SubmitInfoForm threw before ajax'); if (!ajaxRecords.length) submitBlockers.push('SubmitInfoForm did not attempt DoInfoJH ajax submit'); if (ajaxRecords.length !== 1) submitBlockers.push('Expected exactly one DoInfoJH ajax submit attempt'); if (ajaxRecords[0]?.prevented_from_network) submitBlockers.push('DoInfoJH ajax was blocked before network because payload hash mismatched'); if (ajaxRecords[0] && !ajaxRecords[0].completed) submitBlockers.push('DoInfoJH ajax did not complete before timeout'); if (ajaxRecords[0]?.response_contains_login_timeout) submitBlockers.push('DoInfoJH response looks like login timeout'); if (ajaxRecords[0]?.response_contains_permission_text) submitBlockers.push('DoInfoJH response contains permission text'); return { ok: true, status: submitBlockers.length ? 'live_submit_uncertain_or_failed' : 'live_submit_completed', page: { url: doc.location.href, title: doc.title, form_element_count: form.elements.length }, current_payload: { payload_sha256: currentHash, field_count: Array.from(params.keys()).length, value_redacted: true }, ajax_records: ajaxRecords, alerts, blockers: submitBlockers, submit_safety: { live_submit_attempted: ajaxRecords.some((record) => record.live_submit_attempted), approved_payload_sha256_matched: currentHash === expectedPayloadSha256 } }; })()`; } async function main() { const args = parseArgs(process.argv.slice(2)); if (args.help) { console.log(usage()); return; } const localBlockers = []; if (!args['preflight-report']) localBlockers.push('--preflight-report is required'); if (!args['submit-intercept-report']) localBlockers.push('--submit-intercept-report is required'); if (!args['execute-live-submit']) localBlockers.push('--execute-live-submit is required'); if (args['approval-token'] !== APPROVAL_TOKEN) localBlockers.push(`--approval-token must exactly equal ${APPROVAL_TOKEN}`); let preflightReport = null; let interceptReport = null; if (args['preflight-report']) preflightReport = readJson(args['preflight-report']); if (args['submit-intercept-report']) interceptReport = readJson(args['submit-intercept-report']); if (preflightReport && interceptReport) { localBlockers.push(...validateReports(preflightReport, interceptReport).blockers); } if (localBlockers.length) { const output = { generated_at: new Date().toISOString(), status: 'live_submit_refused_before_browser', blockers: localBlockers, submit_safety: { live_submit_attempted: false } }; if (args.out) writeJson(args.out, output); console.log(JSON.stringify({ status: output.status, blockers: localBlockers.length, out: args.out || '' }, null, 2)); process.exit(2); } const validated = validateReports(preflightReport, interceptReport); const target = await getTarget(args.port || DEFAULT_PORT); const cdp = new CDP(target.webSocketDebuggerUrl); await cdp.connect(); try { await cdp.send('Runtime.enable'); const output = await evaluate(cdp, submitExpression({ expectedPayloadSha256: validated.expectedPayloadSha256, expectedFieldCount: validated.expectedFieldCount }), 60000); output.generated_at = new Date().toISOString(); output.source_preflight_report = args['preflight-report']; output.source_submit_intercept_report = args['submit-intercept-report']; output.approval_gate = { execute_live_submit_flag: true, approval_token_matched: true, preflight_and_intercept_hash_matched: true }; if (args.out) writeJson(args.out, output); console.log(JSON.stringify({ status: output.status, blockers: output.blockers?.length || 0, live_submit_attempted: output.submit_safety?.live_submit_attempted || false, ajax_records: output.ajax_records?.length || 0, out: args.out || '' }, null, 2)); if (output.status !== 'live_submit_completed') process.exit(2); } finally { cdp.close(); } } main().catch((error) => { console.error(error.stack || error.message); process.exit(1); });