@nifrajs/middleware 0.1.0-alpha.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 +79 -0
- package/dist/_utils.d.ts +27 -0
- package/dist/_utils.d.ts.map +1 -0
- package/dist/_utils.js +141 -0
- package/dist/_utils.js.map +1 -0
- package/dist/basic-auth.d.ts +27 -0
- package/dist/basic-auth.d.ts.map +1 -0
- package/dist/basic-auth.js +64 -0
- package/dist/basic-auth.js.map +1 -0
- package/dist/body-limit.d.ts +19 -0
- package/dist/body-limit.d.ts.map +1 -0
- package/dist/body-limit.js +42 -0
- package/dist/body-limit.js.map +1 -0
- package/dist/cache-control.d.ts +23 -0
- package/dist/cache-control.d.ts.map +1 -0
- package/dist/cache-control.js +35 -0
- package/dist/cache-control.js.map +1 -0
- package/dist/cache.d.ts +60 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +224 -0
- package/dist/cache.js.map +1 -0
- package/dist/combine.d.ts +7 -0
- package/dist/combine.d.ts.map +1 -0
- package/dist/combine.js +21 -0
- package/dist/combine.js.map +1 -0
- package/dist/compression.d.ts +29 -0
- package/dist/compression.d.ts.map +1 -0
- package/dist/compression.js +105 -0
- package/dist/compression.js.map +1 -0
- package/dist/context-storage.d.ts +14 -0
- package/dist/context-storage.d.ts.map +1 -0
- package/dist/context-storage.js +30 -0
- package/dist/context-storage.js.map +1 -0
- package/dist/cors.d.ts +25 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/cors.js +75 -0
- package/dist/cors.js.map +1 -0
- package/dist/csrf.d.ts +23 -0
- package/dist/csrf.d.ts.map +1 -0
- package/dist/csrf.js +68 -0
- package/dist/csrf.js.map +1 -0
- package/dist/etag.d.ts +14 -0
- package/dist/etag.d.ts.map +1 -0
- package/dist/etag.js +95 -0
- package/dist/etag.js.map +1 -0
- package/dist/healthcheck.d.ts +25 -0
- package/dist/healthcheck.d.ts.map +1 -0
- package/dist/healthcheck.js +41 -0
- package/dist/healthcheck.js.map +1 -0
- package/dist/idempotency.d.ts +83 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +217 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/ip-restriction.d.ts +21 -0
- package/dist/ip-restriction.d.ts.map +1 -0
- package/dist/ip-restriction.js +161 -0
- package/dist/ip-restriction.js.map +1 -0
- package/dist/jwt.d.ts +82 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +327 -0
- package/dist/jwt.js.map +1 -0
- package/dist/language.d.ts +20 -0
- package/dist/language.d.ts.map +1 -0
- package/dist/language.js +94 -0
- package/dist/language.js.map +1 -0
- package/dist/logger.d.ts +19 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +28 -0
- package/dist/logger.js.map +1 -0
- package/dist/method-override.d.ts +28 -0
- package/dist/method-override.d.ts.map +1 -0
- package/dist/method-override.js +67 -0
- package/dist/method-override.js.map +1 -0
- package/dist/openapi.d.ts +83 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +100 -0
- package/dist/openapi.js.map +1 -0
- package/dist/powered-by.d.ts +15 -0
- package/dist/powered-by.d.ts.map +1 -0
- package/dist/powered-by.js +23 -0
- package/dist/powered-by.js.map +1 -0
- package/dist/pretty-json.d.ts +18 -0
- package/dist/pretty-json.d.ts.map +1 -0
- package/dist/pretty-json.js +88 -0
- package/dist/pretty-json.js.map +1 -0
- package/dist/rate-limit.d.ts +92 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +172 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/request-id.d.ts +18 -0
- package/dist/request-id.d.ts.map +1 -0
- package/dist/request-id.js +21 -0
- package/dist/request-id.js.map +1 -0
- package/dist/security-headers.d.ts +23 -0
- package/dist/security-headers.d.ts.map +1 -0
- package/dist/security-headers.js +36 -0
- package/dist/security-headers.js.map +1 -0
- package/dist/timing.d.ts +25 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +101 -0
- package/dist/timing.js.map +1 -0
- package/dist/token-auth.d.ts +65 -0
- package/dist/token-auth.d.ts.map +1 -0
- package/dist/token-auth.js +90 -0
- package/dist/token-auth.js.map +1 -0
- package/dist/trailing-slash.d.ts +23 -0
- package/dist/trailing-slash.d.ts.map +1 -0
- package/dist/trailing-slash.js +70 -0
- package/dist/trailing-slash.js.map +1 -0
- package/package.json +68 -0
- package/src/_utils.ts +163 -0
- package/src/basic-auth.ts +109 -0
- package/src/body-limit.ts +53 -0
- package/src/cache-control.ts +45 -0
- package/src/cache.ts +277 -0
- package/src/combine.ts +24 -0
- package/src/compression.ts +111 -0
- package/src/context-storage.ts +36 -0
- package/src/cors.ts +90 -0
- package/src/csrf.ts +102 -0
- package/src/etag.ts +99 -0
- package/src/healthcheck.ts +59 -0
- package/src/idempotency.ts +313 -0
- package/src/index.ts +110 -0
- package/src/ip-restriction.ts +187 -0
- package/src/jwt.ts +441 -0
- package/src/language.ts +109 -0
- package/src/logger.ts +44 -0
- package/src/method-override.ts +87 -0
- package/src/openapi.ts +174 -0
- package/src/powered-by.ts +31 -0
- package/src/pretty-json.ts +97 -0
- package/src/rate-limit.ts +248 -0
- package/src/request-id.ts +30 -0
- package/src/security-headers.ts +51 -0
- package/src/timing.ts +133 -0
- package/src/token-auth.ts +172 -0
- package/src/trailing-slash.ts +94 -0
package/dist/csrf.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { base64UrlEncode, hmacSha256, jsonError, parseCookies, SAFE_METHODS, secretBytes, timingSafeEqualString, verifyHmacSha256, } from "./_utils.js";
|
|
2
|
+
const TOKEN_PREFIX = "v1";
|
|
3
|
+
function protectedMethod(method, configured) {
|
|
4
|
+
return configured !== undefined ? configured.has(method) : !SAFE_METHODS.has(method);
|
|
5
|
+
}
|
|
6
|
+
function originAllowed(req, origins) {
|
|
7
|
+
const allowed = origins ?? new Set([new URL(req.url).origin]);
|
|
8
|
+
const origin = req.headers.get("origin");
|
|
9
|
+
if (origin !== null)
|
|
10
|
+
return allowed.has(origin);
|
|
11
|
+
const referer = req.headers.get("referer");
|
|
12
|
+
if (referer === null)
|
|
13
|
+
return false;
|
|
14
|
+
try {
|
|
15
|
+
return allowed.has(new URL(referer).origin);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function createCsrfToken(secret, nonce) {
|
|
22
|
+
const key = secretBytes(secret, "csrf");
|
|
23
|
+
const tokenNonce = nonce ?? base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
|
|
24
|
+
if (!/^[A-Za-z0-9_-]{22,}$/.test(tokenNonce)) {
|
|
25
|
+
throw new Error("csrf: nonce must be base64url-like and at least 22 characters");
|
|
26
|
+
}
|
|
27
|
+
const payload = `${TOKEN_PREFIX}.${tokenNonce}`;
|
|
28
|
+
return `${payload}.${await hmacSha256(payload, key)}`;
|
|
29
|
+
}
|
|
30
|
+
export async function verifyCsrfToken(token, secret) {
|
|
31
|
+
const key = secretBytes(secret, "csrf");
|
|
32
|
+
const parts = token.split(".");
|
|
33
|
+
if (parts.length !== 3 || parts[0] !== TOKEN_PREFIX || parts[1] === "" || parts[2] === "") {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const [prefix, nonce, signature] = parts;
|
|
37
|
+
return verifyHmacSha256(`${prefix}.${nonce}`, signature, key);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Signed double-submit CSRF protection. A protected request must carry the same signed token in a
|
|
41
|
+
* cookie and a header, and must come from an allowed Origin/Referer unless `checkOrigin:false` is set.
|
|
42
|
+
*/
|
|
43
|
+
export function csrf(options) {
|
|
44
|
+
const key = secretBytes(options.secret, "csrf");
|
|
45
|
+
const cookie = options.cookie ?? "csrf-token";
|
|
46
|
+
const header = (options.header ?? "x-csrf-token").toLowerCase();
|
|
47
|
+
const methods = options.methods !== undefined ? new Set(options.methods.map((m) => m.toUpperCase())) : undefined;
|
|
48
|
+
const origins = options.origins !== undefined ? new Set(options.origins) : undefined;
|
|
49
|
+
const checkOrigin = options.checkOrigin !== false;
|
|
50
|
+
return {
|
|
51
|
+
name: "csrf",
|
|
52
|
+
async onRequest(req) {
|
|
53
|
+
if (!protectedMethod(req.method, methods))
|
|
54
|
+
return undefined;
|
|
55
|
+
if (checkOrigin && !originAllowed(req, origins))
|
|
56
|
+
return jsonError(403, "csrf_failed");
|
|
57
|
+
const cookieToken = parseCookies(req.headers.get("cookie"))[cookie];
|
|
58
|
+
const headerToken = req.headers.get(header);
|
|
59
|
+
if (cookieToken === undefined || headerToken === null)
|
|
60
|
+
return jsonError(403, "csrf_failed");
|
|
61
|
+
if (!(await timingSafeEqualString(cookieToken, headerToken))) {
|
|
62
|
+
return jsonError(403, "csrf_failed");
|
|
63
|
+
}
|
|
64
|
+
return (await verifyCsrfToken(cookieToken, key)) ? undefined : jsonError(403, "csrf_failed");
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=csrf.js.map
|
package/dist/csrf.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csrf.js","sourceRoot":"","sources":["../src/csrf.ts"],"names":[],"mappings":"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"}
|
package/dist/etag.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ETagOptions {
|
|
2
|
+
/** Emit a weak validator (`W/"…"`). Default `true` — it's a content hash, not a byte-for-byte promise. */
|
|
3
|
+
readonly weak?: boolean;
|
|
4
|
+
/** Maximum response bytes to hash. Default `1_000_000`; larger responses pass through unchanged. */
|
|
5
|
+
readonly maxBytes?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* A {@link definePlugin} plugin that adds a content-hash `ETag` to `GET` `200` responses and returns
|
|
9
|
+
* **`304 Not Modified`** when the client's `If-None-Match` matches — saving bandwidth on unchanged
|
|
10
|
+
* responses. It reads and rebuilds small bodies only; larger responses pass through unchanged.
|
|
11
|
+
* Idempotent.
|
|
12
|
+
*/
|
|
13
|
+
export declare function etag(options?: ETagOptions): import("@nifrajs/core").NifraPlugin<import("@nifrajs/core").AnyServer, import("@nifrajs/core").AnyServer>;
|
|
14
|
+
//# sourceMappingURL=etag.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"etag.d.ts","sourceRoot":"","sources":["../src/etag.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,WAAW;IAC1B,0GAA0G;IAC1G,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAA;IACvB,oGAAoG;IACpG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,OAAO,GAAE,WAAgB,6GAsB7C"}
|
package/dist/etag.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { definePlugin } from "@nifrajs/core";
|
|
2
|
+
/** 32-bit FNV-1a over bytes → hex. A fast, dependency-free content fingerprint for ETags — not crypto. */
|
|
3
|
+
function fnv1a(bytes) {
|
|
4
|
+
let h = 0x811c9dc5;
|
|
5
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
6
|
+
h ^= bytes[i];
|
|
7
|
+
h = Math.imul(h, 0x01000193);
|
|
8
|
+
}
|
|
9
|
+
return (h >>> 0).toString(16);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A {@link definePlugin} plugin that adds a content-hash `ETag` to `GET` `200` responses and returns
|
|
13
|
+
* **`304 Not Modified`** when the client's `If-None-Match` matches — saving bandwidth on unchanged
|
|
14
|
+
* responses. It reads and rebuilds small bodies only; larger responses pass through unchanged.
|
|
15
|
+
* Idempotent.
|
|
16
|
+
*/
|
|
17
|
+
export function etag(options = {}) {
|
|
18
|
+
const prefix = (options.weak ?? true) ? "W/" : "";
|
|
19
|
+
const maxBytes = options.maxBytes ?? 1_000_000;
|
|
20
|
+
if (!Number.isInteger(maxBytes) || maxBytes < 0) {
|
|
21
|
+
throw new Error("etag: maxBytes must be a non-negative integer");
|
|
22
|
+
}
|
|
23
|
+
return definePlugin("etag", (app) => app.onResponse(async (res, req) => {
|
|
24
|
+
if (req.method !== "GET" || res.status !== 200 || res.body === null)
|
|
25
|
+
return res;
|
|
26
|
+
const declared = parseLength(res.headers.get("content-length"));
|
|
27
|
+
if (declared !== undefined && declared > maxBytes)
|
|
28
|
+
return res;
|
|
29
|
+
const body = await readBytesCapped(res, maxBytes);
|
|
30
|
+
if (body === null)
|
|
31
|
+
return res;
|
|
32
|
+
const tag = `${prefix}"${fnv1a(body)}"`;
|
|
33
|
+
const headers = new Headers(res.headers);
|
|
34
|
+
headers.set("ETag", tag);
|
|
35
|
+
if (matchesIfNoneMatch(req.headers.get("if-none-match"), tag)) {
|
|
36
|
+
return new Response(null, { status: 304, headers });
|
|
37
|
+
}
|
|
38
|
+
return new Response(body, { status: res.status, statusText: res.statusText, headers });
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
function matchesIfNoneMatch(value, tag) {
|
|
42
|
+
if (value === null)
|
|
43
|
+
return false;
|
|
44
|
+
const normalizedTag = weakComparableTag(tag);
|
|
45
|
+
for (const part of value.split(",")) {
|
|
46
|
+
const candidate = part.trim();
|
|
47
|
+
if (candidate === "*")
|
|
48
|
+
return true;
|
|
49
|
+
if (weakComparableTag(candidate) === normalizedTag)
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
function weakComparableTag(tag) {
|
|
55
|
+
return tag.startsWith("W/") ? tag.slice(2) : tag;
|
|
56
|
+
}
|
|
57
|
+
function parseLength(value) {
|
|
58
|
+
if (value === null)
|
|
59
|
+
return undefined;
|
|
60
|
+
if (!/^(?:0|[1-9]\d*)$/.test(value))
|
|
61
|
+
return undefined;
|
|
62
|
+
return Number(value);
|
|
63
|
+
}
|
|
64
|
+
async function readBytesCapped(res, maxBytes) {
|
|
65
|
+
const body = res.clone().body;
|
|
66
|
+
if (body === null)
|
|
67
|
+
return new Uint8Array();
|
|
68
|
+
const reader = body.getReader();
|
|
69
|
+
const chunks = [];
|
|
70
|
+
let total = 0;
|
|
71
|
+
try {
|
|
72
|
+
for (;;) {
|
|
73
|
+
const { done, value } = await reader.read();
|
|
74
|
+
if (done) {
|
|
75
|
+
const out = new Uint8Array(total);
|
|
76
|
+
let offset = 0;
|
|
77
|
+
for (const chunk of chunks) {
|
|
78
|
+
out.set(chunk, offset);
|
|
79
|
+
offset += chunk.byteLength;
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
total += value.byteLength;
|
|
84
|
+
if (total > maxBytes) {
|
|
85
|
+
await reader.cancel();
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
chunks.push(value);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=etag.js.map
|
package/dist/etag.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"etag.js","sourceRoot":"","sources":["../src/etag.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
2
|
+
export interface HealthcheckOptions {
|
|
3
|
+
/** Liveness path — always `200` while the process is up. Default `"/health"`. */
|
|
4
|
+
readonly path?: string;
|
|
5
|
+
/** Readiness path — runs `checks`; `200` if all pass, `503` otherwise. Default `"/ready"`. */
|
|
6
|
+
readyPath?: string;
|
|
7
|
+
/** Readiness checks (DB ping, cache reachable, …). Each returns a boolean or throws; a throw counts
|
|
8
|
+
* as failing. `/ready` reports each by name and is `200` only when all pass. */
|
|
9
|
+
readonly checks?: Readonly<Record<string, () => MaybePromise<boolean>>>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Register **liveness** (`/health`) and **readiness** (`/ready`) endpoints. Liveness is a flat `200`
|
|
13
|
+
* (the process is serving). Readiness runs each `check` and returns `200 { status: "ok", checks }`
|
|
14
|
+
* when all pass, or `503 { status: "error", checks }` when any fail (a thrown check counts as failed).
|
|
15
|
+
* Both are `Cache-Control: no-store`.
|
|
16
|
+
*
|
|
17
|
+
* Apply it **before** auth guards so the endpoints stay public (`beforeHandle` is order-scoped):
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* app.use(healthcheck({ checks: { db: () => db.ping() } })).use(bearer({ verify }))
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function healthcheck(options?: HealthcheckOptions): import("@nifrajs/core").NifraPlugin<import("@nifrajs/core").AnyServer, import("@nifrajs/core").AnyServer>;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=healthcheck.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"healthcheck.d.ts","sourceRoot":"","sources":["../src/healthcheck.ts"],"names":[],"mappings":"AAEA,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;AAErC,MAAM,WAAW,kBAAkB;IACjC,iFAAiF;IACjF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,8FAA8F;IAC9F,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;oFACgF;IAChF,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;CACxE;AAID;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,OAAO,GAAE,kBAAuB,6GA8B3D"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { definePlugin } from "@nifrajs/core";
|
|
2
|
+
const noStore = { "cache-control": "no-store" };
|
|
3
|
+
/**
|
|
4
|
+
* Register **liveness** (`/health`) and **readiness** (`/ready`) endpoints. Liveness is a flat `200`
|
|
5
|
+
* (the process is serving). Readiness runs each `check` and returns `200 { status: "ok", checks }`
|
|
6
|
+
* when all pass, or `503 { status: "error", checks }` when any fail (a thrown check counts as failed).
|
|
7
|
+
* Both are `Cache-Control: no-store`.
|
|
8
|
+
*
|
|
9
|
+
* Apply it **before** auth guards so the endpoints stay public (`beforeHandle` is order-scoped):
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* app.use(healthcheck({ checks: { db: () => db.ping() } })).use(bearer({ verify }))
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function healthcheck(options = {}) {
|
|
16
|
+
const path = options.path ?? "/health";
|
|
17
|
+
const readyPath = options.readyPath ?? "/ready";
|
|
18
|
+
const checks = options.checks ?? {};
|
|
19
|
+
const checkNames = Object.keys(checks);
|
|
20
|
+
return definePlugin("healthcheck", (app) => {
|
|
21
|
+
app.register("GET", path, undefined, () => Response.json({ status: "ok" }, { headers: noStore }));
|
|
22
|
+
app.register("GET", readyPath, undefined, async () => {
|
|
23
|
+
const results = {};
|
|
24
|
+
let ready = true;
|
|
25
|
+
// Run checks concurrently; a throw (or false) marks that check — and the whole probe — not ready.
|
|
26
|
+
await Promise.all(checkNames.map(async (name) => {
|
|
27
|
+
try {
|
|
28
|
+
results[name] = (await checks[name]?.()) === true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
results[name] = false;
|
|
32
|
+
}
|
|
33
|
+
if (!results[name])
|
|
34
|
+
ready = false;
|
|
35
|
+
}));
|
|
36
|
+
return Response.json({ status: ready ? "ok" : "error", checks: results }, { status: ready ? 200 : 503, headers: noStore });
|
|
37
|
+
});
|
|
38
|
+
return app;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=healthcheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"healthcheck.js","sourceRoot":"","sources":["../src/healthcheck.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAc5C,MAAM,OAAO,GAAG,EAAE,eAAe,EAAE,UAAU,EAAW,CAAA;AAExD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,WAAW,CAAC,UAA8B,EAAE;IAC1D,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,SAAS,CAAA;IACtC,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,QAAQ,CAAA;IAC/C,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,EAAE,CAAA;IACnC,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACtC,OAAO,YAAY,CAAC,aAAa,EAAE,CAAC,GAAG,EAAE,EAAE;QACzC,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CACxC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CACtD,CAAA;QACD,GAAG,CAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,IAAI,EAAE;YACnD,MAAM,OAAO,GAA4B,EAAE,CAAA;YAC3C,IAAI,KAAK,GAAG,IAAI,CAAA;YAChB,kGAAkG;YAClG,MAAM,OAAO,CAAC,GAAG,CACf,UAAU,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;gBAC5B,IAAI,CAAC;oBACH,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,KAAK,IAAI,CAAA;gBACnD,CAAC;gBAAC,MAAM,CAAC;oBACP,OAAO,CAAC,IAAI,CAAC,GAAG,KAAK,CAAA;gBACvB,CAAC;gBACD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;oBAAE,KAAK,GAAG,KAAK,CAAA;YACnC,CAAC,CAAC,CACH,CAAA;YACD,OAAO,QAAQ,CAAC,IAAI,CAClB,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,EACnD,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,CAChD,CAAA;QACH,CAAC,CAAC,CAAA;QACF,OAAO,GAAG,CAAA;IACZ,CAAC,CAAC,CAAA;AACJ,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Middleware } from "@nifrajs/core";
|
|
2
|
+
/**
|
|
3
|
+
* Idempotency keys for unsafe requests — a client retrying a `POST` (dropped connection, impatient
|
|
4
|
+
* tap) with the same `Idempotency-Key` gets the **first** response replayed instead of the side effect
|
|
5
|
+
* running twice (double-charge, double-publish). Runs in `onRequest` (before the handler), so a replay
|
|
6
|
+
* or an in-flight collision short-circuits before any mutation.
|
|
7
|
+
*
|
|
8
|
+
* Pair it with a DB uniqueness constraint — this stops the *retry*, the constraint is the source of
|
|
9
|
+
* truth for genuinely-concurrent distinct requests. Production MUST use a shared {@link IdempotencyStore}
|
|
10
|
+
* (Redis, etc.) so the guarantee holds across instances; {@link MemoryIdempotencyStore} is dev-only.
|
|
11
|
+
*/
|
|
12
|
+
/** A captured response, replayed verbatim on a retry. Body is base64 (binary-safe + JSON-serializable). */
|
|
13
|
+
export interface IdempotencyRecord {
|
|
14
|
+
readonly status: number;
|
|
15
|
+
/** Response headers, **excluding `Set-Cookie`** (cookies are session-specific — see {@link idempotency}). */
|
|
16
|
+
readonly headers: ReadonlyArray<readonly [string, string]>;
|
|
17
|
+
/** Response body, base64-encoded. */
|
|
18
|
+
readonly body: string;
|
|
19
|
+
}
|
|
20
|
+
export type IdempotencyClaim = {
|
|
21
|
+
readonly state: "new";
|
|
22
|
+
} | {
|
|
23
|
+
readonly state: "in_flight";
|
|
24
|
+
} | {
|
|
25
|
+
readonly state: "replay";
|
|
26
|
+
readonly record: IdempotencyRecord;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Store backing the idempotency guarantee. Production deploys MUST use a shared store so the key holds
|
|
30
|
+
* across instances; `begin` MUST be **atomic** (e.g. Redis `SET key NX PX lockTtlMs`) or two concurrent
|
|
31
|
+
* retries can both see `"new"`. {@link MemoryIdempotencyStore} is for dev / single-instance only.
|
|
32
|
+
*/
|
|
33
|
+
export interface IdempotencyStore {
|
|
34
|
+
/**
|
|
35
|
+
* Atomically claim `key`: `"replay"` if a completed response is stored, `"in_flight"` if another
|
|
36
|
+
* request holds the lock, else `"new"` (the caller now owns the lock and must `complete`/`release`).
|
|
37
|
+
* The in-flight lock expires after `lockTtlMs` so a crashed handler can't wedge the key forever.
|
|
38
|
+
*/
|
|
39
|
+
begin(key: string, lockTtlMs: number): Promise<IdempotencyClaim>;
|
|
40
|
+
/** Store the completed response and release the lock (kept for `ttlMs`). */
|
|
41
|
+
complete(key: string, record: IdempotencyRecord, ttlMs: number): Promise<void>;
|
|
42
|
+
/** Release the lock without storing (handler errored / response not cacheable). */
|
|
43
|
+
release(key: string): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
export interface MemoryIdempotencyStoreOptions {
|
|
46
|
+
/** Allow the in-memory store in production. Off by default — a per-instance store can't dedupe across instances. */
|
|
47
|
+
readonly allowInProduction?: boolean;
|
|
48
|
+
}
|
|
49
|
+
/** In-process store. Refuses to run in production unless explicitly allowed (per-instance ⇒ no cross-instance dedupe). */
|
|
50
|
+
export declare class MemoryIdempotencyStore implements IdempotencyStore {
|
|
51
|
+
private readonly entries;
|
|
52
|
+
constructor(options?: MemoryIdempotencyStoreOptions);
|
|
53
|
+
begin(key: string, lockTtlMs: number): Promise<IdempotencyClaim>;
|
|
54
|
+
complete(key: string, record: IdempotencyRecord, ttlMs: number): Promise<void>;
|
|
55
|
+
release(key: string): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
export interface IdempotencyOptions {
|
|
58
|
+
/** Where claims + cached responses live. `MemoryIdempotencyStore` for dev; a shared store in production. */
|
|
59
|
+
readonly store: IdempotencyStore;
|
|
60
|
+
/** Header carrying the key. Default `"idempotency-key"`. */
|
|
61
|
+
readonly header?: string;
|
|
62
|
+
/** Methods the guard applies to. Default `["POST", "PUT", "PATCH", "DELETE"]` (unsafe methods). */
|
|
63
|
+
readonly methods?: readonly string[];
|
|
64
|
+
/** How long a completed response is replayable, in ms. Default 24h. */
|
|
65
|
+
readonly ttlMs?: number;
|
|
66
|
+
/** How long the in-flight lock survives a crashed handler, in ms. Default 60s. */
|
|
67
|
+
readonly lockTtlMs?: number;
|
|
68
|
+
/** Max response bytes to cache. A larger response is returned but **not** stored. Default 1 MiB. */
|
|
69
|
+
readonly maxBytes?: number;
|
|
70
|
+
/** Whether a response should be cached for replay. Default: status `< 500` (don't replay transient 5xx). */
|
|
71
|
+
readonly shouldCache?: (response: Response) => boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Idempotency-key middleware. Apply with `app.use(idempotency({ store }))`.
|
|
75
|
+
*
|
|
76
|
+
* **`Set-Cookie` is intentionally not cached or replayed** — a cookie set on the first request is
|
|
77
|
+
* session-specific, so replaying it to a different caller (key collision or abuse) would leak/fixate a
|
|
78
|
+
* session. Cache the body + status + the rest of the headers; let auth cookies re-issue per request.
|
|
79
|
+
*
|
|
80
|
+
* Caching buffers the response body, so apply this to JSON/API routes, not streaming SSR responses.
|
|
81
|
+
*/
|
|
82
|
+
export declare function idempotency(options: IdempotencyOptions): Middleware;
|
|
83
|
+
//# sourceMappingURL=idempotency.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE/C;;;;;;;;;GASG;AAEH,2GAA2G;AAC3G,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,6GAA6G;IAC7G,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC1D,qCAAqC;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAA;AAEpE;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAA;IAChE,4EAA4E;IAC5E,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9E,mFAAmF;IACnF,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACpC;AAED,MAAM,WAAW,6BAA6B;IAC5C,oHAAoH;IACpH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CACrC;AAMD,0HAA0H;AAC1H,qBAAa,sBAAuB,YAAW,gBAAgB;IAC7D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;gBAEvC,OAAO,GAAE,6BAAkC;IASvD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAehE,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK9E,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAKpC;AAED,MAAM,WAAW,kBAAkB;IACjC,4GAA4G;IAC5G,QAAQ,CAAC,KAAK,EAAE,gBAAgB,CAAA;IAChC,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,mGAAmG;IACnG,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACpC,uEAAuE;IACvE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,kFAAkF;IAClF,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,oGAAoG;IACpG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,4GAA4G;IAC5G,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,OAAO,CAAA;CACvD;AAuID;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,UAAU,CA2DnE"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/** In-process store. Refuses to run in production unless explicitly allowed (per-instance ⇒ no cross-instance dedupe). */
|
|
2
|
+
export class MemoryIdempotencyStore {
|
|
3
|
+
entries = new Map();
|
|
4
|
+
constructor(options = {}) {
|
|
5
|
+
if (options.allowInProduction !== true && process.env.NODE_ENV === "production") {
|
|
6
|
+
throw new Error("MemoryIdempotencyStore is per-instance and can't dedupe across instances in production. " +
|
|
7
|
+
"Use a shared store (e.g. Redis), or pass { allowInProduction: true } for a single-instance deploy.");
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
begin(key, lockTtlMs) {
|
|
11
|
+
const now = Date.now();
|
|
12
|
+
const entry = this.entries.get(key);
|
|
13
|
+
if (entry !== undefined && entry.expiresAt > now) {
|
|
14
|
+
return Promise.resolve(entry.kind === "record"
|
|
15
|
+
? { state: "replay", record: entry.record }
|
|
16
|
+
: { state: "in_flight" });
|
|
17
|
+
}
|
|
18
|
+
// Free (or expired): take the lock. Synchronous Map write ⇒ atomic on a single instance.
|
|
19
|
+
this.entries.set(key, { kind: "lock", expiresAt: now + lockTtlMs });
|
|
20
|
+
return Promise.resolve({ state: "new" });
|
|
21
|
+
}
|
|
22
|
+
complete(key, record, ttlMs) {
|
|
23
|
+
this.entries.set(key, { kind: "record", record, expiresAt: Date.now() + ttlMs });
|
|
24
|
+
return Promise.resolve();
|
|
25
|
+
}
|
|
26
|
+
release(key) {
|
|
27
|
+
const entry = this.entries.get(key);
|
|
28
|
+
if (entry !== undefined && entry.kind === "lock")
|
|
29
|
+
this.entries.delete(key);
|
|
30
|
+
return Promise.resolve();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const DEFAULT_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
|
|
34
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
35
|
+
const toBase64 = (bytes) => {
|
|
36
|
+
let bin = "";
|
|
37
|
+
for (const b of bytes)
|
|
38
|
+
bin += String.fromCharCode(b);
|
|
39
|
+
return btoa(bin);
|
|
40
|
+
};
|
|
41
|
+
const fromBase64 = (value) => {
|
|
42
|
+
const bin = atob(value);
|
|
43
|
+
const out = new Uint8Array(bin.length);
|
|
44
|
+
for (let i = 0; i < bin.length; i++)
|
|
45
|
+
out[i] = bin.charCodeAt(i);
|
|
46
|
+
return out;
|
|
47
|
+
};
|
|
48
|
+
// 204/205/304 (and an empty body) must be constructed with a `null` body or the Response ctor throws.
|
|
49
|
+
const bodyFor = (bytes) => (bytes.byteLength === 0 ? null : bytes);
|
|
50
|
+
const concat = (chunks, total) => {
|
|
51
|
+
const out = new Uint8Array(total);
|
|
52
|
+
let offset = 0;
|
|
53
|
+
for (const chunk of chunks) {
|
|
54
|
+
out.set(chunk, offset);
|
|
55
|
+
offset += chunk.byteLength;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
};
|
|
59
|
+
function parseLength(value) {
|
|
60
|
+
if (value === null)
|
|
61
|
+
return undefined;
|
|
62
|
+
if (!/^(?:0|[1-9]\d*)$/.test(value))
|
|
63
|
+
return undefined;
|
|
64
|
+
return Number(value);
|
|
65
|
+
}
|
|
66
|
+
function responseWithBody(res, body) {
|
|
67
|
+
return new Response(body, {
|
|
68
|
+
status: res.status,
|
|
69
|
+
statusText: res.statusText,
|
|
70
|
+
headers: res.headers,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function replayBufferedBody(reader, buffered) {
|
|
74
|
+
let index = 0;
|
|
75
|
+
let released = false;
|
|
76
|
+
const release = () => {
|
|
77
|
+
if (!released) {
|
|
78
|
+
released = true;
|
|
79
|
+
reader.releaseLock();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return new ReadableStream({
|
|
83
|
+
async pull(controller) {
|
|
84
|
+
if (index < buffered.length) {
|
|
85
|
+
controller.enqueue(buffered[index]);
|
|
86
|
+
index += 1;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const { done, value } = await reader.read();
|
|
91
|
+
if (done) {
|
|
92
|
+
release();
|
|
93
|
+
controller.close();
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
controller.enqueue(value);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
release();
|
|
101
|
+
controller.error(err);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
cancel(reason) {
|
|
105
|
+
return reader.cancel(reason).finally(release);
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async function captureBody(res, maxBytes) {
|
|
110
|
+
const declared = parseLength(res.headers.get("content-length"));
|
|
111
|
+
if (declared !== undefined && declared > maxBytes)
|
|
112
|
+
return { response: res };
|
|
113
|
+
const body = res.body;
|
|
114
|
+
if (body === null)
|
|
115
|
+
return { bytes: new Uint8Array(), response: responseWithBody(res, null) };
|
|
116
|
+
const reader = body.getReader();
|
|
117
|
+
const chunks = [];
|
|
118
|
+
let total = 0;
|
|
119
|
+
try {
|
|
120
|
+
for (;;) {
|
|
121
|
+
const { done, value } = await reader.read();
|
|
122
|
+
if (done) {
|
|
123
|
+
reader.releaseLock();
|
|
124
|
+
const bytes = concat(chunks, total);
|
|
125
|
+
return { bytes, response: responseWithBody(res, bodyFor(bytes)) };
|
|
126
|
+
}
|
|
127
|
+
chunks.push(value);
|
|
128
|
+
total += value.byteLength;
|
|
129
|
+
if (total > maxBytes) {
|
|
130
|
+
return { response: responseWithBody(res, replayBufferedBody(reader, chunks)) };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
reader.releaseLock();
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function replay(record) {
|
|
140
|
+
const headers = new Headers(record.headers);
|
|
141
|
+
headers.set("idempotent-replayed", "true");
|
|
142
|
+
return new Response(bodyFor(fromBase64(record.body)), { status: record.status, headers });
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Idempotency-key middleware. Apply with `app.use(idempotency({ store }))`.
|
|
146
|
+
*
|
|
147
|
+
* **`Set-Cookie` is intentionally not cached or replayed** — a cookie set on the first request is
|
|
148
|
+
* session-specific, so replaying it to a different caller (key collision or abuse) would leak/fixate a
|
|
149
|
+
* session. Cache the body + status + the rest of the headers; let auth cookies re-issue per request.
|
|
150
|
+
*
|
|
151
|
+
* Caching buffers the response body, so apply this to JSON/API routes, not streaming SSR responses.
|
|
152
|
+
*/
|
|
153
|
+
export function idempotency(options) {
|
|
154
|
+
const { store } = options;
|
|
155
|
+
const header = (options.header ?? "idempotency-key").toLowerCase();
|
|
156
|
+
const methods = new Set((options.methods ?? DEFAULT_METHODS).map((m) => m.toUpperCase()));
|
|
157
|
+
const ttlMs = options.ttlMs ?? DAY_MS;
|
|
158
|
+
const lockTtlMs = options.lockTtlMs ?? 60_000;
|
|
159
|
+
const maxBytes = options.maxBytes ?? 1024 * 1024;
|
|
160
|
+
if (!Number.isInteger(maxBytes) || maxBytes < 0) {
|
|
161
|
+
throw new Error("idempotency: maxBytes must be a non-negative integer");
|
|
162
|
+
}
|
|
163
|
+
const shouldCache = options.shouldCache ?? ((res) => res.status < 500);
|
|
164
|
+
const claimed = new WeakMap();
|
|
165
|
+
return {
|
|
166
|
+
name: "idempotency",
|
|
167
|
+
async onRequest(req) {
|
|
168
|
+
if (!methods.has(req.method))
|
|
169
|
+
return undefined;
|
|
170
|
+
const key = req.headers.get(header);
|
|
171
|
+
if (key === null || key === "")
|
|
172
|
+
return undefined; // opt-in per request — no key ⇒ no dedupe
|
|
173
|
+
const claim = await store.begin(key, lockTtlMs);
|
|
174
|
+
if (claim.state === "replay")
|
|
175
|
+
return replay(claim.record);
|
|
176
|
+
if (claim.state === "in_flight") {
|
|
177
|
+
return new Response(JSON.stringify({ ok: false, error: "idempotency_in_progress" }), {
|
|
178
|
+
status: 409,
|
|
179
|
+
headers: {
|
|
180
|
+
"content-type": "application/json",
|
|
181
|
+
"retry-after": String(Math.ceil(lockTtlMs / 1000)),
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
claimed.set(req, key);
|
|
186
|
+
return undefined;
|
|
187
|
+
},
|
|
188
|
+
async onResponse(res, req) {
|
|
189
|
+
const key = claimed.get(req);
|
|
190
|
+
if (key === undefined)
|
|
191
|
+
return res; // not a claimed request (safe method / no key / a replay)
|
|
192
|
+
claimed.delete(req);
|
|
193
|
+
if (!shouldCache(res)) {
|
|
194
|
+
await store.release(key);
|
|
195
|
+
return res;
|
|
196
|
+
}
|
|
197
|
+
// Buffer the body (consumes `res`), so a fresh Response is returned in its place.
|
|
198
|
+
let captured;
|
|
199
|
+
try {
|
|
200
|
+
captured = await captureBody(res, maxBytes);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
await store.release(key);
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
if (!("bytes" in captured)) {
|
|
207
|
+
await store.release(key); // too large to store — return it, but don't cache
|
|
208
|
+
return captured.response;
|
|
209
|
+
}
|
|
210
|
+
const bytes = captured.bytes;
|
|
211
|
+
const headers = [...captured.response.headers].filter(([name]) => name !== "set-cookie");
|
|
212
|
+
await store.complete(key, { status: res.status, headers, body: toBase64(bytes) }, ttlMs);
|
|
213
|
+
return captured.response;
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=idempotency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/middleware` — composable, dependency-free middleware + plugins applied via `app.use()`.
|
|
3
|
+
* The hardening set (`cors`, `rateLimit`, `securityHeaders`) returns a `@nifrajs/core` `Middleware`
|
|
4
|
+
* (a hook bundle, context-agnostic). The seeded plugins (`requestId`, `logger`, `etag`, `bearer`,
|
|
5
|
+
* `apiKey`) use the `definePlugin` convention — named (idempotent dedupe), and `requestId` threads
|
|
6
|
+
* `c.requestId` into the handler context.
|
|
7
|
+
*/
|
|
8
|
+
export { type BasicAuthPlugin, type BasicAuthStaticOptions, type BasicAuthVerifyOptions, basicAuth, } from "./basic-auth.js";
|
|
9
|
+
export { type BodyLimitOptions, bodyLimit } from "./body-limit.js";
|
|
10
|
+
export { type CachedResponse, type CacheOptions, cache, MemoryResponseCache, type MemoryResponseCacheOptions, type ResponseCacheStore, responseCache, } from "./cache.js";
|
|
11
|
+
export { type CacheControlOptions, cacheControl } from "./cache-control.js";
|
|
12
|
+
export { type Composable, combine, namedCombine } from "./combine.js";
|
|
13
|
+
export { type CompressionOptions, compression } from "./compression.js";
|
|
14
|
+
export { type CorsOptions, cors } from "./cors.js";
|
|
15
|
+
export { type CsrfOptions, createCsrfToken, csrf, verifyCsrfToken, } from "./csrf.js";
|
|
16
|
+
export { type ETagOptions, etag } from "./etag.js";
|
|
17
|
+
export { type HealthcheckOptions, healthcheck } from "./healthcheck.js";
|
|
18
|
+
export { type IdempotencyClaim, type IdempotencyOptions, type IdempotencyRecord, type IdempotencyStore, idempotency, MemoryIdempotencyStore, type MemoryIdempotencyStoreOptions, } from "./idempotency.js";
|
|
19
|
+
export { type IpMatcher, type IpRestrictionOptions, ipRestriction } from "./ip-restriction.js";
|
|
20
|
+
export { type JwkKey, type JwksOptions, type JwtAlgorithm, type JwtClaims, type JwtHeader, type JwtKeyResolver, type JwtOptions, type JwtPlugin, type JwtVerificationKey, jwk, jwks, jwt, tryVerifyJwt, type VerifiedJwt, type VerifyJwtOptions, type VerifyJwtResult, verifyJwt, } from "./jwt.js";
|
|
21
|
+
export { type LanguageMatch, type LanguageOptions, language, pickLanguage, } from "./language.js";
|
|
22
|
+
export { type LoggerOptions, logger, type RequestLogFields } from "./logger.js";
|
|
23
|
+
export { type MethodOverrideOptions, methodOverride } from "./method-override.js";
|
|
24
|
+
export { buildOpenApiDocument, type OpenApiInfo, type OpenApiOptions, type OpenApiServer, type OpenApiTag, type OpenApiUiOptions, openapi, type RouteLike, type SecurityRequirement, } from "./openapi.js";
|
|
25
|
+
export { type PoweredByOptions, poweredBy } from "./powered-by.js";
|
|
26
|
+
export { type PrettyJsonOptions, prettyJson } from "./pretty-json.js";
|
|
27
|
+
export { MemoryStore, type MemoryStoreOptions, type RateLimitOptions, type RateLimitResult, type RateLimitStore, rateLimit, } from "./rate-limit.js";
|
|
28
|
+
export { type RequestIdOptions, requestId } from "./request-id.js";
|
|
29
|
+
export { type SecurityHeadersOptions, securityHeaders } from "./security-headers.js";
|
|
30
|
+
export { type TimingControls, type TimingMetric, type TimingOptions, timing } from "./timing.js";
|
|
31
|
+
export { type ApiKeyStaticOptions, type ApiKeyVerifyOptions, type AuthPlugin, apiKey, type BearerOptions, bearer, } from "./token-auth.js";
|
|
32
|
+
export { appendTrailingSlash, type TrailingSlashOptions, trimTrailingSlash, } from "./trailing-slash.js";
|
|
33
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|