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 (
-
-
{appHost}/api/v1/openapi.yaml
+ {appHost}/api/openapi.yaml
-
-
{appHost}/api/v1/openapi.json
+ {appHost}/api/openapi.json
);
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.