@rapidd/core 2.1.0 → 2.1.2

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/.env.example CHANGED
@@ -40,10 +40,10 @@ DB_USER_PASSWORD_FIELD=
40
40
  # Auto-detected from unique string fields; comma-separated (default: email)
41
41
  DB_USER_IDENTIFIER_FIELDS=
42
42
  # Comma-separated: bearer, basic, cookie, header (default: bearer)
43
- AUTH_STRATEGIES=bearer
44
- # Cookie name for cookie strategy (default: token)
43
+ AUTH_METHODS=bearer
44
+ # Cookie name for cookie auth method (default: token)
45
45
  AUTH_COOKIE_NAME=token
46
- # Header name for header strategy (default: X-Auth-Token)
46
+ # Header name for header auth method (default: X-Auth-Token)
47
47
  AUTH_CUSTOM_HEADER=X-Auth-Token
48
48
 
49
49
  # ── API Settings ───────────────────────────────────────
package/README.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  Code-first REST API framework for TypeScript. Database in, API out.
6
6
 
7
+ [![npm](https://img.shields.io/npm/v/@rapidd/core?color=cb3837&logo=npm&logoColor=white)](https://www.npmjs.com/package/@rapidd/core)
7
8
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
8
9
  [![Fastify](https://img.shields.io/badge/Fastify-5.x-000000?logo=fastify&logoColor=white)](https://fastify.dev/)
9
10
  [![Prisma](https://img.shields.io/badge/Prisma-7.x-2D3748?logo=prisma&logoColor=white)](https://www.prisma.io/)
@@ -29,7 +30,9 @@ Rapidd generates a fully-featured REST API from your database schema — then ge
29
30
  ## Quick Start
30
31
 
31
32
  ```bash
32
- npm install @rapidd/build
33
+ mkdir my-api && cd my-api
34
+ npx rapidd create-project # scaffold project files
35
+ npm install
33
36
  ```
34
37
 
35
38
  ```env
@@ -44,7 +47,7 @@ npm run dev # http://localhost:3000
44
47
 
45
48
  Every table gets full CRUD endpoints. Auth is enabled automatically when a user table is detected. Every auto-detected value — auth fields, password hashing, JWT secrets, session store — is overridable via env vars. See [`.env.example`](.env.example) for the full list.
46
49
 
47
- > 📖 **[Getting Started guide](https://github.com/MertDalbudak/rapidd/wiki/Getting-Started)** — full walkthrough with project structure
50
+ > **[Getting Started guide](https://github.com/MertDalbudak/rapidd/wiki/Getting-Started)** — full walkthrough with project structure
48
51
 
49
52
  ---
50
53
 
@@ -56,7 +59,7 @@ Every table gets full CRUD endpoints. Auth is enabled automatically when a user
56
59
  | Query filtering (20+ operators) | ✓ | ✓ |
57
60
  | Relations & deep includes | ✓ | ✓ |
58
61
  | Field selection | ✓ | ✓ |
59
- | JWT authentication (4 strategies) | ✓ | ✓ |
62
+ | JWT authentication (4 methods) | ✓ | ✓ |
60
63
  | Per-model ACL | ✓ | ✓ |
61
64
  | Row-Level Security (database-enforced) | ✓ | — |
62
65
  | Rate limiting (Redis + memory fallback) | ✓ | ✓ |
@@ -83,7 +86,7 @@ GET /api/v1/posts?sortBy=createdAt&sortOrder=desc&limit=10&offset=20
83
86
 
84
87
  20+ filter operators for strings, numbers, dates, arrays, nulls, and nested relation fields. Responses include pagination metadata with `total`, `count`, `limit`, `offset`, and `hasMore`.
85
88
 
86
- > 📖 **[Query API wiki](https://github.com/MertDalbudak/rapidd/wiki/Query-API)** — all operators, composite PKs, relation filtering
89
+ > **[Query API wiki](https://github.com/MertDalbudak/rapidd/wiki/Query-API)** — all operators, composite PKs, relation filtering
87
90
 
88
91
  ---
89
92
 
@@ -98,11 +101,24 @@ POST /auth/refresh { "refreshToken": "..." }
98
101
  GET /auth/me Authorization: Bearer <token>
99
102
  ```
100
103
 
101
- Four strategies — **bearer** (default), **basic**, **cookie**, and **custom header** — configurable per-route. Multi-identifier login lets users authenticate with any unique field (email, username, phone) in a single endpoint.
104
+ Four methods — **bearer** (default), **basic**, **cookie**, and **custom header** — configurable globally via `AUTH_METHODS` env var or per endpoint prefix in `config/app.json`:
102
105
 
103
- ⚠️ **Production:** `JWT_SECRET` and `JWT_REFRESH_SECRET` must be set explicitly. The server refuses to start without them to prevent session invalidation on restart.
106
+ ```json
107
+ {
108
+ "endpointAuthMethod": {
109
+ "/api/v1": ["basic", "bearer"],
110
+ "/api/v2": "bearer"
111
+ }
112
+ }
113
+ ```
114
+
115
+ Set `null` for the global default, a string for a single method, or an array for multiple. Route-level config takes highest priority, then prefix match, then global default.
116
+
117
+ Multi-identifier login lets users authenticate with any unique field (email, username, phone) in a single endpoint.
104
118
 
105
- > 📖 **[Authentication wiki →](https://github.com/MertDalbudak/rapidd/wiki/Authentication)** session stores, route protection, per-endpoint strategy overrides
119
+ **Production:** `JWT_SECRET` and `JWT_REFRESH_SECRET` must be set explicitly. The server refuses to start without them to prevent session invalidation on restart.
120
+
121
+ > **[Authentication wiki](https://github.com/MertDalbudak/rapidd/wiki/Authentication)** — session stores, route protection, per-endpoint method overrides
106
122
 
107
123
  ---
108
124
 
@@ -126,7 +142,7 @@ const acl: AclConfig = {
126
142
 
127
143
  Return `{}` for full access, a filter object to scope records, or `false` to deny.
128
144
 
129
- > 📖 **[Access Control wiki](https://github.com/MertDalbudak/rapidd/wiki/Access-Control-(ACL))** — all 5 ACL methods, relation ACL, 404 vs 403 distinction
145
+ > **[Access Control wiki](https://github.com/MertDalbudak/rapidd/wiki/Access-Control-(ACL))** — all 5 ACL methods, relation ACL, 404 vs 403 distinction
130
146
 
131
147
  ---
132
148
 
@@ -152,7 +168,7 @@ Model.middleware.use('before', 'delete', async (ctx) => {
152
168
 
153
169
  Supports `create`, `update`, `upsert`, `upsertMany`, `delete`, `get`, `getMany`, and `count`. Middleware can abort operations, modify data, and short-circuit with cached results.
154
170
 
155
- > 📖 **[Model Middleware wiki](https://github.com/MertDalbudak/rapidd/wiki/Model-Middleware)** — all hooks, context object, patterns (soft delete, validation, caching)
171
+ > **[Model Middleware wiki](https://github.com/MertDalbudak/rapidd/wiki/Model-Middleware)** — all hooks, context object, patterns (soft delete, validation, caching)
156
172
 
157
173
  ---
158
174
 
@@ -173,7 +189,7 @@ CREATE POLICY tenant_isolation ON orders
173
189
  USING (tenant_id = current_setting('app.current_tenant_id')::int);
174
190
  ```
175
191
 
176
- > 📖 **[Row-Level Security wiki](https://github.com/MertDalbudak/rapidd/wiki/Row%E2%80%90Level-Security-(RLS))** — policy examples, RLS vs ACL comparison
192
+ > **[Row-Level Security wiki](https://github.com/MertDalbudak/rapidd/wiki/Row%E2%80%90Level-Security-(RLS))** — policy examples, RLS vs ACL comparison
177
193
 
178
194
  ---
179
195
 
@@ -181,11 +197,11 @@ CREATE POLICY tenant_isolation ON orders
181
197
 
182
198
  | Utility | Description | Docs |
183
199
  |---------|-------------|------|
184
- | **ApiClient** | Config-driven HTTP client with Bearer, Basic, API Key, and OAuth2 auth. Automatic token caching, retries, and fluent builder. | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/ApiClient) |
185
- | **Mailer** | SMTP email with EJS template rendering, layout wrappers, i18n support, batch sending, and attachments. | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/Mailer) |
186
- | **File Uploads** | Multipart uploads with MIME validation, size limits, and type presets (`images`, `documents`, etc.). | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/File-Uploads) |
187
- | **Rate Limiting** | Redis-backed with automatic memory fallback. Per-path configuration via `config/rate-limit.json`. | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/Rate-Limiting) |
188
- | **i18n** | 10 languages included. Auto-detected from `Accept-Language` header. Parameter interpolation in error messages. | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/Internationalization-(i18n)) |
200
+ | **ApiClient** | Config-driven HTTP client with Bearer, Basic, API Key, and OAuth2 auth. Automatic token caching, retries, and fluent builder. | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/ApiClient) |
201
+ | **Mailer** | SMTP email with EJS template rendering, layout wrappers, i18n support, batch sending, and attachments. | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/Mailer) |
202
+ | **File Uploads** | Multipart uploads with MIME validation, size limits, and type presets (`images`, `documents`, etc.). | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/File-Uploads) |
203
+ | **Rate Limiting** | Redis-backed with automatic memory fallback. Per-path configuration via `config/rate-limit.json`. | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/Rate-Limiting) |
204
+ | **i18n** | 10 languages included. Auto-detected from `Accept-Language` header. Parameter interpolation in error messages. | [Wiki](https://github.com/MertDalbudak/rapidd/wiki/Internationalization-(i18n)) |
189
205
 
190
206
  ---
191
207
 
@@ -203,12 +219,28 @@ TRUST_PROXY=true
203
219
  npm run build && npm start
204
220
 
205
221
  # or Docker
206
- docker build -t rapidd . && docker run -p 3000:3000 --env-file .env rapidd
222
+ docker build -t my-api . && docker run -p 3000:3000 --env-file .env my-api
207
223
  ```
208
224
 
209
225
  **Security defaults in production:** HSTS, Content-Security-Policy, X-Content-Type-Options, Referrer-Policy, and CORS with explicit origin whitelisting — all enabled automatically.
210
226
 
211
- > 📖 **[Deployment wiki](https://github.com/MertDalbudak/rapidd/wiki/Deployment-&-Production)** — Docker Compose, nginx reverse proxy, production checklist, horizontal scaling
227
+ > **[Deployment wiki](https://github.com/MertDalbudak/rapidd/wiki/Deployment-&-Production)** — Docker Compose, nginx reverse proxy, production checklist, horizontal scaling
228
+
229
+ ---
230
+
231
+ ## Packages
232
+
233
+ | Package | Description |
234
+ |---------|-------------|
235
+ | [`@rapidd/core`](https://www.npmjs.com/package/@rapidd/core) | Framework runtime, project scaffolding, and unified `npx rapidd` CLI |
236
+ | [`@rapidd/build`](https://www.npmjs.com/package/@rapidd/build) | Code generation — models, routes, and ACL from your Prisma schema |
237
+
238
+ All commands go through `npx rapidd`:
239
+
240
+ ```bash
241
+ npx rapidd create-project # scaffold a new project (@rapidd/core)
242
+ npx rapidd build # generate from schema (@rapidd/build)
243
+ ```
212
244
 
213
245
  ---
214
246
 
package/bin/cli.js CHANGED
@@ -2,20 +2,40 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const { execFileSync } = require('child_process');
5
6
 
6
7
  const COMMANDS = { 'create-project': createProject };
7
8
 
8
9
  const args = process.argv.slice(2);
9
10
  const command = args[0];
10
11
 
11
- if (!command || !COMMANDS[command]) {
12
+ if (!command) {
12
13
  console.log('Usage: npx rapidd <command>\n');
13
14
  console.log('Commands:');
14
15
  console.log(' create-project Scaffold a new Rapidd project in the current directory');
15
- process.exit(command ? 1 : 0);
16
+ console.log(' build Generate models, routes & ACL from Prisma schema (@rapidd/build)');
17
+ process.exit(0);
16
18
  }
17
19
 
18
- COMMANDS[command](args.slice(1));
20
+ if (COMMANDS[command]) {
21
+ COMMANDS[command](args.slice(1));
22
+ } else if (command === 'build') {
23
+ // Proxy to @rapidd/build
24
+ try {
25
+ const buildBin = require.resolve('@rapidd/build/bin/cli.js');
26
+ execFileSync(process.execPath, [buildBin, ...args], { stdio: 'inherit' });
27
+ } catch (err) {
28
+ if (err.code === 'MODULE_NOT_FOUND') {
29
+ console.error('@rapidd/build is not installed.\n');
30
+ console.error(' npm install -D @rapidd/build');
31
+ process.exit(1);
32
+ }
33
+ process.exit(err.status ?? 1);
34
+ }
35
+ } else {
36
+ console.error(`Unknown command: ${command}`);
37
+ process.exit(1);
38
+ }
19
39
 
20
40
  function createProject() {
21
41
  const targetDir = process.cwd();
package/config/app.json CHANGED
@@ -151,6 +151,9 @@
151
151
  "name": "Support Team"
152
152
  }
153
153
  },
154
+ "endpointAuthMethod": {
155
+ "/api/v1": null
156
+ },
154
157
  "languages": [
155
158
  "en_US",
156
159
  "es_ES",
package/main.ts CHANGED
@@ -16,6 +16,12 @@ export async function start(): Promise<void> {
16
16
  await app.listen({ port, host });
17
17
  console.log(`[Rapidd] Server running at http://${host}:${port}`);
18
18
  console.log(`[Rapidd] Environment: ${getEnv('NODE_ENV')}`);
19
+
20
+ // Warn if running compiled build with development NODE_ENV
21
+ if (process.argv[1]?.includes('/dist/') && getEnv('NODE_ENV') === 'development') {
22
+ console.warn('[Rapidd] Warning: Running compiled build with NODE_ENV=development.');
23
+ console.warn('[Rapidd] Set NODE_ENV=production in your .env for production use.');
24
+ }
19
25
  } catch (err) {
20
26
  console.error('[Startup Error]', (err as Error).message);
21
27
  process.exit(1);
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "2.1.0",
6
+ "version": "2.1.2",
7
7
  "description": "Code-first REST API framework for TypeScript. Database in, API out.",
8
8
  "main": "dist/main.js",
9
9
  "bin": {
@@ -83,7 +83,7 @@
83
83
  "pg": "^8.16.3"
84
84
  },
85
85
  "devDependencies": {
86
- "@rapidd/build": "^2.1.3",
86
+ "@rapidd/build": "^2.1.5",
87
87
  "@types/bcrypt": "^6.0.0",
88
88
  "@types/ejs": "^3.1.5",
89
89
  "@types/jest": "^30.0.0",
@@ -1,12 +1,11 @@
1
1
  import { FastifyPluginAsync, FastifyRequest, FastifyReply } from 'fastify';
2
- import { Auth } from '../../../src/auth/Auth';
3
2
  import { ErrorResponse } from '../../../src/core/errors';
4
3
 
5
4
  /**
6
5
  * Auth routes — /register, /login, /logout, /refresh, /me
7
6
  */
8
7
  const rootRoutes: FastifyPluginAsync = async (fastify) => {
9
- const auth = fastify.auth ?? new Auth();
8
+ const auth = fastify.auth;
10
9
 
11
10
  /**
12
11
  * POST /register
package/src/app.ts CHANGED
@@ -22,6 +22,13 @@ import rlsPlugin from './plugins/rls';
22
22
 
23
23
  import type { RapiddOptions } from './types';
24
24
 
25
+ // ─── BigInt Serialization ────────────────────────────
26
+ // Prisma returns BigInt values that JSON.stringify cannot handle natively.
27
+ // This polyfill converts them to strings during serialization.
28
+ (BigInt.prototype as any).toJSON = function () {
29
+ return this.toString();
30
+ };
31
+
25
32
  // ─── Path Setup ─────────────────────────────────────
26
33
  // Use process.cwd() as the project root — works from both source (tsx) and compiled (dist/) contexts.
27
34
 
package/src/auth/Auth.ts CHANGED
@@ -5,7 +5,7 @@ import { authPrisma } from '../core/prisma';
5
5
  import { ErrorResponse } from '../core/errors';
6
6
  import { createStore, SessionStoreManager } from './stores';
7
7
  import { loadDMMF, findUserModel, findIdentifierFields, findPasswordField } from '../core/dmmf';
8
- import type { RapiddUser, AuthOptions, AuthStrategy, ISessionStore } from '../types';
8
+ import type { RapiddUser, AuthOptions, AuthMethod, ISessionStore } from '../types';
9
9
 
10
10
  /**
11
11
  * Authentication class for user login, logout, and session management
@@ -26,7 +26,7 @@ import type { RapiddUser, AuthOptions, AuthStrategy, ISessionStore } from '../ty
26
26
  export class Auth {
27
27
  options: Required<Pick<AuthOptions, 'passwordField' | 'saltRounds'>> & AuthOptions & {
28
28
  identifierFields: string[];
29
- strategies: AuthStrategy[];
29
+ methods: AuthMethod[];
30
30
  cookieName: string;
31
31
  customHeaderName: string;
32
32
  session: { ttl: number; store?: string };
@@ -68,8 +68,8 @@ export class Auth {
68
68
  ...options.jwt,
69
69
  },
70
70
  saltRounds: options.saltRounds || parseInt(process.env.AUTH_SALT_ROUNDS || '10', 10),
71
- strategies: options.strategies
72
- || (process.env.AUTH_STRATEGIES?.split(',').map(s => s.trim()) as AuthStrategy[])
71
+ methods: options.methods
72
+ || (process.env.AUTH_METHODS?.split(',').map(s => s.trim()) as AuthMethod[])
73
73
  || ['bearer'],
74
74
  cookieName: options.cookieName || process.env.AUTH_COOKIE_NAME || 'token',
75
75
  customHeaderName: options.customHeaderName || process.env.AUTH_CUSTOM_HEADER || 'X-Auth-Token',
package/src/index.ts CHANGED
@@ -73,7 +73,7 @@ export { buildApp } from './app';
73
73
  // ── Types ────────────────────────────────────────────────────────────────────
74
74
  export type {
75
75
  RapiddUser,
76
- AuthStrategy,
76
+ AuthMethod,
77
77
  RouteAuthConfig,
78
78
  ModelOptions,
79
79
  GetManyResult,
package/src/orm/Model.ts CHANGED
@@ -177,6 +177,7 @@ class Model {
177
177
  const field = this.fields[fieldName];
178
178
  if (!field) return value;
179
179
  if (field.type === 'Int') return parseInt(value, 10);
180
+ if (field.type === 'BigInt') return BigInt(value);
180
181
  if (field.type === 'Float' || field.type === 'Decimal') return parseFloat(value);
181
182
  if (field.type === 'Boolean') return value === 'true';
182
183
  return value;
@@ -239,10 +240,13 @@ class Model {
239
240
  sortBy = sortBy?.trim();
240
241
  sortOrder = sortOrder?.trim();
241
242
 
242
- // Validate sort field - fall back to default for composite PK names
243
+ // Validate sort field - fall back to default for composite PK names or missing 'id'
243
244
  if (!sortBy.includes('.') && this.fields[sortBy] == undefined) {
244
- // If the sortBy is a composite key name (e.g., "email_companyId"), use first PK field
245
245
  if (sortBy === this.primaryKey && this.isCompositePK) {
246
+ // Composite key name (e.g., "email_companyId") → use first PK field
247
+ sortBy = this.defaultSortField;
248
+ } else if (sortBy === 'id') {
249
+ // Model doesn't have an 'id' field → fall back to actual primary key
246
250
  sortBy = this.defaultSortField;
247
251
  } else {
248
252
  throw new ErrorResponse(400, "invalid_sort_field", { sortBy, modelName: this.constructor.name });
@@ -501,10 +501,10 @@ class QueryBuilder {
501
501
  /**
502
502
  * Parse numeric filter operators
503
503
  */
504
- #filterNumber(value: string): Record<string, number> | null {
504
+ #filterNumber(value: string): Record<string, number | bigint> | null {
505
505
  const numOperators = ['lt:', 'lte:', 'gt:', 'gte:', 'eq:', 'ne:', 'between:'];
506
506
  const foundOperator = numOperators.find((op: string) => value.startsWith(op));
507
- let numValue: string | number = value;
507
+ let numValue: string = value;
508
508
  let prismaOp = 'equals';
509
509
 
510
510
  if (foundOperator) {
@@ -517,19 +517,36 @@ class QueryBuilder {
517
517
  case 'eq:': prismaOp = 'equals'; break;
518
518
  case 'ne:': prismaOp = 'not'; break;
519
519
  case 'between:': {
520
- // Support between for decimals: between:1.5;3.7
521
- const [start, end] = (numValue as string).split(';').map((v: string) => parseFloat(v.trim()));
522
- if (isNaN(start) || isNaN(end)) return null;
520
+ const parts = numValue.split(';').map((v: string) => v.trim());
521
+ const [startStr, endStr] = parts;
522
+ const start = this.#parseNumericValue(startStr);
523
+ const end = this.#parseNumericValue(endStr);
524
+ if (start == null || end == null) return null;
523
525
  return { gte: start, lte: end };
524
526
  }
525
527
  }
526
528
  }
527
529
 
528
- // Support decimal numbers
529
- numValue = parseFloat(numValue as string);
530
- if (isNaN(numValue)) return null;
530
+ const parsed = this.#parseNumericValue(numValue);
531
+ if (parsed == null) return null;
531
532
 
532
- return { [prismaOp]: numValue };
533
+ return { [prismaOp]: parsed };
534
+ }
535
+
536
+ /**
537
+ * Parse a numeric string, returning BigInt for integers beyond safe range
538
+ */
539
+ #parseNumericValue(value: string): number | bigint | null {
540
+ if (value.includes('.')) {
541
+ const num = parseFloat(value);
542
+ return isNaN(num) ? null : num;
543
+ }
544
+ const num = Number(value);
545
+ if (isNaN(num)) return null;
546
+ if (!Number.isSafeInteger(num)) {
547
+ try { return BigInt(value); } catch { return null; }
548
+ }
549
+ return num;
533
550
  }
534
551
 
535
552
  /**
@@ -1,8 +1,9 @@
1
+ import path from 'path';
1
2
  import { FastifyPluginAsync, FastifyRequest } from 'fastify';
2
3
  import fp from 'fastify-plugin';
3
4
  import { Auth } from '../auth/Auth';
4
5
  import { ErrorResponse } from '../core/errors';
5
- import type { RapiddUser, AuthOptions, AuthStrategy, RouteAuthConfig } from '../types';
6
+ import type { RapiddUser, AuthOptions, AuthMethod, RouteAuthConfig } from '../types';
6
7
 
7
8
  interface AuthPluginOptions {
8
9
  auth?: Auth;
@@ -35,20 +36,54 @@ const authPlugin: FastifyPluginAsync<AuthPluginOptions> = async (fastify, option
35
36
  return;
36
37
  }
37
38
 
38
- // Parse auth on every request using configured strategies (checked in order).
39
- // Routes can override via config.auth: { strategies, cookieName, customHeaderName }
39
+ // Load endpointAuthMethod from config/app.json (prefix method(s) mapping)
40
+ let endpointAuthMethod: Record<string, AuthMethod | AuthMethod[] | null> = {};
41
+ try {
42
+ const appConfig = require(path.join(process.cwd(), 'config', 'app.json'));
43
+ if (appConfig.endpointAuthMethod) {
44
+ endpointAuthMethod = appConfig.endpointAuthMethod;
45
+ }
46
+ } catch {
47
+ // No app.json or no endpointAuthMethod — use global default
48
+ }
49
+
50
+ // Pre-sort prefixes by length (longest first) for correct matching
51
+ const sortedPrefixes = Object.keys(endpointAuthMethod)
52
+ .sort((a, b) => b.length - a.length);
53
+
54
+ // Parse auth on every request using configured methods (checked in order).
55
+ // Priority: route config > endpointAuthMethod prefix match > global default
40
56
  fastify.addHook('onRequest', async (request) => {
41
57
  const routeAuth = (request.routeOptions?.config as any)?.auth as RouteAuthConfig | undefined;
42
- const strategies = routeAuth?.strategies || auth.options.strategies;
58
+
59
+ let methods: AuthMethod[];
60
+ if (routeAuth?.methods) {
61
+ methods = routeAuth.methods;
62
+ } else {
63
+ const matchedPrefix = sortedPrefixes.find(p => request.url.startsWith(p));
64
+ if (matchedPrefix) {
65
+ const value = endpointAuthMethod[matchedPrefix];
66
+ if (value === null) {
67
+ methods = auth.options.methods;
68
+ } else if (typeof value === 'string') {
69
+ methods = [value];
70
+ } else {
71
+ methods = value;
72
+ }
73
+ } else {
74
+ methods = auth.options.methods;
75
+ }
76
+ }
77
+
43
78
  const cookieName = routeAuth?.cookieName || auth.options.cookieName;
44
79
  const customHeaderName = routeAuth?.customHeaderName || auth.options.customHeaderName;
45
80
 
46
81
  let user: RapiddUser | null = null;
47
82
 
48
- for (const strategy of strategies) {
83
+ for (const method of methods) {
49
84
  if (user) break;
50
85
 
51
- switch (strategy) {
86
+ switch (method) {
52
87
  case 'bearer': {
53
88
  const h = request.headers.authorization;
54
89
  if (h?.startsWith('Bearer ')) {
@@ -90,7 +125,7 @@ const authPlugin: FastifyPluginAsync<AuthPluginOptions> = async (fastify, option
90
125
  fastify.post('/auth/login', async (request, reply) => {
91
126
  const result = await auth.login(request.body as { user: string; password: string });
92
127
 
93
- if (auth.options.strategies.includes('cookie')) {
128
+ if (auth.options.methods.includes('cookie')) {
94
129
  reply.setCookie(auth.options.cookieName, result.accessToken, {
95
130
  path: '/',
96
131
  httpOnly: true,
@@ -106,7 +141,7 @@ const authPlugin: FastifyPluginAsync<AuthPluginOptions> = async (fastify, option
106
141
  fastify.post('/auth/logout', async (request, reply) => {
107
142
  const result = await auth.logout(request.headers.authorization);
108
143
 
109
- if (auth.options.strategies.includes('cookie')) {
144
+ if (auth.options.methods.includes('cookie')) {
110
145
  reply.clearCookie(auth.options.cookieName, { path: '/' });
111
146
  }
112
147
 
package/src/types.ts CHANGED
@@ -10,10 +10,10 @@ export interface RapiddUser {
10
10
  [key: string]: unknown;
11
11
  }
12
12
 
13
- export type AuthStrategy = 'bearer' | 'basic' | 'cookie' | 'header';
13
+ export type AuthMethod = 'bearer' | 'basic' | 'cookie' | 'header';
14
14
 
15
15
  export interface RouteAuthConfig {
16
- strategies?: AuthStrategy[];
16
+ methods?: AuthMethod[];
17
17
  cookieName?: string;
18
18
  customHeaderName?: string;
19
19
  }
@@ -32,7 +32,7 @@ export interface AuthOptions {
32
32
  refreshExpiry?: string;
33
33
  };
34
34
  saltRounds?: number;
35
- strategies?: AuthStrategy[];
35
+ methods?: AuthMethod[];
36
36
  cookieName?: string;
37
37
  customHeaderName?: string;
38
38
  }