canopycms 0.0.57 → 0.0.59

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 (250) hide show
  1. package/dist/ai/json-to-markdown.js +74 -3
  2. package/dist/ai/json-to-markdown.js.map +1 -1
  3. package/dist/api/__test__/mock-client.d.ts +9 -5
  4. package/dist/api/__test__/mock-client.d.ts.map +1 -1
  5. package/dist/api/__test__/mock-client.js +15 -7
  6. package/dist/api/__test__/mock-client.js.map +1 -1
  7. package/dist/api/assets.d.ts +100 -36
  8. package/dist/api/assets.d.ts.map +1 -1
  9. package/dist/api/assets.js +338 -51
  10. package/dist/api/assets.js.map +1 -1
  11. package/dist/api/client.d.ts +16 -5
  12. package/dist/api/client.d.ts.map +1 -1
  13. package/dist/api/client.js +24 -6
  14. package/dist/api/client.js.map +1 -1
  15. package/dist/api/entries.d.ts +4 -4
  16. package/dist/api/index.d.ts +1 -1
  17. package/dist/api/index.d.ts.map +1 -1
  18. package/dist/api/index.js.map +1 -1
  19. package/dist/api/route-builder.d.ts +9 -0
  20. package/dist/api/route-builder.d.ts.map +1 -1
  21. package/dist/api/route-builder.js +1 -0
  22. package/dist/api/route-builder.js.map +1 -1
  23. package/dist/api/schema.d.ts +4 -4
  24. package/dist/api/types.d.ts +11 -1
  25. package/dist/api/types.d.ts.map +1 -1
  26. package/dist/assets/asset-prefixes.d.ts +31 -0
  27. package/dist/assets/asset-prefixes.d.ts.map +1 -0
  28. package/dist/assets/asset-prefixes.js +20 -0
  29. package/dist/assets/asset-prefixes.js.map +1 -0
  30. package/dist/assets/asset-src.d.ts +23 -0
  31. package/dist/assets/asset-src.d.ts.map +1 -0
  32. package/dist/assets/asset-src.js +28 -0
  33. package/dist/assets/asset-src.js.map +1 -0
  34. package/dist/assets/asset-url.d.ts +35 -0
  35. package/dist/assets/asset-url.d.ts.map +1 -0
  36. package/dist/assets/asset-url.js +77 -0
  37. package/dist/assets/asset-url.js.map +1 -0
  38. package/dist/assets/factory.d.ts +18 -0
  39. package/dist/assets/factory.d.ts.map +1 -0
  40. package/dist/assets/factory.js +42 -0
  41. package/dist/assets/factory.js.map +1 -0
  42. package/dist/assets/finalize.d.ts +36 -0
  43. package/dist/assets/finalize.d.ts.map +1 -0
  44. package/dist/assets/finalize.js +72 -0
  45. package/dist/assets/finalize.js.map +1 -0
  46. package/dist/assets/index.d.ts +20 -0
  47. package/dist/assets/index.d.ts.map +1 -0
  48. package/dist/assets/index.js +19 -0
  49. package/dist/assets/index.js.map +1 -0
  50. package/dist/assets/keys.d.ts +56 -0
  51. package/dist/assets/keys.d.ts.map +1 -0
  52. package/dist/assets/keys.js +88 -0
  53. package/dist/assets/keys.js.map +1 -0
  54. package/dist/assets/pipeline.d.ts +50 -0
  55. package/dist/assets/pipeline.d.ts.map +1 -0
  56. package/dist/assets/pipeline.js +202 -0
  57. package/dist/assets/pipeline.js.map +1 -0
  58. package/dist/assets/store-local.d.ts +84 -0
  59. package/dist/assets/store-local.d.ts.map +1 -0
  60. package/dist/assets/store-local.js +254 -0
  61. package/dist/assets/store-local.js.map +1 -0
  62. package/dist/assets/store-s3.d.ts +71 -0
  63. package/dist/assets/store-s3.d.ts.map +1 -0
  64. package/dist/assets/store-s3.js +183 -0
  65. package/dist/assets/store-s3.js.map +1 -0
  66. package/dist/assets/svg-sanitizer.d.ts +37 -0
  67. package/dist/assets/svg-sanitizer.d.ts.map +1 -0
  68. package/dist/assets/svg-sanitizer.js +222 -0
  69. package/dist/assets/svg-sanitizer.js.map +1 -0
  70. package/dist/assets/transform-directives.d.ts +97 -0
  71. package/dist/assets/transform-directives.d.ts.map +1 -0
  72. package/dist/assets/transform-directives.js +267 -0
  73. package/dist/assets/transform-directives.js.map +1 -0
  74. package/dist/assets/transform.d.ts +51 -0
  75. package/dist/assets/transform.d.ts.map +1 -0
  76. package/dist/assets/transform.js +190 -0
  77. package/dist/assets/transform.js.map +1 -0
  78. package/dist/assets/types.d.ts +82 -0
  79. package/dist/assets/types.d.ts.map +1 -0
  80. package/dist/assets/types.js +10 -0
  81. package/dist/assets/types.js.map +1 -0
  82. package/dist/authorization/groups/schema.d.ts +2 -2
  83. package/dist/authorization/permissions/schema.d.ts +2 -2
  84. package/dist/branch-workspace.d.ts.map +1 -1
  85. package/dist/branch-workspace.js +1 -0
  86. package/dist/branch-workspace.js.map +1 -1
  87. package/dist/cli/cli.d.ts +20 -0
  88. package/dist/cli/cli.d.ts.map +1 -1
  89. package/dist/cli/cli.js +9561 -246
  90. package/dist/cli/cli.js.map +1 -1
  91. package/dist/cli/generate-ai-content.js +175 -16
  92. package/dist/cli/init.d.ts.map +1 -1
  93. package/dist/cli/init.js +29 -5
  94. package/dist/cli/init.js.map +1 -1
  95. package/dist/cli/template-files/Dockerfile.cms.template +33 -1
  96. package/dist/cli/template-files/deploy-cms.yml.template +9 -1
  97. package/dist/cli/template-files/dockerignore.template +13 -0
  98. package/dist/cli/template-files/middleware-clerk.ts.template +12 -5
  99. package/dist/cli/template-files/middleware.ts.template +10 -5
  100. package/dist/cli/templates.d.ts +1 -0
  101. package/dist/cli/templates.d.ts.map +1 -1
  102. package/dist/cli/templates.js +3 -0
  103. package/dist/cli/templates.js.map +1 -1
  104. package/dist/config/helpers.d.ts.map +1 -1
  105. package/dist/config/helpers.js +2 -1
  106. package/dist/config/helpers.js.map +1 -1
  107. package/dist/config/index.d.ts +2 -2
  108. package/dist/config/index.d.ts.map +1 -1
  109. package/dist/config/index.js +1 -1
  110. package/dist/config/index.js.map +1 -1
  111. package/dist/config/schemas/config.d.ts +23 -1
  112. package/dist/config/schemas/config.d.ts.map +1 -1
  113. package/dist/config/schemas/config.js +14 -1
  114. package/dist/config/schemas/config.js.map +1 -1
  115. package/dist/config/schemas/field.d.ts +38 -3
  116. package/dist/config/schemas/field.d.ts.map +1 -1
  117. package/dist/config/schemas/field.js +18 -0
  118. package/dist/config/schemas/field.js.map +1 -1
  119. package/dist/config/schemas/media.d.ts +8 -0
  120. package/dist/config/schemas/media.d.ts.map +1 -1
  121. package/dist/config/schemas/media.js +9 -0
  122. package/dist/config/schemas/media.js.map +1 -1
  123. package/dist/config/types.d.ts +63 -3
  124. package/dist/config/types.d.ts.map +1 -1
  125. package/dist/config/types.js +4 -1
  126. package/dist/config/types.js.map +1 -1
  127. package/dist/editor/CanopyEditor.d.ts.map +1 -1
  128. package/dist/editor/CanopyEditor.js +1 -1
  129. package/dist/editor/CanopyEditor.js.map +1 -1
  130. package/dist/editor/Editor.d.ts +2 -0
  131. package/dist/editor/Editor.d.ts.map +1 -1
  132. package/dist/editor/Editor.js +82 -80
  133. package/dist/editor/Editor.js.map +1 -1
  134. package/dist/editor/FormRenderer.d.ts.map +1 -1
  135. package/dist/editor/FormRenderer.js +9 -0
  136. package/dist/editor/FormRenderer.js.map +1 -1
  137. package/dist/editor/components/EditorSidebar.d.ts +6 -1
  138. package/dist/editor/components/EditorSidebar.d.ts.map +1 -1
  139. package/dist/editor/components/EditorSidebar.js +4 -3
  140. package/dist/editor/components/EditorSidebar.js.map +1 -1
  141. package/dist/editor/context/AssetContext.d.ts +25 -0
  142. package/dist/editor/context/AssetContext.d.ts.map +1 -0
  143. package/dist/editor/context/AssetContext.js +26 -0
  144. package/dist/editor/context/AssetContext.js.map +1 -0
  145. package/dist/editor/context/index.d.ts +1 -0
  146. package/dist/editor/context/index.d.ts.map +1 -1
  147. package/dist/editor/context/index.js +1 -0
  148. package/dist/editor/context/index.js.map +1 -1
  149. package/dist/editor/fields/ImageField.d.ts +22 -0
  150. package/dist/editor/fields/ImageField.d.ts.map +1 -0
  151. package/dist/editor/fields/ImageField.js +100 -0
  152. package/dist/editor/fields/ImageField.js.map +1 -0
  153. package/dist/editor/fields/MarkdownField.d.ts.map +1 -1
  154. package/dist/editor/fields/MarkdownField.js +30 -4
  155. package/dist/editor/fields/MarkdownField.js.map +1 -1
  156. package/dist/editor/fields/MdxImageDialog.d.ts +16 -0
  157. package/dist/editor/fields/MdxImageDialog.d.ts.map +1 -0
  158. package/dist/editor/fields/MdxImageDialog.js +73 -0
  159. package/dist/editor/fields/MdxImageDialog.js.map +1 -0
  160. package/dist/editor/media/AssetCard.d.ts +21 -0
  161. package/dist/editor/media/AssetCard.d.ts.map +1 -0
  162. package/dist/editor/media/AssetCard.js +41 -0
  163. package/dist/editor/media/AssetCard.js.map +1 -0
  164. package/dist/editor/media/CropStep.d.ts +26 -0
  165. package/dist/editor/media/CropStep.d.ts.map +1 -0
  166. package/dist/editor/media/CropStep.js +38 -0
  167. package/dist/editor/media/CropStep.js.map +1 -0
  168. package/dist/editor/media/MediaLibrary.d.ts +22 -0
  169. package/dist/editor/media/MediaLibrary.d.ts.map +1 -0
  170. package/dist/editor/media/MediaLibrary.js +17 -0
  171. package/dist/editor/media/MediaLibrary.js.map +1 -0
  172. package/dist/editor/media/MediaLibraryBody.d.ts +17 -0
  173. package/dist/editor/media/MediaLibraryBody.d.ts.map +1 -0
  174. package/dist/editor/media/MediaLibraryBody.js +133 -0
  175. package/dist/editor/media/MediaLibraryBody.js.map +1 -0
  176. package/dist/editor/media/crop-math.d.ts +37 -0
  177. package/dist/editor/media/crop-math.d.ts.map +1 -0
  178. package/dist/editor/media/crop-math.js +62 -0
  179. package/dist/editor/media/crop-math.js.map +1 -0
  180. package/dist/editor/media/upload-asset.d.ts +31 -0
  181. package/dist/editor/media/upload-asset.d.ts.map +1 -0
  182. package/dist/editor/media/upload-asset.js +56 -0
  183. package/dist/editor/media/upload-asset.js.map +1 -0
  184. package/dist/editor/media/upload-constants.d.ts +27 -0
  185. package/dist/editor/media/upload-constants.d.ts.map +1 -0
  186. package/dist/editor/media/upload-constants.js +40 -0
  187. package/dist/editor/media/upload-constants.js.map +1 -0
  188. package/dist/editor/media/useAssetUpload.d.ts +19 -0
  189. package/dist/editor/media/useAssetUpload.d.ts.map +1 -0
  190. package/dist/editor/media/useAssetUpload.js +53 -0
  191. package/dist/editor/media/useAssetUpload.js.map +1 -0
  192. package/dist/editor/media/xhr-upload.d.ts +20 -0
  193. package/dist/editor/media/xhr-upload.d.ts.map +1 -0
  194. package/dist/editor/media/xhr-upload.js +42 -0
  195. package/dist/editor/media/xhr-upload.js.map +1 -0
  196. package/dist/editor/theme.d.ts +2 -1
  197. package/dist/editor/theme.d.ts.map +1 -1
  198. package/dist/editor/theme.js +1 -0
  199. package/dist/editor/theme.js.map +1 -1
  200. package/dist/entry-schema.d.ts +19 -0
  201. package/dist/entry-schema.d.ts.map +1 -1
  202. package/dist/entry-schema.js.map +1 -1
  203. package/dist/git-manager.d.ts +39 -0
  204. package/dist/git-manager.d.ts.map +1 -1
  205. package/dist/git-manager.js +98 -8
  206. package/dist/git-manager.js.map +1 -1
  207. package/dist/http/handler.d.ts +5 -2
  208. package/dist/http/handler.d.ts.map +1 -1
  209. package/dist/http/handler.js +13 -4
  210. package/dist/http/handler.js.map +1 -1
  211. package/dist/http/index.d.ts +2 -2
  212. package/dist/http/index.d.ts.map +1 -1
  213. package/dist/http/index.js +1 -1
  214. package/dist/http/index.js.map +1 -1
  215. package/dist/http/router.d.ts +14 -1
  216. package/dist/http/router.d.ts.map +1 -1
  217. package/dist/http/router.js +4 -1
  218. package/dist/http/router.js.map +1 -1
  219. package/dist/http/types.d.ts +40 -0
  220. package/dist/http/types.d.ts.map +1 -1
  221. package/dist/http/types.js +9 -0
  222. package/dist/http/types.js.map +1 -1
  223. package/dist/index.d.ts +3 -0
  224. package/dist/index.d.ts.map +1 -1
  225. package/dist/index.js +4 -0
  226. package/dist/index.js.map +1 -1
  227. package/dist/schema/schema-store.d.ts +2 -2
  228. package/dist/server.d.ts +45 -0
  229. package/dist/server.d.ts.map +1 -1
  230. package/dist/server.js +41 -0
  231. package/dist/server.js.map +1 -1
  232. package/dist/settings-workspace.d.ts.map +1 -1
  233. package/dist/settings-workspace.js +1 -0
  234. package/dist/settings-workspace.js.map +1 -1
  235. package/dist/utils/git.d.ts +19 -0
  236. package/dist/utils/git.d.ts.map +1 -1
  237. package/dist/utils/git.js +47 -0
  238. package/dist/utils/git.js.map +1 -1
  239. package/dist/validation/entry-validator.d.ts.map +1 -1
  240. package/dist/validation/entry-validator.js +56 -1
  241. package/dist/validation/entry-validator.js.map +1 -1
  242. package/dist/worker/cms-worker.d.ts +28 -0
  243. package/dist/worker/cms-worker.d.ts.map +1 -1
  244. package/dist/worker/cms-worker.js +100 -20
  245. package/dist/worker/cms-worker.js.map +1 -1
  246. package/package.json +13 -1
  247. package/dist/asset-store.d.ts +0 -32
  248. package/dist/asset-store.d.ts.map +0 -1
  249. package/dist/asset-store.js +0 -85
  250. package/dist/asset-store.js.map +0 -1
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Store orchestration around the (pure) finalize pipeline: dedup, write, and
3
+ * best-effort staging cleanup. Split from pipeline.ts so the pipeline itself
4
+ * stays store-agnostic and trivially unit-testable with plain byte fixtures.
5
+ */
6
+ import { runFinalizePipeline } from './pipeline.js';
7
+ import { ASSET_PREFIXES } from './keys.js';
8
+ /**
9
+ * A client-supplied staging key must be exactly `asset-staging/{uuid}` — the
10
+ * shape beginUpload generates. Without this check, finalize's read + cleanup
11
+ * would operate on ANY key the store can reach (asset-meta/, asset-originals/,
12
+ * or in a shared bucket even builds/), letting a non-admin delete arbitrary
13
+ * objects via the best-effort deleteStaging. Stores with overridden staging
14
+ * prefixes must parameterize this check if that escape hatch is ever used.
15
+ */
16
+ const STAGING_UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
17
+ export function isValidStagingKey(key) {
18
+ const prefix = `${ASSET_PREFIXES.staging}/`;
19
+ return key.startsWith(prefix) && STAGING_UUID_PATTERN.test(key.slice(prefix.length));
20
+ }
21
+ /**
22
+ * Run the finalize pipeline over raw bytes and persist the result.
23
+ *
24
+ * Order matters here too: dedup FIRST (after hashing, inside the pipeline) -
25
+ * if an identical blob already exists, return the existing meta and skip all
26
+ * writes entirely (first-name-wins). Otherwise write the original, then the
27
+ * optional public object, then `putMetaIfAbsent` LAST: meta is the commit
28
+ * point for an asset's existence, so a crash between `putOriginal` and here
29
+ * leaves only orphaned (harmless, content-addressed) blobs, never a meta
30
+ * record pointing at a missing blob.
31
+ */
32
+ export async function finalizeAsset(store, input) {
33
+ const result = await runFinalizePipeline(input);
34
+ if (!result.ok)
35
+ return result;
36
+ const { meta, data, publicObject } = result;
37
+ const existing = await store.getMeta(meta.hash32);
38
+ if (existing)
39
+ return { ok: true, meta: existing };
40
+ await store.putOriginal({ hash32: meta.hash32, ext: meta.ext, data, contentType: meta.mime });
41
+ if (publicObject) {
42
+ await store.putPublicObject(publicObject);
43
+ }
44
+ const putResult = await store.putMetaIfAbsent(meta.hash32, meta);
45
+ if (putResult === 'already-exists') {
46
+ // Lost a race against a concurrent identical upload - the winner's meta
47
+ // (already committed) is authoritative; return it rather than ours.
48
+ const winner = await store.getMeta(meta.hash32);
49
+ if (winner)
50
+ return { ok: true, meta: winner };
51
+ }
52
+ return { ok: true, meta };
53
+ }
54
+ /**
55
+ * Finalize a previously-staged upload: read the staged bytes, run
56
+ * `finalizeAsset`, and best-effort delete the staging object regardless of
57
+ * outcome (success OR pipeline rejection - a rejected upload's staged bytes
58
+ * are just as much litter as an accepted one's).
59
+ */
60
+ export async function finalizeStagedUpload(store, stagingKey, filename, uploadedBy) {
61
+ if (!isValidStagingKey(stagingKey)) {
62
+ return { ok: false, status: 400, error: 'Invalid staging key' };
63
+ }
64
+ const staged = await store.readStaging(stagingKey);
65
+ if (!staged) {
66
+ return { ok: false, status: 404, error: 'Staged upload not found or expired' };
67
+ }
68
+ const result = await finalizeAsset(store, { data: staged, filename, uploadedBy });
69
+ await store.deleteStaging(stagingKey).catch(() => { });
70
+ return result;
71
+ }
72
+ //# sourceMappingURL=finalize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"finalize.js","sourceRoot":"","sources":["../../src/assets/finalize.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,mBAAmB,EAA8C,MAAM,YAAY,CAAA;AAC5F,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAGvC;;;;;;;GAOG;AACH,MAAM,oBAAoB,GAAG,gEAAgE,CAAA;AAE7F,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,MAAM,MAAM,GAAG,GAAG,cAAc,CAAC,OAAO,GAAG,CAAA;IAC3C,OAAO,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAA;AACtF,CAAC;AAID;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,KAAiB,EACjB,KAAoB;IAEpB,MAAM,MAAM,GAAG,MAAM,mBAAmB,CAAC,KAAK,CAAC,CAAA;IAC/C,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,OAAO,MAAM,CAAA;IAE7B,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,MAAM,CAAA;IAE3C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACjD,IAAI,QAAQ;QAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;IAEjD,MAAM,KAAK,CAAC,WAAW,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;IAC7F,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,KAAK,CAAC,eAAe,CAAC,YAAY,CAAC,CAAA;IAC3C,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,KAAK,CAAC,eAAe,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;IAChE,IAAI,SAAS,KAAK,gBAAgB,EAAE,CAAC;QACnC,wEAAwE;QACxE,oEAAoE;QACpE,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC/C,IAAI,MAAM;YAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,CAAA;IAC/C,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;AAC3B,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAiB,EACjB,UAAkB,EAClB,QAAgB,EAChB,UAAmB;IAEnB,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,EAAE,CAAC;QACnC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAA;IACjE,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,WAAW,CAAC,UAAU,CAAC,CAAA;IAClD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,oCAAoC,EAAE,CAAA;IAChF,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC,CAAA;IACjF,MAAM,KAAK,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAA;IACrD,OAAO,MAAM,CAAA;AACf,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Internal module barrel for the assets/ store implementation. NOT a package
3
+ * entrypoint — server code within canopycms imports from here or from the
4
+ * individual files; client-side code (the editor) must `import type` from
5
+ * ./types directly instead, so node:fs/S3-SDK imports never reach a browser
6
+ * bundle.
7
+ */
8
+ export type { AssetMeta, AssetStore, BeginUploadInput, PublicObject, StagedUploadTarget, } from './types';
9
+ export { ASSET_PREFIXES, createKeyBuilders, hashBytes, metaKey, metaPrefix, originalKey, originalPrefix, publicKey, slugifyFilename, stagingKey, type AssetPrefixes, } from './keys';
10
+ export { LocalAssetStore, type LocalAssetStoreOptions } from './store-local';
11
+ export { S3AssetStore, type S3AssetStoreOptions } from './store-s3';
12
+ export { createAssetStore } from './factory';
13
+ export { ALLOWED_UPLOAD_CONTENT_TYPES, runFinalizePipeline, type FinalizeInput, type FinalizePublicObject, type FinalizeResult, type FinalizeRejection, type FinalizeSuccess, } from './pipeline';
14
+ export { finalizeAsset, finalizeStagedUpload, type FinalizeAssetResult } from './finalize';
15
+ export { sanitizeSvg } from './svg-sanitizer';
16
+ export { assetSrc, IDENTITY_TRANSFORM_DIRECTIVE } from './asset-src';
17
+ export { formatDirectives, isAllowedTransformWidth, isValidCropRect, parseTransformPath, type CropRect, type IdentityDirectives, type OutputFormat, type ParsedTransformPath, type ParseTransformPathResult, type ResizeDirectives, type TransformDirectives, } from './transform-directives';
18
+ export { applyTransform, type ApplyTransformInput, type TransformResult } from './transform';
19
+ export { assetUrl, assetSrcSet, type AssetRef, type AssetUrlOptions } from './asset-url';
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/assets/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,YAAY,EACV,SAAS,EACT,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,GACnB,MAAM,SAAS,CAAA;AAEhB,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,SAAS,EACT,OAAO,EACP,UAAU,EACV,WAAW,EACX,cAAc,EACd,SAAS,EACT,eAAe,EACf,UAAU,EACV,KAAK,aAAa,GACnB,MAAM,QAAQ,CAAA;AAEf,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAC5E,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAA;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAE5C,OAAO,EACL,4BAA4B,EAC5B,mBAAmB,EACnB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,eAAe,GACrB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAE1F,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAE7C,OAAO,EAAE,QAAQ,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAA;AAEpE,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,eAAe,EACf,kBAAkB,EAClB,KAAK,QAAQ,EACb,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,GACzB,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAA;AAE5F,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAA"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Internal module barrel for the assets/ store implementation. NOT a package
3
+ * entrypoint — server code within canopycms imports from here or from the
4
+ * individual files; client-side code (the editor) must `import type` from
5
+ * ./types directly instead, so node:fs/S3-SDK imports never reach a browser
6
+ * bundle.
7
+ */
8
+ export { ASSET_PREFIXES, createKeyBuilders, hashBytes, metaKey, metaPrefix, originalKey, originalPrefix, publicKey, slugifyFilename, stagingKey, } from './keys.js';
9
+ export { LocalAssetStore } from './store-local.js';
10
+ export { S3AssetStore } from './store-s3.js';
11
+ export { createAssetStore } from './factory.js';
12
+ export { ALLOWED_UPLOAD_CONTENT_TYPES, runFinalizePipeline, } from './pipeline.js';
13
+ export { finalizeAsset, finalizeStagedUpload } from './finalize.js';
14
+ export { sanitizeSvg } from './svg-sanitizer.js';
15
+ export { assetSrc, IDENTITY_TRANSFORM_DIRECTIVE } from './asset-src.js';
16
+ export { formatDirectives, isAllowedTransformWidth, isValidCropRect, parseTransformPath, } from './transform-directives.js';
17
+ export { applyTransform } from './transform.js';
18
+ export { assetUrl, assetSrcSet } from './asset-url.js';
19
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/assets/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAUH,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,SAAS,EACT,OAAO,EACP,UAAU,EACV,WAAW,EACX,cAAc,EACd,SAAS,EACT,eAAe,EACf,UAAU,GAEX,MAAM,QAAQ,CAAA;AAEf,OAAO,EAAE,eAAe,EAA+B,MAAM,eAAe,CAAA;AAC5E,OAAO,EAAE,YAAY,EAA4B,MAAM,YAAY,CAAA;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAE5C,OAAO,EACL,4BAA4B,EAC5B,mBAAmB,GAMpB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAA4B,MAAM,YAAY,CAAA;AAE1F,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAE7C,OAAO,EAAE,QAAQ,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAA;AAEpE,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,eAAe,EACf,kBAAkB,GAQnB,MAAM,wBAAwB,CAAA;AAE/B,OAAO,EAAE,cAAc,EAAkD,MAAM,aAAa,CAAA;AAE5F,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAuC,MAAM,aAAa,CAAA"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Pure key/hash/slug helpers for the asset store. No filesystem or network I/O
3
+ * here — keeps this file trivially unit-testable and safe to reason about for
4
+ * path-traversal / header-injection concerns.
5
+ *
6
+ * Bucket-prefix layout (see .claude/future-tasks/assets-media-system.md):
7
+ * asset-originals/{hash32}.{ext} private; full-fidelity originals
8
+ * asset-staging/{uuid} presigned-POST target
9
+ * asset-meta/{hash32}.json private; filename/uploader/dims/mime
10
+ * assets/t/{directives}/{hash32}/{slug} transform outputs (see transform-directives.ts)
11
+ * assets/{hash32}/{slug}.{ext} public static: sanitized SVG + PDF only
12
+ *
13
+ * Stores must build keys through these helpers rather than hand-rolling prefix
14
+ * strings, so the layout stays defined in exactly one place.
15
+ */
16
+ import { ASSET_PREFIXES, type AssetPrefixes } from './asset-prefixes';
17
+ export { ASSET_PREFIXES, type AssetPrefixes };
18
+ /** sha-256 of the given bytes, truncated to 32 hex chars (128 bits). */
19
+ export declare function hashBytes(data: Uint8Array): string;
20
+ /**
21
+ * Slugify an (untrusted) original filename into a safe slug + normalized
22
+ * extension. Never trust the input for path or header use directly — this
23
+ * strips directory components, non-ASCII/unsafe characters, and caps length.
24
+ *
25
+ * - Directory components (`/`, `\`, including `..` segments) are dropped —
26
+ * only the final path segment is considered.
27
+ * - The slug charset is restricted to `[a-z0-9-]`; anything else (spaces,
28
+ * unicode, punctuation) collapses to a single `-`, with leading/trailing
29
+ * dashes stripped.
30
+ * - Diacritics are stripped (NFKD) before charset filtering, so an accented
31
+ * "e"-with-acute-accent becomes plain "e" rather than being dropped entirely.
32
+ * - The extension (text after the last `.` in the final segment) is
33
+ * lowercased and restricted to `[a-z0-9]`, capped at 10 chars.
34
+ * - An empty/unsafe result falls back to a fixed slug so callers never see
35
+ * an empty string.
36
+ */
37
+ export declare function slugifyFilename(name: string): {
38
+ slug: string;
39
+ ext: string;
40
+ };
41
+ /**
42
+ * Build the key helpers bound to a given prefix set. Stores that support prefix
43
+ * overrides (currently S3AssetStore) call this with their configured prefixes;
44
+ * everyone else uses the default export instances below.
45
+ */
46
+ export declare function createKeyBuilders(prefixes?: AssetPrefixes): {
47
+ originalKey: (hash32: string, ext: string) => string;
48
+ /** Prefix used to look up an original by hash32 alone (extension unknown ahead of time). */
49
+ originalPrefix: (hash32: string) => string;
50
+ stagingKey: (uuid: string) => string;
51
+ metaKey: (hash32: string) => string;
52
+ metaPrefix: () => string;
53
+ publicKey: (hash32: string, slug: string, ext: string) => string;
54
+ };
55
+ export declare const originalKey: (hash32: string, ext: string) => string, originalPrefix: (hash32: string) => string, stagingKey: (uuid: string) => string, metaKey: (hash32: string) => string, metaPrefix: () => string, publicKey: (hash32: string, slug: string, ext: string) => string;
56
+ //# sourceMappingURL=keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/assets/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAIH,OAAO,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAErE,OAAO,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,CAAA;AAE7C,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAElD;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAkC3E;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,GAAE,aAA8B;0BAEhD,MAAM,OAAO,MAAM,KAAG,MAAM;IAClD,4FAA4F;6BACnE,MAAM,KAAG,MAAM;uBACrB,MAAM,KAAG,MAAM;sBAChB,MAAM,KAAG,MAAM;sBACjB,MAAM;wBACF,MAAM,QAAQ,MAAM,OAAO,MAAM,KAAG,MAAM;EAGjE;AAED,eAAO,MAAQ,WAAW,WAXA,MAAM,OAAO,MAAM,KAAG,MAAM,EAW1B,cAAc,WATb,MAAM,KAAG,MAAM,EASA,UAAU,SAR/B,MAAM,KAAG,MAAM,EAQkB,OAAO,WAPzC,MAAM,KAAG,MAAM,EAO4B,UAAU,QANvD,MAAM,EAMmD,SAAS,WAL9D,MAAM,QAAQ,MAAM,OAAO,MAAM,KAAG,MAMvC,CAAA"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Pure key/hash/slug helpers for the asset store. No filesystem or network I/O
3
+ * here — keeps this file trivially unit-testable and safe to reason about for
4
+ * path-traversal / header-injection concerns.
5
+ *
6
+ * Bucket-prefix layout (see .claude/future-tasks/assets-media-system.md):
7
+ * asset-originals/{hash32}.{ext} private; full-fidelity originals
8
+ * asset-staging/{uuid} presigned-POST target
9
+ * asset-meta/{hash32}.json private; filename/uploader/dims/mime
10
+ * assets/t/{directives}/{hash32}/{slug} transform outputs (see transform-directives.ts)
11
+ * assets/{hash32}/{slug}.{ext} public static: sanitized SVG + PDF only
12
+ *
13
+ * Stores must build keys through these helpers rather than hand-rolling prefix
14
+ * strings, so the layout stays defined in exactly one place.
15
+ */
16
+ import { createHash } from 'node:crypto';
17
+ import { ASSET_PREFIXES } from './asset-prefixes.js';
18
+ export { ASSET_PREFIXES };
19
+ /** sha-256 of the given bytes, truncated to 32 hex chars (128 bits). */
20
+ export function hashBytes(data) {
21
+ return createHash('sha256').update(data).digest('hex').slice(0, 32);
22
+ }
23
+ const MAX_SLUG_LENGTH = 80;
24
+ const MAX_EXT_LENGTH = 10;
25
+ const FALLBACK_SLUG = 'file';
26
+ /**
27
+ * Slugify an (untrusted) original filename into a safe slug + normalized
28
+ * extension. Never trust the input for path or header use directly — this
29
+ * strips directory components, non-ASCII/unsafe characters, and caps length.
30
+ *
31
+ * - Directory components (`/`, `\`, including `..` segments) are dropped —
32
+ * only the final path segment is considered.
33
+ * - The slug charset is restricted to `[a-z0-9-]`; anything else (spaces,
34
+ * unicode, punctuation) collapses to a single `-`, with leading/trailing
35
+ * dashes stripped.
36
+ * - Diacritics are stripped (NFKD) before charset filtering, so an accented
37
+ * "e"-with-acute-accent becomes plain "e" rather than being dropped entirely.
38
+ * - The extension (text after the last `.` in the final segment) is
39
+ * lowercased and restricted to `[a-z0-9]`, capped at 10 chars.
40
+ * - An empty/unsafe result falls back to a fixed slug so callers never see
41
+ * an empty string.
42
+ */
43
+ export function slugifyFilename(name) {
44
+ // Drop any directory components (handles both `/` and `\`, and `..` segments)
45
+ // so the untrusted original filename can never influence a constructed path.
46
+ const segments = name.split(/[/\\]+/);
47
+ const basename = segments[segments.length - 1] ?? '';
48
+ const lastDot = basename.lastIndexOf('.');
49
+ const hasExt = lastDot > 0 && lastDot < basename.length - 1;
50
+ const base = hasExt ? basename.slice(0, lastDot) : basename;
51
+ const extRaw = hasExt ? basename.slice(lastDot + 1) : '';
52
+ const ext = extRaw
53
+ .toLowerCase()
54
+ .replace(/[^a-z0-9]/g, '')
55
+ .slice(0, MAX_EXT_LENGTH);
56
+ // U+0300-U+036F is the combining-diacritical-marks block, stripped after NFKD
57
+ // decomposition. Built from fixed char codes (not a literal regex) so this
58
+ // file's source stays pure ASCII; the pattern is not derived from input, so
59
+ // there is no injection risk despite the non-literal RegExp constructor.
60
+ // eslint-disable-next-line security/detect-non-literal-regexp
61
+ const COMBINING_MARKS = new RegExp(`[${String.fromCharCode(0x0300)}-${String.fromCharCode(0x036f)}]`, 'g');
62
+ let slug = base
63
+ .normalize('NFKD')
64
+ .replace(COMBINING_MARKS, '')
65
+ .toLowerCase()
66
+ .replace(/[^a-z0-9]+/g, '-')
67
+ .replace(/^-+|-+$/g, '');
68
+ slug = slug.slice(0, MAX_SLUG_LENGTH).replace(/-+$/g, '');
69
+ return { slug: slug || FALLBACK_SLUG, ext };
70
+ }
71
+ /**
72
+ * Build the key helpers bound to a given prefix set. Stores that support prefix
73
+ * overrides (currently S3AssetStore) call this with their configured prefixes;
74
+ * everyone else uses the default export instances below.
75
+ */
76
+ export function createKeyBuilders(prefixes = ASSET_PREFIXES) {
77
+ return {
78
+ originalKey: (hash32, ext) => `${prefixes.originals}/${hash32}.${ext}`,
79
+ /** Prefix used to look up an original by hash32 alone (extension unknown ahead of time). */
80
+ originalPrefix: (hash32) => `${prefixes.originals}/${hash32}.`,
81
+ stagingKey: (uuid) => `${prefixes.staging}/${uuid}`,
82
+ metaKey: (hash32) => `${prefixes.meta}/${hash32}.json`,
83
+ metaPrefix: () => `${prefixes.meta}/`,
84
+ publicKey: (hash32, slug, ext) => `${prefixes.public}/${hash32}/${slug}.${ext}`,
85
+ };
86
+ }
87
+ export const { originalKey, originalPrefix, stagingKey, metaKey, metaPrefix, publicKey } = createKeyBuilders();
88
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sourceRoot":"","sources":["../../src/assets/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,OAAO,EAAE,cAAc,EAAsB,MAAM,kBAAkB,CAAA;AAErE,OAAO,EAAE,cAAc,EAAsB,CAAA;AAE7C,wEAAwE;AACxE,MAAM,UAAU,SAAS,CAAC,IAAgB;IACxC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AACrE,CAAC;AAED,MAAM,eAAe,GAAG,EAAE,CAAA;AAC1B,MAAM,cAAc,GAAG,EAAE,CAAA;AACzB,MAAM,aAAa,GAAG,MAAM,CAAA;AAE5B;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,8EAA8E;IAC9E,6EAA6E;IAC7E,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IACrC,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;IAEpD,MAAM,OAAO,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAA;IACzC,MAAM,MAAM,GAAG,OAAO,GAAG,CAAC,IAAI,OAAO,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAA;IAC3D,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAA;IAC3D,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAExD,MAAM,GAAG,GAAG,MAAM;SACf,WAAW,EAAE;SACb,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC;SACzB,KAAK,CAAC,CAAC,EAAE,cAAc,CAAC,CAAA;IAE3B,8EAA8E;IAC9E,2EAA2E;IAC3E,4EAA4E;IAC5E,yEAAyE;IACzE,8DAA8D;IAC9D,MAAM,eAAe,GAAG,IAAI,MAAM,CAChC,IAAI,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,GAAG,EACjE,GAAG,CACJ,CAAA;IACD,IAAI,IAAI,GAAG,IAAI;SACZ,SAAS,CAAC,MAAM,CAAC;SACjB,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC;SAC5B,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;IAC1B,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IAEzD,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,aAAa,EAAE,GAAG,EAAE,CAAA;AAC7C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,WAA0B,cAAc;IACxE,OAAO;QACL,WAAW,EAAE,CAAC,MAAc,EAAE,GAAW,EAAU,EAAE,CAAC,GAAG,QAAQ,CAAC,SAAS,IAAI,MAAM,IAAI,GAAG,EAAE;QAC9F,4FAA4F;QAC5F,cAAc,EAAE,CAAC,MAAc,EAAU,EAAE,CAAC,GAAG,QAAQ,CAAC,SAAS,IAAI,MAAM,GAAG;QAC9E,UAAU,EAAE,CAAC,IAAY,EAAU,EAAE,CAAC,GAAG,QAAQ,CAAC,OAAO,IAAI,IAAI,EAAE;QACnE,OAAO,EAAE,CAAC,MAAc,EAAU,EAAE,CAAC,GAAG,QAAQ,CAAC,IAAI,IAAI,MAAM,OAAO;QACtE,UAAU,EAAE,GAAW,EAAE,CAAC,GAAG,QAAQ,CAAC,IAAI,GAAG;QAC7C,SAAS,EAAE,CAAC,MAAc,EAAE,IAAY,EAAE,GAAW,EAAU,EAAE,CAC/D,GAAG,QAAQ,CAAC,MAAM,IAAI,MAAM,IAAI,IAAI,IAAI,GAAG,EAAE;KAChD,CAAA;AACH,CAAC;AAED,MAAM,CAAC,MAAM,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,GACtF,iBAAiB,EAAE,CAAA"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Finalize pipeline: turns raw uploaded bytes into a validated AssetMeta
3
+ * (+ optional public object for svg/pdf), or a typed rejection. Pure
4
+ * transform - no store I/O here (see finalize.ts for the store orchestration
5
+ * that dedups, writes, and calls this).
6
+ *
7
+ * Server-only - never import this module from client/editor code. It pulls
8
+ * in `file-type`, `image-size`, and the SVG sanitizer, none of which belong
9
+ * in a browser bundle.
10
+ */
11
+ import type { AssetMeta } from './types';
12
+ /** Client-declared content types accepted at presign time (UX only - never trusted at finalize). */
13
+ export declare const ALLOWED_UPLOAD_CONTENT_TYPES: Set<string>;
14
+ export interface FinalizeInput {
15
+ data: Uint8Array;
16
+ filename: string;
17
+ uploadedBy?: string;
18
+ }
19
+ export interface FinalizePublicObject {
20
+ key: string;
21
+ data: Uint8Array;
22
+ contentType: string;
23
+ contentDisposition: string;
24
+ cacheControl: string;
25
+ }
26
+ export interface FinalizeSuccess {
27
+ ok: true;
28
+ meta: AssetMeta;
29
+ /** Canonical bytes to persist as the original - sanitized bytes for svg, untouched otherwise. */
30
+ data: Uint8Array;
31
+ publicObject?: FinalizePublicObject;
32
+ }
33
+ export interface FinalizeRejection {
34
+ ok: false;
35
+ status: 413 | 415;
36
+ error: string;
37
+ }
38
+ export type FinalizeResult = FinalizeSuccess | FinalizeRejection;
39
+ /**
40
+ * Run the finalize pipeline over freshly-uploaded bytes: sniff the real file
41
+ * type (never trust the client-declared contentType beyond presign-time UX),
42
+ * sanitize if svg, hash, extract dimensions, and build the AssetMeta + any
43
+ * public object (svg/pdf only) the caller should persist.
44
+ *
45
+ * Order matters: for SVG, sanitization happens BEFORE hashing - the sanitized
46
+ * bytes are what get hashed and stored, so a later re-sanitization (e.g. a
47
+ * stricter sanitizer version) cannot desync a stored hash from its content.
48
+ */
49
+ export declare function runFinalizePipeline(input: FinalizeInput): Promise<FinalizeResult>;
50
+ //# sourceMappingURL=pipeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/assets/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AASH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAMxC,oGAAoG;AACpG,eAAO,MAAM,4BAA4B,aAIvC,CAAA;AAkBF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,UAAU,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,kBAAkB,EAAE,MAAM,CAAA;IAC1B,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,IAAI,CAAA;IACR,IAAI,EAAE,SAAS,CAAA;IACf,iGAAiG;IACjG,IAAI,EAAE,UAAU,CAAA;IAChB,YAAY,CAAC,EAAE,oBAAoB,CAAA;CACpC;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,KAAK,CAAA;IACT,MAAM,EAAE,GAAG,GAAG,GAAG,CAAA;IACjB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,cAAc,GAAG,eAAe,GAAG,iBAAiB,CAAA;AAoEhE;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC,CA8FvF"}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Finalize pipeline: turns raw uploaded bytes into a validated AssetMeta
3
+ * (+ optional public object for svg/pdf), or a typed rejection. Pure
4
+ * transform - no store I/O here (see finalize.ts for the store orchestration
5
+ * that dedups, writes, and calls this).
6
+ *
7
+ * Server-only - never import this module from client/editor code. It pulls
8
+ * in `file-type`, `image-size`, and the SVG sanitizer, none of which belong
9
+ * in a browser bundle.
10
+ */
11
+ import { fileTypeFromBuffer } from 'file-type';
12
+ import { imageSize } from 'image-size';
13
+ import { create as createContentDisposition } from 'content-disposition';
14
+ import { hashBytes, publicKey, slugifyFilename } from './keys.js';
15
+ import { sanitizeSvg } from './svg-sanitizer.js';
16
+ import { MAX_INPUT_PIXELS } from './transform-directives.js';
17
+ const RASTER_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']);
18
+ const PDF_MIME_TYPE = 'application/pdf';
19
+ const SVG_MIME_TYPE = 'image/svg+xml';
20
+ /** Client-declared content types accepted at presign time (UX only - never trusted at finalize). */
21
+ export const ALLOWED_UPLOAD_CONTENT_TYPES = new Set([
22
+ ...RASTER_MIME_TYPES,
23
+ PDF_MIME_TYPE,
24
+ SVG_MIME_TYPE,
25
+ ]);
26
+ const PDF_MAX_BYTES = 25 * 1024 * 1024;
27
+ /**
28
+ * Defensive raster byte cap, independent of whatever the configured
29
+ * AssetStore's own upload-size limit happens to be (that limit lives at the
30
+ * store/API boundary, not here) - mirrors PDF_MAX_BYTES's role as a
31
+ * pipeline-level backstop rather than the sole enforcement point.
32
+ */
33
+ const RASTER_MAX_BYTES = 50 * 1024 * 1024;
34
+ /** EXIF orientation values 5-8 are 90-degree rotations, which swap width/height. */
35
+ const isRotated90 = (orientation) => orientation !== undefined && orientation >= 5 && orientation <= 8;
36
+ const PUBLIC_CACHE_CONTROL = 'public, max-age=31536000, immutable';
37
+ /** U+FEFF (byte-order mark). Built from a char code, not a literal, so this file's source stays pure ASCII. */
38
+ const BOM = String.fromCharCode(0xfeff);
39
+ /**
40
+ * Strip anything that may legally precede the root `<svg>` element: a BOM,
41
+ * whitespace, XML comments, the `<?xml ... ?>` prolog, and a `<!DOCTYPE ...>`
42
+ * declaration - in any order, any number of times (some tools emit more than
43
+ * one comment). Loops until a pass makes no further change.
44
+ */
45
+ function stripSvgPreamble(text) {
46
+ let stripped = text.startsWith(BOM) ? text.slice(BOM.length) : text;
47
+ let previous;
48
+ do {
49
+ previous = stripped;
50
+ stripped = stripped.replace(/^\s+/, '');
51
+ stripped = stripped.replace(/^<!--[\s\S]*?-->/, '');
52
+ stripped = stripped.replace(/^<\?xml[\s\S]*?\?>/i, '');
53
+ stripped = stripped.replace(/^<!DOCTYPE[\s\S]*?>/i, '');
54
+ } while (stripped !== previous);
55
+ return stripped;
56
+ }
57
+ /**
58
+ * Detect whether `data` is an SVG document. `file-type` cannot do this (SVG is
59
+ * a text format, not magic-byte-detectable) - and, discovered empirically,
60
+ * `file-type` does NOT simply return `undefined` for all SVGs either: an SVG
61
+ * with an `<?xml ?>` prolog gets positively sniffed as generic `application/xml`.
62
+ * So the pipeline's real branch condition is "not a recognized raster/pdf mime"
63
+ * (which covers both file-type returning undefined AND returning some other
64
+ * non-allowlisted guess like application/xml), and within that branch this
65
+ * function does the actual SVG determination: decode as UTF-8 (reject on
66
+ * failure), strip anything that may precede the root element, and require the
67
+ * first element to be `<svg` (case-insensitive).
68
+ *
69
+ * Returns the decoded text on success (so the caller doesn't have to decode
70
+ * twice), or `null` if this is not a decodable SVG document.
71
+ */
72
+ function sniffSvg(data) {
73
+ let text;
74
+ try {
75
+ text = new TextDecoder('utf-8', { fatal: true }).decode(data);
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ const stripped = stripSvgPreamble(text);
81
+ return /^<svg[\s>/]/i.test(stripped) ? text : null;
82
+ }
83
+ /**
84
+ * Extract width/height (rasters and svg only - PDFs have no pixel dims).
85
+ * Non-fatal on failure: a corrupt-but-magic-byte-valid file still finalizes,
86
+ * just without dimensions (AssetMeta.width/height are optional).
87
+ */
88
+ function computeDimensions(data) {
89
+ try {
90
+ const result = imageSize(data);
91
+ let { width, height } = result;
92
+ if (isRotated90(result.orientation)) {
93
+ ;
94
+ [width, height] = [height, width];
95
+ }
96
+ return { width, height };
97
+ }
98
+ catch {
99
+ return {};
100
+ }
101
+ }
102
+ /**
103
+ * Run the finalize pipeline over freshly-uploaded bytes: sniff the real file
104
+ * type (never trust the client-declared contentType beyond presign-time UX),
105
+ * sanitize if svg, hash, extract dimensions, and build the AssetMeta + any
106
+ * public object (svg/pdf only) the caller should persist.
107
+ *
108
+ * Order matters: for SVG, sanitization happens BEFORE hashing - the sanitized
109
+ * bytes are what get hashed and stored, so a later re-sanitization (e.g. a
110
+ * stricter sanitizer version) cannot desync a stored hash from its content.
111
+ */
112
+ export async function runFinalizePipeline(input) {
113
+ const sniffed = await fileTypeFromBuffer(input.data);
114
+ let kind;
115
+ let mime;
116
+ let ext;
117
+ let canonicalData = input.data;
118
+ if (sniffed && RASTER_MIME_TYPES.has(sniffed.mime)) {
119
+ if (input.data.byteLength > RASTER_MAX_BYTES) {
120
+ return {
121
+ ok: false,
122
+ status: 413,
123
+ error: `Raster image exceeds the ${RASTER_MAX_BYTES}-byte limit`,
124
+ };
125
+ }
126
+ kind = 'raster';
127
+ mime = sniffed.mime;
128
+ ext = sniffed.ext;
129
+ }
130
+ else if (sniffed && sniffed.mime === PDF_MIME_TYPE) {
131
+ if (input.data.byteLength > PDF_MAX_BYTES) {
132
+ return { ok: false, status: 413, error: `PDF exceeds the ${PDF_MAX_BYTES}-byte limit` };
133
+ }
134
+ kind = 'pdf';
135
+ mime = PDF_MIME_TYPE;
136
+ ext = sniffed.ext;
137
+ }
138
+ else {
139
+ // Not a recognized raster/pdf - could be a bare/prolog'd SVG, or genuine junk.
140
+ const svgText = sniffSvg(input.data);
141
+ if (!svgText) {
142
+ return { ok: false, status: 415, error: 'Unsupported or unrecognized file type' };
143
+ }
144
+ kind = 'svg';
145
+ mime = SVG_MIME_TYPE;
146
+ ext = 'svg';
147
+ canonicalData = new TextEncoder().encode(sanitizeSvg(svgText));
148
+ }
149
+ const hash32 = hashBytes(canonicalData);
150
+ const { slug } = slugifyFilename(input.filename);
151
+ const dims = kind === 'pdf' ? {} : computeDimensions(canonicalData);
152
+ // Decompression-bomb defense: a raster's on-disk size says nothing about
153
+ // its decoded pixel count (a solid-color 30000x30000 PNG compresses to a
154
+ // few KB) - reject on width*height before this asset is ever finalized, so
155
+ // the transform engine (transform.ts's own `limitInputPixels`) is never the
156
+ // only thing standing between an attacker and a huge decode. SVG is exempt:
157
+ // `computeDimensions` reads its declared viewBox/width/height attributes,
158
+ // not a decoded raster, so there is no analogous decode-time memory cost.
159
+ if (kind === 'raster' && dims.width !== undefined && dims.height !== undefined) {
160
+ const pixelCount = dims.width * dims.height;
161
+ if (pixelCount > MAX_INPUT_PIXELS) {
162
+ return {
163
+ ok: false,
164
+ status: 413,
165
+ error: `Image dimensions ${dims.width}x${dims.height} (${pixelCount} pixels) exceed the ${MAX_INPUT_PIXELS}-pixel limit`,
166
+ };
167
+ }
168
+ }
169
+ const meta = {
170
+ hash32,
171
+ filename: input.filename,
172
+ slug,
173
+ ext,
174
+ mime,
175
+ size: canonicalData.byteLength,
176
+ ...(dims.width !== undefined && { width: dims.width }),
177
+ ...(dims.height !== undefined && { height: dims.height }),
178
+ kind,
179
+ ...(input.uploadedBy && { uploadedBy: input.uploadedBy }),
180
+ uploadedAt: new Date().toISOString(),
181
+ };
182
+ let publicObject;
183
+ if (kind === 'svg' || kind === 'pdf') {
184
+ // svg: rendered inline by the browser. pdf: downloaded under its real
185
+ // original filename (never the slug) via Content-Disposition, built with
186
+ // the content-disposition package (RFC 5987) rather than string
187
+ // interpolation, so header-injection characters in the filename can never
188
+ // reach a raw HTTP header value.
189
+ const contentDisposition = createContentDisposition(input.filename, {
190
+ type: kind === 'svg' ? 'inline' : 'attachment',
191
+ });
192
+ publicObject = {
193
+ key: publicKey(hash32, slug, ext),
194
+ data: canonicalData,
195
+ contentType: mime,
196
+ contentDisposition,
197
+ cacheControl: PUBLIC_CACHE_CONTROL,
198
+ };
199
+ }
200
+ return { ok: true, meta, data: canonicalData, publicObject };
201
+ }
202
+ //# sourceMappingURL=pipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../../src/assets/pipeline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Filesystem-backed AssetStore for dev and tests. Mirrors the same
3
+ * bucket-prefix layout as S3AssetStore (see keys.ts) so content behaves
4
+ * identically across adapters.
5
+ */
6
+ import type { AssetMeta, AssetStore, BeginUploadInput, PublicObject, StagedUploadTarget } from './types';
7
+ export interface LocalAssetStoreOptions {
8
+ /** Root directory. The five bucket prefixes are created as subdirectories under it. */
9
+ root: string;
10
+ }
11
+ export declare class LocalAssetStore implements AssetStore {
12
+ readonly capabilities: {
13
+ directUpload: boolean;
14
+ };
15
+ private readonly root;
16
+ constructor(options: LocalAssetStoreOptions);
17
+ /**
18
+ * Resolve a store key to an absolute filesystem path, guarding against
19
+ * escape from the root. Keys are internally generated (never taken
20
+ * verbatim from user input), but this guard is kept as defense-in-depth
21
+ * (ported from the v1 LocalAssetStore).
22
+ *
23
+ * A naive `resolved.startsWith(root)` check would let a sibling directory
24
+ * that merely shares the root's name as a prefix (e.g. root `.../assets`,
25
+ * sibling `.../assets-evil`) pass. Comparing against `root + path.sep`
26
+ * closes that gap.
27
+ */
28
+ private resolveKey;
29
+ private headersSidecarPath;
30
+ private writeHeadersSidecar;
31
+ private readHeadersSidecar;
32
+ private readFileOrNull;
33
+ private writeFile;
34
+ private deleteFileIfPresent;
35
+ beginUpload(_input: BeginUploadInput): Promise<StagedUploadTarget>;
36
+ /**
37
+ * Staging methods accept caller-influenced keys (finalize receives the key
38
+ * from the client), so they must never operate outside the staging prefix —
39
+ * otherwise a crafted key turns the staging cleanup into an arbitrary-object
40
+ * delete. The API layer validates too; this is defense-in-depth.
41
+ */
42
+ private assertStagingKey;
43
+ writeStaging(key: string, data: Uint8Array, _contentType?: string): Promise<void>;
44
+ readStaging(key: string): Promise<Uint8Array | null>;
45
+ deleteStaging(key: string): Promise<void>;
46
+ putOriginal(input: {
47
+ hash32: string;
48
+ ext: string;
49
+ data: Uint8Array;
50
+ contentType: string;
51
+ }): Promise<void>;
52
+ readOriginal(hash32: string): Promise<{
53
+ data: Uint8Array;
54
+ ext: string;
55
+ contentType?: string;
56
+ } | null>;
57
+ putPublicObject(input: {
58
+ key: string;
59
+ data: Uint8Array;
60
+ contentType: string;
61
+ contentDisposition?: string;
62
+ cacheControl?: string;
63
+ }): Promise<void>;
64
+ readPublicObject(key: string): Promise<PublicObject | null>;
65
+ /**
66
+ * Atomic-exclusive create: opens with the `wx` flag so two concurrent
67
+ * writers racing on the same hash32 have exactly one winner ('created')
68
+ * and exactly one loser ('already-exists'). The rename-based
69
+ * `atomicWriteFile` helper is deliberately NOT used here — a rename
70
+ * always clobbers the destination, which would let the second writer
71
+ * silently overwrite the first instead of losing cleanly.
72
+ */
73
+ putMetaIfAbsent(hash32: string, meta: AssetMeta): Promise<'created' | 'already-exists'>;
74
+ getMeta(hash32: string): Promise<AssetMeta | null>;
75
+ listMeta(input?: {
76
+ cursor?: string;
77
+ limit?: number;
78
+ }): Promise<{
79
+ items: AssetMeta[];
80
+ nextCursor?: string;
81
+ }>;
82
+ deleteMeta(hash32: string): Promise<void>;
83
+ }
84
+ //# sourceMappingURL=store-local.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store-local.d.ts","sourceRoot":"","sources":["../../src/assets/store-local.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EACV,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EACnB,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,sBAAsB;IACrC,uFAAuF;IACvF,IAAI,EAAE,MAAM,CAAA;CACb;AAcD,qBAAa,eAAgB,YAAW,UAAU;IAChD,QAAQ,CAAC,YAAY;;MAA0B;IAC/C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAQ;gBAEjB,OAAO,EAAE,sBAAsB;IAI3C;;;;;;;;;;OAUG;IACH,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,kBAAkB;YAIZ,mBAAmB;YAInB,kBAAkB;YAUlB,cAAc;YAUd,SAAS;YAIT,mBAAmB;IAU3B,WAAW,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAQxE;;;;;OAKG;IACH,OAAO,CAAC,gBAAgB;IAMlB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjF,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAKpD,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKzC,WAAW,CAAC,KAAK,EAAE;QACvB,MAAM,EAAE,MAAM,CAAA;QACd,GAAG,EAAE,MAAM,CAAA;QACX,IAAI,EAAE,UAAU,CAAA;QAChB,WAAW,EAAE,MAAM,CAAA;KACpB,GAAG,OAAO,CAAC,IAAI,CAAC;IAMX,YAAY,CAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC;QAAE,IAAI,EAAE,UAAU,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAsBpE,eAAe,CAAC,KAAK,EAAE;QAC3B,GAAG,EAAE,MAAM,CAAA;QACX,IAAI,EAAE,UAAU,CAAA;QAChB,WAAW,EAAE,MAAM,CAAA;QACnB,kBAAkB,CAAC,EAAE,MAAM,CAAA;QAC3B,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,GAAG,OAAO,CAAC,IAAI,CAAC;IAUX,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAajE;;;;;;;OAOG;IACG,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,GAAG,gBAAgB,CAAC;IAYvF,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAWlD,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QACnE,KAAK,EAAE,SAAS,EAAE,CAAA;QAClB,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAC;IAkDI,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAShD"}