canopycms 0.0.56 → 0.0.58

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 (272) 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/branch.d.ts +6 -0
  12. package/dist/api/branch.d.ts.map +1 -1
  13. package/dist/api/branch.js +63 -19
  14. package/dist/api/branch.js.map +1 -1
  15. package/dist/api/client.d.ts +16 -5
  16. package/dist/api/client.d.ts.map +1 -1
  17. package/dist/api/client.js +24 -6
  18. package/dist/api/client.js.map +1 -1
  19. package/dist/api/content.d.ts +2 -2
  20. package/dist/api/entries.d.ts +4 -4
  21. package/dist/api/entries.d.ts.map +1 -1
  22. package/dist/api/entries.js +10 -2
  23. package/dist/api/entries.js.map +1 -1
  24. package/dist/api/index.d.ts +1 -1
  25. package/dist/api/index.d.ts.map +1 -1
  26. package/dist/api/index.js.map +1 -1
  27. package/dist/api/route-builder.d.ts +9 -0
  28. package/dist/api/route-builder.d.ts.map +1 -1
  29. package/dist/api/route-builder.js +1 -0
  30. package/dist/api/route-builder.js.map +1 -1
  31. package/dist/api/schema.d.ts +4 -4
  32. package/dist/api/types.d.ts +11 -1
  33. package/dist/api/types.d.ts.map +1 -1
  34. package/dist/assets/asset-prefixes.d.ts +31 -0
  35. package/dist/assets/asset-prefixes.d.ts.map +1 -0
  36. package/dist/assets/asset-prefixes.js +20 -0
  37. package/dist/assets/asset-prefixes.js.map +1 -0
  38. package/dist/assets/asset-src.d.ts +23 -0
  39. package/dist/assets/asset-src.d.ts.map +1 -0
  40. package/dist/assets/asset-src.js +28 -0
  41. package/dist/assets/asset-src.js.map +1 -0
  42. package/dist/assets/asset-url.d.ts +35 -0
  43. package/dist/assets/asset-url.d.ts.map +1 -0
  44. package/dist/assets/asset-url.js +77 -0
  45. package/dist/assets/asset-url.js.map +1 -0
  46. package/dist/assets/factory.d.ts +18 -0
  47. package/dist/assets/factory.d.ts.map +1 -0
  48. package/dist/assets/factory.js +42 -0
  49. package/dist/assets/factory.js.map +1 -0
  50. package/dist/assets/finalize.d.ts +36 -0
  51. package/dist/assets/finalize.d.ts.map +1 -0
  52. package/dist/assets/finalize.js +72 -0
  53. package/dist/assets/finalize.js.map +1 -0
  54. package/dist/assets/index.d.ts +20 -0
  55. package/dist/assets/index.d.ts.map +1 -0
  56. package/dist/assets/index.js +19 -0
  57. package/dist/assets/index.js.map +1 -0
  58. package/dist/assets/keys.d.ts +56 -0
  59. package/dist/assets/keys.d.ts.map +1 -0
  60. package/dist/assets/keys.js +88 -0
  61. package/dist/assets/keys.js.map +1 -0
  62. package/dist/assets/pipeline.d.ts +50 -0
  63. package/dist/assets/pipeline.d.ts.map +1 -0
  64. package/dist/assets/pipeline.js +202 -0
  65. package/dist/assets/pipeline.js.map +1 -0
  66. package/dist/assets/store-local.d.ts +84 -0
  67. package/dist/assets/store-local.d.ts.map +1 -0
  68. package/dist/assets/store-local.js +254 -0
  69. package/dist/assets/store-local.js.map +1 -0
  70. package/dist/assets/store-s3.d.ts +71 -0
  71. package/dist/assets/store-s3.d.ts.map +1 -0
  72. package/dist/assets/store-s3.js +183 -0
  73. package/dist/assets/store-s3.js.map +1 -0
  74. package/dist/assets/svg-sanitizer.d.ts +37 -0
  75. package/dist/assets/svg-sanitizer.d.ts.map +1 -0
  76. package/dist/assets/svg-sanitizer.js +222 -0
  77. package/dist/assets/svg-sanitizer.js.map +1 -0
  78. package/dist/assets/transform-directives.d.ts +97 -0
  79. package/dist/assets/transform-directives.d.ts.map +1 -0
  80. package/dist/assets/transform-directives.js +267 -0
  81. package/dist/assets/transform-directives.js.map +1 -0
  82. package/dist/assets/transform.d.ts +51 -0
  83. package/dist/assets/transform.d.ts.map +1 -0
  84. package/dist/assets/transform.js +190 -0
  85. package/dist/assets/transform.js.map +1 -0
  86. package/dist/assets/types.d.ts +82 -0
  87. package/dist/assets/types.d.ts.map +1 -0
  88. package/dist/assets/types.js +10 -0
  89. package/dist/assets/types.js.map +1 -0
  90. package/dist/authorization/groups/schema.d.ts +2 -2
  91. package/dist/authorization/permissions/schema.d.ts +2 -2
  92. package/dist/branch-metadata.d.ts +71 -6
  93. package/dist/branch-metadata.d.ts.map +1 -1
  94. package/dist/branch-metadata.js +132 -116
  95. package/dist/branch-metadata.js.map +1 -1
  96. package/dist/branch-registry.d.ts +78 -9
  97. package/dist/branch-registry.d.ts.map +1 -1
  98. package/dist/branch-registry.js +155 -29
  99. package/dist/branch-registry.js.map +1 -1
  100. package/dist/branch-schema-cache.d.ts +126 -11
  101. package/dist/branch-schema-cache.d.ts.map +1 -1
  102. package/dist/branch-schema-cache.js +208 -58
  103. package/dist/branch-schema-cache.js.map +1 -1
  104. package/dist/cli/generate-ai-content.js +1993 -1506
  105. package/dist/cli/init.js +16 -4
  106. package/dist/cli/migrate.d.ts.map +1 -1
  107. package/dist/cli/migrate.js +25 -0
  108. package/dist/cli/migrate.js.map +1 -1
  109. package/dist/cli/sync.js +5 -5
  110. package/dist/cli/sync.js.map +1 -1
  111. package/dist/comment-store.d.ts +45 -15
  112. package/dist/comment-store.d.ts.map +1 -1
  113. package/dist/comment-store.js +80 -101
  114. package/dist/comment-store.js.map +1 -1
  115. package/dist/config/helpers.d.ts.map +1 -1
  116. package/dist/config/helpers.js +2 -1
  117. package/dist/config/helpers.js.map +1 -1
  118. package/dist/config/index.d.ts +2 -2
  119. package/dist/config/index.d.ts.map +1 -1
  120. package/dist/config/index.js +1 -1
  121. package/dist/config/index.js.map +1 -1
  122. package/dist/config/schemas/config.d.ts +10 -0
  123. package/dist/config/schemas/config.d.ts.map +1 -1
  124. package/dist/config/schemas/field.d.ts +38 -3
  125. package/dist/config/schemas/field.d.ts.map +1 -1
  126. package/dist/config/schemas/field.js +18 -0
  127. package/dist/config/schemas/field.js.map +1 -1
  128. package/dist/config/schemas/media.d.ts +8 -0
  129. package/dist/config/schemas/media.d.ts.map +1 -1
  130. package/dist/config/schemas/media.js +9 -0
  131. package/dist/config/schemas/media.js.map +1 -1
  132. package/dist/config/types.d.ts +43 -3
  133. package/dist/config/types.d.ts.map +1 -1
  134. package/dist/config/types.js +4 -1
  135. package/dist/config/types.js.map +1 -1
  136. package/dist/content-index-generation.d.ts +40 -4
  137. package/dist/content-index-generation.d.ts.map +1 -1
  138. package/dist/content-index-generation.js +70 -73
  139. package/dist/content-index-generation.js.map +1 -1
  140. package/dist/content-store.d.ts +63 -0
  141. package/dist/content-store.d.ts.map +1 -1
  142. package/dist/content-store.js +421 -194
  143. package/dist/content-store.js.map +1 -1
  144. package/dist/editor/CanopyEditor.d.ts.map +1 -1
  145. package/dist/editor/CanopyEditor.js +1 -1
  146. package/dist/editor/CanopyEditor.js.map +1 -1
  147. package/dist/editor/Editor.d.ts +2 -0
  148. package/dist/editor/Editor.d.ts.map +1 -1
  149. package/dist/editor/Editor.js +82 -80
  150. package/dist/editor/Editor.js.map +1 -1
  151. package/dist/editor/FormRenderer.d.ts.map +1 -1
  152. package/dist/editor/FormRenderer.js +9 -0
  153. package/dist/editor/FormRenderer.js.map +1 -1
  154. package/dist/editor/components/EditorSidebar.d.ts +6 -1
  155. package/dist/editor/components/EditorSidebar.d.ts.map +1 -1
  156. package/dist/editor/components/EditorSidebar.js +4 -3
  157. package/dist/editor/components/EditorSidebar.js.map +1 -1
  158. package/dist/editor/context/AssetContext.d.ts +25 -0
  159. package/dist/editor/context/AssetContext.d.ts.map +1 -0
  160. package/dist/editor/context/AssetContext.js +26 -0
  161. package/dist/editor/context/AssetContext.js.map +1 -0
  162. package/dist/editor/context/index.d.ts +1 -0
  163. package/dist/editor/context/index.d.ts.map +1 -1
  164. package/dist/editor/context/index.js +1 -0
  165. package/dist/editor/context/index.js.map +1 -1
  166. package/dist/editor/fields/ImageField.d.ts +22 -0
  167. package/dist/editor/fields/ImageField.d.ts.map +1 -0
  168. package/dist/editor/fields/ImageField.js +100 -0
  169. package/dist/editor/fields/ImageField.js.map +1 -0
  170. package/dist/editor/fields/MarkdownField.d.ts.map +1 -1
  171. package/dist/editor/fields/MarkdownField.js +30 -4
  172. package/dist/editor/fields/MarkdownField.js.map +1 -1
  173. package/dist/editor/fields/MdxImageDialog.d.ts +16 -0
  174. package/dist/editor/fields/MdxImageDialog.d.ts.map +1 -0
  175. package/dist/editor/fields/MdxImageDialog.js +73 -0
  176. package/dist/editor/fields/MdxImageDialog.js.map +1 -0
  177. package/dist/editor/media/AssetCard.d.ts +21 -0
  178. package/dist/editor/media/AssetCard.d.ts.map +1 -0
  179. package/dist/editor/media/AssetCard.js +41 -0
  180. package/dist/editor/media/AssetCard.js.map +1 -0
  181. package/dist/editor/media/CropStep.d.ts +26 -0
  182. package/dist/editor/media/CropStep.d.ts.map +1 -0
  183. package/dist/editor/media/CropStep.js +38 -0
  184. package/dist/editor/media/CropStep.js.map +1 -0
  185. package/dist/editor/media/MediaLibrary.d.ts +22 -0
  186. package/dist/editor/media/MediaLibrary.d.ts.map +1 -0
  187. package/dist/editor/media/MediaLibrary.js +17 -0
  188. package/dist/editor/media/MediaLibrary.js.map +1 -0
  189. package/dist/editor/media/MediaLibraryBody.d.ts +17 -0
  190. package/dist/editor/media/MediaLibraryBody.d.ts.map +1 -0
  191. package/dist/editor/media/MediaLibraryBody.js +133 -0
  192. package/dist/editor/media/MediaLibraryBody.js.map +1 -0
  193. package/dist/editor/media/crop-math.d.ts +37 -0
  194. package/dist/editor/media/crop-math.d.ts.map +1 -0
  195. package/dist/editor/media/crop-math.js +62 -0
  196. package/dist/editor/media/crop-math.js.map +1 -0
  197. package/dist/editor/media/upload-asset.d.ts +31 -0
  198. package/dist/editor/media/upload-asset.d.ts.map +1 -0
  199. package/dist/editor/media/upload-asset.js +56 -0
  200. package/dist/editor/media/upload-asset.js.map +1 -0
  201. package/dist/editor/media/upload-constants.d.ts +27 -0
  202. package/dist/editor/media/upload-constants.d.ts.map +1 -0
  203. package/dist/editor/media/upload-constants.js +40 -0
  204. package/dist/editor/media/upload-constants.js.map +1 -0
  205. package/dist/editor/media/useAssetUpload.d.ts +19 -0
  206. package/dist/editor/media/useAssetUpload.d.ts.map +1 -0
  207. package/dist/editor/media/useAssetUpload.js +53 -0
  208. package/dist/editor/media/useAssetUpload.js.map +1 -0
  209. package/dist/editor/media/xhr-upload.d.ts +20 -0
  210. package/dist/editor/media/xhr-upload.d.ts.map +1 -0
  211. package/dist/editor/media/xhr-upload.js +42 -0
  212. package/dist/editor/media/xhr-upload.js.map +1 -0
  213. package/dist/editor/theme.d.ts +2 -1
  214. package/dist/editor/theme.d.ts.map +1 -1
  215. package/dist/editor/theme.js +1 -0
  216. package/dist/editor/theme.js.map +1 -1
  217. package/dist/entry-schema.d.ts +19 -0
  218. package/dist/entry-schema.d.ts.map +1 -1
  219. package/dist/entry-schema.js.map +1 -1
  220. package/dist/git-manager.d.ts +15 -10
  221. package/dist/git-manager.d.ts.map +1 -1
  222. package/dist/git-manager.js +16 -11
  223. package/dist/git-manager.js.map +1 -1
  224. package/dist/http/handler.d.ts +5 -2
  225. package/dist/http/handler.d.ts.map +1 -1
  226. package/dist/http/handler.js +13 -4
  227. package/dist/http/handler.js.map +1 -1
  228. package/dist/http/index.d.ts +2 -2
  229. package/dist/http/index.d.ts.map +1 -1
  230. package/dist/http/index.js +1 -1
  231. package/dist/http/index.js.map +1 -1
  232. package/dist/http/router.d.ts +14 -1
  233. package/dist/http/router.d.ts.map +1 -1
  234. package/dist/http/router.js +4 -1
  235. package/dist/http/router.js.map +1 -1
  236. package/dist/http/types.d.ts +40 -0
  237. package/dist/http/types.d.ts.map +1 -1
  238. package/dist/http/types.js +9 -0
  239. package/dist/http/types.js.map +1 -1
  240. package/dist/index.d.ts +3 -0
  241. package/dist/index.d.ts.map +1 -1
  242. package/dist/index.js +4 -0
  243. package/dist/index.js.map +1 -1
  244. package/dist/resource-generation.d.ts +54 -0
  245. package/dist/resource-generation.d.ts.map +1 -0
  246. package/dist/resource-generation.js +156 -0
  247. package/dist/resource-generation.js.map +1 -0
  248. package/dist/schema/schema-store.d.ts +31 -4
  249. package/dist/schema/schema-store.d.ts.map +1 -1
  250. package/dist/schema/schema-store.js +47 -8
  251. package/dist/schema/schema-store.js.map +1 -1
  252. package/dist/server.d.ts +45 -0
  253. package/dist/server.d.ts.map +1 -1
  254. package/dist/server.js +41 -0
  255. package/dist/server.js.map +1 -1
  256. package/dist/sync-core.js +2 -2
  257. package/dist/sync-core.js.map +1 -1
  258. package/dist/utils/occ-json-write.d.ts +100 -0
  259. package/dist/utils/occ-json-write.d.ts.map +1 -0
  260. package/dist/utils/occ-json-write.js +234 -0
  261. package/dist/utils/occ-json-write.js.map +1 -0
  262. package/dist/validation/entry-validator.d.ts.map +1 -1
  263. package/dist/validation/entry-validator.js +56 -1
  264. package/dist/validation/entry-validator.js.map +1 -1
  265. package/dist/worker/cms-worker.d.ts.map +1 -1
  266. package/dist/worker/cms-worker.js +21 -3
  267. package/dist/worker/cms-worker.js.map +1 -1
  268. package/package.json +13 -1
  269. package/dist/asset-store.d.ts +0 -32
  270. package/dist/asset-store.d.ts.map +0 -1
  271. package/dist/asset-store.js +0 -85
  272. package/dist/asset-store.js.map +0 -1
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Pure parser/formatter for `/assets/t/{directives}/{hash32}/{slug}.{ext}`
3
+ * transform URLs. Isomorphic and dependency-free by design: this module has
4
+ * NO imports (not even other files in this directory) so it can be imported
5
+ * from client bundles (via `assetUrl`/`assetSrcSet` in asset-url.ts, exported
6
+ * off the package's main entry) as well as from the server-only transform
7
+ * engine (transform.ts) and the future prod transform Lambda (PR 7), without
8
+ * ever pulling in node:crypto, sharp, or any other server-only dependency.
9
+ *
10
+ * `{directives}` is either the literal identity token (`orig`) or a
11
+ * comma-separated list of `key=value` pairs drawn from:
12
+ * w={int} output width - allowlisted to multiples of 160 in [160, 4096]
13
+ * (bounds cache-stuffing; upscaling is rejected at transform
14
+ * time via `withoutEnlargement`, not here)
15
+ * f={fmt} output format: webp | jpeg | png - when present, the URL's
16
+ * `{ext}` must equal it exactly
17
+ * q={int} quality 1..100 (encoder-dependent)
18
+ * c={rect} normalized crop rect `x:y:w:h`, four floats in [0,1] with
19
+ * x+w<=1 and y+h<=1, w>0 and h>0 (colon-separated - commas are
20
+ * the directive separator)
21
+ *
22
+ * Duplicate keys, unknown keys, and empty directive strings are rejected.
23
+ * `orig` must appear alone.
24
+ */
25
+ /** The identity transform directive name - EXIF-strip only, no other change. */
26
+ export const IDENTITY_TRANSFORM_DIRECTIVE = 'orig';
27
+ const MIN_WIDTH = 160;
28
+ const MAX_WIDTH = 4096;
29
+ const WIDTH_STEP = 160;
30
+ /**
31
+ * Decompression-bomb cap: the max `width * height` (post-decode pixel count)
32
+ * any raster the pipeline handles may have - checked both at upload finalize
33
+ * time (pipeline.ts, against the raw source dims) and at transform decode
34
+ * time (transform.ts, via sharp's `limitInputPixels`). Shared here (rather
35
+ * than duplicated in each server-only module) so it stays a single number,
36
+ * and because this file is dependency-free/isomorphic it can be imported by
37
+ * both without pulling sharp or file-type into anything. 4096x4096 (16.7 MP)
38
+ * comfortably covers the largest output this system ever serves (MAX_WIDTH)
39
+ * while still bounding decode-time memory for a small/compressible-but-huge
40
+ * source (e.g. a 30000x30000 solid-color PNG).
41
+ */
42
+ export const MAX_INPUT_PIXELS = 4096 * 4096;
43
+ // Quality is allowlisted (multiples of 5 in [30, 95] - 14 values) for the
44
+ // same cache-stuffing reason as width: every accepted directive combination
45
+ // becomes a stored cache object in prod, so unbounded q would multiply the
46
+ // per-asset variant space by 100. Crop remains the one effectively unbounded
47
+ // dimension (editor rects need float precision) - prod mitigation (rate
48
+ // limiting / signed crops) is tracked in the design record for the CDK PR.
49
+ const MIN_QUALITY = 30;
50
+ const MAX_QUALITY = 95;
51
+ const QUALITY_STEP = 5;
52
+ const CROP_PRECISION = 4;
53
+ const HASH32_RE = /^[a-f0-9]{32}$/;
54
+ const SLUG_RE = /^[a-z0-9-]+$/;
55
+ const EXT_RE = /^[a-z0-9]{1,10}$/;
56
+ const POSITIVE_INT_RE = /^[1-9][0-9]*$/;
57
+ // The two alternatives are mutually exclusive on their very first character
58
+ // ('0' vs '1'), and neither branch nests a quantifier inside another - each
59
+ // character class match advances the position by exactly one, so there is
60
+ // exactly one way to parse any matching string (no backtracking ambiguity).
61
+ // The security plugin's heuristic flags any alternation-plus-quantifier
62
+ // shape regardless; input here is also a single bounded URL path segment,
63
+ // not attacker-scalable text.
64
+ // eslint-disable-next-line security/detect-unsafe-regex
65
+ const UNIT_FLOAT_RE = /^(?:0(?:\.[0-9]+)?|1(?:\.0+)?)$/;
66
+ const KNOWN_DIRECTIVE_KEYS = new Set(['w', 'f', 'q', 'c']);
67
+ function isOutputFormat(value) {
68
+ return value === 'webp' || value === 'jpeg' || value === 'png';
69
+ }
70
+ /** True if `width` is on the allowlist: a multiple of 160 in [160, 4096]. */
71
+ export function isAllowedTransformWidth(width) {
72
+ return (Number.isInteger(width) && width >= MIN_WIDTH && width <= MAX_WIDTH && width % WIDTH_STEP === 0);
73
+ }
74
+ function parseWidth(value) {
75
+ if (!POSITIVE_INT_RE.test(value))
76
+ return null;
77
+ const n = Number(value);
78
+ return isAllowedTransformWidth(n) ? n : null;
79
+ }
80
+ /** True if `quality` is on the allowlist: a multiple of 5 in [30, 95]. */
81
+ export function isAllowedTransformQuality(quality) {
82
+ return (Number.isInteger(quality) &&
83
+ quality >= MIN_QUALITY &&
84
+ quality <= MAX_QUALITY &&
85
+ quality % QUALITY_STEP === 0);
86
+ }
87
+ function parseQuality(value) {
88
+ if (!POSITIVE_INT_RE.test(value))
89
+ return null;
90
+ const n = Number(value);
91
+ return isAllowedTransformQuality(n) ? n : null;
92
+ }
93
+ function parseUnitFloat(value) {
94
+ if (!UNIT_FLOAT_RE.test(value))
95
+ return null;
96
+ const n = Number(value);
97
+ return n >= 0 && n <= 1 ? n : null;
98
+ }
99
+ /**
100
+ * True when x, y, w, h describe a valid normalized crop rect: all four values
101
+ * finite and in [0,1], w and h strictly positive, and the rect fits within
102
+ * bounds (x+w<=1, y+h<=1). Shared by the URL-directive parser (`parseCrop`,
103
+ * below, from a `x:y:w:h` string) and image field value validation (from a
104
+ * `{x,y,w,h}` object, in validation/entry-validator.ts) so both enforce
105
+ * identical constraints.
106
+ */
107
+ export function isValidCropRect(x, y, w, h) {
108
+ for (const n of [x, y, w, h]) {
109
+ if (!Number.isFinite(n) || n < 0 || n > 1)
110
+ return false;
111
+ }
112
+ if (w <= 0 || h <= 0)
113
+ return false;
114
+ if (x + w > 1 || y + h > 1)
115
+ return false;
116
+ return true;
117
+ }
118
+ function parseCrop(value) {
119
+ const parts = value.split(':');
120
+ if (parts.length !== 4)
121
+ return null;
122
+ const nums = parts.map(parseUnitFloat);
123
+ if (nums.some((n) => n === null))
124
+ return null;
125
+ const [x, y, w, h] = nums;
126
+ if (!isValidCropRect(x, y, w, h))
127
+ return null;
128
+ return { x, y, w, h };
129
+ }
130
+ function formatUnitFloat(n) {
131
+ return n.toFixed(CROP_PRECISION);
132
+ }
133
+ function err(error) {
134
+ return { ok: false, error };
135
+ }
136
+ /**
137
+ * Parse a `key=value,key=value` directive string (everything between
138
+ * `assets/t/` and the hash32 segment) into a `TransformDirectives`, or
139
+ * `orig` into the identity directive.
140
+ */
141
+ function parseDirectivesString(raw) {
142
+ if (raw === IDENTITY_TRANSFORM_DIRECTIVE) {
143
+ return { ok: true, directives: { identity: true } };
144
+ }
145
+ if (raw.length === 0) {
146
+ return err('Empty directives');
147
+ }
148
+ const seen = new Set();
149
+ let width;
150
+ let format;
151
+ let quality;
152
+ let crop;
153
+ for (const pair of raw.split(',')) {
154
+ const eqIdx = pair.indexOf('=');
155
+ // Also catches `orig` combined with other directives (e.g. `orig,w=320`):
156
+ // `orig` has no `=`, so it falls into this same malformed-pair branch -
157
+ // `orig` is only ever valid when it is the entire directives string.
158
+ if (eqIdx <= 0 || eqIdx === pair.length - 1) {
159
+ return err(`Malformed directive: '${pair}'`);
160
+ }
161
+ const key = pair.slice(0, eqIdx);
162
+ const value = pair.slice(eqIdx + 1);
163
+ if (!KNOWN_DIRECTIVE_KEYS.has(key)) {
164
+ return err(`Unknown directive key: '${key}'`);
165
+ }
166
+ if (seen.has(key)) {
167
+ return err(`Duplicate directive key: '${key}'`);
168
+ }
169
+ seen.add(key);
170
+ switch (key) {
171
+ case 'w': {
172
+ const w = parseWidth(value);
173
+ if (w === null)
174
+ return err(`Invalid width: '${value}'`);
175
+ width = w;
176
+ break;
177
+ }
178
+ case 'f': {
179
+ if (!isOutputFormat(value))
180
+ return err(`Invalid format: '${value}'`);
181
+ format = value;
182
+ break;
183
+ }
184
+ case 'q': {
185
+ const q = parseQuality(value);
186
+ if (q === null)
187
+ return err(`Invalid quality: '${value}'`);
188
+ quality = q;
189
+ break;
190
+ }
191
+ case 'c': {
192
+ const c = parseCrop(value);
193
+ if (!c)
194
+ return err(`Invalid crop: '${value}'`);
195
+ crop = c;
196
+ break;
197
+ }
198
+ }
199
+ }
200
+ return { ok: true, directives: { identity: false, width, format, quality, crop } };
201
+ }
202
+ /**
203
+ * Parse the three path segments after the `assets/t/` prefix:
204
+ * `[directivesRaw, hash32, "{slug}.{ext}"]`. The raw route strips the
205
+ * `assets/t/` prefix and splits the remaining key by `/` before calling this.
206
+ */
207
+ export function parseTransformPath(segments) {
208
+ if (segments.length !== 3) {
209
+ return err(`Expected 3 path segments (directives/hash32/slug.ext), got ${segments.length}`);
210
+ }
211
+ const [directivesRaw, hash32, slugExt] = segments;
212
+ if (!HASH32_RE.test(hash32)) {
213
+ return err(`Invalid hash32: '${hash32}'`);
214
+ }
215
+ const lastDot = slugExt.lastIndexOf('.');
216
+ if (lastDot <= 0 || lastDot === slugExt.length - 1) {
217
+ return err(`Invalid slug.ext segment: '${slugExt}'`);
218
+ }
219
+ const slug = slugExt.slice(0, lastDot);
220
+ const ext = slugExt.slice(lastDot + 1);
221
+ if (!SLUG_RE.test(slug)) {
222
+ return err(`Invalid slug: '${slug}'`);
223
+ }
224
+ if (!EXT_RE.test(ext)) {
225
+ return err(`Invalid ext: '${ext}'`);
226
+ }
227
+ const parsedDirectives = parseDirectivesString(directivesRaw);
228
+ if (!parsedDirectives.ok) {
229
+ return parsedDirectives;
230
+ }
231
+ const { directives } = parsedDirectives;
232
+ if (!directives.identity && directives.format !== undefined && directives.format !== ext) {
233
+ return err(`Extension '${ext}' does not match format '${directives.format}'`);
234
+ }
235
+ return { ok: true, directives, hash32, slug, ext };
236
+ }
237
+ /**
238
+ * Canonical string form of a directive set, so equivalent directive sets
239
+ * (different key order, different float formatting) always map to the same
240
+ * cache key. Order is fixed alphabetically by key: c, f, q, w.
241
+ */
242
+ export function formatDirectives(directives) {
243
+ if (directives.identity) {
244
+ return IDENTITY_TRANSFORM_DIRECTIVE;
245
+ }
246
+ const parts = [];
247
+ if (directives.crop) {
248
+ const { x, y, w, h } = directives.crop;
249
+ parts.push(`c=${formatUnitFloat(x)}:${formatUnitFloat(y)}:${formatUnitFloat(w)}:${formatUnitFloat(h)}`);
250
+ }
251
+ if (directives.format !== undefined) {
252
+ parts.push(`f=${directives.format}`);
253
+ }
254
+ if (directives.quality !== undefined) {
255
+ parts.push(`q=${directives.quality}`);
256
+ }
257
+ if (directives.width !== undefined) {
258
+ parts.push(`w=${directives.width}`);
259
+ }
260
+ // A validly-parsed ResizeDirectives always has at least one field set
261
+ // (parseDirectivesString rejects an empty directive string), but a
262
+ // programmatically-constructed one (e.g. from asset-url.ts's merge logic)
263
+ // could end up all-undefined - fall back to identity rather than emit an
264
+ // empty directives segment.
265
+ return parts.length > 0 ? parts.join(',') : IDENTITY_TRANSFORM_DIRECTIVE;
266
+ }
267
+ //# sourceMappingURL=transform-directives.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform-directives.js","sourceRoot":"","sources":["../../src/assets/transform-directives.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The shared transform engine: applies a parsed `TransformDirectives` to
3
+ * source image bytes with sharp. Server-only - never import this from
4
+ * client/editor code (this is why it lives in its own file, separate from
5
+ * the dependency-free transform-directives.ts). Used by the dev-mode lazy
6
+ * `/assets/t/*` emulation in api/assets.ts today, and will be reused
7
+ * unchanged by the prod transform Lambda (PR 7).
8
+ *
9
+ * Pipeline: a cheap metadata-only probe (`limitInputPixels: MAX_INPUT_PIXELS`,
10
+ * decompression-bomb defense #1) learns the real page count, so animated
11
+ * GIF/WebP frames can be capped at `MAX_ANIMATED_FRAMES` (decompression-bomb
12
+ * defense #2) without exceeding it and making sharp throw -> load for real
13
+ * with `{ pages: min(totalPages, MAX_ANIMATED_FRAMES), limitInputPixels }` ->
14
+ * `.rotate()` with no args (bakes EXIF orientation into pixels,
15
+ * dropping the orientation tag) -> optional crop via `.extract()` -> optional
16
+ * `.resize({ width, withoutEnlargement: true })` (never upscales) -> encode.
17
+ *
18
+ * Identity (`orig`) and any request that omits an explicit `f=` format
19
+ * re-encode through the SOURCE format rather than a fixed one: this is what
20
+ * guarantees EXIF/GPS gets stripped even when no other change is requested
21
+ * (sharp strips metadata by default on re-encode - `withMetadata()` is never
22
+ * called here, which would undo that).
23
+ *
24
+ * GIF has no EXIF segment (EXIF is a JPEG/TIFF APP1 marker; GIF has no such
25
+ * extension block), so an identity GIF has nothing to strip - it is still
26
+ * re-encoded through sharp's `.gif()` (verified: current sharp bundles cgif
27
+ * and re-encodes multi-frame GIFs through the `{ animated: true }` input
28
+ * path with no extra native dependency) purely for pipeline uniformity, not
29
+ * because GIF needs stripping.
30
+ */
31
+ import { type TransformDirectives } from './transform-directives';
32
+ export interface ApplyTransformInput {
33
+ data: Uint8Array;
34
+ /** Source extension (lowercased file-type ext, e.g. 'png' | 'jpg' | 'webp' | 'gif'). */
35
+ ext: string;
36
+ }
37
+ export interface TransformSuccess {
38
+ ok: true;
39
+ data: Uint8Array;
40
+ contentType: string;
41
+ ext: string;
42
+ }
43
+ export interface TransformRejection {
44
+ ok: false;
45
+ /** 400: unsupported input; 413: output too large; 422: sharp failed to process the input. */
46
+ status: 400 | 413 | 422;
47
+ error: string;
48
+ }
49
+ export type TransformResult = TransformSuccess | TransformRejection;
50
+ export declare function applyTransform(input: ApplyTransformInput, directives: TransformDirectives): Promise<TransformResult>;
51
+ //# sourceMappingURL=transform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../src/assets/transform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAKH,OAAO,EAIL,KAAK,mBAAmB,EACzB,MAAM,wBAAwB,CAAA;AAyC/B,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,UAAU,CAAA;IAChB,wFAAwF;IACxF,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,IAAI,CAAA;IACR,IAAI,EAAE,UAAU,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,KAAK,CAAA;IACT,6FAA6F;IAC7F,MAAM,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;IACvB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,MAAM,eAAe,GAAG,gBAAgB,GAAG,kBAAkB,CAAA;AAgFnE,wBAAsB,cAAc,CAClC,KAAK,EAAE,mBAAmB,EAC1B,UAAU,EAAE,mBAAmB,GAC9B,OAAO,CAAC,eAAe,CAAC,CAwE1B"}
@@ -0,0 +1,190 @@
1
+ /**
2
+ * The shared transform engine: applies a parsed `TransformDirectives` to
3
+ * source image bytes with sharp. Server-only - never import this from
4
+ * client/editor code (this is why it lives in its own file, separate from
5
+ * the dependency-free transform-directives.ts). Used by the dev-mode lazy
6
+ * `/assets/t/*` emulation in api/assets.ts today, and will be reused
7
+ * unchanged by the prod transform Lambda (PR 7).
8
+ *
9
+ * Pipeline: a cheap metadata-only probe (`limitInputPixels: MAX_INPUT_PIXELS`,
10
+ * decompression-bomb defense #1) learns the real page count, so animated
11
+ * GIF/WebP frames can be capped at `MAX_ANIMATED_FRAMES` (decompression-bomb
12
+ * defense #2) without exceeding it and making sharp throw -> load for real
13
+ * with `{ pages: min(totalPages, MAX_ANIMATED_FRAMES), limitInputPixels }` ->
14
+ * `.rotate()` with no args (bakes EXIF orientation into pixels,
15
+ * dropping the orientation tag) -> optional crop via `.extract()` -> optional
16
+ * `.resize({ width, withoutEnlargement: true })` (never upscales) -> encode.
17
+ *
18
+ * Identity (`orig`) and any request that omits an explicit `f=` format
19
+ * re-encode through the SOURCE format rather than a fixed one: this is what
20
+ * guarantees EXIF/GPS gets stripped even when no other change is requested
21
+ * (sharp strips metadata by default on re-encode - `withMetadata()` is never
22
+ * called here, which would undo that).
23
+ *
24
+ * GIF has no EXIF segment (EXIF is a JPEG/TIFF APP1 marker; GIF has no such
25
+ * extension block), so an identity GIF has nothing to strip - it is still
26
+ * re-encoded through sharp's `.gif()` (verified: current sharp bundles cgif
27
+ * and re-encodes multi-frame GIFs through the `{ animated: true }` input
28
+ * path with no extra native dependency) purely for pipeline uniformity, not
29
+ * because GIF needs stripping.
30
+ */
31
+ import sharp from 'sharp';
32
+ import { getErrorMessage } from '../utils/error.js';
33
+ import { MAX_INPUT_PIXELS, } from './transform-directives.js';
34
+ /** Raster formats the transform engine accepts as input. svg/pdf never reach here - they're served statically. */
35
+ const ALLOWED_INPUT_EXTS = new Set(['png', 'jpg', 'jpeg', 'webp', 'gif']);
36
+ /** Defensive cap on encoded output size - prevents cache-stuffing with giant re-encodes. */
37
+ const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
38
+ /**
39
+ * Cap on decoded animated frames (GIF/WebP) - without this, `{ animated: true }`
40
+ * (sharp's `pages: -1`) decodes every frame of a maliciously-crafted
41
+ * many-thousand-frame animation into memory at once, a decompression-bomb
42
+ * vector distinct from (and not covered by) `limitInputPixels`, which only
43
+ * bounds a single frame's width x height. 60 frames covers any reasonable
44
+ * animated asset (2s at 30fps) with headroom.
45
+ */
46
+ const MAX_ANIMATED_FRAMES = 60;
47
+ const CONTENT_TYPE_BY_FORMAT = {
48
+ webp: 'image/webp',
49
+ jpeg: 'image/jpeg',
50
+ png: 'image/png',
51
+ };
52
+ const CONTENT_TYPE_BY_EXT = {
53
+ png: 'image/png',
54
+ jpg: 'image/jpeg',
55
+ jpeg: 'image/jpeg',
56
+ webp: 'image/webp',
57
+ gif: 'image/gif',
58
+ };
59
+ /**
60
+ * Dimensions AFTER `.rotate()` has auto-oriented the pixels. sharp's
61
+ * `metadata()` reports the RAW (pre-rotation) width/height plus the EXIF
62
+ * `orientation` tag; for orientation 5-8 (the 90-degree rotations that
63
+ * portrait phone photos almost always carry) the visual axes are swapped.
64
+ * The crop rect was normalized by the editor against the browser's
65
+ * auto-oriented preview, so `.extract()` - which runs on the post-rotation
66
+ * pipeline - must be computed against these swapped dims, NOT the raw ones.
67
+ * Using the raw dims makes the extract region exceed the rotated image's
68
+ * bounds and sharp throws `bad extract area`. sharp >=0.33 also exposes an
69
+ * `autoOrient` block with the corrected dims; prefer it when present.
70
+ */
71
+ function orientedDimensions(meta) {
72
+ const auto = meta.autoOrient;
73
+ if (auto && auto.width && auto.height) {
74
+ return { width: auto.width, height: auto.height };
75
+ }
76
+ if (!meta.width || !meta.height)
77
+ return null;
78
+ const swap = typeof meta.orientation === 'number' && meta.orientation >= 5 && meta.orientation <= 8;
79
+ return swap
80
+ ? { width: meta.height, height: meta.width }
81
+ : { width: meta.width, height: meta.height };
82
+ }
83
+ /**
84
+ * Compute a sharp `.extract()` region from a normalized crop rect and the
85
+ * (post-rotation) image dimensions. Rounds the left/top and right/bottom
86
+ * edges independently, then derives width/height from their difference -
87
+ * rounding width/height directly from `crop.w`/`crop.h` could drift the
88
+ * region past the image bounds when combined with independently-rounded
89
+ * left/top. Clamped defensively so float error can never produce an
90
+ * out-of-range extract (which sharp throws on).
91
+ */
92
+ function computeExtractRegion(crop, width, height) {
93
+ const left = Math.min(Math.max(Math.round(crop.x * width), 0), Math.max(width - 1, 0));
94
+ const top = Math.min(Math.max(Math.round(crop.y * height), 0), Math.max(height - 1, 0));
95
+ const right = Math.min(Math.round((crop.x + crop.w) * width), width);
96
+ const bottom = Math.min(Math.round((crop.y + crop.h) * height), height);
97
+ const extractWidth = Math.max(1, right - left);
98
+ const extractHeight = Math.max(1, bottom - top);
99
+ return { left, top, width: extractWidth, height: extractHeight };
100
+ }
101
+ function encode(pipeline, format, quality) {
102
+ const options = quality !== undefined ? { quality } : undefined;
103
+ switch (format) {
104
+ case 'webp':
105
+ return pipeline.webp(options);
106
+ case 'jpeg':
107
+ return pipeline.jpeg(options);
108
+ case 'png':
109
+ return pipeline.png(options);
110
+ }
111
+ }
112
+ /** Re-encode through the source container format - used for identity and for requests that omit `f=`. */
113
+ function encodeSourceFormat(pipeline, sourceExt) {
114
+ switch (sourceExt) {
115
+ case 'gif':
116
+ return pipeline.gif();
117
+ case 'webp':
118
+ return pipeline.webp();
119
+ case 'jpg':
120
+ case 'jpeg':
121
+ return pipeline.jpeg();
122
+ case 'png':
123
+ default:
124
+ return pipeline.png();
125
+ }
126
+ }
127
+ export async function applyTransform(input, directives) {
128
+ const sourceExt = input.ext.toLowerCase();
129
+ if (!ALLOWED_INPUT_EXTS.has(sourceExt)) {
130
+ return {
131
+ ok: false,
132
+ status: 400,
133
+ error: `Unsupported input format for transform: '${sourceExt}'`,
134
+ };
135
+ }
136
+ const resize = directives.identity ? undefined : directives;
137
+ const format = resize?.format;
138
+ const quality = resize?.quality;
139
+ try {
140
+ // A cheap header/metadata-only probe (no pixel decode - verified this
141
+ // costs effectively nothing even for a several-hundred-frame source) to
142
+ // learn the source's real page count before deciding whether to cap it.
143
+ // This has to happen before constructing the real pipeline: sharp's
144
+ // `pages` constructor option is a fixed request, not an upper bound - a
145
+ // `pages` value that EXCEEDS the source's actual page count makes sharp
146
+ // throw ("bad page number") rather than clamping, so `MAX_ANIMATED_FRAMES`
147
+ // can only be applied as `Math.min(totalPages, MAX_ANIMATED_FRAMES)`.
148
+ // `limitInputPixels` gates this probe exactly like it gates the real
149
+ // pipeline below - an oversized source throws here already, before any
150
+ // pixel buffer is ever allocated.
151
+ const probeMeta = await sharp(input.data, { limitInputPixels: MAX_INPUT_PIXELS }).metadata();
152
+ const totalPages = probeMeta.pages ?? 1;
153
+ const pagesToRead = Math.min(totalPages, MAX_ANIMATED_FRAMES);
154
+ let pipeline = sharp(input.data, {
155
+ pages: pagesToRead,
156
+ limitInputPixels: MAX_INPUT_PIXELS,
157
+ }).rotate();
158
+ if (resize?.crop) {
159
+ const oriented = orientedDimensions(await pipeline.metadata());
160
+ if (!oriented) {
161
+ return { ok: false, status: 422, error: 'Could not read image dimensions for crop' };
162
+ }
163
+ pipeline = pipeline.extract(computeExtractRegion(resize.crop, oriented.width, oriented.height));
164
+ }
165
+ if (resize?.width) {
166
+ pipeline = pipeline.resize({ width: resize.width, withoutEnlargement: true });
167
+ }
168
+ pipeline = format ? encode(pipeline, format, quality) : encodeSourceFormat(pipeline, sourceExt);
169
+ const data = await pipeline.toBuffer();
170
+ if (data.byteLength > MAX_OUTPUT_BYTES) {
171
+ return {
172
+ ok: false,
173
+ status: 413,
174
+ error: `Transformed output (${data.byteLength} bytes) exceeds the ${MAX_OUTPUT_BYTES}-byte cap`,
175
+ };
176
+ }
177
+ const outExt = format ?? sourceExt;
178
+ const contentType = format ? CONTENT_TYPE_BY_FORMAT[format] : CONTENT_TYPE_BY_EXT[sourceExt];
179
+ return {
180
+ ok: true,
181
+ data: new Uint8Array(data.buffer, data.byteOffset, data.byteLength),
182
+ contentType,
183
+ ext: outExt,
184
+ };
185
+ }
186
+ catch (err) {
187
+ return { ok: false, status: 422, error: `Transform failed: ${getErrorMessage(err)}` };
188
+ }
189
+ }
190
+ //# sourceMappingURL=transform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform.js","sourceRoot":"","sources":["../../src/assets/transform.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Asset store v2 contract.
3
+ *
4
+ * Pure type declarations only — no runtime imports. This lets client-side code
5
+ * (the editor) `import type` from this file without ever pulling node:fs or the
6
+ * S3 SDK into a browser bundle. The concrete stores (store-local.ts, store-s3.ts)
7
+ * and the factory are server-only and must never be imported from client code.
8
+ */
9
+ export interface AssetMeta {
10
+ hash32: string;
11
+ filename: string;
12
+ slug: string;
13
+ ext: string;
14
+ mime: string;
15
+ size: number;
16
+ width?: number;
17
+ height?: number;
18
+ kind: 'raster' | 'svg' | 'pdf';
19
+ uploadedBy?: string;
20
+ uploadedAt: string;
21
+ }
22
+ export type StagedUploadTarget = {
23
+ mode: 'direct';
24
+ url: string;
25
+ fields: Record<string, string>;
26
+ stagingKey: string;
27
+ maxBytes: number;
28
+ } | {
29
+ mode: 'proxied';
30
+ stagingKey: string;
31
+ maxBytes: number;
32
+ };
33
+ export interface BeginUploadInput {
34
+ filename: string;
35
+ contentType: string;
36
+ size?: number;
37
+ }
38
+ export interface PublicObject {
39
+ data: Uint8Array;
40
+ contentType?: string;
41
+ contentDisposition?: string;
42
+ cacheControl?: string;
43
+ }
44
+ export interface AssetStore {
45
+ readonly capabilities: {
46
+ directUpload: boolean;
47
+ };
48
+ beginUpload(input: BeginUploadInput): Promise<StagedUploadTarget>;
49
+ writeStaging(stagingKey: string, data: Uint8Array, contentType?: string): Promise<void>;
50
+ readStaging(stagingKey: string): Promise<Uint8Array | null>;
51
+ deleteStaging(stagingKey: string): Promise<void>;
52
+ putOriginal(input: {
53
+ hash32: string;
54
+ ext: string;
55
+ data: Uint8Array;
56
+ contentType: string;
57
+ }): Promise<void>;
58
+ readOriginal(hash32: string): Promise<{
59
+ data: Uint8Array;
60
+ ext: string;
61
+ contentType?: string;
62
+ } | null>;
63
+ putPublicObject(input: {
64
+ key: string;
65
+ data: Uint8Array;
66
+ contentType: string;
67
+ contentDisposition?: string;
68
+ cacheControl?: string;
69
+ }): Promise<void>;
70
+ readPublicObject(key: string): Promise<PublicObject | null>;
71
+ putMetaIfAbsent(hash32: string, meta: AssetMeta): Promise<'created' | 'already-exists'>;
72
+ getMeta(hash32: string): Promise<AssetMeta | null>;
73
+ listMeta(input?: {
74
+ cursor?: string;
75
+ limit?: number;
76
+ }): Promise<{
77
+ items: AssetMeta[];
78
+ nextCursor?: string;
79
+ }>;
80
+ deleteMeta(hash32: string): Promise<void>;
81
+ }
82
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/assets/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,QAAQ,GAAG,KAAK,GAAG,KAAK,CAAA;IAC9B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,MAAM,kBAAkB,GAC1B;IACE,IAAI,EAAE,QAAQ,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;CACjB,GACD;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAA;AAE7D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,UAAU,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,YAAY,EAAE;QAAE,YAAY,EAAE,OAAO,CAAA;KAAE,CAAA;IAChD,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAA;IACjE,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvF,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAA;IAC3D,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChD,WAAW,CAAC,KAAK,EAAE;QACjB,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,CAAA;IACjB,YAAY,CACV,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,CAAA;IAC1E,eAAe,CAAC,KAAK,EAAE;QACrB,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,CAAA;IACjB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAA;IAC3D,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,GAAG,gBAAgB,CAAC,CAAA;IACvF,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAA;IAClD,QAAQ,CAAC,KAAK,CAAC,EAAE;QACf,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,SAAS,EAAE,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxD,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1C"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Asset store v2 contract.
3
+ *
4
+ * Pure type declarations only — no runtime imports. This lets client-side code
5
+ * (the editor) `import type` from this file without ever pulling node:fs or the
6
+ * S3 SDK into a browser bundle. The concrete stores (store-local.ts, store-s3.ts)
7
+ * and the factory are server-only and must never be imported from client code.
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/assets/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
@@ -28,8 +28,8 @@ export declare const GroupsFileSchema: z.ZodObject<{
28
28
  description?: string | undefined;
29
29
  }>, "many">;
30
30
  }, "strip", z.ZodTypeAny, {
31
- updatedAt: string;
32
31
  version: 1;
32
+ updatedAt: string;
33
33
  groups: {
34
34
  name: string;
35
35
  members: string[];
@@ -39,8 +39,8 @@ export declare const GroupsFileSchema: z.ZodObject<{
39
39
  updatedBy: string;
40
40
  contentVersion?: number | undefined;
41
41
  }, {
42
- updatedAt: string;
43
42
  version: 1;
43
+ updatedAt: string;
44
44
  groups: {
45
45
  name: string;
46
46
  members: string[];