diff --git a/src/alta-client.js b/src/alta-client.js index 6ea04ce..f2a53a1 100644 --- a/src/alta-client.js +++ b/src/alta-client.js @@ -98,15 +98,11 @@ function enforceResponseSize(data, headers, maxBytes) { } function safeTransportError(operation, error, cookie) { - if (error && typeof error === 'object' && typeof error.code === 'string' && error.code.startsWith('ALTA_')) { - return error; - } const formatted = formatAltaError(operation, error, { secrets: [cookie, `va=${cookie}`] }); - const safe = altaError(formatted.code, formatted.message, { + return altaError(formatted.code, formatted.message, { status: formatted.status, timeout: formatted.timeout, }); - return safe; } class AltaClient { diff --git a/test/alta-client.test.js b/test/alta-client.test.js index dfc5f1f..b33c295 100644 --- a/test/alta-client.test.js +++ b/test/alta-client.test.js @@ -167,6 +167,57 @@ test('rejects malformed response envelopes, JSON, status and endpoint data shape await assert.rejects(authClient.getAuthInfo(), { code: 'INVALID_ALTA_RESPONSE_DATA' }); }); +test('returns a fresh allowlisted error when transport throws an ALTA error with secret-bearing fields', async () => { + const source = Object.assign(new Error(`Alta request failed for va=${SENTINEL}`), { + code: 'ALTA_TRANSPORT_FAILURE', + status: 503, + timeout: true, + config: { headers: { Cookie: `va=${SENTINEL}` }, data: SENTINEL }, + request: { headers: { Cookie: `va=${SENTINEL}` }, body: SENTINEL }, + response: { status: 502, headers: { 'set-cookie': `va=${SENTINEL}` }, data: SENTINEL }, + body: SENTINEL, + }); + const client = new AltaClient({ + sessionStore: readyStore(), + transport: async () => { throw source; }, + }); + + const caught = await client.getDevices().catch((error) => error); + assert.notEqual(caught, source); + assert.equal(caught.code, 'ALTA_TRANSPORT_FAILURE'); + assert.equal(caught.message, 'Alta request failed for va=[REDACTED]'); + assert.equal(caught.status, 502); + assert.equal(caught.timeout, true); + assert.deepEqual(Object.keys(caught).sort(), ['code', 'status', 'timeout']); + for (const field of ['config', 'request', 'response', 'headers', 'data', 'body']) { + assert.equal(field in caught, false, field); + } + assert.doesNotMatch(JSON.stringify(caught), new RegExp(SENTINEL)); +}); + +test('safely formats a transport error whose code getter throws a secret', async () => { + const source = new Error('Transport failed safely'); + Object.defineProperty(source, 'code', { + enumerable: true, + get() { throw new Error(SENTINEL); }, + }); + source.config = { headers: { Cookie: `va=${SENTINEL}` } }; + const client = new AltaClient({ + sessionStore: readyStore(), + transport: async () => { throw source; }, + }); + + const caught = await client.getDevices().catch((error) => error); + assert.notEqual(caught, source); + assert.equal(caught.code, 'ALTA_ERROR'); + assert.equal(caught.message, 'Transport failed safely'); + assert.equal(caught.status, null); + assert.equal(caught.timeout, false); + assert.deepEqual(Object.keys(caught).sort(), ['code', 'status', 'timeout']); + assert.equal('config' in caught, false); + assert.doesNotMatch(JSON.stringify(caught), new RegExp(SENTINEL)); +}); + test('revalidates the stored origin before each request', async () => { const store = { requireSession: () => Object.freeze({ origin: 'https://evil.example', cookie: SENTINEL }) }; let called = false;