@blamejs/core 0.6.7 → 0.6.11
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 +16 -11
- package/README.md +1 -1
- package/lib/api-key.js +9 -9
- package/lib/archive.js +5 -5
- package/lib/audit.js +2 -2
- package/lib/auth/jwt.js +4 -4
- package/lib/break-glass.js +7 -6
- package/lib/cache.js +9 -9
- package/lib/constants.js +6 -6
- package/lib/credential-hash.js +9 -9
- package/lib/db-declare-view.js +2 -2
- package/lib/external-db-migrate.js +1 -1
- package/lib/http-client.js +3 -3
- package/lib/i18n.js +12 -12
- package/lib/middleware/body-parser.js +32 -0
- package/lib/middleware/cors.js +1 -1
- package/lib/middleware/csrf-protect.js +2 -2
- package/lib/middleware/rate-limit.js +2 -2
- package/lib/notify.js +2 -2
- package/lib/object-store/sigv4-bucket-ops.js +6 -5
- package/lib/object-store/sigv4.js +3 -3
- package/lib/observability.js +39 -13
- package/lib/otel-export.js +2 -2
- package/lib/pagination.js +6 -6
- package/lib/permissions.js +7 -7
- package/lib/protocol-dispatcher.js +2 -2
- package/lib/queue.js +1 -1
- package/lib/request-helpers.js +3 -3
- package/lib/retry.js +20 -19
- package/lib/seeders.js +9 -9
- package/lib/slug.js +6 -6
- package/lib/testing.js +5 -5
- package/lib/validate-opts.js +2 -1
- package/lib/webhook.js +7 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,11 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.6.x
|
|
10
10
|
|
|
11
|
+
- **0.6.11** (2026-05-01) — wiki example-execution validator: fixture init no longer reaches across module realms (unblocks the npm-publish workflow's wiki-e2e gate, which `npm install --install-links` copies the framework into the wiki's node_modules — two distinct singletons before this fix)
|
|
12
|
+
- **0.6.10** (2026-05-01) — README / SECURITY / CONTRIBUTING / wiki: removed stale version stamps and an inaccurate vendored-dep list; SECURITY now points at `lib/vendor/MANIFEST.json` for the authoritative vendor list; supported-versions table no longer pins to a specific minor; wiki archive example names the digest variable correctly (was `sha256`, output is SHA3-512 hex)
|
|
13
|
+
- **0.6.9** (2026-05-01) — b.archive.zip().digest() returns a SHA3-512 hex string (was SHA-256); operators reconciling against an external SHA-256 must hash the bytes themselves
|
|
14
|
+
- **0.6.8** (2026-05-01) — wiki primitive-section validator: presence + opts diff + example execution against canonical fixture
|
|
15
|
+
- **0.6.7** (2026-05-01) — db.role.switched audit + per-role metrics + API snapshot baseline
|
|
11
16
|
- **0.6.6** (2026-05-01) — request-time DB role binding + Postgres RLS migrations
|
|
12
17
|
- **0.6.5** (2026-05-01) — b.db.declareView + b.externalDb.migrate
|
|
13
18
|
- **0.6.4** (2026-05-01) — wiki schema docs realigned with the actual lib API
|
|
@@ -73,7 +78,7 @@ upgrading across more than a few patches at a time.
|
|
|
73
78
|
|
|
74
79
|
## v0.3.x
|
|
75
80
|
|
|
76
|
-
- **0.3.39** (2026-04-29) — MIGRATING.md generator scans deprecate
|
|
81
|
+
- **0.3.39** (2026-04-29) — MIGRATING.md generator scans deprecate calls in lib/
|
|
77
82
|
- **0.3.38** (2026-04-29) — LTS-CALENDAR.md publishes the major-cadence + algorithm posture
|
|
78
83
|
- **0.3.37** (2026-04-29) — bundled pure-JS mTLS engine + vendor-update.sh + CHANGELOG + thanks page
|
|
79
84
|
- **0.3.36** (2026-04-29) — prepack guard refuses to publish any path that matches a gitignore rule
|
|
@@ -88,15 +93,15 @@ upgrading across more than a few patches at a time.
|
|
|
88
93
|
- **0.3.27** (2026-04-29) — Dockerfile pre-builds public/dist in deps stage so runtime container boots cleanly under --cap-drop ALL
|
|
89
94
|
- **0.3.26** (2026-04-29) — operator-facing env-var surface, GitHub-side ops docs, contribution standards
|
|
90
95
|
- **0.3.25** (2026-04-29) — env-var surface documented across Dockerfile, docker-compose.prod, DEPLOY.md, README; smoke-test GHCR login fix
|
|
91
|
-
- **0.3.24** (2026-04-29) — CI + release-container workflows brought
|
|
96
|
+
- **0.3.24** (2026-04-29) — CI + release-container workflows brought (structured output, multi-arch, cosign, post-publish smoke)
|
|
92
97
|
- **0.3.23** (2026-04-29) — _runVault and _runBackup refactored to use b.cliHelpers (sweep complete across the three CLI commands the helper was designed for)
|
|
93
98
|
- **0.3.22** (2026-04-29) — blamejs api-key CLI; b.cliHelpers shared primitive for headless app + reporter shape
|
|
94
99
|
- **0.3.21** (2026-04-29) — blamejs backup CLI: inspect / verify / extract; backup encryption format documented
|
|
95
100
|
- **0.3.20** (2026-04-29) — drop redundant Windows-flaky audit.flush perf assertion
|
|
96
101
|
- **0.3.19** (2026-04-29) — wiki docs for the unwired primitives surface
|
|
97
102
|
- **0.3.18** (2026-04-29) — blamejs vault CLI: seal / unseal / rotate / status
|
|
98
|
-
- **0.3.17** (2026-04-29) —
|
|
99
|
-
- **0.3.16** (2026-04-29) —
|
|
103
|
+
- **0.3.17** (2026-04-29) — validateOpts coverage extended to remaining operator-facing primitives (i18n, bundler, migrations, seeders, template, jobs, cookies, log, mtlsCa, staticServe, handlers, websocket, pqcGate, tracing, metrics — 15 more factories now reject typo'd opts at boot). Coverage is now ~39/lib primitives with the helper; the remaining handful (app-shutdown, chain-writer, dev, error-page, nonce-store, protocol-dispatcher, render, session, pqc-agent) are either internal-only — operators don't construct them directly — or have a single required-key surface where typo guards add no value.
|
|
104
|
+
- **0.3.16** (2026-04-29) — config validation, CSRF cookie mode, HTML balance, dynamic Cache-Control, URL canonicalization
|
|
100
105
|
- **0.3.15** (2026-04-29) — cors: accept Origin: null when Sec-Fetch-Site is same-origin (browsers send opaque Origin: null on form-navigation POSTs from a page whose response sets Referrer-Policy: no-referrer; the Fetch-metadata Sec-Fetch-Site header is the matching disambiguation signal — same-origin or none means accept, anything else stays refused)
|
|
101
106
|
- **0.3.14** (2026-04-29) — Dockerfile hotfix + lint as release gate
|
|
102
107
|
- **0.3.13** (2026-04-29) — CI lint scans + Wolfi container + Trivy two-step + lint cleanup
|
|
@@ -133,7 +138,7 @@ upgrading across more than a few patches at a time.
|
|
|
133
138
|
- **0.2.25** (2026-04-28) — b.slug
|
|
134
139
|
- **0.2.24** (2026-04-28) — b.retry primitive
|
|
135
140
|
- **0.2.23** (2026-04-28) — test helpers consolidate
|
|
136
|
-
- **0.2.22** (2026-04-28) —
|
|
141
|
+
- **0.2.22** (2026-04-28) — input validation
|
|
137
142
|
- **0.2.21** (2026-04-28) — defineClass.factory replaces per-module _err wrappers
|
|
138
143
|
- **0.2.20** (2026-04-28) — b.requestHelpers + metrics _normalizeLabelArg
|
|
139
144
|
- **0.2.19** (2026-04-28) — audit.safeEmit replaces per-module _emit wrappers
|
|
@@ -195,22 +200,22 @@ upgrading across more than a few patches at a time.
|
|
|
195
200
|
- **0.1.78** (2026-04-27) — pqc-agent: locked-posture HTTPS agent + http-client refactor — slice 2
|
|
196
201
|
- **0.1.77** (2026-04-27) — pqc-gate: TCP-level PQC enforcement on ClientHello — slice 1
|
|
197
202
|
- **0.1.76** (2026-04-27) — bundler: content-hashed asset pipeline + manifest — complete
|
|
198
|
-
- **0.1.75** (2026-04-27) — dev: file-watch + child-process restart engine — slice 6 (
|
|
203
|
+
- **0.1.75** (2026-04-27) — dev: file-watch + child-process restart engine — slice 6 (complete)
|
|
199
204
|
- **0.1.74** (2026-04-27) — cli: bin/blamejs + migrate up/down/status — slice 5
|
|
200
205
|
- **0.1.73** (2026-04-27) — migrations: public up/down/status runner — slice 4
|
|
201
206
|
- **0.1.72** (2026-04-27) — cookies: parse/serialize + sealed-value access gate — slice 3
|
|
202
207
|
- **0.1.71** (2026-04-27) — errors-page: rich dev page + safe prod page; middleware shim — slice 2
|
|
203
208
|
- **0.1.70** (2026-04-27) — log: structured JSON logging with request-id correlation — slice 1
|
|
204
|
-
- **0.1.69** (2026-04-27) — scheduler: cron + interval over jobs/queue — slice 5 (
|
|
209
|
+
- **0.1.69** (2026-04-27) — scheduler: cron + interval over jobs/queue — slice 5 (complete)
|
|
205
210
|
- **0.1.68** (2026-04-27) — mail: generalize http transport, demote resend to thin preset
|
|
206
211
|
- **0.1.67** (2026-04-27) — mail: contract + console/memory/smtp/resend transports — slice 4
|
|
207
212
|
- **0.1.66** (2026-04-26) — jobs (define + enqueue + in-process worker) + createApp wiring — slices 2/3
|
|
208
213
|
- **0.1.65** (2026-04-26) — createApp factory — slice 1
|
|
209
|
-
- **0.1.64** (2026-04-26) — forms + csrfProtect — slice 4 (
|
|
214
|
+
- **0.1.64** (2026-04-26) — forms + csrfProtect — slice 4 (complete)
|
|
210
215
|
- **0.1.63** (2026-04-26) — staticServe: file serving + ETag + SRI integrity — slice 3
|
|
211
216
|
- **0.1.62** (2026-04-26) — render: HTTP response helpers — slice 2
|
|
212
217
|
- **0.1.61** (2026-04-26) — template engine (eval-free interpreter) — slice 1
|
|
213
|
-
- **0.1.60** (2026-04-26) — auth.jwt with SLH-DSA-SHAKE-256f default — slice 5/5 (
|
|
218
|
+
- **0.1.60** (2026-04-26) — auth.jwt with SLH-DSA-SHAKE-256f default — slice 5/5 (complete)
|
|
214
219
|
- **0.1.59** (2026-04-26) — middleware.attachUser + middleware.requireAuth — slice 4
|
|
215
220
|
- **0.1.58** (2026-04-26) — auth.passkey (WebAuthn / FIDO2) — slice 3
|
|
216
221
|
- **0.1.57** (2026-04-26) — TOTP defaults match spec (HMAC-SHA512 / 128-byte secret / 8 digits / 30s) + crypto.random truncation fix
|
|
@@ -221,7 +226,7 @@ upgrading across more than a few patches at a time.
|
|
|
221
226
|
- **0.1.52** (2026-04-26) — consent_log integrity parity with audit_log (closes the cluster spec storage move)
|
|
222
227
|
- **0.1.51** (2026-04-26) — queue jobs move to external-db in cluster mode
|
|
223
228
|
- **0.1.50** (2026-04-26) — sessions move to external-db in cluster mode
|
|
224
|
-
- **0.1.49** (2026-04-26) — boot-time vault-key consistency check (closes cluster spec
|
|
229
|
+
- **0.1.49** (2026-04-26) — boot-time vault-key consistency check (closes cluster spec)
|
|
225
230
|
- **0.1.48** (2026-04-26) — boot-time audit-tip rollback detection in cluster mode
|
|
226
231
|
- **0.1.47** (2026-04-26) — fix handlers.drain unbounded loop on recursive emit (cluster-mode audit hang)
|
|
227
232
|
- **0.1.46** (2026-04-26) — cluster-mode audit-tip fencing-token guard
|
|
@@ -278,7 +283,7 @@ upgrading across more than a few patches at a time.
|
|
|
278
283
|
- **0.0.18** (2026-04-26) — strip internal-process narrative from framework comments
|
|
279
284
|
- **0.0.17** (2026-04-25) — internal adoption sweep + functional scale helpers
|
|
280
285
|
- **0.0.16** (2026-04-25) — atomic file I/O + safe parsers (XML, CSV)
|
|
281
|
-
- **0.0.15** (2026-04-25) — HTTP middleware: request lifecycle hardening (
|
|
286
|
+
- **0.0.15** (2026-04-25) — HTTP middleware: request lifecycle hardening (begin)
|
|
282
287
|
- **0.0.14** (2026-04-25) — external DB (Shape B: app-data-only, bring-your-own-client)
|
|
283
288
|
- **0.0.13** (2026-04-25) — log streaming + redaction + bidirectional command channel
|
|
284
289
|
- **0.0.12** (2026-04-25) — queue dispatcher + local SQLite-backed protocol
|
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. Usable end-to-end — operators can build production apps on it today; the surface is still subject to change before 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. The latest release lives on [GitHub](https://github.com/blamejs/blamejs/releases), [npm](https://www.npmjs.com/package/@blamejs/core), and the [container registry](https://github.com/blamejs/blamejs/pkgs/container/blamejs-wiki).
|
|
21
21
|
|
|
22
22
|
```js
|
|
23
23
|
var b = require("@blamejs/core");
|
package/lib/api-key.js
CHANGED
|
@@ -43,14 +43,14 @@
|
|
|
43
43
|
* Same dual-storage pattern as sessions — local SQLite in single-node
|
|
44
44
|
* mode, external-db in cluster mode, dispatched via cluster-storage.
|
|
45
45
|
*
|
|
46
|
-
* Validation
|
|
46
|
+
* Validation policy:
|
|
47
47
|
*
|
|
48
|
-
* - apiKey.create opts →
|
|
49
|
-
* - registry.issue opts →
|
|
50
|
-
* - registry.rotate(id) on missing/revoked →
|
|
51
|
-
* - registry.verify(token) on any failure →
|
|
52
|
-
* - registry.revoke(id) on missing →
|
|
53
|
-
* - registry.getById(id) on missing →
|
|
48
|
+
* - apiKey.create opts → throw at config time
|
|
49
|
+
* - registry.issue opts → throw ApiKeyError at call site
|
|
50
|
+
* - registry.rotate(id) on missing/revoked → throw ApiKeyError at call site
|
|
51
|
+
* - registry.verify(token) on any failure → return null (tolerant read)
|
|
52
|
+
* - registry.revoke(id) on missing → return false (tolerant read)
|
|
53
|
+
* - registry.getById(id) on missing → return null (tolerant read)
|
|
54
54
|
*/
|
|
55
55
|
|
|
56
56
|
var crypto = require("./crypto");
|
|
@@ -69,7 +69,7 @@ var observability = lazyRequire(function () { return require("./observability");
|
|
|
69
69
|
|
|
70
70
|
function _emitEvent(name, value, labels) {
|
|
71
71
|
try { observability().event(name, value, labels || {}); }
|
|
72
|
-
catch (_e) { /*
|
|
72
|
+
catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
var _err = ApiKeyError.factory;
|
|
@@ -127,7 +127,7 @@ var DEFAULTS = Object.freeze({
|
|
|
127
127
|
hashAlgo: "shake256",
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
-
// ----
|
|
130
|
+
// ---- Config-time validation helpers (throw on bad input) ----
|
|
131
131
|
|
|
132
132
|
function _isPositiveInt(n) {
|
|
133
133
|
return typeof n === "number" && isFinite(n) && n >= 1 && Math.floor(n) === n;
|
package/lib/archive.js
CHANGED
|
@@ -216,11 +216,11 @@ function zip() {
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
function digest() {
|
|
219
|
-
//
|
|
220
|
-
// integrity logging on exported bundles.
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
return nodeCrypto.createHash("
|
|
219
|
+
// SHA3-512 of the produced archive bytes — useful for operator-side
|
|
220
|
+
// integrity logging on exported bundles. Matches the framework's
|
|
221
|
+
// PQC-first hash posture; operators reconciling against an external
|
|
222
|
+
// SHA-256 can hash the bytes themselves.
|
|
223
|
+
return nodeCrypto.createHash("sha3-512").update(toBuffer()).digest("hex");
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
return {
|
package/lib/audit.js
CHANGED
|
@@ -711,8 +711,8 @@ function emit(event) {
|
|
|
711
711
|
// audit outage doesn't crash the request handler. This is that helper,
|
|
712
712
|
// hoisted out so each module can stop redefining it.
|
|
713
713
|
//
|
|
714
|
-
//
|
|
715
|
-
//
|
|
714
|
+
// Drop-silent on malformed input by design. safeEmit is called from
|
|
715
|
+
// request hot paths where throwing on a
|
|
716
716
|
// missing `action` would mean a malformed audit attempt crashes the
|
|
717
717
|
// request that triggered it — strictly worse than the missing audit
|
|
718
718
|
// row. Operators who need a hard guarantee the event landed should call
|
package/lib/auth/jwt.js
CHANGED
|
@@ -247,10 +247,10 @@ async function verify(token, opts) {
|
|
|
247
247
|
if (typeof opts.clockToleranceSec !== "number" ||
|
|
248
248
|
!isFinite(opts.clockToleranceSec) ||
|
|
249
249
|
opts.clockToleranceSec < 0) {
|
|
250
|
-
//
|
|
251
|
-
// tokens get rejected as expired or not-yet-valid)
|
|
252
|
-
// value would NaN-poison the comparison. Catch
|
|
253
|
-
//
|
|
250
|
+
// Throw at config time: a negative tolerance over-tightens the
|
|
251
|
+
// window (legit tokens get rejected as expired or not-yet-valid)
|
|
252
|
+
// and a non-finite value would NaN-poison the comparison. Catch
|
|
253
|
+
// both here rather than producing surprise rejections in production.
|
|
254
254
|
throw new AuthError("auth-jwt/bad-clock-tolerance",
|
|
255
255
|
"verify: clockToleranceSec must be a non-negative finite number, got " +
|
|
256
256
|
JSON.stringify(opts.clockToleranceSec));
|
package/lib/break-glass.js
CHANGED
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Spec: memory/specs/blamejs-break-glass-spec.md
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
* 14 error codes + audit chain integration. Model B
|
|
22
|
-
* gate via per-row K_row), passkey factor, service-
|
|
23
|
-
* and admin tools land in
|
|
20
|
+
* Ships Model A (policy gate) + TOTP factor + config-time input
|
|
21
|
+
* validation + 14 error codes + audit chain integration. Model B
|
|
22
|
+
* (cryptographic gate via per-row K_row), passkey factor, service-
|
|
23
|
+
* account bypass, and admin tools land in subsequent patches.
|
|
24
24
|
*
|
|
25
25
|
* Public API:
|
|
26
26
|
*
|
|
@@ -59,8 +59,9 @@ var lockout = lazyRequire(function () { return require("./auth/lockout"); });
|
|
|
59
59
|
var passkey = lazyRequire(function () { return require("./auth/passkey"); });
|
|
60
60
|
|
|
61
61
|
// Errors — all 14 codes documented in the spec. `permanent: true`
|
|
62
|
-
// means caller's input is bad (
|
|
63
|
-
// transient (factor failed, rate-limited)
|
|
62
|
+
// means caller's input is bad (config-time / call-site reject);
|
|
63
|
+
// `permanent: false` means transient (factor failed, rate-limited)
|
|
64
|
+
// — caller may retry.
|
|
64
65
|
var BreakGlassError = defineClass("BreakGlassError", { alwaysPermanent: false });
|
|
65
66
|
|
|
66
67
|
// ---- Defaults (matched to operator-locked decisions) ----
|
package/lib/cache.js
CHANGED
|
@@ -51,14 +51,14 @@
|
|
|
51
51
|
* { get, set, del, clear, size, close } — operator-supplied custom
|
|
52
52
|
* backend (Redis, Memcached, …). All methods async.
|
|
53
53
|
*
|
|
54
|
-
* Validation
|
|
54
|
+
* Validation policy:
|
|
55
55
|
*
|
|
56
|
-
* - create() opts →
|
|
57
|
-
* - get/set/del/has/wrap key arg type →
|
|
58
|
-
* - set value type →
|
|
59
|
-
* - per-call ttlMs override →
|
|
60
|
-
* - audit / observability emit failures →
|
|
61
|
-
* - method called after close() →
|
|
56
|
+
* - create() opts → throw at boot
|
|
57
|
+
* - get/set/del/has/wrap key arg type → throw at call site (programming bug)
|
|
58
|
+
* - set value type → tolerant (operator decides what to store)
|
|
59
|
+
* - per-call ttlMs override → throw at call site (bad ttl is silent footgun)
|
|
60
|
+
* - audit / observability emit failures → drop silent (hot-path sink)
|
|
61
|
+
* - method called after close() → throw BAD_STATE at call site
|
|
62
62
|
*
|
|
63
63
|
* Security defaults:
|
|
64
64
|
*
|
|
@@ -114,7 +114,7 @@ var DEFAULTS = Object.freeze({
|
|
|
114
114
|
auditClear: true,
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
-
// ----
|
|
117
|
+
// ---- Config-time validation helpers (throw on bad input) ----
|
|
118
118
|
|
|
119
119
|
function _isFiniteNonNegative(n) {
|
|
120
120
|
return typeof n === "number" && isFinite(n) && n >= 0;
|
|
@@ -669,7 +669,7 @@ function create(opts) {
|
|
|
669
669
|
try {
|
|
670
670
|
if (operatorObs) operatorObs.event(name, 1, labels || {});
|
|
671
671
|
else observability().event(name, 1, labels || {});
|
|
672
|
-
} catch (_e) { /*
|
|
672
|
+
} catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
|
|
673
673
|
}
|
|
674
674
|
|
|
675
675
|
function emitAudit(action, info) {
|
package/lib/constants.js
CHANGED
|
@@ -18,10 +18,10 @@ var pkg = require("../package.json");
|
|
|
18
18
|
// reader sees the unit at the call site (`C.TIME.minutes(45)` instead of
|
|
19
19
|
// adding a new FORTY_FIVE_MIN constant). All return integer milliseconds.
|
|
20
20
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
21
|
+
// Throw on non-finite or negative input — these are config-time helpers,
|
|
22
|
+
// so a typo (`C.TIME.minutes(opts.x)` where opts.x is undefined) should
|
|
23
|
+
// surface at boot instead of silently becoming `NaN` or `0` and shipping
|
|
24
|
+
// a 0ms timeout into production.
|
|
25
25
|
function _validateDuration(unit, n) {
|
|
26
26
|
if (typeof n !== "number" || !isFinite(n) || n < 0) {
|
|
27
27
|
throw new TypeError("C.TIME." + unit + ": expected non-negative finite number, got " +
|
|
@@ -42,8 +42,8 @@ var TIME = Object.freeze({
|
|
|
42
42
|
// (KiB/MiB/GiB), since every existing byte literal in the framework
|
|
43
43
|
// already uses 1024 multiplication.
|
|
44
44
|
//
|
|
45
|
-
//
|
|
46
|
-
// call site, not as a silent NaN cap that disables size limits.
|
|
45
|
+
// Throw on bad input — same rationale as TIME: bad input surfaces at
|
|
46
|
+
// the call site, not as a silent NaN cap that disables size limits.
|
|
47
47
|
function _validateBytes(unit, n) {
|
|
48
48
|
if (typeof n !== "number" || !isFinite(n) || n < 0) {
|
|
49
49
|
throw new TypeError("C.BYTES." + unit + ": expected non-negative finite number, got " +
|
package/lib/credential-hash.js
CHANGED
|
@@ -61,13 +61,13 @@
|
|
|
61
61
|
* - Transparent rehash via needsRehash() drains old algos at the
|
|
62
62
|
* pace of organic verify traffic.
|
|
63
63
|
*
|
|
64
|
-
* Validation
|
|
64
|
+
* Validation policy:
|
|
65
65
|
*
|
|
66
|
-
* - hash() opts (algo, params) →
|
|
67
|
-
* - hash() secret type / length →
|
|
68
|
-
* - verify() envelope shape unparsable →
|
|
69
|
-
* - verify() unknown algo ID →
|
|
70
|
-
* - inspect() bad envelope →
|
|
66
|
+
* - hash() opts (algo, params) → throw at call site
|
|
67
|
+
* - hash() secret type / length → throw at call site
|
|
68
|
+
* - verify() envelope shape unparsable → return false (tolerant read)
|
|
69
|
+
* - verify() unknown algo ID → return false (tolerant read)
|
|
70
|
+
* - inspect() bad envelope → return null (tolerant read)
|
|
71
71
|
*/
|
|
72
72
|
|
|
73
73
|
var crypto = require("./crypto");
|
|
@@ -79,7 +79,7 @@ var observability = lazyRequire(function () { return require("./observability");
|
|
|
79
79
|
|
|
80
80
|
function _emitEvent(name, value, labels) {
|
|
81
81
|
try { observability().event(name, value, labels || {}); }
|
|
82
|
-
catch (_e) { /*
|
|
82
|
+
catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// Default SHAKE256 output size for newly-issued credentials. 128
|
|
@@ -135,7 +135,7 @@ var ID_TO_NAME = Object.freeze({
|
|
|
135
135
|
0x02: "argon2id",
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
// ----
|
|
138
|
+
// ---- Call-site validation (throw on bad input) ----
|
|
139
139
|
|
|
140
140
|
function _validateSecret(secret) {
|
|
141
141
|
if (typeof secret !== "string" && !Buffer.isBuffer(secret)) {
|
|
@@ -223,7 +223,7 @@ async function hash(secret, opts) {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
async function verify(secret, envelope) {
|
|
226
|
-
//
|
|
226
|
+
// Tolerant read: any malformed envelope → false. Lets operators write
|
|
227
227
|
// if (!await ch.verify(s, row.hash)) return res.status(401);
|
|
228
228
|
// without try/catch ceremony. We still reject obvious caller bugs
|
|
229
229
|
// (non-string-or-Buffer secret) loudly because that signals broken
|
package/lib/db-declare-view.js
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
* typos surface as clear errors at the migrate command, not as silent
|
|
40
40
|
* empty views or grant-to-nonexistent-role footguns.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
42
|
+
* Throw at declareView() call time on:
|
|
43
43
|
* - schema, name, source segments → safeSql.validateIdentifier
|
|
44
44
|
* - column names in redactColumns / sealedColumns / hashColumns →
|
|
45
45
|
* safeSql.validateIdentifier
|
|
@@ -303,7 +303,7 @@ function declareView(opts) {
|
|
|
303
303
|
var qSource = safeSql.quoteQualified([spec.source.schema, spec.source.name], "postgres");
|
|
304
304
|
|
|
305
305
|
async function up(xdb, ctx) {
|
|
306
|
-
//
|
|
306
|
+
// Boundary throw: confirm we're on Postgres before any DDL leaves the process.
|
|
307
307
|
if (ctx && ctx.externalDb && ctx.backendName) {
|
|
308
308
|
_ensureBackendIsPostgres(ctx.externalDb, ctx.backendName);
|
|
309
309
|
}
|
|
@@ -208,7 +208,7 @@ function _emit(audit, action, outcome, info, reason) {
|
|
|
208
208
|
metadata: info || {},
|
|
209
209
|
reason: reason || null,
|
|
210
210
|
});
|
|
211
|
-
} catch (_e) { /*
|
|
211
|
+
} catch (_e) { /* drop-silent — audit emit failure must not crash the migration */ }
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
// ---- Backend resolution ----
|
package/lib/http-client.js
CHANGED
|
@@ -480,8 +480,8 @@ function request(opts) {
|
|
|
480
480
|
return Promise.reject(_makeError(opts && opts.errorClass, "BAD_ARG", "url is required", true));
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
-
// Validate before/after shapes early —
|
|
484
|
-
// passed something un-callable so the bug surfaces
|
|
483
|
+
// Validate before/after shapes early — throw at call site if the
|
|
484
|
+
// operator passed something un-callable so the bug surfaces here
|
|
485
485
|
// rather than inside the request loop.
|
|
486
486
|
if (opts.before !== undefined) {
|
|
487
487
|
if (!Array.isArray(opts.before) || !opts.before.every(function (f) { return typeof f === "function"; })) {
|
|
@@ -896,7 +896,7 @@ function _requestH1(transport, u, opts) {
|
|
|
896
896
|
var bodyBuf = Buffer.isBuffer(opts.body) ? opts.body : Buffer.from(opts.body, "utf8");
|
|
897
897
|
if (onUploadProgress) {
|
|
898
898
|
// Chunked write so progress reports land before req.end().
|
|
899
|
-
var CHUNK = 64
|
|
899
|
+
var CHUNK = C.BYTES.kib(64);
|
|
900
900
|
var off = 0;
|
|
901
901
|
while (off < bodyBuf.length) {
|
|
902
902
|
var slice = bodyBuf.slice(off, Math.min(off + CHUNK, bodyBuf.length));
|
package/lib/i18n.js
CHANGED
|
@@ -38,16 +38,16 @@
|
|
|
38
38
|
* - {var} interpolation; missing vars render as literal {var} unless
|
|
39
39
|
* `interpolation.strict: true`.
|
|
40
40
|
*
|
|
41
|
-
* Validation
|
|
41
|
+
* Validation policy:
|
|
42
42
|
*
|
|
43
|
-
* - create() opts →
|
|
44
|
-
* - bad locale tag at any boundary →
|
|
45
|
-
* - t(missingKey) →
|
|
46
|
-
* - t() with bad locale override →
|
|
47
|
-
* - plural shape missing 'other' →
|
|
48
|
-
* - interpolation missing var →
|
|
49
|
-
* - format* bad input →
|
|
50
|
-
* - middleware Accept-Language parse error →
|
|
43
|
+
* - create() opts → throw at boot
|
|
44
|
+
* - bad locale tag at any boundary → throw at call site
|
|
45
|
+
* - t(missingKey) → return key + observability event
|
|
46
|
+
* - t() with bad locale override → throw at call site (programming bug)
|
|
47
|
+
* - plural shape missing 'other' → throw at load time
|
|
48
|
+
* - interpolation missing var → render literal {var}
|
|
49
|
+
* - format* bad input → throw at call site
|
|
50
|
+
* - middleware Accept-Language parse error → fall back to defaultLocale
|
|
51
51
|
*
|
|
52
52
|
* Security stance: translation values come from operator-controlled
|
|
53
53
|
* files, not user input. {var} interpolation does NOT html-escape;
|
|
@@ -97,7 +97,7 @@ var DEFAULTS = Object.freeze({
|
|
|
97
97
|
RTL_LANGUAGES: RTL_LANGUAGES,
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
-
// ----
|
|
100
|
+
// ---- Call-site validation (throw on bad input) ----
|
|
101
101
|
|
|
102
102
|
function _isValidBcp47(tag) {
|
|
103
103
|
if (typeof tag !== "string" || tag.length === 0) return false;
|
|
@@ -476,7 +476,7 @@ function create(opts) {
|
|
|
476
476
|
try {
|
|
477
477
|
if (operatorObs) operatorObs.event(name, 1, labels || {});
|
|
478
478
|
else observability().event(name, 1, labels || {});
|
|
479
|
-
} catch (_e) { /*
|
|
479
|
+
} catch (_e) { /* drop-silent — observability sink must not crash i18n calls */ }
|
|
480
480
|
}
|
|
481
481
|
|
|
482
482
|
// Cardinal plural-rules instances per locale. `Intl.PluralRules` is
|
|
@@ -721,7 +721,7 @@ function create(opts) {
|
|
|
721
721
|
// Permit setting a non-configured locale (operators may want to
|
|
722
722
|
// experiment), but fall the chain through to the configured ones.
|
|
723
723
|
// Don't throw — i18n.locale is set/observed in many flows; making
|
|
724
|
-
//
|
|
724
|
+
// this throw would force operators into try/catch around UI setters.
|
|
725
725
|
_emitObs("i18n.miss.locale", { requested: newLocale, resolved: defaultLocale });
|
|
726
726
|
}
|
|
727
727
|
currentLocale = newLocale;
|
|
@@ -1041,8 +1041,40 @@ async function _parseJsonFromBuf(buf, opts) {
|
|
|
1041
1041
|
return parsed;
|
|
1042
1042
|
}
|
|
1043
1043
|
|
|
1044
|
+
// raw — convenience wrapper that returns a middleware which buffers
|
|
1045
|
+
// the request body as a Buffer regardless of Content-Type. Webhook
|
|
1046
|
+
// signature-verification routes use this — the HMAC is computed over
|
|
1047
|
+
// the literal body bytes, so JSON-parsing first would change them.
|
|
1048
|
+
//
|
|
1049
|
+
// router.post("/hooks/in", b.middleware.bodyParser.raw(), function (req, res) {
|
|
1050
|
+
// verifier.verify(req.headers["x-signature"], req.body); // req.body is a Buffer
|
|
1051
|
+
// });
|
|
1052
|
+
//
|
|
1053
|
+
// Accepts the same `raw`-section opts as create() (limit, contentTypes).
|
|
1054
|
+
// contentTypes default expands to `["*/*"]` so any Content-Type lands
|
|
1055
|
+
// as raw bytes.
|
|
1056
|
+
function raw(opts) {
|
|
1057
|
+
opts = opts || {};
|
|
1058
|
+
return create({
|
|
1059
|
+
json: false,
|
|
1060
|
+
urlencoded: false,
|
|
1061
|
+
text: false,
|
|
1062
|
+
multipart: false,
|
|
1063
|
+
raw: {
|
|
1064
|
+
limit: opts.limit != null ? opts.limit : DEFAULTS.raw.limit,
|
|
1065
|
+
contentTypes: opts.contentTypes || ["*/*"],
|
|
1066
|
+
},
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// Attach raw onto create so b.middleware.bodyParser.raw() works
|
|
1071
|
+
// (middleware/index.js exports the create function as the namespace
|
|
1072
|
+
// itself, so static helpers hang off it).
|
|
1073
|
+
create.raw = raw;
|
|
1074
|
+
|
|
1044
1075
|
module.exports = {
|
|
1045
1076
|
create: create,
|
|
1077
|
+
raw: raw,
|
|
1046
1078
|
BodyParserError: BodyParserError,
|
|
1047
1079
|
// Internal helpers exposed for tests + the csrf-protect refactor.
|
|
1048
1080
|
_contentType: _contentType,
|
package/lib/middleware/cors.js
CHANGED
|
@@ -191,7 +191,7 @@ function create(opts) {
|
|
|
191
191
|
}
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
//
|
|
194
|
+
// Throw at create() on bad opts.siteOrigin — must parse as http(s) URL.
|
|
195
195
|
// Accept string OR array of strings.
|
|
196
196
|
var siteOrigins = [];
|
|
197
197
|
if (opts.siteOrigin !== undefined && opts.siteOrigin !== null) {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* (e.g. req.session.data.csrfToken). Issuance + req.csrfToken
|
|
35
35
|
* exposure are the operator's responsibility in this mode.
|
|
36
36
|
*
|
|
37
|
-
* If neither is supplied, the middleware throws at create() —
|
|
37
|
+
* If neither is supplied, the middleware throws at create() —
|
|
38
38
|
* config-time validation, no silent passthrough.
|
|
39
39
|
*
|
|
40
40
|
* Submitted-token sources tried in order on state-changing requests:
|
|
@@ -177,7 +177,7 @@ function create(opts) {
|
|
|
177
177
|
? opts.trustProxy : false;
|
|
178
178
|
var _isHttps = _isHttpsFor(trustProxy);
|
|
179
179
|
|
|
180
|
-
//
|
|
180
|
+
// Throw at create() — exactly one issuance source allowed.
|
|
181
181
|
var hasCookie = opts.cookie != null && opts.cookie !== false;
|
|
182
182
|
var hasLookup = typeof opts.tokenLookup === "function";
|
|
183
183
|
if (hasCookie && hasLookup) {
|
|
@@ -256,8 +256,8 @@ function create(opts) {
|
|
|
256
256
|
var bodyOnLimit = opts.bodyOnLimit !== undefined ? opts.bodyOnLimit : "Too Many Requests";
|
|
257
257
|
var emitHeaders = opts.header !== false;
|
|
258
258
|
var skipPaths = opts.skipPaths || [];
|
|
259
|
-
//
|
|
260
|
-
// else would crash _shouldSkip with TypeError on the first request.
|
|
259
|
+
// Throw at create(): each entry must be a string prefix or a RegExp.
|
|
260
|
+
// Anything else would crash _shouldSkip with TypeError on the first request.
|
|
261
261
|
for (var sp = 0; sp < skipPaths.length; sp++) {
|
|
262
262
|
if (typeof skipPaths[sp] !== "string" && !(skipPaths[sp] instanceof RegExp)) {
|
|
263
263
|
throw new Error("middleware.rateLimit: skipPaths[" + sp +
|
package/lib/notify.js
CHANGED
|
@@ -74,7 +74,7 @@ var DEFAULTS = Object.freeze({
|
|
|
74
74
|
// we get the framework's policy without forking the constants.
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
// ----
|
|
77
|
+
// ---- Call-site validation (throw on bad input) ----
|
|
78
78
|
|
|
79
79
|
function _isFiniteNonNegative(n) {
|
|
80
80
|
return typeof n === "number" && isFinite(n) && n >= 0;
|
|
@@ -360,7 +360,7 @@ function create(opts) {
|
|
|
360
360
|
|
|
361
361
|
function _emitObs(name, labels) {
|
|
362
362
|
try { observability().event(name, 1, labels || {}); }
|
|
363
|
-
catch (_e) { /*
|
|
363
|
+
catch (_e) { /* drop-silent — observability sink must not crash send() */ }
|
|
364
364
|
}
|
|
365
365
|
|
|
366
366
|
function _emitAudit(action, info) {
|
|
@@ -44,13 +44,14 @@
|
|
|
44
44
|
* maxAgeSeconds: 3600,
|
|
45
45
|
* }]);
|
|
46
46
|
*
|
|
47
|
-
* Validation
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
47
|
+
* Validation rejects every bad input shape at the call site rather
|
|
48
|
+
* than producing a server-side 400. Errors surface as ObjectStoreError
|
|
49
|
+
* with codes (BUCKET_INVALID_NAME, INVALID_LIFECYCLE, INVALID_CORS_RULE,
|
|
50
|
+
* BUCKET_ALREADY_OWNED, BUCKET_NOT_EMPTY, etc.).
|
|
51
51
|
*/
|
|
52
52
|
var { URL } = require("url");
|
|
53
53
|
var nodeCrypto = require("crypto");
|
|
54
|
+
var C = require("../constants");
|
|
54
55
|
var sigv4 = require("./sigv4");
|
|
55
56
|
var safeXml = require("../parsers/safe-xml");
|
|
56
57
|
var safeUrl = require("../safe-url");
|
|
@@ -261,7 +262,7 @@ function _buildCorsXml(rules) {
|
|
|
261
262
|
body += "</CORSRule>";
|
|
262
263
|
}
|
|
263
264
|
body += "</CORSConfiguration>";
|
|
264
|
-
if (Buffer.byteLength(body, "utf8") > 64
|
|
265
|
+
if (Buffer.byteLength(body, "utf8") > C.BYTES.kib(64)) {
|
|
265
266
|
throw _err("INVALID_CORS_RULE",
|
|
266
267
|
"CORS configuration exceeds 64 KB (S3 spec)", true);
|
|
267
268
|
}
|
|
@@ -214,17 +214,17 @@ function _request(method, url, headers, body, opts) {
|
|
|
214
214
|
// CompleteMultipartUpload with EntityTooSmall. The framework refuses
|
|
215
215
|
// configurations below this floor at create() time so operators don't
|
|
216
216
|
// see surprising failures only on large uploads.
|
|
217
|
-
var MIN_PART_SIZE_BYTES = 5
|
|
217
|
+
var MIN_PART_SIZE_BYTES = C.BYTES.mib(5);
|
|
218
218
|
// S3 spec ceiling on part count. CompleteMultipartUpload rejects
|
|
219
219
|
// uploads with more than 10000 parts.
|
|
220
220
|
var MAX_PARTS = 10000;
|
|
221
221
|
// Auto-multipart trigger: buffered bodies under this stay single-PUT.
|
|
222
222
|
// Streams always go multipart since size isn't known up-front.
|
|
223
|
-
var DEFAULT_MULTIPART_THRESHOLD_BYTES = 64
|
|
223
|
+
var DEFAULT_MULTIPART_THRESHOLD_BYTES = C.BYTES.mib(64);
|
|
224
224
|
// Conservative default part size — large enough to keep round-trip
|
|
225
225
|
// overhead small relative to payload, small enough to fit comfortably
|
|
226
226
|
// in a 4-way-concurrent upload's memory footprint.
|
|
227
|
-
var DEFAULT_PART_SIZE_BYTES = 16
|
|
227
|
+
var DEFAULT_PART_SIZE_BYTES = C.BYTES.mib(16);
|
|
228
228
|
var DEFAULT_PART_CONCURRENCY = 4;
|
|
229
229
|
|
|
230
230
|
// ---- SSE option handling ----
|
package/lib/observability.js
CHANGED
|
@@ -59,18 +59,43 @@ var lazyRequire = require("./lazy-require");
|
|
|
59
59
|
var tracing = lazyRequire(function () { return require("./tracing"); });
|
|
60
60
|
var metrics = lazyRequire(function () { return require("./metrics"); });
|
|
61
61
|
|
|
62
|
+
// Operator-installed tap handler — wired via setTap(). When non-null,
|
|
63
|
+
// every observability event/tap dispatch routes here in addition to
|
|
64
|
+
// the framework's metrics module. Used by b.otelExport.create() so an
|
|
65
|
+
// OTLP/HTTP exporter receives the same hot-path counters the framework
|
|
66
|
+
// emits internally.
|
|
67
|
+
var _externalTap = null;
|
|
68
|
+
|
|
62
69
|
function _safeMetricsTap(name, value, labels) {
|
|
63
70
|
try { metrics().tap(name, value, labels); }
|
|
64
71
|
catch (_e) { /* boot-order tolerance — metrics may not be loaded */ }
|
|
72
|
+
if (_externalTap !== null) {
|
|
73
|
+
try { _externalTap(name, value, labels); }
|
|
74
|
+
catch (_e) { /* operator-installed handler — drop-silent on its throws */ }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// setTap — install an external tap handler. Operators wire this from
|
|
79
|
+
// `b.otelExport.create({...}).tapHandler` so every framework counter
|
|
80
|
+
// also lands in the operator's metrics pipeline.
|
|
81
|
+
//
|
|
82
|
+
// The handler signature mirrors metrics.tap: (name, value, labels).
|
|
83
|
+
// Pass null to remove the previously-installed handler.
|
|
84
|
+
function setTap(handler) {
|
|
85
|
+
if (handler !== null && typeof handler !== "function") {
|
|
86
|
+
throw new TypeError("observability.setTap: handler must be a function or null, got " +
|
|
87
|
+
typeof handler);
|
|
88
|
+
}
|
|
89
|
+
_externalTap = handler;
|
|
65
90
|
}
|
|
66
91
|
|
|
67
92
|
function tap(name, attrs, fn) {
|
|
68
93
|
if (typeof attrs === "function") { fn = attrs; attrs = null; }
|
|
69
|
-
//
|
|
70
|
-
// in the name (e.g. variable holding undefined) silently corrupts
|
|
71
|
-
// the span tree AND the metrics counter route, with no obvious
|
|
72
|
-
// until somebody opens a dashboard. Throw at first call so
|
|
73
|
-
// operator catches it.
|
|
94
|
+
// Throw on bad input: tap is called from many call sites and a typo
|
|
95
|
+
// in the name (e.g. variable holding undefined) silently corrupts
|
|
96
|
+
// both the span tree AND the metrics counter route, with no obvious
|
|
97
|
+
// symptom until somebody opens a dashboard. Throw at first call so
|
|
98
|
+
// the operator catches it.
|
|
74
99
|
if (typeof name !== "string" || name.length === 0) {
|
|
75
100
|
throw new TypeError("observability.tap: name must be a non-empty string, got " +
|
|
76
101
|
(typeof name) + " " + JSON.stringify(name));
|
|
@@ -97,18 +122,19 @@ function tap(name, attrs, fn) {
|
|
|
97
122
|
});
|
|
98
123
|
}
|
|
99
124
|
|
|
100
|
-
//
|
|
101
|
-
// hot paths where throwing would crash the request
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
125
|
+
// Drop-silent on bad input by design: event is the fire-and-forget
|
|
126
|
+
// shape called from hot paths where throwing would crash the request
|
|
127
|
+
// that triggered it. Operators with a misnamed event see the missing
|
|
128
|
+
// counter, not a 500. metrics.tap performs its own label-name regex
|
|
129
|
+
// validation; an invalid call surfaces in the metrics module log, not
|
|
130
|
+
// via a thrown exception.
|
|
106
131
|
function event(name, value, labels) {
|
|
107
132
|
if (typeof name !== "string" || name.length === 0) return;
|
|
108
133
|
_safeMetricsTap(name, value, labels);
|
|
109
134
|
}
|
|
110
135
|
|
|
111
136
|
module.exports = {
|
|
112
|
-
tap:
|
|
113
|
-
event:
|
|
137
|
+
tap: tap,
|
|
138
|
+
event: event,
|
|
139
|
+
setTap: setTap,
|
|
114
140
|
};
|
package/lib/otel-export.js
CHANGED
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
*
|
|
28
28
|
* Wiring with `b.observability`:
|
|
29
29
|
*
|
|
30
|
-
* // Option A:
|
|
31
|
-
* b.observability.
|
|
30
|
+
* // Option A: install as an external tap on b.observability
|
|
31
|
+
* b.observability.setTap(otel.tapHandler);
|
|
32
32
|
*
|
|
33
33
|
* // Option B: alongside b.metrics — operators write their own
|
|
34
34
|
* // multi-tap fan-out (or pick one or the other for v1).
|
package/lib/pagination.js
CHANGED
|
@@ -215,10 +215,10 @@ async function cursor(query, opts) {
|
|
|
215
215
|
}
|
|
216
216
|
var limit = _resolveLimit(opts);
|
|
217
217
|
var orderBy = typeof opts.orderBy === "string" && opts.orderBy.length > 0 ? opts.orderBy : "_id";
|
|
218
|
-
//
|
|
219
|
-
// for the keyset where-clause. Restrict to
|
|
220
|
-
// so a careless caller piping
|
|
221
|
-
// create an SQL-injection vector.
|
|
218
|
+
// Throw at call site on bad orderBy — the value is interpolated into
|
|
219
|
+
// a raw SQL fragment for the keyset where-clause. Restrict to
|
|
220
|
+
// identifier-safe characters so a careless caller piping
|
|
221
|
+
// `req.query.orderBy` through doesn't create an SQL-injection vector.
|
|
222
222
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(orderBy)) {
|
|
223
223
|
throw new PaginationError("pagination/bad-orderby",
|
|
224
224
|
"cursor: orderBy must match /^[A-Za-z_][A-Za-z0-9_]*$/ (identifier-safe), got " +
|
|
@@ -342,8 +342,8 @@ async function offset(query, opts) {
|
|
|
342
342
|
var page = parseInt(opts.page, 10);
|
|
343
343
|
if (isNaN(page) || page < 1) page = 1;
|
|
344
344
|
var orderBy = typeof opts.orderBy === "string" && opts.orderBy.length > 0 ? opts.orderBy : "_id";
|
|
345
|
-
// Same
|
|
346
|
-
// db Query;
|
|
345
|
+
// Same identifier-only check on offset() as cursor() — orderBy passes
|
|
346
|
+
// through to the db Query; throw at call site to prevent SQL injection.
|
|
347
347
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(orderBy)) {
|
|
348
348
|
throw new PaginationError("pagination/bad-orderby",
|
|
349
349
|
"offset: orderBy must match /^[A-Za-z_][A-Za-z0-9_]*$/ (identifier-safe), got " +
|
package/lib/permissions.js
CHANGED
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
* "users:*:read" matches "users:foo:read" (per-segment *)
|
|
32
32
|
* "users:read" matches "users:read" only — no implicit sub-resource grant
|
|
33
33
|
*
|
|
34
|
-
* Validation
|
|
34
|
+
* Validation policy:
|
|
35
35
|
*
|
|
36
|
-
* - create() role table / scope formats →
|
|
37
|
-
* - require(scope) registration arg →
|
|
38
|
-
* - check(actor, scope) bad actor →
|
|
36
|
+
* - create() role table / scope formats → throw at app init
|
|
37
|
+
* - require(scope) registration arg → throw at route declaration
|
|
38
|
+
* - check(actor, scope) bad actor → return false (tolerant read)
|
|
39
39
|
* - resolver returns null in middleware → 401 (missingActorStatus)
|
|
40
40
|
* - actor lacks scope in middleware → 403 (denyStatus)
|
|
41
|
-
* - audit/observability emit failures →
|
|
41
|
+
* - audit/observability emit failures → drop silent (hot-path sink)
|
|
42
42
|
*
|
|
43
43
|
* Audit defaults follow the framework's security-defaults stance
|
|
44
44
|
* default: `auditFailures: true`
|
|
@@ -57,7 +57,7 @@ var observability = lazyRequire(function () { return require("./observability");
|
|
|
57
57
|
|
|
58
58
|
function _emitEvent(name, value, labels) {
|
|
59
59
|
try { observability().event(name, value, labels || {}); }
|
|
60
|
-
catch (_e) { /*
|
|
60
|
+
catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
// Lowercase tokens, digits, dash, underscore, and `*` allowed per
|
|
@@ -331,7 +331,7 @@ function create(opts) {
|
|
|
331
331
|
}
|
|
332
332
|
|
|
333
333
|
// Middleware factory. `mode` is "single" | "all" | "any"; `requested`
|
|
334
|
-
// is the scope or scope list.
|
|
334
|
+
// is the scope or scope list. Throw at registration time on bad shape.
|
|
335
335
|
function _middleware(mode, requested) {
|
|
336
336
|
if (mode === "single") {
|
|
337
337
|
_validateScopePattern(requested, "permissions.require");
|
|
@@ -51,8 +51,8 @@ var { defineClass } = require("./framework-error");
|
|
|
51
51
|
// supply their own get this generic shape with the same code namespace.
|
|
52
52
|
var ProtocolDispatcherError = defineClass("ProtocolDispatcherError", { withStatusCode: true });
|
|
53
53
|
|
|
54
|
-
//
|
|
55
|
-
//
|
|
54
|
+
// Throw at create-time on bad input so operators catch typos at app
|
|
55
|
+
// boot, not first request.
|
|
56
56
|
function _validateConfig(opts) {
|
|
57
57
|
if (!opts || typeof opts !== "object") {
|
|
58
58
|
throw new Error("protocolDispatcher.create: opts is required");
|
package/lib/queue.js
CHANGED
|
@@ -505,7 +505,7 @@ function _resetForTest() {
|
|
|
505
505
|
// ],
|
|
506
506
|
// });
|
|
507
507
|
//
|
|
508
|
-
// Cycle detection runs at registration (
|
|
508
|
+
// Cycle detection runs at registration (throws at call site). Each child enters
|
|
509
509
|
// the queue with availableAt = MAX_SAFE_INTEGER until parent completion
|
|
510
510
|
// bumps it. Returns { flowId, jobs: [{ name, jobId }, ...] }.
|
|
511
511
|
function enqueueFlow(spec) {
|
package/lib/request-helpers.js
CHANGED
|
@@ -175,9 +175,9 @@ function requestProtocol(req, opts) {
|
|
|
175
175
|
// parseListHeader(undefined) → []
|
|
176
176
|
// parseListHeader("") → []
|
|
177
177
|
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
178
|
+
// Tolerant read: non-string input returns [] — these are read from
|
|
179
|
+
// request headers that the network might omit. Callers needing stricter
|
|
180
|
+
// checks layer their own validation on the result.
|
|
181
181
|
function parseListHeader(value, opts) {
|
|
182
182
|
if (value == null) return [];
|
|
183
183
|
opts = opts || {};
|
package/lib/retry.js
CHANGED
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
* compose freely with `safeAsync.withTimeout`, AbortSignals, and any
|
|
19
19
|
* caller-side instrumentation.
|
|
20
20
|
*
|
|
21
|
-
* Validation
|
|
21
|
+
* Validation policy:
|
|
22
22
|
*
|
|
23
|
-
* - withRetry opts at first call →
|
|
24
|
-
* - CircuitBreaker constructor opts →
|
|
25
|
-
* - backoffDelay(attempt) attempt argument →
|
|
26
|
-
* - isRetryable(err) defensive read →
|
|
27
|
-
* - onRetry callback throw →
|
|
28
|
-
* - breaker internal _onSuccess/_onFailure →
|
|
23
|
+
* - withRetry opts at first call → throw at call site
|
|
24
|
+
* - CircuitBreaker constructor opts → throw at call site
|
|
25
|
+
* - backoffDelay(attempt) attempt argument → throw at call site
|
|
26
|
+
* - isRetryable(err) defensive read → tolerant (return defaults)
|
|
27
|
+
* - onRetry callback throw → drop silent (hot-path sink)
|
|
28
|
+
* - breaker internal _onSuccess/_onFailure → drop silent (hot-path sink)
|
|
29
29
|
*
|
|
30
30
|
* HTTP-client auto-retry is intentionally NOT provided here. Callers
|
|
31
31
|
* wrap their own outbound calls in `b.retry.withRetry(...)` to keep
|
|
@@ -40,13 +40,13 @@ var lazyRequire = require("./lazy-require");
|
|
|
40
40
|
// are fully loaded.
|
|
41
41
|
var safeAsync = lazyRequire(function () { return require("./safe-async"); });
|
|
42
42
|
// observability is also lazy-required because the metrics + tracing
|
|
43
|
-
// registry boots after this file loads. event() is
|
|
44
|
-
// throw inside the metrics sink is swallowed by observability itself.
|
|
43
|
+
// registry boots after this file loads. event() is fire-and-forget —
|
|
44
|
+
// any throw inside the metrics sink is swallowed by observability itself.
|
|
45
45
|
var observability = lazyRequire(function () { return require("./observability"); });
|
|
46
46
|
|
|
47
47
|
function _emitEvent(name, value, labels) {
|
|
48
48
|
try { observability().event(name, value, labels || {}); }
|
|
49
|
-
catch (_e) { /*
|
|
49
|
+
catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// ---- Defaults ----
|
|
@@ -80,7 +80,7 @@ var DEFAULT_BREAKER = Object.freeze({
|
|
|
80
80
|
successThreshold: 2, // consecutive HALF probes that close it
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
// ----
|
|
83
|
+
// ---- Call-site validation helpers (throw on bad input) ----
|
|
84
84
|
|
|
85
85
|
function _isPositiveInt(n) {
|
|
86
86
|
return typeof n === "number" && isFinite(n) && n >= 1 && Math.floor(n) === n;
|
|
@@ -147,7 +147,7 @@ function _validateBreakerOpts(name, opts) {
|
|
|
147
147
|
|
|
148
148
|
// ---- Public surface ----
|
|
149
149
|
|
|
150
|
-
//
|
|
150
|
+
// Tolerant read of err shape; missing fields → false.
|
|
151
151
|
function isRetryable(err) {
|
|
152
152
|
if (!err) return false;
|
|
153
153
|
if (err.isObjectStoreError && err.permanent) return false;
|
|
@@ -162,11 +162,12 @@ function isRetryable(err) {
|
|
|
162
162
|
return false; // default: not retryable (avoid masking bugs)
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
//
|
|
166
|
-
// have non-neg-finite baseDelayMs/maxDelayMs and finite jitterFactor
|
|
167
|
-
// We don't full-validate opts here every call (hot path) —
|
|
168
|
-
// frozen, so the only way a bad opts reaches here is via
|
|
169
|
-
// already validated, OR a caller using backoffDelay
|
|
165
|
+
// Throw on bad input: attempt must be a positive int; opts (when supplied)
|
|
166
|
+
// must have non-neg-finite baseDelayMs/maxDelayMs and finite jitterFactor
|
|
167
|
+
// in [0,1]. We don't full-validate opts here every call (hot path) —
|
|
168
|
+
// defaults are frozen, so the only way a bad opts reaches here is via
|
|
169
|
+
// withRetry which already validated, OR a caller using backoffDelay
|
|
170
|
+
// directly. For that
|
|
170
171
|
// direct case we still validate the attempt arg loudly.
|
|
171
172
|
function backoffDelay(attempt, opts) {
|
|
172
173
|
if (!_isPositiveInt(attempt)) {
|
|
@@ -212,8 +213,8 @@ async function withRetry(fn, opts) {
|
|
|
212
213
|
var delay = backoffDelay(attempt, opts);
|
|
213
214
|
_emitEvent("retry.attempt", 1, { attempt: attempt });
|
|
214
215
|
if (typeof opts.onRetry === "function") {
|
|
215
|
-
//
|
|
216
|
-
//
|
|
216
|
+
// Hot-path observability sink — drops silent on observer throw
|
|
217
|
+
// so a thrown observer can't crash the retry loop.
|
|
217
218
|
try { opts.onRetry({ attempt: attempt, delay: delay, error: err }); } catch (_e) {}
|
|
218
219
|
}
|
|
219
220
|
// Honor opts.signal during the backoff sleep — a caller who aborts
|
package/lib/seeders.js
CHANGED
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
* truncating the seeded tables themselves; "unseed" isn't framework-
|
|
37
37
|
* knowable.
|
|
38
38
|
*
|
|
39
|
-
* Validation
|
|
39
|
+
* Validation policy:
|
|
40
40
|
*
|
|
41
|
-
* - create() opts →
|
|
42
|
-
* - run/status `env` arg →
|
|
43
|
-
* - seed file shape (missing run, etc) →
|
|
44
|
-
* - dependsOn cycle →
|
|
45
|
-
* - dependsOn missing →
|
|
46
|
-
* - audit emit failures →
|
|
41
|
+
* - create() opts → throw at boot
|
|
42
|
+
* - run/status `env` arg → throw at call site (explicit)
|
|
43
|
+
* - seed file shape (missing run, etc) → throw at load
|
|
44
|
+
* - dependsOn cycle → throw at load
|
|
45
|
+
* - dependsOn missing → throw at run
|
|
46
|
+
* - audit emit failures → drop silent (hot-path sink)
|
|
47
47
|
*
|
|
48
48
|
* Security defaults:
|
|
49
49
|
*
|
|
@@ -103,7 +103,7 @@ function _runSql(db, sql) {
|
|
|
103
103
|
throw _err("BAD_DB", "seeders: db handle exposes no DDL runner (exec / runSql)");
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
// ----
|
|
106
|
+
// ---- Call-site validation helpers (throw on bad input) ----
|
|
107
107
|
|
|
108
108
|
function _validateEnv(name, value) {
|
|
109
109
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -399,7 +399,7 @@ function create(opts) {
|
|
|
399
399
|
|
|
400
400
|
function _emitObs(name, labels) {
|
|
401
401
|
try { observability().event(name, 1, labels || {}); }
|
|
402
|
-
catch (_e) { /*
|
|
402
|
+
catch (_e) { /* drop-silent — observability sink must not crash seeders */ }
|
|
403
403
|
}
|
|
404
404
|
|
|
405
405
|
function _emitAudit(action, info) {
|
package/lib/slug.js
CHANGED
|
@@ -20,12 +20,12 @@
|
|
|
20
20
|
* symbols, and separators — Cyrillic, Greek, CJK, and other scripts pass
|
|
21
21
|
* through. Operators with non-Latin user content opt into preserveUnicode.
|
|
22
22
|
*
|
|
23
|
-
* Validation
|
|
23
|
+
* Validation policy:
|
|
24
24
|
*
|
|
25
|
-
* - Opts at first call (every public fn) →
|
|
26
|
-
* - title not a string →
|
|
27
|
-
* - title normalizes to empty →
|
|
28
|
-
* - unique() exhausts maxAttempts →
|
|
25
|
+
* - Opts at first call (every public fn) → throw at call site
|
|
26
|
+
* - title not a string → throw at call site
|
|
27
|
+
* - title normalizes to empty → return opts.fallback (tolerant)
|
|
28
|
+
* - unique() exhausts maxAttempts → throw SlugError at call site
|
|
29
29
|
*
|
|
30
30
|
* Out of scope (v1):
|
|
31
31
|
* - Word-by-word transliteration tables (Russian → English, Chinese →
|
|
@@ -61,7 +61,7 @@ var _RESERVED = Object.freeze([
|
|
|
61
61
|
"new", "edit", "delete", "create", "update",
|
|
62
62
|
]);
|
|
63
63
|
|
|
64
|
-
// ----
|
|
64
|
+
// ---- Call-site validation helpers (throw on bad input) ----
|
|
65
65
|
|
|
66
66
|
function _isPositiveInt(n) {
|
|
67
67
|
return typeof n === "number" && isFinite(n) && n >= 1 && Math.floor(n) === n;
|
package/lib/testing.js
CHANGED
|
@@ -82,7 +82,7 @@ var DEFAULTS = Object.freeze({
|
|
|
82
82
|
runMiddlewareTimeoutMs: 5000,
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
// ----
|
|
85
|
+
// ---- Call-site validation helpers (throw on bad input) ----
|
|
86
86
|
|
|
87
87
|
function _isPositiveInt(n) {
|
|
88
88
|
return typeof n === "number" && isFinite(n) && n >= 1 && Math.floor(n) === n;
|
|
@@ -268,7 +268,7 @@ function fakeHttpClient(responder) {
|
|
|
268
268
|
|
|
269
269
|
// ---- captureAudit ----
|
|
270
270
|
//
|
|
271
|
-
// Matches b.audit.safeEmit
|
|
271
|
+
// Matches b.audit.safeEmit's drop-silent contract. captured holds
|
|
272
272
|
// every event pushed; clear() empties; byAction(name) filters.
|
|
273
273
|
|
|
274
274
|
function captureAudit() {
|
|
@@ -276,9 +276,9 @@ function captureAudit() {
|
|
|
276
276
|
return {
|
|
277
277
|
captured: captured,
|
|
278
278
|
safeEmit: function (event) {
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
279
|
+
// Drop-silent on caller-side bugs that throw inside the capture
|
|
280
|
+
// push (shouldn't happen with array.push, but matches the real
|
|
281
|
+
// safeEmit's defensive shape).
|
|
282
282
|
try { captured.push(event); }
|
|
283
283
|
catch (_e) { /* drop-silent */ }
|
|
284
284
|
},
|
package/lib/validate-opts.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* validate-opts — shared
|
|
3
|
+
* validate-opts — shared call-site helper for primitive create() functions
|
|
4
|
+
* that throws on unknown / bad-shape opts at config time.
|
|
4
5
|
*
|
|
5
6
|
* Catches silent operator typos: `cors({ allowedOrigins: [] })` (wrong
|
|
6
7
|
* key name) currently looks like config but does nothing — the primitive
|
package/lib/webhook.js
CHANGED
|
@@ -57,12 +57,12 @@
|
|
|
57
57
|
* reference impl; operators plug in Redis/SQL by passing any object
|
|
58
58
|
* with `checkAndInsert(nonce, expireAt) → bool/Promise<bool>`.
|
|
59
59
|
*
|
|
60
|
-
* Validation
|
|
60
|
+
* Validation policy:
|
|
61
61
|
*
|
|
62
|
-
* - signer/verifier creation opts →
|
|
63
|
-
* - signer.sign body type →
|
|
64
|
-
* - signer.send url shape →
|
|
65
|
-
* - verifier.verify input shape →
|
|
62
|
+
* - signer/verifier creation opts → throw at config time
|
|
63
|
+
* - signer.sign body type → throw at call site
|
|
64
|
+
* - signer.send url shape → throw at call site (via safeUrl)
|
|
65
|
+
* - verifier.verify input shape → throw WebhookError at call site
|
|
66
66
|
* - nonceStore.checkAndInsert err → propagates (fail-closed)
|
|
67
67
|
*/
|
|
68
68
|
|
|
@@ -79,7 +79,7 @@ var observability = lazyRequire(function () { return require("./observability");
|
|
|
79
79
|
|
|
80
80
|
function _emitEvent(name, value, labels) {
|
|
81
81
|
try { observability().event(name, value, labels || {}); }
|
|
82
|
-
catch (_e) { /*
|
|
82
|
+
catch (_e) { /* hot-path observability sink — drops silent on internal throws */ }
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
var _err = WebhookError.factory;
|
|
@@ -111,7 +111,7 @@ var DEFAULTS = Object.freeze({
|
|
|
111
111
|
auditSuccess: true,
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
// ----
|
|
114
|
+
// ---- Call-site validation helpers (throw on bad input) ----
|
|
115
115
|
|
|
116
116
|
function _isPositiveInt(n) {
|
|
117
117
|
return typeof n === "number" && isFinite(n) && n >= 1 && Math.floor(n) === n;
|