@metamask-previews/kyc-controller 0.0.0-preview-e68ab1498 → 0.0.0-preview-4b37525

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 (56) hide show
  1. package/CHANGELOG.md +7 -1
  2. package/dist/KycController-method-action-types.cjs.map +1 -1
  3. package/dist/KycController-method-action-types.d.cts +12 -6
  4. package/dist/KycController-method-action-types.d.cts.map +1 -1
  5. package/dist/KycController-method-action-types.d.mts +12 -6
  6. package/dist/KycController-method-action-types.d.mts.map +1 -1
  7. package/dist/KycController-method-action-types.mjs.map +1 -1
  8. package/dist/KycController.cjs +313 -72
  9. package/dist/KycController.cjs.map +1 -1
  10. package/dist/KycController.d.cts +27 -7
  11. package/dist/KycController.d.cts.map +1 -1
  12. package/dist/KycController.d.mts +27 -7
  13. package/dist/KycController.d.mts.map +1 -1
  14. package/dist/KycController.mjs +314 -73
  15. package/dist/KycController.mjs.map +1 -1
  16. package/dist/KycService-method-action-types.cjs.map +1 -1
  17. package/dist/KycService-method-action-types.d.cts +60 -31
  18. package/dist/KycService-method-action-types.d.cts.map +1 -1
  19. package/dist/KycService-method-action-types.d.mts +60 -31
  20. package/dist/KycService-method-action-types.d.mts.map +1 -1
  21. package/dist/KycService-method-action-types.mjs.map +1 -1
  22. package/dist/KycService.cjs +140 -75
  23. package/dist/KycService.cjs.map +1 -1
  24. package/dist/KycService.d.cts +181 -74
  25. package/dist/KycService.d.cts.map +1 -1
  26. package/dist/KycService.d.mts +181 -74
  27. package/dist/KycService.d.mts.map +1 -1
  28. package/dist/KycService.mjs +140 -75
  29. package/dist/KycService.mjs.map +1 -1
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.cts +3 -3
  32. package/dist/index.d.cts.map +1 -1
  33. package/dist/index.d.mts +3 -3
  34. package/dist/index.d.mts.map +1 -1
  35. package/dist/index.mjs.map +1 -1
  36. package/dist/types.cjs.map +1 -1
  37. package/dist/types.d.cts +49 -4
  38. package/dist/types.d.cts.map +1 -1
  39. package/dist/types.d.mts +49 -4
  40. package/dist/types.d.mts.map +1 -1
  41. package/dist/types.mjs.map +1 -1
  42. package/dist/ukyc/jwtChain.cjs +1 -1
  43. package/dist/ukyc/jwtChain.cjs.map +1 -1
  44. package/dist/ukyc/jwtChain.d.cts +2 -2
  45. package/dist/ukyc/jwtChain.d.mts +2 -2
  46. package/dist/ukyc/jwtChain.mjs +1 -1
  47. package/dist/ukyc/jwtChain.mjs.map +1 -1
  48. package/dist/ukyc/wrapEncryptionKey.cjs +16 -7
  49. package/dist/ukyc/wrapEncryptionKey.cjs.map +1 -1
  50. package/dist/ukyc/wrapEncryptionKey.d.cts +23 -16
  51. package/dist/ukyc/wrapEncryptionKey.d.cts.map +1 -1
  52. package/dist/ukyc/wrapEncryptionKey.d.mts +23 -16
  53. package/dist/ukyc/wrapEncryptionKey.d.mts.map +1 -1
  54. package/dist/ukyc/wrapEncryptionKey.mjs +16 -7
  55. package/dist/ukyc/wrapEncryptionKey.mjs.map +1 -1
  56. package/package.json +1 -1
@@ -10,13 +10,13 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  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");
11
11
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
12
12
  };
13
- var _KycController_instances, _KycController_sumsubLauncher, _KycController_keypair, _KycController_authClientToken, _KycController_generation, _KycController_sessionStatusPollIntervalMs, _KycController_pollTimer, _KycController_pollToken, _KycController_userStatusPollIntervalMs, _KycController_userStatusPollTimer, _KycController_userStatusPolling, _KycController_userStatusPollToken, _KycController_localUserSecretStore, _KycController_startConsentsSession, _KycController_createSession, _KycController_clearAcceptedTerms, _KycController_clearMoonPaySession, _KycController_dropTermsUnlessForVendor, _KycController_hasTermsForVendor, _KycController_handleCheckOutcome, _KycController_handleAuthOutcome, _KycController_continueAfterAuthentication, _KycController_requireTermsReacceptance, _KycController_buildUkycSessionVendorFields, _KycController_fetchAndApplyUserStatus, _KycController_applyUserStatus, _KycController_ensureUserStatusPolling, _KycController_stopUserStatusPolling, _KycController_startSessionStatusPolling, _KycController_pollSessionStatusOnce, _KycController_stopPolling, _KycController_cancelPendingSession, _KycController_updateIfCurrent, _KycController_applyUpdate, _KycController_fail;
13
+ var _KycController_instances, _KycController_sumsubLauncher, _KycController_keypair, _KycController_authClientToken, _KycController_generation, _KycController_sessionStatusPollIntervalMs, _KycController_pollTimer, _KycController_pollToken, _KycController_userStatusPollIntervalMs, _KycController_userStatusPollTimer, _KycController_userStatusPolling, _KycController_userStatusPollToken, _KycController_localUserSecretStore, _KycController_startConsentsSession, _KycController_recordSessionDisclaimers, _KycController_createSession, _KycController_clearAcceptedTerms, _KycController_clearMoonPaySession, _KycController_dropTermsUnlessForVendor, _KycController_hasTermsForVendor, _KycController_handleCheckOutcome, _KycController_handleAuthOutcome, _KycController_continueAfterAuthentication, _KycController_requireTermsReacceptance, _KycController_buildUkycSessionVendorFields, _KycController_createUkycSession, _KycController_fetchAndApplyUserStatus, _KycController_applyUserStatus, _KycController_ensureUserStatusPolling, _KycController_stopUserStatusPolling, _KycController_startSessionStatusPolling, _KycController_pollSessionStatusOnce, _KycController_stopPolling, _KycController_cancelPendingSession, _KycController_updateIfCurrent, _KycController_applyUpdate, _KycController_assertAttestedServerPublicKey, _KycController_fail;
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.KycController = exports.getDefaultKycControllerState = exports.controllerName = void 0;
16
16
  const base_controller_1 = require("@metamask/base-controller");
17
+ const utils_1 = require("@metamask/utils");
17
18
  const ed25519_1 = require("@noble/curves/ed25519");
18
19
  const crypto_js_1 = require("./crypto.cjs");
19
- const encoding_js_1 = require("./encoding.cjs");
20
20
  const deriveClientMaterial_js_1 = require("./ukyc/deriveClientMaterial.cjs");
21
21
  const jwtChain_js_1 = require("./ukyc/jwtChain.cjs");
22
22
  const localUserSecret_js_1 = require("./ukyc/localUserSecret.cjs");
@@ -148,6 +148,12 @@ const kycControllerMetadata = {
148
148
  persist: true,
149
149
  usedInUi: false,
150
150
  },
151
+ credentialReusabilityConsentGiven: {
152
+ includeInDebugSnapshot: true,
153
+ includeInStateLogs: true,
154
+ persist: false,
155
+ usedInUi: false,
156
+ },
151
157
  disclaimers: {
152
158
  includeInDebugSnapshot: false,
153
159
  includeInStateLogs: false,
@@ -160,6 +166,12 @@ const kycControllerMetadata = {
160
166
  persist: false,
161
167
  usedInUi: true,
162
168
  },
169
+ sessionDisclaimers: {
170
+ includeInDebugSnapshot: false,
171
+ includeInStateLogs: false,
172
+ persist: false,
173
+ usedInUi: true,
174
+ },
163
175
  geoCountry: {
164
176
  includeInDebugSnapshot: true,
165
177
  includeInStateLogs: true,
@@ -249,8 +261,10 @@ function getDefaultKycControllerState() {
249
261
  termsAcceptedVendor: null,
250
262
  sumsubTncAccepted: null,
251
263
  idosTncAccepted: null,
264
+ credentialReusabilityConsentGiven: null,
252
265
  disclaimers: [],
253
266
  disclaimersError: null,
267
+ sessionDisclaimers: null,
254
268
  geoCountry: null,
255
269
  sessionToken: null,
256
270
  accessToken: null,
@@ -284,6 +298,60 @@ exports.getDefaultKycControllerState = getDefaultKycControllerState;
284
298
  function isSessionAlreadyCompletedError(error) {
285
299
  return String(error).includes(SESSION_NOT_IN_VALID_STATE);
286
300
  }
301
+ /**
302
+ * Whether recording session disclaimers failed because those document
303
+ * versions were already consented for the session (`409 Conflict`).
304
+ *
305
+ * @param error - The caught error.
306
+ * @returns `true` when the error is an HTTP 409.
307
+ */
308
+ function isConsentConflictError(error) {
309
+ return (typeof error === 'object' &&
310
+ error !== null &&
311
+ typeof error.httpStatus === 'number' &&
312
+ error.httpStatus === 409);
313
+ }
314
+ /**
315
+ * Maps a session-disclaimer catalog into the `{ key, version }` records the
316
+ * record-consents API expects, or an empty list when the user declined that
317
+ * category.
318
+ *
319
+ * @param documents - Catalog documents for one consent category.
320
+ * @param accepted - Whether the user accepted that category.
321
+ * @returns Consent records, or `[]` when not accepted.
322
+ */
323
+ function consentRecordsFromCatalog(documents, accepted) {
324
+ if (!accepted) {
325
+ return [];
326
+ }
327
+ return documents
328
+ .filter((document) => !document.consented)
329
+ .map(({ key, version }) => ({ key, version }));
330
+ }
331
+ /**
332
+ * Whether an accepted T&C2 category has no catalog documents. An empty list
333
+ * would otherwise skip the POST and count as success.
334
+ *
335
+ * @param documents - Catalog documents for one consent category.
336
+ * @param accepted - Whether the user accepted that category.
337
+ * @returns `true` when the user accepted and the catalog is empty.
338
+ */
339
+ function isAcceptedCategoryEmpty(documents, accepted) {
340
+ return accepted && documents.length === 0;
341
+ }
342
+ /**
343
+ * Whether an accepted category is still missing consent after a 409 re-GET:
344
+ * empty catalog or any document still unconsented.
345
+ *
346
+ * @param documents - Latest catalog documents for one consent category.
347
+ * @param accepted - Whether the user accepted that category.
348
+ * @returns `true` when accepted documents are not fully consented.
349
+ */
350
+ function acceptedCategoryStillMissing(documents, accepted) {
351
+ return (accepted &&
352
+ (documents.length === 0 ||
353
+ documents.some((document) => !document.consented)));
354
+ }
287
355
  /**
288
356
  * Vendors other than MoonPay skip Check/Auth frames and use the empty-shell
289
357
  * customer + consents path instead.
@@ -495,6 +563,7 @@ class KycController extends base_controller_1.BaseController {
495
563
  await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_startConsentsSession).call(this, {
496
564
  sumsubTncSigned,
497
565
  idosTncSigned,
566
+ credentialReusabilityConsentGiven: this.state.credentialReusabilityConsentGiven ?? false,
498
567
  });
499
568
  }
500
569
  else {
@@ -601,6 +670,9 @@ class KycController extends base_controller_1.BaseController {
601
670
  * Required for every vendor so callers explicitly declare acceptance.
602
671
  * @param params.idosTncSigned - Whether idOS T&C were accepted (T&C2).
603
672
  * Required for every vendor so callers explicitly declare acceptance.
673
+ * @param params.credentialReusabilityConsentGiven - Whether the customer
674
+ * consented to reuse existing idOS credentials. Used when recording
675
+ * session-scoped disclaimers on the consents path. Defaults to `false`.
604
676
  */
605
677
  async acceptTermsAndStartSession(params) {
606
678
  const sumsubTncSigned = params?.sumsubTncSigned;
@@ -610,6 +682,7 @@ class KycController extends base_controller_1.BaseController {
610
682
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_fail).call(this, 'Missing T&C2 acceptance flags.');
611
683
  return;
612
684
  }
685
+ const credentialReusabilityConsentGiven = params.credentialReusabilityConsentGiven ?? false;
613
686
  const termsAcceptedAt = new Date().toISOString();
614
687
  const disclaimerIds = this.state.disclaimers.map((disclaimer) => disclaimer.id);
615
688
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
@@ -624,9 +697,15 @@ class KycController extends base_controller_1.BaseController {
624
697
  state.termsAcceptedVendor = state.activeVendor;
625
698
  state.sumsubTncAccepted = sumsubTncSigned;
626
699
  state.idosTncAccepted = idosTncSigned;
700
+ state.credentialReusabilityConsentGiven =
701
+ credentialReusabilityConsentGiven;
627
702
  });
628
703
  if (usesConsentsFlow(this.state.activeVendor)) {
629
- await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_startConsentsSession).call(this, { sumsubTncSigned, idosTncSigned });
704
+ await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_startConsentsSession).call(this, {
705
+ sumsubTncSigned,
706
+ idosTncSigned,
707
+ credentialReusabilityConsentGiven,
708
+ });
630
709
  return;
631
710
  }
632
711
  await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_createSession).call(this);
@@ -840,17 +919,20 @@ class KycController extends base_controller_1.BaseController {
840
919
  /**
841
920
  * Runs the SumSub document-verification sub-flow end to end:
842
921
  *
843
- * 1. requests a per-session wrapping key from the UKYC backend;
844
- * 2. verifies its `jwtChain` against the Fractal JWKS and confirms the
845
- * attested session server public key;
922
+ * 1. creates a UKYC session, receiving per-secret encryption schemas;
923
+ * 2. verifies each schema's `jwtChain` against the Fractal JWKS and confirms
924
+ * the attested session server public key;
846
925
  * 3. derives the `data_encryption_key` from the wallet's UKYC
847
926
  * `local_user_secret` and wraps it for the session server;
848
- * 4. mints a client-signed, read-only `ukyc_capability_token` and creates
849
- * the UKYC session (handing over the wrapped key and the token);
927
+ * 4. mints a client-signed, read-only `ukyc_capability_token`, wraps it the
928
+ * same way as the encryption key, and submits both via authorizations;
850
929
  * 5. fetches the SumSub applicant access token; and
851
930
  * 6. presents the SDK via the injected launcher.
852
931
  *
853
- * If session creation reports the applicant is already approved on the relay
932
+ * If a UKYC session already exists (the consents path creates it before
933
+ * recording session disclaimers), steps 1–4 are skipped.
934
+ *
935
+ * If authorizations report the applicant is already approved on the relay
854
936
  * while the vendor is still finalizing (`kycStatus: approved`,
855
937
  * `finalStatus: pending`), the sub-flow stops at step 4 with a
856
938
  * `vendorProcessing` status and a message rather than launching the SDK.
@@ -876,65 +958,32 @@ class KycController extends base_controller_1.BaseController {
876
958
  // presenting the SDK) on a controller that is now idle.
877
959
  const generation = __classPrivateFieldGet(this, _KycController_generation, "f");
878
960
  try {
879
- __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
880
- state.sumsub.status = 'creatingSession';
881
- state.sumsub.result = null;
882
- state.sumsub.sessionStatus = null;
883
- });
884
- const jwtToken = MOCK_JWT_TOKEN;
885
- // Establish a per-session X25519 keypair and exchange our public half for
886
- // the server's wrapping key. The private half stays on the device and is
887
- // used to derive the shared secret that seals the data_encryption_key.
888
- const sessionClientPrivateKey = ed25519_1.x25519.utils.randomSecretKey();
889
- const sessionClientPublicKey = ed25519_1.x25519.getPublicKey(sessionClientPrivateKey);
890
- const wrappingKey = await this.messenger.call('KycService:getWrappingKey', { sessionClientPublicKey: (0, encoding_js_1.toBase64Url)(sessionClientPublicKey) });
891
- // Verify the jwtChain against Fractal's JWKS, then confirm the returned
892
- // sessionServerPublicKey matches the value attested inside the verified
893
- // JWT payload before trusting it for key wrapping.
894
- const { keys } = await this.messenger.call('KycService:fetchJwks');
895
- const jwtChainPayload = (0, jwtChain_js_1.verifyJwtChain)(keys, wrappingKey.jwtChain);
896
- if (jwtChainPayload.sessionServerPublicKeyX !==
897
- wrappingKey.sessionServerPublicKey.x) {
898
- throw new Error('sessionServerPublicKey does not match the verified jwtChain payload (sessionServerPublicKeyX).');
899
- }
900
- // Derive the data_encryption_key from the local_user_secret and wrap it
901
- // for the session server. Only the wrapped (encrypted) key ever leaves
902
- // the device.
903
- const localUserSecret = await (0, localUserSecret_js_1.getOrCreateLocalUserSecret)(__classPrivateFieldGet(this, _KycController_instances, "m", _KycController_localUserSecretStore).call(this));
904
- const clientMaterial = (0, deriveClientMaterial_js_1.deriveClientMaterial)(localUserSecret);
905
- const wrappedEncryptionKey = {
906
- sessionId: wrappingKey.id,
907
- ...(0, wrapEncryptionKey_js_1.wrapEncryptionKey)(sessionClientPrivateKey, wrappingKey.sessionServerPublicKey.x, clientMaterial.dataEncryptionKey),
908
- };
909
- // Mint a read-only `ukyc_capability_token` for the session. Only the
910
- // client holds the signing key derived from `local_user_secret`, so only
911
- // the client can mint it; scoping it to `read` means it authorizes later
912
- // storage reads without granting write or delete access.
913
- const ukycCapabilityToken = (0, storageAccessToken_js_1.signStorageAccessToken)({
914
- material: clientMaterial,
915
- operations: ['read'],
916
- expiresAt: new Date(Date.now() + UKYC_CAPABILITY_TOKEN_TTL_MS),
917
- });
918
- const { sessionId, kycStatus, finalStatus } = await this.messenger.call('KycService:createUkycSession', {
919
- jwtToken,
920
- ...__classPrivateFieldGet(this, _KycController_instances, "m", _KycController_buildUkycSessionVendorFields).call(this),
921
- wrappedEncryptionKey,
922
- ukycCapabilityToken,
923
- });
924
- // A user who already finished the journey can return to a session the
925
- // relay has already approved (`kycStatus`) while the vendor is still
926
- // finalizing its own decision (`finalStatus`). There is nothing left to
927
- // verify, so stop here and surface a message rather than launching the
928
- // SDK again.
929
- if (kycStatus === KYC_STATUSES.approved &&
930
- finalStatus === KYC_STATUSES.pending) {
931
- const stillCurrent = __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
932
- state.sumsub.status = 'vendorProcessing';
933
- state.sumsub.sessionId = sessionId;
934
- state.statusMessage = VENDOR_PROCESSING_MESSAGE;
961
+ if (!this.state.sumsub.sessionId) {
962
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
963
+ state.sumsub.status = 'creatingSession';
964
+ state.sumsub.result = null;
965
+ state.sumsub.sessionStatus = null;
935
966
  });
936
- return stillCurrent ? { kycStatus, finalStatus } : {};
967
+ const created = await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_createUkycSession).call(this, generation);
968
+ if (!created) {
969
+ return {};
970
+ }
971
+ // A user who already finished the journey can return to a session the
972
+ // relay has already approved (`kycStatus`) while the vendor is still
973
+ // finalizing its own decision (`finalStatus`). There is nothing left to
974
+ // verify, so stop here and surface a message rather than launching the
975
+ // SDK again.
976
+ if (created.vendorProcessing) {
977
+ return {
978
+ kycStatus: created.kycStatus,
979
+ finalStatus: created.finalStatus,
980
+ };
981
+ }
937
982
  }
983
+ // Empty string is a valid "no id to poll" session id used by tests and
984
+ // must not be coalesced away as missing.
985
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
986
+ const sessionId = this.state.sumsub.sessionId || '';
938
987
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
939
988
  state.sumsub.status = 'fetchingToken';
940
989
  state.sumsub.sessionId = sessionId;
@@ -1096,6 +1145,8 @@ class KycController extends base_controller_1.BaseController {
1096
1145
  state.error = null;
1097
1146
  state.disclaimers = [];
1098
1147
  state.disclaimersError = null;
1148
+ state.sessionDisclaimers = null;
1149
+ state.credentialReusabilityConsentGiven = null;
1099
1150
  state.sessionToken = null;
1100
1151
  state.accessToken = null;
1101
1152
  state.moonpayCustomerId = null;
@@ -1133,12 +1184,15 @@ _KycController_sumsubLauncher = new WeakMap(), _KycController_keypair = new Weak
1133
1184
  };
1134
1185
  }, _KycController_startConsentsSession =
1135
1186
  /**
1136
- * Consents-path vendors: post vendor signings + Sumsub/idOS ack, then
1137
- * launch SumSub — skipping MoonPay Check/Auth frames.
1187
+ * Consents-path vendors: record vendor T&Cs, create a UKYC session,
1188
+ * record session-scoped idOS / KYC-provider disclaimers, then launch
1189
+ * SumSub — skipping MoonPay Check/Auth frames.
1138
1190
  *
1139
- * @param consents - T&C2 boolean flags.
1191
+ * @param consents - T&C2 flags mapped onto the session disclaimer catalog.
1140
1192
  * @param consents.sumsubTncSigned - Whether Sumsub T&C were accepted.
1141
1193
  * @param consents.idosTncSigned - Whether idOS T&C were accepted.
1194
+ * @param consents.credentialReusabilityConsentGiven - Whether credential
1195
+ * reuse was accepted.
1142
1196
  */
1143
1197
  async function _KycController_startConsentsSession(consents) {
1144
1198
  const { email, acceptedDisclaimerIds } = this.state;
@@ -1155,18 +1209,44 @@ async function _KycController_startConsentsSession(consents) {
1155
1209
  state.error = null;
1156
1210
  state.phase = 'session';
1157
1211
  state.statusMessage = 'Submitting consents...';
1212
+ state.sumsub.status = 'creatingSession';
1213
+ state.sumsub.result = null;
1214
+ state.sumsub.sessionStatus = null;
1158
1215
  // Consents-path vendors have no MoonPay session/access tokens.
1159
1216
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_clearMoonPaySession).call(this, state);
1160
1217
  });
1161
1218
  try {
1162
- await this.messenger.call('KycService:submitConsents', {
1219
+ await this.messenger.call('KycService:submitVendorDisclaimers', {
1220
+ vendor: this.state.activeVendor,
1163
1221
  disclaimerIds: acceptedDisclaimerIds,
1164
- sumsubTncSigned: consents.sumsubTncSigned,
1165
- idosTncSigned: consents.idosTncSigned,
1166
1222
  });
1167
1223
  if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1168
1224
  return;
1169
1225
  }
1226
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1227
+ state.statusMessage = 'Creating session...';
1228
+ });
1229
+ const created = await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_createUkycSession).call(this, generation);
1230
+ if (!created || __classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1231
+ return;
1232
+ }
1233
+ await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_recordSessionDisclaimers).call(this, created.sessionId, consents, generation);
1234
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1235
+ return;
1236
+ }
1237
+ if (created.vendorProcessing) {
1238
+ try {
1239
+ await this.refreshKycStatus();
1240
+ }
1241
+ catch (statusError) {
1242
+ console.error('KYC status refresh failed:', statusError);
1243
+ }
1244
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1245
+ state.phase = 'done';
1246
+ state.statusMessage = VENDOR_PROCESSING_MESSAGE;
1247
+ });
1248
+ return;
1249
+ }
1170
1250
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
1171
1251
  state.phase = 'submit';
1172
1252
  state.statusMessage = 'Starting document verification...';
@@ -1205,6 +1285,24 @@ async function _KycController_startConsentsSession(consents) {
1205
1285
  });
1206
1286
  }
1207
1287
  catch (error) {
1288
+ if (isSessionAlreadyCompletedError(error)) {
1289
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1290
+ return;
1291
+ }
1292
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUserStatus).call(this, {
1293
+ status: 'completed',
1294
+ sumsubSessionId: null,
1295
+ errorCode: null,
1296
+ });
1297
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1298
+ state.sumsub.status = 'complete';
1299
+ state.sumsub.result = { alreadyCompleted: true };
1300
+ state.statusMessage = 'KYC already completed.';
1301
+ state.phase = 'done';
1302
+ state.error = null;
1303
+ });
1304
+ return;
1305
+ }
1208
1306
  console.error('Consents session failed:', error);
1209
1307
  if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1210
1308
  return;
@@ -1212,6 +1310,10 @@ async function _KycController_startConsentsSession(consents) {
1212
1310
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
1213
1311
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_clearAcceptedTerms).call(this, state);
1214
1312
  state.activeProduct = null;
1313
+ state.sessionDisclaimers = null;
1314
+ // Session create ran before recording disclaimers. Drop the leftover
1315
+ // UKYC session so a later `startSumSub` cannot skip consent recording.
1316
+ state.sumsub = { ...getDefaultKycControllerState().sumsub };
1215
1317
  state.error = `Consents session failed: ${String(error)}`;
1216
1318
  state.statusMessage =
1217
1319
  'Consent / verification failed — accept the terms to try again.';
@@ -1219,6 +1321,74 @@ async function _KycController_startConsentsSession(consents) {
1219
1321
  });
1220
1322
  await this.loadDisclaimers();
1221
1323
  }
1324
+ }, _KycController_recordSessionDisclaimers =
1325
+ /**
1326
+ * Fetches the session-scoped disclaimer catalog and records consents
1327
+ * derived from the T&C2 flags. Already-consented catalog rows are omitted
1328
+ * from the POST. A 409 is re-checked with a GET: continue only when every
1329
+ * accepted document is now consented, otherwise fail closed.
1330
+ *
1331
+ * @param sessionId - The UKYC session id.
1332
+ * @param consents - T&C2 flags mapped onto catalog documents.
1333
+ * @param consents.sumsubTncSigned - Whether Sumsub T&C were accepted.
1334
+ * @param consents.idosTncSigned - Whether idOS T&C were accepted.
1335
+ * @param consents.credentialReusabilityConsentGiven - Whether credential
1336
+ * reuse was accepted.
1337
+ * @param generation - Flow generation captured by the caller.
1338
+ */
1339
+ async function _KycController_recordSessionDisclaimers(sessionId, consents, generation) {
1340
+ const catalog = await this.messenger.call('KycService:fetchSessionDisclaimers', { sessionId });
1341
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1342
+ return;
1343
+ }
1344
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
1345
+ state.sessionDisclaimers = catalog;
1346
+ state.statusMessage = 'Submitting consents...';
1347
+ });
1348
+ if (isAcceptedCategoryEmpty(catalog.idOS, consents.idosTncSigned) ||
1349
+ isAcceptedCategoryEmpty(catalog.kycProvider, consents.sumsubTncSigned)) {
1350
+ throw new Error('Session disclaimer catalog is missing documents for an accepted category.');
1351
+ }
1352
+ const idOS = consentRecordsFromCatalog(catalog.idOS, consents.idosTncSigned);
1353
+ const kycProvider = consentRecordsFromCatalog(catalog.kycProvider, consents.sumsubTncSigned);
1354
+ const reuseUnchanged = catalog.credentialReusabilityConsentGiven ===
1355
+ consents.credentialReusabilityConsentGiven;
1356
+ if (idOS.length === 0 && kycProvider.length === 0 && reuseUnchanged) {
1357
+ return;
1358
+ }
1359
+ try {
1360
+ const recorded = await this.messenger.call('KycService:submitSessionDisclaimers', {
1361
+ sessionId,
1362
+ idOS,
1363
+ kycProvider,
1364
+ credentialReusabilityConsentGiven: consents.credentialReusabilityConsentGiven,
1365
+ });
1366
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1367
+ state.sessionDisclaimers = recorded;
1368
+ });
1369
+ }
1370
+ catch (error) {
1371
+ if (!isConsentConflictError(error)) {
1372
+ throw error;
1373
+ }
1374
+ // 409 means some document version was already recorded. Re-fetch and
1375
+ // continue only when every document the user accepted is now consented;
1376
+ // otherwise fail closed so a version bump cannot skip new docs.
1377
+ const latest = await this.messenger.call('KycService:fetchSessionDisclaimers', { sessionId });
1378
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1379
+ return;
1380
+ }
1381
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
1382
+ state.sessionDisclaimers = latest;
1383
+ });
1384
+ const stillMissingIdos = acceptedCategoryStillMissing(latest.idOS, consents.idosTncSigned);
1385
+ const stillMissingProvider = acceptedCategoryStillMissing(latest.kycProvider, consents.sumsubTncSigned);
1386
+ const stillMissingReuse = consents.credentialReusabilityConsentGiven &&
1387
+ !latest.credentialReusabilityConsentGiven;
1388
+ if (stillMissingIdos || stillMissingProvider || stillMissingReuse) {
1389
+ throw error;
1390
+ }
1391
+ }
1222
1392
  }, _KycController_createSession =
1223
1393
  /**
1224
1394
  * Creates a vendor session from the currently stored terms + email.
@@ -1290,6 +1460,7 @@ async function _KycController_createSession() {
1290
1460
  state.termsAcceptedVendor = null;
1291
1461
  state.sumsubTncAccepted = null;
1292
1462
  state.idosTncAccepted = null;
1463
+ state.credentialReusabilityConsentGiven = null;
1293
1464
  }, _KycController_clearMoonPaySession = function _KycController_clearMoonPaySession(state) {
1294
1465
  state.moonpayCustomerId = null;
1295
1466
  state.sessionToken = null;
@@ -1416,6 +1587,71 @@ async function _KycController_continueAfterAuthentication() {
1416
1587
  };
1417
1588
  }
1418
1589
  return { vendor: this.state.activeVendor };
1590
+ }, _KycController_createUkycSession =
1591
+ /**
1592
+ * Creates a UKYC session, wraps the `data_encryption_key` and
1593
+ * `ukyc_capability_token` against the returned encryption schemas, and
1594
+ * submits both via authorizations. Stores `sumsub.sessionId`. Returns `null`
1595
+ * when a `reset()` superseded the flow.
1596
+ *
1597
+ * @param generation - Flow generation captured by the caller.
1598
+ * @returns The created session, or `null` if superseded.
1599
+ */
1600
+ async function _KycController_createUkycSession(generation) {
1601
+ const jwtToken = MOCK_JWT_TOKEN;
1602
+ // Establish a per-session X25519 keypair used to seal both secrets. The
1603
+ // private half stays on the device; each encryption schema from session
1604
+ // creation supplies the matching server public key.
1605
+ const sessionClientPrivateKey = ed25519_1.x25519.utils.randomSecretKey();
1606
+ const { sessionId, encryptionDataKey, ukycCapabilityToken: capabilityTokenSchema, } = await this.messenger.call('KycService:createUkycSession', {
1607
+ jwtToken,
1608
+ ...__classPrivateFieldGet(this, _KycController_instances, "m", _KycController_buildUkycSessionVendorFields).call(this),
1609
+ });
1610
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1611
+ return null;
1612
+ }
1613
+ // Verify each jwtChain against Fractal's JWKS, then confirm the returned
1614
+ // server public key matches the value attested inside the verified JWT
1615
+ // payload before trusting it for wrapping.
1616
+ const { keys } = await this.messenger.call('KycService:fetchJwks');
1617
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_assertAttestedServerPublicKey).call(this, keys, encryptionDataKey);
1618
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_assertAttestedServerPublicKey).call(this, keys, capabilityTokenSchema);
1619
+ // Derive the data_encryption_key from the local_user_secret, mint a
1620
+ // read-only capability token, and wrap both for the session server. Only
1621
+ // the wrapped (encrypted) material ever leaves the device.
1622
+ const localUserSecret = await (0, localUserSecret_js_1.getOrCreateLocalUserSecret)(__classPrivateFieldGet(this, _KycController_instances, "m", _KycController_localUserSecretStore).call(this));
1623
+ const clientMaterial = (0, deriveClientMaterial_js_1.deriveClientMaterial)(localUserSecret);
1624
+ const wrappedEncryptionDataKey = (0, wrapEncryptionKey_js_1.wrapEncryptionKey)(sessionClientPrivateKey, encryptionDataKey.serverPublicKey.x, clientMaterial.dataEncryptionKey);
1625
+ // Only the client holds the signing key derived from `local_user_secret`,
1626
+ // so only the client can mint the token; scoping it to `read` means it
1627
+ // authorizes later storage reads without granting write or delete access.
1628
+ const ukycCapabilityToken = (0, storageAccessToken_js_1.signStorageAccessToken)({
1629
+ material: clientMaterial,
1630
+ operations: ['read'],
1631
+ expiresAt: new Date(Date.now() + UKYC_CAPABILITY_TOKEN_TTL_MS),
1632
+ });
1633
+ const wrappedUkycCapabilityToken = (0, wrapEncryptionKey_js_1.wrapEncryptionKey)(sessionClientPrivateKey, capabilityTokenSchema.serverPublicKey.x, (0, utils_1.stringToBytes)((0, storageAccessToken_js_1.encodeStorageAccessTokenForHeader)(ukycCapabilityToken)));
1634
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1635
+ return null;
1636
+ }
1637
+ const { kycStatus, finalStatus } = await this.messenger.call('KycService:setAuthorizations', {
1638
+ sessionId,
1639
+ wrappedEncryptionDataKey,
1640
+ wrappedUkycCapabilityToken,
1641
+ });
1642
+ const vendorProcessing = kycStatus === KYC_STATUSES.approved &&
1643
+ finalStatus === KYC_STATUSES.pending;
1644
+ const stillCurrent = __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1645
+ state.sumsub.sessionId = sessionId;
1646
+ if (vendorProcessing) {
1647
+ state.sumsub.status = 'vendorProcessing';
1648
+ state.statusMessage = VENDOR_PROCESSING_MESSAGE;
1649
+ }
1650
+ });
1651
+ if (!stillCurrent) {
1652
+ return null;
1653
+ }
1654
+ return { sessionId, kycStatus, finalStatus, vendorProcessing };
1419
1655
  }, _KycController_fetchAndApplyUserStatus =
1420
1656
  /**
1421
1657
  * Fetches `GET /kyc/status` and applies it to state without managing the
@@ -1593,6 +1829,11 @@ async function _KycController_pollSessionStatusOnce(sessionId, token) {
1593
1829
  // @ts-expect-error Avoid "type instantiation is excessively deep".
1594
1830
  updater(state);
1595
1831
  });
1832
+ }, _KycController_assertAttestedServerPublicKey = function _KycController_assertAttestedServerPublicKey(keys, schema) {
1833
+ const jwtChainPayload = (0, jwtChain_js_1.verifyJwtChain)(keys, schema.jwtChain);
1834
+ if (jwtChainPayload.sessionServerPublicKeyX !== schema.serverPublicKey.x) {
1835
+ throw new Error('sessionServerPublicKey does not match the verified jwtChain payload (sessionServerPublicKeyX).');
1836
+ }
1596
1837
  }, _KycController_fail = function _KycController_fail(message) {
1597
1838
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
1598
1839
  state.error = message;