# Slim @suaveplan/currency-providers to a thin plugin core **Package**: `@suaveplan/currency-providers` **Status**: PROPOSED **Type**: refactor (modification of the just-shipped 2026-05-17 migration) - **Design**: ./design.md - **Spec delta**: ./specs/currency-providers/spec.md ## Why The first shipped shape of `@suaveplan/currency-providers` bundled nine concrete providers (ECB, Fixer, Open Exchange Rates, Binance, CoinGecko, CoinMarketCap, CryptoCompare, Kraken, Virtual) into a single npm artefact. That bundling violates the foundation-with-sibling-plugins shape that `@suaveplan/network` and `@suaveplan/drive` established for genesis: every concrete plugin is its own publishable package so consumers install only what they wire in, and each plugin can iterate on its own version cadence without a coordinated bump of the umbrella. Consumers that want only ECB pay the install + tree-shake-graph cost of eight unused providers. Worse, an upstream API change in (say) CoinMarketCap forces a rev of the umbrella that every other provider also ships under. Splitting into `currency-providers` (the contract + registry + shared HTTP helper) and nine `currency-provider-` siblings (in the follow-up wave) gives consumers granular installs, gives each provider an independent surface to evolve, and matches the established `network-plugin-*` / `drive-plugin-*` shape. This change is the **slim**: it deletes the nine concrete provider modules from this package, adds the `CurrencyProviderRegistry` + `defineCurrencyProvider` factory + `PROVIDER_NOT_REGISTERED` code that the nine sibling packages will consume, and factors the small HTTP utilities (`httpRequest`, `composeAbortSignal`) that every provider repeated by hand into a single shared helper. The nine sibling packages land in the follow-up wave; each will depend on this slimmed core and re-implement only its own provider. ## What changes ### REMOVED - `EcbProvider` — moves to `@suaveplan/currency-provider-ecb`. - `FixerProvider` — moves to `@suaveplan/currency-provider-fixer`. - `OpenExchangeRatesProvider` — moves to `@suaveplan/currency-provider-open-exchange-rates`. - `BinanceProvider` — moves to `@suaveplan/currency-provider-binance`. - `CoinGeckoProvider` + `getCoinGeckoId` — move to `@suaveplan/currency-provider-coingecko`. - `CoinMarketCapProvider` — moves to `@suaveplan/currency-provider-coinmarketcap`. - `CryptoCompareProvider` — moves to `@suaveplan/currency-provider-cryptocompare`. - `KrakenProvider` — moves to `@suaveplan/currency-provider-kraken`. - `VirtualCurrencyProvider` + `VirtualCurrencyProviderConfig` — move to `@suaveplan/currency-provider-virtual`. - All nine `./providers//` subpath exports drop from `package.json`. ### ADDED - `CurrencyProviderRegistry` — in-memory registry of `CurrencyProvider` instances keyed by `name`. Methods: `register(provider)`, `get(name)`, `has(name)`, `list()`, `unregister(name)`. `get(unknown)` throws `CurrencyProviderError(PROVIDER_NOT_REGISTERED)`; `register(duplicate)` throws `CurrencyProviderError(PROVIDER_ALREADY_REGISTERED)`. - `defineCurrencyProvider(definition)` — factory that returns a stable-shape `CurrencyProvider`. Lets sibling packages declare a provider as a plain object literal without re-implementing the class boilerplate. - `httpRequest()` shared helper — issues a request via the injected `FetchLike`, applies `HttpFetchOptions.timeoutMs` and `headers`, composes the caller's `AbortSignal` with the timeout signal, throws `CurrencyProviderError(HTTP_ERROR)` on non-2xx with `status` populated. - `composeAbortSignal()` shared helper — merges an optional caller signal with an optional timeout-derived signal into a single `AbortSignal`. - `PROVIDER_NOT_REGISTERED` and `PROVIDER_ALREADY_REGISTERED` codes added to `CURRENCY_PROVIDER_CODES`. ### KEPT (unchanged) - `ApiKeyConfig`, `NoKeyConfig`, `HttpFetchOptions`, `ProviderConfigBase` shared config types — every sibling provider extends these. - `CURRENCY_PROVIDER_CODES` registry (now 8 codes: original 6 plus the 2 registry codes) + `CurrencyProviderError` class with `providerName` / `status` / `upstreamMessage` discriminants. - Universal-tier purity, `sideEffects: false`, explicit named barrels, `@suaveplan/testing/runner` test imports, 100/100/100/100 coverage gate. ## Source mapping This refactor is a MODIFICATION of `openspec/archive/2026-05-17-add-currency-providers/`. The provider implementations now slated for removal are at: | Removed module | Source in just-shipped archive | |---|---| | `EcbProvider` | `src/providers/ecb/ecb.ts` L1-L111 (per archived spec L84-L122) | | `FixerProvider` | `src/providers/fixer/fixer.ts` (per archived spec L126-L158) | | `OpenExchangeRatesProvider` | `src/providers/open-exchange-rates/open-exchange-rates.ts` (per archived spec L162-L187) | | `BinanceProvider` | `src/providers/binance/binance.ts` (per archived spec L191-L217) | | `CoinGeckoProvider` + `getCoinGeckoId` | `src/providers/coingecko/coingecko.ts` (per archived spec L221-L249) | | `CoinMarketCapProvider` | `src/providers/coinmarketcap/coinmarketcap.ts` (per archived spec L253-L276) | | `CryptoCompareProvider` | `src/providers/cryptocompare/cryptocompare.ts` (per archived spec L280-L299) | | `KrakenProvider` | `src/providers/kraken/kraken.ts` (per archived spec L303-L325) | | `VirtualCurrencyProvider` | `src/providers/virtual/virtual.ts` (per archived spec L329-L375) | The new registry / factory / HTTP helper surface is genesis-original, modelled on the established foundation-with-sibling-plugins shapes: | New module | Reference shape | |---|---| | `CurrencyProviderRegistry` | `packages/network/network/src/manager/` `PluginManager` | | `defineCurrencyProvider` | `packages/drive/drive/src/plugin/` factory pattern | | `httpRequest` + `composeAbortSignal` | extracted from the duplicated `fetchImpl(url); if (!response.ok) throw HTTP_ERROR` block present in every provider being removed | The enterprise audit reference is `../suaveplan/enterprise/currency-providers.md`; the slim shape was not present upstream — it is a genesis-native architectural correction. ## Impact ### Affected dependents Run `bunx turbo ls --filter=...@suaveplan/currency-providers` to enumerate. At time of writing there are no current dependents (the package shipped on 2026-05-17 and the commerce stack downstream is still pending migration): - No current dependents (verified by `bunx turbo ls --filter=...@suaveplan/currency-providers`). The future wave of sibling provider packages will all depend on this slimmed core. Each is listed here so the next reviewer can see the blast radius the follow-up wave will land in: - `@suaveplan/currency-provider-ecb` — [x] consumes registry / factory / httpRequest / configs / error - `@suaveplan/currency-provider-fixer` — [x] consumes registry / factory / httpRequest / configs / error - `@suaveplan/currency-provider-open-exchange-rates` — [x] consumes registry / factory / httpRequest / configs / error - `@suaveplan/currency-provider-binance` — [x] consumes registry / factory / httpRequest / configs / error - `@suaveplan/currency-provider-coingecko` — [x] consumes registry / factory / httpRequest / configs / error - `@suaveplan/currency-provider-coinmarketcap` — [x] consumes registry / factory / httpRequest / configs / error - `@suaveplan/currency-provider-cryptocompare` — [x] consumes registry / factory / httpRequest / configs / error - `@suaveplan/currency-provider-kraken` — [x] consumes registry / factory / httpRequest / configs / error - `@suaveplan/currency-provider-virtual` — [x] consumes registry / factory / configs / error (no HTTP) - `@suaveplan/currency` — [ ] tests run / [x] docs reviewed / [x] no action needed (the `CurrencyProvider` contract lives in `@suaveplan/currency` and is unchanged by this refactor; the slim core depends on it, not the other way around) ## Migration The nine concrete provider classes are gone from `@suaveplan/currency-providers` and now live in dedicated sibling packages. Consumers must install the specific provider package they need and either instantiate it directly or register it with the new `CurrencyProviderRegistry`. ### Before ```ts import { CurrencyConverter } from "@suaveplan/currency"; import { EcbProvider, CoinGeckoProvider } from "@suaveplan/currency-providers"; const converter = new CurrencyConverter({ provider: new EcbProvider(), fallbackProviders: [new CoinGeckoProvider()], }); ``` ### After ```ts import { CurrencyConverter } from "@suaveplan/currency"; import { CurrencyProviderRegistry } from "@suaveplan/currency-providers"; import { EcbProvider } from "@suaveplan/currency-provider-ecb"; import { CoinGeckoProvider } from "@suaveplan/currency-provider-coingecko"; const registry = new CurrencyProviderRegistry(); registry.register(new EcbProvider()); registry.register(new CoinGeckoProvider()); const converter = new CurrencyConverter({ provider: registry.get("ecb"), fallbackProviders: [registry.get("coingecko")], }); ``` Direct instantiation without the registry is equally supported — the registry is a convenience for callers that wire many providers and want a single lookup surface. Sibling packages remain `CurrencyProvider`-compatible so the old `new EcbProvider()` form still works once the sibling package is installed. Consumers should run this search command to find call sites that need to add sibling-package imports: ```bash grep -rln "from ['\"]@suaveplan/currency-providers['\"]" \ packages/ apps/ src/ 2>/dev/null | grep -v "/dist/\|/coverage/" ``` Every hit that uses one of `EcbProvider`, `FixerProvider`, `OpenExchangeRatesProvider`, `BinanceProvider`, `CoinGeckoProvider`, `CoinMarketCapProvider`, `CryptoCompareProvider`, `KrakenProvider`, `VirtualCurrencyProvider`, `VirtualCurrencyProviderConfig`, or `getCoinGeckoId` needs its import rewritten to the matching `@suaveplan/currency-provider-` package. ## Out of scope - The nine sibling `@suaveplan/currency-provider-` packages — each ships under its own OpenSpec change in the follow-up wave per AGENTS.md §0c "Multi-package / cross-cutting feature sets" (one change per package; cross-cutting design lives once, in this package's `design.md`). - Rate-limit / retry / historical-rate / WebSocket / on-chain oracle / aggregator / health-monitor / OTel surfaces — same deferral as the 2026-05-17 archive's `## Out of scope`. Nothing in this slim refactor changes those plans; once those wrappers land they consume the `CurrencyProvider` contract through the registry exactly the same way.