@anywalletkit/extension-kit
@anywalletkit/extension-kit
Section titled “@anywalletkit/extension-kit”The security-relevant core of a non-custodial Chromium (MV3) wallet extension,
built on the Any Wallet Kit chain and protocol packages: an encrypted vault, the
unlocked-key session with auto-lock, the dApp request router with per-origin
permissions and an approval queue, signing-request classification, and the
review helpers for swaps and Earn plans. No UI, no @types/chrome, no network
code of its own: the chrome adapters take structural objects
(StorageAreaLike, AlarmsLike), so everything runs under Vitest with
in-memory adapters.
Install
Section titled “Install”npm install @anywalletkit/extension-kit# or: pnpm add @anywalletkit/extension-kit · yarn add @anywalletkit/extension-kitOn npm: @anywalletkit/extension-kit, published from the release workflow with provenance.
This package is for wallet developers who build their own extension. It is a library, not an extension: the popup, approval windows, injected providers, service-worker wiring, build and release pipeline are the wallet’s. The Vaulty browser extension is one product built on it.
pnpm add @anywalletkit/extension-kit @anywalletkit/core @anywalletkit/keyringWhat it provides
Section titled “What it provides”| Area | Exports | What |
|---|---|---|
| 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 |
| 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 |
| Keyring adapters | chromeKeyringStorage, vaultSecretStorage |
@anywalletkit/keyring metadata as plain JSON, secrets through the vault, one DEK use per call |
| 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 |
| 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 |
| Permissions | PermissionStore, CHAIN_ACCOUNT_KEYS |
per-origin accounts (EVM, Solana, TON, Tron, Bitcoin, NEAR) and chain |
| 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 |
| Signing | classifySignRequest, prepareTransaction, createSignHandler, messageText |
connected-address and chain checks, EIP-7702 delegations from sites refused, nonce / gas / fees from the wallet’s RPC |
| Networks | NetworksTable, DEFAULT_NETWORKS, parseAddChainParams, Solana / TON / Tron endpoints |
EVM networks with public RPC failover lists; wallet_addEthereumChain only for catalog chains, https only, eth_chainId verified, site-supplied RPCs stamped custom |
| Review gate | ReviewGate, evmCallMatcher, chainTxMatcher |
after one sheet reviewed a whole plan, exactly those operations pass once within a TTL |
| Swaps | quoteRoutes, reviewSwapPlan, executionFloor, SwapsStore, SwapTracker, parseSwapId |
progressive quotes over swap2 providers, one review sheet per plan (a gasless order is shown and matched by its EIP-712 hash), pre-approvals bound to the reviewing account (meta.account), pending swaps resumed across worker restarts |
| Earn | reviewEarnPlan, earnPlanMatchers |
one sheet for an Earn module’s plan (approvals + call), bound to meta.account |
| TON Connect | parseConnectRequest, parseSendTransaction, parseSignData, tonProofMessage, tonSignDataMessage |
the pure-data half of TON Connect v2 (the digests ton_proof and signData sign, to verify them); the account signs with signTonProof / signTonConnectData (wallet-ton) — sign() refuses 32-byte digests |
| Phishing | PhishingDetector |
MetaMask eth-phishing-detect list format, shipped with the build (never fetched at runtime); a listed site is refused (refusal → refuse on ApprovalQueue and RequestRouter, 4100, no window); fuzzy lookalikes, brand-in-label, punycode warn |
| SIWE | parseSiwe, siweProblems, describeSiwe |
EIP-4361 sign-in messages: domain vs. the asking site, address, chain, expiry, nonce (warnings) |
| Address poisoning | lookalikeOf, RecipientBook |
warns when a new recipient imitates one the user confirmed |
| WebAuthn | verifyAssertion, approvalChallenge, derToRaw, rawToDer |
ES256 assertion check for “passkey on every signature”; the worker never trusts the page’s word |
Security model (summary)
Section titled “Security model (summary)”- Keys at rest. Envelope v2: scrypt N=2^17 r=8 p=1 (
@noble/hashes) → AES-256-GCM wraps a random 32-byte DEK; the DEK seals each secret under AADforgewallet-vault/secret/<id>(boxes cannot be swapped). The DEK box’s AAD binds the exact KDF parameters, so an envelope whose salt or cost was edited on disk fails to open;unlockrefuses sub-floor costs (weak-kdf), and costs or sizes aboveVAULT_LIMITS(scrypt N ≤ 2^20, r ≤ 16, p ≤ 4; PBKDF2 ≤ 5 000 000 iterations; 1 000 secrets, 32 passkeys, 4 MiB) before any KDF runs (unsupported-envelope). Writes to the envelope are serialized (oneVaultper storage area), so parallel imports keep every secret. v1 envelopes (PBKDF2-SHA256, ≥ 600 000 iterations) still open and are re-sealed as v2. Passwords shorter than 8 characters are refused by the vault itself. Wrong password and tampering are the same failure (wrong-password).createnever overwrites; reset is an explicitdestroy(). - Passkeys. A passkey wraps the same DEK under HKDF-SHA256(PRF output, salt = credential id); the password stays the recovery path.
- Re-keying.
changePasswordre-wraps the same DEK, andremovePasskeydoes not take back a DEK the passkey already unwrapped. After a suspected compromise,rotateVaultKey(password)makes a fresh DEK, re-seals every secret, drops the passkeys (register them again) and writes one envelope; hand the returned DEK tosession.unlock, which replaces the old one.setSecretrefuses a DEK that does not open the vault’s secrets. - Unlocked state. Only the DEK is kept, in
chrome.storage.session(memory-only, trusted contexts), with an expiry enforced on every read and an alarm that locks; key buffers this package owns are zeroized after use.unlock/lock/touch/getKeyrun one at a time, so activity racing a lock never brings the key back. - Errors to pages.
toRelayError(e, { log })passes the wallet’s own refusals (EIP-1193 codes) and a node’s JSON-RPC error (code, revert data) with URLs removed; anything else is-32603 Internal erroron the page and goes tolog. - Origins. The content script relays only what the page window itself
posted (
pageRequestFrom:event.sourceis the page,event.originits origin), so an ad or embed in an iframe cannot speak as the site. The service worker answers a relay message only when the content script’s stamped origin equalsport.sender.origin. Permissions are keyed by that origin, never by a URL a page or a WalletConnect peer claims. - Requests. Everything that reveals accounts or moves funds goes through
the approval queue; blind signing (
eth_sign) is not offered; typed data for another chain, deployments and site-originated EIP-7702 delegations are refused; read-only RPC goes to the wallet’s own endpoints. - Network. The kit makes no requests of its own beyond the RPC / provider calls the wallet wires in; the phishing list is data the wallet ships.
What stays the wallet’s job: the extension-page CSP (script-src 'self', no
remote images), a host allowlist for the worker’s fetch, isolating third-party
network SDKs (WalletConnect, Omniston) in a sandbox page, the reproducible
release build and the store account.
Using the chrome adapters
Section titled “Using the chrome adapters”import { Vault, Session, PermissionStore, NetworksTable, ApprovalQueue, RequestRouter, chromeLocalVaultStorage, chromeSessionStorage, chromeAlarms, chromeJsonStorage, chromeKeyringStorage, vaultSecretStorage, checkSenderOrigin, isRelayRequest, PORT_NAME,} from '@anywalletkit/extension-kit'
// service workerconst vault = new Vault({ storage: chromeLocalVaultStorage(chrome.storage.local) })const session = new Session({ storage: chromeSessionStorage(chrome.storage.session), alarms: chromeAlarms(chrome.alarms) })
// onboardingconst dek = await vault.create(password) // refuses if a vault exists or the password is too shortawait vault.setSecret(dek, 'main', utf8(seedPhrase))await session.unlock(dek) // dek is zeroized here
// @anywalletkit/keyring on top of the vaultconst keyringStorage = chromeKeyringStorage(chrome.storage.local)const secrets = vaultSecretStorage(vault, session) // VaultError('locked') once the session expired
// the dApp sideconst permissions = new PermissionStore(chromeJsonStorage(chrome.storage.local, 'my-wallet.permissions'))const networks = new NetworksTable(chromeJsonStorage(chrome.storage.local, 'my-wallet.networks'))const approvals = new ApprovalQueue(myApprovalWindow) // chrome.windows.create({ type: 'popup' }) behind ApprovalWindowconst router = new RequestRouter({ permissions, networks, approvals, accounts, rpc, emit, sign })
chrome.runtime.onConnect.addListener((port) => { if (port.name !== PORT_NAME) return port.onMessage.addListener(async (msg) => { if (!isRelayRequest(msg)) return const origin = checkSenderOrigin(port.sender, msg.origin) if (!origin) return // forged origin: dropped, not answered const result = await router.handle(origin, msg.chain, msg.method, msg.params) // post a RelayResponse { kind: 'response', id: msg.id, … } back on the port })})memoryVaultStorage, memorySessionStorage, memoryAlarms,
memoryJsonStorage and memoryApprovalWindow are the in-memory twins for tests.
Classes
Section titled “Classes”- ApprovalQueue
- NetworksTable
- OriginRefused
- PermissionStore
- PhishingDetector
- QueueLimitError
- RecipientBook
- RequestRouter
- ReviewDeclined
- ReviewGate
- RouterError
- Session
- SwapsStore
- SwapTracker
- TonConnectError
- Vault
- VaultError
Interfaces
Section titled “Interfaces”- AddChainApproval
- AlarmsLike
- ApprovalQueueOptions
- ApprovalRequest
- ApprovalWindow
- AssertionExpectations
- AssertionInput
- ChainDescriptor
- ConnectApproval
- EarnApproval
- EarnPlan
- EarnReviewMeta
- EarnTx
- EvmNetwork
- EvmSigner
- HostCheck
- JsonStorage
- OriginPermission
- OriginVia
- PageEvent
- PageMessageEvent
- PageRequest
- PageResponse
- PageWindow
- PhishingList
- PortSender
- PreparedTx
- QuoteRoutesOptions
- RelayError
- RelayErrorOptions
- RelayEvent
- RelayRequest
- RelayResponse
- RouteListView
- RouterDeps
- RouteView
- SealedBox
- SessionAlarms
- SessionOptions
- SessionStorage
- SignApproval
- SignDeps
- SiweContext
- SiweInfo
- SiweMessage
- StorageAreaLike
- SwapApproval
- SwapRecord
- SwapReviewMeta
- SwapTrackerDeps
- SwitchChainApproval
- TonAddressParts
- TonConnectMessage
- TonConnectRequest
- TonConnectSendTransaction
- TypedData
- VaultEnvelopeV1
- VaultEnvelopeV2
- VaultKdfInfo
- VaultOptions
- VaultPasskey
- VaultStorage
Type Aliases
Section titled “Type Aliases”- ApprovalKind
- AssertionVerdict
- ChainAccountsKey
- GatedOp
- GateMatcher
- PermissionsState
- RecipientsState
- RelayChain
- RelayToContent
- RelayToWorker
- SignRequestInfo
- SwapRecordStatus
- TonSignDataPayload
- VaultEnvelope
- VaultErrorCode
Variables
Section titled “Variables”- ACTIVE_STATUSES
- APPROVALS_MAX_PENDING_PER_ORIGIN
- BTC_CHAIN
- BTC_ESPLORA
- CHAIN_ACCOUNT_KEYS
- DEFAULT_CHAIN
- DEFAULT_NETWORKS
- FLOOR_TOLERANCE_BPS
- KEYRING_STORAGE_KEY
- LOOKALIKE_PREFIX
- LOOKALIKE_SUFFIX
- NEAR_CHAIN
- NEAR_PROVIDERS
- NON_EVM_CHAINS
- PAGE_TARGET
- PORT_NAME
- READ_METHODS
- RELAY_ERRORS
- RELAY_METHOD_MAX
- SESSION_DEFAULT_AUTOLOCK_MS
- SIGNING_METHODS
- SOLANA_CHAIN
- SOLANA_RPCS
- TON_CHAIN
- TON_ENDPOINTS
- TONCONNECT_ERRORS
- TONCONNECT_MAX_MESSAGES
- TRON_CHAIN
- TRON_PROVIDERS
- VAULT_DEFAULT_ITERATIONS
- VAULT_LIMITS
- VAULT_MIN_ITERATIONS
- VAULT_MIN_PASSWORD_LENGTH
- VAULT_MIN_SCRYPT
- VAULT_SCRYPT
- VAULT_STORAGE_KEY
- ZCASH_BLOCKCHAIR
- ZCASH_CHAIN
Functions
Section titled “Functions”- approvalChallenge
- caip2FromChainId
- chainHex
- chainTxMatcher
- checkSenderOrigin
- chromeAlarms
- chromeJsonStorage
- chromeKeyringStorage
- chromeLocalVaultStorage
- chromeSessionStorage
- classifySignRequest
- createSignHandler
- decodedMessage
- derToRaw
- describeSiwe
- earnApprovalPayload
- earnPlanMatchers
- encodeApproveCalldata
- evmCallMatcher
- executionFloor
- fromBase64Url
- hostOf
- isCatalogChain
- isEnvelopeV1
- isEnvelopeV2
- isLocalHost
- isPageEvent
- isPageRequest
- isPageResponse
- isRelayMethod
- isRelayRequest
- isRelayToContent
- levenshtein
- lookalikeOf
- memoryAlarms
- memoryApprovalWindow
- memoryJsonStorage
- memorySessionStorage
- memoryVaultStorage
- messageText
- newRequestId
- nonEvmChain
- normalizeHost
- pageRequestFrom
- parseAddChainParams
- parseConnectRequest
- parseRawAddress
- parseSendTransaction
- parseSignData
- parseSiwe
- parseSwapId
- planMatchers
- prepareTransaction
- proofDomain
- quoteRoutes
- rank
- rawToDer
- relayError
- reviewEarnPlan
- reviewSwapPlan
- siweProblems
- stampOrigin
- swapApprovalPayload
- toBase64Url
- tonConnectErrorCode
- tonProofMessage
- tonSignDataMessage
- toRelayError
- typedDataMatcher
- vaultSecretStorage
- verifyAssertion