decane-connect-kit 2.22.0 → 2.24.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`.
@@ -345,6 +358,19 @@ interface DecaneConnect {
345
358
  connectWithToken(token: string, opts?: {
346
359
  providerId?: string;
347
360
  }): Promise<ConnectResult>;
361
+ /**
362
+ * Adopt a Decane access token another app of the same project handed over
363
+ * (in a URL, say): signed in at once, unlocked on the first signature.
364
+ *
365
+ * The token is the one every API call carries as a Bearer. The backend
366
+ * judges it — a revoked or foreign token is refused — and the session lasts
367
+ * as long as the token does, at most two hours; when it lapses the client
368
+ * emits `session-expired` and reads as signed out. No enclave session is
369
+ * opened: the first `unlock()` (or any signing call) provisions or unwraps
370
+ * the device share as an ordinary returning sign-in would, and with
371
+ * `deferDeviceProtection` prompts for nothing.
372
+ */
373
+ connectWithAccessToken(token: string): Promise<ConnectResult>;
348
374
  verifyEmailCode(email: string, code: string): Promise<ConnectResult>;
349
375
  /**
350
376
  * Send a 6-digit sign-in code by SMS (delivered via Termii).
@@ -428,6 +454,21 @@ interface DecaneConnect {
428
454
  * on the way).
429
455
  */
430
456
  addPasskey(): Promise<void>;
457
+ /**
458
+ * Whether this device holds a wrapped share for the signed-in account, i.e.
459
+ * whether a passkey or password protects the wallet here. False on a device
460
+ * created with `deferDeviceProtection`, or whose share was never written.
461
+ */
462
+ deviceProtected(): Promise<boolean>;
463
+ /**
464
+ * Wrap and store this device's share now: a passkey where the device can
465
+ * hold one, otherwise the unlock password (`promptUnlockPassword`). Needs a
466
+ * live session; provisions a fresh share from the enclave when the device
467
+ * has none. Idempotent — a protected device returns true at once. Resolves
468
+ * false when the user dismissed the prompt, so the app can decide whether
469
+ * to go on without protection; throws for anything else.
470
+ */
471
+ protectDevice(): Promise<boolean>;
431
472
  sessionExpiresAt(): number | null;
432
473
  signMessage(req: SignMessageRequest): Promise<string>;
433
474
  sendTransaction(req: SendTransactionRequest): Promise<string>;
@@ -533,7 +574,7 @@ interface DecaneConnect {
533
574
  off(event: SdkEvent, handler: (...args: unknown[]) => void): void;
534
575
  }
535
576
  interface WrappedShareMeta {
536
- unlockMethod: 'passkey' | 'pin' | 'password';
577
+ unlockMethod: 'passkey' | 'pin' | 'password' | 'deferred';
537
578
  credentialId?: string;
538
579
  prfSalt?: string;
539
580
  pinSalt?: string;
@@ -847,6 +888,13 @@ interface SocialConfig {
847
888
  * a silent multi-second pause reads as a broken page.
848
889
  */
849
890
  showStatusOverlay?: boolean;
891
+ /**
892
+ * Ask for no passkey or password at sign-in; protect the wallet on this
893
+ * device later, when the app calls `protectDevice()` (typically before the
894
+ * first transaction). Mirrors the headless client's option of the same name;
895
+ * see it for what a device gives up until then. Default false.
896
+ */
897
+ deferDeviceProtection?: boolean;
850
898
  /**
851
899
  * Offer to save a recovery-share file at signup. **Default false** since
852
900
  * 2.7.4 — opt in explicitly with `true`.
@@ -1095,6 +1143,9 @@ declare class PromptPasswordNotConfiguredError extends DecaneConnectError {
1095
1143
  declare class WrongUnlockPasswordError extends DecaneConnectError {
1096
1144
  constructor();
1097
1145
  }
1146
+ declare class InvalidAccessTokenError extends DecaneConnectError {
1147
+ constructor(reason: string);
1148
+ }
1098
1149
  declare class UnlockTokenRejectedError extends DecaneConnectError {
1099
1150
  constructor();
1100
1151
  }
@@ -1388,6 +1439,7 @@ declare function useSocialAuth(): {
1388
1439
  signInWithToken: (token: string, opts?: {
1389
1440
  providerId?: string;
1390
1441
  }) => Promise<void>;
1442
+ signInWithAccessToken: (token: string) => Promise<void>;
1391
1443
  signInWithCustomAuth: (() => Promise<void>) | undefined;
1392
1444
  signInWithPasskey: () => Promise<void>;
1393
1445
  unlock: () => Promise<void>;
@@ -1448,6 +1500,8 @@ declare function useSocialWallet(): {
1448
1500
  getAccessToken: () => string | null;
1449
1501
  hasPasskey: () => Promise<boolean>;
1450
1502
  addPasskey: () => Promise<void>;
1503
+ deviceProtected: () => Promise<boolean>;
1504
+ protectDevice: () => Promise<boolean>;
1451
1505
  setUnlockPassword: (password: string) => Promise<void>;
1452
1506
  canUnlockWithPassword: () => Promise<boolean>;
1453
1507
  unlockWithPassword: (password: string) => Promise<void>;
@@ -1561,4 +1615,4 @@ declare class WeakPasswordError extends Error {
1561
1615
  */
1562
1616
  declare function validateUnlockPassword(password: string): void;
1563
1617
 
1564
- export { type ActiveSession, AttestationError, type AuthProfile, type AuthResult, BUILT_IN_CHAINS, type BalanceResult, type CAIP25Method, type CAIP25SessionRequest, type CAIP25SessionResult, CHAINS, CallSequenceError, type Chain, type ChainDescriptor, type ChainId, type ChainScope, type ChainSupport, type ChainType, type ConnectMode, type ConnectResult, type DecaneConfig, type DecaneConnect, type DecaneConnectConfig, DecaneConnectError, DecaneError, DecaneErrorCode, DecaneKit, DecaneLogo, DecaneMark, type DecaneProvider, DecaneWordmark, type Eip1193Provider, type EthereumProviderOptions, ExportNotAuthorizedError, InvalidApiKeyError, InvalidPinError, InvalidRecoveryFileError, type KingsChatTokens, MIN_PASSWORD_LENGTH, NetworkError, NewDeviceError, OriginNotAllowedError, type PortableBackup, PromptPasswordNotConfiguredError, PromptPinNotConfiguredError, ProvisioningTimeoutError, ReconnectRequiredError, type RecordTransactionInput, RecoveryFailedError, type RecoveryProgress, type RecoveryShareFile, RecoveryWalletMismatchError, type SdkEvent, type SendTransactionRequest, SessionExpiredError, ShareCorruptedError, type SignMessageRequest, type SignTypedDataRequest, type SignedAuthorization, type SocialAuthState, type SocialConfig, SocialWalletModal, StaleDeviceShareError, type Theme, type TransactionRecord, type TxParams, type UnifiedWalletAccount, UnlockRequiredError, UnlockTokenRejectedError, UserCancelledError, WalletSelector, WeakPasswordError, WebAuthnNotSupportedError, type WrappedShareMeta, WrongPasswordError, WrongUnlockPasswordError, createDecane, createDecaneConnect, createEthereumProvider, loginWithKingsChatPopup, startEmailAuth, useChainSwitch, useDecane, useEthereumProvider, useSendTransaction, useSignMessage, useSignTypedData, useSocialAuth, useSocialBalance, useSocialWallet, useWalletSelector, validateUnlockPassword, verifyEmailCode };
1618
+ export { type ActiveSession, AttestationError, type AuthProfile, type AuthResult, BUILT_IN_CHAINS, type BalanceResult, type CAIP25Method, type CAIP25SessionRequest, type CAIP25SessionResult, CHAINS, CallSequenceError, type Chain, type ChainDescriptor, type ChainId, type ChainScope, type ChainSupport, type ChainType, type ConnectMode, type ConnectResult, type DecaneConfig, type DecaneConnect, type DecaneConnectConfig, DecaneConnectError, DecaneError, DecaneErrorCode, DecaneKit, DecaneLogo, DecaneMark, type DecaneProvider, DecaneWordmark, type Eip1193Provider, type EthereumProviderOptions, ExportNotAuthorizedError, InvalidAccessTokenError, InvalidApiKeyError, InvalidPinError, InvalidRecoveryFileError, type KingsChatTokens, MIN_PASSWORD_LENGTH, NetworkError, NewDeviceError, OriginNotAllowedError, type PortableBackup, PromptPasswordNotConfiguredError, PromptPinNotConfiguredError, ProvisioningTimeoutError, ReconnectRequiredError, type RecordTransactionInput, RecoveryFailedError, type RecoveryProgress, type RecoveryShareFile, RecoveryWalletMismatchError, type SdkEvent, type SendTransactionRequest, SessionExpiredError, ShareCorruptedError, type SignMessageRequest, type SignTypedDataRequest, type SignedAuthorization, type SocialAuthState, type SocialConfig, SocialWalletModal, StaleDeviceShareError, type Theme, type TransactionRecord, type TxParams, type UnifiedWalletAccount, UnlockRequiredError, UnlockTokenRejectedError, UserCancelledError, WalletSelector, WeakPasswordError, WebAuthnNotSupportedError, type WrappedShareMeta, WrongPasswordError, WrongUnlockPasswordError, createDecane, createDecaneConnect, createEthereumProvider, loginWithKingsChatPopup, startEmailAuth, useChainSwitch, useDecane, useEthereumProvider, useSendTransaction, useSignMessage, useSignTypedData, useSocialAuth, useSocialBalance, useSocialWallet, useWalletSelector, validateUnlockPassword, verifyEmailCode };
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`.
@@ -345,6 +358,19 @@ interface DecaneConnect {
345
358
  connectWithToken(token: string, opts?: {
346
359
  providerId?: string;
347
360
  }): Promise<ConnectResult>;
361
+ /**
362
+ * Adopt a Decane access token another app of the same project handed over
363
+ * (in a URL, say): signed in at once, unlocked on the first signature.
364
+ *
365
+ * The token is the one every API call carries as a Bearer. The backend
366
+ * judges it — a revoked or foreign token is refused — and the session lasts
367
+ * as long as the token does, at most two hours; when it lapses the client
368
+ * emits `session-expired` and reads as signed out. No enclave session is
369
+ * opened: the first `unlock()` (or any signing call) provisions or unwraps
370
+ * the device share as an ordinary returning sign-in would, and with
371
+ * `deferDeviceProtection` prompts for nothing.
372
+ */
373
+ connectWithAccessToken(token: string): Promise<ConnectResult>;
348
374
  verifyEmailCode(email: string, code: string): Promise<ConnectResult>;
349
375
  /**
350
376
  * Send a 6-digit sign-in code by SMS (delivered via Termii).
@@ -428,6 +454,21 @@ interface DecaneConnect {
428
454
  * on the way).
429
455
  */
430
456
  addPasskey(): Promise<void>;
457
+ /**
458
+ * Whether this device holds a wrapped share for the signed-in account, i.e.
459
+ * whether a passkey or password protects the wallet here. False on a device
460
+ * created with `deferDeviceProtection`, or whose share was never written.
461
+ */
462
+ deviceProtected(): Promise<boolean>;
463
+ /**
464
+ * Wrap and store this device's share now: a passkey where the device can
465
+ * hold one, otherwise the unlock password (`promptUnlockPassword`). Needs a
466
+ * live session; provisions a fresh share from the enclave when the device
467
+ * has none. Idempotent — a protected device returns true at once. Resolves
468
+ * false when the user dismissed the prompt, so the app can decide whether
469
+ * to go on without protection; throws for anything else.
470
+ */
471
+ protectDevice(): Promise<boolean>;
431
472
  sessionExpiresAt(): number | null;
432
473
  signMessage(req: SignMessageRequest): Promise<string>;
433
474
  sendTransaction(req: SendTransactionRequest): Promise<string>;
@@ -533,7 +574,7 @@ interface DecaneConnect {
533
574
  off(event: SdkEvent, handler: (...args: unknown[]) => void): void;
534
575
  }
535
576
  interface WrappedShareMeta {
536
- unlockMethod: 'passkey' | 'pin' | 'password';
577
+ unlockMethod: 'passkey' | 'pin' | 'password' | 'deferred';
537
578
  credentialId?: string;
538
579
  prfSalt?: string;
539
580
  pinSalt?: string;
@@ -847,6 +888,13 @@ interface SocialConfig {
847
888
  * a silent multi-second pause reads as a broken page.
848
889
  */
849
890
  showStatusOverlay?: boolean;
891
+ /**
892
+ * Ask for no passkey or password at sign-in; protect the wallet on this
893
+ * device later, when the app calls `protectDevice()` (typically before the
894
+ * first transaction). Mirrors the headless client's option of the same name;
895
+ * see it for what a device gives up until then. Default false.
896
+ */
897
+ deferDeviceProtection?: boolean;
850
898
  /**
851
899
  * Offer to save a recovery-share file at signup. **Default false** since
852
900
  * 2.7.4 — opt in explicitly with `true`.
@@ -1095,6 +1143,9 @@ declare class PromptPasswordNotConfiguredError extends DecaneConnectError {
1095
1143
  declare class WrongUnlockPasswordError extends DecaneConnectError {
1096
1144
  constructor();
1097
1145
  }
1146
+ declare class InvalidAccessTokenError extends DecaneConnectError {
1147
+ constructor(reason: string);
1148
+ }
1098
1149
  declare class UnlockTokenRejectedError extends DecaneConnectError {
1099
1150
  constructor();
1100
1151
  }
@@ -1388,6 +1439,7 @@ declare function useSocialAuth(): {
1388
1439
  signInWithToken: (token: string, opts?: {
1389
1440
  providerId?: string;
1390
1441
  }) => Promise<void>;
1442
+ signInWithAccessToken: (token: string) => Promise<void>;
1391
1443
  signInWithCustomAuth: (() => Promise<void>) | undefined;
1392
1444
  signInWithPasskey: () => Promise<void>;
1393
1445
  unlock: () => Promise<void>;
@@ -1448,6 +1500,8 @@ declare function useSocialWallet(): {
1448
1500
  getAccessToken: () => string | null;
1449
1501
  hasPasskey: () => Promise<boolean>;
1450
1502
  addPasskey: () => Promise<void>;
1503
+ deviceProtected: () => Promise<boolean>;
1504
+ protectDevice: () => Promise<boolean>;
1451
1505
  setUnlockPassword: (password: string) => Promise<void>;
1452
1506
  canUnlockWithPassword: () => Promise<boolean>;
1453
1507
  unlockWithPassword: (password: string) => Promise<void>;
@@ -1561,4 +1615,4 @@ declare class WeakPasswordError extends Error {
1561
1615
  */
1562
1616
  declare function validateUnlockPassword(password: string): void;
1563
1617
 
1564
- export { type ActiveSession, AttestationError, type AuthProfile, type AuthResult, BUILT_IN_CHAINS, type BalanceResult, type CAIP25Method, type CAIP25SessionRequest, type CAIP25SessionResult, CHAINS, CallSequenceError, type Chain, type ChainDescriptor, type ChainId, type ChainScope, type ChainSupport, type ChainType, type ConnectMode, type ConnectResult, type DecaneConfig, type DecaneConnect, type DecaneConnectConfig, DecaneConnectError, DecaneError, DecaneErrorCode, DecaneKit, DecaneLogo, DecaneMark, type DecaneProvider, DecaneWordmark, type Eip1193Provider, type EthereumProviderOptions, ExportNotAuthorizedError, InvalidApiKeyError, InvalidPinError, InvalidRecoveryFileError, type KingsChatTokens, MIN_PASSWORD_LENGTH, NetworkError, NewDeviceError, OriginNotAllowedError, type PortableBackup, PromptPasswordNotConfiguredError, PromptPinNotConfiguredError, ProvisioningTimeoutError, ReconnectRequiredError, type RecordTransactionInput, RecoveryFailedError, type RecoveryProgress, type RecoveryShareFile, RecoveryWalletMismatchError, type SdkEvent, type SendTransactionRequest, SessionExpiredError, ShareCorruptedError, type SignMessageRequest, type SignTypedDataRequest, type SignedAuthorization, type SocialAuthState, type SocialConfig, SocialWalletModal, StaleDeviceShareError, type Theme, type TransactionRecord, type TxParams, type UnifiedWalletAccount, UnlockRequiredError, UnlockTokenRejectedError, UserCancelledError, WalletSelector, WeakPasswordError, WebAuthnNotSupportedError, type WrappedShareMeta, WrongPasswordError, WrongUnlockPasswordError, createDecane, createDecaneConnect, createEthereumProvider, loginWithKingsChatPopup, startEmailAuth, useChainSwitch, useDecane, useEthereumProvider, useSendTransaction, useSignMessage, useSignTypedData, useSocialAuth, useSocialBalance, useSocialWallet, useWalletSelector, validateUnlockPassword, verifyEmailCode };
1618
+ export { type ActiveSession, AttestationError, type AuthProfile, type AuthResult, BUILT_IN_CHAINS, type BalanceResult, type CAIP25Method, type CAIP25SessionRequest, type CAIP25SessionResult, CHAINS, CallSequenceError, type Chain, type ChainDescriptor, type ChainId, type ChainScope, type ChainSupport, type ChainType, type ConnectMode, type ConnectResult, type DecaneConfig, type DecaneConnect, type DecaneConnectConfig, DecaneConnectError, DecaneError, DecaneErrorCode, DecaneKit, DecaneLogo, DecaneMark, type DecaneProvider, DecaneWordmark, type Eip1193Provider, type EthereumProviderOptions, ExportNotAuthorizedError, InvalidAccessTokenError, InvalidApiKeyError, InvalidPinError, InvalidRecoveryFileError, type KingsChatTokens, MIN_PASSWORD_LENGTH, NetworkError, NewDeviceError, OriginNotAllowedError, type PortableBackup, PromptPasswordNotConfiguredError, PromptPinNotConfiguredError, ProvisioningTimeoutError, ReconnectRequiredError, type RecordTransactionInput, RecoveryFailedError, type RecoveryProgress, type RecoveryShareFile, RecoveryWalletMismatchError, type SdkEvent, type SendTransactionRequest, SessionExpiredError, ShareCorruptedError, type SignMessageRequest, type SignTypedDataRequest, type SignedAuthorization, type SocialAuthState, type SocialConfig, SocialWalletModal, StaleDeviceShareError, type Theme, type TransactionRecord, type TxParams, type UnifiedWalletAccount, UnlockRequiredError, UnlockTokenRejectedError, UserCancelledError, WalletSelector, WeakPasswordError, WebAuthnNotSupportedError, type WrappedShareMeta, WrongPasswordError, WrongUnlockPasswordError, createDecane, createDecaneConnect, createEthereumProvider, loginWithKingsChatPopup, startEmailAuth, useChainSwitch, useDecane, useEthereumProvider, useSendTransaction, useSignMessage, useSignTypedData, useSocialAuth, useSocialBalance, useSocialWallet, useWalletSelector, validateUnlockPassword, verifyEmailCode };
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ __export(errors_exports, {
15
15
  CallSequenceError: () => CallSequenceError,
16
16
  DecaneConnectError: () => DecaneConnectError,
17
17
  ExportNotAuthorizedError: () => ExportNotAuthorizedError,
18
+ InvalidAccessTokenError: () => InvalidAccessTokenError,
18
19
  InvalidApiKeyError: () => InvalidApiKeyError,
19
20
  InvalidPinError: () => InvalidPinError,
20
21
  InvalidRecoveryFileError: () => InvalidRecoveryFileError,
@@ -37,7 +38,7 @@ __export(errors_exports, {
37
38
  WrongPasswordError: () => WrongPasswordError,
38
39
  WrongUnlockPasswordError: () => WrongUnlockPasswordError
39
40
  });
40
- var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, ProvisioningTimeoutError, NewDeviceError, StaleDeviceShareError, PromptPinNotConfiguredError, PromptPasswordNotConfiguredError, WrongUnlockPasswordError, UnlockTokenRejectedError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
41
+ var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, ProvisioningTimeoutError, NewDeviceError, StaleDeviceShareError, PromptPinNotConfiguredError, PromptPasswordNotConfiguredError, WrongUnlockPasswordError, InvalidAccessTokenError, UnlockTokenRejectedError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
41
42
  var init_errors = __esm({
42
43
  "src/social/errors.ts"() {
43
44
  "use strict";
@@ -142,6 +143,11 @@ var init_errors = __esm({
142
143
  super("WRONG_UNLOCK_PASSWORD", "That password did not unlock this device");
143
144
  }
144
145
  };
146
+ InvalidAccessTokenError = class extends DecaneConnectError {
147
+ constructor(reason) {
148
+ super("INVALID_ACCESS_TOKEN", `That access token cannot open a session: ${reason}`);
149
+ }
150
+ };
145
151
  UnlockTokenRejectedError = class extends DecaneConnectError {
146
152
  constructor() {
147
153
  super("UNLOCK_TOKEN_REJECTED", "This device's unlock token is no longer valid \u2014 sign in normally to re-enable password unlock");
@@ -2273,6 +2279,20 @@ function b64ToBytes(b644) {
2273
2279
  return Uint8Array.from(atob(b644), (c) => c.charCodeAt(0));
2274
2280
  }
2275
2281
 
2282
+ // src/social/accessToken.ts
2283
+ function decodeAccessToken(token) {
2284
+ try {
2285
+ const payload = token.split(".")[1];
2286
+ if (!payload) return null;
2287
+ const json = JSON.parse(atob(payload.replace(/-/g, "+").replace(/_/g, "/")));
2288
+ if (typeof json.uid !== "string" || json.uid === "") return null;
2289
+ if (typeof json.exp !== "number" || !Number.isFinite(json.exp)) return null;
2290
+ return { userId: json.uid, expiresAt: json.exp * 1e3 };
2291
+ } catch {
2292
+ return null;
2293
+ }
2294
+ }
2295
+
2276
2296
  // src/social/storage.ts
2277
2297
  import { openDB } from "idb";
2278
2298
  var DB_NAME = "decane-connect-kit";
@@ -3667,8 +3687,10 @@ async function createDecaneConnect(config) {
3667
3687
  const durationMs = (config.sessionDurationMinutes ?? DEFAULT_SESSION_MINUTES) * 6e4;
3668
3688
  const rpId = config.rpId ?? (typeof window !== "undefined" ? window.location.hostname : "localhost");
3669
3689
  const rpName = config.rpName ?? "Decane Connect";
3670
- const PERSIST_KEY = makePersistKey(config.apiKey);
3671
- const LAST_USER_KEY = makeLastUserKey(config.apiKey);
3690
+ const PERSIST_KEY = makePersistKey(config.appId);
3691
+ const LAST_USER_KEY = makeLastUserKey(config.appId);
3692
+ const LEGACY_PERSIST_KEY = makePersistKey(config.apiKey);
3693
+ const LEGACY_LAST_USER_KEY = makeLastUserKey(config.apiKey);
3672
3694
  const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
3673
3695
  const events = new EventEmitter();
3674
3696
  const api = new ApiClient(baseUrl, config.apiKey, config.appId);
@@ -3681,9 +3703,17 @@ async function createDecaneConnect(config) {
3681
3703
  );
3682
3704
  let auth = null;
3683
3705
  let persisted = loadPersisted(PERSIST_KEY);
3684
- let lastUserId = loadLastUser(LAST_USER_KEY);
3706
+ if (!persisted && LEGACY_PERSIST_KEY !== PERSIST_KEY) {
3707
+ persisted = loadPersisted(LEGACY_PERSIST_KEY);
3708
+ if (persisted) {
3709
+ savePersisted(PERSIST_KEY, persisted);
3710
+ clearPersisted(LEGACY_PERSIST_KEY);
3711
+ }
3712
+ }
3713
+ let lastUserId = loadLastUser(LAST_USER_KEY) ?? (LEGACY_LAST_USER_KEY !== LAST_USER_KEY ? loadLastUser(LEGACY_LAST_USER_KEY) : null);
3685
3714
  let ethProvider = null;
3686
3715
  const persistSession = config.persistSession !== false;
3716
+ const deferDeviceProtection = config.deferDeviceProtection === true;
3687
3717
  const serverAssistedRecovery = true;
3688
3718
  const RECOVERY_OFFER_TIMEOUT_MS = 5 * 6e4;
3689
3719
  const selfHealAttempted = /* @__PURE__ */ new Set();
@@ -3743,12 +3773,13 @@ async function createDecaneConnect(config) {
3743
3773
  })
3744
3774
  ]).finally(() => clearTimeout(timer));
3745
3775
  }
3746
- const SESSION_KEY = sessionPersistKey(config.apiKey);
3776
+ const SESSION_KEY = sessionPersistKey(config.appId);
3777
+ const LEGACY_SESSION_KEY = sessionPersistKey(config.apiKey);
3747
3778
  function persistLiveSession() {
3748
- if (!persistSession) return;
3779
+ if (!persistSession || !auth) return;
3749
3780
  const handle = session.getHandle();
3750
- const expiresAt = session.expiresAtTimestamp();
3751
- if (!handle || !auth || !expiresAt) return;
3781
+ const expiresAt = handle ? session.expiresAtTimestamp() : decodeAccessToken(auth.jwt)?.expiresAt ?? null;
3782
+ if (!expiresAt) return;
3752
3783
  saveSession(SESSION_KEY, {
3753
3784
  handle,
3754
3785
  jwt: auth.jwt,
@@ -3760,11 +3791,23 @@ async function createDecaneConnect(config) {
3760
3791
  function dropLiveSession() {
3761
3792
  clearSessionPersist(SESSION_KEY);
3762
3793
  }
3794
+ let accessTokenExpiry = null;
3795
+ function scheduleAccessTokenExpiry(jwt, expiresAt) {
3796
+ if (accessTokenExpiry) clearTimeout(accessTokenExpiry);
3797
+ accessTokenExpiry = setTimeout(() => {
3798
+ accessTokenExpiry = null;
3799
+ if (auth?.jwt !== jwt || session.isActive()) return;
3800
+ auth = null;
3801
+ dropLiveSession();
3802
+ events.emit("session-expired");
3803
+ }, Math.max(0, expiresAt - Date.now()));
3804
+ }
3763
3805
  if (persistSession) {
3764
- const saved = loadSession(SESSION_KEY);
3765
- if (saved?.handle && saved.expiresAt > Date.now() + 5e3) {
3806
+ const saved = loadSession(SESSION_KEY) ?? (LEGACY_SESSION_KEY !== SESSION_KEY ? loadSession(LEGACY_SESSION_KEY) : null);
3807
+ if (saved && saved.expiresAt > Date.now() + 5e3) {
3766
3808
  try {
3767
- session.resume(saved.handle, saved.addresses, saved.expiresAt);
3809
+ if (saved.handle) session.resume(saved.handle, saved.addresses, saved.expiresAt);
3810
+ else scheduleAccessTokenExpiry(saved.jwt, saved.expiresAt);
3768
3811
  auth = { jwt: saved.jwt, userId: saved.userId, addresses: saved.addresses };
3769
3812
  persisted = { userId: saved.userId, addresses: saved.addresses };
3770
3813
  } catch {
@@ -3773,6 +3816,7 @@ async function createDecaneConnect(config) {
3773
3816
  } else if (saved) {
3774
3817
  dropLiveSession();
3775
3818
  }
3819
+ if (LEGACY_SESSION_KEY !== SESSION_KEY) clearSessionPersist(LEGACY_SESSION_KEY);
3776
3820
  }
3777
3821
  events.on("session-expired", () => dropLiveSession());
3778
3822
  async function passwordSetup() {
@@ -3995,11 +4039,12 @@ async function createDecaneConnect(config) {
3995
4039
  }
3996
4040
  async function firstTimeSetup(userId, jwt) {
3997
4041
  prewarmAttestation(teeUrl);
3998
- let unlockMethod;
3999
- let wrappingKey;
4042
+ let unlockMethod = "password";
4043
+ let wrappingKey = null;
4000
4044
  let meta = {};
4001
4045
  let unlockTokenKey = null;
4002
- if (isPasskeySupported()) {
4046
+ if (deferDeviceProtection) {
4047
+ } else if (isPasskeySupported()) {
4003
4048
  const prfSalt = generateRandomBytes(32);
4004
4049
  try {
4005
4050
  const { credentialId, prfOutput } = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
@@ -4040,32 +4085,37 @@ async function createDecaneConnect(config) {
4040
4085
  } finally {
4041
4086
  zeroize(master);
4042
4087
  }
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);
4088
+ let wrappedDeviceShare = null;
4089
+ let iv = null;
4090
+ if (wrappingKey) {
4091
+ iv = generateRandomBytes(12);
4092
+ wrappedDeviceShare = await aesGcmEncrypt(
4093
+ wrappingKey,
4094
+ shareSet.device,
4095
+ iv
4096
+ );
4097
+ await storage.putShare({
4098
+ userId,
4099
+ wrappedDeviceShare,
4100
+ iv,
4101
+ unlockMethod,
4102
+ credentialId: meta.credentialId ? b64uToBuffer(meta.credentialId) : void 0,
4103
+ prfSalt: meta.prfSalt ? b64uToBuffer(meta.prfSalt) : void 0,
4104
+ pinSalt: meta.pinSalt ? b64uToBuffer(meta.pinSalt) : void 0,
4105
+ pinIterations: meta.pinIterations,
4106
+ passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
4107
+ passwordIterations: meta.passwordIterations,
4108
+ version: 1
4109
+ });
4110
+ if (unlockTokenKey) await attachUnlockToken(userId, jwt, unlockTokenKey);
4111
+ }
4063
4112
  const serverIv = generateRandomBytes(12);
4064
4113
  await withDeadline(api.putShare(jwt, {
4065
4114
  ciphertext: shareSet.server,
4066
4115
  iv: serverIv,
4067
4116
  wrappedShareMeta: {
4068
- unlockMethod
4117
+ // The server records this on the wallet_created event and nothing else.
4118
+ unlockMethod: deferDeviceProtection ? "deferred" : unlockMethod
4069
4119
  // credential IDs, prfSalt, pinSalt stay on-device — never sent to server
4070
4120
  },
4071
4121
  evmAddress,
@@ -4074,7 +4124,7 @@ async function createDecaneConnect(config) {
4074
4124
  }), remainingBudget(), "storing your wallet");
4075
4125
  zeroize(shareSet.server);
4076
4126
  const addresses = { evm: evmAddress, solana: solanaAddress, tron: tronAddress };
4077
- if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt) {
4127
+ if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt && wrappedDeviceShare && iv) {
4078
4128
  try {
4079
4129
  await api.putRecoveryBackup(jwt, {
4080
4130
  wrappedDeviceShare,
@@ -4157,7 +4207,7 @@ async function createDecaneConnect(config) {
4157
4207
  const tee = new TeeClient(teeUrl);
4158
4208
  const provisioned = await tee.provisionDeviceShare(jwt);
4159
4209
  if (provisioned) {
4160
- await persistProvisionedShare(userId, jwt, provisioned);
4210
+ if (!deferDeviceProtection) await persistProvisionedShare(userId, jwt, provisioned);
4161
4211
  let addrs;
4162
4212
  try {
4163
4213
  addrs = await session.start(provisioned, jwt, recordedAddresses);
@@ -4201,7 +4251,7 @@ async function createDecaneConnect(config) {
4201
4251
  });
4202
4252
  deviceShare = recovered.deviceShare;
4203
4253
  joinedViaPasskeySync = recovered.flow === "passkey-sync";
4204
- if (!await storage.getShare(userId) && isPasskeySupported()) {
4254
+ if (!deferDeviceProtection && !await storage.getShare(userId) && isPasskeySupported()) {
4205
4255
  await registerPasskeyAndBackup(userId, jwt, deviceShare);
4206
4256
  }
4207
4257
  } else {
@@ -4484,6 +4534,27 @@ async function createDecaneConnect(config) {
4484
4534
  const result = await api.authExternal(token, opts?.providerId);
4485
4535
  return handleAuthResult(result);
4486
4536
  },
4537
+ async connectWithAccessToken(token) {
4538
+ const claims = decodeAccessToken(token);
4539
+ if (!claims) throw new InvalidAccessTokenError("not a Decane access token");
4540
+ if (claims.expiresAt <= Date.now() + 5e3) throw new InvalidAccessTokenError("it has expired");
4541
+ const raw = await api.getAddresses(token);
4542
+ if (!raw.evm || !raw.solana) throw new InvalidAccessTokenError("this account holds no wallet yet");
4543
+ const addresses = { evm: raw.evm, solana: raw.solana, tron: raw.tron ?? evmToTronAddress2(raw.evm) };
4544
+ if (session.isActive()) session.end();
4545
+ auth = { jwt: token, userId: claims.userId, addresses };
4546
+ persisted = { userId: claims.userId, addresses };
4547
+ savePersisted(PERSIST_KEY, persisted);
4548
+ events.emit("connected");
4549
+ persistLiveSession();
4550
+ scheduleAccessTokenExpiry(token, claims.expiresAt);
4551
+ const stored = await storage.getShare(claims.userId);
4552
+ return {
4553
+ addresses,
4554
+ isNewUser: false,
4555
+ ...stored?.profile ? { profile: stored.profile } : {}
4556
+ };
4557
+ },
4487
4558
  async connectWithEmail(email) {
4488
4559
  await startEmailAuth(api, email);
4489
4560
  return { codeRequired: true };
@@ -4672,6 +4743,24 @@ async function createDecaneConnect(config) {
4672
4743
  zeroize(deviceShare);
4673
4744
  }
4674
4745
  },
4746
+ async deviceProtected() {
4747
+ if (!auth) return false;
4748
+ return await storage.getShare(auth.userId) !== null;
4749
+ },
4750
+ async protectDevice() {
4751
+ if (!auth) throw new ReconnectRequiredError();
4752
+ if (await storage.getShare(auth.userId)) return true;
4753
+ if (!serverAssistedRecovery) throw new NewDeviceError();
4754
+ const tee = new TeeClient(teeUrl);
4755
+ const provisioned = await tee.provisionDeviceShare(auth.jwt);
4756
+ if (!provisioned) throw new NewDeviceError();
4757
+ try {
4758
+ await persistProvisionedShare(auth.userId, auth.jwt, provisioned);
4759
+ } finally {
4760
+ zeroize(provisioned);
4761
+ }
4762
+ return await storage.getShare(auth.userId) !== null;
4763
+ },
4675
4764
  async unlock() {
4676
4765
  if (!auth) {
4677
4766
  const candidate = await passkeyLoginCandidate();
@@ -7301,6 +7390,7 @@ function DecaneKit({ config, children }) {
7301
7390
  chains,
7302
7391
  rpcUrls,
7303
7392
  requireAssertionPerSignature,
7393
+ deferDeviceProtection,
7304
7394
  // Recovery callbacks. Forwarding these is what makes recovery
7305
7395
  // configurable at all from <DecaneKit>; before, they were silently
7306
7396
  // dropped and the headless client was constructed without them.
@@ -7321,6 +7411,7 @@ function DecaneKit({ config, children }) {
7321
7411
  promptPin,
7322
7412
  promptUnlockPassword,
7323
7413
  requireAssertionPerSignature,
7414
+ deferDeviceProtection,
7324
7415
  offerRecoveryAtSignup,
7325
7416
  onRecoveryShareOffer,
7326
7417
  onRecoveryFileReady,
@@ -7456,6 +7547,18 @@ function DecaneKit({ config, children }) {
7456
7547
  throw e;
7457
7548
  }
7458
7549
  }, [clearPhase]);
7550
+ const connectWithAccessToken = useCallback3(async (token) => {
7551
+ const sdk = socialRef.current;
7552
+ if (!sdk) throw new Error("Social sign-in not configured");
7553
+ try {
7554
+ const result = await sdk.connectWithAccessToken(token);
7555
+ applyResult(result);
7556
+ setSelectorOpen(false);
7557
+ } catch (e) {
7558
+ clearPhase();
7559
+ throw e;
7560
+ }
7561
+ }, [clearPhase]);
7459
7562
  const customAuthConfig = config?.social?.customAuth;
7460
7563
  const connectWithCustomAuth = useCallback3(async () => {
7461
7564
  if (!customAuthConfig) throw new Error("Custom auth not configured");
@@ -7567,6 +7670,16 @@ function DecaneKit({ config, children }) {
7567
7670
  if (!sdk) throw new Error("Social wallet not connected");
7568
7671
  return sdk.addPasskey();
7569
7672
  }, []);
7673
+ const socialDeviceProtected = useCallback3(async () => {
7674
+ const sdk = socialRef.current;
7675
+ if (!sdk) return false;
7676
+ return sdk.deviceProtected();
7677
+ }, []);
7678
+ const socialProtectDevice = useCallback3(async () => {
7679
+ const sdk = socialRef.current;
7680
+ if (!sdk) throw new Error("Social wallet not connected");
7681
+ return sdk.protectDevice();
7682
+ }, []);
7570
7683
  const socialSetUnlockPassword = useCallback3(async (password) => {
7571
7684
  const sdk = socialRef.current;
7572
7685
  if (!sdk) throw new Error("Social wallet not connected");
@@ -7638,6 +7751,7 @@ function DecaneKit({ config, children }) {
7638
7751
  connectWithPhone,
7639
7752
  verifyPhoneCode: verifyPhoneCode2,
7640
7753
  connectWithToken,
7754
+ connectWithAccessToken,
7641
7755
  customAuthLabel: customAuthConfig?.label,
7642
7756
  connectWithCustomAuth: customAuthConfig ? connectWithCustomAuth : void 0,
7643
7757
  socialUnlock,
@@ -7659,6 +7773,8 @@ function DecaneKit({ config, children }) {
7659
7773
  socialHasRecoveryShare,
7660
7774
  socialHasPasskey,
7661
7775
  socialAddPasskey,
7776
+ socialDeviceProtected,
7777
+ socialProtectDevice,
7662
7778
  socialSetUnlockPassword,
7663
7779
  socialCanUnlockWithPassword,
7664
7780
  socialForgetLastUser,
@@ -7751,6 +7867,7 @@ function useSocialAuth() {
7751
7867
  connectWithPhone,
7752
7868
  verifyPhoneCode: verifyPhoneCode2,
7753
7869
  connectWithToken,
7870
+ connectWithAccessToken,
7754
7871
  customAuthLabel,
7755
7872
  connectWithCustomAuth,
7756
7873
  signInWithPasskey: ctxSignInWithPasskey,
@@ -7859,6 +7976,18 @@ function useSocialAuth() {
7859
7976
  setTokenLoading(false);
7860
7977
  }
7861
7978
  }, [connectWithToken]);
7979
+ const signInWithAccessToken = useCallback4(async (token) => {
7980
+ setTokenLoading(true);
7981
+ setError(null);
7982
+ try {
7983
+ await connectWithAccessToken(token);
7984
+ } catch (e) {
7985
+ setError(e.message);
7986
+ throw e;
7987
+ } finally {
7988
+ setTokenLoading(false);
7989
+ }
7990
+ }, [connectWithAccessToken]);
7862
7991
  const signInWithCustomAuth = useCallback4(async () => {
7863
7992
  if (!connectWithCustomAuth) throw new Error("Custom auth not configured");
7864
7993
  setCustomLoading(true);
@@ -7941,6 +8070,7 @@ function useSocialAuth() {
7941
8070
  sendPhoneCode,
7942
8071
  confirmPhoneCode,
7943
8072
  signInWithToken,
8073
+ signInWithAccessToken,
7944
8074
  signInWithCustomAuth: connectWithCustomAuth ? signInWithCustomAuth : void 0,
7945
8075
  signInWithPasskey,
7946
8076
  unlock,
@@ -7969,6 +8099,8 @@ function useSocialWallet() {
7969
8099
  socialHasRecoveryShare,
7970
8100
  socialHasPasskey,
7971
8101
  socialAddPasskey,
8102
+ socialDeviceProtected,
8103
+ socialProtectDevice,
7972
8104
  socialSetUnlockPassword,
7973
8105
  socialCanUnlockWithPassword,
7974
8106
  socialForgetLastUser,
@@ -8071,6 +8203,16 @@ function useSocialWallet() {
8071
8203
  throw e;
8072
8204
  }
8073
8205
  }, [socialAddPasskey]);
8206
+ const deviceProtected = useCallback4(() => run(() => socialDeviceProtected()), [socialDeviceProtected]);
8207
+ const protectDevice = useCallback4(async () => {
8208
+ setError(null);
8209
+ try {
8210
+ return await socialProtectDevice();
8211
+ } catch (e) {
8212
+ setError(e.message);
8213
+ throw e;
8214
+ }
8215
+ }, [socialProtectDevice]);
8074
8216
  const setUnlockPassword = useCallback4(async (password) => {
8075
8217
  setError(null);
8076
8218
  try {
@@ -8159,6 +8301,10 @@ function useSocialWallet() {
8159
8301
  // Passkey management for THIS device
8160
8302
  hasPasskey,
8161
8303
  addPasskey,
8304
+ // Deferred protection (deferDeviceProtection): whether this device holds
8305
+ // a wrapped share, and the call that stores one — passkey or password.
8306
+ deviceProtected,
8307
+ protectDevice,
8162
8308
  // "Unlock with your password": setUnlockPassword needs a live session;
8163
8309
  // canUnlockWithPassword and unlockWithPassword are the signed-out pair that
8164
8310
  // open one without an identity provider or a passkey.
@@ -8473,6 +8619,7 @@ export {
8473
8619
  DecaneMark,
8474
8620
  DecaneWordmark,
8475
8621
  ExportNotAuthorizedError,
8622
+ InvalidAccessTokenError,
8476
8623
  InvalidApiKeyError,
8477
8624
  InvalidPinError,
8478
8625
  InvalidRecoveryFileError,