canopycms 0.0.68-int.98 → 0.1.0

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 (688) hide show
  1. package/CHANGELOG.md +2131 -0
  2. package/README.md +47 -63
  3. package/dist/ai/generate.d.ts +5 -0
  4. package/dist/ai/generate.js +244 -115
  5. package/dist/ai/index.d.ts +1 -1
  6. package/dist/ai/json-to-markdown.d.ts +21 -1
  7. package/dist/ai/json-to-markdown.js +120 -57
  8. package/dist/ai/references.d.ts +72 -0
  9. package/dist/ai/references.js +223 -0
  10. package/dist/ai/resolve-branch.js +9 -2
  11. package/dist/ai/to-plain-text.d.ts +17 -8
  12. package/dist/ai/to-plain-text.js +18 -15
  13. package/dist/ai/types.d.ts +84 -3
  14. package/dist/api/admin-branch-health.d.ts +24 -1
  15. package/dist/api/admin-branch-health.js +52 -20
  16. package/dist/api/admin.d.ts +23 -5
  17. package/dist/api/admin.js +36 -1
  18. package/dist/api/assets.js +88 -78
  19. package/dist/api/branch-create-window.d.ts +12 -0
  20. package/dist/api/branch-create-window.js +13 -0
  21. package/dist/api/branch-merge.d.ts +1 -1
  22. package/dist/api/branch-merge.js +1 -1
  23. package/dist/api/branch-review.d.ts +3 -3
  24. package/dist/api/branch-review.js +16 -7
  25. package/dist/api/branch-status.d.ts +16 -6
  26. package/dist/api/branch-status.js +57 -12
  27. package/dist/api/branch-withdraw.d.ts +2 -2
  28. package/dist/api/branch-withdraw.js +8 -3
  29. package/dist/api/branch.d.ts +25 -12
  30. package/dist/api/branch.js +164 -103
  31. package/dist/api/client.d.ts +78 -58
  32. package/dist/api/client.js +150 -117
  33. package/dist/api/comments.d.ts +12 -3
  34. package/dist/api/comments.js +118 -9
  35. package/dist/api/content.d.ts +9 -9
  36. package/dist/api/content.js +131 -22
  37. package/dist/api/entries-constants.d.ts +14 -3
  38. package/dist/api/entries-constants.js +14 -3
  39. package/dist/api/entries.d.ts +40 -2
  40. package/dist/api/entries.js +102 -6
  41. package/dist/api/github-sync.d.ts +19 -0
  42. package/dist/api/github-sync.js +50 -3
  43. package/dist/api/groups.d.ts +4 -4
  44. package/dist/api/groups.js +2 -1
  45. package/dist/api/guards.js +23 -3
  46. package/dist/api/index.d.ts +3 -1
  47. package/dist/api/index.js +1 -0
  48. package/dist/api/permissions.d.ts +21 -2
  49. package/dist/api/permissions.js +50 -3
  50. package/dist/api/request-deadline.d.ts +59 -0
  51. package/dist/api/request-deadline.js +73 -0
  52. package/dist/api/resolve-references.js +8 -7
  53. package/dist/api/route-builder.d.ts +14 -0
  54. package/dist/api/route-builder.js +14 -1
  55. package/dist/api/schema.d.ts +11 -9
  56. package/dist/api/schema.js +2 -0
  57. package/dist/api/settings-helpers.d.ts +5 -1
  58. package/dist/api/settings-helpers.js +10 -2
  59. package/dist/api/types.d.ts +18 -0
  60. package/dist/api/users-constants.d.ts +6 -0
  61. package/dist/api/users-constants.js +6 -0
  62. package/dist/assets/asset-url.d.ts +28 -16
  63. package/dist/assets/asset-url.js +51 -25
  64. package/dist/assets/finalize.js +3 -0
  65. package/dist/assets/index.d.ts +1 -1
  66. package/dist/assets/materialize.d.ts +153 -0
  67. package/dist/assets/materialize.js +402 -0
  68. package/dist/assets/store-local.d.ts +19 -5
  69. package/dist/assets/store-local.js +69 -10
  70. package/dist/assets/store-s3.d.ts +38 -7
  71. package/dist/assets/store-s3.js +128 -22
  72. package/dist/assets/transform-directives.d.ts +52 -13
  73. package/dist/assets/transform-directives.js +87 -27
  74. package/dist/assets/transform.d.ts +3 -3
  75. package/dist/assets/transform.js +13 -8
  76. package/dist/assets/types.d.ts +31 -4
  77. package/dist/auth/cache.d.ts +1 -1
  78. package/dist/auth/cache.js +1 -1
  79. package/dist/auth/caching-auth-plugin.d.ts +2 -0
  80. package/dist/auth/caching-auth-plugin.js +12 -2
  81. package/dist/auth/file-based-auth-cache.d.ts +8 -0
  82. package/dist/auth/file-based-auth-cache.js +18 -8
  83. package/dist/auth/person-name.d.ts +5 -0
  84. package/dist/auth/person-name.js +8 -0
  85. package/dist/auth/plugin.d.ts +10 -1
  86. package/dist/auth/types.d.ts +2 -0
  87. package/dist/auth/user-metadata-lookup.d.ts +21 -0
  88. package/dist/auth/user-metadata-lookup.js +116 -0
  89. package/dist/authorization/branch.d.ts +1 -1
  90. package/dist/authorization/branch.js +12 -13
  91. package/dist/authorization/groups/schema.d.ts +4 -4
  92. package/dist/authorization/helpers.d.ts +8 -0
  93. package/dist/authorization/helpers.js +8 -0
  94. package/dist/authorization/index.d.ts +1 -1
  95. package/dist/authorization/index.js +1 -1
  96. package/dist/authorization/protected-branch.d.ts +5 -4
  97. package/dist/authorization/protected-branch.js +7 -5
  98. package/dist/branch-health.d.ts +35 -8
  99. package/dist/branch-health.js +54 -18
  100. package/dist/branch-metadata-error.d.ts +27 -0
  101. package/dist/branch-metadata-error.js +23 -0
  102. package/dist/branch-metadata-file.d.ts +205 -24
  103. package/dist/branch-metadata-file.js +126 -25
  104. package/dist/branch-metadata.d.ts +45 -6
  105. package/dist/branch-metadata.js +142 -50
  106. package/dist/branch-provisioning.d.ts +167 -0
  107. package/dist/branch-provisioning.js +501 -0
  108. package/dist/branch-registry.d.ts +8 -0
  109. package/dist/branch-registry.js +61 -9
  110. package/dist/branch-schema-cache.d.ts +29 -12
  111. package/dist/branch-schema-cache.js +64 -21
  112. package/dist/branch-sparse.d.ts +23 -0
  113. package/dist/branch-sparse.js +118 -0
  114. package/dist/branch-workspace.d.ts +42 -3
  115. package/dist/branch-workspace.js +239 -100
  116. package/dist/build/asset-refs.d.ts +94 -0
  117. package/dist/build/asset-refs.js +267 -0
  118. package/dist/build/generate-ai-content.js +5 -0
  119. package/dist/build/inline-flight.d.ts +37 -0
  120. package/dist/build/inline-flight.js +142 -0
  121. package/dist/cli/asset-refs.d.ts +52 -0
  122. package/dist/cli/asset-refs.js +194 -0
  123. package/dist/cli/cli.d.ts +15 -1
  124. package/dist/cli/cli.js +8543 -3193
  125. package/dist/cli/configured-asset-store.d.ts +12 -0
  126. package/dist/cli/configured-asset-store.js +46 -0
  127. package/dist/cli/generate-ai-content.js +4244 -1072
  128. package/dist/cli/init.js +217 -126
  129. package/dist/cli/project-detect.d.ts +2 -0
  130. package/dist/cli/project-detect.js +2 -0
  131. package/dist/cli/template-files/Dockerfile.cms.template +13 -5
  132. package/dist/cli/template-files/cms-stack.ts.template +16 -13
  133. package/dist/client.d.ts +1 -2
  134. package/dist/client.js +1 -2
  135. package/dist/comment-store.d.ts +25 -0
  136. package/dist/comment-store.js +43 -8
  137. package/dist/config/flatten.js +2 -0
  138. package/dist/config/helpers.js +2 -2
  139. package/dist/config/index.d.ts +1 -1
  140. package/dist/config/schemas/config.d.ts +12 -29
  141. package/dist/config/schemas/config.js +9 -12
  142. package/dist/config/schemas/field.d.ts +3 -0
  143. package/dist/config/schemas/field.js +58 -4
  144. package/dist/config/schemas/media.d.ts +4 -13
  145. package/dist/config/schemas/media.js +5 -8
  146. package/dist/config/schemas/url.d.ts +2 -20
  147. package/dist/config/schemas/url.js +2 -25
  148. package/dist/config/types.d.ts +116 -22
  149. package/dist/config/types.js +3 -9
  150. package/dist/config/validation.d.ts +25 -4
  151. package/dist/config/validation.js +116 -3
  152. package/dist/content-id-index.js +2 -2
  153. package/dist/content-index-generation.d.ts +1 -1
  154. package/dist/content-index-generation.js +1 -1
  155. package/dist/content-listing.d.ts +10 -1
  156. package/dist/content-listing.js +22 -5
  157. package/dist/content-reader.d.ts +9 -0
  158. package/dist/content-reader.js +16 -11
  159. package/dist/content-store.d.ts +75 -12
  160. package/dist/content-store.js +249 -86
  161. package/dist/content-tree.js +2 -2
  162. package/dist/context.js +5 -2
  163. package/dist/dangling-reference-log.d.ts +17 -0
  164. package/dist/dangling-reference-log.js +29 -0
  165. package/dist/dev-content-watcher.d.ts +1 -1
  166. package/dist/dev-content-watcher.js +1 -1
  167. package/dist/editor/BranchManager.d.ts +23 -7
  168. package/dist/editor/BranchManager.js +119 -54
  169. package/dist/editor/CanopyEditor.d.ts +1 -1
  170. package/dist/editor/CanopyEditor.js +1 -2
  171. package/dist/editor/CommentsPanel.d.ts +14 -0
  172. package/dist/editor/CommentsPanel.js +68 -43
  173. package/dist/editor/Editor.d.ts +11 -5
  174. package/dist/editor/Editor.js +891 -218
  175. package/dist/editor/EditorAuthGate.d.ts +7 -1
  176. package/dist/editor/EditorAuthGate.js +41 -9
  177. package/dist/editor/EntryNavigator.d.ts +22 -0
  178. package/dist/editor/EntryNavigator.js +353 -105
  179. package/dist/editor/FormHeader.d.ts +25 -0
  180. package/dist/editor/FormHeader.js +82 -0
  181. package/dist/editor/FormRenderer.d.ts +27 -3
  182. package/dist/editor/FormRenderer.js +171 -76
  183. package/dist/editor/PreviewFrame.d.ts +40 -0
  184. package/dist/editor/PreviewFrame.js +215 -0
  185. package/dist/editor/PreviewPane.d.ts +47 -0
  186. package/dist/editor/PreviewPane.js +147 -0
  187. package/dist/editor/PreviewToolbarTools.d.ts +22 -0
  188. package/dist/editor/PreviewToolbarTools.js +73 -0
  189. package/dist/editor/admin/SystemHealthPanel.d.ts +2 -1
  190. package/dist/editor/admin/SystemHealthPanel.js +163 -33
  191. package/dist/editor/admin/useSystemHealth.d.ts +18 -0
  192. package/dist/editor/admin/useSystemHealth.js +70 -1
  193. package/dist/editor/branch-status.d.ts +17 -0
  194. package/dist/editor/branch-status.js +29 -0
  195. package/dist/editor/canopy-path.d.ts +9 -6
  196. package/dist/editor/canopy-path.js +8 -11
  197. package/dist/editor/client-reference-resolver.d.ts +43 -16
  198. package/dist/editor/client-reference-resolver.js +155 -64
  199. package/dist/editor/comments/BranchComments.d.ts +1 -1
  200. package/dist/editor/comments/BranchComments.js +2 -2
  201. package/dist/editor/comments/CloseRefusedNotice.d.ts +4 -0
  202. package/dist/editor/comments/CloseRefusedNotice.js +7 -0
  203. package/dist/editor/comments/CommentThreadsPopover.d.ts +47 -0
  204. package/dist/editor/comments/CommentThreadsPopover.js +274 -0
  205. package/dist/editor/comments/CommentWriteAlert.d.ts +11 -0
  206. package/dist/editor/comments/CommentWriteAlert.js +13 -0
  207. package/dist/editor/comments/EntryComments.d.ts +10 -4
  208. package/dist/editor/comments/EntryComments.js +14 -17
  209. package/dist/editor/comments/FieldWrapper.d.ts +18 -4
  210. package/dist/editor/comments/FieldWrapper.js +62 -28
  211. package/dist/editor/comments/InlineCommentThread.d.ts +9 -2
  212. package/dist/editor/comments/InlineCommentThread.js +11 -47
  213. package/dist/editor/comments/ThreadCarousel.d.ts +1 -1
  214. package/dist/editor/comments/ThreadCarousel.js +18 -21
  215. package/dist/editor/comments/ThreadReplyControls.d.ts +28 -0
  216. package/dist/editor/comments/ThreadReplyControls.js +92 -0
  217. package/dist/editor/comments/field-comment-context.d.ts +14 -0
  218. package/dist/editor/comments/field-comment-context.js +14 -0
  219. package/dist/editor/comments/form-comments.d.ts +48 -0
  220. package/dist/editor/comments/form-comments.js +178 -0
  221. package/dist/editor/comments/item-anchor-copy.d.ts +6 -0
  222. package/dist/editor/comments/item-anchor-copy.js +6 -0
  223. package/dist/editor/comments/move-focus-if-unmoved.d.ts +6 -0
  224. package/dist/editor/comments/move-focus-if-unmoved.js +11 -0
  225. package/dist/editor/comments/popover-placement.d.ts +18 -0
  226. package/dist/editor/comments/popover-placement.js +58 -0
  227. package/dist/editor/comments/use-tracked-writes.d.ts +16 -0
  228. package/dist/editor/comments/use-tracked-writes.js +53 -0
  229. package/dist/editor/components/BranchAccessList.d.ts +15 -0
  230. package/dist/editor/components/BranchAccessList.js +52 -0
  231. package/dist/editor/components/BranchLockBanner.d.ts +22 -0
  232. package/dist/editor/components/BranchLockBanner.js +39 -0
  233. package/dist/editor/components/BranchPill.d.ts +32 -0
  234. package/dist/editor/components/BranchPill.js +33 -0
  235. package/dist/editor/components/BranchesDrawer.d.ts +11 -0
  236. package/dist/editor/components/BranchesDrawer.js +5 -0
  237. package/dist/editor/components/ConfirmDeleteModal.d.ts +6 -1
  238. package/dist/editor/components/ConfirmDeleteModal.js +3 -2
  239. package/dist/editor/components/DestructiveMenuItem.d.ts +7 -0
  240. package/dist/editor/components/DestructiveMenuItem.js +8 -0
  241. package/dist/editor/components/EditorCrashScreen.d.ts +17 -0
  242. package/dist/editor/components/EditorCrashScreen.js +22 -0
  243. package/dist/editor/components/EditorErrorBoundary.d.ts +38 -0
  244. package/dist/editor/components/EditorErrorBoundary.js +40 -0
  245. package/dist/editor/components/EditorHeader.d.ts +56 -106
  246. package/dist/editor/components/EditorHeader.js +130 -131
  247. package/dist/editor/components/EditorRail.d.ts +49 -0
  248. package/dist/editor/components/EditorRail.js +65 -0
  249. package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
  250. package/dist/editor/components/EntryBreadcrumb.js +56 -0
  251. package/dist/editor/components/EntryCreateModal.d.ts +12 -7
  252. package/dist/editor/components/EntryCreateModal.js +51 -57
  253. package/dist/editor/components/NoEditPermissionNotice.d.ts +6 -5
  254. package/dist/editor/components/NoEditPermissionNotice.js +5 -4
  255. package/dist/editor/components/QuickOpenPalette.d.ts +22 -0
  256. package/dist/editor/components/QuickOpenPalette.js +66 -0
  257. package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
  258. package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
  259. package/dist/editor/components/ReasonedMenuItem.d.ts +15 -0
  260. package/dist/editor/components/ReasonedMenuItem.js +15 -0
  261. package/dist/editor/components/ReferencedByList.d.ts +11 -0
  262. package/dist/editor/components/ReferencedByList.js +28 -0
  263. package/dist/editor/components/RelativeTime.d.ts +10 -0
  264. package/dist/editor/components/RelativeTime.js +10 -0
  265. package/dist/editor/components/RenameEntryModal.d.ts +9 -1
  266. package/dist/editor/components/RenameEntryModal.js +19 -31
  267. package/dist/editor/components/RetryToastMessage.d.ts +9 -0
  268. package/dist/editor/components/RetryToastMessage.js +6 -0
  269. package/dist/editor/components/SaveAllResult.d.ts +25 -0
  270. package/dist/editor/components/SaveAllResult.js +86 -0
  271. package/dist/editor/components/SaveSplitButton.d.ts +30 -0
  272. package/dist/editor/components/SaveSplitButton.js +108 -0
  273. package/dist/editor/components/StagedChangesDrawer.d.ts +39 -0
  274. package/dist/editor/components/StagedChangesDrawer.js +92 -0
  275. package/dist/editor/components/UnavailableEntryNotice.d.ts +6 -0
  276. package/dist/editor/components/UnavailableEntryNotice.js +9 -0
  277. package/dist/editor/components/UnavailableTypeMessage.d.ts +6 -0
  278. package/dist/editor/components/UnavailableTypeMessage.js +9 -0
  279. package/dist/editor/components/UndoToastMessage.d.ts +18 -0
  280. package/dist/editor/components/UndoToastMessage.js +10 -0
  281. package/dist/editor/components/UserBadge.d.ts +2 -5
  282. package/dist/editor/components/UserBadge.js +62 -33
  283. package/dist/editor/components/ValidationSummary.d.ts +14 -0
  284. package/dist/editor/components/ValidationSummary.js +24 -0
  285. package/dist/editor/components/index.d.ts +2 -2
  286. package/dist/editor/components/index.js +2 -2
  287. package/dist/editor/context/ApiClientContext.d.ts +8 -2
  288. package/dist/editor/context/ApiClientContext.js +19 -8
  289. package/dist/editor/context/AssetContext.d.ts +13 -35
  290. package/dist/editor/context/AssetContext.js +12 -31
  291. package/dist/editor/context/EditorStateContext.d.ts +4 -4
  292. package/dist/editor/context/EditorStateContext.js +4 -4
  293. package/dist/editor/context/SWRProvider.d.ts +3 -3
  294. package/dist/editor/context/SiteMdxAllowContext.d.ts +4 -0
  295. package/dist/editor/context/SiteMdxAllowContext.js +7 -0
  296. package/dist/editor/context/index.d.ts +2 -1
  297. package/dist/editor/context/index.js +2 -1
  298. package/dist/editor/contrast.d.ts +19 -0
  299. package/dist/editor/contrast.js +41 -0
  300. package/dist/editor/copy.d.ts +18 -0
  301. package/dist/editor/copy.js +18 -0
  302. package/dist/editor/editor-config.js +16 -0
  303. package/dist/editor/editor-utils.d.ts +34 -8
  304. package/dist/editor/editor-utils.js +42 -20
  305. package/dist/editor/entry-search.d.ts +20 -0
  306. package/dist/editor/entry-search.js +34 -0
  307. package/dist/editor/field-breadcrumb.d.ts +12 -0
  308. package/dist/editor/field-breadcrumb.js +85 -0
  309. package/dist/editor/field-props.d.ts +86 -0
  310. package/dist/editor/field-props.js +27 -0
  311. package/dist/editor/field-target.d.ts +30 -0
  312. package/dist/editor/field-target.js +77 -0
  313. package/dist/editor/fields/BlockField.d.ts +12 -0
  314. package/dist/editor/fields/BlockField.js +89 -73
  315. package/dist/editor/fields/CodeField.d.ts +2 -0
  316. package/dist/editor/fields/CodeField.js +6 -2
  317. package/dist/editor/fields/DateTimeField.d.ts +4 -29
  318. package/dist/editor/fields/DateTimeField.js +79 -41
  319. package/dist/editor/fields/ExpandAllButton.d.ts +6 -0
  320. package/dist/editor/fields/ExpandAllButton.js +4 -0
  321. package/dist/editor/fields/FieldCrashFallback.d.ts +17 -0
  322. package/dist/editor/fields/FieldCrashFallback.js +30 -0
  323. package/dist/editor/fields/FieldLabel.d.ts +37 -0
  324. package/dist/editor/fields/FieldLabel.js +19 -0
  325. package/dist/editor/fields/ImageField.d.ts +4 -0
  326. package/dist/editor/fields/ImageField.js +29 -14
  327. package/dist/editor/fields/InlineGroupField.js +4 -3
  328. package/dist/editor/fields/ItemList.d.ts +42 -0
  329. package/dist/editor/fields/ItemList.js +130 -0
  330. package/dist/editor/fields/MarkdownField.d.ts +32 -0
  331. package/dist/editor/fields/MarkdownField.js +223 -46
  332. package/dist/editor/fields/MdxImageDialog.js +1 -1
  333. package/dist/editor/fields/NumberField.d.ts +2 -0
  334. package/dist/editor/fields/NumberField.js +8 -5
  335. package/dist/editor/fields/NumberListField.d.ts +2 -0
  336. package/dist/editor/fields/NumberListField.js +19 -16
  337. package/dist/editor/fields/ObjectField.d.ts +2 -7
  338. package/dist/editor/fields/ObjectField.js +15 -5
  339. package/dist/editor/fields/ObjectListField.d.ts +29 -0
  340. package/dist/editor/fields/ObjectListField.js +57 -0
  341. package/dist/editor/fields/ReferenceField.d.ts +2 -0
  342. package/dist/editor/fields/ReferenceField.js +34 -9
  343. package/dist/editor/fields/SelectField.d.ts +2 -0
  344. package/dist/editor/fields/SelectField.js +11 -2
  345. package/dist/editor/fields/StringListField.d.ts +4 -1
  346. package/dist/editor/fields/StringListField.js +22 -8
  347. package/dist/editor/fields/TextField.d.ts +2 -0
  348. package/dist/editor/fields/TextField.js +6 -2
  349. package/dist/editor/fields/ToggleField.d.ts +2 -0
  350. package/dist/editor/fields/ToggleField.js +8 -3
  351. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
  352. package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
  353. package/dist/editor/fields/form-expansion.d.ts +9 -0
  354. package/dist/editor/fields/form-expansion.js +2 -0
  355. package/dist/editor/fields/item-expansion.d.ts +15 -0
  356. package/dist/editor/fields/item-expansion.js +60 -0
  357. package/dist/editor/fields/list-item-keys.d.ts +8 -0
  358. package/dist/editor/fields/list-item-keys.js +50 -0
  359. package/dist/editor/fields/list-item-title.d.ts +20 -0
  360. package/dist/editor/fields/list-item-title.js +85 -0
  361. package/dist/editor/fields/markdown-export-options.d.ts +2 -0
  362. package/dist/editor/fields/markdown-export-options.js +8 -0
  363. package/dist/editor/fields/markdown-fidelity-visitors.d.ts +11 -0
  364. package/dist/editor/fields/markdown-fidelity-visitors.js +160 -0
  365. package/dist/editor/fields/mdx-jsx-support.d.ts +5 -1
  366. package/dist/editor/fields/mdx-jsx-support.js +157 -69
  367. package/dist/editor/fields/rich-text-failures.d.ts +5 -0
  368. package/dist/editor/fields/rich-text-failures.js +26 -0
  369. package/dist/editor/fields/toolbar-wrap.d.ts +7 -0
  370. package/dist/editor/fields/toolbar-wrap.js +35 -0
  371. package/dist/editor/form-outline.d.ts +13 -0
  372. package/dist/editor/form-outline.js +70 -0
  373. package/dist/editor/group-manager/GroupCard.d.ts +4 -1
  374. package/dist/editor/group-manager/GroupCard.js +12 -3
  375. package/dist/editor/group-manager/GroupForm.js +1 -1
  376. package/dist/editor/group-manager/InternalGroupsTab.d.ts +2 -1
  377. package/dist/editor/group-manager/InternalGroupsTab.js +4 -4
  378. package/dist/editor/group-manager/MemberList.d.ts +2 -1
  379. package/dist/editor/group-manager/MemberList.js +15 -4
  380. package/dist/editor/group-manager/hooks/useGroupState.d.ts +5 -0
  381. package/dist/editor/group-manager/hooks/useGroupState.js +30 -7
  382. package/dist/editor/group-manager/index.js +34 -5
  383. package/dist/editor/group-manager/types.d.ts +5 -2
  384. package/dist/editor/hooks/create-branch-request.d.ts +20 -0
  385. package/dist/editor/hooks/create-branch-request.js +54 -0
  386. package/dist/editor/hooks/save-all.d.ts +68 -0
  387. package/dist/editor/hooks/save-all.js +61 -0
  388. package/dist/editor/hooks/useBranchActions.d.ts +11 -1
  389. package/dist/editor/hooks/useBranchActions.js +42 -19
  390. package/dist/editor/hooks/useBranchManager.d.ts +21 -0
  391. package/dist/editor/hooks/useBranchManager.js +321 -125
  392. package/dist/editor/hooks/useBranchesData.d.ts +8 -4
  393. package/dist/editor/hooks/useBranchesData.js +9 -5
  394. package/dist/editor/hooks/useCommentSystem.d.ts +14 -2
  395. package/dist/editor/hooks/useCommentSystem.js +79 -55
  396. package/dist/editor/hooks/useCommentsData.d.ts +11 -7
  397. package/dist/editor/hooks/useCommentsData.js +20 -19
  398. package/dist/editor/hooks/useDraftManager.d.ts +77 -3
  399. package/dist/editor/hooks/useDraftManager.js +887 -94
  400. package/dist/editor/hooks/useEditorLayout.d.ts +53 -7
  401. package/dist/editor/hooks/useEditorLayout.js +98 -20
  402. package/dist/editor/hooks/useEntriesData.d.ts +2 -1
  403. package/dist/editor/hooks/useEntriesData.js +18 -4
  404. package/dist/editor/hooks/useEntryManager.d.ts +41 -2
  405. package/dist/editor/hooks/useEntryManager.js +227 -41
  406. package/dist/editor/hooks/useGroupManager.js +13 -16
  407. package/dist/editor/hooks/usePermissionManager.js +14 -6
  408. package/dist/editor/hooks/usePreviewMarks.d.ts +21 -0
  409. package/dist/editor/hooks/usePreviewMarks.js +33 -0
  410. package/dist/editor/hooks/useReferenceResolution.d.ts +10 -3
  411. package/dist/editor/hooks/useReferenceResolution.js +34 -177
  412. package/dist/editor/hooks/useSchemaManager.d.ts +25 -6
  413. package/dist/editor/hooks/useSchemaManager.js +97 -39
  414. package/dist/editor/hooks/useUserMetadata.d.ts +14 -4
  415. package/dist/editor/hooks/useUserMetadata.js +76 -47
  416. package/dist/editor/hooks/user-metadata-batcher.d.ts +8 -0
  417. package/dist/editor/hooks/user-metadata-batcher.js +33 -0
  418. package/dist/editor/media/AssetCard.d.ts +6 -7
  419. package/dist/editor/media/AssetCard.js +24 -31
  420. package/dist/editor/media/AssetListRow.d.ts +16 -0
  421. package/dist/editor/media/AssetListRow.js +41 -0
  422. package/dist/editor/media/CropStep.js +6 -1
  423. package/dist/editor/media/MediaLibrary.d.ts +4 -3
  424. package/dist/editor/media/MediaLibrary.js +11 -4
  425. package/dist/editor/media/MediaLibraryBody.d.ts +3 -3
  426. package/dist/editor/media/MediaLibraryBody.js +195 -80
  427. package/dist/editor/media/MediaLibrarySkeleton.d.ts +6 -0
  428. package/dist/editor/media/MediaLibrarySkeleton.js +14 -0
  429. package/dist/editor/media/crop-math.d.ts +3 -4
  430. package/dist/editor/media/crop-math.js +11 -22
  431. package/dist/editor/media/editor-image-src.d.ts +8 -0
  432. package/dist/editor/media/editor-image-src.js +10 -0
  433. package/dist/editor/media/format-bytes.d.ts +2 -0
  434. package/dist/editor/media/format-bytes.js +23 -0
  435. package/dist/editor/media/media-library-prefs.d.ts +23 -0
  436. package/dist/editor/media/media-library-prefs.js +49 -0
  437. package/dist/editor/media/media-library-view.d.ts +34 -0
  438. package/dist/editor/media/media-library-view.js +40 -0
  439. package/dist/editor/media/useAssetLibrary.d.ts +30 -0
  440. package/dist/editor/media/useAssetLibrary.js +126 -0
  441. package/dist/editor/media/useStableAssetOrder.d.ts +11 -0
  442. package/dist/editor/media/useStableAssetOrder.js +37 -0
  443. package/dist/editor/media/useThumbnailFailure.d.ts +6 -0
  444. package/dist/editor/media/useThumbnailFailure.js +16 -0
  445. package/dist/editor/permission-manager/GroupSelector.js +2 -2
  446. package/dist/editor/permission-manager/PermissionEditor.d.ts +1 -1
  447. package/dist/editor/permission-manager/PermissionEditor.js +15 -4
  448. package/dist/editor/permission-manager/PermissionLevelBadge.js +1 -1
  449. package/dist/editor/permission-manager/PermissionTree.d.ts +1 -1
  450. package/dist/editor/permission-manager/UserSelector.d.ts +1 -1
  451. package/dist/editor/permission-manager/UserSelector.js +3 -3
  452. package/dist/editor/permission-manager/index.js +12 -3
  453. package/dist/editor/permission-manager/types.d.ts +5 -2
  454. package/dist/editor/platform.d.ts +3 -0
  455. package/dist/editor/platform.js +9 -0
  456. package/dist/editor/preview-asset-base.d.ts +9 -0
  457. package/dist/editor/preview-asset-base.js +17 -0
  458. package/dist/editor/preview-bridge.d.ts +51 -27
  459. package/dist/editor/preview-bridge.js +103 -144
  460. package/dist/editor/preview-marks.d.ts +15 -0
  461. package/dist/editor/preview-marks.js +74 -0
  462. package/dist/editor/preview-path.d.ts +12 -0
  463. package/dist/editor/preview-path.js +45 -1
  464. package/dist/editor/preview-scale.d.ts +27 -0
  465. package/dist/editor/preview-scale.js +34 -0
  466. package/dist/editor/raw-asset-base.d.ts +5 -0
  467. package/dist/editor/raw-asset-base.js +14 -0
  468. package/dist/editor/relative-time.d.ts +4 -4
  469. package/dist/editor/relative-time.js +16 -8
  470. package/dist/editor/schema-editor/CollectionEditor.d.ts +20 -3
  471. package/dist/editor/schema-editor/CollectionEditor.js +361 -62
  472. package/dist/editor/schema-editor/EntryTypeEditor.d.ts +5 -1
  473. package/dist/editor/schema-editor/EntryTypeEditor.js +11 -6
  474. package/dist/editor/schema-editor/collection-draft.d.ts +22 -0
  475. package/dist/editor/schema-editor/collection-draft.js +40 -0
  476. package/dist/editor/schema-editor/index.d.ts +1 -1
  477. package/dist/editor/schema-label.d.ts +10 -0
  478. package/dist/editor/schema-label.js +17 -0
  479. package/dist/editor/settings-conflict.d.ts +5 -0
  480. package/dist/editor/settings-conflict.js +8 -0
  481. package/dist/editor/shell/ActivityRail.d.ts +37 -0
  482. package/dist/editor/shell/ActivityRail.js +91 -0
  483. package/dist/editor/shell/EditorShell.d.ts +17 -0
  484. package/dist/editor/shell/EditorShell.js +31 -0
  485. package/dist/editor/shell/PaneLayout.d.ts +28 -0
  486. package/dist/editor/shell/PaneLayout.js +116 -0
  487. package/dist/editor/shell/SidePanelHost.d.ts +35 -0
  488. package/dist/editor/shell/SidePanelHost.js +184 -0
  489. package/dist/editor/shell/TopBar.d.ts +12 -0
  490. package/dist/editor/shell/TopBar.js +14 -0
  491. package/dist/editor/shell/host-chrome.d.ts +16 -0
  492. package/dist/editor/shell/host-chrome.js +69 -0
  493. package/dist/editor/slug.d.ts +16 -0
  494. package/dist/editor/slug.js +43 -0
  495. package/dist/editor/theme.d.ts +2 -1
  496. package/dist/editor/theme.js +314 -13
  497. package/dist/editor/unanswered.d.ts +17 -0
  498. package/dist/editor/unanswered.js +26 -0
  499. package/dist/editor/unavailable-entry-type.d.ts +20 -0
  500. package/dist/editor/unavailable-entry-type.js +28 -0
  501. package/dist/editor/undo-removal.d.ts +57 -0
  502. package/dist/editor/undo-removal.js +163 -0
  503. package/dist/editor/user-display.d.ts +4 -0
  504. package/dist/editor/user-display.js +5 -0
  505. package/dist/editor/utils/confirm-modal.d.ts +8 -0
  506. package/dist/editor/utils/confirm-modal.js +12 -0
  507. package/dist/editor/utils/editor-errors.d.ts +19 -0
  508. package/dist/editor/utils/editor-errors.js +32 -0
  509. package/dist/editor/utils/focus-return.d.ts +56 -0
  510. package/dist/editor/utils/focus-return.js +106 -0
  511. package/dist/entry-link-resolver.d.ts +11 -1
  512. package/dist/entry-link-resolver.js +65 -5
  513. package/dist/entry-schema-registry.d.ts +3 -3
  514. package/dist/entry-schema-registry.js +7 -4
  515. package/dist/entry-schema.d.ts +49 -3
  516. package/dist/entry-schema.js +18 -0
  517. package/dist/git-manager.d.ts +172 -10
  518. package/dist/git-manager.js +449 -107
  519. package/dist/github-service.d.ts +39 -2
  520. package/dist/github-service.js +101 -7
  521. package/dist/http/handler.js +74 -9
  522. package/dist/http/types.d.ts +2 -0
  523. package/dist/http/worker-not-ready.d.ts +9 -4
  524. package/dist/http/worker-not-ready.js +59 -6
  525. package/dist/index.d.ts +2 -0
  526. package/dist/index.js +3 -0
  527. package/dist/item-keys.d.ts +46 -0
  528. package/dist/item-keys.js +235 -0
  529. package/dist/operating-mode/editor-mode-check.d.ts +15 -0
  530. package/dist/operating-mode/editor-mode-check.js +24 -0
  531. package/dist/operating-mode/mode-env.d.ts +13 -14
  532. package/dist/operating-mode/mode-env.js +13 -14
  533. package/dist/operating-mode/types.d.ts +2 -2
  534. package/dist/paths/branch.d.ts +0 -1
  535. package/dist/paths/branch.js +1 -8
  536. package/dist/paths/index.d.ts +1 -1
  537. package/dist/paths/index.js +1 -1
  538. package/dist/paths/validation.js +0 -2
  539. package/dist/preview.d.ts +8 -0
  540. package/dist/preview.js +7 -0
  541. package/dist/reference-resolver.js +1 -0
  542. package/dist/resolve-canopy-user.d.ts +1 -1
  543. package/dist/resolve-canopy-user.js +1 -1
  544. package/dist/schema/meta-loader.d.ts +18 -8
  545. package/dist/schema/meta-loader.js +42 -9
  546. package/dist/schema/registry-fingerprint.d.ts +14 -0
  547. package/dist/schema/registry-fingerprint.js +63 -0
  548. package/dist/schema/resolver.d.ts +7 -4
  549. package/dist/schema/resolver.js +7 -6
  550. package/dist/schema/schema-store.d.ts +7 -10
  551. package/dist/schema/schema-store.js +13 -11
  552. package/dist/schema/schema-unavailable-error.d.ts +11 -0
  553. package/dist/schema/schema-unavailable-error.js +15 -0
  554. package/dist/schema/types.d.ts +22 -0
  555. package/dist/schema-registry-record.d.ts +35 -0
  556. package/dist/schema-registry-record.js +134 -0
  557. package/dist/server.d.ts +79 -9
  558. package/dist/server.js +62 -8
  559. package/dist/services.d.ts +33 -5
  560. package/dist/services.js +155 -37
  561. package/dist/settings-workspace.d.ts +23 -3
  562. package/dist/settings-workspace.js +159 -45
  563. package/dist/stamp-on-publish.d.ts +36 -0
  564. package/dist/stamp-on-publish.js +71 -0
  565. package/dist/static/index.d.ts +58 -5
  566. package/dist/static/index.js +133 -3
  567. package/dist/static/seo.d.ts +5 -5
  568. package/dist/static/seo.js +5 -5
  569. package/dist/submission-attribution.d.ts +8 -0
  570. package/dist/submission-attribution.js +22 -0
  571. package/dist/system-users.d.ts +12 -0
  572. package/dist/system-users.js +12 -0
  573. package/dist/task-queue/cms-task-queue.d.ts +1 -1
  574. package/dist/task-queue/cms-task-queue.js +1 -1
  575. package/dist/task-queue/index.d.ts +1 -1
  576. package/dist/task-queue/index.js +1 -1
  577. package/dist/task-queue/task-queue.d.ts +6 -0
  578. package/dist/task-queue/task-queue.js +29 -0
  579. package/dist/task-queue/worker-status.d.ts +35 -9
  580. package/dist/task-queue/worker-status.js +84 -17
  581. package/dist/types.d.ts +90 -9
  582. package/dist/url-collision.js +3 -3
  583. package/dist/url-exclusivity-fixtures.d.ts +8 -1
  584. package/dist/url-exclusivity-fixtures.js +11 -1
  585. package/dist/user.js +2 -1
  586. package/dist/utils/atomic-write.js +2 -1
  587. package/dist/utils/base-branch.d.ts +13 -0
  588. package/dist/utils/base-branch.js +23 -0
  589. package/dist/utils/content-serialize.d.ts +15 -7
  590. package/dist/utils/content-serialize.js +353 -51
  591. package/dist/utils/content-write-lock.d.ts +6 -1
  592. package/dist/utils/content-write-lock.js +21 -6
  593. package/dist/utils/debug.d.ts +1 -1
  594. package/dist/utils/debug.js +5 -3
  595. package/dist/utils/error.js +13 -2
  596. package/dist/utils/escape-text.d.ts +7 -0
  597. package/dist/utils/escape-text.js +27 -0
  598. package/dist/utils/fs.d.ts +12 -4
  599. package/dist/utils/fs.js +18 -5
  600. package/dist/utils/git.d.ts +21 -1
  601. package/dist/utils/git.js +63 -8
  602. package/dist/utils/json-source-splice.d.ts +14 -0
  603. package/dist/utils/json-source-splice.js +120 -0
  604. package/dist/utils/markdown-body-splice.d.ts +39 -0
  605. package/dist/utils/markdown-body-splice.js +421 -0
  606. package/dist/utils/occ-json-write.js +18 -12
  607. package/dist/utils/provision-log.d.ts +25 -0
  608. package/dist/utils/provision-log.js +49 -0
  609. package/dist/utils/provisioning-lock.d.ts +32 -1
  610. package/dist/utils/provisioning-lock.js +78 -32
  611. package/dist/utils/sanitize-href.d.ts +9 -23
  612. package/dist/utils/sanitize-href.js +14 -28
  613. package/dist/utils/title-field.d.ts +10 -0
  614. package/dist/utils/title-field.js +47 -0
  615. package/dist/utils/yaml-source-splice.d.ts +58 -0
  616. package/dist/utils/yaml-source-splice.js +515 -0
  617. package/dist/validation/block-structural-keys.d.ts +1 -1
  618. package/dist/validation/block-structural-keys.js +1 -1
  619. package/dist/validation/deletion-checker.d.ts +21 -29
  620. package/dist/validation/deletion-checker.js +41 -117
  621. package/dist/validation/entry-validator.d.ts +6 -10
  622. package/dist/validation/entry-validator.js +19 -26
  623. package/dist/validation/field-traversal.d.ts +22 -0
  624. package/dist/validation/field-traversal.js +39 -8
  625. package/dist/validation/markdown-safety.d.ts +68 -0
  626. package/dist/validation/markdown-safety.js +596 -0
  627. package/dist/validation/mdx-allowlist.d.ts +55 -0
  628. package/dist/validation/mdx-allowlist.js +183 -0
  629. package/dist/validation/reference-validator.d.ts +2 -0
  630. package/dist/validation/reference-validator.js +3 -0
  631. package/dist/version.d.ts +1 -1
  632. package/dist/version.js +1 -1
  633. package/dist/worker/branch-policy.d.ts +124 -0
  634. package/dist/worker/branch-policy.js +243 -0
  635. package/dist/worker/bundle-header.d.ts +117 -0
  636. package/dist/worker/bundle-header.js +253 -0
  637. package/dist/worker/cms-worker.d.ts +236 -142
  638. package/dist/worker/cms-worker.js +772 -285
  639. package/dist/worker/gateway-egress.d.ts +43 -0
  640. package/dist/worker/gateway-egress.js +209 -0
  641. package/dist/worker/gateway-instance.d.ts +42 -0
  642. package/dist/worker/gateway-instance.js +129 -0
  643. package/dist/worker/git-process.d.ts +81 -0
  644. package/dist/worker/git-process.js +234 -0
  645. package/dist/worker/git-sync.d.ts +34 -17
  646. package/dist/worker/git-sync.js +394 -131
  647. package/dist/worker/github-auth.d.ts +9 -9
  648. package/dist/worker/github-auth.js +13 -15
  649. package/dist/worker/github-gateway-http.d.ts +62 -0
  650. package/dist/worker/github-gateway-http.js +977 -0
  651. package/dist/worker/github-gateway-wire.d.ts +590 -0
  652. package/dist/worker/github-gateway-wire.js +488 -0
  653. package/dist/worker/github-gateway.d.ts +238 -0
  654. package/dist/worker/github-gateway.js +726 -0
  655. package/dist/worker/github-mirror-upkeep.d.ts +75 -0
  656. package/dist/worker/github-mirror-upkeep.js +266 -0
  657. package/dist/worker/github-mirror.d.ts +364 -0
  658. package/dist/worker/github-mirror.js +1030 -0
  659. package/dist/worker/github-transfer.d.ts +56 -0
  660. package/dist/worker/github-transfer.js +444 -0
  661. package/dist/worker/history-rewrite.d.ts +4 -5
  662. package/dist/worker/history-rewrite.js +12 -17
  663. package/dist/worker/provisioned-workspace.d.ts +5 -5
  664. package/dist/worker/provisioned-workspace.js +5 -5
  665. package/dist/worker/rebase.d.ts +1 -1
  666. package/dist/worker/rebase.js +58 -54
  667. package/dist/worker/remote-git-maintenance.d.ts +3 -0
  668. package/dist/worker/remote-git-maintenance.js +12 -0
  669. package/dist/worker/schema-gate.d.ts +49 -0
  670. package/dist/worker/schema-gate.js +226 -0
  671. package/dist/worker/self-checks.d.ts +107 -0
  672. package/dist/worker/self-checks.js +358 -0
  673. package/dist/worker/shared-repo-git.d.ts +96 -0
  674. package/dist/worker/shared-repo-git.js +459 -0
  675. package/dist/worker/sparse-cone.d.ts +20 -0
  676. package/dist/worker/sparse-cone.js +136 -0
  677. package/dist/worker/stale-mirror.d.ts +82 -0
  678. package/dist/worker/stale-mirror.js +130 -0
  679. package/dist/worker/task-runner.d.ts +5 -3
  680. package/dist/worker/task-runner.js +287 -186
  681. package/dist/worker/worker-context.d.ts +36 -37
  682. package/package.json +35 -7
  683. package/dist/editor/EditorPanes.d.ts +0 -10
  684. package/dist/editor/EditorPanes.js +0 -98
  685. package/dist/editor/components/EditorFooter.d.ts +0 -11
  686. package/dist/editor/components/EditorFooter.js +0 -9
  687. package/dist/editor/components/EditorSidebar.d.ts +0 -67
  688. package/dist/editor/components/EditorSidebar.js +0 -22
@@ -1,8 +1,11 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { GitManager } from './git-manager.js';
4
+ import { removeLeftoverDir, settingsStagingDirName, settingsTrashDirName, } from './branch-provisioning.js';
4
5
  import { createDebugLogger } from './utils/debug.js';
5
- import { getErrorMessage } from './utils/error.js';
6
+ import { getErrorMessage, isNodeError } from './utils/error.js';
7
+ import { canopyLogWarn } from './utils/logger.js';
8
+ import { ProvisionLog } from './utils/provision-log.js';
6
9
  import { acquireProvisioningLock } from './utils/provisioning-lock.js';
7
10
  import { RESERVED_SETTINGS_BRANCH_PREFIX } from './paths/index.js';
8
11
  const log = createDebugLogger({ prefix: 'SettingsWorkspace' });
@@ -30,13 +33,27 @@ const ensuredSettingsWorkspaces = new Set();
30
33
  function ensuredKey(options) {
31
34
  return `${path.resolve(options.settingsRoot)}\0${options.branchName}`;
32
35
  }
36
+ async function headBranch(settingsRoot) {
37
+ try {
38
+ const head = (await fs.readFile(path.join(settingsRoot, '.git', 'HEAD'), 'utf-8')).trim();
39
+ return head.startsWith('ref: refs/heads/') ? head.slice('ref: refs/heads/'.length) : undefined;
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
33
45
  async function checkedOutOn(settingsRoot, branchName) {
46
+ return (await headBranch(settingsRoot)) === branchName;
47
+ }
48
+ async function hasLocalBranch(settingsRoot, branchName) {
49
+ const gitDir = path.join(settingsRoot, '.git');
34
50
  try {
35
- const head = await fs.readFile(path.join(settingsRoot, '.git', 'HEAD'), 'utf-8');
36
- return head.trim() === `ref: refs/heads/${branchName}`;
51
+ await fs.access(path.join(gitDir, 'refs', 'heads', branchName));
52
+ return true;
37
53
  }
38
54
  catch {
39
- return false;
55
+ const packed = await fs.readFile(path.join(gitDir, 'packed-refs'), 'utf-8').catch(() => '');
56
+ return packed.split('\n').some((line) => line.endsWith(` refs/heads/${branchName}`));
40
57
  }
41
58
  }
42
59
  const SETTINGS_INIT_LOCK_DIR = '.settings-init';
@@ -49,8 +66,8 @@ const SETTINGS_INIT_LOCK_NAME = 'lock';
49
66
  * its target, and `GitManager.initializeWorkspace` clones INTO the settings
50
67
  * root, which `git clone` refuses if anything is already there. A dedicated
51
68
  * dot-directory also keeps this marker out of `path.dirname(settingsRoot)`,
52
- * where `ensureLocalSimulatedRemote` puts `.remote-init.lock` — settings init
53
- * calls into that while holding this lock, so separate directories make the
69
+ * where `ensureLocalSimulatedRemote` puts `.remote-init.lock` — an in-place
70
+ * settings re-init calls into that while holding this lock, so separate directories make the
54
71
  * nesting obvious rather than incidental. For how a lock's anchor path is
55
72
  * chosen, see utils/provisioning-lock.ts and docs/concurrency.md.
56
73
  * @internal Exported for tests.
@@ -78,6 +95,41 @@ async function settingsFilesPresent(settingsRoot) {
78
95
  }
79
96
  return false;
80
97
  }
98
+ /**
99
+ * What occupies the settings root.
100
+ *
101
+ * `interrupted` is a first init that died before its settings branch existed: not on this
102
+ * deployment's settings branch or any `canopycms-settings-*` one, no local ref for it, and no
103
+ * settings files -- what the rename guard lets through, minus a local settings branch that could
104
+ * hold unpushed commits. A clone killed in place leaves this behind, often with a stale
105
+ * `config.lock` that fails every later git config write.
106
+ */
107
+ async function settingsRootState(options) {
108
+ let entries;
109
+ try {
110
+ entries = await fs.readdir(options.settingsRoot);
111
+ }
112
+ catch (err) {
113
+ if (isNodeError(err) && err.code === 'ENOENT')
114
+ return 'vacant';
115
+ if (isNodeError(err) && err.code === 'ENOTDIR')
116
+ return 'interrupted';
117
+ throw err;
118
+ }
119
+ if (entries.length === 0)
120
+ return 'vacant';
121
+ if (await settingsFilesPresent(options.settingsRoot))
122
+ return 'in-use';
123
+ if (!(await GitManager.repoExistsAt(options.settingsRoot)))
124
+ return 'interrupted';
125
+ const head = await headBranch(options.settingsRoot);
126
+ if (head === options.branchName || head?.startsWith(RESERVED_SETTINGS_BRANCH_PREFIX)) {
127
+ return 'in-use';
128
+ }
129
+ if (await hasLocalBranch(options.settingsRoot, options.branchName))
130
+ return 'in-use';
131
+ return 'interrupted';
132
+ }
81
133
  /**
82
134
  * Rename guard (settings-branch protection): refuse to boot when an
83
135
  * ALREADY-POPULATED settings workspace would be re-orphaned under a different
@@ -144,9 +196,14 @@ async function assertSettingsWorkspaceIdentity(options) {
144
196
  * BranchWorkspaceManager this writes no metadata files and never touches the
145
197
  * branch registry.
146
198
  *
199
+ * A first clone follows [PROV-1] (branch-provisioning.ts): it is built in a
200
+ * staging sibling and renamed into place, so a process killed mid-clone never
201
+ * leaves a half-made workspace at the settings root. Never sparse: the orphan
202
+ * init's `git rm -rf .` would leave out-of-cone base files in the tree.
203
+ *
147
204
  * Two locking layers, mirroring BranchWorkspaceManager: an in-memory promise
148
205
  * lock within the process, and `acquireProvisioningLock` (proper-lockfile)
149
- * across processes and hosts.
206
+ * across processes and hosts around the publish and every in-place init.
150
207
  */
151
208
  export class SettingsWorkspaceManager {
152
209
  constructor(config) {
@@ -178,45 +235,11 @@ export class SettingsWorkspaceManager {
178
235
  // Running it here also means no path reaches initializeWorkspace
179
236
  // without the guard having run in THIS process.
180
237
  await assertSettingsWorkspaceIdentity(options);
181
- // Layer 2: cross-process/cross-host lock around the init itself
182
- // (proper-lockfile: heartbeat-refreshed while the holder lives, so a
183
- // slow EFS clone is not mistaken for a crash, and patient retries so a
184
- // loser WAITS instead of racing into a concurrent clone).
185
- const releaseLock = await acquireProvisioningLock(settingsInitLockTarget(options.settingsRoot), SETTINGS_INIT_LOCK_NAME);
186
- try {
187
- // Re-run the guard on the now-stable state: a previous holder may
188
- // have created the workspace, or moved it onto its own settings
189
- // branch, after we sampled it above, and acting on that stale
190
- // sample is the destructive path the guard exists to stop. Not
191
- // gated on any "did I win the race" flag — by design there is none;
192
- // every process here either holds the lock or has already thrown.
193
- await assertSettingsWorkspaceIdentity(options);
194
- // initializeWorkspace is idempotent (it checks for .git), so this
195
- // is safe even if another process just finished init.
196
- await GitManager.initializeWorkspace({
197
- workspacePath: options.settingsRoot,
198
- branchName: options.branchName,
199
- mode: options.mode,
200
- baseBranch: this.config.defaultBaseBranch,
201
- sourceRoot: this.config.sourceRoot,
202
- defaultRemoteUrl: this.config.defaultRemoteUrl,
203
- remoteUrl: options.remoteUrl,
204
- remoteName: this.config.defaultRemoteName,
205
- allowNetworkRemoteInProd: this.config.allowNetworkRemoteInProd,
206
- branchType: 'orphan', // Key difference: orphan branch for settings
207
- gitBotAuthorName: this.config.gitBotAuthorName,
208
- gitBotAuthorEmail: this.config.gitBotAuthorEmail,
209
- });
210
- ensuredSettingsWorkspaces.add(key);
211
- }
212
- finally {
213
- try {
214
- await releaseLock();
215
- }
216
- catch (err) {
217
- log.debug('workspace', 'Failed to release settings-init lock', { err });
218
- }
238
+ if ((await settingsRootState(options)) !== 'in-use' ||
239
+ !(await this.ensureInPlace(options))) {
240
+ await this.provisionStaged(options);
219
241
  }
242
+ ensuredSettingsWorkspaces.add(key);
220
243
  }
221
244
  finally {
222
245
  settingsInitLock = null;
@@ -225,4 +248,95 @@ export class SettingsWorkspaceManager {
225
248
  await settingsInitLock;
226
249
  });
227
250
  }
251
+ initOptions(options, workspacePath) {
252
+ return {
253
+ workspacePath,
254
+ branchName: options.branchName,
255
+ mode: options.mode,
256
+ baseBranch: this.config.defaultBaseBranch,
257
+ sourceRoot: this.config.sourceRoot,
258
+ defaultRemoteUrl: this.config.defaultRemoteUrl,
259
+ remoteUrl: options.remoteUrl,
260
+ remoteName: this.config.defaultRemoteName,
261
+ allowNetworkRemoteInProd: this.config.allowNetworkRemoteInProd,
262
+ branchType: 'orphan',
263
+ gitBotAuthorName: this.config.gitBotAuthorName,
264
+ gitBotAuthorEmail: this.config.gitBotAuthorEmail,
265
+ };
266
+ }
267
+ /**
268
+ * Layer 2 around the init itself (proper-lockfile: heartbeat-refreshed while the holder lives,
269
+ * so a slow EFS operation is not mistaken for a crash, and patient retries so a loser WAITS
270
+ * instead of racing into a concurrent init).
271
+ */
272
+ async withInitLock(options, run) {
273
+ const releaseLock = await acquireProvisioningLock(settingsInitLockTarget(options.settingsRoot), SETTINGS_INIT_LOCK_NAME);
274
+ try {
275
+ // Re-run the guard on the now-stable state: a previous holder may
276
+ // have created the workspace, or moved it onto its own settings
277
+ // branch, after we sampled it above, and acting on that stale
278
+ // sample is the destructive path the guard exists to stop. Not
279
+ // gated on any "did I win the race" flag — by design there is none;
280
+ // every process here either holds the lock or has already thrown.
281
+ await assertSettingsWorkspaceIdentity(options);
282
+ return await run();
283
+ }
284
+ finally {
285
+ try {
286
+ await releaseLock();
287
+ }
288
+ catch (err) {
289
+ log.debug('workspace', 'Failed to release settings-init lock', { err });
290
+ }
291
+ }
292
+ }
293
+ /** Re-init an existing workspace where it is; false when, under the lock, there is none. */
294
+ async ensureInPlace(options) {
295
+ return this.withInitLock(options, async () => {
296
+ if ((await settingsRootState(options)) !== 'in-use')
297
+ return false;
298
+ await GitManager.initializeWorkspace(this.initOptions(options, options.settingsRoot));
299
+ return true;
300
+ });
301
+ }
302
+ /**
303
+ * Clone and set up the settings branch in a staging sibling, then publish it under the init
304
+ * lock, moving an interrupted first init aside to `.trash-settings-*`. A workspace someone
305
+ * else published meanwhile wins and the staging copy is dropped.
306
+ */
307
+ async provisionStaged(options) {
308
+ const settingsRoot = path.resolve(options.settingsRoot);
309
+ const workspaceRoot = path.dirname(settingsRoot);
310
+ await fs.mkdir(workspaceRoot, { recursive: true });
311
+ const stagingPath = path.join(workspaceRoot, settingsStagingDirName());
312
+ const provisionLog = new ProvisionLog('settings');
313
+ let published = false;
314
+ try {
315
+ await GitManager.initializeWorkspace({
316
+ ...this.initOptions(options, stagingPath),
317
+ provisionLog,
318
+ });
319
+ published = await provisionLog.step('publish', () => this.withInitLock(options, async () => {
320
+ const state = await settingsRootState(options);
321
+ if (state === 'in-use')
322
+ return false;
323
+ if (state === 'interrupted') {
324
+ const trashName = settingsTrashDirName();
325
+ await fs.rename(settingsRoot, path.join(workspaceRoot, trashName));
326
+ canopyLogWarn(`[canopy] Moved an unfinished settings workspace clone aside as ${trashName}`);
327
+ }
328
+ await fs.rename(stagingPath, settingsRoot);
329
+ return true;
330
+ }));
331
+ }
332
+ catch (err) {
333
+ provisionLog.finish('error');
334
+ throw err;
335
+ }
336
+ finally {
337
+ if (!published)
338
+ await removeLeftoverDir(stagingPath);
339
+ }
340
+ provisionLog.finish(published ? 'ok' : 'exists');
341
+ }
228
342
  }
@@ -0,0 +1,36 @@
1
+ import type { FlatSchemaItem } from './config/index.js';
2
+ import type { ContentStore } from './content-store.js';
3
+ import type { GitManager } from './git-manager.js';
4
+ import type { ContentId } from './paths/types.js';
5
+ /**
6
+ * One entry a submit stamped: the editor moves its loaded value, draft and OCC token from
7
+ * `previousVersion` onto `version` with these fields set, instead of finding its next save refused.
8
+ */
9
+ export interface StampedEntry {
10
+ contentId: ContentId;
11
+ /** Stamp field name to the value written. */
12
+ fields: Record<string, string>;
13
+ /** The file's version (`read()`'s mtime) before the stamp. */
14
+ previousVersion: number;
15
+ /** The file's version after the stamp. */
16
+ version: number;
17
+ }
18
+ /**
19
+ * Fill every empty stamp field of every entry the base lacks with `stampedAt`, never
20
+ * overwriting a value, and return the entries written. Lists the base only when the schema has
21
+ * a stamp field, and reads only the entry files the base lacks. Rejects when the base cannot be
22
+ * fetched or listed; a single entry that cannot be written is reported through `warn` and
23
+ * skipped.
24
+ *
25
+ * The caller holds the branch's content-write lock, as `store.fillEmptyFieldWhileHoldingWriteLock`
26
+ * requires.
27
+ */
28
+ export declare function stampNewEntries(options: {
29
+ git: Pick<GitManager, 'listBaseFiles'>;
30
+ store: ContentStore;
31
+ branchRoot: string;
32
+ flatSchema: readonly FlatSchemaItem[];
33
+ contentRoot: string;
34
+ stampedAt: string;
35
+ warn: (message: string) => void;
36
+ }): Promise<StampedEntry[]>;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * `stampOnPublish` (config/types.ts): at submit, each entry new to the base branch gets the
3
+ * submit's time in its empty stamp fields. Canopy never merges and never writes the base branch,
4
+ * and adopters build from the merged tree, so its own submit is the last point it can put the
5
+ * date into the content. An entry counts as new when neither the base tip nor the fork point
6
+ * holds a file with its ContentId, so a renamed or moved entry, which keeps its ID, is never
7
+ * stamped.
8
+ */
9
+ import path from 'node:path';
10
+ import { listCollectionEntries } from './content-listing.js';
11
+ import { getErrorMessage } from './utils/error.js';
12
+ import { flattenGroupFields } from './utils/flatten-group-fields.js';
13
+ import { parseTypedFilename } from './utils/typed-filename.js';
14
+ /** The names of `schema`'s top-level fields that carry `stampOnPublish`. */
15
+ const stampFieldNames = (schema) => flattenGroupFields(schema)
16
+ .filter((f) => f.type === 'datetime' && 'stampOnPublish' in f && f.stampOnPublish === true)
17
+ .map((f) => f.name);
18
+ const isEmpty = (value) => value === undefined || value === null || value === '';
19
+ /** `contentRoot` as a repo-relative directory, or undefined when it is the repository root. */
20
+ const contentRootDir = (contentRoot) => contentRoot
21
+ .split('/')
22
+ .filter((segment) => segment && segment !== '.')
23
+ .join('/') || undefined;
24
+ /**
25
+ * Fill every empty stamp field of every entry the base lacks with `stampedAt`, never
26
+ * overwriting a value, and return the entries written. Lists the base only when the schema has
27
+ * a stamp field, and reads only the entry files the base lacks. Rejects when the base cannot be
28
+ * fetched or listed; a single entry that cannot be written is reported through `warn` and
29
+ * skipped.
30
+ *
31
+ * The caller holds the branch's content-write lock, as `store.fillEmptyFieldWhileHoldingWriteLock`
32
+ * requires.
33
+ */
34
+ export async function stampNewEntries(options) {
35
+ const collections = options.flatSchema.filter((item) => item.type === 'collection' &&
36
+ (item.entries ?? []).some((entryType) => stampFieldNames(entryType.schema).length > 0));
37
+ if (collections.length === 0)
38
+ return [];
39
+ const baseIds = new Set();
40
+ for (const file of await options.git.listBaseFiles(contentRootDir(options.contentRoot))) {
41
+ const id = parseTypedFilename(path.posix.basename(file))?.id;
42
+ if (id)
43
+ baseIds.add(id);
44
+ }
45
+ const stamped = [];
46
+ for (const collection of collections) {
47
+ const entries = await listCollectionEntries(options.branchRoot, collection, undefined, (id) => !baseIds.has(id));
48
+ for (const entry of entries) {
49
+ const schema = collection.entries?.find((e) => e.name === entry.entryType)?.schema ?? [];
50
+ const empty = stampFieldNames(schema).filter((name) => isEmpty(entry.data[name]));
51
+ let written;
52
+ for (const field of empty) {
53
+ try {
54
+ const versions = await options.store.fillEmptyFieldWhileHoldingWriteLock(entry.physicalPath, entry.format, field, options.stampedAt);
55
+ if (!versions)
56
+ continue;
57
+ written ??= { contentId: entry.contentId, fields: {}, ...versions };
58
+ written.fields[field] = options.stampedAt;
59
+ written.version = versions.version;
60
+ }
61
+ catch (err) {
62
+ options.warn(`CanopyCMS: Could not stamp "${field}" on ${entry.physicalPath}; it is submitted ` +
63
+ `without it: ${getErrorMessage(err)}`);
64
+ }
65
+ }
66
+ if (written)
67
+ stamped.push(written);
68
+ }
69
+ }
70
+ return stamped;
71
+ }
@@ -1,3 +1,4 @@
1
+ import type { MdxAllowlist } from '../config/index.js';
1
2
  import type { CanopyBuildContext } from '../context.js';
2
3
  import type { ListEntriesItem } from '../content-listing.js';
3
4
  import { type EntryFieldError } from '../validation/entry-validator.js';
@@ -81,6 +82,11 @@ export interface CollectRoutableEntriesOptions<T = Record<string, unknown>> {
81
82
  */
82
83
  resolveReferences?: boolean;
83
84
  }
85
+ /**
86
+ * What the enumeration helpers take: a listing, plus the services whose config sets
87
+ * `danglingReferences`. A context without `services` gets the default, `'error'`.
88
+ */
89
+ type StaticBuildContext = Pick<CanopyBuildContext, 'listEntries'> & Partial<Pick<CanopyBuildContext, 'services'>>;
84
90
  /**
85
91
  * Enumerate routable content entries WITH their data — the input to any surface that must read
86
92
  * inside an entry to decide what to publish (sitemap, RSS, search index, index grids).
@@ -101,7 +107,7 @@ export interface CollectRoutableEntriesOptions<T = Record<string, unknown>> {
101
107
  * const entries = await collectRoutableEntries(await getCanopyForBuild())
102
108
  * const published = entries.filter((e) => !isNoindexEntry(e.data))
103
109
  */
104
- export declare function collectRoutableEntries<T = Record<string, unknown>>(ctx: Pick<CanopyBuildContext, 'listEntries'>, options?: CollectRoutableEntriesOptions<T>): Promise<RoutableEntry<T>[]>;
110
+ export declare function collectRoutableEntries<T = Record<string, unknown>>(ctx: StaticBuildContext, options?: CollectRoutableEntriesOptions<T>): Promise<RoutableEntry<T>[]>;
105
111
  /**
106
112
  * Enumerate routable content entries as neutral path descriptors for static generation.
107
113
  *
@@ -115,7 +121,7 @@ export declare function collectRoutableEntries<T = Record<string, unknown>>(ctx:
115
121
  * // Single [slug] scoped to one collection:
116
122
  * const posts = await collectStaticPaths(await getCanopyForBuild(), { rootPath: 'content/posts' })
117
123
  */
118
- export declare function collectStaticPaths(ctx: Pick<CanopyBuildContext, 'listEntries'>, options?: CollectStaticPathsOptions): Promise<StaticPathEntry[]>;
124
+ export declare function collectStaticPaths(ctx: StaticBuildContext, options?: CollectStaticPathsOptions): Promise<StaticPathEntry[]>;
119
125
  /**
120
126
  * One schema-invalid entry found during a build-time content scan.
121
127
  */
@@ -132,9 +138,10 @@ export interface InvalidBuildEntry {
132
138
  type BuildScanItem = Pick<ListEntriesItem, 'entryPath' | 'schema'> & {
133
139
  data: unknown;
134
140
  /**
135
- * Only the unknown-key scan reads this, to recognise the md/mdx body key that `listEntries`
136
- * merges into `data`. Optional so a caller assembling items by hand still typechecks; an
137
- * absent format is treated as markdown-shaped, which errs toward under-reporting.
141
+ * The unknown-key and unsafe-markdown scans read this, to recognise the md/mdx body key that
142
+ * `listEntries` merges into `data`. Optional so a caller assembling items by hand still typechecks; an
143
+ * absent format errs toward under-reporting: the unknown-key scan treats it as markdown-shaped,
144
+ * and the unsafe-markdown scan checks no body.
138
145
  */
139
146
  format?: ListEntriesItem['format'];
140
147
  };
@@ -181,6 +188,19 @@ export declare function findEntriesWithUnknownKeys(items: readonly BuildScanItem
181
188
  * value at a time.
182
189
  */
183
190
  export declare function warnUnknownEntryKeys(items: readonly BuildScanItem[], phaseLabel: string): void;
191
+ /** An entry holding code in a markdown or MDX field that is not `executable`. */
192
+ export interface EntryWithUnsafeMarkdown {
193
+ entryPath: string;
194
+ fieldPaths: string[];
195
+ }
196
+ /**
197
+ * Scan listEntries-shaped items for markdown and MDX that runs code where the field is not
198
+ * `executable`. A save keeps such code only in a field saved unchanged, so the build is where it
199
+ * is listed. Non-fatal, like the unknown-key scan.
200
+ */
201
+ export declare function findEntriesWithUnsafeMarkdown(items: readonly BuildScanItem[], siteAllow?: MdxAllowlist): EntryWithUnsafeMarkdown[];
202
+ /** Warn — never throw — about entries holding markdown or MDX that a save of a changed field refuses. */
203
+ export declare function warnUnsafeMarkdown(items: readonly BuildScanItem[], phaseLabel: string, siteAllow?: MdxAllowlist): void;
184
204
  /**
185
205
  * Throw a single, descriptive Error if any item is schema-invalid.
186
206
  *
@@ -273,3 +293,36 @@ export declare function findUnroutableSlugs(items: readonly SlugScanItem[]): Unr
273
293
  * red build.
274
294
  */
275
295
  export declare function assertRoutableSlugs(items: readonly SlugScanItem[], phaseLabel: string): void;
296
+ /** A reference whose id names no listed entry. */
297
+ export interface DanglingReference {
298
+ /** Logical path of the referring entry. */
299
+ entryPath: string;
300
+ /** Field path in `traverseFields`' format, e.g. `author`, `blocks[2].author`, `reviewers[1]`. */
301
+ fieldPath: string;
302
+ /** The id that names no entry. */
303
+ id: string;
304
+ }
305
+ /** What the dangling-reference scan needs off a listing item. */
306
+ type ReferenceScanItem = Pick<ListEntriesItem, 'entryPath' | 'entryId' | 'schema'> & {
307
+ data: unknown;
308
+ };
309
+ /**
310
+ * Find every reference, at any depth (objects, lists, blocks), whose id names none of
311
+ * `knownIds`. `knownIds` defaults to the items' own ids, which is the whole id universe for an
312
+ * unscoped `listEntries()`.
313
+ *
314
+ * Reads ids through `referenceValueId`, so a listing that resolved its references scans the same
315
+ * as a raw one. An id naming a collection is dangling too, since it resolves to no entry.
316
+ * Exported for adopters to assert on directly; `assertNoDanglingReferences` is what the build
317
+ * uses. Sorted by entry, then field path.
318
+ */
319
+ export declare function findDanglingReferences(items: readonly ReferenceScanItem[], knownIds?: ReadonlySet<string>): DanglingReference[];
320
+ /**
321
+ * Fail the build (or, under `danglingReferences: 'warn'`, warn) when a reference names no entry.
322
+ *
323
+ * A dangling reference resolves to a `MissingReference`, so the page builds without whatever the
324
+ * reference supplied (a byline, its structured-data author) and nothing else notices. The save
325
+ * path refuses to introduce one, but a deleted target, a merge, a hand edit or a `sync pull`
326
+ * leaves one behind.
327
+ */
328
+ export declare function assertNoDanglingReferences(items: readonly ReferenceScanItem[], knownIds: ReadonlySet<string>, phaseLabel: string, mode: 'error' | 'warn'): void;
@@ -1,8 +1,11 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { isBuildMode } from '../build-mode.js';
2
3
  import { findUnknownKeys, validateEntryData, } from '../validation/entry-validator.js';
3
4
  import { findBodyFieldName } from '../utils/body-field.js';
4
5
  import { isDataOnlyFormat } from '../utils/format.js';
5
6
  import { parseSlug } from '../paths/index.js';
7
+ import { collectReferenceIds } from '../validation/field-traversal.js';
8
+ import { findMarkdownSafetyIssues } from '../validation/markdown-safety.js';
6
9
  /**
7
10
  * Framework-agnostic helpers for static-site generation. These produce neutral data structures
8
11
  * (no Next.js types) so that any framework adapter — e.g. canopycms-next — can map them to its own
@@ -27,17 +30,20 @@ async function enumerateRoutableEntries(ctx, rootPath, phaseLabel, resolveRefere
27
30
  // an abandoned schema-invalid scaffold shipping into a static build silently drops that page's
28
31
  // route (or worse, renders broken), which is worse than a red build.
29
32
  //
30
- // These four guards run in a fixed order on the RAW listing, before the caller's `filter` —
33
+ // These five guards run in a fixed order on the RAW listing, before the caller's `filter` —
31
34
  // `rootPath` narrows them, `filter` deliberately does not. The unknown-key warning runs first,
32
35
  // so a build about to go red still reports everything found. Slug routability runs next: an
33
- // unroutable entry has no URL at all as far as the other three are concerned, the most
36
+ // unroutable entry has no URL at all as far as the other four are concerned, the most
34
37
  // fundamental problem a listed entry can have. Schema validity runs before duplicate URLs for
35
- // the same "most fundamental first" reason, one level down.
38
+ // the same "most fundamental first" reason, one level down, and dangling references last: a
39
+ // page whose reference is broken still has its own route and data.
36
40
  if (isBuildMode()) {
37
41
  warnUnknownEntryKeys(entries, phaseLabel);
42
+ warnUnsafeMarkdown(entries, phaseLabel, ctx.services?.config.mdxAllow);
38
43
  assertRoutableSlugs(entries, phaseLabel);
39
44
  assertBuildEntriesValid(entries, phaseLabel);
40
45
  assertNoDuplicateUrlPaths(entries, phaseLabel);
46
+ assertNoDanglingReferences(entries, await knownEntryIds(ctx, entries, rootPath), phaseLabel, ctx.services?.config.danglingReferences ?? 'error');
41
47
  }
42
48
  return entries.map((entry) => ({
43
49
  urlPath: entry.urlPath,
@@ -221,6 +227,57 @@ export function warnUnknownEntryKeys(items, phaseLabel) {
221
227
  `These are usually left over from a renamed or reshaped field. Nothing reads them, and they are kept in the file on every save. ` +
222
228
  `Add them to the schema or remove them from the content.`);
223
229
  }
230
+ /**
231
+ * Scan listEntries-shaped items for markdown and MDX that runs code where the field is not
232
+ * `executable`. A save keeps such code only in a field saved unchanged, so the build is where it
233
+ * is listed. Non-fatal, like the unknown-key scan.
234
+ */
235
+ export function findEntriesWithUnsafeMarkdown(items, siteAllow) {
236
+ const found = [];
237
+ for (const item of items) {
238
+ if (!item.schema || typeof item.data !== 'object' || item.data === null)
239
+ continue;
240
+ const fieldPaths = unsafeFieldPaths(item.schema, item.format, item.data, siteAllow);
241
+ if (fieldPaths.length > 0)
242
+ found.push({ entryPath: item.entryPath, fieldPaths });
243
+ }
244
+ return found;
245
+ }
246
+ /**
247
+ * A build lists its entries several times (every catch-all route, the sitemap), and parsing each
248
+ * body costs milliseconds, so results are kept per schema object and hash of an entry's content.
249
+ */
250
+ const unsafeFieldPathsMemo = new WeakMap();
251
+ function unsafeFieldPaths(schema, format, data, siteAllow) {
252
+ let bySchema = unsafeFieldPathsMemo.get(schema);
253
+ if (bySchema === undefined) {
254
+ bySchema = new Map();
255
+ unsafeFieldPathsMemo.set(schema, bySchema);
256
+ }
257
+ const key = createHash('sha256')
258
+ .update(`${format ?? ''}\0${JSON.stringify(siteAllow ?? null)}\0${JSON.stringify(data)}`)
259
+ .digest('hex');
260
+ let fieldPaths = bySchema.get(key);
261
+ if (fieldPaths === undefined) {
262
+ fieldPaths = findMarkdownSafetyIssues(schema, format, data, siteAllow).map((f) => f.fieldPath);
263
+ bySchema.set(key, fieldPaths);
264
+ }
265
+ return fieldPaths;
266
+ }
267
+ /** Warn — never throw — about entries holding markdown or MDX that a save of a changed field refuses. */
268
+ export function warnUnsafeMarkdown(items, phaseLabel, siteAllow) {
269
+ const found = findEntriesWithUnsafeMarkdown(items, siteAllow);
270
+ if (found.length === 0)
271
+ return;
272
+ // Capped and counted for the reason `warnUnknownEntryKeys` gives.
273
+ const shown = found.slice(0, UNKNOWN_KEY_REPORT_LIMIT);
274
+ const lines = shown.map(({ entryPath, fieldPaths }) => ` - ${entryPath} — ${fieldPaths.join(', ')}`);
275
+ if (found.length > shown.length) {
276
+ lines.push(` …and ${found.length - shown.length} more`);
277
+ }
278
+ console.warn(`CanopyCMS static build: ${found.length} ${found.length === 1 ? 'entry holds' : 'entries hold'} markdown or MDX that runs code, or that \`mdxAllow\` excludes, during ${phaseLabel}:\n${lines.join('\n')}\n` +
279
+ `A save keeps it but refuses adding more. Move code into a component, allow a component or tag in \`mdxAllow\`, or set \`executable: true\` on a field whose editors you trust as code authors.`);
280
+ }
224
281
  /**
225
282
  * Throw a single, descriptive Error if any item is schema-invalid.
226
283
  *
@@ -366,3 +423,76 @@ export function assertRoutableSlugs(items, phaseLabel) {
366
423
  'above would build, appear in generateStaticParams and any sitemap, and then 404 on every ' +
367
424
  'visit. Rename the slug (the file itself, not just its content), then rebuild.');
368
425
  }
426
+ /**
427
+ * How many dangling references the warning lists before summarising the rest; the error lists
428
+ * every one, for the reason `warnUnknownEntryKeys` gives.
429
+ */
430
+ const DANGLING_REFERENCE_REPORT_LIMIT = 20;
431
+ /**
432
+ * Find every reference, at any depth (objects, lists, blocks), whose id names none of
433
+ * `knownIds`. `knownIds` defaults to the items' own ids, which is the whole id universe for an
434
+ * unscoped `listEntries()`.
435
+ *
436
+ * Reads ids through `referenceValueId`, so a listing that resolved its references scans the same
437
+ * as a raw one. An id naming a collection is dangling too, since it resolves to no entry.
438
+ * Exported for adopters to assert on directly; `assertNoDanglingReferences` is what the build
439
+ * uses. Sorted by entry, then field path.
440
+ */
441
+ export function findDanglingReferences(items, knownIds = new Set(items.map((item) => item.entryId))) {
442
+ const found = [];
443
+ for (const item of items) {
444
+ if (!item.schema || typeof item.data !== 'object' || item.data === null)
445
+ continue;
446
+ const data = item.data;
447
+ for (const { id, path } of collectReferenceIds(item.schema, data)) {
448
+ if (id && !knownIds.has(id))
449
+ found.push({ entryPath: item.entryPath, fieldPath: path, id });
450
+ }
451
+ }
452
+ return found.sort((a, b) => a.entryPath.localeCompare(b.entryPath) || a.fieldPath.localeCompare(b.fieldPath));
453
+ }
454
+ /**
455
+ * Fail the build (or, under `danglingReferences: 'warn'`, warn) when a reference names no entry.
456
+ *
457
+ * A dangling reference resolves to a `MissingReference`, so the page builds without whatever the
458
+ * reference supplied (a byline, its structured-data author) and nothing else notices. The save
459
+ * path refuses to introduce one, but a deleted target, a merge, a hand edit or a `sync pull`
460
+ * leaves one behind.
461
+ */
462
+ export function assertNoDanglingReferences(items, knownIds, phaseLabel, mode) {
463
+ const found = findDanglingReferences(items, knownIds);
464
+ if (found.length === 0)
465
+ return;
466
+ const shown = mode === 'warn' ? found.slice(0, DANGLING_REFERENCE_REPORT_LIMIT) : found;
467
+ const lines = shown.map(({ entryPath, fieldPath, id }) => ` - ${entryPath} — ${fieldPath} → ${id}`);
468
+ if (found.length > shown.length)
469
+ lines.push(` …and ${found.length - shown.length} more`);
470
+ const message = `CanopyCMS static build: found ${found.length} ${found.length === 1 ? 'reference' : 'references'} to a missing entry during ${phaseLabel}:\n${lines.join('\n')}\n` +
471
+ 'Each resolves as unavailable, so the page renders without what it references. Repoint or ' +
472
+ 'clear each reference, or restore the entry it names, then rebuild.';
473
+ if (mode === 'warn')
474
+ console.warn(message);
475
+ else
476
+ throw new Error(message + " Set `danglingReferences: 'warn'` to build anyway.");
477
+ }
478
+ /**
479
+ * Every entry id a build context can resolve a reference to, computed at most once per context.
480
+ *
481
+ * Targets live anywhere in the content tree, so a `rootPath`-scoped listing is not the id
482
+ * universe; that case lists the whole tree once and keeps the result for the context's lifetime.
483
+ * Safe to keep, unlike a request-time memo, because these guards run only in build mode, which
484
+ * reads a checkout that does not change mid-build (`build-mode.ts`, `readsFromCheckout`). Keyed weakly on the context, so it lives
485
+ * exactly as long as the context does; a rejected listing is forgotten so the next call retries.
486
+ */
487
+ const entryIdUniverse = new WeakMap();
488
+ async function knownEntryIds(ctx, listed, rootPath) {
489
+ const cached = entryIdUniverse.get(ctx);
490
+ if (cached)
491
+ return cached;
492
+ const universe = rootPath === undefined
493
+ ? Promise.resolve(new Set(listed.map((item) => item.entryId)))
494
+ : ctx.listEntries().then((all) => new Set(all.map((item) => item.entryId)));
495
+ entryIdUniverse.set(ctx, universe);
496
+ universe.catch(() => entryIdUniverse.delete(ctx));
497
+ return universe;
498
+ }
@@ -75,12 +75,12 @@ export declare function extractSeoFields(entryData: unknown, opts?: ExtractSeoFi
75
75
  * THE `noindex` predicate. Every surface that suppresses an entry must route through this one
76
76
  * function rather than re-deriving the flag:
77
77
  *
78
- * - the page's own `robots: { index: false }` (canopycms-next's `entryToMetadata`), and
79
- * - exclusion from the sitemap (canopycms-next's `generateContentSitemap`).
78
+ * - the page's own `robots: { index: false }` (canopycms-next's `entryToMetadata`),
79
+ * - exclusion from the sitemap (canopycms-next's `generateContentSitemap`), and
80
+ * - exclusion from AI content (`ai/generate.ts`, unless `exclude.noindex: false`).
80
81
  *
81
- * Those two shipping from one predicate is the point of shipping them together. Where they were
82
- * derived separately, a `noindex` entry stayed advertised in one surface while correctly absent
83
- * from the other, and nothing warned.
82
+ * Those surfaces shipping from one predicate is the point. Derived separately, a `noindex` entry
83
+ * stays advertised in one surface while correctly absent from another, and nothing warns.
84
84
  *
85
85
  * NOT applied to enumeration: `noindex` means "don't advertise this", not "don't build this".
86
86
  * `collectRoutableEntries` and `generateStaticParams` must still emit the entry, so its URL