Skip to content

Latest commit

 

History

History

public-api

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 

README.md

Patcher Public Open API Worker

Local Cloudflare Worker implementation for the key-required Patcher Public Open API at https://api.patcher.xyz/v1.

The MVP is live at api.patcher.xyz. The database foundation, Vault pepper, least-privilege reader login, direct-endpoint Hyperdrive binding, Durable Object, Worker secret, custom domain, and production Worker are active. Catalogue, key lifecycle, quota-reporting, ETag, HEAD, and cache behavior passed production smoke tests, and the temporary smoke Worker has been deleted. The owner deferred an outer WAF rule for the initial launch; mandatory API keys and per-key Durable Object quotas remain active. The production User Area flag is committed on develop and awaits the next app release.

Contract

  • Public consumer docs: docs.patcher.xyz/reference/public-open-api
  • Canonical API contract: openapi.yaml
  • Canonical API contract on GitHub: Polyterative/Patcher/cloudflare/public-api/openapi.yaml
  • Base URL: https://api.patcher.xyz/v1
  • Format: JSON only, GET/HEAD only, CORS * for read requests.
  • Authentication: every data request must send an Authorization header using the Bearer scheme. The credential must start with pk_live_ and then contain exactly 22 base64url characters.
  • Default free quota: 5,000 requests/month and 60 requests/minute. Partner keys are manually provisioned.
  • License for exposed catalogue data: CC BY 4.0; API consumers must attribute Patcher when redisplaying or reusing the data.

No anonymous tier exists. Missing or malformed keys fail before cache lookup.

Architecture

Consumer
  -> Cloudflare Worker route api.patcher.xyz/v1/*
  -> URL normalization + route/query allowlist
  -> API key HMAC verification with API_KEY_PEPPER
  -> Hyperdrive -> direct Supabase Postgres endpoint -> public.verify_api_key(...)
  -> per-key Durable Object API_KEY_COUNTER quota consume
  -> Cloudflare Cache API lookup by normalized public URL only
  -> on cache miss: Hyperdrive -> direct Supabase Postgres endpoint -> api_v1_* views

The Worker never contains a Supabase service_role key, project JWT signing secret, user credential, or raw API key storage. Runtime reads use a least-privilege api_reader database role through Hyperdrive.

Request flow

  1. Reject any method other than GET, HEAD, or OPTIONS with 405 method_not_allowed and an Allow header, before path/query normalization or authentication.
  2. Normalize path and query parameters. Unknown query parameters return 400 unknown_parameter; unsupported q returns 400 unsupported_parameter.
  3. Parse the Authorization header. Missing returns 401 missing_authorization; a non-Bearer scheme, missing pk_live_ prefix, wrong suffix length, or non-base64url suffix returns 401 malformed_authorization.
  4. HMAC the decoded 16-byte key suffix with API_KEY_PEPPER; verify active metadata through public.verify_api_key(bytea).
  5. Consume quota in the per-key Durable Object before cache lookup. Quota responses attach X-RateLimit-Limit-Minute, X-RateLimit-Remaining-Minute, X-RateLimit-Limit-Month, X-RateLimit-Remaining-Month, and X-RateLimit-Reset; in the MVP, X-RateLimit-Reset is the current minute-window start timestamp, not the next reset time. 429 also includes Retry-After.
  6. Serve from Cache API when possible. Cache keys use only method, normalized path, and normalized query; Authorization is never part of the cache key or Vary.
  7. On miss, query only api_v1_* views through parameterized postgres.js calls over Hyperdrive.
  8. Return SHA-256 ETag values. Matching If-None-Match returns 304 after authentication and quota consumption, with current per-key quota headers.

Endpoints

Endpoint Purpose Notes
GET /v1/modules Paginated public modules Filters: manufacturer_id, hp, standard, tag; includes: ins, outs, tags, panels.
GET /v1/modules/{id} One public module Supports fields and module includes.
GET /v1/manufacturers Manufacturers that have at least one publishable module Supports pagination and fields.
GET /v1/manufacturers/{id} One public manufacturer include=modules adds safe module summaries.
GET /v1/standards Reference standards Supports pagination and fields.
GET /v1/tags Reference tags type is a semantic lowercase enum or null; supports pagination and fields.

Module, manufacturer, and tag IDs are positive integers. Standard IDs are nonnegative because production standard 0 is the valid 3U row; the module standard filter therefore accepts 0.

List endpoints support:

  • limit: 1..100, default 50.
  • sort: name or id, ascending, with id as tie-breaker for name.
  • cursor: opaque base64url cursor shaped as {"v":1,"s":<last_sort_value>,"id":<last_id>}.
  • fields: comma-separated allowlisted top-level fields; id is always retained.
  • q: reserved for future pg_trgm search and intentionally returns 400 unsupported_parameter in the MVP.

Examples

Use a placeholder key shape in docs and tests only. Real keys are returned once by the database mint RPC and must not be committed, logged, or pasted into issue trackers.

AUTH_HEADER='Authorization'
AUTH_SCHEME='Bearer'
API_KEY='pk_live_<replace-with-real-22-char-suffix>'
curl -sS 'https://api.patcher.xyz/v1/modules?limit=10&include=tags,panels' \
  -H "${AUTH_HEADER}: ${AUTH_SCHEME} ${API_KEY}"
AUTH_HEADER='Authorization'
AUTH_SCHEME='Bearer'
API_KEY='pk_live_<replace-with-real-22-char-suffix>'
curl -i 'https://api.patcher.xyz/v1/manufacturers/123?include=modules&fields=name,website_url' \
  -H "${AUTH_HEADER}: ${AUTH_SCHEME} ${API_KEY}"
AUTH_HEADER='Authorization'
AUTH_SCHEME='Bearer'
API_KEY='pk_live_<replace-with-real-22-char-suffix>'
curl -i 'https://api.patcher.xyz/v1/tags' \
  -H "${AUTH_HEADER}: ${AUTH_SCHEME} ${API_KEY}" \
  -H 'If-None-Match: "previous-etag"'

Error envelope:

{
  "error": {
    "code": "invalid_key",
    "message": "API key is invalid",
    "request_id": "00000000-0000-4000-8000-000000000000"
  }
}

Public data exclusions

The v1 contract exposes only allowlisted catalogue fields from publishable module/manufacturer data and reference rows. It deliberately excludes:

  • private users, private profiles, private racks, private patches, addresses, marketplace transactions, emails, admin IDs, tokens, and analytics;
  • submitter attribution and moderation/private operational fields;
  • panel image URLs and panel filenames;
  • pricing and store listings;
  • bulk export endpoints;
  • public rack and public patch endpoints.

Bulk JSONL delivery is a Structural follow-up. Public racks and patches are deferred to a later contract review and will use existing opaque public_id patterns rather than integer IDs.

Dataset JSONL contract (local shaping only)

The local src/dataset-export.ts helper defines the row encoding for four future full-catalogue snapshots: modules, manufacturers, standards, and tags. Each line is one UTF-8 JSON object using the same allowlisted fields and normalization as the corresponding v1 catalogue response item. Lines end in LF; an empty dataset produces an empty byte stream. Rows must be supplied in strictly increasing numeric id order, and the helper yields one encoded line at a time so the caller does not need to buffer the whole export.

The future GET /v1/datasets manifest is expected to identify each dataset by name and report its byte size and SHA-256 digest. A future authenticated GET /v1/datasets/{name} will stream the matching JSONL object through the Worker; direct or presigned R2 URLs are not part of the contract. This helper does not add either route, manifest generation, a scheduled export job, R2 storage, or Cloudflare bindings. Those remain separately gated.

Source map

Path Responsibility
src/index.ts Top-level Worker pipeline, auth/quota/catalogue handoff, Cloudflare export.
src/auth.ts Bearer / pk_live_ parsing and HMAC digest generation.
src/api-key-metadata-cache.ts Fixed-TTL isolate metadata cache; revocation bound stays <= 60 seconds.
src/api-key-counter.ts Durable Object quota counter, usage flush, alarm retry.
src/quota.ts and src/quota-response.ts Pure quota boundary logic and rate-limit headers.
src/request.ts Route detection, query allowlists, normalization, cursors.
src/catalogue-provider.ts Parameterized catalogue queries against api_v1_* views.
src/catalogue-serving.ts Cache API, ETag/304, stale-while-revalidate, origin error handling.
src/catalogue-mapping.ts Row normalization, field allowlists, sparse fields, cursor encoding.
src/dataset-export.ts Local streaming JSONL shaping for the four public catalogue datasets; not connected to routes or storage.
src/database.ts postgres.js Hyperdrive client, API key RPC calls, usage reporter.
wrangler.jsonc Local Worker entry and Durable Object class declaration only; no real remote IDs.
openapi.yaml OpenAPI 3.1 consumer contract.
../../supabase/migrations/20260724133100_api_reader_roles.sql Local role foundation, credential-free.
../../supabase/migrations/20260724133200_api_identity.sql API tiers, keys, usage, Vault-backed mint/verify/revoke/report RPCs.
../../supabase/migrations/20260724133300_api_v1_views.sql Security-barrier public catalogue views and least-privilege grants.
../../supabase/migrations/20260724133400_api_vault_permissions.sql Restricted Vault key-ID access required by the managed migration owner.
../../scripts/tests/public-api-worker.test.mjs Local Worker contract tests.
../../scripts/tests/public-open-api-migrations.test.cjs Static migration contract tests.

Local commands

Use pnpm; do not use npm or watch-mode test commands.

pnpm test:functions:public-api-worker
pnpm test:functions:public-open-api-migrations
node scripts/checks/check-docs.cjs

No-dependency OpenAPI smoke check:

node -e "const fs=require('node:fs'); const s=fs.readFileSync('cloudflare/public-api/openapi.yaml','utf8'); for (const t of ['openapi: 3.1.0','/modules:','/manufacturers:','apiKey:','ErrorResponse:']) if (!s.includes(t)) throw new Error('OpenAPI smoke check missing '+t);"

The checked-in wrangler.jsonc intentionally contains no production resource IDs or credentials. Use dependency-injected tests for local validation; operators deploy only through approved environment-specific configuration that remains outside git.

Non-goals

  • No write API.
  • No anonymous access.
  • No GraphQL.
  • No billing or paid self-service tier in v1.
  • No direct PostgREST/anon exposure.
  • No service_role, JWT signing secret, or raw database credential in the Worker.
  • No Patcher-docs repository edits from this package; public marketing/consumer docs live at https://docs.patcher.xyz/reference/public-open-api.