@blamejs/core 0.7.77 → 0.7.79
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/index.js +2 -0
- package/lib/cloud-events.js +210 -0
- package/lib/db.js +22 -0
- package/lib/external-db-migrate.js +9 -1
- package/lib/network-tls.js +75 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.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.7.x
|
|
10
10
|
|
|
11
|
+
- **0.7.79** (2026-05-06) — PQC TLS handshake key shares + DB hardening sweep (3 items). **`b.network.tls.pqc`** — operator-facing TLS 1.3 key-share configuration. The framework's app-layer envelope has been PQC-first since v0.7.28 (ML-KEM-1024 + X25519 hybrid for sealed records); this slice extends PQC posture down to the **TLS handshake itself**. `b.network.tls.pqc.setKeyShares(["X25519MLKEM768", "X25519", "secp256r1"])` configures the TLS 1.3 key-share groups the framework's `https.Server` / `https.Agent` advertises. The first listed group is the operator priority; the peer picks the first mutually supported entry. **`X25519MLKEM768`** is the IETF draft-kwiatkowski-tls-ecdhe-mlkem-02 hybrid KEM that negotiates post-quantum + classical in one handshake — forward-secrecy survives both classical-CRQC and future quantum cryptanalysis. Default list is `["X25519MLKEM768", "X25519", "secp256r1"]` so the framework attempts hybrid first, falls back to classical X25519 with peers that don't support the hybrid (most of the public web today), and to `secp256r1` for legacy peers. **`b.network.tls.applyToContext({ base })`** now threads the configured key-share list through as the `groups` option to Node's TLS context (operators who explicitly set `groups` in their `base` config keep the override). Operators wanting classical-only call `b.network.tls.pqc.setKeyShares(["X25519"])`; calling `.resetKeyShares()` restores the default. Requires Node 24+ with OpenSSL 3.5+ for the X25519MLKEM768 group; older Node falls back silently to the classical entries. **DB hardening: PRAGMA integrity_check at boot** — `b.db.init` now runs `PRAGMA integrity_check` after the existing PRAGMA block and refuses boot if SQLite reports anything other than `"ok"`. Catches B-tree corruption at boot rather than letting it surface mid-query when the engine stumbles on a bad page. Skip via `opts.skipIntegrityCheck: true` for tmpfs-only fixtures (audited reason). **DB hardening: migration-lock holder ID boot token** — `lib/external-db-migrate.js:_lockHolderId()` now appends a per-process random 8-byte boot token, so a recycled PID-on-hostname slot after a container restart can't be misattributed back to the new boot when reading stale lock rows. Closes the PID-reuse-across-container-restart concurrent-ownership window flagged in the v0.7.67 audit batch.
|
|
12
|
+
|
|
13
|
+
- **0.7.78** (2026-05-06) — `b.cloudEvents.wrap` / `.parse` — CloudEvents 1.0 envelope (cloudevents.io/spec/v1.0). Vendor-neutral event-format spec adopted by AWS EventBridge, Knative, Azure Event Grid, Google Eventarc, Datadog, and the broader CNCF event ecosystem; operators wrap outbound events from webhook / pubsub / queue boundaries to interop with these consumers without each consumer learning a bespoke shape. **`b.cloudEvents.wrap({ source, type, data?, subject?, time?, id?, datacontenttype?, dataschema?, extensions? })`** produces a CloudEvents 1.0 envelope: required attributes (`id` auto-minted as RFC 4122 v4 UUID when omitted, `source`, `specversion="1.0"`, `type`, `time` auto-set to `new Date().toISOString()`), optional attributes (`subject`, `datacontenttype` auto-set to `"application/json"` when `data` is JSON-serializable or `"application/octet-stream"` when `data` is a `Buffer` — base64-encoded into `data_base64`, `dataschema`), plus operator-defined extension attributes that conform to the §3.1 naming rules (lowercase ASCII alnum, 1-20 chars). **`b.cloudEvents.parse(envelope)`** validates the envelope shape and returns a structured form with `extensions` surfaced as a separate object so consumers can route on operator-defined fields without grepping the envelope. Refuses `data` + `data_base64` together (CloudEvents §3.1.1), unsupported specversion, missing required attributes, malformed extension names. **Test cleanup**: the `testAuditSafeEmitRedacts` smoke fixture from v0.7.75 now registers the `test` audit namespace before emitting so the audit handler's noise log line ("namespace 'test' is not registered") doesn't appear in CI smoke output.
|
|
14
|
+
|
|
11
15
|
- **0.7.77** (2026-05-06) — Argon2 switched from vendored prebuilds to Node's built-in `crypto.argon2*` (Node 24+). The framework's `lib/vendor/argon2/` directory (with the `argon2.cjs` bundle and the `prebuilds/` tree of platform-specific `.glibc.node` / `.musl.node` artifacts for darwin-arm64 / darwin-x64 / freebsd-arm64 / freebsd-x64 / linux-arm / linux-arm64 / linux-x64 / win32-x64) is **deleted**. New `lib/argon2-builtin.js` is a thin wrapper over `crypto.argon2Sync` that produces and parses the PHC string format (`$argon2id$v=19$m=...,t=...,p=...$<salt>$<hash>`). Wire-format compatibility preserved: existing rows in operator databases continue to verify. Behavior preserved: `b.auth.password.hash` / `.verify` / `.needsRehash` retain their async signatures and return shapes; `b.vault.wrap` and `b.backupCrypto.deriveKey` use the same `raw: true` path for raw-bytes output. Operators wanting to supply their own argon2 implementation (pinned upstream, hardware-accelerated, etc.) override at the call site via `opts.argon2` — the supplied object MUST expose the same `hash` / `verify` / `needsRehash` shape. Drops ~440 KB of platform-specific native prebuilds from the repository and shipped npm tarball; eliminates a supply-chain hop. The vendor manifest's `argon2` entry is removed; `scripts/vendor-update.sh argon2` now refuses with a pointer to `lib/argon2-builtin.js`.
|
|
12
16
|
|
|
13
17
|
- **0.7.76** (2026-05-06) — CVE-class web hardening sweep — Trojan Source (CVE-2021-42574) log defense + drive-by-MIME attachment opt for `staticServe`. **Trojan Source defense** in `b.log` output: every log line now post-processes Unicode bidi / format-control characters (U+061C / U+200E-200F / U+202A-202E / U+2066-2069) into their `\uXXXX` literal escape on the wire so a hostile log message can't re-order the visible line in a TTY / syslog / file reader. JSON.stringify alone does NOT escape these codepoints, so a captured-error message containing `U+202E` (RIGHT-TO-LEFT OVERRIDE) survives into the log surface and silently flips visible field/key associations. The escape applies to the entire serialized JSON line including any extras / bound-context fields. **`staticServe.create({ safeAttachmentForRiskyMimes: true })`** — opt-in flag that adds `Content-Disposition: attachment` to responses whose Content-Type is in the risky-inline-MIME set (`text/html`, `text/xml`, `application/xml`, `application/xhtml+xml`, `image/svg+xml`, `application/javascript`, `text/javascript`, `application/x-javascript`). Defends against drive-by execution of user-uploaded HTML / JS / SVG (CVE-2017-15012 SVG XSS / CVE-2009-1312 HTML drive-by class). Default `false` — operators serving framework asset bundles continue to render inline; operators serving user-content directories opt in. Filename is RFC 5987-encoded (ASCII filename + `filename*=UTF-8''...`) so non-ASCII filenames survive without allowing CR/LF header injection.
|
package/index.js
CHANGED
|
@@ -211,6 +211,7 @@ var fileUpload = require("./lib/file-upload");
|
|
|
211
211
|
var dualControl = require("./lib/dual-control");
|
|
212
212
|
var retention = require("./lib/retention");
|
|
213
213
|
var network = require("./lib/network");
|
|
214
|
+
var cloudEvents = require("./lib/cloud-events");
|
|
214
215
|
|
|
215
216
|
module.exports = {
|
|
216
217
|
crypto: crypto,
|
|
@@ -357,6 +358,7 @@ module.exports = {
|
|
|
357
358
|
dualControl: dualControl,
|
|
358
359
|
retention: retention,
|
|
359
360
|
network: network,
|
|
361
|
+
cloudEvents: cloudEvents,
|
|
360
362
|
ntpCheck: ntpCheck,
|
|
361
363
|
version: constants.version,
|
|
362
364
|
};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CloudEvents 1.0 envelope (cloudevents.io/spec/v1.0).
|
|
4
|
+
*
|
|
5
|
+
* A vendor-neutral event-format spec adopted by AWS EventBridge,
|
|
6
|
+
* Knative, Azure Event Grid, Google Eventarc, Datadog, and the
|
|
7
|
+
* CNCF event ecosystem. Operators wrap outbound events from
|
|
8
|
+
* webhook / pubsub / queue boundaries to interop with these
|
|
9
|
+
* consumers without each consumer having to learn a bespoke shape.
|
|
10
|
+
*
|
|
11
|
+
* var ce = b.cloudEvents.wrap({
|
|
12
|
+
* source: "/services/orders",
|
|
13
|
+
* type: "com.example.order.created",
|
|
14
|
+
* subject: "order/o-1234",
|
|
15
|
+
* data: { id: "o-1234", total: 4250 },
|
|
16
|
+
* });
|
|
17
|
+
* // → {
|
|
18
|
+
* // specversion: "1.0",
|
|
19
|
+
* // id: "<auto-uuid-v4>",
|
|
20
|
+
* // source: "/services/orders",
|
|
21
|
+
* // type: "com.example.order.created",
|
|
22
|
+
* // time: "2026-05-06T...",
|
|
23
|
+
* // subject: "order/o-1234",
|
|
24
|
+
* // datacontenttype: "application/json",
|
|
25
|
+
* // data: { id: "o-1234", total: 4250 },
|
|
26
|
+
* // }
|
|
27
|
+
*
|
|
28
|
+
* var ce = b.cloudEvents.parse(envelope); // throws on shape violation
|
|
29
|
+
*
|
|
30
|
+
* Spec compliance — REQUIRED attributes (CloudEvents §3.1):
|
|
31
|
+
* id, source, specversion, type
|
|
32
|
+
*
|
|
33
|
+
* OPTIONAL attributes:
|
|
34
|
+
* datacontenttype, dataschema, subject, time, data, data_base64
|
|
35
|
+
*
|
|
36
|
+
* Operator-defined extension attributes are passed through unchanged
|
|
37
|
+
* if they conform to the spec's naming rules (lowercase ASCII letters
|
|
38
|
+
* + digits, length 1–20).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
var nodeCrypto = require("crypto");
|
|
42
|
+
var validateOpts = require("./validate-opts");
|
|
43
|
+
var { defineClass } = require("./framework-error");
|
|
44
|
+
|
|
45
|
+
var CloudEventsError = defineClass("CloudEventsError", { alwaysPermanent: true });
|
|
46
|
+
|
|
47
|
+
var SPECVERSION = "1.0";
|
|
48
|
+
|
|
49
|
+
// CloudEvents §3.1 — required string attributes.
|
|
50
|
+
var REQUIRED_ATTRS = ["id", "source", "specversion", "type"];
|
|
51
|
+
|
|
52
|
+
// CloudEvents §3.1 — known optional attributes (other strings get
|
|
53
|
+
// passed through as extension attributes if they conform to the
|
|
54
|
+
// naming rules).
|
|
55
|
+
var KNOWN_OPTIONAL_ATTRS = {
|
|
56
|
+
datacontenttype: 1,
|
|
57
|
+
dataschema: 1,
|
|
58
|
+
subject: 1,
|
|
59
|
+
time: 1,
|
|
60
|
+
data: 1,
|
|
61
|
+
data_base64: 1,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// CloudEvents §3.1 attribute naming — lowercase letters + digits,
|
|
65
|
+
// length 1-20.
|
|
66
|
+
var EXT_ATTR_NAME_RE = /^[a-z0-9]{1,20}$/;
|
|
67
|
+
|
|
68
|
+
function _isoNow() { return new Date().toISOString(); }
|
|
69
|
+
|
|
70
|
+
function _genId() {
|
|
71
|
+
// RFC 4122 v4 UUID — 16 random bytes with version + variant bits.
|
|
72
|
+
return nodeCrypto.randomUUID();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---- wrap ----
|
|
76
|
+
|
|
77
|
+
function wrap(opts) {
|
|
78
|
+
validateOpts.requireObject(opts, "cloudEvents.wrap", CloudEventsError);
|
|
79
|
+
validateOpts.requireNonEmptyString(opts.source,
|
|
80
|
+
"cloudEvents.wrap: source", CloudEventsError, "cloud-events/bad-source");
|
|
81
|
+
validateOpts.requireNonEmptyString(opts.type,
|
|
82
|
+
"cloudEvents.wrap: type", CloudEventsError, "cloud-events/bad-type");
|
|
83
|
+
validateOpts.optionalNonEmptyString(opts.id,
|
|
84
|
+
"cloudEvents.wrap: id", CloudEventsError, "cloud-events/bad-id");
|
|
85
|
+
validateOpts.optionalNonEmptyString(opts.subject,
|
|
86
|
+
"cloudEvents.wrap: subject", CloudEventsError, "cloud-events/bad-subject");
|
|
87
|
+
validateOpts.optionalNonEmptyString(opts.time,
|
|
88
|
+
"cloudEvents.wrap: time", CloudEventsError, "cloud-events/bad-time");
|
|
89
|
+
validateOpts.optionalNonEmptyString(opts.datacontenttype,
|
|
90
|
+
"cloudEvents.wrap: datacontenttype", CloudEventsError, "cloud-events/bad-datacontenttype");
|
|
91
|
+
validateOpts.optionalNonEmptyString(opts.dataschema,
|
|
92
|
+
"cloudEvents.wrap: dataschema", CloudEventsError, "cloud-events/bad-dataschema");
|
|
93
|
+
|
|
94
|
+
var out = {
|
|
95
|
+
specversion: SPECVERSION,
|
|
96
|
+
id: opts.id || _genId(),
|
|
97
|
+
source: opts.source,
|
|
98
|
+
type: opts.type,
|
|
99
|
+
time: opts.time || _isoNow(),
|
|
100
|
+
};
|
|
101
|
+
if (opts.subject !== undefined && opts.subject !== null) out.subject = opts.subject;
|
|
102
|
+
if (opts.dataschema !== undefined && opts.dataschema !== null) out.dataschema = opts.dataschema;
|
|
103
|
+
|
|
104
|
+
// data — choose JSON vs binary based on Buffer-ness; auto-set
|
|
105
|
+
// datacontenttype when caller doesn't supply one.
|
|
106
|
+
if (opts.data !== undefined) {
|
|
107
|
+
if (Buffer.isBuffer(opts.data)) {
|
|
108
|
+
out.data_base64 = opts.data.toString("base64");
|
|
109
|
+
out.datacontenttype = opts.datacontenttype || "application/octet-stream";
|
|
110
|
+
} else {
|
|
111
|
+
out.data = opts.data;
|
|
112
|
+
out.datacontenttype = opts.datacontenttype || "application/json";
|
|
113
|
+
}
|
|
114
|
+
} else if (opts.datacontenttype) {
|
|
115
|
+
out.datacontenttype = opts.datacontenttype;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Extension attributes — operator-defined, must conform to the
|
|
119
|
+
// §3.1 naming rules (lowercase ASCII alnum, 1-20 chars).
|
|
120
|
+
if (opts.extensions !== undefined && opts.extensions !== null) {
|
|
121
|
+
validateOpts.optionalPlainObject(opts.extensions,
|
|
122
|
+
"cloudEvents.wrap: extensions", CloudEventsError, "cloud-events/bad-extensions");
|
|
123
|
+
var extKeys = Object.keys(opts.extensions);
|
|
124
|
+
for (var i = 0; i < extKeys.length; i += 1) {
|
|
125
|
+
var k = extKeys[i];
|
|
126
|
+
// bound BEFORE regex test — k.length > 0 && k.length <= 20
|
|
127
|
+
if (typeof k !== "string" || k.length === 0 || k.length > 20 || !EXT_ATTR_NAME_RE.test(k)) {
|
|
128
|
+
throw new CloudEventsError("cloud-events/bad-extension-name",
|
|
129
|
+
"cloudEvents.wrap: extension '" + k + "' must match [a-z0-9]{1,20}");
|
|
130
|
+
}
|
|
131
|
+
if (REQUIRED_ATTRS.indexOf(k) !== -1 || KNOWN_OPTIONAL_ATTRS[k]) {
|
|
132
|
+
throw new CloudEventsError("cloud-events/extension-conflicts-with-spec",
|
|
133
|
+
"cloudEvents.wrap: extension '" + k + "' conflicts with a spec attribute");
|
|
134
|
+
}
|
|
135
|
+
out[k] = opts.extensions[k];
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ---- parse ----
|
|
142
|
+
|
|
143
|
+
function parse(envelope) {
|
|
144
|
+
if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)) {
|
|
145
|
+
throw new CloudEventsError("cloud-events/bad-envelope",
|
|
146
|
+
"cloudEvents.parse: envelope must be a plain object");
|
|
147
|
+
}
|
|
148
|
+
for (var i = 0; i < REQUIRED_ATTRS.length; i += 1) {
|
|
149
|
+
var k = REQUIRED_ATTRS[i];
|
|
150
|
+
if (typeof envelope[k] !== "string" || envelope[k].length === 0) {
|
|
151
|
+
throw new CloudEventsError("cloud-events/missing-required",
|
|
152
|
+
"cloudEvents.parse: required attribute '" + k + "' missing or empty (CloudEvents §3.1)");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (envelope.specversion !== SPECVERSION) {
|
|
156
|
+
throw new CloudEventsError("cloud-events/unsupported-specversion",
|
|
157
|
+
"cloudEvents.parse: specversion='" + envelope.specversion +
|
|
158
|
+
"' is not supported (this primitive implements CloudEvents 1.0)");
|
|
159
|
+
}
|
|
160
|
+
if (envelope.data !== undefined && envelope.data_base64 !== undefined) {
|
|
161
|
+
throw new CloudEventsError("cloud-events/data-conflict",
|
|
162
|
+
"cloudEvents.parse: envelope has both 'data' and 'data_base64' (CloudEvents §3.1.1)");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Decode binary data if the envelope used base64 mode.
|
|
166
|
+
var decodedData = envelope.data;
|
|
167
|
+
if (envelope.data_base64 !== undefined) {
|
|
168
|
+
if (typeof envelope.data_base64 !== "string") {
|
|
169
|
+
throw new CloudEventsError("cloud-events/bad-data-base64",
|
|
170
|
+
"cloudEvents.parse: data_base64 must be a string");
|
|
171
|
+
}
|
|
172
|
+
try { decodedData = Buffer.from(envelope.data_base64, "base64"); }
|
|
173
|
+
catch (e) {
|
|
174
|
+
throw new CloudEventsError("cloud-events/bad-data-base64",
|
|
175
|
+
"cloudEvents.parse: data_base64 decode failed: " + ((e && e.message) || String(e)));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Surface extension attributes separately so consumers can route on
|
|
180
|
+
// operator-defined fields without grepping the envelope.
|
|
181
|
+
var extensions = {};
|
|
182
|
+
var keys = Object.keys(envelope);
|
|
183
|
+
for (var j = 0; j < keys.length; j += 1) {
|
|
184
|
+
var key = keys[j];
|
|
185
|
+
if (REQUIRED_ATTRS.indexOf(key) !== -1) continue;
|
|
186
|
+
if (KNOWN_OPTIONAL_ATTRS[key]) continue;
|
|
187
|
+
extensions[key] = envelope[key];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
specversion: envelope.specversion,
|
|
192
|
+
id: envelope.id,
|
|
193
|
+
source: envelope.source,
|
|
194
|
+
type: envelope.type,
|
|
195
|
+
time: envelope.time || null,
|
|
196
|
+
subject: envelope.subject || null,
|
|
197
|
+
datacontenttype: envelope.datacontenttype || null,
|
|
198
|
+
dataschema: envelope.dataschema || null,
|
|
199
|
+
data: decodedData,
|
|
200
|
+
extensions: extensions,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
wrap: wrap,
|
|
206
|
+
parse: parse,
|
|
207
|
+
SPECVERSION: SPECVERSION,
|
|
208
|
+
REQUIRED_ATTRS: REQUIRED_ATTRS,
|
|
209
|
+
CloudEventsError: CloudEventsError,
|
|
210
|
+
};
|
package/lib/db.js
CHANGED
|
@@ -681,6 +681,28 @@ async function init(opts) {
|
|
|
681
681
|
// structured `foreignKeys` declarations actually constrain writes.
|
|
682
682
|
runSql(database, "PRAGMA foreign_keys=ON");
|
|
683
683
|
|
|
684
|
+
// PRAGMA integrity_check — refuse boot on B-tree corruption (per
|
|
685
|
+
// audit-batch finding). SQLite returns "ok" for a healthy database;
|
|
686
|
+
// any other result means corruption. Catching it at boot beats
|
|
687
|
+
// stumbling on it later in a query that hits the bad page. Skip
|
|
688
|
+
// when opts.skipIntegrityCheck is set (e.g. tmpfs-only fixtures).
|
|
689
|
+
if (opts.skipIntegrityCheck !== true) {
|
|
690
|
+
var integrityRows = [];
|
|
691
|
+
try {
|
|
692
|
+
// .all-style read; runSql is for statements without rows.
|
|
693
|
+
integrityRows = database.prepare("PRAGMA integrity_check").all();
|
|
694
|
+
} catch (e) {
|
|
695
|
+
throw new DbError("db/integrity-check-failed",
|
|
696
|
+
"PRAGMA integrity_check failed at boot: " + ((e && e.message) || String(e)));
|
|
697
|
+
}
|
|
698
|
+
if (integrityRows.length !== 1 ||
|
|
699
|
+
!integrityRows[0] || integrityRows[0].integrity_check !== "ok") {
|
|
700
|
+
throw new DbError("db/integrity-check-failed",
|
|
701
|
+
"PRAGMA integrity_check reported corruption: " +
|
|
702
|
+
JSON.stringify(integrityRows));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
684
706
|
// Refuse app schema entries that collide with framework-reserved names
|
|
685
707
|
for (var ri = 0; ri < opts.schema.length; ri++) {
|
|
686
708
|
if (RESERVED_TABLE_NAMES.has(opts.schema[ri].name)) {
|
|
@@ -86,8 +86,16 @@ function _err(code, message) {
|
|
|
86
86
|
return new ExternalDbMigrateError(code, message);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// Boot-token suffix ensures _lockHolderId() is unique across container
|
|
90
|
+
// restarts even if the OS recycles a PID into the same hostname slot —
|
|
91
|
+
// without it, a stolen-and-released migration lock could be wrongly
|
|
92
|
+
// attributed back to the new boot. The token is process-scoped so
|
|
93
|
+
// every replica picks a fresh one at module load.
|
|
94
|
+
var _BOOT_TOKEN = require("node:crypto").randomBytes(8).toString("hex"); // allow:raw-byte-literal — boot-id token entropy
|
|
95
|
+
|
|
89
96
|
function _lockHolderId() {
|
|
90
|
-
return String(process.pid) + "@" +
|
|
97
|
+
return String(process.pid) + "@" +
|
|
98
|
+
(require("node:os").hostname() || "unknown") + "@" + _BOOT_TOKEN;
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
async function _ensureTrackingTable(xdb) {
|
package/lib/network-tls.js
CHANGED
|
@@ -17,10 +17,15 @@ var observability = lazyRequire(function () { return require("./observability");
|
|
|
17
17
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
18
18
|
var asn1 = require("./asn1-der");
|
|
19
19
|
|
|
20
|
+
// STATE.tlsKeyShares is initialized to the default PQC group list at
|
|
21
|
+
// module load — operator setKeyShares() overrides; resetKeyShares()
|
|
22
|
+
// restores the default. Empty array means "fall back to Node's TLS
|
|
23
|
+
// default groups" (operator opt-out).
|
|
20
24
|
var STATE = {
|
|
21
25
|
cas: [],
|
|
22
26
|
systemTrust: false,
|
|
23
27
|
baselineFingerprints: null,
|
|
28
|
+
tlsKeyShares: ["X25519MLKEM768", "X25519", "secp256r1"],
|
|
24
29
|
};
|
|
25
30
|
|
|
26
31
|
function _normalizePem(pem) {
|
|
@@ -267,9 +272,77 @@ function applyToContext(opts) {
|
|
|
267
272
|
}
|
|
268
273
|
}
|
|
269
274
|
if (caStrings.length > 0) base.ca = caStrings;
|
|
275
|
+
// PQC TLS handshake — apply the operator-configured key-share groups
|
|
276
|
+
// (default ["X25519MLKEM768", "X25519"]) so https.Server / https.Agent
|
|
277
|
+
// negotiate the hybrid KEM with peers that support it and fall back
|
|
278
|
+
// to classical X25519 with peers that don't. Operators who explicitly
|
|
279
|
+
// pass `groups` in their base config keep the override.
|
|
280
|
+
if (base.groups === undefined && STATE.tlsKeyShares.length > 0) {
|
|
281
|
+
base.groups = STATE.tlsKeyShares.join(":");
|
|
282
|
+
}
|
|
270
283
|
return base;
|
|
271
284
|
}
|
|
272
285
|
|
|
286
|
+
// ---- PQC TLS key shares (RFC draft-ietf-tls-hybrid-design) ----
|
|
287
|
+
//
|
|
288
|
+
// b.network.tls.pqc.setKeyShares(["X25519MLKEM768", "X25519"]) — set the
|
|
289
|
+
// TLS 1.3 key-share groups the framework's https.Server / https.Agent
|
|
290
|
+
// will advertise. The first listed group is the priority; the peer
|
|
291
|
+
// picks the first mutually supported entry. Hybrid groups
|
|
292
|
+
// (X25519MLKEM768) negotiate post-quantum + classical in one
|
|
293
|
+
// handshake so forward-secrecy survives both classical-CRQC and
|
|
294
|
+
// future quantum cryptanalysis.
|
|
295
|
+
//
|
|
296
|
+
// getKeyShares() → string[] (current)
|
|
297
|
+
// setKeyShares(["X25519MLKEM768", "X25519"]) → string[] (after)
|
|
298
|
+
// resetKeyShares() → restores default
|
|
299
|
+
|
|
300
|
+
var DEFAULT_PQC_KEY_SHARES = Object.freeze([
|
|
301
|
+
"X25519MLKEM768", // hybrid KEM, draft-kwiatkowski-tls-ecdhe-mlkem-02
|
|
302
|
+
"X25519", // classical fallback
|
|
303
|
+
"secp256r1", // legacy peers
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
function _validateKeyShare(name) {
|
|
307
|
+
if (typeof name !== "string" || name.length === 0 || name.length > C.BYTES.bytes(64)) { // bound
|
|
308
|
+
throw new TlsTrustError("tls/bad-key-share",
|
|
309
|
+
"tls.pqc.setKeyShares: each entry must be a non-empty string up to 64 chars");
|
|
310
|
+
}
|
|
311
|
+
// RFC draft-ietf-tls-hybrid-design + IANA TLS Group Registry only
|
|
312
|
+
// emit alphanumeric + underscore identifiers. Refuse `:` (the join
|
|
313
|
+
// separator) outright so an operator can't smuggle a second entry
|
|
314
|
+
// through one slot.
|
|
315
|
+
if (!/^[A-Za-z0-9_]+$/.test(name)) {
|
|
316
|
+
throw new TlsTrustError("tls/bad-key-share",
|
|
317
|
+
"tls.pqc.setKeyShares: '" + name + "' has illegal characters " +
|
|
318
|
+
"(must match [A-Za-z0-9_]+)");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function setKeyShares(list) {
|
|
323
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
324
|
+
throw new TlsTrustError("tls/bad-key-shares",
|
|
325
|
+
"tls.pqc.setKeyShares: must be a non-empty array of group names");
|
|
326
|
+
}
|
|
327
|
+
for (var i = 0; i < list.length; i += 1) _validateKeyShare(list[i]);
|
|
328
|
+
STATE.tlsKeyShares = list.slice();
|
|
329
|
+
return getKeyShares();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function getKeyShares() { return STATE.tlsKeyShares.slice(); }
|
|
333
|
+
|
|
334
|
+
function resetKeyShares() {
|
|
335
|
+
STATE.tlsKeyShares = DEFAULT_PQC_KEY_SHARES.slice();
|
|
336
|
+
return getKeyShares();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
var pqc = Object.freeze({
|
|
340
|
+
setKeyShares: setKeyShares,
|
|
341
|
+
getKeyShares: getKeyShares,
|
|
342
|
+
resetKeyShares: resetKeyShares,
|
|
343
|
+
DEFAULT_KEY_SHARES: DEFAULT_PQC_KEY_SHARES,
|
|
344
|
+
});
|
|
345
|
+
|
|
273
346
|
function getCaPems() {
|
|
274
347
|
return STATE.cas.map(function (e) { return e.pem; });
|
|
275
348
|
}
|
|
@@ -307,6 +380,7 @@ function _resetForTest() {
|
|
|
307
380
|
STATE.cas = [];
|
|
308
381
|
STATE.systemTrust = false;
|
|
309
382
|
STATE.baselineFingerprints = null;
|
|
383
|
+
STATE.tlsKeyShares = DEFAULT_PQC_KEY_SHARES.slice();
|
|
310
384
|
}
|
|
311
385
|
|
|
312
386
|
// ---- OCSP / OCSP-stapling wrappers around node:tls ----------------
|
|
@@ -1492,6 +1566,7 @@ module.exports = {
|
|
|
1492
1566
|
getCaPems: getCaPems,
|
|
1493
1567
|
ocsp: ocsp,
|
|
1494
1568
|
ct: ct,
|
|
1569
|
+
pqc: pqc,
|
|
1495
1570
|
TlsTrustError: TlsTrustError,
|
|
1496
1571
|
_resetForTest: _resetForTest,
|
|
1497
1572
|
};
|
package/package.json
CHANGED
package/sbom.cyclonedx.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.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:70a3e68a-1a5d-4506-ae41-38e159762dfe",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-06T05:26:16.435Z",
|
|
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.7.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.7.79",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.7.
|
|
25
|
+
"version": "0.7.79",
|
|
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.7.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.7.79",
|
|
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.7.
|
|
57
|
+
"ref": "@blamejs/core@0.7.79",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|