Files
suaveplan/openspec/archive/2026-05-17-add-currency-provider-open-exchange-rates/proposal.md
2026-05-27 23:29:53 +01:00

5.4 KiB

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/<date>.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=<key>&base=<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.