emdash 0.5.0 → 0.7.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-C2BzVy0p.d.mts → adapters-Di31kZ28.d.mts} +16 -1
- package/dist/adapters-Di31kZ28.d.mts.map +1 -0
- package/dist/{apply-Cma_PiF6.mjs → apply-5uslYdUu.mjs} +197 -25
- package/dist/apply-5uslYdUu.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 +203 -33
- 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 +30 -4
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +11 -4
- 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 +467 -186
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +17 -9
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-WuOq9MFJ.mjs → byline-C4OVd8b3.mjs} +3 -19
- package/dist/byline-C4OVd8b3.mjs.map +1 -0
- package/dist/{bylines-C_Wsnz4L.mjs → bylines-hPTW79hw.mjs} +20 -33
- package/dist/bylines-hPTW79hw.mjs.map +1 -0
- package/dist/{cache-E3Dts-yT.mjs → cache-BkKBuIvS.mjs} +1 -1
- package/dist/{cache-E3Dts-yT.mjs.map → cache-BkKBuIvS.mjs.map} +1 -1
- package/dist/chunks-HGz06Soa.mjs +19 -0
- package/dist/chunks-HGz06Soa.mjs.map +1 -0
- package/dist/cli/index.mjs +12 -11
- 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/{config-DkxPrM9l.mjs → config-BXwuX8Bx.mjs} +1 -1
- package/dist/{config-DkxPrM9l.mjs.map → config-BXwuX8Bx.mjs.map} +1 -1
- package/dist/{connection-B4zVnQIa.mjs → connection-2igzM-AT.mjs} +19 -2
- package/dist/connection-2igzM-AT.mjs.map +1 -0
- package/dist/{content-BsBoyj8G.mjs → content-D7J5y73J.mjs} +27 -1
- package/dist/{content-BsBoyj8G.mjs.map → content-D7J5y73J.mjs.map} +1 -1
- package/dist/database/instrumentation.d.mts +45 -0
- package/dist/database/instrumentation.d.mts.map +1 -0
- package/dist/database/instrumentation.mjs +61 -0
- package/dist/database/instrumentation.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/index.mjs.map +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 +41 -0
- package/dist/db-errors-D0UT85nC.mjs.map +1 -0
- package/dist/{default-PUx9RK6u.mjs → default-CME5YdZ3.mjs} +1 -1
- package/dist/{default-PUx9RK6u.mjs.map → default-CME5YdZ3.mjs.map} +1 -1
- package/dist/{error-HBeQbVhV.mjs → error-CiYn9yDu.mjs} +1 -1
- package/dist/{error-HBeQbVhV.mjs.map → error-CiYn9yDu.mjs.map} +1 -1
- package/dist/{index-CCWzlriB.d.mts → index-De6_Xv3v.d.mts} +209 -19
- package/dist/index-De6_Xv3v.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +23 -21
- package/dist/{load-BhSSm-TS.mjs → load-CBcmDIot.mjs} +1 -1
- package/dist/{load-BhSSm-TS.mjs.map → load-CBcmDIot.mjs.map} +1 -1
- package/dist/{loader-BYzwzORf.mjs → loader-DeiBJEMe.mjs} +18 -12
- package/dist/loader-DeiBJEMe.mjs.map +1 -0
- package/dist/{manifest-schema-BsXINkQD.mjs → manifest-schema-V30qsMft.mjs} +1 -1
- package/dist/{manifest-schema-BsXINkQD.mjs.map → manifest-schema-V30qsMft.mjs.map} +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/{mode-CyPLdO3C.mjs → mode-CpNnGkPz.mjs} +1 -1
- package/dist/{mode-CyPLdO3C.mjs.map → mode-CpNnGkPz.mjs.map} +1 -1
- package/dist/page/index.d.mts +11 -2
- package/dist/page/index.d.mts.map +1 -1
- package/dist/page/index.mjs +23 -1
- package/dist/page/index.mjs.map +1 -1
- package/dist/{placeholder-DntBEQo7.mjs → placeholder-C-fk5hYI.mjs} +1 -1
- package/dist/{placeholder-DntBEQo7.mjs.map → placeholder-C-fk5hYI.mjs.map} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts → placeholder-tzpqGWII.d.mts} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts.map → placeholder-tzpqGWII.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-B6Vu0d2i.mjs → query-g4Ug-9j9.mjs} +79 -12
- package/dist/query-g4Ug-9j9.mjs.map +1 -0
- package/dist/{redirect-7lGhLBNZ.mjs → redirect-CN0Rt9Ob.mjs} +66 -10
- package/dist/redirect-CN0Rt9Ob.mjs.map +1 -0
- package/dist/{registry-BgnP3ysR.mjs → registry-Ci3WxVAr.mjs} +133 -97
- package/dist/registry-Ci3WxVAr.mjs.map +1 -0
- package/dist/request-cache-DiR961CV.mjs +79 -0
- package/dist/request-cache-DiR961CV.mjs.map +1 -0
- package/dist/request-context.d.mts +19 -16
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs.map +1 -1
- package/dist/{runner-DYv3rX8P.d.mts → runner-BR2xKwhn.d.mts} +2 -2
- package/dist/{runner-DYv3rX8P.d.mts.map → runner-BR2xKwhn.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 +1 -1
- package/dist/{search-Cn1SYvYF.mjs → search-B0effn3j.mjs} +210 -226
- package/dist/search-B0effn3j.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +10 -9
- 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.mjs +1 -1
- package/dist/taxonomies-K2z0Uhnj.mjs +308 -0
- package/dist/taxonomies-K2z0Uhnj.mjs.map +1 -0
- package/dist/{tokens-DKHiCYCB.mjs → tokens-BFPFx3CA.mjs} +1 -1
- package/dist/{tokens-DKHiCYCB.mjs.map → tokens-BFPFx3CA.mjs.map} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs → transport-BykRfpyy.mjs} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs.map → transport-BykRfpyy.mjs.map} +1 -1
- package/dist/{transport-CKQA_G44.d.mts → transport-H4Iwx7tC.d.mts} +1 -1
- package/dist/{transport-CKQA_G44.d.mts.map → transport-H4Iwx7tC.d.mts.map} +1 -1
- package/dist/{types-BmkQR1En.d.mts → types-6CUZRrZP.d.mts} +1 -1
- package/dist/{types-BmkQR1En.d.mts.map → types-6CUZRrZP.d.mts.map} +1 -1
- package/dist/{types-Dz9_WMS6.mjs → types-BH2L167P.mjs} +1 -1
- package/dist/{types-Dz9_WMS6.mjs.map → types-BH2L167P.mjs.map} +1 -1
- package/dist/{types-B6BzlZxx.d.mts → types-C2v0c34j.d.mts} +10 -1
- package/dist/{types-B6BzlZxx.d.mts.map → types-C2v0c34j.d.mts.map} +1 -1
- package/dist/{types-DNZpaCBk.d.mts → types-CFWjXmus.d.mts} +1 -1
- package/dist/{types-DNZpaCBk.d.mts.map → types-CFWjXmus.d.mts.map} +1 -1
- package/dist/{types-DeG21anB.d.mts → types-CnZYHyLW.d.mts} +55 -5
- package/dist/types-CnZYHyLW.d.mts.map +1 -0
- package/dist/{types-xxCWI3j0.mjs → types-DDS4MxsT.mjs} +11 -3
- package/dist/types-DDS4MxsT.mjs.map +1 -0
- package/dist/{types-C3ronwXb.d.mts → types-DgrIP0tF.d.mts} +102 -4
- package/dist/types-DgrIP0tF.d.mts.map +1 -0
- package/dist/{validate-DuZDIxfy.mjs → validate-CqsNItbt.mjs} +2 -2
- package/dist/{validate-DuZDIxfy.mjs.map → validate-CqsNItbt.mjs.map} +1 -1
- package/dist/{validate-Db1yNL3i.d.mts → validate-kM8Pjuf7.d.mts} +5 -52
- package/dist/validate-kM8Pjuf7.d.mts.map +1 -0
- package/dist/version-BnTKdfam.mjs +7 -0
- package/dist/{version-CMMjTuqu.mjs.map → version-BnTKdfam.mjs.map} +1 -1
- package/package.json +10 -5
- package/src/after.ts +62 -0
- package/src/api/handlers/content.ts +2 -0
- package/src/api/handlers/oauth-authorization.ts +2 -32
- package/src/api/handlers/oauth-clients.ts +40 -4
- package/src/api/handlers/taxonomies.ts +13 -0
- package/src/api/oauth/redirect-uri.ts +34 -0
- package/src/api/openapi/document.ts +126 -118
- package/src/api/schemas/content.ts +8 -0
- package/src/api/schemas/media.ts +26 -15
- package/src/api/schemas/schema.ts +1 -0
- package/src/astro/integration/font-provider.ts +178 -0
- package/src/astro/integration/index.ts +44 -0
- package/src/astro/integration/routes.ts +6 -0
- package/src/astro/integration/runtime.ts +117 -0
- package/src/astro/integration/virtual-modules.ts +41 -39
- package/src/astro/integration/vite-config.ts +16 -5
- package/src/astro/middleware/auth.ts +33 -1
- package/src/astro/middleware/request-context.ts +15 -3
- package/src/astro/middleware.ts +340 -263
- package/src/astro/routes/admin.astro +21 -10
- package/src/astro/routes/api/auth/magic-link/send.ts +2 -1
- package/src/astro/routes/api/auth/passkey/options.ts +2 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +5 -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]/terms/[taxonomy].ts +5 -0
- 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 +19 -1
- package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
- package/src/astro/routes/api/import/wordpress/execute.ts +1 -1
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +4 -3
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +5 -4
- package/src/astro/routes/api/manifest.ts +7 -0
- package/src/astro/routes/api/media/upload-url.ts +10 -2
- package/src/astro/routes/api/media.ts +10 -7
- 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/oauth/register.ts +178 -0
- package/src/astro/routes/api/oauth/token.ts +15 -0
- package/src/astro/routes/api/openapi.json.ts +15 -5
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +2 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +1 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +1 -0
- package/src/astro/routes/api/search/index.ts +5 -0
- package/src/astro/routes/api/search/suggest.ts +3 -0
- package/src/astro/routes/api/setup/admin-verify.ts +30 -5
- package/src/astro/routes/api/setup/admin.ts +32 -8
- package/src/astro/routes/api/setup/index.ts +5 -2
- package/src/astro/routes/api/taxonomies/index.ts +1 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +1 -1
- package/src/astro/types.ts +9 -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/bylines/index.ts +22 -45
- package/src/components/EmDashHead.astro +23 -7
- package/src/database/connection.ts +23 -1
- package/src/database/instrumentation.ts +98 -0
- package/src/database/migrations/035_bounded_404_log.ts +112 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/content.ts +39 -0
- package/src/database/repositories/options.ts +25 -0
- package/src/database/repositories/redirect.ts +111 -8
- package/src/database/types.ts +9 -0
- package/src/db/adapters.ts +15 -0
- package/src/emdash-runtime.ts +312 -92
- package/src/import/registry.ts +4 -3
- package/src/import/ssrf.ts +253 -12
- package/src/index.ts +6 -0
- package/src/loader.ts +19 -24
- package/src/mcp/server.ts +76 -3
- package/src/menus/index.ts +6 -3
- package/src/page/index.ts +1 -1
- package/src/page/seo-contributions.ts +36 -0
- package/src/plugins/context.ts +15 -3
- package/src/plugins/manager.ts +6 -0
- package/src/plugins/request-meta.ts +66 -15
- package/src/plugins/routes.ts +3 -1
- package/src/query.ts +104 -7
- package/src/request-cache.ts +106 -0
- package/src/request-context.ts +19 -0
- package/src/schema/query.ts +5 -2
- package/src/schema/registry.ts +243 -166
- package/src/schema/types.ts +13 -2
- package/src/schema/zod-generator.ts +4 -0
- package/src/search/fts-manager.ts +19 -5
- package/src/search/query.ts +4 -3
- package/src/seed/apply.ts +41 -1
- package/src/settings/index.ts +24 -5
- package/src/taxonomies/index.ts +324 -124
- package/src/utils/db-errors.ts +46 -0
- package/src/virtual-modules.d.ts +31 -10
- package/src/visual-editing/toolbar.ts +6 -1
- package/src/widgets/index.ts +54 -25
- package/dist/adapters-C2BzVy0p.d.mts.map +0 -1
- package/dist/apply-Cma_PiF6.mjs.map +0 -1
- package/dist/byline-WuOq9MFJ.mjs.map +0 -1
- package/dist/bylines-C_Wsnz4L.mjs.map +0 -1
- package/dist/connection-B4zVnQIa.mjs.map +0 -1
- package/dist/index-CCWzlriB.d.mts.map +0 -1
- package/dist/loader-BYzwzORf.mjs.map +0 -1
- package/dist/query-B6Vu0d2i.mjs.map +0 -1
- package/dist/redirect-7lGhLBNZ.mjs.map +0 -1
- package/dist/registry-BgnP3ysR.mjs.map +0 -1
- package/dist/runner-Cd-_WyDo.mjs.map +0 -1
- package/dist/search-Cn1SYvYF.mjs.map +0 -1
- package/dist/types-C3ronwXb.d.mts.map +0 -1
- package/dist/types-DeG21anB.d.mts.map +0 -1
- package/dist/types-xxCWI3j0.mjs.map +0 -1
- package/dist/validate-Db1yNL3i.d.mts.map +0 -1
- package/dist/version-CMMjTuqu.mjs +0 -7
package/src/emdash-runtime.ts
CHANGED
|
@@ -19,7 +19,9 @@ 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";
|
|
24
|
+
import { kyselyLogOption } from "./database/instrumentation.js";
|
|
23
25
|
import { runMigrations } from "./database/migrations/runner.js";
|
|
24
26
|
import { RevisionRepository } from "./database/repositories/revision.js";
|
|
25
27
|
import type { ContentItem as ContentItemInternal } from "./database/repositories/types.js";
|
|
@@ -88,6 +90,7 @@ function isValidMetadataContribution(c: unknown): c is PageMetadataContribution
|
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
import { after } from "./after.js";
|
|
91
94
|
import { loadBundleFromR2 } from "./api/handlers/marketplace.js";
|
|
92
95
|
import { runSystemCleanup } from "./cleanup.js";
|
|
93
96
|
import {
|
|
@@ -153,6 +156,7 @@ import { FTSManager } from "./search/fts-manager.js";
|
|
|
153
156
|
const FIELD_TYPE_TO_KIND: Record<FieldType, string> = {
|
|
154
157
|
string: "string",
|
|
155
158
|
slug: "string",
|
|
159
|
+
url: "url",
|
|
156
160
|
text: "richText",
|
|
157
161
|
number: "number",
|
|
158
162
|
integer: "number",
|
|
@@ -286,6 +290,18 @@ export class EmDashRuntime {
|
|
|
286
290
|
private enabledPlugins: Set<string>;
|
|
287
291
|
private pluginStates: Map<string, string>;
|
|
288
292
|
|
|
293
|
+
private _cachedManifest: EmDashManifest | null = null;
|
|
294
|
+
private _manifestPromise: Promise<EmDashManifest> | null = null;
|
|
295
|
+
private readonly _manifestCacheKey: string;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Set to true after FTS indexes have been verified for this worker
|
|
299
|
+
* lifetime so we don't re-scan on every admin request. See
|
|
300
|
+
* ensureSearchHealthy().
|
|
301
|
+
*/
|
|
302
|
+
private _searchHealthChecked = false;
|
|
303
|
+
private _searchHealthPromise: Promise<void> | null = null;
|
|
304
|
+
|
|
289
305
|
/** Current hook pipeline. Use the `hooks` getter for external access. */
|
|
290
306
|
get hooks(): HookPipeline {
|
|
291
307
|
return this._hooks;
|
|
@@ -344,6 +360,7 @@ export class EmDashRuntime {
|
|
|
344
360
|
},
|
|
345
361
|
runtimeDeps: RuntimeDependencies,
|
|
346
362
|
pipelineRef: { current: HookPipeline },
|
|
363
|
+
manifestCacheKey: string,
|
|
347
364
|
) {
|
|
348
365
|
this._db = db;
|
|
349
366
|
this.storage = storage;
|
|
@@ -364,6 +381,7 @@ export class EmDashRuntime {
|
|
|
364
381
|
this.pipelineFactoryOptions = pipelineFactoryOptions;
|
|
365
382
|
this.runtimeDeps = runtimeDeps;
|
|
366
383
|
this.pipelineRef = pipelineRef;
|
|
384
|
+
this._manifestCacheKey = manifestCacheKey;
|
|
367
385
|
}
|
|
368
386
|
|
|
369
387
|
/**
|
|
@@ -413,6 +431,7 @@ export class EmDashRuntime {
|
|
|
413
431
|
this.enabledPlugins.delete(pluginId);
|
|
414
432
|
await this.rebuildHookPipeline();
|
|
415
433
|
}
|
|
434
|
+
this.invalidateManifest();
|
|
416
435
|
}
|
|
417
436
|
|
|
418
437
|
/**
|
|
@@ -565,36 +584,46 @@ export class EmDashRuntime {
|
|
|
565
584
|
/**
|
|
566
585
|
* Create and initialize the runtime
|
|
567
586
|
*/
|
|
568
|
-
static async create(
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
|
|
587
|
+
static async create(
|
|
588
|
+
deps: RuntimeDependencies,
|
|
589
|
+
timings?: Array<{ name: string; dur: number; desc?: string }>,
|
|
590
|
+
): Promise<EmDashRuntime> {
|
|
591
|
+
// Helper: time a phase and push into the shared timings array when
|
|
592
|
+
// provided. Uses performance.now() — monotonic across async boundaries.
|
|
593
|
+
// No-op when `timings` wasn't passed (preserves backwards compatibility
|
|
594
|
+
// with callers that don't care about per-phase breakdown).
|
|
595
|
+
const phase = async <T>(name: string, desc: string, fn: () => Promise<T>): Promise<T> => {
|
|
596
|
+
if (!timings) return fn();
|
|
597
|
+
const t0 = performance.now();
|
|
576
598
|
try {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
console.log(`Repaired ${repaired} corrupted FTS index(es) at startup`);
|
|
581
|
-
}
|
|
582
|
-
} catch {
|
|
583
|
-
// FTS tables may not exist yet (pre-setup). Non-fatal.
|
|
599
|
+
return await fn();
|
|
600
|
+
} finally {
|
|
601
|
+
timings.push({ name, dur: performance.now() - t0, desc });
|
|
584
602
|
}
|
|
585
|
-
}
|
|
603
|
+
};
|
|
586
604
|
|
|
587
|
-
// Initialize
|
|
605
|
+
// Initialize database (connects, runs migrations if needed)
|
|
606
|
+
const db = await phase("rt.db", "DB init + migrations", () => EmDashRuntime.getDatabase(deps));
|
|
607
|
+
|
|
608
|
+
// FTS verify/repair is deferred off the cold-start hot path.
|
|
609
|
+
// See EmDashRuntime.ensureSearchHealthy().
|
|
610
|
+
|
|
611
|
+
// Initialize storage (sync)
|
|
588
612
|
const storage = EmDashRuntime.getStorage(deps);
|
|
589
613
|
|
|
590
614
|
// Fetch plugin states from database
|
|
591
615
|
let pluginStates: Map<string, string> = new Map();
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
616
|
+
await phase("rt.plugins", "Plugin states", async () => {
|
|
617
|
+
try {
|
|
618
|
+
const states = await db
|
|
619
|
+
.selectFrom("_plugin_state")
|
|
620
|
+
.select(["plugin_id", "status"])
|
|
621
|
+
.execute();
|
|
622
|
+
pluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));
|
|
623
|
+
} catch {
|
|
624
|
+
// Plugin state table may not exist yet
|
|
625
|
+
}
|
|
626
|
+
});
|
|
598
627
|
|
|
599
628
|
// Build set of enabled plugins
|
|
600
629
|
const enabledPlugins = new Set<string>();
|
|
@@ -605,21 +634,25 @@ export class EmDashRuntime {
|
|
|
605
634
|
}
|
|
606
635
|
}
|
|
607
636
|
|
|
608
|
-
// Load site info for plugin context extensions
|
|
637
|
+
// Load site info for plugin context extensions (1 batch query instead of 3)
|
|
609
638
|
let siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
639
|
+
await phase("rt.site", "Site info options", async () => {
|
|
640
|
+
try {
|
|
641
|
+
const optionsRepo = new OptionsRepository(db);
|
|
642
|
+
const siteOpts = await optionsRepo.getMany<string>([
|
|
643
|
+
"emdash:site_title",
|
|
644
|
+
"emdash:site_url",
|
|
645
|
+
"emdash:locale",
|
|
646
|
+
]);
|
|
647
|
+
siteInfo = {
|
|
648
|
+
siteName: siteOpts.get("emdash:site_title") ?? undefined,
|
|
649
|
+
siteUrl: siteOpts.get("emdash:site_url") ?? undefined,
|
|
650
|
+
locale: siteOpts.get("emdash:locale") ?? undefined,
|
|
651
|
+
};
|
|
652
|
+
} catch {
|
|
653
|
+
// Options table may not exist yet (pre-setup)
|
|
654
|
+
}
|
|
655
|
+
});
|
|
623
656
|
|
|
624
657
|
// Build the full list of pipeline-eligible plugins: all configured
|
|
625
658
|
// plugins (regardless of current enabled status) plus built-in plugins.
|
|
@@ -685,11 +718,15 @@ export class EmDashRuntime {
|
|
|
685
718
|
const pipeline = createHookPipeline(enabledPluginList, pipelineFactoryOptions);
|
|
686
719
|
|
|
687
720
|
// Load sandboxed plugins (build-time)
|
|
688
|
-
const sandboxedPlugins = await
|
|
721
|
+
const sandboxedPlugins = await phase("rt.sandbox", "Sandboxed plugins", () =>
|
|
722
|
+
EmDashRuntime.loadSandboxedPlugins(deps, db),
|
|
723
|
+
);
|
|
689
724
|
|
|
690
725
|
// Cold-start: load marketplace-installed plugins from site R2
|
|
691
726
|
if (deps.config.marketplace && storage) {
|
|
692
|
-
await
|
|
727
|
+
await phase("rt.market", "Marketplace plugins", () =>
|
|
728
|
+
EmDashRuntime.loadMarketplacePlugins(db, storage, deps, sandboxedPlugins),
|
|
729
|
+
);
|
|
693
730
|
}
|
|
694
731
|
|
|
695
732
|
// Initialize media providers
|
|
@@ -707,7 +744,9 @@ export class EmDashRuntime {
|
|
|
707
744
|
}
|
|
708
745
|
|
|
709
746
|
// Resolve exclusive hooks — auto-select providers and sync with DB
|
|
710
|
-
await
|
|
747
|
+
await phase("rt.hooks", "Exclusive hook resolution", () =>
|
|
748
|
+
EmDashRuntime.resolveExclusiveHooks(pipeline, db, deps),
|
|
749
|
+
);
|
|
711
750
|
|
|
712
751
|
// ── Email pipeline ───────────────────────────────────────────────
|
|
713
752
|
// The email pipeline orchestrates beforeSend → deliver → afterSend.
|
|
@@ -740,52 +779,84 @@ export class EmDashRuntime {
|
|
|
740
779
|
let cronExecutor: CronExecutor | null = null;
|
|
741
780
|
let cronScheduler: CronScheduler | null = null;
|
|
742
781
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
782
|
+
await phase("rt.cron", "Cron init (recovery deferred post-response)", async () => {
|
|
783
|
+
try {
|
|
784
|
+
cronExecutor = new CronExecutor(db, invokeCronHook);
|
|
785
|
+
|
|
786
|
+
// Recover stale locks from previous crashes. Pure bookkeeping
|
|
787
|
+
// against the _emdash_cron_tasks table — no request needs the
|
|
788
|
+
// result — so we defer it past the response via after(). On
|
|
789
|
+
// Cloudflare this goes into waitUntil (extending the worker
|
|
790
|
+
// lifetime); on Node it's fire-and-forget (the process stays
|
|
791
|
+
// up anyway). Saves one cold-start write per D1 isolate.
|
|
792
|
+
const executorForRecovery = cronExecutor;
|
|
793
|
+
after(async () => {
|
|
794
|
+
try {
|
|
795
|
+
const recovered = await executorForRecovery.recoverStaleLocks();
|
|
796
|
+
if (recovered > 0) {
|
|
797
|
+
console.log(`[cron] Recovered ${recovered} stale task lock(s)`);
|
|
798
|
+
}
|
|
799
|
+
} catch (error) {
|
|
800
|
+
// Keep the `[cron]` prefix so a failure is easy to trace back
|
|
801
|
+
// rather than surfacing as a generic deferred-task error.
|
|
802
|
+
console.error("[cron] Failed to recover stale task locks:", error);
|
|
803
|
+
}
|
|
804
|
+
});
|
|
751
805
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
if (isWorkersRuntime) {
|
|
761
|
-
cronScheduler = new PiggybackScheduler(cronExecutor);
|
|
762
|
-
} else {
|
|
763
|
-
cronScheduler = new NodeCronScheduler(cronExecutor);
|
|
764
|
-
}
|
|
806
|
+
// Detect platform and create appropriate scheduler.
|
|
807
|
+
// On Cloudflare Workers, setTimeout is available but unreliable for
|
|
808
|
+
// long durations — use PiggybackScheduler as default.
|
|
809
|
+
// In Node/Bun, use NodeCronScheduler with real timers.
|
|
810
|
+
const isWorkersRuntime =
|
|
811
|
+
typeof globalThis.navigator !== "undefined" &&
|
|
812
|
+
globalThis.navigator.userAgent === "Cloudflare-Workers";
|
|
765
813
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
await runSystemCleanup(db, storage ?? undefined);
|
|
771
|
-
} catch (error) {
|
|
772
|
-
// Non-fatal -- individual cleanup failures are already logged
|
|
773
|
-
// by runSystemCleanup. This catches unexpected errors.
|
|
774
|
-
console.error("[cleanup] System cleanup failed:", error);
|
|
814
|
+
if (isWorkersRuntime) {
|
|
815
|
+
cronScheduler = new PiggybackScheduler(cronExecutor);
|
|
816
|
+
} else {
|
|
817
|
+
cronScheduler = new NodeCronScheduler(cronExecutor);
|
|
775
818
|
}
|
|
776
|
-
});
|
|
777
819
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
820
|
+
// Register system cleanup to run alongside each scheduler tick.
|
|
821
|
+
// Pass storage so cleanupPendingUploads can delete orphaned files.
|
|
822
|
+
cronScheduler.setSystemCleanup(async () => {
|
|
823
|
+
try {
|
|
824
|
+
await runSystemCleanup(db, storage ?? undefined);
|
|
825
|
+
} catch (error) {
|
|
826
|
+
// Non-fatal -- individual cleanup failures are already logged
|
|
827
|
+
// by runSystemCleanup. This catches unexpected errors.
|
|
828
|
+
console.error("[cleanup] System cleanup failed:", error);
|
|
829
|
+
}
|
|
830
|
+
});
|
|
782
831
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
832
|
+
// Add cron reschedule callback (merges with existing factory options)
|
|
833
|
+
pipeline.setContextFactory({
|
|
834
|
+
cronReschedule: () => cronScheduler?.reschedule(),
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
// Start the scheduler
|
|
838
|
+
await cronScheduler.start();
|
|
839
|
+
} catch (error) {
|
|
840
|
+
console.warn("[cron] Failed to initialize cron system:", error);
|
|
841
|
+
// Non-fatal — CMS works without cron
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
// SHA of emdash commit + user config that affects the manifest.
|
|
846
|
+
// COMMIT captures emdash code changes; plugin IDs/versions and i18n
|
|
847
|
+
// capture user astro.config changes (e.g. upgrading a plugin package).
|
|
848
|
+
// DB-driven changes (collections, fields, plugin toggle) go through
|
|
849
|
+
// invalidateManifest(). Sorted for stability across nondeterministic
|
|
850
|
+
// plugin ordering.
|
|
851
|
+
const manifestCacheKey = await hashString(
|
|
852
|
+
[
|
|
853
|
+
COMMIT,
|
|
854
|
+
...deps.plugins.map((p) => `${p.id}@${p.version ?? ""}`).toSorted(),
|
|
855
|
+
...deps.sandboxedPluginEntries.map((e) => `${e.id}@${e.version}`).toSorted(),
|
|
856
|
+
virtualConfig?.i18n?.defaultLocale ?? "",
|
|
857
|
+
(virtualConfig?.i18n?.locales ?? []).toSorted().join(","),
|
|
858
|
+
].join("|"),
|
|
859
|
+
);
|
|
789
860
|
|
|
790
861
|
return new EmDashRuntime(
|
|
791
862
|
db,
|
|
@@ -806,6 +877,7 @@ export class EmDashRuntime {
|
|
|
806
877
|
pipelineFactoryOptions,
|
|
807
878
|
deps,
|
|
808
879
|
pipelineRef,
|
|
880
|
+
manifestCacheKey,
|
|
809
881
|
);
|
|
810
882
|
}
|
|
811
883
|
|
|
@@ -837,12 +909,14 @@ export class EmDashRuntime {
|
|
|
837
909
|
* Get or create database instance
|
|
838
910
|
*/
|
|
839
911
|
private static async getDatabase(deps: RuntimeDependencies): Promise<Kysely<Database>> {
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
// the
|
|
912
|
+
// Only use the per-request `ctx.db` when it's an isolated instance
|
|
913
|
+
// (playground / DO preview). Plain D1 Sessions set `ctx.db` on every
|
|
914
|
+
// anonymous request — if we captured one of those session-bound
|
|
915
|
+
// Kyselys into the cached runtime, every request would accidentally
|
|
916
|
+
// share one request's session. The configured `deps.createDialect`
|
|
917
|
+
// path gives us a fresh singleton instead.
|
|
844
918
|
const ctx = getRequestContext();
|
|
845
|
-
if (ctx?.db) {
|
|
919
|
+
if (ctx?.dbIsIsolated && ctx.db) {
|
|
846
920
|
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- db in context is typed as unknown to avoid circular deps
|
|
847
921
|
return ctx.db as Kysely<Database>;
|
|
848
922
|
}
|
|
@@ -878,9 +952,20 @@ export class EmDashRuntime {
|
|
|
878
952
|
|
|
879
953
|
dbInitPromise = (async () => {
|
|
880
954
|
const dialect = deps.createDialect(dbConfig.config);
|
|
881
|
-
const db = new Kysely<Database>({ dialect });
|
|
955
|
+
const db = new Kysely<Database>({ dialect, log: kyselyLogOption() });
|
|
882
956
|
|
|
883
|
-
await runMigrations(db);
|
|
957
|
+
const { applied } = await runMigrations(db);
|
|
958
|
+
|
|
959
|
+
// If migrations were applied, the schema changed — clear the
|
|
960
|
+
// DB-persisted manifest cache so getManifest() rebuilds it.
|
|
961
|
+
if (applied.length > 0) {
|
|
962
|
+
try {
|
|
963
|
+
const options = new OptionsRepository(db);
|
|
964
|
+
await options.delete("emdash:manifest_cache");
|
|
965
|
+
} catch {
|
|
966
|
+
// Non-fatal
|
|
967
|
+
}
|
|
968
|
+
}
|
|
884
969
|
|
|
885
970
|
// Auto-seed schema if no collections exist and setup hasn't run.
|
|
886
971
|
// This covers first-load on sites that skip the setup wizard.
|
|
@@ -1142,9 +1227,82 @@ export class EmDashRuntime {
|
|
|
1142
1227
|
// =========================================================================
|
|
1143
1228
|
|
|
1144
1229
|
/**
|
|
1145
|
-
*
|
|
1230
|
+
* Get the manifest, using an in-memory cache with a DB-persisted
|
|
1231
|
+
* fallback for cold starts. Avoids N+1 schema registry queries
|
|
1232
|
+
* on every request.
|
|
1233
|
+
*
|
|
1234
|
+
* Cache is invalidated by invalidateManifest(), called from schema
|
|
1235
|
+
* API routes, MCP server, plugin toggle, and taxonomy def changes.
|
|
1146
1236
|
*/
|
|
1147
1237
|
async getManifest(): Promise<EmDashManifest> {
|
|
1238
|
+
// When the DB is overridden by an isolated instance (playground /
|
|
1239
|
+
// DO-preview sessions), bypass the module-scoped manifest cache —
|
|
1240
|
+
// its schema may diverge from the configured DB. Plain D1 Sessions
|
|
1241
|
+
// routing does NOT set `dbIsIsolated`, so the cache still applies.
|
|
1242
|
+
if (getRequestContext()?.dbIsIsolated) {
|
|
1243
|
+
return this._buildManifest();
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
if (this._cachedManifest) return this._cachedManifest;
|
|
1247
|
+
|
|
1248
|
+
// DB-persisted cache (1 query instead of N+1 rebuild on cold start).
|
|
1249
|
+
// Keyed by SHA of commit + config to bust on deploys. DB-driven
|
|
1250
|
+
// changes (collections, fields, plugins, taxonomies) go through
|
|
1251
|
+
// invalidateManifest().
|
|
1252
|
+
try {
|
|
1253
|
+
const options = new OptionsRepository(this.db);
|
|
1254
|
+
const cached = await options.get<{ key: string; manifest: EmDashManifest }>(
|
|
1255
|
+
"emdash:manifest_cache",
|
|
1256
|
+
);
|
|
1257
|
+
if (cached && cached.key === this._manifestCacheKey && cached.manifest) {
|
|
1258
|
+
this._cachedManifest = cached.manifest;
|
|
1259
|
+
return cached.manifest;
|
|
1260
|
+
}
|
|
1261
|
+
} catch {
|
|
1262
|
+
// Options table may not exist yet
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Full rebuild, then persist. Track which promise is current so
|
|
1266
|
+
// an invalidation during the build can't be overwritten.
|
|
1267
|
+
if (!this._manifestPromise) {
|
|
1268
|
+
let manifestPromise: Promise<EmDashManifest>;
|
|
1269
|
+
const isCurrentLoad = () => this._manifestPromise === manifestPromise;
|
|
1270
|
+
manifestPromise = this._loadManifest(isCurrentLoad);
|
|
1271
|
+
this._manifestPromise = manifestPromise;
|
|
1272
|
+
}
|
|
1273
|
+
return this._manifestPromise;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
private async _loadManifest(isCurrentLoad: () => boolean): Promise<EmDashManifest> {
|
|
1277
|
+
try {
|
|
1278
|
+
const manifest = await this._buildManifest();
|
|
1279
|
+
|
|
1280
|
+
if (isCurrentLoad()) {
|
|
1281
|
+
this._cachedManifest = manifest;
|
|
1282
|
+
|
|
1283
|
+
try {
|
|
1284
|
+
const options = new OptionsRepository(this.db);
|
|
1285
|
+
await options.set("emdash:manifest_cache", {
|
|
1286
|
+
key: this._manifestCacheKey,
|
|
1287
|
+
manifest,
|
|
1288
|
+
});
|
|
1289
|
+
} catch {
|
|
1290
|
+
// Non-fatal — will just rebuild next time
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
return manifest;
|
|
1295
|
+
} finally {
|
|
1296
|
+
if (isCurrentLoad()) {
|
|
1297
|
+
this._manifestPromise = null;
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* Build the manifest from database (N+1 collection queries).
|
|
1304
|
+
*/
|
|
1305
|
+
private async _buildManifest(): Promise<EmDashManifest> {
|
|
1148
1306
|
// Build collections from database.
|
|
1149
1307
|
// Use this.db (ALS-aware getter) so playground mode picks up the
|
|
1150
1308
|
// per-session DO database instead of the hardcoded singleton.
|
|
@@ -1370,11 +1528,72 @@ export class EmDashRuntime {
|
|
|
1370
1528
|
|
|
1371
1529
|
/**
|
|
1372
1530
|
* Invalidate cached data derived from the manifest/schema.
|
|
1373
|
-
* Called when collections
|
|
1531
|
+
* Called when collections, fields, plugins, or taxonomy defs change.
|
|
1374
1532
|
*/
|
|
1375
1533
|
invalidateManifest(): void {
|
|
1376
|
-
|
|
1534
|
+
this._cachedManifest = null;
|
|
1535
|
+
this._manifestPromise = null;
|
|
1377
1536
|
invalidateUrlPatternCache();
|
|
1537
|
+
// Delete DB-persisted cache so the next cold start rebuilds.
|
|
1538
|
+
// Fire-and-forget: in-memory is already cleared for this worker,
|
|
1539
|
+
// DB delete is best-effort for the next cold start.
|
|
1540
|
+
try {
|
|
1541
|
+
const options = new OptionsRepository(this.db);
|
|
1542
|
+
options.delete("emdash:manifest_cache").catch((error) => {
|
|
1543
|
+
console.error("Failed to delete persisted manifest cache", error);
|
|
1544
|
+
});
|
|
1545
|
+
} catch (error) {
|
|
1546
|
+
console.error("Failed to initialize manifest cache invalidation", error);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
/**
|
|
1551
|
+
* Verify and repair FTS indexes on demand. Runs at most once per worker
|
|
1552
|
+
* lifetime.
|
|
1553
|
+
*
|
|
1554
|
+
* Originally called from `EmDashRuntime.create()`, but on a busy D1 link
|
|
1555
|
+
* (e.g. SIN replica ~80-150ms per query) it added ~1.5s to every cold
|
|
1556
|
+
* start for a modest-sized site — more than every other init phase
|
|
1557
|
+
* combined. Anonymous public reads never touch the search write path,
|
|
1558
|
+
* so the cost isn't paid back for the vast majority of requests.
|
|
1559
|
+
*
|
|
1560
|
+
* Instead, search endpoints call this lazily: the first request that
|
|
1561
|
+
* actually needs the index pays the verify cost (usually fast — no
|
|
1562
|
+
* rebuild needed), everyone else runs cold-free.
|
|
1563
|
+
*
|
|
1564
|
+
* Uses the runtime's singleton database (`this._db`) rather than the
|
|
1565
|
+
* request-scoped DB. Verify reads only, but `rebuildIndex` writes, and
|
|
1566
|
+
* a GET search request on D1 carries a `first-unconstrained` session
|
|
1567
|
+
* that's free to route at a read replica — unsafe for writes. The
|
|
1568
|
+
* singleton always goes through the default binding, which the D1
|
|
1569
|
+
* adapter will promote to `first-primary` for write statements.
|
|
1570
|
+
*
|
|
1571
|
+
* Safe to call concurrently: repeated callers share the same in-flight
|
|
1572
|
+
* promise. Errors are swallowed internally so callers don't need to
|
|
1573
|
+
* defend against FTS not existing yet (pre-setup).
|
|
1574
|
+
*/
|
|
1575
|
+
async ensureSearchHealthy(): Promise<void> {
|
|
1576
|
+
if (this._searchHealthChecked) return;
|
|
1577
|
+
if (this._searchHealthPromise) return this._searchHealthPromise;
|
|
1578
|
+
if (!isSqlite(this._db)) {
|
|
1579
|
+
this._searchHealthChecked = true;
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
this._searchHealthPromise = (async () => {
|
|
1583
|
+
try {
|
|
1584
|
+
const ftsManager = new FTSManager(this._db);
|
|
1585
|
+
const repaired = await ftsManager.verifyAndRepairAll();
|
|
1586
|
+
if (repaired > 0) {
|
|
1587
|
+
console.log(`Repaired ${repaired} corrupted FTS index(es)`);
|
|
1588
|
+
}
|
|
1589
|
+
} catch {
|
|
1590
|
+
// FTS tables may not exist yet (pre-setup). Non-fatal.
|
|
1591
|
+
} finally {
|
|
1592
|
+
this._searchHealthChecked = true;
|
|
1593
|
+
this._searchHealthPromise = null;
|
|
1594
|
+
}
|
|
1595
|
+
})();
|
|
1596
|
+
return this._searchHealthPromise;
|
|
1378
1597
|
}
|
|
1379
1598
|
|
|
1380
1599
|
// =========================================================================
|
|
@@ -1862,6 +2081,7 @@ export class EmDashRuntime {
|
|
|
1862
2081
|
const routeRegistry = new PluginRouteRegistry({
|
|
1863
2082
|
db: this.db,
|
|
1864
2083
|
emailPipeline: this.email ?? undefined,
|
|
2084
|
+
trustedProxyHeaders: getTrustedProxyHeaders(this.config),
|
|
1865
2085
|
});
|
|
1866
2086
|
routeRegistry.register(trustedPlugin);
|
|
1867
2087
|
|
|
@@ -2103,7 +2323,7 @@ export class EmDashRuntime {
|
|
|
2103
2323
|
|
|
2104
2324
|
try {
|
|
2105
2325
|
const headers = sanitizeHeadersForSandbox(request.headers);
|
|
2106
|
-
const meta = extractRequestMeta(request);
|
|
2326
|
+
const meta = extractRequestMeta(request, this.config);
|
|
2107
2327
|
const result = await plugin.invokeRoute(routeName, body, {
|
|
2108
2328
|
url: request.url,
|
|
2109
2329
|
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();
|