@jant/core 0.6.16 → 0.7.1
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/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/export.js +3 -1
- package/bin/commands/import-site.js +749 -220
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/setup.js +136 -0
- package/bin/commands/site/export.js +71 -34
- package/bin/commands/site/pull-media.js +2 -6
- package/bin/commands/site/snapshot/export.js +19 -61
- package/bin/commands/site/snapshot/import.js +23 -24
- package/bin/lib/d1-query.js +92 -18
- package/bin/lib/hugo-markdown.js +4 -0
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-pull-media.js +21 -28
- package/bin/lib/site-selection.js +94 -29
- package/bin/lib/site-snapshot.js +338 -3
- package/bin/lib/sql-export.js +68 -5
- package/bin/lib/wrangler-cli.js +37 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/bin/lib/zip-archive.js +187 -0
- package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
- package/dist/client/_assets/client-DAhoqPdr.css +2 -0
- package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
- package/dist/client/_assets/client-knSEyUJO.js +295 -0
- package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
- package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
- package/dist/github-sync-Gw4orAHk.js +8398 -0
- package/dist/index.js +2 -5
- package/dist/node.js +11 -9
- package/package.json +15 -5
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/dev-scripts.test.ts +203 -0
- package/src/__tests__/export-collection-order.test.ts +276 -0
- package/src/__tests__/export-feed-ids.test.ts +184 -0
- package/src/__tests__/export-feed-order.test.ts +294 -0
- package/src/__tests__/export-hugo-build.test.ts +390 -0
- package/src/__tests__/export-import-roundtrip.test.ts +94 -0
- package/src/__tests__/export-service.test.ts +842 -28
- package/src/__tests__/export-smart-collection.test.ts +329 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/helpers/hugo-site.ts +146 -0
- package/src/__tests__/import-site-command.test.ts +487 -1
- package/src/__tests__/mise-config.test.ts +75 -4
- package/src/__tests__/node-dev-tasks.test.ts +264 -0
- package/src/__tests__/site-export-canonical-import.test.ts +149 -0
- package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
- package/src/__tests__/snapshot-settings.test.ts +97 -0
- package/src/__tests__/snapshot-tables.test.ts +219 -0
- package/src/__tests__/sql-export.test.ts +173 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/__tests__/zip-archive.test.ts +106 -0
- package/src/app.tsx +86 -22
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker-types.ts +6 -1
- package/src/client/components/jant-repo-picker.ts +137 -108
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +622 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
- package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/extensions.ts +0 -3
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/tiptap/structural-keymap.ts +158 -47
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/d1-query.test.ts +56 -1
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +555 -73
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +601 -119
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +614 -132
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/image.test.ts +27 -1
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
- package/src/lib/__tests__/markdown.test.ts +10 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +131 -3
- package/src/lib/__tests__/schemas.test.ts +69 -5
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/timeline.test.ts +87 -0
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +439 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-repo-name.ts +45 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +62 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/image.ts +17 -4
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/markdown-manager.ts +392 -2
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-body-html.ts +11 -4
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +111 -6
- package/src/lib/schemas.ts +251 -15
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +43 -2
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/tiptap-to-markdown.ts +13 -7
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +159 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +53 -25
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-setup.test.ts +163 -0
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
- package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
- package/src/node/__tests__/runtime.test.ts +75 -0
- package/src/node/index.ts +3 -0
- package/src/node/request-handler.ts +3 -1
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/posts.test.ts +24 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/__tests__/upload.test.ts +34 -0
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +6 -3
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/internal/sites.ts +0 -1
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +161 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +44 -7
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/upload.ts +10 -3
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +9 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +714 -111
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
- package/src/routes/pages/archive.tsx +487 -379
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +43 -18
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +228 -45
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +22 -5
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/__tests__/readiness.test.ts +23 -0
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +67 -1
- package/src/runtime/readiness.ts +15 -0
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/custom-url.test.ts +34 -0
- package/src/services/__tests__/github-app-installations.test.ts +153 -0
- package/src/services/__tests__/github-sync-push.test.ts +46 -0
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/media.test.ts +31 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +236 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post-timeline.test.ts +127 -0
- package/src/services/__tests__/post.test.ts +120 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +408 -39
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +29 -24
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/alias.html +27 -1
- package/src/services/export-theme/layouts/_default/list.html +2 -63
- package/src/services/export-theme/layouts/_default/rss.xml +103 -38
- package/src/services/export-theme/layouts/collections/list.html +3 -3
- package/src/services/export-theme/layouts/featured/list.html +1 -4
- package/src/services/export-theme/layouts/index.html +40 -26
- package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
- package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
- package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
- package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/head.html +1 -1
- package/src/services/export-theme/layouts/partials/header.html +5 -3
- package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
- package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
- package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
- package/src/services/export-theme/layouts/post/list.html +1 -1
- package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
- package/src/services/export-theme/styles/main.css +13 -2
- package/src/services/export-theme/theme.toml +1 -1
- package/src/services/export.ts +880 -72
- package/src/services/github-app-installations.ts +171 -4
- package/src/services/github-sync.ts +69 -15
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +22 -2
- package/src/services/media.ts +22 -7
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +254 -6
- package/src/services/post.ts +1234 -190
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +27 -14
- package/src/services/site.ts +160 -32
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1090 -7216
- package/src/types/app-context.ts +23 -0
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +74 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +277 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/export-Dl5KCiEs.js +0 -5562
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/client/tiptap/exitable-marks.ts +0 -73
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -13,19 +13,35 @@ import { z } from "zod";
|
|
|
13
13
|
import type { Bindings } from "../../types.js";
|
|
14
14
|
import type { AppVariables } from "../../types/app-context.js";
|
|
15
15
|
import { sse, dsRedirect, dsToast } from "../../lib/sse.js";
|
|
16
|
-
import { getI18n, isLocale
|
|
16
|
+
import { getI18n, isLocale } from "../../i18n/index.js";
|
|
17
17
|
import { renderPublicPage } from "../../lib/render.js";
|
|
18
18
|
import { getNavigationData } from "../../lib/navigation.js";
|
|
19
19
|
import { buildPageTitle } from "../../lib/page-title.js";
|
|
20
20
|
import { AdminBreadcrumb } from "../../ui/shared/AdminBreadcrumb.js";
|
|
21
21
|
import { getTimeZoneOptions } from "../../lib/timezones.js";
|
|
22
|
-
import {
|
|
22
|
+
import { getOrBuildEntry } from "../../i18n/supported-locales.js";
|
|
23
|
+
import {
|
|
24
|
+
DomainError,
|
|
25
|
+
LanguageInUseError,
|
|
26
|
+
UnauthorizedError,
|
|
27
|
+
ValidationError,
|
|
28
|
+
} from "../../lib/errors.js";
|
|
23
29
|
import { SETTINGS_KEYS } from "../../lib/constants.js";
|
|
30
|
+
import { announceInBackground } from "../discover-announce.js";
|
|
31
|
+
import {
|
|
32
|
+
DISCOVER_FIRST_READ_MAX_HOURS,
|
|
33
|
+
DISCOVER_MIN_PUBLIC_POSTS,
|
|
34
|
+
getDiscoverPageUrls,
|
|
35
|
+
getDiscoverSubmitUrl,
|
|
36
|
+
measureDiscoverMaturity,
|
|
37
|
+
parseDiscoverSetting,
|
|
38
|
+
} from "../../lib/discover.js";
|
|
24
39
|
import { getAvailableThemes } from "../../lib/theme.js";
|
|
25
40
|
import { THEME_MODES, type ThemeMode } from "../../types/config.js";
|
|
26
41
|
import { BUILTIN_FONT_THEMES } from "../../ui/font-themes.js";
|
|
27
42
|
import { SettingsRootContent } from "../../ui/dash/settings/SettingsRootContent.js";
|
|
28
43
|
import { GeneralContent } from "../../ui/dash/settings/GeneralContent.js";
|
|
44
|
+
import { LanguageContent } from "../../ui/dash/settings/LanguageContent.js";
|
|
29
45
|
import { AvatarContent } from "../../ui/dash/settings/AvatarContent.js";
|
|
30
46
|
import { AccountMenuContent } from "../../ui/dash/settings/AccountMenuContent.js";
|
|
31
47
|
import { AccountContent } from "../../ui/dash/settings/AccountContent.js";
|
|
@@ -52,16 +68,24 @@ import {
|
|
|
52
68
|
} from "../../lib/telegram-settings-status.js";
|
|
53
69
|
import { TelegramContent } from "../../ui/dash/settings/TelegramContent.js";
|
|
54
70
|
import { toAbsoluteSiteUrl, toPublicPath } from "../../lib/url.js";
|
|
55
|
-
import {
|
|
71
|
+
import {
|
|
72
|
+
ContentLanguageSchema,
|
|
73
|
+
parseValidated,
|
|
74
|
+
UpdateSiteSettingsSchema,
|
|
75
|
+
} from "../../lib/schemas.js";
|
|
56
76
|
import {
|
|
57
77
|
getHostedControlPlaneAccountPasswordUrl,
|
|
58
78
|
getHostedControlPlaneAccountUrl,
|
|
59
79
|
getHostedControlPlaneProviderLabel,
|
|
60
80
|
getHostedControlPlaneSiteDeleteUrl,
|
|
61
81
|
getHostedControlPlaneSiteSettingsUrl,
|
|
82
|
+
isHostedControlPlaneEnabled,
|
|
62
83
|
} from "../../lib/hosted-signin.js";
|
|
63
84
|
import { syncHostedControlPlaneSiteAvatar } from "../../lib/hosted-control-plane-sync.js";
|
|
64
85
|
import {
|
|
86
|
+
getDiscoverDefault,
|
|
87
|
+
getDiscoverDirectoryBaseUrl,
|
|
88
|
+
getDiscoverPingUrl,
|
|
65
89
|
getGitHubAppConfig,
|
|
66
90
|
getHostedControlPlaneSsoSecret,
|
|
67
91
|
getTelegramBotPool,
|
|
@@ -71,6 +95,7 @@ import {
|
|
|
71
95
|
getInstallation,
|
|
72
96
|
listInstallationReposPage,
|
|
73
97
|
searchInstallationRepos,
|
|
98
|
+
type InstallationAccount,
|
|
74
99
|
} from "../../lib/github-app.js";
|
|
75
100
|
import {
|
|
76
101
|
isSyncPending,
|
|
@@ -79,6 +104,7 @@ import {
|
|
|
79
104
|
triggerGitHubSyncInline,
|
|
80
105
|
} from "../../lib/github-sync-trigger.js";
|
|
81
106
|
import { buildSyncSiteConfig } from "../../lib/github-sync-site-config.js";
|
|
107
|
+
import { suggestSyncRepoName } from "../../lib/github-sync-repo-name.js";
|
|
82
108
|
import { buildConfigEditorFields } from "../../lib/api-settings.js";
|
|
83
109
|
import {
|
|
84
110
|
readGitHubSyncStatus,
|
|
@@ -95,15 +121,26 @@ type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
|
95
121
|
|
|
96
122
|
export const settingsRoutes = new Hono<Env>();
|
|
97
123
|
|
|
98
|
-
const
|
|
99
|
-
siteLanguage: z.string(),
|
|
100
|
-
// Optional for back-compat: absent = leave the dashboard language untouched,
|
|
101
|
-
// "" = clear it (follow content language).
|
|
102
|
-
dashboardLanguage: z.string().optional(),
|
|
103
|
-
cjkSerifFont: z.string(),
|
|
124
|
+
const UpdateTimeSettingsSchema = z.object({
|
|
104
125
|
timeZone: z.string(),
|
|
105
126
|
});
|
|
106
127
|
|
|
128
|
+
// Both fields are optional: the language page saves one control at a time, and
|
|
129
|
+
// an absent field means "leave it alone" rather than "clear it".
|
|
130
|
+
const UpdateLanguageSettingsSchema = z.object({
|
|
131
|
+
contentLanguage: ContentLanguageSchema.optional(),
|
|
132
|
+
dashboardLanguage: z.string().optional(),
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const LanguageTagSchema = z.object({
|
|
136
|
+
language: ContentLanguageSchema,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const EnableMultilingualSchema = z.object({
|
|
140
|
+
primary: ContentLanguageSchema,
|
|
141
|
+
additional: z.array(ContentLanguageSchema),
|
|
142
|
+
});
|
|
143
|
+
|
|
107
144
|
const UpdateFeedSettingsSchema = z.object({
|
|
108
145
|
mainRssFeed: z.enum(["featured", "latest"]),
|
|
109
146
|
});
|
|
@@ -116,6 +153,13 @@ const UpdateSearchSettingsSchema = z.object({
|
|
|
116
153
|
allowIndexing: z.boolean(),
|
|
117
154
|
});
|
|
118
155
|
|
|
156
|
+
// Only the two values the control writes. `featured` is still a stored value
|
|
157
|
+
// older releases wrote and the feed still declares, but nothing writes it any
|
|
158
|
+
// more: which of a blog's posts reach which list is the directory's rule.
|
|
159
|
+
const UpdateDiscoverSettingsSchema = z.object({
|
|
160
|
+
discover: z.enum(["latest", "off"]),
|
|
161
|
+
});
|
|
162
|
+
|
|
119
163
|
function publicPath(c: Context<Env>, path: string): string {
|
|
120
164
|
return toPublicPath(path, c.var.appConfig.sitePathPrefix);
|
|
121
165
|
}
|
|
@@ -124,6 +168,39 @@ function aboutEditPath(c: Context<Env>): string {
|
|
|
124
168
|
return `${publicPath(c, "/about")}?edit=1`;
|
|
125
169
|
}
|
|
126
170
|
|
|
171
|
+
/**
|
|
172
|
+
* The session id behind the current request.
|
|
173
|
+
*
|
|
174
|
+
* Every `/settings` route sits behind `requireAuth`, so a missing session here
|
|
175
|
+
* is a wiring mistake rather than a signed-out visitor — hence a throw instead
|
|
176
|
+
* of a redirect.
|
|
177
|
+
*/
|
|
178
|
+
function requireSessionId(c: Context<Env>): string {
|
|
179
|
+
const sessionId = c.var.session?.session?.id;
|
|
180
|
+
if (!sessionId) {
|
|
181
|
+
throw new UnauthorizedError();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return sessionId;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The user behind the current request.
|
|
189
|
+
*
|
|
190
|
+
* Same contract as `requireSessionId`: `requireAuth` has already proven the
|
|
191
|
+
* session belongs to a member of this site, so an absent user is a wiring
|
|
192
|
+
* mistake. Used wherever a decision is scoped to the person rather than to
|
|
193
|
+
* the site — the GitHub App installations they may reuse, most of all.
|
|
194
|
+
*/
|
|
195
|
+
function requireSessionUserId(c: Context<Env>): string {
|
|
196
|
+
const userId = c.var.session?.user?.id;
|
|
197
|
+
if (!userId) {
|
|
198
|
+
throw new UnauthorizedError();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return userId;
|
|
202
|
+
}
|
|
203
|
+
|
|
127
204
|
/**
|
|
128
205
|
* Breadcrumb labels for admin settings pages.
|
|
129
206
|
*
|
|
@@ -138,6 +215,7 @@ function breadcrumbLabel(
|
|
|
138
215
|
key:
|
|
139
216
|
| "settings"
|
|
140
217
|
| "general"
|
|
218
|
+
| "language"
|
|
141
219
|
| "avatar"
|
|
142
220
|
| "navigation"
|
|
143
221
|
| "colorTheme"
|
|
@@ -163,6 +241,10 @@ function breadcrumbLabel(
|
|
|
163
241
|
return i18n._(
|
|
164
242
|
msg({ message: "General", comment: "@context: Breadcrumb label" }),
|
|
165
243
|
);
|
|
244
|
+
case "language":
|
|
245
|
+
return i18n._(
|
|
246
|
+
msg({ message: "Language", comment: "@context: Breadcrumb label" }),
|
|
247
|
+
);
|
|
166
248
|
case "avatar":
|
|
167
249
|
return i18n._(
|
|
168
250
|
msg({ message: "Avatar", comment: "@context: Breadcrumb label" }),
|
|
@@ -318,11 +400,44 @@ settingsRoutes.get("/general", async (c) => {
|
|
|
318
400
|
|
|
319
401
|
const dbSiteName = allSettings["SITE_NAME"] ?? "";
|
|
320
402
|
const dbSiteDescription = allSettings["SITE_DESCRIPTION"] ?? "";
|
|
403
|
+
// The stored choice, not the effective mode: the control has to be able to
|
|
404
|
+
// show "never chosen", which is what lets `noindex` and then the deployment
|
|
405
|
+
// default decide instead.
|
|
406
|
+
const discoverSetting = parseDiscoverSetting(allSettings["DISCOVER"]);
|
|
407
|
+
// The deployment's own answer, and nothing else folded into it: `latest` on
|
|
408
|
+
// a deployment that lists its fleet, absent on an ordinary self-hosted site.
|
|
409
|
+
// The gates that sit above it — `noindex` most of all — are applied in the
|
|
410
|
+
// browser instead, because they are controls on this same page and a value
|
|
411
|
+
// resolved here would freeze at page load. The component runs the same
|
|
412
|
+
// `resolveDiscoverMode` this route would have.
|
|
413
|
+
const discoverDefault = parseDiscoverSetting(getDiscoverDefault(c.env)) ?? "";
|
|
321
414
|
|
|
322
415
|
const saved = c.req.query("saved") !== undefined;
|
|
323
|
-
|
|
416
|
+
// What the site can answer about its own standing in the directory, without
|
|
417
|
+
// asking one. The directory deliberately does not take status queries, so
|
|
418
|
+
// everything shown here is local evidence.
|
|
419
|
+
const publicPostFilters = {
|
|
420
|
+
status: "published" as const,
|
|
421
|
+
excludeReplies: true,
|
|
422
|
+
excludeLatestHidden: true,
|
|
423
|
+
excludePrivate: true,
|
|
424
|
+
};
|
|
425
|
+
const [
|
|
426
|
+
navData,
|
|
427
|
+
aboutPage,
|
|
428
|
+
announceState,
|
|
429
|
+
publicPostCount,
|
|
430
|
+
featuredPostCount,
|
|
431
|
+
] = await Promise.all([
|
|
324
432
|
getNavigationData(c),
|
|
325
433
|
c.var.services.aboutPage.getStatus(),
|
|
434
|
+
c.var.services.settings.getDiscoverAnnounceState(),
|
|
435
|
+
c.var.services.posts.count(publicPostFilters),
|
|
436
|
+
c.var.services.posts.countFeaturedThreadRoots({
|
|
437
|
+
status: "published",
|
|
438
|
+
excludePrivate: true,
|
|
439
|
+
excludeLatestHidden: true,
|
|
440
|
+
}),
|
|
326
441
|
]);
|
|
327
442
|
const siteUrlForDisplay =
|
|
328
443
|
appConfig.siteUrl || new URL(publicPath(c, "/"), c.req.url).toString();
|
|
@@ -341,13 +456,6 @@ settingsRoutes.get("/general", async (c) => {
|
|
|
341
456
|
<GeneralContent
|
|
342
457
|
siteName={dbSiteName || ""}
|
|
343
458
|
siteDescription={dbSiteDescription || ""}
|
|
344
|
-
siteLanguage={appConfig.siteLanguage}
|
|
345
|
-
dashboardLanguage={
|
|
346
|
-
isLocale(appConfig.dashboardLanguage)
|
|
347
|
-
? appConfig.dashboardLanguage
|
|
348
|
-
: resolveCatalogLocale(appConfig.siteLanguage)
|
|
349
|
-
}
|
|
350
|
-
cjkSerifFont={appConfig.cjkSerifFont}
|
|
351
459
|
siteNameFallback={appConfig.fallbacks.siteName}
|
|
352
460
|
siteDescriptionFallback={appConfig.fallbacks.siteDescription}
|
|
353
461
|
mainRssFeed={appConfig.mainRssFeed}
|
|
@@ -375,6 +483,32 @@ settingsRoutes.get("/general", async (c) => {
|
|
|
375
483
|
siteFooter={appConfig.siteFooter}
|
|
376
484
|
showJantBrandingOnHome={appConfig.showJantBrandingOnHome}
|
|
377
485
|
noindex={appConfig.noindex}
|
|
486
|
+
discover={discoverSetting ?? ""}
|
|
487
|
+
discoverDefault={discoverDefault}
|
|
488
|
+
discoverPages={getDiscoverPageUrls(
|
|
489
|
+
getDiscoverDirectoryBaseUrl(c.env),
|
|
490
|
+
)}
|
|
491
|
+
discoverStatus={{
|
|
492
|
+
announced: announceState?.ok ?? null,
|
|
493
|
+
announceError: announceState?.error ?? null,
|
|
494
|
+
announceAt: announceState?.at ?? null,
|
|
495
|
+
// No directory configured means nothing to announce to, and the
|
|
496
|
+
// whole announcement block is beside the point.
|
|
497
|
+
hasDirectory: getDiscoverPingUrl(c.env) !== undefined,
|
|
498
|
+
// A hosted fleet is enrolled by its control plane, so there is no
|
|
499
|
+
// announcement for the owner to make, chase, or retry.
|
|
500
|
+
managedByHost: isHostedControlPlaneEnabled(c.env),
|
|
501
|
+
submitUrl: getDiscoverSubmitUrl(getDiscoverDirectoryBaseUrl(c.env)),
|
|
502
|
+
// The mode the feeds actually declare, already derived once on
|
|
503
|
+
// `appConfig` — re-deriving it here is how this block used to
|
|
504
|
+
// miss the deployment default.
|
|
505
|
+
declaredMode: appConfig.discover,
|
|
506
|
+
...measureDiscoverMaturity({ publicPostCount }),
|
|
507
|
+
featuredPostCount,
|
|
508
|
+
minPublicPosts: DISCOVER_MIN_PUBLIC_POSTS,
|
|
509
|
+
firstReadMaxHours: DISCOVER_FIRST_READ_MAX_HOURS,
|
|
510
|
+
}}
|
|
511
|
+
rssFeedsEnabled={appConfig.rssFeedsEnabled}
|
|
378
512
|
demoMode={appConfig.demoMode}
|
|
379
513
|
timezones={getTimeZoneOptions(appConfig.timeZone)}
|
|
380
514
|
aboutPage={aboutPage}
|
|
@@ -451,42 +585,286 @@ settingsRoutes.post("/general", async (c) => {
|
|
|
451
585
|
}
|
|
452
586
|
});
|
|
453
587
|
|
|
454
|
-
|
|
588
|
+
// Time zone only. Language moved to its own page, so this no longer sends —
|
|
589
|
+
// and must not clear — the language fields.
|
|
590
|
+
settingsRoutes.post("/general/time", async (c) => {
|
|
455
591
|
const i18n = getI18n(c);
|
|
456
|
-
const body = parseValidated(
|
|
592
|
+
const body = parseValidated(UpdateTimeSettingsSchema, await c.req.json());
|
|
457
593
|
const toast = i18n._(
|
|
458
594
|
msg({
|
|
459
|
-
message: "
|
|
460
|
-
comment: "@context: Toast after saving
|
|
595
|
+
message: "Time zone updated.",
|
|
596
|
+
comment: "@context: Toast after saving the time zone",
|
|
461
597
|
}),
|
|
462
598
|
);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
oldCjkSerifFont: c.var.appConfig.cjkSerifFont,
|
|
467
|
-
oldDashboardLanguage: c.var.appConfig.dashboardLanguage,
|
|
468
|
-
});
|
|
599
|
+
await c.var.services.settings.updateLocaleSettings(body, {
|
|
600
|
+
oldLanguage: c.var.appConfig.siteLanguage,
|
|
601
|
+
});
|
|
469
602
|
|
|
470
603
|
const wantsJson = c.req.header("accept")?.includes("application/json");
|
|
471
604
|
if (wantsJson) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
605
|
+
return c.json({ status: "ok" as const, toast });
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return dsToast(toast);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// ===========================================================================
|
|
612
|
+
// Language
|
|
613
|
+
//
|
|
614
|
+
// Every language setting lives on one page, and every write goes through the
|
|
615
|
+
// language service — the multilingual keys are DB-only precisely so this is the
|
|
616
|
+
// only door.
|
|
617
|
+
// ===========================================================================
|
|
478
618
|
|
|
619
|
+
settingsRoutes.get("/language", async (c) => {
|
|
620
|
+
const { appConfig } = c.var;
|
|
621
|
+
const navData = await getNavigationData(c);
|
|
622
|
+
const [state, preview] = await Promise.all([
|
|
623
|
+
c.var.services.language.getState(),
|
|
624
|
+
c.var.services.language.getEnablePreview(),
|
|
625
|
+
]);
|
|
626
|
+
|
|
627
|
+
return renderPublicPage(c, {
|
|
628
|
+
title: buildPageTitle("Language", navData.siteName),
|
|
629
|
+
navData,
|
|
630
|
+
content: (
|
|
631
|
+
<>
|
|
632
|
+
<AdminBreadcrumb
|
|
633
|
+
parent={breadcrumbLabel(c, "settings")}
|
|
634
|
+
parentHref={publicPath(c, "/settings")}
|
|
635
|
+
current={breadcrumbLabel(c, "language")}
|
|
636
|
+
/>
|
|
637
|
+
<LanguageContent
|
|
638
|
+
contentLanguage={state.primary}
|
|
639
|
+
// Unset stays unset rather than being shown as the locale it
|
|
640
|
+
// currently resolves to: "Follow content language" is a real choice
|
|
641
|
+
// on this page, and it is the one most sites are on.
|
|
642
|
+
dashboardLanguage={
|
|
643
|
+
isLocale(appConfig.dashboardLanguage)
|
|
644
|
+
? appConfig.dashboardLanguage
|
|
645
|
+
: ""
|
|
646
|
+
}
|
|
647
|
+
multilingualEnabled={state.enabled}
|
|
648
|
+
additionalLanguages={state.additional}
|
|
649
|
+
unmarkedPostCount={preview.pendingCount}
|
|
650
|
+
sitePathPrefix={appConfig.sitePathPrefix}
|
|
651
|
+
/>
|
|
652
|
+
</>
|
|
653
|
+
),
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Run a language mutation and report it as JSON.
|
|
659
|
+
*
|
|
660
|
+
* Every language endpoint has the same shape — do the thing, or explain in one
|
|
661
|
+
* sentence why it could not be done — so they share one wrapper rather than
|
|
662
|
+
* repeating the try/catch. The toast may be built from what the action
|
|
663
|
+
* returned, for the one operation whose outcome is not known until it runs.
|
|
664
|
+
*/
|
|
665
|
+
async function respondToLanguageAction<T>(
|
|
666
|
+
c: Context<Env>,
|
|
667
|
+
toast: string | ((result: T) => string),
|
|
668
|
+
action: () => Promise<T>,
|
|
669
|
+
): Promise<Response> {
|
|
670
|
+
try {
|
|
671
|
+
const result = await action();
|
|
479
672
|
return c.json({
|
|
480
673
|
status: "ok" as const,
|
|
481
|
-
toast,
|
|
674
|
+
toast: typeof toast === "function" ? toast(result) : toast,
|
|
482
675
|
});
|
|
676
|
+
} catch (error) {
|
|
677
|
+
// The one refusal an author will actually meet — removing or dropping a
|
|
678
|
+
// language that posts still use — gets a localized sentence naming the
|
|
679
|
+
// language; the settings page pairs it with a link to those posts.
|
|
680
|
+
if (error instanceof LanguageInUseError) {
|
|
681
|
+
const i18n = getI18n(c);
|
|
682
|
+
return c.json(
|
|
683
|
+
{
|
|
684
|
+
error: i18n._(
|
|
685
|
+
msg({
|
|
686
|
+
message:
|
|
687
|
+
"{count, plural, one {# post is} other {# posts are}} still written in {language}. Change their language, or keep the language.",
|
|
688
|
+
comment:
|
|
689
|
+
"@context: Error when a content language that posts still use would be dropped",
|
|
690
|
+
}),
|
|
691
|
+
{
|
|
692
|
+
count: error.postCount,
|
|
693
|
+
language: getOrBuildEntry(error.language).native,
|
|
694
|
+
},
|
|
695
|
+
),
|
|
696
|
+
code: error.code,
|
|
697
|
+
language: error.language,
|
|
698
|
+
},
|
|
699
|
+
400,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
if (error instanceof DomainError) {
|
|
703
|
+
return c.json({ error: error.message, code: error.code }, 400);
|
|
704
|
+
}
|
|
705
|
+
throw error;
|
|
483
706
|
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
settingsRoutes.post("/language", async (c) => {
|
|
710
|
+
const i18n = getI18n(c);
|
|
711
|
+
const body = parseValidated(UpdateLanguageSettingsSchema, await c.req.json());
|
|
712
|
+
|
|
713
|
+
return respondToLanguageAction(
|
|
714
|
+
c,
|
|
715
|
+
i18n._(
|
|
716
|
+
msg({
|
|
717
|
+
message: "Language updated.",
|
|
718
|
+
comment: "@context: Toast after saving a language setting",
|
|
719
|
+
}),
|
|
720
|
+
),
|
|
721
|
+
async () => {
|
|
722
|
+
await c.var.services.settings.updateLocaleSettings(
|
|
723
|
+
{
|
|
724
|
+
siteLanguage: body.contentLanguage,
|
|
725
|
+
dashboardLanguage: body.dashboardLanguage,
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
oldLanguage: c.var.appConfig.siteLanguage,
|
|
729
|
+
oldDashboardLanguage: c.var.appConfig.dashboardLanguage,
|
|
730
|
+
},
|
|
731
|
+
);
|
|
732
|
+
},
|
|
733
|
+
);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
settingsRoutes.post("/language/enable", async (c) => {
|
|
737
|
+
const i18n = getI18n(c);
|
|
738
|
+
const body = parseValidated(EnableMultilingualSchema, await c.req.json());
|
|
484
739
|
|
|
485
|
-
|
|
486
|
-
|
|
740
|
+
try {
|
|
741
|
+
const { markedCount } = await c.var.services.language.enable(body);
|
|
742
|
+
return c.json({
|
|
743
|
+
status: "ok" as const,
|
|
744
|
+
toast:
|
|
745
|
+
markedCount > 0
|
|
746
|
+
? i18n._(
|
|
747
|
+
msg({
|
|
748
|
+
message:
|
|
749
|
+
"Multilingual content is on. {count, plural, one {# post was marked} other {# posts were marked}} as {language}.",
|
|
750
|
+
comment:
|
|
751
|
+
"@context: Toast after turning multilingual content on with existing posts",
|
|
752
|
+
}),
|
|
753
|
+
// The language's own name, not its tag — the author picked
|
|
754
|
+
// "繁體中文" in the dialog, not "zh-Hant".
|
|
755
|
+
{
|
|
756
|
+
count: markedCount,
|
|
757
|
+
language: getOrBuildEntry(body.primary).native,
|
|
758
|
+
},
|
|
759
|
+
)
|
|
760
|
+
: i18n._(
|
|
761
|
+
msg({
|
|
762
|
+
message: "Multilingual content is on.",
|
|
763
|
+
comment:
|
|
764
|
+
"@context: Toast after turning multilingual content on with no existing posts",
|
|
765
|
+
}),
|
|
766
|
+
),
|
|
767
|
+
});
|
|
768
|
+
} catch (error) {
|
|
769
|
+
if (error instanceof LanguageInUseError) {
|
|
770
|
+
// Worded for the dialog it lands in: the fix is putting the language
|
|
771
|
+
// back on the list right there, and the response carries the tag so
|
|
772
|
+
// the dialog can offer that as one click.
|
|
773
|
+
return c.json(
|
|
774
|
+
{
|
|
775
|
+
error: i18n._(
|
|
776
|
+
msg({
|
|
777
|
+
message:
|
|
778
|
+
"{count, plural, one {# post is} other {# posts are}} written in {language}, which is not on this list. Add it back, or change their language first.",
|
|
779
|
+
comment:
|
|
780
|
+
"@context: Refusal in the multilingual dialog when the list drops a language that posts still use",
|
|
781
|
+
}),
|
|
782
|
+
{
|
|
783
|
+
count: error.postCount,
|
|
784
|
+
language: getOrBuildEntry(error.language).native,
|
|
785
|
+
},
|
|
786
|
+
),
|
|
787
|
+
code: error.code,
|
|
788
|
+
language: error.language,
|
|
789
|
+
},
|
|
790
|
+
400,
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
if (error instanceof DomainError) {
|
|
794
|
+
return c.json({ error: error.message, code: error.code }, 400);
|
|
795
|
+
}
|
|
796
|
+
throw error;
|
|
487
797
|
}
|
|
798
|
+
});
|
|
488
799
|
|
|
489
|
-
|
|
800
|
+
settingsRoutes.post("/language/disable", async (c) => {
|
|
801
|
+
const i18n = getI18n(c);
|
|
802
|
+
return respondToLanguageAction(
|
|
803
|
+
c,
|
|
804
|
+
i18n._(
|
|
805
|
+
msg({
|
|
806
|
+
message: "Multilingual content is off. Your languages are still saved.",
|
|
807
|
+
comment: "@context: Toast after turning multilingual content off",
|
|
808
|
+
}),
|
|
809
|
+
),
|
|
810
|
+
() => c.var.services.language.disable(),
|
|
811
|
+
);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
settingsRoutes.post("/language/primary", async (c) => {
|
|
815
|
+
const i18n = getI18n(c);
|
|
816
|
+
const body = parseValidated(LanguageTagSchema, await c.req.json());
|
|
817
|
+
return respondToLanguageAction(
|
|
818
|
+
c,
|
|
819
|
+
i18n._(
|
|
820
|
+
msg({
|
|
821
|
+
message: "Primary language changed.",
|
|
822
|
+
comment: "@context: Toast after changing the primary language",
|
|
823
|
+
}),
|
|
824
|
+
),
|
|
825
|
+
() => c.var.services.language.setPrimary(body.language),
|
|
826
|
+
);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
settingsRoutes.post("/language/add", async (c) => {
|
|
830
|
+
const i18n = getI18n(c);
|
|
831
|
+
const body = parseValidated(LanguageTagSchema, await c.req.json());
|
|
832
|
+
return respondToLanguageAction(
|
|
833
|
+
c,
|
|
834
|
+
i18n._(
|
|
835
|
+
msg({
|
|
836
|
+
message: "Language added.",
|
|
837
|
+
comment: "@context: Toast after adding a content language",
|
|
838
|
+
}),
|
|
839
|
+
),
|
|
840
|
+
() => c.var.services.language.addLanguage(body.language),
|
|
841
|
+
);
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
settingsRoutes.post("/language/remove", async (c) => {
|
|
845
|
+
const i18n = getI18n(c);
|
|
846
|
+
const body = parseValidated(LanguageTagSchema, await c.req.json());
|
|
847
|
+
return respondToLanguageAction(
|
|
848
|
+
c,
|
|
849
|
+
// The last language takes the feature with it, and that is the larger of
|
|
850
|
+
// the two facts — the author leaves this page single-language.
|
|
851
|
+
({ multilingualDisabled }) =>
|
|
852
|
+
multilingualDisabled
|
|
853
|
+
? i18n._(
|
|
854
|
+
msg({
|
|
855
|
+
message: "Language removed. Multilingual content is off.",
|
|
856
|
+
comment:
|
|
857
|
+
"@context: Toast after removing the last additional content language",
|
|
858
|
+
}),
|
|
859
|
+
)
|
|
860
|
+
: i18n._(
|
|
861
|
+
msg({
|
|
862
|
+
message: "Language removed.",
|
|
863
|
+
comment: "@context: Toast after removing a content language",
|
|
864
|
+
}),
|
|
865
|
+
),
|
|
866
|
+
() => c.var.services.language.removeLanguage(body.language),
|
|
867
|
+
);
|
|
490
868
|
});
|
|
491
869
|
|
|
492
870
|
settingsRoutes.post("/general/feeds", async (c) => {
|
|
@@ -527,6 +905,88 @@ settingsRoutes.post("/general/home", async (c) => {
|
|
|
527
905
|
return dsToast(toast);
|
|
528
906
|
});
|
|
529
907
|
|
|
908
|
+
/**
|
|
909
|
+
* Announce, because the owner asked.
|
|
910
|
+
*
|
|
911
|
+
* The route the settings page offers whenever the directory has not heard from
|
|
912
|
+
* this site: an announcement that failed, and one that was never made at all.
|
|
913
|
+
* Sending it is safe at any time — the receiving end treats a repeat as a
|
|
914
|
+
* no-op for a site it already knows, and it carries nothing but the feed
|
|
915
|
+
* address.
|
|
916
|
+
*/
|
|
917
|
+
settingsRoutes.post("/general/discover/announce", async (c) => {
|
|
918
|
+
const i18n = getI18n(c);
|
|
919
|
+
const { appConfig } = c.var;
|
|
920
|
+
|
|
921
|
+
const started =
|
|
922
|
+
!appConfig.demoMode &&
|
|
923
|
+
announceInBackground(c, c.var.allSettings["DISCOVER"]);
|
|
924
|
+
|
|
925
|
+
const toast = started
|
|
926
|
+
? i18n._(
|
|
927
|
+
msg({
|
|
928
|
+
message: "Announcing your site. Reload to see the result.",
|
|
929
|
+
comment:
|
|
930
|
+
"@context: Toast after sending the Discover announcement. It runs in the background, so the page does not yet know how it went.",
|
|
931
|
+
}),
|
|
932
|
+
)
|
|
933
|
+
: i18n._(
|
|
934
|
+
msg({
|
|
935
|
+
message: "This site has no directory to announce to.",
|
|
936
|
+
comment:
|
|
937
|
+
"@context: Toast when the Discover announcement cannot be sent because no directory is configured, or the site cannot be listed at all",
|
|
938
|
+
}),
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
// The settings bridge is the only caller, and it always asks for JSON — a
|
|
942
|
+
// Datastar toast reaches it as a body it cannot parse, which it reports as a
|
|
943
|
+
// failed save whatever the server actually did. Every other settings route
|
|
944
|
+
// answers both shapes; this one has to as well.
|
|
945
|
+
const wantsJson = c.req.header("accept")?.includes("application/json");
|
|
946
|
+
if (wantsJson) {
|
|
947
|
+
return c.json({ status: "ok" as const, toast });
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
return dsToast(toast);
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
settingsRoutes.post("/general/discover", async (c) => {
|
|
954
|
+
const i18n = getI18n(c);
|
|
955
|
+
const body = parseValidated(UpdateDiscoverSettingsSchema, await c.req.json());
|
|
956
|
+
const { appConfig } = c.var;
|
|
957
|
+
|
|
958
|
+
const { shouldAnnounce } =
|
|
959
|
+
await c.var.services.settings.updateDiscoverSetting(body.discover, {
|
|
960
|
+
demoMode: appConfig.demoMode,
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
if (shouldAnnounce) {
|
|
964
|
+
announceInBackground(c, body.discover);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const wantsJson = c.req.header("accept")?.includes("application/json");
|
|
968
|
+
if (wantsJson) {
|
|
969
|
+
return c.json({
|
|
970
|
+
status: "ok" as const,
|
|
971
|
+
toast: i18n._(
|
|
972
|
+
msg({
|
|
973
|
+
message: "Site visibility updated.",
|
|
974
|
+
comment: "@context: Toast after saving the site visibility settings",
|
|
975
|
+
}),
|
|
976
|
+
),
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
return dsToast(
|
|
981
|
+
i18n._(
|
|
982
|
+
msg({
|
|
983
|
+
message: "Site visibility updated.",
|
|
984
|
+
comment: "@context: Toast after saving the site visibility settings",
|
|
985
|
+
}),
|
|
986
|
+
),
|
|
987
|
+
);
|
|
988
|
+
});
|
|
989
|
+
|
|
530
990
|
settingsRoutes.post("/general/search", async (c) => {
|
|
531
991
|
const i18n = getI18n(c);
|
|
532
992
|
const body = parseValidated(UpdateSearchSettingsSchema, await c.req.json());
|
|
@@ -795,7 +1255,7 @@ settingsRoutes.post("/avatar/display", async (c) => {
|
|
|
795
1255
|
settingsRoutes.get("/navigation", async (c) => {
|
|
796
1256
|
const [navItems, directoryData, suggestedLinks] = await Promise.all([
|
|
797
1257
|
c.var.services.navItems.list(),
|
|
798
|
-
c.var.services.collections.listDirectoryData(),
|
|
1258
|
+
c.var.services.collections.listDirectoryData({ isAuthenticated: true }),
|
|
799
1259
|
c.var.services.navItems.listSuggestedLinks({
|
|
800
1260
|
siteOrigin: c.var.appConfig.siteOrigin,
|
|
801
1261
|
sitePathPrefix: c.var.appConfig.sitePathPrefix,
|
|
@@ -1332,7 +1792,9 @@ settingsRoutes.get("/account/delete-account", async (c) => {
|
|
|
1332
1792
|
}
|
|
1333
1793
|
|
|
1334
1794
|
const navData = await getNavigationData(c);
|
|
1335
|
-
const csrfToken = await c.var.services.auth.generateDeleteCsrfToken(
|
|
1795
|
+
const csrfToken = await c.var.services.auth.generateDeleteCsrfToken(
|
|
1796
|
+
requireSessionId(c),
|
|
1797
|
+
);
|
|
1336
1798
|
|
|
1337
1799
|
return renderPublicPage(c, {
|
|
1338
1800
|
title: buildPageTitle("Delete Account", navData.siteName),
|
|
@@ -1390,7 +1852,10 @@ settingsRoutes.post("/account/delete-account", async (c) => {
|
|
|
1390
1852
|
);
|
|
1391
1853
|
}
|
|
1392
1854
|
|
|
1393
|
-
const isValid = await c.var.services.auth.validateDeleteCsrfToken(
|
|
1855
|
+
const isValid = await c.var.services.auth.validateDeleteCsrfToken(
|
|
1856
|
+
csrfToken,
|
|
1857
|
+
requireSessionId(c),
|
|
1858
|
+
);
|
|
1394
1859
|
if (!isValid) {
|
|
1395
1860
|
return dsToast(
|
|
1396
1861
|
i18n._(
|
|
@@ -1404,11 +1869,26 @@ settingsRoutes.post("/account/delete-account", async (c) => {
|
|
|
1404
1869
|
);
|
|
1405
1870
|
}
|
|
1406
1871
|
|
|
1872
|
+
// Sign the browser out first. Every row behind this session is about to
|
|
1873
|
+
// disappear, and better-auth's cookie cache would keep answering "signed in"
|
|
1874
|
+
// from the browser's own copy for minutes afterwards — a ticket to a site
|
|
1875
|
+
// that no longer exists, handed to whoever completes the next setup.
|
|
1876
|
+
let signOutHeaders: Headers | undefined;
|
|
1877
|
+
try {
|
|
1878
|
+
const signedOut = await c.var.auth.api.signOut({
|
|
1879
|
+
headers: c.req.raw.headers,
|
|
1880
|
+
asResponse: true,
|
|
1881
|
+
});
|
|
1882
|
+
signOutHeaders = signedOut.headers;
|
|
1883
|
+
} catch {
|
|
1884
|
+
// The wipe below removes the session either way.
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1407
1887
|
await c.var.services.auth.deleteAllData({
|
|
1408
1888
|
storage: c.var.storage,
|
|
1409
1889
|
});
|
|
1410
1890
|
|
|
1411
|
-
return dsRedirect(publicPath(c, "/setup"));
|
|
1891
|
+
return dsRedirect(publicPath(c, "/setup"), { headers: signOutHeaders });
|
|
1412
1892
|
});
|
|
1413
1893
|
|
|
1414
1894
|
// ===========================================================================
|
|
@@ -1619,6 +2099,49 @@ settingsRoutes.post("/github-sync/disconnect", async (c) => {
|
|
|
1619
2099
|
// GitHub App install flow
|
|
1620
2100
|
// ---------------------------------------------------------------------------
|
|
1621
2101
|
|
|
2102
|
+
/**
|
|
2103
|
+
* Recreate installation bindings for sites that sync through the App
|
|
2104
|
+
* but have no row to prove it.
|
|
2105
|
+
*
|
|
2106
|
+
* GitHub is the only source for the account behind an installation id,
|
|
2107
|
+
* so the repair costs one call per distinct id — bounded by how many
|
|
2108
|
+
* sites the author has, and only ever reached when they would otherwise
|
|
2109
|
+
* be shown a GitHub page that cannot lead anywhere. Ids GitHub no longer
|
|
2110
|
+
* recognizes are skipped: an uninstalled App has nothing to rebuild.
|
|
2111
|
+
*/
|
|
2112
|
+
async function rebuildInstallationBindings(
|
|
2113
|
+
c: Context<Env>,
|
|
2114
|
+
app: import("../../lib/env.js").GitHubAppEnvConfig,
|
|
2115
|
+
userId: string,
|
|
2116
|
+
): Promise<void> {
|
|
2117
|
+
const syncing =
|
|
2118
|
+
await c.var.services.githubAppInstallations.listSyncingInstallationsForUser(
|
|
2119
|
+
userId,
|
|
2120
|
+
);
|
|
2121
|
+
if (syncing.length === 0) return;
|
|
2122
|
+
|
|
2123
|
+
const accounts = new Map<string, InstallationAccount>();
|
|
2124
|
+
for (const { installationId } of syncing) {
|
|
2125
|
+
if (accounts.has(installationId)) continue;
|
|
2126
|
+
try {
|
|
2127
|
+
const installation = await getInstallation(app, installationId);
|
|
2128
|
+
accounts.set(installationId, installation.account);
|
|
2129
|
+
} catch {
|
|
2130
|
+
continue;
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
for (const { siteId, installationId } of syncing) {
|
|
2135
|
+
const account = accounts.get(installationId);
|
|
2136
|
+
if (!account) continue;
|
|
2137
|
+
await c.var.services.githubAppInstallations.upsertInstallation(
|
|
2138
|
+
installationId,
|
|
2139
|
+
siteId,
|
|
2140
|
+
account,
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
1622
2145
|
/**
|
|
1623
2146
|
* Redirect the user to GitHub to install the App on their account/org.
|
|
1624
2147
|
*
|
|
@@ -1631,26 +2154,46 @@ settingsRoutes.get("/github-sync/app/install", async (c) => {
|
|
|
1631
2154
|
return c.text("GitHub App is not configured on this deployment.", 404);
|
|
1632
2155
|
}
|
|
1633
2156
|
|
|
1634
|
-
// Reinstall-same-account predicate
|
|
1635
|
-
//
|
|
1636
|
-
//
|
|
1637
|
-
//
|
|
1638
|
-
//
|
|
2157
|
+
// Reinstall-same-account predicate: when the signed-in author has
|
|
2158
|
+
// already authorized an installation — on this site or on any other
|
|
2159
|
+
// site they belong to — skip the GitHub round-trip and render the
|
|
2160
|
+
// picker directly. The GitHub redirect is a dead end once the App is
|
|
2161
|
+
// installed on the chosen account: GitHub shows its Configure page
|
|
2162
|
+
// and never comes back with installation_id.
|
|
2163
|
+
//
|
|
2164
|
+
// The scope is the user's sites rather than this one because a GitHub
|
|
2165
|
+
// App installs once per GitHub account. An author connecting their
|
|
2166
|
+
// second blog to the same account has no binding here yet and cannot
|
|
2167
|
+
// create one through GitHub, so site-scoping this check would leave
|
|
2168
|
+
// them with no way in at all.
|
|
1639
2169
|
//
|
|
1640
2170
|
// `?force=new` bypasses the predicate so the picker's "Install on
|
|
1641
2171
|
// another account" action can still reach GitHub to add a fresh
|
|
1642
2172
|
// install.
|
|
1643
2173
|
const forceNew = c.req.query("force") === "new";
|
|
1644
2174
|
if (!forceNew) {
|
|
1645
|
-
const
|
|
1646
|
-
|
|
1647
|
-
|
|
2175
|
+
const userId = requireSessionUserId(c);
|
|
2176
|
+
let existing =
|
|
2177
|
+
await c.var.services.githubAppInstallations.listInstallationsForUser(
|
|
2178
|
+
userId,
|
|
1648
2179
|
);
|
|
2180
|
+
// An author with nothing to show but a site already syncing through
|
|
2181
|
+
// the App is in the one state the redirect cannot rescue: GitHub
|
|
2182
|
+
// will not install the App a second time on that account. Rebuild
|
|
2183
|
+
// the binding from the sync settings instead of sending them into
|
|
2184
|
+
// the dead end.
|
|
2185
|
+
if (existing.length === 0) {
|
|
2186
|
+
await rebuildInstallationBindings(c, app, userId);
|
|
2187
|
+
existing =
|
|
2188
|
+
await c.var.services.githubAppInstallations.listInstallationsForUser(
|
|
2189
|
+
userId,
|
|
2190
|
+
);
|
|
2191
|
+
}
|
|
1649
2192
|
if (existing.length > 0) {
|
|
1650
2193
|
const navData = await getNavigationData(c);
|
|
1651
2194
|
const base = publicPath(c, "/settings/github-sync");
|
|
1652
|
-
const
|
|
1653
|
-
const
|
|
2195
|
+
const suggestedRepoName = suggestSyncRepoName(c.var.appConfig.siteUrl);
|
|
2196
|
+
const labels = buildRepoPickerLabels(c, suggestedRepoName);
|
|
1654
2197
|
return renderPublicPage(c, {
|
|
1655
2198
|
title: buildPageTitle(
|
|
1656
2199
|
"GitHub Sync — Pick Repository",
|
|
@@ -1748,19 +2291,17 @@ settingsRoutes.get("/github-sync/app/callback", async (c) => {
|
|
|
1748
2291
|
}
|
|
1749
2292
|
}
|
|
1750
2293
|
|
|
1751
|
-
// One-shot: clear the cookie so it can't be replayed.
|
|
1752
|
-
setCookie(c, "jant_gh_app_state", "", {
|
|
1753
|
-
httpOnly: true,
|
|
1754
|
-
sameSite: "Lax",
|
|
1755
|
-
path: "/",
|
|
1756
|
-
maxAge: 0,
|
|
1757
|
-
});
|
|
1758
|
-
|
|
1759
2294
|
// Fetch the account info for this installation and remember it. The
|
|
1760
|
-
//
|
|
1761
|
-
//
|
|
1762
|
-
//
|
|
1763
|
-
//
|
|
2295
|
+
// binding is what every later step reads: the picker's account
|
|
2296
|
+
// dropdown, the endpoints that authorize an installation id, and the
|
|
2297
|
+
// App webhook's fan-out. Rendering the picker without it would show an
|
|
2298
|
+
// empty account list and nothing to pick.
|
|
2299
|
+
//
|
|
2300
|
+
// So a failure here has to stay recoverable, and the only thing that
|
|
2301
|
+
// makes this page replayable is the state cookie — GitHub will not
|
|
2302
|
+
// hand out a second callback for an App that is now installed. Hence
|
|
2303
|
+
// the clearing below sits *after* the binding is written: until then,
|
|
2304
|
+
// reloading this URL is the whole retry.
|
|
1764
2305
|
try {
|
|
1765
2306
|
const installation = await getInstallation(app, installationId);
|
|
1766
2307
|
await c.var.services.githubAppInstallations.upsertInstallation(
|
|
@@ -1769,14 +2310,24 @@ settingsRoutes.get("/github-sync/app/callback", async (c) => {
|
|
|
1769
2310
|
installation.account,
|
|
1770
2311
|
);
|
|
1771
2312
|
} catch {
|
|
1772
|
-
|
|
1773
|
-
|
|
2313
|
+
return c.text(
|
|
2314
|
+
"The App was installed, but GitHub did not answer with the account details. Reload this page to try again.",
|
|
2315
|
+
502,
|
|
2316
|
+
);
|
|
1774
2317
|
}
|
|
1775
2318
|
|
|
2319
|
+
// One-shot: clear the cookie so it can't be replayed.
|
|
2320
|
+
setCookie(c, "jant_gh_app_state", "", {
|
|
2321
|
+
httpOnly: true,
|
|
2322
|
+
sameSite: "Lax",
|
|
2323
|
+
path: "/",
|
|
2324
|
+
maxAge: 0,
|
|
2325
|
+
});
|
|
2326
|
+
|
|
1776
2327
|
const navData = await getNavigationData(c);
|
|
1777
2328
|
const base = publicPath(c, "/settings/github-sync");
|
|
1778
|
-
const
|
|
1779
|
-
const
|
|
2329
|
+
const suggestedRepoName = suggestSyncRepoName(c.var.appConfig.siteUrl);
|
|
2330
|
+
const labels = buildRepoPickerLabels(c, suggestedRepoName);
|
|
1780
2331
|
|
|
1781
2332
|
return renderPublicPage(c, {
|
|
1782
2333
|
title: buildPageTitle("GitHub Sync — Pick Repository", navData.siteName),
|
|
@@ -1792,7 +2343,7 @@ settingsRoutes.get("/github-sync/app/callback", async (c) => {
|
|
|
1792
2343
|
labels={labels}
|
|
1793
2344
|
api-base={`${base}/app`}
|
|
1794
2345
|
connect-url={`${base}/app/connect`}
|
|
1795
|
-
install-url={`${base}/app/install`}
|
|
2346
|
+
install-url={`${base}/app/install?force=new`}
|
|
1796
2347
|
cancel-url={publicPath(c, "/settings")}
|
|
1797
2348
|
create-repo-name-hint={suggestedRepoName}
|
|
1798
2349
|
>
|
|
@@ -1810,29 +2361,36 @@ settingsRoutes.get("/github-sync/app/callback", async (c) => {
|
|
|
1810
2361
|
});
|
|
1811
2362
|
|
|
1812
2363
|
/**
|
|
1813
|
-
*
|
|
2364
|
+
* Values that keep a placeholder literal for the client to fill in.
|
|
1814
2365
|
*
|
|
1815
|
-
*
|
|
1816
|
-
*
|
|
1817
|
-
*
|
|
1818
|
-
*
|
|
1819
|
-
*
|
|
2366
|
+
* `i18n._()` renders a placeholder it has no value for as an empty string, so
|
|
2367
|
+
* a label whose value only exists in the browser — a repo count, the picked
|
|
2368
|
+
* repo — must be formatted with the placeholder's own token as its value.
|
|
2369
|
+
* `{repo}` then survives into the serialized labels and the component's
|
|
2370
|
+
* `.replace("{repo}", …)` finds it.
|
|
2371
|
+
*
|
|
2372
|
+
* @param names - Placeholder names the client interpolates.
|
|
2373
|
+
* @returns A values object mapping each name to its own `{name}` token.
|
|
2374
|
+
* @example
|
|
2375
|
+
* i18n._(msg({ message: "Type {repo} to confirm" }), keepForClient("repo"));
|
|
2376
|
+
* // "Type {repo} to confirm"
|
|
1820
2377
|
*/
|
|
1821
|
-
function
|
|
1822
|
-
|
|
1823
|
-
try {
|
|
1824
|
-
firstLabel = new URL(c.var.appConfig.siteUrl).host.split(".")[0] ?? "";
|
|
1825
|
-
} catch {
|
|
1826
|
-
/* fall through */
|
|
1827
|
-
}
|
|
1828
|
-
const slug = firstLabel
|
|
1829
|
-
.toLowerCase()
|
|
1830
|
-
.replace(/[^a-z0-9-_]+/g, "-")
|
|
1831
|
-
.replace(/^-+|-+$/g, "");
|
|
1832
|
-
return slug ? `${slug}-jant-sync` : "jant-site-sync";
|
|
2378
|
+
function keepForClient(...names: string[]): Record<string, string> {
|
|
2379
|
+
return Object.fromEntries(names.map((name) => [name, `{${name}}`]));
|
|
1833
2380
|
}
|
|
1834
2381
|
|
|
1835
|
-
|
|
2382
|
+
/**
|
|
2383
|
+
* Serialize the picker's strings for the client component.
|
|
2384
|
+
*
|
|
2385
|
+
* @param c - Request context, for the active locale.
|
|
2386
|
+
* @param suggestedRepoName - Repository name prefilled on github.com/new;
|
|
2387
|
+
* interpolated here because the client cannot format ICU messages.
|
|
2388
|
+
* @returns The labels as a JSON string for the `labels` attribute.
|
|
2389
|
+
*/
|
|
2390
|
+
function buildRepoPickerLabels(
|
|
2391
|
+
c: Context<Env>,
|
|
2392
|
+
suggestedRepoName: string,
|
|
2393
|
+
): string {
|
|
1836
2394
|
const i18n = getI18n(c);
|
|
1837
2395
|
return JSON.stringify({
|
|
1838
2396
|
pageTitle: i18n._(
|
|
@@ -1919,6 +2477,7 @@ function buildRepoPickerLabels(c: Context<Env>): string {
|
|
|
1919
2477
|
comment:
|
|
1920
2478
|
"@context: GitHub sync picker — paginated repo count hint. Placeholders {shown} and {total} are integers.",
|
|
1921
2479
|
}),
|
|
2480
|
+
keepForClient("shown", "total"),
|
|
1922
2481
|
),
|
|
1923
2482
|
repoSearchHint: i18n._(
|
|
1924
2483
|
msg({
|
|
@@ -1943,10 +2502,12 @@ function buildRepoPickerLabels(c: Context<Env>): string {
|
|
|
1943
2502
|
),
|
|
1944
2503
|
createOnGitHubHint: i18n._(
|
|
1945
2504
|
msg({
|
|
1946
|
-
message:
|
|
2505
|
+
message:
|
|
2506
|
+
"Name prefilled as {name}. The list refreshes when you return.",
|
|
1947
2507
|
comment:
|
|
1948
2508
|
"@context: GitHub sync picker — hint under the create-on-github entry. Placeholder {name} is the suggested repo name.",
|
|
1949
2509
|
}),
|
|
2510
|
+
{ name: suggestedRepoName },
|
|
1950
2511
|
),
|
|
1951
2512
|
classifyLoading: i18n._(
|
|
1952
2513
|
msg({
|
|
@@ -1976,6 +2537,7 @@ function buildRepoPickerLabels(c: Context<Env>): string {
|
|
|
1976
2537
|
comment:
|
|
1977
2538
|
"@context: GitHub sync picker — blocking message when marker belongs to another site. Placeholder {host} is the other site's host.",
|
|
1978
2539
|
}),
|
|
2540
|
+
keepForClient("host"),
|
|
1979
2541
|
),
|
|
1980
2542
|
classificationForeign: i18n._(
|
|
1981
2543
|
msg({
|
|
@@ -1998,6 +2560,7 @@ function buildRepoPickerLabels(c: Context<Env>): string {
|
|
|
1998
2560
|
comment:
|
|
1999
2561
|
"@context: GitHub sync picker — explanatory body for foreign-repo confirmation. Placeholder {repo} is the owner/repo slug.",
|
|
2000
2562
|
}),
|
|
2563
|
+
keepForClient("repo"),
|
|
2001
2564
|
),
|
|
2002
2565
|
confirmInputLabel: i18n._(
|
|
2003
2566
|
msg({
|
|
@@ -2005,6 +2568,7 @@ function buildRepoPickerLabels(c: Context<Env>): string {
|
|
|
2005
2568
|
comment:
|
|
2006
2569
|
"@context: GitHub sync picker — label above the typed-confirmation input",
|
|
2007
2570
|
}),
|
|
2571
|
+
keepForClient("repo"),
|
|
2008
2572
|
),
|
|
2009
2573
|
confirmInputPlaceholder: i18n._(
|
|
2010
2574
|
msg({
|
|
@@ -2083,6 +2647,16 @@ settingsRoutes.post("/github-sync/app/connect", async (c) => {
|
|
|
2083
2647
|
: c.text("Missing installationId or repo.", 400);
|
|
2084
2648
|
}
|
|
2085
2649
|
|
|
2650
|
+
// The installation has to be one the author already authorized, here
|
|
2651
|
+
// or on another of their sites. The account snapshot also spares us a
|
|
2652
|
+
// GitHub round-trip when recording the binding further down.
|
|
2653
|
+
const installation = await findVisibleInstallation(c, installationId);
|
|
2654
|
+
if (!installation) {
|
|
2655
|
+
return wantsJson
|
|
2656
|
+
? c.json({ error: "Unknown installation." }, 404)
|
|
2657
|
+
: c.text("Unknown installation.", 404);
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2086
2660
|
const { parseRepoSlug, createGitHubClient } =
|
|
2087
2661
|
await import("../../lib/github-api.js");
|
|
2088
2662
|
const parsed = parseRepoSlug(repo);
|
|
@@ -2143,6 +2717,18 @@ settingsRoutes.post("/github-sync/app/connect", async (c) => {
|
|
|
2143
2717
|
: c.text(msg, 409);
|
|
2144
2718
|
}
|
|
2145
2719
|
|
|
2720
|
+
// Bind the installation to this site. The callback does this too, but
|
|
2721
|
+
// only for the site that ran the install; connecting a second site to
|
|
2722
|
+
// an installation the author already has is exactly the case that
|
|
2723
|
+
// never passes through there. Without the row, the App-level webhook's
|
|
2724
|
+
// fan-out (uninstall, suspend, repository removed) skips this site
|
|
2725
|
+
// while it happily keeps syncing.
|
|
2726
|
+
await c.var.services.githubAppInstallations.upsertInstallation(
|
|
2727
|
+
installationId,
|
|
2728
|
+
c.var.currentSite.id,
|
|
2729
|
+
installation.account,
|
|
2730
|
+
);
|
|
2731
|
+
|
|
2146
2732
|
// Persist config before creating webhook so the sync service can load it.
|
|
2147
2733
|
await c.var.services.settings.set("GITHUB_SYNC_AUTH_MODE", "app");
|
|
2148
2734
|
await c.var.services.settings.set(
|
|
@@ -2227,21 +2813,38 @@ async function getInstallationTokenFromApp(
|
|
|
2227
2813
|
return getInstallationToken(app, installationId);
|
|
2228
2814
|
}
|
|
2229
2815
|
|
|
2230
|
-
/**
|
|
2816
|
+
/**
|
|
2817
|
+
* List the GitHub App installations the signed-in author may connect
|
|
2818
|
+
* this site to: everything authorized on any site they belong to, this
|
|
2819
|
+
* one included.
|
|
2820
|
+
*/
|
|
2231
2821
|
settingsRoutes.get("/github-sync/app/installations", async (c) => {
|
|
2232
2822
|
const installations =
|
|
2233
|
-
await c.var.services.githubAppInstallations.
|
|
2234
|
-
c
|
|
2823
|
+
await c.var.services.githubAppInstallations.listInstallationsForUser(
|
|
2824
|
+
requireSessionUserId(c),
|
|
2235
2825
|
);
|
|
2236
|
-
return c.json({
|
|
2237
|
-
installations: installations.map((entry) => ({
|
|
2238
|
-
installationId: entry.installationId,
|
|
2239
|
-
account: entry.account,
|
|
2240
|
-
addedAt: entry.addedAt,
|
|
2241
|
-
})),
|
|
2242
|
-
});
|
|
2826
|
+
return c.json({ installations });
|
|
2243
2827
|
});
|
|
2244
2828
|
|
|
2829
|
+
/**
|
|
2830
|
+
* Resolve an `installationId` that arrived in a request into an
|
|
2831
|
+
* installation the caller is allowed to act on.
|
|
2832
|
+
*
|
|
2833
|
+
* Every endpoint below takes the id from the client, and an installation
|
|
2834
|
+
* id is a small integer — without this check a signed-in author on one
|
|
2835
|
+
* site could name another tenant's installation and read their private
|
|
2836
|
+
* repository names, or push this site's content into one of their repos.
|
|
2837
|
+
*/
|
|
2838
|
+
async function findVisibleInstallation(
|
|
2839
|
+
c: Context<Env>,
|
|
2840
|
+
installationId: string,
|
|
2841
|
+
) {
|
|
2842
|
+
return c.var.services.githubAppInstallations.findInstallationForUser(
|
|
2843
|
+
installationId,
|
|
2844
|
+
requireSessionUserId(c),
|
|
2845
|
+
);
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2245
2848
|
/**
|
|
2246
2849
|
* List (or search) repositories accessible via an installation.
|
|
2247
2850
|
*
|
|
@@ -2266,18 +2869,13 @@ settingsRoutes.get("/github-sync/app/repos", async (c) => {
|
|
|
2266
2869
|
const page =
|
|
2267
2870
|
Number.isFinite(pageParam) && pageParam > 0 ? Math.floor(pageParam) : 1;
|
|
2268
2871
|
|
|
2872
|
+
const installation = await findVisibleInstallation(c, installationId);
|
|
2873
|
+
if (!installation) {
|
|
2874
|
+
return c.json({ error: "Unknown installation." }, 404);
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2269
2877
|
try {
|
|
2270
2878
|
if (q) {
|
|
2271
|
-
const installations =
|
|
2272
|
-
await c.var.services.githubAppInstallations.listInstallationsForSite(
|
|
2273
|
-
c.var.currentSite.id,
|
|
2274
|
-
);
|
|
2275
|
-
const installation = installations.find(
|
|
2276
|
-
(i) => i.installationId === installationId,
|
|
2277
|
-
);
|
|
2278
|
-
if (!installation) {
|
|
2279
|
-
return c.json({ error: "Unknown installation." }, 404);
|
|
2280
|
-
}
|
|
2281
2879
|
const result = await searchInstallationRepos(
|
|
2282
2880
|
app,
|
|
2283
2881
|
installationId,
|
|
@@ -2304,11 +2902,13 @@ settingsRoutes.get("/github-sync/app/repos", async (c) => {
|
|
|
2304
2902
|
} catch (err) {
|
|
2305
2903
|
const detail = err instanceof Error ? err.message : String(err);
|
|
2306
2904
|
// GitHub returns 401 when an installation was uninstalled on their
|
|
2307
|
-
// side. Clean up our cached
|
|
2905
|
+
// side. Clean up our cached entries so the UI stops showing a dead
|
|
2906
|
+
// owner — across the user's sites, since the binding that surfaced
|
|
2907
|
+
// this account may well belong to another one of them.
|
|
2308
2908
|
if (/\b401\b/.test(detail) || /\b404\b/.test(detail)) {
|
|
2309
|
-
await c.var.services.githubAppInstallations.
|
|
2909
|
+
await c.var.services.githubAppInstallations.removeInstallationForUser(
|
|
2310
2910
|
installationId,
|
|
2311
|
-
c
|
|
2911
|
+
requireSessionUserId(c),
|
|
2312
2912
|
);
|
|
2313
2913
|
return c.json(
|
|
2314
2914
|
{ error: "Installation is no longer accessible.", removed: true },
|
|
@@ -2338,6 +2938,9 @@ settingsRoutes.post("/github-sync/app/classify", async (c) => {
|
|
|
2338
2938
|
if (!installationId || !repo) {
|
|
2339
2939
|
return c.json({ error: "Missing installationId or repo." }, 400);
|
|
2340
2940
|
}
|
|
2941
|
+
if (!(await findVisibleInstallation(c, installationId))) {
|
|
2942
|
+
return c.json({ error: "Unknown installation." }, 404);
|
|
2943
|
+
}
|
|
2341
2944
|
|
|
2342
2945
|
const { parseRepoSlug, createGitHubClient } =
|
|
2343
2946
|
await import("../../lib/github-api.js");
|