Skip to content

fix: correct three false SDK claims and three real defects from the #23 audit - #25

Merged
KruGoL merged 1 commit into
chore/sdk-0.14.2from
fix/audit-23-false-claims-and-bugs
Aug 8, 2026
Merged

fix: correct three false SDK claims and three real defects from the #23 audit#25
KruGoL merged 1 commit into
chore/sdk-0.14.2from
fix/audit-23-false-claims-and-bugs

Conversation

@KruGoL

@KruGoL KruGoL commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to an audit of #23. Stacked on #24 — review that one first; this branch contains
it, so merging this alone is also complete.

Nothing here was crashing. Every test passed, every build succeeded — which is exactly why it
landed. The damage is that the docs teach code which cannot work, and three defects sat in
paths no test exercises.

Every claim below was checked against the pinned SDK, and the handshake ones were reproduced by
driving a real ConnectHost.


False claims

1. The version-floor refusal names the client's version

The docs presented actualSdk: null / "unknown (not reported)" as what a pre-flip dApp
receives. ConnectClient has sent sdkVersion in the handshake since 0.10.1 — only 0.9.x
and 0.10.0 report nothing. Driving a real 0.14.1 ConnectHost with minSdkVersion: '0.14.1-0'
(what the Sphere wallet sets) and the exact handshake a 0.13.1 client emits:

--- 0.13.1 client (real) ---
message : SDK version 0.13.1 is below the required minimum 0.14.1-0
data    : {"requiredSdk":"0.14.1-0","actualSdk":"0.13.1"}

--- client reporting nothing ---
message : SDK version unknown (not reported) is below the required minimum 0.14.1-0
data    : {"requiredSdk":"0.14.1-0","actualSdk":null}

A developer branching on actualSdk == null writes a branch that never fires. The claim was in
CLAUDE.md, browser/CONNECT.md (including a worked-example JSON no 0.10+ client can receive)
and both READMEs, and it had driven useWalletConnect.test.ts and errors.test.ts to assert the
unreachable case as primary. Both now assert the real refusal, with the null case kept as a
secondary test.

2. "Every pre-0.14 name still fires" — 26 do not

SphereEventType went 61 names -> 28. COMPAT_ATTACHERS covers 16 of the 38 removed. The other
26 fail silently: Sphere.on() accepts any string, so the subscribe succeeds and then never
delivers. 24 of them were live emitters in 0.13.1 — swap:failed alone had 20 emit sites.

Whole families went this way: every invoice:*, every swap:*, plus sync:started/:error/
:provider, inventory:conflict, send:partial-remainder, transfer:invalid,
walletapi:session, payment_request:settling. CONNECT.md now carries the full
removed-with-no-adapter table, and a new test keeps a dead name out of the panel's list.

3. payment_request:accepted / :response were not fabrications

A test comment called them names that "never existed in any SDK release". Both are declared in
0.13.1's SphereEventType and SphereEventMap, and :response is emitted by PaymentsModule.
The old list was right; the flip removed them without an adapter.


Defects

badgeFor painted an in-flight transfer green. TransferStatus is
pending | submitted | confirmed | delivered | completed | failed. The implementation reddened
failed, ambered pending/deliveryPending, and let everything else fall through to the
success colour — so submitted (certification in flight, money not settled) and any payload with
no status at all read as "it went through". Colour is now driven by the settled set, so green
must be earned rather than defaulted into.

The bot could miss the one event it waits for. The inventory:updated listener was
registered after the mint and after an await assets() round trip. The server credits a mint
asynchronously; if the credit landed in that window the event fired with nobody attached and the
bot printed an empty balance it never revised. Now: subscribe, mint, read — with the boot read
going through the same epoch guard so it cannot outrun an update either.

mockSphere would throw inside ConnectHost. payments-compat.ts runs
sphere.paymentsV2?.requests.list() for the payment_request:paid/:rejected/:expired
adapters. The optional chain guards paymentsV2, not requests — and the mock had no
requests:

payments keys        : assets, tokens, history
paymentsV2.requests  : undefined
-> TypeError: Cannot read properties of undefined (reading 'list')

Masked only because mockSphere.on is a no-op, so no event can fire. The facade is now
implemented whole, with a test that evaluates the exact expression the host does.


Also

  • sendSafety.test.ts could not detect a code the SDK adds — the direction that costs money,
    since an unrecognised possibly-committed code falls through to the retryable branch and invites
    a double pay. The SDK's POSSIBLY_COMMITTED_SEND_CODES is module-private, so the test now
    sweeps the whole SphereErrorCode universe through the SDK predicate and compares both
    directions, with a type-level guard for completeness. Verified it bites — removing one code:
    src/sendSafety.test.ts(53,7): error TS2322: Type 'true' is not assignable to type '"REENTRANT_GATE"'.
    
  • formatAssets was untestable, living in an entry module that calls main() on import.
    Moved to bot/src/balance.ts with unit tests, matching how coins.ts / sendSafety.ts /
    aggregatorKey.ts are already structured.
  • Both error describers handled one refusal shape of three. nodejs/src/lockResume.ts never
    read err.data at all despite the README pointing at it as the example of doing so, and
    backend-auth's copy dropped the protocol-floor and network branches. Both now cover the SDK
    floor, the protocol floor and the 4008 network mismatch — the last being what a dApp that omits
    network actually hits. describeVersionFloor is renamed describeHandshakeRefusal.
  • CI runs npm ci. The comment justifying npm install described a file: link no lockfile
    carries any more, and npm install will not fail when package.json and package-lock.json
    disagree — precisely the regression an SDK-bump PR must be guarded against.

Verification

Package npm ci typecheck tests build
browser OK OK 76 -> 80 OK
nodejs OK OK 18 -> 36
bot OK OK 37 -> 46
backend-auth/frontend OK OK 12 -> 16 OK
backend-auth/backend OK OK 4

Typechecks were run with the exact commands CI uses (tsc -b for browser and backend-auth
frontend, tsc --noEmit elsewhere).

… audit

The bump PR shipped documentation that teaches code which cannot work, plus
three defects no test could see. Nothing crashed, which is why it landed.

False claims, each verified against the pinned SDK:

- The refusal a pre-flip dApp receives names its version. ConnectClient has
  sent `sdkVersion` in the handshake since 0.10.1, so a 0.13.1 client is
  refused with `actualSdk: "0.13.1"`, not `null`. The docs presented the
  null/"unknown (not reported)" case as what an old dApp sees, and the browser
  and backend-auth tests were written around that unreachable branch. A reader
  branching on `actualSdk == null` gets dead code.
- "Every pre-0.14 name still fires" is wrong. The flip removed 38 event names
  and gave 16 an adapter; the other 26 are accepted by `subscribe` and then
  never emit. 24 of them were live emitters in 0.13.1 — every `invoice:*`,
  every `swap:*`, `sync:started`/`:error`/`:provider` and more. CONNECT.md now
  carries the full removed-with-no-adapter table.
- `payment_request:accepted` and `:response` were not fabrications: both are
  declared in 0.13.1's SphereEventType, and `:response` is emitted by
  PaymentsModule. They were removed by the flip without an adapter.

Defects:

- badgeFor painted an in-flight `submitted` transfer with the success green,
  as it did any payload with no `status`. Colour is now driven by the settled
  set (confirmed/delivered/completed) rather than a blocklist, so an unknown
  or absent status can no longer answer "did it go through?" with yes.
- The bot registered its `inventory:updated` listener after the mint and after
  the first `assets()` round trip, so the credit event it exists to catch could
  fire with nobody attached. Subscribe first, mint second, read third — the
  boot read now goes through the same epoch guard.
- mockSphere's paymentsV2 lacked `requests`, which the host dereferences in
  `sphere.paymentsV2?.requests.list()` — the optional chain guards paymentsV2,
  not requests, so the first live `payment_request:updated` would throw a
  TypeError inside ConnectHost. Masked only by `on` being a no-op stub.

Also:

- sendSafety.test.ts could not detect a code the SDK ADDS, which is the
  direction that costs money. It now sweeps the whole SphereErrorCode universe
  through the SDK predicate, with a type-level guard that fails `tsc` and names
  any code a future SDK adds.
- formatAssets moved to bot/src/balance.ts so it is reachable by tests at all;
  index.ts calls main() at module scope.
- nodejs describeConnectFailure and backend-auth describeVersionFloor each
  handled one of the three handshake-refusal shapes. Both now cover the SDK
  floor, the protocol floor and the 4008 network mismatch — the last being what
  a dApp that omits `network` actually hits. The latter is renamed
  describeHandshakeRefusal to match what it does.
- CI runs npm ci instead of npm install. The comment justifying npm install
  described a file: link that no lockfile carries any more, and npm install
  will not fail when package.json and package-lock.json disagree.
@KruGoL
KruGoL merged commit 192edda into chore/sdk-0.14.2 Aug 8, 2026
5 checks passed
KruGoL added a commit that referenced this pull request Aug 8, 2026
* chore(sdk): 0.14.1 -> 0.14.2 across all five packages

* fix: correct three false SDK claims and three real defects from the #23 audit (#25)

The bump PR shipped documentation that teaches code which cannot work, plus
three defects no test could see. Nothing crashed, which is why it landed.

False claims, each verified against the pinned SDK:

- The refusal a pre-flip dApp receives names its version. ConnectClient has
  sent `sdkVersion` in the handshake since 0.10.1, so a 0.13.1 client is
  refused with `actualSdk: "0.13.1"`, not `null`. The docs presented the
  null/"unknown (not reported)" case as what an old dApp sees, and the browser
  and backend-auth tests were written around that unreachable branch. A reader
  branching on `actualSdk == null` gets dead code.
- "Every pre-0.14 name still fires" is wrong. The flip removed 38 event names
  and gave 16 an adapter; the other 26 are accepted by `subscribe` and then
  never emit. 24 of them were live emitters in 0.13.1 — every `invoice:*`,
  every `swap:*`, `sync:started`/`:error`/`:provider` and more. CONNECT.md now
  carries the full removed-with-no-adapter table.
- `payment_request:accepted` and `:response` were not fabrications: both are
  declared in 0.13.1's SphereEventType, and `:response` is emitted by
  PaymentsModule. They were removed by the flip without an adapter.

Defects:

- badgeFor painted an in-flight `submitted` transfer with the success green,
  as it did any payload with no `status`. Colour is now driven by the settled
  set (confirmed/delivered/completed) rather than a blocklist, so an unknown
  or absent status can no longer answer "did it go through?" with yes.
- The bot registered its `inventory:updated` listener after the mint and after
  the first `assets()` round trip, so the credit event it exists to catch could
  fire with nobody attached. Subscribe first, mint second, read third — the
  boot read now goes through the same epoch guard.
- mockSphere's paymentsV2 lacked `requests`, which the host dereferences in
  `sphere.paymentsV2?.requests.list()` — the optional chain guards paymentsV2,
  not requests, so the first live `payment_request:updated` would throw a
  TypeError inside ConnectHost. Masked only by `on` being a no-op stub.

Also:

- sendSafety.test.ts could not detect a code the SDK ADDS, which is the
  direction that costs money. It now sweeps the whole SphereErrorCode universe
  through the SDK predicate, with a type-level guard that fails `tsc` and names
  any code a future SDK adds.
- formatAssets moved to bot/src/balance.ts so it is reachable by tests at all;
  index.ts calls main() at module scope.
- nodejs describeConnectFailure and backend-auth describeVersionFloor each
  handled one of the three handshake-refusal shapes. Both now cover the SDK
  floor, the protocol floor and the 4008 network mismatch — the last being what
  a dApp that omits `network` actually hits. The latter is renamed
  describeHandshakeRefusal to match what it does.
- CI runs npm ci instead of npm install. The comment justifying npm install
  described a file: link that no lockfile carries any more, and npm install
  will not fail when package.json and package-lock.json disagree.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant