@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
package/src/middleware/auth.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* or Bearer API tokens.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { MiddlewareHandler } from "hono";
|
|
8
|
+
import type { Context, MiddlewareHandler } from "hono";
|
|
9
9
|
import type { Bindings } from "../types.js";
|
|
10
10
|
import type { AppVariables } from "../types/app-context.js";
|
|
11
11
|
import { getDevApiToken, getInternalAdminToken } from "../lib/env.js";
|
|
@@ -110,6 +110,42 @@ function getPostSigninRedirect(requestUrl: string): string | null {
|
|
|
110
110
|
return isSafeInternalRedirect(candidate) ? candidate : null;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Whether the request's session belongs to a member of the current site.
|
|
115
|
+
*
|
|
116
|
+
* This is the single definition of "signed in *here*", and every entrance must
|
|
117
|
+
* ask it rather than `c.var.isAuthenticated`. That flag only answers "carries a
|
|
118
|
+
* session", which is a weaker claim: better-auth's cookie cache keeps answering
|
|
119
|
+
* from the browser's own copy for minutes after the rows behind it are gone, so
|
|
120
|
+
* a session routinely outlives the site it was issued for — a factory reset and
|
|
121
|
+
* a fresh setup being the obvious way. When a guard requires membership and an
|
|
122
|
+
* entrance settles for a session, the two disagree and bounce the visitor
|
|
123
|
+
* between them until the browser gives up.
|
|
124
|
+
*
|
|
125
|
+
* @param c - The request context, after `attachSession` has run
|
|
126
|
+
* @returns `true` when the session's user is a member of `c.var.currentSite`
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* // /signin only sends someone away if they are signed in to *this* site.
|
|
131
|
+
* if (await isCurrentSiteMember(c)) return c.redirect("/");
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
export async function isCurrentSiteMember(c: Context<Env>): Promise<boolean> {
|
|
135
|
+
const session = c.var.session;
|
|
136
|
+
if (!session?.user) return false;
|
|
137
|
+
|
|
138
|
+
// `siteMembers.get` resolves to `null` for "not a member", so a throw is
|
|
139
|
+
// always infrastructure and is left to propagate. Rewriting a failing
|
|
140
|
+
// database read into "not a member" would read to the user as being randomly
|
|
141
|
+
// signed out.
|
|
142
|
+
const membership = await c.var.services.siteMembers.get(
|
|
143
|
+
c.var.currentSite.id,
|
|
144
|
+
session.user.id,
|
|
145
|
+
);
|
|
146
|
+
return !!membership;
|
|
147
|
+
}
|
|
148
|
+
|
|
113
149
|
/**
|
|
114
150
|
* Middleware that requires authentication.
|
|
115
151
|
* Redirects to signin page if not authenticated.
|
|
@@ -136,25 +172,16 @@ export function requireAuth(redirectTo = "/signin"): MiddlewareHandler<Env> {
|
|
|
136
172
|
return `${publicHref}${separator}redirect=${encodeURIComponent(postSignin)}`;
|
|
137
173
|
};
|
|
138
174
|
|
|
139
|
-
// Session was already fetched by `attachSession` middleware.
|
|
140
|
-
|
|
141
|
-
|
|
175
|
+
// Session was already fetched by `attachSession` middleware. The membership
|
|
176
|
+
// read inside `isCurrentSiteMember` and `next()` both stay outside a catch:
|
|
177
|
+
// a failing database read or a route handler blowing up used to be
|
|
178
|
+
// rewritten into a redirect to sign-in, which reads to the user as being
|
|
179
|
+
// randomly signed out and loses whatever they were doing.
|
|
180
|
+
if (!(await isCurrentSiteMember(c))) {
|
|
142
181
|
return c.redirect(buildRedirectTarget());
|
|
143
182
|
}
|
|
144
183
|
|
|
145
|
-
|
|
146
|
-
const membership = await c.var.services.siteMembers.get(
|
|
147
|
-
c.var.currentSite.id,
|
|
148
|
-
session.user.id,
|
|
149
|
-
);
|
|
150
|
-
if (!membership) {
|
|
151
|
-
return c.redirect(buildRedirectTarget());
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
await next();
|
|
155
|
-
} catch {
|
|
156
|
-
return c.redirect(buildRedirectTarget());
|
|
157
|
-
}
|
|
184
|
+
await next();
|
|
158
185
|
};
|
|
159
186
|
}
|
|
160
187
|
|
|
@@ -166,20 +193,12 @@ export function requireAuth(redirectTo = "/signin"): MiddlewareHandler<Env> {
|
|
|
166
193
|
export function requireAuthApi(): MiddlewareHandler<Env> {
|
|
167
194
|
return async (c, next) => {
|
|
168
195
|
// 1. Try session auth (session is pre-fetched by `attachSession` middleware).
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
);
|
|
176
|
-
if (membership) {
|
|
177
|
-
await next();
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
} catch {
|
|
181
|
-
// Membership check failed — fall through to Bearer token
|
|
182
|
-
}
|
|
196
|
+
// Only the membership lookup falls through to Bearer on failure. `next()`
|
|
197
|
+
// must stay outside that catch, or a route handler throwing turns into a
|
|
198
|
+
// 401 and the client treats a server error as an expired login.
|
|
199
|
+
if (await isCurrentSiteMember(c).catch(() => false)) {
|
|
200
|
+
await next();
|
|
201
|
+
return;
|
|
183
202
|
}
|
|
184
203
|
|
|
185
204
|
// 2. Try Bearer token auth
|
|
@@ -20,6 +20,26 @@
|
|
|
20
20
|
* sitemaps, favicons, manifests, static assets) set their own `Cache-Control`
|
|
21
21
|
* explicitly; this middleware leaves those untouched and only fills in the
|
|
22
22
|
* default for the un-annotated dynamic responses.
|
|
23
|
+
*
|
|
24
|
+
* The default differs by audience, because `no-store` costs more than it
|
|
25
|
+
* looks: it also disables the browser's back/forward cache, so going back
|
|
26
|
+
* re-requests the page, re-renders it and re-runs its scripts instead of
|
|
27
|
+
* restoring it instantly. For an anonymous visitor there is nothing to
|
|
28
|
+
* protect — the page is the public one anybody gets — so they get `no-cache`,
|
|
29
|
+
* which keeps shared caches out exactly as before (that is `private`'s job)
|
|
30
|
+
* while letting the browser hold its own copy and revalidate.
|
|
31
|
+
*
|
|
32
|
+
* A signed-in author keeps `no-store`. The back/forward cache restores a whole
|
|
33
|
+
* page snapshot without asking the server, so a page rendered while signed in
|
|
34
|
+
* would still be there after signing out — on a shared machine that shows the
|
|
35
|
+
* previous session's view. Trading the author's back-button latency for that
|
|
36
|
+
* is not worth it; readers are the traffic that matters here anyway.
|
|
37
|
+
*
|
|
38
|
+
* Credential surfaces are the exception to "anonymous has nothing to protect".
|
|
39
|
+
* A sign-in form, a setup form, a reset page whose URL carries a one-time
|
|
40
|
+
* token, an SSO handoff: all of them are served to someone with no session
|
|
41
|
+
* yet, so the anonymous default would let the browser write them to disk.
|
|
42
|
+
* `noStore()` marks those paths, and `app.tsx` mounts it on every one of them.
|
|
23
43
|
*/
|
|
24
44
|
|
|
25
45
|
import type { MiddlewareHandler } from "hono";
|
|
@@ -29,32 +49,77 @@ import type { AppVariables } from "../types/app-context.js";
|
|
|
29
49
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
30
50
|
|
|
31
51
|
/**
|
|
32
|
-
*
|
|
33
|
-
* `
|
|
34
|
-
*
|
|
52
|
+
* Nothing keeps a copy. `private` forbids shared/CDN caches from storing the
|
|
53
|
+
* response; `no-store` prevents any cache — the browser's own, and its
|
|
54
|
+
* back/forward cache — from keeping one either.
|
|
55
|
+
*
|
|
56
|
+
* The default for a signed-in author, and the rule for every credential
|
|
57
|
+
* surface whatever the audience.
|
|
35
58
|
*/
|
|
36
|
-
const
|
|
59
|
+
const NO_STORE_CACHE_CONTROL = "private, no-store";
|
|
37
60
|
|
|
38
61
|
/**
|
|
39
|
-
*
|
|
40
|
-
* `
|
|
62
|
+
* Default for anonymous responses. `private` still keeps shared/CDN caches
|
|
63
|
+
* out; `no-cache` lets the browser keep a copy it must revalidate before
|
|
64
|
+
* reuse, which is what makes the page eligible for the back/forward cache.
|
|
65
|
+
*/
|
|
66
|
+
const ANONYMOUS_CACHE_CONTROL = "private, no-cache";
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Middleware that defaults a missing `Cache-Control` header: `private,
|
|
70
|
+
* no-cache` for anonymous responses, `private, no-store` once signed in.
|
|
41
71
|
*
|
|
42
72
|
* Runs after the route handler: if the handler (or an inner middleware)
|
|
43
73
|
* already set `Cache-Control`, that explicit value wins. Only responses that
|
|
44
|
-
* declare nothing receive the
|
|
74
|
+
* declare nothing receive the default. Requires `attachSession()` to have run
|
|
75
|
+
* first; without it every response is treated as anonymous, so mount it after.
|
|
45
76
|
*
|
|
46
77
|
* @returns Hono middleware enforcing the default cache policy.
|
|
47
78
|
*
|
|
48
79
|
* @example
|
|
49
80
|
* ```ts
|
|
81
|
+
* app.use("*", attachSession());
|
|
50
82
|
* app.use("*", defaultCacheControl());
|
|
51
83
|
* ```
|
|
52
84
|
*/
|
|
53
85
|
export function defaultCacheControl(): MiddlewareHandler<Env> {
|
|
54
86
|
return async (c, next) => {
|
|
55
87
|
await next();
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
88
|
+
if (c.res.headers.has("Cache-Control")) return;
|
|
89
|
+
c.res.headers.set(
|
|
90
|
+
"Cache-Control",
|
|
91
|
+
c.var.isAuthenticated ? NO_STORE_CACHE_CONTROL : ANONYMOUS_CACHE_CONTROL,
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Middleware for a path that must never be written to any cache.
|
|
98
|
+
*
|
|
99
|
+
* For the credential surfaces: sign-in, sign-out, setup, password reset, the
|
|
100
|
+
* SSO handoff, the dev login, and better-auth's own endpoints. Every one of
|
|
101
|
+
* them is reached without a session, so `defaultCacheControl` would hand them
|
|
102
|
+
* the anonymous `no-cache` — which lets the browser keep the page, and the URL
|
|
103
|
+
* it came from, on disk. A reset link's one-time token then outlives the visit
|
|
104
|
+
* on a shared machine.
|
|
105
|
+
*
|
|
106
|
+
* Unconditional, unlike the default: this is a rule about the path, not a
|
|
107
|
+
* fallback for a path that said nothing. Mount it after
|
|
108
|
+
* `defaultCacheControl()` so the header is already set by the time that one
|
|
109
|
+
* looks, and before the handler it protects — a terminal handler registered
|
|
110
|
+
* first ends the chain and this never runs.
|
|
111
|
+
*
|
|
112
|
+
* @returns Hono middleware forcing `private, no-store`.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* app.use("/reset", noStore());
|
|
117
|
+
* app.route("/", resetRoutes);
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
export function noStore(): MiddlewareHandler<Env> {
|
|
121
|
+
return async (c, next) => {
|
|
122
|
+
await next();
|
|
123
|
+
c.res.headers.set("Cache-Control", NO_STORE_CACHE_CONTROL);
|
|
59
124
|
};
|
|
60
125
|
}
|
package/src/middleware/config.ts
CHANGED
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
import { buildThemeStyle, resolveBuiltinTheme } from "../lib/theme.js";
|
|
19
19
|
import {
|
|
20
20
|
BUILTIN_FONT_THEMES,
|
|
21
|
-
getCjkFontCssVariables,
|
|
22
21
|
getFontThemeCssVariables,
|
|
23
22
|
} from "../ui/font-themes.js";
|
|
24
23
|
|
|
@@ -47,14 +46,15 @@ export function withConfig(): MiddlewareHandler<Env> {
|
|
|
47
46
|
// Resolve active color theme
|
|
48
47
|
const activeTheme = resolveBuiltinTheme(appConfig.themeId);
|
|
49
48
|
|
|
50
|
-
// Build font theme CSS variables
|
|
49
|
+
// Build font theme CSS variables. The CJK fallback stacks are deliberately
|
|
50
|
+
// NOT resolved here: they depend on the language of the page being
|
|
51
|
+
// rendered — a post's own language, or the language a filtered list view is
|
|
52
|
+
// showing — which this middleware runs too early to know. `BaseLayout`
|
|
53
|
+
// emits them alongside the matching CJK stylesheet instead.
|
|
51
54
|
const fontTheme = BUILTIN_FONT_THEMES.find(
|
|
52
55
|
(f) => f.id === appConfig.fontThemeId,
|
|
53
56
|
);
|
|
54
|
-
const fontOverrides = {
|
|
55
|
-
...getCjkFontCssVariables(appConfig.siteLanguage, appConfig.cjkSerifFont),
|
|
56
|
-
...(fontTheme ? getFontThemeCssVariables(fontTheme) : {}),
|
|
57
|
-
};
|
|
57
|
+
const fontOverrides = fontTheme ? getFontThemeCssVariables(fontTheme) : {};
|
|
58
58
|
|
|
59
59
|
const themeStyle = buildThemeStyle(
|
|
60
60
|
activeTheme,
|
|
@@ -13,8 +13,10 @@ import {
|
|
|
13
13
|
ConfigurationError,
|
|
14
14
|
DomainError,
|
|
15
15
|
NotFoundError,
|
|
16
|
+
SiteUnavailableError,
|
|
16
17
|
ValidationError,
|
|
17
18
|
} from "../lib/errors.js";
|
|
19
|
+
import { renderSiteUnavailablePage } from "../lib/site-unavailable-page.js";
|
|
18
20
|
import { dsToast } from "../lib/sse.js";
|
|
19
21
|
import { getRuntimeConfigurationErrorPage } from "../lib/startup-config.js";
|
|
20
22
|
|
|
@@ -68,6 +70,16 @@ export const errorHandler: ErrorHandler<Env> = (err, c) => {
|
|
|
68
70
|
return c.json({ error: "Something went wrong on our end" }, 500);
|
|
69
71
|
}
|
|
70
72
|
|
|
73
|
+
// A suspended site: the host is real, so answer with a page that says so
|
|
74
|
+
// instead of the generic 404 shell. 503 keeps the site's search index alive
|
|
75
|
+
// while it can still be restored; `Retry-After` is deliberately omitted
|
|
76
|
+
// because we can't promise when (or whether) it comes back.
|
|
77
|
+
if (err instanceof SiteUnavailableError) {
|
|
78
|
+
return c.html(renderSiteUnavailablePage(c.env), 503, {
|
|
79
|
+
"Cache-Control": "no-store",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
// Non-API routes: map NotFoundError to Hono's built-in 404
|
|
72
84
|
if (err instanceof NotFoundError) {
|
|
73
85
|
return c.notFound();
|
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Onboarding Middleware
|
|
3
3
|
*
|
|
4
|
-
* Redirects key page routes to /setup
|
|
4
|
+
* Redirects key page routes to /setup while first-run setup is unfinished.
|
|
5
5
|
* Uses an allowlist approach: only explicitly listed page routes are redirected,
|
|
6
6
|
* so static assets, API endpoints, feeds, and other resources always pass through.
|
|
7
7
|
* Caches the result in memory so the DB is only queried once per isolate lifetime.
|
|
8
|
+
*
|
|
9
|
+
* How much is gated depends on how far setup has got:
|
|
10
|
+
*
|
|
11
|
+
* - `pending` — nobody owns the site and it holds nothing. Readers have no
|
|
12
|
+
* business being anywhere but setup, so the public root is gated too.
|
|
13
|
+
* - `provisioned` — the site has an owner and owes setup its last answers. A
|
|
14
|
+
* control plane leaves a hosted site here at creation, and a self-hosted one
|
|
15
|
+
* sits here between its two setup screens. Either way the site is real, so
|
|
16
|
+
* readers are served normally; only the author's own entrances are gated,
|
|
17
|
+
* and only once they are signed in, because the remaining questions are
|
|
18
|
+
* theirs to answer. `/signin` staying open is what lets a self-hosted author
|
|
19
|
+
* who lost the session mid-setup get back to the screen they left.
|
|
8
20
|
*/
|
|
9
21
|
|
|
10
22
|
import type { MiddlewareHandler } from "hono";
|
|
11
23
|
import type { Bindings } from "../types.js";
|
|
12
24
|
import type { AppVariables } from "../types/app-context.js";
|
|
25
|
+
import { ONBOARDING_STATUS } from "../lib/constants.js";
|
|
13
26
|
import { getRuntimeSitePathPrefix } from "../lib/site-resolution.js";
|
|
14
27
|
import { toPublicPath } from "../lib/url.js";
|
|
15
28
|
|
|
@@ -31,16 +44,23 @@ export function requireOnboarding(): MiddlewareHandler<Env> {
|
|
|
31
44
|
return next();
|
|
32
45
|
}
|
|
33
46
|
|
|
34
|
-
if (!
|
|
47
|
+
if (!isGatedPath(path)) {
|
|
35
48
|
return next();
|
|
36
49
|
}
|
|
37
50
|
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
51
|
+
const status = await c.var.services.settings.getOnboardingStatus();
|
|
52
|
+
if (status === ONBOARDING_STATUS.COMPLETED) {
|
|
40
53
|
completedOnboardingSites.add(c.var.currentSite.id);
|
|
41
54
|
return next();
|
|
42
55
|
}
|
|
43
56
|
|
|
57
|
+
if (
|
|
58
|
+
status === ONBOARDING_STATUS.PROVISIONED &&
|
|
59
|
+
!(c.var.isAuthenticated && isAuthorPath(path))
|
|
60
|
+
) {
|
|
61
|
+
return next();
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
const sitePathPrefix = getRuntimeSitePathPrefix({
|
|
45
65
|
env: c.env,
|
|
46
66
|
appConfig: c.var.appConfig,
|
|
@@ -52,15 +72,17 @@ export function requireOnboarding(): MiddlewareHandler<Env> {
|
|
|
52
72
|
}
|
|
53
73
|
|
|
54
74
|
/**
|
|
55
|
-
* Only these page routes are redirected to /setup
|
|
75
|
+
* Only these page routes are ever redirected to /setup.
|
|
56
76
|
* Everything else (assets, API, feeds, media, etc.) passes through.
|
|
57
77
|
*/
|
|
58
|
-
function
|
|
78
|
+
function isGatedPath(path: string): boolean {
|
|
79
|
+
return path === "/signin" || path === "/reset" || isAuthorPath(path);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The routes an author uses to reach their own site. */
|
|
83
|
+
function isAuthorPath(path: string): boolean {
|
|
59
84
|
return (
|
|
60
|
-
path === "/" ||
|
|
61
|
-
path === "/signin" ||
|
|
62
|
-
path === "/reset" ||
|
|
63
|
-
path.startsWith("/settings")
|
|
85
|
+
path === "/" || path.startsWith("/settings") || path.startsWith("/compose")
|
|
64
86
|
);
|
|
65
87
|
}
|
|
66
88
|
|
|
@@ -14,7 +14,7 @@ import type { MiddlewareHandler } from "hono";
|
|
|
14
14
|
import type { Bindings } from "../types.js";
|
|
15
15
|
import type { AppVariables } from "../types/app-context.js";
|
|
16
16
|
import { getConfiguredStorageDriver, getEnvString } from "../lib/env.js";
|
|
17
|
-
import { IS_VITE_DEV } from "../lib/
|
|
17
|
+
import { IS_VITE_DEV } from "../lib/build-env.js";
|
|
18
18
|
import {
|
|
19
19
|
buildCspDirectives,
|
|
20
20
|
type ContentSecurityPolicyDirectives,
|
|
@@ -6,31 +6,119 @@
|
|
|
6
6
|
*
|
|
7
7
|
* This replaces ad-hoc `auth.api.getSession()` calls scattered across
|
|
8
8
|
* view helpers (e.g. `lib/navigation.ts`) so each request only parses
|
|
9
|
-
* the session cookie once.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* the session cookie once.
|
|
10
|
+
*
|
|
11
|
+
* `getSession` is not purely a read: better-auth re-issues the session cookie
|
|
12
|
+
* with a fresh `Max-Age` once per `updateAge`, and rebuilds the short-lived
|
|
13
|
+
* `session_data` cookie cache whenever it misses. Those live in the endpoint's
|
|
14
|
+
* response headers, which better-call only hands back under `returnHeaders`
|
|
15
|
+
* — without it the cookies are silently dropped, the browser copy expires a
|
|
16
|
+
* fixed 30 days after sign-in however active the user is, and the cookie cache
|
|
17
|
+
* never survives its first 5 minutes. So we take the headers and write them
|
|
18
|
+
* back onto the outgoing response.
|
|
12
19
|
*
|
|
13
20
|
* Never throws — any lookup error is treated as "not authenticated".
|
|
14
21
|
*/
|
|
15
22
|
|
|
16
|
-
import type { MiddlewareHandler } from "hono";
|
|
23
|
+
import type { Context, MiddlewareHandler } from "hono";
|
|
17
24
|
import type { Bindings } from "../types.js";
|
|
18
25
|
import type { AppVariables } from "../types/app-context.js";
|
|
19
26
|
|
|
20
27
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
21
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Extracts the cookie name from a `Set-Cookie` header value.
|
|
31
|
+
*
|
|
32
|
+
* @param setCookie - A single `Set-Cookie` value, e.g. `a=b; Path=/; Max-Age=1`
|
|
33
|
+
* @returns The cookie name, or the whole trimmed string when there is no `=`
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* readCookieName("__Secure-better-auth.session_token=abc; Path=/"); // "__Secure-better-auth.session_token"
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
function readCookieName(setCookie: string): string {
|
|
41
|
+
const separator = setCookie.indexOf("=");
|
|
42
|
+
return (separator === -1 ? setCookie : setCookie.slice(0, separator)).trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether a `Cache-Control` value lets a shared cache store the response.
|
|
47
|
+
*
|
|
48
|
+
* A missing header counts as not shared-cacheable: `defaultCacheControl` fills
|
|
49
|
+
* those in with `private, no-store` later in the chain, and this middleware
|
|
50
|
+
* must stay correct no matter which of the two unwinds first.
|
|
51
|
+
*
|
|
52
|
+
* @param cacheControl - The response's `Cache-Control`, if any
|
|
53
|
+
* @returns `true` when a CDN or proxy is allowed to keep a copy
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* isSharedCacheable("private, no-store"); // false
|
|
58
|
+
* isSharedCacheable("public, max-age=31536000"); // true
|
|
59
|
+
* isSharedCacheable(null); // false
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
function isSharedCacheable(cacheControl: string | null): boolean {
|
|
63
|
+
if (!cacheControl) return false;
|
|
64
|
+
const directives = cacheControl.toLowerCase();
|
|
65
|
+
return !directives.includes("no-store") && !directives.includes("private");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Copies better-auth's refreshed cookies onto the response the route produced.
|
|
70
|
+
*
|
|
71
|
+
* Two things are deliberately skipped:
|
|
72
|
+
*
|
|
73
|
+
* - **Shared-cacheable responses.** `/media/*` answers with
|
|
74
|
+
* `public, max-age=31536000, immutable`; a session cookie riding along on
|
|
75
|
+
* that response is a session handed to whoever the CDN serves next.
|
|
76
|
+
* `Cache-Control: public` is this codebase's explicit marker for
|
|
77
|
+
* "auth-invariant, safe to store" (see `middleware/cache-control.ts`), so it
|
|
78
|
+
* is exactly the signal to stay out of.
|
|
79
|
+
* - **Cookie names the route already set.** `/signout` clears the session
|
|
80
|
+
* cookie after this middleware read (and possibly refreshed) it. Appending
|
|
81
|
+
* ours last would land after the route's deletion and resurrect the session.
|
|
82
|
+
*/
|
|
83
|
+
function applyAuthCookies(
|
|
84
|
+
c: Context<Env>,
|
|
85
|
+
authHeaders: Headers | null | undefined,
|
|
86
|
+
): void {
|
|
87
|
+
const refreshed = authHeaders?.getSetCookie() ?? [];
|
|
88
|
+
if (refreshed.length === 0) return;
|
|
89
|
+
if (isSharedCacheable(c.res.headers.get("Cache-Control"))) return;
|
|
90
|
+
|
|
91
|
+
const alreadySet = new Set(c.res.headers.getSetCookie().map(readCookieName));
|
|
92
|
+
for (const cookie of refreshed) {
|
|
93
|
+
if (alreadySet.has(readCookieName(cookie))) continue;
|
|
94
|
+
c.res.headers.append("Set-Cookie", cookie);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
22
98
|
export function attachSession(): MiddlewareHandler<Env> {
|
|
23
99
|
return async (c, next) => {
|
|
24
|
-
|
|
25
|
-
|
|
100
|
+
const read = () =>
|
|
101
|
+
c.var.auth.api.getSession({
|
|
26
102
|
headers: c.req.raw.headers,
|
|
103
|
+
returnHeaders: true,
|
|
27
104
|
});
|
|
28
|
-
|
|
29
|
-
|
|
105
|
+
|
|
106
|
+
let authHeaders: Headers | null = null;
|
|
107
|
+
try {
|
|
108
|
+
// One retry: a blipping database read would otherwise present to the
|
|
109
|
+
// user as a logout, and `requireAuth` would send them back to sign-in
|
|
110
|
+
// with a session that was valid the whole time.
|
|
111
|
+
const { headers, response } = await read().catch(() => read());
|
|
112
|
+
authHeaders = headers;
|
|
113
|
+
c.set("session", response ?? null);
|
|
114
|
+
c.set("isAuthenticated", !!response?.user);
|
|
30
115
|
} catch {
|
|
31
116
|
c.set("session", null);
|
|
32
117
|
c.set("isAuthenticated", false);
|
|
33
118
|
}
|
|
119
|
+
|
|
34
120
|
await next();
|
|
121
|
+
|
|
122
|
+
applyAuthCookies(c, authHeaders);
|
|
35
123
|
};
|
|
36
124
|
}
|
|
@@ -1,27 +1,25 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
+
const MIGRATE_ENV_KEYS = [
|
|
4
|
+
"DATABASE_URL",
|
|
5
|
+
"DATA_DIR",
|
|
6
|
+
"JANT_DEBUG_MIGRATE",
|
|
7
|
+
"JANT_ENV_FILE",
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
3
10
|
describe("jant migrate", () => {
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
11
|
+
const originalEnv = Object.fromEntries(
|
|
12
|
+
MIGRATE_ENV_KEYS.map((key) => [key, process.env[key]]),
|
|
13
|
+
) as Record<(typeof MIGRATE_ENV_KEYS)[number], string | undefined>;
|
|
7
14
|
|
|
8
15
|
afterEach(() => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
delete process.env.DATA_DIR;
|
|
17
|
-
} else {
|
|
18
|
-
process.env.DATA_DIR = originalDataDir;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (originalDebugFlag === undefined) {
|
|
22
|
-
delete process.env.JANT_DEBUG_MIGRATE;
|
|
23
|
-
} else {
|
|
24
|
-
process.env.JANT_DEBUG_MIGRATE = originalDebugFlag;
|
|
16
|
+
for (const key of MIGRATE_ENV_KEYS) {
|
|
17
|
+
const value = originalEnv[key];
|
|
18
|
+
if (value === undefined) {
|
|
19
|
+
delete process.env[key];
|
|
20
|
+
} else {
|
|
21
|
+
process.env[key] = value;
|
|
22
|
+
}
|
|
25
23
|
}
|
|
26
24
|
|
|
27
25
|
vi.restoreAllMocks();
|
|
@@ -33,6 +31,10 @@ describe("jant migrate", () => {
|
|
|
33
31
|
"postgres://shell_user:super-secret@db.example.com:5432/shell_db";
|
|
34
32
|
delete process.env.DATA_DIR;
|
|
35
33
|
process.env.JANT_DEBUG_MIGRATE = "1";
|
|
34
|
+
// This test is about the auto-located `.env.node` itself, so it opts back
|
|
35
|
+
// into the search the suite disables in `vitest.config.ts`. `node:fs` is
|
|
36
|
+
// mocked below, so the file it finds is the mock's content, not a real one.
|
|
37
|
+
delete process.env.JANT_ENV_FILE;
|
|
36
38
|
|
|
37
39
|
const migrate = vi.fn(async () => {});
|
|
38
40
|
const poolQuery = vi
|