@jant/core 0.6.16 → 0.7.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/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +60 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- 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-CD3FhrOB.js +257 -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-CgaL2jCs.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-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- 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__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -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__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- 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 +541 -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.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -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/bubble-menu.ts +47 -16
- 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/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__/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 +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- 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__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- 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__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- 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__/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 +437 -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-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 +16 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- 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-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- 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 +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -23
- 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-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- 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__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- 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 +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +159 -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 +23 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- 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 +5 -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__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- 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-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/archive.tsx +471 -369
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- 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 +178 -17
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- 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/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post.test.ts +46 -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 +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +27 -22
- 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/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- 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/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +278 -19
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- 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 +1087 -7219
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -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 +64 -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 +275 -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/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/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for who may answer the last setup question on a provisioned site.
|
|
3
|
+
*
|
|
4
|
+
* A provisioned site already has an owner, so `POST /setup` writes the site's
|
|
5
|
+
* name and language on behalf of that owner. Carrying a session is not enough
|
|
6
|
+
* to be that owner — the session may belong to someone this site has never
|
|
7
|
+
* heard of.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it, vi } from "vitest";
|
|
11
|
+
import { Hono } from "hono";
|
|
12
|
+
import { createI18n } from "../../../i18n/index.js";
|
|
13
|
+
import { attachSession } from "../../../middleware/session.js";
|
|
14
|
+
import { ONBOARDING_STATUS } from "../../../lib/constants.js";
|
|
15
|
+
import { setupRoutes } from "../setup.js";
|
|
16
|
+
import type { Bindings } from "../../../types.js";
|
|
17
|
+
import type { AppVariables } from "../../../types/app-context.js";
|
|
18
|
+
|
|
19
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
20
|
+
|
|
21
|
+
function createMockAuth(signedIn: boolean) {
|
|
22
|
+
return {
|
|
23
|
+
api: {
|
|
24
|
+
getSession: async () => ({
|
|
25
|
+
headers: new Headers(),
|
|
26
|
+
response: signedIn
|
|
27
|
+
? {
|
|
28
|
+
user: {
|
|
29
|
+
id: "usr_outsider",
|
|
30
|
+
email: "someone@test.com",
|
|
31
|
+
name: "S",
|
|
32
|
+
},
|
|
33
|
+
session: { id: "ses_outsider" },
|
|
34
|
+
}
|
|
35
|
+
: null,
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
} as unknown as AppVariables["auth"];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createProvisionedSetupApp(options: {
|
|
42
|
+
signedIn: boolean;
|
|
43
|
+
isMember: boolean;
|
|
44
|
+
}) {
|
|
45
|
+
const completeSiteSetup = vi.fn(async () => {});
|
|
46
|
+
|
|
47
|
+
const app = new Hono<Env>();
|
|
48
|
+
app.use("*", async (c, next) => {
|
|
49
|
+
c.env = {} as Bindings;
|
|
50
|
+
c.set("auth", createMockAuth(options.signedIn));
|
|
51
|
+
c.set("services", {
|
|
52
|
+
settings: {
|
|
53
|
+
getOnboardingStatus: async () => ONBOARDING_STATUS.PROVISIONED,
|
|
54
|
+
get: async () => "My Blog",
|
|
55
|
+
},
|
|
56
|
+
bootstrap: { completeSiteSetup },
|
|
57
|
+
siteMembers: {
|
|
58
|
+
get: async () =>
|
|
59
|
+
options.isMember
|
|
60
|
+
? {
|
|
61
|
+
createdAt: 0,
|
|
62
|
+
role: "owner",
|
|
63
|
+
siteId: "sit_test",
|
|
64
|
+
updatedAt: 0,
|
|
65
|
+
userId: "usr_outsider",
|
|
66
|
+
}
|
|
67
|
+
: null,
|
|
68
|
+
},
|
|
69
|
+
} as unknown as AppVariables["services"]);
|
|
70
|
+
c.set("currentSite", {
|
|
71
|
+
createdAt: 0,
|
|
72
|
+
id: "sit_test",
|
|
73
|
+
key: "default",
|
|
74
|
+
status: "active",
|
|
75
|
+
updatedAt: 0,
|
|
76
|
+
});
|
|
77
|
+
c.set("appConfig", {
|
|
78
|
+
siteName: "Jant",
|
|
79
|
+
sitePathPrefix: "",
|
|
80
|
+
siteLanguage: "en",
|
|
81
|
+
} as AppVariables["appConfig"]);
|
|
82
|
+
c.set("lang", "en");
|
|
83
|
+
c.set("i18n", createI18n("en"));
|
|
84
|
+
await next();
|
|
85
|
+
});
|
|
86
|
+
app.use("*", attachSession());
|
|
87
|
+
app.route("/", setupRoutes);
|
|
88
|
+
|
|
89
|
+
return { app, completeSiteSetup };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function postLanguage(app: Hono<Env>) {
|
|
93
|
+
return app.request("/setup", {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: { "Content-Type": "application/json" },
|
|
96
|
+
body: JSON.stringify({ contentLanguage: "zh-Hans" }),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
describe("POST /setup on a provisioned site", () => {
|
|
101
|
+
it("refuses a session that is not a member of this site", async () => {
|
|
102
|
+
const { app, completeSiteSetup } = createProvisionedSetupApp({
|
|
103
|
+
signedIn: true,
|
|
104
|
+
isMember: false,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const res = await postLanguage(app);
|
|
108
|
+
|
|
109
|
+
await expect(res.text()).resolves.toContain("/signin");
|
|
110
|
+
expect(completeSiteSetup).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("lets the site's own owner answer", async () => {
|
|
114
|
+
const { app, completeSiteSetup } = createProvisionedSetupApp({
|
|
115
|
+
signedIn: true,
|
|
116
|
+
isMember: true,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await postLanguage(app);
|
|
120
|
+
|
|
121
|
+
expect(completeSiteSetup).toHaveBeenCalledOnce();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("GET /setup on a provisioned site", () => {
|
|
126
|
+
it("sends a non-member to the site instead of showing the form", async () => {
|
|
127
|
+
const { app } = createProvisionedSetupApp({
|
|
128
|
+
signedIn: true,
|
|
129
|
+
isMember: false,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const res = await app.request("/setup", { redirect: "manual" });
|
|
133
|
+
|
|
134
|
+
expect(res.status).toBe(302);
|
|
135
|
+
expect(res.headers.get("Location")).toBe("/");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the Discover answer the setup screen collects.
|
|
3
|
+
*
|
|
4
|
+
* The point under test is that setup records an answer rather than inferring
|
|
5
|
+
* one — a ticked box and a cleared box both write, and a form that carried no
|
|
6
|
+
* field at all writes nothing, since that last case is an older client or a
|
|
7
|
+
* scripted setup and reading it as a refusal would opt sites out that never
|
|
8
|
+
* said so — and then acts on it, on the same terms as the settings page's own
|
|
9
|
+
* switch.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { Hono } from "hono";
|
|
14
|
+
import { createI18n } from "../../../i18n/index.js";
|
|
15
|
+
import { attachSession } from "../../../middleware/session.js";
|
|
16
|
+
import { ONBOARDING_STATUS } from "../../../lib/constants.js";
|
|
17
|
+
import { setupRoutes } from "../setup.js";
|
|
18
|
+
import type { Bindings } from "../../../types.js";
|
|
19
|
+
import type { AppVariables } from "../../../types/app-context.js";
|
|
20
|
+
|
|
21
|
+
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
22
|
+
|
|
23
|
+
function createMockAuth() {
|
|
24
|
+
return {
|
|
25
|
+
api: {
|
|
26
|
+
getSession: async () => ({
|
|
27
|
+
headers: new Headers(),
|
|
28
|
+
response: {
|
|
29
|
+
user: { id: "usr_owner", email: "owner@test.com", name: "O" },
|
|
30
|
+
session: { id: "ses_owner" },
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
} as unknown as AppVariables["auth"];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A provisioned site whose owner is answering. Both install kinds reach the
|
|
39
|
+
* Discover write through this one branch — a self-hosted site arrives here from
|
|
40
|
+
* its second screen, a hosted one from its only screen.
|
|
41
|
+
*/
|
|
42
|
+
function createSetupApp(
|
|
43
|
+
options: { demoMode?: boolean; shouldAnnounce?: boolean } = {},
|
|
44
|
+
) {
|
|
45
|
+
const completeSiteSetup = vi.fn(async () => {});
|
|
46
|
+
const updateDiscoverSetting = vi.fn(async () => ({
|
|
47
|
+
shouldAnnounce: options.shouldAnnounce ?? true,
|
|
48
|
+
}));
|
|
49
|
+
const announceToDiscover = vi.fn(async () => {});
|
|
50
|
+
// The hook `runDeferred` offers tests: every backgrounded promise lands
|
|
51
|
+
// here, so the announcement can be awaited instead of raced.
|
|
52
|
+
const pending: Promise<unknown>[] = [];
|
|
53
|
+
|
|
54
|
+
const app = new Hono<Env>();
|
|
55
|
+
app.use("*", async (c, next) => {
|
|
56
|
+
c.env = {
|
|
57
|
+
DISCOVER_PING_URL: "https://directory.test/api/discover/ping",
|
|
58
|
+
__pendingDeferred: pending,
|
|
59
|
+
} as unknown as Bindings;
|
|
60
|
+
c.set("auth", createMockAuth());
|
|
61
|
+
c.set("services", {
|
|
62
|
+
settings: {
|
|
63
|
+
getOnboardingStatus: async () => ONBOARDING_STATUS.PROVISIONED,
|
|
64
|
+
// Already named, so this is the hosted screen: language and Discover
|
|
65
|
+
// and nothing else.
|
|
66
|
+
get: async () => "My Blog",
|
|
67
|
+
updateDiscoverSetting,
|
|
68
|
+
announceToDiscover,
|
|
69
|
+
},
|
|
70
|
+
bootstrap: { completeSiteSetup },
|
|
71
|
+
siteMembers: {
|
|
72
|
+
get: async () => ({
|
|
73
|
+
createdAt: 0,
|
|
74
|
+
role: "owner",
|
|
75
|
+
siteId: "sit_test",
|
|
76
|
+
updatedAt: 0,
|
|
77
|
+
userId: "usr_owner",
|
|
78
|
+
}),
|
|
79
|
+
},
|
|
80
|
+
} as unknown as AppVariables["services"]);
|
|
81
|
+
c.set("currentSite", {
|
|
82
|
+
createdAt: 0,
|
|
83
|
+
id: "sit_test",
|
|
84
|
+
key: "default",
|
|
85
|
+
status: "active",
|
|
86
|
+
updatedAt: 0,
|
|
87
|
+
});
|
|
88
|
+
c.set("appConfig", {
|
|
89
|
+
siteName: "Jant",
|
|
90
|
+
sitePathPrefix: "",
|
|
91
|
+
siteLanguage: "en",
|
|
92
|
+
siteUrl: "https://example.com",
|
|
93
|
+
demoMode: options.demoMode ?? false,
|
|
94
|
+
noindex: false,
|
|
95
|
+
rssFeedsEnabled: true,
|
|
96
|
+
} as AppVariables["appConfig"]);
|
|
97
|
+
c.set("lang", "en");
|
|
98
|
+
c.set("i18n", createI18n("en"));
|
|
99
|
+
await next();
|
|
100
|
+
});
|
|
101
|
+
app.use("*", attachSession());
|
|
102
|
+
app.route("/", setupRoutes);
|
|
103
|
+
|
|
104
|
+
return { app, updateDiscoverSetting, announceToDiscover, pending };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function post(app: Hono<Env>, body: Record<string, unknown>) {
|
|
108
|
+
return app.request("/setup", {
|
|
109
|
+
method: "POST",
|
|
110
|
+
headers: { "Content-Type": "application/json" },
|
|
111
|
+
body: JSON.stringify({ contentLanguage: "zh-Hans", ...body }),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
describe("POST /setup, the Discover answer", () => {
|
|
116
|
+
it("stores `latest` when the box was ticked", async () => {
|
|
117
|
+
const { app, updateDiscoverSetting } = createSetupApp();
|
|
118
|
+
|
|
119
|
+
await post(app, { discover: true });
|
|
120
|
+
|
|
121
|
+
expect(updateDiscoverSetting).toHaveBeenCalledWith("latest", {
|
|
122
|
+
demoMode: false,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// The half that matters on a deployment which lists by default: a cleared box
|
|
127
|
+
// has to be written, or `DISCOVER=latest` keeps deciding for someone who just
|
|
128
|
+
// said no.
|
|
129
|
+
it("stores `off` when the box was cleared", async () => {
|
|
130
|
+
const { app, updateDiscoverSetting } = createSetupApp();
|
|
131
|
+
|
|
132
|
+
await post(app, { discover: false });
|
|
133
|
+
|
|
134
|
+
expect(updateDiscoverSetting).toHaveBeenCalledWith("off", {
|
|
135
|
+
demoMode: false,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("writes nothing when the form carried no answer", async () => {
|
|
140
|
+
const { app, updateDiscoverSetting } = createSetupApp();
|
|
141
|
+
|
|
142
|
+
await post(app, {});
|
|
143
|
+
|
|
144
|
+
expect(updateDiscoverSetting).not.toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// The answer takes effect now, not when the author next happens to open a
|
|
148
|
+
// settings page they have no reason to open. A directory decides eligibility
|
|
149
|
+
// for itself and re-reads the feed on its own schedule, so a site with
|
|
150
|
+
// nothing published yet loses nothing by saying hello early.
|
|
151
|
+
it("announces the site when the box was ticked", async () => {
|
|
152
|
+
const { app, announceToDiscover, pending } = createSetupApp();
|
|
153
|
+
|
|
154
|
+
await post(app, { discover: true });
|
|
155
|
+
await Promise.all(pending);
|
|
156
|
+
|
|
157
|
+
expect(announceToDiscover).toHaveBeenCalledWith({
|
|
158
|
+
endpoint: "https://directory.test/api/discover/ping",
|
|
159
|
+
feedUrl: "https://example.com/latest/feed",
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Not awaited: a directory takes up to twelve seconds to give up on, and the
|
|
164
|
+
// last screen of setup is not a place to spend them. The outcome is recorded
|
|
165
|
+
// for the settings page's status block to read back.
|
|
166
|
+
it("does not hold the response while the directory answers", async () => {
|
|
167
|
+
const { app, announceToDiscover, pending } = createSetupApp();
|
|
168
|
+
let settled = false;
|
|
169
|
+
announceToDiscover.mockImplementation(async () => {
|
|
170
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
171
|
+
settled = true;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await post(app, { discover: true });
|
|
175
|
+
|
|
176
|
+
expect(settled).toBe(false);
|
|
177
|
+
await Promise.all(pending);
|
|
178
|
+
expect(settled).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Nothing to tell a directory that was never told anything: going from unset
|
|
182
|
+
// to `off` leaves the site exactly as unlisted as it already was.
|
|
183
|
+
it("announces nothing when the setting service reports no change", async () => {
|
|
184
|
+
const { app, announceToDiscover, pending } = createSetupApp({
|
|
185
|
+
shouldAnnounce: false,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await post(app, { discover: false });
|
|
189
|
+
await Promise.all(pending);
|
|
190
|
+
|
|
191
|
+
expect(announceToDiscover).not.toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("announces nothing from a demo site", async () => {
|
|
195
|
+
const { app, announceToDiscover, pending } = createSetupApp({
|
|
196
|
+
demoMode: true,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await post(app, { discover: true });
|
|
200
|
+
await Promise.all(pending);
|
|
201
|
+
|
|
202
|
+
expect(announceToDiscover).not.toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The whole self-hosted setup, walked once with nothing mocked.
|
|
3
|
+
*
|
|
4
|
+
* Real services, a real better-auth, a real database: the other setup tests
|
|
5
|
+
* each hold one seam still, and this one is here to catch what only shows up
|
|
6
|
+
* when they are all moving — the session the first screen mints being the one
|
|
7
|
+
* the second screen accepts, the status the first screen writes being the one
|
|
8
|
+
* the second screen reads, and the site ending up named after neither of them
|
|
9
|
+
* guessed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { createTestApp } from "../../../__tests__/helpers/app.js";
|
|
14
|
+
import { createAuth } from "../../../auth.js";
|
|
15
|
+
import { attachSession } from "../../../middleware/session.js";
|
|
16
|
+
import {
|
|
17
|
+
settings as settingsTable,
|
|
18
|
+
siteMembers,
|
|
19
|
+
user,
|
|
20
|
+
} from "../../../db/schema.js";
|
|
21
|
+
import { setupRoutes } from "../setup.js";
|
|
22
|
+
|
|
23
|
+
const PASSWORD = "correct-horse-battery";
|
|
24
|
+
|
|
25
|
+
function createSetupApp(options: Parameters<typeof createTestApp>[0] = {}) {
|
|
26
|
+
const { app, services, db } = createTestApp(options);
|
|
27
|
+
|
|
28
|
+
// The helper installs a stub that answers every session question the same
|
|
29
|
+
// way. Setup needs the real thing: it mints a session on the first screen and
|
|
30
|
+
// has to be believed on the second.
|
|
31
|
+
const auth = createAuth(db, {
|
|
32
|
+
secret: "test-secret-with-enough-entropy-for-session-cookies",
|
|
33
|
+
baseURL: "http://localhost",
|
|
34
|
+
useSecureCookies: false,
|
|
35
|
+
});
|
|
36
|
+
app.use("*", async (c, next) => {
|
|
37
|
+
c.set("auth", auth);
|
|
38
|
+
await next();
|
|
39
|
+
});
|
|
40
|
+
app.use("*", attachSession());
|
|
41
|
+
app.route("/", setupRoutes);
|
|
42
|
+
|
|
43
|
+
return { app, services, db, auth };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function post(
|
|
47
|
+
app: ReturnType<typeof createSetupApp>["app"],
|
|
48
|
+
body: Record<string, unknown>,
|
|
49
|
+
cookie?: string,
|
|
50
|
+
) {
|
|
51
|
+
return app.request("/setup", {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: {
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
...(cookie ? { Cookie: cookie } : {}),
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify(body),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The session cookie a response minted, in the form a request sends back. */
|
|
62
|
+
function sessionCookie(res: Response): string {
|
|
63
|
+
const cookie = res.headers
|
|
64
|
+
.getSetCookie()
|
|
65
|
+
.find((value) => value.includes("session_token="));
|
|
66
|
+
expect(cookie).toBeDefined();
|
|
67
|
+
return cookie!.split(";")[0]!;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function readSettings(db: ReturnType<typeof createSetupApp>["db"]) {
|
|
71
|
+
const rows = await db.select().from(settingsTable);
|
|
72
|
+
return Object.fromEntries(rows.map((row) => [row.key, row.value]));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("self-hosted setup, both screens", () => {
|
|
76
|
+
it("walks an empty install to a finished site", async () => {
|
|
77
|
+
const { app, db } = createSetupApp();
|
|
78
|
+
|
|
79
|
+
const first = await app.request("/setup");
|
|
80
|
+
await expect(first.text()).resolves.toContain("Setup · Step 1 of 2");
|
|
81
|
+
|
|
82
|
+
const account = await post(app, {
|
|
83
|
+
email: "owner@example.com",
|
|
84
|
+
password: PASSWORD,
|
|
85
|
+
});
|
|
86
|
+
const cookie = sessionCookie(account);
|
|
87
|
+
|
|
88
|
+
// Between the screens: an owner, a navigation profile, and no answers.
|
|
89
|
+
const midway = await readSettings(db);
|
|
90
|
+
expect(midway["ONBOARDING_STATUS"]).toBe("provisioned");
|
|
91
|
+
expect(midway["SITE_NAME"]).toBeUndefined();
|
|
92
|
+
|
|
93
|
+
// The session the first screen minted is the one the second screen trusts.
|
|
94
|
+
const second = await app.request("/setup", { headers: { Cookie: cookie } });
|
|
95
|
+
const secondHtml = await second.text();
|
|
96
|
+
expect(secondHtml).toContain("Setup · Step 2 of 2");
|
|
97
|
+
expect(secondHtml).toContain("setup-site-name");
|
|
98
|
+
|
|
99
|
+
await post(
|
|
100
|
+
app,
|
|
101
|
+
{
|
|
102
|
+
siteName: "My Blog",
|
|
103
|
+
contentLanguage: "zh-Hans",
|
|
104
|
+
language: "en-GB",
|
|
105
|
+
timezone: "Asia/Shanghai",
|
|
106
|
+
discover: true,
|
|
107
|
+
},
|
|
108
|
+
cookie,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const finished = await readSettings(db);
|
|
112
|
+
expect(finished["ONBOARDING_STATUS"]).toBe("completed");
|
|
113
|
+
expect(finished["SITE_NAME"]).toBe("My Blog");
|
|
114
|
+
expect(finished["SITE_LANGUAGE"]).toBe("zh-Hans");
|
|
115
|
+
expect(finished["TIME_ZONE"]).toBe("Asia/Shanghai");
|
|
116
|
+
expect(finished["DISCOVER"]).toBe("latest");
|
|
117
|
+
// Left following the content language. An English browser is what every
|
|
118
|
+
// unconfigured machine reports, so it is no evidence against the language
|
|
119
|
+
// the author just chose by hand one field above.
|
|
120
|
+
expect(finished["DASHBOARD_LANGUAGE"]).toBeUndefined();
|
|
121
|
+
|
|
122
|
+
// The account opened under the address, and the second screen renamed it.
|
|
123
|
+
const [owner] = await db.select().from(user);
|
|
124
|
+
expect(owner?.name).toBe("My Blog");
|
|
125
|
+
expect(owner?.email).toBe("owner@example.com");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// A finished site must never offer the screen that opens the first account
|
|
129
|
+
// again, to anyone, signed in or not.
|
|
130
|
+
it("closes setup once the site is finished", async () => {
|
|
131
|
+
const { app } = createSetupApp();
|
|
132
|
+
|
|
133
|
+
const cookie = sessionCookie(
|
|
134
|
+
await post(app, { email: "owner@example.com", password: PASSWORD }),
|
|
135
|
+
);
|
|
136
|
+
await post(app, { siteName: "My Blog", contentLanguage: "en" }, cookie);
|
|
137
|
+
|
|
138
|
+
const page = await app.request("/setup", {
|
|
139
|
+
headers: { Cookie: cookie },
|
|
140
|
+
redirect: "manual",
|
|
141
|
+
});
|
|
142
|
+
expect(page.status).toBe(302);
|
|
143
|
+
expect(page.headers.get("Location")).toBe("/");
|
|
144
|
+
|
|
145
|
+
const submitted = await post(
|
|
146
|
+
app,
|
|
147
|
+
{ email: "someone@example.com", password: PASSWORD },
|
|
148
|
+
cookie,
|
|
149
|
+
);
|
|
150
|
+
expect(submitted.status).toBe(302);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Losing the session between the screens is the failure the split invented.
|
|
154
|
+
// The site is provisioned, so the setup screen refuses an unauthenticated
|
|
155
|
+
// visitor — and `/signin` stays open for them, which is how they get back.
|
|
156
|
+
it("hides the second screen from a lost session", async () => {
|
|
157
|
+
const { app } = createSetupApp();
|
|
158
|
+
|
|
159
|
+
await post(app, { email: "owner@example.com", password: PASSWORD });
|
|
160
|
+
|
|
161
|
+
const res = await app.request("/setup", { redirect: "manual" });
|
|
162
|
+
|
|
163
|
+
expect(res.status).toBe(302);
|
|
164
|
+
expect(res.headers.get("Location")).toBe("/");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// The failure that used to brick an install: the account is opened, standing
|
|
168
|
+
// the site up then fails, and the site is left `pending` with a user row in
|
|
169
|
+
// it. From there `createAuth` refuses every signup and the onboarding
|
|
170
|
+
// middleware sends `/signin` and `/reset` back to this screen — so retrying
|
|
171
|
+
// the screen has to be the way out.
|
|
172
|
+
it("resumes a first screen that failed after opening the account", async () => {
|
|
173
|
+
const { app, services, db } = createSetupApp();
|
|
174
|
+
const provision = vi.spyOn(services.bootstrap, "provisionOwnerAccount");
|
|
175
|
+
provision.mockRejectedValueOnce(new Error("D1 unavailable"));
|
|
176
|
+
|
|
177
|
+
const failed = await post(app, {
|
|
178
|
+
email: "owner@example.com",
|
|
179
|
+
password: PASSWORD,
|
|
180
|
+
});
|
|
181
|
+
await expect(failed.text()).resolves.toContain("Couldn't create your");
|
|
182
|
+
|
|
183
|
+
// The account outlived the failure, and the site did not advance.
|
|
184
|
+
expect((await db.select().from(user)).length).toBe(1);
|
|
185
|
+
expect(await services.settings.getOnboardingStatus()).toBe("pending");
|
|
186
|
+
|
|
187
|
+
// Registration is closed to everyone now, this address included — which is
|
|
188
|
+
// why the retry has to resume rather than create.
|
|
189
|
+
const resumed = await post(app, {
|
|
190
|
+
email: "owner@example.com",
|
|
191
|
+
password: PASSWORD,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(await services.settings.getOnboardingStatus()).toBe("provisioned");
|
|
195
|
+
const second = await app.request("/setup", {
|
|
196
|
+
headers: { Cookie: sessionCookie(resumed) },
|
|
197
|
+
});
|
|
198
|
+
await expect(second.text()).resolves.toContain("Setup · Step 2 of 2");
|
|
199
|
+
// Still one account: resuming picked the existing one up.
|
|
200
|
+
expect((await db.select().from(user)).length).toBe(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("refuses to resume on the wrong password", async () => {
|
|
204
|
+
const { app, services, db } = createSetupApp();
|
|
205
|
+
vi.spyOn(services.bootstrap, "provisionOwnerAccount").mockRejectedValueOnce(
|
|
206
|
+
new Error("D1 unavailable"),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
await post(app, { email: "owner@example.com", password: PASSWORD });
|
|
210
|
+
const res = await post(app, {
|
|
211
|
+
email: "owner@example.com",
|
|
212
|
+
password: "not-the-password",
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
await expect(res.text()).resolves.toContain("Couldn't create your");
|
|
216
|
+
expect(res.headers.getSetCookie()).toHaveLength(0);
|
|
217
|
+
expect(await services.settings.getOnboardingStatus()).toBe("pending");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// The reason a different address is not an escape hatch, and so the reason
|
|
221
|
+
// resuming had to be built: the registration hook counts rows, not emails.
|
|
222
|
+
it("will not open a second account to get around it", async () => {
|
|
223
|
+
const { app, services, db } = createSetupApp();
|
|
224
|
+
vi.spyOn(services.bootstrap, "provisionOwnerAccount").mockRejectedValueOnce(
|
|
225
|
+
new Error("D1 unavailable"),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
await post(app, { email: "owner@example.com", password: PASSWORD });
|
|
229
|
+
const res = await post(app, {
|
|
230
|
+
email: "someone-else@example.com",
|
|
231
|
+
password: PASSWORD,
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await expect(res.text()).resolves.toContain("Couldn't create your");
|
|
235
|
+
expect((await db.select().from(user)).length).toBe(1);
|
|
236
|
+
expect(await services.settings.getOnboardingStatus()).toBe("pending");
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// A hosted site's owner arrives through the control plane's handoff, never
|
|
241
|
+
// through this screen. The control plane creates each hosted site already
|
|
242
|
+
// provisioned, so a `pending` one is a site that lost its status somehow — and
|
|
243
|
+
// the database behind it holds every tenant's accounts. Signing in here with
|
|
244
|
+
// any of them must not come away owning the site.
|
|
245
|
+
describe("setup on a host-based install", () => {
|
|
246
|
+
it("never makes an existing account the owner of a hosted site", async () => {
|
|
247
|
+
const { app, services, db, auth } = createSetupApp({
|
|
248
|
+
siteResolutionMode: "host-based",
|
|
249
|
+
});
|
|
250
|
+
// Another tenant's owner. Being the first account, it also closes
|
|
251
|
+
// registration, as it is closed on any hosted database.
|
|
252
|
+
await auth.api.signUpEmail({
|
|
253
|
+
body: {
|
|
254
|
+
name: "Other Tenant",
|
|
255
|
+
email: "other@example.com",
|
|
256
|
+
password: PASSWORD,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const res = await post(app, {
|
|
261
|
+
email: "other@example.com",
|
|
262
|
+
password: PASSWORD,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await expect(res.text()).resolves.toContain("Couldn't create your");
|
|
266
|
+
expect(res.headers.getSetCookie()).toHaveLength(0);
|
|
267
|
+
expect(await db.select().from(siteMembers)).toHaveLength(0);
|
|
268
|
+
expect(await services.settings.getOnboardingStatus()).toBe("pending");
|
|
269
|
+
});
|
|
270
|
+
});
|