修复轮 1:邮件 GET 改归 ops,不撑大两端共用读路由名单

全仓测试 route-access.test.ts 钉着一份有意枚举、随 Keycloak spec 审过的名单:哪些读路由被两端共用。
之前把邮件 7 条 GET 标成 design|ops,名单从八条长到十四条。裁决改路由、不改名单:邮箱那一屏落在
运营中心,设计端没有消费方;入站正文装的是客户来往内容,ADR-012 已把「设计端能读客户主数据」记为
一处有意承担的代价,不再无故加宽。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TM4kUBvekALu9qqmp7D3wr
This commit is contained in:
xcs
2026-08-26 01:36:12 +08:00
co-authored by Claude Fable 5
parent 43bacffd90
commit c5cb779390
2 changed files with 13 additions and 13 deletions
@@ -1,6 +1,6 @@
/** /**
* 邮件路由的归属:15 条全部进契约,注册表建得起来(字面 / 参数模版互不撞), * 邮件路由的归属:15 条全部进契约,注册表建得起来(字面 / 参数模版互不撞),
* 读标两端、写归运营中心(设计稿 §7.2)。 * 读写都归运营中心(设计稿 §7.2 + 2026-08-26 裁决:设计端没有消费方,不再无故加宽)。
*/ */
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import * as contracts from '@contracts'; import * as contracts from '@contracts';
@@ -17,10 +17,9 @@ describe('邮件路由契约', () => {
} }
}); });
it('读标两端,写归 ops', () => { it('读写都归 ops', () => {
for (const route of MAIL_ROUTES) { for (const route of MAIL_ROUTES) {
if (route.method === 'GET') expect(route.access).toEqual(['design', 'ops']); expect(route.access).toBe('ops');
else expect(route.access).toBe('ops');
} }
}); });
@@ -28,6 +27,6 @@ describe('邮件路由契约', () => {
const registry = buildRouteRegistry(routesOf(Object.values(contracts))); const registry = buildRouteRegistry(routesOf(Object.values(contracts)));
expect(registry.lookup('POST', '/api/gov/mail/accounts/probe')).toBe('ops'); expect(registry.lookup('POST', '/api/gov/mail/accounts/probe')).toBe('ops');
expect(registry.lookup('POST', '/api/gov/mail/accounts/acc-1/probe')).toBe('ops'); expect(registry.lookup('POST', '/api/gov/mail/accounts/acc-1/probe')).toBe('ops');
expect(registry.lookup('GET', '/api/gov/mail/inbound/m-1/attachments/0')).toEqual(['design', 'ops']); expect(registry.lookup('GET', '/api/gov/mail/inbound/m-1/attachments/0')).toBe('ops');
}); });
}); });
+9 -8
View File
@@ -1,7 +1,8 @@
/** /**
* 邮件收发的路由契约(设计稿 §7.2;ADR-013)。 * 邮件收发的路由契约(设计稿 §7.2;ADR-013)。
* *
* 读标两端(设计中心要看通道对上了哪个信箱,运营中心管配置);写归运营中心。 * 读写都归运营中心(设计稿 §7.2 + 2026-08-26 裁决):邮箱那一屏落在运营中心,设计端没有消费方;
* 而入站正文装的是客户来往内容 —— ADR-012 已把「设计端能读客户主数据」记为一处后果,不再无故加宽。
* `/api/gov/*` 的写路由由 app 中间件自动广播 `{kind:'system', id:'gov'}`。 * `/api/gov/*` 的写路由由 app 中间件自动广播 `{kind:'system', id:'gov'}`。
* 探测 / 试发 / 拉取的**厂商失败不是接口错误**:200 的 body 里带原话(脱敏)。 * 探测 / 试发 / 拉取的**厂商失败不是接口错误**:200 的 body 里带原话(脱敏)。
*/ */
@@ -30,7 +31,7 @@ const listQuery = z.object({
export const listMailAccountsRoute = { export const listMailAccountsRoute = {
method: 'GET', method: 'GET',
path: '/api/gov/mail/accounts', path: '/api/gov/mail/accounts',
access: ['design', 'ops'], access: 'ops',
output: z.object({ accounts: z.array(mailAccountViewSchema) }), output: z.object({ accounts: z.array(mailAccountViewSchema) }),
} as const satisfies RouteContract; } as const satisfies RouteContract;
@@ -56,7 +57,7 @@ export const probeMailDraftRoute = {
export const getMailAccountRoute = { export const getMailAccountRoute = {
method: 'GET', method: 'GET',
path: '/api/gov/mail/accounts/:accountId', path: '/api/gov/mail/accounts/:accountId',
access: ['design', 'ops'], access: 'ops',
params: accountParams, params: accountParams,
output: mailAccountViewSchema, output: mailAccountViewSchema,
} as const satisfies RouteContract; } as const satisfies RouteContract;
@@ -109,7 +110,7 @@ export const sendTestMailRoute = {
export const listMailInboundRoute = { export const listMailInboundRoute = {
method: 'GET', method: 'GET',
path: '/api/gov/mail/inbound', path: '/api/gov/mail/inbound',
access: ['design', 'ops'], access: 'ops',
query: listQuery, query: listQuery,
output: z.object({ mails: z.array(mailInboundSummarySchema) }), output: z.object({ mails: z.array(mailInboundSummarySchema) }),
} as const satisfies RouteContract; } as const satisfies RouteContract;
@@ -117,7 +118,7 @@ export const listMailInboundRoute = {
export const getMailInboundRoute = { export const getMailInboundRoute = {
method: 'GET', method: 'GET',
path: '/api/gov/mail/inbound/:mailId', path: '/api/gov/mail/inbound/:mailId',
access: ['design', 'ops'], access: 'ops',
params: mailParams, params: mailParams,
output: mailInboundDetailSchema, output: mailInboundDetailSchema,
} as const satisfies RouteContract; } as const satisfies RouteContract;
@@ -126,7 +127,7 @@ export const getMailInboundRoute = {
export const mailAttachmentRoute = { export const mailAttachmentRoute = {
method: 'GET', method: 'GET',
path: '/api/gov/mail/inbound/:mailId/attachments/:index', path: '/api/gov/mail/inbound/:mailId/attachments/:index',
access: ['design', 'ops'], access: 'ops',
params: z.object({ mailId: z.string().min(1), index: z.coerce.number().int().min(0) }), params: z.object({ mailId: z.string().min(1), index: z.coerce.number().int().min(0) }),
output: z.object({ name: z.string(), contentType: z.string(), dataBase64: z.string() }), output: z.object({ name: z.string(), contentType: z.string(), dataBase64: z.string() }),
} as const satisfies RouteContract; } as const satisfies RouteContract;
@@ -135,7 +136,7 @@ export const mailAttachmentRoute = {
export const mailRawRoute = { export const mailRawRoute = {
method: 'GET', method: 'GET',
path: '/api/gov/mail/inbound/:mailId/raw', path: '/api/gov/mail/inbound/:mailId/raw',
access: ['design', 'ops'], access: 'ops',
params: mailParams, params: mailParams,
output: z.object({ name: z.string(), contentType: z.literal('message/rfc822'), dataBase64: z.string() }), output: z.object({ name: z.string(), contentType: z.literal('message/rfc822'), dataBase64: z.string() }),
} as const satisfies RouteContract; } as const satisfies RouteContract;
@@ -153,7 +154,7 @@ export const redeliverMailInboundRoute = {
export const listMailOutboundRoute = { export const listMailOutboundRoute = {
method: 'GET', method: 'GET',
path: '/api/gov/mail/outbound', path: '/api/gov/mail/outbound',
access: ['design', 'ops'], access: 'ops',
query: listQuery, query: listQuery,
output: z.object({ mails: z.array(mailOutboundViewSchema) }), output: z.object({ mails: z.array(mailOutboundViewSchema) }),
} as const satisfies RouteContract; } as const satisfies RouteContract;