@anywalletkit/extension-kit 0.1.2 → 0.1.4
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 +12 -3
- package/dist/index.cjs +194 -67
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -24
- package/dist/index.d.ts +65 -24
- package/dist/index.js +194 -67
- package/dist/index.js.map +1 -1
- package/package.json +10 -7
package/README.md
CHANGED
|
@@ -33,11 +33,11 @@ pnpm add @anywalletkit/extension-kit @anywalletkit/core @anywalletkit/keyring
|
|
|
33
33
|
|
|
34
34
|
| Area | Exports | What |
|
|
35
35
|
| --- | --- | --- |
|
|
36
|
-
| 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 |
|
|
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
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 |
|
|
40
|
-
| 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 |
|
|
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 |
|
|
43
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 |
|
|
@@ -57,7 +57,11 @@ pnpm add @anywalletkit/extension-kit @anywalletkit/core @anywalletkit/keyring
|
|
|
57
57
|
AES-256-GCM wraps a random 32-byte DEK; the DEK seals each secret under AAD
|
|
58
58
|
`forgewallet-vault/secret/<id>` (boxes cannot be swapped). The DEK box's AAD
|
|
59
59
|
binds the exact KDF parameters, so an envelope whose salt or cost was edited
|
|
60
|
-
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
|
|
61
65
|
envelopes (PBKDF2-SHA256, ≥ 600 000 iterations) still open and are re-sealed
|
|
62
66
|
as v2. Passwords shorter than 8 characters are refused by the vault itself.
|
|
63
67
|
Wrong password and tampering are the same failure (`wrong-password`).
|
|
@@ -67,6 +71,11 @@ pnpm add @anywalletkit/extension-kit @anywalletkit/core @anywalletkit/keyring
|
|
|
67
71
|
- **Unlocked state.** Only the DEK is kept, in `chrome.storage.session`
|
|
68
72
|
(memory-only, trusted contexts), with an expiry enforced on every read and an
|
|
69
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`.
|
|
70
79
|
- **Origins.** The service worker answers a relay message only when the
|
|
71
80
|
content script's stamped origin equals `port.sender.origin`. Permissions are
|
|
72
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). */
|
|
@@ -288,27 +316,30 @@ var Vault = class {
|
|
|
288
316
|
* Register a passkey as a second unlock path: the DEK (from the unlocked session)
|
|
289
317
|
* is sealed under HKDF-SHA256(`prf`, salt = credential id). The password stays
|
|
290
318
|
* the recovery path; `prf` is zeroized here. A v1 envelope must be unlocked (and
|
|
291
|
-
* thereby upgraded) first.
|
|
319
|
+
* thereby upgraded) first, and the vault must hold a secret (it proves the DEK).
|
|
292
320
|
*/
|
|
293
321
|
async addPasskey(dek, passkey) {
|
|
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) {
|
|
@@ -380,11 +433,14 @@ var Vault = class {
|
|
|
380
433
|
const kek = await this._deriveScrypt(password, salt, kdf);
|
|
381
434
|
return { v: 2, kdf, dek: await this._seal(kek, dek, aadDekV2(kdf)), secrets, passkeys, createdAt };
|
|
382
435
|
}
|
|
383
|
-
/**
|
|
436
|
+
/**
|
|
437
|
+
* The caller's DEK must open this envelope's secrets — it is the key the password protects, not
|
|
438
|
+
* any 32 bytes. An empty vault has nothing to prove it with, so it is refused rather than trusted.
|
|
439
|
+
*/
|
|
384
440
|
async _requireDek(dek, envelope) {
|
|
385
441
|
const ids = Object.keys(envelope.secrets);
|
|
386
442
|
const key = await this._importDek(dek);
|
|
387
|
-
if (ids.length === 0)
|
|
443
|
+
if (ids.length === 0) throw new VaultError("Add a secret to the vault before a passkey: an empty vault cannot prove the key is its own.", "invalid-input");
|
|
388
444
|
const probe = await this._open(key, envelope.secrets[ids[0]], aadSecret(ids[0]), "locked");
|
|
389
445
|
(0, import_core.zeroize)(probe);
|
|
390
446
|
}
|
|
@@ -438,6 +494,8 @@ var Vault = class {
|
|
|
438
494
|
const envelope = await this._storage.get();
|
|
439
495
|
if (!envelope) throw new VaultError("No vault in this storage area.", "missing");
|
|
440
496
|
if (!isEnvelopeV1(envelope) && !isEnvelopeV2(envelope)) throw new VaultError("Unsupported vault envelope.", "unsupported-envelope");
|
|
497
|
+
const problem = envelopeProblem(envelope);
|
|
498
|
+
if (problem) throw new VaultError(`The vault envelope is outside the limits this build opens (${problem}).`, "unsupported-envelope");
|
|
441
499
|
return envelope;
|
|
442
500
|
}
|
|
443
501
|
};
|
|
@@ -456,6 +514,22 @@ function isEnvelopeV2(x) {
|
|
|
456
514
|
function isBox(x) {
|
|
457
515
|
return typeof x === "object" && x !== null && typeof x.iv === "string" && typeof x.data === "string";
|
|
458
516
|
}
|
|
517
|
+
function isBoundedScrypt(c) {
|
|
518
|
+
return c.N <= VAULT_LIMITS.scrypt.N && c.r <= VAULT_LIMITS.scrypt.r && c.p <= VAULT_LIMITS.scrypt.p;
|
|
519
|
+
}
|
|
520
|
+
function envelopeProblem(e) {
|
|
521
|
+
if (e.v === 1 && e.kdf.iterations > VAULT_LIMITS.iterations) return `PBKDF2 iterations ${e.kdf.iterations} > ${VAULT_LIMITS.iterations}`;
|
|
522
|
+
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}`;
|
|
523
|
+
const secrets = Object.values(e.secrets);
|
|
524
|
+
if (secrets.length > VAULT_LIMITS.secrets) return `${secrets.length} secrets > ${VAULT_LIMITS.secrets}`;
|
|
525
|
+
const passkeys = e.v === 2 ? e.passkeys : [];
|
|
526
|
+
if (passkeys.length > VAULT_LIMITS.passkeys) return `${passkeys.length} passkeys > ${VAULT_LIMITS.passkeys}`;
|
|
527
|
+
for (const box of [e.dek, ...secrets, ...passkeys.map((p) => p.dek)]) {
|
|
528
|
+
if (box.iv.length > VAULT_LIMITS.boxChars || box.data.length > VAULT_LIMITS.boxChars) return `a sealed box above ${VAULT_LIMITS.boxChars} characters`;
|
|
529
|
+
}
|
|
530
|
+
if (JSON.stringify(e).length > VAULT_LIMITS.envelopeChars) return `envelope above ${VAULT_LIMITS.envelopeChars} characters`;
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
459
533
|
function isStrongScrypt(c) {
|
|
460
534
|
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
535
|
}
|
|
@@ -500,6 +574,13 @@ var Session = class {
|
|
|
500
574
|
_alarm;
|
|
501
575
|
_now;
|
|
502
576
|
_onLock = /* @__PURE__ */ new Set();
|
|
577
|
+
/**
|
|
578
|
+
* Every operation on the stored record runs here, one after another: a `touch` that read the
|
|
579
|
+
* record before a `lock` removed it would otherwise write it back and undo the lock.
|
|
580
|
+
*/
|
|
581
|
+
_queue = Promise.resolve();
|
|
582
|
+
/** Bumped by every lock; a `touch` writes only when no lock happened since it read the record. */
|
|
583
|
+
_epoch = 0;
|
|
503
584
|
constructor(options) {
|
|
504
585
|
this._storage = options.storage;
|
|
505
586
|
this._alarms = options.alarms;
|
|
@@ -515,20 +596,19 @@ var Session = class {
|
|
|
515
596
|
/** Store the DEK and arm the auto-lock. The caller's copy is zeroized. */
|
|
516
597
|
async unlock(dek) {
|
|
517
598
|
if (dek.length !== 32) throw new VaultError("Vault key must be 32 bytes.", "invalid-input");
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
599
|
+
return this._serial(async () => {
|
|
600
|
+
const expiresAt = this._expiry();
|
|
601
|
+
await this._storage.set(this._key, { key: toBase64(dek), expiresAt });
|
|
602
|
+
(0, import_core2.zeroize)(dek);
|
|
603
|
+
await this._arm(expiresAt);
|
|
604
|
+
});
|
|
522
605
|
}
|
|
523
606
|
/** Forget the DEK and cancel the alarm. Safe to call when already locked. */
|
|
524
607
|
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();
|
|
608
|
+
return this._serial(() => this._lockNow());
|
|
529
609
|
}
|
|
530
610
|
async isUnlocked() {
|
|
531
|
-
return await this._read() !== null;
|
|
611
|
+
return this._serial(async () => await this._read() !== null);
|
|
532
612
|
}
|
|
533
613
|
/**
|
|
534
614
|
* The DEK for one operation (zeroize it after). `locked` when there is none or the
|
|
@@ -536,17 +616,22 @@ var Session = class {
|
|
|
536
616
|
* a missed alarm cannot keep the key alive.
|
|
537
617
|
*/
|
|
538
618
|
async getKey() {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
619
|
+
return this._serial(async () => {
|
|
620
|
+
const record = await this._read();
|
|
621
|
+
if (!record) throw new VaultError("Wallet is locked.", "locked");
|
|
622
|
+
return fromBase64(record.key);
|
|
623
|
+
});
|
|
542
624
|
}
|
|
543
625
|
/** User activity: push the auto-lock out again. No-op when locked. */
|
|
544
626
|
async touch() {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
627
|
+
return this._serial(async () => {
|
|
628
|
+
const epoch = this._epoch;
|
|
629
|
+
const record = await this._read();
|
|
630
|
+
if (!record || epoch !== this._epoch) return;
|
|
631
|
+
const expiresAt = this._expiry();
|
|
632
|
+
await this._storage.set(this._key, { ...record, expiresAt });
|
|
633
|
+
await this._arm(expiresAt);
|
|
634
|
+
});
|
|
550
635
|
}
|
|
551
636
|
get autoLockMs() {
|
|
552
637
|
return this._autoLockMs;
|
|
@@ -561,6 +646,19 @@ var Session = class {
|
|
|
561
646
|
this._onLock.add(callback);
|
|
562
647
|
return () => this._onLock.delete(callback);
|
|
563
648
|
}
|
|
649
|
+
_serial(fn) {
|
|
650
|
+
const run = this._queue.then(fn);
|
|
651
|
+
this._queue = run.catch(() => void 0);
|
|
652
|
+
return run;
|
|
653
|
+
}
|
|
654
|
+
/** The lock itself; callers are already in the queue. */
|
|
655
|
+
async _lockNow() {
|
|
656
|
+
this._epoch++;
|
|
657
|
+
const was = await this._storage.get(this._key);
|
|
658
|
+
await this._storage.remove(this._key);
|
|
659
|
+
await this._alarms.clear(this._alarm);
|
|
660
|
+
if (was !== void 0 && was !== null) for (const cb of this._onLock) cb();
|
|
661
|
+
}
|
|
564
662
|
_expiry() {
|
|
565
663
|
return this._autoLockMs > 0 ? this._now() + this._autoLockMs : Number.POSITIVE_INFINITY;
|
|
566
664
|
}
|
|
@@ -572,7 +670,7 @@ var Session = class {
|
|
|
572
670
|
const raw = await this._storage.get(this._key);
|
|
573
671
|
if (!raw || typeof raw.key !== "string" || typeof raw.expiresAt !== "number") return null;
|
|
574
672
|
if (raw.expiresAt <= this._now()) {
|
|
575
|
-
await this.
|
|
673
|
+
await this._lockNow();
|
|
576
674
|
return null;
|
|
577
675
|
}
|
|
578
676
|
return raw;
|
|
@@ -1219,6 +1317,7 @@ function memoryApprovalWindow() {
|
|
|
1219
1317
|
}
|
|
1220
1318
|
|
|
1221
1319
|
// src/router.ts
|
|
1320
|
+
var import_core4 = require("@anywalletkit/core");
|
|
1222
1321
|
var RouterError = class _RouterError extends Error {
|
|
1223
1322
|
code;
|
|
1224
1323
|
data;
|
|
@@ -1232,11 +1331,35 @@ var RouterError = class _RouterError extends Error {
|
|
|
1232
1331
|
return new _RouterError(e.code, e.message, e.data);
|
|
1233
1332
|
}
|
|
1234
1333
|
};
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1334
|
+
var PROVIDER_CODES = /* @__PURE__ */ new Set([4001, 4100, 4200, 4900, 4901, 4902]);
|
|
1335
|
+
var isNodeCode = (code) => typeof code === "number" && Number.isInteger(code) && (code === 3 || code >= -32768 && code <= -32e3);
|
|
1336
|
+
var URL_RE = /\b[a-z][a-z0-9+.-]*:\/\/[^\s"'<>()]+/gi;
|
|
1337
|
+
var pageText = (text) => text.replace(URL_RE, "[url]").split("\n")[0].slice(0, 300);
|
|
1338
|
+
var isHexData = (v) => typeof v === "string" && /^0x[0-9a-fA-F]*$/.test(v) && v.length <= 2 + 2 * 8192;
|
|
1339
|
+
function nodeError(e) {
|
|
1340
|
+
for (let cur = e, depth = 0; cur && typeof cur === "object" && depth < 8; cur = cur.cause, depth++) {
|
|
1341
|
+
const o = cur;
|
|
1342
|
+
if (o.name === "RpcRequestError" && isNodeCode(o.code)) {
|
|
1343
|
+
const text = typeof o.details === "string" && o.details ? o.details : typeof o.shortMessage === "string" ? o.shortMessage : "RPC error";
|
|
1344
|
+
return { code: o.code, message: pageText(text), ...isHexData(o.data) ? { data: o.data } : {} };
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
if (!(e instanceof Error) && typeof e === "object" && e !== null) {
|
|
1348
|
+
const o = e;
|
|
1349
|
+
if (isNodeCode(o.code) && typeof o.message === "string") return { code: o.code, message: pageText(o.message), ...isHexData(o.data) ? { data: o.data } : {} };
|
|
1350
|
+
}
|
|
1351
|
+
return void 0;
|
|
1352
|
+
}
|
|
1353
|
+
function toRelayError(e, options = {}) {
|
|
1354
|
+
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 };
|
|
1355
|
+
if (e instanceof ReviewDeclined) return { code: e.code, message: pageText(e.message) };
|
|
1356
|
+
if (e instanceof import_core4.UserRejectedError) return { code: RELAY_ERRORS.userRejected, message: pageText(e.message) };
|
|
1357
|
+
const node = nodeError(e);
|
|
1358
|
+
if (node) return node;
|
|
1359
|
+
const code = e?.code;
|
|
1360
|
+
if (e instanceof Error && typeof code === "number" && PROVIDER_CODES.has(code)) return { code, message: pageText(e.message) };
|
|
1361
|
+
options.log?.(e);
|
|
1362
|
+
return { code: RELAY_ERRORS.internal, message: "Internal error" };
|
|
1240
1363
|
}
|
|
1241
1364
|
var READ_METHODS = /* @__PURE__ */ new Set([
|
|
1242
1365
|
"eth_blockNumber",
|
|
@@ -1489,6 +1612,10 @@ function describeSiwe(text, ctx) {
|
|
|
1489
1612
|
}
|
|
1490
1613
|
|
|
1491
1614
|
// src/signing.ts
|
|
1615
|
+
var safeNonce = (n) => {
|
|
1616
|
+
if (n < 0n || n > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`The RPC returned an impossible nonce (${n}).`);
|
|
1617
|
+
return Number(n.toString());
|
|
1618
|
+
};
|
|
1492
1619
|
var ADDRESS = /^0x[0-9a-fA-F]{40}$/;
|
|
1493
1620
|
var HEX = /^0x([0-9a-fA-F]{2})*$/;
|
|
1494
1621
|
var lc = (s) => s.toLowerCase();
|
|
@@ -1584,13 +1711,13 @@ async function prepareTransaction(deps, network, from, tx) {
|
|
|
1584
1711
|
const base = block?.baseFeePerGas != null ? big(block.baseFeePerGas) : null;
|
|
1585
1712
|
if (base === null) {
|
|
1586
1713
|
const gasPrice = big(await deps.rpc(network, "eth_gasPrice", []));
|
|
1587
|
-
return { chainId: network.chainId, nonce:
|
|
1714
|
+
return { chainId: network.chainId, nonce: safeNonce(big(nonceHex)), gasLimit: hex(gasLimit), gasPrice: hex(gasPrice), maxFee: (gasLimit * gasPrice).toString() };
|
|
1588
1715
|
}
|
|
1589
1716
|
const floor = deps.tipFloor?.(network.chainId) ?? MIN_TIP_WEI;
|
|
1590
1717
|
const nodeTip = await deps.rpc(network, "eth_maxPriorityFeePerGas", []).then(big).catch(() => 0n);
|
|
1591
1718
|
const tip = nodeTip > floor ? nodeTip : floor;
|
|
1592
1719
|
const maxFeePerGas = base * 2n + tip;
|
|
1593
|
-
return { chainId: network.chainId, nonce:
|
|
1720
|
+
return { chainId: network.chainId, nonce: safeNonce(big(nonceHex)), gasLimit: hex(gasLimit), maxFeePerGas: hex(maxFeePerGas), maxPriorityFeePerGas: hex(tip), maxFee: (gasLimit * maxFeePerGas).toString() };
|
|
1594
1721
|
}
|
|
1595
1722
|
function createSignHandler(deps) {
|
|
1596
1723
|
return async ({ origin, network, method, params, accounts }) => {
|