@anvilkit/plugin-asset-manager 0.1.9 → 0.1.10

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 (235) hide show
  1. package/README.md +96 -3
  2. package/dist/adapters/data-url.d.cts +1 -0
  3. package/dist/adapters/data-url.d.cts.map +1 -1
  4. package/dist/adapters/data-url.d.ts +1 -0
  5. package/dist/adapters/data-url.d.ts.map +1 -1
  6. package/dist/adapters/s3-multipart.cjs +425 -0
  7. package/dist/adapters/s3-multipart.d.cts +43 -0
  8. package/dist/adapters/s3-multipart.d.cts.map +1 -0
  9. package/dist/adapters/s3-multipart.d.ts +43 -0
  10. package/dist/adapters/s3-multipart.d.ts.map +1 -0
  11. package/dist/adapters/s3-multipart.js +387 -0
  12. package/dist/adapters/s3-presigned.d.cts +2 -0
  13. package/dist/adapters/s3-presigned.d.cts.map +1 -1
  14. package/dist/adapters/s3-presigned.d.ts +2 -0
  15. package/dist/adapters/s3-presigned.d.ts.map +1 -1
  16. package/dist/i18n/provider.d.cts +1 -0
  17. package/dist/i18n/provider.d.cts.map +1 -1
  18. package/dist/i18n/provider.d.ts +1 -0
  19. package/dist/i18n/provider.d.ts.map +1 -1
  20. package/dist/index.cjs +14 -0
  21. package/dist/index.d.cts +9 -3
  22. package/dist/index.d.cts.map +1 -1
  23. package/dist/index.d.ts +9 -3
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +3 -1
  26. package/dist/plugin.cjs +152 -12
  27. package/dist/plugin.d.cts +49 -1
  28. package/dist/plugin.d.cts.map +1 -1
  29. package/dist/plugin.d.ts +49 -1
  30. package/dist/plugin.d.ts.map +1 -1
  31. package/dist/plugin.js +147 -13
  32. package/dist/sources/composite-source.cjs +3 -0
  33. package/dist/sources/composite-source.d.cts.map +1 -1
  34. package/dist/sources/composite-source.d.ts.map +1 -1
  35. package/dist/sources/composite-source.js +3 -0
  36. package/dist/sources/federated-search.cjs +45 -7
  37. package/dist/sources/federated-search.d.cts.map +1 -1
  38. package/dist/sources/federated-search.d.ts.map +1 -1
  39. package/dist/sources/federated-search.js +45 -7
  40. package/dist/sources/provider.d.cts +5 -0
  41. package/dist/sources/provider.d.cts.map +1 -1
  42. package/dist/sources/provider.d.ts +5 -0
  43. package/dist/sources/provider.d.ts.map +1 -1
  44. package/dist/sources/unsplash/index.d.cts +1 -0
  45. package/dist/sources/unsplash/index.d.cts.map +1 -1
  46. package/dist/sources/unsplash/index.d.ts +1 -0
  47. package/dist/sources/unsplash/index.d.ts.map +1 -1
  48. package/dist/testing/index.d.cts +4 -0
  49. package/dist/testing/index.d.cts.map +1 -1
  50. package/dist/testing/index.d.ts +4 -0
  51. package/dist/testing/index.d.ts.map +1 -1
  52. package/dist/types/categories.d.cts +3 -0
  53. package/dist/types/categories.d.cts.map +1 -1
  54. package/dist/types/categories.d.ts +3 -0
  55. package/dist/types/categories.d.ts.map +1 -1
  56. package/dist/types/data-source.d.cts +9 -0
  57. package/dist/types/data-source.d.cts.map +1 -1
  58. package/dist/types/data-source.d.ts +9 -0
  59. package/dist/types/data-source.d.ts.map +1 -1
  60. package/dist/types/filter.d.cts +11 -0
  61. package/dist/types/filter.d.cts.map +1 -1
  62. package/dist/types/filter.d.ts +11 -0
  63. package/dist/types/filter.d.ts.map +1 -1
  64. package/dist/types/folders.d.cts +2 -0
  65. package/dist/types/folders.d.cts.map +1 -1
  66. package/dist/types/folders.d.ts +2 -0
  67. package/dist/types/folders.d.ts.map +1 -1
  68. package/dist/types/options.d.cts +57 -1
  69. package/dist/types/options.d.cts.map +1 -1
  70. package/dist/types/options.d.ts +57 -1
  71. package/dist/types/options.d.ts.map +1 -1
  72. package/dist/types/resumable.cjs +42 -0
  73. package/dist/types/resumable.d.cts +204 -0
  74. package/dist/types/resumable.d.cts.map +1 -0
  75. package/dist/types/resumable.d.ts +204 -0
  76. package/dist/types/resumable.d.ts.map +1 -0
  77. package/dist/types/resumable.js +4 -0
  78. package/dist/types/transform.cjs +18 -0
  79. package/dist/types/transform.d.cts +45 -0
  80. package/dist/types/transform.d.cts.map +1 -0
  81. package/dist/types/transform.d.ts +45 -0
  82. package/dist/types/transform.d.ts.map +1 -0
  83. package/dist/types/transform.js +1 -0
  84. package/dist/types/types.d.cts +17 -0
  85. package/dist/types/types.d.cts.map +1 -1
  86. package/dist/types/types.d.ts +17 -0
  87. package/dist/types/types.d.ts.map +1 -1
  88. package/dist/types/unsplash.d.cts +2 -0
  89. package/dist/types/unsplash.d.cts.map +1 -1
  90. package/dist/types/unsplash.d.ts +2 -0
  91. package/dist/types/unsplash.d.ts.map +1 -1
  92. package/dist/ui/AssetBrowser.d.cts +3 -1
  93. package/dist/ui/AssetBrowser.d.cts.map +1 -1
  94. package/dist/ui/AssetBrowser.d.ts +3 -1
  95. package/dist/ui/AssetBrowser.d.ts.map +1 -1
  96. package/dist/ui/AssetCommandPalette.d.cts +4 -1
  97. package/dist/ui/AssetCommandPalette.d.cts.map +1 -1
  98. package/dist/ui/AssetCommandPalette.d.ts +4 -1
  99. package/dist/ui/AssetCommandPalette.d.ts.map +1 -1
  100. package/dist/ui/AssetManagerUI.cjs +3 -1
  101. package/dist/ui/AssetManagerUI.d.cts +11 -2
  102. package/dist/ui/AssetManagerUI.d.cts.map +1 -1
  103. package/dist/ui/AssetManagerUI.d.ts +11 -2
  104. package/dist/ui/AssetManagerUI.d.ts.map +1 -1
  105. package/dist/ui/AssetManagerUI.js +3 -1
  106. package/dist/ui/DeleteAssetDialog.d.cts +4 -1
  107. package/dist/ui/DeleteAssetDialog.d.cts.map +1 -1
  108. package/dist/ui/DeleteAssetDialog.d.ts +4 -1
  109. package/dist/ui/DeleteAssetDialog.d.ts.map +1 -1
  110. package/dist/ui/DeleteFolderDialog.d.cts +4 -1
  111. package/dist/ui/DeleteFolderDialog.d.cts.map +1 -1
  112. package/dist/ui/DeleteFolderDialog.d.ts +4 -1
  113. package/dist/ui/DeleteFolderDialog.d.ts.map +1 -1
  114. package/dist/ui/EmptyFolderState.d.cts +4 -1
  115. package/dist/ui/EmptyFolderState.d.cts.map +1 -1
  116. package/dist/ui/EmptyFolderState.d.ts +4 -1
  117. package/dist/ui/EmptyFolderState.d.ts.map +1 -1
  118. package/dist/ui/FolderBreadcrumb.d.cts +4 -1
  119. package/dist/ui/FolderBreadcrumb.d.cts.map +1 -1
  120. package/dist/ui/FolderBreadcrumb.d.ts +4 -1
  121. package/dist/ui/FolderBreadcrumb.d.ts.map +1 -1
  122. package/dist/ui/FolderNameDialog.d.cts +3 -1
  123. package/dist/ui/FolderNameDialog.d.cts.map +1 -1
  124. package/dist/ui/FolderNameDialog.d.ts +3 -1
  125. package/dist/ui/FolderNameDialog.d.ts.map +1 -1
  126. package/dist/ui/FolderTree.d.cts +4 -1
  127. package/dist/ui/FolderTree.d.cts.map +1 -1
  128. package/dist/ui/FolderTree.d.ts +4 -1
  129. package/dist/ui/FolderTree.d.ts.map +1 -1
  130. package/dist/ui/MetadataPanel.d.cts +4 -1
  131. package/dist/ui/MetadataPanel.d.cts.map +1 -1
  132. package/dist/ui/MetadataPanel.d.ts +4 -1
  133. package/dist/ui/MetadataPanel.d.ts.map +1 -1
  134. package/dist/ui/MoveTargetPicker.d.cts +3 -1
  135. package/dist/ui/MoveTargetPicker.d.cts.map +1 -1
  136. package/dist/ui/MoveTargetPicker.d.ts +3 -1
  137. package/dist/ui/MoveTargetPicker.d.ts.map +1 -1
  138. package/dist/ui/ReplaceAssetDialog.cjs +7 -2
  139. package/dist/ui/ReplaceAssetDialog.d.cts +5 -2
  140. package/dist/ui/ReplaceAssetDialog.d.cts.map +1 -1
  141. package/dist/ui/ReplaceAssetDialog.d.ts +5 -2
  142. package/dist/ui/ReplaceAssetDialog.d.ts.map +1 -1
  143. package/dist/ui/ReplaceAssetDialog.js +7 -2
  144. package/dist/ui/UnsplashPanel.d.cts +4 -1
  145. package/dist/ui/UnsplashPanel.d.cts.map +1 -1
  146. package/dist/ui/UnsplashPanel.d.ts +4 -1
  147. package/dist/ui/UnsplashPanel.d.ts.map +1 -1
  148. package/dist/ui/UploadButton.cjs +7 -2
  149. package/dist/ui/UploadButton.d.cts +6 -2
  150. package/dist/ui/UploadButton.d.cts.map +1 -1
  151. package/dist/ui/UploadButton.d.ts +6 -2
  152. package/dist/ui/UploadButton.d.ts.map +1 -1
  153. package/dist/ui/UploadButton.js +7 -2
  154. package/dist/utils/asset-reference.cjs +87 -4
  155. package/dist/utils/asset-reference.d.cts +30 -6
  156. package/dist/utils/asset-reference.d.cts.map +1 -1
  157. package/dist/utils/asset-reference.d.ts +30 -6
  158. package/dist/utils/asset-reference.d.ts.map +1 -1
  159. package/dist/utils/asset-reference.js +83 -3
  160. package/dist/utils/csp.cjs +16 -0
  161. package/dist/utils/csp.d.cts +23 -0
  162. package/dist/utils/csp.d.cts.map +1 -1
  163. package/dist/utils/csp.d.ts +23 -0
  164. package/dist/utils/csp.d.ts.map +1 -1
  165. package/dist/utils/csp.js +16 -0
  166. package/dist/utils/data-source.cjs +19 -5
  167. package/dist/utils/data-source.d.cts +5 -1
  168. package/dist/utils/data-source.d.cts.map +1 -1
  169. package/dist/utils/data-source.d.ts +5 -1
  170. package/dist/utils/data-source.d.ts.map +1 -1
  171. package/dist/utils/data-source.js +19 -5
  172. package/dist/utils/errors.d.cts +5 -0
  173. package/dist/utils/errors.d.cts.map +1 -1
  174. package/dist/utils/errors.d.ts +5 -0
  175. package/dist/utils/errors.d.ts.map +1 -1
  176. package/dist/utils/query-param-transform.cjs +101 -0
  177. package/dist/utils/query-param-transform.d.cts +46 -0
  178. package/dist/utils/query-param-transform.d.cts.map +1 -0
  179. package/dist/utils/query-param-transform.d.ts +46 -0
  180. package/dist/utils/query-param-transform.d.ts.map +1 -0
  181. package/dist/utils/query-param-transform.js +60 -0
  182. package/dist/utils/registry.cjs +3 -0
  183. package/dist/utils/registry.d.cts +8 -0
  184. package/dist/utils/registry.d.cts.map +1 -1
  185. package/dist/utils/registry.d.ts +8 -0
  186. package/dist/utils/registry.d.ts.map +1 -1
  187. package/dist/utils/registry.js +3 -0
  188. package/dist/utils/resolver.cjs +19 -11
  189. package/dist/utils/resolver.d.cts +24 -0
  190. package/dist/utils/resolver.d.cts.map +1 -1
  191. package/dist/utils/resolver.d.ts +24 -0
  192. package/dist/utils/resolver.d.ts.map +1 -1
  193. package/dist/utils/resolver.js +19 -11
  194. package/dist/utils/retry.d.cts +2 -0
  195. package/dist/utils/retry.d.cts.map +1 -1
  196. package/dist/utils/retry.d.ts +2 -0
  197. package/dist/utils/retry.d.ts.map +1 -1
  198. package/dist/utils/run-resumable-upload.cjs +160 -0
  199. package/dist/utils/run-resumable-upload.d.cts +56 -0
  200. package/dist/utils/run-resumable-upload.d.cts.map +1 -0
  201. package/dist/utils/run-resumable-upload.d.ts +56 -0
  202. package/dist/utils/run-resumable-upload.d.ts.map +1 -0
  203. package/dist/utils/run-resumable-upload.js +122 -0
  204. package/dist/utils/sniff-file-type.cjs +209 -0
  205. package/dist/utils/sniff-file-type.d.cts +25 -0
  206. package/dist/utils/sniff-file-type.d.cts.map +1 -0
  207. package/dist/utils/sniff-file-type.d.ts +25 -0
  208. package/dist/utils/sniff-file-type.d.ts.map +1 -0
  209. package/dist/utils/sniff-file-type.js +164 -0
  210. package/dist/utils/studio-asset-source.cjs +11 -6
  211. package/dist/utils/studio-asset-source.d.cts +5 -1
  212. package/dist/utils/studio-asset-source.d.cts.map +1 -1
  213. package/dist/utils/studio-asset-source.d.ts +5 -1
  214. package/dist/utils/studio-asset-source.d.ts.map +1 -1
  215. package/dist/utils/studio-asset-source.js +11 -6
  216. package/dist/utils/upload-session-store.cjs +125 -0
  217. package/dist/utils/upload-session-store.d.cts +55 -0
  218. package/dist/utils/upload-session-store.d.cts.map +1 -0
  219. package/dist/utils/upload-session-store.d.ts +55 -0
  220. package/dist/utils/upload-session-store.d.ts.map +1 -0
  221. package/dist/utils/upload-session-store.js +84 -0
  222. package/dist/utils/validate-upload-result.cjs +9 -1
  223. package/dist/utils/validate-upload-result.d.cts +1 -0
  224. package/dist/utils/validate-upload-result.d.cts.map +1 -1
  225. package/dist/utils/validate-upload-result.d.ts +1 -0
  226. package/dist/utils/validate-upload-result.d.ts.map +1 -1
  227. package/dist/utils/validate-upload-result.js +9 -1
  228. package/dist/version.cjs +1 -1
  229. package/dist/version.d.cts +1 -1
  230. package/dist/version.d.cts.map +1 -1
  231. package/dist/version.d.ts +1 -1
  232. package/dist/version.d.ts.map +1 -1
  233. package/dist/version.js +1 -1
  234. package/meta/config.json +1 -1
  235. package/package.json +40 -10
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @anvilkit/plugin-asset-manager
2
2
 
3
- > **Alpha (`0.1.9`).** Public surface may still shift before `v1.0`. Bundle budgets enforced in CI: headless entry ≤ 8 KB gzip, UI subpath ≤ 12 KB gzip, Unsplash subpath ≤ 4 KB gzip.
3
+ > **Alpha (`0.1.10`).** Public surface may still shift before `v1.0`. Bundle budgets enforced in CI: headless entry ≤ 8 KB gzip, UI subpath ≤ 12 KB gzip, Unsplash subpath ≤ 4 KB gzip.
4
4
 
5
5
  Headless asset manager plugin for Anvilkit Studio. The host provides the upload backend; the plugin handles validation, registration, search, IR-time resolution, CSP guidance, and (optionally) a React UI for the upload + browse experience. Designed for pluggable production backends (S3, GCS, custom HTTP) with strict trust-boundary enforcement on every adapter response.
6
6
 
@@ -10,7 +10,7 @@ Headless asset manager plugin for Anvilkit Studio. The host provides the upload
10
10
  pnpm add @anvilkit/plugin-asset-manager @anvilkit/core react react-dom @puckeditor/core
11
11
  ```
12
12
 
13
- Non-optional peers: `react >=19.0.0`, `react-dom >=19.0.0`, `@puckeditor/core ^0.21.3`.
13
+ Non-optional peers: `react >=19.2.0`, `react-dom >=19.2.0`, `@puckeditor/core ^0.21.3`.
14
14
 
15
15
  Subpath imports:
16
16
 
@@ -48,6 +48,8 @@ For real uploads pass an `uploader`. `dataUrlUploader` is dev-only — files are
48
48
  - **IR-time resolution** — `createIRAssetResolver` + `resolveAssets` turn `asset://<id>` references into validated URLs at export / render time.
49
49
  - **CSP advisor** — `getRequiredCsp` computes the minimum `connect-src` / `img-src` / `media-src` directives the configured adapters need.
50
50
  - **Production-ready S3 adapter** — `s3PresignedAdapter` POST-then-PUT with exponential-backoff retry on 5xx + network failures (4xx fails fast).
51
+ - **Resumable / multipart upload** — opt-in `resumable` option chunks large media, retries per part, and resumes interrupted uploads after a reload; `s3MultipartAdapter` ships in-box (`./adapters/s3-multipart`). See [Resumable / multipart upload](#resumable--multipart-upload).
52
+ - **Asset transformations** — processing-free transform seam: a declarative `AssetTransform` rides on the `asset://<id>?w=…&fm=…` reference and a host `transformResolver` maps it to a derivative URL (your image CDN); `createQueryParamTransformResolver` ships in-box (`./transform`). See [Asset transformations / variants](#asset-transformations--variants).
51
53
  - **Optional React UI** — `UploadButton`, `AssetBrowser`, `AssetCommandPalette`, `MetadataPanel`, `ReplaceAssetDialog`, `DeleteAssetDialog`, and the composite `AssetManagerUI`.
52
54
  - **Batch upload control** — `StudioAssetSource.upload(files)` honors `maxConcurrentUploads` (default 3) and `AbortSignal`.
53
55
 
@@ -62,6 +64,7 @@ function createAssetManagerPlugin(options: AssetManagerOptions): StudioPlugin;
62
64
  | Field | Type | Default | Purpose |
63
65
  | --------------------------- | ---------------------------------------------- | --------- | ---------------------------------------------------------------------------- |
64
66
  | `uploader` | `UploadAdapter` | in-memory | Binary ingest backend (optional; defaults to an in-memory uploader). |
67
+ | `resumable` | `ResumableUploadConfig` | none | Opt-in chunked/resumable upload for large files (`{ adapter, partSize?, threshold?, sessionStore? }`). |
65
68
  | `dataSource` | `AssetDataSource` | in-memory | Host-backed catalog (list / remove / replace / rename / move + folders). |
66
69
  | `folders` | `boolean \| FolderOptions` | `true` | Folder management; `false` for a flat library, or `{ maxDepth, allowMove }`. |
67
70
  | `providers` | `readonly AssetSourceProvider[]` | `[]` | Extra read-only sources, federated alongside the local library. |
@@ -70,9 +73,14 @@ function createAssetManagerPlugin(options: AssetManagerOptions): StudioPlugin;
70
73
  | `facets` | `readonly AssetFacetDefinition[]` | none | Custom faceted filters (local `valueOf` or remote). |
71
74
  | `maxFileSize` | `number` | none | Bytes. Enforced before the adapter runs. |
72
75
  | `acceptedMimeTypes` | `readonly string[]` | none | Allowlist. Enforced before the adapter runs. |
76
+ | `acceptedFileExtensions` | `readonly string[]` | none | Extension allowlist (`".png"` or `"png"`). Enforced before the adapter runs. |
73
77
  | `dataUrlAllowlistOptIn` | `boolean` | `false` | When `true`, `data:` URLs are valid output. |
74
78
  | `allowMixedScriptHostnames` | `boolean` | `false` | When `true`, hostnames mixing Latin with a confusable script are allowed. |
75
79
  | `getThumbnail` | `(entry: UploadResult) => string \| undefined` | none | Optional override for the displayed thumbnail. |
80
+ | `transformResolver` | `TransformResolver` | none | Map an `AssetTransform` to a derivative URL (your image CDN). See [Asset transformations](#asset-transformations--variants). |
81
+ | `dedupe` | `boolean` | `false` | When `true`, hash uploads (SHA-256) and reuse an existing asset with the same content instead of re-uploading. |
82
+ | `sniffContent` | `boolean` | `false` | When `true`, reject a file whose magic-byte content contradicts its declared `file.type` (defense-in-depth beyond MIME/extension). |
83
+ | `onAssetDeleted` | `(asset: UploadResult) => void \| Promise<void>` | none | Lifecycle hook fired when an asset is deleted via the default source; release backend objects here (`blob:` URLs are auto-revoked). |
76
84
 
77
85
  ### Imperative API on the plugin context
78
86
 
@@ -82,6 +90,13 @@ function createAssetManagerPlugin(options: AssetManagerOptions): StudioPlugin;
82
90
  | `getAssetRegistry` | `(ctx) => AssetRegistry \| undefined` | Read the runtime registry after `onInit`. |
83
91
  | `createAssetReference` | `(id) => string` | Produce a stable `asset://<id>` reference for IR. |
84
92
 
93
+ Runtime events:
94
+
95
+ | Event constant | Event name | Payload |
96
+ | -------------- | ---------- | ------- |
97
+ | `ASSET_MANAGER_UPLOADED_EVENT` | `asset-manager:uploaded` | `AssetManagerUploadedEvent` — `{ asset, reference }` after validation and registry insertion. |
98
+ | `ASSET_MANAGER_ERROR_EVENT` | `asset-manager:error` | `AssetManagerErrorEvent` — `{ code, message }` for upload validation or adapter failures. |
99
+
85
100
  ### `UploadAdapter`
86
101
 
87
102
  ```ts
@@ -119,6 +134,49 @@ interface UploadResult {
119
134
  | `headers` | none | Extra headers on the presign POST (e.g., auth). |
120
135
  | `idGenerator` | `crypto.randomUUID()` | Asset id override. |
121
136
 
137
+ ### Resumable / multipart upload
138
+
139
+ Large media can upload in parts, retry per part, and resume after an interruption (refresh, network drop). Opt in with the `resumable` option; files at or above the threshold take the resumable path, everything else stays single-shot.
140
+
141
+ ```ts
142
+ import { createAssetManagerPlugin } from "@anvilkit/plugin-asset-manager";
143
+ import { s3PresignedAdapter } from "@anvilkit/plugin-asset-manager/adapters/s3";
144
+ import { s3MultipartAdapter } from "@anvilkit/plugin-asset-manager/adapters/s3-multipart";
145
+
146
+ createAssetManagerPlugin({
147
+ uploader: s3PresignedAdapter({ presignEndpoint: "/api/sign" }), // small files
148
+ resumable: {
149
+ adapter: s3MultipartAdapter({ endpoint: "/api/s3-multipart" }),
150
+ partSize: 8 * 1024 * 1024, // bytes/part (clamped up to S3's 5 MiB min)
151
+ threshold: 16 * 1024 * 1024, // route files ≥ this through multipart
152
+ // sessionStore defaults to localStorage (in-memory fallback)
153
+ },
154
+ });
155
+ ```
156
+
157
+ `ResumableUploadConfig`:
158
+
159
+ | Field | Default | Purpose |
160
+ | -------------- | ------------------ | ---------------------------------------------------------------------------------------- |
161
+ | `adapter` | _required_ | A `ResumableUploadAdapter` (`begin` / `uploadPart` / `complete` / `abort`). |
162
+ | `partSize` | 8 MiB | Bytes per part. |
163
+ | `threshold` | `partSize` | Minimum file size routed through the resumable path; smaller files use `uploader`. |
164
+ | `sessionStore` | localStorage store | Where in-progress sessions persist (`createUploadSessionStore`, or a custom one). |
165
+
166
+ **Resume is automatic** when a persistent session store is available. Progress is persisted (keyed by a stable file fingerprint of name + size + last-modified) via the session store; re-selecting the same interrupted file reconciles against the backend and skips the parts already accepted — no explicit "resume" action is needed. The default store uses `localStorage`; where that is unavailable (SSR, private mode) it falls back to an in-memory store, so resume then holds only within the same page session.
167
+
168
+ **`s3MultipartAdapter` (`./adapters/s3-multipart`)** is dependency-free — like `s3PresignedAdapter`, it never bundles the AWS SDK. It brokers every S3 operation through one host `endpoint` POSTed JSON discriminated by `action`:
169
+
170
+ | `action` | Host runs | Returns |
171
+ | ------------ | -------------------------- | -------------------------------- |
172
+ | `create` | `CreateMultipartUpload` | `{ uploadId, key?, partSize? }` |
173
+ | `sign-part` | presign one `UploadPart` | `{ url, headers? }` |
174
+ | `complete` | `CompleteMultipartUpload` | `{ url, publicUrl?, id? }` |
175
+ | `abort` | `AbortMultipartUpload` | — |
176
+ | `list-parts` | `ListParts` (resume) | `{ parts, key? }` (404 ⇒ gone) |
177
+
178
+ The part PUT must expose its `ETag` to the browser — set S3 CORS `ExposeHeaders: ["ETag"]`. For CSP, pass `s3Multipart: { endpoint, bucketHost, publicHost? }` to `getRequiredCsp`.
179
+
122
180
  ### `AssetRegistry`
123
181
 
124
182
  ```ts
@@ -183,9 +241,38 @@ interface CreateIRAssetResolverOptions {
183
241
  readonly registry: AssetRegistry;
184
242
  readonly dataUrlAllowlistOptIn?: boolean;
185
243
  readonly allowMixedScriptHostnames?: boolean;
244
+ readonly transformResolver?: TransformResolver; // see Asset transformations
186
245
  }
187
246
  ```
188
247
 
248
+ ### Asset transformations / variants
249
+
250
+ Request derivative renditions (resize / crop / format / quality) without the plugin processing any bytes — a transform is a declarative `AssetTransform` carried on the reference (`asset://<id>?w=800&fm=webp`), and a host `transformResolver` maps it to a derivative URL produced by your image CDN / service. The IR resolver applies it at export / render time and **re-validates the derivative URL** through the same trust boundary as any asset URL (a hostile derivative is rejected). When no transform is requested, or the resolver returns `undefined`, the original URL is used.
251
+
252
+ ```ts
253
+ import { createAssetManagerPlugin, createAssetReference } from "@anvilkit/plugin-asset-manager";
254
+ import { createQueryParamTransformResolver } from "@anvilkit/plugin-asset-manager/transform";
255
+
256
+ createAssetManagerPlugin({
257
+ // Built-in query-param mapping (imgix-style w/h/fit/fm/q/dpr by default;
258
+ // param names + fit/format vocab are configurable for other CDNs).
259
+ transformResolver: createQueryParamTransformResolver(),
260
+ });
261
+
262
+ // A component stores a transform-bearing reference:
263
+ createAssetReference("asset-1", { width: 800, format: "webp" });
264
+ // → "asset://asset-1?w=800&fm=webp" → resolves to e.g. "https://cdn/asset-1.png?w=800&fm=webp"
265
+ ```
266
+
267
+ `AssetTransform` fields: `width` / `height` (positive integers), `fit` (`cover` \| `contain` \| `fill` \| `inside` \| `outside`), `format` (`webp` \| `avif` \| `jpeg` \| `png` \| `auto`), `quality` (1–100), `dpr`.
268
+
269
+ | Export (`./transform`) | Purpose |
270
+ | ------------------------------------- | ----------------------------------------------------------------------- |
271
+ | `createQueryParamTransformResolver` | Build a `TransformResolver` that appends query params to the asset URL. |
272
+ | `deriveVariantUrl(asset, t, resolver)`| Resolve a derivative URL live (non-IR), falling back to the original. |
273
+
274
+ A derivative's dimensional metadata is host-owned, so the resolver drops stale `width`/`height`/`size` from the resolution while preserving `attribution` (a required credit survives a resize).
275
+
189
276
  ### Validation & security
190
277
 
191
278
  ```ts
@@ -202,6 +289,8 @@ Throws `AssetValidationError` on bad input. Always enforces:
202
289
  - Path traversal: `../` and percent-encoded variants (`%2e%2e/`, `%2e%2e%2f`) rejected on `http`/`https`/`blob` URLs.
203
290
  - IDN homoglyph: hostnames mixing Latin with Cyrillic or Greek are rejected unless `allowMixedScriptHostnames: true`. Single-script IDN hosts (`münchen.de`, `日本.jp`, `россия.рф`) are always allowed.
204
291
 
292
+ Selected files are checked against `maxFileSize`, `acceptedMimeTypes`, and `acceptedFileExtensions` before the adapter runs. Because browser `file.type` is host-set and often empty or spoofable, **`sniffContent: true`** adds magic-byte inspection: a file whose detected type contradicts its declared, specific `file.type` is rejected (`CONTENT_TYPE_MISMATCH`). It's defense-in-depth — unsignable types and generic declarations pass, and the backend should still validate independently.
293
+
205
294
  ### CSP advisor
206
295
 
207
296
  ```ts
@@ -213,6 +302,9 @@ function getRequiredCsp(options: RequiredCspOptions): RequiredCsp;
213
302
  | `dataUrlUploader` | _(none)_ | `data:` | `data:` |
214
303
  | `inMemoryUploader` | _(none)_ | `blob:` | `blob:` |
215
304
  | `s3PresignedAdapter` | presign origin + `publicHost` | `publicHost` ?? presign | `publicHost` ?? presign |
305
+ | `s3MultipartAdapter` | `endpoint` + `bucketHost` + `publicHost` | `publicHost` ?? `bucketHost` | `publicHost` ?? `bucketHost` |
306
+
307
+ Pass `s3Multipart: { endpoint, bucketHost?, publicHost? }` (single or array) alongside `dataUrl` / `inMemory` / `s3`.
216
308
 
217
309
  ### React UI (`./ui`)
218
310
 
@@ -260,6 +352,7 @@ Full-jitter exponential backoff; the optional `retryAfterMs` on a `RetryableErro
260
352
  | ------------------------------ | --------------------------- |
261
353
  | `FILE_TOO_LARGE` | pre-upload file validation |
262
354
  | `UNSUPPORTED_MIME_TYPE` | pre-upload file validation |
355
+ | `UNSUPPORTED_FILE_EXTENSION` | pre-upload file validation |
263
356
  | `INVALID_UPLOAD_ID` | `validateUploadResult` |
264
357
  | `EMPTY_UPLOAD_URL` | `validateUploadResult` |
265
358
  | `UNSCHEMED_UPLOAD_URL` | `validateUploadResult` |
@@ -418,7 +511,7 @@ Only `name`, `size`, and `mimeType` are considered safe to log. If your host end
418
511
  3. **Wire CSP.** Call `getRequiredCsp(...)` and merge the result into your `connect-src` / `img-src` / `media-src` builder. Re-run when you add or remove an adapter.
419
512
  4. **Choose a persistence story.** Plugin state is in-memory; host stores `publicUrl`s server-side and re-seeds on boot.
420
513
  5. **Monitor.** Subscribe to `asset-manager:error` event bus envelopes for upload failures, and log `AssetResolutionError.code` from your export pipeline so `ASSET_NOT_FOUND` / `ASSET_URL_REJECTED` / `ASSET_VALIDATION_FAILED` get separate alerts.
421
- 6. **Lock down the bundle.** `.size-limit.json` keeps the headless entry under 6 KB gzip and the UI subpath under 12 KB. CI gates both.
514
+ 6. **Lock down the bundle.** `.size-limit.json` keeps the headless entry under 8 KB gzip and the UI subpath under 12 KB. CI gates both.
422
515
 
423
516
  ### Optional UI is a separate entry
424
517
 
@@ -1,4 +1,5 @@
1
1
  import type { UploadAdapter } from "../types/types.js";
2
+ /** Options for the development-only data URL uploader. */
2
3
  export interface DataUrlUploaderOptions {
3
4
  /**
4
5
  * Maximum **raw file size** in bytes (default 1 MB). This bounds the input
@@ -1 +1 @@
1
- {"version":3,"file":"data-url.d.cts","sourceRoot":"","sources":["../../src/adapters/data-url.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAgB,MAAM,mBAAmB,CAAC;AAIrE,MAAM,WAAW,sBAAsB;IACtC;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,CAC9B,OAAO,GAAE,sBAA2B,GAClC,aAAa,CA+Bf"}
1
+ {"version":3,"file":"data-url.d.cts","sourceRoot":"","sources":["../../src/adapters/data-url.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAgB,MAAM,mBAAmB,CAAC;AAIrE,0DAA0D;AAC1D,MAAM,WAAW,sBAAsB;IACtC;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,CAC9B,OAAO,GAAE,sBAA2B,GAClC,aAAa,CA+Bf"}
@@ -1,4 +1,5 @@
1
1
  import type { UploadAdapter } from "../types/types.js";
2
+ /** Options for the development-only data URL uploader. */
2
3
  export interface DataUrlUploaderOptions {
3
4
  /**
4
5
  * Maximum **raw file size** in bytes (default 1 MB). This bounds the input
@@ -1 +1 @@
1
- {"version":3,"file":"data-url.d.ts","sourceRoot":"","sources":["../../src/adapters/data-url.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAgB,MAAM,mBAAmB,CAAC;AAIrE,MAAM,WAAW,sBAAsB;IACtC;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,CAC9B,OAAO,GAAE,sBAA2B,GAClC,aAAa,CA+Bf"}
1
+ {"version":3,"file":"data-url.d.ts","sourceRoot":"","sources":["../../src/adapters/data-url.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAgB,MAAM,mBAAmB,CAAC;AAIrE,0DAA0D;AAC1D,MAAM,WAAW,sBAAsB;IACtC;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,CAC9B,OAAO,GAAE,sBAA2B,GAClC,aAAa,CA+Bf"}
@@ -0,0 +1,425 @@
1
+ "use strict";
2
+ var __webpack_require__ = {};
3
+ (()=>{
4
+ __webpack_require__.d = (exports1, getters, values)=>{
5
+ var define = (defs, kind)=>{
6
+ for(var key in defs)if (__webpack_require__.o(defs, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, {
7
+ enumerable: true,
8
+ [kind]: defs[key]
9
+ });
10
+ };
11
+ define(getters, "get");
12
+ define(values, "value");
13
+ };
14
+ })();
15
+ (()=>{
16
+ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop);
17
+ })();
18
+ (()=>{
19
+ __webpack_require__.r = (exports1)=>{
20
+ if ("u" > typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, {
21
+ value: 'Module'
22
+ });
23
+ Object.defineProperty(exports1, '__esModule', {
24
+ value: true
25
+ });
26
+ };
27
+ })();
28
+ var __webpack_exports__ = {};
29
+ __webpack_require__.r(__webpack_exports__);
30
+ __webpack_require__.d(__webpack_exports__, {
31
+ s3MultipartAdapter: ()=>s3MultipartAdapter
32
+ });
33
+ const errors_cjs_namespaceObject = require("../utils/errors.cjs");
34
+ const retry_cjs_namespaceObject = require("../utils/retry.cjs");
35
+ const DEFAULT_PART_SIZE = 8388608;
36
+ const S3_MIN_PART_SIZE = 5242880;
37
+ function s3MultipartAdapter(options) {
38
+ const fetchImpl = options.fetch ?? globalThis.fetch;
39
+ if ("function" != typeof fetchImpl) throw new Error("s3MultipartAdapter: no `fetch` implementation available. Pass `options.fetch`.");
40
+ const generateId = options.idGenerator ?? defaultIdGenerator;
41
+ const partSize = Math.max(options.partSize ?? DEFAULT_PART_SIZE, S3_MIN_PART_SIZE);
42
+ const endpoint = "string" == typeof options.endpoint ? options.endpoint : options.endpoint.toString();
43
+ const retry = options.retry ?? {};
44
+ const keyOf = (session)=>"string" == typeof session.meta?.key ? session.meta.key : void 0;
45
+ return {
46
+ async begin (file, resume, callOptions) {
47
+ const { signal, dispose } = combineSignals(options.signal, callOptions?.signal);
48
+ try {
49
+ if (resume) {
50
+ const resumeKey = "string" == typeof resume.meta?.key ? resume.meta.key : void 0;
51
+ const listed = await (0, retry_cjs_namespaceObject.withRetry)(()=>listParts(endpoint, fetchImpl, options.headers, resume.uploadId, resumeKey, signal), {
52
+ ...retry,
53
+ ...signal ? {
54
+ signal
55
+ } : {}
56
+ });
57
+ if (void 0 !== listed) {
58
+ const key = resumeKey ?? listed.key;
59
+ return {
60
+ uploadId: resume.uploadId,
61
+ parts: listed.parts,
62
+ partSize: resume.partSize,
63
+ meta: buildMeta(key, resume.meta)
64
+ };
65
+ }
66
+ }
67
+ const created = await (0, retry_cjs_namespaceObject.withRetry)(()=>createSession(endpoint, fetchImpl, options.headers, file, partSize, signal), {
68
+ ...retry,
69
+ ...signal ? {
70
+ signal
71
+ } : {}
72
+ });
73
+ return {
74
+ uploadId: created.uploadId,
75
+ parts: [],
76
+ partSize: clampPartSize(created.partSize) ?? partSize,
77
+ meta: buildMeta(created.key, fileMeta(file))
78
+ };
79
+ } finally{
80
+ dispose();
81
+ }
82
+ },
83
+ async uploadPart (session, part, callOptions) {
84
+ const { signal, dispose } = combineSignals(options.signal, callOptions?.signal);
85
+ try {
86
+ const signed = await signPart(endpoint, fetchImpl, options.headers, session.uploadId, keyOf(session), part.partNumber, signal);
87
+ const etag = await putPart(fetchImpl, signed, part, signal);
88
+ return {
89
+ partNumber: part.partNumber,
90
+ etag
91
+ };
92
+ } finally{
93
+ dispose();
94
+ }
95
+ },
96
+ async complete (session, parts, callOptions) {
97
+ const { signal, dispose } = combineSignals(options.signal, callOptions?.signal);
98
+ try {
99
+ const completed = await (0, retry_cjs_namespaceObject.withRetry)(()=>completeSession(endpoint, fetchImpl, options.headers, session.uploadId, keyOf(session), parts, signal), {
100
+ ...retry,
101
+ ...signal ? {
102
+ signal
103
+ } : {}
104
+ });
105
+ const id = completed.id ?? generateId();
106
+ const url = completed.publicUrl ?? stripQueryAndFragment(completed.url);
107
+ return buildResult(id, url, session.meta);
108
+ } finally{
109
+ dispose();
110
+ }
111
+ },
112
+ async abort (session, callOptions) {
113
+ const { signal, dispose } = combineSignals(options.signal, callOptions?.signal);
114
+ try {
115
+ await (0, retry_cjs_namespaceObject.withRetry)(()=>abortSession(endpoint, fetchImpl, options.headers, session.uploadId, keyOf(session), signal), {
116
+ ...retry,
117
+ ...signal ? {
118
+ signal
119
+ } : {}
120
+ });
121
+ } finally{
122
+ dispose();
123
+ }
124
+ }
125
+ };
126
+ }
127
+ async function createSession(endpoint, fetchImpl, headers, file, partSize, signal) {
128
+ const payload = await postAction(endpoint, fetchImpl, headers, {
129
+ action: "create",
130
+ name: file.name,
131
+ type: file.type,
132
+ size: file.size,
133
+ partSize
134
+ }, signal);
135
+ if (!isObject(payload) || "string" != typeof payload.uploadId || "" === payload.uploadId) throw new errors_cjs_namespaceObject.AssetValidationError("UPLOAD_FAILED", "s3MultipartAdapter: create response missing `uploadId`.");
136
+ if (!isOptionalString(payload.key)) throw badShape("create", "`key` must be a string");
137
+ if (void 0 !== payload.partSize && !isPositiveSafeInteger(payload.partSize)) throw badShape("create", "`partSize` must be a positive integer");
138
+ return {
139
+ uploadId: payload.uploadId,
140
+ ...void 0 !== payload.key ? {
141
+ key: payload.key
142
+ } : {},
143
+ ...void 0 !== payload.partSize ? {
144
+ partSize: payload.partSize
145
+ } : {}
146
+ };
147
+ }
148
+ async function signPart(endpoint, fetchImpl, headers, uploadId, key, partNumber, signal) {
149
+ const payload = await postAction(endpoint, fetchImpl, headers, {
150
+ action: "sign-part",
151
+ uploadId,
152
+ key,
153
+ partNumber
154
+ }, signal);
155
+ if (!isObject(payload) || "string" != typeof payload.url || "" === payload.url) throw new errors_cjs_namespaceObject.AssetValidationError("UPLOAD_FAILED", "s3MultipartAdapter: sign-part response missing `url`.");
156
+ if (void 0 !== payload.headers && !isStringRecord(payload.headers)) throw badShape("sign-part", "`headers` must be a string map");
157
+ return {
158
+ url: payload.url,
159
+ ...void 0 !== payload.headers ? {
160
+ headers: payload.headers
161
+ } : {}
162
+ };
163
+ }
164
+ async function completeSession(endpoint, fetchImpl, headers, uploadId, key, parts, signal) {
165
+ const payload = await postAction(endpoint, fetchImpl, headers, {
166
+ action: "complete",
167
+ uploadId,
168
+ key,
169
+ parts
170
+ }, signal);
171
+ if (!isObject(payload) || "string" != typeof payload.url || "" === payload.url) throw new errors_cjs_namespaceObject.AssetValidationError("UPLOAD_FAILED", "s3MultipartAdapter: complete response missing `url`.");
172
+ if (!isOptionalString(payload.publicUrl)) throw badShape("complete", "`publicUrl` must be a string");
173
+ if (!isOptionalString(payload.id)) throw badShape("complete", "`id` must be a string");
174
+ return {
175
+ url: payload.url,
176
+ ...void 0 !== payload.publicUrl ? {
177
+ publicUrl: payload.publicUrl
178
+ } : {},
179
+ ...void 0 !== payload.id ? {
180
+ id: payload.id
181
+ } : {}
182
+ };
183
+ }
184
+ function abortSession(endpoint, fetchImpl, headers, uploadId, key, signal) {
185
+ return postAction(endpoint, fetchImpl, headers, {
186
+ action: "abort",
187
+ uploadId,
188
+ key
189
+ }, signal);
190
+ }
191
+ async function listParts(endpoint, fetchImpl, headers, uploadId, key, signal) {
192
+ let response;
193
+ try {
194
+ response = await fetchImpl(endpoint, {
195
+ method: "POST",
196
+ headers: {
197
+ "Content-Type": "application/json",
198
+ ...headers ?? {}
199
+ },
200
+ body: JSON.stringify({
201
+ action: "list-parts",
202
+ uploadId,
203
+ key
204
+ }),
205
+ ...signal ? {
206
+ signal
207
+ } : {}
208
+ });
209
+ } catch (cause) {
210
+ throw new retry_cjs_namespaceObject.RetryableError(`s3MultipartAdapter: list-parts request failed (${describeError(cause)}).`, {
211
+ cause
212
+ });
213
+ }
214
+ if (404 === response.status) return;
215
+ if (response.status >= 500) throw new retry_cjs_namespaceObject.RetryableError(`s3MultipartAdapter: list-parts returned ${response.status}.`, {
216
+ retryAfterMs: parseRetryAfter(response.headers.get("retry-after"))
217
+ });
218
+ if (!response.ok) throw new errors_cjs_namespaceObject.AssetValidationError("UPLOAD_FAILED", `s3MultipartAdapter: list-parts returned ${response.status}.`);
219
+ const payload = await parseJson(response, "list-parts");
220
+ if (!isObject(payload) || !isPartTagArray(payload.parts)) throw new errors_cjs_namespaceObject.AssetValidationError("UPLOAD_FAILED", "s3MultipartAdapter: list-parts response has a missing or malformed `parts` array.");
221
+ if (!isOptionalString(payload.key)) throw badShape("list-parts", "`key` must be a string");
222
+ return {
223
+ parts: payload.parts,
224
+ ...void 0 !== payload.key ? {
225
+ key: payload.key
226
+ } : {}
227
+ };
228
+ }
229
+ async function putPart(fetchImpl, signed, part, signal) {
230
+ let response;
231
+ try {
232
+ response = await fetchImpl(signed.url, {
233
+ method: "PUT",
234
+ body: part.blob,
235
+ ...signed.headers ? {
236
+ headers: signed.headers
237
+ } : {},
238
+ ...signal ? {
239
+ signal
240
+ } : {}
241
+ });
242
+ } catch (cause) {
243
+ throw new retry_cjs_namespaceObject.RetryableError(`s3MultipartAdapter: part ${part.partNumber} PUT failed (${describeError(cause)}).`, {
244
+ cause
245
+ });
246
+ }
247
+ if (response.status >= 500) throw new retry_cjs_namespaceObject.RetryableError(`s3MultipartAdapter: part ${part.partNumber} PUT returned ${response.status}.`, {
248
+ retryAfterMs: parseRetryAfter(response.headers.get("retry-after"))
249
+ });
250
+ if (!response.ok) throw new errors_cjs_namespaceObject.AssetValidationError("UPLOAD_FAILED", `s3MultipartAdapter: part ${part.partNumber} PUT returned ${response.status}.`);
251
+ const etag = response.headers.get("etag");
252
+ if (null === etag || "" === etag) throw new errors_cjs_namespaceObject.AssetValidationError("UPLOAD_FAILED", `s3MultipartAdapter: part ${part.partNumber} PUT returned no ETag header. Expose it via S3 CORS \`ExposeHeaders: ["ETag"]\`.`);
253
+ return etag;
254
+ }
255
+ async function postAction(endpoint, fetchImpl, headers, body, signal) {
256
+ let response;
257
+ try {
258
+ response = await fetchImpl(endpoint, {
259
+ method: "POST",
260
+ headers: {
261
+ "Content-Type": "application/json",
262
+ ...headers ?? {}
263
+ },
264
+ body: JSON.stringify(body),
265
+ ...signal ? {
266
+ signal
267
+ } : {}
268
+ });
269
+ } catch (cause) {
270
+ throw new retry_cjs_namespaceObject.RetryableError(`s3MultipartAdapter: ${String(body.action)} request failed (${describeError(cause)}).`, {
271
+ cause
272
+ });
273
+ }
274
+ if (response.status >= 500) throw new retry_cjs_namespaceObject.RetryableError(`s3MultipartAdapter: ${String(body.action)} returned ${response.status}.`, {
275
+ retryAfterMs: parseRetryAfter(response.headers.get("retry-after"))
276
+ });
277
+ if (!response.ok) throw new errors_cjs_namespaceObject.AssetValidationError("UPLOAD_FAILED", `s3MultipartAdapter: ${String(body.action)} returned ${response.status}.`);
278
+ if ("abort" === body.action) return;
279
+ return parseJson(response, String(body.action));
280
+ }
281
+ async function parseJson(response, action) {
282
+ try {
283
+ return await response.json();
284
+ } catch (cause) {
285
+ throw new errors_cjs_namespaceObject.AssetValidationError("UPLOAD_FAILED", `s3MultipartAdapter: ${action} response was not JSON.`, {
286
+ cause
287
+ });
288
+ }
289
+ }
290
+ function fileMeta(file) {
291
+ return {
292
+ name: file.name,
293
+ size: file.size,
294
+ ...file.type ? {
295
+ type: file.type
296
+ } : {}
297
+ };
298
+ }
299
+ function buildMeta(key, base) {
300
+ return Object.freeze({
301
+ ...base ?? {},
302
+ ...void 0 !== key ? {
303
+ key
304
+ } : {}
305
+ });
306
+ }
307
+ function buildResult(id, url, meta) {
308
+ const name = "string" == typeof meta?.name ? meta.name : void 0;
309
+ const size = "number" == typeof meta?.size ? meta.size : void 0;
310
+ const type = "string" == typeof meta?.type ? meta.type : void 0;
311
+ const resultMeta = void 0 !== size || void 0 !== type ? {
312
+ ...void 0 !== size ? {
313
+ size
314
+ } : {},
315
+ ...void 0 !== type ? {
316
+ mimeType: type
317
+ } : {}
318
+ } : void 0;
319
+ return {
320
+ id,
321
+ url,
322
+ ...name ? {
323
+ name
324
+ } : {},
325
+ ...resultMeta ? {
326
+ meta: resultMeta
327
+ } : {}
328
+ };
329
+ }
330
+ function clampPartSize(value) {
331
+ if ("number" != typeof value || !Number.isSafeInteger(value) || value <= 0) return;
332
+ return Math.max(value, S3_MIN_PART_SIZE);
333
+ }
334
+ function isObject(value) {
335
+ return null !== value && "object" == typeof value && !Array.isArray(value);
336
+ }
337
+ function isOptionalString(value) {
338
+ return void 0 === value || "string" == typeof value;
339
+ }
340
+ function isPositiveSafeInteger(value) {
341
+ return "number" == typeof value && Number.isSafeInteger(value) && value > 0;
342
+ }
343
+ function isStringRecord(value) {
344
+ return isObject(value) && Object.values(value).every((v)=>"string" == typeof v);
345
+ }
346
+ function isPartTagArray(value) {
347
+ return Array.isArray(value) && value.every((p)=>isObject(p) && "number" == typeof p.partNumber && Number.isInteger(p.partNumber) && p.partNumber >= 1 && "string" == typeof p.etag);
348
+ }
349
+ function badShape(action, detail) {
350
+ return new errors_cjs_namespaceObject.AssetValidationError("UPLOAD_FAILED", `s3MultipartAdapter: ${action} response has an invalid shape — ${detail}.`);
351
+ }
352
+ function stripQueryAndFragment(url) {
353
+ const cuts = [
354
+ url.indexOf("?"),
355
+ url.indexOf("#")
356
+ ].filter((i)=>-1 !== i);
357
+ return 0 === cuts.length ? url : url.slice(0, Math.min(...cuts));
358
+ }
359
+ function parseRetryAfter(header) {
360
+ if (null === header) return;
361
+ const seconds = Number(header);
362
+ if (Number.isFinite(seconds) && seconds >= 0) return 1000 * seconds;
363
+ const date = Date.parse(header);
364
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
365
+ }
366
+ function describeError(error) {
367
+ if (error instanceof Error) return error.message || error.name;
368
+ return String(error);
369
+ }
370
+ function defaultIdGenerator() {
371
+ if (void 0 !== globalThis.crypto && "function" == typeof globalThis.crypto.randomUUID) return globalThis.crypto.randomUUID();
372
+ return `asset-${Math.random().toString(36).slice(2)}-${Date.now()}`;
373
+ }
374
+ const NOOP_DISPOSE = ()=>void 0;
375
+ function combineSignals(a, b) {
376
+ if (!a) return {
377
+ signal: b,
378
+ dispose: NOOP_DISPOSE
379
+ };
380
+ if (!b) return {
381
+ signal: a,
382
+ dispose: NOOP_DISPOSE
383
+ };
384
+ if (a === b) return {
385
+ signal: a,
386
+ dispose: NOOP_DISPOSE
387
+ };
388
+ const anyImpl = AbortSignal.any;
389
+ if ("function" == typeof anyImpl) return {
390
+ signal: anyImpl([
391
+ a,
392
+ b
393
+ ]),
394
+ dispose: NOOP_DISPOSE
395
+ };
396
+ const controller = new AbortController();
397
+ const onAbortA = ()=>{
398
+ if (!controller.signal.aborted) controller.abort(a.reason);
399
+ };
400
+ const onAbortB = ()=>{
401
+ if (!controller.signal.aborted) controller.abort(b.reason);
402
+ };
403
+ if (a.aborted) onAbortA();
404
+ else a.addEventListener("abort", onAbortA, {
405
+ once: true
406
+ });
407
+ if (b.aborted) onAbortB();
408
+ else b.addEventListener("abort", onAbortB, {
409
+ once: true
410
+ });
411
+ return {
412
+ signal: controller.signal,
413
+ dispose: ()=>{
414
+ a.removeEventListener("abort", onAbortA);
415
+ b.removeEventListener("abort", onAbortB);
416
+ }
417
+ };
418
+ }
419
+ exports.s3MultipartAdapter = __webpack_exports__.s3MultipartAdapter;
420
+ for(var __rspack_i in __webpack_exports__)if (-1 === [
421
+ "s3MultipartAdapter"
422
+ ].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
423
+ Object.defineProperty(exports, '__esModule', {
424
+ value: true
425
+ });