@blamejs/core 0.13.6 → 0.13.8
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/README.md +1 -1
- package/lib/ai-disclosure.js +2 -3
- package/lib/archive-gz.js +5 -3
- package/lib/archive-read.js +84 -35
- package/lib/archive-tar-read.js +86 -31
- package/lib/archive-tar.js +2 -3
- package/lib/backup/index.js +2 -3
- package/lib/cose.js +4 -3
- package/lib/mail-server-jmap.js +9 -9
- package/lib/mail-server-submission.js +3 -2
- package/lib/mdoc.js +14 -14
- package/lib/network-dnssec.js +10 -8
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.8 (2026-05-26) — **In-memory archive extraction for read-only / serverless filesystems.** Archive readers gain an in-memory extraction path so an uploaded archive can be opened and its contents read without writing anything to disk — the case a read-only or ephemeral serverless filesystem requires. b.archive.read.zip(...).extractEntries() and b.archive.read.tar(...).extractEntries() are async generators that yield each regular file entry as { name, bytes, size }, applying the same bomb-policy caps, b.guardArchive metadata cascade (which refuses a Zip-Slip / traversal archive wholesale), and entry-type-policy refusals as the disk extract() — only the disk realpath agreement check is omitted, since nothing is written and the caller owns where the returned bytes land. Directory and link entries carry no content and are not yielded. The guard cascade is factored into one shared path so disk and in-memory extraction refuse identically. Also a documentation fix: b.archive.gz no longer claims a b.archive.zip().toGzip() convenience exists — a ZIP is already DEFLATE-compressed per entry, so gzip-wrapping it gains nothing; gzip the uncompressed tar stream (the canonical .tar.gz) instead. **Added:** *`extractEntries()` — in-memory archive extraction (ZIP + tar)* — `b.archive.read.zip(source).extractEntries(opts?)` and `b.archive.read.tar(source).extractEntries(opts?)` are async generators yielding `{ name, bytes, size }` per regular file entry, never touching disk — for serverless / read-only filesystems where the disk `extract({ destination })` path cannot run. Same bomb-policy, guard-archive cascade, and entry-type-policy refusals as disk extraction; the bytes are byte-identical to what `extract()` writes. **Fixed:** *Removed the inaccurate `b.archive.zip().toGzip()` doc claim* — The `b.archive.gz` documentation described a `b.archive.zip().toGzip()` convenience method that does not (and should not) exist: a ZIP is already DEFLATE-compressed per entry, so gzip-wrapping it would compress already-compressed data for no benefit. `b.archive.tar().toGzip()` (the real `.tar.gz`) is unchanged.
|
|
12
|
+
|
|
13
|
+
- v0.13.7 (2026-05-26) — **Documentation accuracy — several primitives described shipped features as deferred.** A documentation sweep corrected primitive descriptions that still called features deferred after they shipped, so the wiki and inline docs now match the code. b.mdoc documents that device authentication (the ISO 18013-5 §9.1.3 signature variant, verifyDeviceAuth) is verified, not deferred — only the COSE_Mac0 device-auth variant remains refused. b.network.dns.dnssec documents that the root-to-zone chain walk against the IANA trust anchors (verifyChain) and NSEC / NSEC3 denial of existence (verifyDenial / nsec3Hash) ship, where the card previously said they were deferred. b.cose lists COSE_Mac0 and COSE_Encrypt0 among what it ships. The JMAP server documents its push channel, blob upload/download, and EmailSubmission handlers as present, and the submission server documents CHUNKING / BDAT as supported. A new test detector keeps this class of drift from recurring: it fails the build when a comment promises a feature lands in a version that has already shipped. **Fixed:** *Corrected `deferred`/`does-not-ship` docs for features that have shipped* — `b.mdoc` (device authentication, §9.1.3), `b.network.dns.dnssec` (chain walk + NSEC/NSEC3), `b.cose` (COSE_Mac0 + COSE_Encrypt0), the JMAP server (push, blob, EmailSubmission), and the submission server (BDAT/CHUNKING) all carried `@card`/`@intro` text describing shipped capabilities as deferred or not-shipped. The descriptions now match the implemented surface; genuinely-deferred items (the mdoc MAC variant, DNSSEC in-RDATA name canonicalization, COSE multi-signer/multi-recipient) remain documented as such. **Detectors:** *Overdue-defer detector in the codebase-pattern gate* — A new check fails the build when a comment promises a feature "lands in" / is "deferred to" / is "not supported in" a version that the package has already reached — catching stale deferral notes (a feature that shipped but whose comment still says otherwise, or a missed deadline) before they reach a release. An allowlist records the deliberate defer-with-condition exceptions.
|
|
14
|
+
|
|
11
15
|
- v0.13.6 (2026-05-26) — **`b.ai.frontierModelProtocol` — California SB 53 frontier-AI obligations.** b.ai.frontierModelProtocol assesses a developer's obligations under California's Transparency in Frontier Artificial Intelligence Act — SB 53, Cal. Bus. & Prof. Code §22757.10, effective 2026-01-01 — from a model's training compute and the developer's revenue. It reports whether the model crosses the frontier threshold (more than 10^26 training FLOPs), whether the developer is a large frontier developer (prior-year revenue, with affiliates, above $500M), and the resulting obligations: every frontier developer must report critical safety incidents and publish a transparency report, and a large frontier developer must additionally publish an annual safety framework and disclose its catastrophic-risk assessment. Passing a candidate safety framework reports which required elements (risk identification, mitigation, governance, cybersecurity, standards alignment) are missing. b.ai.frontierModelProtocol.incidentReport validates a critical-incident type against the Act's four categories and computes the notification deadline to the California Office of Emergency Services — 15 days from discovery, or 24 hours when there is an imminent risk of death or serious physical injury. The ca-tfaia compliance posture was already in the catalog. **Added:** *`b.ai.frontierModelProtocol` — SB 53 threshold classification, obligations, and incident reporting* — `b.ai.frontierModelProtocol({ trainingFlops, annualRevenueUsd, framework? })` returns `isFrontierModel`, `isLargeFrontierDeveloper`, the `obligations` list, and (when a framework is supplied) its `frameworkGaps`. `b.ai.frontierModelProtocol.incidentReport({ type, discoveredAt, imminentRiskToLife? })` builds a critical-safety-incident report with the California OES recipient and a `dueAt` / `deadlineHours` computed from the 15-day (or 24-hour imminent-risk) statutory window; `type` must be one of the four categories in `INCIDENT_TYPES`. Throws `FrontierProtocolError` on malformed input.
|
|
12
16
|
|
|
13
17
|
- v0.13.5 (2026-05-26) — **`b.ai.aedtBiasAudit` — NYC Local Law 144 bias audit.** b.ai.aedtBiasAudit computes the bias-audit figures New York City Local Law 144 requires before an Automated Employment Decision Tool may screen candidates (NYC Admin. Code §20-870 et seq.; DCWP rules 6 RCNY §5-300). Given the per-category counts an independent auditor collected — selected/total for a pass-fail tool, or scored-above-the-overall-median/total for a continuous-score tool — it returns the selection (or scoring) rate, the impact ratio (each group's rate divided by the most-selected group's rate), and an adverse-impact flag (impact ratio below the EEOC four-fifths threshold of 0.8) for every group, across the sex, race/ethnicity, and intersectional dimensions, plus the most-selected group per dimension and an overall flag. Categories under 2% of the audited data are marked excluded per DCWP discretion. It is a pure calculation that produces exactly the figures the annual published summary must contain — the law mandates the calculation, not any particular remediation. The relevant compliance postures (nyc-ll144, and ca-tfaia for California SB 53) were already in the catalog. **Added:** *`b.ai.aedtBiasAudit` — Local Law 144 selection/scoring rates and four-fifths impact ratios* — `b.ai.aedtBiasAudit({ type, metadata, categories, minCategoryShare? })` where `type` is `"selection"` (group entries `{ selected, total }`) or `"scoring"` (`{ scoredAboveMedian, total }`). Returns per-group rate, impact ratio, and `adverseImpact` flag across the `sex`, `raceEthnicity`, and `intersectional` dimensions, plus the most-selected group per dimension and an `anyAdverseImpact` summary. Categories below `minCategoryShare` (2% default) are excluded from the impact-ratio basis. Throws `AedtBiasAuditError` on malformed input.
|
package/README.md
CHANGED
|
@@ -228,7 +228,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
228
228
|
- **i18n** — CLDR plural rules, Accept-Language negotiation, Intl formatters, RTL (`b.i18n`)
|
|
229
229
|
- **CSV** — RFC 4180 with Excel formula-injection prevention (`b.csv`)
|
|
230
230
|
- **IDs + slugs** — RFC 9562 UUID v4 + v7 (`b.uuid`); URL-safe slugs (`b.slug`)
|
|
231
|
-
- **Time + archive** — TZ-aware datetime (`b.time`); ZIP creation + adversarial-safe read with bomb caps + path-traversal + LFH/CD-skew defense (`b.archive` + `b.archive.read.zip`); one-liner quarantine extraction (`b.safeArchive.extract`); fs / objectStore / http / buffer / trusted-stream adapter contract (`b.archive.adapters`)
|
|
231
|
+
- **Time + archive** — TZ-aware datetime (`b.time`); ZIP creation + adversarial-safe read with bomb caps + path-traversal + LFH/CD-skew defense (`b.archive` + `b.archive.read.zip`); one-liner quarantine extraction (`b.safeArchive.extract`); in-memory extraction with no disk write for read-only / serverless filesystems (`b.archive.read.zip(...).extractEntries()` / `.tar`); fs / objectStore / http / buffer / trusted-stream adapter contract (`b.archive.adapters`)
|
|
232
232
|
- **Pagination + forms** — HMAC-signed cursor pagination (`b.pagination`); HTML form rendering + validation + CSRF (`b.forms`)
|
|
233
233
|
|
|
234
234
|
### Production
|
package/lib/ai-disclosure.js
CHANGED
|
@@ -187,9 +187,8 @@ function chatbot(session, opts) {
|
|
|
187
187
|
* machine-readable manner. This primitive returns the disclosure
|
|
188
188
|
* payload (visible label + structured metadata) the operator wires
|
|
189
189
|
* into the encoder / response pipeline. C2PA manifest emission is
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* adapter consumes when it lands.
|
|
190
|
+
* handled by `b.contentCredentials`; this primitive supplies the label
|
|
191
|
+
* markup and the metadata schema that the C2PA adapter consumes.
|
|
193
192
|
*
|
|
194
193
|
* @opts
|
|
195
194
|
* contentType: "image" | "audio" | "video" | "text", // required
|
package/lib/archive-gz.js
CHANGED
|
@@ -47,9 +47,11 @@ function _isGzipMagic(buf) {
|
|
|
47
47
|
* `toAdapter(adapter)` / `digest()` — so gzip slots into the same
|
|
48
48
|
* downstream sinks (object-store + filesystem + http adapters).
|
|
49
49
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* `b.archive.tar().toGzip(adapter)` composes this primitive after
|
|
51
|
+
* materializing the tar bytes (the canonical `.tar.gz`). There is no
|
|
52
|
+
* `zip().toGzip()` — a ZIP is already DEFLATE-compressed per entry, so
|
|
53
|
+
* gzip-wrapping it would compress already-compressed data for no gain;
|
|
54
|
+
* gzip the uncompressed tar stream instead.
|
|
53
55
|
*
|
|
54
56
|
* @opts
|
|
55
57
|
* level: number, // 0-9, default 6 (zlib default).
|
package/lib/archive-read.js
CHANGED
|
@@ -546,21 +546,13 @@ function zip(adapter, opts) {
|
|
|
546
546
|
}
|
|
547
547
|
}
|
|
548
548
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
var
|
|
556
|
-
if (!nodeFs.existsSync(destination)) {
|
|
557
|
-
nodeFs.mkdirSync(destination, { recursive: true });
|
|
558
|
-
}
|
|
559
|
-
var loaded = await _loadCD();
|
|
560
|
-
_enforceBombPolicy(loaded.entries, bombPolicy);
|
|
561
|
-
// Compose b.guardArchive on the metadata pass — operators with a
|
|
562
|
-
// posture set declared via opts.guardProfile get the cascade.
|
|
563
|
-
var guardEntries = loaded.entries.map(function (e) {
|
|
549
|
+
// Run the b.guardArchive metadata cascade and refuse the whole archive on
|
|
550
|
+
// any critical issue. Shared by disk `extract` and in-memory `extractEntries`
|
|
551
|
+
// so both apply the identical posture-aware refusal. `auditAction` names the
|
|
552
|
+
// audit row for the refusal path.
|
|
553
|
+
function _assertGuardMetadata(loadedEntries, auditAction) {
|
|
554
|
+
if (opts.guardProfile === false) return;
|
|
555
|
+
var guardEntries = loadedEntries.map(function (e) {
|
|
564
556
|
return {
|
|
565
557
|
name: e.name,
|
|
566
558
|
size: e.uncompressedSize,
|
|
@@ -573,24 +565,80 @@ function zip(adapter, opts) {
|
|
|
573
565
|
attrs: { externalAttrs: e.externalAttrs },
|
|
574
566
|
};
|
|
575
567
|
});
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
568
|
+
var profile = opts.guardProfile || "balanced";
|
|
569
|
+
var guardResult = guardArchive().validateEntries(guardEntries, { profile: profile });
|
|
570
|
+
if (!guardResult || !Array.isArray(guardResult.issues) || guardResult.issues.length === 0) return;
|
|
571
|
+
var critical = guardResult.issues.filter(function (i) { return i.severity === "critical"; });
|
|
572
|
+
if (critical.length === 0) return;
|
|
573
|
+
_emitAudit(opts, auditAction, "refused", {
|
|
574
|
+
entries: loadedEntries.length,
|
|
575
|
+
issues: critical.map(function (i) { return i.ruleId; }),
|
|
576
|
+
});
|
|
577
|
+
throw new ArchiveReadError("archive-read/guard-refused",
|
|
578
|
+
"extract refused — " + critical.length + " critical guard issue(s): " +
|
|
579
|
+
critical.map(function (i) { return i.ruleId + " (" + i.snippet + ")"; }).join("; "));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// In-memory extraction: yields each file entry's decompressed bytes WITHOUT
|
|
583
|
+
// writing to disk — for read-only / serverless filesystems. Applies the same
|
|
584
|
+
// bomb-policy, guard cascade, entry-type policy, and per-entry filename
|
|
585
|
+
// safety as `extract`; directory entries are skipped (no bytes). The realpath
|
|
586
|
+
// agreement check is disk-specific and intentionally omitted (no extraction
|
|
587
|
+
// root); the caller owns where, if anywhere, the bytes land.
|
|
588
|
+
async function* extractEntries(extractOpts) {
|
|
589
|
+
extractOpts = extractOpts || {};
|
|
590
|
+
var loaded = await _loadCD();
|
|
591
|
+
_enforceBombPolicy(loaded.entries, bombPolicy);
|
|
592
|
+
_assertGuardMetadata(loaded.entries, "archive.read.extractEntries.refused");
|
|
593
|
+
var totalDecompressed = 0;
|
|
594
|
+
var yielded = 0;
|
|
595
|
+
for (var i = 0; i < loaded.entries.length; i += 1) {
|
|
596
|
+
var entry = loaded.entries[i];
|
|
597
|
+
if (entry.isEncrypted && !extractOpts.allowEncrypted) {
|
|
598
|
+
throw new ArchiveReadError("archive-read/encrypted-entry",
|
|
599
|
+
"entry " + JSON.stringify(entry.name) + " is encrypted — not decrypted on the in-memory path");
|
|
600
|
+
}
|
|
601
|
+
var typeRefusal = _enforceEntryTypePolicy(entry, entryTypePolicy);
|
|
602
|
+
if (typeRefusal) {
|
|
603
|
+
throw new ArchiveReadError("archive-read/entry-type-refused",
|
|
604
|
+
"entry " + JSON.stringify(entry.name) + " is a " + typeRefusal +
|
|
605
|
+
" — refused by entryTypePolicy");
|
|
606
|
+
}
|
|
607
|
+
if (_isDirectoryEntry(entry.name, entry.externalAttrs)) continue;
|
|
608
|
+
// Archive-level name threats (Zip-Slip traversal, etc.) are refused for
|
|
609
|
+
// the whole archive by the guardArchive cascade above. The caller owns
|
|
610
|
+
// final placement of the returned bytes; we deliberately do NOT apply the
|
|
611
|
+
// disk-write filename policy (shell-exec extensions / reserved names) here
|
|
612
|
+
// — nothing is written, and over-filtering would drop legitimate names.
|
|
613
|
+
var lfhResult = await _verifyLfhMatchesCd(adapter, entry);
|
|
614
|
+
var body = await _decompressEntry(adapter, entry, lfhResult.dataStart, bombPolicy);
|
|
615
|
+
totalDecompressed += body.length;
|
|
616
|
+
if (totalDecompressed > bombPolicy.maxTotalDecompressedBytes) {
|
|
617
|
+
throw new ArchiveReadError("archive-read/total-too-large",
|
|
618
|
+
"cumulative uncompressed=" + totalDecompressed +
|
|
619
|
+
" exceeds maxTotalDecompressedBytes during extractEntries");
|
|
592
620
|
}
|
|
621
|
+
yielded += 1;
|
|
622
|
+
yield { name: entry.name, bytes: body, size: body.length };
|
|
623
|
+
}
|
|
624
|
+
_emitAudit(opts, "archive.read.extractEntries.completed", "success", { entries: yielded });
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function extract(extractOpts) {
|
|
628
|
+
extractOpts = extractOpts || {};
|
|
629
|
+
if (typeof extractOpts.destination !== "string" || extractOpts.destination.length === 0) {
|
|
630
|
+
throw new ArchiveReadError("archive-read/no-destination",
|
|
631
|
+
"extract: opts.destination must be a non-empty string (target directory)");
|
|
593
632
|
}
|
|
633
|
+
var destination = nodePath.resolve(extractOpts.destination);
|
|
634
|
+
if (!nodeFs.existsSync(destination)) {
|
|
635
|
+
nodeFs.mkdirSync(destination, { recursive: true });
|
|
636
|
+
}
|
|
637
|
+
var loaded = await _loadCD();
|
|
638
|
+
_enforceBombPolicy(loaded.entries, bombPolicy);
|
|
639
|
+
// Compose b.guardArchive on the metadata pass — operators with a
|
|
640
|
+
// posture set declared via opts.guardProfile get the cascade.
|
|
641
|
+
_assertGuardMetadata(loaded.entries, "archive.read.extract.refused");
|
|
594
642
|
var written = [];
|
|
595
643
|
var bytesExtracted = 0;
|
|
596
644
|
var totalDecompressed = 0;
|
|
@@ -693,10 +741,11 @@ function zip(adapter, opts) {
|
|
|
693
741
|
}
|
|
694
742
|
|
|
695
743
|
return {
|
|
696
|
-
kind:
|
|
697
|
-
inspect:
|
|
698
|
-
entries:
|
|
699
|
-
extract:
|
|
744
|
+
kind: "zip-random-access",
|
|
745
|
+
inspect: inspect,
|
|
746
|
+
entries: entries,
|
|
747
|
+
extract: extract,
|
|
748
|
+
extractEntries: extractEntries,
|
|
700
749
|
};
|
|
701
750
|
}
|
|
702
751
|
|
package/lib/archive-tar-read.js
CHANGED
|
@@ -276,6 +276,87 @@ function tar(adapter, opts) {
|
|
|
276
276
|
});
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
+
// Shared b.guardArchive metadata cascade — disk `extract` + in-memory
|
|
280
|
+
// `extractEntries` refuse the whole archive identically on a critical issue.
|
|
281
|
+
function _assertGuardMetadata(entries, auditAction) {
|
|
282
|
+
if (opts.guardProfile === false) return;
|
|
283
|
+
var profile = opts.guardProfile || "balanced";
|
|
284
|
+
var guardEntries = entries.map(function (e) {
|
|
285
|
+
return {
|
|
286
|
+
name: e.name,
|
|
287
|
+
size: e.size,
|
|
288
|
+
compressedSize: e.size,
|
|
289
|
+
isSymlink: e.typeflag === TF_SYMLINK,
|
|
290
|
+
isHardlink: e.typeflag === TF_HARDLINK,
|
|
291
|
+
linkTarget: e.linkname,
|
|
292
|
+
isDirectory: e.typeflag === TF_DIRECTORY,
|
|
293
|
+
isEncrypted: false,
|
|
294
|
+
attrs: { mode: e.mode },
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
var guardResult = guardArchive().validateEntries(guardEntries, { profile: profile });
|
|
298
|
+
if (!guardResult || !Array.isArray(guardResult.issues)) return;
|
|
299
|
+
var critical = guardResult.issues.filter(function (i) { return i.severity === "critical"; });
|
|
300
|
+
if (critical.length === 0) return;
|
|
301
|
+
_emitAudit(opts, auditAction, "refused", {
|
|
302
|
+
entries: entries.length,
|
|
303
|
+
issues: critical.map(function (i) { return i.ruleId; }),
|
|
304
|
+
});
|
|
305
|
+
throw new TarError("archive-tar/guard-refused",
|
|
306
|
+
"extract refused — " + critical.length + " critical guard issue(s)");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// In-memory extraction: yields each regular file entry's bytes without
|
|
310
|
+
// writing to disk (read-only / serverless filesystems). Same guard cascade,
|
|
311
|
+
// type-policy refusals, filename safety, and bomb cap as `extract`; directory
|
|
312
|
+
// and (validated) link entries carry no content and are not yielded.
|
|
313
|
+
async function* extractEntries(extractOpts) {
|
|
314
|
+
extractOpts = extractOpts || {};
|
|
315
|
+
var allowDangerous = extractOpts.allowDangerous || {};
|
|
316
|
+
var walked = await _walk();
|
|
317
|
+
var entries = walked.entries;
|
|
318
|
+
var bytes = walked.bytes;
|
|
319
|
+
_assertGuardMetadata(entries, "archive.read.tar.extractEntries.refused");
|
|
320
|
+
var totalDecompressed = 0;
|
|
321
|
+
var yielded = 0;
|
|
322
|
+
for (var i = 0; i < entries.length; i += 1) {
|
|
323
|
+
var entry = entries[i];
|
|
324
|
+
var type = _classifyTypeflag(entry.typeflag);
|
|
325
|
+
if (type === "device" || type === "fifo" || type === "socket") {
|
|
326
|
+
throw new TarError("archive-tar/entry-type-refused",
|
|
327
|
+
"entry " + JSON.stringify(entry.name) + " is a " + type + " — refused unconditionally");
|
|
328
|
+
}
|
|
329
|
+
if (type === "symlink" && !(allowDangerous.symlinks || entryTypePolicy.symlinks)) {
|
|
330
|
+
throw new TarError("archive-tar/entry-type-refused",
|
|
331
|
+
"entry " + JSON.stringify(entry.name) + " is a symlink — refused by entryTypePolicy");
|
|
332
|
+
}
|
|
333
|
+
if (type === "hardlink" && !(allowDangerous.hardlinks || entryTypePolicy.hardlinks)) {
|
|
334
|
+
throw new TarError("archive-tar/entry-type-refused",
|
|
335
|
+
"entry " + JSON.stringify(entry.name) + " is a hardlink — refused by entryTypePolicy");
|
|
336
|
+
}
|
|
337
|
+
// Directories + (now-permitted) links carry no content bytes.
|
|
338
|
+
if (type === "directory" || type === "symlink" || type === "hardlink") continue;
|
|
339
|
+
// Archive-level name threats are refused by the guardArchive cascade
|
|
340
|
+
// above; the caller owns placement of the returned bytes, so the
|
|
341
|
+
// disk-write filename policy is intentionally not applied here.
|
|
342
|
+
// COPY the slice — bytes.subarray/slice shares the full collected-archive
|
|
343
|
+
// backing store, so a caller retaining one entry would pin the whole
|
|
344
|
+
// archive in memory, defeating the serverless memory goal. Buffer.from
|
|
345
|
+
// gives the entry its own backing store (matching the ZIP path, whose
|
|
346
|
+
// _decompressEntry already returns a fresh buffer).
|
|
347
|
+
var body = Buffer.from(bytes.subarray(entry._bodyStart, entry._bodyStart + entry.size));
|
|
348
|
+
totalDecompressed += body.length;
|
|
349
|
+
if (totalDecompressed > bombPolicy.maxTotalDecompressedBytes) {
|
|
350
|
+
throw new TarError("archive-tar/total-too-large",
|
|
351
|
+
"cumulative uncompressed=" + totalDecompressed +
|
|
352
|
+
" exceeds maxTotalDecompressedBytes during extractEntries");
|
|
353
|
+
}
|
|
354
|
+
yielded += 1;
|
|
355
|
+
yield { name: entry.name, bytes: body, size: body.length };
|
|
356
|
+
}
|
|
357
|
+
_emitAudit(opts, "archive.read.tar.extractEntries.completed", "success", { entries: yielded });
|
|
358
|
+
}
|
|
359
|
+
|
|
279
360
|
async function extract(extractOpts) {
|
|
280
361
|
extractOpts = extractOpts || {};
|
|
281
362
|
if (typeof extractOpts.destination !== "string" || extractOpts.destination.length === 0) {
|
|
@@ -290,34 +371,7 @@ function tar(adapter, opts) {
|
|
|
290
371
|
var walked = await _walk();
|
|
291
372
|
var entries = walked.entries;
|
|
292
373
|
var bytes = walked.bytes;
|
|
293
|
-
|
|
294
|
-
var profile = opts.guardProfile || "balanced";
|
|
295
|
-
var guardEntries = entries.map(function (e) {
|
|
296
|
-
return {
|
|
297
|
-
name: e.name,
|
|
298
|
-
size: e.size,
|
|
299
|
-
compressedSize: e.size,
|
|
300
|
-
isSymlink: e.typeflag === TF_SYMLINK,
|
|
301
|
-
isHardlink: e.typeflag === TF_HARDLINK,
|
|
302
|
-
linkTarget: e.linkname,
|
|
303
|
-
isDirectory: e.typeflag === TF_DIRECTORY,
|
|
304
|
-
isEncrypted: false,
|
|
305
|
-
attrs: { mode: e.mode },
|
|
306
|
-
};
|
|
307
|
-
});
|
|
308
|
-
var guardResult = guardArchive().validateEntries(guardEntries, { profile: profile });
|
|
309
|
-
if (guardResult && Array.isArray(guardResult.issues)) {
|
|
310
|
-
var critical = guardResult.issues.filter(function (i) { return i.severity === "critical"; });
|
|
311
|
-
if (critical.length > 0) {
|
|
312
|
-
_emitAudit(opts, "archive.read.tar.extract.refused", "refused", {
|
|
313
|
-
entries: entries.length,
|
|
314
|
-
issues: critical.map(function (i) { return i.ruleId; }),
|
|
315
|
-
});
|
|
316
|
-
throw new TarError("archive-tar/guard-refused",
|
|
317
|
-
"extract refused — " + critical.length + " critical guard issue(s)");
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
374
|
+
_assertGuardMetadata(entries, "archive.read.tar.extract.refused");
|
|
321
375
|
var written = [];
|
|
322
376
|
var bytesExtracted = 0;
|
|
323
377
|
var totalDecompressed = 0;
|
|
@@ -405,9 +459,10 @@ function tar(adapter, opts) {
|
|
|
405
459
|
}
|
|
406
460
|
|
|
407
461
|
return {
|
|
408
|
-
kind:
|
|
409
|
-
inspect:
|
|
410
|
-
extract:
|
|
462
|
+
kind: "tar-reader",
|
|
463
|
+
inspect: inspect,
|
|
464
|
+
extract: extract,
|
|
465
|
+
extractEntries: extractEntries,
|
|
411
466
|
};
|
|
412
467
|
}
|
|
413
468
|
|
package/lib/archive-tar.js
CHANGED
|
@@ -56,11 +56,10 @@
|
|
|
56
56
|
* - CVE-2024-12905 / CVE-2025-48387 (tar-fs path traversal).
|
|
57
57
|
* - CVE-2025-4138 / 4330 (Python tarfile data filter bypass).
|
|
58
58
|
*
|
|
59
|
-
* Out of scope
|
|
59
|
+
* Compression is via `b.archive.gz` composition (tar.gz). Out of scope
|
|
60
|
+
* (v1):
|
|
60
61
|
* - Sparse-file emission (read reconstructs them; write doesn't
|
|
61
62
|
* produce sparse).
|
|
62
|
-
* - Compression (tar.gz lands v0.12.9 via `b.archive.gz`
|
|
63
|
-
* composition).
|
|
64
63
|
* - BSD-tar extensions beyond pax.
|
|
65
64
|
*
|
|
66
65
|
* @card
|
package/lib/backup/index.js
CHANGED
|
@@ -1028,9 +1028,8 @@ module.exports = {
|
|
|
1028
1028
|
* Adapter-driven storage backend. Wraps the bundle directory's file
|
|
1029
1029
|
* tree into per-file key-value pairs routed through an operator-
|
|
1030
1030
|
* supplied byte-store adapter so backup bundles can land anywhere
|
|
1031
|
-
* that exposes the contract
|
|
1032
|
-
* folding
|
|
1033
|
-
* objectStore v0.12.11).
|
|
1031
|
+
* that exposes the contract: local fs (the default), tar / tar.gz
|
|
1032
|
+
* folding, and S3 / MinIO / Azure / GCS objectStore adapters.
|
|
1034
1033
|
*
|
|
1035
1034
|
* The adapter contract (small surface; an `fs` implementation is the
|
|
1036
1035
|
* default + ships in `lib/backup/_adapter-fs.js`):
|
package/lib/cose.js
CHANGED
|
@@ -41,9 +41,10 @@
|
|
|
41
41
|
* 2) listing a header label the verifier does not understand is
|
|
42
42
|
* refused (RFC 9052 §3.1) — a crit-bypass defense.
|
|
43
43
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* Ships COSE_Sign1 (single-signer, attached payload), COSE_Mac0, and
|
|
45
|
+
* COSE_Encrypt0 (single-recipient AEAD). Detached payload, COSE_Sign
|
|
46
|
+
* (multi-signer), and COSE_Encrypt (multi-recipient) are
|
|
47
|
+
* deferred-with-condition (operator demand).
|
|
47
48
|
*
|
|
48
49
|
* @card
|
|
49
50
|
* COSE_Sign1 sign / verify (RFC 9052) over the in-tree CBOR codec —
|
package/lib/mail-server-jmap.js
CHANGED
|
@@ -95,17 +95,17 @@
|
|
|
95
95
|
* - `urn:ietf:params:jmap:error:accountNotFound`
|
|
96
96
|
* - `urn:ietf:params:jmap:error:serverFail` (opaque last-resort)
|
|
97
97
|
*
|
|
98
|
+
* ## Beyond Core + Mail, this also ships
|
|
99
|
+
*
|
|
100
|
+
* - **Push channel (RFC 8887)** — `eventSourceHandler` (SSE) and
|
|
101
|
+
* `webSocketHandler` (WebSocket, with `StateChange` push).
|
|
102
|
+
* - **Blob upload/download (RFC 8620 §6)** — `uploadHandler` /
|
|
103
|
+
* `downloadHandler`, routing uploads through the guard-* family.
|
|
104
|
+
* - **EmailSubmission/set (RFC 8621 §7.5)** — `emailSubmissionSetHandler`,
|
|
105
|
+
* composing `b.mail.send.deliver`.
|
|
106
|
+
*
|
|
98
107
|
* ## What v1 does NOT ship
|
|
99
108
|
*
|
|
100
|
-
* - **Push channel (SSE + WebSocket per RFC 8887)** — operator wires
|
|
101
|
-
* `b.sse` or `b.websocket` to the `pushSubscribe` hook. v1.5
|
|
102
|
-
* bundles a turnkey push handler.
|
|
103
|
-
* - **Blob upload/download endpoints** — operator wires their own
|
|
104
|
-
* `/jmap/upload` / `/jmap/download` handlers; the framework
|
|
105
|
-
* supplies `b.storage` + `b.objectStore` + the guard-* family
|
|
106
|
-
* for the actual upload path.
|
|
107
|
-
* - **EmailSubmission (RFC 8621 §7)** — operator wires the bridge
|
|
108
|
-
* to `b.mail.server.submission`'s outbound agent.
|
|
109
109
|
* - **Calendars / Contacts (RFC 9610)**, **Sieve (RFC 9404)**,
|
|
110
110
|
* **MDN (RFC 9007)** — opt-in capabilities.
|
|
111
111
|
*
|
|
@@ -79,11 +79,12 @@
|
|
|
79
79
|
*
|
|
80
80
|
* - **DKIM signing pre-relay** — operator wires `b.mail.dkim.sign`
|
|
81
81
|
* in their outbound agent.
|
|
82
|
-
* - **CHUNKING (BDAT) extension** — RFC 3030 BDAT not yet
|
|
83
|
-
* supported on submission; clients use DATA instead.
|
|
84
82
|
* - **Per-actor outbound quota** — operator implements via
|
|
85
83
|
* `b.dailyByteQuota` against the authenticated actor.
|
|
86
84
|
*
|
|
85
|
+
* (CHUNKING / BDAT, RFC 3030, IS supported — advertised in EHLO and
|
|
86
|
+
* handled alongside DATA.)
|
|
87
|
+
*
|
|
87
88
|
* ## Composition contract
|
|
88
89
|
*
|
|
89
90
|
* Every gate is a primitive that already exists. Submission listener
|
package/lib/mdoc.js
CHANGED
|
@@ -30,22 +30,22 @@
|
|
|
30
30
|
* <code>opts.trustAnchorsPem</code> additionally verifies the issuer
|
|
31
31
|
* certificate chain and its validity at the asserted time.
|
|
32
32
|
*
|
|
33
|
-
* <strong>Scope.</strong>
|
|
34
|
-
* (ISO 18013-5 §9.1.2.4
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* ecosystems.
|
|
33
|
+
* <strong>Scope.</strong> Two halves are verified: issuer-data
|
|
34
|
+
* authentication (ISO 18013-5 §9.1.2.4 — the data is genuine and
|
|
35
|
+
* issuer-signed, via <code>verifyIssuerSigned</code>) and mdoc device
|
|
36
|
+
* authentication (§9.1.3 — holder binding over the verifier's
|
|
37
|
+
* <code>SessionTranscript</code>, via <code>verifyDeviceAuth</code>).
|
|
38
|
+
* Device auth covers the COSE_Sign1 signature variant; the COSE_Mac0
|
|
39
|
+
* (deviceMac) variant is refused rather than mis-verified. Composes
|
|
40
|
+
* <code>b.cose</code> + <code>b.cbor</code>; no new runtime dependency.
|
|
41
|
+
* Distinct from W3C VCDM (<code>b.vc</code>) and IETF SD-JWT VC
|
|
42
|
+
* (<code>b.auth.sdJwtVc</code>) — the three credential ecosystems.
|
|
43
43
|
*
|
|
44
44
|
* @card
|
|
45
|
-
* ISO 18013-5 mdoc / mDL
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* b.cose + b.cbor
|
|
45
|
+
* ISO 18013-5 mdoc / mDL verification — issuer-data (COSE_Sign1
|
|
46
|
+
* IssuerAuth, MSO validity window, disclosed-element digests) plus
|
|
47
|
+
* device-auth holder binding (§9.1.3 signature variant over the session
|
|
48
|
+
* transcript). Composes b.cose + b.cbor.
|
|
49
49
|
*/
|
|
50
50
|
|
|
51
51
|
var nodeCrypto = require("node:crypto");
|
package/lib/network-dnssec.js
CHANGED
|
@@ -30,16 +30,18 @@
|
|
|
30
30
|
* <code>dnssec/uncanonicalizable-type</code> rather than mis-validated
|
|
31
31
|
* — the security-critical DNSKEY / DS and the name-free address /
|
|
32
32
|
* text types (A, AAAA, TXT, …) are fully supported. The recursive
|
|
33
|
-
* chain-walk (root → TLD → zone
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* chain-walk (root → TLD → zone via <code>verifyChain</code> against the
|
|
34
|
+
* bundled IANA root trust anchors) and NSEC / NSEC3 denial-of-existence
|
|
35
|
+
* (<code>verifyDenial</code> / <code>nsec3Hash</code>) ship alongside the
|
|
36
|
+
* per-RRset verification core.
|
|
36
37
|
*
|
|
37
38
|
* @card
|
|
38
|
-
* Local DNSSEC verification (RFC 4035) — verify an RRSIG
|
|
39
|
-
* canonicalised RRset against a DNSKEY (RSA / ECDSA P-256·P-384 /
|
|
40
|
-
* Ed25519)
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* Local DNSSEC verification (RFC 4035 / 4034 / 5155) — verify an RRSIG
|
|
40
|
+
* over a canonicalised RRset against a DNSKEY (RSA / ECDSA P-256·P-384 /
|
|
41
|
+
* Ed25519) + DS-digest + key-tag, walk the root→zone chain to the IANA
|
|
42
|
+
* trust anchors, and check NSEC / NSEC3 denial of existence. Don't trust
|
|
43
|
+
* the upstream AD bit; verify the signature. Name-bearing RR types are
|
|
44
|
+
* refused, not mis-validated.
|
|
43
45
|
*/
|
|
44
46
|
|
|
45
47
|
var nodeCrypto = require("node:crypto");
|
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.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:fd9a8f72-de3b-4a8d-8059-9ff47ccebb1b",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-27T05:52:19.814Z",
|
|
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.13.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.13.8",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.13.
|
|
25
|
+
"version": "0.13.8",
|
|
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.13.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.13.8",
|
|
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.13.
|
|
57
|
+
"ref": "@blamejs/core@0.13.8",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|