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 62 additions and 77 deletions

View File

@@ -1052,24 +1052,20 @@ 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
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
path rather than on the home screen: whenever one of those boots reaches a
structural dereference on the view it restored onto, `make check` fails —
including a dereference that takes two corrupted fields at once, because the
verdict is the combined boot and the per-field re-boot that names a culprit can
only decorate the message. Each swept field is driven at both polarities, or
proven unable to be falsy after the floor, since a value nothing writes is
wrong-typed and therefore truthy and would otherwise leave every `if (!state.x)`
branch unentered. So does a field that gains a floor while its row still claims
it has none, and so does a field added to `PERSISTED_FIELDS` with no row at all.
What the boots do not drive is every combination: four value combinations per
view, not the product of the twelve swept fields. The last of the four is itself
a mix — every falsy-capable field falsy against the ones that cannot be falsy —
so many two-field interactions are driven; one needing a pairing none of the
four produces is not. Nor is anything no stored record reaches by itself — a
view only forward navigation opens, and anything behind a click. The per-field
justification that used to live in the header of `src/shared/stateSchema.js`
shipped a false claim in three consecutive changes, each caught only by a
reviewer re-deriving thirty fields by hand.
path rather than on the home screen. Read the claim narrowly, as that file
states it: what those boots prove is no structural dereference on the code paths
a WHOLLY-CORRUPTED PROFILE takes, which is not every path a stored record takes.
Not driven: any 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. Within that boundary the verdict is unconditional — if one of
those boots leaves the popup unhealthy or off the view it stored, `make check`
fails, including when it takes two corrupted fields at once, because the verdict
is the combined boot and the per-field re-boot that names a culprit can only
decorate the message. So does a field that gains a floor while its row still
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,
each caught only by a reviewer re-deriving thirty fields by hand.
The `allowedSites` case is why the entry check is not optional. A stored
`{"0x…": "notalist"}` is a well-formed object holding a malformed entry: it

18
TODO.md
View File

@@ -85,15 +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
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
floor. A field with no row, a field that gains a floor while its row still
claims it has none, and any structural dereference one of those boots reaches
on the view it restored onto now all fail `make check` — including a
dereference that takes two corrupted fields at once, because the verdict is
the combined boot and the per-field re-boot that names a culprit can only
decorate the message. What is not driven is every combination: four value
combinations per view rather than the product of the twelve swept fields, so
an interaction needing a pairing none of the four produces goes unseen, as
does anything no stored record reaches by itself.
floor. The claim is narrow and stated as such: no structural dereference on
the code paths a wholly-corrupted profile takes, which is not every path a
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
the same hop, on both sides of the approval screen
([#359](https://git.eeqj.de/sneak/AutistMask/issues/359) and

View File

@@ -38,24 +38,16 @@
// 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
// RESTORE path and not on Home: whenever one of those boots reaches a
// structural dereference on the view it restored onto, that suite goes red —
// including a dereference that takes two corrupted fields at once, since the
// verdict is the combined boot and the per-field re-boot that names a culprit
// can only decorate the message. Each swept field is driven at both
// polarities, or proven unable to be falsy after the floor: a value nothing
// writes is wrong-typed and so truthy, which would otherwise leave every
// `if (!state.x)` branch unentered. So does a field that gains a floor while
// its row still claims it has none, and a field added to PERSISTED_FIELDS with
// no row at all.
//
// What the boots do NOT drive is every combination: four value combinations
// per view, not the product of the twelve swept fields. The last of the four
// is itself a mix — every falsy-capable field falsy against the ones that
// cannot be falsy — so many two-field interactions are driven; one needing a
// pairing none of the four produces is not. Nor is anything no stored record
// reaches by itself: a view only forward navigation opens, and anything behind
// a click.
// RESTORE path and not on Home. Take the claim NARROWLY, exactly as that file
// states it: what those boots prove is no structural dereference on the code
// paths a WHOLLY-CORRUPTED PROFILE takes — which is not every path a stored
// record takes. Not driven: any 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. Within that boundary the verdict is
// unconditional, including a dereference that takes two corrupted fields at
// once. That suite also goes red on a field that gains a floor while its row
// 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
// hand-written justification per field, and it shipped a false one in three

View File

@@ -44,23 +44,32 @@
// reason, and a field that cannot be falsy after the floor says so in its row
// and is proven so.
//
// What that buys: whenever one of those boots reaches a structural
// dereference on the view it restored onto, this file goes red — INCLUDING a
// dereference that 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 is the one thing an earlier version of this file got
// wrong: it asserted on the per-field list, so an observed dead popup that no
// single field reproduced was reported green.
// READ THE CLAIM NARROWLY. What this file proves is: NO STRUCTURAL
// DEREFERENCE ON THE CODE PATHS A WHOLLY-CORRUPTED PROFILE TAKES. That is not
// every path a stored record takes, and the difference is the whole of what
// this file does not cover:
//
// What it does NOT buy is every combination — four value combinations per
// view are driven, not the product of twelve fields. Note that the last slot
// is itself a MIX rather than a uniform polarity: every falsy-capable field is
// falsy on it while the neverFalsy ones stay hostile-truthy, so many two-field
// interactions are driven and fatal. One that needs a pairing none of the four
// slots produces is not driven at all. Nor is anything no stored record
// reaches by itself: a view only forward navigation opens, and anything behind
// a click. 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:
// rpcUrl reaching `new JsonRpcProvider()` (a synchronous throw, not a caught
@@ -111,19 +120,6 @@ const swept = (row) => row.kind === KIND.LOOSE || Boolean(row.alsoSweep);
// `if (!state.x) { state.y.deref() }`, so the falsy slot is not optional.
const sweptValues = (row) => [...row.hostile, ...(row.falsy || [])];
// What may count toward a row's POLARITY: the swept values, plus only those
// `hostileRestore` entries driven onto every restorable view. An entry that
// carries `views: [...]` reaches only those renderers, so counting it would
// let a future row claim a polarity that one view sees and the other ten do
// not. No current row does that — the restricted entries are all viewData's,
// and viewData is neverFalsy — and this is what keeps it so.
const polarityValues = (row) => [
...sweptValues(row),
...(row.hostileRestore || [])
.filter((entry) => !entry.views)
.map((entry) => entry.value),
];
// ------------------------------------------------------------------ the table
//
// `hostile` is values a stored record can carry that nothing in src/ ever
@@ -610,9 +606,10 @@ describe("both polarities of every swept field are driven", () => {
test(`${row.field}: truthy and falsy`, () => {
// What the boots below actually drive, floored the way a renderer
// sees it — not what the row says it drives.
const driven = polarityValues(row).map((value) =>
floored(row.field, value),
);
const driven = [
...sweptValues(row),
...(row.hostileRestore || []).map((entry) => entry.value),
].map((value) => floored(row.field, value));
expect({
truthy: driven.some((value) => Boolean(value)),