@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
|
@@ -52,6 +52,13 @@ export interface CloudflareRequestRuntime {
|
|
|
52
52
|
export async function createCloudflareRequestRuntime(
|
|
53
53
|
env: Bindings,
|
|
54
54
|
publicRequestUrl: string,
|
|
55
|
+
options?: {
|
|
56
|
+
/**
|
|
57
|
+
* Anchor this request's first read to the primary instead of any replica.
|
|
58
|
+
* Set for requests that carry a session cookie — see `createRequestRuntime`.
|
|
59
|
+
*/
|
|
60
|
+
anchorReadsToPrimary?: boolean;
|
|
61
|
+
},
|
|
55
62
|
): Promise<CloudflareRequestRuntime> {
|
|
56
63
|
if (!env.DB) {
|
|
57
64
|
throw new Error("Cloudflare runtime requires a DB binding.");
|
|
@@ -62,8 +69,19 @@ export async function createCloudflareRequestRuntime(
|
|
|
62
69
|
throw new Error("AUTH_SECRET should be set after startup validation.");
|
|
63
70
|
}
|
|
64
71
|
|
|
65
|
-
// Use withSession() to enable D1 Read Replication.
|
|
66
|
-
|
|
72
|
+
// Use withSession() to enable D1 Read Replication. The constraint applies to
|
|
73
|
+
// the session's *first* query; every later query is anchored to the bookmark
|
|
74
|
+
// that one returns, so the whole request is sequentially consistent.
|
|
75
|
+
//
|
|
76
|
+
// Bookmarks are not carried across requests, so an unconstrained first read
|
|
77
|
+
// may land on a replica that has not caught up yet. For anonymous traffic
|
|
78
|
+
// that is fine and it is the point of replication. For a request holding a
|
|
79
|
+
// session cookie it is not: the session row is written on the primary, and a
|
|
80
|
+
// lagging replica answers "no such session" — the visitor is bounced to
|
|
81
|
+
// sign-in even though they never signed out.
|
|
82
|
+
const session = env.DB.withSession(
|
|
83
|
+
options?.anchorReadsToPrimary ? "first-primary" : "first-unconstrained",
|
|
84
|
+
);
|
|
67
85
|
|
|
68
86
|
// Note: Drizzle ORM doesn't officially support D1DatabaseSession yet
|
|
69
87
|
// (issue #2226), but it works at runtime.
|
|
@@ -89,6 +107,7 @@ export async function createCloudflareRequestRuntime(
|
|
|
89
107
|
|
|
90
108
|
const servicesConfig = {
|
|
91
109
|
databaseDialect: "sqlite" as const,
|
|
110
|
+
authSecret,
|
|
92
111
|
bootstrapSite: getSingleSiteBootstrapOptions(env),
|
|
93
112
|
enforceHostedMediaQuota: getSiteResolutionMode(env) === "host-based",
|
|
94
113
|
hostedControlPlane: createHostedControlPlaneClient(env),
|
package/src/runtime/index.ts
CHANGED
|
@@ -2,13 +2,39 @@ import { createCloudflareRequestRuntime } from "./cloudflare.js";
|
|
|
2
2
|
import { createNodeRequestRuntime } from "./node.js";
|
|
3
3
|
import type { Bindings } from "../types/bindings.js";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Whether a `Cookie` header carries a better-auth session token.
|
|
7
|
+
*
|
|
8
|
+
* Matched on the `.session_token` suffix so it holds for both the plain and
|
|
9
|
+
* the `__Secure-`-prefixed cookie name. This is a cheap "probably signed in"
|
|
10
|
+
* hint used to pick a database consistency mode — it does not verify the
|
|
11
|
+
* cookie's signature, and it must not be used to make an auth decision.
|
|
12
|
+
*
|
|
13
|
+
* @param cookieHeader - Raw `Cookie` request header, if any
|
|
14
|
+
* @returns `true` when a session token cookie appears to be present
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* hasSessionCookie("__Secure-better-auth.session_token=abc"); // true
|
|
19
|
+
* hasSessionCookie("theme=dark"); // false
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export function hasSessionCookie(
|
|
23
|
+
cookieHeader: string | null | undefined,
|
|
24
|
+
): boolean {
|
|
25
|
+
return !!cookieHeader?.includes(".session_token=");
|
|
26
|
+
}
|
|
27
|
+
|
|
5
28
|
export async function createRequestRuntime(
|
|
6
29
|
env: Bindings,
|
|
7
30
|
publicRequestUrl: string,
|
|
31
|
+
request?: Request,
|
|
8
32
|
) {
|
|
9
33
|
if (env.NODE_DATABASE || env.NODE_SQLITE) {
|
|
10
34
|
return createNodeRequestRuntime(env, publicRequestUrl);
|
|
11
35
|
}
|
|
12
36
|
|
|
13
|
-
return createCloudflareRequestRuntime(env, publicRequestUrl
|
|
37
|
+
return createCloudflareRequestRuntime(env, publicRequestUrl, {
|
|
38
|
+
anchorReadsToPrimary: hasSessionCookie(request?.headers.get("cookie")),
|
|
39
|
+
});
|
|
14
40
|
}
|
package/src/runtime/node.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
getResolvedSiteBaseUrl,
|
|
28
28
|
resolveCliSite,
|
|
29
29
|
resolveRequestSite,
|
|
30
|
+
type CliSiteSelector,
|
|
30
31
|
} from "./site.js";
|
|
31
32
|
|
|
32
33
|
export interface NodeRequestRuntime {
|
|
@@ -144,6 +145,7 @@ export async function createNodeRequestRuntime(
|
|
|
144
145
|
|
|
145
146
|
const servicesConfig = {
|
|
146
147
|
databaseDialect,
|
|
148
|
+
authSecret,
|
|
147
149
|
bootstrapSite: getSingleSiteBootstrapOptions(env),
|
|
148
150
|
enforceHostedMediaQuota: getSiteResolutionMode(env) === "host-based",
|
|
149
151
|
hostedControlPlane: createHostedControlPlaneClient(env),
|
|
@@ -175,9 +177,24 @@ export async function createNodeRequestRuntime(
|
|
|
175
177
|
* Builds the runtime objects needed by local CLI commands.
|
|
176
178
|
*
|
|
177
179
|
* Unlike the request runtime, this path does not require auth configuration.
|
|
180
|
+
*
|
|
181
|
+
* @param env - Bindings with a resolved `NODE_DATABASE` or `NODE_SQLITE`
|
|
182
|
+
* @param selector - The site the command acts on. Required in host-based
|
|
183
|
+
* mode; single-site mode falls back to the instance's one site.
|
|
184
|
+
* @returns Services, storage and database scoped to that site
|
|
185
|
+
* @throws {Error} When host-based mode has no selector, or the selector
|
|
186
|
+
* matches no site
|
|
187
|
+
* @example
|
|
188
|
+
* ```ts
|
|
189
|
+
* const runtime = await createNodeCliRuntime(bindings, {
|
|
190
|
+
* kind: "site",
|
|
191
|
+
* idOrKey: "demo",
|
|
192
|
+
* });
|
|
193
|
+
* ```
|
|
178
194
|
*/
|
|
179
195
|
export async function createNodeCliRuntime(
|
|
180
196
|
env: Bindings,
|
|
197
|
+
selector: CliSiteSelector | null = null,
|
|
181
198
|
): Promise<NodeCliRuntime> {
|
|
182
199
|
const nodeDatabase = env.NODE_DATABASE;
|
|
183
200
|
const sqlite = env.NODE_SQLITE;
|
|
@@ -194,7 +211,7 @@ export async function createNodeCliRuntime(
|
|
|
194
211
|
|
|
195
212
|
const slugIdLength =
|
|
196
213
|
parseInt(getEnvString(env, "SLUG_ID_LENGTH") ?? "5", 10) || 5;
|
|
197
|
-
const siteLookup = await resolveCliSite(db, env, databaseSchema);
|
|
214
|
+
const siteLookup = await resolveCliSite(db, env, selector, databaseSchema);
|
|
198
215
|
|
|
199
216
|
return {
|
|
200
217
|
currentSite: siteLookup.site,
|
package/src/runtime/site.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
type SiteLookupResult,
|
|
18
18
|
TRANSIENT_SINGLE_SITE_ID,
|
|
19
19
|
} from "../services/site.js";
|
|
20
|
-
import { NotFoundError } from "../lib/errors.js";
|
|
20
|
+
import { NotFoundError, SiteUnavailableError } from "../lib/errors.js";
|
|
21
21
|
import type { Bindings } from "../types/bindings.js";
|
|
22
22
|
|
|
23
23
|
function logHostedSiteResolutionFailure(input: {
|
|
@@ -115,45 +115,116 @@ export async function resolveRequestSite(
|
|
|
115
115
|
siteKey: resolved.site.key,
|
|
116
116
|
siteStatus: resolved.site.status,
|
|
117
117
|
});
|
|
118
|
-
|
|
118
|
+
// The host resolved, so the site is real — it is just suspended (hosted
|
|
119
|
+
// plan ended, or an operator took it down). Serving a 404 here told the
|
|
120
|
+
// visitor the address was wrong and told crawlers to drop the site, both
|
|
121
|
+
// of which are false while it can still be restored.
|
|
122
|
+
throw new SiteUnavailableError();
|
|
119
123
|
}
|
|
120
124
|
return resolved;
|
|
121
125
|
}
|
|
122
126
|
|
|
123
|
-
|
|
127
|
+
/**
|
|
128
|
+
* The site a CLI command was told to act on. The `jant` CLI builds it from
|
|
129
|
+
* `--site <key|id>`, `--host <host>` with `--path-prefix`, or `--url <url>`
|
|
130
|
+
* (see `parseCliSiteSelector` in `bin/lib/site-selection.js`).
|
|
131
|
+
*/
|
|
132
|
+
export type CliSiteSelector =
|
|
133
|
+
| { kind: "site"; idOrKey: string }
|
|
134
|
+
| {
|
|
135
|
+
kind: "host";
|
|
136
|
+
host: string;
|
|
137
|
+
/** The domain's stored prefix (`/blog`), or null for a root domain. */
|
|
138
|
+
pathPrefix: string | null;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Printed when a host-based CLI command names no site. Kept word for word with
|
|
143
|
+
* the raw-SQL resolver the D1 path uses in `bin/lib/site-selection.js`.
|
|
144
|
+
*/
|
|
145
|
+
const HOST_BASED_CLI_SITE_REQUIRED_MESSAGE =
|
|
146
|
+
"host-based mode needs a target site. Pass --site <key|id>, --host <host>, or --url <url>.";
|
|
147
|
+
|
|
148
|
+
async function resolveSelectedCliSite(
|
|
124
149
|
db: Database,
|
|
125
|
-
|
|
126
|
-
databaseSchema: DatabaseSchema
|
|
150
|
+
selector: CliSiteSelector,
|
|
151
|
+
databaseSchema: DatabaseSchema,
|
|
127
152
|
): Promise<SiteLookupResult> {
|
|
128
153
|
const siteService = createSiteService(db, databaseSchema);
|
|
129
|
-
const resolutionMode = getSiteResolutionMode(env);
|
|
130
154
|
|
|
131
|
-
if (
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
if (resolved.site.id === TRANSIENT_SINGLE_SITE_ID) {
|
|
138
|
-
throw new Error(
|
|
139
|
-
"No site is configured for this instance yet. Finish /setup before running this command.",
|
|
140
|
-
);
|
|
155
|
+
if (selector.kind === "site") {
|
|
156
|
+
const site = await siteService.getByIdOrKey(selector.idOrKey);
|
|
157
|
+
if (!site) {
|
|
158
|
+
throw new Error(`No site found for --site ${selector.idOrKey}.`);
|
|
141
159
|
}
|
|
160
|
+
return {
|
|
161
|
+
site,
|
|
162
|
+
domain: await siteService.getPrimaryDomainForSite(site.id),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
142
165
|
|
|
143
|
-
|
|
166
|
+
const resolved = await siteService.resolveByHost(
|
|
167
|
+
selector.host,
|
|
168
|
+
selector.pathPrefix,
|
|
169
|
+
);
|
|
170
|
+
if (!resolved) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`No site found for host "${selector.host}"${selector.pathPrefix ? ` and path prefix "${selector.pathPrefix}"` : ""}.`,
|
|
173
|
+
);
|
|
144
174
|
}
|
|
175
|
+
return resolved;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Resolve the site a CLI command acts on.
|
|
180
|
+
*
|
|
181
|
+
* A selector wins in either mode. Without one, single-site mode uses the
|
|
182
|
+
* instance's one site, and host-based mode fails before touching the
|
|
183
|
+
* database: a hosted database holds every tenant, so there is no default to
|
|
184
|
+
* fall back to — not even when it happens to hold one site today.
|
|
185
|
+
*
|
|
186
|
+
* @param db - The database
|
|
187
|
+
* @param env - Bindings that carry `SITE_RESOLUTION_MODE` and the single-site
|
|
188
|
+
* origin
|
|
189
|
+
* @param selector - The site the command was told to act on, if any
|
|
190
|
+
* @param databaseSchema - The schema bundle for the database's dialect
|
|
191
|
+
* @returns The site and its domain
|
|
192
|
+
* @throws {Error} When the selector matches no site, when host-based mode has
|
|
193
|
+
* no selector, or when single-site mode has not finished setup
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* await resolveCliSite(db, env, { kind: "site", idOrKey: "demo" });
|
|
197
|
+
* ```
|
|
198
|
+
*/
|
|
199
|
+
export async function resolveCliSite(
|
|
200
|
+
db: Database,
|
|
201
|
+
env: Bindings,
|
|
202
|
+
selector: CliSiteSelector | null = null,
|
|
203
|
+
databaseSchema: DatabaseSchema = sqliteSchemaBundle,
|
|
204
|
+
): Promise<SiteLookupResult> {
|
|
205
|
+
if (selector) {
|
|
206
|
+
return resolveSelectedCliSite(db, selector, databaseSchema);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (getSiteResolutionMode(env) === "host-based") {
|
|
210
|
+
throw new Error(HOST_BASED_CLI_SITE_REQUIRED_MESSAGE);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const resolved = await createSiteService(
|
|
214
|
+
db,
|
|
215
|
+
databaseSchema,
|
|
216
|
+
).resolveSingleSite({
|
|
217
|
+
...getSingleSiteBootstrapOptions(env),
|
|
218
|
+
createIfMissing: false,
|
|
219
|
+
});
|
|
145
220
|
|
|
146
|
-
|
|
147
|
-
if (!onlySite) {
|
|
221
|
+
if (resolved.site.id === TRANSIENT_SINGLE_SITE_ID) {
|
|
148
222
|
throw new Error(
|
|
149
|
-
"
|
|
223
|
+
"No site is configured for this instance yet. Finish /setup before running this command.",
|
|
150
224
|
);
|
|
151
225
|
}
|
|
152
226
|
|
|
153
|
-
return
|
|
154
|
-
site: onlySite,
|
|
155
|
-
domain: null,
|
|
156
|
-
};
|
|
227
|
+
return resolved;
|
|
157
228
|
}
|
|
158
229
|
|
|
159
230
|
export function getResolvedSiteBaseUrl(
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
siteDomains,
|
|
19
19
|
siteMembers,
|
|
20
20
|
sites,
|
|
21
|
+
smartCollections,
|
|
21
22
|
user,
|
|
22
23
|
verification,
|
|
23
24
|
} from "../../db/schema.js";
|
|
@@ -172,6 +173,56 @@ describe("AuthService", () => {
|
|
|
172
173
|
createdAt: timestamp,
|
|
173
174
|
});
|
|
174
175
|
|
|
176
|
+
// A smart collection that filters *by* the collection above, with every row
|
|
177
|
+
// that hangs off it. This is the shape a factory reset used to die on: the
|
|
178
|
+
// condition column once carried `ON DELETE restrict`, so clearing
|
|
179
|
+
// `collection` threw halfway through a reset that runs without a
|
|
180
|
+
// transaction on SQLite and D1, leaving a half-erased site behind.
|
|
181
|
+
await db.insert(smartCollections).values({
|
|
182
|
+
id: "smc_01km9authdelete00000000000",
|
|
183
|
+
siteId: DEFAULT_TEST_SITE_ID,
|
|
184
|
+
title: "Untitled notes",
|
|
185
|
+
collectionId: "col_01km9authdelete00000000000",
|
|
186
|
+
format: "note",
|
|
187
|
+
hasTitle: false,
|
|
188
|
+
sort: "newest",
|
|
189
|
+
createdAt: timestamp,
|
|
190
|
+
updatedAt: timestamp,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await db.insert(pathRegistry).values({
|
|
194
|
+
id: "pth_01km9authsmart0000000000000",
|
|
195
|
+
siteId: DEFAULT_TEST_SITE_ID,
|
|
196
|
+
path: "untitled-notes",
|
|
197
|
+
kind: "slug",
|
|
198
|
+
smartCollectionId: "smc_01km9authdelete00000000000",
|
|
199
|
+
createdAt: timestamp,
|
|
200
|
+
updatedAt: timestamp,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await db.insert(collectionDirectoryItems).values({
|
|
204
|
+
id: "cdi_01km9authsmart0000000000000",
|
|
205
|
+
siteId: DEFAULT_TEST_SITE_ID,
|
|
206
|
+
type: "smart_collection",
|
|
207
|
+
smartCollectionId: "smc_01km9authdelete00000000000",
|
|
208
|
+
label: null,
|
|
209
|
+
position: "a1",
|
|
210
|
+
createdAt: timestamp,
|
|
211
|
+
updatedAt: timestamp,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
await db.insert(navItems).values({
|
|
215
|
+
id: "nav_01km9authsmart0000000000000",
|
|
216
|
+
siteId: DEFAULT_TEST_SITE_ID,
|
|
217
|
+
type: "smart_collection",
|
|
218
|
+
smartCollectionId: "smc_01km9authdelete00000000000",
|
|
219
|
+
label: "",
|
|
220
|
+
url: "/untitled-notes",
|
|
221
|
+
position: "a1",
|
|
222
|
+
createdAt: timestamp,
|
|
223
|
+
updatedAt: timestamp,
|
|
224
|
+
});
|
|
225
|
+
|
|
175
226
|
await db.insert(navItems).values({
|
|
176
227
|
id: "nav_01km9authdelete00000000000",
|
|
177
228
|
siteId: DEFAULT_TEST_SITE_ID,
|
|
@@ -241,6 +292,9 @@ describe("AuthService", () => {
|
|
|
241
292
|
const remainingPosts = await db.select().from(posts);
|
|
242
293
|
const remainingMedia = await db.select().from(media);
|
|
243
294
|
const remainingCollections = await db.select().from(collections);
|
|
295
|
+
const remainingSmartCollections = await db
|
|
296
|
+
.select()
|
|
297
|
+
.from(smartCollections);
|
|
244
298
|
const remainingPaths = await db.select().from(pathRegistry);
|
|
245
299
|
const remainingDirectoryItems = await db
|
|
246
300
|
.select()
|
|
@@ -262,6 +316,7 @@ describe("AuthService", () => {
|
|
|
262
316
|
expect(remainingPosts).toHaveLength(0);
|
|
263
317
|
expect(remainingMedia).toHaveLength(0);
|
|
264
318
|
expect(remainingCollections).toHaveLength(0);
|
|
319
|
+
expect(remainingSmartCollections).toHaveLength(0);
|
|
265
320
|
expect(remainingPaths).toHaveLength(0);
|
|
266
321
|
expect(remainingDirectoryItems).toHaveLength(0);
|
|
267
322
|
expect(remainingThreadCollections).toHaveLength(0);
|
|
@@ -287,12 +342,16 @@ describe("AuthService", () => {
|
|
|
287
342
|
const remainingSiteMembers = await db.select().from(siteMembers);
|
|
288
343
|
const remainingUsers = await db.select().from(user);
|
|
289
344
|
const remainingPosts = await db.select().from(posts);
|
|
345
|
+
const remainingSmartCollections = await db
|
|
346
|
+
.select()
|
|
347
|
+
.from(smartCollections);
|
|
290
348
|
const remainingSettings = await db.select().from(settingsTable);
|
|
291
349
|
const existingSites = await db.select().from(sites);
|
|
292
350
|
const existingSiteDomains = await db.select().from(siteDomains);
|
|
293
351
|
expect(remainingSiteMembers).toHaveLength(0);
|
|
294
352
|
expect(remainingUsers).toHaveLength(0);
|
|
295
353
|
expect(remainingPosts).toHaveLength(0);
|
|
354
|
+
expect(remainingSmartCollections).toHaveLength(0);
|
|
296
355
|
expect(remainingSettings).toHaveLength(0);
|
|
297
356
|
expect(existingSites).toHaveLength(0);
|
|
298
357
|
expect(existingSiteDomains).toHaveLength(0);
|
|
@@ -305,22 +364,72 @@ describe("AuthService", () => {
|
|
|
305
364
|
db = testDb.db as unknown as Database;
|
|
306
365
|
});
|
|
307
366
|
|
|
308
|
-
|
|
367
|
+
function createTokenService(authSecret = "test-auth-secret") {
|
|
309
368
|
const settingsService = createSettingsService(db, DEFAULT_TEST_SITE_ID);
|
|
310
|
-
|
|
369
|
+
return createAuthService(db, settingsService, {
|
|
311
370
|
databaseDialect: "pg",
|
|
371
|
+
authSecret,
|
|
312
372
|
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
it("validates delete CSRF tokens in Node runtimes", async () => {
|
|
376
|
+
const authService = createTokenService();
|
|
313
377
|
|
|
314
|
-
const token = await authService.generateDeleteCsrfToken();
|
|
378
|
+
const token = await authService.generateDeleteCsrfToken("ses_1");
|
|
315
379
|
|
|
316
|
-
await expect(authService.validateDeleteCsrfToken(token)).resolves.toBe(
|
|
317
|
-
true,
|
|
318
|
-
);
|
|
319
380
|
await expect(
|
|
320
|
-
authService.validateDeleteCsrfToken(
|
|
381
|
+
authService.validateDeleteCsrfToken(token, "ses_1"),
|
|
382
|
+
).resolves.toBe(true);
|
|
383
|
+
await expect(
|
|
384
|
+
authService.validateDeleteCsrfToken(`${token}nope`, "ses_1"),
|
|
321
385
|
).resolves.toBe(false);
|
|
322
386
|
});
|
|
323
387
|
|
|
388
|
+
it("issues the same delete token however often a session asks", async () => {
|
|
389
|
+
// Rendering the delete page is a GET. Issuing twice must not invalidate
|
|
390
|
+
// the copy already sitting in an open tab — that turned any prefetch or
|
|
391
|
+
// browser extension re-fetching the page into a dead delete button.
|
|
392
|
+
const authService = createTokenService();
|
|
393
|
+
|
|
394
|
+
const first = await authService.generateDeleteCsrfToken("ses_1");
|
|
395
|
+
const second = await authService.generateDeleteCsrfToken("ses_1");
|
|
396
|
+
|
|
397
|
+
expect(second).toBe(first);
|
|
398
|
+
await expect(
|
|
399
|
+
authService.validateDeleteCsrfToken(first, "ses_1"),
|
|
400
|
+
).resolves.toBe(true);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("refuses a delete token minted for another session", async () => {
|
|
404
|
+
const authService = createTokenService();
|
|
405
|
+
|
|
406
|
+
const token = await authService.generateDeleteCsrfToken("ses_1");
|
|
407
|
+
|
|
408
|
+
await expect(
|
|
409
|
+
authService.validateDeleteCsrfToken(token, "ses_2"),
|
|
410
|
+
).resolves.toBe(false);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("refuses a delete token minted under another signing key", async () => {
|
|
414
|
+
const token =
|
|
415
|
+
await createTokenService("secret-a").generateDeleteCsrfToken("ses_1");
|
|
416
|
+
|
|
417
|
+
await expect(
|
|
418
|
+
createTokenService("secret-b").validateDeleteCsrfToken(token, "ses_1"),
|
|
419
|
+
).resolves.toBe(false);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it("refuses to mint a delete token without a signing key", async () => {
|
|
423
|
+
const settingsService = createSettingsService(db, DEFAULT_TEST_SITE_ID);
|
|
424
|
+
const authService = createAuthService(db, settingsService, {
|
|
425
|
+
databaseDialect: "pg",
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
await expect(
|
|
429
|
+
authService.generateDeleteCsrfToken("ses_1"),
|
|
430
|
+
).rejects.toThrow(/AUTH_SECRET/);
|
|
431
|
+
});
|
|
432
|
+
|
|
324
433
|
it("validates password reset tokens in Node runtimes", async () => {
|
|
325
434
|
const settingsService = createSettingsService(db, DEFAULT_TEST_SITE_ID);
|
|
326
435
|
const authService = createAuthService(db, settingsService, {
|
|
@@ -267,6 +267,102 @@ describe("CollectionService", () => {
|
|
|
267
267
|
});
|
|
268
268
|
|
|
269
269
|
describe("listDirectoryData", () => {
|
|
270
|
+
it("counts only what this reader can see", async () => {
|
|
271
|
+
const reading = await collectionService.create({
|
|
272
|
+
slug: "reading",
|
|
273
|
+
title: "Reading",
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const published = await postService.create({
|
|
277
|
+
format: "note",
|
|
278
|
+
bodyMarkdown: "Published",
|
|
279
|
+
});
|
|
280
|
+
const draft = await postService.create({
|
|
281
|
+
format: "note",
|
|
282
|
+
bodyMarkdown: "Draft",
|
|
283
|
+
status: "draft",
|
|
284
|
+
});
|
|
285
|
+
const secret = await postService.create({
|
|
286
|
+
format: "note",
|
|
287
|
+
bodyMarkdown: "Private",
|
|
288
|
+
visibility: "private",
|
|
289
|
+
});
|
|
290
|
+
for (const post of [published, draft, secret]) {
|
|
291
|
+
await collectionService.addThread(reading.id, post.id);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// A stranger who could subtract the directory's number from the
|
|
295
|
+
// collection page's would learn how much unpublished work is in here.
|
|
296
|
+
const anonymous = await collectionService.listDirectoryData({
|
|
297
|
+
isAuthenticated: false,
|
|
298
|
+
});
|
|
299
|
+
expect(anonymous.collections[0]?.threadCount).toBe(1);
|
|
300
|
+
expect(anonymous.collections[0]?.recentActivityAt).toBe(
|
|
301
|
+
published.lastActivityAt,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const author = await collectionService.listDirectoryData({
|
|
305
|
+
isAuthenticated: true,
|
|
306
|
+
});
|
|
307
|
+
expect(author.collections[0]?.threadCount).toBe(2);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("counts a Thread once, whichever of its posts matched", async () => {
|
|
311
|
+
const reading = await collectionService.create({
|
|
312
|
+
slug: "reading",
|
|
313
|
+
title: "Reading",
|
|
314
|
+
});
|
|
315
|
+
const root = await postService.create({
|
|
316
|
+
format: "note",
|
|
317
|
+
bodyMarkdown: "Root",
|
|
318
|
+
});
|
|
319
|
+
await postService.create({
|
|
320
|
+
format: "note",
|
|
321
|
+
bodyMarkdown: "Reply",
|
|
322
|
+
replyToId: root.id,
|
|
323
|
+
});
|
|
324
|
+
await collectionService.addThread(reading.id, root.id);
|
|
325
|
+
|
|
326
|
+
const directory = await collectionService.listDirectoryData({
|
|
327
|
+
isAuthenticated: false,
|
|
328
|
+
});
|
|
329
|
+
expect(directory.collections[0]?.threadCount).toBe(1);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it("narrows counts to the view's language", async () => {
|
|
333
|
+
const reading = await collectionService.create({
|
|
334
|
+
slug: "reading",
|
|
335
|
+
title: "Reading",
|
|
336
|
+
});
|
|
337
|
+
const english = await postService.create({
|
|
338
|
+
format: "note",
|
|
339
|
+
bodyMarkdown: "English",
|
|
340
|
+
language: "en",
|
|
341
|
+
});
|
|
342
|
+
const japanese = await postService.create({
|
|
343
|
+
format: "note",
|
|
344
|
+
bodyMarkdown: "Japanese",
|
|
345
|
+
language: "ja",
|
|
346
|
+
});
|
|
347
|
+
for (const post of [english, japanese]) {
|
|
348
|
+
await collectionService.addThread(reading.id, post.id);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const englishView = await collectionService.listDirectoryData({
|
|
352
|
+
isAuthenticated: false,
|
|
353
|
+
lang: "en",
|
|
354
|
+
});
|
|
355
|
+
expect(englishView.collections[0]?.threadCount).toBe(1);
|
|
356
|
+
|
|
357
|
+
// The row itself stays listed in every view — only its number narrows.
|
|
358
|
+
const japaneseView = await collectionService.listDirectoryData({
|
|
359
|
+
isAuthenticated: false,
|
|
360
|
+
lang: "ja",
|
|
361
|
+
});
|
|
362
|
+
expect(japaneseView.collections).toHaveLength(1);
|
|
363
|
+
expect(japaneseView.collections[0]?.threadCount).toBe(1);
|
|
364
|
+
});
|
|
365
|
+
|
|
270
366
|
it("returns collections with recent activity and labeled dividers", async () => {
|
|
271
367
|
const reading = await collectionService.create({
|
|
272
368
|
slug: "reading",
|
|
@@ -283,7 +379,9 @@ describe("CollectionService", () => {
|
|
|
283
379
|
});
|
|
284
380
|
await collectionService.addThread(reading.id, post.id);
|
|
285
381
|
|
|
286
|
-
const directory = await collectionService.listDirectoryData(
|
|
382
|
+
const directory = await collectionService.listDirectoryData({
|
|
383
|
+
isAuthenticated: false,
|
|
384
|
+
});
|
|
287
385
|
|
|
288
386
|
expect(directory.collections).toHaveLength(1);
|
|
289
387
|
expect(directory.collections[0]?.recentActivityAt).toBe(
|
|
@@ -329,7 +427,9 @@ describe("CollectionService", () => {
|
|
|
329
427
|
vi.setSystemTime(new Date("2024-01-01T00:01:00Z"));
|
|
330
428
|
await postService.update(root.id, { bodyMarkdown: "after" });
|
|
331
429
|
|
|
332
|
-
const directory = await collectionService.listDirectoryData(
|
|
430
|
+
const directory = await collectionService.listDirectoryData({
|
|
431
|
+
isAuthenticated: false,
|
|
432
|
+
});
|
|
333
433
|
expect(directory.collections[0]?.recentActivityAt).toBe(1704067200);
|
|
334
434
|
} finally {
|
|
335
435
|
vi.useRealTimers();
|
|
@@ -357,7 +457,9 @@ describe("CollectionService", () => {
|
|
|
357
457
|
replyToId: root.id,
|
|
358
458
|
});
|
|
359
459
|
|
|
360
|
-
const directory = await collectionService.listDirectoryData(
|
|
460
|
+
const directory = await collectionService.listDirectoryData({
|
|
461
|
+
isAuthenticated: false,
|
|
462
|
+
});
|
|
361
463
|
expect(directory.collections[0]?.recentActivityAt).toBe(1704067260);
|
|
362
464
|
} finally {
|
|
363
465
|
vi.useRealTimers();
|
|
@@ -371,7 +473,9 @@ describe("CollectionService", () => {
|
|
|
371
473
|
url: "/archive?format=quote",
|
|
372
474
|
});
|
|
373
475
|
|
|
374
|
-
const directory = await collectionService.listDirectoryData(
|
|
476
|
+
const directory = await collectionService.listDirectoryData({
|
|
477
|
+
isAuthenticated: false,
|
|
478
|
+
});
|
|
375
479
|
|
|
376
480
|
expect(directory.directoryItems).toContainEqual(
|
|
377
481
|
expect.objectContaining({
|