@blamejs/core 0.5.18 → 0.6.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/CHANGELOG.md +5 -0
- package/README.md +30 -2
- package/lib/auth/jwt.js +19 -2
- package/lib/middleware/cors.js +34 -18
- package/lib/middleware/csrf-protect.js +39 -5
- package/lib/middleware/rate-limit.js +21 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,8 +6,13 @@ Pre-1.0 the surface is intentionally evolving — every release may
|
|
|
6
6
|
change something operators depend on. Read each entry before
|
|
7
7
|
upgrading across more than a few patches at a time.
|
|
8
8
|
|
|
9
|
+
## v0.6.x
|
|
10
|
+
|
|
11
|
+
- **0.6.0** (2026-05-01) — wiki restructured into 22 focused pages + missing-primitive coverage
|
|
12
|
+
|
|
9
13
|
## v0.5.x
|
|
10
14
|
|
|
15
|
+
- **0.5.18** (2026-05-01) — bypass-fix sweep: route through existing primitives
|
|
11
16
|
- **0.5.17** (2026-05-01) — primitive-drift sweep: csv unify + 3 new shared primitives
|
|
12
17
|
- **0.5.16** (2026-04-30) — b.otelExport: OTLP/HTTP exporter for b.observability
|
|
13
18
|
- **0.5.15** (2026-04-30) — b.archive: ZIP creation
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ The modern Node app is a 1,200-package supply-chain liability with no LTS calend
|
|
|
17
17
|
|
|
18
18
|
## Status
|
|
19
19
|
|
|
20
|
-
Pre-1.0.
|
|
20
|
+
Pre-1.0. Usable end-to-end — operators can build production apps on it today; the surface is still subject to change before 1.0. Recent line is **v0.6.1** ([releases](https://github.com/blamejs/blamejs/releases) · [npm](https://www.npmjs.com/package/@blamejs/core) · [container](https://github.com/blamejs/blamejs/pkgs/container/blamejs-wiki)).
|
|
21
21
|
|
|
22
22
|
```js
|
|
23
23
|
var b = require("@blamejs/core");
|
|
@@ -37,6 +37,34 @@ var b = require("@blamejs/core");
|
|
|
37
37
|
|
|
38
38
|
**Requirements:** Node.js 24+ (current active LTS).
|
|
39
39
|
|
|
40
|
+
## What ships in the box
|
|
41
|
+
|
|
42
|
+
The framework bundles the surface a typical Node app reaches for. Every primitive listed is callable today; nothing is a stub.
|
|
43
|
+
|
|
44
|
+
- **Data layer** — SQLite with sealed-by-default columns (`b.db`), migrations, seeders, atomic-file writes; S3 / R2 / B2 / GCS / Azure object store with multipart upload + SSE + bucket ops (`b.storage`, `b.objectStore`); durable queue with priority + cron + flows (`b.queue`, `b.jobs`); cluster-shared cache (`b.cache`).
|
|
45
|
+
- **Identity & access** — passwords (Argon2id), passkeys (WebAuthn), TOTP, JWT (PQ-default), OAuth, sessions, brute-force lockout (`b.auth.*`, `b.session`); RBAC (`b.permissions`); API keys with rotation (`b.apiKey`); break-glass column gates with second-factor + audit (`b.breakGlass`).
|
|
46
|
+
- **Crypto** — envelope-versioned PQC at rest (ML-KEM-1024 + P-384 hybrid, XChaCha20-Poly1305, SHAKE256), vault sealing, field-level crypto, signed webhooks (SLH-DSA-SHAKE-256f), ECIES API encryption (`b.crypto`, `b.vault`, `b.webhook`); pure-JS mTLS CA, PQC TLS gates inbound + outbound (`b.mtlsCa`, `b.pqcGate`, `b.pqcAgent`).
|
|
47
|
+
- **HTTP** — router with schema-validated routes + OpenAPI publication; full middleware stack (CSRF, CORS, rate-limit, security headers, CSP nonce, body parser, compression, SSE, request log) wired by `createApp`; HTTP/1.1 + HTTP/2 outbound client with SSRF gate, redirects, multipart, interceptors, progress, encrypted cookie jar (`b.httpClient`, `b.ssrfGuard`, `b.safeUrl`).
|
|
48
|
+
- **Defensive parsers** — `b.safeJson`, `b.safeBuffer`, `b.safeSql`, `b.safeSchema`, `b.parsers` (XML / TOML / YAML / .env), `b.config` (schema-validated env).
|
|
49
|
+
- **Communication** — WebSockets with channel/room fan-out across cluster replicas (`b.websocket`, `b.websocketChannels`); mail with multipart + attachments + DKIM + calendar invites + bounce intake (`b.mail`, `b.mailBounce`); generic notification dispatcher with operator-supplied transports (`b.notify`).
|
|
50
|
+
- **Observability** — tamper-evident audit chain with SLH-DSA-signed checkpoints, metrics, tracing (OTel pass-through when wired), PII redaction, log-stream sinks, OTLP/HTTP-JSON exporter for any OTel-compatible backend (`b.audit`, `b.metrics`, `b.tracing`, `b.redact`, `b.logStream`, `b.otelExport`).
|
|
51
|
+
- **i18n** — CLDR plural rules, Accept-Language negotiation, Intl formatters, RTL (`b.i18n`).
|
|
52
|
+
- **Format helpers** — RFC 4180 CSV with Excel formula-injection prevention (`b.csv`), RFC 9562 UUID v4 + v7 (`b.uuid`), URL-safe slugs (`b.slug`), TZ-aware datetime (`b.time`), ZIP creation (`b.archive`), HMAC-signed cursor pagination (`b.pagination`), HTML form rendering + validation + CSRF (`b.forms`).
|
|
53
|
+
- **Production** — cluster leader election with fenced leases over Postgres/SQLite (`b.cluster`); cron + interval scheduler that runs exactly-once globally (`b.scheduler`); retry with full-jitter backoff + circuit breaker (`b.retry`); graceful shutdown (`b.appShutdown`); NTP boot check (`b.ntpCheck`); end-to-end-encrypted backup bundles (`b.backup`).
|
|
54
|
+
|
|
55
|
+
## Documentation
|
|
56
|
+
|
|
57
|
+
Full primitive-by-primitive docs live at [blamejs.com](https://blamejs.com), which is itself the `examples/wiki/` app running in production. The wiki is organized by concern:
|
|
58
|
+
|
|
59
|
+
- **Data** — [Database](https://blamejs.com/database) · [Object Store](https://blamejs.com/object-store) · [Queue & Cache](https://blamejs.com/queue-cache)
|
|
60
|
+
- **Identity** — [Authentication](https://blamejs.com/auth) · [Access Control](https://blamejs.com/access-control)
|
|
61
|
+
- **Crypto** — [Crypto & Vault](https://blamejs.com/crypto-vault) · [Network Crypto](https://blamejs.com/network-crypto)
|
|
62
|
+
- **HTTP** — [Routing](https://blamejs.com/routing) · [Middleware](https://blamejs.com/middleware) · [Outbound HTTP](https://blamejs.com/outbound-http)
|
|
63
|
+
- **Validation** — [Safe Parsers](https://blamejs.com/safe-parsers)
|
|
64
|
+
- **Communication** — [WebSockets](https://blamejs.com/websockets) · [Mail](https://blamejs.com/mail) · [Notifications](https://blamejs.com/notifications)
|
|
65
|
+
- **Tools** — [Observability](https://blamejs.com/observability) · [Testing](https://blamejs.com/testing) · [i18n & Locale](https://blamejs.com/i18n-locale) · [Format Helpers](https://blamejs.com/format-helpers)
|
|
66
|
+
- **Production** — [Cluster Mode](https://blamejs.com/cluster) · [Reliability](https://blamejs.com/reliability) · [Backup & Restore](https://blamejs.com/backup-restore)
|
|
67
|
+
|
|
40
68
|
## CLI
|
|
41
69
|
|
|
42
70
|
`blamejs` ships an operator-facing CLI for the recurring ops work. Each subcommand boots a headless app instance from `--data-dir` (no HTTP listener), runs the operation, and shuts down. Same vault + DB + audit chain the running app uses.
|
|
@@ -87,7 +115,7 @@ Because when something breaks, `blame` should know exactly where it lives. We ow
|
|
|
87
115
|
|
|
88
116
|
## Contributing
|
|
89
117
|
|
|
90
|
-
Patches welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the dev setup, house rules (zero npm runtime deps, PQC-only crypto, audit-on-every-action,
|
|
118
|
+
Patches welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the dev setup, house rules (zero npm runtime deps, PQC-only crypto, audit-on-every-action, ship-complete-not-incremental), and the PR loop. New to the codebase? Start with [ARCHITECTURE.md](ARCHITECTURE.md) for the orientation map.
|
|
91
119
|
|
|
92
120
|
Community standards: [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) (Contributor Covenant 2.1). Be excellent.
|
|
93
121
|
|
package/lib/auth/jwt.js
CHANGED
|
@@ -258,11 +258,28 @@ async function verify(token, opts) {
|
|
|
258
258
|
}
|
|
259
259
|
var tol = typeof opts.clockToleranceSec === "number" ? opts.clockToleranceSec : 0;
|
|
260
260
|
var p = decoded.payload;
|
|
261
|
-
|
|
261
|
+
|
|
262
|
+
// Strict registered-claim typing per RFC 7519 §4.1. exp / nbf / iat MUST
|
|
263
|
+
// be NumericDate values (JSON numeric, seconds since epoch). A claim
|
|
264
|
+
// present-but-not-a-number is a malformed token — silently skipping the
|
|
265
|
+
// check would let a token with `exp: "0"` or `exp: "9999999999"` (string)
|
|
266
|
+
// bypass expiration enforcement entirely.
|
|
267
|
+
function _requireNumericDate(name, value) {
|
|
268
|
+
if (typeof value !== "number" || !isFinite(value)) {
|
|
269
|
+
throw new AuthError("auth-jwt/malformed",
|
|
270
|
+
"claim '" + name + "' must be a finite number (RFC 7519 NumericDate), got " +
|
|
271
|
+
(value === null ? "null" : typeof value));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (p.exp !== undefined) _requireNumericDate("exp", p.exp);
|
|
275
|
+
if (p.nbf !== undefined) _requireNumericDate("nbf", p.nbf);
|
|
276
|
+
if (p.iat !== undefined) _requireNumericDate("iat", p.iat);
|
|
277
|
+
|
|
278
|
+
if (p.exp !== undefined && p.exp + tol < nowSec) {
|
|
262
279
|
throw new AuthError("auth-jwt/expired",
|
|
263
280
|
"token expired at exp=" + p.exp + " (now=" + nowSec + ", tolerance=" + tol + "s)");
|
|
264
281
|
}
|
|
265
|
-
if (
|
|
282
|
+
if (p.nbf !== undefined && p.nbf - tol > nowSec) {
|
|
266
283
|
throw new AuthError("auth-jwt/not-yet-valid",
|
|
267
284
|
"token not yet valid: nbf=" + p.nbf + " (now=" + nowSec + ", tolerance=" + tol + "s)");
|
|
268
285
|
}
|
package/lib/middleware/cors.js
CHANGED
|
@@ -13,14 +13,19 @@
|
|
|
13
13
|
* works behind TLS-terminating proxies where the framework can't
|
|
14
14
|
* infer scheme), or
|
|
15
15
|
* 2. Origin === request's inferred scheme/host/port (from req.socket
|
|
16
|
-
* and req.headers.host — correct for direct deployments)
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
16
|
+
* and req.headers.host — correct for direct deployments).
|
|
17
|
+
*
|
|
18
|
+
* Origin: null shortcut (opt-in via strictNullOrigin: false): browsers
|
|
19
|
+
* send `Origin: null` on form-navigation POSTs from a page whose response
|
|
20
|
+
* carries `Referrer-Policy: no-referrer` (or similarly strict policy) —
|
|
21
|
+
* the Origin is opaqued to prevent a cross-site leak. The Sec-Fetch-Site
|
|
22
|
+
* Fetch-metadata signal distinguishes a same-origin nav from a genuine
|
|
23
|
+
* cross-site post in that opaque-origin world. Default is to REFUSE the
|
|
24
|
+
* shortcut (strictNullOrigin: true) — non-browser clients can forge
|
|
25
|
+
* Sec-Fetch-Site freely, and operators using `refuseUnknown: true` as a
|
|
26
|
+
* stricter "refuse unrecognized origin" policy expect the gate to hold.
|
|
27
|
+
* Operators with a no-referrer page that legitimately produces
|
|
28
|
+
* Origin: null on same-origin POSTs flip strictNullOrigin: false.
|
|
24
29
|
*
|
|
25
30
|
* Options:
|
|
26
31
|
* {
|
|
@@ -33,14 +38,16 @@
|
|
|
33
38
|
* maxAgeSeconds: 600
|
|
34
39
|
* refuseUnknown: true (refuse cross-origin requests from unlisted
|
|
35
40
|
* origins instead of just omitting CORS headers)
|
|
41
|
+
* strictNullOrigin: true (default — refuse Origin: null even with
|
|
42
|
+
* Sec-Fetch-Site: same-origin. Set false to
|
|
43
|
+
* allow the no-referrer-page edge case.)
|
|
36
44
|
* }
|
|
37
45
|
*
|
|
38
46
|
* Audit: refuseUnknown blocks emit system.cors.block with the offending Origin.
|
|
39
47
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* request.
|
|
48
|
+
* Configuration validation: opts.siteOrigin must parse as an http(s) URL,
|
|
49
|
+
* opts.origins entries must be strings or RegExp. Bad config surfaces at
|
|
50
|
+
* create() not at first cross-origin request.
|
|
44
51
|
*/
|
|
45
52
|
var lazyRequire = require("../lazy-require");
|
|
46
53
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
@@ -104,12 +111,15 @@ function _inferRequestOrigin(req, trustProxy) {
|
|
|
104
111
|
return _canonicalOrigin(proto + "://" + host);
|
|
105
112
|
}
|
|
106
113
|
|
|
107
|
-
function _isSameOrigin(req, originHeader, configuredSiteOrigins, trustProxy) {
|
|
108
|
-
// Origin: null
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
114
|
+
function _isSameOrigin(req, originHeader, configuredSiteOrigins, trustProxy, strictNullOrigin) {
|
|
115
|
+
// Origin: null arrives when a browser opaques the Origin (e.g.
|
|
116
|
+
// Referrer-Policy: no-referrer on the page). Sec-Fetch-Site can
|
|
117
|
+
// distinguish the same-origin case, but non-browser clients can forge
|
|
118
|
+
// that header freely — strictNullOrigin: true (default) refuses the
|
|
119
|
+
// shortcut so refuseUnknown holds against forged callers. Operators
|
|
120
|
+
// with a legitimate no-referrer page flip strictNullOrigin: false.
|
|
112
121
|
if (originHeader === "null") {
|
|
122
|
+
if (strictNullOrigin) return false;
|
|
113
123
|
var sfs = req && req.headers && req.headers["sec-fetch-site"];
|
|
114
124
|
if (sfs === "same-origin" || sfs === "none") return true;
|
|
115
125
|
return false;
|
|
@@ -137,6 +147,7 @@ function create(opts) {
|
|
|
137
147
|
validateOpts(opts, [
|
|
138
148
|
"origins", "siteOrigin", "methods", "headers", "exposeHeaders",
|
|
139
149
|
"credentials", "maxAgeSeconds", "refuseUnknown", "trustProxy",
|
|
150
|
+
"strictNullOrigin",
|
|
140
151
|
], "middleware.cors");
|
|
141
152
|
var trustProxy = opts.trustProxy === true || typeof opts.trustProxy === "number"
|
|
142
153
|
? opts.trustProxy : false;
|
|
@@ -185,6 +196,11 @@ function create(opts) {
|
|
|
185
196
|
var credentials = !!opts.credentials;
|
|
186
197
|
var maxAge = String(opts.maxAgeSeconds || 600);
|
|
187
198
|
var refuseUnknown = opts.refuseUnknown !== false;
|
|
199
|
+
// strictNullOrigin defaults true: refuse Origin: null even with
|
|
200
|
+
// Sec-Fetch-Site: same-origin (non-browser callers can forge that
|
|
201
|
+
// header). Operators with a no-referrer page producing legitimate
|
|
202
|
+
// Origin: null on same-origin POSTs flip to false explicitly.
|
|
203
|
+
var strictNullOrigin = opts.strictNullOrigin !== false;
|
|
188
204
|
|
|
189
205
|
return function cors(req, res, next) {
|
|
190
206
|
var origin = req.headers && req.headers.origin;
|
|
@@ -193,7 +209,7 @@ function create(opts) {
|
|
|
193
209
|
// Same-origin POST/PUT/etc. carry an Origin header per the Fetch
|
|
194
210
|
// spec but should not be subject to CORS allow-listing — they're
|
|
195
211
|
// the operator's own site talking to itself.
|
|
196
|
-
if (_isSameOrigin(req, origin, siteOrigins, trustProxy)) return next();
|
|
212
|
+
if (_isSameOrigin(req, origin, siteOrigins, trustProxy, strictNullOrigin)) return next();
|
|
197
213
|
|
|
198
214
|
var matched = _matchOrigin(origin, origins);
|
|
199
215
|
if (!matched) {
|
|
@@ -50,7 +50,9 @@
|
|
|
50
50
|
* Full options:
|
|
51
51
|
* {
|
|
52
52
|
* cookie: true | { EITHER this...
|
|
53
|
-
* name: "csrf",
|
|
53
|
+
* name: auto: "__Host-csrf" over HTTPS, "csrf" over HTTP.
|
|
54
|
+
* Operators with a custom name override here; the framework
|
|
55
|
+
* validates that __Host-* names carry path="/" and Secure.
|
|
54
56
|
* sameSite: "Lax" | "Strict" | "None",
|
|
55
57
|
* secure: auto-detected from request scheme,
|
|
56
58
|
* path: "/",
|
|
@@ -73,7 +75,15 @@ var audit = lazyRequire(function () { return require("../audit"); });
|
|
|
73
75
|
var DEFAULT_FIELD_NAME = "_csrf";
|
|
74
76
|
var DEFAULT_HEADER_NAME = "X-CSRF-Token";
|
|
75
77
|
var DEFAULT_METHODS = Object.freeze(["POST", "PUT", "DELETE", "PATCH"]);
|
|
76
|
-
|
|
78
|
+
|
|
79
|
+
// Default cookie name uses the RFC 6265bis __Host- prefix when the request
|
|
80
|
+
// is over HTTPS. The prefix forces browsers to refuse the cookie unless
|
|
81
|
+
// it carries Secure + Path=/ + no Domain attribute — closing the
|
|
82
|
+
// "malicious sibling subdomain sets a cookie on the parent domain to
|
|
83
|
+
// subvert double-submit verification" attack class. On plain HTTP (dev),
|
|
84
|
+
// browsers reject __Host- entirely, so we fall back to the bare name.
|
|
85
|
+
var DEFAULT_COOKIE_NAME_SECURE = "__Host-csrf";
|
|
86
|
+
var DEFAULT_COOKIE_NAME_INSECURE = "csrf";
|
|
77
87
|
|
|
78
88
|
function _parseCookieHeader(header) {
|
|
79
89
|
// Minimal cookie-header parser — RFC 6265 §5.2 form. Ignores attributes,
|
|
@@ -191,7 +201,9 @@ function create(opts) {
|
|
|
191
201
|
throw new Error("middleware.csrfProtect: opts.cookie must be true or an object");
|
|
192
202
|
}
|
|
193
203
|
cookieCfg = {
|
|
194
|
-
name:
|
|
204
|
+
// name: explicit operator override wins; otherwise auto-resolved
|
|
205
|
+
// per-request based on whether the cookie is being issued Secure.
|
|
206
|
+
name: raw.name || null,
|
|
195
207
|
sameSite: raw.sameSite || "Lax",
|
|
196
208
|
// secure: undefined means auto-detect from request scheme; explicit
|
|
197
209
|
// true/false overrides.
|
|
@@ -203,6 +215,27 @@ function create(opts) {
|
|
|
203
215
|
if (["Lax", "Strict", "None"].indexOf(cookieCfg.sameSite) === -1) {
|
|
204
216
|
throw new Error("middleware.csrfProtect: opts.cookie.sameSite must be Lax|Strict|None");
|
|
205
217
|
}
|
|
218
|
+
// __Host- prefix safety: if operator picks a __Host- name, the
|
|
219
|
+
// Path/Domain/Secure constraints must be compatible. Path must be "/",
|
|
220
|
+
// no Domain (we never set one), Secure resolved per-request. Catch
|
|
221
|
+
// operator-side typos (e.g. __Host-csrf with a custom path) at boot.
|
|
222
|
+
if (cookieCfg.name && /^__Host-/.test(cookieCfg.name)) {
|
|
223
|
+
if (cookieCfg.path !== "/") {
|
|
224
|
+
throw new Error("middleware.csrfProtect: __Host-* cookie name requires path='/'");
|
|
225
|
+
}
|
|
226
|
+
if (cookieCfg.secure === false) {
|
|
227
|
+
throw new Error("middleware.csrfProtect: __Host-* cookie name requires secure (cannot be explicit false)");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Resolve the cookie name for a specific request — operator override
|
|
233
|
+
// wins; otherwise __Host-csrf when the cookie will be Secure, plain
|
|
234
|
+
// csrf when over HTTP (browsers reject __Host- without Secure).
|
|
235
|
+
function _resolveCookieName(req) {
|
|
236
|
+
if (cookieCfg.name) return cookieCfg.name;
|
|
237
|
+
var willBeSecure = cookieCfg.secure == null ? _isHttps(req) : !!cookieCfg.secure;
|
|
238
|
+
return willBeSecure ? DEFAULT_COOKIE_NAME_SECURE : DEFAULT_COOKIE_NAME_INSECURE;
|
|
206
239
|
}
|
|
207
240
|
|
|
208
241
|
function _emitDenied(req, reason) {
|
|
@@ -221,14 +254,15 @@ function create(opts) {
|
|
|
221
254
|
// req.csrfToken so templates have something to render.
|
|
222
255
|
function _issueIfNeeded(req, res) {
|
|
223
256
|
if (!cookieCfg) return null;
|
|
257
|
+
var cookieName = _resolveCookieName(req);
|
|
224
258
|
var cookies = _parseCookieHeader(req.headers && req.headers.cookie);
|
|
225
|
-
var existing = cookies[
|
|
259
|
+
var existing = cookies[cookieName];
|
|
226
260
|
if (existing && /^[a-f0-9]{2,}$/.test(existing)) {
|
|
227
261
|
req.csrfToken = existing;
|
|
228
262
|
return existing;
|
|
229
263
|
}
|
|
230
264
|
var fresh = forms.generateCsrfToken();
|
|
231
|
-
var setCookie = _formatSetCookie(
|
|
265
|
+
var setCookie = _formatSetCookie(cookieName, fresh, {
|
|
232
266
|
path: cookieCfg.path,
|
|
233
267
|
sameSite: cookieCfg.sameSite,
|
|
234
268
|
secure: cookieCfg.secure == null ? _isHttps(req) : !!cookieCfg.secure,
|
|
@@ -64,11 +64,23 @@ function _clientIpFor(trustProxy) {
|
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// Reject NaN / Infinity / negative / non-positive / non-number at create
|
|
68
|
+
// time so a misconfigured rate-limit can't silently degrade to "no
|
|
69
|
+
// limit" or produce divide-by-zero verdicts at request time.
|
|
70
|
+
function _requirePositiveNumber(name, value) {
|
|
71
|
+
if (typeof value !== "number" || !isFinite(value) || value <= 0) {
|
|
72
|
+
throw new Error("middleware.rateLimit: " + name + " must be a positive finite number, got " +
|
|
73
|
+
JSON.stringify(value));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
67
77
|
// ---- Memory backend (token bucket) ----
|
|
68
78
|
|
|
69
79
|
function _memoryBackend(opts) {
|
|
70
|
-
var burst = opts.burst
|
|
71
|
-
var refillPerSecond = opts.refillPerSecond
|
|
80
|
+
var burst = opts.burst != null ? opts.burst : 60;
|
|
81
|
+
var refillPerSecond = opts.refillPerSecond != null ? opts.refillPerSecond : 10;
|
|
82
|
+
_requirePositiveNumber("burst", burst);
|
|
83
|
+
_requirePositiveNumber("refillPerSecond", refillPerSecond);
|
|
72
84
|
var buckets = new Map();
|
|
73
85
|
|
|
74
86
|
// Periodic GC of stale buckets so the map doesn't grow unbounded.
|
|
@@ -128,9 +140,13 @@ function _memoryBackend(opts) {
|
|
|
128
140
|
// ---- Cluster backend (fixed-window counter, SQL-backed) ----
|
|
129
141
|
|
|
130
142
|
function _clusterBackend(opts) {
|
|
131
|
-
var limit = opts.limit
|
|
132
|
-
var windowMs = opts.windowMs
|
|
133
|
-
var pruneIntervalMs = opts.pruneIntervalMs
|
|
143
|
+
var limit = opts.limit != null ? opts.limit : 60;
|
|
144
|
+
var windowMs = opts.windowMs != null ? opts.windowMs : C.TIME.minutes(1);
|
|
145
|
+
var pruneIntervalMs = opts.pruneIntervalMs != null
|
|
146
|
+
? opts.pruneIntervalMs : C.TIME.minutes(5);
|
|
147
|
+
_requirePositiveNumber("limit", limit);
|
|
148
|
+
_requirePositiveNumber("windowMs", windowMs);
|
|
149
|
+
_requirePositiveNumber("pruneIntervalMs", pruneIntervalMs);
|
|
134
150
|
var lastPruneAt = 0;
|
|
135
151
|
|
|
136
152
|
// Best-effort prune of expired window rows. Rate-limited at the
|