@backblaze-labs/b2-sdk 0.1.0 → 0.2.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 (701) hide show
  1. package/CHANGELOG.md +110 -5
  2. package/README.md +144 -33
  3. package/dist/_virtual/_b2-sdk-version-json.cjs +4 -0
  4. package/dist/_virtual/_b2-sdk-version-json.js +4 -0
  5. package/dist/auth/account-info.d.cts +32 -0
  6. package/dist/auth/account-info.d.cts.map +1 -1
  7. package/dist/auth/account-info.d.ts +32 -0
  8. package/dist/auth/account-info.d.ts.map +1 -1
  9. package/dist/auth/file.cjs +408 -207
  10. package/dist/auth/file.cjs.map +1 -1
  11. package/dist/auth/file.d.cts +77 -3
  12. package/dist/auth/file.d.cts.map +1 -1
  13. package/dist/auth/file.d.ts +77 -3
  14. package/dist/auth/file.d.ts.map +1 -1
  15. package/dist/auth/file.js +407 -207
  16. package/dist/auth/file.js.map +1 -1
  17. package/dist/auth/in-memory.cjs +212 -183
  18. package/dist/auth/in-memory.cjs.map +1 -1
  19. package/dist/auth/in-memory.d.cts +8 -0
  20. package/dist/auth/in-memory.d.cts.map +1 -1
  21. package/dist/auth/in-memory.d.ts +8 -0
  22. package/dist/auth/in-memory.d.ts.map +1 -1
  23. package/dist/auth/in-memory.js +211 -182
  24. package/dist/auth/in-memory.js.map +1 -1
  25. package/dist/auth/index.cjs +7 -9
  26. package/dist/auth/index.d.cts +3 -2
  27. package/dist/auth/index.d.cts.map +1 -1
  28. package/dist/auth/index.d.ts +3 -2
  29. package/dist/auth/index.d.ts.map +1 -1
  30. package/dist/auth/index.js +2 -8
  31. package/dist/auth/realms.cjs +80 -7
  32. package/dist/auth/realms.cjs.map +1 -1
  33. package/dist/auth/realms.d.cts +27 -4
  34. package/dist/auth/realms.d.cts.map +1 -1
  35. package/dist/auth/realms.d.ts +27 -4
  36. package/dist/auth/realms.d.ts.map +1 -1
  37. package/dist/auth/realms.js +80 -9
  38. package/dist/auth/realms.js.map +1 -1
  39. package/dist/auth/upload-url-pool.cjs +55 -51
  40. package/dist/auth/upload-url-pool.cjs.map +1 -1
  41. package/dist/auth/upload-url-pool.js +55 -51
  42. package/dist/auth/upload-url-pool.js.map +1 -1
  43. package/dist/bucket.cjs +785 -787
  44. package/dist/bucket.cjs.map +1 -1
  45. package/dist/bucket.d.cts +35 -47
  46. package/dist/bucket.d.cts.map +1 -1
  47. package/dist/bucket.d.ts +35 -47
  48. package/dist/bucket.d.ts.map +1 -1
  49. package/dist/bucket.js +780 -782
  50. package/dist/bucket.js.map +1 -1
  51. package/dist/client.cjs +246 -228
  52. package/dist/client.cjs.map +1 -1
  53. package/dist/client.d.cts +25 -17
  54. package/dist/client.d.cts.map +1 -1
  55. package/dist/client.d.ts +25 -17
  56. package/dist/client.d.ts.map +1 -1
  57. package/dist/client.js +243 -225
  58. package/dist/client.js.map +1 -1
  59. package/dist/copy/large.cjs +114 -71
  60. package/dist/copy/large.cjs.map +1 -1
  61. package/dist/copy/large.d.cts +6 -0
  62. package/dist/copy/large.d.cts.map +1 -1
  63. package/dist/copy/large.d.ts +6 -0
  64. package/dist/copy/large.d.ts.map +1 -1
  65. package/dist/copy/large.js +112 -69
  66. package/dist/copy/large.js.map +1 -1
  67. package/dist/download/checksum.cjs +89 -0
  68. package/dist/download/checksum.cjs.map +1 -0
  69. package/dist/download/checksum.d.cts +40 -0
  70. package/dist/download/checksum.d.cts.map +1 -0
  71. package/dist/download/checksum.d.ts +40 -0
  72. package/dist/download/checksum.d.ts.map +1 -0
  73. package/dist/download/checksum.js +87 -0
  74. package/dist/download/checksum.js.map +1 -0
  75. package/dist/download/parallel.cjs +214 -118
  76. package/dist/download/parallel.cjs.map +1 -1
  77. package/dist/download/parallel.d.cts +5 -1
  78. package/dist/download/parallel.d.cts.map +1 -1
  79. package/dist/download/parallel.d.ts +5 -1
  80. package/dist/download/parallel.d.ts.map +1 -1
  81. package/dist/download/parallel.js +212 -116
  82. package/dist/download/parallel.js.map +1 -1
  83. package/dist/download/single.cjs +188 -97
  84. package/dist/download/single.cjs.map +1 -1
  85. package/dist/download/single.d.cts +6 -1
  86. package/dist/download/single.d.cts.map +1 -1
  87. package/dist/download/single.d.ts +6 -1
  88. package/dist/download/single.d.ts.map +1 -1
  89. package/dist/download/single.js +186 -98
  90. package/dist/download/single.js.map +1 -1
  91. package/dist/errors/index.cjs +699 -243
  92. package/dist/errors/index.cjs.map +1 -1
  93. package/dist/errors/index.d.cts +285 -82
  94. package/dist/errors/index.d.cts.map +1 -1
  95. package/dist/errors/index.d.ts +285 -82
  96. package/dist/errors/index.d.ts.map +1 -1
  97. package/dist/errors/index.js +678 -261
  98. package/dist/errors/index.js.map +1 -1
  99. package/dist/http/retry.cjs +45 -28
  100. package/dist/http/retry.cjs.map +1 -1
  101. package/dist/http/retry.d.cts +15 -2
  102. package/dist/http/retry.d.cts.map +1 -1
  103. package/dist/http/retry.d.ts +15 -2
  104. package/dist/http/retry.d.ts.map +1 -1
  105. package/dist/http/retry.js +46 -31
  106. package/dist/http/retry.js.map +1 -1
  107. package/dist/http/transport.cjs +438 -140
  108. package/dist/http/transport.cjs.map +1 -1
  109. package/dist/http/transport.d.cts +21 -2
  110. package/dist/http/transport.d.cts.map +1 -1
  111. package/dist/http/transport.d.ts +21 -2
  112. package/dist/http/transport.d.ts.map +1 -1
  113. package/dist/http/transport.js +437 -140
  114. package/dist/http/transport.js.map +1 -1
  115. package/dist/http/url-guard.cjs +154 -89
  116. package/dist/http/url-guard.cjs.map +1 -1
  117. package/dist/http/url-guard.d.cts +25 -3
  118. package/dist/http/url-guard.d.cts.map +1 -1
  119. package/dist/http/url-guard.d.ts +25 -3
  120. package/dist/http/url-guard.d.ts.map +1 -1
  121. package/dist/http/url-guard.js +152 -90
  122. package/dist/http/url-guard.js.map +1 -1
  123. package/dist/http/user-agent.cjs +84 -44
  124. package/dist/http/user-agent.cjs.map +1 -1
  125. package/dist/http/user-agent.js +84 -46
  126. package/dist/http/user-agent.js.map +1 -1
  127. package/dist/index.cjs +101 -77
  128. package/dist/index.d.cts +27 -21
  129. package/dist/index.d.cts.map +1 -1
  130. package/dist/index.d.ts +27 -21
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +15 -71
  133. package/dist/internal/b2-naming.cjs +66 -0
  134. package/dist/internal/b2-naming.cjs.map +1 -0
  135. package/dist/internal/b2-naming.d.cts +43 -0
  136. package/dist/internal/b2-naming.d.cts.map +1 -0
  137. package/dist/internal/b2-naming.d.ts +43 -0
  138. package/dist/internal/b2-naming.d.ts.map +1 -0
  139. package/dist/internal/b2-naming.js +60 -0
  140. package/dist/internal/b2-naming.js.map +1 -0
  141. package/dist/internal/upload-retry-options.cjs +21 -0
  142. package/dist/internal/upload-retry-options.cjs.map +1 -0
  143. package/dist/internal/upload-retry-options.d.cts +12 -0
  144. package/dist/internal/upload-retry-options.d.cts.map +1 -0
  145. package/dist/internal/upload-retry-options.d.ts +12 -0
  146. package/dist/internal/upload-retry-options.d.ts.map +1 -0
  147. package/dist/internal/upload-retry-options.js +21 -0
  148. package/dist/internal/upload-retry-options.js.map +1 -0
  149. package/dist/internal/url-redaction.cjs +31 -0
  150. package/dist/internal/url-redaction.cjs.map +1 -0
  151. package/dist/internal/url-redaction.d.cts +17 -0
  152. package/dist/internal/url-redaction.d.cts.map +1 -0
  153. package/dist/internal/url-redaction.d.ts +17 -0
  154. package/dist/internal/url-redaction.d.ts.map +1 -0
  155. package/dist/internal/url-redaction.js +31 -0
  156. package/dist/internal/url-redaction.js.map +1 -0
  157. package/dist/notifications/index.cjs +4 -6
  158. package/dist/notifications/index.d.cts +1 -1
  159. package/dist/notifications/index.d.cts.map +1 -1
  160. package/dist/notifications/index.d.ts +1 -1
  161. package/dist/notifications/index.d.ts.map +1 -1
  162. package/dist/notifications/index.js +1 -6
  163. package/dist/notifications/webhook.cjs +195 -85
  164. package/dist/notifications/webhook.cjs.map +1 -1
  165. package/dist/notifications/webhook.d.cts.map +1 -1
  166. package/dist/notifications/webhook.d.ts.map +1 -1
  167. package/dist/notifications/webhook.js +195 -87
  168. package/dist/notifications/webhook.js.map +1 -1
  169. package/dist/object.cjs +253 -210
  170. package/dist/object.cjs.map +1 -1
  171. package/dist/object.d.cts +9 -55
  172. package/dist/object.d.cts.map +1 -1
  173. package/dist/object.d.ts +9 -55
  174. package/dist/object.d.ts.map +1 -1
  175. package/dist/object.js +249 -206
  176. package/dist/object.js.map +1 -1
  177. package/dist/raw/encoding.cjs +74 -35
  178. package/dist/raw/encoding.cjs.map +1 -1
  179. package/dist/raw/encoding.js +74 -38
  180. package/dist/raw/encoding.js.map +1 -1
  181. package/dist/raw/index.cjs +678 -587
  182. package/dist/raw/index.cjs.map +1 -1
  183. package/dist/raw/index.d.cts +121 -18
  184. package/dist/raw/index.d.cts.map +1 -1
  185. package/dist/raw/index.d.ts +121 -18
  186. package/dist/raw/index.d.ts.map +1 -1
  187. package/dist/raw/index.js +674 -589
  188. package/dist/raw/index.js.map +1 -1
  189. package/dist/s3/index.cjs +355 -18
  190. package/dist/s3/index.cjs.map +1 -1
  191. package/dist/s3/index.d.cts +233 -5
  192. package/dist/s3/index.d.cts.map +1 -1
  193. package/dist/s3/index.d.ts +233 -5
  194. package/dist/s3/index.d.ts.map +1 -1
  195. package/dist/s3/index.js +351 -22
  196. package/dist/s3/index.js.map +1 -1
  197. package/dist/s3/sigv4.cjs +149 -0
  198. package/dist/s3/sigv4.cjs.map +1 -0
  199. package/dist/s3/sigv4.d.cts +43 -0
  200. package/dist/s3/sigv4.d.cts.map +1 -0
  201. package/dist/s3/sigv4.d.ts +43 -0
  202. package/dist/s3/sigv4.d.ts.map +1 -0
  203. package/dist/s3/sigv4.js +148 -0
  204. package/dist/s3/sigv4.js.map +1 -0
  205. package/dist/s3/validation.cjs +69 -0
  206. package/dist/s3/validation.cjs.map +1 -0
  207. package/dist/s3/validation.d.cts +43 -0
  208. package/dist/s3/validation.d.cts.map +1 -0
  209. package/dist/s3/validation.d.ts +43 -0
  210. package/dist/s3/validation.d.ts.map +1 -0
  211. package/dist/s3/validation.js +66 -0
  212. package/dist/s3/validation.js.map +1 -0
  213. package/dist/simulator/capabilities.cjs +63 -48
  214. package/dist/simulator/capabilities.cjs.map +1 -1
  215. package/dist/simulator/capabilities.js +63 -49
  216. package/dist/simulator/capabilities.js.map +1 -1
  217. package/dist/simulator/index.cjs +2124 -1548
  218. package/dist/simulator/index.cjs.map +1 -1
  219. package/dist/simulator/index.d.cts +79 -11
  220. package/dist/simulator/index.d.cts.map +1 -1
  221. package/dist/simulator/index.d.ts +79 -11
  222. package/dist/simulator/index.d.ts.map +1 -1
  223. package/dist/simulator/index.js +2112 -1546
  224. package/dist/simulator/index.js.map +1 -1
  225. package/dist/simulator/validation.cjs +191 -153
  226. package/dist/simulator/validation.cjs.map +1 -1
  227. package/dist/simulator/validation.d.cts +3 -21
  228. package/dist/simulator/validation.d.cts.map +1 -1
  229. package/dist/simulator/validation.d.ts +3 -21
  230. package/dist/simulator/validation.d.ts.map +1 -1
  231. package/dist/simulator/validation.js +188 -162
  232. package/dist/simulator/validation.js.map +1 -1
  233. package/dist/streams/collect.cjs +74 -24
  234. package/dist/streams/collect.cjs.map +1 -1
  235. package/dist/streams/collect.d.cts +14 -1
  236. package/dist/streams/collect.d.cts.map +1 -1
  237. package/dist/streams/collect.d.ts +14 -1
  238. package/dist/streams/collect.d.ts.map +1 -1
  239. package/dist/streams/collect.js +74 -25
  240. package/dist/streams/collect.js.map +1 -1
  241. package/dist/streams/file-source.cjs +287 -0
  242. package/dist/streams/file-source.cjs.map +1 -0
  243. package/dist/streams/file-source.d.cts +96 -0
  244. package/dist/streams/file-source.d.cts.map +1 -0
  245. package/dist/streams/file-source.d.ts +96 -0
  246. package/dist/streams/file-source.d.ts.map +1 -0
  247. package/dist/streams/file-source.js +286 -0
  248. package/dist/streams/file-source.js.map +1 -0
  249. package/dist/streams/hash.cjs +207 -115
  250. package/dist/streams/hash.cjs.map +1 -1
  251. package/dist/streams/hash.d.cts +5 -4
  252. package/dist/streams/hash.d.cts.map +1 -1
  253. package/dist/streams/hash.d.ts +5 -4
  254. package/dist/streams/hash.d.ts.map +1 -1
  255. package/dist/streams/hash.js +208 -95
  256. package/dist/streams/hash.js.map +1 -1
  257. package/dist/streams/index.cjs +12 -12
  258. package/dist/streams/index.d.cts +3 -3
  259. package/dist/streams/index.d.cts.map +1 -1
  260. package/dist/streams/index.d.ts +3 -3
  261. package/dist/streams/index.d.ts.map +1 -1
  262. package/dist/streams/index.js +2 -10
  263. package/dist/streams/progress.cjs +60 -46
  264. package/dist/streams/progress.cjs.map +1 -1
  265. package/dist/streams/progress.js +60 -46
  266. package/dist/streams/progress.js.map +1 -1
  267. package/dist/streams/source.cjs +316 -146
  268. package/dist/streams/source.cjs.map +1 -1
  269. package/dist/streams/source.d.cts +56 -18
  270. package/dist/streams/source.d.cts.map +1 -1
  271. package/dist/streams/source.d.ts +56 -18
  272. package/dist/streams/source.d.ts.map +1 -1
  273. package/dist/streams/source.js +312 -149
  274. package/dist/streams/source.js.map +1 -1
  275. package/dist/sync/actions/index.cjs +236 -184
  276. package/dist/sync/actions/index.cjs.map +1 -1
  277. package/dist/sync/actions/index.d.cts +29 -21
  278. package/dist/sync/actions/index.d.cts.map +1 -1
  279. package/dist/sync/actions/index.d.ts +29 -21
  280. package/dist/sync/actions/index.d.ts.map +1 -1
  281. package/dist/sync/actions/index.js +236 -190
  282. package/dist/sync/actions/index.js.map +1 -1
  283. package/dist/sync/b2-sha1-reader.cjs +148 -0
  284. package/dist/sync/b2-sha1-reader.cjs.map +1 -0
  285. package/dist/sync/b2-sha1-reader.d.cts +58 -0
  286. package/dist/sync/b2-sha1-reader.d.cts.map +1 -0
  287. package/dist/sync/b2-sha1-reader.d.ts +58 -0
  288. package/dist/sync/b2-sha1-reader.d.ts.map +1 -0
  289. package/dist/sync/b2-sha1-reader.js +145 -0
  290. package/dist/sync/b2-sha1-reader.js.map +1 -0
  291. package/dist/sync/download-staging.cjs +265 -0
  292. package/dist/sync/download-staging.cjs.map +1 -0
  293. package/dist/sync/download-staging.d.cts +54 -0
  294. package/dist/sync/download-staging.d.cts.map +1 -0
  295. package/dist/sync/download-staging.d.ts +54 -0
  296. package/dist/sync/download-staging.d.ts.map +1 -0
  297. package/dist/sync/download-staging.js +259 -0
  298. package/dist/sync/download-staging.js.map +1 -0
  299. package/dist/sync/filesystem-errors.cjs +32 -0
  300. package/dist/sync/filesystem-errors.cjs.map +1 -0
  301. package/dist/sync/filesystem-errors.d.cts +8 -0
  302. package/dist/sync/filesystem-errors.d.cts.map +1 -0
  303. package/dist/sync/filesystem-errors.d.ts +8 -0
  304. package/dist/sync/filesystem-errors.d.ts.map +1 -0
  305. package/dist/sync/filesystem-errors.js +32 -0
  306. package/dist/sync/filesystem-errors.js.map +1 -0
  307. package/dist/sync/filters.cjs +217 -0
  308. package/dist/sync/filters.cjs.map +1 -0
  309. package/dist/sync/filters.d.cts +51 -0
  310. package/dist/sync/filters.d.cts.map +1 -0
  311. package/dist/sync/filters.d.ts +51 -0
  312. package/dist/sync/filters.d.ts.map +1 -0
  313. package/dist/sync/filters.js +213 -0
  314. package/dist/sync/filters.js.map +1 -0
  315. package/dist/sync/index.cjs +39 -22
  316. package/dist/sync/index.d.cts +14 -9
  317. package/dist/sync/index.d.cts.map +1 -1
  318. package/dist/sync/index.d.ts +14 -9
  319. package/dist/sync/index.d.ts.map +1 -1
  320. package/dist/sync/index.js +9 -20
  321. package/dist/sync/local-file-identity.cjs +61 -0
  322. package/dist/sync/local-file-identity.cjs.map +1 -0
  323. package/dist/sync/local-file-identity.d.cts +37 -0
  324. package/dist/sync/local-file-identity.d.cts.map +1 -0
  325. package/dist/sync/local-file-identity.d.ts +37 -0
  326. package/dist/sync/local-file-identity.d.ts.map +1 -0
  327. package/dist/sync/local-file-identity.js +60 -0
  328. package/dist/sync/local-file-identity.js.map +1 -0
  329. package/dist/sync/local-file-io.cjs +364 -0
  330. package/dist/sync/local-file-io.cjs.map +1 -0
  331. package/dist/sync/local-file-io.d.cts +75 -0
  332. package/dist/sync/local-file-io.d.cts.map +1 -0
  333. package/dist/sync/local-file-io.d.ts +75 -0
  334. package/dist/sync/local-file-io.d.ts.map +1 -0
  335. package/dist/sync/local-file-io.js +360 -0
  336. package/dist/sync/local-file-io.js.map +1 -0
  337. package/dist/sync/local-filesystem-root.cjs +27 -0
  338. package/dist/sync/local-filesystem-root.cjs.map +1 -0
  339. package/dist/sync/local-filesystem-root.d.cts +18 -0
  340. package/dist/sync/local-filesystem-root.d.cts.map +1 -0
  341. package/dist/sync/local-filesystem-root.d.ts +18 -0
  342. package/dist/sync/local-filesystem-root.d.ts.map +1 -0
  343. package/dist/sync/local-filesystem-root.js +26 -0
  344. package/dist/sync/local-filesystem-root.js.map +1 -0
  345. package/dist/sync/local-sha1.cjs +119 -0
  346. package/dist/sync/local-sha1.cjs.map +1 -0
  347. package/dist/sync/local-sha1.d.cts +39 -0
  348. package/dist/sync/local-sha1.d.cts.map +1 -0
  349. package/dist/sync/local-sha1.d.ts +39 -0
  350. package/dist/sync/local-sha1.d.ts.map +1 -0
  351. package/dist/sync/local-sha1.js +117 -0
  352. package/dist/sync/local-sha1.js.map +1 -0
  353. package/dist/sync/pairing.cjs +118 -29
  354. package/dist/sync/pairing.cjs.map +1 -1
  355. package/dist/sync/pairing.d.cts +7 -2
  356. package/dist/sync/pairing.d.cts.map +1 -1
  357. package/dist/sync/pairing.d.ts +7 -2
  358. package/dist/sync/pairing.d.ts.map +1 -1
  359. package/dist/sync/pairing.js +120 -31
  360. package/dist/sync/pairing.js.map +1 -1
  361. package/dist/sync/path-order.cjs +30 -0
  362. package/dist/sync/path-order.cjs.map +1 -0
  363. package/dist/sync/path-order.d.cts +28 -0
  364. package/dist/sync/path-order.d.cts.map +1 -0
  365. package/dist/sync/path-order.d.ts +28 -0
  366. package/dist/sync/path-order.d.ts.map +1 -0
  367. package/dist/sync/path-order.js +29 -0
  368. package/dist/sync/path-order.js.map +1 -0
  369. package/dist/sync/path-safety.cjs +111 -0
  370. package/dist/sync/path-safety.cjs.map +1 -0
  371. package/dist/sync/path-safety.d.cts +78 -0
  372. package/dist/sync/path-safety.d.cts.map +1 -0
  373. package/dist/sync/path-safety.d.ts +78 -0
  374. package/dist/sync/path-safety.d.ts.map +1 -0
  375. package/dist/sync/path-safety.js +105 -0
  376. package/dist/sync/path-safety.js.map +1 -0
  377. package/dist/sync/paths.d.cts +19 -0
  378. package/dist/sync/paths.d.cts.map +1 -0
  379. package/dist/sync/paths.d.ts +19 -0
  380. package/dist/sync/paths.d.ts.map +1 -0
  381. package/dist/sync/policies/compare.cjs +389 -11
  382. package/dist/sync/policies/compare.cjs.map +1 -1
  383. package/dist/sync/policies/compare.d.cts +100 -2
  384. package/dist/sync/policies/compare.d.cts.map +1 -1
  385. package/dist/sync/policies/compare.d.ts +100 -2
  386. package/dist/sync/policies/compare.d.ts.map +1 -1
  387. package/dist/sync/policies/compare.js +387 -13
  388. package/dist/sync/policies/compare.js.map +1 -1
  389. package/dist/sync/policies/index.cjs +76 -65
  390. package/dist/sync/policies/index.cjs.map +1 -1
  391. package/dist/sync/policies/index.d.cts +35 -6
  392. package/dist/sync/policies/index.d.cts.map +1 -1
  393. package/dist/sync/policies/index.d.ts +35 -6
  394. package/dist/sync/policies/index.d.ts.map +1 -1
  395. package/dist/sync/policies/index.js +76 -65
  396. package/dist/sync/policies/index.js.map +1 -1
  397. package/dist/sync/prefix.cjs +96 -0
  398. package/dist/sync/prefix.cjs.map +1 -0
  399. package/dist/sync/prefix.d.cts +55 -0
  400. package/dist/sync/prefix.d.cts.map +1 -0
  401. package/dist/sync/prefix.d.ts +55 -0
  402. package/dist/sync/prefix.d.ts.map +1 -0
  403. package/dist/sync/prefix.js +92 -0
  404. package/dist/sync/prefix.js.map +1 -0
  405. package/dist/sync/regexp-safety.cjs +192 -0
  406. package/dist/sync/regexp-safety.cjs.map +1 -0
  407. package/dist/sync/regexp-safety.d.cts +29 -0
  408. package/dist/sync/regexp-safety.d.cts.map +1 -0
  409. package/dist/sync/regexp-safety.d.ts +29 -0
  410. package/dist/sync/regexp-safety.d.ts.map +1 -0
  411. package/dist/sync/regexp-safety.js +190 -0
  412. package/dist/sync/regexp-safety.js.map +1 -0
  413. package/dist/sync/scan-events.cjs +33 -0
  414. package/dist/sync/scan-events.cjs.map +1 -0
  415. package/dist/sync/scan-events.d.cts +17 -0
  416. package/dist/sync/scan-events.d.cts.map +1 -0
  417. package/dist/sync/scan-events.d.ts +17 -0
  418. package/dist/sync/scan-events.d.ts.map +1 -0
  419. package/dist/sync/scan-events.js +32 -0
  420. package/dist/sync/scan-events.js.map +1 -0
  421. package/dist/sync/scan-limit.cjs +33 -0
  422. package/dist/sync/scan-limit.cjs.map +1 -0
  423. package/dist/sync/scan-limit.d.cts +21 -0
  424. package/dist/sync/scan-limit.d.cts.map +1 -0
  425. package/dist/sync/scan-limit.d.ts +21 -0
  426. package/dist/sync/scan-limit.d.ts.map +1 -0
  427. package/dist/sync/scan-limit.js +31 -0
  428. package/dist/sync/scan-limit.js.map +1 -0
  429. package/dist/sync/scanners/b2.cjs +239 -55
  430. package/dist/sync/scanners/b2.cjs.map +1 -1
  431. package/dist/sync/scanners/b2.d.cts +26 -7
  432. package/dist/sync/scanners/b2.d.cts.map +1 -1
  433. package/dist/sync/scanners/b2.d.ts +26 -7
  434. package/dist/sync/scanners/b2.d.ts.map +1 -1
  435. package/dist/sync/scanners/b2.js +239 -55
  436. package/dist/sync/scanners/b2.js.map +1 -1
  437. package/dist/sync/scanners/local.cjs +215 -56
  438. package/dist/sync/scanners/local.cjs.map +1 -1
  439. package/dist/sync/scanners/local.d.cts +22 -7
  440. package/dist/sync/scanners/local.d.cts.map +1 -1
  441. package/dist/sync/scanners/local.d.ts +22 -7
  442. package/dist/sync/scanners/local.d.ts.map +1 -1
  443. package/dist/sync/scanners/local.js +216 -57
  444. package/dist/sync/scanners/local.js.map +1 -1
  445. package/dist/sync/sha1-metadata.cjs +94 -0
  446. package/dist/sync/sha1-metadata.cjs.map +1 -0
  447. package/dist/sync/sha1-metadata.d.cts +82 -0
  448. package/dist/sync/sha1-metadata.d.cts.map +1 -0
  449. package/dist/sync/sha1-metadata.d.ts +82 -0
  450. package/dist/sync/sha1-metadata.d.ts.map +1 -0
  451. package/dist/sync/sha1-metadata.js +89 -0
  452. package/dist/sync/sha1-metadata.js.map +1 -0
  453. package/dist/sync/sha1-options.cjs +23 -0
  454. package/dist/sync/sha1-options.cjs.map +1 -0
  455. package/dist/sync/sha1-options.d.cts +14 -0
  456. package/dist/sync/sha1-options.d.cts.map +1 -0
  457. package/dist/sync/sha1-options.d.ts +14 -0
  458. package/dist/sync/sha1-options.d.ts.map +1 -0
  459. package/dist/sync/sha1-options.js +21 -0
  460. package/dist/sync/sha1-options.js.map +1 -0
  461. package/dist/sync/synchronizer.cjs +782 -197
  462. package/dist/sync/synchronizer.cjs.map +1 -1
  463. package/dist/sync/synchronizer.d.cts +47 -1
  464. package/dist/sync/synchronizer.d.cts.map +1 -1
  465. package/dist/sync/synchronizer.d.ts +47 -1
  466. package/dist/sync/synchronizer.d.ts.map +1 -1
  467. package/dist/sync/synchronizer.js +779 -173
  468. package/dist/sync/synchronizer.js.map +1 -1
  469. package/dist/sync/types.d.cts +208 -16
  470. package/dist/sync/types.d.cts.map +1 -1
  471. package/dist/sync/types.d.ts +208 -16
  472. package/dist/sync/types.d.ts.map +1 -1
  473. package/dist/types/auth.cjs +76 -56
  474. package/dist/types/auth.cjs.map +1 -1
  475. package/dist/types/auth.d.cts +67 -9
  476. package/dist/types/auth.d.cts.map +1 -1
  477. package/dist/types/auth.d.ts +67 -9
  478. package/dist/types/auth.d.ts.map +1 -1
  479. package/dist/types/auth.js +77 -57
  480. package/dist/types/auth.js.map +1 -1
  481. package/dist/types/bucket.cjs +74 -38
  482. package/dist/types/bucket.cjs.map +1 -1
  483. package/dist/types/bucket.js +75 -41
  484. package/dist/types/bucket.js.map +1 -1
  485. package/dist/types/encryption.cjs +364 -327
  486. package/dist/types/encryption.cjs.map +1 -1
  487. package/dist/types/encryption.d.cts +16 -0
  488. package/dist/types/encryption.d.cts.map +1 -1
  489. package/dist/types/encryption.d.ts +16 -0
  490. package/dist/types/encryption.d.ts.map +1 -1
  491. package/dist/types/encryption.js +364 -310
  492. package/dist/types/encryption.js.map +1 -1
  493. package/dist/types/errors.cjs +47 -0
  494. package/dist/types/errors.cjs.map +1 -0
  495. package/dist/types/errors.d.cts +13 -4
  496. package/dist/types/errors.d.cts.map +1 -1
  497. package/dist/types/errors.d.ts +13 -4
  498. package/dist/types/errors.d.ts.map +1 -1
  499. package/dist/types/errors.js +47 -0
  500. package/dist/types/errors.js.map +1 -0
  501. package/dist/types/file.cjs +36 -19
  502. package/dist/types/file.cjs.map +1 -1
  503. package/dist/types/file.d.cts +2 -2
  504. package/dist/types/file.d.cts.map +1 -1
  505. package/dist/types/file.d.ts +2 -2
  506. package/dist/types/file.d.ts.map +1 -1
  507. package/dist/types/file.js +37 -21
  508. package/dist/types/file.js.map +1 -1
  509. package/dist/types/ids.cjs +53 -9
  510. package/dist/types/ids.cjs.map +1 -1
  511. package/dist/types/ids.js +54 -15
  512. package/dist/types/ids.js.map +1 -1
  513. package/dist/types/index.d.cts +12 -11
  514. package/dist/types/index.d.cts.map +1 -1
  515. package/dist/types/index.d.ts +12 -11
  516. package/dist/types/index.d.ts.map +1 -1
  517. package/dist/types/key.d.cts +44 -8
  518. package/dist/types/key.d.cts.map +1 -1
  519. package/dist/types/key.d.ts +44 -8
  520. package/dist/types/key.d.ts.map +1 -1
  521. package/dist/types/lock.cjs +33 -13
  522. package/dist/types/lock.cjs.map +1 -1
  523. package/dist/types/lock.js +34 -15
  524. package/dist/types/lock.js.map +1 -1
  525. package/dist/types/notifications.cjs +44 -22
  526. package/dist/types/notifications.cjs.map +1 -1
  527. package/dist/types/notifications.js +45 -23
  528. package/dist/types/notifications.js.map +1 -1
  529. package/dist/types/upload.d.cts +39 -5
  530. package/dist/types/upload.d.cts.map +1 -1
  531. package/dist/types/upload.d.ts +39 -5
  532. package/dist/types/upload.d.ts.map +1 -1
  533. package/dist/upload/abort-scope.cjs +91 -0
  534. package/dist/upload/abort-scope.cjs.map +1 -0
  535. package/dist/upload/abort-scope.d.cts +48 -0
  536. package/dist/upload/abort-scope.d.cts.map +1 -0
  537. package/dist/upload/abort-scope.d.ts +48 -0
  538. package/dist/upload/abort-scope.d.ts.map +1 -0
  539. package/dist/upload/abort-scope.js +88 -0
  540. package/dist/upload/abort-scope.js.map +1 -0
  541. package/dist/upload/cancel.cjs +183 -8
  542. package/dist/upload/cancel.cjs.map +1 -1
  543. package/dist/upload/cancel.d.cts +110 -2
  544. package/dist/upload/cancel.d.cts.map +1 -1
  545. package/dist/upload/cancel.d.ts +110 -2
  546. package/dist/upload/cancel.d.ts.map +1 -1
  547. package/dist/upload/cancel.js +178 -9
  548. package/dist/upload/cancel.js.map +1 -1
  549. package/dist/upload/concurrency.cjs +79 -52
  550. package/dist/upload/concurrency.cjs.map +1 -1
  551. package/dist/upload/concurrency.js +79 -53
  552. package/dist/upload/concurrency.js.map +1 -1
  553. package/dist/upload/finish.cjs +59 -0
  554. package/dist/upload/finish.cjs.map +1 -0
  555. package/dist/upload/finish.d.cts +26 -0
  556. package/dist/upload/finish.d.cts.map +1 -0
  557. package/dist/upload/finish.d.ts +26 -0
  558. package/dist/upload/finish.d.ts.map +1 -0
  559. package/dist/upload/finish.js +59 -0
  560. package/dist/upload/finish.js.map +1 -0
  561. package/dist/upload/large.cjs +301 -218
  562. package/dist/upload/large.cjs.map +1 -1
  563. package/dist/upload/large.d.cts +76 -9
  564. package/dist/upload/large.d.cts.map +1 -1
  565. package/dist/upload/large.d.ts +76 -9
  566. package/dist/upload/large.d.ts.map +1 -1
  567. package/dist/upload/large.js +301 -218
  568. package/dist/upload/large.js.map +1 -1
  569. package/dist/upload/options.cjs +28 -0
  570. package/dist/upload/options.cjs.map +1 -0
  571. package/dist/upload/options.d.cts +104 -0
  572. package/dist/upload/options.d.cts.map +1 -0
  573. package/dist/upload/options.d.ts +104 -0
  574. package/dist/upload/options.d.ts.map +1 -0
  575. package/dist/upload/options.js +27 -0
  576. package/dist/upload/options.js.map +1 -0
  577. package/dist/upload/resume.cjs +333 -33
  578. package/dist/upload/resume.cjs.map +1 -1
  579. package/dist/upload/resume.d.cts +82 -12
  580. package/dist/upload/resume.d.cts.map +1 -1
  581. package/dist/upload/resume.d.ts +82 -12
  582. package/dist/upload/resume.d.ts.map +1 -1
  583. package/dist/upload/resume.js +330 -33
  584. package/dist/upload/resume.js.map +1 -1
  585. package/dist/upload/retry.cjs +186 -0
  586. package/dist/upload/retry.cjs.map +1 -0
  587. package/dist/upload/retry.d.cts +127 -0
  588. package/dist/upload/retry.d.cts.map +1 -0
  589. package/dist/upload/retry.d.ts +127 -0
  590. package/dist/upload/retry.d.ts.map +1 -0
  591. package/dist/upload/retry.js +182 -0
  592. package/dist/upload/retry.js.map +1 -0
  593. package/dist/upload/single.cjs +68 -44
  594. package/dist/upload/single.cjs.map +1 -1
  595. package/dist/upload/single.d.cts +2 -1
  596. package/dist/upload/single.d.cts.map +1 -1
  597. package/dist/upload/single.d.ts +2 -1
  598. package/dist/upload/single.d.ts.map +1 -1
  599. package/dist/upload/single.js +68 -44
  600. package/dist/upload/single.js.map +1 -1
  601. package/dist/upload/stream.cjs +317 -193
  602. package/dist/upload/stream.cjs.map +1 -1
  603. package/dist/upload/stream.d.cts +3 -1
  604. package/dist/upload/stream.d.cts.map +1 -1
  605. package/dist/upload/stream.d.ts +3 -1
  606. package/dist/upload/stream.d.ts.map +1 -1
  607. package/dist/upload/stream.js +316 -192
  608. package/dist/upload/stream.js.map +1 -1
  609. package/dist/util/best-effort.cjs +42 -8
  610. package/dist/util/best-effort.cjs.map +1 -1
  611. package/dist/util/best-effort.d.cts +2 -1
  612. package/dist/util/best-effort.d.cts.map +1 -1
  613. package/dist/util/best-effort.d.ts +2 -1
  614. package/dist/util/best-effort.d.ts.map +1 -1
  615. package/dist/util/best-effort.js +43 -9
  616. package/dist/util/best-effort.js.map +1 -1
  617. package/dist/util/bytes.cjs +23 -0
  618. package/dist/util/bytes.cjs.map +1 -0
  619. package/dist/util/bytes.d.cts +15 -0
  620. package/dist/util/bytes.d.cts.map +1 -0
  621. package/dist/util/bytes.d.ts +15 -0
  622. package/dist/util/bytes.d.ts.map +1 -0
  623. package/dist/util/bytes.js +23 -0
  624. package/dist/util/bytes.js.map +1 -0
  625. package/dist/util/crypto.cjs +68 -0
  626. package/dist/util/crypto.cjs.map +1 -0
  627. package/dist/util/crypto.d.cts +26 -0
  628. package/dist/util/crypto.d.cts.map +1 -0
  629. package/dist/util/crypto.d.ts +26 -0
  630. package/dist/util/crypto.d.ts.map +1 -0
  631. package/dist/util/crypto.js +66 -0
  632. package/dist/util/crypto.js.map +1 -0
  633. package/dist/util/defaults.cjs +25 -9
  634. package/dist/util/defaults.cjs.map +1 -1
  635. package/dist/util/defaults.js +26 -11
  636. package/dist/util/defaults.js.map +1 -1
  637. package/dist/util/error-reason.cjs +41 -0
  638. package/dist/util/error-reason.cjs.map +1 -0
  639. package/dist/util/error-reason.d.cts +9 -0
  640. package/dist/util/error-reason.d.cts.map +1 -0
  641. package/dist/util/error-reason.d.ts +9 -0
  642. package/dist/util/error-reason.d.ts.map +1 -0
  643. package/dist/util/error-reason.js +41 -0
  644. package/dist/util/error-reason.js.map +1 -0
  645. package/dist/util/http.cjs +23 -0
  646. package/dist/util/http.cjs.map +1 -0
  647. package/dist/util/http.d.cts +13 -0
  648. package/dist/util/http.d.cts.map +1 -0
  649. package/dist/util/http.d.ts +13 -0
  650. package/dist/util/http.d.ts.map +1 -0
  651. package/dist/util/http.js +23 -0
  652. package/dist/util/http.js.map +1 -0
  653. package/dist/util/normalize.cjs +66 -7
  654. package/dist/util/normalize.cjs.map +1 -1
  655. package/dist/util/normalize.js +67 -10
  656. package/dist/util/normalize.js.map +1 -1
  657. package/dist/util/paginator.cjs +63 -14
  658. package/dist/util/paginator.cjs.map +1 -1
  659. package/dist/util/paginator.js +64 -16
  660. package/dist/util/paginator.js.map +1 -1
  661. package/dist/util/plan-ranges.cjs +47 -22
  662. package/dist/util/plan-ranges.cjs.map +1 -1
  663. package/dist/util/plan-ranges.js +48 -24
  664. package/dist/util/plan-ranges.js.map +1 -1
  665. package/dist/util/sha1.cjs +27 -0
  666. package/dist/util/sha1.cjs.map +1 -0
  667. package/dist/util/sha1.d.cts +17 -0
  668. package/dist/util/sha1.d.cts.map +1 -0
  669. package/dist/util/sha1.d.ts +17 -0
  670. package/dist/util/sha1.d.ts.map +1 -0
  671. package/dist/util/sha1.js +26 -0
  672. package/dist/util/sha1.js.map +1 -0
  673. package/dist/util/text-codec.cjs +29 -5
  674. package/dist/util/text-codec.cjs.map +1 -1
  675. package/dist/util/text-codec.js +30 -7
  676. package/dist/util/text-codec.js.map +1 -1
  677. package/dist/util/to-error.cjs +18 -4
  678. package/dist/util/to-error.cjs.map +1 -1
  679. package/dist/util/to-error.js +19 -5
  680. package/dist/util/to-error.js.map +1 -1
  681. package/dist/version.cjs +17 -5
  682. package/dist/version.cjs.map +1 -1
  683. package/dist/version.d.cts +3 -3
  684. package/dist/version.d.ts +3 -3
  685. package/dist/version.js +19 -6
  686. package/dist/version.js.map +1 -1
  687. package/package.json +19 -16
  688. package/dist/auth/index.cjs.map +0 -1
  689. package/dist/auth/index.js.map +0 -1
  690. package/dist/index.cjs.map +0 -1
  691. package/dist/index.js.map +0 -1
  692. package/dist/notifications/index.cjs.map +0 -1
  693. package/dist/notifications/index.js.map +0 -1
  694. package/dist/package.json.cjs +0 -9
  695. package/dist/package.json.cjs.map +0 -1
  696. package/dist/package.json.js +0 -9
  697. package/dist/package.json.js.map +0 -1
  698. package/dist/streams/index.cjs.map +0 -1
  699. package/dist/streams/index.js.map +0 -1
  700. package/dist/sync/index.cjs.map +0 -1
  701. package/dist/sync/index.js.map +0 -1
@@ -1,1559 +1,2135 @@
1
- "use strict";
2
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const hash = require("../streams/hash.cjs");
4
- const auth = require("../types/auth.cjs");
5
- const bucket = require("../types/bucket.cjs");
6
- const encryption = require("../types/encryption.cjs");
7
- const file = require("../types/file.cjs");
8
- const ids = require("../types/ids.cjs");
9
- const textCodec = require("../util/text-codec.cjs");
10
- const toError = require("../util/to-error.cjs");
11
- const capabilities = require("./capabilities.cjs");
12
- const validation = require("./validation.cjs");
2
+ const require_file = require("../types/file.cjs");
3
+ const require_ids = require("../types/ids.cjs");
4
+ const require_text_codec = require("../util/text-codec.cjs");
5
+ const require_encoding = require("../raw/encoding.cjs");
6
+ const require_crypto = require("../util/crypto.cjs");
7
+ const require_hash = require("../streams/hash.cjs");
8
+ const require_bucket = require("../types/bucket.cjs");
9
+ const require_encryption = require("../types/encryption.cjs");
10
+ const require_to_error = require("../util/to-error.cjs");
11
+ const require_auth = require("../types/auth.cjs");
12
+ const require_capabilities = require("./capabilities.cjs");
13
+ const require_b2_naming = require("../internal/b2-naming.cjs");
14
+ const require_validation = require("./validation.cjs");
15
+ //#region src/simulator/index.ts
16
+ var UPLOAD_TOKEN_SIGNING_KEY = "b2-sdk-typescript-simulator-upload-token-v1";
17
+ function base64UrlEncode(bytes) {
18
+ let binary = "";
19
+ for (const byte of bytes) binary += String.fromCharCode(byte);
20
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
21
+ }
22
+ function base64UrlDecode(encoded) {
23
+ const base64 = encoded.replaceAll("-", "+").replaceAll("_", "/");
24
+ const padding = (4 - base64.length % 4) % 4;
25
+ try {
26
+ const binary = atob(base64.padEnd(base64.length + padding, "="));
27
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+ function timingSafeStringEqual(left, right) {
33
+ const max = Math.max(left.length, right.length);
34
+ let mismatch = left.length === right.length ? 0 : 1;
35
+ for (let i = 0; i < max; i++) mismatch |= (left.charCodeAt(i) || 0) ^ (right.charCodeAt(i) || 0);
36
+ return mismatch === 0;
37
+ }
38
+ /**
39
+ * Parse an RFC 7233 `Range` header value into inclusive start/end byte
40
+ * offsets clamped to the file size. Supports closed (`bytes=0-999`),
41
+ * open-ended (`bytes=1000-`), and suffix (`bytes=-500`) forms.
42
+ *
43
+ * @param header - The raw header value (e.g. `'bytes=0-999'`).
44
+ * @param totalSize - The full file size in bytes (for clamping + suffix).
45
+ *
46
+ * @returns A {@link RangeParseResult} tagged with how the simulator
47
+ * should respond: serve the range, send 416, or ignore the header.
48
+ *
49
+ * @see https://www.rfc-editor.org/rfc/rfc7233#section-2.1
50
+ */
13
51
  function parseRangeHeader(header, totalSize) {
14
- const m = header.match(/^bytes=(\d*)-(\d*)$/);
15
- if (!m) return { kind: "malformed" };
16
- const [, startStr, endStr] = m;
17
- const hasStart = startStr !== "";
18
- const hasEnd = endStr !== "";
19
- if (!hasStart && !hasEnd) return { kind: "malformed" };
20
- if (totalSize === 0) return { kind: "unsatisfiable" };
21
- let start;
22
- let end;
23
- if (!hasStart) {
24
- const suffixLen = Number.parseInt(endStr ?? "0", 10);
25
- if (!Number.isFinite(suffixLen) || suffixLen <= 0) return { kind: "malformed" };
26
- start = Math.max(0, totalSize - suffixLen);
27
- end = totalSize - 1;
28
- } else if (!hasEnd) {
29
- start = Number.parseInt(startStr ?? "0", 10);
30
- end = totalSize - 1;
31
- } else {
32
- start = Number.parseInt(startStr ?? "0", 10);
33
- end = Number.parseInt(endStr ?? "0", 10);
34
- }
35
- if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || start > end) {
36
- return { kind: "malformed" };
37
- }
38
- if (start >= totalSize) return { kind: "unsatisfiable" };
39
- end = Math.min(end, totalSize - 1);
40
- return { kind: "ok", start, end };
52
+ const m = header.match(/^bytes=(\d*)-(\d*)$/);
53
+ if (!m) return { kind: "malformed" };
54
+ const [, startStr, endStr] = m;
55
+ const hasStart = startStr !== "";
56
+ const hasEnd = endStr !== "";
57
+ if (!hasStart && !hasEnd) return { kind: "malformed" };
58
+ if (totalSize === 0) return { kind: "unsatisfiable" };
59
+ let start;
60
+ let end;
61
+ if (!hasStart) {
62
+ const suffixLen = Number.parseInt(endStr ?? "0", 10);
63
+ if (!Number.isFinite(suffixLen) || suffixLen <= 0) return { kind: "malformed" };
64
+ start = Math.max(0, totalSize - suffixLen);
65
+ end = totalSize - 1;
66
+ } else if (!hasEnd) {
67
+ start = Number.parseInt(startStr ?? "0", 10);
68
+ end = totalSize - 1;
69
+ } else {
70
+ start = Number.parseInt(startStr ?? "0", 10);
71
+ end = Number.parseInt(endStr ?? "0", 10);
72
+ }
73
+ if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || start > end) return { kind: "malformed" };
74
+ if (start >= totalSize) return { kind: "unsatisfiable" };
75
+ end = Math.min(end, totalSize - 1);
76
+ return {
77
+ kind: "ok",
78
+ start,
79
+ end
80
+ };
41
81
  }
82
+ /**
83
+ * Parse `X-Bz-Info-*` headers (lowercased Map keys) into a plain
84
+ * fileInfo record. Mirrors the SDK's `parseFileInfoHeaders` in
85
+ * `raw/encoding.ts` but operates on a `Record<string, string>` rather
86
+ * than a `Headers` object so the simulator can reuse the same
87
+ * extraction logic without converting back to `Headers`.
88
+ *
89
+ * @param headers - Lowercased header map.
90
+ *
91
+ * @returns Plain `Record<string, string>` of `fileInfo` keys/values.
92
+ */
42
93
  function parseFileInfoHeaders(headers) {
43
- const info = {};
44
- const prefix = "x-bz-info-";
45
- for (const [key, value] of Object.entries(headers)) {
46
- if (!key.startsWith(prefix)) continue;
47
- const fileInfoKey = decodeURIComponent(key.slice(prefix.length));
48
- info[fileInfoKey] = decodeURIComponent(value);
49
- }
50
- return info;
94
+ const info = {};
95
+ const prefix = "x-bz-info-";
96
+ for (const [key, value] of Object.entries(headers)) {
97
+ if (!key.startsWith(prefix)) continue;
98
+ const fileInfoKey = decodeURIComponent(key.slice(10));
99
+ info[fileInfoKey] = decodeURIComponent(value);
100
+ }
101
+ return info;
102
+ }
103
+ function decodeHeaderValue(value) {
104
+ try {
105
+ return decodeURIComponent(value);
106
+ } catch {
107
+ return value;
108
+ }
109
+ }
110
+ /**
111
+ * Compares B2 file names with deterministic JS string order, not locale collation.
112
+ *
113
+ * @param a - First file name.
114
+ * @param b - Second file name.
115
+ *
116
+ * @returns `-1` when `a` sorts first, `1` when `b` sorts first, otherwise `0`.
117
+ */
118
+ function compareB2FileNames(a, b) {
119
+ if (a < b) return -1;
120
+ if (a > b) return 1;
121
+ return 0;
122
+ }
123
+ function normalizeKeyBucketIds(req) {
124
+ return req.bucketIds === void 0 || req.bucketIds === null ? null : Object.freeze([...req.bucketIds]);
125
+ }
126
+ function singleBucketId(bucketIds) {
127
+ return bucketIds?.length === 1 ? bucketIds[0] ?? null : null;
128
+ }
129
+ function cloneBucketIds(bucketIds) {
130
+ return bucketIds === null ? null : [...bucketIds];
131
+ }
132
+ function hasOwnField(body, field) {
133
+ return typeof body === "object" && body !== null && Object.hasOwn(body, field);
51
134
  }
52
- class B2Simulator {
53
- buckets = /* @__PURE__ */ new Map();
54
- accountId = "sim_account_0001";
55
- largeFiles = /* @__PURE__ */ new Map();
56
- keys = /* @__PURE__ */ new Map();
57
- notificationRules = /* @__PURE__ */ new Map();
58
- minimumPartSize;
59
- recommendedPartSize;
60
- faults = [];
61
- nextFaultId = 1;
62
- onWebhookDeliver;
63
- onReplicate;
64
- onHookError;
65
- strictAuth;
66
- authTokenTtlMs;
67
- /**
68
- * Issued auth tokens with their associated grant scope + expiry. In
69
- * permissive mode (`strictAuth: false`) this is still populated by
70
- * `authorize` but never consulted on subsequent requests. In strict
71
- * mode each request looks up its `Authorization` header here.
72
- */
73
- issuedTokens = /* @__PURE__ */ new Map();
74
- /**
75
- * Virtual-clock offset applied to `Date.now()` for expiry checks.
76
- * Defaults to 0. Tests advance via {@link advanceTime} to fast-forward
77
- * past auth-token expiry without sleeping.
78
- */
79
- clockOffsetMs = 0;
80
- /**
81
- * Per-instance monotonic counter used to mint realistic-looking
82
- * IDs (`b2_bucket_<24-hex>`, `4_z<24-hex>`, etc.) and auth tokens.
83
- * Module-global previously, which leaked state across `B2Simulator`
84
- * instances in the same Vitest worker — concurrent tests could see
85
- * colliding tokens. Per-instance ensures determinism within a single
86
- * simulator's lifetime.
87
- */
88
- nextId = 1;
89
- /**
90
- * Last-issued upload timestamp. The simulator enforces strict
91
- * monotonicity so version ordering is deterministic for tests; if
92
- * two writes hit the same `Date.now()` millisecond, the second
93
- * receives `lastTimestamp + 1`.
94
- */
95
- lastTimestamp = 0;
96
- /**
97
- * Outstanding fire-and-forget hook invocations. Tracked so tests
98
- * (and future {@link flushHooks} callers) can wait for every
99
- * dispatched `onWebhookDeliver` / `onReplicate` callback to settle
100
- * before asserting against observable side effects. The microtask
101
- * dance in fidelity tests was previously brittle: positive cases
102
- * flushed twice, negative cases flushed once, and any addition to
103
- * the hook dispatch chain quietly broke the negative path.
104
- */
105
- pendingHooks = /* @__PURE__ */ new Set();
106
- /**
107
- * Constructs a new in-memory B2 simulator.
108
- * @param options - Optional simulator overrides. See {@link B2SimulatorOptions}.
109
- */
110
- constructor(options = {}) {
111
- this.minimumPartSize = options.minimumPartSize ?? 5e6;
112
- this.recommendedPartSize = options.recommendedPartSize ?? 1e8;
113
- if (options.onWebhookDeliver !== void 0) this.onWebhookDeliver = options.onWebhookDeliver;
114
- if (options.onReplicate !== void 0) this.onReplicate = options.onReplicate;
115
- if (options.onHookError !== void 0) this.onHookError = options.onHookError;
116
- this.strictAuth = options.strictAuth ?? false;
117
- this.authTokenTtlMs = options.authTokenTtlMs ?? 24 * 60 * 60 * 1e3;
118
- }
119
- /**
120
- * Advance the simulator's virtual clock by `ms` milliseconds. Used in
121
- * conjunction with `strictAuth: true` + a finite `authTokenTtlMs` to
122
- * force token expiry without `setTimeout`-based delays.
123
- *
124
- * @param ms - Milliseconds to advance. Negative values rewind (rarely useful).
125
- */
126
- advanceTime(ms) {
127
- this.clockOffsetMs += ms;
128
- }
129
- /**
130
- * Current simulator time. Equal to `Date.now() + clockOffsetMs`.
131
- *
132
- * @returns Unix milliseconds.
133
- */
134
- now() {
135
- return Date.now() + this.clockOffsetMs;
136
- }
137
- /**
138
- * Generate a deterministic-but-realistic-looking B2 identifier of the
139
- * shape `<prefix>_<24-hex-counter>`, which approximates the visual
140
- * width of real B2 wire IDs. The counter is monotonic per instance
141
- * so test fixtures are deterministic across runs of the same
142
- * simulator.
143
- *
144
- * @param prefix - B2-style prefix (`'b2_bucket'`, `'4_z'`, etc.).
145
- *
146
- * @returns A simulator-issued identifier that looks like a B2 wire ID.
147
- */
148
- genId(prefix) {
149
- const n = this.nextId++;
150
- return `${prefix}_${n.toString(16).padStart(24, "0")}`;
151
- }
152
- /**
153
- * Return a strictly-increasing upload timestamp, even when multiple
154
- * writes land in the same `Date.now()` millisecond. Tests rely on
155
- * `uploadTimestamp` ordering to discriminate file versions; ties from
156
- * coarse system-clock resolution would otherwise make version
157
- * selection nondeterministic.
158
- *
159
- * @returns Unix milliseconds.
160
- */
161
- monotonicTimestamp() {
162
- const now = Date.now();
163
- if (now <= this.lastTimestamp) {
164
- this.lastTimestamp += 1;
165
- } else {
166
- this.lastTimestamp = now;
167
- }
168
- return this.lastTimestamp;
169
- }
170
- /**
171
- * Wait for every fire-and-forget hook (`onWebhookDeliver`,
172
- * `onReplicate`) currently in flight to settle. Use in tests after
173
- * an upload/copy/finish-large-file call to deterministically observe
174
- * hook side effects without microtask-flush guesswork.
175
- *
176
- * @returns A promise that resolves once every pending hook callback
177
- * has either resolved or rejected.
178
- */
179
- async flushHooks() {
180
- while (this.pendingHooks.size > 0) {
181
- const snapshot = [...this.pendingHooks];
182
- await Promise.allSettled(snapshot);
183
- }
184
- }
185
- /**
186
- * Authorize a request against the strict-auth bookkeeping. Returns
187
- * a `SimulatorJsonResponse` error on failure or `null` on success.
188
- * Only consulted when `strictAuth: true`.
189
- *
190
- * @param authToken - The `Authorization` header value from the request.
191
- * @param endpoint - B2 endpoint name being invoked.
192
- * @param bucketId - Optional bucket ID the request operates on, for scope check.
193
- * @param fileName - Optional file name the request operates on, for prefix check.
194
- *
195
- * @returns `null` when the request is authorized, otherwise a 401/403 response.
196
- */
197
- authorizeRequest(authToken, endpoint, bucketId, fileName) {
198
- const required = capabilities.missingCapabilitiesFor(endpoint, []);
199
- const noCheckNeeded = required.length === 0 && endpoint === "b2_authorize_account";
200
- if (noCheckNeeded) return null;
201
- if (authToken === void 0 || authToken === "") {
202
- return this.error(401, "bad_auth_token", "missing Authorization header");
203
- }
204
- const token = this.issuedTokens.get(authToken);
205
- if (!token) {
206
- return this.error(401, "bad_auth_token", "unknown auth token");
207
- }
208
- if (this.now() > token.expiresAt) {
209
- return this.error(401, "expired_auth_token", "auth token has expired; reauthorize");
210
- }
211
- const missing = capabilities.missingCapabilitiesFor(endpoint, token.capabilities);
212
- if (missing.length > 0) {
213
- return this.error(
214
- 403,
215
- "unauthorized",
216
- `application key lacks required capabilities: ${missing.join(", ")}`
217
- );
218
- }
219
- if (token.bucketId !== null && bucketId !== void 0 && bucketId !== token.bucketId) {
220
- return this.error(
221
- 403,
222
- "unauthorized",
223
- `application key is scoped to bucket ${token.bucketId}; cannot access ${bucketId}`
224
- );
225
- }
226
- if (token.namePrefix !== null && fileName !== void 0 && !fileName.startsWith(token.namePrefix)) {
227
- return this.error(
228
- 403,
229
- "unauthorized",
230
- `application key is scoped to prefix "${token.namePrefix}"; "${fileName}" is outside scope`
231
- );
232
- }
233
- return null;
234
- }
235
- /**
236
- * Look up the application key matching the `Authorization` header on
237
- * an `authorize_account` request. The header is in the form
238
- * `Basic base64(applicationKeyId:applicationKey)`.
239
- *
240
- * Returns `null` for the implicit master credential (anything that
241
- * does not match a key minted via `b2_create_key`); in that case
242
- * `authorize` grants the full master capability set.
243
- *
244
- * @param authzHeader - Raw HTTP `Authorization` header value.
245
- *
246
- * @returns The matching key's grant scope, or `null` for the master.
247
- */
248
- findKeyForAuthHeader(authzHeader) {
249
- if (!authzHeader || !authzHeader.startsWith("Basic ")) return null;
250
- const decoded = (() => {
251
- try {
252
- return atob(authzHeader.slice(6));
253
- } catch {
254
- return null;
255
- }
256
- })();
257
- if (decoded === null) return null;
258
- const idx = decoded.indexOf(":");
259
- if (idx === -1) return null;
260
- const applicationKeyId = decoded.slice(0, idx);
261
- const applicationKey = decoded.slice(idx + 1);
262
- const stored = this.keys.get(applicationKeyId);
263
- if (!stored || stored.applicationKey !== applicationKey) return null;
264
- return {
265
- capabilities: stored.capabilities,
266
- bucketId: stored.bucketId,
267
- namePrefix: stored.namePrefix,
268
- applicationKeyId
269
- };
270
- }
271
- /**
272
- * Creates an {@link HttpTransport} that routes requests to this simulator.
273
- * @returns A transport instance backed by this in-memory simulator.
274
- */
275
- transport() {
276
- return new SimulatorTransport(this);
277
- }
278
- /**
279
- * Register a synthetic failure to inject on requests whose URL contains
280
- * `spec.on`. Use this to exercise retry / backoff / error-handling
281
- * paths in tests without hand-rolling a wrapping `HttpTransport`. The
282
- * fault is consumed in registration order on each matched request;
283
- * once its `count` budget is exhausted it auto-retires.
284
- *
285
- * Faults are checked BEFORE the simulator's real handlers run, so a
286
- * matched request never touches in-memory state — failed uploads
287
- * don't create partial parts, failed deletes don't remove anything.
288
- *
289
- * @param spec - The failure to inject. See {@link FaultSpec}.
290
- *
291
- * @returns A handle whose `clear()` method removes this specific
292
- * fault registration (other faults remain in effect).
293
- *
294
- * @example
295
- * ```ts
296
- * // Fail the next 2 b2_upload_part calls with 503, then succeed.
297
- * sim.injectFailure({ on: 'b2_upload_part', status: 503, count: 2 })
298
- *
299
- * // Fail every b2_authorize_account with 401 + Retry-After: 5.
300
- * const handle = sim.injectFailure({
301
- * on: 'b2_authorize_account',
302
- * status: 401,
303
- * code: 'expired_auth_token',
304
- * retryAfter: 5,
305
- * })
306
- * // ... later
307
- * handle.clear()
308
- * ```
309
- */
310
- injectFailure(spec) {
311
- const id = this.nextFaultId++;
312
- const fault = {
313
- id,
314
- spec,
315
- remainingSkip: spec.skip ?? 0,
316
- remainingCount: spec.count ?? Number.POSITIVE_INFINITY
317
- };
318
- this.faults.push(fault);
319
- return {
320
- clear: () => {
321
- const idx = this.faults.findIndex((f) => f.id === id);
322
- if (idx !== -1) this.faults.splice(idx, 1);
323
- }
324
- };
325
- }
326
- /**
327
- * Remove every registered fault. Equivalent to calling `.clear()` on
328
- * every handle returned by {@link injectFailure}, plus a defensive
329
- * reset for tests that re-use a simulator across cases.
330
- */
331
- clearFaults() {
332
- this.faults.length = 0;
333
- }
334
- /**
335
- * Internal: checks the registered faults for a match on the given URL
336
- * and consumes one if it should fire. Called from
337
- * {@link SimulatorTransport.send} before any real handler runs.
338
- *
339
- * @param url - The request URL to match against each fault's `on`
340
- * substring.
341
- *
342
- * @returns The fault to apply, or `null` if no fault should fire.
343
- *
344
- * @internal
345
- */
346
- consumeMatchingFault(url) {
347
- for (let i = 0; i < this.faults.length; i++) {
348
- const fault = this.faults[i];
349
- if (!url.includes(fault.spec.on)) continue;
350
- if (fault.remainingSkip > 0) {
351
- fault.remainingSkip -= 1;
352
- continue;
353
- }
354
- if (fault.remainingCount <= 0) continue;
355
- fault.remainingCount -= 1;
356
- if (fault.remainingCount <= 0) {
357
- this.faults.splice(i, 1);
358
- }
359
- return fault.spec;
360
- }
361
- return null;
362
- }
363
- /**
364
- * Dispatches a JSON API request to the appropriate handler.
365
- * @param _method - The HTTP method (unused).
366
- * @param path - The request URL path containing the B2 endpoint name.
367
- * @param headers - The HTTP request headers; consulted by the
368
- * strict-auth gate to look up the issued auth token.
369
- * @param body - The parsed JSON request body.
370
- *
371
- * @returns An object with HTTP status and JSON response body.
372
- */
373
- async handleRequest(_method, path, headers, body) {
374
- const endpoint = path.split("/").pop() ?? "";
375
- if (this.strictAuth) {
376
- const authError = this.authorizeRequest(headers["authorization"], endpoint);
377
- if (authError !== null) return authError;
378
- }
379
- switch (endpoint) {
380
- case "b2_authorize_account":
381
- return this.authorize(headers["authorization"]);
382
- case "b2_create_bucket":
383
- return this.createBucket(
384
- body
385
- );
386
- case "b2_list_buckets":
387
- return this.listBuckets();
388
- case "b2_delete_bucket":
389
- return this.deleteBucket(body);
390
- case "b2_update_bucket":
391
- return this.updateBucket(body);
392
- case "b2_get_upload_url":
393
- return this.getUploadUrl(body);
394
- case "b2_list_file_names":
395
- return this.listFileNames(
396
- body
397
- );
398
- case "b2_list_file_versions":
399
- return this.listFileVersions(
400
- body
401
- );
402
- case "b2_get_file_info":
403
- return this.getFileInfo(body);
404
- case "b2_hide_file":
405
- return this.hideFile(body);
406
- case "b2_delete_file_version":
407
- return this.deleteFileVersion(
408
- body
409
- );
410
- case "b2_copy_file":
411
- return this.copyFile(
412
- body
413
- );
414
- case "b2_start_large_file":
415
- return this.startLargeFile(
416
- body
417
- );
418
- case "b2_get_upload_part_url":
419
- return this.getUploadPartUrl(body);
420
- case "b2_finish_large_file":
421
- return this.finishLargeFile(body);
422
- case "b2_cancel_large_file":
423
- return this.cancelLargeFile(body);
424
- case "b2_list_unfinished_large_files":
425
- return this.listUnfinishedLargeFiles(
426
- body
427
- );
428
- case "b2_list_parts":
429
- return this.listParts(
430
- body
431
- );
432
- case "b2_copy_part":
433
- return await this.copyPart(
434
- body
435
- );
436
- case "b2_get_download_authorization":
437
- return this.getDownloadAuthorization(
438
- body
439
- );
440
- case "b2_create_key":
441
- return this.createKey(
442
- body
443
- );
444
- case "b2_list_keys":
445
- return this.listKeys(
446
- body
447
- );
448
- case "b2_delete_key":
449
- return this.deleteKey(body);
450
- case "b2_update_file_retention":
451
- return this.updateFileRetention(
452
- body
453
- );
454
- case "b2_update_file_legal_hold":
455
- return this.updateFileLegalHold(
456
- body
457
- );
458
- case "b2_get_bucket_notification_rules":
459
- return this.getBucketNotificationRules(body);
460
- case "b2_set_bucket_notification_rules":
461
- return this.setBucketNotificationRules(
462
- body
463
- );
464
- default:
465
- return {
466
- status: 400,
467
- body: { status: 400, code: "bad_request", message: `Unknown endpoint: ${endpoint}` }
468
- };
469
- }
470
- }
471
- /**
472
- * Handles file and part upload requests (`b2_upload_file`, `b2_upload_part`).
473
- * Dispatches to the appropriate internal handler based on the URL.
474
- * @param url - The upload endpoint URL used to determine the upload type.
475
- * @param headers - The HTTP headers containing file metadata and authorization.
476
- * @param data - The raw file or part content as bytes.
477
- *
478
- * @returns An object with HTTP status and JSON response body.
479
- */
480
- handleUpload(url, headers, data) {
481
- const isPart = url.includes("b2_upload_part");
482
- if (this.strictAuth) {
483
- const endpoint = isPart ? "b2_upload_part" : "b2_upload_file";
484
- const authError = this.authorizeRequest(headers["authorization"], endpoint);
485
- if (authError !== null) return authError;
486
- }
487
- if (isPart) {
488
- return this.handleUploadPart(url, headers, data);
489
- }
490
- return this.handleUploadFile(url, headers, data);
491
- }
492
- /**
493
- * Handles file download requests (`b2_download_file_by_id`, `/file/` by name).
494
- * Returns the file data along with B2 response headers.
495
- * @param path - The request URL path identifying the file to download.
496
- * @param headers - The HTTP request headers for range or authorization.
497
- * @param method - The HTTP method; `'HEAD'` suppresses the response body.
498
- *
499
- * @returns The download response containing file data and B2 headers.
500
- */
501
- handleDownload(path, headers, method = "GET") {
502
- if (path.includes("b2_download_file_by_id")) {
503
- if (this.strictAuth) {
504
- const authError = this.authorizeRequest(headers["authorization"], "b2_download_file_by_id");
505
- if (authError !== null) return this.errorAsDownload(authError);
506
- }
507
- const url = new URL(`http://localhost${path}`);
508
- const fileId = url.searchParams.get("fileId") ?? "";
509
- return this.finalizeDownload(this.downloadById(fileId, headers["range"]), url, method);
510
- }
511
- const fileMatch = path.match(/^([^?]+)/)?.[1]?.match(/\/file\/([^/]+)\/(.+)/);
512
- if (fileMatch) {
513
- const bucketName = decodeURIComponent(fileMatch[1] ?? "");
514
- const fileName = decodeURIComponent(fileMatch[2] ?? "");
515
- if (this.strictAuth) {
516
- const authError = this.authorizeRequest(
517
- headers["authorization"],
518
- "b2_download_file_by_name",
519
- void 0,
520
- fileName
521
- );
522
- if (authError !== null) return this.errorAsDownload(authError);
523
- }
524
- const url = new URL(`http://localhost${path}`);
525
- return this.finalizeDownload(
526
- this.downloadByName(bucketName, fileName, headers["range"]),
527
- url,
528
- method
529
- );
530
- }
531
- return { status: 404, headers: {}, data: null };
532
- }
533
- /**
534
- * Convert a synthetic JSON error body into a `SimulatorDownloadResponse`
535
- * shape so the strict-auth gate on download paths can surface 401/403s
536
- * through the same transport plumbing as a regular failed download.
537
- *
538
- * @param json - The JSON error from {@link authorizeRequest}.
539
- *
540
- * @returns A download-shaped response with the error body bytes inline.
541
- */
542
- errorAsDownload(json) {
543
- return {
544
- status: json.status,
545
- headers: { "Content-Type": "application/json" },
546
- data: textCodec.utf8Encoder.encode(JSON.stringify(json.body))
547
- };
548
- }
549
- /**
550
- * Applies HEAD-method body suppression and `b2Content*` response-header
551
- * overrides parsed from the download URL's query string. Mirrors what the
552
- * real B2 service does: any `b2Content*` query parameter is echoed back as
553
- * the corresponding response header.
554
- *
555
- * @param response - The download response produced by {@link downloadById} or {@link downloadByName}.
556
- * @param url - The parsed download URL (used to read `b2Content*` query params).
557
- * @param method - The HTTP method of the originating request.
558
- *
559
- * @returns The response with overrides applied.
560
- */
561
- finalizeDownload(response, url, method) {
562
- const overrideMap = {
563
- b2ContentDisposition: "Content-Disposition",
564
- b2ContentLanguage: "Content-Language",
565
- b2ContentEncoding: "Content-Encoding",
566
- b2ContentType: "Content-Type",
567
- b2CacheControl: "Cache-Control",
568
- b2Expires: "Expires"
569
- };
570
- const newHeaders = { ...response.headers };
571
- for (const [param, header] of Object.entries(overrideMap)) {
572
- const value = url.searchParams.get(param);
573
- if (value !== null) newHeaders[header] = value;
574
- }
575
- const data = method === "HEAD" ? null : response.data;
576
- return { status: response.status, headers: newHeaders, data };
577
- }
578
- handleUploadFile(url, headers, data) {
579
- const bucketId = new URL(url).searchParams.get("bucketId");
580
- if (!bucketId) return this.error(400, "bad_request", "Missing bucketId");
581
- const bucket2 = this.buckets.get(bucketId);
582
- if (!bucket2) return this.error(400, "bad_bucket_id", "Bucket not found");
583
- const fileName = decodeURIComponent(headers["x-bz-file-name"] ?? "");
584
- const contentType = headers["content-type"] ?? "application/octet-stream";
585
- const sha1 = headers["x-bz-content-sha1"] ?? "none";
586
- const fileNameError = validation.validateFileName(fileName);
587
- if (fileNameError) return this.error(400, fileNameError.code, fileNameError.message);
588
- const fileInfo = parseFileInfoHeaders(headers);
589
- const fileInfoError = validation.validateFileInfo(fileInfo);
590
- if (fileInfoError) return this.error(400, fileInfoError.code, fileInfoError.message);
591
- const fileVersion = this.makeFileVersion({
592
- bucketId,
593
- fileName,
594
- contentType,
595
- contentLength: data.byteLength,
596
- contentSha1: sha1,
597
- action: file.FileAction.Upload
598
- });
599
- const stored = { fileVersion, data };
600
- const existing = bucket2.files.get(fileName);
601
- if (existing) {
602
- existing.push(stored);
603
- } else {
604
- bucket2.files.set(fileName, [stored]);
605
- }
606
- this.firePostUploadHooks(fileVersion, bucketId, "b2:ObjectCreated:Upload");
607
- return { status: 200, body: fileVersion };
608
- }
609
- handleUploadPart(url, headers, data) {
610
- const fileId = new URL(url).searchParams.get("fileId");
611
- if (!fileId) return this.error(400, "bad_request", "Missing fileId");
612
- const large = this.largeFiles.get(fileId);
613
- if (!large) return this.error(400, "bad_request", "Large file not found");
614
- const partNumber = Number.parseInt(headers["x-bz-part-number"] ?? "0", 10);
615
- const sha1 = headers["x-bz-content-sha1"] ?? "none";
616
- large.parts.set(partNumber, { data, sha1 });
617
- return {
618
- status: 200,
619
- body: {
620
- fileId: large.fileId,
621
- partNumber,
622
- contentLength: data.byteLength,
623
- contentSha1: sha1,
624
- serverSideEncryption: { mode: encryption.EncryptionMode.None },
625
- uploadTimestamp: Date.now()
626
- }
627
- };
628
- }
629
- downloadById(fileId, range) {
630
- const found = this.findFile(fileId);
631
- if (found === null) return { status: 404, headers: {}, data: null };
632
- return this.serveFile(found.stored, range);
633
- }
634
- downloadByName(bucketName, fileName, range) {
635
- for (const bucket2 of this.buckets.values()) {
636
- if (bucket2.info.bucketName !== bucketName) continue;
637
- const versions = bucket2.files.get(fileName);
638
- if (!versions || versions.length === 0) break;
639
- const latest = versions[versions.length - 1];
640
- if (!latest || latest.fileVersion.action === file.FileAction.Hide) {
641
- return { status: 404, headers: {}, data: null };
642
- }
643
- return this.serveFile(latest, range);
644
- }
645
- return { status: 404, headers: {}, data: null };
646
- }
647
- serveFile(stored, range) {
648
- const fullData = stored.data;
649
- let data = fullData;
650
- let status = 200;
651
- let contentRange = null;
652
- if (range) {
653
- const parsed = parseRangeHeader(range, fullData.byteLength);
654
- if (parsed.kind === "ok") {
655
- data = fullData.subarray(parsed.start, parsed.end + 1);
656
- status = 206;
657
- contentRange = `bytes ${parsed.start}-${parsed.end}/${fullData.byteLength}`;
658
- } else if (parsed.kind === "unsatisfiable") {
659
- return {
660
- status: 416,
661
- headers: {
662
- "Content-Type": stored.fileVersion.contentType,
663
- "Content-Range": `bytes */${fullData.byteLength}`
664
- },
665
- data: new Uint8Array(0)
666
- };
667
- }
668
- }
669
- const fv = stored.fileVersion;
670
- const headers = {
671
- "Content-Type": fv.contentType,
672
- "Content-Length": String(data.byteLength),
673
- "X-Bz-File-Id": fv.fileId,
674
- "X-Bz-File-Name": encodeURIComponent(fv.fileName),
675
- "X-Bz-Content-Sha1": fv.contentSha1 ?? "none",
676
- "X-Bz-Upload-Timestamp": String(fv.uploadTimestamp),
677
- "X-Bz-Info-src_last_modified_millis": String(fv.uploadTimestamp)
678
- };
679
- if (contentRange !== null) {
680
- headers["Content-Range"] = contentRange;
681
- }
682
- return { status, headers, data };
683
- }
684
- // --- API handlers ---
685
- authorize(authzHeader) {
686
- const capabilities2 = [
687
- auth.Capability.ListBuckets,
688
- auth.Capability.ReadBuckets,
689
- auth.Capability.WriteBuckets,
690
- auth.Capability.DeleteBuckets,
691
- auth.Capability.ListFiles,
692
- auth.Capability.ReadFiles,
693
- auth.Capability.WriteFiles,
694
- auth.Capability.DeleteFiles,
695
- auth.Capability.ListKeys,
696
- auth.Capability.WriteKeys,
697
- auth.Capability.DeleteKeys,
698
- auth.Capability.ShareFiles,
699
- auth.Capability.ReadBucketNotifications,
700
- auth.Capability.WriteBucketNotifications
701
- ];
702
- const keyForAuth = this.findKeyForAuthHeader(authzHeader);
703
- const tokenStr = `sim_auth_token_${this.nextId++}`;
704
- this.issuedTokens.set(tokenStr, {
705
- capabilities: keyForAuth?.capabilities ?? capabilities2,
706
- bucketId: keyForAuth?.bucketId ?? null,
707
- namePrefix: keyForAuth?.namePrefix ?? null,
708
- expiresAt: this.now() + this.authTokenTtlMs,
709
- applicationKeyId: keyForAuth?.applicationKeyId ?? null
710
- });
711
- return {
712
- status: 200,
713
- body: {
714
- accountId: ids.accountId(this.accountId),
715
- // `AuthToken` has no public factory by design — auth tokens are
716
- // minted by B2, not constructed by user code. The simulator is
717
- // the only legitimate place that needs to forge one.
718
- authorizationToken: tokenStr,
719
- apiInfo: {
720
- storageApi: {
721
- absoluteMinimumPartSize: this.minimumPartSize,
722
- apiUrl: "http://localhost:0",
723
- bucketId: keyForAuth?.bucketId ?? null,
724
- bucketName: null,
725
- downloadUrl: "http://localhost:0",
726
- infoType: "storageApi",
727
- namePrefix: keyForAuth?.namePrefix ?? null,
728
- recommendedPartSize: this.recommendedPartSize,
729
- s3ApiUrl: "http://localhost:0",
730
- allowed: {
731
- capabilities: keyForAuth?.capabilities ?? capabilities2,
732
- bucketId: keyForAuth?.bucketId ?? null,
733
- bucketName: null,
734
- namePrefix: keyForAuth?.namePrefix ?? null
735
- }
736
- }
737
- },
738
- applicationKeyExpirationTimestamp: null
739
- }
740
- };
741
- }
742
- createBucket(req) {
743
- const nameError = validation.validateBucketName(req.bucketName);
744
- if (nameError) return this.error(400, nameError.code, nameError.message);
745
- if (req.bucketInfo !== void 0) {
746
- const infoError = validation.validateBucketInfo(req.bucketInfo);
747
- if (infoError) return this.error(400, infoError.code, infoError.message);
748
- }
749
- for (const b of this.buckets.values()) {
750
- if (b.info.bucketName === req.bucketName) {
751
- return this.error(400, "duplicate_bucket_name", "Bucket name already in use");
752
- }
753
- }
754
- const bid = ids.bucketId(this.genId("b2_bucket"));
755
- const defaultRetention = req.defaultRetention ?? {
756
- mode: bucket.BucketRetentionMode.None,
757
- period: null
758
- };
759
- const info = {
760
- accountId: ids.accountId(req.accountId),
761
- bucketId: bid,
762
- bucketName: req.bucketName,
763
- bucketType: req.bucketType,
764
- bucketInfo: req.bucketInfo ?? {},
765
- corsRules: req.corsRules ?? [],
766
- defaultServerSideEncryption: req.defaultServerSideEncryption ?? { mode: encryption.EncryptionMode.None },
767
- fileLockConfiguration: {
768
- isClientAuthorizedToRead: true,
769
- value: {
770
- isFileLockEnabled: req.fileLockEnabled ?? false,
771
- defaultRetention
772
- }
773
- },
774
- lifecycleRules: req.lifecycleRules ?? [],
775
- options: [],
776
- revision: 1,
777
- defaultRetention,
778
- replicationConfiguration: req.replicationConfiguration ?? {
779
- asReplicationSource: null,
780
- asReplicationDestination: null
781
- }
782
- };
783
- this.buckets.set(bid, { info, files: /* @__PURE__ */ new Map() });
784
- return { status: 200, body: info };
785
- }
786
- listBuckets() {
787
- const buckets = [...this.buckets.values()].map((b) => b.info);
788
- return { status: 200, body: { buckets } };
789
- }
790
- deleteBucket(req) {
791
- const bucket2 = this.buckets.get(req.bucketId);
792
- if (!bucket2) return this.error(400, "bad_bucket_id", "Bucket not found");
793
- this.buckets.delete(req.bucketId);
794
- return { status: 200, body: bucket2.info };
795
- }
796
- updateBucket(req) {
797
- const bucket2 = this.buckets.get(req["bucketId"]);
798
- if (!bucket2) return this.error(400, "bad_bucket_id", "Bucket not found");
799
- if (req["bucketInfo"] !== void 0) {
800
- const infoError = validation.validateBucketInfo(req["bucketInfo"]);
801
- if (infoError) return this.error(400, infoError.code, infoError.message);
802
- }
803
- const updated = {
804
- ...bucket2.info,
805
- ...req["bucketType"] !== void 0 ? { bucketType: req["bucketType"] } : {},
806
- ...req["bucketInfo"] !== void 0 ? { bucketInfo: req["bucketInfo"] } : {},
807
- ...req["lifecycleRules"] !== void 0 ? { lifecycleRules: req["lifecycleRules"] } : {},
808
- ...req["corsRules"] !== void 0 ? { corsRules: req["corsRules"] } : {},
809
- ...req["replicationConfiguration"] !== void 0 ? {
810
- replicationConfiguration: req["replicationConfiguration"]
811
- } : {},
812
- ...req["defaultRetention"] !== void 0 ? {
813
- defaultRetention: req["defaultRetention"],
814
- fileLockConfiguration: {
815
- isClientAuthorizedToRead: true,
816
- value: {
817
- isFileLockEnabled: bucket2.info.fileLockConfiguration.value?.isFileLockEnabled ?? false,
818
- defaultRetention: req["defaultRetention"]
819
- }
820
- }
821
- } : {},
822
- ...req["defaultServerSideEncryption"] !== void 0 ? {
823
- defaultServerSideEncryption: req["defaultServerSideEncryption"]
824
- } : {},
825
- revision: bucket2.info.revision + 1
826
- };
827
- this.buckets.set(req["bucketId"], { info: updated, files: bucket2.files });
828
- return { status: 200, body: updated };
829
- }
830
- getUploadUrl(req) {
831
- if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
832
- return {
833
- status: 200,
834
- body: {
835
- bucketId: req.bucketId,
836
- uploadUrl: `http://localhost:0/b2api/v3/b2_upload_file?bucketId=${req.bucketId}`,
837
- authorizationToken: "sim_upload_token"
838
- }
839
- };
840
- }
841
- listFileNames(req) {
842
- const bucket2 = this.buckets.get(req.bucketId);
843
- if (!bucket2) return this.error(400, "bad_bucket_id", "Bucket not found");
844
- const countError = validation.validateMaxCount(req.maxFileCount, "b2_list_file_names");
845
- if (countError) return this.error(400, countError.code, countError.message);
846
- const max = req.maxFileCount ?? 1e3;
847
- const prefix = req.prefix ?? "";
848
- let allFiles = [...bucket2.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) => a.fileName.localeCompare(b.fileName));
849
- if (req.startFileName) {
850
- const start = req.startFileName;
851
- allFiles = allFiles.filter((f) => f.fileName >= start);
852
- }
853
- const files = allFiles.slice(0, max);
854
- const nextFileName = allFiles.length > max ? allFiles[max]?.fileName ?? null : null;
855
- return { status: 200, body: { files, nextFileName } };
856
- }
857
- listFileVersions(req) {
858
- const bucket2 = this.buckets.get(req.bucketId);
859
- if (!bucket2) return this.error(400, "bad_bucket_id", "Bucket not found");
860
- const countError = validation.validateMaxCount(req.maxFileCount, "b2_list_file_versions");
861
- if (countError) return this.error(400, countError.code, countError.message);
862
- const max = req.maxFileCount ?? 1e3;
863
- const prefix = req.prefix ?? "";
864
- const allVersions = [...bucket2.files.entries()].filter(([name]) => name.startsWith(prefix)).flatMap(([_, versions]) => versions.map((v) => v.fileVersion)).sort((a, b) => {
865
- const nameCmp = a.fileName.localeCompare(b.fileName);
866
- if (nameCmp !== 0) return nameCmp;
867
- return b.uploadTimestamp - a.uploadTimestamp;
868
- });
869
- let startIdx = 0;
870
- if (req.startFileName !== void 0) {
871
- const startName = req.startFileName;
872
- const startId = req.startFileId;
873
- const nameIdx = allVersions.findIndex((f) => f.fileName >= startName);
874
- if (nameIdx === -1) {
875
- startIdx = allVersions.length;
876
- } else if (startId !== void 0) {
877
- const exactIdx = allVersions.findIndex((f, i) => i >= nameIdx && f.fileId === startId);
878
- startIdx = exactIdx !== -1 ? exactIdx : nameIdx;
879
- } else {
880
- startIdx = nameIdx;
881
- }
882
- }
883
- const sliced = allVersions.slice(startIdx, startIdx + max);
884
- const hasMore = startIdx + max < allVersions.length;
885
- const nextFileName = hasMore ? allVersions[startIdx + max]?.fileName ?? null : null;
886
- const nextFileId = hasMore ? allVersions[startIdx + max]?.fileId ?? null : null;
887
- return { status: 200, body: { files: sliced, nextFileName, nextFileId } };
888
- }
889
- getFileInfo(req) {
890
- const found = this.findFile(req.fileId);
891
- if (found === null) return this.error(404, "file_not_present", "File not found");
892
- return { status: 200, body: found.stored.fileVersion };
893
- }
894
- hideFile(req) {
895
- const bucket2 = this.buckets.get(req.bucketId);
896
- if (!bucket2) return this.error(400, "bad_bucket_id", "Bucket not found");
897
- const nameError = validation.validateFileName(req.fileName);
898
- if (nameError) return this.error(400, nameError.code, nameError.message);
899
- const fileVersion = this.makeFileVersion({
900
- bucketId: req.bucketId,
901
- fileName: req.fileName,
902
- contentType: "application/octet-stream",
903
- contentLength: 0,
904
- contentSha1: "none",
905
- action: file.FileAction.Hide
906
- });
907
- const existing = bucket2.files.get(req.fileName);
908
- const stored = { fileVersion, data: new Uint8Array(0) };
909
- if (existing) {
910
- existing.push(stored);
911
- } else {
912
- bucket2.files.set(req.fileName, [stored]);
913
- }
914
- return { status: 200, body: fileVersion };
915
- }
916
- deleteFileVersion(req) {
917
- const found = this.findFile(req.fileId);
918
- if (found === null || found.stored.fileVersion.fileName !== req.fileName) {
919
- return this.error(400, "file_not_present", "File version not found");
920
- }
921
- const fv = found.stored.fileVersion;
922
- const retention = fv.fileRetention?.value;
923
- const legalHold = fv.legalHold?.value;
924
- const now = Date.now();
925
- if (legalHold === "on") {
926
- return this.error(
927
- 400,
928
- "file_lock_legal_hold_protected",
929
- "File is on legal hold and cannot be deleted"
930
- );
931
- }
932
- if (retention?.mode === "compliance" && retention.retainUntilTimestamp !== null && retention.retainUntilTimestamp > now) {
933
- return this.error(
934
- 400,
935
- "file_lock_compliance_protected",
936
- `File is under compliance-mode retention and cannot be deleted until ${new Date(retention.retainUntilTimestamp).toISOString()}`
937
- );
938
- }
939
- if (retention?.mode === "governance" && retention.retainUntilTimestamp !== null && retention.retainUntilTimestamp > now && req.bypassGovernance !== true) {
940
- return this.error(
941
- 400,
942
- "file_lock_governance_protected",
943
- "File is under governance-mode retention; pass bypassGovernance: true to delete"
944
- );
945
- }
946
- found.versions.splice(found.index, 1);
947
- if (found.versions.length === 0) found.bucket.files.delete(req.fileName);
948
- return { status: 200, body: { fileId: req.fileId, fileName: req.fileName } };
949
- }
950
- copyFile(req) {
951
- const nameError = validation.validateFileName(req.fileName);
952
- if (nameError) return this.error(400, nameError.code, nameError.message);
953
- const found = this.findFile(req.sourceFileId);
954
- if (found === null) return this.error(404, "file_not_present", "Source file not found");
955
- const sourceStored = found.stored;
956
- const destBucketId = req.destinationBucketId ?? found.bucketId;
957
- const destBucket = this.buckets.get(destBucketId);
958
- if (!destBucket) return this.error(400, "bad_bucket_id", "Destination bucket not found");
959
- const fileVersion = this.makeFileVersion({
960
- bucketId: destBucketId,
961
- fileName: req.fileName,
962
- contentType: sourceStored.fileVersion.contentType,
963
- contentLength: sourceStored.data.byteLength,
964
- contentSha1: sourceStored.fileVersion.contentSha1 ?? "none",
965
- action: file.FileAction.Copy
966
- });
967
- const copied = { fileVersion, data: new Uint8Array(sourceStored.data) };
968
- const existing = destBucket.files.get(req.fileName);
969
- if (existing) {
970
- existing.push(copied);
971
- } else {
972
- destBucket.files.set(req.fileName, [copied]);
973
- }
974
- this.firePostUploadHooks(fileVersion, destBucketId, "b2:ObjectCreated:Copy");
975
- return { status: 200, body: fileVersion };
976
- }
977
- startLargeFile(req) {
978
- if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
979
- const nameError = validation.validateFileName(req.fileName);
980
- if (nameError) return this.error(400, nameError.code, nameError.message);
981
- if (req.fileInfo !== void 0) {
982
- const infoError = validation.validateFileInfo(req.fileInfo);
983
- if (infoError) return this.error(400, infoError.code, infoError.message);
984
- }
985
- const fid = this.genId("4_z");
986
- const large = {
987
- fileId: fid,
988
- bucketId: req.bucketId,
989
- fileName: req.fileName,
990
- contentType: req.contentType,
991
- fileInfo: req.fileInfo ?? {},
992
- parts: /* @__PURE__ */ new Map()
993
- };
994
- this.largeFiles.set(fid, large);
995
- return {
996
- status: 200,
997
- body: {
998
- fileId: fid,
999
- fileName: req.fileName,
1000
- accountId: this.accountId,
1001
- bucketId: req.bucketId,
1002
- contentType: req.contentType,
1003
- fileInfo: large.fileInfo
1004
- }
1005
- };
1006
- }
1007
- getUploadPartUrl(req) {
1008
- if (!this.largeFiles.has(req.fileId))
1009
- return this.error(400, "bad_request", "Large file not found");
1010
- return {
1011
- status: 200,
1012
- body: {
1013
- fileId: req.fileId,
1014
- uploadUrl: `http://localhost:0/b2api/v3/b2_upload_part?fileId=${req.fileId}`,
1015
- authorizationToken: "sim_part_token"
1016
- }
1017
- };
1018
- }
1019
- finishLargeFile(req) {
1020
- const large = this.largeFiles.get(req.fileId);
1021
- if (!large) return this.error(400, "bad_request", "Large file not found");
1022
- const bucket2 = this.buckets.get(large.bucketId);
1023
- if (!bucket2) return this.error(400, "bad_bucket_id", "Bucket not found");
1024
- const sortedParts = [...large.parts.entries()].sort((a, b) => a[0] - b[0]);
1025
- if (sortedParts.length > 1e4) {
1026
- return this.error(
1027
- 400,
1028
- "bad_request",
1029
- `multipart upload has ${sortedParts.length} parts; B2 caps at 10000`
1030
- );
1031
- }
1032
- for (const [partNumber] of sortedParts) {
1033
- if (!Number.isInteger(partNumber) || partNumber < 1 || partNumber > 1e4) {
1034
- return this.error(
1035
- 400,
1036
- "bad_request",
1037
- `partNumber ${partNumber} is outside the [1, 10000] range B2 accepts`
1038
- );
1039
- }
1040
- }
1041
- if (req.partSha1Array.length !== sortedParts.length) {
1042
- return this.error(
1043
- 400,
1044
- "bad_request",
1045
- `partSha1Array has ${req.partSha1Array.length} entries but ${sortedParts.length} parts were uploaded`
1046
- );
1047
- }
1048
- for (let i = 0; i < sortedParts.length - 1; i++) {
1049
- const [partNumber, part] = sortedParts[i];
1050
- if (part.data.byteLength < this.minimumPartSize) {
1051
- return this.error(
1052
- 400,
1053
- "bad_request",
1054
- `part ${partNumber} (${part.data.byteLength} bytes) is smaller than the minimum part size of ${this.minimumPartSize}`
1055
- );
1056
- }
1057
- }
1058
- let totalSize = 0;
1059
- for (const [_, part] of sortedParts) totalSize += part.data.byteLength;
1060
- const combined = new Uint8Array(totalSize);
1061
- let offset = 0;
1062
- for (const [_, part] of sortedParts) {
1063
- combined.set(part.data, offset);
1064
- offset += part.data.byteLength;
1065
- }
1066
- const fileVersion = this.makeFileVersion({
1067
- bucketId: large.bucketId,
1068
- fileName: large.fileName,
1069
- contentType: large.contentType,
1070
- contentLength: totalSize,
1071
- contentSha1: "none",
1072
- action: file.FileAction.Upload
1073
- });
1074
- const stored = { fileVersion, data: combined };
1075
- const existing = bucket2.files.get(large.fileName);
1076
- if (existing) {
1077
- existing.push(stored);
1078
- } else {
1079
- bucket2.files.set(large.fileName, [stored]);
1080
- }
1081
- this.largeFiles.delete(req.fileId);
1082
- this.firePostUploadHooks(fileVersion, large.bucketId, "b2:ObjectCreated:Upload");
1083
- return { status: 200, body: fileVersion };
1084
- }
1085
- cancelLargeFile(req) {
1086
- const large = this.largeFiles.get(req.fileId);
1087
- if (!large) return this.error(400, "bad_request", "Large file not found");
1088
- this.largeFiles.delete(req.fileId);
1089
- return {
1090
- status: 200,
1091
- body: {
1092
- fileId: large.fileId,
1093
- accountId: this.accountId,
1094
- bucketId: large.bucketId,
1095
- fileName: large.fileName
1096
- }
1097
- };
1098
- }
1099
- listUnfinishedLargeFiles(req) {
1100
- const countError = validation.validateMaxCount(req.maxFileCount, "b2_list_unfinished_large_files");
1101
- if (countError) return this.error(400, countError.code, countError.message);
1102
- const prefix = req.namePrefix ?? "";
1103
- const max = req.maxFileCount ?? 100;
1104
- const candidates = [...this.largeFiles.values()].filter((f) => f.bucketId === req.bucketId).filter((f) => f.fileName.startsWith(prefix)).sort((a, b) => a.fileName.localeCompare(b.fileName));
1105
- let startIndex = 0;
1106
- if (req.startFileId !== void 0) {
1107
- const found = candidates.findIndex((f) => f.fileId === req.startFileId);
1108
- startIndex = found >= 0 ? found : 0;
1109
- }
1110
- const slice = candidates.slice(startIndex, startIndex + max);
1111
- const files = slice.map((f) => ({
1112
- fileId: f.fileId,
1113
- fileName: f.fileName,
1114
- accountId: this.accountId,
1115
- bucketId: f.bucketId,
1116
- contentType: f.contentType,
1117
- fileInfo: f.fileInfo
1118
- }));
1119
- const hasMore = startIndex + max < candidates.length;
1120
- const nextFileId = hasMore ? candidates[startIndex + max]?.fileId ?? null : null;
1121
- return { status: 200, body: { files, nextFileId } };
1122
- }
1123
- listParts(req) {
1124
- const large = this.largeFiles.get(req.fileId);
1125
- if (!large) return this.error(400, "bad_request", "Large file not found");
1126
- const countError = validation.validateMaxCount(req.maxPartCount, "b2_list_parts");
1127
- if (countError) return this.error(400, countError.code, countError.message);
1128
- const start = req.startPartNumber ?? 1;
1129
- const max = req.maxPartCount ?? 1e3;
1130
- const allParts = [...large.parts.entries()].filter(([n]) => n >= start).sort((a, b) => a[0] - b[0]).map(([partNumber, part]) => ({
1131
- fileId: req.fileId,
1132
- partNumber,
1133
- contentLength: part.data.byteLength,
1134
- contentSha1: part.sha1,
1135
- uploadTimestamp: Date.now()
1136
- }));
1137
- const parts = allParts.slice(0, max);
1138
- const nextPartNumber = allParts.length > max ? allParts[max]?.partNumber ?? null : null;
1139
- return { status: 200, body: { parts, nextPartNumber } };
1140
- }
1141
- async copyPart(req) {
1142
- const large = this.largeFiles.get(req.largeFileId);
1143
- if (!large) return this.error(400, "bad_request", "Large file not found");
1144
- const found = this.findFile(req.sourceFileId);
1145
- if (found === null) return this.error(404, "file_not_present", "Source file not found");
1146
- const sourceStored = found.stored;
1147
- let partData = sourceStored.data;
1148
- if (req.range) {
1149
- const match = req.range.match(/bytes=(\d+)-(\d+)?/);
1150
- if (match) {
1151
- const rs = Number.parseInt(match[1] ?? "0", 10);
1152
- const re = match[2] !== void 0 ? Number.parseInt(match[2], 10) : sourceStored.data.byteLength - 1;
1153
- partData = sourceStored.data.slice(rs, re + 1);
1154
- }
1155
- }
1156
- const sha1 = await hash.sha1Hex(partData);
1157
- large.parts.set(req.partNumber, { data: new Uint8Array(partData), sha1 });
1158
- return {
1159
- status: 200,
1160
- body: {
1161
- fileId: req.largeFileId,
1162
- partNumber: req.partNumber,
1163
- contentLength: partData.byteLength,
1164
- contentSha1: sha1
1165
- }
1166
- };
1167
- }
1168
- getDownloadAuthorization(req) {
1169
- if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
1170
- return {
1171
- status: 200,
1172
- body: {
1173
- bucketId: req.bucketId,
1174
- fileNamePrefix: req.fileNamePrefix,
1175
- authorizationToken: `sim_dl_auth_${this.genId("tok")}`
1176
- }
1177
- };
1178
- }
1179
- // --- Keys ---
1180
- createKey(req) {
1181
- const kid = this.genId("sim_key");
1182
- const appKey = this.genId("sim_secret");
1183
- const expiration = req.validDurationInSeconds !== void 0 ? Date.now() + req.validDurationInSeconds * 1e3 : null;
1184
- const stored = {
1185
- applicationKeyId: kid,
1186
- keyName: req.keyName,
1187
- capabilities: req.capabilities,
1188
- accountId: req.accountId,
1189
- applicationKey: appKey,
1190
- bucketId: req.bucketId ?? null,
1191
- namePrefix: req.namePrefix ?? null,
1192
- expirationTimestamp: expiration
1193
- };
1194
- this.keys.set(kid, stored);
1195
- return {
1196
- status: 200,
1197
- body: {
1198
- keyName: stored.keyName,
1199
- applicationKeyId: stored.applicationKeyId,
1200
- applicationKey: stored.applicationKey,
1201
- capabilities: stored.capabilities,
1202
- accountId: stored.accountId,
1203
- expirationTimestamp: stored.expirationTimestamp,
1204
- bucketId: stored.bucketId,
1205
- namePrefix: stored.namePrefix,
1206
- options: []
1207
- }
1208
- };
1209
- }
1210
- listKeys(req) {
1211
- const countError = validation.validateMaxCount(req.maxKeyCount, "b2_list_keys");
1212
- if (countError) return this.error(400, countError.code, countError.message);
1213
- const max = req.maxKeyCount ?? 1e3;
1214
- let allKeys = [...this.keys.values()].sort(
1215
- (a, b) => a.applicationKeyId.localeCompare(b.applicationKeyId)
1216
- );
1217
- if (req.startApplicationKeyId) {
1218
- const start = req.startApplicationKeyId;
1219
- allKeys = allKeys.filter((k) => k.applicationKeyId >= start);
1220
- }
1221
- const keys = allKeys.slice(0, max).map((k) => ({
1222
- keyName: k.keyName,
1223
- applicationKeyId: k.applicationKeyId,
1224
- capabilities: k.capabilities,
1225
- accountId: k.accountId,
1226
- expirationTimestamp: k.expirationTimestamp,
1227
- bucketId: k.bucketId,
1228
- namePrefix: k.namePrefix,
1229
- options: []
1230
- }));
1231
- const nextId = allKeys.length > max ? allKeys[max]?.applicationKeyId ?? null : null;
1232
- return { status: 200, body: { keys, nextApplicationKeyId: nextId } };
1233
- }
1234
- deleteKey(req) {
1235
- const key = this.keys.get(req.applicationKeyId);
1236
- if (!key) return this.error(400, "bad_request", "Key not found");
1237
- this.keys.delete(req.applicationKeyId);
1238
- for (const [token, scope] of this.issuedTokens.entries()) {
1239
- if (scope.applicationKeyId === req.applicationKeyId) {
1240
- this.issuedTokens.delete(token);
1241
- }
1242
- }
1243
- return {
1244
- status: 200,
1245
- body: {
1246
- keyName: key.keyName,
1247
- applicationKeyId: key.applicationKeyId,
1248
- capabilities: key.capabilities,
1249
- accountId: key.accountId,
1250
- expirationTimestamp: key.expirationTimestamp,
1251
- bucketId: key.bucketId,
1252
- namePrefix: key.namePrefix,
1253
- options: []
1254
- }
1255
- };
1256
- }
1257
- // --- File lock ---
1258
- updateFileRetention(req) {
1259
- const found = this.findFile(req.fileId);
1260
- if (found === null || found.stored.fileVersion.fileName !== req.fileName) {
1261
- return this.error(404, "file_not_present", "File not found");
1262
- }
1263
- found.versions[found.index] = {
1264
- fileVersion: {
1265
- ...found.stored.fileVersion,
1266
- fileRetention: { isClientAuthorizedToRead: true, value: req.fileRetention }
1267
- },
1268
- data: found.stored.data
1269
- };
1270
- return {
1271
- status: 200,
1272
- body: {
1273
- fileName: req.fileName,
1274
- fileId: req.fileId,
1275
- fileRetention: req.fileRetention
1276
- }
1277
- };
1278
- }
1279
- updateFileLegalHold(req) {
1280
- const found = this.findFile(req.fileId);
1281
- if (found === null || found.stored.fileVersion.fileName !== req.fileName) {
1282
- return this.error(404, "file_not_present", "File not found");
1283
- }
1284
- found.versions[found.index] = {
1285
- fileVersion: {
1286
- ...found.stored.fileVersion,
1287
- legalHold: {
1288
- isClientAuthorizedToRead: true,
1289
- value: req.legalHold
1290
- }
1291
- },
1292
- data: found.stored.data
1293
- };
1294
- return {
1295
- status: 200,
1296
- body: {
1297
- fileName: req.fileName,
1298
- fileId: req.fileId,
1299
- legalHold: req.legalHold
1300
- }
1301
- };
1302
- }
1303
- // --- Notifications ---
1304
- getBucketNotificationRules(req) {
1305
- if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
1306
- const rules = this.notificationRules.get(req.bucketId) ?? [];
1307
- return { status: 200, body: { bucketId: req.bucketId, eventNotificationRules: rules } };
1308
- }
1309
- setBucketNotificationRules(req) {
1310
- if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
1311
- this.notificationRules.set(req.bucketId, req.eventNotificationRules);
1312
- return {
1313
- status: 200,
1314
- body: { bucketId: req.bucketId, eventNotificationRules: req.eventNotificationRules }
1315
- };
1316
- }
1317
- // --- Helpers ---
1318
- /**
1319
- * Locates a stored file version by its `fileId`, scanning every bucket.
1320
- *
1321
- * Returns enough context to support read-only inspection (`stored`,
1322
- * `bucketId`) AND in-place mutation (`versions`, `index`) so callers
1323
- * that need to splice the version out can do so without re-scanning.
1324
- *
1325
- * Real B2 fileIds embed the bucketId, so production lookups are O(1);
1326
- * the simulator's flat ID generator (`this.genId('4_z')`) doesn't, so this
1327
- * is O(buckets × files × versions). Acceptable for tests.
1328
- *
1329
- * @param fileId - The file version ID to locate.
1330
- *
1331
- * @returns The location of the matching version, or `null` if not found.
1332
- */
1333
- findFile(fileId) {
1334
- for (const [bid, bucket2] of this.buckets.entries()) {
1335
- for (const versions of bucket2.files.values()) {
1336
- const idx = versions.findIndex((v) => v.fileVersion.fileId === fileId);
1337
- if (idx !== -1) {
1338
- const stored = versions[idx];
1339
- return { stored, bucketId: bid, bucket: bucket2, versions, index: idx };
1340
- }
1341
- }
1342
- }
1343
- return null;
1344
- }
1345
- makeFileVersion(params) {
1346
- return {
1347
- accountId: ids.accountId(this.accountId),
1348
- action: params.action,
1349
- bucketId: ids.bucketId(params.bucketId),
1350
- contentLength: params.contentLength,
1351
- contentMd5: null,
1352
- contentSha1: params.contentSha1,
1353
- contentType: params.contentType,
1354
- fileId: ids.fileId(this.genId("4_z")),
1355
- fileInfo: {},
1356
- fileName: params.fileName,
1357
- fileRetention: { isClientAuthorizedToRead: true, value: null },
1358
- legalHold: { isClientAuthorizedToRead: true, value: null },
1359
- replicationStatus: null,
1360
- serverSideEncryption: { mode: encryption.EncryptionMode.None },
1361
- uploadTimestamp: this.monotonicTimestamp()
1362
- };
1363
- }
1364
- error(status, code, message) {
1365
- return { status, body: { status, code, message } };
1366
- }
1367
- /**
1368
- * Fire the pluggable post-upload hooks for a file that just landed in
1369
- * a bucket: matching event-notification rules → `onWebhookDeliver`,
1370
- * configured replication source rules → `onReplicate`. Errors thrown
1371
- * from user-supplied hooks are swallowed so a buggy listener never
1372
- * masks an otherwise-successful API response.
1373
- *
1374
- * Fired-and-forgotten (no await) by the handler so the synthetic
1375
- * response is returned to the caller as fast as production B2 would
1376
- * acknowledge the write.
1377
- *
1378
- * @param fileVersion - The freshly-stored file metadata.
1379
- * @param bucketId - The bucket the upload landed in.
1380
- * @param eventType - The B2 event-type tag (e.g. `'b2:ObjectCreated:Upload'`)
1381
- * used to match against `EventNotificationRule.eventTypes` globs.
1382
- */
1383
- firePostUploadHooks(fileVersion, bucketId, eventType) {
1384
- const bucket2 = this.buckets.get(bucketId);
1385
- if (!bucket2) return;
1386
- if (this.onWebhookDeliver !== void 0) {
1387
- const rules = this.notificationRules.get(bucketId) ?? [];
1388
- for (const rule of rules) {
1389
- if (!rule.isEnabled) continue;
1390
- const matches = rule.eventTypes.some((pattern) => eventTypeMatches(pattern, eventType));
1391
- if (!matches) continue;
1392
- const hook = this.onWebhookDeliver;
1393
- this.dispatchHook("webhook", () => hook({ rule, fileVersion, bucketId }));
1394
- }
1395
- }
1396
- if (this.onReplicate !== void 0) {
1397
- const replConfig = bucket2.info.replicationConfiguration;
1398
- const sourceRules = replConfig.asReplicationSource?.replicationRules ?? [];
1399
- for (const rule of sourceRules) {
1400
- if (!rule.isEnabled) continue;
1401
- if (rule.fileNamePrefix && !fileVersion.fileName.startsWith(rule.fileNamePrefix)) continue;
1402
- const hook = this.onReplicate;
1403
- this.dispatchHook(
1404
- "replication",
1405
- () => hook({
1406
- sourceFileVersion: fileVersion,
1407
- sourceBucketId: bucketId,
1408
- destinationBucketId: rule.destinationBucketId
1409
- })
1410
- );
1411
- }
1412
- }
1413
- }
1414
- /**
1415
- * Schedule a user-supplied hook callback. Errors are routed to
1416
- * `onHookError` if configured, swallowed otherwise. The returned
1417
- * promise is tracked in {@link pendingHooks} so `flushHooks()` can
1418
- * wait for every dispatched hook to settle.
1419
- *
1420
- * @param kind - Which hook this is (`'webhook'` or `'replication'`).
1421
- * @param fn - Thunk that performs the hook invocation.
1422
- */
1423
- dispatchHook(kind, fn) {
1424
- const task = Promise.resolve().then(() => fn()).catch((err) => {
1425
- if (this.onHookError !== void 0) {
1426
- this.onHookError({ kind, error: toError.toError(err) });
1427
- }
1428
- }).finally(() => {
1429
- this.pendingHooks.delete(task);
1430
- });
1431
- this.pendingHooks.add(task);
1432
- }
135
+ function requestStringField(body, field) {
136
+ if (typeof body !== "object" || body === null) return void 0;
137
+ const value = body[field];
138
+ return typeof value === "string" ? value : void 0;
1433
139
  }
1434
- const eventTypeRegexCache = /* @__PURE__ */ new Map();
140
+ function fileNames(...names) {
141
+ const present = names.filter((name) => name !== void 0);
142
+ return present.length > 0 ? present : void 0;
143
+ }
144
+ function notificationRulePrefixes(body) {
145
+ if (typeof body !== "object" || body === null) return void 0;
146
+ const rules = body["eventNotificationRules"];
147
+ if (!Array.isArray(rules)) return void 0;
148
+ return rules.map((rule) => {
149
+ if (typeof rule !== "object" || rule === null) return "";
150
+ const objectNamePrefix = rule["objectNamePrefix"];
151
+ return typeof objectNamePrefix === "string" ? objectNamePrefix : "";
152
+ });
153
+ }
154
+ function storedNotificationRulePrefixes(rules) {
155
+ return (rules ?? []).map((rule) => typeof rule.objectNamePrefix === "string" ? rule.objectNamePrefix : "");
156
+ }
157
+ function hasKeyManagementCapability(capabilities) {
158
+ return capabilities.some((capability) => capability === require_auth.Capability.ListKeys || capability === require_auth.Capability.WriteKeys || capability === require_auth.Capability.DeleteKeys);
159
+ }
160
+ function publicServerSideEncryption(encryption) {
161
+ if (encryption.mode === require_encryption.EncryptionMode.SseC) return {
162
+ mode: encryption.mode,
163
+ algorithm: encryption.algorithm
164
+ };
165
+ if (encryption.mode === require_encryption.EncryptionMode.None) return {
166
+ mode: null,
167
+ algorithm: null
168
+ };
169
+ return encryption;
170
+ }
171
+ function uploadServerSideEncryption(headers, fallback) {
172
+ if (headers["x-bz-server-side-encryption-customer-algorithm"] === require_encryption.EncryptionAlgorithm.Aes256) return {
173
+ mode: require_encryption.EncryptionMode.SseC,
174
+ algorithm: require_encryption.EncryptionAlgorithm.Aes256,
175
+ customerKey: headers["x-bz-server-side-encryption-customer-key"] ?? "",
176
+ customerKeyMd5: headers["x-bz-server-side-encryption-customer-key-md5"] ?? ""
177
+ };
178
+ if (headers["x-bz-server-side-encryption"] === require_encryption.EncryptionAlgorithm.Aes256) return {
179
+ mode: require_encryption.EncryptionMode.SseB2,
180
+ algorithm: require_encryption.EncryptionAlgorithm.Aes256
181
+ };
182
+ return fallback;
183
+ }
184
+ function defaultFileRetention(policy, uploadTimestamp) {
185
+ if (policy.mode === require_bucket.BucketRetentionMode.None || policy.period === null) return null;
186
+ const days = policy.period.unit === "days" ? policy.period.duration : policy.period.duration * 365;
187
+ return {
188
+ mode: policy.mode,
189
+ retainUntilTimestamp: uploadTimestamp + days * 24 * 60 * 60 * 1e3
190
+ };
191
+ }
192
+ /**
193
+ * In-memory B2 simulator for testing. Implements the B2 native API at the
194
+ * request/response level without any network I/O. Supports 25+ operations
195
+ * including buckets, files, large files, keys, and notifications.
196
+ *
197
+ * @example
198
+ * ```ts
199
+ * const sim = new B2Simulator()
200
+ * const client = new B2Client({
201
+ * applicationKeyId: 'test-key-id',
202
+ * applicationKey: 'test-key',
203
+ * transport: sim.transport(),
204
+ * })
205
+ * await client.authorize()
206
+ * ```
207
+ */
208
+ var B2Simulator = class {
209
+ buckets = /* @__PURE__ */ new Map();
210
+ accountId = "sim_account_0001";
211
+ largeFiles = /* @__PURE__ */ new Map();
212
+ keys = /* @__PURE__ */ new Map();
213
+ notificationRules = /* @__PURE__ */ new Map();
214
+ minimumPartSize;
215
+ recommendedPartSize;
216
+ faults = [];
217
+ nextFaultId = 1;
218
+ onWebhookDeliver;
219
+ onReplicate;
220
+ onHookError;
221
+ strictAuth;
222
+ authTokenTtlMs;
223
+ /**
224
+ * Issued auth tokens with their associated grant scope + expiry. In
225
+ * permissive mode (`strictAuth: false`) this is still populated by
226
+ * `authorize` but never consulted on subsequent requests. In strict
227
+ * mode each request looks up its `Authorization` header here.
228
+ */
229
+ issuedTokens = /* @__PURE__ */ new Map();
230
+ /**
231
+ * Mutable upload-token overrides for tokens minted by `b2_get_upload_url`
232
+ * and `b2_get_upload_part_url`. The token string is self-describing
233
+ * and signed so another simulator instance can validate issued-token
234
+ * state, while this map records local invalidation / forced-expiry
235
+ * state for tests.
236
+ */
237
+ uploadTokens = /* @__PURE__ */ new Map();
238
+ /**
239
+ * Virtual-clock offset applied to `Date.now()` for expiry checks.
240
+ * Defaults to 0. Tests advance via {@link advanceTime} to fast-forward
241
+ * past auth-token expiry without sleeping.
242
+ */
243
+ clockOffsetMs = 0;
244
+ /**
245
+ * Per-instance monotonic counter used to mint realistic-looking
246
+ * IDs (`b2_bucket_<24-hex>`, `4_z<24-hex>`, etc.) and auth tokens.
247
+ * Module-global previously, which leaked state across `B2Simulator`
248
+ * instances in the same Vitest worker — concurrent tests could see
249
+ * colliding tokens. Per-instance ensures determinism within a single
250
+ * simulator's lifetime.
251
+ */
252
+ nextId = 1;
253
+ /**
254
+ * Last-issued upload timestamp. The simulator enforces strict
255
+ * monotonicity so version ordering is deterministic for tests; if
256
+ * two writes hit the same `Date.now()` millisecond, the second
257
+ * receives `lastTimestamp + 1`.
258
+ */
259
+ lastTimestamp = 0;
260
+ /**
261
+ * Outstanding fire-and-forget hook invocations. Tracked so tests
262
+ * (and future {@link flushHooks} callers) can wait for every
263
+ * dispatched `onWebhookDeliver` / `onReplicate` callback to settle
264
+ * before asserting against observable side effects. The microtask
265
+ * dance in fidelity tests was previously brittle: positive cases
266
+ * flushed twice, negative cases flushed once, and any addition to
267
+ * the hook dispatch chain quietly broke the negative path.
268
+ */
269
+ pendingHooks = /* @__PURE__ */ new Set();
270
+ /**
271
+ * Constructs a new in-memory B2 simulator.
272
+ * @param options - Optional simulator overrides. See {@link B2SimulatorOptions}.
273
+ */
274
+ constructor(options = {}) {
275
+ this.minimumPartSize = options.minimumPartSize ?? 5e6;
276
+ this.recommendedPartSize = options.recommendedPartSize ?? 1e8;
277
+ if (options.onWebhookDeliver !== void 0) this.onWebhookDeliver = options.onWebhookDeliver;
278
+ if (options.onReplicate !== void 0) this.onReplicate = options.onReplicate;
279
+ if (options.onHookError !== void 0) this.onHookError = options.onHookError;
280
+ this.strictAuth = options.strictAuth ?? false;
281
+ this.authTokenTtlMs = options.authTokenTtlMs ?? 1440 * 60 * 1e3;
282
+ }
283
+ /**
284
+ * Advance the simulator's virtual clock by `ms` milliseconds. Used in
285
+ * conjunction with `strictAuth: true` + a finite `authTokenTtlMs` to
286
+ * force token expiry without `setTimeout`-based delays.
287
+ *
288
+ * @param ms - Milliseconds to advance. Negative values rewind (rarely useful).
289
+ */
290
+ advanceTime(ms) {
291
+ this.clockOffsetMs += ms;
292
+ }
293
+ /**
294
+ * Expire an upload authorization token previously returned by
295
+ * `b2_get_upload_url` or `b2_get_upload_part_url`. Tests can use this
296
+ * to simulate a stale upload URL and verify retry paths that fetch a
297
+ * fresh URL/token pair.
298
+ *
299
+ * @param authorizationToken - The upload authorization token to expire.
300
+ *
301
+ * @returns `true` when the token existed and was expired, otherwise `false`.
302
+ */
303
+ expireUploadToken(authorizationToken) {
304
+ const now = this.now();
305
+ const token = this.uploadTokens.get(authorizationToken);
306
+ if (token === void 0 || now >= token.cleanupAt) return false;
307
+ token.expiresAt = now - 1;
308
+ this.uploadTokens.set(authorizationToken, token);
309
+ return true;
310
+ }
311
+ /**
312
+ * Invalidate an upload authorization token previously returned by
313
+ * `b2_get_upload_url` or `b2_get_upload_part_url`.
314
+ *
315
+ * @param authorizationToken - The upload authorization token to invalidate.
316
+ *
317
+ * @returns `true` when the token existed and was invalidated, otherwise `false`.
318
+ */
319
+ invalidateUploadToken(authorizationToken) {
320
+ const now = this.now();
321
+ const token = this.uploadTokens.get(authorizationToken);
322
+ if (token === void 0 || now >= token.cleanupAt) {
323
+ this.pruneExpiredUploadTokens(now);
324
+ return false;
325
+ }
326
+ token.invalidated = true;
327
+ this.uploadTokens.set(authorizationToken, token);
328
+ this.pruneExpiredUploadTokens();
329
+ return true;
330
+ }
331
+ /**
332
+ * Current simulator time. Equal to `Date.now() + clockOffsetMs`.
333
+ *
334
+ * @returns Unix milliseconds.
335
+ */
336
+ now() {
337
+ return Date.now() + this.clockOffsetMs;
338
+ }
339
+ pruneExpiredUploadTokens(now = this.now()) {
340
+ for (const [authorizationToken, token] of this.uploadTokens.entries()) if (now >= token.cleanupAt) this.uploadTokens.delete(authorizationToken);
341
+ }
342
+ async uploadTokenState(authorizationToken) {
343
+ const stored = this.uploadTokens.get(authorizationToken);
344
+ if (stored !== void 0) return stored;
345
+ return await this.decodeUploadAuthorizationToken(authorizationToken);
346
+ }
347
+ async encodeUploadAuthorizationToken(token) {
348
+ const payload = {
349
+ v: 1,
350
+ kind: token.kind,
351
+ fileName: token.fileName,
352
+ uploadUrl: token.uploadUrl,
353
+ namePrefix: token.namePrefix,
354
+ applicationKeyId: token.applicationKeyId,
355
+ expiresAt: token.expiresAt
356
+ };
357
+ const encoded = base64UrlEncode(require_text_codec.utf8Encoder.encode(JSON.stringify(payload)));
358
+ const signature = await this.signUploadTokenPayload(encoded);
359
+ return `${token.kind === "file" ? "sim_upload_auth" : "sim_part_auth"}_${encoded}.${signature}`;
360
+ }
361
+ async decodeUploadAuthorizationToken(authorizationToken) {
362
+ const expectedKind = authorizationToken.startsWith("sim_upload_auth_") ? "file" : authorizationToken.startsWith("sim_part_auth_") ? "part" : null;
363
+ if (expectedKind === null) return null;
364
+ const tokenParts = authorizationToken.slice(expectedKind === "file" ? 16 : 14).split(".");
365
+ if (tokenParts.length !== 2) return null;
366
+ const [payloadBytes, signature] = tokenParts;
367
+ if (!timingSafeStringEqual(signature, await this.signUploadTokenPayload(payloadBytes))) return null;
368
+ let parsed;
369
+ try {
370
+ const bytes = base64UrlDecode(payloadBytes);
371
+ if (bytes === null) return null;
372
+ parsed = JSON.parse(require_text_codec.utf8Decoder.decode(bytes));
373
+ } catch {
374
+ return null;
375
+ }
376
+ if (typeof parsed !== "object" || parsed === null) return null;
377
+ const payload = parsed;
378
+ if (payload["v"] !== 1 || payload["kind"] !== expectedKind || typeof payload["uploadUrl"] !== "string" || typeof payload["expiresAt"] !== "number" || !Number.isFinite(payload["expiresAt"]) || !(payload["namePrefix"] === null || typeof payload["namePrefix"] === "string") || !(payload["applicationKeyId"] === null || typeof payload["applicationKeyId"] === "string")) return null;
379
+ let fileName;
380
+ if (expectedKind === "part") {
381
+ if (typeof payload["fileName"] !== "string") return null;
382
+ fileName = payload["fileName"];
383
+ } else {
384
+ if (payload["fileName"] !== null) return null;
385
+ fileName = null;
386
+ }
387
+ return {
388
+ kind: expectedKind,
389
+ fileName,
390
+ uploadUrl: payload["uploadUrl"],
391
+ namePrefix: payload["namePrefix"],
392
+ applicationKeyId: payload["applicationKeyId"],
393
+ expiresAt: payload["expiresAt"],
394
+ cleanupAt: payload["expiresAt"],
395
+ invalidated: false
396
+ };
397
+ }
398
+ async signUploadTokenPayload(encodedPayload) {
399
+ return base64UrlEncode(await require_crypto.hmacSha256(UPLOAD_TOKEN_SIGNING_KEY, encodedPayload));
400
+ }
401
+ /**
402
+ * Generate a deterministic-but-realistic-looking B2 identifier of the
403
+ * shape `<prefix>_<24-hex-counter>`, which approximates the visual
404
+ * width of real B2 wire IDs. The counter is monotonic per instance
405
+ * so test fixtures are deterministic across runs of the same
406
+ * simulator.
407
+ *
408
+ * @param prefix - B2-style prefix (`'b2_bucket'`, `'4_z'`, etc.).
409
+ *
410
+ * @returns A simulator-issued identifier that looks like a B2 wire ID.
411
+ */
412
+ genId(prefix) {
413
+ return `${prefix}_${(this.nextId++).toString(16).padStart(24, "0")}`;
414
+ }
415
+ /**
416
+ * Return a strictly-increasing upload timestamp, even when multiple
417
+ * writes land in the same `Date.now()` millisecond. Tests rely on
418
+ * `uploadTimestamp` ordering to discriminate file versions; ties from
419
+ * coarse system-clock resolution would otherwise make version
420
+ * selection nondeterministic.
421
+ *
422
+ * @returns Unix milliseconds.
423
+ */
424
+ monotonicTimestamp() {
425
+ const now = Date.now();
426
+ if (now <= this.lastTimestamp) this.lastTimestamp += 1;
427
+ else this.lastTimestamp = now;
428
+ return this.lastTimestamp;
429
+ }
430
+ /**
431
+ * Wait for every fire-and-forget hook (`onWebhookDeliver`,
432
+ * `onReplicate`) currently in flight to settle. Use in tests after
433
+ * an upload/copy/finish-large-file call to deterministically observe
434
+ * hook side effects without microtask-flush guesswork.
435
+ *
436
+ * @returns A promise that resolves once every pending hook callback
437
+ * has either resolved or rejected.
438
+ */
439
+ async flushHooks() {
440
+ while (this.pendingHooks.size > 0) {
441
+ const snapshot = [...this.pendingHooks];
442
+ await Promise.allSettled(snapshot);
443
+ }
444
+ }
445
+ /**
446
+ * Authorize a request against the strict-auth bookkeeping. Returns
447
+ * a `SimulatorJsonResponse` error on failure or `null` on success.
448
+ * Only consulted when `strictAuth: true`.
449
+ *
450
+ * @param authToken - The `Authorization` header value from the request.
451
+ * @param endpoint - B2 endpoint name being invoked.
452
+ * @param scope - Optional effective bucket and file-name scope derived from the request.
453
+ *
454
+ * @returns `null` when the request is authorized, otherwise a 401/403 response.
455
+ */
456
+ authorizeRequest(authToken, endpoint, scope) {
457
+ if (require_capabilities.missingCapabilitiesFor(endpoint, []).length === 0 && endpoint === "b2_authorize_account") return null;
458
+ if (authToken === void 0 || authToken === "") return this.error(401, "bad_auth_token", "missing Authorization header");
459
+ const token = this.issuedTokens.get(authToken);
460
+ if (!token) return this.error(401, "bad_auth_token", "unknown auth token");
461
+ if (this.now() > token.expiresAt) return this.error(401, "expired_auth_token", "auth token has expired; reauthorize");
462
+ const missing = require_capabilities.missingCapabilitiesFor(endpoint, token.capabilities);
463
+ if (missing.length > 0) return this.error(403, "unauthorized", `application key lacks required capabilities: ${missing.join(", ")}`);
464
+ if (token.bucketIds !== null) {
465
+ 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`);
466
+ 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}`);
467
+ }
468
+ if (token.namePrefix !== null) for (const fileName of scope?.fileNames ?? []) {
469
+ if (fileName.startsWith(token.namePrefix)) continue;
470
+ return this.error(403, "unauthorized", `application key is scoped to prefix "${token.namePrefix}"; "${fileName}" is outside scope`);
471
+ }
472
+ return null;
473
+ }
474
+ requestScope(endpoint, body) {
475
+ const directBucketId = requestStringField(body, "bucketId");
476
+ const directFileName = requestStringField(body, "fileName");
477
+ switch (endpoint) {
478
+ case "b2_create_bucket": return {
479
+ bucketIds: [],
480
+ requiresBucketScope: true
481
+ };
482
+ case "b2_list_buckets": return this.listBucketsScope(body, directBucketId);
483
+ case "b2_list_file_names":
484
+ case "b2_list_file_versions": return {
485
+ bucketIds: directBucketId === void 0 ? [] : [directBucketId],
486
+ fileNames: [requestStringField(body, "prefix") ?? ""],
487
+ requiresBucketScope: true
488
+ };
489
+ case "b2_get_file_info": return this.fileIdScope(requestStringField(body, "fileId")) ?? {
490
+ bucketIds: [],
491
+ requiresBucketScope: true
492
+ };
493
+ case "b2_delete_file_version":
494
+ case "b2_update_file_retention":
495
+ case "b2_update_file_legal_hold": return this.fileVersionScope(body, directFileName);
496
+ case "b2_get_upload_part_url":
497
+ case "b2_finish_large_file":
498
+ case "b2_cancel_large_file":
499
+ case "b2_list_parts": return this.largeFileScope(requestStringField(body, "fileId")) ?? {
500
+ bucketIds: [],
501
+ requiresBucketScope: true
502
+ };
503
+ case "b2_copy_file": return this.copyFileScope(body, directFileName);
504
+ case "b2_copy_part": return this.copyPartScope(body);
505
+ case "b2_list_unfinished_large_files": return {
506
+ bucketIds: directBucketId === void 0 ? [] : [directBucketId],
507
+ fileNames: [requestStringField(body, "namePrefix") ?? ""],
508
+ requiresBucketScope: true
509
+ };
510
+ case "b2_get_download_authorization": return {
511
+ bucketIds: directBucketId === void 0 ? [] : [directBucketId],
512
+ fileNames: [requestStringField(body, "fileNamePrefix") ?? ""],
513
+ requiresBucketScope: true
514
+ };
515
+ case "b2_get_bucket_notification_rules": return this.notificationRulesScope(body, directBucketId, false);
516
+ case "b2_set_bucket_notification_rules": return this.notificationRulesScope(body, directBucketId, true);
517
+ default: return this.defaultRequestScope(directBucketId, directFileName);
518
+ }
519
+ }
520
+ listBucketsScope(body, directBucketId) {
521
+ if (directBucketId !== void 0) return {
522
+ bucketIds: [directBucketId],
523
+ requiresBucketScope: true
524
+ };
525
+ const bucketName = requestStringField(body, "bucketName");
526
+ if (bucketName !== void 0) {
527
+ const bucket = [...this.buckets.values()].find((b) => b.info.bucketName === bucketName);
528
+ return {
529
+ bucketIds: bucket === void 0 ? [] : [bucket.info.bucketId],
530
+ requiresBucketScope: true
531
+ };
532
+ }
533
+ return {
534
+ bucketIds: [],
535
+ requiresBucketScope: true
536
+ };
537
+ }
538
+ fileVersionScope(body, directFileName) {
539
+ const fileScope = this.fileIdScope(requestStringField(body, "fileId"));
540
+ if (fileScope !== void 0) return fileScope;
541
+ return {
542
+ bucketIds: [],
543
+ ...directFileName !== void 0 ? { fileNames: [directFileName] } : {},
544
+ requiresBucketScope: true
545
+ };
546
+ }
547
+ copyFileScope(body, directFileName) {
548
+ const sourceScope = this.fileIdScope(requestStringField(body, "sourceFileId"));
549
+ const destinationBucketId = requestStringField(body, "destinationBucketId");
550
+ const bucketIds = [...sourceScope?.bucketIds ?? [], ...destinationBucketId !== void 0 ? [destinationBucketId] : []];
551
+ const scopedFileNames = fileNames(...sourceScope?.fileNames ?? [], directFileName);
552
+ return {
553
+ bucketIds,
554
+ ...scopedFileNames !== void 0 ? { fileNames: scopedFileNames } : {},
555
+ requiresBucketScope: true
556
+ };
557
+ }
558
+ copyPartScope(body) {
559
+ const sourceScope = this.fileIdScope(requestStringField(body, "sourceFileId"));
560
+ const largeScope = this.largeFileScope(requestStringField(body, "largeFileId"));
561
+ const scopedFileNames = fileNames(...sourceScope?.fileNames ?? [], ...largeScope?.fileNames ?? []);
562
+ return {
563
+ bucketIds: [...sourceScope?.bucketIds ?? [], ...largeScope?.bucketIds ?? []],
564
+ ...scopedFileNames !== void 0 ? { fileNames: scopedFileNames } : {},
565
+ requiresBucketScope: true
566
+ };
567
+ }
568
+ notificationRulesScope(body, directBucketId, includeRequestRules) {
569
+ const existingPrefixes = directBucketId === void 0 ? [] : storedNotificationRulePrefixes(this.notificationRules.get(directBucketId));
570
+ const requestPrefixes = includeRequestRules ? notificationRulePrefixes(body) ?? [] : [];
571
+ const prefixes = [...existingPrefixes, ...requestPrefixes];
572
+ return {
573
+ bucketIds: directBucketId === void 0 ? [] : [directBucketId],
574
+ ...prefixes.length > 0 ? { fileNames: prefixes } : {},
575
+ requiresBucketScope: true
576
+ };
577
+ }
578
+ defaultRequestScope(directBucketId, directFileName) {
579
+ if (directBucketId !== void 0) return {
580
+ bucketIds: [directBucketId],
581
+ ...directFileName !== void 0 ? { fileNames: [directFileName] } : {},
582
+ requiresBucketScope: true
583
+ };
584
+ return directFileName !== void 0 ? {
585
+ bucketIds: [],
586
+ fileNames: [directFileName],
587
+ requiresBucketScope: false
588
+ } : void 0;
589
+ }
590
+ fileIdScope(fileId) {
591
+ if (fileId === void 0) return void 0;
592
+ const found = this.findFile(fileId);
593
+ if (found === null) return void 0;
594
+ return {
595
+ bucketIds: [found.bucketId],
596
+ fileNames: [found.stored.fileVersion.fileName],
597
+ requiresBucketScope: true
598
+ };
599
+ }
600
+ largeFileScope(fileId) {
601
+ if (fileId === void 0) return void 0;
602
+ const large = this.largeFiles.get(fileId);
603
+ if (large === void 0) return void 0;
604
+ return {
605
+ bucketIds: [large.bucketId],
606
+ fileNames: [large.fileName],
607
+ requiresBucketScope: true
608
+ };
609
+ }
610
+ async issueUploadAuthorization(options) {
611
+ const now = this.now();
612
+ this.pruneExpiredUploadTokens(now);
613
+ const endpoint = options.kind === "file" ? "b2_upload_file" : "b2_upload_part";
614
+ const idParam = options.kind === "file" ? "bucketId" : "fileId";
615
+ const scopedId = options.kind === "file" ? options.bucketId : options.fileId;
616
+ const uploadId = this.genId(options.kind === "file" ? "upload_file" : "upload_part");
617
+ const uploadUrl = new URL(`http://localhost:0/b2api/v3/${endpoint}`);
618
+ uploadUrl.searchParams.set(idParam, scopedId);
619
+ uploadUrl.searchParams.set("uploadId", uploadId);
620
+ const sourceToken = options.sourceAuthToken === void 0 ? void 0 : this.issuedTokens.get(options.sourceAuthToken);
621
+ const token = {
622
+ kind: options.kind,
623
+ fileName: options.kind === "part" ? options.fileName : null,
624
+ uploadUrl: uploadUrl.toString(),
625
+ namePrefix: this.strictAuth ? sourceToken?.namePrefix ?? null : null,
626
+ applicationKeyId: sourceToken?.applicationKeyId ?? null,
627
+ expiresAt: now + this.authTokenTtlMs,
628
+ cleanupAt: now + this.authTokenTtlMs,
629
+ invalidated: false
630
+ };
631
+ const authorizationToken = await this.encodeUploadAuthorizationToken(token);
632
+ this.uploadTokens.set(authorizationToken, token);
633
+ return {
634
+ uploadUrl: uploadUrl.toString(),
635
+ authorizationToken
636
+ };
637
+ }
638
+ async validateUploadAuthorization(kind, uploadUrl, authToken, fileName) {
639
+ if (authToken === void 0 || authToken === "") return this.error(401, "bad_auth_token", "missing upload Authorization header");
640
+ const token = await this.uploadTokenState(authToken);
641
+ if (token === null) {
642
+ this.pruneExpiredUploadTokens();
643
+ return this.error(401, "bad_auth_token", "unknown upload authorization token");
644
+ }
645
+ const now = this.now();
646
+ if (now >= token.expiresAt) {
647
+ if (now >= token.cleanupAt) this.uploadTokens.delete(authToken);
648
+ else this.uploadTokens.set(authToken, token);
649
+ this.pruneExpiredUploadTokens(now);
650
+ return this.error(401, "expired_auth_token", "upload authorization token has expired; get a new upload URL");
651
+ }
652
+ if (token.invalidated) {
653
+ this.uploadTokens.set(authToken, token);
654
+ this.pruneExpiredUploadTokens(now);
655
+ return this.error(401, "bad_auth_token", "upload authorization token has been invalidated");
656
+ }
657
+ if (token.kind !== kind) {
658
+ this.pruneExpiredUploadTokens(now);
659
+ return this.error(401, "bad_auth_token", "upload authorization token type mismatch");
660
+ }
661
+ this.pruneExpiredUploadTokens(now);
662
+ if (!this.uploadUrlMatches(token, uploadUrl)) return this.error(401, "bad_auth_token", "upload authorization token is not valid for this upload URL");
663
+ const scopedName = kind === "file" ? fileName : token.fileName ?? void 0;
664
+ if (token.namePrefix !== null && scopedName !== void 0) {
665
+ if (!scopedName.startsWith(token.namePrefix)) return this.error(403, "unauthorized", `application key is scoped to prefix "${token.namePrefix}"; "${scopedName}" is outside scope`);
666
+ }
667
+ return null;
668
+ }
669
+ uploadUrlMatches(token, uploadUrl) {
670
+ let expected;
671
+ let actual;
672
+ try {
673
+ expected = new URL(token.uploadUrl);
674
+ actual = new URL(uploadUrl);
675
+ } catch {
676
+ return false;
677
+ }
678
+ if (actual.origin !== expected.origin || actual.pathname !== expected.pathname) return false;
679
+ const scopeParam = token.kind === "file" ? "bucketId" : "fileId";
680
+ return actual.searchParams.get(scopeParam) === expected.searchParams.get(scopeParam) && actual.searchParams.get("uploadId") === expected.searchParams.get("uploadId");
681
+ }
682
+ allowedBuckets(bucketIds) {
683
+ if (bucketIds === void 0 || bucketIds === null) return null;
684
+ return bucketIds.map((id) => ({
685
+ id: require_ids.bucketId(id),
686
+ name: this.buckets.get(id)?.info.bucketName ?? null
687
+ }));
688
+ }
689
+ /**
690
+ * Look up the application key matching the `Authorization` header on
691
+ * an `authorize_account` request. The header is in the form
692
+ * `Basic base64(applicationKeyId:applicationKey)`.
693
+ *
694
+ * Returns `null` for the implicit master credential (anything that
695
+ * does not match a key minted via `b2_create_key`); in that case
696
+ * `authorize` grants the full master capability set.
697
+ *
698
+ * @param authzHeader - Raw HTTP `Authorization` header value.
699
+ *
700
+ * @returns The matching key's grant scope, or `null` for the master.
701
+ */
702
+ findKeyForAuthHeader(authzHeader) {
703
+ if (!authzHeader?.startsWith("Basic ")) return null;
704
+ const decoded = (() => {
705
+ try {
706
+ return atob(authzHeader.slice(6));
707
+ } catch {
708
+ return null;
709
+ }
710
+ })();
711
+ if (decoded === null) return null;
712
+ const idx = decoded.indexOf(":");
713
+ if (idx === -1) return null;
714
+ const applicationKeyId = decoded.slice(0, idx);
715
+ const applicationKey = decoded.slice(idx + 1);
716
+ const stored = this.keys.get(applicationKeyId);
717
+ if (!stored || stored.applicationKey !== applicationKey) return null;
718
+ return {
719
+ capabilities: stored.capabilities,
720
+ bucketIds: stored.bucketIds,
721
+ namePrefix: stored.namePrefix,
722
+ applicationKeyId
723
+ };
724
+ }
725
+ /**
726
+ * Creates an {@link HttpTransport} that routes requests to this simulator.
727
+ * @returns A transport instance backed by this in-memory simulator.
728
+ */
729
+ transport() {
730
+ return new SimulatorTransport(this);
731
+ }
732
+ /**
733
+ * Register a synthetic failure to inject on requests whose URL contains
734
+ * `spec.on`. Use this to exercise retry / backoff / error-handling
735
+ * paths in tests without hand-rolling a wrapping `HttpTransport`. The
736
+ * fault is consumed in registration order on each matched request;
737
+ * once its `count` budget is exhausted it auto-retires.
738
+ *
739
+ * Faults are checked BEFORE the simulator's real handlers run, so a
740
+ * matched request never touches in-memory state — failed uploads
741
+ * don't create partial parts, failed deletes don't remove anything.
742
+ *
743
+ * @param spec - The failure to inject. See {@link FaultSpec}.
744
+ *
745
+ * @returns A handle whose `clear()` method removes this specific
746
+ * fault registration (other faults remain in effect).
747
+ *
748
+ * @example
749
+ * ```ts
750
+ * // Fail the next 2 b2_upload_part calls with 503, then succeed.
751
+ * sim.injectFailure({ on: 'b2_upload_part', status: 503, count: 2 })
752
+ *
753
+ * // Fail every b2_authorize_account with 401 + Retry-After: 5.
754
+ * const handle = sim.injectFailure({
755
+ * on: 'b2_authorize_account',
756
+ * status: 401,
757
+ * code: 'expired_auth_token',
758
+ * retryAfter: 5,
759
+ * })
760
+ * // ... later
761
+ * handle.clear()
762
+ * ```
763
+ */
764
+ injectFailure(spec) {
765
+ const id = this.nextFaultId++;
766
+ const fault = {
767
+ id,
768
+ spec,
769
+ remainingSkip: spec.skip ?? 0,
770
+ remainingCount: spec.count ?? Number.POSITIVE_INFINITY
771
+ };
772
+ this.faults.push(fault);
773
+ return { clear: () => {
774
+ const idx = this.faults.findIndex((f) => f.id === id);
775
+ if (idx !== -1) this.faults.splice(idx, 1);
776
+ } };
777
+ }
778
+ /**
779
+ * Remove every registered fault. Equivalent to calling `.clear()` on
780
+ * every handle returned by {@link injectFailure}, plus a defensive
781
+ * reset for tests that re-use a simulator across cases.
782
+ */
783
+ clearFaults() {
784
+ this.faults.length = 0;
785
+ }
786
+ /**
787
+ * Internal: checks the registered faults for a match on the given URL
788
+ * and consumes one if it should fire. Called from
789
+ * {@link SimulatorTransport.send} before any real handler runs.
790
+ *
791
+ * @param url - The request URL to match against each fault's `on`
792
+ * substring.
793
+ *
794
+ * @returns The fault to apply, or `null` if no fault should fire.
795
+ *
796
+ * @internal
797
+ */
798
+ consumeMatchingFault(url) {
799
+ for (let i = 0; i < this.faults.length; i++) {
800
+ const fault = this.faults[i];
801
+ if (!url.includes(fault.spec.on)) continue;
802
+ if (fault.remainingSkip > 0) {
803
+ fault.remainingSkip -= 1;
804
+ continue;
805
+ }
806
+ if (fault.remainingCount <= 0) continue;
807
+ fault.remainingCount -= 1;
808
+ if (fault.remainingCount <= 0) this.faults.splice(i, 1);
809
+ return fault.spec;
810
+ }
811
+ return null;
812
+ }
813
+ /**
814
+ * Dispatches a JSON API request to the appropriate handler.
815
+ * @param _method - The HTTP method (unused).
816
+ * @param origin - The request URL origin used for simulator-issued endpoints.
817
+ * @param path - The request URL path containing the B2 endpoint name.
818
+ * @param headers - The HTTP request headers; consulted by the
819
+ * strict-auth gate to look up the issued auth token.
820
+ * @param body - The parsed JSON request body.
821
+ *
822
+ * @returns An object with HTTP status and JSON response body.
823
+ */
824
+ async handleRequest(_method, origin, path, headers, body) {
825
+ const endpoint = path.split("/").pop() ?? "";
826
+ const apiVersion = path.match(/\/b2api\/(v\d+)\//)?.[1] ?? "v3";
827
+ if (this.strictAuth) {
828
+ const authError = this.authorizeRequest(headers["authorization"], endpoint, this.requestScope(endpoint, body));
829
+ if (authError !== null) return authError;
830
+ }
831
+ switch (endpoint) {
832
+ case "b2_authorize_account": return this.authorize(headers["authorization"], origin);
833
+ case "b2_create_bucket": return this.createBucket(body);
834
+ case "b2_list_buckets": return this.listBuckets(body);
835
+ case "b2_delete_bucket": return this.deleteBucket(body);
836
+ case "b2_update_bucket": return this.updateBucket(body);
837
+ case "b2_get_upload_url": return await this.getUploadUrl(body, headers["authorization"]);
838
+ case "b2_list_file_names": return this.listFileNames(body);
839
+ case "b2_list_file_versions": return this.listFileVersions(body);
840
+ case "b2_get_file_info": return this.getFileInfo(body);
841
+ case "b2_hide_file": return this.hideFile(body);
842
+ case "b2_delete_file_version": return this.deleteFileVersion(body);
843
+ case "b2_copy_file": return await this.copyFile(body);
844
+ case "b2_start_large_file": return this.startLargeFile(body);
845
+ case "b2_get_upload_part_url": return await this.getUploadPartUrl(body, headers["authorization"]);
846
+ case "b2_finish_large_file": return this.finishLargeFile(body);
847
+ case "b2_cancel_large_file": return this.cancelLargeFile(body);
848
+ case "b2_list_unfinished_large_files": return this.listUnfinishedLargeFiles(body);
849
+ case "b2_list_parts": return this.listParts(body);
850
+ case "b2_copy_part": return await this.copyPart(body);
851
+ case "b2_get_download_authorization": return this.getDownloadAuthorization(body);
852
+ case "b2_create_key": return this.createKey(body, apiVersion);
853
+ case "b2_list_keys": return this.listKeys(body);
854
+ case "b2_delete_key": return this.deleteKey(body);
855
+ case "b2_update_file_retention": return this.updateFileRetention(body);
856
+ case "b2_update_file_legal_hold": return this.updateFileLegalHold(body);
857
+ case "b2_get_bucket_notification_rules": return this.getBucketNotificationRules(body);
858
+ case "b2_set_bucket_notification_rules": return this.setBucketNotificationRules(body);
859
+ default: return {
860
+ status: 400,
861
+ body: {
862
+ status: 400,
863
+ code: "bad_request",
864
+ message: `Unknown endpoint: ${endpoint}`
865
+ }
866
+ };
867
+ }
868
+ }
869
+ /**
870
+ * Handles file and part upload requests (`b2_upload_file`, `b2_upload_part`).
871
+ * Dispatches to the appropriate internal handler based on the URL.
872
+ * @param url - The upload endpoint URL used to determine the upload type.
873
+ * @param headers - The HTTP headers containing file metadata and authorization.
874
+ * @param data - The raw file or part content as bytes.
875
+ *
876
+ * @returns A promise resolving to an object with HTTP status and JSON response body.
877
+ */
878
+ async handleUpload(url, headers, data) {
879
+ const endpoint = new URL(url).pathname.split("/").pop() ?? "";
880
+ const kind = endpoint === "b2_upload_file" ? "file" : endpoint === "b2_upload_part" ? "part" : null;
881
+ if (kind === null) return this.error(400, "bad_request", `Unknown upload endpoint: ${endpoint}`);
882
+ const fileName = kind === "part" || headers["x-bz-file-name"] === void 0 ? void 0 : decodeHeaderValue(headers["x-bz-file-name"]);
883
+ const authError = await this.validateUploadAuthorization(kind, url, headers["authorization"], fileName);
884
+ if (authError !== null) return authError;
885
+ if (kind === "part") return await this.handleUploadPart(url, headers, data);
886
+ return await this.handleUploadFile(url, headers, data);
887
+ }
888
+ /**
889
+ * Handles file download requests (`b2_download_file_by_id`, `/file/` by name).
890
+ * Returns the file data along with B2 response headers.
891
+ * @param path - The request URL path identifying the file to download.
892
+ * @param headers - The HTTP request headers for range or authorization.
893
+ * @param method - The HTTP method; `'HEAD'` suppresses the response body.
894
+ *
895
+ * @returns The download response containing file data and B2 headers.
896
+ */
897
+ handleDownload(path, headers, method = "GET") {
898
+ if (path.includes("b2_download_file_by_id")) {
899
+ if (this.strictAuth) {
900
+ const url = new URL(`http://localhost${path}`);
901
+ const authError = this.authorizeRequest(headers["authorization"], "b2_download_file_by_id", this.fileIdScope(url.searchParams.get("fileId") ?? void 0) ?? {
902
+ bucketIds: [],
903
+ requiresBucketScope: true
904
+ });
905
+ if (authError !== null) return this.errorAsDownload(authError);
906
+ }
907
+ const url = new URL(`http://localhost${path}`);
908
+ const fileId = url.searchParams.get("fileId") ?? "";
909
+ return this.finalizeDownload(this.downloadById(fileId, headers["range"]), url, method);
910
+ }
911
+ const fileMatch = path.match(/^([^?]+)/)?.[1]?.match(/\/file\/([^/]+)\/(.+)/);
912
+ if (fileMatch) {
913
+ const bucketName = decodeURIComponent(fileMatch[1] ?? "");
914
+ const fileName = decodeURIComponent(fileMatch[2] ?? "");
915
+ if (this.strictAuth) {
916
+ const bucket = [...this.buckets.values()].find((b) => b.info.bucketName === bucketName);
917
+ const authError = this.authorizeRequest(headers["authorization"], "b2_download_file_by_name", {
918
+ bucketIds: bucket === void 0 ? [] : [bucket.info.bucketId],
919
+ fileNames: [fileName],
920
+ requiresBucketScope: true
921
+ });
922
+ if (authError !== null) return this.errorAsDownload(authError);
923
+ }
924
+ const url = new URL(`http://localhost${path}`);
925
+ return this.finalizeDownload(this.downloadByName(bucketName, fileName, headers["range"]), url, method);
926
+ }
927
+ return {
928
+ status: 404,
929
+ headers: {},
930
+ data: null
931
+ };
932
+ }
933
+ /**
934
+ * Convert a synthetic JSON error body into a `SimulatorDownloadResponse`
935
+ * shape so the strict-auth gate on download paths can surface 401/403s
936
+ * through the same transport plumbing as a regular failed download.
937
+ *
938
+ * @param json - The JSON error from {@link authorizeRequest}.
939
+ *
940
+ * @returns A download-shaped response with the error body bytes inline.
941
+ */
942
+ errorAsDownload(json) {
943
+ return {
944
+ status: json.status,
945
+ headers: { "Content-Type": "application/json" },
946
+ data: require_text_codec.utf8Encoder.encode(JSON.stringify(json.body))
947
+ };
948
+ }
949
+ /**
950
+ * Applies HEAD-method body suppression and `b2Content*` response-header
951
+ * overrides parsed from the download URL's query string. Mirrors what the
952
+ * real B2 service does: any `b2Content*` query parameter is echoed back as
953
+ * the corresponding response header.
954
+ *
955
+ * @param response - The download response produced by {@link downloadById} or {@link downloadByName}.
956
+ * @param url - The parsed download URL (used to read `b2Content*` query params).
957
+ * @param method - The HTTP method of the originating request.
958
+ *
959
+ * @returns The response with overrides applied.
960
+ */
961
+ finalizeDownload(response, url, method) {
962
+ const overrideMap = {
963
+ b2ContentDisposition: "Content-Disposition",
964
+ b2ContentLanguage: "Content-Language",
965
+ b2ContentEncoding: "Content-Encoding",
966
+ b2ContentType: "Content-Type",
967
+ b2CacheControl: "Cache-Control",
968
+ b2Expires: "Expires"
969
+ };
970
+ const newHeaders = { ...response.headers };
971
+ for (const [param, header] of Object.entries(overrideMap)) {
972
+ const value = url.searchParams.get(param);
973
+ if (value !== null) newHeaders[header] = value;
974
+ }
975
+ const data = method === "HEAD" ? null : response.data;
976
+ return {
977
+ status: response.status,
978
+ headers: newHeaders,
979
+ data
980
+ };
981
+ }
982
+ async handleUploadFile(url, headers, data) {
983
+ const bucketId = new URL(url).searchParams.get("bucketId");
984
+ if (!bucketId) return this.error(400, "bad_request", "Missing bucketId");
985
+ const bucket = this.buckets.get(bucketId);
986
+ if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
987
+ const fileName = decodeHeaderValue(headers["x-bz-file-name"] ?? "");
988
+ const contentType = headers["content-type"] ?? "application/octet-stream";
989
+ const fileNameError = require_validation.validateFileName(fileName);
990
+ if (fileNameError) return this.error(400, fileNameError.code, fileNameError.message);
991
+ const fileInfo = parseFileInfoHeaders(headers);
992
+ const fileInfoError = require_validation.validateFileInfo(fileInfo);
993
+ if (fileInfoError) return this.error(400, fileInfoError.code, fileInfoError.message);
994
+ const resolved = await this.resolveUploadSha1(headers["x-bz-content-sha1"], data);
995
+ if ("status" in resolved) return resolved;
996
+ const { sha1: contentSha1, data: storedData } = resolved;
997
+ const fileVersion = this.makeFileVersion({
998
+ bucketId,
999
+ fileName,
1000
+ contentType,
1001
+ contentLength: storedData.byteLength,
1002
+ contentSha1,
1003
+ fileInfo,
1004
+ action: require_file.FileAction.Upload,
1005
+ serverSideEncryption: uploadServerSideEncryption(headers, bucket.info.defaultServerSideEncryption)
1006
+ });
1007
+ const stored = {
1008
+ fileVersion,
1009
+ data: storedData
1010
+ };
1011
+ const existing = bucket.files.get(fileName);
1012
+ if (existing) existing.push(stored);
1013
+ else bucket.files.set(fileName, [stored]);
1014
+ this.firePostUploadHooks(fileVersion, bucketId, "b2:ObjectCreated:Upload");
1015
+ return {
1016
+ status: 200,
1017
+ body: fileVersion
1018
+ };
1019
+ }
1020
+ /**
1021
+ * Resolve the `X-Bz-Content-Sha1` header into the SHA-1 to store, verifying
1022
+ * it against the uploaded bytes where B2 would. Returns the SHA-1 string to
1023
+ * persist, or a `400 bad_request` response when a verifiable hash does not
1024
+ * match the data received.
1025
+ *
1026
+ * @param header - The raw `X-Bz-Content-Sha1` header value, if any.
1027
+ * @param data - The uploaded bytes.
1028
+ *
1029
+ * @returns The SHA-1 string to store, or an error response on mismatch.
1030
+ */
1031
+ async resolveUploadSha1(header, data) {
1032
+ const value = header ?? "none";
1033
+ if (value === "none" || value === "do_not_verify") return {
1034
+ sha1: "none",
1035
+ data
1036
+ };
1037
+ if (value.startsWith("unverified:")) return {
1038
+ sha1: value.slice(11).toLowerCase(),
1039
+ data
1040
+ };
1041
+ if (value === "hex_digits_at_end") {
1042
+ if (data.byteLength < 40) return this.error(400, "bad_request", "Sha1 did not match data received");
1043
+ const body = data.subarray(0, data.byteLength - 40);
1044
+ const trailer = require_text_codec.utf8Decoder.decode(data.subarray(data.byteLength - 40)).toLowerCase();
1045
+ const actual = await require_hash.sha1Hex(body);
1046
+ if (actual !== trailer) return this.error(400, "bad_request", "Sha1 did not match data received");
1047
+ return {
1048
+ sha1: actual,
1049
+ data: body
1050
+ };
1051
+ }
1052
+ const expected = value.toLowerCase();
1053
+ if (await require_hash.sha1Hex(data) !== expected) return this.error(400, "bad_request", "Sha1 did not match data received");
1054
+ return {
1055
+ sha1: expected,
1056
+ data
1057
+ };
1058
+ }
1059
+ async handleUploadPart(url, headers, data) {
1060
+ const fileId = new URL(url).searchParams.get("fileId");
1061
+ if (!fileId) return this.error(400, "bad_request", "Missing fileId");
1062
+ const large = this.largeFiles.get(fileId);
1063
+ if (!large) return this.error(400, "bad_request", "Large file not found");
1064
+ const partNumber = Number.parseInt(headers["x-bz-part-number"] ?? "0", 10);
1065
+ const resolved = await this.resolveUploadSha1(headers["x-bz-content-sha1"], data);
1066
+ if ("status" in resolved) return resolved;
1067
+ const { sha1, data: partData } = resolved;
1068
+ large.parts.set(partNumber, {
1069
+ data: partData,
1070
+ sha1
1071
+ });
1072
+ return {
1073
+ status: 200,
1074
+ body: {
1075
+ fileId: large.fileId,
1076
+ partNumber,
1077
+ contentLength: partData.byteLength,
1078
+ contentSha1: sha1,
1079
+ serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
1080
+ uploadTimestamp: Date.now()
1081
+ }
1082
+ };
1083
+ }
1084
+ downloadById(fileId, range) {
1085
+ const found = this.findFile(fileId);
1086
+ if (found === null) return {
1087
+ status: 404,
1088
+ headers: {},
1089
+ data: null
1090
+ };
1091
+ return this.serveFile(found.stored, range);
1092
+ }
1093
+ downloadByName(bucketName, fileName, range) {
1094
+ for (const bucket of this.buckets.values()) {
1095
+ if (bucket.info.bucketName !== bucketName) continue;
1096
+ const versions = bucket.files.get(fileName);
1097
+ if (!versions || versions.length === 0) break;
1098
+ const latest = versions[versions.length - 1];
1099
+ if (!latest || latest.fileVersion.action === require_file.FileAction.Hide) return {
1100
+ status: 404,
1101
+ headers: {},
1102
+ data: null
1103
+ };
1104
+ return this.serveFile(latest, range);
1105
+ }
1106
+ return {
1107
+ status: 404,
1108
+ headers: {},
1109
+ data: null
1110
+ };
1111
+ }
1112
+ serveFile(stored, range) {
1113
+ const fullData = stored.data;
1114
+ let data = fullData;
1115
+ let status = 200;
1116
+ let contentRange = null;
1117
+ if (range) {
1118
+ const parsed = parseRangeHeader(range, fullData.byteLength);
1119
+ if (parsed.kind === "ok") {
1120
+ data = fullData.subarray(parsed.start, parsed.end + 1);
1121
+ status = 206;
1122
+ contentRange = `bytes ${parsed.start}-${parsed.end}/${fullData.byteLength}`;
1123
+ } else if (parsed.kind === "unsatisfiable") return {
1124
+ status: 416,
1125
+ headers: {
1126
+ "Content-Type": stored.fileVersion.contentType,
1127
+ "Content-Range": `bytes */${fullData.byteLength}`
1128
+ },
1129
+ data: /* @__PURE__ */ new Uint8Array(0)
1130
+ };
1131
+ }
1132
+ const fv = stored.fileVersion;
1133
+ const headers = {
1134
+ "Content-Type": fv.contentType,
1135
+ "Content-Length": String(data.byteLength),
1136
+ "X-Bz-File-Id": fv.fileId,
1137
+ "X-Bz-File-Name": require_encoding.encodeFileName(fv.fileName),
1138
+ "X-Bz-Content-Sha1": fv.contentSha1 ?? "none",
1139
+ "X-Bz-Upload-Timestamp": String(fv.uploadTimestamp)
1140
+ };
1141
+ for (const [key, value] of Object.entries(fv.fileInfo)) headers[`X-Bz-Info-${require_encoding.encodeFileName(key)}`] = require_encoding.encodeFileName(value);
1142
+ if (!("src_last_modified_millis" in fv.fileInfo)) headers["X-Bz-Info-src_last_modified_millis"] = String(fv.uploadTimestamp);
1143
+ if (contentRange !== null) headers["Content-Range"] = contentRange;
1144
+ return {
1145
+ status,
1146
+ headers,
1147
+ data
1148
+ };
1149
+ }
1150
+ authorize(authzHeader, origin = "http://localhost:0") {
1151
+ const capabilities = [
1152
+ require_auth.Capability.ListBuckets,
1153
+ require_auth.Capability.ReadBuckets,
1154
+ require_auth.Capability.WriteBuckets,
1155
+ require_auth.Capability.DeleteBuckets,
1156
+ require_auth.Capability.ListFiles,
1157
+ require_auth.Capability.ReadFiles,
1158
+ require_auth.Capability.WriteFiles,
1159
+ require_auth.Capability.DeleteFiles,
1160
+ require_auth.Capability.ListKeys,
1161
+ require_auth.Capability.WriteKeys,
1162
+ require_auth.Capability.DeleteKeys,
1163
+ require_auth.Capability.ShareFiles,
1164
+ require_auth.Capability.ReadBucketNotifications,
1165
+ require_auth.Capability.WriteBucketNotifications
1166
+ ];
1167
+ const keyForAuth = this.findKeyForAuthHeader(authzHeader);
1168
+ const allowedBuckets = this.allowedBuckets(keyForAuth?.bucketIds);
1169
+ const legacyBucketId = singleBucketId(keyForAuth?.bucketIds);
1170
+ const legacyBucketName = legacyBucketId === null ? null : this.buckets.get(legacyBucketId)?.info.bucketName ?? null;
1171
+ const tokenStr = `sim_auth_token_${this.nextId++}`;
1172
+ this.issuedTokens.set(tokenStr, {
1173
+ capabilities: keyForAuth?.capabilities ?? capabilities,
1174
+ bucketIds: keyForAuth?.bucketIds ?? null,
1175
+ namePrefix: keyForAuth?.namePrefix ?? null,
1176
+ expiresAt: this.now() + this.authTokenTtlMs,
1177
+ applicationKeyId: keyForAuth?.applicationKeyId ?? null
1178
+ });
1179
+ return {
1180
+ status: 200,
1181
+ body: {
1182
+ accountId: require_ids.accountId(this.accountId),
1183
+ authorizationToken: tokenStr,
1184
+ apiInfo: { storageApi: {
1185
+ absoluteMinimumPartSize: this.minimumPartSize,
1186
+ apiUrl: origin,
1187
+ bucketId: legacyBucketId === null ? null : require_ids.bucketId(legacyBucketId),
1188
+ bucketName: legacyBucketName,
1189
+ downloadUrl: origin,
1190
+ infoType: "storageApi",
1191
+ namePrefix: keyForAuth?.namePrefix ?? null,
1192
+ recommendedPartSize: this.recommendedPartSize,
1193
+ s3ApiUrl: origin,
1194
+ allowed: {
1195
+ capabilities: keyForAuth?.capabilities ?? capabilities,
1196
+ buckets: allowedBuckets,
1197
+ bucketId: legacyBucketId === null ? null : require_ids.bucketId(legacyBucketId),
1198
+ bucketName: legacyBucketName,
1199
+ namePrefix: keyForAuth?.namePrefix ?? null
1200
+ }
1201
+ } },
1202
+ applicationKeyExpirationTimestamp: null
1203
+ }
1204
+ };
1205
+ }
1206
+ createBucket(req) {
1207
+ const nameError = require_validation.validateBucketName(req.bucketName);
1208
+ if (nameError) return this.error(400, nameError.code, nameError.message);
1209
+ if (req.bucketInfo !== void 0) {
1210
+ const infoError = require_validation.validateBucketInfo(req.bucketInfo);
1211
+ if (infoError) return this.error(400, infoError.code, infoError.message);
1212
+ }
1213
+ 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");
1214
+ const bid = require_ids.bucketId(this.genId("b2_bucket"));
1215
+ const defaultRetention = req.defaultRetention ?? {
1216
+ mode: require_bucket.BucketRetentionMode.None,
1217
+ period: null
1218
+ };
1219
+ const info = {
1220
+ accountId: require_ids.accountId(req.accountId),
1221
+ bucketId: bid,
1222
+ bucketName: req.bucketName,
1223
+ bucketType: req.bucketType,
1224
+ bucketInfo: req.bucketInfo ?? {},
1225
+ corsRules: req.corsRules ?? [],
1226
+ defaultServerSideEncryption: req.defaultServerSideEncryption ?? { mode: require_encryption.EncryptionMode.None },
1227
+ fileLockConfiguration: {
1228
+ isClientAuthorizedToRead: true,
1229
+ value: {
1230
+ isFileLockEnabled: req.fileLockEnabled ?? false,
1231
+ defaultRetention
1232
+ }
1233
+ },
1234
+ lifecycleRules: req.lifecycleRules ?? [],
1235
+ options: [],
1236
+ revision: 1,
1237
+ defaultRetention,
1238
+ replicationConfiguration: req.replicationConfiguration ?? {
1239
+ asReplicationSource: null,
1240
+ asReplicationDestination: null
1241
+ }
1242
+ };
1243
+ this.buckets.set(bid, {
1244
+ info,
1245
+ files: /* @__PURE__ */ new Map()
1246
+ });
1247
+ return {
1248
+ status: 200,
1249
+ body: info
1250
+ };
1251
+ }
1252
+ listBuckets(req) {
1253
+ return {
1254
+ status: 200,
1255
+ 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)) }
1256
+ };
1257
+ }
1258
+ deleteBucket(req) {
1259
+ const bucket = this.buckets.get(req.bucketId);
1260
+ if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
1261
+ this.buckets.delete(req.bucketId);
1262
+ return {
1263
+ status: 200,
1264
+ body: bucket.info
1265
+ };
1266
+ }
1267
+ updateBucket(req) {
1268
+ const bucket = this.buckets.get(req["bucketId"]);
1269
+ if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
1270
+ if (req["bucketInfo"] !== void 0) {
1271
+ const infoError = require_validation.validateBucketInfo(req["bucketInfo"]);
1272
+ if (infoError) return this.error(400, infoError.code, infoError.message);
1273
+ }
1274
+ const updated = {
1275
+ ...bucket.info,
1276
+ ...req["bucketType"] !== void 0 ? { bucketType: req["bucketType"] } : {},
1277
+ ...req["bucketInfo"] !== void 0 ? { bucketInfo: req["bucketInfo"] } : {},
1278
+ ...req["lifecycleRules"] !== void 0 ? { lifecycleRules: req["lifecycleRules"] } : {},
1279
+ ...req["corsRules"] !== void 0 ? { corsRules: req["corsRules"] } : {},
1280
+ ...req["replicationConfiguration"] !== void 0 ? { replicationConfiguration: req["replicationConfiguration"] } : {},
1281
+ ...req["defaultRetention"] !== void 0 ? {
1282
+ defaultRetention: req["defaultRetention"],
1283
+ fileLockConfiguration: {
1284
+ isClientAuthorizedToRead: true,
1285
+ value: {
1286
+ isFileLockEnabled: bucket.info.fileLockConfiguration.value?.isFileLockEnabled ?? false,
1287
+ defaultRetention: req["defaultRetention"]
1288
+ }
1289
+ }
1290
+ } : {},
1291
+ ...req["defaultServerSideEncryption"] !== void 0 ? { defaultServerSideEncryption: req["defaultServerSideEncryption"] } : {},
1292
+ revision: bucket.info.revision + 1
1293
+ };
1294
+ this.buckets.set(req["bucketId"], {
1295
+ info: updated,
1296
+ files: bucket.files
1297
+ });
1298
+ return {
1299
+ status: 200,
1300
+ body: updated
1301
+ };
1302
+ }
1303
+ async getUploadUrl(req, authToken) {
1304
+ if (!this.buckets.get(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
1305
+ const uploadAuth = await this.issueUploadAuthorization({
1306
+ kind: "file",
1307
+ sourceAuthToken: authToken,
1308
+ bucketId: req.bucketId
1309
+ });
1310
+ return {
1311
+ status: 200,
1312
+ body: {
1313
+ bucketId: req.bucketId,
1314
+ uploadUrl: uploadAuth.uploadUrl,
1315
+ authorizationToken: uploadAuth.authorizationToken
1316
+ }
1317
+ };
1318
+ }
1319
+ listFileNames(req) {
1320
+ const bucket = this.buckets.get(req.bucketId);
1321
+ if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
1322
+ const countError = require_validation.validateMaxCount(req.maxFileCount, "b2_list_file_names");
1323
+ if (countError) return this.error(400, countError.code, countError.message);
1324
+ const max = req.maxFileCount ?? 1e3;
1325
+ const prefix = req.prefix ?? "";
1326
+ 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));
1327
+ if (req.startFileName) {
1328
+ const start = req.startFileName;
1329
+ allFiles = allFiles.filter((f) => f.fileName >= start);
1330
+ }
1331
+ return {
1332
+ status: 200,
1333
+ body: {
1334
+ files: allFiles.slice(0, max),
1335
+ nextFileName: allFiles.length > max ? allFiles[max]?.fileName ?? null : null
1336
+ }
1337
+ };
1338
+ }
1339
+ listFileVersions(req) {
1340
+ const bucket = this.buckets.get(req.bucketId);
1341
+ if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
1342
+ const countError = require_validation.validateMaxCount(req.maxFileCount, "b2_list_file_versions");
1343
+ if (countError) return this.error(400, countError.code, countError.message);
1344
+ const max = req.maxFileCount ?? 1e3;
1345
+ const prefix = req.prefix ?? "";
1346
+ const allVersions = [...bucket.files.entries()].filter(([name]) => name.startsWith(prefix)).flatMap(([_, versions]) => versions.map((v) => v.fileVersion)).sort((a, b) => {
1347
+ const nameCmp = compareB2FileNames(a.fileName, b.fileName);
1348
+ if (nameCmp !== 0) return nameCmp;
1349
+ return b.uploadTimestamp - a.uploadTimestamp;
1350
+ });
1351
+ let startIdx = 0;
1352
+ if (req.startFileName !== void 0) {
1353
+ const startName = req.startFileName;
1354
+ const startId = req.startFileId;
1355
+ const nameIdx = allVersions.findIndex((f) => f.fileName >= startName);
1356
+ if (nameIdx === -1) startIdx = allVersions.length;
1357
+ else if (startId !== void 0) {
1358
+ const exactIdx = allVersions.findIndex((f, i) => i >= nameIdx && f.fileId === startId);
1359
+ startIdx = exactIdx !== -1 ? exactIdx : nameIdx;
1360
+ } else startIdx = nameIdx;
1361
+ }
1362
+ const sliced = allVersions.slice(startIdx, startIdx + max);
1363
+ const hasMore = startIdx + max < allVersions.length;
1364
+ return {
1365
+ status: 200,
1366
+ body: {
1367
+ files: sliced,
1368
+ nextFileName: hasMore ? allVersions[startIdx + max]?.fileName ?? null : null,
1369
+ nextFileId: hasMore ? allVersions[startIdx + max]?.fileId ?? null : null
1370
+ }
1371
+ };
1372
+ }
1373
+ getFileInfo(req) {
1374
+ const found = this.findFile(req.fileId);
1375
+ if (found === null) return this.error(404, "file_not_present", "File not found");
1376
+ return {
1377
+ status: 200,
1378
+ body: found.stored.fileVersion
1379
+ };
1380
+ }
1381
+ hideFile(req) {
1382
+ const bucket = this.buckets.get(req.bucketId);
1383
+ if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
1384
+ const nameError = require_validation.validateFileName(req.fileName);
1385
+ if (nameError) return this.error(400, nameError.code, nameError.message);
1386
+ const fileVersion = this.makeFileVersion({
1387
+ bucketId: req.bucketId,
1388
+ fileName: req.fileName,
1389
+ contentType: "application/octet-stream",
1390
+ contentLength: 0,
1391
+ contentSha1: "none",
1392
+ action: require_file.FileAction.Hide
1393
+ });
1394
+ const existing = bucket.files.get(req.fileName);
1395
+ const stored = {
1396
+ fileVersion,
1397
+ data: /* @__PURE__ */ new Uint8Array(0)
1398
+ };
1399
+ if (existing) existing.push(stored);
1400
+ else bucket.files.set(req.fileName, [stored]);
1401
+ return {
1402
+ status: 200,
1403
+ body: fileVersion
1404
+ };
1405
+ }
1406
+ deleteFileVersion(req) {
1407
+ const found = this.findFile(req.fileId);
1408
+ if (found === null || found.stored.fileVersion.fileName !== req.fileName) return this.error(400, "file_not_present", "File version not found");
1409
+ const fv = found.stored.fileVersion;
1410
+ const retention = fv.fileRetention?.value;
1411
+ const legalHold = fv.legalHold?.value;
1412
+ const now = Date.now();
1413
+ if (legalHold === "on") return this.error(400, "file_lock_legal_hold_protected", "File is on legal hold and cannot be deleted");
1414
+ 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()}`);
1415
+ 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");
1416
+ found.versions.splice(found.index, 1);
1417
+ if (found.versions.length === 0) found.bucket.files.delete(req.fileName);
1418
+ return {
1419
+ status: 200,
1420
+ body: {
1421
+ fileId: req.fileId,
1422
+ fileName: req.fileName
1423
+ }
1424
+ };
1425
+ }
1426
+ async copyFile(req) {
1427
+ const nameError = require_validation.validateFileName(req.fileName);
1428
+ if (nameError) return this.error(400, nameError.code, nameError.message);
1429
+ const found = this.findFile(req.sourceFileId);
1430
+ if (found === null) return this.error(404, "file_not_present", "Source file not found");
1431
+ const sourceStored = found.stored;
1432
+ const destBucketId = req.destinationBucketId ?? found.bucketId;
1433
+ const destBucket = this.buckets.get(destBucketId);
1434
+ if (!destBucket) return this.error(400, "bad_bucket_id", "Destination bucket not found");
1435
+ let data = sourceStored.data;
1436
+ let contentSha1 = sourceStored.fileVersion.contentSha1 ?? "none";
1437
+ if (req.range !== void 0) {
1438
+ const parsed = parseRangeHeader(req.range, sourceStored.data.byteLength);
1439
+ if (parsed.kind === "malformed") return this.error(400, "bad_request", `Malformed copy range: ${req.range}`);
1440
+ if (parsed.kind === "unsatisfiable") return this.error(416, "range_not_satisfiable", `Unsatisfiable copy range: ${req.range}`);
1441
+ data = sourceStored.data.subarray(parsed.start, parsed.end + 1);
1442
+ contentSha1 = await require_hash.sha1Hex(data);
1443
+ }
1444
+ const directive = req.metadataDirective;
1445
+ if (directive !== void 0 && directive !== "COPY" && directive !== "REPLACE") return this.error(400, "bad_request", `Invalid metadataDirective: ${directive}`);
1446
+ let contentType;
1447
+ let fileInfo;
1448
+ if (directive === "REPLACE") {
1449
+ if (req.contentType === void 0) return this.error(400, "bad_request", "contentType is required when metadataDirective is REPLACE");
1450
+ const replaceFileInfo = req.fileInfo ?? {};
1451
+ const fileInfoError = require_validation.validateFileInfo(replaceFileInfo);
1452
+ if (fileInfoError) return this.error(400, fileInfoError.code, fileInfoError.message);
1453
+ contentType = req.contentType;
1454
+ fileInfo = replaceFileInfo;
1455
+ } else {
1456
+ if (req.contentType !== void 0 || req.fileInfo !== void 0) return this.error(400, "bad_request", "contentType and fileInfo may only be set when metadataDirective is REPLACE");
1457
+ contentType = sourceStored.fileVersion.contentType;
1458
+ fileInfo = sourceStored.fileVersion.fileInfo;
1459
+ }
1460
+ const fileVersion = this.makeFileVersion({
1461
+ bucketId: destBucketId,
1462
+ fileName: req.fileName,
1463
+ contentType,
1464
+ contentLength: data.byteLength,
1465
+ contentSha1,
1466
+ fileInfo,
1467
+ action: require_file.FileAction.Copy
1468
+ });
1469
+ const copied = {
1470
+ fileVersion,
1471
+ data: new Uint8Array(data)
1472
+ };
1473
+ const existing = destBucket.files.get(req.fileName);
1474
+ if (existing) existing.push(copied);
1475
+ else destBucket.files.set(req.fileName, [copied]);
1476
+ this.firePostUploadHooks(fileVersion, destBucketId, "b2:ObjectCreated:Copy");
1477
+ return {
1478
+ status: 200,
1479
+ body: fileVersion
1480
+ };
1481
+ }
1482
+ startLargeFile(req) {
1483
+ const bucket = this.buckets.get(req.bucketId);
1484
+ if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
1485
+ const nameError = require_validation.validateFileName(req.fileName);
1486
+ if (nameError) return this.error(400, nameError.code, nameError.message);
1487
+ if (req.fileInfo !== void 0) {
1488
+ const infoError = require_validation.validateFileInfo(req.fileInfo);
1489
+ if (infoError) return this.error(400, infoError.code, infoError.message);
1490
+ }
1491
+ const fid = this.genId("4_z");
1492
+ const uploadTimestamp = this.monotonicTimestamp();
1493
+ const large = {
1494
+ fileId: fid,
1495
+ bucketId: req.bucketId,
1496
+ fileName: req.fileName,
1497
+ contentType: req.contentType,
1498
+ fileInfo: req.fileInfo ?? {},
1499
+ fileRetention: req.fileRetention ?? defaultFileRetention(bucket.info.defaultRetention, uploadTimestamp),
1500
+ legalHold: req.legalHold ?? null,
1501
+ serverSideEncryption: req.serverSideEncryption ?? bucket.info.defaultServerSideEncryption,
1502
+ uploadTimestamp,
1503
+ parts: /* @__PURE__ */ new Map()
1504
+ };
1505
+ this.largeFiles.set(fid, large);
1506
+ return {
1507
+ status: 200,
1508
+ body: {
1509
+ fileId: fid,
1510
+ fileName: req.fileName,
1511
+ accountId: this.accountId,
1512
+ bucketId: req.bucketId,
1513
+ contentType: req.contentType,
1514
+ fileInfo: large.fileInfo,
1515
+ action: require_file.FileAction.Start,
1516
+ contentLength: 0,
1517
+ contentSha1: "none",
1518
+ contentMd5: null,
1519
+ fileRetention: {
1520
+ isClientAuthorizedToRead: true,
1521
+ value: large.fileRetention
1522
+ },
1523
+ legalHold: {
1524
+ isClientAuthorizedToRead: true,
1525
+ value: large.legalHold
1526
+ },
1527
+ serverSideEncryption: publicServerSideEncryption(large.serverSideEncryption),
1528
+ uploadTimestamp: large.uploadTimestamp
1529
+ }
1530
+ };
1531
+ }
1532
+ async getUploadPartUrl(req, authToken) {
1533
+ const large = this.largeFiles.get(req.fileId);
1534
+ if (large === void 0) return this.error(400, "bad_request", "Large file not found");
1535
+ const uploadAuth = await this.issueUploadAuthorization({
1536
+ kind: "part",
1537
+ sourceAuthToken: authToken,
1538
+ fileId: req.fileId,
1539
+ fileName: large.fileName
1540
+ });
1541
+ return {
1542
+ status: 200,
1543
+ body: {
1544
+ fileId: req.fileId,
1545
+ uploadUrl: uploadAuth.uploadUrl,
1546
+ authorizationToken: uploadAuth.authorizationToken
1547
+ }
1548
+ };
1549
+ }
1550
+ finishLargeFile(req) {
1551
+ const large = this.largeFiles.get(req.fileId);
1552
+ if (!large) return this.error(400, "bad_request", "Large file not found");
1553
+ const bucket = this.buckets.get(large.bucketId);
1554
+ if (!bucket) return this.error(400, "bad_bucket_id", "Bucket not found");
1555
+ const sortedParts = [...large.parts.entries()].sort((a, b) => a[0] - b[0]);
1556
+ if (sortedParts.length > 1e4) return this.error(400, "bad_request", `multipart upload has ${sortedParts.length} parts; B2 caps at 10000`);
1557
+ 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`);
1558
+ if (req.partSha1Array.length !== sortedParts.length) return this.error(400, "bad_request", `partSha1Array has ${req.partSha1Array.length} entries but ${sortedParts.length} parts were uploaded`);
1559
+ for (let i = 0; i < sortedParts.length; i++) {
1560
+ const [partNumber, part] = sortedParts[i];
1561
+ if (req.partSha1Array[i]?.toLowerCase() !== part.sha1) return this.error(400, "bad_request", `part ${partNumber} SHA-1 does not match the uploaded part`);
1562
+ }
1563
+ for (let i = 0; i < sortedParts.length - 1; i++) {
1564
+ const [partNumber, part] = sortedParts[i];
1565
+ 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
+ }
1567
+ let totalSize = 0;
1568
+ for (const [_, part] of sortedParts) totalSize += part.data.byteLength;
1569
+ const combined = new Uint8Array(totalSize);
1570
+ let offset = 0;
1571
+ for (const [_, part] of sortedParts) {
1572
+ combined.set(part.data, offset);
1573
+ offset += part.data.byteLength;
1574
+ }
1575
+ const fileVersion = this.makeFileVersion({
1576
+ bucketId: large.bucketId,
1577
+ fileName: large.fileName,
1578
+ contentType: large.contentType,
1579
+ contentLength: totalSize,
1580
+ contentSha1: "none",
1581
+ fileInfo: large.fileInfo,
1582
+ action: require_file.FileAction.Upload,
1583
+ fileRetention: large.fileRetention,
1584
+ legalHold: large.legalHold,
1585
+ serverSideEncryption: large.serverSideEncryption
1586
+ });
1587
+ const stored = {
1588
+ fileVersion,
1589
+ data: combined
1590
+ };
1591
+ const existing = bucket.files.get(large.fileName);
1592
+ if (existing) existing.push(stored);
1593
+ else bucket.files.set(large.fileName, [stored]);
1594
+ this.largeFiles.delete(req.fileId);
1595
+ this.firePostUploadHooks(fileVersion, large.bucketId, "b2:ObjectCreated:Upload");
1596
+ return {
1597
+ status: 200,
1598
+ body: fileVersion
1599
+ };
1600
+ }
1601
+ cancelLargeFile(req) {
1602
+ const large = this.largeFiles.get(req.fileId);
1603
+ if (!large) return this.error(400, "bad_request", "Large file not found");
1604
+ this.largeFiles.delete(req.fileId);
1605
+ return {
1606
+ status: 200,
1607
+ body: {
1608
+ fileId: large.fileId,
1609
+ accountId: this.accountId,
1610
+ bucketId: large.bucketId,
1611
+ fileName: large.fileName
1612
+ }
1613
+ };
1614
+ }
1615
+ listUnfinishedLargeFiles(req) {
1616
+ const countError = require_validation.validateMaxCount(req.maxFileCount, "b2_list_unfinished_large_files");
1617
+ if (countError) return this.error(400, countError.code, countError.message);
1618
+ const prefix = req.namePrefix ?? "";
1619
+ const max = req.maxFileCount ?? 100;
1620
+ const candidates = [...this.largeFiles.values()].filter((f) => f.bucketId === req.bucketId).filter((f) => f.fileName.startsWith(prefix)).sort((a, b) => compareB2FileNames(a.fileName, b.fileName));
1621
+ let startIndex = 0;
1622
+ if (req.startFileId !== void 0) {
1623
+ const found = candidates.findIndex((f) => f.fileId === req.startFileId);
1624
+ startIndex = found >= 0 ? found : 0;
1625
+ }
1626
+ return {
1627
+ status: 200,
1628
+ body: {
1629
+ files: candidates.slice(startIndex, startIndex + max).map((f) => ({
1630
+ fileId: f.fileId,
1631
+ fileName: f.fileName,
1632
+ accountId: this.accountId,
1633
+ bucketId: f.bucketId,
1634
+ contentType: f.contentType,
1635
+ action: require_file.FileAction.Start,
1636
+ contentLength: 0,
1637
+ contentSha1: "none",
1638
+ contentMd5: null,
1639
+ fileInfo: f.fileInfo,
1640
+ fileRetention: {
1641
+ isClientAuthorizedToRead: true,
1642
+ value: f.fileRetention
1643
+ },
1644
+ legalHold: {
1645
+ isClientAuthorizedToRead: true,
1646
+ value: f.legalHold
1647
+ },
1648
+ serverSideEncryption: publicServerSideEncryption(f.serverSideEncryption),
1649
+ uploadTimestamp: f.uploadTimestamp
1650
+ })),
1651
+ nextFileId: startIndex + max < candidates.length ? candidates[startIndex + max]?.fileId ?? null : null
1652
+ }
1653
+ };
1654
+ }
1655
+ listParts(req) {
1656
+ const large = this.largeFiles.get(req.fileId);
1657
+ if (!large) return this.error(400, "bad_request", "Large file not found");
1658
+ const countError = require_validation.validateMaxCount(req.maxPartCount, "b2_list_parts");
1659
+ if (countError) return this.error(400, countError.code, countError.message);
1660
+ const start = req.startPartNumber ?? 1;
1661
+ const max = req.maxPartCount ?? 1e3;
1662
+ const allParts = [...large.parts.entries()].filter(([n]) => n >= start).sort((a, b) => a[0] - b[0]).map(([partNumber, part]) => ({
1663
+ fileId: req.fileId,
1664
+ partNumber,
1665
+ contentLength: part.data.byteLength,
1666
+ contentSha1: part.sha1,
1667
+ uploadTimestamp: Date.now()
1668
+ }));
1669
+ return {
1670
+ status: 200,
1671
+ body: {
1672
+ parts: allParts.slice(0, max),
1673
+ nextPartNumber: allParts.length > max ? allParts[max]?.partNumber ?? null : null
1674
+ }
1675
+ };
1676
+ }
1677
+ async copyPart(req) {
1678
+ const large = this.largeFiles.get(req.largeFileId);
1679
+ if (!large) return this.error(400, "bad_request", "Large file not found");
1680
+ const found = this.findFile(req.sourceFileId);
1681
+ if (found === null) return this.error(404, "file_not_present", "Source file not found");
1682
+ const sourceStored = found.stored;
1683
+ let partData = sourceStored.data;
1684
+ if (req.range) {
1685
+ const match = req.range.match(/bytes=(\d+)-(\d+)?/);
1686
+ if (match) {
1687
+ const rs = Number.parseInt(match[1] ?? "0", 10);
1688
+ const re = match[2] !== void 0 ? Number.parseInt(match[2], 10) : sourceStored.data.byteLength - 1;
1689
+ partData = sourceStored.data.slice(rs, re + 1);
1690
+ }
1691
+ }
1692
+ const sha1 = await require_hash.sha1Hex(partData);
1693
+ large.parts.set(req.partNumber, {
1694
+ data: new Uint8Array(partData),
1695
+ sha1
1696
+ });
1697
+ return {
1698
+ status: 200,
1699
+ body: {
1700
+ fileId: req.largeFileId,
1701
+ partNumber: req.partNumber,
1702
+ contentLength: partData.byteLength,
1703
+ contentSha1: sha1
1704
+ }
1705
+ };
1706
+ }
1707
+ getDownloadAuthorization(req) {
1708
+ if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
1709
+ return {
1710
+ status: 200,
1711
+ body: {
1712
+ bucketId: req.bucketId,
1713
+ fileNamePrefix: req.fileNamePrefix,
1714
+ authorizationToken: `sim_dl_auth_${this.genId("tok")}`
1715
+ }
1716
+ };
1717
+ }
1718
+ createKey(req, apiVersion) {
1719
+ if (apiVersion === "v4" && hasOwnField(req, "bucketId")) return this.error(400, "bad_request", "bucketId is not accepted by v4 b2_create_key; use bucketIds");
1720
+ 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");
1721
+ const bucketIds = apiVersion !== "v4" && req.bucketId !== void 0 ? Object.freeze([req.bucketId]) : normalizeKeyBucketIds(req);
1722
+ const namePrefix = req.namePrefix === void 0 || req.namePrefix === "" ? null : req.namePrefix;
1723
+ 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");
1724
+ const kid = this.genId("sim_key");
1725
+ const appKey = this.genId("sim_secret");
1726
+ const expiration = req.validDurationInSeconds !== void 0 ? Date.now() + req.validDurationInSeconds * 1e3 : null;
1727
+ const stored = {
1728
+ applicationKeyId: kid,
1729
+ keyName: req.keyName,
1730
+ capabilities: req.capabilities,
1731
+ accountId: req.accountId,
1732
+ applicationKey: appKey,
1733
+ bucketIds,
1734
+ namePrefix,
1735
+ expirationTimestamp: expiration
1736
+ };
1737
+ this.keys.set(kid, stored);
1738
+ return {
1739
+ status: 200,
1740
+ body: {
1741
+ keyName: stored.keyName,
1742
+ applicationKeyId: stored.applicationKeyId,
1743
+ applicationKey: stored.applicationKey,
1744
+ capabilities: stored.capabilities,
1745
+ accountId: stored.accountId,
1746
+ expirationTimestamp: stored.expirationTimestamp,
1747
+ bucketIds: cloneBucketIds(stored.bucketIds),
1748
+ bucketId: singleBucketId(stored.bucketIds),
1749
+ namePrefix: stored.namePrefix,
1750
+ options: []
1751
+ }
1752
+ };
1753
+ }
1754
+ listKeys(req) {
1755
+ const countError = require_validation.validateMaxCount(req.maxKeyCount, "b2_list_keys");
1756
+ if (countError) return this.error(400, countError.code, countError.message);
1757
+ const max = req.maxKeyCount ?? 1e3;
1758
+ let allKeys = [...this.keys.values()].sort((a, b) => a.applicationKeyId.localeCompare(b.applicationKeyId));
1759
+ if (req.startApplicationKeyId) {
1760
+ const start = req.startApplicationKeyId;
1761
+ allKeys = allKeys.filter((k) => k.applicationKeyId >= start);
1762
+ }
1763
+ return {
1764
+ status: 200,
1765
+ body: {
1766
+ keys: allKeys.slice(0, max).map((k) => ({
1767
+ keyName: k.keyName,
1768
+ applicationKeyId: k.applicationKeyId,
1769
+ capabilities: k.capabilities,
1770
+ accountId: k.accountId,
1771
+ expirationTimestamp: k.expirationTimestamp,
1772
+ bucketIds: cloneBucketIds(k.bucketIds),
1773
+ bucketId: singleBucketId(k.bucketIds),
1774
+ namePrefix: k.namePrefix,
1775
+ options: []
1776
+ })),
1777
+ nextApplicationKeyId: allKeys.length > max ? allKeys[max]?.applicationKeyId ?? null : null
1778
+ }
1779
+ };
1780
+ }
1781
+ deleteKey(req) {
1782
+ const key = this.keys.get(req.applicationKeyId);
1783
+ if (!key) return this.error(400, "bad_request", "Key not found");
1784
+ this.keys.delete(req.applicationKeyId);
1785
+ for (const [token, scope] of this.issuedTokens.entries()) if (scope.applicationKeyId === req.applicationKeyId) this.issuedTokens.delete(token);
1786
+ for (const [token, scope] of this.uploadTokens.entries()) if (scope.applicationKeyId === req.applicationKeyId) {
1787
+ scope.invalidated = true;
1788
+ this.uploadTokens.set(token, scope);
1789
+ }
1790
+ return {
1791
+ status: 200,
1792
+ body: {
1793
+ keyName: key.keyName,
1794
+ applicationKeyId: key.applicationKeyId,
1795
+ capabilities: key.capabilities,
1796
+ accountId: key.accountId,
1797
+ expirationTimestamp: key.expirationTimestamp,
1798
+ bucketIds: cloneBucketIds(key.bucketIds),
1799
+ bucketId: singleBucketId(key.bucketIds),
1800
+ namePrefix: key.namePrefix,
1801
+ options: []
1802
+ }
1803
+ };
1804
+ }
1805
+ updateFileRetention(req) {
1806
+ const found = this.findFile(req.fileId);
1807
+ if (found === null || found.stored.fileVersion.fileName !== req.fileName) return this.error(404, "file_not_present", "File not found");
1808
+ found.versions[found.index] = {
1809
+ fileVersion: {
1810
+ ...found.stored.fileVersion,
1811
+ fileRetention: {
1812
+ isClientAuthorizedToRead: true,
1813
+ value: req.fileRetention
1814
+ }
1815
+ },
1816
+ data: found.stored.data
1817
+ };
1818
+ return {
1819
+ status: 200,
1820
+ body: {
1821
+ fileName: req.fileName,
1822
+ fileId: req.fileId,
1823
+ fileRetention: req.fileRetention
1824
+ }
1825
+ };
1826
+ }
1827
+ updateFileLegalHold(req) {
1828
+ const found = this.findFile(req.fileId);
1829
+ if (found === null || found.stored.fileVersion.fileName !== req.fileName) return this.error(404, "file_not_present", "File not found");
1830
+ found.versions[found.index] = {
1831
+ fileVersion: {
1832
+ ...found.stored.fileVersion,
1833
+ legalHold: {
1834
+ isClientAuthorizedToRead: true,
1835
+ value: req.legalHold
1836
+ }
1837
+ },
1838
+ data: found.stored.data
1839
+ };
1840
+ return {
1841
+ status: 200,
1842
+ body: {
1843
+ fileName: req.fileName,
1844
+ fileId: req.fileId,
1845
+ legalHold: req.legalHold
1846
+ }
1847
+ };
1848
+ }
1849
+ getBucketNotificationRules(req) {
1850
+ if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
1851
+ const rules = this.notificationRules.get(req.bucketId) ?? [];
1852
+ return {
1853
+ status: 200,
1854
+ body: {
1855
+ bucketId: req.bucketId,
1856
+ eventNotificationRules: rules
1857
+ }
1858
+ };
1859
+ }
1860
+ setBucketNotificationRules(req) {
1861
+ if (!this.buckets.has(req.bucketId)) return this.error(400, "bad_bucket_id", "Bucket not found");
1862
+ this.notificationRules.set(req.bucketId, req.eventNotificationRules);
1863
+ return {
1864
+ status: 200,
1865
+ body: {
1866
+ bucketId: req.bucketId,
1867
+ eventNotificationRules: req.eventNotificationRules
1868
+ }
1869
+ };
1870
+ }
1871
+ /**
1872
+ * Locates a stored file version by its `fileId`, scanning every bucket.
1873
+ *
1874
+ * Returns enough context to support read-only inspection (`stored`,
1875
+ * `bucketId`) AND in-place mutation (`versions`, `index`) so callers
1876
+ * that need to splice the version out can do so without re-scanning.
1877
+ *
1878
+ * Real B2 fileIds embed the bucketId, so production lookups are O(1);
1879
+ * the simulator's flat ID generator (`this.genId('4_z')`) doesn't, so this
1880
+ * is O(buckets × files × versions). Acceptable for tests.
1881
+ *
1882
+ * @param fileId - The file version ID to locate.
1883
+ *
1884
+ * @returns The location of the matching version, or `null` if not found.
1885
+ */
1886
+ findFile(fileId) {
1887
+ for (const [bid, bucket] of this.buckets.entries()) for (const versions of bucket.files.values()) {
1888
+ const idx = versions.findIndex((v) => v.fileVersion.fileId === fileId);
1889
+ if (idx !== -1) return {
1890
+ stored: versions[idx],
1891
+ bucketId: bid,
1892
+ bucket,
1893
+ versions,
1894
+ index: idx
1895
+ };
1896
+ }
1897
+ return null;
1898
+ }
1899
+ makeFileVersion(params) {
1900
+ return {
1901
+ accountId: require_ids.accountId(this.accountId),
1902
+ action: params.action,
1903
+ bucketId: require_ids.bucketId(params.bucketId),
1904
+ contentLength: params.contentLength,
1905
+ contentMd5: null,
1906
+ contentSha1: params.contentSha1,
1907
+ contentType: params.contentType,
1908
+ fileId: require_ids.fileId(this.genId("4_z")),
1909
+ fileInfo: params.fileInfo ?? {},
1910
+ fileName: params.fileName,
1911
+ fileRetention: {
1912
+ isClientAuthorizedToRead: true,
1913
+ value: params.fileRetention ?? null
1914
+ },
1915
+ legalHold: {
1916
+ isClientAuthorizedToRead: true,
1917
+ value: params.legalHold ?? null
1918
+ },
1919
+ replicationStatus: null,
1920
+ serverSideEncryption: publicServerSideEncryption(params.serverSideEncryption ?? { mode: require_encryption.EncryptionMode.None }),
1921
+ uploadTimestamp: this.monotonicTimestamp()
1922
+ };
1923
+ }
1924
+ error(status, code, message) {
1925
+ return {
1926
+ status,
1927
+ body: {
1928
+ status,
1929
+ code,
1930
+ message
1931
+ }
1932
+ };
1933
+ }
1934
+ /**
1935
+ * Fire the pluggable post-upload hooks for a file that just landed in
1936
+ * a bucket: matching event-notification rules → `onWebhookDeliver`,
1937
+ * configured replication source rules → `onReplicate`. Errors thrown
1938
+ * from user-supplied hooks are swallowed so a buggy listener never
1939
+ * masks an otherwise-successful API response.
1940
+ *
1941
+ * Fired-and-forgotten (no await) by the handler so the synthetic
1942
+ * response is returned to the caller as fast as production B2 would
1943
+ * acknowledge the write.
1944
+ *
1945
+ * @param fileVersion - The freshly-stored file metadata.
1946
+ * @param bucketId - The bucket the upload landed in.
1947
+ * @param eventType - The B2 event-type tag (e.g. `'b2:ObjectCreated:Upload'`)
1948
+ * used to match against `EventNotificationRule.eventTypes` globs.
1949
+ */
1950
+ firePostUploadHooks(fileVersion, bucketId, eventType) {
1951
+ const bucket = this.buckets.get(bucketId);
1952
+ if (!bucket) return;
1953
+ if (this.onWebhookDeliver !== void 0) {
1954
+ const rules = this.notificationRules.get(bucketId) ?? [];
1955
+ for (const rule of rules) {
1956
+ if (!rule.isEnabled) continue;
1957
+ if (!rule.eventTypes.some((pattern) => eventTypeMatches(pattern, eventType))) continue;
1958
+ const hook = this.onWebhookDeliver;
1959
+ this.dispatchHook("webhook", () => hook({
1960
+ rule,
1961
+ fileVersion,
1962
+ bucketId
1963
+ }));
1964
+ }
1965
+ }
1966
+ if (this.onReplicate !== void 0) {
1967
+ const sourceRules = bucket.info.replicationConfiguration.asReplicationSource?.replicationRules ?? [];
1968
+ for (const rule of sourceRules) {
1969
+ if (!rule.isEnabled) continue;
1970
+ if (rule.fileNamePrefix && !fileVersion.fileName.startsWith(rule.fileNamePrefix)) continue;
1971
+ const hook = this.onReplicate;
1972
+ this.dispatchHook("replication", () => hook({
1973
+ sourceFileVersion: fileVersion,
1974
+ sourceBucketId: bucketId,
1975
+ destinationBucketId: rule.destinationBucketId
1976
+ }));
1977
+ }
1978
+ }
1979
+ }
1980
+ /**
1981
+ * Schedule a user-supplied hook callback. Errors are routed to
1982
+ * `onHookError` if configured, swallowed otherwise. The returned
1983
+ * promise is tracked in {@link pendingHooks} so `flushHooks()` can
1984
+ * wait for every dispatched hook to settle.
1985
+ *
1986
+ * @param kind - Which hook this is (`'webhook'` or `'replication'`).
1987
+ * @param fn - Thunk that performs the hook invocation.
1988
+ */
1989
+ dispatchHook(kind, fn) {
1990
+ const task = Promise.resolve().then(() => fn()).catch((err) => {
1991
+ if (this.onHookError !== void 0) this.onHookError({
1992
+ kind,
1993
+ error: require_to_error.toError(err)
1994
+ });
1995
+ }).finally(() => {
1996
+ this.pendingHooks.delete(task);
1997
+ });
1998
+ this.pendingHooks.add(task);
1999
+ }
2000
+ };
2001
+ /**
2002
+ * Module-local cache of compiled glob regexes. Avoids recompiling the
2003
+ * same `eventTypes` pattern on every upload's hook dispatch — patterns
2004
+ * are stable for the lifetime of a `setBucketNotificationRules` call
2005
+ * and typically reused across thousands of uploads.
2006
+ */
2007
+ var eventTypeRegexCache = /* @__PURE__ */ new Map();
2008
+ /**
2009
+ * Wildcard match for B2 event-type globs. Supports the prefix-glob form
2010
+ * the B2 docs document: `b2:ObjectCreated:*` matches
2011
+ * `b2:ObjectCreated:Upload`, `b2:ObjectCreated:Copy`, etc. Exact
2012
+ * matches without `*` match literally.
2013
+ *
2014
+ * @param pattern - Glob from an `EventNotificationRule.eventTypes` entry.
2015
+ * @param eventType - Concrete event type produced by the simulator.
2016
+ *
2017
+ * @returns `true` when the pattern matches the event type.
2018
+ */
1435
2019
  function eventTypeMatches(pattern, eventType) {
1436
- if (!pattern.includes("*")) return pattern === eventType;
1437
- let compiled = eventTypeRegexCache.get(pattern);
1438
- if (compiled === void 0) {
1439
- const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
1440
- compiled = new RegExp(`^${escaped}$`);
1441
- eventTypeRegexCache.set(pattern, compiled);
1442
- }
1443
- return compiled.test(eventType);
2020
+ if (!pattern.includes("*")) return pattern === eventType;
2021
+ let compiled = eventTypeRegexCache.get(pattern);
2022
+ if (compiled === void 0) {
2023
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
2024
+ compiled = new RegExp(`^${escaped}$`);
2025
+ eventTypeRegexCache.set(pattern, compiled);
2026
+ }
2027
+ return compiled.test(eventType);
1444
2028
  }
2029
+ /**
2030
+ * Build a synthetic {@link HttpResponse} from a consumed {@link FaultSpec}.
2031
+ * Mirrors the shape of real B2 error responses so the SDK's
2032
+ * `RetryTransport` / `classifyError` paths see realistic input.
2033
+ *
2034
+ * @param fault - The fault spec to render.
2035
+ *
2036
+ * @returns An `HttpResponse` ready to return from `transport.send`.
2037
+ */
1445
2038
  function buildFaultResponse(fault) {
1446
- const status = fault.status ?? 503;
1447
- const code = fault.code ?? "service_unavailable";
1448
- const message = fault.message ?? "simulated failure";
1449
- const body = JSON.stringify({ status, code, message });
1450
- const headers = new Headers({ "Content-Type": "application/json" });
1451
- if (fault.retryAfter !== void 0) {
1452
- headers.set("Retry-After", String(fault.retryAfter));
1453
- }
1454
- return {
1455
- status,
1456
- headers,
1457
- body: new ReadableStream({
1458
- start(controller) {
1459
- controller.enqueue(textCodec.utf8Encoder.encode(body));
1460
- controller.close();
1461
- }
1462
- }),
1463
- json: () => Promise.resolve(JSON.parse(body)),
1464
- text: () => Promise.resolve(body),
1465
- arrayBuffer: () => Promise.resolve(textCodec.utf8Encoder.encode(body).buffer)
1466
- };
1467
- }
1468
- class SimulatorTransport {
1469
- constructor(sim) {
1470
- this.sim = sim;
1471
- }
1472
- async send(request) {
1473
- const url = request.url;
1474
- const fault = this.sim.consumeMatchingFault(url);
1475
- if (fault !== null) {
1476
- return buildFaultResponse(fault);
1477
- }
1478
- const headers = {};
1479
- if (request.headers) {
1480
- for (const [k, v] of Object.entries(request.headers)) {
1481
- headers[k.toLowerCase()] = v;
1482
- }
1483
- }
1484
- const isUpload = url.includes("b2_upload_file") || url.includes("b2_upload_part");
1485
- const parsedUrl = new URL(url);
1486
- const isDownload = parsedUrl.pathname.includes("b2_download_file_by_id") || parsedUrl.pathname.includes("/file/");
1487
- if (isDownload) {
1488
- const method = request.method === "HEAD" ? "HEAD" : "GET";
1489
- const result2 = this.sim.handleDownload(parsedUrl.pathname + parsedUrl.search, headers, method);
1490
- const data = result2.data ?? new Uint8Array(0);
1491
- const responseHeaders = new Headers(result2.headers);
1492
- responseHeaders.set(
1493
- "Content-Type",
1494
- result2.headers["Content-Type"] ?? "application/octet-stream"
1495
- );
1496
- const body = method === "HEAD" || result2.data === null ? null : new ReadableStream({
1497
- start(controller) {
1498
- controller.enqueue(data);
1499
- controller.close();
1500
- }
1501
- });
1502
- return {
1503
- status: result2.status,
1504
- headers: responseHeaders,
1505
- body,
1506
- json: () => Promise.reject(new Error("Download response is not JSON")),
1507
- text: () => Promise.resolve(textCodec.utf8Decoder.decode(data)),
1508
- arrayBuffer: () => Promise.resolve(
1509
- data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
1510
- )
1511
- };
1512
- }
1513
- let result;
1514
- if (isUpload && request.body) {
1515
- const data = new Uint8Array(
1516
- request.body instanceof ArrayBuffer ? request.body : request.body instanceof Uint8Array ? request.body.buffer.slice(
1517
- request.body.byteOffset,
1518
- request.body.byteOffset + request.body.byteLength
1519
- ) : await new Response(request.body).arrayBuffer()
1520
- );
1521
- result = this.sim.handleUpload(url, headers, data);
1522
- } else {
1523
- let body = null;
1524
- if (request.body) {
1525
- const text = typeof request.body === "string" ? request.body : await new Response(request.body).text();
1526
- try {
1527
- body = JSON.parse(text);
1528
- } catch {
1529
- body = text;
1530
- }
1531
- }
1532
- result = await this.sim.handleRequest(request.method, parsedUrl.pathname, headers, body);
1533
- }
1534
- const responseBody = JSON.stringify(result.body);
1535
- return {
1536
- status: result.status,
1537
- headers: new Headers({ "Content-Type": "application/json" }),
1538
- body: new ReadableStream({
1539
- start(controller) {
1540
- controller.enqueue(textCodec.utf8Encoder.encode(responseBody));
1541
- controller.close();
1542
- }
1543
- }),
1544
- json: () => Promise.resolve(result.body),
1545
- text: () => Promise.resolve(responseBody),
1546
- arrayBuffer: () => Promise.resolve(textCodec.utf8Encoder.encode(responseBody).buffer)
1547
- };
1548
- }
2039
+ const status = fault.status ?? 503;
2040
+ const code = fault.code ?? "service_unavailable";
2041
+ const message = fault.message ?? "simulated failure";
2042
+ const body = JSON.stringify({
2043
+ status,
2044
+ code,
2045
+ message
2046
+ });
2047
+ const headers = new Headers({ "Content-Type": "application/json" });
2048
+ if (fault.retryAfter !== void 0) headers.set("Retry-After", String(fault.retryAfter));
2049
+ return {
2050
+ status,
2051
+ headers,
2052
+ body: new ReadableStream({ start(controller) {
2053
+ controller.enqueue(require_text_codec.utf8Encoder.encode(body));
2054
+ controller.close();
2055
+ } }),
2056
+ json: () => Promise.resolve(JSON.parse(body)),
2057
+ text: () => Promise.resolve(body),
2058
+ arrayBuffer: () => Promise.resolve(require_text_codec.utf8Encoder.encode(body).buffer)
2059
+ };
1549
2060
  }
1550
- exports.BUCKET_INFO_MAX_KEYS = validation.BUCKET_INFO_MAX_KEYS;
1551
- exports.BUCKET_INFO_VALUE_MAX = validation.BUCKET_INFO_VALUE_MAX;
1552
- exports.BUCKET_NAME_MAX = validation.BUCKET_NAME_MAX;
1553
- exports.BUCKET_NAME_MIN = validation.BUCKET_NAME_MIN;
1554
- exports.FILE_INFO_TOTAL_MAX = validation.FILE_INFO_TOTAL_MAX;
1555
- exports.FILE_INFO_VALUE_MAX = validation.FILE_INFO_VALUE_MAX;
1556
- exports.FILE_NAME_MAX_BYTES = validation.FILE_NAME_MAX_BYTES;
1557
- exports.LIST_ENDPOINT_CAPS = validation.LIST_ENDPOINT_CAPS;
2061
+ var SimulatorTransport = class {
2062
+ sim;
2063
+ constructor(sim) {
2064
+ this.sim = sim;
2065
+ }
2066
+ async send(request) {
2067
+ const url = request.url;
2068
+ const fault = this.sim.consumeMatchingFault(url);
2069
+ if (fault !== null) return buildFaultResponse(fault);
2070
+ const headers = {};
2071
+ if (request.headers) for (const [k, v] of Object.entries(request.headers)) headers[k.toLowerCase()] = v;
2072
+ const parsedUrl = new URL(url);
2073
+ const endpoint = parsedUrl.pathname.split("/").pop() ?? "";
2074
+ const isUpload = endpoint === "b2_upload_file" || endpoint === "b2_upload_part";
2075
+ if (parsedUrl.pathname.includes("b2_download_file_by_id") || parsedUrl.pathname.includes("/file/")) {
2076
+ const method = request.method === "HEAD" ? "HEAD" : "GET";
2077
+ const result = this.sim.handleDownload(parsedUrl.pathname + parsedUrl.search, headers, method);
2078
+ const data = result.data ?? /* @__PURE__ */ new Uint8Array(0);
2079
+ const responseHeaders = new Headers(result.headers);
2080
+ responseHeaders.set("Content-Type", result.headers["Content-Type"] ?? "application/octet-stream");
2081
+ const body = method === "HEAD" || result.data === null ? null : new ReadableStream({ start(controller) {
2082
+ controller.enqueue(data);
2083
+ controller.close();
2084
+ } });
2085
+ return {
2086
+ status: result.status,
2087
+ headers: responseHeaders,
2088
+ body,
2089
+ json: () => Promise.reject(/* @__PURE__ */ new Error("Download response is not JSON")),
2090
+ text: () => Promise.resolve(require_text_codec.utf8Decoder.decode(data)),
2091
+ arrayBuffer: () => Promise.resolve(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength))
2092
+ };
2093
+ }
2094
+ let result;
2095
+ if (isUpload && request.body) {
2096
+ 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());
2097
+ result = await this.sim.handleUpload(url, headers, data);
2098
+ } else {
2099
+ let body = null;
2100
+ if (request.body) {
2101
+ const text = typeof request.body === "string" ? request.body : await new Response(request.body).text();
2102
+ try {
2103
+ body = JSON.parse(text);
2104
+ } catch {
2105
+ body = text;
2106
+ }
2107
+ }
2108
+ result = await this.sim.handleRequest(request.method, parsedUrl.origin, parsedUrl.pathname, headers, body);
2109
+ }
2110
+ const responseBody = JSON.stringify(result.body);
2111
+ return {
2112
+ status: result.status,
2113
+ headers: new Headers({ "Content-Type": "application/json" }),
2114
+ body: new ReadableStream({ start(controller) {
2115
+ controller.enqueue(require_text_codec.utf8Encoder.encode(responseBody));
2116
+ controller.close();
2117
+ } }),
2118
+ json: () => Promise.resolve(result.body),
2119
+ text: () => Promise.resolve(responseBody),
2120
+ arrayBuffer: () => Promise.resolve(require_text_codec.utf8Encoder.encode(responseBody).buffer)
2121
+ };
2122
+ }
2123
+ };
2124
+ //#endregion
1558
2125
  exports.B2Simulator = B2Simulator;
1559
- //# sourceMappingURL=index.cjs.map
2126
+ exports.BUCKET_INFO_MAX_KEYS = require_validation.BUCKET_INFO_MAX_KEYS;
2127
+ exports.BUCKET_INFO_VALUE_MAX = require_validation.BUCKET_INFO_VALUE_MAX;
2128
+ exports.BUCKET_NAME_MAX = require_b2_naming.BUCKET_NAME_MAX;
2129
+ exports.BUCKET_NAME_MIN = require_b2_naming.BUCKET_NAME_MIN;
2130
+ exports.FILE_INFO_TOTAL_MAX = require_validation.FILE_INFO_TOTAL_MAX;
2131
+ exports.FILE_INFO_VALUE_MAX = require_validation.FILE_INFO_VALUE_MAX;
2132
+ exports.FILE_NAME_MAX_BYTES = require_b2_naming.FILE_NAME_MAX_BYTES;
2133
+ exports.LIST_ENDPOINT_CAPS = require_validation.LIST_ENDPOINT_CAPS;
2134
+
2135
+ //# sourceMappingURL=index.cjs.map