decane-connect-kit 2.26.1 → 2.28.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 +217 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -6
- package/dist/index.d.ts +112 -6
- package/dist/index.js +213 -61
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ __export(errors_exports, {
|
|
|
14
14
|
AttestationError: () => AttestationError,
|
|
15
15
|
CallSequenceError: () => CallSequenceError,
|
|
16
16
|
DecaneConnectError: () => DecaneConnectError,
|
|
17
|
+
EnclaveHttpError: () => EnclaveHttpError,
|
|
17
18
|
ExportNotAuthorizedError: () => ExportNotAuthorizedError,
|
|
18
19
|
InvalidAccessTokenError: () => InvalidAccessTokenError,
|
|
19
20
|
InvalidApiKeyError: () => InvalidApiKeyError,
|
|
@@ -24,6 +25,7 @@ __export(errors_exports, {
|
|
|
24
25
|
OriginNotAllowedError: () => OriginNotAllowedError,
|
|
25
26
|
PromptPasswordNotConfiguredError: () => PromptPasswordNotConfiguredError,
|
|
26
27
|
PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
|
|
28
|
+
ProvisioningRateLimitedError: () => ProvisioningRateLimitedError,
|
|
27
29
|
ProvisioningTimeoutError: () => ProvisioningTimeoutError,
|
|
28
30
|
ReconnectRequiredError: () => ReconnectRequiredError,
|
|
29
31
|
RecoveryFailedError: () => RecoveryFailedError,
|
|
@@ -34,11 +36,13 @@ __export(errors_exports, {
|
|
|
34
36
|
UnlockRequiredError: () => UnlockRequiredError,
|
|
35
37
|
UnlockTokenRejectedError: () => UnlockTokenRejectedError,
|
|
36
38
|
UserCancelledError: () => UserCancelledError,
|
|
39
|
+
WalletEnrolmentError: () => WalletEnrolmentError,
|
|
40
|
+
WalletExistsError: () => WalletExistsError,
|
|
37
41
|
WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
|
|
38
42
|
WrongPasswordError: () => WrongPasswordError,
|
|
39
43
|
WrongUnlockPasswordError: () => WrongUnlockPasswordError
|
|
40
44
|
});
|
|
41
|
-
var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, ProvisioningTimeoutError, NewDeviceError, StaleDeviceShareError, PromptPinNotConfiguredError, PromptPasswordNotConfiguredError, WrongUnlockPasswordError, InvalidAccessTokenError, UnlockTokenRejectedError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
|
|
45
|
+
var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, ProvisioningTimeoutError, NewDeviceError, StaleDeviceShareError, PromptPinNotConfiguredError, PromptPasswordNotConfiguredError, WrongUnlockPasswordError, InvalidAccessTokenError, UnlockTokenRejectedError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError, WalletExistsError, WalletEnrolmentError, ProvisioningRateLimitedError, EnclaveHttpError;
|
|
42
46
|
var init_errors = __esm({
|
|
43
47
|
"src/social/errors.ts"() {
|
|
44
48
|
"use strict";
|
|
@@ -187,6 +191,36 @@ var init_errors = __esm({
|
|
|
187
191
|
super("EXPORT_NOT_AUTHORIZED", "Export requires a fresh security check", true);
|
|
188
192
|
}
|
|
189
193
|
};
|
|
194
|
+
WalletExistsError = class extends DecaneConnectError {
|
|
195
|
+
constructor() {
|
|
196
|
+
super("WALLET_EXISTS", "This account already has a wallet; opening it instead.", true);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
WalletEnrolmentError = class extends DecaneConnectError {
|
|
200
|
+
constructor(cause) {
|
|
201
|
+
super(
|
|
202
|
+
"WALLET_ENROLMENT_FAILED",
|
|
203
|
+
"Your wallet could not be set up: the signing enclave did not accept it. Nothing was created \u2014 sign in again to try again.",
|
|
204
|
+
true
|
|
205
|
+
);
|
|
206
|
+
if (cause !== void 0) this.cause = cause;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
ProvisioningRateLimitedError = class extends DecaneConnectError {
|
|
210
|
+
constructor() {
|
|
211
|
+
super(
|
|
212
|
+
"PROVISIONING_RATE_LIMITED",
|
|
213
|
+
"This wallet has been opened on too many devices or tabs in the last hour. Wait a little and sign in again.",
|
|
214
|
+
true
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
EnclaveHttpError = class extends DecaneConnectError {
|
|
219
|
+
constructor(status, message) {
|
|
220
|
+
super("ENCLAVE_HTTP", message, status >= 500);
|
|
221
|
+
this.status = status;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
190
224
|
}
|
|
191
225
|
});
|
|
192
226
|
|
|
@@ -1995,7 +2029,9 @@ var ERROR_MAP = {
|
|
|
1995
2029
|
// The backend's authMiddleware code for an expired/revoked JWT. Surfacing it
|
|
1996
2030
|
// as ReconnectRequiredError routes callers to the sign-in UI instead of a
|
|
1997
2031
|
// generic failure message.
|
|
1998
|
-
UNAUTHORIZED: ReconnectRequiredError
|
|
2032
|
+
UNAUTHORIZED: ReconnectRequiredError,
|
|
2033
|
+
// A restorable wallet already exists: the client provisions instead of creating.
|
|
2034
|
+
WALLET_EXISTS: WalletExistsError
|
|
1999
2035
|
};
|
|
2000
2036
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
2001
2037
|
var ApiClient = class {
|
|
@@ -2114,7 +2150,8 @@ var ApiClient = class {
|
|
|
2114
2150
|
wrappedShareMeta: btoa(JSON.stringify(share.wrappedShareMeta)),
|
|
2115
2151
|
evmAddress: share.evmAddress,
|
|
2116
2152
|
solanaAddress: share.solanaAddress,
|
|
2117
|
-
tronAddress: share.tronAddress
|
|
2153
|
+
tronAddress: share.tronAddress,
|
|
2154
|
+
...share.pendingEnrolment === void 0 ? {} : { pendingEnrolment: share.pendingEnrolment }
|
|
2118
2155
|
})
|
|
2119
2156
|
});
|
|
2120
2157
|
}
|
|
@@ -2399,7 +2436,7 @@ function dropCollateral(teeUrl) {
|
|
|
2399
2436
|
// src/social/attestation.ts
|
|
2400
2437
|
init_errors();
|
|
2401
2438
|
|
|
2402
|
-
//
|
|
2439
|
+
// ../infra/tee-measurement.json
|
|
2403
2440
|
var tee_measurement_default = {
|
|
2404
2441
|
signerVersion: "0.2.10",
|
|
2405
2442
|
acceptedMrtd: [
|
|
@@ -2698,7 +2735,8 @@ var TeeClient = class {
|
|
|
2698
2735
|
});
|
|
2699
2736
|
if (!res.ok) {
|
|
2700
2737
|
const detail = await res.text().catch(() => "");
|
|
2701
|
-
throw new
|
|
2738
|
+
throw new EnclaveHttpError(
|
|
2739
|
+
res.status,
|
|
2702
2740
|
`auth share enrolment failed (HTTP ${res.status})${detail ? `: ${detail.slice(0, 200)}` : ""}`
|
|
2703
2741
|
);
|
|
2704
2742
|
}
|
|
@@ -2719,9 +2757,11 @@ var TeeClient = class {
|
|
|
2719
2757
|
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" }
|
|
2720
2758
|
});
|
|
2721
2759
|
if (res.status === 409) return null;
|
|
2760
|
+
if (res.status === 429) throw new ProvisioningRateLimitedError();
|
|
2722
2761
|
if (!res.ok) {
|
|
2723
2762
|
const detail = await res.text().catch(() => "");
|
|
2724
|
-
throw new
|
|
2763
|
+
throw new EnclaveHttpError(
|
|
2764
|
+
res.status,
|
|
2725
2765
|
`device share provisioning failed (HTTP ${res.status})${detail ? `: ${detail.slice(0, 200)}` : ""}`
|
|
2726
2766
|
);
|
|
2727
2767
|
}
|
|
@@ -2936,6 +2976,35 @@ var SessionManager = class {
|
|
|
2936
2976
|
}
|
|
2937
2977
|
};
|
|
2938
2978
|
|
|
2979
|
+
// src/social/identityTier.ts
|
|
2980
|
+
function resolveProtectionTier(config) {
|
|
2981
|
+
if (config.protection) return config.protection;
|
|
2982
|
+
return config.deferDeviceProtection === true ? "identity" : "device";
|
|
2983
|
+
}
|
|
2984
|
+
function resumeAcrossTabsDefault(config, identityTier) {
|
|
2985
|
+
return config.resumeSessionAcrossTabs ?? identityTier;
|
|
2986
|
+
}
|
|
2987
|
+
var ENROL_BACKOFF_MS = [0, 1e3, 3e3];
|
|
2988
|
+
function enrolmentRetryable(status) {
|
|
2989
|
+
return status !== 429 && status !== 403;
|
|
2990
|
+
}
|
|
2991
|
+
function shouldTryProvisionFirst(identityTier, hasStoredShare) {
|
|
2992
|
+
return identityTier || !hasStoredShare;
|
|
2993
|
+
}
|
|
2994
|
+
function routeAfterProvision(input) {
|
|
2995
|
+
switch (input.outcome) {
|
|
2996
|
+
case "share":
|
|
2997
|
+
return "session";
|
|
2998
|
+
case "rate-limited":
|
|
2999
|
+
return "fail";
|
|
3000
|
+
case "not-enrolled":
|
|
3001
|
+
if (!input.identityTier) return "recover";
|
|
3002
|
+
return input.hasStoredShare ? "migrate" : "new-device";
|
|
3003
|
+
case "error":
|
|
3004
|
+
return input.identityTier ? "fail" : "recover";
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
|
|
2939
3008
|
// src/social/crypto/shares.ts
|
|
2940
3009
|
import { secp256k1 } from "@noble/curves/secp256k1";
|
|
2941
3010
|
import { split, combine } from "shamir-secret-sharing";
|
|
@@ -3846,7 +3915,8 @@ function clearSessionPersist(key) {
|
|
|
3846
3915
|
async function createDecaneConnect(config) {
|
|
3847
3916
|
if (!config.appId) throw new Error("decane-connect-kit: appId is required");
|
|
3848
3917
|
if (!config.apiKey) throw new Error("decane-connect-kit: apiKey is required");
|
|
3849
|
-
|
|
3918
|
+
const identityTier = resolveProtectionTier(config) === "identity";
|
|
3919
|
+
useCrossTabSessionStore(resumeAcrossTabsDefault(config, identityTier));
|
|
3850
3920
|
if (!config.chains?.length)
|
|
3851
3921
|
throw new Error("decane-connect-kit: at least one chain is required");
|
|
3852
3922
|
if (config.rotateOnNewDeviceRecovery && !config.onRecoveryRotated) {
|
|
@@ -3884,7 +3954,6 @@ async function createDecaneConnect(config) {
|
|
|
3884
3954
|
let lastUserId = loadLastUser(LAST_USER_KEY) ?? (LEGACY_LAST_USER_KEY !== LAST_USER_KEY ? loadLastUser(LEGACY_LAST_USER_KEY) : null);
|
|
3885
3955
|
let ethProvider = null;
|
|
3886
3956
|
const persistSession = config.persistSession !== false;
|
|
3887
|
-
const deferDeviceProtection = config.deferDeviceProtection === true;
|
|
3888
3957
|
const serverAssistedRecovery = true;
|
|
3889
3958
|
const RECOVERY_OFFER_TIMEOUT_MS = 5 * 6e4;
|
|
3890
3959
|
const selfHealAttempted = /* @__PURE__ */ new Set();
|
|
@@ -3905,22 +3974,27 @@ async function createDecaneConnect(config) {
|
|
|
3905
3974
|
zeroize(share);
|
|
3906
3975
|
}
|
|
3907
3976
|
}
|
|
3908
|
-
async function
|
|
3977
|
+
async function enrolBlocking(jwt, share, budgetMs) {
|
|
3978
|
+
const startedAt = Date.now();
|
|
3909
3979
|
try {
|
|
3910
|
-
|
|
3980
|
+
let lastError;
|
|
3981
|
+
for (const wait of ENROL_BACKOFF_MS) {
|
|
3982
|
+
if (wait) await new Promise((r) => setTimeout(r, wait));
|
|
3911
3983
|
try {
|
|
3912
|
-
await
|
|
3984
|
+
await withDeadline(
|
|
3985
|
+
new TeeClient(teeUrl).enrolAuthShare(jwt, share),
|
|
3986
|
+
budgetMs - (Date.now() - startedAt),
|
|
3987
|
+
"enrolling your wallet"
|
|
3988
|
+
);
|
|
3913
3989
|
return;
|
|
3914
3990
|
} catch (e) {
|
|
3915
|
-
if (
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
"decane-connect-kit: could not enrol this wallet for server-assisted recovery. The wallet is fine and fully usable; it will be re-enrolled automatically on the next unlock. Until then a brand-new device would need a passkey backup or a recovery file.",
|
|
3919
|
-
e
|
|
3920
|
-
);
|
|
3921
|
-
}
|
|
3991
|
+
if (e instanceof AttestationError || e instanceof ProvisioningTimeoutError) throw e;
|
|
3992
|
+
lastError = e;
|
|
3993
|
+
if (!enrolmentRetryable(e.status)) break;
|
|
3922
3994
|
}
|
|
3923
3995
|
}
|
|
3996
|
+
events.emit("enrolment-failed");
|
|
3997
|
+
throw new WalletEnrolmentError(lastError);
|
|
3924
3998
|
} finally {
|
|
3925
3999
|
zeroize(share);
|
|
3926
4000
|
}
|
|
@@ -4016,6 +4090,7 @@ async function createDecaneConnect(config) {
|
|
|
4016
4090
|
else scheduleAccessTokenExpiry(saved.jwt, saved.expiresAt);
|
|
4017
4091
|
auth = { jwt: saved.jwt, userId: saved.userId, addresses: saved.addresses };
|
|
4018
4092
|
persisted = { userId: saved.userId, addresses: saved.addresses };
|
|
4093
|
+
if (saved.handle) armTokenRefresh(saved.jwt);
|
|
4019
4094
|
} catch {
|
|
4020
4095
|
dropLiveSession();
|
|
4021
4096
|
}
|
|
@@ -4025,6 +4100,13 @@ async function createDecaneConnect(config) {
|
|
|
4025
4100
|
if (LEGACY_SESSION_KEY !== SESSION_KEY) clearSessionPersist(LEGACY_SESSION_KEY);
|
|
4026
4101
|
}
|
|
4027
4102
|
events.on("session-expired", () => dropLiveSession());
|
|
4103
|
+
function armTokenRefresh(jwt) {
|
|
4104
|
+
const expiresAt = decodeAccessToken(jwt)?.expiresAt;
|
|
4105
|
+
if (expiresAt) scheduleTokenRefresh(jwt, expiresAt);
|
|
4106
|
+
}
|
|
4107
|
+
events.on("connected", () => {
|
|
4108
|
+
if (auth) armTokenRefresh(auth.jwt);
|
|
4109
|
+
});
|
|
4028
4110
|
async function passwordSetup() {
|
|
4029
4111
|
if (!config.promptUnlockPassword) throw new PromptPasswordNotConfiguredError();
|
|
4030
4112
|
const password = await config.promptUnlockPassword({ kind: "set" });
|
|
@@ -4249,7 +4331,7 @@ async function createDecaneConnect(config) {
|
|
|
4249
4331
|
let wrappingKey = null;
|
|
4250
4332
|
let meta = {};
|
|
4251
4333
|
let unlockTokenKey = null;
|
|
4252
|
-
if (
|
|
4334
|
+
if (identityTier) {
|
|
4253
4335
|
} else if (isPasskeySupported()) {
|
|
4254
4336
|
const prfSalt = generateRandomBytes(32);
|
|
4255
4337
|
try {
|
|
@@ -4316,19 +4398,47 @@ async function createDecaneConnect(config) {
|
|
|
4316
4398
|
if (unlockTokenKey) await attachUnlockToken(userId, jwt, unlockTokenKey);
|
|
4317
4399
|
}
|
|
4318
4400
|
const serverIv = generateRandomBytes(12);
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4401
|
+
try {
|
|
4402
|
+
await withDeadline(api.putShare(jwt, {
|
|
4403
|
+
ciphertext: shareSet.server,
|
|
4404
|
+
iv: serverIv,
|
|
4405
|
+
wrappedShareMeta: {
|
|
4406
|
+
// The server records this on the wallet_created event and nothing else.
|
|
4407
|
+
unlockMethod: identityTier ? "identity" : unlockMethod
|
|
4408
|
+
// credential IDs, prfSalt, pinSalt stay on-device — never sent to server
|
|
4409
|
+
},
|
|
4410
|
+
evmAddress,
|
|
4411
|
+
solanaAddress,
|
|
4412
|
+
tronAddress,
|
|
4413
|
+
// Provisional until the enclave has sealed the auth share, just below. A
|
|
4414
|
+
// backend that knows the flag keeps this row invisible to hasShare until
|
|
4415
|
+
// then, so a creation that fails past this point leaves no wallet behind
|
|
4416
|
+
// and the next sign-in simply creates again — with a new key, which is
|
|
4417
|
+
// why the backend also replaces the recorded addresses outright.
|
|
4418
|
+
pendingEnrolment: true
|
|
4419
|
+
}), remainingBudget(), "storing your wallet");
|
|
4420
|
+
} catch (e) {
|
|
4421
|
+
if (e instanceof WalletExistsError) {
|
|
4422
|
+
zeroize(shareSet.server);
|
|
4423
|
+
zeroize(shareSet.device);
|
|
4424
|
+
zeroize(shareSet.recovery);
|
|
4425
|
+
if (wrappingKey) await storage2.deleteShare(userId).catch(() => {
|
|
4426
|
+
});
|
|
4427
|
+
events.emit("wallet-unlocking");
|
|
4428
|
+
return returningUserUnlock(userId, jwt);
|
|
4429
|
+
}
|
|
4430
|
+
throw e;
|
|
4431
|
+
}
|
|
4331
4432
|
zeroize(shareSet.server);
|
|
4433
|
+
try {
|
|
4434
|
+
await enrolBlocking(jwt, new Uint8Array(shareSet.recovery), remainingBudget());
|
|
4435
|
+
} catch (e) {
|
|
4436
|
+
zeroize(shareSet.device);
|
|
4437
|
+
zeroize(shareSet.recovery);
|
|
4438
|
+
if (wrappingKey) await storage2.deleteShare(userId).catch(() => {
|
|
4439
|
+
});
|
|
4440
|
+
throw e;
|
|
4441
|
+
}
|
|
4332
4442
|
const addresses = { evm: evmAddress, solana: solanaAddress, tron: tronAddress };
|
|
4333
4443
|
if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt && wrappedDeviceShare && iv) {
|
|
4334
4444
|
try {
|
|
@@ -4357,10 +4467,7 @@ async function createDecaneConnect(config) {
|
|
|
4357
4467
|
addresses.evm = evmAddress = confirmed.evm;
|
|
4358
4468
|
addresses.solana = solanaAddress = confirmed.solana;
|
|
4359
4469
|
addresses.tron = tronAddress = confirmed.tron ?? evmToTronAddress2(confirmed.evm);
|
|
4360
|
-
if (
|
|
4361
|
-
void enrolInBackground(jwt, new Uint8Array(shareSet.recovery));
|
|
4362
|
-
}
|
|
4363
|
-
if (config.offerRecoveryAtSignup === true && config.onRecoveryShareOffer) {
|
|
4470
|
+
if (!identityTier && config.offerRecoveryAtSignup === true && config.onRecoveryShareOffer) {
|
|
4364
4471
|
void offerRecoveryShare(
|
|
4365
4472
|
jwt,
|
|
4366
4473
|
new Uint8Array(shareSet.recovery),
|
|
@@ -4407,31 +4514,44 @@ async function createDecaneConnect(config) {
|
|
|
4407
4514
|
const stored = await storage2.getShare(userId);
|
|
4408
4515
|
let deviceShare;
|
|
4409
4516
|
let joinedViaPasskeySync = false;
|
|
4410
|
-
|
|
4411
|
-
|
|
4517
|
+
let migrating = false;
|
|
4518
|
+
if (serverAssistedRecovery && shouldTryProvisionFirst(identityTier, !!stored)) {
|
|
4519
|
+
let provisioned = null;
|
|
4520
|
+
let failure;
|
|
4521
|
+
let outcome;
|
|
4522
|
+
try {
|
|
4523
|
+
provisioned = await new TeeClient(teeUrl).provisionDeviceShare(jwt);
|
|
4524
|
+
outcome = provisioned ? "share" : "not-enrolled";
|
|
4525
|
+
} catch (e) {
|
|
4526
|
+
failure = e;
|
|
4527
|
+
outcome = e instanceof ProvisioningRateLimitedError ? "rate-limited" : "error";
|
|
4528
|
+
}
|
|
4529
|
+
const route = routeAfterProvision({ identityTier, hasStoredShare: !!stored, outcome });
|
|
4530
|
+
if (route === "session" && provisioned) {
|
|
4531
|
+
if (!identityTier) await persistProvisionedShare(userId, jwt, provisioned);
|
|
4532
|
+
let addrs;
|
|
4412
4533
|
try {
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
if (!deferDeviceProtection) await persistProvisionedShare(userId, jwt, provisioned);
|
|
4417
|
-
let addrs;
|
|
4418
|
-
try {
|
|
4419
|
-
addrs = await session.start(provisioned, jwt, recordedAddresses);
|
|
4420
|
-
} finally {
|
|
4421
|
-
zeroize(provisioned);
|
|
4422
|
-
}
|
|
4423
|
-
return addrs;
|
|
4424
|
-
}
|
|
4425
|
-
console.warn(
|
|
4426
|
-
"decane-connect-kit: this wallet is not enrolled for server-assisted provisioning (/signer/provision returned 409 not_enrolled), so a device share cannot be issued from the JWT alone. Falling back to passkey sync, then the recovery file."
|
|
4427
|
-
);
|
|
4428
|
-
} catch (e) {
|
|
4429
|
-
console.warn(
|
|
4430
|
-
"decane-connect-kit: server-assisted provisioning unavailable; falling back to passkey/recovery-file recovery.",
|
|
4431
|
-
e
|
|
4432
|
-
);
|
|
4534
|
+
addrs = await session.start(provisioned, jwt, recordedAddresses);
|
|
4535
|
+
} finally {
|
|
4536
|
+
zeroize(provisioned);
|
|
4433
4537
|
}
|
|
4538
|
+
return addrs;
|
|
4539
|
+
}
|
|
4540
|
+
if (route === "fail") throw failure;
|
|
4541
|
+
if (route === "new-device") throw new NewDeviceError();
|
|
4542
|
+
if (route === "migrate") migrating = true;
|
|
4543
|
+
if (route === "recover" && outcome === "not-enrolled") {
|
|
4544
|
+
console.warn(
|
|
4545
|
+
"decane-connect-kit: this wallet is not enrolled for server-assisted provisioning (/signer/provision returned 409 not_enrolled), so a device share cannot be issued from the JWT alone. Falling back to passkey sync, then the recovery file."
|
|
4546
|
+
);
|
|
4547
|
+
} else if (route === "recover") {
|
|
4548
|
+
console.warn(
|
|
4549
|
+
"decane-connect-kit: server-assisted provisioning unavailable; falling back to passkey/recovery-file recovery.",
|
|
4550
|
+
failure
|
|
4551
|
+
);
|
|
4434
4552
|
}
|
|
4553
|
+
}
|
|
4554
|
+
if (!stored) {
|
|
4435
4555
|
if (!config.onRecoveryRotated) {
|
|
4436
4556
|
throw new CallSequenceError(
|
|
4437
4557
|
"Recovery rotates the share set and issues a new recovery file, but no onRecoveryRotated callback is configured to save it. Configure onRecoveryRotated before allowing recovery."
|
|
@@ -4457,7 +4577,7 @@ async function createDecaneConnect(config) {
|
|
|
4457
4577
|
});
|
|
4458
4578
|
deviceShare = recovered.deviceShare;
|
|
4459
4579
|
joinedViaPasskeySync = recovered.flow === "passkey-sync";
|
|
4460
|
-
if (!
|
|
4580
|
+
if (!identityTier && !await storage2.getShare(userId) && isPasskeySupported()) {
|
|
4461
4581
|
await registerPasskeyAndBackup(userId, jwt, deviceShare);
|
|
4462
4582
|
}
|
|
4463
4583
|
} else {
|
|
@@ -4471,6 +4591,14 @@ async function createDecaneConnect(config) {
|
|
|
4471
4591
|
} catch {
|
|
4472
4592
|
throw stored.unlockMethod === "pin" ? new InvalidPinError() : new ShareCorruptedError();
|
|
4473
4593
|
}
|
|
4594
|
+
if (migrating) {
|
|
4595
|
+
try {
|
|
4596
|
+
await enrolBlocking(jwt, new Uint8Array(deviceShare), PROVISIONING_BUDGET_MS);
|
|
4597
|
+
} catch (e) {
|
|
4598
|
+
zeroize(deviceShare);
|
|
4599
|
+
throw e;
|
|
4600
|
+
}
|
|
4601
|
+
}
|
|
4474
4602
|
}
|
|
4475
4603
|
let addresses;
|
|
4476
4604
|
try {
|
|
@@ -4485,7 +4613,11 @@ async function createDecaneConnect(config) {
|
|
|
4485
4613
|
}
|
|
4486
4614
|
throw e;
|
|
4487
4615
|
}
|
|
4488
|
-
|
|
4616
|
+
if (migrating) {
|
|
4617
|
+
events.emit("migrated-to-identity");
|
|
4618
|
+
} else if (!identityTier) {
|
|
4619
|
+
void selfHealEnrolment(userId, jwt, new Uint8Array(deviceShare));
|
|
4620
|
+
}
|
|
4489
4621
|
zeroize(deviceShare);
|
|
4490
4622
|
if (recordedAddresses && recordedAddresses.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
|
|
4491
4623
|
console.warn(
|
|
@@ -4668,7 +4800,7 @@ async function createDecaneConnect(config) {
|
|
|
4668
4800
|
if (auth && session.isActive()) {
|
|
4669
4801
|
return { addresses: auth.addresses, isNewUser: false };
|
|
4670
4802
|
}
|
|
4671
|
-
const candidate = await passkeyLoginCandidate();
|
|
4803
|
+
const candidate = identityTier ? null : await passkeyLoginCandidate();
|
|
4672
4804
|
if (candidate) {
|
|
4673
4805
|
try {
|
|
4674
4806
|
return await passkeyLogin(candidate);
|
|
@@ -4778,9 +4910,15 @@ async function createDecaneConnect(config) {
|
|
|
4778
4910
|
return handleAuthResult(result);
|
|
4779
4911
|
},
|
|
4780
4912
|
async canSignInWithPasskey() {
|
|
4913
|
+
if (identityTier) return false;
|
|
4781
4914
|
return canOfferPasskey();
|
|
4782
4915
|
},
|
|
4783
4916
|
async signInWithPasskey() {
|
|
4917
|
+
if (identityTier) {
|
|
4918
|
+
throw new CallSequenceError(
|
|
4919
|
+
"signInWithPasskey() is not available in the identity tier (protection: 'identity'); use connect()."
|
|
4920
|
+
);
|
|
4921
|
+
}
|
|
4784
4922
|
const candidate = await passkeyDeviceCandidate();
|
|
4785
4923
|
if (candidate) return passkeyLogin(candidate);
|
|
4786
4924
|
const known = persisted?.userId ?? lastUserId;
|
|
@@ -4916,6 +5054,11 @@ async function createDecaneConnect(config) {
|
|
|
4916
5054
|
return stored?.unlockMethod === "passkey";
|
|
4917
5055
|
},
|
|
4918
5056
|
async addPasskey() {
|
|
5057
|
+
if (identityTier) {
|
|
5058
|
+
throw new CallSequenceError(
|
|
5059
|
+
"addPasskey() is not available in the identity tier (protection: 'identity'): nothing is stored on the device for a passkey to protect."
|
|
5060
|
+
);
|
|
5061
|
+
}
|
|
4919
5062
|
if (!auth) throw new ReconnectRequiredError();
|
|
4920
5063
|
if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
|
|
4921
5064
|
const stored = await storage2.getShare(auth.userId);
|
|
@@ -4954,6 +5097,7 @@ async function createDecaneConnect(config) {
|
|
|
4954
5097
|
return await storage2.getShare(auth.userId) !== null;
|
|
4955
5098
|
},
|
|
4956
5099
|
async protectDevice() {
|
|
5100
|
+
if (identityTier) return false;
|
|
4957
5101
|
if (!auth) throw new ReconnectRequiredError();
|
|
4958
5102
|
if (await storage2.getShare(auth.userId)) return true;
|
|
4959
5103
|
if (!serverAssistedRecovery) throw new NewDeviceError();
|
|
@@ -4969,7 +5113,7 @@ async function createDecaneConnect(config) {
|
|
|
4969
5113
|
},
|
|
4970
5114
|
async unlock() {
|
|
4971
5115
|
if (!auth) {
|
|
4972
|
-
const candidate = await passkeyLoginCandidate();
|
|
5116
|
+
const candidate = identityTier ? null : await passkeyLoginCandidate();
|
|
4973
5117
|
if (candidate) {
|
|
4974
5118
|
await passkeyLogin(candidate);
|
|
4975
5119
|
return;
|
|
@@ -5138,6 +5282,8 @@ async function createDecaneConnect(config) {
|
|
|
5138
5282
|
},
|
|
5139
5283
|
async disconnect() {
|
|
5140
5284
|
session.end();
|
|
5285
|
+
if (tokenRefresh) clearTimeout(tokenRefresh);
|
|
5286
|
+
tokenRefresh = null;
|
|
5141
5287
|
if (auth?.jwt) {
|
|
5142
5288
|
await api.revoke(auth.jwt).catch(() => {
|
|
5143
5289
|
});
|
|
@@ -7595,6 +7741,7 @@ function DecaneKit({ config, children }) {
|
|
|
7595
7741
|
chains,
|
|
7596
7742
|
rpcUrls,
|
|
7597
7743
|
requireAssertionPerSignature,
|
|
7744
|
+
protection,
|
|
7598
7745
|
deferDeviceProtection,
|
|
7599
7746
|
resumeSessionAcrossTabs,
|
|
7600
7747
|
// Recovery callbacks. Forwarding these is what makes recovery
|
|
@@ -7617,6 +7764,7 @@ function DecaneKit({ config, children }) {
|
|
|
7617
7764
|
promptPin,
|
|
7618
7765
|
promptUnlockPassword,
|
|
7619
7766
|
requireAssertionPerSignature,
|
|
7767
|
+
protection,
|
|
7620
7768
|
deferDeviceProtection,
|
|
7621
7769
|
resumeSessionAcrossTabs,
|
|
7622
7770
|
offerRecoveryAtSignup,
|
|
@@ -8825,6 +8973,7 @@ export {
|
|
|
8825
8973
|
DecaneLogo,
|
|
8826
8974
|
DecaneMark,
|
|
8827
8975
|
DecaneWordmark,
|
|
8976
|
+
EnclaveHttpError,
|
|
8828
8977
|
ExportNotAuthorizedError,
|
|
8829
8978
|
InvalidAccessTokenError,
|
|
8830
8979
|
InvalidApiKeyError,
|
|
@@ -8836,6 +8985,7 @@ export {
|
|
|
8836
8985
|
OriginNotAllowedError,
|
|
8837
8986
|
PromptPasswordNotConfiguredError,
|
|
8838
8987
|
PromptPinNotConfiguredError,
|
|
8988
|
+
ProvisioningRateLimitedError,
|
|
8839
8989
|
ProvisioningTimeoutError,
|
|
8840
8990
|
ReconnectRequiredError,
|
|
8841
8991
|
RecoveryFailedError,
|
|
@@ -8847,6 +8997,8 @@ export {
|
|
|
8847
8997
|
UnlockRequiredError,
|
|
8848
8998
|
UnlockTokenRejectedError,
|
|
8849
8999
|
UserCancelledError,
|
|
9000
|
+
WalletEnrolmentError,
|
|
9001
|
+
WalletExistsError,
|
|
8850
9002
|
WalletSelector,
|
|
8851
9003
|
WeakPasswordError,
|
|
8852
9004
|
WebAuthnNotSupportedError,
|