@arkstack/driver-express 0.5.2 → 0.5.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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @arkstack/driver-express
2
2
 
3
+ [![@arkstack/driver-express](https://img.shields.io/npm/dt/@arkstack/driver-express?style=flat-square&label=@arkstack/driver-express&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F@arkstack/driver-express)](https://www.npmjs.com/package/@arkstack/driver-express)
4
+
3
5
  Express driver for Arkstack, providing Express-based runtime integration for the framework.
4
6
 
5
7
  ## Auth Middleware
package/dist/app.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import type { Request, Response } from 'express'
2
+
3
+ declare module '@arkstack/common' {
4
+ interface HookRegistry {
5
+ 'middleware:auth': {
6
+ before: (ctx: { req: Request, res: Response }) => Promise<void>
7
+ after: (ctx: { req: Request, res: Response }) => Promise<void>
8
+ error: (error: unknown, ctx: { req: Request, res: Response }) => Promise<void>
9
+ }
10
+ }
11
+ }
12
+
13
+ declare module '@arkstack/foundry' {
14
+ interface HookRegistry {
15
+ 'middleware:auth': {
16
+ before: (ctx: { req: Request, res: Response }) => Promise<void>
17
+ after: (ctx: { req: Request, res: Response }) => Promise<void>
18
+ error: (error: unknown, ctx: { req: Request, res: Response }) => Promise<void>
19
+ }
20
+ }
21
+ }
22
+
23
+ declare global {
24
+ var tunnelUrl: () => string
25
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
+ /// <reference path="./app.d.ts" />
2
+ import { Middleware as Middleware$1, MiddlewareConfig } from "./types.js";
1
3
  import { ErrorRequestHandler, Express, Handler, Router as Router$1 } from "express";
2
- import { ArkstackKitDriver, ArkstackMiddlewareConfig, ArkstackRouteListOptions, PromiseOrValue } from "@arkstack/contract";
4
+ import { ArkstackKitDriver, ArkstackRouteListOptions, PromiseOrValue } from "@arkstack/contract";
3
5
  import { Router as Router$2 } from "clear-router/express";
4
6
  import { Route } from "clear-router";
5
7
  import { Handler as Handler$1, HttpContext, Middleware } from "clear-router/types/express";
@@ -19,17 +21,71 @@ interface ExpressDriverOptions {
19
21
  mountPublicAssets?: (app: Express, publicPath: string) => PromiseOrValue<void>;
20
22
  errorHandler?: ErrorRequestHandler | Handler;
21
23
  }
24
+ /**
25
+ * The ExpressDriver class implements the ArkstackKitDriver
26
+ * contract for the Express framework.
27
+ */
22
28
  declare class ExpressDriver extends ArkstackKitDriver<Express, Handler> {
23
29
  readonly name = "express";
30
+ private tunnel_url?;
24
31
  private readonly options;
32
+ /**
33
+ * Creates an instance of ExpressDriver.
34
+ *
35
+ * @param options
36
+ */
25
37
  constructor(options: ExpressDriverOptions);
38
+ /**
39
+ * Creates an Express application instance.
40
+ *
41
+ * @returns
42
+ */
26
43
  createApp(): Express;
44
+ /**
45
+ * Mounts static assets from the specified public path to the Express application.
46
+ *
47
+ * @param app
48
+ * @param publicPath
49
+ */
27
50
  mountPublicAssets(app: Express, publicPath: string): PromiseOrValue<void>;
51
+ /**
52
+ * Binds the router to the Express application using the provided bindRouter function.
53
+ *
54
+ * @param app
55
+ */
28
56
  bindRouter(app: Express): PromiseOrValue<void>;
29
- applyMiddleware(app: Express, middleware: Handler | ArkstackMiddlewareConfig<Handler>): void;
57
+ /**
58
+ * Applies middleware to the Express application.
59
+ *
60
+ * @param app
61
+ * @param middleware
62
+ */
63
+ applyMiddleware(app: Express, middleware: Middleware$1 | MiddlewareConfig): void;
64
+ /**
65
+ * Registers an error handler middleware to the Express
66
+ * application if provided in the options.
67
+ *
68
+ * @param app
69
+ */
30
70
  registerErrorHandler(app: Express): void;
31
- start(app: Express, port: number): void;
71
+ /**
72
+ * If trafic has been proxied via ngrok, this will return the tunnel URL.
73
+ *
74
+ * @returns
75
+ */
76
+ geTunnelUrl(): string | undefined;
77
+ /**
78
+ * Starts the Express server on the specified port.
79
+ *
80
+ * The bind host can be overridden with the `APP_HOST` (or `HOST`) env
81
+ * variable. It defaults to `0.0.0.0` so the server is reachable on all
82
+ * network interfaces, which platforms like Railway require for their
83
+ * healthcheck proxy to reach the app.
84
+ *
85
+ * @param app
86
+ * @param port
87
+ */
88
+ start(app: Express, port: number): Promise<void>;
32
89
  }
33
90
  //#endregion
34
- export { ExpressDriver, ExpressDriverOptions, Router, defaultErrorHandler };
35
- //# sourceMappingURL=index.d.ts.map
91
+ export { ExpressDriver, ExpressDriverOptions, Router, defaultErrorHandler };
package/dist/index.js CHANGED
@@ -1,12 +1,34 @@
1
1
  import express from "express";
2
2
  import { ArkstackKitDriver } from "@arkstack/contract";
3
- import { ErrorHandler, Logger, RequestException, importFile, renderError } from "@arkstack/common";
3
+ import { ErrorHandler, Logger, RequestException, devTlsCredentials, env, localNetworkAddress, renderError, resolveRuntimeModule } from "@arkstack/common";
4
+ import https from "node:https";
5
+ import { resolveMiddleware } from "@arkstack/http";
6
+ import ngrok from "@ngrok/ngrok";
4
7
  import { Router as Router$2 } from "clear-router/express";
5
8
  import { clearRouterExpressPlugin } from "@resora/plugin-clear-router";
6
- import { join } from "node:path";
7
9
  import { registerPlugin } from "resora";
8
10
  //#region src/error-handler.ts
9
- const defaultErrorHandler = (err, req, res, next) => {
11
+ const webMiddlewareKey = Symbol.for("arkstack:http:web");
12
+ const isRecord = (value) => {
13
+ return typeof value === "object" && value !== null;
14
+ };
15
+ const isWebRequest = (value) => {
16
+ if (!isRecord(value)) return false;
17
+ return value[webMiddlewareKey] === true || value.arkstackWeb === true || isWebRequest(value.context) || isWebRequest(value.req);
18
+ };
19
+ const redirectBackTarget = (req) => {
20
+ const referer = req.headers?.referer ?? req.headers?.referrer;
21
+ const value = Array.isArray(referer) ? referer[0] : referer;
22
+ return typeof value === "string" && value ? value : "/";
23
+ };
24
+ const flashValidationState = async (err, req, errors) => {
25
+ const session = req.httpSession ?? (isRecord(req.session) && typeof req.session.addValidationErrors === "function" ? req.session : void 0);
26
+ if (!session) return;
27
+ if (errors) session.addErrors?.(errors);
28
+ else session.addValidationErrors?.(err);
29
+ await session.save?.();
30
+ };
31
+ const defaultErrorHandler = async (err, req, res, next) => {
10
32
  const responseBody = ErrorHandler.createErrorPayload(err);
11
33
  if (ErrorHandler.shouldLogError(err)) ErrorHandler.logUnhandledError(err, {
12
34
  headers: req.headers,
@@ -20,6 +42,11 @@ const defaultErrorHandler = (err, req, res, next) => {
20
42
  }
21
43
  const expectsJson = (Array.isArray(req.headers.accept) ? req.headers.accept.join(",") : req.headers.accept ?? "").includes("application/json") || req.originalUrl.startsWith("/api/");
22
44
  const code = ErrorHandler.normalizeStatusCode(responseBody.code);
45
+ if (code === 422 && !expectsJson && isWebRequest(req)) {
46
+ await flashValidationState(err, req, responseBody.errors);
47
+ res.redirect(302, redirectBackTarget(req));
48
+ return;
49
+ }
23
50
  if (expectsJson) {
24
51
  res.status(code).json(responseBody);
25
52
  return;
@@ -37,12 +64,16 @@ Router$2.configure({ inferParamName: true });
37
64
  var Router = class extends Router$2 {
38
65
  static async bind() {
39
66
  const router = express.Router();
40
- await Router$2.group("/api", async () => {
41
- await importFile(join(process.cwd(), "src/routes/api.ts"));
42
- });
43
- await Router$2.group("/", async () => {
44
- await importFile(join(process.cwd(), "src/routes/web.ts"));
45
- });
67
+ try {
68
+ await Router$2.group("/api", resolveRuntimeModule("src/routes/api.ts"));
69
+ } catch (e) {
70
+ Logger.error("ERROR: Unable to load \"api.ts\" routes: " + e.message, false);
71
+ }
72
+ try {
73
+ await Router$2.group("/", resolveRuntimeModule("src/routes/web.ts"));
74
+ } catch (e) {
75
+ Logger.error("ERROR: Unable to load \"web.ts\" routes: " + e.message, false);
76
+ }
46
77
  Router$2.apply(router);
47
78
  router.all("/*splat", (req, _res, next) => {
48
79
  const url = req.originalUrl || req.url;
@@ -63,6 +94,7 @@ var Router = class extends Router$2 {
63
94
  */
64
95
  var ExpressDriver = class extends ArkstackKitDriver {
65
96
  name = "express";
97
+ tunnel_url;
66
98
  options;
67
99
  /**
68
100
  * Creates an instance of ExpressDriver.
@@ -114,17 +146,21 @@ var ExpressDriver = class extends ArkstackKitDriver {
114
146
  * @param middleware
115
147
  */
116
148
  applyMiddleware(app, middleware) {
149
+ if (!middleware) return;
117
150
  if (typeof middleware === "function") {
118
- app.use(middleware);
151
+ app.use(resolveMiddleware(middleware));
119
152
  return;
120
153
  }
121
- for (const [pos, entries] of Object.entries(middleware)) for (const entry of entries) if (pos === "after") app.use(async (req, res, next) => {
122
- res.once("finish", async () => {
123
- await entry(req, res, next);
154
+ for (const [pos, entries] of Object.entries(middleware)) for (const instance of entries) {
155
+ const entry = resolveMiddleware(instance);
156
+ if (pos === "after") app.use(async (req, res, next) => {
157
+ res.once("finish", async () => {
158
+ await entry(req, res, next);
159
+ });
160
+ next();
124
161
  });
125
- next();
126
- });
127
- else app.use(entry);
162
+ else app.use(entry);
163
+ }
128
164
  }
129
165
  /**
130
166
  * Registers an error handler middleware to the Express
@@ -136,18 +172,71 @@ var ExpressDriver = class extends ArkstackKitDriver {
136
172
  app.use(this.options.errorHandler ?? defaultErrorHandler);
137
173
  }
138
174
  /**
139
- * Starts the Express server on the specified port.
175
+ * If trafic has been proxied via ngrok, this will return the tunnel URL.
140
176
  *
141
- * @param app
142
- * @param port
177
+ * @returns
143
178
  */
144
- start(app, port) {
145
- app.listen(port, () => {
146
- Logger.log([["Server is running on", "white"], [`http://localhost:${port}`, "cyan"]], " ");
147
- });
179
+ geTunnelUrl() {
180
+ return this.tunnel_url;
181
+ }
182
+ /**
183
+ * Starts the Express server on the specified port.
184
+ *
185
+ * The bind host can be overridden with the `APP_HOST` (or `HOST`) env
186
+ * variable. It defaults to `0.0.0.0` so the server is reachable on all
187
+ * network interfaces, which platforms like Railway require for their
188
+ * healthcheck proxy to reach the app.
189
+ *
190
+ * @param app
191
+ * @param port
192
+ */
193
+ async start(app, port) {
194
+ const host = env("APP_HOST", env("HOST", "0.0.0.0"));
195
+ const secure = env("APP_SECURE", false) === true;
196
+ const tunneled = env("TUNNEL", false);
197
+ const scheme = secure ? "https" : "http";
198
+ const onListen = async () => {
199
+ let log = startupLogLines(scheme, host, port);
200
+ if (tunneled === true) {
201
+ const url = (await ngrok.forward({
202
+ addr: port,
203
+ authtoken: env("NGROK_AUTHTOKEN"),
204
+ domain: env("NGROK_DOMAIN")
205
+ })).url();
206
+ if (url) {
207
+ log = log.concat(Logger.log([["Trafic has been tunnelled to", "white"], [url, "green"]], " ", false));
208
+ process.env.TUNNEL_URL = url;
209
+ this.tunnel_url = url;
210
+ globalThis.tunnelUrl = () => url;
211
+ }
212
+ }
213
+ console.log(log.join("\n"));
214
+ };
215
+ if (secure) {
216
+ const credentials = await devTlsCredentials();
217
+ https.createServer({
218
+ key: credentials.key,
219
+ cert: credentials.cert
220
+ }, app).listen(port, host, onListen);
221
+ return;
222
+ }
223
+ app.listen(port, host, onListen);
224
+ }
225
+ };
226
+ /**
227
+ * Build the "Server is running" startup lines, adding a local-network URL when
228
+ * the server is bound to all interfaces (`0.0.0.0`/`::`) so it is reachable from
229
+ * other devices.
230
+ */
231
+ const startupLogLines = (scheme, host, port) => {
232
+ const bindsAll = host === "0.0.0.0" || host === "::";
233
+ const localHost = bindsAll ? "localhost" : host;
234
+ const lines = [Logger.log([["Server is running on", "white"], [`${scheme}://${localHost}:${port}`, "cyan"]], " ", false)];
235
+ if (bindsAll) {
236
+ const address = localNetworkAddress();
237
+ if (address) lines.push(Logger.log([["Network access via", "white"], [`${scheme}://${address}:${port}`, "cyan"]], " ", false));
148
238
  }
239
+ return lines;
149
240
  };
150
241
  //#endregion
151
242
  export { ExpressDriver, Router, defaultErrorHandler };
152
-
153
- //# sourceMappingURL=index.js.map
@@ -1,18 +1,88 @@
1
1
  import { Handler, NextFunction, Request, Response } from "express";
2
2
  import multer from "multer";
3
+ import * as _$express_rate_limit0 from "express-rate-limit";
4
+ import { ValueDeterminingMiddleware } from "express-rate-limit";
3
5
 
4
6
  //#region src/middlewares/auth.d.ts
5
7
  declare const auth: Handler;
8
+ declare class AuthMiddleware {
9
+ handler(req: Request, res: Response, next: NextFunction): unknown;
10
+ }
6
11
  //#endregion
7
12
  //#region src/middlewares/formdata.d.ts
8
13
  declare const formdata: multer.Multer;
14
+ declare class FormDataMiddleware {
15
+ private type;
16
+ private name?;
17
+ private count?;
18
+ private options?;
19
+ constructor(type: 'array', fieldName: string, maxCount?: number | undefined, options?: multer.Options);
20
+ constructor(type: 'fields', fields: multer.Field[], options?: multer.Options);
21
+ constructor(type: 'single', fieldName: string, options?: multer.Options);
22
+ handler(req: Request, res: Response, next: NextFunction): unknown;
23
+ }
24
+ //#endregion
25
+ //#region src/middlewares/inertia.d.ts
26
+ /**
27
+ * Bind the Inertia request context for Express.
28
+ *
29
+ * Normalizes the Express request into the adapter's driver-agnostic shape and
30
+ * runs the downstream handler inside Inertia's async-local context (mirroring the
31
+ * `resora` middleware), so `inertia()` / `Inertia.*` resolve the active request.
32
+ *
33
+ * It also upgrades `302` redirects to `303 See Other` for `PUT`/`PATCH`/`DELETE`
34
+ * Inertia visits, which the Inertia client requires to follow the redirect with
35
+ * a `GET`.
36
+ *
37
+ * `@arkstack/inertia` is imported dynamically so the package stays optional.
38
+ */
39
+ declare const inertia: () => Handler;
40
+ declare class InertiaMiddleware {
41
+ handler(req: Request, res: Response, next: NextFunction): unknown;
42
+ }
43
+ //#endregion
44
+ //#region src/middlewares/limiter.d.ts
45
+ /**
46
+ * create a rate limiter middleware
47
+ *
48
+ * @param requests number of requests allowed per windowMs
49
+ * @param perMin number of minutes for the window
50
+ * @param message custom message to be returned when rate limit is exceeded
51
+ * @returns
52
+ */
53
+ declare const limiter: (requests?: number | ValueDeterminingMiddleware<number>, perSec?: number, message?: string | ValueDeterminingMiddleware<string>) => _$express_rate_limit0.RateLimitRequestHandler;
9
54
  //#endregion
10
55
  //#region src/middlewares/request-logger.d.ts
56
+ /**
57
+ * Middleware to log incoming requests and their response times.
58
+ *
59
+ * @param config Configuration options for the request logger middleware.
60
+ * @param config.allowInProduction If true, the logger will also log requests in production environment. Default is false.
61
+ * @returns
62
+ */
11
63
  declare const requestLogger: ({
12
64
  allowInProduction
13
65
  }?: {
14
66
  allowInProduction?: boolean;
15
67
  }) => (req: Request, res: Response, next: NextFunction) => Promise<void>;
68
+ declare class RequestLoggerMiddleware {
69
+ private options;
70
+ constructor(options?: {
71
+ allowInProduction?: boolean;
72
+ });
73
+ handler(req: Request, res: Response, next: NextFunction): Promise<void>;
74
+ }
75
+ //#endregion
76
+ //#region src/middlewares/resora.d.ts
77
+ /**
78
+ * Apply the application's resora configuration (`src/config/resources.ts`) and
79
+ * bind the per-request `{ req, res }` context so Resources can build URLs and
80
+ * pagination links.
81
+ *
82
+ * Replaces the manual `Resource.setCtx(...)` wiring: resora's runtime config is
83
+ * applied from `config('resources')`, and the request is run within resora's
84
+ * async context so downstream handlers resolve the correct context.
85
+ */
86
+ declare const resora: () => Handler;
16
87
  //#endregion
17
- export { auth, formdata, requestLogger };
18
- //# sourceMappingURL=index.d.ts.map
88
+ export { AuthMiddleware, FormDataMiddleware, InertiaMiddleware, RequestLoggerMiddleware, auth, formdata, inertia, limiter, requestLogger, resora };
@@ -1,8 +1,10 @@
1
- import { Hook, Logger, nodeEnv } from "@arkstack/common";
2
- import { Auth, AuthenticationException } from "@arkstack/auth";
1
+ import { Exception, Hook, Logger, config, env, nodeEnv, resolveRuntimeDir } from "@arkstack/common";
2
+ import { applyRuntimeConfig, getDefaultConfig, runWithCtx, setCtx } from "resora";
3
3
  import multer from "multer";
4
+ import { rateLimit } from "express-rate-limit";
4
5
  //#region src/middlewares/auth.ts
5
6
  const auth = async (req, res, next) => {
7
+ const { Auth, AuthenticationException } = await import("@arkstack/auth");
6
8
  try {
7
9
  if (Hook.has("middleware:auth", "before")) await Promise.resolve(Hook.get("middleware:auth", "before")?.({
8
10
  req,
@@ -14,10 +16,11 @@ const auth = async (req, res, next) => {
14
16
  status: 401
15
17
  });
16
18
  const auth = Auth.make().setRequest(req);
17
- const user = await Auth.make().setRequest(req).authorizeToken(token);
19
+ const user = await auth.authorizeToken(token);
18
20
  req.user = user;
19
21
  req.auth = auth;
20
22
  req.authUser = user;
23
+ req.session = auth.session();
21
24
  req.authToken = token;
22
25
  if (Hook.has("middleware:auth", "after")) await Promise.resolve(Hook.get("middleware:auth", "after")?.({
23
26
  req,
@@ -32,6 +35,11 @@ const auth = async (req, res, next) => {
32
35
  next(error);
33
36
  }
34
37
  };
38
+ var AuthMiddleware = class {
39
+ handler(req, res, next) {
40
+ return auth(req, res, next);
41
+ }
42
+ };
35
43
  const readBearerToken = (authorization) => {
36
44
  const value = Array.isArray(authorization) ? authorization[0] : authorization;
37
45
  if (!value?.startsWith("Bearer ")) return null;
@@ -40,6 +48,110 @@ const readBearerToken = (authorization) => {
40
48
  //#endregion
41
49
  //#region src/middlewares/formdata.ts
42
50
  const formdata = multer({ storage: multer.memoryStorage() });
51
+ var FormDataMiddleware = class {
52
+ type;
53
+ name;
54
+ count;
55
+ options;
56
+ constructor(type, name, count, options) {
57
+ this.type = type;
58
+ this.name = name;
59
+ this.count = count;
60
+ this.options = options;
61
+ }
62
+ handler(req, res, next) {
63
+ let inst;
64
+ const options = this.options ?? (typeof this.count === "object" && "storage" in this.count ? this.count : {});
65
+ const formdata = multer({
66
+ storage: multer.memoryStorage(),
67
+ ...options
68
+ });
69
+ if (this.type === "any" || this.type === "none") inst = formdata.any();
70
+ else if (this.type === "array") inst = formdata.array(this.name, this.count);
71
+ else if (this.type === "fields") inst = formdata.fields(this.name);
72
+ else inst = formdata.single(this.name);
73
+ return inst.call(inst, req, res, next);
74
+ }
75
+ };
76
+ //#endregion
77
+ //#region src/middlewares/inertia.ts
78
+ /**
79
+ * Bind the Inertia request context for Express.
80
+ *
81
+ * Normalizes the Express request into the adapter's driver-agnostic shape and
82
+ * runs the downstream handler inside Inertia's async-local context (mirroring the
83
+ * `resora` middleware), so `inertia()` / `Inertia.*` resolve the active request.
84
+ *
85
+ * It also upgrades `302` redirects to `303 See Other` for `PUT`/`PATCH`/`DELETE`
86
+ * Inertia visits, which the Inertia client requires to follow the redirect with
87
+ * a `GET`.
88
+ *
89
+ * `@arkstack/inertia` is imported dynamically so the package stays optional.
90
+ */
91
+ const inertia = () => {
92
+ return async (req, res, next) => {
93
+ try {
94
+ const { runInertia, shouldUpgradeRedirect } = await import("@arkstack/inertia");
95
+ const request = {
96
+ method: String(req.method ?? "GET").toUpperCase(),
97
+ url: req.originalUrl || req.url || "/",
98
+ header: (name) => {
99
+ const value = req.headers[name.toLowerCase()];
100
+ return Array.isArray(value) ? value[0] : value;
101
+ }
102
+ };
103
+ if (request.header("x-inertia") === "true") {
104
+ const original = res.redirect.bind(res);
105
+ res.redirect = ((...args) => {
106
+ let status = typeof args[0] === "number" ? args[0] : 302;
107
+ const url = typeof args[0] === "number" ? args[1] : args[0];
108
+ if (shouldUpgradeRedirect(request.method, status)) status = 303;
109
+ return original(status, url);
110
+ });
111
+ }
112
+ runInertia(request, () => next());
113
+ } catch (error) {
114
+ next(error);
115
+ }
116
+ };
117
+ };
118
+ var InertiaMiddleware = class {
119
+ handler(req, res, next) {
120
+ return inertia()(req, res, next);
121
+ }
122
+ };
123
+ //#endregion
124
+ //#region src/Exceptions/RateLimitExceededException.ts
125
+ var RateLimitExceededException = class extends Exception {
126
+ statusCode = 429;
127
+ name;
128
+ constructor(options) {
129
+ super(options.message);
130
+ this.name = "RateLimitExceededException";
131
+ this.statusCode = options.statusCode ?? 429;
132
+ }
133
+ };
134
+ //#endregion
135
+ //#region src/middlewares/limiter.ts
136
+ /**
137
+ * create a rate limiter middleware
138
+ *
139
+ * @param requests number of requests allowed per windowMs
140
+ * @param perMin number of minutes for the window
141
+ * @param message custom message to be returned when rate limit is exceeded
142
+ * @returns
143
+ */
144
+ const limiter = (requests = 100, perSec = 900, message) => rateLimit({
145
+ message,
146
+ limit: requests,
147
+ windowMs: (env("NODE_ENV") === "production" ? perSec : 30) * 1e3,
148
+ standardHeaders: true,
149
+ legacyHeaders: false,
150
+ ipv6Subnet: 56,
151
+ handler: (_, __, ___, options) => {
152
+ throw new RateLimitExceededException(options);
153
+ }
154
+ });
43
155
  //#endregion
44
156
  //#region src/middlewares/request-logger.ts
45
157
  const colors = {
@@ -57,7 +169,8 @@ const colors = {
57
169
  * @returns
58
170
  */
59
171
  const requestLogger = ({ allowInProduction = false } = {}) => async (req, res, next) => {
60
- if (nodeEnv() === "prod" && !allowInProduction) return next();
172
+ const VERBOSE = process.env.VERBOSITY != "0";
173
+ if (nodeEnv() === "prod" && !allowInProduction || !VERBOSE) return next();
61
174
  const start = Date.now();
62
175
  const status = res.statusCode || 200;
63
176
  const duration = Date.now() - start;
@@ -69,7 +182,49 @@ const requestLogger = ({ allowInProduction = false } = {}) => async (req, res, n
69
182
  ], " ");
70
183
  next();
71
184
  };
185
+ var RequestLoggerMiddleware = class {
186
+ options;
187
+ constructor(options = {}) {
188
+ this.options = options;
189
+ }
190
+ handler(req, res, next) {
191
+ const inst = requestLogger(this.options);
192
+ return inst.call(inst, req, res, next);
193
+ }
194
+ };
195
+ //#endregion
196
+ //#region src/middlewares/resora.ts
197
+ /**
198
+ * Apply the application's resora configuration (`src/config/resources.ts`) and
199
+ * bind the per-request `{ req, res }` context so Resources can build URLs and
200
+ * pagination links.
201
+ *
202
+ * Replaces the manual `Resource.setCtx(...)` wiring: resora's runtime config is
203
+ * applied from `config('resources')`, and the request is run within resora's
204
+ * async context so downstream handlers resolve the correct context.
205
+ */
206
+ const resora = () => {
207
+ let resources;
208
+ return (req, res, next) => {
209
+ try {
210
+ if (!resources) {
211
+ resources = {
212
+ ...getDefaultConfig(),
213
+ ...config("resources", {})
214
+ };
215
+ if (typeof resources.resourcesDir === "string") resources.resourcesDir = resolveRuntimeDir(resources.resourcesDir);
216
+ }
217
+ applyRuntimeConfig(resources);
218
+ } catch {}
219
+ setCtx({
220
+ req,
221
+ res
222
+ });
223
+ return runWithCtx({
224
+ req,
225
+ res
226
+ }, () => next());
227
+ };
228
+ };
72
229
  //#endregion
73
- export { auth, formdata, requestLogger };
74
-
75
- //# sourceMappingURL=index.js.map
230
+ export { AuthMiddleware, FormDataMiddleware, InertiaMiddleware, RequestLoggerMiddleware, auth, formdata, inertia, limiter, requestLogger, resora };
@@ -0,0 +1,9 @@
1
+ import { Handler } from "express";
2
+ import { ArkstackMiddlewareConfig } from "@arkstack/contract";
3
+ import { ClassMiddleware } from "clear-router/types/basic";
4
+
5
+ //#region src/types.d.ts
6
+ type Middleware = Handler | ClassMiddleware<Handler>;
7
+ type MiddlewareConfig = ArkstackMiddlewareConfig<Middleware>;
8
+ //#endregion
9
+ export { Middleware, MiddlewareConfig };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arkstack/driver-express",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "type": "module",
5
5
  "description": "Express driver for Arkstack, providing Express-based runtime integration for the framework.",
6
6
  "homepage": "https://arkstack.toneflix.net",
@@ -33,32 +33,39 @@
33
33
  "exports": {
34
34
  ".": "./dist/index.js",
35
35
  "./middlewares": "./dist/middlewares/index.js",
36
+ "./types": "./dist/types.js",
36
37
  "./package.json": "./package.json"
37
38
  },
38
39
  "dependencies": {
39
- "multer": "^2.1.1",
40
- "clear-router": "^2.6.4",
40
+ "@ngrok/ngrok": "^1.7.0",
41
+ "@resora/plugin-clear-router": "^1.0.66",
42
+ "clear-router": "^2.9.0",
41
43
  "express-rate-limit": "^8.4.1",
42
- "@resora/plugin-clear-router": "^1.0.14",
43
- "resora": "^1.2.4",
44
- "@arkstack/contract": "^0.5.2"
44
+ "multer": "^2.1.1",
45
+ "resora": "^1.3.27",
46
+ "@arkstack/contract": "^0.5.3"
45
47
  },
46
48
  "peerDependencies": {
47
49
  "express": "^5.2.1",
48
- "@arkstack/auth": "^0.5.2",
49
- "@arkstack/common": "^0.5.2"
50
+ "@arkstack/auth": "^0.5.3",
51
+ "@arkstack/foundry": "^0.5.3",
52
+ "@arkstack/inertia": "^0.5.3",
53
+ "@arkstack/common": "^0.5.3"
50
54
  },
51
55
  "peerDependenciesMeta": {
52
56
  "@arkstack/auth": {
53
57
  "optional": true
58
+ },
59
+ "@arkstack/inertia": {
60
+ "optional": true
54
61
  }
55
62
  },
56
63
  "devDependencies": {
57
- "@types/multer": "^2.1.0",
58
- "@types/express": "^5.0.6"
64
+ "@types/express": "^5.0.6",
65
+ "@types/multer": "^2.1.0"
59
66
  },
60
67
  "scripts": {
61
- "build": "tsdown",
68
+ "build": "tsdown --config-loader unrun",
62
69
  "version:patch": "pnpm version patch"
63
70
  }
64
71
  }
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","names":["ClearRouter"],"sources":["../src/error-handler.ts","../src/Router.ts","../src/index.ts"],"sourcesContent":["import {\n ErrorHandler,\n renderError,\n} from '@arkstack/common'\n\nimport type { ErrorRequestHandler } from 'express'\n\nexport const defaultErrorHandler: ErrorRequestHandler = (err, req, res, next) => {\n const responseBody = ErrorHandler.createErrorPayload(err)\n\n if (ErrorHandler.shouldLogError(err)) {\n ErrorHandler.logUnhandledError(err, {\n headers: req.headers,\n method: req.method,\n url: req.originalUrl || req.url,\n }, 'Unhandled Express request error')\n }\n\n if (process.env.NODE_ENV === 'development') console.error(responseBody)\n\n if (res.headersSent) {\n next(err)\n\n return\n }\n\n const acceptsHeader = Array.isArray(req.headers.accept) ? req.headers.accept.join(',') : req.headers.accept ?? ''\n const expectsJson = acceptsHeader.includes('application/json') || req.originalUrl.startsWith('/api/')\n const code = ErrorHandler.normalizeStatusCode(responseBody.code)\n\n if (expectsJson) {\n res.status(code).json(responseBody)\n\n return\n }\n\n res.status(code).setHeader('Content-Type', 'text/html').send(renderError({\n message: String(responseBody.message),\n stack: typeof responseBody.stack === 'string' ? responseBody.stack : undefined,\n code,\n }))\n}\n\nexport default defaultErrorHandler\n","import { RequestException, importFile } from '@arkstack/common'\nimport express, { Router as ExpressRouter } from 'express'\n\nimport { ArkstackRouteListOptions } from '@arkstack/contract'\nimport { Router as ClearRouter } from 'clear-router/express'\nimport { type Route } from 'clear-router'\nimport { clearRouterExpressPlugin } from '@resora/plugin-clear-router'\nimport { join } from 'node:path'\nimport { registerPlugin } from 'resora'\nimport type { Handler, HttpContext, Middleware } from 'clear-router/types/express'\n\nregisterPlugin(clearRouterExpressPlugin)\nClearRouter.configure({\n inferParamName: true\n})\n\nexport class Router extends ClearRouter {\n static async bind (): Promise<ExpressRouter> {\n const router = express.Router()\n\n // Register API routes\n await ClearRouter.group('/api', async () => {\n await importFile(join(process.cwd(), 'src/routes/api.ts'))\n })\n\n // Register web routes\n await ClearRouter.group('/', async () => {\n await importFile(join(process.cwd(), 'src/routes/web.ts'))\n })\n\n // Apply the registered routes to the Express application\n ClearRouter.apply(router)\n\n // Handle unmatched routes\n router.all('/*splat', (req, _res, next) => {\n const url = req.originalUrl || req.url\n next(new RequestException(`Cannot find any route matching [${req.method}] ${url}`, 404))\n })\n\n return router\n }\n\n static async list (\n _options: ArkstackRouteListOptions = {}\n ): Promise<Array<Route<HttpContext, Middleware, Handler>>> {\n await this.bind()\n\n return this.allRoutes() as never\n }\n}\n","import express, { type ErrorRequestHandler, type Express, type Handler } from 'express'\n\nimport { ArkstackKitDriver, ArkstackMiddlewareConfig, PromiseOrValue } from '@arkstack/contract'\nimport { Logger } from '@arkstack/common'\nimport { defaultErrorHandler } from './error-handler'\n\nexport interface ExpressDriverOptions {\n bindRouter: (app: Express) => PromiseOrValue<void>;\n mountPublicAssets?: (app: Express, publicPath: string) => PromiseOrValue<void>;\n errorHandler?: ErrorRequestHandler | Handler;\n}\n\n/**\n * The ExpressDriver class implements the ArkstackKitDriver \n * contract for the Express framework.\n */\nexport class ExpressDriver extends ArkstackKitDriver<Express, Handler> {\n readonly name = 'express'\n private readonly options: ExpressDriverOptions\n\n /**\n * Creates an instance of ExpressDriver.\n * \n * @param options \n */\n constructor(options: ExpressDriverOptions) {\n super()\n this.options = options\n }\n\n /**\n * Creates an Express application instance.\n * \n * @returns \n */\n createApp (): Express {\n return express()\n }\n\n /**\n * Mounts static assets from the specified public path to the Express application.\n * \n * @param app \n * @param publicPath \n */\n mountPublicAssets (app: Express, publicPath: string): PromiseOrValue<void> {\n if (this.options.mountPublicAssets) {\n return this.options.mountPublicAssets(app, publicPath)\n }\n\n app.use(express.static(publicPath, {\n maxAge: '1y',\n immutable: true,\n setHeaders: (res) => {\n res.setHeader('Access-Control-Allow-Origin', '*')\n res.setHeader('Access-Control-Allow-Methods', 'GET, HEAD, OPTIONS')\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')\n },\n }))\n }\n\n /**\n * Binds the router to the Express application using the provided bindRouter function.\n * \n * @param app \n */\n bindRouter (app: Express): PromiseOrValue<void> {\n return this.options.bindRouter(app)\n }\n\n /**\n * Applies middleware to the Express application.\n * \n * @param app \n * @param middleware \n */\n applyMiddleware (\n app: Express,\n middleware: Handler | ArkstackMiddlewareConfig<Handler>,\n ): void {\n if (typeof middleware === 'function') {\n app.use(middleware)\n\n return\n }\n\n for (const [pos, entries] of Object.entries(middleware) as [string, Handler[]][]) {\n for (const entry of entries) {\n if (pos === 'after') {\n app.use(async (req, res, next) => {\n res.once('finish', async () => {\n await entry(req, res, next)\n })\n next()\n })\n } else {\n app.use(entry)\n }\n }\n }\n }\n\n /**\n * Registers an error handler middleware to the Express \n * application if provided in the options.\n * \n * @param app \n */\n registerErrorHandler (app: Express): void {\n app.use((this.options.errorHandler ?? defaultErrorHandler) as ErrorRequestHandler)\n }\n\n /**\n * Starts the Express server on the specified port.\n * \n * @param app \n * @param port \n */\n start (app: Express, port: number): void {\n app.listen(port, () => {\n Logger.log([\n ['Server is running on', 'white'],\n [`http://localhost:${port}`, 'cyan']\n ], ' ')\n })\n }\n}\n\nexport * from './error-handler'\nexport * from './Router'\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/middlewares/auth.ts","../../src/middlewares/formdata.ts","../../src/middlewares/request-logger.ts"],"sourcesContent":["import { Auth, AuthenticationException } from '@arkstack/auth'\n\nimport type { Handler } from 'express'\nimport { Hook } from '@arkstack/common'\n\nexport const auth: Handler = async (req, res, next) => {\n try {\n if (Hook.has('middleware:auth', 'before'))\n await Promise.resolve(Hook.get('middleware:auth', 'before')?.({ req, res }))\n\n const token = readBearerToken(req.headers.authorization)\n\n if (!token) {\n throw new AuthenticationException('Unauthenticated', { req, status: 401 })\n }\n\n const auth = Auth.make().setRequest(req)\n const user = await Auth.make().setRequest(req).authorizeToken(token)\n\n req.user = user\n req.auth = auth\n req.authUser = user\n req.authToken = token\n\n if (Hook.has('middleware:auth', 'after'))\n await Promise.resolve(Hook.get('middleware:auth', 'after')?.({ req, res }))\n\n next()\n } catch (error) {\n if (Hook.has('middleware:auth', 'error'))\n await Promise.resolve(Hook.get('middleware:auth', 'error')?.(error, {\n req,\n res\n }))\n\n next(error)\n }\n}\n\nconst readBearerToken = (authorization: string | string[] | undefined) => {\n const value = Array.isArray(authorization) ? authorization[0] : authorization\n\n if (!value?.startsWith('Bearer ')) {\n return null\n }\n\n return value.substring(7)\n}\n","import multer from 'multer'\n\nexport const formdata = multer({ storage: multer.memoryStorage() })","import { Logger, nodeEnv } from '@arkstack/common'\nimport { NextFunction, Request, Response } from 'express'\n\nconst colors: Record<string, 'green' | 'blue' | 'yellow' | 'red' | 'cyan'> = {\n GET: 'green',\n POST: 'blue',\n PUT: 'yellow',\n DELETE: 'red',\n PATCH: 'cyan',\n}\n\n/**\n * Middleware to log incoming requests and their response times.\n * \n * @param config Configuration options for the request logger middleware.\n * @param config.allowInProduction If true, the logger will also log requests in production environment. Default is false. \n * @returns \n */\nexport const requestLogger = ({\n allowInProduction = false,\n}: {\n allowInProduction?: boolean\n} = {}) => async (req: Request, res: Response, next: NextFunction) => {\n if (nodeEnv() === 'prod' && !allowInProduction) return next()\n\n const start = Date.now()\n\n const status = res.statusCode || 200\n const duration = Date.now() - start\n Logger.log([\n [`[${req.method}]`, colors[req.method] || 'white'],\n [req.url, 'cyan'],\n [status.toString(), status >= 500 ? 'red' : status >= 400 ? 'yellow' : 'green'],\n [`- ${duration}ms`, 'dim']\n ], ' ')\n\n next()\n}"],"mappings":";;;;AAKA,MAAa,OAAgB,OAAO,KAAK,KAAK,SAAS;CACnD,IAAI;EACA,IAAI,KAAK,IAAI,mBAAmB,SAAS,EACrC,MAAM,QAAQ,QAAQ,KAAK,IAAI,mBAAmB,SAAS,GAAG;GAAE;GAAK;GAAK,CAAC,CAAC;EAEhF,MAAM,QAAQ,gBAAgB,IAAI,QAAQ,cAAc;EAExD,IAAI,CAAC,OACD,MAAM,IAAI,wBAAwB,mBAAmB;GAAE;GAAK,QAAQ;GAAK,CAAC;EAG9E,MAAM,OAAO,KAAK,MAAM,CAAC,WAAW,IAAI;EACxC,MAAM,OAAO,MAAM,KAAK,MAAM,CAAC,WAAW,IAAI,CAAC,eAAe,MAAM;EAEpE,IAAI,OAAO;EACX,IAAI,OAAO;EACX,IAAI,WAAW;EACf,IAAI,YAAY;EAEhB,IAAI,KAAK,IAAI,mBAAmB,QAAQ,EACpC,MAAM,QAAQ,QAAQ,KAAK,IAAI,mBAAmB,QAAQ,GAAG;GAAE;GAAK;GAAK,CAAC,CAAC;EAE/E,MAAM;UACD,OAAO;EACZ,IAAI,KAAK,IAAI,mBAAmB,QAAQ,EACpC,MAAM,QAAQ,QAAQ,KAAK,IAAI,mBAAmB,QAAQ,GAAG,OAAO;GAChE;GACA;GACH,CAAC,CAAC;EAEP,KAAK,MAAM;;;AAInB,MAAM,mBAAmB,kBAAiD;CACtE,MAAM,QAAQ,MAAM,QAAQ,cAAc,GAAG,cAAc,KAAK;CAEhE,IAAI,CAAC,OAAO,WAAW,UAAU,EAC7B,OAAO;CAGX,OAAO,MAAM,UAAU,EAAE;;;;AC5C7B,MAAa,WAAW,OAAO,EAAE,SAAS,OAAO,eAAe,EAAE,CAAA;;;ACClE,MAAM,SAAuE;CACzE,KAAK;CACL,MAAM;CACN,KAAK;CACL,QAAQ;CACR,OAAO;CACV;;;;;;;;AASD,MAAa,iBAAiB,EAC1B,oBAAoB,UAGpB,EAAE,KAAK,OAAO,KAAc,KAAe,SAAuB;CAClE,IAAI,SAAS,KAAK,UAAU,CAAC,mBAAmB,OAAO,MAAM;CAE7D,MAAM,QAAQ,KAAK,KAAK;CAExB,MAAM,SAAS,IAAI,cAAc;CACjC,MAAM,WAAW,KAAK,KAAK,GAAG;CAC9B,OAAO,IAAI;EACP,CAAC,IAAI,IAAI,OAAO,IAAI,OAAO,IAAI,WAAW,QAAQ;EAClD,CAAC,IAAI,KAAK,OAAO;EACjB,CAAC,OAAO,UAAU,EAAE,UAAU,MAAM,QAAQ,UAAU,MAAM,WAAW,QAAQ;EAC/E,CAAC,KAAK,SAAS,KAAK,MAAM;EAC7B,EAAE,IAAI;CAEP,MAAM"}