@anywalletkit/extension-kit 0.1.1 → 0.1.3

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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # @anywalletkit/extension-kit
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@anywalletkit/extension-kit?label=npm)](https://www.npmjs.com/package/@anywalletkit/extension-kit)
4
+
3
5
  The security-relevant core of a non-custodial Chromium (MV3) wallet extension,
4
6
  built on the Any Wallet Kit chain and protocol packages: an encrypted vault, the
5
7
  unlocked-key session with auto-lock, the dApp request router with per-origin
@@ -9,6 +11,15 @@ code of its own: the chrome adapters take structural objects
9
11
  (`StorageAreaLike`, `AlarmsLike`), so everything runs under Vitest with
10
12
  in-memory adapters.
11
13
 
14
+ ## Install
15
+
16
+ ```sh
17
+ npm install @anywalletkit/extension-kit
18
+ # or: pnpm add @anywalletkit/extension-kit · yarn add @anywalletkit/extension-kit
19
+ ```
20
+
21
+ On npm: [@anywalletkit/extension-kit](https://www.npmjs.com/package/@anywalletkit/extension-kit), published from the [release workflow](https://github.com/ultratestbro/anywalletkit/blob/main/RELEASING.md) with provenance.
22
+
12
23
  This package is for wallet developers who build their own extension. It is a
13
24
  library, not an extension: the popup, approval windows, injected providers,
14
25
  service-worker wiring, build and release pipeline are the wallet's. The Vaulty
@@ -22,11 +33,11 @@ pnpm add @anywalletkit/extension-kit @anywalletkit/core @anywalletkit/keyring
22
33
 
23
34
  | Area | Exports | What |
24
35
  | --- | --- | --- |
25
- | Vault | `Vault`, `chromeLocalVaultStorage`, `memoryVaultStorage`, `VAULT_SCRYPT`, `VAULT_MIN_PASSWORD_LENGTH` | every secret at rest in `chrome.storage.local`, two key layers (a KEK wraps a DEK, the DEK seals each secret); passkey (WebAuthn PRF) unlock as a second wrap of the same DEK |
36
+ | Vault | `Vault`, `chromeLocalVaultStorage`, `memoryVaultStorage`, `VAULT_SCRYPT`, `VAULT_LIMITS`, `VAULT_MIN_PASSWORD_LENGTH` | every secret at rest in `chrome.storage.local`, two key layers (a KEK wraps a DEK, the DEK seals each secret); passkey (WebAuthn PRF) unlock as a second wrap of the same DEK |
26
37
  | Session | `Session`, `chromeSessionStorage`, `chromeAlarms`, `SESSION_DEFAULT_AUTOLOCK_MS` | the DEK in `chrome.storage.session` with an expiry and an alarm; `getKey()` re-checks the expiry itself |
27
38
  | Keyring adapters | `chromeKeyringStorage`, `vaultSecretStorage` | `@anywalletkit/keyring` metadata as plain JSON, secrets through the vault, one DEK use per call |
28
39
  | Relay | `PageRequest` / `RelayRequest` shapes and guards, `stampOrigin`, `checkSenderOrigin`, `RELAY_ERRORS` | page ⇄ content script ⇄ service worker messages; the origin is the browser's (`port.sender`), never the page's |
29
- | Router | `RequestRouter`, `READ_METHODS`, `SIGNING_METHODS` | what a dApp may call: accounts only for connected origins, chain add / switch through the approval window, reads from the wallet's own RPCs, `eth_sign` never offered, everything else 4200 |
40
+ | Router | `RequestRouter`, `READ_METHODS`, `SIGNING_METHODS`, `toRelayError` | what a dApp may call: accounts only for connected origins, chain add / switch through the approval window, reads from the wallet's own RPCs, `eth_sign` never offered, everything else 4200 |
30
41
  | Permissions | `PermissionStore`, `CHAIN_ACCOUNT_KEYS` | per-origin accounts (EVM, Solana, TON, Tron, Bitcoin, NEAR) and chain |
31
42
  | Approvals | `ApprovalQueue`, `ReviewDeclined`, `QueueLimitError`, `APPROVALS_MAX_PENDING_PER_ORIGIN` | one approval window at a time, at most 3 pending requests per origin and one pending `connect`; transport notes (`setVia`) for WalletConnect peers |
32
43
  | Signing | `classifySignRequest`, `prepareTransaction`, `createSignHandler`, `messageText` | connected-address and chain checks, EIP-7702 delegations from sites refused, nonce / gas / fees from the wallet's RPC |
@@ -46,7 +57,11 @@ pnpm add @anywalletkit/extension-kit @anywalletkit/core @anywalletkit/keyring
46
57
  AES-256-GCM wraps a random 32-byte DEK; the DEK seals each secret under AAD
47
58
  `forgewallet-vault/secret/<id>` (boxes cannot be swapped). The DEK box's AAD
48
59
  binds the exact KDF parameters, so an envelope whose salt or cost was edited
49
- on disk fails to open; `unlock` refuses sub-floor costs (`weak-kdf`). v1
60
+ on disk fails to open; `unlock` refuses sub-floor costs (`weak-kdf`), and
61
+ costs or sizes above `VAULT_LIMITS` (scrypt N ≤ 2^20, r ≤ 16, p ≤ 4; PBKDF2
62
+ ≤ 5 000 000 iterations; 1 000 secrets, 32 passkeys, 4 MiB) before any KDF
63
+ runs (`unsupported-envelope`). Writes to the envelope are serialized (one
64
+ `Vault` per storage area), so parallel imports keep every secret. v1
50
65
  envelopes (PBKDF2-SHA256, ≥ 600 000 iterations) still open and are re-sealed
51
66
  as v2. Passwords shorter than 8 characters are refused by the vault itself.
52
67
  Wrong password and tampering are the same failure (`wrong-password`).
@@ -56,6 +71,11 @@ pnpm add @anywalletkit/extension-kit @anywalletkit/core @anywalletkit/keyring
56
71
  - **Unlocked state.** Only the DEK is kept, in `chrome.storage.session`
57
72
  (memory-only, trusted contexts), with an expiry enforced on every read and an
58
73
  alarm that locks; key buffers this package owns are zeroized after use.
74
+ `unlock` / `lock` / `touch` / `getKey` run one at a time, so activity racing a
75
+ lock never brings the key back.
76
+ - **Errors to pages.** `toRelayError(e, { log })` passes the wallet's own refusals
77
+ (EIP-1193 codes) and a node's JSON-RPC error (code, revert data) with URLs
78
+ removed; anything else is `-32603 Internal error` on the page and goes to `log`.
59
79
  - **Origins.** The service worker answers a relay message only when the
60
80
  content script's stamped origin equals `port.sender.origin`. Permissions are
61
81
  keyed by that origin, never by a URL a page or a WalletConnect peer claims.
package/dist/index.cjs CHANGED
@@ -63,6 +63,7 @@ __export(src_exports, {
63
63
  TRON_PROVIDERS: () => TRON_PROVIDERS,
64
64
  TonConnectError: () => TonConnectError,
65
65
  VAULT_DEFAULT_ITERATIONS: () => VAULT_DEFAULT_ITERATIONS,
66
+ VAULT_LIMITS: () => VAULT_LIMITS,
66
67
  VAULT_MIN_ITERATIONS: () => VAULT_MIN_ITERATIONS,
67
68
  VAULT_MIN_PASSWORD_LENGTH: () => VAULT_MIN_PASSWORD_LENGTH,
68
69
  VAULT_MIN_SCRYPT: () => VAULT_MIN_SCRYPT,
@@ -185,6 +186,22 @@ var VAULT_DEFAULT_ITERATIONS = 6e5;
185
186
  var VAULT_SCRYPT = { N: 1 << 17, r: 8, p: 1 };
186
187
  var VAULT_MIN_SCRYPT = { N: 1 << 14, r: 8, p: 1 };
187
188
  var VAULT_MIN_PASSWORD_LENGTH = 8;
189
+ var VAULT_LIMITS = Object.freeze({
190
+ /** scrypt cost: 2^20 × 16 = 2 GiB is the most any envelope may ask for. */
191
+ scrypt: Object.freeze({ N: 1 << 20, r: 16, p: 4 }),
192
+ /** PBKDF2-SHA256 iterations (v1 envelopes). */
193
+ iterations: 5e6,
194
+ /** Sealed secrets in one vault. */
195
+ secrets: 1e3,
196
+ /** Registered passkeys. */
197
+ passkeys: 32,
198
+ /** Plaintext bytes of one secret (`setSecret`). */
199
+ secretBytes: 16 * 1024,
200
+ /** base64 characters of one sealed box (iv or data). */
201
+ boxChars: 32 * 1024,
202
+ /** JSON characters of the whole envelope. */
203
+ envelopeChars: 4 * 1024 * 1024
204
+ });
188
205
  var SALT_BYTES = 32;
189
206
  var IV_BYTES = 12;
190
207
  var DEK_BYTES = 32;
@@ -196,6 +213,8 @@ var aadSecret = (id) => `forgewallet-vault/secret/${id}`;
196
213
  var HKDF_INFO = "forgewallet-vault/2/passkey";
197
214
  var Vault = class {
198
215
  _storage;
216
+ /** Every read-modify-write of the envelope runs here, one after another (a parallel write would drop the other's change). */
217
+ _queue = Promise.resolve();
199
218
  _kdf;
200
219
  _iterations;
201
220
  _scrypt;
@@ -208,10 +227,11 @@ var Vault = class {
208
227
  this._scrypt = options.scrypt ?? VAULT_SCRYPT;
209
228
  this._onProgress = options.onProgress;
210
229
  this._crypto = options.crypto ?? globalThis.crypto;
211
- if (!Number.isInteger(this._iterations) || this._iterations < VAULT_MIN_ITERATIONS) {
212
- throw new VaultError(`PBKDF2 iterations must be an integer \u2265 ${VAULT_MIN_ITERATIONS}`, "invalid-input");
230
+ if (!Number.isInteger(this._iterations) || this._iterations < VAULT_MIN_ITERATIONS || this._iterations > VAULT_LIMITS.iterations) {
231
+ throw new VaultError(`PBKDF2 iterations must be an integer from ${VAULT_MIN_ITERATIONS} to ${VAULT_LIMITS.iterations}`, "invalid-input");
213
232
  }
214
233
  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");
234
+ 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");
215
235
  }
216
236
  async exists() {
217
237
  return await this._storage.get() !== null;
@@ -230,10 +250,12 @@ var Vault = class {
230
250
  */
231
251
  async create(password) {
232
252
  requirePassword(password, VAULT_MIN_PASSWORD_LENGTH);
233
- if (await this.exists()) throw new VaultError("A vault already exists; unlock it or reset explicitly.", "exists");
234
- const dek = this._random(DEK_BYTES);
235
- await this._storage.set(await this._sealEnvelope(password, dek, {}, [], Date.now()));
236
- return dek;
253
+ return this._exclusive(async () => {
254
+ if (await this.exists()) throw new VaultError("A vault already exists; unlock it or reset explicitly.", "exists");
255
+ const dek = this._random(DEK_BYTES);
256
+ await this._write(await this._sealEnvelope(password, dek, {}, [], Date.now()));
257
+ return dek;
258
+ });
237
259
  }
238
260
  /**
239
261
  * Derive the KEK from `password`, unseal and return the DEK. `wrong-password` on
@@ -242,6 +264,9 @@ var Vault = class {
242
264
  */
243
265
  async unlock(password) {
244
266
  requirePassword(password);
267
+ return this._exclusive(() => this._unlock(password));
268
+ }
269
+ async _unlock(password) {
245
270
  const envelope = await this._load();
246
271
  if (isEnvelopeV1(envelope)) {
247
272
  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");
@@ -249,7 +274,7 @@ var Vault = class {
249
274
  const dek = await this._open(kek2, envelope.dek, aadDekV1(envelope.kdf), "wrong-password");
250
275
  if (this._kdf === "scrypt") {
251
276
  const next = await this._sealEnvelope(password, dek, envelope.secrets, [], envelope.createdAt);
252
- await this._storage.set(isEnvelopeV2(next) ? { ...next, upgradedAt: Date.now() } : next);
277
+ await this._write(isEnvelopeV2(next) ? { ...next, upgradedAt: Date.now() } : next);
253
278
  }
254
279
  return dek;
255
280
  }
@@ -260,15 +285,18 @@ var Vault = class {
260
285
  /** Re-wrap the DEK under a new password (fresh salt, current KDF). Secrets and passkeys are untouched. */
261
286
  async changePassword(oldPassword, newPassword) {
262
287
  requirePassword(newPassword, VAULT_MIN_PASSWORD_LENGTH);
263
- const dek = await this.unlock(oldPassword);
264
- try {
265
- const envelope = await this._load();
266
- const passkeys = isEnvelopeV2(envelope) ? envelope.passkeys : [];
267
- const next = await this._sealEnvelope(newPassword, dek, envelope.secrets, passkeys, envelope.createdAt);
268
- await this._storage.set(isEnvelopeV2(envelope) && envelope.upgradedAt !== void 0 && isEnvelopeV2(next) ? { ...next, upgradedAt: envelope.upgradedAt } : next);
269
- } finally {
270
- (0, import_core.zeroize)(dek);
271
- }
288
+ requirePassword(oldPassword);
289
+ return this._exclusive(async () => {
290
+ const dek = await this._unlock(oldPassword);
291
+ try {
292
+ const envelope = await this._load();
293
+ const passkeys = isEnvelopeV2(envelope) ? envelope.passkeys : [];
294
+ const next = await this._sealEnvelope(newPassword, dek, envelope.secrets, passkeys, envelope.createdAt);
295
+ await this._write(isEnvelopeV2(envelope) && envelope.upgradedAt !== void 0 && isEnvelopeV2(next) ? { ...next, upgradedAt: envelope.upgradedAt } : next);
296
+ } finally {
297
+ (0, import_core.zeroize)(dek);
298
+ }
299
+ });
272
300
  }
273
301
  // ---- passkeys (v2) ----
274
302
  /** Public view of the registered passkeys (ids, labels; whether each can verify assertions). */
@@ -294,21 +322,24 @@ var Vault = class {
294
322
  try {
295
323
  requireId(passkey.id);
296
324
  if (passkey.prf.length !== PRF_BYTES) throw new VaultError(`A PRF output is ${PRF_BYTES} bytes.`, "invalid-input");
297
- const envelope = await this._load();
298
- if (!isEnvelopeV2(envelope)) throw new VaultError("Unlock with the password once so the vault is upgraded before adding a passkey.", "unsupported-envelope");
299
- if (envelope.passkeys.some((p) => p.id === passkey.id)) throw new VaultError("This passkey is already registered.", "exists");
300
- await this._requireDek(dek, envelope);
301
- const key = await this._derivePasskeyKek(passkey.prf, passkey.id);
302
- const entry = { id: passkey.id, label: passkey.label.slice(0, 64), createdAt: Date.now(), dek: await this._seal(key, dek, aadPasskey(passkey.id)) };
303
- if (passkey.publicKey && passkey.alg !== void 0) {
304
- entry.publicKey = passkey.publicKey;
305
- entry.alg = passkey.alg;
306
- }
307
- await this._storage.set({ ...envelope, passkeys: [...envelope.passkeys, entry] });
325
+ await this._exclusive(() => this._addPasskey(dek, passkey));
308
326
  } finally {
309
327
  (0, import_core.zeroize)(passkey.prf);
310
328
  }
311
329
  }
330
+ async _addPasskey(dek, passkey) {
331
+ const envelope = await this._load();
332
+ if (!isEnvelopeV2(envelope)) throw new VaultError("Unlock with the password once so the vault is upgraded before adding a passkey.", "unsupported-envelope");
333
+ if (envelope.passkeys.some((p) => p.id === passkey.id)) throw new VaultError("This passkey is already registered.", "exists");
334
+ await this._requireDek(dek, envelope);
335
+ const key = await this._derivePasskeyKek(passkey.prf, passkey.id);
336
+ const entry = { id: passkey.id, label: passkey.label.slice(0, 64), createdAt: Date.now(), dek: await this._seal(key, dek, aadPasskey(passkey.id)) };
337
+ if (passkey.publicKey && passkey.alg !== void 0) {
338
+ entry.publicKey = passkey.publicKey;
339
+ entry.alg = passkey.alg;
340
+ }
341
+ await this._write({ ...envelope, passkeys: [...envelope.passkeys, entry] });
342
+ }
312
343
  /** Unseal the DEK with a passkey's PRF output. `wrong-password` when the box does not open, `missing` when the id is unknown. */
313
344
  async unlockWithPasskey(id, prf) {
314
345
  try {
@@ -326,9 +357,11 @@ var Vault = class {
326
357
  }
327
358
  async removePasskey(id) {
328
359
  requireId(id);
329
- const envelope = await this._load();
330
- if (!isEnvelopeV2(envelope)) return;
331
- await this._storage.set({ ...envelope, passkeys: envelope.passkeys.filter((p) => p.id !== id) });
360
+ return this._exclusive(async () => {
361
+ const envelope = await this._load();
362
+ if (!isEnvelopeV2(envelope)) return;
363
+ await this._write({ ...envelope, passkeys: envelope.passkeys.filter((p) => p.id !== id) });
364
+ });
332
365
  }
333
366
  // ---- secrets ----
334
367
  async listSecretIds() {
@@ -338,10 +371,16 @@ var Vault = class {
338
371
  /** Seal `secret` under the DEK as `sourceId` (overwrites an entry with the same id). */
339
372
  async setSecret(dek, sourceId, secret) {
340
373
  requireId(sourceId);
341
- const envelope = await this._load();
374
+ if (secret.length > VAULT_LIMITS.secretBytes) throw new VaultError(`A secret is at most ${VAULT_LIMITS.secretBytes} bytes.`, "invalid-input");
342
375
  const key = await this._importDek(dek);
343
- const box = await this._seal(key, secret, aadSecret(sourceId));
344
- await this._storage.set({ ...envelope, secrets: { ...envelope.secrets, [sourceId]: box } });
376
+ return this._exclusive(async () => {
377
+ const envelope = await this._load();
378
+ if (!(sourceId in envelope.secrets) && Object.keys(envelope.secrets).length >= VAULT_LIMITS.secrets) {
379
+ throw new VaultError(`A vault holds at most ${VAULT_LIMITS.secrets} secrets.`, "invalid-input");
380
+ }
381
+ const box = await this._seal(key, secret, aadSecret(sourceId));
382
+ await this._write({ ...envelope, secrets: { ...envelope.secrets, [sourceId]: box } });
383
+ });
345
384
  }
346
385
  /** Unseal one secret. `null` when the id is unknown; the caller zeroizes the result after use. */
347
386
  async getSecret(dek, sourceId) {
@@ -354,15 +393,29 @@ var Vault = class {
354
393
  }
355
394
  async deleteSecret(sourceId) {
356
395
  requireId(sourceId);
357
- const envelope = await this._load();
358
- if (!(sourceId in envelope.secrets)) return;
359
- const secrets = { ...envelope.secrets };
360
- delete secrets[sourceId];
361
- await this._storage.set({ ...envelope, secrets });
396
+ return this._exclusive(async () => {
397
+ const envelope = await this._load();
398
+ if (!(sourceId in envelope.secrets)) return;
399
+ const secrets = { ...envelope.secrets };
400
+ delete secrets[sourceId];
401
+ await this._write({ ...envelope, secrets });
402
+ });
362
403
  }
363
404
  /** Remove the whole vault. The explicit destructive flow only (back up → wipe → create). */
364
405
  async destroy() {
365
- await this._storage.clear();
406
+ return this._exclusive(() => this._storage.clear());
407
+ }
408
+ // ---- serialized writes ----
409
+ _exclusive(fn) {
410
+ const run = this._queue.then(fn);
411
+ this._queue = run.catch(() => void 0);
412
+ return run;
413
+ }
414
+ /** Store an envelope this build will open again (bounds checked before it is written). */
415
+ async _write(envelope) {
416
+ const problem = envelopeProblem(envelope);
417
+ if (problem) throw new VaultError(`The vault would exceed its limits (${problem}); nothing was written.`, "invalid-input");
418
+ await this._storage.set(envelope);
366
419
  }
367
420
  // ---- crypto ----
368
421
  _random(n) {
@@ -438,6 +491,8 @@ var Vault = class {
438
491
  const envelope = await this._storage.get();
439
492
  if (!envelope) throw new VaultError("No vault in this storage area.", "missing");
440
493
  if (!isEnvelopeV1(envelope) && !isEnvelopeV2(envelope)) throw new VaultError("Unsupported vault envelope.", "unsupported-envelope");
494
+ const problem = envelopeProblem(envelope);
495
+ if (problem) throw new VaultError(`The vault envelope is outside the limits this build opens (${problem}).`, "unsupported-envelope");
441
496
  return envelope;
442
497
  }
443
498
  };
@@ -456,6 +511,22 @@ function isEnvelopeV2(x) {
456
511
  function isBox(x) {
457
512
  return typeof x === "object" && x !== null && typeof x.iv === "string" && typeof x.data === "string";
458
513
  }
514
+ function isBoundedScrypt(c) {
515
+ return c.N <= VAULT_LIMITS.scrypt.N && c.r <= VAULT_LIMITS.scrypt.r && c.p <= VAULT_LIMITS.scrypt.p;
516
+ }
517
+ function envelopeProblem(e) {
518
+ if (e.v === 1 && e.kdf.iterations > VAULT_LIMITS.iterations) return `PBKDF2 iterations ${e.kdf.iterations} > ${VAULT_LIMITS.iterations}`;
519
+ 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}`;
520
+ const secrets = Object.values(e.secrets);
521
+ if (secrets.length > VAULT_LIMITS.secrets) return `${secrets.length} secrets > ${VAULT_LIMITS.secrets}`;
522
+ const passkeys = e.v === 2 ? e.passkeys : [];
523
+ if (passkeys.length > VAULT_LIMITS.passkeys) return `${passkeys.length} passkeys > ${VAULT_LIMITS.passkeys}`;
524
+ for (const box of [e.dek, ...secrets, ...passkeys.map((p) => p.dek)]) {
525
+ if (box.iv.length > VAULT_LIMITS.boxChars || box.data.length > VAULT_LIMITS.boxChars) return `a sealed box above ${VAULT_LIMITS.boxChars} characters`;
526
+ }
527
+ if (JSON.stringify(e).length > VAULT_LIMITS.envelopeChars) return `envelope above ${VAULT_LIMITS.envelopeChars} characters`;
528
+ return null;
529
+ }
459
530
  function isStrongScrypt(c) {
460
531
  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;
461
532
  }
@@ -500,6 +571,13 @@ var Session = class {
500
571
  _alarm;
501
572
  _now;
502
573
  _onLock = /* @__PURE__ */ new Set();
574
+ /**
575
+ * Every operation on the stored record runs here, one after another: a `touch` that read the
576
+ * record before a `lock` removed it would otherwise write it back and undo the lock.
577
+ */
578
+ _queue = Promise.resolve();
579
+ /** Bumped by every lock; a `touch` writes only when no lock happened since it read the record. */
580
+ _epoch = 0;
503
581
  constructor(options) {
504
582
  this._storage = options.storage;
505
583
  this._alarms = options.alarms;
@@ -515,20 +593,19 @@ var Session = class {
515
593
  /** Store the DEK and arm the auto-lock. The caller's copy is zeroized. */
516
594
  async unlock(dek) {
517
595
  if (dek.length !== 32) throw new VaultError("Vault key must be 32 bytes.", "invalid-input");
518
- const expiresAt = this._expiry();
519
- await this._storage.set(this._key, { key: toBase64(dek), expiresAt });
520
- (0, import_core2.zeroize)(dek);
521
- await this._arm(expiresAt);
596
+ return this._serial(async () => {
597
+ const expiresAt = this._expiry();
598
+ await this._storage.set(this._key, { key: toBase64(dek), expiresAt });
599
+ (0, import_core2.zeroize)(dek);
600
+ await this._arm(expiresAt);
601
+ });
522
602
  }
523
603
  /** Forget the DEK and cancel the alarm. Safe to call when already locked. */
524
604
  async lock() {
525
- const was = await this._storage.get(this._key);
526
- await this._storage.remove(this._key);
527
- await this._alarms.clear(this._alarm);
528
- if (was !== void 0 && was !== null) for (const cb of this._onLock) cb();
605
+ return this._serial(() => this._lockNow());
529
606
  }
530
607
  async isUnlocked() {
531
- return await this._read() !== null;
608
+ return this._serial(async () => await this._read() !== null);
532
609
  }
533
610
  /**
534
611
  * The DEK for one operation (zeroize it after). `locked` when there is none or the
@@ -536,17 +613,22 @@ var Session = class {
536
613
  * a missed alarm cannot keep the key alive.
537
614
  */
538
615
  async getKey() {
539
- const record = await this._read();
540
- if (!record) throw new VaultError("Wallet is locked.", "locked");
541
- return fromBase64(record.key);
616
+ return this._serial(async () => {
617
+ const record = await this._read();
618
+ if (!record) throw new VaultError("Wallet is locked.", "locked");
619
+ return fromBase64(record.key);
620
+ });
542
621
  }
543
622
  /** User activity: push the auto-lock out again. No-op when locked. */
544
623
  async touch() {
545
- const record = await this._read();
546
- if (!record) return;
547
- const expiresAt = this._expiry();
548
- await this._storage.set(this._key, { ...record, expiresAt });
549
- await this._arm(expiresAt);
624
+ return this._serial(async () => {
625
+ const epoch = this._epoch;
626
+ const record = await this._read();
627
+ if (!record || epoch !== this._epoch) return;
628
+ const expiresAt = this._expiry();
629
+ await this._storage.set(this._key, { ...record, expiresAt });
630
+ await this._arm(expiresAt);
631
+ });
550
632
  }
551
633
  get autoLockMs() {
552
634
  return this._autoLockMs;
@@ -561,6 +643,19 @@ var Session = class {
561
643
  this._onLock.add(callback);
562
644
  return () => this._onLock.delete(callback);
563
645
  }
646
+ _serial(fn) {
647
+ const run = this._queue.then(fn);
648
+ this._queue = run.catch(() => void 0);
649
+ return run;
650
+ }
651
+ /** The lock itself; callers are already in the queue. */
652
+ async _lockNow() {
653
+ this._epoch++;
654
+ const was = await this._storage.get(this._key);
655
+ await this._storage.remove(this._key);
656
+ await this._alarms.clear(this._alarm);
657
+ if (was !== void 0 && was !== null) for (const cb of this._onLock) cb();
658
+ }
564
659
  _expiry() {
565
660
  return this._autoLockMs > 0 ? this._now() + this._autoLockMs : Number.POSITIVE_INFINITY;
566
661
  }
@@ -572,7 +667,7 @@ var Session = class {
572
667
  const raw = await this._storage.get(this._key);
573
668
  if (!raw || typeof raw.key !== "string" || typeof raw.expiresAt !== "number") return null;
574
669
  if (raw.expiresAt <= this._now()) {
575
- await this.lock();
670
+ await this._lockNow();
576
671
  return null;
577
672
  }
578
673
  return raw;
@@ -1219,6 +1314,7 @@ function memoryApprovalWindow() {
1219
1314
  }
1220
1315
 
1221
1316
  // src/router.ts
1317
+ var import_core4 = require("@anywalletkit/core");
1222
1318
  var RouterError = class _RouterError extends Error {
1223
1319
  code;
1224
1320
  data;
@@ -1232,11 +1328,35 @@ var RouterError = class _RouterError extends Error {
1232
1328
  return new _RouterError(e.code, e.message, e.data);
1233
1329
  }
1234
1330
  };
1235
- function toRelayError(e) {
1236
- if (e instanceof RouterError) return e.data === void 0 ? { code: e.code, message: e.message } : { code: e.code, message: e.message, data: e.data };
1237
- if (e instanceof ReviewDeclined) return { code: e.code, message: e.message };
1238
- const code = typeof e?.code === "number" ? e.code : RELAY_ERRORS.internal;
1239
- return { code, message: e instanceof Error ? e.message : String(e) };
1331
+ var PROVIDER_CODES = /* @__PURE__ */ new Set([4001, 4100, 4200, 4900, 4901, 4902]);
1332
+ var isNodeCode = (code) => typeof code === "number" && Number.isInteger(code) && (code === 3 || code >= -32768 && code <= -32e3);
1333
+ var URL_RE = /\b[a-z][a-z0-9+.-]*:\/\/[^\s"'<>()]+/gi;
1334
+ var pageText = (text) => text.replace(URL_RE, "[url]").split("\n")[0].slice(0, 300);
1335
+ var isHexData = (v) => typeof v === "string" && /^0x[0-9a-fA-F]*$/.test(v) && v.length <= 2 + 2 * 8192;
1336
+ function nodeError(e) {
1337
+ for (let cur = e, depth = 0; cur && typeof cur === "object" && depth < 8; cur = cur.cause, depth++) {
1338
+ const o = cur;
1339
+ if (o.name === "RpcRequestError" && isNodeCode(o.code)) {
1340
+ const text = typeof o.details === "string" && o.details ? o.details : typeof o.shortMessage === "string" ? o.shortMessage : "RPC error";
1341
+ return { code: o.code, message: pageText(text), ...isHexData(o.data) ? { data: o.data } : {} };
1342
+ }
1343
+ }
1344
+ if (!(e instanceof Error) && typeof e === "object" && e !== null) {
1345
+ const o = e;
1346
+ if (isNodeCode(o.code) && typeof o.message === "string") return { code: o.code, message: pageText(o.message), ...isHexData(o.data) ? { data: o.data } : {} };
1347
+ }
1348
+ return void 0;
1349
+ }
1350
+ function toRelayError(e, options = {}) {
1351
+ 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 };
1352
+ if (e instanceof ReviewDeclined) return { code: e.code, message: pageText(e.message) };
1353
+ if (e instanceof import_core4.UserRejectedError) return { code: RELAY_ERRORS.userRejected, message: pageText(e.message) };
1354
+ const node = nodeError(e);
1355
+ if (node) return node;
1356
+ const code = e?.code;
1357
+ if (e instanceof Error && typeof code === "number" && PROVIDER_CODES.has(code)) return { code, message: pageText(e.message) };
1358
+ options.log?.(e);
1359
+ return { code: RELAY_ERRORS.internal, message: "Internal error" };
1240
1360
  }
1241
1361
  var READ_METHODS = /* @__PURE__ */ new Set([
1242
1362
  "eth_blockNumber",
@@ -1489,6 +1609,10 @@ function describeSiwe(text, ctx) {
1489
1609
  }
1490
1610
 
1491
1611
  // src/signing.ts
1612
+ var safeNonce = (n) => {
1613
+ if (n < 0n || n > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`The RPC returned an impossible nonce (${n}).`);
1614
+ return Number(n.toString());
1615
+ };
1492
1616
  var ADDRESS = /^0x[0-9a-fA-F]{40}$/;
1493
1617
  var HEX = /^0x([0-9a-fA-F]{2})*$/;
1494
1618
  var lc = (s) => s.toLowerCase();
@@ -1584,13 +1708,13 @@ async function prepareTransaction(deps, network, from, tx) {
1584
1708
  const base = block?.baseFeePerGas != null ? big(block.baseFeePerGas) : null;
1585
1709
  if (base === null) {
1586
1710
  const gasPrice = big(await deps.rpc(network, "eth_gasPrice", []));
1587
- return { chainId: network.chainId, nonce: Number(big(nonceHex)), gasLimit: hex(gasLimit), gasPrice: hex(gasPrice), maxFee: (gasLimit * gasPrice).toString() };
1711
+ return { chainId: network.chainId, nonce: safeNonce(big(nonceHex)), gasLimit: hex(gasLimit), gasPrice: hex(gasPrice), maxFee: (gasLimit * gasPrice).toString() };
1588
1712
  }
1589
1713
  const floor = deps.tipFloor?.(network.chainId) ?? MIN_TIP_WEI;
1590
1714
  const nodeTip = await deps.rpc(network, "eth_maxPriorityFeePerGas", []).then(big).catch(() => 0n);
1591
1715
  const tip = nodeTip > floor ? nodeTip : floor;
1592
1716
  const maxFeePerGas = base * 2n + tip;
1593
- return { chainId: network.chainId, nonce: Number(big(nonceHex)), gasLimit: hex(gasLimit), maxFeePerGas: hex(maxFeePerGas), maxPriorityFeePerGas: hex(tip), maxFee: (gasLimit * maxFeePerGas).toString() };
1717
+ return { chainId: network.chainId, nonce: safeNonce(big(nonceHex)), gasLimit: hex(gasLimit), maxFeePerGas: hex(maxFeePerGas), maxPriorityFeePerGas: hex(tip), maxFee: (gasLimit * maxFeePerGas).toString() };
1594
1718
  }
1595
1719
  function createSignHandler(deps) {
1596
1720
  return async ({ origin, network, method, params, accounts }) => {