canopycms 0.0.67 → 0.0.68-int.101

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 (281) hide show
  1. package/README.md +24 -17
  2. package/dist/ai/handler.js +8 -0
  3. package/dist/api/admin-branch-health.js +17 -20
  4. package/dist/api/admin.d.ts +30 -9
  5. package/dist/api/admin.js +32 -2
  6. package/dist/api/assets.js +84 -76
  7. package/dist/api/branch-create-window.d.ts +12 -0
  8. package/dist/api/branch-create-window.js +13 -0
  9. package/dist/api/branch-review.d.ts +3 -3
  10. package/dist/api/branch-review.js +14 -5
  11. package/dist/api/branch-status.d.ts +5 -5
  12. package/dist/api/branch-status.js +26 -6
  13. package/dist/api/branch-withdraw.d.ts +2 -2
  14. package/dist/api/branch-withdraw.js +7 -2
  15. package/dist/api/branch.d.ts +15 -2
  16. package/dist/api/branch.js +118 -92
  17. package/dist/api/client.d.ts +18 -6
  18. package/dist/api/client.js +79 -12
  19. package/dist/api/content.d.ts +6 -5
  20. package/dist/api/content.js +44 -41
  21. package/dist/api/entries.js +16 -15
  22. package/dist/api/github-sync.d.ts +19 -1
  23. package/dist/api/github-sync.js +57 -10
  24. package/dist/api/index.d.ts +1 -1
  25. package/dist/api/reference-options.js +1 -1
  26. package/dist/api/resolve-references.js +14 -40
  27. package/dist/api/settings-helpers.d.ts +1 -1
  28. package/dist/api/settings-helpers.js +1 -4
  29. package/dist/api/user.d.ts +3 -0
  30. package/dist/api/user.js +3 -0
  31. package/dist/assets/asset-url.d.ts +22 -16
  32. package/dist/assets/asset-url.js +42 -25
  33. package/dist/assets/factory.d.ts +5 -0
  34. package/dist/assets/factory.js +5 -0
  35. package/dist/assets/finalize.js +3 -0
  36. package/dist/assets/index.d.ts +1 -1
  37. package/dist/assets/materialize.d.ts +153 -0
  38. package/dist/assets/materialize.js +402 -0
  39. package/dist/assets/store-local.d.ts +19 -5
  40. package/dist/assets/store-local.js +69 -10
  41. package/dist/assets/store-s3.d.ts +38 -7
  42. package/dist/assets/store-s3.js +128 -22
  43. package/dist/assets/transform-directives.d.ts +52 -13
  44. package/dist/assets/transform-directives.js +87 -27
  45. package/dist/assets/transform.d.ts +3 -3
  46. package/dist/assets/transform.js +13 -8
  47. package/dist/assets/types.d.ts +31 -4
  48. package/dist/auth/file-based-auth-cache.js +7 -8
  49. package/dist/authorization/content.d.ts +5 -4
  50. package/dist/authorization/content.js +5 -5
  51. package/dist/authorization/path.d.ts +11 -6
  52. package/dist/authorization/path.js +11 -6
  53. package/dist/authorization/types.d.ts +2 -2
  54. package/dist/branch-health.d.ts +7 -1
  55. package/dist/branch-health.js +8 -3
  56. package/dist/branch-metadata.d.ts +5 -5
  57. package/dist/branch-metadata.js +44 -36
  58. package/dist/branch-provisioning.d.ts +167 -0
  59. package/dist/branch-provisioning.js +501 -0
  60. package/dist/branch-schema-cache.d.ts +5 -3
  61. package/dist/branch-schema-cache.js +29 -11
  62. package/dist/branch-sparse.d.ts +23 -0
  63. package/dist/branch-sparse.js +118 -0
  64. package/dist/branch-workspace.d.ts +42 -3
  65. package/dist/branch-workspace.js +240 -94
  66. package/dist/build/asset-refs.d.ts +93 -0
  67. package/dist/build/asset-refs.js +251 -0
  68. package/dist/build-identity.d.ts +3 -0
  69. package/dist/build-identity.js +13 -0
  70. package/dist/cli/asset-refs.d.ts +52 -0
  71. package/dist/cli/asset-refs.js +194 -0
  72. package/dist/cli/cli.d.ts +15 -1
  73. package/dist/cli/cli.js +5928 -2195
  74. package/dist/cli/configured-asset-store.d.ts +12 -0
  75. package/dist/cli/configured-asset-store.js +46 -0
  76. package/dist/cli/generate-ai-content.js +2185 -587
  77. package/dist/cli/github-app-manifest.d.ts +4 -3
  78. package/dist/cli/github-app-manifest.js +4 -3
  79. package/dist/cli/init.js +30 -27
  80. package/dist/cli/migrate.js +12 -8
  81. package/dist/cli/sync.js +83 -37
  82. package/dist/cli/template-files/Dockerfile.cms.template +8 -0
  83. package/dist/cli/template-files/cdk-app.ts.template +4 -0
  84. package/dist/cli/template-files/cms-stack.ts.template +23 -12
  85. package/dist/cli/template-files/deploy-cms.yml.template +2 -0
  86. package/dist/client.d.ts +1 -1
  87. package/dist/client.js +1 -1
  88. package/dist/config/helpers.js +1 -2
  89. package/dist/config/schemas/config.d.ts +19 -32
  90. package/dist/config/schemas/config.js +9 -11
  91. package/dist/config/schemas/field.js +1 -0
  92. package/dist/config/schemas/media.d.ts +4 -13
  93. package/dist/config/schemas/media.js +5 -8
  94. package/dist/config/schemas/url.d.ts +4 -20
  95. package/dist/config/schemas/url.js +6 -22
  96. package/dist/config/types.d.ts +30 -20
  97. package/dist/config/validation.d.ts +6 -0
  98. package/dist/config/validation.js +37 -0
  99. package/dist/content-listing.d.ts +12 -12
  100. package/dist/content-listing.js +10 -6
  101. package/dist/content-reader.d.ts +11 -2
  102. package/dist/content-reader.js +22 -14
  103. package/dist/content-store.d.ts +37 -9
  104. package/dist/content-store.js +70 -43
  105. package/dist/content-tree.d.ts +1 -1
  106. package/dist/content-tree.js +2 -2
  107. package/dist/context.d.ts +27 -9
  108. package/dist/context.js +154 -70
  109. package/dist/editor/BranchManager.d.ts +7 -1
  110. package/dist/editor/BranchManager.js +29 -14
  111. package/dist/editor/CanopyEditor.d.ts +1 -1
  112. package/dist/editor/CanopyEditor.js +2 -6
  113. package/dist/editor/Editor.d.ts +4 -3
  114. package/dist/editor/Editor.js +32 -22
  115. package/dist/editor/FormRenderer.js +34 -18
  116. package/dist/editor/PreviewFrame.d.ts +24 -0
  117. package/dist/editor/PreviewFrame.js +140 -0
  118. package/dist/editor/admin/SystemHealthPanel.js +47 -7
  119. package/dist/editor/components/BranchesDrawer.d.ts +9 -0
  120. package/dist/editor/components/BranchesDrawer.js +4 -0
  121. package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
  122. package/dist/editor/components/NoEditPermissionNotice.js +17 -0
  123. package/dist/editor/context/AssetContext.d.ts +18 -35
  124. package/dist/editor/context/AssetContext.js +20 -29
  125. package/dist/editor/context/index.d.ts +1 -1
  126. package/dist/editor/context/index.js +1 -1
  127. package/dist/editor/editor-config.d.ts +1 -2
  128. package/dist/editor/editor-config.js +0 -23
  129. package/dist/editor/editor-utils.d.ts +24 -21
  130. package/dist/editor/editor-utils.js +75 -62
  131. package/dist/editor/fields/BlockField.d.ts +1 -0
  132. package/dist/editor/fields/BlockField.js +6 -4
  133. package/dist/editor/fields/CodeField.d.ts +1 -0
  134. package/dist/editor/fields/CodeField.js +2 -2
  135. package/dist/editor/fields/DateTimeField.d.ts +1 -0
  136. package/dist/editor/fields/DateTimeField.js +2 -2
  137. package/dist/editor/fields/FieldDescription.d.ts +16 -0
  138. package/dist/editor/fields/FieldDescription.js +11 -0
  139. package/dist/editor/fields/ImageField.d.ts +1 -0
  140. package/dist/editor/fields/ImageField.js +3 -2
  141. package/dist/editor/fields/InlineGroupField.js +4 -1
  142. package/dist/editor/fields/MarkdownField.d.ts +1 -0
  143. package/dist/editor/fields/MarkdownField.js +115 -20
  144. package/dist/editor/fields/NumberField.d.ts +1 -0
  145. package/dist/editor/fields/NumberField.js +2 -2
  146. package/dist/editor/fields/NumberListField.d.ts +1 -0
  147. package/dist/editor/fields/NumberListField.js +2 -2
  148. package/dist/editor/fields/ObjectField.d.ts +1 -0
  149. package/dist/editor/fields/ObjectField.js +5 -2
  150. package/dist/editor/fields/ReferenceField.d.ts +1 -0
  151. package/dist/editor/fields/ReferenceField.js +5 -4
  152. package/dist/editor/fields/SelectField.d.ts +1 -0
  153. package/dist/editor/fields/SelectField.js +2 -2
  154. package/dist/editor/fields/StringListField.d.ts +1 -0
  155. package/dist/editor/fields/StringListField.js +2 -2
  156. package/dist/editor/fields/TextField.d.ts +1 -0
  157. package/dist/editor/fields/TextField.js +2 -2
  158. package/dist/editor/fields/ToggleField.d.ts +1 -0
  159. package/dist/editor/fields/ToggleField.js +7 -4
  160. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
  161. package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
  162. package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
  163. package/dist/editor/fields/mdx-jsx-support.js +137 -0
  164. package/dist/editor/hooks/create-branch-request.d.ts +20 -0
  165. package/dist/editor/hooks/create-branch-request.js +52 -0
  166. package/dist/editor/hooks/useBranchActions.d.ts +13 -2
  167. package/dist/editor/hooks/useBranchActions.js +48 -17
  168. package/dist/editor/hooks/useBranchManager.d.ts +21 -0
  169. package/dist/editor/hooks/useBranchManager.js +204 -37
  170. package/dist/editor/hooks/useBranchesData.d.ts +6 -0
  171. package/dist/editor/hooks/useBranchesData.js +9 -3
  172. package/dist/editor/hooks/useCommentSystem.js +25 -6
  173. package/dist/editor/hooks/useDraftManager.d.ts +16 -1
  174. package/dist/editor/hooks/useDraftManager.js +193 -36
  175. package/dist/editor/hooks/useEntriesData.js +3 -3
  176. package/dist/editor/hooks/useEntryManager.d.ts +2 -1
  177. package/dist/editor/hooks/useEntryManager.js +34 -19
  178. package/dist/editor/media/AssetCard.d.ts +1 -1
  179. package/dist/editor/media/crop-math.d.ts +3 -4
  180. package/dist/editor/media/crop-math.js +11 -22
  181. package/dist/editor/media/editor-image-src.d.ts +7 -0
  182. package/dist/editor/media/editor-image-src.js +12 -0
  183. package/dist/editor/preview-asset-base.d.ts +8 -0
  184. package/dist/editor/preview-asset-base.js +13 -0
  185. package/dist/editor/preview-bridge.d.ts +9 -21
  186. package/dist/editor/preview-bridge.js +19 -139
  187. package/dist/editor/preview-path.d.ts +10 -0
  188. package/dist/editor/preview-path.js +20 -0
  189. package/dist/editor/theme.js +2 -2
  190. package/dist/entry-schema-registry.d.ts +4 -4
  191. package/dist/entry-schema-registry.js +10 -6
  192. package/dist/entry-schema.d.ts +27 -4
  193. package/dist/entry-schema.js +20 -3
  194. package/dist/git-manager.d.ts +173 -6
  195. package/dist/git-manager.js +594 -76
  196. package/dist/github-service.d.ts +15 -0
  197. package/dist/github-service.js +19 -1
  198. package/dist/http/handler.js +37 -13
  199. package/dist/http/index.d.ts +2 -0
  200. package/dist/http/index.js +2 -0
  201. package/dist/http/router.d.ts +2 -0
  202. package/dist/http/router.js +2 -1
  203. package/dist/http/types.d.ts +2 -0
  204. package/dist/http/worker-not-ready.d.ts +10 -0
  205. package/dist/http/worker-not-ready.js +24 -0
  206. package/dist/operating-mode/client-unsafe-strategy.js +0 -6
  207. package/dist/operating-mode/types.d.ts +1 -4
  208. package/dist/paths/branch-name.d.ts +5 -0
  209. package/dist/paths/branch-name.js +9 -0
  210. package/dist/paths/branch.d.ts +6 -2
  211. package/dist/paths/branch.js +16 -10
  212. package/dist/paths/index.d.ts +3 -3
  213. package/dist/paths/index.js +3 -3
  214. package/dist/paths/normalize.d.ts +7 -0
  215. package/dist/paths/normalize.js +9 -0
  216. package/dist/paths/validation.js +0 -2
  217. package/dist/preview.d.ts +8 -0
  218. package/dist/preview.js +7 -0
  219. package/dist/reference-resolver.d.ts +5 -21
  220. package/dist/reference-resolver.js +9 -44
  221. package/dist/resolve-canopy-user.js +2 -1
  222. package/dist/schema/schema-store.d.ts +54 -31
  223. package/dist/schema/schema-store.js +77 -31
  224. package/dist/server.d.ts +64 -9
  225. package/dist/server.js +48 -8
  226. package/dist/services.d.ts +18 -6
  227. package/dist/services.js +85 -70
  228. package/dist/settings-workspace.d.ts +23 -3
  229. package/dist/settings-workspace.js +197 -45
  230. package/dist/static/seo.d.ts +2 -14
  231. package/dist/static/seo.js +2 -25
  232. package/dist/submission-attribution.d.ts +73 -0
  233. package/dist/submission-attribution.js +221 -0
  234. package/dist/sync-core.d.ts +12 -1
  235. package/dist/sync-core.js +31 -13
  236. package/dist/task-queue/worker-status.d.ts +8 -0
  237. package/dist/task-queue/worker-status.js +17 -0
  238. package/dist/types.d.ts +31 -2
  239. package/dist/utils/content-serialize.d.ts +5 -2
  240. package/dist/utils/content-serialize.js +98 -24
  241. package/dist/utils/content-write-lock.d.ts +18 -7
  242. package/dist/utils/content-write-lock.js +18 -9
  243. package/dist/utils/debug.d.ts +8 -0
  244. package/dist/utils/debug.js +10 -2
  245. package/dist/utils/git.d.ts +32 -0
  246. package/dist/utils/git.js +42 -0
  247. package/dist/utils/occ-json-write.js +10 -2
  248. package/dist/utils/provision-log.d.ts +25 -0
  249. package/dist/utils/provision-log.js +49 -0
  250. package/dist/utils/provisioning-lock.d.ts +35 -5
  251. package/dist/utils/provisioning-lock.js +73 -12
  252. package/dist/utils/request-timing.d.ts +25 -0
  253. package/dist/utils/request-timing.js +101 -0
  254. package/dist/utils/sanitize-href.d.ts +8 -22
  255. package/dist/utils/sanitize-href.js +11 -28
  256. package/dist/utils/url-prefix.d.ts +30 -0
  257. package/dist/utils/url-prefix.js +61 -0
  258. package/dist/utils/yaml-source-splice.d.ts +58 -0
  259. package/dist/utils/yaml-source-splice.js +515 -0
  260. package/dist/validation/entry-validator.js +9 -3
  261. package/dist/version.d.ts +1 -0
  262. package/dist/version.js +3 -0
  263. package/dist/worker/canopy-state.d.ts +45 -0
  264. package/dist/worker/canopy-state.js +75 -0
  265. package/dist/worker/cms-worker.d.ts +8 -3
  266. package/dist/worker/cms-worker.js +38 -2
  267. package/dist/worker/git-sync.d.ts +30 -12
  268. package/dist/worker/git-sync.js +253 -58
  269. package/dist/worker/history-rewrite.d.ts +1 -1
  270. package/dist/worker/history-rewrite.js +1 -1
  271. package/dist/worker/provisioned-workspace.d.ts +35 -0
  272. package/dist/worker/provisioned-workspace.js +50 -0
  273. package/dist/worker/rebase.d.ts +1 -1
  274. package/dist/worker/rebase.js +95 -23
  275. package/dist/worker/remote-git-maintenance.d.ts +3 -0
  276. package/dist/worker/remote-git-maintenance.js +11 -0
  277. package/dist/worker/sparse-cone.d.ts +20 -0
  278. package/dist/worker/sparse-cone.js +132 -0
  279. package/dist/worker/task-runner.js +99 -14
  280. package/dist/worker/worker-context.d.ts +6 -1
  281. package/package.json +8 -2
@@ -6,13 +6,7 @@
6
6
  * (no react-easy-crop import) so it's trivially unit-testable and reusable
7
7
  * without pulling in the cropper UI library.
8
8
  */
9
- import { isValidCropRect } from '../../assets/transform-directives.js';
10
- /** Matches transform-directives.ts's CROP_PRECISION so a crop rect round-trips through the URL directive unchanged. */
11
- const CROP_PRECISION = 4;
12
- function roundTo(value, decimals) {
13
- const factor = 10 ** decimals;
14
- return Math.round(value * factor) / factor;
15
- }
9
+ import { isValidCropRect, roundCropRect } from '../../assets/transform-directives.js';
16
10
  function clamp01(value) {
17
11
  if (!Number.isFinite(value))
18
12
  return 0;
@@ -20,26 +14,21 @@ function clamp01(value) {
20
14
  }
21
15
  /**
22
16
  * Convert react-easy-crop's `onCropComplete(croppedArea, croppedAreaPixels)`
23
- * percentage area into a normalized `CropRect`, rounded to 4 decimals and
24
- * clamped to fit within bounds. Rounding can push `x+w`/`y+h` a hair over 1
25
- * (e.g. x=0.6667, w=0.3334 -> 1.0001) - shrink the extent to the remaining
26
- * space rather than reject a rect that was valid before rounding.
17
+ * percentage area into a normalized `CropRect`, clamped to [0,1] and then
18
+ * rounded by `roundCropRect` — the same rounding the transform URL's canonical
19
+ * form applies, so the stored crop is already canonical.
27
20
  *
28
21
  * Returns `null` if the input can't be coerced into a valid rect (e.g. a
29
22
  * zero-area selection).
30
23
  */
31
24
  export function cropAreaPercentToRect(area) {
32
- const x = roundTo(clamp01(area.x / 100), CROP_PRECISION);
33
- const y = roundTo(clamp01(area.y / 100), CROP_PRECISION);
34
- let w = roundTo(clamp01(area.width / 100), CROP_PRECISION);
35
- let h = roundTo(clamp01(area.height / 100), CROP_PRECISION);
36
- if (x + w > 1)
37
- w = roundTo(1 - x, CROP_PRECISION);
38
- if (y + h > 1)
39
- h = roundTo(1 - y, CROP_PRECISION);
40
- if (!isValidCropRect(x, y, w, h))
41
- return null;
42
- return { x, y, w, h };
25
+ const rect = roundCropRect({
26
+ x: clamp01(area.x / 100),
27
+ y: clamp01(area.y / 100),
28
+ w: clamp01(area.width / 100),
29
+ h: clamp01(area.height / 100),
30
+ });
31
+ return isValidCropRect(rect.x, rect.y, rect.w, rect.h) ? rect : null;
43
32
  }
44
33
  /**
45
34
  * Inverse of `cropAreaPercentToRect` - seeds react-easy-crop's
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The URL the editor displays a content image at. A src in canopycms's own `/assets` space is put
3
+ * behind `baseUrl` (the authenticated raw route, see `AssetContext`); any other src - a site's own
4
+ * static file, an off-site URL, a `data:` URI - is shown exactly as written, because the raw route
5
+ * serves only the asset store.
6
+ */
7
+ export declare function editorImageSrc(src: string, baseUrl: string): string;
@@ -0,0 +1,12 @@
1
+ import { assetUrl } from '../../assets/asset-url.js';
2
+ import { ASSET_PREFIXES } from '../../assets/asset-prefixes.js';
3
+ const ASSET_SPACE = `/${ASSET_PREFIXES.public}/`;
4
+ /**
5
+ * The URL the editor displays a content image at. A src in canopycms's own `/assets` space is put
6
+ * behind `baseUrl` (the authenticated raw route, see `AssetContext`); any other src - a site's own
7
+ * static file, an off-site URL, a `data:` URI - is shown exactly as written, because the raw route
8
+ * serves only the asset store.
9
+ */
10
+ export function editorImageSrc(src, baseUrl) {
11
+ return src.startsWith(ASSET_SPACE) ? assetUrl({ src }, { baseUrl }) : src;
12
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The asset prefix a framing editor hands its live preview, held for `assets/asset-url.ts`. Its
3
+ * only writer outside tests is `usePreviewData`, on a trusted draft message; the read is gated on `window` too,
4
+ * because `assetUrl` also runs in server renders and static builds. Imports nothing.
5
+ */
6
+ export declare function setPreviewAssetBase(base: string | undefined): void;
7
+ /** The stored prefix in a browser; always `undefined` on the server. */
8
+ export declare function getPreviewAssetBase(): string | undefined;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * The asset prefix a framing editor hands its live preview, held for `assets/asset-url.ts`. Its
3
+ * only writer outside tests is `usePreviewData`, on a trusted draft message; the read is gated on `window` too,
4
+ * because `assetUrl` also runs in server renders and static builds. Imports nothing.
5
+ */
6
+ let previewAssetBase;
7
+ export function setPreviewAssetBase(base) {
8
+ previewAssetBase = base;
9
+ }
10
+ /** The stored prefix in a browser; always `undefined` on the server. */
11
+ export function getPreviewAssetBase() {
12
+ return typeof window === 'undefined' ? undefined : previewAssetBase;
13
+ }
@@ -1,6 +1,4 @@
1
- import type { CSSProperties } from 'react';
2
1
  import { type CanopyPathSegment } from './canopy-path.js';
3
- export declare const __CANOPY_PREVIEW_CLIENT__ = true;
4
2
  export declare const CANOPY_PREVIEW_MESSAGE = "canopycms:draft:update";
5
3
  export declare const CANOPY_PREVIEW_FOCUS = "canopycms:preview:focus";
6
4
  export declare const CANOPY_PREVIEW_HIGHLIGHT = "canopycms:preview:highlight";
@@ -11,6 +9,8 @@ export interface DraftUpdateMessage {
11
9
  path: string;
12
10
  data?: unknown;
13
11
  isLoading?: unknown;
12
+ /** The editor's authenticated asset route; see `PreviewFrame`'s `assetBase`. */
13
+ assetBase?: unknown;
14
14
  }
15
15
  /**
16
16
  * Resolve a (possibly relative) URL to an origin for postMessage targeting.
@@ -18,6 +18,13 @@ export interface DraftUpdateMessage {
18
18
  * need no configuration.
19
19
  */
20
20
  export declare const resolveMessageOrigin: (url?: string) => string;
21
+ /**
22
+ * Opaque origins (sandboxed iframes, data: URLs) serialize to the string 'null':
23
+ * never trust them for inbound messages, and never post to them — every window in
24
+ * a sandboxed embed shares that string, so it identifies nothing, and postMessage
25
+ * throws on a literal 'null' targetOrigin. '' covers the SSR fallback.
26
+ */
27
+ export declare const isOpaqueOrigin: (origin: string) => boolean;
21
28
  /**
22
29
  * True only for messages from the embedding editor window with the expected origin.
23
30
  *
@@ -28,7 +35,6 @@ export declare const resolveMessageOrigin: (url?: string) => string;
28
35
  * expected editor origin (same-origin unless `editorOrigin` is configured).
29
36
  */
30
37
  export declare const isTrustedEditorMessage: (event: MessageEvent, editorOrigin?: string) => boolean;
31
- export declare const sendDraftUpdate: (iframe: HTMLIFrameElement | null, message: DraftUpdateMessage, targetOrigin?: string) => void;
32
38
  export interface PreviewFocusMessage {
33
39
  type: typeof CANOPY_PREVIEW_FOCUS;
34
40
  entryPath: string;
@@ -88,21 +94,3 @@ export declare const usePreviewHighlight: (opts?: {
88
94
  export declare const usePreviewFocusEmitter: (entryPath: string, opts?: {
89
95
  editorOrigin?: string;
90
96
  }) => void;
91
- /**
92
- * Lightweight iframe wrapper to keep the preview in sync with form state.
93
- * It posts the latest draft data to the iframe after load and when data changes.
94
- */
95
- export declare const PreviewFrame: ({ src, path, data, isLoading, className, style, highlightEnabled, onPreviewError, }: {
96
- src: string;
97
- path: string;
98
- data?: unknown;
99
- isLoading?: unknown;
100
- className?: string;
101
- style?: CSSProperties;
102
- highlightEnabled?: boolean;
103
- /** Called when the preview reports a draft compile/render error; null clears it. */
104
- onPreviewError?: (error: {
105
- message: string;
106
- fieldPath?: string;
107
- } | null) => void;
108
- }) => import("react/jsx-runtime").JSX.Element;
@@ -1,13 +1,23 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useRef, useState } from 'react';
2
+ /**
3
+ * The preview bridge's protocol and its host-page side: the hooks a framed page runs. Served to
4
+ * hosts through `canopycms/preview`, so everything it reaches stays free of CSS and editor UI
5
+ * (`pnpm lint:bundle`); the editor's side is `PreviewFrame.tsx`.
6
+ */
7
+ import { useCallback, useEffect, useState } from 'react';
8
+ import { toSameOriginPath } from '../utils/url-prefix.js';
4
9
  import { formatCanopyPath } from './canopy-path.js';
5
- export const __CANOPY_PREVIEW_CLIENT__ = true;
10
+ import { setPreviewAssetBase } from './preview-asset-base.js';
11
+ import { isSamePreviewPath } from './preview-path.js';
6
12
  export const CANOPY_PREVIEW_MESSAGE = 'canopycms:draft:update';
7
13
  export const CANOPY_PREVIEW_FOCUS = 'canopycms:preview:focus';
8
14
  export const CANOPY_PREVIEW_HIGHLIGHT = 'canopycms:preview:highlight';
9
15
  export const CANOPY_PREVIEW_READY = 'canopycms:preview:ready';
10
16
  export const CANOPY_PREVIEW_ERROR = 'canopycms:preview:error';
17
+ /** `assetBase` from a draft, only if it is a same-origin path: nothing else may steer `<img>`s. */
18
+ const readAssetBase = (value) => typeof value === 'string' && value.startsWith('/') && toSameOriginPath(value) === value
19
+ ? value
20
+ : undefined;
11
21
  /**
12
22
  * Resolve a (possibly relative) URL to an origin for postMessage targeting.
13
23
  * Falls back to the page's own origin, so same-origin editor/preview setups
@@ -31,7 +41,7 @@ export const resolveMessageOrigin = (url) => {
31
41
  * a sandboxed embed shares that string, so it identifies nothing, and postMessage
32
42
  * throws on a literal 'null' targetOrigin. '' covers the SSR fallback.
33
43
  */
34
- const isOpaqueOrigin = (origin) => origin === 'null' || origin === '';
44
+ export const isOpaqueOrigin = (origin) => origin === 'null' || origin === '';
35
45
  /**
36
46
  * True only for messages from the embedding editor window with the expected origin.
37
47
  *
@@ -49,14 +59,6 @@ export const isTrustedEditorMessage = (event, editorOrigin) => {
49
59
  return false;
50
60
  return event.origin === expected && event.source === window.parent;
51
61
  };
52
- export const sendDraftUpdate = (iframe, message, targetOrigin) => {
53
- if (!iframe?.contentWindow)
54
- return;
55
- const target = targetOrigin ?? resolveMessageOrigin(iframe.src);
56
- if (isOpaqueOrigin(target))
57
- return;
58
- iframe.contentWindow.postMessage(message, target);
59
- };
60
62
  const resolvePreviewPath = (explicit) => {
61
63
  if (explicit)
62
64
  return explicit;
@@ -116,8 +118,12 @@ export const usePreviewData = (path, initialData, opts) => {
116
118
  if (!isTrustedEditorMessage(event, editorOrigin))
117
119
  return;
118
120
  const msg = event.data;
119
- if (!msg || msg.type !== CANOPY_PREVIEW_MESSAGE || msg.path !== path)
121
+ if (!msg || msg.type !== CANOPY_PREVIEW_MESSAGE)
122
+ return;
123
+ if (typeof msg.path !== 'string' || !isSamePreviewPath(msg.path, path))
120
124
  return;
125
+ // Before setData, so the render the draft triggers already reads it.
126
+ setPreviewAssetBase(readAssetBase(msg.assetBase));
121
127
  setData(msg.data);
122
128
  if (msg.isLoading !== undefined) {
123
129
  setIsLoading(msg.isLoading);
@@ -204,129 +210,3 @@ export const usePreviewFocusEmitter = (entryPath, opts) => {
204
210
  return () => document.removeEventListener('click', handleClick);
205
211
  }, [entryPath, editorOrigin]);
206
212
  };
207
- /**
208
- * Lightweight iframe wrapper to keep the preview in sync with form state.
209
- * It posts the latest draft data to the iframe after load and when data changes.
210
- */
211
- export const PreviewFrame = ({ src, path, data, isLoading, className, style, highlightEnabled, onPreviewError, }) => {
212
- const iframeRef = useRef(null);
213
- // Pin the preview origin from the src prop: outbound messages target it (never '*'),
214
- // and inbound messages must come from it. An iframe that navigates cross-origin
215
- // silently stops participating in the bridge.
216
- const previewOrigin = resolveMessageOrigin(src);
217
- // Show progress bar while waiting for the preview's ready handshake.
218
- const [syncPending, setSyncPending] = useState(data !== undefined);
219
- // Reset when navigating to a different entry (src change = new iframe page load).
220
- const [prevSrc, setPrevSrc] = useState(src);
221
- if (src !== prevSrc) {
222
- setPrevSrc(src);
223
- setSyncPending(data !== undefined);
224
- }
225
- // Inject the progress bar keyframe animation once per page.
226
- useEffect(() => {
227
- const styleId = 'canopycms-preview-sync-style';
228
- if (!document.getElementById(styleId)) {
229
- const el = document.createElement('style');
230
- el.id = styleId;
231
- el.textContent = `@keyframes canopy-preview-sync { 0% { transform: translateX(-100%); } 100% { transform: translateX(250%); } }`;
232
- document.head.appendChild(el);
233
- }
234
- }, []);
235
- const post = () => {
236
- if (data === undefined)
237
- return;
238
- sendDraftUpdate(iframeRef.current, {
239
- type: CANOPY_PREVIEW_MESSAGE,
240
- path,
241
- data,
242
- isLoading,
243
- }, previewOrigin);
244
- };
245
- const postHighlight = () => {
246
- if (!iframeRef.current?.contentWindow)
247
- return;
248
- if (isOpaqueOrigin(previewOrigin))
249
- return;
250
- const msg = {
251
- type: CANOPY_PREVIEW_HIGHLIGHT,
252
- enabled: Boolean(highlightEnabled),
253
- };
254
- iframeRef.current.contentWindow.postMessage(msg, previewOrigin);
255
- };
256
- // Keep refs pointing at the latest closures so the message handler below never goes stale.
257
- const postRef = useRef(post);
258
- const postHighlightRef = useRef(postHighlight);
259
- const onPreviewErrorRef = useRef(onPreviewError);
260
- useEffect(() => {
261
- postRef.current = post;
262
- postHighlightRef.current = postHighlight;
263
- onPreviewErrorRef.current = onPreviewError;
264
- });
265
- useEffect(() => {
266
- post();
267
- // eslint-disable-next-line react-hooks/exhaustive-deps -- post is tracked via ref
268
- }, [data, isLoading]);
269
- useEffect(() => {
270
- postHighlight();
271
- // eslint-disable-next-line react-hooks/exhaustive-deps -- postHighlight is tracked via ref
272
- }, [highlightEnabled]);
273
- // When the preview page's React effects have run and its message listener is ready,
274
- // it sends CANOPY_PREVIEW_READY. We respond with the current data so the preview
275
- // receives the draft even if it wasn't ready when onLoad fired.
276
- useEffect(() => {
277
- const handleMessage = (event) => {
278
- if (!iframeRef.current || event.source !== iframeRef.current.contentWindow)
279
- return;
280
- // Source check alone is insufficient if the iframe navigated cross-origin.
281
- if (isOpaqueOrigin(previewOrigin) || event.origin !== previewOrigin)
282
- return;
283
- const type = event.data?.type;
284
- if (type === CANOPY_PREVIEW_READY) {
285
- postRef.current();
286
- postHighlightRef.current();
287
- setSyncPending(false);
288
- }
289
- else if (type === CANOPY_PREVIEW_ERROR) {
290
- const msg = event.data;
291
- // Shape-check the payload: an adopter passing an Error object (instead of
292
- // err.message) must not crash the editor when rendered as a React child.
293
- if (msg.message != null && typeof msg.message !== 'string')
294
- return;
295
- const fieldPath = typeof msg.fieldPath === 'string' ? msg.fieldPath : undefined;
296
- onPreviewErrorRef.current?.(msg.message == null
297
- ? null
298
- : { message: msg.message, ...(fieldPath ? { fieldPath } : {}) });
299
- }
300
- };
301
- window.addEventListener('message', handleMessage);
302
- return () => window.removeEventListener('message', handleMessage);
303
- }, [previewOrigin]);
304
- return (_jsxs("div", { className: className, style: { position: 'relative', overflow: 'hidden', ...style }, children: [syncPending && (_jsx("div", { style: {
305
- position: 'absolute',
306
- top: 0,
307
- left: 0,
308
- right: 0,
309
- height: 3,
310
- zIndex: 1,
311
- overflow: 'hidden',
312
- }, children: _jsx("div", { style: {
313
- position: 'absolute',
314
- top: 0,
315
- left: 0,
316
- height: '100%',
317
- width: '40%',
318
- background: 'var(--mantine-color-blue-filled, #228be6)',
319
- borderRadius: '0 2px 2px 0',
320
- animation: 'canopy-preview-sync 1.5s ease-in-out infinite',
321
- } }) })), _jsx("iframe", { ref: iframeRef, src: src, style: {
322
- display: 'block',
323
- position: 'absolute',
324
- inset: 0,
325
- width: '100%',
326
- height: '100%',
327
- border: 'none',
328
- }, onLoad: () => {
329
- post();
330
- postHighlight();
331
- } })] }));
332
- };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Reduces a preview URL to what identifies the page, its path and query, with no origin,
3
+ * fragment or trailing slash. The editor names a page by the `src` it built and the framed page
4
+ * by its own location. An absolute `src`, or a host redirect that adds or drops a trailing slash,
5
+ * spells the same page two ways, and the bridge must still match them.
6
+ * @internal Exported for tests.
7
+ */
8
+ export declare const normalizePreviewPath: (url: string) => string;
9
+ /** Whether two preview URLs name the same page; see `normalizePreviewPath`. */
10
+ export declare const isSamePreviewPath: (a: string, b: string) => boolean;
@@ -0,0 +1,20 @@
1
+ import { stripTrailingSlashes } from '../utils/url-prefix.js';
2
+ /**
3
+ * Reduces a preview URL to what identifies the page, its path and query, with no origin,
4
+ * fragment or trailing slash. The editor names a page by the `src` it built and the framed page
5
+ * by its own location. An absolute `src`, or a host redirect that adds or drops a trailing slash,
6
+ * spells the same page two ways, and the bridge must still match them.
7
+ * @internal Exported for tests.
8
+ */
9
+ export const normalizePreviewPath = (url) => {
10
+ let parsed;
11
+ try {
12
+ parsed = new URL(url, 'http://preview.invalid');
13
+ }
14
+ catch {
15
+ return url;
16
+ }
17
+ return `${stripTrailingSlashes(parsed.pathname) || '/'}${parsed.search}`;
18
+ };
19
+ /** Whether two preview URLs name the same page; see `normalizePreviewPath`. */
20
+ export const isSamePreviewPath = (a, b) => normalizePreviewPath(a) === normalizePreviewPath(b);
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useMemo } from 'react';
4
- import { DEFAULT_THEME, MantineProvider, createTheme, } from '@mantine/core';
4
+ import { DEFAULT_THEME, MantineProvider, getDefaultZIndex, createTheme, } from '@mantine/core';
5
5
  import { Notifications } from '@mantine/notifications';
6
6
  import { ModalsProvider } from '@mantine/modals';
7
7
  import { generateColors } from '@mantine/colors-generator';
@@ -62,5 +62,5 @@ export const createCanopyTheme = (options) => {
62
62
  };
63
63
  export const CanopyCMSProvider = ({ children, colors, colorScheme = 'light', themeOverride, withNotifications = true, }) => {
64
64
  const theme = useMemo(() => createCanopyTheme({ colors, themeOverride }), [colors, themeOverride]);
65
- return (_jsx(MantineProvider, { theme: theme, defaultColorScheme: colorScheme, children: _jsxs(ModalsProvider, { children: [withNotifications ? _jsx(Notifications, { position: "bottom-left" }) : null, children] }) }));
65
+ return (_jsx(MantineProvider, { theme: theme, defaultColorScheme: colorScheme, children: _jsxs(ModalsProvider, { modalProps: { zIndex: getDefaultZIndex('popover') }, children: [withNotifications ? _jsx(Notifications, { position: "bottom-left" }) : null, children] }) }));
66
66
  };
@@ -12,10 +12,10 @@ import type { EntrySchemaRegistry } from './schema/types.js';
12
12
  * interface to maintain. Keyed any other way, the derived map is keyed by that
13
13
  * string instead and will not plug into `TEntryTypes`.
14
14
  *
15
- * Rejected at call time: an empty registry; a schema that is not a non-empty
16
- * `EntrySchema` array; more than one `isTitle` per schema, or one on a non-string
17
- * field or inside a list; more than one `isBody`, or one on a field that is not
18
- * markdown/mdx or is named one of `RESOLVED_REFERENCE_KEYS`. This is also the one
15
+ * Rejected at call time: an empty registry; a schema that is not a non-empty `EntrySchema`
16
+ * array; more than one `isTitle` per schema, or one on a non-string field or inside a list;
17
+ * more than one `isBody`, or one on a field that is not markdown/mdx or is named one of
18
+ * `RESOLVED_REFERENCE_KEYS`; a top-level or inline-group field named `unavailable`. This is also the one
19
19
  * place the shared field-shape checks run: select fields must have options,
20
20
  * reference fields must have `collections` or `entryTypes`, no inline groups
21
21
  * inside object/block fields, no field-name collisions after group flattening.
@@ -1,8 +1,8 @@
1
1
  import { countTitleFields, findInvalidTitleFields, findTitleFieldsInLists, } from './utils/title-field.js';
2
2
  import { countBodyFields, findInvalidBodyFields, findReservedBodyFieldName, } from './utils/body-field.js';
3
- import { RESOLVED_REFERENCE_KEYS } from './entry-schema.js';
3
+ import { RESOLVED_REFERENCE_KEYS, RESTRICTED_REFERENCE_MARKER } from './entry-schema.js';
4
4
  import { flattenGroupFields } from './utils/flatten-group-fields.js';
5
- import { ensureSelectFieldsHaveOptions, ensureReferenceFieldsHaveScope, ensureNoFlattenedFieldNameCollisions, ensureNoGroupsInsideComplexFields, } from './config/validation.js';
5
+ import { ensureSelectFieldsHaveOptions, ensureReferenceFieldsHaveScope, ensureItemTitleFieldsExist, ensureNoFlattenedFieldNameCollisions, ensureNoGroupsInsideComplexFields, } from './config/validation.js';
6
6
  /** Look up a field's type by dotted path (e.g., "meta.order").
7
7
  * Groups are transparent — their children are searched at the same path level. */
8
8
  function findFieldType(fields, dottedPath) {
@@ -35,10 +35,10 @@ function findFieldType(fields, dottedPath) {
35
35
  * interface to maintain. Keyed any other way, the derived map is keyed by that
36
36
  * string instead and will not plug into `TEntryTypes`.
37
37
  *
38
- * Rejected at call time: an empty registry; a schema that is not a non-empty
39
- * `EntrySchema` array; more than one `isTitle` per schema, or one on a non-string
40
- * field or inside a list; more than one `isBody`, or one on a field that is not
41
- * markdown/mdx or is named one of `RESOLVED_REFERENCE_KEYS`. This is also the one
38
+ * Rejected at call time: an empty registry; a schema that is not a non-empty `EntrySchema`
39
+ * array; more than one `isTitle` per schema, or one on a non-string field or inside a list;
40
+ * more than one `isBody`, or one on a field that is not markdown/mdx or is named one of
41
+ * `RESOLVED_REFERENCE_KEYS`; a top-level or inline-group field named `unavailable`. This is also the one
42
42
  * place the shared field-shape checks run: select fields must have options,
43
43
  * reference fields must have `collections` or `entryTypes`, no inline groups
44
44
  * inside object/block fields, no field-name collisions after group flattening.
@@ -92,8 +92,12 @@ export function createEntrySchemaRegistry(registry) {
92
92
  if (reservedBodyField) {
93
93
  throw new Error(`Entry schema registry entry "${key}": field "${reservedBodyField}" has isBody: true but "${reservedBodyField}" is reserved — reference resolution sets ${RESOLVED_REFERENCE_KEYS.map((k) => `"${k}"`).join(', ')} on a resolved reference, and a body field with one of those names would overwrite it. Rename the field (the body's field name is yours to choose; only these four are reserved).`);
94
94
  }
95
+ if (flattenGroupFields(schema).some((f) => f.name === RESTRICTED_REFERENCE_MARKER)) {
96
+ throw new Error(`Entry schema registry entry "${key}": field "${RESTRICTED_REFERENCE_MARKER}" is reserved — a resolved reference carries "${RESTRICTED_REFERENCE_MARKER}: true" only when the reader may not read its target, so a field with that name cannot be delivered on a reference to this entry type. Rename the field.`);
97
+ }
95
98
  ensureSelectFieldsHaveOptions(schema);
96
99
  ensureReferenceFieldsHaveScope(schema);
100
+ ensureItemTitleFieldsExist(schema);
97
101
  ensureNoGroupsInsideComplexFields(schema);
98
102
  ensureNoFlattenedFieldNameCollisions(schema, `entry schema "${key}"`);
99
103
  }
@@ -81,9 +81,6 @@ export interface ResolvedReferenceMeta {
81
81
  * Assemble a resolved reference: the target's own data, then its body if the field asked to
82
82
  * embed it, then the reserved metadata.
83
83
  *
84
- * Exists so the two places that construct one — the server resolver in content-store.ts and
85
- * the editor's live-preview endpoint in api/resolve-references.ts — cannot drift.
86
- *
87
84
  * The ordering is the contract, not a detail. Metadata LAST means a target that models `id` as
88
85
  * a content field cannot shadow the real content ID, which the write boundary recovers from
89
86
  * `value.id` (`referenceValueId`) — a shadowed id makes a re-save persist the wrong value and
@@ -95,6 +92,30 @@ export declare function buildResolvedReference(data: Record<string, unknown>, me
95
92
  fieldName: string;
96
93
  value: string | undefined;
97
94
  }): Record<string, unknown>;
95
+ /**
96
+ * The key that marks a `RestrictedReference`. Reserved as an entry field name, at the top level
97
+ * of any schema, because any schema can be a reference target.
98
+ */
99
+ export declare const RESTRICTED_REFERENCE_MARKER = "unavailable";
100
+ /**
101
+ * What a reference resolves to when the reader may not read its target: enough to render a
102
+ * link (a title and the URL, which a denied reader can follow to a sign-in page) and nothing
103
+ * else of the target's data.
104
+ *
105
+ * `unavailable: true` is the marker a renderer branches on, so a denied target never arrives
106
+ * looking like a full one with its fields `undefined`; `reason` says why, leaving room for
107
+ * other reasons a target cannot be shown. A reference the reader may see in full never carries
108
+ * `unavailable`. `id` is always present, so a save of the referring entry writes the reference
109
+ * back unchanged (`normalizeReferenceValues` recovers it from `value.id`).
110
+ */
111
+ export type RestrictedReference = Simplify<ResolvedReferenceMeta & {
112
+ /** The target's display title, by `resolveEntryTitle`'s fallback chain. */
113
+ title: string;
114
+ unavailable: true;
115
+ reason: 'restricted';
116
+ }>;
117
+ /** Assemble a {@link RestrictedReference}. Takes no target data, so none can leak into it. */
118
+ export declare function buildRestrictedReference(meta: ResolvedReferenceMeta, title: string): RestrictedReference;
98
119
  /**
99
120
  * Recursively flatten inline groups (type: 'group') out of a field tuple: they contribute
100
121
  * no key to the content shape, so their children merge into the parent level and
@@ -205,7 +226,9 @@ type FieldValue<F extends InferableField> = F extends {
205
226
  } ? ScalarValue<F, SelectValue<F>> : F extends {
206
227
  type: 'reference';
207
228
  resolvedSchema: infer S;
208
- } ? ScalarValue<F, (InferContentShape<Extract<S, readonly InferableField[]>> & ResolvedReferenceMeta) | null> : F extends {
229
+ } ? ScalarValue<F, (InferContentShape<Extract<S, readonly InferableField[]>> & ResolvedReferenceMeta & {
230
+ unavailable?: undefined;
231
+ }) | RestrictedReference | null> : F extends {
209
232
  type: 'reference';
210
233
  } ? ScalarValue<F, string | null> : F extends {
211
234
  type: 'image';
@@ -14,9 +14,6 @@ export const RESOLVED_REFERENCE_KEYS = ['id', 'slug', 'collection', 'urlPath'];
14
14
  * Assemble a resolved reference: the target's own data, then its body if the field asked to
15
15
  * embed it, then the reserved metadata.
16
16
  *
17
- * Exists so the two places that construct one — the server resolver in content-store.ts and
18
- * the editor's live-preview endpoint in api/resolve-references.ts — cannot drift.
19
- *
20
17
  * The ordering is the contract, not a detail. Metadata LAST means a target that models `id` as
21
18
  * a content field cannot shadow the real content ID, which the write boundary recovers from
22
19
  * `value.id` (`referenceValueId`) — a shadowed id makes a re-save persist the wrong value and
@@ -35,8 +32,28 @@ export function buildResolvedReference(data, meta, body) {
35
32
  resolved.slug = meta.slug;
36
33
  resolved.collection = meta.collection;
37
34
  resolved.urlPath = meta.urlPath;
35
+ // Backstop for the registry's rejection of a field with this name (entry-schema-registry.ts):
36
+ // a full reference carrying the marker would read as one the reader may not see.
37
+ delete resolved[RESTRICTED_REFERENCE_MARKER];
38
38
  return resolved;
39
39
  }
40
+ /**
41
+ * The key that marks a `RestrictedReference`. Reserved as an entry field name, at the top level
42
+ * of any schema, because any schema can be a reference target.
43
+ */
44
+ export const RESTRICTED_REFERENCE_MARKER = 'unavailable';
45
+ /** Assemble a {@link RestrictedReference}. Takes no target data, so none can leak into it. */
46
+ export function buildRestrictedReference(meta, title) {
47
+ return {
48
+ id: meta.id,
49
+ slug: meta.slug,
50
+ collection: meta.collection,
51
+ urlPath: meta.urlPath,
52
+ title,
53
+ unavailable: true,
54
+ reason: 'restricted',
55
+ };
56
+ }
40
57
  /** Define an entry schema's field array with literal inference, without sprinkling `as const`. */
41
58
  export const defineEntrySchema = (fields) => fields;
42
59
  /**