@blamejs/blamejs-shop 0.2.12 → 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 (113) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +1 -1
  3. package/lib/admin.js +616 -21
  4. package/lib/asset-manifest.json +5 -5
  5. package/lib/catalog.js +19 -0
  6. package/lib/storefront.js +1 -1
  7. package/lib/vendor/MANIFEST.json +3 -3
  8. package/lib/vendor/blamejs/.github/workflows/actions-lint.yml +71 -23
  9. package/lib/vendor/blamejs/.github/workflows/cflite_batch.yml +2 -0
  10. package/lib/vendor/blamejs/.github/workflows/cflite_pr.yml +2 -0
  11. package/lib/vendor/blamejs/.github/workflows/ci.yml +17 -0
  12. package/lib/vendor/blamejs/.github/workflows/npm-publish.yml +18 -10
  13. package/lib/vendor/blamejs/.github/workflows/release-container.yml +29 -16
  14. package/lib/vendor/blamejs/.github/workflows/sha-to-tag-verify.yml +22 -3
  15. package/lib/vendor/blamejs/.github/zizmor.yml +29 -0
  16. package/lib/vendor/blamejs/.pinact.yaml +24 -8
  17. package/lib/vendor/blamejs/CHANGELOG.md +20 -0
  18. package/lib/vendor/blamejs/api-snapshot.json +6 -2
  19. package/lib/vendor/blamejs/lib/agent-idempotency.js +50 -10
  20. package/lib/vendor/blamejs/lib/agent-orchestrator.js +58 -5
  21. package/lib/vendor/blamejs/lib/agent-saga.js +8 -4
  22. package/lib/vendor/blamejs/lib/agent-tenant.js +56 -4
  23. package/lib/vendor/blamejs/lib/agent-trace.js +4 -3
  24. package/lib/vendor/blamejs/lib/ai-adverse-decision.js +1 -1
  25. package/lib/vendor/blamejs/lib/archive-wrap.js +1 -1
  26. package/lib/vendor/blamejs/lib/audit-daily-review.js +3 -3
  27. package/lib/vendor/blamejs/lib/audit.js +11 -8
  28. package/lib/vendor/blamejs/lib/auth/jwt.js +7 -4
  29. package/lib/vendor/blamejs/lib/auth/saml.js +2 -2
  30. package/lib/vendor/blamejs/lib/calendar.js +14 -10
  31. package/lib/vendor/blamejs/lib/circuit-breaker.js +6 -4
  32. package/lib/vendor/blamejs/lib/compliance-ai-act-transparency.js +2 -2
  33. package/lib/vendor/blamejs/lib/compliance.js +7 -9
  34. package/lib/vendor/blamejs/lib/cookies.js +2 -2
  35. package/lib/vendor/blamejs/lib/cose.js +1 -1
  36. package/lib/vendor/blamejs/lib/crypto-field.js +8 -3
  37. package/lib/vendor/blamejs/lib/data-act.js +4 -3
  38. package/lib/vendor/blamejs/lib/did.js +1 -1
  39. package/lib/vendor/blamejs/lib/file-upload.js +18 -1
  40. package/lib/vendor/blamejs/lib/guard-archive.js +2 -2
  41. package/lib/vendor/blamejs/lib/guard-auth.js +5 -5
  42. package/lib/vendor/blamejs/lib/guard-cidr.js +4 -4
  43. package/lib/vendor/blamejs/lib/guard-csv.js +6 -6
  44. package/lib/vendor/blamejs/lib/guard-domain.js +4 -4
  45. package/lib/vendor/blamejs/lib/guard-email.js +6 -5
  46. package/lib/vendor/blamejs/lib/guard-filename.js +4 -4
  47. package/lib/vendor/blamejs/lib/guard-graphql.js +5 -5
  48. package/lib/vendor/blamejs/lib/guard-html.js +2 -2
  49. package/lib/vendor/blamejs/lib/guard-image.js +1 -1
  50. package/lib/vendor/blamejs/lib/guard-json.js +5 -5
  51. package/lib/vendor/blamejs/lib/guard-jsonpath.js +4 -4
  52. package/lib/vendor/blamejs/lib/guard-jwt.js +5 -5
  53. package/lib/vendor/blamejs/lib/guard-markdown.js +6 -6
  54. package/lib/vendor/blamejs/lib/guard-mime.js +6 -6
  55. package/lib/vendor/blamejs/lib/guard-oauth.js +5 -5
  56. package/lib/vendor/blamejs/lib/guard-pdf.js +1 -1
  57. package/lib/vendor/blamejs/lib/guard-regex.js +4 -4
  58. package/lib/vendor/blamejs/lib/guard-shell.js +4 -4
  59. package/lib/vendor/blamejs/lib/guard-svg.js +5 -5
  60. package/lib/vendor/blamejs/lib/guard-template.js +4 -4
  61. package/lib/vendor/blamejs/lib/guard-time.js +6 -6
  62. package/lib/vendor/blamejs/lib/guard-uuid.js +6 -6
  63. package/lib/vendor/blamejs/lib/guard-xml.js +4 -4
  64. package/lib/vendor/blamejs/lib/guard-yaml.js +5 -5
  65. package/lib/vendor/blamejs/lib/link-header.js +3 -2
  66. package/lib/vendor/blamejs/lib/mail-agent.js +4 -2
  67. package/lib/vendor/blamejs/lib/mail-bimi.js +3 -2
  68. package/lib/vendor/blamejs/lib/mail-crypto-pgp.js +7 -11
  69. package/lib/vendor/blamejs/lib/mail-crypto-smime.js +6 -5
  70. package/lib/vendor/blamejs/lib/mail-crypto.js +5 -5
  71. package/lib/vendor/blamejs/lib/mail-store.js +3 -2
  72. package/lib/vendor/blamejs/lib/mcp.js +2 -3
  73. package/lib/vendor/blamejs/lib/middleware/age-gate.js +10 -6
  74. package/lib/vendor/blamejs/lib/middleware/ai-act-disclosure.js +1 -1
  75. package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +3 -3
  76. package/lib/vendor/blamejs/lib/middleware/compose-pipeline.js +1 -1
  77. package/lib/vendor/blamejs/lib/middleware/csp-nonce.js +2 -2
  78. package/lib/vendor/blamejs/lib/middleware/flag-context.js +1 -1
  79. package/lib/vendor/blamejs/lib/middleware/rate-limit.js +2 -2
  80. package/lib/vendor/blamejs/lib/middleware/require-auth.js +2 -1
  81. package/lib/vendor/blamejs/lib/queue-redis.js +9 -0
  82. package/lib/vendor/blamejs/lib/queue.js +17 -1
  83. package/lib/vendor/blamejs/lib/retry.js +26 -0
  84. package/lib/vendor/blamejs/lib/router.js +4 -1
  85. package/lib/vendor/blamejs/lib/safe-decompress.js +3 -2
  86. package/lib/vendor/blamejs/lib/safe-icap.js +3 -2
  87. package/lib/vendor/blamejs/lib/safe-mime.js +1 -1
  88. package/lib/vendor/blamejs/lib/safe-smtp.js +1 -1
  89. package/lib/vendor/blamejs/lib/sec-cyber.js +1 -1
  90. package/lib/vendor/blamejs/lib/storage.js +20 -16
  91. package/lib/vendor/blamejs/lib/tsa.js +2 -1
  92. package/lib/vendor/blamejs/lib/vault/index.js +1 -0
  93. package/lib/vendor/blamejs/lib/vault-aad.js +2 -2
  94. package/lib/vendor/blamejs/package.json +1 -1
  95. package/lib/vendor/blamejs/release-notes/v0.13.23.json +42 -0
  96. package/lib/vendor/blamejs/release-notes/v0.13.24.json +26 -0
  97. package/lib/vendor/blamejs/release-notes/v0.13.25.json +39 -0
  98. package/lib/vendor/blamejs/release-notes/v0.13.26.json +31 -0
  99. package/lib/vendor/blamejs/release-notes/v0.13.27.json +34 -0
  100. package/lib/vendor/blamejs/release-notes/v0.13.28.json +30 -0
  101. package/lib/vendor/blamejs/release-notes/v0.13.29.json +30 -0
  102. package/lib/vendor/blamejs/release-notes/v0.13.30.json +30 -0
  103. package/lib/vendor/blamejs/release-notes/v0.13.31.json +26 -0
  104. package/lib/vendor/blamejs/release-notes/v0.13.32.json +34 -0
  105. package/lib/vendor/blamejs/test/20-db.js +30 -0
  106. package/lib/vendor/blamejs/test/integration/queue-redis.test.js +14 -0
  107. package/lib/vendor/blamejs/test/layer-0-primitives/agent-idempotency.test.js +37 -0
  108. package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +37 -0
  109. package/lib/vendor/blamejs/test/layer-0-primitives/agent-tenant.test.js +26 -0
  110. package/lib/vendor/blamejs/test/layer-0-primitives/audit-daily-review.test.js +9 -0
  111. package/lib/vendor/blamejs/test/layer-0-primitives/retry.test.js +25 -0
  112. package/lib/vendor/blamejs/test/smoke.js +25 -0
  113. package/package.json +1 -1
@@ -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
@@ -36,7 +36,7 @@
36
36
  * header: true // set X-RateLimit-* response headers
37
37
  * skipPaths: [] // string-prefix or regex matchers
38
38
  * scope: 'global' | 'per-route' (default 'global')
39
- * backend: 'memory' (default) | 'cluster' | { take, reset, gc? }
39
+ * backend: 'memory' (default) | 'cluster' | { take, reset }
40
40
  * algorithm: 'token-bucket' (default) | 'fixed-window'
41
41
  * // memory backend only; ignored
42
42
  * // for cluster backend (which is
@@ -366,7 +366,7 @@ function _resolveBackend(opts) {
366
366
  * header: boolean, // default true
367
367
  * skipPaths: Array<string|RegExp>,
368
368
  * scope: "global"|"per-route",
369
- * backend: "memory"|"cluster"|{ take, reset, gc },
369
+ * backend: "memory"|"cluster"|{ take, reset },
370
370
  * algorithm: "token-bucket"|"fixed-window",
371
371
  * burst: number,
372
372
  * refillPerSecond: number,
@@ -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 }
@@ -833,10 +833,10 @@ function _requireInit() {
833
833
  //
834
834
  // Resumable-chunked-upload primitive. Operators handling large file
835
835
  // uploads (multipart form / tus / S3-multipart-style flow) need to
836
- // persist incoming chunks during the upload window, then assemble
837
- // them into a final file when the upload completes. Without a
838
- // framework primitive every consumer ended up reinventing the
839
- // per-assembly directory layout + atomic finalize + GC of partial
836
+ // persist incoming chunks during the upload window, then concatenate
837
+ // them in order when the upload completes. Without a framework
838
+ // primitive every consumer ended up reinventing the per-assembly
839
+ // directory layout + ordered gap-checked assembly + GC of partial
840
840
  // assemblies that never completed.
841
841
  //
842
842
  // chunkScratch owns:
@@ -844,8 +844,8 @@ function _requireInit() {
844
844
  // storage backend just like saveFile)
845
845
  // - chunk persistence + retrieval with the framework envelope
846
846
  // - assembly metadata tracking createdAt/totalChunks/chunkHashes
847
- // - atomic concat into the final file (no consumer ever sees a
848
- // half-assembled file)
847
+ // - ordered, gap-checked concat returning the assembled bytes for
848
+ // the caller to persist (the primitive does not write a final file)
849
849
  // - GC of stale partial assemblies (operator opts in via gc())
850
850
  //
851
851
  // Backend is the same `b.storage` backend the operator already
@@ -919,10 +919,11 @@ function _validateChunkIndex(idx) {
919
919
  * @related b.storage.saveFile, b.storage.getFileBuffer
920
920
  *
921
921
  * Resumable-chunked-upload primitive. Persists incoming upload chunks
922
- * during the upload window + atomically assembles them into the
923
- * final file on completion. Owns per-assembly directory layout,
924
- * envelope-encrypted chunk persistence, atomic finalize, and GC of
925
- * partial assemblies.
922
+ * during the upload window, then concatenates them in order on
923
+ * completion and returns the assembled bytes for the caller to persist
924
+ * (the primitive does not itself write a final file). Owns per-assembly
925
+ * directory layout, envelope-encrypted chunk persistence, ordered
926
+ * gap-checked assembly, and GC of partial assemblies.
926
927
  *
927
928
  * Composes existing primitives: each chunk routes through
928
929
  * `b.storage.saveFile` (same XChaCha20-Poly1305 envelope as the
@@ -974,13 +975,16 @@ function _validateChunkIndex(idx) {
974
975
  * b.storage.init({ backend: "local", uploadDir: "./data/uploads" });
975
976
  * var cs = b.storage.chunkScratch({ rootKeyPrefix: "uploads/scratch" });
976
977
  *
977
- * // During upload — each PUT lands one chunk
978
- * await cs.saveChunk({ assemblyId: "upload-abc", chunkIndex: 0, data: chunk0 });
979
- * await cs.saveChunk({ assemblyId: "upload-abc", chunkIndex: 1, data: chunk1 });
980
- * await cs.saveChunk({ assemblyId: "upload-abc", chunkIndex: 2, data: chunk2 });
978
+ * // During upload — each PUT lands one chunk. saveChunk returns the
979
+ * // chunk's sealed encryptionKey; collect them in order for assemble.
980
+ * var keys = [];
981
+ * keys[0] = (await cs.saveChunk({ assemblyId: "upload-abc", chunkIndex: 0, data: chunk0 })).encryptionKey;
982
+ * keys[1] = (await cs.saveChunk({ assemblyId: "upload-abc", chunkIndex: 1, data: chunk1 })).encryptionKey;
983
+ * keys[2] = (await cs.saveChunk({ assemblyId: "upload-abc", chunkIndex: 2, data: chunk2 })).encryptionKey;
981
984
  *
982
- * // On completion — atomic assemble + cleanup
983
- * var assembled = await cs.assemble({ assemblyId: "upload-abc", expectedTotal: 3 });
985
+ * // On completion — concat the chunks (in order) into the assembled
986
+ * // buffer, then clean up. chunkEncryptionKeys is one key per chunk.
987
+ * var assembled = await cs.assemble({ assemblyId: "upload-abc", expectedTotal: 3, chunkEncryptionKeys: keys });
984
988
  * await cs.removeAssembly("upload-abc");
985
989
  *
986
990
  * // Periodic GC of partial uploads abandoned mid-stream
@@ -183,7 +183,8 @@ function buildRequest(data, opts) {
183
183
  nonce = Buffer.isBuffer(opts.nonce) ? opts.nonce : nodeCrypto.randomBytes(8); // allow:raw-byte-literal — RFC 3161 nonce: 64-bit random
184
184
  children.push(asn1.writeInteger(nonce));
185
185
  }
186
- // certReq DEFAULT FALSE only encode when true (the default we want).
186
+ // certReq DEFAULTS TRUE (RFC 3161 §2.4.1) encode the boolean unless
187
+ // the caller explicitly opts out with certReq:false.
187
188
  var certReq = opts.certReq !== false;
188
189
  if (certReq) children.push(asn1.writeBoolean(true));
189
190
 
@@ -625,6 +625,7 @@ module.exports = {
625
625
  getKeysJson: getKeysJson,
626
626
  getCurrentPassphrase: getCurrentPassphrase,
627
627
  getMode: getMode,
628
+ isInitialized: function () { return initialized; },
628
629
  VaultError: VaultError,
629
630
  sealPemFile: sealPemFileModule.sealPemFile,
630
631
  SealPemFileError: sealPemFileModule.SealPemFileError,
@@ -35,8 +35,8 @@
35
35
  * .isAadSealed(value) → boolean
36
36
  *
37
37
  * Per the framework's security-first stance:
38
- * - Symmetric key derivation uses HKDF-SHAKE256 (matching the
39
- * vault's KDF) over the vault root key concatenated with the
38
+ * - Symmetric key derivation uses SHAKE256 (matching the vault's
39
+ * KDF) over the vault root key concatenated with the
40
40
  * canonicalized AAD.
41
41
  * - AEAD: XChaCha20-Poly1305 with the AAD threaded into the tag.
42
42
  * - 24-byte nonce, generated fresh per-seal via
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.13.22",
3
+ "version": "0.13.32",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -0,0 +1,42 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.13.23",
4
+ "date": "2026-05-28",
5
+ "headline": "Documentation corrected to match actual behavior across several primitives",
6
+ "summary": "A set of JSDoc / doc-comment corrections where the documented contract had drifted from what the code does. No behavior changes — the implementations already behaved as now documented; only the docs were wrong. The most operator-relevant is the JWT signer doc: an expiring token signed without an explicit jti receives an auto-minted 128-bit jti (so the replay-defense path has the jti it needs), which the sign-opts doc previously denied. Also corrected: did.resolve's unsupported-method error now names did:jwk (always supported); b.cose.verify is marked stable to match its stable sign sibling and the CWT / EAT / SCITT / mdoc verifiers built on it; b.linkHeader.serialize's doc now states every parameter value is double-quoted; b.auth.saml verifyResponse's documented return shape now lists inResponseTo and issuer (both always returned); and the rate-limit custom-backend contract drops a gc member the middleware never invoked.",
7
+ "sections": [
8
+ {
9
+ "heading": "Fixed",
10
+ "items": [
11
+ {
12
+ "title": "JWT signer doc now describes the auto-minted jti on expiring tokens",
13
+ "body": "`b.auth.jwt.sign`'s opts doc claimed that omitting `jti` adds no jti. In fact, when a token carries an `exp` and no operator-supplied `jti`, the signer auto-mints a random 128-bit `jti` so a replay-protected token always carries the identifier `verify`'s replay store requires. The doc now describes this; pass an explicit `jti` for a deterministic value. Behavior is unchanged."
14
+ },
15
+ {
16
+ "title": "`b.did.resolve` unsupported-method error now names did:jwk",
17
+ "body": "The thrown error for an unsupported DID method listed only `did:key` and `did:web`, omitting `did:jwk`, which `resolve` fully supports. The message now reads `(did:key, did:jwk, and did:web only)`."
18
+ },
19
+ {
20
+ "title": "`b.cose.verify` marked stable",
21
+ "body": "`b.cose.verify` carried `@status experimental` while its `b.cose.sign` sibling is stable and the CWT / EAT / SCITT / mdoc verifiers that depend on it are stable and shipped. The verifier is the same maturity as the rest of the COSE_Sign1 round-trip; its status now reflects that."
22
+ },
23
+ {
24
+ "title": "`b.linkHeader.serialize` doc matches its quoting behavior",
25
+ "body": "The doc said parameters are token-encoded when they fit RFC 7230 token grammar and double-quoted otherwise. The serializer always double-quotes every value (valid under RFC 8288, and required for space-separated multi-rel and media-type values). The doc now states that."
26
+ },
27
+ {
28
+ "title": "`b.auth.saml` verifyResponse documented return shape lists all fields",
29
+ "body": "The prose and the example each omitted a different field that `verifyResponse` always returns. The documented shape now lists all of `nameId`, `nameIdFormat`, `sessionIndex`, `attributes`, `audience`, `inResponseTo`, and `issuer`."
30
+ },
31
+ {
32
+ "title": "rate-limit custom-backend contract is `{ take, reset }`",
33
+ "body": "The custom-backend opts doc listed a `gc` member that the middleware never reads or invokes (the runtime contract is `take` / `reset` / `close`, and the error message already said `{ take, reset }`). The documented shape now matches; an operator-supplied `gc` was always silently ignored."
34
+ },
35
+ {
36
+ "title": "`b.mail.agent.create` doc no longer lists consumer as a method",
37
+ "body": "The created agent's method list named `consumer`, which is not a method on the returned object — the queue consumer is the sibling export `b.mail.agent.consumer`. The doc now says so."
38
+ }
39
+ ]
40
+ }
41
+ ]
42
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.13.24",
4
+ "date": "2026-05-28",
5
+ "headline": "`b.guard*` docs corrected: the compliance-posture opt key, the gate API, and validate return shapes",
6
+ "summary": "Documentation corrections across the b.guard* family. The most consequential: the posture-selection option was documented as `compliance:` in many guards' @opts, but the working key is `compliancePosture:` — passing the documented `{ compliance: \"hipaa\" }` was silently ignored, so a compliance posture (e.g. HIPAA PII redaction) never activated. If you select a posture via the gate/validate/sanitize options, use `compliancePosture:`; `compliance:` had no effect. The guard docs now name the correct key uniformly. Also corrected: gate examples and prose that invoked the gate as a callable or via `.run` / `.inspect` (the gate is an object whose method is `.check(ctx)`), and validate() return shapes that listed `severities` / `summary` / `refusal` fields the function never returned (it returns `{ ok, issues }`).",
7
+ "sections": [
8
+ {
9
+ "heading": "Fixed",
10
+ "items": [
11
+ {
12
+ "title": "guard posture option is `compliancePosture:`, not `compliance:`",
13
+ "body": "Many `b.guard*` primitives documented the compliance-posture selector as `compliance: \"hipaa\"|\"pci-dss\"|\"gdpr\"|\"soc2\"` in their `@opts`, but the family resolver reads `compliancePosture:`. Passing `{ compliance: \"hipaa\" }` was accepted and silently ignored — the posture overlay (e.g. CSV `piiPolicy: \"redact\"` under HIPAA) never applied, leaving the default policy in force. The docs across the guard family now name `compliancePosture:` consistently (the key the resolver and `b.guardX.compliancePosture(name)` already used). Action: if you selected a posture with `compliance:`, switch to `compliancePosture:` — the posture was not taking effect before."
14
+ },
15
+ {
16
+ "title": "guard gate is an object with `.check(ctx)`, not a callable",
17
+ "body": "Several guards' gate `@example`s and prose invoked the gate as a function (`g({...})`), via `.run(...)`, or via `.inspect(...)`, and a few described it as \"an async function\". `b.guardX.gate(opts)` returns an object whose async method is `.check(ctx)`; the examples and prose now use `.check`, so they run as written."
18
+ },
19
+ {
20
+ "title": "guard validate() returns `{ ok, issues }`",
21
+ "body": "Several guards documented `validate()` as returning `{ ok, issues, severities }`, `{ ok, issues, summary }`, or `{ ok, issues, refusal? }`. The function returns `{ ok, issues }` (each issue carries its own `severity` / `kind`); the documented extra top-level fields were never present. The docs now state the actual shape."
22
+ }
23
+ ]
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.13.25",
4
+ "date": "2026-05-28",
5
+ "headline": "Agent idempotency results and orchestrator/tenant registry rows are sealed at rest",
6
+ "summary": "The b.agent.idempotency, b.agent.orchestrator, and b.agent.tenant primitives documented their stored rows as sealed at rest, but the values were written as plaintext JSON — a database dump could expose cached result payloads (which can carry mail-move / search data), the tenant ids that own each agent, and operator-supplied endpoint metadata. Those values are now sealed via b.cryptoField (XChaCha20-Poly1305 through the vault) before they reach the backing store and unsealed on read, when a vault is configured — which is the default in a booted app. Each ciphertext is AAD-bound to its row identity so a database-write attacker cannot copy a sealed value between rows. Reads of rows written before this release (plain JSON) continue to work unchanged, and a vault-less deployment stores rows as before. No API or call-site changes are required.",
7
+ "sections": [
8
+ {
9
+ "heading": "Security",
10
+ "items": [
11
+ {
12
+ "title": "Agent idempotency / orchestrator / tenant rows sealed at rest",
13
+ "body": "`b.agent.idempotency` cached result blobs, `b.agent.orchestrator` registry rows (owning tenant id + endpoint metadata), and `b.agent.tenant` registry-row metadata are now sealed via `b.cryptoField` when a vault is configured (the default in a booted app via `b.start`). The fields were previously stored as plaintext despite the docs describing them as sealed, so a DB dump exposed cached payloads, agent↔tenant ownership, and endpoint detail. Each sealed value is AAD-bound to the row identity (the idempotency key hash / the agent name / the tenant id), so a sealed value cannot be copied between rows. Rows written before this release remain readable (a non-sealed value passes through unseal), and a vault-less deployment behaves as before. No call-site changes."
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "heading": "Fixed",
19
+ "items": [
20
+ {
21
+ "title": "`b.agent.saga` run() return/throw shape documented correctly",
22
+ "body": "The doc said `run()` resolves to `{ status: \"failed\", failedStep, lastCompensationError }` on failure and to a bare final state. In fact it resolves to `{ status: \"completed\", sagaId, state }` on success and rejects (throws) on step failure with an error carrying `failedStep`, `cause`, `compensationCause`, and `failedCompStepName`. The docs now describe the actual contract."
23
+ },
24
+ {
25
+ "title": "`b.agent.idempotency` put() example uses the real option name",
26
+ "body": "The example passed `{ argsFingerprint: ... }`, which the function does not read; the fingerprint option is `requestFingerprint` (or `args`). The example now uses `requestFingerprint`."
27
+ },
28
+ {
29
+ "title": "`b.agent.tenant.derivedKey` @since corrected to 0.9.26",
30
+ "body": "It was tagged `@since 0.9.25`, a version before the tenant module existed; it ships in 0.9.26 alongside `b.agent.tenant.create`."
31
+ },
32
+ {
33
+ "title": "`b.agent.trace.injectIntoEnvelope` documented as single-argument",
34
+ "body": "The doc listed a second `currentSpan` argument the function ignores — it always injects the currently-active span's trace context. The doc now shows `injectIntoEnvelope(envelope)` and notes it should be called while the intended span is active."
35
+ }
36
+ ]
37
+ }
38
+ ]
39
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.13.26",
4
+ "date": "2026-05-28",
5
+ "headline": "`b.cryptoField.unsealRow` nulls a sealed column on unseal failure instead of returning the forged ciphertext",
6
+ "summary": "When a sealed column failed to unseal — a DB-write attacker's forged `vault:<…>` payload, or a valid ciphertext copied into a different row so the AAD no longer matches — unsealRow recorded the failure on the audit chain but then kept the original attacker-crafted string in the field rather than nulling it, despite the documented contract that downstream sees 'no value'. A write-back guard discarded the intended null on the failure path. The column is now nulled on any unseal failure, so a forged or cross-row-copied value never reaches downstream code as if it were a real plaintext. Valid values round-trip unchanged and genuinely-unsealed pass-through values are still kept. This hardens every sealed-column reader, including the agent idempotency / orchestrator / tenant rows sealed in 0.13.25.",
7
+ "sections": [
8
+ {
9
+ "heading": "Security",
10
+ "items": [
11
+ {
12
+ "title": "Sealed columns are nulled on unseal failure (forged / cross-row ciphertext)",
13
+ "body": "`b.cryptoField.unsealRow` now nulls a sealed field when its value fails to unseal — a crafted `vault:`/`vault.aad:` payload written by a DB-write attacker, or a valid ciphertext copied to a different row (AAD mismatch). Previously the field kept the attacker-controlled string, so downstream code could read the forged ciphertext as if it were the plaintext. The audit emit (`system.crypto.unseal_failed`) is unchanged. Valid round-trips and not-actually-sealed pass-through values are unaffected. A regression test pins the forged-value, cross-row-copy, and pass-through cases."
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "heading": "Fixed",
19
+ "items": [
20
+ {
21
+ "title": "Audit checkpoint docs name the actual signature algorithm",
22
+ "body": "`b.audit.checkpoint` / `b.audit.verifyCheckpoints` described the anchor signature as ML-DSA-87, but the checkpoint is signed with the configured `b.auditSign` algorithm — SLH-DSA-SHAKE-256f by default (ML-DSA-87 / ML-DSA-65 are opt-in). The docs and the verify-failure reason now refer to the post-quantum signature without naming a specific algorithm the operator may not be using."
23
+ },
24
+ {
25
+ "title": "`b.storage.chunkScratch` example and assembly description corrected",
26
+ "body": "The `assemble()` example omitted the mandatory `chunkEncryptionKeys` argument (one sealed key per chunk, returned by `saveChunk`), so it would have thrown as written; it now collects and passes the keys. The prose no longer claims the primitive writes a final file with an 'atomic finalize' — `assemble()` concatenates the chunks in order and returns the assembled bytes for the caller to persist."
27
+ }
28
+ ]
29
+ }
30
+ ]
31
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.13.27",
4
+ "date": "2026-05-28",
5
+ "headline": "Documentation corrected across api-encrypt, mail-crypto, mail-store, and calendar",
6
+ "summary": "A set of JSDoc corrections where the documented contract had drifted from the code. The most actionable: b.middleware.apiEncrypt's @opts named the keypair fields secretKey / ecSecretKey, but the middleware requires privateKey / ecPrivateKey (the shape b.crypto.generateEncryptionKeyPair returns), so a keypair built from the docs threw INVALID_KEYPAIR at construction; the same block documented a wrong custom-nonceStore interface and an example calling a non-existent b.crypto.keypair(). Also corrected: the b.mail.crypto facade and the PGP module described S/MIME sign/verify and PGP encrypt/decrypt/WKD as deferred when they ship and are live; b.mail.crypto.smime.checkCert documented a return shape whose field names did not match what it returns; and b.mailStore.create listed a destroy method it does not expose. No code behavior changed — only the docs were wrong.",
7
+ "sections": [
8
+ {
9
+ "heading": "Fixed",
10
+ "items": [
11
+ {
12
+ "title": "`b.middleware.apiEncrypt` options documented with the correct field names",
13
+ "body": "The `@opts` listed the keypair as `{ publicKey, secretKey, ecPublicKey, ecSecretKey }`, but the middleware requires `{ publicKey, privateKey, ecPublicKey, ecPrivateKey }` — the shape `b.crypto.generateEncryptionKeyPair()` returns — and threw `INVALID_KEYPAIR` for the documented shape. The custom `nonceStore` interface was documented as `{ has, add, prune }` but the middleware calls `{ checkAndInsert, purgeExpired, close }`, and the example called a non-existent `b.crypto.keypair()`. All three now match the implementation (`b.crypto.generateEncryptionKeyPair()`)."
14
+ },
15
+ {
16
+ "title": "`b.mail.crypto` S/MIME and PGP availability described accurately",
17
+ "body": "The `b.mail.crypto` facade said S/MIME `sign()` / `verify()` were deferred, and the PGP module's intro said in-process encrypt / decrypt and WKD discovery would 'ship in v0.10.14'. All of these are implemented and live (PGP encrypt/decrypt/WKD were promoted to the stable surface in v0.11.32; S/MIME sign/verify/verifyAll run on the `b.cms` substrate). The docs now describe them as available; the genuinely-deferred PGP v6-signature-packet support remains noted as deferred."
18
+ },
19
+ {
20
+ "title": "`b.mail.crypto.smime.checkCert` return shape documented correctly",
21
+ "body": "The doc and example described the result as `{ subjectCN, issuerCN, validFrom, validTo, keyAlg, keyBits, sigAlg }`; the function returns `{ subject, issuer, validFrom, validTo, sigAlgName, sigAlgOid, keyType, fingerprint256 }` (full DN strings, no key-size field). The documented shape now matches."
22
+ },
23
+ {
24
+ "title": "`b.mailStore.create` method list matches the returned handle",
25
+ "body": "The doc listed a `destroy` method the handle does not expose and omitted `search` / `moveMessages` / `hardExpunge` that it does. The list now reflects the actual methods."
26
+ },
27
+ {
28
+ "title": "Smaller doc corrections",
29
+ "body": "`b.calendar` fromIcal / toIcal / validate now document that they also handle Task (VTODO), Note (VJOURNAL), and Group, not just Event. `b.middleware.requireAuth`'s `prefersJson` default is documented as Accept / X-Requested-With only (Content-Type is intentionally not a signal, as the module already noted). The default CSP nonce is 24 base64 chars (16 bytes), not 22. `b.mail.bimi`'s returned `evidenceDocument` is noted as echoed from the operator-supplied option rather than pulled from the certificate."
30
+ }
31
+ ]
32
+ }
33
+ ]
34
+ }