@anywalletkit/extension-kit 0.1.2 → 0.1.4
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/README.md +12 -3
- package/dist/index.cjs +194 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -24
- package/dist/index.d.ts +65 -24
- package/dist/index.js +194 -67
- package/dist/index.js.map +1 -1
- package/package.json +10 -7
package/dist/index.js
CHANGED
|
@@ -40,6 +40,22 @@ var VAULT_DEFAULT_ITERATIONS = 6e5;
|
|
|
40
40
|
var VAULT_SCRYPT = { N: 1 << 17, r: 8, p: 1 };
|
|
41
41
|
var VAULT_MIN_SCRYPT = { N: 1 << 14, r: 8, p: 1 };
|
|
42
42
|
var VAULT_MIN_PASSWORD_LENGTH = 8;
|
|
43
|
+
var VAULT_LIMITS = Object.freeze({
|
|
44
|
+
/** scrypt cost: 2^20 × 16 = 2 GiB is the most any envelope may ask for. */
|
|
45
|
+
scrypt: Object.freeze({ N: 1 << 20, r: 16, p: 4 }),
|
|
46
|
+
/** PBKDF2-SHA256 iterations (v1 envelopes). */
|
|
47
|
+
iterations: 5e6,
|
|
48
|
+
/** Sealed secrets in one vault. */
|
|
49
|
+
secrets: 1e3,
|
|
50
|
+
/** Registered passkeys. */
|
|
51
|
+
passkeys: 32,
|
|
52
|
+
/** Plaintext bytes of one secret (`setSecret`). */
|
|
53
|
+
secretBytes: 16 * 1024,
|
|
54
|
+
/** base64 characters of one sealed box (iv or data). */
|
|
55
|
+
boxChars: 32 * 1024,
|
|
56
|
+
/** JSON characters of the whole envelope. */
|
|
57
|
+
envelopeChars: 4 * 1024 * 1024
|
|
58
|
+
});
|
|
43
59
|
var SALT_BYTES = 32;
|
|
44
60
|
var IV_BYTES = 12;
|
|
45
61
|
var DEK_BYTES = 32;
|
|
@@ -51,6 +67,8 @@ var aadSecret = (id) => `forgewallet-vault/secret/${id}`;
|
|
|
51
67
|
var HKDF_INFO = "forgewallet-vault/2/passkey";
|
|
52
68
|
var Vault = class {
|
|
53
69
|
_storage;
|
|
70
|
+
/** Every read-modify-write of the envelope runs here, one after another (a parallel write would drop the other's change). */
|
|
71
|
+
_queue = Promise.resolve();
|
|
54
72
|
_kdf;
|
|
55
73
|
_iterations;
|
|
56
74
|
_scrypt;
|
|
@@ -63,10 +81,11 @@ var Vault = class {
|
|
|
63
81
|
this._scrypt = options.scrypt ?? VAULT_SCRYPT;
|
|
64
82
|
this._onProgress = options.onProgress;
|
|
65
83
|
this._crypto = options.crypto ?? globalThis.crypto;
|
|
66
|
-
if (!Number.isInteger(this._iterations) || this._iterations < VAULT_MIN_ITERATIONS) {
|
|
67
|
-
throw new VaultError(`PBKDF2 iterations must be an integer
|
|
84
|
+
if (!Number.isInteger(this._iterations) || this._iterations < VAULT_MIN_ITERATIONS || this._iterations > VAULT_LIMITS.iterations) {
|
|
85
|
+
throw new VaultError(`PBKDF2 iterations must be an integer from ${VAULT_MIN_ITERATIONS} to ${VAULT_LIMITS.iterations}`, "invalid-input");
|
|
68
86
|
}
|
|
69
87
|
if (!isStrongScrypt(this._scrypt)) throw new VaultError(`scrypt cost must be at least N=${VAULT_MIN_SCRYPT.N} r=${VAULT_MIN_SCRYPT.r} p=${VAULT_MIN_SCRYPT.p} (N a power of two)`, "invalid-input");
|
|
88
|
+
if (!isBoundedScrypt(this._scrypt)) throw new VaultError(`scrypt cost must be at most N=${VAULT_LIMITS.scrypt.N} r=${VAULT_LIMITS.scrypt.r} p=${VAULT_LIMITS.scrypt.p}`, "invalid-input");
|
|
70
89
|
}
|
|
71
90
|
async exists() {
|
|
72
91
|
return await this._storage.get() !== null;
|
|
@@ -85,10 +104,12 @@ var Vault = class {
|
|
|
85
104
|
*/
|
|
86
105
|
async create(password) {
|
|
87
106
|
requirePassword(password, VAULT_MIN_PASSWORD_LENGTH);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
107
|
+
return this._exclusive(async () => {
|
|
108
|
+
if (await this.exists()) throw new VaultError("A vault already exists; unlock it or reset explicitly.", "exists");
|
|
109
|
+
const dek = this._random(DEK_BYTES);
|
|
110
|
+
await this._write(await this._sealEnvelope(password, dek, {}, [], Date.now()));
|
|
111
|
+
return dek;
|
|
112
|
+
});
|
|
92
113
|
}
|
|
93
114
|
/**
|
|
94
115
|
* Derive the KEK from `password`, unseal and return the DEK. `wrong-password` on
|
|
@@ -97,6 +118,9 @@ var Vault = class {
|
|
|
97
118
|
*/
|
|
98
119
|
async unlock(password) {
|
|
99
120
|
requirePassword(password);
|
|
121
|
+
return this._exclusive(() => this._unlock(password));
|
|
122
|
+
}
|
|
123
|
+
async _unlock(password) {
|
|
100
124
|
const envelope = await this._load();
|
|
101
125
|
if (isEnvelopeV1(envelope)) {
|
|
102
126
|
if (envelope.kdf.iterations < VAULT_MIN_ITERATIONS) throw new VaultError(`The vault's KDF (${envelope.kdf.iterations} iterations) is below the floor this build accepts.`, "weak-kdf");
|
|
@@ -104,7 +128,7 @@ var Vault = class {
|
|
|
104
128
|
const dek = await this._open(kek2, envelope.dek, aadDekV1(envelope.kdf), "wrong-password");
|
|
105
129
|
if (this._kdf === "scrypt") {
|
|
106
130
|
const next = await this._sealEnvelope(password, dek, envelope.secrets, [], envelope.createdAt);
|
|
107
|
-
await this.
|
|
131
|
+
await this._write(isEnvelopeV2(next) ? { ...next, upgradedAt: Date.now() } : next);
|
|
108
132
|
}
|
|
109
133
|
return dek;
|
|
110
134
|
}
|
|
@@ -115,15 +139,18 @@ var Vault = class {
|
|
|
115
139
|
/** Re-wrap the DEK under a new password (fresh salt, current KDF). Secrets and passkeys are untouched. */
|
|
116
140
|
async changePassword(oldPassword, newPassword) {
|
|
117
141
|
requirePassword(newPassword, VAULT_MIN_PASSWORD_LENGTH);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
142
|
+
requirePassword(oldPassword);
|
|
143
|
+
return this._exclusive(async () => {
|
|
144
|
+
const dek = await this._unlock(oldPassword);
|
|
145
|
+
try {
|
|
146
|
+
const envelope = await this._load();
|
|
147
|
+
const passkeys = isEnvelopeV2(envelope) ? envelope.passkeys : [];
|
|
148
|
+
const next = await this._sealEnvelope(newPassword, dek, envelope.secrets, passkeys, envelope.createdAt);
|
|
149
|
+
await this._write(isEnvelopeV2(envelope) && envelope.upgradedAt !== void 0 && isEnvelopeV2(next) ? { ...next, upgradedAt: envelope.upgradedAt } : next);
|
|
150
|
+
} finally {
|
|
151
|
+
zeroize(dek);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
127
154
|
}
|
|
128
155
|
// ---- passkeys (v2) ----
|
|
129
156
|
/** Public view of the registered passkeys (ids, labels; whether each can verify assertions). */
|
|
@@ -143,27 +170,30 @@ var Vault = class {
|
|
|
143
170
|
* Register a passkey as a second unlock path: the DEK (from the unlocked session)
|
|
144
171
|
* is sealed under HKDF-SHA256(`prf`, salt = credential id). The password stays
|
|
145
172
|
* the recovery path; `prf` is zeroized here. A v1 envelope must be unlocked (and
|
|
146
|
-
* thereby upgraded) first.
|
|
173
|
+
* thereby upgraded) first, and the vault must hold a secret (it proves the DEK).
|
|
147
174
|
*/
|
|
148
175
|
async addPasskey(dek, passkey) {
|
|
149
176
|
try {
|
|
150
177
|
requireId(passkey.id);
|
|
151
178
|
if (passkey.prf.length !== PRF_BYTES) throw new VaultError(`A PRF output is ${PRF_BYTES} bytes.`, "invalid-input");
|
|
152
|
-
|
|
153
|
-
if (!isEnvelopeV2(envelope)) throw new VaultError("Unlock with the password once so the vault is upgraded before adding a passkey.", "unsupported-envelope");
|
|
154
|
-
if (envelope.passkeys.some((p) => p.id === passkey.id)) throw new VaultError("This passkey is already registered.", "exists");
|
|
155
|
-
await this._requireDek(dek, envelope);
|
|
156
|
-
const key = await this._derivePasskeyKek(passkey.prf, passkey.id);
|
|
157
|
-
const entry = { id: passkey.id, label: passkey.label.slice(0, 64), createdAt: Date.now(), dek: await this._seal(key, dek, aadPasskey(passkey.id)) };
|
|
158
|
-
if (passkey.publicKey && passkey.alg !== void 0) {
|
|
159
|
-
entry.publicKey = passkey.publicKey;
|
|
160
|
-
entry.alg = passkey.alg;
|
|
161
|
-
}
|
|
162
|
-
await this._storage.set({ ...envelope, passkeys: [...envelope.passkeys, entry] });
|
|
179
|
+
await this._exclusive(() => this._addPasskey(dek, passkey));
|
|
163
180
|
} finally {
|
|
164
181
|
zeroize(passkey.prf);
|
|
165
182
|
}
|
|
166
183
|
}
|
|
184
|
+
async _addPasskey(dek, passkey) {
|
|
185
|
+
const envelope = await this._load();
|
|
186
|
+
if (!isEnvelopeV2(envelope)) throw new VaultError("Unlock with the password once so the vault is upgraded before adding a passkey.", "unsupported-envelope");
|
|
187
|
+
if (envelope.passkeys.some((p) => p.id === passkey.id)) throw new VaultError("This passkey is already registered.", "exists");
|
|
188
|
+
await this._requireDek(dek, envelope);
|
|
189
|
+
const key = await this._derivePasskeyKek(passkey.prf, passkey.id);
|
|
190
|
+
const entry = { id: passkey.id, label: passkey.label.slice(0, 64), createdAt: Date.now(), dek: await this._seal(key, dek, aadPasskey(passkey.id)) };
|
|
191
|
+
if (passkey.publicKey && passkey.alg !== void 0) {
|
|
192
|
+
entry.publicKey = passkey.publicKey;
|
|
193
|
+
entry.alg = passkey.alg;
|
|
194
|
+
}
|
|
195
|
+
await this._write({ ...envelope, passkeys: [...envelope.passkeys, entry] });
|
|
196
|
+
}
|
|
167
197
|
/** Unseal the DEK with a passkey's PRF output. `wrong-password` when the box does not open, `missing` when the id is unknown. */
|
|
168
198
|
async unlockWithPasskey(id, prf) {
|
|
169
199
|
try {
|
|
@@ -181,9 +211,11 @@ var Vault = class {
|
|
|
181
211
|
}
|
|
182
212
|
async removePasskey(id) {
|
|
183
213
|
requireId(id);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
214
|
+
return this._exclusive(async () => {
|
|
215
|
+
const envelope = await this._load();
|
|
216
|
+
if (!isEnvelopeV2(envelope)) return;
|
|
217
|
+
await this._write({ ...envelope, passkeys: envelope.passkeys.filter((p) => p.id !== id) });
|
|
218
|
+
});
|
|
187
219
|
}
|
|
188
220
|
// ---- secrets ----
|
|
189
221
|
async listSecretIds() {
|
|
@@ -193,10 +225,16 @@ var Vault = class {
|
|
|
193
225
|
/** Seal `secret` under the DEK as `sourceId` (overwrites an entry with the same id). */
|
|
194
226
|
async setSecret(dek, sourceId, secret) {
|
|
195
227
|
requireId(sourceId);
|
|
196
|
-
|
|
228
|
+
if (secret.length > VAULT_LIMITS.secretBytes) throw new VaultError(`A secret is at most ${VAULT_LIMITS.secretBytes} bytes.`, "invalid-input");
|
|
197
229
|
const key = await this._importDek(dek);
|
|
198
|
-
|
|
199
|
-
|
|
230
|
+
return this._exclusive(async () => {
|
|
231
|
+
const envelope = await this._load();
|
|
232
|
+
if (!(sourceId in envelope.secrets) && Object.keys(envelope.secrets).length >= VAULT_LIMITS.secrets) {
|
|
233
|
+
throw new VaultError(`A vault holds at most ${VAULT_LIMITS.secrets} secrets.`, "invalid-input");
|
|
234
|
+
}
|
|
235
|
+
const box = await this._seal(key, secret, aadSecret(sourceId));
|
|
236
|
+
await this._write({ ...envelope, secrets: { ...envelope.secrets, [sourceId]: box } });
|
|
237
|
+
});
|
|
200
238
|
}
|
|
201
239
|
/** Unseal one secret. `null` when the id is unknown; the caller zeroizes the result after use. */
|
|
202
240
|
async getSecret(dek, sourceId) {
|
|
@@ -209,15 +247,29 @@ var Vault = class {
|
|
|
209
247
|
}
|
|
210
248
|
async deleteSecret(sourceId) {
|
|
211
249
|
requireId(sourceId);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
250
|
+
return this._exclusive(async () => {
|
|
251
|
+
const envelope = await this._load();
|
|
252
|
+
if (!(sourceId in envelope.secrets)) return;
|
|
253
|
+
const secrets = { ...envelope.secrets };
|
|
254
|
+
delete secrets[sourceId];
|
|
255
|
+
await this._write({ ...envelope, secrets });
|
|
256
|
+
});
|
|
217
257
|
}
|
|
218
258
|
/** Remove the whole vault. The explicit destructive flow only (back up → wipe → create). */
|
|
219
259
|
async destroy() {
|
|
220
|
-
|
|
260
|
+
return this._exclusive(() => this._storage.clear());
|
|
261
|
+
}
|
|
262
|
+
// ---- serialized writes ----
|
|
263
|
+
_exclusive(fn) {
|
|
264
|
+
const run = this._queue.then(fn);
|
|
265
|
+
this._queue = run.catch(() => void 0);
|
|
266
|
+
return run;
|
|
267
|
+
}
|
|
268
|
+
/** Store an envelope this build will open again (bounds checked before it is written). */
|
|
269
|
+
async _write(envelope) {
|
|
270
|
+
const problem = envelopeProblem(envelope);
|
|
271
|
+
if (problem) throw new VaultError(`The vault would exceed its limits (${problem}); nothing was written.`, "invalid-input");
|
|
272
|
+
await this._storage.set(envelope);
|
|
221
273
|
}
|
|
222
274
|
// ---- crypto ----
|
|
223
275
|
_random(n) {
|
|
@@ -235,11 +287,14 @@ var Vault = class {
|
|
|
235
287
|
const kek = await this._deriveScrypt(password, salt, kdf);
|
|
236
288
|
return { v: 2, kdf, dek: await this._seal(kek, dek, aadDekV2(kdf)), secrets, passkeys, createdAt };
|
|
237
289
|
}
|
|
238
|
-
/**
|
|
290
|
+
/**
|
|
291
|
+
* The caller's DEK must open this envelope's secrets — it is the key the password protects, not
|
|
292
|
+
* any 32 bytes. An empty vault has nothing to prove it with, so it is refused rather than trusted.
|
|
293
|
+
*/
|
|
239
294
|
async _requireDek(dek, envelope) {
|
|
240
295
|
const ids = Object.keys(envelope.secrets);
|
|
241
296
|
const key = await this._importDek(dek);
|
|
242
|
-
if (ids.length === 0)
|
|
297
|
+
if (ids.length === 0) throw new VaultError("Add a secret to the vault before a passkey: an empty vault cannot prove the key is its own.", "invalid-input");
|
|
243
298
|
const probe = await this._open(key, envelope.secrets[ids[0]], aadSecret(ids[0]), "locked");
|
|
244
299
|
zeroize(probe);
|
|
245
300
|
}
|
|
@@ -293,6 +348,8 @@ var Vault = class {
|
|
|
293
348
|
const envelope = await this._storage.get();
|
|
294
349
|
if (!envelope) throw new VaultError("No vault in this storage area.", "missing");
|
|
295
350
|
if (!isEnvelopeV1(envelope) && !isEnvelopeV2(envelope)) throw new VaultError("Unsupported vault envelope.", "unsupported-envelope");
|
|
351
|
+
const problem = envelopeProblem(envelope);
|
|
352
|
+
if (problem) throw new VaultError(`The vault envelope is outside the limits this build opens (${problem}).`, "unsupported-envelope");
|
|
296
353
|
return envelope;
|
|
297
354
|
}
|
|
298
355
|
};
|
|
@@ -311,6 +368,22 @@ function isEnvelopeV2(x) {
|
|
|
311
368
|
function isBox(x) {
|
|
312
369
|
return typeof x === "object" && x !== null && typeof x.iv === "string" && typeof x.data === "string";
|
|
313
370
|
}
|
|
371
|
+
function isBoundedScrypt(c) {
|
|
372
|
+
return c.N <= VAULT_LIMITS.scrypt.N && c.r <= VAULT_LIMITS.scrypt.r && c.p <= VAULT_LIMITS.scrypt.p;
|
|
373
|
+
}
|
|
374
|
+
function envelopeProblem(e) {
|
|
375
|
+
if (e.v === 1 && e.kdf.iterations > VAULT_LIMITS.iterations) return `PBKDF2 iterations ${e.kdf.iterations} > ${VAULT_LIMITS.iterations}`;
|
|
376
|
+
if (e.v === 2 && !isBoundedScrypt(e.kdf)) return `scrypt N=${e.kdf.N} r=${e.kdf.r} p=${e.kdf.p} above N=${VAULT_LIMITS.scrypt.N} r=${VAULT_LIMITS.scrypt.r} p=${VAULT_LIMITS.scrypt.p}`;
|
|
377
|
+
const secrets = Object.values(e.secrets);
|
|
378
|
+
if (secrets.length > VAULT_LIMITS.secrets) return `${secrets.length} secrets > ${VAULT_LIMITS.secrets}`;
|
|
379
|
+
const passkeys = e.v === 2 ? e.passkeys : [];
|
|
380
|
+
if (passkeys.length > VAULT_LIMITS.passkeys) return `${passkeys.length} passkeys > ${VAULT_LIMITS.passkeys}`;
|
|
381
|
+
for (const box of [e.dek, ...secrets, ...passkeys.map((p) => p.dek)]) {
|
|
382
|
+
if (box.iv.length > VAULT_LIMITS.boxChars || box.data.length > VAULT_LIMITS.boxChars) return `a sealed box above ${VAULT_LIMITS.boxChars} characters`;
|
|
383
|
+
}
|
|
384
|
+
if (JSON.stringify(e).length > VAULT_LIMITS.envelopeChars) return `envelope above ${VAULT_LIMITS.envelopeChars} characters`;
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
314
387
|
function isStrongScrypt(c) {
|
|
315
388
|
return Number.isInteger(c.N) && c.N >= VAULT_MIN_SCRYPT.N && (c.N & c.N - 1) === 0 && Number.isInteger(c.r) && c.r >= VAULT_MIN_SCRYPT.r && Number.isInteger(c.p) && c.p >= VAULT_MIN_SCRYPT.p;
|
|
316
389
|
}
|
|
@@ -355,6 +428,13 @@ var Session = class {
|
|
|
355
428
|
_alarm;
|
|
356
429
|
_now;
|
|
357
430
|
_onLock = /* @__PURE__ */ new Set();
|
|
431
|
+
/**
|
|
432
|
+
* Every operation on the stored record runs here, one after another: a `touch` that read the
|
|
433
|
+
* record before a `lock` removed it would otherwise write it back and undo the lock.
|
|
434
|
+
*/
|
|
435
|
+
_queue = Promise.resolve();
|
|
436
|
+
/** Bumped by every lock; a `touch` writes only when no lock happened since it read the record. */
|
|
437
|
+
_epoch = 0;
|
|
358
438
|
constructor(options) {
|
|
359
439
|
this._storage = options.storage;
|
|
360
440
|
this._alarms = options.alarms;
|
|
@@ -370,20 +450,19 @@ var Session = class {
|
|
|
370
450
|
/** Store the DEK and arm the auto-lock. The caller's copy is zeroized. */
|
|
371
451
|
async unlock(dek) {
|
|
372
452
|
if (dek.length !== 32) throw new VaultError("Vault key must be 32 bytes.", "invalid-input");
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
453
|
+
return this._serial(async () => {
|
|
454
|
+
const expiresAt = this._expiry();
|
|
455
|
+
await this._storage.set(this._key, { key: toBase64(dek), expiresAt });
|
|
456
|
+
zeroize2(dek);
|
|
457
|
+
await this._arm(expiresAt);
|
|
458
|
+
});
|
|
377
459
|
}
|
|
378
460
|
/** Forget the DEK and cancel the alarm. Safe to call when already locked. */
|
|
379
461
|
async lock() {
|
|
380
|
-
|
|
381
|
-
await this._storage.remove(this._key);
|
|
382
|
-
await this._alarms.clear(this._alarm);
|
|
383
|
-
if (was !== void 0 && was !== null) for (const cb of this._onLock) cb();
|
|
462
|
+
return this._serial(() => this._lockNow());
|
|
384
463
|
}
|
|
385
464
|
async isUnlocked() {
|
|
386
|
-
return await this._read() !== null;
|
|
465
|
+
return this._serial(async () => await this._read() !== null);
|
|
387
466
|
}
|
|
388
467
|
/**
|
|
389
468
|
* The DEK for one operation (zeroize it after). `locked` when there is none or the
|
|
@@ -391,17 +470,22 @@ var Session = class {
|
|
|
391
470
|
* a missed alarm cannot keep the key alive.
|
|
392
471
|
*/
|
|
393
472
|
async getKey() {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
473
|
+
return this._serial(async () => {
|
|
474
|
+
const record = await this._read();
|
|
475
|
+
if (!record) throw new VaultError("Wallet is locked.", "locked");
|
|
476
|
+
return fromBase64(record.key);
|
|
477
|
+
});
|
|
397
478
|
}
|
|
398
479
|
/** User activity: push the auto-lock out again. No-op when locked. */
|
|
399
480
|
async touch() {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
481
|
+
return this._serial(async () => {
|
|
482
|
+
const epoch = this._epoch;
|
|
483
|
+
const record = await this._read();
|
|
484
|
+
if (!record || epoch !== this._epoch) return;
|
|
485
|
+
const expiresAt = this._expiry();
|
|
486
|
+
await this._storage.set(this._key, { ...record, expiresAt });
|
|
487
|
+
await this._arm(expiresAt);
|
|
488
|
+
});
|
|
405
489
|
}
|
|
406
490
|
get autoLockMs() {
|
|
407
491
|
return this._autoLockMs;
|
|
@@ -416,6 +500,19 @@ var Session = class {
|
|
|
416
500
|
this._onLock.add(callback);
|
|
417
501
|
return () => this._onLock.delete(callback);
|
|
418
502
|
}
|
|
503
|
+
_serial(fn) {
|
|
504
|
+
const run = this._queue.then(fn);
|
|
505
|
+
this._queue = run.catch(() => void 0);
|
|
506
|
+
return run;
|
|
507
|
+
}
|
|
508
|
+
/** The lock itself; callers are already in the queue. */
|
|
509
|
+
async _lockNow() {
|
|
510
|
+
this._epoch++;
|
|
511
|
+
const was = await this._storage.get(this._key);
|
|
512
|
+
await this._storage.remove(this._key);
|
|
513
|
+
await this._alarms.clear(this._alarm);
|
|
514
|
+
if (was !== void 0 && was !== null) for (const cb of this._onLock) cb();
|
|
515
|
+
}
|
|
419
516
|
_expiry() {
|
|
420
517
|
return this._autoLockMs > 0 ? this._now() + this._autoLockMs : Number.POSITIVE_INFINITY;
|
|
421
518
|
}
|
|
@@ -427,7 +524,7 @@ var Session = class {
|
|
|
427
524
|
const raw = await this._storage.get(this._key);
|
|
428
525
|
if (!raw || typeof raw.key !== "string" || typeof raw.expiresAt !== "number") return null;
|
|
429
526
|
if (raw.expiresAt <= this._now()) {
|
|
430
|
-
await this.
|
|
527
|
+
await this._lockNow();
|
|
431
528
|
return null;
|
|
432
529
|
}
|
|
433
530
|
return raw;
|
|
@@ -1074,6 +1171,7 @@ function memoryApprovalWindow() {
|
|
|
1074
1171
|
}
|
|
1075
1172
|
|
|
1076
1173
|
// src/router.ts
|
|
1174
|
+
import { UserRejectedError } from "@anywalletkit/core";
|
|
1077
1175
|
var RouterError = class _RouterError extends Error {
|
|
1078
1176
|
code;
|
|
1079
1177
|
data;
|
|
@@ -1087,11 +1185,35 @@ var RouterError = class _RouterError extends Error {
|
|
|
1087
1185
|
return new _RouterError(e.code, e.message, e.data);
|
|
1088
1186
|
}
|
|
1089
1187
|
};
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1188
|
+
var PROVIDER_CODES = /* @__PURE__ */ new Set([4001, 4100, 4200, 4900, 4901, 4902]);
|
|
1189
|
+
var isNodeCode = (code) => typeof code === "number" && Number.isInteger(code) && (code === 3 || code >= -32768 && code <= -32e3);
|
|
1190
|
+
var URL_RE = /\b[a-z][a-z0-9+.-]*:\/\/[^\s"'<>()]+/gi;
|
|
1191
|
+
var pageText = (text) => text.replace(URL_RE, "[url]").split("\n")[0].slice(0, 300);
|
|
1192
|
+
var isHexData = (v) => typeof v === "string" && /^0x[0-9a-fA-F]*$/.test(v) && v.length <= 2 + 2 * 8192;
|
|
1193
|
+
function nodeError(e) {
|
|
1194
|
+
for (let cur = e, depth = 0; cur && typeof cur === "object" && depth < 8; cur = cur.cause, depth++) {
|
|
1195
|
+
const o = cur;
|
|
1196
|
+
if (o.name === "RpcRequestError" && isNodeCode(o.code)) {
|
|
1197
|
+
const text = typeof o.details === "string" && o.details ? o.details : typeof o.shortMessage === "string" ? o.shortMessage : "RPC error";
|
|
1198
|
+
return { code: o.code, message: pageText(text), ...isHexData(o.data) ? { data: o.data } : {} };
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
if (!(e instanceof Error) && typeof e === "object" && e !== null) {
|
|
1202
|
+
const o = e;
|
|
1203
|
+
if (isNodeCode(o.code) && typeof o.message === "string") return { code: o.code, message: pageText(o.message), ...isHexData(o.data) ? { data: o.data } : {} };
|
|
1204
|
+
}
|
|
1205
|
+
return void 0;
|
|
1206
|
+
}
|
|
1207
|
+
function toRelayError(e, options = {}) {
|
|
1208
|
+
if (e instanceof RouterError) return e.data === void 0 ? { code: e.code, message: pageText(e.message) } : { code: e.code, message: pageText(e.message), data: e.data };
|
|
1209
|
+
if (e instanceof ReviewDeclined) return { code: e.code, message: pageText(e.message) };
|
|
1210
|
+
if (e instanceof UserRejectedError) return { code: RELAY_ERRORS.userRejected, message: pageText(e.message) };
|
|
1211
|
+
const node = nodeError(e);
|
|
1212
|
+
if (node) return node;
|
|
1213
|
+
const code = e?.code;
|
|
1214
|
+
if (e instanceof Error && typeof code === "number" && PROVIDER_CODES.has(code)) return { code, message: pageText(e.message) };
|
|
1215
|
+
options.log?.(e);
|
|
1216
|
+
return { code: RELAY_ERRORS.internal, message: "Internal error" };
|
|
1095
1217
|
}
|
|
1096
1218
|
var READ_METHODS = /* @__PURE__ */ new Set([
|
|
1097
1219
|
"eth_blockNumber",
|
|
@@ -1344,6 +1466,10 @@ function describeSiwe(text, ctx) {
|
|
|
1344
1466
|
}
|
|
1345
1467
|
|
|
1346
1468
|
// src/signing.ts
|
|
1469
|
+
var safeNonce = (n) => {
|
|
1470
|
+
if (n < 0n || n > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`The RPC returned an impossible nonce (${n}).`);
|
|
1471
|
+
return Number(n.toString());
|
|
1472
|
+
};
|
|
1347
1473
|
var ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
1348
1474
|
var HEX = /^0x([0-9a-fA-F]{2})*$/;
|
|
1349
1475
|
var lc = (s) => s.toLowerCase();
|
|
@@ -1439,13 +1565,13 @@ async function prepareTransaction(deps, network, from, tx) {
|
|
|
1439
1565
|
const base = block?.baseFeePerGas != null ? big(block.baseFeePerGas) : null;
|
|
1440
1566
|
if (base === null) {
|
|
1441
1567
|
const gasPrice = big(await deps.rpc(network, "eth_gasPrice", []));
|
|
1442
|
-
return { chainId: network.chainId, nonce:
|
|
1568
|
+
return { chainId: network.chainId, nonce: safeNonce(big(nonceHex)), gasLimit: hex(gasLimit), gasPrice: hex(gasPrice), maxFee: (gasLimit * gasPrice).toString() };
|
|
1443
1569
|
}
|
|
1444
1570
|
const floor = deps.tipFloor?.(network.chainId) ?? MIN_TIP_WEI;
|
|
1445
1571
|
const nodeTip = await deps.rpc(network, "eth_maxPriorityFeePerGas", []).then(big).catch(() => 0n);
|
|
1446
1572
|
const tip = nodeTip > floor ? nodeTip : floor;
|
|
1447
1573
|
const maxFeePerGas = base * 2n + tip;
|
|
1448
|
-
return { chainId: network.chainId, nonce:
|
|
1574
|
+
return { chainId: network.chainId, nonce: safeNonce(big(nonceHex)), gasLimit: hex(gasLimit), maxFeePerGas: hex(maxFeePerGas), maxPriorityFeePerGas: hex(tip), maxFee: (gasLimit * maxFeePerGas).toString() };
|
|
1449
1575
|
}
|
|
1450
1576
|
function createSignHandler(deps) {
|
|
1451
1577
|
return async ({ origin, network, method, params, accounts }) => {
|
|
@@ -2201,6 +2327,7 @@ export {
|
|
|
2201
2327
|
TRON_PROVIDERS,
|
|
2202
2328
|
TonConnectError,
|
|
2203
2329
|
VAULT_DEFAULT_ITERATIONS,
|
|
2330
|
+
VAULT_LIMITS,
|
|
2204
2331
|
VAULT_MIN_ITERATIONS,
|
|
2205
2332
|
VAULT_MIN_PASSWORD_LENGTH,
|
|
2206
2333
|
VAULT_MIN_SCRYPT,
|