@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
|
@@ -36,13 +36,18 @@ function createTestHonoApp() {
|
|
|
36
36
|
function createMockAuth(authenticated: boolean) {
|
|
37
37
|
return {
|
|
38
38
|
api: {
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// `attachSession` reads with `returnHeaders: true` so it can write
|
|
40
|
+
// better-auth's refreshed cookies back onto the response, which puts the
|
|
41
|
+
// result in better-call's `{ headers, response }` shape.
|
|
42
|
+
getSession: async () => ({
|
|
43
|
+
headers: new Headers(),
|
|
44
|
+
response: authenticated
|
|
41
45
|
? {
|
|
42
46
|
user: { id: "user-1", email: "test@test.com", name: "Test" },
|
|
43
47
|
session: { id: "session-1" },
|
|
44
48
|
}
|
|
45
49
|
: null,
|
|
50
|
+
}),
|
|
46
51
|
},
|
|
47
52
|
} as AppVariables["auth"];
|
|
48
53
|
}
|
|
@@ -200,9 +205,72 @@ describe("requireAuth", () => {
|
|
|
200
205
|
expect(res.status).toBe(302);
|
|
201
206
|
expect(res.headers.get("Location")).toBe("/login");
|
|
202
207
|
});
|
|
208
|
+
|
|
209
|
+
it("surfaces a route handler's error instead of bouncing to signin", async () => {
|
|
210
|
+
const app = createTestHonoApp();
|
|
211
|
+
app.use("*", async (c, next) => {
|
|
212
|
+
c.set("auth", createMockAuth(true));
|
|
213
|
+
c.set("services", {
|
|
214
|
+
siteMembers: createMockSiteMembers(),
|
|
215
|
+
} as AppVariables["services"]);
|
|
216
|
+
await next();
|
|
217
|
+
});
|
|
218
|
+
app.use("*", attachSession());
|
|
219
|
+
app.get("/settings", requireAuth(), () => {
|
|
220
|
+
throw new Error("saving the post blew up");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// Rewriting this into a redirect reads to the author as being randomly
|
|
224
|
+
// signed out, and throws away whatever they were editing.
|
|
225
|
+
const res = await app.request("/settings", { redirect: "manual" });
|
|
226
|
+
expect(res.status).toBe(500);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("surfaces a failed membership lookup instead of bouncing to signin", async () => {
|
|
230
|
+
const app = createTestHonoApp();
|
|
231
|
+
app.use("*", async (c, next) => {
|
|
232
|
+
c.set("auth", createMockAuth(true));
|
|
233
|
+
c.set("services", {
|
|
234
|
+
siteMembers: {
|
|
235
|
+
...createMockSiteMembers(),
|
|
236
|
+
get: vi.fn(async () => {
|
|
237
|
+
throw new Error("database unavailable");
|
|
238
|
+
}),
|
|
239
|
+
},
|
|
240
|
+
} as unknown as AppVariables["services"]);
|
|
241
|
+
await next();
|
|
242
|
+
});
|
|
243
|
+
app.use("*", attachSession());
|
|
244
|
+
app.get("/settings", requireAuth(), (c) => c.text("Settings"));
|
|
245
|
+
|
|
246
|
+
// `siteMembers.get` resolves to `null` for "not a member", so a throw is
|
|
247
|
+
// always infrastructure — never a reason to claim the user is signed out.
|
|
248
|
+
const res = await app.request("/settings", { redirect: "manual" });
|
|
249
|
+
expect(res.status).toBe(500);
|
|
250
|
+
});
|
|
203
251
|
});
|
|
204
252
|
|
|
205
253
|
describe("requireAuthApi", () => {
|
|
254
|
+
it("surfaces a route handler's error instead of answering 401", async () => {
|
|
255
|
+
const app = createTestHonoApp();
|
|
256
|
+
app.use("*", async (c, next) => {
|
|
257
|
+
c.set("auth", createMockAuth(true));
|
|
258
|
+
c.set("services", {
|
|
259
|
+
siteMembers: createMockSiteMembers(),
|
|
260
|
+
} as AppVariables["services"]);
|
|
261
|
+
await next();
|
|
262
|
+
});
|
|
263
|
+
app.use("*", attachSession());
|
|
264
|
+
app.get("/api/data", requireAuthApi(), () => {
|
|
265
|
+
throw new Error("upstream failed");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// A 401 here would tell the client the login expired and send the author
|
|
269
|
+
// back to sign-in over what is really a server-side failure.
|
|
270
|
+
const res = await app.request("/api/data");
|
|
271
|
+
expect(res.status).toBe(500);
|
|
272
|
+
});
|
|
273
|
+
|
|
206
274
|
it("allows authenticated requests via session", async () => {
|
|
207
275
|
const app = createTestHonoApp();
|
|
208
276
|
app.onError(errorHandler);
|
|
@@ -2,12 +2,18 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import type { Bindings } from "../../types.js";
|
|
4
4
|
import type { AppVariables } from "../../types/app-context.js";
|
|
5
|
-
import { defaultCacheControl } from "../cache-control.js";
|
|
5
|
+
import { defaultCacheControl, noStore } from "../cache-control.js";
|
|
6
6
|
|
|
7
7
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
8
8
|
|
|
9
|
-
function buildApp(): Hono<Env> {
|
|
9
|
+
function buildApp(isAuthenticated = false): Hono<Env> {
|
|
10
10
|
const app = new Hono<Env>();
|
|
11
|
+
// Stands in for `attachSession()`, which runs before this middleware and is
|
|
12
|
+
// what puts `isAuthenticated` on the context.
|
|
13
|
+
app.use("*", async (c, next) => {
|
|
14
|
+
c.set("isAuthenticated", isAuthenticated);
|
|
15
|
+
await next();
|
|
16
|
+
});
|
|
11
17
|
app.use("*", defaultCacheControl());
|
|
12
18
|
|
|
13
19
|
// Un-annotated dynamic page — the common case.
|
|
@@ -27,11 +33,31 @@ function buildApp(): Hono<Env> {
|
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
describe("defaultCacheControl", () => {
|
|
30
|
-
it("
|
|
36
|
+
it("lets an anonymous page stay in the browser's back/forward cache", async () => {
|
|
37
|
+
// `no-store` would disable it, making the back button re-request and
|
|
38
|
+
// re-render a page that is the same for everyone anyway. `private` is
|
|
39
|
+
// what keeps shared caches out, and it is still here.
|
|
31
40
|
const response = await buildApp().request("/");
|
|
41
|
+
expect(response.headers.get("Cache-Control")).toBe("private, no-cache");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("keeps a signed-in page out of every cache", async () => {
|
|
45
|
+
// The back/forward cache restores a snapshot without asking the server,
|
|
46
|
+
// so an authenticated view would survive signing out.
|
|
47
|
+
const response = await buildApp(true).request("/");
|
|
32
48
|
expect(response.headers.get("Cache-Control")).toBe("private, no-store");
|
|
33
49
|
});
|
|
34
50
|
|
|
51
|
+
it("never allows a shared cache to store a dynamic page", async () => {
|
|
52
|
+
for (const authed of [false, true]) {
|
|
53
|
+
const value =
|
|
54
|
+
(await buildApp(authed).request("/")).headers.get("Cache-Control") ??
|
|
55
|
+
"";
|
|
56
|
+
expect(value).toContain("private");
|
|
57
|
+
expect(value).not.toContain("public");
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
35
61
|
it("leaves an explicit public cache policy untouched", async () => {
|
|
36
62
|
const response = await buildApp().request("/feed");
|
|
37
63
|
expect(response.headers.get("Cache-Control")).toBe("public, max-age=180");
|
|
@@ -45,6 +71,48 @@ describe("defaultCacheControl", () => {
|
|
|
45
71
|
it("defaults not-found responses too", async () => {
|
|
46
72
|
const response = await buildApp().request("/missing");
|
|
47
73
|
expect(response.status).toBe(404);
|
|
74
|
+
expect(response.headers.get("Cache-Control")).toBe("private, no-cache");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("noStore", () => {
|
|
79
|
+
function buildCredentialApp(): Hono<Env> {
|
|
80
|
+
const app = new Hono<Env>();
|
|
81
|
+
app.use("*", async (c, next) => {
|
|
82
|
+
c.set("isAuthenticated", false);
|
|
83
|
+
await next();
|
|
84
|
+
});
|
|
85
|
+
app.use("*", defaultCacheControl());
|
|
86
|
+
// Mounted exactly as `app.tsx` does: after the default, before the route.
|
|
87
|
+
app.use("/reset", noStore());
|
|
88
|
+
app.use("/api/auth/*", noStore());
|
|
89
|
+
|
|
90
|
+
app.get("/reset", (c) => c.html("<form/>"));
|
|
91
|
+
// A terminal handler that never calls next(), like better-auth's.
|
|
92
|
+
app.all("/api/auth/*", (c) => c.json({ ok: true }));
|
|
93
|
+
app.get("/", (c) => c.html("<h1>home</h1>"));
|
|
94
|
+
|
|
95
|
+
return app;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
it("keeps an anonymous credential page out of the browser's disk cache", async () => {
|
|
99
|
+
// The reset URL carries a one-time token, so the anonymous `no-cache`
|
|
100
|
+
// default — which lets the browser keep the page — is the wrong answer.
|
|
101
|
+
const response = await buildCredentialApp().request("/reset");
|
|
102
|
+
expect(response.headers.get("Cache-Control")).toBe("private, no-store");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("still reaches a terminal handler that never calls next()", async () => {
|
|
106
|
+
// better-auth's `/api/auth/*` handler returns without yielding, so the
|
|
107
|
+
// middleware only runs if it was registered first.
|
|
108
|
+
const response = await buildCredentialApp().request(
|
|
109
|
+
"/api/auth/get-session",
|
|
110
|
+
);
|
|
48
111
|
expect(response.headers.get("Cache-Control")).toBe("private, no-store");
|
|
49
112
|
});
|
|
113
|
+
|
|
114
|
+
it("leaves every other anonymous page on the default", async () => {
|
|
115
|
+
const response = await buildCredentialApp().request("/");
|
|
116
|
+
expect(response.headers.get("Cache-Control")).toBe("private, no-cache");
|
|
117
|
+
});
|
|
50
118
|
});
|
|
@@ -4,28 +4,42 @@ import { requireOnboarding, resetOnboardingCache } from "../onboarding.js";
|
|
|
4
4
|
import type { Bindings } from "../../types.js";
|
|
5
5
|
import type { AppVariables } from "../../types/app-context.js";
|
|
6
6
|
import { DEFAULT_TEST_SITE_ID } from "../../__tests__/helpers/db.js";
|
|
7
|
+
import {
|
|
8
|
+
ONBOARDING_STATUS,
|
|
9
|
+
type OnboardingStatus,
|
|
10
|
+
} from "../../lib/constants.js";
|
|
7
11
|
|
|
8
12
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
9
13
|
|
|
10
|
-
function createMockServices(
|
|
14
|
+
function createMockServices(status: OnboardingStatus) {
|
|
11
15
|
let callCount = 0;
|
|
12
16
|
const services = {
|
|
13
17
|
settings: {
|
|
14
|
-
|
|
18
|
+
getOnboardingStatus: async () => {
|
|
15
19
|
callCount++;
|
|
16
|
-
return
|
|
20
|
+
return status;
|
|
17
21
|
},
|
|
18
22
|
},
|
|
19
23
|
} as AppVariables["services"];
|
|
20
24
|
return { services, getCallCount: () => callCount };
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
function createApp(
|
|
24
|
-
|
|
27
|
+
function createApp(
|
|
28
|
+
status: OnboardingStatus | boolean,
|
|
29
|
+
options?: { authenticated?: boolean },
|
|
30
|
+
) {
|
|
31
|
+
const resolved: OnboardingStatus =
|
|
32
|
+
typeof status === "boolean"
|
|
33
|
+
? status
|
|
34
|
+
? ONBOARDING_STATUS.COMPLETED
|
|
35
|
+
: ONBOARDING_STATUS.PENDING
|
|
36
|
+
: status;
|
|
37
|
+
const mock = createMockServices(resolved);
|
|
25
38
|
const app = new Hono<Env>();
|
|
26
39
|
|
|
27
40
|
app.use("*", async (c, next) => {
|
|
28
41
|
c.set("services", mock.services);
|
|
42
|
+
c.set("isAuthenticated", options?.authenticated ?? false);
|
|
29
43
|
c.set("currentSite", {
|
|
30
44
|
id: DEFAULT_TEST_SITE_ID,
|
|
31
45
|
key: "default",
|
|
@@ -55,6 +69,7 @@ function createApp(complete: boolean) {
|
|
|
55
69
|
app.get("/robots.txt", (c) => c.text("robots"));
|
|
56
70
|
app.get("/sitemap.xml", (c) => c.text("sitemap"));
|
|
57
71
|
app.get("/media/abc.jpg", (c) => c.text("image"));
|
|
72
|
+
app.get("/compose", (c) => c.text("Compose"));
|
|
58
73
|
|
|
59
74
|
return { app, getCallCount: mock.getCallCount };
|
|
60
75
|
}
|
|
@@ -130,6 +145,62 @@ describe("requireOnboarding", () => {
|
|
|
130
145
|
expect(getCallCount()).toBe(2); // queried again
|
|
131
146
|
});
|
|
132
147
|
|
|
148
|
+
// A control plane already built the site and its owner, so readers must be
|
|
149
|
+
// served normally; only the author still owes setup an answer.
|
|
150
|
+
describe("provisioned sites", () => {
|
|
151
|
+
const provisioned = ONBOARDING_STATUS.PROVISIONED;
|
|
152
|
+
|
|
153
|
+
it("serves the public root to a signed-out visitor", async () => {
|
|
154
|
+
const { app } = createApp(provisioned);
|
|
155
|
+
const res = await app.request("/", { redirect: "manual" });
|
|
156
|
+
expect(res.status).toBe(200);
|
|
157
|
+
expect(await res.text()).toBe("Home");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("serves /signin to a signed-out visitor", async () => {
|
|
161
|
+
const { app } = createApp(provisioned);
|
|
162
|
+
const res = await app.request("/signin", { redirect: "manual" });
|
|
163
|
+
expect(res.status).toBe(200);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("redirects the signed-in author from / to /setup", async () => {
|
|
167
|
+
const { app } = createApp(provisioned, { authenticated: true });
|
|
168
|
+
const res = await app.request("/", { redirect: "manual" });
|
|
169
|
+
expect(res.status).toBe(302);
|
|
170
|
+
expect(res.headers.get("Location")).toBe("/setup");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("redirects the signed-in author from /settings to /setup", async () => {
|
|
174
|
+
const { app } = createApp(provisioned, { authenticated: true });
|
|
175
|
+
const res = await app.request("/settings", { redirect: "manual" });
|
|
176
|
+
expect(res.status).toBe(302);
|
|
177
|
+
expect(res.headers.get("Location")).toBe("/setup");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("redirects the signed-in author from /compose to /setup", async () => {
|
|
181
|
+
const { app } = createApp(provisioned, { authenticated: true });
|
|
182
|
+
const res = await app.request("/compose", { redirect: "manual" });
|
|
183
|
+
expect(res.status).toBe(302);
|
|
184
|
+
expect(res.headers.get("Location")).toBe("/setup");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("leaves an archive page alone even for the author", async () => {
|
|
188
|
+
const { app } = createApp(provisioned, { authenticated: true });
|
|
189
|
+
const res = await app.request("/archive", { redirect: "manual" });
|
|
190
|
+
expect(res.status).toBe(200);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("is not cached as complete", async () => {
|
|
194
|
+
const { app, getCallCount } = createApp(provisioned, {
|
|
195
|
+
authenticated: true,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await app.request("/", { redirect: "manual" });
|
|
199
|
+
await app.request("/settings", { redirect: "manual" });
|
|
200
|
+
expect(getCallCount()).toBe(2);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
133
204
|
describe("non-redirected paths (pass through without DB check)", () => {
|
|
134
205
|
it("allows /setup", async () => {
|
|
135
206
|
const { app, getCallCount } = createApp(false);
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
2
3
|
import { Hono } from "hono";
|
|
3
4
|
import { attachSession } from "../session.js";
|
|
5
|
+
import { createAuth } from "../../auth.js";
|
|
6
|
+
import { createTestDatabase } from "../../__tests__/helpers/db.js";
|
|
7
|
+
import { session as sessionTable } from "../../db/schema.js";
|
|
4
8
|
import type { Bindings } from "../../types.js";
|
|
5
|
-
import type { AppVariables } from "../../types/app-context.js";
|
|
9
|
+
import type { AppSession, AppVariables } from "../../types/app-context.js";
|
|
6
10
|
|
|
7
11
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
8
12
|
|
|
9
|
-
function createAppWithAuth(mockAuth: AppVariables["auth"]): Hono<Env>
|
|
10
|
-
// ensures tests see session/isAuthenticated via c.var
|
|
11
|
-
_lastSession?: AppVariables["session"];
|
|
12
|
-
_lastIsAuthenticated?: boolean;
|
|
13
|
-
} {
|
|
13
|
+
function createAppWithAuth(mockAuth: AppVariables["auth"]): Hono<Env> {
|
|
14
14
|
const app = new Hono<Env>();
|
|
15
15
|
app.use("*", async (c, next) => {
|
|
16
16
|
c.set("auth", mockAuth);
|
|
@@ -20,26 +20,40 @@ function createAppWithAuth(mockAuth: AppVariables["auth"]): Hono<Env> & {
|
|
|
20
20
|
return app;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* `attachSession` calls `getSession` with `returnHeaders: true`, so the mock
|
|
25
|
+
* has to answer in better-call's `{ headers, response }` shape.
|
|
26
|
+
*/
|
|
23
27
|
function buildSessionMock(
|
|
24
|
-
impl: () => Promise<
|
|
28
|
+
impl: () => Promise<{ headers: Headers; response: AppSession }>,
|
|
25
29
|
): AppVariables["auth"] {
|
|
26
30
|
return {
|
|
27
|
-
api: {
|
|
28
|
-
getSession: impl,
|
|
29
|
-
},
|
|
31
|
+
api: { getSession: impl },
|
|
30
32
|
} as unknown as AppVariables["auth"];
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
const VALID_SESSION = {
|
|
36
|
+
user: { id: "user-1", email: "x@y.z", name: "X" },
|
|
37
|
+
session: { id: "sess-1" },
|
|
38
|
+
} as unknown as AppSession;
|
|
39
|
+
|
|
40
|
+
function respondWith(
|
|
41
|
+
response: AppSession,
|
|
42
|
+
setCookies: string[] = [],
|
|
43
|
+
): () => Promise<{ headers: Headers; response: AppSession }> {
|
|
44
|
+
return async () => {
|
|
45
|
+
const headers = new Headers();
|
|
46
|
+
for (const cookie of setCookies) headers.append("Set-Cookie", cookie);
|
|
47
|
+
return { headers, response };
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const REFRESHED_SESSION_COOKIE =
|
|
52
|
+
"__Secure-better-auth.session_token=fresh; Path=/; Max-Age=2592000; HttpOnly; Secure; SameSite=Lax";
|
|
53
|
+
|
|
33
54
|
describe("attachSession", () => {
|
|
34
55
|
it("populates c.var.session and isAuthenticated on a valid session", async () => {
|
|
35
|
-
const
|
|
36
|
-
async () =>
|
|
37
|
-
({
|
|
38
|
-
user: { id: "user-1", email: "x@y.z", name: "X" },
|
|
39
|
-
session: { id: "sess-1" },
|
|
40
|
-
}) as unknown as AppVariables["session"],
|
|
41
|
-
);
|
|
42
|
-
const app = createAppWithAuth(mockAuth);
|
|
56
|
+
const app = createAppWithAuth(buildSessionMock(respondWith(VALID_SESSION)));
|
|
43
57
|
app.get("/", (c) =>
|
|
44
58
|
c.json({
|
|
45
59
|
authed: c.var.isAuthenticated,
|
|
@@ -53,33 +67,167 @@ describe("attachSession", () => {
|
|
|
53
67
|
});
|
|
54
68
|
|
|
55
69
|
it("sets isAuthenticated=false and session=null when no session is present", async () => {
|
|
56
|
-
const
|
|
57
|
-
const app = createAppWithAuth(mockAuth);
|
|
70
|
+
const app = createAppWithAuth(buildSessionMock(respondWith(null)));
|
|
58
71
|
app.get("/", (c) =>
|
|
59
|
-
c.json({
|
|
60
|
-
authed: c.var.isAuthenticated,
|
|
61
|
-
session: c.var.session,
|
|
62
|
-
}),
|
|
72
|
+
c.json({ authed: c.var.isAuthenticated, session: c.var.session }),
|
|
63
73
|
);
|
|
64
74
|
|
|
65
75
|
const res = await app.request("/");
|
|
66
76
|
expect(await res.json()).toEqual({ authed: false, session: null });
|
|
67
77
|
});
|
|
68
78
|
|
|
69
|
-
it("
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
it("retries once so a blipping lookup does not present as a logout", async () => {
|
|
80
|
+
let calls = 0;
|
|
81
|
+
const app = createAppWithAuth(
|
|
82
|
+
buildSessionMock(async () => {
|
|
83
|
+
calls += 1;
|
|
84
|
+
if (calls === 1) throw new Error("transient database error");
|
|
85
|
+
return { headers: new Headers(), response: VALID_SESSION };
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
app.get("/", (c) => c.json({ authed: c.var.isAuthenticated }));
|
|
89
|
+
|
|
90
|
+
const res = await app.request("/");
|
|
91
|
+
expect(calls).toBe(2);
|
|
92
|
+
expect(await res.json()).toEqual({ authed: true });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("swallows a persistent lookup failure and treats the request as unauthenticated", async () => {
|
|
96
|
+
let calls = 0;
|
|
97
|
+
const app = createAppWithAuth(
|
|
98
|
+
buildSessionMock(async () => {
|
|
99
|
+
calls += 1;
|
|
100
|
+
throw new Error("session lookup failed");
|
|
78
101
|
}),
|
|
79
102
|
);
|
|
103
|
+
app.get("/", (c) =>
|
|
104
|
+
c.json({ authed: c.var.isAuthenticated, session: c.var.session }),
|
|
105
|
+
);
|
|
80
106
|
|
|
81
107
|
const res = await app.request("/");
|
|
82
108
|
expect(res.status).toBe(200);
|
|
109
|
+
expect(calls).toBe(2);
|
|
83
110
|
expect(await res.json()).toEqual({ authed: false, session: null });
|
|
84
111
|
});
|
|
112
|
+
|
|
113
|
+
it("writes better-auth's refreshed cookies onto the response", async () => {
|
|
114
|
+
const app = createAppWithAuth(
|
|
115
|
+
buildSessionMock(respondWith(VALID_SESSION, [REFRESHED_SESSION_COOKIE])),
|
|
116
|
+
);
|
|
117
|
+
app.get("/", (c) => c.json({ ok: true }));
|
|
118
|
+
|
|
119
|
+
const res = await app.request("/");
|
|
120
|
+
expect(res.headers.getSetCookie()).toEqual([REFRESHED_SESSION_COOKIE]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("never attaches cookies to a shared-cacheable response", async () => {
|
|
124
|
+
const app = createAppWithAuth(
|
|
125
|
+
buildSessionMock(respondWith(VALID_SESSION, [REFRESHED_SESSION_COOKIE])),
|
|
126
|
+
);
|
|
127
|
+
// Mirrors the media routes, which answer with a year-long public cache
|
|
128
|
+
// directive. A session cookie stored alongside that response is handed to
|
|
129
|
+
// whoever the CDN serves next.
|
|
130
|
+
app.get("/media/x.png", (c) =>
|
|
131
|
+
c.body("bytes", 200, {
|
|
132
|
+
"Cache-Control": "public, max-age=31536000, immutable",
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const res = await app.request("/media/x.png");
|
|
137
|
+
expect(res.headers.getSetCookie()).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("attaches cookies to an explicitly private response", async () => {
|
|
141
|
+
const app = createAppWithAuth(
|
|
142
|
+
buildSessionMock(respondWith(VALID_SESSION, [REFRESHED_SESSION_COOKIE])),
|
|
143
|
+
);
|
|
144
|
+
app.get("/", (c) =>
|
|
145
|
+
c.json({ ok: true }, 200, { "Cache-Control": "private, no-store" }),
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const res = await app.request("/");
|
|
149
|
+
expect(res.headers.getSetCookie()).toEqual([REFRESHED_SESSION_COOKIE]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("lets a route's own cookie win, so sign-out is not undone by the refresh", async () => {
|
|
153
|
+
const cleared =
|
|
154
|
+
"__Secure-better-auth.session_token=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax";
|
|
155
|
+
const app = createAppWithAuth(
|
|
156
|
+
buildSessionMock(respondWith(VALID_SESSION, [REFRESHED_SESSION_COOKIE])),
|
|
157
|
+
);
|
|
158
|
+
app.post("/signout", (c) => c.body(null, 302, { "Set-Cookie": cleared }));
|
|
159
|
+
|
|
160
|
+
const res = await app.request("/signout", { method: "POST" });
|
|
161
|
+
expect(res.headers.getSetCookie()).toEqual([cleared]);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe("attachSession with a real better-auth instance", () => {
|
|
166
|
+
it("re-issues the session cookie so its browser lifetime rolls forward", async () => {
|
|
167
|
+
const { db } = createTestDatabase();
|
|
168
|
+
const auth = createAuth(db, {
|
|
169
|
+
secret: "test-secret-with-enough-entropy-for-session-cookies",
|
|
170
|
+
baseURL: "https://example.com",
|
|
171
|
+
useSecureCookies: true,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const signUp = await auth.api.signUpEmail({
|
|
175
|
+
body: {
|
|
176
|
+
name: "Owner",
|
|
177
|
+
email: "owner@example.com",
|
|
178
|
+
password: "correct-horse-battery",
|
|
179
|
+
},
|
|
180
|
+
returnHeaders: true,
|
|
181
|
+
});
|
|
182
|
+
const signUpCookies = signUp.headers.getSetCookie();
|
|
183
|
+
const sessionCookie = signUpCookies.find((cookie) =>
|
|
184
|
+
cookie.includes(".session_token="),
|
|
185
|
+
);
|
|
186
|
+
expect(sessionCookie).toBeDefined();
|
|
187
|
+
// Send only the session token, not the `session_data` cache cookie: that
|
|
188
|
+
// one is written with `Max-Age=300`, so any request more than five minutes
|
|
189
|
+
// after the last one arrives without it and takes the database path. A
|
|
190
|
+
// cache hit deliberately short-circuits before any refresh.
|
|
191
|
+
const cookieHeader = sessionCookie!.split(";")[0]!;
|
|
192
|
+
|
|
193
|
+
// better-auth only re-issues the cookie once per `updateAge` (1 day). Age
|
|
194
|
+
// the stored session past that threshold so this request is the one that
|
|
195
|
+
// refreshes: with `expiresIn` at 90 days, any expiry inside the next 89
|
|
196
|
+
// days is due.
|
|
197
|
+
const rows = await db.select().from(sessionTable);
|
|
198
|
+
expect(rows).toHaveLength(1);
|
|
199
|
+
const due = new Date(Date.now() + 28 * 24 * 60 * 60 * 1000);
|
|
200
|
+
await db
|
|
201
|
+
.update(sessionTable)
|
|
202
|
+
.set({ expiresAt: due })
|
|
203
|
+
.where(eq(sessionTable.id, rows[0]!.id));
|
|
204
|
+
|
|
205
|
+
const app = new Hono<Env>();
|
|
206
|
+
app.use("*", async (c, next) => {
|
|
207
|
+
c.set("auth", auth);
|
|
208
|
+
await next();
|
|
209
|
+
});
|
|
210
|
+
app.use("*", attachSession());
|
|
211
|
+
app.get("/", (c) => c.json({ authed: c.var.isAuthenticated }));
|
|
212
|
+
|
|
213
|
+
const res = await app.request("/", { headers: { cookie: cookieHeader } });
|
|
214
|
+
expect(await res.json()).toEqual({ authed: true });
|
|
215
|
+
|
|
216
|
+
// The regression this guards: without `returnHeaders` the refreshed cookie
|
|
217
|
+
// is dropped, so the browser copy keeps the expiry it was born with and
|
|
218
|
+
// dies a fixed `expiresIn` after sign-in however active the user is.
|
|
219
|
+
// Asserted against the aged-down 28 days rather than the configured window,
|
|
220
|
+
// so this stays a test of "it rolls forward" and not of the current policy.
|
|
221
|
+
const refreshed = res.headers
|
|
222
|
+
.getSetCookie()
|
|
223
|
+
.find((cookie) => cookie.includes(".session_token="));
|
|
224
|
+
expect(refreshed).toBeDefined();
|
|
225
|
+
expect(refreshed).toMatch(/Max-Age=\d+/);
|
|
226
|
+
const maxAge = Number(/Max-Age=(\d+)/.exec(refreshed!)?.[1]);
|
|
227
|
+
expect(maxAge).toBeGreaterThan(29 * 24 * 60 * 60);
|
|
228
|
+
|
|
229
|
+
// And the database row rolled forward with it.
|
|
230
|
+
const [after] = await db.select().from(sessionTable);
|
|
231
|
+
expect(after!.expiresAt.getTime()).toBeGreaterThan(due.getTime());
|
|
232
|
+
});
|
|
85
233
|
});
|