Compare commits

..

2 Commits

Author SHA1 Message Date
23817de7d3 milestone 1.0.0: approval-path security, build-integrity guard, e2e harness and filter coverage (#190)
All checks were successful
check / check (push) Successful in 1m6s
e2e / e2e-chrome (push) Successful in 2m4s
e2e / e2e-firefox (push) Successful in 53s
Reviewed-on: #190

I understand this is ready for 1.0.0 - user acceptance testing remains.  No tags for now but we can think of this as `1.0.0b1`.
2026-08-30 04:11:51 +02:00
a098bb0c32 fix: floor malformed allowedSites, fraudContracts and selectedToken entries (closes #362)
All checks were successful
check / check (push) Successful in 42s
e2e / e2e-chrome (push) Successful in 1m45s
e2e / e2e-firefox (push) Successful in 31s
A stored allowedSites whose value was not a list rendered a working popup and then made every subsequent save fail silently, so the user operated a wallet that persisted nothing -- worse than a blank popup, which is at least visibly broken. fraudContracts and selectedToken had the same shape: a container floored by truthiness or not at all, while its entries were dereferenced. Entries are now floored as well as containers, following the idiom #311 established, and a failed save raises a persistent banner instead of vanishing into a swallowed rejection.

The per-field justifications that used to live in a hand-written header are replaced by a contract test that drives each field's hostile and falsy values through a real popup boot, so a claim about a field answers to the code rather than to prose. Its guarantee is stated narrowly and deliberately: no structural dereference on the code paths a wholly-corrupted profile takes, which is not every path a stored record takes. The paths it does not drive are named where the claim is made, and are tracked in #379.
2026-08-23 23:06:17 +02:00
4 changed files with 102 additions and 55 deletions

View File

@@ -1052,17 +1052,18 @@ driving the real code with hostile values — and, for every field whose only
defence is that nothing dereferences it, by booting the real popup entry point defence is that nothing dereferences it, by booting the real popup entry point
over that value onto every view the popup can reopen onto. That last part is over that value onto every view the popup can reopen onto. That last part is
what makes the claim falsifiable, because this defect class lives on the restore what makes the claim falsifiable, because this defect class lives on the restore
path rather than on the home screen: a field one of those boots corrupts and a path rather than on the home screen. Read the claim narrowly, as that file
restorable view dereferences on its render fails `make check`, at either states it: what those boots prove is no structural dereference on the code paths
polarity — a value nothing writes is a wrong-typed one and therefore truthy, so a WHOLLY-CORRUPTED PROFILE takes, which is not every path a stored record takes.
each such field is also driven falsy, or proven unable to be falsy after the Not driven: any pairing of values the four slots do not produce, a view only
floor. So does a field that gains a floor while its row still claims it has forward navigation opens, anything behind a click, and everything a healthy
none, and so does a field added to `PERSISTED_FIELDS` with no row at all. Two profile reaches. Within that boundary the verdict is unconditional — if one of
things the boots do not drive: a MIX of polarities, since one boot puts every those boots leaves the popup unhealthy or off the view it stored, `make check`
corrupted field on the same slot, so a branch reached only when one is truthy fails, including when it takes two corrupted fields at once, because the verdict
and another falsy is not entered; and whatever no stored record reaches by is the combined boot and the per-field re-boot that names a culprit can only
itself — a view only forward navigation opens, and anything behind a click. The decorate the message. So does a field that gains a floor while its row still
per-field justification that used to live in the header of claims it has none, and so does a field added to `PERSISTED_FIELDS` with no row
at all. The per-field justification that used to live in the header of
`src/shared/stateSchema.js` shipped a false claim in three consecutive changes, `src/shared/stateSchema.js` shipped a false claim in three consecutive changes,
each caught only by a reviewer re-deriving thirty fields by hand. each caught only by a reviewer re-deriving thirty fields by hand.

12
TODO.md
View File

@@ -85,9 +85,15 @@ but the review is broader than any of them.
the path this whole class of defect lives on. Each such field is driven at the path this whole class of defect lives on. Each such field is driven at
both polarities — a value nothing writes is wrong-typed and so truthy, so a both polarities — a value nothing writes is wrong-typed and so truthy, so a
falsy slot is driven too, or the field is proven unable to be falsy after the falsy slot is driven too, or the field is proven unable to be falsy after the
floor. A field with no row, a field that gains a floor while its row still floor. The claim is narrow and stated as such: no structural dereference on
claims it has none, and a field one of those boots corrupts and a restorable the code paths a wholly-corrupted profile takes, which is not every path a
view dereferences on its render now all fail `make check`. stored record takes — a pairing of values the four slots do not produce, a
view only forward navigation opens, anything behind a click, and everything a
healthy profile reaches are all undriven. Within that boundary the verdict is
unconditional, including a dereference that takes two corrupted fields at
once, since the assertion is on the combined boot and the per-field re-boot
can only decorate the message. A field with no row and a field that gains a
floor while its row still claims it has none also fail `make check`.
- 2026-08-23: A swap amount and the token it is counted in now always come from - 2026-08-23: A swap amount and the token it is counted in now always come from
the same hop, on both sides of the approval screen the same hop, on both sides of the approval screen
([#359](https://git.eeqj.de/sneak/AutistMask/issues/359) and ([#359](https://git.eeqj.de/sneak/AutistMask/issues/359) and

View File

@@ -38,15 +38,16 @@
// real popup entry point onto EVERY view the popup can reopen onto. // real popup entry point onto EVERY view the popup can reopen onto.
// //
// That last part is the whole point, because this defect class lives on the // That last part is the whole point, because this defect class lives on the
// RESTORE path and not on Home: a field one of those boots corrupts and a // RESTORE path and not on Home. Take the claim NARROWLY, exactly as that file
// restorable view dereferences on its render turns that suite red, at either // states it: what those boots prove is no structural dereference on the code
// polarity — a value nothing writes is wrong-typed and so truthy, so each such // paths a WHOLLY-CORRUPTED PROFILE takes — which is not every path a stored
// field is also driven falsy, or proven unable to be falsy after the floor. So // record takes. Not driven: any pairing of values the four slots do not
// does a field that gains a floor while its row still claims it has none. Two // produce, a view only forward navigation opens, anything behind a click, and
// things the boots do not drive: a MIX of polarities, since one boot puts every // everything a healthy profile reaches. Within that boundary the verdict is
// corrupted field on the same slot; and whatever no stored record reaches by // unconditional, including a dereference that takes two corrupted fields at
// itself — a view only forward navigation opens, and anything behind a click. A // once. That suite also goes red on a field that gains a floor while its row
// field added to PERSISTED_FIELDS with no row fails the suite too. // still claims it has none, and on a field added to PERSISTED_FIELDS with no
// row at all.
// //
// That test exists because this comment did not work. It carried a // That test exists because this comment did not work. It carried a
// hand-written justification per field, and it shipped a false one in three // hand-written justification per field, and it shipped a false one in three

View File

@@ -44,12 +44,32 @@
// reason, and a field that cannot be falsy after the floor says so in its row // reason, and a field that cannot be falsy after the floor says so in its row
// and is proven so. // and is proven so.
// //
// What that buys: a field any restorable view dereferences on that view's // READ THE CLAIM NARROWLY. What this file proves is: NO STRUCTURAL
// render turns this file red, at either polarity. What it does not buy is a // DEREFERENCE ON THE CODE PATHS A WHOLLY-CORRUPTED PROFILE TAKES. That is not
// MIX of polarities — one boot puts every swept field on the same slot, so a // every path a stored record takes, and the difference is the whole of what
// branch reached only when one swept field is truthy and another falsy is not // this file does not cover:
// entered. Booting every field separately at every value would be several //
// hundred boots and most of the suite's budget; this is forty-four. // - Only the values in the table, in the SLOT arrangement below: four value
// combinations per view, not the product of twelve fields. A dereference
// reached only under a pairing no slot produces is not driven at all.
// - Only what a stored record reaches by ITSELF. A view only forward
// navigation opens, and anything behind a click, is not driven.
// - Nothing about the paths a HEALTHY profile takes, which is most of the
// popup. This file is a floor under one defect class, not a proof about
// the renderers.
//
// Within that boundary it is unconditional: if one of these boots leaves the
// popup unhealthy or off the view it stored, this file goes red — including
// when it takes two corrupted fields at once, because the verdict is the
// combined boot itself and the per-field re-boot below can only decorate the
// message. That last part is the one thing an earlier version got wrong: it
// asserted on the per-field list, so an observed dead popup that no single
// field reproduced was reported green.
//
// Booting every field separately at every value would be several hundred boots
// and most of the suite's budget; this is forty-four. Widening it further is
// out of scope — proving no field is dereferenced on any reachable render path
// is exhaustive verification of the popup, not a floor under a stored record.
// //
// The three claims this replaced, all false, all caught here by construction: // The three claims this replaced, all false, all caught here by construction:
// rpcUrl reaching `new JsonRpcProvider()` (a synchronous throw, not a caught // rpcUrl reaching `new JsonRpcProvider()` (a synchronous throw, not a caught
@@ -738,17 +758,23 @@ describe("a hostile routing value restoring onto", () => {
// routing turns this red and has to declare `routes` and take the individual // routing turns this red and has to declare `routes` and take the individual
// sweep above. // sweep above.
// //
// Combining does hide one thing, and the last slot is what stops it. A hostile // Combining hides one thing, and the last slot is what stops it. A hostile
// value is wrong-typed and therefore TRUTHY, so on a boot where every swept // value is wrong-typed and therefore TRUTHY, so on a boot where every swept
// field is hostile, no `if (!state.x)` branch is entered — and a dereference // field is hostile, no `if (!state.x)` branch is entered — and a dereference
// inside such a branch would go unseen however loudly it throws. The last slot // inside such a branch would go unseen however loudly it throws. The last slot
// is the falsy one: every swept field that CAN be falsy is falsy on it, which // is the falsy one: every swept field that CAN be falsy is falsy on it, which
// is also the state an ordinary install boots in for three of them, while the // is also the state an ordinary install boots in for three of them, while the
// fields that cannot be falsy stay hostile. Beyond that, a throw fails the boot // fields that cannot be falsy stay hostile-truthy. That makes it a MIX, and a
// whichever field threw, and a renderer that never ran is what `restored` // deliberate one — the interaction between a falsy flag and a still-hostile
// forbids. When it does go red, the same view is re-booted one field at a time // theme is a shape a stored record really produces.
// so the failure names the fields rather than leaving a reader to bisect twelve //
// of them. // The verdict is the combined boot, always. When it goes red the same view is
// re-booted one field at a time, so the failure NAMES a culprit instead of
// leaving a reader to bisect twelve fields — but that loop only decorates the
// message. It cannot clear the failure. A dereference that needs two corrupted
// fields at once is reproduced by neither field alone, and a version of this
// file that asserted on the named list reported exactly that case green while
// watching the popup die.
const UNROUTED = CONTRACT.filter((row) => swept(row) && !routes(row)); const UNROUTED = CONTRACT.filter((row) => swept(row) && !routes(row));
const HOSTILE_SLOTS = Math.max( const HOSTILE_SLOTS = Math.max(
...UNROUTED.map((row) => sweptValues(row).length), ...UNROUTED.map((row) => sweptValues(row).length),
@@ -774,29 +800,42 @@ describe("every field the router does not read, corrupted at once, onto", () =>
restoringOnto(view, fields), restoringOnto(view, fields),
view, view,
); );
if (together.errors.length === 0 && together.restored) {
expect(together).toEqual(RESTORED); // The per-field re-boot only DECORATES the message. The
return; // verdict is `together`, unconditionally: a dereference that
// needs two corrupted fields at once is reproduced by NEITHER
// field alone, so an assertion on the named list would report
// an observed dead popup as green.
const named = [];
if (together.errors.length > 0 || !together.restored) {
for (const row of UNROUTED) {
const one = await restoredHealth(
restoringOnto(view, {
[row.field]: fields[row.field],
}),
view,
);
if (one.errors.length === 0 && one.restored) continue;
named.push(
`${row.field}=${JSON.stringify(
fields[row.field],
)}: ` +
(one.errors.join("; ") || `fell off ${view}`),
);
}
if (named.length === 0) {
named.push(
"no single field reproduces it; it takes two or " +
`more of ${JSON.stringify(fields)}`,
);
}
} }
const named = []; expect({
for (const row of UNROUTED) {
const one = await restoredHealth(
restoringOnto(view, {
[row.field]: fields[row.field],
}),
view,
);
if (one.errors.length === 0 && one.restored) continue;
named.push(
`${row.field}=${JSON.stringify(fields[row.field])}: ` +
(one.errors.join("; ") || `fell off ${view}`),
);
}
expect({ view: view, fields: named }).toEqual({
view: view, view: view,
fields: [], together: together,
}); fields: named,
}).toEqual({ view: view, together: RESTORED, fields: [] });
}); });
} }
} }