From a2d3a27d80418632576b92341c4723147348fed9 Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Sat, 18 Jul 2026 08:35:36 +0800 Subject: [PATCH] fix(rn-common): redact API diagnostics --- docs/IMPLEMENTATION_HANDOFF.md | 8 +++ packages/common/src/api/diagnostics.ts | 58 +++++++++++++++++++ packages/common/src/api/useApi.ts | 28 ++++----- packages/common/tests/api-diagnostics.test.ts | 52 +++++++++++++++++ 4 files changed, 129 insertions(+), 17 deletions(-) create mode 100644 packages/common/src/api/diagnostics.ts create mode 100644 packages/common/tests/api-diagnostics.test.ts diff --git a/docs/IMPLEMENTATION_HANDOFF.md b/docs/IMPLEMENTATION_HANDOFF.md index 26a1e67..557ee64 100644 --- a/docs/IMPLEMENTATION_HANDOFF.md +++ b/docs/IMPLEMENTATION_HANDOFF.md @@ -105,6 +105,14 @@ pnpm --dir packages/update test ## 7. 本轮变更记录 +### 2026-07-18 / 网络诊断安全收敛 + +- `rn-common` 不再打印完整 Axios 响应、请求配置、headers 或完整 URL。 +- 网络诊断的唯一实现位于 `packages/common/src/api/diagnostics.ts`,只保留 + method、相对 path、HTTP 状态、错误码和 Zod 字段路径。 +- 新增测试使用带手机号、sessionId、userId、token 的伪响应,强制验证 + 序列化后的诊断结果不包含这些敏感值。 + ### 2026-07-18 / Jenkins #58—#60 与 App4 精确版本接入 - `sdk-rn-publish #58` 被 Windows Hermes 测试正确阻断:测试曾把文本写成 `hermesc.exe`,Windows `spawnSync` 无法执行;该构建未发布任何包。 diff --git a/packages/common/src/api/diagnostics.ts b/packages/common/src/api/diagnostics.ts new file mode 100644 index 0000000..417a149 --- /dev/null +++ b/packages/common/src/api/diagnostics.ts @@ -0,0 +1,58 @@ +import type { AxiosError, AxiosResponse } from 'axios' +import type { z } from 'zod' + +type SafeIssue = { + code: string + message: string + path: string +} + +export type SafeApiDiagnostic = { + code?: string + httpStatus?: number + issues?: SafeIssue[] + method?: string + path?: string + type: 'AxiosError' | 'Response' | 'ValidationError' +} + +/** + * 网络诊断只能描述请求形态,不得复制请求/响应 body、headers 或完整 URL。 + * 这些字段可能包含手机号、userId、sessionId、签名和其它业务数据。 + */ +function requestShape(response: AxiosResponse) { + return { + httpStatus: response.status, + method: response.config.method?.toUpperCase(), + path: response.config.url, + } +} + +export function createResponseDiagnostic(response: AxiosResponse): SafeApiDiagnostic { + return { type: 'Response', ...requestShape(response) } +} + +export function createValidationDiagnostic( + response: AxiosResponse, + issues: z.ZodIssue[], +): SafeApiDiagnostic { + return { + type: 'ValidationError', + ...requestShape(response), + issues: issues.map(issue => ({ + code: issue.code, + message: issue.message, + path: issue.path.join('.'), + })), + } +} + +export function createAxiosDiagnostic(error: AxiosError): SafeApiDiagnostic { + return { + type: 'AxiosError', + code: error.code, + httpStatus: error.response?.status, + method: error.config?.method?.toUpperCase(), + path: error.config?.url, + } +} diff --git a/packages/common/src/api/useApi.ts b/packages/common/src/api/useApi.ts index aec79d8..d1a9e5b 100644 --- a/packages/common/src/api/useApi.ts +++ b/packages/common/src/api/useApi.ts @@ -7,6 +7,11 @@ import { isCancel, } from 'axios' import { z } from 'zod' +import { + createAxiosDiagnostic, + createResponseDiagnostic, + createValidationDiagnostic, +} from './diagnostics' import { RequestError } from './errors' import { type RequestOptions, useRequest, ValidationError } from './useRequest' @@ -80,7 +85,7 @@ export const useApi = = z.infer const responseInterceptor = responseInterceptors.use( r => { if (options?.log) { - console.debug(JSON.stringify(r, null, 2)) + console.debug('[XuqmAPI] response', JSON.stringify(createResponseDiagnostic(r))) } return r }, @@ -92,19 +97,8 @@ export const useApi = = z.infer if (thrownError instanceof ValidationError) { const e = thrownError console.error( - JSON.stringify( - { - name: e.name, - status: e.response.status, - data: e.response.data, - config: e.response.config, - headers: e.response.headers, - issues: e.issues, - url: `${e.response.config.baseURL}${e.response.config.url}`, - }, - null, - 2, - ), + '[XuqmAPI] response validation failed', + JSON.stringify(createValidationDiagnostic(e.response, e.issues)), ) if (e.issues.length <= 0) { @@ -123,13 +117,13 @@ export const useApi = = z.infer if (isAxiosError(thrownError)) { console.error( - JSON.stringify(thrownError, null, 2), - `${thrownError.config?.baseURL}${thrownError.config?.url}`, + '[XuqmAPI] request failed', + JSON.stringify(createAxiosDiagnostic(thrownError)), ) return Promise.reject(new RequestError('网络请求失败', 'AxiosError', thrownError)) } - console.error(JSON.stringify(thrownError, null, 2)) + console.error('[XuqmAPI] unexpected request failure') return Promise.reject(new RequestError('网络请求失败', 'OtherError', thrownError)) }, ) diff --git a/packages/common/tests/api-diagnostics.test.ts b/packages/common/tests/api-diagnostics.test.ts new file mode 100644 index 0000000..414cd82 --- /dev/null +++ b/packages/common/tests/api-diagnostics.test.ts @@ -0,0 +1,52 @@ +import assert from 'node:assert/strict' +import test from 'node:test' +import type { AxiosResponse } from 'axios' + +import { createResponseDiagnostic, createValidationDiagnostic } from '../src/api/diagnostics' + +function sensitiveResponse(): AxiosResponse { + return { + config: { + baseURL: 'https://api.example.com/', + data: JSON.stringify({ phone: '13800000000' }), + headers: { + sessionId: 'secret-session', + userId: 'secret-user', + }, + method: 'post', + url: '/am/example', + }, + data: { token: 'secret-token' }, + headers: { authorization: 'secret-authorization' }, + status: 200, + statusText: 'OK', + } +} + +test('API diagnostics only retain non-sensitive request shape', () => { + const diagnostic = createResponseDiagnostic(sensitiveResponse()) + + assert.deepEqual(diagnostic, { + httpStatus: 200, + method: 'POST', + path: '/am/example', + type: 'Response', + }) + const serialized = JSON.stringify(diagnostic) + assert.doesNotMatch(serialized, /secret|phone|token|authorization|example\.com/) +}) + +test('validation diagnostics retain issue paths without response data', () => { + const diagnostic = createValidationDiagnostic(sensitiveResponse(), [ + { + code: 'invalid_type', + expected: 'string', + message: 'Invalid input: expected string, received number', + path: ['data', 'userId'], + }, + ]) + + assert.equal(diagnostic.issues?.[0]?.path, 'data.userId') + const serialized = JSON.stringify(diagnostic) + assert.doesNotMatch(serialized, /secret|13800000000|authorization|example\.com/) +})