canopycms 0.0.67 → 0.0.68-int.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/README.md +21 -14
  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 +111 -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 +10 -1
  23. package/dist/api/github-sync.js +20 -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/index.d.ts +1 -1
  36. package/dist/assets/materialize.d.ts +122 -0
  37. package/dist/assets/materialize.js +305 -0
  38. package/dist/assets/store-local.d.ts +4 -0
  39. package/dist/assets/store-local.js +11 -0
  40. package/dist/assets/store-s3.d.ts +10 -1
  41. package/dist/assets/store-s3.js +55 -3
  42. package/dist/assets/transform-directives.d.ts +52 -13
  43. package/dist/assets/transform-directives.js +87 -27
  44. package/dist/assets/transform.d.ts +3 -3
  45. package/dist/assets/transform.js +13 -8
  46. package/dist/assets/types.d.ts +14 -1
  47. package/dist/auth/file-based-auth-cache.js +7 -8
  48. package/dist/authorization/content.d.ts +5 -4
  49. package/dist/authorization/content.js +5 -5
  50. package/dist/authorization/path.d.ts +11 -6
  51. package/dist/authorization/path.js +11 -6
  52. package/dist/authorization/types.d.ts +2 -2
  53. package/dist/branch-health.d.ts +7 -1
  54. package/dist/branch-health.js +8 -3
  55. package/dist/branch-metadata.d.ts +5 -5
  56. package/dist/branch-metadata.js +44 -36
  57. package/dist/branch-provisioning.d.ts +167 -0
  58. package/dist/branch-provisioning.js +501 -0
  59. package/dist/branch-schema-cache.d.ts +5 -3
  60. package/dist/branch-schema-cache.js +29 -11
  61. package/dist/branch-sparse.d.ts +23 -0
  62. package/dist/branch-sparse.js +118 -0
  63. package/dist/branch-workspace.d.ts +42 -3
  64. package/dist/branch-workspace.js +240 -94
  65. package/dist/build/asset-refs.d.ts +93 -0
  66. package/dist/build/asset-refs.js +251 -0
  67. package/dist/build-identity.d.ts +3 -0
  68. package/dist/build-identity.js +13 -0
  69. package/dist/cli/asset-refs.d.ts +30 -0
  70. package/dist/cli/asset-refs.js +168 -0
  71. package/dist/cli/cli.d.ts +2 -1
  72. package/dist/cli/cli.js +4843 -1870
  73. package/dist/cli/generate-ai-content.js +1584 -444
  74. package/dist/cli/github-app-manifest.d.ts +4 -3
  75. package/dist/cli/github-app-manifest.js +4 -3
  76. package/dist/cli/init.js +30 -27
  77. package/dist/cli/migrate.js +12 -8
  78. package/dist/cli/sync.js +83 -37
  79. package/dist/cli/template-files/Dockerfile.cms.template +8 -0
  80. package/dist/cli/template-files/cdk-app.ts.template +4 -0
  81. package/dist/cli/template-files/cms-stack.ts.template +23 -12
  82. package/dist/cli/template-files/deploy-cms.yml.template +2 -0
  83. package/dist/config/helpers.js +1 -2
  84. package/dist/config/schemas/config.d.ts +19 -32
  85. package/dist/config/schemas/config.js +9 -11
  86. package/dist/config/schemas/field.js +1 -0
  87. package/dist/config/schemas/media.d.ts +4 -13
  88. package/dist/config/schemas/media.js +5 -8
  89. package/dist/config/schemas/url.d.ts +4 -20
  90. package/dist/config/schemas/url.js +6 -22
  91. package/dist/config/types.d.ts +30 -20
  92. package/dist/config/validation.d.ts +6 -0
  93. package/dist/config/validation.js +37 -0
  94. package/dist/content-listing.d.ts +12 -12
  95. package/dist/content-listing.js +10 -6
  96. package/dist/content-reader.d.ts +11 -2
  97. package/dist/content-reader.js +22 -14
  98. package/dist/content-store.d.ts +37 -9
  99. package/dist/content-store.js +70 -43
  100. package/dist/content-tree.d.ts +1 -1
  101. package/dist/content-tree.js +2 -2
  102. package/dist/context.d.ts +27 -9
  103. package/dist/context.js +154 -70
  104. package/dist/editor/BranchManager.d.ts +7 -1
  105. package/dist/editor/BranchManager.js +29 -14
  106. package/dist/editor/CanopyEditor.d.ts +1 -1
  107. package/dist/editor/CanopyEditor.js +2 -6
  108. package/dist/editor/Editor.d.ts +4 -3
  109. package/dist/editor/Editor.js +24 -20
  110. package/dist/editor/FormRenderer.js +34 -18
  111. package/dist/editor/admin/SystemHealthPanel.js +47 -7
  112. package/dist/editor/components/BranchesDrawer.d.ts +9 -0
  113. package/dist/editor/components/BranchesDrawer.js +4 -0
  114. package/dist/editor/components/NoEditPermissionNotice.d.ts +10 -0
  115. package/dist/editor/components/NoEditPermissionNotice.js +17 -0
  116. package/dist/editor/context/AssetContext.d.ts +18 -35
  117. package/dist/editor/context/AssetContext.js +20 -29
  118. package/dist/editor/context/index.d.ts +1 -1
  119. package/dist/editor/context/index.js +1 -1
  120. package/dist/editor/editor-config.d.ts +1 -2
  121. package/dist/editor/editor-config.js +0 -23
  122. package/dist/editor/editor-utils.d.ts +24 -21
  123. package/dist/editor/editor-utils.js +75 -62
  124. package/dist/editor/fields/BlockField.d.ts +1 -0
  125. package/dist/editor/fields/BlockField.js +6 -4
  126. package/dist/editor/fields/CodeField.d.ts +1 -0
  127. package/dist/editor/fields/CodeField.js +2 -2
  128. package/dist/editor/fields/DateTimeField.d.ts +1 -0
  129. package/dist/editor/fields/DateTimeField.js +2 -2
  130. package/dist/editor/fields/FieldDescription.d.ts +16 -0
  131. package/dist/editor/fields/FieldDescription.js +11 -0
  132. package/dist/editor/fields/ImageField.d.ts +1 -0
  133. package/dist/editor/fields/ImageField.js +3 -2
  134. package/dist/editor/fields/InlineGroupField.js +4 -1
  135. package/dist/editor/fields/MarkdownField.d.ts +1 -0
  136. package/dist/editor/fields/MarkdownField.js +115 -20
  137. package/dist/editor/fields/NumberField.d.ts +1 -0
  138. package/dist/editor/fields/NumberField.js +2 -2
  139. package/dist/editor/fields/NumberListField.d.ts +1 -0
  140. package/dist/editor/fields/NumberListField.js +2 -2
  141. package/dist/editor/fields/ObjectField.d.ts +1 -0
  142. package/dist/editor/fields/ObjectField.js +5 -2
  143. package/dist/editor/fields/ReferenceField.d.ts +1 -0
  144. package/dist/editor/fields/ReferenceField.js +5 -4
  145. package/dist/editor/fields/SelectField.d.ts +1 -0
  146. package/dist/editor/fields/SelectField.js +2 -2
  147. package/dist/editor/fields/StringListField.d.ts +1 -0
  148. package/dist/editor/fields/StringListField.js +2 -2
  149. package/dist/editor/fields/TextField.d.ts +1 -0
  150. package/dist/editor/fields/TextField.js +2 -2
  151. package/dist/editor/fields/ToggleField.d.ts +1 -0
  152. package/dist/editor/fields/ToggleField.js +7 -4
  153. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
  154. package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -2
  155. package/dist/editor/fields/mdx-jsx-support.d.ts +1 -0
  156. package/dist/editor/fields/mdx-jsx-support.js +137 -0
  157. package/dist/editor/hooks/create-branch-request.d.ts +20 -0
  158. package/dist/editor/hooks/create-branch-request.js +52 -0
  159. package/dist/editor/hooks/useBranchActions.d.ts +13 -2
  160. package/dist/editor/hooks/useBranchActions.js +48 -17
  161. package/dist/editor/hooks/useBranchManager.d.ts +11 -0
  162. package/dist/editor/hooks/useBranchManager.js +80 -2
  163. package/dist/editor/hooks/useBranchesData.d.ts +6 -0
  164. package/dist/editor/hooks/useBranchesData.js +9 -3
  165. package/dist/editor/hooks/useCommentSystem.js +25 -6
  166. package/dist/editor/hooks/useDraftManager.d.ts +16 -1
  167. package/dist/editor/hooks/useDraftManager.js +193 -36
  168. package/dist/editor/hooks/useEntriesData.js +3 -3
  169. package/dist/editor/hooks/useEntryManager.d.ts +2 -1
  170. package/dist/editor/hooks/useEntryManager.js +34 -19
  171. package/dist/editor/media/AssetCard.d.ts +1 -1
  172. package/dist/editor/media/crop-math.d.ts +3 -4
  173. package/dist/editor/media/crop-math.js +11 -22
  174. package/dist/editor/media/editor-image-src.d.ts +7 -0
  175. package/dist/editor/media/editor-image-src.js +12 -0
  176. package/dist/editor/preview-asset-base.d.ts +8 -0
  177. package/dist/editor/preview-asset-base.js +13 -0
  178. package/dist/editor/preview-bridge.d.ts +8 -1
  179. package/dist/editor/preview-bridge.js +15 -2
  180. package/dist/editor/preview-path.d.ts +10 -0
  181. package/dist/editor/preview-path.js +20 -0
  182. package/dist/editor/theme.js +2 -2
  183. package/dist/entry-schema-registry.d.ts +4 -4
  184. package/dist/entry-schema-registry.js +10 -6
  185. package/dist/entry-schema.d.ts +27 -4
  186. package/dist/entry-schema.js +20 -3
  187. package/dist/git-manager.d.ts +173 -6
  188. package/dist/git-manager.js +594 -76
  189. package/dist/github-service.d.ts +8 -0
  190. package/dist/github-service.js +5 -1
  191. package/dist/http/handler.js +37 -13
  192. package/dist/http/index.d.ts +2 -0
  193. package/dist/http/index.js +2 -0
  194. package/dist/http/router.d.ts +2 -0
  195. package/dist/http/router.js +2 -1
  196. package/dist/http/types.d.ts +2 -0
  197. package/dist/http/worker-not-ready.d.ts +10 -0
  198. package/dist/http/worker-not-ready.js +24 -0
  199. package/dist/operating-mode/client-unsafe-strategy.js +0 -6
  200. package/dist/operating-mode/types.d.ts +1 -4
  201. package/dist/paths/branch-name.d.ts +5 -0
  202. package/dist/paths/branch-name.js +9 -0
  203. package/dist/paths/branch.d.ts +6 -2
  204. package/dist/paths/branch.js +16 -10
  205. package/dist/paths/index.d.ts +3 -3
  206. package/dist/paths/index.js +3 -3
  207. package/dist/paths/normalize.d.ts +7 -0
  208. package/dist/paths/normalize.js +9 -0
  209. package/dist/paths/validation.js +0 -2
  210. package/dist/reference-resolver.d.ts +5 -21
  211. package/dist/reference-resolver.js +9 -44
  212. package/dist/resolve-canopy-user.js +2 -1
  213. package/dist/schema/schema-store.d.ts +54 -31
  214. package/dist/schema/schema-store.js +77 -31
  215. package/dist/server.d.ts +24 -9
  216. package/dist/server.js +22 -8
  217. package/dist/services.d.ts +18 -6
  218. package/dist/services.js +85 -70
  219. package/dist/settings-workspace.d.ts +23 -3
  220. package/dist/settings-workspace.js +197 -45
  221. package/dist/static/seo.d.ts +2 -14
  222. package/dist/static/seo.js +2 -25
  223. package/dist/submission-attribution.d.ts +73 -0
  224. package/dist/submission-attribution.js +221 -0
  225. package/dist/sync-core.d.ts +12 -1
  226. package/dist/sync-core.js +31 -13
  227. package/dist/task-queue/worker-status.d.ts +8 -0
  228. package/dist/task-queue/worker-status.js +17 -0
  229. package/dist/types.d.ts +31 -2
  230. package/dist/utils/content-write-lock.d.ts +18 -7
  231. package/dist/utils/content-write-lock.js +18 -9
  232. package/dist/utils/debug.d.ts +8 -0
  233. package/dist/utils/debug.js +10 -2
  234. package/dist/utils/git.d.ts +32 -0
  235. package/dist/utils/git.js +42 -0
  236. package/dist/utils/occ-json-write.js +10 -2
  237. package/dist/utils/provision-log.d.ts +25 -0
  238. package/dist/utils/provision-log.js +49 -0
  239. package/dist/utils/provisioning-lock.d.ts +35 -5
  240. package/dist/utils/provisioning-lock.js +73 -12
  241. package/dist/utils/request-timing.d.ts +25 -0
  242. package/dist/utils/request-timing.js +101 -0
  243. package/dist/utils/sanitize-href.d.ts +8 -22
  244. package/dist/utils/sanitize-href.js +11 -28
  245. package/dist/utils/url-prefix.d.ts +30 -0
  246. package/dist/utils/url-prefix.js +61 -0
  247. package/dist/validation/entry-validator.js +9 -3
  248. package/dist/version.d.ts +1 -0
  249. package/dist/version.js +3 -0
  250. package/dist/worker/canopy-state.d.ts +45 -0
  251. package/dist/worker/canopy-state.js +75 -0
  252. package/dist/worker/cms-worker.d.ts +8 -3
  253. package/dist/worker/cms-worker.js +38 -2
  254. package/dist/worker/git-sync.d.ts +30 -12
  255. package/dist/worker/git-sync.js +253 -58
  256. package/dist/worker/history-rewrite.d.ts +1 -1
  257. package/dist/worker/history-rewrite.js +1 -1
  258. package/dist/worker/provisioned-workspace.d.ts +35 -0
  259. package/dist/worker/provisioned-workspace.js +50 -0
  260. package/dist/worker/rebase.d.ts +1 -1
  261. package/dist/worker/rebase.js +95 -23
  262. package/dist/worker/remote-git-maintenance.d.ts +3 -0
  263. package/dist/worker/remote-git-maintenance.js +11 -0
  264. package/dist/worker/sparse-cone.d.ts +20 -0
  265. package/dist/worker/sparse-cone.js +132 -0
  266. package/dist/worker/task-runner.js +39 -4
  267. package/dist/worker/worker-context.d.ts +6 -1
  268. package/package.json +2 -1
@@ -1,13 +1,20 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useCallback, useEffect, useRef, useState } from 'react';
4
+ import { toSameOriginPath } from '../utils/url-prefix.js';
4
5
  import { formatCanopyPath } from './canopy-path.js';
6
+ import { setPreviewAssetBase } from './preview-asset-base.js';
7
+ import { isSamePreviewPath } from './preview-path.js';
5
8
  export const __CANOPY_PREVIEW_CLIENT__ = true;
6
9
  export const CANOPY_PREVIEW_MESSAGE = 'canopycms:draft:update';
7
10
  export const CANOPY_PREVIEW_FOCUS = 'canopycms:preview:focus';
8
11
  export const CANOPY_PREVIEW_HIGHLIGHT = 'canopycms:preview:highlight';
9
12
  export const CANOPY_PREVIEW_READY = 'canopycms:preview:ready';
10
13
  export const CANOPY_PREVIEW_ERROR = 'canopycms:preview:error';
14
+ /** `assetBase` from a draft, only if it is a same-origin path: nothing else may steer `<img>`s. */
15
+ const readAssetBase = (value) => typeof value === 'string' && value.startsWith('/') && toSameOriginPath(value) === value
16
+ ? value
17
+ : undefined;
11
18
  /**
12
19
  * Resolve a (possibly relative) URL to an origin for postMessage targeting.
13
20
  * Falls back to the page's own origin, so same-origin editor/preview setups
@@ -116,8 +123,12 @@ export const usePreviewData = (path, initialData, opts) => {
116
123
  if (!isTrustedEditorMessage(event, editorOrigin))
117
124
  return;
118
125
  const msg = event.data;
119
- if (!msg || msg.type !== CANOPY_PREVIEW_MESSAGE || msg.path !== path)
126
+ if (!msg || msg.type !== CANOPY_PREVIEW_MESSAGE)
120
127
  return;
128
+ if (typeof msg.path !== 'string' || !isSamePreviewPath(msg.path, path))
129
+ return;
130
+ // Before setData, so the render the draft triggers already reads it.
131
+ setPreviewAssetBase(readAssetBase(msg.assetBase));
121
132
  setData(msg.data);
122
133
  if (msg.isLoading !== undefined) {
123
134
  setIsLoading(msg.isLoading);
@@ -208,7 +219,7 @@ export const usePreviewFocusEmitter = (entryPath, opts) => {
208
219
  * Lightweight iframe wrapper to keep the preview in sync with form state.
209
220
  * It posts the latest draft data to the iframe after load and when data changes.
210
221
  */
211
- export const PreviewFrame = ({ src, path, data, isLoading, className, style, highlightEnabled, onPreviewError, }) => {
222
+ export const PreviewFrame = ({ src, path, data, isLoading, className, style, highlightEnabled, onPreviewError, assetBase, }) => {
212
223
  const iframeRef = useRef(null);
213
224
  // Pin the preview origin from the src prop: outbound messages target it (never '*'),
214
225
  // and inbound messages must come from it. An iframe that navigates cross-origin
@@ -235,11 +246,13 @@ export const PreviewFrame = ({ src, path, data, isLoading, className, style, hig
235
246
  const post = () => {
236
247
  if (data === undefined)
237
248
  return;
249
+ const sameOrigin = previewOrigin === window.location.origin;
238
250
  sendDraftUpdate(iframeRef.current, {
239
251
  type: CANOPY_PREVIEW_MESSAGE,
240
252
  path,
241
253
  data,
242
254
  isLoading,
255
+ ...(assetBase !== undefined && sameOrigin ? { assetBase } : {}),
243
256
  }, previewOrigin);
244
257
  };
245
258
  const postHighlight = () => {
@@ -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
  /**
@@ -1,7 +1,64 @@
1
1
  import { type SimpleGitOptions, type StatusResult } from 'simple-git';
2
2
  import type { OperatingMode } from './operating-mode/index.js';
3
+ import type { ProvisionLog } from './utils/provision-log.js';
3
4
  export declare function gitChildEnv(overrides: Record<string, string>): Record<string, string>;
4
5
  export declare function gitNetworkChildEnv(): Record<string, string>;
6
+ /**
7
+ * Settings written into every bare `remote.git` CanopyCMS creates: the worker's (prod) and the
8
+ * simulated remote (dev). A push runs `receive-pack` with remote.git's own config and none of the
9
+ * pusher's: git unsets `GIT_CONFIG_PARAMETERS` before spawning it for a local-path push. Unset, a
10
+ * push starts auto-housekeeping there (detached, or inside a Lambda that is frozen mid-run) that
11
+ * repacks while another process clones, and pushes under the default `unpackLimit` (100) leave
12
+ * loose objects that every local clone copies one NFS round trip at a time.
13
+ *
14
+ * Never an `extensions.*` key: once a repo is at `repositoryformatversion` 1, a git that does not
15
+ * know the key refuses it, and Lambda runs git 2.39.
16
+ * @internal Exported for tests.
17
+ */
18
+ export declare const REMOTE_GIT_CONFIG: ReadonlyArray<readonly [key: string, value: string]>;
19
+ /** Write each of {@link REMOTE_GIT_CONFIG} into the bare repo `gitDir` only where it differs. */
20
+ export declare function ensureRemoteGitConfig(gitDir: string): Promise<void>;
21
+ interface BareRemoteObjectCounts {
22
+ loose: number;
23
+ packs: number;
24
+ }
25
+ export type BareRemoteRepackResult = {
26
+ repacked: false;
27
+ before: BareRemoteObjectCounts;
28
+ } | {
29
+ repacked: true;
30
+ before: BareRemoteObjectCounts;
31
+ after: BareRemoteObjectCounts;
32
+ ms: number;
33
+ };
34
+ /**
35
+ * Repack the bare repo `gitDir` once it holds more than {@link REMOTE_GIT_MAX_LOOSE_OBJECTS}
36
+ * loose objects or {@link REMOTE_GIT_MAX_PACKS} packs, then pack its refs. Under
37
+ * {@link REMOTE_GIT_CONFIG} every push adds a pack, and nothing else ever compacts them.
38
+ *
39
+ * Safe beside concurrent pushes and clones, which is why it is `--cruft` with no expiry:
40
+ * unreachable objects move into a cruft pack rather than being dropped, so a push that read an
41
+ * old object before the repack still finds it, and a repack only deletes packs it listed when it
42
+ * began. A clone that hardlinked a pack keeps that inode when the repack unlinks the remote's
43
+ * name for it, and packs are never modified in place. `repack -d` also removes the loose objects
44
+ * it packed and their emptied fan-out directories.
45
+ */
46
+ export declare function repackBareRemoteIfNeeded(gitDir: string): Promise<BareRemoteRepackResult>;
47
+ /**
48
+ * Config every workspace clone writes into its own `.git/config`: the managed
49
+ * marker that ensureRemote's guard checks, and a fallback author for internal
50
+ * commits such as the orphan settings init (ensureAuthor() sets the real one
51
+ * before user-facing commits).
52
+ */
53
+ export declare function managedWorkspaceConfig(gitBotAuthorName: string, gitBotAuthorEmail: string): Record<string, string>;
54
+ export interface CloneRepoOptions {
55
+ /** The clone's remote name; git's default is `origin`. */
56
+ remoteName?: string;
57
+ /** Leave the working tree unpopulated for the caller to check out. */
58
+ noCheckout?: boolean;
59
+ /** Written into the new repo's own config by the clone (`git clone -c`). */
60
+ config?: Record<string, string>;
61
+ }
5
62
  /**
6
63
  * Remote-tracking namespace that `syncGit()`'s GitHub fetch lands refs in
7
64
  * (`+refs/heads/*:${GITHUB_TRACKING_REF_PREFIX}*`), instead of writing
@@ -69,6 +126,17 @@ export declare class GitRemoteRefMissingError extends Error {
69
126
  */
70
127
  gitError?: unknown | undefined);
71
128
  }
129
+ /**
130
+ * No git remote is configured and the one the strategy auto-detects does not
131
+ * exist yet: in prod, the EC2 worker creates `{workspaceRoot}/remote.git` on
132
+ * its first boot, so until then every request that needs a workspace fails
133
+ * here. Usually transient, unlike a missing remote in a mode with nothing to wait for.
134
+ * The HTTP layer maps it to a 503 (`http/worker-not-ready.ts`).
135
+ */
136
+ export declare class RemoteNotReadyError extends Error {
137
+ readonly expectedRemotePath: string;
138
+ constructor(expectedRemotePath: string);
139
+ }
72
140
  export interface ResolveRemoteUrlOptions {
73
141
  mode: OperatingMode;
74
142
  remoteUrl?: string;
@@ -108,6 +176,8 @@ export interface InitializeWorkspaceOptions {
108
176
  * only; settings workspaces commit by explicit path and don't need it.
109
177
  */
110
178
  gitExcludePattern?: string;
179
+ /** Times each provisioning step; no step lines are printed without one. */
180
+ provisionLog?: ProvisionLog;
111
181
  }
112
182
  export declare class GitManager {
113
183
  private readonly git;
@@ -116,7 +186,13 @@ export declare class GitManager {
116
186
  private readonly remote;
117
187
  private readonly skipIndexMarker;
118
188
  constructor(options: GitManagerOptions, gitOptions?: Partial<SimpleGitOptions>);
119
- static cloneRepo(remoteUrl: string, targetPath: string, baseBranch?: string): Promise<void>;
189
+ /**
190
+ * Single-branch clone of `baseBranch`, retried once: a clone can lose a race
191
+ * with the worker's repack of `remote.git` deleting a pack it had just
192
+ * listed. Before the retry it clears only what it created itself, so a
193
+ * target that held files beforehand is never touched.
194
+ */
195
+ static cloneRepo(remoteUrl: string, targetPath: string, baseBranch?: string, options?: CloneRepoOptions): Promise<void>;
120
196
  /**
121
197
  * Initializes a local bare git repository to simulate a remote for dev mode,
122
198
  * seeded with the current state of baseBranch. Idempotent: an existing remote
@@ -173,7 +249,7 @@ export declare class GitManager {
173
249
  * deleteBranchHandler: a head left in `remote.git` forever makes the
174
250
  * create -> publish -> squash-merge -> delete -> reuse-the-name cycle reject
175
251
  * the reused branch's first publish non-fast-forward against the stale head
176
- * (`GitManager.push()` pushes `branch:branch`, and a squash-merged old tip is
252
+ * (`GitManager.push()` pushes the branch to the same name, and a squash-merged old tip is
177
253
  * not an ancestor of the new branch), and a retried submit then skips the
178
254
  * local push on a clean tree and enqueues the worker push of the STALE head,
179
255
  * resurrecting the deleted branch's content on GitHub as an apparent success.
@@ -223,6 +299,8 @@ export declare class GitManager {
223
299
  * @param source - Where `url` came from, for the thrown error message only.
224
300
  */
225
301
  private static assertRemoteUrlAllowedInMode;
302
+ /** The not-ready error for a mode whose remote is auto-detected, or `undefined` for other modes. */
303
+ private static remoteNotReadyError;
226
304
  /**
227
305
  * Resolves the remote URL for git operations following the priority:
228
306
  * 1. Explicit remoteUrl parameter
@@ -239,6 +317,17 @@ export declare class GitManager {
239
317
  * @returns Remote URL or undefined if no remote is needed
240
318
  */
241
319
  static resolveRemoteUrl(options: ResolveRemoteUrlOptions): Promise<string | undefined>;
320
+ /**
321
+ * {@link resolveRemoteUrl} for a workspace about to be cloned, which needs a
322
+ * remote: none resolving is the worker-not-ready error in a mode whose
323
+ * remote the worker creates, and a configuration error otherwise.
324
+ */
325
+ static resolveCloneRemoteUrl(options: ResolveRemoteUrlOptions): Promise<string>;
326
+ /**
327
+ * {@link cloneRepo} for a workspace, failing with one message that names the
328
+ * workspace, the remote and the base branch, which git's own error mixes.
329
+ */
330
+ static cloneWorkspace(remoteUrl: string, workspacePath: string, baseBranch: string, options: CloneRepoOptions): Promise<void>;
242
331
  /**
243
332
  * Check whether a git repository is already initialized at `workspacePath`.
244
333
  *
@@ -275,6 +364,14 @@ export declare class GitManager {
275
364
  */
276
365
  private invalidateContentIndexes;
277
366
  checkoutBranch(branch: string): Promise<void>;
367
+ /**
368
+ * The one checkout of a `--no-checkout` clone, which has the base branch
369
+ * and its remote-tracking ref and nothing else, so neither the branch
370
+ * listing nor the fetch that {@link checkoutBranch} starts with is needed.
371
+ * The missing index makes git populate the whole tree, even for the base
372
+ * branch HEAD already names.
373
+ */
374
+ checkoutFreshClone(branch: string): Promise<void>;
278
375
  private checkoutBranchInner;
279
376
  pullBase(): Promise<void>;
280
377
  private pullBaseInner;
@@ -282,9 +379,22 @@ export declare class GitManager {
282
379
  private pullCurrentBranchInner;
283
380
  rebaseOntoBase(): Promise<void>;
284
381
  private rebaseOntoBaseInner;
382
+ /** `--sparse` stages a named path outside a sparse clone's cone (branch-sparse.ts). */
285
383
  add(files: string | string[]): Promise<void>;
384
+ /**
385
+ * Restrict the working tree to `dirs` in cone mode, which always keeps the root-level files.
386
+ * Runs before a `--no-checkout` clone's one checkout, which invalidates the content caches.
387
+ */
388
+ setSparseCone(dirs: readonly string[]): Promise<void>;
389
+ /** Stage every working-tree change except canopycms's own state. See {@link stageAllExceptCanopyState}. */
390
+ addAllExceptCanopyState(): Promise<void>;
286
391
  commit(message: string): Promise<void>;
287
392
  push(branch?: string): Promise<void>;
393
+ /**
394
+ * The checked-out branch's plain name. `rev-parse --abbrev-ref HEAD` prints
395
+ * `heads/<name>` when a same-named tag exists, which is not a branch name.
396
+ */
397
+ private currentBranchName;
288
398
  /**
289
399
  * Whether the local branch has commits the remote mirror doesn't -- i.e.
290
400
  * whether push() would move the remote ref forward.
@@ -303,10 +413,19 @@ export declare class GitManager {
303
413
  * ("couldn't find remote ref"); that counts as "ahead", not an error.
304
414
  */
305
415
  hasUnpushedCommits(branch?: string): Promise<boolean>;
416
+ /**
417
+ * Repo-relative paths that differ between HEAD and its merge base with the
418
+ * base branch: everything the branch changes, across all of its commits.
419
+ * Diffs against the just-fetched base tip pinned to a SHA — the pullBaseInner
420
+ * constraint.
421
+ */
422
+ listChangedPathsSinceBase(): Promise<string[]>;
306
423
  ensureAuthor(author: {
307
424
  name: string;
308
425
  email: string;
309
426
  }): Promise<void>;
427
+ /** Writes each key into this repo's own config only where it differs. */
428
+ ensureLocalConfig(values: Record<string, string>): Promise<void>;
310
429
  ensureRemote(remoteUrl: string): Promise<void>;
311
430
  hasUncommittedChanges(): Promise<boolean>;
312
431
  getUncommittedFiles(): Promise<string[]>;
@@ -317,11 +436,59 @@ export declare class GitManager {
317
436
  */
318
437
  ensureGitExclude(pattern: string): Promise<void>;
319
438
  /**
320
- * Create an orphan branch (no shared history) for settings, so
321
- * deployment-specific settings never pollute content history. It holds only
322
- * settings files committed by explicit path (permissions.json, groups.json at
323
- * the workspace root).
439
+ * Put the workspace on the settings branch: an orphan (no shared history),
440
+ * so deployment-specific settings never pollute content history. It holds
441
+ * only settings files committed by explicit path (permissions.json,
442
+ * groups.json at the workspace root).
443
+ *
444
+ * The remote is the settings branch's durable copy, and the workspace clone
445
+ * is `--single-branch` at the base branch, so the remote is asked first:
446
+ * - no local branch: check out the remote's, or create an empty orphan only
447
+ * when the remote has none. An unreadable remote throws
448
+ * {@link SettingsRemoteUnreadableError} rather than guess "none".
449
+ * - local branch present: a history unrelated to the remote's is repaired
450
+ * onto the remote's when the local branch is still its empty initial
451
+ * commit with a clean working tree, and otherwise throws
452
+ * {@link SettingsBranchDivergedError}; local commits are never discarded.
453
+ * An unreadable remote throws only for that empty branch, since one that
454
+ * holds settings can serve them and the next save surfaces the remote error.
455
+ * Either way a remote branch holding content is never adopted:
456
+ * {@link SettingsBranchHasContentHistoryError}.
324
457
  */
325
458
  createOrphanSettingsBranch(branchName: string, initialFiles: Record<string, string>): Promise<void>;
326
459
  private createOrphanSettingsBranchInner;
460
+ /**
461
+ * Compares the checked-out local settings branch with the remote's tip.
462
+ * Related histories are left for the next settings pull to reconcile; only
463
+ * an unrelated one is acted on here.
464
+ */
465
+ private reconcileLocalSettingsBranch;
466
+ /**
467
+ * The remote's tip for `branch`, or `undefined` when the remote has no such
468
+ * branch. A remote that cannot be read throws: `ls-remote` exits 0 with no
469
+ * output only for a reachable remote without the ref.
470
+ */
471
+ private remoteBranchTip;
472
+ /**
473
+ * Fetch `branch` from the remote and return the fetched commit, pinned as
474
+ * pullBaseInner explains. The full ref: a bare name resolves to a
475
+ * same-named tag first.
476
+ */
477
+ private fetchBranchTip;
478
+ /**
479
+ * Refuse to adopt a remote settings branch that holds content: its tip has a file other than
480
+ * the settings files, or its roots include the base branch's (a settings branch is an orphan).
481
+ * Neither alone suffices: a base can have a root of its own, and a content tree can be pruned.
482
+ * The base comes from the remote when the clone lacks it (it was cloned at an earlier base).
483
+ */
484
+ private assertNotContentHistory;
485
+ private baseBranchRev;
486
+ private rootCommits;
487
+ /**
488
+ * Whether `branch` is a single parentless commit with an empty tree, as
489
+ * orphan creation leaves it. Full refs throughout: a bare name resolves to a
490
+ * same-named tag first, and clones fetch tags.
491
+ */
492
+ private isEmptyInitialBranch;
327
493
  }
494
+ export {};