emdash 0.6.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapters-Di31kZ28.d.mts → adapters-BKSf3T9R.d.mts} +1 -1
- package/dist/{adapters-Di31kZ28.d.mts.map → adapters-BKSf3T9R.d.mts.map} +1 -1
- package/dist/{apply-B4MsLM-w.mjs → apply-x0eMK1lX.mjs} +186 -28
- package/dist/apply-x0eMK1lX.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +92 -17
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +22 -2
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware/request-context.mjs +7 -2
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +263 -74
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +25 -8
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-C4OVd8b3.mjs → byline-Chbr2GoP.mjs} +3 -3
- package/dist/byline-Chbr2GoP.mjs.map +1 -0
- package/dist/{bylines-hPTW79hw.mjs → bylines-CRNsVG88.mjs} +4 -4
- package/dist/{bylines-hPTW79hw.mjs.map → bylines-CRNsVG88.mjs.map} +1 -1
- package/dist/cli/index.mjs +17 -13
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/{content-BsBoyj8G.mjs → content-BcQPYxdV.mjs} +39 -15
- package/dist/content-BcQPYxdV.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{db-errors-D0UT85nC.mjs → db-errors-l1Qh2RPR.mjs} +1 -1
- package/dist/{db-errors-D0UT85nC.mjs.map → db-errors-l1Qh2RPR.mjs.map} +1 -1
- package/dist/{default-CME5YdZ3.mjs → default-DCVqE5ib.mjs} +1 -1
- package/dist/{default-CME5YdZ3.mjs.map → default-DCVqE5ib.mjs.map} +1 -1
- package/dist/{error-CiYn9yDu.mjs → error-zG5T1UGA.mjs} +1 -1
- package/dist/error-zG5T1UGA.mjs.map +1 -0
- package/dist/{index-BYv0mB9g.d.mts → index-DIb-CzNx.d.mts} +232 -15
- package/dist/index-DIb-CzNx.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +23 -21
- package/dist/{load-CBcmDIot.mjs → load-CyEoextb.mjs} +1 -1
- package/dist/{load-CBcmDIot.mjs.map → load-CyEoextb.mjs.map} +1 -1
- package/dist/{loader-DeiBJEMe.mjs → loader-CndGj8kM.mjs} +8 -6
- package/dist/loader-CndGj8kM.mjs.map +1 -0
- package/dist/{manifest-schema-V30qsMft.mjs → manifest-schema-DH9xhc6t.mjs} +13 -1
- package/dist/manifest-schema-DH9xhc6t.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/media/local-runtime.mjs +2 -2
- package/dist/{media-DqHVh136.mjs → media-D8FbNsl0.mjs} +4 -7
- package/dist/media-D8FbNsl0.mjs.map +1 -0
- package/dist/{mode-CpNnGkPz.mjs → mode-BnAOqItE.mjs} +1 -1
- package/dist/mode-BnAOqItE.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/placeholder-C-fk5hYI.mjs.map +1 -1
- package/dist/{placeholder-tzpqGWII.d.mts → placeholder-D29tWZ7o.d.mts} +1 -1
- package/dist/{placeholder-tzpqGWII.d.mts.map → placeholder-D29tWZ7o.d.mts.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-Bk_3vKvU.mjs → query-fqEdLFms.mjs} +9 -9
- package/dist/{query-Bk_3vKvU.mjs.map → query-fqEdLFms.mjs.map} +1 -1
- package/dist/{redirect-7lGhLBNZ.mjs → redirect-D_pshWdf.mjs} +69 -13
- package/dist/redirect-D_pshWdf.mjs.map +1 -0
- package/dist/{registry-Ci3WxVAr.mjs → registry-C3Mr0ODu.mjs} +33 -9
- package/dist/registry-C3Mr0ODu.mjs.map +1 -0
- package/dist/{request-cache-DiR961CV.mjs → request-cache-Ci7f5pBb.mjs} +1 -1
- package/dist/request-cache-Ci7f5pBb.mjs.map +1 -0
- package/dist/{runner-Fl2NcUUz.d.mts → runner-OURCaApa.d.mts} +2 -2
- package/dist/{runner-Fl2NcUUz.d.mts.map → runner-OURCaApa.d.mts.map} +1 -1
- package/dist/{runner-Cd-_WyDo.mjs → runner-tQ7BJ4T7.mjs} +211 -134
- package/dist/runner-tQ7BJ4T7.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-DI4bM2w9.mjs → search-BoZYFuUk.mjs} +339 -102
- package/dist/search-BoZYFuUk.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +12 -12
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.d.mts.map +1 -1
- package/dist/storage/s3.mjs +4 -4
- package/dist/storage/s3.mjs.map +1 -1
- package/dist/{taxonomies-DbrKzDju.mjs → taxonomies-B4IAshV8.mjs} +5 -5
- package/dist/{taxonomies-DbrKzDju.mjs.map → taxonomies-B4IAshV8.mjs.map} +1 -1
- package/dist/{tokens-BFPFx3CA.mjs → tokens-D9vnZqYS.mjs} +1 -1
- package/dist/{tokens-BFPFx3CA.mjs.map → tokens-D9vnZqYS.mjs.map} +1 -1
- package/dist/{transport-BykRfpyy.mjs → transport-C9ugt2Nr.mjs} +1 -1
- package/dist/{transport-BykRfpyy.mjs.map → transport-C9ugt2Nr.mjs.map} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts → transport-CUnEL3Vs.d.mts} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts.map → transport-CUnEL3Vs.d.mts.map} +1 -1
- package/dist/types-BIgulNsW.mjs +68 -0
- package/dist/types-BIgulNsW.mjs.map +1 -0
- package/dist/{types-DDS4MxsT.mjs → types-Bm1dn-q3.mjs} +1 -1
- package/dist/{types-DDS4MxsT.mjs.map → types-Bm1dn-q3.mjs.map} +1 -1
- package/dist/{types-CnZYHyLW.d.mts → types-BmPPSUEx.d.mts} +1 -1
- package/dist/{types-CnZYHyLW.d.mts.map → types-BmPPSUEx.d.mts.map} +1 -1
- package/dist/{types-6CUZRrZP.d.mts → types-BrA0xf5I.d.mts} +24 -2
- package/dist/{types-6CUZRrZP.d.mts.map → types-BrA0xf5I.d.mts.map} +1 -1
- package/dist/{types-8xrvl_68.d.mts → types-CS8FIX7L.d.mts} +10 -1
- package/dist/{types-8xrvl_68.d.mts.map → types-CS8FIX7L.d.mts.map} +1 -1
- package/dist/{types-BH2L167P.mjs → types-CgqmmMJB.mjs} +1 -1
- package/dist/{types-BH2L167P.mjs.map → types-CgqmmMJB.mjs.map} +1 -1
- package/dist/{types-CFWjXmus.d.mts → types-DIMwPFub.d.mts} +1 -1
- package/dist/{types-CFWjXmus.d.mts.map → types-DIMwPFub.d.mts.map} +1 -1
- package/dist/{types-DgrIP0tF.d.mts → types-i36XcA_X.d.mts} +49 -6
- package/dist/types-i36XcA_X.d.mts.map +1 -0
- package/dist/{validate-CqsNItbt.mjs → validate-CxVsLehf.mjs} +2 -2
- package/dist/{validate-CqsNItbt.mjs.map → validate-CxVsLehf.mjs.map} +1 -1
- package/dist/{validate-CaLH1Ia2.d.mts → validate-DHxmpFJt.d.mts} +4 -4
- package/dist/{validate-CaLH1Ia2.d.mts.map → validate-DHxmpFJt.d.mts.map} +1 -1
- package/dist/validation-C-ZpN2GI.mjs +144 -0
- package/dist/validation-C-ZpN2GI.mjs.map +1 -0
- package/dist/version-DJrV1K0M.mjs +7 -0
- package/dist/{version-Uaf2ynPX.mjs.map → version-DJrV1K0M.mjs.map} +1 -1
- package/dist/zod-generator-CpwccCIv.mjs +132 -0
- package/dist/zod-generator-CpwccCIv.mjs.map +1 -0
- package/package.json +19 -6
- package/src/api/auth-storage.ts +37 -0
- package/src/api/error.ts +6 -0
- package/src/api/errors.ts +8 -0
- package/src/api/handlers/comments.ts +13 -0
- package/src/api/handlers/content.ts +124 -3
- package/src/api/handlers/index.ts +2 -0
- package/src/api/handlers/media.ts +8 -1
- package/src/api/handlers/menus.ts +160 -21
- package/src/api/handlers/redirects.ts +16 -3
- package/src/api/handlers/sections.ts +8 -1
- package/src/api/handlers/taxonomies.ts +128 -16
- package/src/api/handlers/validation.ts +212 -0
- package/src/api/openapi/document.ts +4 -1
- package/src/api/public-url.ts +6 -3
- package/src/api/route-utils.ts +14 -0
- package/src/api/schemas/common.ts +1 -1
- package/src/api/schemas/content.ts +8 -0
- package/src/api/schemas/setup.ts +8 -0
- package/src/api/schemas/widgets.ts +12 -10
- package/src/api/setup-complete.ts +40 -0
- package/src/astro/integration/font-provider.ts +3 -1
- package/src/astro/integration/index.ts +15 -2
- package/src/astro/integration/routes.ts +28 -0
- package/src/astro/integration/runtime.ts +74 -2
- package/src/astro/integration/virtual-modules.ts +41 -0
- package/src/astro/integration/vite-config.ts +43 -12
- package/src/astro/middleware/auth.ts +21 -0
- package/src/astro/middleware.ts +18 -1
- package/src/astro/routes/PluginRegistry.tsx +10 -1
- package/src/astro/routes/admin.astro +14 -7
- package/src/astro/routes/api/auth/magic-link/send.ts +2 -1
- package/src/astro/routes/api/auth/mode.ts +57 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +23 -3
- package/src/astro/routes/api/auth/oauth/[provider].ts +10 -4
- package/src/astro/routes/api/auth/passkey/options.ts +2 -1
- package/src/astro/routes/api/auth/signup/request.ts +26 -8
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +10 -6
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +26 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +30 -2
- package/src/astro/routes/api/content/[collection]/index.ts +20 -10
- package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
- package/src/astro/routes/api/import/wordpress/media.ts +2 -7
- package/src/astro/routes/api/import/wordpress/prepare.ts +10 -0
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +4 -3
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +4 -3
- package/src/astro/routes/api/manifest.ts +7 -0
- package/src/astro/routes/api/oauth/device/code.ts +2 -1
- package/src/astro/routes/api/oauth/device/token.ts +2 -1
- package/src/astro/routes/api/settings/email.ts +4 -9
- package/src/astro/routes/api/setup/admin-verify.ts +30 -5
- package/src/astro/routes/api/setup/admin.ts +38 -8
- package/src/astro/routes/api/setup/index.ts +7 -4
- package/src/astro/routes/api/setup/status.ts +3 -1
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +4 -1
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +4 -1
- package/src/astro/routes/api/widget-areas/[name].ts +4 -1
- package/src/astro/routes/api/widget-areas/index.ts +4 -1
- package/src/astro/types.ts +18 -0
- package/src/auth/mode.ts +15 -3
- package/src/auth/providers/github-admin.tsx +29 -0
- package/src/auth/providers/github.ts +31 -0
- package/src/auth/providers/google-admin.tsx +44 -0
- package/src/auth/providers/google.ts +31 -0
- package/src/auth/rate-limit.ts +50 -22
- package/src/auth/setup-nonce.ts +22 -0
- package/src/auth/trusted-proxy.ts +92 -0
- package/src/auth/types.ts +114 -4
- package/src/cli/commands/bundle.ts +3 -1
- package/src/components/EmDashImage.astro +7 -6
- package/src/components/Gallery.astro +5 -3
- package/src/components/Image.astro +8 -3
- package/src/components/InlinePortableTextEditor.tsx +2 -1
- package/src/components/LiveSearch.astro +5 -14
- package/src/database/migrations/035_bounded_404_log.ts +112 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/audit.ts +6 -8
- package/src/database/repositories/byline.ts +6 -8
- package/src/database/repositories/comment.ts +12 -16
- package/src/database/repositories/content.ts +79 -40
- package/src/database/repositories/index.ts +1 -1
- package/src/database/repositories/media.ts +10 -13
- package/src/database/repositories/options.ts +25 -0
- package/src/database/repositories/plugin-storage.ts +4 -6
- package/src/database/repositories/redirect.ts +123 -24
- package/src/database/repositories/taxonomy.ts +14 -3
- package/src/database/repositories/types.ts +57 -8
- package/src/database/repositories/user.ts +6 -8
- package/src/database/types.ts +9 -0
- package/src/emdash-runtime.ts +309 -91
- package/src/import/registry.ts +4 -3
- package/src/import/ssrf.ts +253 -12
- package/src/index.ts +5 -1
- package/src/loader.ts +6 -5
- package/src/mcp/server.ts +753 -107
- package/src/media/normalize.ts +1 -1
- package/src/media/url.ts +78 -0
- package/src/plugins/context.ts +15 -3
- package/src/plugins/email-console.ts +10 -3
- package/src/plugins/hooks.ts +11 -0
- package/src/plugins/manager.ts +6 -0
- package/src/plugins/manifest-schema.ts +12 -0
- package/src/plugins/request-meta.ts +66 -15
- package/src/plugins/routes.ts +3 -1
- package/src/plugins/types.ts +23 -2
- package/src/query.ts +1 -1
- package/src/request-cache.ts +3 -0
- package/src/schema/registry.ts +41 -5
- package/src/search/fts-manager.ts +0 -2
- package/src/search/query.ts +111 -26
- package/src/search/types.ts +8 -1
- package/src/sections/index.ts +7 -9
- package/src/seed/apply.ts +26 -0
- package/src/storage/s3.ts +12 -6
- package/src/virtual-modules.d.ts +21 -1
- package/src/visual-editing/toolbar.ts +6 -1
- package/src/widgets/index.ts +1 -1
- package/dist/apply-B4MsLM-w.mjs.map +0 -1
- package/dist/byline-C4OVd8b3.mjs.map +0 -1
- package/dist/content-BsBoyj8G.mjs.map +0 -1
- package/dist/error-CiYn9yDu.mjs.map +0 -1
- package/dist/index-BYv0mB9g.d.mts.map +0 -1
- package/dist/loader-DeiBJEMe.mjs.map +0 -1
- package/dist/manifest-schema-V30qsMft.mjs.map +0 -1
- package/dist/media-DqHVh136.mjs.map +0 -1
- package/dist/mode-CpNnGkPz.mjs.map +0 -1
- package/dist/redirect-7lGhLBNZ.mjs.map +0 -1
- package/dist/registry-Ci3WxVAr.mjs.map +0 -1
- package/dist/request-cache-DiR961CV.mjs.map +0 -1
- package/dist/runner-Cd-_WyDo.mjs.map +0 -1
- package/dist/search-DI4bM2w9.mjs.map +0 -1
- package/dist/types-CMMN0pNg.mjs +0 -31
- package/dist/types-CMMN0pNg.mjs.map +0 -1
- package/dist/types-DgrIP0tF.d.mts.map +0 -1
- package/dist/version-Uaf2ynPX.mjs +0 -7
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { n as validateJsonFieldName, r as validatePluginIdentifier, t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
|
|
2
2
|
import { o as jsonExtractExpr } from "./dialect-helpers-DhTzaUxP.mjs";
|
|
3
|
-
import { a as slugify, r as RevisionRepository, t as ContentRepository } from "./content-
|
|
3
|
+
import { a as slugify, r as RevisionRepository, t as ContentRepository } from "./content-BcQPYxdV.mjs";
|
|
4
4
|
import { r as encodeBase64, t as decodeBase64 } from "./base64-MBPo9ozB.mjs";
|
|
5
|
-
import { n as
|
|
6
|
-
import { t as MediaRepository } from "./media-
|
|
7
|
-
import { a as
|
|
5
|
+
import { i as encodeCursor, n as InvalidCursorError, r as decodeCursor, t as EmDashValidationError } from "./types-BIgulNsW.mjs";
|
|
6
|
+
import { t as MediaRepository } from "./media-D8FbNsl0.mjs";
|
|
7
|
+
import { a as ssrfSafeFetch, i as resolveAndValidateExternalUrl, o as stripCredentialHeaders, p as OptionsRepository, r as SsrfError, s as validateExternalUrl } from "./apply-x0eMK1lX.mjs";
|
|
8
8
|
import { t as withTransaction } from "./transaction-Cn2rjY78.mjs";
|
|
9
|
-
import { t as RedirectRepository } from "./redirect-
|
|
9
|
+
import { t as RedirectRepository } from "./redirect-D_pshWdf.mjs";
|
|
10
10
|
import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-HGz06Soa.mjs";
|
|
11
|
-
import { t as BylineRepository } from "./byline-
|
|
11
|
+
import { t as BylineRepository } from "./byline-Chbr2GoP.mjs";
|
|
12
12
|
import { r as isI18nEnabled } from "./config-BXwuX8Bx.mjs";
|
|
13
13
|
import { r as invalidateRedirectCache } from "./cache-BkKBuIvS.mjs";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { n as
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
14
|
+
import { t as isMissingTableError } from "./db-errors-l1Qh2RPR.mjs";
|
|
15
|
+
import { r as hashString } from "./zod-generator-CpwccCIv.mjs";
|
|
16
|
+
import { i as FTSManager, n as SchemaRegistry } from "./registry-C3Mr0ODu.mjs";
|
|
17
|
+
import { n as getDb } from "./loader-CndGj8kM.mjs";
|
|
18
|
+
import { n as requestCached } from "./request-cache-Ci7f5pBb.mjs";
|
|
19
|
+
import { i as pluginManifestSchema } from "./manifest-schema-DH9xhc6t.mjs";
|
|
20
|
+
import { t as generatePreviewToken } from "./tokens-D9vnZqYS.mjs";
|
|
19
21
|
import { sql } from "kysely";
|
|
20
22
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
21
23
|
import { ulid } from "ulidx";
|
|
@@ -78,7 +80,7 @@ var UserRepository = class UserRepository {
|
|
|
78
80
|
if (options.role !== void 0) query = query.where("role", "=", UserRepository.resolveRole(options.role));
|
|
79
81
|
if (options.cursor) {
|
|
80
82
|
const decoded = decodeCursor(options.cursor);
|
|
81
|
-
|
|
83
|
+
query = query.where((eb) => eb.or([eb("created_at", "<", decoded.orderValue), eb.and([eb("created_at", "=", decoded.orderValue), eb("id", "<", decoded.id)])]));
|
|
82
84
|
}
|
|
83
85
|
const rows = await query.execute();
|
|
84
86
|
const items = rows.slice(0, limit).map((row) => this.rowToUser(row));
|
|
@@ -228,7 +230,7 @@ var CommentRepository = class CommentRepository {
|
|
|
228
230
|
if (options.status) query = query.where("status", "=", options.status);
|
|
229
231
|
if (options.cursor) {
|
|
230
232
|
const decoded = decodeCursor(options.cursor);
|
|
231
|
-
|
|
233
|
+
query = query.where((eb) => eb.or([eb("created_at", ">", decoded.orderValue), eb.and([eb("created_at", "=", decoded.orderValue), eb("id", ">", decoded.id)])]));
|
|
232
234
|
}
|
|
233
235
|
query = query.orderBy("created_at", "asc").orderBy("id", "asc").limit(limit + 1);
|
|
234
236
|
const rows = await query.execute();
|
|
@@ -259,7 +261,7 @@ var CommentRepository = class CommentRepository {
|
|
|
259
261
|
}
|
|
260
262
|
if (options.cursor) {
|
|
261
263
|
const decoded = decodeCursor(options.cursor);
|
|
262
|
-
|
|
264
|
+
query = query.where((eb) => eb.or([eb("created_at", "<", decoded.orderValue), eb.and([eb("created_at", "=", decoded.orderValue), eb("id", "<", decoded.id)])]));
|
|
263
265
|
}
|
|
264
266
|
query = query.orderBy("created_at", "desc").orderBy("id", "desc").limit(limit + 1);
|
|
265
267
|
const rows = await query.execute();
|
|
@@ -683,7 +685,7 @@ var PluginStorageRepository = class {
|
|
|
683
685
|
}
|
|
684
686
|
if (cursor) {
|
|
685
687
|
const decoded = decodeCursor(cursor);
|
|
686
|
-
|
|
688
|
+
query = query.where(({ eb }) => eb(sql`(created_at, id)`, ">", sql`(${decoded.orderValue}, ${decoded.id})`));
|
|
687
689
|
}
|
|
688
690
|
if (Object.keys(orderBy).length > 0) for (const [field, direction] of Object.entries(orderBy)) {
|
|
689
691
|
const extract = jsonExtract(this.db, field);
|
|
@@ -976,6 +978,16 @@ function validateRev(rev, item) {
|
|
|
976
978
|
//#endregion
|
|
977
979
|
//#region src/api/handlers/content.ts
|
|
978
980
|
/**
|
|
981
|
+
* Narrow a caught error to one carrying a structured `apiError` discriminant.
|
|
982
|
+
* Used by transaction callbacks that want to surface a specific error code
|
|
983
|
+
* through the standard Error throwing path.
|
|
984
|
+
*/
|
|
985
|
+
function hasApiError(error) {
|
|
986
|
+
if (!(error instanceof Error) || !("apiError" in error)) return false;
|
|
987
|
+
const { apiError } = error;
|
|
988
|
+
return typeof apiError === "object" && apiError !== null && "code" in apiError && typeof apiError.code === "string";
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
979
991
|
* Extract a slug source (title or name) from content data.
|
|
980
992
|
* Returns null if no suitable string field is found.
|
|
981
993
|
*/
|
|
@@ -1125,6 +1137,27 @@ async function handleContentList(db, collection, params) {
|
|
|
1125
1137
|
}
|
|
1126
1138
|
};
|
|
1127
1139
|
} catch (error) {
|
|
1140
|
+
if (error instanceof InvalidCursorError) return {
|
|
1141
|
+
success: false,
|
|
1142
|
+
error: {
|
|
1143
|
+
code: "INVALID_CURSOR",
|
|
1144
|
+
message: error.message
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
if (isMissingTableError(error)) return {
|
|
1148
|
+
success: false,
|
|
1149
|
+
error: {
|
|
1150
|
+
code: "COLLECTION_NOT_FOUND",
|
|
1151
|
+
message: `Collection '${collection}' not found`
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
if (error instanceof EmDashValidationError) return {
|
|
1155
|
+
success: false,
|
|
1156
|
+
error: {
|
|
1157
|
+
code: "VALIDATION_ERROR",
|
|
1158
|
+
message: error.message
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1128
1161
|
console.error("Content list error:", error);
|
|
1129
1162
|
return {
|
|
1130
1163
|
success: false,
|
|
@@ -1255,6 +1288,37 @@ async function handleContentCreate(db, collection, body) {
|
|
|
1255
1288
|
}
|
|
1256
1289
|
};
|
|
1257
1290
|
} catch (error) {
|
|
1291
|
+
if (isMissingTableError(error)) return {
|
|
1292
|
+
success: false,
|
|
1293
|
+
error: {
|
|
1294
|
+
code: "COLLECTION_NOT_FOUND",
|
|
1295
|
+
message: `Collection '${collection}' not found`
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
if (error instanceof EmDashValidationError) return {
|
|
1299
|
+
success: false,
|
|
1300
|
+
error: {
|
|
1301
|
+
code: "VALIDATION_ERROR",
|
|
1302
|
+
message: error.message
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
const message = error instanceof Error ? error.message.toLowerCase() : "";
|
|
1306
|
+
if (message.includes("unique constraint failed") || message.includes("duplicate key")) {
|
|
1307
|
+
if (message.includes("slug")) return {
|
|
1308
|
+
success: false,
|
|
1309
|
+
error: {
|
|
1310
|
+
code: "SLUG_CONFLICT",
|
|
1311
|
+
message: `Slug '${body.slug ?? "(auto-generated)"}' already exists in collection '${collection}'`
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
return {
|
|
1315
|
+
success: false,
|
|
1316
|
+
error: {
|
|
1317
|
+
code: "CONFLICT",
|
|
1318
|
+
message: "Unique constraint violation"
|
|
1319
|
+
}
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1258
1322
|
console.error("Content create error:", error);
|
|
1259
1323
|
return {
|
|
1260
1324
|
success: false,
|
|
@@ -1298,7 +1362,8 @@ async function handleContentUpdate(db, collection, id, body) {
|
|
|
1298
1362
|
data: body.data,
|
|
1299
1363
|
slug: body.slug,
|
|
1300
1364
|
status: body.status,
|
|
1301
|
-
authorId: body.authorId
|
|
1365
|
+
authorId: body.authorId,
|
|
1366
|
+
publishedAt: body.publishedAt
|
|
1302
1367
|
});
|
|
1303
1368
|
if (body.bylines !== void 0) {
|
|
1304
1369
|
await bylineRepo.setContentBylines(collection, resolvedId, body.bylines);
|
|
@@ -1323,13 +1388,41 @@ async function handleContentUpdate(db, collection, id, body) {
|
|
|
1323
1388
|
}
|
|
1324
1389
|
};
|
|
1325
1390
|
} catch (error) {
|
|
1326
|
-
if (error
|
|
1327
|
-
|
|
1391
|
+
if (hasApiError(error)) return {
|
|
1392
|
+
success: false,
|
|
1393
|
+
error: {
|
|
1394
|
+
code: error.apiError.code,
|
|
1395
|
+
message: error.message
|
|
1396
|
+
}
|
|
1397
|
+
};
|
|
1398
|
+
if (isMissingTableError(error)) return {
|
|
1399
|
+
success: false,
|
|
1400
|
+
error: {
|
|
1401
|
+
code: "COLLECTION_NOT_FOUND",
|
|
1402
|
+
message: `Collection '${collection}' not found`
|
|
1403
|
+
}
|
|
1404
|
+
};
|
|
1405
|
+
if (error instanceof EmDashValidationError) return {
|
|
1406
|
+
success: false,
|
|
1407
|
+
error: {
|
|
1408
|
+
code: "VALIDATION_ERROR",
|
|
1409
|
+
message: error.message
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
const message = error instanceof Error ? error.message.toLowerCase() : "";
|
|
1413
|
+
if (message.includes("unique constraint failed") || message.includes("duplicate key")) {
|
|
1414
|
+
if (message.includes("slug")) return {
|
|
1415
|
+
success: false,
|
|
1416
|
+
error: {
|
|
1417
|
+
code: "SLUG_CONFLICT",
|
|
1418
|
+
message: `Slug '${body.slug ?? id}' already exists in collection '${collection}'`
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1328
1421
|
return {
|
|
1329
1422
|
success: false,
|
|
1330
1423
|
error: {
|
|
1331
|
-
code,
|
|
1332
|
-
message:
|
|
1424
|
+
code: "CONFLICT",
|
|
1425
|
+
message: "Unique constraint violation"
|
|
1333
1426
|
}
|
|
1334
1427
|
};
|
|
1335
1428
|
}
|
|
@@ -1518,6 +1611,13 @@ async function handleContentListTrashed(db, collection, options = {}) {
|
|
|
1518
1611
|
}
|
|
1519
1612
|
};
|
|
1520
1613
|
} catch (error) {
|
|
1614
|
+
if (error instanceof InvalidCursorError) return {
|
|
1615
|
+
success: false,
|
|
1616
|
+
error: {
|
|
1617
|
+
code: "INVALID_CURSOR",
|
|
1618
|
+
message: error.message
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1521
1621
|
console.error("Content list trashed error:", error);
|
|
1522
1622
|
return {
|
|
1523
1623
|
success: false,
|
|
@@ -1840,37 +1940,6 @@ async function syncNonTranslatableFields(trx, collectionSlug, updatedItemId, tra
|
|
|
1840
1940
|
`.execute(trx);
|
|
1841
1941
|
}
|
|
1842
1942
|
|
|
1843
|
-
//#endregion
|
|
1844
|
-
//#region src/utils/hash.ts
|
|
1845
|
-
/**
|
|
1846
|
-
* SHA-256 hash of a string, truncated to 16 hex chars (64 bits).
|
|
1847
|
-
* For cache invalidation / ETags — not for security.
|
|
1848
|
-
*/
|
|
1849
|
-
async function hashString(content) {
|
|
1850
|
-
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(content));
|
|
1851
|
-
return Array.from(new Uint8Array(buf).slice(0, 8), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1852
|
-
}
|
|
1853
|
-
/**
|
|
1854
|
-
* Compute content hash using Web Crypto API
|
|
1855
|
-
*
|
|
1856
|
-
* Uses SHA-1 which is the fastest option in SubtleCrypto.
|
|
1857
|
-
* SHA-1 is cryptographically weak but fine for content deduplication
|
|
1858
|
-
* where we only need to detect identical files, not resist attacks.
|
|
1859
|
-
*
|
|
1860
|
-
* Returns hex string prefixed with "sha1:" for future-proofing
|
|
1861
|
-
*/
|
|
1862
|
-
async function computeContentHash(content) {
|
|
1863
|
-
let buf;
|
|
1864
|
-
if (content instanceof ArrayBuffer) buf = content;
|
|
1865
|
-
else {
|
|
1866
|
-
buf = new ArrayBuffer(content.byteLength);
|
|
1867
|
-
new Uint8Array(buf).set(content);
|
|
1868
|
-
}
|
|
1869
|
-
const hashBuffer = await crypto.subtle.digest("SHA-1", buf);
|
|
1870
|
-
const hashArray = new Uint8Array(hashBuffer);
|
|
1871
|
-
return `sha1:${Array.from(hashArray, (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
1943
|
//#endregion
|
|
1875
1944
|
//#region src/api/handlers/manifest.ts
|
|
1876
1945
|
/**
|
|
@@ -2105,7 +2174,14 @@ async function handleMediaList(db, params) {
|
|
|
2105
2174
|
nextCursor: result.nextCursor
|
|
2106
2175
|
}
|
|
2107
2176
|
};
|
|
2108
|
-
} catch {
|
|
2177
|
+
} catch (error) {
|
|
2178
|
+
if (error instanceof InvalidCursorError) return {
|
|
2179
|
+
success: false,
|
|
2180
|
+
error: {
|
|
2181
|
+
code: "INVALID_CURSOR",
|
|
2182
|
+
message: error.message
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
2109
2185
|
return {
|
|
2110
2186
|
success: false,
|
|
2111
2187
|
error: {
|
|
@@ -2435,7 +2511,7 @@ async function getSectionsWithDb(db, options = {}) {
|
|
|
2435
2511
|
query = query.orderBy("title", "asc").orderBy("id", "asc");
|
|
2436
2512
|
if (options.cursor) {
|
|
2437
2513
|
const decoded = decodeCursor(options.cursor);
|
|
2438
|
-
|
|
2514
|
+
query = query.where((eb) => eb.or([eb("title", ">", decoded.orderValue), eb.and([eb("title", "=", decoded.orderValue), eb("id", ">", decoded.id)])]));
|
|
2439
2515
|
}
|
|
2440
2516
|
query = query.limit(limit + 1);
|
|
2441
2517
|
const rows = await query.$castTo().execute();
|
|
@@ -2541,7 +2617,7 @@ const VALID_ROLE_LEVELS = new Set([
|
|
|
2541
2617
|
const roleLevel = z$1.coerce.number().int().refine((n) => VALID_ROLE_LEVELS.has(n), { message: "Invalid role level. Must be 10, 20, 30, 40, or 50" });
|
|
2542
2618
|
/** Pagination query params — cursor-based */
|
|
2543
2619
|
const cursorPaginationQuery = z$1.object({
|
|
2544
|
-
cursor: z$1.string().optional().meta({ description: "Opaque cursor for pagination" }),
|
|
2620
|
+
cursor: z$1.string().max(2048).optional().meta({ description: "Opaque cursor for pagination" }),
|
|
2545
2621
|
limit: z$1.coerce.number().int().min(1).max(100).optional().default(50).meta({ description: "Maximum number of items to return (1-100, default 50)" })
|
|
2546
2622
|
}).meta({ id: "CursorPaginationQuery" });
|
|
2547
2623
|
/** Pagination query params — offset-based */
|
|
@@ -2640,6 +2716,11 @@ const contentListQuery = cursorPaginationQuery.extend({
|
|
|
2640
2716
|
order: z$1.enum(["asc", "desc"]).optional(),
|
|
2641
2717
|
locale: localeCode.optional()
|
|
2642
2718
|
}).meta({ id: "ContentListQuery" });
|
|
2719
|
+
/** ISO 8601 datetime for `publishedAt` / `createdAt`. Routes gate writes behind `content:publish_any`. */
|
|
2720
|
+
const contentDateOverride = z$1.iso.datetime({
|
|
2721
|
+
offset: true,
|
|
2722
|
+
message: "must be an ISO 8601 datetime"
|
|
2723
|
+
}).nullish();
|
|
2643
2724
|
const contentCreateBody = z$1.object({
|
|
2644
2725
|
data: z$1.record(z$1.string(), z$1.unknown()),
|
|
2645
2726
|
slug: z$1.string().nullish(),
|
|
@@ -2647,7 +2728,9 @@ const contentCreateBody = z$1.object({
|
|
|
2647
2728
|
bylines: z$1.array(contentBylineInputSchema).optional(),
|
|
2648
2729
|
locale: localeCode.optional(),
|
|
2649
2730
|
translationOf: z$1.string().optional(),
|
|
2650
|
-
seo: contentSeoInput.optional()
|
|
2731
|
+
seo: contentSeoInput.optional(),
|
|
2732
|
+
publishedAt: contentDateOverride,
|
|
2733
|
+
createdAt: contentDateOverride
|
|
2651
2734
|
}).meta({ id: "ContentCreateBody" });
|
|
2652
2735
|
const contentUpdateBody = z$1.object({
|
|
2653
2736
|
data: z$1.record(z$1.string(), z$1.unknown()).optional(),
|
|
@@ -2657,7 +2740,8 @@ const contentUpdateBody = z$1.object({
|
|
|
2657
2740
|
bylines: z$1.array(contentBylineInputSchema).optional(),
|
|
2658
2741
|
_rev: z$1.string().optional().meta({ description: "Opaque revision token for optimistic concurrency" }),
|
|
2659
2742
|
skipRevision: z$1.boolean().optional(),
|
|
2660
|
-
seo: contentSeoInput.optional()
|
|
2743
|
+
seo: contentSeoInput.optional(),
|
|
2744
|
+
publishedAt: contentDateOverride
|
|
2661
2745
|
}).meta({ id: "ContentUpdateBody" });
|
|
2662
2746
|
const contentScheduleBody = z$1.object({ scheduledAt: z$1.string().min(1, "scheduledAt is required").meta({
|
|
2663
2747
|
description: "ISO 8601 datetime for scheduled publishing",
|
|
@@ -3489,6 +3573,8 @@ const setupAdminBody = z$1.object({
|
|
|
3489
3573
|
name: z$1.string().optional()
|
|
3490
3574
|
});
|
|
3491
3575
|
const setupAdminVerifyBody = z$1.object({ credential: registrationCredential });
|
|
3576
|
+
const atprotoLoginBody = z$1.object({ handle: z$1.string().trim().min(1) });
|
|
3577
|
+
const setupAtprotoAdminBody = z$1.object({ handle: z$1.string().trim().min(1) });
|
|
3492
3578
|
|
|
3493
3579
|
//#endregion
|
|
3494
3580
|
//#region src/api/schemas/users.ts
|
|
@@ -3592,18 +3678,15 @@ const widgetAreaSchema = z$1.object({
|
|
|
3592
3678
|
}).meta({ id: "WidgetArea" });
|
|
3593
3679
|
const widgetSchema = z$1.object({
|
|
3594
3680
|
id: z$1.string(),
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
component_props: z$1.string().nullable(),
|
|
3602
|
-
sort_order: z$1.number().int(),
|
|
3603
|
-
created_at: z$1.string(),
|
|
3604
|
-
updated_at: z$1.string()
|
|
3681
|
+
type: widgetType,
|
|
3682
|
+
title: z$1.string().optional(),
|
|
3683
|
+
content: z$1.array(z$1.record(z$1.string(), z$1.unknown())).optional(),
|
|
3684
|
+
menuName: z$1.string().optional(),
|
|
3685
|
+
componentId: z$1.string().optional(),
|
|
3686
|
+
componentProps: z$1.record(z$1.string(), z$1.unknown()).optional()
|
|
3605
3687
|
}).meta({ id: "Widget" });
|
|
3606
3688
|
const widgetAreaWithWidgetsSchema = widgetAreaSchema.extend({ widgets: z$1.array(widgetSchema) }).meta({ id: "WidgetAreaWithWidgets" });
|
|
3689
|
+
const widgetAreaWithWidgetsAndCountSchema = widgetAreaWithWidgetsSchema.extend({ widgetCount: z$1.number().int() }).meta({ id: "WidgetAreaWithWidgetsAndCount" });
|
|
3607
3690
|
|
|
3608
3691
|
//#endregion
|
|
3609
3692
|
//#region src/api/schemas/redirects.ts
|
|
@@ -4983,6 +5066,55 @@ function resolveHook(hook, pluginId) {
|
|
|
4983
5066
|
};
|
|
4984
5067
|
}
|
|
4985
5068
|
|
|
5069
|
+
//#endregion
|
|
5070
|
+
//#region src/auth/trusted-proxy.ts
|
|
5071
|
+
/**
|
|
5072
|
+
* RFC 7230 token — valid characters for an HTTP header name. Invalid names
|
|
5073
|
+
* passed to `Headers.get()` throw a TypeError at runtime, which would
|
|
5074
|
+
* otherwise surface as a 500 from every auth route.
|
|
5075
|
+
*/
|
|
5076
|
+
const HEADER_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9a-z]+$/;
|
|
5077
|
+
/**
|
|
5078
|
+
* Normalise a list of header names the way both the config path and any
|
|
5079
|
+
* caller passing a pre-resolved list should do: trim, lowercase, drop
|
|
5080
|
+
* empty, drop anything that isn't a valid RFC 7230 token. Invalid names
|
|
5081
|
+
* would crash `Headers.get()` at runtime.
|
|
5082
|
+
*/
|
|
5083
|
+
function normalizeTrustedHeaders(names) {
|
|
5084
|
+
return names.map((h) => h.trim().toLowerCase()).filter((h) => h.length > 0 && HEADER_NAME_PATTERN.test(h));
|
|
5085
|
+
}
|
|
5086
|
+
function isValidHeaderName(name) {
|
|
5087
|
+
return HEADER_NAME_PATTERN.test(name);
|
|
5088
|
+
}
|
|
5089
|
+
/** Cache for the env-derived value. `null` means "not yet parsed". */
|
|
5090
|
+
let _envCache = null;
|
|
5091
|
+
function getEnvTrustedHeaders() {
|
|
5092
|
+
if (_envCache !== null) return _envCache;
|
|
5093
|
+
let raw;
|
|
5094
|
+
try {
|
|
5095
|
+
const importMetaEnv = import.meta.env;
|
|
5096
|
+
raw = (typeof process !== "undefined" ? process.env?.EMDASH_TRUSTED_PROXY_HEADERS : void 0) || importMetaEnv?.EMDASH_TRUSTED_PROXY_HEADERS;
|
|
5097
|
+
} catch {
|
|
5098
|
+
raw = void 0;
|
|
5099
|
+
}
|
|
5100
|
+
if (!raw) {
|
|
5101
|
+
_envCache = [];
|
|
5102
|
+
return _envCache;
|
|
5103
|
+
}
|
|
5104
|
+
_envCache = raw.split(",").map((s) => s.trim().toLowerCase()).filter((s) => s.length > 0 && isValidHeaderName(s));
|
|
5105
|
+
return _envCache;
|
|
5106
|
+
}
|
|
5107
|
+
/**
|
|
5108
|
+
* Return the lowercased list of headers to trust for client-IP resolution.
|
|
5109
|
+
*
|
|
5110
|
+
* When `config?.trustedProxyHeaders` is explicitly set (even to `[]`), it
|
|
5111
|
+
* wins. Otherwise fall through to the env var, then to `[]`.
|
|
5112
|
+
*/
|
|
5113
|
+
function getTrustedProxyHeaders(config) {
|
|
5114
|
+
if (config && config.trustedProxyHeaders !== void 0) return config.trustedProxyHeaders.map((h) => h.trim().toLowerCase()).filter((h) => h.length > 0 && isValidHeaderName(h));
|
|
5115
|
+
return getEnvTrustedHeaders();
|
|
5116
|
+
}
|
|
5117
|
+
|
|
4986
5118
|
//#endregion
|
|
4987
5119
|
//#region src/plugins/request-meta.ts
|
|
4988
5120
|
/**
|
|
@@ -5004,6 +5136,20 @@ function parseFirstForwardedIp(header) {
|
|
|
5004
5136
|
return IP_PATTERN.test(trimmed) ? trimmed : null;
|
|
5005
5137
|
}
|
|
5006
5138
|
/**
|
|
5139
|
+
* Read an IP from an operator-declared trusted header. XFF-style headers
|
|
5140
|
+
* (any name ending in `forwarded-for`) are parsed as comma-separated lists
|
|
5141
|
+
* and the first entry is used; everything else is treated as a single
|
|
5142
|
+
* trimmed value.
|
|
5143
|
+
*/
|
|
5144
|
+
function readIpFromHeader(headers, name) {
|
|
5145
|
+
const value = headers.get(name);
|
|
5146
|
+
if (!value) return null;
|
|
5147
|
+
if (name.endsWith("forwarded-for")) return parseFirstForwardedIp(value);
|
|
5148
|
+
const trimmed = value.trim();
|
|
5149
|
+
if (!trimmed) return null;
|
|
5150
|
+
return IP_PATTERN.test(trimmed) ? trimmed : null;
|
|
5151
|
+
}
|
|
5152
|
+
/**
|
|
5007
5153
|
* Get the Cloudflare `cf` object from the request, if present.
|
|
5008
5154
|
* Returns undefined when not running on Cloudflare Workers.
|
|
5009
5155
|
*/
|
|
@@ -5030,24 +5176,39 @@ function extractGeo(cf) {
|
|
|
5030
5176
|
* Extract normalized request metadata from a Request object.
|
|
5031
5177
|
*
|
|
5032
5178
|
* IP resolution order:
|
|
5033
|
-
* 1. `CF-Connecting-IP`
|
|
5034
|
-
*
|
|
5035
|
-
*
|
|
5036
|
-
*
|
|
5037
|
-
*
|
|
5038
|
-
* 3. `
|
|
5039
|
-
|
|
5040
|
-
|
|
5179
|
+
* 1. `CF-Connecting-IP` — trusted only when a `cf` object is present on the
|
|
5180
|
+
* request. CF edge overwrites any client-supplied value, so this is the
|
|
5181
|
+
* cryptographically trustworthy path on Workers. Operator-declared
|
|
5182
|
+
* trusted headers cannot override it.
|
|
5183
|
+
* 2. `X-Forwarded-For` first entry — trusted only with a `cf` object.
|
|
5184
|
+
* 3. Operator-declared trusted proxy headers (from `config.trustedProxyHeaders`
|
|
5185
|
+
* or the `EMDASH_TRUSTED_PROXY_HEADERS` env var), tried in order. Used as
|
|
5186
|
+
* the primary source off-CF and as a fill-in on CF.
|
|
5187
|
+
* 4. `null`
|
|
5188
|
+
*
|
|
5189
|
+
* The second argument accepts either the EmDash config or a pre-resolved
|
|
5190
|
+
* list of trusted headers, so callers that already have the list don't have
|
|
5191
|
+
* to round-trip through the config every request.
|
|
5192
|
+
*/
|
|
5193
|
+
function extractRequestMeta(request, configOrTrustedHeaders) {
|
|
5041
5194
|
const headers = request.headers;
|
|
5042
5195
|
const cf = getCfObject(request);
|
|
5196
|
+
const trusted = resolveTrustedHeaders(configOrTrustedHeaders);
|
|
5043
5197
|
let ip = null;
|
|
5044
5198
|
if (cf) {
|
|
5045
5199
|
const cfIp = headers.get("cf-connecting-ip")?.trim();
|
|
5046
5200
|
if (cfIp && IP_PATTERN.test(cfIp)) ip = cfIp;
|
|
5201
|
+
if (!ip) {
|
|
5202
|
+
const xff = headers.get("x-forwarded-for");
|
|
5203
|
+
ip = xff ? parseFirstForwardedIp(xff) : null;
|
|
5204
|
+
}
|
|
5047
5205
|
}
|
|
5048
|
-
if (!ip
|
|
5049
|
-
const
|
|
5050
|
-
|
|
5206
|
+
if (!ip) for (const name of trusted) {
|
|
5207
|
+
const value = readIpFromHeader(headers, name);
|
|
5208
|
+
if (value) {
|
|
5209
|
+
ip = value;
|
|
5210
|
+
break;
|
|
5211
|
+
}
|
|
5051
5212
|
}
|
|
5052
5213
|
const userAgent = headers.get("user-agent")?.trim() || null;
|
|
5053
5214
|
const referer = headers.get("referer")?.trim() || null;
|
|
@@ -5059,6 +5220,10 @@ function extractRequestMeta(request) {
|
|
|
5059
5220
|
geo
|
|
5060
5221
|
};
|
|
5061
5222
|
}
|
|
5223
|
+
function resolveTrustedHeaders(value) {
|
|
5224
|
+
if (Array.isArray(value)) return normalizeTrustedHeaders(value);
|
|
5225
|
+
return getTrustedProxyHeaders(value);
|
|
5226
|
+
}
|
|
5062
5227
|
/**
|
|
5063
5228
|
* Headers that must never cross the RPC boundary to sandboxed plugins.
|
|
5064
5229
|
* Session tokens, auth credentials, and infrastructure headers are stripped
|
|
@@ -5707,7 +5872,7 @@ function createUnrestrictedHttpAccess(pluginId) {
|
|
|
5707
5872
|
let currentInit = init;
|
|
5708
5873
|
for (let i = 0; i <= MAX_PLUGIN_REDIRECTS; i++) {
|
|
5709
5874
|
try {
|
|
5710
|
-
|
|
5875
|
+
await resolveAndValidateExternalUrl(currentUrl);
|
|
5711
5876
|
} catch (e) {
|
|
5712
5877
|
const msg = e instanceof SsrfError ? e.message : "SSRF validation failed";
|
|
5713
5878
|
throw new Error(`Plugin "${pluginId}": blocked fetch to "${new URL(currentUrl).hostname}": ${msg}`, { cause: e });
|
|
@@ -6724,6 +6889,15 @@ var HookPipeline = class HookPipeline {
|
|
|
6724
6889
|
return (this.hooks.get(hookName) ?? []).filter((h) => h.exclusive).map((h) => ({ pluginId: h.pluginId }));
|
|
6725
6890
|
}
|
|
6726
6891
|
/**
|
|
6892
|
+
* Get all plugins that registered a non-exclusive handler for a given
|
|
6893
|
+
* hook (e.g. `email:beforeSend`, `email:afterSend`), preserving priority
|
|
6894
|
+
* order. Partitions with `getExclusiveHookProviders()`, which returns
|
|
6895
|
+
* plugins whose registration is marked `exclusive: true`.
|
|
6896
|
+
*/
|
|
6897
|
+
getHookProviders(hookName) {
|
|
6898
|
+
return (this.hooks.get(hookName) ?? []).filter((h) => !h.exclusive).map((h) => ({ pluginId: h.pluginId }));
|
|
6899
|
+
}
|
|
6900
|
+
/**
|
|
6727
6901
|
* Invoke an exclusive hook — dispatch only to the selected provider.
|
|
6728
6902
|
* Returns null if no provider is selected or if the selected hook
|
|
6729
6903
|
* is not found in the pipeline.
|
|
@@ -6967,8 +7141,15 @@ const MAX_STORED_EMAILS = 100;
|
|
|
6967
7141
|
* instances (the runtime and the route handler may load separate copies
|
|
6968
7142
|
* of this module, but globalThis is always the same object).
|
|
6969
7143
|
*/
|
|
6970
|
-
const GLOBAL_KEY = "
|
|
6971
|
-
const
|
|
7144
|
+
const GLOBAL_KEY = Symbol.for("emdash:dev-emails");
|
|
7145
|
+
const g = globalThis;
|
|
7146
|
+
const storedEmails = (() => {
|
|
7147
|
+
const existing = g[GLOBAL_KEY];
|
|
7148
|
+
if (existing) return existing;
|
|
7149
|
+
const fresh = [];
|
|
7150
|
+
g[GLOBAL_KEY] = fresh;
|
|
7151
|
+
return fresh;
|
|
7152
|
+
})();
|
|
6972
7153
|
/**
|
|
6973
7154
|
* The email:deliver handler for the dev console provider.
|
|
6974
7155
|
* Logs to console and stores in memory.
|
|
@@ -7001,9 +7182,11 @@ async function devConsoleEmailDeliver(event, _ctx) {
|
|
|
7001
7182
|
var PluginRouteHandler = class {
|
|
7002
7183
|
contextFactory;
|
|
7003
7184
|
plugin;
|
|
7185
|
+
trustedProxyHeaders;
|
|
7004
7186
|
constructor(plugin, factoryOptions) {
|
|
7005
7187
|
this.plugin = plugin;
|
|
7006
7188
|
this.contextFactory = new PluginContextFactory(factoryOptions);
|
|
7189
|
+
this.trustedProxyHeaders = factoryOptions.trustedProxyHeaders ?? [];
|
|
7007
7190
|
}
|
|
7008
7191
|
/**
|
|
7009
7192
|
* Invoke a route by name
|
|
@@ -7036,7 +7219,7 @@ var PluginRouteHandler = class {
|
|
|
7036
7219
|
...this.contextFactory.createContext(this.plugin),
|
|
7037
7220
|
input: validatedInput,
|
|
7038
7221
|
request: options.request,
|
|
7039
|
-
requestMeta: extractRequestMeta(options.request)
|
|
7222
|
+
requestMeta: extractRequestMeta(options.request, this.trustedProxyHeaders)
|
|
7040
7223
|
};
|
|
7041
7224
|
try {
|
|
7042
7225
|
return {
|
|
@@ -7215,7 +7398,8 @@ var PluginManager = class {
|
|
|
7215
7398
|
this.factoryOptions = {
|
|
7216
7399
|
db: options.db,
|
|
7217
7400
|
storage: options.storage,
|
|
7218
|
-
getUploadUrl: options.getUploadUrl
|
|
7401
|
+
getUploadUrl: options.getUploadUrl,
|
|
7402
|
+
trustedProxyHeaders: options.trustedProxyHeaders
|
|
7219
7403
|
};
|
|
7220
7404
|
}
|
|
7221
7405
|
/**
|
|
@@ -7712,7 +7896,7 @@ async function probeUrl(url) {
|
|
|
7712
7896
|
let normalizedUrl = url.trim();
|
|
7713
7897
|
if (!normalizedUrl.startsWith("http")) normalizedUrl = `https://${normalizedUrl}`;
|
|
7714
7898
|
normalizedUrl = normalizedUrl.replace(TRAILING_SLASHES_PATTERN, "");
|
|
7715
|
-
|
|
7899
|
+
await resolveAndValidateExternalUrl(normalizedUrl);
|
|
7716
7900
|
const results = [];
|
|
7717
7901
|
const probePromises = getUrlSources().map(async (source) => {
|
|
7718
7902
|
try {
|
|
@@ -9092,6 +9276,22 @@ const WHITESPACE_SPLIT_PATTERN = /\s+/;
|
|
|
9092
9276
|
const FTS_OPERATORS_PATTERN = /\b(AND|OR|NOT|NEAR)\b/i;
|
|
9093
9277
|
const DOUBLE_QUOTE_PATTERN = /"/g;
|
|
9094
9278
|
/**
|
|
9279
|
+
* Detect FTS5 query syntax errors. Match specifically on the SQLite FTS5
|
|
9280
|
+
* error fingerprints rather than a broad "fts5" / "syntax error" filter
|
|
9281
|
+
* (which would also swallow internal table-corruption errors). The two
|
|
9282
|
+
* fingerprints we care about are:
|
|
9283
|
+
*
|
|
9284
|
+
* - "fts5: syntax error near …" — unbalanced quotes, stray operators,
|
|
9285
|
+
* other malformed user input
|
|
9286
|
+
* - "unknown special query: …" — bare special tokens like `^*` that
|
|
9287
|
+
* parse but don't resolve to a real FTS5 directive
|
|
9288
|
+
*/
|
|
9289
|
+
function isFts5SyntaxError(error) {
|
|
9290
|
+
if (!(error instanceof Error)) return false;
|
|
9291
|
+
const message = error.message.toLowerCase();
|
|
9292
|
+
return message.includes("fts5: syntax error") || message.includes("unknown special query");
|
|
9293
|
+
}
|
|
9294
|
+
/**
|
|
9095
9295
|
* Search across multiple collections
|
|
9096
9296
|
*
|
|
9097
9297
|
* Public API that auto-injects the database.
|
|
@@ -9188,7 +9388,9 @@ async function searchSingleCollection(db, collection, query, options, weights) {
|
|
|
9188
9388
|
bm25Args = weightValues.join(", ");
|
|
9189
9389
|
}
|
|
9190
9390
|
const bm25Expr = bm25Args ? `bm25("${ftsTable}", ${bm25Args})` : `bm25("${ftsTable}")`;
|
|
9191
|
-
|
|
9391
|
+
let results;
|
|
9392
|
+
try {
|
|
9393
|
+
results = await sql`
|
|
9192
9394
|
SELECT
|
|
9193
9395
|
c.id,
|
|
9194
9396
|
c.slug,
|
|
@@ -9204,16 +9406,45 @@ async function searchSingleCollection(db, collection, query, options, weights) {
|
|
|
9204
9406
|
${locale ? sql`AND c.locale = ${locale}` : sql``}
|
|
9205
9407
|
ORDER BY score
|
|
9206
9408
|
LIMIT ${limit}
|
|
9207
|
-
`.execute(db)
|
|
9409
|
+
`.execute(db);
|
|
9410
|
+
} catch (error) {
|
|
9411
|
+
if (isFts5SyntaxError(error)) return [];
|
|
9412
|
+
throw error;
|
|
9413
|
+
}
|
|
9414
|
+
return results.rows.map((row) => ({
|
|
9208
9415
|
collection,
|
|
9209
9416
|
id: row.id,
|
|
9210
9417
|
slug: row.slug,
|
|
9211
9418
|
locale: row.locale,
|
|
9212
9419
|
title: row.title ?? void 0,
|
|
9213
|
-
snippet: row.snippet,
|
|
9420
|
+
snippet: row.snippet === null ? void 0 : sanitizeSnippet(row.snippet),
|
|
9214
9421
|
score: Math.abs(row.score)
|
|
9215
9422
|
}));
|
|
9216
9423
|
}
|
|
9424
|
+
const SNIPPET_AMP_RE = /&/g;
|
|
9425
|
+
const SNIPPET_LT_RE = /</g;
|
|
9426
|
+
const SNIPPET_GT_RE = />/g;
|
|
9427
|
+
const SNIPPET_QUOT_RE = /"/g;
|
|
9428
|
+
const SNIPPET_APOS_RE = /'/g;
|
|
9429
|
+
/**
|
|
9430
|
+
* Make an FTS5 snippet safe to render with `set:html` / `innerHTML`.
|
|
9431
|
+
*
|
|
9432
|
+
* SQLite's `snippet()` function splices literal `<mark>` and `</mark>`
|
|
9433
|
+
* markers around matched terms but does not escape the surrounding
|
|
9434
|
+
* source text. Posts that legitimately contain `<`, `>`, `&`, `"` or
|
|
9435
|
+
* `'` would render as broken markup, and a `<script>` literal in a
|
|
9436
|
+
* title (or any other indexed field) would execute when displayed.
|
|
9437
|
+
*
|
|
9438
|
+
* The fix: HTML-escape the whole string, which turns the markers into
|
|
9439
|
+
* `<mark>` / `</mark>`. Then restore those two patterns to
|
|
9440
|
+
* their original tag form. The result is "the indexed text with all
|
|
9441
|
+
* HTML metacharacters escaped, plus a small set of literal `<mark>`
|
|
9442
|
+
* highlight tags around matched terms" — which matches the API's
|
|
9443
|
+
* documented contract.
|
|
9444
|
+
*/
|
|
9445
|
+
function sanitizeSnippet(snippet) {
|
|
9446
|
+
return snippet.replace(SNIPPET_AMP_RE, "&").replace(SNIPPET_LT_RE, "<").replace(SNIPPET_GT_RE, ">").replace(SNIPPET_QUOT_RE, """).replace(SNIPPET_APOS_RE, "'").replaceAll("<mark>", "<mark>").replaceAll("</mark>", "</mark>");
|
|
9447
|
+
}
|
|
9217
9448
|
/**
|
|
9218
9449
|
* Get search suggestions for autocomplete
|
|
9219
9450
|
*
|
|
@@ -9237,20 +9468,26 @@ async function getSuggestions(db, query, options = {}) {
|
|
|
9237
9468
|
const contentTable = ftsManager.getContentTableName(collection);
|
|
9238
9469
|
const prefixQuery = escapeQuery(query);
|
|
9239
9470
|
if (!prefixQuery) continue;
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
|
|
9245
|
-
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9471
|
+
let results;
|
|
9472
|
+
try {
|
|
9473
|
+
results = await sql`
|
|
9474
|
+
SELECT
|
|
9475
|
+
c.id,
|
|
9476
|
+
c.title
|
|
9477
|
+
FROM "${sql.raw(ftsTable)}" f
|
|
9478
|
+
JOIN "${sql.raw(contentTable)}" c ON f.id = c.id
|
|
9479
|
+
WHERE "${sql.raw(ftsTable)}" MATCH ${prefixQuery}
|
|
9480
|
+
AND c.status = 'published'
|
|
9481
|
+
AND c.deleted_at IS NULL
|
|
9482
|
+
AND c.title IS NOT NULL
|
|
9483
|
+
${locale ? sql`AND c.locale = ${locale}` : sql``}
|
|
9484
|
+
ORDER BY bm25("${sql.raw(ftsTable)}")
|
|
9485
|
+
LIMIT ${limit}
|
|
9486
|
+
`.execute(db);
|
|
9487
|
+
} catch (error) {
|
|
9488
|
+
if (isFts5SyntaxError(error)) continue;
|
|
9489
|
+
throw error;
|
|
9490
|
+
}
|
|
9254
9491
|
for (const row of results.rows) suggestions.push({
|
|
9255
9492
|
collection,
|
|
9256
9493
|
id: row.id,
|
|
@@ -9396,5 +9633,5 @@ function extractSearchableFields(entry, fields) {
|
|
|
9396
9633
|
}
|
|
9397
9634
|
|
|
9398
9635
|
//#endregion
|
|
9399
|
-
export {
|
|
9400
|
-
//# sourceMappingURL=search-
|
|
9636
|
+
export { prosemirrorToPortableText as $, isStandardPluginDefinition as A, handleContentSchedule as At, EmailPipeline as B, getAllSources as C, handleContentGet as Ct, probeUrl as D, handleContentPermanentDelete as Dt, getUrlSources as E, handleContentListTrashed as Et, createPluginManager as F, validateRev as Ft, extractRequestMeta as G, createHookPipeline as H, PluginRouteError as I, portableText as It, definePlugin as J, sanitizeHeadersForSandbox as K, PluginRouteRegistry as L, reference as Lt, SandboxNotAvailableError as M, handleContentUnpublish as Mt, createNoopSandboxRunner as N, handleContentUnschedule as Nt, registerSource as O, handleContentPublish as Ot, PluginManager as P, handleContentUpdate as Pt, portableTextToProsemirror as Q, DEV_CONSOLE_EMAIL_PLUGIN_ID as R, image as Rt, clearSources as S, handleContentDuplicate as St, getSource as T, handleContentList as Tt, resolveExclusiveHooks as U, HookPipeline as V, CronExecutor as W, parseWxrString as X, parseWxr as Y, after as Z, buildPreviewUrl as _, handleContentCountScheduled as _t, search as a, PluginStateRepository as at, parseWxrDate as b, handleContentDelete as bt, getWidgetArea as c, handleMediaDelete as ct, getMenu as d, handleMediaUpdate as dt, isSafeHref as et, getMenus as f, handleRevisionGet as ft, isPreviewRequest as g, handleContentCompare as gt, getPreviewToken as h, generateManifest as ht, getSuggestions as i, getSections as it, NoopSandboxRunner as j, handleContentTranslations as jt, importReusableBlocksAsSections as k, handleContentRestore as kt, getWidgetAreas as l, handleMediaGet as lt, getComments as m, handleRevisionRestore as mt, extractSearchableFields as n, loadBundleFromR2 as nt, searchCollection as o, getCollectionInfo as ot, getCommentCount as p, handleRevisionList as pt, getTrustedProxyHeaders as q, getSearchStats as r, getSection as rt, searchWithDb as s, handleMediaCreate as st, extractPlainText as t, sanitizeHref as tt, getWidgetComponents as u, handleMediaList as ut, getPreviewUrl as v, handleContentCountTrashed as vt, getFileSources as w, handleContentGetIncludingTrashed as wt, wxrSource as x, handleContentDiscardDraft as xt, wordpressRestSource as y, handleContentCreate as yt, devConsoleEmailDeliver as z };
|
|
9637
|
+
//# sourceMappingURL=search-BoZYFuUk.mjs.map
|