@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
package/src/services/auth.ts
CHANGED
|
@@ -21,8 +21,12 @@ import {
|
|
|
21
21
|
import type { SettingsService } from "./settings.js";
|
|
22
22
|
import type { StorageDriver } from "../lib/storage.js";
|
|
23
23
|
import { SETTINGS_KEYS } from "../lib/constants.js";
|
|
24
|
-
import { timingSafeEqualText } from "../lib/crypto.js";
|
|
25
|
-
import {
|
|
24
|
+
import { hmacHex, timingSafeEqualText } from "../lib/crypto.js";
|
|
25
|
+
import {
|
|
26
|
+
ConfigurationError,
|
|
27
|
+
ValidationError,
|
|
28
|
+
NotFoundError,
|
|
29
|
+
} from "../lib/errors.js";
|
|
26
30
|
import { hashPassword } from "../lib/password.js";
|
|
27
31
|
|
|
28
32
|
/** Dependencies for account deletion */
|
|
@@ -53,20 +57,27 @@ export interface AuthService {
|
|
|
53
57
|
resetPassword(token: string, newPassword: string): Promise<void>;
|
|
54
58
|
|
|
55
59
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
60
|
+
* Derive the account-deletion CSRF token for a session.
|
|
61
|
+
*
|
|
62
|
+
* Derived, not stored: rendering the delete page is a GET, and a GET that
|
|
63
|
+
* wrote a fresh token somewhere would invalidate every copy handed out
|
|
64
|
+
* before it — a second tab, a prefetch, or a browser extension re-fetching
|
|
65
|
+
* the page would silently break the button the author is looking at. Same
|
|
66
|
+
* session in, same token out, however many times it is asked for.
|
|
58
67
|
*
|
|
59
|
-
* @
|
|
68
|
+
* @param sessionId - The signed-in session this token is bound to
|
|
69
|
+
* @returns The token to embed in the page
|
|
60
70
|
*/
|
|
61
|
-
generateDeleteCsrfToken(): Promise<string>;
|
|
71
|
+
generateDeleteCsrfToken(sessionId: string): Promise<string>;
|
|
62
72
|
|
|
63
73
|
/**
|
|
64
|
-
* Validate
|
|
74
|
+
* Validate an account-deletion CSRF token against the requesting session.
|
|
65
75
|
*
|
|
66
|
-
* @param token - The
|
|
67
|
-
* @
|
|
76
|
+
* @param token - The token submitted with the request
|
|
77
|
+
* @param sessionId - The session the request arrived on
|
|
78
|
+
* @returns `true` when the token belongs to this session
|
|
68
79
|
*/
|
|
69
|
-
validateDeleteCsrfToken(token: string): Promise<boolean>;
|
|
80
|
+
validateDeleteCsrfToken(token: string, sessionId: string): Promise<boolean>;
|
|
70
81
|
|
|
71
82
|
/**
|
|
72
83
|
* Delete all user data and reset the instance to a pristine state.
|
|
@@ -87,6 +98,8 @@ export function createAuthService(
|
|
|
87
98
|
settings: SettingsService,
|
|
88
99
|
config?: {
|
|
89
100
|
databaseDialect?: DatabaseDialect;
|
|
101
|
+
/** Signing key for derived tokens. Absent only where no route needs one. */
|
|
102
|
+
authSecret?: string;
|
|
90
103
|
},
|
|
91
104
|
databaseSchema: DatabaseSchema = sqliteSchemaBundle,
|
|
92
105
|
): AuthService {
|
|
@@ -97,6 +110,7 @@ export function createAuthService(
|
|
|
97
110
|
verification,
|
|
98
111
|
media,
|
|
99
112
|
collections,
|
|
113
|
+
smartCollections,
|
|
100
114
|
threadCollections,
|
|
101
115
|
pathRegistry,
|
|
102
116
|
collectionDirectoryItems: directoryItemsTable,
|
|
@@ -108,6 +122,7 @@ export function createAuthService(
|
|
|
108
122
|
apiTokens,
|
|
109
123
|
} = databaseSchema;
|
|
110
124
|
const databaseDialect = config?.databaseDialect ?? "sqlite";
|
|
125
|
+
const authSecret = config?.authSecret;
|
|
111
126
|
|
|
112
127
|
async function deleteDataRows(targetDb: Database): Promise<void> {
|
|
113
128
|
// Junction/dependent tables first
|
|
@@ -125,6 +140,12 @@ export function createAuthService(
|
|
|
125
140
|
);
|
|
126
141
|
await executeStatement(targetDb, sql`DELETE FROM post`);
|
|
127
142
|
|
|
143
|
+
// Both kinds of collection, and smart ones first — a smart collection holds
|
|
144
|
+
// a `collection_id`. There is no constraint forcing that order any more
|
|
145
|
+
// (see the note on the column in `db/schema.ts`), but a reset that walks
|
|
146
|
+
// every table by hand should not leave one of them to an implicit cascade
|
|
147
|
+
// from `site` at the very end.
|
|
148
|
+
await targetDb.delete(smartCollections);
|
|
128
149
|
await targetDb.delete(collections);
|
|
129
150
|
await targetDb.delete(apiTokens);
|
|
130
151
|
await targetDb.delete(settingsTable);
|
|
@@ -174,14 +195,21 @@ export function createAuthService(
|
|
|
174
195
|
return timingSafeEqualText(tokenHash, storedHash);
|
|
175
196
|
}
|
|
176
197
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
198
|
+
/**
|
|
199
|
+
* The delete-account token for a session — a pure function of the session and
|
|
200
|
+
* the server's signing key, so nothing has to be written down to check it
|
|
201
|
+
* later. It stops being valid when the session does, which is the only
|
|
202
|
+
* lifetime that means anything here: the token is a second factor on top of
|
|
203
|
+
* the session cookie, useless to anyone who does not already hold it.
|
|
204
|
+
*/
|
|
205
|
+
async function deriveDeleteCsrfToken(sessionId: string): Promise<string> {
|
|
206
|
+
if (!authSecret) {
|
|
207
|
+
throw new ConfigurationError(
|
|
208
|
+
"AUTH_SECRET is required to protect account deletion.",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return hmacHex(authSecret, `delete-account:${sessionId}`);
|
|
185
213
|
}
|
|
186
214
|
|
|
187
215
|
return {
|
|
@@ -217,39 +245,13 @@ export function createAuthService(
|
|
|
217
245
|
await settings.remove(SETTINGS_KEYS.PASSWORD_RESET_TOKEN);
|
|
218
246
|
},
|
|
219
247
|
|
|
220
|
-
async generateDeleteCsrfToken() {
|
|
221
|
-
|
|
222
|
-
crypto.getRandomValues(bytes);
|
|
223
|
-
const plaintext = Array.from(bytes)
|
|
224
|
-
.map((b) => b.toString(16).padStart(2, "0"))
|
|
225
|
-
.join("");
|
|
226
|
-
|
|
227
|
-
const tokenHash = await hashToken(plaintext);
|
|
228
|
-
// Token valid for 15 minutes
|
|
229
|
-
const expiry = Math.floor(Date.now() / 1000) + 15 * 60;
|
|
230
|
-
|
|
231
|
-
await settings.set(
|
|
232
|
-
SETTINGS_KEYS.DELETE_CSRF_TOKEN,
|
|
233
|
-
`${tokenHash}:${expiry}`,
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
return plaintext;
|
|
248
|
+
async generateDeleteCsrfToken(sessionId) {
|
|
249
|
+
return deriveDeleteCsrfToken(sessionId);
|
|
237
250
|
},
|
|
238
251
|
|
|
239
|
-
async validateDeleteCsrfToken(token) {
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const separatorIndex = stored.lastIndexOf(":");
|
|
244
|
-
const storedHash = stored.substring(0, separatorIndex);
|
|
245
|
-
const expiry = parseInt(stored.substring(separatorIndex + 1), 10);
|
|
246
|
-
const now = Math.floor(Date.now() / 1000);
|
|
247
|
-
|
|
248
|
-
if (now > expiry) return false;
|
|
249
|
-
|
|
250
|
-
const tokenHash = await hashToken(token);
|
|
251
|
-
|
|
252
|
-
return timingSafeEqualText(tokenHash, storedHash);
|
|
252
|
+
async validateDeleteCsrfToken(token, sessionId) {
|
|
253
|
+
const expected = await deriveDeleteCsrfToken(sessionId);
|
|
254
|
+
return timingSafeEqualText(token, expected);
|
|
253
255
|
},
|
|
254
256
|
|
|
255
257
|
async deleteAllData(deps) {
|
|
@@ -1,83 +1,452 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bootstrap Service
|
|
3
3
|
*
|
|
4
|
-
* Owns first-run
|
|
4
|
+
* Owns first-run setup, which runs in two acts on a self-hosted site: the
|
|
5
|
+
* account is created first, and everything about the site itself is settled
|
|
6
|
+
* afterwards. The two are separate methods because the site is left standing
|
|
7
|
+
* between them — `markSiteProvisioned` closes the first act, which is the same
|
|
8
|
+
* state a control plane leaves a hosted site in, so the second act is one code
|
|
9
|
+
* path for both install kinds.
|
|
10
|
+
*
|
|
11
|
+
* The first act belongs to self-hosted installs alone. A hosted site gets its
|
|
12
|
+
* owner through the control plane's handoff, so on a host-based install only
|
|
13
|
+
* the second act runs, against the site the request resolved — the same site
|
|
14
|
+
* the route's membership check reads. On a hosted install the database holds
|
|
15
|
+
* every tenant, so "the only site" is not a question with an answer there.
|
|
16
|
+
*
|
|
17
|
+
* A self-hosted install that is deployed rather than clicked through runs both
|
|
18
|
+
* acts back to back without a browser — see `setUpInstance`.
|
|
5
19
|
*/
|
|
6
20
|
|
|
21
|
+
import { and, eq } from "drizzle-orm";
|
|
22
|
+
import type { DatabaseDialect } from "../db/dialect.js";
|
|
7
23
|
import type { Database } from "../db/index.js";
|
|
8
24
|
import {
|
|
9
25
|
sqliteSchemaBundle,
|
|
10
26
|
type DatabaseSchema,
|
|
11
27
|
} from "../db/schema-bundle.js";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
resolveCatalogLocale,
|
|
17
|
-
} from "../i18n/locales.js";
|
|
28
|
+
import { AUTH_ID_PREFIX, createTypeId } from "../lib/ids.js";
|
|
29
|
+
import { hashPassword, verifyPassword } from "../lib/password.js";
|
|
30
|
+
import { deriveAccountName } from "../lib/schemas.js";
|
|
31
|
+
import { now } from "../lib/time.js";
|
|
18
32
|
import { createNavItemService } from "./navigation.js";
|
|
19
33
|
import { createSettingsService } from "./settings.js";
|
|
20
34
|
import { createSiteMemberService } from "./site-member.js";
|
|
21
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
createSiteService,
|
|
37
|
+
TRANSIENT_SINGLE_SITE_ID,
|
|
38
|
+
type EnsureSingleSiteOptions,
|
|
39
|
+
} from "./site.js";
|
|
22
40
|
|
|
23
|
-
export interface
|
|
41
|
+
export interface ProvisionOwnerAccountData {
|
|
42
|
+
/** The account the first setup screen just created. */
|
|
24
43
|
ownerUserId: string;
|
|
25
|
-
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CompleteSiteSetupData {
|
|
47
|
+
/**
|
|
48
|
+
* The site's name, on an install that does not have one yet.
|
|
49
|
+
*
|
|
50
|
+
* Omitted on a hosted site: the control plane named it at creation, so the
|
|
51
|
+
* second screen never asks and must not overwrite.
|
|
52
|
+
*/
|
|
53
|
+
siteName?: string | null;
|
|
54
|
+
/** Language the site publishes in, chosen explicitly during setup. */
|
|
26
55
|
siteLanguage?: string | null;
|
|
27
|
-
|
|
56
|
+
/**
|
|
57
|
+
* The browser's own language, used only to pin the dashboard UI locale.
|
|
58
|
+
*
|
|
59
|
+
* Kept separate from `siteLanguage` because the two genuinely differ for the
|
|
60
|
+
* people this matters to most: someone running an English-language blog from
|
|
61
|
+
* a Chinese browser wants an English site and a Chinese dashboard.
|
|
62
|
+
*/
|
|
63
|
+
browserLanguage?: string | null;
|
|
64
|
+
/**
|
|
65
|
+
* The browser's time zone, already mapped to a supported one by the caller.
|
|
66
|
+
*
|
|
67
|
+
* Omitted where the shell already carries one, so a second screen loaded in
|
|
68
|
+
* a different place cannot move a hosted site's clock.
|
|
69
|
+
*/
|
|
28
70
|
timeZone?: string | null;
|
|
29
71
|
}
|
|
30
72
|
|
|
73
|
+
export interface CompleteSiteSetupDeps {
|
|
74
|
+
/**
|
|
75
|
+
* Align the operator's better-auth account name with the site's display
|
|
76
|
+
* name, the way the general settings page does whenever the name changes.
|
|
77
|
+
*
|
|
78
|
+
* Injected rather than called here: the update needs the request's own
|
|
79
|
+
* headers to know whose account it is, and a service has none.
|
|
80
|
+
*/
|
|
81
|
+
updateCurrentUserName?: ((displayName: string) => Promise<void>) | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface SetUpInstanceData {
|
|
85
|
+
/** The owner's sign-in address, normalized the way sign-in normalizes it. */
|
|
86
|
+
email: string;
|
|
87
|
+
/** The owner's password in plain text. Only its hash is stored. */
|
|
88
|
+
password: string;
|
|
89
|
+
/** The id to create the site with. See `EnsureSingleSiteOptions.id`. */
|
|
90
|
+
siteId?: string;
|
|
91
|
+
/** The site's name. Omitted leaves the built-in fallback name in place. */
|
|
92
|
+
siteName?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Language the site publishes in. Omitted means the base locale, the same
|
|
95
|
+
* as an empty answer on the screen.
|
|
96
|
+
*/
|
|
97
|
+
siteLanguage?: string;
|
|
98
|
+
/** The site's time zone. Omitted means UTC, as on the screen. */
|
|
99
|
+
timeZone?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* What `setUpInstance` found:
|
|
104
|
+
* - `created`: nothing existed, and now the account, the site and its answers do
|
|
105
|
+
* - `resumed`: an earlier run stopped after creating the account; this one
|
|
106
|
+
* finished the rest
|
|
107
|
+
* - `already-set-up`: setup had finished before, and nothing was changed
|
|
108
|
+
*/
|
|
109
|
+
export type SetUpInstanceOutcome = "created" | "resumed" | "already-set-up";
|
|
110
|
+
|
|
111
|
+
export interface SetUpInstanceResult {
|
|
112
|
+
outcome: SetUpInstanceOutcome;
|
|
113
|
+
siteId: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
31
116
|
export interface BootstrapService {
|
|
32
117
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
118
|
+
* Stand the site up around a newly created owner account.
|
|
119
|
+
*
|
|
120
|
+
* Everything here is what a site needs to exist at all — a row, an owner, a
|
|
121
|
+
* navigation profile. Nothing here is an answer the author gave, because at
|
|
122
|
+
* this point they have given none beyond their credentials.
|
|
123
|
+
*
|
|
124
|
+
* Ends by marking the site provisioned rather than complete. That is what
|
|
125
|
+
* makes the gap between the two screens survivable: at `provisioned` the
|
|
126
|
+
* onboarding middleware serves `/signin` normally, so an author who loses
|
|
127
|
+
* the session before finishing signs back in and lands on the second screen.
|
|
128
|
+
*
|
|
129
|
+
* @param data - The account the first screen created
|
|
130
|
+
* @throws {Error} On a host-based install, where a site's owner arrives
|
|
131
|
+
* through the control plane's handoff instead
|
|
132
|
+
*/
|
|
133
|
+
provisionOwnerAccount(data: ProvisionOwnerAccountData): Promise<void>;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Close first-run setup by recording what the author said about their site.
|
|
137
|
+
*
|
|
138
|
+
* Shared by both install kinds: a self-hosted site arrives here from its
|
|
139
|
+
* second screen with a name to set, a hosted site from its only screen with
|
|
140
|
+
* the name already stored. Onboarding is marked complete last, so an
|
|
141
|
+
* interrupted run resumes rather than stranding a half-answered site.
|
|
142
|
+
*
|
|
143
|
+
* @param data - The answers the second screen collected
|
|
144
|
+
* @param opts - The language in effect before this answer
|
|
145
|
+
* @param deps - Cross-cutting work the route owns, such as the better-auth
|
|
146
|
+
* account rename that needs the request's headers
|
|
147
|
+
*/
|
|
148
|
+
completeSiteSetup(
|
|
149
|
+
data: CompleteSiteSetupData,
|
|
150
|
+
opts: { oldLanguage: string },
|
|
151
|
+
deps?: CompleteSiteSetupDeps,
|
|
152
|
+
): Promise<void>;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Set a self-hosted install up without a browser: the owner account, the
|
|
156
|
+
* site around it, and the answers the second screen would have collected,
|
|
157
|
+
* ending with onboarding complete.
|
|
36
158
|
*
|
|
37
|
-
*
|
|
159
|
+
* The site half is the screens' own code. The account half differs in one
|
|
160
|
+
* way: the first screen opens the account through better-auth so the
|
|
161
|
+
* browser leaves with a session, and there is no browser here to hand one
|
|
162
|
+
* to. The rows are written directly instead, with the same password hash
|
|
163
|
+
* better-auth is configured with, and named the way the first screen names
|
|
164
|
+
* them until the site's name replaces it.
|
|
165
|
+
*
|
|
166
|
+
* Safe to run on every start. An install that finished setup is left as it
|
|
167
|
+
* is, whatever this call was given — the owner may have changed their
|
|
168
|
+
* address or password since, and a restart must not undo that. A run that
|
|
169
|
+
* stopped after creating the account resumes, but only with that account's
|
|
170
|
+
* address and password, which is what the first screen asks for too.
|
|
171
|
+
*
|
|
172
|
+
* @param data - The owner's credentials and the site's answers, already
|
|
173
|
+
* validated
|
|
174
|
+
* @returns What was found, and the site
|
|
175
|
+
* @throws {Error} On a host-based install; when the site exists under an id
|
|
176
|
+
* other than `siteId`; when an account for another address exists; when
|
|
177
|
+
* the password does not match the account an earlier run left
|
|
178
|
+
* @example
|
|
179
|
+
* ```ts
|
|
180
|
+
* await bootstrap.setUpInstance({
|
|
181
|
+
* email: "owner@example.com",
|
|
182
|
+
* password: "correct horse battery",
|
|
183
|
+
* siteName: "Field Notes",
|
|
184
|
+
* });
|
|
185
|
+
* ```
|
|
38
186
|
*/
|
|
39
|
-
|
|
187
|
+
setUpInstance(data: SetUpInstanceData): Promise<SetUpInstanceResult>;
|
|
40
188
|
}
|
|
41
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Create the first-run setup service for one request.
|
|
192
|
+
*
|
|
193
|
+
* @param db - The database
|
|
194
|
+
* @param siteId - The site the request resolved. On a self-hosted install that
|
|
195
|
+
* has not finished the account step this is the transient placeholder, since
|
|
196
|
+
* no site row exists yet.
|
|
197
|
+
* @param options - The schema, dialect and resolution mode the runtime uses,
|
|
198
|
+
* and where a self-hosted site's row and domain come from
|
|
199
|
+
* @returns The bootstrap service bound to that site
|
|
200
|
+
*/
|
|
42
201
|
export function createBootstrapService(
|
|
43
202
|
db: Database,
|
|
203
|
+
siteId: string,
|
|
44
204
|
options?: {
|
|
45
205
|
schema?: DatabaseSchema;
|
|
206
|
+
databaseDialect?: DatabaseDialect;
|
|
46
207
|
bootstrapSite?: EnsureSingleSiteOptions;
|
|
208
|
+
siteResolutionMode?: "single-site" | "host-based";
|
|
47
209
|
},
|
|
48
210
|
): BootstrapService {
|
|
49
211
|
const databaseSchema = options?.schema ?? sqliteSchemaBundle;
|
|
212
|
+
const dialect = options?.databaseDialect ?? "sqlite";
|
|
213
|
+
const siteResolutionMode = options?.siteResolutionMode ?? "single-site";
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The request's site, which must be a real row by the time anything is
|
|
217
|
+
* written to it. The placeholder only stands in for a self-hosted site the
|
|
218
|
+
* account step has yet to create.
|
|
219
|
+
*/
|
|
220
|
+
function existingSiteId(step: string): string {
|
|
221
|
+
if (siteId === TRANSIENT_SINGLE_SITE_ID) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`${step} needs an existing site, but the request resolved none.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return siteId;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function settingsFor(targetSiteId: string) {
|
|
230
|
+
return createSettingsService(db, targetSiteId, databaseSchema, dialect);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* A host-based install refuses before anything is written. The control
|
|
235
|
+
* plane creates each hosted site already provisioned, and its owner arrives
|
|
236
|
+
* through the hosted handoff, the only way anyone becomes a member of one.
|
|
237
|
+
* Attaching whoever just signed in to the site the host resolved would let
|
|
238
|
+
* any account in a database every tenant shares take a site that had lost
|
|
239
|
+
* its onboarding status.
|
|
240
|
+
*/
|
|
241
|
+
function assertSelfHosted(step: string): void {
|
|
242
|
+
if (siteResolutionMode === "host-based") {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`${step} runs on self-hosted installs only. A hosted site's owner arrives through the control plane's handoff.`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Stand the site up around its owner. A self-hosted install has no row until
|
|
251
|
+
* now, so this is the step that creates it — along with its domain, when one
|
|
252
|
+
* is configured.
|
|
253
|
+
*
|
|
254
|
+
* @returns The site's id
|
|
255
|
+
*/
|
|
256
|
+
async function provisionSite(
|
|
257
|
+
step: string,
|
|
258
|
+
ownerUserId: string,
|
|
259
|
+
siteOptions: EnsureSingleSiteOptions | undefined,
|
|
260
|
+
): Promise<string> {
|
|
261
|
+
assertSelfHosted(step);
|
|
262
|
+
const { site } = await createSiteService(
|
|
263
|
+
db,
|
|
264
|
+
databaseSchema,
|
|
265
|
+
).ensureSingleSite(siteOptions);
|
|
266
|
+
const navItems = createNavItemService(db, site.id, databaseSchema);
|
|
267
|
+
const siteMembers = createSiteMemberService(db, databaseSchema);
|
|
268
|
+
|
|
269
|
+
await siteMembers.ensure(site.id, ownerUserId, "owner");
|
|
270
|
+
await navItems.materializeDefaultNavigation();
|
|
271
|
+
await settingsFor(site.id).markSiteProvisioned();
|
|
272
|
+
return site.id;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function recordSiteAnswers(
|
|
276
|
+
targetSiteId: string,
|
|
277
|
+
data: CompleteSiteSetupData,
|
|
278
|
+
opts: { oldLanguage: string },
|
|
279
|
+
deps: CompleteSiteSetupDeps | undefined,
|
|
280
|
+
): Promise<void> {
|
|
281
|
+
const settings = settingsFor(targetSiteId);
|
|
282
|
+
|
|
283
|
+
const siteName = data.siteName?.trim();
|
|
284
|
+
if (siteName) {
|
|
285
|
+
await settings.set("SITE_NAME", siteName);
|
|
286
|
+
// Before onboarding closes, deliberately: if the rename fails the site
|
|
287
|
+
// is still provisioned, and the author retries the screen they were
|
|
288
|
+
// already on rather than landing in a finished site under a stale name.
|
|
289
|
+
await deps?.updateCurrentUserName?.(siteName);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// `undefined` means the shell already has one. `null` is a browser that
|
|
293
|
+
// reported nothing, which is the same UTC default the site would fall
|
|
294
|
+
// back to anyway — written so a later reader sees a chosen value.
|
|
295
|
+
if (data.timeZone !== undefined) {
|
|
296
|
+
await settings.set("TIME_ZONE", data.timeZone ?? "UTC");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
await settings.confirmFirstRunLanguage(
|
|
300
|
+
{
|
|
301
|
+
siteLanguage: data.siteLanguage ?? "",
|
|
302
|
+
browserLanguage: data.browserLanguage,
|
|
303
|
+
},
|
|
304
|
+
opts,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* The account a browserless setup stands the site up around: a new one when
|
|
310
|
+
* the install has none, or the one an interrupted run left, claimed with its
|
|
311
|
+
* own password — the way the first screen resumes by signing in.
|
|
312
|
+
*/
|
|
313
|
+
async function claimOwnerAccount(
|
|
314
|
+
email: string,
|
|
315
|
+
password: string,
|
|
316
|
+
): Promise<{ userId: string; created: boolean }> {
|
|
317
|
+
const { account, user } = databaseSchema;
|
|
318
|
+
const [existing] = await db
|
|
319
|
+
.select({ id: user.id, passwordHash: account.password })
|
|
320
|
+
.from(user)
|
|
321
|
+
.leftJoin(
|
|
322
|
+
account,
|
|
323
|
+
and(eq(account.userId, user.id), eq(account.providerId, "credential")),
|
|
324
|
+
)
|
|
325
|
+
.where(eq(user.email, email))
|
|
326
|
+
.limit(1);
|
|
327
|
+
|
|
328
|
+
if (existing) {
|
|
329
|
+
const matches =
|
|
330
|
+
!!existing.passwordHash &&
|
|
331
|
+
(await verifyPassword({ hash: existing.passwordHash, password }));
|
|
332
|
+
if (!matches) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`An earlier setup created the account for ${email}, and the password given does not match it.`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
return { userId: existing.id, created: false };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const [other] = await db.select({ id: user.id }).from(user).limit(1);
|
|
341
|
+
if (other) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`This install already has an account under another address. Pass that account's address and password to finish setup.`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const userId = createTypeId(AUTH_ID_PREFIX.user);
|
|
348
|
+
const timestamp = new Date(now() * 1000);
|
|
349
|
+
await db.insert(user).values({
|
|
350
|
+
id: userId,
|
|
351
|
+
email,
|
|
352
|
+
emailVerified: false,
|
|
353
|
+
image: null,
|
|
354
|
+
// The first screen's stand-in, replaced when the site is named.
|
|
355
|
+
name: deriveAccountName(email),
|
|
356
|
+
role: "admin",
|
|
357
|
+
createdAt: timestamp,
|
|
358
|
+
updatedAt: timestamp,
|
|
359
|
+
});
|
|
360
|
+
await db.insert(account).values({
|
|
361
|
+
id: createTypeId(AUTH_ID_PREFIX.account),
|
|
362
|
+
accountId: userId,
|
|
363
|
+
userId,
|
|
364
|
+
providerId: "credential",
|
|
365
|
+
password: await hashPassword(password),
|
|
366
|
+
accessToken: null,
|
|
367
|
+
accessTokenExpiresAt: null,
|
|
368
|
+
idToken: null,
|
|
369
|
+
refreshToken: null,
|
|
370
|
+
refreshTokenExpiresAt: null,
|
|
371
|
+
scope: null,
|
|
372
|
+
createdAt: timestamp,
|
|
373
|
+
updatedAt: timestamp,
|
|
374
|
+
});
|
|
375
|
+
return { userId, created: true };
|
|
376
|
+
}
|
|
50
377
|
|
|
51
378
|
return {
|
|
52
|
-
async
|
|
53
|
-
|
|
54
|
-
|
|
379
|
+
async provisionOwnerAccount(data) {
|
|
380
|
+
await provisionSite(
|
|
381
|
+
"provisionOwnerAccount",
|
|
382
|
+
data.ownerUserId,
|
|
55
383
|
options?.bootstrapSite,
|
|
56
384
|
);
|
|
57
|
-
|
|
58
|
-
const navItems = createNavItemService(db, site.id, databaseSchema);
|
|
59
|
-
const siteMembers = createSiteMemberService(db, databaseSchema);
|
|
385
|
+
},
|
|
60
386
|
|
|
61
|
-
|
|
62
|
-
await
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
? normalizeContentLanguage(data.siteLanguage)
|
|
68
|
-
: baseLocale;
|
|
69
|
-
await settings.set("SITE_LANGUAGE", siteLanguage);
|
|
70
|
-
// Pin the dashboard UI locale to the detected catalog language so it stays
|
|
71
|
-
// stable if the operator later changes the public content language.
|
|
72
|
-
await settings.set(
|
|
73
|
-
"DASHBOARD_LANGUAGE",
|
|
74
|
-
resolveCatalogLocale(siteLanguage),
|
|
387
|
+
async completeSiteSetup(data, opts, deps) {
|
|
388
|
+
await recordSiteAnswers(
|
|
389
|
+
existingSiteId("completeSiteSetup"),
|
|
390
|
+
data,
|
|
391
|
+
opts,
|
|
392
|
+
deps,
|
|
75
393
|
);
|
|
76
|
-
|
|
77
|
-
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
async setUpInstance(data) {
|
|
397
|
+
assertSelfHosted("setUpInstance");
|
|
398
|
+
|
|
399
|
+
const existingSite = await createSiteService(
|
|
400
|
+
db,
|
|
401
|
+
databaseSchema,
|
|
402
|
+
).getOnlySite();
|
|
403
|
+
if (
|
|
404
|
+
existingSite &&
|
|
405
|
+
data.siteId !== undefined &&
|
|
406
|
+
existingSite.id !== data.siteId
|
|
407
|
+
) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`This install's site is ${existingSite.id}, not ${data.siteId}. Check that the database is the one you meant.`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
if (
|
|
413
|
+
existingSite &&
|
|
414
|
+
(await settingsFor(existingSite.id).isOnboardingComplete())
|
|
415
|
+
) {
|
|
416
|
+
return { outcome: "already-set-up", siteId: existingSite.id };
|
|
78
417
|
}
|
|
79
418
|
|
|
80
|
-
await
|
|
419
|
+
const owner = await claimOwnerAccount(data.email, data.password);
|
|
420
|
+
const setUpSiteId = await provisionSite("setUpInstance", owner.userId, {
|
|
421
|
+
...options?.bootstrapSite,
|
|
422
|
+
id: data.siteId,
|
|
423
|
+
});
|
|
424
|
+
const { user } = databaseSchema;
|
|
425
|
+
await recordSiteAnswers(
|
|
426
|
+
setUpSiteId,
|
|
427
|
+
{
|
|
428
|
+
siteName: data.siteName,
|
|
429
|
+
siteLanguage: data.siteLanguage,
|
|
430
|
+
timeZone: data.timeZone ?? null,
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
oldLanguage:
|
|
434
|
+
(await settingsFor(setUpSiteId).get("SITE_LANGUAGE")) ?? "",
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
async updateCurrentUserName(displayName) {
|
|
438
|
+
await db
|
|
439
|
+
.update(user)
|
|
440
|
+
.set({ name: displayName, updatedAt: new Date(now() * 1000) })
|
|
441
|
+
.where(eq(user.id, owner.userId));
|
|
442
|
+
},
|
|
443
|
+
},
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
outcome: owner.created ? "created" : "resumed",
|
|
448
|
+
siteId: setUpSiteId,
|
|
449
|
+
};
|
|
81
450
|
},
|
|
82
451
|
};
|
|
83
452
|
}
|