import * as http from 'node:http' import { Challenge } from 'mppx' import { ApiKeys, App, Metrics, RateLimit, Store } from 'tapimo' import * as Docs from 'tapimo/docs' import * as Tokenlist from './apps/data/routes/tokenlist.js' import * as Tokens from './apps/data/routes/tokens.js' import type * as Log from './internal/Log.js' import * as TestApp from '../test/App.js' import * as Runtime from '../test/runtime.js' import * as Tempo from '../test/Tempo.js' describe('create', () => { test('rejects duplicate provider ids', () => { expect(() => TestApp.create({ providers: [ { id: 'duplicate', name: 'First', type: 'funding' }, { id: 'duplicate', name: 'Second', type: 'exchange' }, ], }), ).toThrowErrorMatchingInlineSnapshot( `[App.DuplicateProviderIdError: Duplicate provider id "duplicate".]`, ) }) test('rejects provider ids that are not normalized', () => { expect(() => TestApp.create({ providers: [{ id: ' Relay ', name: 'Relay', type: 'funding' }] }), ).toThrowErrorMatchingInlineSnapshot( `[App.InvalidProviderIdError: Invalid provider id " Relay ". Provider ids must be lowercase and trimmed.]`, ) }) test('composes default dependencies', async () => { const store = Store.memory() TestApp.create({ cache: { store } }) await store.put('status', 'ready') expect(await store.get('status')).toMatchInlineSnapshot(`"ready"`) }) test('keeps cache persistence alive via waitUntil', async () => { const pending = Promise.withResolvers() const waited: Promise[] = [] const store = Store.from({ ...Store.memory(), put: () => pending.promise, type: 'cache', }) const app = TestApp.create({ auth: false, cache: { store } }) app.get('/cache', async (c) => c.json( await Store.memoize(async () => ({ value: 'fresh' }), { key: 'app:cache', store: c.get('store'), ttl: 60_000, }), ), ) const executionCtx = { passThroughOnException: () => {}, waitUntil: (promise: Promise) => void waited.push(promise), } const response = await app.request('/cache', undefined, undefined, executionCtx as never) expect(response.status).toBe(200) expect(waited).toHaveLength(1) pending.resolve() await Promise.all(waited) }) test('leaves routes public without auth middleware', async () => { const app = TestApp.create({ auth: false }) const response = await app.request('/v1/tokens/not-an-address') expect(response.status).toMatchInlineSnapshot(`400`) }) test('bounds a stalled request with a 504 envelope', async () => { vi.useFakeTimers() try { const app = TestApp.create({ auth: false }) app.get('/hang', () => new Promise(() => {})) const pending = app.request('/hang') await vi.advanceTimersByTimeAsync(60_000) const response = await pending const { requestId, ...body } = (await response.json()) as Record expect(response.status).toMatchInlineSnapshot(`504`) expect(requestId).toBeTypeOf('string') expect(body).toMatchInlineSnapshot(` { "error": { "code": "request_timeout", "message": "Request timed out", }, } `) } finally { vi.useRealTimers() } }) test('exempts mutations and paid requests from the deadline', async () => { vi.useFakeTimers() try { const app = TestApp.create({ auth: false }) const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) app.post('/slow', async (c) => { await wait(90_000) return c.json({ ok: true }, 200) }) app.get('/slow-paid', async (c) => { await wait(90_000) return c.json({ ok: true }, 200) }) const post = app.request('/slow', { method: 'POST' }) const paid = app.request('/slow-paid', { headers: { authorization: 'Payment credential' } }) await vi.advanceTimersByTimeAsync(90_000) expect((await post).status).toMatchInlineSnapshot(`200`) expect((await paid).status).toMatchInlineSnapshot(`200`) } finally { vi.useRealTimers() } }) test('serves public health check', async () => { const app = TestApp.create({ path: 'api/' }) const response = await app.request('/api/health') const body = await response.json() expect(response.status).toMatchInlineSnapshot(`200`) expect(body).toMatchInlineSnapshot(` { "status": "ok", } `) }) test('normalizes configured path', async () => { const app = TestApp.create({ path: 'api/' }) const response = await app.request('/api/health') const body = await response.json() const unprefixed = await app.request('/health') expect(response.status).toMatchInlineSnapshot(`200`) expect(body).toMatchInlineSnapshot(` { "status": "ok", } `) expect(unprefixed.status).toMatchInlineSnapshot(`404`) }) test('applies middleware under configured path', async () => { const app = TestApp.create({ path: 'api/' }) const response = await app.request('/api/v1/tokens/not-an-address', { headers: { 'tempo-api-key': 'wrong' }, }) const body = await response.json() const { requestId, ...stable } = body expect(response.status).toMatchInlineSnapshot(`401`) expect(response.headers.get('tempo-request-id') === requestId).toMatchInlineSnapshot(`true`) expect(typeof requestId).toMatchInlineSnapshot(`"string"`) expect(stable).toMatchInlineSnapshot(` { "error": { "code": "api_key_invalid", "message": "Invalid API key", }, } `) }) test('ignores client-supplied request ids', async () => { const entries: Log.Entry[] = [] const app = TestApp.create({ logger: (entry) => void entries.push(entry) }) const response = await app.request('/health', { headers: { 'tempo-request-id': 'client-controlled' }, }) const requestId = response.headers.get('tempo-request-id') expect(requestId === 'client-controlled').toMatchInlineSnapshot(`false`) expect(entries[0]?.requestId === requestId).toMatchInlineSnapshot(`true`) }) test('documents supported API key transports', async () => { const app = TestApp.create() const spec = await (await app.request('/openapi.json')).json() expect({ queryApiKey: { in: spec.components.securitySchemes.queryApiKey.in, name: spec.components.securitySchemes.queryApiKey.name, type: spec.components.securitySchemes.queryApiKey.type, }, security: spec.security, }).toMatchInlineSnapshot(` { "queryApiKey": { "in": "query", "name": "key", "type": "apiKey", }, "security": [ { "apiKey": [], }, { "queryApiKey": [], }, { "bearerAuth": [], }, ], } `) expect(spec.paths['/v1/orgs'].get.security).toContainEqual({ queryApiKey: [] }) expect(spec.paths['/v1/orgs'].get['x-required-scopes']).toMatchInlineSnapshot(` [ "management:read", ] `) }) test('preserves named error schemas across generated documents', async () => { const first = await (await TestApp.create().request('/openapi.json')).json() const second = await (await TestApp.create().request('/openapi.json')).json() expect( [first, second].map((spec) => ({ forbidden: spec.components.schemas.ForbiddenError.type, internal: spec.components.schemas.InternalError.type, })), ).toEqual([ { forbidden: 'object', internal: 'object' }, { forbidden: 'object', internal: 'object' }, ]) }) test('keeps standard and custom error component names distinct', async () => { const spec = await (await TestApp.create().request('/openapi.json')).json() const billing = spec.paths['/v1/orgs/{orgId}/billing/stripe/checkout'].post.responses[501].content[ 'application/json' ].schema.$ref const funding = spec.paths['/v1/funding/transfers/vault'].post.responses[501].content['application/json'] .schema.$ref const billingName = billing.split('/').at(-1) const fundingName = funding.split('/').at(-1) expect({ billing: { codes: spec.components.schemas[billingName].properties.error.properties.code.enum, ref: billing, }, funding: { codes: spec.components.schemas[fundingName].properties.error.properties.code.enum, ref: funding, }, }).toEqual({ billing: { codes: ['billing_unconfigured'], ref: '#/components/schemas/NotImplementedError', }, funding: { codes: ['not_implemented'], ref: '#/components/schemas/NotImplemented501Error', }, }) }) test('authenticates a minted store-backed key and rejects after revoke', async () => { const store = Store.memory() const { record, token } = await ApiKeys.mint(store, { orgId: 'org_test', scopes: ['data:read'], }) // Uncached resolution so the revoke takes effect on the next request. const app = TestApp.create({ auth: { cache: false }, kv: { store } }) const headers = { authorization: `Bearer ${token}` } // Auth passes (request reaches validation, which rejects the bad address). const authed = await app.request('/v1/tokens/not-an-address', { headers }) expect(authed.status).toMatchInlineSnapshot(`400`) // After revoke the token is unknown and the request is rejected. expect(await ApiKeys.revoke(store, record.id)).toMatchInlineSnapshot(`true`) const revoked = await app.request('/v1/tokens/not-an-address', { headers }) expect(revoked.status).toMatchInlineSnapshot(`401`) }) test('authenticates via x-api-key header and key query param', async () => { const store = Store.memory() const { token } = await ApiKeys.mint(store, { orgId: 'org_test', scopes: ['data:read'] }) const app = TestApp.create({ auth: { cache: false }, kv: { store } }) // Deprecated `x-api-key` header authenticates (reaches validation, which // rejects the bad address). const viaHeader = await app.request('/v1/tokens/not-an-address', { headers: { 'x-api-key': token }, }) expect(viaHeader.status).toMatchInlineSnapshot(`400`) // The `key` query parameter authenticates the same way. const viaQuery = await app.request(`/v1/tokens/not-an-address?key=${encodeURIComponent(token)}`) expect(viaQuery.status).toMatchInlineSnapshot(`400`) const rejectedQuery = await app.request('/v1/tokens/not-an-address?key=wrong') expect(rejectedQuery.status).toMatchInlineSnapshot(`401`) // An unknown legacy credential is still rejected. const rejected = await app.request('/v1/tokens/not-an-address', { headers: { 'x-api-key': 'wrong' }, }) expect(rejected.status).toMatchInlineSnapshot(`401`) }) test('caches key resolution by default, so a revoke lags up to the TTL', async () => { const store = Store.memory() const { record, token } = await ApiKeys.mint(store, { orgId: 'org_test', scopes: ['data:read'], }) const app = TestApp.create({ kv: { store } }) const headers = { authorization: `Bearer ${token}` } const authed = await app.request('/v1/tokens/not-an-address', { headers }) expect(authed.status).toMatchInlineSnapshot(`400`) // The positive resolve is cached per isolate (60s by default), so the // revoked key keeps authenticating until the entry expires — the // documented trade-off for skipping a backend read per request. expect(await ApiKeys.revoke(store, record.id)).toMatchInlineSnapshot(`true`) const revoked = await app.request('/v1/tokens/not-an-address', { headers }) expect(revoked.status).toMatchInlineSnapshot(`400`) }) test('applies endpoint auth overrides', async () => { const app = TestApp.create({ auth: { overrides: { 'GET /v1/tokens/:token': { mpp: false, public: false, }, }, }, path: 'api/', }) const response = await app.request('/api/v1/tokens/not-an-address') const body = await response.json() const { requestId, ...stable } = body expect(response.status).toMatchInlineSnapshot(`401`) expect(response.headers.get('tempo-request-id') === requestId).toMatchInlineSnapshot(`true`) expect(typeof requestId).toMatchInlineSnapshot(`"string"`) expect(stable).toMatchInlineSnapshot(` { "error": { "code": "api_key_missing", "message": "Missing API key", }, } `) }) test('applies the configured global MPP rate limit', async () => { const store = Store.memory() const limit = { limit: 2, period: 'minute' } satisfies RateLimit.Limit const app = TestApp.create({ auth: { mpp: { rateLimit: limit } }, rateLimit: { store }, }) await exhaustQuota(RateLimit.memory({ store }), 'mpp:public:203.0.113.10', limit) const response = await app.request(`/v1/tokens/${TestApp.token}`, { headers: { authorization: 'Payment malformed', 'cf-connecting-ip': '203.0.113.10', }, }) expect(response.status).toBe(429) expect(response.headers.get('RateLimit-Limit')).toBe('2') expect(response.headers.get('RateLimit-Scope')).toBe('mpp') }) test('rejects an unsupported MPP payment chain without throwing', async () => { const store = Store.memory() const limit = { limit: 1, period: 'minute' } satisfies RateLimit.Limit const app = TestApp.create({ auth: { mpp: { secretKey: 'secret_test_key_0123456789abcdef', session: { chainId: Tempo.chain.id, decimals: 6, getClient: () => Tempo.client, recipient: Tempo.accounts[2].address, }, sessionChainIds: [Tempo.chain.id], }, overrides: { 'GET /v1/tokens/:token': { public: { rateLimit: limit } }, }, }, rateLimit: { store }, supportedChainIds: [31318], }) await exhaustQuota(RateLimit.memory({ store }), 'public:anonymous', limit) const response = await app.request('/v1/tokens/not-an-address?chainId=31318') const body = (await response.json()) as { error: { code: string } } expect(response.status).toBe(429) expect(response.headers.has('www-authenticate')).toBe(false) expect(body.error.code).toBe('payment_required') }) test('endpoint MPP rate limit overrides the global limit', async () => { const store = Store.memory() const routeLimit = { limit: 1, period: 'minute' } satisfies RateLimit.Limit const app = TestApp.create({ auth: { mpp: { rateLimit: { limit: 2, period: 'minute' } }, overrides: { 'GET /v1/tokens/:token': { mpp: { rateLimit: routeLimit } }, }, }, rateLimit: { store }, }) await exhaustQuota(RateLimit.memory({ store }), 'mpp:public:203.0.113.10', routeLimit) const response = await app.request(`/v1/tokens/${TestApp.token}`, { headers: { authorization: 'Payment malformed', 'cf-connecting-ip': '203.0.113.10', }, }) expect(response.status).toBe(429) expect(response.headers.get('RateLimit-Limit')).toBe('1') expect(response.headers.get('RateLimit-Scope')).toBe('mpp') }) test('serves Vocs API reference', async () => { const app = TestApp.create({ docs: Docs.docs() }) const response = await app.request('/') const body = await response.text() expect(response.status).toMatchInlineSnapshot(`200`) expect(response.headers.get('content-type')).toMatchInlineSnapshot(`"text/html; charset=UTF-8"`) expect(body.includes('Tempo API Reference')).toMatchInlineSnapshot(`true`) expect(body.includes('/_vocs/openapi/')).toMatchInlineSnapshot(`true`) }) test('serves Vocs API reference assets', async () => { const app = TestApp.create({ docs: Docs.docs() }) // The HTML shell references its bundled entry script; it must resolve. const html = await (await app.request('/')).text() const src = html.match(/src="([^"]*_vocs\/openapi\/[^"]+)"/)?.[1] expect(typeof src).toMatchInlineSnapshot(`"string"`) const response = await app.request(src!) expect(response.status).toMatchInlineSnapshot(`200`) expect(response.headers.get('content-type')).toMatchInlineSnapshot( `"text/javascript; charset=utf-8"`, ) }) test('serves Vocs API reference under configured path', async () => { const app = TestApp.create({ docs: Docs.docs(), path: 'api/' }) const response = await app.request('/api') const body = await response.text() expect(response.status).toMatchInlineSnapshot(`200`) expect(body.includes('/api/_vocs/openapi/')).toMatchInlineSnapshot(`true`) }) test('configures Vocs API reference', async () => { const app = TestApp.create({ docs: Docs.docs({ title: 'Custom API Reference' }) }) const response = await app.request('/') const body = await response.text() expect(response.status).toMatchInlineSnapshot(`200`) expect(body.includes('Custom API Reference')).toMatchInlineSnapshot(`true`) }) test('serves no API reference by default', async () => { const app = TestApp.create() const response = await app.request('/') const body = await response.json() const { requestId, ...stable } = body expect(response.status).toMatchInlineSnapshot(`404`) expect(response.headers.get('tempo-request-id') === requestId).toMatchInlineSnapshot(`true`) expect(typeof requestId).toMatchInlineSnapshot(`"string"`) expect(stable).toMatchInlineSnapshot(` { "error": { "code": "not_found", "message": "Route not found", }, } `) }) test('enables CORS by default', async () => { const app = TestApp.create() const response = await app.request('/health', { headers: { origin: 'https://example.com' }, }) expect(response.status).toMatchInlineSnapshot(`200`) expect(response.headers.get('access-control-allow-origin')).toMatchInlineSnapshot(`"*"`) }) test('disables CORS when cors is false', async () => { const app = TestApp.create({ cors: false }) const response = await app.request('/health', { headers: { origin: 'https://example.com' }, }) expect(response.status).toMatchInlineSnapshot(`200`) expect(response.headers.get('access-control-allow-origin')).toMatchInlineSnapshot(`null`) }) test('applies custom CORS options', async () => { const app = TestApp.create({ cors: { origin: 'https://allowed.example' } }) const allowed = await app.request('/health', { headers: { origin: 'https://allowed.example' }, }) const blocked = await app.request('/health', { headers: { origin: 'https://other.example' }, }) expect(allowed.headers.get('access-control-allow-origin')).toMatchInlineSnapshot( `"https://allowed.example"`, ) expect(blocked.headers.get('access-control-allow-origin')).toMatchInlineSnapshot(`null`) }) test('enables response compression when compress is set', async () => { const app = TestApp.create({ compress: true, docs: Docs.docs() }) const response = await app.request('/', { headers: { 'accept-encoding': 'gzip' }, }) expect(response.status).toMatchInlineSnapshot(`200`) expect(response.headers.get('content-encoding')).toMatchInlineSnapshot(`"gzip"`) }) test('emits one canonical log entry per request when logger is set', async () => { const entries: Log.Entry[] = [] const app = TestApp.create({ logger: (entry) => void entries.push(entry) }) const response = await app.request('/nope') expect(response.status).toMatchInlineSnapshot(`404`) expect(entries.length).toMatchInlineSnapshot(`1`) const { duration: _duration, requestId, ...stable } = entries[0]! expect(requestId === response.headers.get('tempo-request-id')).toMatchInlineSnapshot(`true`) expect(stable).toMatchInlineSnapshot(` { "chainId": 42431, "errorCode": "not_found", "level": "warn", "method": "GET", "path": "/nope", "route": "/*", "status": 404, } `) }) test('passes the original request error to the configured logger', async () => { const cause = new Error('boom') let logged: Error | undefined const app = TestApp.create({ logger: (_entry, error) => { logged = error }, }) app.get('/boom', () => { throw cause }) const response = await app.request('/boom') expect(response.status).toBe(500) expect(logged).toBe(cause) }) test('logs the selected chain for anonymous edge-cache hits', async () => { const entries: Log.Entry[] = [] const app = TestApp.create({ defaultChainId: 4217, logger: (entry) => void entries.push(entry), }) const path = '/v1/tokenlist?chainId=42431' await app.request(path) await app.request(path) expect( entries.map((entry) => ({ cache: entry.cache, chainId: entry.chainId, route: entry.route })), ).toMatchInlineSnapshot(` [ { "cache": undefined, "chainId": 42431, "route": "/v1/tokenlist", }, { "cache": "hit", "chainId": 42431, "route": "/*", }, ] `) }) test('invokes the metrics analytics hook once per request', async () => { const entries: Log.Entry[] = [] const app = TestApp.create({ metrics: Metrics.cloudflare({ analytics: () => (entry) => void entries.push(entry), enabled: false, }), }) const response = await app.request('/nope') expect(response.status).toMatchInlineSnapshot(`404`) expect(entries.length).toMatchInlineSnapshot(`1`) expect(entries[0]?.route).toMatchInlineSnapshot(`"/*"`) expect(entries[0]?.status).toMatchInlineSnapshot(`404`) }) test('keeps an async analytics sink alive via waitUntil', async () => { // Regression: the app's `emit` closure must *return* the analytics // promise so `Log.middleware` extends the response with `waitUntil`. // Otherwise the sink's async work (e.g. a Cloudflare Queue `send`) is a // floating promise the Workers runtime cancels once the response is // returned, and no analytics ever reach the sink (e.g. ClickHouse). let completed = false const app = TestApp.create({ metrics: Metrics.cloudflare({ analytics: () => async () => { await Promise.resolve() completed = true }, enabled: false, }), }) const waited: Promise[] = [] const executionCtx = { passThroughOnException: () => {}, waitUntil: (promise: Promise) => void waited.push(promise), } // Fourth arg is the Worker `ExecutionContext`; Hono exposes it as // `c.executionCtx`, which `Log.middleware` calls `waitUntil` on. const response = await app.request('/nope', undefined, undefined, executionCtx as never) expect(response.status).toBe(404) // The async sink hasn't run to completion synchronously, but its promise // was handed to `waitUntil` rather than dropped. expect(waited.length).toBe(1) await Promise.all(waited) expect(completed).toBe(true) }) test('serves static token logo data', async () => { const app = TestApp.create() const response = await app.request( '/assets/42431/icons/0x20c0000000000000000000000000000000000000', ) const body = await response.text() expect(response.status).toMatchInlineSnapshot(`200`) expect(response.headers.get('cache-control')).toMatchInlineSnapshot(`"public, max-age=86400"`) expect(response.headers.get('content-type')).toMatchInlineSnapshot(`"image/svg+xml"`) expect(body.trimStart().startsWith(' { const app = TestApp.create() const response = await app.request( '/assets/4217/icons/0x20c0000000000000000000006fd9a167923ba194', ) expect(response.status).toMatchInlineSnapshot(`200`) expect(response.headers.get('content-type')).toMatchInlineSnapshot(`"image/png"`) }) test('omits the static asset route when no asset loader is configured', async () => { const app = App.create({ db: TestApp.database() }) const response = await app.request( '/assets/42431/icons/0x20c0000000000000000000000000000000000000', ) expect(response.status).toMatchInlineSnapshot(`404`) }) test('resolves verified-token logos from an exact-key asset store', async () => { // Exact-key store (like R2): only the single-slash `:chainId/icons/:address` // key matches, so a `:chainId//icons` regression would drop every logo. const db = TestApp.database() await TestApp.verifiedSeed(db, 42431) const app = App.create({ db, defaultChainId: 42431 }).route( '/', App.data({ assets: { get: async (key) => /^42431\/icons\/0x[0-9a-f]+$/.test(key) ? { body: new TextEncoder().encode('').buffer as ArrayBuffer, contentType: 'image/svg+xml', } : undefined, }, }), ) const response = await app.request('/v1/tokens?verified=true') const body = (await response.json()) as { data: { logoUri?: string }[] } expect(response.status).toBe(200) expect(body.data.length).toBeGreaterThan(0) expect(body.data.every((token) => typeof token.logoUri === 'string')).toBe(true) }) test('builds token lists from asset metadata without loading bodies', async () => { const db = TestApp.database() await TestApp.verifiedSeed(db, 42431) const address = '0x20c0000000000000000000000000000000000000' const key = `42431/icons/${address}` const app = App.create({ auth: false, db, defaultChainId: 42431 }).route( '/', App.data({ assets: { get: async () => { throw new Error('Token list loaded an asset body.') }, list: async (prefix) => (prefix === '42431/icons/' ? [key] : []), }, }), ) const response = await app.request('/v1/tokenlist') const body = await TestApp.json(response, Tokenlist.schema.getTokenList.Response) const token = body.tokens.find((token) => token.address === address) expect(response.status).toBe(200) expect(token?.logoURI).toBe(`http://localhost/assets/${key}`) }) test('serves token lists when the asset store is unavailable', async () => { const db = TestApp.database() await TestApp.verifiedSeed(db, 42431) const app = App.create({ auth: false, db, defaultChainId: 42431 }).route( '/', App.data({ assets: { get: async () => { throw new Error('Asset store unavailable') }, list: async () => { throw new Error('Asset store unavailable') }, }, }), ) const response = await app.request('/v1/tokenlist') const body = await TestApp.json(response, Tokenlist.schema.getTokenList.Response) expect(response.status).toBe(200) expect(body.tokens.length).toBeGreaterThan(0) }) test('returns token metadata from RPC', async () => { const app = TestApp.create() const response = await app.request(`/v1/tokens/${TestApp.token}`, { headers: { authorization: `Bearer ${TestApp.key.token}` }, }) const body = await TestApp.json(response, Tokens.schema.getToken.Response) expect(response.status).toMatchInlineSnapshot(`200`) expect(body).toMatchObject({ address: TestApp.token, currency: 'USD', decimals: 6, id: TestApp.token, verified: false, }) }) // Credential-bearing requests must reach auth even when the route supports // shared caching, so per-key policies and quotas run on every request. test('bypasses edge caching for authenticated reads', async () => { const app = TestApp.create() const headers = { authorization: `Bearer ${TestApp.key.token}` } const miss = await app.request(`/v1/tokens/${TestApp.token}`, { headers }) const hit = await app.request(`/v1/tokens/${TestApp.token}`, { headers }) expect(miss.status).toMatchInlineSnapshot(`200`) expect(hit.status).toMatchInlineSnapshot(`200`) expect(miss.headers.has('RateLimit-Limit')).toMatchInlineSnapshot(`true`) expect(hit.headers.has('RateLimit-Limit')).toMatchInlineSnapshot(`true`) expect(await hit.json()).toMatchObject({ address: TestApp.token }) }) test('enforces a key allowlist after an anonymous edge-cache hit', async () => { const key = { ...TestApp.key, allowedIps: ['203.0.113.0/24'] } const app = TestApp.create({ auth: { keys: [key] } }) await app.request(`/v1/tokens/${TestApp.token}`) const cached = await app.request(`/v1/tokens/${TestApp.token}`) const denied = await app.request(`/v1/tokens/${TestApp.token}`, { headers: { 'cf-connecting-ip': '198.51.100.7', 'tempo-api-key': key.token, }, }) expect(cached.status).toBe(200) expect(cached.headers.has('RateLimit-Limit')).toBe(false) expect(denied.status).toBe(403) expect(await denied.json()).toMatchObject({ error: { code: 'api_key_ip_forbidden' } }) }) test('rejects invalid API key without falling back to public quota', async () => { const app = TestApp.create() const response = await app.request(`/v1/tokens/${TestApp.token}`, { headers: { 'tempo-api-key': 'wrong' }, }) const body = await response.json() const { requestId, ...stable } = body expect(response.status).toMatchInlineSnapshot(`401`) expect(response.headers.get('tempo-request-id') === requestId).toMatchInlineSnapshot(`true`) expect(typeof requestId).toMatchInlineSnapshot(`"string"`) expect(stable).toMatchInlineSnapshot(` { "error": { "code": "api_key_invalid", "message": "Invalid API key", }, } `) }) test('returns payment challenge when public quota is exhausted', async () => { const rateLimitStore = Store.memory() const app = TestApp.create({ rateLimit: { store: rateLimitStore } }) await exhaustQuota(RateLimit.memory({ store: rateLimitStore }), 'public:anonymous', { limit: 60, period: 'minute', }) const response = await app.request(`/v1/tokens/${TestApp.token}`) expect(response.status).toMatchInlineSnapshot(`402`) expect(response.headers.get('www-authenticate')?.startsWith('Payment ')).toMatchInlineSnapshot( `true`, ) }) test('returns payment challenge when API-key quota is exhausted', async () => { const limit = { limit: 1, period: 'minute' } satisfies RateLimit.Limit const key = { ...TestApp.key, rateLimits: { 'data:read': limit } } const rateLimitStore = Store.memory() const app = TestApp.create({ auth: { keys: [key] }, rateLimit: { store: rateLimitStore }, }) await exhaustQuota( RateLimit.memory({ store: rateLimitStore }), `api_key:${key.id}:scope:data:read`, limit, ) const response = await app.request(`/v1/tokens/${TestApp.token}`, { headers: { 'tempo-api-key': key.token }, }) expect(response.status).toMatchInlineSnapshot(`402`) expect(response.headers.get('www-authenticate')?.startsWith('Payment ')).toMatchInlineSnapshot( `true`, ) }) test('validates token route params', async () => { const app = TestApp.create() const response = await app.request('/v1/tokens/not-an-address', { headers: { 'tempo-api-key': TestApp.key.token }, }) const body = await response.json() const { requestId, ...stable } = body expect(response.status).toMatchInlineSnapshot(`400`) expect(response.headers.get('tempo-request-id') === requestId).toMatchInlineSnapshot(`true`) expect(typeof requestId).toMatchInlineSnapshot(`"string"`) expect(stable).toMatchInlineSnapshot(` { "error": { "code": "token_invalid", "details": [ { "message": "Invalid input", "path": [ "token", ], }, ], "message": "Invalid token address", }, } `) }) test('returns error envelope for unknown route', async () => { const app = TestApp.create() const response = await app.request('/missing') const body = await response.json() const { requestId, ...stable } = body expect(response.status).toMatchInlineSnapshot(`404`) expect(response.headers.get('tempo-request-id') === requestId).toMatchInlineSnapshot(`true`) expect(typeof requestId).toMatchInlineSnapshot(`"string"`) expect(stable).toMatchInlineSnapshot(` { "error": { "code": "not_found", "message": "Route not found", }, } `) }) test('uses configured dependencies', async () => { const auth = { keys: [ { id: 'key_test', orgId: 'org_test', scopes: ['data:read'], token: 'secret_test_key', }, ], mpp: { secretKey: 'secret_test_key_0123456789abcdef', session: { chainId: Tempo.chain.id, decimals: 6, getClient: () => Tempo.client, recipient: Tempo.accounts[2].address, }, }, } satisfies NonNullable const store = Store.memory({ entries: [['status', 'ready']] }) const rateLimitStore = Store.memory() const now = () => new Date('2026-05-25T00:00:30.000Z') const app = TestApp.create({ auth, cache: { store }, rateLimit: { now, store: rateLimitStore, }, }) // `store` is owned by the caller now, so identity is implicit. expect(await store.get('status')).toMatchInlineSnapshot(`"ready"`) const authorized = await app.request('/v1/tokens/not-an-address', { headers: { authorization: `Bearer ${TestApp.key.token}` }, }) expect(authorized.status).toMatchInlineSnapshot(`400`) await exhaustQuota(RateLimit.memory({ now, store: rateLimitStore }), 'public:anonymous', { limit: 60, period: 'minute', }) const overQuota = await app.request('/v1/tokens/not-an-address') expect(overQuota.status).toMatchInlineSnapshot(`402`) expect(Challenge.fromHeaders(overQuota.headers).request['currency']).toBe(Tempo.currency) const head = await app.request('/v1/tokens/not-an-address', { method: 'HEAD' }) expect(head.status).toMatchInlineSnapshot(`402`) expect(Challenge.fromHeaders(head.headers).intent).toBe('session') }) test('supports bearer API-key fallback', async () => { const auth = { keys: [ { id: 'key_test', orgId: 'org_test', scopes: ['data:read'], token: 'secret_test_key', }, ], } satisfies NonNullable const app = TestApp.create({ auth }) const response = await app.request('/v1/tokens/not-an-address', { headers: { authorization: `Bearer ${TestApp.key.token}` }, }) expect(response.status).toMatchInlineSnapshot(`400`) }) test('exposes Node.js listener', async () => { const app = TestApp.create() const server = http.createServer(App.listener(app)) const url = await new Promise((resolve) => { server.listen(0, '127.0.0.1', () => { const address = server.address() if (!address || typeof address === 'string') throw new Error('expected address info') resolve(new URL(`http://127.0.0.1:${address.port}/health`)) }) }) try { const response = await fetch(url) const body = await response.json() expect(response.status).toMatchInlineSnapshot(`200`) expect(body).toMatchInlineSnapshot(` { "status": "ok", } `) } finally { await new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())) }) } }) }) describe('chain id validation', () => { test('rejects an unsupported chain id with a clear 400 before any upstream call', async () => { const app = TestApp.create({ auth: false }) const response = await app.request('/v1/transfers?chainId=999999') const body = (await response.json()) as Record const error = body['error'] as { code: string; message: string } expect(response.status).toMatchInlineSnapshot(`400`) expect(typeof body['requestId']).toMatchInlineSnapshot(`"string"`) expect(error.code).toMatchInlineSnapshot(`"chain_id_unsupported"`) expect(error.message).toContain('Unsupported chain id: 999999.') expect(error.message).toContain(String(Runtime.get().chainId)) }) test('honors supportedChainIds for self-hosted chains (e.g. a localnet)', async () => { const app = TestApp.create({ auth: false, rpc: { url: 'https://rpc.example' }, supportedChainIds: [31337], }) const response = await app.request('/v1/transfers?chainId=999999') const body = (await response.json()) as { error: { message: string } } expect(response.status).toMatchInlineSnapshot(`400`) expect(body.error.message).toContain('Unsupported chain id: 999999.') expect(body.error.message).toContain('31337') expect(body.error.message).toContain(String(Runtime.get().chainId)) }) }) async function exhaustQuota(rateLimit: RateLimit.Store, key: string, limit: RateLimit.Limit) { for (let index = 0; index < limit.limit; index++) await rateLimit.consume({ key, limit }) }