文档:认证错误包装改为信任 ts-rs 映射,不做运行时形状嗅探

- ADR §2 去掉 type 存在性检查:Rust 与 TS 同包发布、形状由 ts-rs 保证,异常形状属 Tauri/Rust 缺陷,仍由 switch 的 default 抛出去上报
- requireInvoke 移到 try 之外,认证桥未安装的错误保持原样抛出
- Error.message 只从拒绝值读可展示字符串,不是分流判据
- ADR §1 记录保留 19 个具名载荷类型的理由:它们是每个分支 as 的目标;内联 struct 变体虽能少 19 个文件,但分支就拿不到可 as 的具名类型
This commit is contained in:
2026-10-02 01:11:01 +08:00
parent f9c8c429be
commit 35f10c2051
@@ -16,7 +16,7 @@ Rust 已经返回结构化错误,但 Tauri 的 `invoke` 拒绝值是**普通
上报链路的 `error instanceof Error` 判断会把它降级成 `new Error(String(error))`
(`[object Object]`),文案与类型一起丢掉。
- 前一版在渲染层加了 `isClientAuthError` / `getClientAuthErrorMessage` /
`presentAuthFailure` 三层:形状读取、文案回落、分类提示。它们既不是类型事实源,又在
`presentAuthFailure` 三层:形状读取、文案回落、分类提示(均已删除)。它们既不是类型事实源,又在
"取文案"里悄悄承担了"要不要上报"的判断,与"由调用方判定"的口径冲突。
## 决策
@@ -36,6 +36,10 @@ class ClientAuthFailure extends ClientActionError {
上报方不需要知道上游是谁。
- `payload` 是完整的结构化拒绝(判别联合),不是再抄一份的派生字段;`payload.type`
就是分流键。**不再新增 `kind` / `notice` / `severity` 之类的派生值。**
- 19 个具名载荷类型是有意保留的:它们是 §3 每个 `case` 里 `as X` 的目标,也正是"不要假设
所有变体字段相同"的落点。改成内联 struct 变体确实能让 ts-rs 把字段内联进联合成员、少掉
19 个生成文件,但每个分支就再也拿不到可 `as` 的具名类型(只能手写内联对象类型,等于放弃
ts-rs)。本 ADR 选择保留具名载荷。
### 2. 一个包装函数:`invokeClientAuth`
@@ -43,18 +47,29 @@ class ClientAuthFailure extends ClientActionError {
```ts
async function invokeClientAuth<T>(command, args): Promise<T> {
const invoke = requireInvoke(); // 认证桥未装:我们自己的失败关闭错误,原样抛出
try {
return await requireInvoke()(command, args);
return await invoke(command, args);
} catch (error) {
const rejection = error as { type?: unknown };
if (typeof rejection?.type !== 'string') throw error; // 非结构化:原样抛出
throw new ClientAuthFailure(...);
// 信任 tauri + ts-rs 的映射:认证命令的拒绝就是 ClientAuthError,不做运行时形状校验。
const rejection = error as { message?: unknown };
throw new ClientAuthFailure(
typeof rejection.message === 'string' ? rejection.message : '',
error as ClientAuthError,
command,
error,
);
}
}
```
- `type` 是 `string` 即认为是 Rust 结构化拒绝:形状由 ts-rs 保证,**不校验字段名与文案**。
- 非结构化拒绝原样抛出:那是 Tauri / JS 运行时自己的错误,不属于认证命令契约。
- **不做运行时形状嗅探**:不再检查 `type` 存不存在。Rust 与 TS 同包发布,形状由 ts-rs 保证;
出现别的形状属于 Tauri / Rust 侧的缺陷,`switch` 的 `default` 分支仍会把它抛出去上报,不会
静默吞掉——只是不再在包装层替 Tauri 兜底。
- `requireInvoke()` 放在 `try` 之外:认证桥未安装是我们自己的失败关闭错误,不是命令拒绝,保持
原样抛出(`需要在 Tauri App 内登录`)。
- `Error.message` 只从拒绝值里读一个可展示字符串(`typeof === 'string'`),**不是分流判据**;
分流永远只按 `payload.type`。
- 该包装是"Rust 结构化错误 → JS 错误对象"的唯一转换点:不做分类、不读文案判断、不兜底文案。
### 3. 判定只写在 catch 子句里,用具体变体