decane-connect-kit 2.21.0 → 2.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +140 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -1
- package/dist/index.d.ts +47 -1
- package/dist/index.js +140 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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`.
|
|
@@ -398,6 +411,14 @@ interface DecaneConnect {
|
|
|
398
411
|
setUnlockPassword(password: string): Promise<void>;
|
|
399
412
|
/** True when this device can open a session from a password alone. */
|
|
400
413
|
canUnlockWithPassword(): Promise<boolean>;
|
|
414
|
+
/**
|
|
415
|
+
* "Use a different account." disconnect() keeps one fact about the account
|
|
416
|
+
* that signed out — its userId — so canSignInWithPasskey() and
|
|
417
|
+
* canUnlockWithPassword() can offer that account's own way back in even
|
|
418
|
+
* when several accounts are enrolled on this browser. This drops it. The
|
|
419
|
+
* device shares themselves are untouched.
|
|
420
|
+
*/
|
|
421
|
+
forgetLastUser(): void;
|
|
401
422
|
/** Open a session from the password alone — no provider, no passkey. */
|
|
402
423
|
unlockWithPassword(password: string): Promise<ConnectResult>;
|
|
403
424
|
/** Turn the password path off, revoking the token account-wide. */
|
|
@@ -420,6 +441,21 @@ interface DecaneConnect {
|
|
|
420
441
|
* on the way).
|
|
421
442
|
*/
|
|
422
443
|
addPasskey(): Promise<void>;
|
|
444
|
+
/**
|
|
445
|
+
* Whether this device holds a wrapped share for the signed-in account, i.e.
|
|
446
|
+
* whether a passkey or password protects the wallet here. False on a device
|
|
447
|
+
* created with `deferDeviceProtection`, or whose share was never written.
|
|
448
|
+
*/
|
|
449
|
+
deviceProtected(): Promise<boolean>;
|
|
450
|
+
/**
|
|
451
|
+
* Wrap and store this device's share now: a passkey where the device can
|
|
452
|
+
* hold one, otherwise the unlock password (`promptUnlockPassword`). Needs a
|
|
453
|
+
* live session; provisions a fresh share from the enclave when the device
|
|
454
|
+
* has none. Idempotent — a protected device returns true at once. Resolves
|
|
455
|
+
* false when the user dismissed the prompt, so the app can decide whether
|
|
456
|
+
* to go on without protection; throws for anything else.
|
|
457
|
+
*/
|
|
458
|
+
protectDevice(): Promise<boolean>;
|
|
423
459
|
sessionExpiresAt(): number | null;
|
|
424
460
|
signMessage(req: SignMessageRequest): Promise<string>;
|
|
425
461
|
sendTransaction(req: SendTransactionRequest): Promise<string>;
|
|
@@ -525,7 +561,7 @@ interface DecaneConnect {
|
|
|
525
561
|
off(event: SdkEvent, handler: (...args: unknown[]) => void): void;
|
|
526
562
|
}
|
|
527
563
|
interface WrappedShareMeta {
|
|
528
|
-
unlockMethod: 'passkey' | 'pin' | 'password';
|
|
564
|
+
unlockMethod: 'passkey' | 'pin' | 'password' | 'deferred';
|
|
529
565
|
credentialId?: string;
|
|
530
566
|
prfSalt?: string;
|
|
531
567
|
pinSalt?: string;
|
|
@@ -839,6 +875,13 @@ interface SocialConfig {
|
|
|
839
875
|
* a silent multi-second pause reads as a broken page.
|
|
840
876
|
*/
|
|
841
877
|
showStatusOverlay?: boolean;
|
|
878
|
+
/**
|
|
879
|
+
* Ask for no passkey or password at sign-in; protect the wallet on this
|
|
880
|
+
* device later, when the app calls `protectDevice()` (typically before the
|
|
881
|
+
* first transaction). Mirrors the headless client's option of the same name;
|
|
882
|
+
* see it for what a device gives up until then. Default false.
|
|
883
|
+
*/
|
|
884
|
+
deferDeviceProtection?: boolean;
|
|
842
885
|
/**
|
|
843
886
|
* Offer to save a recovery-share file at signup. **Default false** since
|
|
844
887
|
* 2.7.4 — opt in explicitly with `true`.
|
|
@@ -1440,10 +1483,13 @@ declare function useSocialWallet(): {
|
|
|
1440
1483
|
getAccessToken: () => string | null;
|
|
1441
1484
|
hasPasskey: () => Promise<boolean>;
|
|
1442
1485
|
addPasskey: () => Promise<void>;
|
|
1486
|
+
deviceProtected: () => Promise<boolean>;
|
|
1487
|
+
protectDevice: () => Promise<boolean>;
|
|
1443
1488
|
setUnlockPassword: (password: string) => Promise<void>;
|
|
1444
1489
|
canUnlockWithPassword: () => Promise<boolean>;
|
|
1445
1490
|
unlockWithPassword: (password: string) => Promise<void>;
|
|
1446
1491
|
removeUnlockPassword: () => Promise<void>;
|
|
1492
|
+
forgetLastUser: () => void;
|
|
1447
1493
|
rotateShares: (opts: {
|
|
1448
1494
|
password: string;
|
|
1449
1495
|
passwordHint?: string;
|
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`.
|
|
@@ -398,6 +411,14 @@ interface DecaneConnect {
|
|
|
398
411
|
setUnlockPassword(password: string): Promise<void>;
|
|
399
412
|
/** True when this device can open a session from a password alone. */
|
|
400
413
|
canUnlockWithPassword(): Promise<boolean>;
|
|
414
|
+
/**
|
|
415
|
+
* "Use a different account." disconnect() keeps one fact about the account
|
|
416
|
+
* that signed out — its userId — so canSignInWithPasskey() and
|
|
417
|
+
* canUnlockWithPassword() can offer that account's own way back in even
|
|
418
|
+
* when several accounts are enrolled on this browser. This drops it. The
|
|
419
|
+
* device shares themselves are untouched.
|
|
420
|
+
*/
|
|
421
|
+
forgetLastUser(): void;
|
|
401
422
|
/** Open a session from the password alone — no provider, no passkey. */
|
|
402
423
|
unlockWithPassword(password: string): Promise<ConnectResult>;
|
|
403
424
|
/** Turn the password path off, revoking the token account-wide. */
|
|
@@ -420,6 +441,21 @@ interface DecaneConnect {
|
|
|
420
441
|
* on the way).
|
|
421
442
|
*/
|
|
422
443
|
addPasskey(): Promise<void>;
|
|
444
|
+
/**
|
|
445
|
+
* Whether this device holds a wrapped share for the signed-in account, i.e.
|
|
446
|
+
* whether a passkey or password protects the wallet here. False on a device
|
|
447
|
+
* created with `deferDeviceProtection`, or whose share was never written.
|
|
448
|
+
*/
|
|
449
|
+
deviceProtected(): Promise<boolean>;
|
|
450
|
+
/**
|
|
451
|
+
* Wrap and store this device's share now: a passkey where the device can
|
|
452
|
+
* hold one, otherwise the unlock password (`promptUnlockPassword`). Needs a
|
|
453
|
+
* live session; provisions a fresh share from the enclave when the device
|
|
454
|
+
* has none. Idempotent — a protected device returns true at once. Resolves
|
|
455
|
+
* false when the user dismissed the prompt, so the app can decide whether
|
|
456
|
+
* to go on without protection; throws for anything else.
|
|
457
|
+
*/
|
|
458
|
+
protectDevice(): Promise<boolean>;
|
|
423
459
|
sessionExpiresAt(): number | null;
|
|
424
460
|
signMessage(req: SignMessageRequest): Promise<string>;
|
|
425
461
|
sendTransaction(req: SendTransactionRequest): Promise<string>;
|
|
@@ -525,7 +561,7 @@ interface DecaneConnect {
|
|
|
525
561
|
off(event: SdkEvent, handler: (...args: unknown[]) => void): void;
|
|
526
562
|
}
|
|
527
563
|
interface WrappedShareMeta {
|
|
528
|
-
unlockMethod: 'passkey' | 'pin' | 'password';
|
|
564
|
+
unlockMethod: 'passkey' | 'pin' | 'password' | 'deferred';
|
|
529
565
|
credentialId?: string;
|
|
530
566
|
prfSalt?: string;
|
|
531
567
|
pinSalt?: string;
|
|
@@ -839,6 +875,13 @@ interface SocialConfig {
|
|
|
839
875
|
* a silent multi-second pause reads as a broken page.
|
|
840
876
|
*/
|
|
841
877
|
showStatusOverlay?: boolean;
|
|
878
|
+
/**
|
|
879
|
+
* Ask for no passkey or password at sign-in; protect the wallet on this
|
|
880
|
+
* device later, when the app calls `protectDevice()` (typically before the
|
|
881
|
+
* first transaction). Mirrors the headless client's option of the same name;
|
|
882
|
+
* see it for what a device gives up until then. Default false.
|
|
883
|
+
*/
|
|
884
|
+
deferDeviceProtection?: boolean;
|
|
842
885
|
/**
|
|
843
886
|
* Offer to save a recovery-share file at signup. **Default false** since
|
|
844
887
|
* 2.7.4 — opt in explicitly with `true`.
|
|
@@ -1440,10 +1483,13 @@ declare function useSocialWallet(): {
|
|
|
1440
1483
|
getAccessToken: () => string | null;
|
|
1441
1484
|
hasPasskey: () => Promise<boolean>;
|
|
1442
1485
|
addPasskey: () => Promise<void>;
|
|
1486
|
+
deviceProtected: () => Promise<boolean>;
|
|
1487
|
+
protectDevice: () => Promise<boolean>;
|
|
1443
1488
|
setUnlockPassword: (password: string) => Promise<void>;
|
|
1444
1489
|
canUnlockWithPassword: () => Promise<boolean>;
|
|
1445
1490
|
unlockWithPassword: (password: string) => Promise<void>;
|
|
1446
1491
|
removeUnlockPassword: () => Promise<void>;
|
|
1492
|
+
forgetLastUser: () => void;
|
|
1447
1493
|
rotateShares: (opts: {
|
|
1448
1494
|
password: string;
|
|
1449
1495
|
passwordHint?: string;
|
package/dist/index.js
CHANGED
|
@@ -3607,6 +3607,29 @@ function clearPersisted(key) {
|
|
|
3607
3607
|
} catch {
|
|
3608
3608
|
}
|
|
3609
3609
|
}
|
|
3610
|
+
function makeLastUserKey(apiKey) {
|
|
3611
|
+
return `${makePersistKey(apiKey)}:last-user`;
|
|
3612
|
+
}
|
|
3613
|
+
function loadLastUser(key) {
|
|
3614
|
+
try {
|
|
3615
|
+
const raw = typeof localStorage !== "undefined" ? localStorage.getItem(key) : null;
|
|
3616
|
+
return raw ? JSON.parse(raw).userId ?? null : null;
|
|
3617
|
+
} catch {
|
|
3618
|
+
return null;
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
function saveLastUser(key, userId) {
|
|
3622
|
+
try {
|
|
3623
|
+
localStorage.setItem(key, JSON.stringify({ userId }));
|
|
3624
|
+
} catch {
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
function clearLastUser(key) {
|
|
3628
|
+
try {
|
|
3629
|
+
localStorage.removeItem(key);
|
|
3630
|
+
} catch {
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3610
3633
|
function sessionPersistKey(apiKey) {
|
|
3611
3634
|
return `decane:sess:${apiKey.slice(0, 20)}`;
|
|
3612
3635
|
}
|
|
@@ -3645,6 +3668,7 @@ async function createDecaneConnect(config) {
|
|
|
3645
3668
|
const rpId = config.rpId ?? (typeof window !== "undefined" ? window.location.hostname : "localhost");
|
|
3646
3669
|
const rpName = config.rpName ?? "Decane Connect";
|
|
3647
3670
|
const PERSIST_KEY = makePersistKey(config.apiKey);
|
|
3671
|
+
const LAST_USER_KEY = makeLastUserKey(config.apiKey);
|
|
3648
3672
|
const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
|
|
3649
3673
|
const events = new EventEmitter();
|
|
3650
3674
|
const api = new ApiClient(baseUrl, config.apiKey, config.appId);
|
|
@@ -3657,8 +3681,10 @@ async function createDecaneConnect(config) {
|
|
|
3657
3681
|
);
|
|
3658
3682
|
let auth = null;
|
|
3659
3683
|
let persisted = loadPersisted(PERSIST_KEY);
|
|
3684
|
+
let lastUserId = loadLastUser(LAST_USER_KEY);
|
|
3660
3685
|
let ethProvider = null;
|
|
3661
3686
|
const persistSession = config.persistSession !== false;
|
|
3687
|
+
const deferDeviceProtection = config.deferDeviceProtection === true;
|
|
3662
3688
|
const serverAssistedRecovery = true;
|
|
3663
3689
|
const RECOVERY_OFFER_TIMEOUT_MS = 5 * 6e4;
|
|
3664
3690
|
const selfHealAttempted = /* @__PURE__ */ new Set();
|
|
@@ -3802,6 +3828,10 @@ async function createDecaneConnect(config) {
|
|
|
3802
3828
|
const stored = await storage.getShare(persisted.userId);
|
|
3803
3829
|
return hasPasswordUnlock(stored) ? stored : null;
|
|
3804
3830
|
}
|
|
3831
|
+
if (lastUserId) {
|
|
3832
|
+
const stored = await storage.getShare(lastUserId);
|
|
3833
|
+
return hasPasswordUnlock(stored) ? stored : null;
|
|
3834
|
+
}
|
|
3805
3835
|
const candidates = (await storage.listShares()).filter(hasPasswordUnlock);
|
|
3806
3836
|
return candidates.length === 1 ? candidates[0] : null;
|
|
3807
3837
|
}
|
|
@@ -3966,11 +3996,12 @@ async function createDecaneConnect(config) {
|
|
|
3966
3996
|
}
|
|
3967
3997
|
async function firstTimeSetup(userId, jwt) {
|
|
3968
3998
|
prewarmAttestation(teeUrl);
|
|
3969
|
-
let unlockMethod;
|
|
3970
|
-
let wrappingKey;
|
|
3999
|
+
let unlockMethod = "password";
|
|
4000
|
+
let wrappingKey = null;
|
|
3971
4001
|
let meta = {};
|
|
3972
4002
|
let unlockTokenKey = null;
|
|
3973
|
-
if (
|
|
4003
|
+
if (deferDeviceProtection) {
|
|
4004
|
+
} else if (isPasskeySupported()) {
|
|
3974
4005
|
const prfSalt = generateRandomBytes(32);
|
|
3975
4006
|
try {
|
|
3976
4007
|
const { credentialId, prfOutput } = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
|
|
@@ -4011,32 +4042,37 @@ async function createDecaneConnect(config) {
|
|
|
4011
4042
|
} finally {
|
|
4012
4043
|
zeroize(master);
|
|
4013
4044
|
}
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4045
|
+
let wrappedDeviceShare = null;
|
|
4046
|
+
let iv = null;
|
|
4047
|
+
if (wrappingKey) {
|
|
4048
|
+
iv = generateRandomBytes(12);
|
|
4049
|
+
wrappedDeviceShare = await aesGcmEncrypt(
|
|
4050
|
+
wrappingKey,
|
|
4051
|
+
shareSet.device,
|
|
4052
|
+
iv
|
|
4053
|
+
);
|
|
4054
|
+
await storage.putShare({
|
|
4055
|
+
userId,
|
|
4056
|
+
wrappedDeviceShare,
|
|
4057
|
+
iv,
|
|
4058
|
+
unlockMethod,
|
|
4059
|
+
credentialId: meta.credentialId ? b64uToBuffer(meta.credentialId) : void 0,
|
|
4060
|
+
prfSalt: meta.prfSalt ? b64uToBuffer(meta.prfSalt) : void 0,
|
|
4061
|
+
pinSalt: meta.pinSalt ? b64uToBuffer(meta.pinSalt) : void 0,
|
|
4062
|
+
pinIterations: meta.pinIterations,
|
|
4063
|
+
passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
|
|
4064
|
+
passwordIterations: meta.passwordIterations,
|
|
4065
|
+
version: 1
|
|
4066
|
+
});
|
|
4067
|
+
if (unlockTokenKey) await attachUnlockToken(userId, jwt, unlockTokenKey);
|
|
4068
|
+
}
|
|
4034
4069
|
const serverIv = generateRandomBytes(12);
|
|
4035
4070
|
await withDeadline(api.putShare(jwt, {
|
|
4036
4071
|
ciphertext: shareSet.server,
|
|
4037
4072
|
iv: serverIv,
|
|
4038
4073
|
wrappedShareMeta: {
|
|
4039
|
-
|
|
4074
|
+
// The server records this on the wallet_created event and nothing else.
|
|
4075
|
+
unlockMethod: deferDeviceProtection ? "deferred" : unlockMethod
|
|
4040
4076
|
// credential IDs, prfSalt, pinSalt stay on-device — never sent to server
|
|
4041
4077
|
},
|
|
4042
4078
|
evmAddress,
|
|
@@ -4045,7 +4081,7 @@ async function createDecaneConnect(config) {
|
|
|
4045
4081
|
}), remainingBudget(), "storing your wallet");
|
|
4046
4082
|
zeroize(shareSet.server);
|
|
4047
4083
|
const addresses = { evm: evmAddress, solana: solanaAddress, tron: tronAddress };
|
|
4048
|
-
if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt) {
|
|
4084
|
+
if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt && wrappedDeviceShare && iv) {
|
|
4049
4085
|
try {
|
|
4050
4086
|
await api.putRecoveryBackup(jwt, {
|
|
4051
4087
|
wrappedDeviceShare,
|
|
@@ -4128,7 +4164,7 @@ async function createDecaneConnect(config) {
|
|
|
4128
4164
|
const tee = new TeeClient(teeUrl);
|
|
4129
4165
|
const provisioned = await tee.provisionDeviceShare(jwt);
|
|
4130
4166
|
if (provisioned) {
|
|
4131
|
-
await persistProvisionedShare(userId, jwt, provisioned);
|
|
4167
|
+
if (!deferDeviceProtection) await persistProvisionedShare(userId, jwt, provisioned);
|
|
4132
4168
|
let addrs;
|
|
4133
4169
|
try {
|
|
4134
4170
|
addrs = await session.start(provisioned, jwt, recordedAddresses);
|
|
@@ -4172,7 +4208,7 @@ async function createDecaneConnect(config) {
|
|
|
4172
4208
|
});
|
|
4173
4209
|
deviceShare = recovered.deviceShare;
|
|
4174
4210
|
joinedViaPasskeySync = recovered.flow === "passkey-sync";
|
|
4175
|
-
if (!await storage.getShare(userId) && isPasskeySupported()) {
|
|
4211
|
+
if (!deferDeviceProtection && !await storage.getShare(userId) && isPasskeySupported()) {
|
|
4176
4212
|
await registerPasskeyAndBackup(userId, jwt, deviceShare);
|
|
4177
4213
|
}
|
|
4178
4214
|
} else {
|
|
@@ -4255,14 +4291,16 @@ async function createDecaneConnect(config) {
|
|
|
4255
4291
|
async function passkeyDeviceCandidate() {
|
|
4256
4292
|
if (!isPasskeySupported()) return null;
|
|
4257
4293
|
if (persisted?.userId) return passkeyLoginCandidate();
|
|
4294
|
+
if (lastUserId) return usablePasskeyShare(await storage.getShare(lastUserId));
|
|
4258
4295
|
const candidates = (await storage.listShares()).map(usablePasskeyShare).filter(Boolean);
|
|
4259
4296
|
return candidates.length === 1 ? candidates[0] : null;
|
|
4260
4297
|
}
|
|
4261
4298
|
async function canOfferPasskey() {
|
|
4262
4299
|
if (!isPasskeySupported()) return false;
|
|
4263
4300
|
if (await passkeyDeviceCandidate()) return true;
|
|
4264
|
-
|
|
4265
|
-
|
|
4301
|
+
const known = persisted?.userId ?? lastUserId;
|
|
4302
|
+
if (!known) return false;
|
|
4303
|
+
return await storage.getShare(known) == null;
|
|
4266
4304
|
}
|
|
4267
4305
|
async function rememberProfile(userId, profile) {
|
|
4268
4306
|
const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
|
|
@@ -4475,8 +4513,9 @@ async function createDecaneConnect(config) {
|
|
|
4475
4513
|
async signInWithPasskey() {
|
|
4476
4514
|
const candidate = await passkeyDeviceCandidate();
|
|
4477
4515
|
if (candidate) return passkeyLogin(candidate);
|
|
4478
|
-
|
|
4479
|
-
|
|
4516
|
+
const known = persisted?.userId ?? lastUserId;
|
|
4517
|
+
if (isPasskeySupported() && known && await storage.getShare(known) == null) {
|
|
4518
|
+
return passkeyLoginWithoutShare(known);
|
|
4480
4519
|
}
|
|
4481
4520
|
throw new NewDeviceError();
|
|
4482
4521
|
},
|
|
@@ -4640,6 +4679,24 @@ async function createDecaneConnect(config) {
|
|
|
4640
4679
|
zeroize(deviceShare);
|
|
4641
4680
|
}
|
|
4642
4681
|
},
|
|
4682
|
+
async deviceProtected() {
|
|
4683
|
+
if (!auth) return false;
|
|
4684
|
+
return await storage.getShare(auth.userId) !== null;
|
|
4685
|
+
},
|
|
4686
|
+
async protectDevice() {
|
|
4687
|
+
if (!auth) throw new ReconnectRequiredError();
|
|
4688
|
+
if (await storage.getShare(auth.userId)) return true;
|
|
4689
|
+
if (!serverAssistedRecovery) throw new NewDeviceError();
|
|
4690
|
+
const tee = new TeeClient(teeUrl);
|
|
4691
|
+
const provisioned = await tee.provisionDeviceShare(auth.jwt);
|
|
4692
|
+
if (!provisioned) throw new NewDeviceError();
|
|
4693
|
+
try {
|
|
4694
|
+
await persistProvisionedShare(auth.userId, auth.jwt, provisioned);
|
|
4695
|
+
} finally {
|
|
4696
|
+
zeroize(provisioned);
|
|
4697
|
+
}
|
|
4698
|
+
return await storage.getShare(auth.userId) !== null;
|
|
4699
|
+
},
|
|
4643
4700
|
async unlock() {
|
|
4644
4701
|
if (!auth) {
|
|
4645
4702
|
const candidate = await passkeyLoginCandidate();
|
|
@@ -4816,12 +4873,21 @@ async function createDecaneConnect(config) {
|
|
|
4816
4873
|
await api.revoke(auth.jwt).catch(() => {
|
|
4817
4874
|
});
|
|
4818
4875
|
}
|
|
4876
|
+
const leaving = auth?.userId ?? persisted?.userId ?? null;
|
|
4877
|
+
if (leaving) {
|
|
4878
|
+
lastUserId = leaving;
|
|
4879
|
+
saveLastUser(LAST_USER_KEY, leaving);
|
|
4880
|
+
}
|
|
4819
4881
|
auth = null;
|
|
4820
4882
|
persisted = null;
|
|
4821
4883
|
clearPersisted(PERSIST_KEY);
|
|
4822
4884
|
dropLiveSession();
|
|
4823
4885
|
events.emit("disconnected");
|
|
4824
4886
|
},
|
|
4887
|
+
forgetLastUser() {
|
|
4888
|
+
lastUserId = null;
|
|
4889
|
+
clearLastUser(LAST_USER_KEY);
|
|
4890
|
+
},
|
|
4825
4891
|
getAddresses() {
|
|
4826
4892
|
return auth?.addresses ?? persisted?.addresses ?? null;
|
|
4827
4893
|
},
|
|
@@ -7260,6 +7326,7 @@ function DecaneKit({ config, children }) {
|
|
|
7260
7326
|
chains,
|
|
7261
7327
|
rpcUrls,
|
|
7262
7328
|
requireAssertionPerSignature,
|
|
7329
|
+
deferDeviceProtection,
|
|
7263
7330
|
// Recovery callbacks. Forwarding these is what makes recovery
|
|
7264
7331
|
// configurable at all from <DecaneKit>; before, they were silently
|
|
7265
7332
|
// dropped and the headless client was constructed without them.
|
|
@@ -7280,6 +7347,7 @@ function DecaneKit({ config, children }) {
|
|
|
7280
7347
|
promptPin,
|
|
7281
7348
|
promptUnlockPassword,
|
|
7282
7349
|
requireAssertionPerSignature,
|
|
7350
|
+
deferDeviceProtection,
|
|
7283
7351
|
offerRecoveryAtSignup,
|
|
7284
7352
|
onRecoveryShareOffer,
|
|
7285
7353
|
onRecoveryFileReady,
|
|
@@ -7526,6 +7594,16 @@ function DecaneKit({ config, children }) {
|
|
|
7526
7594
|
if (!sdk) throw new Error("Social wallet not connected");
|
|
7527
7595
|
return sdk.addPasskey();
|
|
7528
7596
|
}, []);
|
|
7597
|
+
const socialDeviceProtected = useCallback3(async () => {
|
|
7598
|
+
const sdk = socialRef.current;
|
|
7599
|
+
if (!sdk) return false;
|
|
7600
|
+
return sdk.deviceProtected();
|
|
7601
|
+
}, []);
|
|
7602
|
+
const socialProtectDevice = useCallback3(async () => {
|
|
7603
|
+
const sdk = socialRef.current;
|
|
7604
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
7605
|
+
return sdk.protectDevice();
|
|
7606
|
+
}, []);
|
|
7529
7607
|
const socialSetUnlockPassword = useCallback3(async (password) => {
|
|
7530
7608
|
const sdk = socialRef.current;
|
|
7531
7609
|
if (!sdk) throw new Error("Social wallet not connected");
|
|
@@ -7536,6 +7614,14 @@ function DecaneKit({ config, children }) {
|
|
|
7536
7614
|
if (!sdk) return false;
|
|
7537
7615
|
return sdk.canUnlockWithPassword();
|
|
7538
7616
|
}, []);
|
|
7617
|
+
const socialForgetLastUser = useCallback3(() => {
|
|
7618
|
+
socialRef.current?.forgetLastUser();
|
|
7619
|
+
const sdk = socialRef.current;
|
|
7620
|
+
if (sdk) {
|
|
7621
|
+
sdk.canSignInWithPasskey().then((can) => setSocialAuth((p) => ({ ...p, canUsePasskey: can }))).catch(() => {
|
|
7622
|
+
});
|
|
7623
|
+
}
|
|
7624
|
+
}, []);
|
|
7539
7625
|
const socialUnlockWithPassword = useCallback3(async (password) => {
|
|
7540
7626
|
const sdk = socialRef.current;
|
|
7541
7627
|
if (!sdk) throw new Error("Social wallet not connected");
|
|
@@ -7610,8 +7696,11 @@ function DecaneKit({ config, children }) {
|
|
|
7610
7696
|
socialHasRecoveryShare,
|
|
7611
7697
|
socialHasPasskey,
|
|
7612
7698
|
socialAddPasskey,
|
|
7699
|
+
socialDeviceProtected,
|
|
7700
|
+
socialProtectDevice,
|
|
7613
7701
|
socialSetUnlockPassword,
|
|
7614
7702
|
socialCanUnlockWithPassword,
|
|
7703
|
+
socialForgetLastUser,
|
|
7615
7704
|
socialUnlockWithPassword,
|
|
7616
7705
|
socialRemoveUnlockPassword,
|
|
7617
7706
|
socialShouldPromptRecovery,
|
|
@@ -7919,8 +8008,11 @@ function useSocialWallet() {
|
|
|
7919
8008
|
socialHasRecoveryShare,
|
|
7920
8009
|
socialHasPasskey,
|
|
7921
8010
|
socialAddPasskey,
|
|
8011
|
+
socialDeviceProtected,
|
|
8012
|
+
socialProtectDevice,
|
|
7922
8013
|
socialSetUnlockPassword,
|
|
7923
8014
|
socialCanUnlockWithPassword,
|
|
8015
|
+
socialForgetLastUser,
|
|
7924
8016
|
socialUnlockWithPassword,
|
|
7925
8017
|
socialRemoveUnlockPassword,
|
|
7926
8018
|
socialShouldPromptRecovery,
|
|
@@ -8020,6 +8112,16 @@ function useSocialWallet() {
|
|
|
8020
8112
|
throw e;
|
|
8021
8113
|
}
|
|
8022
8114
|
}, [socialAddPasskey]);
|
|
8115
|
+
const deviceProtected = useCallback4(() => run(() => socialDeviceProtected()), [socialDeviceProtected]);
|
|
8116
|
+
const protectDevice = useCallback4(async () => {
|
|
8117
|
+
setError(null);
|
|
8118
|
+
try {
|
|
8119
|
+
return await socialProtectDevice();
|
|
8120
|
+
} catch (e) {
|
|
8121
|
+
setError(e.message);
|
|
8122
|
+
throw e;
|
|
8123
|
+
}
|
|
8124
|
+
}, [socialProtectDevice]);
|
|
8023
8125
|
const setUnlockPassword = useCallback4(async (password) => {
|
|
8024
8126
|
setError(null);
|
|
8025
8127
|
try {
|
|
@@ -8029,6 +8131,7 @@ function useSocialWallet() {
|
|
|
8029
8131
|
throw e;
|
|
8030
8132
|
}
|
|
8031
8133
|
}, [socialSetUnlockPassword]);
|
|
8134
|
+
const forgetLastUser = useCallback4(() => socialForgetLastUser(), [socialForgetLastUser]);
|
|
8032
8135
|
const canUnlockWithPassword = useCallback4(
|
|
8033
8136
|
() => socialCanUnlockWithPassword(),
|
|
8034
8137
|
[socialCanUnlockWithPassword]
|
|
@@ -8107,6 +8210,10 @@ function useSocialWallet() {
|
|
|
8107
8210
|
// Passkey management for THIS device
|
|
8108
8211
|
hasPasskey,
|
|
8109
8212
|
addPasskey,
|
|
8213
|
+
// Deferred protection (deferDeviceProtection): whether this device holds
|
|
8214
|
+
// a wrapped share, and the call that stores one — passkey or password.
|
|
8215
|
+
deviceProtected,
|
|
8216
|
+
protectDevice,
|
|
8110
8217
|
// "Unlock with your password": setUnlockPassword needs a live session;
|
|
8111
8218
|
// canUnlockWithPassword and unlockWithPassword are the signed-out pair that
|
|
8112
8219
|
// open one without an identity provider or a passkey.
|
|
@@ -8114,6 +8221,8 @@ function useSocialWallet() {
|
|
|
8114
8221
|
canUnlockWithPassword,
|
|
8115
8222
|
unlockWithPassword,
|
|
8116
8223
|
removeUnlockPassword,
|
|
8224
|
+
// "Use a different account": stop offering the account that signed out.
|
|
8225
|
+
forgetLastUser,
|
|
8117
8226
|
// Recovery (2-of-3 Shamir share set)
|
|
8118
8227
|
rotateShares,
|
|
8119
8228
|
exportPortableBackup,
|