diff --git a/apps/api-service/README.md b/apps/api-service/README.md index f6a4183..3fd9404 100644 --- a/apps/api-service/README.md +++ b/apps/api-service/README.md @@ -40,6 +40,7 @@ bun run --filter api-service dev | `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_NAME` | `DB_PORT` `5432`, others required | UNS indexer Postgres database, read only | | `DB_PASS` | none | its password, handled as a [secret](../../packages/service-kit/#secrets) | | `UNS_DOMAINS_CACHE_TTL_SECONDS` | `30` | how long the domain list is served from memory | +| `HARDWARE_ENDPOINT_ENABLED` | `false` | `true` serves `POST /hardware`; any value other than `true` or `false` stops the service at startup | | `PORT`, `LOG_LEVEL` | | see [service-kit](../../packages/service-kit/#environment) | The Nomad jobs set `GEO_IP_SERVICE_URL` from Consul, as [operations](../../operations/#reaching-it-from-another-job) describes. @@ -54,7 +55,7 @@ The Nomad jobs set `GEO_IP_SERVICE_URL` from Consul, as [operations](../../opera | `GET /relays?fingerprints=,` | the same for every known relay in the list; `400` without fingerprints | | `GET /relay-map/` | H3 cells holding relays: `index`, `relayCount`, `geo` (cell center) and `boundary` | | `GET /fingerprint-map/` | per relay fingerprint: `hexId`, `coordinates` (cell center), country, region, city and autonomous system | -| `POST /hardware` | validates hardware relay info, which must include an `ATEC` entry in `serNums`, and stores it in Onionoo; `400` with the list of validation errors | +| `POST /hardware` | only with `HARDWARE_ENDPOINT_ENABLED=true`, otherwise the not found page: validates hardware relay info, which must include an `ATEC` entry in `serNums`, and stores it in Onionoo; `400` with the list of validation errors | | `GET /operators` | operator addresses; `?withDomains=true` adds the `.anyone` domains each one owns | | `GET /anyone-domains` | every `.anyone` domain with its token id and owner | | `GET /healthz` | service health, see [Health](#health) | diff --git a/apps/api-service/src/app.test.ts b/apps/api-service/src/app.test.ts index 306c26e..a198a5e 100644 --- a/apps/api-service/src/app.test.ts +++ b/apps/api-service/src/app.test.ts @@ -2,7 +2,7 @@ import { GeoIpClient, type GeoLocation } from '@anyone-protocol/geo-ip-client'; import { createLogger } from '@anyone-protocol/service-kit'; import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; import { cellToBoundary } from 'h3-js'; -import { startServer } from './app'; +import { type ServerOptions, startServer } from './app'; import { Onionoo, type Relay } from './onionoo'; import { OperatorRegistry } from './operator-registry'; import { StubGeoIpService, StubUpstreams } from './testing/stubs'; @@ -83,7 +83,7 @@ let server: ReturnType; let clock: number; let domainRows: () => Promise; -function serve() { +function serve(options: Partial = {}) { const logger = createLogger({ service: 'api-service', write: () => {} }); server = startServer({ port: 0, @@ -98,6 +98,7 @@ function serve() { operatorRegistry: new OperatorRegistry({ nodeUrl: upstreams.url, processId: PROCESS_ID, cacheTtlMs: 0, logger, read: { retryDelayMs: 1 } }), unsDomains: new UnsDomains({ rows: () => domainRows(), cacheTtlMs: 0, logger }), logger, + ...options, }); return server; } @@ -299,7 +300,22 @@ describe('when geo-ip-service is down', () => { }); }); +describe('POST /hardware when the endpoint is off', () => { + it('answers 404 by default and does not call Onionoo', async () => { + const response = await post('/hardware', JSON.stringify({ fingerprint: 'A'.repeat(40) })); + + expect(response.status).toBe(404); + expect(await response.text()).toContain('Cannot POST /hardware'); + expect(upstreams.requests).toHaveLength(0); + }); +}); + describe('POST /hardware', () => { + beforeEach(async () => { + await server.stop(true); + serve({ hardwareEnabled: true }); + }); + const hardwareInfo = { id: 'id', company: 'company', diff --git a/apps/api-service/src/app.ts b/apps/api-service/src/app.ts index 3eb413a..3bcaea6 100644 --- a/apps/api-service/src/app.ts +++ b/apps/api-service/src/app.ts @@ -18,6 +18,7 @@ export interface ServerOptions { operatorRegistry: OperatorRegistry; unsDomains: UnsDomains; logger: Logger; + hardwareEnabled?: boolean; } type Handler = (request: BunRequest) => Response | Promise; @@ -46,7 +47,7 @@ function withTrailingSlashes(routes: Routes): Routes { return Object.fromEntries(Object.entries(routes).flatMap(([path, methods]) => [[path, methods], [`${path}/`, methods]])); } -export function startServer({ port, onionoo, relayMetrics, geo, hexagonResolution, operatorRegistry, unsDomains, logger }: ServerOptions) { +export function startServer({ port, onionoo, relayMetrics, geo, hexagonResolution, operatorRegistry, unsDomains, logger, hardwareEnabled = false }: ServerOptions) { const health = createHealthReporter('api-service', { checks: { geoIp: () => geoIpCheck(geo) } }); const answer = @@ -107,17 +108,21 @@ export function startServer({ port, onionoo, relayMetrics, geo, hexagonResolutio }), }, - '/hardware': { - POST: answer('Error posting hardware info', async (request) => { - const body = await readJsonBody(request); - if (body instanceof Response) return body; + ...(hardwareEnabled + ? { + '/hardware': { + POST: answer('Error posting hardware info', async (request) => { + const body = await readJsonBody(request); + if (body instanceof Response) return body; - const errors = validateHardwareInfo(body); - if (errors.length > 0) return json(request, { errors }, 400); + const errors = validateHardwareInfo(body); + if (errors.length > 0) return json(request, { errors }, 400); - return relayedText(request, await onionoo.updateHardwareInfo(body as HardwareInfo)); - }), - }, + return relayedText(request, await onionoo.updateHardwareInfo(body as HardwareInfo)); + }), + }, + } + : {}), '/operators': { GET: answer('Error querying operators', async (request) => { diff --git a/apps/api-service/src/config.test.ts b/apps/api-service/src/config.test.ts index 7d2b852..54b0cc7 100644 --- a/apps/api-service/src/config.test.ts +++ b/apps/api-service/src/config.test.ts @@ -30,9 +30,18 @@ describe('loadConfig', () => { OPERATOR_REGISTRY_CACHE_TTL_SECONDS: 0, DB_PORT: 5432, UNS_DOMAINS_CACHE_TTL_SECONDS: 30, + HARDWARE_ENDPOINT_ENABLED: 'false', }); }); + it('turns the hardware endpoint on only for true', () => { + expect(loadConfig({ ...REQUIRED, HARDWARE_ENDPOINT_ENABLED: 'true' }).HARDWARE_ENDPOINT_ENABLED).toBe('true'); + }); + + it.each(['yes', '1', 'TRUE', 'on'])('refuses HARDWARE_ENDPOINT_ENABLED=%s', (value) => { + expect(() => loadConfig({ ...REQUIRED, HARDWARE_ENDPOINT_ENABLED: value })).toThrow('HARDWARE_ENDPOINT_ENABLED must be one of true, false'); + }); + it.each(['HB_URL', 'OPERATOR_REGISTRY_PROCESS_ID', 'ONIONOO_INSTANCE', 'VICTORIA_METRICS_ADDRESS', 'GEO_IP_SERVICE_URL', 'DB_HOST'])('fails closed without %s', (name) => { expect(() => loadConfig({ ...REQUIRED, [name]: undefined })).toThrow(`${name} is required`); }); diff --git a/apps/api-service/src/config.ts b/apps/api-service/src/config.ts index 7ff0381..92748fd 100644 --- a/apps/api-service/src/config.ts +++ b/apps/api-service/src/config.ts @@ -27,6 +27,7 @@ export const apiServiceEnv = { DB_PASS: env.secret(env.string({ optional: true })), DB_NAME: env.string(), UNS_DOMAINS_CACHE_TTL_SECONDS: env.integer({ default: 30 }), + HARDWARE_ENDPOINT_ENABLED: env.oneOf(['true', 'false'], { default: 'false' }), }; export type ApiServiceConfig = ReturnType; diff --git a/apps/api-service/src/main.ts b/apps/api-service/src/main.ts index fb97c0c..769dfc5 100644 --- a/apps/api-service/src/main.ts +++ b/apps/api-service/src/main.ts @@ -44,6 +44,7 @@ const server = startServer({ }), unsDomains: new UnsDomains({ rows: unsTokenRows(database), cacheTtlMs: config.UNS_DOMAINS_CACHE_TTL_SECONDS * SECOND_MS, logger }), logger, + hardwareEnabled: config.HARDWARE_ENDPOINT_ENABLED === 'true', }); logger.info('Listening', { @@ -51,6 +52,7 @@ logger.info('Listening', { onionoo: config.ONIONOO_INSTANCE, operatorRegistryProcessId: config.OPERATOR_REGISTRY_PROCESS_ID, geoIpService: config.GEO_IP_SERVICE_URL, + hardwareEndpointEnabled: config.HARDWARE_ENDPOINT_ENABLED === 'true', }); handleShutdownSignals(