diff --git a/README.md b/README.md index cb89ab2..eaeb32a 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,12 @@ Generates static content into the `build/` directory. The site builds in one of two modes, controlled by `DEPLOY_ENV`: -- `DEPLOY_ENV=production` — emits `https://docs.trakrf.id` / `https://app.trakrf.id` URLs in SSR HTML, sitemap, canonical tags, and the `` family of components. The Redoc-rendered `/api` page fetches the spec from `https://app.trakrf.id/api/v1/openapi.yaml` at build time. -- `DEPLOY_ENV=preview` (default for local dev) — emits the `*.preview.trakrf.id` equivalents and fetches the spec from `https://app.preview.trakrf.id/api/v1/openapi.yaml`. +- `DEPLOY_ENV=production` — emits `https://docs.trakrf.id` / `https://app.trakrf.id` URLs in SSR HTML, sitemap, canonical tags, and the `` family of components. The Redoc-rendered `/api` page fetches the spec from `https://app.trakrf.id/api/openapi.yaml` at build time. +- `DEPLOY_ENV=preview` (default for local dev) — emits the `*.preview.trakrf.id` equivalents and fetches the spec from `https://app.preview.trakrf.id/api/openapi.yaml`. On Cloudflare Pages, set `DEPLOY_ENV=production` on the production environment and `DEPLOY_ENV=preview` on the preview environment. If unset, the build auto-detects from `CF_PAGES_BRANCH` (`main` → production, anything else → preview). -The OpenAPI spec is single-source on the platform; this site never stores a mirrored copy. `/api/openapi.{json,yaml}` 302s to `https://app.{env}.trakrf.id/api/v1/openapi.{json,yaml}` via `functions/_middleware.js`. +The OpenAPI spec is single-source on the platform; this site never stores a mirrored copy. `/api/openapi.{json,yaml}` 302s to `https://app.{env}.trakrf.id/api/openapi.{json,yaml}` via `functions/_middleware.js`. ### Serve diff --git a/docs/api/README.mdx b/docs/api/README.mdx index ae795d1..8e22dc2 100644 --- a/docs/api/README.mdx +++ b/docs/api/README.mdx @@ -15,8 +15,8 @@ export const SpecLinks = () => { const { appHost } = useDeployEnv(); return ( <> - {appHost}/api/v1/openapi.json /{" "} - {appHost}/api/v1/openapi.yaml + {appHost}/api/openapi.json /{" "} + {appHost}/api/openapi.yaml ); }; diff --git a/docs/api/postman.mdx b/docs/api/postman.mdx index 9904060..d2e1b4d 100644 --- a/docs/api/postman.mdx +++ b/docs/api/postman.mdx @@ -19,10 +19,10 @@ export const SpecUrls = () => { return ( ); diff --git a/docs/api/quickstart.mdx b/docs/api/quickstart.mdx index 2dc07bf..39e6a4c 100644 --- a/docs/api/quickstart.mdx +++ b/docs/api/quickstart.mdx @@ -157,7 +157,7 @@ Strict-typed codegen (Pydantic, Java with generated POJOs, Go with generated str Prefer a GUI? Postman (and Insomnia, Bruno, Hoppscotch) imports the OpenAPI spec from a URL and generates a request collection automatically: -1. In Postman, **File → Import → Link**, paste /api/v1/openapi.yaml. +1. In Postman, **File → Import → Link**, paste /api/openapi.yaml. 2. Set the collection variables: - `baseUrl` → `https://app.trakrf.id` (or `https://app.preview.trakrf.id` for preview accounts) — bare host, no `/api/v1` suffix; paths in the collection already include the version prefix - `bearerToken` → the JWT from step 2 @@ -170,9 +170,9 @@ Full detail: [API clients](./postman). If you'd rather generate a typed client, the OpenAPI spec is served from the platform in both formats: - - /api/v1/openapi.json (JSON) + /api/openapi.json (JSON) - - /api/v1/openapi.yaml (YAML) + /api/openapi.yaml (YAML) Feed either into `openapi-generator-cli`, NSwag, `oapi-codegen`, etc. to scaffold client code in your language. The spec is regenerated from the Go handlers on every platform release, so the generated client stays in sync with the running service. diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 84f6592..bab0c46 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -21,7 +21,7 @@ const appHost = deployEnv === "production" ? "https://app.trakrf.id" : "https://app.preview.trakrf.id"; -const specUrl = `${appHost}/api/v1/openapi.yaml`; +const specUrl = `${appHost}/api/openapi.yaml`; const config: Config = { title: "TrakRF Docs", diff --git a/functions/_middleware.js b/functions/_middleware.js index 5c5562a..f553201 100644 --- a/functions/_middleware.js +++ b/functions/_middleware.js @@ -1,6 +1,6 @@ // Cloudflare Pages Functions middleware. // -// Two jobs: +// Three jobs: // // 1. Spec asset redirects. /api/openapi.{json,yaml} 302 to the platform's // canonical spec URL on app.{env}.trakrf.id. Single source of truth lives @@ -12,6 +12,13 @@ // (see REWRITE_MAP below). All collapse to /api/openapi.{json,yaml}, // which then redirects out via job (1). // +// 3. Explicit 410 Gone for retired mirror artifacts. `platform-meta.json` +// and the bundled Postman collection were deleted in TRA-743 but +// Cloudflare's edge cache may keep serving the last-deploy 200 for the +// TTL window. Returning an authoritative 410 with `Cache-Control: +// no-store` displaces the stale entry on next request and prevents any +// BB cycle from treating the stale body as ground truth. +// // History note: redirects used to live in static/_redirects, but Cloudflare // Pages on this project does not appear to honor _redirects entries — the // original /redocusaurus/trakrf-api.yaml rule (TRA-598) was never actually @@ -35,6 +42,13 @@ const REWRITE_MAP = { "/redocusaurus/trakrf-api.yaml": "/api/openapi.yaml", }; +/** Retired mirror artifacts — return 410 Gone with no-store to displace + * any stale CF-edge-cached 200 responses from the pre-TRA-743 deploy. */ +const RETIRED_PATHS = new Set([ + "/api/platform-meta.json", + "/api/trakrf-api.postman_collection.json", +]); + /** @param {URL} url */ function resolveAppOrigin(url) { const host = url.hostname; @@ -49,6 +63,21 @@ function resolveAppOrigin(url) { export const onRequest = async ({ request, next }) => { const url = new URL(request.url); + if (RETIRED_PATHS.has(url.pathname)) { + return new Response( + `Gone. This artifact was retired in TRA-743 (single-source OpenAPI ` + + `spec on platform). See https://docs.trakrf.id/docs/api/postman for ` + + `URL-import guidance.\n`, + { + status: 410, + headers: { + "Content-Type": "text/plain; charset=utf-8", + "Cache-Control": "no-store", + }, + }, + ); + } + const alias = REWRITE_MAP[url.pathname]; if (alias) { const dest = new URL(alias, url.origin); @@ -58,7 +87,11 @@ export const onRequest = async ({ request, next }) => { const specFormat = SPEC_TARGETS[url.pathname]; if (specFormat) { const appOrigin = resolveAppOrigin(url); - const dest = `${appOrigin}/api/v1/openapi.${specFormat}`; + // Target the platform's canonical path (`/api/openapi.*`); the + // `/api/v1/openapi.*` form on platform is itself a 301 to the canonical, + // so naming the intermediate hop would add an extra redirect to every + // client. + const dest = `${appOrigin}/api/openapi.${specFormat}`; return Response.redirect(dest, 302); } diff --git a/tests/blackbox/BB.md b/tests/blackbox/BB.md index c9d4bc3..44ff783 100644 --- a/tests/blackbox/BB.md +++ b/tests/blackbox/BB.md @@ -74,7 +74,7 @@ After your exploratory evaluation, run a mechanical pass against the published O The OpenAPI spec is published at `$API_TEST_DOCS_URL/api/openapi.yaml` (JSON variant: `$API_TEST_DOCS_URL/api/openapi.json`). If that path 404s, that is itself a finding worth reporting. If the docs don't link to it from a discoverable location, that's also a finding. -The docs origin 302-redirects these paths to the platform's canonical spec on `app.{env}.trakrf.id/api/v1/openapi.{yaml,json}` — single source of truth, no mirror. Standard tooling follows the redirect transparently (curl with `-L`, every OpenAPI codegen client, every API-explorer import-by-URL flow). The spec at this URL is the contract you're testing against. +The docs origin 302-redirects these paths to the platform's canonical spec on `app.{env}.trakrf.id/api/openapi.{yaml,json}` — single source of truth, no mirror. Standard tooling follows the redirect transparently (curl with `-L`, every OpenAPI codegen client, every API-explorer import-by-URL flow). The spec at this URL is the contract you're testing against. ### 2. Walk every path @@ -173,11 +173,10 @@ For each generator, run a CRUD lifecycle through the generated client against th - Authentication setup friction — does the generated client know how to attach the API key from the spec alone, or did you have to wire it manually? - Generated identifier names — are model class names, method names, and field names clean and conventional in the target language? Flag double-prefixed names (e.g., `AssetPublicAssetView` where the spec schema is `asset.PublicAssetView`), unusual casing (`assets_create` where `createAsset` is conventional), or names that leak the backend's package/namespace structure into the client. -### 11. Multi-format spec consistency +### 11. Multi-format spec sanity -The spec may be served in multiple formats (YAML, JSON, Redoc-served copy). Fetch each variant and compare: +`/api/openapi.yaml` and `/api/openapi.json` resolve to the same platform binary via redirect — divergence between formats would mean the platform is encoding the spec twice and the two encoders disagree, which is a platform bug, not a docs-mirror drift class. So this check is light: -- Are the artifacts byte-equivalent after canonical sort? - Do numeric literals match across formats? YAML scientific notation (`2.147483647e+09`) parses as float in standard YAML loaders, even when paired with `type: integer` — flag any divergence between YAML and JSON variants for the same field. - Do any artifacts contain hardcoded URLs that differ from the environment the spec is served from (preview spec hardcoding production docs URL, etc.)? - Are all variants linked from at least one discoverable docs page? An undiscoverable artifact is a finding.