@blamejs/core 0.15.12 → 0.15.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 (365) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/index.js +2 -0
  3. package/lib/a2a-tasks.js +45 -29
  4. package/lib/acme.js +6 -5
  5. package/lib/agent-event-bus.js +18 -4
  6. package/lib/agent-idempotency.js +7 -6
  7. package/lib/agent-orchestrator.js +2 -5
  8. package/lib/agent-saga.js +3 -5
  9. package/lib/agent-snapshot.js +32 -2
  10. package/lib/agent-tenant.js +2 -5
  11. package/lib/ai-adverse-decision.js +2 -15
  12. package/lib/ai-aedt-bias-audit.js +2 -1
  13. package/lib/ai-capability.js +1 -6
  14. package/lib/ai-content-detect.js +1 -3
  15. package/lib/ai-dp.js +1 -5
  16. package/lib/ai-frontier-protocol.js +1 -1
  17. package/lib/ai-input.js +2 -2
  18. package/lib/ai-model-manifest.js +1 -1
  19. package/lib/ai-output.js +16 -7
  20. package/lib/ai-pref.js +3 -8
  21. package/lib/ai-quota.js +3 -14
  22. package/lib/api-key.js +37 -28
  23. package/lib/api-snapshot.js +4 -1
  24. package/lib/app-shutdown.js +7 -1
  25. package/lib/archive-adapters.js +2 -4
  26. package/lib/archive-entry-policy.js +32 -0
  27. package/lib/archive-gz.js +9 -0
  28. package/lib/archive-read.js +14 -24
  29. package/lib/archive-tar-read.js +56 -24
  30. package/lib/archive.js +6 -12
  31. package/lib/arg-parser.js +7 -6
  32. package/lib/asn1-der.js +70 -22
  33. package/lib/asyncapi-traits.js +2 -6
  34. package/lib/atomic-file.js +312 -33
  35. package/lib/audit-chain.js +183 -54
  36. package/lib/audit-daily-review.js +36 -16
  37. package/lib/audit-emit.js +82 -0
  38. package/lib/audit-sign.js +258 -0
  39. package/lib/audit-tools.js +108 -23
  40. package/lib/audit.js +91 -2
  41. package/lib/auth/access-lock.js +7 -29
  42. package/lib/auth/bot-challenge.js +1 -1
  43. package/lib/auth/ciba.js +43 -4
  44. package/lib/auth/dpop.js +29 -36
  45. package/lib/auth/fido-mds3.js +14 -16
  46. package/lib/auth/jwt-external.js +26 -8
  47. package/lib/auth/jwt.js +15 -17
  48. package/lib/auth/lockout.js +24 -27
  49. package/lib/auth/oauth.js +67 -45
  50. package/lib/auth/oid4vci.js +55 -32
  51. package/lib/auth/oid4vp.js +3 -2
  52. package/lib/auth/openid-federation.js +6 -6
  53. package/lib/auth/passkey.js +4 -4
  54. package/lib/auth/password.js +8 -2
  55. package/lib/auth/saml.js +70 -40
  56. package/lib/auth/sd-jwt-vc-holder.js +2 -14
  57. package/lib/auth/sd-jwt-vc-issuer.js +2 -14
  58. package/lib/auth/sd-jwt-vc.js +25 -5
  59. package/lib/auth/status-list.js +21 -9
  60. package/lib/auth/step-up.js +15 -13
  61. package/lib/auth-bot-challenge.js +27 -39
  62. package/lib/backup/bundle.js +18 -20
  63. package/lib/backup/crypto.js +23 -8
  64. package/lib/backup/index.js +55 -67
  65. package/lib/backup/manifest.js +7 -1
  66. package/lib/bounded-map.js +112 -1
  67. package/lib/breach-deadline.js +1 -11
  68. package/lib/break-glass.js +41 -22
  69. package/lib/cache.js +7 -18
  70. package/lib/cbor.js +36 -12
  71. package/lib/cdn-cache-control.js +15 -12
  72. package/lib/cert.js +8 -13
  73. package/lib/chain-writer.js +162 -47
  74. package/lib/cli.js +10 -2
  75. package/lib/client-hints.js +7 -9
  76. package/lib/cloud-events.js +43 -33
  77. package/lib/cluster-storage.js +9 -41
  78. package/lib/cms-codec.js +2 -1
  79. package/lib/codepoint-class.js +84 -1
  80. package/lib/compliance-ai-act-logging.js +1 -6
  81. package/lib/compliance-ai-act-transparency.js +2 -4
  82. package/lib/compliance-eaa.js +2 -12
  83. package/lib/compliance-sanctions-fetcher.js +21 -28
  84. package/lib/compliance-sanctions.js +11 -21
  85. package/lib/compliance.js +3 -10
  86. package/lib/config-drift.js +24 -20
  87. package/lib/content-credentials.js +11 -8
  88. package/lib/content-digest.js +21 -12
  89. package/lib/cookies.js +15 -13
  90. package/lib/cose.js +39 -11
  91. package/lib/cra-report.js +1 -11
  92. package/lib/crdt.js +2 -1
  93. package/lib/crypto-field.js +34 -33
  94. package/lib/crypto.js +138 -7
  95. package/lib/csp.js +239 -3
  96. package/lib/daemon.js +46 -42
  97. package/lib/data-act.js +46 -13
  98. package/lib/db-file-lifecycle.js +14 -6
  99. package/lib/db-query.js +75 -49
  100. package/lib/db.js +66 -27
  101. package/lib/dbsc.js +5 -6
  102. package/lib/ddl-change-control.js +18 -14
  103. package/lib/deprecate.js +4 -5
  104. package/lib/did.js +6 -2
  105. package/lib/dora.js +43 -13
  106. package/lib/dr-runbook.js +1 -1
  107. package/lib/dsa.js +2 -1
  108. package/lib/dsr.js +30 -20
  109. package/lib/early-hints.js +19 -0
  110. package/lib/eat.js +5 -1
  111. package/lib/external-db-migrate.js +21 -22
  112. package/lib/external-db.js +74 -30
  113. package/lib/fda-21cfr11.js +42 -13
  114. package/lib/fdx.js +22 -18
  115. package/lib/file-upload.js +80 -66
  116. package/lib/flag-evaluation-context.js +5 -8
  117. package/lib/flag-providers.js +6 -2
  118. package/lib/forms.js +1 -1
  119. package/lib/framework-error.js +12 -0
  120. package/lib/framework-schema.js +19 -0
  121. package/lib/fsm.js +7 -12
  122. package/lib/gate-contract.js +911 -39
  123. package/lib/gdpr-ropa.js +22 -22
  124. package/lib/graphql-federation.js +18 -5
  125. package/lib/guard-agent-registry.js +2 -1
  126. package/lib/guard-all.js +3 -2
  127. package/lib/guard-archive.js +9 -30
  128. package/lib/guard-auth.js +23 -80
  129. package/lib/guard-cidr.js +20 -96
  130. package/lib/guard-csv.js +135 -196
  131. package/lib/guard-domain.js +23 -106
  132. package/lib/guard-dsn.js +17 -14
  133. package/lib/guard-email.js +46 -53
  134. package/lib/guard-envelope.js +2 -2
  135. package/lib/guard-event-bus-topic.js +2 -1
  136. package/lib/guard-filename.js +12 -60
  137. package/lib/guard-graphql.js +28 -75
  138. package/lib/guard-html-wcag.js +15 -2
  139. package/lib/guard-html.js +74 -128
  140. package/lib/guard-idempotency-key.js +2 -1
  141. package/lib/guard-image.js +280 -77
  142. package/lib/guard-imap-command.js +9 -10
  143. package/lib/guard-jmap.js +1 -1
  144. package/lib/guard-json.js +101 -109
  145. package/lib/guard-jsonpath.js +20 -88
  146. package/lib/guard-jwt.js +32 -114
  147. package/lib/guard-list-id.js +2 -7
  148. package/lib/guard-list-unsubscribe.js +2 -7
  149. package/lib/guard-mail-compose.js +5 -6
  150. package/lib/guard-mail-move.js +3 -2
  151. package/lib/guard-mail-query.js +5 -3
  152. package/lib/guard-mail-sieve.js +6 -7
  153. package/lib/guard-managesieve-command.js +6 -5
  154. package/lib/guard-markdown.js +76 -110
  155. package/lib/guard-message-id.js +5 -6
  156. package/lib/guard-mime.js +20 -99
  157. package/lib/guard-oauth.js +19 -73
  158. package/lib/guard-pdf.js +65 -72
  159. package/lib/guard-pop3-command.js +13 -14
  160. package/lib/guard-posture-chain.js +2 -1
  161. package/lib/guard-regex.js +24 -99
  162. package/lib/guard-saga-config.js +2 -1
  163. package/lib/guard-shell.js +22 -87
  164. package/lib/guard-smtp-command.js +9 -12
  165. package/lib/guard-sql.js +15 -13
  166. package/lib/guard-stream-args.js +2 -1
  167. package/lib/guard-svg.js +103 -149
  168. package/lib/guard-template.js +23 -80
  169. package/lib/guard-tenant-id.js +2 -1
  170. package/lib/guard-text.js +592 -0
  171. package/lib/guard-time.js +27 -95
  172. package/lib/guard-uuid.js +21 -93
  173. package/lib/guard-xml.js +76 -106
  174. package/lib/guard-yaml.js +24 -60
  175. package/lib/html-balance.js +7 -3
  176. package/lib/http-client-cache.js +5 -12
  177. package/lib/http-client-cookie-jar.js +35 -16
  178. package/lib/http-client.js +233 -74
  179. package/lib/http-message-signature.js +3 -2
  180. package/lib/i18n-messageformat.js +5 -7
  181. package/lib/i18n.js +83 -26
  182. package/lib/iab-tcf.js +3 -2
  183. package/lib/importmap-integrity.js +41 -1
  184. package/lib/inbox.js +8 -8
  185. package/lib/incident-report.js +12 -27
  186. package/lib/ip-utils.js +49 -6
  187. package/lib/jobs.js +3 -2
  188. package/lib/json-patch.js +1 -1
  189. package/lib/json-path.js +24 -3
  190. package/lib/jtd.js +2 -2
  191. package/lib/keychain.js +6 -18
  192. package/lib/legal-hold.js +30 -23
  193. package/lib/log-stream-cloudwatch.js +44 -112
  194. package/lib/log-stream-otlp-grpc.js +17 -14
  195. package/lib/log-stream-otlp.js +16 -80
  196. package/lib/log-stream-webhook.js +20 -92
  197. package/lib/log.js +2 -2
  198. package/lib/mail-agent.js +2 -2
  199. package/lib/mail-arc-sign.js +8 -3
  200. package/lib/mail-arf.js +4 -3
  201. package/lib/mail-auth.js +43 -69
  202. package/lib/mail-bimi.js +35 -55
  203. package/lib/mail-bounce.js +3 -3
  204. package/lib/mail-crypto-pgp.js +3 -2
  205. package/lib/mail-crypto-smime.js +71 -6
  206. package/lib/mail-dav.js +8 -35
  207. package/lib/mail-deploy.js +15 -14
  208. package/lib/mail-dkim.js +19 -38
  209. package/lib/mail-greylist.js +17 -30
  210. package/lib/mail-helo.js +4 -7
  211. package/lib/mail-journal.js +13 -9
  212. package/lib/mail-mdn.js +12 -7
  213. package/lib/mail-rbl.js +7 -12
  214. package/lib/mail-scan.js +5 -7
  215. package/lib/mail-send-deliver.js +33 -20
  216. package/lib/mail-server-imap.js +32 -87
  217. package/lib/mail-server-jmap.js +39 -52
  218. package/lib/mail-server-managesieve.js +29 -83
  219. package/lib/mail-server-mx.js +33 -74
  220. package/lib/mail-server-net.js +177 -0
  221. package/lib/mail-server-pop3.js +30 -83
  222. package/lib/mail-server-rate-limit.js +30 -6
  223. package/lib/mail-server-registry.js +1 -1
  224. package/lib/mail-server-submission.js +34 -73
  225. package/lib/mail-server-tls.js +98 -2
  226. package/lib/mail-spam-score.js +9 -12
  227. package/lib/mail-store-fts.js +1 -1
  228. package/lib/mail-store.js +3 -2
  229. package/lib/mail.js +28 -13
  230. package/lib/markup-escape.js +31 -0
  231. package/lib/markup-tokenizer.js +78 -0
  232. package/lib/mcp-tool-registry.js +26 -23
  233. package/lib/mcp.js +7 -5
  234. package/lib/mdoc.js +36 -14
  235. package/lib/metrics.js +46 -33
  236. package/lib/middleware/age-gate.js +3 -23
  237. package/lib/middleware/api-encrypt.js +13 -24
  238. package/lib/middleware/assetlinks.js +2 -7
  239. package/lib/middleware/bearer-auth.js +2 -1
  240. package/lib/middleware/body-parser.js +41 -52
  241. package/lib/middleware/bot-disclose.js +7 -10
  242. package/lib/middleware/bot-guard.js +28 -28
  243. package/lib/middleware/clear-site-data.js +5 -1
  244. package/lib/middleware/compression.js +9 -0
  245. package/lib/middleware/cors.js +32 -23
  246. package/lib/middleware/csrf-protect.js +73 -23
  247. package/lib/middleware/daily-byte-quota.js +12 -30
  248. package/lib/middleware/deny-response.js +19 -1
  249. package/lib/middleware/fetch-metadata.js +35 -4
  250. package/lib/middleware/idempotency-key.js +4 -20
  251. package/lib/middleware/network-allowlist.js +61 -30
  252. package/lib/middleware/rate-limit.js +45 -44
  253. package/lib/middleware/scim-server.js +5 -3
  254. package/lib/middleware/security-headers.js +33 -7
  255. package/lib/middleware/security-txt.js +2 -6
  256. package/lib/middleware/speculation-rules.js +6 -3
  257. package/lib/middleware/tus-upload.js +14 -29
  258. package/lib/middleware/web-app-manifest.js +2 -7
  259. package/lib/migrations.js +4 -13
  260. package/lib/mime-parse.js +34 -17
  261. package/lib/module-loader.js +44 -0
  262. package/lib/money.js +105 -0
  263. package/lib/mtls-ca.js +125 -41
  264. package/lib/network-byte-quota.js +4 -18
  265. package/lib/network-dane.js +8 -6
  266. package/lib/network-dns-resolver.js +98 -7
  267. package/lib/network-dns.js +9 -2
  268. package/lib/network-dnssec.js +18 -17
  269. package/lib/network-heartbeat.js +3 -2
  270. package/lib/network-smtp-policy.js +52 -46
  271. package/lib/network-tls.js +67 -47
  272. package/lib/network-tsig.js +2 -2
  273. package/lib/nis2-report.js +2 -12
  274. package/lib/nist-crosswalk.js +1 -1
  275. package/lib/nonce-store.js +81 -3
  276. package/lib/ntp-check.js +28 -0
  277. package/lib/numeric-bounds.js +31 -8
  278. package/lib/object-store/azure-blob-bucket-ops.js +2 -6
  279. package/lib/object-store/azure-blob.js +6 -47
  280. package/lib/object-store/gcs-bucket-ops.js +4 -2
  281. package/lib/object-store/gcs.js +14 -75
  282. package/lib/object-store/http-put.js +2 -7
  283. package/lib/object-store/http-request.js +157 -0
  284. package/lib/object-store/local.js +37 -17
  285. package/lib/object-store/sigv4-bucket-ops.js +2 -1
  286. package/lib/object-store/sigv4.js +10 -79
  287. package/lib/observability-otlp-exporter.js +29 -29
  288. package/lib/observability.js +95 -0
  289. package/lib/openapi-paths-builder.js +7 -3
  290. package/lib/otel-export.js +7 -10
  291. package/lib/outbox.js +54 -41
  292. package/lib/pagination.js +11 -15
  293. package/lib/parsers/safe-env.js +5 -4
  294. package/lib/parsers/safe-ini.js +10 -4
  295. package/lib/parsers/safe-toml.js +11 -9
  296. package/lib/parsers/safe-xml.js +3 -3
  297. package/lib/parsers/safe-yaml.js +28 -9
  298. package/lib/pick.js +63 -5
  299. package/lib/pipl-cn.js +69 -59
  300. package/lib/privacy-pass.js +8 -6
  301. package/lib/problem-details.js +2 -5
  302. package/lib/pubsub.js +4 -8
  303. package/lib/queue-local.js +10 -3
  304. package/lib/redact.js +9 -4
  305. package/lib/render.js +4 -2
  306. package/lib/request-helpers.js +334 -29
  307. package/lib/resource-access-lock.js +3 -3
  308. package/lib/restore-bundle.js +46 -18
  309. package/lib/restore-rollback.js +10 -4
  310. package/lib/restore.js +24 -22
  311. package/lib/retention.js +23 -9
  312. package/lib/router.js +17 -4
  313. package/lib/safe-async.js +457 -0
  314. package/lib/safe-buffer.js +137 -6
  315. package/lib/safe-decompress.js +2 -1
  316. package/lib/safe-dns.js +2 -1
  317. package/lib/safe-ical.js +14 -28
  318. package/lib/safe-icap.js +1 -1
  319. package/lib/safe-json.js +51 -8
  320. package/lib/safe-jsonpath.js +6 -5
  321. package/lib/safe-mime.js +25 -29
  322. package/lib/safe-redirect.js +2 -5
  323. package/lib/safe-schema.js +7 -6
  324. package/lib/safe-sieve.js +1 -1
  325. package/lib/safe-sql.js +126 -0
  326. package/lib/safe-vcard.js +13 -27
  327. package/lib/sandbox-worker.js +15 -2
  328. package/lib/sandbox.js +4 -3
  329. package/lib/scheduler.js +22 -13
  330. package/lib/sec-cyber.js +82 -37
  331. package/lib/seeders.js +4 -11
  332. package/lib/self-update-standalone-verifier.js +16 -0
  333. package/lib/self-update.js +92 -69
  334. package/lib/session-device-binding.js +112 -66
  335. package/lib/session.js +219 -7
  336. package/lib/sql.js +228 -81
  337. package/lib/sse.js +6 -10
  338. package/lib/static.js +201 -111
  339. package/lib/storage.js +4 -2
  340. package/lib/structured-fields.js +275 -16
  341. package/lib/template.js +7 -5
  342. package/lib/tenant-quota.js +53 -38
  343. package/lib/tsa.js +16 -17
  344. package/lib/validate-opts.js +154 -8
  345. package/lib/vault/index.js +5 -0
  346. package/lib/vault/passphrase-ops.js +22 -26
  347. package/lib/vault/passphrase-source.js +8 -3
  348. package/lib/vault/rotate.js +13 -18
  349. package/lib/vault/seal-pem-file.js +34 -49
  350. package/lib/vault-aad.js +3 -2
  351. package/lib/vc.js +3 -8
  352. package/lib/vendor/MANIFEST.json +10 -10
  353. package/lib/vendor/public-suffix-list.dat +23 -10
  354. package/lib/vendor/public-suffix-list.data.js +5498 -5494
  355. package/lib/vex.js +35 -13
  356. package/lib/web-push-vapid.js +23 -20
  357. package/lib/webhook-dispatcher.js +706 -0
  358. package/lib/webhook.js +59 -19
  359. package/lib/websocket-channels.js +9 -7
  360. package/lib/websocket.js +8 -4
  361. package/lib/worm.js +3 -4
  362. package/lib/ws-client.js +65 -56
  363. package/lib/x509-chain.js +44 -0
  364. package/package.json +1 -1
  365. package/sbom.cdx.json +6 -6
@@ -53,6 +53,7 @@ var safeBuffer = require("./safe-buffer");
53
53
  var safeUrl = require("./safe-url");
54
54
  var ssrfGuard = require("./ssrf-guard");
55
55
  var networkProxy = require("./network-proxy");
56
+ var numericBounds = require("./numeric-bounds");
56
57
  var validateOpts = require("./validate-opts");
57
58
  var { FrameworkError, HttpClientError } = require("./framework-error");
58
59
 
@@ -165,15 +166,9 @@ function configurePool(opts) {
165
166
  "'. Allowed: " + allowed.join(", "));
166
167
  }
167
168
  }
168
- function _requirePositiveInt(name, value) {
169
- if (typeof value !== "number" || !isFinite(value) || value <= 0 || Math.floor(value) !== value) {
170
- throw new Error("httpClient.configurePool: " + name +
171
- " must be a positive integer, got " + JSON.stringify(value));
172
- }
173
- }
174
- if (opts.maxSockets !== undefined) _requirePositiveInt("maxSockets", opts.maxSockets);
175
- if (opts.maxFreeSockets !== undefined) _requirePositiveInt("maxFreeSockets", opts.maxFreeSockets);
176
- if (opts.keepAliveMsecs !== undefined) _requirePositiveInt("keepAliveMsecs", opts.keepAliveMsecs);
169
+ numericBounds.requireAllPositiveFiniteIntIfPresent(opts,
170
+ ["maxSockets", "maxFreeSockets", "keepAliveMsecs"], "httpClient.configurePool",
171
+ HttpClientError, "httpclient/bad-opts", { permanent: true });
177
172
  if (opts.keepAlive !== undefined && typeof opts.keepAlive !== "boolean") {
178
173
  throw new Error("httpClient.configurePool: keepAlive must be a boolean");
179
174
  }
@@ -721,6 +716,9 @@ function _stripCrossOriginAuth(headers) {
721
716
  * responseMode: "buffer", // "buffer" | "stream" | "always-resolve"
722
717
  * maxResponseBytes: undefined, // 16 MiB control / 1 GiB GET defaults; ignored in "stream"
723
718
  * onChunk: undefined, // (chunk: Buffer) => void — fires per response chunk
719
+ * maxBytesPerSec: undefined, // token-bucket bandwidth cap (bytes/sec) — paces BOTH the download response and the upload body with backpressure
720
+ * downloadTransform: undefined, // Transform | () => Transform | array — interpose on the response stream (e.g. a hashing or progress Transform)
721
+ * uploadTransform: undefined, // Transform | () => Transform | array — interpose on the request body before the wire
724
722
  * signal: undefined, // AbortSignal — propagated to req / stream
725
723
  * errorClass: HttpClientError, // FrameworkError subclass for thrown errors
726
724
  * observer: undefined, // (stage, info) => void — lifecycle hook
@@ -738,6 +736,132 @@ function _stripCrossOriginAuth(headers) {
738
736
  * });
739
737
  * // → { statusCode: 200, headers: { "content-type": "application/json", ... }, body: <Buffer> }
740
738
  */
739
+ // Token-bucket bandwidth throttle as a Transform. Splits each chunk to the
740
+ // credit available, releasing at most `bytesPerSec` per second (1s burst
741
+ // capacity). Because `_transform`'s callback is not called until the whole
742
+ // chunk has been released, backpressure propagates upstream — on a download
743
+ // the socket pauses, on an upload the body source pauses. Wired into both the
744
+ // response (download) and request-body (upload) pipelines so the helpers'
745
+ // SSRF guard / DNS pinning / byte caps / atomic-rename are preserved.
746
+ function _throttleStream(bytesPerSec) {
747
+ var capacity = bytesPerSec; // allow at most 1s worth of burst
748
+ var tokens = capacity;
749
+ var last = Date.now();
750
+ function refill() {
751
+ var now = Date.now();
752
+ if (now > last) {
753
+ tokens = Math.min(capacity, tokens + ((now - last) / 1000) * bytesPerSec);
754
+ last = now;
755
+ }
756
+ }
757
+ return new nodeStream.Transform({
758
+ transform: function (chunk, _enc, cb) {
759
+ var self = this;
760
+ var offset = 0;
761
+ function pump() {
762
+ refill();
763
+ if (offset >= chunk.length) { cb(); return; }
764
+ var avail = Math.floor(tokens);
765
+ if (avail >= 1) {
766
+ var n = Math.min(chunk.length - offset, avail);
767
+ tokens -= n;
768
+ self.push(chunk.subarray(offset, offset + n));
769
+ offset += n;
770
+ if (offset >= chunk.length) { cb(); return; }
771
+ setImmediate(pump); // more bytes, maybe more credit already
772
+ } else {
773
+ // ms to accrue one more byte of credit = (deficit / rate) seconds.
774
+ var waitMs = Math.ceil(C.TIME.seconds((1 - tokens) / bytesPerSec)) + 1;
775
+ var t = setTimeout(pump, waitMs);
776
+ if (t && typeof t.unref === "function") t.unref();
777
+ }
778
+ }
779
+ pump();
780
+ },
781
+ });
782
+ }
783
+
784
+ // Validate + normalize a transform opt into an array. Each entry may be a
785
+ // Transform stream instance OR a `() => Transform` factory (the factory form is
786
+ // retry/redirect-safe — a single Transform instance can only be piped once, so
787
+ // a fresh one is built per attempt). Returns { ok: [...] } or { err: Error }.
788
+ function _coerceTransforms(value, errorClass, name) {
789
+ if (value === undefined || value === null) return { ok: [] };
790
+ var list = Array.isArray(value) ? value : [value];
791
+ for (var i = 0; i < list.length; i++) {
792
+ var t = list[i];
793
+ var isFactory = typeof t === "function";
794
+ var isTransform = t && typeof t.pipe === "function" && typeof t.write === "function";
795
+ if (!isFactory && !isTransform) {
796
+ return { err: _makeError(errorClass, "BAD_ARG",
797
+ name + " must be a Transform stream or a () => Transform factory (or an array of them)", true) };
798
+ }
799
+ }
800
+ return { ok: list };
801
+ }
802
+
803
+ // Resolve a transform entry to a fresh stream instance (call the factory form).
804
+ function _resolveStage(t) { return typeof t === "function" ? t() : t; }
805
+
806
+ // Compose the response (download) stream: source → [throttle?] → [transforms…]
807
+ // → observe(progress + onChunk). Every stage is a Transform so backpressure
808
+ // flows from the consumer back to the socket (the throttle paces it). Returns
809
+ // `source` unchanged when nothing is interposed (preserves the bare-stream
810
+ // contract). A stage error propagates downstream so the returned body emits it.
811
+ function _buildDownloadStream(source, emitDownload, onChunk, throttleBps, transforms) {
812
+ var stages = [];
813
+ if (throttleBps) stages.push(_throttleStream(throttleBps));
814
+ for (var i = 0; i < transforms.length; i++) stages.push(_resolveStage(transforms[i]));
815
+ if (stages.length === 0 && !emitDownload && !onChunk) return source;
816
+ var observe = new nodeStream.Transform({
817
+ transform: function (chunk, _enc, cb) {
818
+ if (emitDownload) emitDownload(chunk.length);
819
+ if (onChunk) { try { onChunk(chunk); } catch (_e) { /* operator hook — drop-silent */ } }
820
+ cb(null, chunk);
821
+ },
822
+ });
823
+ var chain = [source].concat(stages).concat([observe]);
824
+ for (var c = 0; c < chain.length - 1; c++) {
825
+ var src = chain[c], dst = chain[c + 1];
826
+ src.on("error", (function (d) { return function (e) { d.destroy(e); }; })(dst));
827
+ src.pipe(dst);
828
+ }
829
+ return observe;
830
+ }
831
+
832
+ // Build the upload throttle/transform stages (parallel to the download side).
833
+ function _uploadStages(throttleBps, transforms) {
834
+ var stages = [];
835
+ if (throttleBps) stages.push(_throttleStream(throttleBps));
836
+ for (var i = 0; i < transforms.length; i++) stages.push(_resolveStage(transforms[i]));
837
+ return stages;
838
+ }
839
+
840
+ // Pipe a body (stream | Buffer | string) through throttle/transform stages into
841
+ // the request sink, pacing the upload and emitting progress on bytes delivered
842
+ // to the sink. Used only when a throttle/transform is configured; the plain
843
+ // no-stages path keeps its original direct write. A stage/source error tears
844
+ // down the request via onBodyError.
845
+ function _pipeThrottledUpload(body, sink, stages, emitUpload, onBodyError) {
846
+ var source = (body && typeof body.pipe === "function")
847
+ ? body
848
+ : nodeStream.Readable.from(Buffer.isBuffer(body) ? body : Buffer.from(String(body), "utf8"));
849
+ var observe = new nodeStream.Transform({
850
+ transform: function (chunk, _enc, cb) {
851
+ if (emitUpload) emitUpload(chunk.length);
852
+ cb(null, chunk);
853
+ },
854
+ });
855
+ var chain = [source].concat(stages).concat([observe]);
856
+ for (var c = 0; c < chain.length - 1; c++) {
857
+ var s = chain[c], d = chain[c + 1];
858
+ s.on("error", (function (dd) { return function (e) { dd.destroy(e); }; })(d));
859
+ s.pipe(d);
860
+ }
861
+ observe.on("error", onBodyError);
862
+ observe.pipe(sink);
863
+ }
864
+
741
865
  function request(opts) {
742
866
  if (!opts || !opts.url) {
743
867
  return Promise.reject(_makeError(opts && opts.errorClass, "BAD_ARG", "url is required", true));
@@ -770,6 +894,20 @@ function request(opts) {
770
894
  return Promise.reject(_makeError(opts.errorClass, "BAD_ARG",
771
895
  "onChunk must be a function (chunk: Buffer) -> void", true));
772
896
  }
897
+ // Bandwidth cap + transform interpose (download response + upload request
898
+ // body) — validated here, derived per-attempt in _requestSingle. maxBytesPerSec
899
+ // paces BOTH legs; downloadTransform / uploadTransform interpose custom
900
+ // Transform(s) (instance or () => Transform factory) on the respective leg.
901
+ if (opts.maxBytesPerSec !== undefined && opts.maxBytesPerSec !== null) {
902
+ if (typeof opts.maxBytesPerSec !== "number" || !isFinite(opts.maxBytesPerSec) || opts.maxBytesPerSec <= 0) {
903
+ return Promise.reject(_makeError(opts.errorClass, "BAD_ARG",
904
+ "maxBytesPerSec must be a positive finite number (bytes/sec)", true));
905
+ }
906
+ }
907
+ var _dlChk = _coerceTransforms(opts.downloadTransform, opts.errorClass, "downloadTransform");
908
+ if (_dlChk.err) return Promise.reject(_dlChk.err);
909
+ var _ulChk = _coerceTransforms(opts.uploadTransform, opts.errorClass, "uploadTransform");
910
+ if (_ulChk.err) return Promise.reject(_ulChk.err);
773
911
  if (opts.jar !== undefined && opts.jar !== null) {
774
912
  if (typeof opts.jar !== "object" ||
775
913
  typeof opts.jar.cookieHeaderFor !== "function" ||
@@ -1384,6 +1522,12 @@ function _requestSingle(opts) {
1384
1522
  // ---- _requestH1: existing node:http(s) path ----
1385
1523
 
1386
1524
  function _requestH1(transport, u, opts) {
1525
+ // Bandwidth/transform interpose — opts validated in request(); derived fresh
1526
+ // per attempt (each builds its own throttle + resolves transform factories, so
1527
+ // a retry / redirect never reuses a consumed Transform).
1528
+ var throttleBps = opts.maxBytesPerSec || null; // validated in request(); positive number or absent
1529
+ var downloadTransforms = _coerceTransforms(opts.downloadTransform, opts.errorClass, "downloadTransform").ok;
1530
+ var uploadTransforms = _coerceTransforms(opts.uploadTransform, opts.errorClass, "uploadTransform").ok;
1387
1531
  return new Promise(function (resolve, reject) {
1388
1532
  var method = (opts.method || "GET").toUpperCase();
1389
1533
  var headers = Object.assign({}, opts.headers || {});
@@ -1402,7 +1546,12 @@ function _requestH1(transport, u, opts) {
1402
1546
  return;
1403
1547
  }
1404
1548
 
1405
- if (Buffer.isBuffer(opts.body)) {
1549
+ if (Buffer.isBuffer(opts.body) && uploadTransforms.length === 0) {
1550
+ // A size-changing uploadTransform (gzip / encrypt / frame) makes the
1551
+ // original buffer length wrong, so the server would truncate extra bytes
1552
+ // or wait for bytes that never arrive. Omit Content-Length in that case
1553
+ // and let the body be chunked, framed on the actual transformed bytes. A
1554
+ // throttle alone preserves size, so Content-Length stays valid then.
1406
1555
  headers["Content-Length"] = opts.body.length;
1407
1556
  }
1408
1557
  // CVE-2026-22036 mitigation — refuse compressed responses by
@@ -1470,35 +1619,30 @@ function _requestH1(transport, u, opts) {
1470
1619
  _rejectStreamHttpError(res, opts.errorClass, res.statusCode, res.statusMessage || "", _reject);
1471
1620
  return;
1472
1621
  }
1473
- if (onDownloadProgress || onChunk) {
1474
- // Wrap the stream so chunks emit progress + onChunk to the
1475
- // operator. The framework's contract is to hand back the
1476
- // response stream unmodified; fix-up via a passthrough keeps
1477
- // that contract while observing the chunk sizes. onChunk
1478
- // gets the buffer itself (for hash-as-you-go); a throw from
1479
- // it is caught and dropped so a hash-mismatch detector can
1480
- // raise without breaking the response stream caller
1481
- // surfaces the error through their own pipe handler.
1482
- var passthrough = new nodeStream.PassThrough();
1483
- res.on("data", function (chunk) {
1484
- _emitDownload(chunk.length);
1485
- if (onChunk) {
1486
- try { onChunk(chunk); }
1487
- catch (_e) { /* operator-supplied hook — drop-silent */ }
1488
- }
1489
- passthrough.write(chunk);
1490
- });
1491
- res.on("end", function () { passthrough.end(); });
1492
- res.on("error", function (e) { passthrough.destroy(e); });
1493
- return _resolve({ statusCode: res.statusCode, headers: res.headers, body: passthrough });
1494
- }
1495
- return _resolve({ statusCode: res.statusCode, headers: res.headers, body: res });
1622
+ // Compose the response stream with the optional bandwidth throttle +
1623
+ // custom transforms + progress/onChunk observation. Each is a Transform
1624
+ // so backpressure reaches the socket; onChunk sees the bytes in order
1625
+ // (for hash-as-you-go) and a throw from it is dropped. Returns `res`
1626
+ // unchanged when nothing is interposed (bare-stream contract).
1627
+ var body = _buildDownloadStream(
1628
+ res, onDownloadProgress ? _emitDownload : null, onChunk, throttleBps, downloadTransforms);
1629
+ return _resolve({ statusCode: res.statusCode, headers: res.headers, body: body });
1496
1630
  }
1497
1631
 
1498
1632
  var collector = safeBuffer.boundedChunkCollector({ maxBytes: maxResponseBytes });
1499
1633
  var capExceeded = false;
1500
1634
 
1501
- res.on("data", function (chunk) {
1635
+ // Route the buffered read through the SAME throttle + transform pipeline
1636
+ // as stream mode, so maxBytesPerSec and downloadTransform apply in buffer
1637
+ // and always-resolve modes too (both documented to pace / transform the
1638
+ // download regardless of how the body is consumed). With nothing
1639
+ // interposed this returns `res` unchanged, so the plain path is identical.
1640
+ // The cap now bounds post-transform bytes — strictly safer for a
1641
+ // decompressing transform. Progress + onChunk fire inside the pipeline.
1642
+ var dlSource = _buildDownloadStream(
1643
+ res, onDownloadProgress ? _emitDownload : null, onChunk, throttleBps, downloadTransforms);
1644
+
1645
+ dlSource.on("data", function (chunk) {
1502
1646
  if (capExceeded) return;
1503
1647
  try { collector.push(chunk); }
1504
1648
  catch (_e) {
@@ -1508,13 +1652,8 @@ function _requestH1(transport, u, opts) {
1508
1652
  "response body exceeds " + maxResponseBytes + " bytes", true));
1509
1653
  return;
1510
1654
  }
1511
- _emitDownload(chunk.length);
1512
- if (onChunk) {
1513
- try { onChunk(chunk); }
1514
- catch (_e) { /* operator-supplied hook — drop-silent */ }
1515
- }
1516
1655
  });
1517
- res.on("end", function () {
1656
+ dlSource.on("end", function () {
1518
1657
  if (capExceeded) return;
1519
1658
  var buf = collector.result();
1520
1659
  if (observer) observer("response:end", {
@@ -1544,7 +1683,7 @@ function _requestH1(transport, u, opts) {
1544
1683
  _isPermanentStatus(res.statusCode), res.statusCode));
1545
1684
  }
1546
1685
  });
1547
- res.on("error", function (e) {
1686
+ dlSource.on("error", function (e) {
1548
1687
  if (capExceeded) return;
1549
1688
  if (observer) observer("error", { phase: "response", message: e.message });
1550
1689
  _reject(_makeError(opts.errorClass, e.code || "RES_ERROR", e.message, false));
@@ -1588,7 +1727,17 @@ function _requestH1(transport, u, opts) {
1588
1727
  catch (_e) { /* progress hooks are best-effort */ }
1589
1728
  }
1590
1729
 
1591
- if (opts.body && typeof opts.body.pipe === "function") {
1730
+ var ulStages = _uploadStages(throttleBps, uploadTransforms);
1731
+ if (opts.body != null && opts.body !== "" && ulStages.length > 0) {
1732
+ // Pace / transform the upload body through the stages, into req.
1733
+ _pipeThrottledUpload(opts.body, req, ulStages,
1734
+ onUploadProgress ? _emitUpload : null,
1735
+ function (e) {
1736
+ try { req.destroy(); } catch (_) { /* best-effort req teardown */ }
1737
+ _reject(_makeError(opts.errorClass, "REQ_BODY_ERROR",
1738
+ "request body stream error: " + e.message, false));
1739
+ });
1740
+ } else if (opts.body && typeof opts.body.pipe === "function") {
1592
1741
  if (onUploadProgress) {
1593
1742
  opts.body.on("data", function (c) { _emitUpload(c.length); });
1594
1743
  }
@@ -1623,6 +1772,10 @@ function _requestH1(transport, u, opts) {
1623
1772
  // ---- _requestH2: node:http2 path ----
1624
1773
 
1625
1774
  function _requestH2(transport, u, opts) {
1775
+ // Bandwidth/transform interpose — see _requestH1; derived fresh per attempt.
1776
+ var throttleBps = opts.maxBytesPerSec || null; // validated in request(); positive number or absent
1777
+ var downloadTransforms = _coerceTransforms(opts.downloadTransform, opts.errorClass, "downloadTransform").ok;
1778
+ var uploadTransforms = _coerceTransforms(opts.uploadTransform, opts.errorClass, "uploadTransform").ok;
1626
1779
  return new Promise(function (resolve, reject) {
1627
1780
  var method = (opts.method || "GET").toUpperCase();
1628
1781
  var responseMode = opts.responseMode || "buffer";
@@ -1642,7 +1795,13 @@ function _requestH2(transport, u, opts) {
1642
1795
  }
1643
1796
 
1644
1797
  var headers = _toH2Headers(method, u, opts.headers || {});
1645
- if (Buffer.isBuffer(opts.body)) headers["content-length"] = String(opts.body.length);
1798
+ // Omit content-length when a size-changing uploadTransform is interposed —
1799
+ // the original buffer length no longer frames the transformed body (HTTP/2
1800
+ // frames via DATA + END_STREAM, so no header is required). A throttle alone
1801
+ // keeps the size, so the length stays valid then.
1802
+ if (Buffer.isBuffer(opts.body) && uploadTransforms.length === 0) {
1803
+ headers["content-length"] = String(opts.body.length);
1804
+ }
1646
1805
 
1647
1806
  if (observer) observer("request:start", { method: method, url: String(opts.url), protocol: "h2" });
1648
1807
 
@@ -1686,24 +1845,29 @@ function _requestH2(transport, u, opts) {
1686
1845
  _rejectStreamHttpError(stream, opts.errorClass, statusCode, "", _reject);
1687
1846
  return;
1688
1847
  }
1689
- if (onChunkH2) {
1690
- var passthroughH2 = new nodeStream.PassThrough();
1691
- stream.on("data", function (chunk) {
1692
- try { onChunkH2(chunk); }
1693
- catch (_e) { /* operator-supplied hook — drop-silent */ }
1694
- passthroughH2.write(chunk);
1695
- });
1696
- stream.on("end", function () { passthroughH2.end(); });
1697
- stream.on("error", function (e) { passthroughH2.destroy(e); });
1698
- return _resolve({ statusCode: statusCode, headers: responseHeaders, body: passthroughH2 });
1699
- }
1700
- return _resolve({ statusCode: statusCode, headers: responseHeaders, body: stream });
1848
+ var bodyH2 = _buildDownloadStream(stream, null, onChunkH2, throttleBps, downloadTransforms);
1849
+ return _resolve({ statusCode: statusCode, headers: responseHeaders, body: bodyH2 });
1701
1850
  }
1702
1851
 
1703
1852
  var collector = safeBuffer.boundedChunkCollector({ maxBytes: maxResponseBytes });
1704
1853
  var capExceeded = false;
1705
1854
 
1706
- stream.on("data", function (chunk) {
1855
+ // Apply the throttle + transform pipeline in buffer / always-resolve mode
1856
+ // too (matches H1 + the documented contract); returns `stream` unchanged
1857
+ // when nothing is interposed. onChunk fires inside the pipeline.
1858
+ var dlH2 = _buildDownloadStream(stream, null, onChunkH2, throttleBps, downloadTransforms);
1859
+ // When a stage is interposed, a transform error surfaces on the pipeline
1860
+ // tail, not the raw h2 stream — reject on it. The plain path (dlH2 ===
1861
+ // stream) is already covered by the stream-level error handler below.
1862
+ if (dlH2 !== stream) {
1863
+ dlH2.on("error", function (e) {
1864
+ if (capExceeded) return;
1865
+ if (observer) observer("error", { phase: "stream", message: e.message });
1866
+ _reject(_makeError(opts.errorClass, e.code || "H2_STREAM_ERROR", e.message, false));
1867
+ });
1868
+ }
1869
+
1870
+ dlH2.on("data", function (chunk) {
1707
1871
  if (capExceeded) return;
1708
1872
  try { collector.push(chunk); }
1709
1873
  catch (_e) {
@@ -1713,12 +1877,8 @@ function _requestH2(transport, u, opts) {
1713
1877
  "response body exceeds " + maxResponseBytes + " bytes", true));
1714
1878
  return;
1715
1879
  }
1716
- if (onChunkH2) {
1717
- try { onChunkH2(chunk); }
1718
- catch (_e) { /* operator-supplied hook — drop-silent */ }
1719
- }
1720
1880
  });
1721
- stream.on("end", function () {
1881
+ dlH2.on("end", function () {
1722
1882
  if (capExceeded) return;
1723
1883
  var buf = collector.result();
1724
1884
  if (observer) observer("response:end", {
@@ -1756,7 +1916,14 @@ function _requestH2(transport, u, opts) {
1756
1916
  signal.addEventListener("abort", onAbort, { once: true });
1757
1917
  }
1758
1918
 
1759
- if (opts.body && typeof opts.body.pipe === "function") {
1919
+ var ulStagesH2 = _uploadStages(throttleBps, uploadTransforms);
1920
+ if (opts.body != null && opts.body !== "" && ulStagesH2.length > 0) {
1921
+ _pipeThrottledUpload(opts.body, stream, ulStagesH2, null, function (e) {
1922
+ try { stream.close(http2.constants.NGHTTP2_INTERNAL_ERROR); } catch (_) { /* best-effort h2 stream cancel */ }
1923
+ _reject(_makeError(opts.errorClass, "REQ_BODY_ERROR",
1924
+ "request body stream error: " + e.message, false));
1925
+ });
1926
+ } else if (opts.body && typeof opts.body.pipe === "function") {
1760
1927
  opts.body.on("error", function (e) {
1761
1928
  try { stream.close(http2.constants.NGHTTP2_INTERNAL_ERROR); } catch (_) { /* best-effort h2 stream cancel */ }
1762
1929
  _reject(_makeError(opts.errorClass, "REQ_BODY_ERROR",
@@ -1825,12 +1992,8 @@ function _validateDownloadOpts(opts) {
1825
1992
  }
1826
1993
  validateOpts.optionalPositiveFinite(opts.timeoutMs, "downloadStream: timeoutMs",
1827
1994
  HttpClientError, "httpclient/bad-opts");
1828
- if (opts.maxBytes !== undefined &&
1829
- (typeof opts.maxBytes !== "number" || !isFinite(opts.maxBytes) || opts.maxBytes <= 0 ||
1830
- Math.floor(opts.maxBytes) !== opts.maxBytes)) {
1831
- throw _hcErr("httpclient/bad-opts",
1832
- "downloadStream: maxBytes must be a positive finite integer");
1833
- }
1995
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.maxBytes,
1996
+ "downloadStream: maxBytes", HttpClientError, "httpclient/bad-opts", { permanent: true });
1834
1997
  }
1835
1998
 
1836
1999
  function _emitAudit(opts, action, outcome, metadata) {
@@ -2050,12 +2213,8 @@ function _validateUploadOpts(opts) {
2050
2213
  }
2051
2214
  validateOpts.optionalPositiveFinite(opts.timeoutMs, "uploadMultipartStream: timeoutMs",
2052
2215
  HttpClientError, "httpclient/bad-opts");
2053
- if (opts.maxBytes !== undefined &&
2054
- (typeof opts.maxBytes !== "number" || !isFinite(opts.maxBytes) || opts.maxBytes <= 0 ||
2055
- Math.floor(opts.maxBytes) !== opts.maxBytes)) {
2056
- throw _hcErr("httpclient/bad-opts",
2057
- "uploadMultipartStream: maxBytes must be a positive finite integer");
2058
- }
2216
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.maxBytes,
2217
+ "uploadMultipartStream: maxBytes", HttpClientError, "httpclient/bad-opts", { permanent: true });
2059
2218
  }
2060
2219
 
2061
2220
  /**
@@ -542,8 +542,9 @@ function verify(msg, opts) {
542
542
  var member = digestMembers[di].trim();
543
543
  var deq = member.indexOf("=");
544
544
  if (deq < 1) continue;
545
- if (member.slice(0, deq).trim().toLowerCase() !== "sha3-512") continue;
546
- var memberCanonical = "sha3-512=" + member.slice(deq + 1).trim();
545
+ var dkv = structuredFields.parseKeyValuePiece(member);
546
+ if (dkv.key !== "sha3-512") continue;
547
+ var memberCanonical = "sha3-512=" + dkv.value.trim();
547
548
  // crypto.timingSafeEqual is the length-tolerant constant-time wrapper
548
549
  // (returns false for unequal lengths without leaking via a length branch).
549
550
  if (bCrypto.timingSafeEqual(memberCanonical, expectedDigest)) { matchedDigest = true; break; }
@@ -45,6 +45,7 @@
45
45
  * shaped JSON entries continue to work unchanged.
46
46
  */
47
47
  var lazyRequire = require("./lazy-require");
48
+ var boundedMap = require("./bounded-map");
48
49
  var { defineClass } = require("./framework-error");
49
50
 
50
51
  var I18nMessageFormatError = defineClass("I18nMessageFormatError",
@@ -314,12 +315,9 @@ function _skipWs(state) {
314
315
  var _pluralRulesCache = new Map();
315
316
  function _pluralRules(locale, type) {
316
317
  var key = locale + "\x1f" + type;
317
- var pr = _pluralRulesCache.get(key);
318
- if (!pr) {
319
- pr = new Intl.PluralRules(locale, { type: type });
320
- _pluralRulesCache.set(key, pr);
321
- }
322
- return pr;
318
+ return boundedMap.getOrInsert(_pluralRulesCache, key, function () {
319
+ return new Intl.PluralRules(locale, { type: type });
320
+ });
323
321
  }
324
322
 
325
323
  function format(template, vars, locale) {
@@ -382,7 +380,7 @@ function looksLikeMessageFormat(template) {
382
380
  if (typeof template !== "string") return false;
383
381
  // Cheap structural check — full-syntax detection comes from parse()
384
382
  // throwing if it isn't valid MessageFormat.
385
- return /\{[^}]+,\s*(plural|select|selectordinal)\b/.test(template);
383
+ return /\{[^{}]+,\s*(plural|select|selectordinal)\b/.test(template);
386
384
  }
387
385
 
388
386
  module.exports = {
package/lib/i18n.js CHANGED
@@ -64,7 +64,7 @@ var requestHelpers = require("./request-helpers");
64
64
  var safeJson = require("./safe-json");
65
65
  var validateOpts = require("./validate-opts");
66
66
  var { I18nError } = require("./framework-error");
67
- var { boundedMap } = require("./bounded-map");
67
+ var { boundedMap, getOrInsert } = require("./bounded-map");
68
68
 
69
69
  // Per-instance formatter caches are keyed on (locale, JSON.stringify
70
70
  // formatOpts). A fixed `locales` set bounds the locale axis, but operators
@@ -133,6 +133,80 @@ function _validateLocaleArray(name, value) {
133
133
  }
134
134
  }
135
135
 
136
+ // Pure resolution-chain builder — the subtag-strip walk shared by the
137
+ // instance lookup and the public localeChain primitive. Start at the requested
138
+ // locale and strip subtag suffixes right-to-left ("pt-BR" -> "pt"); subtag
139
+ // stripping always applies (same language, less specific). Cross-locale
140
+ // fallback (to fallbackLocale, then defaultLocale) fires only when
141
+ // fallbackLocale is non-null — fallbackLocale: null is strict "this locale or
142
+ // miss". No duplicate baseline.
143
+ function _buildLocaleChain(locale, fallbackLocale, defaultLocale) {
144
+ var chain = [];
145
+ var current = locale;
146
+ while (current && chain.indexOf(current) === -1) {
147
+ chain.push(current);
148
+ var dash = current.lastIndexOf("-");
149
+ if (dash === -1) break;
150
+ current = current.slice(0, dash);
151
+ }
152
+ if (fallbackLocale === null) return chain;
153
+ if (chain.indexOf(fallbackLocale) === -1) chain.push(fallbackLocale);
154
+ if (chain.indexOf(defaultLocale) === -1) chain.push(defaultLocale);
155
+ return chain;
156
+ }
157
+
158
+ /**
159
+ * @primitive b.i18n.localeChain
160
+ * @signature b.i18n.localeChain(locale, opts)
161
+ * @since 0.15.13
162
+ * @status stable
163
+ * @related b.i18n.create
164
+ *
165
+ * Resolve a requested BCP 47 locale to its ordered fallback chain — the same
166
+ * subtag-strip logic the file-backed `t()` lookup uses, surfaced as a pure,
167
+ * instance-independent function so a data-backed consumer (translations in a
168
+ * database row, a CMS page, a CDN-cached asset) can drive its own per-`(resource,
169
+ * locale, field)` lookups on the framework's BCP 47 fallback instead of
170
+ * re-deriving subtag stripping by hand.
171
+ *
172
+ * Pure: no file loading, no message lookup. The chain is the requested locale,
173
+ * then each subtag-stripped parent ("fr-CA" -> "fr"), then `fallbackLocale`,
174
+ * then `defaultLocale`, de-duplicated. `fallbackLocale: null` gives strict
175
+ * "this locale or its parents only" (no cross-locale jump).
176
+ *
177
+ * @opts
178
+ * defaultLocale: string, // BCP 47; required; the final baseline
179
+ * fallbackLocale: string | null, // null = strict; omitted = defaultLocale
180
+ * locales: string[], // optional configured set; when given, defaultLocale + fallbackLocale must be members
181
+ *
182
+ * @example
183
+ * b.i18n.localeChain("fr-CA", { defaultLocale: "en", fallbackLocale: "en" });
184
+ * // → ["fr-CA", "fr", "en"]
185
+ * b.i18n.localeChain("zh-Hant-TW", { defaultLocale: "en", fallbackLocale: null });
186
+ * // → ["zh-Hant-TW", "zh-Hant", "zh"] (strict — no jump to en)
187
+ */
188
+ function localeChain(locale, opts) {
189
+ opts = opts || {};
190
+ _validateLocale("i18n.localeChain: locale", locale);
191
+ _validateLocale("i18n.localeChain: defaultLocale", opts.defaultLocale);
192
+ var defaultLocale = opts.defaultLocale;
193
+ var fallbackLocale = (opts.fallbackLocale === null) ? null
194
+ : ((opts.fallbackLocale === undefined) ? defaultLocale : opts.fallbackLocale);
195
+ if (fallbackLocale !== null) _validateLocale("i18n.localeChain: fallbackLocale", fallbackLocale);
196
+ if (opts.locales !== undefined) {
197
+ _validateLocaleArray("i18n.localeChain: locales", opts.locales);
198
+ if (opts.locales.indexOf(defaultLocale) === -1) {
199
+ throw _err("BAD_OPT", "i18n.localeChain: defaultLocale '" + defaultLocale +
200
+ "' must be one of the configured locales");
201
+ }
202
+ if (fallbackLocale !== null && opts.locales.indexOf(fallbackLocale) === -1) {
203
+ throw _err("BAD_OPT", "i18n.localeChain: fallbackLocale '" + fallbackLocale +
204
+ "' must be one of the configured locales");
205
+ }
206
+ }
207
+ return _buildLocaleChain(locale, fallbackLocale, defaultLocale);
208
+ }
209
+
136
210
  function _validateInterpolation(value) {
137
211
  if (value === undefined) return DEFAULTS.interpolation;
138
212
  if (typeof value !== "object" || value === null) {
@@ -366,13 +440,11 @@ function _makeFormatterCache(make, kind, emitObs) {
366
440
  return function getFormatter(locale, formatOpts) {
367
441
  var optsKey = formatOpts ? JSON.stringify(formatOpts) : "";
368
442
  var cacheKey = locale + "\x1f" + optsKey;
369
- var f = cache.get(cacheKey);
370
- if (!f) {
371
- f = make(locale, formatOpts);
372
- cache.set(cacheKey, f);
443
+ return getOrInsert(cache, cacheKey, function () {
444
+ var f = make(locale, formatOpts);
373
445
  emitObs("i18n.format.created", { kind: kind, locale: locale });
374
- }
375
- return f;
446
+ return f;
447
+ });
376
448
  };
377
449
  }
378
450
 
@@ -572,25 +644,9 @@ function create(opts) {
572
644
  }
573
645
 
574
646
  function _localeChain(locale) {
575
- // Build the resolution chain. Start with the requested locale and
576
- // strip subtag suffixes (`pt-BR` `pt`); subtag stripping always
577
- // applies because it's "same language, less specific" rather than a
578
- // cross-locale jump. Cross-locale fallback (to fallbackLocale, then
579
- // defaultLocale) only fires when fallbackLocale is non-null —
580
- // operators who set fallbackLocale: null get strict "this locale or
581
- // miss" semantics.
582
- var chain = [];
583
- var current = locale;
584
- while (current && chain.indexOf(current) === -1) {
585
- chain.push(current);
586
- var dash = current.lastIndexOf("-");
587
- if (dash === -1) break;
588
- current = current.slice(0, dash);
589
- }
590
- if (fallbackLocale === null) return chain;
591
- if (chain.indexOf(fallbackLocale) === -1) chain.push(fallbackLocale);
592
- if (chain.indexOf(defaultLocale) === -1) chain.push(defaultLocale);
593
- return chain;
647
+ // Delegates to the module-level pure builder; fallbackLocale /
648
+ // defaultLocale are this instance's resolved config.
649
+ return _buildLocaleChain(locale, fallbackLocale, defaultLocale);
594
650
  }
595
651
 
596
652
  function _lookupRaw(key, locale) {
@@ -932,6 +988,7 @@ var messageFormat = require("./i18n-messageformat");
932
988
 
933
989
  module.exports = {
934
990
  create: create,
991
+ localeChain: localeChain,
935
992
  messageFormat: messageFormat,
936
993
  I18nError: I18nError,
937
994
  DEFAULTS: DEFAULTS,
package/lib/iab-tcf.js CHANGED
@@ -96,6 +96,7 @@
96
96
 
97
97
  var audit = require("./audit");
98
98
  var bCrypto = require("./crypto");
99
+ var numericBounds = require("./numeric-bounds");
99
100
  var { defineClass } = require("./framework-error");
100
101
  var IabTcfError = defineClass("IabTcfError", { alwaysPermanent: true });
101
102
 
@@ -521,7 +522,7 @@ function _idArray(x) {
521
522
  var seen = Object.create(null);
522
523
  var out = [];
523
524
  list.forEach(function (id) {
524
- if (typeof id !== "number" || !isFinite(id) || id < 1 || Math.floor(id) !== id) {
525
+ if (!numericBounds.isPositiveFiniteInt(id)) {
525
526
  throw IabTcfError.factory("iab-tcf/bad-value", "iabTcf.encode: vendor/purpose ids must be positive integers, got " + id);
526
527
  }
527
528
  if (!seen[id]) { seen[id] = 1; out.push(id); }
@@ -554,7 +555,7 @@ function _decisec(t) {
554
555
  function _bitWriter() {
555
556
  var bits = "";
556
557
  function writeInt(v, n) {
557
- if (typeof v !== "number" || !isFinite(v) || v < 0 || Math.floor(v) !== v) throw IabTcfError.factory("iab-tcf/bad-value", "iabTcf.encode: expected a non-negative integer, got " + v);
558
+ if (!numericBounds.isNonNegativeFiniteInt(v)) throw IabTcfError.factory("iab-tcf/bad-value", "iabTcf.encode: expected a non-negative integer, got " + v);
558
559
  if (v >= Math.pow(2, n)) throw IabTcfError.factory("iab-tcf/value-overflow", "iabTcf.encode: " + v + " does not fit in " + n + " bits");
559
560
  bits += v.toString(2).padStart(n, "0");
560
561
  }