@jant/core 0.6.16 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/bin/commands/assets/upload.js +192 -15
- package/bin/commands/import-site.js +60 -4
- package/bin/commands/reset-password.js +21 -8
- package/bin/commands/site/snapshot/export.js +1 -1
- package/bin/commands/site/snapshot/import.js +1 -1
- package/bin/lib/d1-query.js +5 -16
- package/bin/lib/node-env.js +40 -4
- package/bin/lib/site-selection.js +85 -29
- package/bin/lib/wrangler-cli.js +28 -2
- package/bin/lib/wrangler-json.js +35 -0
- package/dist/{app-C7_4oab9.js → app-K_Aa1MMn.js} +52476 -13330
- package/dist/client/.vite/manifest.json +255 -90
- package/dist/client/_assets/chunks/confirm-D7GrwsCN.js +45 -0
- package/dist/client/_assets/chunks/create-editor-CD3FhrOB.js +257 -0
- package/dist/client/_assets/chunks/{native-DpcrFAPh.js → emoji-mart-C4RpRKcU.js} +716 -1
- package/dist/client/_assets/chunks/footnote-rail-CmeoVtnU.js +1 -0
- package/dist/client/_assets/chunks/heic-to-CuP2Qlsw.js +1 -0
- package/dist/client/_assets/chunks/jant-locale-picker-DL6mIOT5.js +69 -0
- package/dist/client/_assets/chunks/lit-DW8VmJAT.js +2 -0
- package/dist/client/_assets/chunks/mediabunny-DRaCQ1r-.js +31 -0
- package/dist/client/_assets/chunks/rolldown-runtime-hePW80VL.js +1 -0
- package/dist/client/_assets/chunks/settings-paths-BcdySNx6.js +84 -0
- package/dist/client/_assets/chunks/site-header-nav-DzXsFk05.js +1 -0
- package/dist/client/_assets/chunks/slug-format-CRXO1AL5.js +1 -0
- package/dist/client/_assets/chunks/slugify-373citbs.js +1 -0
- package/dist/client/_assets/chunks/sortable-list-CgaL2jCs.js +2 -0
- package/dist/client/_assets/chunks/types-D5iF3H1a.js +1 -0
- package/dist/client/_assets/chunks/unsafe-html-B5ZEXaxg.js +1 -0
- package/dist/client/_assets/chunks/unsafe-svg-0QCkP0vZ.js +2 -0
- package/dist/client/_assets/client-GiYENVw8.css +2 -0
- package/dist/client/_assets/client-VnFxJN7G.js +295 -0
- package/dist/client/_assets/client-auth-k8gJ6QJj.js +828 -0
- package/dist/client/_assets/client-author-DQ8R-8KR.css +2 -0
- package/dist/client/_assets/client-compose-B_kDtWkW.js +2004 -0
- package/dist/client/_assets/client-manage-M90aSTOk.js +2170 -0
- package/dist/client/_assets/client-settings-DfYs9n1F.js +1112 -0
- package/dist/{export-Dl5KCiEs.js → github-sync-BPAvT999.js} +2254 -107
- package/dist/index.js +2 -5
- package/dist/node.js +5 -7
- package/package.json +3 -3
- package/src/__tests__/auth-session-freshness.test.ts +57 -0
- package/src/__tests__/basecoat-variants.test.ts +121 -0
- package/src/__tests__/bin/assets-upload.test.ts +68 -0
- package/src/__tests__/export-hugo-build.test.ts +260 -0
- package/src/__tests__/export-service.test.ts +231 -0
- package/src/__tests__/helpers/component-css.ts +91 -0
- package/src/__tests__/helpers/db.ts +33 -0
- package/src/__tests__/helpers/export-fixtures.ts +3 -0
- package/src/__tests__/stylesheet-audience.test.ts +159 -0
- package/src/__tests__/stylesheet-breakpoints.test.ts +183 -0
- package/src/app.tsx +71 -16
- package/src/auth.ts +18 -1
- package/src/client/__tests__/collection-page-actions.test.ts +51 -2
- package/src/client/__tests__/compose-bridge-image-upload.test.ts +196 -0
- package/src/client/__tests__/compose-bridge-video-poster.test.ts +159 -0
- package/src/client/__tests__/compose-bridge.test.ts +272 -0
- package/src/client/__tests__/compose-launch.test.ts +62 -1
- package/src/client/__tests__/compose-shortcuts.test.ts +38 -9
- package/src/client/__tests__/compose-triggers.test.ts +90 -0
- package/src/client/__tests__/copy-field.test.ts +162 -0
- package/src/client/__tests__/custom-url-menu.test.ts +127 -6
- package/src/client/__tests__/image-processor.test.ts +324 -2
- package/src/client/__tests__/lazy-entries.test.ts +92 -0
- package/src/client/__tests__/media-scroll-hint.test.ts +155 -0
- package/src/client/__tests__/runtime-paths.test.ts +84 -0
- package/src/client/__tests__/smart-collection-page-actions.test.ts +158 -0
- package/src/client/__tests__/toast.test.ts +23 -0
- package/src/client/__tests__/video-processor-probe.test.ts +175 -0
- package/src/client/__tests__/video-processor.test.ts +181 -0
- package/src/client/audio-processor.ts +24 -15
- package/src/client/audio-waveform.ts +49 -0
- package/src/client/collection-dialog-host.ts +72 -0
- package/src/client/collection-navigation.ts +51 -16
- package/src/client/collection-page-actions.ts +26 -0
- package/src/client/components/__tests__/jant-collection-dialog.test.ts +329 -0
- package/src/client/components/__tests__/jant-collection-directory.test.ts +274 -10
- package/src/client/components/__tests__/jant-collection-form.test.ts +59 -178
- package/src/client/components/__tests__/jant-command-palette.test.ts +28 -0
- package/src/client/components/__tests__/jant-compose-dialog-draft-storage.test.ts +340 -0
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +1250 -53
- package/src/client/components/__tests__/jant-compose-editor.test.ts +544 -40
- package/src/client/components/__tests__/jant-compose-fullscreen.test.ts +0 -1
- package/src/client/components/__tests__/jant-locale-picker.test.ts +207 -0
- package/src/client/components/__tests__/jant-nav-manager.test.ts +234 -15
- package/src/client/components/__tests__/jant-post-menu.test.ts +413 -2
- package/src/client/components/__tests__/jant-post-picker.test.ts +197 -0
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +15 -12
- package/src/client/components/__tests__/jant-settings-general.test.ts +541 -172
- package/src/client/components/__tests__/jant-settings-language.test.ts +851 -0
- package/src/client/components/__tests__/jant-smart-collection-dialog.test.ts +665 -0
- package/src/client/components/collection-dialog-types.ts +49 -0
- package/src/client/components/collection-manager-types.ts +32 -1
- package/src/client/components/collection-types.ts +1 -15
- package/src/client/components/compose-types.ts +75 -1
- package/src/client/components/jant-collection-dialog.ts +722 -0
- package/src/client/components/jant-collection-directory.ts +567 -182
- package/src/client/components/jant-collection-form.ts +53 -232
- package/src/client/components/jant-command-palette.ts +100 -49
- package/src/client/components/jant-compose-dialog.ts +1486 -707
- package/src/client/components/jant-compose-editor.ts +673 -336
- package/src/client/components/jant-compose-fullscreen.ts +51 -43
- package/src/client/components/jant-config-editor.ts +65 -53
- package/src/client/components/jant-confirm-dialog.ts +13 -11
- package/src/client/components/jant-locale-picker.ts +418 -0
- package/src/client/components/jant-media-lightbox.ts +146 -136
- package/src/client/components/jant-nav-manager.ts +690 -366
- package/src/client/components/jant-post-menu.ts +1086 -282
- package/src/client/components/jant-post-picker.ts +324 -0
- package/src/client/components/jant-repo-picker.ts +138 -106
- package/src/client/components/jant-settings-avatar.ts +31 -27
- package/src/client/components/jant-settings-general.ts +617 -543
- package/src/client/components/jant-settings-language.ts +1087 -0
- package/src/client/components/jant-smart-collection-dialog.ts +1021 -0
- package/src/client/components/jant-text-preview.ts +94 -86
- package/src/client/components/nav-manager-types.ts +62 -1
- package/src/client/components/settings-types.ts +14 -29
- package/src/client/components/smart-collection-conditions.ts +285 -0
- package/src/client/components/smart-collection-dialog-types.ts +68 -0
- package/src/client/compose-bridge.ts +214 -141
- package/src/client/compose-launch.ts +76 -2
- package/src/client/compose-shortcuts.ts +6 -8
- package/src/client/compose-triggers.ts +50 -0
- package/src/client/copy-field.ts +108 -0
- package/src/client/custom-url-menu.ts +60 -4
- package/src/client/icons.ts +14 -0
- package/src/client/image-plan.ts +165 -0
- package/src/client/image-processor.ts +195 -117
- package/src/client/image-worker-client.ts +45 -0
- package/src/client/image-worker.ts +135 -0
- package/src/client/lazy-entries.ts +95 -0
- package/src/client/lazy-slugify.ts +2 -2
- package/src/client/media-scroll-hint.ts +43 -2
- package/src/client/mediabunny.ts +29 -0
- package/src/client/post-picker.ts +41 -0
- package/src/client/runtime-paths.ts +74 -1
- package/src/client/settings-bridge.ts +4 -10
- package/src/client/site-header-nav.js +18 -6
- package/src/client/smart-collection-dialog-host.ts +116 -0
- package/src/client/smart-collection-page-actions.ts +208 -0
- package/src/client/tiptap/__tests__/bubble-menu.test.ts +110 -0
- package/src/client/tiptap/__tests__/floating-position.test.ts +136 -1
- package/src/client/tiptap/__tests__/inline-image-upload.test.ts +37 -0
- package/src/client/tiptap/bubble-menu.ts +47 -16
- package/src/client/tiptap/floating-position.ts +216 -2
- package/src/client/tiptap/inline-image-upload.ts +7 -3
- package/src/client/tiptap/link-toolbar.ts +22 -9
- package/src/client/toast.ts +13 -0
- package/src/client/types/vite-worker.d.ts +12 -0
- package/src/client/upload-with-metadata.ts +3 -13
- package/src/client/video-processor.ts +351 -107
- package/src/client-auth.ts +14 -31
- package/src/client-compose.ts +19 -0
- package/src/client-manage.ts +13 -0
- package/src/client-settings.ts +14 -0
- package/src/client-site.ts +10 -3
- package/src/client.ts +4 -0
- package/src/db/__tests__/backfill-nav-label-mirrors.test.ts +149 -0
- package/src/db/__tests__/schema-enum-sources.test.ts +33 -0
- package/src/db/__tests__/smart-collection-placement.test.ts +402 -0
- package/src/db/backfills/0006_clear_mirrored_page_nav_labels.sql +31 -0
- package/src/db/migrations/0032_married_tomas.sql +3 -0
- package/src/db/migrations/0033_busy_johnny_storm.sql +198 -0
- package/src/db/migrations/0034_cheerful_rumiko_fujikawa.sql +35 -0
- package/src/db/migrations/meta/0032_snapshot.json +2582 -0
- package/src/db/migrations/meta/0033_snapshot.json +2816 -0
- package/src/db/migrations/meta/0034_snapshot.json +2816 -0
- package/src/db/migrations/meta/_journal.json +21 -0
- package/src/db/migrations/pg/0030_abandoned_nighthawk.sql +3 -0
- package/src/db/migrations/pg/0031_strong_ultimatum.sql +128 -0
- package/src/db/migrations/pg/0032_furry_multiple_man.sql +6 -0
- package/src/db/migrations/pg/meta/0030_snapshot.json +3329 -0
- package/src/db/migrations/pg/meta/0031_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/0032_snapshot.json +3660 -0
- package/src/db/migrations/pg/meta/_journal.json +21 -0
- package/src/db/pg/schema.ts +195 -27
- package/src/db/post-visibility.ts +117 -0
- package/src/db/schema.ts +222 -27
- package/src/i18n/__tests__/detect.test.ts +26 -81
- package/src/i18n/__tests__/supported-locales.test.ts +83 -0
- package/src/i18n/context.tsx +12 -2
- package/src/i18n/detect.ts +7 -66
- package/src/i18n/locales/glossary.zh-Hans.yml +44 -4
- package/src/i18n/locales/glossary.zh-Hant.yml +44 -4
- package/src/i18n/locales/public/en.po +490 -158
- package/src/i18n/locales/public/en.ts +1 -1
- package/src/i18n/locales/public/zh-Hans.po +487 -155
- package/src/i18n/locales/public/zh-Hans.ts +1 -1
- package/src/i18n/locales/public/zh-Hant.po +487 -155
- package/src/i18n/locales/public/zh-Hant.ts +1 -1
- package/src/i18n/locales/settings/en.po +551 -69
- package/src/i18n/locales/settings/en.ts +1 -1
- package/src/i18n/locales/settings/zh-Hans.po +597 -115
- package/src/i18n/locales/settings/zh-Hans.ts +1 -1
- package/src/i18n/locales/settings/zh-Hant.po +610 -128
- package/src/i18n/locales/settings/zh-Hant.ts +1 -1
- package/src/i18n/locales.ts +94 -0
- package/src/i18n/middleware.ts +12 -2
- package/src/i18n/supported-locales.ts +80 -0
- package/src/lib/__tests__/api-settings.test.ts +11 -2
- package/src/lib/__tests__/asset-path.test.ts +92 -0
- package/src/lib/__tests__/deferred.test.ts +90 -0
- package/src/lib/__tests__/discover-ping.test.ts +89 -0
- package/src/lib/__tests__/discover.test.ts +410 -0
- package/src/lib/__tests__/feed-policy.test.ts +26 -0
- package/src/lib/__tests__/feed.test.ts +1526 -15
- package/src/lib/__tests__/filter-dimensions.test.ts +341 -0
- package/src/lib/__tests__/hosted-signin.test.ts +1 -1
- package/src/lib/__tests__/http-cache.test.ts +162 -0
- package/src/lib/__tests__/jant-docs-dev.test.ts +21 -0
- package/src/lib/__tests__/jant-docs.test.ts +23 -0
- package/src/lib/__tests__/lang-detect.test.ts +231 -0
- package/src/lib/__tests__/media-helpers.test.ts +81 -0
- package/src/lib/__tests__/navigation.test.ts +135 -1
- package/src/lib/__tests__/resolve-config.test.ts +64 -3
- package/src/lib/__tests__/schemas.test.ts +42 -4
- package/src/lib/__tests__/site-unavailable-page.test.ts +25 -0
- package/src/lib/__tests__/slug.test.ts +70 -0
- package/src/lib/__tests__/slugify.test.ts +85 -0
- package/src/lib/__tests__/storage.test.ts +32 -0
- package/src/lib/__tests__/thread-fold.test.ts +113 -0
- package/src/lib/__tests__/upload-sideload.test.ts +14 -0
- package/src/lib/__tests__/url-fetch.test.ts +1 -2
- package/src/lib/__tests__/url.test.ts +78 -72
- package/src/lib/__tests__/view-language.test.ts +413 -0
- package/src/lib/__tests__/worker-response-cache.test.ts +90 -0
- package/src/lib/address.ts +57 -0
- package/src/lib/api-posts.ts +12 -0
- package/src/lib/asset-path.ts +98 -1
- package/src/lib/build-env.ts +16 -0
- package/src/lib/collection-groups.ts +5 -1
- package/src/lib/collection-paths.ts +3 -10
- package/src/lib/constants.ts +28 -3
- package/src/lib/copy-field.ts +36 -0
- package/src/lib/crypto.ts +40 -0
- package/src/lib/csp-builder.ts +2 -1
- package/src/lib/deferred.ts +79 -0
- package/src/lib/discover-ping.ts +141 -0
- package/src/lib/discover.ts +437 -0
- package/src/lib/env.ts +134 -0
- package/src/lib/errors.ts +33 -0
- package/src/lib/feed-policy.ts +236 -2
- package/src/lib/feed.ts +926 -73
- package/src/lib/filter-dimensions.ts +1349 -0
- package/src/lib/github-sync-site-config.ts +3 -0
- package/src/lib/hosted-signin.ts +1 -1
- package/src/lib/http-cache.ts +148 -0
- package/src/lib/hugo-markdown.ts +16 -0
- package/src/lib/icons.ts +9 -2
- package/src/lib/ids.ts +3 -0
- package/src/lib/jant-docs.ts +42 -0
- package/src/lib/lang-detect.ts +308 -0
- package/src/lib/media-helpers.ts +13 -1
- package/src/lib/navigation.ts +78 -11
- package/src/lib/pagination.ts +28 -24
- package/src/lib/per-language-surfaces.ts +67 -0
- package/src/lib/post-meta.ts +19 -0
- package/src/lib/render.tsx +55 -7
- package/src/lib/resolve-config.ts +53 -5
- package/src/lib/schemas.ts +201 -10
- package/src/lib/site-header-fragment.tsx +2 -1
- package/src/lib/site-unavailable-page.ts +47 -0
- package/src/lib/slug.ts +51 -7
- package/src/lib/slugify.ts +113 -0
- package/src/lib/startup-config.ts +4 -6
- package/src/lib/storage.ts +7 -0
- package/src/lib/summary.ts +50 -0
- package/src/lib/thread-fold.ts +161 -0
- package/src/lib/time.ts +32 -0
- package/src/lib/timeline.ts +42 -1
- package/src/lib/tiptap-render.ts +1 -2
- package/src/lib/upload.ts +16 -13
- package/src/lib/url.ts +139 -86
- package/src/lib/version.ts +10 -8
- package/src/lib/view-language.ts +436 -0
- package/src/lib/view.ts +51 -23
- package/src/lib/viewer-context.ts +62 -0
- package/src/middleware/__tests__/auth.test.ts +70 -2
- package/src/middleware/__tests__/cache-control.test.ts +71 -3
- package/src/middleware/__tests__/onboarding.test.ts +76 -5
- package/src/middleware/__tests__/secure-headers.test.ts +1 -1
- package/src/middleware/__tests__/session.test.ts +181 -33
- package/src/middleware/auth.ts +50 -31
- package/src/middleware/cache-control.ts +75 -10
- package/src/middleware/config.ts +6 -6
- package/src/middleware/error-handler.ts +12 -0
- package/src/middleware/onboarding.ts +32 -10
- package/src/middleware/secure-headers.ts +1 -1
- package/src/middleware/session.ts +96 -8
- package/src/node/__tests__/cli-migrate.test.ts +21 -19
- package/src/node/__tests__/cli-reset-password.test.ts +81 -8
- package/src/node/__tests__/cli-runtime-target.test.ts +48 -1
- package/src/node/__tests__/cli-site-selection.test.ts +128 -38
- package/src/node/__tests__/runtime.test.ts +37 -0
- package/src/node/index.ts +1 -0
- package/src/preset.css +76 -22
- package/src/routes/__tests__/feed-validator.test.ts +156 -0
- package/src/routes/api/__tests__/collections.test.ts +114 -0
- package/src/routes/api/__tests__/discover.test.ts +222 -0
- package/src/routes/api/__tests__/nav-items.test.ts +212 -2
- package/src/routes/api/__tests__/post-translations.test.ts +829 -0
- package/src/routes/api/__tests__/settings.test.ts +24 -3
- package/src/routes/api/__tests__/telegram.test.ts +6 -5
- package/src/routes/api/collections.ts +49 -6
- package/src/routes/api/custom-urls.ts +0 -1
- package/src/routes/api/discover.ts +95 -0
- package/src/routes/api/export.ts +3 -0
- package/src/routes/api/internal/__tests__/sites.test.ts +42 -2
- package/src/routes/api/nav-items.ts +55 -8
- package/src/routes/api/posts.ts +159 -0
- package/src/routes/api/public/__tests__/archive.test.ts +111 -0
- package/src/routes/api/public/archive.ts +77 -81
- package/src/routes/api/public/posts.ts +23 -6
- package/src/routes/api/smart-collections.ts +132 -0
- package/src/routes/api/telegram.ts +2 -40
- package/src/routes/api/uploads.ts +1 -2
- package/src/routes/auth/__tests__/setup-auth.test.ts +137 -0
- package/src/routes/auth/__tests__/setup-discover.test.ts +204 -0
- package/src/routes/auth/__tests__/setup-flow.test.ts +270 -0
- package/src/routes/auth/__tests__/setup-page.test.tsx +329 -0
- package/src/routes/auth/__tests__/setup-steps.test.ts +310 -0
- package/src/routes/auth/__tests__/setup.test.ts +278 -26
- package/src/routes/auth/__tests__/signin.test.ts +135 -0
- package/src/routes/auth/setup.tsx +934 -186
- package/src/routes/auth/signin.tsx +10 -8
- package/src/routes/compose.tsx +5 -0
- package/src/routes/dash/__tests__/custom-urls-archive.test.ts +44 -0
- package/src/routes/dash/__tests__/custom-urls-page.test.ts +49 -0
- package/src/routes/dash/__tests__/delete-account.test.ts +106 -0
- package/src/routes/dash/__tests__/settings-discover.test.ts +213 -0
- package/src/routes/dash/custom-urls.tsx +1 -37
- package/src/routes/dash/settings.tsx +502 -41
- package/src/routes/discover-announce.ts +96 -0
- package/src/routes/feed/__tests__/feed.test.ts +165 -2
- package/src/routes/feed/feed.ts +69 -40
- package/src/routes/feed/sitemap.ts +95 -11
- package/src/routes/pages/__tests__/archive-indexing.test.ts +200 -0
- package/src/routes/pages/__tests__/archive-params.test.ts +370 -6
- package/src/routes/pages/__tests__/collection-routing.test.ts +16 -0
- package/src/routes/pages/__tests__/collections-feed-links.test.ts +72 -0
- package/src/routes/pages/__tests__/collections.test.ts +9 -3
- package/src/routes/pages/__tests__/custom-archive-url.test.ts +119 -0
- package/src/routes/pages/__tests__/feed-autodiscovery.test.ts +148 -0
- package/src/routes/pages/__tests__/language-routing.test.ts +679 -0
- package/src/routes/pages/__tests__/post-preview-partial.test.ts +96 -0
- package/src/routes/pages/__tests__/smart-collection.test.ts +298 -0
- package/src/routes/pages/__tests__/subscribe.test.ts +248 -0
- package/src/routes/pages/archive.tsx +471 -369
- package/src/routes/pages/collection.tsx +94 -96
- package/src/routes/pages/collections.tsx +38 -50
- package/src/routes/pages/featured.tsx +32 -13
- package/src/routes/pages/home.tsx +25 -6
- package/src/routes/pages/language.tsx +113 -0
- package/src/routes/pages/latest.tsx +26 -16
- package/src/routes/pages/new.tsx +3 -0
- package/src/routes/pages/page.tsx +178 -17
- package/src/routes/pages/partials.tsx +41 -0
- package/src/routes/pages/search.tsx +18 -3
- package/src/routes/pages/smart-collection.tsx +340 -0
- package/src/routes/pages/subscribe.tsx +182 -0
- package/src/runtime/__tests__/index.test.ts +19 -0
- package/src/runtime/__tests__/node.test.ts +129 -4
- package/src/runtime/cloudflare.ts +21 -2
- package/src/runtime/index.ts +27 -1
- package/src/runtime/node.ts +18 -1
- package/src/runtime/site.ts +95 -24
- package/src/services/__tests__/auth.test.ts +116 -7
- package/src/services/__tests__/collection.test.ts +108 -4
- package/src/services/__tests__/language.test.ts +495 -0
- package/src/services/__tests__/navigation.test.ts +213 -10
- package/src/services/__tests__/path.test.ts +180 -0
- package/src/services/__tests__/post-language.test.ts +830 -0
- package/src/services/__tests__/post.test.ts +46 -9
- package/src/services/__tests__/settings.test.ts +228 -17
- package/src/services/__tests__/site-admin.test.ts +5 -5
- package/src/services/__tests__/smart-collection.test.ts +651 -0
- package/src/services/about-page.ts +22 -1
- package/src/services/auth.ts +51 -49
- package/src/services/bootstrap.ts +188 -38
- package/src/services/collection-directory-position.ts +76 -0
- package/src/services/collection.ts +331 -46
- package/src/services/custom-url.ts +27 -22
- package/src/services/export-theme/assets/client-site.css +1 -1
- package/src/services/export-theme/assets/client-site.js +123 -123
- package/src/services/export-theme/layouts/_default/rss.xml +77 -1
- package/src/services/export-theme/layouts/partials/feed-post-content.xml +105 -2
- package/src/services/export-theme/layouts/partials/media-gallery.html +35 -21
- package/src/services/export-theme/layouts/partials/pagination.html +14 -13
- package/src/services/export-theme/layouts/partials/post-card.html +8 -4
- package/src/services/export-theme/layouts/partials/reply.html +2 -1
- package/src/services/export-theme/layouts/partials/thread-preview.html +40 -30
- package/src/services/export-theme/styles/main.css +13 -1
- package/src/services/export.ts +278 -19
- package/src/services/index.ts +34 -3
- package/src/services/language.ts +412 -0
- package/src/services/mcp.ts +3 -1
- package/src/services/media.ts +14 -6
- package/src/services/navigation.ts +429 -67
- package/src/services/path.ts +221 -6
- package/src/services/post.ts +1021 -152
- package/src/services/search.ts +39 -2
- package/src/services/settings.ts +238 -43
- package/src/services/site-admin.ts +24 -7
- package/src/services/site.ts +77 -1
- package/src/services/smart-collection.ts +636 -0
- package/src/services/telegram.ts +20 -22
- package/src/services/upload-session.ts +1 -3
- package/src/style-author.css +27 -0
- package/src/styles/components-author.css +1622 -0
- package/src/styles/components.css +24 -1571
- package/src/styles/site-media.css +16 -2
- package/src/styles/tokens.css +82 -12
- package/src/styles/ui-author.css +7602 -0
- package/src/styles/ui.css +1087 -7219
- package/src/types/app-context.ts +7 -0
- package/src/types/bindings.ts +3 -0
- package/src/types/config.ts +94 -15
- package/src/types/constants.ts +188 -16
- package/src/types/entities.ts +87 -1
- package/src/types/operations.ts +64 -2
- package/src/types/props.ts +157 -31
- package/src/types/views.ts +55 -1
- package/src/ui/__tests__/font-themes.test.ts +52 -12
- package/src/ui/compose/ComposeDialog.tsx +201 -7
- package/src/ui/compose/ComposePrompt.tsx +1 -5
- package/src/ui/compose/__tests__/ComposeDialog.test.tsx +125 -0
- package/src/ui/dash/appearance/AdvancedContent.tsx +2 -2
- package/src/ui/dash/appearance/CodeInjectionContent.tsx +12 -16
- package/src/ui/dash/appearance/NavigationContent.tsx +117 -16
- package/src/ui/dash/appearance/__tests__/NavigationContent.test.tsx +33 -11
- package/src/ui/dash/appearance/__tests__/system-nav-descriptions.test.ts +44 -0
- package/src/ui/dash/appearance/system-nav-descriptions.ts +75 -0
- package/src/ui/dash/settings/AccountMenuContent.tsx +2 -2
- package/src/ui/dash/settings/ApiTokensContent.tsx +2 -1
- package/src/ui/dash/settings/ConfigEditorContent.tsx +35 -16
- package/src/ui/dash/settings/GeneralContent.tsx +275 -113
- package/src/ui/dash/settings/LanguageContent.tsx +409 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +19 -1
- package/src/ui/dash/settings/__tests__/GeneralContent.test.tsx +324 -4
- package/src/ui/dash/settings/__tests__/LanguageContent.test.tsx +144 -0
- package/src/ui/dash/settings/__tests__/discover-copy.test.ts +66 -0
- package/src/ui/dash/settings/discover-copy.ts +116 -0
- package/src/ui/feed/LinkCard.tsx +55 -45
- package/src/ui/feed/NoteCard.tsx +9 -4
- package/src/ui/feed/PostStatusBadges.tsx +90 -43
- package/src/ui/feed/QuoteCard.tsx +55 -30
- package/src/ui/feed/ThreadPreview.tsx +6 -2
- package/src/ui/feed/__tests__/thread-preview.test.ts +13 -16
- package/src/ui/feed/__tests__/timeline-cards.test.ts +314 -49
- package/src/ui/feed/thread-preview-state.ts +20 -6
- package/src/ui/font-themes.ts +39 -29
- package/src/ui/layouts/BaseLayout.tsx +155 -10
- package/src/ui/layouts/SiteLayout.tsx +192 -34
- package/src/ui/layouts/__tests__/BaseLayout.test.tsx +196 -17
- package/src/ui/layouts/__tests__/SiteLayout.test.tsx +109 -85
- package/src/ui/pages/ArchivePage.tsx +282 -289
- package/src/ui/pages/CollectionPage.tsx +86 -76
- package/src/ui/pages/CollectionsPage.tsx +5 -0
- package/src/ui/pages/ComposePage.tsx +8 -0
- package/src/ui/pages/PostPage.tsx +47 -0
- package/src/ui/pages/SearchPage.tsx +3 -3
- package/src/ui/pages/SmartCollectionPage.tsx +434 -0
- package/src/ui/pages/SubscribePage.tsx +236 -0
- package/src/ui/pages/__tests__/ArchivePage.test.tsx +65 -9
- package/src/ui/pages/__tests__/collections-directory-rows.test.tsx +92 -0
- package/src/ui/pages/__tests__/collections-view-links.test.tsx +81 -0
- package/src/ui/shared/CollectionDirectory.tsx +178 -3
- package/src/ui/shared/CollectionsManager.tsx +70 -17
- package/src/ui/shared/CopyField.tsx +127 -0
- package/src/ui/shared/EmptyPostContent.tsx +23 -0
- package/src/ui/shared/MediaGallery.tsx +125 -46
- package/src/ui/shared/Pagination.tsx +20 -5
- package/src/ui/shared/PostFooter.tsx +20 -11
- package/src/ui/shared/__tests__/archive-labels.test.ts +100 -0
- package/src/ui/shared/__tests__/media-gallery.test.ts +210 -2
- package/src/ui/shared/__tests__/navigation-labels.test.ts +16 -22
- package/src/ui/shared/__tests__/pagination.test.ts +38 -24
- package/src/ui/shared/__tests__/post-footer.test.ts +32 -0
- package/src/ui/shared/archive-labels.ts +98 -0
- package/src/ui/shared/collection-management-labels.ts +94 -16
- package/src/ui/shared/navigation-labels.ts +45 -71
- package/src/ui/shared/post-article-attributes.ts +2 -0
- package/src/ui/shared/smart-collection-labels.ts +343 -0
- package/dist/app-C75Zc8dD.js +0 -6
- package/dist/client/_assets/chunks/heic-to-DUUaO23q.js +0 -1
- package/dist/client/_assets/chunks/module-DcsAZQZ_.js +0 -716
- package/dist/client/_assets/chunks/url-CU9pEbk5.js +0 -1
- package/dist/client/_assets/client-Bq0vre8Y.js +0 -296
- package/dist/client/_assets/client-D3VZyG4L.css +0 -2
- package/dist/client/_assets/client-auth-F2rb9WfF.js +0 -5571
- package/dist/env-BPYViDJJ.js +0 -281
- package/dist/github-api-BNF35Lkx.js +0 -176
- package/dist/github-app-DdiD-bC4.js +0 -275
- package/dist/github-sync-Bqg62IvV.js +0 -4
- package/dist/github-sync-HdK2EgvJ.js +0 -431
- package/dist/url-CbLAtlZe.js +0 -900
- package/src/client/__tests__/collection-created-notice.test.ts +0 -36
- package/src/client/__tests__/collection-form-bridge.test.ts +0 -216
- package/src/client/__tests__/post-form-bridge.test.ts +0 -82
- package/src/client/collection-created-notice.ts +0 -66
- package/src/client/collection-form-bridge.ts +0 -191
- package/src/client/components/__tests__/jant-post-form.test.ts +0 -193
- package/src/client/components/jant-post-form.ts +0 -422
- package/src/client/components/post-form-template.ts +0 -352
- package/src/client/components/post-form-types.ts +0 -96
- package/src/client/media-metadata.ts +0 -247
- package/src/client/post-form-bridge.ts +0 -193
- package/src/ui/pages/CollectionEditorPage.tsx +0 -175
package/src/lib/asset-path.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Static asset path helpers.
|
|
3
3
|
*
|
|
4
4
|
* Build outputs live under an internal `/_assets` directory, while the public
|
|
5
|
-
* asset base path may be prefixed by the site's deployment path
|
|
5
|
+
* asset base path may be prefixed by the site's deployment path, or replaced
|
|
6
|
+
* by an absolute CDN URL. The connection hints a page needs are derived from
|
|
7
|
+
* those same base URLs, so they live here too.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { toPublicPath } from "./url.js";
|
|
@@ -67,6 +69,101 @@ export function isAssetPath(path: string, basePath = ASSET_BASE_PATH): boolean {
|
|
|
67
69
|
);
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
/** One `<link rel="preconnect">` the layout should emit. */
|
|
73
|
+
export interface PreconnectHint {
|
|
74
|
+
/** Origin to open a connection to. */
|
|
75
|
+
href: string;
|
|
76
|
+
/**
|
|
77
|
+
* Whether this is the CORS-mode connection. A browser pools credentialed
|
|
78
|
+
* and uncredentialed connections separately, so an origin serving both a
|
|
79
|
+
* stylesheet (no CORS) and a module script (CORS) needs one of each.
|
|
80
|
+
*/
|
|
81
|
+
crossorigin: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The origin of `base`, or `null` when it is not a cross-origin absolute URL.
|
|
86
|
+
*/
|
|
87
|
+
function crossOrigin(base: string | undefined, siteOrigin: string | null) {
|
|
88
|
+
const trimmed = base?.trim();
|
|
89
|
+
if (
|
|
90
|
+
!trimmed ||
|
|
91
|
+
(!trimmed.startsWith("http://") && !trimmed.startsWith("https://"))
|
|
92
|
+
) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const { origin } = new URL(trimmed);
|
|
97
|
+
return origin === siteOrigin ? null : origin;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Origins worth a `<link rel="preconnect">` for the subresources of a page.
|
|
105
|
+
*
|
|
106
|
+
* Only a host the page does not already have a connection to earns a hint:
|
|
107
|
+
* the site's own origin is connected by the time the HTML arrives, and a
|
|
108
|
+
* same-origin base path resolves to it. What is left is a CDN asset host and
|
|
109
|
+
* a media host, and warming DNS, TCP and TLS while the rest of `<head>` parses
|
|
110
|
+
* takes that round trip off the critical path.
|
|
111
|
+
*
|
|
112
|
+
* The asset host serves the render-blocking stylesheet and the module script,
|
|
113
|
+
* so it gets both connection kinds; the media host serves images, so it gets
|
|
114
|
+
* the uncredentialed one. When the two are the same host, the asset hints
|
|
115
|
+
* already cover the images.
|
|
116
|
+
*
|
|
117
|
+
* @param options - Resolved base URLs for the page's subresources
|
|
118
|
+
* @param options.assetBasePath - Public asset base path or absolute CDN URL
|
|
119
|
+
* @param options.mediaBaseUrl - Public media base URL, empty when same-origin
|
|
120
|
+
* @param options.siteUrl - The site's own URL, so its origin is never hinted
|
|
121
|
+
* @returns Hints in the order they should be emitted, possibly empty
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* getPreconnectHints({
|
|
126
|
+
* assetBasePath: "https://cdn.example.com/a",
|
|
127
|
+
* mediaBaseUrl: "https://media.example.com",
|
|
128
|
+
* siteUrl: "https://example.com",
|
|
129
|
+
* });
|
|
130
|
+
* // [
|
|
131
|
+
* // { href: "https://cdn.example.com", crossorigin: false },
|
|
132
|
+
* // { href: "https://cdn.example.com", crossorigin: true },
|
|
133
|
+
* // { href: "https://media.example.com", crossorigin: false },
|
|
134
|
+
* // ]
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
export function getPreconnectHints(options: {
|
|
138
|
+
assetBasePath: string;
|
|
139
|
+
mediaBaseUrl?: string;
|
|
140
|
+
siteUrl?: string;
|
|
141
|
+
}): PreconnectHint[] {
|
|
142
|
+
let siteOrigin: string | null = null;
|
|
143
|
+
if (options.siteUrl) {
|
|
144
|
+
try {
|
|
145
|
+
siteOrigin = new URL(options.siteUrl).origin;
|
|
146
|
+
} catch {
|
|
147
|
+
// An unparseable siteUrl tells us nothing; hint the other hosts anyway.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const assetOrigin = crossOrigin(options.assetBasePath, siteOrigin);
|
|
152
|
+
const mediaOrigin = crossOrigin(options.mediaBaseUrl, siteOrigin);
|
|
153
|
+
|
|
154
|
+
const hints: PreconnectHint[] = [];
|
|
155
|
+
if (assetOrigin) {
|
|
156
|
+
hints.push(
|
|
157
|
+
{ href: assetOrigin, crossorigin: false },
|
|
158
|
+
{ href: assetOrigin, crossorigin: true },
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (mediaOrigin && mediaOrigin !== assetOrigin) {
|
|
162
|
+
hints.push({ href: mediaOrigin, crossorigin: false });
|
|
163
|
+
}
|
|
164
|
+
return hints;
|
|
165
|
+
}
|
|
166
|
+
|
|
70
167
|
/**
|
|
71
168
|
* Convert an internal asset path into its public deployment path.
|
|
72
169
|
*
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time environment detection
|
|
3
|
+
*
|
|
4
|
+
* In Vite dev, `__JANT_DEV__` is replaced with `true` via Vite's `define`
|
|
5
|
+
* config. In production (wrangler/esbuild) the typeof check evaluates to false
|
|
6
|
+
* safely.
|
|
7
|
+
*
|
|
8
|
+
* Kept apart from `version.ts` so client bundles can read the flag: the client
|
|
9
|
+
* build defines `__JANT_VERSION__` but none of the Worker-only asset globals
|
|
10
|
+
* that `version.ts` reads at module scope.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
declare const __JANT_DEV__: boolean | undefined;
|
|
14
|
+
|
|
15
|
+
export const IS_VITE_DEV =
|
|
16
|
+
typeof __JANT_DEV__ !== "undefined" && __JANT_DEV__ === true;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export interface GroupableCollectionItem {
|
|
6
|
-
type: "collection" | "divider" | "link";
|
|
6
|
+
type: "collection" | "smart_collection" | "divider" | "link";
|
|
7
7
|
label?: string | null;
|
|
8
8
|
url?: string | null;
|
|
9
9
|
collection?: {
|
|
@@ -23,6 +23,10 @@ export interface DividerCollectionGroup {
|
|
|
23
23
|
* next divider. Groups with fewer than two collections do not produce an
|
|
24
24
|
* aggregate selection.
|
|
25
25
|
*
|
|
26
|
+
* A smart collection sitting in the group contributes nothing and does not
|
|
27
|
+
* break it: `/collections/{a+b}` is a union of tagged sets, and a set defined
|
|
28
|
+
* by conditions has no slug to union in. It is skipped the same way a link is.
|
|
29
|
+
*
|
|
26
30
|
* @param items - Ordered collection directory items
|
|
27
31
|
* @param dividerIndex - Index of the divider item to inspect
|
|
28
32
|
* @returns Aggregate slug expression and count, or `null`
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Collection URL helpers.
|
|
3
3
|
*
|
|
4
|
-
* Single collections use root-level paths.
|
|
5
|
-
*
|
|
4
|
+
* Single collections use root-level paths. The collections directory and
|
|
5
|
+
* aggregate views live under `/collections`. There is no editor path: a
|
|
6
|
+
* collection is created and edited in a dialog.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
export const COLLECTIONS_DIRECTORY_PATH = "/collections";
|
|
@@ -30,11 +31,3 @@ export function getCollectionSelectionPath(slugExpression: string): string {
|
|
|
30
31
|
export function getCollectionSelectionFeedPath(slugExpression: string): string {
|
|
31
32
|
return `${getCollectionSelectionPath(slugExpression)}/feed`;
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
-
export function getNewCollectionPath(): string {
|
|
35
|
-
return `${COLLECTIONS_DIRECTORY_PATH}/new`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function getCollectionEditPath(slug: string): string {
|
|
39
|
-
return `${COLLECTIONS_DIRECTORY_PATH}/${slug}/edit`;
|
|
40
|
-
}
|
package/src/lib/constants.ts
CHANGED
|
@@ -16,6 +16,7 @@ export const RESERVED_PATHS = [
|
|
|
16
16
|
"api",
|
|
17
17
|
"feed",
|
|
18
18
|
"search",
|
|
19
|
+
"subscribe",
|
|
19
20
|
"archive",
|
|
20
21
|
"media",
|
|
21
22
|
"pages",
|
|
@@ -36,10 +37,27 @@ export type ReservedPath = (typeof RESERVED_PATHS)[number];
|
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Check if a path is reserved
|
|
40
|
+
*
|
|
41
|
+
* @param path - Stored path form (no leading slash)
|
|
42
|
+
* @param languagePrefixes - URL prefixes of the site's additional languages
|
|
43
|
+
* (lowercase tags). While those are live, `/{prefix}` and everything under it
|
|
44
|
+
* is served by the language views, so no slug or custom URL may claim them.
|
|
45
|
+
* Passed per call rather than baked into the static list because the set is
|
|
46
|
+
* per-site and changes at runtime.
|
|
47
|
+
* @returns Whether the path's first segment is unavailable
|
|
48
|
+
* @example
|
|
49
|
+
* isReservedPath("archive"); // true
|
|
50
|
+
* isReservedPath("ja/hello", ["ja"]); // true
|
|
51
|
+
* isReservedPath("ja/hello"); // false — no language configured
|
|
39
52
|
*/
|
|
40
|
-
export function isReservedPath(
|
|
53
|
+
export function isReservedPath(
|
|
54
|
+
path: string,
|
|
55
|
+
languagePrefixes: readonly string[] = [],
|
|
56
|
+
): boolean {
|
|
41
57
|
const firstSegment = path.split("/")[0]?.toLowerCase();
|
|
42
|
-
|
|
58
|
+
if (!firstSegment) return false;
|
|
59
|
+
if (RESERVED_PATHS.includes(firstSegment as ReservedPath)) return true;
|
|
60
|
+
return languagePrefixes.includes(firstSegment);
|
|
43
61
|
}
|
|
44
62
|
|
|
45
63
|
/**
|
|
@@ -66,10 +84,17 @@ export const SETTINGS_KEYS = Object.fromEntries(
|
|
|
66
84
|
export type SettingsKey = SettingsFieldKey;
|
|
67
85
|
|
|
68
86
|
/**
|
|
69
|
-
* Onboarding status values
|
|
87
|
+
* Onboarding status values.
|
|
88
|
+
*
|
|
89
|
+
* `provisioned` is the hosted middle state: a control plane created the site
|
|
90
|
+
* and its owner, so the site is real and servable, but nobody has yet answered
|
|
91
|
+
* the questions only a person can answer — the language they write in. It is
|
|
92
|
+
* deliberately distinct from `pending`, which means the site has no owner at
|
|
93
|
+
* all and nothing but setup should be reachable.
|
|
70
94
|
*/
|
|
71
95
|
export const ONBOARDING_STATUS = {
|
|
72
96
|
PENDING: "pending",
|
|
97
|
+
PROVISIONED: "provisioned",
|
|
73
98
|
COMPLETED: "completed",
|
|
74
99
|
} as const;
|
|
75
100
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy field contract.
|
|
3
|
+
*
|
|
4
|
+
* A copy field is a read-only text input holding an address, with a button that
|
|
5
|
+
* puts it on the clipboard. It is rendered from two places — server JSX on the
|
|
6
|
+
* public subscribe page, a Lit template in General settings — so the hooks the
|
|
7
|
+
* client enhancer looks for and the classes that give the two the same shape
|
|
8
|
+
* are declared once, here, rather than written out twice.
|
|
9
|
+
*
|
|
10
|
+
* The behavior lives in `client/copy-field.ts`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Marks the button. Its value is the toast shown after a successful copy. */
|
|
14
|
+
export const COPY_FIELD_BUTTON_ATTR = "data-copy-field";
|
|
15
|
+
|
|
16
|
+
/** On the button. Its value is the toast shown when the clipboard refuses. */
|
|
17
|
+
export const COPY_FIELD_FAILED_ATTR = "data-copy-field-failed";
|
|
18
|
+
|
|
19
|
+
/** Marks the input holding the address to copy. */
|
|
20
|
+
export const COPY_FIELD_VALUE_ATTR = "data-copy-field-value";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The address input. Monospaced because these are URLs read character by
|
|
24
|
+
* character, and right-padded to clear the button sitting on top of it.
|
|
25
|
+
*/
|
|
26
|
+
export const COPY_FIELD_INPUT_CLASS = "input w-full pr-20 font-mono text-sm";
|
|
27
|
+
|
|
28
|
+
/** The button, laid over the input's right edge. */
|
|
29
|
+
export const COPY_FIELD_BUTTON_CLASS =
|
|
30
|
+
"btn-sm-outline absolute right-2 top-1/2 h-7 -translate-y-1/2 px-2.5 text-xs";
|
|
31
|
+
|
|
32
|
+
/** Wraps input and button so the button can be positioned against the input. */
|
|
33
|
+
export const COPY_FIELD_CONTROL_CLASS = "relative";
|
|
34
|
+
|
|
35
|
+
/** Wraps the whole field: label, optional description, then the control. */
|
|
36
|
+
export const COPY_FIELD_CLASS = "flex min-w-0 flex-col gap-1";
|
package/src/lib/crypto.ts
CHANGED
|
@@ -45,3 +45,43 @@ export function timingSafeEqualText(a: string, b: string): boolean {
|
|
|
45
45
|
const encoder = new TextEncoder();
|
|
46
46
|
return timingSafeEqualBytes(encoder.encode(a), encoder.encode(b));
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Derive a hex HMAC-SHA256 of `message` under `secret`.
|
|
51
|
+
*
|
|
52
|
+
* Deriving a value instead of storing one is what lets a token be handed out
|
|
53
|
+
* repeatedly without a server-side record: the same inputs always produce the
|
|
54
|
+
* same output, so re-issuing is free and issuing twice cannot invalidate the
|
|
55
|
+
* first copy.
|
|
56
|
+
*
|
|
57
|
+
* @param secret - Key material, never leaves the server
|
|
58
|
+
* @param message - Value being signed
|
|
59
|
+
* @returns Lowercase hex signature, 64 characters
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* const signature = await hmacHex(env.AUTH_SECRET, `delete-account:${sessionId}`);
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export async function hmacHex(
|
|
67
|
+
secret: string,
|
|
68
|
+
message: string,
|
|
69
|
+
): Promise<string> {
|
|
70
|
+
const encoder = new TextEncoder();
|
|
71
|
+
const key = await crypto.subtle.importKey(
|
|
72
|
+
"raw",
|
|
73
|
+
encoder.encode(secret),
|
|
74
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
75
|
+
false,
|
|
76
|
+
["sign"],
|
|
77
|
+
);
|
|
78
|
+
const signature = await crypto.subtle.sign(
|
|
79
|
+
"HMAC",
|
|
80
|
+
key,
|
|
81
|
+
encoder.encode(message),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
return Array.from(new Uint8Array(signature))
|
|
85
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
86
|
+
.join("");
|
|
87
|
+
}
|
package/src/lib/csp-builder.ts
CHANGED
|
@@ -71,7 +71,8 @@ export function buildCspDirectives(
|
|
|
71
71
|
} = input;
|
|
72
72
|
|
|
73
73
|
// Base script-src: same-origin, Datastar's `unsafe-eval` for data-on-* /
|
|
74
|
-
// data-signals expressions, blob: for media workers (heic-to, mediabunny
|
|
74
|
+
// data-signals expressions, blob: for media workers (heic-to, mediabunny,
|
|
75
|
+
// the inline image worker).
|
|
75
76
|
const scriptSrc = ["'self'", "'unsafe-eval'", "blob:"];
|
|
76
77
|
if (allowInlineScript) scriptSrc.push("'unsafe-inline'");
|
|
77
78
|
appendUnique(scriptSrc, assetOrigin);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background work that outlives the HTTP response that started it.
|
|
3
|
+
*
|
|
4
|
+
* Some work must not be waited on: a Telegram webhook has to ACK before the
|
|
5
|
+
* album it triggered is assembled, or Telegram serializes the next update in
|
|
6
|
+
* that chat behind it; a Discover announcement has to leave the settings save
|
|
7
|
+
* alone, because a directory being down is not a reason a save fails. Both
|
|
8
|
+
* want the same thing — start it, answer the request, let it finish.
|
|
9
|
+
*
|
|
10
|
+
* The shape is small but every part of it is load-bearing, which is why it is
|
|
11
|
+
* one function rather than four lines copied per call site:
|
|
12
|
+
*
|
|
13
|
+
* - **The promise carries its own handler.** Nothing is awaiting it, so a
|
|
14
|
+
* rejection has nobody to reject to. Left unhandled, Node ends the process.
|
|
15
|
+
* - **`waitUntil` is optional and its accessor throws.** On Workers it keeps
|
|
16
|
+
* the isolate alive until the promise settles. Under Node and in tests
|
|
17
|
+
* Hono's `c.executionCtx` getter throws rather than returning undefined, so
|
|
18
|
+
* reaching for it needs a `try`, and the promise is simply left to the event
|
|
19
|
+
* loop.
|
|
20
|
+
* - **Tests need a handle on it.** With no `waitUntil` there is nothing to
|
|
21
|
+
* await, so a test that has to assert on the result registers an array at
|
|
22
|
+
* `__pendingDeferred` on the env and gets every promise pushed into it.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* An env binding carrying the test hook.
|
|
27
|
+
*
|
|
28
|
+
* Registered by a test that needs to `await` deferred work before asserting —
|
|
29
|
+
* production bindings never have it, and the array is simply not found.
|
|
30
|
+
*/
|
|
31
|
+
export interface DeferredPendingEnv {
|
|
32
|
+
__pendingDeferred?: Promise<unknown>[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Start work that must finish after the response has been sent.
|
|
37
|
+
*
|
|
38
|
+
* Never throws and never rejects: a failure is logged under `label` and goes
|
|
39
|
+
* no further, because there is no caller left to tell.
|
|
40
|
+
*
|
|
41
|
+
* @param c - Request context, for the env binding and the runtime's
|
|
42
|
+
* background-work hook. A full Hono `Context` satisfies this.
|
|
43
|
+
* @param label - What this work is, for the log line when it fails. A short
|
|
44
|
+
* subject in the sentence "`<label>` background error" — "Telegram",
|
|
45
|
+
* "Discover announcement".
|
|
46
|
+
* @param work - The work itself. Its rejection is handled here.
|
|
47
|
+
* @returns Nothing — the point is that the caller does not wait.
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* runDeferred(c, "Discover announcement", async () => {
|
|
51
|
+
* await services.settings.announceToDiscover({ endpoint, feedUrl });
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function runDeferred(
|
|
56
|
+
c: {
|
|
57
|
+
env: object;
|
|
58
|
+
executionCtx?: { waitUntil: (promise: Promise<unknown>) => void };
|
|
59
|
+
},
|
|
60
|
+
label: string,
|
|
61
|
+
work: () => Promise<void>,
|
|
62
|
+
): void {
|
|
63
|
+
const promise = work().catch((error: unknown) => {
|
|
64
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
65
|
+
// eslint-disable-next-line no-console -- Background failures must be visible.
|
|
66
|
+
console.error(`[Jant] ${label} background error: ${message}`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
c.executionCtx?.waitUntil(promise);
|
|
71
|
+
} catch {
|
|
72
|
+
// executionCtx not available (e.g. Node, tests) — the promise still runs.
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const pending = (c.env as DeferredPendingEnv).__pendingDeferred;
|
|
76
|
+
if (Array.isArray(pending)) {
|
|
77
|
+
pending.push(promise);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Announcing a site to a Jant Discover directory.
|
|
3
|
+
*
|
|
4
|
+
* A directory cannot list a self-hosted site it has never heard of, and a site
|
|
5
|
+
* that publishes quietly has no reason to be found. The ping closes that gap
|
|
6
|
+
* with the smallest thing that works: one request, carrying one URL, sent only
|
|
7
|
+
* when someone deliberately turns the feature on.
|
|
8
|
+
*
|
|
9
|
+
* It is not a heartbeat and not a publish notification. It never fires
|
|
10
|
+
* periodically, never per post, and never while the setting is off. Whether a
|
|
11
|
+
* site is listed is still decided by what its own feed says, which the
|
|
12
|
+
* directory has to fetch and read for itself.
|
|
13
|
+
*
|
|
14
|
+
* This module is transport only. It reports what happened; recording it is the
|
|
15
|
+
* settings service's job, because an owner who cannot see whether the
|
|
16
|
+
* announcement got through has no way to tell "waiting" from "lost".
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** How long to wait before giving up on one attempt. */
|
|
20
|
+
const PING_TIMEOUT_MS = 5_000;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Attempts one announcement makes before it is left to the owner.
|
|
24
|
+
*
|
|
25
|
+
* Two, because the failure worth retrying automatically is the directory being
|
|
26
|
+
* mid-deploy or answering a transient 5xx. Anything that survives a second
|
|
27
|
+
* attempt — a blocked egress, a proxy eating the request, a directory that
|
|
28
|
+
* moved — is not going to be fixed by a third, and the settings page offers a
|
|
29
|
+
* Retry the owner can press when they have fixed it.
|
|
30
|
+
*/
|
|
31
|
+
const PING_ATTEMPTS = 2;
|
|
32
|
+
|
|
33
|
+
/** Wait between the two attempts. */
|
|
34
|
+
const PING_RETRY_DELAY_MS = 2_000;
|
|
35
|
+
|
|
36
|
+
/** What one announcement attempt came to. */
|
|
37
|
+
export interface DiscoverAnnounceOutcome {
|
|
38
|
+
/** Unix seconds when the attempt finished. */
|
|
39
|
+
at: number;
|
|
40
|
+
/** The directory acknowledged it. */
|
|
41
|
+
ok: boolean;
|
|
42
|
+
/** The feed address that was sent. */
|
|
43
|
+
feedUrl: string;
|
|
44
|
+
/** HTTP status, when there was a response at all. */
|
|
45
|
+
status?: number;
|
|
46
|
+
/** Why it failed, for the owner to read. Absent on success. */
|
|
47
|
+
error?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Tell the configured directory where this site's feed is.
|
|
52
|
+
*
|
|
53
|
+
* Resolves rather than throws: the caller runs this as background work behind
|
|
54
|
+
* a settings save, which must not fail because a directory is down.
|
|
55
|
+
*
|
|
56
|
+
* @param input - The directory endpoint, the feed to announce, and the clock
|
|
57
|
+
* @returns What the attempt came to, success or not
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* const outcome = await sendDiscoverPing({
|
|
61
|
+
* endpoint: "https://jant.me/api/discover/ping",
|
|
62
|
+
* feedUrl: "https://example.com/latest/feed",
|
|
63
|
+
* now: () => Math.floor(Date.now() / 1000),
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export async function sendDiscoverPing(input: {
|
|
68
|
+
endpoint: string;
|
|
69
|
+
feedUrl: string;
|
|
70
|
+
now: () => number;
|
|
71
|
+
/** Injectable so tests do not sit through the retry delay. */
|
|
72
|
+
sleep?: (ms: number) => Promise<void>;
|
|
73
|
+
}): Promise<DiscoverAnnounceOutcome> {
|
|
74
|
+
const sleep =
|
|
75
|
+
input.sleep ??
|
|
76
|
+
((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
|
|
77
|
+
|
|
78
|
+
let last: { status?: number; error: string } = {
|
|
79
|
+
error: "The announcement was not attempted.",
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
for (let attempt = 1; attempt <= PING_ATTEMPTS; attempt += 1) {
|
|
83
|
+
const result = await attemptPing(input.endpoint, input.feedUrl);
|
|
84
|
+
if (result.ok) {
|
|
85
|
+
return {
|
|
86
|
+
at: input.now(),
|
|
87
|
+
ok: true,
|
|
88
|
+
feedUrl: input.feedUrl,
|
|
89
|
+
status: result.status,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
last = {
|
|
93
|
+
error: result.error,
|
|
94
|
+
...(result.status ? { status: result.status } : {}),
|
|
95
|
+
};
|
|
96
|
+
if (attempt < PING_ATTEMPTS) await sleep(PING_RETRY_DELAY_MS);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
at: input.now(),
|
|
101
|
+
ok: false,
|
|
102
|
+
feedUrl: input.feedUrl,
|
|
103
|
+
...(last.status === undefined ? {} : { status: last.status }),
|
|
104
|
+
error: last.error,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function attemptPing(
|
|
109
|
+
endpoint: string,
|
|
110
|
+
feedUrl: string,
|
|
111
|
+
): Promise<
|
|
112
|
+
{ ok: true; status: number } | { ok: false; status?: number; error: string }
|
|
113
|
+
> {
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
const timer = setTimeout(() => controller.abort(), PING_TIMEOUT_MS);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
// The feed URL and nothing else. No identity, no version, no counts: a
|
|
119
|
+
// directory needs an address to fetch, and anything more would make this
|
|
120
|
+
// telemetry.
|
|
121
|
+
const response = await fetch(endpoint, {
|
|
122
|
+
method: "POST",
|
|
123
|
+
headers: { "Content-Type": "application/json" },
|
|
124
|
+
body: JSON.stringify({ feed: feedUrl }),
|
|
125
|
+
signal: controller.signal,
|
|
126
|
+
});
|
|
127
|
+
if (response.ok) return { ok: true, status: response.status };
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
status: response.status,
|
|
131
|
+
error: `The directory answered ${response.status}.`,
|
|
132
|
+
};
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: error instanceof Error ? error.message : String(error),
|
|
137
|
+
};
|
|
138
|
+
} finally {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
}
|
|
141
|
+
}
|