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.cjs
CHANGED
|
@@ -36,6 +36,7 @@ __export(errors_exports, {
|
|
|
36
36
|
AttestationError: () => AttestationError,
|
|
37
37
|
CallSequenceError: () => CallSequenceError,
|
|
38
38
|
DecaneConnectError: () => DecaneConnectError,
|
|
39
|
+
EnclaveHttpError: () => EnclaveHttpError,
|
|
39
40
|
ExportNotAuthorizedError: () => ExportNotAuthorizedError,
|
|
40
41
|
InvalidAccessTokenError: () => InvalidAccessTokenError,
|
|
41
42
|
InvalidApiKeyError: () => InvalidApiKeyError,
|
|
@@ -46,6 +47,7 @@ __export(errors_exports, {
|
|
|
46
47
|
OriginNotAllowedError: () => OriginNotAllowedError,
|
|
47
48
|
PromptPasswordNotConfiguredError: () => PromptPasswordNotConfiguredError,
|
|
48
49
|
PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
|
|
50
|
+
ProvisioningRateLimitedError: () => ProvisioningRateLimitedError,
|
|
49
51
|
ProvisioningTimeoutError: () => ProvisioningTimeoutError,
|
|
50
52
|
ReconnectRequiredError: () => ReconnectRequiredError,
|
|
51
53
|
RecoveryFailedError: () => RecoveryFailedError,
|
|
@@ -56,11 +58,13 @@ __export(errors_exports, {
|
|
|
56
58
|
UnlockRequiredError: () => UnlockRequiredError,
|
|
57
59
|
UnlockTokenRejectedError: () => UnlockTokenRejectedError,
|
|
58
60
|
UserCancelledError: () => UserCancelledError,
|
|
61
|
+
WalletEnrolmentError: () => WalletEnrolmentError,
|
|
62
|
+
WalletExistsError: () => WalletExistsError,
|
|
59
63
|
WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
|
|
60
64
|
WrongPasswordError: () => WrongPasswordError,
|
|
61
65
|
WrongUnlockPasswordError: () => WrongUnlockPasswordError
|
|
62
66
|
});
|
|
63
|
-
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;
|
|
67
|
+
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;
|
|
64
68
|
var init_errors = __esm({
|
|
65
69
|
"src/social/errors.ts"() {
|
|
66
70
|
"use strict";
|
|
@@ -209,6 +213,36 @@ var init_errors = __esm({
|
|
|
209
213
|
super("EXPORT_NOT_AUTHORIZED", "Export requires a fresh security check", true);
|
|
210
214
|
}
|
|
211
215
|
};
|
|
216
|
+
WalletExistsError = class extends DecaneConnectError {
|
|
217
|
+
constructor() {
|
|
218
|
+
super("WALLET_EXISTS", "This account already has a wallet; opening it instead.", true);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
WalletEnrolmentError = class extends DecaneConnectError {
|
|
222
|
+
constructor(cause) {
|
|
223
|
+
super(
|
|
224
|
+
"WALLET_ENROLMENT_FAILED",
|
|
225
|
+
"Your wallet could not be set up: the signing enclave did not accept it. Nothing was created \u2014 sign in again to try again.",
|
|
226
|
+
true
|
|
227
|
+
);
|
|
228
|
+
if (cause !== void 0) this.cause = cause;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
ProvisioningRateLimitedError = class extends DecaneConnectError {
|
|
232
|
+
constructor() {
|
|
233
|
+
super(
|
|
234
|
+
"PROVISIONING_RATE_LIMITED",
|
|
235
|
+
"This wallet has been opened on too many devices or tabs in the last hour. Wait a little and sign in again.",
|
|
236
|
+
true
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
EnclaveHttpError = class extends DecaneConnectError {
|
|
241
|
+
constructor(status, message) {
|
|
242
|
+
super("ENCLAVE_HTTP", message, status >= 500);
|
|
243
|
+
this.status = status;
|
|
244
|
+
}
|
|
245
|
+
};
|
|
212
246
|
}
|
|
213
247
|
});
|
|
214
248
|
|
|
@@ -226,6 +260,7 @@ __export(index_exports, {
|
|
|
226
260
|
DecaneLogo: () => DecaneLogo,
|
|
227
261
|
DecaneMark: () => DecaneMark,
|
|
228
262
|
DecaneWordmark: () => DecaneWordmark,
|
|
263
|
+
EnclaveHttpError: () => EnclaveHttpError,
|
|
229
264
|
ExportNotAuthorizedError: () => ExportNotAuthorizedError,
|
|
230
265
|
InvalidAccessTokenError: () => InvalidAccessTokenError,
|
|
231
266
|
InvalidApiKeyError: () => InvalidApiKeyError,
|
|
@@ -237,6 +272,7 @@ __export(index_exports, {
|
|
|
237
272
|
OriginNotAllowedError: () => OriginNotAllowedError,
|
|
238
273
|
PromptPasswordNotConfiguredError: () => PromptPasswordNotConfiguredError,
|
|
239
274
|
PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
|
|
275
|
+
ProvisioningRateLimitedError: () => ProvisioningRateLimitedError,
|
|
240
276
|
ProvisioningTimeoutError: () => ProvisioningTimeoutError,
|
|
241
277
|
ReconnectRequiredError: () => ReconnectRequiredError,
|
|
242
278
|
RecoveryFailedError: () => RecoveryFailedError,
|
|
@@ -248,6 +284,8 @@ __export(index_exports, {
|
|
|
248
284
|
UnlockRequiredError: () => UnlockRequiredError,
|
|
249
285
|
UnlockTokenRejectedError: () => UnlockTokenRejectedError,
|
|
250
286
|
UserCancelledError: () => UserCancelledError,
|
|
287
|
+
WalletEnrolmentError: () => WalletEnrolmentError,
|
|
288
|
+
WalletExistsError: () => WalletExistsError,
|
|
251
289
|
WalletSelector: () => WalletSelector,
|
|
252
290
|
WeakPasswordError: () => WeakPasswordError,
|
|
253
291
|
WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
|
|
@@ -2078,7 +2116,9 @@ var ERROR_MAP = {
|
|
|
2078
2116
|
// The backend's authMiddleware code for an expired/revoked JWT. Surfacing it
|
|
2079
2117
|
// as ReconnectRequiredError routes callers to the sign-in UI instead of a
|
|
2080
2118
|
// generic failure message.
|
|
2081
|
-
UNAUTHORIZED: ReconnectRequiredError
|
|
2119
|
+
UNAUTHORIZED: ReconnectRequiredError,
|
|
2120
|
+
// A restorable wallet already exists: the client provisions instead of creating.
|
|
2121
|
+
WALLET_EXISTS: WalletExistsError
|
|
2082
2122
|
};
|
|
2083
2123
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
2084
2124
|
var ApiClient = class {
|
|
@@ -2197,7 +2237,8 @@ var ApiClient = class {
|
|
|
2197
2237
|
wrappedShareMeta: btoa(JSON.stringify(share.wrappedShareMeta)),
|
|
2198
2238
|
evmAddress: share.evmAddress,
|
|
2199
2239
|
solanaAddress: share.solanaAddress,
|
|
2200
|
-
tronAddress: share.tronAddress
|
|
2240
|
+
tronAddress: share.tronAddress,
|
|
2241
|
+
...share.pendingEnrolment === void 0 ? {} : { pendingEnrolment: share.pendingEnrolment }
|
|
2201
2242
|
})
|
|
2202
2243
|
});
|
|
2203
2244
|
}
|
|
@@ -2477,7 +2518,7 @@ function dropCollateral(teeUrl) {
|
|
|
2477
2518
|
// src/social/attestation.ts
|
|
2478
2519
|
init_errors();
|
|
2479
2520
|
|
|
2480
|
-
//
|
|
2521
|
+
// ../infra/tee-measurement.json
|
|
2481
2522
|
var tee_measurement_default = {
|
|
2482
2523
|
signerVersion: "0.2.10",
|
|
2483
2524
|
acceptedMrtd: [
|
|
@@ -2776,7 +2817,8 @@ var TeeClient = class {
|
|
|
2776
2817
|
});
|
|
2777
2818
|
if (!res.ok) {
|
|
2778
2819
|
const detail = await res.text().catch(() => "");
|
|
2779
|
-
throw new
|
|
2820
|
+
throw new EnclaveHttpError(
|
|
2821
|
+
res.status,
|
|
2780
2822
|
`auth share enrolment failed (HTTP ${res.status})${detail ? `: ${detail.slice(0, 200)}` : ""}`
|
|
2781
2823
|
);
|
|
2782
2824
|
}
|
|
@@ -2797,9 +2839,11 @@ var TeeClient = class {
|
|
|
2797
2839
|
headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" }
|
|
2798
2840
|
});
|
|
2799
2841
|
if (res.status === 409) return null;
|
|
2842
|
+
if (res.status === 429) throw new ProvisioningRateLimitedError();
|
|
2800
2843
|
if (!res.ok) {
|
|
2801
2844
|
const detail = await res.text().catch(() => "");
|
|
2802
|
-
throw new
|
|
2845
|
+
throw new EnclaveHttpError(
|
|
2846
|
+
res.status,
|
|
2803
2847
|
`device share provisioning failed (HTTP ${res.status})${detail ? `: ${detail.slice(0, 200)}` : ""}`
|
|
2804
2848
|
);
|
|
2805
2849
|
}
|
|
@@ -3014,6 +3058,35 @@ var SessionManager = class {
|
|
|
3014
3058
|
}
|
|
3015
3059
|
};
|
|
3016
3060
|
|
|
3061
|
+
// src/social/identityTier.ts
|
|
3062
|
+
function resolveProtectionTier(config) {
|
|
3063
|
+
if (config.protection) return config.protection;
|
|
3064
|
+
return config.deferDeviceProtection === true ? "identity" : "device";
|
|
3065
|
+
}
|
|
3066
|
+
function resumeAcrossTabsDefault(config, identityTier) {
|
|
3067
|
+
return config.resumeSessionAcrossTabs ?? identityTier;
|
|
3068
|
+
}
|
|
3069
|
+
var ENROL_BACKOFF_MS = [0, 1e3, 3e3];
|
|
3070
|
+
function enrolmentRetryable(status) {
|
|
3071
|
+
return status !== 429 && status !== 403;
|
|
3072
|
+
}
|
|
3073
|
+
function shouldTryProvisionFirst(identityTier, hasStoredShare) {
|
|
3074
|
+
return identityTier || !hasStoredShare;
|
|
3075
|
+
}
|
|
3076
|
+
function routeAfterProvision(input) {
|
|
3077
|
+
switch (input.outcome) {
|
|
3078
|
+
case "share":
|
|
3079
|
+
return "session";
|
|
3080
|
+
case "rate-limited":
|
|
3081
|
+
return "fail";
|
|
3082
|
+
case "not-enrolled":
|
|
3083
|
+
if (!input.identityTier) return "recover";
|
|
3084
|
+
return input.hasStoredShare ? "migrate" : "new-device";
|
|
3085
|
+
case "error":
|
|
3086
|
+
return input.identityTier ? "fail" : "recover";
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3017
3090
|
// src/social/crypto/shares.ts
|
|
3018
3091
|
var import_secp256k1 = require("@noble/curves/secp256k1");
|
|
3019
3092
|
var import_shamir_secret_sharing = require("shamir-secret-sharing");
|
|
@@ -3924,7 +3997,8 @@ function clearSessionPersist(key) {
|
|
|
3924
3997
|
async function createDecaneConnect(config) {
|
|
3925
3998
|
if (!config.appId) throw new Error("decane-connect-kit: appId is required");
|
|
3926
3999
|
if (!config.apiKey) throw new Error("decane-connect-kit: apiKey is required");
|
|
3927
|
-
|
|
4000
|
+
const identityTier = resolveProtectionTier(config) === "identity";
|
|
4001
|
+
useCrossTabSessionStore(resumeAcrossTabsDefault(config, identityTier));
|
|
3928
4002
|
if (!config.chains?.length)
|
|
3929
4003
|
throw new Error("decane-connect-kit: at least one chain is required");
|
|
3930
4004
|
if (config.rotateOnNewDeviceRecovery && !config.onRecoveryRotated) {
|
|
@@ -3962,7 +4036,6 @@ async function createDecaneConnect(config) {
|
|
|
3962
4036
|
let lastUserId = loadLastUser(LAST_USER_KEY) ?? (LEGACY_LAST_USER_KEY !== LAST_USER_KEY ? loadLastUser(LEGACY_LAST_USER_KEY) : null);
|
|
3963
4037
|
let ethProvider = null;
|
|
3964
4038
|
const persistSession = config.persistSession !== false;
|
|
3965
|
-
const deferDeviceProtection = config.deferDeviceProtection === true;
|
|
3966
4039
|
const serverAssistedRecovery = true;
|
|
3967
4040
|
const RECOVERY_OFFER_TIMEOUT_MS = 5 * 6e4;
|
|
3968
4041
|
const selfHealAttempted = /* @__PURE__ */ new Set();
|
|
@@ -3983,22 +4056,27 @@ async function createDecaneConnect(config) {
|
|
|
3983
4056
|
zeroize(share);
|
|
3984
4057
|
}
|
|
3985
4058
|
}
|
|
3986
|
-
async function
|
|
4059
|
+
async function enrolBlocking(jwt, share, budgetMs) {
|
|
4060
|
+
const startedAt = Date.now();
|
|
3987
4061
|
try {
|
|
3988
|
-
|
|
4062
|
+
let lastError;
|
|
4063
|
+
for (const wait of ENROL_BACKOFF_MS) {
|
|
4064
|
+
if (wait) await new Promise((r) => setTimeout(r, wait));
|
|
3989
4065
|
try {
|
|
3990
|
-
await
|
|
4066
|
+
await withDeadline(
|
|
4067
|
+
new TeeClient(teeUrl).enrolAuthShare(jwt, share),
|
|
4068
|
+
budgetMs - (Date.now() - startedAt),
|
|
4069
|
+
"enrolling your wallet"
|
|
4070
|
+
);
|
|
3991
4071
|
return;
|
|
3992
4072
|
} catch (e) {
|
|
3993
|
-
if (
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
"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.",
|
|
3997
|
-
e
|
|
3998
|
-
);
|
|
3999
|
-
}
|
|
4073
|
+
if (e instanceof AttestationError || e instanceof ProvisioningTimeoutError) throw e;
|
|
4074
|
+
lastError = e;
|
|
4075
|
+
if (!enrolmentRetryable(e.status)) break;
|
|
4000
4076
|
}
|
|
4001
4077
|
}
|
|
4078
|
+
events.emit("enrolment-failed");
|
|
4079
|
+
throw new WalletEnrolmentError(lastError);
|
|
4002
4080
|
} finally {
|
|
4003
4081
|
zeroize(share);
|
|
4004
4082
|
}
|
|
@@ -4094,6 +4172,7 @@ async function createDecaneConnect(config) {
|
|
|
4094
4172
|
else scheduleAccessTokenExpiry(saved.jwt, saved.expiresAt);
|
|
4095
4173
|
auth = { jwt: saved.jwt, userId: saved.userId, addresses: saved.addresses };
|
|
4096
4174
|
persisted = { userId: saved.userId, addresses: saved.addresses };
|
|
4175
|
+
if (saved.handle) armTokenRefresh(saved.jwt);
|
|
4097
4176
|
} catch {
|
|
4098
4177
|
dropLiveSession();
|
|
4099
4178
|
}
|
|
@@ -4103,6 +4182,13 @@ async function createDecaneConnect(config) {
|
|
|
4103
4182
|
if (LEGACY_SESSION_KEY !== SESSION_KEY) clearSessionPersist(LEGACY_SESSION_KEY);
|
|
4104
4183
|
}
|
|
4105
4184
|
events.on("session-expired", () => dropLiveSession());
|
|
4185
|
+
function armTokenRefresh(jwt) {
|
|
4186
|
+
const expiresAt = decodeAccessToken(jwt)?.expiresAt;
|
|
4187
|
+
if (expiresAt) scheduleTokenRefresh(jwt, expiresAt);
|
|
4188
|
+
}
|
|
4189
|
+
events.on("connected", () => {
|
|
4190
|
+
if (auth) armTokenRefresh(auth.jwt);
|
|
4191
|
+
});
|
|
4106
4192
|
async function passwordSetup() {
|
|
4107
4193
|
if (!config.promptUnlockPassword) throw new PromptPasswordNotConfiguredError();
|
|
4108
4194
|
const password = await config.promptUnlockPassword({ kind: "set" });
|
|
@@ -4327,7 +4413,7 @@ async function createDecaneConnect(config) {
|
|
|
4327
4413
|
let wrappingKey = null;
|
|
4328
4414
|
let meta = {};
|
|
4329
4415
|
let unlockTokenKey = null;
|
|
4330
|
-
if (
|
|
4416
|
+
if (identityTier) {
|
|
4331
4417
|
} else if (isPasskeySupported()) {
|
|
4332
4418
|
const prfSalt = generateRandomBytes(32);
|
|
4333
4419
|
try {
|
|
@@ -4394,19 +4480,47 @@ async function createDecaneConnect(config) {
|
|
|
4394
4480
|
if (unlockTokenKey) await attachUnlockToken(userId, jwt, unlockTokenKey);
|
|
4395
4481
|
}
|
|
4396
4482
|
const serverIv = generateRandomBytes(12);
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4483
|
+
try {
|
|
4484
|
+
await withDeadline(api.putShare(jwt, {
|
|
4485
|
+
ciphertext: shareSet.server,
|
|
4486
|
+
iv: serverIv,
|
|
4487
|
+
wrappedShareMeta: {
|
|
4488
|
+
// The server records this on the wallet_created event and nothing else.
|
|
4489
|
+
unlockMethod: identityTier ? "identity" : unlockMethod
|
|
4490
|
+
// credential IDs, prfSalt, pinSalt stay on-device — never sent to server
|
|
4491
|
+
},
|
|
4492
|
+
evmAddress,
|
|
4493
|
+
solanaAddress,
|
|
4494
|
+
tronAddress,
|
|
4495
|
+
// Provisional until the enclave has sealed the auth share, just below. A
|
|
4496
|
+
// backend that knows the flag keeps this row invisible to hasShare until
|
|
4497
|
+
// then, so a creation that fails past this point leaves no wallet behind
|
|
4498
|
+
// and the next sign-in simply creates again — with a new key, which is
|
|
4499
|
+
// why the backend also replaces the recorded addresses outright.
|
|
4500
|
+
pendingEnrolment: true
|
|
4501
|
+
}), remainingBudget(), "storing your wallet");
|
|
4502
|
+
} catch (e) {
|
|
4503
|
+
if (e instanceof WalletExistsError) {
|
|
4504
|
+
zeroize(shareSet.server);
|
|
4505
|
+
zeroize(shareSet.device);
|
|
4506
|
+
zeroize(shareSet.recovery);
|
|
4507
|
+
if (wrappingKey) await storage2.deleteShare(userId).catch(() => {
|
|
4508
|
+
});
|
|
4509
|
+
events.emit("wallet-unlocking");
|
|
4510
|
+
return returningUserUnlock(userId, jwt);
|
|
4511
|
+
}
|
|
4512
|
+
throw e;
|
|
4513
|
+
}
|
|
4409
4514
|
zeroize(shareSet.server);
|
|
4515
|
+
try {
|
|
4516
|
+
await enrolBlocking(jwt, new Uint8Array(shareSet.recovery), remainingBudget());
|
|
4517
|
+
} catch (e) {
|
|
4518
|
+
zeroize(shareSet.device);
|
|
4519
|
+
zeroize(shareSet.recovery);
|
|
4520
|
+
if (wrappingKey) await storage2.deleteShare(userId).catch(() => {
|
|
4521
|
+
});
|
|
4522
|
+
throw e;
|
|
4523
|
+
}
|
|
4410
4524
|
const addresses = { evm: evmAddress, solana: solanaAddress, tron: tronAddress };
|
|
4411
4525
|
if (unlockMethod === "passkey" && meta.credentialId && meta.prfSalt && wrappedDeviceShare && iv) {
|
|
4412
4526
|
try {
|
|
@@ -4435,10 +4549,7 @@ async function createDecaneConnect(config) {
|
|
|
4435
4549
|
addresses.evm = evmAddress = confirmed.evm;
|
|
4436
4550
|
addresses.solana = solanaAddress = confirmed.solana;
|
|
4437
4551
|
addresses.tron = tronAddress = confirmed.tron ?? evmToTronAddress2(confirmed.evm);
|
|
4438
|
-
if (
|
|
4439
|
-
void enrolInBackground(jwt, new Uint8Array(shareSet.recovery));
|
|
4440
|
-
}
|
|
4441
|
-
if (config.offerRecoveryAtSignup === true && config.onRecoveryShareOffer) {
|
|
4552
|
+
if (!identityTier && config.offerRecoveryAtSignup === true && config.onRecoveryShareOffer) {
|
|
4442
4553
|
void offerRecoveryShare(
|
|
4443
4554
|
jwt,
|
|
4444
4555
|
new Uint8Array(shareSet.recovery),
|
|
@@ -4485,31 +4596,44 @@ async function createDecaneConnect(config) {
|
|
|
4485
4596
|
const stored = await storage2.getShare(userId);
|
|
4486
4597
|
let deviceShare;
|
|
4487
4598
|
let joinedViaPasskeySync = false;
|
|
4488
|
-
|
|
4489
|
-
|
|
4599
|
+
let migrating = false;
|
|
4600
|
+
if (serverAssistedRecovery && shouldTryProvisionFirst(identityTier, !!stored)) {
|
|
4601
|
+
let provisioned = null;
|
|
4602
|
+
let failure;
|
|
4603
|
+
let outcome;
|
|
4604
|
+
try {
|
|
4605
|
+
provisioned = await new TeeClient(teeUrl).provisionDeviceShare(jwt);
|
|
4606
|
+
outcome = provisioned ? "share" : "not-enrolled";
|
|
4607
|
+
} catch (e) {
|
|
4608
|
+
failure = e;
|
|
4609
|
+
outcome = e instanceof ProvisioningRateLimitedError ? "rate-limited" : "error";
|
|
4610
|
+
}
|
|
4611
|
+
const route = routeAfterProvision({ identityTier, hasStoredShare: !!stored, outcome });
|
|
4612
|
+
if (route === "session" && provisioned) {
|
|
4613
|
+
if (!identityTier) await persistProvisionedShare(userId, jwt, provisioned);
|
|
4614
|
+
let addrs;
|
|
4490
4615
|
try {
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
if (!deferDeviceProtection) await persistProvisionedShare(userId, jwt, provisioned);
|
|
4495
|
-
let addrs;
|
|
4496
|
-
try {
|
|
4497
|
-
addrs = await session.start(provisioned, jwt, recordedAddresses);
|
|
4498
|
-
} finally {
|
|
4499
|
-
zeroize(provisioned);
|
|
4500
|
-
}
|
|
4501
|
-
return addrs;
|
|
4502
|
-
}
|
|
4503
|
-
console.warn(
|
|
4504
|
-
"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."
|
|
4505
|
-
);
|
|
4506
|
-
} catch (e) {
|
|
4507
|
-
console.warn(
|
|
4508
|
-
"decane-connect-kit: server-assisted provisioning unavailable; falling back to passkey/recovery-file recovery.",
|
|
4509
|
-
e
|
|
4510
|
-
);
|
|
4616
|
+
addrs = await session.start(provisioned, jwt, recordedAddresses);
|
|
4617
|
+
} finally {
|
|
4618
|
+
zeroize(provisioned);
|
|
4511
4619
|
}
|
|
4620
|
+
return addrs;
|
|
4621
|
+
}
|
|
4622
|
+
if (route === "fail") throw failure;
|
|
4623
|
+
if (route === "new-device") throw new NewDeviceError();
|
|
4624
|
+
if (route === "migrate") migrating = true;
|
|
4625
|
+
if (route === "recover" && outcome === "not-enrolled") {
|
|
4626
|
+
console.warn(
|
|
4627
|
+
"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."
|
|
4628
|
+
);
|
|
4629
|
+
} else if (route === "recover") {
|
|
4630
|
+
console.warn(
|
|
4631
|
+
"decane-connect-kit: server-assisted provisioning unavailable; falling back to passkey/recovery-file recovery.",
|
|
4632
|
+
failure
|
|
4633
|
+
);
|
|
4512
4634
|
}
|
|
4635
|
+
}
|
|
4636
|
+
if (!stored) {
|
|
4513
4637
|
if (!config.onRecoveryRotated) {
|
|
4514
4638
|
throw new CallSequenceError(
|
|
4515
4639
|
"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."
|
|
@@ -4535,7 +4659,7 @@ async function createDecaneConnect(config) {
|
|
|
4535
4659
|
});
|
|
4536
4660
|
deviceShare = recovered.deviceShare;
|
|
4537
4661
|
joinedViaPasskeySync = recovered.flow === "passkey-sync";
|
|
4538
|
-
if (!
|
|
4662
|
+
if (!identityTier && !await storage2.getShare(userId) && isPasskeySupported()) {
|
|
4539
4663
|
await registerPasskeyAndBackup(userId, jwt, deviceShare);
|
|
4540
4664
|
}
|
|
4541
4665
|
} else {
|
|
@@ -4549,6 +4673,14 @@ async function createDecaneConnect(config) {
|
|
|
4549
4673
|
} catch {
|
|
4550
4674
|
throw stored.unlockMethod === "pin" ? new InvalidPinError() : new ShareCorruptedError();
|
|
4551
4675
|
}
|
|
4676
|
+
if (migrating) {
|
|
4677
|
+
try {
|
|
4678
|
+
await enrolBlocking(jwt, new Uint8Array(deviceShare), PROVISIONING_BUDGET_MS);
|
|
4679
|
+
} catch (e) {
|
|
4680
|
+
zeroize(deviceShare);
|
|
4681
|
+
throw e;
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4552
4684
|
}
|
|
4553
4685
|
let addresses;
|
|
4554
4686
|
try {
|
|
@@ -4563,7 +4695,11 @@ async function createDecaneConnect(config) {
|
|
|
4563
4695
|
}
|
|
4564
4696
|
throw e;
|
|
4565
4697
|
}
|
|
4566
|
-
|
|
4698
|
+
if (migrating) {
|
|
4699
|
+
events.emit("migrated-to-identity");
|
|
4700
|
+
} else if (!identityTier) {
|
|
4701
|
+
void selfHealEnrolment(userId, jwt, new Uint8Array(deviceShare));
|
|
4702
|
+
}
|
|
4567
4703
|
zeroize(deviceShare);
|
|
4568
4704
|
if (recordedAddresses && recordedAddresses.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
|
|
4569
4705
|
console.warn(
|
|
@@ -4746,7 +4882,7 @@ async function createDecaneConnect(config) {
|
|
|
4746
4882
|
if (auth && session.isActive()) {
|
|
4747
4883
|
return { addresses: auth.addresses, isNewUser: false };
|
|
4748
4884
|
}
|
|
4749
|
-
const candidate = await passkeyLoginCandidate();
|
|
4885
|
+
const candidate = identityTier ? null : await passkeyLoginCandidate();
|
|
4750
4886
|
if (candidate) {
|
|
4751
4887
|
try {
|
|
4752
4888
|
return await passkeyLogin(candidate);
|
|
@@ -4856,9 +4992,15 @@ async function createDecaneConnect(config) {
|
|
|
4856
4992
|
return handleAuthResult(result);
|
|
4857
4993
|
},
|
|
4858
4994
|
async canSignInWithPasskey() {
|
|
4995
|
+
if (identityTier) return false;
|
|
4859
4996
|
return canOfferPasskey();
|
|
4860
4997
|
},
|
|
4861
4998
|
async signInWithPasskey() {
|
|
4999
|
+
if (identityTier) {
|
|
5000
|
+
throw new CallSequenceError(
|
|
5001
|
+
"signInWithPasskey() is not available in the identity tier (protection: 'identity'); use connect()."
|
|
5002
|
+
);
|
|
5003
|
+
}
|
|
4862
5004
|
const candidate = await passkeyDeviceCandidate();
|
|
4863
5005
|
if (candidate) return passkeyLogin(candidate);
|
|
4864
5006
|
const known = persisted?.userId ?? lastUserId;
|
|
@@ -4994,6 +5136,11 @@ async function createDecaneConnect(config) {
|
|
|
4994
5136
|
return stored?.unlockMethod === "passkey";
|
|
4995
5137
|
},
|
|
4996
5138
|
async addPasskey() {
|
|
5139
|
+
if (identityTier) {
|
|
5140
|
+
throw new CallSequenceError(
|
|
5141
|
+
"addPasskey() is not available in the identity tier (protection: 'identity'): nothing is stored on the device for a passkey to protect."
|
|
5142
|
+
);
|
|
5143
|
+
}
|
|
4997
5144
|
if (!auth) throw new ReconnectRequiredError();
|
|
4998
5145
|
if (!isPasskeySupported()) throw new WebAuthnNotSupportedError();
|
|
4999
5146
|
const stored = await storage2.getShare(auth.userId);
|
|
@@ -5032,6 +5179,7 @@ async function createDecaneConnect(config) {
|
|
|
5032
5179
|
return await storage2.getShare(auth.userId) !== null;
|
|
5033
5180
|
},
|
|
5034
5181
|
async protectDevice() {
|
|
5182
|
+
if (identityTier) return false;
|
|
5035
5183
|
if (!auth) throw new ReconnectRequiredError();
|
|
5036
5184
|
if (await storage2.getShare(auth.userId)) return true;
|
|
5037
5185
|
if (!serverAssistedRecovery) throw new NewDeviceError();
|
|
@@ -5047,7 +5195,7 @@ async function createDecaneConnect(config) {
|
|
|
5047
5195
|
},
|
|
5048
5196
|
async unlock() {
|
|
5049
5197
|
if (!auth) {
|
|
5050
|
-
const candidate = await passkeyLoginCandidate();
|
|
5198
|
+
const candidate = identityTier ? null : await passkeyLoginCandidate();
|
|
5051
5199
|
if (candidate) {
|
|
5052
5200
|
await passkeyLogin(candidate);
|
|
5053
5201
|
return;
|
|
@@ -5216,6 +5364,8 @@ async function createDecaneConnect(config) {
|
|
|
5216
5364
|
},
|
|
5217
5365
|
async disconnect() {
|
|
5218
5366
|
session.end();
|
|
5367
|
+
if (tokenRefresh) clearTimeout(tokenRefresh);
|
|
5368
|
+
tokenRefresh = null;
|
|
5219
5369
|
if (auth?.jwt) {
|
|
5220
5370
|
await api.revoke(auth.jwt).catch(() => {
|
|
5221
5371
|
});
|
|
@@ -7666,6 +7816,7 @@ function DecaneKit({ config, children }) {
|
|
|
7666
7816
|
chains,
|
|
7667
7817
|
rpcUrls,
|
|
7668
7818
|
requireAssertionPerSignature,
|
|
7819
|
+
protection,
|
|
7669
7820
|
deferDeviceProtection,
|
|
7670
7821
|
resumeSessionAcrossTabs,
|
|
7671
7822
|
// Recovery callbacks. Forwarding these is what makes recovery
|
|
@@ -7688,6 +7839,7 @@ function DecaneKit({ config, children }) {
|
|
|
7688
7839
|
promptPin,
|
|
7689
7840
|
promptUnlockPassword,
|
|
7690
7841
|
requireAssertionPerSignature,
|
|
7842
|
+
protection,
|
|
7691
7843
|
deferDeviceProtection,
|
|
7692
7844
|
resumeSessionAcrossTabs,
|
|
7693
7845
|
offerRecoveryAtSignup,
|
|
@@ -8897,6 +9049,7 @@ function useSignTypedData() {
|
|
|
8897
9049
|
DecaneLogo,
|
|
8898
9050
|
DecaneMark,
|
|
8899
9051
|
DecaneWordmark,
|
|
9052
|
+
EnclaveHttpError,
|
|
8900
9053
|
ExportNotAuthorizedError,
|
|
8901
9054
|
InvalidAccessTokenError,
|
|
8902
9055
|
InvalidApiKeyError,
|
|
@@ -8908,6 +9061,7 @@ function useSignTypedData() {
|
|
|
8908
9061
|
OriginNotAllowedError,
|
|
8909
9062
|
PromptPasswordNotConfiguredError,
|
|
8910
9063
|
PromptPinNotConfiguredError,
|
|
9064
|
+
ProvisioningRateLimitedError,
|
|
8911
9065
|
ProvisioningTimeoutError,
|
|
8912
9066
|
ReconnectRequiredError,
|
|
8913
9067
|
RecoveryFailedError,
|
|
@@ -8919,6 +9073,8 @@ function useSignTypedData() {
|
|
|
8919
9073
|
UnlockRequiredError,
|
|
8920
9074
|
UnlockTokenRejectedError,
|
|
8921
9075
|
UserCancelledError,
|
|
9076
|
+
WalletEnrolmentError,
|
|
9077
|
+
WalletExistsError,
|
|
8922
9078
|
WalletSelector,
|
|
8923
9079
|
WeakPasswordError,
|
|
8924
9080
|
WebAuthnNotSupportedError,
|