Split utils into core and platform - #244
Closed
droplister wants to merge 4 commits into
Closed
Conversation
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
This was referenced Aug 4, 2026
Member
Author
|
Squashed into #249. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.tshas 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.*,wxtorwebext-bridge.blockchain/validation/encryption/,qr-code/,hardware/storage/auth/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 cacheplatform/— is the extension: chrome storage, the session keychain, the dapp bridge, analytics, popup and proxy plumbingThe 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.tsasserts platform may call into core but core may not reach back, and that no file undercoretoucheschrome.*or importswebext-bridge/#imports/wxt/. An editor auto-import would otherwise undo this silently, and the entire value is in the direction holding.Alternatives considered
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.wallet/,transactions/,dapp/). Popular, and already howpages/is organised — but wrong here.blockchain/bitcoin/psbt.tsisn't owned by one feature; this layer is genuinely shared infrastructure, and slicing it would duplicate or scatter it.Scope
534 files, +1227 / −1176 — but 269 are renames, and the edits are import specifiers plus Biome reordering them. No logic changed.
Verification
tsc --noEmitcleanbiome check srcclean (679 files)vitest run src— 4049 passed, 49 skipped, 217 files (up 3: the new layering test)wxt buildsucceeds, bundle unchanged at 2.46 MBplaywright teston compose and dapp-approval paths — 20 passedsrc/utils/hardware. Fixed in a follow-up commit.Note on in-flight work
#232 (Trezor Connect 10, draft) touches
utils/hardware/trezorAdapter.ts, nowcore/hardware/. It will need rebasing whenever v10 stabilises.