diff --git a/scripts/admin-test-env-modules/fleet-handover-acceptance-contract.test.mjs b/scripts/admin-test-env-modules/fleet-handover-acceptance-contract.test.mjs index f3e99b6f4..7b20a2b86 100644 --- a/scripts/admin-test-env-modules/fleet-handover-acceptance-contract.test.mjs +++ b/scripts/admin-test-env-modules/fleet-handover-acceptance-contract.test.mjs @@ -210,7 +210,8 @@ test('missing persistence and failed pre-send checkpoints cannot send HTTP', asy assert.ok(blocked) assert.ok(!f.calls.some(row => row.method === blocked.method && row.path === blocked.path)) if (target === 'multipart') { - assert.match(blocked.file.sha256, /^[a-f0-9]{64}$/); assert.ok(blocked.file.bytes > 0) + assert.match(blocked.file.sha256, /^[a-f0-9]{64}$/); assert.ok(blocked.file.sizeBytes > 0) + assert.equal(Object.hasOwn(blocked.file, 'bytes'), false) assert.equal(blocked.file.content, undefined) } } diff --git a/scripts/admin-test-env-modules/fleet-handover-acceptance.mjs b/scripts/admin-test-env-modules/fleet-handover-acceptance.mjs index b83e0b1eb..086fcf118 100644 --- a/scripts/admin-test-env-modules/fleet-handover-acceptance.mjs +++ b/scripts/admin-test-env-modules/fleet-handover-acceptance.mjs @@ -38,7 +38,7 @@ function io(context, module, proof) { } async function request(method, path, body, label, { token, conflict, file } = {}) { const entry = { method, path, label, actor: token ? 'PDA' : 'PLATFORM', body: clone(body), status: 'SENDING' } - if (file) entry.file = { name: file.name, type: file.type, sha256: createHash('sha256').update(file.content).digest('hex'), bytes: file.content.length } + if (file) entry.file = { name: file.name, type: file.type, sha256: createHash('sha256').update(file.content).digest('hex'), sizeBytes: file.content.length } proof.requests.push(entry) // A failed write must stop before HTTP. Keep the durable SENDING record if the process dies in flight. await checkpoint(proof) diff --git a/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance-contract.test.mjs b/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance-contract.test.mjs index cdd70dd09..64e564473 100644 --- a/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance-contract.test.mjs +++ b/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance-contract.test.mjs @@ -117,6 +117,9 @@ test('fresh return/redispatch chain proves A pending, B complete, then A complet assert.equal(result.afterRedispatch.departed.shippedPackages, 2) assert.ok(!JSON.stringify(f.checkpoints).includes('private-pda-token')) assert.ok(f.checkpoints.some(row => row.requests.at(-1)?.status === 'SENDING' && row.requests.at(-1)?.file?.sha256)) + const uploads = f.trip.returnRedispatch.requests.filter(row => row.file) + assert.ok(uploads.length > 0) + assert.ok(uploads.every(row => Number.isSafeInteger(row.file.sizeBytes) && row.file.sizeBytes > 0 && !Object.hasOwn(row.file, 'bytes'))) }) for (const missing of [false, true]) test(`unapproved or missing pre-signature gate cannot sign or close new trip; missing=${missing}`, async () => { diff --git a/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance.mjs b/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance.mjs index 4a2fffba4..3fb9d3c2b 100644 --- a/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance.mjs +++ b/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance.mjs @@ -51,7 +51,7 @@ export async function verifyFleetReturnRedispatch(context, module, trip, input, io.assert = (...args) => { context.assert(...args); if (!args[3]) throw Error(args[1]) } async function invoke(method, path, body, token, file, options) { const entry = { method, path, body: copy(body), actor: token ? 'PDA' : 'PLATFORM', status: 'SENDING' } - if (file) entry.file = { name: file.name, type: file.type, bytes: file.content.byteLength, sha256: createHash('sha256').update(file.content).digest('hex') } + if (file) entry.file = { name: file.name, type: file.type, sizeBytes: file.content.byteLength, sha256: createHash('sha256').update(file.content).digest('hex') } proof.requests.push(entry); await context.checkpoint() try { const response = file ? await context.requestMultipart(method, path, file) : await context.request(method, path, body, token, options) diff --git a/scripts/fleet-test-env-acceptance.mjs b/scripts/fleet-test-env-acceptance.mjs index 3fc65800e..79b8b54ff 100644 --- a/scripts/fleet-test-env-acceptance.mjs +++ b/scripts/fleet-test-env-acceptance.mjs @@ -573,18 +573,35 @@ export function fleetCostShares(trip, dispatchDetails) { return shares } +export function fleetCostActorJournalData(value) { + const clean = redactEvidence(value) + function removeCredentials(item) { + if (Array.isArray(item)) return item.map(removeCredentials) + if (!item || typeof item !== 'object') return item + const result = {}, forbidden = /^(?:password|newPassword|temporaryPassword|accessToken|refreshToken|authorization)$/i + let removed = false + for (const [name, field] of Object.entries(item)) { + if (forbidden.test(name)) removed = true + else result[name] = removeCredentials(field) + } + if (removed) result.credentialFieldsRemoved = true + return result + } + return removeCredentials(clean) +} + export async function prepareFleetCostActors(context) { if (!context.marker || context.createdResources.fleetCostActors || typeof context.checkpoint !== 'function') throw Error('Cost actors require fresh current-run ownership and durable checkpoints') const proof = context.createdResources.fleetCostActors = { marker: context.marker, status: 'CREATING', actors: [], requests: [] } const base = '/api/platform/v1/iam', sessions = [] async function request(method, path, body, token) { - const entry = { method, path, body: redactEvidence(body), status: 'SENDING' } + const entry = { method, path, body: fleetCostActorJournalData(body), status: 'SENDING' } proof.requests.push(entry); await context.checkpoint() try { const response = await context.request(method, path, body, token) Object.assign(entry, { status: 'RESPONDED', http: response?.http, code: response?.body?.code, - requestId: response?.requestId, traceId: response?.traceId, result: redactEvidence(response?.body?.data) }) + requestId: response?.requestId, traceId: response?.traceId, result: fleetCostActorJournalData(response?.body?.data) }) await context.checkpoint() if (!success(response) || !response.requestId || !response.traceId) throw Error(`Cost actor request failed: ${method} ${path}`) return response.body.data diff --git a/scripts/fleet-test-env-acceptance.test.mjs b/scripts/fleet-test-env-acceptance.test.mjs index 6003bb26a..58682d9d8 100644 --- a/scripts/fleet-test-env-acceptance.test.mjs +++ b/scripts/fleet-test-env-acceptance.test.mjs @@ -519,6 +519,17 @@ test('missing isolated cost actor preparation rejects before readers or fixture assert.equal(touched, false) }) +test('cost actor journal removes credential keys while retaining IDs, roles and boolean removal evidence', async () => { + const { fleetCostActorJournalData } = await import('./fleet-test-env-acceptance.mjs') + const source = { membershipId: '12', roleIds: ['7'], password: 'actual-secret', + nested: { accessToken: 'actual-session', refreshToken: 'actual-refresh', status: 'ACTIVE' } } + const result = fleetCostActorJournalData(source) + assert.deepEqual(result, { membershipId: '12', roleIds: ['7'], credentialFieldsRemoved: true, + nested: { status: 'ACTIVE', credentialFieldsRemoved: true } }) + assert.doesNotMatch(JSON.stringify(result), /"(?:password|accessToken|refreshToken)"\s*:/) + assert.equal(source.nested.accessToken, 'actual-session') +}) + for (const failLogin of [false, true]) test(`cost actors create exact isolated roles and disable only owned accounts; login failure=${failLogin}`, async () => { const calls = [], saved = [], roles = [], users = [] const context = { marker: 'RUN', token: 'admin-secret', createdResources: {}, async findAvailablePhone() { return '19900000000' },