nextly 0.0.1 → 0.0.2-alpha.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 (268) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +122 -0
  3. package/dist/_dts-chunks/collections-handler.d-DjgO74Wt.d.ts +20540 -0
  4. package/dist/_dts-chunks/config.d-DNwsDnjs.d.ts +2589 -0
  5. package/dist/_dts-chunks/define-component.d-BUgTHmt3.d.ts +1149 -0
  6. package/dist/_dts-chunks/image-processor.d-OO1PmMrv.d.ts +335 -0
  7. package/dist/_dts-chunks/index.d-axCAzZ7m.d.ts +17842 -0
  8. package/dist/_dts-chunks/media.d-DjDOZo4B.d.ts +117 -0
  9. package/dist/_dts-chunks/on-error.d-CHIKWNxd.d.ts +38 -0
  10. package/dist/_dts-chunks/storage.d-BUhQ2we_.d.ts +404 -0
  11. package/dist/actions/index.d.ts +239 -0
  12. package/dist/actions/index.mjs +281 -0
  13. package/dist/api/auth-state.d.ts +5 -0
  14. package/dist/api/auth-state.mjs +131 -0
  15. package/dist/api/collections-schema-detail.d.ts +56 -0
  16. package/dist/api/collections-schema-detail.mjs +244 -0
  17. package/dist/api/collections-schema-export.d.ts +56 -0
  18. package/dist/api/collections-schema-export.mjs +129 -0
  19. package/dist/api/collections-schema.d.ts +59 -0
  20. package/dist/api/collections-schema.mjs +207 -0
  21. package/dist/api/components-detail.d.ts +50 -0
  22. package/dist/api/components-detail.mjs +132 -0
  23. package/dist/api/components.d.ts +69 -0
  24. package/dist/api/components.mjs +144 -0
  25. package/dist/api/email-providers-default.d.ts +40 -0
  26. package/dist/api/email-providers-default.mjs +75 -0
  27. package/dist/api/email-providers-detail.d.ts +81 -0
  28. package/dist/api/email-providers-detail.mjs +109 -0
  29. package/dist/api/email-providers-test.d.ts +43 -0
  30. package/dist/api/email-providers-test.mjs +114 -0
  31. package/dist/api/email-providers.d.ts +69 -0
  32. package/dist/api/email-providers.mjs +110 -0
  33. package/dist/api/email-send-template.d.ts +41 -0
  34. package/dist/api/email-send-template.mjs +58 -0
  35. package/dist/api/email-send.d.ts +42 -0
  36. package/dist/api/email-send.mjs +58 -0
  37. package/dist/api/email-templates-detail.d.ts +74 -0
  38. package/dist/api/email-templates-detail.mjs +112 -0
  39. package/dist/api/email-templates-layout.d.ts +55 -0
  40. package/dist/api/email-templates-layout.mjs +92 -0
  41. package/dist/api/email-templates-preview.d.ts +48 -0
  42. package/dist/api/email-templates-preview.mjs +93 -0
  43. package/dist/api/email-templates.d.ts +61 -0
  44. package/dist/api/email-templates.mjs +118 -0
  45. package/dist/api/health.d.ts +68 -0
  46. package/dist/api/health.mjs +67 -0
  47. package/dist/api/index.d.ts +54 -0
  48. package/dist/api/index.mjs +16 -0
  49. package/dist/api/media-bulk.d.ts +74 -0
  50. package/dist/api/media-bulk.mjs +196 -0
  51. package/dist/api/media-folders.d.ts +112 -0
  52. package/dist/api/media-folders.mjs +187 -0
  53. package/dist/api/media-handlers.d.ts +102 -0
  54. package/dist/api/media-handlers.mjs +437 -0
  55. package/dist/api/media.d.ts +117 -0
  56. package/dist/api/media.mjs +242 -0
  57. package/dist/api/singles-detail.d.ts +87 -0
  58. package/dist/api/singles-detail.mjs +170 -0
  59. package/dist/api/singles-schema-detail.d.ts +54 -0
  60. package/dist/api/singles-schema-detail.mjs +182 -0
  61. package/dist/api/singles.d.ts +34 -0
  62. package/dist/api/singles.mjs +94 -0
  63. package/dist/api/storage-upload-url.d.ts +48 -0
  64. package/dist/api/storage-upload-url.mjs +202 -0
  65. package/dist/api/uploads.d.ts +109 -0
  66. package/dist/api/uploads.mjs +359 -0
  67. package/dist/auth/index.d.ts +425 -0
  68. package/dist/auth/index.mjs +199 -0
  69. package/dist/boot-apply-PQSYLDIN.mjs +7 -0
  70. package/dist/chunk-2OALJTK6.mjs +489 -0
  71. package/dist/chunk-2Q2SX2CS.mjs +365 -0
  72. package/dist/chunk-2TFX4ND3.mjs +13 -0
  73. package/dist/chunk-2TWPDSYD.mjs +87 -0
  74. package/dist/chunk-2W3DVD7S.mjs +647 -0
  75. package/dist/chunk-2ZFKXPQM.mjs +88 -0
  76. package/dist/chunk-3FA7FKAV.mjs +832 -0
  77. package/dist/chunk-3NZ2KMBL.mjs +58 -0
  78. package/dist/chunk-4MJLT6PZ.mjs +0 -0
  79. package/dist/chunk-56WO4WX7.mjs +0 -0
  80. package/dist/chunk-5APFUGAD.mjs +89 -0
  81. package/dist/chunk-5HMZ644B.mjs +108 -0
  82. package/dist/chunk-67GXH6PR.mjs +32 -0
  83. package/dist/chunk-6JNEPWRW.mjs +14368 -0
  84. package/dist/chunk-6NFHQIJD.mjs +45 -0
  85. package/dist/chunk-7P6ASYW6.mjs +9 -0
  86. package/dist/chunk-A3WPLSDT.mjs +1364 -0
  87. package/dist/chunk-AGJ6F2T3.mjs +144 -0
  88. package/dist/chunk-AK6Z23OX.mjs +1464 -0
  89. package/dist/chunk-APKKRD2G.mjs +102 -0
  90. package/dist/chunk-B2GV2BWH.mjs +73 -0
  91. package/dist/chunk-D5HQBNUB.mjs +74 -0
  92. package/dist/chunk-DNNG377Z.mjs +204 -0
  93. package/dist/chunk-DP3G27G5.mjs +135 -0
  94. package/dist/chunk-DV6WVX2Q.mjs +0 -0
  95. package/dist/chunk-DXGGXIUZ.mjs +57 -0
  96. package/dist/chunk-EGXBZCGC.mjs +943 -0
  97. package/dist/chunk-ERCNLX3V.mjs +176 -0
  98. package/dist/chunk-FQULBZ53.mjs +850 -0
  99. package/dist/chunk-G2AA4QLC.mjs +262 -0
  100. package/dist/chunk-GDBJ5JCU.mjs +488 -0
  101. package/dist/chunk-GJNSJU4S.mjs +19 -0
  102. package/dist/chunk-GZ6DCQKC.mjs +69 -0
  103. package/dist/chunk-H26B4FYG.mjs +167 -0
  104. package/dist/chunk-I4JMR3UR.mjs +21 -0
  105. package/dist/chunk-INV7QKLG.mjs +508 -0
  106. package/dist/chunk-IUDOC7N7.mjs +46 -0
  107. package/dist/chunk-IZWPRDC3.mjs +206 -0
  108. package/dist/chunk-KIMNCZGV.mjs +15 -0
  109. package/dist/chunk-L6HW2DA7.mjs +15 -0
  110. package/dist/chunk-LAZXX4HR.mjs +100 -0
  111. package/dist/chunk-LDKCUMHK.mjs +95 -0
  112. package/dist/chunk-LRXMECUA.mjs +0 -0
  113. package/dist/chunk-M52VMPGA.mjs +119 -0
  114. package/dist/chunk-MGUWEEI6.mjs +160 -0
  115. package/dist/chunk-NRUWQ5Z7.mjs +419 -0
  116. package/dist/chunk-NSEFNNU4.mjs +25360 -0
  117. package/dist/chunk-NTHVDFGO.mjs +138 -0
  118. package/dist/chunk-O3QHXMOX.mjs +3166 -0
  119. package/dist/chunk-P7NH2OSC.mjs +2605 -0
  120. package/dist/chunk-PKMABBB5.mjs +184 -0
  121. package/dist/chunk-PWS6XGJK.mjs +76 -0
  122. package/dist/chunk-R6JJQHFC.mjs +20 -0
  123. package/dist/chunk-RJLLGGPG.mjs +0 -0
  124. package/dist/chunk-SBACDPNX.mjs +689 -0
  125. package/dist/chunk-TO5AFLVQ.mjs +124 -0
  126. package/dist/chunk-TS7GHTG2.mjs +5436 -0
  127. package/dist/chunk-UJ2IMJ4W.mjs +133 -0
  128. package/dist/chunk-UOP63Q54.mjs +102 -0
  129. package/dist/chunk-UUOFWCM6.mjs +78 -0
  130. package/dist/chunk-V4EQTOA4.mjs +893 -0
  131. package/dist/chunk-VJ66NCL4.mjs +193 -0
  132. package/dist/chunk-VQJQHVEV.mjs +29 -0
  133. package/dist/chunk-VTJADRO3.mjs +141 -0
  134. package/dist/chunk-VWF3JO32.mjs +0 -0
  135. package/dist/chunk-W4MGXIRR.mjs +27 -0
  136. package/dist/chunk-W5KKPZT5.mjs +1204 -0
  137. package/dist/chunk-WD34YQ6T.mjs +381 -0
  138. package/dist/chunk-WZBYMYVW.mjs +14 -0
  139. package/dist/chunk-X23WKS3Z.mjs +50 -0
  140. package/dist/chunk-X7TXCYYN.mjs +6496 -0
  141. package/dist/chunk-XGI4EMS3.mjs +140 -0
  142. package/dist/chunk-XZKLBMN6.mjs +1153 -0
  143. package/dist/chunk-YB7INWPY.mjs +0 -0
  144. package/dist/chunk-YV4Y7SDL.mjs +83 -0
  145. package/dist/chunk-YZNBLFIW.mjs +1688 -0
  146. package/dist/chunk-YZZCTONM.mjs +263 -0
  147. package/dist/chunk-ZE6A3FYH.mjs +289 -0
  148. package/dist/cli/nextly.mjs +68 -0
  149. package/dist/cli/utils/index.d.ts +449 -0
  150. package/dist/cli/utils/index.mjs +49 -0
  151. package/dist/component-schema-service-5577KVW6.mjs +11 -0
  152. package/dist/config-loader-23YEMC3Z.mjs +23 -0
  153. package/dist/config.d.ts +44 -0
  154. package/dist/config.mjs +109 -0
  155. package/dist/container-ORGFGYSZ.mjs +9 -0
  156. package/dist/database/index.d.ts +12 -0
  157. package/dist/database/index.mjs +40 -0
  158. package/dist/database/seeders/index.d.ts +93 -0
  159. package/dist/database/seeders/index.mjs +47 -0
  160. package/dist/db-sync-demote-LJGKLB3S.mjs +117 -0
  161. package/dist/db-sync-promote-B26VSYQF.mjs +113 -0
  162. package/dist/dev-reload-broadcaster-B73IQ53V.mjs +25 -0
  163. package/dist/dist-M2NOU37V.mjs +19 -0
  164. package/dist/drizzle-kit-lazy-D2M2PXR2.mjs +13 -0
  165. package/dist/dynamic-collection-schema-service-IEXTPIZ7.mjs +8 -0
  166. package/dist/errors/index.d.ts +159 -0
  167. package/dist/errors/index.mjs +10 -0
  168. package/dist/factory-IWMBKUJM.mjs +15 -0
  169. package/dist/first-run-QIVKWJIF.mjs +63 -0
  170. package/dist/fresh-push-NR67DC3R.mjs +8 -0
  171. package/dist/index.d.ts +4175 -0
  172. package/dist/index.mjs +1336 -0
  173. package/dist/local-plugin-PTET4NAT.mjs +7 -0
  174. package/dist/logger-NU46DXNY.mjs +15 -0
  175. package/dist/logger-YE4TC7ZN.mjs +9 -0
  176. package/dist/migration-journal-EP532Y4L.mjs +139 -0
  177. package/dist/migrations/mysql/0000_eager_sentry.sql +174 -0
  178. package/dist/migrations/mysql/0001_soft_giant_girl.sql +27 -0
  179. package/dist/migrations/mysql/0002_media_table.sql +24 -0
  180. package/dist/migrations/mysql/0003_dynamic_singles.sql +37 -0
  181. package/dist/migrations/mysql/0004_dynamic_components.sql +35 -0
  182. package/dist/migrations/mysql/0005_user_management_tables.sql +92 -0
  183. package/dist/migrations/mysql/0006_api_keys.sql +36 -0
  184. package/dist/migrations/mysql/0007_general_settings.sql +20 -0
  185. package/dist/migrations/mysql/0008_site_settings_logo_url.sql +9 -0
  186. package/dist/migrations/mysql/0009_activity_log.sql +30 -0
  187. package/dist/migrations/mysql/0010_site_settings_sidebar.sql +13 -0
  188. package/dist/migrations/mysql/0011_missing_tables_and_columns.sql +54 -0
  189. package/dist/migrations/mysql/0012_image_sizes_and_focal_point.sql +30 -0
  190. package/dist/migrations/mysql/0012_media_folders.sql +43 -0
  191. package/dist/migrations/mysql/0013_user_brute_force_protection.sql +31 -0
  192. package/dist/migrations/mysql/0014_email_template_attachments.sql +12 -0
  193. package/dist/migrations/mysql/0015_media_uploaded_by_nullable.sql +15 -0
  194. package/dist/migrations/mysql/20260429_000000_000_initial_journal.sql +22 -0
  195. package/dist/migrations/mysql/20260501_000000_journal_batch.sql +17 -0
  196. package/dist/migrations/mysql/20260501_000001_audit_log.sql +24 -0
  197. package/dist/migrations/mysql/20260504_000000_nextly_meta.sql +21 -0
  198. package/dist/migrations/mysql/meta/0000_snapshot.json +1005 -0
  199. package/dist/migrations/mysql/meta/0001_snapshot.json +1099 -0
  200. package/dist/migrations/mysql/meta/_journal.json +41 -0
  201. package/dist/migrations/postgresql/0000_misty_king_bedlam.sql +169 -0
  202. package/dist/migrations/postgresql/0001_perpetual_captain_marvel.sql +8 -0
  203. package/dist/migrations/postgresql/0002_sad_spectrum.sql +16 -0
  204. package/dist/migrations/postgresql/0003_hesitant_ultron.sql +17 -0
  205. package/dist/migrations/postgresql/0004_media_table.sql +24 -0
  206. package/dist/migrations/postgresql/0005_media_folders.sql +36 -0
  207. package/dist/migrations/postgresql/0006_dynamic_collections_update.sql +50 -0
  208. package/dist/migrations/postgresql/0007_dynamic_singles.sql +38 -0
  209. package/dist/migrations/postgresql/0008_dynamic_components.sql +37 -0
  210. package/dist/migrations/postgresql/0009_user_management_tables.sql +95 -0
  211. package/dist/migrations/postgresql/0010_api_keys.sql +34 -0
  212. package/dist/migrations/postgresql/0011_general_settings.sql +20 -0
  213. package/dist/migrations/postgresql/0012_site_settings_logo_url.sql +9 -0
  214. package/dist/migrations/postgresql/0013_activity_log.sql +29 -0
  215. package/dist/migrations/postgresql/0014_image_sizes_and_focal_point.sql +33 -0
  216. package/dist/migrations/postgresql/0014_site_settings_sidebar.sql +13 -0
  217. package/dist/migrations/postgresql/0015_user_brute_force_protection.sql +29 -0
  218. package/dist/migrations/postgresql/0016_email_template_attachments.sql +12 -0
  219. package/dist/migrations/postgresql/0017_media_uploaded_by_nullable.sql +15 -0
  220. package/dist/migrations/postgresql/20260429_000000_000_initial_journal.sql +24 -0
  221. package/dist/migrations/postgresql/20260501_000000_journal_batch.sql +17 -0
  222. package/dist/migrations/postgresql/20260501_000001_audit_log.sql +24 -0
  223. package/dist/migrations/postgresql/20260504_000000_nextly_meta.sql +22 -0
  224. package/dist/migrations/postgresql/meta/0000_snapshot.json +1286 -0
  225. package/dist/migrations/postgresql/meta/0001_snapshot.json +1407 -0
  226. package/dist/migrations/postgresql/meta/0002_snapshot.json +1552 -0
  227. package/dist/migrations/postgresql/meta/0003_snapshot.json +1695 -0
  228. package/dist/migrations/postgresql/meta/0010_snapshot.json +2345 -0
  229. package/dist/migrations/postgresql/meta/_journal.json +90 -0
  230. package/dist/migrations/sqlite/0000_api_keys.sql +34 -0
  231. package/dist/migrations/sqlite/0001_general_settings.sql +20 -0
  232. package/dist/migrations/sqlite/0002_site_settings_logo_url.sql +9 -0
  233. package/dist/migrations/sqlite/0003_activity_log.sql +29 -0
  234. package/dist/migrations/sqlite/0004_image_sizes_and_focal_point.sql +29 -0
  235. package/dist/migrations/sqlite/0004_site_settings_sidebar.sql +11 -0
  236. package/dist/migrations/sqlite/0005_user_brute_force_protection.sql +29 -0
  237. package/dist/migrations/sqlite/0006_email_template_attachments.sql +12 -0
  238. package/dist/migrations/sqlite/0007_media_uploaded_by_nullable.sql +111 -0
  239. package/dist/migrations/sqlite/20260429_000000_000_initial_journal.sql +24 -0
  240. package/dist/migrations/sqlite/20260501_000000_journal_batch.sql +19 -0
  241. package/dist/migrations/sqlite/20260501_000001_audit_log.sql +24 -0
  242. package/dist/migrations/sqlite/20260504_000000_nextly_meta.sql +21 -0
  243. package/dist/migrations/sqlite/20260505_000000_user_management_tables.sql +77 -0
  244. package/dist/next.d.ts +57 -0
  245. package/dist/next.mjs +55 -0
  246. package/dist/observability/index.d.ts +87 -0
  247. package/dist/observability/index.mjs +57 -0
  248. package/dist/permissions-3DZZQZMI.mjs +39 -0
  249. package/dist/pipeline-YOML7SWF.mjs +29 -0
  250. package/dist/preview-ZZTR3QGS.mjs +9 -0
  251. package/dist/program-PW6UB2ZC.mjs +5934 -0
  252. package/dist/reconcile-single-tables-7ENVXJGB.mjs +7 -0
  253. package/dist/register-SF6E6FVU.mjs +49 -0
  254. package/dist/reload-config-HWQ4G5MM.mjs +23 -0
  255. package/dist/resolve-single-table-name-JSOMUB3R.mjs +7 -0
  256. package/dist/routeHandler-UNMMJIBM.mjs +77 -0
  257. package/dist/runtime-schema-generator-NRA6A6Z6.mjs +8 -0
  258. package/dist/runtime.d.ts +120 -0
  259. package/dist/runtime.mjs +73 -0
  260. package/dist/schema-hash-FMMG6VPJ.mjs +13 -0
  261. package/dist/schema-registry-EQ36FZDP.mjs +7 -0
  262. package/dist/scripts/load-env.mjs +42 -0
  263. package/dist/storage/index.d.ts +566 -0
  264. package/dist/storage/index.mjs +45 -0
  265. package/dist/super-admin-G5ZK5F4T.mjs +39 -0
  266. package/dist/system-table-service-WGSRVEGT.mjs +17 -0
  267. package/dist/users-7KELGRYJ.mjs +38 -0
  268. package/package.json +308 -9
@@ -0,0 +1,943 @@
1
+ import {
2
+ LocalStorageAdapter
3
+ } from "./chunk-G2AA4QLC.mjs";
4
+
5
+ // src/storage/storage.ts
6
+ var MediaStorage = class {
7
+ /** Registered storage plugins by name */
8
+ plugins = /* @__PURE__ */ new Map();
9
+ /** Storage adapter per collection */
10
+ collectionAdapters = /* @__PURE__ */ new Map();
11
+ /** Storage configuration per collection */
12
+ collectionConfigs = /* @__PURE__ */ new Map();
13
+ /** Local storage adapter (always available as fallback) */
14
+ localAdapter;
15
+ /**
16
+ * Create a new MediaStorage instance.
17
+ *
18
+ * @param config - Optional configuration for storage initialization
19
+ */
20
+ constructor(config) {
21
+ this.localAdapter = new LocalStorageAdapter({
22
+ basePath: config?.local?.uploadDir ?? "./public/uploads",
23
+ baseUrl: config?.local?.publicPath ?? "/uploads"
24
+ });
25
+ if (config?.plugins) {
26
+ for (const plugin of config.plugins) {
27
+ this.registerPlugin(plugin);
28
+ }
29
+ }
30
+ }
31
+ // ============================================================
32
+ // Plugin Registration
33
+ // ============================================================
34
+ /**
35
+ * Register a storage plugin.
36
+ *
37
+ * Plugins provide storage adapters for specific collections.
38
+ * When a collection is registered with a plugin, uploads for that
39
+ * collection will be routed to the plugin's adapter.
40
+ *
41
+ * @param plugin - The storage plugin to register
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * const storage = new MediaStorage();
46
+ *
47
+ * storage.registerPlugin(s3Storage({
48
+ * bucket: 'my-bucket',
49
+ * region: 'us-east-1',
50
+ * collections: {
51
+ * media: true,
52
+ * 'private-docs': { prefix: 'private/' }
53
+ * }
54
+ * }));
55
+ * ```
56
+ */
57
+ registerPlugin(plugin) {
58
+ if (!plugin.adapter) {
59
+ return;
60
+ }
61
+ this.plugins.set(plugin.name, plugin);
62
+ for (const [collectionSlug, config] of Object.entries(plugin.collections)) {
63
+ const collectionConfig = typeof config === "boolean" ? {} : config;
64
+ this.collectionAdapters.set(collectionSlug, plugin.adapter);
65
+ this.collectionConfigs.set(collectionSlug, collectionConfig);
66
+ }
67
+ }
68
+ // ============================================================
69
+ // Adapter Resolution
70
+ // ============================================================
71
+ /**
72
+ * Check if any storage adapter is configured.
73
+ *
74
+ * @returns True if at least one storage plugin is registered
75
+ */
76
+ hasAdapter() {
77
+ return true;
78
+ }
79
+ /**
80
+ * Get the storage adapter if available, or null if not configured.
81
+ *
82
+ * Unlike getAdapter(), this method does not throw an error if no storage
83
+ * is configured. Useful for optional storage scenarios.
84
+ *
85
+ * @param collection - The collection slug (optional)
86
+ * @returns The storage adapter instance, or null if not configured
87
+ */
88
+ getAdapterOrNull(collection) {
89
+ if (collection && this.collectionAdapters.has(collection)) {
90
+ return this.collectionAdapters.get(collection);
91
+ }
92
+ return this.localAdapter;
93
+ }
94
+ /**
95
+ * Get the storage adapter for a specific collection.
96
+ *
97
+ * If a plugin is configured for the collection, returns the plugin's adapter.
98
+ * Otherwise, returns the default adapter (first registered plugin).
99
+ *
100
+ * @param collection - The collection slug (optional)
101
+ * @returns The appropriate storage adapter
102
+ * @throws Error if no storage plugin is configured
103
+ */
104
+ getAdapterForCollection(collection) {
105
+ if (collection && this.collectionAdapters.has(collection)) {
106
+ return this.collectionAdapters.get(collection);
107
+ }
108
+ return this.localAdapter;
109
+ }
110
+ /**
111
+ * Get configuration for a specific collection.
112
+ *
113
+ * @param collection - The collection slug
114
+ * @returns The collection's storage configuration, or undefined
115
+ */
116
+ getCollectionConfig(collection) {
117
+ return this.collectionConfigs.get(collection);
118
+ }
119
+ // ============================================================
120
+ // Core Storage Operations
121
+ // ============================================================
122
+ /**
123
+ * Upload file to appropriate storage based on collection.
124
+ *
125
+ * Routes the upload to the correct adapter based on collection
126
+ * configuration. Applies collection-specific prefix if configured.
127
+ *
128
+ * @param buffer - The file buffer to upload
129
+ * @param options - Upload options including filename, mimeType, collection
130
+ * @returns Upload result with URL and path
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * const result = await storage.upload(buffer, {
135
+ * filename: 'photo.jpg',
136
+ * mimeType: 'image/jpeg',
137
+ * collection: 'media'
138
+ * });
139
+ * console.log(result.url); // Public URL
140
+ * console.log(result.path); // Storage path for deletion
141
+ * ```
142
+ */
143
+ async upload(buffer, options) {
144
+ const adapter = this.getAdapterForCollection(options.collection);
145
+ const config = options.collection ? this.getCollectionConfig(options.collection) : void 0;
146
+ const uploadOptions = { ...options };
147
+ if (config?.prefix) {
148
+ uploadOptions.folder = config.prefix + (options.folder || "");
149
+ }
150
+ return adapter.upload(buffer, uploadOptions);
151
+ }
152
+ /**
153
+ * Delete file from storage.
154
+ *
155
+ * Determines correct adapter based on collection.
156
+ *
157
+ * @param filePath - The storage path/key of the file
158
+ * @param collection - The collection slug (optional, for routing)
159
+ */
160
+ async delete(filePath, collection) {
161
+ const adapter = this.getAdapterForCollection(collection);
162
+ return adapter.delete(filePath);
163
+ }
164
+ /**
165
+ * Bulk delete files from storage.
166
+ * Uses adapter's native bulkDelete if available, otherwise falls back to
167
+ * sequential individual deletes in chunks of 10.
168
+ */
169
+ async bulkDelete(filePaths, collection) {
170
+ const adapter = this.getAdapterForCollection(collection);
171
+ if (adapter.bulkDelete) {
172
+ return adapter.bulkDelete(filePaths);
173
+ }
174
+ const successful = [];
175
+ const failed = [];
176
+ const chunkSize = 10;
177
+ for (let i = 0; i < filePaths.length; i += chunkSize) {
178
+ const chunk = filePaths.slice(i, i + chunkSize);
179
+ const results = await Promise.allSettled(
180
+ chunk.map((fp) => adapter.delete(fp))
181
+ );
182
+ results.forEach((result, idx) => {
183
+ const fp = chunk[idx];
184
+ if (result.status === "fulfilled") {
185
+ successful.push(fp);
186
+ } else {
187
+ failed.push({
188
+ filePath: fp,
189
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason)
190
+ });
191
+ }
192
+ });
193
+ }
194
+ return { successful, failed };
195
+ }
196
+ /**
197
+ * Check if file exists in storage.
198
+ *
199
+ * @param filePath - The storage path/key to check
200
+ * @param collection - The collection slug (optional, for routing)
201
+ * @returns True if file exists
202
+ */
203
+ async exists(filePath, collection) {
204
+ const adapter = this.getAdapterForCollection(collection);
205
+ return adapter.exists(filePath);
206
+ }
207
+ /**
208
+ * Get public URL for file.
209
+ *
210
+ * @param filePath - The storage path/key
211
+ * @param collection - The collection slug (optional, for routing)
212
+ * @returns Public URL to access the file
213
+ */
214
+ getPublicUrl(filePath, collection) {
215
+ const adapter = this.getAdapterForCollection(collection);
216
+ return adapter.getPublicUrl(filePath);
217
+ }
218
+ /**
219
+ * Get storage type for a collection.
220
+ *
221
+ * @param collection - The collection slug (optional)
222
+ * @returns Storage type identifier ('s3', 'vercel-blob')
223
+ */
224
+ getStorageType(collection) {
225
+ const adapter = this.getAdapterForCollection(collection);
226
+ return adapter.getType();
227
+ }
228
+ // ============================================================
229
+ // Client Upload Support
230
+ // ============================================================
231
+ /**
232
+ * Check if collection supports client-side uploads.
233
+ *
234
+ * Client-side uploads allow direct-to-storage uploads, bypassing
235
+ * the server. This is essential for serverless platforms with
236
+ * request body size limits (e.g., Vercel's 4.5MB limit).
237
+ *
238
+ * @param collection - The collection slug
239
+ * @returns True if client uploads are enabled and supported
240
+ */
241
+ supportsClientUploads(collection) {
242
+ const config = this.getCollectionConfig(collection);
243
+ if (!config?.clientUploads) return false;
244
+ const adapter = this.getAdapterForCollection(collection);
245
+ const info = adapter.getInfo?.();
246
+ return info?.supportsClientUploads ?? false;
247
+ }
248
+ /**
249
+ * Get client upload URL for direct-to-storage uploads.
250
+ *
251
+ * Generates a pre-signed URL that allows the client to upload
252
+ * directly to the storage backend, bypassing the server.
253
+ *
254
+ * Only available if:
255
+ * 1. Collection is configured with `clientUploads: true`
256
+ * 2. The storage adapter supports client uploads
257
+ *
258
+ * @param filename - Original filename
259
+ * @param mimeType - File MIME type
260
+ * @param collection - Collection slug
261
+ * @returns Client upload data, or null if not supported
262
+ *
263
+ * @example
264
+ * ```typescript
265
+ * // Server-side: generate upload URL
266
+ * const uploadData = await storage.getClientUploadUrl(
267
+ * 'photo.jpg',
268
+ * 'image/jpeg',
269
+ * 'media'
270
+ * );
271
+ *
272
+ * // Client-side: upload directly to storage
273
+ * await fetch(uploadData.uploadUrl, {
274
+ * method: uploadData.method,
275
+ * headers: uploadData.headers,
276
+ * body: file
277
+ * });
278
+ * ```
279
+ */
280
+ async getClientUploadUrl(filename, mimeType, collection) {
281
+ if (!this.supportsClientUploads(collection)) {
282
+ return null;
283
+ }
284
+ for (const plugin of this.plugins.values()) {
285
+ if (collection in plugin.collections && plugin.getClientUploadUrl) {
286
+ return plugin.getClientUploadUrl(filename, mimeType, collection);
287
+ }
288
+ }
289
+ return null;
290
+ }
291
+ // ============================================================
292
+ // Signed Download Support
293
+ // ============================================================
294
+ /**
295
+ * Check if collection supports signed download URLs.
296
+ *
297
+ * @param collection - The collection slug
298
+ * @returns True if signed downloads are enabled and supported
299
+ */
300
+ supportsSignedDownloads(collection) {
301
+ const config = this.getCollectionConfig(collection);
302
+ if (!config?.signedDownloads) return false;
303
+ const adapter = this.getAdapterForCollection(collection);
304
+ const info = adapter.getInfo?.();
305
+ return info?.supportsSignedUrls ?? false;
306
+ }
307
+ /**
308
+ * Get signed download URL for secure file access.
309
+ *
310
+ * Generates a time-limited signed URL for accessing files in
311
+ * private storage buckets. Only works if:
312
+ * 1. Collection is configured with `signedDownloads: true`
313
+ * 2. The storage adapter supports signed URLs
314
+ *
315
+ * @param filePath - Storage path/key of the file
316
+ * @param collection - Collection slug
317
+ * @param expiresIn - URL expiry time in seconds (optional)
318
+ * @returns Signed URL, or null if not supported
319
+ *
320
+ * @example
321
+ * ```typescript
322
+ * const signedUrl = await storage.getSignedDownloadUrl(
323
+ * 'private/doc.pdf',
324
+ * 'private-docs',
325
+ * 3600 // 1 hour
326
+ * );
327
+ * ```
328
+ */
329
+ async getSignedDownloadUrl(filePath, collection, expiresIn) {
330
+ if (!this.supportsSignedDownloads(collection)) {
331
+ return null;
332
+ }
333
+ const config = this.getCollectionConfig(collection);
334
+ for (const plugin of this.plugins.values()) {
335
+ if (collection in plugin.collections && plugin.getSignedDownloadUrl) {
336
+ return plugin.getSignedDownloadUrl(
337
+ filePath,
338
+ expiresIn ?? config?.signedUrlExpiresIn ?? 3600
339
+ );
340
+ }
341
+ }
342
+ return null;
343
+ }
344
+ // ============================================================
345
+ // Accessor Methods
346
+ // ============================================================
347
+ /**
348
+ * Get the default storage adapter.
349
+ *
350
+ * @returns The default storage adapter (first registered plugin)
351
+ * @throws Error if no storage plugin is configured
352
+ */
353
+ getDefaultAdapter() {
354
+ return this.localAdapter;
355
+ }
356
+ /**
357
+ * Get list of registered plugins.
358
+ *
359
+ * @returns Array of registered storage plugins
360
+ */
361
+ getPlugins() {
362
+ return Array.from(this.plugins.values());
363
+ }
364
+ /**
365
+ * Get the underlying storage adapter for a collection.
366
+ *
367
+ * Useful for passing to registerServices() which requires IStorageAdapter.
368
+ *
369
+ * @param collection - The collection slug (optional)
370
+ * @returns The storage adapter instance
371
+ */
372
+ getAdapter(collection) {
373
+ return this.getAdapterForCollection(collection);
374
+ }
375
+ /**
376
+ * Check if a collection has a configured storage adapter.
377
+ *
378
+ * @param collection - The collection slug
379
+ * @returns True if a plugin is configured for this collection
380
+ */
381
+ hasCollectionAdapter(collection) {
382
+ return this.collectionAdapters.has(collection);
383
+ }
384
+ /**
385
+ * Get list of collections with configured storage.
386
+ *
387
+ * @returns Array of collection slugs that have plugin storage
388
+ */
389
+ getConfiguredCollections() {
390
+ return Array.from(this.collectionAdapters.keys());
391
+ }
392
+ /**
393
+ * Check if any storage plugin is configured.
394
+ *
395
+ * @returns True if at least one storage plugin is registered
396
+ */
397
+ hasPlugins() {
398
+ return this.plugins.size > 0;
399
+ }
400
+ };
401
+ var storageInstance = null;
402
+ function initializeMediaStorage(config) {
403
+ storageInstance = new MediaStorage(config);
404
+ return storageInstance;
405
+ }
406
+ function getMediaStorage() {
407
+ if (!storageInstance) {
408
+ storageInstance = new MediaStorage();
409
+ }
410
+ return storageInstance;
411
+ }
412
+ function resetMediaStorage() {
413
+ storageInstance = null;
414
+ }
415
+
416
+ // src/storage/image-processor.ts
417
+ var sharpModule = null;
418
+ async function getSharp() {
419
+ if (!sharpModule) {
420
+ sharpModule = (await import("sharp")).default;
421
+ }
422
+ return sharpModule;
423
+ }
424
+ var ImageProcessor = class {
425
+ /**
426
+ * Get image metadata without loading full image
427
+ */
428
+ async getMetadata(buffer) {
429
+ const sharp = await getSharp();
430
+ const metadata = await sharp(buffer).metadata();
431
+ return {
432
+ width: metadata.width || 0,
433
+ height: metadata.height || 0,
434
+ format: metadata.format || "unknown",
435
+ size: buffer.length
436
+ };
437
+ }
438
+ /**
439
+ * Generate thumbnail (300x300 by default, cropped to center)
440
+ *
441
+ * Uses "cover" fit to fill the entire 300x300 area while maintaining aspect ratio
442
+ */
443
+ async generateThumbnail(buffer, size = 300) {
444
+ const sharp = await getSharp();
445
+ const processed = await sharp(buffer).resize(size, size, {
446
+ fit: "cover",
447
+ // Crop to fill entire area
448
+ position: "center"
449
+ // Crop from center
450
+ }).jpeg({ quality: 80, progressive: true }).toBuffer({ resolveWithObject: true });
451
+ return {
452
+ buffer: processed.data,
453
+ metadata: {
454
+ width: processed.info.width,
455
+ height: processed.info.height,
456
+ format: processed.info.format,
457
+ size: processed.data.length
458
+ }
459
+ };
460
+ }
461
+ /**
462
+ * Optimize image (compress, convert to WebP if beneficial)
463
+ *
464
+ * Strategy:
465
+ * - Small images (<100KB) and already WebP: return as-is
466
+ * - Otherwise: convert to WebP with quality 80
467
+ */
468
+ async optimize(buffer, quality = 80) {
469
+ const sharp = await getSharp();
470
+ const metadata = await sharp(buffer).metadata();
471
+ if (buffer.length < 100 * 1024 && metadata.format === "webp") {
472
+ return {
473
+ buffer,
474
+ metadata: {
475
+ width: metadata.width || 0,
476
+ height: metadata.height || 0,
477
+ format: metadata.format,
478
+ size: buffer.length
479
+ }
480
+ };
481
+ }
482
+ const processed = await sharp(buffer).webp({ quality, effort: 4 }).toBuffer({ resolveWithObject: true });
483
+ return {
484
+ buffer: processed.data,
485
+ metadata: {
486
+ width: processed.info.width,
487
+ height: processed.info.height,
488
+ format: "webp",
489
+ size: processed.data.length
490
+ }
491
+ };
492
+ }
493
+ /**
494
+ * Resize image to specific dimensions
495
+ *
496
+ * @param maxWidth Maximum width (maintains aspect ratio)
497
+ * @param maxHeight Maximum height (maintains aspect ratio)
498
+ */
499
+ async resize(buffer, maxWidth, maxHeight) {
500
+ const sharp = await getSharp();
501
+ const processed = await sharp(buffer).resize(maxWidth, maxHeight, {
502
+ fit: "inside",
503
+ // Fit within bounds, maintaining aspect ratio
504
+ withoutEnlargement: true
505
+ // Don't upscale small images
506
+ }).toBuffer({ resolveWithObject: true });
507
+ return {
508
+ buffer: processed.data,
509
+ metadata: {
510
+ width: processed.info.width,
511
+ height: processed.info.height,
512
+ format: processed.info.format,
513
+ size: processed.data.length
514
+ }
515
+ };
516
+ }
517
+ /**
518
+ * Resize an image with focal point awareness and format conversion.
519
+ *
520
+ * When fit is 'cover' and a focal point is set, the crop anchors at that
521
+ * point instead of center. Supports format conversion ('auto' outputs webp
522
+ * for jpeg/png/tiff sources, keeps original for gif).
523
+ */
524
+ async resizeWithFocalPoint(buffer, options) {
525
+ const sharp = await getSharp();
526
+ const quality = options.quality ?? 80;
527
+ const metadata = await sharp(buffer).metadata();
528
+ const originalFormat = metadata.format || "jpeg";
529
+ let outputFormat = originalFormat;
530
+ if (options.format && options.format !== "auto") {
531
+ outputFormat = options.format;
532
+ } else if (options.format === "auto") {
533
+ const convertibleFormats = ["jpeg", "png", "tiff", "jpg"];
534
+ if (convertibleFormats.includes(originalFormat)) {
535
+ outputFormat = "webp";
536
+ }
537
+ }
538
+ let pipeline = sharp(buffer);
539
+ const hasFocalPoint = options.fit === "cover" && (options.focalX !== void 0 || options.focalY !== void 0) && options.width && options.height && metadata.width && metadata.height;
540
+ if (hasFocalPoint) {
541
+ const srcW = metadata.width;
542
+ const srcH = metadata.height;
543
+ const tgtW = options.width;
544
+ const tgtH = options.height;
545
+ const fx = (options.focalX ?? 50) / 100;
546
+ const fy = (options.focalY ?? 50) / 100;
547
+ const tgtAspect = tgtW / tgtH;
548
+ let cropW;
549
+ let cropH;
550
+ if (srcW / srcH > tgtAspect) {
551
+ cropH = srcH;
552
+ cropW = Math.round(srcH * tgtAspect);
553
+ } else {
554
+ cropW = srcW;
555
+ cropH = Math.round(srcW / tgtAspect);
556
+ }
557
+ let left = Math.round(fx * srcW - cropW / 2);
558
+ let top = Math.round(fy * srcH - cropH / 2);
559
+ left = Math.max(0, Math.min(srcW - cropW, left));
560
+ top = Math.max(0, Math.min(srcH - cropH, top));
561
+ pipeline = pipeline.extract({ left, top, width: cropW, height: cropH }).resize(tgtW, tgtH, { fit: "fill" });
562
+ } else {
563
+ pipeline = pipeline.resize(
564
+ options.width || void 0,
565
+ options.height || void 0,
566
+ {
567
+ fit: options.fit,
568
+ position: "center",
569
+ withoutEnlargement: true
570
+ }
571
+ );
572
+ }
573
+ switch (outputFormat) {
574
+ case "webp":
575
+ pipeline = pipeline.webp({ quality, effort: 4 });
576
+ break;
577
+ case "jpeg":
578
+ case "jpg":
579
+ pipeline = pipeline.jpeg({ quality, progressive: true });
580
+ outputFormat = "jpeg";
581
+ break;
582
+ case "png":
583
+ pipeline = pipeline.png({ quality });
584
+ break;
585
+ case "avif":
586
+ pipeline = pipeline.avif({ quality });
587
+ break;
588
+ default:
589
+ break;
590
+ }
591
+ const result = await pipeline.toBuffer({ resolveWithObject: true });
592
+ return {
593
+ buffer: result.data,
594
+ width: result.info.width,
595
+ height: result.info.height,
596
+ format: outputFormat,
597
+ size: result.data.length
598
+ };
599
+ }
600
+ /**
601
+ * Check if buffer is a valid image
602
+ */
603
+ async isValidImage(buffer) {
604
+ try {
605
+ const sharp = await getSharp();
606
+ await sharp(buffer).metadata();
607
+ return true;
608
+ } catch {
609
+ return false;
610
+ }
611
+ }
612
+ /**
613
+ * Get image dimensions quickly (without full processing)
614
+ */
615
+ async getDimensions(buffer) {
616
+ try {
617
+ const sharp = await getSharp();
618
+ const metadata = await sharp(buffer).metadata();
619
+ if (metadata.width && metadata.height) {
620
+ return { width: metadata.width, height: metadata.height };
621
+ }
622
+ return null;
623
+ } catch {
624
+ return null;
625
+ }
626
+ }
627
+ };
628
+ var processorInstance = null;
629
+ function getImageProcessor() {
630
+ if (!processorInstance) {
631
+ processorInstance = new ImageProcessor();
632
+ }
633
+ return processorInstance;
634
+ }
635
+ function resetImageProcessor() {
636
+ processorInstance = null;
637
+ }
638
+
639
+ // src/storage/image-sizes.ts
640
+ function getExtensionForFormat(format) {
641
+ switch (format) {
642
+ case "jpeg":
643
+ case "jpg":
644
+ return "jpg";
645
+ case "webp":
646
+ return "webp";
647
+ case "png":
648
+ return "png";
649
+ case "avif":
650
+ return "avif";
651
+ default:
652
+ return format;
653
+ }
654
+ }
655
+ function getMimeTypeForFormat(format) {
656
+ switch (format) {
657
+ case "jpeg":
658
+ case "jpg":
659
+ return "image/jpeg";
660
+ case "webp":
661
+ return "image/webp";
662
+ case "png":
663
+ return "image/png";
664
+ case "avif":
665
+ return "image/avif";
666
+ default:
667
+ return `image/${format}`;
668
+ }
669
+ }
670
+ function buildVariantFilename(originalFilename, sizeName, format) {
671
+ const lastDot = originalFilename.lastIndexOf(".");
672
+ const baseName = lastDot > 0 ? originalFilename.substring(0, lastDot) : originalFilename;
673
+ const ext = getExtensionForFormat(format);
674
+ return `${baseName}-${sizeName}.${ext}`;
675
+ }
676
+ async function generateImageSizes(originalBuffer, originalFilename, sizes, uploadFn, options = {}) {
677
+ if (sizes.length === 0) return {};
678
+ const processor = getImageProcessor();
679
+ const results = {};
680
+ for (const sizeConfig of sizes) {
681
+ try {
682
+ if (!sizeConfig.width && !sizeConfig.height) continue;
683
+ const resized = await processor.resizeWithFocalPoint(originalBuffer, {
684
+ width: sizeConfig.width ?? void 0,
685
+ height: sizeConfig.height ?? void 0,
686
+ fit: sizeConfig.fit,
687
+ quality: sizeConfig.quality,
688
+ format: sizeConfig.format,
689
+ focalX: options.focalX ?? void 0,
690
+ focalY: options.focalY ?? void 0
691
+ });
692
+ const variantFilename = buildVariantFilename(
693
+ originalFilename,
694
+ sizeConfig.name,
695
+ resized.format
696
+ );
697
+ const mimeType = getMimeTypeForFormat(resized.format);
698
+ const uploadResult = await uploadFn(resized.buffer, {
699
+ filename: variantFilename,
700
+ mimeType,
701
+ folder: options.folder,
702
+ collection: options.collection
703
+ });
704
+ results[sizeConfig.name] = {
705
+ url: uploadResult.url,
706
+ path: uploadResult.path,
707
+ width: resized.width,
708
+ height: resized.height,
709
+ filesize: resized.size,
710
+ mimeType,
711
+ filename: variantFilename
712
+ };
713
+ } catch (error) {
714
+ console.warn(
715
+ `[ImageSizes] Failed to generate size "${sizeConfig.name}":`,
716
+ error instanceof Error ? error.message : error
717
+ );
718
+ }
719
+ }
720
+ return results;
721
+ }
722
+ async function deleteImageSizes(sizes, deleteFn) {
723
+ if (!sizes) return;
724
+ const paths = Object.values(sizes).map((v) => v.path).filter(Boolean);
725
+ await Promise.allSettled(paths.map((path) => deleteFn(path)));
726
+ }
727
+
728
+ // src/storage/retry.ts
729
+ var DEFAULT_OPTIONS = {
730
+ maxAttempts: 3,
731
+ baseDelayMs: 1e3,
732
+ maxDelayMs: 3e4,
733
+ backoffFactor: 2,
734
+ jitter: true
735
+ };
736
+ function isTransientError(error) {
737
+ if (!error) return false;
738
+ if (error instanceof Error) {
739
+ const message = error.message.toLowerCase();
740
+ const name = error.name.toLowerCase();
741
+ if (message.includes("timeout") || message.includes("timed out") || message.includes("etimedout") || message.includes("econnreset") || message.includes("econnrefused") || message.includes("enotfound") || message.includes("enetunreach") || message.includes("socket hang up") || message.includes("network") || name.includes("timeout") || name.includes("abort")) {
742
+ return true;
743
+ }
744
+ if (message.includes("rate limit") || message.includes("too many")) {
745
+ return true;
746
+ }
747
+ }
748
+ const errorAny = error;
749
+ const metadata = errorAny.$metadata;
750
+ if (errorAny.statusCode || errorAny.status || metadata?.httpStatusCode) {
751
+ const status = errorAny.statusCode || errorAny.status || metadata?.httpStatusCode;
752
+ const statusNum = typeof status === "number" ? status : Number(status);
753
+ if (statusNum === 429 || statusNum >= 500 && statusNum < 600) {
754
+ return true;
755
+ }
756
+ }
757
+ if (errorAny.code) {
758
+ const code = String(errorAny.code).toLowerCase();
759
+ if (code.includes("timeout") || code.includes("throttl") || code.includes("serviceunavailable") || code.includes("slowdown") || code === "econnreset" || code === "epipe") {
760
+ return true;
761
+ }
762
+ }
763
+ return false;
764
+ }
765
+ function calculateDelay(attempt, options) {
766
+ const { baseDelayMs, maxDelayMs, backoffFactor, jitter } = options;
767
+ const exponentialDelay = baseDelayMs * Math.pow(backoffFactor, attempt - 1);
768
+ const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
769
+ if (jitter) {
770
+ const jitterAmount = cappedDelay * Math.random() * 0.5;
771
+ return Math.floor(cappedDelay + jitterAmount);
772
+ }
773
+ return Math.floor(cappedDelay);
774
+ }
775
+ function sleep(ms) {
776
+ return new Promise((resolve) => setTimeout(resolve, ms));
777
+ }
778
+ async function withRetry(fn, options = {}) {
779
+ const config = { ...DEFAULT_OPTIONS, ...options };
780
+ const { maxAttempts, shouldRetry, onRetry } = {
781
+ ...config,
782
+ shouldRetry: options.shouldRetry ?? isTransientError,
783
+ onRetry: options.onRetry
784
+ };
785
+ let lastError;
786
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
787
+ try {
788
+ return await fn();
789
+ } catch (error) {
790
+ lastError = error;
791
+ const isLastAttempt = attempt >= maxAttempts;
792
+ const canRetry = !isLastAttempt && shouldRetry(error, attempt);
793
+ if (!canRetry) {
794
+ throw error;
795
+ }
796
+ const delayMs = calculateDelay(attempt, config);
797
+ if (onRetry) {
798
+ onRetry(error, attempt, delayMs);
799
+ }
800
+ await sleep(delayMs);
801
+ }
802
+ }
803
+ throw lastError;
804
+ }
805
+ function createRetryable(fn, options = {}) {
806
+ return (...args) => withRetry(() => fn(...args), options);
807
+ }
808
+
809
+ // src/storage/svg-security.ts
810
+ var SVG_CSP_HEADER = "script-src 'none'; style-src 'unsafe-inline'";
811
+ function isSvgMimeType(mimeType) {
812
+ return mimeType.toLowerCase().trim() === "image/svg+xml";
813
+ }
814
+ function getSvgSecurityHeaders() {
815
+ return {
816
+ "Content-Security-Policy": SVG_CSP_HEADER,
817
+ "X-Content-Type-Options": "nosniff"
818
+ };
819
+ }
820
+
821
+ // src/storage/env-config.ts
822
+ async function ensureEnvLoaded() {
823
+ if (typeof process !== "undefined" && !process.env._NEXTLY_ENV_LOADED) {
824
+ try {
825
+ const dotenv = await import("dotenv");
826
+ dotenv.config();
827
+ process.env._NEXTLY_ENV_LOADED = "true";
828
+ } catch {
829
+ }
830
+ }
831
+ }
832
+ async function getStorageFromEnv() {
833
+ await ensureEnvLoaded();
834
+ const blobToken = process.env.BLOB_READ_WRITE_TOKEN;
835
+ if (blobToken) {
836
+ try {
837
+ const pkg = "@nextlyhq/storage-vercel-blob";
838
+ const { vercelBlobStorage } = await import(
839
+ /* webpackIgnore: true */
840
+ pkg
841
+ );
842
+ console.log(
843
+ "[Nextly] Storage: Vercel Blob (auto-detected from BLOB_READ_WRITE_TOKEN)"
844
+ );
845
+ return [
846
+ vercelBlobStorage({ token: blobToken, collections: { media: true } })
847
+ ];
848
+ } catch {
849
+ console.warn(
850
+ "[Nextly] BLOB_READ_WRITE_TOKEN set but @nextlyhq/storage-vercel-blob not installed. Run: pnpm add @nextlyhq/storage-vercel-blob"
851
+ );
852
+ }
853
+ }
854
+ const s3Bucket = process.env.S3_BUCKET;
855
+ if (s3Bucket) {
856
+ const region = process.env.S3_REGION;
857
+ const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
858
+ const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
859
+ if (!region || !accessKeyId || !secretAccessKey) {
860
+ const missing = [];
861
+ if (!region) missing.push("S3_REGION");
862
+ if (!accessKeyId) missing.push("AWS_ACCESS_KEY_ID");
863
+ if (!secretAccessKey) missing.push("AWS_SECRET_ACCESS_KEY");
864
+ console.warn(
865
+ `[Nextly] S3_BUCKET set but missing: ${missing.join(", ")}. Falling back to local storage.`
866
+ );
867
+ } else {
868
+ try {
869
+ const pkg = "@nextlyhq/storage-s3";
870
+ const { s3Storage } = await import(
871
+ /* webpackIgnore: true */
872
+ pkg
873
+ );
874
+ console.log("[Nextly] Storage: S3 (auto-detected from S3_BUCKET)");
875
+ return [
876
+ s3Storage({
877
+ bucket: s3Bucket,
878
+ region,
879
+ accessKeyId,
880
+ secretAccessKey,
881
+ endpoint: process.env.S3_ENDPOINT,
882
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
883
+ publicUrl: process.env.S3_PUBLIC_URL,
884
+ // eslint-disable-next-line turbo/no-undeclared-env-vars
885
+ forcePathStyle: process.env.S3_FORCE_PATH_STYLE === "true",
886
+ collections: { media: true }
887
+ })
888
+ ];
889
+ } catch {
890
+ console.warn(
891
+ "[Nextly] S3_BUCKET set but @nextlyhq/storage-s3 not installed. Run: pnpm add @nextlyhq/storage-s3"
892
+ );
893
+ }
894
+ }
895
+ }
896
+ const uploadthingToken = process.env.UPLOADTHING_TOKEN;
897
+ if (uploadthingToken) {
898
+ try {
899
+ const pkg = "@nextlyhq/storage-uploadthing";
900
+ const { uploadthingStorage } = await import(
901
+ /* webpackIgnore: true */
902
+ pkg
903
+ );
904
+ console.log(
905
+ "[Nextly] Storage: Uploadthing (auto-detected from UPLOADTHING_TOKEN)"
906
+ );
907
+ return [
908
+ uploadthingStorage({
909
+ token: uploadthingToken,
910
+ collections: { media: true }
911
+ })
912
+ ];
913
+ } catch {
914
+ console.warn(
915
+ "[Nextly] UPLOADTHING_TOKEN set but @nextlyhq/storage-uploadthing not installed. Run: pnpm add @nextlyhq/storage-uploadthing"
916
+ );
917
+ }
918
+ }
919
+ const { localStorage: localStorage2 } = await import("./local-plugin-PTET4NAT.mjs");
920
+ console.log(
921
+ "[Nextly] Storage: Local disk (no cloud env vars detected, using ./public/uploads)"
922
+ );
923
+ return [localStorage2({ collections: { media: true } })];
924
+ }
925
+
926
+ export {
927
+ MediaStorage,
928
+ initializeMediaStorage,
929
+ getMediaStorage,
930
+ resetMediaStorage,
931
+ ImageProcessor,
932
+ getImageProcessor,
933
+ resetImageProcessor,
934
+ generateImageSizes,
935
+ deleteImageSizes,
936
+ isTransientError,
937
+ withRetry,
938
+ createRetryable,
939
+ SVG_CSP_HEADER,
940
+ isSvgMimeType,
941
+ getSvgSecurityHeaders,
942
+ getStorageFromEnv
943
+ };