@wolfstar/plugin-api 0.0.0-next-20260816113531
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 +202 -0
- package/README.md +114 -0
- package/dist/esm/index-D-l34LxR.d.ts +314 -0
- package/dist/esm/index-D-l34LxR.d.ts.map +1 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/register.d.ts +20 -0
- package/dist/esm/register.d.ts.map +1 -0
- package/dist/esm/register.js +32 -0
- package/dist/esm/register.js.map +1 -0
- package/dist/esm/src-CscMIChC.js +664 -0
- package/dist/esm/src-CscMIChC.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { n as loadListeners, r as ApiServer, t as loadMiddlewares } from "./src-CscMIChC.js";
|
|
2
|
+
import { Client, Plugin, container, postInitialization, postListen } from "@wolfstar/http-framework";
|
|
3
|
+
|
|
4
|
+
//#region src/register.ts
|
|
5
|
+
/**
|
|
6
|
+
* Registers a standalone {@link ApiServer} for auxiliary REST routes (health checks, dashboards,
|
|
7
|
+
* webhooks from other services, etc), independent from the Discord interactions webhook server.
|
|
8
|
+
*
|
|
9
|
+
* Activate by importing the side-effecting entrypoint before creating the client:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import '@wolfstar/plugin-api/register';
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
var ApiPlugin = class extends Plugin {
|
|
16
|
+
static [postInitialization](options) {
|
|
17
|
+
const server = new ApiServer(options.api);
|
|
18
|
+
container.stores.register(server.routes).register(server.middlewares);
|
|
19
|
+
loadListeners().catch((error) => console.error("[plugin-api] Failed to load listeners:", error));
|
|
20
|
+
loadMiddlewares().catch((error) => console.error("[plugin-api] Failed to load middlewares:", error));
|
|
21
|
+
}
|
|
22
|
+
static async [postListen](options) {
|
|
23
|
+
if ((options.api?.automaticallyConnect ?? true) === false) return;
|
|
24
|
+
await container.server.connect();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
Client.plugins.registerPostInitializationHook(ApiPlugin[postInitialization], "WolfStar-Api-PostInitialization");
|
|
28
|
+
Client.plugins.registerPostListenHook(ApiPlugin[postListen], "WolfStar-Api-PostListen");
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { ApiPlugin };
|
|
32
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.js","names":[],"sources":["../../src/register.ts"],"sourcesContent":["import {\n Client,\n container,\n Plugin,\n postInitialization,\n postListen,\n type ClientOptions,\n} from \"@wolfstar/http-framework\";\nimport \"./index\";\nimport { ApiServer } from \"./lib/http/ApiServer\";\nimport { loadListeners } from \"./listeners/_load\";\nimport { loadMiddlewares } from \"./middlewares/_load\";\n\n/**\n * Registers a standalone {@link ApiServer} for auxiliary REST routes (health checks, dashboards,\n * webhooks from other services, etc), independent from the Discord interactions webhook server.\n *\n * Activate by importing the side-effecting entrypoint before creating the client:\n *\n * ```ts\n * import '@wolfstar/plugin-api/register';\n * ```\n */\nexport class ApiPlugin extends Plugin {\n public static [postInitialization](this: Client, options: ClientOptions): void {\n const server = new ApiServer(options.api);\n\n container.stores //\n .register(server.routes)\n .register(server.middlewares);\n\n loadListeners().catch((error: unknown) =>\n console.error(\"[plugin-api] Failed to load listeners:\", error),\n );\n loadMiddlewares().catch((error: unknown) =>\n console.error(\"[plugin-api] Failed to load middlewares:\", error),\n );\n }\n\n public static async [postListen](this: Client, options: ClientOptions): Promise<void> {\n if ((options.api?.automaticallyConnect ?? true) === false) return;\n await container.server.connect();\n }\n}\n\nClient.plugins.registerPostInitializationHook(\n ApiPlugin[postInitialization],\n \"WolfStar-Api-PostInitialization\",\n);\nClient.plugins.registerPostListenHook(ApiPlugin[postListen], \"WolfStar-Api-PostListen\");\n"],"mappings":";;;;;;;;;;;;;;AAuBA,IAAa,YAAb,cAA+B,OAAO;CACpC,QAAe,oBAAkC,SAA8B;EAC7E,MAAM,SAAS,IAAI,UAAU,QAAQ,GAAG;EAExC,UAAU,OACP,SAAS,OAAO,MAAM,CAAC,CACvB,SAAS,OAAO,WAAW;EAE9B,cAAc,CAAC,CAAC,OAAO,UACrB,QAAQ,MAAM,0CAA0C,KAAK,CAC/D;EACA,gBAAgB,CAAC,CAAC,OAAO,UACvB,QAAQ,MAAM,4CAA4C,KAAK,CACjE;CACF;CAEA,cAAqB,YAA0B,SAAuC;EACpF,KAAK,QAAQ,KAAK,wBAAwB,UAAU,OAAO;EAC3D,MAAM,UAAU,OAAO,QAAQ;CACjC;AACF;AAEA,OAAO,QAAQ,+BACb,UAAU,qBACV,iCACF;AACA,OAAO,QAAQ,uBAAuB,UAAU,aAAa,yBAAyB"}
|
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse, createServer } from "node:http";
|
|
2
|
+
import { HttpCodes, Listener, container } from "@wolfstar/http-framework";
|
|
3
|
+
import { AsyncEventEmitter } from "@vladfrangu/async_event_emitter";
|
|
4
|
+
import { LoaderStrategy, Piece, Store } from "@sapphire/pieces";
|
|
5
|
+
|
|
6
|
+
//#region src/lib/http/ApiRequest.ts
|
|
7
|
+
var ApiRequest = class extends IncomingMessage {
|
|
8
|
+
/**
|
|
9
|
+
* The parsed query string parameters.
|
|
10
|
+
*/
|
|
11
|
+
query = new URLSearchParams();
|
|
12
|
+
/**
|
|
13
|
+
* The extracted dynamic (`[param]`) path segment values.
|
|
14
|
+
*/
|
|
15
|
+
params = {};
|
|
16
|
+
/**
|
|
17
|
+
* The matched router branch for this request's pathname, if any.
|
|
18
|
+
*/
|
|
19
|
+
routerNode = null;
|
|
20
|
+
/**
|
|
21
|
+
* The matched route for this request's pathname and method, if any.
|
|
22
|
+
*/
|
|
23
|
+
route = null;
|
|
24
|
+
/**
|
|
25
|
+
* Reads the full request body as a UTF-8 string.
|
|
26
|
+
* @param limit The maximum number of bytes to read before rejecting. Defaults to 1 MiB.
|
|
27
|
+
*/
|
|
28
|
+
async readBodyText(limit = 1024 * 1024) {
|
|
29
|
+
const chunks = [];
|
|
30
|
+
let size = 0;
|
|
31
|
+
for await (const chunk of this) {
|
|
32
|
+
size += chunk.length;
|
|
33
|
+
if (size > limit) throw new RangeError(`Request body exceeded the ${limit} byte limit`);
|
|
34
|
+
chunks.push(chunk);
|
|
35
|
+
}
|
|
36
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Reads and parses the request body as JSON.
|
|
40
|
+
* @param limit The maximum number of bytes to read before rejecting. Defaults to 1 MiB.
|
|
41
|
+
*/
|
|
42
|
+
async readBodyJson(limit) {
|
|
43
|
+
const text = await this.readBodyText(limit);
|
|
44
|
+
return JSON.parse(text);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/lib/http/ApiResponse.ts
|
|
50
|
+
var ApiResponse = class extends ServerResponse {
|
|
51
|
+
status(statusCode) {
|
|
52
|
+
this.statusCode = statusCode;
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
json(body, statusCode = HttpCodes.OK) {
|
|
56
|
+
this.statusCode = statusCode;
|
|
57
|
+
if (!this.hasHeader("Content-Type")) this.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
58
|
+
this.end(JSON.stringify(body));
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
text(body, statusCode = HttpCodes.OK) {
|
|
62
|
+
this.statusCode = statusCode;
|
|
63
|
+
if (!this.hasHeader("Content-Type")) this.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
64
|
+
this.end(body);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
noContent() {
|
|
68
|
+
this.statusCode = HttpCodes.NoContent;
|
|
69
|
+
this.end();
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
notFound(body = { error: "Not Found" }) {
|
|
73
|
+
return this.json(body, HttpCodes.NotFound);
|
|
74
|
+
}
|
|
75
|
+
methodNotAllowed(body = { error: "Method Not Allowed" }) {
|
|
76
|
+
return this.json(body, HttpCodes.MethodNotAllowed);
|
|
77
|
+
}
|
|
78
|
+
badRequest(body = { error: "Bad Request" }) {
|
|
79
|
+
return this.json(body, HttpCodes.BadRequest);
|
|
80
|
+
}
|
|
81
|
+
error(statusCode = HttpCodes.InternalServerError, body = { error: "Internal Server Error" }) {
|
|
82
|
+
return this.json(body, statusCode);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/lib/structures/Middleware.ts
|
|
88
|
+
/**
|
|
89
|
+
* A piece that runs on every incoming request, in ascending {@link Middleware.position} order,
|
|
90
|
+
* before route dispatch. A middleware stops the chain implicitly by ending the response
|
|
91
|
+
* (`response.writableEnded`); there is no explicit `next()` callback.
|
|
92
|
+
*/
|
|
93
|
+
var Middleware = class extends Piece {
|
|
94
|
+
/**
|
|
95
|
+
* The built-in middlewares use the following positions:
|
|
96
|
+
* - `headers`: 10 (CORS headers, 404/405 short-circuit)
|
|
97
|
+
* - `body`: 20 (`Content-Length` validation)
|
|
98
|
+
*/
|
|
99
|
+
position;
|
|
100
|
+
constructor(context, options) {
|
|
101
|
+
super(context, options);
|
|
102
|
+
this.position = options.position ?? 1e3;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/lib/structures/MiddlewareStore.ts
|
|
108
|
+
var MiddlewareStore = class extends Store {
|
|
109
|
+
/**
|
|
110
|
+
* Enabled middlewares, kept sorted in ascending {@link Middleware.position} order.
|
|
111
|
+
*/
|
|
112
|
+
sortedMiddlewares = [];
|
|
113
|
+
constructor() {
|
|
114
|
+
super(Middleware, { name: "middlewares" });
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Runs every enabled middleware in order, stopping early once the response has ended.
|
|
118
|
+
*/
|
|
119
|
+
async run(request, response) {
|
|
120
|
+
for (const middleware of this.sortedMiddlewares) {
|
|
121
|
+
if (response.writableEnded) return;
|
|
122
|
+
if (middleware.enabled) await middleware.run(request, response);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
set(key, value) {
|
|
126
|
+
const index = this.sortedMiddlewares.findIndex((middleware) => middleware.position >= value.position);
|
|
127
|
+
if (index === -1) this.sortedMiddlewares.push(value);
|
|
128
|
+
else this.sortedMiddlewares.splice(index, 0, value);
|
|
129
|
+
return super.set(key, value);
|
|
130
|
+
}
|
|
131
|
+
delete(key) {
|
|
132
|
+
const index = this.sortedMiddlewares.findIndex((middleware) => middleware.name === key);
|
|
133
|
+
if (index !== -1) this.sortedMiddlewares.splice(index, 1);
|
|
134
|
+
return super.delete(key);
|
|
135
|
+
}
|
|
136
|
+
clear() {
|
|
137
|
+
this.sortedMiddlewares.length = 0;
|
|
138
|
+
super.clear();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/lib/http/HttpMethod.ts
|
|
144
|
+
/**
|
|
145
|
+
* The HTTP methods that a {@link Route} can be registered for.
|
|
146
|
+
*/
|
|
147
|
+
const HttpMethods = [
|
|
148
|
+
"DELETE",
|
|
149
|
+
"GET",
|
|
150
|
+
"HEAD",
|
|
151
|
+
"OPTIONS",
|
|
152
|
+
"PATCH",
|
|
153
|
+
"POST",
|
|
154
|
+
"PUT"
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/lib/structures/router/RouterNode.ts
|
|
159
|
+
/**
|
|
160
|
+
* A single path segment's registered routes, keyed by HTTP method.
|
|
161
|
+
*/
|
|
162
|
+
var RouterNode = class extends Map {
|
|
163
|
+
branch;
|
|
164
|
+
constructor(branch) {
|
|
165
|
+
super();
|
|
166
|
+
this.branch = branch;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Walks up the branch chain collecting the values captured by dynamic (`[param]`) segments.
|
|
170
|
+
* @param parts The path segments of the matched request, in the same order used to reach this node.
|
|
171
|
+
*/
|
|
172
|
+
extractParameters(parts) {
|
|
173
|
+
const params = {};
|
|
174
|
+
let branch = this.branch;
|
|
175
|
+
let index = parts.length - 1;
|
|
176
|
+
while (branch && branch.parent) {
|
|
177
|
+
if (branch.dynamic) params[branch.name] = decodeURIComponent(parts[index]);
|
|
178
|
+
branch = branch.parent;
|
|
179
|
+
index -= 1;
|
|
180
|
+
}
|
|
181
|
+
return params;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/lib/structures/router/RouterBranch.ts
|
|
187
|
+
/**
|
|
188
|
+
* A single segment of the route trie. Each branch owns exactly one {@link RouterNode} (its
|
|
189
|
+
* method-to-route map) and may have any number of static children plus at most one dynamic
|
|
190
|
+
* (`[param]`) child, which is only checked after every static child fails to match.
|
|
191
|
+
*/
|
|
192
|
+
var RouterBranch = class RouterBranch {
|
|
193
|
+
name;
|
|
194
|
+
dynamic;
|
|
195
|
+
parent;
|
|
196
|
+
node;
|
|
197
|
+
staticChildren = [];
|
|
198
|
+
dynamicChild = null;
|
|
199
|
+
constructor(name, dynamic, parent) {
|
|
200
|
+
this.name = name;
|
|
201
|
+
this.dynamic = dynamic;
|
|
202
|
+
this.parent = parent;
|
|
203
|
+
this.node = new RouterNode(this);
|
|
204
|
+
}
|
|
205
|
+
matches(part) {
|
|
206
|
+
return this.dynamic || this.name === part;
|
|
207
|
+
}
|
|
208
|
+
insertAt(parts, index, route) {
|
|
209
|
+
if (index >= parts.length) {
|
|
210
|
+
for (const method of route.methods) this.node.set(method, route);
|
|
211
|
+
return this.node;
|
|
212
|
+
}
|
|
213
|
+
const part = parts[index];
|
|
214
|
+
if (part.startsWith("[") && part.endsWith("]")) {
|
|
215
|
+
if (this.dynamicChild) return this.dynamicChild.insertAt(parts, index + 1, route);
|
|
216
|
+
const branch = new RouterBranch(part.slice(1, -1), true, this);
|
|
217
|
+
this.dynamicChild = branch;
|
|
218
|
+
return branch.insertAt(parts, index + 1, route);
|
|
219
|
+
}
|
|
220
|
+
const staticChild = this.staticChildren.find((branch) => branch.name === part);
|
|
221
|
+
if (staticChild) return staticChild.insertAt(parts, index + 1, route);
|
|
222
|
+
const branch = new RouterBranch(part, false, this);
|
|
223
|
+
this.staticChildren.push(branch);
|
|
224
|
+
return branch.insertAt(parts, index + 1, route);
|
|
225
|
+
}
|
|
226
|
+
removeAt(parts, index, route) {
|
|
227
|
+
if (index >= parts.length) {
|
|
228
|
+
let removed = false;
|
|
229
|
+
for (const method of route.methods) if (this.node.delete(method)) removed = true;
|
|
230
|
+
return removed;
|
|
231
|
+
}
|
|
232
|
+
const part = parts[index];
|
|
233
|
+
const child = this.staticChildren.find((branch) => branch.matches(part)) ?? this.dynamicChild;
|
|
234
|
+
return child ? child.removeAt(parts, index + 1, route) : false;
|
|
235
|
+
}
|
|
236
|
+
findAt(parts, index) {
|
|
237
|
+
if (index >= parts.length) return this;
|
|
238
|
+
const part = parts[index];
|
|
239
|
+
const child = this.staticChildren.find((branch) => branch.matches(part)) ?? this.dynamicChild;
|
|
240
|
+
return child ? child.findAt(parts, index + 1) : null;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/lib/structures/router/RouterRoot.ts
|
|
246
|
+
const ROOT_NAME = "::ROOT::";
|
|
247
|
+
/**
|
|
248
|
+
* The root of the route trie, owned by {@link RouteStore.router}. Provides the public
|
|
249
|
+
* add/remove/find API plus the static path-parsing helpers used by {@link Route}'s constructor.
|
|
250
|
+
*/
|
|
251
|
+
var RouterRoot = class extends RouterBranch {
|
|
252
|
+
constructor() {
|
|
253
|
+
super(ROOT_NAME, false, null);
|
|
254
|
+
}
|
|
255
|
+
add(route) {
|
|
256
|
+
return this.insertAt(route.path, 0, route);
|
|
257
|
+
}
|
|
258
|
+
remove(route) {
|
|
259
|
+
return this.removeAt(route.path, 0, route);
|
|
260
|
+
}
|
|
261
|
+
find(parts) {
|
|
262
|
+
return this.findAt(parts, 0);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Splits a `/`-delimited path into its non-empty segments.
|
|
266
|
+
*/
|
|
267
|
+
static normalize(path) {
|
|
268
|
+
if (!path) return [];
|
|
269
|
+
return path.split("/").filter((part) => part.length > 0);
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Builds a route path from a piece's directory structure and file name: `(group)`-style
|
|
273
|
+
* directories are skipped, and a file named `index` collapses into its parent directory.
|
|
274
|
+
*/
|
|
275
|
+
static makeRoutePathForPiece(directories, name) {
|
|
276
|
+
const parts = [];
|
|
277
|
+
for (const directory of directories) {
|
|
278
|
+
const trimmed = directory.trim();
|
|
279
|
+
if (trimmed.length === 0) continue;
|
|
280
|
+
if (trimmed.startsWith("(") && trimmed.endsWith(")")) continue;
|
|
281
|
+
parts.push(trimmed);
|
|
282
|
+
}
|
|
283
|
+
const trimmedName = name.trim();
|
|
284
|
+
if (trimmedName !== "index") parts.push(trimmedName);
|
|
285
|
+
return parts.join("/");
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Extracts a trailing `.<method>` suffix from a piece name, e.g. `hello.post` → `POST`.
|
|
289
|
+
*/
|
|
290
|
+
static extractMethod(name) {
|
|
291
|
+
const index = name.lastIndexOf(".");
|
|
292
|
+
if (index === -1) return null;
|
|
293
|
+
const method = name.slice(index + 1).toUpperCase();
|
|
294
|
+
return HttpMethods.includes(method) ? method : null;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/lib/structures/Route.ts
|
|
300
|
+
/**
|
|
301
|
+
* A single HTTP endpoint. The route's path and methods are either given explicitly via
|
|
302
|
+
* {@link Route.Options.route}/{@link Route.Options.methods}, or inferred from the piece's file
|
|
303
|
+
* system location: directories become path segments (`(group)`-style directories are skipped,
|
|
304
|
+
* `index` collapses into its parent), `[param]` segments become dynamic, and a `.<method>`
|
|
305
|
+
* filename suffix (e.g. `hello.post.ts`) implies that HTTP method.
|
|
306
|
+
*/
|
|
307
|
+
var Route = class extends Piece {
|
|
308
|
+
/**
|
|
309
|
+
* The normalized path segments this route is registered under, e.g. `['users', '[id]']`.
|
|
310
|
+
*/
|
|
311
|
+
path;
|
|
312
|
+
/**
|
|
313
|
+
* The HTTP methods this route responds to.
|
|
314
|
+
*/
|
|
315
|
+
methods;
|
|
316
|
+
constructor(context, options) {
|
|
317
|
+
super(context, options);
|
|
318
|
+
const methods = new Set(options.methods ?? []);
|
|
319
|
+
let path;
|
|
320
|
+
if (options.route) path = options.route;
|
|
321
|
+
else {
|
|
322
|
+
let name = context.name;
|
|
323
|
+
const implied = RouterRoot.extractMethod(name);
|
|
324
|
+
if (implied) {
|
|
325
|
+
name = name.slice(0, name.length - implied.length - 1);
|
|
326
|
+
methods.add(implied);
|
|
327
|
+
}
|
|
328
|
+
path = RouterRoot.makeRoutePathForPiece(this.location.directories, name);
|
|
329
|
+
}
|
|
330
|
+
if (methods.size === 0) methods.add("GET");
|
|
331
|
+
this.path = RouterRoot.normalize(path);
|
|
332
|
+
this.methods = methods;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/lib/structures/RouteLoaderStrategy.ts
|
|
338
|
+
/**
|
|
339
|
+
* Keeps {@link RouteStore.router} in sync as route pieces are loaded and unloaded.
|
|
340
|
+
*/
|
|
341
|
+
var RouteLoaderStrategy = class extends LoaderStrategy {
|
|
342
|
+
onLoad(store, piece) {
|
|
343
|
+
super.onLoad(store, piece);
|
|
344
|
+
store.router.add(piece);
|
|
345
|
+
}
|
|
346
|
+
onUnload(store, piece) {
|
|
347
|
+
super.onUnload(store, piece);
|
|
348
|
+
store.router.remove(piece);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
//#endregion
|
|
353
|
+
//#region src/lib/structures/RouteStore.ts
|
|
354
|
+
var RouteStore = class extends Store {
|
|
355
|
+
/**
|
|
356
|
+
* The trie used to match an incoming request's pathname to a registered {@link Route}.
|
|
357
|
+
*/
|
|
358
|
+
router = new RouterRoot();
|
|
359
|
+
constructor() {
|
|
360
|
+
super(Route, {
|
|
361
|
+
name: "routes",
|
|
362
|
+
strategy: new RouteLoaderStrategy()
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
//#endregion
|
|
368
|
+
//#region src/lib/http/ApiServer.ts
|
|
369
|
+
let ApiServerEvent = /* @__PURE__ */ function(ApiServerEvent) {
|
|
370
|
+
ApiServerEvent["Error"] = "error";
|
|
371
|
+
ApiServerEvent["Request"] = "request";
|
|
372
|
+
ApiServerEvent["RouterBranchNotFound"] = "routerBranchNotFound";
|
|
373
|
+
ApiServerEvent["RouterBranchMethodNotAllowed"] = "routerBranchMethodNotAllowed";
|
|
374
|
+
ApiServerEvent["RouterFound"] = "routerFound";
|
|
375
|
+
ApiServerEvent["RouteError"] = "routeError";
|
|
376
|
+
ApiServerEvent["MiddlewareError"] = "middlewareError";
|
|
377
|
+
return ApiServerEvent;
|
|
378
|
+
}({});
|
|
379
|
+
/**
|
|
380
|
+
* A standalone HTTP server for auxiliary REST routes (health checks, dashboards, webhooks from
|
|
381
|
+
* other services, etc). It is deliberately independent from {@link Client.server}, which is
|
|
382
|
+
* reserved for the Discord interactions webhook.
|
|
383
|
+
*/
|
|
384
|
+
var ApiServer = class extends AsyncEventEmitter {
|
|
385
|
+
routes;
|
|
386
|
+
middlewares;
|
|
387
|
+
server;
|
|
388
|
+
options;
|
|
389
|
+
constructor(options = {}) {
|
|
390
|
+
super();
|
|
391
|
+
container.server = this;
|
|
392
|
+
this.options = options;
|
|
393
|
+
const serverOptions = {
|
|
394
|
+
...options.server,
|
|
395
|
+
IncomingMessage: ApiRequest,
|
|
396
|
+
ServerResponse: ApiResponse
|
|
397
|
+
};
|
|
398
|
+
this.server = createServer(serverOptions);
|
|
399
|
+
this.routes = new RouteStore();
|
|
400
|
+
this.middlewares = new MiddlewareStore();
|
|
401
|
+
this.server.on("error", (error) => this.emit("error", error));
|
|
402
|
+
this.server.on("request", (request, response) => this.emit("request", request, response));
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Starts listening for requests.
|
|
406
|
+
*/
|
|
407
|
+
connect() {
|
|
408
|
+
return new Promise((resolve, reject) => {
|
|
409
|
+
const onError = (error) => {
|
|
410
|
+
this.server.off("listening", onListening);
|
|
411
|
+
reject(error);
|
|
412
|
+
};
|
|
413
|
+
const onListening = () => {
|
|
414
|
+
this.server.off("error", onError);
|
|
415
|
+
resolve();
|
|
416
|
+
};
|
|
417
|
+
this.server.once("error", onError);
|
|
418
|
+
this.server.once("listening", onListening);
|
|
419
|
+
this.server.listen({
|
|
420
|
+
port: 4e3,
|
|
421
|
+
...this.options.listenOptions
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Stops the server from accepting new connections.
|
|
427
|
+
*/
|
|
428
|
+
disconnect() {
|
|
429
|
+
return new Promise((resolve, reject) => {
|
|
430
|
+
this.server.close((error) => error ? reject(error) : resolve());
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
//#endregion
|
|
436
|
+
//#region src/listeners/PluginRouteError.ts
|
|
437
|
+
/**
|
|
438
|
+
* Logs and responds with a 500 when a route's `run` method throws.
|
|
439
|
+
*/
|
|
440
|
+
var PluginRouteErrorListener = class extends Listener {
|
|
441
|
+
constructor(context) {
|
|
442
|
+
super(context, {
|
|
443
|
+
emitter: "server",
|
|
444
|
+
event: "routeError"
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
run(error, _request, response) {
|
|
448
|
+
console.error("[plugin-api] Route error:", error);
|
|
449
|
+
if (!response.writableEnded) response.error();
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/listeners/PluginServerMiddlewareError.ts
|
|
455
|
+
/**
|
|
456
|
+
* Logs and responds with a 500 when a middleware throws.
|
|
457
|
+
*/
|
|
458
|
+
var PluginServerMiddlewareErrorListener = class extends Listener {
|
|
459
|
+
constructor(context) {
|
|
460
|
+
super(context, {
|
|
461
|
+
emitter: "server",
|
|
462
|
+
event: "middlewareError"
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
run(error, _request, response) {
|
|
466
|
+
console.error("[plugin-api] Middleware error:", error);
|
|
467
|
+
if (!response.writableEnded) response.error();
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
//#endregion
|
|
472
|
+
//#region src/listeners/PluginServerRequest.ts
|
|
473
|
+
/**
|
|
474
|
+
* Parses the request URL, matches it against the route trie, runs every middleware, then emits
|
|
475
|
+
* the outcome (`routerFound`, `routerBranchNotFound`, or `routerBranchMethodNotAllowed`) for the
|
|
476
|
+
* downstream listeners to handle.
|
|
477
|
+
*/
|
|
478
|
+
var PluginServerRequestListener = class extends Listener {
|
|
479
|
+
constructor(context) {
|
|
480
|
+
super(context, {
|
|
481
|
+
emitter: "server",
|
|
482
|
+
event: "request"
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
async run(request, response) {
|
|
486
|
+
const [pathname, querystring] = splitUrl(request.url);
|
|
487
|
+
request.query = new URLSearchParams(querystring);
|
|
488
|
+
const parts = RouterRoot.normalize(pathname);
|
|
489
|
+
const branch = container.server.routes.router.find(parts);
|
|
490
|
+
const node = branch?.node ?? null;
|
|
491
|
+
const route = node?.get(request.method ?? "GET") ?? null;
|
|
492
|
+
if (node) request.params = node.extractParameters(parts);
|
|
493
|
+
request.routerNode = branch;
|
|
494
|
+
request.route = route;
|
|
495
|
+
try {
|
|
496
|
+
await container.server.middlewares.run(request, response);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
container.server.emit("middlewareError", error, request, response);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (response.writableEnded) return;
|
|
502
|
+
if (branch === null) container.server.emit("routerBranchNotFound", request, response);
|
|
503
|
+
else if (route === null) container.server.emit("routerBranchMethodNotAllowed", request, response);
|
|
504
|
+
else container.server.emit("routerFound", request, response);
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
function splitUrl(url = "/") {
|
|
508
|
+
const index = url.indexOf("?");
|
|
509
|
+
return index === -1 ? [url, ""] : [url.slice(0, index), url.slice(index + 1)];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
//#endregion
|
|
513
|
+
//#region src/listeners/PluginServerRouterBranchMethodNotAllowed.ts
|
|
514
|
+
/**
|
|
515
|
+
* Responds with a 405 when the pathname matched but no route handles this HTTP method.
|
|
516
|
+
*/
|
|
517
|
+
var PluginServerRouterBranchMethodNotAllowedListener = class extends Listener {
|
|
518
|
+
constructor(context) {
|
|
519
|
+
super(context, {
|
|
520
|
+
emitter: "server",
|
|
521
|
+
event: "routerBranchMethodNotAllowed"
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
run(_request, response) {
|
|
525
|
+
if (!response.writableEnded) response.methodNotAllowed();
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/listeners/PluginServerRouterBranchNotFound.ts
|
|
531
|
+
/**
|
|
532
|
+
* Responds with a 404 when no route matched the request's pathname.
|
|
533
|
+
*/
|
|
534
|
+
var PluginServerRouterBranchNotFoundListener = class extends Listener {
|
|
535
|
+
constructor(context) {
|
|
536
|
+
super(context, {
|
|
537
|
+
emitter: "server",
|
|
538
|
+
event: "routerBranchNotFound"
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
run(_request, response) {
|
|
542
|
+
if (!response.writableEnded) response.notFound();
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/listeners/PluginServerRouterFound.ts
|
|
548
|
+
/**
|
|
549
|
+
* Invokes the matched route's `run` method, emitting `routeError` if it throws.
|
|
550
|
+
*/
|
|
551
|
+
var PluginServerRouterFoundListener = class extends Listener {
|
|
552
|
+
constructor(context) {
|
|
553
|
+
super(context, {
|
|
554
|
+
emitter: "server",
|
|
555
|
+
event: "routerFound"
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
async run(request, response) {
|
|
559
|
+
try {
|
|
560
|
+
await request.route.run(request, response);
|
|
561
|
+
} catch (error) {
|
|
562
|
+
container.server.emit("routeError", error, request, response);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
//#endregion
|
|
568
|
+
//#region src/listeners/_load.ts
|
|
569
|
+
/**
|
|
570
|
+
* Registers the built-in dispatch-pipeline listeners into the framework's existing listener
|
|
571
|
+
* store, targeting the `server` container entry (see {@link ApiServer}).
|
|
572
|
+
*/
|
|
573
|
+
async function loadListeners() {
|
|
574
|
+
await Promise.all([
|
|
575
|
+
container.stores.loadPiece({
|
|
576
|
+
store: "listeners",
|
|
577
|
+
name: "pluginServerRequest",
|
|
578
|
+
piece: PluginServerRequestListener
|
|
579
|
+
}),
|
|
580
|
+
container.stores.loadPiece({
|
|
581
|
+
store: "listeners",
|
|
582
|
+
name: "pluginServerRouterFound",
|
|
583
|
+
piece: PluginServerRouterFoundListener
|
|
584
|
+
}),
|
|
585
|
+
container.stores.loadPiece({
|
|
586
|
+
store: "listeners",
|
|
587
|
+
name: "pluginServerRouterBranchNotFound",
|
|
588
|
+
piece: PluginServerRouterBranchNotFoundListener
|
|
589
|
+
}),
|
|
590
|
+
container.stores.loadPiece({
|
|
591
|
+
store: "listeners",
|
|
592
|
+
name: "pluginServerRouterBranchMethodNotAllowed",
|
|
593
|
+
piece: PluginServerRouterBranchMethodNotAllowedListener
|
|
594
|
+
}),
|
|
595
|
+
container.stores.loadPiece({
|
|
596
|
+
store: "listeners",
|
|
597
|
+
name: "pluginServerMiddlewareError",
|
|
598
|
+
piece: PluginServerMiddlewareErrorListener
|
|
599
|
+
}),
|
|
600
|
+
container.stores.loadPiece({
|
|
601
|
+
store: "listeners",
|
|
602
|
+
name: "pluginRouteError",
|
|
603
|
+
piece: PluginRouteErrorListener
|
|
604
|
+
})
|
|
605
|
+
]);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
//#endregion
|
|
609
|
+
//#region src/middlewares/body.ts
|
|
610
|
+
/**
|
|
611
|
+
* Rejects requests whose declared `Content-Length` exceeds {@link ApiServerOptions.maximumBodyLength}.
|
|
612
|
+
* Runs second (position 20), after CORS headers have been set.
|
|
613
|
+
*/
|
|
614
|
+
var BodyMiddleware = class extends Middleware {
|
|
615
|
+
constructor(context) {
|
|
616
|
+
super(context, { position: 20 });
|
|
617
|
+
}
|
|
618
|
+
run(request, response) {
|
|
619
|
+
const limit = container.server.options.maximumBodyLength ?? 1024 * 1024 * 50;
|
|
620
|
+
if (Number(request.headers["content-length"] ?? 0) > limit) response.json({ error: "Payload Too Large" }, HttpCodes.PayloadTooLarge);
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
//#endregion
|
|
625
|
+
//#region src/middlewares/headers.ts
|
|
626
|
+
/**
|
|
627
|
+
* Sets CORS headers on every response and short-circuits `OPTIONS` pre-flight requests.
|
|
628
|
+
* Runs first (position 10); 404/405 handling stays the sole responsibility of the router
|
|
629
|
+
* listeners, so it is not duplicated here.
|
|
630
|
+
*/
|
|
631
|
+
var HeadersMiddleware = class extends Middleware {
|
|
632
|
+
constructor(context) {
|
|
633
|
+
super(context, { position: 10 });
|
|
634
|
+
}
|
|
635
|
+
run(request, response) {
|
|
636
|
+
const origin = container.server.options.origin ?? "*";
|
|
637
|
+
response.setHeader("Access-Control-Allow-Origin", origin);
|
|
638
|
+
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
639
|
+
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS");
|
|
640
|
+
response.setHeader("Date", (/* @__PURE__ */ new Date()).toUTCString());
|
|
641
|
+
if (request.method === "OPTIONS") response.noContent();
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
//#endregion
|
|
646
|
+
//#region src/middlewares/_load.ts
|
|
647
|
+
/**
|
|
648
|
+
* Registers the built-in middlewares (`headers`, `body`) into {@link ApiServer.middlewares}.
|
|
649
|
+
*/
|
|
650
|
+
async function loadMiddlewares() {
|
|
651
|
+
await Promise.all([container.stores.loadPiece({
|
|
652
|
+
store: "middlewares",
|
|
653
|
+
name: "headers",
|
|
654
|
+
piece: HeadersMiddleware
|
|
655
|
+
}), container.stores.loadPiece({
|
|
656
|
+
store: "middlewares",
|
|
657
|
+
name: "body",
|
|
658
|
+
piece: BodyMiddleware
|
|
659
|
+
})]);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
//#endregion
|
|
663
|
+
export { RouteStore as a, RouterBranch as c, MiddlewareStore as d, Middleware as f, ApiServerEvent as i, RouterNode as l, ApiRequest as m, loadListeners as n, Route as o, ApiResponse as p, ApiServer as r, RouterRoot as s, loadMiddlewares as t, HttpMethods as u };
|
|
664
|
+
//# sourceMappingURL=src-CscMIChC.js.map
|