cabloy 5.1.240 → 5.1.242

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 (55) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +24 -0
  3. package/package.json +1 -1
  4. package/repo-docs/backend/i18n-guide.md +109 -11
  5. package/repo-docs/backend/openapi-guide.md +18 -0
  6. package/repo-docs/frontend/environment-config-guide.md +43 -0
  7. package/repo-docs/frontend/module-scope.md +88 -0
  8. package/repo-docs/frontend/system-startup-guide.md +31 -0
  9. package/repo-e2e/specs/home-user-account.spec.ts +53 -8
  10. package/vona/packages-vona/vona/package.json +1 -1
  11. package/vona/pnpm-lock.yaml +17 -142
  12. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/dto/operatorContext.ts +4 -1
  13. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/service/operator.ts +1 -0
  14. package/vona/src/suite/a-commerce/modules/commerce-siteadmin/test/operatorAccess.test.ts +1 -0
  15. package/vona/src/suite/a-home/modules/home-user/src/bean/meta.version.ts +1 -0
  16. package/vona/src/suite/a-home/modules/home-user/src/controller/passport.ts +1 -0
  17. package/vona/src/suite/a-home/modules/home-user/src/dto/accountCurrent.ts +3 -0
  18. package/vona/src/suite/a-home/modules/home-user/src/dto/accountProfileUpdate.ts +9 -2
  19. package/vona/src/suite/a-home/modules/home-user/src/dto/passportUser.ts +3 -0
  20. package/vona/src/suite/a-home/modules/home-user/src/entity/user.ts +3 -0
  21. package/vona/src/suite/a-home/modules/home-user/src/service/account.ts +7 -3
  22. package/vona/src/suite/a-home/modules/home-user/src/service/userAdapter.ts +1 -0
  23. package/vona/src/suite/a-home/modules/home-user/test/account.test.ts +133 -8
  24. package/vona/src/suite/a-home/modules/home-user/test/accountOpenapi.test.ts +32 -5
  25. package/vona/src/suite/a-home/modules/home-user/test/passport.test.ts +159 -1
  26. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  27. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  28. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/package.json +1 -1
  29. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/bean/bean.captcha.ts +271 -137
  30. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/bean/cacheRedis.captcha.ts +199 -2
  31. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/lib/captchaLua.ts +90 -0
  32. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/types/captcha.ts +15 -1
  33. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/types/captchaProvider.ts +38 -1
  34. package/vona/src/suite-vendor/a-captcha/package.json +1 -1
  35. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  36. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +54 -8
  37. package/vona/src/suite-vendor/a-vona/modules/a-permission/test/permission.test.ts +12 -15
  38. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/package.json +1 -1
  39. package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
  40. package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/user.ts +1 -0
  41. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  42. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/api/openapi/schemas.ts +68 -52
  43. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/api/openapi/types.ts +530 -344
  44. package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/page/dashboard/controller.tsx +7 -1
  45. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +6 -1
  46. package/zova/src/suite/a-home/modules/home-base/src/config/locale/en-us.ts +1 -0
  47. package/zova/src/suite/a-home/modules/home-base/src/config/locale/zh-cn.ts +1 -0
  48. package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.user.tsx +1 -1
  49. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.user.tsx +3 -1
  50. package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +4 -0
  51. package/zova/src/suite/a-home/modules/home-user/src/config/locale/en-us.ts +1 -0
  52. package/zova/src/suite/a-home/modules/home-user/src/config/locale/zh-cn.ts +1 -0
  53. package/zova/src/suite/a-home/modules/home-user/src/model/account.ts +2 -1
  54. package/zova/src/suite/a-home/modules/home-user/src/page/account/controller.tsx +17 -3
  55. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/test/aggregate.test.ts +0 -247
@@ -1,169 +1,308 @@
1
1
  import type { VonaContext } from 'vona';
2
2
 
3
3
  import { getRandomInt } from '@cabloy/utils';
4
- import { BeanBase, beanFullNameFromOnionName, deepExtend, uuidv4 } from 'vona';
4
+ import { BeanBase, beanFullNameFromOnionName, uuidv4 } from 'vona';
5
5
  import { Bean } from 'vona-module-a-bean';
6
6
 
7
7
  import type { ICaptchaData, ICaptchaDataCache } from '../types/captcha.ts';
8
- import type { ICaptchaProviderExecute, ICaptchaProviderRecord } from '../types/captchaProvider.ts';
8
+ import type {
9
+ ICaptchaAuthorization,
10
+ ICaptchaProviderData,
11
+ ICaptchaProviderExecute,
12
+ ICaptchaProviderRecord,
13
+ } from '../types/captchaProvider.ts';
9
14
  import type {
10
15
  ICaptchaSceneOptionsProviders,
11
16
  ICaptchaSceneOptionsResolverResult,
12
17
  ICaptchaSceneRecord,
13
18
  } from '../types/captchaScene.ts';
14
-
15
- import { SymbolCacheSceneProviders } from '../lib/const.ts';
19
+ import type { ICaptchaSnapshot } from './cacheRedis.captcha.ts';
16
20
 
17
21
  @Bean()
18
22
  export class BeanCaptcha extends BeanBase {
19
- async create(sceneName: keyof ICaptchaSceneRecord): Promise<ICaptchaData> {
20
- // resolve provider
23
+ async create(sceneName: keyof ICaptchaSceneRecord, context?: unknown): Promise<ICaptchaData> {
21
24
  const provider = await this._resolveProvider(sceneName);
22
25
  if (!provider) throw new Error(`not found captcha provider for scene: ${sceneName}`);
23
- // create
24
26
  const beanInstance = this._getProviderInstance(provider.name);
27
+ if (beanInstance.bound === true)
28
+ return await this._createBound(sceneName, provider, beanInstance, context);
25
29
  const captcha = await beanInstance.create(provider.options);
26
- // data
27
30
  const id = uuidv4();
28
- const captchaData: ICaptchaDataCache = {
31
+ const data: ICaptchaDataCache = {
29
32
  scene: sceneName,
30
33
  provider: provider.name,
31
34
  token: captcha.token,
35
+ revision: uuidv4(),
32
36
  };
33
- // cache
34
- await this.scope.cacheRedis.captcha.set(captchaData, id, {
35
- ttl: provider.options.ttl ?? this.scope.config.captchaProvider.ttl,
36
- });
37
- this.$loggerChild('captcha').debug(
38
- () =>
39
- `captcha.create: id:${id}, sceneName:${sceneName}, provider:${provider.name}, token:${captcha.token}`,
37
+ const created = await this.scope.cacheRedis.captcha.createImage(
38
+ id,
39
+ data,
40
+ provider.options.ttl ?? this.scope.config.captchaProvider.ttl,
40
41
  );
41
- // result
42
+ if (!created) throw new Error('captcha identity collision');
42
43
  const result: ICaptchaData = { id, provider: provider.name, payload: captcha.payload };
43
- if (this.scope.config.captcha.showToken) {
44
- result.token = captcha.token;
44
+ if (this.scope.config.captcha.showToken) result.token = captcha.token;
45
+ return result;
46
+ }
47
+
48
+ private async _createBound(
49
+ sceneName: keyof ICaptchaSceneRecord,
50
+ provider: ICaptchaSceneOptionsResolverResult,
51
+ beanInstance: ICaptchaProviderExecute,
52
+ context: unknown,
53
+ ): Promise<ICaptchaData> {
54
+ const authorization = await this._authorize(beanInstance, context, 'issue', sceneName);
55
+ if (!authorization) return this.app.throw(403);
56
+ const cache = this.scope.cacheRedis.captcha;
57
+ const id = `${cache.boundPrefix()}${uuidv4()}`;
58
+ const pending: ICaptchaDataCache = {
59
+ scene: sceneName,
60
+ provider: provider.name,
61
+ bound: {
62
+ binding: authorization.binding,
63
+ scope: authorization.scope,
64
+ state: 'pending',
65
+ expiresAt: 0,
66
+ resendAt: 0,
67
+ attemptsRemaining: authorization.maxAttempts,
68
+ version: 0,
69
+ },
70
+ };
71
+ const reservation = await cache.reserveBound(id, pending, authorization);
72
+ if (!reservation.reserved) {
73
+ return {
74
+ id: '',
75
+ provider: provider.name,
76
+ payload: undefined,
77
+ status: 'limited',
78
+ resendAt: reservation.resendAt || undefined,
79
+ retryAt: reservation.retryAt,
80
+ };
81
+ }
82
+ const snapshot = reservation.snapshot;
83
+ const timing = {
84
+ expiresAt: snapshot.data.bound!.expiresAt,
85
+ resendAt: snapshot.data.bound!.resendAt,
86
+ };
87
+ let captcha: ICaptchaProviderData;
88
+ try {
89
+ // A reservation authorizes exactly one external attempt. Never retry sends.
90
+ captcha = await beanInstance.create(provider.options, context);
91
+ } catch {
92
+ await cache.finalizeBound(id, snapshot);
93
+ return { id: '', provider: provider.name, payload: undefined, status: 'unknown', ...timing };
45
94
  }
95
+ const outcome = captcha.outcome ?? 'accepted';
96
+ if (outcome !== 'accepted' || captcha.token === undefined || captcha.token === null) {
97
+ await cache.finalizeBound(id, snapshot);
98
+ return {
99
+ id: '',
100
+ provider: provider.name,
101
+ payload: undefined,
102
+ status: outcome === 'rejected' ? 'rejected' : 'unknown',
103
+ ...timing,
104
+ };
105
+ }
106
+ const active: ICaptchaDataCache = {
107
+ ...snapshot.data,
108
+ token: captcha.token,
109
+ bound: { ...snapshot.data.bound!, state: 'active', version: 1 },
110
+ };
111
+ const finalized = await cache.finalizeBound(id, snapshot, active);
112
+ if (!finalized)
113
+ return { id: '', provider: provider.name, payload: undefined, status: 'unknown', ...timing };
114
+ const result: ICaptchaData = {
115
+ id,
116
+ provider: provider.name,
117
+ payload: captcha.payload,
118
+ status: 'accepted',
119
+ ...timing,
120
+ };
121
+ // Generic showToken never grants disclosure for a bound provider.
122
+ if (captcha.exposeToken === true) result.token = captcha.token;
46
123
  return result;
47
124
  }
48
125
 
49
126
  async refresh(id: string, sceneName: keyof ICaptchaSceneRecord) {
50
- let captchaData = await this.getCaptchaData(id);
51
- if (!captchaData) {
52
- // create
53
- return await this.create(sceneName);
54
- }
55
- // scene
56
- if (captchaData.scene !== sceneName) this.app.throw(403);
57
- // create
58
- const beanInstance = this._getProviderInstance(captchaData.provider);
59
- const providerOptions = this._getProviderOptions(captchaData.scene, captchaData.provider)!;
60
- const captcha = await beanInstance.create(providerOptions);
61
- // data
62
- captchaData = { scene: sceneName, provider: captchaData.provider, token: captcha.token };
63
- // cache
64
- await this.scope.cacheRedis.captcha.set(captchaData, id, {
65
- ttl: providerOptions.ttl ?? this.scope.config.captchaProvider.ttl,
66
- });
67
- this.$loggerChild('captcha').debug(
68
- () =>
69
- `captcha.refresh: id:${id}, sceneName:${sceneName}, provider:${captchaData.provider}, token:${captcha.token}`,
70
- );
71
- // result
72
- const result: ICaptchaData = { id, provider: captchaData.provider, payload: captcha.payload };
73
- if (this.scope.config.captcha.showToken) {
74
- result.token = captcha.token;
127
+ const cache = this.scope.cacheRedis.captcha;
128
+ const snapshot = await cache.snapshot(id);
129
+ if (!snapshot) return await this.create(sceneName);
130
+ const data = snapshot.data;
131
+ if (data.scene !== sceneName) return this.app.throw(403);
132
+ const beanInstance = this._getProviderInstance(data.provider);
133
+ if (data.bound || beanInstance.bound === true) return this.app.throw(403);
134
+ const options = this._getProviderOptions(data.scene, data.provider);
135
+ if (!options) return this.app.throw(403);
136
+ const captcha = await beanInstance.create(options);
137
+ const next = {
138
+ scene: sceneName,
139
+ provider: data.provider,
140
+ token: captcha.token,
141
+ revision: uuidv4(),
142
+ };
143
+ if (
144
+ !(await cache.compareAndSet(
145
+ id,
146
+ snapshot,
147
+ next,
148
+ options.ttl ?? this.scope.config.captchaProvider.ttl,
149
+ ))
150
+ ) {
151
+ return this.app.throw(403);
75
152
  }
153
+ const result: ICaptchaData = { id, provider: data.provider, payload: captcha.payload };
154
+ if (this.scope.config.captcha.showToken) result.token = captcha.token;
76
155
  return result;
77
156
  }
78
157
 
79
- async verify(id: string, token: unknown, sceneName: keyof ICaptchaSceneRecord): Promise<boolean> {
80
- const captchaData = await this.getCaptchaData(id);
81
- const verified = await this._verifyInner(captchaData, id, token, sceneName);
82
- this.$loggerChild('captcha').debug(
83
- () =>
84
- `captcha.verify: id:${id}, sceneName:${sceneName}, provider:${captchaData?.provider}, token:${token}, tokenWanted:${captchaData?.token}`,
158
+ async verify(
159
+ id: string,
160
+ token: unknown,
161
+ sceneName: keyof ICaptchaSceneRecord,
162
+ context?: unknown,
163
+ ): Promise<boolean> {
164
+ const cache = this.scope.cacheRedis.captcha;
165
+ const snapshot = await cache.snapshot(id);
166
+ if (!snapshot || snapshot.data.scene !== sceneName) return false;
167
+ const data = snapshot.data;
168
+ const beanInstance = this._getProviderInstance(data.provider);
169
+ if (data.bound || beanInstance.bound === true) {
170
+ if (!data.bound || beanInstance.bound !== true) return false;
171
+ const authorization = await this._authorize(beanInstance, context, 'consume', sceneName);
172
+ if (!authorization) return false;
173
+ return await this._verifyBound(id, token, sceneName, beanInstance, authorization, snapshot);
174
+ }
175
+ if (data.token2) {
176
+ const consumed = await cache.compareAndSet(id, snapshot);
177
+ return consumed && data.token2 === token;
178
+ }
179
+ const options = this._getProviderOptions(data.scene, data.provider);
180
+ if (!options || !data.token) return false;
181
+ const verified = await beanInstance.verify(data.token, token, options);
182
+ const changed = await cache.compareAndSet(
183
+ id,
184
+ snapshot,
185
+ verified ? undefined : { ...data, token: undefined, revision: uuidv4() },
85
186
  );
86
- return verified;
187
+ return verified && changed;
87
188
  }
88
189
 
89
- private async _verifyInner(
90
- captchaData: ICaptchaDataCache | undefined,
190
+ private async _verifyBound(
91
191
  id: string,
92
192
  token: unknown,
93
193
  sceneName: keyof ICaptchaSceneRecord,
194
+ beanInstance: ICaptchaProviderExecute,
195
+ authorization: ICaptchaAuthorization,
196
+ original: ICaptchaSnapshot,
94
197
  ): Promise<boolean> {
95
- if (!captchaData) return false;
96
- // scene
97
- if (captchaData.scene !== sceneName) return false;
98
- // tokenSecondary
99
- const tokenSecondary = captchaData.token2;
100
- if (tokenSecondary) {
101
- // delete cache
102
- await this.scope.cacheRedis.captcha.del(id);
103
- return tokenSecondary === token;
198
+ const cache = this.scope.cacheRedis.captcha;
199
+ let snapshot: ICaptchaSnapshot | undefined = original;
200
+ // At most five competing wrong writes exist for this generation. A stale
201
+ // wrong verdict is retried against the new snapshot, never a new generation.
202
+ for (let retry = 0; retry < 5 && snapshot; retry++) {
203
+ const data = snapshot.data;
204
+ const bound = data.bound;
205
+ if (
206
+ !bound ||
207
+ bound.state !== 'active' ||
208
+ bound.attemptsRemaining <= 0 ||
209
+ data.scene !== sceneName ||
210
+ data.provider !== original.data.provider ||
211
+ bound.scope !== authorization.scope ||
212
+ !this._bindingMatches(bound.binding, authorization.binding)
213
+ ) {
214
+ return false;
215
+ }
216
+ const options = this._getProviderOptions(sceneName, data.provider);
217
+ if (!options || data.token === undefined) return false;
218
+ const verified = await beanInstance.verify(data.token, token, options);
219
+ const committed = await cache.consumeBound(id, snapshot, verified);
220
+ if (committed !== -1 || verified) return verified && committed === 1;
221
+ snapshot = await cache.snapshot(id);
104
222
  }
105
- // provider
106
- const beanInstance = this._getProviderInstance(captchaData.provider);
107
- const providerOptions = this._getProviderOptions(captchaData.scene, captchaData.provider)!;
108
- // verify
109
- if (!captchaData.token) return false;
110
- const verified = await beanInstance.verify(captchaData.token, token, providerOptions);
111
- if (!verified) {
112
- // update token. not delete cache for refresh
113
- captchaData = { ...captchaData, token: undefined };
114
- await this.scope.cacheRedis.captcha.set(captchaData, id, {
115
- ttl: providerOptions.ttl ?? this.scope.config.captchaProvider.ttl,
116
- });
117
- return false;
118
- }
119
- // delete cache
120
- await this.scope.cacheRedis.captcha.del(id);
121
- // ok
122
- return true;
223
+ return false;
123
224
  }
124
225
 
125
226
  async verifyImmediate(id: string, token: unknown): Promise<false | string> {
126
- let captchaData = await this.getCaptchaData(id);
127
- if (!captchaData) return false;
128
- if (!captchaData.token) return false;
129
- // provider
130
- const beanInstance = this._getProviderInstance(captchaData.provider);
131
- const providerOptions = this._getProviderOptions(captchaData.scene, captchaData.provider)!;
132
- // verify
133
- const verified = await beanInstance.verify(captchaData.token, token, providerOptions);
227
+ const cache = this.scope.cacheRedis.captcha;
228
+ const snapshot = await cache.snapshot(id);
229
+ if (!snapshot) return false;
230
+ const data = snapshot.data;
231
+ const beanInstance = this._getProviderInstance(data.provider);
232
+ if (data.bound || beanInstance.bound === true || !data.token) return false;
233
+ const options = this._getProviderOptions(data.scene, data.provider);
234
+ if (!options) return false;
235
+ const verified = await beanInstance.verify(data.token, token, options);
134
236
  if (!verified) {
135
- // update token. not delete cache for refresh
136
- captchaData = { ...captchaData, token: undefined };
137
- await this.scope.cacheRedis.captcha.set(captchaData, id, {
138
- ttl: providerOptions.ttl ?? this.scope.config.captchaProvider.ttl,
139
- });
237
+ await cache.compareAndSet(id, snapshot, { ...data, token: undefined, revision: uuidv4() });
140
238
  return false;
141
239
  }
142
- // tokenSecondary
143
240
  const tokenSecondary = uuidv4();
144
- captchaData = { ...captchaData, token: undefined, token2: tokenSecondary };
145
- // update cache
146
- await this.scope.cacheRedis.captcha.set(captchaData, id, {
147
- ttl: providerOptions.ttlSecondary ?? this.scope.config.captchaProvider.ttlSecondary,
148
- });
149
- // ok
150
- return tokenSecondary;
241
+ const next = { ...data, token: undefined, token2: tokenSecondary, revision: uuidv4() };
242
+ const changed = await cache.compareAndSet(
243
+ id,
244
+ snapshot,
245
+ next,
246
+ options.ttlSecondary ?? this.scope.config.captchaProvider.ttlSecondary,
247
+ );
248
+ return changed ? tokenSecondary : false;
151
249
  }
152
250
 
153
251
  async getCaptchaData(id: string) {
154
- return await this.scope.cacheRedis.captcha.get(id);
252
+ return await this.scope.cacheRedis.captcha.get(id, { updateAgeOnGet: false });
155
253
  }
156
254
 
157
255
  async updateCaptchaToken(id: string, token: unknown) {
158
- let captchaData = await this.getCaptchaData(id);
159
- if (!captchaData) return this.app.throw(403);
160
- // provider
161
- const providerOptions = this._getProviderOptions(captchaData.scene, captchaData.provider)!;
162
- // update cache
163
- captchaData = { ...captchaData, token };
164
- await this.scope.cacheRedis.captcha.set(captchaData, id, {
165
- ttl: providerOptions.ttl ?? this.scope.config.captchaProvider.ttl,
166
- });
256
+ const cache = this.scope.cacheRedis.captcha;
257
+ const snapshot = await cache.snapshot(id);
258
+ if (!snapshot) return this.app.throw(403);
259
+ const data = snapshot.data;
260
+ const beanInstance = this._getProviderInstance(data.provider);
261
+ if (data.bound || beanInstance.bound === true) return this.app.throw(403);
262
+ const options = this._getProviderOptions(data.scene, data.provider);
263
+ if (!options) return this.app.throw(403);
264
+ if (
265
+ !(await cache.compareAndSet(
266
+ id,
267
+ snapshot,
268
+ { ...data, token, revision: uuidv4() },
269
+ options.ttl ?? this.scope.config.captchaProvider.ttl,
270
+ ))
271
+ ) {
272
+ return this.app.throw(403);
273
+ }
274
+ }
275
+
276
+ private async _authorize(
277
+ provider: ICaptchaProviderExecute,
278
+ context: unknown,
279
+ operation: 'issue' | 'consume',
280
+ sceneName: keyof ICaptchaSceneRecord,
281
+ ): Promise<ICaptchaAuthorization | undefined> {
282
+ const authorization = await provider.authorize?.(context, operation, sceneName);
283
+ if (!authorization) return;
284
+ this.scope.cacheRedis.captcha.validateAuthorization(authorization);
285
+ // Snapshot only the server-authorized contract; later provider work cannot
286
+ // mutate the binding/policy already used for this issuance or consumption.
287
+ return {
288
+ ...authorization,
289
+ binding: {
290
+ recipient: authorization.binding.recipient,
291
+ purpose: authorization.binding.purpose,
292
+ ...(authorization.binding.subject === undefined
293
+ ? {}
294
+ : { subject: authorization.binding.subject }),
295
+ },
296
+ recipientLimits: authorization.recipientLimits.map(limit => ({ ...limit })),
297
+ ipLimits: authorization.ipLimits.map(limit => ({ ...limit })),
298
+ };
299
+ }
300
+
301
+ private _bindingMatches(
302
+ a: ICaptchaAuthorization['binding'],
303
+ b: ICaptchaAuthorization['binding'],
304
+ ) {
305
+ return a.recipient === b.recipient && a.purpose === b.purpose && a.subject === b.subject;
167
306
  }
168
307
 
169
308
  private _getProviderInstance(providerName: keyof ICaptchaProviderRecord) {
@@ -175,51 +314,46 @@ export class BeanCaptcha extends BeanBase {
175
314
  sceneName: keyof ICaptchaSceneRecord,
176
315
  providerName: keyof ICaptchaProviderRecord,
177
316
  ) {
178
- // providers
179
- const providers = this._getProviders(sceneName);
180
- return providers[providerName];
317
+ return this._getProviders(sceneName)[providerName];
181
318
  }
182
319
 
183
320
  private async _resolveProvider(
184
321
  sceneName: keyof ICaptchaSceneRecord,
185
322
  ): Promise<ICaptchaSceneOptionsResolverResult | undefined> {
186
- // providers
187
323
  const providers = this._getProviders(sceneName);
188
324
  if (Object.keys(providers).length === 0) return;
189
- // resolver
190
- const onionSlice = this.bean.onion.captchaScene.getOnionSlice(sceneName);
191
- const onionOptions = onionSlice.beanOptions.options;
325
+ const onionOptions = this.bean.onion.captchaScene.getOnionOptionsDynamic(sceneName);
192
326
  const resolver = onionOptions?.resolver ?? resolverDefault;
193
327
  const providerName = await resolver(this.ctx, providers);
194
- if (!providerName) return;
328
+ if (!providerName || !providers[providerName]) return;
195
329
  return { name: providerName, options: providers[providerName] };
196
330
  }
197
331
 
198
332
  private _getProviders(sceneName: keyof ICaptchaSceneRecord) {
199
- if (!this.app.meta[SymbolCacheSceneProviders]) this.app.meta[SymbolCacheSceneProviders] = {};
200
- if (!this.app.meta[SymbolCacheSceneProviders][sceneName]) {
201
- const onionSlice = this.bean.onion.captchaScene.getOnionSlice(sceneName);
202
- if (!onionSlice) throw new Error(`not found captcha scene: ${sceneName}`);
203
- const onionOptions = onionSlice.beanOptions.options;
204
- this.app.meta[SymbolCacheSceneProviders][sceneName] = this._prepareProviders(
205
- onionOptions?.providers,
206
- );
207
- }
208
- return this.app.meta[SymbolCacheSceneProviders][sceneName];
333
+ const scene = this.bean.onion.captchaScene.getOnionSlice(sceneName);
334
+ if (!scene) throw new Error(`not found captcha scene: ${sceneName}`);
335
+ const options = this.bean.onion.captchaScene.getOnionOptionsDynamic(sceneName);
336
+ return this._prepareProviders(options?.providers);
209
337
  }
210
338
 
211
339
  private _prepareProviders(providers?: ICaptchaSceneOptionsProviders): ICaptchaProviderRecord {
212
- if (!providers) return {} as any;
213
- const providersNew: ICaptchaProviderRecord = {} as any;
214
- for (const _key in providers) {
215
- const key: keyof ICaptchaProviderRecord = _key as any;
340
+ const result: ICaptchaProviderRecord = {} as any;
341
+ if (!providers) return result;
342
+ for (const _key of Object.keys(providers)) {
343
+ const key = _key as keyof ICaptchaProviderRecord;
216
344
  const providerOptions = providers[key];
217
345
  if (providerOptions === false) continue;
218
- const onionSlice = this.bean.onion.captchaProvider.getOnionSliceEnabled(true, key);
219
- if (!onionSlice) continue;
220
- providersNew[key] = deepExtend({}, onionSlice.beanOptions.options, providerOptions);
346
+ const onion = this.bean.onion.captchaProvider;
347
+ const slice = onion.getOnionSlice(key);
348
+ if (!slice) continue;
349
+ const options = onion.getOnionOptionsDynamic(
350
+ key,
351
+ typeof providerOptions === 'object' ? providerOptions : undefined,
352
+ );
353
+ if (!this.bean.onion.checkOnionOptionsEnabled(options)) continue;
354
+ Object.assign(result, { [key]: options });
221
355
  }
222
- return providersNew;
356
+ return result;
223
357
  }
224
358
  }
225
359