@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
@@ -9,15 +9,15 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
9
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
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
- 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;
12
+ 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;
13
13
  import { BaseController } from "@metamask/base-controller";
14
+ import { stringToBytes } from "@metamask/utils";
14
15
  import { x25519 } from "@noble/curves/ed25519";
15
16
  import { decryptCredentials, generateKeyPair } from "./crypto.mjs";
16
- import { toBase64Url } from "./encoding.mjs";
17
17
  import { deriveClientMaterial } from "./ukyc/deriveClientMaterial.mjs";
18
18
  import { verifyJwtChain } from "./ukyc/jwtChain.mjs";
19
19
  import { getOrCreateLocalUserSecret } from "./ukyc/localUserSecret.mjs";
20
- import { signStorageAccessToken } from "./ukyc/storageAccessToken.mjs";
20
+ import { encodeStorageAccessTokenForHeader, signStorageAccessToken } from "./ukyc/storageAccessToken.mjs";
21
21
  import { wrapEncryptionKey } from "./ukyc/wrapEncryptionKey.mjs";
22
22
  // === GENERAL ===
23
23
  export const controllerName = 'KycController';
@@ -145,6 +145,12 @@ const kycControllerMetadata = {
145
145
  persist: true,
146
146
  usedInUi: false,
147
147
  },
148
+ credentialReusabilityConsentGiven: {
149
+ includeInDebugSnapshot: true,
150
+ includeInStateLogs: true,
151
+ persist: false,
152
+ usedInUi: false,
153
+ },
148
154
  disclaimers: {
149
155
  includeInDebugSnapshot: false,
150
156
  includeInStateLogs: false,
@@ -157,6 +163,12 @@ const kycControllerMetadata = {
157
163
  persist: false,
158
164
  usedInUi: true,
159
165
  },
166
+ sessionDisclaimers: {
167
+ includeInDebugSnapshot: false,
168
+ includeInStateLogs: false,
169
+ persist: false,
170
+ usedInUi: true,
171
+ },
160
172
  geoCountry: {
161
173
  includeInDebugSnapshot: true,
162
174
  includeInStateLogs: true,
@@ -246,8 +258,10 @@ export function getDefaultKycControllerState() {
246
258
  termsAcceptedVendor: null,
247
259
  sumsubTncAccepted: null,
248
260
  idosTncAccepted: null,
261
+ credentialReusabilityConsentGiven: null,
249
262
  disclaimers: [],
250
263
  disclaimersError: null,
264
+ sessionDisclaimers: null,
251
265
  geoCountry: null,
252
266
  sessionToken: null,
253
267
  accessToken: null,
@@ -280,6 +294,60 @@ export function getDefaultKycControllerState() {
280
294
  function isSessionAlreadyCompletedError(error) {
281
295
  return String(error).includes(SESSION_NOT_IN_VALID_STATE);
282
296
  }
297
+ /**
298
+ * Whether recording session disclaimers failed because those document
299
+ * versions were already consented for the session (`409 Conflict`).
300
+ *
301
+ * @param error - The caught error.
302
+ * @returns `true` when the error is an HTTP 409.
303
+ */
304
+ function isConsentConflictError(error) {
305
+ return (typeof error === 'object' &&
306
+ error !== null &&
307
+ typeof error.httpStatus === 'number' &&
308
+ error.httpStatus === 409);
309
+ }
310
+ /**
311
+ * Maps a session-disclaimer catalog into the `{ key, version }` records the
312
+ * record-consents API expects, or an empty list when the user declined that
313
+ * category.
314
+ *
315
+ * @param documents - Catalog documents for one consent category.
316
+ * @param accepted - Whether the user accepted that category.
317
+ * @returns Consent records, or `[]` when not accepted.
318
+ */
319
+ function consentRecordsFromCatalog(documents, accepted) {
320
+ if (!accepted) {
321
+ return [];
322
+ }
323
+ return documents
324
+ .filter((document) => !document.consented)
325
+ .map(({ key, version }) => ({ key, version }));
326
+ }
327
+ /**
328
+ * Whether an accepted T&C2 category has no catalog documents. An empty list
329
+ * would otherwise skip the POST and count as success.
330
+ *
331
+ * @param documents - Catalog documents for one consent category.
332
+ * @param accepted - Whether the user accepted that category.
333
+ * @returns `true` when the user accepted and the catalog is empty.
334
+ */
335
+ function isAcceptedCategoryEmpty(documents, accepted) {
336
+ return accepted && documents.length === 0;
337
+ }
338
+ /**
339
+ * Whether an accepted category is still missing consent after a 409 re-GET:
340
+ * empty catalog or any document still unconsented.
341
+ *
342
+ * @param documents - Latest catalog documents for one consent category.
343
+ * @param accepted - Whether the user accepted that category.
344
+ * @returns `true` when accepted documents are not fully consented.
345
+ */
346
+ function acceptedCategoryStillMissing(documents, accepted) {
347
+ return (accepted &&
348
+ (documents.length === 0 ||
349
+ documents.some((document) => !document.consented)));
350
+ }
283
351
  /**
284
352
  * Vendors other than MoonPay skip Check/Auth frames and use the empty-shell
285
353
  * customer + consents path instead.
@@ -491,6 +559,7 @@ export class KycController extends BaseController {
491
559
  await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_startConsentsSession).call(this, {
492
560
  sumsubTncSigned,
493
561
  idosTncSigned,
562
+ credentialReusabilityConsentGiven: this.state.credentialReusabilityConsentGiven ?? false,
494
563
  });
495
564
  }
496
565
  else {
@@ -597,6 +666,9 @@ export class KycController extends BaseController {
597
666
  * Required for every vendor so callers explicitly declare acceptance.
598
667
  * @param params.idosTncSigned - Whether idOS T&C were accepted (T&C2).
599
668
  * Required for every vendor so callers explicitly declare acceptance.
669
+ * @param params.credentialReusabilityConsentGiven - Whether the customer
670
+ * consented to reuse existing idOS credentials. Used when recording
671
+ * session-scoped disclaimers on the consents path. Defaults to `false`.
600
672
  */
601
673
  async acceptTermsAndStartSession(params) {
602
674
  const sumsubTncSigned = params?.sumsubTncSigned;
@@ -606,6 +678,7 @@ export class KycController extends BaseController {
606
678
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_fail).call(this, 'Missing T&C2 acceptance flags.');
607
679
  return;
608
680
  }
681
+ const credentialReusabilityConsentGiven = params.credentialReusabilityConsentGiven ?? false;
609
682
  const termsAcceptedAt = new Date().toISOString();
610
683
  const disclaimerIds = this.state.disclaimers.map((disclaimer) => disclaimer.id);
611
684
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
@@ -620,9 +693,15 @@ export class KycController extends BaseController {
620
693
  state.termsAcceptedVendor = state.activeVendor;
621
694
  state.sumsubTncAccepted = sumsubTncSigned;
622
695
  state.idosTncAccepted = idosTncSigned;
696
+ state.credentialReusabilityConsentGiven =
697
+ credentialReusabilityConsentGiven;
623
698
  });
624
699
  if (usesConsentsFlow(this.state.activeVendor)) {
625
- await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_startConsentsSession).call(this, { sumsubTncSigned, idosTncSigned });
700
+ await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_startConsentsSession).call(this, {
701
+ sumsubTncSigned,
702
+ idosTncSigned,
703
+ credentialReusabilityConsentGiven,
704
+ });
626
705
  return;
627
706
  }
628
707
  await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_createSession).call(this);
@@ -836,17 +915,20 @@ export class KycController extends BaseController {
836
915
  /**
837
916
  * Runs the SumSub document-verification sub-flow end to end:
838
917
  *
839
- * 1. requests a per-session wrapping key from the UKYC backend;
840
- * 2. verifies its `jwtChain` against the Fractal JWKS and confirms the
841
- * attested session server public key;
918
+ * 1. creates a UKYC session, receiving per-secret encryption schemas;
919
+ * 2. verifies each schema's `jwtChain` against the Fractal JWKS and confirms
920
+ * the attested session server public key;
842
921
  * 3. derives the `data_encryption_key` from the wallet's UKYC
843
922
  * `local_user_secret` and wraps it for the session server;
844
- * 4. mints a client-signed, read-only `ukyc_capability_token` and creates
845
- * the UKYC session (handing over the wrapped key and the token);
923
+ * 4. mints a client-signed, read-only `ukyc_capability_token`, wraps it the
924
+ * same way as the encryption key, and submits both via authorizations;
846
925
  * 5. fetches the SumSub applicant access token; and
847
926
  * 6. presents the SDK via the injected launcher.
848
927
  *
849
- * If session creation reports the applicant is already approved on the relay
928
+ * If a UKYC session already exists (the consents path creates it before
929
+ * recording session disclaimers), steps 1–4 are skipped.
930
+ *
931
+ * If authorizations report the applicant is already approved on the relay
850
932
  * while the vendor is still finalizing (`kycStatus: approved`,
851
933
  * `finalStatus: pending`), the sub-flow stops at step 4 with a
852
934
  * `vendorProcessing` status and a message rather than launching the SDK.
@@ -872,65 +954,32 @@ export class KycController extends BaseController {
872
954
  // presenting the SDK) on a controller that is now idle.
873
955
  const generation = __classPrivateFieldGet(this, _KycController_generation, "f");
874
956
  try {
875
- __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
876
- state.sumsub.status = 'creatingSession';
877
- state.sumsub.result = null;
878
- state.sumsub.sessionStatus = null;
879
- });
880
- const jwtToken = MOCK_JWT_TOKEN;
881
- // Establish a per-session X25519 keypair and exchange our public half for
882
- // the server's wrapping key. The private half stays on the device and is
883
- // used to derive the shared secret that seals the data_encryption_key.
884
- const sessionClientPrivateKey = x25519.utils.randomSecretKey();
885
- const sessionClientPublicKey = x25519.getPublicKey(sessionClientPrivateKey);
886
- const wrappingKey = await this.messenger.call('KycService:getWrappingKey', { sessionClientPublicKey: toBase64Url(sessionClientPublicKey) });
887
- // Verify the jwtChain against Fractal's JWKS, then confirm the returned
888
- // sessionServerPublicKey matches the value attested inside the verified
889
- // JWT payload before trusting it for key wrapping.
890
- const { keys } = await this.messenger.call('KycService:fetchJwks');
891
- const jwtChainPayload = verifyJwtChain(keys, wrappingKey.jwtChain);
892
- if (jwtChainPayload.sessionServerPublicKeyX !==
893
- wrappingKey.sessionServerPublicKey.x) {
894
- throw new Error('sessionServerPublicKey does not match the verified jwtChain payload (sessionServerPublicKeyX).');
895
- }
896
- // Derive the data_encryption_key from the local_user_secret and wrap it
897
- // for the session server. Only the wrapped (encrypted) key ever leaves
898
- // the device.
899
- const localUserSecret = await getOrCreateLocalUserSecret(__classPrivateFieldGet(this, _KycController_instances, "m", _KycController_localUserSecretStore).call(this));
900
- const clientMaterial = deriveClientMaterial(localUserSecret);
901
- const wrappedEncryptionKey = {
902
- sessionId: wrappingKey.id,
903
- ...wrapEncryptionKey(sessionClientPrivateKey, wrappingKey.sessionServerPublicKey.x, clientMaterial.dataEncryptionKey),
904
- };
905
- // Mint a read-only `ukyc_capability_token` for the session. Only the
906
- // client holds the signing key derived from `local_user_secret`, so only
907
- // the client can mint it; scoping it to `read` means it authorizes later
908
- // storage reads without granting write or delete access.
909
- const ukycCapabilityToken = signStorageAccessToken({
910
- material: clientMaterial,
911
- operations: ['read'],
912
- expiresAt: new Date(Date.now() + UKYC_CAPABILITY_TOKEN_TTL_MS),
913
- });
914
- const { sessionId, kycStatus, finalStatus } = await this.messenger.call('KycService:createUkycSession', {
915
- jwtToken,
916
- ...__classPrivateFieldGet(this, _KycController_instances, "m", _KycController_buildUkycSessionVendorFields).call(this),
917
- wrappedEncryptionKey,
918
- ukycCapabilityToken,
919
- });
920
- // A user who already finished the journey can return to a session the
921
- // relay has already approved (`kycStatus`) while the vendor is still
922
- // finalizing its own decision (`finalStatus`). There is nothing left to
923
- // verify, so stop here and surface a message rather than launching the
924
- // SDK again.
925
- if (kycStatus === KYC_STATUSES.approved &&
926
- finalStatus === KYC_STATUSES.pending) {
927
- const stillCurrent = __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
928
- state.sumsub.status = 'vendorProcessing';
929
- state.sumsub.sessionId = sessionId;
930
- state.statusMessage = VENDOR_PROCESSING_MESSAGE;
957
+ if (!this.state.sumsub.sessionId) {
958
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
959
+ state.sumsub.status = 'creatingSession';
960
+ state.sumsub.result = null;
961
+ state.sumsub.sessionStatus = null;
931
962
  });
932
- return stillCurrent ? { kycStatus, finalStatus } : {};
963
+ const created = await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_createUkycSession).call(this, generation);
964
+ if (!created) {
965
+ return {};
966
+ }
967
+ // A user who already finished the journey can return to a session the
968
+ // relay has already approved (`kycStatus`) while the vendor is still
969
+ // finalizing its own decision (`finalStatus`). There is nothing left to
970
+ // verify, so stop here and surface a message rather than launching the
971
+ // SDK again.
972
+ if (created.vendorProcessing) {
973
+ return {
974
+ kycStatus: created.kycStatus,
975
+ finalStatus: created.finalStatus,
976
+ };
977
+ }
933
978
  }
979
+ // Empty string is a valid "no id to poll" session id used by tests and
980
+ // must not be coalesced away as missing.
981
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
982
+ const sessionId = this.state.sumsub.sessionId || '';
934
983
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
935
984
  state.sumsub.status = 'fetchingToken';
936
985
  state.sumsub.sessionId = sessionId;
@@ -1092,6 +1141,8 @@ export class KycController extends BaseController {
1092
1141
  state.error = null;
1093
1142
  state.disclaimers = [];
1094
1143
  state.disclaimersError = null;
1144
+ state.sessionDisclaimers = null;
1145
+ state.credentialReusabilityConsentGiven = null;
1095
1146
  state.sessionToken = null;
1096
1147
  state.accessToken = null;
1097
1148
  state.moonpayCustomerId = null;
@@ -1128,12 +1179,15 @@ _KycController_sumsubLauncher = new WeakMap(), _KycController_keypair = new Weak
1128
1179
  };
1129
1180
  }, _KycController_startConsentsSession =
1130
1181
  /**
1131
- * Consents-path vendors: post vendor signings + Sumsub/idOS ack, then
1132
- * launch SumSub — skipping MoonPay Check/Auth frames.
1182
+ * Consents-path vendors: record vendor T&Cs, create a UKYC session,
1183
+ * record session-scoped idOS / KYC-provider disclaimers, then launch
1184
+ * SumSub — skipping MoonPay Check/Auth frames.
1133
1185
  *
1134
- * @param consents - T&C2 boolean flags.
1186
+ * @param consents - T&C2 flags mapped onto the session disclaimer catalog.
1135
1187
  * @param consents.sumsubTncSigned - Whether Sumsub T&C were accepted.
1136
1188
  * @param consents.idosTncSigned - Whether idOS T&C were accepted.
1189
+ * @param consents.credentialReusabilityConsentGiven - Whether credential
1190
+ * reuse was accepted.
1137
1191
  */
1138
1192
  async function _KycController_startConsentsSession(consents) {
1139
1193
  const { email, acceptedDisclaimerIds } = this.state;
@@ -1150,18 +1204,44 @@ async function _KycController_startConsentsSession(consents) {
1150
1204
  state.error = null;
1151
1205
  state.phase = 'session';
1152
1206
  state.statusMessage = 'Submitting consents...';
1207
+ state.sumsub.status = 'creatingSession';
1208
+ state.sumsub.result = null;
1209
+ state.sumsub.sessionStatus = null;
1153
1210
  // Consents-path vendors have no MoonPay session/access tokens.
1154
1211
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_clearMoonPaySession).call(this, state);
1155
1212
  });
1156
1213
  try {
1157
- await this.messenger.call('KycService:submitConsents', {
1214
+ await this.messenger.call('KycService:submitVendorDisclaimers', {
1215
+ vendor: this.state.activeVendor,
1158
1216
  disclaimerIds: acceptedDisclaimerIds,
1159
- sumsubTncSigned: consents.sumsubTncSigned,
1160
- idosTncSigned: consents.idosTncSigned,
1161
1217
  });
1162
1218
  if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1163
1219
  return;
1164
1220
  }
1221
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1222
+ state.statusMessage = 'Creating session...';
1223
+ });
1224
+ const created = await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_createUkycSession).call(this, generation);
1225
+ if (!created || __classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1226
+ return;
1227
+ }
1228
+ await __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_recordSessionDisclaimers).call(this, created.sessionId, consents, generation);
1229
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1230
+ return;
1231
+ }
1232
+ if (created.vendorProcessing) {
1233
+ try {
1234
+ await this.refreshKycStatus();
1235
+ }
1236
+ catch (statusError) {
1237
+ console.error('KYC status refresh failed:', statusError);
1238
+ }
1239
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1240
+ state.phase = 'done';
1241
+ state.statusMessage = VENDOR_PROCESSING_MESSAGE;
1242
+ });
1243
+ return;
1244
+ }
1165
1245
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
1166
1246
  state.phase = 'submit';
1167
1247
  state.statusMessage = 'Starting document verification...';
@@ -1200,6 +1280,24 @@ async function _KycController_startConsentsSession(consents) {
1200
1280
  });
1201
1281
  }
1202
1282
  catch (error) {
1283
+ if (isSessionAlreadyCompletedError(error)) {
1284
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1285
+ return;
1286
+ }
1287
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUserStatus).call(this, {
1288
+ status: 'completed',
1289
+ sumsubSessionId: null,
1290
+ errorCode: null,
1291
+ });
1292
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1293
+ state.sumsub.status = 'complete';
1294
+ state.sumsub.result = { alreadyCompleted: true };
1295
+ state.statusMessage = 'KYC already completed.';
1296
+ state.phase = 'done';
1297
+ state.error = null;
1298
+ });
1299
+ return;
1300
+ }
1203
1301
  console.error('Consents session failed:', error);
1204
1302
  if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1205
1303
  return;
@@ -1207,6 +1305,10 @@ async function _KycController_startConsentsSession(consents) {
1207
1305
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
1208
1306
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_clearAcceptedTerms).call(this, state);
1209
1307
  state.activeProduct = null;
1308
+ state.sessionDisclaimers = null;
1309
+ // Session create ran before recording disclaimers. Drop the leftover
1310
+ // UKYC session so a later `startSumSub` cannot skip consent recording.
1311
+ state.sumsub = { ...getDefaultKycControllerState().sumsub };
1210
1312
  state.error = `Consents session failed: ${String(error)}`;
1211
1313
  state.statusMessage =
1212
1314
  'Consent / verification failed — accept the terms to try again.';
@@ -1214,6 +1316,74 @@ async function _KycController_startConsentsSession(consents) {
1214
1316
  });
1215
1317
  await this.loadDisclaimers();
1216
1318
  }
1319
+ }, _KycController_recordSessionDisclaimers =
1320
+ /**
1321
+ * Fetches the session-scoped disclaimer catalog and records consents
1322
+ * derived from the T&C2 flags. Already-consented catalog rows are omitted
1323
+ * from the POST. A 409 is re-checked with a GET: continue only when every
1324
+ * accepted document is now consented, otherwise fail closed.
1325
+ *
1326
+ * @param sessionId - The UKYC session id.
1327
+ * @param consents - T&C2 flags mapped onto catalog documents.
1328
+ * @param consents.sumsubTncSigned - Whether Sumsub T&C were accepted.
1329
+ * @param consents.idosTncSigned - Whether idOS T&C were accepted.
1330
+ * @param consents.credentialReusabilityConsentGiven - Whether credential
1331
+ * reuse was accepted.
1332
+ * @param generation - Flow generation captured by the caller.
1333
+ */
1334
+ async function _KycController_recordSessionDisclaimers(sessionId, consents, generation) {
1335
+ const catalog = await this.messenger.call('KycService:fetchSessionDisclaimers', { sessionId });
1336
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1337
+ return;
1338
+ }
1339
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
1340
+ state.sessionDisclaimers = catalog;
1341
+ state.statusMessage = 'Submitting consents...';
1342
+ });
1343
+ if (isAcceptedCategoryEmpty(catalog.idOS, consents.idosTncSigned) ||
1344
+ isAcceptedCategoryEmpty(catalog.kycProvider, consents.sumsubTncSigned)) {
1345
+ throw new Error('Session disclaimer catalog is missing documents for an accepted category.');
1346
+ }
1347
+ const idOS = consentRecordsFromCatalog(catalog.idOS, consents.idosTncSigned);
1348
+ const kycProvider = consentRecordsFromCatalog(catalog.kycProvider, consents.sumsubTncSigned);
1349
+ const reuseUnchanged = catalog.credentialReusabilityConsentGiven ===
1350
+ consents.credentialReusabilityConsentGiven;
1351
+ if (idOS.length === 0 && kycProvider.length === 0 && reuseUnchanged) {
1352
+ return;
1353
+ }
1354
+ try {
1355
+ const recorded = await this.messenger.call('KycService:submitSessionDisclaimers', {
1356
+ sessionId,
1357
+ idOS,
1358
+ kycProvider,
1359
+ credentialReusabilityConsentGiven: consents.credentialReusabilityConsentGiven,
1360
+ });
1361
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1362
+ state.sessionDisclaimers = recorded;
1363
+ });
1364
+ }
1365
+ catch (error) {
1366
+ if (!isConsentConflictError(error)) {
1367
+ throw error;
1368
+ }
1369
+ // 409 means some document version was already recorded. Re-fetch and
1370
+ // continue only when every document the user accepted is now consented;
1371
+ // otherwise fail closed so a version bump cannot skip new docs.
1372
+ const latest = await this.messenger.call('KycService:fetchSessionDisclaimers', { sessionId });
1373
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1374
+ return;
1375
+ }
1376
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
1377
+ state.sessionDisclaimers = latest;
1378
+ });
1379
+ const stillMissingIdos = acceptedCategoryStillMissing(latest.idOS, consents.idosTncSigned);
1380
+ const stillMissingProvider = acceptedCategoryStillMissing(latest.kycProvider, consents.sumsubTncSigned);
1381
+ const stillMissingReuse = consents.credentialReusabilityConsentGiven &&
1382
+ !latest.credentialReusabilityConsentGiven;
1383
+ if (stillMissingIdos || stillMissingProvider || stillMissingReuse) {
1384
+ throw error;
1385
+ }
1386
+ }
1217
1387
  }, _KycController_createSession =
1218
1388
  /**
1219
1389
  * Creates a vendor session from the currently stored terms + email.
@@ -1285,6 +1455,7 @@ async function _KycController_createSession() {
1285
1455
  state.termsAcceptedVendor = null;
1286
1456
  state.sumsubTncAccepted = null;
1287
1457
  state.idosTncAccepted = null;
1458
+ state.credentialReusabilityConsentGiven = null;
1288
1459
  }, _KycController_clearMoonPaySession = function _KycController_clearMoonPaySession(state) {
1289
1460
  state.moonpayCustomerId = null;
1290
1461
  state.sessionToken = null;
@@ -1411,6 +1582,71 @@ async function _KycController_continueAfterAuthentication() {
1411
1582
  };
1412
1583
  }
1413
1584
  return { vendor: this.state.activeVendor };
1585
+ }, _KycController_createUkycSession =
1586
+ /**
1587
+ * Creates a UKYC session, wraps the `data_encryption_key` and
1588
+ * `ukyc_capability_token` against the returned encryption schemas, and
1589
+ * submits both via authorizations. Stores `sumsub.sessionId`. Returns `null`
1590
+ * when a `reset()` superseded the flow.
1591
+ *
1592
+ * @param generation - Flow generation captured by the caller.
1593
+ * @returns The created session, or `null` if superseded.
1594
+ */
1595
+ async function _KycController_createUkycSession(generation) {
1596
+ const jwtToken = MOCK_JWT_TOKEN;
1597
+ // Establish a per-session X25519 keypair used to seal both secrets. The
1598
+ // private half stays on the device; each encryption schema from session
1599
+ // creation supplies the matching server public key.
1600
+ const sessionClientPrivateKey = x25519.utils.randomSecretKey();
1601
+ const { sessionId, encryptionDataKey, ukycCapabilityToken: capabilityTokenSchema, } = await this.messenger.call('KycService:createUkycSession', {
1602
+ jwtToken,
1603
+ ...__classPrivateFieldGet(this, _KycController_instances, "m", _KycController_buildUkycSessionVendorFields).call(this),
1604
+ });
1605
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1606
+ return null;
1607
+ }
1608
+ // Verify each jwtChain against Fractal's JWKS, then confirm the returned
1609
+ // server public key matches the value attested inside the verified JWT
1610
+ // payload before trusting it for wrapping.
1611
+ const { keys } = await this.messenger.call('KycService:fetchJwks');
1612
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_assertAttestedServerPublicKey).call(this, keys, encryptionDataKey);
1613
+ __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_assertAttestedServerPublicKey).call(this, keys, capabilityTokenSchema);
1614
+ // Derive the data_encryption_key from the local_user_secret, mint a
1615
+ // read-only capability token, and wrap both for the session server. Only
1616
+ // the wrapped (encrypted) material ever leaves the device.
1617
+ const localUserSecret = await getOrCreateLocalUserSecret(__classPrivateFieldGet(this, _KycController_instances, "m", _KycController_localUserSecretStore).call(this));
1618
+ const clientMaterial = deriveClientMaterial(localUserSecret);
1619
+ const wrappedEncryptionDataKey = wrapEncryptionKey(sessionClientPrivateKey, encryptionDataKey.serverPublicKey.x, clientMaterial.dataEncryptionKey);
1620
+ // Only the client holds the signing key derived from `local_user_secret`,
1621
+ // so only the client can mint the token; scoping it to `read` means it
1622
+ // authorizes later storage reads without granting write or delete access.
1623
+ const ukycCapabilityToken = signStorageAccessToken({
1624
+ material: clientMaterial,
1625
+ operations: ['read'],
1626
+ expiresAt: new Date(Date.now() + UKYC_CAPABILITY_TOKEN_TTL_MS),
1627
+ });
1628
+ const wrappedUkycCapabilityToken = wrapEncryptionKey(sessionClientPrivateKey, capabilityTokenSchema.serverPublicKey.x, stringToBytes(encodeStorageAccessTokenForHeader(ukycCapabilityToken)));
1629
+ if (__classPrivateFieldGet(this, _KycController_generation, "f") !== generation) {
1630
+ return null;
1631
+ }
1632
+ const { kycStatus, finalStatus } = await this.messenger.call('KycService:setAuthorizations', {
1633
+ sessionId,
1634
+ wrappedEncryptionDataKey,
1635
+ wrappedUkycCapabilityToken,
1636
+ });
1637
+ const vendorProcessing = kycStatus === KYC_STATUSES.approved &&
1638
+ finalStatus === KYC_STATUSES.pending;
1639
+ const stillCurrent = __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_updateIfCurrent).call(this, generation, (state) => {
1640
+ state.sumsub.sessionId = sessionId;
1641
+ if (vendorProcessing) {
1642
+ state.sumsub.status = 'vendorProcessing';
1643
+ state.statusMessage = VENDOR_PROCESSING_MESSAGE;
1644
+ }
1645
+ });
1646
+ if (!stillCurrent) {
1647
+ return null;
1648
+ }
1649
+ return { sessionId, kycStatus, finalStatus, vendorProcessing };
1414
1650
  }, _KycController_fetchAndApplyUserStatus =
1415
1651
  /**
1416
1652
  * Fetches `GET /kyc/status` and applies it to state without managing the
@@ -1588,6 +1824,11 @@ async function _KycController_pollSessionStatusOnce(sessionId, token) {
1588
1824
  // @ts-expect-error Avoid "type instantiation is excessively deep".
1589
1825
  updater(state);
1590
1826
  });
1827
+ }, _KycController_assertAttestedServerPublicKey = function _KycController_assertAttestedServerPublicKey(keys, schema) {
1828
+ const jwtChainPayload = verifyJwtChain(keys, schema.jwtChain);
1829
+ if (jwtChainPayload.sessionServerPublicKeyX !== schema.serverPublicKey.x) {
1830
+ throw new Error('sessionServerPublicKey does not match the verified jwtChain payload (sessionServerPublicKeyX).');
1831
+ }
1591
1832
  }, _KycController_fail = function _KycController_fail(message) {
1592
1833
  __classPrivateFieldGet(this, _KycController_instances, "m", _KycController_applyUpdate).call(this, (state) => {
1593
1834
  state.error = message;