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