@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/dist/access-key.js +1 -0
- package/dist/auth.js +1 -0
- package/dist/index.js +1 -0
- package/dist/login-ticket.js +1 -0
- package/dist/schemas.js +1 -0
- package/dist/token-broker.js +1 -0
- package/dist/user.js +1 -0
- package/dist/wechat.js +1 -0
- package/package.json +15 -6
- package/src/access-key.js +0 -130
- package/src/auth.js +0 -229
- package/src/index.js +0 -70
- package/src/schemas.js +0 -333
- package/src/token-broker.js +0 -62
- package/src/user.js +0 -241
- package/src/wechat.js +0 -20
package/src/token-broker.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* token-broker.js — 官方 token 托管(单飞刷新 · spec §3.6 · §3.7)
|
|
3
|
-
*
|
|
4
|
-
* 职责: get(uid) 查 __user_token_cache → 未过期直返;过期则单飞刷新(实例内 Map<uid, Promise>)→ 回写;
|
|
5
|
-
* upsert(uid, access_token, refresh_token, expiresAt) 供登录收尾落库;markInvalid(uid) 删行。
|
|
6
|
-
* 失败语义: 刷新失败/无行 → 删行 + 401「官方凭证已失效,请重新登录」(fail-closed,refresh_token 不出域)。
|
|
7
|
-
* 接缝: runSql=kit.ctx.run('pg.exec',…) 封装(§3.7 唯一表读写入口;未来换后端只换适配器)。
|
|
8
|
-
* 禁止: 档案表(__user_access_key)读写——那是 access-key.js 的地盘。
|
|
9
|
-
*/
|
|
10
|
-
const SQL_SELECT = 'SELECT uid, access_token, refresh_token, expires_at FROM __user_token_cache WHERE uid=$1'
|
|
11
|
-
const SQL_UPSERT = `INSERT INTO __user_token_cache (uid, access_token, refresh_token, expires_at)
|
|
12
|
-
VALUES ($1, $2, $3, $4)
|
|
13
|
-
ON CONFLICT (uid) DO UPDATE SET access_token = EXCLUDED.access_token,
|
|
14
|
-
refresh_token = EXCLUDED.refresh_token, expires_at = EXCLUDED.expires_at, updated_at = now()`
|
|
15
|
-
const SQL_DELETE = 'DELETE FROM __user_token_cache WHERE uid=$1'
|
|
16
|
-
|
|
17
|
-
const err = (code, message) => Object.assign(new Error(message), { code })
|
|
18
|
-
const nowSec = () => Math.floor(Date.now() / 1000)
|
|
19
|
-
|
|
20
|
-
/** pg 适配器:全包唯一表读写接缝(kit.ctx.run('pg.exec') + cloudbase_postgres 角色) */
|
|
21
|
-
export const makeRunSql = kit => (sql, parameters = []) =>
|
|
22
|
-
kit.ctx.run('pg.exec', { sql, parameters, role: 'cloudbase_postgres' })
|
|
23
|
-
|
|
24
|
-
export function createCbTokenBroker({ cfg, refreshTokenRequest, runSql }) {
|
|
25
|
-
const inflight = new Map() // uid → Promise(实例内单飞;跨实例靠 DB 幂等写兜底)
|
|
26
|
-
|
|
27
|
-
// 单飞刷新:失败删行 + 401;成功回写(expires_at=now+expires_in)
|
|
28
|
-
async function refresh(uid, row) {
|
|
29
|
-
let res
|
|
30
|
-
try {
|
|
31
|
-
res = await refreshTokenRequest({ grant_type: 'refresh_token', refresh_token: row.refresh_token, client_id: cfg.envId })
|
|
32
|
-
} catch {
|
|
33
|
-
await runSql(SQL_DELETE, [uid]).catch(() => {})
|
|
34
|
-
throw err(401, '官方凭证已失效,请重新登录')
|
|
35
|
-
}
|
|
36
|
-
const expiresAt = nowSec() + (Number(res?.expires_in) || 0)
|
|
37
|
-
await runSql(SQL_UPSERT, [uid, res.access_token, res.refresh_token, expiresAt])
|
|
38
|
-
return { access_token: res.access_token, expires_at: expiresAt, from_cache: false }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function get(uid) {
|
|
42
|
-
const rows = await runSql(SQL_SELECT, [uid])
|
|
43
|
-
const row = rows?.[0]
|
|
44
|
-
if (row && Number(row.expires_at) > nowSec()) {
|
|
45
|
-
return { access_token: row.access_token, expires_at: Number(row.expires_at), from_cache: true }
|
|
46
|
-
}
|
|
47
|
-
if (!row) throw err(401, '官方凭证已失效,请重新登录') // 无行 = 从未托管/已失效,无可刷新
|
|
48
|
-
if (inflight.has(uid)) return inflight.get(uid)
|
|
49
|
-
const task = refresh(uid, row).finally(() => inflight.delete(uid))
|
|
50
|
-
inflight.set(uid, task)
|
|
51
|
-
return task
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const upsert = async (uid, access_token, refresh_token, expiresAt) => {
|
|
55
|
-
await runSql(SQL_UPSERT, [uid, access_token, refresh_token, expiresAt])
|
|
56
|
-
return { uid, expires_at: expiresAt }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const markInvalid = async uid => { await runSql(SQL_DELETE, [uid]); return { uid } }
|
|
60
|
-
|
|
61
|
-
return { get, upsert, markInvalid }
|
|
62
|
-
}
|
package/src/user.js
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* user.js — user 域能力目录(10 条)+ SQL 纯函数 + token/网关共用件(spec §3.6 · §3.9 · §3.10)
|
|
3
|
-
*
|
|
4
|
-
* 建档面(本地 pg):
|
|
5
|
-
* user.upsert { uid, profile?, env? } — 建档/刷 updated_at(ns 参数化 ARRAY['uid:'||$4]::text[],ISSUE-01)
|
|
6
|
-
* user.get { uid } — 查档(不过滤 status/deleted_at,403 判定留调用方;无行 → null)
|
|
7
|
-
* user.patchProfile { uid, patch } — 档案段更新(jsonb_set 浅合并 profile 段);0 行 → upsert 建档 →
|
|
8
|
-
* 重查 → active 未删重试一次;仍不可用 → 403「用户不可用」(tag/status/ns 形状同 upsert,不新增表)。
|
|
9
|
-
*
|
|
10
|
-
* schema 面(§3.11,幂等初始化;不在 createKit/登录链路隐式执行,调用方显式调):
|
|
11
|
-
* user.schemaStatus — 只读探测(表/四索引/主键);user.ensureSchema — 缺什么补什么(DDL 全 if not exists,
|
|
12
|
-
* 每条单独 pg.exec(网关不支持多语句,实测 DATABASE_42601);顺序:建表→主键→唯一→gin→owner/identity_type;
|
|
13
|
-
* 主键仅缺失时补;DDL 失败原样抛 { code: 网关状态, payload },不静默降级)。
|
|
14
|
-
*
|
|
15
|
-
* token 面(4 条,网关透传不裁剪;inject.gateway(method, path, { body, token }) 统一覆盖):
|
|
16
|
-
* user.me — GET;user.updateBasic — POST /auth/v1/user/basic/edit(body 只发传入字段,
|
|
17
|
-
* 成功 → patchProfile({ uid: verify.uidRef, patch: 可入档字段 }),verification_token 不入档);
|
|
18
|
-
* user.changePassword PATCH /auth/v1/user/password;user.deleteMe DELETE /auth/v1/user/me。
|
|
19
|
-
*
|
|
20
|
-
* 硬规则(§3.3): token 类能力入参 access_token → apikey;统一序 verify(apikey) → 401「apikey 无效」→
|
|
21
|
-
* broker.get(uid) → 官方 Bearer;**不接收调用方传 uid** 防越权。changePassword/deleteMe 不自动写档。
|
|
22
|
-
*
|
|
23
|
-
* 数据流: 入参(uid 校验/token 解码)→ gateway 网关原样 / build*Query → ctx.run('pg.exec',
|
|
24
|
-
* { sql, parameters, role:'cloudbase_postgres' });网关非 2xx → { code: 网关状态, payload } 原样,
|
|
25
|
-
* 网络异常 → 502「登录请求失败」。
|
|
26
|
-
* 下期留白(不实现):user.updateSection 旧接口(patchProfile 取代)/ user.bindPhone(crm_cache access_token)。
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { withSchemas } from './schemas.js'
|
|
30
|
-
import { DDL_USER_TOKEN_CACHE, DDL_USER_ACCESS_KEY, DDL_USER_ACCESS_KEY_INDEX } from './access-key.js'
|
|
31
|
-
|
|
32
|
-
const DEFAULT_DOMAIN = 'api.tcloudbasegateway.com'
|
|
33
|
-
|
|
34
|
-
/* —— 共用件:JWT 不验签解码 / token 身份硬规则 / 网关错误透传 / gateway 注入缺省 —— */
|
|
35
|
-
export function decodeJWT(token) {
|
|
36
|
-
try { return JSON.parse(Buffer.from(String(token).split('.')[1], 'base64url').toString('utf8')) } catch { return {} }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// token 类能力身份来源:uid 只认 JWT.sub(防越权);解码失败 → 400「access_token 无效」
|
|
40
|
-
export function uidFromToken(access_token) {
|
|
41
|
-
const sub = decodeJWT(access_token)?.sub
|
|
42
|
-
if (!sub) throw Object.assign(new Error('access_token 无效'), { code: 400 })
|
|
43
|
-
return sub
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// 网关错误透传:非 2xx → { code: 网关状态, payload } 原样(不改写错误码);无 status = 网络层 → 502
|
|
47
|
-
export const throwGatewayError = e => {
|
|
48
|
-
throw e?.status
|
|
49
|
-
? Object.assign(new Error(e.message), { code: e.status, payload: e.payload })
|
|
50
|
-
: Object.assign(new Error('登录请求失败'), { code: 502 })
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// inject.gateway(method, path, { body, token }) — §3.8/§3.9 全部网关调用的统一注入口(缺省 kit.req + Bearer)
|
|
54
|
-
export function makeGateway(cfg, kit) {
|
|
55
|
-
return cfg.inject?.gateway || ((method, path, { body, token } = {}) =>
|
|
56
|
-
kit.req(`https://${cfg.envId}.${cfg.domain || DEFAULT_DOMAIN}${path}`, {
|
|
57
|
-
method,
|
|
58
|
-
...(body !== undefined ? { body } : {}),
|
|
59
|
-
...(token ? { headers: { Authorization: `Bearer ${token}` } } : {}),
|
|
60
|
-
}))
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/* —— uid 校验(建档面入口;ISSUE-01)—— */
|
|
64
|
-
export const assertUid = uid => {
|
|
65
|
-
if (typeof uid !== 'string' || uid === '') throw Object.assign(new Error('缺少 uid'), { code: 400 })
|
|
66
|
-
if (uid.length > 128 || /[\u0000-\u001f\u007f]/.test(uid)) throw Object.assign(new Error('缺少 uid'), { code: 400 })
|
|
67
|
-
return uid
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/* —— SQL 纯函数(逐字对齐现网形状)—— */
|
|
71
|
-
export function buildUpsertUserQuery(uid, profile, env) {
|
|
72
|
-
return {
|
|
73
|
-
// 只 upsert data:冲突分支逐字照抄 spec §3.6(主场已实库验证;t = 本表别名)
|
|
74
|
-
sql: `INSERT INTO public.crm_user_detail AS t (ns, data, ext, tag, status)
|
|
75
|
-
VALUES (ARRAY['uid:' || $4]::text[], $1::jsonb, NULL, $2::text[] || ARRAY['from_env:'||$3]::text[], 'active')
|
|
76
|
-
ON CONFLICT (ns) WHERE (deleted_at IS NULL)
|
|
77
|
-
DO UPDATE SET
|
|
78
|
-
data = CASE WHEN EXCLUDED.data ? 'profile' THEN
|
|
79
|
-
(COALESCE(t.data,'{}'::jsonb) || EXCLUDED.data)
|
|
80
|
-
|| jsonb_build_object('profile', COALESCE(t.data->'profile','{}'::jsonb) || COALESCE(EXCLUDED.data->'profile','{}'::jsonb))
|
|
81
|
-
ELSE COALESCE(t.data,'{}'::jsonb) || EXCLUDED.data END,
|
|
82
|
-
updated_at = NOW()
|
|
83
|
-
RETURNING id, data`,
|
|
84
|
-
parameters: [JSON.stringify({ profile: profile ?? {} }), [], env || 'prod', uid],
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function buildSelectUserQuery(uid) {
|
|
89
|
-
return {
|
|
90
|
-
sql: `SELECT id, data, status, deleted_at
|
|
91
|
-
FROM public.crm_user_detail
|
|
92
|
-
WHERE $1 = ANY(ns)
|
|
93
|
-
ORDER BY created_at ASC
|
|
94
|
-
LIMIT 1`,
|
|
95
|
-
// uid 是绑定参数值(=ANY(ns) 比较),无注入面;拼接避免模板字面量(ISSUE-01 grep 白名单)
|
|
96
|
-
parameters: ['uid:' + uid],
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// patchProfile:profile 段内浅合并(对齐 nuxt-layer-api-user updateUserSection 语义)
|
|
101
|
-
export function buildPatchProfileQuery(uid, patch) {
|
|
102
|
-
return {
|
|
103
|
-
sql: `UPDATE public.crm_user_detail
|
|
104
|
-
SET data = jsonb_set(
|
|
105
|
-
COALESCE(data, '{}'::jsonb),
|
|
106
|
-
ARRAY['profile'],
|
|
107
|
-
COALESCE(data->'profile', '{}'::jsonb) || $2::jsonb,
|
|
108
|
-
true
|
|
109
|
-
),
|
|
110
|
-
updated_at = NOW()
|
|
111
|
-
WHERE $1 = ANY(ns)
|
|
112
|
-
AND status = 'active'
|
|
113
|
-
AND deleted_at IS NULL
|
|
114
|
-
RETURNING id, data`,
|
|
115
|
-
parameters: ['uid:' + uid, JSON.stringify(patch ?? {})],
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/* —— schema 面(§3.11 建档表 + §3.1 两张新表;DDL 全 if not exists,先探后建)—— */
|
|
120
|
-
const PROBE_TABLE = `select to_regclass('public.crm_user_detail') is not null as "table", to_regclass('public.__user_token_cache') is not null as "token_cache", to_regclass('public.__user_access_key') is not null as "access_key"`
|
|
121
|
-
const PROBE_INDEXES = `select indexname from pg_indexes where schemaname='public' and tablename='crm_user_detail'`
|
|
122
|
-
const PROBE_PKEY = `select 1 from pg_constraint where conrelid='public.crm_user_detail'::regclass and contype='p'`
|
|
123
|
-
const DDL_TABLE = `create table if not exists public.crm_user_detail (id uuid default gen_random_uuid() not null, ns text[] not null, data jsonb default '{}'::jsonb not null, ext jsonb, tag text[] default '{}'::text[], status text default 'active' not null, created_at timestamptz default now() not null, updated_at timestamptz default now() not null, deleted_at timestamptz)`
|
|
124
|
-
const DDL_PKEY = `alter table public.crm_user_detail add constraint crm_user_detail_pkey primary key (id)` // 建表不带 pk(与 schema.full.sql 一致),仅缺失时单独补
|
|
125
|
-
const DDL_INDEXES = {
|
|
126
|
-
idx_crm_user_detail_ns_unique: `create unique index if not exists idx_crm_user_detail_ns_unique on public.crm_user_detail using btree (ns) where (deleted_at is null)`, // upsert 的 ON CONFLICT 依赖它,必须有
|
|
127
|
-
idx_crm_user_detail_ns: `create index if not exists idx_crm_user_detail_ns on public.crm_user_detail using gin (ns)`,
|
|
128
|
-
idx_crm_user_detail_owner: `create index if not exists idx_crm_user_detail_owner on public.crm_user_detail using btree (((data #>> '{identity,owner_uid}'::text[]))) where (deleted_at is null)`,
|
|
129
|
-
idx_crm_user_detail_identity_type: `create index if not exists idx_crm_user_detail_identity_type on public.crm_user_detail using btree (((data #>> '{identity,type}'::text[]))) where (deleted_at is null)`,
|
|
130
|
-
}
|
|
131
|
-
const INDEX_ORDER = ['idx_crm_user_detail_ns_unique', 'idx_crm_user_detail_ns', 'idx_crm_user_detail_owner', 'idx_crm_user_detail_identity_type']
|
|
132
|
-
|
|
133
|
-
/* —— user 域 token 面字段表 —— */
|
|
134
|
-
const EDIT_BODY = ['nickname', 'username', 'phone', 'email', 'avatar_url', 'gender', 'description', 'verification_token']
|
|
135
|
-
const ARCHIVABLE = ['nickname', 'username', 'phone', 'email', 'avatar_url', 'gender', 'description']
|
|
136
|
-
|
|
137
|
-
// deps(index.js 注入): verify(apikey 裁决)/ broker(token 托管,官方 Bearer 来源)
|
|
138
|
-
export function createUserAbilities(cfg, kit, deps = {}) {
|
|
139
|
-
const gateway = makeGateway(cfg, kit)
|
|
140
|
-
// 网关调用统一翻错(非 2xx 原样 / 网络 502)
|
|
141
|
-
const gw = async (method, path, opts) => { try { return await gateway(method, path, opts) } catch (e) { throwGatewayError(e) } }
|
|
142
|
-
// 凭证类统一序(§3.3):verify(apikey) → 401 → broker.get(uid) → 官方 Bearer
|
|
143
|
-
const sessionOf = async apikey => {
|
|
144
|
-
const v = await deps.verify(apikey)
|
|
145
|
-
if (!v.ok) throw Object.assign(new Error('apikey 无效'), { code: 401 })
|
|
146
|
-
const token = await deps.broker.get(v.auth.uidRef)
|
|
147
|
-
return { uid: v.auth.uidRef, access_token: token.access_token }
|
|
148
|
-
}
|
|
149
|
-
const pg = q => kit.ctx.run('pg.exec', { sql: q.sql, parameters: q.parameters, role: 'cloudbase_postgres' })
|
|
150
|
-
// 探测:表在不在 / 四索引在不在 / 主键在不在(只读,三次 pg.exec)
|
|
151
|
-
const probe = async () => {
|
|
152
|
-
const [t, i, k] = [await pg({ sql: PROBE_TABLE, parameters: [] }), await pg({ sql: PROBE_INDEXES, parameters: [] }), await pg({ sql: PROBE_PKEY, parameters: [] })]
|
|
153
|
-
const names = new Set(i.map(r => r.indexname))
|
|
154
|
-
return {
|
|
155
|
-
table: !!t[0]?.table, tokenCache: !!t[0]?.token_cache, accessKey: !!t[0]?.access_key,
|
|
156
|
-
indexes: Object.fromEntries(INDEX_ORDER.map(n => [n, names.has(n)])), primaryKey: k.length > 0,
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return withSchemas([
|
|
161
|
-
/* 建档面(本地 pg) */
|
|
162
|
-
['user.upsert', { describe: '建档/刷 updated_at(crm_user_detail;写走 cloudbase_postgres)', run: async (args = {}) => {
|
|
163
|
-
const { uid, profile, env } = args
|
|
164
|
-
const rows = await pg(buildUpsertUserQuery(assertUid(uid), profile, env ?? cfg.env))
|
|
165
|
-
return rows?.[0] ?? null
|
|
166
|
-
} }],
|
|
167
|
-
|
|
168
|
-
['user.get', { describe: '查档(不过滤 status/deleted_at;无行 → null)', run: async (args = {}) => {
|
|
169
|
-
const rows = await pg(buildSelectUserQuery(assertUid(args.uid)))
|
|
170
|
-
return rows?.[0] ?? null
|
|
171
|
-
} }],
|
|
172
|
-
|
|
173
|
-
['user.patchProfile', { describe: '档案段更新(profile 浅合并;0 行→建档→重试一次→403 用户不可用)', run: async (args = {}) => {
|
|
174
|
-
const uid = assertUid(args.uid)
|
|
175
|
-
const rows = await pg(buildPatchProfileQuery(uid, args.patch))
|
|
176
|
-
if (rows?.length) return rows[0]
|
|
177
|
-
await kit.ctx.run('user.upsert', { uid }) // 0 行 → 先建档
|
|
178
|
-
const check = await kit.ctx.run('user.get', { uid }) // 重查
|
|
179
|
-
if (check?.status === 'active' && !check.deleted_at) {
|
|
180
|
-
const rows2 = await pg(buildPatchProfileQuery(uid, args.patch)) // active 未删 → 重试一次
|
|
181
|
-
if (rows2?.length) return rows2[0]
|
|
182
|
-
}
|
|
183
|
-
throw Object.assign(new Error('用户不可用'), { code: 403 })
|
|
184
|
-
} }],
|
|
185
|
-
|
|
186
|
-
/* token 面(网关透传;身份 = apikey → verify.uidRef → broker 托管 token) */
|
|
187
|
-
['user.me', { describe: '当前用户(资料/绑定/角色):GET /auth/v1/user/me,网关原样', run: async ({ apikey } = {}) => {
|
|
188
|
-
const { access_token } = await sessionOf(apikey)
|
|
189
|
-
return gw('GET', '/auth/v1/user/me', { token: access_token })
|
|
190
|
-
} }],
|
|
191
|
-
|
|
192
|
-
['user.updateBasic', { describe: '资料/手机/邮箱变更:POST /auth/v1/user/basic/edit(body 只发传入字段)→ 成功 patchProfile', run: async (args = {}) => {
|
|
193
|
-
const { uid, access_token } = await sessionOf(args.apikey)
|
|
194
|
-
const body = {}
|
|
195
|
-
for (const k of EDIT_BODY) if (args[k] !== undefined) body[k] = args[k]
|
|
196
|
-
const payload = await gw('POST', '/auth/v1/user/basic/edit', { body, token: access_token })
|
|
197
|
-
const patch = {}
|
|
198
|
-
for (const k of ARCHIVABLE) if (k in body) patch[k] = body[k] // verification_token 不入档
|
|
199
|
-
await kit.ctx.run('user.patchProfile', { uid, patch })
|
|
200
|
-
return payload
|
|
201
|
-
} }],
|
|
202
|
-
|
|
203
|
-
['user.changePassword', { describe: '改密(前置 reauthenticate):PATCH /auth/v1/user/password,网关原样(不写档)', run: async (args = {}) => {
|
|
204
|
-
const { apikey, ...rest } = args
|
|
205
|
-
const { access_token } = await sessionOf(apikey)
|
|
206
|
-
return gw('PATCH', '/auth/v1/user/password', { body: rest, token: access_token })
|
|
207
|
-
} }],
|
|
208
|
-
|
|
209
|
-
['user.deleteMe', { describe: '删号(前置 reauthenticate):DELETE /auth/v1/user/me,网关原样(不自行标删/不写档)', run: async (args = {}) => {
|
|
210
|
-
const { apikey, ...rest } = args
|
|
211
|
-
const { access_token } = await sessionOf(apikey)
|
|
212
|
-
return gw('DELETE', '/auth/v1/user/me', { body: rest, token: access_token })
|
|
213
|
-
} }],
|
|
214
|
-
|
|
215
|
-
/* schema 面(§3.11:探测只读;补齐逐条 DDL,失败原样透传不静默降级) */
|
|
216
|
-
['user.schemaStatus', { describe: '建档表只读探测:表/四索引/主键在位状态', run: async () => probe() }],
|
|
217
|
-
|
|
218
|
-
['user.ensureSchema', { describe: '建档表幂等初始化:缺什么补什么(DDL 逐条 pg.exec;调用方显式执行,登录不做 DDL)', run: async () => {
|
|
219
|
-
const before = await probe()
|
|
220
|
-
const plan = []
|
|
221
|
-
if (!before.table) plan.push(DDL_TABLE)
|
|
222
|
-
if (!before.tokenCache) plan.push(DDL_USER_TOKEN_CACHE) // §3.1 token 托管表
|
|
223
|
-
if (!before.accessKey) plan.push(DDL_USER_ACCESS_KEY, DDL_USER_ACCESS_KEY_INDEX) // §3.1 key 档案表 + uid 索引
|
|
224
|
-
if (!before.primaryKey) plan.push(DDL_PKEY) // 主键仅缺失时补(alter 无 if not exists)
|
|
225
|
-
const missing = INDEX_ORDER.filter(n => !before.indexes[n])
|
|
226
|
-
for (const name of missing) plan.push(DDL_INDEXES[name])
|
|
227
|
-
for (const sql of plan) {
|
|
228
|
-
try { await pg({ sql, parameters: [] }) } catch (e) {
|
|
229
|
-
throw e?.status ? Object.assign(new Error(e.message), { code: e.status, payload: e.payload }) : e // 原样透传
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return {
|
|
233
|
-
table: before.table ? 'exists' : 'created',
|
|
234
|
-
tokenCache: before.tokenCache ? 'exists' : 'created',
|
|
235
|
-
accessKey: before.accessKey ? 'exists' : 'created',
|
|
236
|
-
indexes: { created: missing, existing: INDEX_ORDER.filter(n => !missing.includes(n)) },
|
|
237
|
-
status: await probe(),
|
|
238
|
-
}
|
|
239
|
-
} }],
|
|
240
|
-
])
|
|
241
|
-
}
|
package/src/wechat.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* wechat.js — 微信接口缺省实现(code2Session · 稳定叶,不在能力面待定范围)
|
|
3
|
-
*
|
|
4
|
-
* 职责: wx.login 的 code 换 openid/unionid/session_key(URLSearchParams 直连,不走 kit 代理)。
|
|
5
|
-
* 数据流: code + cfg.wxAppid/wxSecret → GET api.weixin.qq.com/sns/jscode2session
|
|
6
|
-
* → { openid, unionid?, errcode? };errcode 交给调用方翻译(auth 域 → {code:400, errcode, errmsg})
|
|
7
|
-
* 网络失败抛错由调用方兜底(与 nuxt-layer-api-cb-dependences/server/utils/wechat.ts 同语义)。
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export async function code2Session(cfg, code) {
|
|
11
|
-
const params = new URLSearchParams({
|
|
12
|
-
appid: cfg.wxAppid,
|
|
13
|
-
secret: cfg.wxSecret,
|
|
14
|
-
js_code: code,
|
|
15
|
-
grant_type: 'authorization_code',
|
|
16
|
-
})
|
|
17
|
-
const res = await fetch(`https://api.weixin.qq.com/sns/jscode2session?${params}`)
|
|
18
|
-
if (!res.ok) throw new Error(`code2Session request failed with status ${res.status}`)
|
|
19
|
-
return await res.json()
|
|
20
|
-
}
|