Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
# Add @suaveplan/currency-provider-cryptocompare sibling provider
**Package**: `@suaveplan/currency-provider-cryptocompare`
**Status**: ARCHIVED
**Type**: new package (sibling of slimmed `@suaveplan/currency-providers`)
- **Spec delta**: ./specs/currency-provider-cryptocompare/spec.md
## Why
The 2026-05-17 `slim-currency-providers` refactor moved the nine concrete
provider classes out of `@suaveplan/currency-providers` and committed each
one to its own publishable sibling package. This change lands the
`CryptoCompare` sibling — a crypto rate source backed by the
`min-api.cryptocompare.com/data/pricemulti` REST endpoint.
CryptoCompare distinguishes itself from the keyed siblings (Fixer, OXR,
CoinMarketCap) by being usable **fully keyless** — the free tier covers
~100,000 calls per month without any auth. Callers that want the higher
rate limit can supply an `apiKey` in the constructor config; the provider
attaches it on every request via the `authorization: Apikey <key>` header
per CryptoCompare's documented Apikey scheme.
## What changes
### ADDED
- `@suaveplan/currency-provider-cryptocompare` package at
`packages/finance/currency-provider-cryptocompare/`, version `0.1.0`,
universal-tier (`sideEffects: false`).
- `CryptoCompareProvider` class implementing `CurrencyProvider` from
`@suaveplan/currency`. Constructed from `CryptoCompareConfig` (extends
`NoKeyConfig`: `apiKey?` + `baseUrl?` + injectable `FetchLike?`).
- `supportsCrypto: true`, `supportsFiat: false`, no `supportsVirtual`.
CryptoCompare is a crypto-rates source — fiat rates are out of scope
for this sibling (consumers wanting fiat install `currency-provider-ecb`
/ `currency-provider-fixer` / `currency-provider-open-exchange-rates`).
- `fetchRates(options?)` calls
`GET <baseUrl>/pricemulti?fsyms=<targets>&tsyms=<base>` via the shared
`httpRequest()` helper from `@suaveplan/currency-providers`. The
`authorization: Apikey <key>` header is attached **only when**
`config.apiKey` is provided and non-whitespace.
- 15 default crypto symbols (BTC, ETH, SOL, USDT, USDC, BNB, XRP, ADA,
DOGE, DOT, AVAX, LINK, UNI, ATOM, LTC) when callers omit `targets` or
pass an empty array. Default base currency: `"USD"`.
- Response parsing raises `CurrencyProviderError(PARSE_ERROR)` when the
payload is not valid JSON or is not a JSON object.
- Response parsing raises `CurrencyProviderError(UPSTREAM_ERROR)` when
the payload carries `Response: "Error"`. The `Message` field (or
`"unknown upstream error"` when absent) is copied to
`error.upstreamMessage`.
- `fetchRates()` raises `CurrencyProviderError(UNKNOWN_CURRENCY)` when
no requested symbol produces a finite positive quote in the configured
base currency.
- Non-2xx HTTP responses surface as `CurrencyProviderError(HTTP_ERROR)`
(raised by the shared `httpRequest()` helper, with `status` populated).
- README, sibling `.md` per implementation file, and subpath export for
`./cryptocompare/cryptocompare`.
### KEPT (from slim core, consumed unchanged)
- `CurrencyProviderError` + `CURRENCY_PROVIDER_CODES` registry
(`HTTP_ERROR`, `PARSE_ERROR`, `UPSTREAM_ERROR`, `UNKNOWN_CURRENCY`).
- `NoKeyConfig` shared config type (extended with the optional `apiKey`).
- `httpRequest()` shared HTTP helper.
- `FetchLike` from `@suaveplan/types`.
## Source mapping
| Module | Source |
|---|---|
| `CryptoCompareProvider` | `../suaveplan/packages/finance/currency-providers/src/crypto/cryptocompare.ts` L1-L69 (upstream class) |
| Error mapping (`PARSE_ERROR` / `UPSTREAM_ERROR` / `UNKNOWN_CURRENCY`) | genesis-original — upstream threw raw `Error`; the slim core's typed `CurrencyProviderError` registry replaces that |
| `httpRequest` / `composeAbortSignal` plumbing | `@suaveplan/currency-providers/src/http/http.ts` (slim core, genesis-original) |
| Optional API-key header (`authorization: Apikey <key>`) | CryptoCompare public documentation — Apikey auth scheme; not present in the upstream class (which ignored the key entirely) |
| Inverted-price + zero-skip semantics | upstream behaviour preserved (`rates[symbol] = 1 / prices[base]` when `prices[base] > 0`) |
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/currency-provider-cryptocompare`
to enumerate.
- No current dependents (new sibling package; verified by
`bunx turbo ls --filter=...@suaveplan/currency-provider-cryptocompare`).
The package's only upstream dependencies are
`@suaveplan/currency-providers` (slim core, `httpRequest` / error registry
/ `NoKeyConfig`), `@suaveplan/currency` (the `CurrencyProvider` contract),
`@suaveplan/types` (`FetchLike`), `@suaveplan/error` (transitive), and
`tslib`. None of those packages need to be touched by this change — the
sibling consumes their public surfaces unchanged.
## Out of scope
- Historical-rate endpoints (`/v2/histoday`, `/v2/histohour`,
`/v2/histominute`), OHLCV, WebSocket streaming. Same deferral as every
other sibling provider — interface extension lands once
`@suaveplan/currency`'s `CurrencyProvider` contract grows the slots.
- Rate-limit and retry orchestration. Deferred to `@suaveplan/rate-limiter`
and `@suaveplan/retry` integrations once those wrappers land — every
sibling provider gains them uniformly through a single composition.
- Multi-tsym ("multi-base") queries. `pricemulti` accepts a CSV `tsyms`
list but the `CurrencyProvider` contract returns a single-base map;
multi-base aggregation belongs in a future wrapper.
- CryptoCompare-specific metadata endpoints (`coin/general`, social,
news). Out of scope for a rates provider; a future
`@suaveplan/cryptocompare` package can expose those if needed.
- Fiat rates. CryptoCompare supports fiat tsyms but the sibling stays
scoped to crypto (`supportsFiat: false`) per the slim-core sibling
pattern; consumers needing fiat install one of the dedicated fiat
siblings.

View File

@@ -0,0 +1,153 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: currency-provider-cryptocompare (currency-provider-cryptocompare)
**Package**: `@suaveplan/currency-provider-cryptocompare`
**Status**: ARCHIVED
> Every requirement below carries a `**Verified by:**` line at authoring time.
## ADDED Requirements
### Requirement: CryptoCompareProvider — public pricemulti rate fetch
The package SHALL export `CryptoCompareProvider` implementing
`CurrencyProvider` from `@suaveplan/currency` with `name === "cryptocompare"`,
`supportsCrypto === true`, `supportsFiat === false`. `fetchRates(options?)`
SHALL call
`GET <baseUrl>/pricemulti?fsyms=<targets>&tsyms=<base>` via the shared
`httpRequest` helper from `@suaveplan/currency-providers`, defaulting
`base` to `"USD"` and `targets` to the 15-symbol built-in list when
`options.targets` is omitted or empty. Returned `rates` SHALL be the
inverted spot price (`1 / price`) for every symbol whose quote in the
requested `base` is a finite positive number; non-finite and non-positive
quotes SHALL be skipped.
#### Scenario: defaults base to USD and inverts pricemulti payload
- **WHEN** `fetchRates()` is called against
`{ BTC: { USD: 67000 }, ETH: { USD: 3500 }, FAKE: { USD: 0 } }`
- **THEN** `result.base === "USD"`, `result.rates.BTC` ≈ `1 / 67000`,
`result.rates.ETH` ≈ `1 / 3500`, and `result.rates.FAKE` is absent.
#### Scenario: honours custom baseUrl and explicit targets
- **WHEN** `fetchRates({ base: "EUR", targets: ["BTC", "ETH"] })` is called
with `baseUrl: "https://staging.example.com/data"`
- **THEN** the request URL is exactly
`https://staging.example.com/data/pricemulti?fsyms=BTC,ETH&tsyms=EUR`.
#### Scenario: empty targets array falls back to built-in defaults
- **WHEN** `fetchRates({ base: "USD", targets: [] })` is called
- **THEN** the request URL `fsyms` parameter starts with
`BTC,ETH,SOL,USDT`.
**Verified by:** `src/cryptocompare/cryptocompare.test.ts::"reports stable metadata identifying the provider"` AND `src/cryptocompare/cryptocompare.test.ts::"inverts pricemulti payload, skips zero prices, and defaults base to USD"` AND `src/cryptocompare/cryptocompare.test.ts::"honours custom baseUrl and explicit targets list"` AND `src/cryptocompare/cryptocompare.test.ts::"falls back to defaults when targets array is empty"`
---
### Requirement: Optional API key attached via Apikey authorization header
The package SHALL accept an **optional** `apiKey` field on the constructor
config. When `apiKey` is provided and non-whitespace, every outbound
request SHALL carry an `authorization: Apikey <key>` header per
CryptoCompare's documented Apikey auth scheme. When `apiKey` is omitted,
`undefined`, or whitespace-only, the request SHALL be issued with no
authorization header (keyless free-tier mode).
#### Scenario: keyless construction issues no authorization header
- **WHEN** `new CryptoCompareProvider({ fetch }).fetchRates(...)` runs
- **THEN** the injected fetch sees `init?.headers === undefined`.
#### Scenario: keyed construction issues authorization: Apikey <key>
- **WHEN** `new CryptoCompareProvider({ fetch, apiKey: "secret-key" }).fetchRates(...)`
runs
- **THEN** the injected fetch sees
`init.headers.authorization === "Apikey secret-key"`.
#### Scenario: whitespace-only apiKey is treated as absent
- **WHEN** `new CryptoCompareProvider({ fetch, apiKey: " " }).fetchRates(...)`
runs
- **THEN** the injected fetch sees `init?.headers === undefined`.
**Verified by:** `src/cryptocompare/cryptocompare.test.ts::"sends authorization header only when an apiKey is provided"` AND `src/cryptocompare/cryptocompare.test.ts::"treats whitespace-only apiKey as absent and omits the header"`
---
### Requirement: Typed-error mapping for HTTP, parse, upstream, and unknown-currency failures
The package SHALL surface every failure as `CurrencyProviderError` from
`@suaveplan/currency-providers` with `providerName === "cryptocompare"`.
Non-2xx responses SHALL raise `HTTP_ERROR` (via the shared `httpRequest`
helper, populating `status`). Invalid JSON bodies and non-object parsed
payloads SHALL raise `PARSE_ERROR`. Payloads carrying `Response: "Error"`
SHALL raise `UPSTREAM_ERROR` with `upstreamMessage` set to the upstream
`Message` field, or to `"unknown upstream error"` when `Message` is
absent. `fetchRates()` SHALL raise `UNKNOWN_CURRENCY` when no requested
symbol resolves to a finite positive quote in the configured `base`.
#### Scenario: non-2xx response raises HTTP_ERROR
- **WHEN** the injected fetch returns HTTP 502
- **THEN** the call rejects with `code === "CURRENCY_PROVIDER.HTTP_ERROR"`
and `status === 502`.
#### Scenario: invalid JSON body raises PARSE_ERROR
- **WHEN** the body is `"<<not json>>"` with `content-type: application/json`
- **THEN** the call rejects with `code === "CURRENCY_PROVIDER.PARSE_ERROR"`.
#### Scenario: non-object payload (e.g. JSON null) raises PARSE_ERROR
- **WHEN** the body is `"null"` parsed as JSON
- **THEN** the call rejects with `code === "CURRENCY_PROVIDER.PARSE_ERROR"`.
#### Scenario: upstream error payload raises UPSTREAM_ERROR
- **WHEN** the payload is
`{ Response: "Error", Message: "tsym XYZ is not available" }`
- **THEN** the call rejects with
`code === "CURRENCY_PROVIDER.UPSTREAM_ERROR"` and `upstreamMessage`
contains `"tsym XYZ"`.
#### Scenario: upstream error payload without Message uses default text
- **WHEN** the payload is `{ Response: "Error" }`
- **THEN** the call rejects with
`code === "CURRENCY_PROVIDER.UPSTREAM_ERROR"` and
`upstreamMessage === "unknown upstream error"`.
#### Scenario: empty result map raises UNKNOWN_CURRENCY
- **WHEN** `fetchRates({ base: "USD", targets: ["BTC"] })` is called and
the payload is `{}`
- **THEN** the call rejects with
`code === "CURRENCY_PROVIDER.UNKNOWN_CURRENCY"`.
#### Scenario: every quote non-finite or non-positive raises UNKNOWN_CURRENCY
- **WHEN** the payload contains only `0`, `NaN`, and negative quotes
- **THEN** the call rejects with
`code === "CURRENCY_PROVIDER.UNKNOWN_CURRENCY"`.
**Verified by:** `src/cryptocompare/cryptocompare.test.ts::"raises HTTP_ERROR on non-2xx response"` AND `src/cryptocompare/cryptocompare.test.ts::"raises PARSE_ERROR when the response body is not valid JSON"` AND `src/cryptocompare/cryptocompare.test.ts::"raises PARSE_ERROR when the parsed payload is not an object"` AND `src/cryptocompare/cryptocompare.test.ts::"raises UPSTREAM_ERROR when payload carries Response='Error' with Message"` AND `src/cryptocompare/cryptocompare.test.ts::"raises UPSTREAM_ERROR with default message when Message field is absent"` AND `src/cryptocompare/cryptocompare.test.ts::"raises UNKNOWN_CURRENCY when no symbols resolve to a quote"` AND `src/cryptocompare/cryptocompare.test.ts::"raises UNKNOWN_CURRENCY when every quote is non-finite or non-positive"`
---
### Requirement: Caller AbortSignal forwarded into shared httpRequest
The package SHALL forward `RateFetchOptions.signal` (when provided)
through the shared `httpRequest` helper so caller-initiated aborts
propagate to the underlying `FetchLike` call.
#### Scenario: caller signal reaches the injected fetch
- **WHEN** `fetchRates({ signal: controller.signal, ... })` is called with
a recording `FetchLike`
- **THEN** the recorded `init.signal` is exactly `controller.signal`.
**Verified by:** `src/cryptocompare/cryptocompare.test.ts::"forwards a caller AbortSignal through httpRequest"`

View File

@@ -0,0 +1,88 @@
# Tasks — Add @suaveplan/currency-provider-cryptocompare
## Spec-completeness preflight (AGENTS.md §12a)
- [x] Numbered checklist of every `### Requirement` and `#### Scenario`
drafted in `specs/currency-provider-cryptocompare/spec.md`
(4 requirements, 11 scenarios).
## Package scaffolding
- [x] Create `packages/finance/currency-provider-cryptocompare/` with
`package.json` (`0.1.0`, sibling deps on slim core / currency /
types / error / tslib), `tsconfig.json`, `tsconfig.lib.json`,
`vite.config.ts`, `vitest.config.ts`.
- [x] Create `tests/setup/polyfills.ts` no-op shim mirroring sibling
packages.
## Implementation
- [x] `src/cryptocompare/cryptocompare.ts` — `CryptoCompareConfig`
(extends `NoKeyConfig` with optional `apiKey`), `CryptoCompareProvider`
class implementing `CurrencyProvider`. Defaults: `name =
"cryptocompare"`, `supportsCrypto = true`, `supportsFiat = false`,
base `"USD"`, 15-symbol default `targets`.
- [x] Optional `apiKey` attached via `authorization: Apikey <key>` header
on every request when present and non-whitespace.
- [x] Error mapping: `HTTP_ERROR` (shared `httpRequest`), `PARSE_ERROR`
(invalid JSON / non-object payload), `UPSTREAM_ERROR`
(`Response === "Error"`), `UNKNOWN_CURRENCY` (no positive quotes).
- [x] `src/cryptocompare/index.ts` explicit-named barrel.
- [x] `src/index.ts` explicit-named root barrel.
## Tests
- [x] `src/cryptocompare/cryptocompare.test.ts` — runner imports from
`@suaveplan/testing/runner`, recording `FetchLike`, 14 cases
covering: metadata, default invert + zero-skip + USD default,
keyless vs keyed header, whitespace-only key treated as absent,
custom `baseUrl` + explicit targets, empty `targets` falls back to
defaults, `HTTP_ERROR`, `PARSE_ERROR` (invalid JSON), `PARSE_ERROR`
(non-object payload), `UPSTREAM_ERROR` (with `Message`),
`UPSTREAM_ERROR` (no `Message` → default message),
`UNKNOWN_CURRENCY` (empty map), `UNKNOWN_CURRENCY` (every quote
non-finite or non-positive), `AbortSignal` forwarding. Every test
carries `{ timeout: 5000 }`.
## Documentation
- [x] `src/cryptocompare/cryptocompare.md` sibling doc (≥200 words,
Purpose / Features / Usage Basic / Usage Advanced / API Reference /
Implementation Notes).
- [x] Root `README.md` (install, quick start, modules link, error table).
## Pre-archive gates (AGENTS.md §13)
- [x] Step 0 — spec reconciliation: every `### Requirement` and
`**Verified by:**` resolves to a real file + verbatim test name.
- [x] Step 1 — `bunx turbo lint typecheck test --filter=@suaveplan/currency-provider-cryptocompare`
green.
- [x] Step 2 — `bunx turbo test:coverage --filter=@suaveplan/currency-provider-cryptocompare`
reports 100/100/100/100 via istanbul.
- [x] Step 3 — `bunx turbo build --filter=@suaveplan/currency-provider-cryptocompare`
green.
- [x] Step 4 — banned-pattern grep returns zero.
- [x] Step 4a — barrel-shape grep returns zero (`src/index.ts` exempt).
- [x] Step 4b — test-runner-import grep returns zero.
- [x] Step 5 — missing-`.md` grep returns zero.
- [x] Step 6 — README present.
- [x] Step 7 — bare-version grep returns zero.
- [x] Step 8 — subpath-export count = 1 (`./cryptocompare/cryptocompare`).
- [x] Step 9 — security-scan grep returns zero.
- [x] Step 10 — no dependents (new package).
## Root-doc refresh (AGENTS.md §13b)
- [x] Re-run `bun scripts/folder-reorg/generate-manifest.ts` to refresh
`MANIFEST.md`.
- [x] Re-run `bun scripts/generate-missing.ts` to refresh `MISSING.yml`.
- [x] Update `STATUS.md` row for `@suaveplan/currency-provider-cryptocompare`.
- [x] Update `SUMMARY.md` row likewise.
- [x] `bunx markdownlint-cli2 --fix` across touched root + change files.
## Archive (AGENTS.md §14)
- [x] Fold `specs/currency-provider-cryptocompare/spec.md` into
`openspec/specs/currency-provider-cryptocompare/spec.md`.
- [x] Move change folder to
`openspec/archive/2026-05-17-add-currency-provider-cryptocompare/`.