Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
10 changes: 5 additions & 5 deletions .changeset/admin-api-key-pagination.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
'lens': minor
---

Add pagination and a revoked filter to `GET /admin/keys`. `limit` (default 50, max 100) and `offset` page the listing, `revoked=true|false` (default `false`) selects live or revoked keys, and the response adds `pagination` metadata with the total matching count. Invalid values return 400.
---
'lens': minor
---
Add pagination and a revoked filter to `GET /admin/keys`. `limit` (default 50, max 100) and `offset` page the listing, `revoked=true|false` (default `false`) selects live or revoked keys, and the response adds `pagination` metadata with the total matching count. Invalid values return 400.
10 changes: 5 additions & 5 deletions .changeset/admin-structural-auth.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
'lens': patch
---

Guard admin endpoints structurally (#514). An `onRequest` hook enforces `ADMIN_TOKEN` over every route that declares `{ admin: true, auth: 'admin' }` or is registered under `/admin/`, so a new admin endpoint is protected by where it lives rather than by its handler remembering to call `isAdminAuthorized`. `public: true` now means genuinely unauthenticated, and all seven admin routes declare the admin tier instead. The guard reads the matched route pattern, not `req.url`, so a percent-encoded path cannot route to an admin handler while reading as non-admin.
---
'lens': patch
---
Guard admin endpoints structurally (#514). An `onRequest` hook enforces `ADMIN_TOKEN` over every route that declares `{ admin: true, auth: 'admin' }` or is registered under `/admin/`, so a new admin endpoint is protected by where it lives rather than by its handler remembering to call `isAdminAuthorized`. `public: true` now means genuinely unauthenticated, and all seven admin routes declare the admin tier instead. The guard reads the matched route pattern, not `req.url`, so a percent-encoded path cannot route to an admin handler while reading as non-admin.
10 changes: 5 additions & 5 deletions .changeset/admin-token-diagnostics.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
'lens': patch
---

Distinguish an unconfigured admin surface from a rejected token (#508). `checkAdminAuth` returns `authorized` / `unauthorized` / `not_configured`, and admin routes answer 503 `admin_not_configured` when `ADMIN_TOKEN` is unset, blank or an obvious placeholder, instead of a 401 that tells an operator their correct token is wrong. The structural admin hook makes the same three-way distinction, startup logs a warning when the surface is disabled, and `GET /status` reports `adminConfigured` — whether a token is set, never any part of its value.
---
'lens': patch
---
Distinguish an unconfigured admin surface from a rejected token (#508). `checkAdminAuth` returns `authorized` / `unauthorized` / `not_configured`, and admin routes answer 503 `admin_not_configured` when `ADMIN_TOKEN` is unset, blank or an obvious placeholder, instead of a 401 that tells an operator their correct token is wrong. The structural admin hook makes the same three-way distinction, startup logs a warning when the surface is disabled, and `GET /status` reports `adminConfigured` — whether a token is set, never any part of its value.
10 changes: 5 additions & 5 deletions .changeset/api-key-last-used.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
'lens': minor
---

Record when an API key was last used to authenticate (#500). `api_keys.last_used_at` is stamped on each successful authentication, throttled to one write per key per `KEY_LAST_USED_THROTTLE_SECONDS` (default 60) so authentication stays a read for busy keys, and surfaced on `GET /admin/keys` — null meaning the key has not authenticated since the column existed, which is what makes an abandoned key visible. A failed write never refuses a valid request, but is logged once per key rather than swallowed.
---
'lens': minor
---
Record when an API key was last used to authenticate (#500). `api_keys.last_used_at` is stamped on each successful authentication, throttled to one write per key per `KEY_LAST_USED_THROTTLE_SECONDS` (default 60) so authentication stays a read for busy keys, and surfaced on `GET /admin/keys` — null meaning the key has not authenticated since the column existed, which is what makes an abandoned key visible. A failed write never refuses a valid request, but is logged once per key rather than swallowed.
10 changes: 5 additions & 5 deletions .changeset/api-key-lookup-cache.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
'lens': minor
---

Cache API-key lookups in-process so authentication is no longer a Postgres round-trip per request (#495). Positive and negative lookups are held for `API_KEY_CACHE_TTL` seconds (default 30), the cache is bounded at 10,000 entries so invalid keys cannot grow it without limit, hit/miss is exported as `api_key_cache_lookups_total`, and a key's own `expiresAt` is re-checked on every hit so the TTL can never outlive the key. Revoking or rotating a key drops its cached entry immediately on the instance handling the request; another instance picks it up within the TTL. Rotation now also carries the old key's scopes to the replacement.
---
'lens': minor
---
Cache API-key lookups in-process so authentication is no longer a Postgres round-trip per request (#495). Positive and negative lookups are held for `API_KEY_CACHE_TTL` seconds (default 30), the cache is bounded at 10,000 entries so invalid keys cannot grow it without limit, hit/miss is exported as `api_key_cache_lookups_total`, and a key's own `expiresAt` is re-checked on every hit so the TTL can never outlive the key. Revoking or rotating a key drops its cached entry immediately on the instance handling the request; another instance picks it up within the TTL. Rotation now also carries the old key's scopes to the replacement.
48 changes: 24 additions & 24 deletions .changeset/api-versioning.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
---
'lens': minor
---

Serve every public route under a `/v1` version segment, keeping the
unversioned paths as deprecated aliases that answer identically.

`src/api/versioning.ts` strips the segment in a Fastify `rewriteUrl` hook before
the request is routed, so `/v1/price/XLM/USDC` and `/price/XLM/USDC` resolve to
the same route definition — same handler, same auth, same x402 gating and rate
limits — rather than being mounted twice and drifting. Existing callers are not
broken: the unversioned form keeps working, and every response to it now carries
`Deprecation: true` and a `Link: </v1/...>; rel="successor-version"` header
pointing at the versioned path. The versioned response carries neither.

`openapi.yaml` (and the generated `openapi.json`) now document the `/v1` paths;
`tests/openapi.test.ts` asserts that every documented versioned path maps onto a
registered route, that the unversioned alias exists for each, and that the two
forms answer identically. Operator-only and non-HTTP routes (`/metrics`, `/ws`,
`/admin/*`, `/graphiql*`) are excluded and remain unversioned.

The warning contract and the alias-retirement window — at least two minor
releases, no more than twelve months — are written down in
[`docs/api-versioning.md`](docs/api-versioning.md).
---
'lens': minor
---
Serve every public route under a `/v1` version segment, keeping the
unversioned paths as deprecated aliases that answer identically.
`src/api/versioning.ts` strips the segment in a Fastify `rewriteUrl` hook before
the request is routed, so `/v1/price/XLM/USDC` and `/price/XLM/USDC` resolve to
the same route definition — same handler, same auth, same x402 gating and rate
limits — rather than being mounted twice and drifting. Existing callers are not
broken: the unversioned form keeps working, and every response to it now carries
`Deprecation: true` and a `Link: </v1/...>; rel="successor-version"` header
pointing at the versioned path. The versioned response carries neither.
`openapi.yaml` (and the generated `openapi.json`) now document the `/v1` paths;
`tests/openapi.test.ts` asserts that every documented versioned path maps onto a
registered route, that the unversioned alias exists for each, and that the two
forms answer identically. Operator-only and non-HTTP routes (`/metrics`, `/ws`,
`/admin/*`, `/graphiql*`) are excluded and remain unversioned.
The warning contract and the alias-retirement window — at least two minor
releases, no more than twelve months — are written down in
[`docs/api-versioning.md`](docs/api-versioning.md).
10 changes: 5 additions & 5 deletions .changeset/bazaar-automatic-cataloging-130.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
'lens': minor
---

Automatic cataloging of resources from discovery extension upon settlement (#130)
---
'lens': minor
---
Automatic cataloging of resources from discovery extension upon settlement (#130)
24 changes: 12 additions & 12 deletions .changeset/bazaar-search-ssrf-guard.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
'lens': patch
---

`bazaar_search` no longer fetches any URL it is handed (#407). The MCP server now
parses the target, refuses anything that is not `http`/`https`, resolves the
hostname and rejects it if any address is private, loopback, link-local, or a
cloud metadata endpoint (e.g. `169.254.169.254`), and follows redirects manually
so a permitted host cannot bounce the request to a forbidden address. Operators
can constrain reachable hosts further with the shared `OUTBOUND_ALLOWED_HOSTS`
list, which narrows access but never re-opens a blocked address. Blocked requests
return the new `ERR_URL_BLOCKED` code.
---
'lens': patch
---
`bazaar_search` no longer fetches any URL it is handed (#407). The MCP server now
parses the target, refuses anything that is not `http`/`https`, resolves the
hostname and rejects it if any address is private, loopback, link-local, or a
cloud metadata endpoint (e.g. `169.254.169.254`), and follows redirects manually
so a permitted host cannot bounce the request to a forbidden address. Operators
can constrain reachable hosts further with the shared `OUTBOUND_ALLOWED_HOSTS`
list, which narrows access but never re-opens a blocked address. Blocked requests
return the new `ERR_URL_BLOCKED` code.
10 changes: 5 additions & 5 deletions .changeset/blend-lending-ingestion.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
'lens': minor
---

Ingest Blend lending pool state (#212). A dedicated ingester reads pool reserves over Soroban, derives utilisation and supply/borrow APR, and stores them in `lending_rates` — not `price_points`, because a lending rate is not a spot price. `GET /rates/:asset` serves them, a retention job prunes old rows, and the cycle is bounded by the same per-ingester deadline every other loop uses.
---
'lens': minor
---
Ingest Blend lending pool state (#212). A dedicated ingester reads pool reserves over Soroban, derives utilisation and supply/borrow APR, and stores them in `lending_rates` — not `price_points`, because a lending rate is not a spot price. `GET /rates/:asset` serves them, a retention job prunes old rows, and the cycle is bounded by the same per-ingester deadline every other loop uses.
14 changes: 7 additions & 7 deletions .changeset/coinbase-cex-adapter-215.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
"lens": minor
---

Ship the first real centralized-exchange adapter in `src/ingest/cex/` (issue #215). The directory previously held only the `CexAdapter` interface and a mock, so every cross-venue comparison compared Stellar venues against nothing. `CoinbaseAdapter` implements `CexAdapter` in full — `fetchTicker` and `fetchOrderBook` against Coinbase Exchange's public market-data endpoints — and is built and registered through a new exchange-agnostic `createCexAdapter(name)` factory (`registerCexAdapterFactory` wires concrete exchanges in).

It normalises pairs in both directions, mapping the Lens canonical form (`XLM/USD`) to the identifier the exchange actually uses (`XLM-USD`) and back, since a slash-form request 404s. Requests are throttled to Coinbase's published public limit (10 req/s per IP, burst up to 15) with a token bucket; a `429` honours `Retry-After` when present and otherwise backs off exponentially. Any outage, timeout, non-2xx, or malformed body degrades to a `null` return — the adapter never throws, so a dead exchange cannot take down the process or surface an error response. Prices carry a distinct `source` (`coinbase_cex`) and take the `network` from the caller rather than hard-coding it, so a CEX number is never mistaken for, or silently blended with, an on-chain one.
---
"lens": minor
---
Ship the first real centralized-exchange adapter in `src/ingest/cex/` (issue #215). The directory previously held only the `CexAdapter` interface and a mock, so every cross-venue comparison compared Stellar venues against nothing. `CoinbaseAdapter` implements `CexAdapter` in full — `fetchTicker` and `fetchOrderBook` against Coinbase Exchange's public market-data endpoints — and is built and registered through a new exchange-agnostic `createCexAdapter(name)` factory (`registerCexAdapterFactory` wires concrete exchanges in).
It normalises pairs in both directions, mapping the Lens canonical form (`XLM/USD`) to the identifier the exchange actually uses (`XLM-USD`) and back, since a slash-form request 404s. Requests are throttled to Coinbase's published public limit (10 req/s per IP, burst up to 15) with a token bucket; a `429` honours `Retry-After` when present and otherwise backs off exponentially. Any outage, timeout, non-2xx, or malformed body degrades to a `null` return — the adapter never throws, so a dead exchange cannot take down the process or surface an error response. Prices carry a distinct `source` (`coinbase_cex`) and take the `network` from the caller rather than hard-coding it, so a CEX number is never mistaken for, or silently blended with, an on-chain one.
10 changes: 5 additions & 5 deletions .changeset/decimal-strings-boundary.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
'lens': minor
---

Serialise decimal quantities as exact strings across the API boundary via header/query/Accept negotiation to preserve 18-decimal precision without IEEE-754 floating-point loss.
---
'lens': minor
---
Serialise decimal quantities as exact strings across the API boundary via header/query/Accept negotiation to preserve 18-decimal precision without IEEE-754 floating-point loss.
30 changes: 15 additions & 15 deletions .changeset/defindex-vault-adapter.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
---
'lens': minor
---

DeFindex vault shares are now priced. A new `DeFindex` venue adapter
(`src/ingest/venues/defindex.ts`) enumerates every vault from the protocol's
on-chain factory registry, reads each vault's total managed assets and total
share supply straight from the chain, and derives the share price as
`totalAssets / totalShares` — never from a published figure that could be stale.
A vault with zero shares yields no point rather than dividing by zero. Rows are
written with `source = 'defindex_vault'` so a derived vault price is never
silently blended with a traded one, and each row carries the network it was read
from, so the ingester runs once per `ENABLED_NETWORKS` entry without merging
mainnet and testnet series. The factory contract ids are taken whole from DeFindex's
published `mainnet.contracts.json` / `testnet.contracts.json`.
---
'lens': minor
---
DeFindex vault shares are now priced. A new `DeFindex` venue adapter
(`src/ingest/venues/defindex.ts`) enumerates every vault from the protocol's
on-chain factory registry, reads each vault's total managed assets and total
share supply straight from the chain, and derives the share price as
`totalAssets / totalShares` — never from a published figure that could be stale.
A vault with zero shares yields no point rather than dividing by zero. Rows are
written with `source = 'defindex_vault'` so a derived vault price is never
silently blended with a traded one, and each row carries the network it was read
from, so the ingester runs once per `ENABLED_NETWORKS` entry without merging
mainnet and testnet series. The factory contract ids are taken whole from DeFindex's
published `mainnet.contracts.json` / `testnet.contracts.json`.
10 changes: 5 additions & 5 deletions .changeset/discovery-search-129.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
'lens': minor
---

Add GET /discovery/search with relevance ranking and quality evaluation (#129)
---
'lens': minor
---
Add GET /discovery/search with relevance ranking and quality evaluation (#129)
10 changes: 5 additions & 5 deletions .changeset/empty-search-is-success.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
'lens': patch
---

An empty `bazaar_search` result is a success, not an error (#414). A search that ran correctly and matched nothing returned `isError: true` with `ERR_NO_RESULTS`, which an agent answers by retrying or rephrasing — a paid call spent on a question already answered. It now returns the empty collection. `ERR_NO_RESULTS` is unchanged for `lens_get_price`, where a refused pair really is an error.
---
'lens': patch
---
An empty `bazaar_search` result is a success, not an error (#414). A search that ran correctly and matched nothing returned `isError: true` with `ERR_NO_RESULTS`, which an agent answers by retrying or rephrasing — a paid call spent on a question already answered. It now returns the empty collection. `ERR_NO_RESULTS` is unchanged for `lens_get_price`, where a refused pair really is an error.
10 changes: 5 additions & 5 deletions .changeset/enforce-rate-per-day-175.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
'lens': patch
---

Enforce ratePerDay daily quota on authenticated API keys (#175)
---
'lens': patch
---
Enforce ratePerDay daily quota on authenticated API keys (#175)
10 changes: 5 additions & 5 deletions .changeset/failed-venue-fetch-is-not-empty.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
'lens': patch
---

A failed venue fetch is no longer indistinguishable from an empty venue (#329). The Horizon AMM pool fetch, the Soroswap token list, the Aquarius pool fetch and the Reflector oracle read now all return one result shape, `{ ok: true, data } | { ok: false, error }`, instead of turning an error into `[]` or `null`. A failure is logged (Reflector used to swallow it silently) and recorded per source, network and pair; the next successful fetch clears it. `aggregatePrice` excludes a failed source from the composite and from the deviation baseline instead of counting it as though it answered with nothing, and returns the failed sources as `failedSources`. `GET /status` lists them under `failedSources` (source, network, pair or asset, time; the error text stays in the log because the route is public).
---
'lens': patch
---
A failed venue fetch is no longer indistinguishable from an empty venue (#329). The Horizon AMM pool fetch, the Soroswap token list, the Aquarius pool fetch and the Reflector oracle read now all return one result shape, `{ ok: true, data } | { ok: false, error }`, instead of turning an error into `[]` or `null`. A failure is logged (Reflector used to swallow it silently) and recorded per source, network and pair; the next successful fetch clears it. `aggregatePrice` excludes a failed source from the composite and from the deviation baseline instead of counting it as though it answered with nothing, and returns the failed sources as `failedSources`. `GET /status` lists them under `failedSources` (source, network, pair or asset, time; the error text stays in the log because the route is public).
10 changes: 5 additions & 5 deletions .changeset/fiat-pricing-naira-290.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
'lens': minor
---

Add GET /price/:asset/fiat for local currency conversion and naira valuation (#290)
---
'lens': minor
---
Add GET /price/:asset/fiat for local currency conversion and naira valuation (#290)
16 changes: 8 additions & 8 deletions .changeset/generate-readme-venues-and-endpoints.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
'lens': patch
---

Generate README venues section and endpoints table from code registries:
- Added `scripts/generate-readme.ts` (`npm run readme:gen`) to render the venues table from `venueAdapters` and the endpoints table from `openapi.yaml`.
- Added `type` and `description` to `VenueAdapter` and existing venue adapters (SDEX, AMM, Soroswap, Aquarius).
- Added `tests/readme.test.ts` to assert that `README.md` venues and endpoints sections never drift from source registries.
---
'lens': patch
---
Generate README venues section and endpoints table from code registries:
- Added `scripts/generate-readme.ts` (`npm run readme:gen`) to render the venues table from `venueAdapters` and the endpoints table from `openapi.yaml`.
- Added `type` and `description` to `VenueAdapter` and existing venue adapters (SDEX, AMM, Soroswap, Aquarius).
- Added `tests/readme.test.ts` to assert that `README.md` venues and endpoints sections never drift from source registries.
10 changes: 5 additions & 5 deletions .changeset/graphql-screener-query.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
'lens': minor
---

Add a `screener` GraphQL query (#367). REST and GraphQL now share one implementation in `src/services/screener.ts`, so filters, sorting, the keyset cursor and the `[1, 100]` limit clamp cannot drift between the two surfaces. `sortBy` and `order` are allow-listed at the SQL boundary, and every CTE is scoped to a single network.
---
'lens': minor
---
Add a `screener` GraphQL query (#367). REST and GraphQL now share one implementation in `src/services/screener.ts`, so filters, sorting, the keyset cursor and the `[1, 100]` limit clamp cannot drift between the two surfaces. `sortBy` and `order` are allow-listed at the SQL boundary, and every CTE is scoped to a single network.
Loading
Loading