@fluixi/api 0.0.0-stage → 0.1.0-alpha.3
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/LICENSE +21 -0
- package/README.md +37 -2
- package/dist/client.cjs +103 -0
- package/dist/client.d.ts +95 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +87 -0
- package/dist/client.mjs +82 -0
- package/dist/errors.cjs +63 -0
- package/dist/errors.d.ts +47 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +55 -0
- package/dist/errors.mjs +42 -0
- package/dist/index.cjs +667 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.mjs +5442 -0
- package/dist/input.cjs +91 -0
- package/dist/input.d.ts +15 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.js +73 -0
- package/dist/input.mjs +68 -0
- package/dist/middleware.cjs +41 -0
- package/dist/middleware.d.ts +9 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +19 -0
- package/dist/middleware.mjs +20 -0
- package/dist/openapi.cjs +282 -0
- package/dist/openapi.d.ts +60 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +312 -0
- package/dist/openapi.mjs +259 -0
- package/dist/rate-limit.cjs +158 -0
- package/dist/rate-limit.d.ts +69 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +121 -0
- package/dist/rate-limit.mjs +4935 -0
- package/dist/route.cjs +192 -0
- package/dist/route.d.ts +97 -0
- package/dist/route.d.ts.map +1 -0
- package/dist/route.js +98 -0
- package/dist/route.mjs +169 -0
- package/dist/security.cjs +222 -0
- package/dist/security.d.ts +64 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +135 -0
- package/dist/security.mjs +4997 -0
- package/dist/standard-schema.cjs +18 -0
- package/dist/standard-schema.d.ts +39 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/dist/standard-schema.js +6 -0
- package/dist/standard-schema.mjs +0 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -0
- package/package.json +54 -3
package/dist/input.cjs
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/input.ts
|
|
21
|
+
var input_exports = {};
|
|
22
|
+
__export(input_exports, {
|
|
23
|
+
DEFAULT_BODY_LIMIT: () => DEFAULT_BODY_LIMIT,
|
|
24
|
+
readJson: () => readJson,
|
|
25
|
+
readQuery: () => readQuery
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(input_exports);
|
|
28
|
+
|
|
29
|
+
// src/errors.ts
|
|
30
|
+
var ApiError = class extends Error {
|
|
31
|
+
constructor(status, code, message, details) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "ApiError";
|
|
34
|
+
this.status = status;
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.details = details;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// src/input.ts
|
|
41
|
+
var DEFAULT_BODY_LIMIT = 1024 * 1024;
|
|
42
|
+
function readQuery(url) {
|
|
43
|
+
const out = {};
|
|
44
|
+
for (const [key, value] of url.searchParams) {
|
|
45
|
+
const seen = out[key];
|
|
46
|
+
if (seen === void 0) out[key] = value;
|
|
47
|
+
else out[key] = Array.isArray(seen) ? [...seen, value] : [seen, value];
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
async function readJson(request, limit = DEFAULT_BODY_LIMIT) {
|
|
52
|
+
const declared = Number(request.headers.get("content-length"));
|
|
53
|
+
if (Number.isFinite(declared) && declared > limit) throw tooLarge(limit);
|
|
54
|
+
if (!request.body) return void 0;
|
|
55
|
+
const type = request.headers.get("content-type") ?? "";
|
|
56
|
+
const reader = request.body.getReader();
|
|
57
|
+
const chunks = [];
|
|
58
|
+
let size = 0;
|
|
59
|
+
for (; ; ) {
|
|
60
|
+
const { done, value } = await reader.read();
|
|
61
|
+
if (done) break;
|
|
62
|
+
size += value.byteLength;
|
|
63
|
+
if (size > limit) {
|
|
64
|
+
await reader.cancel();
|
|
65
|
+
throw tooLarge(limit);
|
|
66
|
+
}
|
|
67
|
+
chunks.push(value);
|
|
68
|
+
}
|
|
69
|
+
if (size === 0) return void 0;
|
|
70
|
+
if (!isJson(type)) {
|
|
71
|
+
throw new ApiError(415, "unsupported_media_type", "Send the body as application/json.");
|
|
72
|
+
}
|
|
73
|
+
const bytes = new Uint8Array(size);
|
|
74
|
+
let offset = 0;
|
|
75
|
+
for (const chunk of chunks) {
|
|
76
|
+
bytes.set(chunk, offset);
|
|
77
|
+
offset += chunk.byteLength;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
81
|
+
} catch {
|
|
82
|
+
throw new ApiError(400, "invalid_json", "The body is not valid JSON.");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function isJson(type) {
|
|
86
|
+
const mime = type.split(";")[0].trim().toLowerCase();
|
|
87
|
+
return mime === "application/json" || mime.endsWith("+json");
|
|
88
|
+
}
|
|
89
|
+
function tooLarge(limit) {
|
|
90
|
+
return new ApiError(413, "payload_too_large", `The body is larger than ${limit} bytes.`);
|
|
91
|
+
}
|
package/dist/input.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** 1 MiB. A JSON API rarely needs more; files go to storage directly, not through a route. */
|
|
2
|
+
export declare const DEFAULT_BODY_LIMIT: number;
|
|
3
|
+
/**
|
|
4
|
+
* The query string as a plain object: `?a=1` is `{ a: '1' }`, and a repeated key becomes an
|
|
5
|
+
* array, `?tag=x&tag=y` is `{ tag: ['x', 'y'] }`. Schemas coerce from there.
|
|
6
|
+
*/
|
|
7
|
+
export declare function readQuery(url: URL): Record<string, string | string[]>;
|
|
8
|
+
/**
|
|
9
|
+
* The JSON body, or `undefined` when there is none.
|
|
10
|
+
*
|
|
11
|
+
* The size limit is checked on the bytes read, not only on `Content-Length`, which a client can
|
|
12
|
+
* leave out or get wrong: reading stops at the limit instead of buffering whatever arrives.
|
|
13
|
+
*/
|
|
14
|
+
export declare function readJson(request: Request, limit?: number): Promise<unknown>;
|
|
15
|
+
//# sourceMappingURL=input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"AAKA,8FAA8F;AAC9F,eAAO,MAAM,kBAAkB,QAAc,CAAC;AAE9C;;;GAGG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAQrE;AAED;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,SAAqB,GAAG,OAAO,CAAC,OAAO,CAAC,CAmC7F"}
|
package/dist/input.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading what a request carries, before any schema sees it.
|
|
3
|
+
*/
|
|
4
|
+
import { ApiError } from './errors.js';
|
|
5
|
+
/** 1 MiB. A JSON API rarely needs more; files go to storage directly, not through a route. */
|
|
6
|
+
export const DEFAULT_BODY_LIMIT = 1024 * 1024;
|
|
7
|
+
/**
|
|
8
|
+
* The query string as a plain object: `?a=1` is `{ a: '1' }`, and a repeated key becomes an
|
|
9
|
+
* array, `?tag=x&tag=y` is `{ tag: ['x', 'y'] }`. Schemas coerce from there.
|
|
10
|
+
*/
|
|
11
|
+
export function readQuery(url) {
|
|
12
|
+
const out = {};
|
|
13
|
+
for (const [key, value] of url.searchParams) {
|
|
14
|
+
const seen = out[key];
|
|
15
|
+
if (seen === undefined)
|
|
16
|
+
out[key] = value;
|
|
17
|
+
else
|
|
18
|
+
out[key] = Array.isArray(seen) ? [...seen, value] : [seen, value];
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The JSON body, or `undefined` when there is none.
|
|
24
|
+
*
|
|
25
|
+
* The size limit is checked on the bytes read, not only on `Content-Length`, which a client can
|
|
26
|
+
* leave out or get wrong: reading stops at the limit instead of buffering whatever arrives.
|
|
27
|
+
*/
|
|
28
|
+
export async function readJson(request, limit = DEFAULT_BODY_LIMIT) {
|
|
29
|
+
const declared = Number(request.headers.get('content-length'));
|
|
30
|
+
if (Number.isFinite(declared) && declared > limit)
|
|
31
|
+
throw tooLarge(limit);
|
|
32
|
+
if (!request.body)
|
|
33
|
+
return undefined;
|
|
34
|
+
const type = request.headers.get('content-type') ?? '';
|
|
35
|
+
const reader = request.body.getReader();
|
|
36
|
+
const chunks = [];
|
|
37
|
+
let size = 0;
|
|
38
|
+
for (;;) {
|
|
39
|
+
const { done, value } = await reader.read();
|
|
40
|
+
if (done)
|
|
41
|
+
break;
|
|
42
|
+
size += value.byteLength;
|
|
43
|
+
if (size > limit) {
|
|
44
|
+
await reader.cancel();
|
|
45
|
+
throw tooLarge(limit);
|
|
46
|
+
}
|
|
47
|
+
chunks.push(value);
|
|
48
|
+
}
|
|
49
|
+
if (size === 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
if (!isJson(type)) {
|
|
52
|
+
throw new ApiError(415, 'unsupported_media_type', 'Send the body as application/json.');
|
|
53
|
+
}
|
|
54
|
+
const bytes = new Uint8Array(size);
|
|
55
|
+
let offset = 0;
|
|
56
|
+
for (const chunk of chunks) {
|
|
57
|
+
bytes.set(chunk, offset);
|
|
58
|
+
offset += chunk.byteLength;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new ApiError(400, 'invalid_json', 'The body is not valid JSON.');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function isJson(type) {
|
|
68
|
+
const mime = type.split(';')[0].trim().toLowerCase();
|
|
69
|
+
return mime === 'application/json' || mime.endsWith('+json');
|
|
70
|
+
}
|
|
71
|
+
function tooLarge(limit) {
|
|
72
|
+
return new ApiError(413, 'payload_too_large', `The body is larger than ${limit} bytes.`);
|
|
73
|
+
}
|
package/dist/input.mjs
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var ApiError = class extends Error {
|
|
3
|
+
constructor(status, code, message, details) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "ApiError";
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.details = details;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/input.ts
|
|
13
|
+
var DEFAULT_BODY_LIMIT = 1024 * 1024;
|
|
14
|
+
function readQuery(url) {
|
|
15
|
+
const out = {};
|
|
16
|
+
for (const [key, value] of url.searchParams) {
|
|
17
|
+
const seen = out[key];
|
|
18
|
+
if (seen === void 0) out[key] = value;
|
|
19
|
+
else out[key] = Array.isArray(seen) ? [...seen, value] : [seen, value];
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
async function readJson(request, limit = DEFAULT_BODY_LIMIT) {
|
|
24
|
+
const declared = Number(request.headers.get("content-length"));
|
|
25
|
+
if (Number.isFinite(declared) && declared > limit) throw tooLarge(limit);
|
|
26
|
+
if (!request.body) return void 0;
|
|
27
|
+
const type = request.headers.get("content-type") ?? "";
|
|
28
|
+
const reader = request.body.getReader();
|
|
29
|
+
const chunks = [];
|
|
30
|
+
let size = 0;
|
|
31
|
+
for (; ; ) {
|
|
32
|
+
const { done, value } = await reader.read();
|
|
33
|
+
if (done) break;
|
|
34
|
+
size += value.byteLength;
|
|
35
|
+
if (size > limit) {
|
|
36
|
+
await reader.cancel();
|
|
37
|
+
throw tooLarge(limit);
|
|
38
|
+
}
|
|
39
|
+
chunks.push(value);
|
|
40
|
+
}
|
|
41
|
+
if (size === 0) return void 0;
|
|
42
|
+
if (!isJson(type)) {
|
|
43
|
+
throw new ApiError(415, "unsupported_media_type", "Send the body as application/json.");
|
|
44
|
+
}
|
|
45
|
+
const bytes = new Uint8Array(size);
|
|
46
|
+
let offset = 0;
|
|
47
|
+
for (const chunk of chunks) {
|
|
48
|
+
bytes.set(chunk, offset);
|
|
49
|
+
offset += chunk.byteLength;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
53
|
+
} catch {
|
|
54
|
+
throw new ApiError(400, "invalid_json", "The body is not valid JSON.");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function isJson(type) {
|
|
58
|
+
const mime = type.split(";")[0].trim().toLowerCase();
|
|
59
|
+
return mime === "application/json" || mime.endsWith("+json");
|
|
60
|
+
}
|
|
61
|
+
function tooLarge(limit) {
|
|
62
|
+
return new ApiError(413, "payload_too_large", `The body is larger than ${limit} bytes.`);
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
DEFAULT_BODY_LIMIT,
|
|
66
|
+
readJson,
|
|
67
|
+
readQuery
|
|
68
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/middleware.ts
|
|
21
|
+
var middleware_exports = {};
|
|
22
|
+
__export(middleware_exports, {
|
|
23
|
+
appendVary: () => appendVary,
|
|
24
|
+
withHeaders: () => withHeaders
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(middleware_exports);
|
|
27
|
+
function withHeaders(response, set) {
|
|
28
|
+
try {
|
|
29
|
+
set(response.headers);
|
|
30
|
+
return response;
|
|
31
|
+
} catch {
|
|
32
|
+
const copy = new Response(response.body, response);
|
|
33
|
+
set(copy.headers);
|
|
34
|
+
return copy;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function appendVary(headers, name) {
|
|
38
|
+
const vary = headers.get("vary");
|
|
39
|
+
if (!vary) headers.set("vary", name);
|
|
40
|
+
else if (!vary.split(",").some((v) => v.trim().toLowerCase() === name.toLowerCase())) headers.set("vary", `${vary}, ${name}`);
|
|
41
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape of `@fluixi/start` middleware, restated so this package does not import start:
|
|
3
|
+
* `(request, next)` to a `Response`, where `next()` runs the rest of the chain.
|
|
4
|
+
*/
|
|
5
|
+
export type Middleware = (request: Request, next: () => Promise<Response>) => Response | Promise<Response>;
|
|
6
|
+
/** Headers set on a response, cloning it first when its headers are immutable (a fetched one). */
|
|
7
|
+
export declare function withHeaders(response: Response, set: (headers: Headers) => void): Response;
|
|
8
|
+
export declare function appendVary(headers: Headers, name: string): void;
|
|
9
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,QAAQ,CAAC,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAE3G,kGAAkG;AAClG,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,QAAQ,CASzF;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAI/D"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Headers set on a response, cloning it first when its headers are immutable (a fetched one). */
|
|
2
|
+
export function withHeaders(response, set) {
|
|
3
|
+
try {
|
|
4
|
+
set(response.headers);
|
|
5
|
+
return response;
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
const copy = new Response(response.body, response);
|
|
9
|
+
set(copy.headers);
|
|
10
|
+
return copy;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function appendVary(headers, name) {
|
|
14
|
+
const vary = headers.get('vary');
|
|
15
|
+
if (!vary)
|
|
16
|
+
headers.set('vary', name);
|
|
17
|
+
else if (!vary.split(',').some((v) => v.trim().toLowerCase() === name.toLowerCase()))
|
|
18
|
+
headers.set('vary', `${vary}, ${name}`);
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/middleware.ts
|
|
2
|
+
function withHeaders(response, set) {
|
|
3
|
+
try {
|
|
4
|
+
set(response.headers);
|
|
5
|
+
return response;
|
|
6
|
+
} catch {
|
|
7
|
+
const copy = new Response(response.body, response);
|
|
8
|
+
set(copy.headers);
|
|
9
|
+
return copy;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function appendVary(headers, name) {
|
|
13
|
+
const vary = headers.get("vary");
|
|
14
|
+
if (!vary) headers.set("vary", name);
|
|
15
|
+
else if (!vary.split(",").some((v) => v.trim().toLowerCase() === name.toLowerCase())) headers.set("vary", `${vary}, ${name}`);
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
appendVary,
|
|
19
|
+
withHeaders
|
|
20
|
+
};
|
package/dist/openapi.cjs
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/openapi.ts
|
|
21
|
+
var openapi_exports = {};
|
|
22
|
+
__export(openapi_exports, {
|
|
23
|
+
createOpenApiDocument: () => createOpenApiDocument,
|
|
24
|
+
describedBy: () => describedBy,
|
|
25
|
+
includeDocument: () => includeDocument,
|
|
26
|
+
openApiRoute: () => openApiRoute,
|
|
27
|
+
setJsonSchemaConverter: () => setJsonSchemaConverter
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(openapi_exports);
|
|
30
|
+
|
|
31
|
+
// src/input.ts
|
|
32
|
+
var DEFAULT_BODY_LIMIT = 1024 * 1024;
|
|
33
|
+
|
|
34
|
+
// src/route.ts
|
|
35
|
+
var CONTRACT = /* @__PURE__ */ Symbol.for("fluixi.api.contract");
|
|
36
|
+
function contractOf(handler) {
|
|
37
|
+
return typeof handler === "function" ? handler[CONTRACT] : void 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/openapi.ts
|
|
41
|
+
var convert;
|
|
42
|
+
function setJsonSchemaConverter(converter) {
|
|
43
|
+
convert = converter;
|
|
44
|
+
}
|
|
45
|
+
var METHODS = ["get", "post", "put", "patch", "delete"];
|
|
46
|
+
var DESCRIBES = /* @__PURE__ */ Symbol.for("fluixi.api.describes");
|
|
47
|
+
var DESCRIBED_BY = /* @__PURE__ */ Symbol.for("fluixi.api.described-by");
|
|
48
|
+
function describedBy(document, handler) {
|
|
49
|
+
Object.defineProperty(handler, DESCRIBED_BY, { value: document });
|
|
50
|
+
return handler;
|
|
51
|
+
}
|
|
52
|
+
var ERROR_SCHEMA = {
|
|
53
|
+
type: "object",
|
|
54
|
+
required: ["error"],
|
|
55
|
+
properties: {
|
|
56
|
+
error: {
|
|
57
|
+
type: "object",
|
|
58
|
+
required: ["code", "message"],
|
|
59
|
+
properties: {
|
|
60
|
+
code: { type: "string", description: "Machine-readable, stable: `validation_failed`, `not_found`, `internal`." },
|
|
61
|
+
message: { type: "string", description: "For people. May change between versions." },
|
|
62
|
+
details: { description: "For `validation_failed`, the list of `{ path, message }` issues." }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
function createOpenApiDocument(routes, info) {
|
|
68
|
+
const paths = {};
|
|
69
|
+
const securitySchemes = {};
|
|
70
|
+
for (const route of routes) {
|
|
71
|
+
if (!route.path) continue;
|
|
72
|
+
for (const method of METHODS) {
|
|
73
|
+
const handler = route.mod[method.toUpperCase()];
|
|
74
|
+
if (typeof handler !== "function") continue;
|
|
75
|
+
const marks = handler;
|
|
76
|
+
if (marks[DESCRIBES] || marks[DESCRIBED_BY]) continue;
|
|
77
|
+
(paths[route.path] ??= {})[method] = operation(method, route.path, contractOf(handler), securitySchemes);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const { servers, ...rest } = info;
|
|
81
|
+
return {
|
|
82
|
+
openapi: "3.1.0",
|
|
83
|
+
info: rest,
|
|
84
|
+
...servers?.length ? { servers } : {},
|
|
85
|
+
paths,
|
|
86
|
+
components: { schemas: { Error: ERROR_SCHEMA }, ...Object.keys(securitySchemes).length ? { securitySchemes } : {} }
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function openApiRoute(routes, info) {
|
|
90
|
+
let cached;
|
|
91
|
+
const handler = async () => {
|
|
92
|
+
cached ??= describe(routes(), info).catch((error) => {
|
|
93
|
+
cached = void 0;
|
|
94
|
+
throw error;
|
|
95
|
+
});
|
|
96
|
+
return new Response(await cached, { headers: { "content-type": "application/json; charset=utf-8" } });
|
|
97
|
+
};
|
|
98
|
+
Object.defineProperty(handler, DESCRIBES, { value: true });
|
|
99
|
+
return handler;
|
|
100
|
+
}
|
|
101
|
+
async function describe(routes, info) {
|
|
102
|
+
const doc = createOpenApiDocument(routes, info);
|
|
103
|
+
const mounts = /* @__PURE__ */ new Map();
|
|
104
|
+
for (const route of routes) {
|
|
105
|
+
if (!route.path) continue;
|
|
106
|
+
for (const handler of Object.values(route.mod)) {
|
|
107
|
+
const source = typeof handler === "function" ? handler[DESCRIBED_BY] : void 0;
|
|
108
|
+
if (source && !mounts.has(source)) mounts.set(source, route.path.replace(/\/\{[^}]+\}$/, ""));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const [source, mount] of mounts) includeDocument(doc, await source(), mount);
|
|
112
|
+
return JSON.stringify(doc, null, 2);
|
|
113
|
+
}
|
|
114
|
+
function includeDocument(target, source, mount) {
|
|
115
|
+
const prefix = pascal(mount.split("/").filter(Boolean).pop() ?? "included");
|
|
116
|
+
const components = target.components ??= {};
|
|
117
|
+
const prefixed = (name) => prefix + name[0].toUpperCase() + name.slice(1);
|
|
118
|
+
const renames = /* @__PURE__ */ new Map();
|
|
119
|
+
for (const [section, entries] of Object.entries(source.components ?? {})) {
|
|
120
|
+
for (const [name, value] of Object.entries(entries)) {
|
|
121
|
+
const existing = components[section]?.[name];
|
|
122
|
+
if (existing !== void 0 && JSON.stringify(existing) !== JSON.stringify(value)) {
|
|
123
|
+
renames.set(`#/components/${section}/${name}`, `#/components/${section}/${prefixed(name)}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const rewrite = (node) => {
|
|
128
|
+
if (Array.isArray(node)) return node.map(rewrite);
|
|
129
|
+
if (!node || typeof node !== "object") return node;
|
|
130
|
+
const out = Object.fromEntries(
|
|
131
|
+
Object.entries(node).map(([key, value]) => [key, key === "$ref" && typeof value === "string" ? renames.get(value) ?? value : rewrite(value)])
|
|
132
|
+
);
|
|
133
|
+
return "nullable" in out ? nullable(out) : out;
|
|
134
|
+
};
|
|
135
|
+
for (const [section, entries] of Object.entries(source.components ?? {})) {
|
|
136
|
+
const into = components[section] ??= {};
|
|
137
|
+
for (const [name, value] of Object.entries(entries)) {
|
|
138
|
+
const renamed = renames.has(`#/components/${section}/${name}`) ? prefixed(name) : name;
|
|
139
|
+
into[renamed] = rewrite(value);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const paths = target.paths ??= {};
|
|
143
|
+
const ids = new Set(Object.values(paths).flatMap((item) => Object.values(item).map((op) => op?.operationId)));
|
|
144
|
+
for (const [path, item] of Object.entries(source.paths ?? {})) {
|
|
145
|
+
const described = rewrite(item);
|
|
146
|
+
for (const [method, op] of Object.entries(described)) {
|
|
147
|
+
if (Array.isArray(op.security)) {
|
|
148
|
+
op.security = op.security.map(
|
|
149
|
+
(requirement) => Object.fromEntries(
|
|
150
|
+
Object.entries(requirement).map(([name, scopes]) => [
|
|
151
|
+
renames.has(`#/components/securitySchemes/${name}`) ? prefixed(name) : name,
|
|
152
|
+
scopes
|
|
153
|
+
])
|
|
154
|
+
)
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (METHODS.includes(method) && !op.summary) op.summary = summaryOf(op, method, mount + path);
|
|
158
|
+
if (typeof op.operationId === "string" && ids.has(op.operationId)) {
|
|
159
|
+
op.operationId = prefix[0].toLowerCase() + prefix.slice(1) + op.operationId[0].toUpperCase() + op.operationId.slice(1);
|
|
160
|
+
}
|
|
161
|
+
if (typeof op.operationId === "string") ids.add(op.operationId);
|
|
162
|
+
}
|
|
163
|
+
paths[mount + path] = described;
|
|
164
|
+
}
|
|
165
|
+
const tags = source.tags ?? [];
|
|
166
|
+
if (tags.length) {
|
|
167
|
+
const known = target.tags ??= [];
|
|
168
|
+
for (const tag of tags) if (!known.some((t) => t.name === tag.name)) known.push(tag);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function nullable(schema) {
|
|
172
|
+
const { nullable: isNullable, ...rest } = schema;
|
|
173
|
+
if (isNullable !== true) return rest;
|
|
174
|
+
if (typeof rest.type === "string") return { ...rest, type: [rest.type, "null"] };
|
|
175
|
+
if (Array.isArray(rest.type)) return rest.type.includes("null") ? rest : { ...rest, type: [...rest.type, "null"] };
|
|
176
|
+
return { anyOf: [rest, { type: "null" }] };
|
|
177
|
+
}
|
|
178
|
+
function summaryOf(op, method, path) {
|
|
179
|
+
const description = typeof op.description === "string" ? op.description.split(/\.\s|\n/)[0].trim() : "";
|
|
180
|
+
if (description && description.length <= 60) return description.replace(/\.$/, "");
|
|
181
|
+
return typeof op.operationId === "string" ? op.operationId.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/^./, (c) => c.toUpperCase()) : summaryFor(method, path);
|
|
182
|
+
}
|
|
183
|
+
function pascal(word) {
|
|
184
|
+
return word.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
|
|
185
|
+
}
|
|
186
|
+
function operation(method, path, contract, schemes) {
|
|
187
|
+
const operationId = contract?.operationId ?? idFor(method, path);
|
|
188
|
+
if (!contract) {
|
|
189
|
+
return {
|
|
190
|
+
operationId,
|
|
191
|
+
tags: [tagFor(path)],
|
|
192
|
+
summary: `${method.toUpperCase()} ${path}`,
|
|
193
|
+
description: "This route declares no contract, so nothing about its input or output is known.",
|
|
194
|
+
"x-fluixi-contract": false,
|
|
195
|
+
responses: { default: { description: "Undocumented." } }
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const parameters = [
|
|
199
|
+
...parametersFrom(contract.params, "path", pathNames(path)),
|
|
200
|
+
...parametersFrom(contract.query, "query", [])
|
|
201
|
+
];
|
|
202
|
+
const success = contract.status ?? (contract.response ? 200 : 204);
|
|
203
|
+
const responses = {
|
|
204
|
+
[success]: contract.response ? { description: "Success.", content: { "application/json": { schema: schemaFor(contract.response, "output") } } } : { description: "Success, no content." }
|
|
205
|
+
};
|
|
206
|
+
const error = (description) => ({ description, content: { "application/json": { schema: { $ref: "#/components/schemas/Error" } } } });
|
|
207
|
+
if (contract.params || contract.query || contract.body) responses[400] = error("The request is not valid: `validation_failed`, `invalid_json`.");
|
|
208
|
+
if (contract.body) {
|
|
209
|
+
responses[413] = error("The body is over the limit.");
|
|
210
|
+
responses[415] = error("The body is not JSON.");
|
|
211
|
+
}
|
|
212
|
+
const guard = contract.guard;
|
|
213
|
+
if (guard) {
|
|
214
|
+
const refuses = guard.refuses ?? [401];
|
|
215
|
+
if (refuses.includes(401)) responses[401] = error("Not signed in: `unauthorized`.");
|
|
216
|
+
if (refuses.includes(403)) responses[403] = error("Signed in, not allowed: `forbidden`, or a more precise code.");
|
|
217
|
+
if (refuses.includes(503)) responses[503] = error("A service the check depends on cannot answer now; try again.");
|
|
218
|
+
}
|
|
219
|
+
if (pathNames(path).length) responses[404] = error("Nothing at this path.");
|
|
220
|
+
responses[500] = error("A fault on the server. Its detail is logged, never sent.");
|
|
221
|
+
return {
|
|
222
|
+
operationId,
|
|
223
|
+
tags: contract.tags?.length ? contract.tags : [tagFor(path)],
|
|
224
|
+
// Tools name the request after it (Postman does), so there is always one.
|
|
225
|
+
summary: contract.summary ?? summaryFor(method, path),
|
|
226
|
+
...contract.description ? { description: contract.description } : {},
|
|
227
|
+
...contract.deprecated ? { deprecated: true } : {},
|
|
228
|
+
// Stated on every contracted route, so a public one reads as public rather than unknown.
|
|
229
|
+
security: security(guard, schemes),
|
|
230
|
+
...parameters.length ? { parameters } : {},
|
|
231
|
+
...contract.body ? { requestBody: { required: true, content: { "application/json": { schema: schemaFor(contract.body, "input") } } } } : {},
|
|
232
|
+
responses
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function security(guard, schemes) {
|
|
236
|
+
if (!guard?.security) return [];
|
|
237
|
+
const { name, scheme, scopes } = guard.security;
|
|
238
|
+
schemes[name] = scheme;
|
|
239
|
+
return [{ [name]: scopes ?? [] }];
|
|
240
|
+
}
|
|
241
|
+
function parametersFrom(schema, where, names) {
|
|
242
|
+
if (!schema) return where === "path" ? names.map((name) => ({ name, in: "path", required: true, schema: { type: "string" } })) : [];
|
|
243
|
+
const json = schemaFor(schema, "input");
|
|
244
|
+
const properties = json.properties ?? {};
|
|
245
|
+
const required = new Set(json.required ?? []);
|
|
246
|
+
return Object.entries(properties).map(([name, property]) => ({
|
|
247
|
+
name,
|
|
248
|
+
in: where,
|
|
249
|
+
required: where === "path" ? true : required.has(name),
|
|
250
|
+
...typeof property.description === "string" ? { description: property.description } : {},
|
|
251
|
+
schema: property,
|
|
252
|
+
...where === "query" && property.type === "array" ? { style: "form", explode: true } : {}
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
function schemaFor(schema, io) {
|
|
256
|
+
const standard = schema["~standard"].jsonSchema;
|
|
257
|
+
try {
|
|
258
|
+
const json = standard?.[io]({ target: "draft-2020-12", libraryOptions: { unrepresentable: "any" } }) ?? convert?.(schema, io);
|
|
259
|
+
if (json) {
|
|
260
|
+
const { $schema: _dialect, ...rest } = json;
|
|
261
|
+
return rest;
|
|
262
|
+
}
|
|
263
|
+
} catch {
|
|
264
|
+
}
|
|
265
|
+
return { "x-fluixi-unconverted": schema["~standard"].vendor };
|
|
266
|
+
}
|
|
267
|
+
function pathNames(path) {
|
|
268
|
+
return [...path.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]);
|
|
269
|
+
}
|
|
270
|
+
function tagFor(path) {
|
|
271
|
+
const segments = path.split("/").filter((s) => s && !s.startsWith("{"));
|
|
272
|
+
const meaningful = segments.filter((s, i) => !(i === 0 && s === "api") && !/^v\d+$/.test(s));
|
|
273
|
+
return meaningful[0] ?? "default";
|
|
274
|
+
}
|
|
275
|
+
function summaryFor(method, path) {
|
|
276
|
+
const words = idFor(method, path).replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
|
|
277
|
+
return words[0].toUpperCase() + words.slice(1);
|
|
278
|
+
}
|
|
279
|
+
function idFor(method, path) {
|
|
280
|
+
const words = path.split("/").filter((s) => s && s !== "api" && !/^v\d+$/.test(s)).map((s) => s.startsWith("{") ? `By-${s.slice(1, -1)}` : s).flatMap((s) => s.split(/[^a-zA-Z0-9]+/)).filter(Boolean);
|
|
281
|
+
return method + words.map((w) => w[0].toUpperCase() + w.slice(1)).join("");
|
|
282
|
+
}
|