Skip to content

Split utils into core and platform - #244

Closed
droplister wants to merge 4 commits into
mainfrom
refactor/core-platform-split
Closed

Split utils into core and platform#244
droplister wants to merge 4 commits into
mainfrom
refactor/core-platform-split

Conversation

@droplister

@droplister droplister commented Aug 4, 2026

Copy link
Copy Markdown
Member

utils/ was 139 files and 32,332 lines — 42% of the codebase, and every security-critical module lived in it. The name said "small helpers"; the contents were the domain.

That mis-naming isn't cosmetic. It's why my own review ranking went wrong earlier: sorting by directory or by direct import count put an icon barrel at the top and buried the modules everything depends on. api-status.ts has two direct importers and 290 files transitively depending on it — sitting unfiled at the root of "utils".

Why this split, on its own merits

The argument is a security property made enforceable, not a folder preference.

For a wallet, the code that derives keys, builds transactions and verifies them should be unable to depend on browser or session state. That's what lets it be audited in isolation, tested without mocking a browser, and reasoned about without asking "what was the extension doing at the time?"

I measured whether that property already held: every file checked for whether it — or anything it transitively imports — touches chrome.*, browser.*, wxt or webext-bridge.

coupled total
blockchain/ 0 77 portable
validation/ 0 14 portable
encryption/, qr-code/, hardware/ 0 9 portable
storage/ 9 10 platform
auth/ 2 2 platform

100 of 139 files are fully portable, and exactly one file violated the boundary. So the property was already true by accident. This makes it true on purpose:

  • core/ — cannot reach the extension runtime: message packing and unpacking, signing, key derivation, encryption, validation, formatting, the API client and its cache
  • platform/ — is the extension: chrome storage, the session keychain, the dapp bridge, analytics, popup and proxy plumbing

The secondary benefit is ergonomic: "where does this new file go?" becomes an objective question — does it need chrome? — instead of a matter of taste. That's what stops a utils/ forming again.

One file crossed the line

walletManager.ts, the orchestrator, which reaches for session storage. Moving that single file took core's dependencies on platform from three to zero. Nothing else needed splitting; every other folder moved whole.

The rule is enforced, not documented

core/__tests__/layering.test.ts asserts platform may call into core but core may not reach back, and that no file under core touches chrome.* or imports webext-bridge / #imports / wxt/. An editor auto-import would otherwise undo this silently, and the entire value is in the direction holding.

Alternatives considered

  • Rename utils/core/ and just file the loose root files. Less churn, fixes the naming, but creates no enforceable boundary — the thing that stops this drifting back.
  • Feature/vertical slices (wallet/, transactions/, dapp/). Popular, and already how pages/ is organised — but wrong here. blockchain/bitcoin/psbt.ts isn't owned by one feature; this layer is genuinely shared infrastructure, and slicing it would duplicate or scatter it.
  • Leave it and only file the root files. Cheapest, and legitimate if the churn isn't wanted. It leaves the 42%-named-"utils" problem in place.

Scope

534 files, +1227 / −1176 — but 269 are renames, and the edits are import specifiers plus Biome reordering them. No logic changed.

Verification

  • tsc --noEmit clean
  • biome check src clean (679 files)
  • vitest run src4049 passed, 49 skipped, 217 files (up 3: the new layering test)
  • wxt build succeeds, bundle unchanged at 2.46 MB
  • playwright test on compose and dapp-approval paths — 20 passed
  • CI caught one thing the local sweep missed: workflow YAML still referenced src/utils/hardware. Fixed in a follow-up commit.

Note on in-flight work

#232 (Trezor Connect 10, draft) touches utils/hardware/trezorAdapter.ts, now core/hardware/. It will need rebasing whenever v10 stabilises.

utils/ was 139 files and 32k lines - 42% of the codebase and every
security-critical module in it. The name said "small helpers" and the
contents were the domain. That mis-naming is not cosmetic: ranking files by
directory or by direct import count put an icon barrel at the top and buried
the modules everything actually depends on.

The split is measured, not stylistic. Each file was checked for whether it
or anything it transitively imports touches chrome.*, browser.*, wxt or
webext-bridge - the things that cannot run outside an extension. The seam
was already there: blockchain (77 files), validation, encryption and qr-code
came back at 0% coupling, auth at 100% and storage at 90%.

  core/      what does not need an extension to run - message packing and
             unpacking, signing, key derivation, encryption, validation,
             formatting, the API client and its cache
  platform/  what does - chrome storage, the session keychain, the dapp
             bridge, analytics, the popup and proxy plumbing

Only one file crossed the line: walletManager, which is the orchestrator and
reaches for session storage. Moving it alone to platform took core's
dependencies on platform from three to zero. Nothing else needed splitting;
every other folder moved whole.

This mirrors the mobile app, which already separates @core from @platform -
so the shared half now has the same shape in both codebases.

The direction is asserted rather than documented, in
core/__tests__/layering.test.ts: platform may call into core, core may not
reach back, and no file under core may touch chrome.* or import
webext-bridge, #imports or wxt. An editor auto-import would otherwise undo
this quietly.

Mechanical throughout - 269 renames, and the edits are import specifiers
plus Biome reordering them. No logic changed.

Claude-Session: https://claude.ai/code/session_01CcjnCrgosSeshymXLBxdGj
The rewrite covered TypeScript and configs but not workflow YAML, so the
hardware job still ran vitest against src/utils/hardware and the emulator
job still filtered on a path that no longer exists.

Claude-Session: https://claude.ai/code/session_01CcjnCrgosSeshymXLBxdGj
The path rewrite covered TypeScript, configs and workflow YAML but not
markdown, so the ADR index and the trust-boundary note still linked into
src/utils. Fourteen dead links; every rewritten target is checked to exist.

Claude-Session: https://claude.ai/code/session_01CcjnCrgosSeshymXLBxdGj
Three groupings inside core, on the same rule the split used: a folder
should answer a question with an objective answer.

api/ - apiClient, api-status and cache existed as three loose root files
that only exist to serve each other: one issues the requests, one tracks
whether the endpoint is healthy, one caches the responses. Now
api/{client,status,cache}.ts.

blockchain/ removed. It contained bitcoin, counterparty and counterwallet
and nothing else, and nothing outside it was a chain - so it discriminated
nothing while prefixing 77 files. The longest path in core drops from 73
characters to 62.

Flattening collided two error modules that were both called errors.ts, so
they are now named for what they are: rpcErrors.ts holds the JSON-RPC and
provider codes, errors.ts holds the blockchain error hierarchy that most of
core throws.

security/ removed. It held cspValidation and replayPrevention, two unrelated
files under a word that describes nearly everything in a wallet - the same
shape as utils/, and able to absorb anything for the same reason. Both are
used by the provider surface rather than by each other: csp validation moves
to platform/provider/csp.ts, replay prevention to core, where the compose
flow also uses it.

Mechanical - moves plus import specifiers, and Biome reordering them. The
AUDIT.md links were rewritten again and every target checked to exist.

Claude-Session: https://claude.ai/code/session_01CcjnCrgosSeshymXLBxdGj
@droplister

Copy link
Copy Markdown
Member Author

Squashed into #249.

@droplister droplister closed this Aug 4, 2026
@droplister
droplister deleted the refactor/core-platform-split branch August 4, 2026 21:49
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