@nano-app/backend 0.19.0 → 0.20.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 +30 -31
- package/build/index.d.ts +87 -40
- package/build/index.js +388 -287
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -85,11 +85,13 @@ Every option is optional.
|
|
|
85
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
|
| `routesDir` | `"src/routes"` | Folder of your routes, relative to `cwd`. |
|
|
88
|
-
| `database` | none | Your Drizzle
|
|
88
|
+
| `database` | none | Your Drizzle relations and how its tables are applied: see [Database](#database). |
|
|
89
89
|
| `auth` | none | Better Auth options: see [Better Auth](#better-auth). |
|
|
90
90
|
| `plugins` | `{}` | Plugins mounted `before` and `after` your routes, in order: see [Plugins](#plugins). |
|
|
91
91
|
|
|
92
|
-
`packages` accepts `morgan`, `cors`, `helmet`, `compression`, `express` (`express.json` options) and `urlencoded`. Each one you set replaces its default whole and the others keep theirs; pass `false` to disable one. `settings` works the same way, key by key.
|
|
92
|
+
`packages` accepts `morgan`, `cors`, `helmet`, `rateLimit`, `compression`, `express` (`express.json` options) and `urlencoded`. Each one you set replaces its default whole and the others keep theirs; pass `false` to disable one. `settings` works the same way, key by key.
|
|
93
|
+
|
|
94
|
+
`rateLimit` is off by default. With `{ capacity, refillPerSecond, message }` (defaults `100`, `1` and a generic message) it limits every request per client IP, Better Auth's endpoints too: each request spends a token of the client's bucket, which refills over time; without one it answers `429` with `message` and `Retry-After`. The addresses of an IPv6 `/56` are one client, and the buckets live in the memory of each process.
|
|
93
95
|
|
|
94
96
|
`compression` is off by default: the proxy in front of the app (an ingress, nginx) usually compresses the answers. Turn it on with `packages: { compression: true }` when nothing does.
|
|
95
97
|
|
|
@@ -106,7 +108,7 @@ createNanoApp({
|
|
|
106
108
|
|
|
107
109
|
### Client IP
|
|
108
110
|
|
|
109
|
-
The [
|
|
111
|
+
The [rate limit](#configuration) 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`.
|
|
110
112
|
|
|
111
113
|
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`.
|
|
112
114
|
|
|
@@ -116,7 +118,7 @@ By default the backend reads that header only when the request comes from loopba
|
|
|
116
118
|
|
|
117
119
|
## Database
|
|
118
120
|
|
|
119
|
-
Off until the app passes its
|
|
121
|
+
Off until the app passes its relations: nano-app creates a [Drizzle](https://orm.drizzle.team) client of them on `DATABASE_URL` and passes it to your handlers as `db`. It changes your tables only when you ask: `push` or `migrate`. Without them, applying the schema (`drizzle-kit push`, `drizzle-kit generate` and `migrate`) is up to you.
|
|
120
122
|
|
|
121
123
|
```bash
|
|
122
124
|
pnpm add drizzle-orm pg
|
|
@@ -135,10 +137,11 @@ export const users = pgTable('users', {
|
|
|
135
137
|
|
|
136
138
|
```ts
|
|
137
139
|
// src/app.ts — DATABASE_URL comes from the environment (or a .env file)
|
|
140
|
+
import { defineRelations } from 'drizzle-orm';
|
|
138
141
|
import { createNanoApp } from '@nano-app/backend';
|
|
139
142
|
import * as schema from './db/schema';
|
|
140
143
|
|
|
141
|
-
createNanoApp({ database: { schema } });
|
|
144
|
+
createNanoApp({ database: { relations: defineRelations(schema) } });
|
|
142
145
|
```
|
|
143
146
|
|
|
144
147
|
```ts
|
|
@@ -150,9 +153,10 @@ export default createRoute({
|
|
|
150
153
|
});
|
|
151
154
|
```
|
|
152
155
|
|
|
153
|
-
- `database.
|
|
154
|
-
- `database.push: true` applies the
|
|
155
|
-
- `database.
|
|
156
|
+
- `database.relations` is what `defineRelations` gives of your tables, with their relations when you relate them. A `pgSchema` the tables use is exported from the module too, so drizzle-kit creates it. The rest of `database` goes to Drizzle as its options.
|
|
157
|
+
- `database.push: true` applies the tables at startup with drizzle-kit's push (`pnpm add drizzle-kit`), only to the Postgres schemas they use. A change that would lose data stops the startup.
|
|
158
|
+
- `database.migrate` is the folder `drizzle-kit generate` writes: its pending migrations are applied at startup. With both, the migrations run first, then the push.
|
|
159
|
+
- One startup at a time changes the database: a Postgres lock covers the migrations and the push of every database, across replicas.
|
|
156
160
|
- One pool serves the app, Better Auth and every plugin with a database. It connects on its first query: a missing table fails that query, not the startup.
|
|
157
161
|
- Startup fails if `DATABASE_URL` is missing.
|
|
158
162
|
- Each `nano_*` Postgres schema is reserved for the plugin of its name (`nano_auth` for Better Auth): a table of yours in one fails the startup.
|
|
@@ -175,13 +179,13 @@ createNanoApp({ auth: { emailAndPassword: { enabled: true } } });
|
|
|
175
179
|
```
|
|
176
180
|
|
|
177
181
|
- It uses the pool of `DATABASE_URL`, with or without a database of the app: startup fails if it is missing.
|
|
178
|
-
- Its tables live in the Postgres schema `nano_auth
|
|
182
|
+
- Its tables live in the Postgres schema `nano_auth`, and Better Auth decides them from its options (plugins, `user.additionalFields`).
|
|
183
|
+
- `auth.database` says how they are applied at startup, `{ push: true }` by default: `push` creates them with Better Auth's migrations and adds what new options need, never dropping anything; `migrate` is a folder of Drizzle migrations of them (`npx auth generate --adapter drizzle`, then `drizzle-kit generate`), applied in `nano_auth`.
|
|
179
184
|
- Its endpoints are mounted before your routes, at its `basePath` (default `/api/auth`), and stay public.
|
|
180
185
|
- 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).
|
|
181
186
|
- With it on, every route of the app and of every plugin needs a session: without one it answers 401 `Not authenticated`.
|
|
182
187
|
- `createRoute({ public: true, ... })` answers without a session. An unknown path answers 404.
|
|
183
188
|
- Handlers and `onBefore` receive the session in `params.session` (`{ user, session }`), also on a public route when the request has one.
|
|
184
|
-
- The middlewares a plugin adds with `app.use` in `onMount` are not checked: a plugin that exposes something public says it in its README.
|
|
185
189
|
- 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.
|
|
186
190
|
- Extra user fields go in its `user.additionalFields`.
|
|
187
191
|
- Set `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` in the environment, as Better Auth asks.
|
|
@@ -199,9 +203,9 @@ export default createRoute({
|
|
|
199
203
|
});
|
|
200
204
|
```
|
|
201
205
|
|
|
202
|
-
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.
|
|
206
|
+
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.session, permissions)` asks Better Auth the same.
|
|
203
207
|
|
|
204
|
-
A public route does not check them. Without Better Auth the route answers anyway,
|
|
208
|
+
A public route does not check them. Without Better Auth nothing checks them: the route answers anyway, so turn on `auth` before you rely on them. The params of handlers and callbacks are read-only.
|
|
205
209
|
|
|
206
210
|
## Plugins
|
|
207
211
|
|
|
@@ -213,9 +217,9 @@ import { createPlugin } from '@nano-app/backend';
|
|
|
213
217
|
import { dirname, resolve } from 'node:path';
|
|
214
218
|
import { fileURLToPath } from 'node:url';
|
|
215
219
|
|
|
216
|
-
export default createPlugin((
|
|
220
|
+
export default createPlugin(() => ({
|
|
217
221
|
name: 'hello',
|
|
218
|
-
basePath:
|
|
222
|
+
basePath: '/hello',
|
|
219
223
|
routesDir: resolve(dirname(fileURLToPath(import.meta.url)), 'routes'),
|
|
220
224
|
}));
|
|
221
225
|
```
|
|
@@ -237,36 +241,31 @@ import hello from './plugins/hello';
|
|
|
237
241
|
createNanoApp({ plugins: { before: [hello()] } });
|
|
238
242
|
```
|
|
239
243
|
|
|
240
|
-
`
|
|
244
|
+
A plugin may also make routes in code with `routes`, by path, beside or instead of `routesDir`: they are checked as the files, and a path in both takes the one in `routes`. Useful when its routes come from its options, as one per table with its own permissions:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
routes: Object.fromEntries(
|
|
248
|
+
models.map(sModel => [`/${sModel}`, createRoute({ GET: { handler, permissions: { [sModel]: ['read'] } } })]),
|
|
249
|
+
),
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`createPlugin` types the options of that plugin, and adds `basePath` to them: each plugin chooses its default, and the app moves it with `hello({ basePath: '/hi' })`. The app also chooses whether it mounts `before` or `after` its routes: a route of a `before` plugin takes its path from the app's routes, and an `after` plugin only gets what they leave. What the plugin puts in `options` reaches every callback and handler of its routes in `params`.
|
|
241
253
|
|
|
242
254
|
### Callbacks
|
|
243
255
|
|
|
244
|
-
Two levels
|
|
256
|
+
Two levels: the plugin's runs once for the whole plugin, the endpoint's for its route.
|
|
245
257
|
|
|
246
258
|
| Level | Callback | Runs | Receives | Use |
|
|
247
259
|
|---|---|---|---|---|
|
|
248
|
-
| Plugin (`callbacks` in its config) | `onMount` | Once, after its database and before its endpoints | `(
|
|
260
|
+
| Plugin (`callbacks` in its config) | `onMount` | Once, after its database and before its endpoints | `(params)` | Shared setup (a client, a job) |
|
|
249
261
|
| Endpoint (`callbacks` in `createRoute`) | `onInit` | Once, when the endpoint is mounted | `(params)` | Setup of that route |
|
|
250
262
|
| Endpoint | `onBefore` | Before every request the endpoint handles | `(req, method, params)` | Validate or load; its return value reaches the handler as `params.onBeforeResult` (`{}` without `onBefore`) |
|
|
251
263
|
|
|
252
|
-
```ts
|
|
253
|
-
export default createPlugin((options?: { publicPaths?: string[] }) => ({
|
|
254
|
-
name: 'guard',
|
|
255
|
-
routesDir: resolve(dirname(fileURLToPath(import.meta.url)), 'routes'),
|
|
256
|
-
options: { publicPaths: options?.publicPaths ?? [] },
|
|
257
|
-
callbacks: {
|
|
258
|
-
onMount: async (app, { publicPaths }) => {
|
|
259
|
-
app.use((req, _res, next) => next(publicPaths.includes(req.path) ? undefined : { status: 401, message: 'Unauthorized' }));
|
|
260
|
-
},
|
|
261
|
-
},
|
|
262
|
-
}));
|
|
263
|
-
```
|
|
264
|
-
|
|
265
264
|
The endpoints of one plugin have no guaranteed order: one route's `onInit` must not depend on another's.
|
|
266
265
|
|
|
267
266
|
Everything is mounted on one Express application, in this order: base middlewares → Better Auth → `before` plugins → your routes → `after` plugins → 404 → error handler.
|
|
268
267
|
|
|
269
|
-
A plugin with a database passes its own `database` (`
|
|
268
|
+
A plugin with a database passes its own `database` (`relations`, `push`, `migrate`, as the app's). Only it may keep tables in the Postgres schema `nano_<its name>`: a table in the `nano_*` schema of another fails the startup. Without a database of its own, a plugin receives the app's `db`. See [`@nano-app/plugins`](https://github.com/Nano-Nathan/nano-app/blob/main/plugins/README.md#database).
|
|
270
269
|
|
|
271
270
|
## Errors
|
|
272
271
|
|
package/build/index.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import express, { Express, Request, Request as Request$1 } from "express";
|
|
2
|
-
import { CorsOptions } from "cors";
|
|
3
|
-
import "dotenv/config";
|
|
4
|
-
import "jiti";
|
|
5
1
|
import { z } from "zod";
|
|
2
|
+
import express, { Express, Request, Request as Request$1, RequestHandler } from "express";
|
|
3
|
+
import "dotenv/config";
|
|
4
|
+
import { CorsOptions } from "cors";
|
|
6
5
|
import { BetterAuthOptions } from "better-auth";
|
|
6
|
+
import { AnyRelations } from "drizzle-orm";
|
|
7
|
+
import { DrizzlePgConfig } from "drizzle-orm/pg-core";
|
|
8
|
+
/** Generic object */
|
|
9
|
+
type NanoObject<T = any> = Record<string, T>;
|
|
7
10
|
/** API response */
|
|
8
11
|
type ApiResponse<T = any> = {
|
|
9
12
|
status?: number;
|
|
@@ -26,10 +29,34 @@ type QueryParams = {
|
|
|
26
29
|
limit?: number;
|
|
27
30
|
offset?: number;
|
|
28
31
|
};
|
|
32
|
+
/** Error of the framework, with the status a handler answers */
|
|
33
|
+
declare class NanoError extends Error {
|
|
34
|
+
status?: number;
|
|
35
|
+
/** Error with its message, and the status a handler answers (500 without one) */
|
|
36
|
+
constructor(sMessage: string, iStatus?: number);
|
|
37
|
+
/** Prefix of the framework's messages, shown before the message */
|
|
38
|
+
get name(): string;
|
|
39
|
+
}
|
|
40
|
+
/** What every database has, whatever its client */
|
|
41
|
+
type DatabaseOptions = {
|
|
42
|
+
/** Folder of its migrations, applied at startup */
|
|
43
|
+
migrate?: string;
|
|
44
|
+
/** Applies its tables at startup as they are declared, failing on data loss; not with `migrate` */
|
|
45
|
+
push?: boolean;
|
|
46
|
+
};
|
|
47
|
+
/** Drizzle's options for its client: its tables and how they relate, from `defineRelations` */
|
|
48
|
+
type DrizzleConfig = DrizzlePgConfig<AnyRelations> & {
|
|
49
|
+
relations: AnyRelations;
|
|
50
|
+
};
|
|
51
|
+
/** Database of a plugin or of the app: the options of its client and what every database has */
|
|
52
|
+
type DatabaseConfig = DatabaseOptions & DrizzleConfig;
|
|
53
|
+
/** Better Auth options; `database` says how its tables are applied, `push` by default */
|
|
54
|
+
type AuthOptions = Omit<BetterAuthOptions, 'database'> & {
|
|
55
|
+
/** How its tables are applied at startup: `migrate` or `push`. Defaults to `{ push: true }` */
|
|
56
|
+
database?: DatabaseOptions;
|
|
57
|
+
};
|
|
29
58
|
/** Extra params of handlers and callbacks: what the app injects (options, `db`) and the framework adds */
|
|
30
59
|
type Params<Deps = NanoObject<any>> = Deps & {
|
|
31
|
-
/** Better Auth's instance, when it is on */
|
|
32
|
-
auth?: NanoObject;
|
|
33
60
|
/** `{ user, session }` of Better Auth, when the request has one */
|
|
34
61
|
session?: NanoObject;
|
|
35
62
|
};
|
|
@@ -57,8 +84,8 @@ type Handlers<Deps = NanoObject<any>> = {
|
|
|
57
84
|
type Callback<Deps> = (params: Params<Deps>) => Promise<void>;
|
|
58
85
|
/** Callback run before a method, with its request */
|
|
59
86
|
type CallbackMethod<Deps> = (req: Request$1, method: string, params: Params<Deps>) => Promise<NanoObject>;
|
|
60
|
-
/** Callback run with the
|
|
61
|
-
type CallbackMount<Deps> = (
|
|
87
|
+
/** Callback run once with the params */
|
|
88
|
+
type CallbackMount<Deps> = (params: Params<Deps>) => Promise<void>;
|
|
62
89
|
/** Callbacks of an endpoint; what affects the whole app goes in the plugin's onMount */
|
|
63
90
|
type Callbacks<Deps = NanoObject<any>> = {
|
|
64
91
|
/** Run once, when the endpoint is mounted */
|
|
@@ -68,46 +95,32 @@ type Callbacks<Deps = NanoObject<any>> = {
|
|
|
68
95
|
};
|
|
69
96
|
/** Callbacks of a plugin */
|
|
70
97
|
type PluginCallbacks<Deps = NanoObject<any>> = {
|
|
71
|
-
/** Run once
|
|
98
|
+
/** Run once, after its database and before its endpoints: the plugin's setup */
|
|
72
99
|
onMount?: CallbackMount<Deps>;
|
|
73
100
|
};
|
|
74
|
-
/**
|
|
75
|
-
type
|
|
76
|
-
/** Express route expression appended to the path (e.g. `/:id`) */
|
|
77
|
-
expression?: string;
|
|
101
|
+
/** Endpoint params */
|
|
102
|
+
type EndpointParams<Deps = NanoObject<any>> = {
|
|
78
103
|
/** With Better Auth on, answers without a session; the handlers still receive it when there is one */
|
|
79
104
|
public?: boolean;
|
|
80
105
|
/** With Better Auth on, the permissions of its admin plugin the user needs (`{ notes: ['read'] }`) */
|
|
81
106
|
permissions?: NanoObject<string[]>;
|
|
82
107
|
callbacks?: Callbacks<Deps>;
|
|
83
108
|
} & Handlers<Deps>;
|
|
84
|
-
/**
|
|
85
|
-
type
|
|
86
|
-
/**
|
|
87
|
-
|
|
88
|
-
/** Error of the framework, with the status a handler answers */
|
|
89
|
-
declare class NanoError extends Error {
|
|
90
|
-
status?: number;
|
|
91
|
-
/** Error with its message, and the status a handler answers (500 without one) */
|
|
92
|
-
constructor(sMessage: string, iStatus?: number);
|
|
93
|
-
/** Prefix of the framework's messages, shown before the message */
|
|
94
|
-
get name(): string;
|
|
95
|
-
}
|
|
96
|
-
/** Database of a plugin or of the app */
|
|
97
|
-
type DatabaseConfig = {
|
|
98
|
-
/** Its Drizzle schema: the module of its tables and `relations` */
|
|
99
|
-
schema: NanoObject;
|
|
100
|
-
/** Applies the schema at startup with drizzle-kit's push, failing on data loss. Defaults to false */
|
|
101
|
-
push?: boolean;
|
|
102
|
-
/** Folder of the migrations `drizzle-kit generate` writes, applied at startup */
|
|
103
|
-
migrations?: string;
|
|
109
|
+
/** Route params */
|
|
110
|
+
type RouteParams = {
|
|
111
|
+
/** Express route expression appended to the path (e.g. `/:id`) */
|
|
112
|
+
expression?: string;
|
|
104
113
|
};
|
|
114
|
+
/** Entry of a route file (`*.route.ts`) */
|
|
115
|
+
type RouteEntry<Deps = NanoObject<any>> = RouteParams & EndpointParams<Deps>;
|
|
105
116
|
/** Plugin configuration: a group of endpoints */
|
|
106
117
|
type PluginConfig<Options = NanoObject> = {
|
|
107
118
|
/** Name of the plugin, also the name of its database when it has one */
|
|
108
119
|
name: string;
|
|
109
120
|
/** Directory used to discover the `*.route.ts` files */
|
|
110
|
-
routesDir
|
|
121
|
+
routesDir?: string;
|
|
122
|
+
/** Routes made in code, by path (`{ '/notes': createRoute({...}) }`), validated as the files */
|
|
123
|
+
routes?: NanoObject<RouteEntry<any>>;
|
|
111
124
|
/** Base path prepended to every endpoint, e.g. "/auth" (no trailing slash). Defaults to none */
|
|
112
125
|
basePath?: string;
|
|
113
126
|
/** Drizzle schema of the plugin's database, mounted before its endpoints */
|
|
@@ -117,6 +130,11 @@ type PluginConfig<Options = NanoObject> = {
|
|
|
117
130
|
/** Plugin callbacks */
|
|
118
131
|
callbacks?: PluginCallbacks;
|
|
119
132
|
};
|
|
133
|
+
/** Options every plugin takes, besides its own */
|
|
134
|
+
type PluginOptions = {
|
|
135
|
+
/** Base path of its endpoints, over the plugin's default */
|
|
136
|
+
basePath?: string;
|
|
137
|
+
};
|
|
120
138
|
/** What PluginManager reads: the app's plugin and the ones around it */
|
|
121
139
|
type PluginManagerConfig = {
|
|
122
140
|
/** Folder of the app's routes. Defaults to "src/routes" */
|
|
@@ -131,6 +149,15 @@ type PluginManagerConfig = {
|
|
|
131
149
|
after?: PluginConfig[];
|
|
132
150
|
};
|
|
133
151
|
};
|
|
152
|
+
/** Rate limit per client IP: a bucket of tokens per client, refilled over time */
|
|
153
|
+
type RateLimitConfig = {
|
|
154
|
+
/** Tokens of a full bucket. Defaults to 100 */
|
|
155
|
+
capacity?: number;
|
|
156
|
+
/** Tokens refilled per second. Defaults to 1 */
|
|
157
|
+
refillPerSecond?: number;
|
|
158
|
+
/** Message of the 429 */
|
|
159
|
+
message?: string;
|
|
160
|
+
};
|
|
134
161
|
/** Configuration of the framework's built-in packages */
|
|
135
162
|
type PackagesConfig = {
|
|
136
163
|
/** Morgan configuration */
|
|
@@ -142,6 +169,8 @@ type PackagesConfig = {
|
|
|
142
169
|
};
|
|
143
170
|
/** CORS configuration */
|
|
144
171
|
cors?: CorsOptions | false;
|
|
172
|
+
/** Rate limit per client IP, off by default; it covers every request, Better Auth's too */
|
|
173
|
+
rateLimit?: RateLimitConfig | false;
|
|
145
174
|
/** Helmet configuration */
|
|
146
175
|
helmet?: boolean;
|
|
147
176
|
/** Compression configuration */
|
|
@@ -164,8 +193,28 @@ type ServerConfig = {
|
|
|
164
193
|
/** Better Auth options: with them, every route needs a session unless it is `public` */
|
|
165
194
|
auth?: AuthOptions;
|
|
166
195
|
};
|
|
196
|
+
/** A route NanoApp registers */
|
|
197
|
+
type Mount = {
|
|
198
|
+
/** Express path of the route */
|
|
199
|
+
path: string;
|
|
200
|
+
/** Its handler */
|
|
201
|
+
handler: RequestHandler;
|
|
202
|
+
};
|
|
167
203
|
/** Application configuration: the server and its plugins */
|
|
168
204
|
type NanoAppConfig = ServerConfig & PluginManagerConfig;
|
|
205
|
+
/** Better Auth of the app: its tables in `nano_auth`, its endpoints and the access of each request */
|
|
206
|
+
declare abstract class Auth {
|
|
207
|
+
private static oAuth?;
|
|
208
|
+
private static fnHeaders?;
|
|
209
|
+
/** Mounts its database and creates the instance; its endpoints, none without options */
|
|
210
|
+
static init(oOptions?: AuthOptions): Promise<Mount[]>;
|
|
211
|
+
/** The session of a request; 401 without one unless public, 403 without the permissions */
|
|
212
|
+
static guard(oReq: Request$1, isPublic: boolean, oPermissions?: NanoObject<string[]>): Promise<NanoObject | undefined>;
|
|
213
|
+
/** The session of a request, when it has one */
|
|
214
|
+
private static session;
|
|
215
|
+
/** Whether the user of a session has the permissions; always without Better Auth or permissions */
|
|
216
|
+
static isGranted(oSession?: NanoObject, oPermissions?: NanoObject<string[]>): Promise<boolean>;
|
|
217
|
+
}
|
|
169
218
|
/** Main NanoApp class */
|
|
170
219
|
declare class NanoApp {
|
|
171
220
|
protected oApp: Express;
|
|
@@ -175,8 +224,6 @@ declare class NanoApp {
|
|
|
175
224
|
private start;
|
|
176
225
|
/** Initializes the application in the right order */
|
|
177
226
|
private initialize;
|
|
178
|
-
/** Adds the packages according to the configuration */
|
|
179
|
-
private setPackages;
|
|
180
227
|
}
|
|
181
228
|
/** Creates a new NanoApp instance */
|
|
182
229
|
export declare function createNanoApp(oConfig?: NanoAppConfig): NanoApp;
|
|
@@ -184,9 +231,9 @@ export declare function createNanoApp(oConfig?: NanoAppConfig): NanoApp;
|
|
|
184
231
|
export declare function createRoute<Deps = NanoObject<any>>(oRoute: RouteEntry<Deps>): RouteEntry<Deps>;
|
|
185
232
|
/** Creates a plugin: a function of the plugin's own options that returns its configuration. */
|
|
186
233
|
type Creator<Options> = (options?: Options) => PluginConfig<Options>;
|
|
187
|
-
export declare function createPlugin<Options>(fnCreator: Creator<Options>):
|
|
234
|
+
export declare function createPlugin<Options extends NanoObject = NanoObject>(fnCreator: Creator<Options>): (options?: Options & PluginOptions) => PluginConfig<Options>;
|
|
188
235
|
/** Creates the callbacks of a plugin, typed with what it injects in its params. */
|
|
189
236
|
export declare function createPluginCallbacks<Deps = NanoObject<any>>(oCallbacks: PluginCallbacks<Deps>): PluginCallbacks<Deps>;
|
|
190
|
-
/** Whether the session
|
|
191
|
-
declare
|
|
192
|
-
export { type ApiPromise, type ApiResponse, type Callbacks, type DatabaseConfig, type Handler, type HandlerParams, type Handlers, type MethodEntry, NanoApp, type NanoAppConfig, NanoError, type NanoObject, type PackagesConfig, type Params, type PluginCallbacks, type PluginConfig, type PluginManagerConfig, type QueryParams, type Request, type RouteEntry, type ServerConfig
|
|
237
|
+
/** Whether the user of a session has the permissions, asked to Better Auth */
|
|
238
|
+
export declare const isGranted: typeof Auth.isGranted;
|
|
239
|
+
export { type ApiPromise, type ApiResponse, type Callbacks, type DatabaseConfig, type Handler, type HandlerParams, type Handlers, type MethodEntry, NanoApp, type NanoAppConfig, NanoError, type NanoObject, type PackagesConfig, type Params, type PluginCallbacks, type PluginConfig, type PluginManagerConfig, type PluginOptions, type QueryParams, type Request, type RouteEntry, type ServerConfig };
|
package/build/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import express from "express";
|
|
2
|
-
import morgan from "morgan";
|
|
3
|
-
import cors from "cors";
|
|
4
|
-
import helmet from "helmet";
|
|
5
|
-
import compression from "compression";
|
|
6
|
-
import "dotenv/config";
|
|
7
1
|
import { glob } from "glob";
|
|
8
2
|
import { isAbsolute, resolve } from "node:path";
|
|
9
3
|
import { existsSync } from "node:fs";
|
|
10
4
|
import { createJiti } from "jiti";
|
|
11
5
|
import { z } from "zod";
|
|
6
|
+
import express from "express";
|
|
7
|
+
import "dotenv/config";
|
|
8
|
+
import morgan from "morgan";
|
|
9
|
+
import cors from "cors";
|
|
10
|
+
import helmet from "helmet";
|
|
11
|
+
import compression from "compression";
|
|
12
|
+
import { ipKeyGenerator } from "express-rate-limit";
|
|
12
13
|
// ../core/utils/NanoError.ts
|
|
13
14
|
/** Error of the framework, with the status a handler answers */
|
|
14
15
|
var NanoError = class extends Error {
|
|
@@ -39,11 +40,21 @@ var FileReader = class {
|
|
|
39
40
|
return sAbsolutePath;
|
|
40
41
|
}
|
|
41
42
|
};
|
|
43
|
+
// ../core/utils/Validator.ts
|
|
44
|
+
/** Checks the consumer's files; an invalid file throws a `NanoError` */
|
|
45
|
+
var Validator = class {
|
|
46
|
+
/** Checks a file with a schema: an invalid one throws with its key */
|
|
47
|
+
static validate(oSchema, oFile, sKey) {
|
|
48
|
+
const oResult = oSchema.safeParse(oFile);
|
|
49
|
+
if (!oResult.success) throw new NanoError(`${sKey}:\n${z.prettifyError(oResult.error)}`);
|
|
50
|
+
return oResult.data;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
42
53
|
// ../core/files/FileLoader.ts
|
|
43
54
|
/** Finds and loads the consumer's files */
|
|
44
55
|
var FileLoader = class {
|
|
45
|
-
/**
|
|
46
|
-
static async findMany(sExtension, sSourceDir) {
|
|
56
|
+
/** The `*.<extension>.ts` files of a folder by subpath, checked with a schema */
|
|
57
|
+
static async findMany(sExtension, oSchema, sSourceDir) {
|
|
47
58
|
const sSrcDir = (sSourceDir ?? `src/${sExtension}s`).replace(/^\.\//, "").replace(/\/$/, "");
|
|
48
59
|
const aFilePaths = await glob(`${sSrcDir}/**/*.${sExtension}.{ts,js}`, {
|
|
49
60
|
posix: true,
|
|
@@ -51,29 +62,13 @@ var FileLoader = class {
|
|
|
51
62
|
});
|
|
52
63
|
const oEntries = {};
|
|
53
64
|
for (const sFilePath of aFilePaths) {
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
oEntries[sKey] =
|
|
65
|
+
const sKey = sFilePath.slice(sSrcDir.length).replace(new RegExp(`\\.${sExtension}\\.(ts|js)$`), "");
|
|
66
|
+
const oFile = await FileReader.loadModule(sFilePath, { default: true });
|
|
67
|
+
oEntries[sKey] = Validator.validate(oSchema, oFile, sKey);
|
|
57
68
|
}
|
|
58
69
|
return oEntries;
|
|
59
70
|
}
|
|
60
71
|
};
|
|
61
|
-
// ../core/utils/Validator.ts
|
|
62
|
-
/** Checks values of the framework and the consumer's files; an invalid file throws a `NanoError` */
|
|
63
|
-
var Validator = class {
|
|
64
|
-
/** Whether a value is a plain object */
|
|
65
|
-
static isObject(oValue) {
|
|
66
|
-
return Object.prototype.toString.call(oValue) === "[object Object]";
|
|
67
|
-
}
|
|
68
|
-
/** Checks every entry with a zod schema (`ROUTE`, `STORE`): the first invalid one throws with its key */
|
|
69
|
-
static validate(oEntries, oSchema) {
|
|
70
|
-
return Object.fromEntries(Object.entries(oEntries).map(([sKey, oEntry]) => {
|
|
71
|
-
const oResult = oSchema.safeParse(oEntry);
|
|
72
|
-
if (!oResult.success) throw new NanoError(`${sKey}:\n${z.prettifyError(oResult.error)}`);
|
|
73
|
-
return [sKey, oResult.data];
|
|
74
|
-
}));
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
72
|
// ../core/utils/Logger.ts
|
|
78
73
|
/** Messages of the framework on the console */
|
|
79
74
|
var Logger = class Logger {
|
|
@@ -98,17 +93,7 @@ var Logger = class Logger {
|
|
|
98
93
|
return oDate.toISOString().slice(0, 19).replace("T", " ");
|
|
99
94
|
}
|
|
100
95
|
};
|
|
101
|
-
//
|
|
102
|
-
/** Paths of the app's pages */
|
|
103
|
-
var Paths = class {
|
|
104
|
-
/** Whether a path matches a pattern: exact (`/admin`), or the path and all below it (`/admin/*`) */
|
|
105
|
-
static matches(sPattern, sPath) {
|
|
106
|
-
if (!sPattern.endsWith("/*")) return sPattern === sPath;
|
|
107
|
-
const sBase = sPattern.slice(0, -2);
|
|
108
|
-
return sPath === sBase || sPath.startsWith(`${sBase}/`);
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
// ../core/const.ts
|
|
96
|
+
// src/const.ts
|
|
112
97
|
/** Loopback and private networks: the proxies the backend trusts by default */
|
|
113
98
|
const PRIVATE_NETWORKS = [
|
|
114
99
|
"127.0.0.0/8",
|
|
@@ -118,179 +103,263 @@ const PRIVATE_NETWORKS = [
|
|
|
118
103
|
"192.168.0.0/16",
|
|
119
104
|
"fc00::/7"
|
|
120
105
|
];
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
path: z.string(),
|
|
156
|
-
routes: z.array(z.object({
|
|
157
|
-
match: PATH,
|
|
158
|
-
redirect: PATH.optional(),
|
|
159
|
-
roles: z.string().optional(),
|
|
160
|
-
permissions: z.record(z.string(), z.string()).optional()
|
|
161
|
-
}).refine(({ match, redirect }) => !Paths.matches(match, redirect ?? "/"), { error: "Its \"redirect\" must be outside its \"match\"" })).optional()
|
|
162
|
-
}).optional()
|
|
163
|
-
});
|
|
106
|
+
/** Express settings: the client IP comes from `X-Forwarded-For` only through private proxies */
|
|
107
|
+
const SETTINGS = { "trust proxy": PRIVATE_NETWORKS };
|
|
108
|
+
/** Base middlewares */
|
|
109
|
+
const PACKAGES = {
|
|
110
|
+
morgan: {
|
|
111
|
+
tokens: {
|
|
112
|
+
timestamp: () => Logger.date(),
|
|
113
|
+
path: (oReq) => oReq.path
|
|
114
|
+
},
|
|
115
|
+
format: ":timestamp | :method :path :status"
|
|
116
|
+
},
|
|
117
|
+
cors: { methods: [
|
|
118
|
+
"GET",
|
|
119
|
+
"HEAD",
|
|
120
|
+
"PUT",
|
|
121
|
+
"POST",
|
|
122
|
+
"DELETE",
|
|
123
|
+
"QUERY"
|
|
124
|
+
] },
|
|
125
|
+
helmet: true,
|
|
126
|
+
compression: false,
|
|
127
|
+
express: { limit: "10kb" },
|
|
128
|
+
urlencoded: { extended: false }
|
|
129
|
+
};
|
|
130
|
+
/** Key of the Postgres lock that lets one startup change the database at a time */
|
|
131
|
+
const LOCK = 7310002;
|
|
132
|
+
/** Name of Better Auth's database: its tables live in `nano_auth` */
|
|
133
|
+
const AUTH_NAME = "auth";
|
|
134
|
+
/** Marks of the dynamic segments of a path, from the least to the most generic */
|
|
135
|
+
const MARKS = [
|
|
136
|
+
":",
|
|
137
|
+
"{",
|
|
138
|
+
"*"
|
|
139
|
+
];
|
|
164
140
|
// src/database/DatabaseManager.ts
|
|
165
|
-
/** The databases of the app and its plugins:
|
|
141
|
+
/** The databases of the app and its plugins: the pool of `DATABASE_URL`, its lock and one client per config */
|
|
166
142
|
var DatabaseManager = class {
|
|
167
143
|
static oPool;
|
|
168
|
-
static oClients =
|
|
169
|
-
/**
|
|
170
|
-
static async mount(
|
|
171
|
-
if (!
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
static async create(sName, oConfig) {
|
|
177
|
-
const { schema, push, migrations } = oConfig;
|
|
178
|
-
const { defineRelations, is } = await import("drizzle-orm");
|
|
179
|
-
const { getTableConfig, PgTable } = await import("drizzle-orm/pg-core");
|
|
180
|
-
const { drizzle } = await import("drizzle-orm/node-postgres");
|
|
181
|
-
const sOwn = sName === "app" ? void 0 : `nano_${sName}`;
|
|
182
|
-
const aSchemas = Object.values(schema).filter((oValue) => is(oValue, PgTable)).map((oTable) => {
|
|
183
|
-
const { name, schema: sSchema = "public" } = getTableConfig(oTable);
|
|
184
|
-
if (sSchema.startsWith("nano_") && sSchema !== sOwn) throw new NanoError(`The database "${sName}": table "${sSchema}.${name}" breaks the rule, each nano_* schema is reserved for the plugin of its name.`);
|
|
185
|
-
return sSchema;
|
|
186
|
-
});
|
|
187
|
-
const oClient = drizzle({
|
|
188
|
-
client: await this.pool(),
|
|
189
|
-
relations: schema.relations ?? defineRelations(schema)
|
|
190
|
-
});
|
|
191
|
-
if (push && aSchemas.length) {
|
|
192
|
-
const { pushSchema } = await import("drizzle-kit/api-postgres");
|
|
193
|
-
const { hints, apply } = await pushSchema(schema, oClient, {
|
|
194
|
-
schemas: aSchemas,
|
|
195
|
-
tables: void 0,
|
|
196
|
-
entities: void 0,
|
|
197
|
-
extensions: void 0
|
|
198
|
-
});
|
|
199
|
-
if (hints.length) throw new NanoError(`The database "${sName}" cannot be pushed without losing data: ${hints.map((oHint) => oHint.hint).join("; ")}. Run \`pnpm drizzle-kit push\` by hand.`);
|
|
200
|
-
await apply();
|
|
201
|
-
}
|
|
202
|
-
if (migrations) {
|
|
203
|
-
const { migrate } = await import("drizzle-orm/node-postgres/migrator");
|
|
204
|
-
await migrate(oClient, { migrationsFolder: migrations });
|
|
205
|
-
}
|
|
206
|
-
Logger.info(`Database "${sName}" mounted`);
|
|
207
|
-
return oClient;
|
|
208
|
-
}
|
|
209
|
-
/** The pool of `DATABASE_URL`, shared by every client and Better Auth, created on the first call */
|
|
210
|
-
static pool() {
|
|
211
|
-
this.oPool ??= this.createPool();
|
|
212
|
-
return this.oPool;
|
|
213
|
-
}
|
|
214
|
-
/** Creates the pool of `DATABASE_URL` */
|
|
215
|
-
static async createPool() {
|
|
144
|
+
static oClients = /* @__PURE__ */ new Map();
|
|
145
|
+
/** Mounts a database once, whatever its client, and gives its client; undefined without one */
|
|
146
|
+
static async mount(oDB) {
|
|
147
|
+
if (!oDB) return void 0;
|
|
148
|
+
return this.getClient(oDB);
|
|
149
|
+
}
|
|
150
|
+
/** The pool of `DATABASE_URL`, shared by every client, created on the first call */
|
|
151
|
+
static async pool() {
|
|
216
152
|
const sUrl = process.env.DATABASE_URL;
|
|
217
153
|
if (!sUrl) throw new NanoError("The DATABASE_URL environment variable is not defined.");
|
|
218
154
|
const { Pool } = await import("pg");
|
|
219
|
-
|
|
155
|
+
this.oPool ??= new Pool({ connectionString: sUrl });
|
|
156
|
+
return this.oPool;
|
|
157
|
+
}
|
|
158
|
+
/** Creates the client of a database on the shared pool, with the lock for its changes */
|
|
159
|
+
static async getClient(oDB) {
|
|
160
|
+
const { sName, oConfig } = oDB;
|
|
161
|
+
const oClient = this.oClients.get(oConfig);
|
|
162
|
+
if (oClient !== void 0) return oClient;
|
|
163
|
+
await this.pool();
|
|
164
|
+
await oDB.checkTables();
|
|
165
|
+
const oDBClient = await oDB.connect(this.oPool);
|
|
166
|
+
const oConnection = await this.oPool.connect();
|
|
167
|
+
await oConnection.query("SELECT pg_advisory_lock($1)", [LOCK]);
|
|
168
|
+
const { migrate, push } = oConfig;
|
|
169
|
+
if (migrate) await oDB.migrate(migrate);
|
|
170
|
+
if (push) await oDB.push();
|
|
171
|
+
oConnection.release(true);
|
|
172
|
+
Logger.info(`Database "${sName}" mounted`);
|
|
173
|
+
this.oClients.set(oConfig, oDBClient);
|
|
174
|
+
return oDBClient;
|
|
220
175
|
}
|
|
221
176
|
};
|
|
222
|
-
// src/
|
|
223
|
-
/**
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
177
|
+
// src/database/clients/DatabaseClient.ts
|
|
178
|
+
/** A database the manager mounts, whatever its client: its tables, its client and its changes */
|
|
179
|
+
var DatabaseClient = class {
|
|
180
|
+
sName;
|
|
181
|
+
oConfig;
|
|
182
|
+
/** Its nano_* schema, reserved for its plugin; the app has none */
|
|
183
|
+
sSchema;
|
|
184
|
+
/** Creates a new client of a database, named as its plugin */
|
|
185
|
+
constructor(sName, oConfig) {
|
|
186
|
+
this.sName = sName;
|
|
187
|
+
this.oConfig = oConfig;
|
|
188
|
+
this.sSchema = sName === "app" ? void 0 : `nano_${sName}`;
|
|
234
189
|
}
|
|
235
|
-
/**
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const oHeaders = this.fnHeaders(oReq.headers);
|
|
239
|
-
return await this.oAuth.api.getSession({ headers: oHeaders }) ?? void 0;
|
|
190
|
+
/** Checks its tables: a nano_* schema only for the plugin of its name */
|
|
191
|
+
async checkTables() {
|
|
192
|
+
for (const { schema, name } of await this.tables()) if (schema.startsWith("nano_") && schema !== this.sSchema) throw new NanoError(`The database "${this.sName}": table "${schema}.${name}" breaks the rule, each nano_* schema is reserved for the plugin of its name.`);
|
|
240
193
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
194
|
+
};
|
|
195
|
+
// src/database/clients/BetterAuth.ts
|
|
196
|
+
/** Better Auth's tables, as its options declare them, in its nano_* schema on its Kysely adapter */
|
|
197
|
+
var BetterAuth = class extends DatabaseClient {
|
|
198
|
+
oOptions;
|
|
199
|
+
oPool;
|
|
200
|
+
oDatabase;
|
|
201
|
+
/** Creates the database of Better Auth from its options */
|
|
202
|
+
constructor(sName, oConfig, oOptions) {
|
|
203
|
+
super(sName, oConfig);
|
|
204
|
+
this.oOptions = oOptions;
|
|
205
|
+
}
|
|
206
|
+
/** Its tables, from the options: the base ones, its plugins' and the additional fields */
|
|
207
|
+
async tables() {
|
|
208
|
+
const { getSchema } = await import("better-auth/db");
|
|
209
|
+
const sSchema = this.sSchema ?? "public";
|
|
210
|
+
return Object.keys(getSchema(this.oOptions)).map((name) => ({
|
|
211
|
+
schema: sSchema,
|
|
212
|
+
name
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
/** Better Auth's `database` option on the shared pool, its tables in its nano_* schema */
|
|
216
|
+
async connect(oPool) {
|
|
217
|
+
const { PostgresDialect } = await import("kysely");
|
|
218
|
+
this.oPool = oPool;
|
|
219
|
+
this.oDatabase = {
|
|
220
|
+
dialect: new PostgresDialect({ pool: oPool }),
|
|
221
|
+
type: "postgres",
|
|
222
|
+
schemaName: this.sSchema
|
|
223
|
+
};
|
|
224
|
+
return this.oDatabase;
|
|
225
|
+
}
|
|
226
|
+
/** Applies the pending migrations of a folder, generated from `npx auth generate --adapter drizzle` */
|
|
227
|
+
async migrate(sFolder) {
|
|
228
|
+
const { drizzle } = await import("drizzle-orm/node-postgres");
|
|
229
|
+
const { migrate } = await import("drizzle-orm/node-postgres/migrator");
|
|
230
|
+
await migrate(drizzle({ client: this.oPool }), {
|
|
231
|
+
migrationsFolder: sFolder,
|
|
232
|
+
migrationsSchema: this.sSchema
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/** Creates the tables and columns its options need; it only adds */
|
|
236
|
+
async push() {
|
|
245
237
|
const { getMigrations } = await import("better-auth/db/migration");
|
|
238
|
+
const { runMigrations } = await getMigrations({
|
|
239
|
+
...this.oOptions,
|
|
240
|
+
database: this.oDatabase
|
|
241
|
+
});
|
|
242
|
+
await runMigrations();
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
// src/auth/Auth.ts
|
|
246
|
+
/** Better Auth of the app: its tables in `nano_auth`, its endpoints and the access of each request */
|
|
247
|
+
var Auth = class {
|
|
248
|
+
static oAuth;
|
|
249
|
+
static fnHeaders;
|
|
250
|
+
/** Mounts its database and creates the instance; its endpoints, none without options */
|
|
251
|
+
static async init(oOptions) {
|
|
252
|
+
if (!oOptions) return [];
|
|
253
|
+
const { betterAuth } = await import("better-auth");
|
|
246
254
|
const { fromNodeHeaders, toNodeHandler } = await import("better-auth/node");
|
|
247
|
-
const {
|
|
248
|
-
const oPool = await DatabaseManager.pool();
|
|
249
|
-
const { advanced, basePath = "/api/auth" } = oOptions;
|
|
255
|
+
const { database = { push: true }, advanced, basePath = "/api/auth", ...oRest } = oOptions;
|
|
250
256
|
const oFullOptions = {
|
|
251
|
-
...
|
|
257
|
+
...oRest,
|
|
258
|
+
basePath,
|
|
252
259
|
advanced: {
|
|
253
260
|
...advanced,
|
|
254
261
|
ipAddress: {
|
|
255
262
|
trustedProxies: PRIVATE_NETWORKS,
|
|
256
263
|
...advanced?.ipAddress
|
|
257
264
|
}
|
|
258
|
-
},
|
|
259
|
-
database: {
|
|
260
|
-
dialect: new PostgresDialect({ pool: oPool }),
|
|
261
|
-
type: "postgres",
|
|
262
|
-
schemaName: SCHEMA
|
|
263
265
|
}
|
|
264
266
|
};
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
267
|
+
const oDatabase = await DatabaseManager.mount(new BetterAuth(AUTH_NAME, database, oFullOptions));
|
|
268
|
+
const oAuth = betterAuth({
|
|
269
|
+
...oFullOptions,
|
|
270
|
+
database: oDatabase
|
|
271
|
+
});
|
|
270
272
|
this.oAuth = oAuth;
|
|
271
273
|
this.fnHeaders = fromNodeHeaders;
|
|
272
|
-
|
|
274
|
+
return [{
|
|
275
|
+
path: `${basePath}/{*path}`,
|
|
276
|
+
handler: toNodeHandler(oAuth)
|
|
277
|
+
}];
|
|
278
|
+
}
|
|
279
|
+
/** The session of a request; 401 without one unless public, 403 without the permissions */
|
|
280
|
+
static async guard(oReq, isPublic, oPermissions) {
|
|
281
|
+
const oSession = await this.session(oReq);
|
|
282
|
+
if (this.oAuth && !oSession && !isPublic) throw new NanoError("Not authenticated", 401);
|
|
283
|
+
if (!await this.isGranted(oSession, isPublic ? void 0 : oPermissions)) throw new NanoError("Forbidden", 403);
|
|
284
|
+
return oSession;
|
|
285
|
+
}
|
|
286
|
+
/** The session of a request, when it has one */
|
|
287
|
+
static async session(oReq) {
|
|
288
|
+
if (!this.oAuth || !this.fnHeaders) return void 0;
|
|
289
|
+
const headers = this.fnHeaders(oReq.headers);
|
|
290
|
+
return await this.oAuth.api.getSession({ headers }) ?? void 0;
|
|
291
|
+
}
|
|
292
|
+
/** Whether the user of a session has the permissions; always without Better Auth or permissions */
|
|
293
|
+
static async isGranted(oSession, oPermissions) {
|
|
294
|
+
if (!this.oAuth || !oPermissions) return true;
|
|
295
|
+
const { success } = await this.oAuth.api.userHasPermission({ body: {
|
|
296
|
+
userId: oSession?.user.id,
|
|
297
|
+
role: oSession?.user.role,
|
|
298
|
+
permissions: oPermissions
|
|
299
|
+
} });
|
|
300
|
+
return success;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
// src/database/clients/Drizzle.ts
|
|
304
|
+
/** Drizzle client of the app or of a plugin: its tables, its client and its changes */
|
|
305
|
+
var Drizzle = class extends DatabaseClient {
|
|
306
|
+
oClient;
|
|
307
|
+
/** The Postgres schema and name of each of its tables, from Drizzle's metadata */
|
|
308
|
+
async tables() {
|
|
309
|
+
const { is } = await import("drizzle-orm");
|
|
310
|
+
const { getTableConfig, PgTable } = await import("drizzle-orm/pg-core");
|
|
311
|
+
return Object.values(this.oConfig.relations).map(({ table }) => table).filter((oTable) => is(oTable, PgTable)).map((oTable) => {
|
|
312
|
+
const { name, schema = "public" } = getTableConfig(oTable);
|
|
313
|
+
return {
|
|
314
|
+
schema,
|
|
315
|
+
name
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
/** Creates its client on a pool */
|
|
320
|
+
async connect(oPool) {
|
|
321
|
+
const { migrate, push, ...oDrizzleConfig } = this.oConfig;
|
|
322
|
+
const { drizzle } = await import("drizzle-orm/node-postgres");
|
|
323
|
+
this.oClient = drizzle({
|
|
324
|
+
...oDrizzleConfig,
|
|
325
|
+
client: oPool
|
|
326
|
+
});
|
|
327
|
+
return this.oClient;
|
|
328
|
+
}
|
|
329
|
+
/** Applies the pending migrations of a folder, recorded in its own nano_* schema */
|
|
330
|
+
async migrate(sFolder) {
|
|
331
|
+
const { migrate } = await import("drizzle-orm/node-postgres/migrator");
|
|
332
|
+
await migrate(this.oClient, {
|
|
333
|
+
migrationsFolder: sFolder,
|
|
334
|
+
migrationsSchema: this.sSchema
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/** Pushes its tables to their Postgres schemas, failing when it would lose data */
|
|
338
|
+
async push() {
|
|
339
|
+
const { pushSchema } = await import("drizzle-kit/api-postgres");
|
|
340
|
+
const oTables = Object.fromEntries(Object.values(this.oConfig.relations).map(({ name, table }) => [name, table]));
|
|
341
|
+
const schemas = (await this.tables()).map(({ schema }) => schema);
|
|
342
|
+
const { hints, apply } = await pushSchema(oTables, this.oClient, {
|
|
343
|
+
schemas,
|
|
344
|
+
tables: void 0,
|
|
345
|
+
entities: void 0,
|
|
346
|
+
extensions: void 0
|
|
347
|
+
});
|
|
348
|
+
if (hints.length) {
|
|
349
|
+
const sHints = hints.map((oHint) => oHint.hint).join("; ");
|
|
350
|
+
throw new NanoError(`The database "${this.sName}" cannot be pushed without losing data: ${sHints}. Run \`pnpm drizzle-kit push\` by hand.`);
|
|
351
|
+
}
|
|
352
|
+
await apply();
|
|
273
353
|
}
|
|
274
354
|
};
|
|
275
|
-
// src/auth/permissions.ts
|
|
276
|
-
/** Whether the session's user has the permissions; always without Better Auth or permissions */
|
|
277
|
-
async function isGranted({ auth, session }, oPermissions) {
|
|
278
|
-
if (!auth || !oPermissions) return true;
|
|
279
|
-
const { success } = await auth.api.userHasPermission({ body: {
|
|
280
|
-
userId: session?.user.id,
|
|
281
|
-
role: session?.user.role,
|
|
282
|
-
permissions: oPermissions
|
|
283
|
-
} });
|
|
284
|
-
return success;
|
|
285
|
-
}
|
|
286
355
|
// src/router/Endpoint.ts
|
|
287
|
-
/**
|
|
356
|
+
/** A route: its methods, its access and its callbacks */
|
|
288
357
|
var Endpoint = class {
|
|
289
358
|
oMethods;
|
|
290
359
|
oCallbacks;
|
|
291
360
|
isPublic;
|
|
292
361
|
/** Allowed methods and the options of the route */
|
|
293
|
-
constructor(
|
|
362
|
+
constructor({ callbacks = {}, public: isPublic = false, permissions, ...oMethods }) {
|
|
294
363
|
this.oMethods = Object.fromEntries(Object.entries(oMethods).map(([sMethod, oMethod]) => [sMethod, typeof oMethod === "function" ? {
|
|
295
364
|
handler: oMethod,
|
|
296
365
|
permissions
|
|
@@ -301,15 +370,9 @@ var Endpoint = class {
|
|
|
301
370
|
this.oCallbacks = callbacks;
|
|
302
371
|
this.isPublic = isPublic;
|
|
303
372
|
}
|
|
304
|
-
/**
|
|
305
|
-
async
|
|
306
|
-
const isUnchecked = Object.values(this.oMethods).some(({ permissions }) => permissions);
|
|
307
|
-
if (!oParams.auth && isUnchecked) Logger.warn(`${sPath}: its permissions are not checked without Better Auth. Set auth in createNanoApp.`);
|
|
373
|
+
/** Runs its onInit and gives its handler for all methods */
|
|
374
|
+
async init(oParams) {
|
|
308
375
|
await this.oCallbacks.onInit?.(oParams);
|
|
309
|
-
return this.createHandler(oParams);
|
|
310
|
-
}
|
|
311
|
-
/** Express handler: the request, answered as an ApiResponse */
|
|
312
|
-
createHandler(oParams) {
|
|
313
376
|
return (oReq, oRes) => this.run(oReq, oParams).then((oResponse) => {
|
|
314
377
|
const { status, ...oBody } = oResponse;
|
|
315
378
|
const iStatus = status ?? 200;
|
|
@@ -319,18 +382,16 @@ var Endpoint = class {
|
|
|
319
382
|
});
|
|
320
383
|
});
|
|
321
384
|
}
|
|
322
|
-
/** Checks the session and the permissions, then runs onBefore and
|
|
385
|
+
/** Checks the session and the permissions of the method, then runs onBefore and it */
|
|
323
386
|
async run(oReq, oParams) {
|
|
324
|
-
const oSession = await BetterAuth.session(oReq);
|
|
325
|
-
const oRequestParams = {
|
|
326
|
-
...oParams,
|
|
327
|
-
session: oSession
|
|
328
|
-
};
|
|
329
|
-
if (oParams.auth && !oSession && !this.isPublic) throw new NanoError("Not authenticated", 401);
|
|
330
387
|
const sMethod = oReq.method;
|
|
331
388
|
const oMethod = this.oMethods[sMethod];
|
|
389
|
+
const session = await Auth.guard(oReq, this.isPublic, oMethod?.permissions);
|
|
332
390
|
if (!oMethod) throw new NanoError(`Method ${sMethod} not implemented`, 405);
|
|
333
|
-
|
|
391
|
+
const oRequestParams = {
|
|
392
|
+
...oParams,
|
|
393
|
+
session
|
|
394
|
+
};
|
|
334
395
|
const onBeforeResult = await this.oCallbacks.onBefore?.(oReq, sMethod, oRequestParams) ?? {};
|
|
335
396
|
return oMethod.handler(oReq, {
|
|
336
397
|
...oRequestParams,
|
|
@@ -338,36 +399,56 @@ var Endpoint = class {
|
|
|
338
399
|
});
|
|
339
400
|
}
|
|
340
401
|
};
|
|
402
|
+
// src/router/validations.ts
|
|
403
|
+
/** A function of the consumer, typed as what the framework calls */
|
|
404
|
+
function callable() {
|
|
405
|
+
return z.custom((oValue) => typeof oValue === "function");
|
|
406
|
+
}
|
|
407
|
+
/** Permissions of Better Auth's admin plugin: the actions per resource */
|
|
408
|
+
const PERMISSIONS = z.record(z.string(), z.array(z.string())).optional();
|
|
409
|
+
/** A method of a route: its handler, or it with its own permissions */
|
|
410
|
+
const METHOD = z.union([callable(), z.object({
|
|
411
|
+
handler: callable(),
|
|
412
|
+
permissions: PERMISSIONS
|
|
413
|
+
})]).optional();
|
|
414
|
+
/** A `*.route.ts` file: its options, its callbacks and at least one HTTP method, nothing else */
|
|
415
|
+
const ROUTE = z.strictObject({
|
|
416
|
+
expression: z.string().optional(),
|
|
417
|
+
public: z.boolean().optional(),
|
|
418
|
+
permissions: PERMISSIONS,
|
|
419
|
+
callbacks: z.object({
|
|
420
|
+
onInit: callable().optional(),
|
|
421
|
+
onBefore: callable().optional()
|
|
422
|
+
}).optional(),
|
|
423
|
+
GET: METHOD,
|
|
424
|
+
POST: METHOD,
|
|
425
|
+
PUT: METHOD,
|
|
426
|
+
DELETE: METHOD,
|
|
427
|
+
QUERY: METHOD
|
|
428
|
+
}).refine(({ GET, POST, PUT, DELETE, QUERY }) => GET ?? POST ?? PUT ?? DELETE ?? QUERY, { error: "It defines no HTTP method" });
|
|
341
429
|
// src/router/Router.ts
|
|
342
|
-
/** Turns a folder of `*.route.ts` files into Express paths and endpoints */
|
|
343
|
-
var Router = class {
|
|
430
|
+
/** Turns a folder of `*.route.ts` files and routes made in code into Express paths and endpoints */
|
|
431
|
+
var Router = class Router {
|
|
344
432
|
sRoutesDir;
|
|
345
433
|
sBasePath;
|
|
346
|
-
|
|
434
|
+
oRoutes;
|
|
435
|
+
constructor(sRoutesDir, sBasePath, oRoutes) {
|
|
347
436
|
this.sRoutesDir = sRoutesDir;
|
|
348
437
|
this.sBasePath = sBasePath ?? "";
|
|
438
|
+
this.oRoutes = oRoutes ?? {};
|
|
349
439
|
}
|
|
350
|
-
/** Loads the routes and creates one endpoint per
|
|
351
|
-
async
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return {
|
|
359
|
-
path: sPath,
|
|
360
|
-
endpoint: new Endpoint(methods, {
|
|
361
|
-
callbacks,
|
|
362
|
-
public: isPublic,
|
|
363
|
-
permissions
|
|
364
|
-
})
|
|
365
|
-
};
|
|
366
|
-
});
|
|
440
|
+
/** Loads the routes and creates one endpoint per route, the most specific path first */
|
|
441
|
+
async load() {
|
|
442
|
+
const oRoutes = this.sRoutesDir ? await FileLoader.findMany("route", ROUTE, this.sRoutesDir) : {};
|
|
443
|
+
for (const [sPath, oRoute] of Object.entries(this.oRoutes)) oRoutes[sPath] = Validator.validate(ROUTE, oRoute, sPath);
|
|
444
|
+
return Object.entries(oRoutes).map(([sFilePath, { expression = "", ...oEndpointParams }]) => ({
|
|
445
|
+
path: `${this.sBasePath}${sFilePath.replace(/\/index$/, "")}${expression}` || "/",
|
|
446
|
+
endpoint: new Endpoint(oEndpointParams)
|
|
447
|
+
})).sort((oA, oB) => Router.specificity(oA.path).localeCompare(Router.specificity(oB.path)));
|
|
367
448
|
}
|
|
368
|
-
/**
|
|
369
|
-
|
|
370
|
-
return
|
|
449
|
+
/** One digit per segment: 0 static, 1 `:param`, 2 `{optional}`, 3 `*wildcard` */
|
|
450
|
+
static specificity(sPath) {
|
|
451
|
+
return sPath.split("/").map((sSegment) => MARKS.reduce((iRank, sMark, iIndex) => sSegment.includes(sMark) ? iIndex + 1 : iRank, 0)).join("");
|
|
371
452
|
}
|
|
372
453
|
};
|
|
373
454
|
// src/plugins/Plugin.ts
|
|
@@ -378,65 +459,43 @@ var Plugin = class {
|
|
|
378
459
|
constructor(oConfig) {
|
|
379
460
|
this.oConfig = oConfig;
|
|
380
461
|
}
|
|
381
|
-
/**
|
|
382
|
-
async
|
|
383
|
-
const { name, routesDir, basePath, database, options, callbacks } = this.oConfig;
|
|
384
|
-
const
|
|
462
|
+
/** Its endpoints, the most specific path first, for NanoApp to register */
|
|
463
|
+
async build(oAppDB) {
|
|
464
|
+
const { name, routesDir, routes, basePath, database, options, callbacks } = this.oConfig;
|
|
465
|
+
const db = await DatabaseManager.mount(database ? new Drizzle(name, database) : oAppDB);
|
|
385
466
|
const oParams = Object.freeze({
|
|
386
467
|
...options,
|
|
387
|
-
db
|
|
388
|
-
auth: BetterAuth.instance()
|
|
468
|
+
db
|
|
389
469
|
});
|
|
390
|
-
await callbacks?.onMount?.(
|
|
391
|
-
const
|
|
392
|
-
|
|
470
|
+
await callbacks?.onMount?.(oParams);
|
|
471
|
+
const aRoutes = await new Router(routesDir, basePath, routes).load();
|
|
472
|
+
return Promise.all(aRoutes.map(async ({ path, endpoint }) => ({
|
|
473
|
+
path,
|
|
474
|
+
handler: await endpoint.init(oParams)
|
|
475
|
+
})));
|
|
393
476
|
}
|
|
394
477
|
};
|
|
395
478
|
// src/plugins/PluginManager.ts
|
|
396
479
|
/** The plugins of the app: its own, the ones before it and the ones after it */
|
|
397
480
|
var PluginManager = class {
|
|
398
|
-
/**
|
|
399
|
-
static async
|
|
481
|
+
/** The endpoints of every plugin, in order: before, the app's, after */
|
|
482
|
+
static async build(oConfig) {
|
|
400
483
|
const { routesDir = "src/routes", database, plugins } = oConfig;
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
484
|
+
const oAppDB = database && new Drizzle("app", database);
|
|
485
|
+
const { before = [], after = [] } = plugins ?? {};
|
|
486
|
+
const aPlugins = [
|
|
487
|
+
...before,
|
|
488
|
+
{
|
|
489
|
+
name: "app",
|
|
490
|
+
routesDir
|
|
491
|
+
},
|
|
492
|
+
...after
|
|
493
|
+
];
|
|
494
|
+
const aMounts = [];
|
|
495
|
+
for (const oPlugin of aPlugins) aMounts.push(...await new Plugin(oPlugin).build(oAppDB));
|
|
496
|
+
return aMounts;
|
|
414
497
|
}
|
|
415
498
|
};
|
|
416
|
-
/** Express settings: the client IP comes from `X-Forwarded-For` only through private proxies */
|
|
417
|
-
const SETTINGS = { "trust proxy": PRIVATE_NETWORKS };
|
|
418
|
-
/** Base middlewares */
|
|
419
|
-
const PACKAGES = {
|
|
420
|
-
morgan: {
|
|
421
|
-
tokens: {
|
|
422
|
-
timestamp: () => Logger.date(),
|
|
423
|
-
path: (oReq) => oReq.path
|
|
424
|
-
},
|
|
425
|
-
format: ":timestamp | :method :path :status"
|
|
426
|
-
},
|
|
427
|
-
cors: { methods: [
|
|
428
|
-
"GET",
|
|
429
|
-
"HEAD",
|
|
430
|
-
"PUT",
|
|
431
|
-
"POST",
|
|
432
|
-
"DELETE",
|
|
433
|
-
"QUERY"
|
|
434
|
-
] },
|
|
435
|
-
helmet: true,
|
|
436
|
-
compression: false,
|
|
437
|
-
express: { limit: "10kb" },
|
|
438
|
-
urlencoded: { extended: false }
|
|
439
|
-
};
|
|
440
499
|
// src/app/handlers/errors.ts
|
|
441
500
|
/** Turns every unmatched request into a 404 for the error handler */
|
|
442
501
|
const notFoundHandler = (oReq, _oRes, fnNext) => {
|
|
@@ -454,6 +513,50 @@ const errorHandler = (oError, _oReq, oRes, _fnNext) => {
|
|
|
454
513
|
message: sMessage
|
|
455
514
|
});
|
|
456
515
|
};
|
|
516
|
+
// src/app/handlers/rateLimit.ts
|
|
517
|
+
/** Rate limit per client IP: each request spends a token of the client's bucket, refilled over time */
|
|
518
|
+
function rateLimit({ capacity = 100, refillPerSecond = 1, message = "Too many requests, please try again later." }) {
|
|
519
|
+
const fnRefill = (oBucket, iNow) => Math.min(capacity, oBucket.iTokens + (iNow - oBucket.iTime) / 1e3 * refillPerSecond);
|
|
520
|
+
const oBuckets = /* @__PURE__ */ new Map();
|
|
521
|
+
setInterval(() => {
|
|
522
|
+
const iNow = Date.now();
|
|
523
|
+
for (const [sIp, oBucket] of oBuckets) if (fnRefill(oBucket, iNow) >= capacity) oBuckets.delete(sIp);
|
|
524
|
+
}, 6e4).unref();
|
|
525
|
+
return (oReq, oRes, fnNext) => {
|
|
526
|
+
const sIp = ipKeyGenerator(oReq.ip ?? "");
|
|
527
|
+
const iNow = Date.now();
|
|
528
|
+
const iTokens = fnRefill(oBuckets.get(sIp) ?? {
|
|
529
|
+
iTokens: capacity,
|
|
530
|
+
iTime: iNow
|
|
531
|
+
}, iNow);
|
|
532
|
+
if (iTokens < 1) {
|
|
533
|
+
oRes.set("Retry-After", String(Math.ceil((1 - iTokens) / refillPerSecond)));
|
|
534
|
+
return fnNext({
|
|
535
|
+
status: 429,
|
|
536
|
+
message
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
oBuckets.set(sIp, {
|
|
540
|
+
iTokens: iTokens - 1,
|
|
541
|
+
iTime: iNow
|
|
542
|
+
});
|
|
543
|
+
fnNext();
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
// src/app/packages.ts
|
|
547
|
+
/** The base middlewares that are on, in the order the app uses them */
|
|
548
|
+
function packages(oConfig) {
|
|
549
|
+
if (oConfig.morgan) for (const [sKey, fnToken] of Object.entries(oConfig.morgan.tokens ?? {})) morgan.token(sKey, fnToken);
|
|
550
|
+
return [
|
|
551
|
+
oConfig.morgan && morgan(oConfig.morgan.format),
|
|
552
|
+
oConfig.cors && cors(oConfig.cors),
|
|
553
|
+
oConfig.helmet && helmet(),
|
|
554
|
+
oConfig.rateLimit && rateLimit(oConfig.rateLimit),
|
|
555
|
+
oConfig.compression && compression(),
|
|
556
|
+
oConfig.express && express.json(oConfig.express),
|
|
557
|
+
oConfig.urlencoded && express.urlencoded(oConfig.urlencoded)
|
|
558
|
+
].filter((fnPackage) => typeof fnPackage === "function");
|
|
559
|
+
}
|
|
457
560
|
// src/app/NanoApp.ts
|
|
458
561
|
/** Main NanoApp class */
|
|
459
562
|
var NanoApp = class {
|
|
@@ -484,30 +587,19 @@ var NanoApp = class {
|
|
|
484
587
|
...SETTINGS,
|
|
485
588
|
...this.oConfig.settings
|
|
486
589
|
})) this.oApp.set(sKey, oValue);
|
|
487
|
-
|
|
590
|
+
for (const fnPackage of packages({
|
|
488
591
|
...PACKAGES,
|
|
489
592
|
...this.oConfig.packages
|
|
490
|
-
});
|
|
491
|
-
await
|
|
492
|
-
await PluginManager.
|
|
593
|
+
})) this.oApp.use(fnPackage);
|
|
594
|
+
const aAuth = await Auth.init(this.oConfig.auth);
|
|
595
|
+
const aPlugins = await PluginManager.build(this.oConfig);
|
|
596
|
+
for (const { path, handler } of [...aAuth, ...aPlugins]) {
|
|
597
|
+
Logger.info(path);
|
|
598
|
+
this.oApp.all(path, handler);
|
|
599
|
+
}
|
|
493
600
|
this.oApp.use(notFoundHandler);
|
|
494
601
|
this.oApp.use(errorHandler);
|
|
495
602
|
}
|
|
496
|
-
/** Adds the packages according to the configuration */
|
|
497
|
-
setPackages(oConfig) {
|
|
498
|
-
if (oConfig.morgan) {
|
|
499
|
-
for (const [sKey, fnToken] of Object.entries(oConfig.morgan.tokens ?? {})) morgan.token(sKey, fnToken);
|
|
500
|
-
this.oApp.use(morgan(oConfig.morgan.format));
|
|
501
|
-
}
|
|
502
|
-
const aPackages = [
|
|
503
|
-
oConfig.cors && cors(oConfig.cors),
|
|
504
|
-
oConfig.helmet && helmet(),
|
|
505
|
-
oConfig.compression && compression(),
|
|
506
|
-
oConfig.express && express.json(oConfig.express),
|
|
507
|
-
oConfig.urlencoded && express.urlencoded(oConfig.urlencoded)
|
|
508
|
-
];
|
|
509
|
-
for (const fnPackage of aPackages) if (fnPackage) this.oApp.use(fnPackage);
|
|
510
|
-
}
|
|
511
603
|
};
|
|
512
604
|
// src/lib/creators.ts
|
|
513
605
|
/** Creates a new NanoApp instance */
|
|
@@ -519,10 +611,19 @@ function createRoute(oRoute) {
|
|
|
519
611
|
return oRoute;
|
|
520
612
|
}
|
|
521
613
|
function createPlugin(fnCreator) {
|
|
522
|
-
return
|
|
614
|
+
return (options) => {
|
|
615
|
+
const oConfig = fnCreator(options);
|
|
616
|
+
return {
|
|
617
|
+
...oConfig,
|
|
618
|
+
basePath: options?.basePath ?? oConfig.basePath
|
|
619
|
+
};
|
|
620
|
+
};
|
|
523
621
|
}
|
|
524
622
|
/** Creates the callbacks of a plugin, typed with what it injects in its params. */
|
|
525
623
|
function createPluginCallbacks(oCallbacks) {
|
|
526
624
|
return oCallbacks;
|
|
527
625
|
}
|
|
626
|
+
// src/index.ts
|
|
627
|
+
/** Whether the user of a session has the permissions, asked to Better Auth */
|
|
628
|
+
const isGranted = Auth.isGranted.bind(Auth);
|
|
528
629
|
export { NanoApp, NanoError, createNanoApp, createPlugin, createPluginCallbacks, createRoute, isGranted };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nano-app/backend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"cors": "^2.8.6",
|
|
49
49
|
"dotenv": "^18.0.9",
|
|
50
50
|
"express": "^5.3.0",
|
|
51
|
+
"express-rate-limit": "^8.7.3",
|
|
51
52
|
"glob": "^14.0.4",
|
|
52
53
|
"helmet": "^8.3.0",
|
|
53
54
|
"jiti": "^2.7.0",
|