@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.
- package/CHANGELOG.md +2 -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 +129 -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
|
@@ -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:
|
|
194
|
-
|
|
296
|
+
create: create,
|
|
297
|
+
conformityAssessment: conformityAssessment,
|
|
298
|
+
CraReportError: CraReportError,
|
|
195
299
|
};
|
package/lib/crypto-field.js
CHANGED
|
@@ -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
|
};
|