@anywalletkit/extension-kit 0.2.1 → 0.3.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/README.md CHANGED
@@ -36,7 +36,7 @@ pnpm add @anywalletkit/extension-kit @anywalletkit/core @anywalletkit/keyring
36
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 |
37
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 |
38
38
  | Keyring adapters | `chromeKeyringStorage`, `vaultSecretStorage` | `@anywalletkit/keyring` metadata as plain JSON, secrets through the vault, one DEK use per call |
39
- | Relay | `PageRequest` / `RelayRequest` shapes and guards, `pageRequestFrom`, `stampOrigin`, `checkSenderOrigin`, `RELAY_ERRORS` | page ⇄ content script ⇄ service worker messages; the content script takes only messages the page window itself posted (`pageRequestFrom(event, window)`: an iframe's `parent.postMessage` is not the site), and the origin is the browser's (`port.sender`), never the page's |
39
+ | Relay | `PageRequest` / `RelayRequest` shapes and guards, `isRelayMethod`, `pageRequestFrom`, `stampOrigin`, `checkSenderOrigin`, `RELAY_ERRORS` | page ⇄ content script ⇄ service worker messages; the content script takes only messages the page window itself posted (`pageRequestFrom(event, window)`: an iframe's `parent.postMessage` is not the site), and the origin is the browser's (`port.sender`), never the page's |
40
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 |
41
41
  | Permissions | `PermissionStore`, `CHAIN_ACCOUNT_KEYS` | per-origin accounts (EVM, Solana, TON, Tron, Bitcoin, NEAR) and chain |
42
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 |
@@ -68,6 +68,12 @@ pnpm add @anywalletkit/extension-kit @anywalletkit/core @anywalletkit/keyring
68
68
  `create` never overwrites; reset is an explicit `destroy()`.
69
69
  - **Passkeys.** A passkey wraps the same DEK under HKDF-SHA256(PRF output,
70
70
  salt = credential id); the password stays the recovery path.
71
+ - **Re-keying.** `changePassword` re-wraps the same DEK, and `removePasskey`
72
+ does not take back a DEK the passkey already unwrapped. After a suspected
73
+ compromise, `rotateVaultKey(password)` makes a fresh DEK, re-seals every
74
+ secret, drops the passkeys (register them again) and writes one envelope;
75
+ hand the returned DEK to `session.unlock`, which replaces the old one.
76
+ `setSecret` refuses a DEK that does not open the vault's secrets.
71
77
  - **Unlocked state.** Only the DEK is kept, in `chrome.storage.session`
72
78
  (memory-only, trusted contexts), with an expiry enforced on every read and an
73
79
  alarm that locks; key buffers this package owns are zeroized after use.
package/dist/index.cjs CHANGED
@@ -44,6 +44,7 @@ __export(src_exports, {
44
44
  QueueLimitError: () => QueueLimitError,
45
45
  READ_METHODS: () => READ_METHODS,
46
46
  RELAY_ERRORS: () => RELAY_ERRORS,
47
+ RELAY_METHOD_MAX: () => RELAY_METHOD_MAX,
47
48
  RecipientBook: () => RecipientBook,
48
49
  RequestRouter: () => RequestRouter,
49
50
  ReviewDeclined: () => ReviewDeclined,
@@ -103,6 +104,7 @@ __export(src_exports, {
103
104
  isPageEvent: () => isPageEvent,
104
105
  isPageRequest: () => isPageRequest,
105
106
  isPageResponse: () => isPageResponse,
107
+ isRelayMethod: () => isRelayMethod,
106
108
  isRelayRequest: () => isRelayRequest,
107
109
  isRelayToContent: () => isRelayToContent,
108
110
  levenshtein: () => levenshtein,
@@ -293,7 +295,12 @@ var Vault = class {
293
295
  const kek = await this._deriveScrypt(password, fromBase64(envelope.kdf.salt), envelope.kdf);
294
296
  return this._open(kek, envelope.dek, aadDekV2(envelope.kdf), "wrong-password");
295
297
  }
296
- /** Re-wrap the DEK under a new password (fresh salt, current KDF). Secrets and passkeys are untouched. */
298
+ /**
299
+ * Re-wrap the DEK under a new password (fresh salt, current KDF). Secrets and passkeys are untouched —
300
+ * and so is the DEK: after a suspected compromise (a copied profile, a leaked session, a lost passkey)
301
+ * this is not enough, because whoever holds the DEK still opens every secret, old and new. Use
302
+ * {@link Vault.rotateVaultKey} then.
303
+ */
297
304
  async changePassword(oldPassword, newPassword) {
298
305
  requirePassword(newPassword, VAULT_MIN_PASSWORD_LENGTH);
299
306
  requirePassword(oldPassword);
@@ -309,6 +316,45 @@ var Vault = class {
309
316
  }
310
317
  });
311
318
  }
319
+ /**
320
+ * Replace the DEK (scan 2026-10-27 modules, F30): a fresh random DEK, every secret re-sealed under it,
321
+ * the DEK wrapped under `password` with a fresh salt — written as one envelope, so a failure leaves the
322
+ * old vault as it was. Returns the new DEK: hand it to `session.unlock(dek)`, which overwrites the old
323
+ * one in `chrome.storage.session`. A DEK copied before the rotation opens nothing in the new envelope,
324
+ * and {@link Vault.setSecret} refuses it, so a stale session cannot seal new secrets under it.
325
+ *
326
+ * Passkeys are dropped — their boxes hold the old DEK and cannot be re-wrapped without their PRF
327
+ * output — and must be registered again ({@link Vault.addPasskey} with the new DEK).
328
+ */
329
+ async rotateVaultKey(password) {
330
+ requirePassword(password);
331
+ return this._exclusive(async () => {
332
+ const oldDek = await this._unlock(password);
333
+ const dek = this._random(DEK_BYTES);
334
+ try {
335
+ const envelope = await this._load();
336
+ const [oldKey, newKey] = await Promise.all([this._importDek(oldDek), this._importDek(dek)]);
337
+ const secrets = {};
338
+ for (const [id, box] of Object.entries(envelope.secrets)) {
339
+ const plaintext = await this._open(oldKey, box, aadSecret(id), "locked");
340
+ try {
341
+ secrets[id] = await this._seal(newKey, plaintext, aadSecret(id));
342
+ } finally {
343
+ (0, import_core.zeroize)(plaintext);
344
+ }
345
+ }
346
+ const next = await this._sealEnvelope(password, dek, secrets, [], envelope.createdAt);
347
+ const upgradedAt = isEnvelopeV2(envelope) ? envelope.upgradedAt : void 0;
348
+ await this._write(isEnvelopeV2(next) ? { ...next, ...upgradedAt !== void 0 ? { upgradedAt } : {}, rotatedAt: Date.now() } : next);
349
+ return dek;
350
+ } catch (e) {
351
+ (0, import_core.zeroize)(dek);
352
+ throw e;
353
+ } finally {
354
+ (0, import_core.zeroize)(oldDek);
355
+ }
356
+ });
357
+ }
312
358
  // ---- passkeys (v2) ----
313
359
  /** Public view of the registered passkeys (ids, labels; whether each can verify assertions). */
314
360
  async listPasskeys() {
@@ -366,6 +412,7 @@ var Vault = class {
366
412
  (0, import_core.zeroize)(prf);
367
413
  }
368
414
  }
415
+ /** Forget one passkey's box. A DEK that passkey already unwrapped stays valid: after a lost or compromised passkey, {@link Vault.rotateVaultKey}. */
369
416
  async removePasskey(id) {
370
417
  requireId(id);
371
418
  return this._exclusive(async () => {
@@ -379,13 +426,17 @@ var Vault = class {
379
426
  const envelope = await this._storage.get();
380
427
  return envelope ? Object.keys(envelope.secrets) : [];
381
428
  }
382
- /** Seal `secret` under the DEK as `sourceId` (overwrites an entry with the same id). */
429
+ /**
430
+ * Seal `secret` under the DEK as `sourceId` (overwrites an entry with the same id). The DEK must open the
431
+ * vault's secrets (`locked` otherwise): a key retired by {@link Vault.rotateVaultKey} never seals a new one.
432
+ */
383
433
  async setSecret(dek, sourceId, secret) {
384
434
  requireId(sourceId);
385
435
  if (secret.length > VAULT_LIMITS.secretBytes) throw new VaultError(`A secret is at most ${VAULT_LIMITS.secretBytes} bytes.`, "invalid-input");
386
436
  const key = await this._importDek(dek);
387
437
  return this._exclusive(async () => {
388
438
  const envelope = await this._load();
439
+ if (Object.keys(envelope.secrets).length > 0) await this._requireDek(dek, envelope);
389
440
  if (!(sourceId in envelope.secrets) && Object.keys(envelope.secrets).length >= VAULT_LIMITS.secrets) {
390
441
  throw new VaultError(`A vault holds at most ${VAULT_LIMITS.secrets} secrets.`, "invalid-input");
391
442
  }
@@ -769,8 +820,10 @@ var RELAY_ERRORS = {
769
820
  var CHAINS = /* @__PURE__ */ new Set(["eip155", "solana", "ton", "tron", "bip122", "near"]);
770
821
  var isRecord = (x) => typeof x === "object" && x !== null;
771
822
  var isId = (x) => typeof x === "string" && x.length > 0 && x.length <= 64;
823
+ var RELAY_METHOD_MAX = 128;
824
+ var isRelayMethod = (x) => typeof x === "string" && x.length > 0 && x.length <= RELAY_METHOD_MAX && /^[A-Za-z0-9_]+$/.test(x);
772
825
  function isPageRequest(x) {
773
- return isRecord(x) && x.target === PAGE_TARGET.toContent && isId(x.id) && CHAINS.has(x.chain) && typeof x.method === "string";
826
+ return isRecord(x) && x.target === PAGE_TARGET.toContent && isId(x.id) && CHAINS.has(x.chain) && isRelayMethod(x.method);
774
827
  }
775
828
  function isPageResponse(x) {
776
829
  return isRecord(x) && x.target === PAGE_TARGET.toPage && isId(x.id);
@@ -779,7 +832,7 @@ function isPageEvent(x) {
779
832
  return isRecord(x) && x.target === PAGE_TARGET.toPage && !("id" in x) && CHAINS.has(x.chain) && typeof x.event === "string";
780
833
  }
781
834
  function isRelayRequest(x) {
782
- return isRecord(x) && x.kind === "request" && isId(x.id) && typeof x.origin === "string" && CHAINS.has(x.chain) && typeof x.method === "string";
835
+ return isRecord(x) && x.kind === "request" && isId(x.id) && typeof x.origin === "string" && CHAINS.has(x.chain) && isRelayMethod(x.method);
783
836
  }
784
837
  function isRelayToContent(x) {
785
838
  return isRecord(x) && (x.kind === "response" && isId(x.id) || x.kind === "event" && typeof x.event === "string");
@@ -906,6 +959,7 @@ function vaultSecretStorage(vault, session) {
906
959
  }
907
960
 
908
961
  // src/networks.ts
962
+ var import_url = require("@anywalletkit/networks/url");
909
963
  var import_swap2_assets = require("@anywalletkit/swap2-assets");
910
964
  var DEFAULT_CHAIN = "eip155:1";
911
965
  var net = (chainId, name, symbol, rpcs, explorer) => explorer === void 0 ? { caip2: `eip155:${chainId}`, chainId, name, symbol, rpcs } : { caip2: `eip155:${chainId}`, chainId, name, symbol, rpcs, explorer };
@@ -1019,26 +1073,14 @@ var NetworksTable = class {
1019
1073
  return this._loading;
1020
1074
  }
1021
1075
  };
1022
- function isPrivateRpcHost(url) {
1023
- const m = /^https:\/\/(?:[^@/?#]*@)?(\[[^\]]+\]|[^:/?#]+)/i.exec(url);
1024
- if (!m) return true;
1025
- const h = m[1].toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
1026
- if (h === "localhost" || h.endsWith(".localhost") || h.endsWith(".local") || h === "0.0.0.0") return true;
1027
- const v4 = /^(\d{1,3})\.(\d{1,3})\.\d{1,3}\.\d{1,3}$/.exec(h);
1028
- if (v4) {
1029
- const a = Number(v4[1]), b = Number(v4[2]);
1030
- return a === 127 || a === 10 || a === 0 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
1031
- }
1032
- if (h.includes(":")) return h === "::1" || h === "::" || /^f[cd]/.test(h) || /^fe[89ab]/.test(h);
1033
- return false;
1034
- }
1076
+ var isSiteRpcUrl = (u) => typeof u === "string" && u.length <= 2048 && (0, import_url.isPublicHttpsUrl)(u);
1035
1077
  function parseAddChainParams(params, known = isCatalogChain) {
1036
1078
  const p = Array.isArray(params) ? params[0] : params;
1037
1079
  if (!p || typeof p !== "object") return relayError(RELAY_ERRORS.invalidParams, "wallet_addEthereumChain expects one object parameter.");
1038
1080
  const caip2 = caip2FromChainId(p.chainId);
1039
1081
  if (!caip2) return relayError(RELAY_ERRORS.invalidParams, "chainId must be a 0x-prefixed hex string.");
1040
1082
  if (!known(caip2)) return relayError(RELAY_ERRORS.chainNotAdded, `Chain ${String(p.chainId)} is not in this wallet's catalog.`);
1041
- const rpcs = Array.isArray(p.rpcUrls) ? p.rpcUrls.filter((u) => typeof u === "string" && /^https:\/\/[^\s/?#]+/i.test(u) && !isPrivateRpcHost(u)) : [];
1083
+ const rpcs = Array.isArray(p.rpcUrls) ? p.rpcUrls.filter(isSiteRpcUrl) : [];
1042
1084
  if (rpcs.length === 0) return relayError(RELAY_ERRORS.invalidParams, "rpcUrls must contain at least one public https URL (a site cannot add a private / local endpoint).");
1043
1085
  const name = typeof p.chainName === "string" && p.chainName.trim() ? p.chainName.trim().slice(0, 64) : caip2;
1044
1086
  const currency = p.nativeCurrency;
@@ -1399,8 +1441,9 @@ var RouterError = class _RouterError extends Error {
1399
1441
  };
1400
1442
  var PROVIDER_CODES = /* @__PURE__ */ new Set([4001, 4100, 4200, 4900, 4901, 4902]);
1401
1443
  var isNodeCode = (code) => typeof code === "number" && Number.isInteger(code) && (code === 3 || code >= -32768 && code <= -32e3);
1402
- var URL_RE = /\b[a-z][a-z0-9+.-]*:\/\/[^\s"'<>()]+/gi;
1403
- var pageText = (text) => text.replace(URL_RE, "[url]").split("\n")[0].slice(0, 300);
1444
+ var URL_RE = /[a-z][a-z0-9+.-]{0,31}:\/\/[^\s"'<>()]+/gi;
1445
+ var PAGE_TEXT_WINDOW = 1024;
1446
+ var pageText = (text) => text.slice(0, PAGE_TEXT_WINDOW).split("\n", 1)[0].replace(URL_RE, "[url]").slice(0, 300);
1404
1447
  var isHexData = (v) => typeof v === "string" && /^0x[0-9a-fA-F]*$/.test(v) && v.length <= 2 + 2 * 8192;
1405
1448
  function nodeError(e) {
1406
1449
  for (let cur = e, depth = 0; cur && typeof cur === "object" && depth < 8; cur = cur.cause, depth++) {
@@ -1459,6 +1502,7 @@ var RequestRouter = class {
1459
1502
  async handle(origin, chain, method, rawParams) {
1460
1503
  const refused = this.deps.refuse?.(origin);
1461
1504
  if (refused) throw new RouterError(RELAY_ERRORS.unauthorized, refused);
1505
+ if (!isRelayMethod(method)) throw new RouterError(RELAY_ERRORS.invalidRequest, "The request has no valid method name.");
1462
1506
  if (chain !== "eip155") throw new RouterError(RELAY_ERRORS.unsupportedMethod, `${chain} providers are not available yet.`);
1463
1507
  const params = Array.isArray(rawParams) ? rawParams : rawParams === void 0 ? [] : [rawParams];
1464
1508
  const network = await this.chainFor(origin);