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,726 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createCanopyOctokit, createOrUpdatePullRequest, } from '../github-service.js';
|
|
4
|
+
import { getErrorMessage } from '../utils/error.js';
|
|
5
|
+
import { isStaleLeaseRejection, workflowPushRefusalFile } from '../utils/git.js';
|
|
6
|
+
import { isTransientAuthFailure } from './github-auth.js';
|
|
7
|
+
import { GitHubMirror, GitHubTransientError, RefusedPushError, assertPlainBranchName, isObjectId, } from './github-mirror.js';
|
|
8
|
+
import { BranchPolicy, GatewayRefusalError, assertPullRequestText } from './branch-policy.js';
|
|
9
|
+
import { gatewayLog, gatewayLogError, gatewayLogWarn, redactEgress, redactError, redactValue, registerLiveSecret, } from './gateway-egress.js';
|
|
10
|
+
import { GatewayInstance, untilAborted } from './gateway-instance.js';
|
|
11
|
+
/** The most `have` IDs one fetch takes. */
|
|
12
|
+
export const MAX_FETCH_HAVE = 10_000;
|
|
13
|
+
/** How long a completed GitHub fetch is reused: one sync cycle's fetch, reconcile and settings push. */
|
|
14
|
+
const FETCH_COALESCE_MS = 30_000;
|
|
15
|
+
/**
|
|
16
|
+
* How long a fetched auth snapshot is answered again. Well under the default 15-minute auth-cache
|
|
17
|
+
* refresh interval, and a worker asking in a loop starts no fetch within it of a success.
|
|
18
|
+
*/
|
|
19
|
+
const AUTH_SNAPSHOT_REUSE_MS = 60_000;
|
|
20
|
+
/**
|
|
21
|
+
* The longest an operation waits for a credential re-read in flight before it goes on with the
|
|
22
|
+
* credential it has. A provider can take 87 s for one `getSecret`, and an operation that waited it
|
|
23
|
+
* out would die at the task deadline for nothing; the task's retry still sees a rotated token, and
|
|
24
|
+
* a re-read a hostile worker armed cannot hold every push for its length.
|
|
25
|
+
*/
|
|
26
|
+
const REFRESH_JOIN_CAP_MS = 15_000;
|
|
27
|
+
/** The most prerequisites one push names. */
|
|
28
|
+
const MAX_PUSH_PREREQUISITES = 3;
|
|
29
|
+
/** The default cap on a pushed bundle, in bytes. */
|
|
30
|
+
const DEFAULT_MAX_BUNDLE_BYTES = 1024 * 1024 * 1024;
|
|
31
|
+
/**
|
|
32
|
+
* A push to GitHub failed: `rejected` on the first attempt, `rejected-after-stale-lease` on the
|
|
33
|
+
* plain retry after GitHub refused a stale lease. The message is the failure's own: git's output,
|
|
34
|
+
* for a refusal, which the task runner classifies. A {@link RefusedPushError} from the first
|
|
35
|
+
* attempt is thrown as itself.
|
|
36
|
+
*/
|
|
37
|
+
export class GitHubPushError extends Error {
|
|
38
|
+
constructor(kind, message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.kind = kind;
|
|
41
|
+
this.name = 'GitHubPushError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** A moved ref for a log line: `<old7>..<new7>`, or `new branch at <new7>`. */
|
|
45
|
+
export function describeMove(update) {
|
|
46
|
+
const to = update.to.slice(0, 7);
|
|
47
|
+
return update.from === null ? `new branch at ${to}` : `${update.from.slice(0, 7)}..${to}`;
|
|
48
|
+
}
|
|
49
|
+
/** The in-process {@link GitHubGateway}. */
|
|
50
|
+
export function createLocalGitHubGateway(options) {
|
|
51
|
+
return new LocalGitHubGateway(options);
|
|
52
|
+
}
|
|
53
|
+
class LocalGitHubGateway {
|
|
54
|
+
constructor(options) {
|
|
55
|
+
this.options = options;
|
|
56
|
+
/** The credential re-read in flight, which every credentialed operation joins. */
|
|
57
|
+
this.refreshing = null;
|
|
58
|
+
/** The GitHub fetch that operations join: in flight, or completed and still reusable. */
|
|
59
|
+
this.githubFetch = null;
|
|
60
|
+
/** Pushes about to run and branch deletions settled at GitHub: each ends reuse of an earlier fetch. */
|
|
61
|
+
this.pushes = 0;
|
|
62
|
+
this.preparing = null;
|
|
63
|
+
/** The auth snapshot being fetched, which every concurrent caller joins. */
|
|
64
|
+
this.snapshotFetch = null;
|
|
65
|
+
/** The last snapshot fetched, and when (`performance.now()`). */
|
|
66
|
+
this.lastSnapshot = null;
|
|
67
|
+
this.preflight = 'pending';
|
|
68
|
+
this.mirror = new GitHubMirror(options.stateDirectory, options.timeoutMs);
|
|
69
|
+
this.instance = new GatewayInstance(options);
|
|
70
|
+
this.octokit =
|
|
71
|
+
options.octokit ??
|
|
72
|
+
createCanopyOctokit(options.auth.octokitAuth, { warn: gatewayLogWarn, error: gatewayLogError },
|
|
73
|
+
// A token the credential provider rotated or an App minted is scrubbed from the first
|
|
74
|
+
// request that carries it, before any `credential()` call has registered it.
|
|
75
|
+
{
|
|
76
|
+
onAuthorization: (value) => registerLiveSecret(value.replace(/^(?:token|bearer)\s+/i, '')),
|
|
77
|
+
});
|
|
78
|
+
this.policy = new BranchPolicy({
|
|
79
|
+
baseBranch: options.baseBranch,
|
|
80
|
+
protectedBranches: options.protectedBranches,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
get repo() {
|
|
84
|
+
return { owner: this.options.githubOwner, repo: this.options.githubRepo };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Run one operation that uses the credential.
|
|
88
|
+
*
|
|
89
|
+
* It first JOINS a re-read in flight, bounded by its own `signal` and by
|
|
90
|
+
* {@link REFRESH_JOIN_CAP_MS}, so the 5s/10s/20s task retries see a rotated token. Then, if the
|
|
91
|
+
* operation fails after any of its GitHub-bound git commands or Octokit calls failed, it ARMS a
|
|
92
|
+
* re-read. Not gated on the failure looking
|
|
93
|
+
* auth-shaped: a dead token's git failure is a plain exit 128 with no HTTP status, and a token
|
|
94
|
+
* that lost access fails as a 404 or a 403. What never arms it: a refusal of the gateway's own
|
|
95
|
+
* (`GatewayRefusalError`, whatever came before it; an invalid object ID), anything else local, a lock file the mirror's own
|
|
96
|
+
* housekeeping holds (`isOwnLockFailure`), and an Octokit 422 (`api`). A 404 does arm it, the
|
|
97
|
+
* branch-already-gone one `deleteBranch` callers tolerate included. What bounds a caller that
|
|
98
|
+
* fails on purpose is the floors behind `auth.refreshCredential`:
|
|
99
|
+
* `refreshGitHubTokenMinIntervalMs` (github-auth.ts) and the provider's own. On the GitHub App
|
|
100
|
+
* path a re-read is a no-op.
|
|
101
|
+
*
|
|
102
|
+
* The re-read starts detached, after the failure has been delivered: a provider can take 87s for
|
|
103
|
+
* one `getSecret`, longer than a task's whole deadline.
|
|
104
|
+
*
|
|
105
|
+
* What it returns or throws leaves the gateway, so it is scrubbed here (worker/gateway-egress.ts).
|
|
106
|
+
*/
|
|
107
|
+
async credentialed(signal, operation) {
|
|
108
|
+
try {
|
|
109
|
+
await this.joinRefresh(signal);
|
|
110
|
+
const reach = { failed: false };
|
|
111
|
+
try {
|
|
112
|
+
return redactValue(await operation(reach));
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
if (reach.failed && !(err instanceof GatewayRefusalError))
|
|
116
|
+
this.armRefresh();
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
throw redactError(err);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* One Octokit call inside {@link credentialed}, whose failure arms a re-read unless it is a 422.
|
|
126
|
+
* A 422 is GitHub validating the request, never judging the credential, and the worker treats
|
|
127
|
+
* two as success: a head branch GitHub already deleted after a merge (`isRefAlreadyGoneError`),
|
|
128
|
+
* and a pull request with no commits between (`isNoCommitsBetweenError`). Arming on them would
|
|
129
|
+
* spend the provider's floor on routine traffic and hold off the re-read a real rotation needs.
|
|
130
|
+
*/
|
|
131
|
+
async api(reach, call) {
|
|
132
|
+
try {
|
|
133
|
+
return await call();
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
if (!(typeof err === 'object' && err !== null && 'status' in err && err.status === 422)) {
|
|
137
|
+
reach.failed = true;
|
|
138
|
+
}
|
|
139
|
+
throw err;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The credential for one operation's GitHub-bound git, resolved ONCE per operation, so every
|
|
144
|
+
* command in it carries the same token and a resolution failure cannot replace a git failure
|
|
145
|
+
* being classified. Async because a GitHub App's installation token is minted on demand and
|
|
146
|
+
* lasts about an hour, so nothing may cache it. A mint failure propagates as thrown, carrying
|
|
147
|
+
* the `.status` `isPermanentTaskFailure` classifies on.
|
|
148
|
+
*/
|
|
149
|
+
async credential(reach) {
|
|
150
|
+
const { remoteUrl, githubOwner, githubRepo } = this.options;
|
|
151
|
+
const url = remoteUrl === undefined
|
|
152
|
+
? `https://github.com/${githubOwner}/${githubRepo}.git`
|
|
153
|
+
: typeof remoteUrl === 'string'
|
|
154
|
+
? remoteUrl
|
|
155
|
+
: await remoteUrl();
|
|
156
|
+
const token = await this.api(reach, () => this.options.auth.resolveGitToken());
|
|
157
|
+
registerLiveSecret(token);
|
|
158
|
+
return {
|
|
159
|
+
url,
|
|
160
|
+
token,
|
|
161
|
+
onFailure: () => {
|
|
162
|
+
reach.failed = true;
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Wait for a re-read in flight, for at most {@link REFRESH_JOIN_CAP_MS}, unless `signal` aborts
|
|
168
|
+
* first; past the cap the operation goes on with the credential as it stands.
|
|
169
|
+
*/
|
|
170
|
+
async joinRefresh(signal) {
|
|
171
|
+
const inFlight = this.refreshing;
|
|
172
|
+
if (inFlight === null)
|
|
173
|
+
return;
|
|
174
|
+
let timer;
|
|
175
|
+
const capped = new Promise((resolve) => {
|
|
176
|
+
timer = setTimeout(resolve, this.options.refreshJoinCapMs ?? REFRESH_JOIN_CAP_MS);
|
|
177
|
+
});
|
|
178
|
+
try {
|
|
179
|
+
await untilAborted(Promise.race([inFlight, capped]), signal);
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Bring the mirror up to GitHub, coalesced: join the GitHub fetch in flight, or reuse one that
|
|
187
|
+
* completed less than {@link FETCH_COALESCE_MS} ago and is still current ({@link isCurrent}), or
|
|
188
|
+
* else start one with the credential `resolve` gives, resolved only then, so a joining operation
|
|
189
|
+
* mints no token. A failed fetch is never joined or reused. The fetch is
|
|
190
|
+
* detached from every caller's `signal`, which ends only that caller's wait: a fetch of a whole
|
|
191
|
+
* repository can outlast any one caller, and killing it on an abort would start it over each
|
|
192
|
+
* time. Its own bound is the mirror's inactivity timeout. It runs in a mirror session of its
|
|
193
|
+
* own, which repairs a corrupt mirror first ({@link GitHubMirror.heal}), and its failure re-reads
|
|
194
|
+
* the credential when it reached GitHub, whoever started it.
|
|
195
|
+
*/
|
|
196
|
+
async joinGitHubFetch(resolve, signal) {
|
|
197
|
+
const current = this.githubFetch;
|
|
198
|
+
const window = this.options.fetchCoalesceMs ?? FETCH_COALESCE_MS;
|
|
199
|
+
const reusable = current !== null &&
|
|
200
|
+
!current.failed &&
|
|
201
|
+
(current.completedAt === null ||
|
|
202
|
+
(Date.now() - current.completedAt < window && this.isCurrent(current.snapshot)));
|
|
203
|
+
const job = reusable ? current : this.startGitHubFetch(resolve);
|
|
204
|
+
return untilAborted(job.done, signal);
|
|
205
|
+
}
|
|
206
|
+
/** Whether the mirror still holds GitHub as the fetch that saw `snapshot` left it. */
|
|
207
|
+
isCurrent(snapshot) {
|
|
208
|
+
return (snapshot !== null &&
|
|
209
|
+
snapshot.generation === this.mirror.generation &&
|
|
210
|
+
snapshot.pushes === this.pushes);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Run `fn` in a mirror session over what a GitHub fetch put in the mirror: `join` one, and
|
|
214
|
+
* check inside the session that nothing has changed the mirror or GitHub since
|
|
215
|
+
* ({@link isCurrent}). A session between the two may have re-created the mirror, leaving no
|
|
216
|
+
* heads, which would read as GitHub having none, or pushed. When something has, `join` again,
|
|
217
|
+
* which then fetches afresh; a second miss is transient.
|
|
218
|
+
*/
|
|
219
|
+
async afterGitHubFetch(join, fn) {
|
|
220
|
+
for (let attempt = 1;; attempt++) {
|
|
221
|
+
const snapshot = await join();
|
|
222
|
+
const result = await this.mirror.exclusive(async (session) => this.isCurrent(snapshot) ? { value: await fn(session) } : null);
|
|
223
|
+
if (result !== null)
|
|
224
|
+
return result.value;
|
|
225
|
+
if (attempt === 2) {
|
|
226
|
+
throw new GitHubTransientError('github-fetch-failed', 'The GitHub mirror or GitHub changed after each of two GitHub fetches');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** Start a GitHub fetch, taking its place at once, before the credential resolves. */
|
|
231
|
+
startGitHubFetch(resolve) {
|
|
232
|
+
let reachedGitHub = false;
|
|
233
|
+
const run = async () => {
|
|
234
|
+
const credential = await resolve();
|
|
235
|
+
const own = {
|
|
236
|
+
url: credential.url,
|
|
237
|
+
token: credential.token,
|
|
238
|
+
onFailure: () => {
|
|
239
|
+
reachedGitHub = true;
|
|
240
|
+
credential.onFailure();
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
const snapshot = await this.mirror.exclusive(async (mirror) => {
|
|
244
|
+
const pushes = this.pushes;
|
|
245
|
+
await this.mirror.heal(mirror, own);
|
|
246
|
+
await this.mirror.fetchFromGitHub(mirror, own);
|
|
247
|
+
return { generation: this.mirror.generation, pushes };
|
|
248
|
+
});
|
|
249
|
+
await this.mirror.afterFetch();
|
|
250
|
+
return snapshot;
|
|
251
|
+
};
|
|
252
|
+
const job = { failed: false, completedAt: null, snapshot: null, done: run() };
|
|
253
|
+
job.done.then((snapshot) => {
|
|
254
|
+
job.completedAt = Date.now();
|
|
255
|
+
job.snapshot = snapshot;
|
|
256
|
+
}, () => {
|
|
257
|
+
job.failed = true;
|
|
258
|
+
if (reachedGitHub)
|
|
259
|
+
this.armRefresh();
|
|
260
|
+
});
|
|
261
|
+
this.githubFetch = job;
|
|
262
|
+
return job;
|
|
263
|
+
}
|
|
264
|
+
/** A push or branch deletion at GitHub: every later read must come from a fetch after it. */
|
|
265
|
+
invalidateGitHubFetch() {
|
|
266
|
+
this.pushes++;
|
|
267
|
+
this.githubFetch = null;
|
|
268
|
+
}
|
|
269
|
+
/** Start a re-read on the next turn of the event loop, unless one is already in flight. */
|
|
270
|
+
armRefresh() {
|
|
271
|
+
if (this.refreshing !== null)
|
|
272
|
+
return;
|
|
273
|
+
const refreshing = new Promise((resolve) => setImmediate(resolve))
|
|
274
|
+
.then(() => this.refreshCredential())
|
|
275
|
+
.finally(() => {
|
|
276
|
+
if (this.refreshing === refreshing)
|
|
277
|
+
this.refreshing = null;
|
|
278
|
+
});
|
|
279
|
+
this.refreshing = refreshing;
|
|
280
|
+
}
|
|
281
|
+
prepare() {
|
|
282
|
+
return (this.preparing ??= this.prepareOnce());
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* One fetch serves every caller: concurrent callers join it, and its result is answered again
|
|
286
|
+
* for {@link AUTH_SNAPSHOT_REUSE_MS}. A fetch is a request per user, so a worker asking in a loop
|
|
287
|
+
* would otherwise hold the provider's API at its rate limit. The fetch runs apart from every
|
|
288
|
+
* caller's signal, which ends only that caller's wait, so a retry after a timed-out wait joins it.
|
|
289
|
+
*/
|
|
290
|
+
async authSnapshot(signal) {
|
|
291
|
+
const provider = this.options.authSnapshot;
|
|
292
|
+
if (provider === undefined) {
|
|
293
|
+
throw new Error('The GitHub gateway has no auth provider key configured');
|
|
294
|
+
}
|
|
295
|
+
// Before a fetch starts: one no caller waits on would leave its failure unhandled.
|
|
296
|
+
signal?.throwIfAborted();
|
|
297
|
+
const reuseMs = this.options.authSnapshotReuseMs ?? AUTH_SNAPSHOT_REUSE_MS;
|
|
298
|
+
const last = this.lastSnapshot;
|
|
299
|
+
if (last !== null && performance.now() - last.at < reuseMs)
|
|
300
|
+
return last.snapshot;
|
|
301
|
+
// Not `credentialed()`: this never touches GitHub, and a provider failure must not arm the
|
|
302
|
+
// GitHub credential re-read (the provider's own 401/403 re-read lives with the provider).
|
|
303
|
+
const fetching = (this.snapshotFetch ??= (async () => provider())()
|
|
304
|
+
.then((snapshot) => {
|
|
305
|
+
const scrubbed = redactValue(snapshot);
|
|
306
|
+
this.lastSnapshot = { snapshot: scrubbed, at: performance.now() };
|
|
307
|
+
return scrubbed;
|
|
308
|
+
})
|
|
309
|
+
.finally(() => {
|
|
310
|
+
this.snapshotFetch = null;
|
|
311
|
+
}));
|
|
312
|
+
try {
|
|
313
|
+
return await untilAborted(fetching, signal);
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
throw redactError(err);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
termination(signal) {
|
|
320
|
+
return this.instance.termination(signal);
|
|
321
|
+
}
|
|
322
|
+
completeLifecycle(signal) {
|
|
323
|
+
return this.instance.completeLifecycle(signal);
|
|
324
|
+
}
|
|
325
|
+
async health(signal) {
|
|
326
|
+
let ready = false;
|
|
327
|
+
let fatalError = null;
|
|
328
|
+
if (this.preparing !== null) {
|
|
329
|
+
try {
|
|
330
|
+
await untilAborted(this.preparing, signal);
|
|
331
|
+
ready = true;
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
if (signal?.aborted)
|
|
335
|
+
throw err;
|
|
336
|
+
fatalError = redactEgress(getErrorMessage(err));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
ready,
|
|
341
|
+
fatalError,
|
|
342
|
+
preflight: this.preflight,
|
|
343
|
+
clerkConfigured: this.options.authSnapshot !== undefined,
|
|
344
|
+
protectedBranches: this.policy.describe(),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Fatal: a failed App preflight (unless it looks transient), a state directory inside the
|
|
349
|
+
* workspace, and one this process cannot create or does not own outright, since no later session
|
|
350
|
+
* could clear those and the credential is used in the mirror. Not fatal: creating the mirror,
|
|
351
|
+
* which every session retries ({@link GitHubMirror.exclusive}) while a failure memoized here
|
|
352
|
+
* would outlast a transient cause (a full disk, a failed git spawn) until the process restarted.
|
|
353
|
+
*/
|
|
354
|
+
async prepareOnce() {
|
|
355
|
+
try {
|
|
356
|
+
// Before the mirror's first fetch, whose failure would otherwise blame the repository.
|
|
357
|
+
await this.preflightGitHubAppAuth();
|
|
358
|
+
if (this.options.workspacePath !== undefined) {
|
|
359
|
+
await this.ensureStateDirectoryIsPrivate(this.options.workspacePath);
|
|
360
|
+
}
|
|
361
|
+
await this.mirror.assertStateDirectory();
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
throw redactError(err);
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
await this.mirror.ensure();
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
gatewayLogWarn(`Could not create the GitHub mirror at startup (${redactEgress(getErrorMessage(err))}); ` +
|
|
371
|
+
'the next operation tries again');
|
|
372
|
+
}
|
|
373
|
+
// The first GitHub fetch starts now, detached, so the first operation that needs one joins
|
|
374
|
+
// it under way. Its failure surfaces to whoever joins it.
|
|
375
|
+
if (this.options.prefetch === false)
|
|
376
|
+
return;
|
|
377
|
+
void this.credentialed(undefined, (reach) => this.joinGitHubFetch(() => this.credential(reach))).catch(() => undefined);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Prove a GitHub App credential works, failing startup UNLESS THE FAILURE POSITIVELY LOOKS
|
|
381
|
+
* TRANSIENT. Not always fatal, so the two credential paths degrade alike: on the token path a
|
|
382
|
+
* GitHub 502 at boot is absorbed and the loops retry, where rethrowing would crash-loop an App
|
|
383
|
+
* worker until GitHub recovered, each time telling the operator to check the key. Fail CLOSED,
|
|
384
|
+
* through `isTransientAuthFailure` rather than the inverse of `isPermanentTaskFailure`, which
|
|
385
|
+
* defaults a status-less error to transient: a key too mangled to sign with fails locally and
|
|
386
|
+
* status-lessly, and would otherwise boot a worker on a dead credential with no
|
|
387
|
+
* `lastFatalError`. No-op on the token path, where the first real request checks as much.
|
|
388
|
+
*/
|
|
389
|
+
async preflightGitHubAppAuth() {
|
|
390
|
+
if (!this.options.githubApp) {
|
|
391
|
+
this.preflight = 'not-an-app';
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
registerLiveSecret(await this.options.auth.resolveGitToken());
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
// [REDACT] Both messages below reach worker-status.json and the browser.
|
|
399
|
+
const detail = redactEgress(getErrorMessage(err));
|
|
400
|
+
if (isTransientAuthFailure(err)) {
|
|
401
|
+
this.preflight = 'unverified';
|
|
402
|
+
gatewayLogWarn(`Could not verify GitHub App authentication at startup: ${detail}. ` +
|
|
403
|
+
'Continuing — this looks transient, and the credential is minted again on first use.');
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
// Re-thrown with context, unlike credential(): nothing classifies a startup failure.
|
|
407
|
+
throw new Error(`GitHub App authentication failed: ${detail}. ` +
|
|
408
|
+
'Check the app id, the installation id, and that the private key belongs to that app.');
|
|
409
|
+
}
|
|
410
|
+
this.preflight = 'verified';
|
|
411
|
+
gatewayLog('GitHub App authentication verified');
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Refuse a state directory on the shared filesystem: the mirror there would be as writable by
|
|
415
|
+
* the Lambda as remote.git is.
|
|
416
|
+
*/
|
|
417
|
+
async ensureStateDirectoryIsPrivate(workspacePath) {
|
|
418
|
+
const workspace = await realpathOfNearest(workspacePath);
|
|
419
|
+
const state = await realpathOfNearest(this.options.stateDirectory);
|
|
420
|
+
const relative = path.relative(workspace, state);
|
|
421
|
+
if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) {
|
|
422
|
+
throw new Error(`The worker's state directory (${this.options.stateDirectory}) is inside its shared workspace ` +
|
|
423
|
+
`(${workspacePath}). It holds the repository the GitHub credential is used ` +
|
|
424
|
+
`in, so it must be somewhere the CMS Lambda cannot write.`);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
defaultBranch(signal) {
|
|
428
|
+
return this.credentialed(signal, async (reach) => {
|
|
429
|
+
const name = await this.policy.recordGitHubDefault(() => this.readDefaultBranch(reach, signal));
|
|
430
|
+
if (name === null)
|
|
431
|
+
throw new Error('GitHub reported no default branch');
|
|
432
|
+
return name;
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
/** Reads GitHub's default branch with git, in a mirror session, never through Octokit. */
|
|
436
|
+
isProtectedBranch(branch, signal) {
|
|
437
|
+
return this.credentialed(signal, (reach) => this.policy.isProtected(branch, async () => {
|
|
438
|
+
const credential = await this.credential(reach);
|
|
439
|
+
// A session can wait behind a whole-repository fetch; an abort ends only this wait.
|
|
440
|
+
return untilAborted(this.mirror.exclusive((session) => session.githubDefaultBranch(credential, signal)), signal);
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* GitHub's default branch, through the API, for the policy's operations outside a mirror
|
|
445
|
+
* session; a push reads it with git, inside its session (`MirrorSession.pushToGitHub`).
|
|
446
|
+
*/
|
|
447
|
+
async readDefaultBranch(reach, signal) {
|
|
448
|
+
const { data } = await this.api(reach, () => this.octokit.repos.get({ ...this.repo, ...(signal ? { request: { signal } } : {}) }));
|
|
449
|
+
return data.default_branch;
|
|
450
|
+
}
|
|
451
|
+
/** Validate a pull request's head and base, and apply the policy to its head and text. */
|
|
452
|
+
async assertPullRequestAllowed(reach, request, signal) {
|
|
453
|
+
const git = await this.mirror.checkGit();
|
|
454
|
+
await assertPlainBranchName(request.head, git, 'pull-request');
|
|
455
|
+
// The base is GitHub's to validate and the policy's to leave alone (BranchPolicy), and may be
|
|
456
|
+
// a configured name outside branchNameProblem's narrower set: git's rules, the length cap and
|
|
457
|
+
// no leading `-` only.
|
|
458
|
+
await assertPlainBranchName(request.base, git, 'pull-request', false);
|
|
459
|
+
await this.policy.assertPullRequestAllowed(request, () => this.readDefaultBranch(reach, signal));
|
|
460
|
+
}
|
|
461
|
+
async fetch(request, signal, onProgress) {
|
|
462
|
+
const stopWatching = onProgress === undefined ? null : this.mirror.watchActivity(onProgress);
|
|
463
|
+
try {
|
|
464
|
+
return await this.fetchOnce(request, signal);
|
|
465
|
+
}
|
|
466
|
+
finally {
|
|
467
|
+
stopWatching?.();
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
fetchOnce(request, signal) {
|
|
471
|
+
const have = [...new Set(request.have)];
|
|
472
|
+
if (have.length > MAX_FETCH_HAVE) {
|
|
473
|
+
throw new Error(`A fetch takes at most ${MAX_FETCH_HAVE} have IDs, not ${have.length}`);
|
|
474
|
+
}
|
|
475
|
+
for (const id of have)
|
|
476
|
+
assertObjectId(id);
|
|
477
|
+
return this.credentialed(signal, (reach) => this.afterGitHubFetch(() => this.joinGitHubFetch(() => this.credential(reach), signal), async (mirror) => {
|
|
478
|
+
this.mirror.assertUsable();
|
|
479
|
+
const refs = await mirror.heads();
|
|
480
|
+
const file = await this.mirror.outgoingBundlePath();
|
|
481
|
+
const created = await mirror.createBundle(have, file, signal);
|
|
482
|
+
const bundleId = await this.mirror.setPendingBundle(created ? file : null);
|
|
483
|
+
return { refs, bundleId };
|
|
484
|
+
}));
|
|
485
|
+
}
|
|
486
|
+
openBundle(bundleId) {
|
|
487
|
+
return this.mirror.openBundle(bundleId);
|
|
488
|
+
}
|
|
489
|
+
push(request, bundle, signal) {
|
|
490
|
+
return this.credentialed(signal, (reach) => this.pushOnce(request, bundle, reach, signal)).finally(
|
|
491
|
+
// A refusal can come before the body is read: never leave its writer blocked.
|
|
492
|
+
() => bundle?.destroy());
|
|
493
|
+
}
|
|
494
|
+
async pushOnce(request, bundle, reach, signal) {
|
|
495
|
+
assertObjectId(request.sha, 'a commit ID');
|
|
496
|
+
if (request.lease !== undefined)
|
|
497
|
+
assertObjectId(request.lease, 'a commit ID');
|
|
498
|
+
if (request.prerequisites.length > MAX_PUSH_PREREQUISITES) {
|
|
499
|
+
throw new Error(`A push names at most ${MAX_PUSH_PREREQUISITES} prerequisites`);
|
|
500
|
+
}
|
|
501
|
+
for (const id of request.prerequisites)
|
|
502
|
+
assertObjectId(id, 'a commit ID');
|
|
503
|
+
// Before `branch` reaches anything else: the bundle's expected head, a refspec.
|
|
504
|
+
await assertPlainBranchName(request.branch, await this.mirror.checkGit());
|
|
505
|
+
// Every attempt, and a GitHub fetch this push starts, carries this one credential (see
|
|
506
|
+
// credential()), resolved once and only when first needed.
|
|
507
|
+
let resolved = null;
|
|
508
|
+
const credential = () => (resolved ??= this.credential(reach));
|
|
509
|
+
// The join comes before the session, never inside it: the fetch takes a session of its own,
|
|
510
|
+
// and sessions are not re-entrant. So does resolving the credential, so the session never
|
|
511
|
+
// waits on GitHub's API. A body is read first, so its writer is not left waiting.
|
|
512
|
+
const session = (received) => this.afterGitHubFetch(async () => {
|
|
513
|
+
const snapshot = await this.joinGitHubFetchForPush(credential, request.branch, signal);
|
|
514
|
+
await credential();
|
|
515
|
+
return snapshot;
|
|
516
|
+
}, async (mirror) => this.pushInSession(mirror, await credential(), request, received, signal));
|
|
517
|
+
return bundle === undefined
|
|
518
|
+
? session(null)
|
|
519
|
+
: this.mirror.ingest(bundle, request, this.maxBundleBytes, session);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* The plan's step 2 (§3.2): the push goes ahead only once a GitHub fetch has SUCCEEDED, fresh or
|
|
523
|
+
* reused, so the mirror's heads are GitHub's for the reachability test and the prerequisite
|
|
524
|
+
* check; otherwise it is transient. A token mint GitHub refuses is the exception: it carries its HTTP
|
|
525
|
+
* status through, so the task runner classifies it. That is the whole clause: a mirror with no heads after a
|
|
526
|
+
* successful fetch means GitHub has none, a first push.
|
|
527
|
+
*/
|
|
528
|
+
async joinGitHubFetchForPush(credential, branch, signal) {
|
|
529
|
+
try {
|
|
530
|
+
return await this.joinGitHubFetch(credential, signal);
|
|
531
|
+
}
|
|
532
|
+
catch (err) {
|
|
533
|
+
// An HTTP failure is the credential's own (a mint refused), classified by its status.
|
|
534
|
+
if (err instanceof GitHubTransientError || hasHttpStatus(err))
|
|
535
|
+
throw err;
|
|
536
|
+
// [REDACT] The message reaches the task's lastError.
|
|
537
|
+
throw new GitHubTransientError('github-fetch-failed', `Could not fetch from GitHub before pushing ${branch}: ` +
|
|
538
|
+
redactEgress(getErrorMessage(err)).trim());
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
get maxBundleBytes() {
|
|
542
|
+
return this.options.maxBundleBytes ?? DEFAULT_MAX_BUNDLE_BYTES;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* The push, in one mirror session, every call in it a plain call (the session is not
|
|
546
|
+
* re-entrant), over the mirror a GitHub fetch just filled ({@link afterGitHubFetch}), so its
|
|
547
|
+
* heads are GitHub's for the reachability test and the prerequisite check: find `sha`, in the
|
|
548
|
+
* mirror or in the quarantine `received` goes into; push from there, which refuses a protected
|
|
549
|
+
* branch first. The mirror kills its git when `signal` aborts, and GitHub moves the ref only
|
|
550
|
+
* after receiving the whole pack, so a killed push changes nothing or is found already done by
|
|
551
|
+
* the re-run, unless git's transport child outlives it
|
|
552
|
+
* (safeinsights/canopycms#907).
|
|
553
|
+
*/
|
|
554
|
+
async pushInSession(mirror, credential, request, received, signal) {
|
|
555
|
+
const { branch, sha } = request;
|
|
556
|
+
// Never fires: finding corruption changes the generation, so afterGitHubFetch never runs this
|
|
557
|
+
// over a mirror found corrupt after its fetch. It refuses rather than push from a corrupt one.
|
|
558
|
+
this.mirror.assertUsable();
|
|
559
|
+
const heads = await mirror.heads();
|
|
560
|
+
let repo;
|
|
561
|
+
if (received === null) {
|
|
562
|
+
const type = (await mirror.objectTypes([sha])).get(sha);
|
|
563
|
+
if (type === 'missing')
|
|
564
|
+
return this.needObjects(mirror, request);
|
|
565
|
+
if (type !== 'commit')
|
|
566
|
+
throw new RefusedPushError(branch, 'not-a-commit', sha);
|
|
567
|
+
if (!(await mirror.reachableFromHeads(sha)))
|
|
568
|
+
return this.needObjects(mirror, request);
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
repo = await mirror.quarantine(received, request, heads, signal);
|
|
572
|
+
}
|
|
573
|
+
const attempt = (lease) => mirror.pushToGitHub(credential, branch, sha, {
|
|
574
|
+
lease,
|
|
575
|
+
signal,
|
|
576
|
+
repo,
|
|
577
|
+
policy: this.policy,
|
|
578
|
+
// Whatever the push does to GitHub, a fetch reused from before it may not show it.
|
|
579
|
+
onPushing: () => this.invalidateGitHubFetch(),
|
|
580
|
+
});
|
|
581
|
+
try {
|
|
582
|
+
return { kind: 'pushed', ...(await attempt(request.lease)), pastStaleLease: false };
|
|
583
|
+
}
|
|
584
|
+
catch (err) {
|
|
585
|
+
if (err instanceof RefusedPushError || err instanceof GitHubTransientError)
|
|
586
|
+
throw err;
|
|
587
|
+
const message = getErrorMessage(err);
|
|
588
|
+
// A refused lease means GitHub is not at the commit the lease names, which is routine: a
|
|
589
|
+
// task re-runs after a crash, and the marker it came from survives any failure to clear
|
|
590
|
+
// it. So retry PLAIN and let git adjudicate: a non-forced push succeeds only as a
|
|
591
|
+
// fast-forward, so it can destroy nothing. git evaluates a lease only when it has an
|
|
592
|
+
// update to apply, so an up-to-date ref never gets here. A workflow refusal is final
|
|
593
|
+
// whatever the lease, so it is not retried.
|
|
594
|
+
if (request.lease &&
|
|
595
|
+
isStaleLeaseRejection(message) &&
|
|
596
|
+
workflowPushRefusalFile(message) === null) {
|
|
597
|
+
let update;
|
|
598
|
+
try {
|
|
599
|
+
update = await attempt();
|
|
600
|
+
}
|
|
601
|
+
catch (retryErr) {
|
|
602
|
+
throw new GitHubPushError('rejected-after-stale-lease', getErrorMessage(retryErr));
|
|
603
|
+
}
|
|
604
|
+
return { kind: 'pushed', ...update, pastStaleLease: true };
|
|
605
|
+
}
|
|
606
|
+
throw new GitHubPushError('rejected', message);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
async needObjects(mirror, request) {
|
|
610
|
+
return { kind: 'need-objects', usable: await mirror.usablePrerequisites(request.prerequisites) };
|
|
611
|
+
}
|
|
612
|
+
createPullRequest(request, signal) {
|
|
613
|
+
return this.credentialed(signal, async (reach) => {
|
|
614
|
+
await this.assertPullRequestAllowed(reach, request, signal);
|
|
615
|
+
const { data } = await this.api(reach, () => this.octokit.pulls.create({ ...this.repo, ...request, request: { signal } }));
|
|
616
|
+
return { number: data.number, url: data.html_url };
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
updatePullRequest(number, update, signal) {
|
|
620
|
+
return this.credentialed(signal, async (reach) => {
|
|
621
|
+
assertPullRequestText(`Refusing to update pull request #${number}`, `#${number}`, update);
|
|
622
|
+
await this.api(reach, () => this.octokit.pulls.update({
|
|
623
|
+
...this.repo,
|
|
624
|
+
pull_number: number,
|
|
625
|
+
...update,
|
|
626
|
+
request: { signal },
|
|
627
|
+
}));
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
getPullRequest(number, signal) {
|
|
631
|
+
return this.credentialed(signal, (reach) => this.pullRequest(reach, number, signal));
|
|
632
|
+
}
|
|
633
|
+
async pullRequest(reach, number, signal) {
|
|
634
|
+
const { data } = await this.api(reach, () => this.octokit.pulls.get({ ...this.repo, pull_number: number, request: { signal } }));
|
|
635
|
+
return data;
|
|
636
|
+
}
|
|
637
|
+
createOrUpdatePullRequest(request, signal) {
|
|
638
|
+
return this.credentialed(signal, async (reach) => {
|
|
639
|
+
await this.assertPullRequestAllowed(reach, request, signal);
|
|
640
|
+
return this.api(reach, () => createOrUpdatePullRequest({
|
|
641
|
+
octokit: this.octokit,
|
|
642
|
+
...this.repo,
|
|
643
|
+
...request,
|
|
644
|
+
signal,
|
|
645
|
+
warn: gatewayLogWarn,
|
|
646
|
+
}));
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
convertPullRequestToDraft(number, signal) {
|
|
650
|
+
return this.credentialed(signal, async (reach) => {
|
|
651
|
+
// The REST API cannot convert a PR to a draft; GraphQL can.
|
|
652
|
+
const pr = await this.pullRequest(reach, number, signal);
|
|
653
|
+
await this.api(reach, () => this.octokit.graphql(`mutation($id: ID!) { convertPullRequestToDraft(input: { pullRequestId: $id }) { pullRequest { isDraft } } }`, { id: pr.node_id, request: { signal } }));
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
deleteBranch(branch, signal) {
|
|
657
|
+
return this.credentialed(signal, async (reach) => {
|
|
658
|
+
await assertPlainBranchName(branch, await this.mirror.checkGit(), 'delete');
|
|
659
|
+
await this.policy.assertDeleteAllowed(branch, () => this.readDefaultBranch(reach, signal));
|
|
660
|
+
// Once the call has settled, when GitHub may have acted: any fetch before then, a fetch whose
|
|
661
|
+
// session ran during the call included, may still show the branch.
|
|
662
|
+
try {
|
|
663
|
+
await this.api(reach, () => this.octokit.git.deleteRef({ ...this.repo, ref: `heads/${branch}`, request: { signal } }));
|
|
664
|
+
}
|
|
665
|
+
finally {
|
|
666
|
+
this.invalidateGitHubFetch();
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Re-read the credential, for {@link armRefresh}. Every consumer reads the credential per use,
|
|
672
|
+
* so a re-read repairs all of them for their next use. Never throws: the failure that armed it
|
|
673
|
+
* has already been reported. Bounded by the timeout, because every credentialed operation joins
|
|
674
|
+
* it and a provider may have no bound of its own. A losing read is not cancelled, but
|
|
675
|
+
* `auth.refreshCredential` discards a result older than one already applied.
|
|
676
|
+
*/
|
|
677
|
+
async refreshCredential() {
|
|
678
|
+
let timer;
|
|
679
|
+
const timedOut = new Promise((_, reject) => {
|
|
680
|
+
timer = setTimeout(() => reject(new Error(`the re-read did not settle within ${this.options.timeoutMs}ms`)), this.options.timeoutMs);
|
|
681
|
+
});
|
|
682
|
+
try {
|
|
683
|
+
await Promise.race([this.options.auth.refreshCredential(), timedOut]);
|
|
684
|
+
// The token path's rotated token is scrubbed before any operation uses it. On the App path
|
|
685
|
+
// the re-read is a no-op, so there is nothing new to scrub.
|
|
686
|
+
if (!this.options.githubApp)
|
|
687
|
+
registerLiveSecret(await this.options.auth.resolveGitToken());
|
|
688
|
+
}
|
|
689
|
+
catch (err) {
|
|
690
|
+
logFailedCredentialRefresh(err);
|
|
691
|
+
}
|
|
692
|
+
finally {
|
|
693
|
+
clearTimeout(timer);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function logFailedCredentialRefresh(err) {
|
|
698
|
+
// [REDACT] The message can name the secret and, on a malformed-secret path, quote what was read.
|
|
699
|
+
gatewayLogError('Failed to re-read the GitHub credential after a failure:', redactEgress(getErrorMessage(err)));
|
|
700
|
+
}
|
|
701
|
+
/** Refuse anything but a full object ID, before it can reach git. */
|
|
702
|
+
function assertObjectId(value, what = 'an object ID') {
|
|
703
|
+
if (!isObjectId(value))
|
|
704
|
+
throw new Error(`Not ${what}: ${JSON.stringify(value.slice(0, 80))}`);
|
|
705
|
+
}
|
|
706
|
+
/** `target` with symlinks resolved as far as it exists, so a link cannot hide where it lands. */
|
|
707
|
+
async function realpathOfNearest(target) {
|
|
708
|
+
const missing = [];
|
|
709
|
+
let current = path.resolve(target);
|
|
710
|
+
for (;;) {
|
|
711
|
+
try {
|
|
712
|
+
return path.join(await fs.realpath(current), ...missing.reverse());
|
|
713
|
+
}
|
|
714
|
+
catch {
|
|
715
|
+
const parent = path.dirname(current);
|
|
716
|
+
if (parent === current)
|
|
717
|
+
return path.resolve(target);
|
|
718
|
+
missing.push(path.basename(current));
|
|
719
|
+
current = parent;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
/** Whether `err` carries an HTTP status, as an Octokit or a token mint's failure does. */
|
|
724
|
+
function hasHttpStatus(err) {
|
|
725
|
+
return (typeof err === 'object' && err !== null && 'status' in err && typeof err.status === 'number');
|
|
726
|
+
}
|