@blamejs/core 0.17.18 → 0.17.20

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 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.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.
12
+
13
+ - 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.
14
+
11
15
  - 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.
12
16
 
13
17
  - v0.17.17 (2026-07-23) — **Three verifier hardening fixes: timestamp trust anchors are required by default, OIDC id_token verification requires a configured issuer, and ed25519 DKIM uses the RFC 8463 raw-key format.** An audit of the signature-verifier surface found three verifiers that accepted or rejected the wrong thing. b.tsa.verifyToken authenticated a timestamp against a certificate embedded in the token itself and only ran the trust-anchor chain when the caller supplied one, so a self-signed token carrying the timestamping EKU was accepted as a valid timestamp; trust anchors are now required by default. b.auth.oauth's verifyIdToken skipped the CVE-2026-23552 cross-realm iss check entirely when the client was created without an issuer, so any OIDC id_token verified regardless of its issuer; an OIDC client must now be configured with an issuer to verify id_tokens. And the ed25519 DKIM verifier could not read the RFC 8463 raw 32-byte key format that every conformant sender publishes -- while the framework's own bootstrap published the non-standard SPKI form -- so ed25519, the default DKIM algorithm, was non-interoperable in both directions. **Fixed:** *ed25519 DKIM uses the RFC 8463 raw-key format on both sign and verify* — RFC 8463 §3-4 publishes an ed25519 DKIM public key as the raw 32-byte key, base64'd -- the form every conformant sender uses -- but the verifier wrapped that raw key in SubjectPublicKeyInfo PEM markers, which is not valid SPKI, so createPublicKey threw and a valid ed25519 signature returned permerror. Compounding it, b.mail.dkim.bootstrap published the key as SPKI DER (60 base64 chars) instead of the raw form (44 chars), so the framework's own default-algorithm signatures would not verify at a conformant receiver. The verifier now accepts a raw 32-byte key (wrapping it in the ed25519 SPKI header) and bootstrap publishes the raw key; RSA is unchanged. The same raw-key handling is applied on the ARC (b.mail.arc.verify) message-signature path. **Security:** *Timestamp verification requires a trust anchor by default* — b.tsa.verifyToken verified an RFC 3161 timestamp against the signer certificate embedded in the token -- which is attacker-controlled -- and ran the certificate-chain / trust-anchor check only when the caller passed trustAnchorsPem. With it omitted, a token whose self-signed leaf carried a critical, sole id-kp-timeStamping EKU was accepted as a valid timestamp: a forged proof that data existed at an arbitrary time (RFC 3161 §2.4.2 requires a trusted TSA). trustAnchorsPem is now required by default; an operator who deliberately wants trust-anchor-free verification sets allowUntrustedIssuer:true and the result carries issuerTrusted:false so the unauthenticated posture is visible (mirrors b.mdoc.verifyIssuerSigned). · *OIDC id_token verification requires a configured issuer* — b.auth.oauth.verifyIdToken wrapped the expected-issuer comparison -- the CVE-2026-23552 cross-realm / cross-issuer defense -- in a check that ran only when the client was configured with an issuer. An OIDC client (the default) created without an issuer therefore accepted an id_token with any iss, or none, as long as the signature, aud, and exp were valid: exactly the cross-realm acceptance the verifier exists to close, dangerous against a shared-signing-key multi-tenant OP. verifyIdToken now refuses to verify an OIDC id_token when no expected issuer is configured (OIDC Core §3.1.3.7); pass issuer to b.auth.oauth.create().
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-23)
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
@@ -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
- if (raw === undefined || raw === null || raw === "" || raw === "false" || raw === "0") return false;
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), `minLength` / `maxLength` bounds,
394
- * regex `pattern`, `enum` membership. Returns
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", minLength: 1 },
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
- if (f.required && _isEmpty(coerced) && coerced !== false) {
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
- if (f.pattern) {
490
- // Pattern accepts a pre-compiled RegExp only taking a string
491
- // and compiling it here would be a ReDoS surface against the
492
- // form-render engine. Operators construct the RegExp at config
493
- // time so the framework never compiles operator-supplied source
494
- // on the request path.
495
- if (!(f.pattern instanceof RegExp)) {
496
- throw new Error("forms.validate: field '" + f.name +
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)) {
@@ -18,7 +18,7 @@
18
18
  "hashes": {
19
19
  "server": "sha256:2b30a26f728c5349f4c4b47834f862a4f77393b1224fc12b22abe3ce2cfab78f"
20
20
  },
21
- "refreshedAt": "2026-07-23T22:29:23.692Z"
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-23T22:29:23.692Z"
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-23T22:29:23.692Z"
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-23T22:29:23.692Z"
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-23T22:29:23.692Z"
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-23T22:29:23.692Z"
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-23T00:00:00Z",
155
+ "bundledAt": "2026-07-24T00:00:00Z",
156
156
  "hashes": {
157
- "server": "sha256:04220f6e1c20c9af1edfcd891dd4663b2c92a020b13e698dd13b3630573520ca",
158
- "data_js": "sha256:52428436de68a79537db52f2b0cb7ac0a1cc3e6397db87533c6e2f1bd2e12087"
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-23T22:29:23.692Z"
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-23T22:29:23.692Z"
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-23_18-24-27_UTC
9
- // COMMIT: 93b4eb174b2da9ee3f6effc363b579a96a65c93a
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-15T16:20:56Z
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