first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
# @suaveplan/currency-provider-open-exchange-rates - Universal @suaveplan/currency provider for openexchangerates.org. API-key (app_id) required. Free tier limited to USD base; paid tiers opt in via allowNonUsdBase. Routes every request through @suaveplan/currency-providers httpRequest() and raises typed CurrencyProviderError for HTTP / parse / upstream / config / base / currency failures.
Universal `@suaveplan/currency` provider for
[openexchangerates.org](https://openexchangerates.org). Routes `/latest.json`
through the slim-core `httpRequest()` helper, validates the upstream payload,
and raises typed `CurrencyProviderError` for every failure mode.
<!-- begin:auto-docs -->
## Documentation tree
- [provider](./src/provider/provider.md) — `OpenExchangeRatesProvider` is the `@suaveplan/currency` provider for openexchangerates.org. It wraps the `/latest.json…
- [provider](./src/provider/provider.md) — `OpenExchangeRatesProvider` is the `@suaveplan/currency` provider for openexchangerates.org. It wraps the `/latest.json…
<!-- end:auto-docs -->
## Install
```bash
bun add @suaveplan/currency-provider-open-exchange-rates
```
## Quick start
```ts
import { CurrencyConverter } from "@suaveplan/currency";
import { OpenExchangeRatesProvider } from "@suaveplan/currency-provider-open-exchange-rates";
const oxr = new OpenExchangeRatesProvider({
apiKey: process.env.OXR_APP_ID!,
// allowNonUsdBase: true, // paid plans only
});
const converter = new CurrencyConverter({ provider: oxr });
const usdToEur = await converter.getRate({ from: "USD", to: "EUR" });
console.log(usdToEur);
```
## Configuration
| Field | Type | Default | Notes |
| ----------------- | ----------- | --------------------------------------- | ---------------------------------------------------------------- |
| `apiKey` | `string` | — | OXR `app_id`. Empty string raises `MISSING_CONFIG`. |
| `baseUrl` | `string?` | `https://openexchangerates.org/api` | Override for staging / proxy fixtures. |
| `fetch` | `FetchLike?`| `globalThis.fetch` | Injectable for testing. |
| `allowNonUsdBase` | `boolean?` | `false` | Set `true` on paid plans to forward non-USD `base` requests. |
## Error codes
Every failure mode raises `CurrencyProviderError` from
`@suaveplan/currency-providers` with one of:
- `CURRENCY_PROVIDER.MISSING_CONFIG` — empty `apiKey`.
- `CURRENCY_PROVIDER.UNSUPPORTED_BASE` — non-USD base on free tier.
- `CURRENCY_PROVIDER.HTTP_ERROR` — non-2xx response (raised by `httpRequest()`).
- `CURRENCY_PROVIDER.PARSE_ERROR` — body is not JSON or missing required fields.
- `CURRENCY_PROVIDER.UPSTREAM_ERROR` — 2xx with OXR `{ error: true, … }` body.
- `CURRENCY_PROVIDER.UNKNOWN_CURRENCY` — `options.targets` contains a code absent from the upstream rates map.
## Modules
- [`src/provider/provider.md`](./src/provider/provider.md) — `OpenExchangeRatesProvider` class and config.
## Tier
Universal — runs in Node 18+, Bun, browsers, and Web Workers without any
runtime polyfill.

View File

@@ -0,0 +1,148 @@
# `OpenExchangeRatesProvider`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`OpenExchangeRatesProvider` is the `@suaveplan/currency` provider for
[openexchangerates.org](https://openexchangerates.org). It wraps the
`/latest.json` bulk-fetch endpoint, validates the upstream payload, and
surfaces every failure mode through the typed `CurrencyProviderError`
hierarchy from `@suaveplan/currency-providers`. The provider is universal
— it relies only on `globalThis.fetch` (or an injected `FetchLike`) and the
slim-core `httpRequest()` helper, so it runs unchanged in Node, Bun, browsers,
and Web Workers.
## Features
- Implements `CurrencyProvider` with `name === "open-exchange-rates"`,
`supportsFiat === true`, `supportsCrypto === false`.
- Constructor takes the OXR `app_id` as `apiKey`. Empty string raises
`CurrencyProviderError(MISSING_CONFIG)` synchronously.
- Free-tier safe by default: any `RateFetchOptions.base !== "USD"` raises
`CurrencyProviderError(UNSUPPORTED_BASE)` before any network call. Paid-plan
callers opt in with `allowNonUsdBase: true`.
- Routes every request through `@suaveplan/currency-providers` `httpRequest()`
so non-2xx responses surface `CurrencyProviderError(HTTP_ERROR)` with
`status` and `providerName` populated. Caller `AbortSignal` from
`RateFetchOptions.signal` is forwarded.
- Validates the upstream `{ base, rates, timestamp }` shape. Mismatches raise
`CurrencyProviderError(PARSE_ERROR)`.
- Detects the OXR 2xx error-payload shape (`{ error: true, status, message }`)
and raises `CurrencyProviderError(UPSTREAM_ERROR)` with `upstreamMessage`
carrying the upstream `message` field.
- Filters the rates map to `RateFetchOptions.targets`. Missing target codes
raise `CurrencyProviderError(UNKNOWN_CURRENCY)`.
- Converts the OXR Unix-seconds timestamp to milliseconds.
- Injectable `FetchLike` for offline testing; defaults to `globalThis.fetch`.
## Usage: Basic Example
```typescript
import { OpenExchangeRatesProvider } from "@suaveplan/currency-provider-open-exchange-rates";
const provider = new OpenExchangeRatesProvider({
apiKey: process.env.OXR_APP_ID ?? "",
});
const result = await provider.fetchRates({ base: "USD" });
console.log(result.base, result.rates.EUR, result.timestamp);
```
## Usage: Advanced Example
Wiring the provider into `CurrencyConverter` with a paid-tier app_id, custom
`baseUrl` for staging, and `AbortSignal`-driven cancellation:
```typescript
import { CurrencyConverter } from "@suaveplan/currency";
import { OpenExchangeRatesProvider } from "@suaveplan/currency-provider-open-exchange-rates";
const oxr = new OpenExchangeRatesProvider({
apiKey: process.env.OXR_APP_ID!,
allowNonUsdBase: true,
baseUrl: process.env.OXR_BASE_URL ?? "https://openexchangerates.org/api",
});
const converter = new CurrencyConverter({
provider: oxr,
staleDurationMs: 30 * 60 * 1000, // 30 minutes
});
const controller = new AbortController();
setTimeout(() => controller.abort(new Error("timeout")), 5000);
const rates = await oxr.fetchRates({
base: "EUR",
targets: ["USD", "GBP", "JPY"],
signal: controller.signal,
});
```
The slim-core `CurrencyProviderRegistry` is also supported when the consumer
wires multiple providers behind a single lookup surface — see
`@suaveplan/currency-providers` README.
## API Reference
### `OpenExchangeRatesProviderConfig`
Extends `ApiKeyConfig` from `@suaveplan/currency-providers`:
- `apiKey: string` — required. The OXR `app_id`.
- `baseUrl?: string` — defaults to `"https://openexchangerates.org/api"`.
- `fetch?: FetchLike` — defaults to `globalThis.fetch`.
- `allowNonUsdBase?: boolean` — defaults to `false`. When `false`, any
non-USD base raises `UNSUPPORTED_BASE` before any network call.
### `class OpenExchangeRatesProvider`
#### Properties
- `readonly name: "open-exchange-rates"`
- `readonly supportsFiat: true`
- `readonly supportsCrypto: false`
#### `constructor(config: OpenExchangeRatesProviderConfig)`
Throws `CurrencyProviderError(MISSING_CONFIG)` when `config.apiKey === ""`.
#### `fetchRates(options?: RateFetchOptions): Promise<RateFetchResult>`
Issues `GET ${baseUrl}/latest.json?app_id=<apiKey>&base=<base>` via
`httpRequest()`. Returns `{ base, rates, timestamp }` with `timestamp` in
milliseconds.
Throws:
- `MISSING_CONFIG` — from the constructor when `apiKey === ""`.
- `UNSUPPORTED_BASE` — when `options.base !== "USD"` and `allowNonUsdBase`
is not set to `true`.
- `HTTP_ERROR` — via `httpRequest()` for any non-2xx response.
- `PARSE_ERROR` — when the response body is not JSON, not an object, or is
missing one of `base` / `rates` / `timestamp` in the expected shape.
- `UPSTREAM_ERROR` — when the response is 2xx but the body is the OXR error
shape (`{ error: true, status, message }`).
- `UNKNOWN_CURRENCY` — when `options.targets` contains a code not present in
the upstream `rates` map.
## Implementation Notes
- The OXR free tier restricts the base currency to USD; the provider enforces
this client-side so the failure surfaces as a typed `UNSUPPORTED_BASE`
rather than a generic upstream error. Paid plans pass `allowNonUsdBase:
true` to opt out.
- OXR returns `timestamp` in Unix seconds; the provider multiplies by 1000 so
every `CurrencyProvider` answer is in milliseconds, matching
`RateFetchResult.timestamp`'s contract.
- OXR's error-payload shape is returned with a 2xx HTTP status, which would
otherwise pass `httpRequest()`'s non-2xx check silently. The provider
detects `{ error: true, … }` after parsing and surfaces `UPSTREAM_ERROR`
with `status` and `upstreamMessage` populated.
- The provider does not retry, dedupe, or rate-limit by itself — the slim
core's `## Out of scope` defers those concerns to a cross-cutting follow-up
wave so every sibling provider gains them in lockstep.
- `apiKey` is `encodeURIComponent`-wrapped in the query string to prevent
injection when callers pass keys containing reserved characters.