@zarex/fastify 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +157 -2
- package/dist/index.cjs +176 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +63 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +158 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kodjogbe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,158 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zarex/fastify
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Hook Zarex pour Fastify — protège votre application contre les bots, les abus de trafic, les injections (SQL, XSS, SSRF...) et les fuites de données sensibles, en quelques lignes.
|
|
4
|
+
|
|
5
|
+
Compatible Fastify 4 et 5 (aucune dépendance à `fastify` à l'exécution). Fonctionne aussi avec NestJS (plateforme Fastify).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @zarex/fastify
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Démarrage rapide
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import Fastify from "fastify";
|
|
17
|
+
import { zarexFastify, shield, rateLimit } from "@zarex/fastify";
|
|
18
|
+
|
|
19
|
+
const app = Fastify();
|
|
20
|
+
|
|
21
|
+
app.addHook(
|
|
22
|
+
"onRequest",
|
|
23
|
+
zarexFastify({
|
|
24
|
+
key: process.env.ZAREX_KEY!,
|
|
25
|
+
rules: [shield(), rateLimit({ max: 100, window: "1m" })],
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
app.get("/", async () => ({ ok: true }));
|
|
30
|
+
app.listen({ port: 3000 });
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Une requête refusée reçoit automatiquement `403 Forbidden` (ou `429 Too Many Requests` avec l'en-tête `retry-after` pour une limite de débit) — sans aucun code supplémentaire.
|
|
34
|
+
|
|
35
|
+
**Fail-open** : si Zarex est injoignable, lent ou répond de façon inattendue, la requête passe toujours. Une panne chez Zarex ne doit jamais faire tomber votre application.
|
|
36
|
+
|
|
37
|
+
### Avec NestJS (plateforme Fastify)
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import type { FastifyInstance } from "fastify";
|
|
41
|
+
(app.getHttpAdapter().getInstance() as FastifyInstance).addHook(
|
|
42
|
+
"onRequest",
|
|
43
|
+
zarexFastify({ key, rules: [shield()] })
|
|
44
|
+
);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## L'adresse IP du visiteur
|
|
48
|
+
|
|
49
|
+
Par défaut (`ipSource` non renseigné), l'adaptateur utilise `request.ip`, calculé par Fastify selon l'option [`trustProxy`](https://fastify.dev/docs/latest/Reference/Server/#trustproxy) de votre serveur :
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const app = Fastify({ trustProxy: 1 }); // 1 proxy de confiance devant Fastify
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Sans `trustProxy`, Fastify ignore `X-Forwarded-For` — configurez-le pour correspondre à **votre** infrastructure réelle, sinon tous vos visiteurs apparaissent sous la même adresse IP et toute règle de limite de débit basée sur l'IP devient inutile.
|
|
56
|
+
|
|
57
|
+
## Options
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
zarexFastify({
|
|
61
|
+
key: string, // obligatoire : clé API du projet
|
|
62
|
+
rules: Rule[], // obligatoire : 1 à 10 règles, voir ci-dessous
|
|
63
|
+
ipSource?: IpSource, // défaut "framework" (voir ci-dessus)
|
|
64
|
+
inspectBody?: boolean, // défaut false — voir "Inspecter le corps"
|
|
65
|
+
secureHeaders?: boolean | SecureHeadersOptions,
|
|
66
|
+
responseGuard?: boolean | ResponseGuardOptions, // voir zarexFastifyOnSend ci-dessous
|
|
67
|
+
onDeny?: (req, reply, decision) => void,
|
|
68
|
+
onDegraded?: (req, decision) => void,
|
|
69
|
+
call?: (req) => ProtectCall | undefined,
|
|
70
|
+
timeoutMs?: number, // défaut 500 ms
|
|
71
|
+
breaker?: BreakerOptions | false,
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Inspecter le corps de la requête (`inspectBody`) — attention au hook choisi
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// ⚠️ "onRequest" : Fastify n'a PAS ENCORE lu le corps à ce stade — inspectBody ne verrait jamais rien.
|
|
79
|
+
app.addHook("onRequest", zarexFastify({ ..., inspectBody: true })); // piège
|
|
80
|
+
|
|
81
|
+
// ✅ "preHandler" : le corps est déjà disponible.
|
|
82
|
+
app.addHook("preHandler", zarexFastify({ ..., inspectBody: true }));
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Si vous enregistrez le hook en `onRequest` avec `inspectBody: true` alors qu'une requête a manifestement un corps (`Content-Length` > 0), un avertissement unique s'affiche dans la console (`console.warn`) pour éviter que cette erreur passe inaperçue.
|
|
86
|
+
|
|
87
|
+
### En-têtes de sécurité (`secureHeaders`)
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
app.addHook("onRequest", zarexFastify({ ..., secureHeaders: true }));
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Ajoutés à **toutes** les réponses, y compris les refus. `true` applique des défauts prudents (HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy...). Retire aussi `x-powered-by`.
|
|
94
|
+
|
|
95
|
+
### Surveillance des réponses (`responseGuard`) — un hook séparé
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
app.addHook("onSend", zarexFastifyOnSend({ responseGuard: true }));
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Contrairement aux autres adaptateurs, Fastify demande d'enregistrer un **second hook** (`onSend`) pour surveiller les réponses de votre application et masquer (ou bloquer) les cartes bancaires, clés d'API, jetons JWT, mots de passe... qui s'y retrouveraient par erreur. Les flux (streams) ne sont pas analysés.
|
|
102
|
+
|
|
103
|
+
### Identifiants personnalisés (`call`)
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
app.addHook(
|
|
107
|
+
"preHandler",
|
|
108
|
+
zarexFastify({
|
|
109
|
+
key,
|
|
110
|
+
rules: [rateLimit({ max: 100, window: "1m", characteristics: ["userId"] })],
|
|
111
|
+
call: (req) => ({ characteristics: { userId: (req as any).user?.id } }),
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Les valeurs sont hachées en SHA-256 avant l'envoi à Zarex. Une erreur dans cette fonction est silencieusement ignorée. `call` peut aussi fournir `tokensRequested`, `email`/`phone` ou `text` (voir [`@zarex/core`](https://www.npmjs.com/package/@zarex/core)).
|
|
117
|
+
|
|
118
|
+
### Lire le résultat de la décision
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
app.get("/", async (req) => {
|
|
122
|
+
console.log(req.zarex?.verdict); // "ALLOW" | "DENY" | "ERROR"
|
|
123
|
+
return { ok: true };
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Règles disponibles
|
|
128
|
+
|
|
129
|
+
| Fonction | Résumé |
|
|
130
|
+
|---|---|
|
|
131
|
+
| `shield(opts?)` | Pare-feu applicatif : injection SQL, XSS, traversée de répertoires, SSRF, SSTI... |
|
|
132
|
+
| `rateLimit({ max, window, ... })` | Limite de requêtes par visiteur sur une fenêtre de temps. |
|
|
133
|
+
| `tokenBucket({ capacity, refillRate, interval })` | Budget de jetons avec pics autorisés. |
|
|
134
|
+
| `detectBot(opts?)` | Détecte les robots connus, avec vérification DNS inversée. |
|
|
135
|
+
| `requestFilter({ expressions, logic })` | Règles personnalisées sur IP, chemin, en-têtes, cookies, pays... |
|
|
136
|
+
| `sensitiveInfo(opts?)` | Refuse une requête contenant une donnée sensible (nécessite `inspectBody`). |
|
|
137
|
+
| `emailValidation(opts?)` | Valide un email donné via `call: () => ({ email })`. |
|
|
138
|
+
| `phoneValidation(opts?)` | Valide un numéro donné, orienté opérateurs africains (MTN, Moov, Orange, Wave...). |
|
|
139
|
+
| `promptInjection(opts?)` | Détecte les tentatives de manipulation d'un assistant IA, via `call: () => ({ text })`. |
|
|
140
|
+
|
|
141
|
+
Chaque règle accepte `mode: "LIVE" | "DRY_RUN" | "OFF"`. Détails complets des options : voir [`@zarex/core`](https://www.npmjs.com/package/@zarex/core).
|
|
142
|
+
|
|
143
|
+
## Réponse de refus personnalisée (`onDeny`)
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
zarexFastify({
|
|
147
|
+
...,
|
|
148
|
+
onDeny: (req, reply, decision) => {
|
|
149
|
+
reply.code(403).send({ message: "Accès refusé", id: decision.id });
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Sans `onDeny`, la réponse par défaut est `403` (ou `429` + `retry-after`), au format `{ error: { code, message, request_id } }`.
|
|
155
|
+
|
|
156
|
+
## Licence
|
|
157
|
+
|
|
158
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
CLOUDFLARE_RANGES: () => import_core2.CLOUDFLARE_RANGES,
|
|
24
|
+
PRIVATE_NETWORKS: () => import_core2.PRIVATE_NETWORKS,
|
|
25
|
+
createZarex: () => import_core2.createZarex,
|
|
26
|
+
detectBot: () => import_core2.detectBot,
|
|
27
|
+
emailValidation: () => import_core2.emailValidation,
|
|
28
|
+
phoneValidation: () => import_core2.phoneValidation,
|
|
29
|
+
promptInjection: () => import_core2.promptInjection,
|
|
30
|
+
rateLimit: () => import_core2.rateLimit,
|
|
31
|
+
requestFilter: () => import_core2.requestFilter,
|
|
32
|
+
secureHeaders: () => import_core2.secureHeaders,
|
|
33
|
+
sensitiveInfo: () => import_core2.sensitiveInfo,
|
|
34
|
+
shield: () => import_core2.shield,
|
|
35
|
+
tokenBucket: () => import_core2.tokenBucket,
|
|
36
|
+
withSecureHeaders: () => import_core2.withSecureHeaders,
|
|
37
|
+
zarexFastify: () => zarexFastify,
|
|
38
|
+
zarexFastifyOnSend: () => zarexFastifyOnSend
|
|
39
|
+
});
|
|
40
|
+
module.exports = __toCommonJS(index_exports);
|
|
41
|
+
var import_core = require("@zarex/core");
|
|
42
|
+
var import_core2 = require("@zarex/core");
|
|
43
|
+
function requestLooksLikeItHasAnUnparsedBody(req) {
|
|
44
|
+
if (req.body !== void 0) return false;
|
|
45
|
+
const te = req.headers["transfer-encoding"];
|
|
46
|
+
if (typeof te === "string" ? te.length > 0 : Array.isArray(te) && te.length > 0) return true;
|
|
47
|
+
const cl = req.headers["content-length"];
|
|
48
|
+
const clNum = typeof cl === "string" ? Number(cl) : Array.isArray(cl) ? Number(cl[0]) : NaN;
|
|
49
|
+
return Number.isFinite(clNum) && clNum > 0;
|
|
50
|
+
}
|
|
51
|
+
function bodyToString(body) {
|
|
52
|
+
if (body === void 0 || body === null) return void 0;
|
|
53
|
+
if (typeof body === "string") return body;
|
|
54
|
+
try {
|
|
55
|
+
const s = JSON.stringify(body);
|
|
56
|
+
return typeof s === "string" ? s : void 0;
|
|
57
|
+
} catch {
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function defaultDeny(_req, reply, d) {
|
|
62
|
+
const limited = d.deniedBy?.ruleType === "RATE_LIMIT" || d.deniedBy?.ruleType === "TOKEN_BUCKET";
|
|
63
|
+
if (limited) {
|
|
64
|
+
if (d.retryAfterSeconds !== void 0) reply.header("retry-after", String(d.retryAfterSeconds));
|
|
65
|
+
reply.code(429).send({ error: { code: "rate_limited", message: "Too many requests", request_id: d.id } });
|
|
66
|
+
} else {
|
|
67
|
+
reply.code(403).send({ error: { code: "forbidden", message: "Request blocked", request_id: d.id } });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function zarexFastify(options, extra = {}) {
|
|
71
|
+
const isInstance = typeof options.protect === "function";
|
|
72
|
+
const opts = isInstance ? extra : options;
|
|
73
|
+
const instance = isInstance ? options : (0, import_core.createZarex)({ ...options, ipSource: options.ipSource ?? "framework" });
|
|
74
|
+
const prepared = (0, import_core.prepareSecureHeaders)(opts.secureHeaders);
|
|
75
|
+
let warnedAboutUnparsedBody = false;
|
|
76
|
+
return async function zarexHook(req, reply) {
|
|
77
|
+
let decision;
|
|
78
|
+
try {
|
|
79
|
+
if (prepared) {
|
|
80
|
+
const set = (0, import_core.isLocalHost)(req.hostname ?? (typeof req.headers["host"] === "string" ? req.headers["host"] : void 0)) ? prepared.http : prepared.https;
|
|
81
|
+
for (const [k, v] of Object.entries(set)) reply.header(k, v);
|
|
82
|
+
if (prepared.removePoweredBy) reply.removeHeader?.("x-powered-by");
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
let call;
|
|
88
|
+
try {
|
|
89
|
+
call = opts.call?.(req);
|
|
90
|
+
} catch {
|
|
91
|
+
call = void 0;
|
|
92
|
+
}
|
|
93
|
+
const host = typeof req.headers["host"] === "string" ? req.headers["host"] : req.hostname;
|
|
94
|
+
if (opts.inspectBody && !warnedAboutUnparsedBody && requestLooksLikeItHasAnUnparsedBody(req)) {
|
|
95
|
+
warnedAboutUnparsedBody = true;
|
|
96
|
+
console.warn(
|
|
97
|
+
'[zarex] fastify: `inspectBody: true` but request.body is still undefined on a request that has one. This hook is probably registered on "onRequest", where Fastify has not parsed the body yet \u2014 register it as "preHandler" instead, otherwise the body is silently never sent to the engine.'
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
decision = await instance.protect(
|
|
101
|
+
{
|
|
102
|
+
method: req.method,
|
|
103
|
+
path: req.url,
|
|
104
|
+
host,
|
|
105
|
+
headers: req.headers,
|
|
106
|
+
socketIp: req.raw?.socket?.remoteAddress,
|
|
107
|
+
frameworkIp: req.ip,
|
|
108
|
+
body: opts.inspectBody ? bodyToString(req.body) : void 0
|
|
109
|
+
},
|
|
110
|
+
call
|
|
111
|
+
);
|
|
112
|
+
req.zarex = decision;
|
|
113
|
+
} catch {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (decision.denied) {
|
|
117
|
+
try {
|
|
118
|
+
(opts.onDeny ?? defaultDeny)(req, reply, decision);
|
|
119
|
+
} catch {
|
|
120
|
+
if (!reply.sent) defaultDeny(req, reply, decision);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (decision.degraded && opts.onDegraded) {
|
|
125
|
+
try {
|
|
126
|
+
opts.onDegraded(req, decision);
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function zarexFastifyOnSend(options) {
|
|
133
|
+
const guard = (0, import_core.normalizeGuardOptions)(options.responseGuard);
|
|
134
|
+
const decoder = new TextDecoder("utf-8");
|
|
135
|
+
return async function onSend(_req, reply, payload) {
|
|
136
|
+
if (!guard) return payload;
|
|
137
|
+
try {
|
|
138
|
+
const ct = String(reply.getHeader?.("content-type") ?? "");
|
|
139
|
+
const isString = typeof payload === "string";
|
|
140
|
+
const isBytes = payload instanceof Uint8Array;
|
|
141
|
+
if (!(isString || isBytes) || !(0, import_core.isScannableContentType)(ct)) return payload;
|
|
142
|
+
const r = (0, import_core.applyGuard)(isString ? payload : decoder.decode(payload), ct, guard);
|
|
143
|
+
if (r.action === "masked" || r.action === "blocked") {
|
|
144
|
+
reply.removeHeader?.("content-length");
|
|
145
|
+
reply.removeHeader?.("etag");
|
|
146
|
+
if (r.action === "blocked") {
|
|
147
|
+
reply.code(500);
|
|
148
|
+
reply.header("content-type", "application/json; charset=utf-8");
|
|
149
|
+
}
|
|
150
|
+
return r.body;
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
}
|
|
154
|
+
return payload;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
158
|
+
0 && (module.exports = {
|
|
159
|
+
CLOUDFLARE_RANGES,
|
|
160
|
+
PRIVATE_NETWORKS,
|
|
161
|
+
createZarex,
|
|
162
|
+
detectBot,
|
|
163
|
+
emailValidation,
|
|
164
|
+
phoneValidation,
|
|
165
|
+
promptInjection,
|
|
166
|
+
rateLimit,
|
|
167
|
+
requestFilter,
|
|
168
|
+
secureHeaders,
|
|
169
|
+
sensitiveInfo,
|
|
170
|
+
shield,
|
|
171
|
+
tokenBucket,
|
|
172
|
+
withSecureHeaders,
|
|
173
|
+
zarexFastify,
|
|
174
|
+
zarexFastifyOnSend
|
|
175
|
+
});
|
|
176
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\r\n applyGuard,\r\n createZarex,\r\n isLocalHost,\r\n isScannableContentType,\r\n normalizeGuardOptions,\r\n prepareSecureHeaders,\r\n type Decision,\r\n type ProtectCall,\r\n type ResponseGuardOptions,\r\n type SecureHeadersOptions,\r\n type Zarex,\r\n type ZarexOptions,\r\n} from \"@zarex/core\";\r\n\r\n/** Types minimaux de Fastify (pas de dépendance à `fastify` à l'exécution : Fastify 4 et 5). */\r\nexport interface FastifyReqLike {\r\n method: string;\r\n url: string;\r\n ip?: string | undefined;\r\n hostname?: string;\r\n headers: Record<string, string | string[] | undefined>;\r\n raw?: { socket?: { remoteAddress?: string | undefined } | undefined };\r\n body?: unknown;\r\n zarex?: Decision;\r\n}\r\nexport interface FastifyReplyLike {\r\n code(status: number): this;\r\n header(name: string, value: string): this;\r\n send(payload?: unknown): this;\r\n getHeader?(name: string): unknown;\r\n removeHeader?(name: string): unknown;\r\n sent?: boolean;\r\n}\r\n\r\nexport interface FastifyZarexOptions extends Omit<ZarexOptions, \"ipSource\"> {\r\n /**\r\n * Défaut : \"framework\" — l'adresse est celle de `request.ip`, que Fastify calcule selon l'option\r\n * `trustProxy` de VOTRE serveur (`Fastify({ trustProxy: 1 })`). Sans `trustProxy`, Fastify ignore X-Forwarded-For.\r\n */\r\n ipSource?: ZarexOptions[\"ipSource\"];\r\n /** En-têtes de sécurité ajoutés à toutes les réponses (true = défauts prudents). Retire X-Powered-By. */\r\n secureHeaders?: SecureHeadersOptions | boolean;\r\n /** Surveillance des réponses : enregistrez aussi `zarexFastifyOnSend(options)` comme hook `onSend`. */\r\n responseGuard?: ResponseGuardOptions | boolean;\r\n /** Nécessite d'enregistrer le hook en `preHandler` (le corps n'est analysé qu'après `onRequest`). */\r\n inspectBody?: boolean;\r\n onDeny?: (req: FastifyReqLike, reply: FastifyReplyLike, decision: Decision) => void;\r\n onDegraded?: (req: FastifyReqLike, decision: Decision) => void;\r\n call?: (req: FastifyReqLike) => ProtectCall | undefined;\r\n}\r\n\r\n// Trouvé lors de l'audit du 04/11/2026 (voir AUDIT_SECURITE_COMPLET.md) : l'exemple JSDoc principal\r\n// de ce fichier montre le hook enregistré en \"onRequest\", où Fastify n'a pas encore analysé le\r\n// corps — `inspectBody: true` n'y transmet alors jamais rien au moteur, SANS AUCUN avertissement.\r\n// Heuristique : `req.body` indéfini alors que la requête annonce clairement un corps\r\n// (Content-Length > 0 ou Transfer-Encoding) signale presque certainement ce piège plutôt qu'une\r\n// requête sans corps (GET…), pour laquelle il n'y a rien à signaler.\r\nfunction requestLooksLikeItHasAnUnparsedBody(req: FastifyReqLike): boolean {\r\n if (req.body !== undefined) return false;\r\n const te = req.headers[\"transfer-encoding\"];\r\n if (typeof te === \"string\" ? te.length > 0 : Array.isArray(te) && te.length > 0) return true;\r\n const cl = req.headers[\"content-length\"];\r\n const clNum = typeof cl === \"string\" ? Number(cl) : Array.isArray(cl) ? Number(cl[0]) : NaN;\r\n return Number.isFinite(clNum) && clNum > 0;\r\n}\r\n\r\nfunction bodyToString(body: unknown): string | undefined {\r\n if (body === undefined || body === null) return undefined;\r\n if (typeof body === \"string\") return body;\r\n try {\r\n const s = JSON.stringify(body);\r\n return typeof s === \"string\" ? s : undefined;\r\n } catch {\r\n return undefined;\r\n }\r\n}\r\n\r\nfunction defaultDeny(_req: FastifyReqLike, reply: FastifyReplyLike, d: Decision): void {\r\n const limited = d.deniedBy?.ruleType === \"RATE_LIMIT\" || d.deniedBy?.ruleType === \"TOKEN_BUCKET\";\r\n if (limited) {\r\n if (d.retryAfterSeconds !== undefined) reply.header(\"retry-after\", String(d.retryAfterSeconds));\r\n reply.code(429).send({ error: { code: \"rate_limited\", message: \"Too many requests\", request_id: d.id } });\r\n } else {\r\n reply.code(403).send({ error: { code: \"forbidden\", message: \"Request blocked\", request_id: d.id } });\r\n }\r\n}\r\n\r\n/**\r\n * app.addHook(\"onRequest\", zarexFastify({ key, baseUrl, rules: [shield()] }));\r\n *\r\n * Avec `inspectBody: true`, enregistrez le hook en \"preHandler\" (pas \"onRequest\" ci-dessus) :\r\n * Fastify n'a pas encore lu le corps en \"onRequest\", le moteur ne verrait jamais rien.\r\n * app.addHook(\"preHandler\", zarexFastify({ key, baseUrl, rules: [shield()], inspectBody: true }));\r\n *\r\n * Fonctionne aussi avec NestJS (plateforme Fastify) :\r\n * (app.getHttpAdapter().getInstance() as FastifyInstance).addHook(\"onRequest\", zarexFastify({...}));\r\n */\r\nexport function zarexFastify(options: FastifyZarexOptions | Zarex, extra: Pick<FastifyZarexOptions, \"inspectBody\" | \"onDeny\" | \"onDegraded\" | \"call\" | \"secureHeaders\" | \"responseGuard\"> = {}) {\r\n const isInstance = typeof (options as Zarex).protect === \"function\";\r\n const opts = (isInstance ? extra : options) as FastifyZarexOptions;\r\n const instance: Zarex = isInstance\r\n ? (options as Zarex)\r\n : createZarex({ ...(options as FastifyZarexOptions), ipSource: (options as FastifyZarexOptions).ipSource ?? \"framework\" });\r\n\r\n const prepared = prepareSecureHeaders(opts.secureHeaders);\r\n let warnedAboutUnparsedBody = false;\r\n\r\n return async function zarexHook(req: FastifyReqLike, reply: FastifyReplyLike): Promise<void> {\r\n let decision: Decision;\r\n try {\r\n if (prepared) {\r\n const set = isLocalHost(req.hostname ?? (typeof req.headers[\"host\"] === \"string\" ? (req.headers[\"host\"] as string) : undefined)) ? prepared.http : prepared.https;\r\n for (const [k, v] of Object.entries(set)) reply.header(k, v);\r\n if (prepared.removePoweredBy) reply.removeHeader?.(\"x-powered-by\");\r\n }\r\n } catch {\r\n /* sans effet sur la requête */\r\n }\r\n try {\r\n let call: ProtectCall | undefined;\r\n try {\r\n call = opts.call?.(req);\r\n } catch {\r\n call = undefined;\r\n }\r\n const host = typeof req.headers[\"host\"] === \"string\" ? (req.headers[\"host\"] as string) : req.hostname;\r\n if (opts.inspectBody && !warnedAboutUnparsedBody && requestLooksLikeItHasAnUnparsedBody(req)) {\r\n warnedAboutUnparsedBody = true;\r\n console.warn(\r\n '[zarex] fastify: `inspectBody: true` but request.body is still undefined on a request that has one. ' +\r\n 'This hook is probably registered on \"onRequest\", where Fastify has not parsed the body yet — register it ' +\r\n 'as \"preHandler\" instead, otherwise the body is silently never sent to the engine.'\r\n );\r\n }\r\n decision = await instance.protect(\r\n {\r\n method: req.method,\r\n path: req.url,\r\n host,\r\n headers: req.headers,\r\n socketIp: req.raw?.socket?.remoteAddress,\r\n frameworkIp: req.ip,\r\n body: opts.inspectBody ? bodyToString(req.body) : undefined,\r\n },\r\n call\r\n );\r\n req.zarex = decision;\r\n } catch {\r\n return;\r\n }\r\n if (decision.denied) {\r\n try {\r\n (opts.onDeny ?? defaultDeny)(req, reply, decision);\r\n } catch {\r\n /* si le refus personnalisé échoue, on retombe sur le refus par défaut */\r\n if (!reply.sent) defaultDeny(req, reply, decision);\r\n }\r\n return; // async hook : la réponse est déjà envoyée, Fastify s'arrête là (reply.sent)\r\n }\r\n if (decision.degraded && opts.onDegraded) {\r\n try {\r\n opts.onDegraded(req, decision);\r\n } catch {\r\n /* sans effet sur la requête */\r\n }\r\n }\r\n };\r\n}\r\n\r\n/**\r\n * Hook `onSend` : surveille les corps de réponse TEXTE (string / Buffer) et masque ou bloque les données sensibles.\r\n *\r\n * app.addHook(\"onSend\", zarexFastifyOnSend({ responseGuard: true }));\r\n *\r\n * Les flux (streams) ne sont pas analysés.\r\n */\r\nexport function zarexFastifyOnSend(options: Pick<FastifyZarexOptions, \"responseGuard\">) {\r\n const guard = normalizeGuardOptions(options.responseGuard);\r\n const decoder = new TextDecoder(\"utf-8\");\r\n return async function onSend(_req: unknown, reply: FastifyReplyLike, payload: unknown): Promise<unknown> {\r\n if (!guard) return payload;\r\n try {\r\n const ct = String(reply.getHeader?.(\"content-type\") ?? \"\");\r\n const isString = typeof payload === \"string\";\r\n const isBytes = payload instanceof Uint8Array;\r\n if (!(isString || isBytes) || !isScannableContentType(ct)) return payload;\r\n const r = applyGuard(isString ? payload : decoder.decode(payload), ct, guard);\r\n if (r.action === \"masked\" || r.action === \"blocked\") {\r\n reply.removeHeader?.(\"content-length\"); // Fastify le recalcule\r\n reply.removeHeader?.(\"etag\");\r\n if (r.action === \"blocked\") {\r\n reply.code(500);\r\n reply.header(\"content-type\", \"application/json; charset=utf-8\");\r\n }\r\n return r.body;\r\n }\r\n } catch {\r\n /* jamais casser la réponse */\r\n }\r\n return payload;\r\n };\r\n}\r\n\r\nexport {\r\n shield,\r\n rateLimit,\r\n tokenBucket,\r\n detectBot,\r\n requestFilter,\r\n sensitiveInfo,\r\n emailValidation,\r\n phoneValidation,\r\n promptInjection,\r\n secureHeaders,\r\n withSecureHeaders,\r\n createZarex,\r\n PRIVATE_NETWORKS,\r\n CLOUDFLARE_RANGES,\r\n} from \"@zarex/core\";\r\nexport type { SecureHeadersOptions, ResponseGuardOptions, ResponseGuardEvent } from \"@zarex/core\";\r\nexport type { Decision, Zarex, ZarexOptions, IpSource, Rule, ProtectCall } from \"@zarex/core\";"],"mappings":"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","names":["import_core"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Decision, ZarexOptions, SecureHeadersOptions, ResponseGuardOptions, ProtectCall, Zarex } from '@zarex/core';
|
|
2
|
+
export { CLOUDFLARE_RANGES, Decision, IpSource, PRIVATE_NETWORKS, ProtectCall, ResponseGuardEvent, ResponseGuardOptions, Rule, SecureHeadersOptions, Zarex, ZarexOptions, createZarex, detectBot, emailValidation, phoneValidation, promptInjection, rateLimit, requestFilter, secureHeaders, sensitiveInfo, shield, tokenBucket, withSecureHeaders } from '@zarex/core';
|
|
3
|
+
|
|
4
|
+
/** Types minimaux de Fastify (pas de dépendance à `fastify` à l'exécution : Fastify 4 et 5). */
|
|
5
|
+
interface FastifyReqLike {
|
|
6
|
+
method: string;
|
|
7
|
+
url: string;
|
|
8
|
+
ip?: string | undefined;
|
|
9
|
+
hostname?: string;
|
|
10
|
+
headers: Record<string, string | string[] | undefined>;
|
|
11
|
+
raw?: {
|
|
12
|
+
socket?: {
|
|
13
|
+
remoteAddress?: string | undefined;
|
|
14
|
+
} | undefined;
|
|
15
|
+
};
|
|
16
|
+
body?: unknown;
|
|
17
|
+
zarex?: Decision;
|
|
18
|
+
}
|
|
19
|
+
interface FastifyReplyLike {
|
|
20
|
+
code(status: number): this;
|
|
21
|
+
header(name: string, value: string): this;
|
|
22
|
+
send(payload?: unknown): this;
|
|
23
|
+
getHeader?(name: string): unknown;
|
|
24
|
+
removeHeader?(name: string): unknown;
|
|
25
|
+
sent?: boolean;
|
|
26
|
+
}
|
|
27
|
+
interface FastifyZarexOptions extends Omit<ZarexOptions, "ipSource"> {
|
|
28
|
+
/**
|
|
29
|
+
* Défaut : "framework" — l'adresse est celle de `request.ip`, que Fastify calcule selon l'option
|
|
30
|
+
* `trustProxy` de VOTRE serveur (`Fastify({ trustProxy: 1 })`). Sans `trustProxy`, Fastify ignore X-Forwarded-For.
|
|
31
|
+
*/
|
|
32
|
+
ipSource?: ZarexOptions["ipSource"];
|
|
33
|
+
/** En-têtes de sécurité ajoutés à toutes les réponses (true = défauts prudents). Retire X-Powered-By. */
|
|
34
|
+
secureHeaders?: SecureHeadersOptions | boolean;
|
|
35
|
+
/** Surveillance des réponses : enregistrez aussi `zarexFastifyOnSend(options)` comme hook `onSend`. */
|
|
36
|
+
responseGuard?: ResponseGuardOptions | boolean;
|
|
37
|
+
/** Nécessite d'enregistrer le hook en `preHandler` (le corps n'est analysé qu'après `onRequest`). */
|
|
38
|
+
inspectBody?: boolean;
|
|
39
|
+
onDeny?: (req: FastifyReqLike, reply: FastifyReplyLike, decision: Decision) => void;
|
|
40
|
+
onDegraded?: (req: FastifyReqLike, decision: Decision) => void;
|
|
41
|
+
call?: (req: FastifyReqLike) => ProtectCall | undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* app.addHook("onRequest", zarexFastify({ key, baseUrl, rules: [shield()] }));
|
|
45
|
+
*
|
|
46
|
+
* Avec `inspectBody: true`, enregistrez le hook en "preHandler" (pas "onRequest" ci-dessus) :
|
|
47
|
+
* Fastify n'a pas encore lu le corps en "onRequest", le moteur ne verrait jamais rien.
|
|
48
|
+
* app.addHook("preHandler", zarexFastify({ key, baseUrl, rules: [shield()], inspectBody: true }));
|
|
49
|
+
*
|
|
50
|
+
* Fonctionne aussi avec NestJS (plateforme Fastify) :
|
|
51
|
+
* (app.getHttpAdapter().getInstance() as FastifyInstance).addHook("onRequest", zarexFastify({...}));
|
|
52
|
+
*/
|
|
53
|
+
declare function zarexFastify(options: FastifyZarexOptions | Zarex, extra?: Pick<FastifyZarexOptions, "inspectBody" | "onDeny" | "onDegraded" | "call" | "secureHeaders" | "responseGuard">): (req: FastifyReqLike, reply: FastifyReplyLike) => Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Hook `onSend` : surveille les corps de réponse TEXTE (string / Buffer) et masque ou bloque les données sensibles.
|
|
56
|
+
*
|
|
57
|
+
* app.addHook("onSend", zarexFastifyOnSend({ responseGuard: true }));
|
|
58
|
+
*
|
|
59
|
+
* Les flux (streams) ne sont pas analysés.
|
|
60
|
+
*/
|
|
61
|
+
declare function zarexFastifyOnSend(options: Pick<FastifyZarexOptions, "responseGuard">): (_req: unknown, reply: FastifyReplyLike, payload: unknown) => Promise<unknown>;
|
|
62
|
+
|
|
63
|
+
export { type FastifyReplyLike, type FastifyReqLike, type FastifyZarexOptions, zarexFastify, zarexFastifyOnSend };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Decision, ZarexOptions, SecureHeadersOptions, ResponseGuardOptions, ProtectCall, Zarex } from '@zarex/core';
|
|
2
|
+
export { CLOUDFLARE_RANGES, Decision, IpSource, PRIVATE_NETWORKS, ProtectCall, ResponseGuardEvent, ResponseGuardOptions, Rule, SecureHeadersOptions, Zarex, ZarexOptions, createZarex, detectBot, emailValidation, phoneValidation, promptInjection, rateLimit, requestFilter, secureHeaders, sensitiveInfo, shield, tokenBucket, withSecureHeaders } from '@zarex/core';
|
|
3
|
+
|
|
4
|
+
/** Types minimaux de Fastify (pas de dépendance à `fastify` à l'exécution : Fastify 4 et 5). */
|
|
5
|
+
interface FastifyReqLike {
|
|
6
|
+
method: string;
|
|
7
|
+
url: string;
|
|
8
|
+
ip?: string | undefined;
|
|
9
|
+
hostname?: string;
|
|
10
|
+
headers: Record<string, string | string[] | undefined>;
|
|
11
|
+
raw?: {
|
|
12
|
+
socket?: {
|
|
13
|
+
remoteAddress?: string | undefined;
|
|
14
|
+
} | undefined;
|
|
15
|
+
};
|
|
16
|
+
body?: unknown;
|
|
17
|
+
zarex?: Decision;
|
|
18
|
+
}
|
|
19
|
+
interface FastifyReplyLike {
|
|
20
|
+
code(status: number): this;
|
|
21
|
+
header(name: string, value: string): this;
|
|
22
|
+
send(payload?: unknown): this;
|
|
23
|
+
getHeader?(name: string): unknown;
|
|
24
|
+
removeHeader?(name: string): unknown;
|
|
25
|
+
sent?: boolean;
|
|
26
|
+
}
|
|
27
|
+
interface FastifyZarexOptions extends Omit<ZarexOptions, "ipSource"> {
|
|
28
|
+
/**
|
|
29
|
+
* Défaut : "framework" — l'adresse est celle de `request.ip`, que Fastify calcule selon l'option
|
|
30
|
+
* `trustProxy` de VOTRE serveur (`Fastify({ trustProxy: 1 })`). Sans `trustProxy`, Fastify ignore X-Forwarded-For.
|
|
31
|
+
*/
|
|
32
|
+
ipSource?: ZarexOptions["ipSource"];
|
|
33
|
+
/** En-têtes de sécurité ajoutés à toutes les réponses (true = défauts prudents). Retire X-Powered-By. */
|
|
34
|
+
secureHeaders?: SecureHeadersOptions | boolean;
|
|
35
|
+
/** Surveillance des réponses : enregistrez aussi `zarexFastifyOnSend(options)` comme hook `onSend`. */
|
|
36
|
+
responseGuard?: ResponseGuardOptions | boolean;
|
|
37
|
+
/** Nécessite d'enregistrer le hook en `preHandler` (le corps n'est analysé qu'après `onRequest`). */
|
|
38
|
+
inspectBody?: boolean;
|
|
39
|
+
onDeny?: (req: FastifyReqLike, reply: FastifyReplyLike, decision: Decision) => void;
|
|
40
|
+
onDegraded?: (req: FastifyReqLike, decision: Decision) => void;
|
|
41
|
+
call?: (req: FastifyReqLike) => ProtectCall | undefined;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* app.addHook("onRequest", zarexFastify({ key, baseUrl, rules: [shield()] }));
|
|
45
|
+
*
|
|
46
|
+
* Avec `inspectBody: true`, enregistrez le hook en "preHandler" (pas "onRequest" ci-dessus) :
|
|
47
|
+
* Fastify n'a pas encore lu le corps en "onRequest", le moteur ne verrait jamais rien.
|
|
48
|
+
* app.addHook("preHandler", zarexFastify({ key, baseUrl, rules: [shield()], inspectBody: true }));
|
|
49
|
+
*
|
|
50
|
+
* Fonctionne aussi avec NestJS (plateforme Fastify) :
|
|
51
|
+
* (app.getHttpAdapter().getInstance() as FastifyInstance).addHook("onRequest", zarexFastify({...}));
|
|
52
|
+
*/
|
|
53
|
+
declare function zarexFastify(options: FastifyZarexOptions | Zarex, extra?: Pick<FastifyZarexOptions, "inspectBody" | "onDeny" | "onDegraded" | "call" | "secureHeaders" | "responseGuard">): (req: FastifyReqLike, reply: FastifyReplyLike) => Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Hook `onSend` : surveille les corps de réponse TEXTE (string / Buffer) et masque ou bloque les données sensibles.
|
|
56
|
+
*
|
|
57
|
+
* app.addHook("onSend", zarexFastifyOnSend({ responseGuard: true }));
|
|
58
|
+
*
|
|
59
|
+
* Les flux (streams) ne sont pas analysés.
|
|
60
|
+
*/
|
|
61
|
+
declare function zarexFastifyOnSend(options: Pick<FastifyZarexOptions, "responseGuard">): (_req: unknown, reply: FastifyReplyLike, payload: unknown) => Promise<unknown>;
|
|
62
|
+
|
|
63
|
+
export { type FastifyReplyLike, type FastifyReqLike, type FastifyZarexOptions, zarexFastify, zarexFastifyOnSend };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import {
|
|
3
|
+
applyGuard,
|
|
4
|
+
createZarex,
|
|
5
|
+
isLocalHost,
|
|
6
|
+
isScannableContentType,
|
|
7
|
+
normalizeGuardOptions,
|
|
8
|
+
prepareSecureHeaders
|
|
9
|
+
} from "@zarex/core";
|
|
10
|
+
import {
|
|
11
|
+
shield,
|
|
12
|
+
rateLimit,
|
|
13
|
+
tokenBucket,
|
|
14
|
+
detectBot,
|
|
15
|
+
requestFilter,
|
|
16
|
+
sensitiveInfo,
|
|
17
|
+
emailValidation,
|
|
18
|
+
phoneValidation,
|
|
19
|
+
promptInjection,
|
|
20
|
+
secureHeaders,
|
|
21
|
+
withSecureHeaders,
|
|
22
|
+
createZarex as createZarex2,
|
|
23
|
+
PRIVATE_NETWORKS,
|
|
24
|
+
CLOUDFLARE_RANGES
|
|
25
|
+
} from "@zarex/core";
|
|
26
|
+
function requestLooksLikeItHasAnUnparsedBody(req) {
|
|
27
|
+
if (req.body !== void 0) return false;
|
|
28
|
+
const te = req.headers["transfer-encoding"];
|
|
29
|
+
if (typeof te === "string" ? te.length > 0 : Array.isArray(te) && te.length > 0) return true;
|
|
30
|
+
const cl = req.headers["content-length"];
|
|
31
|
+
const clNum = typeof cl === "string" ? Number(cl) : Array.isArray(cl) ? Number(cl[0]) : NaN;
|
|
32
|
+
return Number.isFinite(clNum) && clNum > 0;
|
|
33
|
+
}
|
|
34
|
+
function bodyToString(body) {
|
|
35
|
+
if (body === void 0 || body === null) return void 0;
|
|
36
|
+
if (typeof body === "string") return body;
|
|
37
|
+
try {
|
|
38
|
+
const s = JSON.stringify(body);
|
|
39
|
+
return typeof s === "string" ? s : void 0;
|
|
40
|
+
} catch {
|
|
41
|
+
return void 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function defaultDeny(_req, reply, d) {
|
|
45
|
+
const limited = d.deniedBy?.ruleType === "RATE_LIMIT" || d.deniedBy?.ruleType === "TOKEN_BUCKET";
|
|
46
|
+
if (limited) {
|
|
47
|
+
if (d.retryAfterSeconds !== void 0) reply.header("retry-after", String(d.retryAfterSeconds));
|
|
48
|
+
reply.code(429).send({ error: { code: "rate_limited", message: "Too many requests", request_id: d.id } });
|
|
49
|
+
} else {
|
|
50
|
+
reply.code(403).send({ error: { code: "forbidden", message: "Request blocked", request_id: d.id } });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function zarexFastify(options, extra = {}) {
|
|
54
|
+
const isInstance = typeof options.protect === "function";
|
|
55
|
+
const opts = isInstance ? extra : options;
|
|
56
|
+
const instance = isInstance ? options : createZarex({ ...options, ipSource: options.ipSource ?? "framework" });
|
|
57
|
+
const prepared = prepareSecureHeaders(opts.secureHeaders);
|
|
58
|
+
let warnedAboutUnparsedBody = false;
|
|
59
|
+
return async function zarexHook(req, reply) {
|
|
60
|
+
let decision;
|
|
61
|
+
try {
|
|
62
|
+
if (prepared) {
|
|
63
|
+
const set = isLocalHost(req.hostname ?? (typeof req.headers["host"] === "string" ? req.headers["host"] : void 0)) ? prepared.http : prepared.https;
|
|
64
|
+
for (const [k, v] of Object.entries(set)) reply.header(k, v);
|
|
65
|
+
if (prepared.removePoweredBy) reply.removeHeader?.("x-powered-by");
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
let call;
|
|
71
|
+
try {
|
|
72
|
+
call = opts.call?.(req);
|
|
73
|
+
} catch {
|
|
74
|
+
call = void 0;
|
|
75
|
+
}
|
|
76
|
+
const host = typeof req.headers["host"] === "string" ? req.headers["host"] : req.hostname;
|
|
77
|
+
if (opts.inspectBody && !warnedAboutUnparsedBody && requestLooksLikeItHasAnUnparsedBody(req)) {
|
|
78
|
+
warnedAboutUnparsedBody = true;
|
|
79
|
+
console.warn(
|
|
80
|
+
'[zarex] fastify: `inspectBody: true` but request.body is still undefined on a request that has one. This hook is probably registered on "onRequest", where Fastify has not parsed the body yet \u2014 register it as "preHandler" instead, otherwise the body is silently never sent to the engine.'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
decision = await instance.protect(
|
|
84
|
+
{
|
|
85
|
+
method: req.method,
|
|
86
|
+
path: req.url,
|
|
87
|
+
host,
|
|
88
|
+
headers: req.headers,
|
|
89
|
+
socketIp: req.raw?.socket?.remoteAddress,
|
|
90
|
+
frameworkIp: req.ip,
|
|
91
|
+
body: opts.inspectBody ? bodyToString(req.body) : void 0
|
|
92
|
+
},
|
|
93
|
+
call
|
|
94
|
+
);
|
|
95
|
+
req.zarex = decision;
|
|
96
|
+
} catch {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (decision.denied) {
|
|
100
|
+
try {
|
|
101
|
+
(opts.onDeny ?? defaultDeny)(req, reply, decision);
|
|
102
|
+
} catch {
|
|
103
|
+
if (!reply.sent) defaultDeny(req, reply, decision);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (decision.degraded && opts.onDegraded) {
|
|
108
|
+
try {
|
|
109
|
+
opts.onDegraded(req, decision);
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function zarexFastifyOnSend(options) {
|
|
116
|
+
const guard = normalizeGuardOptions(options.responseGuard);
|
|
117
|
+
const decoder = new TextDecoder("utf-8");
|
|
118
|
+
return async function onSend(_req, reply, payload) {
|
|
119
|
+
if (!guard) return payload;
|
|
120
|
+
try {
|
|
121
|
+
const ct = String(reply.getHeader?.("content-type") ?? "");
|
|
122
|
+
const isString = typeof payload === "string";
|
|
123
|
+
const isBytes = payload instanceof Uint8Array;
|
|
124
|
+
if (!(isString || isBytes) || !isScannableContentType(ct)) return payload;
|
|
125
|
+
const r = applyGuard(isString ? payload : decoder.decode(payload), ct, guard);
|
|
126
|
+
if (r.action === "masked" || r.action === "blocked") {
|
|
127
|
+
reply.removeHeader?.("content-length");
|
|
128
|
+
reply.removeHeader?.("etag");
|
|
129
|
+
if (r.action === "blocked") {
|
|
130
|
+
reply.code(500);
|
|
131
|
+
reply.header("content-type", "application/json; charset=utf-8");
|
|
132
|
+
}
|
|
133
|
+
return r.body;
|
|
134
|
+
}
|
|
135
|
+
} catch {
|
|
136
|
+
}
|
|
137
|
+
return payload;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export {
|
|
141
|
+
CLOUDFLARE_RANGES,
|
|
142
|
+
PRIVATE_NETWORKS,
|
|
143
|
+
createZarex2 as createZarex,
|
|
144
|
+
detectBot,
|
|
145
|
+
emailValidation,
|
|
146
|
+
phoneValidation,
|
|
147
|
+
promptInjection,
|
|
148
|
+
rateLimit,
|
|
149
|
+
requestFilter,
|
|
150
|
+
secureHeaders,
|
|
151
|
+
sensitiveInfo,
|
|
152
|
+
shield,
|
|
153
|
+
tokenBucket,
|
|
154
|
+
withSecureHeaders,
|
|
155
|
+
zarexFastify,
|
|
156
|
+
zarexFastifyOnSend
|
|
157
|
+
};
|
|
158
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import {\r\n applyGuard,\r\n createZarex,\r\n isLocalHost,\r\n isScannableContentType,\r\n normalizeGuardOptions,\r\n prepareSecureHeaders,\r\n type Decision,\r\n type ProtectCall,\r\n type ResponseGuardOptions,\r\n type SecureHeadersOptions,\r\n type Zarex,\r\n type ZarexOptions,\r\n} from \"@zarex/core\";\r\n\r\n/** Types minimaux de Fastify (pas de dépendance à `fastify` à l'exécution : Fastify 4 et 5). */\r\nexport interface FastifyReqLike {\r\n method: string;\r\n url: string;\r\n ip?: string | undefined;\r\n hostname?: string;\r\n headers: Record<string, string | string[] | undefined>;\r\n raw?: { socket?: { remoteAddress?: string | undefined } | undefined };\r\n body?: unknown;\r\n zarex?: Decision;\r\n}\r\nexport interface FastifyReplyLike {\r\n code(status: number): this;\r\n header(name: string, value: string): this;\r\n send(payload?: unknown): this;\r\n getHeader?(name: string): unknown;\r\n removeHeader?(name: string): unknown;\r\n sent?: boolean;\r\n}\r\n\r\nexport interface FastifyZarexOptions extends Omit<ZarexOptions, \"ipSource\"> {\r\n /**\r\n * Défaut : \"framework\" — l'adresse est celle de `request.ip`, que Fastify calcule selon l'option\r\n * `trustProxy` de VOTRE serveur (`Fastify({ trustProxy: 1 })`). Sans `trustProxy`, Fastify ignore X-Forwarded-For.\r\n */\r\n ipSource?: ZarexOptions[\"ipSource\"];\r\n /** En-têtes de sécurité ajoutés à toutes les réponses (true = défauts prudents). Retire X-Powered-By. */\r\n secureHeaders?: SecureHeadersOptions | boolean;\r\n /** Surveillance des réponses : enregistrez aussi `zarexFastifyOnSend(options)` comme hook `onSend`. */\r\n responseGuard?: ResponseGuardOptions | boolean;\r\n /** Nécessite d'enregistrer le hook en `preHandler` (le corps n'est analysé qu'après `onRequest`). */\r\n inspectBody?: boolean;\r\n onDeny?: (req: FastifyReqLike, reply: FastifyReplyLike, decision: Decision) => void;\r\n onDegraded?: (req: FastifyReqLike, decision: Decision) => void;\r\n call?: (req: FastifyReqLike) => ProtectCall | undefined;\r\n}\r\n\r\n// Trouvé lors de l'audit du 04/11/2026 (voir AUDIT_SECURITE_COMPLET.md) : l'exemple JSDoc principal\r\n// de ce fichier montre le hook enregistré en \"onRequest\", où Fastify n'a pas encore analysé le\r\n// corps — `inspectBody: true` n'y transmet alors jamais rien au moteur, SANS AUCUN avertissement.\r\n// Heuristique : `req.body` indéfini alors que la requête annonce clairement un corps\r\n// (Content-Length > 0 ou Transfer-Encoding) signale presque certainement ce piège plutôt qu'une\r\n// requête sans corps (GET…), pour laquelle il n'y a rien à signaler.\r\nfunction requestLooksLikeItHasAnUnparsedBody(req: FastifyReqLike): boolean {\r\n if (req.body !== undefined) return false;\r\n const te = req.headers[\"transfer-encoding\"];\r\n if (typeof te === \"string\" ? te.length > 0 : Array.isArray(te) && te.length > 0) return true;\r\n const cl = req.headers[\"content-length\"];\r\n const clNum = typeof cl === \"string\" ? Number(cl) : Array.isArray(cl) ? Number(cl[0]) : NaN;\r\n return Number.isFinite(clNum) && clNum > 0;\r\n}\r\n\r\nfunction bodyToString(body: unknown): string | undefined {\r\n if (body === undefined || body === null) return undefined;\r\n if (typeof body === \"string\") return body;\r\n try {\r\n const s = JSON.stringify(body);\r\n return typeof s === \"string\" ? s : undefined;\r\n } catch {\r\n return undefined;\r\n }\r\n}\r\n\r\nfunction defaultDeny(_req: FastifyReqLike, reply: FastifyReplyLike, d: Decision): void {\r\n const limited = d.deniedBy?.ruleType === \"RATE_LIMIT\" || d.deniedBy?.ruleType === \"TOKEN_BUCKET\";\r\n if (limited) {\r\n if (d.retryAfterSeconds !== undefined) reply.header(\"retry-after\", String(d.retryAfterSeconds));\r\n reply.code(429).send({ error: { code: \"rate_limited\", message: \"Too many requests\", request_id: d.id } });\r\n } else {\r\n reply.code(403).send({ error: { code: \"forbidden\", message: \"Request blocked\", request_id: d.id } });\r\n }\r\n}\r\n\r\n/**\r\n * app.addHook(\"onRequest\", zarexFastify({ key, baseUrl, rules: [shield()] }));\r\n *\r\n * Avec `inspectBody: true`, enregistrez le hook en \"preHandler\" (pas \"onRequest\" ci-dessus) :\r\n * Fastify n'a pas encore lu le corps en \"onRequest\", le moteur ne verrait jamais rien.\r\n * app.addHook(\"preHandler\", zarexFastify({ key, baseUrl, rules: [shield()], inspectBody: true }));\r\n *\r\n * Fonctionne aussi avec NestJS (plateforme Fastify) :\r\n * (app.getHttpAdapter().getInstance() as FastifyInstance).addHook(\"onRequest\", zarexFastify({...}));\r\n */\r\nexport function zarexFastify(options: FastifyZarexOptions | Zarex, extra: Pick<FastifyZarexOptions, \"inspectBody\" | \"onDeny\" | \"onDegraded\" | \"call\" | \"secureHeaders\" | \"responseGuard\"> = {}) {\r\n const isInstance = typeof (options as Zarex).protect === \"function\";\r\n const opts = (isInstance ? extra : options) as FastifyZarexOptions;\r\n const instance: Zarex = isInstance\r\n ? (options as Zarex)\r\n : createZarex({ ...(options as FastifyZarexOptions), ipSource: (options as FastifyZarexOptions).ipSource ?? \"framework\" });\r\n\r\n const prepared = prepareSecureHeaders(opts.secureHeaders);\r\n let warnedAboutUnparsedBody = false;\r\n\r\n return async function zarexHook(req: FastifyReqLike, reply: FastifyReplyLike): Promise<void> {\r\n let decision: Decision;\r\n try {\r\n if (prepared) {\r\n const set = isLocalHost(req.hostname ?? (typeof req.headers[\"host\"] === \"string\" ? (req.headers[\"host\"] as string) : undefined)) ? prepared.http : prepared.https;\r\n for (const [k, v] of Object.entries(set)) reply.header(k, v);\r\n if (prepared.removePoweredBy) reply.removeHeader?.(\"x-powered-by\");\r\n }\r\n } catch {\r\n /* sans effet sur la requête */\r\n }\r\n try {\r\n let call: ProtectCall | undefined;\r\n try {\r\n call = opts.call?.(req);\r\n } catch {\r\n call = undefined;\r\n }\r\n const host = typeof req.headers[\"host\"] === \"string\" ? (req.headers[\"host\"] as string) : req.hostname;\r\n if (opts.inspectBody && !warnedAboutUnparsedBody && requestLooksLikeItHasAnUnparsedBody(req)) {\r\n warnedAboutUnparsedBody = true;\r\n console.warn(\r\n '[zarex] fastify: `inspectBody: true` but request.body is still undefined on a request that has one. ' +\r\n 'This hook is probably registered on \"onRequest\", where Fastify has not parsed the body yet — register it ' +\r\n 'as \"preHandler\" instead, otherwise the body is silently never sent to the engine.'\r\n );\r\n }\r\n decision = await instance.protect(\r\n {\r\n method: req.method,\r\n path: req.url,\r\n host,\r\n headers: req.headers,\r\n socketIp: req.raw?.socket?.remoteAddress,\r\n frameworkIp: req.ip,\r\n body: opts.inspectBody ? bodyToString(req.body) : undefined,\r\n },\r\n call\r\n );\r\n req.zarex = decision;\r\n } catch {\r\n return;\r\n }\r\n if (decision.denied) {\r\n try {\r\n (opts.onDeny ?? defaultDeny)(req, reply, decision);\r\n } catch {\r\n /* si le refus personnalisé échoue, on retombe sur le refus par défaut */\r\n if (!reply.sent) defaultDeny(req, reply, decision);\r\n }\r\n return; // async hook : la réponse est déjà envoyée, Fastify s'arrête là (reply.sent)\r\n }\r\n if (decision.degraded && opts.onDegraded) {\r\n try {\r\n opts.onDegraded(req, decision);\r\n } catch {\r\n /* sans effet sur la requête */\r\n }\r\n }\r\n };\r\n}\r\n\r\n/**\r\n * Hook `onSend` : surveille les corps de réponse TEXTE (string / Buffer) et masque ou bloque les données sensibles.\r\n *\r\n * app.addHook(\"onSend\", zarexFastifyOnSend({ responseGuard: true }));\r\n *\r\n * Les flux (streams) ne sont pas analysés.\r\n */\r\nexport function zarexFastifyOnSend(options: Pick<FastifyZarexOptions, \"responseGuard\">) {\r\n const guard = normalizeGuardOptions(options.responseGuard);\r\n const decoder = new TextDecoder(\"utf-8\");\r\n return async function onSend(_req: unknown, reply: FastifyReplyLike, payload: unknown): Promise<unknown> {\r\n if (!guard) return payload;\r\n try {\r\n const ct = String(reply.getHeader?.(\"content-type\") ?? \"\");\r\n const isString = typeof payload === \"string\";\r\n const isBytes = payload instanceof Uint8Array;\r\n if (!(isString || isBytes) || !isScannableContentType(ct)) return payload;\r\n const r = applyGuard(isString ? payload : decoder.decode(payload), ct, guard);\r\n if (r.action === \"masked\" || r.action === \"blocked\") {\r\n reply.removeHeader?.(\"content-length\"); // Fastify le recalcule\r\n reply.removeHeader?.(\"etag\");\r\n if (r.action === \"blocked\") {\r\n reply.code(500);\r\n reply.header(\"content-type\", \"application/json; charset=utf-8\");\r\n }\r\n return r.body;\r\n }\r\n } catch {\r\n /* jamais casser la réponse */\r\n }\r\n return payload;\r\n };\r\n}\r\n\r\nexport {\r\n shield,\r\n rateLimit,\r\n tokenBucket,\r\n detectBot,\r\n requestFilter,\r\n sensitiveInfo,\r\n emailValidation,\r\n phoneValidation,\r\n promptInjection,\r\n secureHeaders,\r\n withSecureHeaders,\r\n createZarex,\r\n PRIVATE_NETWORKS,\r\n CLOUDFLARE_RANGES,\r\n} from \"@zarex/core\";\r\nexport type { SecureHeadersOptions, ResponseGuardOptions, ResponseGuardEvent } from \"@zarex/core\";\r\nexport type { Decision, Zarex, ZarexOptions, IpSource, Rule, ProtectCall } from \"@zarex/core\";"],"mappings":"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","names":["createZarex"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zarex/fastify",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Zarex — Fastify (et NestJS avec Fastify)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.cjs",
|
|
11
|
+
"module": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"require": {
|
|
20
|
+
"types": "./dist/index.d.cts",
|
|
21
|
+
"default": "./dist/index.cjs"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@zarex/core": "^0.1.1"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"fastify": ">=4"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"fastify": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup"
|
|
42
|
+
}
|
|
6
43
|
}
|