@nano-app/backend 0.12.4 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,7 +58,7 @@ export default createRoute({
58
58
  });
59
59
  ```
60
60
 
61
- Each handler receives `(req, params)` and returns an `ApiResponse` without `success`: its `status` is the HTTP status (`200` by default) and the framework sets `success` to `status < 400`. `params` holds the plugin's `options`, `db` (the Prisma client when a database is enabled, see [Database](#database)) and `onBeforeResult` (see [Callbacks](#callbacks)):
61
+ Each handler receives `(req, params)` and returns an `ApiResponse` without `success`: its `status` is the HTTP status (`200` by default) and the framework sets `success` to `status < 400`. `params` holds the plugin's `options`, `db` (the Prisma client when a database is enabled, see [Database](#database); a plugin without its own gets the app's) and `onBeforeResult` (see [Callbacks](#callbacks)):
62
62
 
63
63
  ```ts
64
64
  type ApiResponse<T> = {
@@ -82,26 +82,34 @@ Every option is optional.
82
82
  |---|---|---|
83
83
  | `port` | `3000` | Listening port. |
84
84
  | `listener` | `() => {}` | Callback once the server is listening. |
85
- | `settings` | `{}` | Values for `app.set()` (e.g. `{ "trust proxy": 1 }`). |
85
+ | `settings` | `{ "trust proxy": <private networks> }` | Values for `app.set()`: see [Client IP](#client-ip). |
86
86
  | `packages` | see below | Framework base middlewares. |
87
87
  | `mainPlugin` | `{ routesDir: "src/routes" }` | Your routes (`routesDir`, relative to `cwd`), `basePath` (e.g. `/api`, no trailing slash) and database: see [Database](#database). |
88
88
  | `plugins` | `[]` | Plugins mounted around your routes, in order: see [Plugins](#plugins). |
89
89
 
90
- `packages` accepts `morgan`, `cors`, `helmet`, `compression`, `rateLimit`, `express` (`express.json` options), `urlencoded` and `betterAuth` (off by default: see [Better Auth](#better-auth)). The merge is **per section**: passing one key keeps the others; pass `false` to disable one.
90
+ `packages` accepts `morgan`, `cors`, `helmet`, `compression`, `express` (`express.json` options), `urlencoded` and `betterAuth` (off by default: see [Better Auth](#better-auth)). The merge is **per section**: passing one key keeps the others; pass `false` to disable one.
91
91
 
92
92
  `morgan` logs `:timestamp | :method :path :status` by default: `:path` is the path without its query string, so tokens sent there stay out of the logs. Set `format` with `:url` to log the full URL.
93
93
 
94
94
  ```ts
95
95
  createNanoApp({
96
- settings: { 'trust proxy': 1 },
97
96
  packages: {
98
97
  cors: { origin: 'https://my-app.com' },
99
98
  helmet: false,
100
- rateLimit: { windowMs: 60_000, max: 120 },
101
99
  },
102
100
  });
103
101
  ```
104
102
 
103
+ ### Client IP
104
+
105
+ The [`rate-limit` plugin](../plugins/README.md) and Better Auth count each client by its IP. Behind a proxy (the frontend of `@nano-app/frontend`, nginx, a load balancer, a CDN, Tailscale Funnel), the IP of the request is the proxy's: the client's is the last one the proxies appended to `X-Forwarded-For`.
106
+
107
+ By default the backend reads that header only when the request comes from loopback or a private network (`127.0.0.0/8`, `::1`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `fc00::/7`), and takes the last address that is not one of them. A request from a public IP is counted by that IP, whatever header it sends. Better Auth gets the same networks as `advanced.ipAddress.trustedProxies`.
108
+
109
+ - A public proxy in front (a CDN): set its addresses in `settings['trust proxy']` (any [Express value](https://expressjs.com/en/guide/behind-proxies.html)) and in Better Auth's `advanced.ipAddress.trustedProxies`.
110
+ - No proxy at all, or one you do not trust: `settings: { 'trust proxy': false }` and `advanced.ipAddress.trustedProxies: []`.
111
+ - Anything that can reach the backend from a private network can choose the IP it is counted by: keep the backend reachable only from its proxies.
112
+
105
113
  ## Database
106
114
 
107
115
  Off until enabled: at startup nano-app generates the Prisma client of your schema, checks that its tables can be read and passes it to your handlers as their second parameter. **It never modifies your schemas**: applying them (`prisma db push`, `prisma migrate dev`, `prisma migrate deploy`) is up to you, with Prisma.
@@ -165,20 +173,21 @@ pnpm add better-auth
165
173
  ```
166
174
 
167
175
  ```ts
168
- // src/auth.ts: the options, and an instance only for the Better Auth CLI
169
- import { betterAuth, type BetterAuthOptions } from 'better-auth';
176
+ // src/auth.ts: the options; nano-app creates the instance, the Better Auth CLI reads `options`
177
+ import type { BetterAuthOptions } from 'better-auth';
170
178
 
171
- export const authOptions = { emailAndPassword: { enabled: true } } satisfies BetterAuthOptions;
172
- export const auth = betterAuth(authOptions);
179
+ export default {
180
+ options: { emailAndPassword: { enabled: true } } satisfies BetterAuthOptions,
181
+ };
173
182
  ```
174
183
 
175
184
  ```ts
176
185
  // src/app.ts
177
186
  import { createNanoApp } from '@nano-app/backend';
178
- import { authOptions } from './auth';
187
+ import auth from './auth';
179
188
 
180
189
  createNanoApp({
181
- packages: { betterAuth: authOptions },
190
+ packages: { betterAuth: auth.options },
182
191
  mainPlugin: { database: { enabled: true } },
183
192
  });
184
193
  ```
@@ -193,13 +202,33 @@ npx prisma migrate dev
193
202
  - It needs the database: `packages.betterAuth` without `mainPlugin.database.enabled` fails at startup.
194
203
  - Startup also fails if your schema lacks a model the options need (`user`, `session`, ...), naming them.
195
204
  - Its endpoints are mounted with your routes, at its `basePath` (default `/api/auth`), and stay public.
205
+ - In production, Better Auth limits sign-in, sign-up and password reset to a few attempts per client IP (its `rateLimit`): see [Client IP](#client-ip).
196
206
  - Every route of yours and of the `after` plugins needs a session: without one, 401. The `before` plugins stay public (a landing, an about page).
197
207
  - Handlers and `onBefore` receive the session in `params.session` (`{ user, session }`).
198
208
  - Sign-up works as Better Auth sets it: with `emailAndPassword` anyone can sign up and reach your routes. Close it with `disableSignUp: true`, or check roles in your handlers.
199
209
  - Relate your models to `User` in the same schema, as any Prisma relation. Extra user fields go in its `user.additionalFields`.
200
210
  - With several PostgreSQL schemas, add `@@schema(...)` to the generated models.
201
211
  - Set `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` in the environment, as Better Auth asks.
212
+ - With it on, every route of the app and of the plugins mounted after it (`stage: 'after'`) needs a session: without one it answers 401 `Not authenticated`. The handlers receive it in `session`.
213
+ - `createRoute({ public: true, ... })` answers without a session; its handlers still receive `session` when the request has one. An unknown path answers 404.
214
+ - Its own endpoints, the plugins mounted before the app and the middlewares a plugin adds with `app.use` in `onMount` are not checked: a plugin that exposes something public says it in its README.
202
215
  - Better Auth is loaded only when it is on.
216
+ - nano-app creates the one instance the app uses: do not call `betterAuth()` in `src/auth.ts`. The CLI's `generate` only reads `options` of its default export.
217
+
218
+ ### Permissions
219
+
220
+ A route asks for permissions of Better Auth's [admin plugin](https://www.better-auth.com/docs/plugins/admin): add `admin()` to the `plugins` of your options, with your roles (`ac` and `roles`), and generate its fields with `npx auth generate`. A user without them gets 403:
221
+
222
+ ```ts
223
+ export default createRoute({
224
+ permissions: { notes: ['read'] },
225
+ GET: async (_req, { db }) => ({ result: await db.note.findMany() }),
226
+ });
227
+ ```
228
+
229
+ A method may ask for its own permissions instead of the route's: `POST: { handler, permissions: { notes: ['create'] } }`. A plain function takes those of the route. Without any, a session is enough. In a handler of your own, `isGranted(params, permissions)` asks Better Auth the same.
230
+
231
+ A public route does not check them. Without Better Auth the route answers anyway, with a warning at startup. Handlers and callbacks receive Better Auth's instance as `auth`, next to `db`; these params are read-only.
203
232
 
204
233
  ## Plugins
205
234
 
@@ -273,7 +302,9 @@ The framework mounts the 404 and the error handler: an unmatched request becomes
273
302
  ```ts
274
303
  throw { status: 409, message: 'Already exists' }; // → 409 { success: false, warning: false, message: "Already exists" }
275
304
  throw 'Field name is required'; // → 400 { success: false, message: "Field name is required" }
276
- throw new Error('boom'); // → 500, logged to stderr
305
+ throw new Error('boom'); // → 500 "Internal server error", logged to stderr
306
+ // A Prisma error the request caused answers without its message, which names tables and columns:
307
+ // P2025 → 404 "Not found", P2002 and P2003 → 409 "Conflict", validation → 400 "Invalid request"
277
308
  GET /unknown // → 404 { success: false, warning: false, message: "/unknown not found" }
278
309
  ```
279
310
 
package/build/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import express, { Express, Request, Request as Request$1 } from "express";
2
2
  import { CorsOptions } from "cors";
3
- import { Options } from "express-rate-limit";
4
3
  import "dotenv/config";
5
4
  import "jiti";
6
5
  import { BetterAuthOptions } from "better-auth";
@@ -9,6 +8,8 @@ type ApiResponse<T = any> = {
9
8
  status?: number;
10
9
  success?: boolean;
11
10
  result?: T;
11
+ /** Total of a list, for a table to paginate */
12
+ count?: number;
12
13
  message?: string;
13
14
  warning?: boolean;
14
15
  };
@@ -21,48 +22,61 @@ type QueryParams = {
21
22
  filter?: NanoObject;
22
23
  orderby?: NanoObject<'asc' | 'desc'>;
23
24
  select?: string;
25
+ /** Relations to load, dot notation for nested ones: `"user,posts.comments"` */
26
+ expand?: string;
24
27
  };
25
- /** Extra params of handlers and callbacks */
26
- type Params<DB = any> = Partial<NanoObject & {
27
- db?: DB;
28
+ /** Extra params of handlers and callbacks: what the app injects (options, `db`) and the framework adds */
29
+ type Params<Deps = NanoObject<any>> = Deps & {
30
+ /** Better Auth's instance, when it is on */
31
+ auth?: NanoObject;
28
32
  onBeforeResult?: NanoObject;
29
- /** `{ user, session }` of Better Auth, on the routes behind its guard */
33
+ /** `{ user, session }` of Better Auth, when the request has one */
30
34
  session?: NanoObject;
31
- }>;
35
+ };
32
36
  /** Endpoint handler */
33
- type Handler<DB> = (req: Request$1, params: Params<DB>) => ApiPromise;
34
- /** Endpoint handlers, one per HTTP method */
35
- type Handlers<DB = any> = {
36
- GET?: Handler<DB>;
37
- POST?: Handler<DB>;
38
- PUT?: Handler<DB>;
39
- DELETE?: Handler<DB>;
40
- QUERY?: Handler<DB>;
37
+ type Handler<Deps> = (req: Request$1, params: Params<Deps>) => ApiPromise;
38
+ /** A method with its own permissions, instead of those of the route */
39
+ type MethodEntry<Deps = NanoObject<any>> = {
40
+ handler: Handler<Deps>;
41
+ /** With Better Auth on, the permissions this method needs instead of the route's */
42
+ permissions?: NanoObject<string[]>;
43
+ };
44
+ /** Endpoint handlers, one per HTTP method: the handler, or it with its permissions */
45
+ type Handlers<Deps = NanoObject<any>> = {
46
+ GET?: Handler<Deps> | MethodEntry<Deps>;
47
+ POST?: Handler<Deps> | MethodEntry<Deps>;
48
+ PUT?: Handler<Deps> | MethodEntry<Deps>;
49
+ DELETE?: Handler<Deps> | MethodEntry<Deps>;
50
+ QUERY?: Handler<Deps> | MethodEntry<Deps>;
41
51
  };
42
52
  /** Callback run with the params */
43
- type Callback<DB> = (params: Params<DB>) => Promise<void>;
53
+ type Callback<Deps> = (params: Params<Deps>) => Promise<void>;
44
54
  /** Callback run before a method, with its request */
45
- type CallbackMethod<DB> = (req: Request$1, method: string, params: Params<DB>) => Promise<NanoObject>;
55
+ type CallbackMethod<Deps> = (req: Request$1, method: string, params: Params<Deps>) => Promise<NanoObject>;
46
56
  /** Callback run with the Express app */
47
- type CallbackMount<DB> = (app: Express, params: Params<DB>) => Promise<void>;
57
+ type CallbackMount<Deps> = (app: Express, params: Params<Deps>) => Promise<void>;
48
58
  /** Callbacks of an endpoint. They never receive the app: what affects the whole app goes in the plugin's onMount */
49
- type Callbacks<DB = any> = {
59
+ type Callbacks<Deps = NanoObject<any>> = {
50
60
  /** Run once, when the endpoint is mounted */
51
- onInit?: Callback<DB>;
61
+ onInit?: Callback<Deps>;
52
62
  /** Run before the endpoint is executed */
53
- onBefore?: CallbackMethod<DB>;
63
+ onBefore?: CallbackMethod<Deps>;
54
64
  };
55
65
  /** Callbacks of a plugin */
56
- type PluginCallbacks<DB = any> = {
66
+ type PluginCallbacks<Deps = NanoObject<any>> = {
57
67
  /** Run once with the Express app, after its database and before its endpoints (e.g. app-level middlewares) */
58
- onMount?: CallbackMount<DB>;
68
+ onMount?: CallbackMount<Deps>;
59
69
  };
60
70
  /** Entry of a route file (`*.route.ts`) */
61
- type RouteEntry<DB = any> = {
71
+ type RouteEntry<Deps = NanoObject<any>> = {
62
72
  /** Express route expression appended to the path (e.g. `/:id`) */
63
73
  expression?: string;
64
- callbacks?: Callbacks<DB>;
65
- } & Handlers<DB>;
74
+ /** With Better Auth on, answers without a session; the handlers still receive it when there is one */
75
+ public?: boolean;
76
+ /** With Better Auth on, the permissions of its admin plugin the user needs (`{ notes: ['read'] }`) */
77
+ permissions?: NanoObject<string[]>;
78
+ callbacks?: Callbacks<Deps>;
79
+ } & Handlers<Deps>;
66
80
  /** Generic object */
67
81
  type NanoObject<T = any> = Record<string, T>;
68
82
  /** Database client configuration. */
@@ -124,8 +138,6 @@ type PackagesConfig = {
124
138
  helmet?: boolean;
125
139
  /** Compression configuration */
126
140
  compression?: boolean;
127
- /** Rate-limit configuration */
128
- rateLimit?: Partial<Options> | false;
129
141
  /** express.json configuration */
130
142
  express?: Partial<Parameters<typeof express.json>[0]> | false;
131
143
  /** urlencoded configuration */
@@ -166,11 +178,13 @@ declare class NanoApp {
166
178
  }
167
179
  /** Creates a new NanoApp instance */
168
180
  export declare function createNanoApp(oConfig?: NanoAppConfig): NanoApp;
169
- /** Creates and types a route object. */
170
- export declare function createRoute<DB>(oRoute: RouteEntry<DB>): RouteEntry<DB>;
181
+ /** Creates a route object, typed with what the app injects in its params. */
182
+ export declare function createRoute<Deps = NanoObject<any>>(oRoute: RouteEntry<Deps>): RouteEntry<Deps>;
171
183
  /** Creates a plugin: a function of the plugin's own options that returns its configuration. */
172
184
  type Creator<Options> = (options?: Options) => PluginConfig<Options>;
173
185
  export declare function createPlugin<Options>(fnCreator: Creator<Options>): Creator<Options>;
174
- /** Creates and types the callbacks of a plugin. */
175
- export declare function createPluginCallbacks<DB>(oCallbacks: PluginCallbacks<DB>): PluginCallbacks<DB>;
176
- export { type ApiPromise, type ApiResponse, type Callbacks, type DatabaseConfig, type Handler, type Handlers, type MainPluginConfig, NanoApp, type NanoAppConfig, type NanoObject, type PackagesConfig, type Params, type PluginCallbacks, type PluginConfig, type PluginStage, type QueryParams, type Request, type RouteEntry };
186
+ /** Creates the callbacks of a plugin, typed with what it injects in its params. */
187
+ export declare function createPluginCallbacks<Deps = NanoObject<any>>(oCallbacks: PluginCallbacks<Deps>): PluginCallbacks<Deps>;
188
+ /** Whether the session's user has the permissions; always without Better Auth or permissions */
189
+ declare function isGranted({ auth, session }: Params, oPermissions?: NanoObject<string[]>): Promise<boolean>;
190
+ export { type ApiPromise, type ApiResponse, type Callbacks, type DatabaseConfig, type Handler, type Handlers, type MainPluginConfig, type MethodEntry, NanoApp, type NanoAppConfig, type NanoObject, type PackagesConfig, type Params, type PluginCallbacks, type PluginConfig, type PluginStage, type QueryParams, type Request, type RouteEntry, isGranted };
package/build/index.js CHANGED
@@ -4,7 +4,6 @@ import morgan from "morgan";
4
4
  import cors from "cors";
5
5
  import helmet from "helmet";
6
6
  import compression from "compression";
7
- import rateLimit from "express-rate-limit";
8
7
  import "dotenv/config";
9
8
  import { glob } from "glob";
10
9
  import { dirname, isAbsolute, join, resolve } from "node:path";
@@ -25,6 +24,15 @@ const HTTP_METHODS = [
25
24
  const OFFICIAL_PLUGINS = "@nano-app/plugins";
26
25
  /** Error when the database URL is missing at startup */
27
26
  const MISSING_DATABASE_URL = "[nano-app] [ERROR] The DATABASE_URL environment variable is not defined.";
27
+ /** Loopback and private networks: the proxies the backend trusts by default */
28
+ const PRIVATE_NETWORKS = [
29
+ "127.0.0.0/8",
30
+ "::1/128",
31
+ "10.0.0.0/8",
32
+ "172.16.0.0/12",
33
+ "192.168.0.0/16",
34
+ "fc00::/7"
35
+ ];
28
36
  // ../core/loader/Validator.ts
29
37
  const STORE_TYPES = ["app", "api"];
30
38
  /** Checks the consumer's files: invalid ones are warned about and discarded */
@@ -48,7 +56,11 @@ var Validator = class Validator {
48
56
  static isRoute(oEntry, sFilePath) {
49
57
  if (!Validator.isObject(oEntry, sFilePath)) return false;
50
58
  if (oEntry.expression && typeof oEntry.expression !== "string") return Validator.warn(sFilePath, "\"expression\" must be a string.");
51
- for (const sMethod of HTTP_METHODS) if (oEntry[sMethod] && typeof oEntry[sMethod] !== "function") return Validator.warn(sFilePath, `"${sMethod}" must be a function.`);
59
+ if (oEntry.public !== void 0 && typeof oEntry.public !== "boolean") return Validator.warn(sFilePath, "\"public\" must be a boolean.");
60
+ for (const sMethod of HTTP_METHODS) {
61
+ const oMethod = oEntry[sMethod];
62
+ if (oMethod && typeof oMethod !== "function" && typeof oMethod.handler !== "function") return Validator.warn(sFilePath, `"${sMethod}" must be a function or { handler, permissions }.`);
63
+ }
52
64
  if (!HTTP_METHODS.some((sMethod) => oEntry[sMethod])) return Validator.warn(sFilePath, "defines no HTTP method.");
53
65
  return true;
54
66
  }
@@ -153,28 +165,52 @@ var FileLoader = class {
153
165
  return Validator.validate(oEntries, extension);
154
166
  }
155
167
  };
168
+ // src/auth/permissions.ts
169
+ /** Whether the session's user has the permissions; always without Better Auth or permissions */
170
+ async function isGranted({ auth, session }, oPermissions) {
171
+ if (!auth || !oPermissions) return true;
172
+ const { success } = await auth.api.userHasPermission({ body: {
173
+ userId: session?.user.id,
174
+ role: session?.user.role,
175
+ permissions: oPermissions
176
+ } });
177
+ return success;
178
+ }
156
179
  // src/routes/Endpoint.ts
157
180
  /** Base class for all endpoints */
158
181
  var Endpoint = class {
159
182
  oMethods;
160
183
  oCallbacks;
161
- /** Allowed methods */
162
- constructor(oMethods, oCallbacks = {}) {
163
- this.oMethods = oMethods;
164
- this.oCallbacks = oCallbacks;
184
+ isPublic;
185
+ /** Allowed methods and the options of the route */
186
+ constructor(oMethods, { callbacks = {}, public: isPublic = false, permissions }) {
187
+ this.oMethods = Object.fromEntries(Object.entries(oMethods).map(([sMethod, oMethod]) => [sMethod, typeof oMethod === "function" ? {
188
+ handler: oMethod,
189
+ permissions
190
+ } : {
191
+ permissions,
192
+ ...oMethod
193
+ }]));
194
+ this.oCallbacks = callbacks;
195
+ this.isPublic = isPublic;
165
196
  }
166
197
  /** Handler for all methods */
167
- async mount(oParams) {
198
+ async mount(oParams, sPath) {
199
+ if (!oParams.auth && Object.values(this.oMethods).some(({ permissions }) => permissions)) console.warn(`[nano-app] [WARNING] ${sPath}: its permissions are not checked without Better Auth. Turn on packages.betterAuth.`);
168
200
  await this.oCallbacks.onInit?.(oParams);
169
201
  return this.createHandler(oParams);
170
202
  }
171
203
  /** Express handler: onBefore, then the method of the request */
172
204
  createHandler(oParams) {
173
205
  return (oReq, oRes, fnNext) => {
206
+ if (oParams.auth && !oRes.locals.session && !this.isPublic) return fnNext({
207
+ status: 401,
208
+ message: "Not authenticated"
209
+ });
174
210
  const sMethod = oReq.method.toUpperCase();
175
211
  const sHandled = HTTP_METHODS.find((sHttpMethod) => sHttpMethod === sMethod);
176
- const fnMethod = sHandled && this.oMethods[sHandled];
177
- if (!fnMethod) return fnNext({
212
+ const oMethod = sHandled && this.oMethods[sHandled];
213
+ if (!oMethod) return fnNext({
178
214
  status: 405,
179
215
  message: `Method ${sMethod} not implemented`
180
216
  });
@@ -182,7 +218,13 @@ var Endpoint = class {
182
218
  ...oParams,
183
219
  session: oRes.locals.session
184
220
  };
185
- (this.oCallbacks.onBefore?.(oReq, sMethod, oRequestParams) ?? Promise.resolve({})).then((onBeforeResult) => fnMethod(oReq, {
221
+ isGranted(oRequestParams, this.isPublic ? void 0 : oMethod.permissions).then((isAllowed) => {
222
+ if (!isAllowed) throw {
223
+ status: 403,
224
+ message: "Forbidden"
225
+ };
226
+ return this.oCallbacks.onBefore?.(oReq, sMethod, oRequestParams) ?? {};
227
+ }).then((onBeforeResult) => oMethod.handler(oReq, {
186
228
  ...oRequestParams,
187
229
  onBeforeResult
188
230
  })).then((oResponse) => {
@@ -212,12 +254,16 @@ var Router = class {
212
254
  sourceDir: this.sRoutesDir,
213
255
  extension: "route"
214
256
  });
215
- return Object.entries(this.oRoutes).map(([sFilePath, { expression, callbacks, ...methods }]) => {
257
+ return Object.entries(this.oRoutes).map(([sFilePath, { expression, callbacks, public: isPublic, permissions, ...methods }]) => {
216
258
  const sPath = this.createPath(sFilePath, expression);
217
259
  console.log(`[nano-app] [INFO] ${sPath}`);
218
260
  return {
219
261
  path: sPath,
220
- endpoint: new Endpoint(methods, callbacks)
262
+ endpoint: new Endpoint(methods, {
263
+ callbacks,
264
+ public: isPublic,
265
+ permissions
266
+ })
221
267
  };
222
268
  });
223
269
  }
@@ -227,9 +273,14 @@ var Router = class {
227
273
  }
228
274
  };
229
275
  // src/auth/BetterAuth.ts
230
- /** Better Auth on the app's Prisma client: the instance, its handler and a session guard */
276
+ /** Better Auth on the app's Prisma client: the instance, its handler and the session lookup */
231
277
  var BetterAuth = class {
232
- /** Creates the instance on the app's client, its handler and the guard */
278
+ static oAuth;
279
+ /** The instance, once the app's plugin created it */
280
+ static get() {
281
+ return this.oAuth;
282
+ }
283
+ /** Creates the instance on the app's client, its handler and the session lookup */
233
284
  static async create(oOptions, oDb) {
234
285
  const { betterAuth } = await import("better-auth");
235
286
  const { prismaAdapter } = await import("better-auth/adapters/prisma");
@@ -239,21 +290,25 @@ var BetterAuth = class {
239
290
  if (aMissing.length) throw new Error(`[nano-app] [ERROR] Better Auth needs the models ${aMissing.join(", ")} in the app's schema. Run \`npx auth generate\` and apply your schema with Prisma.`);
240
291
  const oAuth = betterAuth({
241
292
  ...oOptions,
293
+ advanced: {
294
+ ...oOptions.advanced,
295
+ ipAddress: {
296
+ trustedProxies: PRIVATE_NETWORKS,
297
+ ...oOptions.advanced?.ipAddress
298
+ }
299
+ },
242
300
  database: prismaAdapter(oDb, { provider: "postgresql" })
243
301
  });
244
- const fnGuard = (oReq, oRes, fnNext) => {
302
+ this.oAuth = oAuth;
303
+ const fnSession = (oReq, oRes, fnNext) => {
245
304
  oAuth.api.getSession({ headers: fromNodeHeaders(oReq.headers) }).then((oSession) => {
246
- if (!oSession) return fnNext({
247
- status: 401,
248
- message: "Not authenticated"
249
- });
250
- oRes.locals.session = oSession;
305
+ oRes.locals.session = oSession ?? void 0;
251
306
  fnNext();
252
307
  }).catch(fnNext);
253
308
  };
254
309
  return {
255
310
  oHandler: toNodeHandler(oAuth),
256
- fnGuard,
311
+ fnSession,
257
312
  sPath: `${oOptions.basePath ?? "/api/auth"}/{*path}`
258
313
  };
259
314
  }
@@ -405,20 +460,21 @@ var Plugin = class {
405
460
  /** Mounts its endpoints on express app */
406
461
  async mount(oApp) {
407
462
  const { name, routesDir, basePath, database, options, callbacks } = this.oConfig;
408
- const oDb = database?.enabled ? await Database.mount(name, database) : void 0;
409
- const oParams = {
410
- ...options,
411
- db: oDb
412
- };
463
+ const oDb = database?.enabled ? await Database.mount(name, database) : Database.get("app");
413
464
  if (this.oAuthOptions) {
414
465
  if (!oDb) throw new Error("[nano-app] [ERROR] packages.betterAuth needs the database of the app: set mainPlugin.database.enabled to true.");
415
- const { sPath, oHandler, fnGuard } = await BetterAuth.create(this.oAuthOptions, oDb);
466
+ const { sPath, oHandler, fnSession } = await BetterAuth.create(this.oAuthOptions, oDb);
416
467
  oApp.all(sPath, oHandler);
417
- oApp.use(fnGuard);
468
+ oApp.use(fnSession);
418
469
  }
470
+ const oParams = Object.freeze({
471
+ ...options,
472
+ db: oDb,
473
+ auth: BetterAuth.get()
474
+ });
419
475
  await callbacks?.onMount?.(oApp, oParams);
420
476
  const oRouter = new Router(routesDir, basePath);
421
- for (const { path, endpoint } of await oRouter.mount()) oApp.all(path, await endpoint.mount(oParams));
477
+ for (const { path, endpoint } of await oRouter.mount()) oApp.all(path, await endpoint.mount(oParams, path));
422
478
  }
423
479
  };
424
480
  // src/app/config.ts
@@ -428,8 +484,8 @@ const INITIAL_CONFIG = {
428
484
  port: 3e3,
429
485
  /** Application listener */
430
486
  listener: () => {},
431
- /** Express settings */
432
- settings: {},
487
+ /** Express settings: the client IP comes from `X-Forwarded-For` only through private proxies */
488
+ settings: { "trust proxy": PRIVATE_NETWORKS },
433
489
  /** Initial middlewares */
434
490
  packages: {
435
491
  morgan: {
@@ -449,10 +505,6 @@ const INITIAL_CONFIG = {
449
505
  ] },
450
506
  helmet: true,
451
507
  compression: true,
452
- rateLimit: {
453
- max: 1e3,
454
- message: "Too many requests, please try again later."
455
- },
456
508
  express: { limit: "10kb" },
457
509
  urlencoded: { extended: false },
458
510
  betterAuth: false
@@ -500,7 +552,26 @@ function mergeConfig(oConfig = {}) {
500
552
  };
501
553
  }
502
554
  // src/app/errors.ts
503
- /** Whether anything thrown is an object, read as a HandlerError */
555
+ /** Prisma errors the request caused; their own messages name tables and columns */
556
+ const PRISMA_ERRORS = {
557
+ P2025: {
558
+ status: 404,
559
+ message: "Not found"
560
+ },
561
+ P2002: {
562
+ status: 409,
563
+ message: "Conflict"
564
+ },
565
+ P2003: {
566
+ status: 409,
567
+ message: "Conflict"
568
+ },
569
+ PrismaClientValidationError: {
570
+ status: 400,
571
+ message: "Invalid request"
572
+ }
573
+ };
574
+ /** Whether anything thrown is an object, read as a HandlerError or a Prisma error */
504
575
  function isHandlerError(oError) {
505
576
  return typeof oError === "object" && oError !== null;
506
577
  }
@@ -513,13 +584,15 @@ function normalize(oError) {
513
584
  message: oError
514
585
  }
515
586
  };
516
- const { status, message, warning } = isHandlerError(oError) ? oError : {};
587
+ const oThrown = isHandlerError(oError) ? oError : {};
588
+ const { status, message, warning } = PRISMA_ERRORS[oThrown.code ?? oThrown.name ?? ""] ?? oThrown;
589
+ const iStatus = status ?? 500;
517
590
  return {
518
- status: status ?? 500,
591
+ status: iStatus,
519
592
  body: {
520
593
  success: false,
521
594
  warning: warning ?? false,
522
- message: message || "Internal server error"
595
+ message: iStatus < 500 && message || "Internal server error"
523
596
  }
524
597
  };
525
598
  }
@@ -585,7 +658,6 @@ var NanoApp = class {
585
658
  if (oConfig.cors) this.oApp.use(cors(oConfig.cors));
586
659
  if (oConfig.helmet) this.oApp.use(helmet());
587
660
  if (oConfig.compression) this.oApp.use(compression());
588
- if (oConfig.rateLimit) this.oApp.use(rateLimit(oConfig.rateLimit));
589
661
  if (oConfig.express) this.oApp.use(express.json(oConfig.express));
590
662
  if (oConfig.urlencoded) this.oApp.use(express.urlencoded(oConfig.urlencoded));
591
663
  }
@@ -595,15 +667,15 @@ var NanoApp = class {
595
667
  function createNanoApp(oConfig) {
596
668
  return new NanoApp(oConfig);
597
669
  }
598
- /** Creates and types a route object. */
670
+ /** Creates a route object, typed with what the app injects in its params. */
599
671
  function createRoute(oRoute) {
600
672
  return oRoute;
601
673
  }
602
674
  function createPlugin(fnCreator) {
603
675
  return fnCreator;
604
676
  }
605
- /** Creates and types the callbacks of a plugin. */
677
+ /** Creates the callbacks of a plugin, typed with what it injects in its params. */
606
678
  function createPluginCallbacks(oCallbacks) {
607
679
  return oCallbacks;
608
680
  }
609
- export { NanoApp, createNanoApp, createPlugin, createPluginCallbacks, createRoute };
681
+ export { NanoApp, createNanoApp, createPlugin, createPluginCallbacks, createRoute, isGranted };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nano-app/backend",
3
- "version": "0.12.4",
3
+ "version": "0.13.0",
4
4
  "description": "Express server with file-convention routes, plugins and an optional PostgreSQL layer",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
@@ -47,7 +47,6 @@
47
47
  "cors": "^2.8.6",
48
48
  "dotenv": "^18.0.9",
49
49
  "express": "^5.3.0",
50
- "express-rate-limit": "^8.7.2",
51
50
  "glob": "^14.0.3",
52
51
  "helmet": "^8.3.0",
53
52
  "jiti": "^2.7.0",