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
package/src/media/normalize.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import type { MediaProvider, MediaProviderItem, MediaValue } from "./types.js";
|
|
13
13
|
|
|
14
|
-
const INTERNAL_MEDIA_PREFIX = "/_emdash/api/media/file/";
|
|
14
|
+
export const INTERNAL_MEDIA_PREFIX = "/_emdash/api/media/file/";
|
|
15
15
|
const URL_PATTERN = /^https?:\/\//;
|
|
16
16
|
|
|
17
17
|
/**
|
package/src/media/url.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public media URL resolution.
|
|
3
|
+
*
|
|
4
|
+
* Used at render time by the Image components to decide whether a storage
|
|
5
|
+
* key should be served from the configured `publicUrl` (R2 custom domain,
|
|
6
|
+
* S3 CDN) or through the internal `/_emdash/api/media/file/{key}` route.
|
|
7
|
+
*/
|
|
8
|
+
import type { Storage } from "../storage/types.js";
|
|
9
|
+
import { INTERNAL_MEDIA_PREFIX } from "./normalize.js";
|
|
10
|
+
|
|
11
|
+
// Keys accepted by the public-URL rewrite: the `{ulid}{ext}` shape produced by
|
|
12
|
+
// the upload pipeline, with letters, digits, dots, dashes, and underscores.
|
|
13
|
+
// Slashes, `?`, `#`, and `%` are rejected so attacker-controlled content in a
|
|
14
|
+
// portable-text `asset.url` cannot traverse or reroute on the CDN origin.
|
|
15
|
+
const SAFE_STORAGE_KEY = /^[A-Za-z0-9._-]+$/;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the public URL for a locally stored media key. Returns an empty
|
|
19
|
+
* string when no key is given. When a storage adapter is supplied, defers to
|
|
20
|
+
* `storage.getPublicUrl()`; otherwise returns the internal proxy route.
|
|
21
|
+
*/
|
|
22
|
+
export function resolvePublicMediaUrl(
|
|
23
|
+
storage: Storage | null | undefined,
|
|
24
|
+
storageKey: string,
|
|
25
|
+
): string {
|
|
26
|
+
if (!storageKey) return "";
|
|
27
|
+
if (storage) return storage.getPublicUrl(storageKey);
|
|
28
|
+
return `/_emdash/api/media/file/${storageKey}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the `getPublicMediaUrl` closure attached to `Astro.locals.emdash`.
|
|
33
|
+
* Shared by the anonymous fast path and the full-runtime path in middleware.
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export function createPublicMediaUrlResolver(
|
|
38
|
+
storage: Storage | null | undefined,
|
|
39
|
+
): (key: string) => string {
|
|
40
|
+
return (key) => resolvePublicMediaUrl(storage, key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Input shape for {@link buildRenderMediaUrl}. */
|
|
44
|
+
export interface RenderMediaRef {
|
|
45
|
+
/** Storage key with extension (the canonical shape from the upload pipeline). */
|
|
46
|
+
storageKey?: string;
|
|
47
|
+
/** Pre-baked URL (either an internal proxy URL or an external URL). */
|
|
48
|
+
url?: string;
|
|
49
|
+
/** Bare media id (ULID without extension); only the internal proxy can look this up. */
|
|
50
|
+
id?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build a render-time media URL. Prefers `storageKey`, then rewrites an
|
|
55
|
+
* internal `url` via `resolve`, then falls back to the internal proxy for a
|
|
56
|
+
* bare `id`. External URLs and non-matching internal-looking URLs pass
|
|
57
|
+
* through untouched. Returns `""` when nothing usable is present.
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export function buildRenderMediaUrl(
|
|
62
|
+
resolve: ((key: string) => string) | undefined,
|
|
63
|
+
ref: RenderMediaRef,
|
|
64
|
+
): string {
|
|
65
|
+
const { storageKey, url, id } = ref;
|
|
66
|
+
if (storageKey) {
|
|
67
|
+
return resolve ? resolve(storageKey) : `${INTERNAL_MEDIA_PREFIX}${storageKey}`;
|
|
68
|
+
}
|
|
69
|
+
if (url) {
|
|
70
|
+
if (resolve && url.startsWith(INTERNAL_MEDIA_PREFIX)) {
|
|
71
|
+
const key = url.slice(INTERNAL_MEDIA_PREFIX.length);
|
|
72
|
+
if (SAFE_STORAGE_KEY.test(key)) return resolve(key);
|
|
73
|
+
}
|
|
74
|
+
return url;
|
|
75
|
+
}
|
|
76
|
+
if (id) return `${INTERNAL_MEDIA_PREFIX}${id}`;
|
|
77
|
+
return "";
|
|
78
|
+
}
|
package/src/plugins/context.ts
CHANGED
|
@@ -16,7 +16,11 @@ import { SeoRepository } from "../database/repositories/seo.js";
|
|
|
16
16
|
import { UserRepository } from "../database/repositories/user.js";
|
|
17
17
|
import { withTransaction } from "../database/transaction.js";
|
|
18
18
|
import type { Database } from "../database/types.js";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
resolveAndValidateExternalUrl,
|
|
21
|
+
SsrfError,
|
|
22
|
+
stripCredentialHeaders,
|
|
23
|
+
} from "../import/ssrf.js";
|
|
20
24
|
import type { Storage } from "../storage/types.js";
|
|
21
25
|
import { CronAccessImpl } from "./cron.js";
|
|
22
26
|
import type { EmailPipeline } from "./email.js";
|
|
@@ -599,9 +603,10 @@ export function createUnrestrictedHttpAccess(pluginId: string): HttpAccess {
|
|
|
599
603
|
let currentInit = init;
|
|
600
604
|
|
|
601
605
|
for (let i = 0; i <= MAX_PLUGIN_REDIRECTS; i++) {
|
|
602
|
-
// Validate each URL against SSRF rules (private IPs, metadata
|
|
606
|
+
// Validate each URL against SSRF rules (private IPs, metadata
|
|
607
|
+
// endpoints, wildcard DNS, resolved-IP private ranges).
|
|
603
608
|
try {
|
|
604
|
-
|
|
609
|
+
await resolveAndValidateExternalUrl(currentUrl);
|
|
605
610
|
} catch (e) {
|
|
606
611
|
const msg = e instanceof SsrfError ? e.message : "SSRF validation failed";
|
|
607
612
|
throw new Error(
|
|
@@ -849,6 +854,13 @@ export interface PluginContextFactoryOptions {
|
|
|
849
854
|
* If not provided (or no provider configured), ctx.email will be undefined.
|
|
850
855
|
*/
|
|
851
856
|
emailPipeline?: EmailPipeline;
|
|
857
|
+
/**
|
|
858
|
+
* Pre-resolved list of trusted proxy header names (from the runtime
|
|
859
|
+
* `EmDashConfig.trustedProxyHeaders` or the env var). Plugin route
|
|
860
|
+
* handlers pass this to `extractRequestMeta` so plugins see the same
|
|
861
|
+
* client IP the core auth path does.
|
|
862
|
+
*/
|
|
863
|
+
trustedProxyHeaders?: string[];
|
|
852
864
|
}
|
|
853
865
|
|
|
854
866
|
/**
|
|
@@ -30,9 +30,16 @@ export interface StoredEmail {
|
|
|
30
30
|
* instances (the runtime and the route handler may load separate copies
|
|
31
31
|
* of this module, but globalThis is always the same object).
|
|
32
32
|
*/
|
|
33
|
-
const GLOBAL_KEY = "
|
|
34
|
-
const
|
|
35
|
-
|
|
33
|
+
const GLOBAL_KEY = Symbol.for("emdash:dev-emails");
|
|
34
|
+
const g = globalThis as Record<symbol, unknown>;
|
|
35
|
+
const storedEmails: StoredEmail[] = (() => {
|
|
36
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- globalThis singleton pattern (see request-context.ts)
|
|
37
|
+
const existing = g[GLOBAL_KEY] as StoredEmail[] | undefined;
|
|
38
|
+
if (existing) return existing;
|
|
39
|
+
const fresh: StoredEmail[] = [];
|
|
40
|
+
g[GLOBAL_KEY] = fresh;
|
|
41
|
+
return fresh;
|
|
42
|
+
})();
|
|
36
43
|
|
|
37
44
|
/**
|
|
38
45
|
* Get all stored dev emails (most recent first).
|
package/src/plugins/hooks.ts
CHANGED
|
@@ -1218,6 +1218,17 @@ export class HookPipeline {
|
|
|
1218
1218
|
return hooks.filter((h) => h.exclusive).map((h) => ({ pluginId: h.pluginId }));
|
|
1219
1219
|
}
|
|
1220
1220
|
|
|
1221
|
+
/**
|
|
1222
|
+
* Get all plugins that registered a non-exclusive handler for a given
|
|
1223
|
+
* hook (e.g. `email:beforeSend`, `email:afterSend`), preserving priority
|
|
1224
|
+
* order. Partitions with `getExclusiveHookProviders()`, which returns
|
|
1225
|
+
* plugins whose registration is marked `exclusive: true`.
|
|
1226
|
+
*/
|
|
1227
|
+
getHookProviders(hookName: string): Array<{ pluginId: string }> {
|
|
1228
|
+
const hooks = this.hooks.get(hookName as HookNameV2) ?? [];
|
|
1229
|
+
return hooks.filter((h) => !h.exclusive).map((h) => ({ pluginId: h.pluginId }));
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1221
1232
|
/**
|
|
1222
1233
|
* Invoke an exclusive hook — dispatch only to the selected provider.
|
|
1223
1234
|
* Returns null if no provider is selected or if the selected hook
|
package/src/plugins/manager.ts
CHANGED
|
@@ -62,6 +62,11 @@ export interface PluginManagerOptions {
|
|
|
62
62
|
filename: string,
|
|
63
63
|
contentType: string,
|
|
64
64
|
) => Promise<{ uploadUrl: string; mediaId: string }>;
|
|
65
|
+
/**
|
|
66
|
+
* Pre-resolved list of trusted proxy header names for client-IP
|
|
67
|
+
* resolution in plugin route handlers. Thread through from the runtime.
|
|
68
|
+
*/
|
|
69
|
+
trustedProxyHeaders?: string[];
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
/**
|
|
@@ -81,6 +86,7 @@ export class PluginManager {
|
|
|
81
86
|
db: options.db,
|
|
82
87
|
storage: options.storage,
|
|
83
88
|
getUploadUrl: options.getUploadUrl,
|
|
89
|
+
trustedProxyHeaders: options.trustedProxyHeaders,
|
|
84
90
|
};
|
|
85
91
|
}
|
|
86
92
|
|
|
@@ -131,6 +131,18 @@ const settingFieldSchema = z.discriminatedUnion("type", [
|
|
|
131
131
|
default: z.string().optional(),
|
|
132
132
|
}),
|
|
133
133
|
z.object({ ...baseSettingFields, type: z.literal("secret") }),
|
|
134
|
+
z.object({
|
|
135
|
+
...baseSettingFields,
|
|
136
|
+
type: z.literal("url"),
|
|
137
|
+
default: z.string().optional(),
|
|
138
|
+
placeholder: z.string().optional(),
|
|
139
|
+
}),
|
|
140
|
+
z.object({
|
|
141
|
+
...baseSettingFields,
|
|
142
|
+
type: z.literal("email"),
|
|
143
|
+
default: z.string().optional(),
|
|
144
|
+
placeholder: z.string().optional(),
|
|
145
|
+
}),
|
|
134
146
|
]);
|
|
135
147
|
|
|
136
148
|
const adminPageSchema = z.object({
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import type { EmDashConfig } from "../astro/integration/runtime.js";
|
|
11
|
+
import { getTrustedProxyHeaders, normalizeTrustedHeaders } from "../auth/trusted-proxy.js";
|
|
10
12
|
import type { GeoInfo, RequestMeta } from "./types.js";
|
|
11
13
|
|
|
12
14
|
/**
|
|
@@ -40,6 +42,23 @@ function parseFirstForwardedIp(header: string): string | null {
|
|
|
40
42
|
return IP_PATTERN.test(trimmed) ? trimmed : null;
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Read an IP from an operator-declared trusted header. XFF-style headers
|
|
47
|
+
* (any name ending in `forwarded-for`) are parsed as comma-separated lists
|
|
48
|
+
* and the first entry is used; everything else is treated as a single
|
|
49
|
+
* trimmed value.
|
|
50
|
+
*/
|
|
51
|
+
function readIpFromHeader(headers: Headers, name: string): string | null {
|
|
52
|
+
const value = headers.get(name);
|
|
53
|
+
if (!value) return null;
|
|
54
|
+
if (name.endsWith("forwarded-for")) {
|
|
55
|
+
return parseFirstForwardedIp(value);
|
|
56
|
+
}
|
|
57
|
+
const trimmed = value.trim();
|
|
58
|
+
if (!trimmed) return null;
|
|
59
|
+
return IP_PATTERN.test(trimmed) ? trimmed : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
43
62
|
/**
|
|
44
63
|
* Get the Cloudflare `cf` object from the request, if present.
|
|
45
64
|
* Returns undefined when not running on Cloudflare Workers.
|
|
@@ -69,32 +88,52 @@ function extractGeo(cf: CfProperties | undefined): GeoInfo | null {
|
|
|
69
88
|
* Extract normalized request metadata from a Request object.
|
|
70
89
|
*
|
|
71
90
|
* IP resolution order:
|
|
72
|
-
* 1. `CF-Connecting-IP`
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* 3. `
|
|
91
|
+
* 1. `CF-Connecting-IP` — trusted only when a `cf` object is present on the
|
|
92
|
+
* request. CF edge overwrites any client-supplied value, so this is the
|
|
93
|
+
* cryptographically trustworthy path on Workers. Operator-declared
|
|
94
|
+
* trusted headers cannot override it.
|
|
95
|
+
* 2. `X-Forwarded-For` first entry — trusted only with a `cf` object.
|
|
96
|
+
* 3. Operator-declared trusted proxy headers (from `config.trustedProxyHeaders`
|
|
97
|
+
* or the `EMDASH_TRUSTED_PROXY_HEADERS` env var), tried in order. Used as
|
|
98
|
+
* the primary source off-CF and as a fill-in on CF.
|
|
99
|
+
* 4. `null`
|
|
100
|
+
*
|
|
101
|
+
* The second argument accepts either the EmDash config or a pre-resolved
|
|
102
|
+
* list of trusted headers, so callers that already have the list don't have
|
|
103
|
+
* to round-trip through the config every request.
|
|
78
104
|
*/
|
|
79
|
-
export function extractRequestMeta(
|
|
105
|
+
export function extractRequestMeta(
|
|
106
|
+
request: Request,
|
|
107
|
+
configOrTrustedHeaders?: EmDashConfig | null | { trustedProxyHeaders?: string[] } | string[],
|
|
108
|
+
): RequestMeta {
|
|
80
109
|
const headers = request.headers;
|
|
81
110
|
const cf = getCfObject(request);
|
|
111
|
+
const trusted = resolveTrustedHeaders(configOrTrustedHeaders);
|
|
82
112
|
|
|
83
|
-
// IP: only trust headers when the cf object confirms we're on Cloudflare.
|
|
84
|
-
// Without a trusted reverse proxy, X-Forwarded-For is trivially spoofable.
|
|
85
113
|
let ip: string | null = null;
|
|
114
|
+
|
|
115
|
+
// On Cloudflare, prefer the cryptographically trustworthy headers first.
|
|
86
116
|
if (cf) {
|
|
87
117
|
const cfIp = headers.get("cf-connecting-ip")?.trim();
|
|
88
118
|
if (cfIp && IP_PATTERN.test(cfIp)) {
|
|
89
119
|
ip = cfIp;
|
|
90
120
|
}
|
|
121
|
+
if (!ip) {
|
|
122
|
+
const xff = headers.get("x-forwarded-for");
|
|
123
|
+
ip = xff ? parseFirstForwardedIp(xff) : null;
|
|
124
|
+
}
|
|
91
125
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
126
|
+
|
|
127
|
+
// Fall through to operator-declared trusted headers. On CF this fills
|
|
128
|
+
// in when the CF headers are absent; off-CF it's the primary source.
|
|
129
|
+
if (!ip) {
|
|
130
|
+
for (const name of trusted) {
|
|
131
|
+
const value = readIpFromHeader(headers, name);
|
|
132
|
+
if (value) {
|
|
133
|
+
ip = value;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
98
137
|
}
|
|
99
138
|
|
|
100
139
|
const userAgent = headers.get("user-agent")?.trim() || null;
|
|
@@ -104,6 +143,18 @@ export function extractRequestMeta(request: Request): RequestMeta {
|
|
|
104
143
|
return { ip, userAgent, referer, geo };
|
|
105
144
|
}
|
|
106
145
|
|
|
146
|
+
function resolveTrustedHeaders(
|
|
147
|
+
value: EmDashConfig | null | { trustedProxyHeaders?: string[] } | string[] | undefined,
|
|
148
|
+
): string[] {
|
|
149
|
+
if (Array.isArray(value)) {
|
|
150
|
+
// Apply the same RFC 7230 validation the config/env path does so a
|
|
151
|
+
// caller passing a pre-resolved list with bad entries can't crash
|
|
152
|
+
// `Headers.get()` downstream.
|
|
153
|
+
return normalizeTrustedHeaders(value);
|
|
154
|
+
}
|
|
155
|
+
return getTrustedProxyHeaders(value);
|
|
156
|
+
}
|
|
157
|
+
|
|
107
158
|
// =============================================================================
|
|
108
159
|
// Header Sanitization for Sandbox
|
|
109
160
|
// =============================================================================
|
package/src/plugins/routes.ts
CHANGED
|
@@ -50,10 +50,12 @@ export interface InvokeRouteOptions {
|
|
|
50
50
|
export class PluginRouteHandler {
|
|
51
51
|
private contextFactory: PluginContextFactory;
|
|
52
52
|
private plugin: ResolvedPlugin;
|
|
53
|
+
private trustedProxyHeaders: string[];
|
|
53
54
|
|
|
54
55
|
constructor(plugin: ResolvedPlugin, factoryOptions: PluginContextFactoryOptions) {
|
|
55
56
|
this.plugin = plugin;
|
|
56
57
|
this.contextFactory = new PluginContextFactory(factoryOptions);
|
|
58
|
+
this.trustedProxyHeaders = factoryOptions.trustedProxyHeaders ?? [];
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/**
|
|
@@ -99,7 +101,7 @@ export class PluginRouteHandler {
|
|
|
99
101
|
...baseContext,
|
|
100
102
|
input: validatedInput,
|
|
101
103
|
request: options.request,
|
|
102
|
-
requestMeta: extractRequestMeta(options.request),
|
|
104
|
+
requestMeta: extractRequestMeta(options.request, this.trustedProxyHeaders),
|
|
103
105
|
};
|
|
104
106
|
|
|
105
107
|
// Execute handler
|
package/src/plugins/types.ts
CHANGED
|
@@ -1114,7 +1114,14 @@ export interface PluginDashboardWidget {
|
|
|
1114
1114
|
/**
|
|
1115
1115
|
* Settings field types (for admin UI generation)
|
|
1116
1116
|
*/
|
|
1117
|
-
export type SettingFieldType =
|
|
1117
|
+
export type SettingFieldType =
|
|
1118
|
+
| "string"
|
|
1119
|
+
| "number"
|
|
1120
|
+
| "boolean"
|
|
1121
|
+
| "select"
|
|
1122
|
+
| "secret"
|
|
1123
|
+
| "url"
|
|
1124
|
+
| "email";
|
|
1118
1125
|
|
|
1119
1126
|
export interface BaseSettingField {
|
|
1120
1127
|
type: SettingFieldType;
|
|
@@ -1150,12 +1157,26 @@ export interface SecretSettingField extends BaseSettingField {
|
|
|
1150
1157
|
type: "secret";
|
|
1151
1158
|
}
|
|
1152
1159
|
|
|
1160
|
+
export interface UrlSettingField extends BaseSettingField {
|
|
1161
|
+
type: "url";
|
|
1162
|
+
default?: string;
|
|
1163
|
+
placeholder?: string;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
export interface EmailSettingField extends BaseSettingField {
|
|
1167
|
+
type: "email";
|
|
1168
|
+
default?: string;
|
|
1169
|
+
placeholder?: string;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1153
1172
|
export type SettingField =
|
|
1154
1173
|
| StringSettingField
|
|
1155
1174
|
| NumberSettingField
|
|
1156
1175
|
| BooleanSettingField
|
|
1157
1176
|
| SelectSettingField
|
|
1158
|
-
| SecretSettingField
|
|
1177
|
+
| SecretSettingField
|
|
1178
|
+
| UrlSettingField
|
|
1179
|
+
| EmailSettingField;
|
|
1159
1180
|
|
|
1160
1181
|
/**
|
|
1161
1182
|
* Block Kit element for block editing fields.
|
package/src/query.ts
CHANGED
|
@@ -478,7 +478,7 @@ export async function getEmDashEntry<T extends string, D = InferCollectionData<T
|
|
|
478
478
|
// Edit mode (authenticated editors) has collection-wide draft access.
|
|
479
479
|
if (isPreviewMode && !isEditMode) {
|
|
480
480
|
const dbId = entryDatabaseId(baseEntry);
|
|
481
|
-
if (preview
|
|
481
|
+
if (preview.id !== dbId && preview.id !== id) {
|
|
482
482
|
// Token doesn't match — serve only if publicly visible, without draft access
|
|
483
483
|
if (isVisible(baseEntry)) {
|
|
484
484
|
return successResult(wrapEntry(baseEntry), {
|
package/src/request-cache.ts
CHANGED
|
@@ -22,6 +22,7 @@ type CacheStore = WeakMap<EmDashRequestContext, Map<string, Promise<unknown>>>;
|
|
|
22
22
|
const STORE_KEY = Symbol.for("emdash:request-cache");
|
|
23
23
|
const g = globalThis as Record<symbol, unknown>;
|
|
24
24
|
const store: CacheStore =
|
|
25
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- globalThis singleton pattern (see request-context.ts)
|
|
25
26
|
(g[STORE_KEY] as CacheStore | undefined) ??
|
|
26
27
|
(() => {
|
|
27
28
|
const wm: CacheStore = new WeakMap();
|
|
@@ -47,6 +48,7 @@ export function requestCached<T>(key: string, fn: () => Promise<T>): Promise<T>
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
const existing = cache.get(key);
|
|
51
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- heterogeneous cache; key namespacing guarantees the stored promise resolves to T
|
|
50
52
|
if (existing) return existing as Promise<T>;
|
|
51
53
|
|
|
52
54
|
const promise = Promise.resolve()
|
|
@@ -74,6 +76,7 @@ export function peekRequestCache<T>(key: string): Promise<T> | undefined {
|
|
|
74
76
|
const ctx = getRequestContext();
|
|
75
77
|
if (!ctx) return undefined;
|
|
76
78
|
const cache = store.get(ctx);
|
|
79
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- heterogeneous cache; caller is responsible for using a T-compatible key
|
|
77
80
|
return cache?.get(key) as Promise<T> | undefined;
|
|
78
81
|
}
|
|
79
82
|
|
package/src/schema/registry.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { FTSManager } from "../search/fts-manager.js";
|
|
|
11
11
|
import {
|
|
12
12
|
type Collection,
|
|
13
13
|
type CollectionSource,
|
|
14
|
+
type CollectionSupport,
|
|
14
15
|
type ColumnType,
|
|
15
16
|
type Field,
|
|
16
17
|
type CreateCollectionInput,
|
|
@@ -49,6 +50,34 @@ function isColumnType(value: string): value is ColumnType {
|
|
|
49
50
|
return COLUMN_TYPES.has(value);
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
const VALID_COLLECTION_SUPPORTS: ReadonlySet<string> = new Set<CollectionSupport>([
|
|
54
|
+
"drafts",
|
|
55
|
+
"revisions",
|
|
56
|
+
"preview",
|
|
57
|
+
"scheduling",
|
|
58
|
+
"search",
|
|
59
|
+
"seo",
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
function isCollectionSupport(value: unknown): value is CollectionSupport {
|
|
63
|
+
return typeof value === "string" && VALID_COLLECTION_SUPPORTS.has(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse a collection's `supports` column (stored as a JSON array of
|
|
68
|
+
* CollectionSupport keys). Unknown/invalid entries are filtered out so the
|
|
69
|
+
* runtime value matches the declared `CollectionSupport[]` type.
|
|
70
|
+
*
|
|
71
|
+
* Throws on malformed JSON so corruption surfaces loudly; returns an empty
|
|
72
|
+
* array only for explicitly null/empty values or non-array JSON.
|
|
73
|
+
*/
|
|
74
|
+
function parseSupports(raw: string | null | undefined): CollectionSupport[] {
|
|
75
|
+
if (!raw) return [];
|
|
76
|
+
const parsed: unknown = JSON.parse(raw);
|
|
77
|
+
if (!Array.isArray(parsed)) return [];
|
|
78
|
+
return parsed.filter(isCollectionSupport);
|
|
79
|
+
}
|
|
80
|
+
|
|
52
81
|
/**
|
|
53
82
|
* Error thrown when a schema operation fails
|
|
54
83
|
*/
|
|
@@ -132,11 +161,18 @@ export class SchemaRegistry {
|
|
|
132
161
|
|
|
133
162
|
const id = ulid();
|
|
134
163
|
|
|
164
|
+
// Default `supports` to drafts + revisions when the caller didn't
|
|
165
|
+
// specify it. Explicit empty array (`[]`) is preserved as an opt-out
|
|
166
|
+
// — only `undefined` triggers the default. This is the canonical
|
|
167
|
+
// default for new collections; the MCP and admin UI layers used to
|
|
168
|
+
// duplicate this default but now defer to the registry.
|
|
169
|
+
const supports = input.supports ?? ["drafts", "revisions"];
|
|
170
|
+
|
|
135
171
|
// Insert collection record and create content table in a transaction
|
|
136
172
|
// so a failure in table creation doesn't leave an orphaned row.
|
|
137
173
|
// Uses withTransaction for D1 compatibility (no transaction support).
|
|
138
174
|
// Derive hasSeo from supports array if not explicitly set
|
|
139
|
-
const hasSeo = input.hasSeo ??
|
|
175
|
+
const hasSeo = input.hasSeo ?? supports.includes("seo") ?? false;
|
|
140
176
|
|
|
141
177
|
await withTransaction(this.db, async (trx) => {
|
|
142
178
|
await trx
|
|
@@ -148,7 +184,7 @@ export class SchemaRegistry {
|
|
|
148
184
|
label_singular: input.labelSingular ?? null,
|
|
149
185
|
description: input.description ?? null,
|
|
150
186
|
icon: input.icon ?? null,
|
|
151
|
-
supports:
|
|
187
|
+
supports: JSON.stringify(supports),
|
|
152
188
|
source: input.source ?? "manual",
|
|
153
189
|
has_seo: hasSeo ? 1 : 0,
|
|
154
190
|
comments_enabled: input.commentsEnabled ? 1 : 0,
|
|
@@ -243,7 +279,7 @@ export class SchemaRegistry {
|
|
|
243
279
|
// Sync FTS state when the supports array changes (e.g. search toggled on/off)
|
|
244
280
|
if (input.supports !== undefined) {
|
|
245
281
|
const hadSearch = existing.supports.includes("search");
|
|
246
|
-
const hasSearch = (
|
|
282
|
+
const hasSearch = parseSupports(row.supports).includes("search");
|
|
247
283
|
if (hadSearch !== hasSearch) {
|
|
248
284
|
await this.syncSearchState(slug, trx);
|
|
249
285
|
}
|
|
@@ -525,7 +561,7 @@ export class SchemaRegistry {
|
|
|
525
561
|
.executeTakeFirst();
|
|
526
562
|
if (!row) return;
|
|
527
563
|
|
|
528
|
-
const wantsSearch = (
|
|
564
|
+
const wantsSearch = parseSupports(row.supports).includes("search");
|
|
529
565
|
const searchableFields = await ftsManager.getSearchableFields(collectionSlug);
|
|
530
566
|
const config = await ftsManager.getSearchConfig(collectionSlug);
|
|
531
567
|
const ftsActive = config?.enabled === true;
|
|
@@ -881,7 +917,7 @@ export class SchemaRegistry {
|
|
|
881
917
|
labelSingular: row.label_singular ?? undefined,
|
|
882
918
|
description: row.description ?? undefined,
|
|
883
919
|
icon: row.icon ?? undefined,
|
|
884
|
-
supports:
|
|
920
|
+
supports: parseSupports(row.supports),
|
|
885
921
|
source: row.source && isCollectionSource(row.source) ? row.source : undefined,
|
|
886
922
|
hasSeo: row.has_seo === 1,
|
|
887
923
|
urlPattern: row.url_pattern ?? undefined,
|
|
@@ -418,8 +418,6 @@ export class FTSManager {
|
|
|
418
418
|
console.warn(
|
|
419
419
|
`FTS index for "${collectionSlug}" has ${ftsRows} rows but content table has ${contentRows}. Rebuilding.`,
|
|
420
420
|
);
|
|
421
|
-
const fields = await this.getSearchableFields(collectionSlug);
|
|
422
|
-
const config = await this.getSearchConfig(collectionSlug);
|
|
423
421
|
if (fields.length > 0) {
|
|
424
422
|
await this.rebuildIndex(collectionSlug, fields, config?.weights);
|
|
425
423
|
}
|