@jant/core 0.6.16 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/export.js +3 -1
- package/bin/commands/import-site.js +749 -220
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/setup.js +136 -0
- package/bin/commands/site/export.js +71 -34
- package/bin/commands/site/pull-media.js +2 -6
- package/bin/commands/site/snapshot/export.js +19 -61
- package/bin/commands/site/snapshot/import.js +23 -24
- package/bin/lib/d1-query.js +92 -18
- package/bin/lib/hugo-markdown.js +4 -0
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-pull-media.js +21 -28
- package/bin/lib/site-selection.js +94 -29
- package/bin/lib/site-snapshot.js +338 -3
- package/bin/lib/sql-export.js +68 -5
- package/bin/lib/wrangler-cli.js +37 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/bin/lib/zip-archive.js +187 -0
- package/dist/{app-C7_4oab9.js → app-ZcaI1kPN.js} +53190 -13529
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-B-m7X7S5.js +261 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-BJyd-LXE.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-BkscJx69.js +2 -0
- package/dist/client/_assets/client-DAhoqPdr.css +2 -0
- package/dist/client/_assets/client-auth-CAMfTrKW.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-D6rExKVK.js +2004 -0
- package/dist/client/_assets/client-knSEyUJO.js +295 -0
- package/dist/client/_assets/client-manage-sVOkqVbH.js +2170 -0
- package/dist/client/_assets/client-settings-CGR_nZ78.js +1112 -0
- package/dist/github-sync-Gw4orAHk.js +8398 -0
- package/dist/index.js +2 -5
- package/dist/node.js +11 -9
- package/package.json +15 -5
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/dev-scripts.test.ts +203 -0
- package/src/__tests__/export-collection-order.test.ts +276 -0
- package/src/__tests__/export-feed-ids.test.ts +184 -0
- package/src/__tests__/export-feed-order.test.ts +294 -0
- package/src/__tests__/export-hugo-build.test.ts +390 -0
- package/src/__tests__/export-import-roundtrip.test.ts +94 -0
- package/src/__tests__/export-service.test.ts +842 -28
- package/src/__tests__/export-smart-collection.test.ts +329 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/helpers/hugo-site.ts +146 -0
- package/src/__tests__/import-site-command.test.ts +487 -1
- package/src/__tests__/mise-config.test.ts +75 -4
- package/src/__tests__/node-dev-tasks.test.ts +264 -0
- package/src/__tests__/site-export-canonical-import.test.ts +149 -0
- package/src/__tests__/snapshot-canonical-replay.test.ts +179 -0
- package/src/__tests__/snapshot-settings.test.ts +97 -0
- package/src/__tests__/snapshot-tables.test.ts +219 -0
- package/src/__tests__/sql-export.test.ts +173 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/__tests__/zip-archive.test.ts +106 -0
- package/src/app.tsx +86 -22
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +561 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker-types.ts +6 -1
- package/src/client/components/jant-repo-picker.ts +137 -108
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +622 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/__tests__/list-editing.test.ts +224 -86
- package/src/client/tiptap/__tests__/mark-exit.test.ts +1 -2
- package/src/client/tiptap/__tests__/markdown-clipboard.test.ts +26 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/extensions.ts +0 -3
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/tiptap/structural-keymap.ts +158 -47
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/d1-query.test.ts +56 -1
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +555 -73
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +601 -119
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +614 -132
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/github-sync-repo-name.test.ts +40 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/image.test.ts +27 -1
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +105 -0
- package/src/lib/__tests__/markdown.test.ts +10 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +131 -3
- package/src/lib/__tests__/schemas.test.ts +69 -5
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/timeline.test.ts +87 -0
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +172 -4
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +439 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-repo-name.ts +45 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +62 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/image.ts +17 -4
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/markdown-manager.ts +392 -2
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-body-html.ts +11 -4
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +111 -6
- package/src/lib/schemas.ts +251 -15
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +43 -2
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/tiptap-to-markdown.ts +13 -7
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +159 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +53 -25
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-setup.test.ts +163 -0
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/cli-site-snapshot.test.ts +67 -0
- package/src/node/__tests__/cli-snapshot-meta.test.ts +20 -0
- package/src/node/__tests__/runtime.test.ts +75 -0
- package/src/node/index.ts +3 -0
- package/src/node/request-handler.ts +3 -1
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/posts.test.ts +24 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/__tests__/upload.test.ts +34 -0
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +6 -3
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/internal/sites.ts +0 -1
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +161 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +44 -7
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/upload.ts +10 -3
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +9 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/github-sync-app.test.ts +365 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +714 -111
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-page-round-trips.test.ts +211 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/__tests__/thread-order.test.ts +179 -0
- package/src/routes/pages/archive.tsx +487 -379
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +43 -18
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +228 -45
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +22 -5
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/__tests__/readiness.test.ts +23 -0
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +67 -1
- package/src/runtime/readiness.ts +15 -0
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/bootstrap-setup-instance.test.ts +230 -0
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/custom-url.test.ts +34 -0
- package/src/services/__tests__/github-app-installations.test.ts +153 -0
- package/src/services/__tests__/github-sync-push.test.ts +46 -0
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/media.test.ts +31 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +236 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post-timeline.test.ts +127 -0
- package/src/services/__tests__/post.test.ts +120 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +408 -39
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +29 -24
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/alias.html +27 -1
- package/src/services/export-theme/layouts/_default/list.html +2 -63
- package/src/services/export-theme/layouts/_default/rss.xml +103 -38
- package/src/services/export-theme/layouts/collections/list.html +3 -3
- package/src/services/export-theme/layouts/featured/list.html +1 -4
- package/src/services/export-theme/layouts/index.html +40 -26
- package/src/services/export-theme/layouts/partials/collection-members.html +100 -0
- package/src/services/export-theme/layouts/partials/collection-threads.html +33 -0
- package/src/services/export-theme/layouts/partials/featured-members.html +35 -0
- package/src/services/export-theme/layouts/partials/featured-thread.html +1 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/footer.html +1 -1
- package/src/services/export-theme/layouts/partials/head.html +1 -1
- package/src/services/export-theme/layouts/partials/header.html +5 -3
- package/src/services/export-theme/layouts/partials/jant-data.html +23 -0
- package/src/services/export-theme/layouts/partials/latest-members.html +48 -0
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/smart-collection-members.html +129 -0
- package/src/services/export-theme/layouts/partials/thread-preview.html +41 -31
- package/src/services/export-theme/layouts/post/list.html +1 -1
- package/src/services/export-theme/layouts/smart_collection/list.html +24 -0
- package/src/services/export-theme/styles/main.css +13 -2
- package/src/services/export-theme/theme.toml +1 -1
- package/src/services/export.ts +880 -72
- package/src/services/github-app-installations.ts +171 -4
- package/src/services/github-sync.ts +69 -15
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +22 -2
- package/src/services/media.ts +22 -7
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +254 -6
- package/src/services/post.ts +1234 -190
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +27 -14
- package/src/services/site.ts +160 -32
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1090 -7216
- package/src/types/app-context.ts +23 -0
- package/src/types/bindings.ts +8 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +74 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +277 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/export-Dl5KCiEs.js +0 -5562
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/client/tiptap/exitable-marks.ts +0 -73
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
|
@@ -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, {
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { createTestDatabase } from "../../__tests__/helpers/db.js";
|
|
4
|
+
import type { Database } from "../../db/index.js";
|
|
5
|
+
import {
|
|
6
|
+
account,
|
|
7
|
+
navItems,
|
|
8
|
+
settings,
|
|
9
|
+
siteDomains,
|
|
10
|
+
siteMembers,
|
|
11
|
+
sites,
|
|
12
|
+
user,
|
|
13
|
+
} from "../../db/schema.js";
|
|
14
|
+
import { verifyPassword } from "../../lib/password.js";
|
|
15
|
+
import { createBootstrapService } from "../bootstrap.js";
|
|
16
|
+
import { createSiteService, TRANSIENT_SINGLE_SITE_ID } from "../site.js";
|
|
17
|
+
|
|
18
|
+
const SNAPSHOT_SITE_ID = "sit_01kn8jq3t4famtyg9hjd074ckr";
|
|
19
|
+
const OWNER = { email: "owner@example.com", password: "correct horse" };
|
|
20
|
+
|
|
21
|
+
async function readSettings(db: Database, siteId: string) {
|
|
22
|
+
const rows = await db.select().from(settings);
|
|
23
|
+
return Object.fromEntries(
|
|
24
|
+
rows
|
|
25
|
+
.filter((row) => row.siteId === siteId)
|
|
26
|
+
.map((row) => [row.key, row.value]),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function storedPasswordHash(db: Database) {
|
|
31
|
+
const rows = await db.select({ password: account.password }).from(account);
|
|
32
|
+
expect(rows).toHaveLength(1);
|
|
33
|
+
return rows[0]?.password ?? "";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("EnsureSingleSiteOptions.id", () => {
|
|
37
|
+
let db: Database;
|
|
38
|
+
|
|
39
|
+
beforeEach(async () => {
|
|
40
|
+
db = createTestDatabase().db as unknown as Database;
|
|
41
|
+
await db.run(sql`DELETE FROM "site"`);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("creates the site with the given id", async () => {
|
|
45
|
+
const { site } = await createSiteService(db).ensureSingleSite({
|
|
46
|
+
id: SNAPSHOT_SITE_ID,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(site.id).toBe(SNAPSHOT_SITE_ID);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Like the host and key, the id says what a new site is created with. It is
|
|
53
|
+
// never a request to rename a site that exists.
|
|
54
|
+
it("leaves an existing site's id alone", async () => {
|
|
55
|
+
const { site: existing } = await createSiteService(db).ensureSingleSite();
|
|
56
|
+
const { site } = await createSiteService(db).ensureSingleSite({
|
|
57
|
+
id: SNAPSHOT_SITE_ID,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(site.id).toBe(existing.id);
|
|
61
|
+
expect(await db.select().from(sites)).toHaveLength(1);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("BootstrapService.setUpInstance", () => {
|
|
66
|
+
let db: Database;
|
|
67
|
+
|
|
68
|
+
function bootstrap(options?: Parameters<typeof createBootstrapService>[2]) {
|
|
69
|
+
return createBootstrapService(db, TRANSIENT_SINGLE_SITE_ID, options);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
beforeEach(async () => {
|
|
73
|
+
db = createTestDatabase().db as unknown as Database;
|
|
74
|
+
await db.run(sql`DELETE FROM "site"`);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("leaves a fresh install set up, the way both screens would", async () => {
|
|
78
|
+
const result = await bootstrap().setUpInstance({
|
|
79
|
+
...OWNER,
|
|
80
|
+
siteId: SNAPSHOT_SITE_ID,
|
|
81
|
+
siteName: "Field Notes",
|
|
82
|
+
siteLanguage: "zh-Hans",
|
|
83
|
+
timeZone: "Asia/Shanghai",
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
expect(result).toEqual({ outcome: "created", siteId: SNAPSHOT_SITE_ID });
|
|
87
|
+
|
|
88
|
+
const [owner] = await db.select().from(user);
|
|
89
|
+
expect(owner).toMatchObject({
|
|
90
|
+
email: OWNER.email,
|
|
91
|
+
name: "Field Notes",
|
|
92
|
+
role: "admin",
|
|
93
|
+
});
|
|
94
|
+
expect(
|
|
95
|
+
await verifyPassword({
|
|
96
|
+
hash: await storedPasswordHash(db),
|
|
97
|
+
password: OWNER.password,
|
|
98
|
+
}),
|
|
99
|
+
).toBe(true);
|
|
100
|
+
|
|
101
|
+
expect(await db.select().from(siteMembers)).toEqual([
|
|
102
|
+
expect.objectContaining({
|
|
103
|
+
siteId: SNAPSHOT_SITE_ID,
|
|
104
|
+
userId: owner?.id,
|
|
105
|
+
role: "owner",
|
|
106
|
+
}),
|
|
107
|
+
]);
|
|
108
|
+
expect(await db.select().from(navItems)).toHaveLength(5);
|
|
109
|
+
expect(await readSettings(db, SNAPSHOT_SITE_ID)).toMatchObject({
|
|
110
|
+
ONBOARDING_STATUS: "completed",
|
|
111
|
+
SITE_NAME: "Field Notes",
|
|
112
|
+
SITE_LANGUAGE: "zh-Hans",
|
|
113
|
+
TIME_ZONE: "Asia/Shanghai",
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// An empty answer on the screens: the base locale and UTC, and no name, so
|
|
118
|
+
// the site keeps its fallback and the account keeps the address's stand-in.
|
|
119
|
+
it("gives unanswered questions the screens' defaults", async () => {
|
|
120
|
+
const result = await bootstrap().setUpInstance(OWNER);
|
|
121
|
+
|
|
122
|
+
expect(result.outcome).toBe("created");
|
|
123
|
+
expect(result.siteId).toMatch(/^sit_/);
|
|
124
|
+
const stored = await readSettings(db, result.siteId);
|
|
125
|
+
expect(stored).toMatchObject({
|
|
126
|
+
ONBOARDING_STATUS: "completed",
|
|
127
|
+
SITE_LANGUAGE: "en",
|
|
128
|
+
TIME_ZONE: "UTC",
|
|
129
|
+
});
|
|
130
|
+
expect(stored.SITE_NAME).toBeUndefined();
|
|
131
|
+
expect((await db.select().from(user))[0]?.name).toBe("owner");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("gives the site its configured domain", async () => {
|
|
135
|
+
await createBootstrapService(db, TRANSIENT_SINGLE_SITE_ID, {
|
|
136
|
+
bootstrapSite: { host: "notes.example.com" },
|
|
137
|
+
}).setUpInstance(OWNER);
|
|
138
|
+
|
|
139
|
+
expect(await db.select().from(siteDomains)).toEqual([
|
|
140
|
+
expect.objectContaining({ host: "notes.example.com", kind: "primary" }),
|
|
141
|
+
]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// A deployment runs this on every start. The owner may have changed the
|
|
145
|
+
// password since, and a restart must not quietly put the old one back.
|
|
146
|
+
it("changes nothing on an install that finished setup", async () => {
|
|
147
|
+
const first = await bootstrap().setUpInstance({
|
|
148
|
+
...OWNER,
|
|
149
|
+
siteName: "Field Notes",
|
|
150
|
+
});
|
|
151
|
+
const hashBefore = await storedPasswordHash(db);
|
|
152
|
+
|
|
153
|
+
const second = await bootstrap().setUpInstance({
|
|
154
|
+
email: "someone-else@example.com",
|
|
155
|
+
password: "a different password",
|
|
156
|
+
siteName: "Renamed",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(second).toEqual({
|
|
160
|
+
outcome: "already-set-up",
|
|
161
|
+
siteId: first.siteId,
|
|
162
|
+
});
|
|
163
|
+
expect(await storedPasswordHash(db)).toBe(hashBefore);
|
|
164
|
+
expect(await db.select().from(user)).toHaveLength(1);
|
|
165
|
+
expect((await readSettings(db, first.siteId)).SITE_NAME).toBe(
|
|
166
|
+
"Field Notes",
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("refuses a site id that is not the install's", async () => {
|
|
171
|
+
await bootstrap().setUpInstance(OWNER);
|
|
172
|
+
|
|
173
|
+
await expect(
|
|
174
|
+
bootstrap().setUpInstance({ ...OWNER, siteId: SNAPSHOT_SITE_ID }),
|
|
175
|
+
).rejects.toThrow(`not ${SNAPSHOT_SITE_ID}`);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("after a run that stopped once the account existed", () => {
|
|
179
|
+
beforeEach(async () => {
|
|
180
|
+
await bootstrap().setUpInstance(OWNER);
|
|
181
|
+
// Undo everything past the account, which is where an interrupted run
|
|
182
|
+
// leaves an install.
|
|
183
|
+
await db.run(sql`DELETE FROM "site"`);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("finishes with the same address and password", async () => {
|
|
187
|
+
const result = await bootstrap().setUpInstance({
|
|
188
|
+
...OWNER,
|
|
189
|
+
siteId: SNAPSHOT_SITE_ID,
|
|
190
|
+
siteName: "Field Notes",
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(result).toEqual({ outcome: "resumed", siteId: SNAPSHOT_SITE_ID });
|
|
194
|
+
expect(await db.select().from(user)).toHaveLength(1);
|
|
195
|
+
expect((await db.select().from(user))[0]?.name).toBe("Field Notes");
|
|
196
|
+
expect((await readSettings(db, SNAPSHOT_SITE_ID)).ONBOARDING_STATUS).toBe(
|
|
197
|
+
"completed",
|
|
198
|
+
);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("refuses the wrong password", async () => {
|
|
202
|
+
await expect(
|
|
203
|
+
bootstrap().setUpInstance({ ...OWNER, password: "not the password" }),
|
|
204
|
+
).rejects.toThrow("does not match");
|
|
205
|
+
expect(await db.select().from(sites)).toHaveLength(0);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("refuses another address", async () => {
|
|
209
|
+
await expect(
|
|
210
|
+
bootstrap().setUpInstance({
|
|
211
|
+
email: "someone-else@example.com",
|
|
212
|
+
password: OWNER.password,
|
|
213
|
+
}),
|
|
214
|
+
).rejects.toThrow("already has an account under another address");
|
|
215
|
+
expect(await db.select().from(user)).toHaveLength(1);
|
|
216
|
+
expect(await db.select().from(sites)).toHaveLength(0);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// A hosted site's owner arrives through the control plane's handoff, so
|
|
221
|
+
// this refuses before creating an account in a database every tenant shares.
|
|
222
|
+
it("refuses a host-based install before writing anything", async () => {
|
|
223
|
+
await expect(
|
|
224
|
+
bootstrap({ siteResolutionMode: "host-based" }).setUpInstance(OWNER),
|
|
225
|
+
).rejects.toThrow("setUpInstance runs on self-hosted installs only");
|
|
226
|
+
|
|
227
|
+
expect(await db.select().from(user)).toHaveLength(0);
|
|
228
|
+
expect(await db.select().from(sites)).toHaveLength(0);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
@@ -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({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { sql } from "drizzle-orm";
|
|
2
3
|
import {
|
|
3
4
|
createTestDatabase,
|
|
4
5
|
DEFAULT_TEST_SITE_ID,
|
|
@@ -96,6 +97,39 @@ describe("CustomUrlService", () => {
|
|
|
96
97
|
});
|
|
97
98
|
});
|
|
98
99
|
|
|
100
|
+
// Postgres gives no order among rows that tie on the sort key, so paging
|
|
101
|
+
// by created_at alone repeated some custom URLs and never showed others
|
|
102
|
+
// (482 rows listed, 478 distinct, on a real site).
|
|
103
|
+
describe("list order", () => {
|
|
104
|
+
it("orders custom URLs created in the same second by ID", async () => {
|
|
105
|
+
const created = [];
|
|
106
|
+
for (const path of ["a-page", "b-page", "c-page"]) {
|
|
107
|
+
created.push(
|
|
108
|
+
await customUrlService.create({
|
|
109
|
+
path,
|
|
110
|
+
targetType: "redirect",
|
|
111
|
+
toPath: "/target",
|
|
112
|
+
redirectType: 301,
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
await db.run(sql`UPDATE path_registry SET created_at = 1000`);
|
|
117
|
+
|
|
118
|
+
const listed = await customUrlService.list();
|
|
119
|
+
const expected = created
|
|
120
|
+
.map((url) => url.id)
|
|
121
|
+
.sort()
|
|
122
|
+
.reverse();
|
|
123
|
+
expect(listed.map((url) => url.id)).toEqual(expected);
|
|
124
|
+
|
|
125
|
+
const pages = [
|
|
126
|
+
...(await customUrlService.list({ limit: 2, offset: 0 })),
|
|
127
|
+
...(await customUrlService.list({ limit: 2, offset: 2 })),
|
|
128
|
+
];
|
|
129
|
+
expect(pages.map((url) => url.id)).toEqual(expected);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
99
133
|
describe("getByPath", () => {
|
|
100
134
|
it("returns custom URL by path", async () => {
|
|
101
135
|
await customUrlService.create({
|