@jant/core 0.3.42 → 0.3.44
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/bin/commands/import-site.js +1 -1
- package/bin/commands/search-reindex.js +175 -0
- package/bin/lib/hugo-markdown.js +102 -0
- package/bin/lib/site-pull-media.js +1 -4
- package/dist/app-BI9bnCkO.js +5 -0
- package/dist/{app-Cu3lveYI.js → app-CtJDxZBb.js} +969 -373
- package/dist/client/.vite/manifest.json +2 -2
- package/dist/client/_assets/client-BQH7AQ24.css +2 -0
- package/dist/client/_assets/{client-auth-BRFl5zQA.js → client-auth-CXILhW1b.js} +144 -144
- package/dist/{env-wCpMcNXs.js → env-CgaH9Mut.js} +1 -1
- package/dist/{github-api-CficQztC.js → github-api-BkRWnqMx.js} +1 -1
- package/dist/{github-app-F4qZ05xk.js → github-app-WeadXMb8.js} +1 -1
- package/dist/{github-sync-zohnA9qv.js → github-sync-7y_nTXx1.js} +41 -14
- package/dist/index.js +5 -5
- package/dist/node.js +5 -5
- package/dist/{url-FvvgARU9.js → url-umUptr5z.js} +30 -1
- package/package.json +1 -1
- package/src/__tests__/helpers/app.ts +15 -4
- package/src/app.tsx +8 -0
- package/src/client/tiptap/__tests__/insert-paragraph-around.test.ts +228 -0
- package/src/client/tiptap/extensions.ts +3 -0
- package/src/client/tiptap/insert-paragraph-around.ts +79 -0
- package/src/db/migrations/0018_yummy_franklin_richards.sql +6 -0
- package/src/db/migrations/0019_bored_magus.sql +2 -0
- package/src/db/migrations/meta/0018_snapshot.json +2225 -0
- package/src/db/migrations/meta/0019_snapshot.json +2238 -0
- package/src/db/migrations/meta/_journal.json +15 -1
- package/src/db/migrations/pg/0016_familiar_lionheart.sql +6 -0
- package/src/db/migrations/pg/0017_bright_beyonder.sql +2 -0
- package/src/db/migrations/pg/meta/0016_snapshot.json +2840 -0
- package/src/db/migrations/pg/meta/0017_snapshot.json +2862 -0
- package/src/db/migrations/pg/meta/_journal.json +15 -1
- package/src/db/pg/schema.ts +22 -0
- package/src/db/schema.ts +27 -0
- package/src/index.ts +1 -2
- package/src/lib/__tests__/hosted-signin.test.ts +30 -0
- package/src/lib/__tests__/navigation.test.ts +4 -20
- package/src/lib/__tests__/rate-limit-d1.test.ts +82 -0
- package/src/lib/__tests__/rate-limit-memory.test.ts +69 -0
- package/src/lib/__tests__/summary.test.ts +140 -0
- package/src/lib/__tests__/view.test.ts +66 -0
- package/src/lib/feed.ts +70 -34
- package/src/lib/hosted-signin.ts +9 -3
- package/src/lib/icons.ts +37 -0
- package/src/lib/navigation.ts +11 -12
- package/src/lib/post-meta.ts +20 -2
- package/src/lib/rate-limit-d1.ts +99 -0
- package/src/lib/rate-limit-memory.ts +105 -0
- package/src/lib/rate-limit.ts +63 -0
- package/src/lib/render.tsx +9 -0
- package/src/lib/resolve-config.ts +9 -0
- package/src/lib/summary.ts +42 -7
- package/src/lib/url.ts +34 -0
- package/src/lib/view.ts +42 -8
- package/src/middleware/__tests__/auth.test.ts +44 -4
- package/src/middleware/__tests__/rate-limit.test.ts +113 -0
- package/src/middleware/__tests__/session.test.ts +85 -0
- package/src/middleware/auth.ts +62 -25
- package/src/middleware/rate-limit.ts +54 -0
- package/src/middleware/session.ts +36 -0
- package/src/routes/__tests__/compose.test.ts +1 -1
- package/src/routes/api/__tests__/search.test.ts +48 -0
- package/src/routes/api/__tests__/upload-multipart.test.ts +11 -4
- package/src/routes/api/internal/search-reindex.ts +40 -0
- package/src/routes/api/internal/sites.ts +1 -0
- package/src/routes/api/search.ts +13 -0
- package/src/routes/auth/dev.ts +1 -1
- package/src/routes/auth/signin.tsx +23 -5
- package/src/routes/dash/settings.tsx +3 -5
- package/src/routes/feed/__tests__/sitemap.test.ts +320 -4
- package/src/routes/feed/sitemap.ts +208 -33
- package/src/routes/pages/__tests__/page-canonical.test.ts +101 -0
- package/src/routes/pages/home.tsx +24 -15
- package/src/routes/pages/page.tsx +34 -0
- package/src/routes/pages/partials.tsx +4 -15
- package/src/runtime/cloudflare.ts +4 -0
- package/src/runtime/node.ts +16 -0
- package/src/services/__tests__/post.test.ts +205 -0
- package/src/services/__tests__/search.test.ts +44 -0
- package/src/services/__tests__/site-admin.test.ts +85 -0
- package/src/services/export.ts +9 -2
- package/src/services/post.ts +200 -2
- package/src/services/site-admin.ts +66 -1
- package/src/styles/ui.css +12 -0
- package/src/types/app-context.ts +20 -0
- package/src/types/config.ts +8 -0
- package/src/types/props.ts +0 -7
- package/src/ui/feed/LinkCard.tsx +3 -20
- package/src/ui/feed/LinkPreview.tsx +5 -19
- package/src/ui/feed/PostStatusBadges.tsx +4 -38
- package/src/ui/layouts/BaseLayout.tsx +23 -29
- package/src/ui/shared/DecorativeQuoteMark.tsx +2 -13
- package/src/ui/shared/Icon.tsx +60 -0
- package/src/ui/shared/IconSprite.tsx +57 -0
- package/src/ui/shared/PostFooter.tsx +6 -62
- package/src/ui/shared/custom-icons.ts +132 -0
- package/src/ui/shared/icon-collector.ts +37 -0
- package/dist/app-DzCB4yOp.js +0 -5
- package/dist/client/_assets/client-C_kImWZj.css +0 -2
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { A as
|
|
3
|
-
import { C as coalesceDisplayText, S as shouldUseSecureCookies, _ as getInternalAdminToken, a as getConfiguredSingleSiteUrl, b as getSiteResolutionMode, c as getDevApiToken, d as getHostedControlPlaneBaseUrl, f as getHostedControlPlaneDomainCheckSecret, g as getHostedControlPlaneSsoSecret, h as getHostedControlPlaneProviderLabel$1, i as getConfiguredSingleSitePathPrefix, l as getEnvString, m as getHostedControlPlaneInternalToken, n as getAuthSecret, o as getConfiguredStorageDriver, p as getHostedControlPlaneInternalBaseUrl, r as getConfiguredSingleSiteOrigin, s as getCorsOrigins, u as getGitHubAppConfig, v as getLocalStoragePath } from "./env-
|
|
4
|
-
import { a as listInstallationReposPage, n as getInstallation, o as searchInstallationRepos, t as buildInstallUrl } from "./github-app-
|
|
5
|
-
import { r as parseRepoSlug, t as createGitHubClient } from "./github-api-
|
|
1
|
+
import { a as getSitePathPrefix, c as normalizePath, d as sanitizeUrl, f as slugify, g as toPublicPath, h as toPublicHref, i as getSiteOrigin, m as toAbsoluteSiteUrl, n as extractDisplayDomain, o as isFullUrl, p as stripSitePathPrefix, r as extractDomain, s as isSafeInternalRedirect, t as buildSiteUrl, u as normalizeSiteUrl, v as __exportAll } from "./url-umUptr5z.js";
|
|
2
|
+
import { A as JANT_BRAND_PACK_FILENAME, B as getJantLogoFills, C as formatTime, D as toISOString, F as getJantBrandPackHref, G as arrayBufferToBase64, H as getJantPositiveLogoPngHref, I as getJantBundledAsset, K as base64ToUint8Array, L as getJantIconFilename, M as JANT_REPO_URL, N as getDefaultJantAppleTouchIconBytes, O as HOME_BRANDING_LINK_LABEL, P as getDefaultJantFaviconIcoBytes, R as getJantIconHref, S as formatRelativeTime, T as now, U as JANT_LOGO_PATH_DATA, V as getJantLogoHref, W as JANT_LOGO_VIEW_BOX, _ as getImageUrl, a as tiptapJsonToMarkdown, b as formatDate, c as render, d as extractSummary, f as extractSummaryHtml, g as escapeHtml, h as trimTiptapBody, i as createExportService, j as JANT_POSITIVE_LOGO_PNG_FILENAME, k as HOME_BRANDING_PREFIX, l as toPlainText, m as renderTiptapJson, n as createGitHubSyncService, o as markdownToTiptapJson, p as renderTiptapDocument, u as extractBodyText, v as getMediaUrl, w as formatYearMonth, x as formatRelativeAge, y as getPublicUrlForProvider, z as getJantLogoFilename } from "./github-sync-7y_nTXx1.js";
|
|
3
|
+
import { C as coalesceDisplayText, S as shouldUseSecureCookies, _ as getInternalAdminToken, a as getConfiguredSingleSiteUrl, b as getSiteResolutionMode, c as getDevApiToken, d as getHostedControlPlaneBaseUrl, f as getHostedControlPlaneDomainCheckSecret, g as getHostedControlPlaneSsoSecret, h as getHostedControlPlaneProviderLabel$1, i as getConfiguredSingleSitePathPrefix, l as getEnvString, m as getHostedControlPlaneInternalToken, n as getAuthSecret, o as getConfiguredStorageDriver, p as getHostedControlPlaneInternalBaseUrl, r as getConfiguredSingleSiteOrigin, s as getCorsOrigins, u as getGitHubAppConfig, v as getLocalStoragePath } from "./env-CgaH9Mut.js";
|
|
4
|
+
import { a as listInstallationReposPage, n as getInstallation, o as searchInstallationRepos, t as buildInstallUrl } from "./github-app-WeadXMb8.js";
|
|
5
|
+
import { r as parseRepoSlug, t as createGitHubClient } from "./github-api-BkRWnqMx.js";
|
|
6
6
|
import { I18n } from "@lingui/core";
|
|
7
|
+
import * as lucideIcons from "lucide-static";
|
|
7
8
|
import { z } from "zod";
|
|
8
9
|
import { fromString, typeidUnboxed } from "typeid-js";
|
|
9
10
|
import { decode } from "blurhash";
|
|
10
|
-
import { and, asc, desc, eq, inArray, isNotNull, isNull, lt, lte, ne, or, sql } from "drizzle-orm";
|
|
11
|
+
import { and, asc, desc, eq, gt, inArray, isNotNull, isNull, lt, lte, ne, or, sql } from "drizzle-orm";
|
|
11
12
|
import { generateKeyBetween } from "fractional-indexing";
|
|
12
13
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
13
14
|
import { drizzle as drizzle$1 } from "drizzle-orm/d1";
|
|
14
15
|
import { check, foreignKey, index, integer, primaryKey, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
|
|
15
16
|
import { boolean, check as check$1, customType, foreignKey as foreignKey$1, index as index$1, integer as integer$1, pgTable, primaryKey as primaryKey$1, text as text$1, timestamp, unique, uniqueIndex as uniqueIndex$1 } from "drizzle-orm/pg-core";
|
|
16
|
-
import * as lucideIcons from "lucide-static";
|
|
17
17
|
import { APIError, betterAuth } from "better-auth";
|
|
18
18
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
19
19
|
import { verifyPassword } from "better-auth/crypto";
|
|
@@ -3352,15 +3352,289 @@ function normalizeThemeColorForMeta(color) {
|
|
|
3352
3352
|
* internal paths (e.g. `/_assets/client-HASH.js`) embedded by the Worker build
|
|
3353
3353
|
* from the Vite client manifest. Used only in production (IS_VITE_DEV=false).
|
|
3354
3354
|
*/ var IS_VITE_DEV = typeof __JANT_DEV__ !== "undefined" && __JANT_DEV__ === true;
|
|
3355
|
-
var CORE_VERSION = "0.3.
|
|
3355
|
+
var CORE_VERSION = "0.3.44-c595e1fa6d741ba8";
|
|
3356
3356
|
var CLIENT_JS_FILE = "/_assets/client-D95FNDg5.js";
|
|
3357
|
-
var CLIENT_AUTH_JS_FILE = "/_assets/client-auth-
|
|
3358
|
-
var CLIENT_CSS_FILE = "/_assets/client-
|
|
3357
|
+
var CLIENT_AUTH_JS_FILE = "/_assets/client-auth-CXILhW1b.js";
|
|
3358
|
+
var CLIENT_CSS_FILE = "/_assets/client-BQH7AQ24.css";
|
|
3359
3359
|
var CLIENT_CJK_CSS_FILE = "/_assets/client-cjk-B7Z0snDu.css";
|
|
3360
3360
|
var CLIENT_CJK_TC_CSS_FILE = "/_assets/client-cjk-tc-BesJYrb2.css";
|
|
3361
3361
|
var CLIENT_CJK_JP_CSS_FILE = "/_assets/client-cjk-jp-DZwrTzQC.css";
|
|
3362
3362
|
var CLIENT_CJK_KR_CSS_FILE = "/_assets/client-cjk-kr-_3ZNI2ZP.css";
|
|
3363
3363
|
//#endregion
|
|
3364
|
+
//#region src/ui/shared/icon-collector.ts
|
|
3365
|
+
/**
|
|
3366
|
+
* Request-scoped icon collector for SSR SVG sprite pattern.
|
|
3367
|
+
*
|
|
3368
|
+
* The <Icon> component registers each used icon name here during render.
|
|
3369
|
+
* At the end of <body>, <IconSprite> reads the collected set and emits a
|
|
3370
|
+
* single <svg><symbol>...</symbol></svg> block so every <use href="#icon-x">
|
|
3371
|
+
* reference in the page resolves to a definition.
|
|
3372
|
+
*
|
|
3373
|
+
* Mirrors the I18nProvider pattern in i18n/context.tsx: Hono JSX renders
|
|
3374
|
+
* synchronously per request, so a module-level singleton is safe.
|
|
3375
|
+
*/ var currentCollector = null;
|
|
3376
|
+
/**
|
|
3377
|
+
* Start a new collection scope for the current render pass.
|
|
3378
|
+
* Call at the top of the root layout before children render.
|
|
3379
|
+
*/ function resetIconCollector() {
|
|
3380
|
+
currentCollector = /* @__PURE__ */ new Set();
|
|
3381
|
+
}
|
|
3382
|
+
/**
|
|
3383
|
+
* Register an icon as used during this render.
|
|
3384
|
+
* Safe to call even when no collector is active (no-op).
|
|
3385
|
+
*/ function collectIcon(name) {
|
|
3386
|
+
currentCollector?.add(name);
|
|
3387
|
+
}
|
|
3388
|
+
/**
|
|
3389
|
+
* Get the icon names collected so far during this render.
|
|
3390
|
+
* Returns an empty set if no collection scope is active.
|
|
3391
|
+
*/ function getCollectedIcons() {
|
|
3392
|
+
return currentCollector ?? /* @__PURE__ */ new Set();
|
|
3393
|
+
}
|
|
3394
|
+
//#endregion
|
|
3395
|
+
//#region src/lib/featured-icons.ts
|
|
3396
|
+
/**
|
|
3397
|
+
* Shared icon definitions for featured post affordances.
|
|
3398
|
+
*
|
|
3399
|
+
* These paths are reused across Hono JSX, Lit, and exported static markup so
|
|
3400
|
+
* "Featured" keeps one visual language everywhere.
|
|
3401
|
+
*/ var FEATURED_SPARKLE_PATH = "M12 3 10.1 10.1 3 12l7.1 1.9L12 21l1.9-7.1L21 12l-7.1-1.9Z";
|
|
3402
|
+
var FEATURED_SPARKLE_OFF_SLASH_PATH = "M4 4 20 20";
|
|
3403
|
+
/**
|
|
3404
|
+
* Build inline SVG markup for the shared featured sparkle icon.
|
|
3405
|
+
*
|
|
3406
|
+
* @param options - Render options for the sparkle icon.
|
|
3407
|
+
* @returns SVG markup string for inline insertion.
|
|
3408
|
+
* @example
|
|
3409
|
+
* getFeaturedIconSvg({ off: true, className: "icon-fine" });
|
|
3410
|
+
*/ function getFeaturedIconSvg(options = {}) {
|
|
3411
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"${options.className ? ` class="${options.className}"` : ""} aria-hidden="true"><path d="${FEATURED_SPARKLE_PATH}" />${options.off ? `<path d="${FEATURED_SPARKLE_OFF_SLASH_PATH}" />` : ""}</svg>`;
|
|
3412
|
+
}
|
|
3413
|
+
//#endregion
|
|
3414
|
+
//#region src/lib/decorative-quote-mark.ts
|
|
3415
|
+
var DECORATIVE_QUOTE_MARK_VIEWBOX = "0 0 96 96";
|
|
3416
|
+
var DECORATIVE_QUOTE_MARK_PATHS = ["M24.4 10.5C16.9 17.7 11.5 26.8 8.2 37.7C4.9 48.7 4.8 58.9 7.8 68.2C10.3 75.7 15.4 79.5 22.9 79.5C28 79.5 32.2 77.8 35.4 74.2C38.6 70.7 40.2 66.5 40.2 61.4C40.2 56.5 38.8 52.6 36 49.6C33.3 46.6 29.7 45.1 25.2 45.1C23.4 45.1 21.8 45.3 20.2 45.8C22.2 37.3 26.7 29.2 33.6 21.4L24.4 10.5Z", "M60.8 10.5C53.3 17.7 47.9 26.8 44.6 37.7C41.3 48.7 41.2 58.9 44.2 68.2C46.7 75.7 51.8 79.5 59.3 79.5C64.4 79.5 68.6 77.8 71.8 74.2C75 70.7 76.6 66.5 76.6 61.4C76.6 56.5 75.2 52.6 72.4 49.6C69.7 46.6 66.1 45.1 61.6 45.1C59.8 45.1 58.2 45.3 56.6 45.8C58.6 37.3 63.1 29.2 70 21.4L60.8 10.5Z"];
|
|
3417
|
+
DECORATIVE_QUOTE_MARK_PATHS.map((path) => `<path fill="currentColor" d="${path}" />`).join("");
|
|
3418
|
+
//#endregion
|
|
3419
|
+
//#region src/ui/shared/custom-icons.ts
|
|
3420
|
+
/**
|
|
3421
|
+
* Custom (non-lucide) SVG symbol definitions used by the icon sprite.
|
|
3422
|
+
*
|
|
3423
|
+
* Icons here fall into three groups:
|
|
3424
|
+
* 1. Jant-specific paths (decorative quote mark, featured sparkle).
|
|
3425
|
+
* 2. Lucide-equivalent paths the UI uses with non-default stroke widths
|
|
3426
|
+
* or sizes that don't match the stock lucide symbol (we keep them as
|
|
3427
|
+
* custom symbols to preserve exact visual fidelity during refactor).
|
|
3428
|
+
* 3. Fixed-color SVGs (video play overlay) that don't use currentColor.
|
|
3429
|
+
*
|
|
3430
|
+
* Each entry provides everything needed to render a <symbol> element:
|
|
3431
|
+
* <symbol id="icon-${name}" viewBox={viewBox}>{inner}</symbol>
|
|
3432
|
+
* Consumers of <Icon name="..."> pass `size` / `className` on the outer
|
|
3433
|
+
* <svg><use/></svg>; `<symbol>` children inherit the outer attributes.
|
|
3434
|
+
*/ var STROKE_THIN = "fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.35\" stroke-linecap=\"round\" stroke-linejoin=\"round\"";
|
|
3435
|
+
var STROKE_POST_BADGE = "fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.75\" stroke-linecap=\"round\" stroke-linejoin=\"round\"";
|
|
3436
|
+
var CUSTOM_SYMBOLS = {
|
|
3437
|
+
"featured-sparkle": {
|
|
3438
|
+
viewBox: "0 0 24 24",
|
|
3439
|
+
inner: `<path ${STROKE_THIN} d="${FEATURED_SPARKLE_PATH}" />`
|
|
3440
|
+
},
|
|
3441
|
+
"featured-sparkle-off": {
|
|
3442
|
+
viewBox: "0 0 24 24",
|
|
3443
|
+
inner: `<path ${STROKE_THIN} d="${FEATURED_SPARKLE_PATH}" /><path ${STROKE_THIN} d="${FEATURED_SPARKLE_OFF_SLASH_PATH}" />`
|
|
3444
|
+
},
|
|
3445
|
+
"decorative-quote": {
|
|
3446
|
+
viewBox: DECORATIVE_QUOTE_MARK_VIEWBOX,
|
|
3447
|
+
inner: DECORATIVE_QUOTE_MARK_PATHS.map((path) => `<path fill="currentColor" d="${path}" />`).join("")
|
|
3448
|
+
},
|
|
3449
|
+
"post-collection-lock": {
|
|
3450
|
+
viewBox: "0 0 16 16",
|
|
3451
|
+
inner: `<rect ${STROKE_THIN} x="3" y="5.05" width="10" height="8.15" rx="2.2" /><path ${STROKE_THIN} d="M5.1 5.05V4.2a1.1 1.1 0 0 1 1.1-1.1h3.6a1.1 1.1 0 0 1 1.1 1.1v.85" />`
|
|
3452
|
+
},
|
|
3453
|
+
"post-menu-dots": {
|
|
3454
|
+
viewBox: "0 0 24 24",
|
|
3455
|
+
inner: `<circle cx="5" cy="12" r="1.75" fill="currentColor" /><circle cx="12" cy="12" r="1.75" fill="currentColor" /><circle cx="19" cy="12" r="1.75" fill="currentColor" />`
|
|
3456
|
+
},
|
|
3457
|
+
"post-external-link": {
|
|
3458
|
+
viewBox: "0 0 24 24",
|
|
3459
|
+
inner: `<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M7 17 17 7" /><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M9 7h8v8" />`
|
|
3460
|
+
},
|
|
3461
|
+
"post-reply": {
|
|
3462
|
+
viewBox: "0 0 24 24",
|
|
3463
|
+
inner: `<polyline fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" points="9 17 4 12 9 7" /><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M20 18v-2a4 4 0 0 0-4-4H4" />`
|
|
3464
|
+
},
|
|
3465
|
+
"link-domain": {
|
|
3466
|
+
viewBox: "0 0 24 24",
|
|
3467
|
+
inner: `<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />`
|
|
3468
|
+
},
|
|
3469
|
+
"link-preview-play": {
|
|
3470
|
+
viewBox: "0 0 68 48",
|
|
3471
|
+
inner: "<path class=\"link-preview-play-bg\" fill=\"rgba(0,0,0,.65)\" d=\"M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55C3.97 2.33 2.27 4.81 1.48 7.74.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z\" /><path fill=\"#fff\" d=\"M45 24L27 14v20\" />"
|
|
3472
|
+
},
|
|
3473
|
+
"link-preview-badge-play": {
|
|
3474
|
+
viewBox: "0 0 16 16",
|
|
3475
|
+
inner: "<path fill=\"currentColor\" d=\"M5.5 3.5v9l7-4.5z\" />"
|
|
3476
|
+
},
|
|
3477
|
+
"toast-success": {
|
|
3478
|
+
viewBox: "0 0 24 24",
|
|
3479
|
+
inner: `<circle fill="none" stroke="currentColor" stroke-width="2" cx="12" cy="12" r="10" /><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="m9 12 2 2 4-4" />`
|
|
3480
|
+
},
|
|
3481
|
+
"toast-error": {
|
|
3482
|
+
viewBox: "0 0 24 24",
|
|
3483
|
+
inner: `<circle fill="none" stroke="currentColor" stroke-width="2" cx="12" cy="12" r="10" /><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="m15 9-6 6M9 9l6 6" />`
|
|
3484
|
+
},
|
|
3485
|
+
"toast-close": {
|
|
3486
|
+
viewBox: "0 0 24 24",
|
|
3487
|
+
inner: `<path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M18 6 6 18M6 6l12 12" />`
|
|
3488
|
+
},
|
|
3489
|
+
"post-status-pin": {
|
|
3490
|
+
viewBox: "0 0 24 24",
|
|
3491
|
+
inner: `<line ${STROKE_POST_BADGE} x1="12" x2="12" y1="17" y2="22" /><path ${STROKE_POST_BADGE} d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z" />`
|
|
3492
|
+
},
|
|
3493
|
+
"post-status-private": {
|
|
3494
|
+
viewBox: "0 0 24 24",
|
|
3495
|
+
inner: `<path ${STROKE_POST_BADGE} d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" /><path ${STROKE_POST_BADGE} d="M14.084 14.158a3 3 0 0 1-4.242-4.242" /><path ${STROKE_POST_BADGE} d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" /><path ${STROKE_POST_BADGE} d="m2 2 20 20" />`
|
|
3496
|
+
}
|
|
3497
|
+
};
|
|
3498
|
+
function getCustomSymbol(name) {
|
|
3499
|
+
return CUSTOM_SYMBOLS[name] ?? null;
|
|
3500
|
+
}
|
|
3501
|
+
/**
|
|
3502
|
+
* Return the viewBox for an icon's outer <svg> wrapper.
|
|
3503
|
+
*
|
|
3504
|
+
* This must match the <symbol>'s viewBox so the browser computes the correct
|
|
3505
|
+
* intrinsic aspect ratio. Without this, outer <svg> with `height: auto` in
|
|
3506
|
+
* CSS falls back to the 300×150 replaced-element default instead of the
|
|
3507
|
+
* icon's real aspect ratio.
|
|
3508
|
+
*
|
|
3509
|
+
* Falls back to lucide's "0 0 24 24" for lucide-sourced icons.
|
|
3510
|
+
*/ function getIconViewBox(name) {
|
|
3511
|
+
return CUSTOM_SYMBOLS[name]?.viewBox ?? "0 0 24 24";
|
|
3512
|
+
}
|
|
3513
|
+
//#endregion
|
|
3514
|
+
//#region src/ui/shared/Icon.tsx
|
|
3515
|
+
/**
|
|
3516
|
+
* <Icon> — sprite-based SVG icon for SSR pages.
|
|
3517
|
+
*
|
|
3518
|
+
* Renders a lightweight <svg><use href="#icon-${name}"/></svg> stub and
|
|
3519
|
+
* registers the icon name with the request-scoped collector so the final
|
|
3520
|
+
* sprite (rendered by <IconSprite>) contains exactly the icons used on
|
|
3521
|
+
* this page.
|
|
3522
|
+
*
|
|
3523
|
+
* Name can refer to any lucide-static icon (kebab-case) or one of the
|
|
3524
|
+
* custom symbols defined in `custom-icons.ts`. Unknown names render an
|
|
3525
|
+
* empty <svg> — the same failure mode as the previous getIconSvg() path.
|
|
3526
|
+
*
|
|
3527
|
+
* Size: outer <svg> width/height in pixels. Defaults to 24 (lucide default).
|
|
3528
|
+
* Pass `class` to add CSS classes, e.g. for sizing via stylesheet instead
|
|
3529
|
+
* of inline width/height.
|
|
3530
|
+
*/ var Icon$1 = ({ name, size, class: cls, "aria-label": ariaLabel, "aria-hidden": ariaHidden }) => {
|
|
3531
|
+
collectIcon(name);
|
|
3532
|
+
const hidden = ariaHidden ?? (ariaLabel ? void 0 : true);
|
|
3533
|
+
return /* @__PURE__ */ jsxDEV$1("svg", {
|
|
3534
|
+
viewBox: getIconViewBox(name),
|
|
3535
|
+
...size !== void 0 ? {
|
|
3536
|
+
width: size,
|
|
3537
|
+
height: size
|
|
3538
|
+
} : {},
|
|
3539
|
+
...cls ? { class: cls } : {},
|
|
3540
|
+
...ariaLabel ? {
|
|
3541
|
+
"aria-label": ariaLabel,
|
|
3542
|
+
role: "img"
|
|
3543
|
+
} : {},
|
|
3544
|
+
...hidden ? { "aria-hidden": "true" } : {},
|
|
3545
|
+
children: /* @__PURE__ */ jsxDEV$1("use", { href: `#icon-${name}` })
|
|
3546
|
+
});
|
|
3547
|
+
};
|
|
3548
|
+
//#endregion
|
|
3549
|
+
//#region src/lib/icons.ts
|
|
3550
|
+
/**
|
|
3551
|
+
* Shared icon utilities.
|
|
3552
|
+
*
|
|
3553
|
+
* Provides a small wrapper around lucide-static so server-rendered UI can fetch
|
|
3554
|
+
* SVG markup by kebab-case icon name.
|
|
3555
|
+
*/
|
|
3556
|
+
/**
|
|
3557
|
+
* Convert a kebab-case icon name to PascalCase for lucide-static lookup.
|
|
3558
|
+
*
|
|
3559
|
+
* @param name - Kebab-case icon name such as "book-open"
|
|
3560
|
+
* @returns PascalCase name such as "BookOpen"
|
|
3561
|
+
*/ function toPascalCase(name) {
|
|
3562
|
+
return name.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
3563
|
+
}
|
|
3564
|
+
/**
|
|
3565
|
+
* Get SVG markup for a Lucide icon by kebab-case name.
|
|
3566
|
+
*
|
|
3567
|
+
* @param name - Kebab-case icon name
|
|
3568
|
+
* @returns SVG string or null when the icon is unknown
|
|
3569
|
+
*
|
|
3570
|
+
* @example
|
|
3571
|
+
* ```ts
|
|
3572
|
+
* getIconSvg("book-open");
|
|
3573
|
+
* ```
|
|
3574
|
+
*/ function getIconSvg(name) {
|
|
3575
|
+
const svg = lucideIcons[toPascalCase(name)];
|
|
3576
|
+
return typeof svg === "string" ? svg : null;
|
|
3577
|
+
}
|
|
3578
|
+
/**
|
|
3579
|
+
* Get the inner SVG contents for a Lucide icon (the path children only,
|
|
3580
|
+
* without the outer <svg> wrapper). Used by the icon sprite to build
|
|
3581
|
+
* <symbol> definitions.
|
|
3582
|
+
*
|
|
3583
|
+
* @param name - Kebab-case icon name
|
|
3584
|
+
* @returns Inner SVG markup (e.g. "<path ... />"), or null when unknown
|
|
3585
|
+
*
|
|
3586
|
+
* @example
|
|
3587
|
+
* ```ts
|
|
3588
|
+
* getIconInnerSvg("book-open");
|
|
3589
|
+
* // -> "<path d=\"...\"/><path d=\"...\"/>"
|
|
3590
|
+
* ```
|
|
3591
|
+
*/ function getIconInnerSvg(name) {
|
|
3592
|
+
const svg = getIconSvg(name);
|
|
3593
|
+
if (!svg) return null;
|
|
3594
|
+
const openTagEnd = svg.indexOf(">");
|
|
3595
|
+
const closeTagStart = svg.lastIndexOf("</svg>");
|
|
3596
|
+
if (openTagEnd < 0 || closeTagStart < 0) return null;
|
|
3597
|
+
return svg.slice(openTagEnd + 1, closeTagStart);
|
|
3598
|
+
}
|
|
3599
|
+
/**
|
|
3600
|
+
* Default stroke/fill attributes inherited by <symbol> children when a
|
|
3601
|
+
* lucide icon is referenced via <use>. These mirror the attributes lucide
|
|
3602
|
+
* normally sets on the outer <svg> so the currentColor-based theming keeps
|
|
3603
|
+
* working through <use>.
|
|
3604
|
+
*/ var LUCIDE_SYMBOL_ATTRS = "fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"";
|
|
3605
|
+
var LUCIDE_VIEWBOX = "0 0 24 24";
|
|
3606
|
+
//#endregion
|
|
3607
|
+
//#region src/ui/shared/IconSprite.tsx
|
|
3608
|
+
/**
|
|
3609
|
+
* <IconSprite> — emits the SVG symbol definitions used by this render.
|
|
3610
|
+
*
|
|
3611
|
+
* Must be rendered AFTER all <Icon> usages in the document (e.g. at the
|
|
3612
|
+
* end of <body>) so the collector has the full set of icon names. Hono
|
|
3613
|
+
* JSX stringifies synchronously in document order, so children declared
|
|
3614
|
+
* earlier in the tree are evaluated before this component.
|
|
3615
|
+
*
|
|
3616
|
+
* <use href="#icon-x"> anywhere in the document resolves correctly even
|
|
3617
|
+
* when the <symbol> definition comes after the reference, since browsers
|
|
3618
|
+
* wire up the references after the full document is parsed.
|
|
3619
|
+
*/ function buildSymbol(name) {
|
|
3620
|
+
const custom = getCustomSymbol(name);
|
|
3621
|
+
if (custom) return `<symbol id="icon-${name}" viewBox="${custom.viewBox}">${custom.inner}</symbol>`;
|
|
3622
|
+
const inner = getIconInnerSvg(name);
|
|
3623
|
+
if (inner === null) return null;
|
|
3624
|
+
return `<symbol id="icon-${name}" viewBox="${LUCIDE_VIEWBOX}" ${LUCIDE_SYMBOL_ATTRS}>${inner}</symbol>`;
|
|
3625
|
+
}
|
|
3626
|
+
var IconSprite = () => {
|
|
3627
|
+
const symbols = Array.from(getCollectedIcons()).sort().map(buildSymbol).filter((s) => s !== null).join("");
|
|
3628
|
+
if (!symbols) return null;
|
|
3629
|
+
return /* @__PURE__ */ jsxDEV$1("svg", {
|
|
3630
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
3631
|
+
style: "display:none",
|
|
3632
|
+
"aria-hidden": "true",
|
|
3633
|
+
"data-icon-sprite": true,
|
|
3634
|
+
children: raw(symbols)
|
|
3635
|
+
});
|
|
3636
|
+
};
|
|
3637
|
+
//#endregion
|
|
3364
3638
|
//#region src/ui/layouts/BaseLayout.tsx
|
|
3365
3639
|
/**
|
|
3366
3640
|
* Base HTML Layout
|
|
@@ -3370,7 +3644,8 @@ var CLIENT_CJK_KR_CSS_FILE = "/_assets/client-cjk-kr-_3ZNI2ZP.css";
|
|
|
3370
3644
|
*
|
|
3371
3645
|
* In dev mode (Vite), serves assets via Vite's dev server.
|
|
3372
3646
|
* In production, serves pre-built assets with content-hashed filenames.
|
|
3373
|
-
*/ var BaseLayout = ({ title, description, lang, c, toast, faviconHref, appleTouchHref, faviconUrl, faviconVersion, socialImageUrl, noindex, isAuthenticated = false, clientBundle, children }) => {
|
|
3647
|
+
*/ var BaseLayout = ({ title, description, lang, c, toast, faviconHref, appleTouchHref, faviconUrl, faviconVersion, socialImageUrl, canonicalHref, noindex, isAuthenticated = false, clientBundle, children }) => {
|
|
3648
|
+
resetIconCollector();
|
|
3374
3649
|
const resolvedLang = lang ?? (c ? c.get("lang") : "en");
|
|
3375
3650
|
const appConfig = c ? c.get("appConfig") : void 0;
|
|
3376
3651
|
const resolvedSocialImagePath = socialImageUrl ?? faviconUrl ?? appConfig?.siteAvatarUrl ?? getJantIconHref("socialImage", appConfig?.sitePathPrefix || "");
|
|
@@ -3398,7 +3673,7 @@ var CLIENT_CJK_KR_CSS_FILE = "/_assets/client-cjk-kr-_3ZNI2ZP.css";
|
|
|
3398
3673
|
const cjkSerifFont = appConfig?.cjkSerifFont ?? "off";
|
|
3399
3674
|
const cjkStylesheetPath = cjkSerifFont === "zh-Hans" ? IS_VITE_DEV ? assetPath("/src/style-cjk.css") : toPublicAssetPath(CLIENT_CJK_CSS_FILE, assetBasePath) : cjkSerifFont === "zh-Hant" ? IS_VITE_DEV ? assetPath("/src/style-cjk-tc.css") : toPublicAssetPath(CLIENT_CJK_TC_CSS_FILE, assetBasePath) : cjkSerifFont === "ja" ? IS_VITE_DEV ? assetPath("/src/style-cjk-jp.css") : toPublicAssetPath(CLIENT_CJK_JP_CSS_FILE, assetBasePath) : cjkSerifFont === "ko" ? IS_VITE_DEV ? assetPath("/src/style-cjk-kr.css") : toPublicAssetPath(CLIENT_CJK_KR_CSS_FILE, assetBasePath) : null;
|
|
3400
3675
|
const clientScriptPath = IS_VITE_DEV ? resolvedClientBundle === "full" ? assetPath("/src/client-auth.ts") : assetPath("/src/client.ts") : toPublicAssetPath(resolvedClientBundle === "full" ? CLIENT_AUTH_JS_FILE : CLIENT_JS_FILE, assetBasePath);
|
|
3401
|
-
const faviconAssetVersion = resolvedFaviconVersion || "0.3.
|
|
3676
|
+
const faviconAssetVersion = resolvedFaviconVersion || "0.3.44-c595e1fa6d741ba8";
|
|
3402
3677
|
const resolvedFaviconHref = faviconHref ?? (faviconAssetVersion ? toPublicPath(`/favicon.ico?v=${faviconAssetVersion}`, sitePathPrefix) : toPublicPath("/favicon.ico", sitePathPrefix));
|
|
3403
3678
|
const resolvedAppleTouchHref = appleTouchHref ?? (faviconAssetVersion ? toPublicPath(`/apple-touch-icon.png?v=${faviconAssetVersion}`, sitePathPrefix) : toPublicPath("/apple-touch-icon.png", sitePathPrefix));
|
|
3404
3679
|
const socialImageHref = resolvedSocialImagePath && (isFullUrl(resolvedSocialImagePath) || resolvedSocialImagePath.startsWith("//") ? resolvedSocialImagePath : toAbsoluteSiteUrl(resolvedSocialImagePath, appConfig?.siteUrl || "", sitePathPrefix));
|
|
@@ -3497,6 +3772,10 @@ var CLIENT_CJK_KR_CSS_FILE = "/_assets/client-cjk-kr-_3ZNI2ZP.css";
|
|
|
3497
3772
|
name: "robots",
|
|
3498
3773
|
content: "noindex, nofollow"
|
|
3499
3774
|
}),
|
|
3775
|
+
canonicalHref && /* @__PURE__ */ jsxDEV$1("link", {
|
|
3776
|
+
rel: "canonical",
|
|
3777
|
+
href: canonicalHref
|
|
3778
|
+
}),
|
|
3500
3779
|
/* @__PURE__ */ jsxDEV$1("link", {
|
|
3501
3780
|
rel: "icon",
|
|
3502
3781
|
href: resolvedFaviconHref,
|
|
@@ -3555,46 +3834,18 @@ var CLIENT_CJK_KR_CSS_FILE = "/_assets/client-cjk-kr-_3ZNI2ZP.css";
|
|
|
3555
3834
|
class: `toast ${toast.type === "error" ? "toast-error" : "toast-success"}`,
|
|
3556
3835
|
"data-init": "el.closest('[popover]').showPopover(); history.replaceState({}, '', location.pathname); setTimeout(() => { el.classList.add('toast-out'); el.addEventListener('animationend', () => el.remove()) }, 3000)",
|
|
3557
3836
|
children: [
|
|
3558
|
-
toast.type === "error" ? /* @__PURE__ */ jsxDEV$1("
|
|
3559
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
3560
|
-
fill: "none",
|
|
3561
|
-
viewBox: "0 0 24 24",
|
|
3562
|
-
"stroke-width": "2",
|
|
3563
|
-
stroke: "currentColor",
|
|
3564
|
-
children: [/* @__PURE__ */ jsxDEV$1("circle", {
|
|
3565
|
-
cx: "12",
|
|
3566
|
-
cy: "12",
|
|
3567
|
-
r: "10"
|
|
3568
|
-
}), /* @__PURE__ */ jsxDEV$1("path", { d: "m15 9-6 6M9 9l6 6" })]
|
|
3569
|
-
}) : /* @__PURE__ */ jsxDEV$1("svg", {
|
|
3570
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
3571
|
-
fill: "none",
|
|
3572
|
-
viewBox: "0 0 24 24",
|
|
3573
|
-
"stroke-width": "2",
|
|
3574
|
-
stroke: "currentColor",
|
|
3575
|
-
children: [/* @__PURE__ */ jsxDEV$1("circle", {
|
|
3576
|
-
cx: "12",
|
|
3577
|
-
cy: "12",
|
|
3578
|
-
r: "10"
|
|
3579
|
-
}), /* @__PURE__ */ jsxDEV$1("path", { d: "m9 12 2 2 4-4" })]
|
|
3580
|
-
}),
|
|
3837
|
+
toast.type === "error" ? /* @__PURE__ */ jsxDEV$1(Icon$1, { name: "toast-error" }) : /* @__PURE__ */ jsxDEV$1(Icon$1, { name: "toast-success" }),
|
|
3581
3838
|
/* @__PURE__ */ jsxDEV$1("span", { children: toast.message }),
|
|
3582
3839
|
/* @__PURE__ */ jsxDEV$1("button", {
|
|
3583
3840
|
class: "toast-close",
|
|
3584
3841
|
"data-on:click": "el.closest('.toast').classList.add('toast-out'); el.closest('.toast').addEventListener('animationend', () => el.closest('.toast').remove())",
|
|
3585
|
-
children: /* @__PURE__ */ jsxDEV$1("
|
|
3586
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
3587
|
-
fill: "none",
|
|
3588
|
-
viewBox: "0 0 24 24",
|
|
3589
|
-
"stroke-width": "2",
|
|
3590
|
-
stroke: "currentColor",
|
|
3591
|
-
children: /* @__PURE__ */ jsxDEV$1("path", { d: "M18 6 6 18M6 6l12 12" })
|
|
3592
|
-
})
|
|
3842
|
+
children: /* @__PURE__ */ jsxDEV$1(Icon$1, { name: "toast-close" })
|
|
3593
3843
|
})
|
|
3594
3844
|
]
|
|
3595
3845
|
})
|
|
3596
3846
|
}),
|
|
3597
|
-
customBodyEndHtml && raw(customBodyEndHtml)
|
|
3847
|
+
customBodyEndHtml && raw(customBodyEndHtml),
|
|
3848
|
+
/* @__PURE__ */ jsxDEV$1(IconSprite, {})
|
|
3598
3849
|
]
|
|
3599
3850
|
})]
|
|
3600
3851
|
})] });
|
|
@@ -5381,9 +5632,10 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
5381
5632
|
});
|
|
5382
5633
|
//#endregion
|
|
5383
5634
|
//#region src/lib/hosted-signin.ts
|
|
5384
|
-
function getHostedAdminContinuationPath(publicRequestUrl) {
|
|
5635
|
+
function getHostedAdminContinuationPath(publicRequestUrl, redirect) {
|
|
5385
5636
|
const currentHost = new URL(publicRequestUrl).host;
|
|
5386
|
-
|
|
5637
|
+
const safeRedirect = isSafeInternalRedirect(redirect) ? redirect : "/";
|
|
5638
|
+
return `/auth/handoff/start?host=${encodeURIComponent(currentHost)}&redirect=${encodeURIComponent(safeRedirect)}`;
|
|
5387
5639
|
}
|
|
5388
5640
|
function buildHostedControlPlaneUrl(env, pathname, search) {
|
|
5389
5641
|
const hostedControlPlaneBaseUrl = getHostedControlPlaneBaseUrl(env);
|
|
@@ -5393,8 +5645,8 @@ function buildHostedControlPlaneUrl(env, pathname, search) {
|
|
|
5393
5645
|
location.search = search ?? "";
|
|
5394
5646
|
return location.toString();
|
|
5395
5647
|
}
|
|
5396
|
-
function getHostedControlPlaneSigninUrl(env, publicRequestUrl) {
|
|
5397
|
-
return buildHostedControlPlaneUrl(env, "/auth/handoff/start", getHostedAdminContinuationPath(publicRequestUrl).replace(/^\/auth\/handoff\/start/, ""));
|
|
5648
|
+
function getHostedControlPlaneSigninUrl(env, publicRequestUrl, redirect) {
|
|
5649
|
+
return buildHostedControlPlaneUrl(env, "/auth/handoff/start", getHostedAdminContinuationPath(publicRequestUrl, redirect).replace(/^\/auth\/handoff\/start/, ""));
|
|
5398
5650
|
}
|
|
5399
5651
|
function getHostedControlPlaneResetUrl(env, publicRequestUrl) {
|
|
5400
5652
|
const search = new URLSearchParams();
|
|
@@ -5419,11 +5671,12 @@ function getHostedControlPlaneProviderLabel(env) {
|
|
|
5419
5671
|
//#region src/routes/auth/signin.tsx
|
|
5420
5672
|
/**
|
|
5421
5673
|
* Sign-in / Sign-out Routes
|
|
5422
|
-
*/ var SigninContent = ({ demoEmail, demoPassword, sitePathPrefix = "" }) => {
|
|
5674
|
+
*/ var SigninContent = ({ demoEmail, demoPassword, sitePathPrefix = "", redirect }) => {
|
|
5423
5675
|
const { i18n } = useLingui();
|
|
5424
5676
|
const signals = JSON.stringify({
|
|
5425
5677
|
email: demoEmail || "",
|
|
5426
|
-
password: demoPassword || ""
|
|
5678
|
+
password: demoPassword || "",
|
|
5679
|
+
...redirect ? { redirect } : {}
|
|
5427
5680
|
}).replace(/</g, "\\u003c");
|
|
5428
5681
|
return /* @__PURE__ */ jsxDEV$1("div", {
|
|
5429
5682
|
class: "min-h-screen flex items-center justify-center",
|
|
@@ -5488,7 +5741,9 @@ function getHostedControlPlaneProviderLabel(env) {
|
|
|
5488
5741
|
};
|
|
5489
5742
|
var signinRoutes = new Hono();
|
|
5490
5743
|
signinRoutes.get("/signin", async (c) => {
|
|
5491
|
-
const
|
|
5744
|
+
const rawRedirect = c.req.query("redirect");
|
|
5745
|
+
const redirect = isSafeInternalRedirect(rawRedirect) ? rawRedirect : void 0;
|
|
5746
|
+
const hostedSigninUrl = getHostedControlPlaneSigninUrl(c.env, c.var.publicRequestUrl, redirect);
|
|
5492
5747
|
if (hostedSigninUrl) return c.redirect(hostedSigninUrl);
|
|
5493
5748
|
const i18n = getI18n(c);
|
|
5494
5749
|
const isSetup = c.req.query("setup") !== void 0;
|
|
@@ -5503,7 +5758,8 @@ signinRoutes.get("/signin", async (c) => {
|
|
|
5503
5758
|
children: /* @__PURE__ */ jsxDEV$1(SigninContent, {
|
|
5504
5759
|
demoEmail: c.var.appConfig.demoEmail,
|
|
5505
5760
|
demoPassword: c.var.appConfig.demoPassword,
|
|
5506
|
-
sitePathPrefix: c.var.appConfig.sitePathPrefix
|
|
5761
|
+
sitePathPrefix: c.var.appConfig.sitePathPrefix,
|
|
5762
|
+
redirect
|
|
5507
5763
|
})
|
|
5508
5764
|
}));
|
|
5509
5765
|
});
|
|
@@ -5514,6 +5770,8 @@ signinRoutes.post("/signin", async (c) => {
|
|
|
5514
5770
|
const parsed = SigninSchema.safeParse(body);
|
|
5515
5771
|
if (!parsed.success) return dsToast(parsed.error.issues[0]?.message ?? i18n._({ id: "vXCC6J" }), "error");
|
|
5516
5772
|
const { email, password } = parsed.data;
|
|
5773
|
+
const rawRedirect = body && typeof body === "object" && "redirect" in body ? body.redirect : void 0;
|
|
5774
|
+
const redirectTarget = typeof rawRedirect === "string" && isSafeInternalRedirect(rawRedirect) ? rawRedirect : "/";
|
|
5517
5775
|
try {
|
|
5518
5776
|
const { headers } = await c.var.auth.api.signInEmail({
|
|
5519
5777
|
returnHeaders: true,
|
|
@@ -5523,7 +5781,7 @@ signinRoutes.post("/signin", async (c) => {
|
|
|
5523
5781
|
},
|
|
5524
5782
|
headers: c.req.raw.headers
|
|
5525
5783
|
});
|
|
5526
|
-
return dsRedirect(toPublicPath(
|
|
5784
|
+
return dsRedirect(toPublicPath(redirectTarget, c.var.appConfig.sitePathPrefix), { headers });
|
|
5527
5785
|
} catch {
|
|
5528
5786
|
return dsToast(i18n._({ id: "nFukaP" }), "error");
|
|
5529
5787
|
}
|
|
@@ -5721,23 +5979,53 @@ function getRequestHostname(requestUrl, requestHost) {
|
|
|
5721
5979
|
return hostname ? isLocalHostname(hostname) : false;
|
|
5722
5980
|
}
|
|
5723
5981
|
/**
|
|
5982
|
+
* Paths that should never be used as post-signin redirect targets (would
|
|
5983
|
+
* either loop back to signin or hit an unauthenticated endpoint).
|
|
5984
|
+
*/ var POST_SIGNIN_REDIRECT_BLOCKLIST = new Set([
|
|
5985
|
+
"/signin",
|
|
5986
|
+
"/signout",
|
|
5987
|
+
"/setup",
|
|
5988
|
+
"/reset",
|
|
5989
|
+
"/__sso"
|
|
5990
|
+
]);
|
|
5991
|
+
function getPostSigninRedirect(requestUrl) {
|
|
5992
|
+
let url;
|
|
5993
|
+
try {
|
|
5994
|
+
url = new URL(requestUrl);
|
|
5995
|
+
} catch {
|
|
5996
|
+
return null;
|
|
5997
|
+
}
|
|
5998
|
+
const pathname = url.pathname || "/";
|
|
5999
|
+
if (POST_SIGNIN_REDIRECT_BLOCKLIST.has(pathname)) return null;
|
|
6000
|
+
if (pathname.startsWith("/api/")) return null;
|
|
6001
|
+
const candidate = `${pathname}${url.search}`;
|
|
6002
|
+
return isSafeInternalRedirect(candidate) ? candidate : null;
|
|
6003
|
+
}
|
|
6004
|
+
/**
|
|
5724
6005
|
* Middleware that requires authentication.
|
|
5725
6006
|
* Redirects to signin page if not authenticated.
|
|
5726
6007
|
* Session-only — Bearer tokens are not accepted for dashboard pages.
|
|
5727
6008
|
*/ function requireAuth(redirectTo = "/signin") {
|
|
5728
6009
|
return async (c, next) => {
|
|
5729
|
-
const
|
|
6010
|
+
const sitePathPrefix = getRuntimeSitePathPrefix({
|
|
5730
6011
|
env: c.env,
|
|
5731
6012
|
appConfig: c.var.appConfig,
|
|
5732
6013
|
currentSiteDomain: c.var.currentSiteDomain
|
|
5733
|
-
})
|
|
6014
|
+
});
|
|
6015
|
+
const buildRedirectTarget = () => {
|
|
6016
|
+
const publicHref = toPublicHref(redirectTo, sitePathPrefix);
|
|
6017
|
+
if (redirectTo !== "/signin") return publicHref;
|
|
6018
|
+
const postSignin = getPostSigninRedirect(c.req.url);
|
|
6019
|
+
if (!postSignin) return publicHref;
|
|
6020
|
+
return `${publicHref}${publicHref.includes("?") ? "&" : "?"}redirect=${encodeURIComponent(postSignin)}`;
|
|
6021
|
+
};
|
|
6022
|
+
const session = c.var.session;
|
|
6023
|
+
if (!session?.user) return c.redirect(buildRedirectTarget());
|
|
5734
6024
|
try {
|
|
5735
|
-
|
|
5736
|
-
if (!session?.user) return c.redirect(redirectTarget);
|
|
5737
|
-
if (!await c.var.services.siteMembers.get(c.var.currentSite.id, session.user.id)) return c.redirect(redirectTarget);
|
|
6025
|
+
if (!await c.var.services.siteMembers.get(c.var.currentSite.id, session.user.id)) return c.redirect(buildRedirectTarget());
|
|
5738
6026
|
await next();
|
|
5739
6027
|
} catch {
|
|
5740
|
-
return c.redirect(
|
|
6028
|
+
return c.redirect(buildRedirectTarget());
|
|
5741
6029
|
}
|
|
5742
6030
|
};
|
|
5743
6031
|
}
|
|
@@ -5747,10 +6035,9 @@ function getRequestHostname(requestUrl, requestHost) {
|
|
|
5747
6035
|
* Returns 401 if neither method succeeds.
|
|
5748
6036
|
*/ function requireAuthApi() {
|
|
5749
6037
|
return async (c, next) => {
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
if (session
|
|
5753
|
-
if (!await c.var.services.siteMembers.get(c.var.currentSite.id, session.user.id)) throw new UnauthorizedError();
|
|
6038
|
+
const session = c.var.session;
|
|
6039
|
+
if (session?.user) try {
|
|
6040
|
+
if (await c.var.services.siteMembers.get(c.var.currentSite.id, session.user.id)) {
|
|
5754
6041
|
await next();
|
|
5755
6042
|
return;
|
|
5756
6043
|
}
|
|
@@ -5827,7 +6114,7 @@ devAuthRoutes.get("/__dev/login", async (c) => {
|
|
|
5827
6114
|
headers: responseHeaders
|
|
5828
6115
|
});
|
|
5829
6116
|
} catch {
|
|
5830
|
-
return c.text("Dev login failed. Finish /setup once or run `mise run db-
|
|
6117
|
+
return c.text("Dev login failed. Finish /setup once or run `mise run db-wrangler-rebuild-demo`, then retry.", 500);
|
|
5831
6118
|
}
|
|
5832
6119
|
});
|
|
5833
6120
|
//#endregion
|
|
@@ -6314,7 +6601,8 @@ function getLegacyBodyPreview(post) {
|
|
|
6314
6601
|
}
|
|
6315
6602
|
function getPlainSummary(post) {
|
|
6316
6603
|
if (post.format === "quote") return normalizePreviewText(post.quoteText);
|
|
6317
|
-
|
|
6604
|
+
const cleanBody = post.body ? extractBodyText(post.body) : null;
|
|
6605
|
+
return normalizePreviewText(post.summary) || normalizePreviewText(cleanBody) || normalizePreviewText(post.bodyText) || getLegacyBodyPreview(post) || normalizePreviewText(post.url);
|
|
6318
6606
|
}
|
|
6319
6607
|
function clipPreviewText(text, maxChars) {
|
|
6320
6608
|
if (!text) return void 0;
|
|
@@ -6344,10 +6632,16 @@ function clipPreviewText(text, maxChars) {
|
|
|
6344
6632
|
if (result) {
|
|
6345
6633
|
summaryHtml = result.html;
|
|
6346
6634
|
summaryHasMore = result.hasMore;
|
|
6347
|
-
if (result.hasMore && post.bodyHtml) {
|
|
6348
|
-
const
|
|
6349
|
-
|
|
6350
|
-
|
|
6635
|
+
if (result.hasMore && post.bodyHtml) try {
|
|
6636
|
+
const doc = JSON.parse(post.body);
|
|
6637
|
+
if (doc.type === "doc" && Array.isArray(doc.content) && result.breakAtIndex > 0 && result.breakAtIndex <= doc.content.length) {
|
|
6638
|
+
const beforeHtml = renderTiptapDocument({
|
|
6639
|
+
type: "doc",
|
|
6640
|
+
content: doc.content.slice(0, result.breakAtIndex)
|
|
6641
|
+
});
|
|
6642
|
+
if (post.bodyHtml.startsWith(beforeHtml)) bodyHtmlWithAnchor = beforeHtml + "<span id=\"continue\"></span>" + post.bodyHtml.slice(beforeHtml.length);
|
|
6643
|
+
}
|
|
6644
|
+
} catch {}
|
|
6351
6645
|
}
|
|
6352
6646
|
}
|
|
6353
6647
|
const collections = (postCollections ?? []).map((c) => ({
|
|
@@ -6589,8 +6883,8 @@ function clipPreviewText(text, maxChars) {
|
|
|
6589
6883
|
* content: <MyContent />,
|
|
6590
6884
|
* });
|
|
6591
6885
|
* ```
|
|
6592
|
-
*/ async function getNavigationData(c) {
|
|
6593
|
-
const items = await c.var.services.navItems.list();
|
|
6886
|
+
*/ async function getNavigationData(c, options) {
|
|
6887
|
+
const items = options?.preloadedItems ?? await c.var.services.navItems.list();
|
|
6594
6888
|
const currentPath = c.var.publicPath;
|
|
6595
6889
|
const appConfig = c.var.appConfig;
|
|
6596
6890
|
const siteName = appConfig.siteName;
|
|
@@ -6602,11 +6896,8 @@ function clipPreviewText(text, maxChars) {
|
|
|
6602
6896
|
const siteAvatarUrl = appConfig.siteAvatarUrl || void 0;
|
|
6603
6897
|
const showHeaderAvatar = appConfig.showHeaderAvatar;
|
|
6604
6898
|
const siteFooterHtml = siteFooter ? render(siteFooter) : void 0;
|
|
6605
|
-
|
|
6899
|
+
const isAuthenticated = c.var.isAuthenticated;
|
|
6606
6900
|
let collections = [];
|
|
6607
|
-
try {
|
|
6608
|
-
isAuthenticated = !!(await c.var.auth.api.getSession({ headers: c.req.raw.headers }))?.user;
|
|
6609
|
-
} catch {}
|
|
6610
6901
|
const collectionNavIds = [];
|
|
6611
6902
|
for (const item of items) if (item.type === "collection" && item.collectionId) collectionNavIds.push(item.collectionId);
|
|
6612
6903
|
const collectionFreshness = collectionNavIds.length > 0 ? await c.var.services.navItems.getCollectionFreshness(collectionNavIds) : void 0;
|
|
@@ -7510,7 +7801,7 @@ var SiteLayout = ({ siteName, links, currentPath, sitePathPrefix = "", isAuthent
|
|
|
7510
7801
|
* });
|
|
7511
7802
|
* ```
|
|
7512
7803
|
*/ function renderPublicPage(c, options) {
|
|
7513
|
-
const { title, description, faviconHref, appleTouchHref, socialImageUrl, navData, content, sidebar, toast, showComposeDialog, showHeader, showHomeBranding, composeCollectionId } = options;
|
|
7804
|
+
const { title, description, faviconHref, appleTouchHref, socialImageUrl, canonicalHref, navData, content, sidebar, toast, showComposeDialog, showHeader, showHomeBranding, composeCollectionId } = options;
|
|
7514
7805
|
const metaDescription = description || navData.siteDescription || void 0;
|
|
7515
7806
|
const appConfig = c.get("appConfig");
|
|
7516
7807
|
const allSettings = c.get("allSettings");
|
|
@@ -7543,6 +7834,7 @@ var SiteLayout = ({ siteName, links, currentPath, sitePathPrefix = "", isAuthent
|
|
|
7543
7834
|
faviconHref,
|
|
7544
7835
|
appleTouchHref,
|
|
7545
7836
|
socialImageUrl,
|
|
7837
|
+
canonicalHref,
|
|
7546
7838
|
faviconUrl,
|
|
7547
7839
|
faviconVersion,
|
|
7548
7840
|
noindex,
|
|
@@ -9009,25 +9301,6 @@ var MediaGallery = ({ attachments, postPermalink }) => {
|
|
|
9009
9301
|
});
|
|
9010
9302
|
};
|
|
9011
9303
|
//#endregion
|
|
9012
|
-
//#region src/lib/featured-icons.ts
|
|
9013
|
-
/**
|
|
9014
|
-
* Shared icon definitions for featured post affordances.
|
|
9015
|
-
*
|
|
9016
|
-
* These paths are reused across Hono JSX, Lit, and exported static markup so
|
|
9017
|
-
* "Featured" keeps one visual language everywhere.
|
|
9018
|
-
*/ var FEATURED_SPARKLE_PATH = "M12 3 10.1 10.1 3 12l7.1 1.9L12 21l1.9-7.1L21 12l-7.1-1.9Z";
|
|
9019
|
-
var FEATURED_SPARKLE_OFF_SLASH_PATH = "M4 4 20 20";
|
|
9020
|
-
/**
|
|
9021
|
-
* Build inline SVG markup for the shared featured sparkle icon.
|
|
9022
|
-
*
|
|
9023
|
-
* @param options - Render options for the sparkle icon.
|
|
9024
|
-
* @returns SVG markup string for inline insertion.
|
|
9025
|
-
* @example
|
|
9026
|
-
* getFeaturedIconSvg({ off: true, className: "icon-fine" });
|
|
9027
|
-
*/ function getFeaturedIconSvg(options = {}) {
|
|
9028
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.35" stroke-linecap="round" stroke-linejoin="round"${options.className ? ` class="${options.className}"` : ""} aria-hidden="true"><path d="${FEATURED_SPARKLE_PATH}" />${options.off ? `<path d="${FEATURED_SPARKLE_OFF_SLASH_PATH}" />` : ""}</svg>`;
|
|
9029
|
-
}
|
|
9030
|
-
//#endregion
|
|
9031
9304
|
//#region src/ui/shared/PostFooter.tsx
|
|
9032
9305
|
/**
|
|
9033
9306
|
* Post Footer
|
|
@@ -9054,22 +9327,7 @@ var FEATURED_SPARKLE_OFF_SLASH_PATH = "M4 4 20 20";
|
|
|
9054
9327
|
children: [showIcon && /* @__PURE__ */ jsxDEV$1("span", {
|
|
9055
9328
|
class: "post-collection-primary-icon",
|
|
9056
9329
|
"aria-hidden": "true",
|
|
9057
|
-
children: /* @__PURE__ */ jsxDEV$1("
|
|
9058
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
9059
|
-
viewBox: "0 0 16 16",
|
|
9060
|
-
fill: "none",
|
|
9061
|
-
stroke: "currentColor",
|
|
9062
|
-
"stroke-width": "1.35",
|
|
9063
|
-
"stroke-linecap": "round",
|
|
9064
|
-
"stroke-linejoin": "round",
|
|
9065
|
-
children: [/* @__PURE__ */ jsxDEV$1("rect", {
|
|
9066
|
-
x: "3",
|
|
9067
|
-
y: "5.05",
|
|
9068
|
-
width: "10",
|
|
9069
|
-
height: "8.15",
|
|
9070
|
-
rx: "2.2"
|
|
9071
|
-
}), /* @__PURE__ */ jsxDEV$1("path", { d: "M5.1 5.05V4.2a1.1 1.1 0 0 1 1.1-1.1h3.6a1.1 1.1 0 0 1 1.1 1.1v.85" })]
|
|
9072
|
-
})
|
|
9330
|
+
children: /* @__PURE__ */ jsxDEV$1(Icon$1, { name: "post-collection-lock" })
|
|
9073
9331
|
}), /* @__PURE__ */ jsxDEV$1("span", {
|
|
9074
9332
|
class: "post-collection-tag-text",
|
|
9075
9333
|
children: first.title
|
|
@@ -9140,29 +9398,9 @@ var PostMenuTriggerButton = ({ className = "post-menu-trigger" }) => {
|
|
|
9140
9398
|
"aria-label": i18n._({ id: "JcD7qf" }),
|
|
9141
9399
|
"aria-expanded": "false",
|
|
9142
9400
|
"data-post-menu-trigger": true,
|
|
9143
|
-
children: /* @__PURE__ */ jsxDEV$1(
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
height: "15",
|
|
9147
|
-
viewBox: "0 0 24 24",
|
|
9148
|
-
fill: "currentColor",
|
|
9149
|
-
children: [
|
|
9150
|
-
/* @__PURE__ */ jsxDEV$1("circle", {
|
|
9151
|
-
cx: "5",
|
|
9152
|
-
cy: "12",
|
|
9153
|
-
r: "1.75"
|
|
9154
|
-
}),
|
|
9155
|
-
/* @__PURE__ */ jsxDEV$1("circle", {
|
|
9156
|
-
cx: "12",
|
|
9157
|
-
cy: "12",
|
|
9158
|
-
r: "1.75"
|
|
9159
|
-
}),
|
|
9160
|
-
/* @__PURE__ */ jsxDEV$1("circle", {
|
|
9161
|
-
cx: "19",
|
|
9162
|
-
cy: "12",
|
|
9163
|
-
r: "1.75"
|
|
9164
|
-
})
|
|
9165
|
-
]
|
|
9401
|
+
children: /* @__PURE__ */ jsxDEV$1(Icon$1, {
|
|
9402
|
+
name: "post-menu-dots",
|
|
9403
|
+
size: 15
|
|
9166
9404
|
})
|
|
9167
9405
|
});
|
|
9168
9406
|
};
|
|
@@ -9190,17 +9428,7 @@ var PostFooter = ({ post, detail, display }) => {
|
|
|
9190
9428
|
"aria-label": featuredLabel,
|
|
9191
9429
|
"data-tooltip": featuredLabel,
|
|
9192
9430
|
"data-align": "center",
|
|
9193
|
-
children: /* @__PURE__ */ jsxDEV$1("
|
|
9194
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
9195
|
-
viewBox: "0 0 24 24",
|
|
9196
|
-
fill: "none",
|
|
9197
|
-
stroke: "currentColor",
|
|
9198
|
-
"stroke-width": "1.35",
|
|
9199
|
-
"stroke-linecap": "round",
|
|
9200
|
-
"stroke-linejoin": "round",
|
|
9201
|
-
"aria-hidden": "true",
|
|
9202
|
-
children: /* @__PURE__ */ jsxDEV$1("path", { d: FEATURED_SPARKLE_PATH })
|
|
9203
|
-
})
|
|
9431
|
+
children: /* @__PURE__ */ jsxDEV$1(Icon$1, { name: "featured-sparkle" })
|
|
9204
9432
|
}),
|
|
9205
9433
|
showTimestamp && /* @__PURE__ */ jsxDEV$1(PostPublishedLink, {
|
|
9206
9434
|
post,
|
|
@@ -9212,16 +9440,7 @@ var PostFooter = ({ post, detail, display }) => {
|
|
|
9212
9440
|
target: "_blank",
|
|
9213
9441
|
rel: "noopener noreferrer",
|
|
9214
9442
|
"aria-label": i18n._({ id: "9dr9Nh" }),
|
|
9215
|
-
children: /* @__PURE__ */ jsxDEV$1("
|
|
9216
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
9217
|
-
viewBox: "0 0 24 24",
|
|
9218
|
-
fill: "none",
|
|
9219
|
-
stroke: "currentColor",
|
|
9220
|
-
"stroke-width": "2",
|
|
9221
|
-
"stroke-linecap": "round",
|
|
9222
|
-
"stroke-linejoin": "round",
|
|
9223
|
-
children: [/* @__PURE__ */ jsxDEV$1("path", { d: "M7 17 17 7" }), /* @__PURE__ */ jsxDEV$1("path", { d: "M9 7h8v8" })]
|
|
9224
|
-
})
|
|
9443
|
+
children: /* @__PURE__ */ jsxDEV$1(Icon$1, { name: "post-external-link" })
|
|
9225
9444
|
}),
|
|
9226
9445
|
/* @__PURE__ */ jsxDEV$1(CompactCollectionTags, {
|
|
9227
9446
|
collections: post.collections,
|
|
@@ -9236,17 +9455,9 @@ var PostFooter = ({ post, detail, display }) => {
|
|
|
9236
9455
|
class: "reply-trigger",
|
|
9237
9456
|
"aria-label": i18n._({ id: "ImOQa9" }),
|
|
9238
9457
|
"data-reply-trigger": true,
|
|
9239
|
-
children: /* @__PURE__ */ jsxDEV$1(
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
height: "14",
|
|
9243
|
-
viewBox: "0 0 24 24",
|
|
9244
|
-
fill: "none",
|
|
9245
|
-
stroke: "currentColor",
|
|
9246
|
-
"stroke-width": "2",
|
|
9247
|
-
"stroke-linecap": "round",
|
|
9248
|
-
"stroke-linejoin": "round",
|
|
9249
|
-
children: [/* @__PURE__ */ jsxDEV$1("polyline", { points: "9 17 4 12 9 7" }), /* @__PURE__ */ jsxDEV$1("path", { d: "M20 18v-2a4 4 0 0 0-4-4H4" })]
|
|
9458
|
+
children: /* @__PURE__ */ jsxDEV$1(Icon$1, {
|
|
9459
|
+
name: "post-reply",
|
|
9460
|
+
size: 14
|
|
9250
9461
|
})
|
|
9251
9462
|
}), /* @__PURE__ */ jsxDEV$1(PostMenuTriggerButton, {})]
|
|
9252
9463
|
})]
|
|
@@ -9268,57 +9479,15 @@ var PostFooter = ({ post, detail, display }) => {
|
|
|
9268
9479
|
children: [
|
|
9269
9480
|
/* @__PURE__ */ jsxDEV$1("span", {
|
|
9270
9481
|
class: "post-status-badge post-status-pinned",
|
|
9271
|
-
children: [/* @__PURE__ */ jsxDEV$1("
|
|
9272
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
9273
|
-
viewBox: "0 0 24 24",
|
|
9274
|
-
fill: "none",
|
|
9275
|
-
stroke: "currentColor",
|
|
9276
|
-
"stroke-width": "1.75",
|
|
9277
|
-
"stroke-linecap": "round",
|
|
9278
|
-
"stroke-linejoin": "round",
|
|
9279
|
-
children: [/* @__PURE__ */ jsxDEV$1("line", {
|
|
9280
|
-
x1: "12",
|
|
9281
|
-
x2: "12",
|
|
9282
|
-
y1: "17",
|
|
9283
|
-
y2: "22"
|
|
9284
|
-
}), /* @__PURE__ */ jsxDEV$1("path", { d: "M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z" })]
|
|
9285
|
-
}), "Pinned"]
|
|
9482
|
+
children: [/* @__PURE__ */ jsxDEV$1(Icon$1, { name: "post-status-pin" }), "Pinned"]
|
|
9286
9483
|
}),
|
|
9287
9484
|
/* @__PURE__ */ jsxDEV$1("span", {
|
|
9288
9485
|
class: "post-status-badge post-status-pinned-in-collection",
|
|
9289
|
-
children: [/* @__PURE__ */ jsxDEV$1("
|
|
9290
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
9291
|
-
viewBox: "0 0 24 24",
|
|
9292
|
-
fill: "none",
|
|
9293
|
-
stroke: "currentColor",
|
|
9294
|
-
"stroke-width": "1.75",
|
|
9295
|
-
"stroke-linecap": "round",
|
|
9296
|
-
"stroke-linejoin": "round",
|
|
9297
|
-
children: [/* @__PURE__ */ jsxDEV$1("line", {
|
|
9298
|
-
x1: "12",
|
|
9299
|
-
x2: "12",
|
|
9300
|
-
y1: "17",
|
|
9301
|
-
y2: "22"
|
|
9302
|
-
}), /* @__PURE__ */ jsxDEV$1("path", { d: "M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z" })]
|
|
9303
|
-
}), "Pinned"]
|
|
9486
|
+
children: [/* @__PURE__ */ jsxDEV$1(Icon$1, { name: "post-status-pin" }), "Pinned"]
|
|
9304
9487
|
}),
|
|
9305
9488
|
/* @__PURE__ */ jsxDEV$1("span", {
|
|
9306
9489
|
class: "post-status-badge post-status-private",
|
|
9307
|
-
children: [/* @__PURE__ */ jsxDEV$1("
|
|
9308
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
9309
|
-
viewBox: "0 0 24 24",
|
|
9310
|
-
fill: "none",
|
|
9311
|
-
stroke: "currentColor",
|
|
9312
|
-
"stroke-width": "1.75",
|
|
9313
|
-
"stroke-linecap": "round",
|
|
9314
|
-
"stroke-linejoin": "round",
|
|
9315
|
-
children: [
|
|
9316
|
-
/* @__PURE__ */ jsxDEV$1("path", { d: "M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" }),
|
|
9317
|
-
/* @__PURE__ */ jsxDEV$1("path", { d: "M14.084 14.158a3 3 0 0 1-4.242-4.242" }),
|
|
9318
|
-
/* @__PURE__ */ jsxDEV$1("path", { d: "M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" }),
|
|
9319
|
-
/* @__PURE__ */ jsxDEV$1("path", { d: "m2 2 20 20" })
|
|
9320
|
-
]
|
|
9321
|
-
}), "Private"]
|
|
9490
|
+
children: [/* @__PURE__ */ jsxDEV$1(Icon$1, { name: "post-status-private" }), "Private"]
|
|
9322
9491
|
})
|
|
9323
9492
|
]
|
|
9324
9493
|
});
|
|
@@ -9452,29 +9621,17 @@ var LinkPreview = ({ imageUrl, linkUrl, kind, provider }) => {
|
|
|
9452
9621
|
isVideo && /* @__PURE__ */ jsxDEV$1("div", {
|
|
9453
9622
|
class: "link-preview-play",
|
|
9454
9623
|
"aria-hidden": "true",
|
|
9455
|
-
children: /* @__PURE__ */ jsxDEV$1(
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
9459
|
-
children: [/* @__PURE__ */ jsxDEV$1("path", {
|
|
9460
|
-
class: "link-preview-play-bg",
|
|
9461
|
-
d: "M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55C3.97 2.33 2.27 4.81 1.48 7.74.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z",
|
|
9462
|
-
fill: "rgba(0,0,0,.65)"
|
|
9463
|
-
}), /* @__PURE__ */ jsxDEV$1("path", {
|
|
9464
|
-
d: "M45 24L27 14v20",
|
|
9465
|
-
fill: "#fff"
|
|
9466
|
-
})]
|
|
9624
|
+
children: /* @__PURE__ */ jsxDEV$1(Icon$1, {
|
|
9625
|
+
name: "link-preview-play",
|
|
9626
|
+
class: "link-preview-play-icon"
|
|
9467
9627
|
})
|
|
9468
9628
|
}),
|
|
9469
9629
|
providerLabel && /* @__PURE__ */ jsxDEV$1("span", {
|
|
9470
9630
|
class: "link-preview-badge",
|
|
9471
9631
|
"aria-hidden": "true",
|
|
9472
|
-
children: [isVideo && /* @__PURE__ */ jsxDEV$1(
|
|
9473
|
-
|
|
9474
|
-
|
|
9475
|
-
xmlns: "http://www.w3.org/2000/svg",
|
|
9476
|
-
fill: "currentColor",
|
|
9477
|
-
children: /* @__PURE__ */ jsxDEV$1("path", { d: "M5.5 3.5v9l7-4.5z" })
|
|
9632
|
+
children: [isVideo && /* @__PURE__ */ jsxDEV$1(Icon$1, {
|
|
9633
|
+
name: "link-preview-badge-play",
|
|
9634
|
+
class: "link-preview-badge-icon"
|
|
9478
9635
|
}), providerLabel]
|
|
9479
9636
|
})
|
|
9480
9637
|
]
|
|
@@ -9501,25 +9658,15 @@ var LinkPreview = ({ imageUrl, linkUrl, kind, provider }) => {
|
|
|
9501
9658
|
class: "feed-link-domain",
|
|
9502
9659
|
target: "_blank",
|
|
9503
9660
|
rel: "noopener noreferrer",
|
|
9504
|
-
children: [/* @__PURE__ */ jsxDEV$1(
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
fill: "none",
|
|
9508
|
-
viewBox: "0 0 24 24",
|
|
9509
|
-
"stroke-width": "2",
|
|
9510
|
-
stroke: "currentColor",
|
|
9511
|
-
children: /* @__PURE__ */ jsxDEV$1("path", { d: "M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" })
|
|
9661
|
+
children: [/* @__PURE__ */ jsxDEV$1(Icon$1, {
|
|
9662
|
+
name: "link-domain",
|
|
9663
|
+
class: "feed-link-domain-icon"
|
|
9512
9664
|
}), /* @__PURE__ */ jsxDEV$1("span", { children: domain })]
|
|
9513
9665
|
}) : /* @__PURE__ */ jsxDEV$1("div", {
|
|
9514
9666
|
class: "feed-link-domain",
|
|
9515
|
-
children: [/* @__PURE__ */ jsxDEV$1(
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
fill: "none",
|
|
9519
|
-
viewBox: "0 0 24 24",
|
|
9520
|
-
"stroke-width": "2",
|
|
9521
|
-
stroke: "currentColor",
|
|
9522
|
-
children: /* @__PURE__ */ jsxDEV$1("path", { d: "M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" })
|
|
9667
|
+
children: [/* @__PURE__ */ jsxDEV$1(Icon$1, {
|
|
9668
|
+
name: "link-domain",
|
|
9669
|
+
class: "feed-link-domain-icon"
|
|
9523
9670
|
}), /* @__PURE__ */ jsxDEV$1("span", { children: domain })]
|
|
9524
9671
|
}));
|
|
9525
9672
|
const previewEl = !isCompact && post.previewImageUrl && /* @__PURE__ */ jsxDEV$1(LinkPreview, {
|
|
@@ -9600,11 +9747,6 @@ var LinkPreview = ({ imageUrl, linkUrl, kind, provider }) => {
|
|
|
9600
9747
|
});
|
|
9601
9748
|
};
|
|
9602
9749
|
//#endregion
|
|
9603
|
-
//#region src/lib/decorative-quote-mark.ts
|
|
9604
|
-
var DECORATIVE_QUOTE_MARK_VIEWBOX = "0 0 96 96";
|
|
9605
|
-
var DECORATIVE_QUOTE_MARK_PATHS = ["M24.4 10.5C16.9 17.7 11.5 26.8 8.2 37.7C4.9 48.7 4.8 58.9 7.8 68.2C10.3 75.7 15.4 79.5 22.9 79.5C28 79.5 32.2 77.8 35.4 74.2C38.6 70.7 40.2 66.5 40.2 61.4C40.2 56.5 38.8 52.6 36 49.6C33.3 46.6 29.7 45.1 25.2 45.1C23.4 45.1 21.8 45.3 20.2 45.8C22.2 37.3 26.7 29.2 33.6 21.4L24.4 10.5Z", "M60.8 10.5C53.3 17.7 47.9 26.8 44.6 37.7C41.3 48.7 41.2 58.9 44.2 68.2C46.7 75.7 51.8 79.5 59.3 79.5C64.4 79.5 68.6 77.8 71.8 74.2C75 70.7 76.6 66.5 76.6 61.4C76.6 56.5 75.2 52.6 72.4 49.6C69.7 46.6 66.1 45.1 61.6 45.1C59.8 45.1 58.2 45.3 56.6 45.8C58.6 37.3 63.1 29.2 70 21.4L60.8 10.5Z"];
|
|
9606
|
-
DECORATIVE_QUOTE_MARK_PATHS.map((path) => `<path fill="currentColor" d="${path}" />`).join("");
|
|
9607
|
-
//#endregion
|
|
9608
9750
|
//#region src/ui/shared/DecorativeQuoteMark.tsx
|
|
9609
9751
|
/**
|
|
9610
9752
|
* Decorative double-quote mark rendered as SVG so the shape stays consistent
|
|
@@ -9613,15 +9755,7 @@ DECORATIVE_QUOTE_MARK_PATHS.map((path) => `<path fill="currentColor" d="${path}"
|
|
|
9613
9755
|
class: `decorative-quote-mark${cls ? ` ${cls}` : ""}`,
|
|
9614
9756
|
"data-direction": direction,
|
|
9615
9757
|
"aria-hidden": "true",
|
|
9616
|
-
children: /* @__PURE__ */ jsxDEV$1("
|
|
9617
|
-
viewBox: DECORATIVE_QUOTE_MARK_VIEWBOX,
|
|
9618
|
-
role: "presentation",
|
|
9619
|
-
focusable: "false",
|
|
9620
|
-
children: DECORATIVE_QUOTE_MARK_PATHS.map((path) => /* @__PURE__ */ jsxDEV$1("path", {
|
|
9621
|
-
fill: "currentColor",
|
|
9622
|
-
d: path
|
|
9623
|
-
}))
|
|
9624
|
-
})
|
|
9758
|
+
children: /* @__PURE__ */ jsxDEV$1(Icon$1, { name: "decorative-quote" })
|
|
9625
9759
|
});
|
|
9626
9760
|
//#endregion
|
|
9627
9761
|
//#region src/ui/feed/QuoteCard.tsx
|
|
@@ -10086,16 +10220,23 @@ var PaginatedPageHeader = ({ title, currentPage = 1, totalPages, description, ic
|
|
|
10086
10220
|
* Latest and Featured in navigation. The explicit feed routes still work.
|
|
10087
10221
|
*/ var homeRoutes = new Hono();
|
|
10088
10222
|
homeRoutes.get("/", async (c) => {
|
|
10089
|
-
const navData = await getNavigationData(c);
|
|
10090
10223
|
const i18n = getI18n(c);
|
|
10091
10224
|
const page = parsePageNumber(c.req.query("page"));
|
|
10092
10225
|
const paginatedPageTitle = formatPageLabel(page);
|
|
10093
|
-
|
|
10226
|
+
const isAuthenticated = c.var.isAuthenticated;
|
|
10227
|
+
const navItems = await c.var.services.navItems.list();
|
|
10228
|
+
const homeDefaultView = getHomeDefaultViewFromNavItems(navItems);
|
|
10229
|
+
const timelinePromise = homeDefaultView === "featured" ? assembleFeaturedTimeline(c, {
|
|
10230
|
+
page,
|
|
10231
|
+
isAuthenticated
|
|
10232
|
+
}) : assembleTimeline(c, {
|
|
10233
|
+
page,
|
|
10234
|
+
isAuthenticated
|
|
10235
|
+
});
|
|
10236
|
+
const [navData, timeline] = await Promise.all([getNavigationData(c, { preloadedItems: navItems }), timelinePromise]);
|
|
10237
|
+
const { items, currentPage, totalPages } = timeline;
|
|
10238
|
+
if (homeDefaultView === "featured") {
|
|
10094
10239
|
const featuredTitle = i18n._({ id: "FkMol5" });
|
|
10095
|
-
const { items, currentPage, totalPages } = await assembleFeaturedTimeline(c, {
|
|
10096
|
-
page,
|
|
10097
|
-
isAuthenticated: navData.isAuthenticated
|
|
10098
|
-
});
|
|
10099
10240
|
return renderPublicPage(c, {
|
|
10100
10241
|
title: page > 1 ? buildPageTitle(featuredTitle, paginatedPageTitle, navData.siteName) : navData.siteName,
|
|
10101
10242
|
navData,
|
|
@@ -10109,10 +10250,6 @@ homeRoutes.get("/", async (c) => {
|
|
|
10109
10250
|
});
|
|
10110
10251
|
}
|
|
10111
10252
|
const latestTitle = i18n._({ id: "wL3cK8" });
|
|
10112
|
-
const { items, currentPage, totalPages } = await assembleTimeline(c, {
|
|
10113
|
-
page,
|
|
10114
|
-
isAuthenticated: navData.isAuthenticated
|
|
10115
|
-
});
|
|
10116
10253
|
return renderPublicPage(c, {
|
|
10117
10254
|
title: page > 1 ? buildPageTitle(latestTitle, paginatedPageTitle, navData.siteName) : navData.siteName,
|
|
10118
10255
|
navData,
|
|
@@ -10170,6 +10307,17 @@ var PostPage = ({ post, threadPosts }) => {
|
|
|
10170
10307
|
//#region src/lib/post-meta.ts
|
|
10171
10308
|
var TITLE_MAX_CHARS = 72;
|
|
10172
10309
|
var DESCRIPTION_MAX_CHARS = 160;
|
|
10310
|
+
/**
|
|
10311
|
+
* Derive a clean plain-text projection of the body for human-facing meta.
|
|
10312
|
+
*
|
|
10313
|
+
* We cannot reuse `post.bodyText` here: that column is written with
|
|
10314
|
+
* `includeLinkHrefs: true` so inline link URLs land in the FTS index, which
|
|
10315
|
+
* pollutes the stored text with trailing URLs. Re-derive from the source
|
|
10316
|
+
* TipTap JSON (`post.body`) without that option.
|
|
10317
|
+
*/ function getCleanBodyText(post) {
|
|
10318
|
+
if (post.body) return extractBodyText(post.body) ?? "";
|
|
10319
|
+
return post.bodyText ?? "";
|
|
10320
|
+
}
|
|
10173
10321
|
function normalizeText(text) {
|
|
10174
10322
|
return (text ?? "").replace(/\s+/g, " ").trim();
|
|
10175
10323
|
}
|
|
@@ -10193,7 +10341,7 @@ function getTitleCandidate(post) {
|
|
|
10193
10341
|
if (normalizeText(post.title)) return normalizeText(post.title);
|
|
10194
10342
|
const summarySnippet = getFirstParagraph(post.summary);
|
|
10195
10343
|
if (summarySnippet) return clipText(summarySnippet, TITLE_MAX_CHARS);
|
|
10196
|
-
const bodySnippet = getFirstParagraph(post
|
|
10344
|
+
const bodySnippet = getFirstParagraph(getCleanBodyText(post));
|
|
10197
10345
|
if (bodySnippet) return clipText(bodySnippet, TITLE_MAX_CHARS);
|
|
10198
10346
|
if (post.format === "link" && post.url) return extractDisplayDomain(post.url) || post.url;
|
|
10199
10347
|
return "";
|
|
@@ -10205,7 +10353,7 @@ function getDescriptionCandidate(post) {
|
|
|
10205
10353
|
}
|
|
10206
10354
|
const summaryText = normalizeText(post.summary);
|
|
10207
10355
|
if (summaryText) return clipText(summaryText, DESCRIPTION_MAX_CHARS);
|
|
10208
|
-
const bodyText = normalizeText(post
|
|
10356
|
+
const bodyText = normalizeText(getCleanBodyText(post));
|
|
10209
10357
|
if (bodyText) return clipText(bodyText, DESCRIPTION_MAX_CHARS);
|
|
10210
10358
|
const quoteText = normalizeText(post.quoteText);
|
|
10211
10359
|
if (quoteText) return clipText(quoteText, DESCRIPTION_MAX_CHARS);
|
|
@@ -10305,6 +10453,7 @@ function buildPostMeta(post, siteName) {
|
|
|
10305
10453
|
pathRegistry: () => pathRegistry$1,
|
|
10306
10454
|
postCollections: () => postCollections$1,
|
|
10307
10455
|
posts: () => posts$1,
|
|
10456
|
+
rateLimit: () => rateLimit$2,
|
|
10308
10457
|
session: () => session$1,
|
|
10309
10458
|
settings: () => settings$1,
|
|
10310
10459
|
siteDomains: () => siteDomains$1,
|
|
@@ -10367,9 +10516,14 @@ var sites$1 = sqliteTable("site", {
|
|
|
10367
10516
|
id: text("id").primaryKey(),
|
|
10368
10517
|
key: text("key").notNull(),
|
|
10369
10518
|
status: text("status", { enum: SITE_STATUSES$1 }).notNull().default("active"),
|
|
10519
|
+
provisioningIdempotencyKey: text("provisioning_idempotency_key"),
|
|
10370
10520
|
createdAt: integer("created_at").notNull(),
|
|
10371
10521
|
updatedAt: integer("updated_at").notNull()
|
|
10372
|
-
}, (table) => [
|
|
10522
|
+
}, (table) => [
|
|
10523
|
+
uniqueIndex("uq_site_key").on(table.key),
|
|
10524
|
+
uniqueIndex("uq_site_provisioning_idempotency_key").on(table.provisioningIdempotencyKey).where(sql`${table.provisioningIdempotencyKey} IS NOT NULL`),
|
|
10525
|
+
check("chk_site_status", sql`${table.status} IN (${sqlTextEnum$1(SITE_STATUSES$1)})`)
|
|
10526
|
+
]);
|
|
10373
10527
|
var siteDomains$1 = sqliteTable("site_domain", {
|
|
10374
10528
|
id: text("id").primaryKey(),
|
|
10375
10529
|
siteId: text("site_id").notNull().references(() => sites$1.id, { onDelete: "cascade" }),
|
|
@@ -10752,6 +10906,19 @@ var githubAppInstallation$1 = sqliteTable("github_app_installation", {
|
|
|
10752
10906
|
index("github_app_installation_by_site").on(table.siteId),
|
|
10753
10907
|
check("chk_github_app_installation_account_type", sql`${table.accountType} IN (${sqlTextEnum$1(GITHUB_APP_ACCOUNT_TYPES$1)})`)
|
|
10754
10908
|
]);
|
|
10909
|
+
/**
|
|
10910
|
+
* Per-key sliding-window rate-limit counters. Used by the Cloudflare Workers
|
|
10911
|
+
* runtime; Node deployments keep the state in memory instead.
|
|
10912
|
+
*
|
|
10913
|
+
* `key` is typically a scope prefix plus client IP (e.g. "search:1.2.3.4").
|
|
10914
|
+
* `window_start` is the aligned start of the window in Unix seconds — we
|
|
10915
|
+
* store two adjacent windows per key to support the sliding-window-counter
|
|
10916
|
+
* algorithm.
|
|
10917
|
+
*/ var rateLimit$2 = sqliteTable("rate_limit", {
|
|
10918
|
+
key: text("key").notNull(),
|
|
10919
|
+
windowStart: integer("window_start").notNull(),
|
|
10920
|
+
count: integer("count").notNull().default(0)
|
|
10921
|
+
}, (table) => [primaryKey({ columns: [table.key, table.windowStart] })]);
|
|
10755
10922
|
//#endregion
|
|
10756
10923
|
//#region src/db/index.ts
|
|
10757
10924
|
/**
|
|
@@ -10856,6 +11023,7 @@ function isNodeSqliteDatabase(db) {
|
|
|
10856
11023
|
pathRegistry: () => pathRegistry,
|
|
10857
11024
|
postCollections: () => postCollections,
|
|
10858
11025
|
posts: () => posts,
|
|
11026
|
+
rateLimit: () => rateLimit$1,
|
|
10859
11027
|
session: () => session,
|
|
10860
11028
|
settings: () => settings,
|
|
10861
11029
|
siteDomains: () => siteDomains,
|
|
@@ -10921,9 +11089,14 @@ var sites = pgTable("site", {
|
|
|
10921
11089
|
id: text$1("id").primaryKey(),
|
|
10922
11090
|
key: text$1("key").notNull(),
|
|
10923
11091
|
status: text$1("status", { enum: SITE_STATUSES }).notNull().default("active"),
|
|
11092
|
+
provisioningIdempotencyKey: text$1("provisioning_idempotency_key"),
|
|
10924
11093
|
createdAt: integer$1("created_at").notNull(),
|
|
10925
11094
|
updatedAt: integer$1("updated_at").notNull()
|
|
10926
|
-
}, (table) => [
|
|
11095
|
+
}, (table) => [
|
|
11096
|
+
uniqueIndex$1("uq_site_key").on(table.key),
|
|
11097
|
+
uniqueIndex$1("uq_site_provisioning_idempotency_key").on(table.provisioningIdempotencyKey).where(sql`${table.provisioningIdempotencyKey} IS NOT NULL`),
|
|
11098
|
+
check$1("chk_site_status", sql`${table.status} IN (${sqlTextEnum(SITE_STATUSES)})`)
|
|
11099
|
+
]);
|
|
10927
11100
|
var siteDomains = pgTable("site_domain", {
|
|
10928
11101
|
id: text$1("id").primaryKey(),
|
|
10929
11102
|
siteId: text$1("site_id").notNull().references(() => sites.id, { onDelete: "cascade" }),
|
|
@@ -11344,6 +11517,14 @@ var githubAppInstallation = pgTable("github_app_installation", {
|
|
|
11344
11517
|
index$1("github_app_installation_by_site").on(table.siteId),
|
|
11345
11518
|
check$1("chk_github_app_installation_account_type", sql`${table.accountType} IN (${sqlTextEnum(GITHUB_APP_ACCOUNT_TYPES)})`)
|
|
11346
11519
|
]);
|
|
11520
|
+
/**
|
|
11521
|
+
* Per-key sliding-window rate-limit counters. Mirrors the SQLite `rate_limit`
|
|
11522
|
+
* table — kept in lockstep because both dialects are production targets.
|
|
11523
|
+
*/ var rateLimit$1 = pgTable("rate_limit", {
|
|
11524
|
+
key: text$1("key").notNull(),
|
|
11525
|
+
windowStart: integer$1("window_start").notNull(),
|
|
11526
|
+
count: integer$1("count").notNull().default(0)
|
|
11527
|
+
}, (table) => [primaryKey$1({ columns: [table.key, table.windowStart] })]);
|
|
11347
11528
|
//#endregion
|
|
11348
11529
|
//#region src/db/schema-bundle.ts
|
|
11349
11530
|
var sqliteSchemaBundle = schema_exports$1;
|
|
@@ -11766,36 +11947,6 @@ function createMediaService(db, siteId, databaseSchema = sqliteSchemaBundle, dat
|
|
|
11766
11947
|
};
|
|
11767
11948
|
}
|
|
11768
11949
|
//#endregion
|
|
11769
|
-
//#region src/lib/icons.ts
|
|
11770
|
-
/**
|
|
11771
|
-
* Shared icon utilities.
|
|
11772
|
-
*
|
|
11773
|
-
* Provides a small wrapper around lucide-static so server-rendered UI can fetch
|
|
11774
|
-
* SVG markup by kebab-case icon name.
|
|
11775
|
-
*/
|
|
11776
|
-
/**
|
|
11777
|
-
* Convert a kebab-case icon name to PascalCase for lucide-static lookup.
|
|
11778
|
-
*
|
|
11779
|
-
* @param name - Kebab-case icon name such as "book-open"
|
|
11780
|
-
* @returns PascalCase name such as "BookOpen"
|
|
11781
|
-
*/ function toPascalCase(name) {
|
|
11782
|
-
return name.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
11783
|
-
}
|
|
11784
|
-
/**
|
|
11785
|
-
* Get SVG markup for a Lucide icon by kebab-case name.
|
|
11786
|
-
*
|
|
11787
|
-
* @param name - Kebab-case icon name
|
|
11788
|
-
* @returns SVG string or null when the icon is unknown
|
|
11789
|
-
*
|
|
11790
|
-
* @example
|
|
11791
|
-
* ```ts
|
|
11792
|
-
* getIconSvg("book-open");
|
|
11793
|
-
* ```
|
|
11794
|
-
*/ function getIconSvg(name) {
|
|
11795
|
-
const svg = lucideIcons[toPascalCase(name)];
|
|
11796
|
-
return typeof svg === "string" ? svg : null;
|
|
11797
|
-
}
|
|
11798
|
-
//#endregion
|
|
11799
11950
|
//#region src/ui/pages/ArchivePage.tsx
|
|
11800
11951
|
/**
|
|
11801
11952
|
* Archive Page
|
|
@@ -12706,33 +12857,33 @@ function getAtomTitle(post) {
|
|
|
12706
12857
|
</feed>`;
|
|
12707
12858
|
}
|
|
12708
12859
|
/**
|
|
12709
|
-
*
|
|
12860
|
+
* Render a sitemap `<urlset>` XML document from a list of URL entries.
|
|
12710
12861
|
*
|
|
12711
|
-
*
|
|
12712
|
-
|
|
12713
|
-
*/ function defaultSitemapRenderer(data) {
|
|
12714
|
-
const { siteUrl, sitemapUrl, posts } = data;
|
|
12715
|
-
const postUrls = posts.map((post) => {
|
|
12716
|
-
return `
|
|
12717
|
-
<url>
|
|
12718
|
-
<loc>${escapeXml(new URL(post.permalink, siteUrl).toString())}</loc>
|
|
12719
|
-
<lastmod>${post.updatedAt.split("T")[0]}</lastmod>
|
|
12720
|
-
<priority>${post.featured ? "0.8" : "0.6"}</priority>
|
|
12721
|
-
</url>`;
|
|
12722
|
-
}).join("");
|
|
12723
|
-
const homepageUrl = `
|
|
12724
|
-
<url>
|
|
12725
|
-
<loc>${escapeXml(siteUrl)}</loc>
|
|
12726
|
-
<priority>1.0</priority>
|
|
12727
|
-
<changefreq>daily</changefreq>
|
|
12728
|
-
</url>`;
|
|
12862
|
+
* Used by the sharded sitemap endpoints in `routes/feed/sitemap.ts`.
|
|
12863
|
+
*/ function renderSitemapUrlSet(entries) {
|
|
12729
12864
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
12730
12865
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
12731
|
-
|
|
12732
|
-
|
|
12733
|
-
|
|
12866
|
+
${entries.map((entry) => {
|
|
12867
|
+
const parts = [` <loc>${escapeXml(entry.loc)}</loc>`];
|
|
12868
|
+
if (entry.lastmod) parts.push(` <lastmod>${escapeXml(entry.lastmod)}</lastmod>`);
|
|
12869
|
+
if (entry.changefreq) parts.push(` <changefreq>${escapeXml(entry.changefreq)}</changefreq>`);
|
|
12870
|
+
if (entry.priority) parts.push(` <priority>${escapeXml(entry.priority)}</priority>`);
|
|
12871
|
+
return ` <url>\n${parts.join("\n")}\n </url>`;
|
|
12872
|
+
}).join("\n")}
|
|
12734
12873
|
</urlset>`;
|
|
12735
12874
|
}
|
|
12875
|
+
/**
|
|
12876
|
+
* Render a `<sitemapindex>` XML document listing shard sitemap URLs.
|
|
12877
|
+
*/ function renderSitemapIndex(entries) {
|
|
12878
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
12879
|
+
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
12880
|
+
${entries.map((entry) => {
|
|
12881
|
+
const parts = [` <loc>${escapeXml(entry.loc)}</loc>`];
|
|
12882
|
+
if (entry.lastmod) parts.push(` <lastmod>${escapeXml(entry.lastmod)}</lastmod>`);
|
|
12883
|
+
return ` <sitemap>\n${parts.join("\n")}\n </sitemap>`;
|
|
12884
|
+
}).join("\n")}
|
|
12885
|
+
</sitemapindex>`;
|
|
12886
|
+
}
|
|
12736
12887
|
//#endregion
|
|
12737
12888
|
//#region src/routes/pages/archive.tsx
|
|
12738
12889
|
/**
|
|
@@ -13760,12 +13911,29 @@ collectionRoutes.get("/:slug/feed", async (c) => {
|
|
|
13760
13911
|
* Resolves post slugs, aliases, redirects, and collection aliases.
|
|
13761
13912
|
* Must be registered last.
|
|
13762
13913
|
*/ var pageRoutes = new Hono();
|
|
13914
|
+
/**
|
|
13915
|
+
* Build the canonical absolute URL for a post page.
|
|
13916
|
+
*
|
|
13917
|
+
* Reply URLs render the full thread, so search engines see overlapping
|
|
13918
|
+
* content at every reply URL. Point the canonical to the thread root so
|
|
13919
|
+
* crawlers consolidate ranking on one URL.
|
|
13920
|
+
*
|
|
13921
|
+
* The root post is always at index 0 of `threadPostViews` (getThread orders
|
|
13922
|
+
* by createdAt ASC, and the DB check constraint guarantees root has the
|
|
13923
|
+
* smallest createdAt in its thread). When `threadPostViews` is undefined the
|
|
13924
|
+
* post is not part of a multi-post thread, so the post itself is the root.
|
|
13925
|
+
*/ function buildPostCanonicalHref(postView, threadPostViews, siteUrl) {
|
|
13926
|
+
const rootPermalink = threadPostViews?.[0]?.permalink ?? postView.permalink;
|
|
13927
|
+
if (!siteUrl) return rootPermalink;
|
|
13928
|
+
return new URL(rootPermalink, siteUrl).toString();
|
|
13929
|
+
}
|
|
13763
13930
|
async function renderPostWithTextPreview(c, post, autoOpen) {
|
|
13764
13931
|
const navDataPromise = getNavigationData(c);
|
|
13765
13932
|
const display = await assemblePostPageDisplay(c, post, { isAuthenticated: true });
|
|
13766
13933
|
if (!display) return c.notFound();
|
|
13767
13934
|
const navData = await navDataPromise;
|
|
13768
13935
|
const meta = buildPostMeta(post, navData.siteName);
|
|
13936
|
+
const canonicalHref = buildPostCanonicalHref(display.postView, display.threadPostViews, c.var.appConfig.siteUrl);
|
|
13769
13937
|
const pageTitle = autoOpen.attachmentTitle || meta.title;
|
|
13770
13938
|
const autoOpenMeta = JSON.stringify({
|
|
13771
13939
|
shareHref: autoOpen.shareHref,
|
|
@@ -13776,6 +13944,7 @@ async function renderPostWithTextPreview(c, post, autoOpen) {
|
|
|
13776
13944
|
return renderPublicPage(c, {
|
|
13777
13945
|
title: pageTitle,
|
|
13778
13946
|
description: meta.description,
|
|
13947
|
+
canonicalHref,
|
|
13779
13948
|
navData,
|
|
13780
13949
|
content: /* @__PURE__ */ jsxDEV$1(Fragment$1, { children: [
|
|
13781
13950
|
/* @__PURE__ */ jsxDEV$1(PostPage, {
|
|
@@ -13824,9 +13993,11 @@ async function renderPost(c, post) {
|
|
|
13824
13993
|
if (!display) return c.notFound();
|
|
13825
13994
|
const navData = await navDataPromise;
|
|
13826
13995
|
const meta = buildPostMeta(post, navData.siteName);
|
|
13996
|
+
const canonicalHref = buildPostCanonicalHref(display.postView, display.threadPostViews, c.var.appConfig.siteUrl);
|
|
13827
13997
|
return renderPublicPage(c, {
|
|
13828
13998
|
title: meta.title,
|
|
13829
13999
|
description: meta.description,
|
|
14000
|
+
canonicalHref,
|
|
13830
14001
|
navData,
|
|
13831
14002
|
content: /* @__PURE__ */ jsxDEV$1(PostPage, {
|
|
13832
14003
|
post: display.postView,
|
|
@@ -14845,18 +15016,11 @@ newPostRoutes.get("/new", async (c) => {
|
|
|
14845
15016
|
//#endregion
|
|
14846
15017
|
//#region src/routes/pages/partials.tsx
|
|
14847
15018
|
var partialPageRoutes = new Hono();
|
|
14848
|
-
async function getIsAuthenticated(c) {
|
|
14849
|
-
try {
|
|
14850
|
-
return !!(await c.var.auth.api.getSession({ headers: c.req.raw.headers }))?.user;
|
|
14851
|
-
} catch {
|
|
14852
|
-
return false;
|
|
14853
|
-
}
|
|
14854
|
-
}
|
|
14855
15019
|
partialPageRoutes.get("/_/version", (c) => {
|
|
14856
15020
|
return c.json({ version: CORE_VERSION });
|
|
14857
15021
|
});
|
|
14858
15022
|
partialPageRoutes.get("/_/timeline-item/:threadRootId", async (c) => {
|
|
14859
|
-
const item = await assembleTimelineItem(c, parseIdParam(c.req.param("threadRootId"), ID_PREFIX.post), { isAuthenticated:
|
|
15023
|
+
const item = await assembleTimelineItem(c, parseIdParam(c.req.param("threadRootId"), ID_PREFIX.post), { isAuthenticated: c.var.isAuthenticated });
|
|
14860
15024
|
if (!item) return c.notFound();
|
|
14861
15025
|
return c.html(/* @__PURE__ */ jsxDEV$1(I18nProvider, {
|
|
14862
15026
|
c,
|
|
@@ -14864,7 +15028,7 @@ partialPageRoutes.get("/_/timeline-item/:threadRootId", async (c) => {
|
|
|
14864
15028
|
}));
|
|
14865
15029
|
});
|
|
14866
15030
|
partialPageRoutes.get("/_/post-card/:postId", async (c) => {
|
|
14867
|
-
const postView = await assemblePostCardView(c, parseIdParam(c.req.param("postId"), ID_PREFIX.post), { isAuthenticated:
|
|
15031
|
+
const postView = await assemblePostCardView(c, parseIdParam(c.req.param("postId"), ID_PREFIX.post), { isAuthenticated: c.var.isAuthenticated });
|
|
14868
15032
|
if (!postView) return c.notFound();
|
|
14869
15033
|
return c.html(/* @__PURE__ */ jsxDEV$1(I18nProvider, {
|
|
14870
15034
|
c,
|
|
@@ -14872,7 +15036,7 @@ partialPageRoutes.get("/_/post-card/:postId", async (c) => {
|
|
|
14872
15036
|
}));
|
|
14873
15037
|
});
|
|
14874
15038
|
partialPageRoutes.get("/_/post-view/:postId", async (c) => {
|
|
14875
|
-
const display = await assemblePostPageDisplay(c, parseIdParam(c.req.param("postId"), ID_PREFIX.post), { isAuthenticated:
|
|
15039
|
+
const display = await assemblePostPageDisplay(c, parseIdParam(c.req.param("postId"), ID_PREFIX.post), { isAuthenticated: c.var.isAuthenticated });
|
|
14876
15040
|
if (!display) return c.notFound();
|
|
14877
15041
|
return c.html(/* @__PURE__ */ jsxDEV$1(I18nProvider, {
|
|
14878
15042
|
c,
|
|
@@ -19829,6 +19993,10 @@ function parseConfigInt(value, fallback) {
|
|
|
19829
19993
|
showHeaderAvatar: allSettings["SHOW_HEADER_AVATAR"] === "true",
|
|
19830
19994
|
siteAvatarUrl,
|
|
19831
19995
|
faviconVersion: allSettings["SITE_FAVICON_VERSION"] ?? "",
|
|
19996
|
+
rateLimit: {
|
|
19997
|
+
disabled: getEnvString(env, "RATE_LIMIT_DISABLED") === "true",
|
|
19998
|
+
searchPerMinute: parseInt(getEnvString(env, "RATE_LIMIT_SEARCH_PER_MIN") ?? "30", 10) || 30
|
|
19999
|
+
},
|
|
19832
20000
|
fallbacks: {
|
|
19833
20001
|
siteName: resolveFallback("SITE_NAME", env),
|
|
19834
20002
|
siteDescription: resolveFallback("SITE_DESCRIPTION", env),
|
|
@@ -20019,8 +20187,8 @@ async function syncHostedControlPlaneSiteAvatar(input) {
|
|
|
20019
20187
|
return;
|
|
20020
20188
|
}
|
|
20021
20189
|
await markSyncPending(settings);
|
|
20022
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
20023
|
-
const { getGitHubAppConfig } = await import("./env-
|
|
20190
|
+
const { createGitHubSyncService } = await import("./github-sync-7y_nTXx1.js").then((n) => n.r);
|
|
20191
|
+
const { getGitHubAppConfig } = await import("./env-CgaH9Mut.js").then((n) => n.t);
|
|
20024
20192
|
const run = runBackgroundSync(settings, createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
20025
20193
|
storage: c.var.storage,
|
|
20026
20194
|
githubApp: getGitHubAppConfig(c.env)
|
|
@@ -20634,7 +20802,7 @@ settingsRoutes.get("/account", async (c) => {
|
|
|
20634
20802
|
settingsRoutes.get("/account/sessions", async (c) => {
|
|
20635
20803
|
if (c.var.appConfig.demoMode) return c.redirect(publicPath(c, "/settings/account"));
|
|
20636
20804
|
const navData = await getNavigationData(c);
|
|
20637
|
-
const currentToken =
|
|
20805
|
+
const currentToken = c.var.session?.session?.token ?? "";
|
|
20638
20806
|
const sessions = (await c.var.auth.api.listSessions({ headers: c.req.raw.headers }) ?? []).map((s) => ({
|
|
20639
20807
|
token: s.token,
|
|
20640
20808
|
ipAddress: s.ipAddress ?? null,
|
|
@@ -20787,7 +20955,7 @@ settingsRoutes.post("/github-sync/connect", async (c) => {
|
|
|
20787
20955
|
if (getGitHubAppConfig(c.env)) return dsToast("This deployment uses GitHub App authentication. Use Install GitHub App instead.", "error");
|
|
20788
20956
|
const body = await c.req.json();
|
|
20789
20957
|
if (!body.token?.trim() || !body.repo?.trim()) return dsToast("Token and repository are required.", "error");
|
|
20790
|
-
const { parseRepoSlug, createGitHubClient } = await import("./github-api-
|
|
20958
|
+
const { parseRepoSlug, createGitHubClient } = await import("./github-api-BkRWnqMx.js").then((n) => n.n);
|
|
20791
20959
|
const parsed = parseRepoSlug(body.repo);
|
|
20792
20960
|
if (!parsed) return dsToast("Invalid repository format. Use owner/repo.", "error");
|
|
20793
20961
|
const client = createGitHubClient(body.token);
|
|
@@ -20806,7 +20974,7 @@ settingsRoutes.post("/github-sync/connect", async (c) => {
|
|
|
20806
20974
|
await c.var.services.settings.set("GITHUB_SYNC_AUTH_MODE", "pat");
|
|
20807
20975
|
await c.var.services.settings.set("GITHUB_SYNC_APP_INSTALLATION_ID", "");
|
|
20808
20976
|
await c.var.services.settings.set("GITHUB_SYNC_ENABLED", "true");
|
|
20809
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
20977
|
+
const { createGitHubSyncService } = await import("./github-sync-7y_nTXx1.js").then((n) => n.r);
|
|
20810
20978
|
const syncService = createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
20811
20979
|
storage: c.var.storage,
|
|
20812
20980
|
githubApp: getGitHubAppConfig(c.env)
|
|
@@ -20825,7 +20993,7 @@ settingsRoutes.post("/github-sync/connect", async (c) => {
|
|
|
20825
20993
|
return dsRedirect(publicPath(c, "/settings/github-sync"));
|
|
20826
20994
|
});
|
|
20827
20995
|
settingsRoutes.post("/github-sync/push", async (c) => {
|
|
20828
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
20996
|
+
const { createGitHubSyncService } = await import("./github-sync-7y_nTXx1.js").then((n) => n.r);
|
|
20829
20997
|
const syncService = createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
20830
20998
|
storage: c.var.storage,
|
|
20831
20999
|
githubApp: getGitHubAppConfig(c.env)
|
|
@@ -20845,7 +21013,7 @@ settingsRoutes.post("/github-sync/push", async (c) => {
|
|
|
20845
21013
|
});
|
|
20846
21014
|
});
|
|
20847
21015
|
settingsRoutes.post("/github-sync/disconnect", async (c) => {
|
|
20848
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
21016
|
+
const { createGitHubSyncService } = await import("./github-sync-7y_nTXx1.js").then((n) => n.r);
|
|
20849
21017
|
await createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), { githubApp: getGitHubAppConfig(c.env) }).teardownWebhook();
|
|
20850
21018
|
return dsRedirect(publicPath(c, "/settings/github-sync"));
|
|
20851
21019
|
});
|
|
@@ -21033,10 +21201,10 @@ function buildRepoPickerLabels(c) {
|
|
|
21033
21201
|
const repo = String(body.repo ?? "").trim();
|
|
21034
21202
|
const confirmForeign = body.confirmForeign === true || body.confirmForeign === "true";
|
|
21035
21203
|
if (!installationId || !repo) return wantsJson ? c.json({ error: "Missing installationId or repo." }, 400) : c.text("Missing installationId or repo.", 400);
|
|
21036
|
-
const { parseRepoSlug, createGitHubClient } = await import("./github-api-
|
|
21204
|
+
const { parseRepoSlug, createGitHubClient } = await import("./github-api-BkRWnqMx.js").then((n) => n.n);
|
|
21037
21205
|
const parsed = parseRepoSlug(repo);
|
|
21038
21206
|
if (!parsed) return wantsJson ? c.json({ error: "Invalid repository format." }, 400) : c.text("Invalid repository format.", 400);
|
|
21039
|
-
const { classifyRepoForSync } = await import("./github-sync-
|
|
21207
|
+
const { classifyRepoForSync } = await import("./github-sync-7y_nTXx1.js").then((n) => n.r);
|
|
21040
21208
|
const ghClient = createGitHubClient(() => getInstallationTokenFromApp(app, installationId));
|
|
21041
21209
|
let classification;
|
|
21042
21210
|
try {
|
|
@@ -21066,7 +21234,7 @@ function buildRepoPickerLabels(c) {
|
|
|
21066
21234
|
await c.var.services.settings.set("GITHUB_SYNC_REPO", repo);
|
|
21067
21235
|
await c.var.services.settings.set("GITHUB_SYNC_TOKEN", "");
|
|
21068
21236
|
await c.var.services.settings.set("GITHUB_SYNC_ENABLED", "true");
|
|
21069
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
21237
|
+
const { createGitHubSyncService } = await import("./github-sync-7y_nTXx1.js").then((n) => n.r);
|
|
21070
21238
|
const syncService = createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
21071
21239
|
storage: c.var.storage,
|
|
21072
21240
|
githubApp: app
|
|
@@ -21105,7 +21273,7 @@ function requireGitHubApp(c) {
|
|
|
21105
21273
|
* build a client for classification without adding the helper to the
|
|
21106
21274
|
* top-level imports (the module already lazy-imports github-api).
|
|
21107
21275
|
*/ async function getInstallationTokenFromApp(app, installationId) {
|
|
21108
|
-
const { getInstallationToken } = await import("./github-app-
|
|
21276
|
+
const { getInstallationToken } = await import("./github-app-WeadXMb8.js").then((n) => n.i);
|
|
21109
21277
|
return getInstallationToken(app, installationId);
|
|
21110
21278
|
}
|
|
21111
21279
|
/** List GitHub App installations authorized for this site. */ settingsRoutes.get("/github-sync/app/installations", async (c) => {
|
|
@@ -21179,10 +21347,10 @@ function requireGitHubApp(c) {
|
|
|
21179
21347
|
const installationId = String(body.installationId ?? "").trim();
|
|
21180
21348
|
const repo = String(body.repo ?? "").trim();
|
|
21181
21349
|
if (!installationId || !repo) return c.json({ error: "Missing installationId or repo." }, 400);
|
|
21182
|
-
const { parseRepoSlug, createGitHubClient } = await import("./github-api-
|
|
21350
|
+
const { parseRepoSlug, createGitHubClient } = await import("./github-api-BkRWnqMx.js").then((n) => n.n);
|
|
21183
21351
|
const parsed = parseRepoSlug(repo);
|
|
21184
21352
|
if (!parsed) return c.json({ error: "Invalid repository format." }, 400);
|
|
21185
|
-
const { classifyRepoForSync } = await import("./github-sync-
|
|
21353
|
+
const { classifyRepoForSync } = await import("./github-sync-7y_nTXx1.js").then((n) => n.r);
|
|
21186
21354
|
const client = createGitHubClient(() => getInstallationTokenFromApp(app, installationId));
|
|
21187
21355
|
try {
|
|
21188
21356
|
const classification = await classifyRepoForSync(client, parsed.owner, parsed.repo, c.var.currentSite.id);
|
|
@@ -23389,10 +23557,78 @@ function toSearchApiResult(post, snippet, sitePathPrefix) {
|
|
|
23389
23557
|
};
|
|
23390
23558
|
}
|
|
23391
23559
|
//#endregion
|
|
23560
|
+
//#region src/lib/rate-limit.ts
|
|
23561
|
+
/**
|
|
23562
|
+
* Rate Limiting Abstraction
|
|
23563
|
+
*
|
|
23564
|
+
* Shared interface for per-key rate limiting. Runtimes provide their own
|
|
23565
|
+
* implementation: Cloudflare Workers uses a D1-backed sliding-window table
|
|
23566
|
+
* (ephemeral isolates can't hold memory state), while Node uses an
|
|
23567
|
+
* in-process Map (the process is persistent and avoids DB round-trips).
|
|
23568
|
+
*
|
|
23569
|
+
* Consumers depend only on this interface; they are runtime-agnostic.
|
|
23570
|
+
*/ /**
|
|
23571
|
+
* Extracts the client IP from a Hono request context.
|
|
23572
|
+
*
|
|
23573
|
+
* On Cloudflare Workers, `cf-connecting-ip` is set by the edge and is
|
|
23574
|
+
* authoritative. On Node deployments we fall back to the leftmost
|
|
23575
|
+
* `x-forwarded-for` entry, which is the conventional client IP when the
|
|
23576
|
+
* app sits behind a single trusted proxy. When neither header is
|
|
23577
|
+
* available we return `"unknown"` so all such requests share a bucket —
|
|
23578
|
+
* preferable to skipping the rate limit entirely.
|
|
23579
|
+
*
|
|
23580
|
+
* Note: this helper does not verify proxy trust. It is used for DoS
|
|
23581
|
+
* protection, not authentication. If header-forgery resistance becomes
|
|
23582
|
+
* important, gate the `x-forwarded-for` branch on `shouldTrustProxy`.
|
|
23583
|
+
*/ function getClientIp(c) {
|
|
23584
|
+
const cf = c.req.header("cf-connecting-ip");
|
|
23585
|
+
if (cf) return cf;
|
|
23586
|
+
const fwd = c.req.header("x-forwarded-for");
|
|
23587
|
+
if (fwd) {
|
|
23588
|
+
const first = fwd.split(",")[0]?.trim();
|
|
23589
|
+
if (first) return first;
|
|
23590
|
+
}
|
|
23591
|
+
return "unknown";
|
|
23592
|
+
}
|
|
23593
|
+
//#endregion
|
|
23594
|
+
//#region src/middleware/rate-limit.ts
|
|
23595
|
+
/**
|
|
23596
|
+
* Rate-Limit Middleware
|
|
23597
|
+
*
|
|
23598
|
+
* Applies a per-IP rate limit to the routes it wraps. Which storage
|
|
23599
|
+
* backs the limiter (D1 or in-memory) is decided upstream by the
|
|
23600
|
+
* runtime; this middleware only reads `c.var.rateLimiter` and doesn't
|
|
23601
|
+
* care.
|
|
23602
|
+
*
|
|
23603
|
+
* When the limit is exceeded, responds with HTTP 429 and a
|
|
23604
|
+
* `Retry-After` header. When `appConfig.rateLimit.disabled` is true the
|
|
23605
|
+
* middleware short-circuits to `next()` so test and dev environments
|
|
23606
|
+
* don't have to reason about bucket state.
|
|
23607
|
+
*/ function rateLimit(opts) {
|
|
23608
|
+
return async (c, next) => {
|
|
23609
|
+
if (c.var.appConfig.rateLimit.disabled) return next();
|
|
23610
|
+
const ip = getClientIp(c);
|
|
23611
|
+
const result = await c.var.rateLimiter.check(`${opts.name}:${ip}`, {
|
|
23612
|
+
limit: opts.limit,
|
|
23613
|
+
windowSec: opts.windowSec
|
|
23614
|
+
});
|
|
23615
|
+
if (!result.ok) {
|
|
23616
|
+
c.header("Retry-After", String(result.retryAfterSec ?? opts.windowSec));
|
|
23617
|
+
return c.json({ error: "Too many requests. Please slow down." }, 429);
|
|
23618
|
+
}
|
|
23619
|
+
return next();
|
|
23620
|
+
};
|
|
23621
|
+
}
|
|
23622
|
+
//#endregion
|
|
23392
23623
|
//#region src/routes/api/search.ts
|
|
23393
23624
|
/**
|
|
23394
23625
|
* Search API Routes
|
|
23395
23626
|
*/ var searchApiRoutes = new Hono();
|
|
23627
|
+
searchApiRoutes.use("*", async (c, next) => rateLimit({
|
|
23628
|
+
name: "search",
|
|
23629
|
+
limit: c.var.appConfig.rateLimit.searchPerMinute,
|
|
23630
|
+
windowSec: 60
|
|
23631
|
+
})(c, next));
|
|
23396
23632
|
searchApiRoutes.get("/", async (c) => {
|
|
23397
23633
|
const query = c.req.query("q");
|
|
23398
23634
|
if (!query || query.trim().length === 0) throw new ValidationError("Query parameter 'q' is required");
|
|
@@ -24472,7 +24708,8 @@ var CreateManagedSiteSchema = z.object({
|
|
|
24472
24708
|
primaryHost: z.string().trim().toLowerCase().min(1).max(255).regex(/^(?=.{1,255}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/, "Primary host must be a valid hostname."),
|
|
24473
24709
|
siteName: z.string().trim().min(1).max(120),
|
|
24474
24710
|
siteLanguage: z.string().trim().max(35).optional(),
|
|
24475
|
-
timeZone: z.string().trim().max(100).optional()
|
|
24711
|
+
timeZone: z.string().trim().max(100).optional(),
|
|
24712
|
+
idempotencyKey: z.string().trim().min(1).max(128).optional()
|
|
24476
24713
|
});
|
|
24477
24714
|
var ManagedSiteDomainSchema = z.object({
|
|
24478
24715
|
host: z.string().trim().toLowerCase().min(1).max(255).regex(/^(?=.{1,255}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/, "Domain host must be a valid hostname."),
|
|
@@ -24857,6 +25094,30 @@ var internalTextAttachmentsRoutes = new Hono();
|
|
|
24857
25094
|
return c.json(result);
|
|
24858
25095
|
});
|
|
24859
25096
|
//#endregion
|
|
25097
|
+
//#region src/routes/api/internal/search-reindex.ts
|
|
25098
|
+
var ReindexSchema = z.object({
|
|
25099
|
+
limit: z.number().int().positive().max(500).optional(),
|
|
25100
|
+
cursor: z.string().min(1).optional()
|
|
25101
|
+
});
|
|
25102
|
+
var internalSearchReindexRoutes = new Hono();
|
|
25103
|
+
/**
|
|
25104
|
+
* Rebuild `post.body_text` for a batch of non-deleted posts. FTS indexes
|
|
25105
|
+
* (SQLite trigger / Postgres generated column) refresh automatically when
|
|
25106
|
+
* `body_text` changes.
|
|
25107
|
+
*
|
|
25108
|
+
* Idempotent. Callers loop with the returned `nextCursor` until `done: true`.
|
|
25109
|
+
* Used by the `jant search-reindex` CLI to backfill search indexes for
|
|
25110
|
+
* existing posts after changes to the text extraction logic (e.g. including
|
|
25111
|
+
* link mark hrefs so inline URLs become searchable).
|
|
25112
|
+
*/ internalSearchReindexRoutes.post("/", requireInternalAdminApi(), async (c) => {
|
|
25113
|
+
const body = parseValidated(ReindexSchema, (c.req.header("Content-Type") || "").includes("application/json") ? await c.req.json().catch(() => ({})) : {});
|
|
25114
|
+
const result = await c.var.services.posts.reindexBodyText({
|
|
25115
|
+
limit: body.limit,
|
|
25116
|
+
cursor: body.cursor
|
|
25117
|
+
});
|
|
25118
|
+
return c.json(result);
|
|
25119
|
+
});
|
|
25120
|
+
//#endregion
|
|
24860
25121
|
//#region src/routes/api/internal/uploads.ts
|
|
24861
25122
|
var CleanupUploadsSchema = z.object({ limit: z.number().int().positive().max(500).optional() });
|
|
24862
25123
|
var internalUploadsRoutes = new Hono();
|
|
@@ -25354,30 +25615,111 @@ feedRoutes.get("/all/atom.xml", (c) => {
|
|
|
25354
25615
|
//#region src/routes/feed/sitemap.ts
|
|
25355
25616
|
/**
|
|
25356
25617
|
* Sitemap Routes
|
|
25618
|
+
*
|
|
25619
|
+
* Sitemap is sharded to keep each shard small, cache-friendly, and stable:
|
|
25620
|
+
*
|
|
25621
|
+
* /sitemap.xml → sitemap index listing all shards
|
|
25622
|
+
* /sitemap-posts-N.xml → one shard of published non-reply posts
|
|
25623
|
+
* /sitemap-collections.xml → public collection pages
|
|
25624
|
+
* /sitemap-pages.xml → homepage + static aggregate pages
|
|
25625
|
+
*
|
|
25626
|
+
* Post shards are keyset-paginated by post `id` (TypeIDs embed a
|
|
25627
|
+
* creation-ordered UUIDv7 timestamp), so once a shard fills up its membership
|
|
25628
|
+
* never changes: new posts always land in the last shard, never rewriting an
|
|
25629
|
+
* older one. This lets old shards be cached at the edge for a long time.
|
|
25357
25630
|
*/ var sitemapRoutes = new Hono();
|
|
25358
|
-
|
|
25359
|
-
|
|
25360
|
-
|
|
25361
|
-
const posts = await c.var.services.posts.list({
|
|
25362
|
-
status: "published",
|
|
25363
|
-
excludeReplies: true,
|
|
25364
|
-
excludePrivate: true,
|
|
25365
|
-
limit: 1e3
|
|
25366
|
-
});
|
|
25367
|
-
const mediaCtx = createMediaContext(appConfig);
|
|
25368
|
-
const aliasesMap = await c.var.services.paths.getPostAliases(posts.map((p) => p.id));
|
|
25369
|
-
const aliasMap = /* @__PURE__ */ new Map();
|
|
25370
|
-
for (const [id, aliases] of aliasesMap) if (aliases[0]) aliasMap.set(id, aliases[0]);
|
|
25371
|
-
const postViews = toPostViewsFromPosts(posts, mediaCtx, void 0, aliasMap);
|
|
25372
|
-
const xml = defaultSitemapRenderer({
|
|
25373
|
-
siteUrl,
|
|
25374
|
-
sitemapUrl: toAbsoluteSiteUrl("/sitemap.xml", siteUrl, appConfig.sitePathPrefix),
|
|
25375
|
-
posts: postViews
|
|
25376
|
-
});
|
|
25631
|
+
var CACHE_SHORT = "public, max-age=180";
|
|
25632
|
+
var CACHE_FULL_SHARD = "public, max-age=86400, s-maxage=86400";
|
|
25633
|
+
function xmlResponse(xml, cacheControl) {
|
|
25377
25634
|
return new Response(xml, { headers: {
|
|
25378
25635
|
"Content-Type": "application/xml; charset=utf-8",
|
|
25379
|
-
"Cache-Control":
|
|
25636
|
+
"Cache-Control": cacheControl
|
|
25380
25637
|
} });
|
|
25638
|
+
}
|
|
25639
|
+
/**
|
|
25640
|
+
* Build a public URL entry (absolute URL) from an internal path.
|
|
25641
|
+
*/ function absoluteUrl(internalPath, siteUrl, sitePathPrefix) {
|
|
25642
|
+
return toAbsoluteSiteUrl(internalPath, siteUrl, sitePathPrefix);
|
|
25643
|
+
}
|
|
25644
|
+
/** Convert a unix-seconds timestamp into a `YYYY-MM-DD` string. */ function toIsoDate(unixSeconds) {
|
|
25645
|
+
return (/* @__PURE__ */ new Date(unixSeconds * 1e3)).toISOString().slice(0, 10);
|
|
25646
|
+
}
|
|
25647
|
+
sitemapRoutes.get("/sitemap.xml", async (c) => {
|
|
25648
|
+
const { appConfig } = c.var;
|
|
25649
|
+
const { siteUrl, sitePathPrefix } = appConfig;
|
|
25650
|
+
const postCount = await c.var.services.posts.countForSitemap();
|
|
25651
|
+
const postShardCount = Math.max(1, Math.ceil(postCount / 500));
|
|
25652
|
+
const entries = [{ loc: absoluteUrl("/sitemap-pages.xml", siteUrl, sitePathPrefix) }];
|
|
25653
|
+
for (let page = 1; page <= postShardCount; page++) entries.push({ loc: absoluteUrl(`/sitemap-posts-${page}.xml`, siteUrl, sitePathPrefix) });
|
|
25654
|
+
if ((await c.var.services.collections.list()).length > 0) entries.push({ loc: absoluteUrl("/sitemap-collections.xml", siteUrl, sitePathPrefix) });
|
|
25655
|
+
return xmlResponse(renderSitemapIndex(entries), CACHE_SHORT);
|
|
25656
|
+
});
|
|
25657
|
+
sitemapRoutes.get("/:file{sitemap-posts-[0-9]+\\.xml}", async (c) => {
|
|
25658
|
+
const { appConfig } = c.var;
|
|
25659
|
+
const { siteUrl, sitePathPrefix } = appConfig;
|
|
25660
|
+
const file = c.req.param("file");
|
|
25661
|
+
const match = /^sitemap-posts-([0-9]+)\.xml$/.exec(file);
|
|
25662
|
+
if (!match) return c.notFound();
|
|
25663
|
+
const page = Number(match[1]);
|
|
25664
|
+
if (!Number.isFinite(page) || page < 1) return c.notFound();
|
|
25665
|
+
let afterId;
|
|
25666
|
+
if (page > 1) {
|
|
25667
|
+
const cursorOffset = (page - 1) * 500 - 1;
|
|
25668
|
+
const cursor = await c.var.services.posts.getSitemapIdAt(cursorOffset);
|
|
25669
|
+
if (cursor === null) return c.notFound();
|
|
25670
|
+
afterId = cursor;
|
|
25671
|
+
}
|
|
25672
|
+
const shardEntries = await c.var.services.posts.listForSitemap({
|
|
25673
|
+
afterId,
|
|
25674
|
+
limit: 500
|
|
25675
|
+
});
|
|
25676
|
+
const urls = shardEntries.map((entry) => {
|
|
25677
|
+
return {
|
|
25678
|
+
loc: absoluteUrl(entry.alias ?? `/${entry.slug}`, siteUrl, sitePathPrefix),
|
|
25679
|
+
lastmod: toIsoDate(entry.updatedAt),
|
|
25680
|
+
priority: entry.featuredAt ? "0.8" : "0.6"
|
|
25681
|
+
};
|
|
25682
|
+
});
|
|
25683
|
+
const cacheControl = shardEntries.length === 500 ? CACHE_FULL_SHARD : CACHE_SHORT;
|
|
25684
|
+
return xmlResponse(renderSitemapUrlSet(urls), cacheControl);
|
|
25685
|
+
});
|
|
25686
|
+
sitemapRoutes.get("/sitemap-collections.xml", async (c) => {
|
|
25687
|
+
const { appConfig } = c.var;
|
|
25688
|
+
const { siteUrl, sitePathPrefix } = appConfig;
|
|
25689
|
+
const collections = await c.var.services.collections.list();
|
|
25690
|
+
return xmlResponse(renderSitemapUrlSet(await Promise.all(collections.map(async (collection) => {
|
|
25691
|
+
const alias = await c.var.services.customUrls.getByTarget("collection", collection.id);
|
|
25692
|
+
return {
|
|
25693
|
+
loc: absoluteUrl(alias ? `/${alias.path}` : `/${collection.slug}`, siteUrl, sitePathPrefix),
|
|
25694
|
+
lastmod: toIsoDate(collection.updatedAt),
|
|
25695
|
+
priority: "0.7"
|
|
25696
|
+
};
|
|
25697
|
+
}))), CACHE_SHORT);
|
|
25698
|
+
});
|
|
25699
|
+
sitemapRoutes.get("/sitemap-pages.xml", async (c) => {
|
|
25700
|
+
const { appConfig } = c.var;
|
|
25701
|
+
const { siteUrl, sitePathPrefix, homeDefaultView } = appConfig;
|
|
25702
|
+
const urls = [{
|
|
25703
|
+
loc: absoluteUrl("/", siteUrl, sitePathPrefix),
|
|
25704
|
+
priority: "1.0",
|
|
25705
|
+
changefreq: "daily"
|
|
25706
|
+
}, {
|
|
25707
|
+
loc: absoluteUrl("/archive", siteUrl, sitePathPrefix),
|
|
25708
|
+
priority: "0.5",
|
|
25709
|
+
changefreq: "weekly"
|
|
25710
|
+
}];
|
|
25711
|
+
const secondaryAggregate = homeDefaultView === "featured" ? "/latest" : "/featured";
|
|
25712
|
+
urls.push({
|
|
25713
|
+
loc: absoluteUrl(secondaryAggregate, siteUrl, sitePathPrefix),
|
|
25714
|
+
priority: "0.6",
|
|
25715
|
+
changefreq: "daily"
|
|
25716
|
+
});
|
|
25717
|
+
if ((await c.var.services.collections.list()).length > 0) urls.push({
|
|
25718
|
+
loc: absoluteUrl("/collections", siteUrl, sitePathPrefix),
|
|
25719
|
+
priority: "0.5",
|
|
25720
|
+
changefreq: "weekly"
|
|
25721
|
+
});
|
|
25722
|
+
return xmlResponse(renderSitemapUrlSet(urls), CACHE_SHORT);
|
|
25381
25723
|
});
|
|
25382
25724
|
sitemapRoutes.get("/robots.txt", async (c) => {
|
|
25383
25725
|
const { appConfig } = c.var;
|
|
@@ -25395,7 +25737,7 @@ sitemapRoutes.get("/robots.txt", async (c) => {
|
|
|
25395
25737
|
].join("\n");
|
|
25396
25738
|
return new Response(robots, { headers: {
|
|
25397
25739
|
"Content-Type": "text/plain; charset=utf-8",
|
|
25398
|
-
"Cache-Control":
|
|
25740
|
+
"Cache-Control": CACHE_SHORT
|
|
25399
25741
|
} });
|
|
25400
25742
|
});
|
|
25401
25743
|
//#endregion
|
|
@@ -25438,6 +25780,34 @@ manifestRoutes.get("/manifest.webmanifest", (c) => {
|
|
|
25438
25780
|
} });
|
|
25439
25781
|
});
|
|
25440
25782
|
//#endregion
|
|
25783
|
+
//#region src/middleware/session.ts
|
|
25784
|
+
/**
|
|
25785
|
+
* Session Middleware
|
|
25786
|
+
*
|
|
25787
|
+
* Runs once per request (after runtime init) to look up the better-auth
|
|
25788
|
+
* session and stash it on `c.var.session` / `c.var.isAuthenticated`.
|
|
25789
|
+
*
|
|
25790
|
+
* This replaces ad-hoc `auth.api.getSession()` calls scattered across
|
|
25791
|
+
* view helpers (e.g. `lib/navigation.ts`) so each request only parses
|
|
25792
|
+
* the session cookie once. better-auth's own cookieCache (5 min) still
|
|
25793
|
+
* keeps this cheap, but centralizing the call also unlocks `Promise.all`
|
|
25794
|
+
* patterns in routes that previously serialized on a hidden session fetch.
|
|
25795
|
+
*
|
|
25796
|
+
* Never throws — any lookup error is treated as "not authenticated".
|
|
25797
|
+
*/ function attachSession() {
|
|
25798
|
+
return async (c, next) => {
|
|
25799
|
+
try {
|
|
25800
|
+
const session = await c.var.auth.api.getSession({ headers: c.req.raw.headers });
|
|
25801
|
+
c.set("session", session ?? null);
|
|
25802
|
+
c.set("isAuthenticated", !!session?.user);
|
|
25803
|
+
} catch {
|
|
25804
|
+
c.set("session", null);
|
|
25805
|
+
c.set("isAuthenticated", false);
|
|
25806
|
+
}
|
|
25807
|
+
await next();
|
|
25808
|
+
};
|
|
25809
|
+
}
|
|
25810
|
+
//#endregion
|
|
25441
25811
|
//#region src/middleware/onboarding.ts
|
|
25442
25812
|
/**
|
|
25443
25813
|
* Onboarding Middleware
|
|
@@ -26361,6 +26731,60 @@ async function deriveScryptKey(password, saltHex, opts) {
|
|
|
26361
26731
|
});
|
|
26362
26732
|
}
|
|
26363
26733
|
//#endregion
|
|
26734
|
+
//#region src/lib/rate-limit-d1.ts
|
|
26735
|
+
/**
|
|
26736
|
+
* D1 Sliding-Window Rate Limiter
|
|
26737
|
+
*
|
|
26738
|
+
* Used by the Cloudflare Workers runtime where isolates are ephemeral and
|
|
26739
|
+
* memory-based limiters would silently drop state between requests. Each
|
|
26740
|
+
* check performs one SELECT (over a two-row range) plus one UPSERT — both
|
|
26741
|
+
* hit a composite primary key so the round-trips are cheap.
|
|
26742
|
+
*
|
|
26743
|
+
* Algorithm: two-window weighted counter. The previous window's tally
|
|
26744
|
+
* decays linearly as the current window fills, giving a smoother limit
|
|
26745
|
+
* than a naive fixed window (which would allow a 2x burst at the
|
|
26746
|
+
* boundary) while remaining a single-key storage primitive.
|
|
26747
|
+
*
|
|
26748
|
+
* For Node deployments use `createMemoryRateLimiter` instead.
|
|
26749
|
+
*/
|
|
26750
|
+
/**
|
|
26751
|
+
* Probability of running opportunistic cleanup on any given write.
|
|
26752
|
+
* 1% strikes a balance between bounded table growth and per-request cost.
|
|
26753
|
+
*/ var CLEANUP_PROBABILITY = .01;
|
|
26754
|
+
function createD1RateLimiter(db, schema, now = () => Math.floor(Date.now() / 1e3)) {
|
|
26755
|
+
const { rateLimit } = schema;
|
|
26756
|
+
return { async check(key, opts) {
|
|
26757
|
+
const { limit, windowSec } = opts;
|
|
26758
|
+
const nowSec = now();
|
|
26759
|
+
const currentWindow = Math.floor(nowSec / windowSec) * windowSec;
|
|
26760
|
+
const previousWindow = currentWindow - windowSec;
|
|
26761
|
+
const rows = await db.select({
|
|
26762
|
+
windowStart: rateLimit.windowStart,
|
|
26763
|
+
count: rateLimit.count
|
|
26764
|
+
}).from(rateLimit).where(and(eq(rateLimit.key, key), inArray(rateLimit.windowStart, [currentWindow, previousWindow])));
|
|
26765
|
+
let currentCount = 0;
|
|
26766
|
+
let previousCount = 0;
|
|
26767
|
+
for (const row of rows) if (row.windowStart === currentWindow) currentCount = row.count;
|
|
26768
|
+
else if (row.windowStart === previousWindow) previousCount = row.count;
|
|
26769
|
+
const elapsed = nowSec - currentWindow;
|
|
26770
|
+
const prevWeight = 1 - elapsed / windowSec;
|
|
26771
|
+
if (previousCount * prevWeight + currentCount >= limit) return {
|
|
26772
|
+
ok: false,
|
|
26773
|
+
retryAfterSec: Math.max(1, windowSec - elapsed)
|
|
26774
|
+
};
|
|
26775
|
+
await db.insert(rateLimit).values({
|
|
26776
|
+
key,
|
|
26777
|
+
windowStart: currentWindow,
|
|
26778
|
+
count: 1
|
|
26779
|
+
}).onConflictDoUpdate({
|
|
26780
|
+
target: [rateLimit.key, rateLimit.windowStart],
|
|
26781
|
+
set: { count: sql`${rateLimit.count} + 1` }
|
|
26782
|
+
});
|
|
26783
|
+
if (Math.random() < CLEANUP_PROBABILITY) await db.delete(rateLimit).where(lt(rateLimit.windowStart, currentWindow - windowSec * 2));
|
|
26784
|
+
return { ok: true };
|
|
26785
|
+
} };
|
|
26786
|
+
}
|
|
26787
|
+
//#endregion
|
|
26364
26788
|
//#region src/lib/hosted-sso.ts
|
|
26365
26789
|
var textEncoder = new TextEncoder();
|
|
26366
26790
|
var textDecoder = new TextDecoder();
|
|
@@ -27649,6 +28073,51 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
27649
28073
|
if (filters.offset !== void 0) query = query.offset(filters.offset);
|
|
27650
28074
|
return hydratePosts(await query);
|
|
27651
28075
|
},
|
|
28076
|
+
async listForSitemap({ afterId, limit }) {
|
|
28077
|
+
const conditions = buildFilterConditions({
|
|
28078
|
+
status: "published",
|
|
28079
|
+
excludePrivate: true,
|
|
28080
|
+
excludeReplies: true
|
|
28081
|
+
});
|
|
28082
|
+
if (afterId !== void 0) conditions.push(sql`${posts.id} > ${afterId}`);
|
|
28083
|
+
const rows = await db.select({
|
|
28084
|
+
id: posts.id,
|
|
28085
|
+
updatedAt: posts.updatedAt,
|
|
28086
|
+
featuredAt: posts.featuredAt
|
|
28087
|
+
}).from(posts).where(conditions.length > 0 ? and(...conditions) : void 0).orderBy(asc(posts.id)).limit(limit);
|
|
28088
|
+
if (rows.length === 0) return [];
|
|
28089
|
+
const ids = rows.map((row) => row.id);
|
|
28090
|
+
const [slugMap, aliasesMap] = await Promise.all([resolvedPaths.getPostSlugMap(ids), resolvedPaths.getPostAliases(ids)]);
|
|
28091
|
+
return rows.map((row) => {
|
|
28092
|
+
const slug = slugMap.get(row.id);
|
|
28093
|
+
if (!slug) return null;
|
|
28094
|
+
const alias = aliasesMap.get(row.id)?.[0] ?? null;
|
|
28095
|
+
return {
|
|
28096
|
+
id: row.id,
|
|
28097
|
+
slug,
|
|
28098
|
+
alias,
|
|
28099
|
+
updatedAt: row.updatedAt,
|
|
28100
|
+
featuredAt: row.featuredAt
|
|
28101
|
+
};
|
|
28102
|
+
}).filter((entry) => entry !== null);
|
|
28103
|
+
},
|
|
28104
|
+
async countForSitemap() {
|
|
28105
|
+
const conditions = buildFilterConditions({
|
|
28106
|
+
status: "published",
|
|
28107
|
+
excludePrivate: true,
|
|
28108
|
+
excludeReplies: true
|
|
28109
|
+
});
|
|
28110
|
+
return (await db.select({ count: sql`CAST(count(*) AS INTEGER)`.as("count") }).from(posts).where(conditions.length > 0 ? and(...conditions) : void 0))[0]?.count ?? 0;
|
|
28111
|
+
},
|
|
28112
|
+
async getSitemapIdAt(offset) {
|
|
28113
|
+
if (offset < 0) return null;
|
|
28114
|
+
const conditions = buildFilterConditions({
|
|
28115
|
+
status: "published",
|
|
28116
|
+
excludePrivate: true,
|
|
28117
|
+
excludeReplies: true
|
|
28118
|
+
});
|
|
28119
|
+
return (await db.select({ id: posts.id }).from(posts).where(conditions.length > 0 ? and(...conditions) : void 0).orderBy(asc(posts.id)).limit(1).offset(offset))[0]?.id ?? null;
|
|
28120
|
+
},
|
|
27652
28121
|
async count(filters = {}) {
|
|
27653
28122
|
const conditions = buildFilterConditions(filters);
|
|
27654
28123
|
return (await db.select({ count: sql`CAST(count(*) AS INTEGER)`.as("count") }).from(posts).where(conditions.length > 0 ? and(...conditions) : void 0))[0]?.count ?? 0;
|
|
@@ -27688,7 +28157,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
27688
28157
|
hasAttachments: data.attachments ? data.attachments.length > 0 : void 0
|
|
27689
28158
|
});
|
|
27690
28159
|
const bodyHtml = body ? renderTiptapJson(body) : null;
|
|
27691
|
-
const bodyText = body ? extractBodyText(body) : null;
|
|
28160
|
+
const bodyText = body ? extractBodyText(body, { includeLinkHrefs: true }) : null;
|
|
27692
28161
|
let summary = null;
|
|
27693
28162
|
if (format === "note" && title && body && summaryConfig) summary = extractSummary(body, summaryConfig.maxParagraphs, summaryConfig.maxChars);
|
|
27694
28163
|
let threadId = id;
|
|
@@ -27968,7 +28437,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
27968
28437
|
const normalizedBody = rawBody ? trimTiptapBody(rawBody) : null;
|
|
27969
28438
|
updates.body = normalizedBody;
|
|
27970
28439
|
updates.bodyHtml = normalizedBody ? renderTiptapJson(normalizedBody) : null;
|
|
27971
|
-
updates.bodyText = normalizedBody ? extractBodyText(normalizedBody) : null;
|
|
28440
|
+
updates.bodyText = normalizedBody ? extractBodyText(normalizedBody, { includeLinkHrefs: true }) : null;
|
|
27972
28441
|
}
|
|
27973
28442
|
if (summaryConfig) {
|
|
27974
28443
|
const format = nextFormat;
|
|
@@ -28406,6 +28875,40 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
28406
28875
|
const conditions = [...buildFilterConditions(filters), isNotNull(posts.publishedAt)];
|
|
28407
28876
|
const publishedYearExpr = buildPublishedYearExpr();
|
|
28408
28877
|
return (await db.select({ year: publishedYearExpr.as("year") }).from(posts).where(conditions.length > 0 ? and(...conditions) : void 0).groupBy(publishedYearExpr).orderBy(desc(publishedYearExpr))).map((r) => parseInt(r.year, 10));
|
|
28878
|
+
},
|
|
28879
|
+
async reindexBodyText(options = {}) {
|
|
28880
|
+
const requested = options.limit ?? 50;
|
|
28881
|
+
const limit = Math.min(Math.max(Math.trunc(requested), 1), 500);
|
|
28882
|
+
const cursor = options.cursor;
|
|
28883
|
+
const whereConditions = [eq(posts.siteId, siteId), isNull(posts.deletedAt)];
|
|
28884
|
+
if (cursor) whereConditions.push(gt(posts.id, cursor));
|
|
28885
|
+
const rows = await db.select({
|
|
28886
|
+
id: posts.id,
|
|
28887
|
+
body: posts.body,
|
|
28888
|
+
bodyText: posts.bodyText
|
|
28889
|
+
}).from(posts).where(and(...whereConditions)).orderBy(asc(posts.id)).limit(limit + 1);
|
|
28890
|
+
const hasMore = rows.length > limit;
|
|
28891
|
+
const batch = hasMore ? rows.slice(0, limit) : rows;
|
|
28892
|
+
let updated = 0;
|
|
28893
|
+
let skipped = 0;
|
|
28894
|
+
for (const row of batch) {
|
|
28895
|
+
const nextBodyText = row.body ? extractBodyText(row.body, { includeLinkHrefs: true }) : null;
|
|
28896
|
+
if (nextBodyText === row.bodyText) {
|
|
28897
|
+
skipped++;
|
|
28898
|
+
continue;
|
|
28899
|
+
}
|
|
28900
|
+
await db.update(posts).set({ bodyText: nextBodyText }).where(and(eq(posts.siteId, siteId), eq(posts.id, row.id)));
|
|
28901
|
+
updated++;
|
|
28902
|
+
}
|
|
28903
|
+
const lastRow = batch.at(-1);
|
|
28904
|
+
const lastId = lastRow ? lastRow.id : null;
|
|
28905
|
+
return {
|
|
28906
|
+
processed: batch.length,
|
|
28907
|
+
updated,
|
|
28908
|
+
skipped,
|
|
28909
|
+
nextCursor: hasMore ? lastId : null,
|
|
28910
|
+
done: !hasMore
|
|
28911
|
+
};
|
|
28409
28912
|
}
|
|
28410
28913
|
};
|
|
28411
28914
|
}
|
|
@@ -29933,10 +30436,28 @@ function createSiteAdminService(db, databaseSchema = sqliteSchemaBundle, databas
|
|
|
29933
30436
|
const pathPrefix = domain.pathPrefix ?? "";
|
|
29934
30437
|
return `${protocol}//${domain.host}${pathPrefix}`;
|
|
29935
30438
|
}
|
|
30439
|
+
async function loadByIdempotencyKey(targetDb, idempotencyKey) {
|
|
30440
|
+
const siteRow = (await targetDb.select().from(sites).where(eq(sites.provisioningIdempotencyKey, idempotencyKey)).limit(1))[0];
|
|
30441
|
+
if (!siteRow) return null;
|
|
30442
|
+
const domainRow = (await targetDb.select().from(siteDomains).where(and(eq(siteDomains.siteId, siteRow.id), eq(siteDomains.kind, "primary"))).limit(1))[0];
|
|
30443
|
+
if (!domainRow) return null;
|
|
30444
|
+
return {
|
|
30445
|
+
site: toSite(siteRow),
|
|
30446
|
+
domain: toSiteDomain(domainRow)
|
|
30447
|
+
};
|
|
30448
|
+
}
|
|
29936
30449
|
async function createWithDatabase(targetDb, input) {
|
|
29937
30450
|
const siteKey = input.key.trim();
|
|
29938
30451
|
const primaryHost = input.primaryHost.trim().toLowerCase();
|
|
29939
30452
|
const siteName = input.siteName.trim();
|
|
30453
|
+
const idempotencyKey = input.idempotencyKey?.trim() || null;
|
|
30454
|
+
if (idempotencyKey) {
|
|
30455
|
+
const existing = await loadByIdempotencyKey(targetDb, idempotencyKey);
|
|
30456
|
+
if (existing) {
|
|
30457
|
+
if (existing.site.key !== siteKey || existing.domain.host !== primaryHost) throw new ConflictError("Idempotency key was reused with a different site key or primary host.");
|
|
30458
|
+
return existing;
|
|
30459
|
+
}
|
|
30460
|
+
}
|
|
29940
30461
|
if ((await targetDb.select({ id: sites.id }).from(sites).where(eq(sites.key, siteKey)).limit(1))[0]) throw new ConflictError("Site key is already in use.");
|
|
29941
30462
|
if ((await targetDb.select({ id: siteDomains.id }).from(siteDomains).where(eq(siteDomains.host, primaryHost)).limit(1))[0]) throw new ConflictError("Primary host is already in use.");
|
|
29942
30463
|
const timestamp = now();
|
|
@@ -29946,6 +30467,7 @@ function createSiteAdminService(db, databaseSchema = sqliteSchemaBundle, databas
|
|
|
29946
30467
|
id: siteId,
|
|
29947
30468
|
key: siteKey,
|
|
29948
30469
|
status: "active",
|
|
30470
|
+
provisioningIdempotencyKey: idempotencyKey,
|
|
29949
30471
|
createdAt: timestamp,
|
|
29950
30472
|
updatedAt: timestamp
|
|
29951
30473
|
}).returning())[0];
|
|
@@ -30863,6 +31385,7 @@ function getResolvedSiteBaseUrl(env, publicRequestUrl, pathPrefix) {
|
|
|
30863
31385
|
schema: sqliteSchemaBundle,
|
|
30864
31386
|
secret: hostedControlPlaneSsoSecret
|
|
30865
31387
|
}),
|
|
31388
|
+
rateLimiter: createD1RateLimiter(db, sqliteSchemaBundle),
|
|
30866
31389
|
services: createServices(db, session, siteLookup.site.id, {
|
|
30867
31390
|
databaseDialect: "sqlite",
|
|
30868
31391
|
bootstrapSite: getSingleSiteBootstrapOptions(env),
|
|
@@ -30876,7 +31399,76 @@ function getResolvedSiteBaseUrl(env, publicRequestUrl, pathPrefix) {
|
|
|
30876
31399
|
};
|
|
30877
31400
|
}
|
|
30878
31401
|
//#endregion
|
|
31402
|
+
//#region src/lib/rate-limit-memory.ts
|
|
31403
|
+
/**
|
|
31404
|
+
* In-Memory Rate Limiter
|
|
31405
|
+
*
|
|
31406
|
+
* Used by the Node runtime. The server process is long-lived and
|
|
31407
|
+
* single-instance, so a local `Map` is reliable and avoids unnecessary DB
|
|
31408
|
+
* round-trips. Uses the classic sliding-window-counter algorithm (two
|
|
31409
|
+
* aligned buckets with a weighted estimate) for smooth limiting without
|
|
31410
|
+
* the 2x boundary burst of a fixed window.
|
|
31411
|
+
*
|
|
31412
|
+
* On Cloudflare Workers use `createD1RateLimiter` instead — isolates are
|
|
31413
|
+
* ephemeral and cannot share memory across requests.
|
|
31414
|
+
*/ /**
|
|
31415
|
+
* Number of live keys after which we prune old buckets on the next write.
|
|
31416
|
+
* Keeps the Map bounded under abuse without paying for eager sweeps.
|
|
31417
|
+
*/ var SWEEP_THRESHOLD = 1e4;
|
|
31418
|
+
/**
|
|
31419
|
+
* Creates an isolated in-memory limiter. Tests construct one per test app;
|
|
31420
|
+
* the Node runtime holds a single module-level instance across requests.
|
|
31421
|
+
*
|
|
31422
|
+
* `now` is injectable so tests can assert window-rollover behavior without
|
|
31423
|
+
* relying on real time.
|
|
31424
|
+
*/ function createMemoryRateLimiter(now = () => Math.floor(Date.now() / 1e3)) {
|
|
31425
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
31426
|
+
function sweep(nowSec) {
|
|
31427
|
+
if (buckets.size < SWEEP_THRESHOLD) return;
|
|
31428
|
+
const cutoff = nowSec - 600;
|
|
31429
|
+
for (const [key, bucket] of buckets) if (bucket.windowStart < cutoff) buckets.delete(key);
|
|
31430
|
+
}
|
|
31431
|
+
return { async check(key, opts) {
|
|
31432
|
+
const { limit, windowSec } = opts;
|
|
31433
|
+
const nowSec = now();
|
|
31434
|
+
const currentWindow = Math.floor(nowSec / windowSec) * windowSec;
|
|
31435
|
+
let bucket = buckets.get(key);
|
|
31436
|
+
if (!bucket) {
|
|
31437
|
+
bucket = {
|
|
31438
|
+
windowStart: currentWindow,
|
|
31439
|
+
count: 0,
|
|
31440
|
+
prevCount: 0
|
|
31441
|
+
};
|
|
31442
|
+
buckets.set(key, bucket);
|
|
31443
|
+
} else if (bucket.windowStart !== currentWindow) {
|
|
31444
|
+
if (bucket.windowStart === currentWindow - windowSec) bucket.prevCount = bucket.count;
|
|
31445
|
+
else bucket.prevCount = 0;
|
|
31446
|
+
bucket.count = 0;
|
|
31447
|
+
bucket.windowStart = currentWindow;
|
|
31448
|
+
}
|
|
31449
|
+
const elapsed = nowSec - currentWindow;
|
|
31450
|
+
const prevWeight = 1 - elapsed / windowSec;
|
|
31451
|
+
if (bucket.prevCount * prevWeight + bucket.count >= limit) return {
|
|
31452
|
+
ok: false,
|
|
31453
|
+
retryAfterSec: Math.max(1, windowSec - elapsed)
|
|
31454
|
+
};
|
|
31455
|
+
bucket.count += 1;
|
|
31456
|
+
sweep(nowSec);
|
|
31457
|
+
return { ok: true };
|
|
31458
|
+
} };
|
|
31459
|
+
}
|
|
31460
|
+
//#endregion
|
|
30879
31461
|
//#region src/runtime/node.ts
|
|
31462
|
+
/**
|
|
31463
|
+
* Single process-wide rate limiter for the Node runtime. Node serves all
|
|
31464
|
+
* requests out of one persistent process, so in-memory counters are
|
|
31465
|
+
* reliable and avoid per-request D1 round-trips. Constructed lazily on
|
|
31466
|
+
* first use so tests that never build a request runtime don't pay for it.
|
|
31467
|
+
*/ var sharedNodeRateLimiter = null;
|
|
31468
|
+
function getNodeRateLimiter() {
|
|
31469
|
+
sharedNodeRateLimiter ??= createMemoryRateLimiter();
|
|
31470
|
+
return sharedNodeRateLimiter;
|
|
31471
|
+
}
|
|
30880
31472
|
function createBetterSqliteRawQuery(sqlite) {
|
|
30881
31473
|
return { prepare(query) {
|
|
30882
31474
|
let params = [];
|
|
@@ -30928,6 +31520,7 @@ function createBetterSqliteRawQuery(sqlite) {
|
|
|
30928
31520
|
schema: databaseSchema,
|
|
30929
31521
|
secret: hostedControlPlaneSsoSecret
|
|
30930
31522
|
}),
|
|
31523
|
+
rateLimiter: getNodeRateLimiter(),
|
|
30931
31524
|
services: createServices(db, rawQuery, siteLookup.site.id, {
|
|
30932
31525
|
databaseDialect,
|
|
30933
31526
|
bootstrapSite: getSingleSiteBootstrapOptions(env),
|
|
@@ -31204,8 +31797,10 @@ async function servePublicStorage(c) {
|
|
|
31204
31797
|
c.set("auth", runtime.auth);
|
|
31205
31798
|
c.set("currentSite", runtime.currentSite);
|
|
31206
31799
|
c.set("currentSiteDomain", runtime.currentSiteDomain);
|
|
31800
|
+
c.set("rateLimiter", runtime.rateLimiter);
|
|
31207
31801
|
await next();
|
|
31208
31802
|
});
|
|
31803
|
+
app.use("*", attachSession());
|
|
31209
31804
|
app.use("*", async (c, next) => {
|
|
31210
31805
|
const redirectUrl = await getHostedCanonicalRedirect({
|
|
31211
31806
|
currentSite: c.var.currentSite,
|
|
@@ -31223,6 +31818,7 @@ async function servePublicStorage(c) {
|
|
|
31223
31818
|
app.route("/api/internal/api-tokens", internalApiTokensRoutes);
|
|
31224
31819
|
app.route("/api/internal/sites", internalSitesRoutes);
|
|
31225
31820
|
app.route("/api/internal/text-attachments", internalTextAttachmentsRoutes);
|
|
31821
|
+
app.route("/api/internal/search/reindex", internalSearchReindexRoutes);
|
|
31226
31822
|
app.route("/api/internal/uploads", internalUploadsRoutes);
|
|
31227
31823
|
app.route("/api/github-sync", githubSyncWebhookRoutes);
|
|
31228
31824
|
app.get("/api/media/:id/content", async (c) => {
|
|
@@ -31352,4 +31948,4 @@ async function servePublicStorage(c) {
|
|
|
31352
31948
|
return app;
|
|
31353
31949
|
}
|
|
31354
31950
|
//#endregion
|
|
31355
|
-
export {
|
|
31951
|
+
export { NAV_ITEM_TYPES$2 as A, toPostView as C, MAX_MEDIA_ATTACHMENTS as D, FORMATS$2 as E, BUILTIN_COLOR_THEMES as F, getPublicAssetBasePath as I, isAssetPath as L, STATUSES$2 as M, TEXT_ATTACHMENT_CONTENT_FORMATS as N, MAX_PINNED_POSTS as O, buildThemeStyle as P, toNavItemViews as S, toSearchResultView as T, createMediaContext as _, createSiteService as a, toMediaView as b, resolveConfig as c, getFontThemeCssVariables as d, defaultFeedRenderer as f, schema_exports$1 as g, createNodeDatabase as h, createNodeRequestRuntime as i, SORT_ORDERS as j, MEDIA_KINDS as k, BUILTIN_FONT_THEMES as l, sqliteSchemaBundle as m, createRequestRuntime as n, resolveDatabaseDialect as o, pgSchemaBundle as p, createNodeCliRuntime as r, getHostBasedStartupConfigurationIssues as s, createApp as t, getCjkSerifCssVariables as u, toArchiveGroups as v, toPostViews as w, toNavItemView as x, toArchiveGroupsWithMedia as y };
|