fix(rn-common): redact API diagnostics

这个提交包含在:
XuqmGroup 2026-07-18 08:35:36 +08:00
父节点 7589db7332
当前提交 a2d3a27d80
共有 4 个文件被更改,包括 129 次插入17 次删除

查看文件

@ -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` 无法执行;该构建未发布任何包。

查看文件

@ -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'
}
/**
* / bodyheaders URL
* userIdsessionId
*/
function requestShape(response: AxiosResponse<unknown>) {
return {
httpStatus: response.status,
method: response.config.method?.toUpperCase(),
path: response.config.url,
}
}
export function createResponseDiagnostic(response: AxiosResponse<unknown>): SafeApiDiagnostic {
return { type: 'Response', ...requestShape(response) }
}
export function createValidationDiagnostic(
response: AxiosResponse<unknown>,
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,
}
}

查看文件

@ -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 = <S extends z.ZodTypeAny, T extends z.infer<S> = z.infer<S>
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 = <S extends z.ZodTypeAny, T extends z.infer<S> = z.infer<S>
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 = <S extends z.ZodTypeAny, T extends z.infer<S> = z.infer<S>
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))
},
)

查看文件

@ -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<unknown> {
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/)
})