decane-connect-kit 2.22.0 → 2.23.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
@@ -146,6 +146,19 @@ interface DecaneConnectConfig {
146
146
  * page load (stricter, but the reload friction that motivated this returns).
147
147
  */
148
148
  persistSession?: boolean;
149
+ /**
150
+ * Do not wrap and store this device's share at wallet creation or when a
151
+ * device is first provisioned — so sign-in asks for no passkey and no
152
+ * password. The session works without a stored share: signing runs on the
153
+ * open session, and a later visit provisions a fresh share against a live
154
+ * sign-in. What the device loses until it is protected is the short way
155
+ * back in (passkey / password unlock) — every visit is a full sign-in.
156
+ *
157
+ * Call `protectDevice()` at the moment the app wants that step, typically
158
+ * before the user's first transaction. `deviceProtected()` says whether it
159
+ * has happened. Default false: the share is wrapped and stored at creation.
160
+ */
161
+ deferDeviceProtection?: boolean;
149
162
  /**
150
163
  * Offer to save a recovery-share file at signup. **Default false** since
151
164
  * 2.7.4 — opt in explicitly with `true`.
@@ -428,6 +441,21 @@ interface DecaneConnect {
428
441
  * on the way).
429
442
  */
430
443
  addPasskey(): Promise<void>;
444
+ /**
445
+ * Whether this device holds a wrapped share for the signed-in account, i.e.
446
+ * whether a passkey or password protects the wallet here. False on a device
447
+ * created with `deferDeviceProtection`, or whose share was never written.
448
+ */
449
+ deviceProtected(): Promise<boolean>;
450
+ /**
451
+ * Wrap and store this device's share now: a passkey where the device can
452
+ * hold one, otherwise the unlock password (`promptUnlockPassword`). Needs a
453
+ * live session; provisions a fresh share from the enclave when the device
454
+ * has none. Idempotent — a protected device returns true at once. Resolves
455
+ * false when the user dismissed the prompt, so the app can decide whether
456
+ * to go on without protection; throws for anything else.
457
+ */
458
+ protectDevice(): Promise<boolean>;
431
459
  sessionExpiresAt(): number | null;
432
460
  signMessage(req: SignMessageRequest): Promise<string>;
433
461
  sendTransaction(req: SendTransactionRequest): Promise<string>;
@@ -533,7 +561,7 @@ interface DecaneConnect {
533
561
  off(event: SdkEvent, handler: (...args: unknown[]) => void): void;
534
562
  }
535
563
  interface WrappedShareMeta {
536
- unlockMethod: 'passkey' | 'pin' | 'password';
564
+ unlockMethod: 'passkey' | 'pin' | 'password' | 'deferred';
537
565
  credentialId?: string;
538
566
  prfSalt?: string;
539
567
  pinSalt?: string;
@@ -847,6 +875,13 @@ interface SocialConfig {
847
875
  * a silent multi-second pause reads as a broken page.
848
876
  */
849
877
  showStatusOverlay?: boolean;
878
+ /**
879
+ * Ask for no passkey or password at sign-in; protect the wallet on this
880
+ * device later, when the app calls `protectDevice()` (typically before the
881
+ * first transaction). Mirrors the headless client's option of the same name;
882
+ * see it for what a device gives up until then. Default false.
883
+ */
884
+ deferDeviceProtection?: boolean;
850
885
  /**
851
886
  * Offer to save a recovery-share file at signup. **Default false** since
852
887
  * 2.7.4 — opt in explicitly with `true`.
@@ -1448,6 +1483,8 @@ declare function useSocialWallet(): {
1448
1483
  getAccessToken: () => string | null;
1449
1484
  hasPasskey: () => Promise<boolean>;
1450
1485
  addPasskey: () => Promise<void>;
1486
+ deviceProtected: () => Promise<boolean>;
1487
+ protectDevice: () => Promise<boolean>;
1451
1488
  setUnlockPassword: (password: string) => Promise<void>;
1452
1489
  canUnlockWithPassword: () => Promise<boolean>;
1453
1490
  unlockWithPassword: (password: string) => Promise<void>;
package/dist/index.d.ts CHANGED
@@ -146,6 +146,19 @@ interface DecaneConnectConfig {
146
146
  * page load (stricter, but the reload friction that motivated this returns).
147
147
  */
148
148
  persistSession?: boolean;
149
+ /**
150
+ * Do not wrap and store this device's share at wallet creation or when a
151
+ * device is first provisioned — so sign-in asks for no passkey and no
152
+ * password. The session works without a stored share: signing runs on the
153
+ * open session, and a later visit provisions a fresh share against a live
154
+ * sign-in. What the device loses until it is protected is the short way
155
+ * back in (passkey / password unlock) — every visit is a full sign-in.
156
+ *
157
+ * Call `protectDevice()` at the moment the app wants that step, typically
158
+ * before the user's first transaction. `deviceProtected()` says whether it
159
+ * has happened. Default false: the share is wrapped and stored at creation.
160
+ */
161
+ deferDeviceProtection?: boolean;
149
162
  /**
150
163
  * Offer to save a recovery-share file at signup. **Default false** since
151
164
  * 2.7.4 — opt in explicitly with `true`.
@@ -428,6 +441,21 @@ interface DecaneConnect {
428
441
  * on the way).
429
442
  */
430
443
  addPasskey(): Promise<void>;
444
+ /**
445
+ * Whether this device holds a wrapped share for the signed-in account, i.e.
446
+ * whether a passkey or password protects the wallet here. False on a device
447
+ * created with `deferDeviceProtection`, or whose share was never written.
448
+ */
449
+ deviceProtected(): Promise<boolean>;
450
+ /**
451
+ * Wrap and store this device's share now: a passkey where the device can
452
+ * hold one, otherwise the unlock password (`promptUnlockPassword`). Needs a
453
+ * live session; provisions a fresh share from the enclave when the device
454
+ * has none. Idempotent — a protected device returns true at once. Resolves
455
+ * false when the user dismissed the prompt, so the app can decide whether
456
+ * to go on without protection; throws for anything else.
457
+ */
458
+ protectDevice(): Promise<boolean>;
431
459
  sessionExpiresAt(): number | null;
432
460
  signMessage(req: SignMessageRequest): Promise<string>;
433
461
  sendTransaction(req: SendTransactionRequest): Promise<string>;
@@ -533,7 +561,7 @@ interface DecaneConnect {
533
561
  off(event: SdkEvent, handler: (...args: unknown[]) => void): void;
534
562
  }
535
563
  interface WrappedShareMeta {
536
- unlockMethod: 'passkey' | 'pin' | 'password';
564
+ unlockMethod: 'passkey' | 'pin' | 'password' | 'deferred';
537
565
  credentialId?: string;
538
566
  prfSalt?: string;
539
567
  pinSalt?: string;
@@ -847,6 +875,13 @@ interface SocialConfig {
847
875
  * a silent multi-second pause reads as a broken page.
848
876
  */
849
877
  showStatusOverlay?: boolean;
878
+ /**
879
+ * Ask for no passkey or password at sign-in; protect the wallet on this
880
+ * device later, when the app calls `protectDevice()` (typically before the
881
+ * first transaction). Mirrors the headless client's option of the same name;
882
+ * see it for what a device gives up until then. Default false.
883
+ */
884
+ deferDeviceProtection?: boolean;
850
885
  /**
851
886
  * Offer to save a recovery-share file at signup. **Default false** since
852
887
  * 2.7.4 — opt in explicitly with `true`.
@@ -1448,6 +1483,8 @@ declare function useSocialWallet(): {
1448
1483
  getAccessToken: () => string | null;
1449
1484
  hasPasskey: () => Promise<boolean>;
1450
1485
  addPasskey: () => Promise<void>;
1486
+ deviceProtected: () => Promise<boolean>;
1487
+ protectDevice: () => Promise<boolean>;
1451
1488
  setUnlockPassword: (password: string) => Promise<void>;
1452
1489
  canUnlockWithPassword: () => Promise<boolean>;
1453
1490
  unlockWithPassword: (password: string) => Promise<void>;
package/dist/index.js CHANGED
@@ -3684,6 +3684,7 @@ async function createDecaneConnect(config) {
3684
3684
  let lastUserId = loadLastUser(LAST_USER_KEY);
3685
3685
  let ethProvider = null;
3686
3686
  const persistSession = config.persistSession !== false;
3687
+ const deferDeviceProtection = config.deferDeviceProtection === true;
3687
3688
  const serverAssistedRecovery = true;
3688
3689
  const RECOVERY_OFFER_TIMEOUT_MS = 5 * 6e4;
3689
3690
  const selfHealAttempted = /* @__PURE__ */ new Set();
@@ -3995,11 +3996,12 @@ async function createDecaneConnect(config) {
3995
3996
  }
3996
3997
  async function firstTimeSetup(userId, jwt) {
3997
3998
  prewarmAttestation(teeUrl);
3998
- let unlockMethod;
3999
- let wrappingKey;
3999
+ let unlockMethod = "password";
4000
+ let wrappingKey = null;
4000
4001
  let meta = {};
4001
4002
  let unlockTokenKey = null;
4002
- if (isPasskeySupported()) {
4003
+ if (deferDeviceProtection) {
4004
+ } else if (isPasskeySupported()) {
4003
4005
  const prfSalt = generateRandomBytes(32);
4004
4006
  try {
4005
4007
  const { credentialId, prfOutput } = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
@@ -4040,32 +4042,37 @@ async function createDecaneConnect(config) {
4040
4042
  } finally {
4041
4043
  zeroize(master);
4042
4044
  }
4043
- const iv = generateRandomBytes(12);
4044
- const wrappedDeviceShare = await aesGcmEncrypt(
4045
- wrappingKey,
4046
- shareSet.device,
4047
- iv
4048
- );
4049
- await storage.putShare({
4050
- userId,
4051
- wrappedDeviceShare,
4052
- iv,
4053
- unlockMethod,
4054
- credentialId: meta.credentialId ? b64uToBuffer(meta.credentialId) : void 0,
4055
- prfSalt: meta.prfSalt ? b64uToBuffer(meta.prfSalt) : void 0,
4056
- pinSalt: meta.pinSalt ? b64uToBuffer(meta.pinSalt) : void 0,
4057
- pinIterations: meta.pinIterations,
4058
- passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
4059
- passwordIterations: meta.passwordIterations,
4060
- version: 1
4061
- });
4062
- if (unlockTokenKey) await attachUnlockToken(userId, jwt, unlockTokenKey);
4045
+ let wrappedDeviceShare = null;
4046
+ let iv = null;
4047
+ if (wrappingKey) {
4048
+ iv = generateRandomBytes(12);
4049
+ wrappedDeviceShare = await aesGcmEncrypt(
4050
+ wrappingKey,
4051
+ shareSet.device,
4052
+ iv
4053
+ );
4054
+ await storage.putShare({
4055
+ userId,
4056
+ wrappedDeviceShare,
4057
+ iv,
4058
+ unlockMethod,
4059
+ credentialId: meta.credentialId ? b64uToBuffer(meta.credentialId) : void 0,
4060
+ prfSalt: meta.prfSalt ? b64uToBuffer(meta.prfSalt) : void 0,
4061
+ pinSalt: meta.pinSalt ? b64uToBuffer(meta.pinSalt) : void 0,
4062
+ pinIterations: meta.pinIterations,
4063
+ passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
4064
+ passwordIterations: meta.passwordIterations,
4065
+ version: 1
4066
+ });
4067
+ if (unlockTokenKey) await attachUnlockToken(userId, jwt, unlockTokenKey);
4068
+ }
4063
4069
  const serverIv = generateRandomBytes(12);
4064
4070
  await withDeadline(api.putShare(jwt, {
4065
4071
  ciphertext: shareSet.server,
4066
4072
  iv: serverIv,
4067
4073
  wrappedShareMeta: {
4068
- unlockMethod
4074
+ // The server records this on the wallet_created event and nothing else.
4075
+ unlockMethod: deferDeviceProtection ? "deferred" : unlockMethod
4069
4076
  // credential IDs, prfSalt, pinSalt stay on-device — never sent to server
4070
4077
  },
4071
4078
  evmAddress,
@@ -4074,7 +4081,7 @@ async function createDecaneConnect(config) {
4074
4081
  }), remainingBudget(), "storing your wallet");
4075
4082
  zeroize(shareSet.server);
4076
4083
  const addresses = { evm: evmAddress, solana: solanaAddress, tron: tronAddress };
4077
- if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt) {
4084
+ if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt && wrappedDeviceShare && iv) {
4078
4085
  try {
4079
4086
  await api.putRecoveryBackup(jwt, {
4080
4087
  wrappedDeviceShare,
@@ -4157,7 +4164,7 @@ async function createDecaneConnect(config) {
4157
4164
  const tee = new TeeClient(teeUrl);
4158
4165
  const provisioned = await tee.provisionDeviceShare(jwt);
4159
4166
  if (provisioned) {
4160
- await persistProvisionedShare(userId, jwt, provisioned);
4167
+ if (!deferDeviceProtection) await persistProvisionedShare(userId, jwt, provisioned);
4161
4168
  let addrs;
4162
4169
  try {
4163
4170
  addrs = await session.start(provisioned, jwt, recordedAddresses);
@@ -4201,7 +4208,7 @@ async function createDecaneConnect(config) {
4201
4208
  });
4202
4209
  deviceShare = recovered.deviceShare;
4203
4210
  joinedViaPasskeySync = recovered.flow === "passkey-sync";
4204
- if (!await storage.getShare(userId) && isPasskeySupported()) {
4211
+ if (!deferDeviceProtection && !await storage.getShare(userId) && isPasskeySupported()) {
4205
4212
  await registerPasskeyAndBackup(userId, jwt, deviceShare);
4206
4213
  }
4207
4214
  } else {
@@ -4672,6 +4679,24 @@ async function createDecaneConnect(config) {
4672
4679
  zeroize(deviceShare);
4673
4680
  }
4674
4681
  },
4682
+ async deviceProtected() {
4683
+ if (!auth) return false;
4684
+ return await storage.getShare(auth.userId) !== null;
4685
+ },
4686
+ async protectDevice() {
4687
+ if (!auth) throw new ReconnectRequiredError();
4688
+ if (await storage.getShare(auth.userId)) return true;
4689
+ if (!serverAssistedRecovery) throw new NewDeviceError();
4690
+ const tee = new TeeClient(teeUrl);
4691
+ const provisioned = await tee.provisionDeviceShare(auth.jwt);
4692
+ if (!provisioned) throw new NewDeviceError();
4693
+ try {
4694
+ await persistProvisionedShare(auth.userId, auth.jwt, provisioned);
4695
+ } finally {
4696
+ zeroize(provisioned);
4697
+ }
4698
+ return await storage.getShare(auth.userId) !== null;
4699
+ },
4675
4700
  async unlock() {
4676
4701
  if (!auth) {
4677
4702
  const candidate = await passkeyLoginCandidate();
@@ -7301,6 +7326,7 @@ function DecaneKit({ config, children }) {
7301
7326
  chains,
7302
7327
  rpcUrls,
7303
7328
  requireAssertionPerSignature,
7329
+ deferDeviceProtection,
7304
7330
  // Recovery callbacks. Forwarding these is what makes recovery
7305
7331
  // configurable at all from <DecaneKit>; before, they were silently
7306
7332
  // dropped and the headless client was constructed without them.
@@ -7321,6 +7347,7 @@ function DecaneKit({ config, children }) {
7321
7347
  promptPin,
7322
7348
  promptUnlockPassword,
7323
7349
  requireAssertionPerSignature,
7350
+ deferDeviceProtection,
7324
7351
  offerRecoveryAtSignup,
7325
7352
  onRecoveryShareOffer,
7326
7353
  onRecoveryFileReady,
@@ -7567,6 +7594,16 @@ function DecaneKit({ config, children }) {
7567
7594
  if (!sdk) throw new Error("Social wallet not connected");
7568
7595
  return sdk.addPasskey();
7569
7596
  }, []);
7597
+ const socialDeviceProtected = useCallback3(async () => {
7598
+ const sdk = socialRef.current;
7599
+ if (!sdk) return false;
7600
+ return sdk.deviceProtected();
7601
+ }, []);
7602
+ const socialProtectDevice = useCallback3(async () => {
7603
+ const sdk = socialRef.current;
7604
+ if (!sdk) throw new Error("Social wallet not connected");
7605
+ return sdk.protectDevice();
7606
+ }, []);
7570
7607
  const socialSetUnlockPassword = useCallback3(async (password) => {
7571
7608
  const sdk = socialRef.current;
7572
7609
  if (!sdk) throw new Error("Social wallet not connected");
@@ -7659,6 +7696,8 @@ function DecaneKit({ config, children }) {
7659
7696
  socialHasRecoveryShare,
7660
7697
  socialHasPasskey,
7661
7698
  socialAddPasskey,
7699
+ socialDeviceProtected,
7700
+ socialProtectDevice,
7662
7701
  socialSetUnlockPassword,
7663
7702
  socialCanUnlockWithPassword,
7664
7703
  socialForgetLastUser,
@@ -7969,6 +8008,8 @@ function useSocialWallet() {
7969
8008
  socialHasRecoveryShare,
7970
8009
  socialHasPasskey,
7971
8010
  socialAddPasskey,
8011
+ socialDeviceProtected,
8012
+ socialProtectDevice,
7972
8013
  socialSetUnlockPassword,
7973
8014
  socialCanUnlockWithPassword,
7974
8015
  socialForgetLastUser,
@@ -8071,6 +8112,16 @@ function useSocialWallet() {
8071
8112
  throw e;
8072
8113
  }
8073
8114
  }, [socialAddPasskey]);
8115
+ const deviceProtected = useCallback4(() => run(() => socialDeviceProtected()), [socialDeviceProtected]);
8116
+ const protectDevice = useCallback4(async () => {
8117
+ setError(null);
8118
+ try {
8119
+ return await socialProtectDevice();
8120
+ } catch (e) {
8121
+ setError(e.message);
8122
+ throw e;
8123
+ }
8124
+ }, [socialProtectDevice]);
8074
8125
  const setUnlockPassword = useCallback4(async (password) => {
8075
8126
  setError(null);
8076
8127
  try {
@@ -8159,6 +8210,10 @@ function useSocialWallet() {
8159
8210
  // Passkey management for THIS device
8160
8211
  hasPasskey,
8161
8212
  addPasskey,
8213
+ // Deferred protection (deferDeviceProtection): whether this device holds
8214
+ // a wrapped share, and the call that stores one — passkey or password.
8215
+ deviceProtected,
8216
+ protectDevice,
8162
8217
  // "Unlock with your password": setUnlockPassword needs a live session;
8163
8218
  // canUnlockWithPassword and unlockWithPassword are the signed-out pair that
8164
8219
  // open one without an identity provider or a passkey.