# Add @suaveplan/currency-provider-open-exchange-rates **Package**: `@suaveplan/currency-provider-open-exchange-rates` **Status**: PROPOSED **Type**: new-package - **Spec delta**: ./specs/currency-provider-open-exchange-rates/spec.md ## Why The 2026-05-17 slim of `@suaveplan/currency-providers` removed the nine bundled provider implementations and committed to landing each as an independent sibling package on the foundation-with-sibling-plugins shape established by `@suaveplan/network-plugin-*` and `@suaveplan/drive-plugin-*`. This change lands the Open Exchange Rates (OXR) provider so consumers that only need OXR install one small package instead of the bundled umbrella, and OXR can iterate on its own version cadence when the upstream API changes (free-plan base-only, historical endpoints, currency-list updates). ## What changes - ADDED: `OpenExchangeRatesProvider` — class implementing `CurrencyProvider` from `@suaveplan/currency`; consumes `httpRequest()` from `@suaveplan/currency-providers`; raises `CurrencyProviderError` with codes `HTTP_ERROR`, `PARSE_ERROR`, `UPSTREAM_ERROR`, `MISSING_CONFIG`, `UNSUPPORTED_BASE`, `UNKNOWN_CURRENCY`. - ADDED: subpath export `./provider/provider` for tree-shakeable import. - ADDED: package root barrel re-exporting `OpenExchangeRatesProvider` and the type alias `OpenExchangeRatesProviderConfig` (= `ApiKeyConfig` from `@suaveplan/currency-providers`). ## Out of scope - Historical-rate endpoint (`/historical/.json`) — deferred to a follow-up change. The upstream slim refactor's `## Out of scope` block explicitly defers historical rates for every provider in the wave; this package keeps parity with that boundary. - Retry / backoff / rate-limit-header surfacing / OTel — deferred to the same cross-cutting follow-up that the slim core deferred them to. - The five other OXR endpoints (`/currencies.json`, `/usage.json`, `/convert/{value}/{from}/{to}`, `/ohlc.json`, `/time-series.json`) — the `CurrencyProvider` contract only exposes bulk-fetch via `/latest.json`; the rest are out of contract. ## Source mapping This change derives from `../suaveplan/enterprise/currency-providers.md` and the upstream OXR implementation at `../suaveplan/packages/finance/currency-providers/src/fiat/open-exchange-rates.ts`. | Requirement | Source lines | Notes | |---|---|---| | OXR-001 implements `CurrencyProvider` with `supportsFiat=true`, `supportsCrypto=false`, `name="open-exchange-rates"` | `open-exchange-rates.ts` L15-L19 | verbatim, plus tier purity (no Node-only APIs) | | OXR-002 fetches `/latest.json?app_id=&base=` via `httpRequest()` | `open-exchange-rates.ts` L28-L37 | reworded — upstream called `fetch()` directly; genesis routes through slim-core `httpRequest()` per slim spec | | OXR-003 maps response `{ base, rates, timestamp }` to `RateFetchResult` with `timestamp * 1000` to convert OXR's Unix seconds to ms | `open-exchange-rates.ts` L39-L43, L56-L60 | verbatim | | OXR-004 honours `RateFetchOptions.targets` by filtering `rates` map | `open-exchange-rates.ts` L46-L54 | verbatim | | OXR-005 free-tier limits base to USD; explicit non-USD base raises `UNSUPPORTED_BASE` | enterprise audit L12, OXR.md L11-L13, L41-L43 | extended — upstream silently let upstream API reject; genesis pre-rejects with a stable error code (the `UNSUPPORTED_BASE` code is already registered in slim core) | ### Requirements with no source line | Requirement | Justification | |---|---| | OXR-006 raises `MISSING_CONFIG` on empty `apiKey` | `MISSING_CONFIG` code exists in slim-core registry per archived slim spec; surfacing it from constructor is the genesis baseline for all api-key providers | | OXR-007 raises `PARSE_ERROR` when upstream payload is not the expected shape | Defensive validation — upstream library threw a generic JSON-parse error; genesis maps to typed `PARSE_ERROR` | | OXR-008 raises `UPSTREAM_ERROR` when upstream returns 2xx with `error: true` body | OXR documented `error/status/message` payload shape; upstream library missed it. Genesis surfaces typed `UPSTREAM_ERROR` with `upstreamMessage` populated | | OXR-009 raises `UNKNOWN_CURRENCY` when `options.targets` requests a code not in upstream response | `UNKNOWN_CURRENCY` code exists in slim-core registry; surfacing it instead of silently dropping the target gives callers a typed handle | | OXR-010 honours `RateFetchOptions.signal` for cancellation | Universal-tier abort propagation — upstream did not honour signal at all; genesis composes via `httpRequest()` which forwards to the injected fetch | | OXR-011 universal-tier purity — no Node APIs, no DOM APIs | Genesis-wide tier policy | | OXR-012 100% test coverage, sibling `.md` on every implementation file | Genesis-wide quality policy | ## Verification approach Every requirement carries a `**Verified by:**` line in the spec delta pointing at concrete `it(...)` titles. The test surface is: - `src/provider/provider.test.ts` — covers OXR-001 through OXR-011 with injected `FetchLike` recording calls and returning canned responses. - `tests/dep-graph.test.ts` — pins dependency invariants (only consumes `@suaveplan/currency`, `@suaveplan/currency-providers`, `@suaveplan/error`, `@suaveplan/types`). ## Migration impact None — additive only. No current consumers reference this package; the slim core's `## Affected dependents` block listed this package as one of nine sibling consumers that would land in the follow-up wave.