decane-connect-kit 2.7.5 → 2.8.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.ts CHANGED
@@ -1,258 +1,569 @@
1
1
  import * as _simplewebauthn_browser from '@simplewebauthn/browser';
2
2
  import React, { ReactNode } from 'react';
3
3
 
4
- type ChainType = "evm" | "solana" | "tron" | "bitcoin";
5
- type ChainSupport = ChainType[];
6
- interface ChainDescriptor {
7
- id: string;
8
- type: ChainType;
9
- name: string;
10
- nativeCurrency: {
11
- name: string;
12
- symbol: string;
13
- decimals: number;
4
+ interface RecoveryShareFile {
5
+ version: number;
6
+ type: 'decane-recovery-share';
7
+ shareSetVersion: number;
8
+ kdf: 'pbkdf2-sha256';
9
+ kdfParams: {
10
+ iterations: number;
11
+ salt: string;
14
12
  };
15
- rpcUrl?: string;
16
- blockExplorerUrl?: string;
17
- iconUrl?: string;
18
- chainId?: number;
19
- isCustom?: boolean;
20
- }
21
- interface UnifiedWalletAccount {
22
- id: string;
23
- name: string;
24
- icon: string;
25
- rdns?: string;
26
- chainSupport: ChainSupport;
27
- evmProvider?: EIP1193Provider;
28
- solanaProvider?: StandardWallet;
29
- tronProvider?: TronProvider;
30
- bitcoinProvider?: BitcoinProvider;
31
- }
32
- type CAIP25Method = "wallet_requestPermissions" | "wallet_revokePermissions" | "wallet_getPermissions" | "wallet_sendTransaction" | "wallet_signMessage" | "wallet_signTypedData";
33
- interface ChainScope {
34
- methods: CAIP25Method[];
35
- accounts: string[];
36
- events?: string[];
37
- }
38
- interface CAIP25SessionRequest {
39
- requiredScopes: Partial<Record<string, ChainScope>>;
40
- optionalScopes: Partial<Record<string, ChainScope>>;
13
+ cipher: 'aes-256-gcm';
14
+ iv: string;
15
+ ciphertext: string;
16
+ addresses: {
17
+ evm: string;
18
+ solana: string;
19
+ };
20
+ passwordHint: string | null;
21
+ createdAt: string;
41
22
  }
42
- interface CAIP25SessionResult {
43
- sessionId: string;
44
- grantedScopes: Partial<Record<string, ChainScope>>;
23
+
24
+ interface PortableBackup {
25
+ version: number;
26
+ type: 'decane-portable-backup';
27
+ shareSetVersion: number;
28
+ kdf: 'pbkdf2-sha256';
29
+ kdfParams: {
30
+ iterations: number;
31
+ salt: string;
32
+ };
33
+ cipher: 'aes-256-gcm';
34
+ iv: string;
35
+ ciphertext: string;
36
+ addresses: {
37
+ evm: string;
38
+ solana: string;
39
+ };
40
+ passwordHint: string | null;
41
+ createdAt: string;
45
42
  }
46
- interface ActiveSession {
47
- sessionId: string;
48
- grantedScopes: Partial<Record<string, ChainScope>>;
49
- walletId: string;
50
- walletType: ChainType | "multi";
51
- createdAt: number;
43
+
44
+ interface BalanceResult {
45
+ /** Smallest unit — wei for EVM, lamports for Solana. */
46
+ raw: bigint;
47
+ /** Human-readable amount in whole native units, e.g. "1.2345". */
48
+ formatted: string;
49
+ /** Native currency symbol, e.g. "ETH", "MATIC", "SOL". */
50
+ symbol: string;
51
+ /** Decimals of the native currency (18 for EVM, 9 for Solana). */
52
+ decimals: number;
52
53
  }
53
- interface DecaneProvider {
54
+
55
+ interface Eip1193Provider {
54
56
  request(args: {
55
57
  method: string;
56
- params?: unknown[];
58
+ params?: unknown[] | object;
57
59
  }): Promise<unknown>;
58
- on(event: string, handler: (...args: unknown[]) => void): void;
59
- off(event: string, handler: (...args: unknown[]) => void): void;
60
- readonly wallets: UnifiedWalletAccount[];
61
- readonly evmWallets: UnifiedWalletAccount[];
62
- readonly solanaWallets: UnifiedWalletAccount[];
63
- readonly multiChainWallets: UnifiedWalletAccount[];
64
- readonly tronWallets: UnifiedWalletAccount[];
65
- readonly bitcoinWallets: UnifiedWalletAccount[];
66
- connect(wallet: UnifiedWalletAccount, preferredChain?: ChainType): Promise<void>;
67
- disconnect(): Promise<void>;
68
- addChain(chain: ChainDescriptor): void;
69
- removeChain(chainId: string): void;
70
- getChain(chainId: string): ChainDescriptor | undefined;
71
- readonly chains: ChainDescriptor[];
72
- readonly connectedWallet: UnifiedWalletAccount | null;
73
- readonly activeSession: ActiveSession | null;
74
- readonly activeChain: string | null;
60
+ on(event: string, handler: (...args: unknown[]) => void): this;
61
+ removeListener(event: string, handler: (...args: unknown[]) => void): this;
62
+ /** Marks this as the Decane provider (parallel to wallets' isMetaMask etc.). */
63
+ readonly isDecane: true;
75
64
  }
76
- type ConnectMode = "all" | "social" | "wallets" | "evm" | "solana" | "tron" | "evm+social" | "solana+social" | "tron+social" | "evm+tron" | "evm+tron+social";
77
- interface SocialConfig {
65
+ interface EthereumProviderOptions {
66
+ /** Initial chain id (numeric). Defaults to the first configured EVM chain, else 1. */
67
+ chainId?: number;
68
+ /** Per-chain RPC overrides ('evm:8453' -> url). Falls back to Decane's defaults. */
69
+ rpcUrls?: Record<string, string>;
70
+ }
71
+ /**
72
+ * Build an EIP-1193 provider over a connected Decane social client.
73
+ *
74
+ * `signAuthorization` (EIP-7702) is wired only when the client exposes it — that
75
+ * requires the TEE signer to support the authorization signing path.
76
+ */
77
+ declare function createEthereumProvider(client: DecaneConnect, opts?: EthereumProviderOptions): Eip1193Provider;
78
+
79
+ interface SignedAuthorization {
80
+ chainId: number;
81
+ address: string;
82
+ nonce: number;
83
+ yParity: number;
84
+ r: string;
85
+ s: string;
86
+ }
87
+
88
+ type Chain = `evm:${number}` | `solana:${'mainnet' | 'devnet'}` | `tron:${'mainnet' | 'shasta' | 'nile'}`;
89
+ interface DecaneConnectConfig {
90
+ /** Dashboard project ID (public). Sent with every request so the backend can
91
+ * confirm the API key below actually belongs to this project. */
92
+ appId: string;
78
93
  apiKey: string;
79
- authMethods?: ("google" | "email" | "kingschat")[];
94
+ chains: Chain[];
95
+ authMethods: Array<'google' | 'email' | 'phone' | 'kingschat'>;
80
96
  /**
81
- * Scopes requested from KingsChat. Defaults to `['send_chat_message']`, which
82
- * is what KingsChat's own SDK asks for and is enough to establish identity.
97
+ * Scopes requested from KingsChat at sign-in. Defaults to
98
+ * `['send_chat_message']`, which is what KingsChat's own SDK asks for and is
99
+ * enough to establish identity. Widen only if the app actually calls
100
+ * KingsChat APIs on the user's behalf — the consent screen lists every scope,
101
+ * so asking for more than you use costs conversions.
83
102
  */
84
103
  kingsChatScopes?: string[];
85
- promptPin?: () => Promise<string>;
86
104
  apiBaseUrl?: string;
87
- chains?: string[];
88
- rpcUrls?: Record<string, string>;
105
+ sessionDurationMinutes?: number;
106
+ promptPin?: () => Promise<string>;
89
107
  /**
90
- * Show the built-in full-screen status overlay while a wallet is being
91
- * created or unlocked. Default **true**.
108
+ * Require a fresh WebAuthn assertion for every signature, instead of letting
109
+ * an unlocked session sign freely until it expires. The TEE session stays
110
+ * warm — this costs one authenticator prompt per signature, not a re-auth or
111
+ * re-attestation — and the check is enforced inside the enclave, not here.
92
112
  *
93
- * The overlay exists because those flows can take several seconds and often
94
- * run right after a Google redirect, when no modal of yours is mounted — so
95
- * without it the page can look frozen. It is rendered by `<DecaneKit>` itself,
96
- * as a portal, and injects its own theme tokens.
113
+ * Default false, which preserves the session-signing model: unlock once, sign
114
+ * for the session's lifetime with no further prompts.
97
115
  *
98
- * Set false to suppress it and render your own. Everything it displays is
99
- * already available to you: `useSocialAuth()` exposes `phase`
100
- * (`"creating" | "unlocking" | null`) plus the `isCreatingWallet` /
101
- * `isUnlockingWallet` booleans, and the client emits `wallet-creating` and
102
- * `wallet-unlocking` events. If you suppress it, show something of your own —
103
- * a silent multi-second pause reads as a broken page.
116
+ * Two things worth knowing before turning this on:
117
+ * - Multi-step flows prompt per leg. An ERC-20 approve + swap is two
118
+ * prompts, not one.
119
+ * - The policy is declared by the client at session open, so it hardens a
120
+ * leaked session handle (useless without the authenticator) but a fully
121
+ * compromised page could open a session without it. Making it
122
+ * unbypassable requires the policy to come from the server per user or
123
+ * per project; that is not implemented yet.
104
124
  */
105
- showStatusOverlay?: boolean;
125
+ requireAssertionPerSignature?: boolean;
126
+ rpId?: string;
127
+ rpName?: string;
128
+ rpcUrls?: Record<string, string>;
129
+ teeUrl?: string;
106
130
  /**
107
- * Require a fresh WebAuthn assertion for every signature rather than letting
108
- * an unlocked session sign freely until it expires. Enforced inside the TEE.
131
+ * Persist the live session (handle + JWT) in sessionStorage so a page RELOAD
132
+ * silently resumes it — no repeat passkey/sign-in. Survives reloads, cleared
133
+ * on tab close. Default true. Set false to require a fresh unlock on every
134
+ * page load (stricter, but the reload friction that motivated this returns).
135
+ */
136
+ persistSession?: boolean;
137
+ /**
138
+ * Offer to save a recovery-share file at signup. **Default false** since
139
+ * 2.7.4 — opt in explicitly with `true`.
109
140
  *
110
- * Does not change the session lifetime — the session stays warm (no
111
- * re-attestation, no re-auth) and authority is re-proven per signature
112
- * instead. Default false. Needs a passkey; PIN-only wallets have no
113
- * authenticator to assert with. See SocialConfig's counterpart in
114
- * social/types.ts for the full caveats.
141
+ * The offer existed because a new device could not otherwise get a device
142
+ * share. Server-assisted provisioning now covers that case: every wallet is
143
+ * enrolled at creation, un-enrolled wallets repair themselves on the next
144
+ * unlock, and a returning user is provisioned from their sign-in alone. For
145
+ * almost every app the dialog is now a password prompt during onboarding that
146
+ * buys nothing, so it is off unless asked for.
147
+ *
148
+ * Recovery files themselves are unchanged and still work — `rotateShares()`
149
+ * issues one on demand, and `promptForRecoveryFile` still accepts one during
150
+ * recovery. Turn this back on if you want users to hold an offline fallback
151
+ * that does not depend on Decane being reachable.
115
152
  */
116
- requireAssertionPerSignature?: boolean;
153
+ offerRecoveryAtSignup?: boolean;
154
+ /** Ask the user whether they want a recovery share and, if so, a password for it. */
155
+ onRecoveryShareOffer?: (addresses: {
156
+ evmAddress: string;
157
+ solanaAddress: string;
158
+ }) => Promise<{
159
+ wants: boolean;
160
+ password: string;
161
+ passwordHint?: string;
162
+ }>;
163
+ /** Hand the built file + suggested filename to the dApp, which owns the actual download. */
164
+ onRecoveryFileReady?: (file: RecoveryShareFile, filename: string) => Promise<void>;
165
+ /** Prompt for a recovery-share file (and its password) during Flow B. Return null to cancel. */
166
+ promptForRecoveryFile?: () => Promise<{
167
+ value: unknown;
168
+ getPassword: () => Promise<string | null>;
169
+ } | null>;
117
170
  /**
118
- * Sign in with a token from your own auth system (Auth0, Clerk, Firebase,
119
- * Cognito, or your own backend) — surfaced as a button in the built-in
120
- * WalletSelector / SocialWalletModal UI, alongside Google and email.
121
- * getToken() runs when the user clicks the button; wire it to however you
122
- * already obtain a token from your auth system (e.g. after your own login
123
- * flow completes). Configure the issuer and JWKS URL for this token
124
- * against your project in the dashboard first. `providerId` is only
125
- * needed when the project has more than one custom provider configured.
171
+ * Every successful recovery/rotation invalidates the previous recovery
172
+ * file — this MUST prompt for a new password and block progress until the
173
+ * dApp confirms the user saved the new file (see MIGRATE_SSS_RECOVERY.md §10).
126
174
  */
127
- customAuth?: {
128
- label: string;
129
- providerId?: string;
130
- getToken: () => Promise<string>;
131
- };
132
- }
133
- interface DecaneConfig {
134
- appId: string;
135
- chains?: ChainDescriptor[];
136
- supportedChainIds?: string[];
137
- networks?: "evm" | "solana" | "tron" | "bitcoin" | "all";
138
- defaultChain?: string;
139
- theme?: "light" | "dark" | "auto";
140
- mode?: ConnectMode;
141
- social?: SocialConfig;
175
+ onRecoveryRotated?: (addresses: {
176
+ evmAddress: string;
177
+ solanaAddress: string;
178
+ }) => Promise<{
179
+ password: string;
180
+ passwordHint?: string;
181
+ }>;
182
+ /** Recovery stage callbacks, for dApp UI. */
183
+ onRecoveryProgress?: (event: RecoveryProgress) => void;
184
+ /**
185
+ * Rotate the whole share set whenever a NEW device recovers via passkey sync
186
+ * (Flow A) — the "every device holds a distinct share" model.
187
+ *
188
+ * Default false, which preserves the historical behaviour: every device
189
+ * synced to the same passkey caches the byte-identical wrapped share, so a
190
+ * device that is later lost keeps a share that stays valid until someone
191
+ * explicitly calls rotateShares(). There is no way to revoke one device
192
+ * without revoking all of them.
193
+ *
194
+ * With this on, joining a new device re-splits the master and issues that
195
+ * device its own share, which invalidates every other device's cached copy
196
+ * at the same instant. That is what makes per-device revocation possible,
197
+ * and it has three consequences worth accepting deliberately:
198
+ *
199
+ * - **Other devices are logged out, not just the lost one.** Each surviving
200
+ * device hits StaleDeviceShareError on its next unlock, clears its share,
201
+ * and silently re-recovers through Flow A (one passkey prompt). Expected,
202
+ * but it means a shared-family-device setup re-prompts more often.
203
+ * - **The user's recovery FILE dies too.** Rotation issues a new recovery
204
+ * share, so `onRecoveryRotated` MUST be configured to capture it —
205
+ * enabling this without that callback throws at connect time rather than
206
+ * stranding the user with a dead file.
207
+ * - **Adding a device costs a TEE round trip.** Recovery is slower.
208
+ */
209
+ rotateOnNewDeviceRecovery?: boolean;
142
210
  }
143
- interface EIP1193Provider {
144
- request(args: {
145
- method: string;
146
- params?: unknown[];
147
- }): Promise<unknown>;
148
- on(event: string, handler: (...args: unknown[]) => void): void;
149
- removeListener(event: string, handler: (...args: unknown[]) => void): void;
211
+ interface ConnectResult {
212
+ addresses: {
213
+ evm?: string;
214
+ solana?: string;
215
+ tron?: string;
216
+ };
217
+ isNewUser: boolean;
218
+ /** Present when the provider released a profile — see {@link AuthProfile}. */
219
+ profile?: AuthProfile;
150
220
  }
151
- interface StandardWallet {
152
- name: string;
153
- icon: string;
154
- features: Record<string, unknown>;
155
- accounts: StandardWalletAccount[];
221
+ /** A recorded on-chain transaction in the user's history. */
222
+ interface TransactionRecord {
223
+ id: string;
224
+ chain: string;
225
+ kind: string;
226
+ txHash: string | null;
227
+ to: string | null;
228
+ value: string | null;
229
+ status: 'submitted' | 'confirmed' | 'failed' | string;
230
+ metadata: Record<string, unknown> | null;
231
+ createdAt: string;
232
+ updatedAt?: string;
156
233
  }
157
- interface StandardWalletAccount {
158
- address: string;
159
- publicKey: Uint8Array;
160
- chains: string[];
234
+ /** What the dApp passes to record a transaction it broadcast. */
235
+ interface RecordTransactionInput {
236
+ chain: string;
237
+ txHash?: string;
238
+ to?: string;
239
+ value?: string;
240
+ kind?: string;
241
+ status?: string;
242
+ metadata?: Record<string, unknown>;
161
243
  }
162
- interface TronProvider {
163
- request(args: {
164
- method: string;
165
- params?: unknown[] | Record<string, unknown>;
166
- }): Promise<unknown>;
167
- on(event: string, handler: (...args: unknown[]) => void): void;
168
- removeListener(event: string, handler: (...args: unknown[]) => void): void;
244
+ interface SignMessageRequest {
245
+ chain: Chain;
246
+ message: string | Uint8Array;
169
247
  }
170
- interface BitcoinProvider {
171
- requestAccounts(): Promise<string[] | Array<{
172
- address: string;
173
- }>>;
174
- getAccounts?(): Promise<string[] | Array<{
175
- address: string;
176
- }>>;
177
- signMessage(message: string, ...args: unknown[]): Promise<string>;
178
- sendBitcoin?(toAddress: string, satoshis: number, options?: unknown): Promise<string>;
179
- send?(params: unknown): Promise<string>;
180
- signPsbt?(psbtHex: string, options?: unknown): Promise<string>;
181
- on?(event: string, handler: (...args: unknown[]) => void): void;
248
+ interface SendTransactionRequest {
249
+ chain: Chain;
250
+ to: string;
251
+ value?: bigint;
252
+ data?: string;
253
+ gasLimit?: bigint;
254
+ gasPrice?: bigint;
182
255
  }
183
-
184
- declare function createDecane(config: DecaneConfig): DecaneProvider;
185
-
186
- declare class ApiClient {
187
- private readonly baseUrl;
188
- private readonly apiKey;
189
- private readonly appId;
190
- constructor(baseUrl: string, apiKey: string, appId: string);
191
- private _request;
192
- getGoogleInitUrl(): Promise<{
193
- url: string;
194
- }>;
256
+ interface SignTypedDataRequest {
257
+ chain: Chain;
258
+ domain: Record<string, unknown>;
259
+ types: Record<string, unknown>;
260
+ message: Record<string, unknown>;
261
+ primaryType?: string;
262
+ }
263
+ type SdkEvent = 'connected' | 'disconnected' | 'session-expired' | 'session-started' | 'wallet-creating' | 'wallet-unlocking'
264
+ /**
265
+ * Emitted when this wallet could not be enrolled for server-assisted
266
+ * recovery. Not fatal — the wallet works — but until it is enrolled the user
267
+ * cannot get a device share from login alone, so a future new device will
268
+ * need a passkey backup or a recovery file. The SDK retries automatically on
269
+ * later unlocks; surface or report this if you want visibility rather than a
270
+ * console line.
271
+ */
272
+ | 'enrolment-failed';
273
+ interface DecaneConnect {
274
+ connect(): Promise<ConnectResult>;
275
+ connectWithGoogle(): Promise<ConnectResult>;
195
276
  /**
196
- * Exchange a Google ID token the app already holds for a Decane session.
197
- * Resolves to the SAME identity as the redirect flow, so one Google account
198
- * means one wallet across web and mobile.
277
+ * Sign in with a Google ID token the app already obtained — native Google
278
+ * Sign-In on mobile, or Google Identity Services on the web.
279
+ *
280
+ * Resolves to the SAME wallet as connectWithGoogle(): both end at the
281
+ * backend's Google identity, so a user who signs in on your website and then
282
+ * on your app lands on one wallet, not two.
199
283
  */
200
- authGoogleToken(idToken: string): Promise<AuthResult>;
201
- /** The project's KingsChat clientId, so the dApp never has to hardcode it. */
202
- getKingsChatConfig(): Promise<{
203
- clientId: string;
204
- authorizationUrl: string;
284
+ connectWithGoogleToken(idToken: string): Promise<ConnectResult>;
285
+ /**
286
+ * Sign in with KingsChat.
287
+ *
288
+ * Opens KingsChat's consent screen in a popup, so this MUST be called
289
+ * directly from a click handler — a popup opened after an await is blocked by
290
+ * every browser. The resulting access token is exchanged server-side for a
291
+ * Decane session; it is never treated as one on its own.
292
+ */
293
+ connectWithKingsChat(): Promise<ConnectResult>;
294
+ /**
295
+ * Sign in with a KingsChat access token the app already holds — from
296
+ * kingschat-web-sdk, a native SDK, or any other flow. Use when you want to
297
+ * own the consent step yourself; connectWithKingsChat() is the turnkey path.
298
+ */
299
+ connectWithKingsChatToken(accessToken: string): Promise<ConnectResult>;
300
+ connectWithEmail(email: string): Promise<{
301
+ codeRequired: true;
205
302
  }>;
206
303
  /**
207
- * Exchange a KingsChat access token for a Decane session. The backend spends
208
- * the token against KingsChat's own API to establish the identity — it is
209
- * never trusted on its face, and never stored.
304
+ * Sign in with a JWT from your own auth system (Auth0, Clerk, Firebase,
305
+ * Cognito, your own backend — anything that publishes a JWKS endpoint).
306
+ *
307
+ * Configure the issuer and JWKS URL against your project in the dashboard;
308
+ * no client-side setup and no secret in your dApp. `providerId` is only
309
+ * required if the project has more than one provider configured.
310
+ *
311
+ * The user's wallet is derived from the configured identity claim, so the
312
+ * same token subject always resolves to the same wallet.
210
313
  */
211
- authKingsChat(accessToken: string): Promise<AuthResult>;
212
- authEmailStart(email: string): Promise<void>;
213
- authEmailVerify(email: string, code: string): Promise<AuthResult>;
214
- getAddresses(jwt: string): Promise<{
215
- evm: string | null;
216
- solana: string | null;
217
- tron: string | null;
314
+ connectWithToken(token: string, opts?: {
315
+ providerId?: string;
316
+ }): Promise<ConnectResult>;
317
+ verifyEmailCode(email: string, code: string): Promise<ConnectResult>;
318
+ /**
319
+ * Send a 6-digit sign-in code by SMS (delivered via Termii).
320
+ *
321
+ * `phone` must be **E.164** — `+2348012345678`. Local formats like
322
+ * `08012345678` are rejected rather than guessed at: the wallet is derived
323
+ * from the number, so guessing a country would either merge two people onto
324
+ * one wallet or split one person across two. Collect the country in your UI.
325
+ *
326
+ * Always resolves, whether or not the number has an account — the backend
327
+ * answers identically either way so this cannot be used to discover who is
328
+ * registered. Follow with {@link verifyPhoneCode}.
329
+ */
330
+ connectWithPhone(phone: string): Promise<{
331
+ codeRequired: true;
218
332
  }>;
219
- putShare(jwt: string, share: {
220
- ciphertext: Uint8Array;
221
- iv: Uint8Array;
222
- wrappedShareMeta: WrappedShareMeta;
223
- evmAddress: string;
224
- solanaAddress: string;
225
- tronAddress: string;
226
- }): Promise<void>;
227
- recordTransaction(jwt: string, tx: {
228
- chain: string;
229
- txHash?: string;
230
- to?: string;
231
- value?: string;
232
- kind?: string;
233
- status?: string;
234
- metadata?: Record<string, unknown>;
235
- }): Promise<TransactionRecord>;
236
- getTransactions(jwt: string, limit?: number): Promise<TransactionRecord[]>;
237
- getWebAuthnRegisterOptions(jwt: string, rpId: string, rpName: string): Promise<_simplewebauthn_browser.PublicKeyCredentialCreationOptionsJSON>;
238
- postWebAuthnRegister(jwt: string, response: _simplewebauthn_browser.RegistrationResponseJSON, origin: string): Promise<void>;
333
+ /** Verify the SMS code and open the wallet. Pass the same E.164 number. */
334
+ verifyPhoneCode(phone: string, code: string): Promise<ConnectResult>;
335
+ unlock(): Promise<void>;
336
+ isUnlocked(): boolean;
239
337
  /**
240
- * Exchange a token from the developer's own auth system for a Decane
241
- * session. `providerId` is only needed when the project has more than one
242
- * provider configured.
338
+ * True when the user's identity is known from a previous visit but there is
339
+ * no live session, so unlock() cannot succeed and they must sign in again.
340
+ * Check this before offering an "Unlock" action.
341
+ *
342
+ * Note this can be true while canSignInWithPasskey() is also true — in which
343
+ * case "sign in again" costs one biometric prompt, not a full auth round
344
+ * trip. Prefer offering that over a Google/email button.
243
345
  */
244
- authExternal(token: string, providerId?: string): Promise<AuthResult>;
346
+ needsReconnect(): boolean;
245
347
  /**
246
- * Returns null when this user has no server-registered passkey, which is the
247
- * expected answer for wallets created before passkey sign-in existed or for
248
- * PIN-only users — the caller falls back to the normal sign-in flow.
348
+ * True when a returning user can be signed straight back in with the passkey
349
+ * already on this device — no Google redirect, no email code. Requires a
350
+ * remembered identity plus a passkey-wrapped device share in local storage,
351
+ * so it is false on new devices and for PIN-only wallets.
249
352
  */
250
- getWebAuthnLoginOptions(userId: string, rpId: string): Promise<_simplewebauthn_browser.PublicKeyCredentialRequestOptionsJSON | null>;
251
- postWebAuthnLogin(userId: string, response: _simplewebauthn_browser.AuthenticationResponseJSON, origin: string): Promise<AuthResult>;
252
- revoke(jwt: string): Promise<void>;
253
- hasRecoveryBackup(jwt: string): Promise<boolean>;
254
- getRecoveryBackup(jwt: string): Promise<{
255
- wrappedDeviceShare: Uint8Array;
353
+ canSignInWithPasskey(): Promise<boolean>;
354
+ /**
355
+ * Sign in using only the device passkey. connect() already tries this first
356
+ * when it's available, so call this directly only when driving the UI
357
+ * yourself. Throws NewDeviceError when this device isn't eligible.
358
+ */
359
+ signInWithPasskey(): Promise<ConnectResult>;
360
+ sessionExpiresAt(): number | null;
361
+ signMessage(req: SignMessageRequest): Promise<string>;
362
+ sendTransaction(req: SendTransactionRequest): Promise<string>;
363
+ /** Sign EIP-712 typed data (EVM only). Throws on a Solana chain. */
364
+ signTypedData(req: SignTypedDataRequest): Promise<string>;
365
+ /**
366
+ * A standard EIP-1193 Ethereum provider backed by this wallet — drop it into
367
+ * viem (`custom(provider)`), wagmi, or ethers (`BrowserProvider`). Signing
368
+ * goes through the TEE; reads are forwarded to the chain RPC. Memoized, so
369
+ * repeated calls return the same instance (safe for wagmi).
370
+ */
371
+ getEthereumProvider(opts?: {
372
+ chainId?: number;
373
+ }): Eip1193Provider;
374
+ /**
375
+ * Sign an EIP-7702 authorization, delegating this EOA to a smart-account
376
+ * implementation contract on `chainId`. Enables account-abstraction /
377
+ * sponsored-gas flows. Returns the full viem `SignedAuthorization` tuple, so
378
+ * it drops straight into a userOperation's `authorization` field or a type-4
379
+ * transaction's authorizationList. `address` and `contractAddress` are
380
+ * interchangeable (the latter matches Privy's/older viem's param name).
381
+ */
382
+ signAuthorization(req: {
383
+ chainId: number;
384
+ address?: string;
385
+ contractAddress?: string;
386
+ nonce: number;
387
+ }): Promise<SignedAuthorization>;
388
+ /**
389
+ * The current session's access token (the Decane JWT). Use it to authenticate
390
+ * the user to your own backend (e.g. a sponsored-bundler proxy); verify it
391
+ * server-side against backend.decane.app/.well-known/jwks.json. Null with no
392
+ * live session.
393
+ */
394
+ getAccessToken(): string | null;
395
+ signSolanaTransaction(tx: Uint8Array): Promise<Uint8Array>;
396
+ /**
397
+ * Sign a Tron transaction. Pass the transaction's `raw_data_hex` (the value
398
+ * TronWeb puts on a built transaction). The enclave computes the txID
399
+ * (sha256(raw_data)) and signs it with the same secp256k1 key as the EVM
400
+ * wallet; returns the 65-byte recoverable signature as 0x-hex. Attach it to
401
+ * the transaction's `signature` array and broadcast via TronWeb / a full node.
402
+ */
403
+ signTronTransaction(rawDataHex: string): Promise<string>;
404
+ /**
405
+ * Record a transaction this wallet broadcast, so it appears in the user's
406
+ * history (and the developer dashboard). EVM `sendTransaction` records itself
407
+ * automatically; call this for Solana/Tron transactions your dApp broadcasts,
408
+ * or to attach a `confirmed`/`failed` status update to an existing hash.
409
+ */
410
+ recordTransaction(tx: RecordTransactionInput): Promise<TransactionRecord>;
411
+ /** The user's transaction history, most recent first. Requires a live session. */
412
+ getTransactionHistory(limit?: number): Promise<TransactionRecord[]>;
413
+ disconnect(): Promise<void>;
414
+ getAddresses(): {
415
+ evm?: string;
416
+ solana?: string;
417
+ tron?: string;
418
+ } | null;
419
+ /**
420
+ * Native-token balance for a chain (defaults to the first configured EVM
421
+ * chain, else Solana). Read-only public data — works even while the wallet
422
+ * is locked. Returns raw (wei/lamports), a human `formatted` string, the
423
+ * native `symbol`, and `decimals`.
424
+ */
425
+ getBalance(chain?: string): Promise<BalanceResult>;
426
+ /** Native balances across every configured chain the wallet has an address for. */
427
+ getBalances(): Promise<Array<{
428
+ chain: string;
429
+ balance: BalanceResult | null;
430
+ }>>;
431
+ /**
432
+ * Voluntary share rotation — after a suspected device compromise, or just
433
+ * to invalidate an old recovery file. Requires a fresh WebAuthn assertion.
434
+ * Returns the new recovery-share file (the caller still owns delivering it
435
+ * to the user — this does not download anything itself).
436
+ */
437
+ rotateShares(opts: {
438
+ password: string;
439
+ passwordHint?: string;
440
+ }): Promise<RecoveryShareFile>;
441
+ /**
442
+ * The two-share bearer portable backup (device + recovery). This file plus
443
+ * its password gives complete control of the wallet — see the mandatory
444
+ * warning-screen requirements in MIGRATE_SSS_RECOVERY.md §10. Requires a
445
+ * fresh WebAuthn assertion and rotates the share set on every export.
446
+ */
447
+ exportPortableBackup(opts: {
448
+ password: string;
449
+ passwordHint?: string;
450
+ }): Promise<PortableBackup>;
451
+ /** Whether the user has ever saved a recovery-share file (UX flag only — Decane never stores the share). */
452
+ hasRecoveryShare(): Promise<boolean>;
453
+ /** True at most once per account: no recovery share saved, never nagged, wallet holds value. */
454
+ shouldPromptRecovery(): Promise<boolean>;
455
+ /** Call regardless of the user's answer — the recovery nag fires once, ever. */
456
+ dismissRecoveryPrompt(): Promise<void>;
457
+ /** Whether a wrapped-share backup exists on the server. */
458
+ hasBackup(): Promise<boolean>;
459
+ /** Delete the server-side backup. Disables Flow A recovery. */
460
+ deleteBackup(): Promise<void>;
461
+ on(event: SdkEvent, handler: (...args: unknown[]) => void): void;
462
+ off(event: SdkEvent, handler: (...args: unknown[]) => void): void;
463
+ }
464
+ interface WrappedShareMeta {
465
+ unlockMethod: 'passkey' | 'pin';
466
+ credentialId?: string;
467
+ prfSalt?: string;
468
+ pinSalt?: string;
469
+ pinIterations?: number;
470
+ }
471
+ /**
472
+ * Who the user is, as the identity provider just described them.
473
+ *
474
+ * Passed through from the provider at sign-in and never stored by Decane — the
475
+ * backend keeps only an HMAC and a masked identifier, so there is no profile to
476
+ * look up later. Cache what you need at sign-in if you want to show a name
477
+ * after a page reload.
478
+ *
479
+ * Every field is optional because providers differ in what they release, and a
480
+ * user can withhold most of it. Email sign-in returns an empty profile.
481
+ */
482
+ interface AuthProfile {
483
+ name?: string;
484
+ email?: string;
485
+ picture?: string;
486
+ }
487
+ interface AuthResult {
488
+ jwt: string;
489
+ isNewUser: boolean;
490
+ hasShare: boolean;
491
+ userId: string;
492
+ profile?: AuthProfile;
493
+ }
494
+
495
+ declare class ApiClient {
496
+ private readonly baseUrl;
497
+ private readonly apiKey;
498
+ private readonly appId;
499
+ constructor(baseUrl: string, apiKey: string, appId: string);
500
+ private _request;
501
+ getGoogleInitUrl(): Promise<{
502
+ url: string;
503
+ }>;
504
+ /**
505
+ * Exchange a Google ID token the app already holds for a Decane session.
506
+ * Resolves to the SAME identity as the redirect flow, so one Google account
507
+ * means one wallet across web and mobile.
508
+ */
509
+ authGoogleToken(idToken: string): Promise<AuthResult>;
510
+ /** The project's KingsChat clientId, so the dApp never has to hardcode it. */
511
+ getKingsChatConfig(): Promise<{
512
+ clientId: string;
513
+ authorizationUrl: string;
514
+ }>;
515
+ /**
516
+ * Exchange a KingsChat access token for a Decane session. The backend spends
517
+ * the token against KingsChat's own API to establish the identity — it is
518
+ * never trusted on its face, and never stored.
519
+ */
520
+ authKingsChat(accessToken: string): Promise<AuthResult>;
521
+ authEmailStart(email: string): Promise<void>;
522
+ authEmailVerify(email: string, code: string): Promise<AuthResult>;
523
+ authPhoneStart(phone: string): Promise<void>;
524
+ authPhoneVerify(phone: string, code: string): Promise<AuthResult>;
525
+ getAddresses(jwt: string): Promise<{
526
+ evm: string | null;
527
+ solana: string | null;
528
+ tron: string | null;
529
+ }>;
530
+ putShare(jwt: string, share: {
531
+ ciphertext: Uint8Array;
532
+ iv: Uint8Array;
533
+ wrappedShareMeta: WrappedShareMeta;
534
+ evmAddress: string;
535
+ solanaAddress: string;
536
+ tronAddress: string;
537
+ }): Promise<void>;
538
+ recordTransaction(jwt: string, tx: {
539
+ chain: string;
540
+ txHash?: string;
541
+ to?: string;
542
+ value?: string;
543
+ kind?: string;
544
+ status?: string;
545
+ metadata?: Record<string, unknown>;
546
+ }): Promise<TransactionRecord>;
547
+ getTransactions(jwt: string, limit?: number): Promise<TransactionRecord[]>;
548
+ getWebAuthnRegisterOptions(jwt: string, rpId: string, rpName: string): Promise<_simplewebauthn_browser.PublicKeyCredentialCreationOptionsJSON>;
549
+ postWebAuthnRegister(jwt: string, response: _simplewebauthn_browser.RegistrationResponseJSON, origin: string): Promise<void>;
550
+ /**
551
+ * Exchange a token from the developer's own auth system for a Decane
552
+ * session. `providerId` is only needed when the project has more than one
553
+ * provider configured.
554
+ */
555
+ authExternal(token: string, providerId?: string): Promise<AuthResult>;
556
+ /**
557
+ * Returns null when this user has no server-registered passkey, which is the
558
+ * expected answer for wallets created before passkey sign-in existed or for
559
+ * PIN-only users — the caller falls back to the normal sign-in flow.
560
+ */
561
+ getWebAuthnLoginOptions(userId: string, rpId: string): Promise<_simplewebauthn_browser.PublicKeyCredentialRequestOptionsJSON | null>;
562
+ postWebAuthnLogin(userId: string, response: _simplewebauthn_browser.AuthenticationResponseJSON, origin: string): Promise<AuthResult>;
563
+ revoke(jwt: string): Promise<void>;
564
+ hasRecoveryBackup(jwt: string): Promise<boolean>;
565
+ getRecoveryBackup(jwt: string): Promise<{
566
+ wrappedDeviceShare: Uint8Array;
256
567
  iv: Uint8Array;
257
568
  unlockMethod: 'passkey';
258
569
  credentialId: Uint8Array;
@@ -301,480 +612,234 @@ type RecoveryProgress = {
301
612
  shareSetVersion: number;
302
613
  };
303
614
 
304
- interface RecoveryShareFile {
305
- version: number;
306
- type: 'decane-recovery-share';
307
- shareSetVersion: number;
308
- kdf: 'pbkdf2-sha256';
309
- kdfParams: {
310
- iterations: number;
311
- salt: string;
312
- };
313
- cipher: 'aes-256-gcm';
314
- iv: string;
315
- ciphertext: string;
316
- addresses: {
317
- evm: string;
318
- solana: string;
615
+ type ChainType = "evm" | "solana" | "tron" | "bitcoin";
616
+ type ChainSupport = ChainType[];
617
+ interface ChainDescriptor {
618
+ id: string;
619
+ type: ChainType;
620
+ name: string;
621
+ nativeCurrency: {
622
+ name: string;
623
+ symbol: string;
624
+ decimals: number;
319
625
  };
320
- passwordHint: string | null;
321
- createdAt: string;
626
+ rpcUrl?: string;
627
+ blockExplorerUrl?: string;
628
+ iconUrl?: string;
629
+ chainId?: number;
630
+ isCustom?: boolean;
322
631
  }
323
-
324
- interface PortableBackup {
325
- version: number;
326
- type: 'decane-portable-backup';
327
- shareSetVersion: number;
328
- kdf: 'pbkdf2-sha256';
329
- kdfParams: {
330
- iterations: number;
331
- salt: string;
332
- };
333
- cipher: 'aes-256-gcm';
334
- iv: string;
335
- ciphertext: string;
336
- addresses: {
337
- evm: string;
338
- solana: string;
339
- };
340
- passwordHint: string | null;
341
- createdAt: string;
342
- }
343
-
344
- interface BalanceResult {
345
- /** Smallest unit — wei for EVM, lamports for Solana. */
346
- raw: bigint;
347
- /** Human-readable amount in whole native units, e.g. "1.2345". */
348
- formatted: string;
349
- /** Native currency symbol, e.g. "ETH", "MATIC", "SOL". */
350
- symbol: string;
351
- /** Decimals of the native currency (18 for EVM, 9 for Solana). */
352
- decimals: number;
353
- }
354
-
355
- interface Eip1193Provider {
356
- request(args: {
357
- method: string;
358
- params?: unknown[] | object;
359
- }): Promise<unknown>;
360
- on(event: string, handler: (...args: unknown[]) => void): this;
361
- removeListener(event: string, handler: (...args: unknown[]) => void): this;
362
- /** Marks this as the Decane provider (parallel to wallets' isMetaMask etc.). */
363
- readonly isDecane: true;
364
- }
365
- interface EthereumProviderOptions {
366
- /** Initial chain id (numeric). Defaults to the first configured EVM chain, else 1. */
367
- chainId?: number;
368
- /** Per-chain RPC overrides ('evm:8453' -> url). Falls back to Decane's defaults. */
369
- rpcUrls?: Record<string, string>;
370
- }
371
- /**
372
- * Build an EIP-1193 provider over a connected Decane social client.
373
- *
374
- * `signAuthorization` (EIP-7702) is wired only when the client exposes it — that
375
- * requires the TEE signer to support the authorization signing path.
376
- */
377
- declare function createEthereumProvider(client: DecaneConnect, opts?: EthereumProviderOptions): Eip1193Provider;
378
-
379
- interface SignedAuthorization {
380
- chainId: number;
381
- address: string;
382
- nonce: number;
383
- yParity: number;
384
- r: string;
385
- s: string;
386
- }
387
-
388
- type Chain = `evm:${number}` | `solana:${'mainnet' | 'devnet'}` | `tron:${'mainnet' | 'shasta' | 'nile'}`;
389
- interface DecaneConnectConfig {
390
- /** Dashboard project ID (public). Sent with every request so the backend can
391
- * confirm the API key below actually belongs to this project. */
392
- appId: string;
393
- apiKey: string;
394
- chains: Chain[];
395
- authMethods: Array<'google' | 'email' | 'kingschat'>;
396
- /**
397
- * Scopes requested from KingsChat at sign-in. Defaults to
398
- * `['send_chat_message']`, which is what KingsChat's own SDK asks for and is
399
- * enough to establish identity. Widen only if the app actually calls
400
- * KingsChat APIs on the user's behalf — the consent screen lists every scope,
401
- * so asking for more than you use costs conversions.
402
- */
403
- kingsChatScopes?: string[];
404
- apiBaseUrl?: string;
405
- sessionDurationMinutes?: number;
406
- promptPin?: () => Promise<string>;
407
- /**
408
- * Require a fresh WebAuthn assertion for every signature, instead of letting
409
- * an unlocked session sign freely until it expires. The TEE session stays
410
- * warm — this costs one authenticator prompt per signature, not a re-auth or
411
- * re-attestation — and the check is enforced inside the enclave, not here.
412
- *
413
- * Default false, which preserves the session-signing model: unlock once, sign
414
- * for the session's lifetime with no further prompts.
415
- *
416
- * Two things worth knowing before turning this on:
417
- * - Multi-step flows prompt per leg. An ERC-20 approve + swap is two
418
- * prompts, not one.
419
- * - The policy is declared by the client at session open, so it hardens a
420
- * leaked session handle (useless without the authenticator) but a fully
421
- * compromised page could open a session without it. Making it
422
- * unbypassable requires the policy to come from the server per user or
423
- * per project; that is not implemented yet.
424
- */
425
- requireAssertionPerSignature?: boolean;
426
- rpId?: string;
427
- rpName?: string;
428
- rpcUrls?: Record<string, string>;
429
- teeUrl?: string;
430
- /**
431
- * Persist the live session (handle + JWT) in sessionStorage so a page RELOAD
432
- * silently resumes it — no repeat passkey/sign-in. Survives reloads, cleared
433
- * on tab close. Default true. Set false to require a fresh unlock on every
434
- * page load (stricter, but the reload friction that motivated this returns).
435
- */
436
- persistSession?: boolean;
437
- /**
438
- * Offer to save a recovery-share file at signup. **Default false** since
439
- * 2.7.4 — opt in explicitly with `true`.
440
- *
441
- * The offer existed because a new device could not otherwise get a device
442
- * share. Server-assisted provisioning now covers that case: every wallet is
443
- * enrolled at creation, un-enrolled wallets repair themselves on the next
444
- * unlock, and a returning user is provisioned from their sign-in alone. For
445
- * almost every app the dialog is now a password prompt during onboarding that
446
- * buys nothing, so it is off unless asked for.
447
- *
448
- * Recovery files themselves are unchanged and still work — `rotateShares()`
449
- * issues one on demand, and `promptForRecoveryFile` still accepts one during
450
- * recovery. Turn this back on if you want users to hold an offline fallback
451
- * that does not depend on Decane being reachable.
452
- */
453
- offerRecoveryAtSignup?: boolean;
454
- /** Ask the user whether they want a recovery share and, if so, a password for it. */
455
- onRecoveryShareOffer?: (addresses: {
456
- evmAddress: string;
457
- solanaAddress: string;
458
- }) => Promise<{
459
- wants: boolean;
460
- password: string;
461
- passwordHint?: string;
462
- }>;
463
- /** Hand the built file + suggested filename to the dApp, which owns the actual download. */
464
- onRecoveryFileReady?: (file: RecoveryShareFile, filename: string) => Promise<void>;
465
- /** Prompt for a recovery-share file (and its password) during Flow B. Return null to cancel. */
466
- promptForRecoveryFile?: () => Promise<{
467
- value: unknown;
468
- getPassword: () => Promise<string | null>;
469
- } | null>;
470
- /**
471
- * Every successful recovery/rotation invalidates the previous recovery
472
- * file — this MUST prompt for a new password and block progress until the
473
- * dApp confirms the user saved the new file (see MIGRATE_SSS_RECOVERY.md §10).
474
- */
475
- onRecoveryRotated?: (addresses: {
476
- evmAddress: string;
477
- solanaAddress: string;
478
- }) => Promise<{
479
- password: string;
480
- passwordHint?: string;
481
- }>;
482
- /** Recovery stage callbacks, for dApp UI. */
483
- onRecoveryProgress?: (event: RecoveryProgress) => void;
484
- /**
485
- * Rotate the whole share set whenever a NEW device recovers via passkey sync
486
- * (Flow A) — the "every device holds a distinct share" model.
487
- *
488
- * Default false, which preserves the historical behaviour: every device
489
- * synced to the same passkey caches the byte-identical wrapped share, so a
490
- * device that is later lost keeps a share that stays valid until someone
491
- * explicitly calls rotateShares(). There is no way to revoke one device
492
- * without revoking all of them.
493
- *
494
- * With this on, joining a new device re-splits the master and issues that
495
- * device its own share, which invalidates every other device's cached copy
496
- * at the same instant. That is what makes per-device revocation possible,
497
- * and it has three consequences worth accepting deliberately:
498
- *
499
- * - **Other devices are logged out, not just the lost one.** Each surviving
500
- * device hits StaleDeviceShareError on its next unlock, clears its share,
501
- * and silently re-recovers through Flow A (one passkey prompt). Expected,
502
- * but it means a shared-family-device setup re-prompts more often.
503
- * - **The user's recovery FILE dies too.** Rotation issues a new recovery
504
- * share, so `onRecoveryRotated` MUST be configured to capture it —
505
- * enabling this without that callback throws at connect time rather than
506
- * stranding the user with a dead file.
507
- * - **Adding a device costs a TEE round trip.** Recovery is slower.
508
- */
509
- rotateOnNewDeviceRecovery?: boolean;
510
- }
511
- interface ConnectResult {
512
- addresses: {
513
- evm?: string;
514
- solana?: string;
515
- tron?: string;
516
- };
517
- isNewUser: boolean;
518
- /** Present when the provider released a profile — see {@link AuthProfile}. */
519
- profile?: AuthProfile;
520
- }
521
- /** A recorded on-chain transaction in the user's history. */
522
- interface TransactionRecord {
632
+ interface UnifiedWalletAccount {
523
633
  id: string;
524
- chain: string;
525
- kind: string;
526
- txHash: string | null;
527
- to: string | null;
528
- value: string | null;
529
- status: 'submitted' | 'confirmed' | 'failed' | string;
530
- metadata: Record<string, unknown> | null;
531
- createdAt: string;
532
- updatedAt?: string;
533
- }
534
- /** What the dApp passes to record a transaction it broadcast. */
535
- interface RecordTransactionInput {
536
- chain: string;
537
- txHash?: string;
538
- to?: string;
539
- value?: string;
540
- kind?: string;
541
- status?: string;
542
- metadata?: Record<string, unknown>;
543
- }
544
- interface SignMessageRequest {
545
- chain: Chain;
546
- message: string | Uint8Array;
547
- }
548
- interface SendTransactionRequest {
549
- chain: Chain;
550
- to: string;
551
- value?: bigint;
552
- data?: string;
553
- gasLimit?: bigint;
554
- gasPrice?: bigint;
555
- }
556
- interface SignTypedDataRequest {
557
- chain: Chain;
558
- domain: Record<string, unknown>;
559
- types: Record<string, unknown>;
560
- message: Record<string, unknown>;
561
- primaryType?: string;
634
+ name: string;
635
+ icon: string;
636
+ rdns?: string;
637
+ chainSupport: ChainSupport;
638
+ evmProvider?: EIP1193Provider;
639
+ solanaProvider?: StandardWallet;
640
+ tronProvider?: TronProvider;
641
+ bitcoinProvider?: BitcoinProvider;
562
642
  }
563
- type SdkEvent = 'connected' | 'disconnected' | 'session-expired' | 'session-started' | 'wallet-creating' | 'wallet-unlocking'
564
- /**
565
- * Emitted when this wallet could not be enrolled for server-assisted
566
- * recovery. Not fatal — the wallet works — but until it is enrolled the user
567
- * cannot get a device share from login alone, so a future new device will
568
- * need a passkey backup or a recovery file. The SDK retries automatically on
569
- * later unlocks; surface or report this if you want visibility rather than a
570
- * console line.
571
- */
572
- | 'enrolment-failed';
573
- interface DecaneConnect {
574
- connect(): Promise<ConnectResult>;
575
- connectWithGoogle(): Promise<ConnectResult>;
576
- /**
577
- * Sign in with a Google ID token the app already obtained — native Google
578
- * Sign-In on mobile, or Google Identity Services on the web.
579
- *
580
- * Resolves to the SAME wallet as connectWithGoogle(): both end at the
581
- * backend's Google identity, so a user who signs in on your website and then
582
- * on your app lands on one wallet, not two.
583
- */
584
- connectWithGoogleToken(idToken: string): Promise<ConnectResult>;
585
- /**
586
- * Sign in with KingsChat.
587
- *
588
- * Opens KingsChat's consent screen in a popup, so this MUST be called
589
- * directly from a click handler — a popup opened after an await is blocked by
590
- * every browser. The resulting access token is exchanged server-side for a
591
- * Decane session; it is never treated as one on its own.
592
- */
593
- connectWithKingsChat(): Promise<ConnectResult>;
594
- /**
595
- * Sign in with a KingsChat access token the app already holds — from
596
- * kingschat-web-sdk, a native SDK, or any other flow. Use when you want to
597
- * own the consent step yourself; connectWithKingsChat() is the turnkey path.
598
- */
599
- connectWithKingsChatToken(accessToken: string): Promise<ConnectResult>;
600
- connectWithEmail(email: string): Promise<{
601
- codeRequired: true;
602
- }>;
603
- /**
604
- * Sign in with a JWT from your own auth system (Auth0, Clerk, Firebase,
605
- * Cognito, your own backend — anything that publishes a JWKS endpoint).
606
- *
607
- * Configure the issuer and JWKS URL against your project in the dashboard;
608
- * no client-side setup and no secret in your dApp. `providerId` is only
609
- * required if the project has more than one provider configured.
610
- *
611
- * The user's wallet is derived from the configured identity claim, so the
612
- * same token subject always resolves to the same wallet.
613
- */
614
- connectWithToken(token: string, opts?: {
615
- providerId?: string;
616
- }): Promise<ConnectResult>;
617
- verifyEmailCode(email: string, code: string): Promise<ConnectResult>;
618
- unlock(): Promise<void>;
619
- isUnlocked(): boolean;
620
- /**
621
- * True when the user's identity is known from a previous visit but there is
622
- * no live session, so unlock() cannot succeed and they must sign in again.
623
- * Check this before offering an "Unlock" action.
624
- *
625
- * Note this can be true while canSignInWithPasskey() is also true — in which
626
- * case "sign in again" costs one biometric prompt, not a full auth round
627
- * trip. Prefer offering that over a Google/email button.
628
- */
629
- needsReconnect(): boolean;
630
- /**
631
- * True when a returning user can be signed straight back in with the passkey
632
- * already on this device — no Google redirect, no email code. Requires a
633
- * remembered identity plus a passkey-wrapped device share in local storage,
634
- * so it is false on new devices and for PIN-only wallets.
635
- */
636
- canSignInWithPasskey(): Promise<boolean>;
637
- /**
638
- * Sign in using only the device passkey. connect() already tries this first
639
- * when it's available, so call this directly only when driving the UI
640
- * yourself. Throws NewDeviceError when this device isn't eligible.
641
- */
642
- signInWithPasskey(): Promise<ConnectResult>;
643
- sessionExpiresAt(): number | null;
644
- signMessage(req: SignMessageRequest): Promise<string>;
645
- sendTransaction(req: SendTransactionRequest): Promise<string>;
646
- /** Sign EIP-712 typed data (EVM only). Throws on a Solana chain. */
647
- signTypedData(req: SignTypedDataRequest): Promise<string>;
648
- /**
649
- * A standard EIP-1193 Ethereum provider backed by this wallet — drop it into
650
- * viem (`custom(provider)`), wagmi, or ethers (`BrowserProvider`). Signing
651
- * goes through the TEE; reads are forwarded to the chain RPC. Memoized, so
652
- * repeated calls return the same instance (safe for wagmi).
653
- */
654
- getEthereumProvider(opts?: {
655
- chainId?: number;
656
- }): Eip1193Provider;
657
- /**
658
- * Sign an EIP-7702 authorization, delegating this EOA to a smart-account
659
- * implementation contract on `chainId`. Enables account-abstraction /
660
- * sponsored-gas flows. Returns the full viem `SignedAuthorization` tuple, so
661
- * it drops straight into a userOperation's `authorization` field or a type-4
662
- * transaction's authorizationList. `address` and `contractAddress` are
663
- * interchangeable (the latter matches Privy's/older viem's param name).
664
- */
665
- signAuthorization(req: {
666
- chainId: number;
667
- address?: string;
668
- contractAddress?: string;
669
- nonce: number;
670
- }): Promise<SignedAuthorization>;
643
+ type CAIP25Method = "wallet_requestPermissions" | "wallet_revokePermissions" | "wallet_getPermissions" | "wallet_sendTransaction" | "wallet_signMessage" | "wallet_signTypedData";
644
+ interface ChainScope {
645
+ methods: CAIP25Method[];
646
+ accounts: string[];
647
+ events?: string[];
648
+ }
649
+ interface CAIP25SessionRequest {
650
+ requiredScopes: Partial<Record<string, ChainScope>>;
651
+ optionalScopes: Partial<Record<string, ChainScope>>;
652
+ }
653
+ interface CAIP25SessionResult {
654
+ sessionId: string;
655
+ grantedScopes: Partial<Record<string, ChainScope>>;
656
+ }
657
+ interface ActiveSession {
658
+ sessionId: string;
659
+ grantedScopes: Partial<Record<string, ChainScope>>;
660
+ walletId: string;
661
+ walletType: ChainType | "multi";
662
+ createdAt: number;
663
+ }
664
+ interface DecaneProvider {
665
+ request(args: {
666
+ method: string;
667
+ params?: unknown[];
668
+ }): Promise<unknown>;
669
+ on(event: string, handler: (...args: unknown[]) => void): void;
670
+ off(event: string, handler: (...args: unknown[]) => void): void;
671
+ readonly wallets: UnifiedWalletAccount[];
672
+ readonly evmWallets: UnifiedWalletAccount[];
673
+ readonly solanaWallets: UnifiedWalletAccount[];
674
+ readonly multiChainWallets: UnifiedWalletAccount[];
675
+ readonly tronWallets: UnifiedWalletAccount[];
676
+ readonly bitcoinWallets: UnifiedWalletAccount[];
677
+ connect(wallet: UnifiedWalletAccount, preferredChain?: ChainType): Promise<void>;
678
+ disconnect(): Promise<void>;
679
+ addChain(chain: ChainDescriptor): void;
680
+ removeChain(chainId: string): void;
681
+ getChain(chainId: string): ChainDescriptor | undefined;
682
+ readonly chains: ChainDescriptor[];
683
+ readonly connectedWallet: UnifiedWalletAccount | null;
684
+ readonly activeSession: ActiveSession | null;
685
+ readonly activeChain: string | null;
686
+ }
687
+ type ConnectMode = "all" | "social" | "wallets" | "evm" | "solana" | "tron" | "evm+social" | "solana+social" | "tron+social" | "evm+tron" | "evm+tron+social";
688
+ interface SocialConfig {
689
+ apiKey: string;
690
+ authMethods?: ("google" | "email" | "phone" | "kingschat")[];
671
691
  /**
672
- * The current session's access token (the Decane JWT). Use it to authenticate
673
- * the user to your own backend (e.g. a sponsored-bundler proxy); verify it
674
- * server-side against backend.decane.app/.well-known/jwks.json. Null with no
675
- * live session.
692
+ * Scopes requested from KingsChat. Defaults to `['send_chat_message']`, which
693
+ * is what KingsChat's own SDK asks for and is enough to establish identity.
676
694
  */
677
- getAccessToken(): string | null;
678
- signSolanaTransaction(tx: Uint8Array): Promise<Uint8Array>;
695
+ kingsChatScopes?: string[];
696
+ promptPin?: () => Promise<string>;
697
+ apiBaseUrl?: string;
698
+ chains?: string[];
699
+ rpcUrls?: Record<string, string>;
679
700
  /**
680
- * Sign a Tron transaction. Pass the transaction's `raw_data_hex` (the value
681
- * TronWeb puts on a built transaction). The enclave computes the txID
682
- * (sha256(raw_data)) and signs it with the same secp256k1 key as the EVM
683
- * wallet; returns the 65-byte recoverable signature as 0x-hex. Attach it to
684
- * the transaction's `signature` array and broadcast via TronWeb / a full node.
701
+ * Show the built-in full-screen status overlay while a wallet is being
702
+ * created or unlocked. Default **true**.
703
+ *
704
+ * The overlay exists because those flows can take several seconds and often
705
+ * run right after a Google redirect, when no modal of yours is mounted — so
706
+ * without it the page can look frozen. It is rendered by `<DecaneKit>` itself,
707
+ * as a portal, and injects its own theme tokens.
708
+ *
709
+ * Set false to suppress it and render your own. Everything it displays is
710
+ * already available to you: `useSocialAuth()` exposes `phase`
711
+ * (`"creating" | "unlocking" | null`) plus the `isCreatingWallet` /
712
+ * `isUnlockingWallet` booleans, and the client emits `wallet-creating` and
713
+ * `wallet-unlocking` events. If you suppress it, show something of your own —
714
+ * a silent multi-second pause reads as a broken page.
685
715
  */
686
- signTronTransaction(rawDataHex: string): Promise<string>;
716
+ showStatusOverlay?: boolean;
687
717
  /**
688
- * Record a transaction this wallet broadcast, so it appears in the user's
689
- * history (and the developer dashboard). EVM `sendTransaction` records itself
690
- * automatically; call this for Solana/Tron transactions your dApp broadcasts,
691
- * or to attach a `confirmed`/`failed` status update to an existing hash.
718
+ * Offer to save a recovery-share file at signup. **Default false** since
719
+ * 2.7.4 — opt in explicitly with `true`.
720
+ *
721
+ * Server-assisted provisioning now covers the new-device case (wallets are
722
+ * enrolled at creation and repair themselves on unlock), so for most apps
723
+ * this dialog is a password prompt during onboarding that buys nothing. Turn
724
+ * it on if you want users to hold an offline fallback that works when Decane
725
+ * is unreachable.
692
726
  */
693
- recordTransaction(tx: RecordTransactionInput): Promise<TransactionRecord>;
694
- /** The user's transaction history, most recent first. Requires a live session. */
695
- getTransactionHistory(limit?: number): Promise<TransactionRecord[]>;
696
- disconnect(): Promise<void>;
697
- getAddresses(): {
698
- evm?: string;
699
- solana?: string;
700
- tron?: string;
701
- } | null;
727
+ offerRecoveryAtSignup?: boolean;
728
+ /** Ask the user whether they want a recovery share and, if so, a password for it. */
729
+ onRecoveryShareOffer?: (addresses: {
730
+ evmAddress: string;
731
+ solanaAddress: string;
732
+ }) => Promise<{
733
+ wants: boolean;
734
+ password: string;
735
+ passwordHint?: string;
736
+ }>;
737
+ /** Hand the built file + suggested filename to the app, which owns the download. */
738
+ onRecoveryFileReady?: (file: RecoveryShareFile, filename: string) => Promise<void>;
702
739
  /**
703
- * Native-token balance for a chain (defaults to the first configured EVM
704
- * chain, else Solana). Read-only public data — works even while the wallet
705
- * is locked. Returns raw (wei/lamports), a human `formatted` string, the
706
- * native `symbol`, and `decimals`.
740
+ * Prompt for a recovery-share file (and its password) during recovery.
741
+ * Return null to cancel. **Without this, a user who needs the recovery-file
742
+ * path fails immediately with NewDeviceError** — there is nowhere to ask them
743
+ * for the file.
707
744
  */
708
- getBalance(chain?: string): Promise<BalanceResult>;
709
- /** Native balances across every configured chain the wallet has an address for. */
710
- getBalances(): Promise<Array<{
711
- chain: string;
712
- balance: BalanceResult | null;
713
- }>>;
745
+ promptForRecoveryFile?: () => Promise<{
746
+ value: unknown;
747
+ getPassword: () => Promise<string | null>;
748
+ } | null>;
714
749
  /**
715
- * Voluntary share rotation — after a suspected device compromise, or just
716
- * to invalidate an old recovery file. Requires a fresh WebAuthn assertion.
717
- * Returns the new recovery-share file (the caller still owns delivering it
718
- * to the user — this does not download anything itself).
750
+ * Every successful recovery/rotation invalidates the previous recovery file,
751
+ * so this must prompt for a new password and capture the replacement.
752
+ * Required before recovery can run at all.
719
753
  */
720
- rotateShares(opts: {
754
+ onRecoveryRotated?: (addresses: {
755
+ evmAddress: string;
756
+ solanaAddress: string;
757
+ }) => Promise<{
721
758
  password: string;
722
759
  passwordHint?: string;
723
- }): Promise<RecoveryShareFile>;
760
+ }>;
761
+ /** Recovery stage callbacks, for your own progress UI. */
762
+ onRecoveryProgress?: (event: RecoveryProgress) => void;
724
763
  /**
725
- * The two-share bearer portable backup (device + recovery). This file plus
726
- * its password gives complete control of the wallet — see the mandatory
727
- * warning-screen requirements in MIGRATE_SSS_RECOVERY.md §10. Requires a
728
- * fresh WebAuthn assertion and rotates the share set on every export.
764
+ * Require a fresh WebAuthn assertion for every signature rather than letting
765
+ * an unlocked session sign freely until it expires. Enforced inside the TEE.
766
+ *
767
+ * Does not change the session lifetime — the session stays warm (no
768
+ * re-attestation, no re-auth) and authority is re-proven per signature
769
+ * instead. Default false. Needs a passkey; PIN-only wallets have no
770
+ * authenticator to assert with. See SocialConfig's counterpart in
771
+ * social/types.ts for the full caveats.
729
772
  */
730
- exportPortableBackup(opts: {
731
- password: string;
732
- passwordHint?: string;
733
- }): Promise<PortableBackup>;
734
- /** Whether the user has ever saved a recovery-share file (UX flag only — Decane never stores the share). */
735
- hasRecoveryShare(): Promise<boolean>;
736
- /** True at most once per account: no recovery share saved, never nagged, wallet holds value. */
737
- shouldPromptRecovery(): Promise<boolean>;
738
- /** Call regardless of the user's answer — the recovery nag fires once, ever. */
739
- dismissRecoveryPrompt(): Promise<void>;
740
- /** Whether a wrapped-share backup exists on the server. */
741
- hasBackup(): Promise<boolean>;
742
- /** Delete the server-side backup. Disables Flow A recovery. */
743
- deleteBackup(): Promise<void>;
744
- on(event: SdkEvent, handler: (...args: unknown[]) => void): void;
745
- off(event: SdkEvent, handler: (...args: unknown[]) => void): void;
773
+ requireAssertionPerSignature?: boolean;
774
+ /**
775
+ * Sign in with a token from your own auth system (Auth0, Clerk, Firebase,
776
+ * Cognito, or your own backend) — surfaced as a button in the built-in
777
+ * WalletSelector / SocialWalletModal UI, alongside Google and email.
778
+ * getToken() runs when the user clicks the button; wire it to however you
779
+ * already obtain a token from your auth system (e.g. after your own login
780
+ * flow completes). Configure the issuer and JWKS URL for this token
781
+ * against your project in the dashboard first. `providerId` is only
782
+ * needed when the project has more than one custom provider configured.
783
+ */
784
+ customAuth?: {
785
+ label: string;
786
+ providerId?: string;
787
+ getToken: () => Promise<string>;
788
+ };
746
789
  }
747
- interface WrappedShareMeta {
748
- unlockMethod: 'passkey' | 'pin';
749
- credentialId?: string;
750
- prfSalt?: string;
751
- pinSalt?: string;
752
- pinIterations?: number;
790
+ interface DecaneConfig {
791
+ appId: string;
792
+ chains?: ChainDescriptor[];
793
+ supportedChainIds?: string[];
794
+ networks?: "evm" | "solana" | "tron" | "bitcoin" | "all";
795
+ defaultChain?: string;
796
+ theme?: "light" | "dark" | "auto";
797
+ mode?: ConnectMode;
798
+ social?: SocialConfig;
753
799
  }
754
- /**
755
- * Who the user is, as the identity provider just described them.
756
- *
757
- * Passed through from the provider at sign-in and never stored by Decane — the
758
- * backend keeps only an HMAC and a masked identifier, so there is no profile to
759
- * look up later. Cache what you need at sign-in if you want to show a name
760
- * after a page reload.
761
- *
762
- * Every field is optional because providers differ in what they release, and a
763
- * user can withhold most of it. Email sign-in returns an empty profile.
764
- */
765
- interface AuthProfile {
766
- name?: string;
767
- email?: string;
768
- picture?: string;
800
+ interface EIP1193Provider {
801
+ request(args: {
802
+ method: string;
803
+ params?: unknown[];
804
+ }): Promise<unknown>;
805
+ on(event: string, handler: (...args: unknown[]) => void): void;
806
+ removeListener(event: string, handler: (...args: unknown[]) => void): void;
769
807
  }
770
- interface AuthResult {
771
- jwt: string;
772
- isNewUser: boolean;
773
- hasShare: boolean;
774
- userId: string;
775
- profile?: AuthProfile;
808
+ interface StandardWallet {
809
+ name: string;
810
+ icon: string;
811
+ features: Record<string, unknown>;
812
+ accounts: StandardWalletAccount[];
813
+ }
814
+ interface StandardWalletAccount {
815
+ address: string;
816
+ publicKey: Uint8Array;
817
+ chains: string[];
818
+ }
819
+ interface TronProvider {
820
+ request(args: {
821
+ method: string;
822
+ params?: unknown[] | Record<string, unknown>;
823
+ }): Promise<unknown>;
824
+ on(event: string, handler: (...args: unknown[]) => void): void;
825
+ removeListener(event: string, handler: (...args: unknown[]) => void): void;
826
+ }
827
+ interface BitcoinProvider {
828
+ requestAccounts(): Promise<string[] | Array<{
829
+ address: string;
830
+ }>>;
831
+ getAccounts?(): Promise<string[] | Array<{
832
+ address: string;
833
+ }>>;
834
+ signMessage(message: string, ...args: unknown[]): Promise<string>;
835
+ sendBitcoin?(toAddress: string, satoshis: number, options?: unknown): Promise<string>;
836
+ send?(params: unknown): Promise<string>;
837
+ signPsbt?(psbtHex: string, options?: unknown): Promise<string>;
838
+ on?(event: string, handler: (...args: unknown[]) => void): void;
776
839
  }
777
840
 
841
+ declare function createDecane(config: DecaneConfig): DecaneProvider;
842
+
778
843
  declare function createDecaneConnect(config: DecaneConnectConfig): Promise<DecaneConnect>;
779
844
 
780
845
  /**
@@ -1006,13 +1071,17 @@ interface WalletSelectorProps {
1006
1071
  theme?: "light" | "dark" | "auto";
1007
1072
  networks?: "evm" | "solana" | "tron" | "bitcoin" | "all";
1008
1073
  mode?: ConnectMode;
1009
- socialAuthMethods?: ("google" | "email" | "kingschat")[];
1074
+ socialAuthMethods?: ("google" | "email" | "phone" | "kingschat")[];
1010
1075
  onGoogleSignIn?: () => Promise<void>;
1011
1076
  onKingsChatSignIn?: () => Promise<void>;
1012
1077
  onEmailStart?: (email: string) => Promise<{
1013
1078
  codeRequired: true;
1014
1079
  }>;
1015
1080
  onEmailVerify?: (email: string, code: string) => Promise<void>;
1081
+ onPhoneStart?: (phone: string) => Promise<{
1082
+ codeRequired: true;
1083
+ }>;
1084
+ onPhoneVerify?: (phone: string, code: string) => Promise<void>;
1016
1085
  /** Button text for the developer's own auth system, e.g. "Continue with Acme Inc". Omitted entirely when not configured. */
1017
1086
  customAuthLabel?: string;
1018
1087
  onCustomAuthSignIn?: () => Promise<void>;
@@ -1022,7 +1091,7 @@ interface WalletSelectorProps {
1022
1091
  passkeyAddress?: string | null;
1023
1092
  onPasskeySignIn?: () => Promise<unknown>;
1024
1093
  }
1025
- declare function WalletSelector({ provider, open, onClose, onConnect, theme, networks, mode, socialAuthMethods, onGoogleSignIn, onKingsChatSignIn, onEmailStart, onEmailVerify, customAuthLabel, onCustomAuthSignIn, canUsePasskey, passkeyAddress, onPasskeySignIn, }: WalletSelectorProps): React.ReactPortal | null;
1094
+ declare function WalletSelector({ provider, open, onClose, onConnect, theme, networks, mode, socialAuthMethods, onGoogleSignIn, onKingsChatSignIn, onEmailStart, onEmailVerify, onPhoneStart, onPhoneVerify, customAuthLabel, onCustomAuthSignIn, canUsePasskey, passkeyAddress, onPasskeySignIn, }: WalletSelectorProps): React.ReactPortal | null;
1026
1095
 
1027
1096
  type Theme = "light" | "dark" | "auto";
1028
1097
  interface SocialAuthState {
@@ -1085,7 +1154,11 @@ interface SocialWalletModalProps {
1085
1154
  codeRequired: true;
1086
1155
  }>;
1087
1156
  onEmailVerify: (email: string, code: string) => Promise<void>;
1088
- authMethods?: Array<"google" | "email" | "kingschat">;
1157
+ onPhoneStart?: (phone: string) => Promise<{
1158
+ codeRequired: true;
1159
+ }>;
1160
+ onPhoneVerify?: (phone: string, code: string) => Promise<void>;
1161
+ authMethods?: Array<"google" | "email" | "phone" | "kingschat">;
1089
1162
  customAuthLabel?: string;
1090
1163
  onCustomAuthSignIn?: () => Promise<void>;
1091
1164
  canUsePasskey?: boolean;
@@ -1102,7 +1175,7 @@ interface SocialWalletModalProps {
1102
1175
  rpcUrls?: Record<string, string>;
1103
1176
  chains?: string[];
1104
1177
  }
1105
- declare function SocialWalletModal({ open, onClose, theme, socialAuth, onUnlock, onDisconnect, onGoogleSignIn, onKingsChatSignIn, onEmailStart, onEmailVerify, authMethods, customAuthLabel, onCustomAuthSignIn, canUsePasskey, passkeyAddress, onPasskeySignIn, onRotateShares, onExportPortableBackup, rpcUrls, chains, }: SocialWalletModalProps): React.ReactPortal | null;
1178
+ declare function SocialWalletModal({ open, onClose, theme, socialAuth, onUnlock, onDisconnect, onGoogleSignIn, onKingsChatSignIn, onEmailStart, onEmailVerify, onPhoneStart, onPhoneVerify, authMethods, customAuthLabel, onCustomAuthSignIn, canUsePasskey, passkeyAddress, onPasskeySignIn, onRotateShares, onExportPortableBackup, rpcUrls, chains, }: SocialWalletModalProps): React.ReactPortal | null;
1106
1179
 
1107
1180
  declare function DecaneMark({ size, muted }: {
1108
1181
  size?: number;
@@ -1133,6 +1206,7 @@ declare function useSocialAuth(): {
1133
1206
  needsReconnect: boolean;
1134
1207
  googleLoading: boolean;
1135
1208
  emailLoading: boolean;
1209
+ phoneLoading: boolean;
1136
1210
  tokenLoading: boolean;
1137
1211
  customLoading: boolean;
1138
1212
  customAuthLabel: string | undefined;
@@ -1143,6 +1217,10 @@ declare function useSocialAuth(): {
1143
1217
  codeRequired: true;
1144
1218
  }>;
1145
1219
  confirmEmailCode: (email: string, code: string) => Promise<void>;
1220
+ sendPhoneCode: (phone: string) => Promise<{
1221
+ codeRequired: true;
1222
+ }>;
1223
+ confirmPhoneCode: (phone: string, code: string) => Promise<void>;
1146
1224
  signInWithToken: (token: string, opts?: {
1147
1225
  providerId?: string;
1148
1226
  }) => Promise<void>;