@zarex/fastify 0.0.0-stage → 0.1.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/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/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"]}
@@ -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 };
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
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.0"
31
+ },
32
+ "peerDependencies": {
33
+ "fastify": ">=4"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "fastify": {
37
+ "optional": true
38
+ }
39
+ },
40
+ "scripts": {
41
+ "build": "tsup"
42
+ }
6
43
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.