@cloudbase/app 3.0.4 → 3.0.6

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.
Files changed (47) hide show
  1. package/dist/cjs/index.js +13 -9
  2. package/dist/cjs/libs/adapter-node/constants.d.ts +33 -0
  3. package/dist/cjs/libs/adapter-node/constants.js +38 -0
  4. package/dist/cjs/libs/adapter-node/context.d.ts +13 -0
  5. package/dist/cjs/libs/adapter-node/context.js +162 -0
  6. package/dist/cjs/libs/adapter-node/index.d.ts +23 -0
  7. package/dist/cjs/libs/adapter-node/index.js +124 -0
  8. package/dist/cjs/libs/adapter-node/metadata.d.ts +14 -0
  9. package/dist/cjs/libs/adapter-node/metadata.js +130 -0
  10. package/dist/cjs/libs/adapter-node/request.d.ts +38 -0
  11. package/dist/cjs/libs/adapter-node/request.js +440 -0
  12. package/dist/cjs/libs/adapter-node/tool.d.ts +2 -0
  13. package/dist/cjs/libs/adapter-node/tool.js +227 -0
  14. package/dist/cjs/libs/adapter-node/types.d.ts +78 -0
  15. package/dist/cjs/libs/adapter-node/types.js +3 -0
  16. package/dist/cjs/libs/adapter-node/utils.d.ts +17 -0
  17. package/dist/cjs/libs/adapter-node/utils.js +221 -0
  18. package/dist/esm/index.js +13 -9
  19. package/dist/esm/libs/adapter-node/constants.d.ts +33 -0
  20. package/dist/esm/libs/adapter-node/constants.js +35 -0
  21. package/dist/esm/libs/adapter-node/context.d.ts +13 -0
  22. package/dist/esm/libs/adapter-node/context.js +156 -0
  23. package/dist/esm/libs/adapter-node/index.d.ts +23 -0
  24. package/dist/esm/libs/adapter-node/index.js +118 -0
  25. package/dist/esm/libs/adapter-node/metadata.d.ts +14 -0
  26. package/dist/esm/libs/adapter-node/metadata.js +123 -0
  27. package/dist/esm/libs/adapter-node/request.d.ts +38 -0
  28. package/dist/esm/libs/adapter-node/request.js +437 -0
  29. package/dist/esm/libs/adapter-node/tool.d.ts +2 -0
  30. package/dist/esm/libs/adapter-node/tool.js +223 -0
  31. package/dist/esm/libs/adapter-node/types.d.ts +78 -0
  32. package/dist/esm/libs/adapter-node/types.js +2 -0
  33. package/dist/esm/libs/adapter-node/utils.d.ts +17 -0
  34. package/dist/esm/libs/adapter-node/utils.js +205 -0
  35. package/dist/miniprogram/index.js +1 -1
  36. package/package.json +20 -4
  37. package/src/index.ts +14 -2
  38. package/src/libs/adapter-node/constants.ts +42 -0
  39. package/src/libs/adapter-node/context.ts +238 -0
  40. package/src/libs/adapter-node/index.ts +166 -0
  41. package/src/libs/adapter-node/metadata.ts +69 -0
  42. package/src/libs/adapter-node/request.ts +486 -0
  43. package/src/libs/adapter-node/tool.ts +223 -0
  44. package/src/libs/adapter-node/types.ts +116 -0
  45. package/src/libs/adapter-node/utils.ts +182 -0
  46. package/webpack/web.prod.js +14 -13
  47. package/webpack/webpack.miniprogram.js +10 -3
@@ -0,0 +1,486 @@
1
+ /* eslint-disable no-nested-ternary */
2
+ import {
3
+ IFetchOptions,
4
+ IRequestConfig,
5
+ IRequestMethod,
6
+ IRequestOptions,
7
+ ResponseObject,
8
+ SDKRequestInterface,
9
+ formatUrl,
10
+ } from '@cloudbase/adapter-interface'
11
+ import { ADMIN_PATH, CLIENT_AUTH_PATH } from './constants'
12
+ import { getCloudbaseContext, getSecretInfo, INIT_CONFIG } from './context'
13
+ import type { ICustomReqOpts } from './types'
14
+ import {
15
+ getEnv,
16
+ isFormData,
17
+ toQueryString,
18
+ safeParseJson,
19
+ obj2StrRecord,
20
+ headersInit2Indexable,
21
+ getCurrRunEnvTag,
22
+ } from './utils'
23
+ import { ICloudbaseConfig } from '@cloudbase/types'
24
+ import { getSdkVersion } from '../../constants/common'
25
+ // 延迟加载,避免非 Node 环境打包时引入此包
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ let signFn: ((...args: any[]) => any) | null = null
28
+ async function getSign() {
29
+ if (!signFn) {
30
+ try {
31
+ // @ts-ignore — 该包仅在 Node 运行时存在,开发环境可能未安装
32
+ const mod = await import('@cloudbase/signature-nodejs')
33
+ signFn = mod.sign
34
+ } catch (e) {
35
+ throw new Error('缺少依赖 @cloudbase/signature-nodejs,请执行以下命令安装:\n\n'
36
+ + ' npm install @cloudbase/signature-nodejs\n\n'
37
+ + '该依赖用于 Node 环境下的请求签名。')
38
+ }
39
+ }
40
+ return signFn!
41
+ }
42
+
43
+ /**
44
+ * Node.js 环境下的 HTTP 请求实现
45
+ * 基于原生 fetch,支持 GET/POST/PUT/上传/下载/流式请求
46
+ * 自动处理 V3 签名和 Bearer Token 认证
47
+ */
48
+ export class NodeRequest implements SDKRequestInterface {
49
+ /** 请求配置,包含认证信息 */
50
+ config: IRequestConfig & ICloudbaseConfig
51
+
52
+ /** 请求超时时间(毫秒),默认 15000 */
53
+ private readonly timeout: number
54
+
55
+ /** 受超时控制的请求类型列表 */
56
+ private readonly restrictedMethods: Array<IRequestMethod>
57
+
58
+ constructor(config: IRequestConfig & ICloudbaseConfig) {
59
+ const { timeout, restrictedMethods } = config
60
+ this.timeout = timeout || 15000
61
+ this.restrictedMethods = restrictedMethods || ['get', 'post', 'upload', 'download']
62
+ this.config = config
63
+ }
64
+
65
+ /**
66
+ * 获取客户端凭证 access_token,为管理员权限
67
+ * 用于不支持 V3 签名的接口(如数据模型、关系型数据库、AI)
68
+ */
69
+ async getClientCredential(opts: { origin: string }): Promise<any> {
70
+ const res = await this.fetch({
71
+ url: `${opts.origin}${CLIENT_AUTH_PATH}`,
72
+ method: 'POST',
73
+ headers: {
74
+ 'content-type': 'application/json',
75
+ },
76
+ body: JSON.stringify({
77
+ grant_type: 'client_credentials',
78
+ }),
79
+ })
80
+
81
+ return res.data?.access_token
82
+ }
83
+
84
+ /**
85
+ * 获取实际请求 URL
86
+ * 当请求未携带 Authorization 且未自带 access_token 时,
87
+ * 将 /web 路径替换为 /admin(管理端签名路径)
88
+ */
89
+ getRealUrl(url: string, headers: Record<string, any>, body: any) {
90
+ if (headers.Authorization?.replace('Bearer', '')?.trim()) {
91
+ return url
92
+ }
93
+
94
+ const params = typeof body === 'object' ? body : safeParseJson(body) || ''
95
+
96
+ if (params.access_token) {
97
+ return url
98
+ }
99
+
100
+ const urlObj = new URL(url)
101
+
102
+ if (urlObj.pathname === '/web' && this.config.auth?.secretId && this.config.auth?.secretKey) {
103
+ urlObj.pathname = ADMIN_PATH
104
+ }
105
+ return urlObj.toString()
106
+ }
107
+
108
+ /**
109
+ * 为请求添加签名或认证信息
110
+ *
111
+ * 处理逻辑优先级:
112
+ * 1. 已有 Authorization header → 直接返回
113
+ * 2. /auth/ 路径 → SDK 认证接口,无需签名
114
+ * 3. 不支持 V3 签名的路径(/v1/model/ 等)→ 用 clientCredential Bearer Token
115
+ * 4. 参数含 access_token → 无需签名
116
+ * 5. 其他 → 使用 V3 签名(TC3-HMAC-SHA256)
117
+ */
118
+ public getReqOptions = async (
119
+ url: string,
120
+ options: {
121
+ method?: any
122
+ headers: any
123
+ body?: any
124
+ credentials?: string
125
+ signal?: AbortSignal
126
+ url?: any
127
+ },
128
+ ) => {
129
+ const urlObj = new URL(url)
130
+
131
+ const { TCB_SOURCE } = getCloudbaseContext()
132
+ // Note: 云函数被调用时可能调用端未传递 SOURCE,TCB_SOURCE 可能为空
133
+ const SOURCE = `${INIT_CONFIG.context?.extendedContext?.source || TCB_SOURCE || ''},${await getCurrRunEnvTag()}`
134
+
135
+ options.headers = {
136
+ ...options.headers,
137
+ 'User-Agent': `adapter-node/${options.headers?.['X-SDK-Version'] || `@cloudbase/js-sdk/${getSdkVersion()}`}`,
138
+ 'X-TCB-Source': SOURCE,
139
+ 'X-Client-Timestamp': new Date().valueOf(),
140
+ 'X-TCB-Region': INIT_CONFIG.region || getEnv('TENCENTCLOUD_REGION') || '',
141
+ Host: urlObj.host,
142
+ }
143
+
144
+ // 已携带有效 Authorization,直接返回
145
+ if (options.headers.Authorization?.replace('Bearer', '')?.trim()) {
146
+ return options
147
+ }
148
+
149
+ // auth 相关接口不需要签名
150
+ if (urlObj.pathname.startsWith('/auth/') && urlObj.pathname !== CLIENT_AUTH_PATH) {
151
+ return options
152
+ }
153
+
154
+ const isAdminPath = urlObj.pathname === ADMIN_PATH
155
+
156
+ // 这些路径不支持 V3 签名,改用 clientCredential 获取 Bearer Token
157
+ const UNSUPPORTED_V3_SIGN_PATH = ['/v1/model/', '/v1/rdb/', '/v1/ai/']
158
+
159
+ if (UNSUPPORTED_V3_SIGN_PATH.some(v => urlObj.pathname.startsWith(v))) {
160
+ const token = await this.getClientCredential({ origin: urlObj.origin })
161
+ options.headers.Authorization = `Bearer ${token}`
162
+ return options
163
+ }
164
+
165
+ let { secretId, secretKey, sessionToken } = this.config?.auth || {}
166
+ const { secretType } = this.config?.auth || {}
167
+ const { method, body } = options
168
+ const headers = JSON.parse(JSON.stringify(options.headers))
169
+ let params = typeof body === 'object' ? body : safeParseJson(body) || ''
170
+
171
+ // 参数自带 access_token,无需签名
172
+ if (params.access_token) {
173
+ return options
174
+ }
175
+
176
+ // GET 请求参数已在 URL 中,签名时 params 置空
177
+ if (method.toLowerCase() === 'get') {
178
+ params = undefined
179
+ }
180
+
181
+ // SESSION_SECRET 类型或无密钥时,从环境变量获取临时密钥
182
+ if (!secretId || !secretKey || secretType === 'SESSION_SECRET') {
183
+ const secretInfo = getSecretInfo()
184
+ secretId = secretInfo.secretId
185
+ secretKey = secretInfo.secretKey
186
+ sessionToken = secretInfo.sessionToken
187
+ }
188
+
189
+ // 仍无密钥则跳过签名
190
+ if (!secretId || !secretKey) {
191
+ return options
192
+ }
193
+
194
+ delete headers.Authorization
195
+
196
+ // content-type 需与服务端签名验证保持大小写一致
197
+ if (headers['content-type']) {
198
+ headers['content-type'] = headers['content-type'].toLowerCase()
199
+ } else if (headers['Content-Type']) {
200
+ headers['Content-Type'] = headers['Content-Type'].toLowerCase()
201
+ }
202
+
203
+ if (isAdminPath && !!sessionToken) {
204
+ params.sessionToken = sessionToken
205
+ }
206
+
207
+ const signedParams = {
208
+ secretId,
209
+ secretKey,
210
+ method,
211
+ url,
212
+ params,
213
+ headers,
214
+ withSignedParams: isAdminPath,
215
+ timestamp: Math.floor(new Date().getTime() / 1000) - 1,
216
+ isCloudApi: !isAdminPath,
217
+ }
218
+
219
+
220
+ const sign = await getSign()
221
+ if (!sign) return options
222
+
223
+ const { authorization, timestamp } = sign(signedParams)
224
+
225
+ if (typeof sessionToken === 'string' && sessionToken !== '') {
226
+ if (isAdminPath) {
227
+ headers.Authorization = `${authorization}`
228
+ headers['X-Timestamp'] = timestamp
229
+ headers['X-Signature-Expires'] = 600
230
+ } else {
231
+ // 临时密钥需要额外附带 Token
232
+ headers.Authorization = `${authorization}, Timestamp=${timestamp}, Token=${sessionToken}`
233
+ }
234
+ } else {
235
+ headers.Authorization = `${authorization}, Timestamp=${timestamp}`
236
+
237
+ // admin 路径需要额外的时间戳和过期头
238
+ if (isAdminPath) {
239
+ headers['X-Timestamp'] = timestamp
240
+ headers['X-Signature-Expires'] = 600
241
+ }
242
+ }
243
+
244
+ return {
245
+ ...options,
246
+ headers,
247
+ body: typeof params === 'object' ? JSON.stringify(params) : params,
248
+ }
249
+ }
250
+
251
+ public get = (options: IRequestOptions): Promise<ResponseObject> => this.request(
252
+ {
253
+ ...options,
254
+ method: 'get',
255
+ },
256
+ this.restrictedMethods.includes('get'),
257
+ )
258
+
259
+ public post = (options: IRequestOptions): Promise<ResponseObject> => this.request(
260
+ {
261
+ ...options,
262
+ method: 'post',
263
+ },
264
+ this.restrictedMethods.includes('post'),
265
+ )
266
+
267
+ public put = (options: IRequestOptions): Promise<ResponseObject> => this.request({
268
+ ...options,
269
+ method: 'put',
270
+ })
271
+
272
+ /**
273
+ * 文件上传
274
+ * POST 方式:构建 FormData 上传
275
+ * PUT 方式:直接将文件作为 body 上传
276
+ */
277
+ public upload = (options: IRequestOptions): Promise<ResponseObject> => {
278
+ const { data: _data, file, name, method, headers = {} } = options
279
+ if (file === undefined || name == undefined) {
280
+ throw new Error('file and name is required')
281
+ }
282
+ const data = obj2StrRecord(_data ?? {})
283
+ const loweredMethod = method?.toLowerCase()
284
+ const reqMethod = ['post', 'put'].find(m => m === loweredMethod) ?? 'put'
285
+ const formData = new FormData()
286
+ if (reqMethod === 'post') {
287
+ Object.keys(data).forEach((key) => {
288
+ formData.append(key, data[key])
289
+ })
290
+ formData.append('key', name)
291
+ formData.append('file', file)
292
+ return this.request(
293
+ {
294
+ ...options,
295
+ data: formData,
296
+ method: reqMethod,
297
+ },
298
+ this.restrictedMethods.includes('upload'),
299
+ )
300
+ }
301
+
302
+ return this.request(
303
+ {
304
+ ...options,
305
+ method: 'put',
306
+ headers,
307
+ body: file,
308
+ },
309
+ this.restrictedMethods.includes('upload'),
310
+ )
311
+ }
312
+
313
+ /** 文件下载(浏览器环境,通过创建 <a> 标签触发下载) */
314
+ public download = async (options: IRequestOptions): Promise<unknown> => {
315
+ const { data } = await this.get({
316
+ ...options,
317
+ headers: {},
318
+ responseType: 'blob',
319
+ })
320
+ const url = window.URL.createObjectURL(new Blob([data]))
321
+ const fileName = decodeURIComponent(new URL(options?.url ?? '').pathname.split('/').pop() || '')
322
+ const link = document.createElement('a')
323
+
324
+ link.href = url
325
+ link.setAttribute('download', fileName)
326
+ link.style.display = 'none'
327
+
328
+ document.body.appendChild(link)
329
+ link.click()
330
+ window.URL.revokeObjectURL(url)
331
+ document.body.removeChild(link)
332
+ return new Promise((resolve) => {
333
+ resolve({
334
+ statusCode: 200,
335
+ tempFilePath: options.url,
336
+ })
337
+ })
338
+ }
339
+
340
+ /**
341
+ * 底层 fetch 请求,支持流式响应
342
+ * 超时通过 AbortController + setTimeout 实现
343
+ */
344
+ public fetch = async (options: Omit<IFetchOptions, 'signal'> & { signal?: AbortSignal; customReqOpts?: ICustomReqOpts }): Promise<ResponseObject> => {
345
+ const { enableAbort = false, stream = false, signal, customReqOpts } = options
346
+ const url = this.getRealUrl(options.url, options.headers || {}, options.body)
347
+ const abortController = new AbortController()
348
+ const timeout = customReqOpts?.timeout || this.timeout
349
+
350
+ // 桥接外部 signal 到内部 AbortController
351
+ if (signal) {
352
+ if (signal.aborted) {
353
+ abortController.abort()
354
+ } else {
355
+ signal.addEventListener('abort', () => abortController.abort())
356
+ }
357
+ }
358
+
359
+ // 超时自动中断
360
+ let timer = undefined
361
+ if (enableAbort || timeout) {
362
+ timer = setTimeout(() => {
363
+ const timeoutMsg = `请求在${timeout / 1000}s内未完成,已中断`
364
+ console.warn(timeoutMsg)
365
+ abortController.abort(new Error(timeoutMsg))
366
+ }, timeout)
367
+ }
368
+
369
+ const headers = options.headers ? headersInit2Indexable(options.headers) : undefined
370
+
371
+ const fetchOptions = await this.getReqOptions(url, {
372
+ ...options,
373
+ headers,
374
+ body: options.body as any as NodeJS.ReadableStream,
375
+ signal: abortController.signal,
376
+ })
377
+
378
+ const res = await fetch(url, fetchOptions as RequestInit)
379
+ .then((x) => {
380
+ clearTimeout(timer)
381
+ return x
382
+ })
383
+ .catch((x) => {
384
+ clearTimeout(timer)
385
+ return Promise.reject(x)
386
+ })
387
+
388
+ const ret = {
389
+ data:
390
+ +res.status === 204
391
+ ? '' // 204 No Content
392
+ : stream
393
+ ? res.body
394
+ : await res.json(),
395
+ statusCode: res.status,
396
+ header: res.headers,
397
+ }
398
+ return ret
399
+ }
400
+
401
+ /**
402
+ * 通用请求方法,被 get/post/put/upload 等调用
403
+ * 负责构建 payload、设置超时、解析响应
404
+ *
405
+ * @param options - 请求选项
406
+ * @param enableAbort - 是否启用超时中断(由 restrictedMethods 决定)
407
+ */
408
+ public request = async (options: IRequestOptions, enableAbort = false): Promise<ResponseObject> => {
409
+ const {
410
+ url,
411
+ headers: _headers = {},
412
+ data,
413
+ responseType,
414
+ withCredentials,
415
+ body,
416
+ method: _method,
417
+ customReqOpts,
418
+ } = options
419
+ const headers = obj2StrRecord(_headers)
420
+ const method = String(_method).toLowerCase() || 'get'
421
+ const abortController = new AbortController()
422
+ const { signal } = abortController
423
+ const timeout = customReqOpts?.timeout || this.timeout
424
+
425
+ // 构建请求 payload:FormData > urlencoded > raw body > JSON
426
+ let payload
427
+ if (isFormData(data)) {
428
+ payload = data
429
+ } else if (headers['content-type'] === 'application/x-www-form-urlencoded') {
430
+ payload = toQueryString(data ?? {})
431
+ } else if (body) {
432
+ payload = body
433
+ } else {
434
+ payload = data ? JSON.stringify(data) : undefined
435
+ }
436
+
437
+ const realUrl = this.getRealUrl(
438
+ formatUrl('https', url ?? '', method === 'get' ? data : {}),
439
+ options.headers || {},
440
+ payload,
441
+ )
442
+
443
+ // 超时通过 AbortController + setTimeout 实现
444
+ let timer
445
+
446
+ if (enableAbort || timeout) {
447
+ timer = setTimeout(() => {
448
+ const timeoutMsg = `请求在${timeout / 1000}s内未完成,已中断`
449
+ console.warn(timeoutMsg)
450
+ abortController.abort(new Error(timeoutMsg))
451
+ }, timeout)
452
+ }
453
+
454
+ const requestOptions = await this.getReqOptions(realUrl, {
455
+ method,
456
+ headers,
457
+ body: payload,
458
+ credentials: withCredentials ? 'include' : 'same-origin',
459
+ signal,
460
+ })
461
+
462
+ try {
463
+ const response = await fetch(realUrl, requestOptions as RequestInit)
464
+ const result: ResponseObject = {
465
+ header: {},
466
+ statusCode: response.status,
467
+ }
468
+
469
+ try {
470
+ result.data = responseType === 'blob' ? await response.blob() : safeParseJson(await response.text())
471
+ } catch (e) {
472
+ // 上传 POST 请求可能返回 XML 等非 JSON 格式,容错处理
473
+ console.log('catch an error', e)
474
+ result.data = responseType === 'blob' ? await response.blob() : await response.text()
475
+ }
476
+
477
+ // 将响应头统一转为小写 key
478
+ const { headers } = response
479
+ headers.forEach((val, key) => (result.header[key.toLowerCase()] = val))
480
+
481
+ return result
482
+ } finally {
483
+ clearTimeout(timer)
484
+ }
485
+ }
486
+ }
@@ -0,0 +1,223 @@
1
+ import { ERROR } from './constants'
2
+ import { getCloudbaseContext, parseContext } from './context'
3
+ import { validateUid } from './utils'
4
+ import type {
5
+ ICreateTicketOpts,
6
+ IGetUserInfoResult,
7
+ IGetEndUserInfoResult,
8
+ IUserInfoQuery,
9
+ ITemplateNotifyReq,
10
+ } from './types'
11
+ import { ICloudbaseConfig } from '@cloudbase/types'
12
+
13
+ // 延迟加载,避免非 Node 环境打包时引入此包
14
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
+ let jwtSign: ((...args: any[]) => any) | null = null
16
+ async function getJwtSign() {
17
+ if (!jwtSign) {
18
+ try {
19
+ // @ts-ignore — 该包仅在 Node 运行时存在,开发环境可能未安装
20
+ const mod = await import('jsonwebtoken')
21
+ jwtSign = mod.default?.sign || mod.sign
22
+ } catch (e) {
23
+ throw new Error('缺少依赖 jsonwebtoken,请执行以下命令安装:\n\n'
24
+ + ' npm install jsonwebtoken\n\n'
25
+ + '该依赖用于 Node 环境下的自定义登录票据生成。')
26
+ }
27
+ }
28
+ return jwtSign!
29
+ }
30
+
31
+ /**
32
+ * 从云函数运行时上下文中获取当前请求的用户信息
33
+ * 数据来源为环境变量(微信 openId、TCB uuid 等)
34
+ */
35
+ function getDefaultUserInfo(): IGetUserInfoResult {
36
+ const { WX_OPENID, WX_APPID, TCB_UUID, TCB_CUSTOM_USER_ID, TCB_ISANONYMOUS_USER } = getCloudbaseContext()
37
+
38
+ return {
39
+ openId: WX_OPENID || '',
40
+ appId: WX_APPID || '',
41
+ uid: TCB_UUID || '',
42
+ customUserId: TCB_CUSTOM_USER_ID || '',
43
+ isAnonymous: TCB_ISANONYMOUS_USER === 'true',
44
+ }
45
+ }
46
+
47
+ /**
48
+ * 调用后端管理接口查询用户信息
49
+ * 统一封装 auth.getUserInfoForAdmin 的请求逻辑
50
+ */
51
+ function sendUserInfoRequest(app: any, params: Record<string, any>): Promise<any> {
52
+ return app?.request?.send?.('auth.getUserInfoForAdmin', params, {
53
+ pathname: 'web',
54
+ endPointMode: 'CLOUD_API',
55
+ })
56
+ }
57
+
58
+ /**
59
+ * 初始化 Node 端工具方法,挂载到 js-sdk app 实例上
60
+ * 包含:auth 相关方法、模板消息推送、context 解析
61
+ *
62
+ * @param app - js-sdk cloudbase 实例
63
+ * @param config - 配置信息,包含认证凭证和环境 ID
64
+ */
65
+ export const nodeTool = (app: any, config: ICloudbaseConfig) => {
66
+ // 仅当 app 已初始化 auth 模块时,才注入 auth 相关方法
67
+ if (app.auth) {
68
+ const auth = {
69
+ /** 获取当前请求的用户信息(从环境变量读取,同步) */
70
+ getUserInfo(): IGetUserInfoResult {
71
+ return getDefaultUserInfo()
72
+ },
73
+
74
+ /**
75
+ * 获取终端用户信息
76
+ * 不传 uid 时返回当前请求用户信息,传 uid 时查询指定用户
77
+ */
78
+ async getEndUserInfo(uid?: string): Promise<IGetEndUserInfoResult> {
79
+ const defaultUserInfo = getDefaultUserInfo()
80
+
81
+ if (uid === undefined) {
82
+ return { userInfo: defaultUserInfo }
83
+ }
84
+ validateUid(uid)
85
+
86
+ return sendUserInfoRequest(app, {
87
+ uuid: uid,
88
+ envName: config.env,
89
+ }).then((result: any) => {
90
+ if (result.code) {
91
+ return result
92
+ }
93
+ return {
94
+ userInfo: { ...defaultUserInfo, ...result.data },
95
+ requestId: result.requestId,
96
+ }
97
+ })
98
+ },
99
+
100
+ /**
101
+ * 创建自定义登录 Ticket
102
+ * 使用 RSA 私钥签发 JWT,客户端凭此 Ticket 换取登录态
103
+ *
104
+ * @param uid - 自定义用户 ID(4~32 位)
105
+ * @param options - 刷新间隔和过期时间配置
106
+ * @returns 格式为 "{private_key_id}/@@/{jwt_token}" 的 Ticket 字符串
107
+ */
108
+ async createTicket(uid: string, options: ICreateTicketOpts = {}): Promise<string> {
109
+ validateUid(uid)
110
+
111
+ const timestamp = new Date().getTime()
112
+ const { credentials } = config.auth || {}
113
+ const { env } = config
114
+
115
+ if (!env) {
116
+ throw { ...ERROR.INVALID_PARAM, message: 'no env in config' }
117
+ }
118
+
119
+ if (!credentials?.env_id) {
120
+ throw {
121
+ ...ERROR.INVALID_PARAM,
122
+ message: '当前私钥未包含env_id 信息, 请前往腾讯云云开发控制台,获取自定义登录最新私钥',
123
+ }
124
+ }
125
+
126
+ if (credentials.env_id !== env) {
127
+ throw {
128
+ ...ERROR.INVALID_PARAM,
129
+ message: '当前私钥所属环境与 init 指定环境不一致!',
130
+ }
131
+ }
132
+
133
+ const {
134
+ refresh = 3600 * 1000, // 默认 1 小时刷新
135
+ expire = timestamp + 7 * 24 * 60 * 60 * 1000, // 默认 7 天过期
136
+ } = options
137
+ const sign = await getJwtSign()
138
+ const token = sign(
139
+ {
140
+ alg: 'RS256',
141
+ env,
142
+ iat: timestamp,
143
+ exp: timestamp + 10 * 60 * 1000, // Ticket 本身 10 分钟有效
144
+ uid,
145
+ refresh,
146
+ expire,
147
+ },
148
+ credentials.private_key,
149
+ {
150
+ allowInsecureKeySizes: true,
151
+ algorithm: 'RS256',
152
+ },
153
+ )
154
+
155
+ return `${credentials.private_key_id}/@@/${token}`
156
+ },
157
+
158
+ /**
159
+ * 按条件查询用户信息(管理端接口)
160
+ * 支持按 uid、platform、platformId 查询
161
+ */
162
+ async queryUserInfo(query: IUserInfoQuery): Promise<any> {
163
+ const { uid, platform, platformId } = query
164
+ return sendUserInfoRequest(app, {
165
+ uuid: uid,
166
+ platform,
167
+ platformId,
168
+ envName: config.env,
169
+ }).then((result: any) => {
170
+ if (result.code) {
171
+ return result
172
+ }
173
+ return {
174
+ userInfo: { ...result.data },
175
+ requestId: result.requestId,
176
+ }
177
+ })
178
+ },
179
+
180
+ /** 获取客户端 IP 地址 */
181
+ getClientIP(): string {
182
+ const { TCB_SOURCE_IP } = getCloudbaseContext()
183
+ return TCB_SOURCE_IP || ''
184
+ },
185
+ }
186
+
187
+ // 将 auth 方法逐一挂载到 app.auth 上
188
+ Object.keys(auth).forEach((key) => {
189
+ app.auth[key] = (auth as Record<string, any>)[key]
190
+ })
191
+ }
192
+
193
+ /**
194
+ * 发送模板消息通知
195
+ * 通过调用 lowcode-datasource 云函数间接调用微搭 API
196
+ *
197
+ * @param params - 通知参数(策略 ID、模板变量、跳转链接)
198
+ * @param opts - 可选配置,如超时时间
199
+ */
200
+ app.sendTemplateNotification = async (params: ITemplateNotifyReq, opts?: { timeout?: number }) => await app?.callFunction?.(
201
+ {
202
+ name: 'lowcode-datasource',
203
+ data: {
204
+ methodName: 'callWedaApi',
205
+ params: {
206
+ action: 'PushNotifyMsg',
207
+ data: {
208
+ NotifyId: params.notifyId,
209
+ Data: JSON.stringify(params.data),
210
+ NotifyUsers: undefined,
211
+ Url: params.url,
212
+ },
213
+ },
214
+ mode: 'c',
215
+ },
216
+ },
217
+ undefined,
218
+ opts,
219
+ )
220
+
221
+ /** 挂载 context 解析工具,方便用户在云函数中使用 */
222
+ app.parseContext = parseContext
223
+ }