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