decane-connect-kit 2.12.0 → 2.13.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.
@@ -755,6 +757,17 @@ interface SocialConfig {
755
757
  */
756
758
  kingsChatScopes?: string[];
757
759
  promptPin?: () => Promise<string>;
760
+ /**
761
+ * Called when the SDK needs the device unlock password — to choose one
762
+ * (`kind: 'set'`, ask twice) or to enter it (`kind: 'unlock'`).
763
+ *
764
+ * Required on any device using "unlock with your password". Unlike promptPin,
765
+ * this secret can open a session on its own, so never log or persist it and
766
+ * let a password manager fill it.
767
+ */
768
+ promptUnlockPassword?: (ctx: {
769
+ kind: 'set' | 'unlock';
770
+ }) => Promise<string>;
758
771
  apiBaseUrl?: string;
759
772
  chains?: string[];
760
773
  rpcUrls?: Record<string, string>;
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.
@@ -755,6 +757,17 @@ interface SocialConfig {
755
757
  */
756
758
  kingsChatScopes?: string[];
757
759
  promptPin?: () => Promise<string>;
760
+ /**
761
+ * Called when the SDK needs the device unlock password — to choose one
762
+ * (`kind: 'set'`, ask twice) or to enter it (`kind: 'unlock'`).
763
+ *
764
+ * Required on any device using "unlock with your password". Unlike promptPin,
765
+ * this secret can open a session on its own, so never log or persist it and
766
+ * let a password manager fill it.
767
+ */
768
+ promptUnlockPassword?: (ctx: {
769
+ kind: 'set' | 'unlock';
770
+ }) => Promise<string>;
758
771
  apiBaseUrl?: string;
759
772
  chains?: string[];
760
773
  rpcUrls?: Record<string, string>;
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) {
@@ -3810,6 +3824,7 @@ async function createDecaneConnect(config) {
3810
3824
  let unlockMethod;
3811
3825
  let wrappingKey;
3812
3826
  let meta = {};
3827
+ let unlockTokenKey = null;
3813
3828
  if (isPasskeySupported()) {
3814
3829
  const prfSalt = generateRandomBytes(32);
3815
3830
  try {
@@ -3823,18 +3838,18 @@ async function createDecaneConnect(config) {
3823
3838
  prfSalt: bufferToB64u(prfSalt)
3824
3839
  };
3825
3840
  } else {
3826
- ({ wrappingKey, meta } = await pinSetup());
3827
- unlockMethod = "pin";
3841
+ ({ wrappingKey, meta, tokenKey: unlockTokenKey } = await passwordSetup());
3842
+ unlockMethod = "password";
3828
3843
  }
3829
3844
  } catch (e) {
3830
3845
  if (e.name === "UserCancelledError")
3831
3846
  throw new UserCancelledError();
3832
- ({ wrappingKey, meta } = await pinSetup());
3833
- unlockMethod = "pin";
3847
+ ({ wrappingKey, meta, tokenKey: unlockTokenKey } = await passwordSetup());
3848
+ unlockMethod = "password";
3834
3849
  }
3835
3850
  } else {
3836
- ({ wrappingKey, meta } = await pinSetup());
3837
- unlockMethod = "pin";
3851
+ ({ wrappingKey, meta, tokenKey: unlockTokenKey } = await passwordSetup());
3852
+ unlockMethod = "password";
3838
3853
  }
3839
3854
  const provisioningStartedAt = Date.now();
3840
3855
  const remainingBudget = () => PROVISIONING_BUDGET_MS - (Date.now() - provisioningStartedAt);
@@ -3866,8 +3881,11 @@ async function createDecaneConnect(config) {
3866
3881
  prfSalt: meta.prfSalt ? b64uToBuffer(meta.prfSalt) : void 0,
3867
3882
  pinSalt: meta.pinSalt ? b64uToBuffer(meta.pinSalt) : void 0,
3868
3883
  pinIterations: meta.pinIterations,
3884
+ passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
3885
+ passwordIterations: meta.passwordIterations,
3869
3886
  version: 1
3870
3887
  });
3888
+ if (unlockTokenKey) await attachUnlockToken(userId, jwt, unlockTokenKey);
3871
3889
  const serverIv = generateRandomBytes(12);
3872
3890
  await withDeadline(api.putShare(jwt, {
3873
3891
  ciphertext: shareSet.server,
@@ -6943,6 +6961,7 @@ function DecaneKit({ config, children }) {
6943
6961
  apiKey,
6944
6962
  authMethods,
6945
6963
  promptPin,
6964
+ promptUnlockPassword,
6946
6965
  apiBaseUrl,
6947
6966
  chains,
6948
6967
  rpcUrls,
@@ -6965,6 +6984,7 @@ function DecaneKit({ config, children }) {
6965
6984
  chains: chains ?? ["evm:1", "solana:mainnet"],
6966
6985
  rpcUrls,
6967
6986
  promptPin,
6987
+ promptUnlockPassword,
6968
6988
  requireAssertionPerSignature,
6969
6989
  offerRecoveryAtSignup,
6970
6990
  onRecoveryShareOffer,