@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 +1 -0
- package/README.md +1 -1
- package/lib/acme.js +189 -5
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
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:
|
|
5
|
+
"serialNumber": "urn:uuid:eac2848d-d02f-43f8-9152-ffdf0f6a1cba",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-11T15:
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.8.83",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.8.83",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|