@backblaze-labs/b2-sdk 0.2.0 → 0.3.0

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 (460) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/README.md +20 -2
  3. package/dist/_virtual/_b2-sdk-version-json.cjs +1 -1
  4. package/dist/_virtual/_b2-sdk-version-json.js +1 -1
  5. package/dist/auth/file.cjs.map +1 -1
  6. package/dist/backup/client.cjs +206 -0
  7. package/dist/backup/client.cjs.map +1 -0
  8. package/dist/backup/client.d.cts +238 -0
  9. package/dist/backup/client.d.cts.map +1 -0
  10. package/dist/backup/client.d.ts +238 -0
  11. package/dist/backup/client.d.ts.map +1 -0
  12. package/dist/backup/client.js +206 -0
  13. package/dist/backup/client.js.map +1 -0
  14. package/dist/backup/index.cjs +15 -0
  15. package/dist/backup/index.d.cts +22 -0
  16. package/dist/backup/index.d.cts.map +1 -0
  17. package/dist/backup/index.d.ts +22 -0
  18. package/dist/backup/index.d.ts.map +1 -0
  19. package/dist/backup/index.js +7 -0
  20. package/dist/backup/raw.cjs +146 -0
  21. package/dist/backup/raw.cjs.map +1 -0
  22. package/dist/backup/raw.d.cts +122 -0
  23. package/dist/backup/raw.d.cts.map +1 -0
  24. package/dist/backup/raw.d.ts +122 -0
  25. package/dist/backup/raw.d.ts.map +1 -0
  26. package/dist/backup/raw.js +146 -0
  27. package/dist/backup/raw.js.map +1 -0
  28. package/dist/bucket.cjs +2 -1
  29. package/dist/bucket.cjs.map +1 -1
  30. package/dist/bucket.js +2 -1
  31. package/dist/bucket.js.map +1 -1
  32. package/dist/copy/large.cjs +5 -3
  33. package/dist/copy/large.cjs.map +1 -1
  34. package/dist/copy/large.d.cts.map +1 -1
  35. package/dist/copy/large.d.ts.map +1 -1
  36. package/dist/copy/large.js +5 -3
  37. package/dist/copy/large.js.map +1 -1
  38. package/dist/download/checksum.cjs +6 -2
  39. package/dist/download/checksum.cjs.map +1 -1
  40. package/dist/download/checksum.js +6 -2
  41. package/dist/download/checksum.js.map +1 -1
  42. package/dist/download/parallel.cjs +52 -15
  43. package/dist/download/parallel.cjs.map +1 -1
  44. package/dist/download/parallel.d.cts +8 -1
  45. package/dist/download/parallel.d.cts.map +1 -1
  46. package/dist/download/parallel.d.ts +8 -1
  47. package/dist/download/parallel.d.ts.map +1 -1
  48. package/dist/download/parallel.js +52 -15
  49. package/dist/download/parallel.js.map +1 -1
  50. package/dist/download/single.cjs +141 -6
  51. package/dist/download/single.cjs.map +1 -1
  52. package/dist/download/single.d.cts.map +1 -1
  53. package/dist/download/single.d.ts.map +1 -1
  54. package/dist/download/single.js +141 -6
  55. package/dist/download/single.js.map +1 -1
  56. package/dist/errors/index.cjs +173 -3
  57. package/dist/errors/index.cjs.map +1 -1
  58. package/dist/errors/index.d.cts +119 -0
  59. package/dist/errors/index.d.cts.map +1 -1
  60. package/dist/errors/index.d.ts +119 -0
  61. package/dist/errors/index.d.ts.map +1 -1
  62. package/dist/errors/index.js +161 -4
  63. package/dist/errors/index.js.map +1 -1
  64. package/dist/http/retry.cjs +1 -1
  65. package/dist/http/retry.cjs.map +1 -1
  66. package/dist/http/retry.js +1 -1
  67. package/dist/http/retry.js.map +1 -1
  68. package/dist/http/transport.cjs +61 -23
  69. package/dist/http/transport.cjs.map +1 -1
  70. package/dist/http/transport.d.cts +25 -3
  71. package/dist/http/transport.d.cts.map +1 -1
  72. package/dist/http/transport.d.ts +25 -3
  73. package/dist/http/transport.d.ts.map +1 -1
  74. package/dist/http/transport.js +59 -22
  75. package/dist/http/transport.js.map +1 -1
  76. package/dist/index.cjs +25 -2
  77. package/dist/index.d.cts +6 -4
  78. package/dist/index.d.cts.map +1 -1
  79. package/dist/index.d.ts +6 -4
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +8 -6
  82. package/dist/object.cjs +1 -1
  83. package/dist/object.cjs.map +1 -1
  84. package/dist/object.d.cts +4 -2
  85. package/dist/object.d.cts.map +1 -1
  86. package/dist/object.d.ts +4 -2
  87. package/dist/object.d.ts.map +1 -1
  88. package/dist/object.js +1 -1
  89. package/dist/object.js.map +1 -1
  90. package/dist/partner/account-info.d.cts +43 -0
  91. package/dist/partner/account-info.d.cts.map +1 -0
  92. package/dist/partner/account-info.d.ts +43 -0
  93. package/dist/partner/account-info.d.ts.map +1 -0
  94. package/dist/partner/auth-clone.cjs +64 -0
  95. package/dist/partner/auth-clone.cjs.map +1 -0
  96. package/dist/partner/auth-clone.d.cts +27 -0
  97. package/dist/partner/auth-clone.d.cts.map +1 -0
  98. package/dist/partner/auth-clone.d.ts +27 -0
  99. package/dist/partner/auth-clone.d.ts.map +1 -0
  100. package/dist/partner/auth-clone.js +63 -0
  101. package/dist/partner/auth-clone.js.map +1 -0
  102. package/dist/partner/auth-core.cjs +175 -0
  103. package/dist/partner/auth-core.cjs.map +1 -0
  104. package/dist/partner/auth-core.d.cts +111 -0
  105. package/dist/partner/auth-core.d.cts.map +1 -0
  106. package/dist/partner/auth-core.d.ts +111 -0
  107. package/dist/partner/auth-core.d.ts.map +1 -0
  108. package/dist/partner/auth-core.js +175 -0
  109. package/dist/partner/auth-core.js.map +1 -0
  110. package/dist/partner/auth-shape.cjs +36 -0
  111. package/dist/partner/auth-shape.cjs.map +1 -0
  112. package/dist/partner/auth-shape.d.cts +12 -0
  113. package/dist/partner/auth-shape.d.cts.map +1 -0
  114. package/dist/partner/auth-shape.d.ts +12 -0
  115. package/dist/partner/auth-shape.d.ts.map +1 -0
  116. package/dist/partner/auth-shape.js +36 -0
  117. package/dist/partner/auth-shape.js.map +1 -0
  118. package/dist/partner/client.cjs +255 -0
  119. package/dist/partner/client.cjs.map +1 -0
  120. package/dist/partner/client.d.cts +335 -0
  121. package/dist/partner/client.d.cts.map +1 -0
  122. package/dist/partner/client.d.ts +335 -0
  123. package/dist/partner/client.d.ts.map +1 -0
  124. package/dist/partner/client.js +255 -0
  125. package/dist/partner/client.js.map +1 -0
  126. package/dist/partner/endpoint-url.cjs +108 -0
  127. package/dist/partner/endpoint-url.cjs.map +1 -0
  128. package/dist/partner/endpoint-url.d.cts +72 -0
  129. package/dist/partner/endpoint-url.d.cts.map +1 -0
  130. package/dist/partner/endpoint-url.d.ts +72 -0
  131. package/dist/partner/endpoint-url.d.ts.map +1 -0
  132. package/dist/partner/endpoint-url.js +105 -0
  133. package/dist/partner/endpoint-url.js.map +1 -0
  134. package/dist/partner/in-memory.cjs +135 -0
  135. package/dist/partner/in-memory.cjs.map +1 -0
  136. package/dist/partner/in-memory.d.cts +99 -0
  137. package/dist/partner/in-memory.d.cts.map +1 -0
  138. package/dist/partner/in-memory.d.ts +99 -0
  139. package/dist/partner/in-memory.d.ts.map +1 -0
  140. package/dist/partner/in-memory.js +135 -0
  141. package/dist/partner/in-memory.js.map +1 -0
  142. package/dist/partner/index.cjs +18 -0
  143. package/dist/partner/index.d.cts +23 -0
  144. package/dist/partner/index.d.cts.map +1 -0
  145. package/dist/partner/index.d.ts +23 -0
  146. package/dist/partner/index.d.ts.map +1 -0
  147. package/dist/partner/index.js +7 -0
  148. package/dist/partner/raw.cjs +392 -0
  149. package/dist/partner/raw.cjs.map +1 -0
  150. package/dist/partner/raw.d.cts +223 -0
  151. package/dist/partner/raw.d.cts.map +1 -0
  152. package/dist/partner/raw.d.ts +223 -0
  153. package/dist/partner/raw.d.ts.map +1 -0
  154. package/dist/partner/raw.js +390 -0
  155. package/dist/partner/raw.js.map +1 -0
  156. package/dist/partner/redaction-placeholders.cjs +10 -0
  157. package/dist/partner/redaction-placeholders.cjs.map +1 -0
  158. package/dist/partner/redaction-placeholders.d.cts +5 -0
  159. package/dist/partner/redaction-placeholders.d.cts.map +1 -0
  160. package/dist/partner/redaction-placeholders.d.ts +5 -0
  161. package/dist/partner/redaction-placeholders.d.ts.map +1 -0
  162. package/dist/partner/redaction-placeholders.js +9 -0
  163. package/dist/partner/redaction-placeholders.js.map +1 -0
  164. package/dist/partner/redaction.cjs +267 -0
  165. package/dist/partner/redaction.cjs.map +1 -0
  166. package/dist/partner/redaction.d.cts +124 -0
  167. package/dist/partner/redaction.d.cts.map +1 -0
  168. package/dist/partner/redaction.d.ts +124 -0
  169. package/dist/partner/redaction.d.ts.map +1 -0
  170. package/dist/partner/redaction.js +257 -0
  171. package/dist/partner/redaction.js.map +1 -0
  172. package/dist/raw/index.cjs +81 -19
  173. package/dist/raw/index.cjs.map +1 -1
  174. package/dist/raw/index.d.cts +6 -2
  175. package/dist/raw/index.d.cts.map +1 -1
  176. package/dist/raw/index.d.ts +6 -2
  177. package/dist/raw/index.d.ts.map +1 -1
  178. package/dist/raw/index.js +81 -19
  179. package/dist/raw/index.js.map +1 -1
  180. package/dist/raw/url.cjs +111 -0
  181. package/dist/raw/url.cjs.map +1 -0
  182. package/dist/raw/url.d.cts +41 -0
  183. package/dist/raw/url.d.cts.map +1 -0
  184. package/dist/raw/url.d.ts +41 -0
  185. package/dist/raw/url.d.ts.map +1 -0
  186. package/dist/raw/url.js +110 -0
  187. package/dist/raw/url.js.map +1 -0
  188. package/dist/s3/sigv4.cjs +15 -10
  189. package/dist/s3/sigv4.cjs.map +1 -1
  190. package/dist/s3/sigv4.js +15 -10
  191. package/dist/s3/sigv4.js.map +1 -1
  192. package/dist/simulator/index.cjs +1197 -206
  193. package/dist/simulator/index.cjs.map +1 -1
  194. package/dist/simulator/index.d.cts +109 -11
  195. package/dist/simulator/index.d.cts.map +1 -1
  196. package/dist/simulator/index.d.ts +109 -11
  197. package/dist/simulator/index.d.ts.map +1 -1
  198. package/dist/simulator/index.js +1196 -209
  199. package/dist/simulator/index.js.map +1 -1
  200. package/dist/simulator/partner.cjs +719 -0
  201. package/dist/simulator/partner.cjs.map +1 -0
  202. package/dist/simulator/partner.d.cts +183 -0
  203. package/dist/simulator/partner.d.cts.map +1 -0
  204. package/dist/simulator/partner.d.ts +183 -0
  205. package/dist/simulator/partner.d.ts.map +1 -0
  206. package/dist/simulator/partner.js +718 -0
  207. package/dist/simulator/partner.js.map +1 -0
  208. package/dist/simulator/validation.cjs +577 -0
  209. package/dist/simulator/validation.cjs.map +1 -1
  210. package/dist/simulator/validation.d.cts +117 -0
  211. package/dist/simulator/validation.d.cts.map +1 -1
  212. package/dist/simulator/validation.d.ts +117 -0
  213. package/dist/simulator/validation.d.ts.map +1 -1
  214. package/dist/simulator/validation.js +563 -1
  215. package/dist/simulator/validation.js.map +1 -1
  216. package/dist/streams/file-source.cjs +1 -1
  217. package/dist/streams/file-source.cjs.map +1 -1
  218. package/dist/streams/file-source.js +1 -1
  219. package/dist/streams/file-source.js.map +1 -1
  220. package/dist/streams/index.cjs +1 -1
  221. package/dist/streams/index.js +1 -1
  222. package/dist/streams/source.cjs +2 -1
  223. package/dist/streams/source.cjs.map +1 -1
  224. package/dist/streams/source.js +2 -1
  225. package/dist/streams/source.js.map +1 -1
  226. package/dist/sync/b2-sha1-reader.cjs +6 -2
  227. package/dist/sync/b2-sha1-reader.cjs.map +1 -1
  228. package/dist/sync/b2-sha1-reader.d.cts.map +1 -1
  229. package/dist/sync/b2-sha1-reader.d.ts.map +1 -1
  230. package/dist/sync/b2-sha1-reader.js +6 -2
  231. package/dist/sync/b2-sha1-reader.js.map +1 -1
  232. package/dist/sync/download-staging.cjs +11 -10
  233. package/dist/sync/download-staging.cjs.map +1 -1
  234. package/dist/sync/download-staging.d.cts +2 -1
  235. package/dist/sync/download-staging.d.cts.map +1 -1
  236. package/dist/sync/download-staging.d.ts +2 -1
  237. package/dist/sync/download-staging.d.ts.map +1 -1
  238. package/dist/sync/download-staging.js +11 -10
  239. package/dist/sync/download-staging.js.map +1 -1
  240. package/dist/sync/filters.cjs.map +1 -1
  241. package/dist/sync/filters.js.map +1 -1
  242. package/dist/sync/local-file-io.cjs +11 -6
  243. package/dist/sync/local-file-io.cjs.map +1 -1
  244. package/dist/sync/local-file-io.d.cts +1 -0
  245. package/dist/sync/local-file-io.d.cts.map +1 -1
  246. package/dist/sync/local-file-io.d.ts +1 -0
  247. package/dist/sync/local-file-io.d.ts.map +1 -1
  248. package/dist/sync/local-file-io.js +11 -6
  249. package/dist/sync/local-file-io.js.map +1 -1
  250. package/dist/sync/local-sha1.cjs +6 -15
  251. package/dist/sync/local-sha1.cjs.map +1 -1
  252. package/dist/sync/local-sha1.d.cts +0 -8
  253. package/dist/sync/local-sha1.d.cts.map +1 -1
  254. package/dist/sync/local-sha1.d.ts +0 -8
  255. package/dist/sync/local-sha1.d.ts.map +1 -1
  256. package/dist/sync/local-sha1.js +7 -15
  257. package/dist/sync/local-sha1.js.map +1 -1
  258. package/dist/sync/policies/compare.cjs +5 -3
  259. package/dist/sync/policies/compare.cjs.map +1 -1
  260. package/dist/sync/policies/compare.d.cts.map +1 -1
  261. package/dist/sync/policies/compare.d.ts.map +1 -1
  262. package/dist/sync/policies/compare.js +6 -4
  263. package/dist/sync/policies/compare.js.map +1 -1
  264. package/dist/sync/policies/index.cjs +4 -10
  265. package/dist/sync/policies/index.cjs.map +1 -1
  266. package/dist/sync/policies/index.js +4 -10
  267. package/dist/sync/policies/index.js.map +1 -1
  268. package/dist/sync/scanners/b2.cjs +2 -2
  269. package/dist/sync/scanners/b2.cjs.map +1 -1
  270. package/dist/sync/scanners/b2.js +2 -2
  271. package/dist/sync/scanners/b2.js.map +1 -1
  272. package/dist/sync/scanners/local.cjs.map +1 -1
  273. package/dist/sync/scanners/local.js.map +1 -1
  274. package/dist/sync/synchronizer.cjs +3 -2
  275. package/dist/sync/synchronizer.cjs.map +1 -1
  276. package/dist/sync/synchronizer.js +3 -2
  277. package/dist/sync/synchronizer.js.map +1 -1
  278. package/dist/types/auth.cjs +4 -0
  279. package/dist/types/auth.cjs.map +1 -1
  280. package/dist/types/auth.d.cts +4 -0
  281. package/dist/types/auth.d.cts.map +1 -1
  282. package/dist/types/auth.d.ts +4 -0
  283. package/dist/types/auth.d.ts.map +1 -1
  284. package/dist/types/auth.js +4 -0
  285. package/dist/types/auth.js.map +1 -1
  286. package/dist/types/backup.d.cts +80 -0
  287. package/dist/types/backup.d.cts.map +1 -0
  288. package/dist/types/backup.d.ts +80 -0
  289. package/dist/types/backup.d.ts.map +1 -0
  290. package/dist/types/bucket.cjs.map +1 -1
  291. package/dist/types/bucket.d.cts +4 -2
  292. package/dist/types/bucket.d.cts.map +1 -1
  293. package/dist/types/bucket.d.ts +4 -2
  294. package/dist/types/bucket.d.ts.map +1 -1
  295. package/dist/types/bucket.js.map +1 -1
  296. package/dist/types/download.cjs +62 -0
  297. package/dist/types/download.cjs.map +1 -0
  298. package/dist/types/download.d.cts +98 -0
  299. package/dist/types/download.d.cts.map +1 -1
  300. package/dist/types/download.d.ts +98 -0
  301. package/dist/types/download.d.ts.map +1 -1
  302. package/dist/types/download.js +61 -0
  303. package/dist/types/download.js.map +1 -0
  304. package/dist/types/encryption.cjs +58 -229
  305. package/dist/types/encryption.cjs.map +1 -1
  306. package/dist/types/encryption.d.cts +32 -0
  307. package/dist/types/encryption.d.cts.map +1 -1
  308. package/dist/types/encryption.d.ts +32 -0
  309. package/dist/types/encryption.d.ts.map +1 -1
  310. package/dist/types/encryption.js +56 -229
  311. package/dist/types/encryption.js.map +1 -1
  312. package/dist/types/errors.cjs +9 -0
  313. package/dist/types/errors.cjs.map +1 -1
  314. package/dist/types/errors.d.cts +1 -1
  315. package/dist/types/errors.d.cts.map +1 -1
  316. package/dist/types/errors.d.ts +1 -1
  317. package/dist/types/errors.d.ts.map +1 -1
  318. package/dist/types/errors.js +9 -0
  319. package/dist/types/errors.js.map +1 -1
  320. package/dist/types/file.cjs.map +1 -1
  321. package/dist/types/file.d.cts +15 -2
  322. package/dist/types/file.d.cts.map +1 -1
  323. package/dist/types/file.d.ts +15 -2
  324. package/dist/types/file.d.ts.map +1 -1
  325. package/dist/types/file.js.map +1 -1
  326. package/dist/types/ids.cjs +30 -0
  327. package/dist/types/ids.cjs.map +1 -1
  328. package/dist/types/ids.d.cts +32 -0
  329. package/dist/types/ids.d.cts.map +1 -1
  330. package/dist/types/ids.d.ts +32 -0
  331. package/dist/types/ids.d.ts.map +1 -1
  332. package/dist/types/ids.js +28 -1
  333. package/dist/types/ids.js.map +1 -1
  334. package/dist/types/index.d.cts +10 -6
  335. package/dist/types/index.d.cts.map +1 -1
  336. package/dist/types/index.d.ts +10 -6
  337. package/dist/types/index.d.ts.map +1 -1
  338. package/dist/types/lock.cjs.map +1 -1
  339. package/dist/types/lock.d.cts +14 -0
  340. package/dist/types/lock.d.cts.map +1 -1
  341. package/dist/types/lock.d.ts +14 -0
  342. package/dist/types/lock.d.ts.map +1 -1
  343. package/dist/types/lock.js.map +1 -1
  344. package/dist/types/notifications.cjs +1 -1
  345. package/dist/types/notifications.cjs.map +1 -1
  346. package/dist/types/notifications.d.cts +5 -3
  347. package/dist/types/notifications.d.cts.map +1 -1
  348. package/dist/types/notifications.d.ts +5 -3
  349. package/dist/types/notifications.d.ts.map +1 -1
  350. package/dist/types/notifications.js +1 -1
  351. package/dist/types/notifications.js.map +1 -1
  352. package/dist/types/partner.cjs +32 -0
  353. package/dist/types/partner.cjs.map +1 -0
  354. package/dist/types/partner.d.cts +394 -0
  355. package/dist/types/partner.d.cts.map +1 -0
  356. package/dist/types/partner.d.ts +394 -0
  357. package/dist/types/partner.d.ts.map +1 -0
  358. package/dist/types/partner.js +29 -0
  359. package/dist/types/partner.js.map +1 -0
  360. package/dist/types/upload.d.cts +31 -18
  361. package/dist/types/upload.d.cts.map +1 -1
  362. package/dist/types/upload.d.ts +31 -18
  363. package/dist/types/upload.d.ts.map +1 -1
  364. package/dist/upload/abort-scope.cjs +0 -45
  365. package/dist/upload/abort-scope.cjs.map +1 -1
  366. package/dist/upload/abort-scope.d.cts +0 -22
  367. package/dist/upload/abort-scope.d.cts.map +1 -1
  368. package/dist/upload/abort-scope.d.ts +0 -22
  369. package/dist/upload/abort-scope.d.ts.map +1 -1
  370. package/dist/upload/abort-scope.js +1 -44
  371. package/dist/upload/abort-scope.js.map +1 -1
  372. package/dist/upload/finish.cjs +3 -8
  373. package/dist/upload/finish.cjs.map +1 -1
  374. package/dist/upload/finish.d.cts.map +1 -1
  375. package/dist/upload/finish.d.ts.map +1 -1
  376. package/dist/upload/finish.js +1 -6
  377. package/dist/upload/finish.js.map +1 -1
  378. package/dist/upload/large.cjs +5 -1
  379. package/dist/upload/large.cjs.map +1 -1
  380. package/dist/upload/large.d.cts +16 -2
  381. package/dist/upload/large.d.cts.map +1 -1
  382. package/dist/upload/large.d.ts +16 -2
  383. package/dist/upload/large.d.ts.map +1 -1
  384. package/dist/upload/large.js +5 -1
  385. package/dist/upload/large.js.map +1 -1
  386. package/dist/upload/options.cjs.map +1 -1
  387. package/dist/upload/options.d.cts +12 -0
  388. package/dist/upload/options.d.cts.map +1 -1
  389. package/dist/upload/options.d.ts +12 -0
  390. package/dist/upload/options.d.ts.map +1 -1
  391. package/dist/upload/options.js.map +1 -1
  392. package/dist/upload/resume.cjs +2 -1
  393. package/dist/upload/resume.cjs.map +1 -1
  394. package/dist/upload/resume.d.cts +3 -1
  395. package/dist/upload/resume.d.cts.map +1 -1
  396. package/dist/upload/resume.d.ts +3 -1
  397. package/dist/upload/resume.d.ts.map +1 -1
  398. package/dist/upload/resume.js +2 -1
  399. package/dist/upload/resume.js.map +1 -1
  400. package/dist/upload/retry.cjs +8 -3
  401. package/dist/upload/retry.cjs.map +1 -1
  402. package/dist/upload/retry.js +8 -3
  403. package/dist/upload/retry.js.map +1 -1
  404. package/dist/upload/single.cjs +2 -1
  405. package/dist/upload/single.cjs.map +1 -1
  406. package/dist/upload/single.d.cts +6 -0
  407. package/dist/upload/single.d.cts.map +1 -1
  408. package/dist/upload/single.d.ts +6 -0
  409. package/dist/upload/single.d.ts.map +1 -1
  410. package/dist/upload/single.js +2 -1
  411. package/dist/upload/single.js.map +1 -1
  412. package/dist/upload/stream.cjs +6 -4
  413. package/dist/upload/stream.cjs.map +1 -1
  414. package/dist/upload/stream.d.cts +6 -0
  415. package/dist/upload/stream.d.cts.map +1 -1
  416. package/dist/upload/stream.d.ts +6 -0
  417. package/dist/upload/stream.d.ts.map +1 -1
  418. package/dist/upload/stream.js +3 -1
  419. package/dist/upload/stream.js.map +1 -1
  420. package/dist/util/abort.cjs +114 -0
  421. package/dist/util/abort.cjs.map +1 -0
  422. package/dist/util/abort.d.cts +65 -0
  423. package/dist/util/abort.d.cts.map +1 -0
  424. package/dist/util/abort.d.ts +65 -0
  425. package/dist/util/abort.d.ts.map +1 -0
  426. package/dist/util/abort.js +109 -0
  427. package/dist/util/abort.js.map +1 -0
  428. package/dist/util/defaults.cjs +3 -0
  429. package/dist/util/defaults.cjs.map +1 -1
  430. package/dist/util/defaults.d.cts +2 -0
  431. package/dist/util/defaults.d.cts.map +1 -1
  432. package/dist/util/defaults.d.ts +2 -0
  433. package/dist/util/defaults.d.ts.map +1 -1
  434. package/dist/util/defaults.js +3 -1
  435. package/dist/util/defaults.js.map +1 -1
  436. package/dist/util/md5.cjs +211 -0
  437. package/dist/util/md5.cjs.map +1 -0
  438. package/dist/util/md5.d.cts +43 -0
  439. package/dist/util/md5.d.cts.map +1 -0
  440. package/dist/util/md5.d.ts +43 -0
  441. package/dist/util/md5.d.ts.map +1 -0
  442. package/dist/util/md5.js +209 -0
  443. package/dist/util/md5.js.map +1 -0
  444. package/dist/util/normalize.cjs +5 -5
  445. package/dist/util/normalize.cjs.map +1 -1
  446. package/dist/util/normalize.d.cts +7 -7
  447. package/dist/util/normalize.d.cts.map +1 -1
  448. package/dist/util/normalize.d.ts +7 -7
  449. package/dist/util/normalize.d.ts.map +1 -1
  450. package/dist/util/normalize.js +5 -5
  451. package/dist/util/normalize.js.map +1 -1
  452. package/dist/util/paginator.cjs +7 -1
  453. package/dist/util/paginator.cjs.map +1 -1
  454. package/dist/util/paginator.d.cts +3 -1
  455. package/dist/util/paginator.d.cts.map +1 -1
  456. package/dist/util/paginator.d.ts +3 -1
  457. package/dist/util/paginator.d.ts.map +1 -1
  458. package/dist/util/paginator.js +7 -1
  459. package/dist/util/paginator.js.map +1 -1
  460. package/package.json +38 -16
@@ -2,17 +2,83 @@ import { FileAction } from "../types/file.js";
2
2
  import { accountId, bucketId, fileId } from "../types/ids.js";
3
3
  import { utf8Decoder, utf8Encoder } from "../util/text-codec.js";
4
4
  import { encodeFileName } from "../raw/encoding.js";
5
- import { hmacSha256 } from "../util/crypto.js";
5
+ import { DownloadClientUnauthorizedToReadMarker, DownloadHeaderName } from "../types/download.js";
6
+ import { md5Base64, md5Base64Sync } from "../util/md5.js";
7
+ import { EncryptionAlgorithm, EncryptionMode } from "../types/encryption.js";
8
+ import { LegalHoldValue, RetentionMode } from "../types/lock.js";
9
+ import { hexEncode, hmacSha256 } from "../util/crypto.js";
6
10
  import { sha1Hex } from "../streams/hash.js";
7
11
  import { BucketRetentionMode } from "../types/bucket.js";
8
- import { EncryptionAlgorithm, EncryptionMode } from "../types/encryption.js";
9
12
  import { toError } from "../util/to-error.js";
13
+ import { b2Url, isB2ApiVersion } from "../raw/url.js";
10
14
  import { Capability } from "../types/auth.js";
15
+ import { PartnerSimulator, isPartnerQueryEndpoint } from "./partner.js";
11
16
  import { missingCapabilitiesFor } from "./capabilities.js";
12
17
  import { BUCKET_NAME_MAX, BUCKET_NAME_MIN, FILE_NAME_MAX_BYTES } from "../internal/b2-naming.js";
13
- import { BUCKET_INFO_MAX_KEYS, BUCKET_INFO_VALUE_MAX, FILE_INFO_TOTAL_MAX, FILE_INFO_VALUE_MAX, LIST_ENDPOINT_CAPS, validateBucketInfo, validateBucketName, validateFileInfo, validateFileName, validateMaxCount } from "./validation.js";
18
+ import { BUCKET_INFO_MAX_KEYS, BUCKET_INFO_VALUE_MAX, DOWNLOAD_AUTH_DURATION_MAX_SECONDS, DOWNLOAD_AUTH_DURATION_MIN_SECONDS, FILE_INFO_TOTAL_MAX, FILE_INFO_VALUE_MAX, KEY_NAME_MAX, KEY_NAME_MIN, LIST_ENDPOINT_CAPS, normalizeCreateKeyCapabilities, validateBucketInfo, validateBucketName, validateBucketTypes, validateCorsRules, validateCreateKeyCapabilities, validateCreateKeyName, validateDefaultRetention, validateDownloadAuthorizationDuration, validateDownloadAuthorizationPrefix, validateFileInfo, validateFileName, validateLifecycleRules, validateMaxCount, validateNotificationRules, validateReplicationConfiguration } from "./validation.js";
14
19
  //#region src/simulator/index.ts
15
- var UPLOAD_TOKEN_SIGNING_KEY = "b2-sdk-typescript-simulator-upload-token-v1";
20
+ var UPLOAD_TOKEN_SIGNING_KEY = [
21
+ "b2 sdk typescript",
22
+ "simulator upload authorization",
23
+ "v1"
24
+ ].join(":");
25
+ var SIMULATOR_MASTER_APPLICATION_KEY_ID = "master-key-id";
26
+ var SIMULATOR_MASTER_APPLICATION_KEY = "master-key";
27
+ var SIMULATOR_TEST_APPLICATION_KEY_ID = "test-key-id";
28
+ var SIMULATOR_TEST_APPLICATION_KEY = "test-key";
29
+ var SIMULATOR_APPLICATION_KEY_ID_PREFIX = "sim_key_";
30
+ var SIMULATOR_MASTER_CAPABILITIES = [
31
+ Capability.ListBuckets,
32
+ Capability.ListAllBucketNames,
33
+ Capability.ReadBuckets,
34
+ Capability.WriteBuckets,
35
+ Capability.DeleteBuckets,
36
+ Capability.ReadBucketEncryption,
37
+ Capability.WriteBucketEncryption,
38
+ Capability.ReadBucketReplications,
39
+ Capability.WriteBucketReplications,
40
+ Capability.ReadBucketNotifications,
41
+ Capability.WriteBucketNotifications,
42
+ Capability.ReadBucketLogging,
43
+ Capability.WriteBucketLogging,
44
+ Capability.ReadBucketLifecycleRules,
45
+ Capability.WriteBucketLifecycleRules,
46
+ Capability.ListFiles,
47
+ Capability.ReadFiles,
48
+ Capability.WriteFiles,
49
+ Capability.DeleteFiles,
50
+ Capability.ListKeys,
51
+ Capability.WriteKeys,
52
+ Capability.DeleteKeys,
53
+ Capability.ShareFiles
54
+ ];
55
+ function apiPathParts(path) {
56
+ const segments = path.split("/").filter((segment) => segment.length > 0);
57
+ const candidate = segments.at(-2);
58
+ return {
59
+ endpoint: segments.at(-1) ?? "",
60
+ version: candidate !== void 0 && isB2ApiVersion(candidate) ? candidate : "v3"
61
+ };
62
+ }
63
+ function parseBasicAuthorizationHeader(authzHeader) {
64
+ if (!authzHeader?.startsWith("Basic ")) return null;
65
+ const decoded = (() => {
66
+ try {
67
+ return atob(authzHeader.slice(6));
68
+ } catch {
69
+ return null;
70
+ }
71
+ })();
72
+ if (decoded === null) return null;
73
+ const idx = decoded.indexOf(":");
74
+ if (idx <= 0 || idx === decoded.length - 1) return null;
75
+ return {
76
+ applicationKeyId: decoded.slice(0, idx),
77
+ applicationKey: decoded.slice(idx + 1)
78
+ };
79
+ }
80
+ var B2_MIN_PART_NUMBER = 1;
81
+ var B2_MAX_PART_NUMBER = 1e4;
16
82
  function base64UrlEncode(bytes) {
17
83
  let binary = "";
18
84
  for (const byte of bytes) binary += String.fromCharCode(byte);
@@ -34,6 +100,11 @@ function timingSafeStringEqual(left, right) {
34
100
  for (let i = 0; i < max; i++) mismatch |= (left.charCodeAt(i) || 0) ^ (right.charCodeAt(i) || 0);
35
101
  return mismatch === 0;
36
102
  }
103
+ var MS_PER_DAY = 864e5;
104
+ var MAX_JS_DATE_MS = 864e13;
105
+ function isValidRetentionTimestamp(timestamp, now = Date.now()) {
106
+ return Number.isFinite(timestamp) && timestamp <= MAX_JS_DATE_MS && timestamp > now;
107
+ }
37
108
  /**
38
109
  * Parse an RFC 7233 `Range` header value into inclusive start/end byte
39
110
  * offsets clamped to the file size. Supports closed (`bytes=0-999`),
@@ -119,6 +190,51 @@ function compareB2FileNames(a, b) {
119
190
  if (a > b) return 1;
120
191
  return 0;
121
192
  }
193
+ var DOWNLOAD_AUTH_TOKEN_BYTES = 32;
194
+ var DOWNLOAD_AUTH_TOKEN_PREFIX = "sim_dl_auth_";
195
+ var DOWNLOAD_AUTH_PURGE_BATCH_SIZE = 128;
196
+ var DOWNLOAD_RESPONSE_OVERRIDE_PARAMS = [
197
+ "b2ContentDisposition",
198
+ "b2ContentLanguage",
199
+ "b2ContentEncoding",
200
+ "b2ContentType",
201
+ "b2CacheControl",
202
+ "b2Expires"
203
+ ];
204
+ var DOWNLOAD_RESPONSE_OVERRIDE_HEADERS = {
205
+ b2ContentDisposition: DownloadHeaderName.ContentDisposition,
206
+ b2ContentLanguage: DownloadHeaderName.ContentLanguage,
207
+ b2ContentEncoding: DownloadHeaderName.ContentEncoding,
208
+ b2ContentType: DownloadHeaderName.ContentType,
209
+ b2CacheControl: DownloadHeaderName.CacheControl,
210
+ b2Expires: DownloadHeaderName.Expires
211
+ };
212
+ function randomDownloadAuthorizationToken() {
213
+ const crypto = globalThis.crypto;
214
+ if (typeof crypto?.getRandomValues !== "function") throw new Error("Download authorization tokens require globalThis.crypto.getRandomValues");
215
+ const bytes = new Uint8Array(DOWNLOAD_AUTH_TOKEN_BYTES);
216
+ crypto.getRandomValues(bytes);
217
+ return `${DOWNLOAD_AUTH_TOKEN_PREFIX}${hexEncode(bytes)}`;
218
+ }
219
+ function pickDownloadResponseOverrides(req) {
220
+ const overrides = {};
221
+ for (const param of DOWNLOAD_RESPONSE_OVERRIDE_PARAMS) {
222
+ const value = req[param];
223
+ if (typeof value === "string") overrides[param] = value;
224
+ }
225
+ return overrides;
226
+ }
227
+ function validateDownloadResponseOverrides(req) {
228
+ for (const param of DOWNLOAD_RESPONSE_OVERRIDE_PARAMS) {
229
+ const value = req[param];
230
+ if (value === void 0 || typeof value === "string") continue;
231
+ return {
232
+ code: "bad_request",
233
+ message: `${param} must be a string`
234
+ };
235
+ }
236
+ return null;
237
+ }
122
238
  function normalizeKeyBucketIds(req) {
123
239
  return req.bucketIds === void 0 || req.bucketIds === null ? null : Object.freeze([...req.bucketIds]);
124
240
  }
@@ -128,6 +244,18 @@ function singleBucketId(bucketIds) {
128
244
  function cloneBucketIds(bucketIds) {
129
245
  return bucketIds === null ? null : [...bucketIds];
130
246
  }
247
+ function cloneCapabilities(capabilities) {
248
+ return [...capabilities];
249
+ }
250
+ function isImplicitStorageMasterCredential(credentials) {
251
+ return credentials.applicationKeyId === SIMULATOR_MASTER_APPLICATION_KEY_ID && credentials.applicationKey === SIMULATOR_MASTER_APPLICATION_KEY || credentials.applicationKeyId === SIMULATOR_TEST_APPLICATION_KEY_ID && credentials.applicationKey === SIMULATOR_TEST_APPLICATION_KEY;
252
+ }
253
+ function isImplicitPartnerMasterCredential(credentials) {
254
+ return credentials.applicationKeyId === SIMULATOR_MASTER_APPLICATION_KEY_ID && credentials.applicationKey === SIMULATOR_MASTER_APPLICATION_KEY;
255
+ }
256
+ function isSimulatorApplicationKeyId(applicationKeyId) {
257
+ return applicationKeyId.startsWith(SIMULATOR_APPLICATION_KEY_ID_PREFIX);
258
+ }
131
259
  function hasOwnField(body, field) {
132
260
  return typeof body === "object" && body !== null && Object.hasOwn(body, field);
133
261
  }
@@ -136,10 +264,148 @@ function requestStringField(body, field) {
136
264
  const value = body[field];
137
265
  return typeof value === "string" ? value : void 0;
138
266
  }
267
+ function requestRecord(body) {
268
+ return typeof body === "object" && body !== null && !Array.isArray(body) ? body : null;
269
+ }
270
+ function replicationDestinationBucketIds(body) {
271
+ const rules = requestRecord(requestRecord(requestRecord(body)?.["replicationConfiguration"])?.["asReplicationSource"])?.["replicationRules"];
272
+ if (!Array.isArray(rules)) return [];
273
+ const bucketIds = [];
274
+ for (const rule of rules) {
275
+ const destinationBucketId = requestRecord(rule)?.["destinationBucketId"];
276
+ if (typeof destinationBucketId === "string") bucketIds.push(destinationBucketId);
277
+ }
278
+ return [...new Set(bucketIds)];
279
+ }
280
+ function normalizeReplicationConfiguration(config) {
281
+ return {
282
+ asReplicationSource: config.asReplicationSource ?? null,
283
+ asReplicationDestination: config.asReplicationDestination ?? null
284
+ };
285
+ }
286
+ function validateBucketConfigurationFields(fields, options) {
287
+ if (fields.bucketInfo !== void 0) {
288
+ const infoError = validateBucketInfo(fields.bucketInfo);
289
+ if (infoError) return infoError;
290
+ }
291
+ if (fields.corsRules !== void 0) {
292
+ const corsError = validateCorsRules(fields.corsRules);
293
+ if (corsError) return corsError;
294
+ }
295
+ if (fields.lifecycleRules !== void 0) {
296
+ const lifecycleError = validateLifecycleRules(fields.lifecycleRules);
297
+ if (lifecycleError) return lifecycleError;
298
+ }
299
+ if (fields.replicationConfiguration !== void 0) {
300
+ const replicationError = validateReplicationConfiguration(fields.replicationConfiguration);
301
+ if (replicationError) return replicationError;
302
+ }
303
+ if (fields.defaultRetention !== void 0) {
304
+ const retentionError = validateDefaultRetention(fields.defaultRetention);
305
+ if (retentionError) return retentionError;
306
+ if (requestRecord(fields.defaultRetention)?.["mode"] !== BucketRetentionMode.None && !options.objectLockEnabled) return {
307
+ code: "file_lock_not_enabled",
308
+ message: "Bucket must have Object Lock enabled to set defaultRetention"
309
+ };
310
+ }
311
+ return null;
312
+ }
313
+ function nextObjectLockEnabled(current, requested) {
314
+ if (requested === void 0) return current;
315
+ if (typeof requested !== "boolean") return {
316
+ code: "bad_request",
317
+ message: "fileLockEnabled must be a boolean"
318
+ };
319
+ if (current && requested === false) return {
320
+ code: "file_lock_conflict",
321
+ message: "Object Lock cannot be disabled"
322
+ };
323
+ return current || requested;
324
+ }
325
+ function requestHeaderValue(headers, name) {
326
+ const exact = headers[name];
327
+ if (exact !== void 0) return exact;
328
+ const lowerName = name.toLowerCase();
329
+ for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === lowerName) return value;
330
+ }
331
+ function parseContentLengthHeader(headers) {
332
+ const header = requestHeaderValue(headers, "content-length");
333
+ if (header === void 0) return { kind: "absent" };
334
+ if (!/^\d+$/.test(header)) return {
335
+ kind: "error",
336
+ message: `Content-Length must be a byte count: ${header}`
337
+ };
338
+ const expectedLength = Number(header);
339
+ if (!Number.isSafeInteger(expectedLength)) return {
340
+ kind: "error",
341
+ message: `Content-Length is too large: ${header}`
342
+ };
343
+ return {
344
+ kind: "ok",
345
+ expectedLength
346
+ };
347
+ }
348
+ function contentLengthMismatchMessage(expectedLength, actualLength) {
349
+ return `Content-Length ${expectedLength} does not match request body length ${actualLength}`;
350
+ }
139
351
  function fileNames(...names) {
140
352
  const present = names.filter((name) => name !== void 0);
141
353
  return present.length > 0 ? present : void 0;
142
354
  }
355
+ function queryParamsBody(params) {
356
+ const body = Object.create(null);
357
+ let hasParams = false;
358
+ for (const [key, value] of params) {
359
+ if (body[key] !== void 0) continue;
360
+ body[key] = value;
361
+ hasParams = true;
362
+ }
363
+ return hasParams ? body : null;
364
+ }
365
+ var JSON_GET_ENDPOINTS = /* @__PURE__ */ new Set([
366
+ "b2_authorize_account",
367
+ "b2_list_groups",
368
+ "b2_list_group_members",
369
+ "bz_list_computers"
370
+ ]);
371
+ var JSON_POST_ENDPOINTS = /* @__PURE__ */ new Set([
372
+ "b2_create_group_member",
373
+ "b2_eject_group_member",
374
+ "b2_reserve_trial_create_account",
375
+ "bz_delete_computer",
376
+ "b2_create_bucket",
377
+ "b2_list_buckets",
378
+ "b2_delete_bucket",
379
+ "b2_update_bucket",
380
+ "b2_get_upload_url",
381
+ "b2_list_file_names",
382
+ "b2_list_file_versions",
383
+ "b2_get_file_info",
384
+ "b2_hide_file",
385
+ "b2_delete_file_version",
386
+ "b2_copy_file",
387
+ "b2_start_large_file",
388
+ "b2_get_upload_part_url",
389
+ "b2_finish_large_file",
390
+ "b2_cancel_large_file",
391
+ "b2_list_unfinished_large_files",
392
+ "b2_list_parts",
393
+ "b2_copy_part",
394
+ "b2_get_download_authorization",
395
+ "b2_create_key",
396
+ "b2_list_keys",
397
+ "b2_delete_key",
398
+ "b2_update_file_retention",
399
+ "b2_update_file_legal_hold",
400
+ "b2_get_bucket_notification_rules",
401
+ "b2_set_bucket_notification_rules"
402
+ ]);
403
+ function jsonEndpointAllowsMethod(method, endpoint) {
404
+ const normalizedMethod = method.toUpperCase();
405
+ if (JSON_GET_ENDPOINTS.has(endpoint)) return normalizedMethod === "GET";
406
+ if (JSON_POST_ENDPOINTS.has(endpoint)) return normalizedMethod === "POST";
407
+ return true;
408
+ }
143
409
  function notificationRulePrefixes(body) {
144
410
  if (typeof body !== "object" || body === null) return void 0;
145
411
  const rules = body["eventNotificationRules"];
@@ -156,6 +422,13 @@ function storedNotificationRulePrefixes(rules) {
156
422
  function hasKeyManagementCapability(capabilities) {
157
423
  return capabilities.some((capability) => capability === Capability.ListKeys || capability === Capability.WriteKeys || capability === Capability.DeleteKeys);
158
424
  }
425
+ function missingStoredKeyCapabilities(key, required) {
426
+ const capabilities = new Set(key.capabilities);
427
+ return required.filter((capability) => !capabilities.has(capability));
428
+ }
429
+ function storedKeyAllowsBucket(key, bucketId) {
430
+ return key.bucketIds === null || key.bucketIds.includes(bucketId);
431
+ }
159
432
  function publicServerSideEncryption(encryption) {
160
433
  if (encryption.mode === EncryptionMode.SseC) return {
161
434
  mode: encryption.mode,
@@ -167,26 +440,164 @@ function publicServerSideEncryption(encryption) {
167
440
  };
168
441
  return encryption;
169
442
  }
170
- function uploadServerSideEncryption(headers, fallback) {
171
- if (headers["x-bz-server-side-encryption-customer-algorithm"] === EncryptionAlgorithm.Aes256) return {
172
- mode: EncryptionMode.SseC,
173
- algorithm: EncryptionAlgorithm.Aes256,
174
- customerKey: headers["x-bz-server-side-encryption-customer-key"] ?? "",
175
- customerKeyMd5: headers["x-bz-server-side-encryption-customer-key-md5"] ?? ""
176
- };
177
- if (headers["x-bz-server-side-encryption"] === EncryptionAlgorithm.Aes256) return {
178
- mode: EncryptionMode.SseB2,
179
- algorithm: EncryptionAlgorithm.Aes256
443
+ function customerKeyDigest(customerKey) {
444
+ let first = 2166136261;
445
+ let second = 668265261;
446
+ for (const byte of customerKey) {
447
+ first = Math.imul(first ^ byte, 16777619) >>> 0;
448
+ second = Math.imul(second ^ byte, 2246822507) >>> 0;
449
+ }
450
+ return `${first.toString(16).padStart(8, "0")}${second.toString(16).padStart(8, "0")}`;
451
+ }
452
+ var BASE64_ALPHABET = /^[A-Za-z0-9+/]+$/;
453
+ function normalizeSizedBase64(value, decodedByteLength) {
454
+ const paddedLength = Math.ceil(decodedByteLength / 3) * 4;
455
+ const noPaddingLength = Math.ceil(decodedByteLength * 8 / 6);
456
+ const padding = "=".repeat(paddedLength - noPaddingLength);
457
+ if (value.length === noPaddingLength && BASE64_ALPHABET.test(value)) return value.padEnd(paddedLength, "=");
458
+ if (value.length === paddedLength && value.endsWith(padding) && BASE64_ALPHABET.test(value.slice(0, noPaddingLength))) return value;
459
+ return null;
460
+ }
461
+ function base64ToBytes(value, decodedByteLength) {
462
+ const normalized = normalizeSizedBase64(value, decodedByteLength);
463
+ if (normalized === null) return null;
464
+ try {
465
+ const decoded = atob(normalized);
466
+ if (decoded.length !== decodedByteLength) return null;
467
+ const bytes = new Uint8Array(decoded.length);
468
+ for (let i = 0; i < decoded.length; i++) bytes[i] = decoded.charCodeAt(i);
469
+ return bytes;
470
+ } catch {
471
+ return null;
472
+ }
473
+ }
474
+ function encryptionValidationError(message) {
475
+ return {
476
+ status: 400,
477
+ body: {
478
+ status: 400,
479
+ code: "bad_request",
480
+ message
481
+ }
180
482
  };
181
- return fallback;
483
+ }
484
+ function sameStoredServerSideEncryption(a, b) {
485
+ if (a.mode !== b.mode) return false;
486
+ if (a.mode === EncryptionMode.None) return true;
487
+ if (a.mode === EncryptionMode.SseB2 && b.mode === EncryptionMode.SseB2) return a.algorithm === b.algorithm;
488
+ if (a.mode === EncryptionMode.SseC && b.mode === EncryptionMode.SseC) return a.algorithm === b.algorithm && a.customerKeyDigest === b.customerKeyDigest && a.customerKeyMd5 === b.customerKeyMd5;
489
+ return false;
490
+ }
491
+ function hasCustomerEncryptionFields(encryption) {
492
+ return encryption["customerKey"] !== void 0 || encryption["customerKeyMd5"] !== void 0 || encryption["customerAlgorithm"] !== void 0;
493
+ }
494
+ function hasCustomerEncryptionHeaders(headers) {
495
+ return headers["x-bz-server-side-encryption-customer-algorithm"] !== void 0 || headers["x-bz-server-side-encryption-customer-key"] !== void 0 || headers["x-bz-server-side-encryption-customer-key-md5"] !== void 0;
496
+ }
497
+ async function storedServerSideEncryption(encryption) {
498
+ const runtimeEncryption = encryption;
499
+ if (encryption.mode === EncryptionMode.SseC) {
500
+ if (runtimeEncryption["algorithm"] === void 0) return encryptionValidationError("SSE-C customer algorithm is required");
501
+ if (encryption.algorithm !== EncryptionAlgorithm.Aes256) return encryptionValidationError("SSE-C customer algorithm must be AES256");
502
+ const customerKey = encryption.customerKey;
503
+ const customerKeyMd5 = encryption.customerKeyMd5;
504
+ if (typeof customerKey !== "string" || customerKey === "") return encryptionValidationError("SSE-C customer key is required");
505
+ if (typeof customerKeyMd5 !== "string" || customerKeyMd5 === "") return encryptionValidationError("SSE-C customer key MD5 is required");
506
+ const customerKeyBytes = base64ToBytes(customerKey, 32);
507
+ if (customerKeyBytes === null) return encryptionValidationError("SSE-C customer key must be a base64-encoded 256-bit key");
508
+ const normalizedCustomerKeyMd5 = normalizeSizedBase64(customerKeyMd5, 16);
509
+ if (normalizedCustomerKeyMd5 === null) return encryptionValidationError("SSE-C customer key MD5 must be a base64-encoded 128-bit digest");
510
+ if (await md5Base64(customerKeyBytes) !== normalizedCustomerKeyMd5) return encryptionValidationError("SSE-C customer key MD5 does not match the key");
511
+ return {
512
+ mode: encryption.mode,
513
+ algorithm: encryption.algorithm,
514
+ customerKeyDigest: customerKeyDigest(customerKeyBytes),
515
+ customerKeyMd5: normalizedCustomerKeyMd5
516
+ };
517
+ }
518
+ if (encryption.mode === EncryptionMode.SseB2) {
519
+ if (hasCustomerEncryptionFields(runtimeEncryption)) return encryptionValidationError("SSE-B2 settings must not include SSE-C customer keys");
520
+ if (encryption.algorithm !== EncryptionAlgorithm.Aes256) return encryptionValidationError("SSE-B2 algorithm must be AES256");
521
+ return {
522
+ mode: encryption.mode,
523
+ algorithm: encryption.algorithm
524
+ };
525
+ }
526
+ if (encryption.mode === EncryptionMode.None) {
527
+ if (hasCustomerEncryptionFields(runtimeEncryption)) return encryptionValidationError("No-encryption settings must not include SSE-C customer keys");
528
+ return { mode: EncryptionMode.None };
529
+ }
530
+ return encryptionValidationError(`Unsupported server-side encryption mode: ${String(runtimeEncryption["mode"])}`);
531
+ }
532
+ async function uploadServerSideEncryption(headers, fallback) {
533
+ const customerAlgorithm = headers["x-bz-server-side-encryption-customer-algorithm"];
534
+ const customerKey = headers["x-bz-server-side-encryption-customer-key"];
535
+ const customerKeyMd5 = headers["x-bz-server-side-encryption-customer-key-md5"];
536
+ const managedEncryption = headers["x-bz-server-side-encryption"];
537
+ if (hasCustomerEncryptionHeaders(headers)) {
538
+ if (managedEncryption !== void 0) return encryptionValidationError("SSE-B2 settings must not include SSE-C customer keys");
539
+ if (customerAlgorithm === void 0) return encryptionValidationError("SSE-C customer algorithm is required");
540
+ if (customerAlgorithm !== EncryptionAlgorithm.Aes256) return encryptionValidationError("SSE-C customer algorithm must be AES256");
541
+ if (!customerKey || !customerKeyMd5) return encryptionValidationError("SSE-C customer key and MD5 are required");
542
+ return await storedServerSideEncryption({
543
+ mode: EncryptionMode.SseC,
544
+ algorithm: EncryptionAlgorithm.Aes256,
545
+ customerKey,
546
+ customerKeyMd5
547
+ });
548
+ }
549
+ if (managedEncryption !== void 0) {
550
+ if (managedEncryption !== EncryptionAlgorithm.Aes256) return encryptionValidationError("SSE-B2 algorithm must be AES256");
551
+ return {
552
+ mode: EncryptionMode.SseB2,
553
+ algorithm: EncryptionAlgorithm.Aes256
554
+ };
555
+ }
556
+ return await storedServerSideEncryption(fallback);
182
557
  }
183
558
  function defaultFileRetention(policy, uploadTimestamp) {
184
559
  if (policy.mode === BucketRetentionMode.None || policy.period === null) return null;
185
- const days = policy.period.unit === "days" ? policy.period.duration : policy.period.duration * 365;
560
+ const retainUntilTimestamp = uploadTimestamp + (policy.period.unit === "days" ? policy.period.duration : policy.period.duration * 365) * MS_PER_DAY;
561
+ if (!isValidRetentionTimestamp(retainUntilTimestamp, uploadTimestamp)) return null;
186
562
  return {
187
563
  mode: policy.mode,
188
- retainUntilTimestamp: uploadTimestamp + days * 24 * 60 * 60 * 1e3
564
+ retainUntilTimestamp
565
+ };
566
+ }
567
+ function isRetentionMode(value) {
568
+ return value === RetentionMode.Compliance || value === RetentionMode.Governance;
569
+ }
570
+ function parseFileRetentionValue(value, now) {
571
+ if (typeof value !== "object" || value === null) return null;
572
+ const candidate = value;
573
+ const mode = candidate["mode"];
574
+ const retainUntilTimestamp = candidate["retainUntilTimestamp"];
575
+ if (mode === null && retainUntilTimestamp === null) return {
576
+ mode,
577
+ retainUntilTimestamp
189
578
  };
579
+ if (isRetentionMode(mode) && typeof retainUntilTimestamp === "number") {
580
+ if (!isValidRetentionTimestamp(retainUntilTimestamp, now)) return null;
581
+ return {
582
+ mode,
583
+ retainUntilTimestamp
584
+ };
585
+ }
586
+ return null;
587
+ }
588
+ function isRetentionWeakened(current, next) {
589
+ if (current === null || current === void 0) return false;
590
+ if (current.mode === null) return false;
591
+ if (next.mode === null) return true;
592
+ if (current.mode === RetentionMode.Compliance && next.mode !== RetentionMode.Compliance) return true;
593
+ if (current.retainUntilTimestamp === null) return next.mode !== current.mode;
594
+ if (next.retainUntilTimestamp === null) return true;
595
+ return next.retainUntilTimestamp < current.retainUntilTimestamp;
596
+ }
597
+ function requiresGovernanceBypass(current, next) {
598
+ if (current?.mode !== RetentionMode.Governance) return false;
599
+ if (next.mode === RetentionMode.Compliance) return true;
600
+ return isRetentionWeakened(current, next);
190
601
  }
191
602
  /**
192
603
  * In-memory B2 simulator for testing. Implements the B2 native API at the
@@ -203,6 +614,15 @@ function defaultFileRetention(policy, uploadTimestamp) {
203
614
  * })
204
615
  * await client.authorize()
205
616
  * ```
617
+ *
618
+ * `test-key-id:test-key` is the documented implicit full-access
619
+ * storage credential. `master-key-id:master-key` is also accepted for
620
+ * tests that share storage and Partner simulator credentials. In the
621
+ * default permissive mode, other well-formed Basic credentials whose
622
+ * applicationKeyId was not minted by `b2_create_key` receive the same
623
+ * implicit master grant for backward compatibility. A simulator-created
624
+ * key always authorizes with its own capabilities, bucket restrictions,
625
+ * name prefix, and expiration.
206
626
  */
207
627
  var B2Simulator = class {
208
628
  buckets = /* @__PURE__ */ new Map();
@@ -219,6 +639,8 @@ var B2Simulator = class {
219
639
  onHookError;
220
640
  strictAuth;
221
641
  authTokenTtlMs;
642
+ customUploadTimestampsEnabled;
643
+ partner;
222
644
  /**
223
645
  * Issued auth tokens with their associated grant scope + expiry. In
224
646
  * permissive mode (`strictAuth: false`) this is still populated by
@@ -234,6 +656,8 @@ var B2Simulator = class {
234
656
  * state for tests.
235
657
  */
236
658
  uploadTokens = /* @__PURE__ */ new Map();
659
+ downloadAuthorizationTokens = /* @__PURE__ */ new Map();
660
+ downloadAuthorizationExpiryHeap = [];
237
661
  /**
238
662
  * Virtual-clock offset applied to `Date.now()` for expiry checks.
239
663
  * Defaults to 0. Tests advance via {@link advanceTime} to fast-forward
@@ -277,7 +701,23 @@ var B2Simulator = class {
277
701
  if (options.onReplicate !== void 0) this.onReplicate = options.onReplicate;
278
702
  if (options.onHookError !== void 0) this.onHookError = options.onHookError;
279
703
  this.strictAuth = options.strictAuth ?? false;
280
- this.authTokenTtlMs = options.authTokenTtlMs ?? 1440 * 60 * 1e3;
704
+ this.customUploadTimestampsEnabled = options.customUploadTimestampsEnabled ?? false;
705
+ this.authTokenTtlMs = options.authTokenTtlMs ?? 864e5;
706
+ this.partner = new PartnerSimulator({
707
+ accountId: this.accountId,
708
+ authTokenTtlMs: this.authTokenTtlMs,
709
+ minimumPartSize: this.minimumPartSize,
710
+ recommendedPartSize: this.recommendedPartSize,
711
+ canAuthorize: (authzHeader) => this.canAuthorizePartner(authzHeader),
712
+ createBucket: (req) => this.createBucket(req),
713
+ error: (status, code, message) => this.error(status, code, message),
714
+ genId: (prefix) => this.genId(prefix),
715
+ monotonicTimestamp: () => this.monotonicTimestamp(),
716
+ now: () => this.now(),
717
+ storeKey: (key) => {
718
+ this.keys.set(key.applicationKeyId, key);
719
+ }
720
+ }, options);
281
721
  }
282
722
  /**
283
723
  * Advance the simulator's virtual clock by `ms` milliseconds. Used in
@@ -335,6 +775,9 @@ var B2Simulator = class {
335
775
  now() {
336
776
  return Date.now() + this.clockOffsetMs;
337
777
  }
778
+ isStoredKeyExpired(key) {
779
+ return key.expirationTimestamp !== null && key.expirationTimestamp <= this.now();
780
+ }
338
781
  pruneExpiredUploadTokens(now = this.now()) {
339
782
  for (const [authorizationToken, token] of this.uploadTokens.entries()) if (now >= token.cleanupAt) this.uploadTokens.delete(authorizationToken);
340
783
  }
@@ -458,15 +901,195 @@ var B2Simulator = class {
458
901
  const token = this.issuedTokens.get(authToken);
459
902
  if (!token) return this.error(401, "bad_auth_token", "unknown auth token");
460
903
  if (this.now() > token.expiresAt) return this.error(401, "expired_auth_token", "auth token has expired; reauthorize");
904
+ if (token.applicationKeyId !== null) {
905
+ const key = this.keys.get(token.applicationKeyId);
906
+ if (key === void 0) return this.error(401, "bad_auth_token", "application key has been deleted; reauthorize");
907
+ if (this.isStoredKeyExpired(key)) return this.error(401, "expired_auth_token", "application key has expired; reauthorize");
908
+ }
461
909
  const missing = missingCapabilitiesFor(endpoint, token.capabilities);
462
910
  if (missing.length > 0) return this.error(403, "unauthorized", `application key lacks required capabilities: ${missing.join(", ")}`);
463
- if (token.bucketIds !== null) {
464
- if ((scope?.bucketIds.length ?? 0) === 0 && scope?.requiresBucketScope === true) return this.error(403, "unauthorized", `application key is scoped to buckets ${token.bucketIds.join(", ")}; bucket scope is required`);
465
- for (const bucketId of scope?.bucketIds ?? []) if (!token.bucketIds.includes(bucketId)) return this.error(403, "unauthorized", `application key is scoped to buckets ${token.bucketIds.join(", ")}; cannot access ${bucketId}`);
911
+ return this.authorizeScopeGrant(scope, {
912
+ bucketIds: token.bucketIds,
913
+ namePrefix: token.namePrefix,
914
+ bucketScopeRequiredMessage: () => `application key is scoped to buckets ${token.bucketIds?.join(", ")}; bucket scope is required`,
915
+ bucketMismatchMessage: (bucketId) => `application key is scoped to buckets ${token.bucketIds?.join(", ")}; cannot access ${bucketId}`,
916
+ prefixMismatchMessage: (fileName) => `application key is scoped to prefix "${token.namePrefix}"; "${fileName}" is outside scope`
917
+ });
918
+ }
919
+ authorizeScopeGrant(scope, grant) {
920
+ if (grant.bucketIds !== null) {
921
+ if (scope?.requiresAccountLevelBucketAccess === true) return this.error(403, "unauthorized", grant.bucketScopeRequiredMessage());
922
+ if ((scope?.bucketIds.length ?? 0) === 0 && scope?.requiresBucketScope === true) return this.error(403, "unauthorized", grant.bucketScopeRequiredMessage());
923
+ for (const bucketId of scope?.bucketIds ?? []) {
924
+ if (grant.bucketIds.includes(bucketId)) continue;
925
+ return this.error(403, "unauthorized", grant.bucketMismatchMessage(bucketId));
926
+ }
927
+ }
928
+ if (grant.namePrefix !== null) for (const fileName of scope?.fileNames ?? []) {
929
+ if (fileName.startsWith(grant.namePrefix)) continue;
930
+ return this.error(403, "unauthorized", grant.prefixMismatchMessage(fileName));
931
+ }
932
+ return null;
933
+ }
934
+ authorizeDownloadRequest(authToken, endpoint, scope, url) {
935
+ if (authToken === void 0 || authToken === "") return this.error(401, "bad_auth_token", "missing authorization token");
936
+ const downloadAuth = this.downloadAuthorizationTokens.get(authToken);
937
+ if (downloadAuth === void 0) {
938
+ this.purgeExpiredDownloadAuthorizationTokens();
939
+ return this.authorizeRequest(authToken, endpoint, scope);
940
+ }
941
+ if (this.isExpiredDownloadAuthorizationToken(downloadAuth)) {
942
+ this.downloadAuthorizationTokens.delete(authToken);
943
+ this.purgeExpiredDownloadAuthorizationTokens();
944
+ return this.error(401, "expired_auth_token", "download authorization token has expired");
945
+ }
946
+ if (endpoint === "b2_download_file_by_id") return this.error(403, "unauthorized", "download authorization tokens cannot be used with b2_download_file_by_id");
947
+ const overrideError = this.authorizeDownloadResponseOverrides(downloadAuth, url);
948
+ if (overrideError !== null) return overrideError;
949
+ return this.authorizeScopeGrant(scope, {
950
+ bucketIds: [downloadAuth.bucketId],
951
+ namePrefix: downloadAuth.fileNamePrefix,
952
+ bucketScopeRequiredMessage: () => `download authorization is scoped to bucket ${downloadAuth.bucketId}; bucket scope is required`,
953
+ bucketMismatchMessage: (bucketId) => `download authorization is scoped to bucket ${downloadAuth.bucketId}; cannot access ${bucketId}`,
954
+ prefixMismatchMessage: (fileName) => `download authorization is scoped to prefix "${downloadAuth.fileNamePrefix}"; "${fileName}" is outside scope`
955
+ });
956
+ }
957
+ isExpiredDownloadAuthorizationToken(token, now = this.now()) {
958
+ return now >= token.expiresAt;
959
+ }
960
+ purgeExpiredDownloadAuthorizationTokens() {
961
+ const now = this.now();
962
+ for (let i = 0; i < DOWNLOAD_AUTH_PURGE_BATCH_SIZE; i++) {
963
+ const next = this.downloadAuthorizationExpiryHeap[0];
964
+ if (next === void 0 || next.expiresAt > now) return;
965
+ this.popDownloadAuthorizationExpiry();
966
+ if (this.downloadAuthorizationTokens.get(next.token)?.expiresAt === next.expiresAt) this.downloadAuthorizationTokens.delete(next.token);
967
+ }
968
+ }
969
+ pushDownloadAuthorizationExpiry(entry) {
970
+ const heap = this.downloadAuthorizationExpiryHeap;
971
+ heap.push(entry);
972
+ let idx = heap.length - 1;
973
+ while (idx > 0) {
974
+ const parentIdx = Math.floor((idx - 1) / 2);
975
+ const parent = heap[parentIdx];
976
+ if (parent.expiresAt <= entry.expiresAt) break;
977
+ heap[idx] = parent;
978
+ idx = parentIdx;
979
+ }
980
+ heap[idx] = entry;
981
+ }
982
+ popDownloadAuthorizationExpiry() {
983
+ const heap = this.downloadAuthorizationExpiryHeap;
984
+ const root = heap[0];
985
+ const last = heap.pop();
986
+ if (root === void 0 || last === void 0 || heap.length === 0) return root;
987
+ let idx = 0;
988
+ while (true) {
989
+ const leftIdx = idx * 2 + 1;
990
+ const rightIdx = leftIdx + 1;
991
+ const left = heap[leftIdx];
992
+ const right = heap[rightIdx];
993
+ if (left === void 0) break;
994
+ const childIdx = right !== void 0 && right.expiresAt < left.expiresAt ? rightIdx : leftIdx;
995
+ const child = heap[childIdx];
996
+ if (child.expiresAt >= last.expiresAt) break;
997
+ heap[idx] = child;
998
+ idx = childIdx;
999
+ }
1000
+ heap[idx] = last;
1001
+ return root;
1002
+ }
1003
+ authorizeDownloadResponseOverrides(downloadAuth, url) {
1004
+ for (const param of DOWNLOAD_RESPONSE_OVERRIDE_PARAMS) {
1005
+ const expected = downloadAuth.responseHeaderOverrides[param];
1006
+ if (expected === void 0) continue;
1007
+ const actual = url.searchParams.get(param);
1008
+ if (actual === expected) continue;
1009
+ const expectedText = JSON.stringify(expected);
1010
+ const actualText = actual === null ? "absent" : JSON.stringify(actual);
1011
+ return this.error(403, "unauthorized", `download authorization requires ${param}=${expectedText}; got ${actualText}`);
1012
+ }
1013
+ return null;
1014
+ }
1015
+ downloadAuthorizationFromRequest(headers, url, options = {}) {
1016
+ const headerToken = requestHeaderValue(headers, "authorization");
1017
+ if (headerToken !== void 0) return headerToken;
1018
+ if (options.allowQueryAuthorization !== true) return void 0;
1019
+ const queryToken = url.searchParams.get("Authorization") ?? url.searchParams.get("authorization");
1020
+ return queryToken?.startsWith(DOWNLOAD_AUTH_TOKEN_PREFIX) ? queryToken : void 0;
1021
+ }
1022
+ requestHasCapability(authToken, capability) {
1023
+ if (!this.strictAuth) return true;
1024
+ if (authToken === void 0) return false;
1025
+ return this.issuedTokens.get(authToken)?.capabilities.includes(capability) === true;
1026
+ }
1027
+ requireFileLockEnabled(bucket) {
1028
+ if (bucket.info.fileLockConfiguration.value?.isFileLockEnabled === true) return null;
1029
+ return this.error(400, "file_lock_not_enabled", "Bucket does not have file lock enabled");
1030
+ }
1031
+ requireGovernanceBypass(req, authToken, message) {
1032
+ if (req.bypassGovernance !== true) return this.error(400, "file_lock_governance_protected", message);
1033
+ if (!this.requestHasCapability(authToken, Capability.BypassGovernance)) return this.error(403, "unauthorized", `application key lacks required capabilities: ${Capability.BypassGovernance}`);
1034
+ return null;
1035
+ }
1036
+ validateReplicationApplicationKey(applicationKeyId, requiredCapabilities, options) {
1037
+ const key = this.keys.get(applicationKeyId);
1038
+ if (key === void 0) return this.invalidReplicationApplicationKey(options.role);
1039
+ if (missingStoredKeyCapabilities(key, requiredCapabilities).length > 0) return this.invalidReplicationApplicationKey(options.role);
1040
+ if (options.bucketId !== void 0 && !storedKeyAllowsBucket(key, options.bucketId)) return this.invalidReplicationApplicationKey(options.role);
1041
+ return null;
1042
+ }
1043
+ invalidReplicationApplicationKey(role) {
1044
+ return this.error(400, "bad_request", `${role} application key is invalid or not authorized for this replication configuration`);
1045
+ }
1046
+ invalidReplicationDestinationBucket() {
1047
+ return this.error(400, "bad_request", "replication destination bucket is invalid or not configured for this source application key");
1048
+ }
1049
+ validateReplicationDestinationBuckets(sourceApplicationKeyId, rules) {
1050
+ for (const rule of rules) {
1051
+ const destinationBucketId = requestRecord(rule)?.["destinationBucketId"];
1052
+ if (typeof destinationBucketId !== "string") continue;
1053
+ const destinationApplicationKeyId = this.buckets.get(destinationBucketId)?.info.replicationConfiguration.asReplicationDestination?.sourceToDestinationKeyMapping[sourceApplicationKeyId];
1054
+ if (destinationApplicationKeyId === void 0) return this.invalidReplicationDestinationBucket();
1055
+ const destinationKeyError = this.validateReplicationApplicationKey(destinationApplicationKeyId, [Capability.WriteFiles], {
1056
+ bucketId: destinationBucketId,
1057
+ role: "replication destination"
1058
+ });
1059
+ if (destinationKeyError !== null) return destinationKeyError;
466
1060
  }
467
- if (token.namePrefix !== null) for (const fileName of scope?.fileNames ?? []) {
468
- if (fileName.startsWith(token.namePrefix)) continue;
469
- return this.error(403, "unauthorized", `application key is scoped to prefix "${token.namePrefix}"; "${fileName}" is outside scope`);
1061
+ return null;
1062
+ }
1063
+ validateReplicationApplicationKeys(config, bucketId) {
1064
+ if (!this.strictAuth || config === void 0) return null;
1065
+ const replicationConfiguration = requestRecord(config);
1066
+ const source = requestRecord(replicationConfiguration?.["asReplicationSource"]);
1067
+ if (source !== null) {
1068
+ const sourceApplicationKeyId = source["sourceApplicationKeyId"];
1069
+ if (typeof sourceApplicationKeyId === "string") {
1070
+ const sourceKeyError = this.validateReplicationApplicationKey(sourceApplicationKeyId, [Capability.ReadFiles, Capability.ListFiles], {
1071
+ bucketId,
1072
+ role: "replication source"
1073
+ });
1074
+ if (sourceKeyError !== null) return sourceKeyError;
1075
+ }
1076
+ const rules = source["replicationRules"];
1077
+ if (typeof sourceApplicationKeyId === "string" && Array.isArray(rules)) {
1078
+ const destinationError = this.validateReplicationDestinationBuckets(sourceApplicationKeyId, rules);
1079
+ if (destinationError !== null) return destinationError;
1080
+ }
1081
+ }
1082
+ const mapping = requestRecord(requestRecord(replicationConfiguration?.["asReplicationDestination"])?.["sourceToDestinationKeyMapping"]);
1083
+ if (mapping !== null) for (const [sourceApplicationKeyId, destinationApplicationKeyId] of Object.entries(mapping)) {
1084
+ const sourceKeyError = this.validateReplicationApplicationKey(sourceApplicationKeyId, [Capability.ReadFiles, Capability.ListFiles], { role: "replication source" });
1085
+ if (sourceKeyError !== null) return sourceKeyError;
1086
+ if (typeof destinationApplicationKeyId === "string") {
1087
+ const destinationKeyError = this.validateReplicationApplicationKey(destinationApplicationKeyId, [Capability.WriteFiles], {
1088
+ bucketId,
1089
+ role: "replication destination"
1090
+ });
1091
+ if (destinationKeyError !== null) return destinationKeyError;
1092
+ }
470
1093
  }
471
1094
  return null;
472
1095
  }
@@ -474,10 +1097,7 @@ var B2Simulator = class {
474
1097
  const directBucketId = requestStringField(body, "bucketId");
475
1098
  const directFileName = requestStringField(body, "fileName");
476
1099
  switch (endpoint) {
477
- case "b2_create_bucket": return {
478
- bucketIds: [],
479
- requiresBucketScope: true
480
- };
1100
+ case "b2_create_bucket": return this.createBucketScope(body);
481
1101
  case "b2_list_buckets": return this.listBucketsScope(body, directBucketId);
482
1102
  case "b2_list_file_names":
483
1103
  case "b2_list_file_versions": return {
@@ -513,9 +1133,23 @@ var B2Simulator = class {
513
1133
  };
514
1134
  case "b2_get_bucket_notification_rules": return this.notificationRulesScope(body, directBucketId, false);
515
1135
  case "b2_set_bucket_notification_rules": return this.notificationRulesScope(body, directBucketId, true);
1136
+ case "b2_update_bucket": return this.updateBucketScope(body, directBucketId);
516
1137
  default: return this.defaultRequestScope(directBucketId, directFileName);
517
1138
  }
518
1139
  }
1140
+ createBucketScope(body) {
1141
+ return {
1142
+ bucketIds: replicationDestinationBucketIds(body),
1143
+ requiresBucketScope: true,
1144
+ requiresAccountLevelBucketAccess: true
1145
+ };
1146
+ }
1147
+ updateBucketScope(body, directBucketId) {
1148
+ return {
1149
+ bucketIds: [...directBucketId === void 0 ? [] : [directBucketId], ...replicationDestinationBucketIds(body)],
1150
+ requiresBucketScope: true
1151
+ };
1152
+ }
519
1153
  listBucketsScope(body, directBucketId) {
520
1154
  if (directBucketId !== void 0) return {
521
1155
  bucketIds: [directBucketId],
@@ -613,7 +1247,10 @@ var B2Simulator = class {
613
1247
  const idParam = options.kind === "file" ? "bucketId" : "fileId";
614
1248
  const scopedId = options.kind === "file" ? options.bucketId : options.fileId;
615
1249
  const uploadId = this.genId(options.kind === "file" ? "upload_file" : "upload_part");
616
- const uploadUrl = new URL(`http://localhost:0/b2api/v3/${endpoint}`);
1250
+ const uploadUrl = new URL(b2Url("http://localhost:0", {
1251
+ version: "v3",
1252
+ endpoint
1253
+ }));
617
1254
  uploadUrl.searchParams.set(idParam, scopedId);
618
1255
  uploadUrl.searchParams.set("uploadId", uploadId);
619
1256
  const sourceToken = options.sourceAuthToken === void 0 ? void 0 : this.issuedTokens.get(options.sourceAuthToken);
@@ -686,40 +1323,61 @@ var B2Simulator = class {
686
1323
  }));
687
1324
  }
688
1325
  /**
689
- * Look up the application key matching the `Authorization` header on
1326
+ * Resolve the application key matching the `Authorization` header on
690
1327
  * an `authorize_account` request. The header is in the form
691
1328
  * `Basic base64(applicationKeyId:applicationKey)`.
692
1329
  *
693
- * Returns `null` for the implicit master credential (anything that
694
- * does not match a key minted via `b2_create_key`); in that case
695
- * `authorize` grants the full master capability set.
1330
+ * The simulator has two implicit full-access storage credentials:
1331
+ * the documented `test-key-id:test-key` pair used by SDK tests and
1332
+ * `master-key-id:master-key`. In default permissive mode, any other
1333
+ * well-formed Basic credential whose applicationKeyId is not a stored
1334
+ * key also receives that implicit master grant for backward
1335
+ * compatibility. A credential whose applicationKeyId matches a stored
1336
+ * key must use the stored secret and must not be expired.
696
1337
  *
697
1338
  * @param authzHeader - Raw HTTP `Authorization` header value.
698
1339
  *
699
- * @returns The matching key's grant scope, or `null` for the master.
1340
+ * @returns The authorizing key's grant scope or the rejection reason.
700
1341
  */
701
- findKeyForAuthHeader(authzHeader) {
702
- if (!authzHeader?.startsWith("Basic ")) return null;
703
- const decoded = (() => {
704
- try {
705
- return atob(authzHeader.slice(6));
706
- } catch {
707
- return null;
1342
+ findAuthorizationGrant(authzHeader) {
1343
+ const credentials = parseBasicAuthorizationHeader(authzHeader);
1344
+ if (credentials === null) return { kind: "invalid" };
1345
+ const stored = this.keys.get(credentials.applicationKeyId);
1346
+ if (stored !== void 0) {
1347
+ if (!timingSafeStringEqual(stored.applicationKey, credentials.applicationKey)) return { kind: "invalid" };
1348
+ if (this.isStoredKeyExpired(stored)) return { kind: "expired" };
1349
+ return {
1350
+ kind: "ok",
1351
+ grant: {
1352
+ capabilities: stored.capabilities,
1353
+ bucketIds: stored.bucketIds,
1354
+ namePrefix: stored.namePrefix,
1355
+ applicationKeyId: credentials.applicationKeyId,
1356
+ accountId: stored.accountId,
1357
+ expirationTimestamp: stored.expirationTimestamp
1358
+ }
1359
+ };
1360
+ }
1361
+ if (isSimulatorApplicationKeyId(credentials.applicationKeyId)) return { kind: "invalid" };
1362
+ if (isImplicitStorageMasterCredential(credentials) || !this.strictAuth) return {
1363
+ kind: "ok",
1364
+ grant: {
1365
+ capabilities: SIMULATOR_MASTER_CAPABILITIES,
1366
+ bucketIds: null,
1367
+ namePrefix: null,
1368
+ applicationKeyId: null,
1369
+ accountId: this.accountId,
1370
+ expirationTimestamp: null
708
1371
  }
709
- })();
710
- if (decoded === null) return null;
711
- const idx = decoded.indexOf(":");
712
- if (idx === -1) return null;
713
- const applicationKeyId = decoded.slice(0, idx);
714
- const applicationKey = decoded.slice(idx + 1);
715
- const stored = this.keys.get(applicationKeyId);
716
- if (!stored || stored.applicationKey !== applicationKey) return null;
717
- return {
718
- capabilities: stored.capabilities,
719
- bucketIds: stored.bucketIds,
720
- namePrefix: stored.namePrefix,
721
- applicationKeyId
722
1372
  };
1373
+ return { kind: "invalid" };
1374
+ }
1375
+ canAuthorizePartner(authzHeader) {
1376
+ const credentials = parseBasicAuthorizationHeader(authzHeader);
1377
+ if (credentials === null) return false;
1378
+ if (isImplicitPartnerMasterCredential(credentials)) return true;
1379
+ const stored = this.keys.get(credentials.applicationKeyId);
1380
+ return stored !== void 0 && stored.accountId === this.accountId && !this.isStoredKeyExpired(stored) && timingSafeStringEqual(stored.applicationKey, credentials.applicationKey);
723
1381
  }
724
1382
  /**
725
1383
  * Creates an {@link HttpTransport} that routes requests to this simulator.
@@ -811,7 +1469,7 @@ var B2Simulator = class {
811
1469
  }
812
1470
  /**
813
1471
  * Dispatches a JSON API request to the appropriate handler.
814
- * @param _method - The HTTP method (unused).
1472
+ * @param method - The HTTP method.
815
1473
  * @param origin - The request URL origin used for simulator-issued endpoints.
816
1474
  * @param path - The request URL path containing the B2 endpoint name.
817
1475
  * @param headers - The HTTP request headers; consulted by the
@@ -820,15 +1478,24 @@ var B2Simulator = class {
820
1478
  *
821
1479
  * @returns An object with HTTP status and JSON response body.
822
1480
  */
823
- async handleRequest(_method, origin, path, headers, body) {
824
- const endpoint = path.split("/").pop() ?? "";
825
- const apiVersion = path.match(/\/b2api\/(v\d+)\//)?.[1] ?? "v3";
826
- if (this.strictAuth) {
1481
+ async handleRequest(method, origin, path, headers, body) {
1482
+ const { endpoint, version } = apiPathParts(path);
1483
+ if (!jsonEndpointAllowsMethod(method, endpoint)) return this.error(405, "method_not_allowed", `${endpoint} does not support ${method.toUpperCase()}`);
1484
+ if (this.strictAuth && !this.partner.isEndpoint(endpoint)) {
827
1485
  const authError = this.authorizeRequest(headers["authorization"], endpoint, this.requestScope(endpoint, body));
828
1486
  if (authError !== null) return authError;
829
1487
  }
830
1488
  switch (endpoint) {
831
- case "b2_authorize_account": return this.authorize(headers["authorization"], origin);
1489
+ case "b2_authorize_account":
1490
+ if (version === "v3" && this.partner.isAuthorizeEnabled()) return this.partner.authorize(headers["authorization"], origin);
1491
+ return this.authorize(headers["authorization"], origin);
1492
+ case "b2_create_group_member": return this.partner.createGroupMember(body, headers["authorization"]);
1493
+ case "b2_eject_group_member": return this.partner.ejectGroupMember(body, headers["authorization"]);
1494
+ case "b2_list_groups": return this.partner.listGroups(body, headers["authorization"]);
1495
+ case "b2_list_group_members": return this.partner.listGroupMembers(body, headers["authorization"]);
1496
+ case "b2_reserve_trial_create_account": return this.partner.reserveTrialCreateAccount(body, headers["authorization"]);
1497
+ case "bz_list_computers": return this.partner.listComputers(body, headers["authorization"]);
1498
+ case "bz_delete_computer": return this.partner.deleteComputer(body, headers["authorization"]);
832
1499
  case "b2_create_bucket": return this.createBucket(body);
833
1500
  case "b2_list_buckets": return this.listBuckets(body);
834
1501
  case "b2_delete_bucket": return this.deleteBucket(body);
@@ -838,9 +1505,9 @@ var B2Simulator = class {
838
1505
  case "b2_list_file_versions": return this.listFileVersions(body);
839
1506
  case "b2_get_file_info": return this.getFileInfo(body);
840
1507
  case "b2_hide_file": return this.hideFile(body);
841
- case "b2_delete_file_version": return this.deleteFileVersion(body);
1508
+ case "b2_delete_file_version": return this.deleteFileVersion(body, headers["authorization"]);
842
1509
  case "b2_copy_file": return await this.copyFile(body);
843
- case "b2_start_large_file": return this.startLargeFile(body);
1510
+ case "b2_start_large_file": return await this.startLargeFile(body);
844
1511
  case "b2_get_upload_part_url": return await this.getUploadPartUrl(body, headers["authorization"]);
845
1512
  case "b2_finish_large_file": return this.finishLargeFile(body);
846
1513
  case "b2_cancel_large_file": return this.cancelLargeFile(body);
@@ -848,10 +1515,10 @@ var B2Simulator = class {
848
1515
  case "b2_list_parts": return this.listParts(body);
849
1516
  case "b2_copy_part": return await this.copyPart(body);
850
1517
  case "b2_get_download_authorization": return this.getDownloadAuthorization(body);
851
- case "b2_create_key": return this.createKey(body, apiVersion);
1518
+ case "b2_create_key": return this.createKey(body, version, headers["authorization"]);
852
1519
  case "b2_list_keys": return this.listKeys(body);
853
1520
  case "b2_delete_key": return this.deleteKey(body);
854
- case "b2_update_file_retention": return this.updateFileRetention(body);
1521
+ case "b2_update_file_retention": return this.updateFileRetention(body, headers["authorization"]);
855
1522
  case "b2_update_file_legal_hold": return this.updateFileLegalHold(body);
856
1523
  case "b2_get_bucket_notification_rules": return this.getBucketNotificationRules(body);
857
1524
  case "b2_set_bucket_notification_rules": return this.setBucketNotificationRules(body);
@@ -881,9 +1548,25 @@ var B2Simulator = class {
881
1548
  const fileName = kind === "part" || headers["x-bz-file-name"] === void 0 ? void 0 : decodeHeaderValue(headers["x-bz-file-name"]);
882
1549
  const authError = await this.validateUploadAuthorization(kind, url, headers["authorization"], fileName);
883
1550
  if (authError !== null) return authError;
1551
+ const contentLengthError = this.validateContentLength(headers, data.byteLength);
1552
+ if (contentLengthError !== null) return contentLengthError;
884
1553
  if (kind === "part") return await this.handleUploadPart(url, headers, data);
885
1554
  return await this.handleUploadFile(url, headers, data);
886
1555
  }
1556
+ validateContentLength(headers, actualLength) {
1557
+ const parsed = parseContentLengthHeader(headers);
1558
+ if (parsed.kind === "absent") return null;
1559
+ if (parsed.kind === "error") return this.error(400, "bad_request", parsed.message);
1560
+ if (parsed.expectedLength !== actualLength) return this.error(400, "bad_request", contentLengthMismatchMessage(parsed.expectedLength, actualLength));
1561
+ return null;
1562
+ }
1563
+ parseCustomUploadTimestamp(value) {
1564
+ if (value === void 0 || value === null) return { timestamp: null };
1565
+ if (!this.customUploadTimestampsEnabled) return this.error(400, "custom_timestamp_not_allowed", "Custom upload timestamps are not enabled for this account.");
1566
+ const timestamp = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : NaN;
1567
+ if (!Number.isSafeInteger(timestamp) || timestamp < 0 || timestamp > this.now()) return this.error(400, "custom_timestamp_invalid", "The request has an invalid custom upload timestamp.");
1568
+ return { timestamp };
1569
+ }
887
1570
  /**
888
1571
  * Handles file download requests (`b2_download_file_by_id`, `/file/` by name).
889
1572
  * Returns the file data along with B2 response headers.
@@ -895,33 +1578,32 @@ var B2Simulator = class {
895
1578
  */
896
1579
  handleDownload(path, headers, method = "GET") {
897
1580
  if (path.includes("b2_download_file_by_id")) {
1581
+ const url = new URL(`http://localhost${path}`);
898
1582
  if (this.strictAuth) {
899
- const url = new URL(`http://localhost${path}`);
900
- const authError = this.authorizeRequest(headers["authorization"], "b2_download_file_by_id", this.fileIdScope(url.searchParams.get("fileId") ?? void 0) ?? {
1583
+ const authError = this.authorizeDownloadRequest(this.downloadAuthorizationFromRequest(headers, url, { allowQueryAuthorization: false }), "b2_download_file_by_id", this.fileIdScope(url.searchParams.get("fileId") ?? void 0) ?? {
901
1584
  bucketIds: [],
902
1585
  requiresBucketScope: true
903
- });
1586
+ }, url);
904
1587
  if (authError !== null) return this.errorAsDownload(authError);
905
1588
  }
906
- const url = new URL(`http://localhost${path}`);
907
1589
  const fileId = url.searchParams.get("fileId") ?? "";
908
- return this.finalizeDownload(this.downloadById(fileId, headers["range"]), url, method);
1590
+ return this.finalizeDownload(this.downloadById(fileId, headers, requestHeaderValue(headers, "range")), url, method);
909
1591
  }
910
1592
  const fileMatch = path.match(/^([^?]+)/)?.[1]?.match(/\/file\/([^/]+)\/(.+)/);
911
1593
  if (fileMatch) {
912
1594
  const bucketName = decodeURIComponent(fileMatch[1] ?? "");
913
1595
  const fileName = decodeURIComponent(fileMatch[2] ?? "");
1596
+ const url = new URL(`http://localhost${path}`);
914
1597
  if (this.strictAuth) {
915
1598
  const bucket = [...this.buckets.values()].find((b) => b.info.bucketName === bucketName);
916
- const authError = this.authorizeRequest(headers["authorization"], "b2_download_file_by_name", {
1599
+ const authError = this.authorizeDownloadRequest(this.downloadAuthorizationFromRequest(headers, url, { allowQueryAuthorization: true }), "b2_download_file_by_name", {
917
1600
  bucketIds: bucket === void 0 ? [] : [bucket.info.bucketId],
918
1601
  fileNames: [fileName],
919
1602
  requiresBucketScope: true
920
- });
1603
+ }, url);
921
1604
  if (authError !== null) return this.errorAsDownload(authError);
922
1605
  }
923
- const url = new URL(`http://localhost${path}`);
924
- return this.finalizeDownload(this.downloadByName(bucketName, fileName, headers["range"]), url, method);
1606
+ return this.finalizeDownload(this.downloadByName(bucketName, fileName, headers, requestHeaderValue(headers, "range")), url, method);
925
1607
  }
926
1608
  return {
927
1609
  status: 404,
@@ -958,16 +1640,8 @@ var B2Simulator = class {
958
1640
  * @returns The response with overrides applied.
959
1641
  */
960
1642
  finalizeDownload(response, url, method) {
961
- const overrideMap = {
962
- b2ContentDisposition: "Content-Disposition",
963
- b2ContentLanguage: "Content-Language",
964
- b2ContentEncoding: "Content-Encoding",
965
- b2ContentType: "Content-Type",
966
- b2CacheControl: "Cache-Control",
967
- b2Expires: "Expires"
968
- };
969
1643
  const newHeaders = { ...response.headers };
970
- for (const [param, header] of Object.entries(overrideMap)) {
1644
+ for (const [param, header] of Object.entries(DOWNLOAD_RESPONSE_OVERRIDE_HEADERS)) {
971
1645
  const value = url.searchParams.get(param);
972
1646
  if (value !== null) newHeaders[header] = value;
973
1647
  }
@@ -993,6 +1667,10 @@ var B2Simulator = class {
993
1667
  const resolved = await this.resolveUploadSha1(headers["x-bz-content-sha1"], data);
994
1668
  if ("status" in resolved) return resolved;
995
1669
  const { sha1: contentSha1, data: storedData } = resolved;
1670
+ const serverSideEncryption = await uploadServerSideEncryption(headers, bucket.info.defaultServerSideEncryption);
1671
+ if ("status" in serverSideEncryption) return serverSideEncryption;
1672
+ const customUploadTimestamp = this.parseCustomUploadTimestamp(requestHeaderValue(headers, "x-bz-custom-upload-timestamp"));
1673
+ if ("status" in customUploadTimestamp) return customUploadTimestamp;
996
1674
  const fileVersion = this.makeFileVersion({
997
1675
  bucketId,
998
1676
  fileName,
@@ -1001,11 +1679,14 @@ var B2Simulator = class {
1001
1679
  contentSha1,
1002
1680
  fileInfo,
1003
1681
  action: FileAction.Upload,
1004
- serverSideEncryption: uploadServerSideEncryption(headers, bucket.info.defaultServerSideEncryption)
1682
+ serverSideEncryption,
1683
+ ...this.newReplicationStatusField(bucketId, fileName),
1684
+ ...customUploadTimestamp.timestamp !== null ? { uploadTimestamp: customUploadTimestamp.timestamp } : {}
1005
1685
  });
1006
1686
  const stored = {
1007
1687
  fileVersion,
1008
- data: storedData
1688
+ data: storedData,
1689
+ serverSideEncryption
1009
1690
  };
1010
1691
  const existing = bucket.files.get(fileName);
1011
1692
  if (existing) existing.push(stored);
@@ -1055,18 +1736,39 @@ var B2Simulator = class {
1055
1736
  data
1056
1737
  };
1057
1738
  }
1739
+ async validateUploadPartEncryption(large, headers) {
1740
+ if (large.serverSideEncryption.mode !== EncryptionMode.SseC) {
1741
+ if (!hasCustomerEncryptionHeaders(headers)) return null;
1742
+ return this.error(400, "bad_request", "SSE-C upload part headers require an SSE-C large file");
1743
+ }
1744
+ const supplied = await storedServerSideEncryption({
1745
+ mode: EncryptionMode.SseC,
1746
+ algorithm: headers["x-bz-server-side-encryption-customer-algorithm"],
1747
+ customerKey: headers["x-bz-server-side-encryption-customer-key"],
1748
+ customerKeyMd5: headers["x-bz-server-side-encryption-customer-key-md5"]
1749
+ });
1750
+ if ("status" in supplied) return supplied;
1751
+ if (!sameStoredServerSideEncryption(large.serverSideEncryption, supplied)) return this.error(400, "bad_request", "SSE-C upload parts require matching customer encryption headers");
1752
+ return null;
1753
+ }
1058
1754
  async handleUploadPart(url, headers, data) {
1059
1755
  const fileId = new URL(url).searchParams.get("fileId");
1060
1756
  if (!fileId) return this.error(400, "bad_request", "Missing fileId");
1061
1757
  const large = this.largeFiles.get(fileId);
1062
1758
  if (!large) return this.error(400, "bad_request", "Large file not found");
1063
- const partNumber = Number.parseInt(headers["x-bz-part-number"] ?? "0", 10);
1759
+ const partNumber = this.parsePartNumberHeader(requestHeaderValue(headers, "x-bz-part-number"));
1760
+ if (typeof partNumber !== "number") return partNumber;
1761
+ const encryptionError = await this.validateUploadPartEncryption(large, headers);
1762
+ if (encryptionError !== null) return encryptionError;
1064
1763
  const resolved = await this.resolveUploadSha1(headers["x-bz-content-sha1"], data);
1065
1764
  if ("status" in resolved) return resolved;
1066
1765
  const { sha1, data: partData } = resolved;
1766
+ const uploadTimestamp = this.monotonicTimestamp();
1067
1767
  large.parts.set(partNumber, {
1068
1768
  data: partData,
1069
- sha1
1769
+ sha1,
1770
+ contentMd5: null,
1771
+ uploadTimestamp
1070
1772
  });
1071
1773
  return {
1072
1774
  status: 200,
@@ -1075,21 +1777,32 @@ var B2Simulator = class {
1075
1777
  partNumber,
1076
1778
  contentLength: partData.byteLength,
1077
1779
  contentSha1: sha1,
1780
+ contentMd5: null,
1078
1781
  serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
1079
- uploadTimestamp: Date.now()
1782
+ uploadTimestamp
1080
1783
  }
1081
1784
  };
1082
1785
  }
1083
- downloadById(fileId, range) {
1786
+ parsePartNumberHeader(header) {
1787
+ if (header === void 0) return this.error(400, "bad_request", "X-Bz-Part-Number header is required");
1788
+ if (!/^\d+$/.test(header)) return this.error(400, "bad_request", `X-Bz-Part-Number must be an integer between ${B2_MIN_PART_NUMBER} and ${B2_MAX_PART_NUMBER}; received ${header}`);
1789
+ const partNumber = Number(header);
1790
+ return this.validatePartNumber(partNumber) ?? partNumber;
1791
+ }
1792
+ validatePartNumber(partNumber) {
1793
+ if (Number.isInteger(partNumber) && partNumber >= B2_MIN_PART_NUMBER && partNumber <= B2_MAX_PART_NUMBER) return null;
1794
+ return this.error(400, "bad_request", `partNumber must be an integer between ${B2_MIN_PART_NUMBER} and ${B2_MAX_PART_NUMBER}; received ${String(partNumber)}`);
1795
+ }
1796
+ downloadById(fileId, headers, range) {
1084
1797
  const found = this.findFile(fileId);
1085
1798
  if (found === null) return {
1086
1799
  status: 404,
1087
1800
  headers: {},
1088
1801
  data: null
1089
1802
  };
1090
- return this.serveFile(found.stored, range);
1803
+ return this.serveFile(found.stored, headers, range);
1091
1804
  }
1092
- downloadByName(bucketName, fileName, range) {
1805
+ downloadByName(bucketName, fileName, headers, range) {
1093
1806
  for (const bucket of this.buckets.values()) {
1094
1807
  if (bucket.info.bucketName !== bucketName) continue;
1095
1808
  const versions = bucket.files.get(fileName);
@@ -1100,7 +1813,7 @@ var B2Simulator = class {
1100
1813
  headers: {},
1101
1814
  data: null
1102
1815
  };
1103
- return this.serveFile(latest, range);
1816
+ return this.serveFile(latest, headers, range);
1104
1817
  }
1105
1818
  return {
1106
1819
  status: 404,
@@ -1108,7 +1821,41 @@ var B2Simulator = class {
1108
1821
  data: null
1109
1822
  };
1110
1823
  }
1111
- serveFile(stored, range) {
1824
+ validateDownloadEncryption(stored, headers) {
1825
+ const encryption = stored.serverSideEncryption;
1826
+ if (encryption.mode !== EncryptionMode.SseC) return null;
1827
+ const algorithm = headers["x-bz-server-side-encryption-customer-algorithm"];
1828
+ const customerKey = headers["x-bz-server-side-encryption-customer-key"];
1829
+ const customerKeyMd5 = headers["x-bz-server-side-encryption-customer-key-md5"];
1830
+ const customerKeyBytes = customerKey === void 0 ? null : base64ToBytes(customerKey, 32);
1831
+ const normalizedCustomerKeyMd5 = customerKeyMd5 === void 0 ? null : normalizeSizedBase64(customerKeyMd5, 16);
1832
+ if (algorithm !== encryption.algorithm || customerKeyBytes === null || normalizedCustomerKeyMd5 === null || md5Base64Sync(customerKeyBytes) !== normalizedCustomerKeyMd5) return this.errorAsDownload(this.error(400, "bad_request", "SSE-C downloads require matching customer encryption headers"));
1833
+ if (normalizedCustomerKeyMd5 !== encryption.customerKeyMd5 || customerKeyDigest(customerKeyBytes) !== encryption.customerKeyDigest) return this.errorAsDownload(this.error(403, "access_denied", "SSE-C customer encryption key does not match the stored file"));
1834
+ return null;
1835
+ }
1836
+ async validateSourceServerSideEncryption(stored, encryption) {
1837
+ const expected = stored.serverSideEncryption;
1838
+ if (expected.mode !== EncryptionMode.SseC) return null;
1839
+ if (encryption?.mode !== EncryptionMode.SseC) return this.error(400, "bad_request", "SSE-C source copies require customer encryption");
1840
+ const supplied = await storedServerSideEncryption(encryption);
1841
+ if ("status" in supplied) return supplied;
1842
+ if (!sameStoredServerSideEncryption(expected, supplied)) return this.error(400, "bad_request", "SSE-C source customer encryption does not match");
1843
+ return null;
1844
+ }
1845
+ async resolveCopyDestinationEncryption(destinationBucket, requested) {
1846
+ if (requested !== void 0) return await storedServerSideEncryption(requested);
1847
+ return await storedServerSideEncryption(destinationBucket.info.defaultServerSideEncryption);
1848
+ }
1849
+ async validateCopyPartDestinationEncryption(large, encryption) {
1850
+ if (encryption === void 0) return large.serverSideEncryption.mode === EncryptionMode.SseC ? this.error(400, "bad_request", "SSE-C destination copies require customer encryption") : null;
1851
+ const supplied = await storedServerSideEncryption(encryption);
1852
+ if ("status" in supplied) return supplied;
1853
+ if (!sameStoredServerSideEncryption(large.serverSideEncryption, supplied)) return this.error(400, "bad_request", "Destination server-side encryption does not match");
1854
+ return null;
1855
+ }
1856
+ serveFile(stored, requestHeaders, range) {
1857
+ const encryptionError = this.validateDownloadEncryption(stored, requestHeaders);
1858
+ if (encryptionError !== null) return encryptionError;
1112
1859
  const fullData = stored.data;
1113
1860
  let data = fullData;
1114
1861
  let status = 200;
@@ -1139,6 +1886,10 @@ var B2Simulator = class {
1139
1886
  };
1140
1887
  for (const [key, value] of Object.entries(fv.fileInfo)) headers[`X-Bz-Info-${encodeFileName(key)}`] = encodeFileName(value);
1141
1888
  if (!("src_last_modified_millis" in fv.fileInfo)) headers["X-Bz-Info-src_last_modified_millis"] = String(fv.uploadTimestamp);
1889
+ const unauthorizedToRead = [];
1890
+ this.addDownloadEncryptionHeaders(headers, stored.serverSideEncryption, requestHeaders, unauthorizedToRead);
1891
+ this.addDownloadObjectLockHeaders(headers, fv, requestHeaders, unauthorizedToRead);
1892
+ if (unauthorizedToRead.length > 0) headers[DownloadHeaderName.ClientUnauthorizedToRead] = unauthorizedToRead.join(",");
1142
1893
  if (contentRange !== null) headers["Content-Range"] = contentRange;
1143
1894
  return {
1144
1895
  status,
@@ -1146,39 +1897,58 @@ var B2Simulator = class {
1146
1897
  data
1147
1898
  };
1148
1899
  }
1900
+ addDownloadEncryptionHeaders(headers, encryption, requestHeaders, unauthorizedToRead) {
1901
+ if (encryption.mode === EncryptionMode.None) return;
1902
+ if (!this.requestHasCapability(requestHeaderValue(requestHeaders, "authorization"), Capability.ReadBucketEncryption)) {
1903
+ if (encryption.mode === EncryptionMode.SseB2) unauthorizedToRead.push(DownloadClientUnauthorizedToReadMarker.ServerSideEncryption);
1904
+ else unauthorizedToRead.push(DownloadClientUnauthorizedToReadMarker.ServerSideEncryptionCustomerAlgorithm, DownloadClientUnauthorizedToReadMarker.ServerSideEncryptionCustomerKeyMd5);
1905
+ return;
1906
+ }
1907
+ if (encryption.mode === EncryptionMode.SseB2) {
1908
+ headers[DownloadHeaderName.ServerSideEncryption] = encryption.algorithm;
1909
+ return;
1910
+ }
1911
+ headers[DownloadHeaderName.ServerSideEncryptionCustomerAlgorithm] = encryption.algorithm;
1912
+ headers[DownloadHeaderName.ServerSideEncryptionCustomerKeyMd5] = encryption.customerKeyMd5;
1913
+ }
1914
+ addDownloadObjectLockHeaders(headers, fileVersion, requestHeaders, unauthorizedToRead) {
1915
+ const authToken = requestHeaderValue(requestHeaders, "authorization");
1916
+ const retention = fileVersion.fileRetention.value;
1917
+ if (retention !== null && retention.mode !== null && retention.retainUntilTimestamp !== null) {
1918
+ if (this.requestHasCapability(authToken, Capability.ReadFileRetentions)) {
1919
+ headers[DownloadHeaderName.FileRetentionMode] = retention.mode;
1920
+ headers[DownloadHeaderName.FileRetentionRetainUntilTimestamp] = String(retention.retainUntilTimestamp);
1921
+ } else unauthorizedToRead.push(DownloadClientUnauthorizedToReadMarker.FileRetentionMode, DownloadClientUnauthorizedToReadMarker.FileRetentionRetainUntilTimestamp);
1922
+ }
1923
+ const legalHold = fileVersion.legalHold.value;
1924
+ if (legalHold === null) return;
1925
+ if (this.requestHasCapability(authToken, Capability.ReadFileLegalHolds)) {
1926
+ headers[DownloadHeaderName.FileLegalHold] = legalHold;
1927
+ return;
1928
+ }
1929
+ unauthorizedToRead.push(DownloadClientUnauthorizedToReadMarker.FileLegalHold);
1930
+ }
1149
1931
  authorize(authzHeader, origin = "http://localhost:0") {
1150
- const capabilities = [
1151
- Capability.ListBuckets,
1152
- Capability.ReadBuckets,
1153
- Capability.WriteBuckets,
1154
- Capability.DeleteBuckets,
1155
- Capability.ListFiles,
1156
- Capability.ReadFiles,
1157
- Capability.WriteFiles,
1158
- Capability.DeleteFiles,
1159
- Capability.ListKeys,
1160
- Capability.WriteKeys,
1161
- Capability.DeleteKeys,
1162
- Capability.ShareFiles,
1163
- Capability.ReadBucketNotifications,
1164
- Capability.WriteBucketNotifications
1165
- ];
1166
- const keyForAuth = this.findKeyForAuthHeader(authzHeader);
1167
- const allowedBuckets = this.allowedBuckets(keyForAuth?.bucketIds);
1168
- const legacyBucketId = singleBucketId(keyForAuth?.bucketIds);
1932
+ const resolution = this.findAuthorizationGrant(authzHeader);
1933
+ if (resolution.kind === "invalid") return this.error(401, "unauthorized", "application key is not valid");
1934
+ if (resolution.kind === "expired") return this.error(401, "unauthorized", "application key is expired");
1935
+ const { grant } = resolution;
1936
+ const allowedBuckets = this.allowedBuckets(grant.bucketIds);
1937
+ const legacyBucketId = singleBucketId(grant.bucketIds);
1169
1938
  const legacyBucketName = legacyBucketId === null ? null : this.buckets.get(legacyBucketId)?.info.bucketName ?? null;
1170
1939
  const tokenStr = `sim_auth_token_${this.nextId++}`;
1171
1940
  this.issuedTokens.set(tokenStr, {
1172
- capabilities: keyForAuth?.capabilities ?? capabilities,
1173
- bucketIds: keyForAuth?.bucketIds ?? null,
1174
- namePrefix: keyForAuth?.namePrefix ?? null,
1941
+ accountId: grant.accountId,
1942
+ capabilities: grant.capabilities,
1943
+ bucketIds: grant.bucketIds,
1944
+ namePrefix: grant.namePrefix,
1175
1945
  expiresAt: this.now() + this.authTokenTtlMs,
1176
- applicationKeyId: keyForAuth?.applicationKeyId ?? null
1946
+ applicationKeyId: grant.applicationKeyId
1177
1947
  });
1178
1948
  return {
1179
1949
  status: 200,
1180
1950
  body: {
1181
- accountId: accountId(this.accountId),
1951
+ accountId: accountId(grant.accountId),
1182
1952
  authorizationToken: tokenStr,
1183
1953
  apiInfo: { storageApi: {
1184
1954
  absoluteMinimumPartSize: this.minimumPartSize,
@@ -1187,28 +1957,30 @@ var B2Simulator = class {
1187
1957
  bucketName: legacyBucketName,
1188
1958
  downloadUrl: origin,
1189
1959
  infoType: "storageApi",
1190
- namePrefix: keyForAuth?.namePrefix ?? null,
1960
+ namePrefix: grant.namePrefix,
1191
1961
  recommendedPartSize: this.recommendedPartSize,
1192
1962
  s3ApiUrl: origin,
1193
1963
  allowed: {
1194
- capabilities: keyForAuth?.capabilities ?? capabilities,
1964
+ capabilities: cloneCapabilities(grant.capabilities),
1195
1965
  buckets: allowedBuckets,
1196
1966
  bucketId: legacyBucketId === null ? null : bucketId(legacyBucketId),
1197
1967
  bucketName: legacyBucketName,
1198
- namePrefix: keyForAuth?.namePrefix ?? null
1968
+ namePrefix: grant.namePrefix
1199
1969
  }
1200
1970
  } },
1201
- applicationKeyExpirationTimestamp: null
1971
+ applicationKeyExpirationTimestamp: grant.expirationTimestamp
1202
1972
  }
1203
1973
  };
1204
1974
  }
1205
1975
  createBucket(req) {
1206
1976
  const nameError = validateBucketName(req.bucketName);
1207
1977
  if (nameError) return this.error(400, nameError.code, nameError.message);
1208
- if (req.bucketInfo !== void 0) {
1209
- const infoError = validateBucketInfo(req.bucketInfo);
1210
- if (infoError) return this.error(400, infoError.code, infoError.message);
1211
- }
1978
+ const objectLockEnabled = nextObjectLockEnabled(false, req.fileLockEnabled);
1979
+ if (typeof objectLockEnabled !== "boolean") return this.error(400, objectLockEnabled.code, objectLockEnabled.message);
1980
+ const configError = validateBucketConfigurationFields(req, { objectLockEnabled });
1981
+ if (configError) return this.error(400, configError.code, configError.message);
1982
+ const replicationKeyError = this.validateReplicationApplicationKeys(req.replicationConfiguration, void 0);
1983
+ if (replicationKeyError !== null) return replicationKeyError;
1212
1984
  for (const b of this.buckets.values()) if (b.info.bucketName === req.bucketName) return this.error(400, "duplicate_bucket_name", "Bucket name already in use");
1213
1985
  const bid = bucketId(this.genId("b2_bucket"));
1214
1986
  const defaultRetention = req.defaultRetention ?? {
@@ -1226,7 +1998,7 @@ var B2Simulator = class {
1226
1998
  fileLockConfiguration: {
1227
1999
  isClientAuthorizedToRead: true,
1228
2000
  value: {
1229
- isFileLockEnabled: req.fileLockEnabled ?? false,
2001
+ isFileLockEnabled: objectLockEnabled,
1230
2002
  defaultRetention
1231
2003
  }
1232
2004
  },
@@ -1234,10 +2006,10 @@ var B2Simulator = class {
1234
2006
  options: [],
1235
2007
  revision: 1,
1236
2008
  defaultRetention,
1237
- replicationConfiguration: req.replicationConfiguration ?? {
2009
+ replicationConfiguration: req.replicationConfiguration === void 0 ? {
1238
2010
  asReplicationSource: null,
1239
2011
  asReplicationDestination: null
1240
- }
2012
+ } : normalizeReplicationConfiguration(req.replicationConfiguration)
1241
2013
  };
1242
2014
  this.buckets.set(bid, {
1243
2015
  info,
@@ -1249,6 +2021,8 @@ var B2Simulator = class {
1249
2021
  };
1250
2022
  }
1251
2023
  listBuckets(req) {
2024
+ const bucketTypesError = validateBucketTypes(req.bucketTypes);
2025
+ if (bucketTypesError) return this.error(400, bucketTypesError.code, bucketTypesError.message);
1252
2026
  return {
1253
2027
  status: 200,
1254
2028
  body: { buckets: [...this.buckets.values()].map((b) => b.info).filter((bucket) => req.bucketId === void 0 || bucket.bucketId === req.bucketId).filter((bucket) => req.bucketName === void 0 || bucket.bucketName === req.bucketName).filter((bucket) => req.bucketTypes === void 0 || req.bucketTypes.includes(bucket.bucketType)) }
@@ -1257,36 +2031,62 @@ var B2Simulator = class {
1257
2031
  deleteBucket(req) {
1258
2032
  const bucket = this.buckets.get(req.bucketId);
1259
2033
  if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
2034
+ if (this.bucketHasContents(bucket)) return this.error(400, "cannot_delete_non_empty_bucket", "Bucket is not empty and cannot be deleted");
1260
2035
  this.buckets.delete(req.bucketId);
1261
2036
  return {
1262
2037
  status: 200,
1263
2038
  body: bucket.info
1264
2039
  };
1265
2040
  }
2041
+ bucketHasContents(bucket) {
2042
+ for (const versions of bucket.files.values()) if (versions.length > 0) return true;
2043
+ for (const large of this.largeFiles.values()) if (large.bucketId === bucket.info.bucketId) return true;
2044
+ return false;
2045
+ }
1266
2046
  updateBucket(req) {
1267
2047
  const bucket = this.buckets.get(req["bucketId"]);
1268
2048
  if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
1269
- if (req["bucketInfo"] !== void 0) {
1270
- const infoError = validateBucketInfo(req["bucketInfo"]);
1271
- if (infoError) return this.error(400, infoError.code, infoError.message);
2049
+ const revisionGuard = req["ifRevisionIs"];
2050
+ if (revisionGuard !== void 0) {
2051
+ if (typeof revisionGuard !== "number" || !Number.isInteger(revisionGuard)) return this.error(400, "bad_request", "ifRevisionIs must be an integer");
2052
+ if (revisionGuard !== bucket.info.revision) return this.error(409, "conflict", "ifRevisionIs test failed");
1272
2053
  }
2054
+ const objectLockEnabled = nextObjectLockEnabled(bucket.info.fileLockConfiguration.value?.isFileLockEnabled ?? false, req["fileLockEnabled"]);
2055
+ if (typeof objectLockEnabled !== "boolean") return this.error(400, objectLockEnabled.code, objectLockEnabled.message);
2056
+ const configError = validateBucketConfigurationFields({
2057
+ bucketInfo: req["bucketInfo"],
2058
+ corsRules: req["corsRules"],
2059
+ defaultRetention: req["defaultRetention"],
2060
+ lifecycleRules: req["lifecycleRules"],
2061
+ replicationConfiguration: req["replicationConfiguration"]
2062
+ }, { objectLockEnabled });
2063
+ if (configError) return this.error(400, configError.code, configError.message);
2064
+ const replicationKeyError = this.validateReplicationApplicationKeys(req["replicationConfiguration"], req["bucketId"]);
2065
+ if (replicationKeyError !== null) return replicationKeyError;
1273
2066
  const updated = {
1274
2067
  ...bucket.info,
1275
2068
  ...req["bucketType"] !== void 0 ? { bucketType: req["bucketType"] } : {},
1276
2069
  ...req["bucketInfo"] !== void 0 ? { bucketInfo: req["bucketInfo"] } : {},
1277
2070
  ...req["lifecycleRules"] !== void 0 ? { lifecycleRules: req["lifecycleRules"] } : {},
1278
2071
  ...req["corsRules"] !== void 0 ? { corsRules: req["corsRules"] } : {},
1279
- ...req["replicationConfiguration"] !== void 0 ? { replicationConfiguration: req["replicationConfiguration"] } : {},
2072
+ ...req["replicationConfiguration"] !== void 0 ? { replicationConfiguration: normalizeReplicationConfiguration(req["replicationConfiguration"]) } : {},
1280
2073
  ...req["defaultRetention"] !== void 0 ? {
1281
2074
  defaultRetention: req["defaultRetention"],
1282
2075
  fileLockConfiguration: {
1283
2076
  isClientAuthorizedToRead: true,
1284
2077
  value: {
1285
- isFileLockEnabled: bucket.info.fileLockConfiguration.value?.isFileLockEnabled ?? false,
2078
+ isFileLockEnabled: objectLockEnabled,
1286
2079
  defaultRetention: req["defaultRetention"]
1287
2080
  }
1288
2081
  }
1289
2082
  } : {},
2083
+ ...req["fileLockEnabled"] !== void 0 && req["defaultRetention"] === void 0 ? { fileLockConfiguration: {
2084
+ isClientAuthorizedToRead: true,
2085
+ value: {
2086
+ isFileLockEnabled: objectLockEnabled,
2087
+ defaultRetention: bucket.info.defaultRetention
2088
+ }
2089
+ } } : {},
1290
2090
  ...req["defaultServerSideEncryption"] !== void 0 ? { defaultServerSideEncryption: req["defaultServerSideEncryption"] } : {},
1291
2091
  revision: bucket.info.revision + 1
1292
2092
  };
@@ -1393,7 +2193,8 @@ var B2Simulator = class {
1393
2193
  const existing = bucket.files.get(req.fileName);
1394
2194
  const stored = {
1395
2195
  fileVersion,
1396
- data: /* @__PURE__ */ new Uint8Array(0)
2196
+ data: /* @__PURE__ */ new Uint8Array(0),
2197
+ serverSideEncryption: { mode: EncryptionMode.None }
1397
2198
  };
1398
2199
  if (existing) existing.push(stored);
1399
2200
  else bucket.files.set(req.fileName, [stored]);
@@ -1402,16 +2203,19 @@ var B2Simulator = class {
1402
2203
  body: fileVersion
1403
2204
  };
1404
2205
  }
1405
- deleteFileVersion(req) {
2206
+ deleteFileVersion(req, authToken) {
1406
2207
  const found = this.findFile(req.fileId);
1407
2208
  if (found === null || found.stored.fileVersion.fileName !== req.fileName) return this.error(400, "file_not_present", "File version not found");
1408
2209
  const fv = found.stored.fileVersion;
1409
2210
  const retention = fv.fileRetention?.value;
1410
2211
  const legalHold = fv.legalHold?.value;
1411
2212
  const now = Date.now();
1412
- if (legalHold === "on") return this.error(400, "file_lock_legal_hold_protected", "File is on legal hold and cannot be deleted");
1413
- if (retention?.mode === "compliance" && retention.retainUntilTimestamp !== null && retention.retainUntilTimestamp > now) return this.error(400, "file_lock_compliance_protected", `File is under compliance-mode retention and cannot be deleted until ${new Date(retention.retainUntilTimestamp).toISOString()}`);
1414
- if (retention?.mode === "governance" && retention.retainUntilTimestamp !== null && retention.retainUntilTimestamp > now && req.bypassGovernance !== true) return this.error(400, "file_lock_governance_protected", "File is under governance-mode retention; pass bypassGovernance: true to delete");
2213
+ if (legalHold === LegalHoldValue.On) return this.error(400, "file_lock_legal_hold_protected", "File is on legal hold and cannot be deleted");
2214
+ if (retention?.mode === RetentionMode.Compliance && retention.retainUntilTimestamp !== null && retention.retainUntilTimestamp > now) return this.error(400, "file_lock_compliance_protected", `File is under compliance-mode retention and cannot be deleted until ${new Date(retention.retainUntilTimestamp).toISOString()}`);
2215
+ if (retention?.mode === RetentionMode.Governance && retention.retainUntilTimestamp !== null && retention.retainUntilTimestamp > now) {
2216
+ const bypassError = this.requireGovernanceBypass(req, authToken, "File is under governance-mode retention; pass bypassGovernance: true to delete");
2217
+ if (bypassError !== null) return bypassError;
2218
+ }
1415
2219
  found.versions.splice(found.index, 1);
1416
2220
  if (found.versions.length === 0) found.bucket.files.delete(req.fileName);
1417
2221
  return {
@@ -1431,6 +2235,10 @@ var B2Simulator = class {
1431
2235
  const destBucketId = req.destinationBucketId ?? found.bucketId;
1432
2236
  const destBucket = this.buckets.get(destBucketId);
1433
2237
  if (!destBucket) return this.error(400, "bad_bucket_id", "Destination bucket not found");
2238
+ const sourceEncryptionError = await this.validateSourceServerSideEncryption(sourceStored, req.sourceServerSideEncryption);
2239
+ if (sourceEncryptionError !== null) return sourceEncryptionError;
2240
+ const destinationEncryption = await this.resolveCopyDestinationEncryption(destBucket, req.destinationServerSideEncryption);
2241
+ if ("status" in destinationEncryption) return destinationEncryption;
1434
2242
  let data = sourceStored.data;
1435
2243
  let contentSha1 = sourceStored.fileVersion.contentSha1 ?? "none";
1436
2244
  if (req.range !== void 0) {
@@ -1463,11 +2271,14 @@ var B2Simulator = class {
1463
2271
  contentLength: data.byteLength,
1464
2272
  contentSha1,
1465
2273
  fileInfo,
1466
- action: FileAction.Copy
2274
+ action: FileAction.Copy,
2275
+ serverSideEncryption: destinationEncryption,
2276
+ ...this.newReplicationStatusField(destBucketId, req.fileName)
1467
2277
  });
1468
2278
  const copied = {
1469
2279
  fileVersion,
1470
- data: new Uint8Array(data)
2280
+ data: new Uint8Array(data),
2281
+ serverSideEncryption: destinationEncryption
1471
2282
  };
1472
2283
  const existing = destBucket.files.get(req.fileName);
1473
2284
  if (existing) existing.push(copied);
@@ -1478,7 +2289,7 @@ var B2Simulator = class {
1478
2289
  body: fileVersion
1479
2290
  };
1480
2291
  }
1481
- startLargeFile(req) {
2292
+ async startLargeFile(req) {
1482
2293
  const bucket = this.buckets.get(req.bucketId);
1483
2294
  if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
1484
2295
  const nameError = validateFileName(req.fileName);
@@ -1487,8 +2298,13 @@ var B2Simulator = class {
1487
2298
  const infoError = validateFileInfo(req.fileInfo);
1488
2299
  if (infoError) return this.error(400, infoError.code, infoError.message);
1489
2300
  }
2301
+ const customUploadTimestamp = this.parseCustomUploadTimestamp(req.customUploadTimestamp);
2302
+ if ("status" in customUploadTimestamp) return customUploadTimestamp;
1490
2303
  const fid = this.genId("4_z");
1491
- const uploadTimestamp = this.monotonicTimestamp();
2304
+ const uploadTimestamp = customUploadTimestamp.timestamp ?? this.monotonicTimestamp();
2305
+ const serverSideEncryption = await storedServerSideEncryption(req.serverSideEncryption ?? bucket.info.defaultServerSideEncryption);
2306
+ if ("status" in serverSideEncryption) return serverSideEncryption;
2307
+ const replicationStatus = this.replicationStatusForNewFile(req.bucketId, req.fileName);
1492
2308
  const large = {
1493
2309
  fileId: fid,
1494
2310
  bucketId: req.bucketId,
@@ -1497,7 +2313,8 @@ var B2Simulator = class {
1497
2313
  fileInfo: req.fileInfo ?? {},
1498
2314
  fileRetention: req.fileRetention ?? defaultFileRetention(bucket.info.defaultRetention, uploadTimestamp),
1499
2315
  legalHold: req.legalHold ?? null,
1500
- serverSideEncryption: req.serverSideEncryption ?? bucket.info.defaultServerSideEncryption,
2316
+ ...this.replicationStatusMetadataFor(replicationStatus),
2317
+ serverSideEncryption,
1501
2318
  uploadTimestamp,
1502
2319
  parts: /* @__PURE__ */ new Map()
1503
2320
  };
@@ -1523,6 +2340,7 @@ var B2Simulator = class {
1523
2340
  isClientAuthorizedToRead: true,
1524
2341
  value: large.legalHold
1525
2342
  },
2343
+ ...this.replicationStatusMetadataFor(large.replicationStatus),
1526
2344
  serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
1527
2345
  uploadTimestamp: large.uploadTimestamp
1528
2346
  }
@@ -1552,17 +2370,11 @@ var B2Simulator = class {
1552
2370
  const bucket = this.buckets.get(large.bucketId);
1553
2371
  if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
1554
2372
  const sortedParts = [...large.parts.entries()].sort((a, b) => a[0] - b[0]);
1555
- if (sortedParts.length > 1e4) return this.error(400, "bad_request", `multipart upload has ${sortedParts.length} parts; B2 caps at 10000`);
1556
- for (const [partNumber] of sortedParts) if (!Number.isInteger(partNumber) || partNumber < 1 || partNumber > 1e4) return this.error(400, "bad_request", `partNumber ${partNumber} is outside the [1, 10000] range B2 accepts`);
2373
+ if (sortedParts.length > B2_MAX_PART_NUMBER) return this.error(400, "bad_request", `multipart upload has ${sortedParts.length} parts; B2 caps at ${B2_MAX_PART_NUMBER}`);
2374
+ for (const [partNumber] of sortedParts) if (!Number.isInteger(partNumber) || partNumber < B2_MIN_PART_NUMBER || partNumber > B2_MAX_PART_NUMBER) return this.error(400, "bad_request", `partNumber ${partNumber} is outside the [${B2_MIN_PART_NUMBER}, ${B2_MAX_PART_NUMBER}] range B2 accepts`);
1557
2375
  if (req.partSha1Array.length !== sortedParts.length) return this.error(400, "bad_request", `partSha1Array has ${req.partSha1Array.length} entries but ${sortedParts.length} parts were uploaded`);
1558
- for (let i = 0; i < sortedParts.length; i++) {
1559
- const [partNumber, part] = sortedParts[i];
1560
- if (req.partSha1Array[i]?.toLowerCase() !== part.sha1) return this.error(400, "bad_request", `part ${partNumber} SHA-1 does not match the uploaded part`);
1561
- }
1562
- for (let i = 0; i < sortedParts.length - 1; i++) {
1563
- const [partNumber, part] = sortedParts[i];
1564
- if (part.data.byteLength < this.minimumPartSize) return this.error(400, "bad_request", `part ${partNumber} (${part.data.byteLength} bytes) is smaller than the minimum part size of ${this.minimumPartSize}`);
1565
- }
2376
+ for (const [i, [partNumber, part]] of sortedParts.entries()) if (req.partSha1Array[i]?.toLowerCase() !== part.sha1) return this.error(400, "bad_request", `part ${partNumber} SHA-1 does not match the uploaded part`);
2377
+ for (const [partNumber, part] of sortedParts.slice(0, -1)) if (part.data.byteLength < this.minimumPartSize) return this.error(400, "bad_request", `part ${partNumber} (${part.data.byteLength} bytes) is smaller than the minimum part size of ${this.minimumPartSize}`);
1566
2378
  let totalSize = 0;
1567
2379
  for (const [_, part] of sortedParts) totalSize += part.data.byteLength;
1568
2380
  const combined = new Uint8Array(totalSize);
@@ -1581,11 +2393,14 @@ var B2Simulator = class {
1581
2393
  action: FileAction.Upload,
1582
2394
  fileRetention: large.fileRetention,
1583
2395
  legalHold: large.legalHold,
1584
- serverSideEncryption: large.serverSideEncryption
2396
+ serverSideEncryption: large.serverSideEncryption,
2397
+ uploadTimestamp: large.uploadTimestamp,
2398
+ ...this.replicationStatusMetadataFor(large.replicationStatus)
1585
2399
  });
1586
2400
  const stored = {
1587
2401
  fileVersion,
1588
- data: combined
2402
+ data: combined,
2403
+ serverSideEncryption: large.serverSideEncryption
1589
2404
  };
1590
2405
  const existing = bucket.files.get(large.fileName);
1591
2406
  if (existing) existing.push(stored);
@@ -1644,6 +2459,7 @@ var B2Simulator = class {
1644
2459
  isClientAuthorizedToRead: true,
1645
2460
  value: f.legalHold
1646
2461
  },
2462
+ ...this.replicationStatusMetadataFor(f.replicationStatus),
1647
2463
  serverSideEncryption: publicServerSideEncryption(f.serverSideEncryption),
1648
2464
  uploadTimestamp: f.uploadTimestamp
1649
2465
  })),
@@ -1663,7 +2479,9 @@ var B2Simulator = class {
1663
2479
  partNumber,
1664
2480
  contentLength: part.data.byteLength,
1665
2481
  contentSha1: part.sha1,
1666
- uploadTimestamp: Date.now()
2482
+ contentMd5: part.contentMd5,
2483
+ serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
2484
+ uploadTimestamp: part.uploadTimestamp
1667
2485
  }));
1668
2486
  return {
1669
2487
  status: 200,
@@ -1676,22 +2494,29 @@ var B2Simulator = class {
1676
2494
  async copyPart(req) {
1677
2495
  const large = this.largeFiles.get(req.largeFileId);
1678
2496
  if (!large) return this.error(400, "bad_request", "Large file not found");
2497
+ const partNumberError = this.validatePartNumber(req.partNumber);
2498
+ if (partNumberError !== null) return partNumberError;
1679
2499
  const found = this.findFile(req.sourceFileId);
1680
2500
  if (found === null) return this.error(404, "file_not_present", "Source file not found");
1681
2501
  const sourceStored = found.stored;
2502
+ const sourceEncryptionError = await this.validateSourceServerSideEncryption(sourceStored, req.sourceServerSideEncryption);
2503
+ if (sourceEncryptionError !== null) return sourceEncryptionError;
2504
+ const destinationEncryptionError = await this.validateCopyPartDestinationEncryption(large, req.destinationServerSideEncryption);
2505
+ if (destinationEncryptionError !== null) return destinationEncryptionError;
1682
2506
  let partData = sourceStored.data;
1683
- if (req.range) {
1684
- const match = req.range.match(/bytes=(\d+)-(\d+)?/);
1685
- if (match) {
1686
- const rs = Number.parseInt(match[1] ?? "0", 10);
1687
- const re = match[2] !== void 0 ? Number.parseInt(match[2], 10) : sourceStored.data.byteLength - 1;
1688
- partData = sourceStored.data.slice(rs, re + 1);
1689
- }
2507
+ if (req.range !== void 0) {
2508
+ const parsed = parseRangeHeader(req.range, sourceStored.data.byteLength);
2509
+ if (parsed.kind === "malformed") return this.error(400, "bad_request", `Malformed copy range: ${req.range}`);
2510
+ if (parsed.kind === "unsatisfiable") return this.error(416, "range_not_satisfiable", `Unsatisfiable copy range: ${req.range}`);
2511
+ partData = sourceStored.data.subarray(parsed.start, parsed.end + 1);
1690
2512
  }
1691
2513
  const sha1 = await sha1Hex(partData);
2514
+ const uploadTimestamp = this.monotonicTimestamp();
1692
2515
  large.parts.set(req.partNumber, {
1693
2516
  data: new Uint8Array(partData),
1694
- sha1
2517
+ sha1,
2518
+ contentMd5: null,
2519
+ uploadTimestamp
1695
2520
  });
1696
2521
  return {
1697
2522
  status: 200,
@@ -1699,39 +2524,80 @@ var B2Simulator = class {
1699
2524
  fileId: req.largeFileId,
1700
2525
  partNumber: req.partNumber,
1701
2526
  contentLength: partData.byteLength,
1702
- contentSha1: sha1
2527
+ contentSha1: sha1,
2528
+ contentMd5: null,
2529
+ serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
2530
+ uploadTimestamp
1703
2531
  }
1704
2532
  };
1705
2533
  }
1706
2534
  getDownloadAuthorization(req) {
2535
+ this.purgeExpiredDownloadAuthorizationTokens();
1707
2536
  if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
2537
+ const prefixError = validateDownloadAuthorizationPrefix(req.fileNamePrefix);
2538
+ if (prefixError) return this.error(400, prefixError.code, prefixError.message);
2539
+ const durationError = validateDownloadAuthorizationDuration(req.validDurationInSeconds);
2540
+ if (durationError) return this.error(400, durationError.code, durationError.message);
2541
+ const overrideError = validateDownloadResponseOverrides(req);
2542
+ if (overrideError) return this.error(400, overrideError.code, overrideError.message);
2543
+ let authorizationToken = randomDownloadAuthorizationToken();
2544
+ while (this.downloadAuthorizationTokens.has(authorizationToken)) authorizationToken = randomDownloadAuthorizationToken();
2545
+ const expiresAt = this.now() + req.validDurationInSeconds * 1e3;
2546
+ this.downloadAuthorizationTokens.set(authorizationToken, {
2547
+ bucketId: req.bucketId,
2548
+ fileNamePrefix: req.fileNamePrefix,
2549
+ expiresAt,
2550
+ responseHeaderOverrides: pickDownloadResponseOverrides(req)
2551
+ });
2552
+ this.pushDownloadAuthorizationExpiry({
2553
+ token: authorizationToken,
2554
+ expiresAt
2555
+ });
1708
2556
  return {
1709
2557
  status: 200,
1710
2558
  body: {
1711
2559
  bucketId: req.bucketId,
1712
2560
  fileNamePrefix: req.fileNamePrefix,
1713
- authorizationToken: `sim_dl_auth_${this.genId("tok")}`
2561
+ authorizationToken
1714
2562
  }
1715
2563
  };
1716
2564
  }
1717
- createKey(req, apiVersion) {
1718
- if (apiVersion === "v4" && hasOwnField(req, "bucketId")) return this.error(400, "bad_request", "bucketId is not accepted by v4 b2_create_key; use bucketIds");
1719
- if (apiVersion !== "v4" && req.bucketId !== void 0 && req.bucketIds !== void 0) return this.error(400, "bad_request", "b2_create_key accepts either bucketIds or bucketId");
1720
- const bucketIds = apiVersion !== "v4" && req.bucketId !== void 0 ? Object.freeze([req.bucketId]) : normalizeKeyBucketIds(req);
2565
+ createKeyCapabilitiesOutsideGrant(authToken, requested) {
2566
+ const creator = authToken === void 0 ? void 0 : this.issuedTokens.get(authToken);
2567
+ if (creator === void 0) return this.strictAuth ? requested : [];
2568
+ if (creator.applicationKeyId === null) return [];
2569
+ const granted = new Set(creator.capabilities);
2570
+ return requested.filter((capability) => !granted.has(capability));
2571
+ }
2572
+ createKeyCreatorAccountId(authToken) {
2573
+ return (authToken === void 0 ? void 0 : this.issuedTokens.get(authToken))?.accountId ?? this.accountId;
2574
+ }
2575
+ createKey(req, version, authToken) {
2576
+ if (version === "v4" && hasOwnField(req, "bucketId")) return this.error(400, "bad_request", "bucketId is not accepted by v4 b2_create_key; use bucketIds");
2577
+ if (version !== "v4" && req.bucketId !== void 0 && req.bucketIds !== void 0) return this.error(400, "bad_request", "b2_create_key accepts either bucketIds or bucketId");
2578
+ const bucketIds = version !== "v4" && req.bucketId !== void 0 ? Object.freeze([req.bucketId]) : normalizeKeyBucketIds(req);
1721
2579
  const namePrefix = req.namePrefix === void 0 || req.namePrefix === "" ? null : req.namePrefix;
1722
- if (hasKeyManagementCapability(req.capabilities) && (bucketIds !== null || namePrefix !== null)) return this.error(400, "bad_request", "key-management capabilities are account-level and cannot be bucket or name-prefix scoped");
2580
+ const creatorAccountId = this.createKeyCreatorAccountId(authToken);
2581
+ if (req.accountId !== creatorAccountId) return this.error(400, "bad_request", "accountId must match authorized account");
2582
+ const keyNameError = validateCreateKeyName(req.keyName);
2583
+ if (keyNameError) return this.error(400, keyNameError.code, keyNameError.message);
2584
+ const keyName = req.keyName;
2585
+ const capabilitiesError = validateCreateKeyCapabilities(req.capabilities);
2586
+ if (capabilitiesError) return this.error(400, capabilitiesError.code, capabilitiesError.message);
2587
+ const capabilities = normalizeCreateKeyCapabilities(req.capabilities);
2588
+ const unauthorizedCapabilities = this.createKeyCapabilitiesOutsideGrant(authToken, capabilities);
2589
+ if (unauthorizedCapabilities.length > 0) return this.error(400, "bad_request", `requested capabilities exceed creator grant: ${unauthorizedCapabilities.join(", ")}`);
2590
+ if (hasKeyManagementCapability(capabilities) && (bucketIds !== null || namePrefix !== null)) return this.error(400, "bad_request", "key-management capabilities are account-level and cannot be bucket or name-prefix scoped");
1723
2591
  const kid = this.genId("sim_key");
1724
- const appKey = this.genId("sim_secret");
1725
- const expiration = req.validDurationInSeconds !== void 0 ? Date.now() + req.validDurationInSeconds * 1e3 : null;
1726
2592
  const stored = {
1727
2593
  applicationKeyId: kid,
1728
- keyName: req.keyName,
1729
- capabilities: req.capabilities,
1730
- accountId: req.accountId,
1731
- applicationKey: appKey,
2594
+ keyName,
2595
+ capabilities,
2596
+ accountId: creatorAccountId,
2597
+ applicationKey: this.genId("sim_secret"),
1732
2598
  bucketIds,
1733
2599
  namePrefix,
1734
- expirationTimestamp: expiration
2600
+ expirationTimestamp: req.validDurationInSeconds !== void 0 ? this.now() + req.validDurationInSeconds * 1e3 : null
1735
2601
  };
1736
2602
  this.keys.set(kid, stored);
1737
2603
  return {
@@ -1740,7 +2606,7 @@ var B2Simulator = class {
1740
2606
  keyName: stored.keyName,
1741
2607
  applicationKeyId: stored.applicationKeyId,
1742
2608
  applicationKey: stored.applicationKey,
1743
- capabilities: stored.capabilities,
2609
+ capabilities: cloneCapabilities(stored.capabilities),
1744
2610
  accountId: stored.accountId,
1745
2611
  expirationTimestamp: stored.expirationTimestamp,
1746
2612
  bucketIds: cloneBucketIds(stored.bucketIds),
@@ -1765,7 +2631,7 @@ var B2Simulator = class {
1765
2631
  keys: allKeys.slice(0, max).map((k) => ({
1766
2632
  keyName: k.keyName,
1767
2633
  applicationKeyId: k.applicationKeyId,
1768
- capabilities: k.capabilities,
2634
+ capabilities: cloneCapabilities(k.capabilities),
1769
2635
  accountId: k.accountId,
1770
2636
  expirationTimestamp: k.expirationTimestamp,
1771
2637
  bucketIds: cloneBucketIds(k.bucketIds),
@@ -1791,7 +2657,7 @@ var B2Simulator = class {
1791
2657
  body: {
1792
2658
  keyName: key.keyName,
1793
2659
  applicationKeyId: key.applicationKeyId,
1794
- capabilities: key.capabilities,
2660
+ capabilities: cloneCapabilities(key.capabilities),
1795
2661
  accountId: key.accountId,
1796
2662
  expirationTimestamp: key.expirationTimestamp,
1797
2663
  bucketIds: cloneBucketIds(key.bucketIds),
@@ -1801,31 +2667,45 @@ var B2Simulator = class {
1801
2667
  }
1802
2668
  };
1803
2669
  }
1804
- updateFileRetention(req) {
2670
+ updateFileRetention(req, authToken) {
1805
2671
  const found = this.findFile(req.fileId);
1806
2672
  if (found === null || found.stored.fileVersion.fileName !== req.fileName) return this.error(404, "file_not_present", "File not found");
2673
+ const fileLockError = this.requireFileLockEnabled(found.bucket);
2674
+ if (fileLockError !== null) return fileLockError;
2675
+ const fileRetention = parseFileRetentionValue(req.fileRetention, Date.now());
2676
+ if (fileRetention === null) return this.error(400, "bad_request", "fileRetention must use a valid mode and retainUntilTimestamp pair");
2677
+ const current = found.stored.fileVersion.fileRetention?.value;
2678
+ if (current?.mode === RetentionMode.Compliance && (fileRetention.mode !== RetentionMode.Compliance || isRetentionWeakened(current, fileRetention))) return this.error(400, "file_lock_compliance_protected", "Compliance-mode retention cannot be shortened or removed");
2679
+ if (requiresGovernanceBypass(current, fileRetention)) {
2680
+ const bypassError = this.requireGovernanceBypass(req, authToken, "Governance-mode retention cannot be shortened, removed, or converted without bypassGovernance");
2681
+ if (bypassError !== null) return bypassError;
2682
+ }
1807
2683
  found.versions[found.index] = {
1808
2684
  fileVersion: {
1809
2685
  ...found.stored.fileVersion,
1810
2686
  fileRetention: {
1811
2687
  isClientAuthorizedToRead: true,
1812
- value: req.fileRetention
2688
+ value: fileRetention
1813
2689
  }
1814
2690
  },
1815
- data: found.stored.data
2691
+ data: found.stored.data,
2692
+ serverSideEncryption: found.stored.serverSideEncryption
1816
2693
  };
1817
2694
  return {
1818
2695
  status: 200,
1819
2696
  body: {
1820
2697
  fileName: req.fileName,
1821
2698
  fileId: req.fileId,
1822
- fileRetention: req.fileRetention
2699
+ fileRetention
1823
2700
  }
1824
2701
  };
1825
2702
  }
1826
2703
  updateFileLegalHold(req) {
1827
2704
  const found = this.findFile(req.fileId);
1828
2705
  if (found === null || found.stored.fileVersion.fileName !== req.fileName) return this.error(404, "file_not_present", "File not found");
2706
+ const fileLockError = this.requireFileLockEnabled(found.bucket);
2707
+ if (fileLockError !== null) return fileLockError;
2708
+ if (req.legalHold !== LegalHoldValue.On && req.legalHold !== LegalHoldValue.Off) return this.error(400, "bad_request", "legalHold must be \"on\" or \"off\"");
1829
2709
  found.versions[found.index] = {
1830
2710
  fileVersion: {
1831
2711
  ...found.stored.fileVersion,
@@ -1834,7 +2714,8 @@ var B2Simulator = class {
1834
2714
  value: req.legalHold
1835
2715
  }
1836
2716
  },
1837
- data: found.stored.data
2717
+ data: found.stored.data,
2718
+ serverSideEncryption: found.stored.serverSideEncryption
1838
2719
  };
1839
2720
  return {
1840
2721
  status: 200,
@@ -1858,6 +2739,8 @@ var B2Simulator = class {
1858
2739
  }
1859
2740
  setBucketNotificationRules(req) {
1860
2741
  if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
2742
+ const rulesError = validateNotificationRules(req.eventNotificationRules);
2743
+ if (rulesError) return this.error(400, rulesError.code, rulesError.message);
1861
2744
  this.notificationRules.set(req.bucketId, req.eventNotificationRules);
1862
2745
  return {
1863
2746
  status: 200,
@@ -1915,11 +2798,23 @@ var B2Simulator = class {
1915
2798
  isClientAuthorizedToRead: true,
1916
2799
  value: params.legalHold ?? null
1917
2800
  },
1918
- replicationStatus: null,
2801
+ ...this.replicationStatusMetadataFor(params.replicationStatus),
1919
2802
  serverSideEncryption: publicServerSideEncryption(params.serverSideEncryption ?? { mode: EncryptionMode.None }),
1920
- uploadTimestamp: this.monotonicTimestamp()
2803
+ uploadTimestamp: params.uploadTimestamp ?? this.monotonicTimestamp()
1921
2804
  };
1922
2805
  }
2806
+ newReplicationStatusField(bucketId, fileName) {
2807
+ return this.replicationStatusMetadataFor(this.replicationStatusForNewFile(bucketId, fileName));
2808
+ }
2809
+ replicationStatusMetadataFor(replicationStatus) {
2810
+ return replicationStatus === void 0 ? {} : { replicationStatus };
2811
+ }
2812
+ replicationStatusForNewFile(bucketId, fileName) {
2813
+ return this.matchingEnabledReplicationRules(bucketId, fileName).length > 0 ? "PENDING" : void 0;
2814
+ }
2815
+ matchingEnabledReplicationRules(bucketId, fileName) {
2816
+ return (this.buckets.get(bucketId)?.info.replicationConfiguration.asReplicationSource?.replicationRules)?.filter((rule) => rule.isEnabled && fileName.startsWith(rule.fileNamePrefix)) ?? [];
2817
+ }
1923
2818
  error(status, code, message) {
1924
2819
  return {
1925
2820
  status,
@@ -1947,8 +2842,7 @@ var B2Simulator = class {
1947
2842
  * used to match against `EventNotificationRule.eventTypes` globs.
1948
2843
  */
1949
2844
  firePostUploadHooks(fileVersion, bucketId, eventType) {
1950
- const bucket = this.buckets.get(bucketId);
1951
- if (!bucket) return;
2845
+ if (!this.buckets.get(bucketId)) return;
1952
2846
  if (this.onWebhookDeliver !== void 0) {
1953
2847
  const rules = this.notificationRules.get(bucketId) ?? [];
1954
2848
  for (const rule of rules) {
@@ -1962,18 +2856,13 @@ var B2Simulator = class {
1962
2856
  }));
1963
2857
  }
1964
2858
  }
1965
- if (this.onReplicate !== void 0) {
1966
- const sourceRules = bucket.info.replicationConfiguration.asReplicationSource?.replicationRules ?? [];
1967
- for (const rule of sourceRules) {
1968
- if (!rule.isEnabled) continue;
1969
- if (rule.fileNamePrefix && !fileVersion.fileName.startsWith(rule.fileNamePrefix)) continue;
1970
- const hook = this.onReplicate;
1971
- this.dispatchHook("replication", () => hook({
1972
- sourceFileVersion: fileVersion,
1973
- sourceBucketId: bucketId,
1974
- destinationBucketId: rule.destinationBucketId
1975
- }));
1976
- }
2859
+ if (this.onReplicate !== void 0) for (const rule of this.matchingEnabledReplicationRules(bucketId, fileVersion.fileName)) {
2860
+ const hook = this.onReplicate;
2861
+ this.dispatchHook("replication", () => hook({
2862
+ sourceFileVersion: fileVersion,
2863
+ sourceBucketId: bucketId,
2864
+ destinationBucketId: rule.destinationBucketId
2865
+ }));
1977
2866
  }
1978
2867
  }
1979
2868
  /**
@@ -2057,6 +2946,83 @@ function buildFaultResponse(fault) {
2057
2946
  arrayBuffer: () => Promise.resolve(utf8Encoder.encode(body).buffer)
2058
2947
  };
2059
2948
  }
2949
+ function rawUrlPathContainsLiteralBackslash(rawUrl) {
2950
+ const schemeEnd = rawUrl.indexOf("://");
2951
+ const authorityStart = schemeEnd === -1 ? 0 : schemeEnd + 3;
2952
+ const urlEndCandidates = [rawUrl.indexOf("?", authorityStart), rawUrl.indexOf("#", authorityStart)].filter((index) => index !== -1);
2953
+ const urlEnd = urlEndCandidates.length === 0 ? rawUrl.length : Math.min(...urlEndCandidates);
2954
+ const pathStartCandidates = [rawUrl.indexOf("/", authorityStart), rawUrl.indexOf("\\", authorityStart)].filter((index) => index !== -1 && index < urlEnd);
2955
+ if (pathStartCandidates.length === 0) return false;
2956
+ const pathStart = Math.min(...pathStartCandidates);
2957
+ const pathEndCandidates = [rawUrl.indexOf("?", pathStart), rawUrl.indexOf("#", pathStart)].filter((index) => index !== -1);
2958
+ const pathEnd = pathEndCandidates.length === 0 ? rawUrl.length : Math.min(...pathEndCandidates);
2959
+ return rawUrl.slice(pathStart, pathEnd).includes("\\");
2960
+ }
2961
+ function badRequestJson(message) {
2962
+ return {
2963
+ status: 400,
2964
+ body: {
2965
+ status: 400,
2966
+ code: "bad_request",
2967
+ message
2968
+ }
2969
+ };
2970
+ }
2971
+ function checkedUploadBody(data, expectedLength) {
2972
+ if (expectedLength !== null && data.byteLength !== expectedLength) return { error: badRequestJson(contentLengthMismatchMessage(expectedLength, data.byteLength)) };
2973
+ return { data };
2974
+ }
2975
+ function copyBytes(bytes) {
2976
+ const copy = new Uint8Array(bytes.byteLength);
2977
+ copy.set(bytes);
2978
+ return copy;
2979
+ }
2980
+ function concatenateChunks(chunks, totalLength) {
2981
+ if (chunks.length === 0) return /* @__PURE__ */ new Uint8Array(0);
2982
+ if (chunks.length === 1) return copyBytes(chunks[0]);
2983
+ const data = new Uint8Array(totalLength);
2984
+ let offset = 0;
2985
+ for (const chunk of chunks) {
2986
+ data.set(chunk, offset);
2987
+ offset += chunk.byteLength;
2988
+ }
2989
+ return data;
2990
+ }
2991
+ async function readStreamingUploadBody(stream, expectedLength) {
2992
+ const reader = stream.getReader();
2993
+ const chunks = [];
2994
+ let totalLength = 0;
2995
+ try {
2996
+ while (true) {
2997
+ const { done, value } = await reader.read();
2998
+ if (done) break;
2999
+ if (value === void 0) continue;
3000
+ const chunk = value instanceof Uint8Array ? value : new Uint8Array(value);
3001
+ totalLength += chunk.byteLength;
3002
+ if (expectedLength !== null && totalLength > expectedLength) {
3003
+ await reader.cancel().catch(() => void 0);
3004
+ return { error: badRequestJson(contentLengthMismatchMessage(expectedLength, totalLength)) };
3005
+ }
3006
+ chunks.push(chunk);
3007
+ }
3008
+ } finally {
3009
+ reader.releaseLock();
3010
+ }
3011
+ if (expectedLength !== null && totalLength !== expectedLength) return { error: badRequestJson(contentLengthMismatchMessage(expectedLength, totalLength)) };
3012
+ return { data: concatenateChunks(chunks, totalLength) };
3013
+ }
3014
+ async function readUploadBody(body, expectedLength) {
3015
+ if (body instanceof ReadableStream) return readStreamingUploadBody(body, expectedLength);
3016
+ if (body instanceof ArrayBuffer) return checkedUploadBody(new Uint8Array(body), expectedLength);
3017
+ if (ArrayBuffer.isView(body)) {
3018
+ const view = body;
3019
+ return checkedUploadBody(copyBytes(new Uint8Array(view.buffer, view.byteOffset, view.byteLength)), expectedLength);
3020
+ }
3021
+ if (typeof Blob !== "undefined" && body instanceof Blob) {
3022
+ if (expectedLength !== null && body.size !== expectedLength) return { error: badRequestJson(contentLengthMismatchMessage(expectedLength, body.size)) };
3023
+ }
3024
+ return checkedUploadBody(new Uint8Array(await new Response(body).arrayBuffer()), expectedLength);
3025
+ }
2060
3026
  var SimulatorTransport = class {
2061
3027
  sim;
2062
3028
  constructor(sim) {
@@ -2066,6 +3032,12 @@ var SimulatorTransport = class {
2066
3032
  const url = request.url;
2067
3033
  const fault = this.sim.consumeMatchingFault(url);
2068
3034
  if (fault !== null) return buildFaultResponse(fault);
3035
+ if (rawUrlPathContainsLiteralBackslash(url)) return buildFaultResponse({
3036
+ on: "\\",
3037
+ status: 400,
3038
+ code: "bad_request",
3039
+ message: "URL path must not contain literal backslashes"
3040
+ });
2069
3041
  const headers = {};
2070
3042
  if (request.headers) for (const [k, v] of Object.entries(request.headers)) headers[k.toLowerCase()] = v;
2071
3043
  const parsedUrl = new URL(url);
@@ -2073,10 +3045,15 @@ var SimulatorTransport = class {
2073
3045
  const isUpload = endpoint === "b2_upload_file" || endpoint === "b2_upload_part";
2074
3046
  if (parsedUrl.pathname.includes("b2_download_file_by_id") || parsedUrl.pathname.includes("/file/")) {
2075
3047
  const method = request.method === "HEAD" ? "HEAD" : "GET";
2076
- const result = this.sim.handleDownload(parsedUrl.pathname + parsedUrl.search, headers, method);
3048
+ const result = await this.sim.handleDownload(parsedUrl.pathname + parsedUrl.search, headers, method);
2077
3049
  const data = result.data ?? /* @__PURE__ */ new Uint8Array(0);
2078
3050
  const responseHeaders = new Headers(result.headers);
2079
3051
  responseHeaders.set("Content-Type", result.headers["Content-Type"] ?? "application/octet-stream");
3052
+ let decodedText = null;
3053
+ const text = () => {
3054
+ decodedText ??= utf8Decoder.decode(data);
3055
+ return decodedText;
3056
+ };
2080
3057
  const body = method === "HEAD" || result.data === null ? null : new ReadableStream({ start(controller) {
2081
3058
  controller.enqueue(data);
2082
3059
  controller.close();
@@ -2085,15 +3062,25 @@ var SimulatorTransport = class {
2085
3062
  status: result.status,
2086
3063
  headers: responseHeaders,
2087
3064
  body,
2088
- json: () => Promise.reject(/* @__PURE__ */ new Error("Download response is not JSON")),
2089
- text: () => Promise.resolve(utf8Decoder.decode(data)),
3065
+ json: () => {
3066
+ try {
3067
+ return Promise.resolve(JSON.parse(text()));
3068
+ } catch {
3069
+ return Promise.reject(/* @__PURE__ */ new Error("Download response is not JSON"));
3070
+ }
3071
+ },
3072
+ text: () => Promise.resolve(text()),
2090
3073
  arrayBuffer: () => Promise.resolve(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength))
2091
3074
  };
2092
3075
  }
2093
3076
  let result;
2094
- if (isUpload && request.body) {
2095
- const data = new Uint8Array(request.body instanceof ArrayBuffer ? request.body : request.body instanceof Uint8Array ? request.body.buffer.slice(request.body.byteOffset, request.body.byteOffset + request.body.byteLength) : await new Response(request.body).arrayBuffer());
2096
- result = await this.sim.handleUpload(url, headers, data);
3077
+ if (isUpload) {
3078
+ const parsedContentLength = parseContentLengthHeader(headers);
3079
+ if (parsedContentLength.kind === "error") result = badRequestJson(parsedContentLength.message);
3080
+ else {
3081
+ const uploadBody = await readUploadBody(request.body ?? /* @__PURE__ */ new Uint8Array(0), parsedContentLength.kind === "ok" ? parsedContentLength.expectedLength : null);
3082
+ result = "error" in uploadBody ? uploadBody.error : await this.sim.handleUpload(url, headers, uploadBody.data);
3083
+ }
2097
3084
  } else {
2098
3085
  let body = null;
2099
3086
  if (request.body) {
@@ -2103,7 +3090,7 @@ var SimulatorTransport = class {
2103
3090
  } catch {
2104
3091
  body = text;
2105
3092
  }
2106
- }
3093
+ } else if (request.method.toUpperCase() === "GET" && isPartnerQueryEndpoint(apiPathParts(parsedUrl.pathname).endpoint)) body = queryParamsBody(parsedUrl.searchParams);
2107
3094
  result = await this.sim.handleRequest(request.method, parsedUrl.origin, parsedUrl.pathname, headers, body);
2108
3095
  }
2109
3096
  const responseBody = JSON.stringify(result.body);
@@ -2121,6 +3108,6 @@ var SimulatorTransport = class {
2121
3108
  }
2122
3109
  };
2123
3110
  //#endregion
2124
- export { B2Simulator, BUCKET_INFO_MAX_KEYS, BUCKET_INFO_VALUE_MAX, BUCKET_NAME_MAX, BUCKET_NAME_MIN, FILE_INFO_TOTAL_MAX, FILE_INFO_VALUE_MAX, FILE_NAME_MAX_BYTES, LIST_ENDPOINT_CAPS };
3111
+ export { B2Simulator, BUCKET_INFO_MAX_KEYS, BUCKET_INFO_VALUE_MAX, BUCKET_NAME_MAX, BUCKET_NAME_MIN, DOWNLOAD_AUTH_DURATION_MAX_SECONDS, DOWNLOAD_AUTH_DURATION_MIN_SECONDS, FILE_INFO_TOTAL_MAX, FILE_INFO_VALUE_MAX, FILE_NAME_MAX_BYTES, KEY_NAME_MAX, KEY_NAME_MIN, LIST_ENDPOINT_CAPS };
2125
3112
 
2126
3113
  //# sourceMappingURL=index.js.map