@blod-nm/user-kit 0.1.0 → 0.1.2
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.
- package/package.json +1 -1
- package/src/auth.js +3 -2
- package/src/schemas.js +244 -0
- package/src/user.js +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blod-nm/user-kit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "CloudBase user-center kit: auth (wechat code / unionid / password / sms / email / signup) + crm_user_detail archive on @blod-nm/base-cb.",
|
package/src/auth.js
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
import cloudbase from '@cloudbase/node-sdk'
|
|
37
37
|
import { code2Session as wxCode2Session } from './wechat.js'
|
|
38
38
|
import { decodeJWT, throwGatewayError, makeGateway, uidFromToken } from './user.js'
|
|
39
|
+
import { withSchemas } from './schemas.js'
|
|
39
40
|
|
|
40
41
|
export { decodeJWT } // 供消费方/测试从包主入口取(真身在 user.js)
|
|
41
42
|
|
|
@@ -115,7 +116,7 @@ export function createAuthAbilities(cfg, kit) {
|
|
|
115
116
|
return { ...payload, uid }
|
|
116
117
|
}
|
|
117
118
|
|
|
118
|
-
return [
|
|
119
|
+
return withSchemas([
|
|
119
120
|
['auth.sendVerification', { describe: '发验证码(phone/email 二选一):POST /auth/v1/verification,网关原样(含 verification_token)', run: async (args = {}) => {
|
|
120
121
|
const body = {}
|
|
121
122
|
for (const k of ['phone', 'email', 'email_redirect_to']) if (args[k] !== undefined) body[k] = args[k]
|
|
@@ -203,5 +204,5 @@ export function createAuthAbilities(cfg, kit) {
|
|
|
203
204
|
try { res = await refreshTokenRequest({ grant_type: 'refresh_token', refresh_token, client_id: cfg.envId }) } catch { throw err(502, '令牌刷新请求失败') }
|
|
204
205
|
return { access_token: res.access_token, refresh_token: res.refresh_token, expires_in: res.expires_in }
|
|
205
206
|
} }],
|
|
206
|
-
]
|
|
207
|
+
])
|
|
207
208
|
}
|
package/src/schemas.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
schemas.js — 能力面元数据(纯数据 · 纯对象 JSON Schema,不引 zod)
|
|
3
|
+
|
|
4
|
+
职责: 23 条 user-kit 能力的 title / inputSchema / outputSchema / MCP annotations。
|
|
5
|
+
能力实现(auth.js / user.js)只负责 run;约束集中在这里,挂 MCP 时原样透传。
|
|
6
|
+
|
|
7
|
+
取用: withSchemas([[key, ability], ...])
|
|
8
|
+
|
|
9
|
+
必填约定:
|
|
10
|
+
· required 列出的字段缺一即 400(实现里已有同名校验)
|
|
11
|
+
· 二选一用 anyOf(signup: phone_number/email;sendVerification: phone/email)
|
|
12
|
+
· 可选字段在 description 里写明「可选」
|
|
13
|
+
· 本地/自定义链路 additionalProperties:false(不接受多余参数);网关透传类保持开放
|
|
14
|
+
*/
|
|
15
|
+
const str = (description, extra = {}) => ({ type: 'string', description, ...extra })
|
|
16
|
+
const obj = (properties = {}, { required = [], anyOf, additionalProperties = true } = {}) => ({
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties,
|
|
19
|
+
...(required.length ? { required } : {}),
|
|
20
|
+
...(anyOf ? { anyOf } : {}),
|
|
21
|
+
additionalProperties,
|
|
22
|
+
})
|
|
23
|
+
const tokens = {
|
|
24
|
+
access_token: str('访问令牌(必返)'),
|
|
25
|
+
refresh_token: str('刷新令牌(必返)'),
|
|
26
|
+
expires_in: { type: 'number', description: '有效期秒数(必返)' },
|
|
27
|
+
}
|
|
28
|
+
const tokenOut = { type: 'object', properties: { ...tokens }, required: ['access_token', 'refresh_token', 'expires_in'] }
|
|
29
|
+
const tokenIn = obj({ access_token: str('用户访问令牌(必填,Bearer)') }, { required: ['access_token'], additionalProperties: false })
|
|
30
|
+
|
|
31
|
+
export const SCHEMAS = {
|
|
32
|
+
/* ---------- auth:自定义登录 ---------- */
|
|
33
|
+
'auth.loginByWxcode': {
|
|
34
|
+
title: '微信小程序登录',
|
|
35
|
+
inputSchema: obj({ code: str('wx.login 得到的 code(必填)') }, { required: ['code'], additionalProperties: false }),
|
|
36
|
+
outputSchema: obj({
|
|
37
|
+
uid: str('唯一用户 id(必返)'),
|
|
38
|
+
openid: str('微信 openid(必返)'),
|
|
39
|
+
unionid: { type: ['string', 'null'], description: '微信 unionid,可能为 null(必返)' },
|
|
40
|
+
...tokens,
|
|
41
|
+
config: { type: 'object', description: 'prefix_config(必返,客户端强依赖)' },
|
|
42
|
+
}, { required: ['uid', 'openid', 'unionid', 'access_token', 'refresh_token', 'expires_in', 'config'], additionalProperties: false }),
|
|
43
|
+
annotations: { openWorldHint: true },
|
|
44
|
+
},
|
|
45
|
+
'auth.loginByUnionid': {
|
|
46
|
+
title: '唯一 id 登录',
|
|
47
|
+
inputSchema: obj({ unionid: str('调用方唯一用户 id(必填)') }, { required: ['unionid'], additionalProperties: false }),
|
|
48
|
+
outputSchema: obj({
|
|
49
|
+
uid: str('唯一用户 id(必返)'),
|
|
50
|
+
unionid: str('唯一用户 id(必返)'),
|
|
51
|
+
...tokens,
|
|
52
|
+
}, { required: ['uid', 'unionid', 'access_token', 'refresh_token', 'expires_in'], additionalProperties: false }),
|
|
53
|
+
annotations: { openWorldHint: true },
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
/* ---------- auth:透传登录 ---------- */
|
|
57
|
+
'auth.loginByPassword': {
|
|
58
|
+
title: '账号密码登录',
|
|
59
|
+
inputSchema: obj({ username: str('用户名(必填)'), password: str('密码(必填)') }, { required: ['username', 'password'] }),
|
|
60
|
+
outputSchema: obj({ uid: str('唯一用户 id(必返)'), access_token: str('访问令牌(必返)') }, { required: ['uid', 'access_token'] }),
|
|
61
|
+
annotations: { openWorldHint: true },
|
|
62
|
+
},
|
|
63
|
+
'auth.loginBySms': {
|
|
64
|
+
title: '短信验证码登录',
|
|
65
|
+
inputSchema: obj({ verification_token: str('verifyCode 得到的令牌(必填)') }, { required: ['verification_token'] }),
|
|
66
|
+
outputSchema: obj({ uid: str('唯一用户 id(必返)'), access_token: str('访问令牌(必返)') }, { required: ['uid', 'access_token'] }),
|
|
67
|
+
annotations: { openWorldHint: true },
|
|
68
|
+
},
|
|
69
|
+
'auth.loginByEmail': {
|
|
70
|
+
title: '邮箱验证码登录',
|
|
71
|
+
inputSchema: obj({ verification_token: str('verifyCode 得到的令牌(必填)') }, { required: ['verification_token'] }),
|
|
72
|
+
outputSchema: obj({ uid: str('唯一用户 id(必返)'), access_token: str('访问令牌(必返)') }, { required: ['uid', 'access_token'] }),
|
|
73
|
+
annotations: { openWorldHint: true },
|
|
74
|
+
},
|
|
75
|
+
'auth.signup': {
|
|
76
|
+
title: '注册',
|
|
77
|
+
inputSchema: obj({
|
|
78
|
+
phone_number: str('手机号(与 email 二选一)'),
|
|
79
|
+
email: str('邮箱(与 phone_number 二选一)'),
|
|
80
|
+
verification_token: str('验证码令牌(必填)'),
|
|
81
|
+
username: str('用户名(可选)'),
|
|
82
|
+
password: str('密码(可选)'),
|
|
83
|
+
}, {
|
|
84
|
+
required: ['verification_token'],
|
|
85
|
+
anyOf: [{ required: ['phone_number'] }, { required: ['email'] }],
|
|
86
|
+
}),
|
|
87
|
+
outputSchema: obj({ uid: str('唯一用户 id(必返)'), access_token: str('访问令牌(必返)') }, { required: ['uid', 'access_token'] }),
|
|
88
|
+
annotations: { openWorldHint: true },
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/* ---------- auth:验证码 ---------- */
|
|
92
|
+
'auth.sendVerification': {
|
|
93
|
+
title: '发送验证码',
|
|
94
|
+
inputSchema: obj({
|
|
95
|
+
phone: str('手机号(与 email 二选一)'),
|
|
96
|
+
email: str('邮箱(与 phone 二选一)'),
|
|
97
|
+
email_redirect_to: str('Magic Link 跳转地址(可选,仅邮箱)'),
|
|
98
|
+
}, { anyOf: [{ required: ['phone'] }, { required: ['email'] }] }),
|
|
99
|
+
outputSchema: obj({ verification_id: str('验证码会话 id(必返)') }, { required: ['verification_id'] }),
|
|
100
|
+
},
|
|
101
|
+
'auth.sendSmsCode': {
|
|
102
|
+
title: '发送短信验证码',
|
|
103
|
+
inputSchema: obj({ phone: str('手机号(必填)') }, { required: ['phone'], additionalProperties: false }),
|
|
104
|
+
outputSchema: obj({ verification_id: str('验证码会话 id(必返)') }, { required: ['verification_id'] }),
|
|
105
|
+
},
|
|
106
|
+
'auth.sendEmailCode': {
|
|
107
|
+
title: '发送邮箱验证码',
|
|
108
|
+
inputSchema: obj({ email: str('邮箱(必填)') }, { required: ['email'], additionalProperties: false }),
|
|
109
|
+
outputSchema: obj({ verification_id: str('验证码会话 id(必返)') }, { required: ['verification_id'] }),
|
|
110
|
+
},
|
|
111
|
+
'auth.verifyCode': {
|
|
112
|
+
title: '校验验证码',
|
|
113
|
+
inputSchema: obj({
|
|
114
|
+
verification_id: str('验证码会话 id(必填)'),
|
|
115
|
+
verification_code: str('用户输入的验证码(必填)'),
|
|
116
|
+
}, { required: ['verification_id', 'verification_code'], additionalProperties: false }),
|
|
117
|
+
outputSchema: obj({ verification_token: str('验证码令牌(必返)') }, { required: ['verification_token'] }),
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/* ---------- auth:会话 ---------- */
|
|
121
|
+
'auth.reauthenticate': {
|
|
122
|
+
title: '敏感操作二次验证',
|
|
123
|
+
inputSchema: obj({
|
|
124
|
+
access_token: str('用户访问令牌(必填)'),
|
|
125
|
+
verify_opt: { type: 'string', enum: ['phone_code', 'email_code'], description: '验证方式(必填)' },
|
|
126
|
+
}, { required: ['access_token', 'verify_opt'], additionalProperties: false }),
|
|
127
|
+
outputSchema: obj({ verification_id: str('验证码会话 id(必返)') }),
|
|
128
|
+
},
|
|
129
|
+
'auth.introspect': {
|
|
130
|
+
title: '校验令牌',
|
|
131
|
+
inputSchema: tokenIn,
|
|
132
|
+
outputSchema: obj({ sub: str('令牌对应的用户 id'), active: { type: 'boolean', description: '是否有效' } }),
|
|
133
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
134
|
+
},
|
|
135
|
+
'auth.refresh': {
|
|
136
|
+
title: '刷新令牌',
|
|
137
|
+
inputSchema: obj({ refresh_token: str('刷新令牌(必填)') }, { required: ['refresh_token'], additionalProperties: false }),
|
|
138
|
+
outputSchema: tokenOut,
|
|
139
|
+
},
|
|
140
|
+
'auth.signOut': {
|
|
141
|
+
title: '退出登录',
|
|
142
|
+
inputSchema: obj({
|
|
143
|
+
access_token: str('用户访问令牌(必填)'),
|
|
144
|
+
client_id: str('客户端 id(可选,默认环境 id)'),
|
|
145
|
+
x_device_id: str('设备 id(可选)'),
|
|
146
|
+
}, { required: ['access_token'] }),
|
|
147
|
+
outputSchema: { type: 'object', description: '网关响应(通常为空对象)' },
|
|
148
|
+
annotations: { destructiveHint: true },
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/* ---------- user:本地档案 ---------- */
|
|
152
|
+
'user.upsert': {
|
|
153
|
+
title: '建档 / 合并档案',
|
|
154
|
+
inputSchema: obj({
|
|
155
|
+
uid: str('唯一用户 id(必填,≤128 字符)'),
|
|
156
|
+
profile: { type: 'object', description: '要合并进档案 profile 段的字段(可选)' },
|
|
157
|
+
env: str('建档环境标签(可选,缺省 prod)'),
|
|
158
|
+
}, { required: ['uid'], additionalProperties: false }),
|
|
159
|
+
outputSchema: obj({ id: str('档案行 id'), data: { type: 'object', description: '档案 data 段' } }, { additionalProperties: false }),
|
|
160
|
+
},
|
|
161
|
+
'user.get': {
|
|
162
|
+
title: '读取档案',
|
|
163
|
+
inputSchema: obj({ uid: str('唯一用户 id(必填)') }, { required: ['uid'], additionalProperties: false }),
|
|
164
|
+
outputSchema: {
|
|
165
|
+
type: ['object', 'null'],
|
|
166
|
+
description: '档案行;不存在时为 null',
|
|
167
|
+
properties: { id: str('档案行 id'), data: { type: 'object' }, status: str('状态'), deleted_at: { type: ['string', 'null'] } },
|
|
168
|
+
},
|
|
169
|
+
annotations: { readOnlyHint: true },
|
|
170
|
+
},
|
|
171
|
+
'user.patchProfile': {
|
|
172
|
+
title: '局部更新档案',
|
|
173
|
+
inputSchema: obj({
|
|
174
|
+
uid: str('唯一用户 id(必填)'),
|
|
175
|
+
patch: { type: 'object', description: '要浅合并进 profile 的字段(必填)' },
|
|
176
|
+
}, { required: ['uid', 'patch'], additionalProperties: false }),
|
|
177
|
+
outputSchema: obj({ section: { type: ['object', 'null'], description: '合并后的 profile 段' } }),
|
|
178
|
+
},
|
|
179
|
+
'user.schemaStatus': {
|
|
180
|
+
title: '档案表状态',
|
|
181
|
+
inputSchema: obj({}, { required: [], additionalProperties: false }),
|
|
182
|
+
outputSchema: obj({
|
|
183
|
+
table: { type: 'boolean', description: '表是否存在' },
|
|
184
|
+
primaryKey: { type: 'boolean', description: '主键是否存在' },
|
|
185
|
+
indexes: { type: 'object', description: '四个索引的存在性' },
|
|
186
|
+
}, { required: ['table', 'primaryKey', 'indexes'], additionalProperties: false }),
|
|
187
|
+
annotations: { readOnlyHint: true },
|
|
188
|
+
},
|
|
189
|
+
'user.ensureSchema': {
|
|
190
|
+
title: '初始化档案表',
|
|
191
|
+
inputSchema: obj({}, { required: [], additionalProperties: false }),
|
|
192
|
+
outputSchema: obj({
|
|
193
|
+
table: { type: 'string', enum: ['created', 'exists'], description: '本次建表结果' },
|
|
194
|
+
indexes: { type: 'object', description: '{ created: string[], existing: string[] }' },
|
|
195
|
+
status: { type: 'object', description: '执行后的探测结果' },
|
|
196
|
+
}, { required: ['table', 'indexes', 'status'], additionalProperties: false }),
|
|
197
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/* ---------- user:用户令牌面(网关透传) ---------- */
|
|
201
|
+
'user.me': {
|
|
202
|
+
title: '当前用户',
|
|
203
|
+
inputSchema: tokenIn,
|
|
204
|
+
outputSchema: { type: 'object', description: '网关用户信息(原样)' },
|
|
205
|
+
annotations: { readOnlyHint: true },
|
|
206
|
+
},
|
|
207
|
+
'user.updateBasic': {
|
|
208
|
+
title: '修改资料(含手机号/邮箱)',
|
|
209
|
+
inputSchema: obj({
|
|
210
|
+
access_token: str('用户访问令牌(必填)'),
|
|
211
|
+
nickname: str('昵称(可选)'),
|
|
212
|
+
username: str('用户名(可选)'),
|
|
213
|
+
phone: str('手机号(可选,修改需验证码)'),
|
|
214
|
+
email: str('邮箱(可选,修改需验证码)'),
|
|
215
|
+
avatar_url: str('头像 URL(可选)'),
|
|
216
|
+
gender: { type: 'string', enum: ['MALE', 'FEMALE'], description: '性别(可选)' },
|
|
217
|
+
description: str('个人简介(可选)'),
|
|
218
|
+
verification_token: str('手机号/邮箱变更的验证码令牌(可选,改手机/邮箱时需要)'),
|
|
219
|
+
}, { required: ['access_token'] }),
|
|
220
|
+
outputSchema: { type: 'object', description: '网关响应(原样)' },
|
|
221
|
+
},
|
|
222
|
+
'user.changePassword': {
|
|
223
|
+
title: '修改密码',
|
|
224
|
+
inputSchema: obj({
|
|
225
|
+
access_token: str('用户访问令牌(必填)'),
|
|
226
|
+
password: str('新密码(必填)'),
|
|
227
|
+
verification_token: str('二次验证令牌(可选,按网关要求)'),
|
|
228
|
+
}, { required: ['access_token', 'password'] }),
|
|
229
|
+
outputSchema: { type: 'object', description: '网关响应(通常为空对象)' },
|
|
230
|
+
annotations: { destructiveHint: true },
|
|
231
|
+
},
|
|
232
|
+
'user.deleteMe': {
|
|
233
|
+
title: '注销账号',
|
|
234
|
+
inputSchema: obj({
|
|
235
|
+
access_token: str('用户访问令牌(必填)'),
|
|
236
|
+
verification_token: str('二次验证令牌(可选,按网关要求)'),
|
|
237
|
+
}, { required: ['access_token'] }),
|
|
238
|
+
outputSchema: { type: 'object', description: '网关响应(通常为空对象)' },
|
|
239
|
+
annotations: { destructiveHint: true },
|
|
240
|
+
},
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** 把元数据合并进 [key, ability] 列表(能力对象原样保留 describe/run) */
|
|
244
|
+
export const withSchemas = entries => entries.map(([key, ability]) => [key, { ...ability, ...(SCHEMAS[key] ?? {}) }])
|
package/src/user.js
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
* 下期留白(不实现):user.updateSection 旧接口(patchProfile 取代)/ user.bindPhone(crm_cache access_token)。
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
import { withSchemas } from './schemas.js'
|
|
30
|
+
|
|
29
31
|
const DEFAULT_DOMAIN = 'api.tcloudbasegateway.com'
|
|
30
32
|
|
|
31
33
|
/* —— 共用件:JWT 不验签解码 / token 身份硬规则 / 网关错误透传 / gateway 注入缺省 —— */
|
|
@@ -143,7 +145,7 @@ export function createUserAbilities(cfg, kit) {
|
|
|
143
145
|
return { table: !!t[0]?.exists, indexes: Object.fromEntries(INDEX_ORDER.map(n => [n, names.has(n)])), primaryKey: k.length > 0 }
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
return [
|
|
148
|
+
return withSchemas([
|
|
147
149
|
/* 建档面(本地 pg) */
|
|
148
150
|
['user.upsert', { describe: '建档/刷 updated_at(crm_user_detail;写走 cloudbase_postgres)', run: async (args = {}) => {
|
|
149
151
|
const { uid, profile, env } = args
|
|
@@ -215,5 +217,5 @@ export function createUserAbilities(cfg, kit) {
|
|
|
215
217
|
}
|
|
216
218
|
return { table: before.table ? 'exists' : 'created', indexes: { created: missing, existing: INDEX_ORDER.filter(n => !missing.includes(n)) }, status: await probe() }
|
|
217
219
|
} }],
|
|
218
|
-
]
|
|
220
|
+
])
|
|
219
221
|
}
|