canopycms 0.0.67-int.90 → 0.0.67

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 (539) hide show
  1. package/README.md +30 -3
  2. package/dist/ai/generate.js +1 -27
  3. package/dist/ai/handler.d.ts +0 -4
  4. package/dist/ai/handler.js +0 -11
  5. package/dist/ai/index.d.ts +1 -8
  6. package/dist/ai/index.js +1 -8
  7. package/dist/ai/json-to-markdown.js +15 -58
  8. package/dist/ai/resolve-branch.d.ts +2 -7
  9. package/dist/ai/resolve-branch.js +2 -8
  10. package/dist/ai/strip-mdx.d.ts +5 -13
  11. package/dist/ai/strip-mdx.js +5 -17
  12. package/dist/ai/to-plain-text.d.ts +10 -28
  13. package/dist/ai/to-plain-text.js +10 -28
  14. package/dist/ai/transform-components.d.ts +2 -4
  15. package/dist/ai/transform-components.js +8 -19
  16. package/dist/ai/types.d.ts +5 -10
  17. package/dist/api/admin-branch-health.d.ts +4 -12
  18. package/dist/api/admin-branch-health.js +26 -66
  19. package/dist/api/admin.d.ts +4 -12
  20. package/dist/api/admin.js +10 -26
  21. package/dist/api/assets.d.ts +0 -25
  22. package/dist/api/assets.js +50 -91
  23. package/dist/api/branch-merge.d.ts +1 -1
  24. package/dist/api/branch-merge.js +0 -2
  25. package/dist/api/branch-review.d.ts +0 -8
  26. package/dist/api/branch-review.js +0 -13
  27. package/dist/api/branch-status.d.ts +0 -3
  28. package/dist/api/branch-status.js +8 -33
  29. package/dist/api/branch-withdraw.d.ts +0 -1
  30. package/dist/api/branch-withdraw.js +6 -9
  31. package/dist/api/branch.d.ts +7 -12
  32. package/dist/api/branch.js +7 -44
  33. package/dist/api/client.d.ts +71 -224
  34. package/dist/api/client.js +82 -233
  35. package/dist/api/comments.d.ts +2 -4
  36. package/dist/api/comments.js +0 -17
  37. package/dist/api/content.d.ts +0 -6
  38. package/dist/api/content.js +66 -142
  39. package/dist/api/entries-constants.d.ts +5 -9
  40. package/dist/api/entries-constants.js +5 -9
  41. package/dist/api/entries.d.ts +8 -13
  42. package/dist/api/entries.js +20 -51
  43. package/dist/api/github-sync.d.ts +2 -4
  44. package/dist/api/github-sync.js +4 -8
  45. package/dist/api/groups.d.ts +3 -0
  46. package/dist/api/groups.js +8 -21
  47. package/dist/api/guards.d.ts +4 -16
  48. package/dist/api/guards.js +3 -22
  49. package/dist/api/index.d.ts +4 -4
  50. package/dist/api/permissions.d.ts +2 -0
  51. package/dist/api/permissions.js +18 -40
  52. package/dist/api/reference-options.d.ts +0 -1
  53. package/dist/api/reference-options.js +3 -11
  54. package/dist/api/request-body-hash.d.ts +12 -23
  55. package/dist/api/request-body-hash.js +12 -23
  56. package/dist/api/resolve-references.d.ts +0 -1
  57. package/dist/api/resolve-references.js +13 -28
  58. package/dist/api/route-builder.d.ts +3 -7
  59. package/dist/api/route-builder.js +1 -1
  60. package/dist/api/routes.d.ts +12 -0
  61. package/dist/api/routes.js +47 -0
  62. package/dist/api/schema.d.ts +23 -33
  63. package/dist/api/schema.js +20 -74
  64. package/dist/api/settings-helpers.js +4 -5
  65. package/dist/api/types.d.ts +0 -5
  66. package/dist/api/user.js +0 -5
  67. package/dist/api/validators.d.ts +14 -52
  68. package/dist/api/validators.js +14 -52
  69. package/dist/assets/asset-prefixes.d.ts +4 -8
  70. package/dist/assets/asset-prefixes.js +4 -8
  71. package/dist/assets/asset-src.d.ts +5 -22
  72. package/dist/assets/asset-src.js +5 -21
  73. package/dist/assets/factory.d.ts +1 -2
  74. package/dist/assets/factory.js +1 -2
  75. package/dist/assets/finalize.d.ts +3 -1
  76. package/dist/assets/finalize.js +3 -1
  77. package/dist/assets/index.d.ts +3 -3
  78. package/dist/assets/index.js +1 -1
  79. package/dist/assets/keys.d.ts +5 -3
  80. package/dist/assets/keys.js +6 -3
  81. package/dist/assets/pipeline.d.ts +3 -2
  82. package/dist/assets/pipeline.js +22 -56
  83. package/dist/assets/sharp-loader.d.ts +2 -10
  84. package/dist/assets/sharp-loader.js +2 -10
  85. package/dist/assets/store-local.d.ts +1 -2
  86. package/dist/assets/store-local.js +1 -2
  87. package/dist/assets/store-s3.d.ts +3 -4
  88. package/dist/assets/svg-sanitizer.d.ts +6 -14
  89. package/dist/assets/svg-sanitizer.js +6 -14
  90. package/dist/assets/transform-directives.d.ts +3 -4
  91. package/dist/assets/transform-directives.js +1 -1
  92. package/dist/assets/transform.d.ts +19 -30
  93. package/dist/assets/transform.js +18 -31
  94. package/dist/auth/cache.d.ts +2 -3
  95. package/dist/auth/cache.js +2 -3
  96. package/dist/auth/caching-auth-plugin.d.ts +11 -22
  97. package/dist/auth/caching-auth-plugin.js +12 -14
  98. package/dist/auth/context-helpers.d.ts +6 -20
  99. package/dist/auth/context-helpers.js +5 -17
  100. package/dist/auth/file-based-auth-cache.d.ts +10 -21
  101. package/dist/auth/file-based-auth-cache.js +23 -51
  102. package/dist/auth/plugin.d.ts +29 -61
  103. package/dist/auth/plugin.js +9 -13
  104. package/dist/auth/types.d.ts +11 -20
  105. package/dist/authorization/branch.d.ts +30 -49
  106. package/dist/authorization/branch.js +16 -42
  107. package/dist/authorization/content.d.ts +11 -40
  108. package/dist/authorization/content.js +13 -45
  109. package/dist/authorization/groups/index.d.ts +0 -3
  110. package/dist/authorization/groups/index.js +0 -3
  111. package/dist/authorization/groups/loader.d.ts +14 -28
  112. package/dist/authorization/groups/loader.js +14 -38
  113. package/dist/authorization/groups/schema.d.ts +0 -6
  114. package/dist/authorization/groups/schema.js +4 -7
  115. package/dist/authorization/helpers.d.ts +9 -28
  116. package/dist/authorization/helpers.js +9 -28
  117. package/dist/authorization/index.d.ts +3 -25
  118. package/dist/authorization/index.js +3 -35
  119. package/dist/authorization/path.d.ts +6 -13
  120. package/dist/authorization/path.js +8 -25
  121. package/dist/authorization/permissions/index.d.ts +0 -3
  122. package/dist/authorization/permissions/index.js +0 -3
  123. package/dist/authorization/permissions/loader.d.ts +14 -27
  124. package/dist/authorization/permissions/loader.js +14 -32
  125. package/dist/authorization/permissions/schema.d.ts +1 -6
  126. package/dist/authorization/permissions/schema.js +6 -13
  127. package/dist/authorization/protected-branch.d.ts +36 -65
  128. package/dist/authorization/protected-branch.js +29 -41
  129. package/dist/authorization/settings-file-store.d.ts +38 -81
  130. package/dist/authorization/settings-file-store.js +38 -79
  131. package/dist/authorization/types.d.ts +4 -26
  132. package/dist/authorization/types.js +0 -6
  133. package/dist/authorization/validation.d.ts +1 -23
  134. package/dist/authorization/validation.js +1 -24
  135. package/dist/branch-health.d.ts +39 -67
  136. package/dist/branch-health.js +27 -43
  137. package/dist/branch-metadata-file.d.ts +16 -32
  138. package/dist/branch-metadata-file.js +13 -26
  139. package/dist/branch-metadata.d.ts +49 -80
  140. package/dist/branch-metadata.js +57 -87
  141. package/dist/branch-registry.d.ts +26 -64
  142. package/dist/branch-registry.js +44 -93
  143. package/dist/branch-schema-cache.d.ts +34 -124
  144. package/dist/branch-schema-cache.js +49 -155
  145. package/dist/branch-workspace.d.ts +4 -4
  146. package/dist/branch-workspace.js +15 -19
  147. package/dist/build/generate-ai-content.d.ts +1 -0
  148. package/dist/build/generate-ai-content.js +12 -25
  149. package/dist/build-canopy.d.ts +17 -27
  150. package/dist/build-canopy.js +16 -23
  151. package/dist/build-mode.d.ts +25 -27
  152. package/dist/build-mode.js +25 -29
  153. package/dist/cli/cli.d.ts +15 -26
  154. package/dist/cli/cli.js +601 -978
  155. package/dist/cli/generate-ai-content.js +351 -628
  156. package/dist/cli/github-app-manifest.d.ts +84 -0
  157. package/dist/cli/github-app-manifest.js +161 -0
  158. package/dist/cli/init-github-app.d.ts +110 -444
  159. package/dist/cli/init-github-app.js +236 -773
  160. package/dist/cli/init.js +39 -80
  161. package/dist/cli/migrate.d.ts +13 -10
  162. package/dist/cli/migrate.js +22 -30
  163. package/dist/cli/project-detect.d.ts +17 -31
  164. package/dist/cli/project-detect.js +31 -42
  165. package/dist/cli/project-root.d.ts +4 -11
  166. package/dist/cli/project-root.js +4 -11
  167. package/dist/cli/prompt.d.ts +10 -0
  168. package/dist/cli/prompt.js +67 -0
  169. package/dist/cli/sync.d.ts +0 -8
  170. package/dist/cli/sync.js +8 -28
  171. package/dist/cli/template-files/middleware.ts.template +15 -29
  172. package/dist/cli/templates.d.ts +2 -12
  173. package/dist/cli/templates.js +4 -4
  174. package/dist/client.d.ts +1 -1
  175. package/dist/comment-store.d.ts +32 -53
  176. package/dist/comment-store.js +32 -52
  177. package/dist/config/flatten.d.ts +0 -3
  178. package/dist/config/flatten.js +2 -12
  179. package/dist/config/helpers.js +2 -2
  180. package/dist/config/index.d.ts +2 -2
  181. package/dist/config/index.js +1 -4
  182. package/dist/config/schemas/collection.d.ts +0 -8
  183. package/dist/config/schemas/collection.js +0 -10
  184. package/dist/config/schemas/config.d.ts +6 -78
  185. package/dist/config/schemas/config.js +38 -54
  186. package/dist/config/schemas/field.d.ts +2 -242
  187. package/dist/config/schemas/field.js +9 -20
  188. package/dist/config/schemas/media.js +1 -1
  189. package/dist/config/schemas/permissions.js +0 -2
  190. package/dist/config/schemas/url.d.ts +5 -6
  191. package/dist/config/schemas/url.js +5 -6
  192. package/dist/config/types.d.ts +45 -33
  193. package/dist/config/types.js +0 -1
  194. package/dist/config.d.ts +1 -8
  195. package/dist/config.js +1 -9
  196. package/dist/content-id-index.d.ts +76 -188
  197. package/dist/content-id-index.js +79 -230
  198. package/dist/content-index-generation.d.ts +27 -48
  199. package/dist/content-index-generation.js +35 -84
  200. package/dist/content-index-registry.d.ts +15 -24
  201. package/dist/content-index-registry.js +5 -9
  202. package/dist/content-listing.d.ts +64 -107
  203. package/dist/content-listing.js +68 -118
  204. package/dist/content-reader.d.ts +20 -29
  205. package/dist/content-reader.js +10 -18
  206. package/dist/content-store.d.ts +210 -336
  207. package/dist/content-store.js +413 -657
  208. package/dist/content-tree.d.ts +44 -86
  209. package/dist/content-tree.js +26 -54
  210. package/dist/context.d.ts +67 -94
  211. package/dist/context.js +53 -72
  212. package/dist/dev-content-watcher.d.ts +18 -18
  213. package/dist/dev-content-watcher.js +41 -44
  214. package/dist/editor/BranchManager.d.ts +3 -6
  215. package/dist/editor/BranchManager.js +24 -44
  216. package/dist/editor/CanopyEditor.js +5 -2
  217. package/dist/editor/CommentsPanel.js +0 -2
  218. package/dist/editor/Editor.js +19 -76
  219. package/dist/editor/EditorAuthGate.d.ts +27 -0
  220. package/dist/editor/EditorAuthGate.js +140 -0
  221. package/dist/editor/EntryNavigator.js +0 -20
  222. package/dist/editor/FormRenderer.d.ts +1 -2
  223. package/dist/editor/FormRenderer.js +8 -23
  224. package/dist/editor/admin/SystemHealthPanel.js +20 -37
  225. package/dist/editor/admin/useSystemHealth.d.ts +2 -3
  226. package/dist/editor/admin/useSystemHealth.js +10 -12
  227. package/dist/editor/client-reference-resolver.d.ts +2 -5
  228. package/dist/editor/client-reference-resolver.js +2 -19
  229. package/dist/editor/comments/BranchComments.js +0 -4
  230. package/dist/editor/comments/EntryComments.js +0 -4
  231. package/dist/editor/comments/FieldWrapper.js +0 -4
  232. package/dist/editor/comments/InlineCommentThread.js +0 -2
  233. package/dist/editor/comments/ThreadCarousel.d.ts +0 -58
  234. package/dist/editor/comments/ThreadCarousel.js +0 -12
  235. package/dist/editor/components/EditorFooter.d.ts +0 -8
  236. package/dist/editor/components/EditorFooter.js +0 -5
  237. package/dist/editor/components/EditorHeader.d.ts +0 -31
  238. package/dist/editor/components/EditorHeader.js +21 -71
  239. package/dist/editor/components/EditorSidebar.d.ts +0 -19
  240. package/dist/editor/components/EditorSidebar.js +0 -16
  241. package/dist/editor/components/EntryCreateModal.d.ts +4 -4
  242. package/dist/editor/components/EntryCreateModal.js +7 -22
  243. package/dist/editor/components/RenameEntryModal.js +0 -4
  244. package/dist/editor/components/UserBadge.js +0 -10
  245. package/dist/editor/components/index.js +0 -2
  246. package/dist/editor/context/ApiClientContext.d.ts +8 -7
  247. package/dist/editor/context/ApiClientContext.js +39 -11
  248. package/dist/editor/context/AssetContext.d.ts +7 -17
  249. package/dist/editor/context/AssetContext.js +7 -17
  250. package/dist/editor/context/EditorIdentityContext.d.ts +13 -0
  251. package/dist/editor/context/EditorIdentityContext.js +7 -0
  252. package/dist/editor/context/EditorStateContext.d.ts +10 -41
  253. package/dist/editor/context/EditorStateContext.js +7 -23
  254. package/dist/editor/context/SWRProvider.d.ts +3 -6
  255. package/dist/editor/context/index.d.ts +1 -20
  256. package/dist/editor/context/index.js +1 -20
  257. package/dist/editor/editor-utils.d.ts +8 -47
  258. package/dist/editor/editor-utils.js +19 -72
  259. package/dist/editor/fields/BlockField.d.ts +2 -2
  260. package/dist/editor/fields/BlockField.js +1 -2
  261. package/dist/editor/fields/CodeField.d.ts +0 -1
  262. package/dist/editor/fields/CodeField.js +0 -1
  263. package/dist/editor/fields/DateTimeField.d.ts +15 -22
  264. package/dist/editor/fields/DateTimeField.js +17 -26
  265. package/dist/editor/fields/ImageField.d.ts +2 -2
  266. package/dist/editor/fields/ImageField.js +8 -14
  267. package/dist/editor/fields/InlineGroupField.d.ts +0 -1
  268. package/dist/editor/fields/InlineGroupField.js +0 -1
  269. package/dist/editor/fields/MarkdownField.d.ts +0 -1
  270. package/dist/editor/fields/MarkdownField.js +6 -11
  271. package/dist/editor/fields/MdxImageDialog.d.ts +1 -1
  272. package/dist/editor/fields/MdxImageDialog.js +0 -1
  273. package/dist/editor/fields/NumberField.d.ts +9 -19
  274. package/dist/editor/fields/NumberField.js +11 -22
  275. package/dist/editor/fields/NumberListField.d.ts +7 -10
  276. package/dist/editor/fields/NumberListField.js +7 -10
  277. package/dist/editor/fields/ObjectField.d.ts +5 -8
  278. package/dist/editor/fields/ObjectField.js +0 -1
  279. package/dist/editor/fields/ReferenceField.d.ts +1 -1
  280. package/dist/editor/fields/ReferenceField.js +11 -16
  281. package/dist/editor/fields/SelectField.d.ts +2 -2
  282. package/dist/editor/fields/SelectField.js +0 -1
  283. package/dist/editor/fields/StringListField.d.ts +0 -4
  284. package/dist/editor/fields/StringListField.js +4 -9
  285. package/dist/editor/fields/TextField.d.ts +0 -1
  286. package/dist/editor/fields/TextField.js +0 -1
  287. package/dist/editor/fields/ToggleField.d.ts +0 -1
  288. package/dist/editor/fields/ToggleField.js +0 -1
  289. package/dist/editor/fields/entry-link/EntryLinkContext.js +2 -4
  290. package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +0 -3
  291. package/dist/editor/fields/entry-link/InsertEntryLink.js +0 -4
  292. package/dist/editor/group-manager/ExternalGroupsTab.d.ts +0 -3
  293. package/dist/editor/group-manager/ExternalGroupsTab.js +0 -1
  294. package/dist/editor/group-manager/GroupCard.d.ts +0 -3
  295. package/dist/editor/group-manager/GroupForm.d.ts +0 -3
  296. package/dist/editor/group-manager/InternalGroupsTab.d.ts +0 -3
  297. package/dist/editor/group-manager/InternalGroupsTab.js +0 -2
  298. package/dist/editor/group-manager/MemberList.d.ts +0 -3
  299. package/dist/editor/group-manager/hooks/useExternalGroupSearch.d.ts +0 -3
  300. package/dist/editor/group-manager/hooks/useExternalGroupSearch.js +0 -3
  301. package/dist/editor/group-manager/hooks/useGroupState.d.ts +0 -3
  302. package/dist/editor/group-manager/hooks/useGroupState.js +0 -5
  303. package/dist/editor/group-manager/hooks/useUserSearch.d.ts +0 -3
  304. package/dist/editor/group-manager/hooks/useUserSearch.js +0 -3
  305. package/dist/editor/group-manager/index.d.ts +0 -6
  306. package/dist/editor/group-manager/index.js +0 -10
  307. package/dist/editor/group-manager/types.d.ts +2 -5
  308. package/dist/editor/group-manager/types.js +0 -3
  309. package/dist/editor/hooks/index.d.ts +6 -11
  310. package/dist/editor/hooks/index.js +6 -12
  311. package/dist/editor/hooks/useBranchActions.d.ts +0 -14
  312. package/dist/editor/hooks/useBranchActions.js +0 -22
  313. package/dist/editor/hooks/useBranchManager.d.ts +2 -48
  314. package/dist/editor/hooks/useBranchManager.js +4 -64
  315. package/dist/editor/hooks/useBranchesData.d.ts +7 -6
  316. package/dist/editor/hooks/useBranchesData.js +7 -6
  317. package/dist/editor/hooks/useCommentSystem.d.ts +0 -49
  318. package/dist/editor/hooks/useCommentSystem.js +1 -36
  319. package/dist/editor/hooks/useDraftManager.d.ts +2 -35
  320. package/dist/editor/hooks/useDraftManager.js +23 -64
  321. package/dist/editor/hooks/useEditorLayout.d.ts +0 -20
  322. package/dist/editor/hooks/useEditorLayout.js +0 -23
  323. package/dist/editor/hooks/useEntriesData.d.ts +1 -0
  324. package/dist/editor/hooks/useEntriesData.js +2 -3
  325. package/dist/editor/hooks/useEntryLinkResolution.d.ts +2 -2
  326. package/dist/editor/hooks/useEntryLinkResolution.js +2 -2
  327. package/dist/editor/hooks/useEntryManager.d.ts +2 -27
  328. package/dist/editor/hooks/useEntryManager.js +29 -78
  329. package/dist/editor/hooks/useGroupManager.d.ts +0 -21
  330. package/dist/editor/hooks/useGroupManager.js +0 -21
  331. package/dist/editor/hooks/usePermissionManager.d.ts +0 -20
  332. package/dist/editor/hooks/usePermissionManager.js +0 -20
  333. package/dist/editor/hooks/useReferenceResolution.d.ts +2 -33
  334. package/dist/editor/hooks/useReferenceResolution.js +1 -48
  335. package/dist/editor/hooks/useSchemaManager.d.ts +8 -27
  336. package/dist/editor/hooks/useSchemaManager.js +0 -19
  337. package/dist/editor/hooks/useUserContext.d.ts +2 -9
  338. package/dist/editor/hooks/useUserContext.js +10 -10
  339. package/dist/editor/hooks/useUserMetadata.d.ts +0 -2
  340. package/dist/editor/hooks/useUserMetadata.js +0 -3
  341. package/dist/editor/media/AssetCard.d.ts +0 -1
  342. package/dist/editor/media/AssetCard.js +1 -2
  343. package/dist/editor/media/CropStep.d.ts +0 -1
  344. package/dist/editor/media/CropStep.js +0 -1
  345. package/dist/editor/media/MediaLibrary.d.ts +1 -2
  346. package/dist/editor/media/MediaLibrary.js +1 -2
  347. package/dist/editor/media/MediaLibraryBody.d.ts +0 -1
  348. package/dist/editor/media/MediaLibraryBody.js +0 -1
  349. package/dist/editor/media/crop-math.d.ts +1 -1
  350. package/dist/editor/media/crop-math.js +1 -1
  351. package/dist/editor/permission-manager/GroupSelector.d.ts +0 -3
  352. package/dist/editor/permission-manager/PermissionEditor.d.ts +0 -4
  353. package/dist/editor/permission-manager/PermissionEditor.js +0 -1
  354. package/dist/editor/permission-manager/PermissionLevelBadge.d.ts +0 -7
  355. package/dist/editor/permission-manager/PermissionLevelBadge.js +0 -4
  356. package/dist/editor/permission-manager/PermissionTree.d.ts +0 -4
  357. package/dist/editor/permission-manager/UserSelector.d.ts +0 -3
  358. package/dist/editor/permission-manager/constants.d.ts +0 -3
  359. package/dist/editor/permission-manager/hooks/useGroupsAndUsers.d.ts +0 -3
  360. package/dist/editor/permission-manager/hooks/useGroupsAndUsers.js +0 -10
  361. package/dist/editor/permission-manager/hooks/usePermissionTree.d.ts +0 -14
  362. package/dist/editor/permission-manager/hooks/usePermissionTree.js +0 -11
  363. package/dist/editor/permission-manager/index.d.ts +0 -7
  364. package/dist/editor/permission-manager/index.js +0 -16
  365. package/dist/editor/permission-manager/types.d.ts +2 -12
  366. package/dist/editor/permission-manager/types.js +0 -3
  367. package/dist/editor/permission-manager/utils.d.ts +0 -28
  368. package/dist/editor/permission-manager/utils.js +9 -23
  369. package/dist/editor/preview-bridge.d.ts +5 -1
  370. package/dist/editor/preview-bridge.js +4 -4
  371. package/dist/editor/schema-editor/CollectionEditor.d.ts +0 -5
  372. package/dist/editor/schema-editor/CollectionEditor.js +9 -31
  373. package/dist/editor/schema-editor/EntryTypeEditor.d.ts +0 -6
  374. package/dist/editor/schema-editor/EntryTypeEditor.js +3 -24
  375. package/dist/editor/schema-editor/index.d.ts +0 -7
  376. package/dist/editor/schema-editor/index.js +0 -7
  377. package/dist/editor/utils/env.d.ts +3 -18
  378. package/dist/editor/utils/env.js +6 -25
  379. package/dist/entry-link-resolver.d.ts +13 -35
  380. package/dist/entry-link-resolver.js +20 -53
  381. package/dist/entry-schema-registry.d.ts +20 -60
  382. package/dist/entry-schema-registry.js +20 -69
  383. package/dist/entry-schema.d.ts +118 -223
  384. package/dist/entry-schema.js +46 -91
  385. package/dist/git-manager.d.ts +101 -200
  386. package/dist/git-manager.js +211 -384
  387. package/dist/github-service.d.ts +34 -75
  388. package/dist/github-service.js +37 -91
  389. package/dist/http/handler.d.ts +6 -37
  390. package/dist/http/handler.js +65 -120
  391. package/dist/http/router.d.ts +15 -32
  392. package/dist/http/router.js +34 -99
  393. package/dist/http/types.d.ts +15 -40
  394. package/dist/http/types.js +3 -7
  395. package/dist/id.d.ts +4 -10
  396. package/dist/id.js +4 -10
  397. package/dist/index.js +6 -9
  398. package/dist/operating-mode/client-safe-strategy.d.ts +7 -14
  399. package/dist/operating-mode/client-safe-strategy.js +8 -28
  400. package/dist/operating-mode/client-unsafe-strategy.d.ts +6 -16
  401. package/dist/operating-mode/client-unsafe-strategy.js +9 -34
  402. package/dist/operating-mode/client.d.ts +3 -10
  403. package/dist/operating-mode/client.js +3 -8
  404. package/dist/operating-mode/deployment-name-fixtures.d.ts +14 -18
  405. package/dist/operating-mode/deployment-name-fixtures.js +14 -18
  406. package/dist/operating-mode/deployment-name.d.ts +19 -29
  407. package/dist/operating-mode/deployment-name.js +29 -47
  408. package/dist/operating-mode/index.d.ts +4 -25
  409. package/dist/operating-mode/index.js +9 -31
  410. package/dist/operating-mode/mode-env.d.ts +32 -54
  411. package/dist/operating-mode/mode-env.js +36 -58
  412. package/dist/operating-mode/types.d.ts +24 -72
  413. package/dist/operating-mode/types.js +1 -7
  414. package/dist/paths/branch-name.d.ts +30 -40
  415. package/dist/paths/branch-name.js +30 -40
  416. package/dist/paths/branch.d.ts +4 -18
  417. package/dist/paths/branch.js +6 -22
  418. package/dist/paths/index.d.ts +2 -13
  419. package/dist/paths/index.js +3 -23
  420. package/dist/paths/normalize-server.d.ts +3 -15
  421. package/dist/paths/normalize-server.js +3 -15
  422. package/dist/paths/normalize.d.ts +13 -33
  423. package/dist/paths/normalize.js +13 -33
  424. package/dist/paths/resolve.d.ts +4 -17
  425. package/dist/paths/resolve.js +4 -22
  426. package/dist/paths/types.d.ts +12 -34
  427. package/dist/paths/types.js +5 -7
  428. package/dist/paths/validation.d.ts +22 -119
  429. package/dist/paths/validation.js +30 -148
  430. package/dist/reference-resolver.d.ts +0 -13
  431. package/dist/reference-resolver.js +0 -18
  432. package/dist/resolve-canopy-user.d.ts +16 -18
  433. package/dist/resolve-canopy-user.js +17 -26
  434. package/dist/resource-generation.d.ts +16 -25
  435. package/dist/resource-generation.js +34 -97
  436. package/dist/schema/meta-loader.d.ts +2 -14
  437. package/dist/schema/meta-loader.js +15 -64
  438. package/dist/schema/resolver.d.ts +1 -4
  439. package/dist/schema/resolver.js +1 -6
  440. package/dist/schema/schema-store-types.d.ts +0 -12
  441. package/dist/schema/schema-store.d.ts +61 -138
  442. package/dist/schema/schema-store.js +85 -282
  443. package/dist/schema/types.d.ts +0 -4
  444. package/dist/server.d.ts +62 -93
  445. package/dist/server.js +63 -94
  446. package/dist/services.d.ts +23 -47
  447. package/dist/services.js +55 -98
  448. package/dist/settings-workspace.d.ts +17 -30
  449. package/dist/settings-workspace.js +51 -81
  450. package/dist/static/index.d.ts +26 -52
  451. package/dist/static/index.js +33 -67
  452. package/dist/{worker/task-queue.d.ts → task-queue/cms-task-queue.d.ts} +7 -9
  453. package/dist/task-queue/cms-task-queue.js +13 -0
  454. package/dist/task-queue/task-queue-config.d.ts +3 -0
  455. package/dist/{worker → task-queue}/task-queue-config.js +1 -6
  456. package/dist/task-queue/task-queue.d.ts +16 -22
  457. package/dist/task-queue/task-queue.js +20 -44
  458. package/dist/task-queue/types.d.ts +0 -1
  459. package/dist/task-queue/worker-status.d.ts +35 -0
  460. package/dist/task-queue/worker-status.js +40 -0
  461. package/dist/types.d.ts +40 -51
  462. package/dist/url-collision.d.ts +26 -40
  463. package/dist/url-collision.js +47 -63
  464. package/dist/url-exclusivity-fixtures.d.ts +15 -20
  465. package/dist/url-exclusivity-fixtures.js +14 -19
  466. package/dist/url-path-resolver.d.ts +8 -16
  467. package/dist/url-path-resolver.js +19 -29
  468. package/dist/user.d.ts +8 -21
  469. package/dist/user.js +10 -19
  470. package/dist/utils/async-mutex.js +4 -6
  471. package/dist/utils/atomic-write.d.ts +2 -11
  472. package/dist/utils/atomic-write.js +2 -11
  473. package/dist/utils/body-field.d.ts +8 -15
  474. package/dist/utils/body-field.js +8 -15
  475. package/dist/utils/content-serialize.d.ts +18 -24
  476. package/dist/utils/content-serialize.js +109 -160
  477. package/dist/utils/content-write-lock.d.ts +51 -106
  478. package/dist/utils/content-write-lock.js +66 -111
  479. package/dist/utils/debug.d.ts +7 -23
  480. package/dist/utils/debug.js +7 -23
  481. package/dist/utils/entry-url.d.ts +11 -23
  482. package/dist/utils/entry-url.js +11 -26
  483. package/dist/utils/error.d.ts +25 -90
  484. package/dist/utils/error.js +60 -147
  485. package/dist/utils/flatten-group-fields.d.ts +5 -7
  486. package/dist/utils/flatten-group-fields.js +5 -7
  487. package/dist/utils/format.d.ts +1 -5
  488. package/dist/utils/format.js +1 -5
  489. package/dist/utils/fs.d.ts +3 -4
  490. package/dist/utils/fs.js +3 -4
  491. package/dist/utils/git.d.ts +52 -83
  492. package/dist/utils/git.js +79 -125
  493. package/dist/utils/logger.d.ts +35 -50
  494. package/dist/utils/logger.js +35 -52
  495. package/dist/utils/provisioning-lock.d.ts +23 -35
  496. package/dist/utils/provisioning-lock.js +49 -69
  497. package/dist/utils/sanitize-href.d.ts +5 -10
  498. package/dist/utils/sanitize-href.js +8 -18
  499. package/dist/utils/title-field.d.ts +17 -20
  500. package/dist/utils/title-field.js +18 -27
  501. package/dist/utils/typed-filename.d.ts +20 -48
  502. package/dist/utils/typed-filename.js +25 -61
  503. package/dist/utils/url-prefix.d.ts +3 -3
  504. package/dist/utils/url-prefix.js +3 -3
  505. package/dist/validation/block-structural-keys.d.ts +16 -31
  506. package/dist/validation/block-structural-keys.js +19 -29
  507. package/dist/validation/deletion-checker.d.ts +0 -34
  508. package/dist/validation/deletion-checker.js +1 -43
  509. package/dist/validation/entry-link-validator.d.ts +6 -13
  510. package/dist/validation/entry-link-validator.js +4 -13
  511. package/dist/validation/entry-type-reference-validator.d.ts +13 -18
  512. package/dist/validation/entry-type-reference-validator.js +13 -18
  513. package/dist/validation/entry-validator.d.ts +13 -25
  514. package/dist/validation/entry-validator.js +15 -32
  515. package/dist/validation/field-traversal.d.ts +3 -30
  516. package/dist/validation/field-traversal.js +1 -32
  517. package/dist/validation/reference-validator.d.ts +5 -24
  518. package/dist/validation/reference-validator.js +5 -30
  519. package/dist/worker/cms-worker.d.ts +172 -232
  520. package/dist/worker/cms-worker.js +241 -333
  521. package/dist/worker/git-sync.d.ts +32 -133
  522. package/dist/worker/git-sync.js +129 -191
  523. package/dist/worker/github-auth.d.ts +13 -19
  524. package/dist/worker/github-auth.js +13 -19
  525. package/dist/worker/history-rewrite.d.ts +53 -66
  526. package/dist/worker/history-rewrite.js +54 -60
  527. package/dist/worker/log.d.ts +24 -41
  528. package/dist/worker/log.js +25 -42
  529. package/dist/worker/rebase.d.ts +20 -110
  530. package/dist/worker/rebase.js +279 -354
  531. package/dist/worker/task-runner.d.ts +32 -66
  532. package/dist/worker/task-runner.js +126 -162
  533. package/dist/worker/worker-context.d.ts +41 -81
  534. package/package.json +4 -4
  535. package/dist/cli/template-files/middleware-clerk.ts.template +0 -37
  536. package/dist/worker/task-queue-config.d.ts +0 -8
  537. package/dist/worker/task-queue.js +0 -19
  538. package/dist/worker/worker-status.d.ts +0 -47
  539. package/dist/worker/worker-status.js +0 -52
@@ -2,46 +2,41 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { simpleGit } from 'simple-git';
4
4
  import lockfile from 'proper-lockfile';
5
- import { recoverOrphanedTasks, cmsTaskQueueLogger } from './task-queue.js';
5
+ import { recoverOrphanedTasks, cmsTaskQueueLogger } from '../task-queue/cms-task-queue.js';
6
6
  import { createCanopyOctokit } from '../github-service.js';
7
7
  import { isTransientAuthFailure, resolveWorkerGitHubAuth, } from './github-auth.js';
8
8
  import { sanitizeBranchName, RESERVED_SETTINGS_BRANCH_PREFIX } from '../paths/branch-name.js';
9
9
  import { resolveDeploymentName } from '../operating-mode/deployment-name.js';
10
10
  import { getErrorMessage, isNodeError, redactCredentials } from '../utils/error.js';
11
- import { writeWorkerStatus } from './worker-status.js';
11
+ import { writeWorkerStatus } from '../task-queue/worker-status.js';
12
12
  import { workerLog, workerLogWarn, workerLogError } from './log.js';
13
13
  import { executeTask, orphanRecoveryMaxAgeMs, processTaskQueue, pushBranchToGitHub, updateBranchMetadata, } from './task-runner.js';
14
14
  import { pollMergeState, runRebaseCycle } from './rebase.js';
15
15
  import { cleanupTrashedBranchDirs, pushSettingsBranches, refreshBaseBranchWorkspace, syncGit, } from './git-sync.js';
16
16
  // Re-exported because this module is the package's advertised worker
17
- // entrypoint (`canopycms/worker/cms-worker`) and both were exported from here
18
- // before the task-queue cluster moved to ./task-runner. cms-worker.test.ts
19
- // imports them from this path.
17
+ // entrypoint (`canopycms/worker/cms-worker`).
20
18
  export { PermanentTaskError, isPermanentTaskFailure } from './task-runner.js';
21
19
  // Re-exported so the AWS entrypoint (packages/canopycms-cdk/worker/index.ts)
22
- // can prefix its own startup lines through the same helpers without adding a
23
- // new package entrypoint - `canopycms/worker/cms-worker` already exists. Every
24
- // line in worker.log must carry the timestamp prefix or it gets folded into
25
- // the previous CloudWatch event; see ./log.ts.
20
+ // can prefix its own startup lines through the same helpers without a new
21
+ // package entrypoint. Every line in worker.log must carry the timestamp prefix
22
+ // or it is folded into the previous CloudWatch event; see ./log.ts.
26
23
  export { workerLog, workerLogWarn, workerLogError, installWorkerLogger } from './log.js';
27
- // Re-exported for the same reason: an entrypoint that authenticates as a
28
- // GitHub App builds the credential itself (core must not import
29
- // `@octokit/auth-app` — see github-auth.ts) and needs the shape to inject and
30
- // the key normalizer to apply, without a new package entrypoint.
24
+ // Re-exported for the same reason: an entrypoint that authenticates as a GitHub
25
+ // App builds the credential itself (core must not import `@octokit/auth-app` —
26
+ // see github-auth.ts) and needs the shape to inject and the key normalizer to
27
+ // apply.
31
28
  export { normalizeGitHubAppPrivateKey, DEFAULT_GIT_TOKEN_MINT_TIMEOUT_MS, DEFAULT_GITHUB_TOKEN_REFRESH_MIN_INTERVAL_MS, } from './github-auth.js';
32
29
  const DEFAULT_TASK_TIMEOUT = 60_000;
33
30
  const DEFAULT_MAX_RETRIES = 3;
34
31
  const DEFAULT_LOCK_STALE_MS = 60_000;
35
32
  /**
36
- * CMS Worker daemon.
37
- * Handles operations that Lambda (with no internet) cannot perform:
38
- * - Processing queued tasks (push branches, create PRs)
39
- * - Syncing bare repo with GitHub
40
- * - Rebasing active branch workspaces
41
- * - Refreshing auth metadata cache (via pluggable callback)
33
+ * CMS Worker daemon: the operations Lambda, which has no internet, cannot
34
+ * perform -- draining the task queue, syncing the bare repo with GitHub,
35
+ * rebasing branch workspaces, and refreshing the auth metadata cache through a
36
+ * pluggable callback.
42
37
  *
43
- * Auth-agnostic: does not depend on any specific auth provider.
44
- * Cloud-agnostic: uses git/Octokit directly, no AWS SDK dependency.
38
+ * Auth-agnostic (no specific auth provider) and cloud-agnostic (git/Octokit
39
+ * directly, no AWS SDK dependency).
45
40
  */
46
41
  export class CmsWorker {
47
42
  constructor(config) {
@@ -64,12 +59,10 @@ export class CmsWorker {
64
59
  this.contentRoot = config.contentRoot ?? 'content';
65
60
  }
66
61
  /**
67
- * Lazily get (and initialize if necessary) this worker's self-reported
68
- * status object (PR-W1). Normally set once, up front, at the top of
69
- * start(). The lazy fallback here covers two cases: (1) something in
70
- * start() reaching a status-write point before that normal init runs
71
- * (defensive -- see start()'s catch), and (2) unit tests that exercise
72
- * syncGit()/processTaskQueue() directly without calling start() first.
62
+ * This worker's self-reported status object, initialized on first use.
63
+ * Normally set at the top of start(); the lazy fallback covers something in
64
+ * start() reaching a status-write point before that (see start()'s catch) and
65
+ * unit tests driving syncGit()/processTaskQueue() without calling start().
73
66
  */
74
67
  ensureStatusReport() {
75
68
  if (!this.statusReport) {
@@ -82,28 +75,23 @@ export class CmsWorker {
82
75
  * Resolve this deployment's settings branch, throwing if the infra-stamped
83
76
  * deployment name is not a valid git ref component.
84
77
  *
85
- * Routed through the shared resolver, not a local `?? 'prod'`: it is the
78
+ * Routed through the shared resolver, not a local `?? 'prod'`: that is the
86
79
  * single definition of the env > config > mode-default precedence the Lambda
87
- * already follows, and the only place the resolved value is validated.
88
- * Without it the worker could silently own a different settings branch than
89
- * the Lambda writing to the same workspace -- and pushSettingsBranches would
90
- * then report the real branch as foreign and never push it.
91
- *
92
- * DEFERRED out of the constructor deliberately, and this is the whole point
93
- * of the method existing. Resolving there made the throw land during `new
94
- * CmsWorker(...)` (canopycms-cdk/worker/index.ts constructs before it calls
95
- * start()), which is BEFORE the only code that writes `lastFatalError` --
96
- * start()'s catch. The result was billed as "a loud startup exit" but was
97
- * nothing of the kind: with systemd `Type=simple` + `Restart=always` and no
98
- * cfn-signal, an invalid deployment name produced an invisible ~5s
99
- * crash-loop that `cdk deploy` reported as success while the admin panel
100
- * showed the worker as 'absent' with no fatal error to explain it. Resolving
101
- * inside start()'s try block instead means the same throw is recorded to
102
- * worker-status.json and surfaces in the admin panel.
103
- *
104
- * Lazy rather than start()-only so the value is still available to unit tests
105
- * that drive pushSettingsBranches() directly without calling start() -- the
106
- * same reason ensureStatusReport() above is shaped this way. Idempotent: the
80
+ * follows, and the only place the resolved value is validated. Without it the
81
+ * worker can silently own a different settings branch than the Lambda writing
82
+ * to the same workspace, and pushSettingsBranches then reports the real branch
83
+ * as foreign and never pushes it.
84
+ *
85
+ * DEFERRED out of the constructor deliberately, which is the whole point of
86
+ * the method existing: canopycms-cdk/worker/index.ts constructs the worker
87
+ * before calling start(), so a throw during `new CmsWorker(...)` lands BEFORE
88
+ * the only code that writes `lastFatalError` -- start()'s catch. Under
89
+ * systemd `Type=simple` + `Restart=always` with no cfn-signal, that is an
90
+ * invisible ~5s crash-loop that `cdk deploy` reports as success while the
91
+ * admin panel shows the worker 'absent' with no fatal error to explain it.
92
+ *
93
+ * Lazy rather than start()-only so unit tests driving pushSettingsBranches()
94
+ * still see the value, exactly as ensureStatusReport() above. Idempotent: the
107
95
  * resolver is pure, so a later call returns the identical string.
108
96
  */
109
97
  ensureSettingsBranch() {
@@ -118,15 +106,11 @@ export class CmsWorker {
118
106
  * Build the {@link WorkerContext} handed to the extracted clusters
119
107
  * (task-runner.ts, git-sync.ts, rebase.ts, history-rewrite.ts).
120
108
  *
121
- * Built FRESH on every call rather than once in the constructor, and the
122
- * instance-backed members are functions rather than copied values. Both
123
- * choices exist for the same reason: the test suite drives this class by
124
- * replacing `octokit` and `buildGitHubUrl` ON THE INSTANCE, by setting
125
- * `running` directly, and by subclassing to override the two rebase test
126
- * hooks. A context that captured any of those at construction time would hand
127
- * the extracted code the pre-test value -- which for `buildGitHubUrl` means a
128
- * test's push going to github.com for real instead of its local fixture repo.
129
- * See WorkerContext's doc comment for the full list.
109
+ * Built FRESH on every call, with instance-backed members as functions rather
110
+ * than copied values. See WorkerContext's INVARIANT: a context that captured
111
+ * any of them at construction time would hand the extracted code the pre-test
112
+ * value, which for `buildGitHubUrl` means a test's push going to github.com
113
+ * for real instead of its local fixture repo.
130
114
  */
131
115
  ctx() {
132
116
  return {
@@ -159,49 +143,37 @@ export class CmsWorker {
159
143
  this.running = true;
160
144
  workerLog('CMS Worker starting...');
161
145
  this.ensureStatusReport();
162
- // Acquire lock to prevent concurrent workers
163
146
  await this.acquireLock();
164
- // Everything below runs while holding the cross-host worker lock. A
165
- // failure here (most notably the empty-remote guard inside
166
- // ensureRemoteGit) means the process is about to exit, and systemd
167
- // (Restart=always) will retry — but a still-held lock would make every
168
- // retry fail with ELOCKED for up to lockStaleMs. Release before
169
- // rethrowing so the next start() (this process's retry, or another host)
170
- // can acquire immediately. We deliberately do NOT reorder ensureRemoteGit
147
+ // Everything below runs while holding the cross-host worker lock. A failure
148
+ // here (most notably the empty-remote guard inside ensureRemoteGit) means
149
+ // the process is about to exit and systemd (Restart=always) will retry —
150
+ // but a still-held lock would make every retry fail with ELOCKED for up to
151
+ // lockStaleMs, so release before rethrowing. Do NOT reorder ensureRemoteGit
171
152
  // ahead of acquireLock: two hosts cold-starting at once would then both
172
- // race to `git clone --bare` into the same remoteGitPath (the same class
173
- // of race that acquireProvisioningLock guards against for workspace
174
- // clones elsewhere) — acquiring the lock first is what already
175
- // serializes that.
153
+ // race to `git clone --bare` into the same remoteGitPath, and acquiring the
154
+ // lock first is what serializes that.
176
155
  try {
177
156
  // FIRST inside the try, before any I/O: an infra-stamped deployment name
178
- // that is not a valid git ref component throws here, where the catch
179
- // below records it to worker-status.json. Resolving it in the
180
- // constructor (as this used to) put the throw outside every
181
- // status-writing path -- see ensureSettingsBranch()'s doc comment.
157
+ // that is not a valid git ref component throws HERE, where the catch
158
+ // below records it to worker-status.json. See ensureSettingsBranch().
182
159
  this.ensureSettingsBranch();
183
- // Same reasoning as the line above, and the same shape: a
184
- // half-configured credential throws HERE, inside the try, rather than
185
- // out of `new CmsWorker(...)` where nothing could record it.
160
+ // Same shape, same reason: a half-configured credential throws HERE,
161
+ // inside the try, rather than out of `new CmsWorker(...)` where nothing
162
+ // could record it.
186
163
  this.ensureGitHubAuth();
187
164
  // BEFORE ensureRemoteGit(): its clone is the first thing to use the
188
165
  // credential, and its catch blames the repository rather than the
189
166
  // credential. See preflightGitHubAppAuth().
190
167
  await this.preflightGitHubAppAuth();
191
- // Ensure remote.git exists (init bare repo if first run)
192
168
  await this.ensureRemoteGit();
193
- // Recover any orphaned tasks from a previous crash, immediately rather
194
- // than waiting for the first processTaskQueue() poll (taskPollInterval,
195
- // default 5s) to run it. Not the only call site any more -
196
- // processTaskQueue() below now repeats this on every cycle; see its
197
- // doc comment for why a boot-only call is insufficient once the
198
- // worker's ASG rolls on every `cdk deploy` (CanopyCmsService's
199
- // UpdatePolicy).
169
+ // Recover orphaned tasks immediately rather than waiting for the first
170
+ // processTaskQueue() poll. Not the only call site: processTaskQueue()
171
+ // repeats this every cycle, and its doc comment says why a boot-only call
172
+ // is insufficient.
200
173
  const recovered = await recoverOrphanedTasks(this.taskDir, orphanRecoveryMaxAgeMs(this.ctx()), this.log);
201
174
  if (recovered > 0) {
202
175
  workerLog(`Recovered ${recovered} orphaned task(s)`);
203
176
  }
204
- // Run initial sync + cache refresh immediately
205
177
  const initialTasks = [this.syncGit()];
206
178
  if (this.config.refreshAuthCache) {
207
179
  initialTasks.push(this.refreshAuthCache());
@@ -209,15 +181,15 @@ export class CmsWorker {
209
181
  await Promise.allSettled(initialTasks);
210
182
  }
211
183
  catch (err) {
212
- // PR-W1: surface a startup failure (e.g. the empty-remote guard's
213
- // poisoned remote.git) to the admin panel via worker-status.json,
214
- // not only journald/CloudWatch. Best-effort and BEFORE releaseLock():
215
- // a status-write failure must never block releasing the lock.
184
+ // Surface a startup failure (e.g. the empty-remote guard's poisoned
185
+ // remote.git) to the admin panel via worker-status.json, not only
186
+ // journald/CloudWatch. Best-effort and BEFORE releaseLock(): a
187
+ // status-write failure must never block releasing the lock.
216
188
  const report = this.ensureStatusReport();
217
189
  report.lastFatalError = {
218
- // [REDACT] Persisted to worker-status.json and served to the
219
- // browser by the admin panel -- must never carry the bot token
220
- // that a poisoned/failed git URL (buildGitHubUrl()) can embed.
190
+ // [REDACT] Persisted to worker-status.json and served to the browser by
191
+ // the admin panel -- must never carry the bot token a poisoned or
192
+ // failed git URL (buildGitHubUrl()) can embed.
221
193
  message: redactCredentials(getErrorMessage(err)),
222
194
  at: new Date().toISOString(),
223
195
  phase: 'startup',
@@ -231,15 +203,13 @@ export class CmsWorker {
231
203
  await this.releaseLock();
232
204
  throw err;
233
205
  }
234
- // Start recurring task loops using setTimeout chaining
235
- // (avoids setInterval overlap when tasks take longer than the interval)
236
206
  const taskInterval = this.config.taskPollInterval ?? 5_000;
237
207
  const gitInterval = this.config.gitSyncInterval ?? 5 * 60_000;
238
208
  this.scheduleLoop(() => this.processTaskQueue(), taskInterval);
239
209
  // The wrapper, not syncGit() itself: a failed sync is where a rotated or
240
- // revoked GitHub credential is noticed. See its doc comment. start()'s own
241
- // initial syncGit() above stays unwrapped -- there is no stale credential
242
- // to refresh one line after reading it at boot.
210
+ // revoked GitHub credential is noticed. start()'s own initial syncGit()
211
+ // above stays unwrapped -- there is no stale credential to refresh one line
212
+ // after reading it at boot.
243
213
  this.scheduleLoop(() => this.syncGitWithCredentialRefresh(), gitInterval);
244
214
  if (this.config.refreshAuthCache) {
245
215
  const cacheInterval = this.config.authCacheRefreshInterval ?? 15 * 60_000;
@@ -256,7 +226,6 @@ export class CmsWorker {
256
226
  clearTimeout(t);
257
227
  }
258
228
  this.activeTimeouts.clear();
259
- // Wait for all in-flight operations to complete (up to taskTimeoutMs)
260
229
  let drainTimer;
261
230
  await Promise.race([
262
231
  Promise.allSettled([...this.activeOperations]),
@@ -272,26 +241,23 @@ export class CmsWorker {
272
241
  * Acquire the cross-host worker lock (DEP-C2).
273
242
  *
274
243
  * The task queue is single-consumer (see task-queue/task-queue.ts): two
275
- * concurrent workers would double-process tasks (duplicate pushes, duplicate
276
- * PRs). The workspace lives on a shared filesystem (EFS), so mutual
277
- * exclusion must be sound ACROSS HOSTS — a PID liveness probe
278
- * (`process.kill(pid, 0)`) only means something on the holder's own machine
279
- * and must never participate in staleness decisions.
244
+ * concurrent workers would double-process tasks, duplicating pushes and PRs.
245
+ * The workspace lives on EFS, so mutual exclusion must be sound ACROSS HOSTS
246
+ * — a PID liveness probe (`process.kill(pid, 0)`) means something only on the
247
+ * holder's own machine and must NEVER participate in staleness decisions.
280
248
  *
281
249
  * proper-lockfile provides a heartbeat lease with no PID involved: the lock
282
250
  * is a directory created atomically (mkdir — atomic on NFS/EFS), the holder
283
- * refreshes its mtime every lockStaleMs/2, and the lock is considered
284
- * abandoned — and taken over — only when that heartbeat is older than
285
- * lockStaleMs. Liveness is judged purely by heartbeat freshness.
251
+ * refreshes its mtime every lockStaleMs/2, and the lock is abandoned, and
252
+ * taken over, only once that heartbeat is older than lockStaleMs.
286
253
  *
287
254
  * No acquire retries: a second worker exits immediately, matching daemon
288
- * semantics (the supervisor restarts it later). After a crash, the dead
289
- * holder's heartbeat expires within lockStaleMs and the next start succeeds.
255
+ * semantics. After a crash the dead holder's heartbeat expires within
256
+ * lockStaleMs and the next start succeeds.
290
257
  *
291
- * Staleness is judged by comparing the lock's mtime against the local
292
- * clock, so correct cross-host takeover assumes reasonable clock agreement
293
- * between hosts (e.g. NTP); with the default TTL, ordinary clock skew is
294
- * negligible, but a host with a badly wrong clock could misjudge liveness.
258
+ * Staleness compares the lock's mtime against the LOCAL clock, so correct
259
+ * cross-host takeover assumes reasonable clock agreement (NTP). Ordinary skew
260
+ * is negligible at the default TTL; a badly wrong clock misjudges liveness.
295
261
  */
296
262
  async acquireLock() {
297
263
  await fs.mkdir(this.taskDir, { recursive: true });
@@ -300,9 +266,9 @@ export class CmsWorker {
300
266
  lockfilePath: this.lockFilePath,
301
267
  stale: this.lockStaleMs,
302
268
  onCompromised: (err) => {
303
- // Our heartbeat could not be maintained (lock deleted or taken
304
- // over). Another worker may now be consuming the queue — stop
305
- // processing to preserve the single-consumer invariant.
269
+ // The heartbeat could not be maintained (lock deleted or taken over),
270
+ // so another worker may now be consuming the queue: stop processing
271
+ // to preserve the single-consumer invariant.
306
272
  workerLogError('Worker lock compromised, shutting down:', getErrorMessage(err));
307
273
  this.releaseLockFn = null; // the lock is already lost; nothing to release
308
274
  void this.stop();
@@ -329,9 +295,9 @@ export class CmsWorker {
329
295
  }
330
296
  }
331
297
  /**
332
- * Schedule a function to run repeatedly with setTimeout chaining.
333
- * The next invocation starts `interval` ms after the previous one completes,
334
- * preventing overlapping executions.
298
+ * Run `fn` repeatedly, the next invocation starting `interval` ms after the
299
+ * previous one COMPLETES. setTimeout chaining rather than setInterval, so
300
+ * executions cannot overlap when one runs longer than the interval.
335
301
  */
336
302
  scheduleLoop(fn, interval) {
337
303
  const run = () => {
@@ -354,18 +320,16 @@ export class CmsWorker {
354
320
  /**
355
321
  * Whether the bare repo at `gitDir` has a local `refs/heads/<baseBranch>`.
356
322
  *
357
- * Uses an explicit `--git-dir` invocation rather than `simpleGit({ baseDir
358
- * })` so this also works in sandboxed/CI git environments that set
359
- * `safe.bareRepository=explicit` (which refuses cwd-based discovery of
360
- * bare repos but expressly allows `--git-dir` — see
361
- * GitManager.bareRemoteHasBranch for the same pattern).
323
+ * Explicit `--git-dir` rather than `simpleGit({ baseDir })`, so this also
324
+ * works where `safe.bareRepository=explicit` refuses cwd-based discovery of
325
+ * bare repos but expressly allows `--git-dir` (same pattern as
326
+ * GitManager.bareRemoteHasBranch).
362
327
  *
363
- * Deliberately omits `--quiet`: simple-git only treats a task as failed
364
- * when the process both exits non-zero AND writes to stderr
365
- * (isTaskError), so a quiet, silent-on-failure `--verify` would leave a
366
- * missing branch indistinguishable from success. Without `--quiet`,
367
- * `rev-parse --verify` writes its "fatal: ..." to stderr on failure, which
368
- * is what makes simple-git reject the promise here.
328
+ * Deliberately omits `--quiet`: simple-git treats a task as failed only when
329
+ * the process exits non-zero AND writes to stderr, so a silent-on-failure
330
+ * `--verify` would leave a missing branch indistinguishable from success.
331
+ * Without `--quiet`, `rev-parse --verify` writes "fatal: ..." to stderr,
332
+ * which is what makes simple-git reject the promise here.
369
333
  */
370
334
  async verifyBaseBranchExists(gitDir) {
371
335
  await simpleGit().raw([
@@ -382,32 +346,27 @@ export class CmsWorker {
382
346
  *
383
347
  * `git clone https://x-access-token:<token>@github.com/...` records that URL
384
348
  * verbatim as `remote.origin.url`, and for `remote.git` that config lives on
385
- * shared EFS. The security model in docs/deploying-to-aws.md -- "If Lambda is
386
- * compromised, an attacker can read/write content on EFS but cannot push to
387
- * GitHub", "Secrets stay on the worker" -- is false while that string is
388
- * there: a compromised Lambda could read the token off EFS and, despite
389
- * having no egress of its own, exfiltrate it by writing it into branch
390
- * content the worker then pushes to GitHub.
349
+ * shared EFS. The security model in docs/deploying-to-aws.md -- a compromised
350
+ * Lambda can read/write EFS content but cannot push to GitHub, secrets stay
351
+ * on the worker -- is false while that string is there: the Lambda could read
352
+ * the token off EFS and, with no egress of its own, exfiltrate it by writing
353
+ * it into branch content the worker then pushes to GitHub.
391
354
  *
392
355
  * Nothing needs the remote: every push passes the URL explicitly as an
393
- * argument (see the `git.push(this.buildGitHubUrl(), ...)` call sites), and
394
- * `verifyBaseBranchExists` reads local refs.
356
+ * argument, and `verifyBaseBranchExists` reads local refs.
395
357
  *
396
358
  * VERIFIES rather than assuming: it re-reads the config and throws if the URL
397
- * survives, because the previous code's `.catch(() => {})` meant a failed
398
- * scrub was indistinguishable from a successful one.
359
+ * survives, so a failed scrub is never indistinguishable from a clean one.
399
360
  */
400
361
  async scrubPersistedRemote(gitDir) {
401
362
  const git = simpleGit({ baseDir: gitDir });
402
- // `git config --get` exits 1 with no output when the key is absent.
403
- // simple-git does NOT reliably throw on that -- verified against
404
- // simple-git 3.36: it resolves with an empty string -- so an empty result
405
- // must be read as "absent" too. Treating "" as a surviving URL is what
406
- // made the first version of this reject every clean scrub.
363
+ // `git config --get` exits 1 with no output when the key is absent, and
364
+ // simple-git resolves with an empty string rather than throwing (verified
365
+ // against 3.36), so an empty result means "absent" too.
407
366
  //
408
- // 'unreadable' is deliberately distinct from 'absent'. A read that fails
409
- // for any OTHER reason must not be mistaken for "no token here": that
410
- // would let the pre-check below short-circuit and skip the scrub entirely,
367
+ // 'unreadable' is deliberately DISTINCT from 'absent'. A read that fails
368
+ // for any other reason must not be mistaken for "no token here": that would
369
+ // let the pre-check below short-circuit and skip the scrub entirely,
411
370
  // silently leaving a token-bearing config on shared EFS -- the exact
412
371
  // outcome this function exists to prevent. Fail closed and attempt the
413
372
  // removal instead.
@@ -418,17 +377,12 @@ export class CmsWorker {
418
377
  }
419
378
  catch {
420
379
  // ANY throw is 'unreadable', never 'absent'. The genuinely-absent case
421
- // does not reach here at all -- simple-git resolves with '' (verified
422
- // against 3.36: it only treats a task as failed when stderr is
423
- // non-empty, and a missing key writes nothing to stderr). So a throw
380
+ // does not reach here at all (simple-git resolves with ''), so a throw
424
381
  // means something actually went wrong, and mapping that to "no token
425
- // here" would be the one fail-OPEN reading available.
426
- //
427
- // An earlier version tried to classify git's exit-1 "key not found"
428
- // from the message text. That was dead code -- simple-git's GitError
429
- // message is raw stdout+stderr with no exit-code text -- and its
430
- // empty-message fallback mapped a hypothetical throw to 'absent',
431
- // which is exactly the direction this must not fail.
382
+ // here" is the one fail-OPEN reading available. Classifying git's
383
+ // exit-1 "key not found" from the message text is not an option either:
384
+ // simple-git's GitError message is raw stdout+stderr with no exit-code
385
+ // text to match on.
432
386
  return 'unreadable';
433
387
  }
434
388
  };
@@ -443,12 +397,12 @@ export class CmsWorker {
443
397
  }
444
398
  catch (err) {
445
399
  // Reached only from the 'unreadable' path, where the remote may in fact
446
- // not exist. Let the verification below decide rather than failing here:
447
- // it is the authoritative check, and it fails closed.
400
+ // not exist. Let the verification below decide rather than failing here;
401
+ // it is the authoritative check and it fails closed.
448
402
  workerLogWarn(` removeRemote('origin') failed in ${gitDir}: ${getErrorMessage(err)} -- verifying directly`);
449
403
  }
450
- // Fails closed on BOTH a surviving URL and an unverifiable read: if we
451
- // cannot prove the token is gone from shared storage, we do not proceed.
404
+ // Fails closed on BOTH a surviving URL and an unverifiable read: without
405
+ // proof the token is gone from shared storage, do not proceed.
452
406
  const remaining = await readOriginUrl();
453
407
  if (remaining !== null) {
454
408
  throw new Error(`Could not confirm the 'origin' remote is gone from ${gitDir} (${remaining === 'unreadable'
@@ -458,19 +412,17 @@ export class CmsWorker {
458
412
  }
459
413
  }
460
414
  /**
461
- * Ensure remote.git bare repo exists.
462
- * On first run, clone from GitHub as a bare repo.
415
+ * Ensure the remote.git bare repo exists, cloning it from GitHub on first
416
+ * run.
463
417
  *
464
418
  * Empty-remote guard: simple-git's bare clone of an EMPTY GitHub repo (no
465
- * commits, or a base branch that's never been pushed) exits 0 and produces
466
- * a refs-less bare repo — HEAD points at an unborn branch. `fs.stat`
467
- * cannot distinguish this from a healthy clone, so left unchecked it
468
- * silently poisons remote.git: every later branch operation (Lambda-side
469
- * clone provisioning, worker pushes) breaks, and the fs.stat short-circuit
470
- * means it never heals on its own. We verify the base branch exists right
471
- * after cloning and again on the already-exists fast path, since a
472
- * previous run could have left a poisoned remote.git behind before this
473
- * guard existed.
419
+ * commits, or a base branch never pushed) exits 0 and produces a refs-less
420
+ * bare repo whose HEAD points at an unborn branch. `fs.stat` cannot tell that
421
+ * from a healthy clone, so left unchecked it silently poisons remote.git —
422
+ * every later branch operation breaks and the fs.stat short-circuit means it
423
+ * never heals. The base branch is therefore verified right after cloning AND
424
+ * on the already-exists fast path, since a previous run can have left a
425
+ * poisoned remote.git behind.
474
426
  */
475
427
  async ensureRemoteGit() {
476
428
  let exists;
@@ -482,32 +434,30 @@ export class CmsWorker {
482
434
  exists = false;
483
435
  }
484
436
  if (exists) {
485
- // SELF-HEAL, before anything else touches this repo. The previous
486
- // already-exists path fast-returned without ever re-checking the config,
487
- // so a token that survived one scrub survived forever -- and a clone
488
- // interrupted by SIGKILL/power-off between `git clone` and the scrub left
489
- // a repo whose config already held the token, which additionally hit the
490
- // "delete remote.git and restart" refusal below and so sat on EFS until
491
- // an operator acted.
437
+ // SELF-HEAL, before anything else touches this repo: re-checked on every
438
+ // boot, not only at clone time, so a token that survived one scrub does
439
+ // not survive forever, and a clone interrupted between `git clone` and
440
+ // the scrub cannot leave a token-bearing config sitting on EFS until an
441
+ // operator acts.
492
442
  await this.scrubPersistedRemote(this.remoteGitPath);
493
443
  try {
494
444
  await this.verifyBaseBranchExists(this.remoteGitPath);
495
445
  }
496
446
  catch (err) {
497
447
  workerLogError(`remote.git base branch verification failed: ${getErrorMessage(err)}`);
498
- // Do NOT auto-delete: an existing remote.git could hold unpushed
499
- // canopycms-settings-* branches or other state worth preserving.
500
- // Deletion here is the operator's call, not ours.
448
+ // Do NOT auto-delete: an existing remote.git can hold unpushed
449
+ // canopycms-settings-* branches or other state worth preserving, so
450
+ // deletion is the operator's call.
501
451
  throw new Error(`remote.git at ${this.remoteGitPath} has no branch '${this.baseBranch}' (likely cloned while the GitHub repo was empty). Delete ${this.remoteGitPath} and restart the worker to re-clone.`);
502
452
  }
503
453
  return; // Already exists and has the base branch
504
454
  }
505
455
  workerLog('Initializing remote.git from GitHub...');
506
456
  const git = simpleGit();
507
- // Clone under a TEMP name and rename into place only once the token has
508
- // been scrubbed and the repo verified, so `remote.git` never exists on EFS
509
- // in a token-bearing state. A crash mid-clone now leaves only this staging
510
- // directory, which the next boot deletes -- rather than a poisoned
457
+ // Clone under a TEMP name and rename into place only once the token is
458
+ // scrubbed and the repo verified, so `remote.git` never exists on EFS in a
459
+ // token-bearing state. A crash mid-clone leaves only this staging
460
+ // directory, which the next boot deletes, rather than a poisoned
511
461
  // `remote.git` that fs.stat cannot distinguish from a healthy one.
512
462
  const stagingPath = `${this.remoteGitPath}.cloning`;
513
463
  await fs.rm(stagingPath, { recursive: true, force: true });
@@ -522,8 +472,8 @@ export class CmsWorker {
522
472
  catch (err) {
523
473
  workerLogError(`remote.git clone failed: ${redactCredentials(getErrorMessage(err))}`);
524
474
  // Deleting before throwing is what makes this recoverable: the next
525
- // start() sees no remote.git and re-clones, instead of being stuck
526
- // forever behind a poisoned bare repo that fs.stat alone can't detect.
475
+ // start() sees no remote.git and re-clones, instead of sticking forever
476
+ // behind a poisoned bare repo fs.stat alone cannot detect.
527
477
  await fs.rm(stagingPath, { recursive: true, force: true });
528
478
  throw new Error(`remote.git clone of ${this.config.githubOwner}/${this.config.githubRepo} failed or has no branch '${this.baseBranch}' - the GitHub repository may be empty, or the base branch may not exist. Push an initial commit to '${this.baseBranch}' and restart the worker (systemd will retry automatically).`);
529
479
  }
@@ -532,24 +482,18 @@ export class CmsWorker {
532
482
  }
533
483
  // --- Task-queue cluster (worker/task-runner.ts) ------------------------
534
484
  //
535
- // READ THIS BEFORE STUBBING ANY DELEGATOR BELOW. They are not all the same,
536
- // and the difference is invisible from here.
537
- //
538
- // `processTaskQueue` is the public loop entry `scheduleLoop` drives, so it IS
539
- // on the production path. The other three exist ONLY so the existing test
540
- // files can reach the implementations through the instance; production never
541
- // dispatches through them, because the extracted modules call each other
542
- // directly at module level.
485
+ // READ THIS BEFORE STUBBING ANY DELEGATOR BELOW: they are not all the same,
486
+ // and the difference is invisible from here. `processTaskQueue` is the public
487
+ // loop entry `scheduleLoop` drives, so it IS on the production path; the
488
+ // other three exist ONLY so test files can reach the implementations through
489
+ // the instance, since the extracted modules call each other at module level.
543
490
  //
544
- // The consequence: replacing one of these on an instance only affects
545
- // production behaviour if the context also routes it. Two do --
546
- // `executeTask` and `pushBranchToGitHub` are on `WorkerContext` precisely
547
- // because cms-worker.test.ts assigns over them and then asserts on the
548
- // replacement. `updateBranchMetadata` is NOT: the test calls it directly and
549
- // never stubs it, so a stub installed there today would be a silent no-op.
550
- // If you need to stub a method that isn't on the context, add it to
551
- // WorkerContext and route the internal caller through `ctx` -- do not assume
552
- // the delegator's existence means the stub takes effect.
491
+ // So replacing one of these on an instance affects production behaviour only
492
+ // if the context also routes it. `executeTask` and `pushBranchToGitHub` are
493
+ // on `WorkerContext` for exactly that reason; `updateBranchMetadata` is NOT,
494
+ // so a stub installed there is a silent no-op. To stub a method that is not
495
+ // on the context, add it to WorkerContext and route the internal caller
496
+ // through `ctx` -- a delegator's existence does not make a stub take effect.
553
497
  async processTaskQueue() {
554
498
  return processTaskQueue(this.ctx());
555
499
  }
@@ -567,16 +511,11 @@ export class CmsWorker {
567
511
  * Octokit client from it.
568
512
  *
569
513
  * DEFERRED out of the constructor deliberately, exactly as
570
- * `ensureSettingsBranch()` is, and for the same reason that method records:
514
+ * `ensureSettingsBranch()` is and for the reason that method records:
571
515
  * `resolveWorkerGitHubAuth` throws for a half-configured credential (both
572
- * set, neither set, an unusable mint timeout or refresh interval), and a throw during
573
- * `new CmsWorker(...)` lands BEFORE the only code that writes
574
- * `lastFatalError` — start()'s catch. The AWS entrypoint constructs the
575
- * worker and calls start() separately, and its `main().catch()` only logs
576
- * and exits, so a constructor throw is an invisible ~5s systemd crash-loop
577
- * that `cdk deploy` reports as success while the admin panel shows the
578
- * worker 'absent' with no fatal error to explain it. That is a shipped
579
- * regression this codebase has already paid for once (#198).
516
+ * set, neither set, an unusable mint timeout or refresh interval), and a
517
+ * throw during `new CmsWorker(...)` lands BEFORE the only code that writes
518
+ * `lastFatalError` — start()'s catch.
580
519
  *
581
520
  * Idempotent, and it does NOT replace an `octokit` a test has already
582
521
  * assigned onto the instance — see the field's comment.
@@ -591,34 +530,29 @@ export class CmsWorker {
591
530
  return this.githubAuth;
592
531
  }
593
532
  /**
594
- * The Octokit client, built on first use.
595
- *
596
- * Every read goes through here rather than touching the field, because the
597
- * field is no longer populated by the constructor: a method reached without
598
- * start() would otherwise see `undefined`. `rebaseActiveBranches()` is
599
- * exactly that case — apps/test-app's e2e route calls it directly, and its
600
- * `pollMergeState` dispatch reads `ctx.octokit()`.
533
+ * The Octokit client, built on first use. Every read goes through here rather
534
+ * than touching the field, which the constructor does not populate: a method
535
+ * reached without start() would otherwise see `undefined`.
536
+ * `rebaseActiveBranches()` is exactly that case — apps/test-app's e2e route
537
+ * calls it directly, and its `pollMergeState` dispatch reads `ctx.octokit()`.
601
538
  */
602
539
  octokitClient() {
603
540
  this.ensureGitHubAuth();
604
541
  return this.octokit;
605
542
  }
606
543
  /**
607
- * The single seam through which every git-over-HTTPS credential reaches a
608
- * git command. The only other consumer of the credential is Octokit, built
609
- * from the same resolution by `ensureGitHubAuth()` above.
544
+ * The single seam through which every git-over-HTTPS credential reaches a git
545
+ * command. The only other consumer is Octokit, built from the same resolution
546
+ * by `ensureGitHubAuth()` above.
610
547
  *
611
- * Async because the credential need not be a value the worker already
612
- * holds: under GitHub App auth `resolveGitToken` mints an installation
613
- * token, which lasts about an hour. Nothing may cache what this returns —
614
- * a URL built from an installation token goes stale with it. Resolving per
615
- * use is cheap: `@octokit/auth-app` answers from its own cache until the
616
- * token is near expiry, so the usual cost is a resolved microtask, and the
617
- * token path is a bare `async` return.
548
+ * Async because under GitHub App auth `resolveGitToken` mints an installation
549
+ * token lasting about an hour. NOTHING may cache what this returns — a URL
550
+ * built from an installation token goes stale with it — and resolving per use
551
+ * is cheap, since `@octokit/auth-app` answers from its own cache until the
552
+ * token is near expiry.
618
553
  *
619
554
  * A mint failure propagates AS THROWN, carrying the `.status` that
620
- * `isPermanentTaskFailure` (task-runner.ts:92) classifies on — see
621
- * github-auth.ts.
555
+ * `isPermanentTaskFailure` classifies on — see github-auth.ts.
622
556
  *
623
557
  * Do NOT add a parallel token accessor alongside it. Every instance-backed
624
558
  * WorkerContext member stays a function precisely so tests can replace it
@@ -632,36 +566,31 @@ export class CmsWorker {
632
566
  /**
633
567
  * Prove the GitHub App credential works before anything depends on it.
634
568
  *
635
- * Without this the first failure comes out of `ensureRemoteGit`'s bare
636
- * clone below, whose catch reads "the GitHub repository may be empty, or
637
- * the base branch may not exist" — which would send an operator holding a
638
- * bad private key to go looking for a repository problem that does not
639
- * exist. Called from start()'s try, so the failure is also recorded as
640
- * `lastFatalError` in worker-status.json and reaches the admin panel.
641
- *
642
- * No-op on the token path: a PAT is a literal, so there is nothing to
643
- * check that the first real request would not check anyway.
644
- *
645
- * FATAL UNLESS THE FAILURE POSITIVELY LOOKS TRANSIENT. Both halves of that
646
- * are load-bearing.
647
- *
648
- * Not always fatal, because the two credential paths must degrade alike: on
649
- * the token path a GitHub 502 during boot is absorbed (a warm `remote.git`
650
- * short-circuits `ensureRemoteGit`, and `Promise.allSettled` swallows the
651
- * initial `syncGit`), so the worker starts and its loops retry. Rethrowing
652
- * every error class would make the App path exit instead, and systemd
653
- * (Restart=always) would crash-loop it until GitHub recovered — each
654
- * iteration telling the operator to go and check their private key.
569
+ * Without this the first failure comes out of `ensureRemoteGit`'s bare clone
570
+ * below, whose catch blames the repository ("may be empty, or the base branch
571
+ * may not exist") and sends an operator holding a bad private key looking for
572
+ * a problem that does not exist. Called from start()'s try, so the failure is
573
+ * also recorded as `lastFatalError` and reaches the admin panel. No-op on the
574
+ * token path: a PAT is a literal, so the first real request checks everything
575
+ * this could.
576
+ *
577
+ * FATAL UNLESS THE FAILURE POSITIVELY LOOKS TRANSIENT. Both halves are
578
+ * load-bearing. Not always fatal, because the two credential paths must
579
+ * degrade alike: on the token path a GitHub 502 during boot is absorbed (a
580
+ * warm `remote.git` short-circuits `ensureRemoteGit`, `Promise.allSettled`
581
+ * swallows the initial `syncGit`) so the worker starts and its loops retry,
582
+ * while rethrowing every error class would make the App path exit and systemd
583
+ * crash-loop it until GitHub recovered — each iteration telling the operator
584
+ * to check their private key.
655
585
  *
656
586
  * But fail CLOSED, via `isTransientAuthFailure` rather than the inverse of
657
- * `isPermanentTaskFailure`. That classifier defaults an error with no HTTP
658
- * status to transient, which is right on the task path (bounded by
659
- * `maxRetries`) and wrong here (bounded by nothing): a key that never
660
- * reaches GitHub at all — the wrong key type, or one too mangled to sign
661
- * with — fails locally and status-lessly, so "default to transient" would
662
- * boot a worker with a dead credential, record no `lastFatalError`, and show
663
- * healthy in the admin panel while every task and every sync failed. See
664
- * isTransientAuthFailure in github-auth.ts.
587
+ * `isPermanentTaskFailure`: that classifier defaults a status-less error to
588
+ * transient, which is right on the task path (bounded by `maxRetries`) and
589
+ * wrong here (bounded by nothing). A key that never reaches GitHub at all —
590
+ * the wrong type, or too mangled to sign with — fails locally and
591
+ * status-lessly, so defaulting to transient would boot a worker with a dead
592
+ * credential, record no `lastFatalError`, and show healthy in the admin panel
593
+ * while every task and every sync failed.
665
594
  */
666
595
  async preflightGitHubAppAuth() {
667
596
  if (!this.config.githubAppAuth)
@@ -677,30 +606,29 @@ export class CmsWorker {
677
606
  'Continuing — this looks transient, and the credential is minted again on first use.');
678
607
  return;
679
608
  }
680
- // Re-thrown with context, unlike buildGitHubUrl() above, which must
681
- // preserve the error identity for task classification. Nothing
682
- // classifies a startup failure -- start()'s catch records the message
683
- // and the process exits -- so here the operator-facing wording wins.
609
+ // Re-thrown WITH context, unlike buildGitHubUrl() above, which must
610
+ // preserve the error identity for task classification. Nothing classifies
611
+ // a startup failure -- start()'s catch records the message and the
612
+ // process exits -- so the operator-facing wording wins here.
684
613
  throw new Error(`GitHub App authentication failed: ${detail}. ` +
685
614
  'Check the app id, the installation id, and that the private key belongs to that app.');
686
615
  }
687
616
  workerLog('GitHub App authentication verified');
688
617
  }
689
618
  /**
690
- * The workspace directory for a branch named by its GIT REF name -- the
691
- * form task payloads carry (`context.branch.name`), not the directory form.
619
+ * The workspace directory for a branch named by its GIT REF name -- the form
620
+ * task payloads carry (`context.branch.name`), not the directory form.
692
621
  *
693
622
  * These differ for any name outside `[A-Za-z0-9._-]`, `/` being the obvious
694
623
  * one: workspaces are provisioned under `sanitizeBranchName(...)` (see
695
624
  * paths/branch.ts's `resolveBranchPaths`), so `feature/x` lives in
696
- * `feature-x`. Joining the raw name instead silently addresses a directory
697
- * that does not exist -- which for the metadata writers below meant the
698
- * update was quietly dropped, and for the leased push meant the
699
- * history-rewrite marker read as absent and the push went out unleased,
700
- * wedging exactly the branch this workstream exists to unwedge.
625
+ * `feature-x`. Joining the RAW name instead silently addresses a directory
626
+ * that does not exist -- which drops the metadata writers' updates, and makes
627
+ * the leased push read the history-rewrite marker as absent and go out
628
+ * unleased, wedging the branch.
701
629
  *
702
- * `name` inside the metadata itself stays the raw ref name; only the path
703
- * is sanitized.
630
+ * `name` inside the metadata itself stays the raw ref name; only the path is
631
+ * sanitized.
704
632
  */
705
633
  branchWorkspacePath(branchRefName) {
706
634
  return path.join(this.contentBranchesPath, sanitizeBranchName(branchRefName));
@@ -711,30 +639,26 @@ export class CmsWorker {
711
639
  * `checkout --theirs` resolution loop overwrites them. No-op in production.
712
640
  *
713
641
  * A test subclass overrides this to land a real `ContentStore` write at
714
- * exactly the instant the rebase is mid-flight -- the window the old TOCTOU
715
- * comment above the dirty check wrongly called safe -- without any sleeps or
716
- * shell rendezvous. See the "Deterministic interleavings" testing pattern in
642
+ * exactly the instant the rebase is mid-flight, with no sleeps or shell
643
+ * rendezvous. See the "Deterministic interleavings" pattern in
717
644
  * docs/concurrency.md, and `ContentStore.afterPrePassForTesting()` for the
718
645
  * same idiom on the write side.
719
646
  */
720
647
  async afterConflictDetectedForTesting() { }
721
648
  /**
722
- * Test seam, sibling of {@link afterConflictDetectedForTesting}: runs at the
649
+ * Test seam, sibling of {@link afterConflictDetectedForTesting}: runs the
723
650
  * instant a rebase round has succeeded, before the completion path (cache
724
- * invalidation, conflict metadata, [SYNC-H1] marker) executes. Exists so a
725
- * test can lose the content-write lock at exactly the point where bailing out
726
- * would strand a rewritten history.
651
+ * invalidation, conflict metadata, [SYNC-H1] marker) executes, so a test can
652
+ * lose the content-write lock at exactly the point where bailing out would
653
+ * strand a rewritten history.
727
654
  */
728
655
  async afterRebaseCompletedForTesting() { }
729
656
  // --- Rebase loop (worker/rebase.ts) ------------------------------------
730
657
  //
731
658
  // Both are TEST-ONLY entry points -- see the task-queue block above. Nothing
732
- // in production dispatches through either; `syncGit` calls `runRebaseCycle`
733
- // directly and that calls `pollMergeState` directly. Four test files call
734
- // `rebaseActiveBranches` (it is the single entry point the whole rebase suite
735
- // drives), and cms-worker-merge-poll.test.ts calls `pollMergeState` with a
736
- // mocked `octokit`. Neither is on WorkerContext, so a stub installed on
737
- // either is a no-op as far as a production code path is concerned.
659
+ // in production dispatches through either: `syncGit` calls `runRebaseCycle`
660
+ // directly and that calls `pollMergeState` directly. Neither is on
661
+ // WorkerContext, so a stub installed on either is a no-op for production.
738
662
  //
739
663
  // `rebaseActiveBranches` also has a consumer outside the test files:
740
664
  // apps/test-app/app/api/e2e-test/rebase/route.ts drives it from an e2e
@@ -746,14 +670,11 @@ export class CmsWorker {
746
670
  //
747
671
  // `syncGit` is the public loop entry `scheduleLoop` drives. The three private
748
672
  // ones below it are TEST-ONLY entry points -- see the task-queue block above
749
- // -- each called directly by one test file (cms-worker-sync-reconcile for the
750
- // settings push, cms-worker-base-refresh for the base workspace,
751
- // cms-worker.test.ts for the trash sweep). `syncGit` reaches all three as
673
+ // -- each called directly by one test file. `syncGit` reaches all three as
752
674
  // module-level calls, so a stub installed on one of these is a no-op.
753
675
  //
754
676
  // `reconcileTrackedBranches` deliberately has NO delegator: no test reaches
755
- // it through the instance. Comments elsewhere that point at it as living in
756
- // this file are stale by definition -- it is in git-sync.ts.
677
+ // it through the instance.
757
678
  async syncGit() {
758
679
  return syncGit(this.ctx());
759
680
  }
@@ -762,10 +683,10 @@ export class CmsWorker {
762
683
  *
763
684
  * One of `refreshGitHubCredential`'s two call sites, and the one that works
764
685
  * when nobody is publishing: it fetches from GitHub every `gitSyncInterval`
765
- * (default 5 minutes) whether or not anyone is editing, so a credential that
766
- * has stopped working surfaces here even with no push queued for days.
686
+ * whether or not anyone is editing, so a credential that has stopped working
687
+ * surfaces here even with no push queued for days.
767
688
  *
768
- * The sync failure is what propagates to `scheduleLoop`'s catch.
689
+ * The SYNC failure is what propagates to `scheduleLoop`'s catch;
769
690
  * `refreshGitHubCredential` never throws, so nothing it does can replace it.
770
691
  */
771
692
  async syncGitWithCredentialRefresh() {
@@ -783,42 +704,29 @@ export class CmsWorker {
783
704
  * **Two call sites, each covering what the other cannot.** The git-sync loop
784
705
  * (`syncGitWithCredentialRefresh`) notices a dead credential when nobody is
785
706
  * publishing. `processTaskQueue`'s per-task catch is what saves a publish: a
786
- * push task spends its retry budget on a 5s/10s/20s backoff
787
- * (task-queue/task-queue.ts), well inside one 5-minute sync interval, so with
788
- * the sync loop as the only trigger a publish that met a rotated token failed
789
- * permanently while the working one was already in the secret store.
790
- *
791
- * Every consumer reaches the credential through `ensureGitHubAuth()`, which
792
- * reads it per use — the sync fetch, `pushBranchToGitHub`,
793
- * `pushSettingsBranches`, `ensureRemoteGit`'s clone, and every Octokit call —
794
- * so a refresh from either site repairs all of them for their NEXT use, with
795
- * nothing to invalidate. It cannot rescue an attempt that has already failed,
796
- * so a task `isPermanentTaskFailure` fails fast is not saved; the task after
797
- * it is.
798
- *
799
- * NOT gated on the error looking auth-shaped, at either site. There is nothing
800
- * to classify on: a `git fetch` or `git push` rejected for a dead token throws
801
- * a plain simple-git error — exit 128, no HTTP `.status` — which
802
- * `isPermanentTaskFailure` reads as transient, so a gate keyed on it would
803
- * never fire. Two floors bound the cost instead: core's own
804
- * `refreshGitHubTokenMinIntervalMs` (default 60s, enforced by
805
- * `refreshCredential` in github-auth.ts), and whatever floor the provider
806
- * keeps — the AWS one reads at most once per five minutes and returns
807
- * `undefined` for an unchanged value (canopycms-cdk/worker/credential-refresh.ts).
808
- * On the GitHub App path the refresh is a no-op. Both call sites share both
809
- * floors, so a read issued by one throttles the other.
810
- *
811
- * **Never throws.** Both callers are already reporting a failure, and that
812
- * failure is the one that must reach the log.
813
- *
814
- * **Bounded by `taskTimeoutMs`**, because the task loop awaits it, and a read
815
- * that never settled would stop every publish queued behind it. An adopter's
816
- * provider may have no bound at all, and the AWS one, bounded as it is
817
- * (canopycms-cdk/worker/secrets.ts), can still take up to 87s for one
818
- * `getSecret` — longer than the 60s default here. A read that loses the race
819
- * is not cancelled, and
820
- * may still land later; `refreshCredential` discards a result older than one
821
- * it has already applied, so a late landing cannot put a stale token back.
707
+ * push task spends its retry budget on a 5s/10s/20s backoff, well inside one
708
+ * 5-minute sync interval, so with the sync loop as the only trigger a publish
709
+ * meeting a rotated token fails permanently while the working one is already
710
+ * in the secret store. Every consumer reaches the credential through
711
+ * `ensureGitHubAuth()`, which reads it per use, so a refresh from either site
712
+ * repairs all of them for their NEXT use — but not an attempt already failed.
713
+ *
714
+ * NOT gated on the error looking auth-shaped, at either site: a `git
715
+ * fetch`/`push` rejected for a dead token throws a plain simple-git error
716
+ * (exit 128, no HTTP `.status`) that `isPermanentTaskFailure` reads as
717
+ * transient, so a gate keyed on it would never fire. Two floors bound the
718
+ * cost instead — core's `refreshGitHubTokenMinIntervalMs` (default 60s,
719
+ * enforced by `refreshCredential` in github-auth.ts) and whatever floor the
720
+ * provider keeps (the AWS one reads at most once per five minutes) — and both
721
+ * call sites share both, so a read issued by one throttles the other. On the
722
+ * GitHub App path the refresh is a no-op.
723
+ *
724
+ * **Never throws**: both callers are already reporting the failure that must
725
+ * reach the log. **Bounded by `taskTimeoutMs`**, because the task loop awaits
726
+ * it and a read that never settled would stop every publish queued behind it
727
+ * (an adopter's provider may have no bound at all; the AWS one can take 87s
728
+ * for one `getSecret`). A losing read is not cancelled and may land later, but
729
+ * `refreshCredential` discards a result older than one already applied.
822
730
  */
823
731
  async refreshGitHubCredential() {
824
732
  let timer;
@@ -832,8 +740,8 @@ export class CmsWorker {
832
740
  }
833
741
  catch (err) {
834
742
  // [REDACT] The message can name the secret and, on a malformed-secret
835
- // path, quote what was read. Console only, but the rule here is
836
- // uniform -- see redactCredentials in utils/error.ts.
743
+ // path, quote what was read. Console only, but the rule is uniform --
744
+ // see redactCredentials in utils/error.ts.
837
745
  workerLogError('Failed to re-read the GitHub credential after a failure:', redactCredentials(getErrorMessage(err)));
838
746
  }
839
747
  finally {