@metamask-previews/kyc-controller 0.0.0-preview-11d78534e

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 (78) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +6 -0
  3. package/LICENSE.APACHE2 +201 -0
  4. package/LICENSE.MIT +21 -0
  5. package/README.md +648 -0
  6. package/dist/KycController-method-action-types.cjs +7 -0
  7. package/dist/KycController-method-action-types.cjs.map +1 -0
  8. package/dist/KycController-method-action-types.d.cts +143 -0
  9. package/dist/KycController-method-action-types.d.cts.map +1 -0
  10. package/dist/KycController-method-action-types.d.mts +143 -0
  11. package/dist/KycController-method-action-types.d.mts.map +1 -0
  12. package/dist/KycController-method-action-types.mjs +6 -0
  13. package/dist/KycController-method-action-types.mjs.map +1 -0
  14. package/dist/KycController.cjs +800 -0
  15. package/dist/KycController.cjs.map +1 -0
  16. package/dist/KycController.d.cts +219 -0
  17. package/dist/KycController.d.cts.map +1 -0
  18. package/dist/KycController.d.mts +219 -0
  19. package/dist/KycController.d.mts.map +1 -0
  20. package/dist/KycController.mjs +795 -0
  21. package/dist/KycController.mjs.map +1 -0
  22. package/dist/KycService-method-action-types.cjs +7 -0
  23. package/dist/KycService-method-action-types.cjs.map +1 -0
  24. package/dist/KycService-method-action-types.d.cts +74 -0
  25. package/dist/KycService-method-action-types.d.cts.map +1 -0
  26. package/dist/KycService-method-action-types.d.mts +74 -0
  27. package/dist/KycService-method-action-types.d.mts.map +1 -0
  28. package/dist/KycService-method-action-types.mjs +6 -0
  29. package/dist/KycService-method-action-types.mjs.map +1 -0
  30. package/dist/KycService.cjs +238 -0
  31. package/dist/KycService.cjs.map +1 -0
  32. package/dist/KycService.d.cts +169 -0
  33. package/dist/KycService.d.cts.map +1 -0
  34. package/dist/KycService.d.mts +169 -0
  35. package/dist/KycService.d.mts.map +1 -0
  36. package/dist/KycService.mjs +234 -0
  37. package/dist/KycService.mjs.map +1 -0
  38. package/dist/countryCodes.cjs +274 -0
  39. package/dist/countryCodes.cjs.map +1 -0
  40. package/dist/countryCodes.d.cts +18 -0
  41. package/dist/countryCodes.d.cts.map +1 -0
  42. package/dist/countryCodes.d.mts +18 -0
  43. package/dist/countryCodes.d.mts.map +1 -0
  44. package/dist/countryCodes.mjs +270 -0
  45. package/dist/countryCodes.mjs.map +1 -0
  46. package/dist/crypto.cjs +165 -0
  47. package/dist/crypto.cjs.map +1 -0
  48. package/dist/crypto.d.cts +83 -0
  49. package/dist/crypto.d.cts.map +1 -0
  50. package/dist/crypto.d.mts +83 -0
  51. package/dist/crypto.d.mts.map +1 -0
  52. package/dist/crypto.mjs +160 -0
  53. package/dist/crypto.mjs.map +1 -0
  54. package/dist/index.cjs +21 -0
  55. package/dist/index.cjs.map +1 -0
  56. package/dist/index.d.cts +12 -0
  57. package/dist/index.d.cts.map +1 -0
  58. package/dist/index.d.mts +12 -0
  59. package/dist/index.d.mts.map +1 -0
  60. package/dist/index.mjs +6 -0
  61. package/dist/index.mjs.map +1 -0
  62. package/dist/selectors.cjs +30 -0
  63. package/dist/selectors.cjs.map +1 -0
  64. package/dist/selectors.d.cts +24 -0
  65. package/dist/selectors.d.cts.map +1 -0
  66. package/dist/selectors.d.mts +24 -0
  67. package/dist/selectors.d.mts.map +1 -0
  68. package/dist/selectors.mjs +24 -0
  69. package/dist/selectors.mjs.map +1 -0
  70. package/dist/types.cjs +10 -0
  71. package/dist/types.cjs.map +1 -0
  72. package/dist/types.d.cts +93 -0
  73. package/dist/types.d.cts.map +1 -0
  74. package/dist/types.d.mts +93 -0
  75. package/dist/types.d.mts.map +1 -0
  76. package/dist/types.mjs +9 -0
  77. package/dist/types.mjs.map +1 -0
  78. package/package.json +89 -0
@@ -0,0 +1,795 @@
1
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
2
+ if (kind === "m") throw new TypeError("Private method is not writable");
3
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
4
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
5
+ return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
6
+ };
7
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
8
+ if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
9
+ if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
10
+ return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
+ };
12
+ var _KycController_instances, _KycController_sumsubLauncher, _KycController_keypair, _KycController_authClientToken, _KycController_generation, _KycController_continuationInFlight, _KycController_createSession, _KycController_clearAcceptedTerms, _KycController_handleCheckOutcome, _KycController_handleAuthOutcome, _KycController_continueAfterAuthentication, _KycController_requireTermsReacceptance, _KycController_updateIfCurrent, _KycController_applyUpdate, _KycController_fail;
13
+ import { BaseController } from "@metamask/base-controller";
14
+ import { decryptCredentials, generateKeyPair } from "./crypto.mjs";
15
+ // === GENERAL ===
16
+ export const controllerName = 'KycController';
17
+ const FRAMES_BASE_URL = 'https://blocks.moonpay.com/platform/v1';
18
+ const CHANNEL_CHECK = 'ch_1';
19
+ const CHANNEL_AUTH = 'ch_2';
20
+ const CHANNEL_RESET = 'ch_reset';
21
+ // Placeholder credentials for the SumSub sub-flow. These are demo values that
22
+ // must be replaced with real UKYC-issued material before production use.
23
+ const MOCK_JWT_TOKEN = 'mock-jwt-token';
24
+ // The SumSub SDK status that signals the applicant finished the flow
25
+ // successfully. Any other resolution (abandonment, failure, or a non-success
26
+ // outcome) must not be recorded as `complete`.
27
+ const SUMSUB_COMPLETED_STATUS = 'Completed';
28
+ const kycControllerMetadata = {
29
+ phase: {
30
+ includeInDebugSnapshot: true,
31
+ includeInStateLogs: true,
32
+ persist: false,
33
+ usedInUi: true,
34
+ },
35
+ statusMessage: {
36
+ includeInDebugSnapshot: true,
37
+ includeInStateLogs: true,
38
+ persist: false,
39
+ usedInUi: true,
40
+ },
41
+ error: {
42
+ includeInDebugSnapshot: true,
43
+ includeInStateLogs: true,
44
+ persist: false,
45
+ usedInUi: true,
46
+ },
47
+ email: {
48
+ includeInDebugSnapshot: false,
49
+ includeInStateLogs: false,
50
+ persist: false,
51
+ usedInUi: false,
52
+ },
53
+ termsAcceptedAt: {
54
+ includeInDebugSnapshot: true,
55
+ includeInStateLogs: true,
56
+ persist: true,
57
+ usedInUi: false,
58
+ },
59
+ acceptedDisclaimerIds: {
60
+ includeInDebugSnapshot: true,
61
+ includeInStateLogs: true,
62
+ persist: true,
63
+ usedInUi: false,
64
+ },
65
+ disclaimers: {
66
+ includeInDebugSnapshot: false,
67
+ includeInStateLogs: false,
68
+ persist: false,
69
+ usedInUi: true,
70
+ },
71
+ disclaimersError: {
72
+ includeInDebugSnapshot: true,
73
+ includeInStateLogs: true,
74
+ persist: false,
75
+ usedInUi: true,
76
+ },
77
+ geoCountry: {
78
+ includeInDebugSnapshot: true,
79
+ includeInStateLogs: true,
80
+ persist: false,
81
+ usedInUi: true,
82
+ },
83
+ sessionToken: {
84
+ includeInDebugSnapshot: false,
85
+ includeInStateLogs: false,
86
+ persist: false,
87
+ usedInUi: false,
88
+ },
89
+ accessToken: {
90
+ includeInDebugSnapshot: false,
91
+ includeInStateLogs: false,
92
+ persist: false,
93
+ usedInUi: false,
94
+ },
95
+ moonpayCustomerId: {
96
+ includeInDebugSnapshot: false,
97
+ includeInStateLogs: false,
98
+ persist: false,
99
+ usedInUi: false,
100
+ },
101
+ activeProduct: {
102
+ includeInDebugSnapshot: true,
103
+ includeInStateLogs: true,
104
+ persist: false,
105
+ usedInUi: true,
106
+ },
107
+ kycRequiredByProduct: {
108
+ includeInDebugSnapshot: true,
109
+ includeInStateLogs: true,
110
+ persist: true,
111
+ usedInUi: true,
112
+ },
113
+ lastCheckedAt: {
114
+ includeInDebugSnapshot: true,
115
+ includeInStateLogs: true,
116
+ persist: true,
117
+ usedInUi: false,
118
+ },
119
+ sumsub: {
120
+ includeInDebugSnapshot: false,
121
+ includeInStateLogs: false,
122
+ persist: false,
123
+ usedInUi: true,
124
+ },
125
+ };
126
+ /**
127
+ * Constructs the default {@link KycController} state.
128
+ *
129
+ * @returns The default state.
130
+ */
131
+ export function getDefaultKycControllerState() {
132
+ return {
133
+ phase: 'idle',
134
+ statusMessage: '',
135
+ error: null,
136
+ email: null,
137
+ termsAcceptedAt: null,
138
+ acceptedDisclaimerIds: [],
139
+ disclaimers: [],
140
+ disclaimersError: null,
141
+ geoCountry: null,
142
+ sessionToken: null,
143
+ accessToken: null,
144
+ moonpayCustomerId: null,
145
+ activeProduct: null,
146
+ kycRequiredByProduct: {},
147
+ lastCheckedAt: null,
148
+ sumsub: {
149
+ status: 'idle',
150
+ result: null,
151
+ sessionId: null,
152
+ applicantAccessToken: null,
153
+ },
154
+ };
155
+ }
156
+ // === MESSENGER ===
157
+ const MESSENGER_EXPOSED_METHODS = [
158
+ 'initialize',
159
+ 'loadDisclaimers',
160
+ 'acceptTermsAndStartSession',
161
+ 'clearSavedTerms',
162
+ 'handleFrameMessage',
163
+ 'buildCheckFrameUrl',
164
+ 'buildAuthFrameUrl',
165
+ 'buildResetFrameUrl',
166
+ 'checkKycRequired',
167
+ 'getKycStatus',
168
+ 'startSumSub',
169
+ 'reset',
170
+ ];
171
+ // === CONTROLLER DEFINITION ===
172
+ /**
173
+ * `KycController` orchestrates the vendor-backed KYC / identity-verification
174
+ * flow (MoonPay identity + SumSub documents) behind a vendor-neutral, per
175
+ * product surface used by ramps and card. It owns all state, HTTP
176
+ * orchestration (via `KycService`), crypto, and the frame message protocol;
177
+ * platform-specific presentation (WebView/iframe, SumSub SDK) is injected.
178
+ */
179
+ export class KycController extends BaseController {
180
+ /**
181
+ * Constructs a new {@link KycController}.
182
+ *
183
+ * @param options - The constructor options.
184
+ * @param options.messenger - The messenger suited for this controller.
185
+ * @param options.state - Partial initial state; merged over defaults.
186
+ * @param options.sumsubLauncher - The platform SumSub launcher adapter.
187
+ */
188
+ constructor({ messenger, state, sumsubLauncher }) {
189
+ super({
190
+ messenger,
191
+ metadata: kycControllerMetadata,
192
+ name: controllerName,
193
+ state: { ...getDefaultKycControllerState(), ...state },
194
+ });
195
+ _KycController_instances.add(this);
196
+ _KycController_sumsubLauncher.set(this, void 0);
197
+ /** Ephemeral X25519 keypair for the frame key exchange (never persisted). */
198
+ _KycController_keypair.set(this, void 0);
199
+ /** Auth-frame client token, kept out of state. */
200
+ _KycController_authClientToken.set(this, null);
201
+ /**
202
+ * Monotonic flow generation. Incremented by {@link reset} so in-flight async
203
+ * work (e.g. the KYC-required check) can detect that it was superseded and
204
+ * avoid writing stale results onto a reset controller.
205
+ */
206
+ _KycController_generation.set(this, 0);
207
+ /**
208
+ * Guards the automatic post-authentication continuation. The Check/Auth
209
+ * frames can post more than one `complete` message (duplicate or late), each
210
+ * of which resolves to an `active` outcome with an access token. Without this
211
+ * flag, every such message would re-enter {@link #continueAfterAuthentication}
212
+ * and run the KYC-required check / SumSub sub-flow again while a prior run is
213
+ * still in flight. Set for the duration of a continuation and cleared by
214
+ * {@link reset} so a fresh flow can continue again.
215
+ */
216
+ _KycController_continuationInFlight.set(this, false);
217
+ __classPrivateFieldSet(this, _KycController_sumsubLauncher, sumsubLauncher, "f");
218
+ __classPrivateFieldSet(this, _KycController_keypair, generateKeyPair(), "f");
219
+ this.messenger.registerMethodActionHandlers(this, MESSENGER_EXPOSED_METHODS);
220
+ }
221
+ /**
222
+ * Resolves persisted terms + geolocation, and auto-creates a session when
223
+ * terms are already accepted and an email is available.
224
+ *
225
+ * @param params - Optional parameters.
226
+ * @param params.email - The account email to associate with the session.
227
+ * @param params.product - The consuming feature the flow runs for. When
228
+ * provided, the controller automatically runs the KYC-required check once
229
+ * authentication completes (and chains into document verification when KYC
230
+ * is required). When omitted, the flow stops at `form` and the consumer must
231
+ * call `checkKycRequired` manually.
232
+ */
233
+ async initialize(params) {
234
+ // `initialize` starts a fresh flow, so `activeProduct` is always reset to
235
+ // this call's product (or `null`). Otherwise a prior run's product could
236
+ // linger and cause `#continueAfterAuthentication` to auto-run the check /
237
+ // sub-flow when the caller intended the manual (product-less) flow.
238
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
239
+ if (params?.email) {
240
+ state.email = params.email;
241
+ }
242
+ state.activeProduct = params?.product ?? null;
243
+ });
244
+ // Resolve country for display; non-blocking.
245
+ try {
246
+ const country = await this.messenger.call('KycService:getGeoCountry');
247
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
248
+ state.geoCountry = country;
249
+ });
250
+ }
251
+ catch {
252
+ // Ignore; disclaimers loading will surface a country error if needed.
253
+ }
254
+ const hasTerms = Boolean(this.state.termsAcceptedAt) &&
255
+ this.state.acceptedDisclaimerIds.length > 0;
256
+ if (hasTerms && this.state.email) {
257
+ await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_createSession).call(this);
258
+ return;
259
+ }
260
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
261
+ state.phase = 'terms';
262
+ });
263
+ await this.loadDisclaimers();
264
+ }
265
+ /**
266
+ * Loads the disclaimers for the resolved (or provided) country.
267
+ *
268
+ * @param params - Optional parameters.
269
+ * @param params.country - ISO 3166-1 alpha-3 country code override.
270
+ */
271
+ async loadDisclaimers(params) {
272
+ try {
273
+ const country = params?.country ??
274
+ this.state.geoCountry ??
275
+ (await this.messenger.call('KycService:getGeoCountry'));
276
+ if (country !== this.state.geoCountry) {
277
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
278
+ state.geoCountry = country;
279
+ });
280
+ }
281
+ const disclaimers = await this.messenger.call('KycService:fetchDisclaimers', { country });
282
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
283
+ state.disclaimers = disclaimers;
284
+ state.disclaimersError = null;
285
+ });
286
+ }
287
+ catch (error) {
288
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
289
+ state.disclaimersError = `Failed to load disclaimers: ${String(error)}`;
290
+ });
291
+ }
292
+ }
293
+ /**
294
+ * Captures terms acceptance for the currently loaded disclaimers and creates
295
+ * a session.
296
+ *
297
+ * @param params - Optional parameters.
298
+ * @param params.email - The account email to associate with the session.
299
+ * @param params.product - The consuming feature the flow runs for. See
300
+ * {@link initialize} for how the product drives the automatic post
301
+ * authentication continuation.
302
+ */
303
+ async acceptTermsAndStartSession(params) {
304
+ const termsAcceptedAt = new Date().toISOString();
305
+ const disclaimerIds = this.state.disclaimers.map((disclaimer) => disclaimer.id);
306
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
307
+ if (params?.email) {
308
+ state.email = params.email;
309
+ }
310
+ if (params?.product) {
311
+ state.activeProduct = params.product;
312
+ }
313
+ state.termsAcceptedAt = termsAcceptedAt;
314
+ state.acceptedDisclaimerIds = disclaimerIds;
315
+ });
316
+ await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_createSession).call(this);
317
+ }
318
+ /**
319
+ * Clears the persisted terms acceptance.
320
+ */
321
+ clearSavedTerms() {
322
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
323
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_clearAcceptedTerms).call(this, state);
324
+ });
325
+ }
326
+ /**
327
+ * Handles a message posted by a Check/Auth frame and advances the flow.
328
+ *
329
+ * The transport-agnostic caller (WebView on mobile, iframe on web) forwards
330
+ * the raw message and injects the returned `reply` back into the frame.
331
+ *
332
+ * @param params - The parameters.
333
+ * @param params.message - The raw message posted by the frame.
334
+ * @returns An object whose optional `reply` should be posted back.
335
+ */
336
+ async handleFrameMessage(params) {
337
+ const payload = params.message;
338
+ if (!payload) {
339
+ return {};
340
+ }
341
+ if (payload.kind === 'handshake') {
342
+ const channelId = payload.meta?.channelId;
343
+ return { reply: { version: 2, meta: { channelId }, kind: 'ack' } };
344
+ }
345
+ if (payload.kind !== 'complete') {
346
+ return {};
347
+ }
348
+ const channelId = payload.meta?.channelId;
349
+ const status = payload.payload?.status;
350
+ const credsEnvelope = payload.payload?.credentials;
351
+ const customerId = payload.payload?.customer?.id ?? null;
352
+ if (customerId) {
353
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
354
+ state.moonpayCustomerId = customerId;
355
+ });
356
+ }
357
+ if (!status) {
358
+ return {};
359
+ }
360
+ let accessToken;
361
+ let clientToken;
362
+ if (credsEnvelope) {
363
+ try {
364
+ const { credentials } = decryptCredentials(credsEnvelope, __classPrivateFieldGet(this, _KycController_keypair, "f").privateKey);
365
+ accessToken = credentials.accessToken;
366
+ clientToken = credentials.clientToken;
367
+ }
368
+ catch (error) {
369
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_fail).call(this, `Failed to decrypt frame credentials: ${String(error)}`);
370
+ return {};
371
+ }
372
+ }
373
+ if (channelId === CHANNEL_CHECK) {
374
+ await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_handleCheckOutcome).call(this, status, accessToken, clientToken);
375
+ return {};
376
+ }
377
+ if (channelId === CHANNEL_AUTH) {
378
+ await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_handleAuthOutcome).call(this, status, accessToken);
379
+ return {};
380
+ }
381
+ return {};
382
+ }
383
+ /**
384
+ * Builds the Check-frame URL, or `null` when no session exists yet.
385
+ *
386
+ * @returns The Check-frame URL or `null`.
387
+ */
388
+ buildCheckFrameUrl() {
389
+ if (!this.state.sessionToken) {
390
+ return null;
391
+ }
392
+ const url = new URL(`${FRAMES_BASE_URL}/check-connection`);
393
+ url.searchParams.set('sessionToken', this.state.sessionToken);
394
+ url.searchParams.set('publicKey', __classPrivateFieldGet(this, _KycController_keypair, "f").publicKeyHex);
395
+ url.searchParams.set('channelId', CHANNEL_CHECK);
396
+ url.searchParams.set('skipKyc', 'true');
397
+ return url.toString();
398
+ }
399
+ /**
400
+ * Builds the Auth-frame URL, or `null` when no client token is available.
401
+ *
402
+ * @returns The Auth-frame URL or `null`.
403
+ */
404
+ buildAuthFrameUrl() {
405
+ if (!__classPrivateFieldGet(this, _KycController_authClientToken, "f")) {
406
+ return null;
407
+ }
408
+ const url = new URL(`${FRAMES_BASE_URL}/auth`);
409
+ url.searchParams.set('clientToken', __classPrivateFieldGet(this, _KycController_authClientToken, "f"));
410
+ url.searchParams.set('publicKey', __classPrivateFieldGet(this, _KycController_keypair, "f").publicKeyHex);
411
+ url.searchParams.set('channelId', CHANNEL_AUTH);
412
+ return url.toString();
413
+ }
414
+ /**
415
+ * Builds the Reset-frame URL.
416
+ *
417
+ * @returns The Reset-frame URL.
418
+ */
419
+ buildResetFrameUrl() {
420
+ const url = new URL(`${FRAMES_BASE_URL}/reset`);
421
+ url.searchParams.set('channelId', CHANNEL_RESET);
422
+ return url.toString();
423
+ }
424
+ /**
425
+ * Checks whether KYC is required for a product and caches the result.
426
+ *
427
+ * @param params - The parameters.
428
+ * @param params.product - The consuming feature.
429
+ * @param params.country - Optional alpha-3 country override.
430
+ * @returns Whether KYC is required.
431
+ */
432
+ async checkKycRequired(params) {
433
+ const { accessToken } = this.state;
434
+ if (!accessToken) {
435
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_fail).call(this, 'Missing accessToken — repeat the authentication step.');
436
+ return false;
437
+ }
438
+ const country = params.country ?? this.state.geoCountry;
439
+ if (!country) {
440
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_fail).call(this, 'Missing country for KYC-required check.');
441
+ return false;
442
+ }
443
+ // Capture the flow generation so we can detect a `reset()` that happens
444
+ // while the HTTP call is in flight and avoid writing stale results.
445
+ const generation = __classPrivateFieldGet(this, _KycController_generation, "f");
446
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
447
+ state.phase = 'submit';
448
+ state.statusMessage = 'Checking KYC status...';
449
+ });
450
+ try {
451
+ const { kycRequired } = await this.messenger.call('KycService:checkKycRequired', { accessToken, country, capabilities: [{ product: params.product }] });
452
+ // The flow was reset while the check was in flight; discard the result
453
+ // rather than resurrecting a done/cached state on an idle controller.
454
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
455
+ return false;
456
+ }
457
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
458
+ state.kycRequiredByProduct[params.product] = kycRequired;
459
+ state.lastCheckedAt = new Date().toISOString();
460
+ state.phase = 'done';
461
+ state.statusMessage = 'KYC check complete.';
462
+ });
463
+ return kycRequired;
464
+ }
465
+ catch (error) {
466
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
467
+ return false;
468
+ }
469
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_fail).call(this, `KYC check failed: ${String(error)}`);
470
+ return false;
471
+ }
472
+ }
473
+ /**
474
+ * Reads the cached "is KYC required" result for a product.
475
+ *
476
+ * @param params - The parameters.
477
+ * @param params.product - The consuming feature.
478
+ * @returns The cached value, or `undefined` if not yet checked.
479
+ */
480
+ getKycStatus(params) {
481
+ return this.state.kycRequiredByProduct[params.product];
482
+ }
483
+ /**
484
+ * Runs the SumSub document-verification sub-flow: creates a UKYC session,
485
+ * exchanges the wrapped key for an applicant access token, and presents the
486
+ * SDK via the injected launcher.
487
+ *
488
+ * @param params - Optional parameters.
489
+ * @param params.locale - BCP-47 locale for the SDK UI.
490
+ * @param params.debug - Enables SDK debug logging.
491
+ * @returns The SDK result.
492
+ */
493
+ async startSumSub(params) {
494
+ if (!__classPrivateFieldGet(this, _KycController_sumsubLauncher, "f").isAvailable()) {
495
+ const error = 'SumSub SDK is not available in this runtime.';
496
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
497
+ state.sumsub.status = 'failed';
498
+ state.sumsub.result = { error };
499
+ });
500
+ throw new Error(error);
501
+ }
502
+ // Capture the flow generation so each async step can detect a `reset()`
503
+ // that lands mid-flight and avoid writing stale sub-flow state (or, worse,
504
+ // presenting the SDK) on a controller that is now idle.
505
+ const generation = __classPrivateFieldGet(this, _KycController_generation, "f");
506
+ try {
507
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
508
+ state.sumsub.status = 'creatingSession';
509
+ state.sumsub.result = null;
510
+ });
511
+ const jwtToken = MOCK_JWT_TOKEN;
512
+ const { sessionId, wrappingPublicKey, idosSessionId } = await this.messenger.call('KycService:createUkycSession', {
513
+ jwtToken,
514
+ vendorMetadata: {
515
+ moonPayAccessToken: this.state.accessToken,
516
+ moonPayUserId: this.state.moonpayCustomerId,
517
+ },
518
+ });
519
+ // Retain the exchange material so the SDK can refresh its token. The
520
+ // session's `wrappingPublicKey` is forwarded opaquely as the request's
521
+ // `wrappedUserKey` field (the /wrapped-key endpoint's body is unchanged).
522
+ const exchange = {
523
+ sessionId,
524
+ wrappedUserKey: wrappingPublicKey,
525
+ idosSessionId,
526
+ jwtToken,
527
+ };
528
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
529
+ state.sumsub.status = 'fetchingToken';
530
+ state.sumsub.sessionId = sessionId;
531
+ });
532
+ const { applicantAccessToken } = await this.messenger.call('KycService:submitWrappedKey', exchange);
533
+ // A reset() landed while the session/token was being prepared; abort
534
+ // before presenting the SDK rather than launching on an idle controller.
535
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
536
+ return {};
537
+ }
538
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
539
+ state.sumsub.status = 'launching';
540
+ state.sumsub.applicantAccessToken = applicantAccessToken;
541
+ });
542
+ // Track whether the SDK ever reported a successful completion. A resolved
543
+ // `launch` alone does not imply success — the applicant may have
544
+ // abandoned the flow or the SDK may have reported a non-success outcome.
545
+ let reachedCompletion = false;
546
+ const result = await __classPrivateFieldGet(this, _KycController_sumsubLauncher, "f").launch({
547
+ applicantAccessToken,
548
+ onTokenExpiration: async () => {
549
+ // A reset() may have superseded this flow while the SDK stayed open.
550
+ // Refuse to refresh against the now-stale UKYC session rather than
551
+ // silently keeping an orphaned SDK alive.
552
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
553
+ throw new Error('KYC flow was reset; SumSub session is no longer active.');
554
+ }
555
+ const refreshed = await this.messenger.call('KycService:submitWrappedKey', exchange);
556
+ return refreshed.applicantAccessToken;
557
+ },
558
+ onStatusChange: (_prev, next) => {
559
+ if (next === SUMSUB_COMPLETED_STATUS) {
560
+ reachedCompletion = true;
561
+ }
562
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
563
+ state.sumsub.status =
564
+ next === SUMSUB_COMPLETED_STATUS ? 'complete' : 'inProgress';
565
+ });
566
+ },
567
+ locale: params?.locale ?? 'en',
568
+ debug: params?.debug ?? false,
569
+ });
570
+ // Only record `complete` when the SDK actually reported completion;
571
+ // otherwise treat the resolved-but-unfinished flow as `failed` so
572
+ // consumers and UI do not mistake it for a finished verification.
573
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
574
+ state.sumsub.status = reachedCompletion ? 'complete' : 'failed';
575
+ state.sumsub.result = result;
576
+ });
577
+ return result;
578
+ }
579
+ catch (error) {
580
+ const result = { error: String(error) };
581
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
582
+ state.sumsub.status = 'failed';
583
+ state.sumsub.result = result;
584
+ });
585
+ return result;
586
+ }
587
+ }
588
+ /**
589
+ * Resets the flow to idle, clearing session tokens and sub-flow state while
590
+ * preserving persisted terms acceptance and the per-product cache.
591
+ */
592
+ reset() {
593
+ __classPrivateFieldSet(this, _KycController_authClientToken, null, "f");
594
+ // Invalidate any in-flight async work started before this reset.
595
+ __classPrivateFieldSet(this, _KycController_generation, __classPrivateFieldGet(this, _KycController_generation, "f") + 1, "f");
596
+ // Allow the next authenticated flow to auto-continue; any continuation from
597
+ // the superseded generation will no longer clear this flag (see the
598
+ // generation guard in `#continueAfterAuthentication`).
599
+ __classPrivateFieldSet(this, _KycController_continuationInFlight, false, "f");
600
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
601
+ state.phase = 'idle';
602
+ state.statusMessage = '';
603
+ state.error = null;
604
+ state.disclaimers = [];
605
+ state.disclaimersError = null;
606
+ state.sessionToken = null;
607
+ state.accessToken = null;
608
+ state.moonpayCustomerId = null;
609
+ state.activeProduct = null;
610
+ state.sumsub = {
611
+ status: 'idle',
612
+ result: null,
613
+ sessionId: null,
614
+ applicantAccessToken: null,
615
+ };
616
+ });
617
+ }
618
+ }
619
+ _KycController_sumsubLauncher = new WeakMap(), _KycController_keypair = new WeakMap(), _KycController_authClientToken = new WeakMap(), _KycController_generation = new WeakMap(), _KycController_continuationInFlight = new WeakMap(), _KycController_instances = new WeakSet(), _KycController_createSession =
620
+ /**
621
+ * Creates a vendor session from the currently stored terms + email.
622
+ */
623
+ async function _KycController_createSession() {
624
+ const { email, termsAcceptedAt, acceptedDisclaimerIds } = this.state;
625
+ if (!email) {
626
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_fail).call(this, 'Missing email for session creation.');
627
+ return;
628
+ }
629
+ if (!termsAcceptedAt || acceptedDisclaimerIds.length === 0) {
630
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_fail).call(this, 'Missing terms acceptance for session creation.');
631
+ return;
632
+ }
633
+ // A new session invalidates any authentication carried over from a prior
634
+ // session. Clear the stale access token and auth-frame client token so
635
+ // `buildAuthFrameUrl` cannot return a URL tied to an old client token and
636
+ // `checkKycRequired` cannot run with an access token from an earlier
637
+ // authentication. The Check/Auth frames re-populate these for the new
638
+ // session.
639
+ __classPrivateFieldSet(this, _KycController_authClientToken, null, "f");
640
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
641
+ state.error = null;
642
+ state.phase = 'session';
643
+ state.statusMessage = 'Creating session...';
644
+ state.accessToken = null;
645
+ });
646
+ try {
647
+ const { sessionToken } = await this.messenger.call('KycService:createSession', { email, termsAcceptedAt, disclaimerIds: acceptedDisclaimerIds });
648
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
649
+ state.sessionToken = sessionToken;
650
+ state.phase = 'check';
651
+ state.statusMessage = 'Authenticating via Check frame...';
652
+ });
653
+ }
654
+ catch (error) {
655
+ // Invalidate the stored acceptance so the customer can retry. Also clear
656
+ // `activeProduct` so a later `acceptTermsAndStartSession` that omits a
657
+ // product cannot auto-run the KYC check / SumSub chain for this failed
658
+ // flow's product — matching how `initialize` starts from a clean product.
659
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
660
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_clearAcceptedTerms).call(this, state);
661
+ state.activeProduct = null;
662
+ state.error = `Session creation failed: ${String(error)}`;
663
+ state.statusMessage =
664
+ 'Session creation failed — accept the terms to try again.';
665
+ state.phase = 'terms';
666
+ });
667
+ await this.loadDisclaimers();
668
+ }
669
+ }, _KycController_clearAcceptedTerms = function _KycController_clearAcceptedTerms(state) {
670
+ state.termsAcceptedAt = null;
671
+ state.acceptedDisclaimerIds = [];
672
+ }, _KycController_handleCheckOutcome =
673
+ /**
674
+ * Applies a Check-frame outcome.
675
+ *
676
+ * @param status - The frame status.
677
+ * @param accessToken - The decrypted access token, if any.
678
+ * @param clientToken - The decrypted client token, if any.
679
+ */
680
+ async function _KycController_handleCheckOutcome(status, accessToken, clientToken) {
681
+ if (status === 'active' && accessToken) {
682
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
683
+ state.accessToken = accessToken;
684
+ state.phase = 'form';
685
+ state.statusMessage = 'Already authenticated. Review to submit.';
686
+ });
687
+ await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_continueAfterAuthentication).call(this);
688
+ return;
689
+ }
690
+ if (status === 'connectionRequired' && clientToken) {
691
+ __classPrivateFieldSet(this, _KycController_authClientToken, clientToken, "f");
692
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
693
+ state.phase = 'auth';
694
+ state.statusMessage = 'Verify your email via OTP in the Auth frame.';
695
+ });
696
+ return;
697
+ }
698
+ if (status === 'termsAcceptanceRequired') {
699
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_requireTermsReacceptance).call(this);
700
+ return;
701
+ }
702
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_fail).call(this, `Check frame returned status: ${status}`);
703
+ }, _KycController_handleAuthOutcome =
704
+ /**
705
+ * Applies an Auth-frame outcome.
706
+ *
707
+ * @param status - The frame status.
708
+ * @param accessToken - The decrypted access token, if any.
709
+ */
710
+ async function _KycController_handleAuthOutcome(status, accessToken) {
711
+ if (status === 'active' && accessToken) {
712
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
713
+ state.accessToken = accessToken;
714
+ state.phase = 'form';
715
+ state.statusMessage = 'Authenticated. Review to submit.';
716
+ });
717
+ await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_continueAfterAuthentication).call(this);
718
+ return;
719
+ }
720
+ if (status === 'termsAcceptanceRequired') {
721
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_requireTermsReacceptance).call(this);
722
+ return;
723
+ }
724
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_fail).call(this, `Auth frame returned status: ${status}`);
725
+ }, _KycController_continueAfterAuthentication =
726
+ /**
727
+ * Continues the flow once authentication has completed (phase `form`).
728
+ *
729
+ * When the flow is scoped to a product (see {@link initialize}), the
730
+ * KYC-required check runs automatically, and — when KYC is required — the
731
+ * document-verification sub-flow is launched. When no product is set, this is
732
+ * a no-op and the flow stays at `form` for the consumer to drive manually.
733
+ *
734
+ * Errors are already recorded on state by `checkKycRequired` (`error`
735
+ * phase) and `startSumSub` (`sumsub.status = 'failed'`); this method swallows
736
+ * them so it can be awaited safely from the frame-message handler.
737
+ */
738
+ async function _KycController_continueAfterAuthentication() {
739
+ const product = this.state.activeProduct;
740
+ if (!product) {
741
+ return;
742
+ }
743
+ // A duplicate or late `complete` message can re-enter here while a prior
744
+ // continuation is still running; ignore it so the check / sub-flow does not
745
+ // run twice concurrently.
746
+ if (__classPrivateFieldGet(this, _KycController_continuationInFlight, "f")) {
747
+ return;
748
+ }
749
+ __classPrivateFieldSet(this, _KycController_continuationInFlight, true, "f");
750
+ // Capture the generation so a `reset()` landing mid-continuation does not
751
+ // let the `finally` clear a flag that belongs to a newer flow.
752
+ const generation = __classPrivateFieldGet(this, _KycController_generation, "f");
753
+ try {
754
+ const kycRequired = await this.checkKycRequired({ product });
755
+ if (!kycRequired) {
756
+ return;
757
+ }
758
+ try {
759
+ await this.startSumSub();
760
+ }
761
+ catch {
762
+ // `startSumSub` already records `sumsub.status = 'failed'`; swallow the
763
+ // rethrown error (e.g. SDK unavailable) so the awaited continuation
764
+ // resolves cleanly rather than surfacing as an unhandled rejection.
765
+ }
766
+ }
767
+ finally {
768
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") === generation) {
769
+ __classPrivateFieldSet(this, _KycController_continuationInFlight, false, "f");
770
+ }
771
+ }
772
+ }, _KycController_requireTermsReacceptance = function _KycController_requireTermsReacceptance() {
773
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
774
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_clearAcceptedTerms).call(this, state);
775
+ state.phase = 'terms';
776
+ state.statusMessage =
777
+ 'The vendor updated its Terms of Use — please re-accept.';
778
+ });
779
+ }, _KycController_updateIfCurrent = function _KycController_updateIfCurrent(generation, updater) {
780
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
781
+ return false;
782
+ }
783
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, updater);
784
+ return true;
785
+ }, _KycController_applyUpdate = function _KycController_applyUpdate(updater) {
786
+ this.update((state) => {
787
+ updater(state);
788
+ });
789
+ }, _KycController_fail = function _KycController_fail(message) {
790
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
791
+ state.error = message;
792
+ state.phase = 'error';
793
+ });
794
+ };
795
+ //# sourceMappingURL=KycController.mjs.map