@blamejs/core 0.8.82 → 0.8.83

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,7 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.8.x
10
10
 
11
+ - v0.8.83 (2026-05-11) — **ACME 47-day-cert readiness**: certificate profiles + dns-account-01 challenge + ARI renewal-window jitter. **draft-aaron-acme-profiles** lands as `acme.listProfiles()` (reads `directory.meta.profiles` and returns the CA-advertised `{ name: description }` map) + `acme.newOrder({ profile })` (passes the chosen profile name through the order payload; refuses non-string + caps length at 64 bytes). As CA/B Forum SC-081v3 phases in the 47-day mandate, profile-name vocabulary becomes the operator-facing handle for "long-lived" vs "47-day" vs "short-lived" cert selection. **draft-ietf-acme-dns-account-label** lands as `acme.dnsAccount01ChallengeRecord(token, { identifier, ttl? })` which builds the per-account-scoped TXT record (`_<accountLabel>._acme-challenge.<host>`) where `accountLabel` is the lowercase base32 of the first 80 bits of `SHA-256(accountUrl)`. Refuses pre-newAccount (label needs accountUrl as seed); caps identifier at 255 bytes; refuses negative / huge TTL. **RFC 9773 §4.2 fleet-scheduling jitter**: `acme.renewIfDue({ jitter: true })` now returns a `renewAt` ISO timestamp picked uniformly across the CA-suggested window so operator fleets running on the same poll cadence stop clustering their renewal storms at the window-start instant. Default behavior (`jitter` off or absent) preserves pre-0.8.83 "renew now" semantics. The `acme.cert.renew.scheduled` audit row carries the chosen `renewAt` when jitter is on.
11
12
  - v0.8.82 (2026-05-11) — **Privacy 2026 posture sweep**. 27 new postures land in `b.compliance.KNOWN_POSTURES` (with matching `REGIME_MAP` + `POSTURE_DEFAULTS` cascade entries) closing the privacy gap surfaced by the 2026-05-11 multi-agent compliance audit. **US federal**: `coppa` + `coppa-2025` (FTC final rule 2025-04-22, effective 2026-06-23 — biometric expansion + knowing-collection-13-and-under disclosure; cascade adds backupEncryptionRequired:true + vacuum-after-erase), `glba-safeguards` (GLBA Safeguards Rule 2024 Amendment, effective 2024-05-13; cascade matches pci-dss + nydfs-500 financial tier), `gina` (Genetic Information Nondiscrimination Act), `vppa` (Video Privacy Protection Act), `can-spam`, `il-gipa` (Illinois Genetic Information Privacy Act with post-2024 private right of action), `hhs-repro-24` (HHS Reproductive Health HIPAA Amendment 2024-12-23), `nist-pf-1.1` (NIST Privacy Framework 1.1, final 2025-04-14). **UK**: `uk-duaa` (Data (Use and Access) Act 2025 — Royal Assent 2025-06-19; replaces the abandoned DPDI Bill; cascade matches GDPR floor with vacuum-after-erase). **Latin America**: `cl-pdpa` (Chile Ley 21.719, enacted 2024-12-13, effective 2026-12-01; cascade mirrors gdpr), `mx-lfpdppp` (Mexico 2025 secondary reform), `ar-pdpa` (Argentina Ley 25.326). **APAC**: `pipa-kr` (Korea PIPA 2023 major amendment, phased 2023-09-15 / 2024-03-15), `au-privacy` (Australia Privacy Act + 2024 Amendment Act — statutory tort effective 2025-06-10), `th-pdpa`, `vn-pdp` (Vietnam PDP Law effective 2026-01-01), `id-pdp` (Indonesia PDP Law effective 2024-10-17), `my-pdpa` (Malaysia 2024 amendments effective 2025-04-30). **US state child-privacy**: `ny-safe-kids` + `ny-saffe` (NY Child Data Protection Act + Stop Addictive Feeds Exploitation, both effective 2025-06-20), `md-kids-code` (Maryland Age-Appropriate Design Code), `vt-aadc` (Vermont AADC). **EU non-personal-data + adjacent**: `dsa` (Digital Services Act, fully applicable 2024-02-17), `dga` (Data Governance Act, applicable 2023-09-24), `eu-cer` (Critical Entities Resilience Directive 2022/2557, transposition 2024-10-17), `eu-cyber-sol` (Cyber Solidarity Act 2025/38, effective 2025-02-04), `eidas-2` (eIDAS 2 / EUDI Wallet, rollout 2026-2027). New REGIME_MAP `domain` values introduced: `child-privacy`, `financial-privacy`, `consumer-privacy`, `genetic-privacy`, `platform-governance`, `identity` — operators rendering compliance dashboards grouped by domain pick up the new buckets via `b.compliance.posturesByDomain(domain)` without code changes.
12
13
  - v0.8.81 (2026-05-11) — **AI-governance compliance postures + ISO 42001/23894 cross-walk + privacy catalog drift fixes**. 18 new postures register in `b.compliance.KNOWN_POSTURES` (and the matching `REGIME_MAP` + `POSTURE_DEFAULTS` cascade): state AI governance (`co-ai`, `il-hb3773`, `tx-traiga`, `ut-aipa`, `nyc-ll144`, `ca-tfaia` — frontier AI critical-incident records cascade to `backupEncryptionRequired:true`), international AI (`kr-ai-basic`, `cn-ai-label`), AI management standards (`iso-42001`, `iso-23894`), California gen-AI content credentials (`ca-sb942`, `ca-ab853`), substrate-to-posture cleanup so existing primitives gain catalog entries (`eaa` for EU Accessibility Act + `b.compliance-eaa`, `wcag-2-2` for `b.guardHtml.wcag`, `eu-data-act` for `b.dataAct`, `hitech` extending HIPAA-tier, `ferpa` for student records), plus `fl-fdbr` (Florida Digital Bill of Rights) and the long-missing `dpdp` (India DPDP Act 2023 — was in `POSTURE_DEFAULTS` cascade table but not in `KNOWN_POSTURES`, so `b.compliance.set("dpdp")` threw `compliance/unknown-posture`). **ISO 42001 + 23894 cross-walk**: new `b.compliance.aiAct.crossWalkIso42001([aiActCitation])` and `crossWalkIso23894()` return a 15-row mapping table linking EU AI Act articles (Art. 9 risk management → Art. 73 incident reporting) to ISO/IEC 42001:2023 Annex A controls and ISO/IEC 23894:2023 risk-management clauses. Operators chasing ISO 42001 certification under AI Act high-risk scope use the table to produce one cross-walk artifact instead of hand-rolling two separate audits; the table is read-only metadata, defensive copies returned, no behavior change at deploy time. **DSR drift fix**: `b.dsr.stateRules("fl-fdbr")` / `stateRules("FL")` now resolve (45-day response window, 15-day extension, 30-day cure, profiling opt-out enabled, minor opt-in 13). **Citation drift fix**: four state-privacy posture citations corrected from "(effective 2026-MM-DD)" to "(effective 2025-MM-DD)" — `modpa`, `nh-nhpa`, `nj-njdpa`, `mn-mncdpa` all took effect during 2025; the year-late citations would have surfaced as audit-trail discrepancies under operator review.
13
14
  - v0.8.80 (2026-05-10) — **Bug fix — `b.config.loadDbBacked` overlapping-tick race**. `cfg.refresh()` calls `_tick()` directly and the periodic poller also invokes `_tick()` independently. When two ticks overlap (two `refresh()`es back-to-back, or `refresh()` racing a poll), the older read could resolve LAST and overwrite a newer config write — so `admin-save → await cfg.refresh()` was not guaranteed to leave the latest value active when `fetchRows` latency varied across calls. Reproducible by serving a 200ms read followed by a 20ms read; without the fix, the slower (older) result clobbered the faster (newer) one. Fix: every tick claims a monotonic sequence number at start; at apply-time, ticks whose sequence is older than the last-applied sequence drop with a `config.reload.skipped` audit emission (phase `stale-tick`). The high-water mark advances ONLY after `cfg.reload` succeeds — a newer tick whose validation fails must not suppress an older in-flight tick that still has valid data (otherwise `refresh(valid)` followed by `refresh(invalid)` could silently keep stale config active even though the valid update was about to land). Fetch / transform failures short-circuit before the apply path and likewise do NOT advance the watermark.
package/README.md CHANGED
@@ -95,7 +95,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
95
95
  - **AAD-bound sealed columns** — AEAD tag tied to `(table, rowId, column, schemaVersion)`; copy-paste between rows or schema-version replay surfaces as refused decrypt (`b.vault.aad`)
96
96
  - **Signed webhooks + API encryption** — SLH-DSA-SHAKE-256f default; ML-DSA-65 opt-in; ECIES API encryption (`b.webhook`, `b.crypto`)
97
97
  - **HPKE / HTTP signatures** — RFC 9180 HPKE with ML-KEM-1024 + HKDF-SHA3-512 + ChaCha20-Poly1305 (`b.crypto.hpke`); RFC 9421 HTTP Message Signatures with derived components and ed25519 / ML-DSA-65 (`b.crypto.httpSig`)
98
- - **TLS / channel binding** — RFC 9266 TLS-Exporter token-to-session pinning (`b.tlsExporter`); RFC 9162 CT v2 inclusion-proof verification (`b.network.tls.ct.verifyInclusion`); RFC 8555 ACME + RFC 9773 ARI for 47-day certs (`b.acme`); RFC 8470 0-RTT inbound posture refuse / replay-cache (`b.router.create({tls0Rtt})`); RFC 9794 SecP256r1MLKEM768 in preferred-group order (`b.network.tls.preferredGroups`)
98
+ - **TLS / channel binding** — RFC 9266 TLS-Exporter token-to-session pinning (`b.tlsExporter`); RFC 9162 CT v2 inclusion-proof verification (`b.network.tls.ct.verifyInclusion`); RFC 8555 ACME + RFC 9773 ARI for 47-day certs with `{ jitter: true }` fleet-scheduling (`b.acme.renewIfDue`); draft-aaron-acme-profiles (`acme.listProfiles()` + `newOrder({ profile })`); draft-ietf-acme-dns-account-label (`acme.dnsAccount01ChallengeRecord(token, { identifier })`); RFC 8470 0-RTT inbound posture refuse / replay-cache (`b.router.create({tls0Rtt})`); RFC 9794 SecP256r1MLKEM768 in preferred-group order (`b.network.tls.preferredGroups`)
99
99
  - **mTLS CA** — pure-JS, issues clientAuth / serverAuth / dual-EKU certs with SAN; auto-detects highest-PQC signature alg (today ECDSA-P384-SHA384; self-upgrades to SLH-DSA / ML-DSA when X.509 ecosystem catches up); PQC TLS gates inbound + outbound (`b.mtlsCa`, `b.pqcGate`, `b.pqcAgent`)
100
100
  ### HTTP
101
101
 
package/lib/acme.js CHANGED
@@ -565,6 +565,23 @@ function create(opts) {
565
565
  var payload = { identifiers: orderOpts.identifiers.slice() };
566
566
  if (typeof orderOpts.notBefore === "string") payload.notBefore = orderOpts.notBefore;
567
567
  if (typeof orderOpts.notAfter === "string") payload.notAfter = orderOpts.notAfter;
568
+ // draft-aaron-acme-profiles — operator-selected certificate profile.
569
+ // The CA advertises profile names + descriptions via
570
+ // `directory.meta.profiles`; operator passes the chosen name through
571
+ // newOrder. CAs honoring the draft return 400 when the name isn't
572
+ // in the advertised set; ones that haven't adopted the draft ignore
573
+ // the field. v1-defensible scope: refuse non-string + cap length so
574
+ // attacker-supplied profile values can't bloat the JSON payload.
575
+ if (typeof orderOpts.profile === "string") {
576
+ if (orderOpts.profile.length === 0 || orderOpts.profile.length > C.BYTES.bytes(64)) {
577
+ throw _err("acme/bad-profile",
578
+ "newOrder: profile name must be a non-empty string <= 64 bytes", true);
579
+ }
580
+ payload.profile = orderOpts.profile;
581
+ } else if (orderOpts.profile !== undefined) {
582
+ throw _err("acme/bad-profile",
583
+ "newOrder: profile must be a string when provided", true);
584
+ }
568
585
  var rsp = await _signedPost(state.directory.newOrder, payload);
569
586
  if (rsp.statusCode !== 201) {
570
587
  _emitAudit(audit, "acme.order.created", "failure",
@@ -710,7 +727,31 @@ function create(opts) {
710
727
  async function renewIfDue(opts2) {
711
728
  var ari = await fetchAri(opts2);
712
729
  var nowMs = Date.now();
713
- if (nowMs < ari.suggestedWindow.startMs) {
730
+ // RFC 9773 §4.2 — when called inside the suggested window, return
731
+ // a renewAt timestamp picked uniformly across the remaining window
732
+ // so a fleet of operators running on the same poll cadence don't
733
+ // cluster their renewal storms at the window-start instant. Operators
734
+ // opt in via `{ jitter: true }`; default behavior preserves the
735
+ // pre-0.8.83 "renew now" semantics.
736
+ var jitter = opts2 && opts2.jitter === true;
737
+ var beforeWindow = nowMs < ari.suggestedWindow.startMs;
738
+ var pastWindow = nowMs > ari.suggestedWindow.endMs;
739
+ var renewAtMs = null;
740
+ if (jitter) {
741
+ // Uniform random point in [max(now, start), end].
742
+ var jLo = beforeWindow ? ari.suggestedWindow.startMs : nowMs;
743
+ var jHi = ari.suggestedWindow.endMs;
744
+ if (jHi >= jLo) {
745
+ // Non-crypto: RFC 9773 §4.2 fleet-scheduling jitter inside the
746
+ // CA-suggested renewal window. Predictability is not a threat
747
+ // here; uniform distribution across the window is the goal.
748
+ renewAtMs = jLo + Math.floor(Math.random() * (jHi - jLo + 1)); // allow:math-random-noncrypto — RFC 9773 fleet jitter, predictability not a threat
749
+ } else {
750
+ // Past-window — renew immediately, no jitter.
751
+ renewAtMs = nowMs;
752
+ }
753
+ }
754
+ if (beforeWindow) {
714
755
  _emitAudit(audit, "acme.cert.renew.skipped", "success", {
715
756
  certId: ari.certId,
716
757
  windowStart: ari.suggestedWindow.start,
@@ -718,24 +759,31 @@ function create(opts) {
718
759
  nowIso: new Date(nowMs).toISOString(),
719
760
  });
720
761
  _emitObs("acme.cert.renew.skipped", { reason: "before-window" });
721
- return { shouldRenew: false, reason: "before-window", ari: ari };
762
+ var ret = { shouldRenew: false, reason: "before-window", ari: ari };
763
+ if (jitter) ret.renewAt = new Date(renewAtMs).toISOString();
764
+ return ret;
722
765
  }
723
- if (nowMs > ari.suggestedWindow.endMs) {
766
+ if (pastWindow) {
724
767
  _emitAudit(audit, "acme.cert.renew.scheduled", "warning", {
725
768
  certId: ari.certId,
726
769
  reason: "past-window",
727
770
  windowEnd: ari.suggestedWindow.end,
728
771
  });
729
772
  _emitObs("acme.cert.renew.scheduled", { reason: "past-window" });
730
- return { shouldRenew: true, reason: "past-window", ari: ari };
773
+ var rp = { shouldRenew: true, reason: "past-window", ari: ari };
774
+ if (jitter) rp.renewAt = new Date(renewAtMs).toISOString();
775
+ return rp;
731
776
  }
732
777
  _emitAudit(audit, "acme.cert.renew.scheduled", "success", {
733
778
  certId: ari.certId,
734
779
  windowStart: ari.suggestedWindow.start,
735
780
  windowEnd: ari.suggestedWindow.end,
781
+ renewAt: jitter ? new Date(renewAtMs).toISOString() : null,
736
782
  });
737
783
  _emitObs("acme.cert.renew.scheduled", { reason: "in-window" });
738
- return { shouldRenew: true, reason: "in-window", ari: ari };
784
+ var ri = { shouldRenew: true, reason: "in-window", ari: ari };
785
+ if (jitter) ri.renewAt = new Date(renewAtMs).toISOString();
786
+ return ri;
739
787
  }
740
788
 
741
789
  /**
@@ -896,6 +944,118 @@ function create(opts) {
896
944
  return crypto.createHash("sha256").update(keyAuth, "utf8").digest();
897
945
  }
898
946
 
947
+ /**
948
+ * @primitive b.acme.create.listProfiles
949
+ * @signature b.acme.create.listProfiles()
950
+ * @since 0.8.83
951
+ * @status experimental
952
+ *
953
+ * Returns the CA-advertised certificate profile catalog as
954
+ * `{ name: description }` per draft-aaron-acme-profiles. Operators
955
+ * pass the chosen name through `newOrder({ profile: name })`; CAs
956
+ * use the profile to select certificate lifetime + key-usage +
957
+ * validation rigor. As CA/B Forum 47-day cert TTLs phase in (Mar
958
+ * 2026 ballot SC-081v3), profile-name vocabulary becomes the
959
+ * operator-facing handle for "long-lived" vs "47-day" vs "short-
960
+ * lived". Returns an empty object when the directory has no
961
+ * `meta.profiles` map (CA hasn't adopted the draft). Refreshes the
962
+ * directory cache when none has been fetched yet.
963
+ *
964
+ * @example
965
+ * await acme.fetchDirectory();
966
+ * var profiles = acme.listProfiles();
967
+ * // → { "default": "Standard 90-day certificate",
968
+ * // "shortlived": "47-day certificate (CA/B Forum SC-081v3)",
969
+ * // "tlsserver": "TLS server profile with Must-Staple" }
970
+ *
971
+ * await acme.newOrder({ identifiers: [{ type: "dns", value: "example.com" }],
972
+ * profile: "shortlived" });
973
+ */
974
+ function listProfiles() {
975
+ if (!state.directory) return {};
976
+ var meta = state.directory.meta;
977
+ if (!meta || typeof meta !== "object") return {};
978
+ var profiles = meta.profiles;
979
+ if (!profiles || typeof profiles !== "object") return {};
980
+ var out = {};
981
+ var keys = Object.keys(profiles);
982
+ for (var i = 0; i < keys.length; i += 1) {
983
+ var k = keys[i];
984
+ var v = profiles[k];
985
+ out[k] = typeof v === "string" ? v : "";
986
+ }
987
+ return out;
988
+ }
989
+
990
+ /**
991
+ * @primitive b.acme.create.dnsAccount01ChallengeRecord
992
+ * @signature b.acme.create.dnsAccount01ChallengeRecord(token, opts?)
993
+ * @since 0.8.83
994
+ * @status experimental
995
+ * @related b.acme.create.tlsAlpn01KeyAuthorization
996
+ *
997
+ * Build the DNS TXT record an operator publishes to satisfy a
998
+ * `dns-account-01` challenge per draft-ietf-acme-dns-account-label.
999
+ * Unlike `dns-01` (record at `_acme-challenge.<host>`),
1000
+ * `dns-account-01` scopes the record by account so the same domain
1001
+ * can be validated from multiple ACME accounts without record-name
1002
+ * collisions; the record name becomes
1003
+ * `_<accountLabel>._acme-challenge.<identifier>` where
1004
+ * `accountLabel` is the SHA-256 truncated-base32 of the account URL.
1005
+ *
1006
+ * Returns `{ name, value, ttl }` where `name` is the FQDN to publish
1007
+ * the TXT record at (with operator-supplied `identifier` substituted
1008
+ * in) and `value` is the SHA-256 of the key authorization in
1009
+ * unpadded base64url (same as `dns-01`). Refuses when `newAccount`
1010
+ * has not run (no accountUrl yet); refuses non-string token /
1011
+ * identifier.
1012
+ *
1013
+ * @opts
1014
+ * identifier: string, // host being validated (required)
1015
+ * ttl: number, // suggested DNS TTL in seconds; default: 60
1016
+ *
1017
+ * @example
1018
+ * await acme.newAccount({ contact: ["mailto:ops@example.com"] });
1019
+ * var rec = acme.dnsAccount01ChallengeRecord("token123", {
1020
+ * identifier: "example.com",
1021
+ * });
1022
+ * // rec.name → "_<accountLabel>._acme-challenge.example.com"
1023
+ * // rec.value → "<base64url-of-sha256(token123.<thumbprint>)>"
1024
+ * // rec.ttl → 60
1025
+ */
1026
+ function dnsAccount01ChallengeRecord(token, opts2) {
1027
+ if (typeof token !== "string" || token.length === 0) {
1028
+ throw _err("acme/bad-token", "dnsAccount01ChallengeRecord: token must be a non-empty string", true);
1029
+ }
1030
+ if (!opts2 || typeof opts2 !== "object" || typeof opts2.identifier !== "string" || opts2.identifier.length === 0) {
1031
+ throw _err("acme/bad-identifier", "dnsAccount01ChallengeRecord: opts.identifier (host) is required", true);
1032
+ }
1033
+ if (opts2.identifier.length > C.BYTES.bytes(255)) {
1034
+ throw _err("acme/bad-identifier", "dnsAccount01ChallengeRecord: identifier exceeds 255 bytes", true);
1035
+ }
1036
+ if (!state.accountUrl) {
1037
+ throw _err("acme/no-account",
1038
+ "dnsAccount01ChallengeRecord: newAccount() must run first (account URL is the label seed)", true);
1039
+ }
1040
+ if (opts2.ttl !== undefined && (typeof opts2.ttl !== "number" || !isFinite(opts2.ttl) || opts2.ttl < 1 || opts2.ttl > C.TIME.hours(24) / C.TIME.seconds(1))) {
1041
+ throw _err("acme/bad-ttl",
1042
+ "dnsAccount01ChallengeRecord: ttl must be a positive integer <= 86400 seconds", true);
1043
+ }
1044
+ var crypto = require("node:crypto");
1045
+ // Account label: lowercase base32 of first 10 bytes of SHA-256(accountUrl)
1046
+ // (per draft-ietf-acme-dns-account-label §3.1 — 80-bit truncated label).
1047
+ var hash = crypto.createHash("sha256").update(state.accountUrl, "utf8").digest();
1048
+ var label = _base32lc(hash.subarray(0, 10));
1049
+ // Record value: same key-authorization digest shape as dns-01.
1050
+ var keyAuth = token + "." + _jwkThumbprint(publicJwk);
1051
+ var digest = crypto.createHash("sha256").update(keyAuth, "utf8").digest();
1052
+ return {
1053
+ name: "_" + label + "._acme-challenge." + opts2.identifier,
1054
+ value: _b64u(digest),
1055
+ ttl: typeof opts2.ttl === "number" ? Math.floor(opts2.ttl) : (C.TIME.minutes(1) / C.TIME.seconds(1)),
1056
+ };
1057
+ }
1058
+
899
1059
  return Object.freeze({
900
1060
  fetchDirectory: fetchDirectory,
901
1061
  newAccount: newAccount,
@@ -908,6 +1068,8 @@ function create(opts) {
908
1068
  accountKeyRollover: accountKeyRollover,
909
1069
  deactivateAccount: deactivateAccount,
910
1070
  tlsAlpn01KeyAuthorization: tlsAlpn01KeyAuthorization,
1071
+ listProfiles: listProfiles,
1072
+ dnsAccount01ChallengeRecord: dnsAccount01ChallengeRecord,
911
1073
  accountUrl: function () { return state.accountUrl; },
912
1074
  directory: function () { return state.directory; },
913
1075
  publicJwk: function () { return Object.assign({}, publicJwk); },
@@ -955,6 +1117,28 @@ function _sleep(ms) {
955
1117
  });
956
1118
  }
957
1119
 
1120
+ // RFC 4648 §6 base32 lowercase (no padding) — used by
1121
+ // draft-ietf-acme-dns-account-label to derive the 80-bit account label
1122
+ // from SHA-256(accountUrl). 5-bit groups MSB-first.
1123
+ function _base32lc(buf) {
1124
+ var alphabet = "abcdefghijklmnopqrstuvwxyz234567";
1125
+ var out = "";
1126
+ var bits = 0;
1127
+ var value = 0;
1128
+ for (var i = 0; i < buf.length; i += 1) {
1129
+ value = (value << 8) | buf[i]; // allow:raw-byte-literal — bit-shift count, byte boundary
1130
+ bits += 8; // allow:raw-byte-literal — bits-per-byte constant
1131
+ while (bits >= 5) {
1132
+ out += alphabet[(value >>> (bits - 5)) & 31];
1133
+ bits -= 5;
1134
+ }
1135
+ }
1136
+ if (bits > 0) {
1137
+ out += alphabet[(value << (5 - bits)) & 31];
1138
+ }
1139
+ return out;
1140
+ }
1141
+
958
1142
  module.exports = {
959
1143
  create: create,
960
1144
  AcmeError: AcmeError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.8.82",
3
+ "version": "0.8.83",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.6",
5
- "serialNumber": "urn:uuid:309a8ed5-6be3-41c5-b29a-f23cdc9a41ca",
5
+ "serialNumber": "urn:uuid:eac2848d-d02f-43f8-9152-ffdf0f6a1cba",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-11T15:08:03.856Z",
8
+ "timestamp": "2026-05-11T15:13:25.519Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.8.82",
22
+ "bom-ref": "@blamejs/core@0.8.83",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.8.82",
25
+ "version": "0.8.83",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.8.82",
29
+ "purl": "pkg:npm/%40blamejs/core@0.8.83",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.8.82",
57
+ "ref": "@blamejs/core@0.8.83",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]