@pylonts/dsl 1.1.15 → 1.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,136 @@
1
+ # gen-login 生成器设计(登录链统一)
2
+
3
+ > 状态:**已实现(2026-08-19)**
4
+ > 关联文档:[token.md](./token.md)(token 令牌体系,底座)、[wechat.md](./wechat.md)(微信小程序登录体系,消费本设计)
5
+ > 定位:把 `gen admin-login`(仅 admin 账密)升级为 `gen login`——按 app 类型(admin / wxmini / mobile)分叉生成登录链,读取同一份 `login.config.ts`,每个 app 一份配置。
6
+
7
+ ## 1. 背景与动机
8
+
9
+ 现状 `gen admin-login` 只服务 admin 类型 app(`derive()` 硬校验 `app.type !== 'admin'` 报错),微信小程序登录链(wechat.md 形态 1 静默 / 形态 2 静默+账密)没有生成器。统一方案:**命令改名 + 配置按 app 类型分叉**。
10
+
11
+ ## 2. 命令
12
+
13
+ ```
14
+ pylonts gen admin-login → pylonts gen login
15
+ ```
16
+
17
+ - cli:`gen-cli.ts` 注册名 `'admin-login'` → `'login'`;`cli/src/admin-login.ts` → `cli/src/login.ts`(runAdminLogin → runLogin);
18
+ - gen:`gen-admin-login.ts` → `gen-login.ts`,导出 `generateAdminLogin` → `generateLogin`;
19
+ - **兼容**:`gen admin-login` 保留 alias(一个版本周期);gen 包 re-export 旧符号 `AdminLoginConfig = AdminLoginAppConfig`、`generateAdminLogin = generateLogin`。
20
+
21
+ ## 3. 配置类型(LoginConfig)
22
+
23
+ `LoginConfig = Record<string, LoginAppConfig>`——key = app/module 名(project.config.ts app name),结构与现状一致(记录式),值类型按 app 类型分叉。
24
+
25
+ ### 3.1 类型定义(判别联合,password 分支 = Admin 超集)
26
+
27
+ ```ts
28
+ /** 公共(所有登录形态) */
29
+ export interface LoginBaseConfig {
30
+ table: TableSchema; // 业务账号表(身份表,必须含 token/refresh_token/login_at 三列,决策 #13)
31
+ token: TokenSchema; // TokenSchema(identity 锚点,必须投影登录表 PK)
32
+ bootstrapSecret: string; // 初始密钥方案 2(登录入口无 token 验签)
33
+ refreshKeySalt: string; // refreshToken 派生密钥盐
34
+ }
35
+
36
+ /** 账密字段(admin 全部;wx password 模式复用——不复制) */
37
+ export interface PasswordFieldsConfig {
38
+ usernameField: Field;
39
+ passwordField: Field;
40
+ statusField: Field;
41
+ statusActiveValue: string | number;
42
+ seedUsername?: string;
43
+ seedPassword?: string;
44
+ }
45
+
46
+ /** admin 账密登录(与现状 AdminLoginConfig 字段完全一致) */
47
+ export type AdminLoginAppConfig = LoginBaseConfig & PasswordFieldsConfig;
48
+
49
+ /** wx 特有字段(@pylonts/wechat + {app}_wx 绑定表) */
50
+ export interface WxFieldsConfig {
51
+ wxTable: TableSchema; // {app}_wx 绑定表(appid + openid 联合主键,见 wechat.md §3.1)
52
+ wechat: {
53
+ appId: string;
54
+ appSecret: string;
55
+ mock?: boolean;
56
+ }; // @pylonts/wechat 配置(getAccessToken 本地缓存,无需外部 store,见 wechat.md §4.2)
57
+ sessionKeyEncKey: string; // session_key 落库加密密钥(AES-256-GCM,merge 进 config.ts auth.token.sessionKeyEncKey)
58
+ }
59
+
60
+ /** wxmini 登录:silent 无账密;password = AdminLoginAppConfig + wx 字段(超集) */
61
+ export type WxLoginAppConfig =
62
+ | (LoginBaseConfig & WxFieldsConfig & { mode: 'silent' })
63
+ | (AdminLoginAppConfig & WxFieldsConfig & { mode: 'password' });
64
+
65
+ /** 匿名签名(MVP 2026-08-19):只发安全材料 token({ token, secret }),无身份、无 refreshToken
66
+ * (token.md #11)。无账户表——不继承 LoginBaseConfig。接口本身用 bootstrap secret 验签
67
+ * (初始密钥方案 2)。任意 app.type 可配(admin/mobile/wxmini)。 */
68
+ export interface SignAppConfig {
69
+ mode: 'sign';
70
+ bootstrapSecret: string; // 初始密钥方案 2(登录入口无 token 验签)
71
+ }
72
+
73
+ /** 判别联合:derive 分支 switch 后类型自动收窄 */
74
+ export type LoginAppConfig = AdminLoginAppConfig | WxLoginAppConfig | SignAppConfig;
75
+ export type LoginConfig = Record<string, LoginAppConfig>;
76
+ ```
77
+
78
+ **设计要点**:
79
+ - **password 分支 = `AdminLoginAppConfig & WxFieldsConfig & { mode }`**——字面即"Admin 的超集",账密字段不复制、类型上强制完整(usernameField/passwordField/statusField 必填,不可能漏);
80
+ - **silent 分支精确无账密**——静默登录(wechat.md 形态 1)没有用户名密码,类型层不允许填;
81
+ - **`mode` 是判别字段**——derive 里 `switch(app.type)` + `switch(mode)` 类型自动收窄,silent 分支访问 `config.usernameField` 直接编译报错;
82
+ - **字段存在性不用运行时判断**——判别联合把"哪种形态有哪些字段"钉在类型层。
83
+
84
+ ### 3.2 兼容性
85
+
86
+ | 维度 | 结论 |
87
+ |------|------|
88
+ | 配置结构(admin) | **完全兼容**——字段一字不差,现有 `login.config.ts` 无需改动(`satisfies LoginConfig` 自动判定 admin 分支,不需加 mode) |
89
+ | `LoginConfig` 类型名 | 保留,`import type { LoginConfig }` 不破 |
90
+ | `AdminLoginConfig` 类型名 | 改 `AdminLoginAppConfig`——re-export alias 兼容(一个版本周期) |
91
+ | 生成产物(admin 形态) | DTO/Entity/DAO/Service/Controller/seed 结构与现状一致 |
92
+ | CLI 命令名 | `gen admin-login` → `gen login`(保留 alias 兼容) |
93
+
94
+ ## 4. 验证规则(按 app 类型分支)
95
+
96
+ `derive()` 现在硬校验 `app.type !== 'admin'`——改为按 app.type 分支:
97
+
98
+ | app.type | 允许形态 | 验证要点 |
99
+ |----------|---------|---------|
100
+ | `admin` | 账密(`AdminLoginAppConfig`)或 `sign` | 账密:现有全量校验(username/password NOT NULL、status enum、statusActiveValue ∈ enum、bootstrapSecret/refreshKeySalt 非空、seed 可选);sign:仅 bootstrapSecret 非空 |
101
+ | `wxmini` | `mode: 'silent'` / `'password'` / `'sign'` | **账密/静默公共**:业务账号表三列(token/refresh_token/login_at)、wxTable 必须为 `{app.name}_wx`(表名 = app name + `_wx`,snake)且含 `appid`+`openid` 联合主键、wechat 配置非空;**password**:复用 admin 账密全量校验(NOT NULL/enum/seed);**silent**:无账密校验、无 seed;**sign**:无表无 wx 约束(仅 bootstrapSecret 非空) |
102
+ | `mobile` | 账密(同 admin,待定)或 `sign` | 同 admin;sign 同上 |
103
+
104
+ **配置与 app 类型不符 → 定义期报错**(如 wxmini app 配了账密但没 wxTable、admin app 配了 wxTable)。sign 是唯一 app.type 无关形态(`resolveLoginKind` 最先短路返回 `'sign'`)。
105
+
106
+ ## 5. 生成产物差异
107
+
108
+ | 产物 | admin 账密 | wxmini silent | wxmini password | sign |
109
+ |------|-----------|---------------|-----------------|------|
110
+ | DTO | LoginRequest{username,password} / RefreshRequest{refreshToken} / LoginResponse{token,refreshToken,secret,user} | LoginRequest{code} | LoginRequest{code,username,password} | SignResponse{token,secret}(无请求体,无 refreshToken) |
111
+ | Service.login | 账密校验 + 单点确认 + 发 token(现有逻辑) | code2Session → 查/建 {app}_wx 行 → 单点确认 + 发 token | code2Session + 账密校验 + bind wx + 单点确认 + 发 token | —(只有 sign()) |
112
+ | Service.sign | — | — | — | `generateSecret + generateCipher → createToken(app, {secret, cipher}) → { token, secret }`(Redis 对象含 cipher,响应只外发 secret;不 attachIdentity) |
113
+ | Service 内部 | 无微信 | `@pylonts/wechat` code2Session + session_key 加密落库(AES-256-GCM) | 同 silent + bcrypt | 无表无 DAO |
114
+ | Controller | `@Rpc('login') + @LoginEntry('{app}')` login/refresh | 同 | 同 | `@Rpc('login') + @LoginEntry('{app}')` 仅 sign(无 @Body) |
115
+ | Entity / DAO | ✅ | ✅ | ✅ | ❌(无账户表) |
116
+ | seed SQL | ✅(初始账号) | 不需要 | 可选 | ❌ |
117
+ | config merge | bootstrapSecrets + refreshKeySalt | + sessionKeyEncKey | + sessionKeyEncKey | 仅 bootstrapSecrets(无 refresh/session 配置) |
118
+
119
+ refresh 三种账密/静默形态一致(token 决策 #11:请求体上送 refreshToken、7 天滑动窗口、每次轮换);sign 无 refresh(决策 #11:获取 token 接口不返回 refreshToken)。
120
+
121
+ ## 6. 落地步骤(已完成)
122
+
123
+ | 步骤 | 内容 | 状态 |
124
+ |------|------|------|
125
+ | 1 | `gen/src/gen-login.ts`:类型重构(LoginBaseConfig / PasswordFieldsConfig / AdminLoginAppConfig / WxFieldsConfig / WxLoginAppConfig / LoginConfig)+ `derive()` 按 app.type 分支验证 | ✅ |
126
+ | 2 | 生成器分叉:wxmini 形态产物(LoginRequest{code}/code2Session/session_key 加密落库/bind wx)——消费 `@pylonts/wechat` | ✅ |
127
+ | 3 | cli:`gen-cli.ts` 命令改名 + alias;`cli/src/login.ts` | ✅ |
128
+ | 4 | 文档同步:onboarding.md 阶段 2(`gen admin-login` → `gen login`)、guide/login.md、wechat.md §9/§10 | ✅ |
129
+ | 5 | 测试:gen-login 测试(admin 兼容 / wx silent / wx password / 类型不符报错) | ✅(221 全绿) |
130
+ | 6 | sign 形态(MVP 2026-08-19):SignAppConfig + signDriver(无表无 DAO 无 seed,service 只 createToken + 响应 {token, secret})+ mergeConfigToken 引号 key 修复 | ✅(225 全绿,含 sign 3 个 + 重跑不重复 key 回归) |
131
+
132
+ ## 7. 边界
133
+
134
+ - 不做 mobile 静默登录(native app 场景待定,先账密);
135
+ - sign 只做匿名签发(无身份 token),**真实登录(手机号)/ 身份升级 / 静默登录分离** 未做(规划中:真实登录 = 静默身份 + getPhoneNumber 手机号 → 升级 token,`rotateToken` + `attachIdentity` 已具备);
136
+ - 不做 wx 前端客户端生成(gen-client 已支持 loginPath/refreshPath,wx 会话管理由前端模板负责)。
@@ -1,152 +1,202 @@
1
- # 定义第三方服务(ThirdServiceSchema)
2
-
3
- 第三方服务适配器契约(如微信支付 tenpay、短信、文件存储)。`defineThirdService` 声明适配器类契约:构造函数配置 + 方法列表。
4
-
5
- > 完整对接流程(前置准备 → 契约化 → gen third → 填充 client → 沙箱验证)见 [methodology/third-party-integration.md](../../docs/methodology/third-party-integration.md)。
6
-
7
- 与两个相近概念区分:
8
-
9
- - `ServiceSchema`(service_schema/)——后端业务服务;
10
- - `ThirdApiSchema`(project.config.ts `thirdApis`)——项目拓扑:第三方系统的目录归属,如 `wx/`、`ble/`。
11
-
12
- `ThirdServiceSchema.schema` 引用 `ThirdApiSchema` 实例(拓扑引用)。第三方方法实现在外部系统,仅声明契约、不建模内部流程。
13
-
14
- ## 定义
15
-
16
- ```ts
17
- import { buildInput, buildOutput, CodeException, defineThirdService, dtoField, intField, IOException, stringField } from '@pylonts/dsl';
18
- import { wx } from '../project.config';
19
-
20
- const totalFee = intField({ optional: false, label: '金额(分)' });
21
-
22
- export const wxPayService = defineThirdService({
23
- schema: wx,
24
- name: 'WxPayService',
25
- description: '微信支付服务(tenpay APIv2)',
26
- methods: {
27
- getPayParams: {
28
- args: buildInput('PayParams', {
29
- out_trade_no: dtoField(stringField({ maxLength: 32, optional: false, label: '订单号' })),
30
- total_fee: dtoField(totalFee),
31
- // Same fact and same type as the entity column — shared instance.
32
- openid: dtoField(user.columns.openid),
33
- }),
34
- results: buildOutput('PayParamsResult', {
35
- appId: dtoField(stringField({ optional: false, label: 'appId' })),
36
- paySign: dtoField(stringField({ optional: false, label: '签名' })),
37
- }),
38
- throws: [CodeException, IOException],
39
- description: '获取支付参数',
40
- },
41
- },
42
- });
43
- ```
44
-
45
- - `methods` 是 **map**:key 即方法名(构建器写回 `method.name`),value 为 `ThirdServiceMethodDef`。
46
- - 每个方法的 `args` / `results` 各是一个 `DtoMessage`,用 `buildInput` / `buildOutput` 构建。`buildInput` 构建 `args`(输入消息),`buildOutput` 构建 `results`(输出消息)——与业务 DTO 同一 POJO 聚合,字段以 `dtoField(field)` 包装,map key 即线格式(wire-format)字段名,**原样保留协议拼写**(`out_trade_no`、`appId`,不做 camelCase)。
47
- - `throws`(**必填**):每个方法必须声明 **`CodeException` + `IOException`** 两个异常——`CodeException`(第三方返回的业务错误码)与 `IOException`(网络/超时/不可恢复故障)。两个异常都来自 `@pylonts/core`,**不能自定义、不能替换**(`pylonts gen third` 会校验,缺失即报错拒绝生成)。原因:第三方集成有两类必然失败——业务层失败(第三方返回错误码,需转译给调用方)与传输层失败(网络/超时,需按故障重试或上报),client 骨架的异常翻译依赖这两个契约。
48
- - `description`(可选):服务或方法说明。
49
-
50
- 字段与本地实体列/其他消息字段的关系,两个通道,按"同一事实"的表达方式选择:
51
-
52
- ### 同一概念且类型一致 → 共享实例
53
-
54
- 直接复用本地实体列实例,类型/语义/默认值自动跟随实体,DTO 投影继承全部语义:
55
-
56
- ```ts
57
- args: {
58
- name: 'PayParams',
59
- fields: {
60
- openid: dtoField(user.columns.openid), // user 是 TableSchema 实例,此处复用其 openid 列的 Field 对象
61
- },
62
- },
63
- ```
64
-
65
- 线格式字段名(map key)与列名无需一致——key 是协议拼写,value 是任意 Field 实例,两者解耦。协议叫 `userId`、列叫 `user_id` 照样共享:
66
-
67
- ```ts
68
- fields: {
69
- userId: dtoField(user.columns.user_id), // key 按协议拼写,value 复用列实例
70
- },
71
- ```
72
-
73
- > `user` 是 `schema/user.table.ts` 中 `export const user = defineTable('user', { ... })` 导出的 **TableSchema 实例**(`user.columns` 是它的列 map,`user.columns.openid` 是该表 `openid` 列的 Field 实例)。共享实例即把**同一个 Field 对象**放入消息字段,类型/语义/默认值全部跟随表定义。
74
-
75
- ### 同一概念但类型/格式不同 自有字段
76
-
77
- 声明自有线格式类型:
78
-
79
- ```ts
80
- const totalFee = intField({ optional: false, label: '金额(分)' });
81
-
82
- fields: {
83
- total_fee: dtoField(totalFee),
84
- },
85
- ```
86
-
87
- wire 字段与本地字段之间的换算/映射(分↔元、加密、脱敏)由 convert 防腐层承载,`FieldRuleSchema` 换算规则为规划能力、尚未接入消息绑定。
88
-
89
- ## 嵌套字段
90
-
91
- 线格式字段支持递归嵌套,用 `objectField` / `arrayField`(Field 体系,非表列):
92
-
93
- ```ts
94
- fields: {
95
- payer_info: dtoField(objectField({
96
- properties: {
97
- openid: stringField({ optional: false, maxLength: 64 }),
98
- },
99
- })),
100
- coupons: dtoField(arrayField({ items: intField() })),
101
- },
102
- ```
103
-
104
- 表列不支持这两个类型(`buildCreateTableSql` 直接报错,定义期即拦截)。
105
-
106
- ## 枚举与异常
107
-
108
- 第三方消息字段可用 `enumField` 挂 `defineEnum` 枚举,两者与 `defineThirdService` 定义在同一源文件中(named export),供 `pylonts gen third` 生成枚举产物。
109
-
110
- 异常不在此处定义——方法 `throws` 固定声明 `@pylonts/core` `CodeException` + `IOException`(见上文「定义」一节),`pylonts gen third` 强校验。
111
-
112
- ## 存储与生成
113
-
114
- - 声明:`third_schema/{thirdApi.name}/*.third-service.ts`——一文件一服务(named export),目录名 = project.config.ts 的 `thirdApis` 实例名。
115
- - 生成:`pylonts gen third`——对每个 thirdApi,扫描 `third_schema/{name}/`,按三步产出到 `third/{name}/`:
116
- 0. **throws 校验**(生成前置闸门):每个方法必须声明 `CodeException` + `IOException`,缺失即报错列出违规方法,不写任何产物;
117
- 1. **枚举**:模块导出的 `defineEnum` 实例 → `third/{name}/enums/{JsName}.enum.ts`(复用 enum-driver 的 `renderEnum`,与表枚举同一渲染),一 jsName 一文件;
118
- 2. **DTO**:每方法 args/results 用 typebox-driver 渲染 TypeBox 消息 + Static 类型,**一源文件一生成文件**,输出 `third/{name}/{stem}.third-service.gen.ts`(覆盖写);
119
- 3. **客户端骨架**:每服务渲染一个 class(构造配置接口 + 每方法 async 签名 + Not-implemented throw),输出 `third/{name}/{stem}.client.ts`——**已存在则跳过**(方法体是用户填充的),`--force` 覆盖。
120
- - DTO 的枚举字段 import 走**相对路径** `./enums/{JsName}.enum`(DTO enums/ 同处 `third/{name}/` 下,`moduleResolution: bundler` 解析 `.enum.ts`),不依赖根 `enums/` 子包。
121
- - 生成物目录是子包:`third/` 目录带 `package.json`,`exports` 声明 `*.third-service.gen` 子路径,供 convert 产物 import。
122
-
123
- ## 客户端骨架是生成的,方法体是手写的
124
-
125
- `defineThirdService` 只描述**契约**(构造配置 + 方法列表)。`gen third` 生成的 `{name}.client.ts` 是一个**骨架**:导出 `{Service}Config` 接口(TODO 注释标注 transport 配置——baseUrl/凭据/密钥属外部实现,不在契约内)+ `{Service}` 类(constructor 空实现),每方法带完整签名(args/results 类型从同名 `.third-service.gen` import type)与 `throw new Error('Not implemented: ...')` stub(含 `// @gen:stub` 标记,与 gen-service 骨架同一套 marker 约定)。**签名/throws 注释由生成器保证与契约同步,方法体、构造配置、签名加密等外部交互由人工填充**——已存在文件默认跳过(避免覆盖人工实现),`--force` 才重写。
126
-
127
- ## convert 防腐接线
128
-
129
- 第三方消息(`args`/`results` 是 `DtoMessage`,天然满足 `ConvertSourceSchema`)可直接作为 convert 的**源或目标**,用于 wire 消息 ↔ 本地模型的防腐翻译。```ts
130
- // convert_schema/{api.name}/{app.name}/convert/wx-pay.convert.ts
131
- import { wxPayService } from '../../../third_schema/wx/wxpay.third-service';
132
-
133
- const getPayParams = wxPayService.methods.getPayParams;
134
-
135
- export const wxPayConvert = defineConvert({
136
- name: 'WxPayConvert',
137
- api,
138
- app: admin,
139
- methods: {
140
- toPayParams: {
141
- sources: [order], // 本地订单表 → wire 请求
142
- target: getPayParams.args,
143
- },
144
- toLocalPayResult: {
145
- sources: [getPayParams.results], // wire 响应 → 本地 DTO
146
- target: WxPayParamsResultDto,
147
- },
148
- },
149
- });
150
- ```
151
-
1
+ # 定义第三方服务(ThirdServiceSchema)
2
+
3
+ 第三方服务适配器契约(如微信支付 tenpay、短信、文件存储)。`defineThirdService` 声明适配器类契约:构造函数配置 + 方法列表。
4
+
5
+ > 完整对接流程(前置准备 → 契约化 → gen third → 填充 client → 沙箱验证)见 [methodology/third-party-integration.md](../../docs/methodology/third-party-integration.md)。
6
+
7
+ 与两个相近概念区分:
8
+
9
+ - `ServiceSchema`(service_schema/)——后端业务服务;
10
+ - `ThirdApiSchema`(project.config.ts `thirdApis`)——项目拓扑:第三方系统的目录归属,如 `wx/`、`ble/`。
11
+
12
+ `ThirdServiceSchema.schema` 引用 `ThirdApiSchema` 实例(拓扑引用)。第三方方法实现在外部系统,仅声明契约、不建模内部流程。
13
+
14
+ ## 定义
15
+
16
+ ```ts
17
+ import { buildInput, buildOutput, CodeException, defineThirdService, dtoField, intField, IOException, stringField } from '@pylonts/dsl';
18
+ import { wx } from '../project.config';
19
+
20
+ const totalFee = intField({ optional: false, label: '金额(分)' });
21
+
22
+ export const wxPayService = defineThirdService({
23
+ schema: wx,
24
+ name: 'WxPayService',
25
+ description: '微信支付服务(tenpay APIv2)',
26
+ methods: {
27
+ getPayParams: {
28
+ args: buildInput('PayParams', {
29
+ out_trade_no: dtoField(stringField({ maxLength: 32, optional: false, label: '订单号' })),
30
+ total_fee: dtoField(totalFee),
31
+ // Same fact and same type as the entity column — shared instance.
32
+ openid: dtoField(user.columns.openid),
33
+ }),
34
+ results: buildOutput('PayParamsResult', {
35
+ appId: dtoField(stringField({ optional: false, label: 'appId' })),
36
+ paySign: dtoField(stringField({ optional: false, label: '签名' })),
37
+ }),
38
+ throws: [CodeException, IOException],
39
+ description: '获取支付参数',
40
+ },
41
+ },
42
+ });
43
+ ```
44
+
45
+ - `methods` 是 **map**:key 即方法名(构建器写回 `method.name`),value 为 `ThirdServiceMethodDef`。
46
+ - 每个方法的 `args` / `results` 各是一个 `DtoMessage`,用 `buildInput` / `buildOutput` 构建。`buildInput` 构建 `args`(输入消息),`buildOutput` 构建 `results`(输出消息)——与业务 DTO 同一 POJO 聚合,字段以 `dtoField(field)` 包装,map key 即线格式(wire-format)字段名,**原样保留协议拼写**(`out_trade_no`、`appId`,不做 camelCase)。
47
+ - `throws`(**必填**):每个方法必须声明 **`CodeException` + `IOException`** 两个异常——`CodeException`(第三方返回的业务错误码)与 `IOException`(网络/超时/不可恢复故障)。两个异常都来自 `@pylonts/core`,**不能自定义、不能替换**(`pylonts gen third` 会校验,缺失即报错拒绝生成)。原因:第三方集成有两类必然失败——业务层失败(第三方返回错误码,需转译给调用方)与传输层失败(网络/超时,需按故障重试或上报),client 骨架的异常翻译依赖这两个契约。
48
+ - `format`(可选):网关**数据格式**——`'form'`(urlencoded)/ `'json'`(默认)/ 自定义字符串。client(出站加密/签名)与 sandbox(入站解密/验签)是同一协议的一对镜像,格式在声明中指定一次,两边的生成骨架都读它。
49
+ - 方法级 `service`(可选):发往网关的 **wire service 名**(如方法 key `uploadImage` → `service: 'pic_upload'`),默认 = 方法 key。camelCase key 与 snake_case wire 名不一致时必须显式声明——sandbox endpoint 的路由靠它匹配。
50
+ - `description`(可选):服务或方法说明。
51
+
52
+ 字段与本地实体列/其他消息字段的关系,两个通道,按"同一事实"的表达方式选择:
53
+
54
+ ### 同一概念且类型一致 → 共享实例
55
+
56
+ 直接复用本地实体列实例,类型/语义/默认值自动跟随实体,DTO 投影继承全部语义:
57
+
58
+ ```ts
59
+ args: {
60
+ name: 'PayParams',
61
+ fields: {
62
+ openid: dtoField(user.columns.openid), // user 是 TableSchema 实例,此处复用其 openid 列的 Field 对象
63
+ },
64
+ },
65
+ ```
66
+
67
+ 线格式字段名(map key)与列名无需一致——key 是协议拼写,value 是任意 Field 实例,两者解耦。协议叫 `userId`、列叫 `user_id` 照样共享:
68
+
69
+ ```ts
70
+ fields: {
71
+ userId: dtoField(user.columns.user_id), // key 按协议拼写,value 复用列实例
72
+ },
73
+ ```
74
+
75
+ > `user` `schema/user.table.ts` 中 `export const user = defineTable('user', { ... })` 导出的 **TableSchema 实例**(`user.columns` 是它的列 map,`user.columns.openid` 是该表 `openid` 列的 Field 实例)。共享实例即把**同一个 Field 对象**放入消息字段,类型/语义/默认值全部跟随表定义。
76
+
77
+ ### 同一概念但类型/格式不同 → 自有字段
78
+
79
+ 声明自有线格式类型:
80
+
81
+ ```ts
82
+ const totalFee = intField({ optional: false, label: '金额(分)' });
83
+
84
+ fields: {
85
+ total_fee: dtoField(totalFee),
86
+ },
87
+ ```
88
+
89
+ wire 字段与本地字段之间的换算/映射(分↔元、加密、脱敏)由 convert 防腐层承载,`FieldRuleSchema` 换算规则为规划能力、尚未接入消息绑定。
90
+
91
+ ## 嵌套字段
92
+
93
+ 线格式字段支持递归嵌套,用 `objectField` / `arrayField`(Field 体系,非表列):
94
+
95
+ ```ts
96
+ fields: {
97
+ payer_info: dtoField(objectField({
98
+ properties: {
99
+ openid: stringField({ optional: false, maxLength: 64 }),
100
+ },
101
+ })),
102
+ coupons: dtoField(arrayField({ items: intField() })),
103
+ },
104
+ ```
105
+
106
+ 表列不支持这两个类型(`buildCreateTableSql` 直接报错,定义期即拦截)。
107
+
108
+ ## 枚举与异常
109
+
110
+ 第三方消息字段可用 `enumField` `defineEnum` 枚举,两者与 `defineThirdService` 定义在同一源文件中(named export),供 `pylonts gen third` 生成枚举产物。
111
+
112
+ 异常不在此处定义——方法 `throws` 固定声明 `@pylonts/core` 的 `CodeException` + `IOException`(见上文「定义」一节),`pylonts gen third` 强校验。
113
+
114
+ ## 回调(callbacks,第三方 平台推送)
115
+
116
+ `defineThirdService` 的 `callbacks` 字段建模**入站推送**:第三方审核完成/状态变更后主动 POST 到平台(如银联自助签约 3.8 入网审核结果推送、3.12 变更推送)。与 `methods`(平台 第三方出站)方向相反,语义是"第三方发来报文,平台应答"。
117
+
118
+ ```ts
119
+ import { buildInput, buildOutput, defineThirdCallback, defineThirdService, dtoField, stringField } from '@pylonts/dsl';
120
+ import { api, unionpay } from '../project.config';
121
+
122
+ export const unionpaySigningService = defineThirdService({
123
+ schema: unionpay,
124
+ name: 'UnionpaySigningService',
125
+ methods: {
126
+ // ... 出站方法(3.1 图片上传等)
127
+ },
128
+ callbacks: {
129
+ applyNotify: defineThirdCallback({
130
+ api, // 生成到 api/src/modules/unionpay/controller/
131
+ service: 'apply_notify', // 第三方推送的 service 名(固定传输值)
132
+ payload: buildInput('ApplyNotifyPayload', {
133
+ ums_reg_id: dtoField(stringField({ maxLength: 30, optional: false })),
134
+ apply_status: dtoField(enumField({ enum: ApplyStatus, optional: true })),
135
+ // ... 推送报文字段
136
+ }),
137
+ response: buildOutput('NotifyResponse', {
138
+ res_code: dtoField(stringField({ maxLength: 10, optional: false, label: '0000 成功' })),
139
+ }),
140
+ description: '入网审核结果推送',
141
+ }),
142
+ },
143
+ });
144
+ ```
145
+
146
+ - `callbacks` 是 **map**:key 即回调方法名(构建器写回 `callback.name`),value 由 **`defineThirdCallback`** 构建。
147
+ - **`api`(必填)**:回调 controller 生成进哪个后端——`{api}/src/modules/{thirdApi.name}/controller/{ServiceName}CallbackController.ts`。这是 **@Public 白名单目录**(`lint controller`:`src/modules/{thirdApi}/controller/` 下允许 @Public,thirdApi 名来自 project.config.ts)——第三方调用不持有平台 appKey,平台在 controller 方法体内验第三方的自有签名。
148
+ - **`service`(必填)**:第三方推送的服务名(如 `apply_notify`、`alter_notify`),供回调实现按 service 分发。
149
+ - **`payload` / `response`**:各是一个 `DtoMessage`(`buildInput`/`buildOutput`),与 `methods` 的 args/results 同一套 POJO 聚合,wire 字段名原样保留协议拼写;`payload` 是第三方推来的报文,`response` 是平台应答报文(如 `{ res_code: '0000' }`)。
150
+ - `description`(可选):回调说明,渲染为 `@Response` 的 OpenAPI 描述。
151
+
152
+ 生成物(`pylonts gen third` 一并产出):
153
+
154
+ | 产物 | 路径 | 性质 |
155
+ |---|---|---|
156
+ | payload/response TypeBox 消息 | `third/{thirdApi}/{stem}.third-service.gen.ts`(与出站 DTO 同文件) | 覆盖写 |
157
+ | 回调 controller 骨架 | `{api}/src/modules/{thirdApi}/controller/{ServiceName}CallbackController.ts` | 已存在合并(用户方法体保留),`--force` 覆盖 |
158
+
159
+ 回调 controller 是 `@Rpc('{serviceStem}Callback')` + 每方法 `@Public()` + `@Body(payload)` + `@Response(desc, response)` 的 RPC 类;**方法体是手写面**:验第三方签名/解密(与出站 client 的密钥对称)、按 `service` 分发、落库/通知业务,最后返回 `response` 消息。RPC 路由为 `POST /{urlPrefix}/{rpcName}/{method}`,第三方按文档推送即可。
160
+
161
+ ## 存储与生成
162
+
163
+ - 声明:`third_schema/{thirdApi.name}/*.third-service.ts`——一文件一服务(named export),目录名 = project.config.ts 的 `thirdApis` 实例名。
164
+ - 生成:`pylonts gen third`——对每个 thirdApi,扫描 `third_schema/{name}/`,按三步产出到 `third/{name}/`:
165
+ 0. **throws 校验**(生成前置闸门):每个方法必须声明 `CodeException` + `IOException`,缺失即报错列出违规方法,不写任何产物;
166
+ 1. **枚举**:模块导出的 `defineEnum` 实例 → `third/{name}/enums/{JsName}.enum.ts`(复用 enum-driver 的 `renderEnum`,与表枚举同一渲染),一 jsName 一文件;
167
+ 2. **DTO**:每方法 args/results 用 typebox-driver 渲染 TypeBox 消息 + Static 类型,**一源文件一生成文件**,输出 `third/{name}/{stem}.third-service.gen.ts`(覆盖写);
168
+ 3. **客户端骨架**:每服务渲染一个 class(构造配置接口 + 每方法 async 签名 + Not-implemented throw),输出 `third/{name}/{stem}.client.ts`——已存在则**合并**(用户填充的方法体保留、签名/DTO import 刷新),`--force` 覆盖;
169
+ 4. **沙箱网关配置**(可选):每 thirdApi 渲染 `sandbox/{name}/config.ts`(defineSandboxConfig 骨架)——每方法一个 endpoint(`service` 名 = 声明的 wire service、`format` = 声明的数据格式、`method` 默认 POST、handler stub `res_code: '0000'`),已存在则**合并**(用户填充的 handler 保留、契约新增接口自动追加、移除接口删除),`--force` 覆盖;**无生成标记的手写 config 保留不动**(cli 打印 `- (hand-written, delete to regenerate)` 提示)。encoder(解密/验签)与 GATEWAY baseUrl 是手写面,不在生成范围。
170
+ - DTO 的枚举字段 import 走**相对路径** `./enums/{JsName}.enum`(DTO 与 enums/ 同处 `third/{name}/` 下,`moduleResolution: bundler` 解析 `.enum.ts`),不依赖根 `enums/` 子包。
171
+ - 生成物目录是子包:`third/` 目录带 `package.json`,`exports` 声明 `*.third-service.gen` 子路径,供 convert 产物 import。
172
+
173
+ ## 客户端骨架是生成的,方法体是手写的
174
+
175
+ `defineThirdService` 只描述**契约**(构造配置 + 方法列表)。`gen third` 生成的 `{name}.client.ts` 是一个**骨架**:导出 `{Service}Config` 接口(TODO 注释标注 transport 配置——baseUrl/凭据/密钥属外部实现,不在契约内)+ `{Service}` 类(constructor 空实现),每方法带完整签名(args/results 类型从同名 `.third-service.gen` import type)与 `throw new Error('Not implemented: ...')` stub(含 `// @gen:stub` 标记,与 gen-service 骨架同一套 marker 约定)。**签名/throws 注释由生成器保证与契约同步,方法体、构造配置、签名加密等外部交互由人工填充**——已存在文件默认跳过(避免覆盖人工实现),`--force` 才重写。
176
+
177
+ ## convert 防腐接线
178
+
179
+ 第三方消息(`args`/`results` 是 `DtoMessage`,天然满足 `ConvertSourceSchema`)可直接作为 convert 的**源或目标**,用于 wire 消息 ↔ 本地模型的防腐翻译。```ts
180
+ // convert_schema/{api.name}/{app.name}/convert/wx-pay.convert.ts
181
+ import { wxPayService } from '../../../third_schema/wx/wxpay.third-service';
182
+
183
+ const getPayParams = wxPayService.methods.getPayParams;
184
+
185
+ export const wxPayConvert = defineConvert({
186
+ name: 'WxPayConvert',
187
+ api,
188
+ app: admin,
189
+ methods: {
190
+ toPayParams: {
191
+ sources: [order], // 本地订单表 → wire 请求
192
+ target: getPayParams.args,
193
+ },
194
+ toLocalPayResult: {
195
+ sources: [getPayParams.results], // wire 响应 → 本地 DTO
196
+ target: WxPayParamsResultDto,
197
+ },
198
+ },
199
+ });
200
+ ```
201
+
152
202
  convert 文件绑定第三方服务身份时按 `{third-service}.convert.ts` 命名(上例 `wx-pay.convert.ts` 对应 `wxpay.third-service.ts` 的 `WxPayService`)。
@@ -0,0 +1,60 @@
1
+ # HMAC 签名 + JWT → token 令牌体系迁移对照
2
+
3
+ > 状态:**已落地**(token 体系为唯一鉴权体系,旧体系完全退役)
4
+ > 关联:[token.md](token.md)(体系总文档)、`pylon-fastify/src/common/auth/TokenStrategy.ts`(auth 链实现)
5
+
6
+ 旧体系是**两个互不相关的层**:HMAC 签名层证明"请求来自合法客户端",JWT 登录层证明"用户是谁"——两条路径、两套密钥、两套过期与吊销管理。token 体系把它们合并为一个**两态对象**:签名材料(security 段)与身份数据(identity 段)统一由 Redis 平面对象承载,验签与认证一次完成。
7
+
8
+ ## 组件对照
9
+
10
+ | 旧体系(HMAC + JWT) | token 体系 | 说明 |
11
+ |---------------------|-----------|------|
12
+ | `sign/issue` + `sign/refresh` 凭证引导端点(SignController) | **退役** | 无公共签发接口;每个 app 的登录入口(`@LoginEntry` 标记的 login + refresh 双端点)即入口 |
13
+ | `schema/api_keys.table.ts`(appKey/appSecret 存储) | **退役** | 会话凭据(token + refresh_token + login_at)存账户表(身份表硬约束三列) |
14
+ | HMAC 签名头:`x-app-key` / `x-timestamp` / `x-nonce` / `x-signature` | `x-token` / `x-timestamp` / `x-nonce` / `x-signature` | `x-app-key` 换成 `x-token`;timestamp + nonce 防重放**保留**;签名串格式不变(method + path + timestamp + nonce + canonical body) |
15
+ | 签名密钥:按 appKey 查密钥表(sign-db-driver / sign-redis-driver) | Redis 对象 secret 字段(`{app}.{token}` 一次还原) | 验签材料与身份同对象,零额外查询 |
16
+ | JWT:`Authorization: Bearer <token>`,payload 携带 user 对象 | token 纯随机 hash(`x-token` 头) | 不可解析、无 payload/exp,客户端只见 hash |
17
+ | `signToken` / `verifyToken` / blacklist 检查 | **退役** | 删 Redis key 即吊销,blacklist 补丁不再需要 |
18
+ | JWT secret 一套密钥 | secret/cipher 随 token 对象存 Redis(login 响应下发 secret) | 每会话独立密钥,轮换随 token 走 |
19
+ | `@Public()`(sign/issue 唯一豁免 + 第三方回调) | **退役,唯一例外:第三方回调** | 所有接口必须签名无豁免;第三方回调 controller(thirdApis 名下模块)是唯一 `@Public` 白名单(lint 强制) |
20
+ | `@Login('<模块名>')` + `user.type` 403 判定 | `@Login('<app>')` 语义保留 | module = app 名,兼作 Redis key 前缀(app 命名空间隔离) |
21
+ | 无登录入口标记 | **`@LoginEntry(app?)`**(新增) | 标记登录入口 controller(login + refresh 双端点),auth 链据此 + body 是否带 refreshToken 区分验证模式 |
22
+ | `(body, user: User)` handler 第二参 | `(body, token: {Name}Token)` | User 退役;按 api+app 自动推导模块 token 类型(无 token 声明 = 生成报错) |
23
+ | `__inject` 只映射一个 id(`(body, user)`) | `fromToken(token, [...])` 多属性注入(`(body, token)`) | 服务器注入字段从 Token 平面对象填充 |
24
+ | refreshToken:无此概念(JWT 过期重新登录) | refreshToken 7 天滑动窗口 + 每次刷新轮换 | 只在 login 返回;**只在 refresh 请求体中上送**(不出现在任何 header) |
25
+ | 登录过期:JWT exp + 可选 blacklist | token 固定 30 分钟(Redis TTL)+ refresh 续期 | 删 token 即时终止会话;refresh 校验账号状态 + 7 天窗口 |
26
+
27
+ ## 验证模式对照
28
+
29
+ | 旧体系(装饰器决定) | 鉴权 | 新体系(请求形态决定) | 签名密钥 |
30
+ |---|---|---|---|
31
+ | `@Public()` | 无 | 第三方回调(唯一白名单) | 不签名(第三方自身验签手写在 controller) |
32
+ | 无装饰器(默认) | HMAC 签名 | 业务请求(带 `x-token`) | Redis 对象 secret |
33
+ | `@Login('<模块>')` | HMAC 签名 + JWT | 业务请求 + app scope | 同上 |
34
+ | — | — | login 入口(`@LoginEntry`,body 无 refreshToken) | bootstrap 初始密钥(固定密钥 + 时间窗口,两端独立计算) |
35
+ | — | — | refresh 入口(`@LoginEntry`,body 带 refreshToken) | refreshToken 派生密钥(`HMAC(refreshToken, 固定盐)`,能签名 == 持有 refreshToken) |
36
+
37
+ ## 端点对照
38
+
39
+ | 旧体系 | token 体系 |
40
+ |--------|-----------|
41
+ | `POST sign/issue` → appKey + appSecret(存 api_keys 表) | 无(客户端用 bootstrap 初始密钥签名 login) |
42
+ | `POST sign/refresh` → 新 appSecret | 无(refreshToken 轮换内建于 login 链) |
43
+ | `POST login/login` → JWT | `POST {app}/login/login` → token + refreshToken + secret(`@LoginEntry`) |
44
+ | 无独立 refresh 端点 | `POST {app}/login/refresh` → 新 token + 新 refreshToken(body 上送 refreshToken,`@LoginEntry`) |
45
+
46
+ ## 客户端对照
47
+
48
+ | 旧体系 | token 体系 |
49
+ |--------|-----------|
50
+ | `createCredentialFetcher` + `createApiClient`(先取 appKey 再签名) | token 会话客户端:login 后持有 token + secret 自动签名;token 过期**内部自动 refresh**(固定流程,不暴露公共 refresh 方法、不生成前端 refresh 调用函数) |
51
+ | 每次请求带 `x-app-key` + HMAC 签名 + `Authorization` | 每次请求带 `x-token` + 签名(secret 取自会话) |
52
+
53
+ ## 迁移清单(旧项目 → token 体系)
54
+
55
+ 1. **删 SignController / sign 链**:`sign/issue`、`sign/refresh` 端点、`schema/api_keys.table.ts`、sign-db/sign-redis-driver 接线全部移除。
56
+ 2. **`@Public` 清理**:除 thirdApis 名下回调 controller 外一律删除(lint 白名单强制)。
57
+ 3. **登录链重建**:`token_schema/{api}/{app}/token/*.token.ts` 定义身份(identity 投影身份表,硬约束三列)→ `pylonts gen token` → `login.config.ts` 引用 token → `pylonts gen login`。
58
+ 4. **auth 链配置**:`AppConfig.auth.token`——`resolveToken`(Redis 还原)、`bootstrapSecrets`(app → 初始密钥)、`refreshKeySalt`(refreshToken 派生盐)、nonceStore。
59
+ 5. **controller 第二参**:`(body, user: User)` → `(body, token: {Name}Token)`——`pylonts gen controller` 自动按 api+app 推导,app 无 token 声明会报错(补齐声明)。
60
+ 6. **JWT 依赖清理**:`signToken` / `verifyToken` / blacklist / `Authorization` 解析全部移除。