@blamejs/blamejs-shop 0.2.13 → 0.2.14

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/README.md +1 -1
  3. package/lib/admin.js +616 -21
  4. package/lib/asset-manifest.json +3 -3
  5. package/lib/catalog.js +19 -0
  6. package/lib/vendor/MANIFEST.json +3 -3
  7. package/lib/vendor/blamejs/CHANGELOG.md +16 -0
  8. package/lib/vendor/blamejs/api-snapshot.json +6 -2
  9. package/lib/vendor/blamejs/lib/agent-idempotency.js +50 -10
  10. package/lib/vendor/blamejs/lib/agent-orchestrator.js +58 -5
  11. package/lib/vendor/blamejs/lib/agent-saga.js +8 -4
  12. package/lib/vendor/blamejs/lib/agent-tenant.js +56 -4
  13. package/lib/vendor/blamejs/lib/agent-trace.js +4 -3
  14. package/lib/vendor/blamejs/lib/ai-adverse-decision.js +1 -1
  15. package/lib/vendor/blamejs/lib/audit-daily-review.js +3 -3
  16. package/lib/vendor/blamejs/lib/audit.js +11 -8
  17. package/lib/vendor/blamejs/lib/calendar.js +14 -10
  18. package/lib/vendor/blamejs/lib/circuit-breaker.js +6 -4
  19. package/lib/vendor/blamejs/lib/compliance-ai-act-transparency.js +2 -2
  20. package/lib/vendor/blamejs/lib/compliance.js +7 -9
  21. package/lib/vendor/blamejs/lib/cookies.js +2 -2
  22. package/lib/vendor/blamejs/lib/crypto-field.js +8 -3
  23. package/lib/vendor/blamejs/lib/data-act.js +4 -3
  24. package/lib/vendor/blamejs/lib/file-upload.js +18 -1
  25. package/lib/vendor/blamejs/lib/mail-bimi.js +3 -2
  26. package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +7 -11
  27. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +6 -5
  28. package/lib/vendor/blamejs/lib/mail-crypto.js +5 -5
  29. package/lib/vendor/blamejs/lib/mail-store.js +3 -2
  30. package/lib/vendor/blamejs/lib/mcp.js +2 -3
  31. package/lib/vendor/blamejs/lib/middleware/age-gate.js +10 -6
  32. package/lib/vendor/blamejs/lib/middleware/ai-act-disclosure.js +1 -1
  33. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +3 -3
  34. package/lib/vendor/blamejs/lib/middleware/compose-pipeline.js +1 -1
  35. package/lib/vendor/blamejs/lib/middleware/csp-nonce.js +2 -2
  36. package/lib/vendor/blamejs/lib/middleware/flag-context.js +1 -1
  37. package/lib/vendor/blamejs/lib/middleware/require-auth.js +2 -1
  38. package/lib/vendor/blamejs/lib/queue-redis.js +9 -0
  39. package/lib/vendor/blamejs/lib/queue.js +17 -1
  40. package/lib/vendor/blamejs/lib/retry.js +26 -0
  41. package/lib/vendor/blamejs/lib/router.js +4 -1
  42. package/lib/vendor/blamejs/lib/safe-decompress.js +3 -2
  43. package/lib/vendor/blamejs/lib/safe-icap.js +3 -2
  44. package/lib/vendor/blamejs/lib/safe-mime.js +1 -1
  45. package/lib/vendor/blamejs/lib/safe-smtp.js +1 -1
  46. package/lib/vendor/blamejs/lib/sec-cyber.js +1 -1
  47. package/lib/vendor/blamejs/lib/storage.js +20 -16
  48. package/lib/vendor/blamejs/lib/vault/index.js +1 -0
  49. package/lib/vendor/blamejs/lib/vault-aad.js +2 -2
  50. package/lib/vendor/blamejs/package.json +1 -1
  51. package/lib/vendor/blamejs/release-notes/v0.13.25.json +39 -0
  52. package/lib/vendor/blamejs/release-notes/v0.13.26.json +31 -0
  53. package/lib/vendor/blamejs/release-notes/v0.13.27.json +34 -0
  54. package/lib/vendor/blamejs/release-notes/v0.13.28.json +30 -0
  55. package/lib/vendor/blamejs/release-notes/v0.13.29.json +30 -0
  56. package/lib/vendor/blamejs/release-notes/v0.13.30.json +30 -0
  57. package/lib/vendor/blamejs/release-notes/v0.13.31.json +26 -0
  58. package/lib/vendor/blamejs/release-notes/v0.13.32.json +34 -0
  59. package/lib/vendor/blamejs/test/20-db.js +30 -0
  60. package/lib/vendor/blamejs/test/integration/queue-redis.test.js +14 -0
  61. package/lib/vendor/blamejs/test/layer-0-primitives/agent-idempotency.test.js +37 -0
  62. package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +37 -0
  63. package/lib/vendor/blamejs/test/layer-0-primitives/agent-tenant.test.js +26 -0
  64. package/lib/vendor/blamejs/test/layer-0-primitives/audit-daily-review.test.js +9 -0
  65. package/lib/vendor/blamejs/test/layer-0-primitives/retry.test.js +25 -0
  66. package/package.json +1 -1
@@ -107,8 +107,9 @@ var MAX_EXPAND_SPAN_MS = 10 * 365 * 24 * 60 * 60 * 1000;
107
107
  * @since 0.11.31
108
108
  * @status stable
109
109
  *
110
- * Validate a JSCalendar Event / Task object's required-field shape per
111
- * RFC 8984 §5 (Event) + §6 (Task). Returns the input on success; throws
110
+ * Validate a JSCalendar Event / Task / Note / Group object's
111
+ * required-field shape per RFC 8984 §5 (Event) / §6 (Task) / §7 (Note) /
112
+ * §1.4.4 (Group). Returns the input on success; throws
112
113
  * `CalendarError` on refusal with a `.code` naming the specific shape
113
114
  * rule that failed.
114
115
  *
@@ -337,10 +338,11 @@ function validate(jsCal) {
337
338
  * @since 0.11.31
338
339
  * @status stable
339
340
  *
340
- * Parse iCalendar text (RFC 5545) via `b.safeIcal.parse` and map the
341
- * first VEVENT into a JSCalendar Event object (RFC 8984 §5). Returns
342
- * a single Event when the VCALENDAR contains exactly one VEVENT, or
343
- * an array when multiple VEVENTs are present.
341
+ * Parse iCalendar text (RFC 5545) via `b.safeIcal.parse` and map each
342
+ * VEVENT JSCalendar Event, VTODO Task, and VJOURNAL → Note
343
+ * (RFC 8984 §5 / §6 / §7). Returns a single object when the VCALENDAR
344
+ * holds exactly one component, or an array across all components when
345
+ * there are several.
344
346
  *
345
347
  * @opts
346
348
  * safeIcalOpts: object, // forwarded to b.safeIcal.parse (caps, allowExperimental, etc.)
@@ -381,10 +383,12 @@ function fromIcal(text, opts) {
381
383
  * @since 0.11.31
382
384
  * @status stable
383
385
  *
384
- * Render a JSCalendar Event back to RFC 5545 iCalendar text. Returns a
385
- * CRLF-terminated string wrapped in a `BEGIN:VCALENDAR / VERSION:2.0 /
386
- * PRODID:-//blamejs//Calendar//EN / BEGIN:VEVENT ... END:VEVENT /
387
- * END:VCALENDAR` envelope per RFC 5545 §3.4.
386
+ * Render a JSCalendar object back to RFC 5545 iCalendar text Event →
387
+ * VEVENT, Task VTODO, Note → VJOURNAL, and a Group to a VCALENDAR
388
+ * carrying each member component. Returns a CRLF-terminated string
389
+ * wrapped in a `BEGIN:VCALENDAR / VERSION:2.0 /
390
+ * PRODID:-//blamejs//Calendar//EN / … / END:VCALENDAR` envelope per
391
+ * RFC 5545 §3.4.
388
392
  *
389
393
  * @opts
390
394
  * prodid: string, // PRODID value to emit; default "-//blamejs//Calendar//EN"
@@ -36,8 +36,10 @@ var retryHelper = require("./retry");
36
36
  * Build a circuit-breaker. Returns a CircuitBreaker instance with
37
37
  * `wrap(fn)` (executes `fn` if the breaker is closed; throws an
38
38
  * `Error` with `code: "CIRCUIT_OPEN"` + `isObjectStoreError: true` +
39
- * `permanent: false` when open), `state()`, `reset()`, and
40
- * `onStateChange(handler)` listener registration. Pass-through
39
+ * `permanent: false` when open), `getState()`, `reset()`, and
40
+ * `onStateChange(handler)` listener registration (the handler, and the
41
+ * `onStateChange` opt, receive `{ name, from, to, at }` on every
42
+ * transition). Pass-through
41
43
  * factory: identical instance shape to `b.retry.CircuitBreaker`,
42
44
  * with the framework's `create(opts)` vocabulary.
43
45
  *
@@ -53,8 +55,8 @@ var retryHelper = require("./retry");
53
55
  * failureThreshold: number, // failures in the closed state before opening
54
56
  * cooldownMs: number, // milliseconds the breaker stays open before probing
55
57
  * successThreshold: number, // probe successes required to close from half-open
56
- * audit: Object, // optional b.audit instance for state-change emission
57
- * onStateChange: Function, // ({ name, from, to, at }) → void
58
+ * onStateChange: Function, // ({ name, from, to, at }) → void; also emits the
59
+ * // `breaker.state.change` observability event
58
60
  *
59
61
  * @example
60
62
  * var cb = b.circuitBreaker.create({
@@ -31,8 +31,8 @@
31
31
  * The framework provides:
32
32
  *
33
33
  * - banner({ kind }) → builder for the standard disclosure banner
34
- * - watermark({ kind, ... }) → builder for content-marking tags
35
- * - cspMetaTag({ ... }) → meta tag pair for HTML pages
34
+ * - watermark({ mediaKind, ... }) → builder for content-marking tags
35
+ * - metaTags({ ... }) → meta tag pair for HTML pages
36
36
  * - jsonLdDisclosure({ ... }) → JSON-LD <script> for structured-data emit
37
37
  * - C2pa-stub → operator-feeds-claims pattern for
38
38
  * C2PA Content Credentials integration
@@ -1407,14 +1407,11 @@ function postureDefault(posture, key) {
1407
1407
  *
1408
1408
  * @example
1409
1409
  * b.compliance.posturesByDomain("privacy");
1410
- * // → ["ccpa", "gdpr", "lgpd-br", "pipl-cn", "appi-jp",
1411
- * // "pdpa-sg", "pipeda-ca", "uk-gdpr"]
1410
+ * // → ["ccpa", "gdpr", "lgpd-br", ...] — every posture whose
1411
+ * // domain is "privacy" (the full set grows as regimes are added)
1412
1412
  *
1413
1413
  * b.compliance.posturesByDomain("health");
1414
- * // → ["hipaa", "wmhmda"]
1415
- *
1416
- * b.compliance.posturesByDomain("payment");
1417
- * // → ["pci-dss"]
1414
+ * // → ["hipaa", "wmhmda", ...] — every "health"-domain posture
1418
1415
  *
1419
1416
  * b.compliance.posturesByDomain("not-a-domain");
1420
1417
  * // → []
@@ -1450,13 +1447,14 @@ function posturesByDomain(domain) {
1450
1447
  *
1451
1448
  * @example
1452
1449
  * b.compliance.posturesByJurisdiction("EU");
1453
- * // → ["gdpr", "dora", "nis2", "cra", "ai-act"]
1450
+ * // → ["gdpr", "dora", "nis2", ...] — every EU-jurisdiction posture
1451
+ * // (the full set grows as regimes are added)
1454
1452
  *
1455
1453
  * b.compliance.posturesByJurisdiction("US");
1456
- * // → ["hipaa", "soc2", "sox"]
1454
+ * // → ["hipaa", "soc2", "sox", ...] — every US-jurisdiction posture
1457
1455
  *
1458
1456
  * b.compliance.posturesByJurisdiction("US-CA");
1459
- * // → ["ccpa"]
1457
+ * // → ["ccpa", ...] — every US-CA (California) posture
1460
1458
  *
1461
1459
  * b.compliance.posturesByJurisdiction("XX");
1462
1460
  * // → []
@@ -127,8 +127,8 @@ function _scrubAttr(s) {
127
127
  * `parseSafe`.
128
128
  *
129
129
  * @example
130
- * var jar = b.cookies.parse("session=abc; theme=%22dark%22");
131
- * // → { session: "abc", theme: "dark" }
130
+ * var jar = b.cookies.parse("session=abc; theme=dark%20mode");
131
+ * // → { session: "abc", theme: "dark mode" } // percent-decoded
132
132
  */
133
133
  function parse(cookieHeader) {
134
134
  var out = {};
@@ -495,9 +495,14 @@ function unsealRow(table, row) {
495
495
  } catch (_e) { /* drop-silent */ }
496
496
  unsealed = null;
497
497
  }
498
- // If the value wasn't actually sealed, vault.unseal returns the input
499
- // unchanged keep the original.
500
- out[field] = unsealed !== undefined && unsealed !== null ? unsealed : out[field];
498
+ // Assign unconditionally. `unsealed` already carries the right value
499
+ // for every branch: the plaintext on success, the original value on
500
+ // the not-actually-sealed pass-through (set above), and `null` on an
501
+ // unseal failure. The failure case MUST null the column so downstream
502
+ // sees "no value" rather than the attacker-crafted `vault:<…>` string
503
+ // (a prior `... ? unsealed : out[field]` guard silently kept the
504
+ // forged ciphertext on failure, defeating the documented defense).
505
+ out[field] = unsealed;
501
506
  }
502
507
  }
503
508
 
@@ -20,8 +20,9 @@
20
20
  *
21
21
  * - Art 4 §1 — let the user access "readily available product
22
22
  * data" generated by their use of the connected product.
23
- * `b.dataAct.userAccessible(productId, userId)` returns the
24
- * operator-supplied data slice.
23
+ * `b.dataAct.recordUserAccess({ productId, userId, dataSlice })`
24
+ * records the operator-supplied data slice for the regulator
25
+ * record.
25
26
  * - Art 5 §1 — share that data with a third-party data
26
27
  * recipient on the user's request, "without undue delay,
27
28
  * free of charge to the user, of the same quality as is
@@ -46,7 +47,7 @@
46
47
  * b.dataAct.declareProduct({ productId, dataHolder, ... })
47
48
  * b.dataAct.recordUserAccess({ productId, userId, dataSlice, ... })
48
49
  * b.dataAct.shareWithThirdParty({ productId, userId, recipient, scope })
49
- * b.dataAct.refuseGatekeeper({ recipient })
50
+ * refuses a DMA-gatekeeper recipient per Art 32 §1
50
51
  * b.dataAct.recordSwitchRequest({ customerId, targetProvider, dataSlices })
51
52
  *
52
53
  * The framework does NOT host the connected-product data itself;
@@ -18,7 +18,14 @@
18
18
  * for content gating and `b.guardFilename.gate({ profile: "strict" })`
19
19
  * for filename gating. Operators opt out via `contentSafety: null`
20
20
  * / `filenameSafety: null` (audited at create time so a security
21
- * review can find the disabled-on-deploy rows). Per-chunk hooks
21
+ * review can find the disabled-on-deploy rows). The byte-level
22
+ * content gate inspects the reassembled buffer, so it runs on uploads
23
+ * up to `maxStreamReassemblyBytes` (default 64 MiB); a larger upload
24
+ * is handed to `onFinalize` as a stream and the byte-content gate is
25
+ * skipped (MIME-sniff + filename gates still run, and the skip emits a
26
+ * `fileUpload.content_safety_skipped` warning audit). To guarantee
27
+ * content-gating of a type, cap `maxFileBytes` at or below
28
+ * `maxStreamReassemblyBytes`. Per-chunk hooks
22
29
  * (`onChunk`) are the integration point for virus scanners and
23
30
  * schema-shape checks; rejecting from the hook surfaces as a
24
31
  * permanent `FileUploadError`.
@@ -1076,6 +1083,16 @@ function create(opts) {
1076
1083
  // Clear the streaming alias if present — sanitized fits in memory.
1077
1084
  bodyStream = null;
1078
1085
  }
1086
+ } else if (safetyGate && typeof safetyGate.check === "function" && !bodyBuffer) {
1087
+ // A content-safety gate is configured for this extension, but the
1088
+ // upload streamed past maxStreamReassemblyBytes and was never
1089
+ // reassembled into a buffer the byte-level gate can inspect. The
1090
+ // MIME-sniff and filename gates still ran; the per-extension
1091
+ // content gate did NOT. Surface it (rather than skipping silently)
1092
+ // via an observability counter so operators can alert, lower
1093
+ // maxStreamReassemblyBytes, or cap maxFileBytes to force
1094
+ // content-gating of this type.
1095
+ _emitObs("fileUpload.content_safety_skipped_streamed", 1, { ext: safetyExt });
1079
1096
  }
1080
1097
  }
1081
1098
 
@@ -34,8 +34,9 @@
34
34
  * subjectAltName URI matches the BIMI domain, and confirms the
35
35
  * cert carries the BIMI mark-verification policy OID
36
36
  * (1.3.6.1.5.5.7.3.31). The verified mark is returned as
37
- * { svg, evidenceDocument } pulled from RFC 3709 logotype extension
38
- * when present.
37
+ * { svg, evidenceDocument } — `svg` pulled from the RFC 3709
38
+ * logotype extension when present, `evidenceDocument` echoed from the
39
+ * operator-supplied opts.evidenceDocument.
39
40
  *
40
41
  * `validateTinyPsSvg` enforces the AuthIndicators-WG Tiny PS subset:
41
42
  * single root <svg>, version="1.2", baseProfile="tiny-ps", viewBox
@@ -57,19 +57,15 @@
57
57
  * to a known operator key rather than trusting any key that
58
58
  * happens to match the signature.
59
59
  *
60
- * Deferred from v1 (each with the documented condition for opting in):
60
+ * Now live (promoted to the stable top-level surface in v0.11.32):
61
61
  * - In-process encrypt + decrypt (Message Encrypted Session Key +
62
62
  * Symmetrically Encrypted Integrity Protected Data packets,
63
- * RFC 9580 §5.1 / §5.13) and WKD key discovery (draft-koch-
64
- * openpgp-webkey-service). Defer condition: ships in v0.10.14
65
- * alongside `b.mail.crypto.smime` sign + verify the CMS
66
- * substrate `b.cms` landed in v0.10.13 unblocked the S/MIME
67
- * side, and OpenPGP encrypt rides the same release so the
68
- * mail-crypto surface lights up coherently rather than half-
69
- * on-each-side across two patches. Cheap escape hatch (pre-
70
- * v0.10.14): operators wire a third-party OpenPGP library in
71
- * their own consumer code and call this module's sign() /
72
- * verify() on the resulting cleartext blob.
63
+ * RFC 9580 §5.1 / §5.13) as `b.mail.crypto.pgp.encrypt` /
64
+ * `.decrypt`, and WKD key discovery (draft-koch-openpgp-webkey-
65
+ * service) as `b.mail.crypto.pgp.wkd` all on the same `b.cms`
66
+ * substrate that backs S/MIME sign/verify.
67
+ *
68
+ * Deferred (with the documented condition for opting in):
73
69
  * - v6 signature packets (RFC 9580 §5.2.3, packet version 6 with
74
70
  * SHA2-512 fingerprints and salted hashes). Defer condition: v6
75
71
  * is not yet emitted by GnuPG 2.4 LTS or by Sequoia stable, so
@@ -670,14 +670,15 @@ function _wrapBase64(s) {
670
670
  * Operator-side cert preflight that lights up at boot: refuses
671
671
  * SHA-1 / MD5 signatures, RSA keys < 2048 bits, MD2 / MD5 / SHA-1
672
672
  * as the certificate-signature algorithm. Returns the parsed cert
673
- * shape (subject CN, issuer CN, validFrom / validTo, key algorithm
674
- * + size, signature algorithm). Throws `mail-crypto/smime/bad-cert`
675
- * on any of the above; throws `mail-crypto/smime/expired-cert` if
676
- * the cert is outside its validity window.
673
+ * shape: the full subject / issuer DN strings, the validity window,
674
+ * the signature algorithm (name + OID), the key type, and the SHA-256
675
+ * fingerprint. Throws `mail-crypto/smime/bad-cert` on any of the above;
676
+ * throws `mail-crypto/smime/expired-cert` if the cert is outside its
677
+ * validity window.
677
678
  *
678
679
  * @example
679
680
  * var info = b.mail.crypto.smime.checkCert({ certPem: pem });
680
- * // → { subjectCN, issuerCN, validFrom, validTo, keyAlg, keyBits, sigAlg }
681
+ * // → { subject, issuer, validFrom, validTo, sigAlgName, sigAlgOid, keyType, fingerprint256 }
681
682
  */
682
683
  function checkCert(opts) {
683
684
  opts = validateOpts.requireObject(opts, "mail.crypto.smime.checkCert",
@@ -22,11 +22,11 @@
22
22
  * (pub-alg 1, EMSA-PKCS1-v1_5 + SHA-256, 2048-bit floor per
23
23
  * RFC 8301).
24
24
  * - `b.mail.crypto.smime` — S/MIME 4.0 per RFC 8551 with CMS
25
- * SignedData per RFC 5652. v1 surface: checkCert() the
26
- * operator-side preflight that refuses SHA-1 / MD5 / < 2048-bit
27
- * RSA certs at boot. sign() + verify() are DEFERRED in v1; see
28
- * the @intro block in lib/mail-crypto-smime.js for the deferral
29
- * conditions and the operator escape hatch.
25
+ * SignedData per RFC 5652. Surface: sign() + verify() + verifyAll()
26
+ * (built on the `b.cms` substrate digest recompute + timing-safe
27
+ * compare + PQC signature verify + X.509 chain walk) plus
28
+ * checkCert(), the operator-side preflight that refuses SHA-1 /
29
+ * MD5 / < 2048-bit RSA certs at boot.
30
30
  *
31
31
  * Both sub-namespaces share `MailCryptoError` (FrameworkError
32
32
  * subclass via defineClass with alwaysPermanent: true) so operator
@@ -90,8 +90,9 @@ var DEFAULT_FOLDERS = Object.freeze([
90
90
  * @related b.safeMime, b.guardMessageId, b.cryptoField
91
91
  *
92
92
  * Build a mail-store handle. Returns an object with `appendMessage` /
93
- * `fetchByObjectId` / `queryByModseq` / `setFlags` / `createFolder` /
94
- * `listFolders` / `threadFor` / `quota` / `setLegalHold` / `destroy`.
93
+ * `fetchByObjectId` / `search` / `queryByModseq` / `setFlags` /
94
+ * `createFolder` / `listFolders` / `threadFor` / `quota` /
95
+ * `moveMessages` / `setLegalHold` / `hardExpunge`.
95
96
  *
96
97
  * @opts
97
98
  * backend: object, // required — sqlite-shaped { prepare(sql) → { run, get, all }, transaction(fn) }
@@ -399,10 +399,10 @@ function serverGuard(opts) {
399
399
  * exfiltration endpoints. The framework's defense:
400
400
  *
401
401
  * - Strip / refuse executable HTML (`<script>` / `<iframe>` /
402
- * `javascript:` URLs) composes b.guardHtml's strict profile
402
+ * `javascript:` URLs) via built-in dangerous-HTML detection
403
403
  * - Refuse known prompt-injection markers ("ignore previous
404
404
  * instructions", "system: you are now ...", role-claim prefixes)
405
- * composes b.ai.input.classify
405
+ * via a built-in injection-marker matcher
406
406
  * - Cap text length so a tool can't blow the host's context window
407
407
  * out from under it
408
408
  * - Refuse content with `image_url` / `audio_url` / `resource_link`
@@ -418,7 +418,6 @@ function serverGuard(opts) {
418
418
  * posture?: "refuse" | "sanitize" | "audit-only", // default "refuse"
419
419
  * maxTextBytes?: number, // default 64 KiB per content block
420
420
  * allowedHosts?: string[], // for image/audio/resource_link refs
421
- * classifyInput?: fn(text)→{verdict, score} | null, // default b.ai.input.classify
422
421
  * }
423
422
  *
424
423
  * @example
@@ -53,16 +53,20 @@ var AgeGateError = defineClass("AgeGateError", { alwaysPermanent: true });
53
53
  * Classifies the request against an operator-supplied age predicate
54
54
  * and applies COPPA / UK Children's Code / California AADC defaults
55
55
  * (no-store cache, no-referrer, X-Privacy-Posture header) for
56
- * below-threshold + unknown-age requests. When `requireAge` is set
57
- * and the request is below threshold without a parental-consent
58
- * record, refuses with HTTP 451. Every classification decision is
59
- * audited.
56
+ * below-threshold + unknown-age requests. `requireAge` is the hard
57
+ * legal floor: a request classified below threshold without a
58
+ * parental-consent record is refused with HTTP 451. It is evaluated
59
+ * within the consent classification, so it takes effect only when
60
+ * `consentRequired` is also set (that is what classifies a request as
61
+ * below-threshold); `requireAge` alone, with `consentRequired: null`,
62
+ * never classifies a request as below-threshold and so the 451 never
63
+ * fires. Every classification decision is audited.
60
64
  *
61
65
  * @opts
62
66
  * {
63
67
  * getAge: function(req): number|null, // required
64
- * requireAge: number|null, // 451 below this
65
- * consentRequired: number|null, // require consent below
68
+ * requireAge: number|null, // 451 floor; requires consentRequired set
69
+ * consentRequired: number|null, // require consent below; enables below-threshold classification
66
70
  * hasParentalConsent: function(req): boolean,
67
71
  * skipPaths: string[],
68
72
  * errorMessage: string,
@@ -60,7 +60,7 @@ var audit = lazyRequire(function () { return require("../audit"); });
60
60
  *
61
61
  * @opts
62
62
  * {
63
- * kind: "ai-interaction"|"deepfake"|"emotion-recognition"|"biometric-categorisation"|"synthetic-content",
63
+ * kind: "ai-interaction"|"ai-generated-content"|"emotion-recognition"|"biometric-categorisation"|"deep-fake"|"ai-text-public-interest",
64
64
  * deployerName: string,
65
65
  * policyUri: string,
66
66
  * mode: "header"|"html", // default "header"
@@ -250,11 +250,11 @@ function _writeRejection(res, code, body) {
250
250
  *
251
251
  * @opts
252
252
  * {
253
- * keypair: { publicKey, secretKey, ecPublicKey, ecSecretKey },
253
+ * keypair: { publicKey, privateKey, ecPublicKey, ecPrivateKey },
254
254
  * keypairs: [...] // multi-key rotation set; first = active
255
255
  * replayWindowMs: number,
256
256
  * pruneIntervalMs: number,
257
- * nonceStore: { has, add, prune },
257
+ * nonceStore: { checkAndInsert, purgeExpired, close },
258
258
  * exemptPaths: string[],
259
259
  * contentTypes: string[], // default ["application/json"]
260
260
  * audit: boolean,
@@ -270,7 +270,7 @@ function _writeRejection(res, code, body) {
270
270
  * @example
271
271
  * var b = require("@blamejs/core");
272
272
  * var app = b.router.create();
273
- * var kp = b.crypto.keypair();
273
+ * var kp = b.crypto.generateEncryptionKeyPair();
274
274
  * app.use(b.middleware.apiEncrypt({
275
275
  * keypair: kp,
276
276
  * replayWindowMs: 30000,
@@ -104,7 +104,7 @@ var CANONICAL_POSITIONS = Object.freeze({
104
104
  /**
105
105
  * @primitive b.middleware.composePipeline
106
106
  * @signature b.middleware.composePipeline(entries, opts?)
107
- * @since 0.9.44
107
+ * @since 0.9.43
108
108
  * @status stable
109
109
  * @related b.middleware.requestId, b.middleware.requireAuth, b.middleware.idempotencyKey
110
110
  *
@@ -8,7 +8,7 @@
8
8
  * browser refuses to execute them. This middleware closes the gap:
9
9
  *
10
10
  * 1. Generate a fresh random nonce per request (base64, default
11
- * 16 bytes / 22 chars).
11
+ * 16 bytes / 24 chars).
12
12
  * 2. Attach it to `req.cspNonce` (handler-readable) AND
13
13
  * `res.locals.cspNonce` (template-data-readable — render.js
14
14
  * auto-merges res.locals into template data).
@@ -233,7 +233,7 @@ function _injectNonce(cspHeader, nonce, directives, strictDynamic) {
233
233
  * Per-request CSP nonce + render integration. Constructed via
234
234
  * `b.middleware.cspNonce(opts)`; the resulting middleware has the
235
235
  * `(req, res, next)` shape shown above. Generates a fresh
236
- * random nonce (16 bytes / 22 chars base64 by default), attaches it
236
+ * random nonce (16 bytes / 24 chars base64 by default), attaches it
237
237
  * to `req.cspNonce` and `res.locals.cspNonce` (auto-merged into
238
238
  * template data), and patches the existing Content-Security-Policy
239
239
  * header to append `'nonce-XYZ'` to the configured directives
@@ -34,7 +34,7 @@ var contextMod = lazyRequire(function () { return require("../flag-evaluation-co
34
34
  * @primitive b.middleware.flagContext
35
35
  * @signature b.middleware.flagContext(opts)
36
36
  * @since 0.1.0
37
- * @related b.flagClient.getBoolean
37
+ * @related b.flag.create
38
38
  *
39
39
  * Extracts an OpenFeature evaluation context onto `req.flagCtx` so
40
40
  * downstream handlers and multiple flag clients read a consistent
@@ -30,7 +30,8 @@
30
30
  * when set, prefersJson()=false rejections
31
31
  * produce 302 instead of 401 text/plain)
32
32
  * prefersJson: function (optional override; defaults to checking
33
- * Accept / X-Requested-With / Content-Type)
33
+ * Accept / X-Requested-With NOT
34
+ * Content-Type, see the note above)
34
35
  * errorMessage: 'Authentication required.'
35
36
  * audit: true (emit auth.required.denied on reject)
36
37
  * }
@@ -582,6 +582,15 @@ function create(opts) {
582
582
  async function fail(jobId, errorMessage, retryDelayMs) {
583
583
  await _ensureConnected();
584
584
  var nowMs = Date.now();
585
+ // b.queue.consume passes the object form `{ retryDelayMs }` (matching
586
+ // the queue-local backend); accept it as well as a bare-number third
587
+ // arg. Without this the object failed the `typeof === "number"` test
588
+ // below and the delay was forced to 0, so the documented exponential
589
+ // backoff was silently discarded and a failing job re-leased
590
+ // immediately on the redis backend (retry storm).
591
+ if (retryDelayMs && typeof retryDelayMs === "object") {
592
+ retryDelayMs = retryDelayMs.retryDelayMs;
593
+ }
585
594
  if (typeof retryDelayMs !== "number" || !isFinite(retryDelayMs) || retryDelayMs < 0) {
586
595
  retryDelayMs = 0;
587
596
  }
@@ -17,6 +17,18 @@
17
17
  * and `nats` are listed as deferred and surface a clear error if
18
18
  * selected.
19
19
  *
20
+ * `local` and `redis` are driven by the generic `b.queue.consume`
21
+ * loop and the lifecycle below (framework-side leasing, deterministic
22
+ * backoff, DLQ, and the sweep timer). `sqs` is an SQS-native adapter
23
+ * with a different model: `complete` / `fail` delete or re-deliver by
24
+ * the message's `receiptHandle` (returned by `lease()`, threaded back
25
+ * by the caller), and DLQ + visibility-expiry are handled server-side
26
+ * by the SQS queue's RedrivePolicy — so `sqs` is driven directly
27
+ * (lease → handle → complete/fail), not by `b.queue.consume`, and it
28
+ * does not use the framework DLQ / sweep described below. See
29
+ * `lib/queue-sqs.js` for its action map and the features that require
30
+ * operator wiring.
31
+ *
20
32
  * Job lifecycle:
21
33
  * enqueued (status='pending', availableAt set by delaySeconds)
22
34
  * ↓ availableAt reached + consumer leases
@@ -271,7 +283,11 @@ function enqueue(queueName, payload, opts) {
271
283
  * through `handler(job, ctx)`. Handler resolution marks the job
272
284
  * `done`; rejection bumps the attempt counter and either re-pends
273
285
  * with deterministic exponential backoff (1s base, 5min cap, no
274
- * jitter) or routes to the DLQ when `attempts >= maxAttempts`.
286
+ * jitter) or routes to the DLQ when `attempts >= maxAttempts`. This
287
+ * loop drives the `local` and `redis` backends; the `sqs` backend uses
288
+ * SQS-native receipt-handle complete/fail and server-side redrive and
289
+ * is driven directly rather than by this consumer (see the module
290
+ * intro).
275
291
  *
276
292
  * Returns a consumer state handle whose `.cancel()` aborts the poll
277
293
  * loop immediately (without waiting for the next `pollIntervalMs`
@@ -187,6 +187,10 @@ function _validateBreakerOpts(name, opts) {
187
187
  throw new TypeError("retry.CircuitBreaker: successThreshold must be a positive integer, got " +
188
188
  typeof opts.successThreshold + " " + JSON.stringify(opts.successThreshold));
189
189
  }
190
+ if (opts.onStateChange != null && typeof opts.onStateChange !== "function") {
191
+ throw new TypeError("retry.CircuitBreaker: onStateChange must be a function, got " +
192
+ typeof opts.onStateChange);
193
+ }
190
194
  }
191
195
 
192
196
  // ---- Public surface ----
@@ -381,6 +385,19 @@ class CircuitBreaker {
381
385
  this.consecutiveFailures = 0;
382
386
  this.consecutiveSuccesses = 0;
383
387
  this.openedAt = 0;
388
+ this._stateListeners = [];
389
+ if (typeof merged.onStateChange === "function") this._stateListeners.push(merged.onStateChange);
390
+ }
391
+
392
+ // Register a state-change listener. Called with { name, from, to, at }
393
+ // on every transition (same payload the constructor's onStateChange
394
+ // opt receives). Returns this for chaining.
395
+ onStateChange(handler) {
396
+ if (typeof handler !== "function") {
397
+ throw new TypeError("retry.CircuitBreaker.onStateChange: handler must be a function, got " + typeof handler);
398
+ }
399
+ this._stateListeners.push(handler);
400
+ return this;
384
401
  }
385
402
 
386
403
  // Wrap an async function. The breaker observes outcomes and may fail-fast.
@@ -415,7 +432,16 @@ class CircuitBreaker {
415
432
  _transition(from, to) {
416
433
  if (from === to) return;
417
434
  this.state = to;
435
+ var at = Date.now();
418
436
  _emitEvent("breaker.state.change", 1, { name: this.name, from: from, to: to });
437
+ if (this._stateListeners.length > 0) {
438
+ var payload = { name: this.name, from: from, to: to, at: at };
439
+ for (var i = 0; i < this._stateListeners.length; i++) {
440
+ // Best-effort: a throwing listener must not derail the breaker's
441
+ // own state machine (the transition has already been applied).
442
+ try { this._stateListeners[i](payload); } catch (_e) { /* drop-silent */ }
443
+ }
444
+ }
419
445
  }
420
446
 
421
447
  _onSuccess() {
@@ -1229,7 +1229,10 @@ function serveStatic(dir) {
1229
1229
  var rel = req.pathname;
1230
1230
  if (rel.includes("\0")) return next();
1231
1231
  var filePath = nodePath.resolve(nodePath.join(root, rel));
1232
- if (!filePath.startsWith(root)) return next();
1232
+ // Anchor on `root + sep` (not a bare prefix) so a sibling directory
1233
+ // that shares the root's name as a prefix — e.g. root `/srv/public`
1234
+ // vs `/srv/public-evil` — cannot satisfy the containment check.
1235
+ if (filePath !== root && !filePath.startsWith(root + nodePath.sep)) return next();
1233
1236
  if (!nodeFs.existsSync(filePath) || nodeFs.statSync(filePath).isDirectory()) return next();
1234
1237
 
1235
1238
  var ext = nodePath.extname(filePath).toLowerCase();
@@ -37,8 +37,9 @@
37
37
  * - Any algorithm not in the allowlist (including operator-typo'd).
38
38
  *
39
39
  * Refusal posture:
40
- * - `safe-decompress/output-too-large` — bomb-by-absolute-size
41
- * (zlib's own `maxOutputLength` already refuses before alloc)
40
+ * - `safe-decompress/decompress-failed` — bomb-by-absolute-size
41
+ * (zlib's own `maxOutputLength` refuses before alloc; the throw is
42
+ * caught and surfaced under this code)
42
43
  * - `safe-decompress/ratio-exceeded` — expansion > `maxRatio`
43
44
  * (zlib accepted the bytes; our post-decompress ratio check
44
45
  * refuses, freeing the bytes immediately)
@@ -37,8 +37,9 @@
37
37
  * `\r\n`; intermediaries that accept bare-LF then desync against
38
38
  * this parser).
39
39
  * - **Status-code allowlist** — only `100` / `200` / `204` / `400`
40
- * / `403` / 5xx are honored. RFC 3507 §4.3.3 enumerates these as
41
- * the legal ICAP response codes; an unexpected `1xx` continuation
40
+ * / `403` / `404` / `405` / `408` / 5xx are honored. RFC 3507
41
+ * §4.3.3 enumerates these as the legal ICAP response codes; an
42
+ * unexpected `1xx` continuation
42
43
  * or `3xx` redirect is refused because it's a classic header-
43
44
  * injection class (attacker smuggles `ICAP/1.0 100 X-Inject:`
44
45
  * through a permissive proxy).
@@ -125,7 +125,7 @@ var DEFAULT_TRANSFER_ENCODINGS = Object.freeze([
125
125
  * maxBodyBytes: number, // default 25 MiB
126
126
  * maxMessageBytes: number, // default 50 MiB
127
127
  * charsetAllowlist: string[], // default UTF-8 / US-ASCII / common legacy 8-bit
128
- * transferEncodingAllowlist: string[], // default 7bit/8bit/binary/qp/base64
128
+ * transferEncodingAllowlist: string[], // default 7bit/8bit/quoted-printable/base64 (binary is opt-in, RFC 3030 BINARYMIME)
129
129
  *
130
130
  * @example
131
131
  * var msg = b.safeMime.parse(messageBuffer);
@@ -61,7 +61,7 @@ var SafeSmtpError = defineClass("SafeSmtpError", { alwaysPermanent: true });
61
61
  * @example
62
62
  * var body = Buffer.from("Hello world.\r\n.\r\n");
63
63
  * b.safeSmtp.findDotTerminator(body);
64
- * // → 13 (index of \r in \r\n.\r\n)
64
+ * // → 12 (index of \r in the terminating \r\n.\r\n)
65
65
  *
66
66
  * b.safeSmtp.findDotTerminator(Buffer.from("incomplete body"));
67
67
  * // → -1
@@ -32,7 +32,7 @@
32
32
  *
33
33
  * Public API:
34
34
  *
35
- * b.secCyber.eightKArtifact(opts) -> { artifact, deadline, audit }
35
+ * b.secCyber.eightKArtifact(opts) -> { artifact, deadline, deadlineBusinessDays }
36
36
  * opts:
37
37
  * incidentId: operator-supplied incident reference (string).
38
38
  * registrant: { name, cik, filer }