@blamejs/core 0.8.76 → 0.8.77
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/index.js +2 -0
- package/lib/acme.js +200 -1
- package/lib/auth/oauth.js +329 -0
- package/lib/compliance-ai-act.js +161 -3
- package/lib/compliance.js +48 -0
- package/lib/config.js +90 -13
- package/lib/content-credentials.js +227 -0
- package/lib/cra-report.js +106 -2
- package/lib/crypto-field.js +5 -0
- package/lib/dsr.js +96 -0
- package/lib/mcp.js +239 -6
- package/lib/middleware/index.js +4 -0
- package/lib/middleware/protected-resource-metadata.js +165 -0
- package/lib/middleware/rate-limit.js +59 -3
- package/lib/middleware/scim-server.js +375 -0
- package/lib/middleware/security-headers.js +12 -0
- package/lib/nist-crosswalk.js +293 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/compliance-ai-act.js
CHANGED
|
@@ -537,6 +537,162 @@ function deployerChecklist(assessment) {
|
|
|
537
537
|
return items;
|
|
538
538
|
}
|
|
539
539
|
|
|
540
|
+
/**
|
|
541
|
+
* @primitive b.complianceAiAct.fundamentalRightsImpactAssessment
|
|
542
|
+
* @signature b.complianceAiAct.fundamentalRightsImpactAssessment(opts)
|
|
543
|
+
* @since 0.8.77
|
|
544
|
+
*
|
|
545
|
+
* EU AI Act Article 27 — Fundamental Rights Impact Assessment (FRIA).
|
|
546
|
+
* Mandatory for deployers of high-risk AI systems listed in Annex III
|
|
547
|
+
* §5 (creditworthiness scoring, life/health insurance risk), §6
|
|
548
|
+
* (law enforcement), §7 (migration/asylum), §8 (justice admin), public
|
|
549
|
+
* authorities, and any private body providing public services. Must
|
|
550
|
+
* be completed BEFORE the first use of the high-risk system, kept
|
|
551
|
+
* updated, and notified to the national market-surveillance authority.
|
|
552
|
+
*
|
|
553
|
+
* Returns the structured FRIA document scaffold — operator fills in
|
|
554
|
+
* the per-deployment specifics; the framework auto-populates the
|
|
555
|
+
* fields it can derive (system identification, GPAI classification
|
|
556
|
+
* if applicable, Annex IV reference, deployment-context audit hooks).
|
|
557
|
+
*
|
|
558
|
+
* @opts
|
|
559
|
+
* {
|
|
560
|
+
* systemId: string, // operator's high-risk system identifier
|
|
561
|
+
* systemDescription: { ... }, // forwarded to classify() for risk-tier verdict
|
|
562
|
+
* deploymentContext: { purpose, sector, geography, scale },
|
|
563
|
+
* affectedPersons: { categories: string[], estimatedCount: number },
|
|
564
|
+
* risksToFundamentalRights: string[], // operator-identified risks
|
|
565
|
+
* mitigations: string[], // mitigations + monitoring per risk
|
|
566
|
+
* humanOversight: { roles: string[], escalationPath: string },
|
|
567
|
+
* residualRisks: string[],
|
|
568
|
+
* reviewCadence: string, // e.g. "quarterly"
|
|
569
|
+
* }
|
|
570
|
+
*
|
|
571
|
+
* @example
|
|
572
|
+
* var fria = b.complianceAiAct.fundamentalRightsImpactAssessment({
|
|
573
|
+
* systemId: "credit-scoring-v3",
|
|
574
|
+
* deploymentContext: { purpose: "loan approval", sector: "financial",
|
|
575
|
+
* geography: "EU", scale: "1M decisions/year" },
|
|
576
|
+
* affectedPersons: { categories: ["EU consumers"], estimatedCount: 1000000 },
|
|
577
|
+
* risksToFundamentalRights: ["discriminatory denial", "right to explanation"],
|
|
578
|
+
* mitigations: ["bias audit every 6 months", "human review threshold"],
|
|
579
|
+
* humanOversight: { roles: ["credit officer"], escalationPath: "ombudsman" },
|
|
580
|
+
* reviewCadence: "semi-annual",
|
|
581
|
+
* });
|
|
582
|
+
*/
|
|
583
|
+
function fundamentalRightsImpactAssessment(opts) {
|
|
584
|
+
if (!opts || typeof opts !== "object") {
|
|
585
|
+
throw new Error("fundamentalRightsImpactAssessment: opts required");
|
|
586
|
+
}
|
|
587
|
+
validateOpts.requireNonEmptyString(opts.systemId, "systemId",
|
|
588
|
+
Error, "compliance-ai-act/no-system-id");
|
|
589
|
+
return {
|
|
590
|
+
"$schema": "https://blamejs.com/schema/ai-act-fria-v1.json",
|
|
591
|
+
regulation: "EU Regulation 2024/1689 — AI Act",
|
|
592
|
+
article: "Article 27 (Fundamental Rights Impact Assessment)",
|
|
593
|
+
generatedAt: new Date().toISOString(),
|
|
594
|
+
systemId: opts.systemId,
|
|
595
|
+
classification: opts.systemDescription ? classify(opts.systemDescription) : null,
|
|
596
|
+
deploymentContext: opts.deploymentContext || {},
|
|
597
|
+
affectedPersons: opts.affectedPersons || { categories: [], estimatedCount: null },
|
|
598
|
+
risks: opts.risksToFundamentalRights || [],
|
|
599
|
+
mitigations: opts.mitigations || [],
|
|
600
|
+
humanOversight: opts.humanOversight || { roles: [], escalationPath: null },
|
|
601
|
+
residualRisks: opts.residualRisks || [],
|
|
602
|
+
reviewCadence: opts.reviewCadence || "annual",
|
|
603
|
+
notificationStatus: "operator-must-notify",
|
|
604
|
+
note: "Notify national market-surveillance authority before first use (Art 27(3))",
|
|
605
|
+
auditHook: "b.audit emission action='aiact.fria.completed' recommended",
|
|
606
|
+
annexIVReference: "see b.complianceAiAct.annexIVScaffold for technical documentation",
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* @primitive b.complianceAiAct.gpai.trainingDataSummary
|
|
612
|
+
* @signature b.complianceAiAct.gpai.trainingDataSummary(opts)
|
|
613
|
+
* @since 0.8.77
|
|
614
|
+
*
|
|
615
|
+
* EU AI Act Article 53(1)(d) — GPAI training-data summary template
|
|
616
|
+
* compliant with the AI Office's template format (published in 2024,
|
|
617
|
+
* mandatory from 2026-08-02). The template requires categories of
|
|
618
|
+
* data, modalities, source provenance, copyright + licensing status,
|
|
619
|
+
* dataset sizes, dates of collection, and steps taken to identify +
|
|
620
|
+
* mitigate biases.
|
|
621
|
+
*
|
|
622
|
+
* Returns the JSON document operators publish under their `/.well-known/
|
|
623
|
+
* ai-training-data-summary` endpoint or attach to model cards.
|
|
624
|
+
*
|
|
625
|
+
* @opts
|
|
626
|
+
* modelId: string, // required
|
|
627
|
+
* modelVersion: string, // optional
|
|
628
|
+
* provider: object, // { name, address, contact }
|
|
629
|
+
* dataCategories: string[], // ["web-crawl", "books", "code", "synthetic", ...]
|
|
630
|
+
* modalities: string[], // ["text", "image", "audio", "video"]
|
|
631
|
+
* sources: object[], // { identifier, url, type, licenseStatus, size, collectedFrom, collectedTo }
|
|
632
|
+
* copyrightStatus: object, // { respectsRightReservations, machineReadableSignalsObserved, tdmExceptionUsed }
|
|
633
|
+
* biasMitigation: object, // { methodsApplied, auditCadence, remediations }
|
|
634
|
+
* contentProvenance: object, // { synthIdEmbed, c2paManifestEmbed, watermarkProvider }
|
|
635
|
+
*
|
|
636
|
+
* @example
|
|
637
|
+
* var summary = b.complianceAiAct.gpai.trainingDataSummary({
|
|
638
|
+
* modelId: "acme-llm-7b",
|
|
639
|
+
* modelVersion: "1.0",
|
|
640
|
+
* provider: { name: "Acme AI", address: "1 St", contact: "ai@acme.example" },
|
|
641
|
+
* dataCategories: ["web-crawl", "books", "code"],
|
|
642
|
+
* modalities: ["text"],
|
|
643
|
+
* sources: [
|
|
644
|
+
* { identifier: "CommonCrawl-2024", type: "web-crawl", licenseStatus: "permitted" },
|
|
645
|
+
* ],
|
|
646
|
+
* biasMitigation: { methodsApplied: ["demographic-balance"], auditCadence: "quarterly" },
|
|
647
|
+
* });
|
|
648
|
+
*/
|
|
649
|
+
function trainingDataSummary(opts) {
|
|
650
|
+
if (!opts || typeof opts !== "object") {
|
|
651
|
+
throw new Error("trainingDataSummary: opts required");
|
|
652
|
+
}
|
|
653
|
+
validateOpts.requireNonEmptyString(opts.modelId, "modelId",
|
|
654
|
+
Error, "compliance-ai-act/no-model-id");
|
|
655
|
+
return {
|
|
656
|
+
"$schema": "https://blamejs.com/schema/ai-act-gpai-training-summary-v1.json",
|
|
657
|
+
regulation: "EU Regulation 2024/1689 — AI Act",
|
|
658
|
+
article: "Article 53(1)(d) (GPAI training data summary)",
|
|
659
|
+
template: "AI Office GPAI Training Data Summary Template",
|
|
660
|
+
generatedAt: new Date().toISOString(),
|
|
661
|
+
modelId: opts.modelId,
|
|
662
|
+
modelVersion: opts.modelVersion || null,
|
|
663
|
+
provider: opts.provider || { name: null, address: null, contact: null },
|
|
664
|
+
dataCategories: opts.dataCategories || [], // ["web-crawl", "books", "code", "synthetic", ...]
|
|
665
|
+
modalities: opts.modalities || [], // ["text", "image", "audio", "video"]
|
|
666
|
+
sources: (opts.sources || []).map(function (s) {
|
|
667
|
+
return {
|
|
668
|
+
identifier: s.identifier,
|
|
669
|
+
url: s.url || null,
|
|
670
|
+
type: s.type || "unknown",
|
|
671
|
+
licenseStatus: s.licenseStatus || "unknown",
|
|
672
|
+
size: s.size || null,
|
|
673
|
+
collectedFrom: s.collectedFrom || null,
|
|
674
|
+
collectedTo: s.collectedTo || null,
|
|
675
|
+
};
|
|
676
|
+
}),
|
|
677
|
+
copyrightStatus: opts.copyrightStatus || {
|
|
678
|
+
respectsRightReservations: null,
|
|
679
|
+
machineReadableSignalsObserved: null,
|
|
680
|
+
tdmExceptionUsed: null,
|
|
681
|
+
},
|
|
682
|
+
biasMitigation: opts.biasMitigation || {
|
|
683
|
+
methodsApplied: [],
|
|
684
|
+
auditCadence: "annual",
|
|
685
|
+
remediations: [],
|
|
686
|
+
},
|
|
687
|
+
contentProvenance: opts.contentProvenance || {
|
|
688
|
+
synthIdEmbed: false,
|
|
689
|
+
c2paManifestEmbed: false,
|
|
690
|
+
watermarkProvider: null,
|
|
691
|
+
},
|
|
692
|
+
note: "Publish at /.well-known/ai-training-data-summary or model card per AI Office template (mandatory 2026-08-02)",
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
540
696
|
module.exports = {
|
|
541
697
|
classify: classify,
|
|
542
698
|
deployerChecklist: deployerChecklist,
|
|
@@ -545,9 +701,10 @@ module.exports = {
|
|
|
545
701
|
transparency: transparency,
|
|
546
702
|
logging: logging,
|
|
547
703
|
gpai: {
|
|
548
|
-
classify:
|
|
549
|
-
listObligations:
|
|
550
|
-
|
|
704
|
+
classify: gpaiClassify,
|
|
705
|
+
listObligations: listGpaiObligations,
|
|
706
|
+
trainingDataSummary: trainingDataSummary,
|
|
707
|
+
OBLIGATIONS: GPAI_OBLIGATIONS,
|
|
551
708
|
},
|
|
552
709
|
articleObligations: articleObligations,
|
|
553
710
|
listArticles: listArticles,
|
|
@@ -555,4 +712,5 @@ module.exports = {
|
|
|
555
712
|
DEADLINES: DEADLINES,
|
|
556
713
|
emitClassificationAudit: emitClassificationAudit,
|
|
557
714
|
annexIVScaffold: annexIVScaffold,
|
|
715
|
+
fundamentalRightsImpactAssessment: fundamentalRightsImpactAssessment,
|
|
558
716
|
};
|
package/lib/compliance.js
CHANGED
|
@@ -110,6 +110,32 @@ var KNOWN_POSTURES = Object.freeze([
|
|
|
110
110
|
"nydfs-500", // NYDFS 23 NYCRR 500 Amendment 2 — financial cybersecurity (multi-factor + asset inventory + governance)
|
|
111
111
|
"hipaa-2026", // HHS HIPAA Security Rule 2026-Q4 final — extends hipaa with mandatory MFA + asset inventory + 72h restoration testing
|
|
112
112
|
"quebec-25", // Quebec Law 25 final phase (effective 2026-09-22) — DPIA + automated-decision opt-out
|
|
113
|
+
// ---- v0.8.77 expansion — US state consumer-privacy postures ----
|
|
114
|
+
// Each posture carries per-state cure-period, profiling opt-out
|
|
115
|
+
// and minor-consent metadata via b.dsr.stateRules(state). The
|
|
116
|
+
// generic DSR primitive (b.dsr.submit) covers ~80% of the surface;
|
|
117
|
+
// these postures fill in the per-state drift.
|
|
118
|
+
"vcdpa", // Virginia Consumer Data Protection Act
|
|
119
|
+
"co-cpa", // Colorado Privacy Act
|
|
120
|
+
"ctdpa", // Connecticut Data Privacy Act
|
|
121
|
+
"ucpa", // Utah Consumer Privacy Act
|
|
122
|
+
"tdpsa", // Texas Data Privacy and Security Act
|
|
123
|
+
"or-cpa", // Oregon Consumer Privacy Act
|
|
124
|
+
"mt-cdpa", // Montana Consumer Data Privacy Act
|
|
125
|
+
"ia-icdpa", // Iowa Consumer Data Protection Act
|
|
126
|
+
"in-indpa", // Indiana Consumer Data Protection Act
|
|
127
|
+
"de-dpdpa", // Delaware Personal Data Privacy Act
|
|
128
|
+
"nh-nhpa", // New Hampshire SB 255 Consumer Privacy Act
|
|
129
|
+
"nj-njdpa", // New Jersey Data Privacy Act
|
|
130
|
+
"ky-kcdpa", // Kentucky Consumer Data Protection Act
|
|
131
|
+
"tn-tipa", // Tennessee Information Protection Act
|
|
132
|
+
"mn-mncdpa", // Minnesota Consumer Data Privacy Act
|
|
133
|
+
"ri-ricpa", // Rhode Island Consumer Privacy Act
|
|
134
|
+
"ne-dpa", // Nebraska Data Privacy Act
|
|
135
|
+
"nv-sb370", // Nevada SB 370 Consumer Health Data Privacy
|
|
136
|
+
"ca-aadc", // California Age-Appropriate Design Code (partial preliminary injunction; track for re-enforcement)
|
|
137
|
+
"ct-sb3", // Connecticut SB 3 Consumer Health Data
|
|
138
|
+
"tx-cubi", // Texas Capture or Use of Biometric Identifier
|
|
113
139
|
]);
|
|
114
140
|
|
|
115
141
|
var STATE = { posture: null, setAt: null };
|
|
@@ -493,6 +519,28 @@ var REGIME_MAP = Object.freeze({
|
|
|
493
519
|
jurisdiction: "CA-QC",
|
|
494
520
|
domain: "privacy",
|
|
495
521
|
},
|
|
522
|
+
// v0.8.77 — US state consumer-privacy postures
|
|
523
|
+
"vcdpa": { name: "Virginia Consumer Data Protection Act", citation: "Va. Code §59.1-575 et seq. (effective 2023-01-01)", jurisdiction: "US-VA", domain: "privacy" },
|
|
524
|
+
"co-cpa": { name: "Colorado Privacy Act", citation: "C.R.S. §6-1-1301 et seq. (effective 2023-07-01)", jurisdiction: "US-CO", domain: "privacy" },
|
|
525
|
+
"ctdpa": { name: "Connecticut Data Privacy Act", citation: "Conn. Gen. Stat. §42-515 et seq. (effective 2023-07-01)", jurisdiction: "US-CT", domain: "privacy" },
|
|
526
|
+
"ucpa": { name: "Utah Consumer Privacy Act", citation: "Utah Code §13-61-101 et seq. (effective 2023-12-31)", jurisdiction: "US-UT", domain: "privacy" },
|
|
527
|
+
"tdpsa": { name: "Texas Data Privacy and Security Act", citation: "Tex. Bus. & Com. Code §541.001 et seq. (effective 2024-07-01)", jurisdiction: "US-TX", domain: "privacy" },
|
|
528
|
+
"or-cpa": { name: "Oregon Consumer Privacy Act", citation: "Or. Rev. Stat. §646A.570 et seq. (effective 2024-07-01)", jurisdiction: "US-OR", domain: "privacy" },
|
|
529
|
+
"mt-cdpa": { name: "Montana Consumer Data Privacy Act", citation: "Mont. Code §30-14-2801 et seq. (effective 2024-10-01)", jurisdiction: "US-MT", domain: "privacy" },
|
|
530
|
+
"ia-icdpa": { name: "Iowa Consumer Data Protection Act", citation: "Iowa Code §715D (effective 2025-01-01)", jurisdiction: "US-IA", domain: "privacy" },
|
|
531
|
+
"in-indpa": { name: "Indiana Consumer Data Protection Act", citation: "Ind. Code §24-15 (effective 2026-01-01)", jurisdiction: "US-IN", domain: "privacy" },
|
|
532
|
+
"de-dpdpa": { name: "Delaware Personal Data Privacy Act", citation: "6 Del. Code Ch. 12D (effective 2026-01-01)", jurisdiction: "US-DE", domain: "privacy" },
|
|
533
|
+
"nh-nhpa": { name: "New Hampshire SB 255 Consumer Privacy Act", citation: "NH RSA Chapter 507-H (effective 2026-01-01)", jurisdiction: "US-NH", domain: "privacy" },
|
|
534
|
+
"nj-njdpa": { name: "New Jersey Data Privacy Act", citation: "N.J. Rev. Stat. §56:8-166.4 et seq. (effective 2026-01-15)", jurisdiction: "US-NJ", domain: "privacy" },
|
|
535
|
+
"ky-kcdpa": { name: "Kentucky Consumer Data Protection Act", citation: "Ky. Rev. Stat. §367.3611 et seq. (effective 2026-01-01)", jurisdiction: "US-KY", domain: "privacy" },
|
|
536
|
+
"tn-tipa": { name: "Tennessee Information Protection Act", citation: "Tenn. Code §47-18-3201 et seq. (effective 2025-07-01)", jurisdiction: "US-TN", domain: "privacy" },
|
|
537
|
+
"mn-mncdpa": { name: "Minnesota Consumer Data Privacy Act", citation: "Minn. Stat. §325O (effective 2026-07-31)", jurisdiction: "US-MN", domain: "privacy" },
|
|
538
|
+
"ri-ricpa": { name: "Rhode Island Consumer Privacy Act", citation: "R.I. Gen. Laws §6-48.1 (effective 2026-01-01)", jurisdiction: "US-RI", domain: "privacy" },
|
|
539
|
+
"ne-dpa": { name: "Nebraska Data Privacy Act", citation: "Neb. Rev. Stat. §87-1101 et seq. (effective 2025-01-01)", jurisdiction: "US-NE", domain: "privacy" },
|
|
540
|
+
"nv-sb370": { name: "Nevada SB 370 Consumer Health Data Privacy", citation: "Nev. Rev. Stat. §603A (consumer-health amendments, effective 2024-03-31)", jurisdiction: "US-NV", domain: "health" },
|
|
541
|
+
"ca-aadc": { name: "California Age-Appropriate Design Code Act", citation: "Cal. Civ. Code §1798.99.28 et seq. (partial preliminary injunction NetChoice v. Bonta)", jurisdiction: "US-CA", domain: "privacy" },
|
|
542
|
+
"ct-sb3": { name: "Connecticut SB 3 Consumer Health Data", citation: "Conn. P.A. 23-56 (effective 2023-07-01)", jurisdiction: "US-CT", domain: "health" },
|
|
543
|
+
"tx-cubi": { name: "Texas Capture or Use of Biometric Identifier", citation: "Tex. Bus. & Com. Code §503.001 (effective 2009-09-01)", jurisdiction: "US-TX", domain: "biometric" },
|
|
496
544
|
});
|
|
497
545
|
|
|
498
546
|
/**
|
package/lib/config.js
CHANGED
|
@@ -185,14 +185,25 @@ function create(opts) {
|
|
|
185
185
|
return value;
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
// `.value` is a getter, not a captured property. Without this,
|
|
189
|
+
// `cfg.value.X` reads from the object that was current at create()
|
|
190
|
+
// and never reflects subsequent reload() updates — operators looking
|
|
191
|
+
// at `cfg.value.FEATURE_X` would see stale values forever, while
|
|
192
|
+
// `cfg.get("FEATURE_X")` saw fresh ones. The @primitive docs
|
|
193
|
+
// (loadDbBacked example) promise `cfg.value.X` always works, so the
|
|
194
|
+
// getter is the contract.
|
|
195
|
+
var handle = {
|
|
190
196
|
get: function (key) { return value[key]; },
|
|
191
197
|
has: function (key) { return Object.prototype.hasOwnProperty.call(value, key); },
|
|
192
198
|
redacted: redactedView,
|
|
193
199
|
subscribe: subscribe,
|
|
194
200
|
reload: reload,
|
|
195
201
|
};
|
|
202
|
+
Object.defineProperty(handle, "value", {
|
|
203
|
+
get: function () { return value; },
|
|
204
|
+
enumerable: true,
|
|
205
|
+
});
|
|
206
|
+
return handle;
|
|
196
207
|
}
|
|
197
208
|
|
|
198
209
|
/**
|
|
@@ -209,16 +220,35 @@ function create(opts) {
|
|
|
209
220
|
* via the underlying handle's `reload`, and re-validates. Reload
|
|
210
221
|
* failures emit a `config.reload.failed` audit row but DO NOT
|
|
211
222
|
* clobber the previous value — the running app stays on the
|
|
212
|
-
* last-good config.
|
|
213
|
-
*
|
|
223
|
+
* last-good config.
|
|
224
|
+
*
|
|
225
|
+
* Returns immediately with a synchronous handle, but kicks off one
|
|
226
|
+
* immediate hydration tick on construction so the first DB read
|
|
227
|
+
* happens at t=0 rather than t=intervalMs. Callers that need to wait
|
|
228
|
+
* for first-data-applied can `await handle.hydrated` before the app
|
|
229
|
+
* starts serving traffic; the Promise resolves after the first tick
|
|
230
|
+
* settles (success OR audit-on-failure path) and never rejects, so
|
|
231
|
+
* the boot path never deadlocks on a temporarily-unreachable DB.
|
|
232
|
+
*
|
|
233
|
+
* The returned handle is the same shape as `create()` plus:
|
|
234
|
+
* - `.hydrated` — Promise<void> for the first tick
|
|
235
|
+
* - `.stop()` — halts the poller
|
|
214
236
|
*
|
|
215
237
|
* @opts
|
|
216
|
-
* schema:
|
|
217
|
-
* env:
|
|
218
|
-
* redactKeys:
|
|
219
|
-
* fetchRows:
|
|
220
|
-
* intervalMs:
|
|
221
|
-
*
|
|
238
|
+
* schema: b.safeSchema instance (required),
|
|
239
|
+
* env: object (env baseline; default process.env),
|
|
240
|
+
* redactKeys: Array<string>,
|
|
241
|
+
* fetchRows: async () => Array<{ key: string, value: string }> (required),
|
|
242
|
+
* intervalMs: number (positive finite poll interval),
|
|
243
|
+
* transformValue: (row) => string | Promise<string> (optional per-row
|
|
244
|
+
* transform — receives `{ key, value, ...rest }` so the
|
|
245
|
+
* row can carry envelope metadata; returns the value
|
|
246
|
+
* that flows into the schema. Common shape: unseal a
|
|
247
|
+
* `b.vault`-sealed ciphertext column before validation.
|
|
248
|
+
* Rows whose transform throws or returns a non-string
|
|
249
|
+
* are skipped with a `config.reload.failed` audit so a
|
|
250
|
+
* single bad row never crashes the poller),
|
|
251
|
+
* audit: boolean (default true; reserved for future per-poll audit),
|
|
222
252
|
*
|
|
223
253
|
* @example
|
|
224
254
|
* var s = b.safeSchema;
|
|
@@ -234,10 +264,23 @@ function create(opts) {
|
|
|
234
264
|
* });
|
|
235
265
|
* cfg.value.FEATURE_X; // → false (until first poll tick lands)
|
|
236
266
|
* cfg.stop(); // halt the poller on shutdown
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* // Sealed values — column stores `b.vault.seal(plain)` ciphertext.
|
|
270
|
+
* var cfg = b.config.loadDbBacked({
|
|
271
|
+
* schema: s.object({ STRIPE_SECRET: s.string() }),
|
|
272
|
+
* fetchRows: async function () {
|
|
273
|
+
* return await db.all("SELECT key, sealed FROM _config WHERE sealed IS NOT NULL");
|
|
274
|
+
* },
|
|
275
|
+
* transformValue: function (row) {
|
|
276
|
+
* return b.vault.unseal(row.sealed).toString("utf8");
|
|
277
|
+
* },
|
|
278
|
+
* intervalMs: 30 * 1000,
|
|
279
|
+
* });
|
|
237
280
|
*/
|
|
238
281
|
function loadDbBacked(opts) {
|
|
239
282
|
opts = opts || {};
|
|
240
|
-
validateOpts(opts, ["schema", "env", "redactKeys", "fetchRows", "intervalMs", "audit"],
|
|
283
|
+
validateOpts(opts, ["schema", "env", "redactKeys", "fetchRows", "intervalMs", "transformValue", "audit"],
|
|
241
284
|
"config.loadDbBacked");
|
|
242
285
|
if (typeof opts.fetchRows !== "function") {
|
|
243
286
|
throw new ConfigError("config/bad-fetch-rows",
|
|
@@ -247,6 +290,9 @@ function loadDbBacked(opts) {
|
|
|
247
290
|
throw new ConfigError("config/bad-interval",
|
|
248
291
|
"loadDbBacked: opts.intervalMs must be a positive finite number");
|
|
249
292
|
}
|
|
293
|
+
var transformValue = validateOpts.optionalFunction(
|
|
294
|
+
opts.transformValue, "loadDbBacked: opts.transformValue",
|
|
295
|
+
ConfigError, "config/bad-transform-value") || null;
|
|
250
296
|
var cfg = create({ schema: opts.schema, env: opts.env, redactKeys: opts.redactKeys });
|
|
251
297
|
var stopped = false;
|
|
252
298
|
async function _tick() {
|
|
@@ -265,9 +311,32 @@ function loadDbBacked(opts) {
|
|
|
265
311
|
if (!Array.isArray(rows)) return;
|
|
266
312
|
var overlay = {};
|
|
267
313
|
for (var i = 0; i < rows.length; i++) {
|
|
268
|
-
|
|
269
|
-
|
|
314
|
+
var row = rows[i];
|
|
315
|
+
if (!row || typeof row.key !== "string") continue;
|
|
316
|
+
var value = row.value;
|
|
317
|
+
if (transformValue) {
|
|
318
|
+
try {
|
|
319
|
+
value = await transformValue(row);
|
|
320
|
+
} catch (e) {
|
|
321
|
+
try {
|
|
322
|
+
lazyAudit().safeEmit({
|
|
323
|
+
action: "config.reload.failed", outcome: "failure",
|
|
324
|
+
metadata: { phase: "transform", key: row.key, reason: e && e.message },
|
|
325
|
+
});
|
|
326
|
+
} catch (_e) { /* audit best-effort */ }
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (typeof value !== "string") {
|
|
330
|
+
try {
|
|
331
|
+
lazyAudit().safeEmit({
|
|
332
|
+
action: "config.reload.failed", outcome: "failure",
|
|
333
|
+
metadata: { phase: "transform", key: row.key, reason: "transformValue did not return a string" },
|
|
334
|
+
});
|
|
335
|
+
} catch (_e) { /* audit best-effort */ }
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
270
338
|
}
|
|
339
|
+
overlay[row.key] = value;
|
|
271
340
|
}
|
|
272
341
|
try { cfg.reload(overlay); }
|
|
273
342
|
catch (e) {
|
|
@@ -279,6 +348,14 @@ function loadDbBacked(opts) {
|
|
|
279
348
|
} catch (_e) { /* audit best-effort */ }
|
|
280
349
|
}
|
|
281
350
|
}
|
|
351
|
+
// Fire one immediate hydration before the interval kicks in so
|
|
352
|
+
// callers can `await cfg.hydrated` and not get an empty config window
|
|
353
|
+
// (env defaults only) for the first intervalMs of process lifetime.
|
|
354
|
+
// The interval still fires every intervalMs afterwards for ongoing
|
|
355
|
+
// drift detection. The hydration Promise NEVER rejects — _tick
|
|
356
|
+
// swallows fetch / transform / validate failures via audit, matching
|
|
357
|
+
// the established "last-good config stays in place" contract.
|
|
358
|
+
cfg.hydrated = _tick();
|
|
282
359
|
var handle = safeAsync.repeating(_tick, opts.intervalMs, { name: "config-db-reload" });
|
|
283
360
|
cfg.stop = function () { stopped = true; if (handle) { handle.stop(); handle = null; } };
|
|
284
361
|
return cfg;
|
|
@@ -327,11 +327,238 @@ function verify(envelope, publicKeyPem, opts) {
|
|
|
327
327
|
return { valid: true, claims: envelope.manifest, reason: null };
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
// ---- C2PA 2.x COSE_Sign1 interop wrapper -------------------------
|
|
331
|
+
//
|
|
332
|
+
// Framework's `sign()` produces a JCS-canonicalized + ML-DSA-87/SLH-DSA
|
|
333
|
+
// signature shape — fine for blamejs-internal verifiers but does NOT
|
|
334
|
+
// interop with the c2patool / JPEG Trust / Adobe verifiers, which
|
|
335
|
+
// expect COSE_Sign1 (RFC 9052) per C2PA spec §11.
|
|
336
|
+
//
|
|
337
|
+
// `signCose` wraps the same manifest payload in a minimal COSE_Sign1
|
|
338
|
+
// CBOR structure with:
|
|
339
|
+
// - protected header { 1: alg } (RFC 9052 §3.1)
|
|
340
|
+
// - unprotected header { 33: x5chain } if certChain supplied
|
|
341
|
+
// - payload: the JCS-canonicalized manifest bytes
|
|
342
|
+
// - signature: the ML-DSA-87 / Ed25519 signature
|
|
343
|
+
//
|
|
344
|
+
// The CBOR is hand-encoded — keeps the framework's "zero npm runtime
|
|
345
|
+
// deps" rule intact. Verifiers consume the bytes via standard COSE
|
|
346
|
+
// libraries (jose-py / c2pa-rs / etc.).
|
|
347
|
+
|
|
348
|
+
// COSE algorithm registry codepoints (RFC 9053 §2.1 + draft-ietf-cose-* for PQ).
|
|
349
|
+
// allow:raw-byte-literal — IANA registry IDs, not byte counts.
|
|
350
|
+
var COSE_ALGS = {
|
|
351
|
+
"ed25519": -8, // allow:raw-byte-literal — COSE alg id
|
|
352
|
+
"es256": -7, // allow:raw-byte-literal — COSE alg id
|
|
353
|
+
"es384": -35, // allow:raw-byte-literal — COSE alg id
|
|
354
|
+
"es512": -36, // allow:raw-byte-literal — COSE alg id
|
|
355
|
+
"ml-dsa-44": -48, // allow:raw-byte-literal — COSE alg id (draft)
|
|
356
|
+
"ml-dsa-65": -49, // allow:raw-byte-literal — COSE alg id (draft)
|
|
357
|
+
"ml-dsa-87": -50, // allow:raw-byte-literal — COSE alg id (draft)
|
|
358
|
+
"slh-dsa-sha2-128s": -51, // allow:raw-byte-literal — COSE alg id (draft)
|
|
359
|
+
"slh-dsa-shake-256f": -56, // allow:raw-byte-literal — COSE alg id (draft)
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// CBOR encoder (RFC 8949 §3). The integer thresholds 24/256/65536/4294967296
|
|
363
|
+
// are CBOR-spec length-encoding boundaries — not byte counts.
|
|
364
|
+
// allow:raw-byte-literal — CBOR encoding thresholds, not byte counts.
|
|
365
|
+
function _cborUint(n) {
|
|
366
|
+
if (n < 24) return Buffer.from([n]); // allow:raw-byte-literal — CBOR threshold
|
|
367
|
+
if (n < 256) return Buffer.from([0x18, n]); // allow:raw-byte-literal — CBOR threshold
|
|
368
|
+
if (n < 65536) return Buffer.from([0x19, (n >> 8) & 0xFF, n & 0xFF]); // allow:raw-byte-literal — CBOR threshold
|
|
369
|
+
if (n < 4294967296) return Buffer.from([0x1A, (n >> 24) & 0xFF, (n >> 16) & 0xFF, (n >> 8) & 0xFF, n & 0xFF]); // allow:raw-byte-literal — CBOR threshold
|
|
370
|
+
throw ContentCredentialsError.factory("CBOR_OVERFLOW", "cbor uint too large: " + n);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function _cborNint(n) {
|
|
374
|
+
var v = -1 - n;
|
|
375
|
+
if (v < 24) return Buffer.from([0x20 | v]); // allow:raw-byte-literal — CBOR threshold
|
|
376
|
+
if (v < 256) return Buffer.from([0x38, v]); // allow:raw-byte-literal — CBOR threshold
|
|
377
|
+
if (v < 65536) return Buffer.from([0x39, (v >> 8) & 0xFF, v & 0xFF]); // allow:raw-byte-literal — CBOR threshold
|
|
378
|
+
return Buffer.from([0x3A, (v >> 24) & 0xFF, (v >> 16) & 0xFF, (v >> 8) & 0xFF, v & 0xFF]);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function _cborInt(n) {
|
|
382
|
+
return n >= 0 ? _cborUint(n) : _cborNint(n);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function _cborBytes(buf) {
|
|
386
|
+
var n = buf.length;
|
|
387
|
+
var head;
|
|
388
|
+
if (n < 24) head = Buffer.from([0x40 | n]); // allow:raw-byte-literal — CBOR threshold
|
|
389
|
+
else if (n < 256) head = Buffer.from([0x58, n]); // allow:raw-byte-literal — CBOR threshold
|
|
390
|
+
else if (n < 65536) head = Buffer.from([0x59, (n >> 8) & 0xFF, n & 0xFF]); // allow:raw-byte-literal — CBOR threshold
|
|
391
|
+
else head = Buffer.from([0x5A, (n >>> 24) & 0xFF, (n >> 16) & 0xFF, (n >> 8) & 0xFF, n & 0xFF]);
|
|
392
|
+
return Buffer.concat([head, buf]);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function _cborArrayHeader(n) {
|
|
396
|
+
if (n < 24) return Buffer.from([0x80 | n]); // allow:raw-byte-literal — CBOR threshold
|
|
397
|
+
if (n < 256) return Buffer.from([0x98, n]); // allow:raw-byte-literal — CBOR threshold
|
|
398
|
+
if (n < 65536) return Buffer.from([0x99, (n >> 8) & 0xFF, n & 0xFF]); // allow:raw-byte-literal — CBOR threshold
|
|
399
|
+
throw ContentCredentialsError.factory("CBOR_OVERFLOW", "cbor array too large: " + n);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function _cborMapHeader(n) {
|
|
403
|
+
if (n < 24) return Buffer.from([0xA0 | n]); // allow:raw-byte-literal — CBOR threshold
|
|
404
|
+
if (n < 256) return Buffer.from([0xB8, n]); // allow:raw-byte-literal — CBOR threshold
|
|
405
|
+
throw ContentCredentialsError.factory("CBOR_OVERFLOW", "cbor map too large: " + n);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function _cborTag(tag) {
|
|
409
|
+
if (tag < 24) return Buffer.from([0xC0 | tag]); // allow:raw-byte-literal — CBOR threshold
|
|
410
|
+
if (tag < 256) return Buffer.from([0xD8, tag]); // allow:raw-byte-literal — CBOR threshold
|
|
411
|
+
if (tag < 65536) return Buffer.from([0xD9, (tag >> 8) & 0xFF, tag & 0xFF]); // allow:raw-byte-literal — CBOR threshold
|
|
412
|
+
return Buffer.from([0xDA, (tag >> 24) & 0xFF, (tag >> 16) & 0xFF, (tag >> 8) & 0xFF, tag & 0xFF]);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* @primitive b.contentCredentials.signCose
|
|
417
|
+
* @signature b.contentCredentials.signCose(manifest, opts)
|
|
418
|
+
* @since 0.8.77
|
|
419
|
+
* @related b.contentCredentials.sign
|
|
420
|
+
*
|
|
421
|
+
* C2PA 2.x interop sign — wraps the manifest in a COSE_Sign1 CBOR
|
|
422
|
+
* envelope (RFC 9052) so the result interops with c2patool / JPEG
|
|
423
|
+
* Trust / Adobe / external C2PA verifiers. The simpler `sign()`
|
|
424
|
+
* primitive ships a blamejs-internal envelope shape; this one ships
|
|
425
|
+
* COSE bytes.
|
|
426
|
+
*
|
|
427
|
+
* Returns `{ manifest, coseSign1: Buffer, alg }`. Operators embed
|
|
428
|
+
* the `coseSign1` Buffer in the image's C2PA box (JPEG XT marker,
|
|
429
|
+
* PNG iTXt chunk, MP4 'jumb' box per C2PA §13).
|
|
430
|
+
*
|
|
431
|
+
* @opts
|
|
432
|
+
* {
|
|
433
|
+
* privateKeyPem: string, // required
|
|
434
|
+
* alg?: "ed25519" | "es256" | "es384" | "es512" |
|
|
435
|
+
* "ml-dsa-44" | "ml-dsa-65" | "ml-dsa-87" |
|
|
436
|
+
* "slh-dsa-shake-256f", // default "ml-dsa-87"
|
|
437
|
+
* certChain?: Buffer[], // X.509 DER buffers; emitted as x5chain (header label 33)
|
|
438
|
+
* audit?: boolean, // default true
|
|
439
|
+
* }
|
|
440
|
+
*
|
|
441
|
+
* @example
|
|
442
|
+
* var pair = b.crypto.generateSigningKeyPair("ml-dsa-87");
|
|
443
|
+
* var manifest = b.contentCredentials.build({
|
|
444
|
+
* provider: "Acme AI", system: "acme-v3",
|
|
445
|
+
* systemVersion: "3.2.1", contentId: "img-001",
|
|
446
|
+
* });
|
|
447
|
+
* var cose = b.contentCredentials.signCose(manifest, {
|
|
448
|
+
* privateKeyPem: pair.privateKey,
|
|
449
|
+
* alg: "ml-dsa-87",
|
|
450
|
+
* });
|
|
451
|
+
* // cose.coseSign1 is the CBOR bytes to embed in the image's C2PA box.
|
|
452
|
+
*/
|
|
453
|
+
function signCose(manifest, opts) {
|
|
454
|
+
opts = opts || {};
|
|
455
|
+
if (!manifest || typeof manifest !== "object") {
|
|
456
|
+
throw ContentCredentialsError.factory("BAD_MANIFEST",
|
|
457
|
+
"contentCredentials.signCose: manifest required");
|
|
458
|
+
}
|
|
459
|
+
validateOpts.requireNonEmptyString(opts.privateKeyPem,
|
|
460
|
+
"contentCredentials.signCose: privateKeyPem", ContentCredentialsError, "BAD_KEY");
|
|
461
|
+
var algName = (opts.alg || "ml-dsa-87").toLowerCase();
|
|
462
|
+
if (!(algName in COSE_ALGS)) {
|
|
463
|
+
throw ContentCredentialsError.factory("BAD_ALG",
|
|
464
|
+
"contentCredentials.signCose: alg '" + algName +
|
|
465
|
+
"' not in COSE alg registry. Known: " + Object.keys(COSE_ALGS).join(", "));
|
|
466
|
+
}
|
|
467
|
+
var algId = COSE_ALGS[algName];
|
|
468
|
+
|
|
469
|
+
// Protected header: map { 1: alg }
|
|
470
|
+
var protBytes = Buffer.concat([
|
|
471
|
+
_cborMapHeader(1),
|
|
472
|
+
_cborInt(1), // key: 1 (alg)
|
|
473
|
+
_cborInt(algId), // value: COSE alg id
|
|
474
|
+
]);
|
|
475
|
+
var protectedBstr = _cborBytes(protBytes);
|
|
476
|
+
|
|
477
|
+
// Unprotected header: map { 33: x5chain } when cert chain supplied;
|
|
478
|
+
// else empty map {}.
|
|
479
|
+
var unprotectedHdr;
|
|
480
|
+
if (Array.isArray(opts.certChain) && opts.certChain.length > 0) {
|
|
481
|
+
var chainArray;
|
|
482
|
+
if (opts.certChain.length === 1) {
|
|
483
|
+
// Single-cert form: header value is the DER bytes directly.
|
|
484
|
+
chainArray = _cborBytes(opts.certChain[0]);
|
|
485
|
+
} else {
|
|
486
|
+
var chainBufs = [_cborArrayHeader(opts.certChain.length)];
|
|
487
|
+
opts.certChain.forEach(function (der) {
|
|
488
|
+
chainBufs.push(_cborBytes(der));
|
|
489
|
+
});
|
|
490
|
+
chainArray = Buffer.concat(chainBufs);
|
|
491
|
+
}
|
|
492
|
+
unprotectedHdr = Buffer.concat([
|
|
493
|
+
_cborMapHeader(1),
|
|
494
|
+
_cborInt(33), // allow:raw-byte-literal allow:raw-time-literal — RFC 9360 x5chain header label, not a duration
|
|
495
|
+
chainArray,
|
|
496
|
+
]);
|
|
497
|
+
} else {
|
|
498
|
+
unprotectedHdr = _cborMapHeader(0); // empty {}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Payload — canonicalized manifest bytes.
|
|
502
|
+
var canonicalPayload = Buffer.from(canonicalJson.stringify(manifest), "utf8");
|
|
503
|
+
var payloadBstr = _cborBytes(canonicalPayload);
|
|
504
|
+
|
|
505
|
+
// Sig_structure per RFC 9052 §4.4: ["Signature1", protected, external_aad="", payload]
|
|
506
|
+
var sigStructureBufs = [
|
|
507
|
+
_cborArrayHeader(4),
|
|
508
|
+
Buffer.concat([_cborBytes(Buffer.from("Signature1", "utf8"))]),
|
|
509
|
+
protectedBstr,
|
|
510
|
+
_cborBytes(Buffer.alloc(0)), // external_aad (empty)
|
|
511
|
+
payloadBstr,
|
|
512
|
+
];
|
|
513
|
+
// First entry is the text string "Signature1" — major-type 3
|
|
514
|
+
var sigText = Buffer.from("Signature1", "utf8");
|
|
515
|
+
var sigTextBstr;
|
|
516
|
+
if (sigText.length < 24) sigTextBstr = Buffer.concat([Buffer.from([0x60 | sigText.length]), sigText]); // allow:raw-byte-literal — CBOR text-string threshold
|
|
517
|
+
else sigTextBstr = Buffer.concat([Buffer.from([0x78, sigText.length]), sigText]);
|
|
518
|
+
sigStructureBufs[1] = sigTextBstr;
|
|
519
|
+
var toBeSigned = Buffer.concat(sigStructureBufs);
|
|
520
|
+
|
|
521
|
+
// Sign with framework's b.crypto.sign — algorithm picked from the PEM.
|
|
522
|
+
var signature = crypto.sign(toBeSigned, opts.privateKeyPem);
|
|
523
|
+
|
|
524
|
+
// COSE_Sign1 = tagged-18 array [protected, unprotected, payload, signature]
|
|
525
|
+
var coseSign1 = Buffer.concat([
|
|
526
|
+
_cborTag(18), // CBOR tag 18 = COSE_Sign1
|
|
527
|
+
_cborArrayHeader(4),
|
|
528
|
+
protectedBstr,
|
|
529
|
+
unprotectedHdr,
|
|
530
|
+
payloadBstr,
|
|
531
|
+
_cborBytes(signature),
|
|
532
|
+
]);
|
|
533
|
+
|
|
534
|
+
if (opts.audit !== false) {
|
|
535
|
+
audit.safeEmit({
|
|
536
|
+
action: "contentcredentials.signed_cose",
|
|
537
|
+
outcome: "success",
|
|
538
|
+
metadata: {
|
|
539
|
+
provider: manifest.provider && manifest.provider.name,
|
|
540
|
+
system: manifest.system && manifest.system.id,
|
|
541
|
+
contentId: manifest.content && manifest.content.id,
|
|
542
|
+
alg: algName,
|
|
543
|
+
bytes: coseSign1.length,
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return {
|
|
549
|
+
manifest: manifest,
|
|
550
|
+
coseSign1: coseSign1,
|
|
551
|
+
alg: algName,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
|
|
330
555
|
module.exports = {
|
|
331
556
|
build: build,
|
|
332
557
|
sign: sign,
|
|
558
|
+
signCose: signCose,
|
|
333
559
|
verify: verify,
|
|
334
560
|
required: required,
|
|
335
561
|
REQUIRED_FIELDS: REQUIRED_FIELDS.slice(),
|
|
562
|
+
COSE_ALGS: Object.assign({}, COSE_ALGS),
|
|
336
563
|
ContentCredentialsError: ContentCredentialsError,
|
|
337
564
|
};
|