@blamejs/core 0.8.76 → 0.8.78

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.
@@ -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
  };
package/lib/cra-report.js CHANGED
@@ -189,7 +189,111 @@ function create(opts) {
189
189
  };
190
190
  }
191
191
 
192
+ /**
193
+ * @primitive b.cra.conformityAssessment
194
+ * @signature b.cra.conformityAssessment(opts)
195
+ * @since 0.8.77
196
+ *
197
+ * EU Cyber Resilience Act (Regulation 2024/2847) — Annex VIII
198
+ * conformity-assessment dossier scaffold. Returns the structured
199
+ * JSON document operators submit to the notified body (Module B/C/D/H
200
+ * route per Annex VII) or self-attest under Annex VI (default for
201
+ * non-critical products). The framework auto-fills sections it can
202
+ * derive from the runtime — SBOM (`sbom.cdx.json` + `sbom.vendored.cdx.json`),
203
+ * vulnerability-handling process (CVD per RFC 9116 + SECURITY.md),
204
+ * security-by-design defaults (cite SECURITY.md threat-model
205
+ * section), end-of-life schedule (operator-supplied) — and leaves
206
+ * Annex I Part II essential-cybersecurity-requirements mapping for
207
+ * the operator to fill (it's product-specific).
208
+ *
209
+ * Enforcement: products placed on the EU market on/after 2027-12-11
210
+ * require a CE marking that depends on this dossier. Notified-body
211
+ * review takes 60-90 days for self-certifying products. Run this
212
+ * primitive at release time + commit the output under `compliance/cra/`.
213
+ *
214
+ * @opts
215
+ * {
216
+ * manufacturer: { name, address, contact },
217
+ * product: { name, identifier, version, description },
218
+ * classification: "default" | "important-class-I" | "important-class-II" | "critical",
219
+ * sbomPaths: string[], // paths to attached SBOMs
220
+ * supportEnd: string, // ISO date — manufacturer support cessation
221
+ * vulnDisclosurePolicy?: string, // URL to /.well-known/security.txt or VDP
222
+ * essentialReqMapping?: object, // operator-supplied Annex I Part II mapping
223
+ * }
224
+ *
225
+ * @example
226
+ * var dossier = b.cra.conformityAssessment({
227
+ * manufacturer: { name: "Acme Inc.", address: "1 St", contact: "ce@acme.example" },
228
+ * product: { name: "Widget Pro", identifier: "WID-001", version: "1.0", description: "..." },
229
+ * classification: "default",
230
+ * supportEnd: "2032-12-31",
231
+ * });
232
+ */
233
+ function conformityAssessment(opts) {
234
+ if (!opts || typeof opts !== "object") {
235
+ throw new CraReportError("cra-report/bad-conformity-opts",
236
+ "conformityAssessment: opts required");
237
+ }
238
+ if (!opts.manufacturer || typeof opts.manufacturer.name !== "string") {
239
+ throw new CraReportError("cra-report/no-manufacturer",
240
+ "conformityAssessment: opts.manufacturer.name required");
241
+ }
242
+ if (!opts.product || typeof opts.product.name !== "string") {
243
+ throw new CraReportError("cra-report/no-product",
244
+ "conformityAssessment: opts.product.name required");
245
+ }
246
+ var classification = opts.classification || "default";
247
+ var validClasses = ["default", "important-class-I", "important-class-II", "critical"];
248
+ if (validClasses.indexOf(classification) === -1) {
249
+ throw new CraReportError("cra-report/bad-classification",
250
+ "conformityAssessment: classification must be one of " + validClasses.join(", "));
251
+ }
252
+ return {
253
+ "$schema": "https://blamejs.com/schema/cra-conformity-assessment-v1.json",
254
+ regulation: "EU 2024/2847 (Cyber Resilience Act)",
255
+ annex: "Annex VIII (technical documentation)",
256
+ generatedAt: new Date().toISOString(),
257
+ manufacturer: opts.manufacturer,
258
+ product: opts.product,
259
+ classification: classification,
260
+ assessmentRoute:
261
+ classification === "default" ? "Module A (Annex VI — internal control)" :
262
+ classification === "important-class-I" ? "Module B+C (Annex VII — EU-type examination)" :
263
+ classification === "important-class-II" ? "Module H (Annex VII — full quality assurance)" :
264
+ "Module H + notified-body for critical (Annex VII)",
265
+ sections: {
266
+ annexI_part1_essentialRequirements: {
267
+ status: "operator-supplied",
268
+ mapping: opts.essentialReqMapping || null,
269
+ note: "Annex I Part I essential cybersecurity requirements — operator supplies the mapping",
270
+ },
271
+ annexI_part2_vulnerabilityHandling: {
272
+ status: "framework-derived",
273
+ sbomAttached: Array.isArray(opts.sbomPaths) ? opts.sbomPaths : ["sbom.cdx.json", "sbom.vendored.cdx.json"],
274
+ vulnDisclosurePolicy: opts.vulnDisclosurePolicy || "https://blamejs.com/.well-known/security.txt",
275
+ cvdProcess: "Coordinated Vulnerability Disclosure per ISO/IEC 29147 + 30111",
276
+ incidentReporter: "b.cra (24h early warning + 14d intermediate + 1m final per Art 14)",
277
+ },
278
+ annexII_userInformation: {
279
+ status: "operator-supplied",
280
+ note: "Operator emits per-product handover docs",
281
+ },
282
+ supportPeriod: {
283
+ end: opts.supportEnd || null,
284
+ note: "Manufacturer support-cessation date triggers end-of-life obligations per Art 13(8)",
285
+ },
286
+ },
287
+ declarations: {
288
+ ceMarking: classification === "critical" ? "requires notified body" : "self-attest eligible",
289
+ eolNotification: "Manufacturer commits to 60-day pre-EOL notification per Art 13(8)",
290
+ vulnReporting: "Active exploitation reported within 24h to ENISA per Art 14(2)",
291
+ },
292
+ };
293
+ }
294
+
192
295
  module.exports = {
193
- create: create,
194
- CraReportError: CraReportError,
296
+ create: create,
297
+ conformityAssessment: conformityAssessment,
298
+ CraReportError: CraReportError,
195
299
  };
@@ -895,6 +895,11 @@ module.exports = {
895
895
  getSealedFields: getSealedFields,
896
896
  sealRow: sealRow,
897
897
  unsealRow: unsealRow,
898
+ // Doc-shaped aliases — operators / tests preparing a JS document
899
+ // object (vs. a SQL row) reach for sealDoc / unsealDoc naming. Same
900
+ // function, identical shape, returns a new object (input untouched).
901
+ sealDoc: sealRow,
902
+ unsealDoc: unsealRow,
898
903
  eraseRow: eraseRow,
899
904
  applyPosture: applyPosture,
900
905
  getActivePosture: getActivePosture,
package/lib/dsr.js CHANGED
@@ -1071,6 +1071,100 @@ function dbTicketStore(opts) {
1071
1071
  };
1072
1072
  }
1073
1073
 
1074
+ // ---- v0.8.77 — US state-law DSR drift registry -------------------
1075
+ //
1076
+ // Each US state consumer-privacy law expresses the same DSR core
1077
+ // (access / deletion / correction / portability) but with per-state
1078
+ // drift on three knobs: cure-period (days between operator-receipt
1079
+ // and statutory-deadline-to-respond), profiling-opt-out
1080
+ // (right-to-limit-automated-decision-making variants), and minor-
1081
+ // consent (age threshold + opt-in vs. opt-out vs. parental-VPC).
1082
+ //
1083
+ // `b.dsr.stateRules(state)` returns the metadata; operators feed it
1084
+ // into their own DSR ticket-routing layer to surface "this VA
1085
+ // resident's correction request must be acknowledged within 45 days
1086
+ // with one 45-day extension".
1087
+
1088
+ // State DSR rule table — `responseDays` / `extensionDays` / `cureDays`
1089
+ // are integer day-counts from per-state statutes (not durations in
1090
+ // seconds/ms). allow:raw-time-literal — statute-defined day counts.
1091
+ var STATE_RULES = Object.freeze({
1092
+ "vcdpa": { posture: "vcdpa", state: "VA", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Cure right sunset 2025-01-01" }, // allow:raw-time-literal
1093
+ "co-cpa": { posture: "co-cpa", state: "CO", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Cure right sunset 2025-01-01; UOOM (GPC) mandatory" }, // allow:raw-time-literal
1094
+ "ctdpa": { posture: "ctdpa", state: "CT", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Cure right sunset 2025-01-01; GPC mandatory" }, // allow:raw-time-literal
1095
+ "ucpa": { posture: "ucpa", state: "UT", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: false, minorOptIn: 13, notes: "Narrowest scope; no cure-period sunset" }, // allow:raw-time-literal
1096
+ "tdpsa": { posture: "tdpsa", state: "TX", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Small-business carve-out applies" }, // allow:raw-time-literal
1097
+ "or-cpa": { posture: "or-cpa", state: "OR", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Specific-third-party-name DSR enhancement" }, // allow:raw-time-literal
1098
+ "mt-cdpa": { posture: "mt-cdpa", state: "MT", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Cure period sunsets 2026-04-01" }, // allow:raw-time-literal
1099
+ "ia-icdpa": { posture: "ia-icdpa", state: "IA", responseDays: 90, extensionDays: 45, cureDays: 90, profilingOptOut: false, minorOptIn: null, notes: "Weakest framework — longest response, no profiling opt-out" }, // allow:raw-time-literal
1100
+ "in-indpa": { posture: "in-indpa", state: "IN", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-01-01" }, // allow:raw-time-literal
1101
+ "de-dpdpa": { posture: "de-dpdpa", state: "DE", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-01-01" }, // allow:raw-time-literal
1102
+ "nh-nhpa": { posture: "nh-nhpa", state: "NH", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-01-01; cure right sunset 2026-01-01" }, // allow:raw-time-literal
1103
+ "nj-njdpa": { posture: "nj-njdpa", state: "NJ", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 17, notes: "Under-17 opt-in default" }, // allow:raw-time-literal
1104
+ "ky-kcdpa": { posture: "ky-kcdpa", state: "KY", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-01-01" }, // allow:raw-time-literal
1105
+ "tn-tipa": { posture: "tn-tipa", state: "TN", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "NIST CSF safe-harbor available" }, // allow:raw-time-literal
1106
+ "mn-mncdpa": { posture: "mn-mncdpa", state: "MN", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-07-31; profiling opt-out for consequential decisions" }, // allow:raw-time-literal
1107
+ "ri-ricpa": { posture: "ri-ricpa", state: "RI", responseDays: 45, extensionDays: 45, cureDays: 0, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2026-01-01; no cure period" }, // allow:raw-time-literal
1108
+ "ne-dpa": { posture: "ne-dpa", state: "NE", responseDays: 45, extensionDays: 45, cureDays: 30, profilingOptOut: true, minorOptIn: 13, notes: "Effective 2025-01-01" }, // allow:raw-time-literal
1109
+ "nv-sb370": { posture: "nv-sb370", state: "NV", responseDays: 60, extensionDays: 30, cureDays: 0, profilingOptOut: false, minorOptIn: null, notes: "Consumer-health data only" }, // allow:raw-time-literal
1110
+ "ca-aadc": { posture: "ca-aadc", state: "CA", responseDays: 0, extensionDays: 0, cureDays: 90, profilingOptOut: true, minorOptIn: 18, notes: "Under-18 default-high-privacy; partial preliminary injunction NetChoice v. Bonta" }, // allow:raw-time-literal
1111
+ "ct-sb3": { posture: "ct-sb3", state: "CT", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: false, minorOptIn: null, notes: "Consumer-health data only" }, // allow:raw-time-literal
1112
+ "tx-cubi": { posture: "tx-cubi", state: "TX", responseDays: 0, extensionDays: 0, cureDays: 0, profilingOptOut: false, minorOptIn: null, notes: "Biometric-only; private-right-of-action absent" }, // allow:raw-time-literal
1113
+ "modpa": { posture: "modpa", state: "MD", responseDays: 45, extensionDays: 45, cureDays: 60, profilingOptOut: true, minorOptIn: 13, notes: "Strict data-minimization; effective 2026-10-01" }, // allow:raw-time-literal
1114
+ "quebec-25": { posture: "quebec-25", state: "QC", responseDays: 30, extensionDays: 30, cureDays: 0, profilingOptOut: true, minorOptIn: 14, notes: "DPIA + automated-decision opt-out; FR-language obligations" }, // allow:raw-time-literal
1115
+ });
1116
+
1117
+ /**
1118
+ * @primitive b.dsr.stateRules
1119
+ * @signature b.dsr.stateRules(state)
1120
+ * @since 0.8.77
1121
+ * @related b.compliance.describe
1122
+ *
1123
+ * Returns per-state DSR rules: response window, extension period,
1124
+ * cure period (statutory grace before enforcement attaches),
1125
+ * profiling-opt-out availability, and minor-consent age threshold.
1126
+ * `state` accepts either the posture name (`"vcdpa"`) or the
1127
+ * 2-letter state abbreviation (`"VA"`). Returns null when unknown.
1128
+ *
1129
+ * @example
1130
+ * var rules = b.dsr.stateRules("vcdpa");
1131
+ * // rules.responseDays → 45
1132
+ * // rules.cureDays → 30
1133
+ * // rules.profilingOptOut → true
1134
+ */
1135
+ function stateRules(state) {
1136
+ if (typeof state !== "string" || state.length === 0) return null;
1137
+ // Direct posture-name lookup first
1138
+ if (STATE_RULES[state]) return Object.assign({}, STATE_RULES[state]);
1139
+ // 2-letter state abbreviation lookup (case-insensitive)
1140
+ var u = state.toUpperCase();
1141
+ var keys = Object.keys(STATE_RULES);
1142
+ for (var i = 0; i < keys.length; i++) {
1143
+ if (STATE_RULES[keys[i]].state === u) {
1144
+ return Object.assign({}, STATE_RULES[keys[i]]);
1145
+ }
1146
+ }
1147
+ return null;
1148
+ }
1149
+
1150
+ /**
1151
+ * @primitive b.dsr.listStateRules
1152
+ * @signature b.dsr.listStateRules()
1153
+ * @since 0.8.77
1154
+ *
1155
+ * Returns every state-rule entry as an array (useful for admin UI
1156
+ * cure-period dashboards / operator-facing matrices).
1157
+ *
1158
+ * @example
1159
+ * var all = b.dsr.listStateRules();
1160
+ * // → [{ posture: "vcdpa", state: "VA", responseDays: 45, ... }, ...]
1161
+ */
1162
+ function listStateRules() {
1163
+ return Object.keys(STATE_RULES).map(function (k) {
1164
+ return Object.assign({}, STATE_RULES[k]);
1165
+ });
1166
+ }
1167
+
1074
1168
  module.exports = {
1075
1169
  create: create,
1076
1170
  memoryTicketStore: memoryTicketStore,
@@ -1080,5 +1174,7 @@ module.exports = {
1080
1174
  VALID_VERIFICATION_LEVELS: VALID_VERIFICATION_LEVELS,
1081
1175
  TYPE_MIN_VERIFICATION: TYPE_MIN_VERIFICATION,
1082
1176
  POSTURE_DEADLINE_MS: POSTURE_DEADLINE_MS,
1177
+ stateRules: stateRules,
1178
+ listStateRules: listStateRules,
1083
1179
  DsrError: DsrError,
1084
1180
  };