@blod-nm/user-kit 0.1.6 → 0.1.8

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/src/auth.js DELETED
@@ -1,229 +0,0 @@
1
- /**
2
- * auth.js — auth 域能力目录(13 条)+ 编排(登录链路/错误翻译 · spec §3.1–3.5 · §3.7)
3
- *
4
- * 端点(主场实测确认,{origin}=https://<envId>.api.tcloudbasegateway.com):
5
- * POST /auth/v1/verification { phone | email } → 发验证码,返回 verification_token(响应原样)
6
- * POST /auth/v1/signin { username,password | verification_token } → 基础登录(响应原样 + 补 uid + 建档)
7
- * POST /auth/v1/signin/custom { provider_id:'custom', ticket } → 自定义登录(uid 链收尾)
8
- * POST /auth/v1/token { grant_type:'refresh_token', refresh_token, client_id:envId }
9
- * POST /auth/v1/verification/verify { verification_id, verification_code } → verification_token
10
- * POST /auth/v1/user/reauthenticate(Bearer);GET /auth/v1/token/introspect(Bearer)
11
- *
12
- * 语义:
13
- * · 透传三种(password/sms/email):body 原样发网关;uid 提取:payload.sub → payload.user.id →
14
- * decodeJWT(access_token).sub(不验签,token 刚由网关签发);都取不到 → 500「用户注册失败」。建档失败 → 同 500。
15
- * · 登录收尾(§3.2):建档 → 官方 token 托管(broker.upsert,失败 500)→ 签发 apikey(失败 500);
16
- * 响应只含 { apikey, expires_at }(wxcode 另有 config),官方 token/uid 字段不出域。
17
- * · 网关非 2xx:原样透传 { code: 网关状态, payload },不改写错误码(INVALID_USERNAME_OR_PASSWORD 原样可见);
18
- * 网络异常(无 status)→ 502「登录请求失败」。
19
- * · 自定义链路:customSignin 非 2xx → 502「自定义登录请求失败」;code2Session errcode 非零 → 400{errcode,errmsg}。
20
- * · loginByWxcode:uid=唯一 id(wxResolveUid 缺省 identity:uid=openid),unionid 字段=微信原始值|null,不可互换。
21
- *
22
- * §3.12 补全: auth.signup(手机/邮箱验证码注册,建档+托管+签发 key)/ auth.signOut(不写档,apikey 入参)。
23
- * 自定义登录只有两个入口(钉死):loginByWxcode(配置的微信小程序 appid/secret,code→openid→唯一 id→ticket→signin/custom)
24
- * 与 loginByUnionid(外部传唯一 id→ticket→signin/custom);二者都必须建档;unionid=我们的唯一 id,与微信 openid/unionid 语义无关。
25
- *
26
- * §3.13 uid 提取顺序(统一所有登录/注册类): payload.sub → payload.user.id → decodeJWT(access_token).sub →
27
- * 无则登录/注册类 500「用户注册失败」。凭证类能力(signOut/reauthenticate/introspect)入参 apikey:
28
- * verify 不通过 → 401「apikey 无效」,通过后 broker.get(uid) 换官方 Bearer。
29
- *
30
- * §3.8 增补(4 条): auth.sendVerification(phone/email 二选一;sendSmsCode/sendEmailCode 保留薄别名)/
31
- * auth.verifyCode / auth.reauthenticate(verify_opt ∈ phone_code|email_code)/ auth.introspect ——
32
- * 返回一律网关原样;错误走 throwGatewayError(非 2xx 原样 / 网络 502「登录请求失败」)。
33
- *
34
- * 注入面(9 个,缺省=真实现,单测零网络): sendVerification / signinRequest / code2Session / wxResolveUid /
35
- * createTicket / customSignin / refreshTokenRequest / fetchPrefixConfig / gateway(method, path, opts)。
36
- * @cloudbase/node-sdk(ticket 签发,Node 侧)只在本文件 import(A3 grep 白名单)。
37
- */
38
- import cloudbase from '@cloudbase/node-sdk'
39
- import { code2Session as wxCode2Session } from './wechat.js'
40
- import { decodeJWT, throwGatewayError, makeGateway } from './user.js'
41
- import { withSchemas } from './schemas.js'
42
-
43
- export { decodeJWT } // 供消费方/测试从包主入口取(真身在 user.js)
44
-
45
- const DEFAULT_DOMAIN = 'api.tcloudbasegateway.com'
46
- const TICKET_TTL = { refresh: 7 * 24 * 3600 * 1000, expire: 30 * 24 * 3600 * 1000 }
47
- const err = (code, message, extra = {}) => Object.assign(new Error(message), { code, ...extra })
48
-
49
- /* —— tcb 私钥/票(懒加载缓存;私钥只在内存解析,不落日志)—— */
50
-
51
- export async function loadCredentials(raw, envId) {
52
- const value = String(raw || '').trim()
53
- if (!value) throw new Error('缺少自定义登录私钥:tcbAuthCredentials(JSON 文案或文件路径,按 { 开头自动识别)')
54
- // 文件路径形态用动态 import 取 fs(保持静态 import 面干净;spec 允许两种形态)
55
- const text = value.startsWith('{') ? value : await (await import('node:fs/promises')).readFile(value, 'utf-8')
56
- const credentials = JSON.parse(text)
57
- // 控制台下载的私钥字段名 private_key_id → SDK 需要 key_id;短形态缺 env_id 段用 cfg.envId 补
58
- if ('private_key_id' in credentials && !('key_id' in credentials)) credentials.key_id = credentials.private_key_id
59
- if (!credentials.key_id || !credentials.private_key) throw new Error('credentials must include key_id and private_key')
60
- if (!credentials.env_id) credentials.env_id = envId
61
- return credentials
62
- }
63
-
64
- let tcbAppCache = null
65
- async function tcbCreateTicket(cfg, uid) {
66
- if (!tcbAppCache) {
67
- const credentials = await loadCredentials(cfg.tcbAuthCredentials, cfg.envId)
68
- tcbAppCache = cloudbase.init({ env: credentials.env_id, credentials })
69
- }
70
- // ★ TTL 与现网一字不差(refresh 7d / expire 30d,相对毫秒;gateway 对这两个值有兜底解释,改了就变)
71
- return tcbAppCache.auth().createTicket(uid, TICKET_TTL)
72
- }
73
-
74
- /* —— 能力目录 + 编排(ability 闭包持有 cfg/inject/kit/deps)—— */
75
-
76
- // 刷新请求器:broker 单飞刷新使用(inject.refreshTokenRequest 可替换)
77
- export const makeRefreshTokenRequest = (cfg, kit) => (cfg.inject || {}).refreshTokenRequest
78
- || (body => kit.req(`https://${cfg.envId}.${cfg.domain || DEFAULT_DOMAIN}/auth/v1/token`, { method: 'POST', body }))
79
-
80
- // deps(index.js 注入): broker(token 托管)/ verify(apikey 裁决)/ issue(签发新 key)/ refreshTokenRequest
81
- export function createAuthAbilities(cfg, kit, deps = {}) {
82
- const inject = cfg.inject || {}
83
- const origin = `https://${cfg.envId}.${cfg.domain || DEFAULT_DOMAIN}`
84
- const sendVerification = inject.sendVerification || (body => kit.req(`${origin}/auth/v1/verification`, { method: 'POST', body }))
85
- const signinRequest = inject.signinRequest || (body => kit.req(`${origin}/auth/v1/signin`, { method: 'POST', body }))
86
- const customSignin = inject.customSignin || (ticket => kit.req(`${origin}/auth/v1/signin/custom`, { method: 'POST', body: { provider_id: 'custom', ticket } }))
87
- const code2Session = inject.code2Session || (code => wxCode2Session(cfg, code))
88
- const wxResolveUid = inject.wxResolveUid || (wx => wx.openid) // 缺省 identity:uid=openid(现网语义)
89
- const createTicket = inject.createTicket || (uid => tcbCreateTicket(cfg, uid))
90
- const fetchPrefixConfig = inject.fetchPrefixConfig || (() => kit.req(`${origin}/v1/storages/object/store/public/wx_mp_conf/prefix_config.json`, { headers: { 'Cache-Control': 'no-cache' } }))
91
- const gateway = makeGateway(cfg, kit)
92
- const gw = async (method, path, opts) => { try { return await gateway(method, path, opts) } catch (e) { throwGatewayError(e) } }
93
- // verification 端点:旧注入口优先(sendSmsCode/sendEmailCode/sendVerification 共用)
94
- const postVerification = body => inject.sendVerification ? inject.sendVerification(body) : gw('POST', '/auth/v1/verification', { body })
95
-
96
- // 自定义链收尾(wxcode / unionid 共用):createTicket → signin/custom
97
- async function finishLogin(uid) {
98
- const ticket = await createTicket(uid)
99
- let res
100
- try { res = await customSignin(ticket) } catch { throw err(502, '自定义登录请求失败') }
101
- const { access_token: at, refresh_token: rt, expires_in } = res
102
- return { at, rt, expires_in } // 仅内部传递;官方 token 不出域
103
- }
104
-
105
- // 建档统一翻 500「用户注册失败」
106
- async function upsertOr500(args) {
107
- try { await kit.ctx.run('user.upsert', args) } catch { throw err(500, '用户注册失败') }
108
- }
109
-
110
- // uid 提取四级顺序(§3.13):payload.sub → payload.user.id → decodeJWT(access_token).sub
111
- const uidOfPayload = p => p?.sub || p?.user?.id || decodeJWT(p?.access_token).sub || null
112
-
113
- // 登录收尾(§3.2):官方 token 托管落库 → 签发 key(托管不落库就不发 key,fail-closed)
114
- async function issueSession(uid, tokens) {
115
- const expiresAt = Math.floor(Date.now() / 1000) + (Number(tokens?.expires_in) || 0)
116
- const at = tokens?.at ?? tokens?.access_token
117
- const rt = tokens?.rt ?? tokens?.refresh_token
118
- try { await deps.broker.upsert(uid, at, rt, expiresAt) }
119
- catch { throw err(500, '官方凭证托管失败') }
120
- let issued
121
- try { issued = await deps.issue(uid) } catch { throw err(500, 'apikey 签发失败') }
122
- return { apikey: issued.apikey, expires_at: issued.expires_at }
123
- }
124
-
125
- // 凭证类统一序(§3.3):verify(apikey) → 401 → broker.get(uid) → 官方 Bearer 调用
126
- async function sessionOf(apikey) {
127
- const v = await deps.verify(apikey)
128
- if (!v.ok) throw err(401, 'apikey 无效')
129
- const { access_token: token } = await deps.broker.get(v.auth.uidRef)
130
- return { uid: v.auth.uidRef, token } // 官方 token 只在内部/网关 Bearer 使用
131
- }
132
-
133
- // 透传三种共用:body 原样发网关 → uid → 建档 → 托管 + 签发 key
134
- async function passthroughLogin(body, required) {
135
- for (const field of required) {
136
- if (!body?.[field]) throw err(400, `${field} 必填`)
137
- }
138
- let payload
139
- try { payload = await signinRequest(body) } catch (e) { throwGatewayError(e) }
140
- const uid = uidOfPayload(payload)
141
- if (!uid) throw err(500, '用户注册失败')
142
- await upsertOr500({ uid, profile: {} })
143
- return issueSession(uid, payload)
144
- }
145
-
146
- return withSchemas([
147
- ['auth.sendVerification', { describe: '发验证码(phone/email 二选一):POST /auth/v1/verification,网关原样(含 verification_token)', run: async (args = {}) => {
148
- const body = {}
149
- for (const k of ['phone', 'email', 'email_redirect_to']) if (args[k] !== undefined) body[k] = args[k]
150
- if (!body.phone && !body.email) throw err(400, 'phone 或 email 二选一必填')
151
- return postVerification(body)
152
- } }],
153
-
154
- ['auth.sendSmsCode', { describe: '发短信验证码:sendVerification 薄别名(phone 必填)', run: async ({ phone } = {}) => {
155
- if (!phone) throw err(400, 'phone 必填')
156
- return postVerification({ phone })
157
- } }],
158
-
159
- ['auth.sendEmailCode', { describe: '发邮箱验证码:sendVerification 薄别名(email 必填)', run: async ({ email } = {}) => {
160
- if (!email) throw err(400, 'email 必填')
161
- return postVerification({ email })
162
- } }],
163
-
164
- ['auth.verifyCode', { describe: '验证码校验:POST /auth/v1/verification/verify,网关原样(含 verification_token)', run: async ({ verification_id, verification_code } = {}) => {
165
- if (!verification_id) throw err(400, 'verification_id 必填')
166
- if (!verification_code) throw err(400, 'verification_code 必填')
167
- return gw('POST', '/auth/v1/verification/verify', { body: { verification_id, verification_code } })
168
- } }],
169
-
170
- ['auth.reauthenticate', { describe: '重认证发码:POST /auth/v1/user/reauthenticate(Bearer;verify_opt ∈ phone_code|email_code)', run: async (args = {}) => {
171
- const { apikey, verify_opt } = args
172
- if (!['phone_code', 'email_code'].includes(verify_opt)) throw err(400, 'verify_opt 必填(phone_code|email_code)')
173
- const { token } = await sessionOf(apikey)
174
- return gw('POST', '/auth/v1/user/reauthenticate', { body: { verify_opt }, token })
175
- } }],
176
-
177
- ['auth.introspect', { describe: 'token 校验:GET /auth/v1/token/introspect(Bearer),网关原样(含 sub)', run: async ({ apikey } = {}) => {
178
- const { token } = await sessionOf(apikey)
179
- return gw('GET', '/auth/v1/token/introspect', { token })
180
- } }],
181
-
182
- ['auth.loginByPassword', { describe: '账号密码登录(透传+建档):POST /auth/v1/signin { username, password },响应原样补 uid', run: async (args = {}) => passthroughLogin({ username: args.username, password: args.password }, ['username', 'password']) }],
183
-
184
- ['auth.loginBySms', { describe: '短信验证码登录(透传+建档):POST /auth/v1/signin { verification_token }', run: async (args = {}) => passthroughLogin({ verification_token: args.verification_token }, ['verification_token']) }],
185
-
186
- ['auth.loginByEmail', { describe: '邮箱验证码登录(与 SMS 同端点同实现,命名只表达调用意图)', run: async (args = {}) => passthroughLogin({ verification_token: args.verification_token }, ['verification_token']) }],
187
-
188
- ['auth.loginByWxcode', { describe: '微信 code 登录全链:建档+自定义令牌+prefix_config(7 字段裸对象)', run: async (args = {}) => {
189
- const { code } = args
190
- if (!code || typeof code !== 'string') throw err(400, 'user-kit: 参数 code 必填')
191
- let wx
192
- try { wx = await code2Session(code) } catch { throw err(502, 'code2Session 请求失败') }
193
- if (wx.errcode) throw err(400, wx.errmsg || 'code2Session 失败', { errcode: wx.errcode, errmsg: wx.errmsg || 'code2Session 失败' })
194
- const uid = await wxResolveUid(wx)
195
- await upsertOr500({ uid, profile: { openid: wx.openid, unionid: wx.unionid || null } })
196
- const session = await issueSession(uid, await finishLogin(uid))
197
- return { ...session, config: await fetchPrefixConfig() }
198
- } }],
199
-
200
- ['auth.loginByUnionid', { describe: '开放登录:唯一 id 注册登录(建档→ticket→signin/custom→托管+签发 key)', run: async (args = {}) => {
201
- const { unionid } = args
202
- if (!unionid) throw err(400, '缺少 unionid')
203
- await upsertOr500({ uid: unionid, profile: {} })
204
- return issueSession(unionid, await finishLogin(unionid))
205
- } }],
206
-
207
- ['auth.signup', { describe: '注册(手机/邮箱/provider_token 三形态):POST /auth/v1/signup,响应原样+补 uid+建档', run: async (args = {}) => {
208
- // 三种形态:手机号+验证码 / 邮箱+验证码 / provider_token(第三方授权注册)
209
- const codePath = !!(args?.phone_number || args?.email)
210
- if (codePath && !args?.verification_token) throw err(400, 'verification_token 必填')
211
- if (!codePath && !args?.provider_token) throw err(400, 'phone_number/email/provider_token 必填其一')
212
- const payload = await gw('POST', '/auth/v1/signup', { body: args })
213
- const uid = uidOfPayload(payload)
214
- if (!uid) throw err(500, '用户注册失败')
215
- const profile = {} // 建档只放传入的可入档字段(username/email/phone 有则带),不编造
216
- if (args.username !== undefined) profile.username = args.username
217
- if (args.email !== undefined) profile.email = args.email
218
- if (args.phone_number !== undefined) profile.phone = args.phone_number
219
- await upsertOr500({ uid, profile })
220
- return issueSession(uid, payload)
221
- } }],
222
-
223
- ['auth.signOut', { describe: '登出:POST /auth/v1/user/signout(Bearer),不写档', run: async (args = {}) => {
224
- const { apikey, ...rest } = args ?? {}
225
- const { token } = await sessionOf(apikey)
226
- return gw('POST', '/auth/v1/user/signout', { body: rest, token })
227
- } }],
228
- ])
229
- }
package/src/index.js DELETED
@@ -1,70 +0,0 @@
1
- /**
2
- * user-kit — user/auth 域能力套件(base-cb 装载叠加 → Ctx 总线)
3
- *
4
- * 主出口(应用型):
5
- * import { createKit } from '@blod-nm/user-kit'
6
- * const kit = createKit({ envId, apiKey, wxAppid, wxSecret, tcbAuthCredentials, env?, inject?, signinProviders?, …透传 base-cb })
7
- * await kit.ctx.run('pg.exec', { sql: 'select 1' }) // base-cb 网关 14 能力原样可用
8
- * await kit.ctx.run('auth.loginByPassword', { username, password }) // → { apikey, expires_at }
9
- * await kit.ctx.run('user.me', { apikey }) // 凭证面统一 apikey
10
- * kit.key.verify({ apikey }) / kit.broker.get(uid) // key 域 / token 托管便捷入口
11
- * kit.describe() // 能力元数据
12
- *
13
- * 数据流: cfg fail-fast → createBaseKit(cfg)(base-cb req/gate/ctx)→ runSql 单接缝(pg.exec)
14
- * → broker(token 托管)/ key 域(签发+verify+缓存)→ auth/user 域闭包持 deps → 注册 auth.cbToken.get(总线消费 broker)
15
- * → kit { ctx, req, cfg, broker, key, describe }
16
- *
17
- * cfg 约定(spec §3.0):
18
- * 必填 envId / apiKey / wxAppid / wxSecret / tcbAuthCredentials(缺一即抛,fail-fast)
19
- * env 缺省 'prod'(建档 tag from_env);akDefaultKeyTtlSec 缺省 2592000(30 天)
20
- * inject.* 九口(含 gateway)原样存 kit.cfg(缺省 = 真实现);domain/proxy/proxyBase/timeoutMs/role 透传 base-cb
21
- */
22
- import { createKit as createBaseKit } from '@blod-nm/base-cb'
23
- import { createAuthAbilities, makeRefreshTokenRequest } from './auth.js'
24
- import { createUserAbilities } from './user.js'
25
- import { createKeyAbilities } from './access-key.js'
26
- import { createCbTokenBroker, makeRunSql } from './token-broker.js'
27
- import { withSchemas } from './schemas.js'
28
-
29
- const REQUIRED = ['envId', 'apiKey', 'wxAppid', 'wxSecret', 'tcbAuthCredentials']
30
- const DEFAULT_KEY_TTL_SEC = 2592000
31
-
32
- export function createKit(cfg = {}) {
33
- for (const field of REQUIRED) {
34
- if (!cfg?.[field]) throw new Error(`user-kit: cfg 缺 ${field}`)
35
- }
36
- const finalCfg = { ...cfg, env: cfg.env || 'prod', akDefaultKeyTtlSec: cfg.akDefaultKeyTtlSec ?? DEFAULT_KEY_TTL_SEC }
37
- const base = createBaseKit(finalCfg)
38
-
39
- // §3.7 存储接缝 + §3.6 token 托管 + §3.5 key 域(verify 唯一裁决路径)
40
- const runSql = makeRunSql(base)
41
- const refreshTokenRequest = makeRefreshTokenRequest(finalCfg, base)
42
- const broker = createCbTokenBroker({ cfg: finalCfg, refreshTokenRequest, runSql })
43
- const keyDomain = createKeyAbilities({ cfg: finalCfg, runSql })
44
- const deps = { broker, runSql, verify: keyDomain.verify, issue: keyDomain.issue, refreshTokenRequest }
45
-
46
- // 宿主经总线消费 broker 的唯一通道(透传前 uid → 托管官方 token)
47
- const cbToken = {
48
- describe: '托管官方 token 兑换:uid → { access_token, expires_at, from_cache }(宿主透传前取 Bearer)',
49
- run: async (args = {}) => {
50
- if (!args?.uid) throw Object.assign(new Error('缺少 uid'), { code: 400 })
51
- return broker.get(args.uid)
52
- },
53
- }
54
-
55
- for (const [key, ability] of [...createAuthAbilities(finalCfg, base, deps), ...createUserAbilities(finalCfg, base, deps), ...keyDomain.abilities, ...withSchemas([['auth.cbToken.get', cbToken]])]) {
56
- base.ctx.register(key, ability)
57
- }
58
-
59
- // key 域便捷入口(同 ctx 能力)
60
- const key = {
61
- create: args => base.ctx.run('auth.key.create', args),
62
- list: args => base.ctx.run('auth.key.list', args),
63
- revoke: args => base.ctx.run('auth.key.revoke', args),
64
- verify: args => base.ctx.run('auth.key.verify', args),
65
- }
66
-
67
- return { ctx: base.ctx, req: base.req, cfg: finalCfg, broker, key, describe: () => base.ctx.describe() }
68
- }
69
-
70
- export default createKit
package/src/schemas.js DELETED
@@ -1,333 +0,0 @@
1
- /**
2
- schemas.js — 能力面元数据(纯数据 · 纯对象 JSON Schema,不引 zod)
3
-
4
- 职责: 22 条在册能力(DC-06:旧 token 刷新能力已注销)的 title / description / auth / inputSchema / outputSchema / annotations。
5
- 能力实现(auth.js / user.js)只负责 run;约束与开放策略集中在这里,挂 MCP 时原样透传。
6
-
7
- auth 分类(DC-06:对外凭证统一 apikey;官方令牌由服务端换托管凭证注入 Authorization: Bearer):
8
- 'public' 开放接口,不校验凭证
9
- 'user' 需要 apikey(schema 只暴露 apikey 入参,官方令牌字段不出域)
10
- 'self' 同 user,且 uid 由 apikey 档案解析(只能操作自己的档案)
11
- 内部能力标 mcp:false —— 不注册为 MCP 工具(登录自动建档 / 部署动作)
12
-
13
- 取用: withSchemas([[key, ability], ...])
14
- */
15
- const str = (description, extra = {}) => ({ type: 'string', description, ...extra })
16
- // 与 assertUid 运行时规则一致:≤128 字符、无控制字符
17
- const uidField = { type: 'string', description: '唯一用户 id(≤128 字符,不含控制字符)', maxLength: 128, pattern: '^[^\\u0000-\\u001f\\u007f]+$' }
18
- const freeObj = description => ({ type: 'object', description, additionalProperties: true })
19
- const obj = (properties = {}, { required = [], anyOf, additionalProperties = true } = {}) => ({
20
- type: 'object',
21
- properties,
22
- ...(required.length ? { required } : {}),
23
- ...(anyOf ? { anyOf } : {}),
24
- additionalProperties,
25
- })
26
- // DC-06 契约:所有登录/注册只出 apikey(官方令牌不出域)
27
- const apikeyField = str('apikey(必填;a1_ 前缀)')
28
- const apikeyIn = { apikey: apikeyField }
29
- const sessionProps = {
30
- apikey: str('新签发的 apikey(必返;明文只出现这一次)'),
31
- expires_at: { type: 'number', description: 'apikey 过期时间(unix 秒,必返)' },
32
- }
33
- const sessionOut = { type: 'object', properties: { ...sessionProps }, required: ['apikey', 'expires_at'], additionalProperties: false }
34
-
35
- export const SCHEMAS = {
36
- /* ---------- auth:自定义登录(public) ---------- */
37
- 'auth.loginByWxcode': {
38
- auth: 'public',
39
- title: '微信小程序登录',
40
- description: '用 wx.login 的 code 登录:自动建档并返回会话令牌与下发配置',
41
- inputSchema: obj({ code: str('wx.login 得到的 code(必填)') }, { required: ['code'], additionalProperties: false }),
42
- outputSchema: obj({
43
- ...sessionProps,
44
- config: { type: 'object', description: 'prefix_config(必返,客户端强依赖)' },
45
- }, { required: ['apikey', 'expires_at', 'config'], additionalProperties: false }),
46
- annotations: { openWorldHint: true },
47
- },
48
- 'auth.loginByUnionid': {
49
- auth: 'public',
50
- title: '唯一 id 登录',
51
- description: '用业务唯一 id 直接登录:自动建档并返回会话令牌',
52
- inputSchema: obj({ unionid: str('调用方唯一用户 id(必填)') }, { required: ['unionid'], additionalProperties: false }),
53
- outputSchema: sessionOut,
54
- annotations: { openWorldHint: true },
55
- },
56
-
57
- /* ---------- auth:透传登录(public) ---------- */
58
- 'auth.loginByPassword': {
59
- auth: 'public',
60
- title: '账号密码登录',
61
- description: '账号密码登录:建档 + 托管官方令牌并签发 apikey',
62
- inputSchema: obj({ username: str('用户名(必填)'), password: str('密码(必填)') }, { required: ['username', 'password'] }),
63
- outputSchema: sessionOut,
64
- annotations: { openWorldHint: true },
65
- },
66
- 'auth.loginBySms': {
67
- auth: 'public',
68
- title: '短信验证码登录',
69
- description: '用 verifyCode 换来的 verification_token 登录:建档 + 托管官方令牌并签发 apikey',
70
- inputSchema: obj({ verification_token: str('verifyCode 得到的令牌(必填)') }, { required: ['verification_token'] }),
71
- outputSchema: sessionOut,
72
- annotations: { openWorldHint: true },
73
- },
74
- 'auth.loginByEmail': {
75
- auth: 'public',
76
- title: '邮箱验证码登录',
77
- description: '用 verifyCode 换来的 verification_token 登录(邮箱):建档 + 托管官方令牌并签发 apikey',
78
- inputSchema: obj({ verification_token: str('verifyCode 得到的令牌(必填)') }, { required: ['verification_token'] }),
79
- outputSchema: sessionOut,
80
- annotations: { openWorldHint: true },
81
- },
82
- 'auth.signup': {
83
- auth: 'public',
84
- title: '注册',
85
- description: '注册新用户:手机号+验证码 / 邮箱+验证码 / 第三方 provider_token 三选一,自动建档',
86
- inputSchema: obj({
87
- phone_number: str('手机号(与 email / provider_token 三选一)'),
88
- email: str('邮箱(与 phone_number / provider_token 三选一)'),
89
- provider_token: str('第三方 provider_token(三选一;与验证码方式互斥)'),
90
- verification_token: str('验证码令牌(手机/邮箱方式必填;provider_token 方式不需要)'),
91
- username: str('用户名(可选)'),
92
- password: str('密码(可选)'),
93
- }, {
94
- anyOf: [
95
- { required: ['verification_token', 'phone_number'] },
96
- { required: ['verification_token', 'email'] },
97
- { required: ['provider_token'] },
98
- ],
99
- }),
100
- outputSchema: sessionOut,
101
- annotations: { openWorldHint: true },
102
- },
103
-
104
- /* ---------- auth:验证码(public) ---------- */
105
- 'auth.sendVerification': {
106
- auth: 'public',
107
- title: '发送验证码',
108
- description: '向手机号或邮箱发送验证码(二选一)',
109
- inputSchema: obj({
110
- phone: str('手机号(与 email 二选一)'),
111
- email: str('邮箱(与 phone 二选一)'),
112
- email_redirect_to: str('Magic Link 跳转地址(可选,仅邮箱)'),
113
- }, { anyOf: [{ required: ['phone'] }, { required: ['email'] }] }),
114
- outputSchema: obj({ verification_id: str('验证码会话 id(必返)') }, { required: ['verification_id'] }),
115
- },
116
- 'auth.sendSmsCode': {
117
- auth: 'public',
118
- title: '发送短信验证码',
119
- description: '向手机号发送短信验证码',
120
- inputSchema: obj({ phone: str('手机号(必填)') }, { required: ['phone'], additionalProperties: false }),
121
- outputSchema: obj({ verification_id: str('验证码会话 id(必返)') }, { required: ['verification_id'] }),
122
- },
123
- 'auth.sendEmailCode': {
124
- auth: 'public',
125
- title: '发送邮箱验证码',
126
- description: '向邮箱发送验证码',
127
- inputSchema: obj({ email: str('邮箱(必填)') }, { required: ['email'], additionalProperties: false }),
128
- outputSchema: obj({ verification_id: str('验证码会话 id(必返)') }, { required: ['verification_id'] }),
129
- },
130
- 'auth.verifyCode': {
131
- auth: 'public',
132
- title: '校验验证码',
133
- description: '校验用户输入的验证码,换取登录用的 verification_token',
134
- inputSchema: obj({
135
- verification_id: str('验证码会话 id(必填)'),
136
- verification_code: str('用户输入的验证码(必填)'),
137
- }, { required: ['verification_id', 'verification_code'], additionalProperties: false }),
138
- outputSchema: obj({ verification_token: str('验证码令牌(必返)') }, { required: ['verification_token'] }),
139
- },
140
-
141
- /* ---------- auth:会话 ---------- */
142
- 'auth.reauthenticate': {
143
- auth: 'user',
144
- title: '敏感操作二次验证',
145
- description: '改密码/注销前的二次验证:向当前用户发送验证码',
146
- inputSchema: obj({
147
- ...apikeyIn,
148
- verify_opt: { type: 'string', enum: ['phone_code', 'email_code'], description: '验证方式(必填)' },
149
- }, { required: ['apikey', 'verify_opt'], additionalProperties: false }),
150
- outputSchema: obj({ verification_id: str('验证码会话 id(必返)') }),
151
- },
152
- 'auth.introspect': {
153
- auth: 'user',
154
- title: '校验令牌',
155
- description: '校验当前 apikey 对应的官方令牌是否有效,返回用户 sub',
156
- inputSchema: obj({ ...apikeyIn }, { required: ['apikey'], additionalProperties: false }),
157
- outputSchema: obj({ sub: str('令牌对应的用户 id'), active: { type: 'boolean', description: '是否有效' } }),
158
- annotations: { readOnlyHint: true, openWorldHint: true },
159
- },
160
- 'auth.signOut': {
161
- auth: 'user',
162
- title: '退出登录',
163
- description: '退出当前会话(令牌作废)',
164
- inputSchema: obj({
165
- ...apikeyIn,
166
- client_id: str('客户端 id(可选,默认环境 id)'),
167
- x_device_id: str('设备 id(可选)'),
168
- }, { required: ['apikey'], additionalProperties: true }),
169
- outputSchema: { type: 'object', description: '网关响应(通常为空对象)' },
170
- annotations: { destructiveHint: true },
171
- },
172
-
173
- /* ---------- user:本地档案 ---------- */
174
- 'user.upsert': {
175
- mcp: false,
176
- auth: 'user',
177
- title: '建档 / 合并档案',
178
- description: '建档或刷新 updated_at(登录链路内部步,登录时自动执行)',
179
- inputSchema: obj({ uid: uidField, profile: freeObj('要合并进档案 profile 段的字段(可选)'), env: str('建档环境标签(可选,缺省 prod)') }, { required: ['uid'], additionalProperties: false }),
180
- outputSchema: obj({ id: str('档案行 id'), data: { type: 'object', description: '档案 data 段' } }, { additionalProperties: false }),
181
- },
182
- 'user.get': {
183
- auth: 'self',
184
- title: '读取自己的档案',
185
- description: '读取当前登录用户的业务档案(profile/settings),无档案时返回 null',
186
- inputSchema: obj({}, { additionalProperties: false }),
187
- outputSchema: {
188
- type: 'object',
189
- description: '档案行(无行时 structuredContent 省略,content 文本为 null)',
190
- properties: { id: str('档案行 id'), data: { type: 'object' }, status: str('状态'), deleted_at: { type: ['string', 'null'] } },
191
- additionalProperties: true,
192
- },
193
- annotations: { readOnlyHint: true },
194
- },
195
- 'user.patchProfile': {
196
- auth: 'self',
197
- title: '更新自己的档案',
198
- description: '浅合并更新当前登录用户的业务档案 profile 段(本地字段,不走网关)',
199
- inputSchema: obj({
200
- patch: freeObj('要浅合并进 profile 的字段(必填)'),
201
- }, { required: ['patch'], additionalProperties: false }),
202
- outputSchema: obj({ section: { type: ['object', 'null'], description: '合并后的 profile 段' } }),
203
- },
204
- 'user.schemaStatus': {
205
- mcp: false,
206
- auth: 'user',
207
- title: '档案表状态',
208
- description: '探测建档表 / 主键 / 四索引的在位状态(运维面)',
209
- inputSchema: obj({}, { required: [], additionalProperties: false }),
210
- outputSchema: obj({
211
- table: { type: 'boolean', description: '表是否存在' },
212
- primaryKey: { type: 'boolean', description: '主键是否存在' },
213
- indexes: { type: 'object', description: '四个索引的存在性' },
214
- }, { required: ['table', 'primaryKey', 'indexes'], additionalProperties: false }),
215
- annotations: { readOnlyHint: true },
216
- },
217
- 'user.ensureSchema': {
218
- mcp: false,
219
- auth: 'user',
220
- title: '初始化档案表',
221
- description: '幂等初始化建档表与索引(部署动作,登录链路不执行)',
222
- inputSchema: obj({}, { required: [], additionalProperties: false }),
223
- outputSchema: obj({
224
- table: { type: 'string', enum: ['created', 'exists'], description: '本次建表结果' },
225
- indexes: { type: 'object', description: '{ created: string[], existing: string[] }' },
226
- status: { type: 'object', description: '执行后的探测结果' },
227
- }, { required: ['table', 'indexes', 'status'], additionalProperties: false }),
228
- annotations: { readOnlyHint: true, idempotentHint: true },
229
- },
230
-
231
- /* ---------- user:用户令牌面(user,token 走头) ---------- */
232
- 'user.me': {
233
- auth: 'user',
234
- title: '当前用户',
235
- description: '读取网关侧当前用户信息(资料 / 绑定 / 角色)',
236
- inputSchema: obj({ ...apikeyIn }, { required: ['apikey'], additionalProperties: false }),
237
- outputSchema: { type: 'object', description: '网关用户信息(原样)' },
238
- annotations: { readOnlyHint: true },
239
- },
240
- 'user.updateBasic': {
241
- auth: 'user',
242
- title: '修改资料(含手机号/邮箱)',
243
- description: '修改网关侧用户资料;改手机号/邮箱需先 reauthenticate 拿验证码令牌;成功后自动同步本地档案',
244
- inputSchema: obj({
245
- ...apikeyIn,
246
- nickname: str('昵称(可选)'),
247
- username: str('用户名(可选)'),
248
- phone: str('手机号(可选,修改需验证码)'),
249
- email: str('邮箱(可选,修改需验证码)'),
250
- avatar_url: str('头像 URL(可选)'),
251
- gender: { type: 'string', enum: ['MALE', 'FEMALE'], description: '性别(可选)' },
252
- description: str('个人简介(可选)'),
253
- verification_token: str('手机号/邮箱变更的验证码令牌(可选,改手机/邮箱时需要)'),
254
- }, { required: ['apikey'], additionalProperties: true }),
255
- outputSchema: { type: 'object', description: '网关响应(原样)' },
256
- },
257
- 'user.changePassword': {
258
- auth: 'user',
259
- title: '修改密码',
260
- description: '修改当前用户密码(需先 reauthenticate 二次验证)',
261
- inputSchema: obj({
262
- ...apikeyIn,
263
- password: str('新密码(必填)'),
264
- verification_token: str('二次验证令牌(可选,按网关要求)'),
265
- }, { required: ['apikey', 'password'] }),
266
- outputSchema: { type: 'object', description: '网关响应(通常为空对象)' },
267
- annotations: { destructiveHint: true },
268
- },
269
- 'user.deleteMe': {
270
- auth: 'user',
271
- title: '注销账号',
272
- description: '注销当前用户(需先 reauthenticate 二次验证)',
273
- inputSchema: obj({
274
- ...apikeyIn,
275
- verification_token: str('二次验证令牌(可选,按网关要求)'),
276
- }, { required: ['apikey'], additionalProperties: true }),
277
- outputSchema: { type: 'object', description: '网关响应(通常为空对象)' },
278
- annotations: { destructiveHint: true },
279
- },
280
-
281
- /* ---------- key 域(DC-06)+ cbToken(mcp:false:宿主能力,挂 MCP = 可冒充任意 uid 兑官方 token) ---------- */
282
- 'auth.key.create': {
283
- auth: 'user',
284
- title: '签发新 apikey',
285
- description: '用当前 apikey 换 uid 再签发一个新 apikey(多设备/多场景);key 明文只在本次响应出现一次',
286
- inputSchema: obj({ apikey: str('当前 apikey(必填)'), scene: str('场景标签(可选,缺省空串)') }, { required: ['apikey'], additionalProperties: false }),
287
- outputSchema: obj({
288
- apikey: str('新 apikey 明文(必返,仅此一次)'),
289
- key_id: str('key id(吊销/list 句柄)'),
290
- key_prefix: str('前 11 字符识别段(不含敏感信息)'),
291
- expires_at: { type: 'number', description: '过期 unix 秒' },
292
- }, { required: ['apikey', 'key_id', 'key_prefix', 'expires_at'], additionalProperties: false }),
293
- },
294
- 'auth.key.list': {
295
- auth: 'user',
296
- title: '列出我的 apikey',
297
- description: '当前 apikey 所属用户的全部 key 档案(不含 key 明文)',
298
- inputSchema: obj({ apikey: str('当前 apikey(必填)') }, { required: ['apikey'], additionalProperties: false }),
299
- outputSchema: { type: 'array', description: 'key 档案行数组(key_id/key_prefix/scene/created_at/expires_at/revoked_at)' },
300
- annotations: { readOnlyHint: true },
301
- },
302
- 'auth.key.revoke': {
303
- auth: 'user',
304
- title: '吊销 apikey',
305
- description: '吊销本用户名下的指定 key(即时生效)',
306
- inputSchema: obj({ apikey: str('当前 apikey(必填)'), key_id: str('要吊销的 key id(必填)') }, { required: ['apikey', 'key_id'], additionalProperties: false }),
307
- outputSchema: obj({ revoked: { type: 'boolean', description: '是否成功' }, key_id: str('被吊销的 key id') }, { required: ['revoked', 'key_id'], additionalProperties: false }),
308
- annotations: { destructiveHint: true },
309
- },
310
- 'auth.key.verify': {
311
- auth: 'public',
312
- title: '验证 apikey',
313
- description: '验证 apikey 合法性:通过返回 { ok, auth },失败返回 { ok:false, reason }',
314
- inputSchema: obj({ apikey: str('待验证的 apikey(必填)') }, { required: ['apikey'], additionalProperties: false }),
315
- outputSchema: { type: 'object', description: 'verify 结果(ok/auth 或 ok/reason)' },
316
- annotations: { readOnlyHint: true },
317
- },
318
- 'auth.cbToken.get': {
319
- mcp: false,
320
- auth: 'user',
321
- title: '官方 token 托管兑换',
322
- description: 'uid → 托管官方 access_token(宿主透传专用;不挂 MCP)',
323
- inputSchema: obj({ uid: uidField }, { required: ['uid'], additionalProperties: false }),
324
- outputSchema: obj({
325
- access_token: str('官方 access_token'),
326
- expires_at: { type: 'number', description: '过期 unix 秒' },
327
- from_cache: { type: 'boolean', description: '是否命中托管缓存' },
328
- }, { required: ['access_token', 'expires_at', 'from_cache'], additionalProperties: false }),
329
- },
330
- }
331
-
332
- /** 把元数据合并进 [key, ability] 列表(能力对象原样保留 describe/run) */
333
- export const withSchemas = entries => entries.map(([key, ability]) => [key, { ...ability, ...(SCHEMAS[key] ?? {}) }])