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.
- package/CHANGELOG.md +2131 -0
- package/README.md +47 -63
- package/dist/ai/generate.d.ts +5 -0
- package/dist/ai/generate.js +244 -115
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/json-to-markdown.d.ts +21 -1
- package/dist/ai/json-to-markdown.js +120 -57
- package/dist/ai/references.d.ts +72 -0
- package/dist/ai/references.js +223 -0
- package/dist/ai/resolve-branch.js +9 -2
- package/dist/ai/to-plain-text.d.ts +17 -8
- package/dist/ai/to-plain-text.js +18 -15
- package/dist/ai/types.d.ts +84 -3
- package/dist/api/admin-branch-health.d.ts +24 -1
- package/dist/api/admin-branch-health.js +52 -20
- package/dist/api/admin.d.ts +23 -5
- package/dist/api/admin.js +36 -1
- package/dist/api/assets.js +88 -78
- package/dist/api/branch-create-window.d.ts +12 -0
- package/dist/api/branch-create-window.js +13 -0
- package/dist/api/branch-merge.d.ts +1 -1
- package/dist/api/branch-merge.js +1 -1
- package/dist/api/branch-review.d.ts +3 -3
- package/dist/api/branch-review.js +16 -7
- package/dist/api/branch-status.d.ts +16 -6
- package/dist/api/branch-status.js +57 -12
- package/dist/api/branch-withdraw.d.ts +2 -2
- package/dist/api/branch-withdraw.js +8 -3
- package/dist/api/branch.d.ts +25 -12
- package/dist/api/branch.js +164 -103
- package/dist/api/client.d.ts +78 -58
- package/dist/api/client.js +150 -117
- package/dist/api/comments.d.ts +12 -3
- package/dist/api/comments.js +118 -9
- package/dist/api/content.d.ts +9 -9
- package/dist/api/content.js +131 -22
- package/dist/api/entries-constants.d.ts +14 -3
- package/dist/api/entries-constants.js +14 -3
- package/dist/api/entries.d.ts +40 -2
- package/dist/api/entries.js +102 -6
- package/dist/api/github-sync.d.ts +19 -0
- package/dist/api/github-sync.js +50 -3
- package/dist/api/groups.d.ts +4 -4
- package/dist/api/groups.js +2 -1
- package/dist/api/guards.js +23 -3
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +1 -0
- package/dist/api/permissions.d.ts +21 -2
- package/dist/api/permissions.js +50 -3
- package/dist/api/request-deadline.d.ts +59 -0
- package/dist/api/request-deadline.js +73 -0
- package/dist/api/resolve-references.js +8 -7
- package/dist/api/route-builder.d.ts +14 -0
- package/dist/api/route-builder.js +14 -1
- package/dist/api/schema.d.ts +11 -9
- package/dist/api/schema.js +2 -0
- package/dist/api/settings-helpers.d.ts +5 -1
- package/dist/api/settings-helpers.js +10 -2
- package/dist/api/types.d.ts +18 -0
- package/dist/api/users-constants.d.ts +6 -0
- package/dist/api/users-constants.js +6 -0
- package/dist/assets/asset-url.d.ts +28 -16
- package/dist/assets/asset-url.js +51 -25
- package/dist/assets/finalize.js +3 -0
- package/dist/assets/index.d.ts +1 -1
- package/dist/assets/materialize.d.ts +153 -0
- package/dist/assets/materialize.js +402 -0
- package/dist/assets/store-local.d.ts +19 -5
- package/dist/assets/store-local.js +69 -10
- package/dist/assets/store-s3.d.ts +38 -7
- package/dist/assets/store-s3.js +128 -22
- package/dist/assets/transform-directives.d.ts +52 -13
- package/dist/assets/transform-directives.js +87 -27
- package/dist/assets/transform.d.ts +3 -3
- package/dist/assets/transform.js +13 -8
- package/dist/assets/types.d.ts +31 -4
- package/dist/auth/cache.d.ts +1 -1
- package/dist/auth/cache.js +1 -1
- package/dist/auth/caching-auth-plugin.d.ts +2 -0
- package/dist/auth/caching-auth-plugin.js +12 -2
- package/dist/auth/file-based-auth-cache.d.ts +8 -0
- package/dist/auth/file-based-auth-cache.js +18 -8
- package/dist/auth/person-name.d.ts +5 -0
- package/dist/auth/person-name.js +8 -0
- package/dist/auth/plugin.d.ts +10 -1
- package/dist/auth/types.d.ts +2 -0
- package/dist/auth/user-metadata-lookup.d.ts +21 -0
- package/dist/auth/user-metadata-lookup.js +116 -0
- package/dist/authorization/branch.d.ts +1 -1
- package/dist/authorization/branch.js +12 -13
- package/dist/authorization/groups/schema.d.ts +4 -4
- package/dist/authorization/helpers.d.ts +8 -0
- package/dist/authorization/helpers.js +8 -0
- package/dist/authorization/index.d.ts +1 -1
- package/dist/authorization/index.js +1 -1
- package/dist/authorization/protected-branch.d.ts +5 -4
- package/dist/authorization/protected-branch.js +7 -5
- package/dist/branch-health.d.ts +35 -8
- package/dist/branch-health.js +54 -18
- package/dist/branch-metadata-error.d.ts +27 -0
- package/dist/branch-metadata-error.js +23 -0
- package/dist/branch-metadata-file.d.ts +205 -24
- package/dist/branch-metadata-file.js +126 -25
- package/dist/branch-metadata.d.ts +45 -6
- package/dist/branch-metadata.js +142 -50
- package/dist/branch-provisioning.d.ts +167 -0
- package/dist/branch-provisioning.js +501 -0
- package/dist/branch-registry.d.ts +8 -0
- package/dist/branch-registry.js +61 -9
- package/dist/branch-schema-cache.d.ts +29 -12
- package/dist/branch-schema-cache.js +64 -21
- package/dist/branch-sparse.d.ts +23 -0
- package/dist/branch-sparse.js +118 -0
- package/dist/branch-workspace.d.ts +42 -3
- package/dist/branch-workspace.js +239 -100
- package/dist/build/asset-refs.d.ts +94 -0
- package/dist/build/asset-refs.js +267 -0
- package/dist/build/generate-ai-content.js +5 -0
- package/dist/build/inline-flight.d.ts +37 -0
- package/dist/build/inline-flight.js +142 -0
- package/dist/cli/asset-refs.d.ts +52 -0
- package/dist/cli/asset-refs.js +194 -0
- package/dist/cli/cli.d.ts +15 -1
- package/dist/cli/cli.js +8543 -3193
- package/dist/cli/configured-asset-store.d.ts +12 -0
- package/dist/cli/configured-asset-store.js +46 -0
- package/dist/cli/generate-ai-content.js +4244 -1072
- package/dist/cli/init.js +217 -126
- package/dist/cli/project-detect.d.ts +2 -0
- package/dist/cli/project-detect.js +2 -0
- package/dist/cli/template-files/Dockerfile.cms.template +13 -5
- package/dist/cli/template-files/cms-stack.ts.template +16 -13
- package/dist/client.d.ts +1 -2
- package/dist/client.js +1 -2
- package/dist/comment-store.d.ts +25 -0
- package/dist/comment-store.js +43 -8
- package/dist/config/flatten.js +2 -0
- package/dist/config/helpers.js +2 -2
- package/dist/config/index.d.ts +1 -1
- package/dist/config/schemas/config.d.ts +12 -29
- package/dist/config/schemas/config.js +9 -12
- package/dist/config/schemas/field.d.ts +3 -0
- package/dist/config/schemas/field.js +58 -4
- package/dist/config/schemas/media.d.ts +4 -13
- package/dist/config/schemas/media.js +5 -8
- package/dist/config/schemas/url.d.ts +2 -20
- package/dist/config/schemas/url.js +2 -25
- package/dist/config/types.d.ts +116 -22
- package/dist/config/types.js +3 -9
- package/dist/config/validation.d.ts +25 -4
- package/dist/config/validation.js +116 -3
- package/dist/content-id-index.js +2 -2
- package/dist/content-index-generation.d.ts +1 -1
- package/dist/content-index-generation.js +1 -1
- package/dist/content-listing.d.ts +10 -1
- package/dist/content-listing.js +22 -5
- package/dist/content-reader.d.ts +9 -0
- package/dist/content-reader.js +16 -11
- package/dist/content-store.d.ts +75 -12
- package/dist/content-store.js +249 -86
- package/dist/content-tree.js +2 -2
- package/dist/context.js +5 -2
- package/dist/dangling-reference-log.d.ts +17 -0
- package/dist/dangling-reference-log.js +29 -0
- package/dist/dev-content-watcher.d.ts +1 -1
- package/dist/dev-content-watcher.js +1 -1
- package/dist/editor/BranchManager.d.ts +23 -7
- package/dist/editor/BranchManager.js +119 -54
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +1 -2
- package/dist/editor/CommentsPanel.d.ts +14 -0
- package/dist/editor/CommentsPanel.js +68 -43
- package/dist/editor/Editor.d.ts +11 -5
- package/dist/editor/Editor.js +891 -218
- package/dist/editor/EditorAuthGate.d.ts +7 -1
- package/dist/editor/EditorAuthGate.js +41 -9
- package/dist/editor/EntryNavigator.d.ts +22 -0
- package/dist/editor/EntryNavigator.js +353 -105
- package/dist/editor/FormHeader.d.ts +25 -0
- package/dist/editor/FormHeader.js +82 -0
- package/dist/editor/FormRenderer.d.ts +27 -3
- package/dist/editor/FormRenderer.js +171 -76
- package/dist/editor/PreviewFrame.d.ts +40 -0
- package/dist/editor/PreviewFrame.js +215 -0
- package/dist/editor/PreviewPane.d.ts +47 -0
- package/dist/editor/PreviewPane.js +147 -0
- package/dist/editor/PreviewToolbarTools.d.ts +22 -0
- package/dist/editor/PreviewToolbarTools.js +73 -0
- package/dist/editor/admin/SystemHealthPanel.d.ts +2 -1
- package/dist/editor/admin/SystemHealthPanel.js +163 -33
- package/dist/editor/admin/useSystemHealth.d.ts +18 -0
- package/dist/editor/admin/useSystemHealth.js +70 -1
- package/dist/editor/branch-status.d.ts +17 -0
- package/dist/editor/branch-status.js +29 -0
- package/dist/editor/canopy-path.d.ts +9 -6
- package/dist/editor/canopy-path.js +8 -11
- package/dist/editor/client-reference-resolver.d.ts +43 -16
- package/dist/editor/client-reference-resolver.js +155 -64
- package/dist/editor/comments/BranchComments.d.ts +1 -1
- package/dist/editor/comments/BranchComments.js +2 -2
- package/dist/editor/comments/CloseRefusedNotice.d.ts +4 -0
- package/dist/editor/comments/CloseRefusedNotice.js +7 -0
- package/dist/editor/comments/CommentThreadsPopover.d.ts +47 -0
- package/dist/editor/comments/CommentThreadsPopover.js +274 -0
- package/dist/editor/comments/CommentWriteAlert.d.ts +11 -0
- package/dist/editor/comments/CommentWriteAlert.js +13 -0
- package/dist/editor/comments/EntryComments.d.ts +10 -4
- package/dist/editor/comments/EntryComments.js +14 -17
- package/dist/editor/comments/FieldWrapper.d.ts +18 -4
- package/dist/editor/comments/FieldWrapper.js +62 -28
- package/dist/editor/comments/InlineCommentThread.d.ts +9 -2
- package/dist/editor/comments/InlineCommentThread.js +11 -47
- package/dist/editor/comments/ThreadCarousel.d.ts +1 -1
- package/dist/editor/comments/ThreadCarousel.js +18 -21
- package/dist/editor/comments/ThreadReplyControls.d.ts +28 -0
- package/dist/editor/comments/ThreadReplyControls.js +92 -0
- package/dist/editor/comments/field-comment-context.d.ts +14 -0
- package/dist/editor/comments/field-comment-context.js +14 -0
- package/dist/editor/comments/form-comments.d.ts +48 -0
- package/dist/editor/comments/form-comments.js +178 -0
- package/dist/editor/comments/item-anchor-copy.d.ts +6 -0
- package/dist/editor/comments/item-anchor-copy.js +6 -0
- package/dist/editor/comments/move-focus-if-unmoved.d.ts +6 -0
- package/dist/editor/comments/move-focus-if-unmoved.js +11 -0
- package/dist/editor/comments/popover-placement.d.ts +18 -0
- package/dist/editor/comments/popover-placement.js +58 -0
- package/dist/editor/comments/use-tracked-writes.d.ts +16 -0
- package/dist/editor/comments/use-tracked-writes.js +53 -0
- package/dist/editor/components/BranchAccessList.d.ts +15 -0
- package/dist/editor/components/BranchAccessList.js +52 -0
- package/dist/editor/components/BranchLockBanner.d.ts +22 -0
- package/dist/editor/components/BranchLockBanner.js +39 -0
- package/dist/editor/components/BranchPill.d.ts +32 -0
- package/dist/editor/components/BranchPill.js +33 -0
- package/dist/editor/components/BranchesDrawer.d.ts +11 -0
- package/dist/editor/components/BranchesDrawer.js +5 -0
- package/dist/editor/components/ConfirmDeleteModal.d.ts +6 -1
- package/dist/editor/components/ConfirmDeleteModal.js +3 -2
- package/dist/editor/components/DestructiveMenuItem.d.ts +7 -0
- package/dist/editor/components/DestructiveMenuItem.js +8 -0
- package/dist/editor/components/EditorCrashScreen.d.ts +17 -0
- package/dist/editor/components/EditorCrashScreen.js +22 -0
- package/dist/editor/components/EditorErrorBoundary.d.ts +38 -0
- package/dist/editor/components/EditorErrorBoundary.js +40 -0
- package/dist/editor/components/EditorHeader.d.ts +56 -106
- package/dist/editor/components/EditorHeader.js +130 -131
- package/dist/editor/components/EditorRail.d.ts +49 -0
- package/dist/editor/components/EditorRail.js +65 -0
- package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
- package/dist/editor/components/EntryBreadcrumb.js +56 -0
- package/dist/editor/components/EntryCreateModal.d.ts +12 -7
- package/dist/editor/components/EntryCreateModal.js +51 -57
- package/dist/editor/components/NoEditPermissionNotice.d.ts +6 -5
- package/dist/editor/components/NoEditPermissionNotice.js +5 -4
- package/dist/editor/components/QuickOpenPalette.d.ts +22 -0
- package/dist/editor/components/QuickOpenPalette.js +66 -0
- package/dist/editor/components/ReadOnlyDraftNotice.d.ts +11 -0
- package/dist/editor/components/ReadOnlyDraftNotice.js +12 -0
- package/dist/editor/components/ReasonedMenuItem.d.ts +15 -0
- package/dist/editor/components/ReasonedMenuItem.js +15 -0
- package/dist/editor/components/ReferencedByList.d.ts +11 -0
- package/dist/editor/components/ReferencedByList.js +28 -0
- package/dist/editor/components/RelativeTime.d.ts +10 -0
- package/dist/editor/components/RelativeTime.js +10 -0
- package/dist/editor/components/RenameEntryModal.d.ts +9 -1
- package/dist/editor/components/RenameEntryModal.js +19 -31
- package/dist/editor/components/RetryToastMessage.d.ts +9 -0
- package/dist/editor/components/RetryToastMessage.js +6 -0
- package/dist/editor/components/SaveAllResult.d.ts +25 -0
- package/dist/editor/components/SaveAllResult.js +86 -0
- package/dist/editor/components/SaveSplitButton.d.ts +30 -0
- package/dist/editor/components/SaveSplitButton.js +108 -0
- package/dist/editor/components/StagedChangesDrawer.d.ts +39 -0
- package/dist/editor/components/StagedChangesDrawer.js +92 -0
- package/dist/editor/components/UnavailableEntryNotice.d.ts +6 -0
- package/dist/editor/components/UnavailableEntryNotice.js +9 -0
- package/dist/editor/components/UnavailableTypeMessage.d.ts +6 -0
- package/dist/editor/components/UnavailableTypeMessage.js +9 -0
- package/dist/editor/components/UndoToastMessage.d.ts +18 -0
- package/dist/editor/components/UndoToastMessage.js +10 -0
- package/dist/editor/components/UserBadge.d.ts +2 -5
- package/dist/editor/components/UserBadge.js +62 -33
- package/dist/editor/components/ValidationSummary.d.ts +14 -0
- package/dist/editor/components/ValidationSummary.js +24 -0
- package/dist/editor/components/index.d.ts +2 -2
- package/dist/editor/components/index.js +2 -2
- package/dist/editor/context/ApiClientContext.d.ts +8 -2
- package/dist/editor/context/ApiClientContext.js +19 -8
- package/dist/editor/context/AssetContext.d.ts +13 -35
- package/dist/editor/context/AssetContext.js +12 -31
- package/dist/editor/context/EditorStateContext.d.ts +4 -4
- package/dist/editor/context/EditorStateContext.js +4 -4
- package/dist/editor/context/SWRProvider.d.ts +3 -3
- package/dist/editor/context/SiteMdxAllowContext.d.ts +4 -0
- package/dist/editor/context/SiteMdxAllowContext.js +7 -0
- package/dist/editor/context/index.d.ts +2 -1
- package/dist/editor/context/index.js +2 -1
- package/dist/editor/contrast.d.ts +19 -0
- package/dist/editor/contrast.js +41 -0
- package/dist/editor/copy.d.ts +18 -0
- package/dist/editor/copy.js +18 -0
- package/dist/editor/editor-config.js +16 -0
- package/dist/editor/editor-utils.d.ts +34 -8
- package/dist/editor/editor-utils.js +42 -20
- package/dist/editor/entry-search.d.ts +20 -0
- package/dist/editor/entry-search.js +34 -0
- package/dist/editor/field-breadcrumb.d.ts +12 -0
- package/dist/editor/field-breadcrumb.js +85 -0
- package/dist/editor/field-props.d.ts +86 -0
- package/dist/editor/field-props.js +27 -0
- package/dist/editor/field-target.d.ts +30 -0
- package/dist/editor/field-target.js +77 -0
- package/dist/editor/fields/BlockField.d.ts +12 -0
- package/dist/editor/fields/BlockField.js +89 -73
- package/dist/editor/fields/CodeField.d.ts +2 -0
- package/dist/editor/fields/CodeField.js +6 -2
- package/dist/editor/fields/DateTimeField.d.ts +4 -29
- package/dist/editor/fields/DateTimeField.js +79 -41
- package/dist/editor/fields/ExpandAllButton.d.ts +6 -0
- package/dist/editor/fields/ExpandAllButton.js +4 -0
- package/dist/editor/fields/FieldCrashFallback.d.ts +17 -0
- package/dist/editor/fields/FieldCrashFallback.js +30 -0
- package/dist/editor/fields/FieldLabel.d.ts +37 -0
- package/dist/editor/fields/FieldLabel.js +19 -0
- package/dist/editor/fields/ImageField.d.ts +4 -0
- package/dist/editor/fields/ImageField.js +29 -14
- package/dist/editor/fields/InlineGroupField.js +4 -3
- package/dist/editor/fields/ItemList.d.ts +42 -0
- package/dist/editor/fields/ItemList.js +130 -0
- package/dist/editor/fields/MarkdownField.d.ts +32 -0
- package/dist/editor/fields/MarkdownField.js +223 -46
- package/dist/editor/fields/MdxImageDialog.js +1 -1
- package/dist/editor/fields/NumberField.d.ts +2 -0
- package/dist/editor/fields/NumberField.js +8 -5
- package/dist/editor/fields/NumberListField.d.ts +2 -0
- package/dist/editor/fields/NumberListField.js +19 -16
- package/dist/editor/fields/ObjectField.d.ts +2 -7
- package/dist/editor/fields/ObjectField.js +15 -5
- package/dist/editor/fields/ObjectListField.d.ts +29 -0
- package/dist/editor/fields/ObjectListField.js +57 -0
- package/dist/editor/fields/ReferenceField.d.ts +2 -0
- package/dist/editor/fields/ReferenceField.js +34 -9
- package/dist/editor/fields/SelectField.d.ts +2 -0
- package/dist/editor/fields/SelectField.js +11 -2
- package/dist/editor/fields/StringListField.d.ts +4 -1
- package/dist/editor/fields/StringListField.js +22 -8
- package/dist/editor/fields/TextField.d.ts +2 -0
- package/dist/editor/fields/TextField.js +6 -2
- package/dist/editor/fields/ToggleField.d.ts +2 -0
- package/dist/editor/fields/ToggleField.js +8 -3
- package/dist/editor/fields/entry-link/InsertEntryLink.d.ts +1 -1
- package/dist/editor/fields/entry-link/InsertEntryLink.js +1 -1
- package/dist/editor/fields/form-expansion.d.ts +9 -0
- package/dist/editor/fields/form-expansion.js +2 -0
- package/dist/editor/fields/item-expansion.d.ts +15 -0
- package/dist/editor/fields/item-expansion.js +60 -0
- package/dist/editor/fields/list-item-keys.d.ts +8 -0
- package/dist/editor/fields/list-item-keys.js +50 -0
- package/dist/editor/fields/list-item-title.d.ts +20 -0
- package/dist/editor/fields/list-item-title.js +85 -0
- package/dist/editor/fields/markdown-export-options.d.ts +2 -0
- package/dist/editor/fields/markdown-export-options.js +8 -0
- package/dist/editor/fields/markdown-fidelity-visitors.d.ts +11 -0
- package/dist/editor/fields/markdown-fidelity-visitors.js +160 -0
- package/dist/editor/fields/mdx-jsx-support.d.ts +5 -1
- package/dist/editor/fields/mdx-jsx-support.js +157 -69
- package/dist/editor/fields/rich-text-failures.d.ts +5 -0
- package/dist/editor/fields/rich-text-failures.js +26 -0
- package/dist/editor/fields/toolbar-wrap.d.ts +7 -0
- package/dist/editor/fields/toolbar-wrap.js +35 -0
- package/dist/editor/form-outline.d.ts +13 -0
- package/dist/editor/form-outline.js +70 -0
- package/dist/editor/group-manager/GroupCard.d.ts +4 -1
- package/dist/editor/group-manager/GroupCard.js +12 -3
- package/dist/editor/group-manager/GroupForm.js +1 -1
- package/dist/editor/group-manager/InternalGroupsTab.d.ts +2 -1
- package/dist/editor/group-manager/InternalGroupsTab.js +4 -4
- package/dist/editor/group-manager/MemberList.d.ts +2 -1
- package/dist/editor/group-manager/MemberList.js +15 -4
- package/dist/editor/group-manager/hooks/useGroupState.d.ts +5 -0
- package/dist/editor/group-manager/hooks/useGroupState.js +30 -7
- package/dist/editor/group-manager/index.js +34 -5
- package/dist/editor/group-manager/types.d.ts +5 -2
- package/dist/editor/hooks/create-branch-request.d.ts +20 -0
- package/dist/editor/hooks/create-branch-request.js +54 -0
- package/dist/editor/hooks/save-all.d.ts +68 -0
- package/dist/editor/hooks/save-all.js +61 -0
- package/dist/editor/hooks/useBranchActions.d.ts +11 -1
- package/dist/editor/hooks/useBranchActions.js +42 -19
- package/dist/editor/hooks/useBranchManager.d.ts +21 -0
- package/dist/editor/hooks/useBranchManager.js +321 -125
- package/dist/editor/hooks/useBranchesData.d.ts +8 -4
- package/dist/editor/hooks/useBranchesData.js +9 -5
- package/dist/editor/hooks/useCommentSystem.d.ts +14 -2
- package/dist/editor/hooks/useCommentSystem.js +79 -55
- package/dist/editor/hooks/useCommentsData.d.ts +11 -7
- package/dist/editor/hooks/useCommentsData.js +20 -19
- package/dist/editor/hooks/useDraftManager.d.ts +77 -3
- package/dist/editor/hooks/useDraftManager.js +887 -94
- package/dist/editor/hooks/useEditorLayout.d.ts +53 -7
- package/dist/editor/hooks/useEditorLayout.js +98 -20
- package/dist/editor/hooks/useEntriesData.d.ts +2 -1
- package/dist/editor/hooks/useEntriesData.js +18 -4
- package/dist/editor/hooks/useEntryManager.d.ts +41 -2
- package/dist/editor/hooks/useEntryManager.js +227 -41
- package/dist/editor/hooks/useGroupManager.js +13 -16
- package/dist/editor/hooks/usePermissionManager.js +14 -6
- package/dist/editor/hooks/usePreviewMarks.d.ts +21 -0
- package/dist/editor/hooks/usePreviewMarks.js +33 -0
- package/dist/editor/hooks/useReferenceResolution.d.ts +10 -3
- package/dist/editor/hooks/useReferenceResolution.js +34 -177
- package/dist/editor/hooks/useSchemaManager.d.ts +25 -6
- package/dist/editor/hooks/useSchemaManager.js +97 -39
- package/dist/editor/hooks/useUserMetadata.d.ts +14 -4
- package/dist/editor/hooks/useUserMetadata.js +76 -47
- package/dist/editor/hooks/user-metadata-batcher.d.ts +8 -0
- package/dist/editor/hooks/user-metadata-batcher.js +33 -0
- package/dist/editor/media/AssetCard.d.ts +6 -7
- package/dist/editor/media/AssetCard.js +24 -31
- package/dist/editor/media/AssetListRow.d.ts +16 -0
- package/dist/editor/media/AssetListRow.js +41 -0
- package/dist/editor/media/CropStep.js +6 -1
- package/dist/editor/media/MediaLibrary.d.ts +4 -3
- package/dist/editor/media/MediaLibrary.js +11 -4
- package/dist/editor/media/MediaLibraryBody.d.ts +3 -3
- package/dist/editor/media/MediaLibraryBody.js +195 -80
- package/dist/editor/media/MediaLibrarySkeleton.d.ts +6 -0
- package/dist/editor/media/MediaLibrarySkeleton.js +14 -0
- package/dist/editor/media/crop-math.d.ts +3 -4
- package/dist/editor/media/crop-math.js +11 -22
- package/dist/editor/media/editor-image-src.d.ts +8 -0
- package/dist/editor/media/editor-image-src.js +10 -0
- package/dist/editor/media/format-bytes.d.ts +2 -0
- package/dist/editor/media/format-bytes.js +23 -0
- package/dist/editor/media/media-library-prefs.d.ts +23 -0
- package/dist/editor/media/media-library-prefs.js +49 -0
- package/dist/editor/media/media-library-view.d.ts +34 -0
- package/dist/editor/media/media-library-view.js +40 -0
- package/dist/editor/media/useAssetLibrary.d.ts +30 -0
- package/dist/editor/media/useAssetLibrary.js +126 -0
- package/dist/editor/media/useStableAssetOrder.d.ts +11 -0
- package/dist/editor/media/useStableAssetOrder.js +37 -0
- package/dist/editor/media/useThumbnailFailure.d.ts +6 -0
- package/dist/editor/media/useThumbnailFailure.js +16 -0
- package/dist/editor/permission-manager/GroupSelector.js +2 -2
- package/dist/editor/permission-manager/PermissionEditor.d.ts +1 -1
- package/dist/editor/permission-manager/PermissionEditor.js +15 -4
- package/dist/editor/permission-manager/PermissionLevelBadge.js +1 -1
- package/dist/editor/permission-manager/PermissionTree.d.ts +1 -1
- package/dist/editor/permission-manager/UserSelector.d.ts +1 -1
- package/dist/editor/permission-manager/UserSelector.js +3 -3
- package/dist/editor/permission-manager/index.js +12 -3
- package/dist/editor/permission-manager/types.d.ts +5 -2
- package/dist/editor/platform.d.ts +3 -0
- package/dist/editor/platform.js +9 -0
- package/dist/editor/preview-asset-base.d.ts +9 -0
- package/dist/editor/preview-asset-base.js +17 -0
- package/dist/editor/preview-bridge.d.ts +51 -27
- package/dist/editor/preview-bridge.js +103 -144
- package/dist/editor/preview-marks.d.ts +15 -0
- package/dist/editor/preview-marks.js +74 -0
- package/dist/editor/preview-path.d.ts +12 -0
- package/dist/editor/preview-path.js +45 -1
- package/dist/editor/preview-scale.d.ts +27 -0
- package/dist/editor/preview-scale.js +34 -0
- package/dist/editor/raw-asset-base.d.ts +5 -0
- package/dist/editor/raw-asset-base.js +14 -0
- package/dist/editor/relative-time.d.ts +4 -4
- package/dist/editor/relative-time.js +16 -8
- package/dist/editor/schema-editor/CollectionEditor.d.ts +20 -3
- package/dist/editor/schema-editor/CollectionEditor.js +361 -62
- package/dist/editor/schema-editor/EntryTypeEditor.d.ts +5 -1
- package/dist/editor/schema-editor/EntryTypeEditor.js +11 -6
- package/dist/editor/schema-editor/collection-draft.d.ts +22 -0
- package/dist/editor/schema-editor/collection-draft.js +40 -0
- package/dist/editor/schema-editor/index.d.ts +1 -1
- package/dist/editor/schema-label.d.ts +10 -0
- package/dist/editor/schema-label.js +17 -0
- package/dist/editor/settings-conflict.d.ts +5 -0
- package/dist/editor/settings-conflict.js +8 -0
- package/dist/editor/shell/ActivityRail.d.ts +37 -0
- package/dist/editor/shell/ActivityRail.js +91 -0
- package/dist/editor/shell/EditorShell.d.ts +17 -0
- package/dist/editor/shell/EditorShell.js +31 -0
- package/dist/editor/shell/PaneLayout.d.ts +28 -0
- package/dist/editor/shell/PaneLayout.js +116 -0
- package/dist/editor/shell/SidePanelHost.d.ts +35 -0
- package/dist/editor/shell/SidePanelHost.js +184 -0
- package/dist/editor/shell/TopBar.d.ts +12 -0
- package/dist/editor/shell/TopBar.js +14 -0
- package/dist/editor/shell/host-chrome.d.ts +16 -0
- package/dist/editor/shell/host-chrome.js +69 -0
- package/dist/editor/slug.d.ts +16 -0
- package/dist/editor/slug.js +43 -0
- package/dist/editor/theme.d.ts +2 -1
- package/dist/editor/theme.js +314 -13
- package/dist/editor/unanswered.d.ts +17 -0
- package/dist/editor/unanswered.js +26 -0
- package/dist/editor/unavailable-entry-type.d.ts +20 -0
- package/dist/editor/unavailable-entry-type.js +28 -0
- package/dist/editor/undo-removal.d.ts +57 -0
- package/dist/editor/undo-removal.js +163 -0
- package/dist/editor/user-display.d.ts +4 -0
- package/dist/editor/user-display.js +5 -0
- package/dist/editor/utils/confirm-modal.d.ts +8 -0
- package/dist/editor/utils/confirm-modal.js +12 -0
- package/dist/editor/utils/editor-errors.d.ts +19 -0
- package/dist/editor/utils/editor-errors.js +32 -0
- package/dist/editor/utils/focus-return.d.ts +56 -0
- package/dist/editor/utils/focus-return.js +106 -0
- package/dist/entry-link-resolver.d.ts +11 -1
- package/dist/entry-link-resolver.js +65 -5
- package/dist/entry-schema-registry.d.ts +3 -3
- package/dist/entry-schema-registry.js +7 -4
- package/dist/entry-schema.d.ts +49 -3
- package/dist/entry-schema.js +18 -0
- package/dist/git-manager.d.ts +172 -10
- package/dist/git-manager.js +449 -107
- package/dist/github-service.d.ts +39 -2
- package/dist/github-service.js +101 -7
- package/dist/http/handler.js +74 -9
- package/dist/http/types.d.ts +2 -0
- package/dist/http/worker-not-ready.d.ts +9 -4
- package/dist/http/worker-not-ready.js +59 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/item-keys.d.ts +46 -0
- package/dist/item-keys.js +235 -0
- package/dist/operating-mode/editor-mode-check.d.ts +15 -0
- package/dist/operating-mode/editor-mode-check.js +24 -0
- package/dist/operating-mode/mode-env.d.ts +13 -14
- package/dist/operating-mode/mode-env.js +13 -14
- package/dist/operating-mode/types.d.ts +2 -2
- package/dist/paths/branch.d.ts +0 -1
- package/dist/paths/branch.js +1 -8
- package/dist/paths/index.d.ts +1 -1
- package/dist/paths/index.js +1 -1
- package/dist/paths/validation.js +0 -2
- package/dist/preview.d.ts +8 -0
- package/dist/preview.js +7 -0
- package/dist/reference-resolver.js +1 -0
- package/dist/resolve-canopy-user.d.ts +1 -1
- package/dist/resolve-canopy-user.js +1 -1
- package/dist/schema/meta-loader.d.ts +18 -8
- package/dist/schema/meta-loader.js +42 -9
- package/dist/schema/registry-fingerprint.d.ts +14 -0
- package/dist/schema/registry-fingerprint.js +63 -0
- package/dist/schema/resolver.d.ts +7 -4
- package/dist/schema/resolver.js +7 -6
- package/dist/schema/schema-store.d.ts +7 -10
- package/dist/schema/schema-store.js +13 -11
- package/dist/schema/schema-unavailable-error.d.ts +11 -0
- package/dist/schema/schema-unavailable-error.js +15 -0
- package/dist/schema/types.d.ts +22 -0
- package/dist/schema-registry-record.d.ts +35 -0
- package/dist/schema-registry-record.js +134 -0
- package/dist/server.d.ts +79 -9
- package/dist/server.js +62 -8
- package/dist/services.d.ts +33 -5
- package/dist/services.js +155 -37
- package/dist/settings-workspace.d.ts +23 -3
- package/dist/settings-workspace.js +159 -45
- package/dist/stamp-on-publish.d.ts +36 -0
- package/dist/stamp-on-publish.js +71 -0
- package/dist/static/index.d.ts +58 -5
- package/dist/static/index.js +133 -3
- package/dist/static/seo.d.ts +5 -5
- package/dist/static/seo.js +5 -5
- package/dist/submission-attribution.d.ts +8 -0
- package/dist/submission-attribution.js +22 -0
- package/dist/system-users.d.ts +12 -0
- package/dist/system-users.js +12 -0
- package/dist/task-queue/cms-task-queue.d.ts +1 -1
- package/dist/task-queue/cms-task-queue.js +1 -1
- package/dist/task-queue/index.d.ts +1 -1
- package/dist/task-queue/index.js +1 -1
- package/dist/task-queue/task-queue.d.ts +6 -0
- package/dist/task-queue/task-queue.js +29 -0
- package/dist/task-queue/worker-status.d.ts +35 -9
- package/dist/task-queue/worker-status.js +84 -17
- package/dist/types.d.ts +90 -9
- package/dist/url-collision.js +3 -3
- package/dist/url-exclusivity-fixtures.d.ts +8 -1
- package/dist/url-exclusivity-fixtures.js +11 -1
- package/dist/user.js +2 -1
- package/dist/utils/atomic-write.js +2 -1
- package/dist/utils/base-branch.d.ts +13 -0
- package/dist/utils/base-branch.js +23 -0
- package/dist/utils/content-serialize.d.ts +15 -7
- package/dist/utils/content-serialize.js +353 -51
- package/dist/utils/content-write-lock.d.ts +6 -1
- package/dist/utils/content-write-lock.js +21 -6
- package/dist/utils/debug.d.ts +1 -1
- package/dist/utils/debug.js +5 -3
- package/dist/utils/error.js +13 -2
- package/dist/utils/escape-text.d.ts +7 -0
- package/dist/utils/escape-text.js +27 -0
- package/dist/utils/fs.d.ts +12 -4
- package/dist/utils/fs.js +18 -5
- package/dist/utils/git.d.ts +21 -1
- package/dist/utils/git.js +63 -8
- package/dist/utils/json-source-splice.d.ts +14 -0
- package/dist/utils/json-source-splice.js +120 -0
- package/dist/utils/markdown-body-splice.d.ts +39 -0
- package/dist/utils/markdown-body-splice.js +421 -0
- package/dist/utils/occ-json-write.js +18 -12
- package/dist/utils/provision-log.d.ts +25 -0
- package/dist/utils/provision-log.js +49 -0
- package/dist/utils/provisioning-lock.d.ts +32 -1
- package/dist/utils/provisioning-lock.js +78 -32
- package/dist/utils/sanitize-href.d.ts +9 -23
- package/dist/utils/sanitize-href.js +14 -28
- package/dist/utils/title-field.d.ts +10 -0
- package/dist/utils/title-field.js +47 -0
- package/dist/utils/yaml-source-splice.d.ts +58 -0
- package/dist/utils/yaml-source-splice.js +515 -0
- package/dist/validation/block-structural-keys.d.ts +1 -1
- package/dist/validation/block-structural-keys.js +1 -1
- package/dist/validation/deletion-checker.d.ts +21 -29
- package/dist/validation/deletion-checker.js +41 -117
- package/dist/validation/entry-validator.d.ts +6 -10
- package/dist/validation/entry-validator.js +19 -26
- package/dist/validation/field-traversal.d.ts +22 -0
- package/dist/validation/field-traversal.js +39 -8
- package/dist/validation/markdown-safety.d.ts +68 -0
- package/dist/validation/markdown-safety.js +596 -0
- package/dist/validation/mdx-allowlist.d.ts +55 -0
- package/dist/validation/mdx-allowlist.js +183 -0
- package/dist/validation/reference-validator.d.ts +2 -0
- package/dist/validation/reference-validator.js +3 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/branch-policy.d.ts +124 -0
- package/dist/worker/branch-policy.js +243 -0
- package/dist/worker/bundle-header.d.ts +117 -0
- package/dist/worker/bundle-header.js +253 -0
- package/dist/worker/cms-worker.d.ts +236 -142
- package/dist/worker/cms-worker.js +772 -285
- package/dist/worker/gateway-egress.d.ts +43 -0
- package/dist/worker/gateway-egress.js +209 -0
- package/dist/worker/gateway-instance.d.ts +42 -0
- package/dist/worker/gateway-instance.js +129 -0
- package/dist/worker/git-process.d.ts +81 -0
- package/dist/worker/git-process.js +234 -0
- package/dist/worker/git-sync.d.ts +34 -17
- package/dist/worker/git-sync.js +394 -131
- package/dist/worker/github-auth.d.ts +9 -9
- package/dist/worker/github-auth.js +13 -15
- package/dist/worker/github-gateway-http.d.ts +62 -0
- package/dist/worker/github-gateway-http.js +977 -0
- package/dist/worker/github-gateway-wire.d.ts +590 -0
- package/dist/worker/github-gateway-wire.js +488 -0
- package/dist/worker/github-gateway.d.ts +238 -0
- package/dist/worker/github-gateway.js +726 -0
- package/dist/worker/github-mirror-upkeep.d.ts +75 -0
- package/dist/worker/github-mirror-upkeep.js +266 -0
- package/dist/worker/github-mirror.d.ts +364 -0
- package/dist/worker/github-mirror.js +1030 -0
- package/dist/worker/github-transfer.d.ts +56 -0
- package/dist/worker/github-transfer.js +444 -0
- package/dist/worker/history-rewrite.d.ts +4 -5
- package/dist/worker/history-rewrite.js +12 -17
- package/dist/worker/provisioned-workspace.d.ts +5 -5
- package/dist/worker/provisioned-workspace.js +5 -5
- package/dist/worker/rebase.d.ts +1 -1
- package/dist/worker/rebase.js +58 -54
- package/dist/worker/remote-git-maintenance.d.ts +3 -0
- package/dist/worker/remote-git-maintenance.js +12 -0
- package/dist/worker/schema-gate.d.ts +49 -0
- package/dist/worker/schema-gate.js +226 -0
- package/dist/worker/self-checks.d.ts +107 -0
- package/dist/worker/self-checks.js +358 -0
- package/dist/worker/shared-repo-git.d.ts +96 -0
- package/dist/worker/shared-repo-git.js +459 -0
- package/dist/worker/sparse-cone.d.ts +20 -0
- package/dist/worker/sparse-cone.js +136 -0
- package/dist/worker/stale-mirror.d.ts +82 -0
- package/dist/worker/stale-mirror.js +130 -0
- package/dist/worker/task-runner.d.ts +5 -3
- package/dist/worker/task-runner.js +287 -186
- package/dist/worker/worker-context.d.ts +36 -37
- package/package.json +35 -7
- package/dist/editor/EditorPanes.d.ts +0 -10
- package/dist/editor/EditorPanes.js +0 -98
- package/dist/editor/components/EditorFooter.d.ts +0 -11
- package/dist/editor/components/EditorFooter.js +0 -9
- package/dist/editor/components/EditorSidebar.d.ts +0 -67
- 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
|
+
}
|