Repository navigation
Expand file tree
/
Copy pathcoverage.ts
More file actions
152 lines (136 loc) · 6.15 KB
/
Copy pathcoverage.ts
File metadata and controls
152 lines (136 loc) · 6.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/**
* Route ↔ OpenAPI coverage bookkeeping.
*
* `openapi.yaml` is published to GitHub Pages on every push, so it is a public
* contract. Nothing used to stop it drifting behind the routes Fastify actually
* registers — it documented seven of roughly thirty. `tests/openapi.test.ts`
* now boots the app, enumerates every registered route and fails when a route
* has no matching `openapi.yaml` entry, so the gap cannot silently widen again.
*
* That test needs to know which routes are deliberately *not* part of the
* public contract. Rather than silently skipping them (which is how a public
* route gets forgotten), every exception is listed in `INTERNAL_ROUTES` below
* with a reason. Anything not listed there must appear in the spec.
*/
/**
* Routes that are intentionally excluded from the public OpenAPI contract.
*
* Entries are OpenAPI-style paths (path parameters in `{braces}`), not the
* Fastify `:param` form — see {@link toOpenApiPath}. Adding a route here is a
* deliberate statement that it is not part of the published API; if you only
* want to skip documenting something, don't — document it instead.
*/
export const INTERNAL_ROUTES: readonly string[] = [
// Prometheus scrape endpoint. Infrastructure, not a client-facing API: it
// speaks the Prometheus text exposition format, not JSON.
'/metrics',
// GraphiQL is the browser IDE for /graphql. It returns an HTML document
// assembled by Mercurius, not data.
'/graphiql',
// WebSocket price stream. OpenAPI 3.0 has no way to describe a websocket
// protocol; the equivalent GraphQL subscription is documented instead.
'/ws',
// Operator-only API-key issuance, rotation, and revocation. Authenticated with a shared
// ADMIN_TOKEN rather than a client API key (see src/api/admin.ts).
'/admin/keys',
'/admin/keys/{id}',
'/admin/keys/{id}/rotate',
// Operator-only usage/quota reporting for minted keys.
'/admin/usage',
'/admin/usage/{keyId}',
'/admin/webhook-delivery-failures',
] as const
/**
* Route prefixes that are internal, for paths whose exact set is owned by a
* dependency rather than by us. Kept separate from `INTERNAL_ROUTES` (which is
* exact-match) so an accidental broad prefix cannot quietly excuse a real
* public route: the only entry today is GraphiQL's static assets, registered
* by Mercurius alongside `/graphiql`.
*/
export const INTERNAL_ROUTE_PREFIXES: readonly string[] = [
// Mercurius serves /graphiql/main.js, /graphiql/sw.js, /graphiql/config.js
// and friends from this namespace. They are part of the GraphiQL IDE, which
// is already allow-listed above.
'/graphiql/',
] as const
/**
* Individual operations that cannot be expressed in this spec, as
* `METHOD /path` pairs.
*
* Kept separate from {@link INTERNAL_ROUTES} because the path here *is* public
* and *is* documented — it is one method on it that OpenAPI cannot describe.
* Excluding the whole path instead would stop the coverage test noticing if
* `GET /graphql` were ever dropped from the spec.
*/
export const UNDOCUMENTABLE_OPERATIONS: readonly string[] = [
// Mercurius registers the HTTP QUERY method on /graphql, from the
// GraphQL-over-HTTP spec. `query` only became a valid OpenAPI operation key
// in 3.2 and this document declares 3.0.0, so there is nowhere to put it.
// `GET /graphql` and `POST /graphql` are documented and carry the same
// semantics; QUERY differs in being a safe, cacheable method with a body.
// Remove this entry if the spec is ever raised to 3.2.
'QUERY /graphql',
] as const
const UNDOCUMENTABLE_OPERATION_SET = new Set<string>(UNDOCUMENTABLE_OPERATIONS)
/**
* Whether a single operation is one OpenAPI 3.0 cannot express.
*
* `method` is matched case-insensitively, since Fastify reports it uppercase
* and OpenAPI keys it lowercase.
*/
export function isUndocumentableOperation(method: string, openApiPath: string): boolean {
return UNDOCUMENTABLE_OPERATION_SET.has(`${method.toUpperCase()} ${openApiPath}`)
}
const INTERNAL_ROUTE_SET = new Set<string>(INTERNAL_ROUTES)
/**
* The version segment every public route is served under.
*
* Fastify registers each route once at its bare path (see `src/api/versioning.ts`
* for the `rewriteUrl` that makes `/v1` resolve to it), so callers can use
* either form. `openapi.yaml` documents the versioned form only — it is the
* canonical one — and the bare form is a deprecated alias.
*/
export const API_VERSION_PREFIX = '/v1'
/**
* Rewrites a Fastify route template into its OpenAPI path equivalent:
* `/price/:assetA/:assetB` → `/price/{assetA}/{assetB}`.
*
* Both frameworks use the same path syntax otherwise, so a single substitution
* of `:name` for `{name}` is all that is needed.
*/
export function toOpenApiPath(fastifyUrl: string): string {
return fastifyUrl.replace(/:([A-Za-z0-9_]+)/g, '{$1}')
}
/** Whether a path (bare or fully-formed URL path) already carries the version prefix. */
export function isVersionedPath(path: string): boolean {
return path === API_VERSION_PREFIX || path.startsWith(`${API_VERSION_PREFIX}/`)
}
/**
* Prefixes a bare route path with the API version segment:
* `/price/{assetA}/{assetB}` → `/v1/price/{assetA}/{assetB}`.
*
* This is the path as it appears in `openapi.yaml`, and the successor a caller
* is pointed at by the deprecation headers on the unversioned alias.
*/
export function toVersionedPath(barePath: string): string {
if (barePath === '/') return API_VERSION_PREFIX
return `${API_VERSION_PREFIX}${barePath}`
}
/**
* Strips the version prefix from a path, yielding the canonical bare route:
* `/v1/price/{assetA}/{assetB}` → `/price/{assetA}/{assetB}`.
*
* A path without the prefix is returned unchanged, so this is safe to call on
* a mix of versioned and bare paths.
*/
export function toBarePath(path: string): string {
if (path === API_VERSION_PREFIX) return '/'
return isVersionedPath(path) ? path.slice(API_VERSION_PREFIX.length) : path
}
/** Whether a route (in OpenAPI path form) is on the internal allow-list. */
export function isInternalRoute(openApiPath: string): boolean {
return (
INTERNAL_ROUTE_SET.has(openApiPath) ||
INTERNAL_ROUTE_PREFIXES.some(prefix => openApiPath.startsWith(prefix))
)
}