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,977 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import fs from 'node:fs/promises';
3
+ import http from 'node:http';
4
+ import net from 'node:net';
5
+ import path from 'node:path';
6
+ import { PassThrough } from 'node:stream';
7
+ import { pipeline } from 'node:stream/promises';
8
+ import { getErrorMessage, isNodeError } from '../utils/error.js';
9
+ import { GitHubTransientError } from './github-mirror.js';
10
+ import { BUNDLE_CONTENT_TYPE, BUNDLE_ROUTE, FETCH_ROUTE, GitHubGatewayProtocolError, GitHubGatewayUnreachableError, JSON_ROUTES, MAX_JSON_ANSWER_BYTES, MAX_JSON_REQUEST_BYTES, PUSH_REQUEST_HEADER, PUSH_ROUTE, UnknownFetchJobError, answers, decodeError, decodePushHeader, encodeError, encodePushHeader, fetchPoll, fetchRequest, fetchStarted, pushAnswer, pushRequest, readAnswerBody, refusedRequestError, requests, } from './github-gateway-wire.js';
11
+ import { gatewayLogWarn } from './gateway-egress.js';
12
+ /**
13
+ * The GitHub gateway over a local unix socket, HTTP/1.1: {@link serveGitHubGateway} puts a
14
+ * {@link GitHubGateway} (the in-process one, `createLocalGitHubGateway`) behind a socket, and
15
+ * {@link connectGitHubGateway} is the worker's {@link GitHubGateway} that reaches it there. Every
16
+ * operation is one request, validated at both ends (worker/github-gateway-wire.ts). Bundles are
17
+ * request and response bodies, streamed; a push's JSON travels in a header beside its bundle, so
18
+ * no framing is invented.
19
+ *
20
+ * A fetch is a job, not a request: `POST fetch` starts it and answers `202` with the job's id, and
21
+ * the client polls `GET fetch/<id>` until it reports the ref map and the bundle. The job runs
22
+ * detached from every client, so a whole-repository fetch outlasts any one request, and an abort
23
+ * ends only the client's wait. Every other operation is given a signal that aborts when its client
24
+ * goes away; `prepare`, which every caller shares, is not.
25
+ */
26
+ /** How long one poll of a fetch job waits for it to finish before answering that it runs on. */
27
+ const MAX_POLL_WAIT_MS = 25_000;
28
+ /** How long a finished fetch job's answer stays readable. */
29
+ const FETCH_JOB_RETENTION_MS = 10 * 60_000;
30
+ /** The most fetch jobs held at once, running or finished; past it the oldest finished one goes. */
31
+ const MAX_FETCH_JOBS = 64;
32
+ /** The most fetch jobs running at once. */
33
+ const MAX_RUNNING_FETCH_JOBS = 8;
34
+ /** How long a request may go on sending a body after its answer, before it is cut off. */
35
+ const DISCARD_BODY_MS = 10_000;
36
+ /** The most connections the server holds at once. */
37
+ const MAX_CONNECTIONS = 64;
38
+ /** The default {@link GitHubGatewayServerOptions.downloadStallMs}: two minutes. */
39
+ const DEFAULT_DOWNLOAD_STALL_MS = 120_000;
40
+ /** The slowest a bundle download may run, on average over each stall period. */
41
+ const MIN_DOWNLOAD_BYTES_PER_SECOND = 16 * 1024;
42
+ /**
43
+ * Serve `gateway` on a unix socket.
44
+ *
45
+ * Who can connect is the socket node's permissions. In production the worker and the gateway run
46
+ * as different users, so systemd creates the socket and passes it as `fd`, and the socket unit
47
+ * sets its owner and mode (`SocketUser=`, `SocketMode=`). A `path`, for development and tests,
48
+ * where both ends are the same user, is bound here: only inside a directory this process owns and
49
+ * no one else can enter, so no other user can reach, plant or swap the node, which is then made
50
+ * `0600`.
51
+ *
52
+ * `gateway.prepare()` starts once the server holds its socket, and every operation but
53
+ * {@link UNPREPARED_OPERATIONS} waits for it; a failure is the gateway's fatal error, answered to
54
+ * each of them.
55
+ */
56
+ export async function serveGitHubGateway(gateway, listen, options = {}) {
57
+ const handler = new GatewayRequestHandler(gateway, options);
58
+ const server = http.createServer((req, res) => {
59
+ void handler.handle(req, res);
60
+ });
61
+ // A push's body waits for the mirror as long as the push does; the caller's signal bounds that.
62
+ server.requestTimeout = 0;
63
+ server.headersTimeout = 60_000;
64
+ server.maxHeadersCount = 32;
65
+ server.maxConnections = MAX_CONNECTIONS;
66
+ if ('fd' in listen) {
67
+ await listenOn(server, { fd: listen.fd });
68
+ }
69
+ else {
70
+ await assertPrivateDirectory(path.dirname(listen.path));
71
+ await removeStaleSocket(listen.path);
72
+ await listenOn(server, { path: listen.path });
73
+ try {
74
+ await fs.chmod(listen.path, 0o600);
75
+ }
76
+ catch (err) {
77
+ // Nothing returned would close it: a server left listening answers no operation.
78
+ await closeServer(server, handler);
79
+ throw err;
80
+ }
81
+ }
82
+ handler.start();
83
+ return {
84
+ idle: () => handler.idle(),
85
+ drain: async (graceMs) => {
86
+ // New requests are answered `stopping` (GitHubGatewayStoppingError to the worker). The socket
87
+ // stays open meanwhile: under socket activation systemd holds it too, so closing ours would
88
+ // only leave new connections waiting for the next gateway.
89
+ handler.close();
90
+ let timer;
91
+ await Promise.race([
92
+ handler.requestsSettled(),
93
+ new Promise((resolve) => {
94
+ timer = setTimeout(resolve, graceMs);
95
+ }),
96
+ ]);
97
+ clearTimeout(timer);
98
+ await closeServer(server, handler);
99
+ },
100
+ close: () => closeServer(server, handler),
101
+ };
102
+ }
103
+ function closeServer(server, handler) {
104
+ return new Promise((resolve) => {
105
+ handler.close();
106
+ server.close(() => resolve());
107
+ server.closeAllConnections();
108
+ });
109
+ }
110
+ function listenOn(server, options) {
111
+ return new Promise((resolve, reject) => {
112
+ server.once('error', reject);
113
+ server.listen(options, () => {
114
+ server.off('error', reject);
115
+ resolve();
116
+ });
117
+ });
118
+ }
119
+ /**
120
+ * Refuse to bind in a directory another user could enter: they could connect to the socket before
121
+ * the `chmod`, which is not atomic with the bind, or replace or plant the node if they can write.
122
+ */
123
+ async function assertPrivateDirectory(dir) {
124
+ const stat = await fs.lstat(dir);
125
+ const uid = process.getuid?.();
126
+ if (!stat.isDirectory() ||
127
+ (uid !== undefined && (stat.uid !== uid || (stat.mode & 0o077) !== 0))) {
128
+ throw new Error(`Refusing to serve the GitHub gateway in ${dir}: it must be a directory this process owns ` +
129
+ `and no one else can enter`);
130
+ }
131
+ }
132
+ /**
133
+ * Remove a socket a stopped gateway of this user's left at `socketPath`. Refuse anything else: a
134
+ * file that is not a socket, someone else's, or one a running gateway still answers on.
135
+ */
136
+ async function removeStaleSocket(socketPath) {
137
+ let stat;
138
+ try {
139
+ stat = await fs.lstat(socketPath);
140
+ }
141
+ catch (err) {
142
+ if (isNodeError(err) && err.code === 'ENOENT')
143
+ return;
144
+ throw err;
145
+ }
146
+ const uid = process.getuid?.();
147
+ if (!stat.isSocket() || (uid !== undefined && stat.uid !== uid)) {
148
+ throw new Error(`Refusing to serve the GitHub gateway at ${socketPath}: something else is there`);
149
+ }
150
+ if (await isServed(socketPath)) {
151
+ throw new Error(`Refusing to serve the GitHub gateway at ${socketPath}: a gateway already serves there`);
152
+ }
153
+ await fs.unlink(socketPath);
154
+ }
155
+ /** Whether something accepts a connection on the socket at `socketPath`. */
156
+ function isServed(socketPath) {
157
+ return new Promise((resolve) => {
158
+ const socket = net.connect(socketPath);
159
+ socket.once('connect', () => {
160
+ socket.destroy();
161
+ resolve(true);
162
+ });
163
+ socket.once('error', () => resolve(false));
164
+ });
165
+ }
166
+ // ---------------------------------------------------------------------------
167
+ // Server
168
+ // ---------------------------------------------------------------------------
169
+ /** A request refused before any operation ran. */
170
+ class RefusedRequest extends Error {
171
+ constructor(status, message) {
172
+ super(message);
173
+ this.status = status;
174
+ }
175
+ }
176
+ /**
177
+ * The operations that run before, or without, a successful `prepare()`: they use the instance role,
178
+ * never the GitHub credential or the mirror. A draining worker can then complete its termination
179
+ * while `prepare()` runs, or during the hold before a gateway whose `prepare()` failed exits.
180
+ */
181
+ const UNPREPARED_OPERATIONS = new Set([
182
+ 'health',
183
+ 'termination',
184
+ 'completeLifecycle',
185
+ ]);
186
+ class GatewayRequestHandler {
187
+ constructor(gateway, options) {
188
+ this.gateway = gateway;
189
+ this.options = options;
190
+ this.startPreparing = () => undefined;
191
+ this.jobs = new Map();
192
+ this.closed = false;
193
+ /** Aborted by {@link close}, which releases the termination polls a drain would otherwise wait out. */
194
+ this.stopping = new AbortController();
195
+ this.loggedStopping = false;
196
+ this.inFlight = 0;
197
+ this.idleWaiters = [];
198
+ this.prepared = new Promise((resolve, reject) => {
199
+ this.startPreparing = () => {
200
+ gateway.prepare().then(resolve, reject);
201
+ };
202
+ });
203
+ // Answered to every operation; never unhandled while none is asked for.
204
+ this.prepared.catch(() => undefined);
205
+ this.routes = new Map(Object.entries(JSON_ROUTES).map(([op, route]) => [route, op]));
206
+ this.runners = {
207
+ prepare: async () => ({}),
208
+ health: (_, signal) => gateway.health(signal),
209
+ defaultBranch: async (_, signal) => ({ branch: await gateway.defaultBranch(signal) }),
210
+ isProtectedBranch: async ({ branch }, signal) => ({
211
+ protected: await gateway.isProtectedBranch(branch, signal),
212
+ }),
213
+ createPullRequest: async (input, signal) => {
214
+ const { number, url } = await gateway.createPullRequest(input, signal);
215
+ return { number, url };
216
+ },
217
+ updatePullRequest: async ({ number, ...update }, signal) => {
218
+ await gateway.updatePullRequest(number, update, signal);
219
+ return {};
220
+ },
221
+ getPullRequest: async ({ number }, signal) => {
222
+ const { merged, merged_at, state, node_id } = await gateway.getPullRequest(number, signal);
223
+ return { merged, merged_at, state, node_id };
224
+ },
225
+ createOrUpdatePullRequest: async (input, signal) => {
226
+ const { number, url, created } = await gateway.createOrUpdatePullRequest(input, signal);
227
+ return { number, url, created };
228
+ },
229
+ convertPullRequestToDraft: async ({ number }, signal) => {
230
+ await gateway.convertPullRequestToDraft(number, signal);
231
+ return {};
232
+ },
233
+ deleteBranch: async ({ branch }, signal) => {
234
+ await gateway.deleteBranch(branch, signal);
235
+ return {};
236
+ },
237
+ authSnapshot: async (_, signal) => gateway.authSnapshot(signal),
238
+ termination: async (_, signal) => {
239
+ try {
240
+ return {
241
+ notice: await gateway.termination(AbortSignal.any([signal, this.stopping.signal])),
242
+ };
243
+ }
244
+ catch (err) {
245
+ // A drain releases the poll: the worker polls again, and is told the gateway is stopping.
246
+ if (this.stopping.signal.aborted && !signal.aborted)
247
+ return { notice: null };
248
+ throw err;
249
+ }
250
+ },
251
+ completeLifecycle: async (_, signal) => {
252
+ await gateway.completeLifecycle(signal);
253
+ return {};
254
+ },
255
+ };
256
+ }
257
+ /** Start the gateway's `prepare()`: see {@link prepared}. */
258
+ start() {
259
+ this.startPreparing();
260
+ }
261
+ close() {
262
+ this.closed = true;
263
+ this.stopping.abort(new Error('The GitHub gateway is stopping'));
264
+ }
265
+ /** Resolves once no request is being handled; fetch jobs aside. */
266
+ requestsSettled() {
267
+ if (this.inFlight === 0)
268
+ return Promise.resolve();
269
+ return new Promise((resolve) => this.idleWaiters.push(resolve));
270
+ }
271
+ async idle() {
272
+ for (;;) {
273
+ await Promise.all([...this.jobs.values()].map((job) => job.settled));
274
+ if (this.inFlight === 0) {
275
+ if ([...this.jobs.values()].every((job) => job.answer !== null))
276
+ return;
277
+ continue;
278
+ }
279
+ await new Promise((resolve) => this.idleWaiters.push(resolve));
280
+ }
281
+ }
282
+ async handle(req, res) {
283
+ this.inFlight++;
284
+ try {
285
+ await this.route(req, res);
286
+ }
287
+ finally {
288
+ if (--this.inFlight === 0) {
289
+ const waiters = this.idleWaiters;
290
+ this.idleWaiters = [];
291
+ for (const resolve of waiters)
292
+ resolve();
293
+ }
294
+ }
295
+ }
296
+ async route(req, res) {
297
+ // Aborts the operation when the client goes away before its answer.
298
+ const controller = new AbortController();
299
+ res.once('close', () => {
300
+ if (!res.writableFinished)
301
+ controller.abort(new Error('The worker closed the request'));
302
+ });
303
+ try {
304
+ if (this.closed) {
305
+ // The worker rebuilds it as GitHubGatewayStoppingError, and releases the task it hits.
306
+ if (!this.loggedStopping) {
307
+ this.loggedStopping = true;
308
+ gatewayLogWarn('Answering requests as stopping while the gateway drains');
309
+ }
310
+ sendJson(req, res, 503, {
311
+ error: {
312
+ kind: 'stopping',
313
+ message: 'it no longer takes requests',
314
+ },
315
+ });
316
+ return;
317
+ }
318
+ const url = new URL(req.url ?? '/', 'http://gateway');
319
+ const route = url.pathname;
320
+ const op = this.routes.get(route);
321
+ if (op !== undefined) {
322
+ assertMethod(req, 'POST');
323
+ await this.json(op, req, res, controller.signal);
324
+ }
325
+ else if (route === PUSH_ROUTE) {
326
+ assertMethod(req, 'POST');
327
+ await this.push(req, res, controller.signal);
328
+ }
329
+ else if (route === FETCH_ROUTE) {
330
+ assertMethod(req, 'POST');
331
+ await this.startFetch(req, res);
332
+ }
333
+ else if (route.startsWith(`${FETCH_ROUTE}/`)) {
334
+ assertMethod(req, 'GET');
335
+ await this.pollFetch(route.slice(FETCH_ROUTE.length + 1), url, req, res);
336
+ }
337
+ else if (route.startsWith(`${BUNDLE_ROUTE}/`)) {
338
+ assertMethod(req, 'GET');
339
+ await this.bundle(route.slice(BUNDLE_ROUTE.length + 1), res);
340
+ }
341
+ else {
342
+ throw new RefusedRequest(404, `no such operation: ${route.slice(0, 200)}`);
343
+ }
344
+ }
345
+ catch (err) {
346
+ this.fail(req, res, err);
347
+ }
348
+ }
349
+ async json(op, req, res, signal) {
350
+ if (!UNPREPARED_OPERATIONS.has(op))
351
+ await this.prepared;
352
+ const input = parseRequest(requests[op], await readJsonBody(req), op);
353
+ const run = this.runners[op];
354
+ const value = await run(input, signal);
355
+ sendJson(req, res, 200, { value: parseAnswer(answers[op], value, op) });
356
+ }
357
+ async push(req, res, signal) {
358
+ await this.prepared;
359
+ const header = req.headers[PUSH_REQUEST_HEADER];
360
+ if (typeof header !== 'string') {
361
+ throw new RefusedRequest(400, `a push carries its request in ${PUSH_REQUEST_HEADER}`);
362
+ }
363
+ let raw;
364
+ try {
365
+ raw = decodePushHeader(header);
366
+ }
367
+ catch (err) {
368
+ throw new RefusedRequest(400, `unreadable ${PUSH_REQUEST_HEADER}: ${getErrorMessage(err)}`);
369
+ }
370
+ const { bundle: hasBundle, ...request } = parseRequest(pushRequest, raw, 'push');
371
+ let body;
372
+ if (hasBundle) {
373
+ if (req.headers['content-type'] !== BUNDLE_CONTENT_TYPE) {
374
+ throw new RefusedRequest(415, `a push's bundle is ${BUNDLE_CONTENT_TYPE}`);
375
+ }
376
+ // Not the request itself: the gateway destroys the stream it is given when it is done with
377
+ // it, and destroying the request would destroy the connection the answer goes back on.
378
+ const stream = new PassThrough();
379
+ // Its reader, if it has one yet, sees an error; an unread one must not crash the gateway.
380
+ stream.on('error', () => undefined);
381
+ req.pipe(stream);
382
+ // A request that dies part way never ends its pipe: fail the body, or its reader waits on it
383
+ // for ever, holding the gateway's one-at-a-time intake.
384
+ req.once('close', () => {
385
+ // Not `complete`: that says the bytes arrived, and a destroyed request drops what it
386
+ // buffered unread.
387
+ if (!req.readableEnded) {
388
+ stream.destroy(new Error('the worker closed the request before the bundle arrived whole'));
389
+ }
390
+ });
391
+ body = stream;
392
+ }
393
+ else if (req.headers['content-length'] !== '0' &&
394
+ req.headers['content-length'] !== undefined) {
395
+ throw new RefusedRequest(400, 'a push without a bundle has no body');
396
+ }
397
+ else if (req.headers['transfer-encoding'] !== undefined) {
398
+ throw new RefusedRequest(400, 'a push without a bundle has no body');
399
+ }
400
+ try {
401
+ const answer = await this.gateway.push(request, body, signal);
402
+ sendJson(req, res, 200, { value: parseAnswer(pushAnswer, answer, 'push') });
403
+ }
404
+ finally {
405
+ if (body !== undefined)
406
+ req.unpipe(body);
407
+ }
408
+ }
409
+ async startFetch(req, res) {
410
+ await this.prepared;
411
+ const { have } = parseRequest(fetchRequest, await readJsonBody(req), 'fetch');
412
+ this.evictJobs();
413
+ const running = [...this.jobs.values()].filter((job) => job.settledAt === null).length;
414
+ if (running >= MAX_RUNNING_FETCH_JOBS) {
415
+ throw new RefusedRequest(503, 'too many fetches are under way; retry after one finishes');
416
+ }
417
+ // Past the cap, the job that finished longest ago goes: a poll for it then fetches afresh.
418
+ while (this.jobs.size >= MAX_FETCH_JOBS) {
419
+ let oldest = null;
420
+ for (const [held, job] of this.jobs) {
421
+ if (job.settledAt !== null && (oldest === null || job.settledAt < oldest[1])) {
422
+ oldest = [held, job.settledAt];
423
+ }
424
+ }
425
+ if (oldest === null)
426
+ break;
427
+ this.jobs.delete(oldest[0]);
428
+ }
429
+ const id = randomBytes(16).toString('hex');
430
+ const job = {
431
+ answer: null,
432
+ settledAt: null,
433
+ progress: 0,
434
+ // No signal: the job outlives the request that started it (see the module doc).
435
+ settled: this.gateway
436
+ .fetch({ have }, undefined, () => {
437
+ job.progress++;
438
+ })
439
+ .then(({ refs, bundleId }) => {
440
+ job.answer = {
441
+ ok: true,
442
+ value: { state: 'done', progress: job.progress, refs: [...refs], bundleId },
443
+ };
444
+ }, (err) => {
445
+ job.answer = { ok: false, error: encodeError(err) };
446
+ }),
447
+ };
448
+ void job.settled.then(() => {
449
+ job.settledAt = Date.now();
450
+ });
451
+ this.jobs.set(id, job);
452
+ sendJson(req, res, 202, { value: parseAnswer(fetchStarted, { job: id }, 'fetch') });
453
+ }
454
+ /** Answer a poll for job `id`, waiting up to the request's `wait` (capped) for it to finish. */
455
+ async pollFetch(id, url, req, res) {
456
+ await this.prepared;
457
+ this.evictJobs();
458
+ const job = this.jobs.get(id);
459
+ if (job === undefined) {
460
+ sendJson(req, res, 404, {
461
+ error: {
462
+ kind: 'unknown-job',
463
+ message: 'The GitHub gateway holds no such fetch: it restarted, or the fetch expired',
464
+ },
465
+ });
466
+ return;
467
+ }
468
+ const waitParam = url.searchParams.get('wait') ?? '0';
469
+ if (!/^\d{1,9}$/.test(waitParam))
470
+ throw new RefusedRequest(400, 'wait must be milliseconds');
471
+ const wait = Math.min(Number(waitParam), MAX_POLL_WAIT_MS);
472
+ if (job.answer === null && wait > 0) {
473
+ let timer;
474
+ let onClose = () => undefined;
475
+ await Promise.race([
476
+ job.settled,
477
+ new Promise((resolve) => {
478
+ timer = setTimeout(resolve, wait);
479
+ onClose = resolve;
480
+ res.once('close', onClose);
481
+ }),
482
+ ]);
483
+ clearTimeout(timer);
484
+ res.off('close', onClose);
485
+ }
486
+ const answer = job.answer;
487
+ if (answer === null) {
488
+ sendJson(req, res, 200, { value: { state: 'running', progress: job.progress } });
489
+ }
490
+ else if (answer.ok)
491
+ sendJson(req, res, 200, { value: parseAnswer(fetchPoll, answer.value, 'fetch') });
492
+ else
493
+ sendJson(req, res, 500, { error: answer.error });
494
+ }
495
+ /** Drop finished jobs past their retention. */
496
+ evictJobs() {
497
+ const now = Date.now();
498
+ for (const [id, job] of this.jobs) {
499
+ if (job.settledAt !== null && now - job.settledAt >= FETCH_JOB_RETENTION_MS) {
500
+ this.jobs.delete(id);
501
+ }
502
+ }
503
+ }
504
+ async bundle(id, res) {
505
+ await this.prepared;
506
+ if (!/^[0-9A-Za-z]{1,128}$/.test(id))
507
+ throw new RefusedRequest(404, 'no such bundle');
508
+ const stream = await this.gateway.openBundle(id);
509
+ res.writeHead(200, { 'content-type': BUNDLE_CONTENT_TYPE });
510
+ // The stream flows only as fast as the client takes it. Each stall period the client must
511
+ // have taken the period's share of the minimum rate, so neither a client that
512
+ // stopped reading nor one reading a trickle keeps the bundle's file: cut it off.
513
+ const stallMs = this.options.downloadStallMs ?? DEFAULT_DOWNLOAD_STALL_MS;
514
+ const rate = this.options.minDownloadBytesPerSecond ?? MIN_DOWNLOAD_BYTES_PER_SECOND;
515
+ const floor = (rate * stallMs) / 1000;
516
+ let taken = 0;
517
+ stream.on('data', (chunk) => {
518
+ taken += chunk.length;
519
+ });
520
+ const check = setInterval(() => {
521
+ if (taken < floor)
522
+ res.destroy();
523
+ taken = 0;
524
+ }, stallMs);
525
+ try {
526
+ // A client gone part way is its own failure to report; the stream is released either way.
527
+ await pipeline(stream, res).catch(() => undefined);
528
+ }
529
+ finally {
530
+ clearInterval(check);
531
+ }
532
+ }
533
+ /** Answer a failure, unless the answer has already begun; then the connection is cut. */
534
+ fail(req, res, err) {
535
+ if (err instanceof RefusedRequest) {
536
+ gatewayLogWarn(`Refused a request to ${req.method ?? '?'} ${(req.url ?? '?').slice(0, 200)}: ${err.message}`);
537
+ }
538
+ if (res.headersSent) {
539
+ res.destroy();
540
+ return;
541
+ }
542
+ if (err instanceof RefusedRequest) {
543
+ sendJson(req, res, err.status, {
544
+ error: { kind: 'refused', status: err.status, message: err.message },
545
+ });
546
+ }
547
+ else {
548
+ sendJson(req, res, 500, { error: encodeError(err) });
549
+ }
550
+ }
551
+ }
552
+ function assertMethod(req, method) {
553
+ if (req.method !== method)
554
+ throw new RefusedRequest(405, `${(req.url ?? '').slice(0, 200)} takes ${method}`);
555
+ }
556
+ /** Read a JSON request body of a declared length, no larger than {@link MAX_JSON_REQUEST_BYTES}. */
557
+ async function readJsonBody(req) {
558
+ if (req.headers['content-type'] !== 'application/json') {
559
+ throw new RefusedRequest(415, 'the request body must be application/json');
560
+ }
561
+ const declared = req.headers['content-length'];
562
+ if (declared === undefined || !/^\d{1,10}$/.test(declared)) {
563
+ throw new RefusedRequest(411, 'the request must declare its length');
564
+ }
565
+ if (Number(declared) > MAX_JSON_REQUEST_BYTES) {
566
+ throw new RefusedRequest(413, `the request is larger than ${MAX_JSON_REQUEST_BYTES} bytes`);
567
+ }
568
+ const chunks = [];
569
+ for await (const chunk of req)
570
+ chunks.push(chunk);
571
+ try {
572
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
573
+ }
574
+ catch {
575
+ throw new RefusedRequest(400, 'the request body is not JSON');
576
+ }
577
+ }
578
+ function parseRequest(schema, value, op) {
579
+ const parsed = schema.safeParse(value);
580
+ if (!parsed.success) {
581
+ // Codes and paths only: an issue's own text can quote the request back, at any length.
582
+ const issues = parsed.error.issues
583
+ .slice(0, 5)
584
+ .map((issue) => `${issue.code} at ${issue.path.join('.').slice(0, 64) || '(root)'}`)
585
+ .join('; ');
586
+ throw new RefusedRequest(400, `malformed ${op} request: ${issues}`);
587
+ }
588
+ return parsed.data;
589
+ }
590
+ /** The gateway's own answer, held to the wire format before it leaves. */
591
+ function parseAnswer(schema, value, op) {
592
+ const parsed = schema.safeParse(value);
593
+ if (!parsed.success) {
594
+ throw new Error(`The GitHub gateway's ${op} answer does not fit the wire format`);
595
+ }
596
+ return parsed.data;
597
+ }
598
+ /**
599
+ * Answer with `body`. When the request's body has not all arrived (an answer that refuses it part
600
+ * way), the answer is written whole but ended only once the client stops sending, which it does on
601
+ * reading the answer; meanwhile the rest of the body is read and dropped. Ending it at once would
602
+ * close the connection under the client's next write, which can then fail before it has read the
603
+ * answer. A client that keeps sending is cut off after {@link DISCARD_BODY_MS}.
604
+ */
605
+ function sendJson(req, res, status, body) {
606
+ const text = JSON.stringify(body);
607
+ res.writeHead(status, {
608
+ 'content-type': 'application/json',
609
+ 'content-length': Buffer.byteLength(text),
610
+ });
611
+ if (req.complete) {
612
+ res.end(text);
613
+ return;
614
+ }
615
+ res.write(text);
616
+ const timer = setTimeout(() => req.socket.destroy(), DISCARD_BODY_MS);
617
+ timer.unref();
618
+ const finish = () => {
619
+ clearTimeout(timer);
620
+ res.end();
621
+ };
622
+ req.once('end', finish);
623
+ req.once('close', finish);
624
+ req.resume();
625
+ }
626
+ // ---------------------------------------------------------------------------
627
+ // Client
628
+ // ---------------------------------------------------------------------------
629
+ /** How long the client asks each poll of a fetch job to wait. */
630
+ const POLL_WAIT_MS = MAX_POLL_WAIT_MS;
631
+ /** How long `health` waits for the gateway: `getSecret` alone has taken 87 s. */
632
+ const HEALTH_WAIT_MS = 300_000;
633
+ /**
634
+ * How long `authSnapshot` waits: a membership request per user, one at a time, so tens of thousands
635
+ * of users take tens of minutes. A later call joins a fetch an earlier one stopped waiting for.
636
+ */
637
+ const AUTH_SNAPSHOT_WAIT_MS = 30 * 60_000;
638
+ /** How long `termination` and `completeLifecycle` wait: a poll's wait is well inside it. */
639
+ const INSTANCE_ROLE_WAIT_MS = 60_000;
640
+ /** `signal` bounded by `ms`. */
641
+ function boundedBy(ms, signal) {
642
+ const bound = AbortSignal.timeout(ms);
643
+ return signal === undefined ? bound : AbortSignal.any([signal, bound]);
644
+ }
645
+ /** How many polls in a row the connection may break off before the fetch fails. */
646
+ const POLL_RETRIES = 3;
647
+ /** How long the client waits before polling again after a broken poll. */
648
+ const POLL_RETRY_MS = 1_000;
649
+ /** Resolve after `ms`, or reject with `signal`'s reason when it aborts first. */
650
+ function delay(ms, signal) {
651
+ return new Promise((resolve, reject) => {
652
+ if (signal?.aborted) {
653
+ reject(signal.reason);
654
+ return;
655
+ }
656
+ const onAbort = () => {
657
+ clearTimeout(timer);
658
+ reject(signal?.reason);
659
+ };
660
+ const timer = setTimeout(() => {
661
+ signal?.removeEventListener('abort', onAbort);
662
+ resolve();
663
+ }, ms);
664
+ signal?.addEventListener('abort', onAbort, { once: true });
665
+ });
666
+ }
667
+ /**
668
+ * The worker's {@link GitHubGateway} for a gateway served at `socketPath`. Every answer is
669
+ * validated before the worker sees it, and every error is rebuilt (worker/github-gateway-wire.ts,
670
+ * `decodeError`), so the task runner classifies it as it would in-process. A
671
+ * gateway it cannot reach throws {@link GitHubGatewayUnreachableError}.
672
+ */
673
+ export function connectGitHubGateway(socketPath, options = {}) {
674
+ return new GitHubGatewayClient(socketPath, options);
675
+ }
676
+ /** The default {@link GitHubGatewayClientOptions.fetchInactivityMs}: two minutes. */
677
+ const DEFAULT_FETCH_INACTIVITY_MS = 120_000;
678
+ class GitHubGatewayClient {
679
+ constructor(socketPath, options) {
680
+ this.socketPath = socketPath;
681
+ this.fetchInactivityMs = options.fetchInactivityMs ?? DEFAULT_FETCH_INACTIVITY_MS;
682
+ }
683
+ async prepare() {
684
+ await this.call('prepare', {});
685
+ }
686
+ /**
687
+ * Waits up to {@link HEALTH_WAIT_MS} for the gateway to serve and prepare: its secret reads, which
688
+ * come before it serves, can be slow.
689
+ */
690
+ health(signal) {
691
+ return this.call('health', {}, boundedBy(HEALTH_WAIT_MS, signal));
692
+ }
693
+ async defaultBranch(signal) {
694
+ return (await this.call('defaultBranch', {}, signal)).branch;
695
+ }
696
+ async isProtectedBranch(branch, signal) {
697
+ return (await this.call('isProtectedBranch', { branch }, signal)).protected;
698
+ }
699
+ /**
700
+ * Start a fetch job and poll it. A poll for a job the gateway does not hold (it restarted, or
701
+ * the job expired) starts one new job; a second loss fails.
702
+ */
703
+ async fetch(request, signal, onProgress) {
704
+ const have = [...new Set(request.have)];
705
+ for (let attempt = 1;; attempt++) {
706
+ const started = await this.send({
707
+ method: 'POST',
708
+ path: FETCH_ROUTE,
709
+ json: { have },
710
+ signal,
711
+ });
712
+ const { job } = await readAnswer(started, fetchStarted, signal);
713
+ try {
714
+ return await this.pollFetch(job, signal, onProgress);
715
+ }
716
+ catch (err) {
717
+ if (err instanceof UnknownFetchJobError && attempt === 1)
718
+ continue;
719
+ throw err;
720
+ }
721
+ }
722
+ }
723
+ /**
724
+ * Poll job `job` until it finishes, giving up (transient) once its progress has not moved for
725
+ * {@link GitHubGatewayClientOptions.fetchInactivityMs}. A poll the connection breaks off (the
726
+ * gateway restarting) is tried again, up to {@link POLL_RETRIES} times in a row,
727
+ * {@link POLL_RETRY_MS} apart: a restarted gateway then answers that it no longer holds the job.
728
+ */
729
+ async pollFetch(job, signal, onProgress) {
730
+ // Each poll answers well inside the bound, so a stalled job is seen in time.
731
+ const wait = Math.max(1, Math.min(POLL_WAIT_MS, Math.floor(this.fetchInactivityMs / 2)));
732
+ let progress = 0;
733
+ let movedAt = performance.now();
734
+ for (let broken = 0;;) {
735
+ let poll;
736
+ try {
737
+ const res = await this.send({
738
+ method: 'GET',
739
+ path: `${FETCH_ROUTE}/${job}?wait=${wait}`,
740
+ signal,
741
+ });
742
+ poll = await readAnswer(res, fetchPoll, signal);
743
+ }
744
+ catch (err) {
745
+ if (!(err instanceof GitHubGatewayUnreachableError) || ++broken > POLL_RETRIES)
746
+ throw err;
747
+ await delay(POLL_RETRY_MS, signal);
748
+ continue;
749
+ }
750
+ broken = 0;
751
+ if (poll.progress > progress) {
752
+ onProgress?.();
753
+ progress = poll.progress;
754
+ movedAt = performance.now();
755
+ }
756
+ if (poll.state === 'done')
757
+ return { refs: new Map(poll.refs), bundleId: poll.bundleId };
758
+ if (performance.now() - movedAt >= this.fetchInactivityMs) {
759
+ throw new GitHubTransientError('github-fetch-failed', `The GitHub gateway's fetch showed no progress for ${this.fetchInactivityMs}ms; it runs on, ` +
760
+ 'and the next fetch joins it');
761
+ }
762
+ }
763
+ }
764
+ async openBundle(bundleId) {
765
+ const res = await this.send({
766
+ method: 'GET',
767
+ path: `${BUNDLE_ROUTE}/${encodeURIComponent(bundleId)}`,
768
+ });
769
+ if (res.statusCode === 200 && res.headers['content-type'] === BUNDLE_CONTENT_TYPE)
770
+ return res;
771
+ await readAnswer(res, answers.prepare);
772
+ throw new GitHubGatewayProtocolError('The GitHub gateway answered a bundle with no bundle');
773
+ }
774
+ async push(request, bundle, signal) {
775
+ const header = encodePushHeader({
776
+ branch: request.branch,
777
+ sha: request.sha,
778
+ ...(request.lease === undefined ? {} : { lease: request.lease }),
779
+ prerequisites: [...request.prerequisites],
780
+ bundle: bundle !== undefined,
781
+ });
782
+ const res = await this.send({
783
+ method: 'POST',
784
+ path: PUSH_ROUTE,
785
+ headers: {
786
+ [PUSH_REQUEST_HEADER]: header,
787
+ ...(bundle === undefined
788
+ ? { 'content-length': '0' }
789
+ : { 'content-type': BUNDLE_CONTENT_TYPE }),
790
+ },
791
+ body: bundle,
792
+ signal,
793
+ });
794
+ const answer = await readAnswer(res, pushAnswer, signal);
795
+ if (answer.kind === 'need-objects')
796
+ return answer;
797
+ const { kind, pastStaleLease } = answer;
798
+ return answer.moved
799
+ ? { kind, moved: true, from: answer.from, to: answer.to, pastStaleLease }
800
+ : { kind, moved: false, pastStaleLease };
801
+ }
802
+ createPullRequest(request, signal) {
803
+ return this.call('createPullRequest', request, signal);
804
+ }
805
+ async updatePullRequest(number, update, signal) {
806
+ await this.call('updatePullRequest', { number, ...update }, signal);
807
+ }
808
+ getPullRequest(number, signal) {
809
+ return this.call('getPullRequest', { number }, signal);
810
+ }
811
+ createOrUpdatePullRequest(request, signal) {
812
+ return this.call('createOrUpdatePullRequest', request, signal);
813
+ }
814
+ async convertPullRequestToDraft(number, signal) {
815
+ await this.call('convertPullRequestToDraft', { number }, signal);
816
+ }
817
+ async deleteBranch(branch, signal) {
818
+ await this.call('deleteBranch', { branch }, signal);
819
+ }
820
+ authSnapshot(signal) {
821
+ return this.call('authSnapshot', {}, boundedBy(AUTH_SNAPSHOT_WAIT_MS, signal));
822
+ }
823
+ async termination(signal) {
824
+ return (await this.call('termination', {}, boundedBy(INSTANCE_ROLE_WAIT_MS, signal))).notice;
825
+ }
826
+ async completeLifecycle(signal) {
827
+ await this.call('completeLifecycle', {}, boundedBy(INSTANCE_ROLE_WAIT_MS, signal));
828
+ }
829
+ async call(op, input, signal) {
830
+ const res = await this.send({ method: 'POST', path: JSON_ROUTES[op], json: input, signal });
831
+ return readAnswer(res, answers[op], signal);
832
+ }
833
+ /**
834
+ * Send one request and resolve with the response once it begins. An abort destroys the request
835
+ * (the gateway then aborts the operation) and rejects with the signal's reason. A streamed `body` stops as soon as the answer begins: an answer
836
+ * before the whole body means the gateway refused it, and destroying it stops its writer.
837
+ */
838
+ send(options) {
839
+ const { signal, body } = options;
840
+ return new Promise((resolve, reject) => {
841
+ if (signal?.aborted) {
842
+ body?.destroy();
843
+ reject(signal.reason);
844
+ return;
845
+ }
846
+ const text = options.json === undefined ? undefined : JSON.stringify(options.json);
847
+ const req = http.request({
848
+ socketPath: this.socketPath,
849
+ method: options.method,
850
+ path: options.path,
851
+ agent: false,
852
+ headers: {
853
+ ...options.headers,
854
+ ...(text === undefined
855
+ ? {}
856
+ : {
857
+ 'content-type': 'application/json',
858
+ 'content-length': String(Buffer.byteLength(text)),
859
+ }),
860
+ },
861
+ });
862
+ let settled = false;
863
+ let bodyError = null;
864
+ const settle = () => {
865
+ settled = true;
866
+ signal?.removeEventListener('abort', onAbort);
867
+ };
868
+ const onAbort = () => {
869
+ if (settled)
870
+ return;
871
+ settle();
872
+ body?.destroy();
873
+ req.destroy();
874
+ reject(signal?.reason);
875
+ };
876
+ signal?.addEventListener('abort', onAbort, { once: true });
877
+ req.once('response', (res) => {
878
+ if (settled) {
879
+ res.destroy();
880
+ return;
881
+ }
882
+ settle();
883
+ if (body !== undefined && !body.readableEnded) {
884
+ body.unpipe(req);
885
+ body.destroy();
886
+ req.end();
887
+ }
888
+ resolve(res);
889
+ });
890
+ req.on('error', (err) => {
891
+ if (settled)
892
+ return;
893
+ settle();
894
+ body?.destroy();
895
+ reject(bodyError ??
896
+ new GitHubGatewayUnreachableError(`Could not reach the GitHub gateway at ${this.socketPath}: ${getErrorMessage(err)}`));
897
+ });
898
+ if (body !== undefined) {
899
+ body.once('error', (err) => {
900
+ // The bundle's own failure (its git), not the gateway's: reported as itself.
901
+ bodyError = err;
902
+ req.destroy(err);
903
+ });
904
+ body.pipe(req);
905
+ }
906
+ else {
907
+ req.end(text);
908
+ }
909
+ });
910
+ }
911
+ }
912
+ /**
913
+ * Read a JSON answer: its value, held to `schema`, or its error, rebuilt. Bounded to
914
+ * {@link MAX_JSON_ANSWER_BYTES}; a connection that breaks part way is unreachable.
915
+ */
916
+ async function readAnswer(res, schema, signal) {
917
+ const onAbort = () => res.destroy();
918
+ signal?.addEventListener('abort', onAbort, { once: true });
919
+ let text;
920
+ try {
921
+ if (res.headers['content-type'] !== 'application/json') {
922
+ res.destroy();
923
+ // node's own refusals (a 431 for an oversize header) carry no JSON.
924
+ throw refusedRequestError(`The GitHub gateway answered ${res.statusCode ?? '?'} with no JSON`, res.statusCode);
925
+ }
926
+ const chunks = [];
927
+ let total = 0;
928
+ try {
929
+ for await (const chunk of res) {
930
+ total += chunk.length;
931
+ if (total > MAX_JSON_ANSWER_BYTES) {
932
+ throw new GitHubGatewayProtocolError(`The GitHub gateway's answer is larger than ${MAX_JSON_ANSWER_BYTES} bytes`);
933
+ }
934
+ chunks.push(chunk);
935
+ }
936
+ }
937
+ catch (err) {
938
+ if (signal?.aborted)
939
+ throw signal.reason;
940
+ if (err instanceof GitHubGatewayProtocolError)
941
+ throw err;
942
+ throw new GitHubGatewayUnreachableError(`The GitHub gateway broke off its answer: ${getErrorMessage(err)}`);
943
+ }
944
+ if (signal?.aborted)
945
+ throw signal.reason;
946
+ text = Buffer.concat(chunks).toString('utf8');
947
+ }
948
+ finally {
949
+ signal?.removeEventListener('abort', onAbort);
950
+ }
951
+ let json;
952
+ try {
953
+ json = JSON.parse(text);
954
+ }
955
+ catch {
956
+ throw new GitHubGatewayProtocolError('The GitHub gateway answered with malformed JSON');
957
+ }
958
+ const body = readAnswerBody(json);
959
+ if (body === null) {
960
+ throw new GitHubGatewayProtocolError('The GitHub gateway answered outside the wire format');
961
+ }
962
+ const status = res.statusCode ?? 0;
963
+ if ('error' in body) {
964
+ if (status < 400) {
965
+ throw new GitHubGatewayProtocolError(`The GitHub gateway answered ${status} with an error`);
966
+ }
967
+ throw decodeError(body.error);
968
+ }
969
+ if (status < 200 || status >= 300) {
970
+ throw new GitHubGatewayProtocolError(`The GitHub gateway answered ${status} with a value`);
971
+ }
972
+ const value = schema.safeParse(body.value);
973
+ if (!value.success) {
974
+ throw new GitHubGatewayProtocolError(`The GitHub gateway's answer does not fit the wire format: ${value.error.message}`);
975
+ }
976
+ return value.data;
977
+ }