@anywalletkit/extension-kit 0.3.6 → 0.4.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 CHANGED
@@ -22,6 +22,8 @@ var src_exports = {};
22
22
  __export(src_exports, {
23
23
  ACTIVE_STATUSES: () => ACTIVE_STATUSES,
24
24
  APPROVALS_MAX_PENDING_PER_ORIGIN: () => APPROVALS_MAX_PENDING_PER_ORIGIN,
25
+ APPROVALS_MAX_PENDING_PER_SITE: () => APPROVALS_MAX_PENDING_PER_SITE,
26
+ APPROVALS_MAX_PENDING_TOTAL: () => APPROVALS_MAX_PENDING_TOTAL,
25
27
  ApprovalQueue: () => ApprovalQueue,
26
28
  BTC_CHAIN: () => BTC_CHAIN,
27
29
  BTC_ESPLORA: () => BTC_ESPLORA,
@@ -29,9 +31,12 @@ __export(src_exports, {
29
31
  DEFAULT_CHAIN: () => DEFAULT_CHAIN,
30
32
  DEFAULT_NETWORKS: () => DEFAULT_NETWORKS,
31
33
  FLOOR_TOLERANCE_BPS: () => FLOOR_TOLERANCE_BPS,
34
+ GETLOGS_MAX_SPAN_UNCONNECTED: () => GETLOGS_MAX_SPAN_UNCONNECTED,
32
35
  KEYRING_STORAGE_KEY: () => KEYRING_STORAGE_KEY,
33
36
  LOOKALIKE_PREFIX: () => LOOKALIKE_PREFIX,
34
37
  LOOKALIKE_SUFFIX: () => LOOKALIKE_SUFFIX,
38
+ MAX_FEE_REASKS: () => MAX_FEE_REASKS,
39
+ MULTI_LABEL_SUFFIXES: () => MULTI_LABEL_SUFFIXES,
35
40
  NEAR_CHAIN: () => NEAR_CHAIN,
36
41
  NEAR_PROVIDERS: () => NEAR_PROVIDERS,
37
42
  NON_EVM_CHAINS: () => NON_EVM_CHAINS,
@@ -52,8 +57,11 @@ __export(src_exports, {
52
57
  RouterError: () => RouterError,
53
58
  SESSION_DEFAULT_AUTOLOCK_MS: () => SESSION_DEFAULT_AUTOLOCK_MS,
54
59
  SIGNING_METHODS: () => SIGNING_METHODS,
60
+ SIWE_FORBIDDEN_CHARS: () => SIWE_FORBIDDEN_CHARS,
55
61
  SOLANA_CHAIN: () => SOLANA_CHAIN,
56
62
  SOLANA_RPCS: () => SOLANA_RPCS,
63
+ STORAGE_MAC_FIELD: () => STORAGE_MAC_FIELD,
64
+ STORAGE_VERSION_FIELD: () => STORAGE_VERSION_FIELD,
57
65
  Session: () => Session,
58
66
  SwapTracker: () => SwapTracker,
59
67
  SwapsStore: () => SwapsStore,
@@ -65,6 +73,7 @@ __export(src_exports, {
65
73
  TON_ENDPOINTS: () => TON_ENDPOINTS,
66
74
  TRON_CHAIN: () => TRON_CHAIN,
67
75
  TRON_PROVIDERS: () => TRON_PROVIDERS,
76
+ TokenBucket: () => TokenBucket,
68
77
  TonConnectError: () => import_tonconnect2.TonConnectError,
69
78
  VAULT_DEFAULT_ITERATIONS: () => VAULT_DEFAULT_ITERATIONS,
70
79
  VAULT_LIMITS: () => VAULT_LIMITS,
@@ -79,6 +88,7 @@ __export(src_exports, {
79
88
  ZCASH_CHAIN: () => ZCASH_CHAIN,
80
89
  approvalChallenge: () => approvalChallenge,
81
90
  caip2FromChainId: () => caip2FromChainId,
91
+ canonicalJson: () => canonicalJson,
82
92
  chainHex: () => chainHex,
83
93
  chainTxMatcher: () => chainTxMatcher,
84
94
  checkSenderOrigin: () => checkSenderOrigin,
@@ -91,6 +101,7 @@ __export(src_exports, {
91
101
  createSignHandler: () => createSignHandler,
92
102
  decodedMessage: () => decodedMessage,
93
103
  derToRaw: () => derToRaw,
104
+ deriveStorageMacKey: () => deriveStorageMacKey,
94
105
  describeSiwe: () => describeSiwe,
95
106
  earnApprovalPayload: () => earnApprovalPayload,
96
107
  earnPlanMatchers: () => earnPlanMatchers,
@@ -98,6 +109,7 @@ __export(src_exports, {
98
109
  evmCallMatcher: () => evmCallMatcher,
99
110
  executionFloor: () => executionFloor,
100
111
  fromBase64Url: () => fromBase64Url,
112
+ gasLimitExcessive: () => gasLimitExcessive,
101
113
  hostOf: () => hostOf,
102
114
  isCatalogChain: () => isCatalogChain,
103
115
  isEnvelopeV1: () => isEnvelopeV1,
@@ -111,6 +123,7 @@ __export(src_exports, {
111
123
  isRelayToContent: () => isRelayToContent,
112
124
  levenshtein: () => levenshtein,
113
125
  lookalikeOf: () => lookalikeOf,
126
+ macJsonStorage: () => macJsonStorage,
114
127
  memoryAlarms: () => memoryAlarms,
115
128
  memoryApprovalWindow: () => memoryApprovalWindow,
116
129
  memoryJsonStorage: () => memoryJsonStorage,
@@ -135,11 +148,15 @@ __export(src_exports, {
135
148
  rank: () => rank,
136
149
  rawToDer: () => rawToDer,
137
150
  relayError: () => relayError,
151
+ revealHidden: () => revealHidden,
138
152
  reviewEarnPlan: () => reviewEarnPlan,
139
153
  reviewSwapPlan: () => reviewSwapPlan,
140
154
  sameTonConnectAddress: () => import_tonconnect2.sameTonConnectAddress,
155
+ siteOf: () => siteOf,
156
+ siweCrossSite: () => siweCrossSite,
141
157
  siweProblems: () => siweProblems,
142
158
  stampOrigin: () => stampOrigin,
159
+ storageVersionLedger: () => storageVersionLedger,
143
160
  swapApprovalPayload: () => swapApprovalPayload,
144
161
  toBase64Url: () => toBase64Url,
145
162
  toRelayError: () => toRelayError,
@@ -291,7 +308,7 @@ var Vault = class {
291
308
  if (isEnvelopeV1(envelope)) {
292
309
  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");
293
310
  const kek2 = await this._derivePbkdf2(password, fromBase64(envelope.kdf.salt), envelope.kdf.iterations);
294
- const dek = await this._open(kek2, envelope.dek, aadDekV1(envelope.kdf), "wrong-password");
311
+ const dek = await this._proveOwn(await this._open(kek2, envelope.dek, aadDekV1(envelope.kdf), "wrong-password"), envelope);
295
312
  if (this._kdf === "scrypt") {
296
313
  const next = await this._sealEnvelope(password, dek, envelope.secrets, [], envelope.createdAt);
297
314
  await this._write(isEnvelopeV2(next) ? { ...next, upgradedAt: Date.now() } : next);
@@ -300,7 +317,24 @@ var Vault = class {
300
317
  }
301
318
  if (!isStrongScrypt(envelope.kdf)) throw new VaultError(`The vault's scrypt cost (N=${envelope.kdf.N} r=${envelope.kdf.r} p=${envelope.kdf.p}) is below the floor this build accepts.`, "weak-kdf");
302
319
  const kek = await this._deriveScrypt(password, fromBase64(envelope.kdf.salt), envelope.kdf);
303
- return this._open(kek, envelope.dek, aadDekV2(envelope.kdf), "wrong-password");
320
+ return this._proveOwn(await this._open(kek, envelope.dek, aadDekV2(envelope.kdf), "wrong-password"), envelope);
321
+ }
322
+ /**
323
+ * A DEK unwrapped from a dek / passkey box must be the key this vault's secrets are sealed under
324
+ * (audit 2026-11-09 E21): a box spliced in from another vault, under another password or passkey,
325
+ * opens with that password but holds a foreign key. Proven with the DEK check, or — in an envelope
326
+ * written before it — the first secret; an old empty envelope has nothing to prove it with. Returns
327
+ * `dek`; on a mismatch zeroizes it and fails as `wrong-password` (the same failure as tampering).
328
+ */
329
+ async _proveOwn(dek, envelope) {
330
+ if (envelope.dekCheck === void 0 && Object.keys(envelope.secrets).length === 0) return dek;
331
+ try {
332
+ await this._requireDek(dek, envelope);
333
+ return dek;
334
+ } catch {
335
+ (0, import_core.zeroize)(dek);
336
+ throw new VaultError("Wrong password.", "wrong-password");
337
+ }
304
338
  }
305
339
  /**
306
340
  * Re-wrap the DEK under a new password (fresh salt, current KDF). Secrets and passkeys are untouched —
@@ -332,9 +366,13 @@ var Vault = class {
332
366
  *
333
367
  * Passkeys are dropped — their boxes hold the old DEK and cannot be re-wrapped without their PRF
334
368
  * output — and must be registered again ({@link Vault.addPasskey} with the new DEK).
369
+ *
370
+ * With `newPassword` this is also the password change: the new DEK is wrapped under `newPassword`
371
+ * (at least {@link VAULT_MIN_PASSWORD_LENGTH} characters), and `password` must be the current one.
335
372
  */
336
- async rotateVaultKey(password) {
373
+ async rotateVaultKey(password, newPassword) {
337
374
  requirePassword(password);
375
+ if (newPassword !== void 0) requirePassword(newPassword, VAULT_MIN_PASSWORD_LENGTH);
338
376
  return this._exclusive(async () => {
339
377
  const oldDek = await this._unlock(password);
340
378
  const dek = this._random(DEK_BYTES);
@@ -350,7 +388,7 @@ var Vault = class {
350
388
  (0, import_core.zeroize)(plaintext);
351
389
  }
352
390
  }
353
- const next = await this._sealEnvelope(password, dek, secrets, [], envelope.createdAt);
391
+ const next = await this._sealEnvelope(newPassword ?? password, dek, secrets, [], envelope.createdAt);
354
392
  const upgradedAt = isEnvelopeV2(envelope) ? envelope.upgradedAt : void 0;
355
393
  await this._write(isEnvelopeV2(next) ? { ...next, ...upgradedAt !== void 0 ? { upgradedAt } : {}, rotatedAt: Date.now() } : next);
356
394
  return dek;
@@ -414,7 +452,7 @@ var Vault = class {
414
452
  const entry = envelope.passkeys.find((p) => p.id === id);
415
453
  if (!entry) throw new VaultError("No such passkey on this vault.", "missing");
416
454
  const key = await this._derivePasskeyKek(prf, id);
417
- return await this._open(key, entry.dek, aadPasskey(id), "wrong-password");
455
+ return await this._proveOwn(await this._open(key, entry.dek, aadPasskey(id), "wrong-password"), envelope);
418
456
  } finally {
419
457
  (0, import_core.zeroize)(prf);
420
458
  }
@@ -709,7 +747,11 @@ var Session = class {
709
747
  return fromBase64(record.key);
710
748
  });
711
749
  }
712
- /** User activity: push the auto-lock out again. No-op when locked. */
750
+ /**
751
+ * User activity: push the auto-lock out again. No-op when locked. Call it for explicit user
752
+ * gestures only (unlock, send, approve / decline, an action in the wallet's own UI) — not for
753
+ * passive polls, page requests or worker restarts, or the timer never runs out.
754
+ */
713
755
  async touch() {
714
756
  return this._serial(async () => {
715
757
  const epoch = this._epoch;
@@ -723,11 +765,25 @@ var Session = class {
723
765
  get autoLockMs() {
724
766
  return this._autoLockMs;
725
767
  }
726
- /** Change the timer (options page); re-arms from now when unlocked. 0 disables. */
768
+ /**
769
+ * Set the timer period (0 disables). Never pushes the deadline out — call this at every worker
770
+ * start with the stored setting: it re-arms the alarm at the stored deadline (a restart must not
771
+ * count as activity, audit 2026-11-09 E11), and only brings the deadline forward when the new
772
+ * period ends sooner. A longer period (or 0) applies from the next {@link Session.touch} /
773
+ * {@link Session.unlock}; call `touch()` right after when the user changed it explicitly.
774
+ */
727
775
  async setAutoLockMs(ms) {
728
776
  if (!Number.isFinite(ms) || ms < 0) throw new VaultError("Auto-lock must be a non-negative number of milliseconds.", "invalid-input");
729
777
  this._autoLockMs = ms;
730
- await this.touch();
778
+ return this._serial(async () => {
779
+ const epoch = this._epoch;
780
+ const record = await this._read();
781
+ if (!record || epoch !== this._epoch) return;
782
+ const cap = this._expiry();
783
+ const expiresAt = cap !== null && (record.expiresAt === null || cap < record.expiresAt) ? cap : record.expiresAt;
784
+ if (expiresAt !== record.expiresAt) await this._storage.set(this._key, { ...record, expiresAt });
785
+ await this._arm(expiresAt);
786
+ });
731
787
  }
732
788
  onLock(callback) {
733
789
  this._onLock.add(callback);
@@ -997,7 +1053,8 @@ var DEFAULT_NETWORKS = Object.freeze([
997
1053
  net(4326, "MegaETH", "ETH", ["https://mainnet.megaeth.com/rpc"]),
998
1054
  net(130, "Unichain", "ETH", ["https://mainnet.unichain.org", "https://unichain-rpc.publicnode.com"], "https://uniscan.xyz"),
999
1055
  net(143, "Monad", "MON", ["https://rpc.monad.xyz"]),
1000
- net(9745, "Plasma", "XPL", ["https://rpc.plasma.to"]),
1056
+ // Second operator (Tenderly's public gateway, keyless): rpc.plasma.to alone cannot satisfy a two-operator check.
1057
+ net(9745, "Plasma", "XPL", ["https://rpc.plasma.to", "https://plasma.gateway.tenderly.co"]),
1001
1058
  net(59144, "Linea", "ETH", ["https://rpc.linea.build", "https://linea-rpc.publicnode.com"], "https://lineascan.build"),
1002
1059
  net(100, "Gnosis", "xDAI", ["https://rpc.gnosischain.com", "https://gnosis-rpc.publicnode.com"], "https://gnosisscan.io"),
1003
1060
  net(146, "Sonic", "S", ["https://rpc.soniclabs.com", "https://sonic-rpc.publicnode.com"], "https://sonicscan.org"),
@@ -1015,7 +1072,11 @@ var DEFAULT_NETWORKS = Object.freeze([
1015
1072
  net(5e3, "Mantle", "MNT", ["https://rpc.mantle.xyz", "https://mantle-rpc.publicnode.com"], "https://mantlescan.xyz"),
1016
1073
  net(747474, "Katana", "ETH", ["https://rpc.katana.network"], "https://explorer.katanarpc.com"),
1017
1074
  net(2818, "Morph", "ETH", ["https://rpc.morphl2.io", "https://rpc-quicknode.morphl2.io"], "https://explorer.morphl2.io"),
1018
- net(14, "Flare", "FLR", ["https://flare-api.flare.network/ext/C/rpc", "https://flare.rpc.thirdweb.com"], "https://flare-explorer.flare.network")
1075
+ net(14, "Flare", "FLR", ["https://flare-api.flare.network/ext/C/rpc", "https://flare.rpc.thirdweb.com"], "https://flare-explorer.flare.network"),
1076
+ // Elysium by Kinetiq (chain 99901, HYPE gas; Arbitrum Orbit on HyperEVM) — public RPC and explorer checked 2026-11-10.
1077
+ // The explorer value is the link base (<root>/tx/<hash>, <root>/address/<a>), not LI.FI's blockExplorerUrls
1078
+ // https://elysium.kinetiq.xyz/explorer/ (the overview page; the bare root is the Elysium site home).
1079
+ net(99901, "Elysium", "HYPE", ["https://rpc.elysium.kinetiq.xyz"], "https://elysium.kinetiq.xyz")
1019
1080
  ]);
1020
1081
  var chainHex = (caip2) => `0x${Number(caip2.split(":")[1]).toString(16)}`;
1021
1082
  function caip2FromChainId(value) {
@@ -1091,6 +1152,17 @@ var NetworksTable = class {
1091
1152
  );
1092
1153
  return this._loading;
1093
1154
  }
1155
+ /**
1156
+ * Forget the cached table: the next read asks storage again. For a storage whose answer changes under
1157
+ * the table — e.g. one that only hands out the user's networks once the wallet is unlocked.
1158
+ */
1159
+ reload() {
1160
+ return this.serialize(async () => {
1161
+ await this._loading?.catch(() => void 0);
1162
+ this._custom = null;
1163
+ this._loading = null;
1164
+ });
1165
+ }
1094
1166
  };
1095
1167
  var isSiteRpcUrl = (u) => typeof u === "string" && u.length <= 2048 && (0, import_url.isPublicHttpsUrl)(u);
1096
1168
  function parseAddChainParams(params, known = isCatalogChain) {
@@ -1204,6 +1276,21 @@ var PermissionStore = class {
1204
1276
  return permission;
1205
1277
  });
1206
1278
  }
1279
+ /**
1280
+ * Add EVM accounts to an origin's grant (per-site grants): the new ones first, the ones it already
1281
+ * holds stay. The chain stays the origin's own when it has one. Used by the router's connect; `connect`
1282
+ * replaces the list instead.
1283
+ */
1284
+ grant(origin, accounts, chain) {
1285
+ return this.serialize(async () => {
1286
+ if (accounts.length === 0) throw new Error("Connect at least one account.");
1287
+ const current = await this.get(origin);
1288
+ const merged = [...accounts, ...(current?.accounts ?? []).filter((a) => !accounts.some((x) => x.toLowerCase() === a.toLowerCase()))];
1289
+ const permission = { ...current ?? {}, origin, accounts: merged, chain: current?.chain ?? chain, connectedAt: current?.connectedAt ?? Date.now() };
1290
+ await this.write(origin, permission);
1291
+ return permission;
1292
+ });
1293
+ }
1207
1294
  /** Connect another chain's accounts to the same origin (one "connected sites" list). */
1208
1295
  connectChain(origin, key, accounts, defaultChain) {
1209
1296
  return this.serialize(async () => {
@@ -1236,6 +1323,11 @@ var PermissionStore = class {
1236
1323
  });
1237
1324
  }
1238
1325
  /**
1326
+ * @deprecated Rewrites every origin's grant to the active account, which shows that account to sites
1327
+ * the user never connected it to (and links accounts across sites). Grants are per site now: the
1328
+ * router shows an origin its granted accounts that are also active, and connecting another account
1329
+ * adds it to that one origin ({@link grant}). Kept for compatibility; nothing in this package calls it.
1330
+ *
1239
1331
  * The active EVM account changed: every origin holding an EVM grant now sees `accounts` (MetaMask behaviour).
1240
1332
  * An origin connected only on another chain (`accounts: []` — Solana/TON/Tron/BTC/NEAR providers, a WalletConnect
1241
1333
  * session without eip155) is skipped: it never passed an EVM connect and must not learn the EVM address (scan F6/F8).
@@ -1281,6 +1373,179 @@ var PermissionStore = class {
1281
1373
  };
1282
1374
  var sameList = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
1283
1375
 
1376
+ // src/site.ts
1377
+ var MULTI_LABEL_SUFFIXES = /* @__PURE__ */ new Set([
1378
+ // country second-level registries
1379
+ "co.uk",
1380
+ "org.uk",
1381
+ "me.uk",
1382
+ "ltd.uk",
1383
+ "plc.uk",
1384
+ "net.uk",
1385
+ "ac.uk",
1386
+ "gov.uk",
1387
+ "com.au",
1388
+ "net.au",
1389
+ "org.au",
1390
+ "edu.au",
1391
+ "gov.au",
1392
+ "id.au",
1393
+ "co.nz",
1394
+ "org.nz",
1395
+ "net.nz",
1396
+ "co.jp",
1397
+ "ne.jp",
1398
+ "or.jp",
1399
+ "ac.jp",
1400
+ "go.jp",
1401
+ "co.kr",
1402
+ "or.kr",
1403
+ "ne.kr",
1404
+ "com.br",
1405
+ "net.br",
1406
+ "org.br",
1407
+ "com.cn",
1408
+ "net.cn",
1409
+ "org.cn",
1410
+ "com.hk",
1411
+ "com.tw",
1412
+ "com.sg",
1413
+ "com.my",
1414
+ "co.in",
1415
+ "net.in",
1416
+ "org.in",
1417
+ "co.id",
1418
+ "co.il",
1419
+ "co.th",
1420
+ "co.za",
1421
+ "com.mx",
1422
+ "com.ar",
1423
+ "com.co",
1424
+ "com.tr",
1425
+ "com.ua",
1426
+ "com.pl",
1427
+ "com.ru",
1428
+ "com.vn",
1429
+ "com.ph",
1430
+ "com.pk",
1431
+ "com.ng",
1432
+ "com.eg",
1433
+ "com.sa",
1434
+ "co.ke",
1435
+ // hosting platforms: every customer gets its own subdomain
1436
+ "github.io",
1437
+ "gitlab.io",
1438
+ "vercel.app",
1439
+ "now.sh",
1440
+ "pages.dev",
1441
+ "workers.dev",
1442
+ "r2.dev",
1443
+ "netlify.app",
1444
+ "netlify.com",
1445
+ "herokuapp.com",
1446
+ "web.app",
1447
+ "firebaseapp.com",
1448
+ "appspot.com",
1449
+ "azurewebsites.net",
1450
+ "azurestaticapps.net",
1451
+ "cloudfront.net",
1452
+ "amplifyapp.com",
1453
+ "onrender.com",
1454
+ "fly.dev",
1455
+ "railway.app",
1456
+ "up.railway.app",
1457
+ "deno.dev",
1458
+ "glitch.me",
1459
+ "repl.co",
1460
+ "replit.app",
1461
+ "replit.dev",
1462
+ "surge.sh",
1463
+ "webflow.io",
1464
+ "framer.app",
1465
+ "framer.website",
1466
+ "wixsite.com",
1467
+ "blogspot.com",
1468
+ "myshopify.com",
1469
+ "notion.site",
1470
+ "carrd.co",
1471
+ "gitbook.io",
1472
+ "readthedocs.io",
1473
+ "ngrok.io",
1474
+ "ngrok-free.app",
1475
+ "loca.lt",
1476
+ "trycloudflare.com",
1477
+ // ENS / IPFS gateways: every name or CID is its own site
1478
+ "eth.limo",
1479
+ "eth.link",
1480
+ "ipfs.dweb.link",
1481
+ "ipns.dweb.link",
1482
+ "ipfs.w3s.link",
1483
+ "ipfs.cf-ipfs.com"
1484
+ ]);
1485
+ var IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;
1486
+ function siteOf(origin) {
1487
+ let host;
1488
+ try {
1489
+ const url = new URL(origin);
1490
+ if (url.protocol !== "http:" && url.protocol !== "https:") return origin;
1491
+ host = url.hostname.toLowerCase().replace(/\.$/, "");
1492
+ } catch {
1493
+ return origin;
1494
+ }
1495
+ if (!host || host.startsWith("[") || host.includes(":") || IPV4.test(host)) return host || origin;
1496
+ const labels = host.split(".");
1497
+ if (labels.length <= 2) return host;
1498
+ for (const size of [3, 2]) {
1499
+ if (labels.length <= size) continue;
1500
+ if (MULTI_LABEL_SUFFIXES.has(labels.slice(-size).join("."))) return labels.slice(-(size + 1)).join(".");
1501
+ }
1502
+ return labels.slice(-2).join(".");
1503
+ }
1504
+ var TokenBucket = class {
1505
+ constructor(options) {
1506
+ this.options = options;
1507
+ }
1508
+ options;
1509
+ buckets = /* @__PURE__ */ new Map();
1510
+ take(key, cost = 1) {
1511
+ const now = (this.options.now ?? Date.now)();
1512
+ const b = this.refill(key, now);
1513
+ if (b.tokens < cost) return false;
1514
+ b.tokens -= cost;
1515
+ return true;
1516
+ }
1517
+ /** Tokens `key` holds right now (full for an unknown key). */
1518
+ available(key) {
1519
+ return this.refill(key, (this.options.now ?? Date.now)()).tokens;
1520
+ }
1521
+ refill(key, now) {
1522
+ let b = this.buckets.get(key);
1523
+ if (!b) {
1524
+ if (this.buckets.size >= (this.options.maxKeys ?? 2e3)) this.prune(now);
1525
+ b = { tokens: this.options.capacity, at: now };
1526
+ this.buckets.set(key, b);
1527
+ return b;
1528
+ }
1529
+ const elapsed = Math.max(0, now - b.at) / 1e3;
1530
+ b.tokens = Math.min(this.options.capacity, b.tokens + elapsed * this.options.refillPerSecond);
1531
+ b.at = now;
1532
+ return b;
1533
+ }
1534
+ /** Drop buckets that are full again (they carry no state); the oldest half when every one is in use. */
1535
+ prune(now) {
1536
+ for (const [k, b] of this.buckets) {
1537
+ if (b.tokens + Math.max(0, now - b.at) / 1e3 * this.options.refillPerSecond >= this.options.capacity) this.buckets.delete(k);
1538
+ }
1539
+ if (this.buckets.size >= (this.options.maxKeys ?? 2e3)) {
1540
+ let drop = Math.ceil(this.buckets.size / 2);
1541
+ for (const k of this.buckets.keys()) {
1542
+ if (drop-- <= 0) break;
1543
+ this.buckets.delete(k);
1544
+ }
1545
+ }
1546
+ }
1547
+ };
1548
+
1284
1549
  // src/approvals.ts
1285
1550
  var ReviewDeclined = class extends Error {
1286
1551
  code = RELAY_ERRORS.userRejected;
@@ -1305,7 +1570,11 @@ var OriginRefused = class extends Error {
1305
1570
  }
1306
1571
  };
1307
1572
  var APPROVALS_MAX_PENDING_PER_ORIGIN = 3;
1308
- var ApprovalQueue = class {
1573
+ var APPROVALS_MAX_PENDING_PER_SITE = 5;
1574
+ var APPROVALS_MAX_PENDING_TOTAL = 20;
1575
+ var FOLLOW_UP_CREDITS_MAX = 8;
1576
+ var FOLLOW_UP_KEYS_MAX = 256;
1577
+ var ApprovalQueue = class _ApprovalQueue {
1309
1578
  constructor(window, newId = () => crypto.randomUUID(), options = {}) {
1310
1579
  this.window = window;
1311
1580
  this.newId = newId;
@@ -1313,6 +1582,8 @@ var ApprovalQueue = class {
1313
1582
  window.onClosed((id) => {
1314
1583
  if (this._current?.request.id === id) this.decline(id);
1315
1584
  });
1585
+ const rate = options.openRate === false ? null : options.openRate ?? {};
1586
+ this._rate = rate ? new TokenBucket({ capacity: rate.burst ?? 10, refillPerSecond: rate.refillPerSecond ?? 1 / 6, ...options.now ? { now: options.now } : {} }) : null;
1316
1587
  }
1317
1588
  window;
1318
1589
  newId;
@@ -1321,8 +1592,62 @@ var ApprovalQueue = class {
1321
1592
  _waiting = [];
1322
1593
  _listeners = /* @__PURE__ */ new Set();
1323
1594
  _via = /* @__PURE__ */ new Map();
1595
+ _rate;
1596
+ /** Per site: cooldown end and strikes (how many times the bucket ran dry recently). */
1597
+ _cooldown = /* @__PURE__ */ new Map();
1598
+ /** Per origin + kind + chain: approvals that have not been followed up (see {@link ApprovalRequestOptions.followUp}). */
1599
+ _approved = /* @__PURE__ */ new Map();
1600
+ site(origin) {
1601
+ try {
1602
+ return (this.options.siteOf ?? siteOf)(origin);
1603
+ } catch {
1604
+ return origin;
1605
+ }
1606
+ }
1607
+ get all() {
1608
+ return [...this._current ? [this._current] : [], ...this._waiting];
1609
+ }
1610
+ /** The per-site rate: a reason to refuse, or null (and one token spent). */
1611
+ rateRefusal(site) {
1612
+ if (!this._rate) return null;
1613
+ const rate = this.options.openRate || {};
1614
+ const now = (this.options.now ?? Date.now)();
1615
+ const max = rate.maxBackoffMs ?? 6e5;
1616
+ const c = this._cooldown.get(site);
1617
+ if (c && now < c.until) return `This site opened too many requests; try again in ${Math.ceil((c.until - now) / 1e3)} s.`;
1618
+ if (this._rate.take(site)) return null;
1619
+ const strikes = c && now - c.last < max ? c.strikes + 1 : 1;
1620
+ const wait = Math.min(max, (rate.backoffMs ?? 3e4) * 2 ** (strikes - 1));
1621
+ this._cooldown.set(site, { until: now + wait, strikes, last: now });
1622
+ if (this._cooldown.size > 1e3) {
1623
+ for (const [k, v] of this._cooldown) if (now - v.last >= max) this._cooldown.delete(k);
1624
+ }
1625
+ return `This site opened too many requests; try again in ${Math.ceil(wait / 1e3)} s.`;
1626
+ }
1627
+ static followUpKey(r) {
1628
+ return `${r.origin}
1629
+ ${r.kind}
1630
+ ${r.chain}`;
1631
+ }
1632
+ /** The user approved `r`: it may be followed up once. */
1633
+ approved(r) {
1634
+ const key = _ApprovalQueue.followUpKey(r);
1635
+ const n = Math.min((this._approved.get(key) ?? 0) + 1, FOLLOW_UP_CREDITS_MAX);
1636
+ this._approved.delete(key);
1637
+ this._approved.set(key, n);
1638
+ if (this._approved.size > FOLLOW_UP_KEYS_MAX) this._approved.delete(this._approved.keys().next().value);
1639
+ }
1640
+ /** Spends one approval of the same origin, kind and chain; false when there is none. */
1641
+ spendApproval(r) {
1642
+ const key = _ApprovalQueue.followUpKey(r);
1643
+ const n = this._approved.get(key) ?? 0;
1644
+ if (n <= 0) return false;
1645
+ if (n === 1) this._approved.delete(key);
1646
+ else this._approved.set(key, n - 1);
1647
+ return true;
1648
+ }
1324
1649
  /** Enqueue and wait for the user's answer. Refuses (never queues) past the per-origin limits. */
1325
- request(input) {
1650
+ request(input, opts = {}) {
1326
1651
  let refused;
1327
1652
  try {
1328
1653
  refused = this.options.refuse?.(input.origin);
@@ -1330,19 +1655,41 @@ var ApprovalQueue = class {
1330
1655
  refused = `The wallet could not check this site (${e instanceof Error ? e.message : String(e)}).`;
1331
1656
  }
1332
1657
  if (refused) return Promise.reject(new OriginRefused(refused));
1333
- const sameOrigin = [...this._current ? [this._current] : [], ...this._waiting].filter((p) => p.request.origin === input.origin);
1334
- if (input.kind === "connect" && sameOrigin.some((p) => p.request.kind === "connect")) {
1335
- return Promise.reject(new QueueLimitError(RELAY_ERRORS.resourceUnavailable, "A connect request from this site is already waiting for your answer."));
1658
+ let exempt = false;
1659
+ try {
1660
+ exempt = this.options.exempt?.(input.origin) === true;
1661
+ } catch {
1662
+ exempt = false;
1336
1663
  }
1337
- const max = this.options.maxPendingPerOrigin ?? APPROVALS_MAX_PENDING_PER_ORIGIN;
1338
- if (sameOrigin.length >= max) {
1339
- return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, `This site already has ${max} requests waiting for you; answer or close them first.`));
1664
+ const followUp = opts.followUp === true && this.spendApproval(input);
1665
+ if (!exempt && !followUp) {
1666
+ const all = this.all;
1667
+ const sameOrigin = all.filter((p) => p.request.origin === input.origin);
1668
+ if (input.kind === "connect" && sameOrigin.some((p) => p.request.kind === "connect")) {
1669
+ return Promise.reject(new QueueLimitError(RELAY_ERRORS.resourceUnavailable, "A connect request from this site is already waiting for your answer."));
1670
+ }
1671
+ const max = this.options.maxPendingPerOrigin ?? APPROVALS_MAX_PENDING_PER_ORIGIN;
1672
+ if (sameOrigin.length >= max) {
1673
+ return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, `This site already has ${max} requests waiting for you; answer or close them first.`));
1674
+ }
1675
+ const site = this.site(input.origin);
1676
+ const maxSite = this.options.maxPendingPerSite ?? APPROVALS_MAX_PENDING_PER_SITE;
1677
+ if (all.filter((p) => this.site(p.request.origin) === site).length >= maxSite) {
1678
+ return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, `This site already has ${maxSite} requests waiting for you across its subdomains; answer or close them first.`));
1679
+ }
1680
+ const maxTotal = this.options.maxPendingTotal ?? APPROVALS_MAX_PENDING_TOTAL;
1681
+ if (all.filter((p) => !this.isExempt(p.request.origin)).length >= maxTotal) {
1682
+ return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, `The wallet already has ${maxTotal} requests waiting for you; answer or close them first.`));
1683
+ }
1684
+ const slow = this.rateRefusal(site);
1685
+ if (slow) return Promise.reject(new QueueLimitError(RELAY_ERRORS.limitExceeded, slow));
1340
1686
  }
1341
1687
  return new Promise((resolve, reject) => {
1342
1688
  const warnings = this.options.annotate?.(input.origin) ?? [];
1343
1689
  const via = this._via.get(input.origin);
1344
1690
  const pending = { request: { ...input, id: this.newId(), createdAt: Date.now(), ...warnings.length ? { originWarnings: warnings } : {}, ...via ? { via } : {} }, resolve, reject };
1345
- this._waiting.push(pending);
1691
+ if (followUp) this._waiting.unshift(pending);
1692
+ else this._waiting.push(pending);
1346
1693
  void this.next();
1347
1694
  });
1348
1695
  }
@@ -1355,9 +1702,37 @@ var ApprovalQueue = class {
1355
1702
  viaFor(origin) {
1356
1703
  return this._via.get(origin);
1357
1704
  }
1705
+ isExempt(origin) {
1706
+ try {
1707
+ return this.options.exempt?.(origin) === true;
1708
+ } catch {
1709
+ return false;
1710
+ }
1711
+ }
1358
1712
  /** Requests waiting for `origin` (current + queued). */
1359
1713
  pendingFor(origin) {
1360
- return [...this._current ? [this._current] : [], ...this._waiting].filter((p) => p.request.origin === origin).length;
1714
+ return this.all.filter((p) => p.request.origin === origin).length;
1715
+ }
1716
+ /** Requests waiting for any origin of `site` (current + queued). */
1717
+ pendingForSite(site) {
1718
+ return this.all.filter((p) => this.site(p.request.origin) === site).length;
1719
+ }
1720
+ /** The site (registrable domain) the queue's limits put `origin` under. */
1721
+ siteFor(origin) {
1722
+ return this.site(origin);
1723
+ }
1724
+ /** Decline everything waiting for `origin` (its last page closed). Returns how many requests were declined. */
1725
+ declineOrigin(origin, error = new ReviewDeclined("The page was closed \u2014 nothing was signed.")) {
1726
+ return this.declineWhere((p) => p.request.origin === origin, error);
1727
+ }
1728
+ /** "Decline all from this site": everything waiting for any origin of `site` (see {@link siteFor}). */
1729
+ declineSite(site, error = new ReviewDeclined()) {
1730
+ return this.declineWhere((p) => this.site(p.request.origin) === site, error);
1731
+ }
1732
+ declineWhere(match, error) {
1733
+ const ids = [...this._waiting.filter(match), ...this._current && match(this._current) ? [this._current] : []].map((p) => p.request.id);
1734
+ for (const id of ids) this.decline(id, error);
1735
+ return ids.length;
1361
1736
  }
1362
1737
  /** The request currently shown (the window asks for it by id on load). */
1363
1738
  get current() {
@@ -1367,7 +1742,10 @@ var ApprovalQueue = class {
1367
1742
  return this._waiting.length;
1368
1743
  }
1369
1744
  resolve(requestId, value) {
1370
- return this.settle(requestId, (p) => p.resolve(value));
1745
+ return this.settle(requestId, (p) => {
1746
+ this.approved(p.request);
1747
+ p.resolve(value);
1748
+ });
1371
1749
  }
1372
1750
  decline(requestId, error = new ReviewDeclined()) {
1373
1751
  return this.settle(requestId, (p) => p.reject(error));
@@ -1375,6 +1753,7 @@ var ApprovalQueue = class {
1375
1753
  /** Lock, disconnect, shutdown: everything pending is declined. */
1376
1754
  declineAll(error = new ReviewDeclined("The wallet was locked.")) {
1377
1755
  const all = [...this._current ? [this._current] : [], ...this._waiting.splice(0)];
1756
+ this._approved.clear();
1378
1757
  for (const p of all) p.reject(error);
1379
1758
  if (this._current) void this.window.close(this._current.request.id);
1380
1759
  this._current = null;
@@ -1506,6 +1885,7 @@ var READ_METHODS = /* @__PURE__ */ new Set([
1506
1885
  "eth_getTransactionReceipt",
1507
1886
  "eth_maxPriorityFeePerGas"
1508
1887
  ]);
1888
+ var GETLOGS_MAX_SPAN_UNCONNECTED = 1e3;
1509
1889
  var SIGNING_METHODS = /* @__PURE__ */ new Set(["personal_sign", "eth_signTypedData_v4", "eth_signTypedData_v3", "eth_signTypedData", "eth_sendTransaction"]);
1510
1890
  var RequestRouter = class {
1511
1891
  constructor(deps) {
@@ -1535,8 +1915,10 @@ var RequestRouter = class {
1535
1915
  return this.visibleAccounts(permission);
1536
1916
  case "eth_requestAccounts":
1537
1917
  return this.connect(origin, network);
1538
- case "wallet_getPermissions":
1539
- return permission && permission.accounts.length > 0 ? [permissionDescriptor(origin, permission.accounts)] : [];
1918
+ case "wallet_getPermissions": {
1919
+ const visible = await this.visibleAccounts(permission);
1920
+ return visible.length > 0 ? [permissionDescriptor(origin, visible)] : [];
1921
+ }
1540
1922
  case "wallet_requestPermissions": {
1541
1923
  const accounts = await this.connect(origin, network);
1542
1924
  return [permissionDescriptor(origin, accounts)];
@@ -1577,11 +1959,20 @@ var RequestRouter = class {
1577
1959
  case "eth_signTransaction":
1578
1960
  throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${method} (blind signing) is never offered by this wallet.`);
1579
1961
  }
1580
- if (READ_METHODS.has(method)) return this.deps.rpc(network, method, params);
1962
+ if (READ_METHODS.has(method)) {
1963
+ if (method === "eth_getLogs" && !(permission && permission.accounts.length > 0)) await this.boundLogs(network, params);
1964
+ return this.deps.rpc(network, method, params);
1965
+ }
1581
1966
  if (SIGNING_METHODS.has(method)) {
1582
1967
  if (!this.deps.sign) throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${method} is not available yet.`);
1583
- if (!permission) throw new RouterError(RELAY_ERRORS.unauthorized, "Connect the wallet first (eth_requestAccounts).");
1584
- return this.deps.sign({ origin, network, method, params, accounts: permission.accounts });
1968
+ if (!permission || permission.accounts.length === 0) throw new RouterError(RELAY_ERRORS.unauthorized, "Connect the wallet first (eth_requestAccounts).");
1969
+ const visible = await this.visibleAccounts(permission);
1970
+ const named = namedAccount(method, params);
1971
+ const inactive = named !== void 0 && permission.accounts.some((a) => a.toLowerCase() === named.toLowerCase()) && !visible.some((a) => a.toLowerCase() === named.toLowerCase());
1972
+ if (visible.length === 0 || inactive) {
1973
+ throw new RouterError(RELAY_ERRORS.unauthorized, "The account this site asks for is not the wallet's active account (or the wallet is locked). Switch to it in the wallet, or connect the active account (eth_requestAccounts).");
1974
+ }
1975
+ return this.deps.sign({ origin, network, method, params, accounts: visible });
1585
1976
  }
1586
1977
  throw new RouterError(RELAY_ERRORS.unsupportedMethod, `This wallet does not support ${method}.`);
1587
1978
  }
@@ -1591,9 +1982,31 @@ var RequestRouter = class {
1591
1982
  const available = await this.deps.accounts();
1592
1983
  return permission.accounts.filter((a) => available.includes(a));
1593
1984
  }
1985
+ /** An unconnected origin's `eth_getLogs`: a block hash, or a range of at most {@link GETLOGS_MAX_SPAN_UNCONNECTED} blocks. */
1986
+ async boundLogs(network, params) {
1987
+ const filter = params[0] ?? {};
1988
+ if (typeof filter !== "object" || filter === null) throw new RouterError(RELAY_ERRORS.invalidParams, "eth_getLogs expects a filter object.");
1989
+ if (filter.blockHash !== void 0 && filter.blockHash !== null) return;
1990
+ let head;
1991
+ const block = async (tag) => {
1992
+ if (tag === void 0 || tag === null || tag === "latest" || tag === "pending" || tag === "safe" || tag === "finalized") {
1993
+ head ??= BigInt(String(await this.deps.rpc(network, "eth_blockNumber", [])));
1994
+ return head;
1995
+ }
1996
+ if (tag === "earliest") return 0n;
1997
+ if (typeof tag === "string" && /^0x[0-9a-fA-F]{1,16}$/.test(tag)) return BigInt(tag);
1998
+ throw new RouterError(RELAY_ERRORS.invalidParams, "eth_getLogs: fromBlock / toBlock must be a block tag or a hex block number.");
1999
+ };
2000
+ const from = await block(filter.fromBlock);
2001
+ const to = await block(filter.toBlock);
2002
+ if (to - from > BigInt(GETLOGS_MAX_SPAN_UNCONNECTED)) {
2003
+ throw new RouterError(RELAY_ERRORS.limitExceeded, `eth_getLogs over more than ${GETLOGS_MAX_SPAN_UNCONNECTED} blocks is answered for connected sites only; narrow the range or connect the wallet.`);
2004
+ }
2005
+ }
1594
2006
  /**
1595
2007
  * Connected and unlocked → the accounts; otherwise the approval window (which
1596
- * unlocks first when needed) picks them. Emits `connect` + `accountsChanged`.
2008
+ * unlocks first when needed) picks them, and they are ADDED to this origin's
2009
+ * grant (other origins are untouched). Emits `connect` + `accountsChanged`.
1597
2010
  */
1598
2011
  async connect(origin, network) {
1599
2012
  const existing = await this.visibleAccounts(await this.deps.permissions.get(origin));
@@ -1602,7 +2015,7 @@ var RequestRouter = class {
1602
2015
  const available = await this.deps.accounts();
1603
2016
  const accounts = Array.isArray(chosen) ? chosen.filter((a) => typeof a === "string" && available.includes(a)) : [];
1604
2017
  if (accounts.length === 0) throw new ReviewDeclined("No account was selected.");
1605
- await this.deps.permissions.connect(origin, accounts, network.caip2);
2018
+ await this.deps.permissions.grant(origin, accounts, network.caip2);
1606
2019
  this.deps.emit(origin, "connect", { chainId: chainHex(network.caip2) });
1607
2020
  this.deps.emit(origin, "accountsChanged", accounts);
1608
2021
  return accounts;
@@ -1622,6 +2035,12 @@ var RequestRouter = class {
1622
2035
  this.deps.emit(origin, "chainChanged", chainHex(to.caip2));
1623
2036
  }
1624
2037
  };
2038
+ function namedAccount(method, params) {
2039
+ const addr = (x) => typeof x === "string" && /^0x[0-9a-fA-F]{40}$/.test(x) ? x : void 0;
2040
+ if (method === "personal_sign") return addr(params[1]) ?? addr(params[0]);
2041
+ if (method === "eth_sendTransaction") return addr(params[0]?.from);
2042
+ return addr(params[0]);
2043
+ }
1625
2044
  var permissionDescriptor = (origin, accounts) => ({
1626
2045
  id: crypto.randomUUID(),
1627
2046
  parentCapability: "eth_accounts",
@@ -1631,6 +2050,7 @@ var permissionDescriptor = (origin, accounts) => ({
1631
2050
  });
1632
2051
 
1633
2052
  // src/siwe.ts
2053
+ var SIWE_FORBIDDEN_CHARS = /[\u061c\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff]/;
1634
2054
  var HEADER_LINE = /^(?<domain>[^\n]+?) wants you to sign in with your Ethereum account:(?:\n|$)/;
1635
2055
  var ADDRESS_LINE = /^0x[0-9a-fA-F]{40}$/;
1636
2056
  var FIELD = /^([A-Za-z ]+): (.*)$/;
@@ -1734,12 +2154,20 @@ function siweProblems(m, ctx) {
1734
2154
  }
1735
2155
  return out;
1736
2156
  }
2157
+ function siweCrossSite(m, ctx) {
2158
+ const site = originWhere(ctx.origin) ?? (ctx.via?.verify === "VALID" && !ctx.via.scam ? whereOf(ctx.via.url) : null);
2159
+ if (!site) return null;
2160
+ const domain = whereOf(m.domain);
2161
+ return !domain || domain.host !== site.host ? { domain: m.domain, origin: `${site.scheme}://${site.host}` } : null;
2162
+ }
1737
2163
  function describeSiwe(text, ctx) {
1738
2164
  const p = parseLoose(text);
1739
2165
  if (!p) return null;
1740
2166
  const problems = siweProblems(p.message, ctx);
1741
- if (p.missing.length === 0) return { message: p.message, problems };
1742
- return { message: p.message, problems: [`This is a malformed sign-in message (EIP-4361): ${p.missing.join(", ")} missing or unreadable \u2014 check the site before signing.`, ...problems], malformed: true };
2167
+ const cross = siweCrossSite(p.message, ctx);
2168
+ const crossSite = cross ? { crossSite: cross } : {};
2169
+ if (p.missing.length === 0) return { message: p.message, problems, ...crossSite };
2170
+ return { message: p.message, problems: [`This is a malformed sign-in message (EIP-4361): ${p.missing.join(", ")} missing or unreadable \u2014 check the site before signing.`, ...problems], malformed: true, ...crossSite };
1743
2171
  }
1744
2172
 
1745
2173
  // src/signing.ts
@@ -1747,18 +2175,31 @@ var safeNonce = (n) => {
1747
2175
  if (n < 0n || n > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`The RPC returned an impossible nonce (${n}).`);
1748
2176
  return Number(n.toString());
1749
2177
  };
2178
+ var MAX_FEE_REASKS = 3;
1750
2179
  var ADDRESS = /^0x[0-9a-fA-F]{40}$/;
1751
2180
  var HEX = /^0x([0-9a-fA-F]{2})*$/;
1752
2181
  var lc = (s) => s.toLowerCase();
2182
+ var INVISIBLE = /[\p{Cf}\u2028\u2029\u115f\u1160\u3164\uffa0\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/gu;
2183
+ function revealHidden(text) {
2184
+ let hidden = false;
2185
+ const out = text.replace(INVISIBLE, (c) => {
2186
+ hidden = true;
2187
+ return `\u27E8U+${c.codePointAt(0).toString(16).toUpperCase().padStart(4, "0")}\u27E9`;
2188
+ });
2189
+ return { text: out, hidden };
2190
+ }
1753
2191
  function messageText(value) {
1754
- if (typeof value !== "string") return "";
1755
- if (!HEX.test(value)) return value;
2192
+ return shownMessage(value).text;
2193
+ }
2194
+ function shownMessage(value) {
2195
+ if (typeof value !== "string") return { text: "", hidden: false };
2196
+ if (!HEX.test(value)) return revealHidden(value);
1756
2197
  try {
1757
2198
  const bytes = Uint8Array.from(value.slice(2).match(/../g) ?? [], (b) => Number.parseInt(b, 16));
1758
2199
  const s = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1759
- return /^[\x09\x0a\x0d\x20-\x7e -￿]*$/.test(s) ? s : value;
2200
+ return /^[\x09\x0a\x0d\x20-\x7e\u00a0-\uffff]*$/.test(s) ? revealHidden(s) : { text: value, hidden: false };
1760
2201
  } catch {
1761
- return value;
2202
+ return { text: value, hidden: false };
1762
2203
  }
1763
2204
  }
1764
2205
  function decodedMessage(value) {
@@ -1772,6 +2213,7 @@ function decodedMessage(value) {
1772
2213
  }
1773
2214
  var HIDDEN_CONTROL = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f]/;
1774
2215
  var err = (code, message) => ({ code, message });
2216
+ var BLIND_HASH = /^0x[0-9a-fA-F]{64}$/;
1775
2217
  function classifySignRequest(input) {
1776
2218
  const { method, params } = input;
1777
2219
  const owned = new Set(input.accounts.map(lc));
@@ -1782,11 +2224,15 @@ function classifySignRequest(input) {
1782
2224
  if (ours(msg) && !ours(addr)) [msg, addr] = [addr, msg];
1783
2225
  if (!ours(addr)) return err(RELAY_ERRORS.unauthorized, "personal_sign for an address this wallet does not hold.");
1784
2226
  if (typeof msg !== "string") return err(RELAY_ERRORS.invalidParams, "personal_sign expects a message string.");
1785
- const text = messageText(msg);
2227
+ const blindHash = BLIND_HASH.test(msg);
2228
+ if (blindHash && !input.allowBlindHash) return err(RELAY_ERRORS.unsupportedMethod, 'This site asks you to sign a raw 32-byte hash (blind signing). It can be a swap order or a transfer (e.g. CoW Protocol) paying someone else, so this wallet refuses it. "Allow blind hash signatures" in Options turns on a review for such requests.');
2229
+ const shown = shownMessage(msg);
2230
+ const text = shown.text;
1786
2231
  const signed = decodedMessage(msg) ?? text;
1787
2232
  const siwe = input.origin ? describeSiwe(signed, { origin: input.origin, account: checksum(addr), chainId: input.network.chainId, ...input.via ? { via: input.via } : {}, ...input.now !== void 0 ? { now: input.now } : {} }) : null;
1788
2233
  if (siwe && HIDDEN_CONTROL.test(signed)) return err(RELAY_ERRORS.invalidParams, "This sign-in message contains hidden control characters \u2014 refused.");
1789
- return { kind: "sign-message", method, account: checksum(addr), text, raw: msg, ...siwe ? { siwe } : {} };
2234
+ if (siwe && SIWE_FORBIDDEN_CHARS.test(signed)) return err(RELAY_ERRORS.invalidParams, "This sign-in message contains invisible or text-direction characters \u2014 refused.");
2235
+ return { kind: "sign-message", method, account: checksum(addr), text, raw: msg, ...siwe ? { siwe } : {}, ...shown.hidden ? { hiddenChars: true } : {}, ...blindHash ? { blindHash: true } : {} };
1790
2236
  }
1791
2237
  if (method === "eth_signTypedData_v4" || method === "eth_signTypedData_v3" || method === "eth_signTypedData") {
1792
2238
  const [addr, data] = params;
@@ -1804,7 +2250,8 @@ function classifySignRequest(input) {
1804
2250
  if (cid !== void 0 && cid !== null && Number(cid) !== input.network.chainId) {
1805
2251
  return err(RELAY_ERRORS.chainDisconnected, `Typed data is for chain ${String(cid)}; this site is on ${input.network.name} (${input.network.chainId}).`);
1806
2252
  }
1807
- return { kind: "sign-typed-data", method, account: checksum(addr), text: JSON.stringify(parsed, bigintSafe, 2), typedData: parsed };
2253
+ const shown = revealHidden(JSON.stringify(parsed, bigintSafe, 2));
2254
+ return { kind: "sign-typed-data", method, account: checksum(addr), text: shown.text, typedData: parsed, ...shown.hidden ? { hiddenChars: true } : {} };
1808
2255
  }
1809
2256
  if (method === "eth_sendTransaction") {
1810
2257
  const tx = params[0] ?? {};
@@ -1853,16 +2300,33 @@ function overpriced(shown, fresh) {
1853
2300
  if (shown.maxFeePerGas !== void 0 && fresh.maxFeePerGas !== void 0) return big(shown.maxFeePerGas) > STALE_FEE_FACTOR * big(fresh.maxFeePerGas);
1854
2301
  return false;
1855
2302
  }
2303
+ function gasLimitExcessive(limit, estimate) {
2304
+ const a = estimate * 3n / 2n;
2305
+ const b = estimate + 50000n;
2306
+ return limit > (a > b ? a : b);
2307
+ }
1856
2308
  async function prepareTransaction(deps, network, from, tx) {
1857
2309
  const call = { from, to: tx.to, value: hex(BigInt(tx.value)), ...tx.data ? { data: tx.data } : {} };
2310
+ const estimate = () => deps.rpc(network, "eth_estimateGas", [call]);
1858
2311
  const [nonceHex, block, gasHex] = await Promise.all([
1859
2312
  deps.rpc(network, "eth_getTransactionCount", [from, "pending"]),
1860
2313
  deps.rpc(network, "eth_getBlockByNumber", ["latest", false]),
1861
- tx.gas ? Promise.resolve(tx.gas) : deps.rpc(network, "eth_estimateGas", [call])
2314
+ tx.gas ? estimate().then((v) => v, () => null) : estimate()
1862
2315
  ]);
1863
- const estimated = big(gasHex);
1864
- const gasLimit = tx.gas ? estimated : (estimated * 120n + 99n) / 100n;
1865
- return withFees(network.chainId, safeNonce(big(nonceHex)), gasLimit, await readFees(deps, network, block));
2316
+ const nonce = safeNonce(big(nonceHex));
2317
+ if (!tx.gas) {
2318
+ const estimated = big(gasHex);
2319
+ return withFees(network.chainId, nonce, (estimated * 120n + 99n) / 100n, await readFees(deps, network, block));
2320
+ }
2321
+ const limit = big(tx.gas);
2322
+ const prepared = withFees(network.chainId, nonce, limit, await readFees(deps, network, block));
2323
+ let est = null;
2324
+ try {
2325
+ est = typeof gasHex === "string" ? big(gasHex) : null;
2326
+ } catch {
2327
+ est = null;
2328
+ }
2329
+ return est !== null && est > 0n ? { ...prepared, gasCheck: { estimate: hex(est), excessive: gasLimitExcessive(limit, est) } } : prepared;
1866
2330
  }
1867
2331
  function createSignHandler(deps) {
1868
2332
  const tails = /* @__PURE__ */ new Map();
@@ -1878,7 +2342,8 @@ function createSignHandler(deps) {
1878
2342
  };
1879
2343
  return async ({ origin, network, method, params, accounts }) => {
1880
2344
  const via = deps.approvals.viaFor(origin);
1881
- const info = classifySignRequest({ network, method, params, accounts, origin, ...via ? { via } : {} });
2345
+ const allowBlindHash = method === "personal_sign" && deps.allowBlindHash ? await deps.allowBlindHash() : false;
2346
+ const info = classifySignRequest({ network, method, params, accounts, origin, ...via ? { via } : {}, ...allowBlindHash ? { allowBlindHash: true } : {} });
1882
2347
  if ("code" in info) throw RouterError.from(info);
1883
2348
  const prepared = info.kind === "send-transaction" ? await prepareTransaction(deps, network, info.account, info.tx) : void 0;
1884
2349
  const preview = deps.preview ? await deps.preview({ network, request: info, accounts }).catch(() => null) : null;
@@ -1888,14 +2353,20 @@ function createSignHandler(deps) {
1888
2353
  const key = `${network.caip2}|${lc(info.account)}`;
1889
2354
  return serial(key, async () => {
1890
2355
  let shown = prepared;
2356
+ let reasks = 0;
1891
2357
  for (; ; ) {
1892
2358
  const [nonceHex, fees] = await Promise.all([deps.rpc(network, "eth_getTransactionCount", [info.account, "pending"]), readFees(deps, network)]);
1893
2359
  const local = nextNonce.get(key) ?? 0;
1894
2360
  const nodeNonce = safeNonce(big(nonceHex));
1895
2361
  const nonce = nodeNonce > local ? nodeNonce : local;
1896
2362
  if (outgrown(shown, fees)) {
1897
- shown = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
1898
- await deps.approvals.request({ origin, chain: network.caip2, kind: info.kind, payload: { ...payload, prepared: shown, repriced: true } });
2363
+ const next = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
2364
+ const ceiling = deps.feeCeiling?.(network);
2365
+ if (ceiling !== void 0 && BigInt(next.maxFee) > ceiling) throw new RouterError(RELAY_ERRORS.internal, `The network fee rose above the wallet's limit for ${network.name}; nothing was signed.`);
2366
+ if (reasks >= MAX_FEE_REASKS) throw new RouterError(RELAY_ERRORS.internal, "The network fee kept rising while you were reviewing; nothing was signed. Try again later.");
2367
+ reasks++;
2368
+ await deps.approvals.request({ origin, chain: network.caip2, kind: info.kind, payload: { ...payload, prepared: next, repriced: true, repricedFrom: shown.maxFee } }, { followUp: true });
2369
+ shown = next;
1899
2370
  continue;
1900
2371
  }
1901
2372
  const fresh = withFees(network.chainId, nonce, big(shown.gasLimit), fees);
@@ -2017,7 +2488,14 @@ async function quoteRoutes(providers, params, options = {}) {
2017
2488
  candidates.push(p);
2018
2489
  }
2019
2490
  const pending = new Set(candidates.map((p) => p.name));
2020
- const view = () => ({ routes: rank(Object.values(quotes)), pending: [...pending], refusals: { ...refusals }, quotes: { ...quotes } });
2491
+ const depositAddressRoute = options.depositAddressRoute;
2492
+ const rankOptions = {
2493
+ ...params.slippageBps !== void 0 ? { slippageBps: params.slippageBps } : {},
2494
+ ...options.depositAddressProviders ? { depositAddressProviders: options.depositAddressProviders } : {},
2495
+ ...depositAddressRoute ? { depositAddressRoute: (q) => depositAddressRoute(q, params) } : {}
2496
+ };
2497
+ const { slippageBps: _slippage, ...shapeParams } = params;
2498
+ const view = () => ({ routes: rank(Object.values(quotes), rankOptions), pending: [...pending], refusals: { ...refusals }, quotes: { ...quotes } });
2021
2499
  const emit = () => options.onUpdate?.(view());
2022
2500
  emit();
2023
2501
  await Promise.all(
@@ -2032,7 +2510,7 @@ async function quoteRoutes(providers, params, options = {}) {
2032
2510
  const q = await Promise.race([p.getQuote({ ...params, dry: true, onRefusal: (r) => {
2033
2511
  refusal = r;
2034
2512
  } }, ctrl.signal), aborted]);
2035
- if (q && (0, import_protocol_swap2_core.validQuote)(q, { ...params, dry: true }, p.name)) quotes[p.name] = q;
2513
+ if (q && (0, import_protocol_swap2_core.validQuote)(q, { ...shapeParams, dry: true }, p.name)) quotes[p.name] = q;
2036
2514
  else refusals[p.name] = refusal ?? (q ? "returned an invalid quote" : "no route for this pair right now");
2037
2515
  } catch (e) {
2038
2516
  refusals[p.name] = ctrl.signal.aborted ? String(ctrl.signal.reason instanceof Error ? ctrl.signal.reason.message : ctrl.signal.reason ?? "cancelled") : e instanceof Error ? e.message : String(e);
@@ -2046,16 +2524,50 @@ async function quoteRoutes(providers, params, options = {}) {
2046
2524
  );
2047
2525
  return view();
2048
2526
  }
2049
- function rank(quotes) {
2527
+ function gapBps(dest, min) {
2528
+ if (dest <= 0n || min >= dest) return 0;
2529
+ return Number((((dest - min) * 10000n + dest - 1n) / dest).toString());
2530
+ }
2531
+ function rank(quotes, options = {}) {
2050
2532
  const sorted = [...quotes].sort((a, b) => BigInt(b.destAmount) > BigInt(a.destAmount) ? 1 : BigInt(b.destAmount) < BigInt(a.destAmount) ? -1 : a.feeUsd - b.feeUsd || a.etaSeconds - b.etaSeconds);
2051
2533
  const fastest = sorted.length ? sorted.reduce((f, q) => q.etaSeconds < f.etaSeconds ? q : f) : null;
2052
- return sorted.map((q, i) => {
2534
+ const depositProviders = new Set(options.depositAddressProviders ?? []);
2535
+ const paysAssignedAddress = (q) => {
2536
+ if (depositProviders.has(q.provider)) return true;
2537
+ if (!options.depositAddressRoute) return false;
2538
+ try {
2539
+ return options.depositAddressRoute(q) === true;
2540
+ } catch {
2541
+ return true;
2542
+ }
2543
+ };
2544
+ const assigned = new Map(sorted.map((q) => [q, paysAssignedAddress(q)]));
2545
+ const s = options.slippageBps;
2546
+ const warningsOf = (q) => {
2547
+ const w = [];
2548
+ if (s !== void 0 && Number.isInteger(s) && s >= 0 && s < 1e4) {
2549
+ const dest = BigInt(q.destAmount);
2550
+ const min = BigInt(q.minDestAmount);
2551
+ if (min < (0, import_protocol_swap2_core.slippageFloor)(dest, s)) w.push({ kind: "min-below-slippage", slippageBps: s, minGapBps: gapBps(dest, min) });
2552
+ }
2553
+ if (assigned.get(q)) w.push({ kind: "deposit-address" });
2554
+ return w;
2555
+ };
2556
+ const warned = new Map(sorted.map((q) => [q, warningsOf(q)]));
2557
+ const verifiableExists = sorted.some((q) => !assigned.get(q));
2558
+ const best = sorted.find((q) => {
2559
+ const w = warned.get(q);
2560
+ return !w.some((x) => x.kind === "min-below-slippage") && !(w.some((x) => x.kind === "deposit-address") && verifiableExists);
2561
+ });
2562
+ const ordered = best ? [best, ...sorted.filter((q) => q !== best)] : sorted;
2563
+ return ordered.map((q) => {
2053
2564
  const poolFee = q.providerData?.poolFee;
2054
2565
  const feeNote = typeof poolFee === "string" && poolFee && q.feeUsd === 0 ? poolFee === "pools" ? "pool fees in price" : `pool ${poolFee} in price` : void 0;
2055
2566
  const tags = [];
2056
- if (i === 0) tags.push("best");
2567
+ if (q === best) tags.push("best");
2057
2568
  if (fastest && q.provider === fastest.provider) tags.push("fastest");
2058
- return { provider: q.provider, destAmount: q.destAmount, minDestAmount: q.minDestAmount, feeUsd: q.feeUsd, etaSeconds: q.etaSeconds, validUntilMs: q.validUntilMs, tags, ...feeNote ? { feeNote } : {} };
2569
+ const warnings = warned.get(q);
2570
+ return { provider: q.provider, destAmount: q.destAmount, minDestAmount: q.minDestAmount, feeUsd: q.feeUsd, etaSeconds: q.etaSeconds, validUntilMs: q.validUntilMs, tags, ...feeNote ? { feeNote } : {}, ...warnings.length ? { warnings } : {} };
2059
2571
  });
2060
2572
  }
2061
2573
  var FLOOR_TOLERANCE_BPS = 50n;
@@ -2074,18 +2586,21 @@ function depositOf(s, approvals) {
2074
2586
  if (!s.data.startsWith(TRANSFER_SELECTOR) || s.data.length < 10 + 64) return void 0;
2075
2587
  return `0x${s.data.slice(10 + 24, 10 + 64)}`;
2076
2588
  }
2589
+ var pct = (bps) => `${Number((bps / 100).toFixed(2))}%`;
2077
2590
  function swapApprovalPayload(plan, meta) {
2078
2591
  const isOwn = meta.own.some((a) => lc3(a) === lc3(plan.recipient));
2592
+ const assigned = meta.depositAddressAssignedByProvider === true;
2079
2593
  const checks = [{ status: "ok", label: `Built by the ${meta.providerLabel} module` }];
2594
+ const depositCheck = assigned ? { status: "warn", label: `Deposit address assigned by the ${meta.providerLabel} server \u2014 funds go to an address the wallet cannot verify` } : { status: "ok", label: `Deposit address from the ${meta.providerLabel} quote` };
2080
2595
  const s = plan.source;
2081
2596
  let source;
2082
2597
  if (s.kind === "call") {
2083
2598
  const deposit = depositOf(s, plan.approvals.length);
2084
2599
  source = { kind: "call", to: s.to, ...s.data ? { data: s.data } : {}, value: s.value.toString(), ...deposit ? { deposit } : {} };
2085
- checks.push(deposit ? { status: "ok", label: `Deposit address from the ${meta.providerLabel} quote` } : { status: "ok", label: `Calls the ${meta.providerLabel} contract` });
2600
+ checks.push(deposit || assigned ? depositCheck : { status: "ok", label: `Calls the ${meta.providerLabel} contract` });
2086
2601
  } else if (s.kind === "token-transfer") {
2087
2602
  source = { kind: "token-transfer", token: s.token, recipient: s.recipient, amount: s.amount.toString(), ...s.memo ? { memo: s.memo } : {} };
2088
- checks.push({ status: "ok", label: `Deposit address from the ${meta.providerLabel} quote` });
2603
+ checks.push(depositCheck);
2089
2604
  } else if (s.kind === "serialized") {
2090
2605
  source = { kind: "serialized" };
2091
2606
  checks.push({ status: "ok", label: "Balance changes are proven before signing (SDK guard)" });
@@ -2096,7 +2611,14 @@ function swapApprovalPayload(plan, meta) {
2096
2611
  }
2097
2612
  for (const a of plan.approvals) checks.push(a.amount > plan.sourceAmount ? { status: "warn", label: "Approval is larger than this swap needs" } : { status: "ok", label: "Approval is exactly the amount needed" });
2098
2613
  if (plan.approvals.length > 1) checks.push({ status: "ok", label: `${plan.approvals.length} approvals (reset to 0 first)` });
2099
- checks.push(isOwn ? { status: "ok", label: "Funds arrive at your address" } : { status: "warn", label: `Funds arrive at ${plan.recipient.slice(0, 6)}\u2026${plan.recipient.slice(-4)}, not your address` });
2614
+ checks.push(!isOwn ? { status: "warn", label: `Funds arrive at ${plan.recipient.slice(0, 6)}\u2026${plan.recipient.slice(-4)}, not your address` } : assigned ? { status: "unknown", label: `Delivery to your address is the ${meta.providerLabel} server\u2019s promise \u2014 this transaction does not enforce it` } : { status: "ok", label: "Funds arrive at your address" });
2615
+ let slippage;
2616
+ const sb = meta.slippageBps;
2617
+ if (sb !== void 0 && Number.isInteger(sb) && sb >= 0 && sb < 1e4) {
2618
+ const below = plan.minDestAmount < (0, import_protocol_swap2_core.slippageFloor)(plan.destAmount, sb);
2619
+ slippage = { bps: sb, minGapBps: gapBps(plan.destAmount, plan.minDestAmount), below };
2620
+ checks.push(below ? { status: "warn", label: `Minimum is ${pct(slippage.minGapBps)} under the quote \u2014 more than your ${pct(sb)} slippage` } : { status: "ok", label: `Minimum within your ${pct(sb)} slippage` });
2621
+ }
2100
2622
  return {
2101
2623
  provider: plan.provider,
2102
2624
  providerLabel: meta.providerLabel,
@@ -2113,7 +2635,9 @@ function swapApprovalPayload(plan, meta) {
2113
2635
  validUntilMs: plan.validUntilMs,
2114
2636
  approvals: plan.approvals.map((a) => ({ token: a.token, spender: a.spender, amount: a.amount.toString() })),
2115
2637
  source,
2116
- checks
2638
+ checks,
2639
+ ...slippage ? { slippage } : {},
2640
+ ...assigned ? { depositAssignedByProvider: true } : {}
2117
2641
  };
2118
2642
  }
2119
2643
  function parseTypedOrder(json) {
@@ -2564,6 +3088,10 @@ var RecipientBook = class {
2564
3088
  await this.storage.set(s);
2565
3089
  this._state = s;
2566
3090
  }
3091
+ /** Forget the cached book: the next read asks storage again (e.g. a storage that only answers once the wallet is unlocked). */
3092
+ reload() {
3093
+ this._state = null;
3094
+ }
2567
3095
  /** `known` → the address is one the user paid before; `lookalike` → it imitates one. */
2568
3096
  async check(namespace, address) {
2569
3097
  const list = await this.list(namespace);
@@ -2571,4 +3099,236 @@ var RecipientBook = class {
2571
3099
  return { known, lookalike: known ? null : lookalikeOf(address, list) };
2572
3100
  }
2573
3101
  };
3102
+
3103
+ // src/mac-storage.ts
3104
+ var import_core5 = require("@anywalletkit/core");
3105
+ var STORAGE_MAC_FIELD = "anywalletkit.mac/1";
3106
+ var STORAGE_VERSION_FIELD = "anywalletkit.ver/1";
3107
+ var HKDF_INFO2 = "anywalletkit/storage-mac/1";
3108
+ var KEY_BYTES = 32;
3109
+ async function deriveStorageMacKey(dek, crypto2 = globalThis.crypto) {
3110
+ if (dek.length !== 32) throw new VaultError("Vault key must be 32 bytes.", "invalid-input");
3111
+ const ikm = await withSecretBuffer(dek, (b) => crypto2.subtle.importKey("raw", b, "HKDF", false, ["deriveBits"]));
3112
+ const bits = await crypto2.subtle.deriveBits({ name: "HKDF", hash: "SHA-256", salt: new ArrayBuffer(0), info: toArrayBuffer(utf8.encode(HKDF_INFO2)) }, ikm, KEY_BYTES * 8);
3113
+ return new Uint8Array(bits);
3114
+ }
3115
+ function macJsonStorage(inner, options) {
3116
+ const { label, versions } = options;
3117
+ if (versions && label.includes("\n")) throw new VaultError("A versioned store label has no line break.", "invalid-input");
3118
+ const readWithoutKey = options.readWithoutKey ?? "unverified";
3119
+ const writeWithoutKey = options.writeWithoutKey ?? "unsigned";
3120
+ const withKey = async (given, fn) => {
3121
+ const key = given ? Uint8Array.from(given) : await options.key();
3122
+ try {
3123
+ return await fn(key);
3124
+ } finally {
3125
+ if (key) (0, import_core5.zeroize)(key);
3126
+ }
3127
+ };
3128
+ const stored = async (key) => {
3129
+ const raw = await inner.get();
3130
+ if (raw === null || raw === void 0) return { state: "empty", value: null };
3131
+ const envelope = isStored(raw);
3132
+ const value = envelope ? raw.value : raw;
3133
+ if (!envelope || raw[STORAGE_MAC_FIELD] === null) return { state: "unsigned", value };
3134
+ if (!key) return { state: "no-key", value };
3135
+ if (!versions) return { state: await verify(key, label, value, raw[STORAGE_MAC_FIELD]) ? "ok" : "invalid", value };
3136
+ const version = STORAGE_VERSION_FIELD in raw ? raw[STORAGE_VERSION_FIELD] : 0;
3137
+ if (version !== 0 && !isVersion(version)) return { state: "invalid", value };
3138
+ if (!await verify(key, label, value, raw[STORAGE_MAC_FIELD], version || void 0)) return { state: "invalid", value };
3139
+ return { state: "ok", value, version };
3140
+ };
3141
+ const put = async (key, value, version) => {
3142
+ const mac = await sign(key, label, value, version);
3143
+ await inner.set(version === void 0 ? { [STORAGE_MAC_FIELD]: mac, value } : { [STORAGE_MAC_FIELD]: mac, [STORAGE_VERSION_FIELD]: version, value });
3144
+ };
3145
+ const seen = async (key) => {
3146
+ const r = await stored(key);
3147
+ return r.state === "ok" ? r.version ?? 0 : 0;
3148
+ };
3149
+ const read = (given) => withKey(given, async (key) => {
3150
+ if (!versions || !key) return stored(key);
3151
+ return versions.exclusive(async (ledger) => {
3152
+ const r = await stored(key);
3153
+ if (r.state === "empty") return await ledger.check(label, null, key) === "ok" ? r : { state: "stale", value: null };
3154
+ if (r.state !== "ok") return r;
3155
+ return await ledger.check(label, r.version ?? 0, key) === "ok" ? r : { ...r, state: "stale" };
3156
+ });
3157
+ });
3158
+ const write = (value, given) => withKey(given, async (key) => {
3159
+ if (!key) throw new VaultError("The wallet is locked: unlock it to change this.", "locked");
3160
+ if (!versions) return put(key, value);
3161
+ await versions.exclusive((ledger) => ledger.advance(label, key, () => seen(key), (version) => put(key, value, version)));
3162
+ });
3163
+ return {
3164
+ read,
3165
+ write,
3166
+ async get() {
3167
+ const r = await read();
3168
+ if (r.state === "ok") return r.value;
3169
+ if (r.state === "no-key" && readWithoutKey === "unverified") return r.value;
3170
+ return null;
3171
+ },
3172
+ async set(value) {
3173
+ await withKey(void 0, async (key) => {
3174
+ if (key) return write(value, key);
3175
+ if (writeWithoutKey === "refuse") throw new VaultError("The wallet is locked: unlock it to change this.", "locked");
3176
+ await inner.set({ [STORAGE_MAC_FIELD]: null, value });
3177
+ });
3178
+ },
3179
+ async rekey(oldKey, newKey) {
3180
+ if (!versions) {
3181
+ const r = await read(oldKey);
3182
+ if (r.state === "ok") await write(r.value, newKey);
3183
+ return r.state;
3184
+ }
3185
+ return versions.exclusive(async (ledger) => {
3186
+ const r = await stored(oldKey);
3187
+ if (r.state === "empty") return await ledger.check(label, null, newKey) === "ok" ? "empty" : "stale";
3188
+ if (r.state !== "ok") return r.state;
3189
+ const version = r.version ?? 0;
3190
+ if (await ledger.check(label, version, newKey) !== "ok") return "stale";
3191
+ if (version > 0) await put(newKey, r.value, version);
3192
+ else await ledger.advance(label, newKey, async () => 0, (v) => put(newKey, r.value, v));
3193
+ return "ok";
3194
+ });
3195
+ }
3196
+ };
3197
+ }
3198
+ function isStored(raw) {
3199
+ if (typeof raw !== "object" || raw === null || !(STORAGE_MAC_FIELD in raw) || !("value" in raw)) return false;
3200
+ const mac = raw[STORAGE_MAC_FIELD];
3201
+ return mac === null || typeof mac === "string";
3202
+ }
3203
+ async function hmacKey(key, usage) {
3204
+ if (key.length !== KEY_BYTES) throw new VaultError("A storage MAC key is 32 bytes.", "invalid-input");
3205
+ return withSecretBuffer(key, (b) => globalThis.crypto.subtle.importKey("raw", b, { name: "HMAC", hash: "SHA-256" }, false, [usage]));
3206
+ }
3207
+ function macInput(label, value, version) {
3208
+ return toArrayBuffer(utf8.encode(version === void 0 ? `${label}
3209
+ ${canonicalJson(value)}` : `${label}
3210
+ ${version}
3211
+ ${canonicalJson(value)}`));
3212
+ }
3213
+ async function sign(key, label, value, version) {
3214
+ return toBase64(new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", await hmacKey(key, "sign"), macInput(label, value, version))));
3215
+ }
3216
+ async function verify(key, label, value, mac, version) {
3217
+ let bytes;
3218
+ try {
3219
+ bytes = fromBase64(mac);
3220
+ } catch {
3221
+ return false;
3222
+ }
3223
+ return globalThis.crypto.subtle.verify("HMAC", await hmacKey(key, "verify"), toArrayBuffer(bytes), macInput(label, value, version));
3224
+ }
3225
+ var isVersion = (x) => typeof x === "number" && Number.isSafeInteger(x) && x > 0;
3226
+ function canonicalJson(value) {
3227
+ return JSON.stringify(sortKeys(JSON.parse(JSON.stringify(value ?? null))));
3228
+ }
3229
+ function sortKeys(x) {
3230
+ if (Array.isArray(x)) return x.map(sortKeys);
3231
+ if (x === null || typeof x !== "object") return x;
3232
+ const out = {};
3233
+ for (const k of Object.keys(x).sort()) out[k] = sortKeys(x[k]);
3234
+ return out;
3235
+ }
3236
+ var LEDGER_LABEL = "anywalletkit/storage-versions/1";
3237
+ function storageVersionLedger(options) {
3238
+ const label = options.label ?? LEDGER_LABEL;
3239
+ const acceptUnversioned = options.acceptUnversioned ?? false;
3240
+ const now = options.now ?? Date.now;
3241
+ let tail = Promise.resolve();
3242
+ const queue = (fn) => {
3243
+ const run = tail.then(fn, fn);
3244
+ tail = run.then(() => void 0, () => void 0);
3245
+ return run;
3246
+ };
3247
+ const high = async () => {
3248
+ const v = await options.mark.get();
3249
+ return isVersion(v) ? v : 0;
3250
+ };
3251
+ const parse = async (raw, key) => {
3252
+ if (!isStored(raw) || raw[STORAGE_MAC_FIELD] === null) return null;
3253
+ const epoch = raw[STORAGE_VERSION_FIELD];
3254
+ const v = raw.value;
3255
+ if (!isVersion(epoch) || typeof v !== "object" || v === null || !isVersion(v.base) || typeof v.sealed !== "boolean") return null;
3256
+ if (typeof v.versions !== "object" || v.versions === null || !Object.values(v.versions).every(isVersion)) return null;
3257
+ if (!await verify(key, label, v, raw[STORAGE_MAC_FIELD], epoch)) return null;
3258
+ return { epoch, base: v.base, sealed: v.sealed, versions: { ...v.versions } };
3259
+ };
3260
+ const load = async (key) => {
3261
+ const mark = await high();
3262
+ const raw = await options.manifest.get();
3263
+ if (raw === null || raw === void 0) return { state: mark > 0 ? "stale" : "empty" };
3264
+ const manifest = await parse(raw, key);
3265
+ if (!manifest) return { state: "invalid" };
3266
+ if (manifest.epoch < mark) return { state: "stale", manifest };
3267
+ if (manifest.epoch > mark) await options.mark.set(manifest.epoch);
3268
+ return { state: "ok", manifest };
3269
+ };
3270
+ const store = async (m, key) => {
3271
+ const value = { base: m.base, sealed: m.sealed, versions: m.versions };
3272
+ await options.manifest.set({ [STORAGE_MAC_FIELD]: await sign(key, label, value, m.epoch), [STORAGE_VERSION_FIELD]: m.epoch, value });
3273
+ if (m.epoch > await high()) await options.mark.set(m.epoch);
3274
+ };
3275
+ const fresh = async (sealed, prior, key) => {
3276
+ const floor = Math.max(await high(), Math.floor(now()), prior ? Math.max(prior.epoch, prior.base, ...Object.values(prior.versions)) : 0);
3277
+ if (!Number.isSafeInteger(floor + 2)) throw new VaultError("Storage version out of range.", "invalid-input");
3278
+ const m = { epoch: floor + 1, base: floor + 1, sealed, versions: {} };
3279
+ await store(m, key);
3280
+ return m;
3281
+ };
3282
+ const record = (m, name, version, key) => store({ ...m, epoch: m.epoch + 1, versions: { ...m.versions, [name]: version } }, key);
3283
+ const named = (name) => {
3284
+ if (name === label || name.includes("\n")) throw new VaultError("Not a store label.", "invalid-input");
3285
+ };
3286
+ const ops = {
3287
+ async check(name, version, key) {
3288
+ named(name);
3289
+ const l = await load(key);
3290
+ if (l.state === "empty") return version === null || version === 0 && acceptUnversioned ? "ok" : "stale";
3291
+ if (l.state !== "ok") return "stale";
3292
+ const m = l.manifest;
3293
+ const listed = m.versions[name];
3294
+ if (listed === void 0 && version === null) return m.sealed ? "stale" : "ok";
3295
+ if (listed === void 0 && version === 0) return !m.sealed && acceptUnversioned ? "ok" : "stale";
3296
+ if (version === null || version === 0) return "stale";
3297
+ if (version === listed) return "ok";
3298
+ if (version !== (listed ?? m.base) + 1) return "stale";
3299
+ await record(m, name, version, key).catch(() => void 0);
3300
+ return "ok";
3301
+ },
3302
+ async advance(name, key, seen, write) {
3303
+ named(name);
3304
+ const l = await load(key);
3305
+ const before = await seen();
3306
+ const m = l.state === "ok" ? l.manifest : await fresh(l.state !== "empty" || before > 0, l.manifest, key);
3307
+ const version = Math.max(m.versions[name] ?? m.base, before) + 1;
3308
+ if (!Number.isSafeInteger(version)) throw new VaultError("Storage version out of range.", "invalid-input");
3309
+ await write(version);
3310
+ await record(m, name, version, key);
3311
+ return version;
3312
+ }
3313
+ };
3314
+ return {
3315
+ exclusive: (fn) => queue(() => fn(ops)),
3316
+ condemn: (key) => queue(async () => {
3317
+ await fresh(true, (await load(key)).manifest, key);
3318
+ }),
3319
+ rekey: (oldKey, newKey) => queue(async () => {
3320
+ const l = await load(oldKey);
3321
+ if (l.state === "ok") await store({ ...l.manifest, epoch: l.manifest.epoch + 1 }, newKey);
3322
+ return l.state;
3323
+ }),
3324
+ read: (key) => queue(async () => {
3325
+ const l = await load(key);
3326
+ return { state: l.state, ...l.manifest ?? { epoch: 0, base: 0, sealed: false, versions: {} } };
3327
+ }),
3328
+ reset: () => queue(async () => {
3329
+ await options.manifest.set(null);
3330
+ await options.mark.set(null);
3331
+ })
3332
+ };
3333
+ }
2574
3334
  //# sourceMappingURL=index.cjs.map