decane-connect-kit 2.12.1 → 2.14.0

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/dist/index.d.cts CHANGED
@@ -506,11 +506,13 @@ interface DecaneConnect {
506
506
  off(event: SdkEvent, handler: (...args: unknown[]) => void): void;
507
507
  }
508
508
  interface WrappedShareMeta {
509
- unlockMethod: 'passkey' | 'pin';
509
+ unlockMethod: 'passkey' | 'pin' | 'password';
510
510
  credentialId?: string;
511
511
  prfSalt?: string;
512
512
  pinSalt?: string;
513
513
  pinIterations?: number;
514
+ passwordSalt?: string;
515
+ passwordIterations?: number;
514
516
  }
515
517
  /**
516
518
  * Who the user is, as the identity provider just described them.
package/dist/index.d.ts CHANGED
@@ -506,11 +506,13 @@ interface DecaneConnect {
506
506
  off(event: SdkEvent, handler: (...args: unknown[]) => void): void;
507
507
  }
508
508
  interface WrappedShareMeta {
509
- unlockMethod: 'passkey' | 'pin';
509
+ unlockMethod: 'passkey' | 'pin' | 'password';
510
510
  credentialId?: string;
511
511
  prfSalt?: string;
512
512
  pinSalt?: string;
513
513
  pinIterations?: number;
514
+ passwordSalt?: string;
515
+ passwordIterations?: number;
514
516
  }
515
517
  /**
516
518
  * Who the user is, as the identity provider just described them.
package/dist/index.js CHANGED
@@ -2886,9 +2886,6 @@ async function deriveKeyFromPin(pin, salt) {
2886
2886
  ["encrypt", "decrypt"]
2887
2887
  );
2888
2888
  }
2889
- function validatePin(pin) {
2890
- if (!/^\d{4,8}$/.test(pin)) throw new Error("PIN must be 4\u20138 digits");
2891
- }
2892
2889
 
2893
2890
  // src/social/crypto/passkey.ts
2894
2891
  init_errors();
@@ -3702,21 +3699,38 @@ async function createDecaneConnect(config) {
3702
3699
  }
3703
3700
  }
3704
3701
  events.on("session-expired", () => dropLiveSession());
3705
- async function pinSetup() {
3706
- if (!config.promptPin) throw new PromptPinNotConfiguredError();
3707
- const pin = await config.promptPin();
3708
- validatePin(pin);
3709
- const pinSalt = generateRandomBytes(32);
3710
- const wrappingKey = await deriveKeyFromPin(pin, pinSalt);
3702
+ async function passwordSetup() {
3703
+ if (!config.promptUnlockPassword) throw new PromptPasswordNotConfiguredError();
3704
+ const password = await config.promptUnlockPassword({ kind: "set" });
3705
+ validateUnlockPassword(password);
3706
+ const passwordSalt = generateRandomBytes(32);
3707
+ const { shareKey, tokenKey } = await derivePasswordKeys(password, passwordSalt);
3711
3708
  return {
3712
- wrappingKey,
3709
+ wrappingKey: shareKey,
3710
+ tokenKey,
3713
3711
  meta: {
3714
- unlockMethod: "pin",
3715
- pinSalt: bufferToB64u(pinSalt),
3716
- pinIterations: 6e5
3712
+ unlockMethod: "password",
3713
+ passwordSalt: bufferToB64u(passwordSalt),
3714
+ passwordIterations: PBKDF2_ITERATIONS
3717
3715
  }
3718
3716
  };
3719
3717
  }
3718
+ async function attachUnlockToken(userId, jwt, tokenKey) {
3719
+ try {
3720
+ const stored = await storage.getShare(userId);
3721
+ if (!stored) return;
3722
+ const { token } = await api.issueUnlockToken(jwt);
3723
+ const unlockTokenIv = generateRandomBytes(12);
3724
+ const wrappedUnlockToken = await aesGcmEncrypt(
3725
+ tokenKey,
3726
+ new TextEncoder().encode(token),
3727
+ unlockTokenIv
3728
+ );
3729
+ await storage.putShare({ ...stored, wrappedUnlockToken, unlockTokenIv });
3730
+ } catch (e) {
3731
+ console.warn("decane-connect-kit: could not store an unlock token for this device.", e);
3732
+ }
3733
+ }
3720
3734
  async function persistRotatedDeviceShare(userId, deviceShare) {
3721
3735
  const stored = await storage.getShare(userId);
3722
3736
  if (!stored) {
@@ -3773,13 +3787,21 @@ async function createDecaneConnect(config) {
3773
3787
  const wrappingKey = await importAesKey(prfOutput);
3774
3788
  const iv = generateRandomBytes(12);
3775
3789
  const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3790
+ const existing = await storage.getShare(userId);
3776
3791
  await storage.putShare({
3792
+ ...existing,
3777
3793
  userId,
3778
3794
  wrappedDeviceShare,
3779
3795
  iv,
3780
3796
  unlockMethod: "passkey",
3781
3797
  credentialId,
3782
3798
  prfSalt,
3799
+ pinSalt: void 0,
3800
+ pinIterations: void 0,
3801
+ passwordSalt: void 0,
3802
+ passwordIterations: void 0,
3803
+ wrappedUnlockToken: void 0,
3804
+ unlockTokenIv: void 0,
3783
3805
  version: 1
3784
3806
  });
3785
3807
  try {
@@ -3805,11 +3827,44 @@ async function createDecaneConnect(config) {
3805
3827
  );
3806
3828
  }
3807
3829
  }
3830
+ async function persistProvisionedShare(userId, jwt, deviceShare) {
3831
+ if (isPasskeySupported()) {
3832
+ try {
3833
+ await registerPasskeyForDevice(userId, jwt, deviceShare);
3834
+ return;
3835
+ } catch (e) {
3836
+ console.warn(
3837
+ "decane-connect-kit: passkey provisioning failed; falling back to an unlock password so the share is still stored.",
3838
+ e
3839
+ );
3840
+ }
3841
+ }
3842
+ if (!config.promptUnlockPassword) {
3843
+ console.error(
3844
+ "decane-connect-kit: this device could not store its share \u2014 no passkey and no promptUnlockPassword. The session works, but every visit will re-provision and no unlock will be offered. Configure promptUnlockPassword."
3845
+ );
3846
+ return;
3847
+ }
3848
+ const { wrappingKey, tokenKey, meta } = await passwordSetup();
3849
+ const iv = generateRandomBytes(12);
3850
+ const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
3851
+ await storage.putShare({
3852
+ userId,
3853
+ wrappedDeviceShare,
3854
+ iv,
3855
+ unlockMethod: "password",
3856
+ passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
3857
+ passwordIterations: meta.passwordIterations,
3858
+ version: 1
3859
+ });
3860
+ await attachUnlockToken(userId, jwt, tokenKey);
3861
+ }
3808
3862
  async function firstTimeSetup(userId, jwt) {
3809
3863
  prewarmAttestation(teeUrl);
3810
3864
  let unlockMethod;
3811
3865
  let wrappingKey;
3812
3866
  let meta = {};
3867
+ let unlockTokenKey = null;
3813
3868
  if (isPasskeySupported()) {
3814
3869
  const prfSalt = generateRandomBytes(32);
3815
3870
  try {
@@ -3823,18 +3878,18 @@ async function createDecaneConnect(config) {
3823
3878
  prfSalt: bufferToB64u(prfSalt)
3824
3879
  };
3825
3880
  } else {
3826
- ({ wrappingKey, meta } = await pinSetup());
3827
- unlockMethod = "pin";
3881
+ ({ wrappingKey, meta, tokenKey: unlockTokenKey } = await passwordSetup());
3882
+ unlockMethod = "password";
3828
3883
  }
3829
3884
  } catch (e) {
3830
3885
  if (e.name === "UserCancelledError")
3831
3886
  throw new UserCancelledError();
3832
- ({ wrappingKey, meta } = await pinSetup());
3833
- unlockMethod = "pin";
3887
+ ({ wrappingKey, meta, tokenKey: unlockTokenKey } = await passwordSetup());
3888
+ unlockMethod = "password";
3834
3889
  }
3835
3890
  } else {
3836
- ({ wrappingKey, meta } = await pinSetup());
3837
- unlockMethod = "pin";
3891
+ ({ wrappingKey, meta, tokenKey: unlockTokenKey } = await passwordSetup());
3892
+ unlockMethod = "password";
3838
3893
  }
3839
3894
  const provisioningStartedAt = Date.now();
3840
3895
  const remainingBudget = () => PROVISIONING_BUDGET_MS - (Date.now() - provisioningStartedAt);
@@ -3866,8 +3921,11 @@ async function createDecaneConnect(config) {
3866
3921
  prfSalt: meta.prfSalt ? b64uToBuffer(meta.prfSalt) : void 0,
3867
3922
  pinSalt: meta.pinSalt ? b64uToBuffer(meta.pinSalt) : void 0,
3868
3923
  pinIterations: meta.pinIterations,
3924
+ passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
3925
+ passwordIterations: meta.passwordIterations,
3869
3926
  version: 1
3870
3927
  });
3928
+ if (unlockTokenKey) await attachUnlockToken(userId, jwt, unlockTokenKey);
3871
3929
  const serverIv = generateRandomBytes(12);
3872
3930
  await withDeadline(api.putShare(jwt, {
3873
3931
  ciphertext: shareSet.server,
@@ -3965,8 +4023,7 @@ async function createDecaneConnect(config) {
3965
4023
  const tee = new TeeClient(teeUrl);
3966
4024
  const provisioned = await tee.provisionDeviceShare(jwt);
3967
4025
  if (provisioned) {
3968
- await registerPasskeyAndBackup(userId, jwt, provisioned).catch(() => {
3969
- });
4026
+ await persistProvisionedShare(userId, jwt, provisioned);
3970
4027
  let addrs;
3971
4028
  try {
3972
4029
  addrs = await session.start(provisioned, jwt, recordedAddresses);
@@ -4096,6 +4153,12 @@ async function createDecaneConnect(config) {
4096
4153
  const candidates = (await storage.listShares()).map(usablePasskeyShare).filter(Boolean);
4097
4154
  return candidates.length === 1 ? candidates[0] : null;
4098
4155
  }
4156
+ async function canOfferPasskey() {
4157
+ if (!isPasskeySupported()) return false;
4158
+ if (await passkeyDeviceCandidate()) return true;
4159
+ if (!persisted?.userId) return false;
4160
+ return await storage.getShare(persisted.userId) == null;
4161
+ }
4099
4162
  async function rememberProfile(userId, profile) {
4100
4163
  const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
4101
4164
  if (!Object.keys(incoming).length) return;
@@ -4111,6 +4174,28 @@ async function createDecaneConnect(config) {
4111
4174
  if (!stored.credentialId || !stored.prfSalt) return null;
4112
4175
  return stored;
4113
4176
  }
4177
+ async function passkeyLoginWithoutShare(userId) {
4178
+ events.emit("wallet-unlocking");
4179
+ const options = await api.getWebAuthnLoginOptions(userId, rpId);
4180
+ if (!options) throw new NewDeviceError();
4181
+ const { response } = await authenticateWithPasskey({
4182
+ optionsJSON: options,
4183
+ prfSalt: generateRandomBytes(32)
4184
+ });
4185
+ const result = await api.postWebAuthnLogin(userId, response, window.location.origin);
4186
+ const addresses = await returningUserUnlock(userId, result.jwt);
4187
+ auth = { jwt: result.jwt, userId, addresses };
4188
+ persisted = { userId, addresses };
4189
+ savePersisted(PERSIST_KEY, persisted);
4190
+ events.emit("connected");
4191
+ persistLiveSession();
4192
+ const stored = await storage.getShare(userId);
4193
+ return {
4194
+ addresses,
4195
+ isNewUser: false,
4196
+ ...stored?.profile ? { profile: stored.profile } : {}
4197
+ };
4198
+ }
4114
4199
  async function passkeyLogin(stored) {
4115
4200
  const userId = stored.userId;
4116
4201
  events.emit("wallet-unlocking");
@@ -4270,12 +4355,15 @@ async function createDecaneConnect(config) {
4270
4355
  return handleAuthResult(result);
4271
4356
  },
4272
4357
  async canSignInWithPasskey() {
4273
- return await passkeyDeviceCandidate() !== null;
4358
+ return canOfferPasskey();
4274
4359
  },
4275
4360
  async signInWithPasskey() {
4276
4361
  const candidate = await passkeyDeviceCandidate();
4277
- if (!candidate) throw new NewDeviceError();
4278
- return passkeyLogin(candidate);
4362
+ if (candidate) return passkeyLogin(candidate);
4363
+ if (isPasskeySupported() && persisted?.userId && await storage.getShare(persisted.userId) == null) {
4364
+ return passkeyLoginWithoutShare(persisted.userId);
4365
+ }
4366
+ throw new NewDeviceError();
4279
4367
  },
4280
4368
  /**
4281
4369
  * Turn on "unlock with your password" for this device.