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) Successful in 2m50s
e2e / e2e-chrome (push) Successful in 5m53s
e2e / e2e-firefox (push) Successful in 2m19s
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
32 changed files with 1407 additions and 328 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
+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
+25 -9
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
@@ -643,12 +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. The `e2e-chrome` figures predate
the two cases that wait for a receipt to end in error, which add about two
minutes of real waiting.
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`)
@@ -2190,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 |
+66
View File
@@ -45,6 +45,72 @@ 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
+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,
};
+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 };
+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,
+4
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>';
+4
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>';
+3
View File
@@ -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) {
+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,
+4
View File
@@ -273,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.
@@ -299,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.");
+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);
});
+25 -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);
@@ -562,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));
@@ -618,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
@@ -693,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:
+104 -6
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",
@@ -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
@@ -4470,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).
+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();
+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"