@streetjs/storage 1.0.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 (363) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +719 -0
  3. package/dist/access.d.ts +95 -0
  4. package/dist/access.d.ts.map +1 -0
  5. package/dist/access.js +114 -0
  6. package/dist/access.js.map +1 -0
  7. package/dist/cli/commands.d.ts +88 -0
  8. package/dist/cli/commands.d.ts.map +1 -0
  9. package/dist/cli/commands.js +369 -0
  10. package/dist/cli/commands.js.map +1 -0
  11. package/dist/cli/generators.d.ts +41 -0
  12. package/dist/cli/generators.d.ts.map +1 -0
  13. package/dist/cli/generators.js +119 -0
  14. package/dist/cli/generators.js.map +1 -0
  15. package/dist/directory.d.ts +88 -0
  16. package/dist/directory.d.ts.map +1 -0
  17. package/dist/directory.js +148 -0
  18. package/dist/directory.js.map +1 -0
  19. package/dist/driver.d.ts +83 -0
  20. package/dist/driver.d.ts.map +1 -0
  21. package/dist/driver.js +16 -0
  22. package/dist/driver.js.map +1 -0
  23. package/dist/drivers/azure.d.ts +239 -0
  24. package/dist/drivers/azure.d.ts.map +1 -0
  25. package/dist/drivers/azure.js +476 -0
  26. package/dist/drivers/azure.js.map +1 -0
  27. package/dist/drivers/backblaze.d.ts +105 -0
  28. package/dist/drivers/backblaze.d.ts.map +1 -0
  29. package/dist/drivers/backblaze.js +279 -0
  30. package/dist/drivers/backblaze.js.map +1 -0
  31. package/dist/drivers/gcs.d.ts +239 -0
  32. package/dist/drivers/gcs.d.ts.map +1 -0
  33. package/dist/drivers/gcs.js +454 -0
  34. package/dist/drivers/gcs.js.map +1 -0
  35. package/dist/drivers/local.d.ts +126 -0
  36. package/dist/drivers/local.d.ts.map +1 -0
  37. package/dist/drivers/local.js +321 -0
  38. package/dist/drivers/local.js.map +1 -0
  39. package/dist/drivers/memory.d.ts +104 -0
  40. package/dist/drivers/memory.d.ts.map +1 -0
  41. package/dist/drivers/memory.js +197 -0
  42. package/dist/drivers/memory.js.map +1 -0
  43. package/dist/drivers/minio.d.ts +90 -0
  44. package/dist/drivers/minio.d.ts.map +1 -0
  45. package/dist/drivers/minio.js +276 -0
  46. package/dist/drivers/minio.js.map +1 -0
  47. package/dist/drivers/r2.d.ts +100 -0
  48. package/dist/drivers/r2.d.ts.map +1 -0
  49. package/dist/drivers/r2.js +240 -0
  50. package/dist/drivers/r2.js.map +1 -0
  51. package/dist/drivers/s3-base.d.ts +270 -0
  52. package/dist/drivers/s3-base.d.ts.map +1 -0
  53. package/dist/drivers/s3-base.js +436 -0
  54. package/dist/drivers/s3-base.js.map +1 -0
  55. package/dist/drivers/s3.d.ts +111 -0
  56. package/dist/drivers/s3.d.ts.map +1 -0
  57. package/dist/drivers/s3.js +311 -0
  58. package/dist/drivers/s3.js.map +1 -0
  59. package/dist/drivers/supabase.d.ts +246 -0
  60. package/dist/drivers/supabase.d.ts.map +1 -0
  61. package/dist/drivers/supabase.js +502 -0
  62. package/dist/drivers/supabase.js.map +1 -0
  63. package/dist/errors.d.ts +115 -0
  64. package/dist/errors.d.ts.map +1 -0
  65. package/dist/errors.js +131 -0
  66. package/dist/errors.js.map +1 -0
  67. package/dist/examples/storage-demo.d.ts +53 -0
  68. package/dist/examples/storage-demo.d.ts.map +1 -0
  69. package/dist/examples/storage-demo.js +242 -0
  70. package/dist/examples/storage-demo.js.map +1 -0
  71. package/dist/facade.d.ts +192 -0
  72. package/dist/facade.d.ts.map +1 -0
  73. package/dist/facade.js +898 -0
  74. package/dist/facade.js.map +1 -0
  75. package/dist/image.d.ts +94 -0
  76. package/dist/image.d.ts.map +1 -0
  77. package/dist/image.js +224 -0
  78. package/dist/image.js.map +1 -0
  79. package/dist/index.d.ts +47 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +53 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/integrations/events.d.ts +108 -0
  84. package/dist/integrations/events.d.ts.map +1 -0
  85. package/dist/integrations/events.js +111 -0
  86. package/dist/integrations/events.js.map +1 -0
  87. package/dist/integrations/queue.d.ts +89 -0
  88. package/dist/integrations/queue.d.ts.map +1 -0
  89. package/dist/integrations/queue.js +91 -0
  90. package/dist/integrations/queue.js.map +1 -0
  91. package/dist/integrations/realtime.d.ts +95 -0
  92. package/dist/integrations/realtime.d.ts.map +1 -0
  93. package/dist/integrations/realtime.js +90 -0
  94. package/dist/integrations/realtime.js.map +1 -0
  95. package/dist/lifecycle.d.ts +137 -0
  96. package/dist/lifecycle.d.ts.map +1 -0
  97. package/dist/lifecycle.js +240 -0
  98. package/dist/lifecycle.js.map +1 -0
  99. package/dist/metadata.d.ts +109 -0
  100. package/dist/metadata.d.ts.map +1 -0
  101. package/dist/metadata.js +139 -0
  102. package/dist/metadata.js.map +1 -0
  103. package/dist/multipart.d.ts +108 -0
  104. package/dist/multipart.d.ts.map +1 -0
  105. package/dist/multipart.js +200 -0
  106. package/dist/multipart.js.map +1 -0
  107. package/dist/observability.d.ts +82 -0
  108. package/dist/observability.d.ts.map +1 -0
  109. package/dist/observability.js +175 -0
  110. package/dist/observability.js.map +1 -0
  111. package/dist/plugin.d.ts +82 -0
  112. package/dist/plugin.d.ts.map +1 -0
  113. package/dist/plugin.js +100 -0
  114. package/dist/plugin.js.map +1 -0
  115. package/dist/resumable.d.ts +118 -0
  116. package/dist/resumable.d.ts.map +1 -0
  117. package/dist/resumable.js +223 -0
  118. package/dist/resumable.js.map +1 -0
  119. package/dist/search.d.ts +57 -0
  120. package/dist/search.d.ts.map +1 -0
  121. package/dist/search.js +172 -0
  122. package/dist/search.js.map +1 -0
  123. package/dist/signed-url.d.ts +110 -0
  124. package/dist/signed-url.d.ts.map +1 -0
  125. package/dist/signed-url.js +237 -0
  126. package/dist/signed-url.js.map +1 -0
  127. package/dist/testing/index.d.ts +219 -0
  128. package/dist/testing/index.d.ts.map +1 -0
  129. package/dist/testing/index.js +393 -0
  130. package/dist/testing/index.js.map +1 -0
  131. package/dist/tests/access.test.d.ts +2 -0
  132. package/dist/tests/access.test.d.ts.map +1 -0
  133. package/dist/tests/access.test.js +270 -0
  134. package/dist/tests/access.test.js.map +1 -0
  135. package/dist/tests/azure-driver.test.d.ts +2 -0
  136. package/dist/tests/azure-driver.test.d.ts.map +1 -0
  137. package/dist/tests/azure-driver.test.js +183 -0
  138. package/dist/tests/azure-driver.test.js.map +1 -0
  139. package/dist/tests/backblaze-driver.test.d.ts +2 -0
  140. package/dist/tests/backblaze-driver.test.d.ts.map +1 -0
  141. package/dist/tests/backblaze-driver.test.js +185 -0
  142. package/dist/tests/backblaze-driver.test.js.map +1 -0
  143. package/dist/tests/bridge-degradation.test.d.ts +2 -0
  144. package/dist/tests/bridge-degradation.test.d.ts.map +1 -0
  145. package/dist/tests/bridge-degradation.test.js +138 -0
  146. package/dist/tests/bridge-degradation.test.js.map +1 -0
  147. package/dist/tests/cli.test.d.ts +2 -0
  148. package/dist/tests/cli.test.d.ts.map +1 -0
  149. package/dist/tests/cli.test.js +127 -0
  150. package/dist/tests/cli.test.js.map +1 -0
  151. package/dist/tests/contract.d.ts +131 -0
  152. package/dist/tests/contract.d.ts.map +1 -0
  153. package/dist/tests/contract.js +222 -0
  154. package/dist/tests/contract.js.map +1 -0
  155. package/dist/tests/contract.test.d.ts +2 -0
  156. package/dist/tests/contract.test.d.ts.map +1 -0
  157. package/dist/tests/contract.test.js +110 -0
  158. package/dist/tests/contract.test.js.map +1 -0
  159. package/dist/tests/directory.test.d.ts +2 -0
  160. package/dist/tests/directory.test.d.ts.map +1 -0
  161. package/dist/tests/directory.test.js +125 -0
  162. package/dist/tests/directory.test.js.map +1 -0
  163. package/dist/tests/errors.test.d.ts +2 -0
  164. package/dist/tests/errors.test.d.ts.map +1 -0
  165. package/dist/tests/errors.test.js +93 -0
  166. package/dist/tests/errors.test.js.map +1 -0
  167. package/dist/tests/events-bridge.test.d.ts +2 -0
  168. package/dist/tests/events-bridge.test.d.ts.map +1 -0
  169. package/dist/tests/events-bridge.test.js +228 -0
  170. package/dist/tests/events-bridge.test.js.map +1 -0
  171. package/dist/tests/example-smoke.test.d.ts +2 -0
  172. package/dist/tests/example-smoke.test.d.ts.map +1 -0
  173. package/dist/tests/example-smoke.test.js +34 -0
  174. package/dist/tests/example-smoke.test.js.map +1 -0
  175. package/dist/tests/gcs-driver.test.d.ts +2 -0
  176. package/dist/tests/gcs-driver.test.d.ts.map +1 -0
  177. package/dist/tests/gcs-driver.test.js +197 -0
  178. package/dist/tests/gcs-driver.test.js.map +1 -0
  179. package/dist/tests/image.test.d.ts +2 -0
  180. package/dist/tests/image.test.d.ts.map +1 -0
  181. package/dist/tests/image.test.js +102 -0
  182. package/dist/tests/image.test.js.map +1 -0
  183. package/dist/tests/lifecycle.test.d.ts +2 -0
  184. package/dist/tests/lifecycle.test.d.ts.map +1 -0
  185. package/dist/tests/lifecycle.test.js +171 -0
  186. package/dist/tests/lifecycle.test.js.map +1 -0
  187. package/dist/tests/local-driver.test.d.ts +2 -0
  188. package/dist/tests/local-driver.test.d.ts.map +1 -0
  189. package/dist/tests/local-driver.test.js +153 -0
  190. package/dist/tests/local-driver.test.js.map +1 -0
  191. package/dist/tests/local-streaming.test.d.ts +2 -0
  192. package/dist/tests/local-streaming.test.d.ts.map +1 -0
  193. package/dist/tests/local-streaming.test.js +121 -0
  194. package/dist/tests/local-streaming.test.js.map +1 -0
  195. package/dist/tests/memory-driver.test.d.ts +2 -0
  196. package/dist/tests/memory-driver.test.d.ts.map +1 -0
  197. package/dist/tests/memory-driver.test.js +180 -0
  198. package/dist/tests/memory-driver.test.js.map +1 -0
  199. package/dist/tests/memory-streaming.test.d.ts +2 -0
  200. package/dist/tests/memory-streaming.test.d.ts.map +1 -0
  201. package/dist/tests/memory-streaming.test.js +92 -0
  202. package/dist/tests/memory-streaming.test.js.map +1 -0
  203. package/dist/tests/minio-driver.test.d.ts +2 -0
  204. package/dist/tests/minio-driver.test.d.ts.map +1 -0
  205. package/dist/tests/minio-driver.test.js +146 -0
  206. package/dist/tests/minio-driver.test.js.map +1 -0
  207. package/dist/tests/non-s3-integration.test.d.ts +2 -0
  208. package/dist/tests/non-s3-integration.test.d.ts.map +1 -0
  209. package/dist/tests/non-s3-integration.test.js +201 -0
  210. package/dist/tests/non-s3-integration.test.js.map +1 -0
  211. package/dist/tests/observability.registration.test.d.ts +2 -0
  212. package/dist/tests/observability.registration.test.d.ts.map +1 -0
  213. package/dist/tests/observability.registration.test.js +103 -0
  214. package/dist/tests/observability.registration.test.js.map +1 -0
  215. package/dist/tests/observability.test.d.ts +2 -0
  216. package/dist/tests/observability.test.d.ts.map +1 -0
  217. package/dist/tests/observability.test.js +240 -0
  218. package/dist/tests/observability.test.js.map +1 -0
  219. package/dist/tests/plugin.test.d.ts +2 -0
  220. package/dist/tests/plugin.test.d.ts.map +1 -0
  221. package/dist/tests/plugin.test.js +112 -0
  222. package/dist/tests/plugin.test.js.map +1 -0
  223. package/dist/tests/prop-concurrent.test.d.ts +2 -0
  224. package/dist/tests/prop-concurrent.test.d.ts.map +1 -0
  225. package/dist/tests/prop-concurrent.test.js +59 -0
  226. package/dist/tests/prop-concurrent.test.js.map +1 -0
  227. package/dist/tests/prop-copy.test.d.ts +2 -0
  228. package/dist/tests/prop-copy.test.d.ts.map +1 -0
  229. package/dist/tests/prop-copy.test.js +63 -0
  230. package/dist/tests/prop-copy.test.js.map +1 -0
  231. package/dist/tests/prop-delete.test.d.ts +2 -0
  232. package/dist/tests/prop-delete.test.d.ts.map +1 -0
  233. package/dist/tests/prop-delete.test.js +35 -0
  234. package/dist/tests/prop-delete.test.js.map +1 -0
  235. package/dist/tests/prop-directory.test.d.ts +2 -0
  236. package/dist/tests/prop-directory.test.d.ts.map +1 -0
  237. package/dist/tests/prop-directory.test.js +118 -0
  238. package/dist/tests/prop-directory.test.js.map +1 -0
  239. package/dist/tests/prop-driver-conformance.test.d.ts +2 -0
  240. package/dist/tests/prop-driver-conformance.test.d.ts.map +1 -0
  241. package/dist/tests/prop-driver-conformance.test.js +110 -0
  242. package/dist/tests/prop-driver-conformance.test.js.map +1 -0
  243. package/dist/tests/prop-driver-equivalence.test.d.ts +2 -0
  244. package/dist/tests/prop-driver-equivalence.test.d.ts.map +1 -0
  245. package/dist/tests/prop-driver-equivalence.test.js +163 -0
  246. package/dist/tests/prop-driver-equivalence.test.js.map +1 -0
  247. package/dist/tests/prop-lifecycle.test.d.ts +2 -0
  248. package/dist/tests/prop-lifecycle.test.d.ts.map +1 -0
  249. package/dist/tests/prop-lifecycle.test.js +83 -0
  250. package/dist/tests/prop-lifecycle.test.js.map +1 -0
  251. package/dist/tests/prop-list.test.d.ts +2 -0
  252. package/dist/tests/prop-list.test.d.ts.map +1 -0
  253. package/dist/tests/prop-list.test.js +53 -0
  254. package/dist/tests/prop-list.test.js.map +1 -0
  255. package/dist/tests/prop-metadata.test.d.ts +2 -0
  256. package/dist/tests/prop-metadata.test.d.ts.map +1 -0
  257. package/dist/tests/prop-metadata.test.js +87 -0
  258. package/dist/tests/prop-metadata.test.js.map +1 -0
  259. package/dist/tests/prop-move.test.d.ts +2 -0
  260. package/dist/tests/prop-move.test.d.ts.map +1 -0
  261. package/dist/tests/prop-move.test.js +67 -0
  262. package/dist/tests/prop-move.test.js.map +1 -0
  263. package/dist/tests/prop-multipart-abort.test.d.ts +2 -0
  264. package/dist/tests/prop-multipart-abort.test.d.ts.map +1 -0
  265. package/dist/tests/prop-multipart-abort.test.js +48 -0
  266. package/dist/tests/prop-multipart-abort.test.js.map +1 -0
  267. package/dist/tests/prop-multipart.test.d.ts +2 -0
  268. package/dist/tests/prop-multipart.test.d.ts.map +1 -0
  269. package/dist/tests/prop-multipart.test.js +65 -0
  270. package/dist/tests/prop-multipart.test.js.map +1 -0
  271. package/dist/tests/prop-resumable.test.d.ts +2 -0
  272. package/dist/tests/prop-resumable.test.d.ts.map +1 -0
  273. package/dist/tests/prop-resumable.test.js +78 -0
  274. package/dist/tests/prop-resumable.test.js.map +1 -0
  275. package/dist/tests/prop-search.test.d.ts +2 -0
  276. package/dist/tests/prop-search.test.d.ts.map +1 -0
  277. package/dist/tests/prop-search.test.js +120 -0
  278. package/dist/tests/prop-search.test.js.map +1 -0
  279. package/dist/tests/prop-signed-url.test.d.ts +2 -0
  280. package/dist/tests/prop-signed-url.test.d.ts.map +1 -0
  281. package/dist/tests/prop-signed-url.test.js +74 -0
  282. package/dist/tests/prop-signed-url.test.js.map +1 -0
  283. package/dist/tests/prop-streaming.test.d.ts +2 -0
  284. package/dist/tests/prop-streaming.test.d.ts.map +1 -0
  285. package/dist/tests/prop-streaming.test.js +34 -0
  286. package/dist/tests/prop-streaming.test.js.map +1 -0
  287. package/dist/tests/prop-upload-download.test.d.ts +2 -0
  288. package/dist/tests/prop-upload-download.test.d.ts.map +1 -0
  289. package/dist/tests/prop-upload-download.test.js +30 -0
  290. package/dist/tests/prop-upload-download.test.js.map +1 -0
  291. package/dist/tests/prop-validation.test.d.ts +2 -0
  292. package/dist/tests/prop-validation.test.d.ts.map +1 -0
  293. package/dist/tests/prop-validation.test.js +50 -0
  294. package/dist/tests/prop-validation.test.js.map +1 -0
  295. package/dist/tests/prop-versioning.test.d.ts +2 -0
  296. package/dist/tests/prop-versioning.test.d.ts.map +1 -0
  297. package/dist/tests/prop-versioning.test.js +74 -0
  298. package/dist/tests/prop-versioning.test.js.map +1 -0
  299. package/dist/tests/queue-bridge.test.d.ts +2 -0
  300. package/dist/tests/queue-bridge.test.d.ts.map +1 -0
  301. package/dist/tests/queue-bridge.test.js +111 -0
  302. package/dist/tests/queue-bridge.test.js.map +1 -0
  303. package/dist/tests/r2-driver.test.d.ts +2 -0
  304. package/dist/tests/r2-driver.test.d.ts.map +1 -0
  305. package/dist/tests/r2-driver.test.js +143 -0
  306. package/dist/tests/r2-driver.test.js.map +1 -0
  307. package/dist/tests/realtime-bridge.test.d.ts +2 -0
  308. package/dist/tests/realtime-bridge.test.d.ts.map +1 -0
  309. package/dist/tests/realtime-bridge.test.js +106 -0
  310. package/dist/tests/realtime-bridge.test.js.map +1 -0
  311. package/dist/tests/resumable.test.d.ts +2 -0
  312. package/dist/tests/resumable.test.d.ts.map +1 -0
  313. package/dist/tests/resumable.test.js +104 -0
  314. package/dist/tests/resumable.test.js.map +1 -0
  315. package/dist/tests/s3-base.test.d.ts +2 -0
  316. package/dist/tests/s3-base.test.d.ts.map +1 -0
  317. package/dist/tests/s3-base.test.js +289 -0
  318. package/dist/tests/s3-base.test.js.map +1 -0
  319. package/dist/tests/s3-integration.test.d.ts +2 -0
  320. package/dist/tests/s3-integration.test.d.ts.map +1 -0
  321. package/dist/tests/s3-integration.test.js +270 -0
  322. package/dist/tests/s3-integration.test.js.map +1 -0
  323. package/dist/tests/s3.test.d.ts +2 -0
  324. package/dist/tests/s3.test.d.ts.map +1 -0
  325. package/dist/tests/s3.test.js +203 -0
  326. package/dist/tests/s3.test.js.map +1 -0
  327. package/dist/tests/scaffold.test.d.ts +2 -0
  328. package/dist/tests/scaffold.test.d.ts.map +1 -0
  329. package/dist/tests/scaffold.test.js +13 -0
  330. package/dist/tests/scaffold.test.js.map +1 -0
  331. package/dist/tests/search.test.d.ts +2 -0
  332. package/dist/tests/search.test.d.ts.map +1 -0
  333. package/dist/tests/search.test.js +106 -0
  334. package/dist/tests/search.test.js.map +1 -0
  335. package/dist/tests/supabase-driver.test.d.ts +2 -0
  336. package/dist/tests/supabase-driver.test.d.ts.map +1 -0
  337. package/dist/tests/supabase-driver.test.js +196 -0
  338. package/dist/tests/supabase-driver.test.js.map +1 -0
  339. package/dist/tests/testing-utilities.test.d.ts +2 -0
  340. package/dist/tests/testing-utilities.test.d.ts.map +1 -0
  341. package/dist/tests/testing-utilities.test.js +138 -0
  342. package/dist/tests/testing-utilities.test.js.map +1 -0
  343. package/dist/tests/validation.test.d.ts +2 -0
  344. package/dist/tests/validation.test.d.ts.map +1 -0
  345. package/dist/tests/validation.test.js +135 -0
  346. package/dist/tests/validation.test.js.map +1 -0
  347. package/dist/tests/versioning.test.d.ts +2 -0
  348. package/dist/tests/versioning.test.d.ts.map +1 -0
  349. package/dist/tests/versioning.test.js +128 -0
  350. package/dist/tests/versioning.test.js.map +1 -0
  351. package/dist/types.d.ts +232 -0
  352. package/dist/types.d.ts.map +1 -0
  353. package/dist/types.js +23 -0
  354. package/dist/types.js.map +1 -0
  355. package/dist/validation.d.ts +105 -0
  356. package/dist/validation.d.ts.map +1 -0
  357. package/dist/validation.js +225 -0
  358. package/dist/validation.js.map +1 -0
  359. package/dist/versioning.d.ts +100 -0
  360. package/dist/versioning.d.ts.map +1 -0
  361. package/dist/versioning.js +179 -0
  362. package/dist/versioning.js.map +1 -0
  363. package/package.json +137 -0
package/dist/facade.js ADDED
@@ -0,0 +1,898 @@
1
+ /**
2
+ * @streetjs/storage — the storage facade and configuration-driven construction.
3
+ *
4
+ * Application code talks only to the {@link Storage} facade returned by
5
+ * {@link createStorage}. The facade presents one public surface that is
6
+ * identical no matter which {@link StorageDriver} backs it, delegating the
7
+ * actual byte persistence to the driver while running (in later tasks) the
8
+ * cross-cutting layers (validation, access control, versioning, lifecycle,
9
+ * streaming/multipart/resumable sessions, signed URLs, images, directory,
10
+ * search, observability, integration bridges).
11
+ *
12
+ * This module (task 5.1) implements **construction and provider selection**:
13
+ *
14
+ * - A built-in driver registry resolves the zero-dependency providers `memory`
15
+ * ({@link MemoryStorageDriver}) and `local` ({@link LocalStorageDriver},
16
+ * which requires `config.root`).
17
+ * - Cloud providers are supplied as an already-constructed `config.driver`
18
+ * instance (imported from the relevant submodule), which takes precedence and
19
+ * works for any provider name.
20
+ * - An unknown provider name with no supplied driver yields a descriptive
21
+ * {@link StorageConfigError} and **no** {@link Storage} instance is produced
22
+ * (Requirement 1.5). Because the error is simply thrown (never caught and
23
+ * swallowed into a fabricated instance), if constructing the error object
24
+ * itself fails for some unexpected internal reason, that internal failure
25
+ * surfaces as a thrown error and, again, no instance is produced
26
+ * (Requirement 1.6).
27
+ *
28
+ * The object-operation method bodies (`put`/`get`/`exists`/`delete`/`copy`/
29
+ * `move`/`rename`/`list`/`stat`), streaming, multipart, resumable, signed URLs,
30
+ * versioning, images, directory, search, and observability are implemented in
31
+ * later tasks (5.2 onward). They are stubbed here so the file compiles, while
32
+ * the facade is structured as a class holding the resolved `driver` + `config`
33
+ * so those tasks can fill in the behavior cleanly.
34
+ *
35
+ * _Requirements: 1.1, 1.2, 1.4, 1.5, 1.6_
36
+ */
37
+ import { createHash } from "node:crypto";
38
+ import { Buffer } from "node:buffer";
39
+ import { AccessController } from "./access.js";
40
+ import { StorageDirectoryApi } from "./directory.js";
41
+ import { StorageImageProcessor } from "./image.js";
42
+ import { MemoryStorageDriver } from "./drivers/memory.js";
43
+ import { LocalStorageDriver } from "./drivers/local.js";
44
+ import { StorageConfigError, ValidationError } from "./errors.js";
45
+ import { LifecycleEngine } from "./lifecycle.js";
46
+ import { DEFAULT_ACCESS_LEVEL, normalizeMetadata, toWriteMetadata } from "./metadata.js";
47
+ import { MultipartManager } from "./multipart.js";
48
+ import { ResumableManager } from "./resumable.js";
49
+ import { searchObjects } from "./search.js";
50
+ import { SignedUrlService } from "./signed-url.js";
51
+ import { ValidationPipeline } from "./validation.js";
52
+ import { VersioningManager } from "./versioning.js";
53
+ import { bridgeStorageEvents } from "./integrations/events.js";
54
+ import { bridgeStorageQueue } from "./integrations/queue.js";
55
+ import { bridgeStorageRealtime } from "./integrations/realtime.js";
56
+ import { registerStorageObservability } from "./observability.js";
57
+ /**
58
+ * The built-in, zero-dependency provider registry (Requirement 1.2). Cloud
59
+ * providers are never listed here — they are supplied as a pre-constructed
60
+ * `config.driver` instead (see {@link resolveDriver}).
61
+ */
62
+ const BUILT_IN_DRIVERS = {
63
+ memory: (config) => new MemoryStorageDriver({ clock: config.clock }),
64
+ local: (config) => {
65
+ if (config.root === undefined || config.root === "") {
66
+ throw new StorageConfigError('The "local" storage provider requires a "root" directory in its configuration.', { provider: "local" });
67
+ }
68
+ return new LocalStorageDriver({ root: config.root, clock: config.clock });
69
+ },
70
+ };
71
+ /**
72
+ * Resolve the {@link StorageDriver} for the given configuration.
73
+ *
74
+ * Precedence:
75
+ * 1. A pre-constructed `config.driver` (used for cloud providers imported from
76
+ * submodules) is used as-is, regardless of the `provider` name.
77
+ * 2. Otherwise the `provider` name is looked up in the built-in registry and the
78
+ * corresponding zero-dependency driver is constructed.
79
+ * 3. If the `provider` name is unknown and no `driver` was supplied, a
80
+ * descriptive {@link StorageConfigError} is thrown and no driver (and hence
81
+ * no {@link Storage} instance) is produced (Requirement 1.5).
82
+ *
83
+ * The error is thrown, never caught-and-swallowed, so an unexpected failure
84
+ * while constructing the error object itself simply propagates as a thrown
85
+ * internal error — neither an instance nor a fabricated error is returned
86
+ * (Requirement 1.6).
87
+ */
88
+ function resolveDriver(config) {
89
+ if (config.driver !== undefined) {
90
+ return config.driver;
91
+ }
92
+ const factory = BUILT_IN_DRIVERS[config.provider];
93
+ if (factory !== undefined) {
94
+ return factory(config);
95
+ }
96
+ throw new StorageConfigError(`Unknown storage provider "${config.provider}". Provide a built-in provider ` +
97
+ `("memory" or "local"), or supply a pre-constructed "driver" for cloud providers.`, { provider: config.provider });
98
+ }
99
+ // ── Facade implementation ──────────────────────────────────────────────────
100
+ /**
101
+ * Concrete {@link Storage} implementation holding the resolved {@link StorageDriver}
102
+ * and the originating {@link StorageConfig}. Task 5.1 wires construction and
103
+ * provider selection; the object-operation, streaming, multipart, resumable,
104
+ * signed-URL, versioning, image, directory, search, and observability method
105
+ * bodies are filled in by later tasks. Until then the not-yet-implemented
106
+ * methods throw a clear, descriptive error.
107
+ */
108
+ class StorageFacade {
109
+ /** The resolved driver every operation delegates byte persistence to. */
110
+ driver;
111
+ /** The configuration this facade was constructed with. */
112
+ config;
113
+ /**
114
+ * The pre-persistence validation pipeline, present only when
115
+ * `config.validation` is supplied. When defined it is run as the first step
116
+ * of every write so a rejection aborts the write before any bytes reach the
117
+ * driver, leaving no partial object stored (Requirements 9.3, 9.4).
118
+ */
119
+ validation;
120
+ /**
121
+ * The provider-agnostic multipart upload manager. It delegates to the driver's
122
+ * native `multipart` capability when present and otherwise simulates multipart
123
+ * over the driver primitives, so `createMultipartUpload`/`uploadPart`/
124
+ * `completeMultipartUpload`/`abortMultipartUpload` behave identically across
125
+ * providers (Requirement 6).
126
+ */
127
+ multipart;
128
+ /**
129
+ * The provider-agnostic resumable upload manager. It delegates to the driver's
130
+ * native `resumable` capability when present and otherwise simulates
131
+ * offset-tracked sessions over the driver primitives, so `startUpload`/
132
+ * `resumeUpload`/`cancelUpload` behave identically across providers
133
+ * (Requirement 7).
134
+ */
135
+ resumable;
136
+ /**
137
+ * The provider-agnostic signed URL service. It delegates to the driver's
138
+ * native `signedUrl` capability when present and otherwise mints/verifies
139
+ * HMAC-signed URLs over `(key, op, expiry)` using `config.signingSecret`,
140
+ * checking expiry against the injected clock, so `signedUrl` behaves
141
+ * identically across providers (Requirement 8).
142
+ */
143
+ signedUrls;
144
+ /**
145
+ * The provider-agnostic versioning manager. It delegates to the driver's
146
+ * native `versioning` capability when present and otherwise simulates version
147
+ * snapshots over the driver primitives (reserved `.versions/<key>/<versionId>`
148
+ * copies), so `listVersions`/`restoreVersion`/`deleteVersion` and the
149
+ * overwrite-time snapshot behave identically across providers (Requirement
150
+ * 12). The snapshot step is only taken when `config.versioning === true`.
151
+ */
152
+ versioning;
153
+ /**
154
+ * The provider-agnostic lifecycle engine. It delegates to the driver's native
155
+ * `lifecycle` capability when present and otherwise simulates rule evaluation
156
+ * over the driver primitives (`list`/`stat`/`get`/`put`/`delete`), measuring
157
+ * object age against the injected clock and applying each rule's action to a
158
+ * qualifying object exactly once, so `applyLifecycle` behaves identically
159
+ * across providers (Requirement 13).
160
+ */
161
+ lifecycle;
162
+ /**
163
+ * The provider-agnostic access controller. It resolves per-object
164
+ * {@link AccessLevel} decisions through the optional structural `config.auth`
165
+ * bridge, denying disallowed operations with an {@link AuthorizationError}
166
+ * before any persistence or read occurs (Requirement 11). When no auth bridge
167
+ * is configured it is a permissive no-op, so drivers that never configure
168
+ * access control behave exactly as before.
169
+ */
170
+ access;
171
+ /**
172
+ * The typed Events bridge, present only when `config.bridges?.events` is
173
+ * supplied. When defined, object mutations (`put` → uploaded/updated,
174
+ * `delete` → deleted, `move`/`rename` → moved, `restoreVersion` → restored)
175
+ * and applied lifecycle actions (`applyLifecycle`) publish the corresponding
176
+ * typed `storage.*` event through it (Requirements 13.4, 18.1, 18.2). Every
177
+ * publish is isolated so a failing events layer never breaks the storage
178
+ * operation. When no events bridge is configured this is `undefined` and event
179
+ * publication is a complete no-op.
180
+ */
181
+ events;
182
+ /**
183
+ * The typed Queue bridge, present only when `config.bridges?.queue` is
184
+ * supplied. When defined, heavy out-of-band work (thumbnail generation, virus
185
+ * scanning, OCR, PDF processing, transcoding, image optimization, archive
186
+ * creation) can be handed off through it (Requirement 17.1). Every dispatch is
187
+ * isolated so a failing queue never breaks the storage operation
188
+ * (Requirement 17.4). When no queue bridge is configured this is `undefined`
189
+ * and job dispatch is a complete no-op — operations proceed unaffected
190
+ * (Requirement 17.3).
191
+ */
192
+ queue;
193
+ /**
194
+ * The typed Realtime bridge, present only when `config.bridges?.realtime` is
195
+ * supplied. When defined, upload state transitions (`putStream`/`resumeUpload`
196
+ * → started/completed, and failed on error) broadcast the corresponding typed
197
+ * `upload.*` event through it (Requirement 19.1). Every broadcast is isolated
198
+ * so a failing realtime layer never breaks the upload (Requirement 19.3). When
199
+ * no realtime bridge is configured this is `undefined` and broadcasting is a
200
+ * complete no-op — uploads proceed unaffected (Requirement 19.3).
201
+ */
202
+ realtime;
203
+ /**
204
+ * The observability handle, present only when `config.metrics` or
205
+ * `config.health` is supplied. When defined, its telemetry sink is fed live as
206
+ * operations occur (uploads/downloads/bytes/failed/active/multipart/resumable
207
+ * counters + latency histogram — Requirement 23.2) and its `attach` registers
208
+ * the `storage` health check sourced from {@link probe} (Requirement 23.3).
209
+ * When neither registry is configured this is `undefined` and every telemetry
210
+ * call is a complete no-op. Metrics/health reuse the existing core registries
211
+ * only; no parallel system is introduced (Requirements 23.1, 23.4).
212
+ */
213
+ observability;
214
+ /**
215
+ * The live, mutable statistics tracked as operations occur. `stats()` returns
216
+ * an immutable snapshot of this state (Requirement 23.2). Kept independent of
217
+ * the optional metrics registry so `stats()` is always available even when no
218
+ * observability is wired.
219
+ */
220
+ statsState = {
221
+ uploads: 0,
222
+ downloads: 0,
223
+ bytesUploaded: 0,
224
+ bytesDownloaded: 0,
225
+ activeUploads: 0,
226
+ failedUploads: 0,
227
+ storageUsage: 0,
228
+ multipartUploads: 0,
229
+ resumableSessions: 0,
230
+ };
231
+ constructor(driver, config) {
232
+ this.driver = driver;
233
+ this.config = config;
234
+ this.events =
235
+ config.bridges?.events !== undefined
236
+ ? bridgeStorageEvents(config.bridges.events)
237
+ : undefined;
238
+ this.queue =
239
+ config.bridges?.queue !== undefined
240
+ ? bridgeStorageQueue(config.bridges.queue)
241
+ : undefined;
242
+ this.realtime =
243
+ config.bridges?.realtime !== undefined
244
+ ? bridgeStorageRealtime(config.bridges.realtime)
245
+ : undefined;
246
+ this.validation =
247
+ config.validation !== undefined ? new ValidationPipeline(config.validation) : undefined;
248
+ this.multipart = new MultipartManager(driver);
249
+ this.resumable = new ResumableManager(driver);
250
+ this.signedUrls = new SignedUrlService({
251
+ signingSecret: config.signingSecret,
252
+ clock: config.clock,
253
+ driver,
254
+ });
255
+ this.versioning = new VersioningManager(driver);
256
+ this.lifecycle = new LifecycleEngine({ driver, clock: config.clock });
257
+ this.access = new AccessController({ auth: config.auth });
258
+ // Wire observability only when a metrics and/or health registry is supplied.
259
+ // The handle registers its metrics/health against the existing core
260
+ // registries idempotently and its telemetry sink is fed live below; when
261
+ // neither is configured this stays undefined and telemetry is a no-op
262
+ // (Requirements 23.1, 23.3, 23.4).
263
+ this.observability =
264
+ config.metrics !== undefined || config.health !== undefined
265
+ ? registerStorageObservability({ metrics: config.metrics, health: config.health })
266
+ : undefined;
267
+ this.observability?.attach(this);
268
+ }
269
+ // ── Stats recording helpers (task 22.1) ─────────────────────────────────────
270
+ /**
271
+ * Record a successful upload of `bytes` taking `latencySeconds`: advances the
272
+ * upload / bytes-uploaded counters and the tracked storage usage, and feeds
273
+ * the live telemetry sink (Requirement 23.2).
274
+ */
275
+ recordUpload(bytes, latencySeconds) {
276
+ this.statsState.uploads += 1;
277
+ this.statsState.bytesUploaded += bytes;
278
+ this.statsState.storageUsage += bytes;
279
+ this.observability?.telemetry.onUpload?.(bytes, latencySeconds);
280
+ this.observability?.telemetry.onStorageUsage?.(this.statsState.storageUsage);
281
+ }
282
+ /**
283
+ * Record a successful download of `bytes` taking `latencySeconds`: advances
284
+ * the download / bytes-downloaded counters and feeds the telemetry sink.
285
+ */
286
+ recordDownload(bytes, latencySeconds) {
287
+ this.statsState.downloads += 1;
288
+ this.statsState.bytesDownloaded += bytes;
289
+ this.observability?.telemetry.onDownload?.(bytes, latencySeconds);
290
+ }
291
+ /** Record a failed upload attempt (Requirement 23.2). */
292
+ recordUploadFailure() {
293
+ this.statsState.failedUploads += 1;
294
+ this.observability?.telemetry.onUploadFailed?.();
295
+ }
296
+ /** Mark an in-flight (streamed/resumable) upload as started. */
297
+ recordActiveStart() {
298
+ this.statsState.activeUploads += 1;
299
+ this.observability?.telemetry.onActiveUploadsChange?.(this.statsState.activeUploads);
300
+ }
301
+ /** Mark an in-flight (streamed/resumable) upload as finished. */
302
+ recordActiveEnd() {
303
+ if (this.statsState.activeUploads > 0) {
304
+ this.statsState.activeUploads -= 1;
305
+ }
306
+ this.observability?.telemetry.onActiveUploadsChange?.(this.statsState.activeUploads);
307
+ }
308
+ /** Record a created multipart upload (Requirement 23.2). */
309
+ recordMultipart() {
310
+ this.statsState.multipartUploads += 1;
311
+ this.observability?.telemetry.onMultipartUpload?.();
312
+ }
313
+ /** Record a started resumable upload session (Requirement 23.2). */
314
+ recordResumable() {
315
+ this.statsState.resumableSessions += 1;
316
+ this.observability?.telemetry.onResumableSession?.();
317
+ }
318
+ /** Current epoch-ms reading used to measure operation latency. */
319
+ nowMs() {
320
+ return Date.now();
321
+ }
322
+ /**
323
+ * Run the access-control check (when an auth bridge is configured) for
324
+ * `operation` on `key` at the given `accessLevel`, throwing an
325
+ * {@link AuthorizationError} on denial so the guarded operation performs no
326
+ * persistence or read (Requirement 11.3). When no auth bridge is configured
327
+ * this is a no-op and every operation is permitted.
328
+ */
329
+ async authorizeAccess(key, operation, accessLevel, owner, tenant) {
330
+ await this.access.authorize({ key, operation, accessLevel, owner, tenant });
331
+ }
332
+ /**
333
+ * Run the validation pipeline (when configured) against `input` and throw a
334
+ * {@link ValidationError} on rejection. This is invoked before any call to
335
+ * `driver.put`, so a rejected upload never persists content (Requirement
336
+ * 9.3) and leaves no partial object behind (Requirement 9.4). When no
337
+ * validation is configured this is a no-op.
338
+ */
339
+ async runValidation(input) {
340
+ if (this.validation === undefined) {
341
+ return;
342
+ }
343
+ const result = await this.validation.validate(input);
344
+ if (!result.ok) {
345
+ throw new ValidationError(result.error ?? "upload rejected by validation", {
346
+ key: input.key,
347
+ });
348
+ }
349
+ }
350
+ // ── Object operations (task 5.2) ────────────────────────────────────────────
351
+ /**
352
+ * Persist `content` under `key` and return the resulting metadata
353
+ * (Requirement 4.1). A `string` payload is encoded as UTF-8 bytes before it
354
+ * reaches the driver so all providers store and hash the identical byte
355
+ * sequence; a `Uint8Array` is passed through untouched. Write-time metadata in
356
+ * `options` (content type, owner, tenant, access level, custom fields) is
357
+ * forwarded to the driver, which computes `etag`/`checksum`/`size`/timestamps.
358
+ */
359
+ async put(key, content, options) {
360
+ const bytes = typeof content === "string" ? encodeUtf8(content) : content;
361
+ const startedMs = this.nowMs();
362
+ try {
363
+ // Access control is the first gate: a denied write throws an
364
+ // AuthorizationError before any validation or persistence, so nothing is
365
+ // written (Requirement 11.3). No-op when no auth bridge is configured.
366
+ await this.authorizeAccess(key, "write", options?.accessLevel ?? DEFAULT_ACCESS_LEVEL, options?.owner, options?.tenant);
367
+ // Validate the fully-known size/contentType/checksum BEFORE any persistence
368
+ // so a rejection aborts the write with no partial object stored (Req 9.3/9.4).
369
+ await this.runValidation({
370
+ key,
371
+ size: bytes.byteLength,
372
+ contentType: options?.contentType,
373
+ checksum: sha256Hex(bytes),
374
+ metadata: options,
375
+ });
376
+ // When versioning is enabled, snapshot the current content (if any) BEFORE
377
+ // the overwrite so the prior Version is retained (Requirement 12.1). The
378
+ // snapshot never throws into this path: a versioning failure returns null
379
+ // and the overwrite proceeds without a Version (Requirement 12.5).
380
+ if (this.config.versioning === true) {
381
+ await this.versioning.snapshot(key);
382
+ }
383
+ // Determine whether this write creates a new object or overwrites an
384
+ // existing one so the correct event (`storage.uploaded` vs
385
+ // `storage.updated`) is published (Requirement 18.1). The existence probe is
386
+ // only performed when an events bridge is configured, so the default path is
387
+ // unchanged and adds no driver call.
388
+ const existedBefore = this.events !== undefined ? await this.driver.exists(key) : false;
389
+ // Surface the complete, typed metadata field set (Requirement 10.1) through
390
+ // the single source of truth so the shape is consistent across drivers.
391
+ const metadata = normalizeMetadata(await this.driver.put(key, bytes, options ?? {}));
392
+ // Publish uploaded/updated after a successful persist. The bridge never
393
+ // throws into this path (Requirement 18.1, 18.2).
394
+ if (this.events !== undefined) {
395
+ if (existedBefore) {
396
+ this.events.updated(metadata);
397
+ }
398
+ else {
399
+ this.events.uploaded(metadata);
400
+ }
401
+ }
402
+ // Record the successful upload for stats/metrics (Requirement 23.2).
403
+ this.recordUpload(bytes.byteLength, (this.nowMs() - startedMs) / 1000);
404
+ return metadata;
405
+ }
406
+ catch (error) {
407
+ // Any pre-persistence rejection (auth/validation) or driver failure counts
408
+ // as a failed upload (Requirement 23.2). The error propagates unchanged.
409
+ this.recordUploadFailure();
410
+ throw error;
411
+ }
412
+ }
413
+ /**
414
+ * Read the object at `key`, converting the driver's discriminated
415
+ * {@link MaybeObject} into the facade {@link GetResult} shape (Requirement
416
+ * 4.2). A present object yields `{ found: true, bytes, metadata }` with the
417
+ * stored bytes returned unchanged; an absent one yields `{ found: false }`
418
+ * without throwing.
419
+ */
420
+ async get(key) {
421
+ // When access control is enforced, resolve the object's access level from
422
+ // its metadata and authorize the read BEFORE returning any bytes; a denied
423
+ // read throws an AuthorizationError and no content is read (Requirement
424
+ // 11.3). A `public` object is readable without authentication unless the
425
+ // configured bridge blocks it (Requirement 11.4). This lookup is skipped
426
+ // entirely when no auth bridge is configured, leaving the default path
427
+ // unchanged.
428
+ if (this.access.enforced) {
429
+ const metadata = await this.driver.stat(key);
430
+ if (metadata !== null) {
431
+ await this.authorizeAccess(key, "read", metadata.accessLevel, metadata.owner, metadata.tenant);
432
+ }
433
+ }
434
+ const startedMs = this.nowMs();
435
+ const result = await this.driver.get(key);
436
+ if (result.found) {
437
+ // Record the successful download for stats/metrics (Requirement 23.2).
438
+ this.recordDownload(result.bytes.byteLength, (this.nowMs() - startedMs) / 1000);
439
+ return { found: true, bytes: result.bytes, metadata: normalizeMetadata(result.metadata) };
440
+ }
441
+ return { found: false };
442
+ }
443
+ /** Report whether an object is stored under `key` (Requirement 4.3). */
444
+ async exists(key) {
445
+ return this.driver.exists(key);
446
+ }
447
+ /**
448
+ * Remove the object at `key` so a subsequent `exists` returns false
449
+ * (Requirement 4.4). Deleting a missing key is a no-op at the driver level.
450
+ */
451
+ async delete(key) {
452
+ // Authorize the delete against the object's access level (Requirement
453
+ // 11.3). Skipped when no auth bridge is configured; a missing object has no
454
+ // access level to enforce and the driver delete remains a no-op.
455
+ if (this.access.enforced) {
456
+ const metadata = await this.driver.stat(key);
457
+ if (metadata !== null) {
458
+ await this.authorizeAccess(key, "delete", metadata.accessLevel, metadata.owner, metadata.tenant);
459
+ }
460
+ }
461
+ await this.driver.delete(key);
462
+ // Publish `storage.deleted` after the removal (Requirement 18.1). The bridge
463
+ // never throws into this path.
464
+ if (this.events !== undefined) {
465
+ this.events.deleted(key);
466
+ }
467
+ }
468
+ /**
469
+ * Copy the object at `source` to `destination` (Requirement 4.5). The copy is
470
+ * **non-mutating**: the source is read and its content plus metadata are
471
+ * written to the destination, and the source object is never touched. When
472
+ * `source` does not exist, no write occurs and a not-found result
473
+ * (`{ copied: false }`) is returned without throwing (Requirement 4.6).
474
+ */
475
+ async copy(source, destination) {
476
+ const result = await this.driver.get(source);
477
+ if (!result.found) {
478
+ return { copied: false };
479
+ }
480
+ const metadata = normalizeMetadata(await this.driver.put(destination, result.bytes, toWriteMetadata(result.metadata)));
481
+ return { copied: true, metadata };
482
+ }
483
+ /**
484
+ * Move the object at `source` to `destination` (Requirements 4.7). The source
485
+ * content is written to the destination and then the source object is
486
+ * removed. When `source` does not exist, no operation is performed and a
487
+ * not-found result (`{ moved: false }`) is returned without throwing.
488
+ */
489
+ async move(source, destination) {
490
+ const result = await this.driver.get(source);
491
+ if (!result.found) {
492
+ return { moved: false };
493
+ }
494
+ const metadata = normalizeMetadata(await this.driver.put(destination, result.bytes, toWriteMetadata(result.metadata)));
495
+ await this.driver.delete(source);
496
+ // Publish `storage.moved` for the relocated object (Requirement 18.1); this
497
+ // also covers `rename`, which delegates here. The bridge never throws into
498
+ // this path.
499
+ if (this.events !== undefined) {
500
+ this.events.moved(metadata);
501
+ }
502
+ return { moved: true, metadata };
503
+ }
504
+ /**
505
+ * Rename the object at `key` to `newKey` (Requirement 4.8). This has the same
506
+ * semantics as {@link move}: the content becomes available under `newKey` and
507
+ * the old key is removed; a missing source yields `{ moved: false }` without
508
+ * throwing.
509
+ */
510
+ async rename(key, newKey) {
511
+ return this.move(key, newKey);
512
+ }
513
+ /**
514
+ * Return the keys (with size/timestamp) of objects whose keys begin with
515
+ * `prefix`, delegating directly to the driver (Requirement 4.9).
516
+ */
517
+ async list(prefix, options) {
518
+ return this.driver.list(prefix, options);
519
+ }
520
+ /**
521
+ * Return the metadata for `key` without its content, or `null` if absent
522
+ * (Requirement 4.10).
523
+ */
524
+ async stat(key) {
525
+ const metadata = await this.driver.stat(key);
526
+ return metadata === null ? null : normalizeMetadata(metadata);
527
+ }
528
+ // ── Streaming (task 7.1) ─────────────────────────────────────────────────────
529
+ /**
530
+ * Persist the content read from a Node {@link NodeReadable} under `key` and
531
+ * return the resulting metadata (Requirement 5.1). The stream is forwarded
532
+ * directly to the driver's streaming primitive along with the write-time
533
+ * metadata in `options` (content type, owner, tenant, access level, custom
534
+ * fields); the driver pipes the content through to storage and never buffers
535
+ * the complete object in memory (Requirement 5.3), which is what lets large
536
+ * files transfer without loading fully into memory.
537
+ */
538
+ async putStream(key, stream, options) {
539
+ // Access control gates the streamed write before any bytes reach the driver
540
+ // (Requirement 11.3). No-op when no auth bridge is configured.
541
+ await this.authorizeAccess(key, "write", options?.accessLevel ?? DEFAULT_ACCESS_LEVEL, options?.owner, options?.tenant);
542
+ // Broadcast the upload state transitions through the realtime bridge when
543
+ // configured (Requirement 19.1). Every broadcast is isolated so it never
544
+ // breaks the upload path (Requirement 19.3).
545
+ const startedMs = this.nowMs();
546
+ this.recordActiveStart();
547
+ this.realtime?.started(key);
548
+ try {
549
+ // With no validation configured, stream straight through the driver so
550
+ // large files never fully buffer (Requirement 5.3).
551
+ if (this.validation === undefined) {
552
+ const metadata = normalizeMetadata(await this.driver.putStream(key, stream, options ?? {}));
553
+ this.realtime?.completed(key);
554
+ this.recordUpload(metadata.size, (this.nowMs() - startedMs) / 1000);
555
+ return metadata;
556
+ }
557
+ // With validation configured, size and checksum can only be known once the
558
+ // full stream is collected. We buffer the stream, validate the complete
559
+ // input, and only then persist via `driver.put`. Because the driver is not
560
+ // touched until validation passes, a rejection leaves no partial object
561
+ // stored (Requirement 9.4); persistence still happens as a single atomic
562
+ // write on success (Requirement 9.3).
563
+ const bytes = await collectStream(stream);
564
+ await this.runValidation({
565
+ key,
566
+ size: bytes.byteLength,
567
+ contentType: options?.contentType,
568
+ checksum: sha256Hex(bytes),
569
+ metadata: options,
570
+ });
571
+ const metadata = normalizeMetadata(await this.driver.put(key, bytes, options ?? {}));
572
+ this.realtime?.completed(key);
573
+ this.recordUpload(metadata.size, (this.nowMs() - startedMs) / 1000);
574
+ return metadata;
575
+ }
576
+ catch (error) {
577
+ this.realtime?.failed(key, error instanceof Error ? error.message : String(error));
578
+ this.recordUploadFailure();
579
+ throw error;
580
+ }
581
+ finally {
582
+ // The streamed upload is no longer in flight regardless of outcome.
583
+ this.recordActiveEnd();
584
+ }
585
+ }
586
+ /**
587
+ * Return a Node {@link NodeReadable} of the content stored at `key`
588
+ * (Requirement 5.2). The stream is produced by the driver's streaming
589
+ * primitive so bytes are pulled incrementally rather than buffered in full
590
+ * (Requirement 5.3), and piping it to a Node Writable delivers the stored
591
+ * bytes unchanged (Requirement 5.4). When `key` does not exist the driver
592
+ * throws {@link NotFoundError}, which propagates unchanged to the caller
593
+ * (Requirement 5.5).
594
+ */
595
+ async getStream(key) {
596
+ // Authorize the streamed read against the object's access level before
597
+ // producing any stream (Requirement 11.3). Skipped entirely when no auth
598
+ // bridge is configured so the default path is unchanged.
599
+ if (this.access.enforced) {
600
+ const metadata = await this.driver.stat(key);
601
+ if (metadata !== null) {
602
+ await this.authorizeAccess(key, "read", metadata.accessLevel, metadata.owner, metadata.tenant);
603
+ }
604
+ }
605
+ return this.driver.getStream(key);
606
+ }
607
+ // ── Multipart (task 10.1) ────────────────────────────────────────────────────
608
+ /**
609
+ * Begin a multipart upload for `key` and return its upload identifier
610
+ * (Requirement 6.1). Write-time metadata in `options` is captured so the
611
+ * assembled object is written with the intended content type / ownership /
612
+ * access level / custom fields at completion.
613
+ */
614
+ createMultipartUpload(key, options) {
615
+ // Count the created multipart upload for stats/metrics (Requirement 23.2).
616
+ this.recordMultipart();
617
+ return this.multipart.create(key, options ?? {});
618
+ }
619
+ /**
620
+ * Persist a single part for `uploadId` and return its {@link StoredPart}
621
+ * descriptor (Requirement 6.2).
622
+ */
623
+ uploadPart(uploadId, partNumber, content) {
624
+ return this.multipart.uploadPart(uploadId, partNumber, content);
625
+ }
626
+ /**
627
+ * Assemble the supplied ordered `parts` into the final object and return its
628
+ * metadata; the content equals the concatenation of the parts in order,
629
+ * equivalent to a single `put` of that concatenation (Requirement 6.3).
630
+ */
631
+ completeMultipartUpload(uploadId, parts) {
632
+ return this.multipart.complete(uploadId, parts);
633
+ }
634
+ /**
635
+ * Discard all uploaded parts for `uploadId` and create no completed object
636
+ * (Requirement 6.4).
637
+ */
638
+ abortMultipartUpload(uploadId) {
639
+ return this.multipart.abort(uploadId);
640
+ }
641
+ // ── Resumable (task 11.1) ────────────────────────────────────────────────────
642
+ /**
643
+ * Create a resumable upload session for `key` and return its session id
644
+ * (Requirement 7.1). Write-time metadata in `options` is captured so the
645
+ * object created on completion carries the intended content type / ownership /
646
+ * access level / custom fields.
647
+ */
648
+ startUpload(key, options) {
649
+ // Count the started resumable session for stats/metrics (Requirement 23.2).
650
+ this.recordResumable();
651
+ return this.resumable.start(key, options ?? {});
652
+ }
653
+ /**
654
+ * Continue the session `sessionId` from its last persisted offset using the
655
+ * full content carried by `stream`, creating the final object on completion
656
+ * and returning its metadata (Requirements 7.2, 7.3). The completed object is
657
+ * byte-identical to an equivalent uninterrupted upload.
658
+ */
659
+ async resumeUpload(sessionId, stream) {
660
+ // Broadcast the resumed upload's state transitions through the realtime
661
+ // bridge when configured (Requirement 19.1); every broadcast is isolated so
662
+ // it never breaks the upload path (Requirement 19.3).
663
+ const startedMs = this.nowMs();
664
+ this.recordActiveStart();
665
+ this.realtime?.started(sessionId);
666
+ try {
667
+ const metadata = await this.resumable.resume(sessionId, stream);
668
+ this.realtime?.completed(metadata.key);
669
+ this.recordUpload(metadata.size, (this.nowMs() - startedMs) / 1000);
670
+ return metadata;
671
+ }
672
+ catch (error) {
673
+ this.realtime?.failed(sessionId, error instanceof Error ? error.message : String(error));
674
+ this.recordUploadFailure();
675
+ throw error;
676
+ }
677
+ finally {
678
+ // The resumed upload is no longer in flight regardless of outcome.
679
+ this.recordActiveEnd();
680
+ }
681
+ }
682
+ /**
683
+ * Discard the session `sessionId` without creating an object (Requirement
684
+ * 7.4), unless it is already completing, in which case the upload is allowed
685
+ * to finish and the object is created (Requirement 7.5).
686
+ */
687
+ cancelUpload(sessionId) {
688
+ return this.resumable.cancel(sessionId);
689
+ }
690
+ // ── Signed URLs (task 13.1) ──────────────────────────────────────────────────
691
+ /**
692
+ * Mint a URL authorizing exactly the operation `op` on `key` (Requirement
693
+ * 8.1), carrying the accepted options for expiration, request headers, content
694
+ * type, maximum size, and custom metadata (Requirement 8.2). Delegates to the
695
+ * driver's native signed-URL capability when present and otherwise mints an
696
+ * HMAC-signed URL over `(key, op, expiry)` using `config.signingSecret`
697
+ * (throwing a {@link StorageConfigError} when that secret is absent).
698
+ */
699
+ signedUrl(key, op, options) {
700
+ return this.signedUrls.sign(key, op, options);
701
+ }
702
+ // ── Versioning (task 14.1) ───────────────────────────────────────────────────
703
+ /**
704
+ * Return the {@link VersionInfo} descriptors of the retained Versions for
705
+ * `key` (Requirement 12.2), delegating to the versioning manager (native
706
+ * capability when present, otherwise the reserved-key simulation).
707
+ */
708
+ listVersions(key) {
709
+ return this.versioning.listVersions(key);
710
+ }
711
+ /**
712
+ * Make the content of the Version identified by `versionId` the current
713
+ * content of `key` and return the resulting metadata (Requirement 12.3).
714
+ */
715
+ async restoreVersion(key, versionId) {
716
+ const metadata = await this.versioning.restoreVersion(key, versionId);
717
+ // Publish `storage.restored` after the restore succeeds (Requirement 18.1).
718
+ // The bridge never throws into this path.
719
+ if (this.events !== undefined) {
720
+ this.events.restored(metadata);
721
+ }
722
+ return metadata;
723
+ }
724
+ /**
725
+ * Remove exactly the Version identified by `versionId` for `key` while
726
+ * retaining the remaining Versions (Requirement 12.4).
727
+ */
728
+ deleteVersion(key, versionId) {
729
+ return this.versioning.deleteVersion(key, versionId);
730
+ }
731
+ // ── Lifecycle (task 15.1) ────────────────────────────────────────────────────
732
+ /**
733
+ * Evaluate the lifecycle `rule` and apply its action to every qualifying
734
+ * object, returning one {@link LifecycleOutcome} per actioned object
735
+ * (Requirements 13.1, 13.2). Object age is measured against the configured
736
+ * clock, and each qualifying object is actioned exactly once — a repeated
737
+ * evaluation produces no further action on an already-actioned object.
738
+ * Delegates to the driver's native `lifecycle` capability when present and
739
+ * otherwise simulates the rule over the driver primitives (Requirement 13.3).
740
+ * When an events bridge is configured, each applied action publishes its
741
+ * corresponding typed lifecycle event through the bridge (Requirement 13.4);
742
+ * publication is isolated so a failing events layer never affects the returned
743
+ * outcomes.
744
+ */
745
+ async applyLifecycle(rule) {
746
+ const outcomes = await this.lifecycle.apply(rule);
747
+ if (this.events !== undefined) {
748
+ for (const outcome of outcomes) {
749
+ this.events.lifecycle(outcome);
750
+ }
751
+ }
752
+ return outcomes;
753
+ }
754
+ // ── Image processing (task 19.1) ─────────────────────────────────────────────
755
+ /**
756
+ * The lazily-constructed image processor exposed as `storage.images`. Built on
757
+ * first access and reused thereafter so its transformation cache is shared
758
+ * across calls on the same facade (Requirement 14.3).
759
+ */
760
+ imageProcessor;
761
+ /**
762
+ * The image processing surface (`transform`) implemented over the driver and
763
+ * the optional structural `config.imageCodec`. It supports the
764
+ * resize/crop/rotate/fit/thumbnail/compress transformations and webp/avif/png/
765
+ * jpeg output formats (Requirements 14.1, 14.2), caches identical
766
+ * transformations (Requirement 14.3), and rejects a non-image source with an
767
+ * {@link UnsupportedImageError} without modifying the source object
768
+ * (Requirement 14.4). Lazily constructed and cached.
769
+ */
770
+ get images() {
771
+ if (this.imageProcessor === undefined) {
772
+ this.imageProcessor = new StorageImageProcessor(this.driver, this.config.imageCodec);
773
+ }
774
+ return this.imageProcessor;
775
+ }
776
+ // ── Directory API (task 17.1) ────────────────────────────────────────────────
777
+ /**
778
+ * The lazily-constructed Directory API over the driver's flat key space
779
+ * (Requirement 15). Built on first access to `storage.directory` and reused
780
+ * thereafter so directory operations share one instance per facade.
781
+ */
782
+ directoryApi;
783
+ /**
784
+ * The directory-style API (`mkdir`/`listDirectory`/`removeDirectory`/`walk`)
785
+ * implemented entirely over the driver's flat key space using `/`-delimited
786
+ * prefixes, so it behaves identically across every provider including
787
+ * prefix-only cloud stores (Requirement 15). Lazily constructed and cached.
788
+ */
789
+ get directory() {
790
+ if (this.directoryApi === undefined) {
791
+ this.directoryApi = new StorageDirectoryApi(this.driver);
792
+ }
793
+ return this.directoryApi;
794
+ }
795
+ // ── Search (task 18.1) ───────────────────────────────────────────────────────
796
+ /**
797
+ * Return the stored objects that satisfy **every** supplied filter
798
+ * (Requirement 16.2), evaluated over the driver's `list` + `stat` primitives.
799
+ * Filtering is conjunctive across prefix, content type, owner, tenant, size
800
+ * range, updated-time range, and custom metadata (Requirement 16.1); when no
801
+ * object matches, an empty result set is returned (Requirement 16.3).
802
+ */
803
+ search(filters) {
804
+ return searchObjects(this.driver, filters);
805
+ }
806
+ // ── Observability (task 22.1) ────────────────────────────────────────────────
807
+ /**
808
+ * Return an immutable snapshot of the live operation statistics tracked as
809
+ * uploads/downloads/multipart/resumable operations occur: uploads, downloads,
810
+ * bytes uploaded/downloaded, active uploads, failed uploads, storage usage,
811
+ * multipart uploads, and resumable sessions (Requirement 23.2). Always
812
+ * available regardless of whether a metrics registry is configured.
813
+ */
814
+ stats() {
815
+ return {
816
+ uploads: this.statsState.uploads,
817
+ downloads: this.statsState.downloads,
818
+ bytesUploaded: this.statsState.bytesUploaded,
819
+ bytesDownloaded: this.statsState.bytesDownloaded,
820
+ activeUploads: this.statsState.activeUploads,
821
+ failedUploads: this.statsState.failedUploads,
822
+ storageUsage: this.statsState.storageUsage,
823
+ multipartUploads: this.statsState.multipartUploads,
824
+ resumableSessions: this.statsState.resumableSessions,
825
+ };
826
+ }
827
+ /**
828
+ * Return the driver's best-effort connectivity/quota probe used by the
829
+ * registered health check (Requirement 23.3). Delegates to the driver's
830
+ * optional `probe()` when present; otherwise returns a sensible all-available
831
+ * default (the zero-dependency `memory`/`local` drivers are always reachable,
832
+ * readable, writable, and unbounded).
833
+ */
834
+ async probe() {
835
+ if (this.driver.probe !== undefined) {
836
+ return this.driver.probe();
837
+ }
838
+ return { connectivity: true, writable: true, readable: true, quotaAvailable: true };
839
+ }
840
+ /**
841
+ * Release any resources held by the facade. Stops the observability handle's
842
+ * refresh timer (when observability is wired); the driver and bridges hold no
843
+ * timers of their own.
844
+ */
845
+ async close() {
846
+ this.observability?.close();
847
+ }
848
+ }
849
+ /** Encode a string payload as UTF-8 bytes for storage. */
850
+ function encodeUtf8(value) {
851
+ return new TextEncoder().encode(value);
852
+ }
853
+ /**
854
+ * Compute the lowercase sha-256 hex digest of `bytes`, matching the checksum
855
+ * scheme the drivers compute at write time so the value passed to the
856
+ * validation pipeline is identical to the object's stored checksum.
857
+ */
858
+ function sha256Hex(bytes) {
859
+ return createHash("sha256").update(bytes).digest("hex");
860
+ }
861
+ /**
862
+ * Drain a Node {@link NodeReadable} fully into a single {@link Uint8Array}. Used
863
+ * by `putStream` only when a validation pipeline is configured, so size and
864
+ * checksum can be computed and validated before any content is persisted. Each
865
+ * chunk is normalized to a `Buffer` before concatenation.
866
+ */
867
+ async function collectStream(stream) {
868
+ const chunks = [];
869
+ for await (const chunk of stream) {
870
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
871
+ }
872
+ return new Uint8Array(Buffer.concat(chunks));
873
+ }
874
+ // ── Public entry point ─────────────────────────────────────────────────────
875
+ /**
876
+ * Construct a {@link Storage} instance backed by the driver selected in `config`
877
+ * (Requirements 1.1, 1.2). The returned facade exposes an identical public
878
+ * method set for every provider (Requirement 1.4).
879
+ *
880
+ * Provider selection:
881
+ * - `config.driver`, when supplied, is used directly (cloud providers imported
882
+ * from `@streetjs/storage/*` submodules).
883
+ * - Otherwise `config.provider` selects a built-in zero-dependency driver
884
+ * (`memory` or `local`; `local` requires `config.root`).
885
+ *
886
+ * @throws StorageConfigError when the provider name is unknown and no `driver`
887
+ * is supplied — no {@link Storage} instance is produced (Requirement 1.5). If
888
+ * constructing that error object itself fails unexpectedly, the internal
889
+ * failure surfaces as a thrown error and, likewise, no instance is produced
890
+ * (Requirement 1.6).
891
+ *
892
+ * @typeParam T - Optional per-application custom metadata map.
893
+ */
894
+ export function createStorage(config) {
895
+ const driver = resolveDriver(config);
896
+ return new StorageFacade(driver, config);
897
+ }
898
+ //# sourceMappingURL=facade.js.map