import { mkdir, writeFile } from 'node:fs/promises' import { resolve, dirname } from 'node:path' import { pathToFileURL, fileURLToPath } from 'node:url' import { createHash } from 'node:crypto' const root = resolve(dirname(fileURLToPath(import.meta.url)), '..') const endpoint = '/merchant/v1/orders/workbench' class VerificationError extends Error {} function requireFact(value, message) { if (!value) throw new VerificationError(message) } const ids = rows => rows.map(row => String(row.orderId)) const equalIds = (a, b) => JSON.stringify(ids(a).sort()) === JSON.stringify(ids(b).sort()) const digest = rows => createHash('sha256').update(JSON.stringify(ids(rows).sort())).digest('hex') export function executionOptions(args, execArgs, env) { if (!args.length || args.length === 1 && args[0] === '--plan') return { execute: false } requireFact(args.length === 3 && args[0] === '--execute' && args[1] === '--environment=TEST' && /^--run-id=[A-Za-z0-9_-]{1,60}$/.test(args[2]), 'Explicit TEST execution and a new run-id are required') const files = execArgs.filter(arg => arg.startsWith('--env-file=')) requireFact(files.length === 1 && execArgs.length === 1 && resolve(files[0].slice(11)).replaceAll('\\', '/') .endsWith('/.codex/env/chongchong-global/test-acceptance.env'), 'Use the explicitly authorized test-acceptance.env') requireFact(String(env.CHONGCHONG_ENV).toUpperCase() === 'TEST', 'TEST profile required') for (const key of ['CHONGCHONG_MERCHANT_API_BASE_URL', 'CHONGCHONG_MERCHANT_USERNAME', 'CHONGCHONG_MERCHANT_PASSWORD']) requireFact(Boolean(env[key]), `Missing ${key}`) const base = new URL(env.CHONGCHONG_MERCHANT_API_BASE_URL) requireFact(['http:', 'https:'].includes(base.protocol) && !base.username && !base.password && base.pathname === '/' && !base.search && !base.hash, 'Expected the configured TEST Gateway origin') return { execute: true, runId: args[2].slice(9), base: base.origin + '/api' } } export function validatePage(page, number, size) { const total = Number(page?.total), pages = Number(page?.pages) requireFact(Number.isSafeInteger(total) && total >= 0 && Number(page.page) === number && Number(page.size) === size && pages === Math.ceil(total / size), 'Invalid page metadata') requireFact(Array.isArray(page.records) && page.records.length === Math.min(size, Math.max(0, total - (number - 1) * size)), 'Truncated page') requireFact(page.records.every(row => row.orderId != null) && new Set(ids(page.records)).size === page.records.length, 'Missing or duplicate order identity') return page } /** GET-only verifier. Caller owns authentication and durable, sanitized request evidence. */ export async function verifyWorkbench(read, customerId) { const checks = [] const check = (name, ok) => { requireFact(ok, name); checks.push({ name, passed: true }) } async function all(path, workbench = false, params = {}) { let rows = [], progress = [], expected = null, first for (let number = 1; ; number++) { const query = new URLSearchParams({ ...params, page: String(number), size: '200' }) const response = await read(`${path}?${query}`) const page = validatePage(workbench ? response.page : response, number, 200) if (number === 1) { expected = Number(page.total); first = response } requireFact(Number(page.total) === expected, 'Dataset changed while paging; rerun in a stable window') requireFact(page.records.every(row => String(row.tenantId) === String(customerId)), 'Customer scope mismatch') if (workbench) { requireFact(Array.isArray(response.progress) && response.progress.length === page.records.length && new Set(response.progress.map(p => String(p.orderId))).size === page.records.length && response.progress.every(p => page.records.some(r => String(r.orderId) === String(p.orderId)) && ['none','loaded','declared','cleared','scheduled','delivering','delivered'].includes(p.stage) && typeof p.partial === 'boolean'), 'Invalid progress coverage') progress.push(...response.progress) } rows.push(...page.records) if (number >= Number(page.pages)) break requireFact(number < 1000, 'Pagination safety bound exceeded') } requireFact(rows.length === expected && new Set(ids(rows)).size === expected, 'Duplicate or missing orders across pages') return { rows, first, progress } } const workbench = await all(endpoint, true) check('sample actually exceeds the former 200-order limit', workbench.rows.length > 200) const formal = await all('/merchant/v1/orders') check('all formal orders match the independent unified query', equalIds(workbench.rows.filter(r => r.mainStatus !== 'DRAFT'), formal.rows)) const queues = workbench.first.queues for (const name of ['all', 'fee', 'customs', 'draft', 'loaded', 'declared', 'cleared', 'scheduled', 'delivering', 'delivered']) { check(`queue ${name} has a valid count`, Number.isSafeInteger(Number(queues?.[name])) && Number(queues[name]) >= 0) } check('all queue count matches complete rows', Number(queues.all) === workbench.rows.length) check('fee count matches authoritative confirmation flags', Number(queues.fee) === workbench.rows.filter(r => r.customerFeeConfirmationRequired === true).length) check('draft count matches draft rows', Number(queues.draft) === workbench.rows.filter(r => r.mainStatus === 'DRAFT').length) for (const queue of ['fee', 'customs', 'draft', 'loaded', 'declared', 'cleared', 'scheduled', 'delivering', 'delivered']) { const selected = await all(endpoint, true, { queue }) check(`${queue} complete page count`, selected.rows.length === Number(queues[queue])) const own = new Set(ids(workbench.rows)) check(`${queue} contains only listed customer orders`, ids(selected.rows).every(id => own.has(id))) if (queue === 'fee') check('fee queue exact membership', equalIds(selected.rows, workbench.rows.filter(r => r.customerFeeConfirmationRequired === true))) if (queue === 'draft') check('draft queue exact membership', equalIds(selected.rows, workbench.rows.filter(r => r.mainStatus === 'DRAFT'))) if (['loaded','declared','cleared','scheduled','delivering','delivered'].includes(queue)) { const expectedIds = new Set(workbench.progress.filter(p => p.stage === queue).map(p => String(p.orderId))) check(`${queue} progress exact membership`, selected.rows.length === expectedIds.size && ids(selected.rows).every(id => expectedIds.has(id))) check(`${queue} selected progress agrees`, selected.progress.every(p => p.stage === queue)) } } for (const [page, size] of [[21, 10], [2, 50]]) { const result = await read(`${endpoint}?page=${page}&size=${size}`) validatePage(result.page, page, size) check(`page ${page} size ${size} preserves exact ordered slice`, JSON.stringify(ids(result.page.records)) === JSON.stringify(ids(workbench.rows.slice((page - 1) * size, page * size)))) } const channels = workbench.first.channelNames check('channel options include every listed channel', Array.isArray(channels) && workbench.rows.every(r => !r.channelName || channels.includes(r.channelName))) const channel = workbench.rows.find(r => r.channelName)?.channelName requireFact(channel, 'A channel-filter sample is required') const filtered = await all(endpoint, true, { channelName: channel }) check('channel filter applies before counting and paging', equalIds(filtered.rows, workbench.rows.filter(r => r.channelName === channel))) const spoofed = await read(`${endpoint}?page=1&size=200&tenantId=0&customerId=0`) check('spoofed customer parameters cannot broaden scope', equalIds(spoofed.page.records, workbench.first.page.records) && Number(spoofed.page.total) === workbench.rows.length) const refreshed = await read(`${endpoint}?page=1&size=200`) check('dataset stayed stable through verification', Number(refreshed.page.total) === workbench.rows.length && JSON.stringify(ids(refreshed.page.records)) === JSON.stringify(ids(workbench.first.page.records))) return { checks, total: workbench.rows.length, formalTotal: formal.rows.length, membershipSha256: digest(workbench.rows), queues, limitations: ['customs queue business membership requires separate customs evidence', 'GET evidence does not replace browser or second-customer authorization tests'] } } export async function execute(args = process.argv.slice(2), execArgs = process.execArgv, env = process.env) { const options = executionOptions(args, execArgs, env) if (!options.execute) return { plan: true, businessWrites: false, checks: ['complete pagination', 'independent formal-order set', 'queue counts', 'page 21', 'channel filtering', 'customer parameter isolation'], required: 'Deploy Firstleg and Order workbench APIs first; use a stable customer dataset with more than 200 rows' } const directory = resolve(root, 'reports', `client-workbench-${options.runId}`) await mkdir(directory) // Exclusive run: existing evidence is never replaced by a rerun. const report = { startedAt: new Date().toISOString(), readOnlyBusiness: true, success: false, requests: [], sessionClosed: false } const save = () => writeFile(resolve(directory, 'report.json'), JSON.stringify(report, null, 2)) let token = '', refresh = '' async function request(method, path, body) { requireFact(method === 'GET' || method === 'POST' && ['/auth/login', '/auth/logout'].includes(path), 'Business writes prohibited') const query = new URLSearchParams(path.split('?')[1] || '') const entry = { method, path: path.split('?')[0], outcome: 'PENDING', page: query.get('page') || undefined, size: query.get('size') || undefined, queue: query.get('queue') || undefined, queryKeys: [...query.keys()], querySha256: createHash('sha256').update(query.toString()).digest('hex') } report.requests.push(entry); await save() try { const response = await fetch(options.base + path, { method, redirect: 'error', signal: AbortSignal.timeout(30000), headers: { Accept: 'application/json', ...(body ? { 'Content-Type': 'application/json' } : {}), ...(token ? { Authorization: `Bearer ${token}` } : {}) }, body: body ? JSON.stringify(body) : undefined }) const payload = await response.json() Object.assign(entry, { outcome: 'RECEIVED', http: response.status, code: payload.code, requestId: payload.requestId, traceId: payload.traceId }) await save() const safeCode = /^[A-Z0-9]{1,16}$/.test(String(payload.code)) ? String(payload.code) : 'INVALID_CODE' requireFact(response.ok && String(payload.code) === '200', `API rejected ${entry.path}: HTTP ${response.status}, code ${safeCode}`) return payload.data } catch (error) { if (entry.outcome === 'PENDING') entry.outcome = 'UNKNOWN'; await save(); throw error } } try { const login = await request('POST', '/auth/login', { loginId: env.CHONGCHONG_MERCHANT_USERNAME, password: env.CHONGCHONG_MERCHANT_PASSWORD }) token = login?.accessToken || ''; refresh = login?.refreshToken || '' requireFact(token && refresh, 'Incomplete authentication response') const profile = await request('GET', '/merchant/v1/iam/tenant/profile') requireFact(profile?.id != null, 'Missing authenticated customer profile') Object.assign(report, await verifyWorkbench(path => request('GET', path), profile.id)) report.success = true } catch (error) { report.success = false // Fixed verifier/HTTP classifications only; never persist an arbitrary response body or credential. report.failure = error instanceof VerificationError ? error.message : 'Transport or response format failure' } finally { if (token) { try { await request('POST', '/auth/logout', { refreshToken: refresh }); report.sessionClosed = true } catch { report.success = false; report.logoutFailure = true } } report.success = report.success && report.sessionClosed report.completedAt = new Date().toISOString(); await save() } return { success: report.success, sessionClosed: report.sessionClosed, report: resolve(directory, 'report.json') } } if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { execute().then(result => { console.log(JSON.stringify(result)); if (result.success === false) process.exitCode = 1 }) .catch(() => { console.error('Workbench verification did not start; check explicit TEST profile and new run-id.'); process.exitCode = 1 }) }