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/emdash-runtime.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
} from "./astro/integration/runtime.js";
|
|
20
20
|
import type { EmDashManifest, ManifestCollection } from "./astro/types.js";
|
|
21
21
|
import { getAuthMode } from "./auth/mode.js";
|
|
22
|
+
import { getTrustedProxyHeaders } from "./auth/trusted-proxy.js";
|
|
22
23
|
import { isSqlite } from "./database/dialect-helpers.js";
|
|
23
24
|
import { kyselyLogOption } from "./database/instrumentation.js";
|
|
24
25
|
import { runMigrations } from "./database/migrations/runner.js";
|
|
@@ -45,6 +46,20 @@ import { COMMIT, VERSION } from "./version.js";
|
|
|
45
46
|
|
|
46
47
|
const LEADING_SLASH_PATTERN = /^\//;
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Parse a JSON column expected to contain an array of strings.
|
|
51
|
+
*
|
|
52
|
+
* Throws on malformed JSON rather than returning []; callers are responsible
|
|
53
|
+
* for deciding how to handle/log the error. Empty string / null inputs return
|
|
54
|
+
* [] (they represent "no value"). Non-string array entries are filtered out.
|
|
55
|
+
*/
|
|
56
|
+
function parseStringArray(raw: string | null | undefined): string[] {
|
|
57
|
+
if (!raw) return [];
|
|
58
|
+
const parsed: unknown = JSON.parse(raw);
|
|
59
|
+
if (!Array.isArray(parsed)) return [];
|
|
60
|
+
return parsed.filter((v): v is string => typeof v === "string");
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
/** Combined result from a single-pass page contribution collection */
|
|
49
64
|
interface PageContributions {
|
|
50
65
|
metadata: PageMetadataContribution[];
|
|
@@ -236,6 +251,45 @@ export interface RuntimeDependencies {
|
|
|
236
251
|
createSandboxRunner: ((opts: { db: Kysely<Database> }) => SandboxRunner) | null;
|
|
237
252
|
}
|
|
238
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Constructor parameters for `EmDashRuntime`.
|
|
256
|
+
*
|
|
257
|
+
* Production code should use `EmDashRuntime.create()` which discovers and
|
|
258
|
+
* loads all parts (database, plugins, hooks, cron, etc.) and then calls the
|
|
259
|
+
* constructor. Direct construction is supported for callers that already
|
|
260
|
+
* have all the dependencies in hand — for example, integration tests that
|
|
261
|
+
* supply a pre-migrated database and an empty plugin set.
|
|
262
|
+
*
|
|
263
|
+
* Every field corresponds 1:1 to internal state set on the runtime — none of
|
|
264
|
+
* these are derived. If you don't have a value for one, see what `create()`
|
|
265
|
+
* passes for that field as the canonical default.
|
|
266
|
+
*/
|
|
267
|
+
export interface EmDashRuntimeParts {
|
|
268
|
+
db: Kysely<Database>;
|
|
269
|
+
storage: Storage | null;
|
|
270
|
+
configuredPlugins: ResolvedPlugin[];
|
|
271
|
+
sandboxedPlugins: Map<string, SandboxedPlugin>;
|
|
272
|
+
sandboxedPluginEntries: SandboxedPluginEntry[];
|
|
273
|
+
hooks: HookPipeline;
|
|
274
|
+
enabledPlugins: Set<string>;
|
|
275
|
+
pluginStates: Map<string, string>;
|
|
276
|
+
config: EmDashConfig;
|
|
277
|
+
mediaProviders: Map<string, MediaProvider>;
|
|
278
|
+
mediaProviderEntries: MediaProviderEntry[];
|
|
279
|
+
cronExecutor: CronExecutor | null;
|
|
280
|
+
cronScheduler: CronScheduler | null;
|
|
281
|
+
emailPipeline: EmailPipeline | null;
|
|
282
|
+
allPipelinePlugins: ResolvedPlugin[];
|
|
283
|
+
pipelineFactoryOptions: {
|
|
284
|
+
db: Kysely<Database>;
|
|
285
|
+
storage?: Storage;
|
|
286
|
+
siteInfo?: { siteName?: string; siteUrl?: string; locale?: string };
|
|
287
|
+
};
|
|
288
|
+
runtimeDeps: RuntimeDependencies;
|
|
289
|
+
pipelineRef: { current: HookPipeline };
|
|
290
|
+
manifestCacheKey: string;
|
|
291
|
+
}
|
|
292
|
+
|
|
239
293
|
/**
|
|
240
294
|
* Convert a ContentItem to Record<string, unknown> for hook consumption.
|
|
241
295
|
* Hooks receive the full item as a flat record.
|
|
@@ -336,51 +390,27 @@ export class EmDashRuntime {
|
|
|
336
390
|
return this._db;
|
|
337
391
|
}
|
|
338
392
|
|
|
339
|
-
|
|
340
|
-
db
|
|
341
|
-
storage
|
|
342
|
-
configuredPlugins
|
|
343
|
-
sandboxedPlugins
|
|
344
|
-
sandboxedPluginEntries
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
runtimeDeps: RuntimeDependencies,
|
|
361
|
-
pipelineRef: { current: HookPipeline },
|
|
362
|
-
manifestCacheKey: string,
|
|
363
|
-
) {
|
|
364
|
-
this._db = db;
|
|
365
|
-
this.storage = storage;
|
|
366
|
-
this.configuredPlugins = configuredPlugins;
|
|
367
|
-
this.sandboxedPlugins = sandboxedPlugins;
|
|
368
|
-
this.sandboxedPluginEntries = sandboxedPluginEntries;
|
|
369
|
-
this.schemaRegistry = new SchemaRegistry(db);
|
|
370
|
-
this._hooks = hooks;
|
|
371
|
-
this.enabledPlugins = enabledPlugins;
|
|
372
|
-
this.pluginStates = pluginStates;
|
|
373
|
-
this.config = config;
|
|
374
|
-
this.mediaProviders = mediaProviders;
|
|
375
|
-
this.mediaProviderEntries = mediaProviderEntries;
|
|
376
|
-
this.cronExecutor = cronExecutor;
|
|
377
|
-
this.cronScheduler = cronScheduler;
|
|
378
|
-
this.email = emailPipeline;
|
|
379
|
-
this.allPipelinePlugins = allPipelinePlugins;
|
|
380
|
-
this.pipelineFactoryOptions = pipelineFactoryOptions;
|
|
381
|
-
this.runtimeDeps = runtimeDeps;
|
|
382
|
-
this.pipelineRef = pipelineRef;
|
|
383
|
-
this._manifestCacheKey = manifestCacheKey;
|
|
393
|
+
constructor(parts: EmDashRuntimeParts) {
|
|
394
|
+
this._db = parts.db;
|
|
395
|
+
this.storage = parts.storage;
|
|
396
|
+
this.configuredPlugins = parts.configuredPlugins;
|
|
397
|
+
this.sandboxedPlugins = parts.sandboxedPlugins;
|
|
398
|
+
this.sandboxedPluginEntries = parts.sandboxedPluginEntries;
|
|
399
|
+
this.schemaRegistry = new SchemaRegistry(parts.db);
|
|
400
|
+
this._hooks = parts.hooks;
|
|
401
|
+
this.enabledPlugins = parts.enabledPlugins;
|
|
402
|
+
this.pluginStates = parts.pluginStates;
|
|
403
|
+
this.config = parts.config;
|
|
404
|
+
this.mediaProviders = parts.mediaProviders;
|
|
405
|
+
this.mediaProviderEntries = parts.mediaProviderEntries;
|
|
406
|
+
this.cronExecutor = parts.cronExecutor;
|
|
407
|
+
this.cronScheduler = parts.cronScheduler;
|
|
408
|
+
this.email = parts.emailPipeline;
|
|
409
|
+
this.allPipelinePlugins = parts.allPipelinePlugins;
|
|
410
|
+
this.pipelineFactoryOptions = parts.pipelineFactoryOptions;
|
|
411
|
+
this.runtimeDeps = parts.runtimeDeps;
|
|
412
|
+
this.pipelineRef = parts.pipelineRef;
|
|
413
|
+
this._manifestCacheKey = parts.manifestCacheKey;
|
|
384
414
|
}
|
|
385
415
|
|
|
386
416
|
/**
|
|
@@ -857,16 +887,16 @@ export class EmDashRuntime {
|
|
|
857
887
|
].join("|"),
|
|
858
888
|
);
|
|
859
889
|
|
|
860
|
-
return new EmDashRuntime(
|
|
890
|
+
return new EmDashRuntime({
|
|
861
891
|
db,
|
|
862
892
|
storage,
|
|
863
|
-
deps.plugins,
|
|
893
|
+
configuredPlugins: deps.plugins,
|
|
864
894
|
sandboxedPlugins,
|
|
865
|
-
deps.sandboxedPluginEntries,
|
|
866
|
-
pipeline,
|
|
895
|
+
sandboxedPluginEntries: deps.sandboxedPluginEntries,
|
|
896
|
+
hooks: pipeline,
|
|
867
897
|
enabledPlugins,
|
|
868
898
|
pluginStates,
|
|
869
|
-
deps.config,
|
|
899
|
+
config: deps.config,
|
|
870
900
|
mediaProviders,
|
|
871
901
|
mediaProviderEntries,
|
|
872
902
|
cronExecutor,
|
|
@@ -874,10 +904,10 @@ export class EmDashRuntime {
|
|
|
874
904
|
emailPipeline,
|
|
875
905
|
allPipelinePlugins,
|
|
876
906
|
pipelineFactoryOptions,
|
|
877
|
-
deps,
|
|
907
|
+
runtimeDeps: deps,
|
|
878
908
|
pipelineRef,
|
|
879
909
|
manifestCacheKey,
|
|
880
|
-
);
|
|
910
|
+
});
|
|
881
911
|
}
|
|
882
912
|
|
|
883
913
|
/**
|
|
@@ -1488,7 +1518,7 @@ export class EmDashRuntime {
|
|
|
1488
1518
|
label: row.label,
|
|
1489
1519
|
labelSingular: row.label_singular ?? undefined,
|
|
1490
1520
|
hierarchical: row.hierarchical === 1,
|
|
1491
|
-
collections:
|
|
1521
|
+
collections: parseStringArray(row.collections).toSorted(),
|
|
1492
1522
|
}));
|
|
1493
1523
|
} catch (error) {
|
|
1494
1524
|
console.debug("EmDash: Could not load taxonomy definitions:", error);
|
|
@@ -1614,11 +1644,75 @@ export class EmDashRuntime {
|
|
|
1614
1644
|
}
|
|
1615
1645
|
|
|
1616
1646
|
async handleContentGet(collection: string, id: string, locale?: string) {
|
|
1617
|
-
|
|
1647
|
+
const result = await handleContentGet(this.db, collection, id, locale);
|
|
1648
|
+
return this.hydrateDraftData(result);
|
|
1618
1649
|
}
|
|
1619
1650
|
|
|
1620
1651
|
async handleContentGetIncludingTrashed(collection: string, id: string, locale?: string) {
|
|
1621
|
-
|
|
1652
|
+
const result = await handleContentGetIncludingTrashed(this.db, collection, id, locale);
|
|
1653
|
+
return this.hydrateDraftData(result);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* If the response item has a `draftRevisionId`, replace `item.data` with
|
|
1658
|
+
* the draft revision's data and expose the original published values as
|
|
1659
|
+
* `liveData`. This makes the content_get / content_update round-trip
|
|
1660
|
+
* intuitive — read returns the latest content the caller has saved
|
|
1661
|
+
* (their pending draft), with the previously-published values still
|
|
1662
|
+
* accessible for compare-style flows.
|
|
1663
|
+
*
|
|
1664
|
+
* No-op when no draft exists or the response is an error.
|
|
1665
|
+
*/
|
|
1666
|
+
private async hydrateDraftData<T>(result: T): Promise<T> {
|
|
1667
|
+
if (!result || typeof result !== "object") return result;
|
|
1668
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- shape probed below
|
|
1669
|
+
const r = result as {
|
|
1670
|
+
success?: boolean;
|
|
1671
|
+
data?: { item?: Record<string, unknown> };
|
|
1672
|
+
};
|
|
1673
|
+
if (!r.success || !r.data?.item) return result;
|
|
1674
|
+
const item = r.data.item;
|
|
1675
|
+
const draftRevisionId = typeof item.draftRevisionId === "string" ? item.draftRevisionId : null;
|
|
1676
|
+
if (!draftRevisionId) return result;
|
|
1677
|
+
try {
|
|
1678
|
+
const revision = await new RevisionRepository(this.db).findById(draftRevisionId);
|
|
1679
|
+
if (!revision) return result;
|
|
1680
|
+
const liveData =
|
|
1681
|
+
item.data && typeof item.data === "object"
|
|
1682
|
+
? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed to object above
|
|
1683
|
+
(item.data as Record<string, unknown>)
|
|
1684
|
+
: {};
|
|
1685
|
+
// Strip leading-underscore keys (`_slug`, `_rev`, etc.) from the
|
|
1686
|
+
// revision data — those are handler-internal markers and don't
|
|
1687
|
+
// belong in the surfaced `data` field. Match syncDataColumns at
|
|
1688
|
+
// content.ts:~1119.
|
|
1689
|
+
const revisionData: Record<string, unknown> = {};
|
|
1690
|
+
for (const [key, value] of Object.entries(revision.data)) {
|
|
1691
|
+
if (!key.startsWith("_")) revisionData[key] = value;
|
|
1692
|
+
}
|
|
1693
|
+
const mergedData = { ...liveData, ...revisionData };
|
|
1694
|
+
// Return a clone rather than mutating in place. The response
|
|
1695
|
+
// object isn't retained by the runtime today, but a future
|
|
1696
|
+
// request-cache layer would observe stale-after-mutation bugs;
|
|
1697
|
+
// cloning closes that footgun.
|
|
1698
|
+
// `r.data` was narrowed to `{ item?: ... }` at the top of this
|
|
1699
|
+
// method; spread its other keys (e.g. `_rev`) alongside the
|
|
1700
|
+
// hydrated item without going back through `unknown`.
|
|
1701
|
+
return {
|
|
1702
|
+
...result,
|
|
1703
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- shape preserved; result has been narrowed to the {success,data:{item}} envelope
|
|
1704
|
+
data: {
|
|
1705
|
+
...r.data,
|
|
1706
|
+
item: { ...item, data: mergedData, liveData },
|
|
1707
|
+
},
|
|
1708
|
+
} as T;
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
// Non-fatal — fall back to the unhydrated response. Log so the
|
|
1711
|
+
// failure isn't completely silent (the response will look stale
|
|
1712
|
+
// to the caller but no error is raised).
|
|
1713
|
+
console.error("[emdash] draft hydration failed:", error);
|
|
1714
|
+
return result;
|
|
1715
|
+
}
|
|
1622
1716
|
}
|
|
1623
1717
|
|
|
1624
1718
|
async handleContentCreate(
|
|
@@ -1646,6 +1740,20 @@ export class EmDashRuntime {
|
|
|
1646
1740
|
// Normalize media fields (fill dimensions, storageKey, etc.)
|
|
1647
1741
|
processedData = await this.normalizeMediaFields(collection, processedData);
|
|
1648
1742
|
|
|
1743
|
+
// Validate against the collection schema. Hook output is validated
|
|
1744
|
+
// rather than `body.data` so plugins that mutate field values can't
|
|
1745
|
+
// sneak invalid data past.
|
|
1746
|
+
const { validateContentData } = await import("./api/handlers/validation.js");
|
|
1747
|
+
const validation = await validateContentData(this.db, collection, processedData, {
|
|
1748
|
+
partial: false,
|
|
1749
|
+
});
|
|
1750
|
+
if (!validation.ok) {
|
|
1751
|
+
return {
|
|
1752
|
+
success: false as const,
|
|
1753
|
+
error: validation.error,
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1649
1757
|
// Create the content
|
|
1650
1758
|
const result = await handleContentCreate(this.db, collection, {
|
|
1651
1759
|
...body,
|
|
@@ -1719,6 +1827,19 @@ export class EmDashRuntime {
|
|
|
1719
1827
|
|
|
1720
1828
|
// Normalize media fields (fill dimensions, storageKey, etc.)
|
|
1721
1829
|
processedData = await this.normalizeMediaFields(collection, processedData);
|
|
1830
|
+
|
|
1831
|
+
// Validate field-level shape BEFORE the draft-revision write so
|
|
1832
|
+
// invalid updates can't silently land in revision history.
|
|
1833
|
+
const { validateContentData } = await import("./api/handlers/validation.js");
|
|
1834
|
+
const validation = await validateContentData(this.db, collection, processedData, {
|
|
1835
|
+
partial: true,
|
|
1836
|
+
});
|
|
1837
|
+
if (!validation.ok) {
|
|
1838
|
+
return {
|
|
1839
|
+
success: false as const,
|
|
1840
|
+
error: validation.error,
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1722
1843
|
}
|
|
1723
1844
|
|
|
1724
1845
|
// Draft-aware revision handling (if collection supports revisions)
|
|
@@ -1794,12 +1915,18 @@ export class EmDashRuntime {
|
|
|
1794
1915
|
bylines: bodyWithoutRev.bylines,
|
|
1795
1916
|
});
|
|
1796
1917
|
|
|
1918
|
+
// Hydrate draft data BEFORE firing afterSave hooks so the hook sees
|
|
1919
|
+
// the same effective data the response surfaces — for revision-
|
|
1920
|
+
// supporting collections, that's the just-saved draft, not the live
|
|
1921
|
+
// columns.
|
|
1922
|
+
const hydrated = await this.hydrateDraftData(result);
|
|
1923
|
+
|
|
1797
1924
|
// Run afterSave hooks (fire-and-forget)
|
|
1798
|
-
if (
|
|
1799
|
-
this.runAfterSaveHooks(contentItemToRecord(
|
|
1925
|
+
if (hydrated.success && hydrated.data) {
|
|
1926
|
+
this.runAfterSaveHooks(contentItemToRecord(hydrated.data.item), collection, false);
|
|
1800
1927
|
}
|
|
1801
1928
|
|
|
1802
|
-
return
|
|
1929
|
+
return hydrated;
|
|
1803
1930
|
}
|
|
1804
1931
|
|
|
1805
1932
|
async handleContentDelete(collection: string, id: string) {
|
|
@@ -1946,6 +2073,7 @@ export class EmDashRuntime {
|
|
|
1946
2073
|
contentHash?: string;
|
|
1947
2074
|
blurhash?: string;
|
|
1948
2075
|
dominantColor?: string;
|
|
2076
|
+
authorId?: string;
|
|
1949
2077
|
}) {
|
|
1950
2078
|
// Run beforeUpload hooks
|
|
1951
2079
|
let processedInput = input;
|
|
@@ -2009,7 +2137,74 @@ export class EmDashRuntime {
|
|
|
2009
2137
|
}
|
|
2010
2138
|
|
|
2011
2139
|
async handleRevisionRestore(revisionId: string, callerUserId: string) {
|
|
2012
|
-
|
|
2140
|
+
// Discover the parent entry up front so we can branch on whether
|
|
2141
|
+
// the collection uses draft revisions.
|
|
2142
|
+
const revisionRepo = new RevisionRepository(this.db);
|
|
2143
|
+
const revision = await revisionRepo.findById(revisionId);
|
|
2144
|
+
if (!revision) {
|
|
2145
|
+
return {
|
|
2146
|
+
success: false as const,
|
|
2147
|
+
error: {
|
|
2148
|
+
code: "NOT_FOUND",
|
|
2149
|
+
message: `Revision not found: ${revisionId}`,
|
|
2150
|
+
},
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
const collectionInfo = await this.schemaRegistry.getCollectionWithFields(revision.collection);
|
|
2155
|
+
const usesDraftRevisions = collectionInfo?.supports?.includes("revisions") ?? false;
|
|
2156
|
+
|
|
2157
|
+
// Non-revision collections: keep the legacy behavior of writing the
|
|
2158
|
+
// revision's data straight onto the live row. This preserves
|
|
2159
|
+
// behavior for collections that opt out of the draft model.
|
|
2160
|
+
if (!usesDraftRevisions) {
|
|
2161
|
+
const result = await handleRevisionRestore(this.db, revisionId, callerUserId);
|
|
2162
|
+
return this.hydrateDraftData(result);
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
// Revision-capable collections: restore is "make this revision the
|
|
2166
|
+
// current draft". The live row's data columns are left untouched
|
|
2167
|
+
// (only `draft_revision_id` and `updated_at` change). The caller
|
|
2168
|
+
// must then `content_publish` to promote the restored draft to
|
|
2169
|
+
// live, matching the documented tool contract.
|
|
2170
|
+
try {
|
|
2171
|
+
const newDraft = await revisionRepo.create({
|
|
2172
|
+
collection: revision.collection,
|
|
2173
|
+
entryId: revision.entryId,
|
|
2174
|
+
data: revision.data,
|
|
2175
|
+
authorId: callerUserId,
|
|
2176
|
+
});
|
|
2177
|
+
|
|
2178
|
+
validateIdentifier(revision.collection, "collection");
|
|
2179
|
+
const tableName = `ec_${revision.collection}`;
|
|
2180
|
+
await sql`
|
|
2181
|
+
UPDATE ${sql.ref(tableName)}
|
|
2182
|
+
SET draft_revision_id = ${newDraft.id},
|
|
2183
|
+
updated_at = ${new Date().toISOString()}
|
|
2184
|
+
WHERE id = ${revision.entryId}
|
|
2185
|
+
`.execute(this.db);
|
|
2186
|
+
|
|
2187
|
+
// Fire-and-forget: prune old revisions to prevent unbounded growth
|
|
2188
|
+
void revisionRepo
|
|
2189
|
+
.pruneOldRevisions(revision.collection, revision.entryId, 50)
|
|
2190
|
+
.catch(() => {});
|
|
2191
|
+
|
|
2192
|
+
// Return the freshly-fetched item with the new draft hydrated
|
|
2193
|
+
// onto `data`. Without this the response would echo the live
|
|
2194
|
+
// columns and the next `content_get` would surface different
|
|
2195
|
+
// values (the bug that motivated this rewrite).
|
|
2196
|
+
const refetched = await handleContentGet(this.db, revision.collection, revision.entryId);
|
|
2197
|
+
return this.hydrateDraftData(refetched);
|
|
2198
|
+
} catch (error) {
|
|
2199
|
+
console.error("[emdash] revision restore failed:", error);
|
|
2200
|
+
return {
|
|
2201
|
+
success: false as const,
|
|
2202
|
+
error: {
|
|
2203
|
+
code: "REVISION_RESTORE_ERROR",
|
|
2204
|
+
message: "Failed to restore revision",
|
|
2205
|
+
},
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2013
2208
|
}
|
|
2014
2209
|
|
|
2015
2210
|
// =========================================================================
|
|
@@ -2080,6 +2275,7 @@ export class EmDashRuntime {
|
|
|
2080
2275
|
const routeRegistry = new PluginRouteRegistry({
|
|
2081
2276
|
db: this.db,
|
|
2082
2277
|
emailPipeline: this.email ?? undefined,
|
|
2278
|
+
trustedProxyHeaders: getTrustedProxyHeaders(this.config),
|
|
2083
2279
|
});
|
|
2084
2280
|
routeRegistry.register(trustedPlugin);
|
|
2085
2281
|
|
|
@@ -2220,22 +2416,34 @@ export class EmDashRuntime {
|
|
|
2220
2416
|
collection: string,
|
|
2221
2417
|
isNew: boolean,
|
|
2222
2418
|
): void {
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
this.hooks
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
const [id] = pluginKey.split(":");
|
|
2233
|
-
if (!id || !this.isPluginEnabled(id)) continue;
|
|
2419
|
+
after(async () => {
|
|
2420
|
+
// Trusted plugins
|
|
2421
|
+
if (this.hooks.hasHooks("content:afterSave")) {
|
|
2422
|
+
try {
|
|
2423
|
+
await this.hooks.runContentAfterSave(content, collection, isNew);
|
|
2424
|
+
} catch (err) {
|
|
2425
|
+
console.error("EmDash afterSave hook error:", err);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2234
2428
|
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2429
|
+
// Sandboxed plugins
|
|
2430
|
+
const tasks: Promise<void>[] = [];
|
|
2431
|
+
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
2432
|
+
const [id] = pluginKey.split(":");
|
|
2433
|
+
if (!id || !this.isPluginEnabled(id)) continue;
|
|
2434
|
+
|
|
2435
|
+
tasks.push(
|
|
2436
|
+
(async () => {
|
|
2437
|
+
try {
|
|
2438
|
+
await plugin.invokeHook("content:afterSave", { content, collection, isNew });
|
|
2439
|
+
} catch (err) {
|
|
2440
|
+
console.error(`EmDash: Sandboxed plugin ${id} afterSave error:`, err);
|
|
2441
|
+
}
|
|
2442
|
+
})(),
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
await Promise.allSettled(tasks);
|
|
2446
|
+
});
|
|
2239
2447
|
}
|
|
2240
2448
|
|
|
2241
2449
|
private runAfterDeleteHooks(id: string, collection: string, permanent: boolean): void {
|
|
@@ -2260,24 +2468,34 @@ export class EmDashRuntime {
|
|
|
2260
2468
|
}
|
|
2261
2469
|
|
|
2262
2470
|
private runAfterPublishHooks(content: Record<string, unknown>, collection: string): void {
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
this.hooks
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
const [pluginId] = pluginKey.split(":");
|
|
2273
|
-
if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
|
|
2471
|
+
after(async () => {
|
|
2472
|
+
// Trusted plugins
|
|
2473
|
+
if (this.hooks.hasHooks("content:afterPublish")) {
|
|
2474
|
+
try {
|
|
2475
|
+
await this.hooks.runContentAfterPublish(content, collection);
|
|
2476
|
+
} catch (err) {
|
|
2477
|
+
console.error("EmDash afterPublish hook error:", err);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2274
2480
|
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2481
|
+
// Sandboxed plugins
|
|
2482
|
+
const tasks: Promise<void>[] = [];
|
|
2483
|
+
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
2484
|
+
const [pluginId] = pluginKey.split(":");
|
|
2485
|
+
if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
|
|
2486
|
+
|
|
2487
|
+
tasks.push(
|
|
2488
|
+
(async () => {
|
|
2489
|
+
try {
|
|
2490
|
+
await plugin.invokeHook("content:afterPublish", { content, collection });
|
|
2491
|
+
} catch (err) {
|
|
2492
|
+
console.error(`EmDash: Sandboxed plugin ${pluginId} afterPublish error:`, err);
|
|
2493
|
+
}
|
|
2494
|
+
})(),
|
|
2279
2495
|
);
|
|
2280
|
-
|
|
2496
|
+
}
|
|
2497
|
+
await Promise.allSettled(tasks);
|
|
2498
|
+
});
|
|
2281
2499
|
}
|
|
2282
2500
|
|
|
2283
2501
|
private runAfterUnpublishHooks(content: Record<string, unknown>, collection: string): void {
|
|
@@ -2321,7 +2539,7 @@ export class EmDashRuntime {
|
|
|
2321
2539
|
|
|
2322
2540
|
try {
|
|
2323
2541
|
const headers = sanitizeHeadersForSandbox(request.headers);
|
|
2324
|
-
const meta = extractRequestMeta(request);
|
|
2542
|
+
const meta = extractRequestMeta(request, this.config);
|
|
2325
2543
|
const result = await plugin.invokeRoute(routeName, body, {
|
|
2326
2544
|
url: request.url,
|
|
2327
2545
|
method: request.method,
|
package/src/import/registry.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Manages available import sources and provides URL probing.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { resolveAndValidateExternalUrl } from "./ssrf.js";
|
|
8
8
|
import type { ImportSource, ProbeResult, SourceProbeResult } from "./types.js";
|
|
9
9
|
|
|
10
10
|
// Regex pattern for URL normalization
|
|
@@ -63,8 +63,9 @@ export async function probeUrl(url: string): Promise<ProbeResult> {
|
|
|
63
63
|
// Remove trailing slash for consistency
|
|
64
64
|
normalizedUrl = normalizedUrl.replace(TRAILING_SLASHES_PATTERN, "");
|
|
65
65
|
|
|
66
|
-
// SSRF: reject internal/private network targets
|
|
67
|
-
|
|
66
|
+
// SSRF: reject internal/private network targets. DNS resolution
|
|
67
|
+
// catches hostnames that resolve to private addresses.
|
|
68
|
+
await resolveAndValidateExternalUrl(normalizedUrl);
|
|
68
69
|
|
|
69
70
|
const results: SourceProbeResult[] = [];
|
|
70
71
|
const urlSources = getUrlSources();
|