@blamejs/core 0.17.19 → 0.17.21
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 +4 -0
- package/NOTICE +1 -1
- package/lib/arg-parser.js +23 -3
- package/lib/cli.js +64 -17
- package/lib/dev.js +16 -1
- package/lib/external-db.js +12 -0
- package/lib/forms.js +113 -27
- package/lib/vendor/MANIFEST.json +11 -11
- package/lib/vendor/public-suffix-list.dat +7 -3
- package/lib/vendor/public-suffix-list.data.js +2484 -2482
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.17.x
|
|
10
10
|
|
|
11
|
+
- v0.17.21 (2026-07-25) — **`blamejs erase` treats only `--confirm true` as confirmation, so `--confirm false` no longer triggers the irreversible erase.** Three fixes to the blamejs command line. The `erase` subcommand (a cryptographic single-row erase) gated its irreversible action on a bare-truthiness check, so `--confirm false` -- a string -- counted as confirmation and the erase proceeded; the gate now accepts only `true` / "true", matching `audit purge`, and refuses anything else. A stray `-v` / `--version` token alongside a subcommand no longer short-circuits to the version print: `blamejs migrate up --db x -v` previously returned 0 after printing the version without running the migration, handing automation a false pass; the version flag is now honored only when it is the whole invocation. And `blamejs dev` now survives a crash of the watched child -- it stays up to hot-restart on the next file change instead of draining the event loop and exiting 0 -- and awaits the child's full termination on SIGINT/SIGTERM so a shutdown cannot orphan it. **Fixed:** *blamejs erase refuses --confirm false instead of performing the irreversible erase* — The erase subcommand confirmed its irreversible action with a bare-truthiness check, so the string `--confirm false` was treated as confirmation and the erase ran. Confirmation is now satisfied only by `--confirm true` (or the bare `--confirm`), matching the check `audit purge` already used; any other value -- including `false` -- is refused with a non-zero exit and no erase. A shared confirmation helper backs every destructive subcommand so the acknowledgement contract cannot drift between them. · *A stray -v / --version no longer turns a subcommand into a silent no-op* — The top-level version flag was evaluated before subcommand dispatch and returned 0 whenever `-v` / `--version` appeared anywhere in the arguments, so a consequential command such as `migrate up`, `seed run`, or `erase` alongside a stray `-v` printed the version and exited 0 without doing anything -- a false success for any script checking the exit code. The version flag is now honored only when the invocation carries no subcommand; otherwise the subcommand runs (or fails) as written. · *blamejs dev survives a watched-child crash and shuts the child down cleanly* — The dev supervisor held the event loop open with an unref'd timer, so when the watched child process crashed the supervisor drained the loop and exited 0 -- defeating crash-resilience (it should stay up and hot-restart on the next file change) and reporting success even though the app had crashed. It now holds a referenced heartbeat so a child crash leaves the supervisor running, and on SIGINT/SIGTERM it awaits the child's full termination (graceful signal, then escalation) before exiting, so a shutdown can no longer race ahead and orphan the child.
|
|
12
|
+
|
|
13
|
+
- v0.17.20 (2026-07-24) — **`b.forms.validate` enforces required checkboxes server-side and rejects malformed field bounds, closing two validation gaps.** b.forms.validate previously let an unchecked required checkbox pass: the renderer emits the HTML `required` attribute for it, but the server-side validator skipped the check, so a client that omitted the box -- or any non-browser caller -- bypassed a constraint the form advertised. It now rejects an unchecked required checkbox, keeping backend validation in lock-step with what the form displays. Separately, a field whose numeric bound (min/max/minlength/maxlength) was not a finite number went silently unenforced -- a NaN comparison is always false -- so the bound became a no-op; validate now throws on such a spec at the entry point, the same way it already rejects a non-precompiled regex pattern. Two smaller fixes: a field rendered without an explicit type now emits type="text" (matching the widget it dispatches to) rather than an empty type attribute, and b.externalDb reports EXPLAIN statements as row-returning so a query plan's rows are not dropped by the local-execution row/no-row chooser. **Changed:** *b.forms.validate option reference corrected* — The validate() reference described length bounds as minLength/maxLength, but the option keys are the HTML-attribute spellings minlength/maxlength (the same keys the renderer reads); the reference and its example are corrected, and the example now uses a numeric min bound on its number field. · *Vendored Public Suffix List refreshed to the current snapshot* — The bundled Public Suffix List that backs domain classification (registrable-domain and public-suffix checks in URL, cookie, and SSRF handling) is updated to the latest upstream publicsuffix.org snapshot, so newly delegated public suffixes and private-domain entries are recognized. **Fixed:** *Required checkboxes are enforced server-side in b.forms.validate* — A checkbox marked required is rendered with the HTML `required` attribute, but validate() treated an unchecked box (which coerces to false) as satisfying the field, so an unchecked required checkbox passed. Any caller that skipped the box -- a scripted client, or a browser with the constraint stripped -- bypassed a documented requirement. validate() now returns an error for an unchecked required checkbox (honouring the field's errorMessages.required), so the backend enforces the same constraint the frontend shows. A form that relied on the previous pass-through for an unchecked required checkbox will now surface a validation error, which is the intended behaviour. · *A malformed numeric field bound is rejected instead of silently ignored* — When a field's min, max, minlength, or maxlength was not a finite number (a non-numeric string, NaN, or Infinity), its comparison could never fire -- a NaN comparison is always false -- so the bound was quietly unenforced while appearing to constrain the field. validate() now throws at the entry point when a defined bound is non-finite, matching how it already requires pattern to be a pre-compiled RegExp. Numeric-string bounds (e.g. min: "1") remain accepted. · *A typeless field renders type="text" instead of an empty type attribute* — b.forms.render dispatches a field with no explicit type to a text input, but the emitted markup carried type="" rather than type="text". The rendered type attribute now matches the widget dispatched, so the markup is self-consistent. · *b.externalDb reports EXPLAIN statements as row-returning* — statementReturnsRows classified a plain EXPLAIN (and an EXPLAIN ANALYZE wrapping a write) as producing no row set, so the local-execution path could route it to a no-row call and drop the query plan. EXPLAIN always returns plan rows to the caller; it is now reported as row-returning whenever its prefix resolves, while an unparseable EXPLAIN prefix stays fail-closed. This is distinct from the cross-border residency read/write classification, which is unchanged.
|
|
14
|
+
|
|
11
15
|
- v0.17.19 (2026-07-24) — **The error and adversarial paths of the sanctions-screening, JSON Schema, and HTTP client primitives are now under test.** This release adds no behaviour change. The fail-closed error paths, boundary conditions, and adversarial-input handling of b.complianceSanctions, b.jsonSchema, and b.httpClient -- previously exercised only on their happy paths -- are now asserted, verifying that each rejects malformed input, unresolvable references, and edge cases the documented contract already promised. No defects were found; the primitives behaved as specified. Genuinely-unreachable defensive fallbacks are documented rather than contorted into coverage. **Changed:** *Verified error-path behaviour for sanctions screening, JSON Schema, and the HTTP client* — The sanctions-screening list parsers (OFAC SDN/alias, EU CSL, UN 1267), the fuzzy/exact match strategy toggle, and the entry normalizer; the JSON Schema $ref/$dynamicRef resolution, JSON-pointer traversal, format assertions, and unevaluated-properties/items handling; and the HTTP client's error, redirect, and stream branches now have explicit tests for their failure and boundary behaviour. Behaviour is unchanged -- these assert guarantees the primitives already met -- so no migration is needed; the value is regression protection for the fail-closed paths of security-relevant primitives.
|
|
12
16
|
|
|
13
17
|
- v0.17.18 (2026-07-24) — **`b.i18n` validates every inline translation tree at boot, closing a path where t() could return undefined.** b.i18n.create validated the translation trees only for the locales listed in the configured locales array. A locale present in the inline translations map but absent from locales is still reachable through an explicit t(key, vars, { locale }) override, and its tree escaped validation: a plural entry there missing the mandatory CLDR 'other' category made t() with a count return undefined instead of the key -- a contract violation that could render the literal string 'undefined' into server-rendered HTML or crash a downstream length/escape. Every inline translation tree is now validated at create, so a malformed one fails closed at boot rather than at the first request that reaches it. **Fixed:** *Every inline translation tree is validated at create, not only the configured locales* — b.i18n.create walked only opts.locales when validating translation trees up front, so a locale that appears in opts.translations but not in opts.locales was never checked even though it is reachable via an explicit locale override on t()/has(). A plural entry in such a tree that omits the mandatory 'other' category caused t(key, { count }) to select 'other', find nothing, and return undefined -- violating the documented contract that t() returns a string (the key on a miss) and never a non-string. create now validates every key of opts.translations, so a malformed tree is rejected at boot regardless of whether its locale is in opts.locales.
|
package/NOTICE
CHANGED
|
@@ -90,7 +90,7 @@ Used for: Top-10000 most-common (breach-derived) passwords. Loaded by
|
|
|
90
90
|
baseline.
|
|
91
91
|
--------------------------------------------------------------------------------
|
|
92
92
|
Component: publicsuffix-list (Mozilla Public Suffix List)
|
|
93
|
-
Version: master snapshot (bundled 2026-07-
|
|
93
|
+
Version: master snapshot (bundled 2026-07-24)
|
|
94
94
|
Source: https://publicsuffix.org/list/public_suffix_list.dat
|
|
95
95
|
License: MPL-2.0
|
|
96
96
|
Copyright: Copyright (c) Mozilla Foundation and Public Suffix List contributors
|
package/lib/arg-parser.js
CHANGED
|
@@ -625,7 +625,7 @@ function create(opts) {
|
|
|
625
625
|
|
|
626
626
|
/**
|
|
627
627
|
* @primitive b.argParser.parseRaw
|
|
628
|
-
* @signature b.argParser.parseRaw(argv)
|
|
628
|
+
* @signature b.argParser.parseRaw(argv, opts?)
|
|
629
629
|
* @since 0.8.48
|
|
630
630
|
* @status stable
|
|
631
631
|
* @related b.argParser.create
|
|
@@ -638,12 +638,21 @@ function create(opts) {
|
|
|
638
638
|
* value`, `--key=value`, and bare `--bool`. `--` terminates flag
|
|
639
639
|
* parsing.
|
|
640
640
|
*
|
|
641
|
+
* Pass `opts.booleanNames` (an array of long-flag names) to declare flags
|
|
642
|
+
* that never consume a following token as their value — a bare
|
|
643
|
+
* `--version` stays boolean instead of swallowing the next token, so
|
|
644
|
+
* `--version foo` yields `flags.version === true` with `foo` left as a
|
|
645
|
+
* positional. An inline `--version=x` still records the explicit value.
|
|
646
|
+
*
|
|
641
647
|
* A flag repeated on the command line accumulates every occurrence into
|
|
642
648
|
* an array, in order — `--watch a --watch b` yields `["a", "b"]`, not
|
|
643
649
|
* just the last value. A flag seen once stays a scalar. This keeps
|
|
644
650
|
* repeatable flags (the `dev` command's `--arg` / `--watch` / `--ignore`)
|
|
645
651
|
* from silently dropping all but the final occurrence.
|
|
646
652
|
*
|
|
653
|
+
* @opts
|
|
654
|
+
* booleanNames: string[], // long-flag names that never consume a following token as a value (default: none)
|
|
655
|
+
*
|
|
647
656
|
* @example
|
|
648
657
|
* var r = b.argParser.parseRaw(
|
|
649
658
|
* ["build", "--target=node", "-v", "--out", "dist", "--", "extra"]);
|
|
@@ -670,11 +679,21 @@ function _assignFlag(flags, name, val) {
|
|
|
670
679
|
}
|
|
671
680
|
}
|
|
672
681
|
|
|
673
|
-
function parseRaw(argv) {
|
|
682
|
+
function parseRaw(argv, opts) {
|
|
674
683
|
if (!Array.isArray(argv)) {
|
|
675
684
|
throw new ArgParserError("arg-parser/argv-not-array",
|
|
676
685
|
"argv must be an array of strings");
|
|
677
686
|
}
|
|
687
|
+
opts = opts || {};
|
|
688
|
+
// Long-flag names declared boolean never consume a following token as a
|
|
689
|
+
// value (a bare `--version` stays true instead of swallowing the next
|
|
690
|
+
// token). Inline `--flag=value` still records the explicit value.
|
|
691
|
+
var booleanNames = Object.create(null);
|
|
692
|
+
if (Array.isArray(opts.booleanNames)) {
|
|
693
|
+
for (var bn = 0; bn < opts.booleanNames.length; bn++) {
|
|
694
|
+
booleanNames[opts.booleanNames[bn]] = true;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
678
697
|
var pos = [];
|
|
679
698
|
var flags = Object.create(null);
|
|
680
699
|
for (var i = 0; i < argv.length; i++) {
|
|
@@ -694,7 +713,8 @@ function parseRaw(argv) {
|
|
|
694
713
|
if (eq !== -1) {
|
|
695
714
|
val = name.slice(eq + 1);
|
|
696
715
|
name = name.slice(0, eq);
|
|
697
|
-
} else if (i + 1 < argv.length && argv[i + 1].indexOf("--") !== 0
|
|
716
|
+
} else if (i + 1 < argv.length && argv[i + 1].indexOf("--") !== 0 &&
|
|
717
|
+
booleanNames[name] !== true) {
|
|
698
718
|
val = argv[++i];
|
|
699
719
|
} else {
|
|
700
720
|
val = true;
|
package/lib/cli.js
CHANGED
|
@@ -84,7 +84,12 @@ function _writeLine(stream, line) {
|
|
|
84
84
|
// written flag validation continues to read the same { pos, flags }
|
|
85
85
|
// shape the cli has always exposed.
|
|
86
86
|
function _parseArgs(argv) {
|
|
87
|
-
|
|
87
|
+
// `--version` is the global boolean version flag — declare it so it never
|
|
88
|
+
// swallows a following token as its value. Without this, `--version <cmd>`
|
|
89
|
+
// (or `<cmd> --version <sub>`) would consume the subcommand and a stray
|
|
90
|
+
// version flag could no-op the command it accompanies. (`-v` is a
|
|
91
|
+
// single-dash flag and is already boolean in parseRaw.)
|
|
92
|
+
return argParser.parseRaw(argv, { booleanNames: ["version"] });
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
function _resolvePath(p, cwd) {
|
|
@@ -93,6 +98,16 @@ function _resolvePath(p, cwd) {
|
|
|
93
98
|
return nodePath.resolve(cwd || process.cwd(), p);
|
|
94
99
|
}
|
|
95
100
|
|
|
101
|
+
// A destructive command's --confirm gate is satisfied ONLY by an explicit
|
|
102
|
+
// `true` / "true"; a bare-truthiness check (`!flags.confirm`) would accept
|
|
103
|
+
// `--confirm false` as confirmation, so an operator who typed `--confirm
|
|
104
|
+
// false` — meaning "do NOT proceed" — would still trigger an irreversible
|
|
105
|
+
// operation. Shared by every destructive subcommand so the acknowledgement
|
|
106
|
+
// contract can't drift between them.
|
|
107
|
+
function _isConfirmed(flags) {
|
|
108
|
+
return flags.confirm === true || flags.confirm === "true";
|
|
109
|
+
}
|
|
110
|
+
|
|
96
111
|
function _openSqlite(dbPath) {
|
|
97
112
|
// Lazy-required so the CLI doesn't crash on `blamejs version` or
|
|
98
113
|
// `blamejs help` if node:sqlite isn't usable for some reason.
|
|
@@ -424,7 +439,7 @@ async function _runDev(args, ctx) {
|
|
|
424
439
|
}
|
|
425
440
|
var killSignal = args.flags["kill-signal"];
|
|
426
441
|
|
|
427
|
-
var d = dev.create({
|
|
442
|
+
var d = (ctx._dev || dev.create)({
|
|
428
443
|
command: String(command),
|
|
429
444
|
args: argList,
|
|
430
445
|
watch: watchList.length ? watchList : undefined,
|
|
@@ -435,31 +450,51 @@ async function _runDev(args, ctx) {
|
|
|
435
450
|
env: ctx.env,
|
|
436
451
|
});
|
|
437
452
|
|
|
438
|
-
// Forward parent SIGINT/SIGTERM to the child via stop()
|
|
453
|
+
// Forward parent SIGINT/SIGTERM to the child via stop(). The supervisor
|
|
454
|
+
// resolves ONLY after stop() has fully completed (SIGTERM → SIGKILL
|
|
455
|
+
// escalation → watchers disarmed), so main() never returns — and the bin
|
|
456
|
+
// shim never process.exit()s — while a child kill is still in flight and
|
|
457
|
+
// could be abandoned, orphaning the app child.
|
|
439
458
|
var stopped = false;
|
|
459
|
+
var onStopComplete;
|
|
460
|
+
var stopComplete = new Promise(function (resolve) { onStopComplete = resolve; });
|
|
440
461
|
function shutdown() {
|
|
441
462
|
if (stopped) return;
|
|
442
463
|
stopped = true;
|
|
443
|
-
d.stop().then(
|
|
464
|
+
Promise.resolve(d.stop()).then(onStopComplete, onStopComplete);
|
|
444
465
|
}
|
|
445
466
|
process.once("SIGINT", shutdown);
|
|
446
467
|
process.once("SIGTERM", shutdown);
|
|
468
|
+
function _clearSignalHandlers() {
|
|
469
|
+
process.removeListener("SIGINT", shutdown);
|
|
470
|
+
process.removeListener("SIGTERM", shutdown);
|
|
471
|
+
}
|
|
447
472
|
|
|
448
473
|
try {
|
|
449
474
|
await d.start();
|
|
450
475
|
} catch (e) {
|
|
451
476
|
_writeLine(ctx.stderr, "blamejs dev: " + ((e && e.message) || String(e)));
|
|
477
|
+
_clearSignalHandlers();
|
|
452
478
|
return 1;
|
|
453
479
|
}
|
|
454
|
-
// The dev loop runs until the operator interrupts.
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
});
|
|
480
|
+
// The dev loop runs until the operator interrupts. A REF'd heartbeat holds
|
|
481
|
+
// the event loop open so a watched-child CRASH does not drain it and exit
|
|
482
|
+
// the supervisor — dev's crash-resilience contract is to stay up and
|
|
483
|
+
// hot-restart on the next file change, but an unref'd keep-alive let a
|
|
484
|
+
// child's exit terminate the parent with a false code 0. Cleared once
|
|
485
|
+
// stop() has completed on SIGINT/SIGTERM. Unref'ing would drain the loop on
|
|
486
|
+
// a child crash and reintroduce the exit-on-crash bug this heartbeat fixes.
|
|
487
|
+
// allow:timer-no-unref-process-pinning — supervisor must stay pinned.
|
|
488
|
+
var heartbeat = setInterval(function () {}, C.TIME.minutes(1));
|
|
489
|
+
// Hand the shutdown trigger to a test seam (no-op in production) so a test
|
|
490
|
+
// can drive the signal path deterministically instead of raising SIGINT.
|
|
491
|
+
if (typeof ctx._onDevRunning === "function") ctx._onDevRunning(shutdown);
|
|
492
|
+
try {
|
|
493
|
+
await stopComplete;
|
|
494
|
+
} finally {
|
|
495
|
+
clearInterval(heartbeat);
|
|
496
|
+
_clearSignalHandlers();
|
|
497
|
+
}
|
|
463
498
|
return 0;
|
|
464
499
|
}
|
|
465
500
|
|
|
@@ -766,7 +801,7 @@ async function _runAudit(args, ctx) {
|
|
|
766
801
|
_writeLine(ctx.stderr, "blamejs audit purge: --archive (path to verified archive bundle) is required");
|
|
767
802
|
return 2;
|
|
768
803
|
}
|
|
769
|
-
if (args.flags
|
|
804
|
+
if (!_isConfirmed(args.flags)) {
|
|
770
805
|
_writeLine(ctx.stderr, "blamejs audit purge: --confirm is REQUIRED — destructive operation");
|
|
771
806
|
return 2;
|
|
772
807
|
}
|
|
@@ -2081,7 +2116,7 @@ async function _runErase(args, ctx) {
|
|
|
2081
2116
|
var rowId = args.flags["row-id"];
|
|
2082
2117
|
if (!table || table === true) return report.error("--table <name> is required", 2);
|
|
2083
2118
|
if (!rowId || rowId === true) return report.error("--row-id <id> is required", 2);
|
|
2084
|
-
if (!args.flags
|
|
2119
|
+
if (!_isConfirmed(args.flags)) {
|
|
2085
2120
|
return report.error("--confirm is required (this operation is irreversible)", 2);
|
|
2086
2121
|
}
|
|
2087
2122
|
var dataDirFlag = args.flags["data-dir"];
|
|
@@ -2317,12 +2352,24 @@ async function main(argv, opts) {
|
|
|
2317
2352
|
stderr: opts.stderr || process.stderr,
|
|
2318
2353
|
env: opts.env || process.env,
|
|
2319
2354
|
cwd: opts.cwd || process.cwd(),
|
|
2355
|
+
// Test seams (undefined in production): _dev injects the dev-supervisor
|
|
2356
|
+
// factory; _onDevRunning receives the shutdown fn once `dev` is live, so
|
|
2357
|
+
// a test can drive graceful teardown without raising a real OS signal.
|
|
2358
|
+
_dev: opts._dev,
|
|
2359
|
+
_onDevRunning: opts._onDevRunning,
|
|
2320
2360
|
};
|
|
2321
2361
|
if (!Array.isArray(argv)) argv = [];
|
|
2322
2362
|
var args = _parseArgs(argv);
|
|
2323
2363
|
|
|
2324
|
-
//
|
|
2325
|
-
|
|
2364
|
+
// Version is honored only as the WHOLE invocation — a bare `-v` /
|
|
2365
|
+
// `--version` with no subcommand. `_parseArgs` declares `--version`
|
|
2366
|
+
// boolean, so a stray version flag never swallows a token: it works in any
|
|
2367
|
+
// order (leading, trailing, or between a command and its subcommand) and
|
|
2368
|
+
// leaves the positional list intact, so the command dispatches instead of
|
|
2369
|
+
// a silent version no-op returning a false 0. A `-v` used as another
|
|
2370
|
+
// option's value stays that value, and a version token after the `--`
|
|
2371
|
+
// terminator stays a literal positional.
|
|
2372
|
+
if ((args.flags.version || args.flags.v) && args.pos.length === 0) {
|
|
2326
2373
|
_writeLine(ctx.stdout, C.version);
|
|
2327
2374
|
return 0;
|
|
2328
2375
|
}
|
package/lib/dev.js
CHANGED
|
@@ -255,14 +255,29 @@ function create(opts) {
|
|
|
255
255
|
if (killTimer) { try { clearTimeoutFn(killTimer); } catch (_e) { /* timer already cleared */ } killTimer = null; }
|
|
256
256
|
resolve();
|
|
257
257
|
}
|
|
258
|
+
// 'exit' is the only event that reliably means the process is gone, so
|
|
259
|
+
// it is the only one that completes shutdown. A child that FAILED TO
|
|
260
|
+
// SPAWN has no pid and never emits 'exit' — it is settled by the
|
|
261
|
+
// no-process check below, NOT by listening for 'error'/'close', which
|
|
262
|
+
// can also fire for a LIVE child (a rejected kill emits 'error' while
|
|
263
|
+
// the process keeps running) and must not complete shutdown then.
|
|
258
264
|
c.once("exit", done);
|
|
259
|
-
|
|
265
|
+
var sent;
|
|
266
|
+
try { sent = c.kill(killSignal); }
|
|
260
267
|
catch (e) {
|
|
261
268
|
_logVia(log, "warn", "kill threw, child may already be gone",
|
|
262
269
|
{ error: (e && e.message) || String(e) });
|
|
263
270
|
done();
|
|
264
271
|
return;
|
|
265
272
|
}
|
|
273
|
+
// kill() returning false means the signal was NOT delivered — which can
|
|
274
|
+
// be either "no process exists" (a child that failed to spawn has no
|
|
275
|
+
// pid; no 'exit' will ever arrive, so settle now) OR "a live process
|
|
276
|
+
// rejected the signal" (e.g. EPERM after the child changed credentials;
|
|
277
|
+
// pid is set). Only the no-process case completes shutdown here; a live
|
|
278
|
+
// child keeps the SIGKILL escalation + awaits exit/close, so stop()
|
|
279
|
+
// never falsely reports termination while the process is still running.
|
|
280
|
+
if (!sent && !c.pid) { done(); return; }
|
|
266
281
|
// Hard-kill if the child ignores SIGTERM
|
|
267
282
|
killTimer = setTimeoutFn(function () {
|
|
268
283
|
if (settled) return;
|
package/lib/external-db.js
CHANGED
|
@@ -381,6 +381,18 @@ var _ROW_RETURNING_CLASS = Object.freeze({ SELECT: true, READ_INFO: true });
|
|
|
381
381
|
|
|
382
382
|
function statementReturnsRows(sql) {
|
|
383
383
|
if (typeof sql !== "string" || sql.length === 0) return false;
|
|
384
|
+
// EXPLAIN — with or without ANALYZE, wrapping any inner statement —
|
|
385
|
+
// always returns a plan row set to the caller: `EXPLAIN ANALYZE INSERT`
|
|
386
|
+
// executes the write AND yields the plan. That is the row-set question,
|
|
387
|
+
// orthogonal to the residency gate's read/write class (where a plain
|
|
388
|
+
// EXPLAIN is a read and EXPLAIN ANALYZE <write> is a write). An EXPLAIN
|
|
389
|
+
// prefix that does not resolve (no inner statement, unbalanced option
|
|
390
|
+
// parens, unterminated span) stays fail-closed false so the .all()/.run()
|
|
391
|
+
// chooser never mis-routes an unparseable statement.
|
|
392
|
+
var m = _STATEMENT_CLASS_RE.exec(sql);
|
|
393
|
+
if (m && m[1].toUpperCase() === "EXPLAIN") {
|
|
394
|
+
return _explainResolve(sql, m.index + m[0].length) !== null;
|
|
395
|
+
}
|
|
384
396
|
if (_ROW_RETURNING_CLASS[_classifyStatement(sql)] === true) return true;
|
|
385
397
|
return _hasTopLevelReturning(sql);
|
|
386
398
|
}
|
package/lib/forms.js
CHANGED
|
@@ -178,8 +178,11 @@ var INPUT_TYPES = {
|
|
|
178
178
|
};
|
|
179
179
|
|
|
180
180
|
function _renderInput(field) {
|
|
181
|
+
// A field with no explicit type defaults to a text input — mirror the
|
|
182
|
+
// `|| "text"` dispatch in _renderField so the emitted type attribute
|
|
183
|
+
// agrees with the widget actually rendered (never a bare type="").
|
|
181
184
|
var attrs = [
|
|
182
|
-
'type="' + escapeAttribute(field.type) + '"',
|
|
185
|
+
'type="' + escapeAttribute(field.type || "text") + '"',
|
|
183
186
|
'name="' + escapeAttribute(field.name) + '"',
|
|
184
187
|
];
|
|
185
188
|
if (field.value !== undefined && field.value !== null) {
|
|
@@ -358,7 +361,14 @@ function _coerce(field, raw) {
|
|
|
358
361
|
// (false), not "not provided." Coerce to false BEFORE the generic
|
|
359
362
|
// undefined-passthrough below.
|
|
360
363
|
if (field.type === "checkbox") {
|
|
361
|
-
|
|
364
|
+
// A urlencoded checkbox submits its VALUE string only when checked, so
|
|
365
|
+
// any present string — including a custom value of "false" / "0" / "" —
|
|
366
|
+
// means checked: presence, not the string content, is the checked
|
|
367
|
+
// signal. Unchecked is the field's ABSENCE (undefined / null) or, for a
|
|
368
|
+
// typed (JSON) body, an explicit boolean false / numeric 0. This keeps a
|
|
369
|
+
// required checkbox satisfiable whatever value it is rendered with, while
|
|
370
|
+
// a JSON false / 0 still reads as unchecked.
|
|
371
|
+
if (raw === undefined || raw === null || raw === false || raw === 0) return false;
|
|
362
372
|
return true;
|
|
363
373
|
}
|
|
364
374
|
if (raw === undefined) return undefined;
|
|
@@ -381,6 +391,74 @@ function _isEmpty(v) {
|
|
|
381
391
|
return v === undefined || v === null || v === "";
|
|
382
392
|
}
|
|
383
393
|
|
|
394
|
+
// A field's numeric bound (min / max / minlength / maxlength) must resolve
|
|
395
|
+
// to a finite number. A non-finite bound (a non-numeric string, NaN,
|
|
396
|
+
// Infinity) would make its comparison a silent no-op — NaN comparisons are
|
|
397
|
+
// always false — turning a documented constraint into nothing. That is an
|
|
398
|
+
// operator config error, so throw at the entry point (mirrors the
|
|
399
|
+
// pre-compiled-RegExp requirement for `pattern`) rather than shipping a
|
|
400
|
+
// form whose bound is quietly unenforced.
|
|
401
|
+
// A clean numeric literal: an optional sign, an integer/decimal mantissa,
|
|
402
|
+
// and an optional exponent. Linear (no nested quantifier over overlapping
|
|
403
|
+
// classes), so it is a framework-internal constant with no ReDoS surface.
|
|
404
|
+
var NUMERIC_LITERAL_RE = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
405
|
+
// Any real numeric bound is short — Number.MAX_VALUE stringifies to ~23
|
|
406
|
+
// chars — so cap the length before the regex test; a longer "number" string
|
|
407
|
+
// is malformed, and running a regex over an unbounded operator string is a
|
|
408
|
+
// DoS shape regardless of the pattern's own linearity.
|
|
409
|
+
var MAX_NUMERIC_BOUND_CHARS = 40;
|
|
410
|
+
|
|
411
|
+
function _numericBound(f, key) {
|
|
412
|
+
var v = f[key];
|
|
413
|
+
// Accept ONLY a genuine finite number or a string that is a clean numeric
|
|
414
|
+
// literal. A bare Number() coerces null / "" / whitespace / false / [] to a
|
|
415
|
+
// finite 0 (and true to 1), which would let a malformed bound — e.g.
|
|
416
|
+
// min: null, which the renderer emits as min="" (no constraint) — silently
|
|
417
|
+
// validate as 0 on the backend and diverge from the browser. Those are
|
|
418
|
+
// rejected here, not coerced.
|
|
419
|
+
var n = NaN;
|
|
420
|
+
if (typeof v === "number") n = v;
|
|
421
|
+
else if (typeof v === "string" && v.length <= MAX_NUMERIC_BOUND_CHARS && NUMERIC_LITERAL_RE.test(v)) n = Number(v);
|
|
422
|
+
if (!Number.isFinite(n)) {
|
|
423
|
+
throw new Error("forms.validate: field '" + f.name + "'." + key +
|
|
424
|
+
" must be a finite number; got " +
|
|
425
|
+
(typeof v === "string" ? JSON.stringify(v) : typeof v) +
|
|
426
|
+
". Fix the form spec.");
|
|
427
|
+
}
|
|
428
|
+
return n;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Validate a field's SPEC — the operator-supplied shape, independent of any
|
|
432
|
+
// submitted value. A numeric bound (min / max / minlength / maxlength) must
|
|
433
|
+
// be finite, and a `pattern` must be a pre-compiled, ReDoS-safe RegExp
|
|
434
|
+
// (compiling operator source on the request path would be an engine ReDoS
|
|
435
|
+
// surface). A malformed spec is an operator config error surfaced
|
|
436
|
+
// deterministically at the entry point — never conditionally on whether a
|
|
437
|
+
// given request happens to carry a value that reaches the bound, which would
|
|
438
|
+
// turn a config bug into an input-dependent runtime failure.
|
|
439
|
+
function _assertFieldSpec(f) {
|
|
440
|
+
if (!f.name || f.type === "submit") return;
|
|
441
|
+
// min / max are compared numerically only for number / range controls;
|
|
442
|
+
// date / time / datetime-local / month / week controls carry ISO-string
|
|
443
|
+
// bounds (e.g. min: "2026-01-01") that validate() never compares
|
|
444
|
+
// numerically, so those are not numeric bounds and must not be rejected.
|
|
445
|
+
if (f.type === "number" || f.type === "range") {
|
|
446
|
+
if (f.min !== undefined) _numericBound(f, "min");
|
|
447
|
+
if (f.max !== undefined) _numericBound(f, "max");
|
|
448
|
+
}
|
|
449
|
+
// minlength / maxlength are always character counts, whatever the control.
|
|
450
|
+
if (f.minlength !== undefined) _numericBound(f, "minlength");
|
|
451
|
+
if (f.maxlength !== undefined) _numericBound(f, "maxlength");
|
|
452
|
+
if (f.pattern) {
|
|
453
|
+
if (!(f.pattern instanceof RegExp)) {
|
|
454
|
+
throw new Error("forms.validate: field '" + f.name +
|
|
455
|
+
"'.pattern must be a pre-compiled RegExp; got " +
|
|
456
|
+
(typeof f.pattern) + ". Wrap the source string with `RegExp` at config time.");
|
|
457
|
+
}
|
|
458
|
+
guardRegex.assertSafe(f.pattern, "forms: field[" + f.name + "].pattern");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
384
462
|
/**
|
|
385
463
|
* @primitive b.forms.validate
|
|
386
464
|
* @signature b.forms.validate(spec, body)
|
|
@@ -390,8 +468,9 @@ function _isEmpty(v) {
|
|
|
390
468
|
*
|
|
391
469
|
* Walks the same spec the renderer accepts and validates a submitted
|
|
392
470
|
* body. Per field: required-field check, type coercion (string /
|
|
393
|
-
* number / boolean / email / url), `
|
|
394
|
-
* regex `pattern`, `enum` membership.
|
|
471
|
+
* number / boolean / email / url), `minlength` / `maxlength` bounds,
|
|
472
|
+
* numeric `min` / `max` bounds, regex `pattern`, `enum` membership.
|
|
473
|
+
* Returns
|
|
395
474
|
* `{ valid: boolean, errors: { field: msg, ... }, values: { ... } }`.
|
|
396
475
|
* The `values` object holds coerced values keyed by field name —
|
|
397
476
|
* route handlers consume `result.values` directly without re-parsing.
|
|
@@ -400,7 +479,7 @@ function _isEmpty(v) {
|
|
|
400
479
|
* var result = b.forms.validate(
|
|
401
480
|
* { fields: [
|
|
402
481
|
* { type: "email", name: "email", required: true },
|
|
403
|
-
* { type: "number", name: "age",
|
|
482
|
+
* { type: "number", name: "age", min: 1 },
|
|
404
483
|
* ] },
|
|
405
484
|
* { email: "ada@example.com", age: "37" }
|
|
406
485
|
* );
|
|
@@ -410,6 +489,12 @@ function validate(spec, body) {
|
|
|
410
489
|
if (!spec || !Array.isArray(spec.fields)) {
|
|
411
490
|
throw new Error("forms.validate: spec.fields must be an array");
|
|
412
491
|
}
|
|
492
|
+
// Validate every field's spec up front — a malformed bound or pattern is
|
|
493
|
+
// rejected on the first call, deterministically, regardless of the body
|
|
494
|
+
// (never only when a request happens to carry a value that reaches it).
|
|
495
|
+
for (var s = 0; s < spec.fields.length; s++) {
|
|
496
|
+
_assertFieldSpec(spec.fields[s]);
|
|
497
|
+
}
|
|
413
498
|
body = body || {};
|
|
414
499
|
var errors = {};
|
|
415
500
|
var values = {};
|
|
@@ -423,7 +508,21 @@ function validate(spec, body) {
|
|
|
423
508
|
var coerced = _coerce(f, raw);
|
|
424
509
|
values[f.name] = coerced;
|
|
425
510
|
|
|
426
|
-
|
|
511
|
+
// Checkbox: the only constraint is `required`, which HTML defines as
|
|
512
|
+
// "must be checked" — and the renderer emits that attribute, so the
|
|
513
|
+
// server enforces the same (backend validates what the frontend
|
|
514
|
+
// displays). An unchecked required box (coerced === false) is an error,
|
|
515
|
+
// never a silent pass. No length / pattern / enum checks apply.
|
|
516
|
+
if (f.type === "checkbox") {
|
|
517
|
+
if (f.required && coerced !== true) {
|
|
518
|
+
errors[f.name] = f.errorMessages && f.errorMessages.required
|
|
519
|
+
? f.errorMessages.required
|
|
520
|
+
: (f.label || f.name) + " is required";
|
|
521
|
+
}
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (f.required && _isEmpty(coerced)) {
|
|
427
526
|
errors[f.name] = f.errorMessages && f.errorMessages.required
|
|
428
527
|
? f.errorMessages.required
|
|
429
528
|
: (f.label || f.name) + " is required";
|
|
@@ -486,27 +585,14 @@ function validate(spec, body) {
|
|
|
486
585
|
errors[f.name] = (f.label || f.name) + " must be at most " + f.maxlength + " characters";
|
|
487
586
|
continue;
|
|
488
587
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
"'.pattern must be a pre-compiled RegExp; got " +
|
|
498
|
-
(typeof f.pattern) + ". Wrap the source string with `RegExp` at config time.");
|
|
499
|
-
}
|
|
500
|
-
// Screen the operator-supplied pattern for catastrophic-backtracking
|
|
501
|
-
// (ReDoS) shapes before the test, so a pathological regex can't be
|
|
502
|
-
// run against the submitted value.
|
|
503
|
-
guardRegex.assertSafe(f.pattern, "forms: field[" + f.name + "].pattern");
|
|
504
|
-
if (!f.pattern.test(coerced)) {
|
|
505
|
-
errors[f.name] = f.errorMessages && f.errorMessages.pattern
|
|
506
|
-
? f.errorMessages.pattern
|
|
507
|
-
: (f.label || f.name) + " has an invalid format";
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
588
|
+
// `pattern` was proven a pre-compiled, ReDoS-safe RegExp by the
|
|
589
|
+
// up-front spec pass, so only the match against the submitted value
|
|
590
|
+
// remains on the request path.
|
|
591
|
+
if (f.pattern && !f.pattern.test(coerced)) {
|
|
592
|
+
errors[f.name] = f.errorMessages && f.errorMessages.pattern
|
|
593
|
+
? f.errorMessages.pattern
|
|
594
|
+
: (f.label || f.name) + " has an invalid format";
|
|
595
|
+
continue;
|
|
510
596
|
}
|
|
511
597
|
}
|
|
512
598
|
if ((f.type === "select" || f.type === "radio") && Array.isArray(f.options)) {
|
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"hashes": {
|
|
19
19
|
"server": "sha256:2b30a26f728c5349f4c4b47834f862a4f77393b1224fc12b22abe3ce2cfab78f"
|
|
20
20
|
},
|
|
21
|
-
"refreshedAt": "2026-07-
|
|
21
|
+
"refreshedAt": "2026-07-25T04:14:23.876Z"
|
|
22
22
|
},
|
|
23
23
|
"@noble/curves": {
|
|
24
24
|
"version": "2.2.0",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"hashes": {
|
|
41
41
|
"server": "sha256:2880c288b1285ef51d356d057bee6f0c8a00de36638cf47b47617e8c1faf10d5"
|
|
42
42
|
},
|
|
43
|
-
"refreshedAt": "2026-07-
|
|
43
|
+
"refreshedAt": "2026-07-25T04:14:23.876Z"
|
|
44
44
|
},
|
|
45
45
|
"@noble/post-quantum": {
|
|
46
46
|
"version": "0.6.1",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"hashes": {
|
|
72
72
|
"server": "sha256:f9c94094b3c10fe73dac5343289da582454ea6053494fab2bf66099d9103d6c3"
|
|
73
73
|
},
|
|
74
|
-
"refreshedAt": "2026-07-
|
|
74
|
+
"refreshedAt": "2026-07-25T04:14:23.876Z"
|
|
75
75
|
},
|
|
76
76
|
"@simplewebauthn/server": {
|
|
77
77
|
"version": "13.3.2",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"hashes": {
|
|
95
95
|
"server": "sha256:e83195dc9f189385da9c856ef38843f4466f93ea8f3d7fc2efcb1e1b18da6f20"
|
|
96
96
|
},
|
|
97
|
-
"refreshedAt": "2026-07-
|
|
97
|
+
"refreshedAt": "2026-07-25T04:14:23.876Z"
|
|
98
98
|
},
|
|
99
99
|
"SecLists-common-passwords-top-10000": {
|
|
100
100
|
"version": "10k-most-common (master)",
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
},
|
|
115
115
|
"runtime_artifact": "lib/vendor/common-passwords-top-10000.data.js",
|
|
116
116
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
117
|
-
"refreshedAt": "2026-07-
|
|
117
|
+
"refreshedAt": "2026-07-25T04:14:23.876Z"
|
|
118
118
|
},
|
|
119
119
|
"bimi-trust-anchors": {
|
|
120
120
|
"version": "operator-managed",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
},
|
|
140
140
|
"runtime_artifact": "lib/vendor/bimi-trust-anchors.data.js",
|
|
141
141
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
142
|
-
"refreshedAt": "2026-07-
|
|
142
|
+
"refreshedAt": "2026-07-25T04:14:23.876Z"
|
|
143
143
|
},
|
|
144
144
|
"publicsuffix-list": {
|
|
145
145
|
"version": "master",
|
|
@@ -152,14 +152,14 @@
|
|
|
152
152
|
"data_js": "lib/vendor/public-suffix-list.data.js"
|
|
153
153
|
},
|
|
154
154
|
"bundler": "curl https://publicsuffix.org/list/public_suffix_list.dat",
|
|
155
|
-
"bundledAt": "2026-07-
|
|
155
|
+
"bundledAt": "2026-07-24T00:00:00Z",
|
|
156
156
|
"hashes": {
|
|
157
|
-
"server": "sha256:
|
|
158
|
-
"data_js": "sha256:
|
|
157
|
+
"server": "sha256:1fc04fd8ebd4b77c78c38c76d832673c27b56ca154fda6339be61f38b9918701",
|
|
158
|
+
"data_js": "sha256:1d17ee3059030d1de7208d699ffdd1595c97cd3009d1f41bbfde2ba1a3315372"
|
|
159
159
|
},
|
|
160
160
|
"runtime_artifact": "lib/vendor/public-suffix-list.data.js",
|
|
161
161
|
"integrity_layers": "sha256 + sha3-512 + SLH-DSA-SHAKE-256f signature + in-payload canary (where applicable)",
|
|
162
|
-
"refreshedAt": "2026-07-
|
|
162
|
+
"refreshedAt": "2026-07-25T04:14:23.876Z"
|
|
163
163
|
},
|
|
164
164
|
"peculiar-pki": {
|
|
165
165
|
"version": "2.0.0+pkijs-3.4.0",
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
"hashes": {
|
|
191
191
|
"server": "sha256:2307ef65e070757ffb13442b377e45efb9fa1a10432d9b39618387720ab990ed"
|
|
192
192
|
},
|
|
193
|
-
"refreshedAt": "2026-07-
|
|
193
|
+
"refreshedAt": "2026-07-25T04:14:23.876Z"
|
|
194
194
|
}
|
|
195
195
|
}
|
|
196
196
|
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// Please pull this list from, and only from https://publicsuffix.org/list/public_suffix_list.dat,
|
|
6
6
|
// rather than any other VCS sites. Pulling from any other URL is not guaranteed to be supported.
|
|
7
7
|
|
|
8
|
-
// VERSION: 2026-07-
|
|
9
|
-
// COMMIT:
|
|
8
|
+
// VERSION: 2026-07-24_16-56-26_UTC
|
|
9
|
+
// COMMIT: f85a38e61c222ea2d9901a41296c6c74fd3e2c28
|
|
10
10
|
|
|
11
11
|
// Instructions on pulling and using this list can be found at https://publicsuffix.org/list/.
|
|
12
12
|
|
|
@@ -6838,7 +6838,7 @@ org.zw
|
|
|
6838
6838
|
|
|
6839
6839
|
// newGTLDs
|
|
6840
6840
|
|
|
6841
|
-
// List of new gTLDs imported from https://www.icann.org/resources/registries/gtlds/v2/gtlds.json on 2026-07-
|
|
6841
|
+
// List of new gTLDs imported from https://www.icann.org/resources/registries/gtlds/v2/gtlds.json on 2026-07-24T16:40:16Z
|
|
6842
6842
|
// This list is auto-generated, don't edit it manually.
|
|
6843
6843
|
// aaa : American Automobile Association, Inc.
|
|
6844
6844
|
// https://www.iana.org/domains/root/db/aaa.html
|
|
@@ -10732,6 +10732,10 @@ weather
|
|
|
10732
10732
|
// https://www.iana.org/domains/root/db/weatherchannel.html
|
|
10733
10733
|
weatherchannel
|
|
10734
10734
|
|
|
10735
|
+
// web : VeriSign, Inc.
|
|
10736
|
+
// https://www.iana.org/domains/root/db/web.html
|
|
10737
|
+
web
|
|
10738
|
+
|
|
10735
10739
|
// webcam : dot Webcam Limited
|
|
10736
10740
|
// https://www.iana.org/domains/root/db/webcam.html
|
|
10737
10741
|
webcam
|