@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.
- package/CHANGELOG.md +7 -1
- package/dist/KycController-method-action-types.cjs.map +1 -1
- package/dist/KycController-method-action-types.d.cts +12 -6
- package/dist/KycController-method-action-types.d.cts.map +1 -1
- package/dist/KycController-method-action-types.d.mts +12 -6
- package/dist/KycController-method-action-types.d.mts.map +1 -1
- package/dist/KycController-method-action-types.mjs.map +1 -1
- package/dist/KycController.cjs +313 -72
- package/dist/KycController.cjs.map +1 -1
- package/dist/KycController.d.cts +27 -7
- package/dist/KycController.d.cts.map +1 -1
- package/dist/KycController.d.mts +27 -7
- package/dist/KycController.d.mts.map +1 -1
- package/dist/KycController.mjs +314 -73
- package/dist/KycController.mjs.map +1 -1
- package/dist/KycService-method-action-types.cjs.map +1 -1
- package/dist/KycService-method-action-types.d.cts +60 -31
- package/dist/KycService-method-action-types.d.cts.map +1 -1
- package/dist/KycService-method-action-types.d.mts +60 -31
- package/dist/KycService-method-action-types.d.mts.map +1 -1
- package/dist/KycService-method-action-types.mjs.map +1 -1
- package/dist/KycService.cjs +140 -75
- package/dist/KycService.cjs.map +1 -1
- package/dist/KycService.d.cts +181 -74
- package/dist/KycService.d.cts.map +1 -1
- package/dist/KycService.d.mts +181 -74
- package/dist/KycService.d.mts.map +1 -1
- package/dist/KycService.mjs +140 -75
- package/dist/KycService.mjs.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +49 -4
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.mts +49 -4
- package/dist/types.d.mts.map +1 -1
- package/dist/types.mjs.map +1 -1
- package/dist/ukyc/jwtChain.cjs +1 -1
- package/dist/ukyc/jwtChain.cjs.map +1 -1
- package/dist/ukyc/jwtChain.d.cts +2 -2
- package/dist/ukyc/jwtChain.d.mts +2 -2
- package/dist/ukyc/jwtChain.mjs +1 -1
- package/dist/ukyc/jwtChain.mjs.map +1 -1
- package/dist/ukyc/wrapEncryptionKey.cjs +16 -7
- package/dist/ukyc/wrapEncryptionKey.cjs.map +1 -1
- package/dist/ukyc/wrapEncryptionKey.d.cts +23 -16
- package/dist/ukyc/wrapEncryptionKey.d.cts.map +1 -1
- package/dist/ukyc/wrapEncryptionKey.d.mts +23 -16
- package/dist/ukyc/wrapEncryptionKey.d.mts.map +1 -1
- package/dist/ukyc/wrapEncryptionKey.mjs +16 -7
- package/dist/ukyc/wrapEncryptionKey.mjs.map +1 -1
- package/package.json +1 -1
package/dist/KycController.mjs
CHANGED
|
@@ -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, {
|
|
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.
|
|
840
|
-
* 2. verifies
|
|
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
|
|
845
|
-
*
|
|
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
|
|
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
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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
|
-
|
|
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:
|
|
1132
|
-
*
|
|
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
|
|
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:
|
|
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;
|