@blamejs/core 0.8.72 → 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.
@@ -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: gpaiClassify,
549
- listObligations: listGpaiObligations,
550
- OBLIGATIONS: GPAI_OBLIGATIONS,
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
- return {
189
- value: value,
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. The returned handle is the same shape as
213
- * `create()` plus a `.stop()` method that halts the poller.
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: b.safeSchema instance (required),
217
- * env: object (env baseline; default process.env),
218
- * redactKeys: Array<string>,
219
- * fetchRows: async () => Array<{ key: string, value: string }> (required),
220
- * intervalMs: number (positive finite poll interval),
221
- * audit: boolean (default true; reserved for future per-poll audit),
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
- if (rows[i] && typeof rows[i].key === "string") {
269
- overlay[rows[i].key] = rows[i].value;
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
  };