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
@@ -0,0 +1,1030 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { simpleGit } from 'simple-git';
5
+ import { gitNetworkChildEnv } from '../git-manager.js';
6
+ import { getErrorMessage } from '../utils/error.js';
7
+ import { BundleHeaderError, BundleHeaderReader, hasOnlyHead, } from './bundle-header.js';
8
+ import { runGit } from './git-process.js';
9
+ import { MaintenanceMutex, findCorruption, maintainMirror, objectStoreKiB, rebuildPaths, recoverRebuild, sweepLocksAndTemporaries, } from './github-mirror-upkeep.js';
10
+ import { GatewayRefusalError, MAX_BRANCH_NAME_LENGTH, branchNameProblem, } from './branch-policy.js';
11
+ import { gatewayLog, gatewayLogError, gatewayLogWarn, redactEgress } from './gateway-egress.js';
12
+ import { mirrorGitOptions, mirrorPins, quarantinePins } from './shared-repo-git.js';
13
+ /**
14
+ * The worker's private bare mirror of the GitHub repository, in a state directory the CMS Lambda
15
+ * cannot reach. It and the quarantines beside it are the only repositories in which git ever runs
16
+ * with the GitHub credential, and the state directory the only place the GitHub gateway runs git
17
+ * at all. Every gateway git names a repository inside it, by `GIT_DIR` and working directory
18
+ * alike, so no config or hook the Lambda writes is ever read by it; Lambda-written objects reach
19
+ * it only inside a pushed bundle, checked in a quarantine. The credential reaches git only as a
20
+ * per-command config file ({@link withCredentialConfig}).
21
+ *
22
+ * Objects cross the boundary as git bundles, never by a fetch or push against `remote.git`:
23
+ * - **out:** a fetch bundles what the caller's `have` lacks into `outgoing/`, one pending bundle
24
+ * at a time ({@link GitHubMirror.openBundle});
25
+ * - **in:** a pushed bundle is written to `incoming/`, its header read in node
26
+ * (worker/bundle-header.ts), and fetched, object-checked, into a fresh quarantine repository
27
+ * that borrows the mirror's objects through `objects/info/alternates`. The push to GitHub goes
28
+ * out from there. Pushed objects therefore never enter the mirror, whose objects come only
29
+ * from GitHub; the quarantine is deleted whole.
30
+ *
31
+ * `refs/heads/*` mirrors GitHub as of the last fetch. A cache: deleting it costs one fetch of the
32
+ * whole repository from GitHub.
33
+ *
34
+ * One process owns it, and {@link GitHubMirror.exclusive} runs one session at a time, so a fetch's
35
+ * `--prune` never meets a push half way, and the mirror's heads cannot move between a push's
36
+ * reachability test and the push. It sweeps what a killed git left, rebuilds itself from GitHub
37
+ * when it would lie, and runs maintenance beside sessions under a mutex of its own
38
+ * (worker/github-mirror-upkeep.ts, whose module doc holds the lock order).
39
+ *
40
+ * Every transfer runs with `--progress`: the timeout is inactivity, and a quiet fetch of a whole
41
+ * repository would otherwise be killed for its size. The connectivity walk after a fetch still
42
+ * prints nothing (safeinsights/canopycms#901).
43
+ */
44
+ export class GitHubMirror {
45
+ /**
46
+ * `maintenanceIntervalMs` (default {@link MAINTENANCE_INTERVAL_MS}) also delays the first run:
47
+ * a mirror just fetched is in few packs. `bundleRetentionMs` (default
48
+ * {@link BUNDLE_RETENTION_MS}) is how long a bundle no one opens is kept.
49
+ */
50
+ constructor(stateDirectory, timeoutMs, maintenanceIntervalMs = MAINTENANCE_INTERVAL_MS, bundleRetentionMs = BUNDLE_RETENTION_MS) {
51
+ this.stateDirectory = stateDirectory;
52
+ this.timeoutMs = timeoutMs;
53
+ this.maintenanceIntervalMs = maintenanceIntervalMs;
54
+ this.bundleRetentionMs = bundleRetentionMs;
55
+ this.tail = Promise.resolve();
56
+ this.ingestTail = Promise.resolve();
57
+ this.warnedSize = false;
58
+ this.ready = false;
59
+ this.swept = false;
60
+ this.transferDirs = null;
61
+ this.pendingBundle = null;
62
+ this.maintenance = new MaintenanceMutex();
63
+ this.corruption = null;
64
+ /**
65
+ * Changes whenever the mirror stops holding what the last GitHub fetch put there: it was
66
+ * created anew, found corrupt, or a rebuild replaced it. A GitHub fetch is reused only within
67
+ * the generation it completed in.
68
+ */
69
+ this.generation = 0;
70
+ this.rebuildFailedAt = 0;
71
+ /** A sweep the maintenance mutex prevented, done at the next session start that can take it. */
72
+ this.sweepOwed = false;
73
+ this.activityListeners = new Set();
74
+ this.activity = () => {
75
+ for (const listener of this.activityListeners)
76
+ listener();
77
+ };
78
+ this.lastMaintenanceAt = Date.now();
79
+ this.gitDir = path.join(stateDirectory, 'github.git');
80
+ this.incomingDir = path.join(stateDirectory, 'incoming');
81
+ this.outgoingDir = path.join(stateDirectory, 'outgoing');
82
+ }
83
+ /**
84
+ * Run `fn` with the mirror to itself, after every session started before it. The first session
85
+ * of a gateway first clears what a killed one left ({@link sweepAtStart}), and any session first
86
+ * does a sweep that is owed ({@link sweepIfOwed}). Creates the mirror if it is missing, and
87
+ * checks it again (for corruption too) after any session that failed. Deletes any credential
88
+ * file a killed or failed cleanup left: no session is running, so none is in use.
89
+ */
90
+ exclusive(fn) {
91
+ const start = async () => {
92
+ try {
93
+ if (!this.swept) {
94
+ await this.sweepAtStart();
95
+ this.swept = true;
96
+ }
97
+ await this.sweepIfOwed();
98
+ if (!this.ready) {
99
+ await this.create();
100
+ this.ready = true;
101
+ }
102
+ await sweepCredentialConfigs(this.stateDirectory);
103
+ await this.ensureTransferDirs();
104
+ return await fn(new MirrorSession(this.gitDir, this.stateDirectory, this.timeoutMs, this.activity));
105
+ }
106
+ catch (err) {
107
+ this.ready = false;
108
+ throw err;
109
+ }
110
+ };
111
+ const run = this.tail.then(start, start);
112
+ this.tail = run.catch(() => undefined);
113
+ return run;
114
+ }
115
+ /**
116
+ * Call `listener` at each sign of life from a session's git (output, input taken) until the
117
+ * returned function is called: what a caller waiting on the mirror sees of progress, whoever's
118
+ * session it is.
119
+ */
120
+ watchActivity(listener) {
121
+ this.activityListeners.add(listener);
122
+ return () => this.activityListeners.delete(listener);
123
+ }
124
+ /**
125
+ * Create the state directory if it is missing, and refuse one this process does not own outright
126
+ * or another user can write: the gateway's boot check, whose failure no later session could clear.
127
+ */
128
+ async assertStateDirectory() {
129
+ await fs.mkdir(this.stateDirectory, { recursive: true, mode: 0o700 });
130
+ await assertOwnDirectory(this.stateDirectory);
131
+ }
132
+ /** Create the mirror now rather than at its first use. */
133
+ ensure() {
134
+ return this.exclusive(async () => undefined);
135
+ }
136
+ /**
137
+ * Clear what a killed gateway left, before any git runs: finish or undo an interrupted rebuild,
138
+ * then every lock and temporary in the mirror. Nothing in this process writes it yet:
139
+ * maintenance starts only after a session. A git a stopped dev worker left running is not
140
+ * covered (safeinsights/canopycms#907).
141
+ */
142
+ async sweepAtStart() {
143
+ await fs.mkdir(this.stateDirectory, { recursive: true, mode: 0o700 });
144
+ await assertOwnDirectory(this.stateDirectory);
145
+ await recoverRebuild(this.gitDir);
146
+ const removed = await sweepLocksAndTemporaries(this.gitDir);
147
+ if (removed.length > 0) {
148
+ gatewayLogWarn(`Removed what a stopped gateway left in the GitHub mirror: ${removed.join(', ')}`);
149
+ }
150
+ }
151
+ /**
152
+ * Create the mirror, or recreate one that is not a readable bare repository, and record whether
153
+ * an existing one is corrupt ({@link findCorruption}), which the next GitHub fetch's session
154
+ * repairs ({@link heal}); until then the generation change keeps every caller off it.
155
+ */
156
+ async create() {
157
+ await fs.mkdir(this.stateDirectory, { recursive: true, mode: 0o700 });
158
+ await assertOwnDirectory(this.stateDirectory);
159
+ if (!(await isBareRepository(this.gitDir))) {
160
+ await fs.rm(this.gitDir, { recursive: true, force: true });
161
+ await initMirror(this.gitDir);
162
+ this.generation++;
163
+ }
164
+ await assertOwnDirectory(this.gitDir);
165
+ this.corruption = await findCorruption(this.gitDir, (args, stdin) => runInRepo(this.gitDir, this.timeoutMs, args, { stdin }));
166
+ // No fetch's snapshot vouches for a corrupt mirror, so no caller uses it until a fetch rebuilds it.
167
+ if (this.corruption !== null)
168
+ this.generation++;
169
+ }
170
+ /**
171
+ * Refuse a session that cannot repair the mirror while it is corrupt: one without the credential
172
+ * a rebuild fetches with.
173
+ */
174
+ assertUsable() {
175
+ if (this.corruption !== null) {
176
+ throw new GitHubTransientError('mirror-corrupt', `The GitHub mirror needs rebuilding (${this.corruption}); the next fetch from GitHub does it`);
177
+ }
178
+ }
179
+ /**
180
+ * Inside a GitHub fetch's session, rebuild a mirror {@link create} found corrupt, as a plain
181
+ * call (the session is not re-entrant), waiting for maintenance to finish: the lock order is session, then the
182
+ * maintenance mutex. The replacement is fetched whole from GitHub into `github.git.rebuild`, and
183
+ * swapped in only once complete: the mirror is renamed to `.old`, `.rebuild` into its place, and
184
+ * `.old` deleted ({@link recoverRebuild} finishes or undoes a swap a crash interrupted). No
185
+ * quarantine is live while the session is held, so moving the objects it would borrow is safe.
186
+ * After a failed rebuild, sessions answer transient for {@link REBUILD_BACKOFF_MS} rather than
187
+ * fetch the whole repository again each.
188
+ */
189
+ async heal(session, credential) {
190
+ const reason = this.corruption;
191
+ if (reason === null)
192
+ return;
193
+ if (Date.now() - this.rebuildFailedAt < REBUILD_BACKOFF_MS)
194
+ this.assertUsable();
195
+ const { rebuild, old } = rebuildPaths(this.gitDir);
196
+ const release = await this.maintenance.acquire();
197
+ try {
198
+ await fs.rm(rebuild, { recursive: true, force: true });
199
+ await initMirror(rebuild);
200
+ await session.fetchFromGitHub(credential, undefined, rebuild);
201
+ await fs.rm(old, { recursive: true, force: true });
202
+ await fs.rename(this.gitDir, old);
203
+ await fs.rename(rebuild, this.gitDir);
204
+ await fs.rm(old, { recursive: true, force: true });
205
+ this.corruption = null;
206
+ this.generation++;
207
+ gatewayLogWarn(`Rebuilt the GitHub mirror from GitHub: ${reason}`);
208
+ }
209
+ catch (err) {
210
+ this.rebuildFailedAt = Date.now();
211
+ await fs.rm(rebuild, { recursive: true, force: true }).catch(() => undefined);
212
+ throw new GitHubTransientError('mirror-corrupt', `The GitHub mirror needs rebuilding (${reason}), and the rebuild failed: ` +
213
+ redactEgress(getErrorMessage(err)).trim());
214
+ }
215
+ finally {
216
+ release();
217
+ }
218
+ }
219
+ /**
220
+ * The GitHub fetch, inside a session. A failure sweeps the mirror's locks and temporaries
221
+ * ({@link sweepUnlessMaintained}), so it leaves no pack of its own behind; when git failed on
222
+ * one of the mirror's own leftovers ({@link isOwnLockFailure}) and the sweep ran, the fetch is
223
+ * tried once more. While maintenance runs, which holds such files live, nothing is swept, the
224
+ * sweep is owed, and a leftover's failure is transient. Never waits for the mutex, so a push
225
+ * never waits behind a repack.
226
+ */
227
+ async fetchFromGitHub(session, credential, signal) {
228
+ try {
229
+ await session.fetchFromGitHub(credential, signal);
230
+ return;
231
+ }
232
+ catch (err) {
233
+ const swept = await this.sweepUnlessMaintained('Removed stale locks from the GitHub mirror');
234
+ if (!isOwnLockFailure(getErrorMessage(err)))
235
+ throw err;
236
+ if (!swept) {
237
+ throw new GitHubTransientError('github-fetch-failed', 'The GitHub mirror is being maintained, and its fetch met a lock maintenance holds; ' +
238
+ 'retrying runs after it');
239
+ }
240
+ }
241
+ try {
242
+ await session.fetchFromGitHub(credential, signal);
243
+ }
244
+ catch (err) {
245
+ await this.sweepUnlessMaintained('Removed what a failed fetch left in the GitHub mirror');
246
+ throw err;
247
+ }
248
+ }
249
+ /**
250
+ * Inside a session, sweep the mirror's locks and temporaries ({@link sweepLocksAndTemporaries}),
251
+ * logging what went under `label`, unless maintenance holds the mutex: then the sweep is owed.
252
+ * Returns whether it swept.
253
+ */
254
+ async sweepUnlessMaintained(label) {
255
+ const release = this.maintenance.tryAcquire();
256
+ if (release === null) {
257
+ this.sweepOwed = true;
258
+ return false;
259
+ }
260
+ try {
261
+ const removed = await sweepLocksAndTemporaries(this.gitDir);
262
+ if (removed.length > 0)
263
+ gatewayLogWarn(`${label}: ${removed.join(', ')}`);
264
+ this.sweepOwed = false;
265
+ return true;
266
+ }
267
+ finally {
268
+ release();
269
+ }
270
+ }
271
+ /** At a session's start, the sweep a held mutex prevented, if it can take the mutex now. */
272
+ async sweepIfOwed() {
273
+ if (this.sweepOwed) {
274
+ await this.sweepUnlessMaintained('Removed what an interrupted git left in the GitHub mirror');
275
+ }
276
+ }
277
+ /**
278
+ * After a completed GitHub fetch's session: warn once if the mirror has grown past
279
+ * {@link MIRROR_SIZE_WARN_KIB}, and start maintenance, detached, at most once per maintenance
280
+ * interval, unless the mutex is held (a rebuild, or maintenance itself).
281
+ */
282
+ async afterFetch() {
283
+ await this.warnIfLarge().catch((err) => gatewayLogWarn(`Could not measure the GitHub mirror: ${getErrorMessage(err)}`));
284
+ if (Date.now() - this.lastMaintenanceAt < this.maintenanceIntervalMs)
285
+ return;
286
+ const release = this.maintenance.tryAcquire();
287
+ if (release === null)
288
+ return;
289
+ this.lastMaintenanceAt = Date.now();
290
+ void this.runMaintenance().finally(release);
291
+ }
292
+ /**
293
+ * One maintenance run, holding the mutex; it never takes a session. A failed run owes a sweep
294
+ * of what it left, and the next run waits a whole interval from this one's end.
295
+ */
296
+ async runMaintenance() {
297
+ const started = Date.now();
298
+ try {
299
+ const { repacked } = await maintainMirror((args, { deadlineMs }) => runInRepo(this.gitDir, deadlineMs, args, { deadlineMs }));
300
+ if (repacked)
301
+ gatewayLog(`GitHub mirror repacked in ${Date.now() - started}ms`);
302
+ }
303
+ catch (err) {
304
+ this.sweepOwed = true;
305
+ gatewayLogError(`GitHub mirror maintenance failed: ${getErrorMessage(err)}`);
306
+ }
307
+ finally {
308
+ this.lastMaintenanceAt = Date.now();
309
+ }
310
+ }
311
+ async warnIfLarge() {
312
+ if (this.warnedSize)
313
+ return;
314
+ const kib = await objectStoreKiB((args) => runInRepo(this.gitDir, this.timeoutMs, args));
315
+ if (kib > MIRROR_SIZE_WARN_KIB) {
316
+ this.warnedSize = true;
317
+ gatewayLogWarn(`The worker's GitHub mirror (${this.gitDir}) holds ${Math.round(kib / 1024)} MiB of ` +
318
+ `objects, on the instance's root volume. See "The worker instance" in ` +
319
+ `docs/deploying-to-aws.md for the headroom it assumes.`);
320
+ }
321
+ }
322
+ /**
323
+ * Empty `incoming/` and `outgoing/` of what an earlier gateway left, once per gateway: after
324
+ * that, everything in them belongs to a transfer of this one's, which removes its own, so a
325
+ * later call only re-creates one that has gone (the state directory removed under it).
326
+ */
327
+ async ensureTransferDirs() {
328
+ this.transferDirs ??= (async () => {
329
+ await fs.mkdir(this.stateDirectory, { recursive: true, mode: 0o700 });
330
+ await assertOwnDirectory(this.stateDirectory);
331
+ for (const dir of [this.incomingDir, this.outgoingDir]) {
332
+ await fs.rm(dir, { recursive: true, force: true });
333
+ }
334
+ })().catch((err) => {
335
+ this.transferDirs = null;
336
+ throw err;
337
+ });
338
+ await this.transferDirs;
339
+ for (const dir of [this.incomingDir, this.outgoingDir]) {
340
+ await fs.mkdir(dir, { recursive: true, mode: 0o700 });
341
+ }
342
+ }
343
+ /**
344
+ * git for `check-ref-format`, run in the state directory like every other gateway git. It reads
345
+ * the repository's config, as any git does, and its refs only to expand `@{-N}`, which
346
+ * `assertPlainBranchName` refuses.
347
+ */
348
+ async checkGit() {
349
+ await this.ensureTransferDirs();
350
+ return simpleGit({ baseDir: this.stateDirectory }).env(mirrorEnv(this.gitDir));
351
+ }
352
+ /** Where a session writes a new outgoing bundle: a fresh name, never a caller's. */
353
+ async outgoingBundlePath() {
354
+ await this.ensureTransferDirs();
355
+ return path.join(this.outgoingDir, `${randomBytes(16).toString('hex')}.bundle`);
356
+ }
357
+ /**
358
+ * Make `file` the one pending outgoing bundle, or none, deleting the one it replaces (never an
359
+ * opened one), so a loop of fetches never fills the state directory, and deleting it after the
360
+ * bundle retention if no one opens it. Returns the id that opens it: a key into this object,
361
+ * never a path.
362
+ */
363
+ async setPendingBundle(file) {
364
+ const replaced = this.pendingBundle;
365
+ if (replaced !== null)
366
+ clearTimeout(replaced.expiry);
367
+ this.pendingBundle = null;
368
+ if (file !== null) {
369
+ const pending = {
370
+ id: randomBytes(16).toString('hex'),
371
+ file,
372
+ // A bundle its caller never opens (a worker gone) is deleted at the retention's end.
373
+ expiry: setTimeout(() => {
374
+ if (this.pendingBundle !== pending)
375
+ return;
376
+ this.pendingBundle = null;
377
+ void fs.rm(file, { force: true }).catch(() => undefined);
378
+ }, this.bundleRetentionMs),
379
+ };
380
+ pending.expiry.unref();
381
+ this.pendingBundle = pending;
382
+ }
383
+ if (replaced !== null)
384
+ await fs.rm(replaced.file, { force: true });
385
+ return this.pendingBundle?.id ?? null;
386
+ }
387
+ /**
388
+ * The pending bundle `id` names, readable once: it is no longer pending once opened, so a later
389
+ * fetch never deletes it, and its file is deleted when the stream closes.
390
+ * Throws {@link GitHubTransientError} for an id that is not the pending bundle's.
391
+ */
392
+ async openBundle(id) {
393
+ const pending = this.pendingBundle;
394
+ const gone = () => new GitHubTransientError('bundle-gone', `The bundle ${JSON.stringify(id.slice(0, 64))} is not available: a later fetch replaced ` +
395
+ `it, or it was already read. Fetch again.`);
396
+ if (pending === null || pending.id !== id)
397
+ throw gone();
398
+ clearTimeout(pending.expiry);
399
+ this.pendingBundle = null;
400
+ let handle;
401
+ try {
402
+ handle = await fs.open(pending.file, 'r');
403
+ }
404
+ catch {
405
+ throw gone();
406
+ }
407
+ const stream = handle.createReadStream();
408
+ stream.once('close', () => {
409
+ void fs.rm(pending.file, { force: true }).catch(() => undefined);
410
+ });
411
+ return stream;
412
+ }
413
+ /**
414
+ * Receive a pushed bundle into a directory of its own under `incoming/`, refusing it as it
415
+ * arrives (header, size), run `fn` with it, then delete the directory. One at a time, so pushed
416
+ * bundles hold at most one cap's worth of disk, plus about as much again for the quarantine's
417
+ * copy of the pack.
418
+ */
419
+ ingest(body, expected, maxBytes, fn) {
420
+ const start = async () => {
421
+ await this.ensureTransferDirs();
422
+ const dir = await fs.mkdtemp(path.join(this.incomingDir, 'push-'));
423
+ try {
424
+ const file = path.join(dir, 'bundle');
425
+ const header = await receiveBundle(body, file, expected, maxBytes);
426
+ return await fn({ dir, file, header });
427
+ }
428
+ finally {
429
+ await fs.rm(dir, { recursive: true, force: true });
430
+ }
431
+ };
432
+ const run = this.ingestTail.then(start, start);
433
+ this.ingestTail = run.catch(() => undefined);
434
+ return run;
435
+ }
436
+ }
437
+ /**
438
+ * Past this the mirror is using a large share of the worker's default 8 GiB root volume, which
439
+ * also holds the OS, Node and the log.
440
+ */
441
+ const MIRROR_SIZE_WARN_KIB = 2 * 1024 * 1024;
442
+ /** How long a fetch's bundle waits to be opened before it is deleted. */
443
+ const BUNDLE_RETENTION_MS = 10 * 60_000;
444
+ /** How often maintenance may run. */
445
+ const MAINTENANCE_INTERVAL_MS = 10 * 60 * 1000;
446
+ /** How long sessions answer transient after a failed rebuild, rather than retry it each. */
447
+ const REBUILD_BACKOFF_MS = 5 * 60 * 1000;
448
+ /** The slowest a GitHub transfer may run, sustained, before curl ends it. */
449
+ const LOW_SPEED_BYTES_PER_SECOND = 1024;
450
+ /** Where a quarantine holds the pushed commit while it is checked and pushed. */
451
+ const STAGING_PREFIX = 'refs/canopy/outgoing/';
452
+ /**
453
+ * A push or fetch the gateway could not complete for a reason a later attempt can clear: GitHub
454
+ * unreachable, a bundle that did not arrive whole or valid, a prerequisite GitHub no longer has,
455
+ * a bundle already gone. The task retries it. The code says where it failed, not whose doing it
456
+ * was: a `github-fetch-failed` or a `mirror-corrupt` can each follow a GitHub-bound git that
457
+ * failed, and only that arms a credential re-read (`LocalGitHubGateway.credentialed`).
458
+ */
459
+ export class GitHubTransientError extends Error {
460
+ constructor(code, message) {
461
+ super(message);
462
+ this.code = code;
463
+ this.name = 'GitHubTransientError';
464
+ }
465
+ }
466
+ /** The origin a credential's header is scoped to when its URL has none of its own. */
467
+ const GITHUB_ORIGIN = 'https://github.com';
468
+ /** `mkdtemp`'s prefix for a credential file's directory; nothing else in the state directory starts so. */
469
+ const CREDENTIAL_DIR_PREFIX = '.canopy-github-credential-';
470
+ const CREDENTIAL_DIR_PATTERN = /^\.canopy-github-credential-[A-Za-z0-9]{6}$/;
471
+ /**
472
+ * The global-scope config a GitHub-bound command gets: the token as an `Authorization` header for
473
+ * the remote's origin only, and an empty credential-helper list, so a 401 asks no helper (a
474
+ * developer's keychain included) and git's prompt is off besides. A URL without an http(s) origin
475
+ * (a test's local fixture) gets GitHub's, which it can never match. The token is written only
476
+ * base64-encoded, so none of its bytes can end a line or a quoted section name.
477
+ */
478
+ function credentialConfig(credential) {
479
+ let origin = GITHUB_ORIGIN;
480
+ try {
481
+ const url = new URL(credential.url);
482
+ if (url.protocol === 'https:' || url.protocol === 'http:')
483
+ origin = url.origin;
484
+ }
485
+ catch {
486
+ // Not a URL: a local path.
487
+ }
488
+ const basic = Buffer.from(`x-access-token:${credential.token}`).toString('base64');
489
+ return (`[http "${origin}/"]\n\textraheader = AUTHORIZATION: basic ${basic}\n` +
490
+ `[credential]\n\thelper =\n`);
491
+ }
492
+ /**
493
+ * Run `fn` with the path of a fresh config file holding the credential, then delete it. The file
494
+ * is created exclusively, `0600`, in a new `mkdtemp` directory inside the state directory, which
495
+ * {@link assertOwnDirectory} proves only this user can write: a path nobody else could have
496
+ * created first or swapped for a link. A file a crash or a failed delete leaves behind is swept
497
+ * before the next session ({@link GitHubMirror.exclusive}).
498
+ */
499
+ async function withCredentialConfig(stateDirectory, credential, fn) {
500
+ await assertOwnDirectory(stateDirectory);
501
+ const dir = await fs.mkdtemp(path.join(stateDirectory, CREDENTIAL_DIR_PREFIX));
502
+ try {
503
+ const configPath = path.join(dir, 'config');
504
+ const file = await fs.open(configPath, 'wx', 0o600);
505
+ try {
506
+ await file.writeFile(credentialConfig(credential));
507
+ }
508
+ finally {
509
+ await file.close();
510
+ }
511
+ return await fn(configPath);
512
+ }
513
+ finally {
514
+ // Never thrown: it would replace the command's own failure.
515
+ await fs
516
+ .rm(dir, { recursive: true, force: true })
517
+ .catch((err) => gatewayLogWarn(`Could not delete a credential file in ${dir}: ${getErrorMessage(err)}`));
518
+ }
519
+ }
520
+ /** Delete every credential directory {@link withCredentialConfig} left in `stateDirectory`. */
521
+ async function sweepCredentialConfigs(stateDirectory) {
522
+ for (const name of await fs.readdir(stateDirectory)) {
523
+ if (CREDENTIAL_DIR_PATTERN.test(name)) {
524
+ await fs.rm(path.join(stateDirectory, name), { recursive: true, force: true });
525
+ }
526
+ }
527
+ }
528
+ /**
529
+ * Whether git's own output, not a `remote:` line relayed from GitHub, says a lock file or
530
+ * `packed-refs.new` was already there: the mirror's own housekeeping holds it, or a killed git
531
+ * left it, which says nothing about the credential.
532
+ * @internal Exported for tests.
533
+ */
534
+ export function isOwnLockFailure(message) {
535
+ return message
536
+ .split('\n')
537
+ .some((line) => !/^\s*remote:/.test(line) &&
538
+ (/Unable to create '.*\.lock': File exists/.test(line) ||
539
+ /unable to create file .*packed-refs\.new: File exists/.test(line)));
540
+ }
541
+ /**
542
+ * Refuse a directory the worker does not own outright: another local user could have created a
543
+ * predictable path (the `os.tmpdir()` default) first, with a mirror config of their own.
544
+ */
545
+ async function assertOwnDirectory(dir) {
546
+ const stat = await fs.lstat(dir);
547
+ // Windows has no POSIX owner or mode bits to check.
548
+ const uid = process.getuid?.();
549
+ if (!stat.isDirectory() ||
550
+ (uid !== undefined && (stat.uid !== uid || (stat.mode & 0o022) !== 0))) {
551
+ throw new Error(`${dir} must be a directory this worker owns and no one else can write, for the GitHub ` +
552
+ `credential is used in the repository there`);
553
+ }
554
+ }
555
+ /**
556
+ * A push the worker never makes: to a name that is not a plain branch name, to a protected branch
557
+ * (`BranchPolicy`), to a settings branch under a lease, or of an object that is not a commit.
558
+ */
559
+ export class RefusedPushError extends GatewayRefusalError {
560
+ constructor(branch, reason, sha) {
561
+ super('push', branch, reason, reason === 'invalid'
562
+ ? `Refusing to push ${JSON.stringify(branch)} to GitHub: it is not a valid branch name.`
563
+ : reason === 'protected'
564
+ ? `Refusing to push "${branch}" to GitHub: it is protected (the base branch, GitHub's ` +
565
+ `default branch, or a name protectedBranches lists), which the CanopyCMS worker never ` +
566
+ `pushes. Nothing in CanopyCMS queues such a push; find out what wrote this task.`
567
+ : reason === 'settings'
568
+ ? `Refusing to push "${branch}" to GitHub under a lease: a settings branch only ever ` +
569
+ `fast-forwards. Find out what set a history-rewrite marker on it.`
570
+ : `Refusing to push "${branch}" to GitHub: ${sha ?? 'the object asked for'} is not a ` +
571
+ `commit, and a branch can only point at one. Find out what put it at ` +
572
+ `refs/heads/${branch} in remote.git.`);
573
+ this.name = 'RefusedPushError';
574
+ }
575
+ }
576
+ /**
577
+ * Throw {@link RefusedPushError} (or, for another `operation`, {@link GatewayRefusalError}) unless
578
+ * `branch` is a plain branch name: one `branchNameProblem` passes (with `shape` false, only its
579
+ * length cap and no leading `-`) and `git check-ref-format --branch` accepts unchanged. That rules out a `:` (git would read what
580
+ * follows it in a refspec as another destination), `..`, `^`, `~`, `@{`, spaces and a leading
581
+ * `-`, so `refs/heads/<branch>` is exactly the ref a push names. `check-ref-format --branch` takes
582
+ * exactly one argument and refuses `--end-of-options`, which is why it alone goes without one.
583
+ * `git` is the caller's instance: the gateway's runs in its state directory.
584
+ */
585
+ export async function assertPlainBranchName(branch, git = simpleGit(), operation = 'push',
586
+ /** False for a name the gateway only passes on, never acts on: git's rules, its length, no leading `-`. */
587
+ shape = true) {
588
+ const invalid = (why) => operation === 'push'
589
+ ? new RefusedPushError(branch, 'invalid')
590
+ : new GatewayRefusalError(operation, branch, 'invalid', `Refusing ${JSON.stringify(branch)}: it is not a valid branch name (${why}).`);
591
+ // A leading `-` is refused whatever `shape` says: git would read it as an option.
592
+ const problem = shape
593
+ ? branchNameProblem(branch)
594
+ : branch.startsWith('-')
595
+ ? "it starts with '-'"
596
+ : branch.length > MAX_BRANCH_NAME_LENGTH
597
+ ? `it is longer than ${MAX_BRANCH_NAME_LENGTH} characters`
598
+ : null;
599
+ if (problem !== null)
600
+ throw invalid(problem);
601
+ let normalized;
602
+ try {
603
+ normalized = (await git.raw(['check-ref-format', '--branch', branch])).trim();
604
+ }
605
+ catch (err) {
606
+ // Only git's own verdict is a refusal; a failure to run git at all is transient.
607
+ if (/is not a valid branch name/.test(getErrorMessage(err))) {
608
+ throw invalid('git check-ref-format refuses it');
609
+ }
610
+ throw err;
611
+ }
612
+ // `@{-1}` and the like resolve to some other name.
613
+ if (normalized !== branch)
614
+ throw invalid('git resolves it to another name');
615
+ }
616
+ /** What one {@link GitHubMirror.exclusive} call may do. */
617
+ export class MirrorSession {
618
+ constructor(gitDir, stateDirectory, timeoutMs, onActivity) {
619
+ this.gitDir = gitDir;
620
+ this.stateDirectory = stateDirectory;
621
+ this.timeoutMs = timeoutMs;
622
+ this.onActivity = onActivity;
623
+ }
624
+ /** simple-git in `repo`: the mirror, or a quarantine of this session's. */
625
+ git(signal, repo = this.gitDir) {
626
+ return simpleGit({
627
+ baseDir: repo,
628
+ ...mirrorGitOptions(),
629
+ timeout: { block: this.timeoutMs },
630
+ abort: signal,
631
+ }).env(mirrorEnv(repo));
632
+ }
633
+ /** git with `stdin` fed to it, in `repo` under `config` (the mirror's pins by default). */
634
+ run(args, options = {}) {
635
+ return runInRepo(options.repo ?? this.gitDir, this.timeoutMs, args, {
636
+ ...options,
637
+ onActivity: this.onActivity,
638
+ });
639
+ }
640
+ /**
641
+ * Run one GitHub-bound command in `repo`, `args` naming `credential.url` where git expects the
642
+ * remote. The credential reaches it only through {@link withCredentialConfig}'s file, named by
643
+ * `GIT_CONFIG_GLOBAL`, so it is in no argv and in no process's environment. No trace variable
644
+ * reaches this git: trace2 can print config values. Its failure is reported to the credential
645
+ * unless {@link isOwnLockFailure}.
646
+ *
647
+ * It runs through worker/git-process.ts, in a process group of its own, so an abort or a kill
648
+ * reaches what git spawned for the transfer too (`git-remote-https`, `pack-objects`); simple-git
649
+ * would signal git alone. Besides the inactivity bound, curl's low-speed limit ends a transfer
650
+ * that trickles: {@link LOW_SPEED_BYTES_PER_SECOND} for as long as the inactivity bound.
651
+ */
652
+ async githubBound(credential, args, signal, repo = this.gitDir) {
653
+ return withCredentialConfig(this.stateDirectory, credential, async (configPath) => {
654
+ try {
655
+ return await runGit(args, {
656
+ cwd: repo,
657
+ env: { ...githubBoundEnv(repo), GIT_CONFIG_GLOBAL: configPath },
658
+ config: [
659
+ ...mirrorPins(),
660
+ `http.lowSpeedLimit=${LOW_SPEED_BYTES_PER_SECOND}`,
661
+ `http.lowSpeedTime=${Math.max(1, Math.ceil(this.timeoutMs / 1000))}`,
662
+ ],
663
+ inactivityTimeoutMs: this.timeoutMs,
664
+ signal,
665
+ stdoutInFailure: true,
666
+ onActivity: this.onActivity,
667
+ });
668
+ }
669
+ catch (err) {
670
+ if (!isOwnLockFailure(getErrorMessage(err)))
671
+ credential.onFailure();
672
+ throw err;
673
+ }
674
+ });
675
+ }
676
+ /**
677
+ * Bring `refs/heads/*` of `repo` (the mirror, or a rebuild's replacement) to exactly what GitHub
678
+ * holds. Sessions fetch through {@link GitHubMirror.fetchFromGitHub}, which cleans up after it.
679
+ */
680
+ async fetchFromGitHub(credential, signal, repo = this.gitDir) {
681
+ await this.githubBound(credential, [
682
+ 'fetch',
683
+ '--prune',
684
+ '--progress',
685
+ '--no-write-fetch-head',
686
+ '--end-of-options',
687
+ credential.url,
688
+ '+refs/heads/*:refs/heads/*',
689
+ ], signal, repo);
690
+ }
691
+ /** Every branch GitHub had at the last fetch, mapped to its tip. */
692
+ async heads() {
693
+ const out = await this.git().raw([
694
+ 'for-each-ref',
695
+ '--format=%(objectname) %(refname)',
696
+ '--end-of-options',
697
+ 'refs/heads/',
698
+ ]);
699
+ const heads = new Map();
700
+ for (const line of out.split('\n')) {
701
+ const space = line.indexOf(' ');
702
+ if (space === -1)
703
+ continue;
704
+ heads.set(line.slice(space + 1 + 'refs/heads/'.length), line.slice(0, space));
705
+ }
706
+ return heads;
707
+ }
708
+ /**
709
+ * Bundle every branch into `file`, less what `have` already holds: each ID the mirror has as a
710
+ * commit is excluded with its history, and any other ID is ignored. False, with no file, when
711
+ * that leaves nothing to send. Each ID is validated before it reaches here, since rev-list's
712
+ * `--stdin` honours a pseudo-option such as `--all`.
713
+ */
714
+ async createBundle(have, file, signal) {
715
+ const types = await this.objectTypes(have);
716
+ const exclusions = have.filter((id) => types.get(id) === 'commit');
717
+ try {
718
+ await this.run(['bundle', 'create', '--progress', '--end-of-options', file, '--branches', '--stdin'], { stdin: exclusions.map((id) => `^${id}\n`).join(''), signal });
719
+ return true;
720
+ }
721
+ catch (err) {
722
+ await fs.rm(file, { force: true });
723
+ await fs.rm(`${file}.lock`, { force: true });
724
+ if (/Refusing to create empty bundle/.test(getErrorMessage(err)))
725
+ return false;
726
+ throw err;
727
+ }
728
+ }
729
+ /**
730
+ * Each of `ids` mapped to its object type in `repo`, or to `missing`, from one
731
+ * `cat-file --batch-check`, which prints `<id> missing` for an absent object where `cat-file -t`
732
+ * shares exit 128 with every other failure. Every ID must already be validated.
733
+ */
734
+ async objectTypes(ids, repo = this.gitDir) {
735
+ const types = new Map();
736
+ if (ids.length === 0)
737
+ return types;
738
+ const lines = (await this.run(['cat-file', '--batch-check=%(objectname) %(objecttype)', '--end-of-options'], {
739
+ stdin: ids.map((id) => `${id}\n`).join(''),
740
+ repo,
741
+ })).split('\n');
742
+ ids.forEach((id, i) => {
743
+ const line = lines[i] ?? '';
744
+ if (line === `${id} missing`)
745
+ types.set(id, 'missing');
746
+ else if (line.startsWith(`${id} `))
747
+ types.set(id, line.slice(id.length + 1));
748
+ else
749
+ throw new Error(`git cat-file printed no line for ${id}`);
750
+ });
751
+ return types;
752
+ }
753
+ /**
754
+ * Whether a GitHub branch contained the commit `sha` at the last fetch. Only for a commit:
755
+ * `--contains` exits 129 on a missing or non-commit object.
756
+ */
757
+ async reachableFromHeads(sha) {
758
+ const found = await this.git().raw([
759
+ 'for-each-ref',
760
+ '--count=1',
761
+ '--format=%(refname)',
762
+ `--contains=${sha}`,
763
+ '--end-of-options',
764
+ 'refs/heads/',
765
+ ]);
766
+ return found.trim() !== '';
767
+ }
768
+ /** Of `prerequisites`, the commits a GitHub branch contained at the last fetch. */
769
+ async usablePrerequisites(prerequisites) {
770
+ const unique = [...new Set(prerequisites)];
771
+ const types = await this.objectTypes(unique);
772
+ const usable = [];
773
+ for (const id of unique) {
774
+ if (types.get(id) === 'commit' && (await this.reachableFromHeads(id)))
775
+ usable.push(id);
776
+ }
777
+ return usable;
778
+ }
779
+ /**
780
+ * Whether every one of `commits` is reachable from one of `tips`, in ONE walk: rev-list fed the
781
+ * commits and a `^<tip>` per tip lists what the tips do not reach, so empty output means all
782
+ * are reached (several `--contains` would mean "any", not "all"). Only for commits: rev-list
783
+ * passes a tree or blob ID silently.
784
+ */
785
+ async allReachable(commits, tips) {
786
+ if (commits.length === 0)
787
+ return true;
788
+ const out = await this.run(['rev-list', '--stdin', '--max-count=1', '--end-of-options'], {
789
+ stdin: [...commits, ...tips.map((tip) => `^${tip}`)].map((line) => `${line}\n`).join(''),
790
+ });
791
+ return out.trim() === '';
792
+ }
793
+ /**
794
+ * The branch GitHub's HEAD names, or null for a repository with none (empty, or HEAD unborn). A
795
+ * failure to reach GitHub throws, so a push that cannot learn it does not happen.
796
+ */
797
+ async githubDefaultBranch(credential, signal) {
798
+ const out = await this.githubBound(credential, ['ls-remote', '--symref', '--end-of-options', credential.url, 'HEAD'], signal);
799
+ const match = /^ref: refs\/heads\/(.+)\tHEAD$/m.exec(out);
800
+ return match ? match[1] : null;
801
+ }
802
+ /**
803
+ * Take a received bundle into a fresh quarantine repository beside it, returning the
804
+ * quarantine's path, with `sha` there as a commit. In order:
805
+ * 1. each prerequisite must be a commit the mirror holds, reachable from one of `heads` (the
806
+ * branches GitHub had at this session's fetch): one `cat-file --batch-check`, then one walk;
807
+ * 2. the quarantine borrows the mirror's objects through `objects/info/alternates`, with the
808
+ * mirror's pins except `core.alternateRefsCommand` ({@link quarantinePins});
809
+ * 3. `fetch` of the bundle's one head into a staging ref, checking every object, by a refspec
810
+ * built from the validated branch, never from the bundle;
811
+ * 4. the staging ref must be `sha`, and `sha` a commit.
812
+ * A refusal is {@link GitHubTransientError} (`bundle-rejected`, or `prerequisite-gone` for a
813
+ * prerequisite GitHub no longer has), except a non-commit `sha`, which is
814
+ * {@link RefusedPushError}. Nothing here writes the mirror.
815
+ */
816
+ async quarantine(bundle, expected, heads, signal) {
817
+ const { prerequisites } = bundle.header;
818
+ const types = await this.objectTypes(prerequisites);
819
+ if (prerequisites.some((id) => types.get(id) === 'missing')) {
820
+ throw new GitHubTransientError('prerequisite-gone', `The bundle for ${expected.branch} builds on a commit GitHub no longer has. Retrying ` +
821
+ `sends a new one.`);
822
+ }
823
+ if (prerequisites.some((id) => types.get(id) !== 'commit')) {
824
+ throw new GitHubTransientError('bundle-rejected', 'Refusing the bundle: a prerequisite is not a commit');
825
+ }
826
+ if (!(await this.allReachable(prerequisites, [...heads.values()]))) {
827
+ throw new GitHubTransientError('prerequisite-gone', `The bundle for ${expected.branch} builds on a commit no GitHub branch contains any more. ` +
828
+ `Retrying sends a new one.`);
829
+ }
830
+ const repo = path.join(bundle.dir, 'repo.git');
831
+ await fs.mkdir(repo, { mode: 0o700 });
832
+ await this.run(['init', '--quiet', '--bare', '--end-of-options'], {
833
+ repo,
834
+ config: quarantinePins(),
835
+ });
836
+ await fs.writeFile(path.join(repo, 'objects', 'info', 'alternates'), `${path.join(this.gitDir, 'objects')}\n`, { mode: 0o600 });
837
+ const staging = `${STAGING_PREFIX}${expected.branch}`;
838
+ try {
839
+ await this.run([
840
+ 'fetch',
841
+ '--progress',
842
+ '--no-write-fetch-head',
843
+ '--end-of-options',
844
+ bundle.file,
845
+ `refs/heads/${expected.branch}:${staging}`,
846
+ ], {
847
+ repo,
848
+ // The severity pins are depth: index-pack already errors on these by default.
849
+ config: [
850
+ ...quarantinePins(),
851
+ 'fetch.fsckObjects=true',
852
+ 'fetch.fsck.hasDot=error',
853
+ 'fetch.fsck.hasDotdot=error',
854
+ 'fetch.fsck.hasDotgit=error',
855
+ ],
856
+ signal,
857
+ });
858
+ }
859
+ catch (err) {
860
+ throw new GitHubTransientError('bundle-rejected', `Refusing the bundle for ${expected.branch}: ${redactEgress(getErrorMessage(err)).trim()}`);
861
+ }
862
+ const staged = (await this.git(signal, repo).raw(['rev-parse', '--verify', '--end-of-options', staging])).split('\n')[0];
863
+ if (staged !== expected.sha) {
864
+ throw new GitHubTransientError('bundle-rejected', `Refusing the bundle for ${expected.branch}: its head is not ${expected.sha}`);
865
+ }
866
+ if ((await this.objectTypes([expected.sha], repo)).get(expected.sha) !== 'commit') {
867
+ throw new RefusedPushError(expected.branch, 'not-a-commit', expected.sha);
868
+ }
869
+ return repo;
870
+ }
871
+ /**
872
+ * Push exactly `sha` from `repo` (the mirror, or a quarantine holding it) to GitHub's
873
+ * `refs/heads/<branch>`. `lease` is the commit GitHub must still be at for the push to replace
874
+ * it (`--force-with-lease`); without one the push is an ordinary fast-forward. Resolves with
875
+ * what the push did to GitHub's ref.
876
+ *
877
+ * Validation and `policy` are here, at the one place every GitHub push (plain or under a lease)
878
+ * goes through, so no caller can skip them. When `policy` needs GitHub's default branch read
879
+ * afresh, it is read here, in this session, from the repository the push goes to. A bundle's quarantine fetch
880
+ * names `branch` first, so the gateway validates it before the ingest too (`pushOnce`).
881
+ */
882
+ async pushToGitHub(credential, branch, sha, options) {
883
+ for (const id of [sha, ...(options.lease === undefined ? [] : [options.lease])]) {
884
+ if (!isObjectId(id))
885
+ throw new Error(`Not a commit ID: ${JSON.stringify(id)}`);
886
+ }
887
+ await assertPlainBranchName(branch, this.git());
888
+ await options.policy.assertPushAllowed(branch, options.lease,
889
+ // Unknown, the policy cannot decide: neither a refusal nor a rejected push, but transient.
890
+ () => this.githubDefaultBranch(credential, options.signal).catch((err) => {
891
+ throw new GitHubTransientError('default-branch-unread', `Could not read GitHub's default branch before pushing ${branch}: ` +
892
+ redactEgress(getErrorMessage(err)).trim());
893
+ }), (reason) => new RefusedPushError(branch, reason));
894
+ options.onPushing?.();
895
+ const status = await this.githubBound(credential, [
896
+ 'push',
897
+ '--porcelain',
898
+ '--progress',
899
+ ...(options.lease ? [`--force-with-lease=refs/heads/${branch}:${options.lease}`] : []),
900
+ '--end-of-options',
901
+ credential.url,
902
+ `${sha}:refs/heads/${branch}`,
903
+ ], options.signal, options.repo);
904
+ return parsePushStatus(status, branch, sha);
905
+ }
906
+ }
907
+ /**
908
+ * Write a pushed bundle to `file` as it arrives, refusing it as soon as it cannot be accepted:
909
+ * past `maxBytes`, or with a header that is not exactly one head, `<sha> refs/heads/<branch>`
910
+ * (worker/bundle-header.ts). Holds at most the header's bounds, plus one chunk, in memory.
911
+ */
912
+ async function receiveBundle(body, file, expected, maxBytes) {
913
+ const rejected = (reason) => new GitHubTransientError('bundle-rejected', `Refusing the bundle for ${expected.branch}: ${reason}`);
914
+ const out = await fs.open(file, 'wx', 0o600);
915
+ const reader = new BundleHeaderReader('refuse');
916
+ let header = null;
917
+ let total = 0;
918
+ try {
919
+ for await (const chunk of body) {
920
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
921
+ total += bytes.length;
922
+ if (total > maxBytes)
923
+ throw rejected(`it is larger than ${maxBytes} bytes`);
924
+ if (header === null) {
925
+ header = reader.push(bytes)?.header ?? null;
926
+ if (header !== null && !hasOnlyHead(header, expected.sha, expected.branch)) {
927
+ throw rejected(`its one head must be ${expected.sha} refs/heads/${expected.branch}`);
928
+ }
929
+ }
930
+ await out.write(bytes);
931
+ }
932
+ }
933
+ catch (err) {
934
+ if (err instanceof GitHubTransientError)
935
+ throw err;
936
+ if (err instanceof BundleHeaderError)
937
+ throw rejected(err.message.replace(/^Refusing the bundle: /, ''));
938
+ throw rejected(`it did not arrive whole (${getErrorMessage(err)})`);
939
+ }
940
+ finally {
941
+ await out.close();
942
+ }
943
+ if (header === null)
944
+ throw rejected('it ends before its header does');
945
+ return header;
946
+ }
947
+ /**
948
+ * Read `refs/heads/<branch>`'s line from `git push --porcelain`'s stdout: `=` up to date, ` ` a
949
+ * fast-forward (`old..new`), `+` a forced update (`old...new`), `*` a new branch. A rejected ref
950
+ * (`!`) makes git exit non-zero before this runs. Output with no such line throws, rather than
951
+ * report a push this did not see.
952
+ * @internal Exported for tests.
953
+ */
954
+ export function parsePushStatus(stdout, branch, sha) {
955
+ for (const line of stdout.split('\n')) {
956
+ const [flag, refs, summary] = line.split('\t');
957
+ if (summary === undefined || !refs.endsWith(`:refs/heads/${branch}`))
958
+ continue;
959
+ if (flag === '=')
960
+ return { moved: false };
961
+ if (flag === '*')
962
+ return { moved: true, from: null, to: sha };
963
+ const range = /^([0-9a-f]+)\.\.\.?[0-9a-f]+/.exec(summary);
964
+ if ((flag === ' ' || flag === '+') && range)
965
+ return { moved: true, from: range[1], to: sha };
966
+ }
967
+ throw new Error(`git push reported no readable status for refs/heads/${branch}`);
968
+ }
969
+ /**
970
+ * The network env (proxies, CA bundles, the C locale the push-rejection classifier needs), with
971
+ * the repository named outright, since a bare repository found from the working directory is
972
+ * refused where `safe.bareRepository=explicit` is set. No object is ever fetched lazily from a
973
+ * promisor remote, and no `refs/replace/*` changes what any command sees.
974
+ */
975
+ function mirrorEnv(gitDir) {
976
+ return {
977
+ ...gitNetworkChildEnv(),
978
+ GIT_DIR: gitDir,
979
+ GIT_NO_LAZY_FETCH: '1',
980
+ GIT_NO_REPLACE_OBJECTS: '1',
981
+ };
982
+ }
983
+ /**
984
+ * {@link mirrorEnv} for a GitHub-bound command: without any `GIT_TRACE*` variable, and with git's
985
+ * terminal prompt off, so a refused credential fails rather than asking a developer's terminal.
986
+ */
987
+ function githubBoundEnv(gitDir) {
988
+ const env = Object.entries(mirrorEnv(gitDir)).filter(([key]) => !key.startsWith('GIT_TRACE'));
989
+ return { ...Object.fromEntries(env), GIT_TERMINAL_PROMPT: '0' };
990
+ }
991
+ /** A full SHA-1 or SHA-256 object ID, so nothing else can reach a refspec or rev-list's stdin. */
992
+ export function isObjectId(value) {
993
+ return (value.length === 40 || value.length === 64) && /^[0-9a-f]+$/.test(value);
994
+ }
995
+ async function isBareRepository(gitDir) {
996
+ try {
997
+ await fs.access(gitDir);
998
+ // rev-parse echoes `--end-of-options`, so only the first line is the answer.
999
+ const out = await simpleGit({ baseDir: gitDir })
1000
+ .env(mirrorEnv(gitDir))
1001
+ .raw(['rev-parse', '--is-bare-repository', '--end-of-options']);
1002
+ return out.split('\n')[0] === 'true';
1003
+ }
1004
+ catch {
1005
+ return false;
1006
+ }
1007
+ }
1008
+ /**
1009
+ * Create an empty bare repository at `gitDir` under the mirror's pins, with loose and packed refs
1010
+ * whatever a user's config defaults to: the sweeps know that format's leftovers.
1011
+ */
1012
+ async function initMirror(gitDir) {
1013
+ await fs.mkdir(gitDir, { mode: 0o700 });
1014
+ await simpleGit({ baseDir: gitDir, ...mirrorGitOptions() })
1015
+ .env(mirrorEnv(gitDir))
1016
+ .raw(['-c', 'init.defaultRefFormat=files', 'init', '--quiet', '--bare', '--end-of-options']);
1017
+ }
1018
+ /** git in `repo`, one of the gateway's own, under `config` (the mirror's pins by default). */
1019
+ function runInRepo(repo, timeoutMs, args, options = {}) {
1020
+ return runGit(args, {
1021
+ cwd: repo,
1022
+ env: mirrorEnv(repo),
1023
+ config: options.config ?? mirrorPins(),
1024
+ stdin: options.stdin,
1025
+ inactivityTimeoutMs: timeoutMs,
1026
+ deadlineMs: options.deadlineMs,
1027
+ signal: options.signal,
1028
+ onActivity: options.onActivity,
1029
+ });
1030
+ }