@blamejs/core 0.8.42 → 0.8.49
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 +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
|
@@ -24,15 +24,9 @@
|
|
|
24
24
|
* });
|
|
25
25
|
* router.use(quota);
|
|
26
26
|
*
|
|
27
|
-
* The middleware
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* Single-node memory backend uses a Map<ip, { bins: Uint32Array(24),
|
|
32
|
-
* windowStartHour: number }>. Each bin holds bytes for one rolling hour;
|
|
33
|
-
* sweeping happens on every account() call so cold storage doesn't grow
|
|
34
|
-
* unbounded. Cluster-aware operators wire opts.cache (b.cache instance)
|
|
35
|
-
* and the same pattern runs in the shared backend.
|
|
27
|
+
* The middleware composes b.network.byteQuota — handlers that already
|
|
28
|
+
* know the byte cost of an op call b.network.byteQuota.check / record
|
|
29
|
+
* directly without going through the middleware lifecycle.
|
|
36
30
|
*
|
|
37
31
|
* Failure modes:
|
|
38
32
|
* - cache backend unreachable → fail-open (count drops, request
|
|
@@ -45,6 +39,7 @@
|
|
|
45
39
|
var C = require("../constants");
|
|
46
40
|
var defineClass = require("../framework-error").defineClass;
|
|
47
41
|
var lazyRequire = require("../lazy-require");
|
|
42
|
+
var networkByteQuota = require("../network-byte-quota");
|
|
48
43
|
var validateOpts = require("../validate-opts");
|
|
49
44
|
|
|
50
45
|
var audit = lazyRequire(function () { return require("../audit"); });
|
|
@@ -53,9 +48,6 @@ var requestHelpers = lazyRequire(function () { return require("../request-helper
|
|
|
53
48
|
|
|
54
49
|
var DailyByteQuotaError = defineClass("DailyByteQuotaError", { alwaysPermanent: true });
|
|
55
50
|
|
|
56
|
-
var BINS_PER_DAY = 24; // allow:raw-byte-literal — 24 hours in a day
|
|
57
|
-
var BIN_MS = C.TIME.hours(1);
|
|
58
|
-
|
|
59
51
|
// Default getKey — req.ip OR the trusted-proxy-resolved peer address
|
|
60
52
|
// when the operator wired b.middleware.requestId or similar earlier in
|
|
61
53
|
// the chain. We don't try to be clever here: req.ip is the canonical
|
|
@@ -64,73 +56,40 @@ function _defaultGetKey(req) {
|
|
|
64
56
|
return requestHelpers().clientIp(req, { trustProxy: false });
|
|
65
57
|
}
|
|
66
58
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
async total(key, nowMs) {
|
|
102
|
-
return _slideAndSum(_get(key), _hourBin(nowMs)).total;
|
|
103
|
-
},
|
|
104
|
-
async account(key, bytes, nowMs) {
|
|
105
|
-
var slid = _slideAndSum(_get(key), _hourBin(nowMs));
|
|
106
|
-
slid.entry.bins[BINS_PER_DAY - 1] += bytes;
|
|
107
|
-
},
|
|
108
|
-
_resetForTest: function () { store.clear(); },
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function _cacheBackend(cache) {
|
|
113
|
-
function _key(k) { return "dailyByteQuota:" + k; }
|
|
114
|
-
async function _read(key) {
|
|
115
|
-
var raw = await cache.get(_key(key));
|
|
116
|
-
return raw && typeof raw === "object" && Array.isArray(raw.bins) ? raw : _newEntry();
|
|
117
|
-
}
|
|
118
|
-
return {
|
|
119
|
-
async total(key, nowMs) {
|
|
120
|
-
var entry = await _read(key);
|
|
121
|
-
var slid = _slideAndSum(entry, _hourBin(nowMs));
|
|
122
|
-
if (slid.moved) await cache.set(_key(key), slid.entry, { ttlMs: BIN_MS * BINS_PER_DAY });
|
|
123
|
-
return slid.total;
|
|
124
|
-
},
|
|
125
|
-
async account(key, bytes, nowMs) {
|
|
126
|
-
var entry = await _read(key);
|
|
127
|
-
var slid = _slideAndSum(entry, _hourBin(nowMs));
|
|
128
|
-
slid.entry.bins[BINS_PER_DAY - 1] += bytes;
|
|
129
|
-
await cache.set(_key(key), slid.entry, { ttlMs: BIN_MS * BINS_PER_DAY });
|
|
130
|
-
},
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
59
|
+
/**
|
|
60
|
+
* @primitive b.middleware.dailyByteQuota
|
|
61
|
+
* @signature b.middleware.dailyByteQuota(opts)
|
|
62
|
+
* @since 0.1.0
|
|
63
|
+
* @related b.middleware.rateLimit
|
|
64
|
+
*
|
|
65
|
+
* Per-IP rolling 24-hour byte budget. Tracks request + response
|
|
66
|
+
* bytes per peer key (default: client IP). When a peer exceeds the
|
|
67
|
+
* configured quota, further requests are refused with HTTP 429 +
|
|
68
|
+
* `Retry-After`. The window slides per-second — a peer can't reset
|
|
69
|
+
* by waiting past midnight. Composes `b.network.byteQuota`; handlers
|
|
70
|
+
* that already know the byte cost of an op can call
|
|
71
|
+
* `b.network.byteQuota.check`/`record` directly. Fails open (request
|
|
72
|
+
* proceeds, audit emitted) when the backing cache is unreachable.
|
|
73
|
+
*
|
|
74
|
+
* @opts
|
|
75
|
+
* {
|
|
76
|
+
* bytesPerDay: number, // required, positive, finite
|
|
77
|
+
* getKey: function(req): string|null, // default: req client IP
|
|
78
|
+
* cache: object, // null = in-memory single-node
|
|
79
|
+
* onExceeded: function(req, res, info): void,
|
|
80
|
+
* skipPaths: string[],
|
|
81
|
+
* now: function(): number,
|
|
82
|
+
* audit: boolean, // default true
|
|
83
|
+
* }
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* var b = require("@blamejs/core");
|
|
87
|
+
* var app = b.router.create();
|
|
88
|
+
* app.use(b.middleware.dailyByteQuota({
|
|
89
|
+
* bytesPerDay: b.constants.BYTES.gib(2),
|
|
90
|
+
* skipPaths: ["/healthz"],
|
|
91
|
+
* }));
|
|
92
|
+
*/
|
|
134
93
|
function create(opts) {
|
|
135
94
|
opts = opts || {};
|
|
136
95
|
validateOpts(opts, [
|
|
@@ -149,9 +108,17 @@ function create(opts) {
|
|
|
149
108
|
var onExceeded = typeof opts.onExceeded === "function" ? opts.onExceeded : null;
|
|
150
109
|
var skipPaths = Array.isArray(opts.skipPaths) ? opts.skipPaths.slice() : [];
|
|
151
110
|
var now = typeof opts.now === "function" ? opts.now : function () { return Date.now(); };
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
111
|
+
|
|
112
|
+
// Compose the standalone primitive — the middleware drives the same
|
|
113
|
+
// counter store the operator-facing b.network.byteQuota.check /
|
|
114
|
+
// record API exposes. No parallel byte-counter implementation.
|
|
115
|
+
var quota = networkByteQuota.create({
|
|
116
|
+
bytesPerDay: bytesPerDay,
|
|
117
|
+
cache: opts.cache || null,
|
|
118
|
+
audit: false, // middleware emits its own per-rejection audits
|
|
119
|
+
now: now,
|
|
120
|
+
});
|
|
121
|
+
var backend = quota._backend;
|
|
155
122
|
|
|
156
123
|
function _shouldSkip(req) {
|
|
157
124
|
if (skipPaths.length === 0) return false;
|
|
@@ -204,7 +171,7 @@ function create(opts) {
|
|
|
204
171
|
var info = {
|
|
205
172
|
quota: bytesPerDay,
|
|
206
173
|
total: total,
|
|
207
|
-
retryAfterSec: Math.
|
|
174
|
+
retryAfterSec: Math.ceil(C.TIME.hours(1) / C.TIME.seconds(1)),
|
|
208
175
|
};
|
|
209
176
|
if (onExceeded) {
|
|
210
177
|
try { return onExceeded(req, res, info); }
|
|
@@ -270,6 +237,7 @@ function create(opts) {
|
|
|
270
237
|
module.exports = {
|
|
271
238
|
create: create,
|
|
272
239
|
DailyByteQuotaError: DailyByteQuotaError,
|
|
273
|
-
|
|
274
|
-
|
|
240
|
+
BINS_PER_DAY: networkByteQuota.BINS_PER_DAY,
|
|
241
|
+
// Backward-compat re-export for tests that mocked the in-process backend.
|
|
242
|
+
_memoryBackend: networkByteQuota._memoryBackend,
|
|
275
243
|
};
|
|
@@ -107,6 +107,46 @@ function _defaultResponder(req, res, status, info) {
|
|
|
107
107
|
res.end(JSON.stringify(info));
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* @primitive b.middleware.dbRoleFor
|
|
112
|
+
* @signature b.middleware.dbRoleFor(opts)
|
|
113
|
+
* @since 0.1.0
|
|
114
|
+
* @related b.middleware.attachUser, b.externalDb.query
|
|
115
|
+
*
|
|
116
|
+
* Binds a request-time database role into AsyncLocalStorage so
|
|
117
|
+
* `b.externalDb.query` / `read` / `write` / `transaction` auto-route
|
|
118
|
+
* to the matching backend (`app_user`, `analytics_user`, etc.) without
|
|
119
|
+
* any operator threading of the role through the handler signature.
|
|
120
|
+
* Resolution order: operator-supplied `resolve(req)` → permissions
|
|
121
|
+
* RBAC `dbRoleFor` → `defaultRole` → null. Throws at create-time on
|
|
122
|
+
* bad opts (resolver/responder shape, malformed default-role
|
|
123
|
+
* identifier, out-of-range missing-role status). Runtime returns
|
|
124
|
+
* are validated against `safeSql.validateIdentifier` — a garbage
|
|
125
|
+
* resolver return surfaces as next(err), not silent fallthrough.
|
|
126
|
+
* Emits `db.role.switched` audit + `db.role.bound` observability.
|
|
127
|
+
*
|
|
128
|
+
* @opts
|
|
129
|
+
* {
|
|
130
|
+
* resolve: function(req): string|null,
|
|
131
|
+
* permissions: object, // b.permissions instance
|
|
132
|
+
* defaultRole: string,
|
|
133
|
+
* requireRole: boolean,
|
|
134
|
+
* missingRoleStatus: number, // default 401
|
|
135
|
+
* responder: function(req, res, status, info): void,
|
|
136
|
+
* audit: object,
|
|
137
|
+
* auditFailures: boolean, // default true
|
|
138
|
+
* auditSuccess: boolean, // default true
|
|
139
|
+
* }
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* var b = require("@blamejs/core");
|
|
143
|
+
* var app = b.router.create();
|
|
144
|
+
* app.use(b.middleware.attachUser({ userLoader: async function () { return { id: 1 }; } }));
|
|
145
|
+
* app.use(b.middleware.dbRoleFor({
|
|
146
|
+
* defaultRole: "app_user",
|
|
147
|
+
* resolve: function (req) { return req.user && req.user.dbRole; },
|
|
148
|
+
* }));
|
|
149
|
+
*/
|
|
110
150
|
function create(opts) {
|
|
111
151
|
opts = opts || {};
|
|
112
152
|
validateOpts(opts, ALLOWED_OPTS, "middleware.dbRoleFor");
|
package/lib/middleware/dpop.js
CHANGED
|
@@ -117,6 +117,46 @@ function _reconstructHtu(req) {
|
|
|
117
117
|
return proto + "://" + host + path;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* @primitive b.middleware.dpop
|
|
122
|
+
* @signature b.middleware.dpop(opts)
|
|
123
|
+
* @since 0.1.0
|
|
124
|
+
* @related b.middleware.bearerAuth, b.auth.jwt
|
|
125
|
+
*
|
|
126
|
+
* RFC 9449 Demonstrating Proof of Possession (DPoP). Verifies the
|
|
127
|
+
* `DPoP` header on inbound requests, attaches `req.dpop = { header,
|
|
128
|
+
* payload, jkt }` for downstream handlers to bind to the access
|
|
129
|
+
* token's `cnf.jkt` claim, and refuses with HTTP 401 +
|
|
130
|
+
* `WWW-Authenticate: DPoP error="invalid_dpop_proof"` on any
|
|
131
|
+
* failure. Replay store enforces single-use proofs within
|
|
132
|
+
* `iatWindowSec`. Optional server-issued nonce (RFC 9449 §8) with
|
|
133
|
+
* `requireNonce: true` rotates a current/previous pair lazily so
|
|
134
|
+
* in-flight clients aren't kicked off at rotation. Algorithm
|
|
135
|
+
* allowlist defaults to ES256 / EdDSA / ML-DSA-87 (PQC-first).
|
|
136
|
+
*
|
|
137
|
+
* @opts
|
|
138
|
+
* {
|
|
139
|
+
* replayStore: object, // required
|
|
140
|
+
* algorithms: string[], // default ES256/EdDSA/ML-DSA-87
|
|
141
|
+
* iatWindowSec: number, // default 60
|
|
142
|
+
* getAccessToken: function(req): string|null,
|
|
143
|
+
* getNonce: async function(req): string|null,
|
|
144
|
+
* getHtu: function(req): string,
|
|
145
|
+
* nonceStore: object,
|
|
146
|
+
* nonceWindowSec: number,
|
|
147
|
+
* nonceRotateSec: number,
|
|
148
|
+
* requireNonce: boolean,
|
|
149
|
+
* audit: boolean, // default true
|
|
150
|
+
* }
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* var b = require("@blamejs/core");
|
|
154
|
+
* var app = b.router.create();
|
|
155
|
+
* app.use("/api", b.middleware.dpop({
|
|
156
|
+
* replayStore: b.nonceStore.create({ backend: "memory" }),
|
|
157
|
+
* iatWindowSec: 60,
|
|
158
|
+
* }));
|
|
159
|
+
*/
|
|
120
160
|
function create(opts) {
|
|
121
161
|
opts = opts || {};
|
|
122
162
|
validateOpts(opts, [
|
|
@@ -1,24 +1,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Error-handler middleware — thin adapter over lib/
|
|
3
|
+
* Error-handler middleware — thin adapter over lib/error-page.
|
|
4
4
|
*
|
|
5
5
|
* Use this for the standard wiring path (`router.onError(b.middleware.
|
|
6
|
-
* errorHandler())`). Constructs an
|
|
7
|
-
* the (err, req, res, next) router signature into it.
|
|
8
|
-
* rendering, content negotiation, dev/prod gating, and audit
|
|
9
|
-
*
|
|
10
|
-
* convention plus the audit-action override that
|
|
11
|
-
*
|
|
12
|
-
* uses for HTTP-layer failures.
|
|
13
|
-
*
|
|
14
|
-
* Options forward to errors-page.create with one default override:
|
|
15
|
-
* auditAction: "system.http.error" (vs errors-page default "request.error")
|
|
16
|
-
*
|
|
17
|
-
* Plus a back-compat alias:
|
|
18
|
-
* exposeStackInDev: forwards to opts.showStack (true|false)
|
|
6
|
+
* errorHandler())`). Constructs an error-page handler and forwards
|
|
7
|
+
* the (err, req, res, next) router signature into it. Classification,
|
|
8
|
+
* rendering, content negotiation, dev/prod gating, and audit
|
|
9
|
+
* emission all live in `b.errorPage` — this file only wires the
|
|
10
|
+
* router middleware convention plus the audit-action override that
|
|
11
|
+
* preserves the `system.http.error` action name the framework's
|
|
12
|
+
* audit log already uses for HTTP-layer failures.
|
|
19
13
|
*/
|
|
20
14
|
var errorPage = require("../error-page");
|
|
21
15
|
|
|
16
|
+
/**
|
|
17
|
+
* @primitive b.middleware.errorHandler
|
|
18
|
+
* @signature b.middleware.errorHandler(err, req, res, next)
|
|
19
|
+
* @since 0.1.0
|
|
20
|
+
* @related b.errorPage.create
|
|
21
|
+
*
|
|
22
|
+
* Thin adapter over `lib/error-page`. Constructed via the factory
|
|
23
|
+
* call `b.middleware.errorHandler(opts)`; the resulting middleware
|
|
24
|
+
* has the `(err, req, res, next)` shape shown above. Forwards the
|
|
25
|
+
* router signature into an errors-page handler.
|
|
26
|
+
* Classification, rendering, content negotiation, and audit emission
|
|
27
|
+
* live in `b.errorPage`; this middleware only sets the audit action
|
|
28
|
+
* to `system.http.error` and defaults to JSON output (page-style
|
|
29
|
+
* HTML negotiation is reachable via `b.errorPage.create` directly).
|
|
30
|
+
*
|
|
31
|
+
* @opts
|
|
32
|
+
* {
|
|
33
|
+
* auditAction: string, // default "system.http.error"
|
|
34
|
+
* defaultFormat: "json"|"html"|"auto",// default "json"
|
|
35
|
+
* showStack: boolean, // dev-stack exposure
|
|
36
|
+
* exposeStackInDev: boolean, // back-compat alias for showStack
|
|
37
|
+
* // ...all other b.errorPage.create opts forward unchanged
|
|
38
|
+
* }
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* var b = require("@blamejs/core");
|
|
42
|
+
* var app = b.router.create();
|
|
43
|
+
* app.onError(b.middleware.errorHandler({ showStack: false }));
|
|
44
|
+
*/
|
|
22
45
|
function create(opts) {
|
|
23
46
|
opts = opts || {};
|
|
24
47
|
var pageOpts = Object.assign({}, opts);
|
|
@@ -52,6 +52,45 @@ function _writeReject(res, message) {
|
|
|
52
52
|
res.end(body);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* @primitive b.middleware.fetchMetadata
|
|
57
|
+
* @signature b.middleware.fetchMetadata(req, res, next)
|
|
58
|
+
* @since 0.1.0
|
|
59
|
+
* @related b.middleware.cors, b.middleware.csrfProtect
|
|
60
|
+
*
|
|
61
|
+
* Resource Isolation Policy enforced via Sec-Fetch-Site / -Mode /
|
|
62
|
+
* -Dest. Constructed via `b.middleware.fetchMetadata(opts)`; the
|
|
63
|
+
* resulting middleware has the `(req, res, next)` shape shown above. Refuses cross-site requests on state-changing methods
|
|
64
|
+
* unless the operator allowlists them — second-line defense
|
|
65
|
+
* alongside CSRF tokens. Same-origin / same-site requests pass
|
|
66
|
+
* through. Cross-site is refused with HTTP 403 unless `allowedDest`
|
|
67
|
+
* contains the request's `Sec-Fetch-Dest`. Legacy browsers without
|
|
68
|
+
* Sec-Fetch-* default to `allowMissing: true` so the gate doesn't
|
|
69
|
+
* break older clients — the browser-fetch-metadata isolation IS
|
|
70
|
+
* the value-add; non-browser callers carry their own auth threat
|
|
71
|
+
* model.
|
|
72
|
+
*
|
|
73
|
+
* @opts
|
|
74
|
+
* {
|
|
75
|
+
* allowSameSite: boolean, // default true
|
|
76
|
+
* allowCrossSite: boolean, // default false
|
|
77
|
+
* allowMissing: boolean, // default true
|
|
78
|
+
* allowedDest: string[],
|
|
79
|
+
* allowedNavigate: boolean, // default true
|
|
80
|
+
* methods: string[], // default POST/PUT/DELETE/PATCH
|
|
81
|
+
* audit: boolean, // default true
|
|
82
|
+
* }
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* var b = require("@blamejs/core");
|
|
86
|
+
* var app = b.router.create();
|
|
87
|
+
* app.use("/api", b.middleware.fetchMetadata({
|
|
88
|
+
* allowSameSite: true,
|
|
89
|
+
* allowCrossSite: false,
|
|
90
|
+
* allowedDest: ["empty", "document"],
|
|
91
|
+
* allowedNavigate: true,
|
|
92
|
+
* }));
|
|
93
|
+
*/
|
|
55
94
|
function create(opts) {
|
|
56
95
|
opts = opts || {};
|
|
57
96
|
validateOpts(opts, [
|
|
@@ -30,6 +30,40 @@ var FlagError = defineClass("FlagError", { alwaysPermanent: true });
|
|
|
30
30
|
|
|
31
31
|
var contextMod = lazyRequire(function () { return require("../flag-evaluation-context"); });
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* @primitive b.middleware.flagContext
|
|
35
|
+
* @signature b.middleware.flagContext(opts)
|
|
36
|
+
* @since 0.1.0
|
|
37
|
+
* @related b.flagClient.getBoolean
|
|
38
|
+
*
|
|
39
|
+
* Extracts an OpenFeature evaluation context onto `req.flagCtx` so
|
|
40
|
+
* downstream handlers and multiple flag clients read a consistent
|
|
41
|
+
* context without re-deriving it per call. The middleware itself
|
|
42
|
+
* does NOT evaluate flags — pair with `flag.middleware()` for the
|
|
43
|
+
* request-attached accessor, or pass `req.flagCtx` directly to a
|
|
44
|
+
* flag client method when several clients with different providers
|
|
45
|
+
* share the same context. `userKey` (literal) takes precedence over
|
|
46
|
+
* `userKeyHeader`; `tenantKeyHeader` augments with tenantId; the
|
|
47
|
+
* operator-supplied `extractAttributes(req)` adds arbitrary fields.
|
|
48
|
+
*
|
|
49
|
+
* @opts
|
|
50
|
+
* {
|
|
51
|
+
* userKey: string,
|
|
52
|
+
* userKeyHeader: string,
|
|
53
|
+
* tenantKeyHeader: string,
|
|
54
|
+
* extractAttributes: function(req): object,
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* var b = require("@blamejs/core");
|
|
59
|
+
* var app = b.router.create();
|
|
60
|
+
* app.use(b.middleware.flagContext({
|
|
61
|
+
* userKeyHeader: "x-user-id",
|
|
62
|
+
* extractAttributes: function (req) {
|
|
63
|
+
* return { environment: "prod" };
|
|
64
|
+
* },
|
|
65
|
+
* }));
|
|
66
|
+
*/
|
|
33
67
|
function create(opts) {
|
|
34
68
|
opts = opts || {};
|
|
35
69
|
validateOpts(opts, [
|
package/lib/middleware/gpc.js
CHANGED
|
@@ -68,6 +68,39 @@ function _emitAudit(audit, action, outcome, metadata) {
|
|
|
68
68
|
} catch (_e) { /* drop-silent */ }
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* @primitive b.middleware.gpc
|
|
73
|
+
* @signature b.middleware.gpc(opts)
|
|
74
|
+
* @since 0.1.0
|
|
75
|
+
* @compliance ccpa, modpa
|
|
76
|
+
* @related b.middleware.cookies
|
|
77
|
+
*
|
|
78
|
+
* Sec-GPC (Global Privacy Control) handler. Reads the `Sec-GPC: 1`
|
|
79
|
+
* inbound header and sets `req.gpcOptOut = true` for downstream
|
|
80
|
+
* consumers. Echoes `Sec-GPC-Status: honored` so the UA + auditors
|
|
81
|
+
* see the acknowledgement. Honoring Sec-GPC is legally required in
|
|
82
|
+
* California (CCPA/CPRA) and a growing list of US states; operators
|
|
83
|
+
* who don't process the listed purposes (`sale`, `share`,
|
|
84
|
+
* `targeted-ads`, `cross-context-behavioral-advertising`,
|
|
85
|
+
* `profiling`) still emit the acknowledgement so audits trace.
|
|
86
|
+
* Optional `consent` integration auto-records purpose withdrawal.
|
|
87
|
+
*
|
|
88
|
+
* @opts
|
|
89
|
+
* {
|
|
90
|
+
* mode: "enforce"|"audit-only", // default "enforce"
|
|
91
|
+
* consent: object, // b.consent instance
|
|
92
|
+
* statusHeader: boolean, // default true
|
|
93
|
+
* audit: object,
|
|
94
|
+
* }
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* var b = require("@blamejs/core");
|
|
98
|
+
* var app = b.router.create();
|
|
99
|
+
* app.use(b.middleware.gpc({ mode: "enforce" }));
|
|
100
|
+
* app.get("/api/data", function (req, res) {
|
|
101
|
+
* res.end(req.gpcOptOut ? "minimal" : "full");
|
|
102
|
+
* });
|
|
103
|
+
*/
|
|
71
104
|
function create(opts) {
|
|
72
105
|
opts = opts || {};
|
|
73
106
|
var mode = opts.mode || "enforce";
|
|
@@ -159,6 +159,41 @@ function _detectIssues(headers, opts) {
|
|
|
159
159
|
return issues;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* @primitive b.middleware.headers
|
|
164
|
+
* @signature b.middleware.headers(opts)
|
|
165
|
+
* @since 0.1.0
|
|
166
|
+
* @related b.middleware.cookies, b.middleware.bodyParser
|
|
167
|
+
*
|
|
168
|
+
* Inbound HTTP header threat detection. Validates header names
|
|
169
|
+
* against the RFC 9110 §5.1 token grammar and surfaces CRLF
|
|
170
|
+
* injection, RFC 9112 §6.1 CL+TE request-smuggling shapes, multiple
|
|
171
|
+
* `Content-Length` / `Transfer-Encoding` values, oversize header
|
|
172
|
+
* count / value, and deprecated `X-Forwarded-*` patterns when the
|
|
173
|
+
* operator hasn't opted into `trustProxy`. In `mode: "enforce"`
|
|
174
|
+
* (default) high-severity issues refuse with HTTP 400 + `Connection:
|
|
175
|
+
* close`; `audit-only` and `log-only` pass through but still emit
|
|
176
|
+
* audits.
|
|
177
|
+
*
|
|
178
|
+
* @opts
|
|
179
|
+
* {
|
|
180
|
+
* mode: "enforce"|"audit-only"|"log-only", // default "enforce"
|
|
181
|
+
* refuseOnHigh: boolean, // default true (enforce only)
|
|
182
|
+
* maxHeaderCount: number, // default 100
|
|
183
|
+
* maxValueBytes: number, // default 8 KiB
|
|
184
|
+
* trustProxy: boolean,
|
|
185
|
+
* audit: object,
|
|
186
|
+
* }
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* var b = require("@blamejs/core");
|
|
190
|
+
* var app = b.router.create();
|
|
191
|
+
* app.use(b.middleware.headers({
|
|
192
|
+
* mode: "enforce",
|
|
193
|
+
* maxHeaderCount: 100,
|
|
194
|
+
* maxValueBytes: b.constants.BYTES.kib(8),
|
|
195
|
+
* }));
|
|
196
|
+
*/
|
|
162
197
|
function create(opts) {
|
|
163
198
|
opts = opts || {};
|
|
164
199
|
var mode = opts.mode || "enforce";
|
package/lib/middleware/health.js
CHANGED
|
@@ -115,6 +115,52 @@ var TIER_SET = new Set(TIERS);
|
|
|
115
115
|
|
|
116
116
|
var DEFAULT_TIMEOUT_MS = C.TIME.seconds(5);
|
|
117
117
|
|
|
118
|
+
/**
|
|
119
|
+
* @primitive b.middleware.health
|
|
120
|
+
* @signature b.middleware.health(req, res, next)
|
|
121
|
+
* @since 0.1.0
|
|
122
|
+
* @related b.middleware.requestId
|
|
123
|
+
*
|
|
124
|
+
* Liveness / readiness / startup probe primitive. Constructed via
|
|
125
|
+
* `b.middleware.health(opts)` returning a controller exposing
|
|
126
|
+
* `registerCheck`, `markShuttingDown`, and `middleware()`; the
|
|
127
|
+
* `middleware()` value has the `(req, res, next)` shape shown above. Three tiers, each
|
|
128
|
+
* its own URL path: `/healthz` (process alive — orchestrator kill
|
|
129
|
+
* decision), `/readyz` (can serve traffic — LB route decision),
|
|
130
|
+
* `/startupz` (slow-init complete — Kubernetes startupProbe).
|
|
131
|
+
* `markShuttingDown()` flips ONLY readiness to 503 so LBs drain
|
|
132
|
+
* while `/healthz` keeps responding 200 through clean exit.
|
|
133
|
+
* Detail level defaults to "minimal" (`{ status }` only — no
|
|
134
|
+
* internal-state leakage); `detailLevel: "detailed"` and
|
|
135
|
+
* `detailPredicate(req)` gate the full per-check breakdown to
|
|
136
|
+
* authed endpoints. Per-check `Promise.all` + `withTimeout` so one
|
|
137
|
+
* stuck check can't block the others. Returns a controller exposing
|
|
138
|
+
* `registerCheck`, `markShuttingDown`, and `middleware()`.
|
|
139
|
+
*
|
|
140
|
+
* @opts
|
|
141
|
+
* {
|
|
142
|
+
* livenessPath: string, // default "/healthz"
|
|
143
|
+
* readinessPath: string, // default "/readyz"
|
|
144
|
+
* startupPath: string, // default "/startupz"
|
|
145
|
+
* detailLevel: "minimal"|"detailed", // default "minimal"
|
|
146
|
+
* detailPredicate: function(req): boolean,
|
|
147
|
+
* defaultTimeoutMs: number, // default 5000
|
|
148
|
+
* cacheMs: number, // default 0
|
|
149
|
+
* includeMeta: boolean,
|
|
150
|
+
* version: string,
|
|
151
|
+
* }
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* var b = require("@blamejs/core");
|
|
155
|
+
* var app = b.router.create();
|
|
156
|
+
* var hc = b.middleware.health({
|
|
157
|
+
* livenessPath: "/healthz",
|
|
158
|
+
* readinessPath: "/readyz",
|
|
159
|
+
* defaultTimeoutMs: 5000,
|
|
160
|
+
* });
|
|
161
|
+
* hc.registerCheck("db", async function () { return { ok: true }; }, { tier: "readiness" });
|
|
162
|
+
* app.use(hc.middleware());
|
|
163
|
+
*/
|
|
118
164
|
function create(opts) {
|
|
119
165
|
opts = opts || {};
|
|
120
166
|
validateOpts(opts, [
|
|
@@ -81,6 +81,36 @@ function _matches(entry, actual) {
|
|
|
81
81
|
return false;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* @primitive b.middleware.hostAllowlist
|
|
86
|
+
* @signature b.middleware.hostAllowlist(opts)
|
|
87
|
+
* @since 0.1.0
|
|
88
|
+
* @related b.middleware.networkAllowlist, b.middleware.cors
|
|
89
|
+
*
|
|
90
|
+
* DNS-rebinding defense. Refuses requests whose `Host` header
|
|
91
|
+
* doesn't match the operator-supplied allowlist. Wildcard-leading
|
|
92
|
+
* entries (`*.example.com`) match a single label. Entries without
|
|
93
|
+
* a port match any port. Refuses with HTTP 421 (Misdirected
|
|
94
|
+
* Request) by default. Operators behind a CDN that rewrites the
|
|
95
|
+
* Host set `hosts` to the post-rewrite values. Skip entirely for
|
|
96
|
+
* services that serve arbitrary subdomains by design (anyone-can-
|
|
97
|
+
* host-the-domain shapes).
|
|
98
|
+
*
|
|
99
|
+
* @opts
|
|
100
|
+
* {
|
|
101
|
+
* hosts: string[], // required, non-empty
|
|
102
|
+
* denyStatus: number, // default 421
|
|
103
|
+
* denyBody: string,
|
|
104
|
+
* audit: boolean, // default true
|
|
105
|
+
* }
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* var b = require("@blamejs/core");
|
|
109
|
+
* var app = b.router.create();
|
|
110
|
+
* app.use(b.middleware.hostAllowlist({
|
|
111
|
+
* hosts: ["app.example.com", "*.example.com"],
|
|
112
|
+
* }));
|
|
113
|
+
*/
|
|
84
114
|
function create(opts) {
|
|
85
115
|
validateOpts.requireObject(opts, "middleware.hostAllowlist", HostAllowlistError);
|
|
86
116
|
validateOpts(opts, [
|
|
@@ -71,6 +71,44 @@ function _validateCidr(cidr) {
|
|
|
71
71
|
catch (_e) { return false; }
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* @primitive b.middleware.networkAllowlist
|
|
76
|
+
* @signature b.middleware.networkAllowlist(req, res, next)
|
|
77
|
+
* @since 0.1.0
|
|
78
|
+
* @related b.middleware.hostAllowlist, b.middleware.requireAuth
|
|
79
|
+
*
|
|
80
|
+
* In-process CIDR fence for path-scoped admin gates. Constructed
|
|
81
|
+
* via `b.middleware.networkAllowlist(opts)`; the resulting
|
|
82
|
+
* middleware has the `(req, res, next)` shape shown above. Path-based
|
|
83
|
+
* authorization prevents unauthorized USERS from reaching sensitive
|
|
84
|
+
* routes; this middleware adds a NETWORK-layer fence so a credential
|
|
85
|
+
* leak doesn't compromise the gate. Path-scoped — requests outside
|
|
86
|
+
* the configured prefixes pass through hot-path-cheap. Resolves
|
|
87
|
+
* client IP via `b.requestHelpers.clientIp` (with `trustProxy`),
|
|
88
|
+
* checks against the CIDR allowlist via `b.ssrfGuard.cidrContains`,
|
|
89
|
+
* and refuses misses with HTTP 404 by default (hides the gate from
|
|
90
|
+
* probes). Throws at create-time on malformed opts.
|
|
91
|
+
*
|
|
92
|
+
* @opts
|
|
93
|
+
* {
|
|
94
|
+
* paths: string[], // pathname prefixes, required
|
|
95
|
+
* allowedCidrs: string[], // required
|
|
96
|
+
* deniedCidrs: string[],
|
|
97
|
+
* trustProxy: boolean, // default false
|
|
98
|
+
* denyStatus: number, // default 404
|
|
99
|
+
* denyBody: string, // default "Not Found"
|
|
100
|
+
* audit: object,
|
|
101
|
+
* }
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* var b = require("@blamejs/core");
|
|
105
|
+
* var app = b.router.create();
|
|
106
|
+
* app.use(b.middleware.networkAllowlist({
|
|
107
|
+
* paths: ["/admin"],
|
|
108
|
+
* allowedCidrs: ["10.0.0.0/8", "::1/128"],
|
|
109
|
+
* trustProxy: true,
|
|
110
|
+
* }));
|
|
111
|
+
*/
|
|
74
112
|
function create(opts) {
|
|
75
113
|
opts = opts || {};
|
|
76
114
|
validateOpts(opts, [
|