@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
@@ -42,6 +42,20 @@ function shape(value) {
42
42
  return (typeof value) + " " + String(value);
43
43
  }
44
44
 
45
+ // _throwInt — construct + throw a code-first defineClass error. When no
46
+ // errorOpts is supplied the call is the historical bare `new ErrorClass(
47
+ // code, message)` (byte-identical for every existing caller); pass
48
+ // errorOpts { permanent, statusCode } to forward the framework error's
49
+ // non-retryable flag / HTTP status (a config opt that's wrong on retry
50
+ // stays wrong; a request-shaped opt carries its 4xx). Message-FIRST
51
+ // classes (AtomicFileError / SafeBufferError / SafeUrlError) must NOT use
52
+ // the require* throwers — they use the isPositiveFiniteInt predicate + their
53
+ // own throw (see the module header) so code/message don't swap.
54
+ function _throwInt(errorClass, code, message, errorOpts) {
55
+ if (errorOpts) throw new errorClass(code, message, errorOpts.permanent, errorOpts.statusCode);
56
+ throw new errorClass(code, message);
57
+ }
58
+
45
59
  function isPositiveFiniteInt(value) {
46
60
  return typeof value === "number" && Number.isFinite(value) &&
47
61
  Number.isInteger(value) && value > 0;
@@ -52,6 +66,14 @@ function isNonNegativeFiniteInt(value) {
52
66
  Number.isInteger(value) && value >= 0;
53
67
  }
54
68
 
69
+ // Any-sign finite integer (no sign bound) — for callers that only require
70
+ // integrality (e.g. a CloudEvents 32-bit signed-integer extension, a DNS
71
+ // algorithm/digest-type code) and reject only non-numbers / floats / Infinity /
72
+ // NaN. Distinct from isNonNegativeFiniteInt (which also forbids negatives).
73
+ function isFiniteInt(value) {
74
+ return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value);
75
+ }
76
+
55
77
  // requirePositiveFiniteIntIfPresent / requireNonNegativeFiniteIntIfPresent —
56
78
  // optional-shape gates that throw via the caller's framework-error class
57
79
  // when the value is present but invalid. Replaces the per-file
@@ -64,11 +86,11 @@ function isNonNegativeFiniteInt(value) {
64
86
  // nb.requirePositiveFiniteIntIfPresent(opts.graceMs,
65
87
  // "app-shutdown.create: opts.graceMs", AppShutdownError,
66
88
  // "app-shutdown/bad-grace-ms");
67
- function requirePositiveFiniteIntIfPresent(value, label, errorClass, code) {
89
+ function requirePositiveFiniteIntIfPresent(value, label, errorClass, code, errorOpts) {
68
90
  if (value === undefined) return value;
69
91
  if (!isPositiveFiniteInt(value)) {
70
- throw new errorClass(code,
71
- (label || "value") + " must be a positive finite integer; got " + shape(value));
92
+ _throwInt(errorClass, code,
93
+ (label || "value") + " must be a positive finite integer; got " + shape(value), errorOpts);
72
94
  }
73
95
  return value;
74
96
  }
@@ -102,13 +124,13 @@ function _rangeSuffix(range) {
102
124
  if (range.min != null) return " >= " + range.min;
103
125
  return "";
104
126
  }
105
- function requirePositiveFiniteInt(value, label, errorClass, code, range) {
127
+ function requirePositiveFiniteInt(value, label, errorClass, code, range, errorOpts) {
106
128
  var inRange = !range ||
107
129
  ((range.min == null || value >= range.min) && (range.max == null || value <= range.max));
108
130
  if (!isPositiveFiniteInt(value) || !inRange) {
109
- throw new errorClass(code,
131
+ _throwInt(errorClass, code,
110
132
  (label || "value") + " must be a positive finite integer" +
111
- _rangeSuffix(range) + "; got " + shape(value));
133
+ _rangeSuffix(range) + "; got " + shape(value), errorOpts);
112
134
  }
113
135
  return value;
114
136
  }
@@ -123,12 +145,12 @@ function requirePositiveFiniteInt(value, label, errorClass, code, range) {
123
145
  // nb.requireAllPositiveFiniteIntIfPresent(opts,
124
146
  // ["maxBytes", "maxAttrValueBytes", "maxTagDepth", "maxAttrsPerTag"],
125
147
  // "guardHtml.validate", GuardHtmlError, "html.bad-opt");
126
- function requireAllPositiveFiniteIntIfPresent(opts, names, labelPrefix, errorClass, code) {
148
+ function requireAllPositiveFiniteIntIfPresent(opts, names, labelPrefix, errorClass, code, errorOpts) {
127
149
  if (!opts || !Array.isArray(names)) return;
128
150
  for (var i = 0; i < names.length; i += 1) {
129
151
  var n = names[i];
130
152
  requirePositiveFiniteIntIfPresent(opts[n],
131
- (labelPrefix || "") + ": " + n, errorClass, code);
153
+ (labelPrefix || "") + ": " + n, errorClass, code, errorOpts);
132
154
  }
133
155
  }
134
156
 
@@ -136,6 +158,7 @@ module.exports = {
136
158
  shape: shape,
137
159
  isPositiveFiniteInt: isPositiveFiniteInt,
138
160
  isNonNegativeFiniteInt: isNonNegativeFiniteInt,
161
+ isFiniteInt: isFiniteInt,
139
162
  requirePositiveFiniteInt: requirePositiveFiniteInt,
140
163
  requirePositiveFiniteIntIfPresent: requirePositiveFiniteIntIfPresent,
141
164
  requireNonNegativeFiniteIntIfPresent: requireNonNegativeFiniteIntIfPresent,
@@ -33,6 +33,7 @@ var C = require("../constants");
33
33
  var httpClient = require("../http-client");
34
34
  var requestHelpers = require("../request-helpers");
35
35
  var safeUrl = require("../safe-url");
36
+ var markupEscape = require("../markup-escape").markupEscape;
36
37
  var { ObjectStoreError } = require("../framework-error");
37
38
 
38
39
  var _err = ObjectStoreError.factory;
@@ -122,12 +123,7 @@ function _validateCorsRule(rule, idx) {
122
123
  }
123
124
 
124
125
  function _xmlEscape(s) {
125
- return String(s)
126
- .replace(/&/g, "&amp;")
127
- .replace(/</g, "&lt;")
128
- .replace(/>/g, "&gt;")
129
- .replace(/"/g, "&quot;")
130
- .replace(/'/g, "&apos;");
126
+ return markupEscape(s, { apos: "&apos;" });
131
127
  }
132
128
 
133
129
  function _buildCorsXml(rules) {
@@ -37,7 +37,6 @@
37
37
  */
38
38
  var nodeCrypto = require("node:crypto");
39
39
  var { URL } = require("node:url");
40
- var { Readable } = require("node:stream");
41
40
  var safeXml = require("../parsers/safe-xml");
42
41
  var sharedRequest = require("./http-request");
43
42
  var sigv4 = require("./sigv4");
@@ -93,14 +92,6 @@ function _encodeBlobKey(key) {
93
92
 
94
93
  var DEFAULT_API_VERSION = "2024-08-04";
95
94
 
96
- // Service SAS expiry bounds. Azure doesn't enforce a hard max, but
97
- // matching the SigV4 / V4 7-day cap keeps semantics uniform across
98
- // backends and reflects best practice (long-lived SAS tokens are
99
- // effectively credentials).
100
- var SAS_DEFAULT_EXPIRES_SECONDS = C.TIME.minutes(15) / C.TIME.seconds(1);
101
- var SAS_MAX_EXPIRES_SECONDS = C.TIME.days(7) / C.TIME.seconds(1);
102
- var SAS_MIN_EXPIRES_SECONDS = 1;
103
-
104
95
  var _err = ObjectStoreError.factory;
105
96
 
106
97
  var _httpRequest = sharedRequest;
@@ -307,38 +298,18 @@ function create(config) {
307
298
  return getResponse(key, opts).then(function (r) { return r.body; });
308
299
  }
309
300
 
310
- function getStream(key, opts) { return Readable.from(get(key, opts)); }
301
+ function getStream(key, opts) { return sharedRequest.promiseToStream(get(key, opts)); }
311
302
 
312
303
  function getResponse(key, opts) {
313
304
  opts = opts || {};
314
305
  var url = _blobUrl(key);
315
- var extraHeaders = {};
316
- if (opts.range) {
317
- extraHeaders["x-ms-range"] = "bytes=" + opts.range.start + "-" + opts.range.end;
318
- }
319
- if (opts.ifNoneMatch) extraHeaders["If-None-Match"] = opts.ifNoneMatch;
320
- if (opts.ifMatch) extraHeaders["If-Match"] = opts.ifMatch;
321
- if (opts.ifModifiedSince) extraHeaders["If-Modified-Since"] = opts.ifModifiedSince;
322
- if (opts.ifUnmodifiedSince) extraHeaders["If-Unmodified-Since"] = opts.ifUnmodifiedSince;
306
+ var extraHeaders = sharedRequest.applyConditionalGetHeaders({}, opts, "x-ms-range");
323
307
  var headers = _signed("GET", url, extraHeaders);
324
308
  return _httpRequest("GET", url, headers, null, reqOpts).then(function (res) {
325
- return {
326
- statusCode: res.statusCode,
327
- body: res.body,
328
- etag: res.headers && res.headers.etag,
329
- lastModified: res.headers && res.headers["last-modified"]
330
- ? Date.parse(res.headers["last-modified"]) : null,
331
- contentRange: res.headers && res.headers["content-range"] || null,
332
- size: res.headers && res.headers["content-length"]
333
- ? parseInt(res.headers["content-length"], 10) : null,
334
- contentType: res.headers && res.headers["content-type"] || null,
335
- };
309
+ return sharedRequest.mapGetResponse(res);
336
310
  }, function (err) {
337
311
  if (err && err.statusCode === requestHelpers.HTTP_STATUS.NOT_MODIFIED) {
338
- return {
339
- statusCode: requestHelpers.HTTP_STATUS.NOT_MODIFIED,
340
- body: null, etag: null, lastModified: null,
341
- };
312
+ return sharedRequest.notModifiedGetResult();
342
313
  }
343
314
  throw err;
344
315
  });
@@ -348,11 +319,7 @@ function create(config) {
348
319
  var url = _blobUrl(key);
349
320
  var headers = _signed("HEAD", url, {});
350
321
  return _httpRequest("HEAD", url, headers, null, reqOpts).then(function (res) {
351
- return {
352
- size: res.headers["content-length"] ? parseInt(res.headers["content-length"], 10) : null,
353
- etag: res.headers.etag,
354
- lastModified: res.headers["last-modified"] ? Date.parse(res.headers["last-modified"]) : null,
355
- };
322
+ return sharedRequest.mapHeadResponse(res);
356
323
  });
357
324
  }
358
325
 
@@ -415,15 +382,7 @@ function create(config) {
415
382
  // "Create a service SAS". We sign with the account key (HMAC-SHA256
416
383
  // over the canonical string) and emit the token as URL query params.
417
384
  function _buildSasToken(permissions, opts) {
418
- var expiresIn = opts.expiresIn != null ? opts.expiresIn : SAS_DEFAULT_EXPIRES_SECONDS;
419
- if (typeof expiresIn !== "number" ||
420
- expiresIn < SAS_MIN_EXPIRES_SECONDS ||
421
- expiresIn > SAS_MAX_EXPIRES_SECONDS) {
422
- throw _err("INVALID_EXPIRES",
423
- "presigned URL: expiresIn must be a number of seconds between " +
424
- SAS_MIN_EXPIRES_SECONDS + " and " + SAS_MAX_EXPIRES_SECONDS +
425
- " (7 days)", true);
426
- }
385
+ var expiresIn = sharedRequest.resolvePresignExpires(opts, "presigned URL", "");
427
386
  var nowDate = opts.date || new Date();
428
387
  var expiry = new Date(nowDate.getTime() + C.TIME.seconds(expiresIn));
429
388
  // Azure accepts ISO 8601 with second precision; strip ms.
@@ -23,13 +23,13 @@
23
23
  * Auth: same service-account JSON / RSA-SHA256-signed JWT exchanged
24
24
  * for an OAuth2 access token as `lib/object-store/gcs.js`.
25
25
  */
26
- var nodeFs = require("node:fs");
27
26
  var gcs = require("./gcs");
28
27
  var authHeader = require("../auth-header");
29
28
  var httpClient = require("../http-client");
30
29
  var safeJson = require("../safe-json");
31
30
  var safeUrl = require("../safe-url");
32
31
  var C = require("../constants");
32
+ var atomicFile = require("../atomic-file");
33
33
  var requestHelpers = require("../request-helpers");
34
34
  var { ObjectStoreError } = require("../framework-error");
35
35
 
@@ -139,7 +139,9 @@ function create(config) {
139
139
  var serviceAccount = config.serviceAccount;
140
140
  if (!serviceAccount && config.serviceAccountFile) {
141
141
  try {
142
- serviceAccount = safeJson.parse(nodeFs.readFileSync(config.serviceAccountFile));
142
+ // Cap + fd-bound read of the GCS service-account JSON (private_key). NO
143
+ // refuseSymlink: commonly a k8s projected-secret mount (symlink).
144
+ serviceAccount = safeJson.parse(atomicFile.fdSafeReadSync(config.serviceAccountFile, { maxBytes: C.BYTES.kib(64), encoding: "utf8" }));
143
145
  } catch (e) {
144
146
  throw _err("BAD_OPT", "gcs bucketOps: failed to read serviceAccountFile '" +
145
147
  config.serviceAccountFile + "': " + ((e && e.message) || String(e)), true);
@@ -22,13 +22,11 @@
22
22
  * https://cloud.google.com/storage/docs/json_api/v1
23
23
  * https://developers.google.com/identity/protocols/oauth2/service-account
24
24
  */
25
- var nodeFs = require("node:fs");
26
25
  var nodeCrypto = require("node:crypto");
27
- var { Readable } = require("node:stream");
28
26
  var bCrypto = require("../crypto");
29
27
  var safeJson = require("../safe-json");
30
28
  var C = require("../constants");
31
- var numericBounds = require("../numeric-bounds");
29
+ var atomicFile = require("../atomic-file");
32
30
  var requestHelpers = require("../request-helpers");
33
31
  var { ObjectStoreError } = require("../framework-error");
34
32
  var safeUrl = require("../safe-url");
@@ -60,11 +58,6 @@ var GCS_V4_ALGORITHM = "GOOG4-RSA-SHA256";
60
58
  var GCS_V4_SERVICE = "storage";
61
59
  var GCS_V4_REGION = "auto";
62
60
 
63
- // Same expiry bounds as SigV4 — V4 presigning shares the spec's 7-day cap.
64
- var PRESIGN_DEFAULT_EXPIRES_SECONDS = C.TIME.minutes(15) / C.TIME.seconds(1);
65
- var PRESIGN_MAX_EXPIRES_SECONDS = C.TIME.days(7) / C.TIME.seconds(1);
66
- var PRESIGN_MIN_EXPIRES_SECONDS = 1;
67
-
68
61
  var SERVICE_ACCOUNT_SCHEMA = {
69
62
  type: "object",
70
63
  required: ["client_email", "private_key"],
@@ -121,7 +114,10 @@ function create(config) {
121
114
  var serviceAccount = config.serviceAccount;
122
115
  if (!serviceAccount && config.serviceAccountFile) {
123
116
  try {
124
- serviceAccount = safeJson.parse(nodeFs.readFileSync(config.serviceAccountFile), { schema: SERVICE_ACCOUNT_SCHEMA });
117
+ // Cap + fd-bound read of the GCS service-account JSON (contains the
118
+ // private_key). NO refuseSymlink: the SA file is commonly a k8s
119
+ // projected-secret mount (symlink). 64 KiB bounds the uncapped read.
120
+ serviceAccount = safeJson.parse(atomicFile.fdSafeReadSync(config.serviceAccountFile, { maxBytes: C.BYTES.kib(64), encoding: "utf8" }), { schema: SERVICE_ACCOUNT_SCHEMA });
125
121
  } catch (e) {
126
122
  throw new Error("gcs: failed to read serviceAccountFile '" + config.serviceAccountFile + "': " + e.message);
127
123
  }
@@ -230,7 +226,7 @@ function create(config) {
230
226
  }
231
227
 
232
228
  function getStream(key, opts) {
233
- return Readable.from(get(key, opts));
229
+ return sharedRequest.promiseToStream(get(key, opts));
234
230
  }
235
231
 
236
232
  async function getResponse(key, opts) {
@@ -238,32 +234,13 @@ function create(config) {
238
234
  var token = await _ensureToken();
239
235
  var url = _objectUrl(key, { alt: "media" });
240
236
  var headers = authHeader.bearer(token);
241
- if (opts.range) {
242
- headers["Range"] = "bytes=" + opts.range.start + "-" + opts.range.end;
243
- }
244
- if (opts.ifNoneMatch) headers["If-None-Match"] = opts.ifNoneMatch;
245
- if (opts.ifMatch) headers["If-Match"] = opts.ifMatch;
246
- if (opts.ifModifiedSince) headers["If-Modified-Since"] = opts.ifModifiedSince;
247
- if (opts.ifUnmodifiedSince) headers["If-Unmodified-Since"] = opts.ifUnmodifiedSince;
237
+ sharedRequest.applyConditionalGetHeaders(headers, opts, "Range");
248
238
  try {
249
239
  var res = await _httpRequest("GET", url, headers, null, reqOpts);
250
- return {
251
- statusCode: res.statusCode,
252
- body: res.body,
253
- etag: res.headers && res.headers.etag,
254
- lastModified: res.headers && res.headers["last-modified"]
255
- ? Date.parse(res.headers["last-modified"]) : null,
256
- contentRange: res.headers && res.headers["content-range"] || null,
257
- size: res.headers && res.headers["content-length"]
258
- ? parseInt(res.headers["content-length"], 10) : null,
259
- contentType: res.headers && res.headers["content-type"] || null,
260
- };
240
+ return sharedRequest.mapGetResponse(res);
261
241
  } catch (err) {
262
242
  if (err && err.statusCode === requestHelpers.HTTP_STATUS.NOT_MODIFIED) {
263
- return {
264
- statusCode: requestHelpers.HTTP_STATUS.NOT_MODIFIED,
265
- body: null, etag: null, lastModified: null,
266
- };
243
+ return sharedRequest.notModifiedGetResult();
267
244
  }
268
245
  throw err;
269
246
  }
@@ -326,21 +303,8 @@ function create(config) {
326
303
 
327
304
  function _v4Presign(method, opts) {
328
305
  opts = opts || {};
329
- if (!opts.key || typeof opts.key !== "string") {
330
- throw _err("INVALID_KEY", "presigned URL: key is required", true);
331
- }
332
- if (opts.key.indexOf("\0") !== -1) {
333
- throw _err("INVALID_KEY", "null byte in key", true);
334
- }
335
- var expiresIn = opts.expiresIn != null ? opts.expiresIn : PRESIGN_DEFAULT_EXPIRES_SECONDS;
336
- if (typeof expiresIn !== "number" ||
337
- expiresIn < PRESIGN_MIN_EXPIRES_SECONDS ||
338
- expiresIn > PRESIGN_MAX_EXPIRES_SECONDS) {
339
- throw _err("INVALID_EXPIRES",
340
- "presigned URL: expiresIn must be a number of seconds between " +
341
- PRESIGN_MIN_EXPIRES_SECONDS + " and " + PRESIGN_MAX_EXPIRES_SECONDS +
342
- " (7 days, V4 hard cap)", true);
343
- }
306
+ sharedRequest.requirePresignKey(opts, "presigned URL");
307
+ var expiresIn = sharedRequest.resolvePresignExpires(opts, "presigned URL", "V4");
344
308
 
345
309
  // V4 presigned URLs target the XML API path-style endpoint
346
310
  // (storage.googleapis.com/{bucket}/{key}), not the JSON API used
@@ -418,34 +382,9 @@ function create(config) {
418
382
  // declared range get a 403 from the bucket itself.
419
383
  function presignedUploadPolicy(opts) {
420
384
  opts = opts || {};
421
- if (!opts.key || typeof opts.key !== "string") {
422
- throw _err("INVALID_KEY", "presignedUploadPolicy: key is required", true);
423
- }
424
- if (opts.key.indexOf("\0") !== -1) {
425
- throw _err("INVALID_KEY", "null byte in key", true);
426
- }
427
- if (typeof opts.maxBytes !== "number" || !Number.isFinite(opts.maxBytes) ||
428
- opts.maxBytes <= 0) {
429
- throw _err("INVALID_MAX_BYTES",
430
- "presignedUploadPolicy: maxBytes (positive number of bytes) is required — " +
431
- "POST-form policy enforces body size via the content-length-range condition; " +
432
- "use presignedUploadUrl if size enforcement is not needed", true);
433
- }
434
- if (opts.minBytes !== undefined && !numericBounds.isNonNegativeFiniteInt(opts.minBytes)) {
435
- throw _err("INVALID_MIN_BYTES",
436
- "presignedUploadPolicy: minBytes must be a non-negative finite integer; got " +
437
- numericBounds.shape(opts.minBytes), true);
438
- }
439
- var minBytes = opts.minBytes !== undefined ? opts.minBytes : 0;
440
- var expiresIn = opts.expiresIn != null ? opts.expiresIn : PRESIGN_DEFAULT_EXPIRES_SECONDS;
441
- if (typeof expiresIn !== "number" ||
442
- expiresIn < PRESIGN_MIN_EXPIRES_SECONDS ||
443
- expiresIn > PRESIGN_MAX_EXPIRES_SECONDS) {
444
- throw _err("INVALID_EXPIRES",
445
- "presignedUploadPolicy: expiresIn must be a number of seconds between " +
446
- PRESIGN_MIN_EXPIRES_SECONDS + " and " + PRESIGN_MAX_EXPIRES_SECONDS +
447
- " (7 days, V4 hard cap)", true);
448
- }
385
+ sharedRequest.requirePresignKey(opts, "presignedUploadPolicy");
386
+ var minBytes = sharedRequest.resolvePresignUploadMinBytes(opts);
387
+ var expiresIn = sharedRequest.resolvePresignExpires(opts, "presignedUploadPolicy", "V4");
449
388
 
450
389
  var date = opts.date || new Date();
451
390
  var amzDate = sigv4.formatAmzDate(date);
@@ -16,7 +16,6 @@
16
16
  * Errors are surfaced as object-store errors with statusCode set so the
17
17
  * retry layer can classify retryable vs permanent.
18
18
  */
19
- var { Readable } = require("node:stream");
20
19
  var { ObjectStoreError } = require("../framework-error");
21
20
  var safeUrl = require("../safe-url");
22
21
  var sharedRequest = require("./http-request");
@@ -96,17 +95,13 @@ function create(config) {
96
95
  // https that doesn't buffer; return a Readable wrapping the buffered
97
96
  // body. Backends that support chunked transfer (e.g. SigV4) implement
98
97
  // a real streaming getStream in their own adapter.
99
- return Readable.from(get(key));
98
+ return sharedRequest.promiseToStream(get(key));
100
99
  }
101
100
 
102
101
  function head(key) {
103
102
  var url = _keyToUrl(baseUrl, key);
104
103
  return _request("HEAD", url, null, headers, reqOpts).then(function (res) {
105
- return {
106
- size: res.headers["content-length"] ? parseInt(res.headers["content-length"], 10) : null,
107
- etag: res.headers.etag,
108
- lastModified: res.headers["last-modified"] ? Date.parse(res.headers["last-modified"]) : null,
109
- };
104
+ return sharedRequest.mapHeadResponse(res);
110
105
  });
111
106
  }
112
107
 
@@ -16,9 +16,19 @@
16
16
  * different class pass `errorClass: SomeError` in opts.
17
17
  */
18
18
 
19
+ var { Readable } = require("node:stream");
19
20
  var httpClient = require("../http-client");
21
+ var C = require("../constants");
22
+ var numericBounds = require("../numeric-bounds");
23
+ var requestHelpers = require("../request-helpers");
20
24
  var { ObjectStoreError } = require("../framework-error");
21
25
 
26
+ // Same code-first error factory the object-store backends use; the bare
27
+ // cloud-provider-style codes (INVALID_KEY / INVALID_EXPIRES / ...) are the
28
+ // existing object-store convention, swept to namespace/kebab-case for the
29
+ // whole family in the v1.0 error-code pass.
30
+ var _err = ObjectStoreError.factory;
31
+
22
32
  function request(method, url, headers, body, opts) {
23
33
  opts = opts || {};
24
34
  var req = {
@@ -35,4 +45,151 @@ function request(method, url, headers, body, opts) {
35
45
  return httpClient.request(req);
36
46
  }
37
47
 
48
+ // ---- Shared presign opts validation (gcs / sigv4) ----
49
+ // Every presigning entry point (presigned GET/PUT URLs and the POST-form
50
+ // upload policy) opened with the same key + expiry bounds checks, varying
51
+ // only in the message prefix and the signature-version label in the
52
+ // hard-cap text. Centralized so the bounds are enforced identically and a
53
+ // future change (a new cap, a different default) is one edit.
54
+ var PRESIGN_DEFAULT_EXPIRES_SECONDS = C.TIME.minutes(15) / C.TIME.seconds(1); // 15 minutes
55
+ var PRESIGN_MAX_EXPIRES_SECONDS = C.TIME.days(7) / C.TIME.seconds(1); // 7 days
56
+ var PRESIGN_MIN_EXPIRES_SECONDS = 1;
57
+
58
+ function requirePresignKey(opts, msgPrefix) {
59
+ if (!opts.key || typeof opts.key !== "string") {
60
+ throw _err("INVALID_KEY", msgPrefix + ": key is required", true);
61
+ }
62
+ if (opts.key.indexOf("\0") !== -1) {
63
+ throw _err("INVALID_KEY", "null byte in key", true);
64
+ }
65
+ }
66
+
67
+ // hardCapLabel names the signing version in the cap text ("V4" / "SigV4"
68
+ // → "(7 days, V4 hard cap)"); an empty / omitted label yields the plain
69
+ // "(7 days)" tail used by the Azure SAS path, whose cap is the same 7 days
70
+ // but is not a SigV4 family limit.
71
+ function resolvePresignExpires(opts, msgPrefix, hardCapLabel) {
72
+ var expiresIn = opts.expiresIn != null ? opts.expiresIn : PRESIGN_DEFAULT_EXPIRES_SECONDS;
73
+ if (typeof expiresIn !== "number" ||
74
+ expiresIn < PRESIGN_MIN_EXPIRES_SECONDS ||
75
+ expiresIn > PRESIGN_MAX_EXPIRES_SECONDS) {
76
+ var capTail = hardCapLabel ? " (7 days, " + hardCapLabel + " hard cap)" : " (7 days)";
77
+ throw _err("INVALID_EXPIRES",
78
+ msgPrefix + ": expiresIn must be a number of seconds between " +
79
+ PRESIGN_MIN_EXPIRES_SECONDS + " and " + PRESIGN_MAX_EXPIRES_SECONDS +
80
+ capTail, true);
81
+ }
82
+ return expiresIn;
83
+ }
84
+
85
+ // POST-form upload policy size bounds — returns the resolved minBytes
86
+ // (defaulting to 0). maxBytes is validated here but consumed by the
87
+ // caller (it becomes the content-length-range upper bound).
88
+ function resolvePresignUploadMinBytes(opts) {
89
+ if (typeof opts.maxBytes !== "number" || !Number.isFinite(opts.maxBytes) ||
90
+ opts.maxBytes <= 0) {
91
+ throw _err("INVALID_MAX_BYTES",
92
+ "presignedUploadPolicy: maxBytes (positive number of bytes) is required — " +
93
+ "POST-form policy enforces body size via the content-length-range condition; " +
94
+ "use presignedUploadUrl if size enforcement is not needed", true);
95
+ }
96
+ if (opts.minBytes !== undefined && !numericBounds.isNonNegativeFiniteInt(opts.minBytes)) {
97
+ throw _err("INVALID_MIN_BYTES",
98
+ "presignedUploadPolicy: minBytes must be a non-negative finite integer; got " +
99
+ numericBounds.shape(opts.minBytes), true);
100
+ }
101
+ return opts.minBytes !== undefined ? opts.minBytes : 0;
102
+ }
103
+
104
+ // ---- Shared conditional-GET request + response mapping (gcs / sigv4 / azure) ----
105
+ // Every backend's getResponse() set the same RFC 7232/7233 conditional
106
+ // headers (Range + If-Match family), mapped the HTTP response into the same
107
+ // { statusCode, body, etag, lastModified, contentRange, size, contentType }
108
+ // shape, and turned a 304 into the same short-circuit result. Only the Range
109
+ // header NAME differs (S3/GCS "Range" vs Azure "x-ms-range"), so it is a
110
+ // parameter; the rest is identical.
111
+ function applyConditionalGetHeaders(target, opts, rangeHeaderName) {
112
+ if (opts.range) {
113
+ // The documented + shipped contract is an array [start, end] (see
114
+ // b.archive.adapters.objectStore and b.backup). Reading .start/.end off an
115
+ // array yields undefined → "bytes=undefined-undefined", which every store
116
+ // IGNORES — silently returning the FULL object instead of the byte range
117
+ // (over-fetch + wrong data for a partial read). Accept the array form (and
118
+ // {start,end} for compatibility) and validate, so a malformed range fails
119
+ // loudly rather than emitting a garbage header.
120
+ var r = opts.range;
121
+ var start = Array.isArray(r) ? r[0] : r.start;
122
+ var end = Array.isArray(r) ? r[1] : r.end;
123
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start) {
124
+ throw _err("INVALID_RANGE",
125
+ "range must be [start, end] (or { start, end }) with 0 <= start <= end, got " +
126
+ JSON.stringify(r), true);
127
+ }
128
+ target[rangeHeaderName] = "bytes=" + start + "-" + end;
129
+ }
130
+ if (opts.ifNoneMatch) target["If-None-Match"] = opts.ifNoneMatch;
131
+ if (opts.ifMatch) target["If-Match"] = opts.ifMatch;
132
+ if (opts.ifModifiedSince) target["If-Modified-Since"] = opts.ifModifiedSince;
133
+ if (opts.ifUnmodifiedSince) target["If-Unmodified-Since"] = opts.ifUnmodifiedSince;
134
+ return target;
135
+ }
136
+
137
+ function mapGetResponse(res) {
138
+ return {
139
+ statusCode: res.statusCode,
140
+ body: res.body,
141
+ etag: res.headers && res.headers.etag,
142
+ lastModified: res.headers && res.headers["last-modified"]
143
+ ? Date.parse(res.headers["last-modified"]) : null,
144
+ contentRange: res.headers && res.headers["content-range"] || null,
145
+ size: res.headers && res.headers["content-length"]
146
+ ? parseInt(res.headers["content-length"], 10) : null,
147
+ contentType: res.headers && res.headers["content-type"] || null,
148
+ };
149
+ }
150
+
151
+ // HEAD response projection — { size, etag, lastModified } from the response
152
+ // headers. Shared by the header-driven backends (azure / sigv4 / http-put);
153
+ // gcs.head is NOT a member (it reads a JSON metadata body, not headers).
154
+ function mapHeadResponse(res) {
155
+ return {
156
+ size: res.headers["content-length"] ? parseInt(res.headers["content-length"], 10) : null,
157
+ etag: res.headers.etag,
158
+ lastModified: res.headers["last-modified"] ? Date.parse(res.headers["last-modified"]) : null,
159
+ };
160
+ }
161
+
162
+ // The structured result a conditional GET returns when the server answers
163
+ // 304 Not Modified (httpClient surfaces it as a non-2xx error each backend
164
+ // catches and maps here).
165
+ function notModifiedGetResult() {
166
+ return {
167
+ statusCode: requestHelpers.HTTP_STATUS.NOT_MODIFIED,
168
+ body: null, etag: null, lastModified: null,
169
+ };
170
+ }
171
+
172
+ // Wrap a Promise<Buffer|string> as a Readable WITHOUT awaiting it first, so a
173
+ // backend's getStream() stays synchronous (the dispatcher hands the returned
174
+ // Readable straight to the consumer). A bare `Readable.from(promise)` throws
175
+ // ERR_INVALID_ARG_TYPE — Readable.from needs an (async-)iterable, not a Promise
176
+ // — which broke getStream on every remote backend. The async generator defers
177
+ // the await to the first read; a rejection surfaces as the stream's 'error'
178
+ // event, matching the dispatcher's "the Readable surfaces its own errors"
179
+ // contract.
180
+ function promiseToStream(promise) {
181
+ return Readable.from((async function* () { yield await promise; })());
182
+ }
183
+
38
184
  module.exports = request;
185
+ module.exports.applyConditionalGetHeaders = applyConditionalGetHeaders;
186
+ module.exports.promiseToStream = promiseToStream;
187
+ module.exports.mapGetResponse = mapGetResponse;
188
+ module.exports.mapHeadResponse = mapHeadResponse;
189
+ module.exports.notModifiedGetResult = notModifiedGetResult;
190
+ module.exports.PRESIGN_DEFAULT_EXPIRES_SECONDS = PRESIGN_DEFAULT_EXPIRES_SECONDS;
191
+ module.exports.PRESIGN_MAX_EXPIRES_SECONDS = PRESIGN_MAX_EXPIRES_SECONDS;
192
+ module.exports.PRESIGN_MIN_EXPIRES_SECONDS = PRESIGN_MIN_EXPIRES_SECONDS;
193
+ module.exports.requirePresignKey = requirePresignKey;
194
+ module.exports.resolvePresignExpires = resolvePresignExpires;
195
+ module.exports.resolvePresignUploadMinBytes = resolvePresignUploadMinBytes;
@@ -14,6 +14,7 @@
14
14
  var nodeFs = require("node:fs");
15
15
  var nodePath = require("node:path");
16
16
  var atomicFile = require("../atomic-file");
17
+ var C = require("../constants");
17
18
  var cluster = require("../cluster");
18
19
  var { ObjectStoreError } = require("../framework-error");
19
20
 
@@ -54,16 +55,14 @@ function create(config) {
54
55
  return Promise.resolve({ size: body.length });
55
56
  }
56
57
  if (body && typeof body.pipe === "function") {
57
- // Streaming put — pipe directly to disk
58
- return new Promise(function (resolve, reject) {
59
- var ws = nodeFs.createWriteStream(full);
60
- var bytes = 0;
61
- body.on("data", function (chunk) { bytes += chunk.length; });
62
- body.pipe(ws);
63
- ws.on("finish", function () { resolve({ size: bytes }); });
64
- ws.on("error", reject);
65
- body.on("error", reject);
66
- });
58
+ // Streaming put — stage into a no-follow exclusive temp + atomic rename
59
+ // (a bare createWriteStream(full) follows a symlink planted at `full` and
60
+ // leaves a half-written object there if the source aborts). maxBytes is
61
+ // raised to the object-store ceiling; default 64 MiB is too small here.
62
+ return atomicFile.writeStream(full, body, {
63
+ fileMode: 0o600,
64
+ maxBytes: C.BYTES.gib(64),
65
+ }).then(function (r) { return { size: r.bytesWritten }; });
67
66
  }
68
67
  if (typeof body === "string") {
69
68
  var buf = Buffer.from(body, "utf8");
@@ -75,18 +74,39 @@ function create(config) {
75
74
 
76
75
  function get(key) {
77
76
  var full = _resolveSafe(rootDir, key);
78
- if (!nodeFs.existsSync(full)) {
79
- return Promise.reject(_err("NOT_FOUND", "key not found: " + key, true));
80
- }
81
- return Promise.resolve(nodeFs.readFileSync(full));
77
+ // One capped fd-bound read (no existsSync check-then-read TOCTOU): get()
78
+ // buffers the whole object, so an uncapped read of a multi-GiB object is an
79
+ // OOM lever — cap it (large reads should use getStream). refuseSymlink stays
80
+ // OFF: operators may legitimately symlink into the store, and _resolveSafe
81
+ // already confines the key to rootDir.
82
+ try {
83
+ return Promise.resolve(atomicFile.fdSafeReadSync(full, {
84
+ maxBytes: C.BYTES.mib(64),
85
+ errorFor: function (kind, detail) {
86
+ if (kind === "enoent") return _err("NOT_FOUND", "key not found: " + key, true);
87
+ if (kind === "too-large") {
88
+ return _err("OBJECT_TOO_LARGE", "object " + key + " exceeds the buffered-get read cap (" +
89
+ detail.size + " > " + detail.max + " bytes) — use getStream()", true);
90
+ }
91
+ return _err("READ_FAILED", "failed to read " + key, true);
92
+ },
93
+ }));
94
+ } catch (e) { return Promise.reject(e); }
82
95
  }
83
96
 
84
97
  function getStream(key) {
85
98
  var full = _resolveSafe(rootDir, key);
86
- if (!nodeFs.existsSync(full)) {
87
- throw _err("NOT_FOUND", "key not found: " + key, true);
99
+ // Open once and stream from that fd, mapping ENOENT to NOT_FOUND, so the
100
+ // existsSync→createReadStream check-then-read window is collapsed. Plain
101
+ // O_RDONLY (follows symlinks): operators may symlink into the store, and the
102
+ // key is already confined by _resolveSafe.
103
+ var fd;
104
+ try { fd = nodeFs.openSync(full, "r"); }
105
+ catch (e) {
106
+ if (e && e.code === "ENOENT") throw _err("NOT_FOUND", "key not found: " + key, true);
107
+ throw e;
88
108
  }
89
- return nodeFs.createReadStream(full);
109
+ return nodeFs.createReadStream(full, { fd: fd });
90
110
  }
91
111
 
92
112
  function head(key) {