@anywalletkit/extension-kit 0.2.2 → 0.3.1
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 +7 -1
- package/dist/index.cjs +64 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -3
- package/dist/index.d.ts +34 -3
- package/dist/index.js +64 -20
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
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
|
-
/**
|
|
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
|
-
/**
|
|
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) &&
|
|
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) &&
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
1403
|
-
var
|
|
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);
|