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.cjs
CHANGED
|
@@ -3689,6 +3689,29 @@ function clearPersisted(key) {
|
|
|
3689
3689
|
} catch {
|
|
3690
3690
|
}
|
|
3691
3691
|
}
|
|
3692
|
+
function makeLastUserKey(apiKey) {
|
|
3693
|
+
return `${makePersistKey(apiKey)}:last-user`;
|
|
3694
|
+
}
|
|
3695
|
+
function loadLastUser(key) {
|
|
3696
|
+
try {
|
|
3697
|
+
const raw = typeof localStorage !== "undefined" ? localStorage.getItem(key) : null;
|
|
3698
|
+
return raw ? JSON.parse(raw).userId ?? null : null;
|
|
3699
|
+
} catch {
|
|
3700
|
+
return null;
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
function saveLastUser(key, userId) {
|
|
3704
|
+
try {
|
|
3705
|
+
localStorage.setItem(key, JSON.stringify({ userId }));
|
|
3706
|
+
} catch {
|
|
3707
|
+
}
|
|
3708
|
+
}
|
|
3709
|
+
function clearLastUser(key) {
|
|
3710
|
+
try {
|
|
3711
|
+
localStorage.removeItem(key);
|
|
3712
|
+
} catch {
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3692
3715
|
function sessionPersistKey(apiKey) {
|
|
3693
3716
|
return `decane:sess:${apiKey.slice(0, 20)}`;
|
|
3694
3717
|
}
|
|
@@ -3727,6 +3750,7 @@ async function createDecaneConnect(config) {
|
|
|
3727
3750
|
const rpId = config.rpId ?? (typeof window !== "undefined" ? window.location.hostname : "localhost");
|
|
3728
3751
|
const rpName = config.rpName ?? "Decane Connect";
|
|
3729
3752
|
const PERSIST_KEY = makePersistKey(config.apiKey);
|
|
3753
|
+
const LAST_USER_KEY = makeLastUserKey(config.apiKey);
|
|
3730
3754
|
const teeUrl = config.teeUrl ?? DEFAULT_TEE_URL;
|
|
3731
3755
|
const events = new EventEmitter();
|
|
3732
3756
|
const api = new ApiClient(baseUrl, config.apiKey, config.appId);
|
|
@@ -3739,8 +3763,10 @@ async function createDecaneConnect(config) {
|
|
|
3739
3763
|
);
|
|
3740
3764
|
let auth = null;
|
|
3741
3765
|
let persisted = loadPersisted(PERSIST_KEY);
|
|
3766
|
+
let lastUserId = loadLastUser(LAST_USER_KEY);
|
|
3742
3767
|
let ethProvider = null;
|
|
3743
3768
|
const persistSession = config.persistSession !== false;
|
|
3769
|
+
const deferDeviceProtection = config.deferDeviceProtection === true;
|
|
3744
3770
|
const serverAssistedRecovery = true;
|
|
3745
3771
|
const RECOVERY_OFFER_TIMEOUT_MS = 5 * 6e4;
|
|
3746
3772
|
const selfHealAttempted = /* @__PURE__ */ new Set();
|
|
@@ -3884,6 +3910,10 @@ async function createDecaneConnect(config) {
|
|
|
3884
3910
|
const stored = await storage.getShare(persisted.userId);
|
|
3885
3911
|
return hasPasswordUnlock(stored) ? stored : null;
|
|
3886
3912
|
}
|
|
3913
|
+
if (lastUserId) {
|
|
3914
|
+
const stored = await storage.getShare(lastUserId);
|
|
3915
|
+
return hasPasswordUnlock(stored) ? stored : null;
|
|
3916
|
+
}
|
|
3887
3917
|
const candidates = (await storage.listShares()).filter(hasPasswordUnlock);
|
|
3888
3918
|
return candidates.length === 1 ? candidates[0] : null;
|
|
3889
3919
|
}
|
|
@@ -4048,11 +4078,12 @@ async function createDecaneConnect(config) {
|
|
|
4048
4078
|
}
|
|
4049
4079
|
async function firstTimeSetup(userId, jwt) {
|
|
4050
4080
|
prewarmAttestation(teeUrl);
|
|
4051
|
-
let unlockMethod;
|
|
4052
|
-
let wrappingKey;
|
|
4081
|
+
let unlockMethod = "password";
|
|
4082
|
+
let wrappingKey = null;
|
|
4053
4083
|
let meta = {};
|
|
4054
4084
|
let unlockTokenKey = null;
|
|
4055
|
-
if (
|
|
4085
|
+
if (deferDeviceProtection) {
|
|
4086
|
+
} else if (isPasskeySupported()) {
|
|
4056
4087
|
const prfSalt = generateRandomBytes(32);
|
|
4057
4088
|
try {
|
|
4058
4089
|
const { credentialId, prfOutput } = await registerWebAuthnCredential({ api, jwt, rpId, rpName, prfSalt });
|
|
@@ -4093,32 +4124,37 @@ async function createDecaneConnect(config) {
|
|
|
4093
4124
|
} finally {
|
|
4094
4125
|
zeroize(master);
|
|
4095
4126
|
}
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4127
|
+
let wrappedDeviceShare = null;
|
|
4128
|
+
let iv = null;
|
|
4129
|
+
if (wrappingKey) {
|
|
4130
|
+
iv = generateRandomBytes(12);
|
|
4131
|
+
wrappedDeviceShare = await aesGcmEncrypt(
|
|
4132
|
+
wrappingKey,
|
|
4133
|
+
shareSet.device,
|
|
4134
|
+
iv
|
|
4135
|
+
);
|
|
4136
|
+
await storage.putShare({
|
|
4137
|
+
userId,
|
|
4138
|
+
wrappedDeviceShare,
|
|
4139
|
+
iv,
|
|
4140
|
+
unlockMethod,
|
|
4141
|
+
credentialId: meta.credentialId ? b64uToBuffer(meta.credentialId) : void 0,
|
|
4142
|
+
prfSalt: meta.prfSalt ? b64uToBuffer(meta.prfSalt) : void 0,
|
|
4143
|
+
pinSalt: meta.pinSalt ? b64uToBuffer(meta.pinSalt) : void 0,
|
|
4144
|
+
pinIterations: meta.pinIterations,
|
|
4145
|
+
passwordSalt: meta.passwordSalt ? b64uToBuffer(meta.passwordSalt) : void 0,
|
|
4146
|
+
passwordIterations: meta.passwordIterations,
|
|
4147
|
+
version: 1
|
|
4148
|
+
});
|
|
4149
|
+
if (unlockTokenKey) await attachUnlockToken(userId, jwt, unlockTokenKey);
|
|
4150
|
+
}
|
|
4116
4151
|
const serverIv = generateRandomBytes(12);
|
|
4117
4152
|
await withDeadline(api.putShare(jwt, {
|
|
4118
4153
|
ciphertext: shareSet.server,
|
|
4119
4154
|
iv: serverIv,
|
|
4120
4155
|
wrappedShareMeta: {
|
|
4121
|
-
|
|
4156
|
+
// The server records this on the wallet_created event and nothing else.
|
|
4157
|
+
unlockMethod: deferDeviceProtection ? "deferred" : unlockMethod
|
|
4122
4158
|
// credential IDs, prfSalt, pinSalt stay on-device — never sent to server
|
|
4123
4159
|
},
|
|
4124
4160
|
evmAddress,
|
|
@@ -4127,7 +4163,7 @@ async function createDecaneConnect(config) {
|
|
|
4127
4163
|
}), remainingBudget(), "storing your wallet");
|
|
4128
4164
|
zeroize(shareSet.server);
|
|
4129
4165
|
const addresses = { evm: evmAddress, solana: solanaAddress, tron: tronAddress };
|
|
4130
|
-
if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt) {
|
|
4166
|
+
if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt && wrappedDeviceShare && iv) {
|
|
4131
4167
|
try {
|
|
4132
4168
|
await api.putRecoveryBackup(jwt, {
|
|
4133
4169
|
wrappedDeviceShare,
|
|
@@ -4210,7 +4246,7 @@ async function createDecaneConnect(config) {
|
|
|
4210
4246
|
const tee = new TeeClient(teeUrl);
|
|
4211
4247
|
const provisioned = await tee.provisionDeviceShare(jwt);
|
|
4212
4248
|
if (provisioned) {
|
|
4213
|
-
await persistProvisionedShare(userId, jwt, provisioned);
|
|
4249
|
+
if (!deferDeviceProtection) await persistProvisionedShare(userId, jwt, provisioned);
|
|
4214
4250
|
let addrs;
|
|
4215
4251
|
try {
|
|
4216
4252
|
addrs = await session.start(provisioned, jwt, recordedAddresses);
|
|
@@ -4254,7 +4290,7 @@ async function createDecaneConnect(config) {
|
|
|
4254
4290
|
});
|
|
4255
4291
|
deviceShare = recovered.deviceShare;
|
|
4256
4292
|
joinedViaPasskeySync = recovered.flow === "passkey-sync";
|
|
4257
|
-
if (!await storage.getShare(userId) && isPasskeySupported()) {
|
|
4293
|
+
if (!deferDeviceProtection && !await storage.getShare(userId) && isPasskeySupported()) {
|
|
4258
4294
|
await registerPasskeyAndBackup(userId, jwt, deviceShare);
|
|
4259
4295
|
}
|
|
4260
4296
|
} else {
|
|
@@ -4337,14 +4373,16 @@ async function createDecaneConnect(config) {
|
|
|
4337
4373
|
async function passkeyDeviceCandidate() {
|
|
4338
4374
|
if (!isPasskeySupported()) return null;
|
|
4339
4375
|
if (persisted?.userId) return passkeyLoginCandidate();
|
|
4376
|
+
if (lastUserId) return usablePasskeyShare(await storage.getShare(lastUserId));
|
|
4340
4377
|
const candidates = (await storage.listShares()).map(usablePasskeyShare).filter(Boolean);
|
|
4341
4378
|
return candidates.length === 1 ? candidates[0] : null;
|
|
4342
4379
|
}
|
|
4343
4380
|
async function canOfferPasskey() {
|
|
4344
4381
|
if (!isPasskeySupported()) return false;
|
|
4345
4382
|
if (await passkeyDeviceCandidate()) return true;
|
|
4346
|
-
|
|
4347
|
-
|
|
4383
|
+
const known = persisted?.userId ?? lastUserId;
|
|
4384
|
+
if (!known) return false;
|
|
4385
|
+
return await storage.getShare(known) == null;
|
|
4348
4386
|
}
|
|
4349
4387
|
async function rememberProfile(userId, profile) {
|
|
4350
4388
|
const incoming = Object.fromEntries(Object.entries(profile).filter(([, v]) => v));
|
|
@@ -4557,8 +4595,9 @@ async function createDecaneConnect(config) {
|
|
|
4557
4595
|
async signInWithPasskey() {
|
|
4558
4596
|
const candidate = await passkeyDeviceCandidate();
|
|
4559
4597
|
if (candidate) return passkeyLogin(candidate);
|
|
4560
|
-
|
|
4561
|
-
|
|
4598
|
+
const known = persisted?.userId ?? lastUserId;
|
|
4599
|
+
if (isPasskeySupported() && known && await storage.getShare(known) == null) {
|
|
4600
|
+
return passkeyLoginWithoutShare(known);
|
|
4562
4601
|
}
|
|
4563
4602
|
throw new NewDeviceError();
|
|
4564
4603
|
},
|
|
@@ -4722,6 +4761,24 @@ async function createDecaneConnect(config) {
|
|
|
4722
4761
|
zeroize(deviceShare);
|
|
4723
4762
|
}
|
|
4724
4763
|
},
|
|
4764
|
+
async deviceProtected() {
|
|
4765
|
+
if (!auth) return false;
|
|
4766
|
+
return await storage.getShare(auth.userId) !== null;
|
|
4767
|
+
},
|
|
4768
|
+
async protectDevice() {
|
|
4769
|
+
if (!auth) throw new ReconnectRequiredError();
|
|
4770
|
+
if (await storage.getShare(auth.userId)) return true;
|
|
4771
|
+
if (!serverAssistedRecovery) throw new NewDeviceError();
|
|
4772
|
+
const tee = new TeeClient(teeUrl);
|
|
4773
|
+
const provisioned = await tee.provisionDeviceShare(auth.jwt);
|
|
4774
|
+
if (!provisioned) throw new NewDeviceError();
|
|
4775
|
+
try {
|
|
4776
|
+
await persistProvisionedShare(auth.userId, auth.jwt, provisioned);
|
|
4777
|
+
} finally {
|
|
4778
|
+
zeroize(provisioned);
|
|
4779
|
+
}
|
|
4780
|
+
return await storage.getShare(auth.userId) !== null;
|
|
4781
|
+
},
|
|
4725
4782
|
async unlock() {
|
|
4726
4783
|
if (!auth) {
|
|
4727
4784
|
const candidate = await passkeyLoginCandidate();
|
|
@@ -4898,12 +4955,21 @@ async function createDecaneConnect(config) {
|
|
|
4898
4955
|
await api.revoke(auth.jwt).catch(() => {
|
|
4899
4956
|
});
|
|
4900
4957
|
}
|
|
4958
|
+
const leaving = auth?.userId ?? persisted?.userId ?? null;
|
|
4959
|
+
if (leaving) {
|
|
4960
|
+
lastUserId = leaving;
|
|
4961
|
+
saveLastUser(LAST_USER_KEY, leaving);
|
|
4962
|
+
}
|
|
4901
4963
|
auth = null;
|
|
4902
4964
|
persisted = null;
|
|
4903
4965
|
clearPersisted(PERSIST_KEY);
|
|
4904
4966
|
dropLiveSession();
|
|
4905
4967
|
events.emit("disconnected");
|
|
4906
4968
|
},
|
|
4969
|
+
forgetLastUser() {
|
|
4970
|
+
lastUserId = null;
|
|
4971
|
+
clearLastUser(LAST_USER_KEY);
|
|
4972
|
+
},
|
|
4907
4973
|
getAddresses() {
|
|
4908
4974
|
return auth?.addresses ?? persisted?.addresses ?? null;
|
|
4909
4975
|
},
|
|
@@ -7335,6 +7401,7 @@ function DecaneKit({ config, children }) {
|
|
|
7335
7401
|
chains,
|
|
7336
7402
|
rpcUrls,
|
|
7337
7403
|
requireAssertionPerSignature,
|
|
7404
|
+
deferDeviceProtection,
|
|
7338
7405
|
// Recovery callbacks. Forwarding these is what makes recovery
|
|
7339
7406
|
// configurable at all from <DecaneKit>; before, they were silently
|
|
7340
7407
|
// dropped and the headless client was constructed without them.
|
|
@@ -7355,6 +7422,7 @@ function DecaneKit({ config, children }) {
|
|
|
7355
7422
|
promptPin,
|
|
7356
7423
|
promptUnlockPassword,
|
|
7357
7424
|
requireAssertionPerSignature,
|
|
7425
|
+
deferDeviceProtection,
|
|
7358
7426
|
offerRecoveryAtSignup,
|
|
7359
7427
|
onRecoveryShareOffer,
|
|
7360
7428
|
onRecoveryFileReady,
|
|
@@ -7601,6 +7669,16 @@ function DecaneKit({ config, children }) {
|
|
|
7601
7669
|
if (!sdk) throw new Error("Social wallet not connected");
|
|
7602
7670
|
return sdk.addPasskey();
|
|
7603
7671
|
}, []);
|
|
7672
|
+
const socialDeviceProtected = (0, import_react5.useCallback)(async () => {
|
|
7673
|
+
const sdk = socialRef.current;
|
|
7674
|
+
if (!sdk) return false;
|
|
7675
|
+
return sdk.deviceProtected();
|
|
7676
|
+
}, []);
|
|
7677
|
+
const socialProtectDevice = (0, import_react5.useCallback)(async () => {
|
|
7678
|
+
const sdk = socialRef.current;
|
|
7679
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
7680
|
+
return sdk.protectDevice();
|
|
7681
|
+
}, []);
|
|
7604
7682
|
const socialSetUnlockPassword = (0, import_react5.useCallback)(async (password) => {
|
|
7605
7683
|
const sdk = socialRef.current;
|
|
7606
7684
|
if (!sdk) throw new Error("Social wallet not connected");
|
|
@@ -7611,6 +7689,14 @@ function DecaneKit({ config, children }) {
|
|
|
7611
7689
|
if (!sdk) return false;
|
|
7612
7690
|
return sdk.canUnlockWithPassword();
|
|
7613
7691
|
}, []);
|
|
7692
|
+
const socialForgetLastUser = (0, import_react5.useCallback)(() => {
|
|
7693
|
+
socialRef.current?.forgetLastUser();
|
|
7694
|
+
const sdk = socialRef.current;
|
|
7695
|
+
if (sdk) {
|
|
7696
|
+
sdk.canSignInWithPasskey().then((can) => setSocialAuth((p) => ({ ...p, canUsePasskey: can }))).catch(() => {
|
|
7697
|
+
});
|
|
7698
|
+
}
|
|
7699
|
+
}, []);
|
|
7614
7700
|
const socialUnlockWithPassword = (0, import_react5.useCallback)(async (password) => {
|
|
7615
7701
|
const sdk = socialRef.current;
|
|
7616
7702
|
if (!sdk) throw new Error("Social wallet not connected");
|
|
@@ -7685,8 +7771,11 @@ function DecaneKit({ config, children }) {
|
|
|
7685
7771
|
socialHasRecoveryShare,
|
|
7686
7772
|
socialHasPasskey,
|
|
7687
7773
|
socialAddPasskey,
|
|
7774
|
+
socialDeviceProtected,
|
|
7775
|
+
socialProtectDevice,
|
|
7688
7776
|
socialSetUnlockPassword,
|
|
7689
7777
|
socialCanUnlockWithPassword,
|
|
7778
|
+
socialForgetLastUser,
|
|
7690
7779
|
socialUnlockWithPassword,
|
|
7691
7780
|
socialRemoveUnlockPassword,
|
|
7692
7781
|
socialShouldPromptRecovery,
|
|
@@ -7994,8 +8083,11 @@ function useSocialWallet() {
|
|
|
7994
8083
|
socialHasRecoveryShare,
|
|
7995
8084
|
socialHasPasskey,
|
|
7996
8085
|
socialAddPasskey,
|
|
8086
|
+
socialDeviceProtected,
|
|
8087
|
+
socialProtectDevice,
|
|
7997
8088
|
socialSetUnlockPassword,
|
|
7998
8089
|
socialCanUnlockWithPassword,
|
|
8090
|
+
socialForgetLastUser,
|
|
7999
8091
|
socialUnlockWithPassword,
|
|
8000
8092
|
socialRemoveUnlockPassword,
|
|
8001
8093
|
socialShouldPromptRecovery,
|
|
@@ -8095,6 +8187,16 @@ function useSocialWallet() {
|
|
|
8095
8187
|
throw e;
|
|
8096
8188
|
}
|
|
8097
8189
|
}, [socialAddPasskey]);
|
|
8190
|
+
const deviceProtected = (0, import_react6.useCallback)(() => run(() => socialDeviceProtected()), [socialDeviceProtected]);
|
|
8191
|
+
const protectDevice = (0, import_react6.useCallback)(async () => {
|
|
8192
|
+
setError(null);
|
|
8193
|
+
try {
|
|
8194
|
+
return await socialProtectDevice();
|
|
8195
|
+
} catch (e) {
|
|
8196
|
+
setError(e.message);
|
|
8197
|
+
throw e;
|
|
8198
|
+
}
|
|
8199
|
+
}, [socialProtectDevice]);
|
|
8098
8200
|
const setUnlockPassword = (0, import_react6.useCallback)(async (password) => {
|
|
8099
8201
|
setError(null);
|
|
8100
8202
|
try {
|
|
@@ -8104,6 +8206,7 @@ function useSocialWallet() {
|
|
|
8104
8206
|
throw e;
|
|
8105
8207
|
}
|
|
8106
8208
|
}, [socialSetUnlockPassword]);
|
|
8209
|
+
const forgetLastUser = (0, import_react6.useCallback)(() => socialForgetLastUser(), [socialForgetLastUser]);
|
|
8107
8210
|
const canUnlockWithPassword = (0, import_react6.useCallback)(
|
|
8108
8211
|
() => socialCanUnlockWithPassword(),
|
|
8109
8212
|
[socialCanUnlockWithPassword]
|
|
@@ -8182,6 +8285,10 @@ function useSocialWallet() {
|
|
|
8182
8285
|
// Passkey management for THIS device
|
|
8183
8286
|
hasPasskey,
|
|
8184
8287
|
addPasskey,
|
|
8288
|
+
// Deferred protection (deferDeviceProtection): whether this device holds
|
|
8289
|
+
// a wrapped share, and the call that stores one — passkey or password.
|
|
8290
|
+
deviceProtected,
|
|
8291
|
+
protectDevice,
|
|
8185
8292
|
// "Unlock with your password": setUnlockPassword needs a live session;
|
|
8186
8293
|
// canUnlockWithPassword and unlockWithPassword are the signed-out pair that
|
|
8187
8294
|
// open one without an identity provider or a passkey.
|
|
@@ -8189,6 +8296,8 @@ function useSocialWallet() {
|
|
|
8189
8296
|
canUnlockWithPassword,
|
|
8190
8297
|
unlockWithPassword,
|
|
8191
8298
|
removeUnlockPassword,
|
|
8299
|
+
// "Use a different account": stop offering the account that signed out.
|
|
8300
|
+
forgetLastUser,
|
|
8192
8301
|
// Recovery (2-of-3 Shamir share set)
|
|
8193
8302
|
rotateShares,
|
|
8194
8303
|
exportPortableBackup,
|