fix(rn-common): redact API diagnostics
这个提交包含在:
父节点
7589db7332
当前提交
a2d3a27d80
@ -105,6 +105,14 @@ pnpm --dir packages/update test
|
|||||||
|
|
||||||
## 7. 本轮变更记录
|
## 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 精确版本接入
|
### 2026-07-18 / Jenkins #58—#60 与 App4 精确版本接入
|
||||||
|
|
||||||
- `sdk-rn-publish #58` 被 Windows Hermes 测试正确阻断:测试曾把文本写成 `hermesc.exe`,Windows `spawnSync` 无法执行;该构建未发布任何包。
|
- `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,
|
isCancel,
|
||||||
} from 'axios'
|
} from 'axios'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
import {
|
||||||
|
createAxiosDiagnostic,
|
||||||
|
createResponseDiagnostic,
|
||||||
|
createValidationDiagnostic,
|
||||||
|
} from './diagnostics'
|
||||||
import { RequestError } from './errors'
|
import { RequestError } from './errors'
|
||||||
import { type RequestOptions, useRequest, ValidationError } from './useRequest'
|
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(
|
const responseInterceptor = responseInterceptors.use(
|
||||||
r => {
|
r => {
|
||||||
if (options?.log) {
|
if (options?.log) {
|
||||||
console.debug(JSON.stringify(r, null, 2))
|
console.debug('[XuqmAPI] response', JSON.stringify(createResponseDiagnostic(r)))
|
||||||
}
|
}
|
||||||
return 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) {
|
if (thrownError instanceof ValidationError) {
|
||||||
const e = thrownError
|
const e = thrownError
|
||||||
console.error(
|
console.error(
|
||||||
JSON.stringify(
|
'[XuqmAPI] response validation failed',
|
||||||
{
|
JSON.stringify(createValidationDiagnostic(e.response, e.issues)),
|
||||||
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,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (e.issues.length <= 0) {
|
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)) {
|
if (isAxiosError(thrownError)) {
|
||||||
console.error(
|
console.error(
|
||||||
JSON.stringify(thrownError, null, 2),
|
'[XuqmAPI] request failed',
|
||||||
`${thrownError.config?.baseURL}${thrownError.config?.url}`,
|
JSON.stringify(createAxiosDiagnostic(thrownError)),
|
||||||
)
|
)
|
||||||
return Promise.reject(new RequestError('网络请求失败', 'AxiosError', 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))
|
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/)
|
||||||
|
})
|
||||||
正在加载...
在新工单中引用
屏蔽一个用户