Compare commits

...
Author SHA1 Message Date
clawbot f8f2461354 fix: a popup reload no longer logs the requests it cancels, except on the transaction detail and confirmation screens (closes #475)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
The transaction lists and ENS name lookups on the address and token
screens, the address scan after a wallet is created, the endpoint checks
in Settings, the wait screen's receipt check, the Send screen's Max fee
estimate and the token lookup on both add-token screens now check the
signal the popup aborts on pagehide before reporting a failed request.
scanForAddresses(), resolveEnsNames() and lookupTokenInfo() take the
signal.

End-to-end tests reload the popup on the address screen and during the
address scan with their requests held. Jest tests show each of these
reports a real failure and stays silent once the popup has closed. The
transaction detail and confirmation screens are left out: they discard
the popup context that carries the signal.

Model: opus-5-5
2026-10-06 12:34:54 +00:00
clawbot 329e64a54d fix: a popup reload mid-refresh no longer logs the requests it cancels (closes #218)
check / check (push) Waiting to run
e2e / e2e-chrome (push) Waiting to run
e2e / e2e-firefox (push) Waiting to run
Chrome cancels a closing popup's open requests just after pagehide, and in
the page a cancelled fetch() fails with the same "Failed to fetch" as a
server that cannot be reached. The home screen's transaction list and the
balance refresh logged an error for each, and the e2e suite failed on them.

The popup now aborts an AbortController on pagehide, and those failure
reports check its signal first. End-to-end tests reload the popup with
Blockscout held and require nothing logged, and fail the transaction list
for real and require the failure reported; a unit test covers the balance
refresh. The address and token screens and the new-wallet address scan are
#475.

Model: opus-5-5
2026-10-06 09:43:09 +02:00
clawbot 88c79e7e05 chore: pin Tailwind 4.3.1, which drops module.register() (closes #355)
check / check (push) Successful in 2m44s
e2e / e2e-chrome (push) Successful in 5m39s
e2e / e2e-firefox (push) Successful in 3m43s
@tailwindcss/node 4.2.1, loaded by the Tailwind CLI, calls Node's
deprecated module.register() as it starts, so every make build on a
current Node printed the DEP0205 warning. 4.3.1 is the first release that
calls module.registerHooks() instead wherever Node has it.

tailwindcss and @tailwindcss/cli are pinned at exactly 4.3.1, and
yarn.lock carries the new versions of the packages they pull in. The
compiled CSS computes to the same values: it drops the unused .start and
.end rules and writes calc(var(--spacing) * 1) as var(--spacing).

Model: opus-5-5
2026-10-06 05:26:09 +02:00
clawbot 25ead55eac fix: make dev rebuilds dist/ on source changes (closes #332)
check / check (push) Successful in 1m17s
e2e / e2e-chrome (push) Successful in 4m10s
e2e / e2e-firefox (push) Successful in 51s
make dev passed --watch to a build.js that read no arguments, so it
built once and exited. build.js --watch now builds, then builds again
after every change to a file under src/, manifest/ or icons/, until
interrupted; any other argument fails. Each directory gets its own
watcher, because Node's recursive watch on Linux loses a file that an
editor saves by renaming a new copy over it. A watch build writes no
build receipt and cannot be verified; README.md and the Makefile say
so and point to make build.

Model: opus-5-5
2026-10-06 03:43:18 +02:00
clawbot a8452a1d46 chore: cap every CI job with timeout-minutes (closes #294)
check / check (push) Failing after 1s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
No workflow set timeout-minutes, so a hung build or browser held the
shared runner until the server's own limit, hours later. check now stops
at 10 minutes, e2e-firefox at 15 and e2e-chrome at 20: each is over two
and a half times the job's slowest cold-cache run. README "In CI" records
the measured times and the caps.

Model: opus-5-5
2026-10-06 01:09:05 +02:00
clawbot 3663f02bf5 chore: drop eth_chainId and net_version from PROXY_METHODS (closes #326)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 1s
handleRpc answers both methods itself before it reaches its proxy branch, so
the two list entries were never used and the list named two methods that are
never sent to the RPC endpoint. No other entry is answered earlier.

PROXY_METHODS is now exported from the background script so that
tests/proxyMethods.test.js can send every listed method from a page and fail
on any that does not reach the RPC endpoint.

Model: opus-5-5
2026-10-06 00:48:45 +02:00
clawbot e590b83df0 harden: drop 'unsafe-inline' from style-src (closes #328)
check / check (push) Failing after 3s
e2e / e2e-chrome (push) Failing after 3s
e2e / e2e-firefox (push) Failing after 2s
The popup's markup no longer carries style attributes. The 42 in
index.html and in the HTML the view helpers build are now Tailwind
classes, each computing to the value it replaced, so style-src is 'self'
in both manifests, pinned in tests/manifest.test.js.

The address dot's 16 colours are written out as whole classes, because
Tailwind builds only the classes it finds in the source. The Settings
debug well is shown and hidden with the hidden class, since clearing an
inline display no longer uncovers it. Two tests that found the colour dot
by its inline style now find it by its class. Script that sets
element.style is unaffected.

Model: opus-5-5
2026-10-05 15:26:06 +02:00
clawbot a0360a7874 chore: drop an AI vendor's tool directory from .prettierignore (closes #363)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 3s
.prettierignore listed an AI vendor's tool directory, the only such
name in the tree. The directory is not tracked, so the line ignored
nothing and removing it changes no formatting result.

Model: opus-5-5
2026-10-05 14:59:16 +02:00
clawbot 9776f62f28 test: drive WaitTx's timeout and failed-lookup exits end to end (closes #315)
check / check (push) Failing after 2s
e2e / e2e-chrome (push) Failing after 2s
e2e / e2e-firefox (push) Failing after 2s
Two Chrome end-to-end cases send ETH and leave the wait for its receipt
running. In one, lookups answer "no receipt" until the 60-second deadline
ends the wait with the timeout message. In the other, a new fixture switch
makes every receipt lookup fail, and the sixth failure in a row ends the
wait with the message naming the unreachable network. Both check the exact
message and that Done returns to the address screen. Both wait in real
time: Playwright's clock would apply to every later test, and backdating
the stored broadcast time races the popup's own save.

Model: opus-5-5
2026-10-05 14:26:06 +02:00
39 changed files with 1627 additions and 474 deletions
+3
View File
@@ -3,6 +3,9 @@ on: [push]
jobs:
check:
runs-on: ubuntu-latest
# Bounds script/cibuild, a cold-cache build included, so a hang frees
# the shared runner. README.md "In CI" has the measured times.
timeout-minutes: 10
steps:
# actions/checkout v4.2.2, 2026-02-22
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+8
View File
@@ -35,6 +35,10 @@ on: [push]
jobs:
e2e-chrome:
runs-on: ubuntu-latest
# Bounds the image build, a cold cache included, and both Chrome
# programs, so a hung browser frees the shared runner. README.md
# "In CI" has the measured times.
timeout-minutes: 20
steps:
# actions/checkout v4.2.2, 2026-02-22
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
@@ -42,6 +46,10 @@ jobs:
e2e-firefox:
runs-on: ubuntu-latest
# Bounds the image build, a cold cache included, and both Firefox
# programs, so a hung browser frees the shared runner. README.md
# "In CI" has the measured times.
timeout-minutes: 15
steps:
# actions/checkout v4.2.2, 2026-02-22
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
-1
View File
@@ -2,4 +2,3 @@ node_modules/
yarn.lock
dist/
release/
.claude/
+8
View File
@@ -107,6 +107,14 @@ vendor-blocklist:
clean:
@rm -rf dist/ release/
# Run the build make build runs, without the checks that follow it, then run it
# again after every change to a file under src/, manifest/ or icons/, until
# interrupted. A failed build is reported, may leave dist/ partly written, and
# watching carries on. It writes no build receipt, so nothing can verify what it
# leaves in dist/: anything handed on comes from make build. A release build
# unless AUTISTMASK_DEBUG=1 is exported. A change anywhere else, package.json
# and build.js included, starts no build, and a directory created while it runs
# is not watched; restart it after either.
dev:
@echo "Building in watch mode..."
@yarn run build --watch 2>&1
+50 -22
View File
@@ -306,7 +306,15 @@ The Makefile shims to those. It also carries a few targets that have no
debug build, and keeping its `dist/` on failure (see
[Debug Builds](#debug-builds))
- `make clean` — remove `dist/` and `release/`
- `make dev` — build in watch mode
- `make dev` — run the build `make build` runs, without the checks that follow
it, then run it again after every change to a file under `src/`, `manifest/`
or `icons/`, until interrupted. A failed build is reported, may leave `dist/`
partly written, and watching carries on. It writes no build receipt, so
nothing can verify what it leaves in `dist/` (see
[Build Receipts](#build-receipts)): anything handed on comes from
`make build`. A release build unless `AUTISTMASK_DEBUG=1` is exported. A
change anywhere else, `package.json` and `build.js` included, starts no build,
and a directory created while it runs is not watched; restart it after either
## End-to-End Tests
@@ -376,6 +384,12 @@ reserve while sitting on the same side of the estimate, so swapping the two in
what [#154](https://git.eeqj.de/sneak/AutistMask/issues/154) was, and it was
previously correct by reading only.
It also covers both ways the wait for a sent transaction's receipt ends on the
error screen: lookups that still find no receipt 60 seconds after the broadcast,
and six lookups in a row that fail. Each must show its own message, and Done
must lead back to the address screen. Both wait in real time, about a minute
each.
It also covers the **dApp approval round trips** — the one place where the
content script, the inpage provider, the background worker and the approval
popup all have to work together. A local test page is served by the route
@@ -637,10 +651,20 @@ Nothing in either job can pass vacuously. There is no `continue-on-error` and no
build fails, and when the browser fails to start; the Chrome harness aborts the
suite outright if its network interception is not in effect.
Measured on this repo's runner: `e2e-chrome` about 1m55s cold, almost all of it
the one-time pull of the pinned ~800MB Playwright layer, and well under a minute
once that layer is cached. `e2e-firefox` about 1m05s cold, and it caches its
Firefox and geckodriver downloads the same way.
Measured on this repo's runner in the green runs of early October 2026, from a
warm docker cache to a cold one: `check` 49s to 3m37s, `e2e-chrome` 1m44s to
4m48s, and `e2e-firefox` 31s to 4m07s. A cold cache adds three to four minutes
to each job, spent rebuilding its image: reinstalling dependencies and, for
`e2e-firefox`, installing Firefox, geckodriver and their system libraries. Those
`e2e-chrome` runs predate the cases that wait in real time for a receipt to end
in error. `make test-e2e` now takes 3m51s locally with its image cached, so a
cold `e2e-chrome` run comes to about seven minutes.
Every job has a `timeout-minutes` cap, so a hung build or browser ends the job
instead of holding the shared runner: `check` 10 minutes, `e2e-firefox` 15 and
`e2e-chrome` 20, each over two and a half times the job's slowest cold run. A
job that reaches its cap has hung; read it as a hang, not as a slow run to
retry.
### Element id guard (part of `make check`)
@@ -806,13 +830,15 @@ discoverable.
on critical screens and when space is available to allow users to disambiguate
addresses visually, as a security feature.
- **Tailwind CSS**: Utility-first CSS via Tailwind. No custom CSS classes for
styling. Tailwind is configured with a minimal monochrome palette. This keeps
the styling co-located with the markup and eliminates CSS file management. The
handful of classes in `styles/main.css` are not styling: `.copy-flash-*`
carries the copy feedback animation, and `.am-address` carries the rule that
an address never wraps. Both are invariants that hold in every place they
appear, and spelling either out as repeated utilities is how one of those
places drifts away from the rest.
styling, and no `style="..."` attributes, which the
[Content Security Policy](#content-security-policy) refuses. Tailwind is
configured with a minimal monochrome palette. This keeps the styling
co-located with the markup and eliminates CSS file management. The handful of
classes in `styles/main.css` are not styling: `.copy-flash-*` carries the copy
feedback animation, and `.am-address` carries the rule that an address never
wraps. Both are invariants that hold in every place they appear, and spelling
either out as repeated utilities is how one of those places drifts away from
the rest.
- **Vanilla JS**: No framework (React, Vue, Svelte, etc.). The popup UI is small
enough that vanilla JS with simple view switching is sufficient. A framework
would add bundle size, build complexity, and attack surface for no benefit at
@@ -2180,8 +2206,8 @@ Dev dependencies (not shipped in extension):
| Package | Version | License | Purpose |
| ------------------ | ------- | ------- | ------------------------- |
| `esbuild` | 0.27.3 | MIT | JS bundler (inlines deps) |
| `tailwindcss` | 4.2.1 | MIT | CSS compilation |
| `@tailwindcss/cli` | 4.2.1 | MIT | Tailwind CLI |
| `tailwindcss` | 4.3.1 | MIT | CSS compilation |
| `@tailwindcss/cli` | 4.3.1 | MIT | Tailwind CLI |
| `jest` | 30.2.0 | MIT | Test runner |
| `prettier` | 3.8.1 | MIT | Code formatter |
@@ -2205,7 +2231,7 @@ a bare string in `manifest/firefox.json` (MV2):
```
default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; object-src 'self';
style-src 'self' 'unsafe-inline'; img-src 'self' data:;
style-src 'self'; img-src 'self' data:;
connect-src 'self' https: http:; frame-src 'none'; form-action 'none';
base-uri 'none'
```
@@ -2217,15 +2243,17 @@ wallet's own UI. Escaping is the primary fix for that (see
`src/shared/html.js`); this is the second line, so an escape that does slip
cannot reach the network.
Four directives are looser than `'self'`, each for a reason that does not
`style-src 'self'` admits the stylesheet and nothing inline: both browsers
refuse a `style="..."` attribute and a `<style>` block. So the popup's markup,
in `src/popup/index.html` and in the HTML the view helpers build, carries
Tailwind classes and never a `style` attribute. Script that sets `element.style`
is not affected; that is how the views show and hide their error lines. An
inline style that slips in anyway is refused with a console error, which fails
both end-to-end suites.
These directives differ from a plain `'self'`, each for a reason that does not
generalise:
- `style-src 'unsafe-inline'` — `src/popup/index.html` and the view helpers set
presentation through `style="..."` attributes, which CSP blocks without this.
Chrome enforces `style-src` on attributes, not only on `<style>` blocks, and
Firefox has never implemented `style-src-attr`, so there is no narrower
spelling that works on both targets. It permits inline **style**; script stays
under `script-src`, which does not allow `'unsafe-inline'`.
- `img-src data:` — identicons are generated in the popup by
`ethereum-blockies-base64` and assigned to `img.src` as `data:` PNGs.
- `connect-src https: http:` — the RPC endpoint is user-configurable and a local
+91
View File
@@ -45,6 +45,97 @@ but the review is broader than any of them.
# Completed Steps
- 2026-10-06: Reloading or closing the popup no longer logs a request it cancels
as a failure in the transaction lists and ENS name lookups on the address and
token screens, the address scan after a wallet is created, the endpoint checks
in Settings, the wait screen's receipt check, the Send screen's Max fee
estimate, or the token lookup on the two add-token screens
([#475](https://git.eeqj.de/sneak/AutistMask/issues/475)). Each checks the
signal the popup aborts on `pagehide`
([#218](https://git.eeqj.de/sneak/AutistMask/issues/218)) before reporting a
failure; a real failure is still logged. `scanForAddresses()`,
`resolveEnsNames()` and `lookupTokenInfo()` take the signal. The e2e suite
reloads the popup on the address screen with Blockscout held, and during the
address scan with that scan held; jest tests cover each of these with the
popup open and closed. Left out: the transaction detail screen and the
confirmation screen (its fee estimate and its recipient checks), because they
discard the popup context that carries the signal.
- 2026-10-06: Reloading or closing the popup mid-refresh no longer logs the
requests that cancels as failures
([#218](https://git.eeqj.de/sneak/AutistMask/issues/218)). Chrome cancels a
closing page's open requests just after `pagehide`, and in the page a
cancelled `fetch()` fails with the same "Failed to fetch" as a server that
cannot be reached, so the home screen's transaction list and the balance
refresh logged an error for each, and the e2e suite failed on them. The popup
now aborts an `AbortController` on `pagehide`, and those two check its signal
before reporting a failure. New e2e tests reload the popup with Blockscout
held and require nothing logged, and fail the transaction list for real and
require the failure reported; `tests/balanceRefreshCancelled.test.js` covers
the balance refresh. The address and token screens and the address scan after
a new wallet still log a cancelled request
([#475](https://git.eeqj.de/sneak/AutistMask/issues/475)).
- 2026-10-05: `make build` no longer prints the Node `DEP0205`
`module.register()` deprecation warning
([#355](https://git.eeqj.de/sneak/AutistMask/issues/355)). The call came from
`@tailwindcss/node`, which the Tailwind CLI loads; `tailwindcss` and
`@tailwindcss/cli` are now pinned at 4.3.1, the first release that uses
`module.registerHooks()` where Node has it. The compiled CSS computes to the
same values; it drops the unused `.start` and `.end` rules and writes
`calc(var(--spacing) * 1)` as `var(--spacing)` and `calc(var(--spacing) * 0)`
as `0`.
- 2026-10-05: `make dev` watches
([#332](https://git.eeqj.de/sneak/AutistMask/issues/332)). It used to pass
`--watch` to a `build.js` that read no arguments, so it built once and exited.
`build.js --watch` now builds, then builds again after every change to a file
under `src/`, `manifest/` or `icons/`; any other argument fails. It watches
each directory rather than using Node's recursive watch, which on Linux stops
seeing a file an editor saves by renaming a new copy over it. It writes no
build receipt, so nothing can verify what it builds; `make build` remains the
way to produce a `dist/` to hand on. `tests/buildWatch.test.js` covers the
watch loop against a temp directory.
- 2026-10-05: Every CI job has a `timeout-minutes` cap
([#294](https://git.eeqj.de/sneak/AutistMask/issues/294)): `check` 10 minutes,
`e2e-firefox` 15 and `e2e-chrome` 20, each over two and a half times the job's
slowest cold-cache run. A hung build or browser now ends its job instead of
holding the shared runner for hours.
- 2026-10-05: `PROXY_METHODS` in `src/background/index.js` no longer lists
`eth_chainId` and `net_version`
([#326](https://git.eeqj.de/sneak/AutistMask/issues/326)). `handleRpc` answers
both itself before its proxy branch, so the list named two methods that are
never sent to the RPC endpoint. No other entry is answered earlier.
`tests/proxyMethods.test.js` sends every listed method from a page and fails
on any that does not reach the RPC endpoint.
- 2026-10-05: The popup's Content Security Policy no longer allows inline style
([#328](https://git.eeqj.de/sneak/AutistMask/issues/328)): `style-src` is
`'self'` in both manifests, pinned in `tests/manifest.test.js`. The 42
`style="..."` attributes in `src/popup/index.html` and in the markup the view
helpers build are now Tailwind classes, each computing to the value it
replaced. The 16 address dot colours are written out as whole classes, because
Tailwind builds only the classes it finds in the source. The Settings debug
well is shown and hidden with the `hidden` class, since clearing an inline
`display` no longer uncovers it. Script that sets `element.style` is
unaffected.
- 2026-10-05: `.prettierignore` no longer lists an AI vendor's tool directory
([#363](https://git.eeqj.de/sneak/AutistMask/issues/363)). The directory is
not tracked, so the line ignored nothing.
- 2026-10-05: The Chrome end-to-end suite drives both ways the wait for a
transaction's receipt ends on the error screen
([#315](https://git.eeqj.de/sneak/AutistMask/issues/315)): lookups that still
find no receipt 60 seconds after the broadcast end it with the timeout
message, and six lookups that fail in a row end it with the message naming the
unreachable network. Done then returns to the address screen. Both cases wait
in real time, about a minute each. Playwright's clock would apply to every
later test in the run and cannot be removed, and moving the stored broadcast
time back can be undone by the save the popup makes every ten seconds.
- 2026-10-05: The Chrome end-to-end suite covers the last of the
[#150](https://git.eeqj.de/sneak/AutistMask/issues/150) and
[#151](https://git.eeqj.de/sneak/AutistMask/issues/151) items
+95 -15
View File
@@ -15,6 +15,15 @@ const DIST_CHROME = path.join(DIST, "chrome");
const DIST_FIREFOX = path.join(DIST, "firefox");
const SRC = path.join(__dirname, "src");
// What `make dev` watches: the directories whose files build() bundles,
// compiles or copies. A change anywhere else, package.json and build.js
// included, starts no build; restart make dev after one.
const WATCHED_DIRS = [
SRC,
path.join(__dirname, "manifest"),
path.join(__dirname, "icons"),
];
// The module whose compiled DEBUG state script/verify-build asserts. Which
// bundles contain it is derived from esbuild's own dependency graph rather
// than from a hardcoded list, so it tracks the bundle layout instead of
@@ -441,6 +450,10 @@ function getBuildInfo() {
async function build() {
console.log("Building AutistMask extension...");
// Under make dev this runs once per rebuild in the same process, and each
// build accounts only for what it emits itself.
emittedFiles.length = 0;
const receiptPath = receiptTarget();
if (!receiptPath) {
console.warn(
@@ -597,27 +610,94 @@ async function build() {
console.log("Build complete: dist/chrome/ and dist/firefox/");
}
// Run only as a program. Required as a module — which is how
// tests/buildForbiddenInputs.test.js reaches the checks below — this file
// builds nothing and writes nothing.
if (require.main === module) {
build().catch((err) => {
console.error(
`Build failed: ${err && err.message ? err.message : err}`,
);
process.exit(1);
});
// make dev: build, then build again after every change under `dirs`, until
// interrupted. A failed build is reported and watching carries on, so a
// half-finished edit does not end it. A change that arrives while a build is
// running is not lost: it causes one more build as soon as that one finishes.
// A directory created after this starts is not watched until a restart.
//
// make dev sets no AUTISTMASK_BUILD_RECEIPT, so these builds write no receipt
// and nothing can verify what they leave in dist/.
//
// `rebuild` is build() everywhere but tests/buildWatch.test.js, which also
// closes the returned watchers.
function watch(dirs, rebuild) {
let building = false;
let changedAgain = false;
async function run() {
if (building) {
changedAgain = true;
return;
}
building = true;
do {
// Let the rest of one save's events arrive first, so that one
// save is one build.
await new Promise((resolve) => setTimeout(resolve, 100));
changedAgain = false;
try {
await rebuild();
} catch (err) {
console.error(
`Build failed: ${err && err.message ? err.message : err}`,
);
}
} while (changedAgain);
building = false;
console.log("Watching for changes (Ctrl-C to stop)...");
}
// One watcher per directory rather than fs.watch's recursive option: on
// Linux, Node's recursive watch watches each file, and stops seeing one
// that an editor saves by renaming a new copy over it. A directory's
// watcher reports every change to the files in it, however they are saved.
const subdirectories = dirs.flatMap((dir) =>
fs
.readdirSync(dir, { recursive: true, withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => path.join(entry.parentPath, entry.name)),
);
const watchers = [...dirs, ...subdirectories].map((dir) =>
fs.watch(dir, run),
);
run();
return watchers;
}
// Exported for tests/buildForbiddenInputs.test.js only. The prohibition these
// three functions enforce is the guarantee behind
// https://git.eeqj.de/sneak/AutistMask/issues/324, and `make check` does not
// run `make build` — so they are unit tested against synthetic metafiles
// rather than being exercised only by CI, where "it ran" is not "it works".
// Run only as a program. Required as a module — which is how
// tests/buildForbiddenInputs.test.js and tests/buildWatch.test.js reach the
// functions below — this file builds nothing and writes nothing.
if (require.main === module) {
const args = process.argv.slice(2);
// An argument this file does not know fails rather than being ignored.
if (args.length > 1 || (args.length === 1 && args[0] !== "--watch")) {
console.error("usage: node build.js [--watch]");
process.exit(2);
}
if (args[0] === "--watch") {
watch(WATCHED_DIRS, build);
} else {
build().catch((err) => {
console.error(
`Build failed: ${err && err.message ? err.message : err}`,
);
process.exit(1);
});
}
}
// Exported for tests only: watch() for tests/buildWatch.test.js, the rest for
// tests/buildForbiddenInputs.test.js. The prohibition those enforce is the
// guarantee behind https://git.eeqj.de/sneak/AutistMask/issues/324, and
// `make check` does not run `make build` — so they are unit tested against
// synthetic metafiles rather than being exercised only by CI, where "it ran"
// is not "it works".
module.exports = {
importChain,
newForbiddenRecord,
recordBundledInputs,
assertNoForbiddenInputs,
assertForbiddenTableCovered,
watch,
};
+1 -1
View File
@@ -7,7 +7,7 @@
"permissions": ["storage", "activeTab", "alarms"],
"host_permissions": ["<all_urls>"],
"content_security_policy": {
"extension_pages": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https: http:; frame-src 'none'; form-action 'none'; base-uri 'none'"
"extension_pages": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https: http:; frame-src 'none'; form-action 'none'; base-uri 'none'"
},
"icons": {
"16": "icons/icon16.png",
+1 -1
View File
@@ -4,7 +4,7 @@
"version": "0.1.0",
"description": "Minimal Ethereum wallet for Firefox",
"permissions": ["storage", "activeTab", "alarms", "<all_urls>"],
"content_security_policy": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self' https: http:; frame-src 'none'; form-action 'none'; base-uri 'none'",
"content_security_policy": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https: http:; frame-src 'none'; form-action 'none'; base-uri 'none'",
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
+2 -2
View File
@@ -15,14 +15,14 @@
},
"devDependencies": {
"@eslint/js": "10.0.1",
"@tailwindcss/cli": "^4.2.1",
"@tailwindcss/cli": "4.3.1",
"esbuild": "^0.27.3",
"eslint": "10.8.1",
"globals": "17.11.0",
"jest": "^30.2.0",
"playwright-core": "1.56.0",
"prettier": "^3.8.1",
"tailwindcss": "^4.2.1"
"tailwindcss": "4.3.1"
},
"dependencies": {
"ethereum-blockies-base64": "^1.0.2",
+5 -3
View File
@@ -741,11 +741,12 @@ async function handleConnectionRequest(origin) {
}
}
// Methods that are safe to proxy directly to the RPC node
// Methods that are safe to proxy directly to the RPC node. A method handleRpc
// answers before its proxy branch does not belong here: it would never reach
// the node. tests/proxyMethods.test.js sends every one of these.
const PROXY_METHODS = [
"eth_blockNumber",
"eth_call",
"eth_chainId",
"eth_estimateGas",
"eth_gasPrice",
"eth_getBalance",
@@ -759,7 +760,6 @@ const PROXY_METHODS = [
"eth_getTransactionReceipt",
"eth_maxPriorityFeePerGas",
"eth_sendRawTransaction",
"net_version",
"web3_clientVersion",
"eth_feeHistory",
"eth_getBlockTransactionCountByHash",
@@ -1802,3 +1802,5 @@ runtime.onMessage.addListener((msg, sender, sendResponse) => {
return false;
}
});
module.exports = { PROXY_METHODS };
+32 -83
View File
@@ -110,8 +110,7 @@
</div>
<div
id="add-wallet-phrase-warning"
class="text-xs mb-2 border border-border border-dashed p-2"
style="visibility: hidden"
class="text-xs mb-2 border border-border border-dashed p-2 invisible"
>
Write these words down and keep them safe. Anyone with
them can take your funds; if you lose them, your wallet
@@ -262,10 +261,7 @@
<!-- recent transactions across all addresses -->
<div>
<div
class="font-bold bg-section py-1 px-2"
style="margin-left: -0.5rem; margin-right: -0.5rem"
>
<div class="font-bold bg-section py-1 px-2 -mx-2">
Recent Transactions
</div>
<div id="home-tx-list">
@@ -273,7 +269,7 @@
</div>
</div>
<div class="py-1" style="margin: 0 -0.5rem">&nbsp;</div>
<div class="py-1 -mx-2">&nbsp;</div>
<div class="text-xs text-muted">
<span
@@ -409,8 +405,7 @@
</p>
<div
id="export-privkey-flash"
class="text-xs mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs mb-2 min-h-[1.25rem] invisible"
></div>
<div id="export-privkey-password-section" class="mb-2">
<label class="block mb-1">Password</label>
@@ -542,8 +537,7 @@
/>
<div
id="send-to-error"
class="text-xs"
style="min-height: 1.25rem; color: #cc0000"
class="text-xs min-h-[1.25rem] text-[#cc0000]"
></div>
</div>
<div class="mb-2">
@@ -619,7 +613,7 @@
<div class="text-xs text-muted mb-1">Your balance</div>
<div id="confirm-balance" class="text-xs"></div>
</div>
<div id="confirm-fee" class="mb-3" style="visibility: hidden">
<div id="confirm-fee" class="mb-3 invisible">
<div class="text-xs text-muted mb-1">Network fee</div>
<div id="confirm-fee-amount" class="text-xs"></div>
<!-- Holds its one line of space from the first paint, so
@@ -627,22 +621,13 @@
nothing. The placeholder is never seen. -->
<div
id="confirm-fee-reserve"
class="text-xs text-muted"
style="visibility: hidden"
class="text-xs text-muted invisible"
>
reserve pending
</div>
</div>
<div
id="confirm-warnings"
class="mb-2"
style="visibility: hidden"
></div>
<div
id="confirm-recipient-warning"
class="mb-2"
style="visibility: hidden"
>
<div id="confirm-warnings" class="mb-2 invisible"></div>
<div id="confirm-recipient-warning" class="mb-2 invisible">
<div
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
>
@@ -655,14 +640,9 @@
in confirmTx.js sets it. -->
<div
id="confirm-contract-warning"
class="mb-2 border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
style="visibility: hidden"
class="mb-2 border border-red-500 border-dashed p-2 text-xs font-bold text-red-500 invisible"
></div>
<div
id="confirm-burn-warning"
class="mb-2"
style="visibility: hidden"
>
<div id="confirm-burn-warning" class="mb-2 invisible">
<div
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
>
@@ -670,11 +650,7 @@
here are permanently destroyed and cannot be recovered.
</div>
</div>
<div
id="confirm-etherscan-warning"
class="mb-2"
style="visibility: hidden"
>
<div id="confirm-etherscan-warning" class="mb-2 invisible">
<div
class="border border-red-500 border-dashed p-2 text-xs font-bold text-red-500"
>
@@ -684,13 +660,11 @@
</div>
<div
id="confirm-errors"
class="mb-2 border border-border border-dashed p-2"
style="visibility: hidden; min-height: 1.25rem"
class="mb-2 border border-border border-dashed p-2 invisible min-h-[1.25rem]"
></div>
<div
id="confirm-amount-fee-error"
class="mb-2 border border-border border-dashed p-2 text-xs"
style="visibility: hidden"
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
>
Your balance does not cover this amount plus the network
fee. Please go back and send a smaller amount.
@@ -699,15 +673,13 @@
in confirmTx.js sets it. -->
<div
id="confirm-gas-error"
class="mb-2 border border-border border-dashed p-2 text-xs"
style="visibility: hidden"
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
></div>
<!-- Its sentence names why the fee could not be estimated,
so show() in confirmTx.js sets it. -->
<div
id="confirm-fee-unknown-error"
class="mb-2 border border-border border-dashed p-2 text-xs"
style="visibility: hidden"
class="mb-2 border border-border border-dashed p-2 text-xs invisible"
></div>
<div class="mb-2">
<label class="block mb-1 text-xs">Password</label>
@@ -719,8 +691,7 @@
</div>
<div
id="confirm-tx-password-error"
class="text-xs mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs mb-2 min-h-[1.25rem] invisible"
></div>
<button
id="btn-confirm-send"
@@ -835,8 +806,7 @@
</button>
<div
id="receive-erc20-warning"
class="text-xs border border-border border-dashed p-2 mt-3"
style="visibility: hidden"
class="text-xs border border-border border-dashed p-2 mt-3 invisible"
></div>
</div>
@@ -864,8 +834,7 @@
</div>
<div
id="add-token-info"
class="text-xs text-muted mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-muted mb-2 min-h-[1.25rem] invisible"
></div>
<div class="mb-2">
<label class="block mb-1 text-xs text-muted"
@@ -1051,8 +1020,7 @@
type="text"
inputmode="numeric"
id="settings-dust-threshold"
class="border border-border p-1 text-xs bg-bg text-fg"
style="width: 10ch"
class="border border-border p-1 text-xs bg-bg text-fg w-[10ch]"
/>
<span class="text-xs text-muted">gwei</span>
</div>
@@ -1129,8 +1097,7 @@
<div
id="settings-debug-well"
class="bg-well p-3 mx-1 mb-3"
style="display: none"
class="bg-well p-3 mx-1 mb-3 hidden"
>
<h3 class="font-bold mb-1">Debug</h3>
<label
@@ -1158,8 +1125,7 @@
</p>
<div
id="delete-wallet-flash"
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
></div>
<div class="mb-2">
<label class="block mb-1">Password</label>
@@ -1232,8 +1198,7 @@
</div>
<div
id="delete-wallet-lost-flash"
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
></div>
<button
id="btn-delete-wallet-lost-confirm"
@@ -1288,8 +1253,7 @@
</p>
<div
id="delete-address-flash"
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
></div>
<button
id="btn-delete-address-confirm"
@@ -1318,8 +1282,7 @@
</div>
<div
id="show-phrase-flash"
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
></div>
<div id="show-phrase-password-section" class="mb-2">
<label class="block mb-1">Password</label>
@@ -1401,8 +1364,7 @@
/>
<div
id="settings-addtoken-info"
class="text-xs text-muted mt-1 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-muted mt-1 min-h-[1.25rem] invisible"
></div>
<button
id="btn-settings-addtoken-manual"
@@ -1635,8 +1597,7 @@
</div>
<div
id="approve-tx-error"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.875rem]"
style="visibility: hidden"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.875rem] invisible"
></div>
<div class="flex justify-between">
<button
@@ -1672,15 +1633,7 @@
<div
id="approve-sign-danger-warning"
class="mb-3 p-2 text-xs font-bold"
style="
visibility: hidden;
min-height: 1.25rem;
background: #fee2e2;
color: #991b1b;
border: 2px solid #dc2626;
border-radius: 6px;
"
class="mb-3 p-2 text-xs font-bold invisible min-h-[1.25rem] bg-[#fee2e2] text-[#991b1b] border-2 border-[#dc2626] rounded-[6px]"
></div>
<div class="mb-3">
@@ -1697,8 +1650,7 @@
<div class="text-xs text-muted mb-1">Message</div>
<div
id="approve-sign-message"
class="text-xs break-all"
style="max-height: 12rem; overflow-y: auto"
class="text-xs break-all max-h-48 overflow-y-auto"
></div>
</div>
@@ -1706,8 +1658,7 @@
<div class="text-xs text-muted mb-1">Raw data</div>
<div
id="approve-sign-hex"
class="text-xs break-all"
style="max-height: 6rem; overflow-y: auto"
class="text-xs break-all max-h-24 overflow-y-auto"
></div>
</div>
@@ -1721,8 +1672,7 @@
</div>
<div
id="approve-sign-error"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.875rem]"
style="visibility: hidden"
class="text-xs mb-2 border border-border border-dashed p-1 min-h-[1.875rem] invisible"
></div>
<div class="flex justify-between">
<button
@@ -1846,8 +1796,7 @@
</div>
<div
id="state-recovery-flash"
class="text-xs text-red-500 mb-2 min-h-[1.25rem]"
style="visibility: hidden"
class="text-xs text-red-500 mb-2 min-h-[1.25rem] invisible"
></div>
<button
id="btn-state-recovery-reset"
+10
View File
@@ -48,6 +48,14 @@ function renderWalletList() {
home.render(ctx);
}
// Aborted when the popup page goes away, closed or reloaded. Chrome then
// cancels the requests the page still has open, and a cancelled fetch() fails
// with the same "Failed to fetch" as a server that cannot be reached. pagehide
// fires first, so code that reports a failed request checks this and stays
// silent about one the page's own closing cancelled.
const pageClosed = new AbortController();
window.addEventListener("pagehide", () => pageClosed.abort());
let refreshInFlight = false;
// The ten-second refresh init() starts, stopped when the popup moves to the
@@ -66,6 +74,7 @@ async function doRefreshAndRender() {
state.blockscoutUrl,
state.trackedTokens,
state.networkId,
pageClosed.signal,
),
]);
state.lastBalanceRefresh = Date.now();
@@ -87,6 +96,7 @@ async function doRefreshAndRender() {
const ctx = {
renderWalletList,
doRefreshAndRender,
pageClosed: pageClosed.signal,
showAddWalletView: () => {
pushCurrentView();
addWallet.show();
+4
View File
@@ -51,6 +51,7 @@ function init(ctx) {
contractAddr,
state.rpcUrl,
state.networkId,
ctx.pageClosed,
);
log.infof("Adding token", info.symbol, contractAddr);
state.trackedTokens.push({
@@ -68,6 +69,9 @@ function init(ctx) {
}
require("./addressDetail").show();
} catch (e) {
// Cancelled by the popup closing, not failed: see pageClosed in
// src/popup/index.js.
if (ctx.pageClosed.aborted) return;
const detail = e.shortMessage || e.message || String(e);
log.errorf("Adding token failed for", contractAddr, detail);
// lookupTokenInfo() rejects a contract with a one-line message
+12 -2
View File
@@ -190,7 +190,12 @@ async function importMnemonic(ctx) {
// Scan for used HD addresses beyond index 0.
showFlash("Scanning for addresses...", 30000);
const scan = await scanForAddresses(xpub, state.rpcUrl, state.networkId);
const scan = await scanForAddresses(
xpub,
state.rpcUrl,
state.networkId,
ctx.pageClosed,
);
if (scan.addresses.length > 1) {
wallet.addresses = scan.addresses.map((a) => ({
address: a.address,
@@ -300,7 +305,12 @@ async function importXprvKey(ctx) {
// Scan for used HD addresses beyond index 0.
showFlash("Scanning for addresses...", 30000);
const scan = await scanForAddresses(xpub, state.rpcUrl, state.networkId);
const scan = await scanForAddresses(
xpub,
state.rpcUrl,
state.networkId,
ctx.pageClosed,
);
if (scan.addresses.length > 1) {
wallet.addresses = scan.addresses.map((a) => ({
address: a.address,
+6 -2
View File
@@ -135,6 +135,7 @@ async function loadTransactions(address) {
counterparties,
state.rpcUrl,
state.networkId,
ctx.pageClosed,
);
} catch {
ensNameMap = new Map();
@@ -143,6 +144,9 @@ async function loadTransactions(address) {
renderTransactions(txs);
} catch (e) {
// Cancelled by the popup closing, not failed: see pageClosed in
// src/popup/index.js.
if (ctx.pageClosed.aborted) return;
log.errorf("loadTransactions failed:", e.message);
$("tx-list").innerHTML =
'<div class="text-muted text-xs py-1">Failed to load transactions.</div>';
@@ -181,10 +185,10 @@ function renderTransactions(txs) {
// it on the line above rather than replacing it.
const nameStr = escapeHtml(title || ensName || "");
const err = tx.isError ? " (failed)" : "";
const opacity = tx.isError ? " opacity:0.5;" : "";
const opacity = tx.isError ? " opacity-50" : "";
const ago = escapeHtml(timeAgo(tx.timestamp));
const iso = escapeHtml(isoDate(tx.timestamp));
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover${opacity}" data-tx="${i}">`;
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
html += txCounterpartyHtml(counterparty, nameStr, amountStr);
html += `</div>`;
+6 -2
View File
@@ -219,6 +219,7 @@ async function loadTransactions(address, tokenId) {
counterparties,
state.rpcUrl,
state.networkId,
ctx.pageClosed,
);
} catch {
ensNameMap = new Map();
@@ -227,6 +228,9 @@ async function loadTransactions(address, tokenId) {
renderTransactions(txs);
} catch (e) {
// Cancelled by the popup closing, not failed: see pageClosed in
// src/popup/index.js.
if (ctx.pageClosed.aborted) return;
log.errorf("loadTransactions failed:", e.message);
$("address-token-tx-list").innerHTML =
'<div class="text-muted text-xs py-1">Failed to load transactions.</div>';
@@ -258,10 +262,10 @@ function renderTransactions(txs) {
// it on the line above rather than replacing it.
const nameStr = escapeHtml(title || ensName || "");
const err = tx.isError ? " (failed)" : "";
const opacity = tx.isError ? " opacity:0.5;" : "";
const opacity = tx.isError ? " opacity-50" : "";
const ago = escapeHtml(timeAgo(tx.timestamp));
const iso = escapeHtml(isoDate(tx.timestamp));
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
html += `<div class="tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover${opacity}" data-tx="${i}">`;
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
html += txCounterpartyHtml(counterparty, nameStr, amountStr);
html += `</div>`;
+23 -20
View File
@@ -335,7 +335,7 @@ function balanceLine(symbol, amount, price, tokenId) {
: "";
return (
`<div class="flex text-xs${clickClass}"${tokenAttr}>` +
`<span class="flex justify-between" style="width:42ch;max-width:100%">` +
`<span class="flex justify-between w-[42ch] max-w-full">` +
`<span>${escapeHtml(displaySymbol(symbol))}</span>` +
`<span>${qty}</span>` +
`</span>` +
@@ -430,23 +430,26 @@ function truncateMiddle(str, maxLen) {
// 16 colors evenly spaced around the hue wheel (22.5° apart),
// all at HSL saturation 70%, lightness 50% for uniform vibrancy.
// Each is a whole Tailwind class: Tailwind builds only the classes it finds
// written out in the source, so the class name cannot be put together at
// runtime.
const ADDRESS_COLORS = [
"#d92626",
"#d96926",
"#d9ac26",
"#c2d926",
"#80d926",
"#3dd926",
"#26d953",
"#26d996",
"#26d9d9",
"#2696d9",
"#2653d9",
"#3d26d9",
"#8026d9",
"#c226d9",
"#d926ac",
"#d92669",
"bg-[#d92626]",
"bg-[#d96926]",
"bg-[#d9ac26]",
"bg-[#c2d926]",
"bg-[#80d926]",
"bg-[#3dd926]",
"bg-[#26d953]",
"bg-[#26d996]",
"bg-[#26d9d9]",
"bg-[#2696d9]",
"bg-[#2653d9]",
"bg-[#3d26d9]",
"bg-[#8026d9]",
"bg-[#c226d9]",
"bg-[#d926ac]",
"bg-[#d92669]",
];
function addressColor(address) {
@@ -456,12 +459,12 @@ function addressColor(address) {
function addressDotHtml(address) {
const color = addressColor(address);
return `<span style="width:8px;height:8px;border-radius:50%;display:inline-block;background:${color};margin-right:4px;vertical-align:middle;flex-shrink:0;"></span>`;
return `<span class="inline-block w-[8px] h-[8px] rounded-[50%] ${color} mr-[4px] align-middle shrink-0"></span>`;
}
function blockieHtml(address) {
const src = makeBlockie(address);
return `<img src="${escapeHtml(src)}" width="48" height="48" style="image-rendering:pixelated;border-radius:50%;display:inline-block">`;
return `<img src="${escapeHtml(src)}" width="48" height="48" class="inline-block rounded-[50%] [image-rendering:pixelated]">`;
}
// Look up an address across all wallets and return its title
@@ -571,7 +574,7 @@ function timeAgo(timestamp) {
// Shared external-link icon SVG used across all views.
const EXT_ICON =
`<span style="display:inline-block;width:10px;height:10px;margin-left:4px;vertical-align:middle">` +
`<span class="inline-block w-[10px] h-[10px] ml-[4px] align-middle">` +
`<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5">` +
`<path d="M4.5 1.5H2a.5.5 0 00-.5.5v8a.5.5 0 00.5.5h8a.5.5 0 00.5-.5V7.5"/>` +
`<path d="M7 1.5h3.5V5M7 5.5L10.5 1.5"/>` +
+8 -5
View File
@@ -131,10 +131,10 @@ function renderHomeTxList(ctx) {
const title = addressTitle(counterparty, state.wallets);
const titleStr = title ? escapeHtml(title) : "";
const err = tx.isError ? " (failed)" : "";
const opacity = tx.isError ? " opacity:0.5;" : "";
const opacity = tx.isError ? " opacity-50" : "";
const ago = escapeHtml(timeAgo(tx.timestamp));
const iso = escapeHtml(isoDate(tx.timestamp));
html += `<div class="home-tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover" data-tx="${i}" style="${opacity}">`;
html += `<div class="home-tx-row py-2 border-b border-border-light text-xs cursor-pointer hover:bg-hover${opacity}" data-tx="${i}">`;
html += `<div class="flex justify-between"><span class="text-muted" title="${iso}">${ago}</span><span>${dirLabel}${err}</span></div>`;
html += txCounterpartyHtml(counterparty, titleStr, amountStr);
html += `</div>`;
@@ -225,6 +225,9 @@ async function loadHomeTxs(ctx) {
homeTxs = merged.slice(0, 25);
renderHomeTxList(ctx);
} catch (e) {
// Cancelled by the popup closing, not failed: see pageClosed in
// src/popup/index.js.
if (ctx.pageClosed.aborted) return;
log.errorf("loadHomeTxs failed:", e.message);
const list = $("home-tx-list");
if (list) {
@@ -241,7 +244,7 @@ function walletListHtml() {
state.wallets.forEach((wallet, wi) => {
const defect = walletDefect(wallet);
html += `<div>`;
html += `<div class="flex justify-between items-center bg-section py-1 px-2" style="margin:0 -0.5rem">`;
html += `<div class="flex justify-between items-center bg-section py-1 px-2 -mx-2">`;
html += `<span class="font-bold cursor-pointer wallet-name underline decoration-dashed" data-wallet="${wi}">${escapeHtml(wallet.name)}</span>`;
// No "+" on a defective wallet: deriving another address from that
// xpub would only add one more address the key does not produce
@@ -255,12 +258,12 @@ function walletListHtml() {
wallet.addresses.forEach((addr, ai) => {
html += `<div class="address-row py-1 border-b border-border-light cursor-pointer hover:bg-hover" data-wallet="${wi}" data-address="${ai}">`;
const isActive = state.activeAddress === addr.address;
const infoBtn = `<span class="btn-addr-info text-xs cursor-pointer border border-border hover:bg-fg hover:text-bg" style="padding:0" data-wallet="${wi}" data-address="${ai}">[info]</span>`;
const infoBtn = `<span class="btn-addr-info text-xs cursor-pointer border border-border hover:bg-fg hover:text-bg p-0" data-wallet="${wi}" data-address="${ai}">[info]</span>`;
// Only where a wallet can spare the address: a wallet holding a
// single address has no remove control, because its last address
// is never removable.
const removeBtn = canRemoveAddress(wallet)
? `<span class="btn-remove-address text-xs cursor-pointer border border-border hover:bg-fg hover:text-bg ml-1" style="padding:0" data-wallet="${wi}" data-address="${ai}" title="Remove this address from the wallet">[x]</span>`
? `<span class="btn-remove-address text-xs cursor-pointer border border-border hover:bg-fg hover:text-bg ml-1 p-0" data-wallet="${wi}" data-address="${ai}" title="Remove this address from the wallet">[x]</span>`
: "";
const dot = addressDotHtml(addr.address);
const titleBold = isActive ? "font-bold" : "";
+3
View File
@@ -292,6 +292,9 @@ async function fillMaxAmount() {
]);
feeWei = feeReserveWei(gasLimit, feeData);
} catch (e) {
// Cancelled by the popup closing, not failed: see pageClosed in
// src/popup/index.js.
if (ctx.pageClosed.aborted) return;
log.errorf(
"max amount fee estimate failed:",
e.shortMessage || e.message,
+6 -7
View File
@@ -213,12 +213,7 @@ function show() {
versionClickCount = 0;
// Show debug well if debug mode is already enabled
const debugWell = $("settings-debug-well");
if (state.debugMode) {
debugWell.style.display = "";
} else {
debugWell.style.display = "none";
}
$("settings-debug-well").classList.toggle("hidden", !state.debugMode);
$("settings-debug-mode").checked = state.debugMode;
showView("settings");
@@ -278,6 +273,9 @@ function init(ctx) {
return;
}
} catch {
// Cancelled by the popup closing, not failed: see pageClosed in
// src/popup/index.js.
if (ctx.pageClosed.aborted) return;
// Not the error's message: fetch puts the whole URL, password and
// key included, in the message of the error it throws for a URL
// with a user name and password or one it cannot parse.
@@ -304,6 +302,7 @@ function init(ctx) {
return;
}
} catch {
if (ctx.pageClosed.aborted) return;
// Not the error's message, as for the RPC check above.
log.errorf("Blockscout validation failed:", urlOrigin(url));
showFlash("Could not reach endpoint.");
@@ -434,7 +433,7 @@ function init(ctx) {
if (versionClickCount >= 10) {
versionClickCount = 0;
clearTimeout(versionClickTimer);
$("settings-debug-well").style.display = "";
$("settings-debug-well").classList.remove("hidden");
}
});
+4
View File
@@ -135,6 +135,7 @@ function init(_ctx) {
addr,
state.rpcUrl,
state.networkId,
ctx.pageClosed,
);
log.infof("Adding token", info.symbol, addr);
state.trackedTokens.push({
@@ -152,6 +153,9 @@ function init(_ctx) {
renderDropdown();
ctx.doRefreshAndRender();
} catch (e) {
// Cancelled by the popup closing, not failed: see pageClosed in
// src/popup/index.js.
if (ctx.pageClosed.aborted) return;
const detail = e.shortMessage || e.message || String(e);
log.errorf("Adding token failed for", addr, detail);
// lookupTokenInfo() rejects a contract with a one-line message
+3
View File
@@ -132,6 +132,9 @@ function startWait(txInfo, txHash, broadcastTime, pollNow) {
try {
receipt = await provider.getTransactionReceipt(txHash);
} catch (e) {
// Cancelled by the popup closing, not failed: see pageClosed in
// src/popup/index.js.
if (ctx.pageClosed.aborted) return;
// A thrown lookup means "no answer this tick", not "no
// receipt": the RPC failed, the chain said nothing. Declaring
// the timeout off it would report a confirmed transaction as
+40 -15
View File
@@ -87,7 +87,15 @@ function rawUnits(value) {
// way `holders` already is. Absence is never filled in here: this is the
// upstream of every screen that displays a token amount, so a value invented
// at this point is indistinguishable from a real one everywhere below it.
async function fetchTokenBalances(address, blockscoutUrl, trackedTokens) {
//
// `signal`, passed by the popup, is aborted when the popup closes; a request
// that fails after that was cancelled by the closing and is not logged.
async function fetchTokenBalances(
address,
blockscoutUrl,
trackedTokens,
signal,
) {
try {
const resp = await debugFetch(
blockscoutUrl + "/addresses/" + address + "/token-balances",
@@ -190,18 +198,22 @@ async function fetchTokenBalances(address, blockscoutUrl, trackedTokens) {
}
return balances;
} catch (e) {
log.errorf("fetchTokenBalances failed:", e.message);
if (!signal?.aborted) {
log.errorf("fetchTokenBalances failed:", e.message);
}
return null;
}
}
// Fetch ETH balances, ENS names, and ERC-20 token balances for all addresses.
// `signal` is as for fetchTokenBalances().
async function refreshBalances(
wallets,
rpcUrl,
blockscoutUrl,
trackedTokens,
networkId,
signal,
) {
log.debugf("refreshBalances start, rpc:", urlOrigin(rpcUrl));
const provider = getProvider(rpcUrl, networkId);
@@ -220,6 +232,7 @@ async function refreshBalances(
log.debugf("ETH balance", addr.address, addr.balance);
})
.catch((e) => {
if (signal?.aborted) return;
log.errorf(
"ETH balance failed",
addr.address,
@@ -243,6 +256,7 @@ async function refreshBalances(
);
})
.catch((e) => {
if (signal?.aborted) return;
log.errorf(
"ENS reverse failed",
addr.address,
@@ -258,6 +272,7 @@ async function refreshBalances(
addr.address,
blockscoutUrl,
trackedTokens,
signal,
).then((balances) => {
if (balances !== null) {
addr.tokenBalances = balances;
@@ -279,7 +294,8 @@ async function refreshBalances(
// Look up token metadata from its contract.
// Calls symbol() and decimals() to verify it implements ERC-20.
async function lookupTokenInfo(contractAddress, rpcUrl, networkId) {
// `signal` is as for fetchTokenBalances().
async function lookupTokenInfo(contractAddress, rpcUrl, networkId, signal) {
log.debugf("lookupTokenInfo", contractAddress, "rpc:", urlOrigin(rpcUrl));
const provider = getProvider(rpcUrl, networkId);
const contract = new Contract(contractAddress, ERC20_ABI, provider);
@@ -289,7 +305,9 @@ async function lookupTokenInfo(contractAddress, rpcUrl, networkId) {
symbol = await contract.symbol();
log.debugf("symbol() =", symbol);
} catch (e) {
log.errorf("symbol() failed:", e.shortMessage || e.message);
if (!signal?.aborted) {
log.errorf("symbol() failed:", e.shortMessage || e.message);
}
throw new Error("Not a valid ERC-20 token (symbol() failed).");
}
@@ -297,7 +315,9 @@ async function lookupTokenInfo(contractAddress, rpcUrl, networkId) {
decimals = await contract.decimals();
log.debugf("decimals() =", decimals);
} catch (e) {
log.errorf("decimals() failed:", e.shortMessage || e.message);
if (!signal?.aborted) {
log.errorf("decimals() failed:", e.shortMessage || e.message);
}
throw new Error("Not a valid ERC-20 token (decimals() failed).");
}
@@ -305,10 +325,12 @@ async function lookupTokenInfo(contractAddress, rpcUrl, networkId) {
name = await contract.name();
log.debugf("name() =", name);
} catch (e) {
log.warnf(
"name() failed, using symbol as name:",
e.shortMessage || e.message,
);
if (!signal?.aborted) {
log.warnf(
"name() failed, using symbol as name:",
e.shortMessage || e.message,
);
}
name = symbol;
}
@@ -324,7 +346,8 @@ async function lookupTokenInfo(contractAddress, rpcUrl, networkId) {
// Checks gapLimit addresses in parallel per batch. Stops when an entire
// batch has no used addresses (i.e. gapLimit consecutive empty addresses).
// Returns { addresses: [{ address, index }], nextIndex }.
async function scanForAddresses(xpub, rpcUrl, networkId, gapLimit = 5) {
// `signal` is as for fetchTokenBalances().
async function scanForAddresses(xpub, rpcUrl, networkId, signal, gapLimit = 5) {
log.debugf("scanForAddresses start, gapLimit:", gapLimit);
const provider = getProvider(rpcUrl, networkId);
const used = [];
@@ -347,11 +370,13 @@ async function scanForAddresses(xpub, rpcUrl, networkId, gapLimit = 5) {
]);
return { addr, index, isUsed: balance > 0n || txCount > 0 };
} catch (e) {
log.errorf(
"scanForAddresses check failed",
addr,
e.shortMessage || e.message,
);
if (!signal?.aborted) {
log.errorf(
"scanForAddresses check failed",
addr,
e.shortMessage || e.message,
);
}
return { addr, index, isUsed: false };
}
}),
+14 -8
View File
@@ -32,7 +32,8 @@ function setCache(address, name) {
localStorage.setItem(key, JSON.stringify({ name, ts: Date.now() }));
}
async function resolveEnsName(address, rpcUrl, networkId) {
// `signal` is as for fetchTokenBalances() in src/shared/balances.js.
async function resolveEnsName(address, rpcUrl, networkId, signal) {
const cached = getCached(address);
if (cached !== undefined) return cached;
@@ -42,21 +43,26 @@ async function resolveEnsName(address, rpcUrl, networkId) {
setCache(address, name);
return name;
} catch (e) {
log.errorf(
"ENS reverse lookup failed",
address,
e.shortMessage || e.message,
);
if (!signal?.aborted) {
log.errorf(
"ENS reverse lookup failed",
address,
e.shortMessage || e.message,
);
}
// Don't cache failures — let subsequent lookups retry
return null;
}
}
async function resolveEnsNames(addresses, rpcUrl, networkId) {
async function resolveEnsNames(addresses, rpcUrl, networkId, signal) {
const results = new Map();
await Promise.all(
addresses.map(async (addr) => {
results.set(addr, await resolveEnsName(addr, rpcUrl, networkId));
results.set(
addr,
await resolveEnsName(addr, rpcUrl, networkId, signal),
);
}),
);
return results;
+95
View File
@@ -0,0 +1,95 @@
// The balance refresh, and the address scan after a wallet is created, do not
// report a request the popup's own closing cancelled, and still report one
// that failed while the popup was open
// (https://git.eeqj.de/sneak/AutistMask/issues/218,
// https://git.eeqj.de/sneak/AutistMask/issues/475).
//
// In the popup a cancelled fetch() fails with the same "Failed to fetch" as a
// server that cannot be reached, so every request here fails that way, and
// only the signal the popup aborts on pagehide tells the two cases apart.
const { FetchRequest } = require("ethers");
const { refreshBalances, scanForAddresses } = require("../src/shared/balances");
const {
generateMnemonic,
hdWalletFromMnemonic,
} = require("../src/shared/wallet");
const RPC_URL = "https://rpc.example.invalid";
const EXPLORER_URL = "https://explorer.example.invalid/api/v2";
const ADDRESS = "0x1111111111111111111111111111111111111111";
const realFetch = globalThis.fetch;
let logged;
beforeEach(() => {
logged = [];
jest.spyOn(console, "error").mockImplementation((...args) => {
logged.push(args.map(String).join(" "));
});
const failedToFetch = async () => {
throw new TypeError("Failed to fetch");
};
// The RPC calls (ETH balance, ENS name, and the scan's balance and
// transaction count) and the explorer request (token balances) all fail
// the same way.
FetchRequest.registerGetUrl(failedToFetch);
globalThis.fetch = jest.fn(failedToFetch);
});
afterEach(() => {
FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc());
globalThis.fetch = realFetch;
jest.restoreAllMocks();
});
function refresh(signal) {
const wallets = [{ addresses: [{ address: ADDRESS }] }];
return refreshBalances(
wallets,
RPC_URL,
EXPLORER_URL,
[],
"mainnet",
signal,
);
}
test("a failure while the popup is open is reported", async () => {
await refresh(new AbortController().signal);
for (const label of [
"ETH balance failed",
"ENS reverse failed",
"fetchTokenBalances failed: Failed to fetch",
]) {
expect(logged.some((line) => line.includes(label))).toBe(true);
}
});
test("a failure once the popup has closed is not", async () => {
const pageClosed = new AbortController();
pageClosed.abort();
await refresh(pageClosed.signal);
expect(logged).toEqual([]);
});
function scan(signal) {
// What the scan found is logged at info level, which is not under test.
jest.spyOn(console, "log").mockImplementation(() => {});
const { xpub } = hdWalletFromMnemonic(generateMnemonic());
return scanForAddresses(xpub, RPC_URL, "mainnet", signal);
}
test("a scan failure while the popup is open is reported", async () => {
await scan(new AbortController().signal);
expect(
logged.some((line) => line.includes("scanForAddresses check failed")),
).toBe(true);
});
test("a scan failure once the popup has closed is not", async () => {
const pageClosed = new AbortController();
pageClosed.abort();
await scan(pageClosed.signal);
expect(logged).toEqual([]);
});
+104
View File
@@ -0,0 +1,104 @@
// `make dev` runs `node build.js --watch`
// (https://git.eeqj.de/sneak/AutistMask/issues/332). These drive build.js's
// watch() over a temp directory with a stand-in for build(), so nothing here
// builds or writes dist/.
const fs = require("fs");
const os = require("os");
const path = require("path");
const { watch } = require("../build");
let dir;
let watchers = [];
beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), "autistmask-watch-"));
fs.mkdirSync(path.join(dir, "nested"));
jest.spyOn(console, "log").mockImplementation(() => {});
jest.spyOn(console, "error").mockImplementation(() => {});
});
afterEach(() => {
for (const watcher of watchers) watcher.close();
watchers = [];
fs.rmSync(dir, { recursive: true, force: true });
jest.restoreAllMocks();
});
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Wait until `condition()` holds; fail the test if it has not within 3s.
async function until(condition) {
const deadline = Date.now() + 3000;
while (!condition()) {
if (Date.now() > deadline) throw new Error("timed out waiting");
await sleep(10);
}
}
// Save the way many editors do: write a new copy, then rename it over the
// original, so the file at that path is a different one afterwards.
function saveByRename(file, contents) {
fs.writeFileSync(`${file}.tmp`, contents);
fs.renameSync(`${file}.tmp`, file);
}
test("builds at start, then once per change to a file in a subdirectory", async () => {
const file = path.join(dir, "nested", "a.js");
fs.writeFileSync(file, "1");
let builds = 0;
watchers = watch([dir], async () => {
builds++;
});
await until(() => builds === 1);
fs.writeFileSync(file, "2");
await until(() => builds === 2);
await sleep(300);
expect(builds).toBe(2);
});
test("keeps seeing a file that is saved by renaming a new copy over it", async () => {
const file = path.join(dir, "nested", "a.js");
fs.writeFileSync(file, "1");
let builds = 0;
watchers = watch([dir], async () => {
builds++;
});
await until(() => builds === 1);
saveByRename(file, "2");
await until(() => builds === 2);
saveByRename(file, "3");
await until(() => builds === 3);
});
test("a failed build is reported and watching carries on", async () => {
let builds = 0;
watchers = watch([dir], async () => {
builds++;
if (builds === 1) throw new Error("unexpected token");
});
await until(() => console.error.mock.calls.length === 1);
expect(console.error).toHaveBeenCalledWith(
"Build failed: unexpected token",
);
fs.writeFileSync(path.join(dir, "a.js"), "1");
await until(() => builds === 2);
});
test("a change made while a build runs causes one more build after it", async () => {
let builds = 0;
watchers = watch([dir], async () => {
builds++;
if (builds === 1) {
fs.writeFileSync(path.join(dir, "a.js"), "1");
await sleep(200);
}
});
await until(() => builds === 2);
await sleep(300);
expect(builds).toBe(2);
});
+2 -2
View File
@@ -301,7 +301,7 @@ describe.each([
test("a contract creation's row says so, with no colour dot and no address line", async () => {
const html = await rowsFor(historyTx(""));
expect(html).toContain(SENTENCE);
expect(html).not.toContain("background:");
expect(html).not.toContain("bg-[#");
expect(html).not.toContain("am-address");
expect(html).not.toContain("undefined");
});
@@ -309,7 +309,7 @@ describe.each([
test("a transaction with a recipient shows its colour dot and address", async () => {
const html = await rowsFor(historyTx(RECIPIENT));
expectAddressLine(html);
expect(html).toContain("background:#");
expect(html).toContain("bg-[#");
expect(html).toContain(`<div class="am-address">${RECIPIENT}</div>`);
});
});
+37 -15
View File
@@ -418,27 +418,23 @@ function sleep(ms) {
const HOLD_POLL_MS = 25;
const HOLD_MAX_MS = 30000;
// Hold a gas estimate open for as long as the test asks.
// Hold a reply open for as long as the test asks: until opts[name] is false.
//
// opts.holdGasEstimate is read here rather than captured, so a test flips it
// on the same options object the route was registered with — the same
// pattern as seedTokenTransfer. This is the only way to observe the
// confirmation screen while its estimate is genuinely in flight; sampling
// the screen and hoping to win a race against the network would assert
// nothing on a slow machine.
// The switch (holdGasEstimate, holdTransactionCount or holdBlockscout) is read
// here rather than captured, so a test flips it on the same options object the
// route was registered with — the same pattern as seedTokenTransfer. This is
// the only way to observe a screen while its request is genuinely in flight;
// sampling the screen and hoping to win a race against the network would
// assert nothing on a slow machine.
//
// It never gives up quietly. A hold that outlives the bound is reported like
// any other harness fault, because a "pending" state that stopped being
// pending on its own is a green assertion about the wrong screen.
async function awaitRelease(opts, report) {
async function awaitRelease(opts, name, report) {
const started = Date.now();
while (opts.holdGasEstimate) {
while (opts[name]) {
if (Date.now() - started > HOLD_MAX_MS) {
report(
"held gas estimate was never released after " +
HOLD_MAX_MS +
"ms",
);
report(name + " was never released after " + HOLD_MAX_MS + "ms");
return;
}
await sleep(HOLD_POLL_MS);
@@ -460,6 +456,16 @@ function rpcReply(req, opts, report) {
return Object.assign(envelope, { result: ethCallResult(req, opts) });
}
if (req.method === "eth_getTransactionReceipt") {
// A lookup that fails, which the wait screen counts differently from
// one that answers "not mined yet" (README.md, WaitTx).
if (opts.failReceiptLookup) {
return Object.assign(envelope, {
error: {
code: -32000,
message: "e2e fixture: receipt lookup failed",
},
});
}
const hash = Array.isArray(req.params) ? req.params[0] : null;
return Object.assign(envelope, {
result: opts.seedReceipt && hash ? transactionReceipt(hash) : null,
@@ -552,7 +558,10 @@ async function handleRpc(route, postData, opts, report) {
return route.abort();
}
if (batch.some((req) => req.method === "eth_estimateGas")) {
await awaitRelease(opts, report);
await awaitRelease(opts, "holdGasEstimate", report);
}
if (batch.some((req) => req.method === "eth_getTransactionCount")) {
await awaitRelease(opts, "holdTransactionCount", report);
}
const replies = batch.map((req) => rpcReply(req, opts, report));
@@ -608,6 +617,12 @@ function traceEnabled(raw) {
* node-side refusal.
* @param {boolean} [opts.holdGasEstimate] hold every batch containing an
* eth_estimateGas until this is cleared again.
* @param {boolean} [opts.holdTransactionCount] hold every batch containing an
* eth_getTransactionCount until this is cleared again.
* @param {boolean} [opts.holdBlockscout] hold every Blockscout request until
* this is cleared again.
* @param {boolean} [opts.failTransactionList] fail every request for an
* address's transaction list as a network error; read at request time.
* @param {string[]} [opts.broadcastTransactions] every raw signed
* transaction handed to eth_sendRawTransaction, appended in order.
* @param {number|string|null} [opts.tokenDecimalsOverride] the scale
@@ -619,6 +634,8 @@ function traceEnabled(raw) {
* symbol is markup; read at request time.
* @param {boolean} [opts.seedReceipt] answer eth_getTransactionReceipt with a
* confirmed receipt instead of null, so a wait screen resolves.
* @param {boolean} [opts.failReceiptLookup] answer eth_getTransactionReceipt
* with an error, so every receipt lookup fails; read at request time.
* @returns {Promise<{waitForServiceWorkerTraffic: (ms: number) =>
* Promise<string|null>}>}
*/
@@ -681,7 +698,12 @@ async function installNetworkStubs(ctx, opts) {
// Blockscout v2
if (p.includes("/api/v2/")) {
await awaitRelease(opts, "holdBlockscout", report);
if (/\/addresses\/0x[0-9a-fA-F]{40}\/transactions$/.test(p)) {
// Aborted rather than answered with an error status: to the
// page this is a server that cannot be reached, and fetch()
// rejects with "Failed to fetch".
if (opts.failTransactionList) return route.abort();
const addr = blockscoutAddress(p);
return jsonResponse(route, {
items:
+185 -7
View File
@@ -249,8 +249,23 @@ test("the exact confirmation phrase erases the record and reloads into Welcome (
}
});
test("wallet creation through the UI reaches the main view", async (env) => {
env.phrase = await createWallet(env.page);
// The address scan that follows creating the wallet is held, so the popup
// reloads while it is in flight. As for the reloads mid-refresh further down,
// the assertion for that is the harness's own: a console.error from the page
// being reloaded fails this test (#475).
test("wallet creation through the UI reaches the main view, and reloading mid-scan reports no failure (#475)", async (env) => {
env.routeOpts.holdTransactionCount = true;
try {
const scanning = env.page.waitForRequest((r) =>
(r.postData() || "").includes("eth_getTransactionCount"),
);
env.phrase = await createWallet(env.page);
await scanning;
await env.page.reload();
} finally {
env.routeOpts.holdTransactionCount = false;
}
await visible(env.page, "#view-main");
assert(
env.phrase.split(/\s+/).length >= 12,
"wallet creation did not yield a recovery phrase",
@@ -311,7 +326,7 @@ test("transaction detail renders an ERC-20 transfer (#151)", async (env) => {
"token contract row missing the contract address, got: " +
JSON.stringify(contractText),
);
const dots = await contract.locator('span[style*="border-radius"]').count();
const dots = await contract.locator('span[class*="rounded-[50%]"]').count();
assert(dots > 0, "token contract row rendered without its colour dot");
});
@@ -696,6 +711,85 @@ test("the token contract row links to the explorer's token page (#151)", async (
);
});
// ------------------------------------------- reload mid-refresh (#218)
//
// Reloading or closing the popup makes Chrome cancel the requests it still has
// open, and in the page a cancelled fetch() fails with the same "Failed to
// fetch" as a server that cannot be reached. The reload during the address
// scan that follows creating a wallet is in the wallet creation test (#475).
// Blockscout is held, so the home screen's transaction list and token balances
// cannot have been answered when the popup reloads. The assertion is the
// harness's own: a console.error from the page being reloaded fails this test.
test("reloading the popup mid-refresh reports no failure (#218)", async (env) => {
await goHome(env.page);
env.routeOpts.holdBlockscout = true;
try {
const inFlight = Promise.all([
env.page.waitForRequest((r) => r.url().endsWith("/transactions")),
env.page.waitForRequest((r) => r.url().endsWith("/token-balances")),
]);
await env.page.reload();
await inFlight;
await env.page.reload();
} finally {
env.routeOpts.holdBlockscout = false;
}
await visible(env.page, "#view-main");
});
// The same "Failed to fetch" from a server that really cannot be reached is
// still reported, so it still fails the run unless a test declares it. Chrome
// reports the failed request itself as well, which a cancelled one does not.
test("a transaction list that cannot be fetched is still reported (#218)", async (env) => {
env.errors.expect(
"loadHomeTxs failed",
/loadHomeTxs failed: Failed to fetch/,
);
env.errors.expect(
"Chrome's report of the failed request",
/Failed to load resource: net::ERR_FAILED/,
);
env.routeOpts.failTransactionList = true;
try {
// Drawn again by the next ten-second refresh.
await env.page.waitForFunction(
() =>
document
.getElementById("home-tx-list")
.textContent.includes("Failed to load transactions."),
null,
{ timeout: 15000 },
);
} finally {
env.routeOpts.failTransactionList = false;
}
});
// The address screen's transaction list, held the same way. The popup reopens
// on the address screen, which asks for its list as it is drawn, so once the
// screen is up its request is in flight (#475).
test("reloading the popup on the address screen reports no failure (#475)", async (env) => {
await openAddressDetail(env.page);
await waitForPersisted(
env.page,
"currentView",
"address",
"before reloading the popup",
);
env.routeOpts.holdBlockscout = true;
try {
await env.page.reload();
await visible(env.page, "#view-address");
await env.page.reload();
} finally {
env.routeOpts.holdBlockscout = false;
}
await visible(env.page, "#view-address");
// Home again, where the tests below expect to start.
await goHome(env.page);
});
// -------------------------------------------- recovery phrase (#161)
// The gear toggles, so pressing it while Settings is already up leaves it.
@@ -2107,10 +2201,7 @@ function quantity(wei) {
// Wait on the main view until a changed balance fixture has been picked up.
//
// Deliberately not a reload: the popup re-refreshes on a 10-second timer by
// itself, and reloading aborts whatever fetch the home screen has open at
// that instant, which the extension reports through log.errorf and the
// harness — correctly — fails the run on.
// Not a reload: the popup re-refreshes on a 10-second timer by itself.
//
// It also deliberately settles on MAIN rather than on the address screen.
// The address screen builds the send screen's token dropdown once, from the
@@ -2799,6 +2890,84 @@ test("a token that lies about decimals() at signing time broadcasts nothing (#30
}
});
// ------------------------------ the wait for a receipt ending in error (#315)
//
// README.md (WaitTx) documents two ways the wait ends on the error screen: a
// lookup that answers "no receipt" 60 seconds or more after the broadcast, and
// six lookups in a row that fail. They are different facts with different
// messages, so each is driven to its own.
//
// Both wait in real time, about a minute each. The wait reads the popup's own
// clock and its own ten-second timer. Playwright's clock would move both, but
// it is installed on the whole browser context and cannot be removed, so every
// later test would run on it. Moving the stored broadcast time back instead can
// be undone by the save the popup makes every ten seconds.
// Send ETH from the address screen and stop on the wait for its receipt.
async function sendEthToWait(env) {
await goToConfirm(env.page, {
token: "ETH",
balance: FUNDED_ETH_TEXT + " ETH",
amount: COMFORTABLE_AMOUNT,
});
await waitForEstimate(env.page);
await fillPasswordAndSend(env.page);
await visible(env.page, "#view-wait-tx", 60000);
}
test("a wait still without a receipt after 60 seconds ends on the timeout message (#315)", async (env) => {
try {
await sendEthToWait(env);
// Lookups run every ten seconds and answer "no receipt", so the one
// that ends the wait comes about 60 seconds after the broadcast.
await visible(env.page, "#view-error-tx", 90000);
const message = (
await env.page.locator("#error-tx-message").innerText()
).trim();
assert(
message ===
"Transaction was not confirmed within 60 seconds. It may still confirm later — check Etherscan.",
"the wait did not end on the timeout message: " +
JSON.stringify(message),
);
await env.page.click("#btn-error-tx-done");
await visible(env.page, "#view-address");
} finally {
await backToAddressAfterSend(env);
}
});
test("six failed receipt lookups in a row end on the unreachable-network message (#315)", async (env) => {
// Each failed lookup is logged through log.errorf, i.e. console.error.
// Exactly six are declared: a wait that ended sooner leaves one unmatched,
// and one that went on logs a seventh, and either fails this test.
for (let i = 1; i <= 6; i++) {
env.errors.expect(
"failed receipt lookup " + i + " of 6",
/poll receipt failed/,
);
}
env.routeOpts.failReceiptLookup = true;
try {
await sendEthToWait(env);
await visible(env.page, "#view-error-tx", 90000);
const message = (
await env.page.locator("#error-tx-message").innerText()
).trim();
assert(
message ===
"The network could not be reached to check this transaction — 6 lookups failed in a row. Check the RPC URL in Settings. The transaction may still have confirmed — check Etherscan.",
"the wait did not end on the unreachable-network message: " +
JSON.stringify(message),
);
await env.page.click("#btn-error-tx-done");
await visible(env.page, "#view-address");
} finally {
env.routeOpts.failReceiptLookup = false;
await backToAddressAfterSend(env);
}
});
// ------------------------------------------- hostile token symbol (#307)
//
// The reproduction from the issue, in the real browser against the real
@@ -4392,6 +4561,13 @@ async function main() {
ethBalanceWei: null,
failGasEstimate: false,
holdGasEstimate: false,
// Whether a JSON-RPC batch asking for a transaction count is held
// unanswered: the address scan after creating a wallet (#475).
holdTransactionCount: false,
// Whether Blockscout requests are held unanswered, and whether an
// address's transaction list fails as a network error (#218).
holdBlockscout: false,
failTransactionList: false,
// What decimals() answers for the stub token, when it is to answer
// something other than the value the same fixture reports through
// Blockscout. The token that lies about its scale (#305).
@@ -4402,6 +4578,8 @@ async function main() {
// Whether eth_getTransactionReceipt confirms a transaction rather than
// answering "not mined yet".
seedReceipt: false,
// Whether eth_getTransactionReceipt fails instead of answering (#315).
failReceiptLookup: false,
// Every raw signed transaction handed to eth_sendRawTransaction, in
// order. The dApp transaction round trip asserts against these bytes
// rather than against anything the extension reported about them.
+1
View File
@@ -91,6 +91,7 @@ describe.each([
require("../src/popup/views/" + view).init({
doRefreshAndRender: () => {},
pageClosed: new AbortController().signal,
});
element(field).value = ADDRESS;
await element(button).listeners.click();
+7 -8
View File
@@ -21,15 +21,14 @@
// escaping in src/shared/html.js is the primary fix; default-src is what
// stops the next escape that slips from reaching the network.
//
// Every directive below is pinned exactly, because each of the four
// And for #328: style-src is 'self' alone, so the browser refuses every
// style="..." attribute in the popup's markup, including one an escape lets
// through. The popup styles with classes; script setting element.style is
// not affected.
//
// Every directive below is pinned exactly, because each of the three
// loosenings is load-bearing and none of them may grow:
//
// style-src 'unsafe-inline' src/popup/index.html and the view helpers
// use style="..." attributes throughout, which
// CSP blocks without it. Chrome enforces this
// on attributes, not just <style> blocks, and
// Firefox has never implemented style-src-attr,
// so there is no narrower spelling available.
// img-src data: blockies are data: PNGs assigned to img.src.
// connect-src https: http: the RPC endpoint is user-configurable, and a
// local node over http://127.0.0.1 is a
@@ -58,7 +57,7 @@ const EXPECTED_DIRECTIVES = {
"default-src": ["'self'"],
"script-src": ["'self'", "'wasm-unsafe-eval'"],
"object-src": ["'self'"],
"style-src": ["'self'", "'unsafe-inline'"],
"style-src": ["'self'"],
"img-src": ["'self'", "data:"],
"connect-src": ["'self'", "http:", "https:"],
"frame-src": ["'none'"],
+89
View File
@@ -0,0 +1,89 @@
// Every method in PROXY_METHODS is sent to the RPC node.
//
// handleRpc answers some methods itself before it reaches its proxy branch. A
// method listed in PROXY_METHODS but answered earlier never reaches the node,
// so the list would name a method that is not proxied
// (https://git.eeqj.de/sneak/AutistMask/issues/326). Each method is sent from
// a page here and must come back with what the node answered.
const { makeStorageStub } = require("./support/storageStub");
async function settle() {
for (let i = 0; i < 50; i++) await Promise.resolve();
}
afterEach(() => {
delete global.chrome;
delete global.fetch;
});
test("every method in PROXY_METHODS reaches the RPC node", async () => {
jest.resetModules();
jest.doMock("../src/shared/balances", () => ({
getProvider: () => ({}),
refreshBalances: jest.fn(async () => {}),
}));
jest.doMock("../src/shared/phishingDomains", () => ({
isPhishingDomain: () => false,
}));
jest.doMock("../src/shared/alarms", () => ({
BALANCE_REFRESH_ALARM: "balance",
BALANCE_REFRESH_PERIOD_MINUTES: 1,
ensureRecurringAlarms: jest.fn(async () => {}),
registerAlarmHandlers: jest.fn(),
}));
// The node answers each method with a value naming that method.
global.fetch = jest.fn(async (url, opts) => ({
status: 200,
json: async () => ({
jsonrpc: "2.0",
id: 1,
result: "node answered " + JSON.parse(opts.body).method,
}),
}));
let messageListener = null;
global.chrome = {
storage: makeStorageStub({
autistmask: {
networkId: "mainnet",
wallets: [],
allowedSites: {},
deniedSites: {},
},
}),
runtime: {
getURL: (path) => "chrome-extension://autistmask/" + path,
onMessage: {
addListener: (fn) => {
messageListener = fn;
},
},
onConnect: { addListener: () => {} },
lastError: null,
},
windows: { onRemoved: { addListener: () => {} } },
action: { setPopup: () => {} },
};
const { PROXY_METHODS } = require("../src/background/index");
const answers = {};
const expected = {};
for (const method of PROXY_METHODS) {
messageListener(
{ type: "AUTISTMASK_RPC", method, params: [] },
{ origin: "https://dapp.example" },
(r) => {
answers[method] = r;
},
);
await settle();
expected[method] = { result: "node answered " + method };
}
expect(PROXY_METHODS.length).toBeGreaterThan(0);
expect(answers).toEqual(expected);
});
+51 -2
View File
@@ -65,7 +65,7 @@ jest.mock("../src/shared/log", () => ({
debugf: () => {},
infof: () => {},
warnf: () => {},
errorf: () => {},
errorf: jest.fn(),
},
// The explorer's token list, which refreshBalances() also fetches.
debugFetch: jest.fn(async () => ({
@@ -143,6 +143,7 @@ const { Transaction, Wallet, formatEther } = require("ethers");
const { refreshBalances } = require("../src/shared/balances");
const { encryptWithPassword } = require("../src/shared/vault");
const { state } = require("../src/shared/state");
const { log } = require("../src/shared/log");
const send = require("../src/popup/views/send");
const confirmTx = require("../src/popup/views/confirmTx");
@@ -226,10 +227,16 @@ function tokenRow(value, decimals = "18") {
// The confirmation screen Review leads to, once shown.
let confirmed = null;
// Stands in for the popup's: aborting it is the popup closing.
let pageClosed;
// Open the Send screen for `token` ("ETH" or a token address), with the
// recipient entered.
function openSend(token = "ETH") {
send.init({ showConfirmTx: (info) => (confirmed = info) });
send.init({
showConfirmTx: (info) => (confirmed = info),
pageClosed: pageClosed.signal,
});
confirmTx.init({});
send.resetSendValidation();
state.currentView = "send";
@@ -260,6 +267,8 @@ function canSend() {
beforeEach(() => {
elements.clear();
log.errorf.mockClear();
pageClosed = new AbortController();
confirmed = null;
state.selectedToken = null;
state.trackedTokens = [];
@@ -447,6 +456,46 @@ describe("Max on an ETH send", () => {
expect(text("flash-msg")).toBe("");
});
// Holds the node's fee answer until the returned function fails it, as a
// node that cannot be reached, or a request the popup's closing
// cancelled, does.
function holdFailingFeeEstimate() {
let fail;
mockNode.feeData = new Promise((_, reject) => {
fail = () => reject(new TypeError("Failed to fetch"));
});
return fail;
}
test("reports a fee estimate that fails while the popup is open", async () => {
await refreshWith(BALANCE_WEI);
openSend();
const fail = holdFailingFeeEstimate();
const pressed = pressMax();
fail();
await pressed;
expect(log.errorf).toHaveBeenCalledWith(
"max amount fee estimate failed:",
"Failed to fetch",
);
expect(text("flash-msg")).toBe(
"The network fee could not be estimated.",
);
});
// https://git.eeqj.de/sneak/AutistMask/issues/475
test("does not report a fee estimate that fails once the popup has closed", async () => {
await refreshWith(BALANCE_WEI);
openSend();
const fail = holdFailingFeeEstimate();
const pressed = pressMax();
pageClosed.abort();
fail();
await pressed;
expect(log.errorf).not.toHaveBeenCalled();
expect(el("send-amount").value).toBe("");
});
test("fills in once the held fee estimate arrives with nothing changed", async () => {
await refreshWith(BALANCE_WEI);
openSend();
+23 -2
View File
@@ -50,7 +50,8 @@ function element(id) {
return (elements[id] ||= fakeElement());
}
function loadSettingsView() {
// `pageClosed` stands in for the popup's: aborting it is the popup closing.
function loadSettingsView(pageClosed = new AbortController()) {
elements = {};
flashes = [];
@@ -74,7 +75,9 @@ function loadSettingsView() {
state.blockscoutUrl = SAVED_BLOCKSCOUT;
require("../src/shared/log").setRuntimeDebug(true);
require("../src/popup/views/settings").init({});
require("../src/popup/views/settings").init({
pageClosed: pageClosed.signal,
});
}
async function save(fieldId, buttonId, typed) {
@@ -146,3 +149,21 @@ test("the Blockscout check of a URL with a user name and password", async () =>
const line = expectFailedWithoutSecrets("Blockscout validation failed");
expect(line).toContain("https://explorer.example.invalid");
});
// In the popup a check its own closing cancelled fails as these do, and is
// not reported (https://git.eeqj.de/sneak/AutistMask/issues/475). Nothing is
// saved either.
test("a check that fails once the popup has closed is not reported", async () => {
const pageClosed = new AbortController();
pageClosed.abort();
loadSettingsView(pageClosed);
await save("settings-rpc", "btn-save-rpc", RPC_UNPARSEABLE);
await save(
"settings-blockscout",
"btn-save-blockscout",
BLOCKSCOUT_WITH_PASSWORD,
);
expect(console.error).not.toHaveBeenCalled();
expect(state.rpcUrl).toBe(SAVED_RPC);
expect(state.blockscoutUrl).toBe(SAVED_BLOCKSCOUT);
});
+103
View File
@@ -0,0 +1,103 @@
// The two add-token screens do not report a token lookup the popup's own
// closing cancelled, and still report one that failed while the popup was open
// (https://git.eeqj.de/sneak/AutistMask/issues/475).
//
// In the popup a cancelled request fails with the same "Failed to fetch" as a
// server that cannot be reached, so every RPC request here fails that way, and
// only the signal the popup aborts on pagehide tells the two cases apart. The
// real lookupTokenInfo() runs, which logs a failed lookup itself before the
// screen does. Driven against the fake elements tests/flashLine.test.js uses.
const { FetchRequest } = require("ethers");
const ADDRESS = "0x1111111111111111111111111111111111111111";
let elements;
function fakeElement() {
return {
value: "",
textContent: "",
style: {},
listeners: {},
addEventListener(event, handler) {
this.listeners[event] = handler;
},
};
}
// Stands in for document.getElementById(): one fake element per id.
function element(id) {
return (elements[id] ||= fakeElement());
}
jest.doMock("../src/popup/views/helpers", () => ({
$: element,
showView: () => {},
showFlash: () => {},
escapeHtml: (s) => s,
goBack: () => {},
}));
// state.js reads chrome.storage.local at load.
globalThis.chrome = {
storage: { local: { get: async () => ({}), set: async () => {} } },
};
const { state } = require("../src/shared/state");
let logged;
beforeEach(() => {
elements = {};
logged = [];
state.trackedTokens = [];
jest.spyOn(console, "error").mockImplementation((...args) => {
logged.push(args.map(String).join(" "));
});
FetchRequest.registerGetUrl(async () => {
throw new TypeError("Failed to fetch");
});
});
afterEach(() => {
FetchRequest.registerGetUrl(FetchRequest.createGetUrlFunc());
jest.restoreAllMocks();
});
describe.each([
["addToken", "add-token-address", "btn-add-token-confirm"],
[
"settingsAddToken",
"settings-addtoken-address",
"btn-settings-addtoken-manual",
],
])("looking up a token on %s", (view, field, button) => {
// Clicks the screen's add button for ADDRESS and waits for the lookup.
async function add(pageClosed) {
require("../src/popup/views/" + view).init({
doRefreshAndRender: () => {},
pageClosed,
});
element(field).value = ADDRESS;
await element(button).listeners.click();
}
test("a failure while the popup is open is reported", async () => {
await add(new AbortController().signal);
expect(logged).toHaveLength(2);
expect(logged[0]).toContain("symbol() failed:");
expect(logged[1]).toBe(
"[AutistMask] Adding token failed for " +
ADDRESS +
" Not a valid ERC-20 token (symbol() failed).",
);
});
test("a failure once the popup has closed is not", async () => {
const pageClosed = new AbortController();
pageClosed.abort();
await add(pageClosed.signal);
expect(logged).toEqual([]);
});
});
+181
View File
@@ -0,0 +1,181 @@
// The address and token screens do not report a transaction list, or an ENS
// name lookup for the addresses in it, that the popup's own closing cancelled,
// and still report one that failed while the popup was open
// (https://git.eeqj.de/sneak/AutistMask/issues/475).
//
// In the popup a cancelled request fails with the same "Failed to fetch" as a
// server that cannot be reached, so the requests here fail that way, and only
// the signal the popup aborts on pagehide tells the two cases apart. Driven
// against a minimal DOM stub in the shape tests/timestampDisplay.test.js uses.
// The explorer answers with mockHistory, and fails when it is null.
let mockHistory = null;
jest.mock("../src/shared/transactions", () => ({
...jest.requireActual("../src/shared/transactions"),
fetchRecentTransactions: async () => {
if (mockHistory === null) throw new TypeError("Failed to fetch");
return mockHistory;
},
}));
// Every ENS name lookup fails.
jest.mock("../src/shared/balances", () => ({
...jest.requireActual("../src/shared/balances"),
getProvider: () => ({
lookupAddress: async () => {
throw new TypeError("Failed to fetch");
},
}),
}));
globalThis.chrome = {
storage: { local: { get: async () => ({}), set: async () => {} } },
};
const { state } = require("../src/shared/state");
const addressDetail = require("../src/popup/views/addressDetail");
const addressToken = require("../src/popup/views/addressToken");
const ADDRESS = "0x1111111111111111111111111111111111111111";
const RECIPIENT = "0x66133E8ea0f5D1d612D2502a968757D1048c214a";
// A transaction ADDRESS sent, as the history lists hold it.
function historyTx() {
return {
hash: "0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84",
from: ADDRESS,
to: RECIPIENT,
value: "0.0000",
exactValue: "0.0",
rawAmount: "0",
rawUnit: "wei",
symbol: "ETH",
timestamp: 1790000000,
isError: false,
directionLabel: "Sent",
direction: "sent",
contractAddress: null,
};
}
function makeElement(id) {
const el = {
id,
textContent: "",
value: "",
innerHTML: "",
style: {},
dataset: {},
classList: {
add: () => {},
remove: () => {},
contains: () => false,
toggle: () => false,
},
addEventListener: () => {},
querySelectorAll: () => [],
appendChild: () => {},
};
// Views reach for .parentElement to hide whole sections.
Object.defineProperty(el, "parentElement", {
get: () => node(id + "-parent"),
});
return el;
}
function makeDocument() {
const els = new Map();
return {
getElementById(id) {
// The debug banner is created on demand by helpers.js; absent
// is the state a non-debug, non-testnet popup is in.
if (id === "debug-banner") return null;
if (!els.has(id)) els.set(id, makeElement(id));
return els.get(id);
},
createElement: () => makeElement("created"),
addEventListener: () => {},
body: { prepend: () => {} },
};
}
function node(id) {
return globalThis.document.getElementById(id);
}
let logged;
beforeEach(() => {
mockHistory = null;
logged = [];
jest.spyOn(console, "error").mockImplementation((...args) => {
logged.push(args.map(String).join(" "));
});
globalThis.document = makeDocument();
globalThis.window = { location: { search: "" } };
state.wallets = [
{
name: "Main",
type: "key",
addresses: [{ address: ADDRESS, balance: "0.0000" }],
},
];
state.trackedTokens = [];
state.viewStack = [];
state.selectedWallet = 0;
state.selectedAddress = 0;
state.selectedToken = "ETH";
});
afterEach(() => {
jest.restoreAllMocks();
});
describe.each([
["the address screen", addressDetail, "tx-list"],
["the token screen", addressToken, "address-token-tx-list"],
])("the transaction list on %s", (_name, view, listId) => {
// Open the screen and wait for its transaction list to load or fail.
async function open(pageClosed) {
view.init({ pageClosed });
view.show();
await new Promise((resolve) => setTimeout(resolve, 0));
}
test("a failure while the popup is open is reported", async () => {
await open(new AbortController().signal);
expect(logged).toEqual([
"[AutistMask] loadTransactions failed: Failed to fetch",
]);
expect(node(listId).innerHTML).toContain(
"Failed to load transactions.",
);
});
test("a failure once the popup has closed is not", async () => {
const pageClosed = new AbortController();
pageClosed.abort();
await open(pageClosed.signal);
expect(logged).toEqual([]);
});
test("a name lookup that fails while the popup is open is reported", async () => {
mockHistory = [historyTx()];
await open(new AbortController().signal);
expect(logged).toContain(
"[AutistMask] ENS reverse lookup failed " +
RECIPIENT +
" Failed to fetch",
);
expect(node(listId).innerHTML).toContain("tx-row");
});
test("a name lookup that fails once the popup has closed is not", async () => {
mockHistory = [historyTx()];
const pageClosed = new AbortController();
pageClosed.abort();
await open(pageClosed.signal);
expect(logged).toEqual([]);
expect(node(listId).innerHTML).toContain("tx-row");
});
});
+35 -2
View File
@@ -21,7 +21,7 @@ jest.mock("../src/shared/log", () => ({
debugf: () => {},
infof: () => {},
warnf: () => {},
errorf: () => {},
errorf: jest.fn(),
},
debugFetch: jest.fn(),
setRuntimeDebug: () => {},
@@ -108,6 +108,7 @@ global.chrome = { storage };
const txStatus = require("../src/popup/views/txStatus");
const { state } = require("../src/shared/state");
const { RESTORABLE_VIEWS } = require("../src/shared/restorableViews");
const { log } = require("../src/shared/log");
const TX_HASH =
"0x85215772ed26ea8b39c2b3b18779030487efbe0b5fd7e882592b2f62b837be84";
@@ -128,16 +129,24 @@ function waitStatusText() {
return getElement("wait-tx-status").textContent;
}
// Stands in for the popup's: aborting it is the popup closing.
let pageClosed;
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(new Date("2026-08-11T12:00:00Z"));
elements.clear();
mockReceiptLookup.mockReset();
log.errorf.mockClear();
state.wallets = [];
state.viewData = {};
state.viewStack = [];
state.currentView = null;
txStatus.init({ doRefreshAndRender: jest.fn() });
pageClosed = new AbortController();
txStatus.init({
doRefreshAndRender: jest.fn(),
pageClosed: pageClosed.signal,
});
});
afterEach(() => {
@@ -466,6 +475,30 @@ describe("WaitTx against an RPC that never answers", () => {
});
});
// In the popup a lookup its own closing cancelled fails as a lookup against an
// RPC that cannot be reached does, and is not reported
// (https://git.eeqj.de/sneak/AutistMask/issues/475).
describe("WaitTx when the popup closes", () => {
test("a lookup that fails while the popup is open is reported", async () => {
mockReceiptLookup.mockRejectedValue(new TypeError("Failed to fetch"));
txStatus.showWait(TX_INFO, TX_HASH);
await jest.advanceTimersByTimeAsync(10000);
expect(log.errorf).toHaveBeenCalledWith(
"poll receipt failed:",
"Failed to fetch",
);
});
test("a lookup that fails once the popup has closed is not", async () => {
mockReceiptLookup.mockRejectedValue(new TypeError("Failed to fetch"));
txStatus.showWait(TX_INFO, TX_HASH);
pageClosed.abort();
await jest.advanceTimersByTimeAsync(10000);
expect(mockReceiptLookup).toHaveBeenCalledTimes(1);
expect(log.errorf).not.toHaveBeenCalled();
});
});
describe("wait-tx is a view the popup may reopen onto", () => {
// The resume feature is wired through RESTORABLE_VIEWS: restoreView()
// refuses any view not in the set, so dropping "wait-tx" from it kills
+279 -247
View File
@@ -275,7 +275,15 @@
resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
"@emnapi/core@^1.4.3", "@emnapi/core@^1.7.1", "@emnapi/core@^1.8.1":
"@emnapi/core@^1.10.0":
version "1.11.3"
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.11.3.tgz#5e95348a42cd1e06f0b9aa380cd74091daa4d520"
integrity sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==
dependencies:
"@emnapi/wasi-threads" "1.2.3"
tslib "^2.4.0"
"@emnapi/core@^1.4.3":
version "1.8.1"
resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.8.1.tgz#fd9efe721a616288345ffee17a1f26ac5dd01349"
integrity sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==
@@ -283,20 +291,34 @@
"@emnapi/wasi-threads" "1.1.0"
tslib "^2.4.0"
"@emnapi/runtime@^1.4.3", "@emnapi/runtime@^1.7.1", "@emnapi/runtime@^1.8.1":
"@emnapi/runtime@^1.10.0":
version "1.11.3"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.11.3.tgz#84257ae3b0531eb2aec1ffa23d70700da007ba95"
integrity sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==
dependencies:
tslib "^2.4.0"
"@emnapi/runtime@^1.4.3":
version "1.8.1"
resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.8.1.tgz#550fa7e3c0d49c5fb175a116e8cd70614f9a22a5"
integrity sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==
dependencies:
tslib "^2.4.0"
"@emnapi/wasi-threads@1.1.0", "@emnapi/wasi-threads@^1.1.0":
"@emnapi/wasi-threads@1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz"
integrity sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==
dependencies:
tslib "^2.4.0"
"@emnapi/wasi-threads@1.2.3", "@emnapi/wasi-threads@^1.2.1":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz#c9bf72fd4be5b928aee894820e8d814ed73916e9"
integrity sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==
dependencies:
tslib "^2.4.0"
"@esbuild/aix-ppc64@0.27.3":
version "0.27.3"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz#815b39267f9bffd3407ea6c376ac32946e24f8d2"
@@ -802,14 +824,12 @@
"@emnapi/runtime" "^1.4.3"
"@tybys/wasm-util" "^0.10.0"
"@napi-rs/wasm-runtime@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz#c3705ab549d176b8dc5172723d6156c3dc426af2"
integrity sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==
"@napi-rs/wasm-runtime@^1.1.4":
version "1.2.5"
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz#8c728c07e2543d80e55ccb34158c4ae9d6bd13e8"
integrity sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==
dependencies:
"@emnapi/core" "^1.7.1"
"@emnapi/runtime" "^1.7.1"
"@tybys/wasm-util" "^0.10.1"
"@tybys/wasm-util" "^0.10.3"
"@noble/curves@1.2.0":
version "1.2.0"
@@ -823,94 +843,94 @@
resolved "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz"
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
"@parcel/watcher-android-arm64@2.5.6":
version "2.5.6"
resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz#5f32e0dba356f4ac9a11068d2a5c134ca3ba6564"
integrity sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==
"@parcel/watcher-android-arm64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1"
integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==
"@parcel/watcher-darwin-arm64@2.5.6":
version "2.5.6"
resolved "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz"
integrity sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==
"@parcel/watcher-darwin-arm64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz#3d26dce38de6590ef79c47ec2c55793c06ad4f67"
integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==
"@parcel/watcher-darwin-x64@2.5.6":
version "2.5.6"
resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz#bf05d76a78bc15974f15ec3671848698b0838063"
integrity sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==
"@parcel/watcher-darwin-x64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz#99f3af3869069ccf774e4ddfccf7e64fd2311ef8"
integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==
"@parcel/watcher-freebsd-x64@2.5.6":
version "2.5.6"
resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz#8bc26e9848e7303ac82922a5ae1b1ef1bdb48a53"
integrity sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==
"@parcel/watcher-freebsd-x64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz#14d6857741a9f51dfe51d5b08b7c8afdbc73ad9b"
integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==
"@parcel/watcher-linux-arm-glibc@2.5.6":
version "2.5.6"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz#1328fee1deb0c2d7865079ef53a2ba4cc2f8b40a"
integrity sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==
"@parcel/watcher-linux-arm-glibc@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz#43c3246d6892381db473bb4f663229ad20b609a1"
integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==
"@parcel/watcher-linux-arm-musl@2.5.6":
version "2.5.6"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz#bad0f45cb3e2157746db8b9d22db6a125711f152"
integrity sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==
"@parcel/watcher-linux-arm-musl@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz#663750f7090bb6278d2210de643eb8a3f780d08e"
integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==
"@parcel/watcher-linux-arm64-glibc@2.5.6":
version "2.5.6"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz#b75913fbd501d9523c5f35d420957bf7d0204809"
integrity sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==
"@parcel/watcher-linux-arm64-glibc@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz#ba60e1f56977f7e47cd7e31ad65d15fdcbd07e30"
integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==
"@parcel/watcher-linux-arm64-musl@2.5.6":
version "2.5.6"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz#da5621a6a576070c8c0de60dea8b46dc9c3827d4"
integrity sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==
"@parcel/watcher-linux-arm64-musl@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz#f7fbcdff2f04c526f96eac01f97419a6a99855d2"
integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==
"@parcel/watcher-linux-x64-glibc@2.5.6":
version "2.5.6"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz#ce437accdc4b30f93a090b4a221fd95cd9b89639"
integrity sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==
"@parcel/watcher-linux-x64-glibc@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz#4d2ea0f633eb1917d83d483392ce6181b6a92e4e"
integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==
"@parcel/watcher-linux-x64-musl@2.5.6":
version "2.5.6"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz#02400c54b4a67efcc7e2327b249711920ac969e2"
integrity sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==
"@parcel/watcher-linux-x64-musl@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz#277b346b05db54f55657301dd77bdf99d63606ee"
integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==
"@parcel/watcher-win32-arm64@2.5.6":
version "2.5.6"
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz#caae3d3c7583ca0a7171e6bd142c34d20ea1691e"
integrity sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==
"@parcel/watcher-win32-arm64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz#7e9e02a26784d47503de1d10e8eab6cceb524243"
integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==
"@parcel/watcher-win32-ia32@2.5.6":
version "2.5.6"
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz#9ac922550896dfe47bfc5ae3be4f1bcaf8155d6d"
integrity sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==
"@parcel/watcher-win32-ia32@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz#2d0f94fa59a873cdc584bf7f6b1dc628ddf976e6"
integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==
"@parcel/watcher-win32-x64@2.5.6":
version "2.5.6"
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz#73fdafba2e21c448f0e456bbe13178d8fe11739d"
integrity sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==
"@parcel/watcher-win32-x64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz#ae52693259664ba6f2228fa61d7ee44b64ea0947"
integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==
"@parcel/watcher@^2.5.1":
version "2.5.6"
resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz"
integrity sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==
"@parcel/watcher@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.1.tgz#342507a9cfaaf172479a882309def1e991fb1200"
integrity sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==
dependencies:
detect-libc "^2.0.3"
detect-libc "^1.0.3"
is-glob "^4.0.3"
micromatch "^4.0.5"
node-addon-api "^7.0.0"
picomatch "^4.0.3"
optionalDependencies:
"@parcel/watcher-android-arm64" "2.5.6"
"@parcel/watcher-darwin-arm64" "2.5.6"
"@parcel/watcher-darwin-x64" "2.5.6"
"@parcel/watcher-freebsd-x64" "2.5.6"
"@parcel/watcher-linux-arm-glibc" "2.5.6"
"@parcel/watcher-linux-arm-musl" "2.5.6"
"@parcel/watcher-linux-arm64-glibc" "2.5.6"
"@parcel/watcher-linux-arm64-musl" "2.5.6"
"@parcel/watcher-linux-x64-glibc" "2.5.6"
"@parcel/watcher-linux-x64-musl" "2.5.6"
"@parcel/watcher-win32-arm64" "2.5.6"
"@parcel/watcher-win32-ia32" "2.5.6"
"@parcel/watcher-win32-x64" "2.5.6"
"@parcel/watcher-android-arm64" "2.5.1"
"@parcel/watcher-darwin-arm64" "2.5.1"
"@parcel/watcher-darwin-x64" "2.5.1"
"@parcel/watcher-freebsd-x64" "2.5.1"
"@parcel/watcher-linux-arm-glibc" "2.5.1"
"@parcel/watcher-linux-arm-musl" "2.5.1"
"@parcel/watcher-linux-arm64-glibc" "2.5.1"
"@parcel/watcher-linux-arm64-musl" "2.5.1"
"@parcel/watcher-linux-x64-glibc" "2.5.1"
"@parcel/watcher-linux-x64-musl" "2.5.1"
"@parcel/watcher-win32-arm64" "2.5.1"
"@parcel/watcher-win32-ia32" "2.5.1"
"@parcel/watcher-win32-x64" "2.5.1"
"@pkgjs/parseargs@^0.11.0":
version "0.11.0"
@@ -941,124 +961,131 @@
dependencies:
"@sinonjs/commons" "^3.0.1"
"@tailwindcss/cli@^4.2.1":
version "4.2.1"
resolved "https://registry.npmjs.org/@tailwindcss/cli/-/cli-4.2.1.tgz"
integrity sha512-b7MGn51IA80oSG+7fuAgzfQ+7pZBgjzbqwmiv6NO7/+a1sev32cGqnwhscT7h0EcAvMa9r7gjRylqOH8Xhc4DA==
"@tailwindcss/cli@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/cli/-/cli-4.3.1.tgz#bc00e49e2b70baad223969071e4e380da7123afe"
integrity sha512-ZWPy20rF+TBfTImxDMG3Wr75Y3RpaPlo9lc+oJbInlMyjT+XPkTVKVIL5RZ7JirXuIahcfHoLNFRmDorKi+JQQ==
dependencies:
"@parcel/watcher" "^2.5.1"
"@tailwindcss/node" "4.2.1"
"@tailwindcss/oxide" "4.2.1"
enhanced-resolve "^5.19.0"
"@parcel/watcher" "2.5.1"
"@tailwindcss/node" "4.3.1"
"@tailwindcss/oxide" "4.3.1"
enhanced-resolve "5.21.6"
mri "^1.2.0"
picocolors "^1.1.1"
tailwindcss "4.2.1"
tailwindcss "4.3.1"
"@tailwindcss/node@4.2.1":
version "4.2.1"
resolved "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz"
integrity sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==
"@tailwindcss/node@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/node/-/node-4.3.1.tgz#77402afcfa29c4b48b8494d0edfc4428d0a504ba"
integrity sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==
dependencies:
"@jridgewell/remapping" "^2.3.5"
enhanced-resolve "^5.19.0"
jiti "^2.6.1"
lightningcss "1.31.1"
enhanced-resolve "5.21.6"
jiti "^2.7.0"
lightningcss "1.32.0"
magic-string "^0.30.21"
source-map-js "^1.2.1"
tailwindcss "4.2.1"
tailwindcss "4.3.1"
"@tailwindcss/oxide-android-arm64@4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz#a7c24919b607e7f884e6ab97799d12c7fb5b47bd"
integrity sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==
"@tailwindcss/oxide-android-arm64@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.1.tgz#83c6762cd383a2ebc6e01897b0f35f19225e6653"
integrity sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==
"@tailwindcss/oxide-darwin-arm64@4.2.1":
version "4.2.1"
resolved "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz"
integrity sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==
"@tailwindcss/oxide-darwin-arm64@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.1.tgz#2558b7e835889ad721823e4dcb50dd5071d747d8"
integrity sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==
"@tailwindcss/oxide-darwin-x64@4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz#1e59ef0665f6cb9e658bf0ebcb3cb50f21b2c175"
integrity sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==
"@tailwindcss/oxide-darwin-x64@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.1.tgz#d987957b87a26668b6d0117ccd4a8a4d1a318a2b"
integrity sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==
"@tailwindcss/oxide-freebsd-x64@4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz#6b0c75e9dac7f1a241cb9a5eaa89f0d9664835b6"
integrity sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==
"@tailwindcss/oxide-freebsd-x64@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.1.tgz#75b342c81a07b1afa437976ec82f86d372431da7"
integrity sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==
"@tailwindcss/oxide-linux-arm-gnueabihf@4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz#717044d8fe746b1f0760485946c0c9a900174f7b"
integrity sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==
"@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.1.tgz#6730adc6d17187eeeff2f14f6a914d009749cb97"
integrity sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==
"@tailwindcss/oxide-linux-arm64-gnu@4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz#f544b0faf166d80791347911b2dd4372a893129d"
integrity sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==
"@tailwindcss/oxide-linux-arm64-gnu@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.1.tgz#869d16b3d9bd8097b797a3dd876db0368c07eae3"
integrity sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==
"@tailwindcss/oxide-linux-arm64-musl@4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz#9fbaf8dc00b858a2b955526abb15d88f5678d1ef"
integrity sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==
"@tailwindcss/oxide-linux-arm64-musl@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.1.tgz#ab110680ce3c7a2a135656db4402dffc1fb9c1d7"
integrity sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==
"@tailwindcss/oxide-linux-x64-gnu@4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz#6ab4e6b8d308d037a1155b8443df5941dbfa6aa1"
integrity sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==
"@tailwindcss/oxide-linux-x64-gnu@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.1.tgz#422a4175a76ae60dd9d17946eec3584cb636352f"
integrity sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==
"@tailwindcss/oxide-linux-x64-musl@4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz#52c55593394dff85f1fa88172f69f8fdcde182b6"
integrity sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==
"@tailwindcss/oxide-linux-x64-musl@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.1.tgz#f4c714a653a0e742955d2af2c53d0064b4c500d1"
integrity sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==
"@tailwindcss/oxide-wasm32-wasi@4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz#7401e35f881d3654b6180badd1243d75a2702ea5"
integrity sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==
"@tailwindcss/oxide-wasm32-wasi@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.1.tgz#32172ca8b2427b9c2bb09c97756960185b7d4fc0"
integrity sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==
dependencies:
"@emnapi/core" "^1.8.1"
"@emnapi/runtime" "^1.8.1"
"@emnapi/wasi-threads" "^1.1.0"
"@napi-rs/wasm-runtime" "^1.1.1"
"@tybys/wasm-util" "^0.10.1"
"@emnapi/core" "^1.10.0"
"@emnapi/runtime" "^1.10.0"
"@emnapi/wasi-threads" "^1.2.1"
"@napi-rs/wasm-runtime" "^1.1.4"
"@tybys/wasm-util" "^0.10.2"
tslib "^2.8.1"
"@tailwindcss/oxide-win32-arm64-msvc@4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz#63a502e7b696dcd976aa356b94ce0f4f8f832c44"
integrity sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==
"@tailwindcss/oxide-win32-arm64-msvc@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.1.tgz#07a11b6eb1f578d012460e6ad6f2352a28d32514"
integrity sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==
"@tailwindcss/oxide-win32-x64-msvc@4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz#8cc59b28ebc4dc866c0c14d7057f07f0ed04c4a8"
integrity sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==
"@tailwindcss/oxide-win32-x64-msvc@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.1.tgz#60c6095d97b141c02de36bb52a16c358d9bdaa98"
integrity sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==
"@tailwindcss/oxide@4.2.1":
version "4.2.1"
resolved "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz"
integrity sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==
"@tailwindcss/oxide@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@tailwindcss/oxide/-/oxide-4.3.1.tgz#6fdd28b3abf785e2c2cac31f52c4755875826828"
integrity sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==
optionalDependencies:
"@tailwindcss/oxide-android-arm64" "4.2.1"
"@tailwindcss/oxide-darwin-arm64" "4.2.1"
"@tailwindcss/oxide-darwin-x64" "4.2.1"
"@tailwindcss/oxide-freebsd-x64" "4.2.1"
"@tailwindcss/oxide-linux-arm-gnueabihf" "4.2.1"
"@tailwindcss/oxide-linux-arm64-gnu" "4.2.1"
"@tailwindcss/oxide-linux-arm64-musl" "4.2.1"
"@tailwindcss/oxide-linux-x64-gnu" "4.2.1"
"@tailwindcss/oxide-linux-x64-musl" "4.2.1"
"@tailwindcss/oxide-wasm32-wasi" "4.2.1"
"@tailwindcss/oxide-win32-arm64-msvc" "4.2.1"
"@tailwindcss/oxide-win32-x64-msvc" "4.2.1"
"@tailwindcss/oxide-android-arm64" "4.3.1"
"@tailwindcss/oxide-darwin-arm64" "4.3.1"
"@tailwindcss/oxide-darwin-x64" "4.3.1"
"@tailwindcss/oxide-freebsd-x64" "4.3.1"
"@tailwindcss/oxide-linux-arm-gnueabihf" "4.3.1"
"@tailwindcss/oxide-linux-arm64-gnu" "4.3.1"
"@tailwindcss/oxide-linux-arm64-musl" "4.3.1"
"@tailwindcss/oxide-linux-x64-gnu" "4.3.1"
"@tailwindcss/oxide-linux-x64-musl" "4.3.1"
"@tailwindcss/oxide-wasm32-wasi" "4.3.1"
"@tailwindcss/oxide-win32-arm64-msvc" "4.3.1"
"@tailwindcss/oxide-win32-x64-msvc" "4.3.1"
"@tybys/wasm-util@^0.10.0", "@tybys/wasm-util@^0.10.1":
"@tybys/wasm-util@^0.10.0":
version "0.10.1"
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414"
integrity sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==
dependencies:
tslib "^2.4.0"
"@tybys/wasm-util@^0.10.2", "@tybys/wasm-util@^0.10.3":
version "0.10.4"
resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.4.tgz#3e85ecb266f9d1722250e89bc69528615720b154"
integrity sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==
dependencies:
tslib "^2.4.0"
"@types/babel__core@^7.20.5":
version "7.20.5"
resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz"
@@ -1582,6 +1609,11 @@ deepmerge@^4.3.1:
resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz"
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
detect-libc@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
detect-libc@^2.0.3:
version "2.1.2"
resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz"
@@ -1622,13 +1654,13 @@ emoji-regex@^9.2.2:
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
enhanced-resolve@^5.19.0:
version "5.19.0"
resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz"
integrity sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==
enhanced-resolve@5.21.6:
version "5.21.6"
resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz#aa207b43cf658e6ab3ba06896edc00c13c3127c6"
integrity sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==
dependencies:
graceful-fs "^4.2.4"
tapable "^2.3.0"
tapable "^2.3.3"
error-ex@^1.3.1:
version "1.3.4"
@@ -2469,10 +2501,10 @@ jest@^30.2.0:
import-local "^3.2.0"
jest-cli "30.2.0"
jiti@^2.6.1:
version "2.6.1"
resolved "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz"
integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==
jiti@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.7.0.tgz#974228f2f4ca2bc21885a1797b45fea68e950c64"
integrity sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==
js-tokens@^4.0.0:
version "4.0.0"
@@ -2549,79 +2581,79 @@ libsodium-wrappers-sumo@^0.8.2:
dependencies:
libsodium-sumo "^0.8.0"
lightningcss-android-arm64@1.31.1:
version "1.31.1"
resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz#609ff48332adff452a8157a7c2842fd692a8eac4"
integrity sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==
lightningcss-android-arm64@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz#f033885116dfefd9c6f54787523e3514b61e1968"
integrity sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==
lightningcss-darwin-arm64@1.31.1:
version "1.31.1"
resolved "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz"
integrity sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==
lightningcss-darwin-arm64@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz#50b71871b01c8199584b649e292547faea7af9b5"
integrity sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==
lightningcss-darwin-x64@1.31.1:
version "1.31.1"
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz#f7482c311273571ec0c2bd8277c1f5f6e90e03a4"
integrity sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==
lightningcss-darwin-x64@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz#35f3e97332d130b9ca181e11b568ded6aebc6d5e"
integrity sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==
lightningcss-freebsd-x64@1.31.1:
version "1.31.1"
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz#91df1bb290f1cb7bb2af832d7d0d8809225e0124"
integrity sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==
lightningcss-freebsd-x64@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz#9777a76472b64ed6ff94342ad64c7bafd794a575"
integrity sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==
lightningcss-linux-arm-gnueabihf@1.31.1:
version "1.31.1"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz#c3cad5ae8b70045f21600dc95295ab6166acf57e"
integrity sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==
lightningcss-linux-arm-gnueabihf@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz#13ae652e1ab73b9135d7b7da172f666c410ad53d"
integrity sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==
lightningcss-linux-arm64-gnu@1.31.1:
version "1.31.1"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz#a5c4f6a5ac77447093f61b209c0bd7fef1f0a3e3"
integrity sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==
lightningcss-linux-arm64-gnu@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz#417858795a94592f680123a1b1f9da8a0e1ef335"
integrity sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==
lightningcss-linux-arm64-musl@1.31.1:
version "1.31.1"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz#af26ab8f829b727ada0a200938a6c8796ff36900"
integrity sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==
lightningcss-linux-arm64-musl@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz#6be36692e810b718040802fd809623cffe732133"
integrity sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==
lightningcss-linux-x64-gnu@1.31.1:
version "1.31.1"
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz#a891d44e84b71c0d88959feb9a7522bbf61450ee"
integrity sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==
lightningcss-linux-x64-gnu@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz#0b7803af4eb21cfd38dd39fe2abbb53c7dd091f6"
integrity sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==
lightningcss-linux-x64-musl@1.31.1:
version "1.31.1"
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz#8c8b21def851f4d477fa897b80cb3db2b650bc6e"
integrity sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==
lightningcss-linux-x64-musl@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz#88dc8ba865ddddb1ac5ef04b0f161804418c163b"
integrity sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==
lightningcss-win32-arm64-msvc@1.31.1:
version "1.31.1"
resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz#79000fb8c57e94a91b8fc643e74d5a54407d7080"
integrity sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==
lightningcss-win32-arm64-msvc@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz#4f30ba3fa5e925f5b79f945e8cc0d176c3b1ab38"
integrity sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==
lightningcss-win32-x64-msvc@1.31.1:
version "1.31.1"
resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz#7f025274c81c7d659829731e09c8b6f442209837"
integrity sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==
lightningcss-win32-x64-msvc@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz#141aa5605645064928902bb4af045fa7d9f4220a"
integrity sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==
lightningcss@1.31.1:
version "1.31.1"
resolved "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz"
integrity sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==
lightningcss@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/lightningcss/-/lightningcss-1.32.0.tgz#b85aae96486dcb1bf49a7c8571221273f4f1e4a9"
integrity sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==
dependencies:
detect-libc "^2.0.3"
optionalDependencies:
lightningcss-android-arm64 "1.31.1"
lightningcss-darwin-arm64 "1.31.1"
lightningcss-darwin-x64 "1.31.1"
lightningcss-freebsd-x64 "1.31.1"
lightningcss-linux-arm-gnueabihf "1.31.1"
lightningcss-linux-arm64-gnu "1.31.1"
lightningcss-linux-arm64-musl "1.31.1"
lightningcss-linux-x64-gnu "1.31.1"
lightningcss-linux-x64-musl "1.31.1"
lightningcss-win32-arm64-msvc "1.31.1"
lightningcss-win32-x64-msvc "1.31.1"
lightningcss-android-arm64 "1.32.0"
lightningcss-darwin-arm64 "1.32.0"
lightningcss-darwin-x64 "1.32.0"
lightningcss-freebsd-x64 "1.32.0"
lightningcss-linux-arm-gnueabihf "1.32.0"
lightningcss-linux-arm64-gnu "1.32.0"
lightningcss-linux-arm64-musl "1.32.0"
lightningcss-linux-x64-gnu "1.32.0"
lightningcss-linux-x64-musl "1.32.0"
lightningcss-win32-arm64-msvc "1.32.0"
lightningcss-win32-x64-msvc "1.32.0"
lines-and-columns@^1.1.6:
version "1.2.4"
@@ -2680,9 +2712,9 @@ merge-stream@^2.0.0:
resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
micromatch@^4.0.8:
micromatch@^4.0.5, micromatch@^4.0.8:
version "4.0.8"
resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies:
braces "^3.0.3"
@@ -2873,7 +2905,7 @@ picomatch@^2.0.4, picomatch@^2.3.1:
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
picomatch@^4.0.2, picomatch@^4.0.3:
picomatch@^4.0.2:
version "4.0.3"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
@@ -3134,15 +3166,15 @@ synckit@^0.11.8:
dependencies:
"@pkgr/core" "^0.2.9"
tailwindcss@4.2.1, tailwindcss@^4.2.1:
version "4.2.1"
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz"
integrity sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==
tailwindcss@4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-4.3.1.tgz#78ee06f6186bc8fb9603f8083eb703dc7dd96a10"
integrity sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==
tapable@^2.3.0:
version "2.3.0"
resolved "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz"
integrity sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==
tapable@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.3.3.tgz#5da7c9992c46038221267985ab28421a8879f160"
integrity sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==
test-exclude@^6.0.0:
version "6.0.0"