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