@jant/core 0.3.42 → 0.3.43
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-Ctl0T0zO.js +5 -0
- package/dist/{app-Cu3lveYI.js → app-GbfwoeDJ.js} +630 -123
- package/dist/client/.vite/manifest.json +1 -1
- 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/meta/0018_snapshot.json +2225 -0
- package/src/db/migrations/meta/_journal.json +8 -1
- package/src/db/migrations/pg/0016_familiar_lionheart.sql +6 -0
- package/src/db/migrations/pg/meta/0016_snapshot.json +2840 -0
- package/src/db/migrations/pg/meta/_journal.json +8 -1
- package/src/db/pg/schema.ts +18 -0
- package/src/db/schema.ts +23 -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/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/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/export.ts +9 -2
- package/src/services/post.ts +200 -2
- 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/layouts/BaseLayout.tsx +9 -0
- package/dist/app-DzCB4yOp.js +0 -5
|
@@ -1,13 +1,13 @@
|
|
|
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
7
|
import { z } from "zod";
|
|
8
8
|
import { fromString, typeidUnboxed } from "typeid-js";
|
|
9
9
|
import { decode } from "blurhash";
|
|
10
|
-
import { and, asc, desc, eq, inArray, isNotNull, isNull, lt, lte, ne, or, sql } from "drizzle-orm";
|
|
10
|
+
import { and, asc, desc, eq, gt, inArray, isNotNull, isNull, lt, lte, ne, or, sql } from "drizzle-orm";
|
|
11
11
|
import { generateKeyBetween } from "fractional-indexing";
|
|
12
12
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
13
13
|
import { drizzle as drizzle$1 } from "drizzle-orm/d1";
|
|
@@ -3352,9 +3352,9 @@ 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.43-8e1ad0b8fb20998c";
|
|
3356
3356
|
var CLIENT_JS_FILE = "/_assets/client-D95FNDg5.js";
|
|
3357
|
-
var CLIENT_AUTH_JS_FILE = "/_assets/client-auth-
|
|
3357
|
+
var CLIENT_AUTH_JS_FILE = "/_assets/client-auth-CXILhW1b.js";
|
|
3358
3358
|
var CLIENT_CSS_FILE = "/_assets/client-C_kImWZj.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";
|
|
@@ -3370,7 +3370,7 @@ var CLIENT_CJK_KR_CSS_FILE = "/_assets/client-cjk-kr-_3ZNI2ZP.css";
|
|
|
3370
3370
|
*
|
|
3371
3371
|
* In dev mode (Vite), serves assets via Vite's dev server.
|
|
3372
3372
|
* 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 }) => {
|
|
3373
|
+
*/ var BaseLayout = ({ title, description, lang, c, toast, faviconHref, appleTouchHref, faviconUrl, faviconVersion, socialImageUrl, canonicalHref, noindex, isAuthenticated = false, clientBundle, children }) => {
|
|
3374
3374
|
const resolvedLang = lang ?? (c ? c.get("lang") : "en");
|
|
3375
3375
|
const appConfig = c ? c.get("appConfig") : void 0;
|
|
3376
3376
|
const resolvedSocialImagePath = socialImageUrl ?? faviconUrl ?? appConfig?.siteAvatarUrl ?? getJantIconHref("socialImage", appConfig?.sitePathPrefix || "");
|
|
@@ -3398,7 +3398,7 @@ var CLIENT_CJK_KR_CSS_FILE = "/_assets/client-cjk-kr-_3ZNI2ZP.css";
|
|
|
3398
3398
|
const cjkSerifFont = appConfig?.cjkSerifFont ?? "off";
|
|
3399
3399
|
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
3400
|
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.
|
|
3401
|
+
const faviconAssetVersion = resolvedFaviconVersion || "0.3.43-8e1ad0b8fb20998c";
|
|
3402
3402
|
const resolvedFaviconHref = faviconHref ?? (faviconAssetVersion ? toPublicPath(`/favicon.ico?v=${faviconAssetVersion}`, sitePathPrefix) : toPublicPath("/favicon.ico", sitePathPrefix));
|
|
3403
3403
|
const resolvedAppleTouchHref = appleTouchHref ?? (faviconAssetVersion ? toPublicPath(`/apple-touch-icon.png?v=${faviconAssetVersion}`, sitePathPrefix) : toPublicPath("/apple-touch-icon.png", sitePathPrefix));
|
|
3404
3404
|
const socialImageHref = resolvedSocialImagePath && (isFullUrl(resolvedSocialImagePath) || resolvedSocialImagePath.startsWith("//") ? resolvedSocialImagePath : toAbsoluteSiteUrl(resolvedSocialImagePath, appConfig?.siteUrl || "", sitePathPrefix));
|
|
@@ -3497,6 +3497,10 @@ var CLIENT_CJK_KR_CSS_FILE = "/_assets/client-cjk-kr-_3ZNI2ZP.css";
|
|
|
3497
3497
|
name: "robots",
|
|
3498
3498
|
content: "noindex, nofollow"
|
|
3499
3499
|
}),
|
|
3500
|
+
canonicalHref && /* @__PURE__ */ jsxDEV$1("link", {
|
|
3501
|
+
rel: "canonical",
|
|
3502
|
+
href: canonicalHref
|
|
3503
|
+
}),
|
|
3500
3504
|
/* @__PURE__ */ jsxDEV$1("link", {
|
|
3501
3505
|
rel: "icon",
|
|
3502
3506
|
href: resolvedFaviconHref,
|
|
@@ -5381,9 +5385,10 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
5381
5385
|
});
|
|
5382
5386
|
//#endregion
|
|
5383
5387
|
//#region src/lib/hosted-signin.ts
|
|
5384
|
-
function getHostedAdminContinuationPath(publicRequestUrl) {
|
|
5388
|
+
function getHostedAdminContinuationPath(publicRequestUrl, redirect) {
|
|
5385
5389
|
const currentHost = new URL(publicRequestUrl).host;
|
|
5386
|
-
|
|
5390
|
+
const safeRedirect = isSafeInternalRedirect(redirect) ? redirect : "/";
|
|
5391
|
+
return `/auth/handoff/start?host=${encodeURIComponent(currentHost)}&redirect=${encodeURIComponent(safeRedirect)}`;
|
|
5387
5392
|
}
|
|
5388
5393
|
function buildHostedControlPlaneUrl(env, pathname, search) {
|
|
5389
5394
|
const hostedControlPlaneBaseUrl = getHostedControlPlaneBaseUrl(env);
|
|
@@ -5393,8 +5398,8 @@ function buildHostedControlPlaneUrl(env, pathname, search) {
|
|
|
5393
5398
|
location.search = search ?? "";
|
|
5394
5399
|
return location.toString();
|
|
5395
5400
|
}
|
|
5396
|
-
function getHostedControlPlaneSigninUrl(env, publicRequestUrl) {
|
|
5397
|
-
return buildHostedControlPlaneUrl(env, "/auth/handoff/start", getHostedAdminContinuationPath(publicRequestUrl).replace(/^\/auth\/handoff\/start/, ""));
|
|
5401
|
+
function getHostedControlPlaneSigninUrl(env, publicRequestUrl, redirect) {
|
|
5402
|
+
return buildHostedControlPlaneUrl(env, "/auth/handoff/start", getHostedAdminContinuationPath(publicRequestUrl, redirect).replace(/^\/auth\/handoff\/start/, ""));
|
|
5398
5403
|
}
|
|
5399
5404
|
function getHostedControlPlaneResetUrl(env, publicRequestUrl) {
|
|
5400
5405
|
const search = new URLSearchParams();
|
|
@@ -5419,11 +5424,12 @@ function getHostedControlPlaneProviderLabel(env) {
|
|
|
5419
5424
|
//#region src/routes/auth/signin.tsx
|
|
5420
5425
|
/**
|
|
5421
5426
|
* Sign-in / Sign-out Routes
|
|
5422
|
-
*/ var SigninContent = ({ demoEmail, demoPassword, sitePathPrefix = "" }) => {
|
|
5427
|
+
*/ var SigninContent = ({ demoEmail, demoPassword, sitePathPrefix = "", redirect }) => {
|
|
5423
5428
|
const { i18n } = useLingui();
|
|
5424
5429
|
const signals = JSON.stringify({
|
|
5425
5430
|
email: demoEmail || "",
|
|
5426
|
-
password: demoPassword || ""
|
|
5431
|
+
password: demoPassword || "",
|
|
5432
|
+
...redirect ? { redirect } : {}
|
|
5427
5433
|
}).replace(/</g, "\\u003c");
|
|
5428
5434
|
return /* @__PURE__ */ jsxDEV$1("div", {
|
|
5429
5435
|
class: "min-h-screen flex items-center justify-center",
|
|
@@ -5488,7 +5494,9 @@ function getHostedControlPlaneProviderLabel(env) {
|
|
|
5488
5494
|
};
|
|
5489
5495
|
var signinRoutes = new Hono();
|
|
5490
5496
|
signinRoutes.get("/signin", async (c) => {
|
|
5491
|
-
const
|
|
5497
|
+
const rawRedirect = c.req.query("redirect");
|
|
5498
|
+
const redirect = isSafeInternalRedirect(rawRedirect) ? rawRedirect : void 0;
|
|
5499
|
+
const hostedSigninUrl = getHostedControlPlaneSigninUrl(c.env, c.var.publicRequestUrl, redirect);
|
|
5492
5500
|
if (hostedSigninUrl) return c.redirect(hostedSigninUrl);
|
|
5493
5501
|
const i18n = getI18n(c);
|
|
5494
5502
|
const isSetup = c.req.query("setup") !== void 0;
|
|
@@ -5503,7 +5511,8 @@ signinRoutes.get("/signin", async (c) => {
|
|
|
5503
5511
|
children: /* @__PURE__ */ jsxDEV$1(SigninContent, {
|
|
5504
5512
|
demoEmail: c.var.appConfig.demoEmail,
|
|
5505
5513
|
demoPassword: c.var.appConfig.demoPassword,
|
|
5506
|
-
sitePathPrefix: c.var.appConfig.sitePathPrefix
|
|
5514
|
+
sitePathPrefix: c.var.appConfig.sitePathPrefix,
|
|
5515
|
+
redirect
|
|
5507
5516
|
})
|
|
5508
5517
|
}));
|
|
5509
5518
|
});
|
|
@@ -5514,6 +5523,8 @@ signinRoutes.post("/signin", async (c) => {
|
|
|
5514
5523
|
const parsed = SigninSchema.safeParse(body);
|
|
5515
5524
|
if (!parsed.success) return dsToast(parsed.error.issues[0]?.message ?? i18n._({ id: "vXCC6J" }), "error");
|
|
5516
5525
|
const { email, password } = parsed.data;
|
|
5526
|
+
const rawRedirect = body && typeof body === "object" && "redirect" in body ? body.redirect : void 0;
|
|
5527
|
+
const redirectTarget = typeof rawRedirect === "string" && isSafeInternalRedirect(rawRedirect) ? rawRedirect : "/";
|
|
5517
5528
|
try {
|
|
5518
5529
|
const { headers } = await c.var.auth.api.signInEmail({
|
|
5519
5530
|
returnHeaders: true,
|
|
@@ -5523,7 +5534,7 @@ signinRoutes.post("/signin", async (c) => {
|
|
|
5523
5534
|
},
|
|
5524
5535
|
headers: c.req.raw.headers
|
|
5525
5536
|
});
|
|
5526
|
-
return dsRedirect(toPublicPath(
|
|
5537
|
+
return dsRedirect(toPublicPath(redirectTarget, c.var.appConfig.sitePathPrefix), { headers });
|
|
5527
5538
|
} catch {
|
|
5528
5539
|
return dsToast(i18n._({ id: "nFukaP" }), "error");
|
|
5529
5540
|
}
|
|
@@ -5721,23 +5732,53 @@ function getRequestHostname(requestUrl, requestHost) {
|
|
|
5721
5732
|
return hostname ? isLocalHostname(hostname) : false;
|
|
5722
5733
|
}
|
|
5723
5734
|
/**
|
|
5735
|
+
* Paths that should never be used as post-signin redirect targets (would
|
|
5736
|
+
* either loop back to signin or hit an unauthenticated endpoint).
|
|
5737
|
+
*/ var POST_SIGNIN_REDIRECT_BLOCKLIST = new Set([
|
|
5738
|
+
"/signin",
|
|
5739
|
+
"/signout",
|
|
5740
|
+
"/setup",
|
|
5741
|
+
"/reset",
|
|
5742
|
+
"/__sso"
|
|
5743
|
+
]);
|
|
5744
|
+
function getPostSigninRedirect(requestUrl) {
|
|
5745
|
+
let url;
|
|
5746
|
+
try {
|
|
5747
|
+
url = new URL(requestUrl);
|
|
5748
|
+
} catch {
|
|
5749
|
+
return null;
|
|
5750
|
+
}
|
|
5751
|
+
const pathname = url.pathname || "/";
|
|
5752
|
+
if (POST_SIGNIN_REDIRECT_BLOCKLIST.has(pathname)) return null;
|
|
5753
|
+
if (pathname.startsWith("/api/")) return null;
|
|
5754
|
+
const candidate = `${pathname}${url.search}`;
|
|
5755
|
+
return isSafeInternalRedirect(candidate) ? candidate : null;
|
|
5756
|
+
}
|
|
5757
|
+
/**
|
|
5724
5758
|
* Middleware that requires authentication.
|
|
5725
5759
|
* Redirects to signin page if not authenticated.
|
|
5726
5760
|
* Session-only — Bearer tokens are not accepted for dashboard pages.
|
|
5727
5761
|
*/ function requireAuth(redirectTo = "/signin") {
|
|
5728
5762
|
return async (c, next) => {
|
|
5729
|
-
const
|
|
5763
|
+
const sitePathPrefix = getRuntimeSitePathPrefix({
|
|
5730
5764
|
env: c.env,
|
|
5731
5765
|
appConfig: c.var.appConfig,
|
|
5732
5766
|
currentSiteDomain: c.var.currentSiteDomain
|
|
5733
|
-
})
|
|
5767
|
+
});
|
|
5768
|
+
const buildRedirectTarget = () => {
|
|
5769
|
+
const publicHref = toPublicHref(redirectTo, sitePathPrefix);
|
|
5770
|
+
if (redirectTo !== "/signin") return publicHref;
|
|
5771
|
+
const postSignin = getPostSigninRedirect(c.req.url);
|
|
5772
|
+
if (!postSignin) return publicHref;
|
|
5773
|
+
return `${publicHref}${publicHref.includes("?") ? "&" : "?"}redirect=${encodeURIComponent(postSignin)}`;
|
|
5774
|
+
};
|
|
5775
|
+
const session = c.var.session;
|
|
5776
|
+
if (!session?.user) return c.redirect(buildRedirectTarget());
|
|
5734
5777
|
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);
|
|
5778
|
+
if (!await c.var.services.siteMembers.get(c.var.currentSite.id, session.user.id)) return c.redirect(buildRedirectTarget());
|
|
5738
5779
|
await next();
|
|
5739
5780
|
} catch {
|
|
5740
|
-
return c.redirect(
|
|
5781
|
+
return c.redirect(buildRedirectTarget());
|
|
5741
5782
|
}
|
|
5742
5783
|
};
|
|
5743
5784
|
}
|
|
@@ -5747,10 +5788,9 @@ function getRequestHostname(requestUrl, requestHost) {
|
|
|
5747
5788
|
* Returns 401 if neither method succeeds.
|
|
5748
5789
|
*/ function requireAuthApi() {
|
|
5749
5790
|
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();
|
|
5791
|
+
const session = c.var.session;
|
|
5792
|
+
if (session?.user) try {
|
|
5793
|
+
if (await c.var.services.siteMembers.get(c.var.currentSite.id, session.user.id)) {
|
|
5754
5794
|
await next();
|
|
5755
5795
|
return;
|
|
5756
5796
|
}
|
|
@@ -5827,7 +5867,7 @@ devAuthRoutes.get("/__dev/login", async (c) => {
|
|
|
5827
5867
|
headers: responseHeaders
|
|
5828
5868
|
});
|
|
5829
5869
|
} catch {
|
|
5830
|
-
return c.text("Dev login failed. Finish /setup once or run `mise run db-
|
|
5870
|
+
return c.text("Dev login failed. Finish /setup once or run `mise run db-wrangler-rebuild-demo`, then retry.", 500);
|
|
5831
5871
|
}
|
|
5832
5872
|
});
|
|
5833
5873
|
//#endregion
|
|
@@ -6314,7 +6354,8 @@ function getLegacyBodyPreview(post) {
|
|
|
6314
6354
|
}
|
|
6315
6355
|
function getPlainSummary(post) {
|
|
6316
6356
|
if (post.format === "quote") return normalizePreviewText(post.quoteText);
|
|
6317
|
-
|
|
6357
|
+
const cleanBody = post.body ? extractBodyText(post.body) : null;
|
|
6358
|
+
return normalizePreviewText(post.summary) || normalizePreviewText(cleanBody) || normalizePreviewText(post.bodyText) || getLegacyBodyPreview(post) || normalizePreviewText(post.url);
|
|
6318
6359
|
}
|
|
6319
6360
|
function clipPreviewText(text, maxChars) {
|
|
6320
6361
|
if (!text) return void 0;
|
|
@@ -6344,10 +6385,16 @@ function clipPreviewText(text, maxChars) {
|
|
|
6344
6385
|
if (result) {
|
|
6345
6386
|
summaryHtml = result.html;
|
|
6346
6387
|
summaryHasMore = result.hasMore;
|
|
6347
|
-
if (result.hasMore && post.bodyHtml) {
|
|
6348
|
-
const
|
|
6349
|
-
|
|
6350
|
-
|
|
6388
|
+
if (result.hasMore && post.bodyHtml) try {
|
|
6389
|
+
const doc = JSON.parse(post.body);
|
|
6390
|
+
if (doc.type === "doc" && Array.isArray(doc.content) && result.breakAtIndex > 0 && result.breakAtIndex <= doc.content.length) {
|
|
6391
|
+
const beforeHtml = renderTiptapDocument({
|
|
6392
|
+
type: "doc",
|
|
6393
|
+
content: doc.content.slice(0, result.breakAtIndex)
|
|
6394
|
+
});
|
|
6395
|
+
if (post.bodyHtml.startsWith(beforeHtml)) bodyHtmlWithAnchor = beforeHtml + "<span id=\"continue\"></span>" + post.bodyHtml.slice(beforeHtml.length);
|
|
6396
|
+
}
|
|
6397
|
+
} catch {}
|
|
6351
6398
|
}
|
|
6352
6399
|
}
|
|
6353
6400
|
const collections = (postCollections ?? []).map((c) => ({
|
|
@@ -6589,8 +6636,8 @@ function clipPreviewText(text, maxChars) {
|
|
|
6589
6636
|
* content: <MyContent />,
|
|
6590
6637
|
* });
|
|
6591
6638
|
* ```
|
|
6592
|
-
*/ async function getNavigationData(c) {
|
|
6593
|
-
const items = await c.var.services.navItems.list();
|
|
6639
|
+
*/ async function getNavigationData(c, options) {
|
|
6640
|
+
const items = options?.preloadedItems ?? await c.var.services.navItems.list();
|
|
6594
6641
|
const currentPath = c.var.publicPath;
|
|
6595
6642
|
const appConfig = c.var.appConfig;
|
|
6596
6643
|
const siteName = appConfig.siteName;
|
|
@@ -6602,11 +6649,8 @@ function clipPreviewText(text, maxChars) {
|
|
|
6602
6649
|
const siteAvatarUrl = appConfig.siteAvatarUrl || void 0;
|
|
6603
6650
|
const showHeaderAvatar = appConfig.showHeaderAvatar;
|
|
6604
6651
|
const siteFooterHtml = siteFooter ? render(siteFooter) : void 0;
|
|
6605
|
-
|
|
6652
|
+
const isAuthenticated = c.var.isAuthenticated;
|
|
6606
6653
|
let collections = [];
|
|
6607
|
-
try {
|
|
6608
|
-
isAuthenticated = !!(await c.var.auth.api.getSession({ headers: c.req.raw.headers }))?.user;
|
|
6609
|
-
} catch {}
|
|
6610
6654
|
const collectionNavIds = [];
|
|
6611
6655
|
for (const item of items) if (item.type === "collection" && item.collectionId) collectionNavIds.push(item.collectionId);
|
|
6612
6656
|
const collectionFreshness = collectionNavIds.length > 0 ? await c.var.services.navItems.getCollectionFreshness(collectionNavIds) : void 0;
|
|
@@ -7510,7 +7554,7 @@ var SiteLayout = ({ siteName, links, currentPath, sitePathPrefix = "", isAuthent
|
|
|
7510
7554
|
* });
|
|
7511
7555
|
* ```
|
|
7512
7556
|
*/ function renderPublicPage(c, options) {
|
|
7513
|
-
const { title, description, faviconHref, appleTouchHref, socialImageUrl, navData, content, sidebar, toast, showComposeDialog, showHeader, showHomeBranding, composeCollectionId } = options;
|
|
7557
|
+
const { title, description, faviconHref, appleTouchHref, socialImageUrl, canonicalHref, navData, content, sidebar, toast, showComposeDialog, showHeader, showHomeBranding, composeCollectionId } = options;
|
|
7514
7558
|
const metaDescription = description || navData.siteDescription || void 0;
|
|
7515
7559
|
const appConfig = c.get("appConfig");
|
|
7516
7560
|
const allSettings = c.get("allSettings");
|
|
@@ -7543,6 +7587,7 @@ var SiteLayout = ({ siteName, links, currentPath, sitePathPrefix = "", isAuthent
|
|
|
7543
7587
|
faviconHref,
|
|
7544
7588
|
appleTouchHref,
|
|
7545
7589
|
socialImageUrl,
|
|
7590
|
+
canonicalHref,
|
|
7546
7591
|
faviconUrl,
|
|
7547
7592
|
faviconVersion,
|
|
7548
7593
|
noindex,
|
|
@@ -10086,16 +10131,23 @@ var PaginatedPageHeader = ({ title, currentPage = 1, totalPages, description, ic
|
|
|
10086
10131
|
* Latest and Featured in navigation. The explicit feed routes still work.
|
|
10087
10132
|
*/ var homeRoutes = new Hono();
|
|
10088
10133
|
homeRoutes.get("/", async (c) => {
|
|
10089
|
-
const navData = await getNavigationData(c);
|
|
10090
10134
|
const i18n = getI18n(c);
|
|
10091
10135
|
const page = parsePageNumber(c.req.query("page"));
|
|
10092
10136
|
const paginatedPageTitle = formatPageLabel(page);
|
|
10093
|
-
|
|
10137
|
+
const isAuthenticated = c.var.isAuthenticated;
|
|
10138
|
+
const navItems = await c.var.services.navItems.list();
|
|
10139
|
+
const homeDefaultView = getHomeDefaultViewFromNavItems(navItems);
|
|
10140
|
+
const timelinePromise = homeDefaultView === "featured" ? assembleFeaturedTimeline(c, {
|
|
10141
|
+
page,
|
|
10142
|
+
isAuthenticated
|
|
10143
|
+
}) : assembleTimeline(c, {
|
|
10144
|
+
page,
|
|
10145
|
+
isAuthenticated
|
|
10146
|
+
});
|
|
10147
|
+
const [navData, timeline] = await Promise.all([getNavigationData(c, { preloadedItems: navItems }), timelinePromise]);
|
|
10148
|
+
const { items, currentPage, totalPages } = timeline;
|
|
10149
|
+
if (homeDefaultView === "featured") {
|
|
10094
10150
|
const featuredTitle = i18n._({ id: "FkMol5" });
|
|
10095
|
-
const { items, currentPage, totalPages } = await assembleFeaturedTimeline(c, {
|
|
10096
|
-
page,
|
|
10097
|
-
isAuthenticated: navData.isAuthenticated
|
|
10098
|
-
});
|
|
10099
10151
|
return renderPublicPage(c, {
|
|
10100
10152
|
title: page > 1 ? buildPageTitle(featuredTitle, paginatedPageTitle, navData.siteName) : navData.siteName,
|
|
10101
10153
|
navData,
|
|
@@ -10109,10 +10161,6 @@ homeRoutes.get("/", async (c) => {
|
|
|
10109
10161
|
});
|
|
10110
10162
|
}
|
|
10111
10163
|
const latestTitle = i18n._({ id: "wL3cK8" });
|
|
10112
|
-
const { items, currentPage, totalPages } = await assembleTimeline(c, {
|
|
10113
|
-
page,
|
|
10114
|
-
isAuthenticated: navData.isAuthenticated
|
|
10115
|
-
});
|
|
10116
10164
|
return renderPublicPage(c, {
|
|
10117
10165
|
title: page > 1 ? buildPageTitle(latestTitle, paginatedPageTitle, navData.siteName) : navData.siteName,
|
|
10118
10166
|
navData,
|
|
@@ -10170,6 +10218,17 @@ var PostPage = ({ post, threadPosts }) => {
|
|
|
10170
10218
|
//#region src/lib/post-meta.ts
|
|
10171
10219
|
var TITLE_MAX_CHARS = 72;
|
|
10172
10220
|
var DESCRIPTION_MAX_CHARS = 160;
|
|
10221
|
+
/**
|
|
10222
|
+
* Derive a clean plain-text projection of the body for human-facing meta.
|
|
10223
|
+
*
|
|
10224
|
+
* We cannot reuse `post.bodyText` here: that column is written with
|
|
10225
|
+
* `includeLinkHrefs: true` so inline link URLs land in the FTS index, which
|
|
10226
|
+
* pollutes the stored text with trailing URLs. Re-derive from the source
|
|
10227
|
+
* TipTap JSON (`post.body`) without that option.
|
|
10228
|
+
*/ function getCleanBodyText(post) {
|
|
10229
|
+
if (post.body) return extractBodyText(post.body) ?? "";
|
|
10230
|
+
return post.bodyText ?? "";
|
|
10231
|
+
}
|
|
10173
10232
|
function normalizeText(text) {
|
|
10174
10233
|
return (text ?? "").replace(/\s+/g, " ").trim();
|
|
10175
10234
|
}
|
|
@@ -10193,7 +10252,7 @@ function getTitleCandidate(post) {
|
|
|
10193
10252
|
if (normalizeText(post.title)) return normalizeText(post.title);
|
|
10194
10253
|
const summarySnippet = getFirstParagraph(post.summary);
|
|
10195
10254
|
if (summarySnippet) return clipText(summarySnippet, TITLE_MAX_CHARS);
|
|
10196
|
-
const bodySnippet = getFirstParagraph(post
|
|
10255
|
+
const bodySnippet = getFirstParagraph(getCleanBodyText(post));
|
|
10197
10256
|
if (bodySnippet) return clipText(bodySnippet, TITLE_MAX_CHARS);
|
|
10198
10257
|
if (post.format === "link" && post.url) return extractDisplayDomain(post.url) || post.url;
|
|
10199
10258
|
return "";
|
|
@@ -10205,7 +10264,7 @@ function getDescriptionCandidate(post) {
|
|
|
10205
10264
|
}
|
|
10206
10265
|
const summaryText = normalizeText(post.summary);
|
|
10207
10266
|
if (summaryText) return clipText(summaryText, DESCRIPTION_MAX_CHARS);
|
|
10208
|
-
const bodyText = normalizeText(post
|
|
10267
|
+
const bodyText = normalizeText(getCleanBodyText(post));
|
|
10209
10268
|
if (bodyText) return clipText(bodyText, DESCRIPTION_MAX_CHARS);
|
|
10210
10269
|
const quoteText = normalizeText(post.quoteText);
|
|
10211
10270
|
if (quoteText) return clipText(quoteText, DESCRIPTION_MAX_CHARS);
|
|
@@ -10305,6 +10364,7 @@ function buildPostMeta(post, siteName) {
|
|
|
10305
10364
|
pathRegistry: () => pathRegistry$1,
|
|
10306
10365
|
postCollections: () => postCollections$1,
|
|
10307
10366
|
posts: () => posts$1,
|
|
10367
|
+
rateLimit: () => rateLimit$2,
|
|
10308
10368
|
session: () => session$1,
|
|
10309
10369
|
settings: () => settings$1,
|
|
10310
10370
|
siteDomains: () => siteDomains$1,
|
|
@@ -10752,6 +10812,19 @@ var githubAppInstallation$1 = sqliteTable("github_app_installation", {
|
|
|
10752
10812
|
index("github_app_installation_by_site").on(table.siteId),
|
|
10753
10813
|
check("chk_github_app_installation_account_type", sql`${table.accountType} IN (${sqlTextEnum$1(GITHUB_APP_ACCOUNT_TYPES$1)})`)
|
|
10754
10814
|
]);
|
|
10815
|
+
/**
|
|
10816
|
+
* Per-key sliding-window rate-limit counters. Used by the Cloudflare Workers
|
|
10817
|
+
* runtime; Node deployments keep the state in memory instead.
|
|
10818
|
+
*
|
|
10819
|
+
* `key` is typically a scope prefix plus client IP (e.g. "search:1.2.3.4").
|
|
10820
|
+
* `window_start` is the aligned start of the window in Unix seconds — we
|
|
10821
|
+
* store two adjacent windows per key to support the sliding-window-counter
|
|
10822
|
+
* algorithm.
|
|
10823
|
+
*/ var rateLimit$2 = sqliteTable("rate_limit", {
|
|
10824
|
+
key: text("key").notNull(),
|
|
10825
|
+
windowStart: integer("window_start").notNull(),
|
|
10826
|
+
count: integer("count").notNull().default(0)
|
|
10827
|
+
}, (table) => [primaryKey({ columns: [table.key, table.windowStart] })]);
|
|
10755
10828
|
//#endregion
|
|
10756
10829
|
//#region src/db/index.ts
|
|
10757
10830
|
/**
|
|
@@ -10856,6 +10929,7 @@ function isNodeSqliteDatabase(db) {
|
|
|
10856
10929
|
pathRegistry: () => pathRegistry,
|
|
10857
10930
|
postCollections: () => postCollections,
|
|
10858
10931
|
posts: () => posts,
|
|
10932
|
+
rateLimit: () => rateLimit$1,
|
|
10859
10933
|
session: () => session,
|
|
10860
10934
|
settings: () => settings,
|
|
10861
10935
|
siteDomains: () => siteDomains,
|
|
@@ -11344,6 +11418,14 @@ var githubAppInstallation = pgTable("github_app_installation", {
|
|
|
11344
11418
|
index$1("github_app_installation_by_site").on(table.siteId),
|
|
11345
11419
|
check$1("chk_github_app_installation_account_type", sql`${table.accountType} IN (${sqlTextEnum(GITHUB_APP_ACCOUNT_TYPES)})`)
|
|
11346
11420
|
]);
|
|
11421
|
+
/**
|
|
11422
|
+
* Per-key sliding-window rate-limit counters. Mirrors the SQLite `rate_limit`
|
|
11423
|
+
* table — kept in lockstep because both dialects are production targets.
|
|
11424
|
+
*/ var rateLimit$1 = pgTable("rate_limit", {
|
|
11425
|
+
key: text$1("key").notNull(),
|
|
11426
|
+
windowStart: integer$1("window_start").notNull(),
|
|
11427
|
+
count: integer$1("count").notNull().default(0)
|
|
11428
|
+
}, (table) => [primaryKey$1({ columns: [table.key, table.windowStart] })]);
|
|
11347
11429
|
//#endregion
|
|
11348
11430
|
//#region src/db/schema-bundle.ts
|
|
11349
11431
|
var sqliteSchemaBundle = schema_exports$1;
|
|
@@ -12706,33 +12788,33 @@ function getAtomTitle(post) {
|
|
|
12706
12788
|
</feed>`;
|
|
12707
12789
|
}
|
|
12708
12790
|
/**
|
|
12709
|
-
*
|
|
12791
|
+
* Render a sitemap `<urlset>` XML document from a list of URL entries.
|
|
12710
12792
|
*
|
|
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>`;
|
|
12793
|
+
* Used by the sharded sitemap endpoints in `routes/feed/sitemap.ts`.
|
|
12794
|
+
*/ function renderSitemapUrlSet(entries) {
|
|
12729
12795
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
12730
12796
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
12731
|
-
|
|
12732
|
-
|
|
12733
|
-
|
|
12797
|
+
${entries.map((entry) => {
|
|
12798
|
+
const parts = [` <loc>${escapeXml(entry.loc)}</loc>`];
|
|
12799
|
+
if (entry.lastmod) parts.push(` <lastmod>${escapeXml(entry.lastmod)}</lastmod>`);
|
|
12800
|
+
if (entry.changefreq) parts.push(` <changefreq>${escapeXml(entry.changefreq)}</changefreq>`);
|
|
12801
|
+
if (entry.priority) parts.push(` <priority>${escapeXml(entry.priority)}</priority>`);
|
|
12802
|
+
return ` <url>\n${parts.join("\n")}\n </url>`;
|
|
12803
|
+
}).join("\n")}
|
|
12734
12804
|
</urlset>`;
|
|
12735
12805
|
}
|
|
12806
|
+
/**
|
|
12807
|
+
* Render a `<sitemapindex>` XML document listing shard sitemap URLs.
|
|
12808
|
+
*/ function renderSitemapIndex(entries) {
|
|
12809
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
12810
|
+
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
12811
|
+
${entries.map((entry) => {
|
|
12812
|
+
const parts = [` <loc>${escapeXml(entry.loc)}</loc>`];
|
|
12813
|
+
if (entry.lastmod) parts.push(` <lastmod>${escapeXml(entry.lastmod)}</lastmod>`);
|
|
12814
|
+
return ` <sitemap>\n${parts.join("\n")}\n </sitemap>`;
|
|
12815
|
+
}).join("\n")}
|
|
12816
|
+
</sitemapindex>`;
|
|
12817
|
+
}
|
|
12736
12818
|
//#endregion
|
|
12737
12819
|
//#region src/routes/pages/archive.tsx
|
|
12738
12820
|
/**
|
|
@@ -13760,12 +13842,29 @@ collectionRoutes.get("/:slug/feed", async (c) => {
|
|
|
13760
13842
|
* Resolves post slugs, aliases, redirects, and collection aliases.
|
|
13761
13843
|
* Must be registered last.
|
|
13762
13844
|
*/ var pageRoutes = new Hono();
|
|
13845
|
+
/**
|
|
13846
|
+
* Build the canonical absolute URL for a post page.
|
|
13847
|
+
*
|
|
13848
|
+
* Reply URLs render the full thread, so search engines see overlapping
|
|
13849
|
+
* content at every reply URL. Point the canonical to the thread root so
|
|
13850
|
+
* crawlers consolidate ranking on one URL.
|
|
13851
|
+
*
|
|
13852
|
+
* The root post is always at index 0 of `threadPostViews` (getThread orders
|
|
13853
|
+
* by createdAt ASC, and the DB check constraint guarantees root has the
|
|
13854
|
+
* smallest createdAt in its thread). When `threadPostViews` is undefined the
|
|
13855
|
+
* post is not part of a multi-post thread, so the post itself is the root.
|
|
13856
|
+
*/ function buildPostCanonicalHref(postView, threadPostViews, siteUrl) {
|
|
13857
|
+
const rootPermalink = threadPostViews?.[0]?.permalink ?? postView.permalink;
|
|
13858
|
+
if (!siteUrl) return rootPermalink;
|
|
13859
|
+
return new URL(rootPermalink, siteUrl).toString();
|
|
13860
|
+
}
|
|
13763
13861
|
async function renderPostWithTextPreview(c, post, autoOpen) {
|
|
13764
13862
|
const navDataPromise = getNavigationData(c);
|
|
13765
13863
|
const display = await assemblePostPageDisplay(c, post, { isAuthenticated: true });
|
|
13766
13864
|
if (!display) return c.notFound();
|
|
13767
13865
|
const navData = await navDataPromise;
|
|
13768
13866
|
const meta = buildPostMeta(post, navData.siteName);
|
|
13867
|
+
const canonicalHref = buildPostCanonicalHref(display.postView, display.threadPostViews, c.var.appConfig.siteUrl);
|
|
13769
13868
|
const pageTitle = autoOpen.attachmentTitle || meta.title;
|
|
13770
13869
|
const autoOpenMeta = JSON.stringify({
|
|
13771
13870
|
shareHref: autoOpen.shareHref,
|
|
@@ -13776,6 +13875,7 @@ async function renderPostWithTextPreview(c, post, autoOpen) {
|
|
|
13776
13875
|
return renderPublicPage(c, {
|
|
13777
13876
|
title: pageTitle,
|
|
13778
13877
|
description: meta.description,
|
|
13878
|
+
canonicalHref,
|
|
13779
13879
|
navData,
|
|
13780
13880
|
content: /* @__PURE__ */ jsxDEV$1(Fragment$1, { children: [
|
|
13781
13881
|
/* @__PURE__ */ jsxDEV$1(PostPage, {
|
|
@@ -13824,9 +13924,11 @@ async function renderPost(c, post) {
|
|
|
13824
13924
|
if (!display) return c.notFound();
|
|
13825
13925
|
const navData = await navDataPromise;
|
|
13826
13926
|
const meta = buildPostMeta(post, navData.siteName);
|
|
13927
|
+
const canonicalHref = buildPostCanonicalHref(display.postView, display.threadPostViews, c.var.appConfig.siteUrl);
|
|
13827
13928
|
return renderPublicPage(c, {
|
|
13828
13929
|
title: meta.title,
|
|
13829
13930
|
description: meta.description,
|
|
13931
|
+
canonicalHref,
|
|
13830
13932
|
navData,
|
|
13831
13933
|
content: /* @__PURE__ */ jsxDEV$1(PostPage, {
|
|
13832
13934
|
post: display.postView,
|
|
@@ -14845,18 +14947,11 @@ newPostRoutes.get("/new", async (c) => {
|
|
|
14845
14947
|
//#endregion
|
|
14846
14948
|
//#region src/routes/pages/partials.tsx
|
|
14847
14949
|
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
14950
|
partialPageRoutes.get("/_/version", (c) => {
|
|
14856
14951
|
return c.json({ version: CORE_VERSION });
|
|
14857
14952
|
});
|
|
14858
14953
|
partialPageRoutes.get("/_/timeline-item/:threadRootId", async (c) => {
|
|
14859
|
-
const item = await assembleTimelineItem(c, parseIdParam(c.req.param("threadRootId"), ID_PREFIX.post), { isAuthenticated:
|
|
14954
|
+
const item = await assembleTimelineItem(c, parseIdParam(c.req.param("threadRootId"), ID_PREFIX.post), { isAuthenticated: c.var.isAuthenticated });
|
|
14860
14955
|
if (!item) return c.notFound();
|
|
14861
14956
|
return c.html(/* @__PURE__ */ jsxDEV$1(I18nProvider, {
|
|
14862
14957
|
c,
|
|
@@ -14864,7 +14959,7 @@ partialPageRoutes.get("/_/timeline-item/:threadRootId", async (c) => {
|
|
|
14864
14959
|
}));
|
|
14865
14960
|
});
|
|
14866
14961
|
partialPageRoutes.get("/_/post-card/:postId", async (c) => {
|
|
14867
|
-
const postView = await assemblePostCardView(c, parseIdParam(c.req.param("postId"), ID_PREFIX.post), { isAuthenticated:
|
|
14962
|
+
const postView = await assemblePostCardView(c, parseIdParam(c.req.param("postId"), ID_PREFIX.post), { isAuthenticated: c.var.isAuthenticated });
|
|
14868
14963
|
if (!postView) return c.notFound();
|
|
14869
14964
|
return c.html(/* @__PURE__ */ jsxDEV$1(I18nProvider, {
|
|
14870
14965
|
c,
|
|
@@ -14872,7 +14967,7 @@ partialPageRoutes.get("/_/post-card/:postId", async (c) => {
|
|
|
14872
14967
|
}));
|
|
14873
14968
|
});
|
|
14874
14969
|
partialPageRoutes.get("/_/post-view/:postId", async (c) => {
|
|
14875
|
-
const display = await assemblePostPageDisplay(c, parseIdParam(c.req.param("postId"), ID_PREFIX.post), { isAuthenticated:
|
|
14970
|
+
const display = await assemblePostPageDisplay(c, parseIdParam(c.req.param("postId"), ID_PREFIX.post), { isAuthenticated: c.var.isAuthenticated });
|
|
14876
14971
|
if (!display) return c.notFound();
|
|
14877
14972
|
return c.html(/* @__PURE__ */ jsxDEV$1(I18nProvider, {
|
|
14878
14973
|
c,
|
|
@@ -19829,6 +19924,10 @@ function parseConfigInt(value, fallback) {
|
|
|
19829
19924
|
showHeaderAvatar: allSettings["SHOW_HEADER_AVATAR"] === "true",
|
|
19830
19925
|
siteAvatarUrl,
|
|
19831
19926
|
faviconVersion: allSettings["SITE_FAVICON_VERSION"] ?? "",
|
|
19927
|
+
rateLimit: {
|
|
19928
|
+
disabled: getEnvString(env, "RATE_LIMIT_DISABLED") === "true",
|
|
19929
|
+
searchPerMinute: parseInt(getEnvString(env, "RATE_LIMIT_SEARCH_PER_MIN") ?? "30", 10) || 30
|
|
19930
|
+
},
|
|
19832
19931
|
fallbacks: {
|
|
19833
19932
|
siteName: resolveFallback("SITE_NAME", env),
|
|
19834
19933
|
siteDescription: resolveFallback("SITE_DESCRIPTION", env),
|
|
@@ -20019,8 +20118,8 @@ async function syncHostedControlPlaneSiteAvatar(input) {
|
|
|
20019
20118
|
return;
|
|
20020
20119
|
}
|
|
20021
20120
|
await markSyncPending(settings);
|
|
20022
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
20023
|
-
const { getGitHubAppConfig } = await import("./env-
|
|
20121
|
+
const { createGitHubSyncService } = await import("./github-sync-7y_nTXx1.js").then((n) => n.r);
|
|
20122
|
+
const { getGitHubAppConfig } = await import("./env-CgaH9Mut.js").then((n) => n.t);
|
|
20024
20123
|
const run = runBackgroundSync(settings, createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
20025
20124
|
storage: c.var.storage,
|
|
20026
20125
|
githubApp: getGitHubAppConfig(c.env)
|
|
@@ -20634,7 +20733,7 @@ settingsRoutes.get("/account", async (c) => {
|
|
|
20634
20733
|
settingsRoutes.get("/account/sessions", async (c) => {
|
|
20635
20734
|
if (c.var.appConfig.demoMode) return c.redirect(publicPath(c, "/settings/account"));
|
|
20636
20735
|
const navData = await getNavigationData(c);
|
|
20637
|
-
const currentToken =
|
|
20736
|
+
const currentToken = c.var.session?.session?.token ?? "";
|
|
20638
20737
|
const sessions = (await c.var.auth.api.listSessions({ headers: c.req.raw.headers }) ?? []).map((s) => ({
|
|
20639
20738
|
token: s.token,
|
|
20640
20739
|
ipAddress: s.ipAddress ?? null,
|
|
@@ -20787,7 +20886,7 @@ settingsRoutes.post("/github-sync/connect", async (c) => {
|
|
|
20787
20886
|
if (getGitHubAppConfig(c.env)) return dsToast("This deployment uses GitHub App authentication. Use Install GitHub App instead.", "error");
|
|
20788
20887
|
const body = await c.req.json();
|
|
20789
20888
|
if (!body.token?.trim() || !body.repo?.trim()) return dsToast("Token and repository are required.", "error");
|
|
20790
|
-
const { parseRepoSlug, createGitHubClient } = await import("./github-api-
|
|
20889
|
+
const { parseRepoSlug, createGitHubClient } = await import("./github-api-BkRWnqMx.js").then((n) => n.n);
|
|
20791
20890
|
const parsed = parseRepoSlug(body.repo);
|
|
20792
20891
|
if (!parsed) return dsToast("Invalid repository format. Use owner/repo.", "error");
|
|
20793
20892
|
const client = createGitHubClient(body.token);
|
|
@@ -20806,7 +20905,7 @@ settingsRoutes.post("/github-sync/connect", async (c) => {
|
|
|
20806
20905
|
await c.var.services.settings.set("GITHUB_SYNC_AUTH_MODE", "pat");
|
|
20807
20906
|
await c.var.services.settings.set("GITHUB_SYNC_APP_INSTALLATION_ID", "");
|
|
20808
20907
|
await c.var.services.settings.set("GITHUB_SYNC_ENABLED", "true");
|
|
20809
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
20908
|
+
const { createGitHubSyncService } = await import("./github-sync-7y_nTXx1.js").then((n) => n.r);
|
|
20810
20909
|
const syncService = createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
20811
20910
|
storage: c.var.storage,
|
|
20812
20911
|
githubApp: getGitHubAppConfig(c.env)
|
|
@@ -20825,7 +20924,7 @@ settingsRoutes.post("/github-sync/connect", async (c) => {
|
|
|
20825
20924
|
return dsRedirect(publicPath(c, "/settings/github-sync"));
|
|
20826
20925
|
});
|
|
20827
20926
|
settingsRoutes.post("/github-sync/push", async (c) => {
|
|
20828
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
20927
|
+
const { createGitHubSyncService } = await import("./github-sync-7y_nTXx1.js").then((n) => n.r);
|
|
20829
20928
|
const syncService = createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
20830
20929
|
storage: c.var.storage,
|
|
20831
20930
|
githubApp: getGitHubAppConfig(c.env)
|
|
@@ -20845,7 +20944,7 @@ settingsRoutes.post("/github-sync/push", async (c) => {
|
|
|
20845
20944
|
});
|
|
20846
20945
|
});
|
|
20847
20946
|
settingsRoutes.post("/github-sync/disconnect", async (c) => {
|
|
20848
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
20947
|
+
const { createGitHubSyncService } = await import("./github-sync-7y_nTXx1.js").then((n) => n.r);
|
|
20849
20948
|
await createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), { githubApp: getGitHubAppConfig(c.env) }).teardownWebhook();
|
|
20850
20949
|
return dsRedirect(publicPath(c, "/settings/github-sync"));
|
|
20851
20950
|
});
|
|
@@ -21033,10 +21132,10 @@ function buildRepoPickerLabels(c) {
|
|
|
21033
21132
|
const repo = String(body.repo ?? "").trim();
|
|
21034
21133
|
const confirmForeign = body.confirmForeign === true || body.confirmForeign === "true";
|
|
21035
21134
|
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-
|
|
21135
|
+
const { parseRepoSlug, createGitHubClient } = await import("./github-api-BkRWnqMx.js").then((n) => n.n);
|
|
21037
21136
|
const parsed = parseRepoSlug(repo);
|
|
21038
21137
|
if (!parsed) return wantsJson ? c.json({ error: "Invalid repository format." }, 400) : c.text("Invalid repository format.", 400);
|
|
21039
|
-
const { classifyRepoForSync } = await import("./github-sync-
|
|
21138
|
+
const { classifyRepoForSync } = await import("./github-sync-7y_nTXx1.js").then((n) => n.r);
|
|
21040
21139
|
const ghClient = createGitHubClient(() => getInstallationTokenFromApp(app, installationId));
|
|
21041
21140
|
let classification;
|
|
21042
21141
|
try {
|
|
@@ -21066,7 +21165,7 @@ function buildRepoPickerLabels(c) {
|
|
|
21066
21165
|
await c.var.services.settings.set("GITHUB_SYNC_REPO", repo);
|
|
21067
21166
|
await c.var.services.settings.set("GITHUB_SYNC_TOKEN", "");
|
|
21068
21167
|
await c.var.services.settings.set("GITHUB_SYNC_ENABLED", "true");
|
|
21069
|
-
const { createGitHubSyncService } = await import("./github-sync-
|
|
21168
|
+
const { createGitHubSyncService } = await import("./github-sync-7y_nTXx1.js").then((n) => n.r);
|
|
21070
21169
|
const syncService = createGitHubSyncService(c.var.services, c.var.currentSite.id, await buildSyncSiteConfig(c), {
|
|
21071
21170
|
storage: c.var.storage,
|
|
21072
21171
|
githubApp: app
|
|
@@ -21105,7 +21204,7 @@ function requireGitHubApp(c) {
|
|
|
21105
21204
|
* build a client for classification without adding the helper to the
|
|
21106
21205
|
* top-level imports (the module already lazy-imports github-api).
|
|
21107
21206
|
*/ async function getInstallationTokenFromApp(app, installationId) {
|
|
21108
|
-
const { getInstallationToken } = await import("./github-app-
|
|
21207
|
+
const { getInstallationToken } = await import("./github-app-WeadXMb8.js").then((n) => n.i);
|
|
21109
21208
|
return getInstallationToken(app, installationId);
|
|
21110
21209
|
}
|
|
21111
21210
|
/** List GitHub App installations authorized for this site. */ settingsRoutes.get("/github-sync/app/installations", async (c) => {
|
|
@@ -21179,10 +21278,10 @@ function requireGitHubApp(c) {
|
|
|
21179
21278
|
const installationId = String(body.installationId ?? "").trim();
|
|
21180
21279
|
const repo = String(body.repo ?? "").trim();
|
|
21181
21280
|
if (!installationId || !repo) return c.json({ error: "Missing installationId or repo." }, 400);
|
|
21182
|
-
const { parseRepoSlug, createGitHubClient } = await import("./github-api-
|
|
21281
|
+
const { parseRepoSlug, createGitHubClient } = await import("./github-api-BkRWnqMx.js").then((n) => n.n);
|
|
21183
21282
|
const parsed = parseRepoSlug(repo);
|
|
21184
21283
|
if (!parsed) return c.json({ error: "Invalid repository format." }, 400);
|
|
21185
|
-
const { classifyRepoForSync } = await import("./github-sync-
|
|
21284
|
+
const { classifyRepoForSync } = await import("./github-sync-7y_nTXx1.js").then((n) => n.r);
|
|
21186
21285
|
const client = createGitHubClient(() => getInstallationTokenFromApp(app, installationId));
|
|
21187
21286
|
try {
|
|
21188
21287
|
const classification = await classifyRepoForSync(client, parsed.owner, parsed.repo, c.var.currentSite.id);
|
|
@@ -23389,10 +23488,78 @@ function toSearchApiResult(post, snippet, sitePathPrefix) {
|
|
|
23389
23488
|
};
|
|
23390
23489
|
}
|
|
23391
23490
|
//#endregion
|
|
23491
|
+
//#region src/lib/rate-limit.ts
|
|
23492
|
+
/**
|
|
23493
|
+
* Rate Limiting Abstraction
|
|
23494
|
+
*
|
|
23495
|
+
* Shared interface for per-key rate limiting. Runtimes provide their own
|
|
23496
|
+
* implementation: Cloudflare Workers uses a D1-backed sliding-window table
|
|
23497
|
+
* (ephemeral isolates can't hold memory state), while Node uses an
|
|
23498
|
+
* in-process Map (the process is persistent and avoids DB round-trips).
|
|
23499
|
+
*
|
|
23500
|
+
* Consumers depend only on this interface; they are runtime-agnostic.
|
|
23501
|
+
*/ /**
|
|
23502
|
+
* Extracts the client IP from a Hono request context.
|
|
23503
|
+
*
|
|
23504
|
+
* On Cloudflare Workers, `cf-connecting-ip` is set by the edge and is
|
|
23505
|
+
* authoritative. On Node deployments we fall back to the leftmost
|
|
23506
|
+
* `x-forwarded-for` entry, which is the conventional client IP when the
|
|
23507
|
+
* app sits behind a single trusted proxy. When neither header is
|
|
23508
|
+
* available we return `"unknown"` so all such requests share a bucket —
|
|
23509
|
+
* preferable to skipping the rate limit entirely.
|
|
23510
|
+
*
|
|
23511
|
+
* Note: this helper does not verify proxy trust. It is used for DoS
|
|
23512
|
+
* protection, not authentication. If header-forgery resistance becomes
|
|
23513
|
+
* important, gate the `x-forwarded-for` branch on `shouldTrustProxy`.
|
|
23514
|
+
*/ function getClientIp(c) {
|
|
23515
|
+
const cf = c.req.header("cf-connecting-ip");
|
|
23516
|
+
if (cf) return cf;
|
|
23517
|
+
const fwd = c.req.header("x-forwarded-for");
|
|
23518
|
+
if (fwd) {
|
|
23519
|
+
const first = fwd.split(",")[0]?.trim();
|
|
23520
|
+
if (first) return first;
|
|
23521
|
+
}
|
|
23522
|
+
return "unknown";
|
|
23523
|
+
}
|
|
23524
|
+
//#endregion
|
|
23525
|
+
//#region src/middleware/rate-limit.ts
|
|
23526
|
+
/**
|
|
23527
|
+
* Rate-Limit Middleware
|
|
23528
|
+
*
|
|
23529
|
+
* Applies a per-IP rate limit to the routes it wraps. Which storage
|
|
23530
|
+
* backs the limiter (D1 or in-memory) is decided upstream by the
|
|
23531
|
+
* runtime; this middleware only reads `c.var.rateLimiter` and doesn't
|
|
23532
|
+
* care.
|
|
23533
|
+
*
|
|
23534
|
+
* When the limit is exceeded, responds with HTTP 429 and a
|
|
23535
|
+
* `Retry-After` header. When `appConfig.rateLimit.disabled` is true the
|
|
23536
|
+
* middleware short-circuits to `next()` so test and dev environments
|
|
23537
|
+
* don't have to reason about bucket state.
|
|
23538
|
+
*/ function rateLimit(opts) {
|
|
23539
|
+
return async (c, next) => {
|
|
23540
|
+
if (c.var.appConfig.rateLimit.disabled) return next();
|
|
23541
|
+
const ip = getClientIp(c);
|
|
23542
|
+
const result = await c.var.rateLimiter.check(`${opts.name}:${ip}`, {
|
|
23543
|
+
limit: opts.limit,
|
|
23544
|
+
windowSec: opts.windowSec
|
|
23545
|
+
});
|
|
23546
|
+
if (!result.ok) {
|
|
23547
|
+
c.header("Retry-After", String(result.retryAfterSec ?? opts.windowSec));
|
|
23548
|
+
return c.json({ error: "Too many requests. Please slow down." }, 429);
|
|
23549
|
+
}
|
|
23550
|
+
return next();
|
|
23551
|
+
};
|
|
23552
|
+
}
|
|
23553
|
+
//#endregion
|
|
23392
23554
|
//#region src/routes/api/search.ts
|
|
23393
23555
|
/**
|
|
23394
23556
|
* Search API Routes
|
|
23395
23557
|
*/ var searchApiRoutes = new Hono();
|
|
23558
|
+
searchApiRoutes.use("*", async (c, next) => rateLimit({
|
|
23559
|
+
name: "search",
|
|
23560
|
+
limit: c.var.appConfig.rateLimit.searchPerMinute,
|
|
23561
|
+
windowSec: 60
|
|
23562
|
+
})(c, next));
|
|
23396
23563
|
searchApiRoutes.get("/", async (c) => {
|
|
23397
23564
|
const query = c.req.query("q");
|
|
23398
23565
|
if (!query || query.trim().length === 0) throw new ValidationError("Query parameter 'q' is required");
|
|
@@ -24857,6 +25024,30 @@ var internalTextAttachmentsRoutes = new Hono();
|
|
|
24857
25024
|
return c.json(result);
|
|
24858
25025
|
});
|
|
24859
25026
|
//#endregion
|
|
25027
|
+
//#region src/routes/api/internal/search-reindex.ts
|
|
25028
|
+
var ReindexSchema = z.object({
|
|
25029
|
+
limit: z.number().int().positive().max(500).optional(),
|
|
25030
|
+
cursor: z.string().min(1).optional()
|
|
25031
|
+
});
|
|
25032
|
+
var internalSearchReindexRoutes = new Hono();
|
|
25033
|
+
/**
|
|
25034
|
+
* Rebuild `post.body_text` for a batch of non-deleted posts. FTS indexes
|
|
25035
|
+
* (SQLite trigger / Postgres generated column) refresh automatically when
|
|
25036
|
+
* `body_text` changes.
|
|
25037
|
+
*
|
|
25038
|
+
* Idempotent. Callers loop with the returned `nextCursor` until `done: true`.
|
|
25039
|
+
* Used by the `jant search-reindex` CLI to backfill search indexes for
|
|
25040
|
+
* existing posts after changes to the text extraction logic (e.g. including
|
|
25041
|
+
* link mark hrefs so inline URLs become searchable).
|
|
25042
|
+
*/ internalSearchReindexRoutes.post("/", requireInternalAdminApi(), async (c) => {
|
|
25043
|
+
const body = parseValidated(ReindexSchema, (c.req.header("Content-Type") || "").includes("application/json") ? await c.req.json().catch(() => ({})) : {});
|
|
25044
|
+
const result = await c.var.services.posts.reindexBodyText({
|
|
25045
|
+
limit: body.limit,
|
|
25046
|
+
cursor: body.cursor
|
|
25047
|
+
});
|
|
25048
|
+
return c.json(result);
|
|
25049
|
+
});
|
|
25050
|
+
//#endregion
|
|
24860
25051
|
//#region src/routes/api/internal/uploads.ts
|
|
24861
25052
|
var CleanupUploadsSchema = z.object({ limit: z.number().int().positive().max(500).optional() });
|
|
24862
25053
|
var internalUploadsRoutes = new Hono();
|
|
@@ -25354,30 +25545,111 @@ feedRoutes.get("/all/atom.xml", (c) => {
|
|
|
25354
25545
|
//#region src/routes/feed/sitemap.ts
|
|
25355
25546
|
/**
|
|
25356
25547
|
* Sitemap Routes
|
|
25548
|
+
*
|
|
25549
|
+
* Sitemap is sharded to keep each shard small, cache-friendly, and stable:
|
|
25550
|
+
*
|
|
25551
|
+
* /sitemap.xml → sitemap index listing all shards
|
|
25552
|
+
* /sitemap-posts-N.xml → one shard of published non-reply posts
|
|
25553
|
+
* /sitemap-collections.xml → public collection pages
|
|
25554
|
+
* /sitemap-pages.xml → homepage + static aggregate pages
|
|
25555
|
+
*
|
|
25556
|
+
* Post shards are keyset-paginated by post `id` (TypeIDs embed a
|
|
25557
|
+
* creation-ordered UUIDv7 timestamp), so once a shard fills up its membership
|
|
25558
|
+
* never changes: new posts always land in the last shard, never rewriting an
|
|
25559
|
+
* older one. This lets old shards be cached at the edge for a long time.
|
|
25357
25560
|
*/ 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
|
-
});
|
|
25561
|
+
var CACHE_SHORT = "public, max-age=180";
|
|
25562
|
+
var CACHE_FULL_SHARD = "public, max-age=86400, s-maxage=86400";
|
|
25563
|
+
function xmlResponse(xml, cacheControl) {
|
|
25377
25564
|
return new Response(xml, { headers: {
|
|
25378
25565
|
"Content-Type": "application/xml; charset=utf-8",
|
|
25379
|
-
"Cache-Control":
|
|
25566
|
+
"Cache-Control": cacheControl
|
|
25380
25567
|
} });
|
|
25568
|
+
}
|
|
25569
|
+
/**
|
|
25570
|
+
* Build a public URL entry (absolute URL) from an internal path.
|
|
25571
|
+
*/ function absoluteUrl(internalPath, siteUrl, sitePathPrefix) {
|
|
25572
|
+
return toAbsoluteSiteUrl(internalPath, siteUrl, sitePathPrefix);
|
|
25573
|
+
}
|
|
25574
|
+
/** Convert a unix-seconds timestamp into a `YYYY-MM-DD` string. */ function toIsoDate(unixSeconds) {
|
|
25575
|
+
return (/* @__PURE__ */ new Date(unixSeconds * 1e3)).toISOString().slice(0, 10);
|
|
25576
|
+
}
|
|
25577
|
+
sitemapRoutes.get("/sitemap.xml", async (c) => {
|
|
25578
|
+
const { appConfig } = c.var;
|
|
25579
|
+
const { siteUrl, sitePathPrefix } = appConfig;
|
|
25580
|
+
const postCount = await c.var.services.posts.countForSitemap();
|
|
25581
|
+
const postShardCount = Math.max(1, Math.ceil(postCount / 500));
|
|
25582
|
+
const entries = [{ loc: absoluteUrl("/sitemap-pages.xml", siteUrl, sitePathPrefix) }];
|
|
25583
|
+
for (let page = 1; page <= postShardCount; page++) entries.push({ loc: absoluteUrl(`/sitemap-posts-${page}.xml`, siteUrl, sitePathPrefix) });
|
|
25584
|
+
if ((await c.var.services.collections.list()).length > 0) entries.push({ loc: absoluteUrl("/sitemap-collections.xml", siteUrl, sitePathPrefix) });
|
|
25585
|
+
return xmlResponse(renderSitemapIndex(entries), CACHE_SHORT);
|
|
25586
|
+
});
|
|
25587
|
+
sitemapRoutes.get("/:file{sitemap-posts-[0-9]+\\.xml}", async (c) => {
|
|
25588
|
+
const { appConfig } = c.var;
|
|
25589
|
+
const { siteUrl, sitePathPrefix } = appConfig;
|
|
25590
|
+
const file = c.req.param("file");
|
|
25591
|
+
const match = /^sitemap-posts-([0-9]+)\.xml$/.exec(file);
|
|
25592
|
+
if (!match) return c.notFound();
|
|
25593
|
+
const page = Number(match[1]);
|
|
25594
|
+
if (!Number.isFinite(page) || page < 1) return c.notFound();
|
|
25595
|
+
let afterId;
|
|
25596
|
+
if (page > 1) {
|
|
25597
|
+
const cursorOffset = (page - 1) * 500 - 1;
|
|
25598
|
+
const cursor = await c.var.services.posts.getSitemapIdAt(cursorOffset);
|
|
25599
|
+
if (cursor === null) return c.notFound();
|
|
25600
|
+
afterId = cursor;
|
|
25601
|
+
}
|
|
25602
|
+
const shardEntries = await c.var.services.posts.listForSitemap({
|
|
25603
|
+
afterId,
|
|
25604
|
+
limit: 500
|
|
25605
|
+
});
|
|
25606
|
+
const urls = shardEntries.map((entry) => {
|
|
25607
|
+
return {
|
|
25608
|
+
loc: absoluteUrl(entry.alias ?? `/${entry.slug}`, siteUrl, sitePathPrefix),
|
|
25609
|
+
lastmod: toIsoDate(entry.updatedAt),
|
|
25610
|
+
priority: entry.featuredAt ? "0.8" : "0.6"
|
|
25611
|
+
};
|
|
25612
|
+
});
|
|
25613
|
+
const cacheControl = shardEntries.length === 500 ? CACHE_FULL_SHARD : CACHE_SHORT;
|
|
25614
|
+
return xmlResponse(renderSitemapUrlSet(urls), cacheControl);
|
|
25615
|
+
});
|
|
25616
|
+
sitemapRoutes.get("/sitemap-collections.xml", async (c) => {
|
|
25617
|
+
const { appConfig } = c.var;
|
|
25618
|
+
const { siteUrl, sitePathPrefix } = appConfig;
|
|
25619
|
+
const collections = await c.var.services.collections.list();
|
|
25620
|
+
return xmlResponse(renderSitemapUrlSet(await Promise.all(collections.map(async (collection) => {
|
|
25621
|
+
const alias = await c.var.services.customUrls.getByTarget("collection", collection.id);
|
|
25622
|
+
return {
|
|
25623
|
+
loc: absoluteUrl(alias ? `/${alias.path}` : `/${collection.slug}`, siteUrl, sitePathPrefix),
|
|
25624
|
+
lastmod: toIsoDate(collection.updatedAt),
|
|
25625
|
+
priority: "0.7"
|
|
25626
|
+
};
|
|
25627
|
+
}))), CACHE_SHORT);
|
|
25628
|
+
});
|
|
25629
|
+
sitemapRoutes.get("/sitemap-pages.xml", async (c) => {
|
|
25630
|
+
const { appConfig } = c.var;
|
|
25631
|
+
const { siteUrl, sitePathPrefix, homeDefaultView } = appConfig;
|
|
25632
|
+
const urls = [{
|
|
25633
|
+
loc: absoluteUrl("/", siteUrl, sitePathPrefix),
|
|
25634
|
+
priority: "1.0",
|
|
25635
|
+
changefreq: "daily"
|
|
25636
|
+
}, {
|
|
25637
|
+
loc: absoluteUrl("/archive", siteUrl, sitePathPrefix),
|
|
25638
|
+
priority: "0.5",
|
|
25639
|
+
changefreq: "weekly"
|
|
25640
|
+
}];
|
|
25641
|
+
const secondaryAggregate = homeDefaultView === "featured" ? "/latest" : "/featured";
|
|
25642
|
+
urls.push({
|
|
25643
|
+
loc: absoluteUrl(secondaryAggregate, siteUrl, sitePathPrefix),
|
|
25644
|
+
priority: "0.6",
|
|
25645
|
+
changefreq: "daily"
|
|
25646
|
+
});
|
|
25647
|
+
if ((await c.var.services.collections.list()).length > 0) urls.push({
|
|
25648
|
+
loc: absoluteUrl("/collections", siteUrl, sitePathPrefix),
|
|
25649
|
+
priority: "0.5",
|
|
25650
|
+
changefreq: "weekly"
|
|
25651
|
+
});
|
|
25652
|
+
return xmlResponse(renderSitemapUrlSet(urls), CACHE_SHORT);
|
|
25381
25653
|
});
|
|
25382
25654
|
sitemapRoutes.get("/robots.txt", async (c) => {
|
|
25383
25655
|
const { appConfig } = c.var;
|
|
@@ -25395,7 +25667,7 @@ sitemapRoutes.get("/robots.txt", async (c) => {
|
|
|
25395
25667
|
].join("\n");
|
|
25396
25668
|
return new Response(robots, { headers: {
|
|
25397
25669
|
"Content-Type": "text/plain; charset=utf-8",
|
|
25398
|
-
"Cache-Control":
|
|
25670
|
+
"Cache-Control": CACHE_SHORT
|
|
25399
25671
|
} });
|
|
25400
25672
|
});
|
|
25401
25673
|
//#endregion
|
|
@@ -25438,6 +25710,34 @@ manifestRoutes.get("/manifest.webmanifest", (c) => {
|
|
|
25438
25710
|
} });
|
|
25439
25711
|
});
|
|
25440
25712
|
//#endregion
|
|
25713
|
+
//#region src/middleware/session.ts
|
|
25714
|
+
/**
|
|
25715
|
+
* Session Middleware
|
|
25716
|
+
*
|
|
25717
|
+
* Runs once per request (after runtime init) to look up the better-auth
|
|
25718
|
+
* session and stash it on `c.var.session` / `c.var.isAuthenticated`.
|
|
25719
|
+
*
|
|
25720
|
+
* This replaces ad-hoc `auth.api.getSession()` calls scattered across
|
|
25721
|
+
* view helpers (e.g. `lib/navigation.ts`) so each request only parses
|
|
25722
|
+
* the session cookie once. better-auth's own cookieCache (5 min) still
|
|
25723
|
+
* keeps this cheap, but centralizing the call also unlocks `Promise.all`
|
|
25724
|
+
* patterns in routes that previously serialized on a hidden session fetch.
|
|
25725
|
+
*
|
|
25726
|
+
* Never throws — any lookup error is treated as "not authenticated".
|
|
25727
|
+
*/ function attachSession() {
|
|
25728
|
+
return async (c, next) => {
|
|
25729
|
+
try {
|
|
25730
|
+
const session = await c.var.auth.api.getSession({ headers: c.req.raw.headers });
|
|
25731
|
+
c.set("session", session ?? null);
|
|
25732
|
+
c.set("isAuthenticated", !!session?.user);
|
|
25733
|
+
} catch {
|
|
25734
|
+
c.set("session", null);
|
|
25735
|
+
c.set("isAuthenticated", false);
|
|
25736
|
+
}
|
|
25737
|
+
await next();
|
|
25738
|
+
};
|
|
25739
|
+
}
|
|
25740
|
+
//#endregion
|
|
25441
25741
|
//#region src/middleware/onboarding.ts
|
|
25442
25742
|
/**
|
|
25443
25743
|
* Onboarding Middleware
|
|
@@ -26361,6 +26661,60 @@ async function deriveScryptKey(password, saltHex, opts) {
|
|
|
26361
26661
|
});
|
|
26362
26662
|
}
|
|
26363
26663
|
//#endregion
|
|
26664
|
+
//#region src/lib/rate-limit-d1.ts
|
|
26665
|
+
/**
|
|
26666
|
+
* D1 Sliding-Window Rate Limiter
|
|
26667
|
+
*
|
|
26668
|
+
* Used by the Cloudflare Workers runtime where isolates are ephemeral and
|
|
26669
|
+
* memory-based limiters would silently drop state between requests. Each
|
|
26670
|
+
* check performs one SELECT (over a two-row range) plus one UPSERT — both
|
|
26671
|
+
* hit a composite primary key so the round-trips are cheap.
|
|
26672
|
+
*
|
|
26673
|
+
* Algorithm: two-window weighted counter. The previous window's tally
|
|
26674
|
+
* decays linearly as the current window fills, giving a smoother limit
|
|
26675
|
+
* than a naive fixed window (which would allow a 2x burst at the
|
|
26676
|
+
* boundary) while remaining a single-key storage primitive.
|
|
26677
|
+
*
|
|
26678
|
+
* For Node deployments use `createMemoryRateLimiter` instead.
|
|
26679
|
+
*/
|
|
26680
|
+
/**
|
|
26681
|
+
* Probability of running opportunistic cleanup on any given write.
|
|
26682
|
+
* 1% strikes a balance between bounded table growth and per-request cost.
|
|
26683
|
+
*/ var CLEANUP_PROBABILITY = .01;
|
|
26684
|
+
function createD1RateLimiter(db, schema, now = () => Math.floor(Date.now() / 1e3)) {
|
|
26685
|
+
const { rateLimit } = schema;
|
|
26686
|
+
return { async check(key, opts) {
|
|
26687
|
+
const { limit, windowSec } = opts;
|
|
26688
|
+
const nowSec = now();
|
|
26689
|
+
const currentWindow = Math.floor(nowSec / windowSec) * windowSec;
|
|
26690
|
+
const previousWindow = currentWindow - windowSec;
|
|
26691
|
+
const rows = await db.select({
|
|
26692
|
+
windowStart: rateLimit.windowStart,
|
|
26693
|
+
count: rateLimit.count
|
|
26694
|
+
}).from(rateLimit).where(and(eq(rateLimit.key, key), inArray(rateLimit.windowStart, [currentWindow, previousWindow])));
|
|
26695
|
+
let currentCount = 0;
|
|
26696
|
+
let previousCount = 0;
|
|
26697
|
+
for (const row of rows) if (row.windowStart === currentWindow) currentCount = row.count;
|
|
26698
|
+
else if (row.windowStart === previousWindow) previousCount = row.count;
|
|
26699
|
+
const elapsed = nowSec - currentWindow;
|
|
26700
|
+
const prevWeight = 1 - elapsed / windowSec;
|
|
26701
|
+
if (previousCount * prevWeight + currentCount >= limit) return {
|
|
26702
|
+
ok: false,
|
|
26703
|
+
retryAfterSec: Math.max(1, windowSec - elapsed)
|
|
26704
|
+
};
|
|
26705
|
+
await db.insert(rateLimit).values({
|
|
26706
|
+
key,
|
|
26707
|
+
windowStart: currentWindow,
|
|
26708
|
+
count: 1
|
|
26709
|
+
}).onConflictDoUpdate({
|
|
26710
|
+
target: [rateLimit.key, rateLimit.windowStart],
|
|
26711
|
+
set: { count: sql`${rateLimit.count} + 1` }
|
|
26712
|
+
});
|
|
26713
|
+
if (Math.random() < CLEANUP_PROBABILITY) await db.delete(rateLimit).where(lt(rateLimit.windowStart, currentWindow - windowSec * 2));
|
|
26714
|
+
return { ok: true };
|
|
26715
|
+
} };
|
|
26716
|
+
}
|
|
26717
|
+
//#endregion
|
|
26364
26718
|
//#region src/lib/hosted-sso.ts
|
|
26365
26719
|
var textEncoder = new TextEncoder();
|
|
26366
26720
|
var textDecoder = new TextDecoder();
|
|
@@ -27649,6 +28003,51 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
27649
28003
|
if (filters.offset !== void 0) query = query.offset(filters.offset);
|
|
27650
28004
|
return hydratePosts(await query);
|
|
27651
28005
|
},
|
|
28006
|
+
async listForSitemap({ afterId, limit }) {
|
|
28007
|
+
const conditions = buildFilterConditions({
|
|
28008
|
+
status: "published",
|
|
28009
|
+
excludePrivate: true,
|
|
28010
|
+
excludeReplies: true
|
|
28011
|
+
});
|
|
28012
|
+
if (afterId !== void 0) conditions.push(sql`${posts.id} > ${afterId}`);
|
|
28013
|
+
const rows = await db.select({
|
|
28014
|
+
id: posts.id,
|
|
28015
|
+
updatedAt: posts.updatedAt,
|
|
28016
|
+
featuredAt: posts.featuredAt
|
|
28017
|
+
}).from(posts).where(conditions.length > 0 ? and(...conditions) : void 0).orderBy(asc(posts.id)).limit(limit);
|
|
28018
|
+
if (rows.length === 0) return [];
|
|
28019
|
+
const ids = rows.map((row) => row.id);
|
|
28020
|
+
const [slugMap, aliasesMap] = await Promise.all([resolvedPaths.getPostSlugMap(ids), resolvedPaths.getPostAliases(ids)]);
|
|
28021
|
+
return rows.map((row) => {
|
|
28022
|
+
const slug = slugMap.get(row.id);
|
|
28023
|
+
if (!slug) return null;
|
|
28024
|
+
const alias = aliasesMap.get(row.id)?.[0] ?? null;
|
|
28025
|
+
return {
|
|
28026
|
+
id: row.id,
|
|
28027
|
+
slug,
|
|
28028
|
+
alias,
|
|
28029
|
+
updatedAt: row.updatedAt,
|
|
28030
|
+
featuredAt: row.featuredAt
|
|
28031
|
+
};
|
|
28032
|
+
}).filter((entry) => entry !== null);
|
|
28033
|
+
},
|
|
28034
|
+
async countForSitemap() {
|
|
28035
|
+
const conditions = buildFilterConditions({
|
|
28036
|
+
status: "published",
|
|
28037
|
+
excludePrivate: true,
|
|
28038
|
+
excludeReplies: true
|
|
28039
|
+
});
|
|
28040
|
+
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;
|
|
28041
|
+
},
|
|
28042
|
+
async getSitemapIdAt(offset) {
|
|
28043
|
+
if (offset < 0) return null;
|
|
28044
|
+
const conditions = buildFilterConditions({
|
|
28045
|
+
status: "published",
|
|
28046
|
+
excludePrivate: true,
|
|
28047
|
+
excludeReplies: true
|
|
28048
|
+
});
|
|
28049
|
+
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;
|
|
28050
|
+
},
|
|
27652
28051
|
async count(filters = {}) {
|
|
27653
28052
|
const conditions = buildFilterConditions(filters);
|
|
27654
28053
|
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 +28087,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
27688
28087
|
hasAttachments: data.attachments ? data.attachments.length > 0 : void 0
|
|
27689
28088
|
});
|
|
27690
28089
|
const bodyHtml = body ? renderTiptapJson(body) : null;
|
|
27691
|
-
const bodyText = body ? extractBodyText(body) : null;
|
|
28090
|
+
const bodyText = body ? extractBodyText(body, { includeLinkHrefs: true }) : null;
|
|
27692
28091
|
let summary = null;
|
|
27693
28092
|
if (format === "note" && title && body && summaryConfig) summary = extractSummary(body, summaryConfig.maxParagraphs, summaryConfig.maxChars);
|
|
27694
28093
|
let threadId = id;
|
|
@@ -27968,7 +28367,7 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
27968
28367
|
const normalizedBody = rawBody ? trimTiptapBody(rawBody) : null;
|
|
27969
28368
|
updates.body = normalizedBody;
|
|
27970
28369
|
updates.bodyHtml = normalizedBody ? renderTiptapJson(normalizedBody) : null;
|
|
27971
|
-
updates.bodyText = normalizedBody ? extractBodyText(normalizedBody) : null;
|
|
28370
|
+
updates.bodyText = normalizedBody ? extractBodyText(normalizedBody, { includeLinkHrefs: true }) : null;
|
|
27972
28371
|
}
|
|
27973
28372
|
if (summaryConfig) {
|
|
27974
28373
|
const format = nextFormat;
|
|
@@ -28406,6 +28805,40 @@ function createPostService(db, config, siteId, paths, databaseSchema = sqliteSch
|
|
|
28406
28805
|
const conditions = [...buildFilterConditions(filters), isNotNull(posts.publishedAt)];
|
|
28407
28806
|
const publishedYearExpr = buildPublishedYearExpr();
|
|
28408
28807
|
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));
|
|
28808
|
+
},
|
|
28809
|
+
async reindexBodyText(options = {}) {
|
|
28810
|
+
const requested = options.limit ?? 50;
|
|
28811
|
+
const limit = Math.min(Math.max(Math.trunc(requested), 1), 500);
|
|
28812
|
+
const cursor = options.cursor;
|
|
28813
|
+
const whereConditions = [eq(posts.siteId, siteId), isNull(posts.deletedAt)];
|
|
28814
|
+
if (cursor) whereConditions.push(gt(posts.id, cursor));
|
|
28815
|
+
const rows = await db.select({
|
|
28816
|
+
id: posts.id,
|
|
28817
|
+
body: posts.body,
|
|
28818
|
+
bodyText: posts.bodyText
|
|
28819
|
+
}).from(posts).where(and(...whereConditions)).orderBy(asc(posts.id)).limit(limit + 1);
|
|
28820
|
+
const hasMore = rows.length > limit;
|
|
28821
|
+
const batch = hasMore ? rows.slice(0, limit) : rows;
|
|
28822
|
+
let updated = 0;
|
|
28823
|
+
let skipped = 0;
|
|
28824
|
+
for (const row of batch) {
|
|
28825
|
+
const nextBodyText = row.body ? extractBodyText(row.body, { includeLinkHrefs: true }) : null;
|
|
28826
|
+
if (nextBodyText === row.bodyText) {
|
|
28827
|
+
skipped++;
|
|
28828
|
+
continue;
|
|
28829
|
+
}
|
|
28830
|
+
await db.update(posts).set({ bodyText: nextBodyText }).where(and(eq(posts.siteId, siteId), eq(posts.id, row.id)));
|
|
28831
|
+
updated++;
|
|
28832
|
+
}
|
|
28833
|
+
const lastRow = batch.at(-1);
|
|
28834
|
+
const lastId = lastRow ? lastRow.id : null;
|
|
28835
|
+
return {
|
|
28836
|
+
processed: batch.length,
|
|
28837
|
+
updated,
|
|
28838
|
+
skipped,
|
|
28839
|
+
nextCursor: hasMore ? lastId : null,
|
|
28840
|
+
done: !hasMore
|
|
28841
|
+
};
|
|
28409
28842
|
}
|
|
28410
28843
|
};
|
|
28411
28844
|
}
|
|
@@ -30863,6 +31296,7 @@ function getResolvedSiteBaseUrl(env, publicRequestUrl, pathPrefix) {
|
|
|
30863
31296
|
schema: sqliteSchemaBundle,
|
|
30864
31297
|
secret: hostedControlPlaneSsoSecret
|
|
30865
31298
|
}),
|
|
31299
|
+
rateLimiter: createD1RateLimiter(db, sqliteSchemaBundle),
|
|
30866
31300
|
services: createServices(db, session, siteLookup.site.id, {
|
|
30867
31301
|
databaseDialect: "sqlite",
|
|
30868
31302
|
bootstrapSite: getSingleSiteBootstrapOptions(env),
|
|
@@ -30876,7 +31310,76 @@ function getResolvedSiteBaseUrl(env, publicRequestUrl, pathPrefix) {
|
|
|
30876
31310
|
};
|
|
30877
31311
|
}
|
|
30878
31312
|
//#endregion
|
|
31313
|
+
//#region src/lib/rate-limit-memory.ts
|
|
31314
|
+
/**
|
|
31315
|
+
* In-Memory Rate Limiter
|
|
31316
|
+
*
|
|
31317
|
+
* Used by the Node runtime. The server process is long-lived and
|
|
31318
|
+
* single-instance, so a local `Map` is reliable and avoids unnecessary DB
|
|
31319
|
+
* round-trips. Uses the classic sliding-window-counter algorithm (two
|
|
31320
|
+
* aligned buckets with a weighted estimate) for smooth limiting without
|
|
31321
|
+
* the 2x boundary burst of a fixed window.
|
|
31322
|
+
*
|
|
31323
|
+
* On Cloudflare Workers use `createD1RateLimiter` instead — isolates are
|
|
31324
|
+
* ephemeral and cannot share memory across requests.
|
|
31325
|
+
*/ /**
|
|
31326
|
+
* Number of live keys after which we prune old buckets on the next write.
|
|
31327
|
+
* Keeps the Map bounded under abuse without paying for eager sweeps.
|
|
31328
|
+
*/ var SWEEP_THRESHOLD = 1e4;
|
|
31329
|
+
/**
|
|
31330
|
+
* Creates an isolated in-memory limiter. Tests construct one per test app;
|
|
31331
|
+
* the Node runtime holds a single module-level instance across requests.
|
|
31332
|
+
*
|
|
31333
|
+
* `now` is injectable so tests can assert window-rollover behavior without
|
|
31334
|
+
* relying on real time.
|
|
31335
|
+
*/ function createMemoryRateLimiter(now = () => Math.floor(Date.now() / 1e3)) {
|
|
31336
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
31337
|
+
function sweep(nowSec) {
|
|
31338
|
+
if (buckets.size < SWEEP_THRESHOLD) return;
|
|
31339
|
+
const cutoff = nowSec - 600;
|
|
31340
|
+
for (const [key, bucket] of buckets) if (bucket.windowStart < cutoff) buckets.delete(key);
|
|
31341
|
+
}
|
|
31342
|
+
return { async check(key, opts) {
|
|
31343
|
+
const { limit, windowSec } = opts;
|
|
31344
|
+
const nowSec = now();
|
|
31345
|
+
const currentWindow = Math.floor(nowSec / windowSec) * windowSec;
|
|
31346
|
+
let bucket = buckets.get(key);
|
|
31347
|
+
if (!bucket) {
|
|
31348
|
+
bucket = {
|
|
31349
|
+
windowStart: currentWindow,
|
|
31350
|
+
count: 0,
|
|
31351
|
+
prevCount: 0
|
|
31352
|
+
};
|
|
31353
|
+
buckets.set(key, bucket);
|
|
31354
|
+
} else if (bucket.windowStart !== currentWindow) {
|
|
31355
|
+
if (bucket.windowStart === currentWindow - windowSec) bucket.prevCount = bucket.count;
|
|
31356
|
+
else bucket.prevCount = 0;
|
|
31357
|
+
bucket.count = 0;
|
|
31358
|
+
bucket.windowStart = currentWindow;
|
|
31359
|
+
}
|
|
31360
|
+
const elapsed = nowSec - currentWindow;
|
|
31361
|
+
const prevWeight = 1 - elapsed / windowSec;
|
|
31362
|
+
if (bucket.prevCount * prevWeight + bucket.count >= limit) return {
|
|
31363
|
+
ok: false,
|
|
31364
|
+
retryAfterSec: Math.max(1, windowSec - elapsed)
|
|
31365
|
+
};
|
|
31366
|
+
bucket.count += 1;
|
|
31367
|
+
sweep(nowSec);
|
|
31368
|
+
return { ok: true };
|
|
31369
|
+
} };
|
|
31370
|
+
}
|
|
31371
|
+
//#endregion
|
|
30879
31372
|
//#region src/runtime/node.ts
|
|
31373
|
+
/**
|
|
31374
|
+
* Single process-wide rate limiter for the Node runtime. Node serves all
|
|
31375
|
+
* requests out of one persistent process, so in-memory counters are
|
|
31376
|
+
* reliable and avoid per-request D1 round-trips. Constructed lazily on
|
|
31377
|
+
* first use so tests that never build a request runtime don't pay for it.
|
|
31378
|
+
*/ var sharedNodeRateLimiter = null;
|
|
31379
|
+
function getNodeRateLimiter() {
|
|
31380
|
+
sharedNodeRateLimiter ??= createMemoryRateLimiter();
|
|
31381
|
+
return sharedNodeRateLimiter;
|
|
31382
|
+
}
|
|
30880
31383
|
function createBetterSqliteRawQuery(sqlite) {
|
|
30881
31384
|
return { prepare(query) {
|
|
30882
31385
|
let params = [];
|
|
@@ -30928,6 +31431,7 @@ function createBetterSqliteRawQuery(sqlite) {
|
|
|
30928
31431
|
schema: databaseSchema,
|
|
30929
31432
|
secret: hostedControlPlaneSsoSecret
|
|
30930
31433
|
}),
|
|
31434
|
+
rateLimiter: getNodeRateLimiter(),
|
|
30931
31435
|
services: createServices(db, rawQuery, siteLookup.site.id, {
|
|
30932
31436
|
databaseDialect,
|
|
30933
31437
|
bootstrapSite: getSingleSiteBootstrapOptions(env),
|
|
@@ -31204,8 +31708,10 @@ async function servePublicStorage(c) {
|
|
|
31204
31708
|
c.set("auth", runtime.auth);
|
|
31205
31709
|
c.set("currentSite", runtime.currentSite);
|
|
31206
31710
|
c.set("currentSiteDomain", runtime.currentSiteDomain);
|
|
31711
|
+
c.set("rateLimiter", runtime.rateLimiter);
|
|
31207
31712
|
await next();
|
|
31208
31713
|
});
|
|
31714
|
+
app.use("*", attachSession());
|
|
31209
31715
|
app.use("*", async (c, next) => {
|
|
31210
31716
|
const redirectUrl = await getHostedCanonicalRedirect({
|
|
31211
31717
|
currentSite: c.var.currentSite,
|
|
@@ -31223,6 +31729,7 @@ async function servePublicStorage(c) {
|
|
|
31223
31729
|
app.route("/api/internal/api-tokens", internalApiTokensRoutes);
|
|
31224
31730
|
app.route("/api/internal/sites", internalSitesRoutes);
|
|
31225
31731
|
app.route("/api/internal/text-attachments", internalTextAttachmentsRoutes);
|
|
31732
|
+
app.route("/api/internal/search/reindex", internalSearchReindexRoutes);
|
|
31226
31733
|
app.route("/api/internal/uploads", internalUploadsRoutes);
|
|
31227
31734
|
app.route("/api/github-sync", githubSyncWebhookRoutes);
|
|
31228
31735
|
app.get("/api/media/:id/content", async (c) => {
|
|
@@ -31352,4 +31859,4 @@ async function servePublicStorage(c) {
|
|
|
31352
31859
|
return app;
|
|
31353
31860
|
}
|
|
31354
31861
|
//#endregion
|
|
31355
|
-
export {
|
|
31862
|
+
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 };
|