@croutonian/with-openapi 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -4
- package/dist/index.d.ts +43 -5
- package/dist/index.js +10 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -129,6 +129,16 @@ callback before that response is built:
|
|
|
129
129
|
| `unsupported_media_type` | 415 | The body's content type is not in the operation's `content`. |
|
|
130
130
|
| `validation_failed` | 400 | A parameter or body failed its schema. |
|
|
131
131
|
|
|
132
|
+
`route_not_found` says which of its two causes it was, because they are the
|
|
133
|
+
same status and very different mistakes — a `basePath` nothing starts with
|
|
134
|
+
turns every route into a 404, and blaming the document sends you looking for a
|
|
135
|
+
path that is already in it:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
no operation in the API description matches "/nope"
|
|
139
|
+
the pathname "/users/me" is outside basePath "/api/v1"
|
|
140
|
+
```
|
|
141
|
+
|
|
132
142
|
The default body:
|
|
133
143
|
|
|
134
144
|
```json
|
|
@@ -213,7 +223,16 @@ handy for customizing one kind and leaving the rest alone.
|
|
|
213
223
|
entry in the document:
|
|
214
224
|
|
|
215
225
|
- `GET /reference` — the HTML page
|
|
216
|
-
- `GET /
|
|
226
|
+
- `GET /openapi.json` — the document, for the page to load
|
|
227
|
+
|
|
228
|
+
Both defaults are derived from `basePath`, so under `basePath: '/api'` they are
|
|
229
|
+
`/api/reference` and `/api/openapi.json`. A reference outside the mount is
|
|
230
|
+
usually unreachable rather than merely unconventional: a host that routes only
|
|
231
|
+
`/api/*` to this handler can never produce a pathname of `/reference`.
|
|
232
|
+
|
|
233
|
+
The document path is derived from the mount, **not** from `path` — the document
|
|
234
|
+
is the artifact and the page is one view of it, so moving the page to `/docs`
|
|
235
|
+
leaves the document where it was.
|
|
217
236
|
|
|
218
237
|
```ts
|
|
219
238
|
withOpenApi({
|
|
@@ -238,11 +257,45 @@ entirely:
|
|
|
238
257
|
|
|
239
258
|
```ts
|
|
240
259
|
reference: {
|
|
241
|
-
html: ({ documentPath }) => myOwnPage(
|
|
260
|
+
html: ({ documentPath, documentUrl }) => myOwnPage(documentUrl)
|
|
242
261
|
}
|
|
243
262
|
```
|
|
244
263
|
|
|
245
|
-
|
|
264
|
+
A path you give explicitly is taken **literally** — `basePath` is not applied
|
|
265
|
+
to it, so an API under `/api/v1` can still put its docs at `/docs`:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
withOpenApi({ document, basePath: '/api/v1', reference: { path: '/docs' } })
|
|
269
|
+
// -> /docs, not /api/v1/docs
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Only the default is derived from the mount.
|
|
273
|
+
|
|
274
|
+
### Behind a gateway that rewrites the path
|
|
275
|
+
|
|
276
|
+
`documentPath` is matched against the pathname this middleware is handed.
|
|
277
|
+
`documentUrl` is what the page tells the browser to fetch. They default to the
|
|
278
|
+
same string, which is right until something rewrites the path in front of you —
|
|
279
|
+
and then no single value works: one spelling never serves the JSON, the other
|
|
280
|
+
renders a page that loads and immediately reports that it could not load the
|
|
281
|
+
document.
|
|
282
|
+
|
|
283
|
+
Supabase Edge Functions is that case by default. The platform routes on
|
|
284
|
+
`/functions/v1/<fn>/...`, strips `/functions/v1`, and hands the worker
|
|
285
|
+
`/<fn>/...`:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
withOpenApi({
|
|
289
|
+
document,
|
|
290
|
+
// What the worker sees — not the public URL, and not `servers[0].url`.
|
|
291
|
+
basePath: '/api',
|
|
292
|
+
reference: {
|
|
293
|
+
path: '/api/reference',
|
|
294
|
+
documentPath: '/api/openapi.json', // where this middleware serves it
|
|
295
|
+
documentUrl: '/functions/v1/api/openapi.json', // where a browser fetches it
|
|
296
|
+
},
|
|
297
|
+
})
|
|
298
|
+
```
|
|
246
299
|
|
|
247
300
|
## Parameters
|
|
248
301
|
|
|
@@ -451,7 +504,7 @@ What it configures, and why each is needed:
|
|
|
451
504
|
| -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
452
505
|
| A **GitHub App** with Contents and Pull requests write, installed on the repo, as `GH_APP_ID` + `GH_APP_PRIVATE_KEY` | release-please has to open a PR, and this org does not let GitHub Actions do that. An App is not GitHub Actions, so the policy does not cover it — and unlike `GITHUB_TOKEN`, its pushes trigger workflows, so the release PR gets CI. |
|
|
453
506
|
| The **pkg.pr.new App** installed on the repo | Branch previews. Without it the preview job warns and skips rather than failing. |
|
|
454
|
-
| An **npm trusted publisher** for `@croutonian/with-openapi
|
|
507
|
+
| An **npm trusted publisher** for `@croutonian/with-openapi`, with direct publish allowed | Publishing without a stored credential. Configurations created after 3 Sep 2026 default to staging only, and `release.yml` runs `npm publish` — stage-only would leave every release waiting in a staging area. |
|
|
455
508
|
| The **JSR package** linked to this repository | Same, on the JSR side. |
|
|
456
509
|
|
|
457
510
|
To do it by hand instead, the same steps are in the comments at the top of
|
package/dist/index.d.ts
CHANGED
|
@@ -124,6 +124,8 @@ declare const SCALAR_CDN_URL = "https://cdn.jsdelivr.net/npm/@scalar/api-referen
|
|
|
124
124
|
interface ScalarHtmlInput {
|
|
125
125
|
/** Absolute path the document JSON is served from. */
|
|
126
126
|
readonly documentPath: string;
|
|
127
|
+
/** URL the page should fetch the document from. */
|
|
128
|
+
readonly documentUrl: string;
|
|
127
129
|
/** Page `<title>`. */
|
|
128
130
|
readonly title: string;
|
|
129
131
|
/** Script URL for Scalar's standalone build. */
|
|
@@ -137,15 +139,44 @@ interface ScalarReferenceOptions {
|
|
|
137
139
|
* Path the HTML page is served from. Matched exactly, and *before* the
|
|
138
140
|
* document's own routes, so it does not need to appear in the document.
|
|
139
141
|
*
|
|
140
|
-
*
|
|
142
|
+
* Given explicitly it is taken literally — `basePath` is **not** applied,
|
|
143
|
+
* so an API under `/api/v1` can still put its docs at `/docs`. The default
|
|
144
|
+
* is derived from `basePath` instead, because a reference sitting outside
|
|
145
|
+
* the mount is usually unreachable rather than merely unconventional: on a
|
|
146
|
+
* host that routes only `/api/*` to this handler, nothing can produce a
|
|
147
|
+
* pathname of `/reference`.
|
|
148
|
+
*
|
|
149
|
+
* @defaultValue `` `${basePath}/reference` ``, or `'/reference'` unmounted
|
|
141
150
|
*/
|
|
142
151
|
path?: string;
|
|
143
152
|
/**
|
|
144
|
-
* Path the document JSON is served from.
|
|
153
|
+
* Path the document JSON is served from. Matched against the pathname this
|
|
154
|
+
* middleware is handed.
|
|
155
|
+
*
|
|
156
|
+
* Derived from `basePath`, not from {@link path}: the document is the
|
|
157
|
+
* artifact and the page is one view of it, so moving the page does not move
|
|
158
|
+
* the document, and the conventional `/openapi.json` is where people —
|
|
159
|
+
* and tooling — look for it.
|
|
145
160
|
*
|
|
146
|
-
* @defaultValue `` `${
|
|
161
|
+
* @defaultValue `` `${basePath}/openapi.json` ``, or `'/openapi.json'`
|
|
147
162
|
*/
|
|
148
163
|
documentPath?: string;
|
|
164
|
+
/**
|
|
165
|
+
* URL the page tells the browser to fetch the document from.
|
|
166
|
+
*
|
|
167
|
+
* Defaults to {@link documentPath}, which is correct whenever the pathname
|
|
168
|
+
* this middleware is handed is the one a browser can reach. Behind a gateway
|
|
169
|
+
* that rewrites the path, it is not, and the two have to be set separately:
|
|
170
|
+
* on Supabase Edge Functions the platform routes on
|
|
171
|
+
* `/functions/v1/<fn>/...` and hands the worker `/<fn>/...`, so the document
|
|
172
|
+
* is *served* at `/api/openapi.json` and *fetched* from
|
|
173
|
+
* `/functions/v1/api/openapi.json`. No single value satisfies both -- set
|
|
174
|
+
* one and the JSON never serves, set the other and the page loads and then
|
|
175
|
+
* reports that it could not load the document.
|
|
176
|
+
*
|
|
177
|
+
* @defaultValue {@link documentPath}
|
|
178
|
+
*/
|
|
179
|
+
documentUrl?: string;
|
|
149
180
|
/** Page title. @defaultValue the document's `info.title`, or `'API Reference'` */
|
|
150
181
|
title?: string;
|
|
151
182
|
/** Script URL for Scalar's standalone build. @defaultValue {@link SCALAR_CDN_URL} */
|
|
@@ -153,7 +184,7 @@ interface ScalarReferenceOptions {
|
|
|
153
184
|
/**
|
|
154
185
|
* Extra options merged into the `Scalar.createApiReference` config — theme,
|
|
155
186
|
* `darkMode`, `proxyUrl`, and anything else Scalar accepts. `url` is set
|
|
156
|
-
* from {@link
|
|
187
|
+
* from {@link documentUrl} and can be overridden here.
|
|
157
188
|
*
|
|
158
189
|
* @see https://scalar.com/products/api-references/configuration
|
|
159
190
|
*/
|
|
@@ -208,6 +239,13 @@ interface OpenApiRejection {
|
|
|
208
239
|
readonly method: string;
|
|
209
240
|
/** The request's pathname, before `basePath` is stripped. */
|
|
210
241
|
readonly pathname: string;
|
|
242
|
+
/**
|
|
243
|
+
* A more specific explanation than the kind's stock wording, when there is
|
|
244
|
+
* one to give. `route_not_found` uses it to say whether the pathname missed
|
|
245
|
+
* `basePath` or matched no template under it, which are the same status and
|
|
246
|
+
* very different mistakes.
|
|
247
|
+
*/
|
|
248
|
+
readonly message?: string;
|
|
211
249
|
/** Path template that matched, when one did. */
|
|
212
250
|
readonly route?: string;
|
|
213
251
|
/** Methods the route does declare. Set on `method_not_allowed`. */
|
|
@@ -316,7 +354,7 @@ interface WithOpenApiConfig {
|
|
|
316
354
|
/**
|
|
317
355
|
* Serve a Scalar API reference and the document JSON. `true` takes every
|
|
318
356
|
* default — the page at `/reference`, the document at
|
|
319
|
-
* `/
|
|
357
|
+
* `/openapi.json`.
|
|
320
358
|
*
|
|
321
359
|
* @defaultValue off
|
|
322
360
|
*/
|
package/dist/index.js
CHANGED
|
@@ -868,18 +868,20 @@ function joinPath(base, child) {
|
|
|
868
868
|
return `${base.endsWith("/") ? base.slice(0, -1) : base}/${child}`;
|
|
869
869
|
}
|
|
870
870
|
/** Fill in the reference endpoint's defaults, once, at construction. */
|
|
871
|
-
function resolveReference(document, options) {
|
|
872
|
-
const path = options.path ?? "
|
|
871
|
+
function resolveReference(document, options, basePath) {
|
|
872
|
+
const path = options.path ?? joinPath(basePath ?? "", "reference");
|
|
873
873
|
if (!path.startsWith("/")) throw new Error(`withOpenApi: reference.path must start with "/", got ${JSON.stringify(path)}`);
|
|
874
|
-
const documentPath = options.documentPath ?? joinPath(
|
|
874
|
+
const documentPath = options.documentPath ?? joinPath(basePath ?? "", "openapi.json");
|
|
875
|
+
const documentUrl = options.documentUrl ?? documentPath;
|
|
875
876
|
const title = options.title ?? document.info?.title ?? "API Reference";
|
|
876
877
|
const cdnUrl = options.cdnUrl ?? SCALAR_CDN_URL;
|
|
877
878
|
const configuration = {
|
|
878
|
-
url:
|
|
879
|
+
url: documentUrl,
|
|
879
880
|
...options.configuration
|
|
880
881
|
};
|
|
881
882
|
const page = (options.html ?? renderScalarHtml)({
|
|
882
883
|
documentPath,
|
|
884
|
+
documentUrl,
|
|
883
885
|
title,
|
|
884
886
|
cdnUrl,
|
|
885
887
|
configuration
|
|
@@ -887,6 +889,7 @@ function resolveReference(document, options) {
|
|
|
887
889
|
return {
|
|
888
890
|
path,
|
|
889
891
|
documentPath,
|
|
892
|
+
documentUrl,
|
|
890
893
|
cacheControl: options.cacheControl ?? "no-cache",
|
|
891
894
|
render: () => page
|
|
892
895
|
};
|
|
@@ -1151,7 +1154,7 @@ function defaultRejectionResponse(rejection) {
|
|
|
1151
1154
|
if (rejection.allow !== void 0 && rejection.allow.length > 0) headers.set("allow", rejection.allow.join(", "));
|
|
1152
1155
|
return Response.json({
|
|
1153
1156
|
error: rejection.kind,
|
|
1154
|
-
message: REJECTION_MESSAGES[rejection.kind],
|
|
1157
|
+
message: rejection.message ?? REJECTION_MESSAGES[rejection.kind],
|
|
1155
1158
|
...rejection.accepts === void 0 ? {} : { accepts: rejection.accepts },
|
|
1156
1159
|
violations: rejection.violations
|
|
1157
1160
|
}, {
|
|
@@ -1246,7 +1249,7 @@ const withOpenApi = defineMiddleware({
|
|
|
1246
1249
|
const onUnknownMethod = config.onUnknownMethod ?? "reject";
|
|
1247
1250
|
const resolve = (schema) => resolveSchema(document, schema);
|
|
1248
1251
|
const validateSchema = options === void 0 ? void 0 : createSchemaValidator(document, config.schemaDraft ?? draftFor(document));
|
|
1249
|
-
const reference = config.reference === void 0 || config.reference === false ? void 0 : resolveReference(document, config.reference === true ? {} : config.reference);
|
|
1252
|
+
const reference = config.reference === void 0 || config.reference === false ? void 0 : resolveReference(document, config.reference === true ? {} : config.reference, basePath);
|
|
1250
1253
|
const documentJson = reference === void 0 ? void 0 : JSON.stringify(document);
|
|
1251
1254
|
const cors = config.cors === void 0 ? void 0 : createCorsPolicy(document, routes, config.cors);
|
|
1252
1255
|
/**
|
|
@@ -1297,6 +1300,7 @@ const withOpenApi = defineMiddleware({
|
|
|
1297
1300
|
status: 404,
|
|
1298
1301
|
method: req.method,
|
|
1299
1302
|
pathname: url.pathname,
|
|
1303
|
+
message: pathname === void 0 ? `the pathname ${JSON.stringify(url.pathname)} is outside basePath ${JSON.stringify(basePath)}` : `no operation in the API description matches ${JSON.stringify(pathname)}`,
|
|
1300
1304
|
violations: []
|
|
1301
1305
|
});
|
|
1302
1306
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@croutonian/with-openapi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "OpenAPI middleware for @supabase/middleware. Matches each request against an OpenAPI 3.1 document, optionally rejects the ones it does not describe, contributes the matched operation and validated params to ctx, and optionally serves a Scalar API reference.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openapi",
|