@blamejs/core 0.10.15 → 0.11.0

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/lib/cms-codec.js CHANGED
@@ -674,10 +674,151 @@ function _encryptedContentInfo(plaintext, contentKey) {
674
674
  ]));
675
675
  }
676
676
 
677
+ /**
678
+ * @primitive b.cms.parseSignedData
679
+ * @signature b.cms.parseSignedData(buf, opts?)
680
+ * @since 0.10.16
681
+ * @status stable
682
+ * @related b.cms.encodeSignedData, b.cms.decode
683
+ *
684
+ * Decode a CMS ContentInfo carrying SignedData and walk into the
685
+ * inner structure per RFC 5652 §5.1. Returns a structured object
686
+ * with `digestAlgs`, `encapContent`, `certificates`, and `signerInfos`
687
+ * arrays so downstream verifiers (b.mail.crypto.smime.verify) can
688
+ * check signatures without re-implementing the SignedData walker.
689
+ *
690
+ * @opts
691
+ * maxBytes: number, // default 64 MiB
692
+ *
693
+ * @example
694
+ * var sd = b.cms.parseSignedData(derBytes);
695
+ * sd.signerInfos[0].sigAlgOid; // → "2.16.840.1.101.3.4.3.18" (ML-DSA-65)
696
+ */
697
+ function parseSignedData(buf, opts) {
698
+ var ci = decode(buf, opts);
699
+ if (ci.contentType !== OID.signedData) {
700
+ throw new CmsCodecError("cms/not-signed-data",
701
+ "parseSignedData: ContentInfo type is " + ci.contentType + ", expected " + OID.signedData);
702
+ }
703
+ if (ci.content.tag !== asn1.TAG.SEQUENCE) {
704
+ throw new CmsCodecError("cms/bad-signed-data", "SignedData must be a SEQUENCE");
705
+ }
706
+ var children = asn1.readSequence(ci.content.value);
707
+ if (children.length < 4) {
708
+ throw new CmsCodecError("cms/bad-signed-data",
709
+ "SignedData SEQUENCE must have at least 4 children");
710
+ }
711
+ var idx = 0;
712
+ idx += 1; // version
713
+ var digestAlgsSet = children[idx]; idx += 1;
714
+ if (digestAlgsSet.tag !== asn1.TAG.SET) {
715
+ throw new CmsCodecError("cms/bad-signed-data", "digestAlgorithms must be a SET");
716
+ }
717
+ var digestAlgs = asn1.readSequence(digestAlgsSet.value).map(_readAlgIdOid);
718
+ var encapInfoNode = children[idx]; idx += 1;
719
+ if (encapInfoNode.tag !== asn1.TAG.SEQUENCE) {
720
+ throw new CmsCodecError("cms/bad-signed-data", "encapContentInfo must be a SEQUENCE");
721
+ }
722
+ var encapContent = _readEncapContent(encapInfoNode);
723
+ var certificates = [];
724
+ while (idx < children.length - 1) {
725
+ var n = children[idx];
726
+ if (n.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && n.tag === 0) {
727
+ var certChildren = asn1.readSequence(n.value);
728
+ for (var ci2 = 0; ci2 < certChildren.length; ci2 += 1) {
729
+ certificates.push(_reEncodeNode(certChildren[ci2]));
730
+ }
731
+ idx += 1;
732
+ } else if (n.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && n.tag === 1) {
733
+ idx += 1;
734
+ } else {
735
+ break;
736
+ }
737
+ }
738
+ var signerInfosSet = children[idx];
739
+ if (!signerInfosSet || signerInfosSet.tag !== asn1.TAG.SET) {
740
+ throw new CmsCodecError("cms/bad-signed-data", "signerInfos must be a SET");
741
+ }
742
+ var signerInfos = asn1.readSequence(signerInfosSet.value).map(_readSignerInfo);
743
+ return {
744
+ digestAlgs: digestAlgs,
745
+ encapContent: encapContent,
746
+ certificates: certificates,
747
+ signerInfos: signerInfos,
748
+ };
749
+ }
750
+
751
+ function _readAlgIdOid(seqNode) {
752
+ if (seqNode.tag !== asn1.TAG.SEQUENCE) {
753
+ throw new CmsCodecError("cms/bad-alg-id", "AlgorithmIdentifier must be a SEQUENCE");
754
+ }
755
+ var c = asn1.readSequence(seqNode.value);
756
+ if (c.length === 0) {
757
+ throw new CmsCodecError("cms/bad-alg-id", "AlgorithmIdentifier missing OID");
758
+ }
759
+ return asn1.readOid(c[0]);
760
+ }
761
+
762
+ function _readEncapContent(encapInfoNode) {
763
+ var children = asn1.readSequence(encapInfoNode.value);
764
+ if (children.length === 0) {
765
+ throw new CmsCodecError("cms/bad-encap", "encapContentInfo missing eContentType");
766
+ }
767
+ var eContentType = asn1.readOid(children[0]);
768
+ var eContent = null;
769
+ if (children.length >= 2) {
770
+ var ec = children[1];
771
+ if (ec.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && ec.tag === 0) {
772
+ var inner = asn1.readNode(ec.value);
773
+ if (inner.tag === asn1.TAG.OCTET_STRING) {
774
+ eContent = asn1.readOctetString(inner);
775
+ }
776
+ }
777
+ }
778
+ return { eContentType: eContentType, eContent: eContent };
779
+ }
780
+
781
+ function _readSignerInfo(siNode) {
782
+ if (siNode.tag !== asn1.TAG.SEQUENCE) {
783
+ throw new CmsCodecError("cms/bad-signer-info", "SignerInfo must be a SEQUENCE");
784
+ }
785
+ var c = asn1.readSequence(siNode.value);
786
+ if (c.length < 5) {
787
+ throw new CmsCodecError("cms/bad-signer-info", "SignerInfo must have at least 5 children");
788
+ }
789
+ var idx = 0;
790
+ idx += 1; // version
791
+ var sidNode = c[idx]; idx += 1;
792
+ var digestAlgOid = _readAlgIdOid(c[idx]); idx += 1;
793
+ // Optional [0] IMPLICIT signedAttrs — re-tag as universal SET
794
+ // (0x31) per RFC 5652 §5.4 to recover the byte sequence the
795
+ // signature was computed over.
796
+ var signedAttrsRaw = null;
797
+ if (c[idx] && c[idx].tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && c[idx].tag === 0) {
798
+ var implicitRaw = _reEncodeNode(c[idx]);
799
+ signedAttrsRaw = Buffer.concat([Buffer.from([0x31]), implicitRaw.slice(1)]); // allow:raw-byte-literal — universal SET tag per RFC 5652 §5.4
800
+ idx += 1;
801
+ }
802
+ var sigAlgOid = _readAlgIdOid(c[idx]); idx += 1;
803
+ var sigNode = c[idx]; idx += 1;
804
+ if (sigNode.tag !== asn1.TAG.OCTET_STRING) {
805
+ throw new CmsCodecError("cms/bad-signer-info", "signature must be an OCTET STRING");
806
+ }
807
+ var signature = asn1.readOctetString(sigNode);
808
+ return {
809
+ sid: _reEncodeNode(sidNode),
810
+ digestAlgOid: digestAlgOid,
811
+ signedAttrsRaw: signedAttrsRaw,
812
+ sigAlgOid: sigAlgOid,
813
+ signature: signature,
814
+ };
815
+ }
816
+
677
817
  module.exports = {
678
818
  encodeSignedData: encodeSignedData,
679
819
  encodeEnvelopedData: encodeEnvelopedData,
680
820
  decode: decode,
821
+ parseSignedData: parseSignedData,
681
822
  OID: OID,
682
823
  MAX_DEPTH: MAX_DEPTH,
683
824
  DEFAULT_MAX_LEN: DEFAULT_MAX_LEN,
package/lib/compliance.js CHANGED
@@ -1126,6 +1126,79 @@ var POSTURE_DEFAULTS = Object.freeze({
1126
1126
  "cmmc-2.0-level-1": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
1127
1127
  "cmmc-2.0-level-2": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
1128
1128
  "cmmc-2.0-level-3": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true, fipsMode: false }),
1129
+ // ---- v0.10.16 — sectoral catch-up ----
1130
+ // 42 CFR Part 2 — Substance Use Disorder records confidentiality
1131
+ // (HHS final rule 2024-04-16 aligns Part 2 with HIPAA but retains
1132
+ // a stricter consent floor; encrypted backups + signed audit chain
1133
+ // + post-erase vacuum because the rule narrows the consent window
1134
+ // and operators must demonstrate effective erasure on revocation).
1135
+ "42-cfr-part-2": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
1136
+ // ONC HTI-1 final rule (45 CFR Part 170 / 89 FR 1192, effective
1137
+ // 2024-12-31) — health IT certification. Brings algorithmic
1138
+ // transparency / DSI (Decision Support Interventions) requirements.
1139
+ // Cascade: encrypted backups + signed audit + vacuum (PHI-tier).
1140
+ "hti-1": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
1141
+ // USCDI v4 (ONC October 2023) — US Core Data for Interoperability
1142
+ // standard data classes for EHR exchange. PHI-tier cascade.
1143
+ "uscdi-v4": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
1144
+ // IRS Publication 1075 — Federal Tax Information (FTI) safeguards.
1145
+ // FTI-tier: encrypted at rest, signed audit, vacuum after erasure
1146
+ // (Pub 1075 §4.3 requires sanitization on disposal).
1147
+ "irs-1075": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
1148
+ // NIST 800-172 Rev 3 — Enhanced Security Requirements for Protecting
1149
+ // CUI. Layered atop 800-171 / CMMC-L2. FIPS-validated crypto
1150
+ // floor — same operator-opt-in flag pattern as fedramp-rev5-moderate.
1151
+ "nist-800-172-r3": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true, fipsMode: false }),
1152
+ // FIRST Traffic Light Protocol 2.0 (August 2022) — controls sharing
1153
+ // of cyber threat information. Cascade: signed audit chain (the
1154
+ // protocol's normative effect is on the audit + sharing surface,
1155
+ // not data-at-rest).
1156
+ "tlp-2.0": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
1157
+ // Security of Critical Infrastructure Act 2018 (Australia, SOCI Act)
1158
+ // + 2021/2022 amendments — critical-infrastructure cyber + ENS
1159
+ // (Enhanced Cyber Security Obligations). Cascade: encrypted backups
1160
+ // + signed audit (ENS §30CT data-integrity obligation).
1161
+ "soci-au": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
1162
+ // EU NIS 2 Directive (Directive (EU) 2022/2555) — transposition
1163
+ // deadline 2024-10-17. Cybersecurity for essential + important
1164
+ // entities. Encrypted backups + signed audit chain (Art. 21(2)(d)
1165
+ // requires backup management + crisis recovery).
1166
+ "nis2": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
1167
+ // EU Cyber Resilience Act (Reg. (EU) 2024/2847) — product
1168
+ // cybersecurity; full applicability 2027-12-11 with reporting
1169
+ // obligations starting 2026-09-11. SUPPLY-tier cascade.
1170
+ "cra": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
1171
+ // FFIEC Cybersecurity Assessment Tool 2.0 — financial-tier; aligns
1172
+ // with NIST CSF 2.0 + CRI Profile. Cascade matches glba-safeguards.
1173
+ "ffiec-cat-2": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
1174
+ // CRI Profile v2.0 (Cyber Risk Institute, May 2024) — financial-tier
1175
+ // cyber risk + NIST CSF 2.0 cross-walk.
1176
+ "cri-profile-v2.0": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
1177
+ // OMB M-22-09 — Moving to Zero Trust (US federal). Cascade: signed
1178
+ // audit + TLS 1.3 (the memorandum's normative effect rides through
1179
+ // the identity + segmentation surfaces).
1180
+ "m-22-09": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
1181
+ // OMB M-22-18 — Enhancing the Security of the Software Supply Chain
1182
+ // (the SSDF / attestation requirement). SUPPLY-tier — audit-chain
1183
+ // signed for the attestation records.
1184
+ "m-22-18": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
1185
+ // NIST 800-53 Rev 5 Privacy baseline — additive privacy controls
1186
+ // overlay. Cascade: vacuum-after-erase per PT-2(2) and SI-12.
1187
+ "nist-800-53-r5-privacy": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
1188
+ // NIST AI-RMF Generative AI Profile (NIST AI 600-1, July 2024) —
1189
+ // generative AI risk management overlay. AI-tier cascade.
1190
+ "nist-ai-600-1-genai": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
1191
+ // NIST CSF 2.0 (February 2024) — Cybersecurity Framework with the
1192
+ // GOVERN function added.
1193
+ "nist-csf-2.0": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
1194
+ // SB 53 / California Frontier AI Disclosure (effective 2026 fiscal)
1195
+ // — frontier-model critical incident disclosure ledger.
1196
+ "sb-53": Object.freeze({ backupEncryptionRequired: true, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: true }),
1197
+ // NYC Local Law 144 (2023) — Automated Employment Decision Tools
1198
+ // (bias-audit + candidate notice) — bias-audit posture (already
1199
+ // present as "nyc-ll144"); 2024 amendment adds annual re-audit
1200
+ // signing.
1201
+ "nyc-ll144-2024": Object.freeze({ backupEncryptionRequired: false, auditChainSignedRequired: true, tlsMinVersion: "TLSv1.3", requireVacuumAfterErase: false }),
1129
1202
  });
1130
1203
 
1131
1204
  /**
package/lib/csp.js ADDED
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.csp
4
+ * @nav Security
5
+ * @title CSP3 builder
6
+ * @order 150
7
+ * @slug csp
8
+ *
9
+ * @card
10
+ * Content Security Policy Level 3 header builder. Composable per-
11
+ * directive surface with Trusted Types defaults and nonce/hash
12
+ * helpers. Operators wire the resulting header value into
13
+ * b.middleware.securityHeaders via the `csp` opt.
14
+ *
15
+ * @intro
16
+ * Content Security Policy Level 3 (W3C CSP3 / candidate
17
+ * recommendation 2024-09) directive builder. The framework's
18
+ * b.middleware.securityHeaders module ships a strict default CSP;
19
+ * this module exposes the per-directive surface so operators can
20
+ * build out a policy by composition without hand-concatenating
21
+ * strings (which is the failure mode behind most CSP-bypass
22
+ * incidents — a missing `'self'`, an accidental `'unsafe-inline'`,
23
+ * or quoting that the UA silently ignores).
24
+ *
25
+ * Posture:
26
+ * - Refuses `'unsafe-inline'` / `'unsafe-eval'` / `'unsafe-hashes'`
27
+ * in any script-* directive unless explicitly acknowledged via
28
+ * `acknowledgeUnsafe: true` with a documented reason. The CSP3
29
+ * spec defines these as no-ops when `'strict-dynamic'` is
30
+ * present, but UAs that haven't shipped strict-dynamic full
31
+ * support still honor the unsafe keywords — refusing at builder
32
+ * time prevents shipping an unintentional bypass.
33
+ * - Defaults `require-trusted-types-for 'script'` + the named
34
+ * Trusted Types policy "default" when operators wire any
35
+ * script-* source (Trusted Types is the strongest defense
36
+ * against DOM-XSS available in browsers today).
37
+ * - Refuses `data:` in img-src / media-src / font-src unless the
38
+ * operator explicitly opts in (data: URLs sidestep most CSP
39
+ * defenses and are a common XSS pivot).
40
+ * - Refuses `https:` / `*` as a source in any directive (catch-all
41
+ * sources defeat the principle of least privilege).
42
+ *
43
+ * v0.10.16 light-up: builder + nonce helper + hash helper. Trusted
44
+ * Types policy declaration helper. CSP-report-uri / report-to wiring
45
+ * composes with b.middleware.cspReport (existing).
46
+ *
47
+ * Spec citations:
48
+ * - W3C CSP Level 3 (CR 2024-09)
49
+ * - W3C Trusted Types (CR 2023-05)
50
+ * - Reporting API Level 1 (W3C 2024)
51
+ */
52
+ var nodeCrypto = require("node:crypto");
53
+ var validateOpts = require("./validate-opts");
54
+ var bCrypto = require("./crypto");
55
+ var { defineClass } = require("./framework-error");
56
+
57
+ var CspError = defineClass("CspError", { alwaysPermanent: true });
58
+
59
+ // Directives that participate in script execution — operator-facing
60
+ // keyword discipline only applies here (refuse 'unsafe-*' unless
61
+ // acknowledgeUnsafe is set).
62
+ var SCRIPT_DIRECTIVES = ["script-src", "script-src-elem", "script-src-attr",
63
+ "style-src", "style-src-elem", "style-src-attr",
64
+ "worker-src", "frame-src", "child-src"];
65
+
66
+ var ALL_DIRECTIVES = [
67
+ "default-src", "script-src", "script-src-elem", "script-src-attr",
68
+ "style-src", "style-src-elem", "style-src-attr",
69
+ "img-src", "media-src", "font-src", "connect-src", "object-src",
70
+ "frame-src", "child-src", "worker-src", "manifest-src", "prefetch-src",
71
+ "form-action", "frame-ancestors", "navigate-to", "base-uri", "sandbox",
72
+ "report-to", "report-uri",
73
+ "require-trusted-types-for", "trusted-types",
74
+ "upgrade-insecure-requests", "block-all-mixed-content",
75
+ ];
76
+
77
+ var UNSAFE_KEYWORDS = ["'unsafe-inline'", "'unsafe-eval'", "'unsafe-hashes'"];
78
+ var CATCH_ALL_SOURCES = ["*", "https:"];
79
+
80
+ /**
81
+ * @primitive b.csp.build
82
+ * @signature b.csp.build(directives, opts?)
83
+ * @since 0.10.16
84
+ * @status stable
85
+ * @compliance soc2, gdpr
86
+ * @related b.middleware.securityHeaders, b.csp.nonce, b.csp.hash
87
+ *
88
+ * Build a CSP3 header value from a per-directive object. Each key is
89
+ * a CSP directive name; each value is an array of sources (strings).
90
+ * Returns a single string ready for `Content-Security-Policy:` or
91
+ * `Content-Security-Policy-Report-Only:`.
92
+ *
93
+ * @opts
94
+ * {
95
+ * acknowledgeUnsafe?: boolean, // default false — refuses 'unsafe-*' otherwise
96
+ * allowDataImages?: boolean, // default false — refuses data: in img-src/media-src/font-src
97
+ * trustedTypesPolicies?: string[], // policy names allowed by trusted-types directive
98
+ * requireTrustedTypes?: boolean, // default true when any script-* is set
99
+ * }
100
+ *
101
+ * @example
102
+ * var policy = b.csp.build({
103
+ * "default-src": ["'self'"],
104
+ * "script-src": ["'self'", "'nonce-" + req.cspNonce + "'"],
105
+ * "style-src": ["'self'"],
106
+ * "img-src": ["'self'"],
107
+ * "connect-src": ["'self'"],
108
+ * "frame-ancestors": ["'none'"],
109
+ * "base-uri": ["'self'"],
110
+ * "form-action": ["'self'"],
111
+ * "object-src": ["'none'"],
112
+ * "report-to": ["default"],
113
+ * }, { trustedTypesPolicies: ["default", "app-sanitizer"] });
114
+ * res.setHeader("Content-Security-Policy", policy);
115
+ */
116
+ function build(directives, opts) {
117
+ if (!directives || typeof directives !== "object") {
118
+ throw new CspError("csp/bad-directives",
119
+ "csp.build: directives must be an object keyed by CSP directive name");
120
+ }
121
+ opts = opts || {};
122
+ validateOpts(opts, ["acknowledgeUnsafe", "allowDataImages",
123
+ "trustedTypesPolicies", "requireTrustedTypes"], "csp.build");
124
+ var acknowledgeUnsafe = opts.acknowledgeUnsafe === true;
125
+ var allowDataImages = opts.allowDataImages === true;
126
+
127
+ var keys = Object.keys(directives);
128
+ var hasScriptDirective = false;
129
+ for (var ki = 0; ki < keys.length; ki += 1) {
130
+ var name = keys[ki];
131
+ if (ALL_DIRECTIVES.indexOf(name) === -1) {
132
+ throw new CspError("csp/unknown-directive",
133
+ "csp.build: '" + name + "' is not a recognized CSP3 directive");
134
+ }
135
+ var values = directives[name];
136
+ if (!Array.isArray(values)) {
137
+ throw new CspError("csp/bad-directive-value",
138
+ "csp.build: directives['" + name + "'] must be an array of source strings");
139
+ }
140
+ if (SCRIPT_DIRECTIVES.indexOf(name) !== -1) hasScriptDirective = true;
141
+ for (var vi = 0; vi < values.length; vi += 1) {
142
+ var src = values[vi];
143
+ if (typeof src !== "string" || src.length === 0) {
144
+ throw new CspError("csp/bad-source",
145
+ "csp.build: directives['" + name + "'][" + vi + "] must be a non-empty string");
146
+ }
147
+ // CR/LF/NUL header-injection rejection.
148
+ if (/[\r\n\0]/.test(src)) { // allow:duplicate-regex — CR/LF/NUL header-injection rejection
149
+ throw new CspError("csp/header-injection",
150
+ "csp.build: source '" + src + "' contains CR/LF/NUL");
151
+ }
152
+ if (!acknowledgeUnsafe && SCRIPT_DIRECTIVES.indexOf(name) !== -1 &&
153
+ UNSAFE_KEYWORDS.indexOf(src) !== -1) {
154
+ throw new CspError("csp/unsafe-keyword",
155
+ "csp.build: " + name + " contains " + src + "; pass acknowledgeUnsafe:true with a " +
156
+ "documented justification to allow it (CSP3 §6.2.5.x — unsafe keywords are a " +
157
+ "common XSS bypass surface)");
158
+ }
159
+ if (CATCH_ALL_SOURCES.indexOf(src) !== -1) {
160
+ throw new CspError("csp/catch-all-source",
161
+ "csp.build: " + name + " contains catch-all source '" + src + "'; CSP3 best " +
162
+ "practice refuses these (use an explicit allowlist instead)");
163
+ }
164
+ if (!allowDataImages && (name === "img-src" || name === "media-src" || name === "font-src") &&
165
+ src === "data:") {
166
+ throw new CspError("csp/data-source",
167
+ "csp.build: " + name + " contains 'data:'; pass allowDataImages:true with a " +
168
+ "documented reason (data: URLs sidestep most CSP defenses)");
169
+ }
170
+ }
171
+ }
172
+
173
+ // Trusted Types defaults. When operator has any script-* directive
174
+ // and didn't override `requireTrustedTypes`, append the require-
175
+ // trusted-types-for + trusted-types directives.
176
+ var requireTt = opts.requireTrustedTypes !== false && hasScriptDirective &&
177
+ directives["require-trusted-types-for"] === undefined;
178
+ if (requireTt) {
179
+ directives["require-trusted-types-for"] = ["'script'"];
180
+ }
181
+ if (opts.trustedTypesPolicies && Array.isArray(opts.trustedTypesPolicies) &&
182
+ directives["trusted-types"] === undefined) {
183
+ directives["trusted-types"] = opts.trustedTypesPolicies;
184
+ }
185
+
186
+ // Emit in canonical order (ALL_DIRECTIVES order) so operators
187
+ // diffing two policies see structural diffs rather than ordering
188
+ // noise.
189
+ var out = [];
190
+ for (var di = 0; di < ALL_DIRECTIVES.length; di += 1) {
191
+ var d = ALL_DIRECTIVES[di];
192
+ var vals = directives[d];
193
+ if (vals === undefined) continue;
194
+ if (Array.isArray(vals) && vals.length === 0) {
195
+ // Directives like upgrade-insecure-requests accept no value.
196
+ if (d === "upgrade-insecure-requests" || d === "block-all-mixed-content") {
197
+ out.push(d);
198
+ continue;
199
+ }
200
+ continue;
201
+ }
202
+ out.push(d + " " + vals.join(" "));
203
+ }
204
+ return out.join("; ");
205
+ }
206
+
207
+ /**
208
+ * @primitive b.csp.nonce
209
+ * @signature b.csp.nonce(byteLen?)
210
+ * @since 0.10.16
211
+ * @status stable
212
+ *
213
+ * Generate a CSP3 nonce — base64url-encoded random bytes for use as
214
+ * `'nonce-<value>'` in script-src / style-src. The CSP3 spec
215
+ * recommends at least 128 bits of entropy (16 bytes); this primitive
216
+ * uses 32 bytes by default for a generous margin.
217
+ *
218
+ * @example
219
+ * req.cspNonce = b.csp.nonce();
220
+ * res.setHeader("Content-Security-Policy",
221
+ * b.csp.build({ "script-src": ["'self'", "'nonce-" + req.cspNonce + "'"] }));
222
+ */
223
+ function nonce(byteLen) {
224
+ var n = typeof byteLen === "number" ? byteLen : 32; // allow:raw-byte-literal — 256-bit nonce default
225
+ if (!isFinite(n) || n < 16 || n > 64) { // allow:raw-byte-literal — CSP3 §6.2.x nonce bounds
226
+ throw new CspError("csp/bad-nonce-len",
227
+ "csp.nonce: byteLen must be 16-64 (CSP3 §6.2 recommends ≥16 bytes)");
228
+ }
229
+ return bCrypto.toBase64Url(nodeCrypto.randomBytes(n));
230
+ }
231
+
232
+ /**
233
+ * @primitive b.csp.hash
234
+ * @signature b.csp.hash(scriptBody, alg?)
235
+ * @since 0.10.16
236
+ * @status stable
237
+ *
238
+ * Compute a CSP3 hash source for an inline script/style. Returns the
239
+ * `'<alg>-<base64>'` token suitable for direct use as a script-src
240
+ * source.
241
+ *
242
+ * @opts
243
+ * alg?: "sha256" | "sha384" | "sha512" // default sha384 (matches
244
+ * // b.crypto.sri default)
245
+ *
246
+ * @example
247
+ * var src = b.csp.hash("console.log('boot');");
248
+ * // → "'sha384-abcd...'"
249
+ */
250
+ function hash(scriptBody, alg) {
251
+ if (typeof scriptBody !== "string" && !Buffer.isBuffer(scriptBody)) {
252
+ throw new CspError("csp/bad-hash-input",
253
+ "csp.hash: scriptBody must be a string or Buffer");
254
+ }
255
+ var algName = alg || "sha384";
256
+ if (algName !== "sha256" && algName !== "sha384" && algName !== "sha512") {
257
+ throw new CspError("csp/bad-hash-alg",
258
+ "csp.hash: alg must be sha256 / sha384 / sha512 (CSP3 §6.2 hash sources)");
259
+ }
260
+ var digest = nodeCrypto.createHash(algName)
261
+ .update(typeof scriptBody === "string" ? Buffer.from(scriptBody, "utf8") : scriptBody)
262
+ .digest("base64");
263
+ return "'" + algName + "-" + digest + "'";
264
+ }
265
+
266
+ module.exports = {
267
+ build: build,
268
+ nonce: nonce,
269
+ hash: hash,
270
+ CspError: CspError,
271
+ };