fix(rn-common): redact API diagnostics
这个提交包含在:
父节点
7589db7332
当前提交
a2d3a27d80
@ -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'
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络诊断只能描述请求形态,不得复制请求/响应 body、headers 或完整 URL。
|
||||
* 这些字段可能包含手机号、userId、sessionId、签名和其它业务数据。
|
||||
*/
|
||||
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/)
|
||||
})
|
||||
正在加载...
在新工单中引用
屏蔽一个用户