@backblaze-labs/b2-sdk 0.2.0 → 0.4.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 (590) hide show
  1. package/CHANGELOG.md +86 -1
  2. package/README.md +38 -7
  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/auth/reauth-coalescer.cjs +65 -0
  7. package/dist/auth/reauth-coalescer.cjs.map +1 -0
  8. package/dist/auth/reauth-coalescer.d.cts +34 -0
  9. package/dist/auth/reauth-coalescer.d.cts.map +1 -0
  10. package/dist/auth/reauth-coalescer.d.ts +34 -0
  11. package/dist/auth/reauth-coalescer.d.ts.map +1 -0
  12. package/dist/auth/reauth-coalescer.js +65 -0
  13. package/dist/auth/reauth-coalescer.js.map +1 -0
  14. package/dist/backup/client.cjs +206 -0
  15. package/dist/backup/client.cjs.map +1 -0
  16. package/dist/backup/client.d.cts +238 -0
  17. package/dist/backup/client.d.cts.map +1 -0
  18. package/dist/backup/client.d.ts +238 -0
  19. package/dist/backup/client.d.ts.map +1 -0
  20. package/dist/backup/client.js +206 -0
  21. package/dist/backup/client.js.map +1 -0
  22. package/dist/backup/index.cjs +15 -0
  23. package/dist/backup/index.d.cts +22 -0
  24. package/dist/backup/index.d.cts.map +1 -0
  25. package/dist/backup/index.d.ts +22 -0
  26. package/dist/backup/index.d.ts.map +1 -0
  27. package/dist/backup/index.js +7 -0
  28. package/dist/backup/raw.cjs +168 -0
  29. package/dist/backup/raw.cjs.map +1 -0
  30. package/dist/backup/raw.d.cts +122 -0
  31. package/dist/backup/raw.d.cts.map +1 -0
  32. package/dist/backup/raw.d.ts +122 -0
  33. package/dist/backup/raw.d.ts.map +1 -0
  34. package/dist/backup/raw.js +168 -0
  35. package/dist/backup/raw.js.map +1 -0
  36. package/dist/bucket-validation.cjs +111 -0
  37. package/dist/bucket-validation.cjs.map +1 -0
  38. package/dist/bucket-validation.d.cts +27 -0
  39. package/dist/bucket-validation.d.cts.map +1 -0
  40. package/dist/bucket-validation.d.ts +27 -0
  41. package/dist/bucket-validation.d.ts.map +1 -0
  42. package/dist/bucket-validation.js +109 -0
  43. package/dist/bucket-validation.js.map +1 -0
  44. package/dist/bucket.cjs +125 -74
  45. package/dist/bucket.cjs.map +1 -1
  46. package/dist/bucket.d.cts +156 -61
  47. package/dist/bucket.d.cts.map +1 -1
  48. package/dist/bucket.d.ts +156 -61
  49. package/dist/bucket.d.ts.map +1 -1
  50. package/dist/bucket.js +125 -74
  51. package/dist/bucket.js.map +1 -1
  52. package/dist/client.cjs +29 -8
  53. package/dist/client.cjs.map +1 -1
  54. package/dist/client.d.cts +30 -9
  55. package/dist/client.d.cts.map +1 -1
  56. package/dist/client.d.ts +30 -9
  57. package/dist/client.d.ts.map +1 -1
  58. package/dist/client.js +29 -8
  59. package/dist/client.js.map +1 -1
  60. package/dist/copy/large.cjs +5 -3
  61. package/dist/copy/large.cjs.map +1 -1
  62. package/dist/copy/large.d.cts.map +1 -1
  63. package/dist/copy/large.d.ts.map +1 -1
  64. package/dist/copy/large.js +5 -3
  65. package/dist/copy/large.js.map +1 -1
  66. package/dist/download/checksum.cjs +6 -2
  67. package/dist/download/checksum.cjs.map +1 -1
  68. package/dist/download/checksum.js +6 -2
  69. package/dist/download/checksum.js.map +1 -1
  70. package/dist/download/parallel.cjs +52 -15
  71. package/dist/download/parallel.cjs.map +1 -1
  72. package/dist/download/parallel.d.cts +8 -1
  73. package/dist/download/parallel.d.cts.map +1 -1
  74. package/dist/download/parallel.d.ts +8 -1
  75. package/dist/download/parallel.d.ts.map +1 -1
  76. package/dist/download/parallel.js +52 -15
  77. package/dist/download/parallel.js.map +1 -1
  78. package/dist/download/single.cjs +179 -19
  79. package/dist/download/single.cjs.map +1 -1
  80. package/dist/download/single.d.cts +10 -4
  81. package/dist/download/single.d.cts.map +1 -1
  82. package/dist/download/single.d.ts +10 -4
  83. package/dist/download/single.d.ts.map +1 -1
  84. package/dist/download/single.js +179 -19
  85. package/dist/download/single.js.map +1 -1
  86. package/dist/errors/index.cjs +196 -3
  87. package/dist/errors/index.cjs.map +1 -1
  88. package/dist/errors/index.d.cts +134 -0
  89. package/dist/errors/index.d.cts.map +1 -1
  90. package/dist/errors/index.d.ts +134 -0
  91. package/dist/errors/index.d.ts.map +1 -1
  92. package/dist/errors/index.js +183 -4
  93. package/dist/errors/index.js.map +1 -1
  94. package/dist/http/non-idempotent-mutations.cjs +38 -0
  95. package/dist/http/non-idempotent-mutations.cjs.map +1 -0
  96. package/dist/http/non-idempotent-mutations.d.cts +25 -0
  97. package/dist/http/non-idempotent-mutations.d.cts.map +1 -0
  98. package/dist/http/non-idempotent-mutations.d.ts +25 -0
  99. package/dist/http/non-idempotent-mutations.d.ts.map +1 -0
  100. package/dist/http/non-idempotent-mutations.js +35 -0
  101. package/dist/http/non-idempotent-mutations.js.map +1 -0
  102. package/dist/http/retry.cjs +1 -1
  103. package/dist/http/retry.cjs.map +1 -1
  104. package/dist/http/retry.js +1 -1
  105. package/dist/http/retry.js.map +1 -1
  106. package/dist/http/transport.cjs +62 -23
  107. package/dist/http/transport.cjs.map +1 -1
  108. package/dist/http/transport.d.cts +30 -3
  109. package/dist/http/transport.d.cts.map +1 -1
  110. package/dist/http/transport.d.ts +30 -3
  111. package/dist/http/transport.d.ts.map +1 -1
  112. package/dist/http/transport.js +60 -22
  113. package/dist/http/transport.js.map +1 -1
  114. package/dist/http/user-agent.cjs +12 -1
  115. package/dist/http/user-agent.cjs.map +1 -1
  116. package/dist/http/user-agent.d.cts +8 -0
  117. package/dist/http/user-agent.d.cts.map +1 -1
  118. package/dist/http/user-agent.d.ts +8 -0
  119. package/dist/http/user-agent.d.ts.map +1 -1
  120. package/dist/http/user-agent.js +13 -3
  121. package/dist/http/user-agent.js.map +1 -1
  122. package/dist/index.cjs +50 -2
  123. package/dist/index.d.cts +15 -12
  124. package/dist/index.d.cts.map +1 -1
  125. package/dist/index.d.ts +15 -12
  126. package/dist/index.d.ts.map +1 -1
  127. package/dist/index.js +13 -10
  128. package/dist/notifications/index.cjs +3 -0
  129. package/dist/notifications/index.d.cts +2 -0
  130. package/dist/notifications/index.d.cts.map +1 -1
  131. package/dist/notifications/index.d.ts +2 -0
  132. package/dist/notifications/index.d.ts.map +1 -1
  133. package/dist/notifications/index.js +2 -1
  134. package/dist/object.cjs +13 -2
  135. package/dist/object.cjs.map +1 -1
  136. package/dist/object.d.cts +11 -3
  137. package/dist/object.d.cts.map +1 -1
  138. package/dist/object.d.ts +11 -3
  139. package/dist/object.d.ts.map +1 -1
  140. package/dist/object.js +13 -2
  141. package/dist/object.js.map +1 -1
  142. package/dist/partner/account-info.d.cts +43 -0
  143. package/dist/partner/account-info.d.cts.map +1 -0
  144. package/dist/partner/account-info.d.ts +43 -0
  145. package/dist/partner/account-info.d.ts.map +1 -0
  146. package/dist/partner/auth-clone.cjs +64 -0
  147. package/dist/partner/auth-clone.cjs.map +1 -0
  148. package/dist/partner/auth-clone.d.cts +27 -0
  149. package/dist/partner/auth-clone.d.cts.map +1 -0
  150. package/dist/partner/auth-clone.d.ts +27 -0
  151. package/dist/partner/auth-clone.d.ts.map +1 -0
  152. package/dist/partner/auth-clone.js +63 -0
  153. package/dist/partner/auth-clone.js.map +1 -0
  154. package/dist/partner/auth-core.cjs +156 -0
  155. package/dist/partner/auth-core.cjs.map +1 -0
  156. package/dist/partner/auth-core.d.cts +111 -0
  157. package/dist/partner/auth-core.d.cts.map +1 -0
  158. package/dist/partner/auth-core.d.ts +111 -0
  159. package/dist/partner/auth-core.d.ts.map +1 -0
  160. package/dist/partner/auth-core.js +156 -0
  161. package/dist/partner/auth-core.js.map +1 -0
  162. package/dist/partner/auth-shape.cjs +36 -0
  163. package/dist/partner/auth-shape.cjs.map +1 -0
  164. package/dist/partner/auth-shape.d.cts +12 -0
  165. package/dist/partner/auth-shape.d.cts.map +1 -0
  166. package/dist/partner/auth-shape.d.ts +12 -0
  167. package/dist/partner/auth-shape.d.ts.map +1 -0
  168. package/dist/partner/auth-shape.js +36 -0
  169. package/dist/partner/auth-shape.js.map +1 -0
  170. package/dist/partner/client.cjs +270 -0
  171. package/dist/partner/client.cjs.map +1 -0
  172. package/dist/partner/client.d.cts +357 -0
  173. package/dist/partner/client.d.cts.map +1 -0
  174. package/dist/partner/client.d.ts +357 -0
  175. package/dist/partner/client.d.ts.map +1 -0
  176. package/dist/partner/client.js +270 -0
  177. package/dist/partner/client.js.map +1 -0
  178. package/dist/partner/endpoint-url.cjs +109 -0
  179. package/dist/partner/endpoint-url.cjs.map +1 -0
  180. package/dist/partner/endpoint-url.d.cts +74 -0
  181. package/dist/partner/endpoint-url.d.cts.map +1 -0
  182. package/dist/partner/endpoint-url.d.ts +74 -0
  183. package/dist/partner/endpoint-url.d.ts.map +1 -0
  184. package/dist/partner/endpoint-url.js +106 -0
  185. package/dist/partner/endpoint-url.js.map +1 -0
  186. package/dist/partner/in-memory.cjs +135 -0
  187. package/dist/partner/in-memory.cjs.map +1 -0
  188. package/dist/partner/in-memory.d.cts +99 -0
  189. package/dist/partner/in-memory.d.cts.map +1 -0
  190. package/dist/partner/in-memory.d.ts +99 -0
  191. package/dist/partner/in-memory.d.ts.map +1 -0
  192. package/dist/partner/in-memory.js +135 -0
  193. package/dist/partner/in-memory.js.map +1 -0
  194. package/dist/partner/index.cjs +27 -0
  195. package/dist/partner/index.d.cts +32 -0
  196. package/dist/partner/index.d.cts.map +1 -0
  197. package/dist/partner/index.d.ts +32 -0
  198. package/dist/partner/index.d.ts.map +1 -0
  199. package/dist/partner/index.js +9 -0
  200. package/dist/partner/raw.cjs +400 -0
  201. package/dist/partner/raw.cjs.map +1 -0
  202. package/dist/partner/raw.d.cts +220 -0
  203. package/dist/partner/raw.d.cts.map +1 -0
  204. package/dist/partner/raw.d.ts +220 -0
  205. package/dist/partner/raw.d.ts.map +1 -0
  206. package/dist/partner/raw.js +398 -0
  207. package/dist/partner/raw.js.map +1 -0
  208. package/dist/partner/redaction-placeholders.cjs +10 -0
  209. package/dist/partner/redaction-placeholders.cjs.map +1 -0
  210. package/dist/partner/redaction-placeholders.d.cts +5 -0
  211. package/dist/partner/redaction-placeholders.d.cts.map +1 -0
  212. package/dist/partner/redaction-placeholders.d.ts +5 -0
  213. package/dist/partner/redaction-placeholders.d.ts.map +1 -0
  214. package/dist/partner/redaction-placeholders.js +9 -0
  215. package/dist/partner/redaction-placeholders.js.map +1 -0
  216. package/dist/partner/redaction.cjs +292 -0
  217. package/dist/partner/redaction.cjs.map +1 -0
  218. package/dist/partner/redaction.d.cts +158 -0
  219. package/dist/partner/redaction.d.cts.map +1 -0
  220. package/dist/partner/redaction.d.ts +158 -0
  221. package/dist/partner/redaction.d.ts.map +1 -0
  222. package/dist/partner/redaction.js +281 -0
  223. package/dist/partner/redaction.js.map +1 -0
  224. package/dist/raw/encoding.cjs +29 -10
  225. package/dist/raw/encoding.cjs.map +1 -1
  226. package/dist/raw/encoding.d.cts +3 -2
  227. package/dist/raw/encoding.d.cts.map +1 -1
  228. package/dist/raw/encoding.d.ts +3 -2
  229. package/dist/raw/encoding.d.ts.map +1 -1
  230. package/dist/raw/encoding.js +29 -10
  231. package/dist/raw/encoding.js.map +1 -1
  232. package/dist/raw/index.cjs +131 -44
  233. package/dist/raw/index.cjs.map +1 -1
  234. package/dist/raw/index.d.cts +21 -15
  235. package/dist/raw/index.d.cts.map +1 -1
  236. package/dist/raw/index.d.ts +21 -15
  237. package/dist/raw/index.d.ts.map +1 -1
  238. package/dist/raw/index.js +114 -45
  239. package/dist/raw/index.js.map +1 -1
  240. package/dist/raw/url.cjs +111 -0
  241. package/dist/raw/url.cjs.map +1 -0
  242. package/dist/raw/url.d.cts +41 -0
  243. package/dist/raw/url.d.cts.map +1 -0
  244. package/dist/raw/url.d.ts +41 -0
  245. package/dist/raw/url.d.ts.map +1 -0
  246. package/dist/raw/url.js +110 -0
  247. package/dist/raw/url.js.map +1 -0
  248. package/dist/s3/sigv4.cjs +15 -10
  249. package/dist/s3/sigv4.cjs.map +1 -1
  250. package/dist/s3/sigv4.js +15 -10
  251. package/dist/s3/sigv4.js.map +1 -1
  252. package/dist/simulator/index.cjs +1579 -257
  253. package/dist/simulator/index.cjs.map +1 -1
  254. package/dist/simulator/index.d.cts +124 -11
  255. package/dist/simulator/index.d.cts.map +1 -1
  256. package/dist/simulator/index.d.ts +124 -11
  257. package/dist/simulator/index.d.ts.map +1 -1
  258. package/dist/simulator/index.js +1568 -262
  259. package/dist/simulator/index.js.map +1 -1
  260. package/dist/simulator/partner-capabilities.cjs +77 -0
  261. package/dist/simulator/partner-capabilities.cjs.map +1 -0
  262. package/dist/simulator/partner-capabilities.d.cts +53 -0
  263. package/dist/simulator/partner-capabilities.d.cts.map +1 -0
  264. package/dist/simulator/partner-capabilities.d.ts +53 -0
  265. package/dist/simulator/partner-capabilities.d.ts.map +1 -0
  266. package/dist/simulator/partner-capabilities.js +73 -0
  267. package/dist/simulator/partner-capabilities.js.map +1 -0
  268. package/dist/simulator/partner.cjs +713 -0
  269. package/dist/simulator/partner.cjs.map +1 -0
  270. package/dist/simulator/partner.d.cts +186 -0
  271. package/dist/simulator/partner.d.cts.map +1 -0
  272. package/dist/simulator/partner.d.ts +186 -0
  273. package/dist/simulator/partner.d.ts.map +1 -0
  274. package/dist/simulator/partner.js +713 -0
  275. package/dist/simulator/partner.js.map +1 -0
  276. package/dist/simulator/validation.cjs +732 -25
  277. package/dist/simulator/validation.cjs.map +1 -1
  278. package/dist/simulator/validation.d.cts +145 -6
  279. package/dist/simulator/validation.d.cts.map +1 -1
  280. package/dist/simulator/validation.d.ts +145 -6
  281. package/dist/simulator/validation.d.ts.map +1 -1
  282. package/dist/simulator/validation.js +705 -26
  283. package/dist/simulator/validation.js.map +1 -1
  284. package/dist/streams/file-source.cjs +2 -2
  285. package/dist/streams/file-source.cjs.map +1 -1
  286. package/dist/streams/file-source.js +2 -2
  287. package/dist/streams/file-source.js.map +1 -1
  288. package/dist/streams/hash.cjs +246 -13
  289. package/dist/streams/hash.cjs.map +1 -1
  290. package/dist/streams/hash.d.cts +37 -12
  291. package/dist/streams/hash.d.cts.map +1 -1
  292. package/dist/streams/hash.d.ts +37 -12
  293. package/dist/streams/hash.d.ts.map +1 -1
  294. package/dist/streams/hash.js +246 -15
  295. package/dist/streams/hash.js.map +1 -1
  296. package/dist/streams/index.cjs +3 -1
  297. package/dist/streams/index.d.cts +1 -1
  298. package/dist/streams/index.d.cts.map +1 -1
  299. package/dist/streams/index.d.ts +1 -1
  300. package/dist/streams/index.d.ts.map +1 -1
  301. package/dist/streams/index.js +3 -3
  302. package/dist/streams/progress.cjs +63 -4
  303. package/dist/streams/progress.cjs.map +1 -1
  304. package/dist/streams/progress.d.cts +28 -2
  305. package/dist/streams/progress.d.cts.map +1 -1
  306. package/dist/streams/progress.d.ts +28 -2
  307. package/dist/streams/progress.d.ts.map +1 -1
  308. package/dist/streams/progress.js +63 -4
  309. package/dist/streams/progress.js.map +1 -1
  310. package/dist/streams/source.cjs +2 -1
  311. package/dist/streams/source.cjs.map +1 -1
  312. package/dist/streams/source.js +2 -1
  313. package/dist/streams/source.js.map +1 -1
  314. package/dist/sync/b2-sha1-reader.cjs +6 -2
  315. package/dist/sync/b2-sha1-reader.cjs.map +1 -1
  316. package/dist/sync/b2-sha1-reader.d.cts.map +1 -1
  317. package/dist/sync/b2-sha1-reader.d.ts.map +1 -1
  318. package/dist/sync/b2-sha1-reader.js +6 -2
  319. package/dist/sync/b2-sha1-reader.js.map +1 -1
  320. package/dist/sync/download-staging.cjs +156 -43
  321. package/dist/sync/download-staging.cjs.map +1 -1
  322. package/dist/sync/download-staging.d.cts +26 -2
  323. package/dist/sync/download-staging.d.cts.map +1 -1
  324. package/dist/sync/download-staging.d.ts +26 -2
  325. package/dist/sync/download-staging.d.ts.map +1 -1
  326. package/dist/sync/download-staging.js +155 -44
  327. package/dist/sync/download-staging.js.map +1 -1
  328. package/dist/sync/filters.cjs.map +1 -1
  329. package/dist/sync/filters.js.map +1 -1
  330. package/dist/sync/index.d.cts +1 -1
  331. package/dist/sync/index.d.cts.map +1 -1
  332. package/dist/sync/index.d.ts +1 -1
  333. package/dist/sync/index.d.ts.map +1 -1
  334. package/dist/sync/local-file-io.cjs +15 -6
  335. package/dist/sync/local-file-io.cjs.map +1 -1
  336. package/dist/sync/local-file-io.d.cts +3 -0
  337. package/dist/sync/local-file-io.d.cts.map +1 -1
  338. package/dist/sync/local-file-io.d.ts +3 -0
  339. package/dist/sync/local-file-io.d.ts.map +1 -1
  340. package/dist/sync/local-file-io.js +15 -6
  341. package/dist/sync/local-file-io.js.map +1 -1
  342. package/dist/sync/local-sha1.cjs +6 -15
  343. package/dist/sync/local-sha1.cjs.map +1 -1
  344. package/dist/sync/local-sha1.d.cts +0 -8
  345. package/dist/sync/local-sha1.d.cts.map +1 -1
  346. package/dist/sync/local-sha1.d.ts +0 -8
  347. package/dist/sync/local-sha1.d.ts.map +1 -1
  348. package/dist/sync/local-sha1.js +7 -15
  349. package/dist/sync/local-sha1.js.map +1 -1
  350. package/dist/sync/pairing.cjs +1 -0
  351. package/dist/sync/pairing.cjs.map +1 -1
  352. package/dist/sync/pairing.js +1 -0
  353. package/dist/sync/pairing.js.map +1 -1
  354. package/dist/sync/policies/compare.cjs +5 -3
  355. package/dist/sync/policies/compare.cjs.map +1 -1
  356. package/dist/sync/policies/compare.d.cts.map +1 -1
  357. package/dist/sync/policies/compare.d.ts.map +1 -1
  358. package/dist/sync/policies/compare.js +6 -4
  359. package/dist/sync/policies/compare.js.map +1 -1
  360. package/dist/sync/policies/index.cjs +4 -10
  361. package/dist/sync/policies/index.cjs.map +1 -1
  362. package/dist/sync/policies/index.js +4 -10
  363. package/dist/sync/policies/index.js.map +1 -1
  364. package/dist/sync/scanners/b2.cjs +4 -2
  365. package/dist/sync/scanners/b2.cjs.map +1 -1
  366. package/dist/sync/scanners/b2.d.cts.map +1 -1
  367. package/dist/sync/scanners/b2.d.ts.map +1 -1
  368. package/dist/sync/scanners/b2.js +4 -2
  369. package/dist/sync/scanners/b2.js.map +1 -1
  370. package/dist/sync/scanners/local.cjs +150 -33
  371. package/dist/sync/scanners/local.cjs.map +1 -1
  372. package/dist/sync/scanners/local.d.cts +6 -0
  373. package/dist/sync/scanners/local.d.cts.map +1 -1
  374. package/dist/sync/scanners/local.d.ts +6 -0
  375. package/dist/sync/scanners/local.d.ts.map +1 -1
  376. package/dist/sync/scanners/local.js +150 -33
  377. package/dist/sync/scanners/local.js.map +1 -1
  378. package/dist/sync/synchronizer.cjs +79 -31
  379. package/dist/sync/synchronizer.cjs.map +1 -1
  380. package/dist/sync/synchronizer.d.cts +1 -0
  381. package/dist/sync/synchronizer.d.cts.map +1 -1
  382. package/dist/sync/synchronizer.d.ts +1 -0
  383. package/dist/sync/synchronizer.d.ts.map +1 -1
  384. package/dist/sync/synchronizer.js +79 -31
  385. package/dist/sync/synchronizer.js.map +1 -1
  386. package/dist/sync/types.d.cts +19 -3
  387. package/dist/sync/types.d.cts.map +1 -1
  388. package/dist/sync/types.d.ts +19 -3
  389. package/dist/sync/types.d.ts.map +1 -1
  390. package/dist/types/auth.cjs +4 -0
  391. package/dist/types/auth.cjs.map +1 -1
  392. package/dist/types/auth.d.cts +4 -0
  393. package/dist/types/auth.d.cts.map +1 -1
  394. package/dist/types/auth.d.ts +4 -0
  395. package/dist/types/auth.d.ts.map +1 -1
  396. package/dist/types/auth.js +4 -0
  397. package/dist/types/auth.js.map +1 -1
  398. package/dist/types/backup.d.cts +85 -0
  399. package/dist/types/backup.d.cts.map +1 -0
  400. package/dist/types/backup.d.ts +85 -0
  401. package/dist/types/backup.d.ts.map +1 -0
  402. package/dist/types/bucket.cjs +81 -11
  403. package/dist/types/bucket.cjs.map +1 -1
  404. package/dist/types/bucket.d.cts +194 -41
  405. package/dist/types/bucket.d.cts.map +1 -1
  406. package/dist/types/bucket.d.ts +194 -41
  407. package/dist/types/bucket.d.ts.map +1 -1
  408. package/dist/types/bucket.js +67 -12
  409. package/dist/types/bucket.js.map +1 -1
  410. package/dist/types/download.cjs +62 -0
  411. package/dist/types/download.cjs.map +1 -0
  412. package/dist/types/download.d.cts +100 -16
  413. package/dist/types/download.d.cts.map +1 -1
  414. package/dist/types/download.d.ts +100 -16
  415. package/dist/types/download.d.ts.map +1 -1
  416. package/dist/types/download.js +61 -0
  417. package/dist/types/download.js.map +1 -0
  418. package/dist/types/encryption.cjs +76 -229
  419. package/dist/types/encryption.cjs.map +1 -1
  420. package/dist/types/encryption.d.cts +48 -6
  421. package/dist/types/encryption.d.cts.map +1 -1
  422. package/dist/types/encryption.d.ts +48 -6
  423. package/dist/types/encryption.d.ts.map +1 -1
  424. package/dist/types/encryption.js +74 -229
  425. package/dist/types/encryption.js.map +1 -1
  426. package/dist/types/errors.cjs +9 -0
  427. package/dist/types/errors.cjs.map +1 -1
  428. package/dist/types/errors.d.cts +1 -1
  429. package/dist/types/errors.d.cts.map +1 -1
  430. package/dist/types/errors.d.ts +1 -1
  431. package/dist/types/errors.d.ts.map +1 -1
  432. package/dist/types/errors.js +9 -0
  433. package/dist/types/errors.js.map +1 -1
  434. package/dist/types/file.cjs +3 -0
  435. package/dist/types/file.cjs.map +1 -1
  436. package/dist/types/file.d.cts +140 -18
  437. package/dist/types/file.d.cts.map +1 -1
  438. package/dist/types/file.d.ts +140 -18
  439. package/dist/types/file.d.ts.map +1 -1
  440. package/dist/types/file.js +3 -1
  441. package/dist/types/file.js.map +1 -1
  442. package/dist/types/ids.cjs +30 -0
  443. package/dist/types/ids.cjs.map +1 -1
  444. package/dist/types/ids.d.cts +32 -0
  445. package/dist/types/ids.d.cts.map +1 -1
  446. package/dist/types/ids.d.ts +32 -0
  447. package/dist/types/ids.d.ts.map +1 -1
  448. package/dist/types/ids.js +28 -1
  449. package/dist/types/ids.js.map +1 -1
  450. package/dist/types/index.d.cts +16 -12
  451. package/dist/types/index.d.cts.map +1 -1
  452. package/dist/types/index.d.ts +16 -12
  453. package/dist/types/index.d.ts.map +1 -1
  454. package/dist/types/key.d.cts +4 -3
  455. package/dist/types/key.d.cts.map +1 -1
  456. package/dist/types/key.d.ts +4 -3
  457. package/dist/types/key.d.ts.map +1 -1
  458. package/dist/types/lock.cjs.map +1 -1
  459. package/dist/types/lock.d.cts +14 -0
  460. package/dist/types/lock.d.cts.map +1 -1
  461. package/dist/types/lock.d.ts +14 -0
  462. package/dist/types/lock.d.ts.map +1 -1
  463. package/dist/types/lock.js.map +1 -1
  464. package/dist/types/notifications.cjs +31 -1
  465. package/dist/types/notifications.cjs.map +1 -1
  466. package/dist/types/notifications.d.cts +31 -4
  467. package/dist/types/notifications.d.cts.map +1 -1
  468. package/dist/types/notifications.d.ts +31 -4
  469. package/dist/types/notifications.d.ts.map +1 -1
  470. package/dist/types/notifications.js +30 -2
  471. package/dist/types/notifications.js.map +1 -1
  472. package/dist/types/partner.cjs +32 -0
  473. package/dist/types/partner.cjs.map +1 -0
  474. package/dist/types/partner.d.cts +386 -0
  475. package/dist/types/partner.d.cts.map +1 -0
  476. package/dist/types/partner.d.ts +386 -0
  477. package/dist/types/partner.d.ts.map +1 -0
  478. package/dist/types/partner.js +29 -0
  479. package/dist/types/partner.js.map +1 -0
  480. package/dist/types/upload.d.cts +35 -18
  481. package/dist/types/upload.d.cts.map +1 -1
  482. package/dist/types/upload.d.ts +35 -18
  483. package/dist/types/upload.d.ts.map +1 -1
  484. package/dist/upload/abort-scope.cjs +0 -45
  485. package/dist/upload/abort-scope.cjs.map +1 -1
  486. package/dist/upload/abort-scope.d.cts +0 -22
  487. package/dist/upload/abort-scope.d.cts.map +1 -1
  488. package/dist/upload/abort-scope.d.ts +0 -22
  489. package/dist/upload/abort-scope.d.ts.map +1 -1
  490. package/dist/upload/abort-scope.js +1 -44
  491. package/dist/upload/abort-scope.js.map +1 -1
  492. package/dist/upload/finish.cjs +3 -8
  493. package/dist/upload/finish.cjs.map +1 -1
  494. package/dist/upload/finish.d.cts.map +1 -1
  495. package/dist/upload/finish.d.ts.map +1 -1
  496. package/dist/upload/finish.js +1 -6
  497. package/dist/upload/finish.js.map +1 -1
  498. package/dist/upload/large.cjs +15 -8
  499. package/dist/upload/large.cjs.map +1 -1
  500. package/dist/upload/large.d.cts +31 -8
  501. package/dist/upload/large.d.cts.map +1 -1
  502. package/dist/upload/large.d.ts +31 -8
  503. package/dist/upload/large.d.ts.map +1 -1
  504. package/dist/upload/large.js +15 -8
  505. package/dist/upload/large.js.map +1 -1
  506. package/dist/upload/options.cjs.map +1 -1
  507. package/dist/upload/options.d.cts +24 -0
  508. package/dist/upload/options.d.cts.map +1 -1
  509. package/dist/upload/options.d.ts +24 -0
  510. package/dist/upload/options.d.ts.map +1 -1
  511. package/dist/upload/options.js.map +1 -1
  512. package/dist/upload/resume.cjs +6 -4
  513. package/dist/upload/resume.cjs.map +1 -1
  514. package/dist/upload/resume.d.cts +9 -5
  515. package/dist/upload/resume.d.cts.map +1 -1
  516. package/dist/upload/resume.d.ts +9 -5
  517. package/dist/upload/resume.d.ts.map +1 -1
  518. package/dist/upload/resume.js +6 -4
  519. package/dist/upload/resume.js.map +1 -1
  520. package/dist/upload/retry.cjs +25 -13
  521. package/dist/upload/retry.cjs.map +1 -1
  522. package/dist/upload/retry.d.cts.map +1 -1
  523. package/dist/upload/retry.d.ts.map +1 -1
  524. package/dist/upload/retry.js +25 -13
  525. package/dist/upload/retry.js.map +1 -1
  526. package/dist/upload/single.cjs +35 -30
  527. package/dist/upload/single.cjs.map +1 -1
  528. package/dist/upload/single.d.cts +12 -0
  529. package/dist/upload/single.d.cts.map +1 -1
  530. package/dist/upload/single.d.ts +12 -0
  531. package/dist/upload/single.d.ts.map +1 -1
  532. package/dist/upload/single.js +35 -30
  533. package/dist/upload/single.js.map +1 -1
  534. package/dist/upload/stream.cjs +18 -5
  535. package/dist/upload/stream.cjs.map +1 -1
  536. package/dist/upload/stream.d.cts +12 -0
  537. package/dist/upload/stream.d.cts.map +1 -1
  538. package/dist/upload/stream.d.ts +12 -0
  539. package/dist/upload/stream.d.ts.map +1 -1
  540. package/dist/upload/stream.js +15 -2
  541. package/dist/upload/stream.js.map +1 -1
  542. package/dist/util/abort.cjs +114 -0
  543. package/dist/util/abort.cjs.map +1 -0
  544. package/dist/util/abort.d.cts +65 -0
  545. package/dist/util/abort.d.cts.map +1 -0
  546. package/dist/util/abort.d.ts +65 -0
  547. package/dist/util/abort.d.ts.map +1 -0
  548. package/dist/util/abort.js +109 -0
  549. package/dist/util/abort.js.map +1 -0
  550. package/dist/util/defaults.cjs +3 -0
  551. package/dist/util/defaults.cjs.map +1 -1
  552. package/dist/util/defaults.d.cts +2 -0
  553. package/dist/util/defaults.d.cts.map +1 -1
  554. package/dist/util/defaults.d.ts +2 -0
  555. package/dist/util/defaults.d.ts.map +1 -1
  556. package/dist/util/defaults.js +3 -1
  557. package/dist/util/defaults.js.map +1 -1
  558. package/dist/util/md5.cjs +211 -0
  559. package/dist/util/md5.cjs.map +1 -0
  560. package/dist/util/md5.d.cts +43 -0
  561. package/dist/util/md5.d.cts.map +1 -0
  562. package/dist/util/md5.d.ts +43 -0
  563. package/dist/util/md5.d.ts.map +1 -0
  564. package/dist/util/md5.js +209 -0
  565. package/dist/util/md5.js.map +1 -0
  566. package/dist/util/normalize.cjs +5 -5
  567. package/dist/util/normalize.cjs.map +1 -1
  568. package/dist/util/normalize.d.cts +7 -7
  569. package/dist/util/normalize.d.cts.map +1 -1
  570. package/dist/util/normalize.d.ts +7 -7
  571. package/dist/util/normalize.d.ts.map +1 -1
  572. package/dist/util/normalize.js +5 -5
  573. package/dist/util/normalize.js.map +1 -1
  574. package/dist/util/paginator.cjs +7 -1
  575. package/dist/util/paginator.cjs.map +1 -1
  576. package/dist/util/paginator.d.cts +3 -1
  577. package/dist/util/paginator.d.cts.map +1 -1
  578. package/dist/util/paginator.d.ts +3 -1
  579. package/dist/util/paginator.d.ts.map +1 -1
  580. package/dist/util/paginator.js +7 -1
  581. package/dist/util/paginator.js.map +1 -1
  582. package/dist/version.cjs +56 -4
  583. package/dist/version.cjs.map +1 -1
  584. package/dist/version.d.cts +53 -4
  585. package/dist/version.d.cts.map +1 -1
  586. package/dist/version.d.ts +53 -4
  587. package/dist/version.d.ts.map +1 -1
  588. package/dist/version.js +52 -5
  589. package/dist/version.js.map +1 -1
  590. package/package.json +47 -18
@@ -1,18 +1,98 @@
1
- import { FileAction } from "../types/file.js";
1
+ import { FileAction, HIDE_MARKER_CONTENT_TYPE } 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";
6
- import { sha1Hex } from "../streams/hash.js";
7
- import { BucketRetentionMode } from "../types/bucket.js";
5
+ import { DownloadClientUnauthorizedToReadMarker, DownloadHeaderName } from "../types/download.js";
6
+ import { md5Base64, md5Base64Sync } from "../util/md5.js";
8
7
  import { EncryptionAlgorithm, EncryptionMode } from "../types/encryption.js";
8
+ import { LegalHoldValue, RetentionMode } from "../types/lock.js";
9
+ import { hexEncode, hmacSha256 } from "../util/crypto.js";
10
+ import { sha1Hex } from "../streams/hash.js";
11
+ import { BUCKET_INFO_KEY_MAX_BYTES, BUCKET_INFO_KEY_MIN_BYTES, BUCKET_INFO_KEY_PATTERN, BUCKET_INFO_RESERVED_PREFIX, BUCKET_INFO_VALUES_MAX_BYTES, BucketRetentionMode, CORS_MAX_AGE_SECONDS_MAX, CORS_RULES_MAX_COUNT, CORS_RULE_MAX_BYTES, CORS_RULE_NAME_MAX_LENGTH, CORS_RULE_NAME_MIN_LENGTH, CORS_RULE_NAME_PATTERN, CORS_RULE_NAME_RESERVED_PREFIX } from "../types/bucket.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 { PartnerEndpoint } from "./partner-capabilities.js";
16
+ import { PartnerSimulator } from "./partner.js";
11
17
  import { missingCapabilitiesFor } from "./capabilities.js";
12
18
  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";
19
+ 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, validateBucketType, validateBucketTypes, validateCorsRules, validateCreateKeyCapabilities, validateCreateKeyName, validateDefaultRetention, validateDownloadAuthorizationDuration, validateDownloadAuthorizationPrefix, validateFileInfo, validateFileName, validateLifecycleRules, validateMaxCount, validateNotificationRules, validateReplicationConfiguration } from "./validation.js";
14
20
  //#region src/simulator/index.ts
15
- var UPLOAD_TOKEN_SIGNING_KEY = "b2-sdk-typescript-simulator-upload-token-v1";
21
+ var UPLOAD_TOKEN_SIGNING_KEY = [
22
+ "b2 sdk typescript",
23
+ "simulator upload authorization",
24
+ "v1"
25
+ ].join(":");
26
+ var SIMULATOR_MASTER_APPLICATION_KEY_ID = "master-key-id";
27
+ var SIMULATOR_MASTER_APPLICATION_KEY = "master-key";
28
+ var SIMULATOR_TEST_APPLICATION_KEY_ID = "test-key-id";
29
+ var SIMULATOR_TEST_APPLICATION_KEY = "test-key";
30
+ var SIMULATOR_APPLICATION_KEY_ID_PREFIX = "sim_key_";
31
+ var SIMULATOR_MASTER_CAPABILITIES = [
32
+ Capability.ListBuckets,
33
+ Capability.ListAllBucketNames,
34
+ Capability.ReadBuckets,
35
+ Capability.WriteBuckets,
36
+ Capability.DeleteBuckets,
37
+ Capability.ReadBucketEncryption,
38
+ Capability.WriteBucketEncryption,
39
+ Capability.ReadBucketReplications,
40
+ Capability.WriteBucketReplications,
41
+ Capability.ReadBucketNotifications,
42
+ Capability.WriteBucketNotifications,
43
+ Capability.ReadBucketLogging,
44
+ Capability.WriteBucketLogging,
45
+ Capability.ReadBucketLifecycleRules,
46
+ Capability.WriteBucketLifecycleRules,
47
+ Capability.ListFiles,
48
+ Capability.ReadFiles,
49
+ Capability.WriteFiles,
50
+ Capability.DeleteFiles,
51
+ Capability.ListKeys,
52
+ Capability.WriteKeys,
53
+ Capability.DeleteKeys,
54
+ Capability.ShareFiles
55
+ ];
56
+ function apiPathParts(path) {
57
+ const segments = path.split("/").filter((segment) => segment.length > 0);
58
+ const candidate = segments.at(-2);
59
+ const endpoint = segments.at(-1) ?? "";
60
+ const isB2ApiPath = segments.at(-3) === "b2api";
61
+ if (candidate !== void 0 && isB2ApiVersion(candidate)) {
62
+ if (candidate === "v3" || candidate === "v4") return {
63
+ endpoint,
64
+ version: candidate
65
+ };
66
+ if (isB2ApiPath) return {
67
+ endpoint,
68
+ version: "v3",
69
+ unsupportedVersion: candidate
70
+ };
71
+ }
72
+ return {
73
+ endpoint,
74
+ version: "v3"
75
+ };
76
+ }
77
+ function parseBasicAuthorizationHeader(authzHeader) {
78
+ if (!authzHeader?.startsWith("Basic ")) return null;
79
+ const decoded = (() => {
80
+ try {
81
+ return atob(authzHeader.slice(6));
82
+ } catch {
83
+ return null;
84
+ }
85
+ })();
86
+ if (decoded === null) return null;
87
+ const idx = decoded.indexOf(":");
88
+ if (idx <= 0 || idx === decoded.length - 1) return null;
89
+ return {
90
+ applicationKeyId: decoded.slice(0, idx),
91
+ applicationKey: decoded.slice(idx + 1)
92
+ };
93
+ }
94
+ var B2_MIN_PART_NUMBER = 1;
95
+ var B2_MAX_PART_NUMBER = 1e4;
16
96
  function base64UrlEncode(bytes) {
17
97
  let binary = "";
18
98
  for (const byte of bytes) binary += String.fromCharCode(byte);
@@ -34,6 +114,11 @@ function timingSafeStringEqual(left, right) {
34
114
  for (let i = 0; i < max; i++) mismatch |= (left.charCodeAt(i) || 0) ^ (right.charCodeAt(i) || 0);
35
115
  return mismatch === 0;
36
116
  }
117
+ var MS_PER_DAY = 864e5;
118
+ var MAX_JS_DATE_MS = 864e13;
119
+ function isValidRetentionTimestamp(timestamp, now = Date.now()) {
120
+ return Number.isFinite(timestamp) && timestamp <= MAX_JS_DATE_MS && timestamp > now;
121
+ }
37
122
  /**
38
123
  * Parse an RFC 7233 `Range` header value into inclusive start/end byte
39
124
  * offsets clamped to the file size. Supports closed (`bytes=0-999`),
@@ -119,6 +204,56 @@ function compareB2FileNames(a, b) {
119
204
  if (a > b) return 1;
120
205
  return 0;
121
206
  }
207
+ function compareFileVersionListEntries(a, b) {
208
+ const nameCmp = compareB2FileNames(a.fileName, b.fileName);
209
+ if (nameCmp !== 0) return nameCmp;
210
+ return b.uploadTimestamp - a.uploadTimestamp;
211
+ }
212
+ var DOWNLOAD_AUTH_TOKEN_BYTES = 32;
213
+ var DOWNLOAD_AUTH_TOKEN_PREFIX = "sim_dl_auth_";
214
+ var DOWNLOAD_AUTH_PURGE_BATCH_SIZE = 128;
215
+ var DOWNLOAD_RESPONSE_OVERRIDE_PARAMS = [
216
+ "b2ContentDisposition",
217
+ "b2ContentLanguage",
218
+ "b2ContentEncoding",
219
+ "b2ContentType",
220
+ "b2CacheControl",
221
+ "b2Expires"
222
+ ];
223
+ var DOWNLOAD_RESPONSE_OVERRIDE_HEADERS = {
224
+ b2ContentDisposition: DownloadHeaderName.ContentDisposition,
225
+ b2ContentLanguage: DownloadHeaderName.ContentLanguage,
226
+ b2ContentEncoding: DownloadHeaderName.ContentEncoding,
227
+ b2ContentType: DownloadHeaderName.ContentType,
228
+ b2CacheControl: DownloadHeaderName.CacheControl,
229
+ b2Expires: DownloadHeaderName.Expires
230
+ };
231
+ function randomDownloadAuthorizationToken() {
232
+ const crypto = globalThis.crypto;
233
+ if (typeof crypto?.getRandomValues !== "function") throw new Error("Download authorization tokens require globalThis.crypto.getRandomValues");
234
+ const bytes = new Uint8Array(DOWNLOAD_AUTH_TOKEN_BYTES);
235
+ crypto.getRandomValues(bytes);
236
+ return `${DOWNLOAD_AUTH_TOKEN_PREFIX}${hexEncode(bytes)}`;
237
+ }
238
+ function pickDownloadResponseOverrides(req) {
239
+ const overrides = {};
240
+ for (const param of DOWNLOAD_RESPONSE_OVERRIDE_PARAMS) {
241
+ const value = req[param];
242
+ if (typeof value === "string") overrides[param] = value;
243
+ }
244
+ return overrides;
245
+ }
246
+ function validateDownloadResponseOverrides(req) {
247
+ for (const param of DOWNLOAD_RESPONSE_OVERRIDE_PARAMS) {
248
+ const value = req[param];
249
+ if (value === void 0 || typeof value === "string") continue;
250
+ return {
251
+ code: "bad_request",
252
+ message: `${param} must be a string`
253
+ };
254
+ }
255
+ return null;
256
+ }
122
257
  function normalizeKeyBucketIds(req) {
123
258
  return req.bucketIds === void 0 || req.bucketIds === null ? null : Object.freeze([...req.bucketIds]);
124
259
  }
@@ -128,6 +263,18 @@ function singleBucketId(bucketIds) {
128
263
  function cloneBucketIds(bucketIds) {
129
264
  return bucketIds === null ? null : [...bucketIds];
130
265
  }
266
+ function cloneCapabilities(capabilities) {
267
+ return [...capabilities];
268
+ }
269
+ function isImplicitStorageMasterCredential(credentials) {
270
+ 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;
271
+ }
272
+ function isImplicitPartnerMasterCredential(credentials) {
273
+ return credentials.applicationKeyId === SIMULATOR_MASTER_APPLICATION_KEY_ID && credentials.applicationKey === SIMULATOR_MASTER_APPLICATION_KEY;
274
+ }
275
+ function isSimulatorApplicationKeyId(applicationKeyId) {
276
+ return applicationKeyId.startsWith(SIMULATOR_APPLICATION_KEY_ID_PREFIX);
277
+ }
131
278
  function hasOwnField(body, field) {
132
279
  return typeof body === "object" && body !== null && Object.hasOwn(body, field);
133
280
  }
@@ -136,10 +283,347 @@ function requestStringField(body, field) {
136
283
  const value = body[field];
137
284
  return typeof value === "string" ? value : void 0;
138
285
  }
286
+ function requestRecord(body) {
287
+ return typeof body === "object" && body !== null && !Array.isArray(body) ? body : null;
288
+ }
289
+ function replicationDestinationBucketIds(body) {
290
+ const rules = requestRecord(requestRecord(requestRecord(body)?.["replicationConfiguration"])?.["asReplicationSource"])?.["replicationRules"];
291
+ if (!Array.isArray(rules)) return [];
292
+ const bucketIds = [];
293
+ for (const rule of rules) {
294
+ const destinationBucketId = requestRecord(rule)?.["destinationBucketId"];
295
+ if (typeof destinationBucketId === "string") bucketIds.push(destinationBucketId);
296
+ }
297
+ return [...new Set(bucketIds)];
298
+ }
299
+ function normalizeReplicationConfiguration(config) {
300
+ return {
301
+ asReplicationSource: config.asReplicationSource ?? null,
302
+ asReplicationDestination: config.asReplicationDestination ?? null
303
+ };
304
+ }
305
+ function wrapReplicationConfiguration(config) {
306
+ if (config === void 0) return {
307
+ isClientAuthorizedToRead: true,
308
+ value: null
309
+ };
310
+ const normalized = normalizeReplicationConfiguration(config);
311
+ return {
312
+ isClientAuthorizedToRead: true,
313
+ value: normalized.asReplicationSource === null && normalized.asReplicationDestination === null ? null : normalized
314
+ };
315
+ }
316
+ function filterReadableReplicationConfiguration(config, canRead) {
317
+ return canRead ? {
318
+ isClientAuthorizedToRead: true,
319
+ value: config.value
320
+ } : {
321
+ isClientAuthorizedToRead: false,
322
+ value: null
323
+ };
324
+ }
325
+ function readableFileRetention(value, canRead) {
326
+ return canRead ? {
327
+ isClientAuthorizedToRead: true,
328
+ value
329
+ } : {
330
+ isClientAuthorizedToRead: false,
331
+ value: null
332
+ };
333
+ }
334
+ function readableLegalHold(value, canRead) {
335
+ return canRead ? {
336
+ isClientAuthorizedToRead: true,
337
+ value
338
+ } : {
339
+ isClientAuthorizedToRead: false,
340
+ value: null
341
+ };
342
+ }
343
+ function validateBucketConfigurationFields(fields, options) {
344
+ if (fields.bucketInfo !== void 0) {
345
+ const infoError = validateBucketInfo(fields.bucketInfo);
346
+ if (infoError) return infoError;
347
+ }
348
+ if (fields.corsRules !== void 0) {
349
+ const corsError = validateCorsRules(fields.corsRules);
350
+ if (corsError) return corsError;
351
+ }
352
+ if (fields.lifecycleRules !== void 0) {
353
+ const lifecycleError = validateLifecycleRules(fields.lifecycleRules);
354
+ if (lifecycleError) return lifecycleError;
355
+ }
356
+ if (fields.defaultServerSideEncryption !== void 0) {
357
+ const encryptionError = validateBucketDefaultServerSideEncryption(fields.defaultServerSideEncryption);
358
+ if (encryptionError) return encryptionError;
359
+ }
360
+ if (fields.replicationConfiguration !== void 0) {
361
+ const replicationError = validateReplicationConfiguration(fields.replicationConfiguration);
362
+ if (replicationError) return replicationError;
363
+ }
364
+ if (fields.defaultRetention !== void 0) {
365
+ const retentionError = validateDefaultRetention(fields.defaultRetention);
366
+ if (retentionError) return retentionError;
367
+ if (requestRecord(fields.defaultRetention)?.["mode"] !== BucketRetentionMode.None && !options.objectLockEnabled) return {
368
+ code: "file_lock_not_enabled",
369
+ message: "Bucket must have Object Lock enabled to set defaultRetention"
370
+ };
371
+ }
372
+ return null;
373
+ }
374
+ function validateBucketDefaultServerSideEncryption(encryption) {
375
+ const runtimeEncryption = requestRecord(encryption);
376
+ if (runtimeEncryption === null) return {
377
+ code: "bad_request",
378
+ message: "defaultServerSideEncryption must be an object"
379
+ };
380
+ if (hasCustomerEncryptionFields(runtimeEncryption)) return {
381
+ code: "bad_request",
382
+ message: "Bucket default server-side encryption cannot use SSE-C"
383
+ };
384
+ const mode = runtimeEncryption["mode"];
385
+ if (mode === EncryptionMode.None) return null;
386
+ if (mode === EncryptionMode.SseB2) {
387
+ if (runtimeEncryption["algorithm"] !== EncryptionAlgorithm.Aes256) return {
388
+ code: "bad_request",
389
+ message: "SSE-B2 algorithm must be AES256"
390
+ };
391
+ return null;
392
+ }
393
+ if (mode === EncryptionMode.SseC) return {
394
+ code: "bad_request",
395
+ message: "Bucket default server-side encryption cannot use SSE-C"
396
+ };
397
+ return {
398
+ code: "bad_request",
399
+ message: `Unsupported bucket default server-side encryption mode: ${String(mode)}`
400
+ };
401
+ }
402
+ function nextObjectLockEnabled(current, requested) {
403
+ if (requested === void 0) return current;
404
+ if (typeof requested !== "boolean") return {
405
+ code: "bad_request",
406
+ message: "fileLockEnabled must be a boolean"
407
+ };
408
+ if (current && requested === false) return {
409
+ code: "file_lock_conflict",
410
+ message: "Object Lock cannot be disabled"
411
+ };
412
+ return current || requested;
413
+ }
414
+ function requestHeaderValue(headers, name) {
415
+ const exact = headers[name];
416
+ if (exact !== void 0) return exact;
417
+ const lowerName = name.toLowerCase();
418
+ for (const [key, value] of Object.entries(headers)) if (key.toLowerCase() === lowerName) return value;
419
+ }
420
+ function parseContentLengthHeader(headers) {
421
+ const header = requestHeaderValue(headers, "content-length");
422
+ if (header === void 0) return { kind: "absent" };
423
+ if (!/^\d+$/.test(header)) return {
424
+ kind: "error",
425
+ message: `Content-Length must be a byte count: ${header}`
426
+ };
427
+ const expectedLength = Number(header);
428
+ if (!Number.isSafeInteger(expectedLength)) return {
429
+ kind: "error",
430
+ message: `Content-Length is too large: ${header}`
431
+ };
432
+ return {
433
+ kind: "ok",
434
+ expectedLength
435
+ };
436
+ }
437
+ function contentLengthMismatchMessage(expectedLength, actualLength) {
438
+ return `Content-Length ${expectedLength} does not match request body length ${actualLength}`;
439
+ }
139
440
  function fileNames(...names) {
140
441
  const present = names.filter((name) => name !== void 0);
141
442
  return present.length > 0 ? present : void 0;
142
443
  }
444
+ function queryParamsBody(params, endpoint) {
445
+ const metadata = jsonEndpointMetadata(endpoint);
446
+ const queryFields = new Set(metadata?.queryFields ?? []);
447
+ const numericFields = new Set(metadata?.numericQueryFields ?? []);
448
+ const body = Object.create(null);
449
+ for (const [key, value] of params) {
450
+ if (!queryFields.has(key)) continue;
451
+ if (body[key] !== void 0) continue;
452
+ body[key] = queryParamValue(value, numericFields.has(key));
453
+ }
454
+ return body;
455
+ }
456
+ function queryParamValue(value, numeric) {
457
+ if (!numeric || !/^-?\d+$/.test(value)) return value;
458
+ const numericValue = Number(value);
459
+ return Number.isSafeInteger(numericValue) ? numericValue : value;
460
+ }
461
+ var JSON_ENDPOINTS = {
462
+ b2_authorize_account: { methods: ["GET"] },
463
+ [PartnerEndpoint.ListGroups]: {
464
+ methods: ["GET"],
465
+ queryFields: [
466
+ "adminAccountId",
467
+ "groupName",
468
+ "startGroupId",
469
+ "maxGroupCount"
470
+ ],
471
+ numericQueryFields: ["maxGroupCount"]
472
+ },
473
+ [PartnerEndpoint.ListGroupMembers]: {
474
+ methods: ["GET"],
475
+ queryFields: [
476
+ "adminAccountId",
477
+ "groupId",
478
+ "startEmail",
479
+ "maxMemberCount"
480
+ ],
481
+ numericQueryFields: ["maxMemberCount"]
482
+ },
483
+ [PartnerEndpoint.ListComputers]: {
484
+ methods: ["GET"],
485
+ queryFields: [
486
+ "accountId",
487
+ "startComputerId",
488
+ "maxComputerCount"
489
+ ],
490
+ numericQueryFields: ["maxComputerCount"]
491
+ },
492
+ b2_get_upload_url: {
493
+ methods: ["GET", "POST"],
494
+ queryFields: ["bucketId"]
495
+ },
496
+ b2_list_file_names: {
497
+ methods: ["GET", "POST"],
498
+ queryFields: [
499
+ "bucketId",
500
+ "delimiter",
501
+ "maxFileCount",
502
+ "prefix",
503
+ "startFileName"
504
+ ],
505
+ numericQueryFields: ["maxFileCount"]
506
+ },
507
+ b2_list_file_versions: {
508
+ methods: ["GET", "POST"],
509
+ queryFields: [
510
+ "bucketId",
511
+ "delimiter",
512
+ "maxFileCount",
513
+ "prefix",
514
+ "startFileName",
515
+ "startFileId"
516
+ ],
517
+ numericQueryFields: ["maxFileCount"]
518
+ },
519
+ b2_get_file_info: {
520
+ methods: ["GET", "POST"],
521
+ queryFields: ["fileId"]
522
+ },
523
+ b2_copy_file: {
524
+ methods: ["GET", "POST"],
525
+ queryFields: [
526
+ "sourceFileId",
527
+ "fileName",
528
+ "destinationBucketId",
529
+ "range",
530
+ "metadataDirective",
531
+ "contentType"
532
+ ]
533
+ },
534
+ b2_get_upload_part_url: {
535
+ methods: ["GET", "POST"],
536
+ queryFields: ["fileId"]
537
+ },
538
+ b2_list_unfinished_large_files: {
539
+ methods: ["GET", "POST"],
540
+ queryFields: [
541
+ "bucketId",
542
+ "namePrefix",
543
+ "startFileId",
544
+ "maxFileCount"
545
+ ],
546
+ numericQueryFields: ["maxFileCount"]
547
+ },
548
+ b2_list_parts: {
549
+ methods: ["GET", "POST"],
550
+ queryFields: [
551
+ "fileId",
552
+ "startPartNumber",
553
+ "maxPartCount"
554
+ ],
555
+ numericQueryFields: ["startPartNumber", "maxPartCount"]
556
+ },
557
+ b2_copy_part: {
558
+ methods: ["GET", "POST"],
559
+ queryFields: [
560
+ "sourceFileId",
561
+ "largeFileId",
562
+ "partNumber",
563
+ "range"
564
+ ],
565
+ numericQueryFields: ["partNumber"]
566
+ },
567
+ b2_get_download_authorization: {
568
+ methods: ["GET", "POST"],
569
+ queryFields: [
570
+ "bucketId",
571
+ "fileNamePrefix",
572
+ "validDurationInSeconds",
573
+ "b2ContentDisposition",
574
+ "b2ContentLanguage",
575
+ "b2ContentEncoding",
576
+ "b2ContentType",
577
+ "b2CacheControl",
578
+ "b2Expires"
579
+ ],
580
+ numericQueryFields: ["validDurationInSeconds"]
581
+ },
582
+ b2_list_keys: {
583
+ methods: ["GET", "POST"],
584
+ queryFields: [
585
+ "accountId",
586
+ "maxKeyCount",
587
+ "startApplicationKeyId"
588
+ ],
589
+ numericQueryFields: ["maxKeyCount"]
590
+ },
591
+ b2_get_bucket_notification_rules: {
592
+ methods: ["GET", "POST"],
593
+ queryFields: ["bucketId"]
594
+ },
595
+ [PartnerEndpoint.CreateGroupMember]: { methods: ["POST"] },
596
+ [PartnerEndpoint.EjectGroupMember]: { methods: ["POST"] },
597
+ [PartnerEndpoint.ReserveTrialCreateAccount]: { methods: ["POST"] },
598
+ [PartnerEndpoint.DeleteComputer]: { methods: ["POST"] },
599
+ b2_create_bucket: { methods: ["POST"] },
600
+ b2_list_buckets: { methods: ["POST"] },
601
+ b2_delete_bucket: { methods: ["POST"] },
602
+ b2_update_bucket: { methods: ["POST"] },
603
+ b2_hide_file: { methods: ["POST"] },
604
+ b2_delete_file_version: { methods: ["POST"] },
605
+ b2_start_large_file: { methods: ["POST"] },
606
+ b2_finish_large_file: { methods: ["POST"] },
607
+ b2_cancel_large_file: { methods: ["POST"] },
608
+ b2_create_key: { methods: ["POST"] },
609
+ b2_delete_key: { methods: ["POST"] },
610
+ b2_update_file_retention: { methods: ["POST"] },
611
+ b2_update_file_legal_hold: { methods: ["POST"] },
612
+ b2_set_bucket_notification_rules: { methods: ["POST"] }
613
+ };
614
+ function jsonEndpointMetadata(endpoint) {
615
+ return JSON_ENDPOINTS[endpoint];
616
+ }
617
+ function jsonEndpointAllowsMethod(method, endpoint) {
618
+ const metadata = jsonEndpointMetadata(endpoint);
619
+ if (metadata === void 0) return true;
620
+ const normalizedMethod = method.toUpperCase();
621
+ if (normalizedMethod !== "GET" && normalizedMethod !== "POST") return false;
622
+ return metadata.methods.includes(normalizedMethod);
623
+ }
624
+ function isJsonQueryEndpoint(endpoint) {
625
+ return jsonEndpointMetadata(endpoint)?.queryFields !== void 0;
626
+ }
143
627
  function notificationRulePrefixes(body) {
144
628
  if (typeof body !== "object" || body === null) return void 0;
145
629
  const rules = body["eventNotificationRules"];
@@ -156,6 +640,13 @@ function storedNotificationRulePrefixes(rules) {
156
640
  function hasKeyManagementCapability(capabilities) {
157
641
  return capabilities.some((capability) => capability === Capability.ListKeys || capability === Capability.WriteKeys || capability === Capability.DeleteKeys);
158
642
  }
643
+ function missingStoredKeyCapabilities(key, required) {
644
+ const capabilities = new Set(key.capabilities);
645
+ return required.filter((capability) => !capabilities.has(capability));
646
+ }
647
+ function storedKeyAllowsBucket(key, bucketId) {
648
+ return key.bucketIds === null || key.bucketIds.includes(bucketId);
649
+ }
159
650
  function publicServerSideEncryption(encryption) {
160
651
  if (encryption.mode === EncryptionMode.SseC) return {
161
652
  mode: encryption.mode,
@@ -167,27 +658,212 @@ function publicServerSideEncryption(encryption) {
167
658
  };
168
659
  return encryption;
169
660
  }
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"] ?? ""
661
+ function publicBucketDefaultServerSideEncryption(encryption) {
662
+ return {
663
+ isClientAuthorizedToRead: true,
664
+ value: publicBucketDefaultServerSideEncryptionValue(encryption)
665
+ };
666
+ }
667
+ function publicBucketDefaultServerSideEncryptionValue(encryption) {
668
+ if (encryption?.mode === EncryptionMode.SseB2) return encryption;
669
+ return {
670
+ mode: null,
671
+ algorithm: null
672
+ };
673
+ }
674
+ function bucketDefaultServerSideEncryptionForStorage(info) {
675
+ const encryption = info.defaultServerSideEncryption;
676
+ if (!encryption.isClientAuthorizedToRead) return {
677
+ mode: null,
678
+ algorithm: null
679
+ };
680
+ return encryption.value ?? {
681
+ mode: null,
682
+ algorithm: null
683
+ };
684
+ }
685
+ function customerKeyDigest(customerKey) {
686
+ let first = 2166136261;
687
+ let second = 668265261;
688
+ for (const byte of customerKey) {
689
+ first = Math.imul(first ^ byte, 16777619) >>> 0;
690
+ second = Math.imul(second ^ byte, 2246822507) >>> 0;
691
+ }
692
+ return `${first.toString(16).padStart(8, "0")}${second.toString(16).padStart(8, "0")}`;
693
+ }
694
+ var BASE64_ALPHABET = /^[A-Za-z0-9+/]+$/;
695
+ function normalizeSizedBase64(value, decodedByteLength) {
696
+ const paddedLength = Math.ceil(decodedByteLength / 3) * 4;
697
+ const noPaddingLength = Math.ceil(decodedByteLength * 8 / 6);
698
+ const padding = "=".repeat(paddedLength - noPaddingLength);
699
+ if (value.length === noPaddingLength && BASE64_ALPHABET.test(value)) return value.padEnd(paddedLength, "=");
700
+ if (value.length === paddedLength && value.endsWith(padding) && BASE64_ALPHABET.test(value.slice(0, noPaddingLength))) return value;
701
+ return null;
702
+ }
703
+ function base64ToBytes(value, decodedByteLength) {
704
+ const normalized = normalizeSizedBase64(value, decodedByteLength);
705
+ if (normalized === null) return null;
706
+ try {
707
+ const decoded = atob(normalized);
708
+ if (decoded.length !== decodedByteLength) return null;
709
+ const bytes = new Uint8Array(decoded.length);
710
+ for (let i = 0; i < decoded.length; i++) bytes[i] = decoded.charCodeAt(i);
711
+ return bytes;
712
+ } catch {
713
+ return null;
714
+ }
715
+ }
716
+ function encryptionValidationError(message) {
717
+ return {
718
+ status: 400,
719
+ body: {
720
+ status: 400,
721
+ code: "bad_request",
722
+ message
723
+ }
176
724
  };
177
- if (headers["x-bz-server-side-encryption"] === EncryptionAlgorithm.Aes256) return {
178
- mode: EncryptionMode.SseB2,
179
- algorithm: EncryptionAlgorithm.Aes256
725
+ }
726
+ function sameStoredServerSideEncryption(a, b) {
727
+ if (a.mode !== b.mode) return false;
728
+ if (a.mode === EncryptionMode.None) return true;
729
+ if (a.mode === EncryptionMode.SseB2 && b.mode === EncryptionMode.SseB2) return a.algorithm === b.algorithm;
730
+ if (a.mode === EncryptionMode.SseC && b.mode === EncryptionMode.SseC) return a.algorithm === b.algorithm && a.customerKeyDigest === b.customerKeyDigest && a.customerKeyMd5 === b.customerKeyMd5;
731
+ return false;
732
+ }
733
+ function hasCustomerEncryptionFields(encryption) {
734
+ return encryption["customerKey"] !== void 0 || encryption["customerKeyMd5"] !== void 0 || encryption["customerAlgorithm"] !== void 0;
735
+ }
736
+ function hasCustomerEncryptionHeaders(headers) {
737
+ 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;
738
+ }
739
+ function isExactNoEncryptionWireSetting(encryption) {
740
+ return encryption["mode"] === null && encryption["algorithm"] === null && Object.keys(encryption).length === 2;
741
+ }
742
+ async function storedServerSideEncryption(encryption) {
743
+ const runtimeEncryption = encryption;
744
+ if (encryption.mode === null) {
745
+ if (isExactNoEncryptionWireSetting(runtimeEncryption)) return { mode: EncryptionMode.None };
746
+ if (hasCustomerEncryptionFields(runtimeEncryption)) return encryptionValidationError("No-encryption settings must not include SSE-C customer keys");
747
+ return encryptionValidationError("No-encryption wire settings must be exactly { mode: null, algorithm: null }");
748
+ }
749
+ if (encryption.mode === EncryptionMode.SseC) {
750
+ if (runtimeEncryption["algorithm"] === void 0) return encryptionValidationError("SSE-C customer algorithm is required");
751
+ if (encryption.algorithm !== EncryptionAlgorithm.Aes256) return encryptionValidationError("SSE-C customer algorithm must be AES256");
752
+ const customerKey = "customerKey" in encryption ? encryption.customerKey : void 0;
753
+ const customerKeyMd5 = "customerKeyMd5" in encryption ? encryption.customerKeyMd5 : void 0;
754
+ if (typeof customerKey !== "string" || customerKey === "") return encryptionValidationError("SSE-C customer key is required");
755
+ if (typeof customerKeyMd5 !== "string" || customerKeyMd5 === "") return encryptionValidationError("SSE-C customer key MD5 is required");
756
+ const customerKeyBytes = base64ToBytes(customerKey, 32);
757
+ if (customerKeyBytes === null) return encryptionValidationError("SSE-C customer key must be a base64-encoded 256-bit key");
758
+ const normalizedCustomerKeyMd5 = normalizeSizedBase64(customerKeyMd5, 16);
759
+ if (normalizedCustomerKeyMd5 === null) return encryptionValidationError("SSE-C customer key MD5 must be a base64-encoded 128-bit digest");
760
+ if (await md5Base64(customerKeyBytes) !== normalizedCustomerKeyMd5) return encryptionValidationError("SSE-C customer key MD5 does not match the key");
761
+ return {
762
+ mode: encryption.mode,
763
+ algorithm: encryption.algorithm,
764
+ customerKeyDigest: customerKeyDigest(customerKeyBytes),
765
+ customerKeyMd5: normalizedCustomerKeyMd5
766
+ };
767
+ }
768
+ if (encryption.mode === EncryptionMode.SseB2) {
769
+ if (hasCustomerEncryptionFields(runtimeEncryption)) return encryptionValidationError("SSE-B2 settings must not include SSE-C customer keys");
770
+ if (encryption.algorithm !== EncryptionAlgorithm.Aes256) return encryptionValidationError("SSE-B2 algorithm must be AES256");
771
+ return {
772
+ mode: encryption.mode,
773
+ algorithm: encryption.algorithm
774
+ };
775
+ }
776
+ if (encryption.mode === EncryptionMode.None) {
777
+ if (hasCustomerEncryptionFields(runtimeEncryption)) return encryptionValidationError("No-encryption settings must not include SSE-C customer keys");
778
+ return { mode: EncryptionMode.None };
779
+ }
780
+ return encryptionValidationError(`Unsupported server-side encryption mode: ${String(runtimeEncryption["mode"])}`);
781
+ }
782
+ async function uploadServerSideEncryption(headers, fallback) {
783
+ const customerAlgorithm = headers["x-bz-server-side-encryption-customer-algorithm"];
784
+ const customerKey = headers["x-bz-server-side-encryption-customer-key"];
785
+ const customerKeyMd5 = headers["x-bz-server-side-encryption-customer-key-md5"];
786
+ const managedEncryption = headers["x-bz-server-side-encryption"];
787
+ if (hasCustomerEncryptionHeaders(headers)) {
788
+ if (managedEncryption !== void 0) return encryptionValidationError("SSE-B2 settings must not include SSE-C customer keys");
789
+ if (customerAlgorithm === void 0) return encryptionValidationError("SSE-C customer algorithm is required");
790
+ if (customerAlgorithm !== EncryptionAlgorithm.Aes256) return encryptionValidationError("SSE-C customer algorithm must be AES256");
791
+ if (!customerKey || !customerKeyMd5) return encryptionValidationError("SSE-C customer key and MD5 are required");
792
+ return await storedServerSideEncryption({
793
+ mode: EncryptionMode.SseC,
794
+ algorithm: EncryptionAlgorithm.Aes256,
795
+ customerKey,
796
+ customerKeyMd5
797
+ });
798
+ }
799
+ if (managedEncryption !== void 0) {
800
+ if (managedEncryption !== EncryptionAlgorithm.Aes256) return encryptionValidationError("SSE-B2 algorithm must be AES256");
801
+ return {
802
+ mode: EncryptionMode.SseB2,
803
+ algorithm: EncryptionAlgorithm.Aes256
804
+ };
805
+ }
806
+ return await storedServerSideEncryption(fallback);
807
+ }
808
+ function noBucketDefaultRetention() {
809
+ return {
810
+ mode: null,
811
+ period: null
180
812
  };
181
- return fallback;
813
+ }
814
+ function bucketDefaultRetentionForResponse(policy) {
815
+ if (policy === void 0 || policy.mode === BucketRetentionMode.None) return noBucketDefaultRetention();
816
+ return policy;
817
+ }
818
+ function bucketDefaultRetention(info) {
819
+ return info.fileLockConfiguration.value?.defaultRetention;
182
820
  }
183
821
  function defaultFileRetention(policy, uploadTimestamp) {
822
+ if (policy === void 0) return null;
823
+ if (policy.mode === null) return null;
184
824
  if (policy.mode === BucketRetentionMode.None || policy.period === null) return null;
185
- const days = policy.period.unit === "days" ? policy.period.duration : policy.period.duration * 365;
825
+ const retainUntilTimestamp = uploadTimestamp + (policy.period.unit === "days" ? policy.period.duration : policy.period.duration * 365) * MS_PER_DAY;
826
+ if (!isValidRetentionTimestamp(retainUntilTimestamp, uploadTimestamp)) return null;
186
827
  return {
187
828
  mode: policy.mode,
188
- retainUntilTimestamp: uploadTimestamp + days * 24 * 60 * 60 * 1e3
829
+ retainUntilTimestamp
189
830
  };
190
831
  }
832
+ function isRetentionMode(value) {
833
+ return value === RetentionMode.Compliance || value === RetentionMode.Governance;
834
+ }
835
+ function parseFileRetentionValue(value, now) {
836
+ if (typeof value !== "object" || value === null) return null;
837
+ const candidate = value;
838
+ const mode = candidate["mode"];
839
+ const retainUntilTimestamp = candidate["retainUntilTimestamp"];
840
+ if (mode === null && retainUntilTimestamp === null) return {
841
+ mode,
842
+ retainUntilTimestamp
843
+ };
844
+ if (isRetentionMode(mode) && typeof retainUntilTimestamp === "number") {
845
+ if (!isValidRetentionTimestamp(retainUntilTimestamp, now)) return null;
846
+ return {
847
+ mode,
848
+ retainUntilTimestamp
849
+ };
850
+ }
851
+ return null;
852
+ }
853
+ function isRetentionWeakened(current, next) {
854
+ if (current === null || current === void 0) return false;
855
+ if (current.mode === null) return false;
856
+ if (next.mode === null) return true;
857
+ if (current.mode === RetentionMode.Compliance && next.mode !== RetentionMode.Compliance) return true;
858
+ if (current.retainUntilTimestamp === null) return next.mode !== current.mode;
859
+ if (next.retainUntilTimestamp === null) return true;
860
+ return next.retainUntilTimestamp < current.retainUntilTimestamp;
861
+ }
862
+ function requiresGovernanceBypass(current, next) {
863
+ if (current?.mode !== RetentionMode.Governance) return false;
864
+ if (next.mode === RetentionMode.Compliance) return true;
865
+ return isRetentionWeakened(current, next);
866
+ }
191
867
  /**
192
868
  * In-memory B2 simulator for testing. Implements the B2 native API at the
193
869
  * request/response level without any network I/O. Supports 25+ operations
@@ -203,6 +879,15 @@ function defaultFileRetention(policy, uploadTimestamp) {
203
879
  * })
204
880
  * await client.authorize()
205
881
  * ```
882
+ *
883
+ * `test-key-id:test-key` is the documented implicit full-access
884
+ * storage credential. `master-key-id:master-key` is also accepted for
885
+ * tests that share storage and Partner simulator credentials. In the
886
+ * default permissive mode, other well-formed Basic credentials whose
887
+ * applicationKeyId was not minted by `b2_create_key` receive the same
888
+ * implicit master grant for backward compatibility. A simulator-created
889
+ * key always authorizes with its own capabilities, bucket restrictions,
890
+ * name prefix, and expiration.
206
891
  */
207
892
  var B2Simulator = class {
208
893
  buckets = /* @__PURE__ */ new Map();
@@ -219,6 +904,8 @@ var B2Simulator = class {
219
904
  onHookError;
220
905
  strictAuth;
221
906
  authTokenTtlMs;
907
+ customUploadTimestampsEnabled;
908
+ partner;
222
909
  /**
223
910
  * Issued auth tokens with their associated grant scope + expiry. In
224
911
  * permissive mode (`strictAuth: false`) this is still populated by
@@ -234,6 +921,8 @@ var B2Simulator = class {
234
921
  * state for tests.
235
922
  */
236
923
  uploadTokens = /* @__PURE__ */ new Map();
924
+ downloadAuthorizationTokens = /* @__PURE__ */ new Map();
925
+ downloadAuthorizationExpiryHeap = [];
237
926
  /**
238
927
  * Virtual-clock offset applied to `Date.now()` for expiry checks.
239
928
  * Defaults to 0. Tests advance via {@link advanceTime} to fast-forward
@@ -277,7 +966,23 @@ var B2Simulator = class {
277
966
  if (options.onReplicate !== void 0) this.onReplicate = options.onReplicate;
278
967
  if (options.onHookError !== void 0) this.onHookError = options.onHookError;
279
968
  this.strictAuth = options.strictAuth ?? false;
280
- this.authTokenTtlMs = options.authTokenTtlMs ?? 1440 * 60 * 1e3;
969
+ this.customUploadTimestampsEnabled = options.customUploadTimestampsEnabled ?? false;
970
+ this.authTokenTtlMs = options.authTokenTtlMs ?? 864e5;
971
+ this.partner = new PartnerSimulator({
972
+ accountId: this.accountId,
973
+ authTokenTtlMs: this.authTokenTtlMs,
974
+ minimumPartSize: this.minimumPartSize,
975
+ recommendedPartSize: this.recommendedPartSize,
976
+ canAuthorize: (authzHeader) => this.canAuthorizePartner(authzHeader),
977
+ createBucket: (req) => this.createBucket(req),
978
+ error: (status, code, message) => this.error(status, code, message),
979
+ genId: (prefix) => this.genId(prefix),
980
+ monotonicTimestamp: () => this.monotonicTimestamp(),
981
+ now: () => this.now(),
982
+ storeKey: (key) => {
983
+ this.keys.set(key.applicationKeyId, key);
984
+ }
985
+ }, options);
281
986
  }
282
987
  /**
283
988
  * Advance the simulator's virtual clock by `ms` milliseconds. Used in
@@ -335,6 +1040,9 @@ var B2Simulator = class {
335
1040
  now() {
336
1041
  return Date.now() + this.clockOffsetMs;
337
1042
  }
1043
+ isStoredKeyExpired(key) {
1044
+ return key.expirationTimestamp !== null && key.expirationTimestamp <= this.now();
1045
+ }
338
1046
  pruneExpiredUploadTokens(now = this.now()) {
339
1047
  for (const [authorizationToken, token] of this.uploadTokens.entries()) if (now >= token.cleanupAt) this.uploadTokens.delete(authorizationToken);
340
1048
  }
@@ -458,15 +1166,205 @@ var B2Simulator = class {
458
1166
  const token = this.issuedTokens.get(authToken);
459
1167
  if (!token) return this.error(401, "bad_auth_token", "unknown auth token");
460
1168
  if (this.now() > token.expiresAt) return this.error(401, "expired_auth_token", "auth token has expired; reauthorize");
1169
+ if (token.applicationKeyId !== null) {
1170
+ const key = this.keys.get(token.applicationKeyId);
1171
+ if (key === void 0) return this.error(401, "bad_auth_token", "application key has been deleted; reauthorize");
1172
+ if (this.isStoredKeyExpired(key)) return this.error(401, "expired_auth_token", "application key has expired; reauthorize");
1173
+ }
461
1174
  const missing = missingCapabilitiesFor(endpoint, token.capabilities);
462
1175
  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}`);
1176
+ return this.authorizeScopeGrant(scope, {
1177
+ bucketIds: token.bucketIds,
1178
+ namePrefix: token.namePrefix,
1179
+ bucketScopeRequiredMessage: () => `application key is scoped to buckets ${token.bucketIds?.join(", ")}; bucket scope is required`,
1180
+ bucketMismatchMessage: (bucketId) => `application key is scoped to buckets ${token.bucketIds?.join(", ")}; cannot access ${bucketId}`,
1181
+ prefixMismatchMessage: (fileName) => `application key is scoped to prefix "${token.namePrefix}"; "${fileName}" is outside scope`
1182
+ });
1183
+ }
1184
+ authorizeScopeGrant(scope, grant) {
1185
+ if (grant.bucketIds !== null) {
1186
+ if (scope?.requiresAccountLevelBucketAccess === true) return this.error(403, "unauthorized", grant.bucketScopeRequiredMessage());
1187
+ if ((scope?.bucketIds.length ?? 0) === 0 && scope?.requiresBucketScope === true) return this.error(403, "unauthorized", grant.bucketScopeRequiredMessage());
1188
+ for (const bucketId of scope?.bucketIds ?? []) {
1189
+ if (grant.bucketIds.includes(bucketId)) continue;
1190
+ return this.error(403, "unauthorized", grant.bucketMismatchMessage(bucketId));
1191
+ }
1192
+ }
1193
+ if (grant.namePrefix !== null) for (const fileName of scope?.fileNames ?? []) {
1194
+ if (fileName.startsWith(grant.namePrefix)) continue;
1195
+ return this.error(403, "unauthorized", grant.prefixMismatchMessage(fileName));
1196
+ }
1197
+ return null;
1198
+ }
1199
+ authorizeDownloadRequest(authToken, endpoint, scope, url) {
1200
+ if (authToken === void 0 || authToken === "") return this.error(401, "bad_auth_token", "missing authorization token");
1201
+ const downloadAuth = this.downloadAuthorizationTokens.get(authToken);
1202
+ if (downloadAuth === void 0) {
1203
+ this.purgeExpiredDownloadAuthorizationTokens();
1204
+ return this.authorizeRequest(authToken, endpoint, scope);
1205
+ }
1206
+ if (this.isExpiredDownloadAuthorizationToken(downloadAuth)) {
1207
+ this.downloadAuthorizationTokens.delete(authToken);
1208
+ this.purgeExpiredDownloadAuthorizationTokens();
1209
+ return this.error(401, "expired_auth_token", "download authorization token has expired");
1210
+ }
1211
+ if (endpoint === "b2_download_file_by_id") return this.error(403, "unauthorized", "download authorization tokens cannot be used with b2_download_file_by_id");
1212
+ const overrideError = this.authorizeDownloadResponseOverrides(downloadAuth, url);
1213
+ if (overrideError !== null) return overrideError;
1214
+ return this.authorizeScopeGrant(scope, {
1215
+ bucketIds: [downloadAuth.bucketId],
1216
+ namePrefix: downloadAuth.fileNamePrefix,
1217
+ bucketScopeRequiredMessage: () => `download authorization is scoped to bucket ${downloadAuth.bucketId}; bucket scope is required`,
1218
+ bucketMismatchMessage: (bucketId) => `download authorization is scoped to bucket ${downloadAuth.bucketId}; cannot access ${bucketId}`,
1219
+ prefixMismatchMessage: (fileName) => `download authorization is scoped to prefix "${downloadAuth.fileNamePrefix}"; "${fileName}" is outside scope`
1220
+ });
1221
+ }
1222
+ isExpiredDownloadAuthorizationToken(token, now = this.now()) {
1223
+ return now >= token.expiresAt;
1224
+ }
1225
+ purgeExpiredDownloadAuthorizationTokens() {
1226
+ const now = this.now();
1227
+ for (let i = 0; i < DOWNLOAD_AUTH_PURGE_BATCH_SIZE; i++) {
1228
+ const next = this.downloadAuthorizationExpiryHeap[0];
1229
+ if (next === void 0 || next.expiresAt > now) return;
1230
+ this.popDownloadAuthorizationExpiry();
1231
+ if (this.downloadAuthorizationTokens.get(next.token)?.expiresAt === next.expiresAt) this.downloadAuthorizationTokens.delete(next.token);
1232
+ }
1233
+ }
1234
+ pushDownloadAuthorizationExpiry(entry) {
1235
+ const heap = this.downloadAuthorizationExpiryHeap;
1236
+ heap.push(entry);
1237
+ let idx = heap.length - 1;
1238
+ while (idx > 0) {
1239
+ const parentIdx = Math.floor((idx - 1) / 2);
1240
+ const parent = heap[parentIdx];
1241
+ if (parent.expiresAt <= entry.expiresAt) break;
1242
+ heap[idx] = parent;
1243
+ idx = parentIdx;
1244
+ }
1245
+ heap[idx] = entry;
1246
+ }
1247
+ popDownloadAuthorizationExpiry() {
1248
+ const heap = this.downloadAuthorizationExpiryHeap;
1249
+ const root = heap[0];
1250
+ const last = heap.pop();
1251
+ if (root === void 0 || last === void 0 || heap.length === 0) return root;
1252
+ let idx = 0;
1253
+ while (true) {
1254
+ const leftIdx = idx * 2 + 1;
1255
+ const rightIdx = leftIdx + 1;
1256
+ const left = heap[leftIdx];
1257
+ const right = heap[rightIdx];
1258
+ if (left === void 0) break;
1259
+ const childIdx = right !== void 0 && right.expiresAt < left.expiresAt ? rightIdx : leftIdx;
1260
+ const child = heap[childIdx];
1261
+ if (child.expiresAt >= last.expiresAt) break;
1262
+ heap[idx] = child;
1263
+ idx = childIdx;
466
1264
  }
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`);
1265
+ heap[idx] = last;
1266
+ return root;
1267
+ }
1268
+ authorizeDownloadResponseOverrides(downloadAuth, url) {
1269
+ for (const param of DOWNLOAD_RESPONSE_OVERRIDE_PARAMS) {
1270
+ const expected = downloadAuth.responseHeaderOverrides[param];
1271
+ if (expected === void 0) continue;
1272
+ const actual = url.searchParams.get(param);
1273
+ if (actual === expected) continue;
1274
+ const expectedText = JSON.stringify(expected);
1275
+ const actualText = actual === null ? "absent" : JSON.stringify(actual);
1276
+ return this.error(403, "unauthorized", `download authorization requires ${param}=${expectedText}; got ${actualText}`);
1277
+ }
1278
+ return null;
1279
+ }
1280
+ downloadAuthorizationFromRequest(headers, url, options = {}) {
1281
+ const headerToken = requestHeaderValue(headers, "authorization");
1282
+ if (headerToken !== void 0) return headerToken;
1283
+ if (options.allowQueryAuthorization !== true) return void 0;
1284
+ const queryToken = url.searchParams.get("Authorization") ?? url.searchParams.get("authorization");
1285
+ return queryToken?.startsWith(DOWNLOAD_AUTH_TOKEN_PREFIX) ? queryToken : void 0;
1286
+ }
1287
+ requestHasCapability(authToken, capability) {
1288
+ if (!this.strictAuth) return true;
1289
+ if (authToken === void 0) return false;
1290
+ return this.issuedTokens.get(authToken)?.capabilities.includes(capability) === true;
1291
+ }
1292
+ bucketInfoForResponse(info, authToken) {
1293
+ return {
1294
+ ...info,
1295
+ defaultServerSideEncryption: this.requestHasCapability(authToken, Capability.ReadBucketEncryption) ? info.defaultServerSideEncryption : {
1296
+ isClientAuthorizedToRead: false,
1297
+ value: null
1298
+ },
1299
+ replicationConfiguration: filterReadableReplicationConfiguration(info.replicationConfiguration, this.requestHasCapability(authToken, Capability.ReadBucketReplications))
1300
+ };
1301
+ }
1302
+ requireFileLockEnabled(bucket) {
1303
+ if (bucket.info.fileLockConfiguration.value?.isFileLockEnabled === true) return null;
1304
+ return this.error(400, "file_lock_not_enabled", "Bucket does not have file lock enabled");
1305
+ }
1306
+ requireGovernanceBypass(req, authToken, message) {
1307
+ if (req.bypassGovernance !== true) return this.error(400, "file_lock_governance_protected", message);
1308
+ if (!this.requestHasCapability(authToken, Capability.BypassGovernance)) return this.error(403, "unauthorized", `application key lacks required capabilities: ${Capability.BypassGovernance}`);
1309
+ return null;
1310
+ }
1311
+ validateReplicationApplicationKey(applicationKeyId, requiredCapabilities, options) {
1312
+ const key = this.keys.get(applicationKeyId);
1313
+ if (key === void 0) return this.invalidReplicationApplicationKey(options.role);
1314
+ if (missingStoredKeyCapabilities(key, requiredCapabilities).length > 0) return this.invalidReplicationApplicationKey(options.role);
1315
+ if (options.bucketId !== void 0 && !storedKeyAllowsBucket(key, options.bucketId)) return this.invalidReplicationApplicationKey(options.role);
1316
+ return null;
1317
+ }
1318
+ invalidReplicationApplicationKey(role) {
1319
+ return this.error(400, "bad_request", `${role} application key is invalid or not authorized for this replication configuration`);
1320
+ }
1321
+ invalidReplicationDestinationBucket() {
1322
+ return this.error(400, "bad_request", "replication destination bucket is invalid or not configured for this source application key");
1323
+ }
1324
+ validateReplicationDestinationBuckets(sourceApplicationKeyId, rules) {
1325
+ for (const rule of rules) {
1326
+ const destinationBucketId = requestRecord(rule)?.["destinationBucketId"];
1327
+ if (typeof destinationBucketId !== "string") continue;
1328
+ const destinationApplicationKeyId = this.buckets.get(destinationBucketId)?.info.replicationConfiguration.value?.asReplicationDestination?.sourceToDestinationKeyMapping[sourceApplicationKeyId];
1329
+ if (destinationApplicationKeyId === void 0) return this.invalidReplicationDestinationBucket();
1330
+ const destinationKeyError = this.validateReplicationApplicationKey(destinationApplicationKeyId, [Capability.WriteFiles], {
1331
+ bucketId: destinationBucketId,
1332
+ role: "replication destination"
1333
+ });
1334
+ if (destinationKeyError !== null) return destinationKeyError;
1335
+ }
1336
+ return null;
1337
+ }
1338
+ validateReplicationApplicationKeys(config, bucketId) {
1339
+ if (!this.strictAuth || config === void 0) return null;
1340
+ const replicationConfiguration = requestRecord(config);
1341
+ const source = requestRecord(replicationConfiguration?.["asReplicationSource"]);
1342
+ if (source !== null) {
1343
+ const sourceApplicationKeyId = source["sourceApplicationKeyId"];
1344
+ if (typeof sourceApplicationKeyId === "string") {
1345
+ const sourceKeyError = this.validateReplicationApplicationKey(sourceApplicationKeyId, [Capability.ReadFiles, Capability.ListFiles], {
1346
+ bucketId,
1347
+ role: "replication source"
1348
+ });
1349
+ if (sourceKeyError !== null) return sourceKeyError;
1350
+ }
1351
+ const rules = source["replicationRules"];
1352
+ if (typeof sourceApplicationKeyId === "string" && Array.isArray(rules)) {
1353
+ const destinationError = this.validateReplicationDestinationBuckets(sourceApplicationKeyId, rules);
1354
+ if (destinationError !== null) return destinationError;
1355
+ }
1356
+ }
1357
+ const mapping = requestRecord(requestRecord(replicationConfiguration?.["asReplicationDestination"])?.["sourceToDestinationKeyMapping"]);
1358
+ if (mapping !== null) for (const [sourceApplicationKeyId, destinationApplicationKeyId] of Object.entries(mapping)) {
1359
+ const sourceKeyError = this.validateReplicationApplicationKey(sourceApplicationKeyId, [Capability.ReadFiles, Capability.ListFiles], { role: "replication source" });
1360
+ if (sourceKeyError !== null) return sourceKeyError;
1361
+ if (typeof destinationApplicationKeyId === "string") {
1362
+ const destinationKeyError = this.validateReplicationApplicationKey(destinationApplicationKeyId, [Capability.WriteFiles], {
1363
+ bucketId,
1364
+ role: "replication destination"
1365
+ });
1366
+ if (destinationKeyError !== null) return destinationKeyError;
1367
+ }
470
1368
  }
471
1369
  return null;
472
1370
  }
@@ -474,10 +1372,7 @@ var B2Simulator = class {
474
1372
  const directBucketId = requestStringField(body, "bucketId");
475
1373
  const directFileName = requestStringField(body, "fileName");
476
1374
  switch (endpoint) {
477
- case "b2_create_bucket": return {
478
- bucketIds: [],
479
- requiresBucketScope: true
480
- };
1375
+ case "b2_create_bucket": return this.createBucketScope(body);
481
1376
  case "b2_list_buckets": return this.listBucketsScope(body, directBucketId);
482
1377
  case "b2_list_file_names":
483
1378
  case "b2_list_file_versions": return {
@@ -513,9 +1408,23 @@ var B2Simulator = class {
513
1408
  };
514
1409
  case "b2_get_bucket_notification_rules": return this.notificationRulesScope(body, directBucketId, false);
515
1410
  case "b2_set_bucket_notification_rules": return this.notificationRulesScope(body, directBucketId, true);
1411
+ case "b2_update_bucket": return this.updateBucketScope(body, directBucketId);
516
1412
  default: return this.defaultRequestScope(directBucketId, directFileName);
517
1413
  }
518
1414
  }
1415
+ createBucketScope(body) {
1416
+ return {
1417
+ bucketIds: replicationDestinationBucketIds(body),
1418
+ requiresBucketScope: true,
1419
+ requiresAccountLevelBucketAccess: true
1420
+ };
1421
+ }
1422
+ updateBucketScope(body, directBucketId) {
1423
+ return {
1424
+ bucketIds: [...directBucketId === void 0 ? [] : [directBucketId], ...replicationDestinationBucketIds(body)],
1425
+ requiresBucketScope: true
1426
+ };
1427
+ }
519
1428
  listBucketsScope(body, directBucketId) {
520
1429
  if (directBucketId !== void 0) return {
521
1430
  bucketIds: [directBucketId],
@@ -613,7 +1522,10 @@ var B2Simulator = class {
613
1522
  const idParam = options.kind === "file" ? "bucketId" : "fileId";
614
1523
  const scopedId = options.kind === "file" ? options.bucketId : options.fileId;
615
1524
  const uploadId = this.genId(options.kind === "file" ? "upload_file" : "upload_part");
616
- const uploadUrl = new URL(`http://localhost:0/b2api/v3/${endpoint}`);
1525
+ const uploadUrl = new URL(b2Url("http://localhost:0", {
1526
+ version: "v3",
1527
+ endpoint
1528
+ }));
617
1529
  uploadUrl.searchParams.set(idParam, scopedId);
618
1530
  uploadUrl.searchParams.set("uploadId", uploadId);
619
1531
  const sourceToken = options.sourceAuthToken === void 0 ? void 0 : this.issuedTokens.get(options.sourceAuthToken);
@@ -686,40 +1598,61 @@ var B2Simulator = class {
686
1598
  }));
687
1599
  }
688
1600
  /**
689
- * Look up the application key matching the `Authorization` header on
1601
+ * Resolve the application key matching the `Authorization` header on
690
1602
  * an `authorize_account` request. The header is in the form
691
1603
  * `Basic base64(applicationKeyId:applicationKey)`.
692
1604
  *
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.
1605
+ * The simulator has two implicit full-access storage credentials:
1606
+ * the documented `test-key-id:test-key` pair used by SDK tests and
1607
+ * `master-key-id:master-key`. In default permissive mode, any other
1608
+ * well-formed Basic credential whose applicationKeyId is not a stored
1609
+ * key also receives that implicit master grant for backward
1610
+ * compatibility. A credential whose applicationKeyId matches a stored
1611
+ * key must use the stored secret and must not be expired.
696
1612
  *
697
1613
  * @param authzHeader - Raw HTTP `Authorization` header value.
698
1614
  *
699
- * @returns The matching key's grant scope, or `null` for the master.
1615
+ * @returns The authorizing key's grant scope or the rejection reason.
700
1616
  */
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;
1617
+ findAuthorizationGrant(authzHeader) {
1618
+ const credentials = parseBasicAuthorizationHeader(authzHeader);
1619
+ if (credentials === null) return { kind: "invalid" };
1620
+ const stored = this.keys.get(credentials.applicationKeyId);
1621
+ if (stored !== void 0) {
1622
+ if (!timingSafeStringEqual(stored.applicationKey, credentials.applicationKey)) return { kind: "invalid" };
1623
+ if (this.isStoredKeyExpired(stored)) return { kind: "expired" };
1624
+ return {
1625
+ kind: "ok",
1626
+ grant: {
1627
+ capabilities: stored.capabilities,
1628
+ bucketIds: stored.bucketIds,
1629
+ namePrefix: stored.namePrefix,
1630
+ applicationKeyId: credentials.applicationKeyId,
1631
+ accountId: stored.accountId,
1632
+ expirationTimestamp: stored.expirationTimestamp
1633
+ }
1634
+ };
1635
+ }
1636
+ if (isSimulatorApplicationKeyId(credentials.applicationKeyId)) return { kind: "invalid" };
1637
+ if (isImplicitStorageMasterCredential(credentials) || !this.strictAuth) return {
1638
+ kind: "ok",
1639
+ grant: {
1640
+ capabilities: SIMULATOR_MASTER_CAPABILITIES,
1641
+ bucketIds: null,
1642
+ namePrefix: null,
1643
+ applicationKeyId: null,
1644
+ accountId: this.accountId,
1645
+ expirationTimestamp: null
708
1646
  }
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
1647
  };
1648
+ return { kind: "invalid" };
1649
+ }
1650
+ canAuthorizePartner(authzHeader) {
1651
+ const credentials = parseBasicAuthorizationHeader(authzHeader);
1652
+ if (credentials === null) return false;
1653
+ if (isImplicitPartnerMasterCredential(credentials)) return true;
1654
+ const stored = this.keys.get(credentials.applicationKeyId);
1655
+ return stored !== void 0 && stored.accountId === this.accountId && !this.isStoredKeyExpired(stored) && timingSafeStringEqual(stored.applicationKey, credentials.applicationKey);
723
1656
  }
724
1657
  /**
725
1658
  * Creates an {@link HttpTransport} that routes requests to this simulator.
@@ -811,7 +1744,7 @@ var B2Simulator = class {
811
1744
  }
812
1745
  /**
813
1746
  * Dispatches a JSON API request to the appropriate handler.
814
- * @param _method - The HTTP method (unused).
1747
+ * @param method - The HTTP method.
815
1748
  * @param origin - The request URL origin used for simulator-issued endpoints.
816
1749
  * @param path - The request URL path containing the B2 endpoint name.
817
1750
  * @param headers - The HTTP request headers; consulted by the
@@ -820,38 +1753,48 @@ var B2Simulator = class {
820
1753
  *
821
1754
  * @returns An object with HTTP status and JSON response body.
822
1755
  */
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) {
1756
+ async handleRequest(method, origin, path, headers, body) {
1757
+ const { endpoint, version, unsupportedVersion } = apiPathParts(path);
1758
+ if (unsupportedVersion !== void 0) return this.error(400, "unsupported_api_version", `B2 API version ${unsupportedVersion} is not supported by the simulator; use v4 or v3`);
1759
+ if (!jsonEndpointAllowsMethod(method, endpoint)) return this.error(405, "method_not_allowed", `${endpoint} does not support ${method.toUpperCase()}`);
1760
+ if (this.strictAuth && !this.partner.isEndpoint(endpoint)) {
827
1761
  const authError = this.authorizeRequest(headers["authorization"], endpoint, this.requestScope(endpoint, body));
828
1762
  if (authError !== null) return authError;
829
1763
  }
830
1764
  switch (endpoint) {
831
- case "b2_authorize_account": return this.authorize(headers["authorization"], origin);
832
- case "b2_create_bucket": return this.createBucket(body);
833
- case "b2_list_buckets": return this.listBuckets(body);
834
- case "b2_delete_bucket": return this.deleteBucket(body);
835
- case "b2_update_bucket": return this.updateBucket(body);
1765
+ case "b2_authorize_account":
1766
+ if (version === "v3" && this.partner.isAuthorizeEnabled()) return this.partner.authorize(headers["authorization"], origin);
1767
+ return this.authorize(headers["authorization"], origin);
1768
+ case PartnerEndpoint.CreateGroupMember: return this.partner.createGroupMember(body, headers["authorization"]);
1769
+ case PartnerEndpoint.EjectGroupMember: return this.partner.ejectGroupMember(body, headers["authorization"]);
1770
+ case PartnerEndpoint.ListGroups: return this.partner.listGroups(body, headers["authorization"]);
1771
+ case PartnerEndpoint.ListGroupMembers: return this.partner.listGroupMembers(body, headers["authorization"]);
1772
+ case PartnerEndpoint.ReserveTrialCreateAccount: return this.partner.reserveTrialCreateAccount(body, headers["authorization"]);
1773
+ case PartnerEndpoint.ListComputers: return this.partner.listComputers(body, headers["authorization"]);
1774
+ case PartnerEndpoint.DeleteComputer: return this.partner.deleteComputer(body, headers["authorization"]);
1775
+ case "b2_create_bucket": return this.createBucket(body, headers["authorization"]);
1776
+ case "b2_list_buckets": return this.listBuckets(body, headers["authorization"]);
1777
+ case "b2_delete_bucket": return this.deleteBucket(body, headers["authorization"]);
1778
+ case "b2_update_bucket": return this.updateBucket(body, headers["authorization"]);
836
1779
  case "b2_get_upload_url": return await this.getUploadUrl(body, headers["authorization"]);
837
1780
  case "b2_list_file_names": return this.listFileNames(body);
838
1781
  case "b2_list_file_versions": return this.listFileVersions(body);
839
1782
  case "b2_get_file_info": return this.getFileInfo(body);
840
1783
  case "b2_hide_file": return this.hideFile(body);
841
- case "b2_delete_file_version": return this.deleteFileVersion(body);
1784
+ case "b2_delete_file_version": return this.deleteFileVersion(body, headers["authorization"]);
842
1785
  case "b2_copy_file": return await this.copyFile(body);
843
- case "b2_start_large_file": return this.startLargeFile(body);
1786
+ case "b2_start_large_file": return await this.startLargeFile(body, headers["authorization"]);
844
1787
  case "b2_get_upload_part_url": return await this.getUploadPartUrl(body, headers["authorization"]);
845
1788
  case "b2_finish_large_file": return this.finishLargeFile(body);
846
1789
  case "b2_cancel_large_file": return this.cancelLargeFile(body);
847
- case "b2_list_unfinished_large_files": return this.listUnfinishedLargeFiles(body);
1790
+ case "b2_list_unfinished_large_files": return this.listUnfinishedLargeFiles(body, headers["authorization"]);
848
1791
  case "b2_list_parts": return this.listParts(body);
849
1792
  case "b2_copy_part": return await this.copyPart(body);
850
1793
  case "b2_get_download_authorization": return this.getDownloadAuthorization(body);
851
- case "b2_create_key": return this.createKey(body, apiVersion);
1794
+ case "b2_create_key": return this.createKey(body, version, headers["authorization"]);
852
1795
  case "b2_list_keys": return this.listKeys(body);
853
1796
  case "b2_delete_key": return this.deleteKey(body);
854
- case "b2_update_file_retention": return this.updateFileRetention(body);
1797
+ case "b2_update_file_retention": return this.updateFileRetention(body, headers["authorization"]);
855
1798
  case "b2_update_file_legal_hold": return this.updateFileLegalHold(body);
856
1799
  case "b2_get_bucket_notification_rules": return this.getBucketNotificationRules(body);
857
1800
  case "b2_set_bucket_notification_rules": return this.setBucketNotificationRules(body);
@@ -881,9 +1824,25 @@ var B2Simulator = class {
881
1824
  const fileName = kind === "part" || headers["x-bz-file-name"] === void 0 ? void 0 : decodeHeaderValue(headers["x-bz-file-name"]);
882
1825
  const authError = await this.validateUploadAuthorization(kind, url, headers["authorization"], fileName);
883
1826
  if (authError !== null) return authError;
1827
+ const contentLengthError = this.validateContentLength(headers, data.byteLength);
1828
+ if (contentLengthError !== null) return contentLengthError;
884
1829
  if (kind === "part") return await this.handleUploadPart(url, headers, data);
885
1830
  return await this.handleUploadFile(url, headers, data);
886
1831
  }
1832
+ validateContentLength(headers, actualLength) {
1833
+ const parsed = parseContentLengthHeader(headers);
1834
+ if (parsed.kind === "absent") return null;
1835
+ if (parsed.kind === "error") return this.error(400, "bad_request", parsed.message);
1836
+ if (parsed.expectedLength !== actualLength) return this.error(400, "bad_request", contentLengthMismatchMessage(parsed.expectedLength, actualLength));
1837
+ return null;
1838
+ }
1839
+ parseCustomUploadTimestamp(value) {
1840
+ if (value === void 0 || value === null) return { timestamp: null };
1841
+ if (!this.customUploadTimestampsEnabled) return this.error(400, "custom_timestamp_not_allowed", "Custom upload timestamps are not enabled for this account.");
1842
+ const timestamp = typeof value === "string" && /^\d+$/.test(value) ? Number(value) : NaN;
1843
+ if (!Number.isSafeInteger(timestamp) || timestamp < 0 || timestamp > this.now()) return this.error(400, "custom_timestamp_invalid", "The request has an invalid custom upload timestamp.");
1844
+ return { timestamp };
1845
+ }
887
1846
  /**
888
1847
  * Handles file download requests (`b2_download_file_by_id`, `/file/` by name).
889
1848
  * Returns the file data along with B2 response headers.
@@ -895,33 +1854,32 @@ var B2Simulator = class {
895
1854
  */
896
1855
  handleDownload(path, headers, method = "GET") {
897
1856
  if (path.includes("b2_download_file_by_id")) {
1857
+ const url = new URL(`http://localhost${path}`);
898
1858
  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) ?? {
1859
+ const authError = this.authorizeDownloadRequest(this.downloadAuthorizationFromRequest(headers, url, { allowQueryAuthorization: false }), "b2_download_file_by_id", this.fileIdScope(url.searchParams.get("fileId") ?? void 0) ?? {
901
1860
  bucketIds: [],
902
1861
  requiresBucketScope: true
903
- });
1862
+ }, url);
904
1863
  if (authError !== null) return this.errorAsDownload(authError);
905
1864
  }
906
- const url = new URL(`http://localhost${path}`);
907
1865
  const fileId = url.searchParams.get("fileId") ?? "";
908
- return this.finalizeDownload(this.downloadById(fileId, headers["range"]), url, method);
1866
+ return this.finalizeDownload(this.downloadById(fileId, headers, requestHeaderValue(headers, "range")), url, method);
909
1867
  }
910
1868
  const fileMatch = path.match(/^([^?]+)/)?.[1]?.match(/\/file\/([^/]+)\/(.+)/);
911
1869
  if (fileMatch) {
912
1870
  const bucketName = decodeURIComponent(fileMatch[1] ?? "");
913
1871
  const fileName = decodeURIComponent(fileMatch[2] ?? "");
1872
+ const url = new URL(`http://localhost${path}`);
914
1873
  if (this.strictAuth) {
915
1874
  const bucket = [...this.buckets.values()].find((b) => b.info.bucketName === bucketName);
916
- const authError = this.authorizeRequest(headers["authorization"], "b2_download_file_by_name", {
1875
+ const authError = this.authorizeDownloadRequest(this.downloadAuthorizationFromRequest(headers, url, { allowQueryAuthorization: true }), "b2_download_file_by_name", {
917
1876
  bucketIds: bucket === void 0 ? [] : [bucket.info.bucketId],
918
1877
  fileNames: [fileName],
919
1878
  requiresBucketScope: true
920
- });
1879
+ }, url);
921
1880
  if (authError !== null) return this.errorAsDownload(authError);
922
1881
  }
923
- const url = new URL(`http://localhost${path}`);
924
- return this.finalizeDownload(this.downloadByName(bucketName, fileName, headers["range"]), url, method);
1882
+ return this.finalizeDownload(this.downloadByName(bucketName, fileName, headers, requestHeaderValue(headers, "range")), url, method);
925
1883
  }
926
1884
  return {
927
1885
  status: 404,
@@ -958,16 +1916,8 @@ var B2Simulator = class {
958
1916
  * @returns The response with overrides applied.
959
1917
  */
960
1918
  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
1919
  const newHeaders = { ...response.headers };
970
- for (const [param, header] of Object.entries(overrideMap)) {
1920
+ for (const [param, header] of Object.entries(DOWNLOAD_RESPONSE_OVERRIDE_HEADERS)) {
971
1921
  const value = url.searchParams.get(param);
972
1922
  if (value !== null) newHeaders[header] = value;
973
1923
  }
@@ -993,6 +1943,10 @@ var B2Simulator = class {
993
1943
  const resolved = await this.resolveUploadSha1(headers["x-bz-content-sha1"], data);
994
1944
  if ("status" in resolved) return resolved;
995
1945
  const { sha1: contentSha1, data: storedData } = resolved;
1946
+ const serverSideEncryption = await uploadServerSideEncryption(headers, bucketDefaultServerSideEncryptionForStorage(bucket.info));
1947
+ if ("status" in serverSideEncryption) return serverSideEncryption;
1948
+ const customUploadTimestamp = this.parseCustomUploadTimestamp(requestHeaderValue(headers, "x-bz-custom-upload-timestamp"));
1949
+ if ("status" in customUploadTimestamp) return customUploadTimestamp;
996
1950
  const fileVersion = this.makeFileVersion({
997
1951
  bucketId,
998
1952
  fileName,
@@ -1001,11 +1955,14 @@ var B2Simulator = class {
1001
1955
  contentSha1,
1002
1956
  fileInfo,
1003
1957
  action: FileAction.Upload,
1004
- serverSideEncryption: uploadServerSideEncryption(headers, bucket.info.defaultServerSideEncryption)
1958
+ serverSideEncryption,
1959
+ ...this.newReplicationStatusField(bucketId, fileName),
1960
+ ...customUploadTimestamp.timestamp !== null ? { uploadTimestamp: customUploadTimestamp.timestamp } : {}
1005
1961
  });
1006
1962
  const stored = {
1007
1963
  fileVersion,
1008
- data: storedData
1964
+ data: storedData,
1965
+ serverSideEncryption
1009
1966
  };
1010
1967
  const existing = bucket.files.get(fileName);
1011
1968
  if (existing) existing.push(stored);
@@ -1055,18 +2012,39 @@ var B2Simulator = class {
1055
2012
  data
1056
2013
  };
1057
2014
  }
2015
+ async validateUploadPartEncryption(large, headers) {
2016
+ if (large.serverSideEncryption.mode !== EncryptionMode.SseC) {
2017
+ if (!hasCustomerEncryptionHeaders(headers)) return null;
2018
+ return this.error(400, "bad_request", "SSE-C upload part headers require an SSE-C large file");
2019
+ }
2020
+ const supplied = await storedServerSideEncryption({
2021
+ mode: EncryptionMode.SseC,
2022
+ algorithm: headers["x-bz-server-side-encryption-customer-algorithm"],
2023
+ customerKey: headers["x-bz-server-side-encryption-customer-key"],
2024
+ customerKeyMd5: headers["x-bz-server-side-encryption-customer-key-md5"]
2025
+ });
2026
+ if ("status" in supplied) return supplied;
2027
+ if (!sameStoredServerSideEncryption(large.serverSideEncryption, supplied)) return this.error(400, "bad_request", "SSE-C upload parts require matching customer encryption headers");
2028
+ return null;
2029
+ }
1058
2030
  async handleUploadPart(url, headers, data) {
1059
2031
  const fileId = new URL(url).searchParams.get("fileId");
1060
2032
  if (!fileId) return this.error(400, "bad_request", "Missing fileId");
1061
2033
  const large = this.largeFiles.get(fileId);
1062
2034
  if (!large) return this.error(400, "bad_request", "Large file not found");
1063
- const partNumber = Number.parseInt(headers["x-bz-part-number"] ?? "0", 10);
2035
+ const partNumber = this.parsePartNumberHeader(requestHeaderValue(headers, "x-bz-part-number"));
2036
+ if (typeof partNumber !== "number") return partNumber;
2037
+ const encryptionError = await this.validateUploadPartEncryption(large, headers);
2038
+ if (encryptionError !== null) return encryptionError;
1064
2039
  const resolved = await this.resolveUploadSha1(headers["x-bz-content-sha1"], data);
1065
2040
  if ("status" in resolved) return resolved;
1066
2041
  const { sha1, data: partData } = resolved;
2042
+ const uploadTimestamp = this.monotonicTimestamp();
1067
2043
  large.parts.set(partNumber, {
1068
2044
  data: partData,
1069
- sha1
2045
+ sha1,
2046
+ contentMd5: null,
2047
+ uploadTimestamp
1070
2048
  });
1071
2049
  return {
1072
2050
  status: 200,
@@ -1075,21 +2053,32 @@ var B2Simulator = class {
1075
2053
  partNumber,
1076
2054
  contentLength: partData.byteLength,
1077
2055
  contentSha1: sha1,
2056
+ contentMd5: null,
1078
2057
  serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
1079
- uploadTimestamp: Date.now()
2058
+ uploadTimestamp
1080
2059
  }
1081
2060
  };
1082
2061
  }
1083
- downloadById(fileId, range) {
2062
+ parsePartNumberHeader(header) {
2063
+ if (header === void 0) return this.error(400, "bad_request", "X-Bz-Part-Number header is required");
2064
+ 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}`);
2065
+ const partNumber = Number(header);
2066
+ return this.validatePartNumber(partNumber) ?? partNumber;
2067
+ }
2068
+ validatePartNumber(partNumber) {
2069
+ if (Number.isInteger(partNumber) && partNumber >= B2_MIN_PART_NUMBER && partNumber <= B2_MAX_PART_NUMBER) return null;
2070
+ return this.error(400, "bad_request", `partNumber must be an integer between ${B2_MIN_PART_NUMBER} and ${B2_MAX_PART_NUMBER}; received ${String(partNumber)}`);
2071
+ }
2072
+ downloadById(fileId, headers, range) {
1084
2073
  const found = this.findFile(fileId);
1085
2074
  if (found === null) return {
1086
2075
  status: 404,
1087
2076
  headers: {},
1088
2077
  data: null
1089
2078
  };
1090
- return this.serveFile(found.stored, range);
2079
+ return this.serveFile(found.stored, headers, range);
1091
2080
  }
1092
- downloadByName(bucketName, fileName, range) {
2081
+ downloadByName(bucketName, fileName, headers, range) {
1093
2082
  for (const bucket of this.buckets.values()) {
1094
2083
  if (bucket.info.bucketName !== bucketName) continue;
1095
2084
  const versions = bucket.files.get(fileName);
@@ -1100,7 +2089,7 @@ var B2Simulator = class {
1100
2089
  headers: {},
1101
2090
  data: null
1102
2091
  };
1103
- return this.serveFile(latest, range);
2092
+ return this.serveFile(latest, headers, range);
1104
2093
  }
1105
2094
  return {
1106
2095
  status: 404,
@@ -1108,7 +2097,41 @@ var B2Simulator = class {
1108
2097
  data: null
1109
2098
  };
1110
2099
  }
1111
- serveFile(stored, range) {
2100
+ validateDownloadEncryption(stored, headers) {
2101
+ const encryption = stored.serverSideEncryption;
2102
+ if (encryption.mode !== EncryptionMode.SseC) return null;
2103
+ const algorithm = headers["x-bz-server-side-encryption-customer-algorithm"];
2104
+ const customerKey = headers["x-bz-server-side-encryption-customer-key"];
2105
+ const customerKeyMd5 = headers["x-bz-server-side-encryption-customer-key-md5"];
2106
+ const customerKeyBytes = customerKey === void 0 ? null : base64ToBytes(customerKey, 32);
2107
+ const normalizedCustomerKeyMd5 = customerKeyMd5 === void 0 ? null : normalizeSizedBase64(customerKeyMd5, 16);
2108
+ 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"));
2109
+ 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"));
2110
+ return null;
2111
+ }
2112
+ async validateSourceServerSideEncryption(stored, encryption) {
2113
+ const expected = stored.serverSideEncryption;
2114
+ if (expected.mode !== EncryptionMode.SseC) return null;
2115
+ if (encryption?.mode !== EncryptionMode.SseC) return this.error(400, "bad_request", "SSE-C source copies require customer encryption");
2116
+ const supplied = await storedServerSideEncryption(encryption);
2117
+ if ("status" in supplied) return supplied;
2118
+ if (!sameStoredServerSideEncryption(expected, supplied)) return this.error(400, "bad_request", "SSE-C source customer encryption does not match");
2119
+ return null;
2120
+ }
2121
+ async resolveCopyDestinationEncryption(destinationBucket, requested) {
2122
+ if (requested !== void 0) return await storedServerSideEncryption(requested);
2123
+ return await storedServerSideEncryption(bucketDefaultServerSideEncryptionForStorage(destinationBucket.info));
2124
+ }
2125
+ async validateCopyPartDestinationEncryption(large, encryption) {
2126
+ if (encryption === void 0) return large.serverSideEncryption.mode === EncryptionMode.SseC ? this.error(400, "bad_request", "SSE-C destination copies require customer encryption") : null;
2127
+ const supplied = await storedServerSideEncryption(encryption);
2128
+ if ("status" in supplied) return supplied;
2129
+ if (!sameStoredServerSideEncryption(large.serverSideEncryption, supplied)) return this.error(400, "bad_request", "Destination server-side encryption does not match");
2130
+ return null;
2131
+ }
2132
+ serveFile(stored, requestHeaders, range) {
2133
+ const encryptionError = this.validateDownloadEncryption(stored, requestHeaders);
2134
+ if (encryptionError !== null) return encryptionError;
1112
2135
  const fullData = stored.data;
1113
2136
  let data = fullData;
1114
2137
  let status = 200;
@@ -1139,6 +2162,10 @@ var B2Simulator = class {
1139
2162
  };
1140
2163
  for (const [key, value] of Object.entries(fv.fileInfo)) headers[`X-Bz-Info-${encodeFileName(key)}`] = encodeFileName(value);
1141
2164
  if (!("src_last_modified_millis" in fv.fileInfo)) headers["X-Bz-Info-src_last_modified_millis"] = String(fv.uploadTimestamp);
2165
+ const unauthorizedToRead = [];
2166
+ this.addDownloadEncryptionHeaders(headers, stored.serverSideEncryption, requestHeaders, unauthorizedToRead);
2167
+ this.addDownloadObjectLockHeaders(headers, fv, requestHeaders, unauthorizedToRead);
2168
+ if (unauthorizedToRead.length > 0) headers[DownloadHeaderName.ClientUnauthorizedToRead] = unauthorizedToRead.join(",");
1142
2169
  if (contentRange !== null) headers["Content-Range"] = contentRange;
1143
2170
  return {
1144
2171
  status,
@@ -1146,39 +2173,58 @@ var B2Simulator = class {
1146
2173
  data
1147
2174
  };
1148
2175
  }
2176
+ addDownloadEncryptionHeaders(headers, encryption, requestHeaders, unauthorizedToRead) {
2177
+ if (encryption.mode === EncryptionMode.None) return;
2178
+ if (!this.requestHasCapability(requestHeaderValue(requestHeaders, "authorization"), Capability.ReadBucketEncryption)) {
2179
+ if (encryption.mode === EncryptionMode.SseB2) unauthorizedToRead.push(DownloadClientUnauthorizedToReadMarker.ServerSideEncryption);
2180
+ else unauthorizedToRead.push(DownloadClientUnauthorizedToReadMarker.ServerSideEncryptionCustomerAlgorithm, DownloadClientUnauthorizedToReadMarker.ServerSideEncryptionCustomerKeyMd5);
2181
+ return;
2182
+ }
2183
+ if (encryption.mode === EncryptionMode.SseB2) {
2184
+ headers[DownloadHeaderName.ServerSideEncryption] = encryption.algorithm;
2185
+ return;
2186
+ }
2187
+ headers[DownloadHeaderName.ServerSideEncryptionCustomerAlgorithm] = encryption.algorithm;
2188
+ headers[DownloadHeaderName.ServerSideEncryptionCustomerKeyMd5] = encryption.customerKeyMd5;
2189
+ }
2190
+ addDownloadObjectLockHeaders(headers, fileVersion, requestHeaders, unauthorizedToRead) {
2191
+ const authToken = requestHeaderValue(requestHeaders, "authorization");
2192
+ const retention = fileVersion.fileRetention.value;
2193
+ if (retention !== null && retention.mode !== null && retention.retainUntilTimestamp !== null) {
2194
+ if (this.requestHasCapability(authToken, Capability.ReadFileRetentions)) {
2195
+ headers[DownloadHeaderName.FileRetentionMode] = retention.mode;
2196
+ headers[DownloadHeaderName.FileRetentionRetainUntilTimestamp] = String(retention.retainUntilTimestamp);
2197
+ } else unauthorizedToRead.push(DownloadClientUnauthorizedToReadMarker.FileRetentionMode, DownloadClientUnauthorizedToReadMarker.FileRetentionRetainUntilTimestamp);
2198
+ }
2199
+ const legalHold = fileVersion.legalHold.value;
2200
+ if (legalHold === null) return;
2201
+ if (this.requestHasCapability(authToken, Capability.ReadFileLegalHolds)) {
2202
+ headers[DownloadHeaderName.FileLegalHold] = legalHold;
2203
+ return;
2204
+ }
2205
+ unauthorizedToRead.push(DownloadClientUnauthorizedToReadMarker.FileLegalHold);
2206
+ }
1149
2207
  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);
2208
+ const resolution = this.findAuthorizationGrant(authzHeader);
2209
+ if (resolution.kind === "invalid") return this.error(401, "unauthorized", "application key is not valid");
2210
+ if (resolution.kind === "expired") return this.error(401, "unauthorized", "application key is expired");
2211
+ const { grant } = resolution;
2212
+ const allowedBuckets = this.allowedBuckets(grant.bucketIds);
2213
+ const legacyBucketId = singleBucketId(grant.bucketIds);
1169
2214
  const legacyBucketName = legacyBucketId === null ? null : this.buckets.get(legacyBucketId)?.info.bucketName ?? null;
1170
2215
  const tokenStr = `sim_auth_token_${this.nextId++}`;
1171
2216
  this.issuedTokens.set(tokenStr, {
1172
- capabilities: keyForAuth?.capabilities ?? capabilities,
1173
- bucketIds: keyForAuth?.bucketIds ?? null,
1174
- namePrefix: keyForAuth?.namePrefix ?? null,
2217
+ accountId: grant.accountId,
2218
+ capabilities: grant.capabilities,
2219
+ bucketIds: grant.bucketIds,
2220
+ namePrefix: grant.namePrefix,
1175
2221
  expiresAt: this.now() + this.authTokenTtlMs,
1176
- applicationKeyId: keyForAuth?.applicationKeyId ?? null
2222
+ applicationKeyId: grant.applicationKeyId
1177
2223
  });
1178
2224
  return {
1179
2225
  status: 200,
1180
2226
  body: {
1181
- accountId: accountId(this.accountId),
2227
+ accountId: accountId(grant.accountId),
1182
2228
  authorizationToken: tokenStr,
1183
2229
  apiInfo: { storageApi: {
1184
2230
  absoluteMinimumPartSize: this.minimumPartSize,
@@ -1187,34 +2233,35 @@ var B2Simulator = class {
1187
2233
  bucketName: legacyBucketName,
1188
2234
  downloadUrl: origin,
1189
2235
  infoType: "storageApi",
1190
- namePrefix: keyForAuth?.namePrefix ?? null,
2236
+ namePrefix: grant.namePrefix,
1191
2237
  recommendedPartSize: this.recommendedPartSize,
1192
2238
  s3ApiUrl: origin,
1193
2239
  allowed: {
1194
- capabilities: keyForAuth?.capabilities ?? capabilities,
2240
+ capabilities: cloneCapabilities(grant.capabilities),
1195
2241
  buckets: allowedBuckets,
1196
2242
  bucketId: legacyBucketId === null ? null : bucketId(legacyBucketId),
1197
2243
  bucketName: legacyBucketName,
1198
- namePrefix: keyForAuth?.namePrefix ?? null
2244
+ namePrefix: grant.namePrefix
1199
2245
  }
1200
2246
  } },
1201
- applicationKeyExpirationTimestamp: null
2247
+ applicationKeyExpirationTimestamp: grant.expirationTimestamp
1202
2248
  }
1203
2249
  };
1204
2250
  }
1205
- createBucket(req) {
2251
+ createBucket(req, authToken) {
1206
2252
  const nameError = validateBucketName(req.bucketName);
1207
2253
  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
- }
2254
+ const bucketTypeError = validateBucketType(req.bucketType);
2255
+ if (bucketTypeError) return this.error(400, bucketTypeError.code, bucketTypeError.message);
2256
+ const objectLockEnabled = nextObjectLockEnabled(false, req.fileLockEnabled);
2257
+ if (typeof objectLockEnabled !== "boolean") return this.error(400, objectLockEnabled.code, objectLockEnabled.message);
2258
+ const configError = validateBucketConfigurationFields(req, { objectLockEnabled });
2259
+ if (configError) return this.error(400, configError.code, configError.message);
2260
+ const replicationKeyError = this.validateReplicationApplicationKeys(req.replicationConfiguration, void 0);
2261
+ if (replicationKeyError !== null) return replicationKeyError;
1212
2262
  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
2263
  const bid = bucketId(this.genId("b2_bucket"));
1214
- const defaultRetention = req.defaultRetention ?? {
1215
- mode: BucketRetentionMode.None,
1216
- period: null
1217
- };
2264
+ const defaultRetention = bucketDefaultRetentionForResponse(req.defaultRetention);
1218
2265
  const info = {
1219
2266
  accountId: accountId(req.accountId),
1220
2267
  bucketId: bid,
@@ -1222,22 +2269,18 @@ var B2Simulator = class {
1222
2269
  bucketType: req.bucketType,
1223
2270
  bucketInfo: req.bucketInfo ?? {},
1224
2271
  corsRules: req.corsRules ?? [],
1225
- defaultServerSideEncryption: req.defaultServerSideEncryption ?? { mode: EncryptionMode.None },
2272
+ defaultServerSideEncryption: publicBucketDefaultServerSideEncryption(req.defaultServerSideEncryption),
1226
2273
  fileLockConfiguration: {
1227
2274
  isClientAuthorizedToRead: true,
1228
2275
  value: {
1229
- isFileLockEnabled: req.fileLockEnabled ?? false,
2276
+ isFileLockEnabled: objectLockEnabled,
1230
2277
  defaultRetention
1231
2278
  }
1232
2279
  },
1233
2280
  lifecycleRules: req.lifecycleRules ?? [],
1234
2281
  options: [],
1235
2282
  revision: 1,
1236
- defaultRetention,
1237
- replicationConfiguration: req.replicationConfiguration ?? {
1238
- asReplicationSource: null,
1239
- asReplicationDestination: null
1240
- }
2283
+ replicationConfiguration: wrapReplicationConfiguration(req.replicationConfiguration)
1241
2284
  };
1242
2285
  this.buckets.set(bid, {
1243
2286
  info,
@@ -1245,49 +2288,79 @@ var B2Simulator = class {
1245
2288
  });
1246
2289
  return {
1247
2290
  status: 200,
1248
- body: info
2291
+ body: this.bucketInfoForResponse(info, authToken)
1249
2292
  };
1250
2293
  }
1251
- listBuckets(req) {
2294
+ listBuckets(req, authToken) {
2295
+ const bucketTypesError = validateBucketTypes(req.bucketTypes);
2296
+ if (bucketTypesError) return this.error(400, bucketTypesError.code, bucketTypesError.message);
1252
2297
  return {
1253
2298
  status: 200,
1254
- 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)) }
2299
+ 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[0] === "all" || req.bucketTypes.some((bucketType) => bucketType === bucket.bucketType)).map((bucket) => this.bucketInfoForResponse(bucket, authToken)) }
1255
2300
  };
1256
2301
  }
1257
- deleteBucket(req) {
2302
+ deleteBucket(req, authToken) {
1258
2303
  const bucket = this.buckets.get(req.bucketId);
1259
2304
  if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
2305
+ if (this.bucketHasContents(bucket)) return this.error(400, "cannot_delete_non_empty_bucket", "Bucket is not empty and cannot be deleted");
1260
2306
  this.buckets.delete(req.bucketId);
1261
2307
  return {
1262
2308
  status: 200,
1263
- body: bucket.info
2309
+ body: this.bucketInfoForResponse(bucket.info, authToken)
1264
2310
  };
1265
2311
  }
1266
- updateBucket(req) {
2312
+ bucketHasContents(bucket) {
2313
+ for (const versions of bucket.files.values()) if (versions.length > 0) return true;
2314
+ for (const large of this.largeFiles.values()) if (large.bucketId === bucket.info.bucketId) return true;
2315
+ return false;
2316
+ }
2317
+ updateBucket(req, authToken) {
1267
2318
  const bucket = this.buckets.get(req["bucketId"]);
1268
2319
  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);
2320
+ const revisionGuard = req["ifRevisionIs"];
2321
+ if (revisionGuard !== void 0) {
2322
+ if (typeof revisionGuard !== "number" || !Number.isInteger(revisionGuard)) return this.error(400, "bad_request", "ifRevisionIs must be an integer");
2323
+ if (revisionGuard !== bucket.info.revision) return this.error(409, "conflict", "ifRevisionIs test failed");
2324
+ }
2325
+ const objectLockEnabled = nextObjectLockEnabled(bucket.info.fileLockConfiguration.value?.isFileLockEnabled ?? false, req["fileLockEnabled"]);
2326
+ if (typeof objectLockEnabled !== "boolean") return this.error(400, objectLockEnabled.code, objectLockEnabled.message);
2327
+ if (req["bucketType"] !== void 0) {
2328
+ const bucketTypeError = validateBucketType(req["bucketType"]);
2329
+ if (bucketTypeError) return this.error(400, bucketTypeError.code, bucketTypeError.message);
1272
2330
  }
2331
+ const configError = validateBucketConfigurationFields({
2332
+ bucketInfo: req["bucketInfo"],
2333
+ corsRules: req["corsRules"],
2334
+ defaultServerSideEncryption: req["defaultServerSideEncryption"],
2335
+ defaultRetention: req["defaultRetention"],
2336
+ lifecycleRules: req["lifecycleRules"],
2337
+ replicationConfiguration: req["replicationConfiguration"]
2338
+ }, { objectLockEnabled });
2339
+ if (configError) return this.error(400, configError.code, configError.message);
2340
+ const replicationKeyError = this.validateReplicationApplicationKeys(req["replicationConfiguration"], req["bucketId"]);
2341
+ if (replicationKeyError !== null) return replicationKeyError;
1273
2342
  const updated = {
1274
2343
  ...bucket.info,
1275
2344
  ...req["bucketType"] !== void 0 ? { bucketType: req["bucketType"] } : {},
1276
2345
  ...req["bucketInfo"] !== void 0 ? { bucketInfo: req["bucketInfo"] } : {},
1277
2346
  ...req["lifecycleRules"] !== void 0 ? { lifecycleRules: req["lifecycleRules"] } : {},
1278
2347
  ...req["corsRules"] !== void 0 ? { corsRules: req["corsRules"] } : {},
1279
- ...req["replicationConfiguration"] !== void 0 ? { replicationConfiguration: req["replicationConfiguration"] } : {},
1280
- ...req["defaultRetention"] !== void 0 ? {
1281
- defaultRetention: req["defaultRetention"],
1282
- fileLockConfiguration: {
1283
- isClientAuthorizedToRead: true,
1284
- value: {
1285
- isFileLockEnabled: bucket.info.fileLockConfiguration.value?.isFileLockEnabled ?? false,
1286
- defaultRetention: req["defaultRetention"]
1287
- }
2348
+ ...req["replicationConfiguration"] !== void 0 ? { replicationConfiguration: wrapReplicationConfiguration(req["replicationConfiguration"]) } : {},
2349
+ ...req["defaultRetention"] !== void 0 ? { fileLockConfiguration: {
2350
+ isClientAuthorizedToRead: true,
2351
+ value: {
2352
+ isFileLockEnabled: objectLockEnabled,
2353
+ defaultRetention: bucketDefaultRetentionForResponse(req["defaultRetention"])
1288
2354
  }
1289
- } : {},
1290
- ...req["defaultServerSideEncryption"] !== void 0 ? { defaultServerSideEncryption: req["defaultServerSideEncryption"] } : {},
2355
+ } } : {},
2356
+ ...req["fileLockEnabled"] !== void 0 && req["defaultRetention"] === void 0 ? { fileLockConfiguration: {
2357
+ isClientAuthorizedToRead: true,
2358
+ value: {
2359
+ isFileLockEnabled: objectLockEnabled,
2360
+ defaultRetention: bucketDefaultRetention(bucket.info) ?? noBucketDefaultRetention()
2361
+ }
2362
+ } } : {},
2363
+ ...req["defaultServerSideEncryption"] !== void 0 ? { defaultServerSideEncryption: publicBucketDefaultServerSideEncryption(req["defaultServerSideEncryption"]) } : {},
1291
2364
  revision: bucket.info.revision + 1
1292
2365
  };
1293
2366
  this.buckets.set(req["bucketId"], {
@@ -1296,7 +2369,7 @@ var B2Simulator = class {
1296
2369
  });
1297
2370
  return {
1298
2371
  status: 200,
1299
- body: updated
2372
+ body: this.bucketInfoForResponse(updated, authToken)
1300
2373
  };
1301
2374
  }
1302
2375
  async getUploadUrl(req, authToken) {
@@ -1322,7 +2395,13 @@ var B2Simulator = class {
1322
2395
  if (countError) return this.error(400, countError.code, countError.message);
1323
2396
  const max = req.maxFileCount ?? 1e3;
1324
2397
  const prefix = req.prefix ?? "";
1325
- let allFiles = [...bucket.files.entries()].filter(([name]) => name.startsWith(prefix)).map(([_, versions]) => versions[versions.length - 1]).filter((v) => v !== void 0).map((v) => v.fileVersion).sort((a, b) => compareB2FileNames(a.fileName, b.fileName));
2398
+ const currentVersions = [...bucket.files.entries()].filter(([name]) => name.startsWith(prefix)).map(([_, versions]) => versions[versions.length - 1]).filter((v) => v !== void 0).map((v) => v.fileVersion).filter((v) => v.action !== FileAction.Hide).sort((a, b) => compareB2FileNames(a.fileName, b.fileName));
2399
+ let allFiles = this.applyListDelimiter({
2400
+ bucketId: req.bucketId,
2401
+ delimiter: req.delimiter,
2402
+ entries: currentVersions,
2403
+ prefix
2404
+ });
1326
2405
  if (req.startFileName) {
1327
2406
  const start = req.startFileName;
1328
2407
  allFiles = allFiles.filter((f) => f.fileName >= start);
@@ -1342,11 +2421,17 @@ var B2Simulator = class {
1342
2421
  if (countError) return this.error(400, countError.code, countError.message);
1343
2422
  const max = req.maxFileCount ?? 1e3;
1344
2423
  const prefix = req.prefix ?? "";
1345
- const allVersions = [...bucket.files.entries()].filter(([name]) => name.startsWith(prefix)).flatMap(([_, versions]) => versions.map((v) => v.fileVersion)).sort((a, b) => {
2424
+ const versions = [...bucket.files.entries()].filter(([name]) => name.startsWith(prefix)).flatMap(([_, versions]) => versions.map((v) => v.fileVersion)).sort((a, b) => {
1346
2425
  const nameCmp = compareB2FileNames(a.fileName, b.fileName);
1347
2426
  if (nameCmp !== 0) return nameCmp;
1348
2427
  return b.uploadTimestamp - a.uploadTimestamp;
1349
2428
  });
2429
+ const allVersions = this.applyListDelimiter({
2430
+ bucketId: req.bucketId,
2431
+ delimiter: req.delimiter,
2432
+ entries: versions,
2433
+ prefix
2434
+ });
1350
2435
  let startIdx = 0;
1351
2436
  if (req.startFileName !== void 0) {
1352
2437
  const startName = req.startFileName;
@@ -1369,6 +2454,48 @@ var B2Simulator = class {
1369
2454
  }
1370
2455
  };
1371
2456
  }
2457
+ applyListDelimiter(params) {
2458
+ if (params.delimiter === void 0 || params.delimiter === "") return params.entries.map((entry) => this.toListEntry(entry));
2459
+ const grouped = [];
2460
+ const seenFolders = /* @__PURE__ */ new Set();
2461
+ for (const entry of params.entries) {
2462
+ const delimiterIndex = entry.fileName.indexOf(params.delimiter, params.prefix.length);
2463
+ if (delimiterIndex === -1) {
2464
+ grouped.push(this.toListEntry(entry));
2465
+ continue;
2466
+ }
2467
+ const folderName = entry.fileName.slice(0, delimiterIndex + params.delimiter.length);
2468
+ if (!seenFolders.has(folderName)) {
2469
+ seenFolders.add(folderName);
2470
+ grouped.push(this.makeFolderListEntry(params.bucketId, folderName));
2471
+ }
2472
+ }
2473
+ return grouped.sort(compareFileVersionListEntries);
2474
+ }
2475
+ toListEntry(entry) {
2476
+ if (entry.action !== FileAction.Hide) return entry;
2477
+ const { contentType: _contentType, fileRetention, legalHold, serverSideEncryption, ...listed } = entry;
2478
+ return {
2479
+ ...listed,
2480
+ action: FileAction.Hide,
2481
+ contentType: HIDE_MARKER_CONTENT_TYPE
2482
+ };
2483
+ }
2484
+ makeFolderListEntry(bucketId$1, fileName) {
2485
+ return {
2486
+ accountId: accountId(this.accountId),
2487
+ action: FileAction.Folder,
2488
+ bucketId: bucketId(bucketId$1),
2489
+ contentLength: 0,
2490
+ contentMd5: null,
2491
+ contentSha1: null,
2492
+ contentType: null,
2493
+ fileId: null,
2494
+ fileInfo: {},
2495
+ fileName,
2496
+ uploadTimestamp: 0
2497
+ };
2498
+ }
1372
2499
  getFileInfo(req) {
1373
2500
  const found = this.findFile(req.fileId);
1374
2501
  if (found === null) return this.error(404, "file_not_present", "File not found");
@@ -1385,7 +2512,7 @@ var B2Simulator = class {
1385
2512
  const fileVersion = this.makeFileVersion({
1386
2513
  bucketId: req.bucketId,
1387
2514
  fileName: req.fileName,
1388
- contentType: "application/octet-stream",
2515
+ contentType: HIDE_MARKER_CONTENT_TYPE,
1389
2516
  contentLength: 0,
1390
2517
  contentSha1: "none",
1391
2518
  action: FileAction.Hide
@@ -1393,7 +2520,8 @@ var B2Simulator = class {
1393
2520
  const existing = bucket.files.get(req.fileName);
1394
2521
  const stored = {
1395
2522
  fileVersion,
1396
- data: /* @__PURE__ */ new Uint8Array(0)
2523
+ data: /* @__PURE__ */ new Uint8Array(0),
2524
+ serverSideEncryption: { mode: EncryptionMode.None }
1397
2525
  };
1398
2526
  if (existing) existing.push(stored);
1399
2527
  else bucket.files.set(req.fileName, [stored]);
@@ -1402,16 +2530,19 @@ var B2Simulator = class {
1402
2530
  body: fileVersion
1403
2531
  };
1404
2532
  }
1405
- deleteFileVersion(req) {
2533
+ deleteFileVersion(req, authToken) {
1406
2534
  const found = this.findFile(req.fileId);
1407
2535
  if (found === null || found.stored.fileVersion.fileName !== req.fileName) return this.error(400, "file_not_present", "File version not found");
1408
2536
  const fv = found.stored.fileVersion;
1409
2537
  const retention = fv.fileRetention?.value;
1410
2538
  const legalHold = fv.legalHold?.value;
1411
2539
  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");
2540
+ if (legalHold === LegalHoldValue.On) return this.error(400, "file_lock_legal_hold_protected", "File is on legal hold and cannot be deleted");
2541
+ 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()}`);
2542
+ if (retention?.mode === RetentionMode.Governance && retention.retainUntilTimestamp !== null && retention.retainUntilTimestamp > now) {
2543
+ const bypassError = this.requireGovernanceBypass(req, authToken, "File is under governance-mode retention; pass bypassGovernance: true to delete");
2544
+ if (bypassError !== null) return bypassError;
2545
+ }
1415
2546
  found.versions.splice(found.index, 1);
1416
2547
  if (found.versions.length === 0) found.bucket.files.delete(req.fileName);
1417
2548
  return {
@@ -1431,6 +2562,10 @@ var B2Simulator = class {
1431
2562
  const destBucketId = req.destinationBucketId ?? found.bucketId;
1432
2563
  const destBucket = this.buckets.get(destBucketId);
1433
2564
  if (!destBucket) return this.error(400, "bad_bucket_id", "Destination bucket not found");
2565
+ const sourceEncryptionError = await this.validateSourceServerSideEncryption(sourceStored, req.sourceServerSideEncryption);
2566
+ if (sourceEncryptionError !== null) return sourceEncryptionError;
2567
+ const destinationEncryption = await this.resolveCopyDestinationEncryption(destBucket, req.destinationServerSideEncryption);
2568
+ if ("status" in destinationEncryption) return destinationEncryption;
1434
2569
  let data = sourceStored.data;
1435
2570
  let contentSha1 = sourceStored.fileVersion.contentSha1 ?? "none";
1436
2571
  if (req.range !== void 0) {
@@ -1463,11 +2598,14 @@ var B2Simulator = class {
1463
2598
  contentLength: data.byteLength,
1464
2599
  contentSha1,
1465
2600
  fileInfo,
1466
- action: FileAction.Copy
2601
+ action: FileAction.Copy,
2602
+ serverSideEncryption: destinationEncryption,
2603
+ ...this.newReplicationStatusField(destBucketId, req.fileName)
1467
2604
  });
1468
2605
  const copied = {
1469
2606
  fileVersion,
1470
- data: new Uint8Array(data)
2607
+ data: new Uint8Array(data),
2608
+ serverSideEncryption: destinationEncryption
1471
2609
  };
1472
2610
  const existing = destBucket.files.get(req.fileName);
1473
2611
  if (existing) existing.push(copied);
@@ -1478,7 +2616,7 @@ var B2Simulator = class {
1478
2616
  body: fileVersion
1479
2617
  };
1480
2618
  }
1481
- startLargeFile(req) {
2619
+ async startLargeFile(req, authToken) {
1482
2620
  const bucket = this.buckets.get(req.bucketId);
1483
2621
  if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
1484
2622
  const nameError = validateFileName(req.fileName);
@@ -1487,17 +2625,23 @@ var B2Simulator = class {
1487
2625
  const infoError = validateFileInfo(req.fileInfo);
1488
2626
  if (infoError) return this.error(400, infoError.code, infoError.message);
1489
2627
  }
2628
+ const customUploadTimestamp = this.parseCustomUploadTimestamp(req.customUploadTimestamp);
2629
+ if ("status" in customUploadTimestamp) return customUploadTimestamp;
1490
2630
  const fid = this.genId("4_z");
1491
- const uploadTimestamp = this.monotonicTimestamp();
2631
+ const uploadTimestamp = customUploadTimestamp.timestamp ?? this.monotonicTimestamp();
2632
+ const serverSideEncryption = await storedServerSideEncryption(req.serverSideEncryption ?? bucketDefaultServerSideEncryptionForStorage(bucket.info));
2633
+ if ("status" in serverSideEncryption) return serverSideEncryption;
2634
+ const replicationStatus = this.replicationStatusForNewFile(req.bucketId, req.fileName);
1492
2635
  const large = {
1493
2636
  fileId: fid,
1494
2637
  bucketId: req.bucketId,
1495
2638
  fileName: req.fileName,
1496
2639
  contentType: req.contentType,
1497
2640
  fileInfo: req.fileInfo ?? {},
1498
- fileRetention: req.fileRetention ?? defaultFileRetention(bucket.info.defaultRetention, uploadTimestamp),
2641
+ fileRetention: req.fileRetention ?? defaultFileRetention(bucketDefaultRetention(bucket.info), uploadTimestamp),
1499
2642
  legalHold: req.legalHold ?? null,
1500
- serverSideEncryption: req.serverSideEncryption ?? bucket.info.defaultServerSideEncryption,
2643
+ ...this.replicationStatusMetadataFor(replicationStatus),
2644
+ serverSideEncryption,
1501
2645
  uploadTimestamp,
1502
2646
  parts: /* @__PURE__ */ new Map()
1503
2647
  };
@@ -1515,14 +2659,9 @@ var B2Simulator = class {
1515
2659
  contentLength: 0,
1516
2660
  contentSha1: "none",
1517
2661
  contentMd5: null,
1518
- fileRetention: {
1519
- isClientAuthorizedToRead: true,
1520
- value: large.fileRetention
1521
- },
1522
- legalHold: {
1523
- isClientAuthorizedToRead: true,
1524
- value: large.legalHold
1525
- },
2662
+ fileRetention: readableFileRetention(large.fileRetention, this.requestHasCapability(authToken, Capability.ReadFileRetentions)),
2663
+ legalHold: readableLegalHold(large.legalHold, this.requestHasCapability(authToken, Capability.ReadFileLegalHolds)),
2664
+ ...this.replicationStatusMetadataFor(large.replicationStatus),
1526
2665
  serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
1527
2666
  uploadTimestamp: large.uploadTimestamp
1528
2667
  }
@@ -1552,17 +2691,11 @@ var B2Simulator = class {
1552
2691
  const bucket = this.buckets.get(large.bucketId);
1553
2692
  if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
1554
2693
  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`);
2694
+ 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}`);
2695
+ 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
2696
  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
- }
2697
+ 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`);
2698
+ 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
2699
  let totalSize = 0;
1567
2700
  for (const [_, part] of sortedParts) totalSize += part.data.byteLength;
1568
2701
  const combined = new Uint8Array(totalSize);
@@ -1581,11 +2714,14 @@ var B2Simulator = class {
1581
2714
  action: FileAction.Upload,
1582
2715
  fileRetention: large.fileRetention,
1583
2716
  legalHold: large.legalHold,
1584
- serverSideEncryption: large.serverSideEncryption
2717
+ serverSideEncryption: large.serverSideEncryption,
2718
+ uploadTimestamp: large.uploadTimestamp,
2719
+ ...this.replicationStatusMetadataFor(large.replicationStatus)
1585
2720
  });
1586
2721
  const stored = {
1587
2722
  fileVersion,
1588
- data: combined
2723
+ data: combined,
2724
+ serverSideEncryption: large.serverSideEncryption
1589
2725
  };
1590
2726
  const existing = bucket.files.get(large.fileName);
1591
2727
  if (existing) existing.push(stored);
@@ -1611,7 +2747,7 @@ var B2Simulator = class {
1611
2747
  }
1612
2748
  };
1613
2749
  }
1614
- listUnfinishedLargeFiles(req) {
2750
+ listUnfinishedLargeFiles(req, authToken) {
1615
2751
  const countError = validateMaxCount(req.maxFileCount, "b2_list_unfinished_large_files");
1616
2752
  if (countError) return this.error(400, countError.code, countError.message);
1617
2753
  const prefix = req.namePrefix ?? "";
@@ -1622,10 +2758,13 @@ var B2Simulator = class {
1622
2758
  const found = candidates.findIndex((f) => f.fileId === req.startFileId);
1623
2759
  startIndex = found >= 0 ? found : 0;
1624
2760
  }
2761
+ const slice = candidates.slice(startIndex, startIndex + max);
2762
+ const canReadFileRetentions = this.requestHasCapability(authToken, Capability.ReadFileRetentions);
2763
+ const canReadFileLegalHolds = this.requestHasCapability(authToken, Capability.ReadFileLegalHolds);
1625
2764
  return {
1626
2765
  status: 200,
1627
2766
  body: {
1628
- files: candidates.slice(startIndex, startIndex + max).map((f) => ({
2767
+ files: slice.map((f) => ({
1629
2768
  fileId: f.fileId,
1630
2769
  fileName: f.fileName,
1631
2770
  accountId: this.accountId,
@@ -1636,14 +2775,9 @@ var B2Simulator = class {
1636
2775
  contentSha1: "none",
1637
2776
  contentMd5: null,
1638
2777
  fileInfo: f.fileInfo,
1639
- fileRetention: {
1640
- isClientAuthorizedToRead: true,
1641
- value: f.fileRetention
1642
- },
1643
- legalHold: {
1644
- isClientAuthorizedToRead: true,
1645
- value: f.legalHold
1646
- },
2778
+ fileRetention: readableFileRetention(f.fileRetention, canReadFileRetentions),
2779
+ legalHold: readableLegalHold(f.legalHold, canReadFileLegalHolds),
2780
+ ...this.replicationStatusMetadataFor(f.replicationStatus),
1647
2781
  serverSideEncryption: publicServerSideEncryption(f.serverSideEncryption),
1648
2782
  uploadTimestamp: f.uploadTimestamp
1649
2783
  })),
@@ -1663,7 +2797,9 @@ var B2Simulator = class {
1663
2797
  partNumber,
1664
2798
  contentLength: part.data.byteLength,
1665
2799
  contentSha1: part.sha1,
1666
- uploadTimestamp: Date.now()
2800
+ contentMd5: part.contentMd5,
2801
+ serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
2802
+ uploadTimestamp: part.uploadTimestamp
1667
2803
  }));
1668
2804
  return {
1669
2805
  status: 200,
@@ -1676,22 +2812,29 @@ var B2Simulator = class {
1676
2812
  async copyPart(req) {
1677
2813
  const large = this.largeFiles.get(req.largeFileId);
1678
2814
  if (!large) return this.error(400, "bad_request", "Large file not found");
2815
+ const partNumberError = this.validatePartNumber(req.partNumber);
2816
+ if (partNumberError !== null) return partNumberError;
1679
2817
  const found = this.findFile(req.sourceFileId);
1680
2818
  if (found === null) return this.error(404, "file_not_present", "Source file not found");
1681
2819
  const sourceStored = found.stored;
2820
+ const sourceEncryptionError = await this.validateSourceServerSideEncryption(sourceStored, req.sourceServerSideEncryption);
2821
+ if (sourceEncryptionError !== null) return sourceEncryptionError;
2822
+ const destinationEncryptionError = await this.validateCopyPartDestinationEncryption(large, req.destinationServerSideEncryption);
2823
+ if (destinationEncryptionError !== null) return destinationEncryptionError;
1682
2824
  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
- }
2825
+ if (req.range !== void 0) {
2826
+ const parsed = parseRangeHeader(req.range, sourceStored.data.byteLength);
2827
+ if (parsed.kind === "malformed") return this.error(400, "bad_request", `Malformed copy range: ${req.range}`);
2828
+ if (parsed.kind === "unsatisfiable") return this.error(416, "range_not_satisfiable", `Unsatisfiable copy range: ${req.range}`);
2829
+ partData = sourceStored.data.subarray(parsed.start, parsed.end + 1);
1690
2830
  }
1691
2831
  const sha1 = await sha1Hex(partData);
2832
+ const uploadTimestamp = this.monotonicTimestamp();
1692
2833
  large.parts.set(req.partNumber, {
1693
2834
  data: new Uint8Array(partData),
1694
- sha1
2835
+ sha1,
2836
+ contentMd5: null,
2837
+ uploadTimestamp
1695
2838
  });
1696
2839
  return {
1697
2840
  status: 200,
@@ -1699,39 +2842,80 @@ var B2Simulator = class {
1699
2842
  fileId: req.largeFileId,
1700
2843
  partNumber: req.partNumber,
1701
2844
  contentLength: partData.byteLength,
1702
- contentSha1: sha1
2845
+ contentSha1: sha1,
2846
+ contentMd5: null,
2847
+ serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
2848
+ uploadTimestamp
1703
2849
  }
1704
2850
  };
1705
2851
  }
1706
2852
  getDownloadAuthorization(req) {
2853
+ this.purgeExpiredDownloadAuthorizationTokens();
1707
2854
  if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
2855
+ const prefixError = validateDownloadAuthorizationPrefix(req.fileNamePrefix);
2856
+ if (prefixError) return this.error(400, prefixError.code, prefixError.message);
2857
+ const durationError = validateDownloadAuthorizationDuration(req.validDurationInSeconds);
2858
+ if (durationError) return this.error(400, durationError.code, durationError.message);
2859
+ const overrideError = validateDownloadResponseOverrides(req);
2860
+ if (overrideError) return this.error(400, overrideError.code, overrideError.message);
2861
+ let authorizationToken = randomDownloadAuthorizationToken();
2862
+ while (this.downloadAuthorizationTokens.has(authorizationToken)) authorizationToken = randomDownloadAuthorizationToken();
2863
+ const expiresAt = this.now() + req.validDurationInSeconds * 1e3;
2864
+ this.downloadAuthorizationTokens.set(authorizationToken, {
2865
+ bucketId: req.bucketId,
2866
+ fileNamePrefix: req.fileNamePrefix,
2867
+ expiresAt,
2868
+ responseHeaderOverrides: pickDownloadResponseOverrides(req)
2869
+ });
2870
+ this.pushDownloadAuthorizationExpiry({
2871
+ token: authorizationToken,
2872
+ expiresAt
2873
+ });
1708
2874
  return {
1709
2875
  status: 200,
1710
2876
  body: {
1711
2877
  bucketId: req.bucketId,
1712
2878
  fileNamePrefix: req.fileNamePrefix,
1713
- authorizationToken: `sim_dl_auth_${this.genId("tok")}`
2879
+ authorizationToken
1714
2880
  }
1715
2881
  };
1716
2882
  }
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);
2883
+ createKeyCapabilitiesOutsideGrant(authToken, requested) {
2884
+ const creator = authToken === void 0 ? void 0 : this.issuedTokens.get(authToken);
2885
+ if (creator === void 0) return this.strictAuth ? requested : [];
2886
+ if (creator.applicationKeyId === null) return [];
2887
+ const granted = new Set(creator.capabilities);
2888
+ return requested.filter((capability) => !granted.has(capability));
2889
+ }
2890
+ createKeyCreatorAccountId(authToken) {
2891
+ return (authToken === void 0 ? void 0 : this.issuedTokens.get(authToken))?.accountId ?? this.accountId;
2892
+ }
2893
+ createKey(req, version, authToken) {
2894
+ if (version === "v4" && hasOwnField(req, "bucketId")) return this.error(400, "bad_request", "bucketId is not accepted by v4 b2_create_key; use bucketIds");
2895
+ 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");
2896
+ const bucketIds = version !== "v4" && req.bucketId !== void 0 ? Object.freeze([req.bucketId]) : normalizeKeyBucketIds(req);
1721
2897
  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");
2898
+ const creatorAccountId = this.createKeyCreatorAccountId(authToken);
2899
+ if (req.accountId !== creatorAccountId) return this.error(400, "bad_request", "accountId must match authorized account");
2900
+ const keyNameError = validateCreateKeyName(req.keyName);
2901
+ if (keyNameError) return this.error(400, keyNameError.code, keyNameError.message);
2902
+ const keyName = req.keyName;
2903
+ const capabilitiesError = validateCreateKeyCapabilities(req.capabilities);
2904
+ if (capabilitiesError) return this.error(400, capabilitiesError.code, capabilitiesError.message);
2905
+ const capabilities = normalizeCreateKeyCapabilities(req.capabilities);
2906
+ const unauthorizedCapabilities = this.createKeyCapabilitiesOutsideGrant(authToken, capabilities);
2907
+ if (unauthorizedCapabilities.length > 0) return this.error(400, "bad_request", `requested capabilities exceed creator grant: ${unauthorizedCapabilities.join(", ")}`);
2908
+ 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
2909
  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
2910
  const stored = {
1727
2911
  applicationKeyId: kid,
1728
- keyName: req.keyName,
1729
- capabilities: req.capabilities,
1730
- accountId: req.accountId,
1731
- applicationKey: appKey,
2912
+ keyName,
2913
+ capabilities,
2914
+ accountId: creatorAccountId,
2915
+ applicationKey: this.genId("sim_secret"),
1732
2916
  bucketIds,
1733
2917
  namePrefix,
1734
- expirationTimestamp: expiration
2918
+ expirationTimestamp: req.validDurationInSeconds !== void 0 ? this.now() + req.validDurationInSeconds * 1e3 : null
1735
2919
  };
1736
2920
  this.keys.set(kid, stored);
1737
2921
  return {
@@ -1740,7 +2924,7 @@ var B2Simulator = class {
1740
2924
  keyName: stored.keyName,
1741
2925
  applicationKeyId: stored.applicationKeyId,
1742
2926
  applicationKey: stored.applicationKey,
1743
- capabilities: stored.capabilities,
2927
+ capabilities: cloneCapabilities(stored.capabilities),
1744
2928
  accountId: stored.accountId,
1745
2929
  expirationTimestamp: stored.expirationTimestamp,
1746
2930
  bucketIds: cloneBucketIds(stored.bucketIds),
@@ -1765,7 +2949,7 @@ var B2Simulator = class {
1765
2949
  keys: allKeys.slice(0, max).map((k) => ({
1766
2950
  keyName: k.keyName,
1767
2951
  applicationKeyId: k.applicationKeyId,
1768
- capabilities: k.capabilities,
2952
+ capabilities: cloneCapabilities(k.capabilities),
1769
2953
  accountId: k.accountId,
1770
2954
  expirationTimestamp: k.expirationTimestamp,
1771
2955
  bucketIds: cloneBucketIds(k.bucketIds),
@@ -1791,7 +2975,7 @@ var B2Simulator = class {
1791
2975
  body: {
1792
2976
  keyName: key.keyName,
1793
2977
  applicationKeyId: key.applicationKeyId,
1794
- capabilities: key.capabilities,
2978
+ capabilities: cloneCapabilities(key.capabilities),
1795
2979
  accountId: key.accountId,
1796
2980
  expirationTimestamp: key.expirationTimestamp,
1797
2981
  bucketIds: cloneBucketIds(key.bucketIds),
@@ -1801,31 +2985,45 @@ var B2Simulator = class {
1801
2985
  }
1802
2986
  };
1803
2987
  }
1804
- updateFileRetention(req) {
2988
+ updateFileRetention(req, authToken) {
1805
2989
  const found = this.findFile(req.fileId);
1806
2990
  if (found === null || found.stored.fileVersion.fileName !== req.fileName) return this.error(404, "file_not_present", "File not found");
2991
+ const fileLockError = this.requireFileLockEnabled(found.bucket);
2992
+ if (fileLockError !== null) return fileLockError;
2993
+ const fileRetention = parseFileRetentionValue(req.fileRetention, Date.now());
2994
+ if (fileRetention === null) return this.error(400, "bad_request", "fileRetention must use a valid mode and retainUntilTimestamp pair");
2995
+ const current = found.stored.fileVersion.fileRetention?.value;
2996
+ 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");
2997
+ if (requiresGovernanceBypass(current, fileRetention)) {
2998
+ const bypassError = this.requireGovernanceBypass(req, authToken, "Governance-mode retention cannot be shortened, removed, or converted without bypassGovernance");
2999
+ if (bypassError !== null) return bypassError;
3000
+ }
1807
3001
  found.versions[found.index] = {
1808
3002
  fileVersion: {
1809
3003
  ...found.stored.fileVersion,
1810
3004
  fileRetention: {
1811
3005
  isClientAuthorizedToRead: true,
1812
- value: req.fileRetention
3006
+ value: fileRetention
1813
3007
  }
1814
3008
  },
1815
- data: found.stored.data
3009
+ data: found.stored.data,
3010
+ serverSideEncryption: found.stored.serverSideEncryption
1816
3011
  };
1817
3012
  return {
1818
3013
  status: 200,
1819
3014
  body: {
1820
3015
  fileName: req.fileName,
1821
3016
  fileId: req.fileId,
1822
- fileRetention: req.fileRetention
3017
+ fileRetention
1823
3018
  }
1824
3019
  };
1825
3020
  }
1826
3021
  updateFileLegalHold(req) {
1827
3022
  const found = this.findFile(req.fileId);
1828
3023
  if (found === null || found.stored.fileVersion.fileName !== req.fileName) return this.error(404, "file_not_present", "File not found");
3024
+ const fileLockError = this.requireFileLockEnabled(found.bucket);
3025
+ if (fileLockError !== null) return fileLockError;
3026
+ if (req.legalHold !== LegalHoldValue.On && req.legalHold !== LegalHoldValue.Off) return this.error(400, "bad_request", "legalHold must be \"on\" or \"off\"");
1829
3027
  found.versions[found.index] = {
1830
3028
  fileVersion: {
1831
3029
  ...found.stored.fileVersion,
@@ -1834,7 +3032,8 @@ var B2Simulator = class {
1834
3032
  value: req.legalHold
1835
3033
  }
1836
3034
  },
1837
- data: found.stored.data
3035
+ data: found.stored.data,
3036
+ serverSideEncryption: found.stored.serverSideEncryption
1838
3037
  };
1839
3038
  return {
1840
3039
  status: 200,
@@ -1858,6 +3057,8 @@ var B2Simulator = class {
1858
3057
  }
1859
3058
  setBucketNotificationRules(req) {
1860
3059
  if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
3060
+ const rulesError = validateNotificationRules(req.eventNotificationRules);
3061
+ if (rulesError) return this.error(400, rulesError.code, rulesError.message);
1861
3062
  this.notificationRules.set(req.bucketId, req.eventNotificationRules);
1862
3063
  return {
1863
3064
  status: 200,
@@ -1915,11 +3116,23 @@ var B2Simulator = class {
1915
3116
  isClientAuthorizedToRead: true,
1916
3117
  value: params.legalHold ?? null
1917
3118
  },
1918
- replicationStatus: null,
3119
+ ...this.replicationStatusMetadataFor(params.replicationStatus),
1919
3120
  serverSideEncryption: publicServerSideEncryption(params.serverSideEncryption ?? { mode: EncryptionMode.None }),
1920
- uploadTimestamp: this.monotonicTimestamp()
3121
+ uploadTimestamp: params.uploadTimestamp ?? this.monotonicTimestamp()
1921
3122
  };
1922
3123
  }
3124
+ newReplicationStatusField(bucketId, fileName) {
3125
+ return this.replicationStatusMetadataFor(this.replicationStatusForNewFile(bucketId, fileName));
3126
+ }
3127
+ replicationStatusMetadataFor(replicationStatus) {
3128
+ return replicationStatus === void 0 ? {} : { replicationStatus };
3129
+ }
3130
+ replicationStatusForNewFile(bucketId, fileName) {
3131
+ return this.matchingEnabledReplicationRules(bucketId, fileName).length > 0 ? "PENDING" : void 0;
3132
+ }
3133
+ matchingEnabledReplicationRules(bucketId, fileName) {
3134
+ return (this.buckets.get(bucketId)?.info.replicationConfiguration.value?.asReplicationSource?.replicationRules)?.filter((rule) => rule.isEnabled && fileName.startsWith(rule.fileNamePrefix)) ?? [];
3135
+ }
1923
3136
  error(status, code, message) {
1924
3137
  return {
1925
3138
  status,
@@ -1947,8 +3160,7 @@ var B2Simulator = class {
1947
3160
  * used to match against `EventNotificationRule.eventTypes` globs.
1948
3161
  */
1949
3162
  firePostUploadHooks(fileVersion, bucketId, eventType) {
1950
- const bucket = this.buckets.get(bucketId);
1951
- if (!bucket) return;
3163
+ if (!this.buckets.get(bucketId)) return;
1952
3164
  if (this.onWebhookDeliver !== void 0) {
1953
3165
  const rules = this.notificationRules.get(bucketId) ?? [];
1954
3166
  for (const rule of rules) {
@@ -1962,18 +3174,13 @@ var B2Simulator = class {
1962
3174
  }));
1963
3175
  }
1964
3176
  }
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
- }
3177
+ if (this.onReplicate !== void 0) for (const rule of this.matchingEnabledReplicationRules(bucketId, fileVersion.fileName)) {
3178
+ const hook = this.onReplicate;
3179
+ this.dispatchHook("replication", () => hook({
3180
+ sourceFileVersion: fileVersion,
3181
+ sourceBucketId: bucketId,
3182
+ destinationBucketId: rule.destinationBucketId
3183
+ }));
1977
3184
  }
1978
3185
  }
1979
3186
  /**
@@ -2057,6 +3264,83 @@ function buildFaultResponse(fault) {
2057
3264
  arrayBuffer: () => Promise.resolve(utf8Encoder.encode(body).buffer)
2058
3265
  };
2059
3266
  }
3267
+ function rawUrlPathContainsLiteralBackslash(rawUrl) {
3268
+ const schemeEnd = rawUrl.indexOf("://");
3269
+ const authorityStart = schemeEnd === -1 ? 0 : schemeEnd + 3;
3270
+ const urlEndCandidates = [rawUrl.indexOf("?", authorityStart), rawUrl.indexOf("#", authorityStart)].filter((index) => index !== -1);
3271
+ const urlEnd = urlEndCandidates.length === 0 ? rawUrl.length : Math.min(...urlEndCandidates);
3272
+ const pathStartCandidates = [rawUrl.indexOf("/", authorityStart), rawUrl.indexOf("\\", authorityStart)].filter((index) => index !== -1 && index < urlEnd);
3273
+ if (pathStartCandidates.length === 0) return false;
3274
+ const pathStart = Math.min(...pathStartCandidates);
3275
+ const pathEndCandidates = [rawUrl.indexOf("?", pathStart), rawUrl.indexOf("#", pathStart)].filter((index) => index !== -1);
3276
+ const pathEnd = pathEndCandidates.length === 0 ? rawUrl.length : Math.min(...pathEndCandidates);
3277
+ return rawUrl.slice(pathStart, pathEnd).includes("\\");
3278
+ }
3279
+ function badRequestJson(message) {
3280
+ return {
3281
+ status: 400,
3282
+ body: {
3283
+ status: 400,
3284
+ code: "bad_request",
3285
+ message
3286
+ }
3287
+ };
3288
+ }
3289
+ function checkedUploadBody(data, expectedLength) {
3290
+ if (expectedLength !== null && data.byteLength !== expectedLength) return { error: badRequestJson(contentLengthMismatchMessage(expectedLength, data.byteLength)) };
3291
+ return { data };
3292
+ }
3293
+ function copyBytes(bytes) {
3294
+ const copy = new Uint8Array(bytes.byteLength);
3295
+ copy.set(bytes);
3296
+ return copy;
3297
+ }
3298
+ function concatenateChunks(chunks, totalLength) {
3299
+ if (chunks.length === 0) return /* @__PURE__ */ new Uint8Array(0);
3300
+ if (chunks.length === 1) return copyBytes(chunks[0]);
3301
+ const data = new Uint8Array(totalLength);
3302
+ let offset = 0;
3303
+ for (const chunk of chunks) {
3304
+ data.set(chunk, offset);
3305
+ offset += chunk.byteLength;
3306
+ }
3307
+ return data;
3308
+ }
3309
+ async function readStreamingUploadBody(stream, expectedLength) {
3310
+ const reader = stream.getReader();
3311
+ const chunks = [];
3312
+ let totalLength = 0;
3313
+ try {
3314
+ while (true) {
3315
+ const { done, value } = await reader.read();
3316
+ if (done) break;
3317
+ if (value === void 0) continue;
3318
+ const chunk = value instanceof Uint8Array ? value : new Uint8Array(value);
3319
+ totalLength += chunk.byteLength;
3320
+ if (expectedLength !== null && totalLength > expectedLength) {
3321
+ await reader.cancel().catch(() => void 0);
3322
+ return { error: badRequestJson(contentLengthMismatchMessage(expectedLength, totalLength)) };
3323
+ }
3324
+ chunks.push(chunk);
3325
+ }
3326
+ } finally {
3327
+ reader.releaseLock();
3328
+ }
3329
+ if (expectedLength !== null && totalLength !== expectedLength) return { error: badRequestJson(contentLengthMismatchMessage(expectedLength, totalLength)) };
3330
+ return { data: concatenateChunks(chunks, totalLength) };
3331
+ }
3332
+ async function readUploadBody(body, expectedLength) {
3333
+ if (body instanceof ReadableStream) return readStreamingUploadBody(body, expectedLength);
3334
+ if (body instanceof ArrayBuffer) return checkedUploadBody(new Uint8Array(body), expectedLength);
3335
+ if (ArrayBuffer.isView(body)) {
3336
+ const view = body;
3337
+ return checkedUploadBody(copyBytes(new Uint8Array(view.buffer, view.byteOffset, view.byteLength)), expectedLength);
3338
+ }
3339
+ if (typeof Blob !== "undefined" && body instanceof Blob) {
3340
+ if (expectedLength !== null && body.size !== expectedLength) return { error: badRequestJson(contentLengthMismatchMessage(expectedLength, body.size)) };
3341
+ }
3342
+ return checkedUploadBody(new Uint8Array(await new Response(body).arrayBuffer()), expectedLength);
3343
+ }
2060
3344
  var SimulatorTransport = class {
2061
3345
  sim;
2062
3346
  constructor(sim) {
@@ -2066,6 +3350,12 @@ var SimulatorTransport = class {
2066
3350
  const url = request.url;
2067
3351
  const fault = this.sim.consumeMatchingFault(url);
2068
3352
  if (fault !== null) return buildFaultResponse(fault);
3353
+ if (rawUrlPathContainsLiteralBackslash(url)) return buildFaultResponse({
3354
+ on: "\\",
3355
+ status: 400,
3356
+ code: "bad_request",
3357
+ message: "URL path must not contain literal backslashes"
3358
+ });
2069
3359
  const headers = {};
2070
3360
  if (request.headers) for (const [k, v] of Object.entries(request.headers)) headers[k.toLowerCase()] = v;
2071
3361
  const parsedUrl = new URL(url);
@@ -2073,10 +3363,15 @@ var SimulatorTransport = class {
2073
3363
  const isUpload = endpoint === "b2_upload_file" || endpoint === "b2_upload_part";
2074
3364
  if (parsedUrl.pathname.includes("b2_download_file_by_id") || parsedUrl.pathname.includes("/file/")) {
2075
3365
  const method = request.method === "HEAD" ? "HEAD" : "GET";
2076
- const result = this.sim.handleDownload(parsedUrl.pathname + parsedUrl.search, headers, method);
3366
+ const result = await this.sim.handleDownload(parsedUrl.pathname + parsedUrl.search, headers, method);
2077
3367
  const data = result.data ?? /* @__PURE__ */ new Uint8Array(0);
2078
3368
  const responseHeaders = new Headers(result.headers);
2079
3369
  responseHeaders.set("Content-Type", result.headers["Content-Type"] ?? "application/octet-stream");
3370
+ let decodedText = null;
3371
+ const text = () => {
3372
+ decodedText ??= utf8Decoder.decode(data);
3373
+ return decodedText;
3374
+ };
2080
3375
  const body = method === "HEAD" || result.data === null ? null : new ReadableStream({ start(controller) {
2081
3376
  controller.enqueue(data);
2082
3377
  controller.close();
@@ -2085,16 +3380,27 @@ var SimulatorTransport = class {
2085
3380
  status: result.status,
2086
3381
  headers: responseHeaders,
2087
3382
  body,
2088
- json: () => Promise.reject(/* @__PURE__ */ new Error("Download response is not JSON")),
2089
- text: () => Promise.resolve(utf8Decoder.decode(data)),
3383
+ json: () => {
3384
+ try {
3385
+ return Promise.resolve(JSON.parse(text()));
3386
+ } catch {
3387
+ return Promise.reject(/* @__PURE__ */ new Error("Download response is not JSON"));
3388
+ }
3389
+ },
3390
+ text: () => Promise.resolve(text()),
2090
3391
  arrayBuffer: () => Promise.resolve(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength))
2091
3392
  };
2092
3393
  }
2093
3394
  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);
3395
+ if (isUpload) {
3396
+ const parsedContentLength = parseContentLengthHeader(headers);
3397
+ if (parsedContentLength.kind === "error") result = badRequestJson(parsedContentLength.message);
3398
+ else {
3399
+ const uploadBody = await readUploadBody(request.body ?? /* @__PURE__ */ new Uint8Array(0), parsedContentLength.kind === "ok" ? parsedContentLength.expectedLength : null);
3400
+ result = "error" in uploadBody ? uploadBody.error : await this.sim.handleUpload(url, headers, uploadBody.data);
3401
+ }
2097
3402
  } else {
3403
+ const requestPath = apiPathParts(parsedUrl.pathname);
2098
3404
  let body = null;
2099
3405
  if (request.body) {
2100
3406
  const text = typeof request.body === "string" ? request.body : await new Response(request.body).text();
@@ -2103,7 +3409,7 @@ var SimulatorTransport = class {
2103
3409
  } catch {
2104
3410
  body = text;
2105
3411
  }
2106
- }
3412
+ } else if (request.method.toUpperCase() === "GET" && isJsonQueryEndpoint(requestPath.endpoint)) body = queryParamsBody(parsedUrl.searchParams, requestPath.endpoint);
2107
3413
  result = await this.sim.handleRequest(request.method, parsedUrl.origin, parsedUrl.pathname, headers, body);
2108
3414
  }
2109
3415
  const responseBody = JSON.stringify(result.body);
@@ -2121,6 +3427,6 @@ var SimulatorTransport = class {
2121
3427
  }
2122
3428
  };
2123
3429
  //#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 };
3430
+ export { B2Simulator, BUCKET_INFO_KEY_MAX_BYTES, BUCKET_INFO_KEY_MIN_BYTES, BUCKET_INFO_KEY_PATTERN, BUCKET_INFO_MAX_KEYS, BUCKET_INFO_RESERVED_PREFIX, BUCKET_INFO_VALUES_MAX_BYTES, BUCKET_INFO_VALUE_MAX, BUCKET_NAME_MAX, BUCKET_NAME_MIN, CORS_MAX_AGE_SECONDS_MAX, CORS_RULES_MAX_COUNT, CORS_RULE_MAX_BYTES, CORS_RULE_NAME_MAX_LENGTH, CORS_RULE_NAME_MIN_LENGTH, CORS_RULE_NAME_PATTERN, CORS_RULE_NAME_RESERVED_PREFIX, 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
3431
 
2126
3432
  //# sourceMappingURL=index.js.map