emdash 0.11.1 → 0.12.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.
Files changed (75) hide show
  1. package/dist/{apply-Ded_1vng.mjs → apply-C1ZORgcy.mjs} +6 -226
  2. package/dist/apply-C1ZORgcy.mjs.map +1 -0
  3. package/dist/astro/index.d.mts +3 -3
  4. package/dist/astro/index.mjs +1 -1
  5. package/dist/astro/middleware/auth.d.mts +3 -3
  6. package/dist/astro/middleware/auth.mjs +1 -1
  7. package/dist/astro/middleware.mjs +16 -12
  8. package/dist/astro/middleware.mjs.map +1 -1
  9. package/dist/astro/types.d.mts +3 -4
  10. package/dist/astro/types.d.mts.map +1 -1
  11. package/dist/cli/index.mjs +4 -4
  12. package/dist/{error-DqnRMM5z.mjs → error-D6LuHLw9.mjs} +1 -1
  13. package/dist/{error-DqnRMM5z.mjs.map → error-D6LuHLw9.mjs.map} +1 -1
  14. package/dist/{index-BogfvE-z.d.mts → index-Dlkzhb4C.d.mts} +5 -5
  15. package/dist/index-Dlkzhb4C.d.mts.map +1 -0
  16. package/dist/index.d.mts +4 -4
  17. package/dist/index.mjs +9 -9
  18. package/dist/{manifest-schema-CXAbd1vH.mjs → manifest-schema-Bp6d4d4n.mjs} +1 -1
  19. package/dist/{manifest-schema-CXAbd1vH.mjs.map → manifest-schema-Bp6d4d4n.mjs.map} +1 -1
  20. package/dist/media/local-runtime.d.mts +3 -3
  21. package/dist/media/local-runtime.d.mts.map +1 -1
  22. package/dist/media/local-runtime.mjs +6 -1
  23. package/dist/media/local-runtime.mjs.map +1 -1
  24. package/dist/page/index.d.mts +15 -4
  25. package/dist/page/index.d.mts.map +1 -1
  26. package/dist/page/index.mjs +16 -5
  27. package/dist/page/index.mjs.map +1 -1
  28. package/dist/plugins/adapt-sandbox-entry.d.mts +3 -3
  29. package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
  30. package/dist/{query-8c_meo_K.mjs → query-yA3-rFji.mjs} +13 -2
  31. package/dist/query-yA3-rFji.mjs.map +1 -0
  32. package/dist/runtime.d.mts +3 -3
  33. package/dist/{search-DuWhx4NG.mjs → search-n-ZCMfr3.mjs} +33 -16
  34. package/dist/search-n-ZCMfr3.mjs.map +1 -0
  35. package/dist/seed/index.d.mts +1 -1
  36. package/dist/seed/index.mjs +2 -2
  37. package/dist/settings-nTXPRi3D.mjs +440 -0
  38. package/dist/settings-nTXPRi3D.mjs.map +1 -0
  39. package/dist/storage/local.mjs +1 -1
  40. package/dist/storage/s3.mjs +1 -1
  41. package/dist/{taxonomies-Bw76xAxo.mjs → taxonomies-JmQQZiG1.mjs} +2 -2
  42. package/dist/{taxonomies-Bw76xAxo.mjs.map → taxonomies-JmQQZiG1.mjs.map} +1 -1
  43. package/dist/{types-BTe41zL6.d.mts → types-B1gLSAH2.d.mts} +13 -9
  44. package/dist/{types-BTe41zL6.d.mts.map → types-B1gLSAH2.d.mts.map} +1 -1
  45. package/dist/{types-DiI8NOG_.mjs → types-Cug_RO3W.mjs} +1 -1
  46. package/dist/{types-DiI8NOG_.mjs.map → types-Cug_RO3W.mjs.map} +1 -1
  47. package/dist/{types-IjUrQMVe.d.mts → types-DgSc9Rpc.d.mts} +149 -4
  48. package/dist/types-DgSc9Rpc.d.mts.map +1 -0
  49. package/dist/{types-K-EkEQCI.mjs → types-PafqtQuM.mjs} +1 -1
  50. package/dist/{types-K-EkEQCI.mjs.map → types-PafqtQuM.mjs.map} +1 -1
  51. package/dist/{validate-CcVQQpmH.d.mts → validate-BcC3m2O7.d.mts} +2 -2
  52. package/dist/{validate-CcVQQpmH.d.mts.map → validate-BcC3m2O7.d.mts.map} +1 -1
  53. package/dist/version-BdP--J1g.mjs +7 -0
  54. package/dist/{version-JjSqv90m.mjs.map → version-BdP--J1g.mjs.map} +1 -1
  55. package/package.json +6 -6
  56. package/src/api/schemas/settings.ts +41 -9
  57. package/src/astro/routes/api/media/[id].ts +2 -1
  58. package/src/components/EmDashHead.astro +26 -5
  59. package/src/emdash-runtime.ts +21 -2
  60. package/src/media/local-runtime.ts +7 -0
  61. package/src/page/absolute-url.ts +146 -0
  62. package/src/page/jsonld.ts +10 -2
  63. package/src/page/seo-contributions.ts +17 -6
  64. package/src/plugins/context.ts +11 -1
  65. package/src/query.ts +12 -0
  66. package/src/settings/index.ts +20 -1
  67. package/src/settings/types.ts +12 -8
  68. package/dist/apply-Ded_1vng.mjs.map +0 -1
  69. package/dist/index-BogfvE-z.d.mts.map +0 -1
  70. package/dist/media-1fFhub9c.mjs +0 -209
  71. package/dist/media-1fFhub9c.mjs.map +0 -1
  72. package/dist/query-8c_meo_K.mjs.map +0 -1
  73. package/dist/search-DuWhx4NG.mjs.map +0 -1
  74. package/dist/types-IjUrQMVe.d.mts.map +0 -1
  75. package/dist/version-JjSqv90m.mjs +0 -7
@@ -1,237 +1,17 @@
1
1
  import { i as __exportAll } from "./runner-DIcU2UCC.mjs";
2
2
  import { n as getI18nConfig } from "./config-CVssduLe.mjs";
3
3
  import { r as RevisionRepository, t as ContentRepository } from "./content-CERxPUN0.mjs";
4
- import { t as MediaRepository } from "./media-1fFhub9c.mjs";
4
+ import { o as setSiteSettings, s as MediaRepository } from "./settings-nTXPRi3D.mjs";
5
5
  import { t as TaxonomyRepository } from "./taxonomy-D6NvlKo8.mjs";
6
- import { t as OptionsRepository } from "./options-nPxWnrya.mjs";
7
6
  import { t as withTransaction } from "./transaction-D44LBXvU.mjs";
8
7
  import { t as RedirectRepository } from "./redirect-C5H7VGIX.mjs";
9
8
  import { t as BylineRepository } from "./byline-gFn1r0vA.mjs";
10
- import { n as requestCached, t as peekRequestCache } from "./request-cache-D4I69LeL.mjs";
11
9
  import { i as FTSManager, n as SchemaRegistry } from "./registry-Do34mz_P.mjs";
12
- import { r as getDb } from "./loader-ou_PXAjg.mjs";
13
10
  import { t as validateSeed } from "./validate-UK4Ja1uo.mjs";
14
11
  import { ulid } from "ulidx";
15
12
  import { imageSize } from "image-size";
16
13
  import mime from "mime/lite";
17
14
 
18
- //#region src/settings/index.ts
19
- /** Prefix for site settings in the options table */
20
- const SETTINGS_PREFIX = "site:";
21
- const SITE_SETTINGS_CACHE_KEY = Symbol.for("emdash:site-settings");
22
- const g = globalThis;
23
- const holder = g[SITE_SETTINGS_CACHE_KEY] ?? (() => {
24
- const h = {
25
- version: 0,
26
- cached: null,
27
- cachedVersion: -1
28
- };
29
- g[SITE_SETTINGS_CACHE_KEY] = h;
30
- return h;
31
- })();
32
- /**
33
- * Bump the isolate-wide site-settings cache version, forcing the next
34
- * `getSiteSettings()` to re-query the database.
35
- *
36
- * Called from every `site:*` write path. Other isolates still serve their
37
- * own cached copy until they expire — staleness bounded by isolate lifetime.
38
- */
39
- function invalidateSiteSettingsCache() {
40
- holder.version++;
41
- holder.cached = null;
42
- holder.cachedVersion = -1;
43
- }
44
- /**
45
- * Type guard for MediaReference values
46
- */
47
- function isMediaReference(value) {
48
- return typeof value === "object" && value !== null && "mediaId" in value;
49
- }
50
- /**
51
- * Resolve a media reference to include the full URL plus content metadata.
52
- *
53
- * Pulls `mimeType` and intrinsic dimensions from the media row so callers
54
- * can emit correct head tags (e.g. `<link rel="icon" type="image/svg+xml">`,
55
- * which Chromium requires when the URL has no `.svg` extension) without
56
- * a second round-trip to the media table.
57
- */
58
- async function resolveMediaReference(mediaRef, db, _storage) {
59
- if (!mediaRef?.mediaId) return mediaRef;
60
- try {
61
- const media = await new MediaRepository(db).findById(mediaRef.mediaId);
62
- if (media) return {
63
- ...mediaRef,
64
- url: `/_emdash/api/media/file/${media.storageKey}`,
65
- contentType: media.mimeType,
66
- ...media.width !== null ? { width: media.width } : {},
67
- ...media.height !== null ? { height: media.height } : {}
68
- };
69
- } catch {}
70
- return mediaRef;
71
- }
72
- /**
73
- * Get a single site setting by key
74
- *
75
- * Returns `undefined` if the setting has not been configured.
76
- * For media settings (logo, favicon), the URL is resolved automatically.
77
- *
78
- * @param key - The setting key (e.g., "title", "logo", "social")
79
- * @returns The setting value, or undefined if not set
80
- *
81
- * @example
82
- * ```ts
83
- * import { getSiteSetting } from "emdash";
84
- *
85
- * const title = await getSiteSetting("title");
86
- * const logo = await getSiteSetting("logo");
87
- * console.log(logo?.url); // Resolved URL
88
- * ```
89
- */
90
- async function getSiteSetting(key) {
91
- const primed = peekRequestCache("siteSettings");
92
- if (primed) return (await primed)[key];
93
- return requestCached(`siteSetting:${key}`, async () => {
94
- return getSiteSettingWithDb(key, await getDb());
95
- });
96
- }
97
- /**
98
- * Get a single site setting by key (with explicit db)
99
- *
100
- * @internal Use `getSiteSetting()` in templates. This variant is for admin routes
101
- * that already have a database handle.
102
- */
103
- async function getSiteSettingWithDb(key, db, storage = null) {
104
- const value = await new OptionsRepository(db).get(`${SETTINGS_PREFIX}${key}`);
105
- if (!value) return;
106
- if ((key === "logo" || key === "favicon") && isMediaReference(value)) return await resolveMediaReference(value, db, storage);
107
- return value;
108
- }
109
- /**
110
- * Get all site settings
111
- *
112
- * Returns all configured settings. Unset values are undefined.
113
- * Media references (logo/favicon) are resolved to include URLs.
114
- *
115
- * @example
116
- * ```ts
117
- * import { getSiteSettings } from "emdash";
118
- *
119
- * const settings = await getSiteSettings();
120
- * console.log(settings.title); // "My Site"
121
- * console.log(settings.logo?.url); // "/_emdash/api/media/file/abc123"
122
- * ```
123
- */
124
- function getSiteSettings() {
125
- return requestCached("siteSettings", () => {
126
- const versionAtCall = holder.version;
127
- if (holder.cached && holder.cachedVersion === versionAtCall) return holder.cached;
128
- const fetchPromise = (async () => {
129
- return getSiteSettingsWithDb(await getDb());
130
- })().catch((error) => {
131
- if (holder.cached === fetchPromise) {
132
- holder.cached = null;
133
- holder.cachedVersion = -1;
134
- }
135
- throw error;
136
- });
137
- holder.cached = fetchPromise;
138
- holder.cachedVersion = versionAtCall;
139
- return fetchPromise;
140
- });
141
- }
142
- /**
143
- * Get all site settings (with explicit db)
144
- *
145
- * @internal Use `getSiteSettings()` in templates. This variant is for admin routes
146
- * that already have a database handle.
147
- */
148
- async function getSiteSettingsWithDb(db, storage = null) {
149
- const allOptions = await new OptionsRepository(db).getByPrefix(SETTINGS_PREFIX);
150
- const settings = {};
151
- for (const [key, value] of allOptions) {
152
- const settingKey = key.replace(SETTINGS_PREFIX, "");
153
- settings[settingKey] = value;
154
- }
155
- const typedSettings = settings;
156
- if (typedSettings.logo) typedSettings.logo = await resolveMediaReference(typedSettings.logo, db, storage);
157
- if (typedSettings.favicon) typedSettings.favicon = await resolveMediaReference(typedSettings.favicon, db, storage);
158
- return typedSettings;
159
- }
160
- /**
161
- * Set site settings (internal function used by admin API)
162
- *
163
- * Merges provided settings with existing ones. Only provided fields are updated.
164
- * Media references should include just the mediaId; URLs are resolved on read.
165
- *
166
- * @param settings - Partial settings object with values to update
167
- * @param db - Kysely database instance
168
- * @returns Promise that resolves when settings are saved
169
- *
170
- * @internal
171
- *
172
- * @example
173
- * ```ts
174
- * // Update multiple settings at once
175
- * await setSiteSettings({
176
- * title: "My Site",
177
- * tagline: "Welcome",
178
- * logo: { mediaId: "med_123", alt: "Logo" }
179
- * }, db);
180
- * ```
181
- */
182
- async function setSiteSettings(settings, db) {
183
- const options = new OptionsRepository(db);
184
- const updates = {};
185
- for (const [key, value] of Object.entries(settings)) if (value !== void 0) updates[`${SETTINGS_PREFIX}${key}`] = value;
186
- try {
187
- await options.setMany(updates);
188
- } finally {
189
- invalidateSiteSettingsCache();
190
- }
191
- }
192
- /**
193
- * Get a single plugin setting by key.
194
- *
195
- * Plugin settings are stored in the options table under
196
- * `plugin:<pluginId>:settings:<key>`.
197
- */
198
- async function getPluginSetting(pluginId, key) {
199
- return getPluginSettingWithDb(pluginId, key, await getDb());
200
- }
201
- /**
202
- * Get a single plugin setting by key (with explicit db).
203
- *
204
- * @internal Use `getPluginSetting()` in templates and plugin rendering code.
205
- */
206
- async function getPluginSettingWithDb(pluginId, key, db) {
207
- return await new OptionsRepository(db).get(`plugin:${pluginId}:settings:${key}`) ?? void 0;
208
- }
209
- /**
210
- * Get all persisted plugin settings for a plugin.
211
- *
212
- * Defaults declared in `admin.settingsSchema` are not materialized
213
- * automatically; callers should apply their own fallback defaults.
214
- */
215
- async function getPluginSettings(pluginId) {
216
- return getPluginSettingsWithDb(pluginId, await getDb());
217
- }
218
- /**
219
- * Get all persisted plugin settings for a plugin (with explicit db).
220
- *
221
- * @internal Use `getPluginSettings()` in templates and plugin rendering code.
222
- */
223
- async function getPluginSettingsWithDb(pluginId, db) {
224
- const prefix = `plugin:${pluginId}:settings:`;
225
- const allOptions = await new OptionsRepository(db).getByPrefix(prefix);
226
- const settings = {};
227
- for (const [key, value] of allOptions) {
228
- if (!key.startsWith(prefix)) continue;
229
- settings[key.slice(prefix.length)] = value;
230
- }
231
- return settings;
232
- }
233
-
234
- //#endregion
235
15
  //#region src/import/ssrf.ts
236
16
  /**
237
17
  * SSRF protection for import URLs.
@@ -1045,7 +825,7 @@ async function applySeed(db, seed, options = {}) {
1045
825
  }
1046
826
  const { invalidateBylineCache } = await import("./bylines-DTFI8nDM.mjs").then((n) => n.t);
1047
827
  const { invalidateRedirectCache } = await import("./cache-BAJbeoZ8.mjs").then((n) => n.t);
1048
- const { invalidateUrlPatternCache } = await import("./query-8c_meo_K.mjs").then((n) => n.o);
828
+ const { invalidateUrlPatternCache } = await import("./query-yA3-rFji.mjs").then((n) => n.o);
1049
829
  invalidateBylineCache();
1050
830
  invalidateRedirectCache();
1051
831
  invalidateUrlPatternCache();
@@ -1129,7 +909,7 @@ async function applyContentTaxonomies(db, collectionSlug, contentId, entry, isUp
1129
909
  if (isUpdate) await db.deleteFrom("content_taxonomies").where("collection", "=", collectionSlug).where("entry_id", "=", contentId).execute();
1130
910
  if (!entry.taxonomies) {
1131
911
  if (isUpdate) {
1132
- const { invalidateTermCache } = await import("./taxonomies-Bw76xAxo.mjs").then((n) => n.u);
912
+ const { invalidateTermCache } = await import("./taxonomies-JmQQZiG1.mjs").then((n) => n.u);
1133
913
  invalidateTermCache();
1134
914
  }
1135
915
  return;
@@ -1141,7 +921,7 @@ async function applyContentTaxonomies(db, collectionSlug, contentId, entry, isUp
1141
921
  if (term) await termRepo.attachToEntry(collectionSlug, contentId, term.id);
1142
922
  }
1143
923
  }
1144
- const { invalidateTermCache } = await import("./taxonomies-Bw76xAxo.mjs").then((n) => n.u);
924
+ const { invalidateTermCache } = await import("./taxonomies-JmQQZiG1.mjs").then((n) => n.u);
1145
925
  invalidateTermCache();
1146
926
  }
1147
927
  /**
@@ -1387,5 +1167,5 @@ function getImageDimensions(buffer) {
1387
1167
  }
1388
1168
 
1389
1169
  //#endregion
1390
- export { ssrfSafeFetch as a, getPluginSetting as c, getSiteSettings as d, setSiteSettings as f, resolveAndValidateExternalUrl as i, getPluginSettings as l, apply_exports as n, stripCredentialHeaders as o, SsrfError as r, validateExternalUrl as s, applySeed as t, getSiteSetting as u };
1391
- //# sourceMappingURL=apply-Ded_1vng.mjs.map
1170
+ export { ssrfSafeFetch as a, resolveAndValidateExternalUrl as i, apply_exports as n, stripCredentialHeaders as o, SsrfError as r, validateExternalUrl as s, applySeed as t };
1171
+ //# sourceMappingURL=apply-C1ZORgcy.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-C1ZORgcy.mjs","names":[],"sources":["../src/import/ssrf.ts","../src/seed/apply.ts"],"sourcesContent":["/**\n * SSRF protection for import URLs.\n *\n * Validates that URLs don't target internal/private network addresses.\n * Applied before any fetch() call in the import pipeline.\n */\n\nconst IPV4_MAPPED_IPV6_DOTTED_PATTERN = /^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/i;\nconst IPV4_MAPPED_IPV6_HEX_PATTERN = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;\nconst IPV4_TRANSLATED_HEX_PATTERN = /^::ffff:0:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;\nconst IPV6_EXPANDED_MAPPED_PATTERN =\n\t/^0{0,4}:0{0,4}:0{0,4}:0{0,4}:0{0,4}:ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;\n\n/**\n * IPv4-compatible (deprecated) addresses: ::XXXX:XXXX\n *\n * The WHATWG URL parser normalizes [::127.0.0.1] to [::7f00:1] (no ffff prefix).\n * These are deprecated but still parsed, and bypass the ffff-based checks.\n */\nconst IPV4_COMPATIBLE_HEX_PATTERN = /^::([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;\n\n/**\n * NAT64 prefix (RFC 6052): 64:ff9b::XXXX:XXXX\n *\n * Used by NAT64 gateways to embed IPv4 addresses in IPv6.\n * [64:ff9b::127.0.0.1] normalizes to [64:ff9b::7f00:1].\n */\nconst NAT64_HEX_PATTERN = /^64:ff9b::([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i;\n\nconst IPV6_BRACKET_PATTERN = /^\\[|\\]$/g;\n\n/** Match fc00::/7 ULA — first byte 0xfc or 0xfd followed by any byte. */\nconst IPV6_ULA_FC_PATTERN = /^fc[0-9a-f]{2}:/;\nconst IPV6_ULA_FD_PATTERN = /^fd[0-9a-f]{2}:/;\n\n/** Strip trailing dots from an FQDN-form hostname (\"localhost.\" -> \"localhost\"). */\nconst TRAILING_DOT_PATTERN = /\\.+$/;\n\n/**\n * Private and reserved IP ranges that should never be fetched.\n *\n * Includes:\n * - Loopback (127.0.0.0/8)\n * - Private (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)\n * - Link-local (169.254.0.0/16)\n * - Cloud metadata (169.254.169.254 — AWS/GCP/Azure)\n * - IPv6 loopback and link-local\n */\nconst BLOCKED_PATTERNS: Array<{ start: number; end: number }> = [\n\t// 127.0.0.0/8 — loopback\n\t{ start: ip4ToNum(127, 0, 0, 0), end: ip4ToNum(127, 255, 255, 255) },\n\t// 10.0.0.0/8 — private\n\t{ start: ip4ToNum(10, 0, 0, 0), end: ip4ToNum(10, 255, 255, 255) },\n\t// 172.16.0.0/12 — private\n\t{ start: ip4ToNum(172, 16, 0, 0), end: ip4ToNum(172, 31, 255, 255) },\n\t// 192.168.0.0/16 — private\n\t{ start: ip4ToNum(192, 168, 0, 0), end: ip4ToNum(192, 168, 255, 255) },\n\t// 169.254.0.0/16 — link-local (includes cloud metadata endpoint)\n\t{ start: ip4ToNum(169, 254, 0, 0), end: ip4ToNum(169, 254, 255, 255) },\n\t// 0.0.0.0/8 — current network\n\t{ start: ip4ToNum(0, 0, 0, 0), end: ip4ToNum(0, 255, 255, 255) },\n];\n\n// Bracket-stripped form is used for lookups (validateExternalUrl strips\n// brackets from parsed.hostname before checking), so \"::1\" appears here\n// without brackets. The \"::1\" case is already covered by isPrivateIp, but\n// keeping it here makes the intent explicit and gives a clearer error\n// message for the common `http://[::1]/` form.\nconst BLOCKED_HOSTNAMES = new Set([\n\t\"localhost\",\n\t\"metadata.google.internal\",\n\t\"metadata.google\",\n\t\"::1\",\n]);\n\n/**\n * Wildcard DNS services that publicly resolve arbitrary IPs embedded in the\n * hostname. Commonly used in local dev and by SSRF exploit tooling to bypass\n * hostname-only blocklists (e.g. 127.0.0.1.nip.io -> 127.0.0.1).\n *\n * Matched case-insensitively as a suffix, so both the apex and any subdomain\n * are blocked.\n */\nconst BLOCKED_HOSTNAME_SUFFIXES = [\n\t\"nip.io\",\n\t\"sslip.io\",\n\t\"xip.io\",\n\t\"traefik.me\",\n\t\"lvh.me\",\n\t\"localtest.me\",\n];\n\n/** Blocked URL schemes */\nconst ALLOWED_SCHEMES = new Set([\"http:\", \"https:\"]);\n\nfunction ip4ToNum(a: number, b: number, c: number, d: number): number {\n\treturn ((a << 24) | (b << 16) | (c << 8) | d) >>> 0;\n}\n\nfunction parseIpv4(ip: string): number | null {\n\tconst parts = ip.split(\".\");\n\tif (parts.length !== 4) return null;\n\n\tconst nums = parts.map(Number);\n\tif (nums.some((n) => isNaN(n) || n < 0 || n > 255)) return null;\n\n\treturn ip4ToNum(nums[0], nums[1], nums[2], nums[3]);\n}\n\n/**\n * Convert IPv4-mapped/translated IPv6 addresses from hex form back to IPv4.\n *\n * The WHATWG URL parser normalizes dotted-decimal to hex:\n * [::ffff:127.0.0.1] -> [::ffff:7f00:1]\n * [::ffff:169.254.169.254] -> [::ffff:a9fe:a9fe]\n *\n * Without this conversion, the hex forms bypass isPrivateIp() regex checks.\n */\nexport function normalizeIPv6MappedToIPv4(ip: string): string | null {\n\t// Match hex-form IPv4-mapped IPv6: ::ffff:XXXX:XXXX\n\tlet match = ip.match(IPV4_MAPPED_IPV6_HEX_PATTERN);\n\tif (!match) {\n\t\t// Match IPv4-translated (RFC 6052): ::ffff:0:XXXX:XXXX\n\t\tmatch = ip.match(IPV4_TRANSLATED_HEX_PATTERN);\n\t}\n\tif (!match) {\n\t\t// Match fully expanded form: 0000:0000:0000:0000:0000:ffff:XXXX:XXXX\n\t\tmatch = ip.match(IPV6_EXPANDED_MAPPED_PATTERN);\n\t}\n\tif (!match) {\n\t\t// Match IPv4-compatible (deprecated) form: ::XXXX:XXXX (no ffff prefix)\n\t\tmatch = ip.match(IPV4_COMPATIBLE_HEX_PATTERN);\n\t}\n\tif (!match) {\n\t\t// Match NAT64 prefix (RFC 6052): 64:ff9b::XXXX:XXXX\n\t\tmatch = ip.match(NAT64_HEX_PATTERN);\n\t}\n\tif (match) {\n\t\tconst high = parseInt(match[1] ?? \"\", 16);\n\t\tconst low = parseInt(match[2] ?? \"\", 16);\n\t\treturn `${(high >> 8) & 0xff}.${high & 0xff}.${(low >> 8) & 0xff}.${low & 0xff}`;\n\t}\n\treturn null;\n}\n\nfunction isPrivateIp(ip: string): boolean {\n\t// Normalize IPv6 strings to lowercase. `new URL().hostname` already\n\t// lowercases, but resolver output (from DoH or an injected resolver) may\n\t// not. Without this, \"FE80::1\" bypasses the link-local check.\n\tconst normalized = ip.toLowerCase();\n\n\t// Handle IPv6 loopback\n\tif (normalized === \"::1\" || normalized === \"::ffff:127.0.0.1\") return true;\n\n\t// Handle IPv4-mapped IPv6 in hex form (WHATWG URL parser normalizes to this)\n\t// e.g. ::ffff:7f00:1 -> 127.0.0.1, ::ffff:a9fe:a9fe -> 169.254.169.254\n\tconst hexIpv4 = normalizeIPv6MappedToIPv4(normalized);\n\tif (hexIpv4) return isPrivateIp(hexIpv4);\n\n\t// Handle IPv4-mapped IPv6 in dotted-decimal form\n\tconst v4Match = normalized.match(IPV4_MAPPED_IPV6_DOTTED_PATTERN);\n\tconst ipv4 = v4Match ? v4Match[1] : normalized;\n\n\tconst num = parseIpv4(ipv4);\n\tif (num === null) {\n\t\t// If we can't parse it, block IPv6 addresses that look internal.\n\t\t// fc00::/7 is Unique Local (first byte 0xfc or 0xfd), fe80::/10 is\n\t\t// link-local. Only match when followed by hex digit + colon to avoid\n\t\t// collisions with hypothetical non-address strings.\n\t\treturn (\n\t\t\tnormalized.startsWith(\"fe80:\") ||\n\t\t\tIPV6_ULA_FC_PATTERN.test(normalized) ||\n\t\t\tIPV6_ULA_FD_PATTERN.test(normalized)\n\t\t);\n\t}\n\n\treturn BLOCKED_PATTERNS.some((range) => num >= range.start && num <= range.end);\n}\n\n/**\n * Error thrown when SSRF protection blocks a URL.\n */\nexport class SsrfError extends Error {\n\tcode = \"SSRF_BLOCKED\" as const;\n\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"SsrfError\";\n\t}\n}\n\n/**\n * Validate that a URL is safe to fetch (not targeting internal networks).\n *\n * Checks:\n * 1. URL is well-formed with http/https scheme\n * 2. Hostname is not a known internal name (localhost, metadata endpoints)\n * 3. If hostname is an IP literal, it's not in a private range\n *\n * Note: DNS rebinding attacks are not fully mitigated (hostname could resolve\n * to a private IP). Full protection requires resolving DNS and checking the IP\n * before connecting, which needs a custom fetch implementation. This covers\n * the most common SSRF vectors.\n *\n * @throws SsrfError if the URL targets an internal address\n */\n/** Maximum number of redirects to follow in ssrfSafeFetch */\nconst MAX_REDIRECTS = 5;\n\nexport function validateExternalUrl(url: string): URL {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(url);\n\t} catch {\n\t\tthrow new SsrfError(\"Invalid URL\");\n\t}\n\n\t// Only allow http/https\n\tif (!ALLOWED_SCHEMES.has(parsed.protocol)) {\n\t\tthrow new SsrfError(`Scheme '${parsed.protocol}' is not allowed`);\n\t}\n\n\t// Strip brackets from IPv6 hostname\n\tconst hostname = parsed.hostname.replace(IPV6_BRACKET_PATTERN, \"\");\n\n\t// Normalize the hostname for blocklist matching: lowercase + strip any\n\t// trailing dots. WHATWG preserves trailing dots on .hostname, so without\n\t// this normalization \"localhost.\" and \"nip.io.\" bypass the checks.\n\tconst normalizedHost = hostname.toLowerCase().replace(TRAILING_DOT_PATTERN, \"\");\n\n\t// Check against known internal hostnames\n\tif (BLOCKED_HOSTNAMES.has(normalizedHost)) {\n\t\tthrow new SsrfError(\"URLs targeting internal hosts are not allowed\");\n\t}\n\n\t// Check against wildcard DNS services used by SSRF tooling to bypass\n\t// hostname-only checks. Match the apex and any subdomain.\n\tfor (const suffix of BLOCKED_HOSTNAME_SUFFIXES) {\n\t\tif (normalizedHost === suffix || normalizedHost.endsWith(`.${suffix}`)) {\n\t\t\tthrow new SsrfError(\"URLs targeting wildcard DNS services are not allowed\");\n\t\t}\n\t}\n\n\t// Check if hostname is an IP address in a private range. Use the\n\t// normalized form so \"127.0.0.1..\" and friends don't bypass parseIpv4\n\t// (which rejects extra trailing dots).\n\tif (isPrivateIp(normalizedHost)) {\n\t\tthrow new SsrfError(\"URLs targeting private IP addresses are not allowed\");\n\t}\n\n\treturn parsed;\n}\n\n// ---------------------------------------------------------------------------\n// DNS-aware validation\n// ---------------------------------------------------------------------------\n\n/**\n * A resolver that maps a hostname to a list of IPv4/IPv6 addresses.\n * Injectable so callers can swap in OS-level DNS on Node, stub it in tests,\n * or point to a different DoH endpoint.\n */\nexport type DnsResolver = (hostname: string) => Promise<string[]>;\n\n/**\n * Module-level default resolver. Tests can swap this with a stub so fetch\n * mocks don't see unexpected DoH round-trips. Production code should leave\n * it alone.\n */\nlet defaultResolver: DnsResolver | null = null;\n\n/** Override the default DNS resolver. Returns the previous value. */\nexport function setDefaultDnsResolver(resolver: DnsResolver | null): DnsResolver | null {\n\tconst previous = defaultResolver;\n\tdefaultResolver = resolver;\n\treturn previous;\n}\n\n/** Timeout for a single DoH request, in milliseconds. */\nconst DOH_TIMEOUT_MS = 3000;\n\n/** Default DoH endpoint — Cloudflare's public resolver. */\nconst DEFAULT_DOH_URL = \"https://cloudflare-dns.com/dns-query\";\n\ninterface DohAnswer {\n\tdata: string;\n}\n\ninterface DohResponse {\n\tStatus: number;\n\tAnswer: DohAnswer[];\n}\n\nfunction hasProperty<K extends string>(obj: unknown, key: K): obj is Record<K, unknown> {\n\treturn typeof obj === \"object\" && obj !== null && key in obj;\n}\n\n/**\n * Narrow an unknown JSON body to a DohResponse shape we can read safely.\n * Throws if the body doesn't look like a DoH response — a malformed body is\n * indistinguishable from a failure and must not be silently treated as empty.\n */\nfunction parseDohResponse(raw: unknown): DohResponse {\n\tif (!hasProperty(raw, \"Status\") || typeof raw.Status !== \"number\") {\n\t\tthrow new Error(\"DoH response missing Status field\");\n\t}\n\tconst answers: DohAnswer[] = [];\n\tif (hasProperty(raw, \"Answer\") && Array.isArray(raw.Answer)) {\n\t\tfor (const entry of raw.Answer) {\n\t\t\tif (hasProperty(entry, \"data\") && typeof entry.data === \"string\") {\n\t\t\t\tanswers.push({ data: entry.data });\n\t\t\t}\n\t\t}\n\t}\n\treturn { Status: raw.Status, Answer: answers };\n}\n\n/**\n * Resolve a hostname via DNS over HTTPS (Cloudflare). Returns all A and AAAA\n * records. Works in both Workers and Node without requiring node:dns.\n *\n * Fails closed: any network error, non-2xx response, or DNS rcode != 0\n * causes a rejected promise so the calling validator treats it as a block.\n */\nexport const cloudflareDohResolver: DnsResolver = async (hostname) => {\n\tasync function query(type: \"A\" | \"AAAA\"): Promise<string[]> {\n\t\tconst params = new URLSearchParams({ name: hostname, type });\n\t\tconst controller = new AbortController();\n\t\tconst timeout = setTimeout(() => controller.abort(), DOH_TIMEOUT_MS);\n\t\ttry {\n\t\t\tconst response = await globalThis.fetch(`${DEFAULT_DOH_URL}?${params.toString()}`, {\n\t\t\t\theaders: { Accept: \"application/dns-json\" },\n\t\t\t\tsignal: controller.signal,\n\t\t\t});\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`DoH lookup failed: ${response.status}`);\n\t\t\t}\n\t\t\tconst raw = await response.json();\n\t\t\tconst body = parseDohResponse(raw);\n\t\t\t// NXDOMAIN (3) is a legitimate \"does not exist\" — treat as empty.\n\t\t\t// Any other non-zero status (SERVFAIL=2, REFUSED=5, etc.) is\n\t\t\t// ambiguous and could be a split-view attacker hiding records\n\t\t\t// from our resolver. Fail closed.\n\t\t\tif (body.Status === 3) return [];\n\t\t\tif (body.Status !== 0) {\n\t\t\t\tthrow new Error(`DoH ${type} lookup failed: rcode=${body.Status}`);\n\t\t\t}\n\t\t\t// DoH Answer arrays often include CNAME records alongside A/AAAA\n\t\t\t// records. Their `data` is a hostname, not an IP. Filter to just\n\t\t\t// IP literals so isPrivateIp sees real addresses.\n\t\t\treturn body.Answer.map((a) => a.data).filter(isIpLiteral);\n\t\t} finally {\n\t\t\tclearTimeout(timeout);\n\t\t}\n\t}\n\n\tconst [a, aaaa] = await Promise.all([query(\"A\"), query(\"AAAA\")]);\n\treturn [...a, ...aaaa];\n};\n\n/**\n * Validate a URL and resolve its hostname to check the actual IPs against\n * the private-range blocklist. This catches DNS rebinding attacks using\n * attacker-controlled domains that publicly resolve to private addresses,\n * and wildcard DNS services like nip.io used by exploit tooling.\n *\n * Runs `validateExternalUrl` first for cheap pre-flight checks (scheme,\n * literal IP, known-bad hostnames). Then resolves the hostname and rejects\n * if ANY returned address is private.\n *\n * Fails closed: if resolution fails or returns no records, throws SsrfError.\n *\n * **Caveats.** This does NOT fully close the TOCTOU between check and\n * connect. Attacks that still work against this layer include:\n *\n * - TTL=0 rebind: authoritative server returns public IP to the check, then\n * private IP to the subsequent fetch() a few milliseconds later.\n * - Split-view via EDNS Client Subnet or source-IP inspection: the\n * authoritative server returns public IP to Cloudflare's DoH resolver and\n * private IP to the victim's own resolver (used by fetch()).\n * - Host-file overrides or split-horizon corporate DNS on self-hosted Node.\n * - Attacker-controlled rebinding services the caller has allowlisted.\n *\n * The only complete defense is a network-layer egress firewall. On\n * Cloudflare Workers, the platform fetch pipeline provides most of that.\n * On self-hosted Node, operators must restrict egress themselves.\n */\nexport async function resolveAndValidateExternalUrl(\n\turl: string,\n\toptions?: { resolver?: DnsResolver },\n): Promise<URL> {\n\tconst parsed = validateExternalUrl(url);\n\n\t// Strip brackets from IPv6 hostnames\n\tconst hostname = parsed.hostname.replace(IPV6_BRACKET_PATTERN, \"\");\n\n\t// If the hostname is already an IP literal, validateExternalUrl has\n\t// already checked it against the private-range list. Skip DNS.\n\tif (isIpLiteral(hostname)) {\n\t\treturn parsed;\n\t}\n\n\tconst resolver = options?.resolver ?? defaultResolver ?? cloudflareDohResolver;\n\n\tlet addresses: string[];\n\ttry {\n\t\taddresses = await resolver(hostname);\n\t} catch (error) {\n\t\tthrow new SsrfError(\n\t\t\t`Could not resolve hostname: ${error instanceof Error ? error.message : String(error)}`,\n\t\t);\n\t}\n\n\tif (addresses.length === 0) {\n\t\tthrow new SsrfError(\"Hostname resolved to no addresses\");\n\t}\n\n\tfor (const ip of addresses) {\n\t\tif (isPrivateIp(ip)) {\n\t\t\tthrow new SsrfError(\"Hostname resolves to a private IP address\");\n\t\t}\n\t}\n\n\treturn parsed;\n}\n\n/** True when a string looks like an IPv4 or IPv6 literal. */\nfunction isIpLiteral(host: string): boolean {\n\tif (parseIpv4(host) !== null) return true;\n\t// Very loose IPv6 heuristic — matches anything with a colon, which is\n\t// never valid in DNS hostnames, so this is safe.\n\treturn host.includes(\":\");\n}\n\n/**\n * Fetch a URL with SSRF protection on redirects.\n *\n * Uses `redirect: \"manual\"` to intercept redirects and re-validate each\n * redirect target against SSRF rules before following it. This prevents\n * an attacker from setting up an allowed external URL that redirects to\n * an internal IP (e.g. 169.254.169.254 for cloud metadata).\n *\n * @throws SsrfError if the initial URL or any redirect target is internal\n */\n/** Headers that must be stripped when a redirect crosses origins */\nconst CREDENTIAL_HEADERS = [\"authorization\", \"cookie\", \"proxy-authorization\"];\n\nexport async function ssrfSafeFetch(\n\turl: string,\n\tinit?: RequestInit,\n\toptions?: { resolver?: DnsResolver },\n): Promise<Response> {\n\tlet currentUrl = url;\n\tlet currentInit = init;\n\n\tfor (let i = 0; i <= MAX_REDIRECTS; i++) {\n\t\tawait resolveAndValidateExternalUrl(currentUrl, options);\n\n\t\tconst response = await globalThis.fetch(currentUrl, {\n\t\t\t...currentInit,\n\t\t\tredirect: \"manual\",\n\t\t});\n\n\t\t// Not a redirect -- return directly\n\t\tif (response.status < 300 || response.status >= 400) {\n\t\t\treturn response;\n\t\t}\n\n\t\t// Extract redirect target\n\t\tconst location = response.headers.get(\"Location\");\n\t\tif (!location) {\n\t\t\treturn response;\n\t\t}\n\n\t\t// Resolve relative redirects against the current URL\n\t\tconst previousOrigin = new URL(currentUrl).origin;\n\t\tcurrentUrl = new URL(location, currentUrl).href;\n\t\tconst nextOrigin = new URL(currentUrl).origin;\n\n\t\t// Strip credential headers on cross-origin redirects\n\t\tif (previousOrigin !== nextOrigin && currentInit) {\n\t\t\tcurrentInit = stripCredentialHeaders(currentInit);\n\t\t}\n\t}\n\n\tthrow new SsrfError(`Too many redirects (max ${MAX_REDIRECTS})`);\n}\n\n/**\n * Return a copy of init with credential headers removed.\n */\nexport function stripCredentialHeaders(init: RequestInit): RequestInit {\n\tif (!init.headers) return init;\n\n\tconst headers = new Headers(init.headers);\n\tfor (const name of CREDENTIAL_HEADERS) {\n\t\theaders.delete(name);\n\t}\n\n\treturn { ...init, headers };\n}\n","/**\n * Seed engine - applies seed files to database\n *\n * This is the core implementation that bootstraps an EmDash site from a seed file.\n * Apply order is critical for foreign keys and references.\n */\n\nimport { imageSize } from \"image-size\";\nimport type { Kysely } from \"kysely\";\nimport mime from \"mime/lite\";\nimport { ulid } from \"ulidx\";\n\nimport { BylineRepository } from \"../database/repositories/byline.js\";\nimport { ContentRepository } from \"../database/repositories/content.js\";\nimport { MediaRepository } from \"../database/repositories/media.js\";\nimport { RedirectRepository } from \"../database/repositories/redirect.js\";\nimport { RevisionRepository } from \"../database/repositories/revision.js\";\nimport { TaxonomyRepository } from \"../database/repositories/taxonomy.js\";\nimport { withTransaction } from \"../database/transaction.js\";\nimport type { Database } from \"../database/types.js\";\nimport type { MediaValue } from \"../fields/types.js\";\nimport { getI18nConfig } from \"../i18n/config.js\";\nimport { ssrfSafeFetch, validateExternalUrl } from \"../import/ssrf.js\";\nimport { SchemaRegistry } from \"../schema/registry.js\";\nimport { FTSManager } from \"../search/fts-manager.js\";\nimport { setSiteSettings } from \"../settings/index.js\";\nimport type { Storage } from \"../storage/types.js\";\nimport type {\n\tSeedFile,\n\tSeedApplyOptions,\n\tSeedApplyResult,\n\tSeedTaxonomyTerm,\n\tSeedMenuItem,\n\tSeedWidget,\n\tSeedMediaReference,\n} from \"./types.js\";\n\nconst FILE_EXTENSION_PATTERN = /\\.([a-z0-9]+)(?:\\?|$)/i;\nimport { validateSeed } from \"./validate.js\";\n\n/** Pattern to remove file extensions */\nconst EXTENSION_PATTERN = /\\.[^.]+$/;\n\n/** Pattern to remove query parameters */\nconst QUERY_PARAM_PATTERN = /\\?.*$/;\n\n/** Pattern to remove non-alphanumeric characters (except dash and underscore) */\nconst SANITIZE_PATTERN = /[^a-zA-Z0-9_-]/g;\n\n/** Pattern to collapse multiple hyphens */\nconst MULTIPLE_HYPHENS_PATTERN = /-+/g;\n\n/**\n * Apply a seed file to the database\n *\n * This function is idempotent - safe to run multiple times.\n *\n * @param db - Kysely database instance\n * @param seed - Seed file to apply\n * @param options - Application options\n * @returns Result summary\n */\nexport async function applySeed(\n\tdb: Kysely<Database>,\n\tseed: SeedFile,\n\toptions: SeedApplyOptions = {},\n): Promise<SeedApplyResult> {\n\t// Validate seed first\n\tconst validation = validateSeed(seed);\n\tif (!validation.valid) {\n\t\tthrow new Error(`Invalid seed file:\\n${validation.errors.join(\"\\n\")}`);\n\t}\n\n\tconst {\n\t\tincludeContent = false,\n\t\tstorage,\n\t\tskipMediaDownload = false,\n\t\tonConflict = \"skip\",\n\t} = options;\n\n\t// Result counters\n\tconst result: SeedApplyResult = {\n\t\tcollections: { created: 0, skipped: 0, updated: 0 },\n\t\tfields: { created: 0, skipped: 0, updated: 0 },\n\t\ttaxonomies: { created: 0, terms: 0 },\n\t\tbylines: { created: 0, skipped: 0, updated: 0 },\n\t\tmenus: { created: 0, items: 0 },\n\t\tredirects: { created: 0, skipped: 0, updated: 0 },\n\t\twidgetAreas: { created: 0, widgets: 0 },\n\t\tsections: { created: 0, skipped: 0, updated: 0 },\n\t\tsettings: { applied: 0 },\n\t\tcontent: { created: 0, skipped: 0, updated: 0 },\n\t\tmedia: { created: 0, skipped: 0 },\n\t};\n\n\t// Media context for $media resolution\n\tconst mediaContext: MediaContext = {\n\t\tdb,\n\t\tstorage: storage ?? null,\n\t\tskipMediaDownload,\n\t\tmediaCache: new Map(), // Cache downloaded media by URL to avoid re-downloading\n\t};\n\n\t// Apply order (critical for foreign keys and references):\n\t// 1. Site settings\n\t// 2. Collections + Fields\n\t// 3. Taxonomy definitions + Terms\n\t// 4. Content (so menu refs can resolve)\n\t// 5. Menus + Menu items (can now resolve content refs)\n\t// 6. Redirects\n\t// 7. Widget areas + Widgets\n\n\t// Track seed content IDs for reference resolution (shared across content and menus)\n\tconst seedIdMap = new Map<string, string>(); // seed id -> real entry id\n\tconst seedBylineIdMap = new Map<string, string>(); // seed byline id -> real byline id\n\n\t// 1. Site settings\n\tif (seed.settings) {\n\t\tawait setSiteSettings(seed.settings, db);\n\t\tresult.settings.applied = Object.keys(seed.settings).length;\n\t}\n\n\t// 2-3. Collections and Fields\n\tif (seed.collections) {\n\t\tconst registry = new SchemaRegistry(db);\n\n\t\tfor (const collection of seed.collections) {\n\t\t\t// Check if collection exists\n\t\t\tconst existing = await registry.getCollection(collection.slug);\n\n\t\t\tif (existing) {\n\t\t\t\tif (onConflict === \"error\") {\n\t\t\t\t\tthrow new Error(`Conflict: collection \"${collection.slug}\" already exists`);\n\t\t\t\t}\n\n\t\t\t\tif (onConflict === \"update\") {\n\t\t\t\t\tawait registry.updateCollection(collection.slug, {\n\t\t\t\t\t\tlabel: collection.label,\n\t\t\t\t\t\tlabelSingular: collection.labelSingular,\n\t\t\t\t\t\tdescription: collection.description,\n\t\t\t\t\t\ticon: collection.icon,\n\t\t\t\t\t\tsupports: collection.supports || [],\n\t\t\t\t\t\turlPattern: collection.urlPattern,\n\t\t\t\t\t\tcommentsEnabled: collection.commentsEnabled,\n\t\t\t\t\t});\n\t\t\t\t\tresult.collections.updated++;\n\n\t\t\t\t\t// Update or create fields\n\t\t\t\t\tfor (const field of collection.fields) {\n\t\t\t\t\t\tconst existingField = await registry.getField(collection.slug, field.slug);\n\t\t\t\t\t\tif (existingField) {\n\t\t\t\t\t\t\tawait registry.updateField(collection.slug, field.slug, {\n\t\t\t\t\t\t\t\tlabel: field.label,\n\t\t\t\t\t\t\t\trequired: field.required || false,\n\t\t\t\t\t\t\t\tunique: field.unique || false,\n\t\t\t\t\t\t\t\tsearchable: field.searchable || false,\n\t\t\t\t\t\t\t\tdefaultValue: field.defaultValue,\n\t\t\t\t\t\t\t\tvalidation: field.validation,\n\t\t\t\t\t\t\t\twidget: field.widget,\n\t\t\t\t\t\t\t\toptions: field.options,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tresult.fields.updated++;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tawait registry.createField(collection.slug, {\n\t\t\t\t\t\t\t\tslug: field.slug,\n\t\t\t\t\t\t\t\tlabel: field.label,\n\t\t\t\t\t\t\t\ttype: field.type,\n\t\t\t\t\t\t\t\trequired: field.required || false,\n\t\t\t\t\t\t\t\tunique: field.unique || false,\n\t\t\t\t\t\t\t\tsearchable: field.searchable || false,\n\t\t\t\t\t\t\t\tdefaultValue: field.defaultValue,\n\t\t\t\t\t\t\t\tvalidation: field.validation,\n\t\t\t\t\t\t\t\twidget: field.widget,\n\t\t\t\t\t\t\t\toptions: field.options,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tresult.fields.created++;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// skip\n\t\t\t\tresult.collections.skipped++;\n\t\t\t\tresult.fields.skipped += collection.fields.length;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Create collection\n\t\t\tawait registry.createCollection({\n\t\t\t\tslug: collection.slug,\n\t\t\t\tlabel: collection.label,\n\t\t\t\tlabelSingular: collection.labelSingular,\n\t\t\t\tdescription: collection.description,\n\t\t\t\ticon: collection.icon,\n\t\t\t\tsupports: collection.supports || [],\n\t\t\t\tsource: \"seed\",\n\t\t\t\turlPattern: collection.urlPattern,\n\t\t\t\tcommentsEnabled: collection.commentsEnabled,\n\t\t\t});\n\t\t\tresult.collections.created++;\n\n\t\t\t// Create fields\n\t\t\tfor (const field of collection.fields) {\n\t\t\t\tawait registry.createField(collection.slug, {\n\t\t\t\t\tslug: field.slug,\n\t\t\t\t\tlabel: field.label,\n\t\t\t\t\ttype: field.type,\n\t\t\t\t\trequired: field.required || false,\n\t\t\t\t\tunique: field.unique || false,\n\t\t\t\t\tsearchable: field.searchable || false,\n\t\t\t\t\tdefaultValue: field.defaultValue,\n\t\t\t\t\tvalidation: field.validation,\n\t\t\t\t\twidget: field.widget,\n\t\t\t\t\toptions: field.options,\n\t\t\t\t});\n\t\t\t\tresult.fields.created++;\n\t\t\t}\n\t\t}\n\t}\n\n\t// 4-5. Taxonomies\n\tif (seed.taxonomies) {\n\t\t// seed-local id -> resolved info, used to wire `translationOf` refs.\n\t\tconst defSeedIdMap = new Map<string, { id: string; translationGroup: string }>();\n\t\tconst termSeedIdMap = new Map<string, string>();\n\t\tconst fallbackLocale = getI18nConfig()?.defaultLocale ?? \"en\";\n\n\t\tfor (const taxonomy of seed.taxonomies) {\n\t\t\tconst defLocale = taxonomy.locale ?? fallbackLocale;\n\n\t\t\t// (name, locale) is the UNIQUE key after migration 036.\n\t\t\tconst existingDef = await db\n\t\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"name\", \"=\", taxonomy.name)\n\t\t\t\t.where(\"locale\", \"=\", defLocale)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tlet defId: string;\n\t\t\tlet defTranslationGroup: string;\n\n\t\t\tif (existingDef) {\n\t\t\t\tdefId = existingDef.id;\n\t\t\t\tdefTranslationGroup = existingDef.translation_group ?? existingDef.id;\n\t\t\t\tif (onConflict === \"error\") {\n\t\t\t\t\tthrow new Error(`Conflict: taxonomy \"${taxonomy.name}\" (${defLocale}) already exists`);\n\t\t\t\t}\n\t\t\t\tif (onConflict === \"update\") {\n\t\t\t\t\tawait db\n\t\t\t\t\t\t.updateTable(\"_emdash_taxonomy_defs\")\n\t\t\t\t\t\t.set({\n\t\t\t\t\t\t\tlabel: taxonomy.label,\n\t\t\t\t\t\t\tlabel_singular: taxonomy.labelSingular ?? null,\n\t\t\t\t\t\t\thierarchical: taxonomy.hierarchical ? 1 : 0,\n\t\t\t\t\t\t\tcollections: JSON.stringify(taxonomy.collections),\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.where(\"id\", \"=\", existingDef.id)\n\t\t\t\t\t\t.execute();\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tdefId = ulid();\n\t\t\t\tdefTranslationGroup = defId;\n\t\t\t\tif (taxonomy.translationOf) {\n\t\t\t\t\tconst source = defSeedIdMap.get(taxonomy.translationOf);\n\t\t\t\t\tif (source) defTranslationGroup = source.translationGroup;\n\t\t\t\t\telse\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`taxonomy \"${taxonomy.name}\" (${defLocale}): translationOf \"${taxonomy.translationOf}\" not found yet; minting a fresh group.`,\n\t\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tawait db\n\t\t\t\t\t.insertInto(\"_emdash_taxonomy_defs\")\n\t\t\t\t\t.values({\n\t\t\t\t\t\tid: defId,\n\t\t\t\t\t\tname: taxonomy.name,\n\t\t\t\t\t\tlabel: taxonomy.label,\n\t\t\t\t\t\tlabel_singular: taxonomy.labelSingular ?? null,\n\t\t\t\t\t\thierarchical: taxonomy.hierarchical ? 1 : 0,\n\t\t\t\t\t\tcollections: JSON.stringify(taxonomy.collections),\n\t\t\t\t\t\tlocale: defLocale,\n\t\t\t\t\t\ttranslation_group: defTranslationGroup,\n\t\t\t\t\t})\n\t\t\t\t\t.execute();\n\t\t\t\tresult.taxonomies.created++;\n\t\t\t}\n\n\t\t\tif (taxonomy.id)\n\t\t\t\tdefSeedIdMap.set(taxonomy.id, { id: defId, translationGroup: defTranslationGroup });\n\n\t\t\t// Create terms (if provided)\n\t\t\tif (taxonomy.terms && taxonomy.terms.length > 0) {\n\t\t\t\tconst termRepo = new TaxonomyRepository(db);\n\n\t\t\t\tif (taxonomy.hierarchical) {\n\t\t\t\t\tawait applyHierarchicalTerms(\n\t\t\t\t\t\ttermRepo,\n\t\t\t\t\t\ttaxonomy.name,\n\t\t\t\t\t\tdefLocale,\n\t\t\t\t\t\ttaxonomy.terms,\n\t\t\t\t\t\ttermSeedIdMap,\n\t\t\t\t\t\tresult,\n\t\t\t\t\t\tonConflict,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tfor (const term of taxonomy.terms) {\n\t\t\t\t\t\tconst termLocale = term.locale ?? defLocale;\n\t\t\t\t\t\tconst existing = await termRepo.findBySlug(taxonomy.name, term.slug, termLocale);\n\t\t\t\t\t\tif (existing) {\n\t\t\t\t\t\t\tif (onConflict === \"error\") {\n\t\t\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t\t\t`Conflict: taxonomy term \"${term.slug}\" in \"${taxonomy.name}\" (${termLocale}) already exists`,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (onConflict === \"update\") {\n\t\t\t\t\t\t\t\tawait termRepo.update(existing.id, {\n\t\t\t\t\t\t\t\t\tlabel: term.label,\n\t\t\t\t\t\t\t\t\tdata: term.description ? { description: term.description } : {},\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\tresult.taxonomies.terms++;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif (term.id) termSeedIdMap.set(term.id, existing.id);\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tconst translationOf = term.translationOf\n\t\t\t\t\t\t\t\t? termSeedIdMap.get(term.translationOf)\n\t\t\t\t\t\t\t\t: undefined;\n\t\t\t\t\t\t\tconst created = await termRepo.create({\n\t\t\t\t\t\t\t\tname: taxonomy.name,\n\t\t\t\t\t\t\t\tslug: term.slug,\n\t\t\t\t\t\t\t\tlabel: term.label,\n\t\t\t\t\t\t\t\tdata: term.description ? { description: term.description } : undefined,\n\t\t\t\t\t\t\t\tlocale: termLocale,\n\t\t\t\t\t\t\t\ttranslationOf,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tif (term.id) termSeedIdMap.set(term.id, created.id);\n\t\t\t\t\t\t\tresult.taxonomies.terms++;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// 6. Bylines\n\tif (seed.bylines) {\n\t\tconst bylineRepo = new BylineRepository(db);\n\t\tfor (const byline of seed.bylines) {\n\t\t\tconst existing = await bylineRepo.findBySlug(byline.slug);\n\t\t\tif (existing) {\n\t\t\t\tif (onConflict === \"error\") {\n\t\t\t\t\tthrow new Error(`Conflict: byline \"${byline.slug}\" already exists`);\n\t\t\t\t}\n\n\t\t\t\tif (onConflict === \"update\") {\n\t\t\t\t\tawait bylineRepo.update(existing.id, {\n\t\t\t\t\t\tdisplayName: byline.displayName,\n\t\t\t\t\t\tbio: byline.bio ?? null,\n\t\t\t\t\t\twebsiteUrl: byline.websiteUrl ?? null,\n\t\t\t\t\t\tisGuest: byline.isGuest,\n\t\t\t\t\t});\n\t\t\t\t\tseedBylineIdMap.set(byline.id, existing.id);\n\t\t\t\t\tresult.bylines.updated++;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// skip\n\t\t\t\tseedBylineIdMap.set(byline.id, existing.id);\n\t\t\t\tresult.bylines.skipped++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst created = await bylineRepo.create({\n\t\t\t\tslug: byline.slug,\n\t\t\t\tdisplayName: byline.displayName,\n\t\t\t\tbio: byline.bio ?? null,\n\t\t\t\twebsiteUrl: byline.websiteUrl ?? null,\n\t\t\t\tisGuest: byline.isGuest,\n\t\t\t});\n\t\t\tseedBylineIdMap.set(byline.id, created.id);\n\t\t\tresult.bylines.created++;\n\t\t}\n\t}\n\n\t// 7. Content (created before menus so refs can resolve)\n\tif (includeContent && seed.content) {\n\t\tconst contentRepo = new ContentRepository(db);\n\n\t\t// Create content entries\n\t\tfor (const [collectionSlug, entries] of Object.entries(seed.content)) {\n\t\t\tfor (const entry of entries) {\n\t\t\t\t// Check if entry exists (by slug + locale for locale-aware lookup)\n\t\t\t\tconst existing = await contentRepo.findBySlug(collectionSlug, entry.slug, entry.locale);\n\n\t\t\t\tif (existing) {\n\t\t\t\t\tif (onConflict === \"error\") {\n\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t`Conflict: content \"${entry.slug}\" in \"${collectionSlug}\" already exists`,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\n\t\t\t\t\tif (onConflict === \"update\") {\n\t\t\t\t\t\t// Resolve $ref and $media in data\n\t\t\t\t\t\tconst resolvedData = await resolveReferences(\n\t\t\t\t\t\t\tentry.data,\n\t\t\t\t\t\t\tseedIdMap,\n\t\t\t\t\t\t\tmediaContext,\n\t\t\t\t\t\t\tresult,\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// Update content + bylines + taxonomies atomically\n\t\t\t\t\t\tconst status = entry.status || \"published\";\n\t\t\t\t\t\tawait withTransaction(db, async (trx) => {\n\t\t\t\t\t\t\tconst trxContentRepo = new ContentRepository(trx);\n\t\t\t\t\t\t\tconst trxBylineRepo = new BylineRepository(trx);\n\t\t\t\t\t\t\tconst trxRevisionRepo = new RevisionRepository(trx);\n\n\t\t\t\t\t\t\tawait trxContentRepo.update(collectionSlug, existing.id, {\n\t\t\t\t\t\t\t\tstatus,\n\t\t\t\t\t\t\t\tdata: resolvedData,\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\tawait applyContentBylines(\n\t\t\t\t\t\t\t\ttrxBylineRepo,\n\t\t\t\t\t\t\t\tcollectionSlug,\n\t\t\t\t\t\t\t\texisting.id,\n\t\t\t\t\t\t\t\tentry,\n\t\t\t\t\t\t\t\tseedBylineIdMap,\n\t\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tawait applyContentTaxonomies(trx, collectionSlug, existing.id, entry, true);\n\n\t\t\t\t\t\t\t// Seed is declarative — when status is \"published\", promote to a live\n\t\t\t\t\t\t\t// revision so the admin UI shows \"Unpublish\" instead of \"Save & Publish\"\n\t\t\t\t\t\t\t// and `live_revision_id` is populated for downstream queries.\n\t\t\t\t\t\t\t//\n\t\t\t\t\t\t\t// Create a fresh revision from the updated data and stage it as the\n\t\t\t\t\t\t\t// draft so `publish()` picks it up instead of re-syncing stale data\n\t\t\t\t\t\t\t// from an existing live revision.\n\t\t\t\t\t\t\tif (status === \"published\") {\n\t\t\t\t\t\t\t\tconst draft = await trxRevisionRepo.create({\n\t\t\t\t\t\t\t\t\tcollection: collectionSlug,\n\t\t\t\t\t\t\t\t\tentryId: existing.id,\n\t\t\t\t\t\t\t\t\tdata: resolvedData,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\tawait trxContentRepo.setDraftRevision(collectionSlug, existing.id, draft.id);\n\t\t\t\t\t\t\t\tawait trxContentRepo.publish(collectionSlug, existing.id);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tseedIdMap.set(entry.id, existing.id);\n\t\t\t\t\t\tresult.content.updated++;\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// skip\n\t\t\t\t\tresult.content.skipped++;\n\t\t\t\t\tseedIdMap.set(entry.id, existing.id);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// Resolve $ref and $media in data\n\t\t\t\tconst resolvedData = await resolveReferences(entry.data, seedIdMap, mediaContext, result);\n\n\t\t\t\t// Resolve translationOf: map from seed-local ID to real EmDash ID\n\t\t\t\tlet translationOf: string | undefined;\n\t\t\t\tif (entry.translationOf) {\n\t\t\t\t\tconst sourceId = seedIdMap.get(entry.translationOf);\n\t\t\t\t\tif (!sourceId) {\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`content.${collectionSlug}: translationOf \"${entry.translationOf}\" not found (not yet created or missing). Skipping translation link.`,\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\ttranslationOf = sourceId;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Create entry + bylines + taxonomies atomically\n\t\t\t\tconst status = entry.status || \"published\";\n\t\t\t\tconst created = await withTransaction(db, async (trx) => {\n\t\t\t\t\tconst trxContentRepo = new ContentRepository(trx);\n\t\t\t\t\tconst trxBylineRepo = new BylineRepository(trx);\n\n\t\t\t\t\tconst item = await trxContentRepo.create({\n\t\t\t\t\t\ttype: collectionSlug,\n\t\t\t\t\t\tslug: entry.slug,\n\t\t\t\t\t\tstatus,\n\t\t\t\t\t\tdata: resolvedData,\n\t\t\t\t\t\tlocale: entry.locale,\n\t\t\t\t\t\ttranslationOf,\n\t\t\t\t\t\tpublishedAt: status === \"published\" ? new Date().toISOString() : null,\n\t\t\t\t\t});\n\n\t\t\t\t\tawait applyContentBylines(trxBylineRepo, collectionSlug, item.id, entry, seedBylineIdMap);\n\t\t\t\t\tawait applyContentTaxonomies(trx, collectionSlug, item.id, entry, false);\n\n\t\t\t\t\t// Seed is declarative — when status is \"published\", promote to a live\n\t\t\t\t\t// revision so the admin UI shows \"Unpublish\" instead of \"Save & Publish\"\n\t\t\t\t\t// and `live_revision_id` is populated for downstream queries.\n\t\t\t\t\tif (status === \"published\") {\n\t\t\t\t\t\tawait trxContentRepo.publish(collectionSlug, item.id);\n\t\t\t\t\t}\n\n\t\t\t\t\treturn item;\n\t\t\t\t});\n\n\t\t\t\tseedIdMap.set(entry.id, created.id);\n\t\t\t\tresult.content.created++;\n\t\t\t}\n\t\t}\n\t}\n\n\t// 8. Menus and Menu Items (after content so refs can resolve)\n\tif (seed.menus) {\n\t\t// seed-local id -> resolved info, used to wire `translationOf` refs.\n\t\tconst menuSeedIdMap = new Map<string, { id: string; translationGroup: string }>();\n\t\t// Shared across menus: translated items reference anchor items in sibling menus.\n\t\tconst itemSeedIdMap = new Map<string, { id: string; translationGroup: string }>();\n\t\tconst fallbackLocale = getI18nConfig()?.defaultLocale ?? \"en\";\n\n\t\tfor (const menu of seed.menus) {\n\t\t\tconst locale = menu.locale ?? fallbackLocale;\n\t\t\tlet lookup = db\n\t\t\t\t.selectFrom(\"_emdash_menus\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"name\", \"=\", menu.name)\n\t\t\t\t.where(\"locale\", \"=\", locale);\n\t\t\tconst existingMenu = await lookup.executeTakeFirst();\n\n\t\t\tlet menuId: string;\n\t\t\tlet translationGroup: string;\n\n\t\t\tif (existingMenu) {\n\t\t\t\tmenuId = existingMenu.id;\n\t\t\t\ttranslationGroup = existingMenu.translation_group ?? existingMenu.id;\n\t\t\t\t// Clear existing items (menus are recreated)\n\t\t\t\tawait db.deleteFrom(\"_emdash_menu_items\").where(\"menu_id\", \"=\", menuId).execute();\n\t\t\t} else {\n\t\t\t\tmenuId = ulid();\n\t\t\t\t// Resolve translationOf to the source menu's translation_group.\n\t\t\t\ttranslationGroup = menuId;\n\t\t\t\tif (menu.translationOf) {\n\t\t\t\t\tconst source = menuSeedIdMap.get(menu.translationOf);\n\t\t\t\t\tif (source) translationGroup = source.translationGroup;\n\t\t\t\t\telse\n\t\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t\t`menu \"${menu.name}\" (${locale}): translationOf \"${menu.translationOf}\" not found yet; minting a fresh group.`,\n\t\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tawait db\n\t\t\t\t\t.insertInto(\"_emdash_menus\")\n\t\t\t\t\t.values({\n\t\t\t\t\t\tid: menuId,\n\t\t\t\t\t\tname: menu.name,\n\t\t\t\t\t\tlabel: menu.label,\n\t\t\t\t\t\tcreated_at: new Date().toISOString(),\n\t\t\t\t\t\tupdated_at: new Date().toISOString(),\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t\ttranslation_group: translationGroup,\n\t\t\t\t\t})\n\t\t\t\t\t.execute();\n\t\t\t\tresult.menus.created++;\n\t\t\t}\n\n\t\t\tif (menu.id) menuSeedIdMap.set(menu.id, { id: menuId, translationGroup });\n\n\t\t\t// Create menu items\n\t\t\tconst itemCount = await applyMenuItems(\n\t\t\t\tdb,\n\t\t\t\tmenuId,\n\t\t\t\tlocale,\n\t\t\t\tmenu.items,\n\t\t\t\tnull, // parent_id\n\t\t\t\t0, // sort_order\n\t\t\t\tseedIdMap,\n\t\t\t\titemSeedIdMap,\n\t\t\t);\n\t\t\tresult.menus.items += itemCount;\n\t\t}\n\t}\n\n\t// 9. Redirects\n\tif (seed.redirects) {\n\t\tconst redirectRepo = new RedirectRepository(db);\n\n\t\tfor (const redirect of seed.redirects) {\n\t\t\tconst existing = await redirectRepo.findBySource(redirect.source);\n\t\t\tif (existing) {\n\t\t\t\tif (onConflict === \"error\") {\n\t\t\t\t\tthrow new Error(`Conflict: redirect \"${redirect.source}\" already exists`);\n\t\t\t\t}\n\n\t\t\t\tif (onConflict === \"update\") {\n\t\t\t\t\tawait redirectRepo.update(existing.id, {\n\t\t\t\t\t\tdestination: redirect.destination,\n\t\t\t\t\t\ttype: redirect.type,\n\t\t\t\t\t\tenabled: redirect.enabled,\n\t\t\t\t\t\tgroupName: redirect.groupName,\n\t\t\t\t\t});\n\t\t\t\t\tresult.redirects.updated++;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// skip\n\t\t\t\tresult.redirects.skipped++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tawait redirectRepo.create({\n\t\t\t\tsource: redirect.source,\n\t\t\t\tdestination: redirect.destination,\n\t\t\t\ttype: redirect.type,\n\t\t\t\tenabled: redirect.enabled,\n\t\t\t\tgroupName: redirect.groupName,\n\t\t\t});\n\t\t\tresult.redirects.created++;\n\t\t}\n\t}\n\n\t// 10. Widget Areas and Widgets\n\tif (seed.widgetAreas) {\n\t\tfor (const area of seed.widgetAreas) {\n\t\t\t// Check if area exists\n\t\t\tconst existingArea = await db\n\t\t\t\t.selectFrom(\"_emdash_widget_areas\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"name\", \"=\", area.name)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tlet areaId: string;\n\n\t\t\tif (existingArea) {\n\t\t\t\tareaId = existingArea.id;\n\t\t\t\t// Clear existing widgets (areas are recreated)\n\t\t\t\tawait db.deleteFrom(\"_emdash_widgets\").where(\"area_id\", \"=\", areaId).execute();\n\t\t\t} else {\n\t\t\t\t// Create area\n\t\t\t\tareaId = ulid();\n\t\t\t\tawait db\n\t\t\t\t\t.insertInto(\"_emdash_widget_areas\")\n\t\t\t\t\t.values({\n\t\t\t\t\t\tid: areaId,\n\t\t\t\t\t\tname: area.name,\n\t\t\t\t\t\tlabel: area.label,\n\t\t\t\t\t\tdescription: area.description ?? null,\n\t\t\t\t\t})\n\t\t\t\t\t.execute();\n\t\t\t\tresult.widgetAreas.created++;\n\t\t\t}\n\n\t\t\t// Create widgets\n\t\t\tfor (let i = 0; i < area.widgets.length; i++) {\n\t\t\t\tconst widget = area.widgets[i];\n\t\t\t\tawait applyWidget(db, areaId, widget, i);\n\t\t\t\tresult.widgetAreas.widgets++;\n\t\t\t}\n\t\t}\n\t}\n\n\t// 11. Sections\n\tif (seed.sections) {\n\t\tfor (const section of seed.sections) {\n\t\t\t// Check if section exists\n\t\t\tconst existing = await db\n\t\t\t\t.selectFrom(\"_emdash_sections\")\n\t\t\t\t.select(\"id\")\n\t\t\t\t.where(\"slug\", \"=\", section.slug)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (existing) {\n\t\t\t\tif (onConflict === \"error\") {\n\t\t\t\t\tthrow new Error(`Conflict: section \"${section.slug}\" already exists`);\n\t\t\t\t}\n\n\t\t\t\tif (onConflict === \"update\") {\n\t\t\t\t\tawait db\n\t\t\t\t\t\t.updateTable(\"_emdash_sections\")\n\t\t\t\t\t\t.set({\n\t\t\t\t\t\t\ttitle: section.title,\n\t\t\t\t\t\t\tdescription: section.description ?? null,\n\t\t\t\t\t\t\tkeywords: section.keywords ? JSON.stringify(section.keywords) : null,\n\t\t\t\t\t\t\tcontent: JSON.stringify(section.content),\n\t\t\t\t\t\t\tsource: section.source || \"theme\",\n\t\t\t\t\t\t\tupdated_at: new Date().toISOString(),\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.where(\"id\", \"=\", existing.id)\n\t\t\t\t\t\t.execute();\n\t\t\t\t\tresult.sections.updated++;\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\t// skip\n\t\t\t\tresult.sections.skipped++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst id = ulid();\n\t\t\tconst now = new Date().toISOString();\n\n\t\t\tawait db\n\t\t\t\t.insertInto(\"_emdash_sections\")\n\t\t\t\t.values({\n\t\t\t\t\tid,\n\t\t\t\t\tslug: section.slug,\n\t\t\t\t\ttitle: section.title,\n\t\t\t\t\tdescription: section.description ?? null,\n\t\t\t\t\tkeywords: section.keywords ? JSON.stringify(section.keywords) : null,\n\t\t\t\t\tcontent: JSON.stringify(section.content),\n\t\t\t\t\tpreview_media_id: null,\n\t\t\t\t\tsource: section.source || \"theme\",\n\t\t\t\t\ttheme_id: section.source === \"theme\" ? section.slug : null,\n\t\t\t\t\tcreated_at: now,\n\t\t\t\t\tupdated_at: now,\n\t\t\t\t})\n\t\t\t\t.execute();\n\n\t\t\tresult.sections.created++;\n\t\t}\n\t}\n\n\t// 11. Enable search for collections that have `search` in supports\n\tif (seed.collections) {\n\t\tconst ftsManager = new FTSManager(db);\n\n\t\tfor (const collection of seed.collections) {\n\t\t\tif (collection.supports?.includes(\"search\")) {\n\t\t\t\t// Check if there are searchable fields\n\t\t\t\tconst searchableFields = await ftsManager.getSearchableFields(collection.slug);\n\t\t\t\tif (searchableFields.length > 0) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait ftsManager.enableSearch(collection.slug);\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t// Log but don't fail - search can be enabled manually later\n\t\t\t\t\t\tconsole.warn(`Failed to enable search for ${collection.slug}:`, err);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Invalidate caches that may have been affected by seed data.\n\t// Seed creates bylines, redirects, and collections, all of which\n\t// have module-level caches in the hot path.\n\tconst { invalidateBylineCache } = await import(\"../bylines/index.js\");\n\tconst { invalidateRedirectCache } = await import(\"../redirects/cache.js\");\n\tconst { invalidateUrlPatternCache } = await import(\"../query.js\");\n\tinvalidateBylineCache();\n\tinvalidateRedirectCache();\n\tinvalidateUrlPatternCache();\n\n\treturn result;\n}\n\n/**\n * Apply hierarchical taxonomy terms (parents before children)\n */\nasync function applyHierarchicalTerms(\n\ttermRepo: TaxonomyRepository,\n\ttaxonomyName: string,\n\tdefLocale: string,\n\tterms: SeedTaxonomyTerm[],\n\ttermSeedIdMap: Map<string, string>,\n\tresult: SeedApplyResult,\n\tonConflict: \"skip\" | \"update\" | \"error\" = \"skip\",\n): Promise<void> {\n\t// \"locale::slug\" -> id, so the same slug can resolve per locale.\n\tconst slugToId = new Map<string, string>();\n\n\t// Multiple passes — handles deep nesting and translationOf forward refs.\n\tlet remaining = [...terms];\n\tlet maxPasses = 10;\n\n\twhile (remaining.length > 0 && maxPasses > 0) {\n\t\tconst processedThisPass: string[] = [];\n\n\t\tfor (const term of remaining) {\n\t\t\tconst termLocale = term.locale ?? defLocale;\n\t\t\tconst parentReady = !term.parent || slugToId.has(`${termLocale}::${term.parent}`);\n\t\t\tconst translationReady = !term.translationOf || termSeedIdMap.has(term.translationOf);\n\n\t\t\tif (!parentReady || !translationReady) continue;\n\n\t\t\tconst parentId = term.parent ? slugToId.get(`${termLocale}::${term.parent}`) : undefined;\n\t\t\tconst translationOf = term.translationOf ? termSeedIdMap.get(term.translationOf) : undefined;\n\n\t\t\tconst existing = await termRepo.findBySlug(taxonomyName, term.slug, termLocale);\n\t\t\tif (existing) {\n\t\t\t\tif (onConflict === \"error\") {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Conflict: taxonomy term \"${term.slug}\" in \"${taxonomyName}\" (${termLocale}) already exists`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (onConflict === \"update\") {\n\t\t\t\t\tawait termRepo.update(existing.id, {\n\t\t\t\t\t\tlabel: term.label,\n\t\t\t\t\t\tparentId,\n\t\t\t\t\t\tdata: term.description ? { description: term.description } : {},\n\t\t\t\t\t});\n\t\t\t\t\tresult.taxonomies.terms++;\n\t\t\t\t}\n\t\t\t\tslugToId.set(`${termLocale}::${term.slug}`, existing.id);\n\t\t\t\tif (term.id) termSeedIdMap.set(term.id, existing.id);\n\t\t\t} else {\n\t\t\t\tconst created = await termRepo.create({\n\t\t\t\t\tname: taxonomyName,\n\t\t\t\t\tslug: term.slug,\n\t\t\t\t\tlabel: term.label,\n\t\t\t\t\tparentId,\n\t\t\t\t\tdata: term.description ? { description: term.description } : undefined,\n\t\t\t\t\tlocale: termLocale,\n\t\t\t\t\ttranslationOf,\n\t\t\t\t});\n\t\t\t\tslugToId.set(`${termLocale}::${term.slug}`, created.id);\n\t\t\t\tif (term.id) termSeedIdMap.set(term.id, created.id);\n\t\t\t\tresult.taxonomies.terms++;\n\t\t\t}\n\n\t\t\tprocessedThisPass.push(term.slug + \"::\" + termLocale);\n\t\t}\n\n\t\tremaining = remaining.filter(\n\t\t\t(t) => !processedThisPass.includes(t.slug + \"::\" + (t.locale ?? defLocale)),\n\t\t);\n\t\tmaxPasses--;\n\t}\n\n\tif (remaining.length > 0) {\n\t\tconsole.warn(`Could not process ${remaining.length} terms due to missing parents/translations`);\n\t}\n}\n\n/**\n * Apply byline credits to a content entry.\n * In update mode, clears existing credits even if the seed has none.\n */\nasync function applyContentBylines(\n\tbylineRepo: BylineRepository,\n\tcollectionSlug: string,\n\tcontentId: string,\n\tentry: { slug: string; bylines?: Array<{ byline: string; roleLabel?: string }> },\n\tseedBylineIdMap: Map<string, string>,\n\tisUpdate = false,\n): Promise<void> {\n\tif (!entry.bylines || entry.bylines.length === 0) {\n\t\t// In update mode, clear existing bylines when the seed entry has none\n\t\tif (isUpdate) {\n\t\t\tawait bylineRepo.setContentBylines(collectionSlug, contentId, []);\n\t\t}\n\t\treturn;\n\t}\n\n\tconst credits = entry.bylines\n\t\t.map((credit) => {\n\t\t\tconst bylineId = seedBylineIdMap.get(credit.byline);\n\t\t\tif (!bylineId) return null;\n\t\t\treturn {\n\t\t\t\tbylineId,\n\t\t\t\troleLabel: credit.roleLabel ?? null,\n\t\t\t};\n\t\t})\n\t\t.filter((credit): credit is { bylineId: string; roleLabel: string | null } => Boolean(credit));\n\n\tif (credits.length !== entry.bylines.length) {\n\t\tconsole.warn(\n\t\t\t`content.${collectionSlug}.${entry.slug}: one or more byline refs could not be resolved`,\n\t\t);\n\t}\n\n\t// In update mode, always call setContentBylines (even with empty credits)\n\t// to clear stale assignments when all byline refs fail to resolve.\n\t// In create mode, only call if there are credits to assign.\n\tif (credits.length > 0 || isUpdate) {\n\t\tawait bylineRepo.setContentBylines(collectionSlug, contentId, credits);\n\t}\n}\n\n/**\n * Apply taxonomy term assignments to a content entry.\n * In update mode, clears existing assignments before re-attaching.\n */\nasync function applyContentTaxonomies(\n\tdb: Kysely<Database>,\n\tcollectionSlug: string,\n\tcontentId: string,\n\tentry: { taxonomies?: Record<string, string[]> },\n\tisUpdate: boolean,\n): Promise<void> {\n\t// In update mode, clear existing taxonomy assignments first\n\tif (isUpdate) {\n\t\tawait db\n\t\t\t.deleteFrom(\"content_taxonomies\")\n\t\t\t.where(\"collection\", \"=\", collectionSlug)\n\t\t\t.where(\"entry_id\", \"=\", contentId)\n\t\t\t.execute();\n\t}\n\n\tif (!entry.taxonomies) {\n\t\t// In update mode we may have just deleted rows above; invalidate so\n\t\t// hydration doesn't serve stale \"has terms\" cached value.\n\t\tif (isUpdate) {\n\t\t\tconst { invalidateTermCache } = await import(\"../taxonomies/index.js\");\n\t\t\tinvalidateTermCache();\n\t\t}\n\t\treturn;\n\t}\n\n\tfor (const [taxonomyName, termSlugs] of Object.entries(entry.taxonomies)) {\n\t\tconst termRepo = new TaxonomyRepository(db);\n\n\t\tfor (const termSlug of termSlugs) {\n\t\t\tconst term = await termRepo.findBySlug(taxonomyName, termSlug);\n\t\t\tif (term) {\n\t\t\t\tawait termRepo.attachToEntry(collectionSlug, contentId, term.id);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Seed writes directly to content_taxonomies. Clear the cache so\n\t// the worker lifetime cached \"has any term assignments\" probe\n\t// re-runs on the next read.\n\tconst { invalidateTermCache } = await import(\"../taxonomies/index.js\");\n\tinvalidateTermCache();\n}\n\n/**\n * Apply menu items recursively.\n *\n * When a `SeedMenuItem` carries `id`/`translationOf`, the import resolves the\n * source item's `translation_group` so cross-locale \"same nav entry\" links\n * survive export → apply. Items without `translationOf` get a fresh group\n * (= their own id).\n */\nasync function applyMenuItems(\n\tdb: Kysely<Database>,\n\tmenuId: string,\n\tlocale: string,\n\titems: SeedMenuItem[],\n\tparentId: string | null,\n\tstartOrder: number,\n\tseedIdMap: Map<string, string>,\n\titemSeedIdMap: Map<string, { id: string; translationGroup: string }>,\n): Promise<number> {\n\tlet count = 0;\n\tlet order = startOrder;\n\n\tfor (const item of items) {\n\t\tconst itemId = ulid();\n\t\tconst itemLocale = item.locale ?? locale;\n\n\t\t// Resolve reference if needed\n\t\tlet referenceId: string | null = null;\n\t\tlet referenceCollection: string | null = null;\n\n\t\tif (item.type === \"page\" || item.type === \"post\") {\n\t\t\t// Try to resolve from seedIdMap\n\t\t\tif (item.ref && seedIdMap.has(item.ref)) {\n\t\t\t\treferenceId = seedIdMap.get(item.ref)!;\n\t\t\t\t// Default to plural collection name (pages/posts) if not specified\n\t\t\t\treferenceCollection = item.collection || `${item.type}s`;\n\t\t\t}\n\t\t\t// If not in map, the content might not exist yet (will be broken link)\n\t\t}\n\n\t\tlet translationGroup = itemId;\n\t\tif (item.translationOf) {\n\t\t\tconst source = itemSeedIdMap.get(item.translationOf);\n\t\t\tif (source) translationGroup = source.translationGroup;\n\t\t\telse\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`menu item \"${item.label ?? item.url ?? item.ref ?? \"(unlabeled)\"}\" (${itemLocale}): translationOf \"${item.translationOf}\" not found yet; minting a fresh group.`,\n\t\t\t\t);\n\t\t}\n\n\t\tawait db\n\t\t\t.insertInto(\"_emdash_menu_items\")\n\t\t\t.values({\n\t\t\t\tid: itemId,\n\t\t\t\tmenu_id: menuId,\n\t\t\t\tparent_id: parentId,\n\t\t\t\tsort_order: order,\n\t\t\t\ttype: item.type,\n\t\t\t\treference_collection: referenceCollection,\n\t\t\t\treference_id: referenceId,\n\t\t\t\tcustom_url: item.url ?? null,\n\t\t\t\tlabel: item.label || \"\",\n\t\t\t\ttitle_attr: item.titleAttr ?? null,\n\t\t\t\ttarget: item.target ?? null,\n\t\t\t\tcss_classes: item.cssClasses ?? null,\n\t\t\t\tcreated_at: new Date().toISOString(),\n\t\t\t\tlocale: itemLocale,\n\t\t\t\ttranslation_group: translationGroup,\n\t\t\t})\n\t\t\t.execute();\n\n\t\tif (item.id) itemSeedIdMap.set(item.id, { id: itemId, translationGroup });\n\n\t\tcount++;\n\t\torder++;\n\n\t\tif (item.children && item.children.length > 0) {\n\t\t\tconst childCount = await applyMenuItems(\n\t\t\t\tdb,\n\t\t\t\tmenuId,\n\t\t\t\titemLocale,\n\t\t\t\titem.children,\n\t\t\t\titemId,\n\t\t\t\t0,\n\t\t\t\tseedIdMap,\n\t\t\t\titemSeedIdMap,\n\t\t\t);\n\t\t\tcount += childCount;\n\t\t}\n\t}\n\n\treturn count;\n}\n\n/**\n * Apply a widget\n */\nasync function applyWidget(\n\tdb: Kysely<Database>,\n\tareaId: string,\n\twidget: SeedWidget,\n\tsortOrder: number,\n): Promise<void> {\n\tawait db\n\t\t.insertInto(\"_emdash_widgets\")\n\t\t.values({\n\t\t\tid: ulid(),\n\t\t\tarea_id: areaId,\n\t\t\tsort_order: sortOrder,\n\t\t\ttype: widget.type,\n\t\t\ttitle: widget.title ?? null,\n\t\t\t// `widget.content` is Portable Text for content-type widgets;\n\t\t\t// for other widget kinds it's null.\n\t\t\tcontent: widget.content ? JSON.stringify(widget.content) : null,\n\t\t\tmenu_name: widget.menuName ?? null,\n\t\t\tcomponent_id: widget.componentId ?? null,\n\t\t\tcomponent_props: widget.props ? JSON.stringify(widget.props) : null,\n\t\t})\n\t\t.execute();\n}\n\n/**\n * Context for media resolution during seed application\n */\ninterface MediaContext {\n\tdb: Kysely<Database>;\n\tstorage: Storage | null;\n\tskipMediaDownload: boolean;\n\tmediaCache: Map<string, MediaValue>; // URL -> resolved MediaValue\n}\n\n/**\n * Type guard for $media reference\n */\nfunction isSeedMediaReference(value: unknown): value is SeedMediaReference {\n\tif (typeof value !== \"object\" || value === null || !(\"$media\" in value)) {\n\t\treturn false;\n\t}\n\tconst media = (value as Record<string, unknown>).$media;\n\treturn (\n\t\ttypeof media === \"object\" &&\n\t\tmedia !== null &&\n\t\t\"url\" in media &&\n\t\ttypeof (media as Record<string, unknown>).url === \"string\"\n\t);\n}\n\n/**\n * Resolve $ref: and $media references in content data\n */\nasync function resolveReferences(\n\tdata: Record<string, unknown>,\n\tseedIdMap: Map<string, string>,\n\tmediaContext: MediaContext,\n\tresult: SeedApplyResult,\n): Promise<Record<string, unknown>> {\n\tconst resolved: Record<string, unknown> = {};\n\n\tfor (const [key, value] of Object.entries(data)) {\n\t\tresolved[key] = await resolveValue(value, seedIdMap, mediaContext, result);\n\t}\n\n\treturn resolved;\n}\n\n/**\n * Resolve a single value recursively\n */\nasync function resolveValue(\n\tvalue: unknown,\n\tseedIdMap: Map<string, string>,\n\tmediaContext: MediaContext,\n\tresult: SeedApplyResult,\n): Promise<unknown> {\n\t// Handle $ref: syntax\n\tif (typeof value === \"string\" && value.startsWith(\"$ref:\")) {\n\t\tconst seedId = value.slice(5);\n\t\treturn seedIdMap.get(seedId) ?? value; // Return unresolved if not found\n\t}\n\n\t// Handle $media syntax\n\tif (isSeedMediaReference(value)) {\n\t\treturn resolveMedia(value, mediaContext, result);\n\t}\n\n\t// Handle arrays\n\tif (Array.isArray(value)) {\n\t\treturn Promise.all(value.map((item) => resolveValue(item, seedIdMap, mediaContext, result)));\n\t}\n\n\t// Handle objects recursively\n\tif (typeof value === \"object\" && value !== null) {\n\t\tconst resolved: Record<string, unknown> = {};\n\t\tfor (const [k, v] of Object.entries(value)) {\n\t\t\tresolved[k] = await resolveValue(v, seedIdMap, mediaContext, result);\n\t\t}\n\t\treturn resolved;\n\t}\n\n\treturn value;\n}\n\n/**\n * Resolve a $media reference by downloading and uploading the media\n */\nasync function resolveMedia(\n\tref: SeedMediaReference,\n\tctx: MediaContext,\n\tresult: SeedApplyResult,\n): Promise<MediaValue | null> {\n\tconst { url, alt, filename, caption } = ref.$media;\n\n\t// Check cache first\n\tconst cached = ctx.mediaCache.get(url);\n\tif (cached) {\n\t\tresult.media.skipped++;\n\t\treturn { ...cached, alt: alt ?? cached.alt };\n\t}\n\n\t// When skipMediaDownload is set, resolve $media to an external URL reference\n\t// without downloading or storing anything. Used by playground mode.\n\tif (ctx.skipMediaDownload) {\n\t\tconst mediaValue: MediaValue = {\n\t\t\tprovider: \"external\",\n\t\t\tid: ulid(),\n\t\t\tsrc: url,\n\t\t\talt: alt ?? undefined,\n\t\t\tfilename: filename ?? undefined,\n\t\t};\n\t\tctx.mediaCache.set(url, mediaValue);\n\t\tresult.media.created++;\n\t\treturn mediaValue;\n\t}\n\n\t// Storage is required for $media resolution\n\tif (!ctx.storage) {\n\t\tconsole.warn(`Skipping $media reference (no storage configured): ${url}`);\n\t\tresult.media.skipped++;\n\t\treturn null;\n\t}\n\n\ttry {\n\t\t// SSRF protection: validate URL before downloading\n\t\tvalidateExternalUrl(url);\n\n\t\t// Download the media (ssrfSafeFetch re-validates redirect targets)\n\t\tconsole.log(` 📥 Downloading: ${url}`);\n\t\tconst response = await ssrfSafeFetch(url, {\n\t\t\theaders: {\n\t\t\t\t// Some services like Unsplash require a user-agent\n\t\t\t\t\"User-Agent\": \"EmDash-CMS/1.0\",\n\t\t\t},\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconsole.warn(` ⚠️ Failed to download ${url}: ${response.status}`);\n\t\t\tresult.media.skipped++;\n\t\t\treturn null;\n\t\t}\n\n\t\t// Get content type and determine extension\n\t\tconst contentType = response.headers.get(\"content-type\") || \"application/octet-stream\";\n\t\tconst ext = getExtensionFromContentType(contentType) || getExtensionFromUrl(url) || \".bin\";\n\n\t\t// Generate filename and storage key\n\t\tconst id = ulid();\n\t\tconst finalFilename = filename || generateFilename(url, ext);\n\t\tconst storageKey = `${id}${ext}`;\n\n\t\t// Get the body as buffer\n\t\tconst arrayBuffer = await response.arrayBuffer();\n\t\tconst body = new Uint8Array(arrayBuffer);\n\n\t\t// Get image dimensions if it's an image\n\t\tlet width: number | undefined;\n\t\tlet height: number | undefined;\n\t\tif (contentType.startsWith(\"image/\")) {\n\t\t\tconst dimensions = getImageDimensions(body);\n\t\t\twidth = dimensions?.width;\n\t\t\theight = dimensions?.height;\n\t\t}\n\n\t\t// Upload to storage\n\t\tawait ctx.storage.upload({\n\t\t\tkey: storageKey,\n\t\t\tbody,\n\t\t\tcontentType,\n\t\t});\n\n\t\t// Create media record\n\t\tconst mediaRepo = new MediaRepository(ctx.db);\n\t\tawait mediaRepo.create({\n\t\t\tfilename: finalFilename,\n\t\t\tmimeType: contentType,\n\t\t\tsize: body.length,\n\t\t\twidth,\n\t\t\theight,\n\t\t\talt,\n\t\t\tcaption,\n\t\t\tstorageKey,\n\t\t\tstatus: \"ready\",\n\t\t});\n\n\t\t// Create the MediaValue - only store id, URL is built at runtime by EmDashMedia\n\t\tconst mediaValue: MediaValue = {\n\t\t\tprovider: \"local\",\n\t\t\tid,\n\t\t\talt: alt ?? undefined,\n\t\t\twidth,\n\t\t\theight,\n\t\t\tmimeType: contentType,\n\t\t\tfilename: finalFilename,\n\t\t\tmeta: { storageKey },\n\t\t};\n\n\t\t// Cache for reuse\n\t\tctx.mediaCache.set(url, mediaValue);\n\t\tresult.media.created++;\n\n\t\tconsole.log(` ✅ Uploaded: ${finalFilename}`);\n\t\treturn mediaValue;\n\t} catch (error) {\n\t\tconsole.warn(\n\t\t\t` ⚠️ Error processing $media ${url}:`,\n\t\t\terror instanceof Error ? error.message : error,\n\t\t);\n\t\tresult.media.skipped++;\n\t\treturn null;\n\t}\n}\n\n/**\n * Get file extension from content type\n */\nfunction getExtensionFromContentType(contentType: string): string | null {\n\t// Handle content-type with parameters like \"image/jpeg; charset=utf-8\"\n\tconst baseMime = contentType.split(\";\")[0].trim();\n\tconst ext = mime.getExtension(baseMime);\n\treturn ext ? `.${ext}` : null;\n}\n\n/**\n * Get file extension from URL\n */\nfunction getExtensionFromUrl(url: string): string | null {\n\ttry {\n\t\tconst pathname = new URL(url).pathname;\n\t\tconst match = pathname.match(FILE_EXTENSION_PATTERN);\n\t\treturn match ? `.${match[1]}` : null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Generate a filename from URL\n */\nfunction generateFilename(url: string, ext: string): string {\n\ttry {\n\t\tconst pathname = new URL(url).pathname;\n\t\tconst basename = pathname.split(\"/\").pop() || \"media\";\n\t\t// Remove any existing extension and query params\n\t\tconst name = basename.replace(EXTENSION_PATTERN, \"\").replace(QUERY_PARAM_PATTERN, \"\");\n\t\t// Sanitize: only alphanumeric, dash, underscore\n\t\tconst sanitized = name.replace(SANITIZE_PATTERN, \"-\").replace(MULTIPLE_HYPHENS_PATTERN, \"-\");\n\t\treturn `${sanitized || \"media\"}${ext}`;\n\t} catch {\n\t\treturn `media${ext}`;\n\t}\n}\n\n/**\n * Get image dimensions from buffer using image-size.\n * Supports PNG, JPEG, GIF, WebP, AVIF, SVG, TIFF, and more.\n */\nfunction getImageDimensions(buffer: Uint8Array): { width: number; height: number } | null {\n\ttry {\n\t\tconst result = imageSize(buffer);\n\t\tif (result.width != null && result.height != null) {\n\t\t\treturn { width: result.width, height: result.height };\n\t\t}\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  import "../types-BQx6ZXpR.mjs";
2
- import { Hi as MediaItem, _n as S3StorageConfig, gn as LocalStorageConfig, hn as getStoredConfig, pn as EmDashConfig, vn as StorageDescriptor } from "../index-BogfvE-z.mjs";
2
+ import { Hi as MediaItem, _n as S3StorageConfig, gn as LocalStorageConfig, hn as getStoredConfig, pn as EmDashConfig, vn as StorageDescriptor } from "../index-Dlkzhb4C.mjs";
3
3
  import "../runner-Iu3IZSDM.mjs";
4
4
  import { r as ContentItem } from "../types-B_CXXnzh.mjs";
5
- import { $ as ResolvedPlugin } from "../types-IjUrQMVe.mjs";
6
- import "../validate-CcVQQpmH.mjs";
5
+ import { $ as ResolvedPlugin } from "../types-DgSc9Rpc.mjs";
6
+ import "../validate-BcC3m2O7.mjs";
7
7
  import { EmDashHandlers, EmDashManifest, ManifestCollection } from "./types.mjs";
8
8
  import { AstroIntegration } from "astro";
9
9
 
@@ -1,5 +1,5 @@
1
1
  import { t as defaultSeed } from "../default-pHuz9WF6.mjs";
2
- import { n as VERSION, t as COMMIT } from "../version-JjSqv90m.mjs";
2
+ import { n as VERSION, t as COMMIT } from "../version-BdP--J1g.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { fontProviders } from "astro/config";
5
5
  import { dirname, isAbsolute, relative, resolve } from "node:path";
@@ -1,8 +1,8 @@
1
1
  import "../../types-BQx6ZXpR.mjs";
2
- import "../../index-BogfvE-z.mjs";
2
+ import "../../index-Dlkzhb4C.mjs";
3
3
  import "../../runner-Iu3IZSDM.mjs";
4
- import "../../types-IjUrQMVe.mjs";
5
- import "../../validate-CcVQQpmH.mjs";
4
+ import "../../types-DgSc9Rpc.mjs";
5
+ import "../../validate-BcC3m2O7.mjs";
6
6
  import { EmDashHandlers } from "../types.mjs";
7
7
  import { User } from "@emdash-cms/auth";
8
8
  import * as astro from "astro";
@@ -1,6 +1,6 @@
1
1
  import "../../base64-MBPo9ozB.mjs";
2
2
  import "../../types-BIgulNsW.mjs";
3
- import { t as apiError } from "../../error-DqnRMM5z.mjs";
3
+ import { t as apiError } from "../../error-D6LuHLw9.mjs";
4
4
  import { t as getPublicOrigin } from "../../public-url-B1AxbbbQ.mjs";
5
5
  import { t as getAuthMode } from "../../mode-YhqNVef_.mjs";
6
6
  import { ulid } from "ulidx";
@@ -5,11 +5,11 @@ import "../connection-2igzM-AT.mjs";
5
5
  import { t as validateIdentifier } from "../validate-VPnKoIzW.mjs";
6
6
  import { o as isSqlite } from "../dialect-helpers-BKCvISIQ.mjs";
7
7
  import { i as setI18nConfig } from "../config-CVssduLe.mjs";
8
- import { At as handleContentTranslations, Ct as handleContentGetIncludingTrashed, Dt as handleContentPublish, Et as handleContentPermanentDelete, G as sanitizeHeadersForSandbox, H as resolveExclusiveHooks, I as PluginRouteRegistry, K as getTrustedProxyHeaders, L as DEV_CONSOLE_EMAIL_PLUGIN_ID, Mt as handleContentUnschedule, Nt as handleContentUpdate, Ot as handleContentRestore, Pt as validateRev, R as devConsoleEmailDeliver, St as handleContentGet, Tt as handleContentListTrashed, U as CronExecutor, V as createHookPipeline, W as extractRequestMeta, X as after, _t as handleContentCountTrashed, bt as handleContentDiscardDraft, ct as handleMediaGet, dt as handleRevisionGet, ft as handleRevisionList, gt as handleContentCountScheduled, ht as handleContentCompare, it as PluginStateRepository, jt as handleContentUnpublish, kt as handleContentSchedule, lt as handleMediaList, ot as handleMediaCreate, pt as handleRevisionRestore, q as definePlugin, st as handleMediaDelete, tt as loadBundleFromR2, ut as handleMediaUpdate, vt as handleContentCreate, wt as handleContentList, xt as handleContentDuplicate, yt as handleContentDelete, z as EmailPipeline } from "../search-DuWhx4NG.mjs";
8
+ import { At as handleContentTranslations, Ct as handleContentGetIncludingTrashed, Dt as handleContentPublish, Et as handleContentPermanentDelete, G as sanitizeHeadersForSandbox, H as resolveExclusiveHooks, I as PluginRouteRegistry, K as getTrustedProxyHeaders, L as DEV_CONSOLE_EMAIL_PLUGIN_ID, Mt as handleContentUnschedule, Nt as handleContentUpdate, Ot as handleContentRestore, Pt as validateRev, R as devConsoleEmailDeliver, St as handleContentGet, Tt as handleContentListTrashed, U as CronExecutor, V as createHookPipeline, W as extractRequestMeta, X as after, _t as handleContentCountTrashed, bt as handleContentDiscardDraft, ct as handleMediaGet, dt as handleRevisionGet, ft as handleRevisionList, gt as handleContentCountScheduled, ht as handleContentCompare, it as PluginStateRepository, jt as handleContentUnpublish, kt as handleContentSchedule, lt as handleMediaList, ot as handleMediaCreate, pt as handleRevisionRestore, q as definePlugin, st as handleMediaDelete, tt as loadBundleFromR2, ut as handleMediaUpdate, vt as handleContentCreate, wt as handleContentList, xt as handleContentDuplicate, yt as handleContentDelete, z as EmailPipeline } from "../search-n-ZCMfr3.mjs";
9
9
  import { r as RevisionRepository } from "../content-CERxPUN0.mjs";
10
10
  import "../base64-MBPo9ozB.mjs";
11
11
  import "../types-BIgulNsW.mjs";
12
- import { t as MediaRepository } from "../media-1fFhub9c.mjs";
12
+ import { a as invalidateSiteSettingsCache, s as MediaRepository } from "../settings-nTXPRi3D.mjs";
13
13
  import "../taxonomy-D6NvlKo8.mjs";
14
14
  import { t as OptionsRepository } from "../options-nPxWnrya.mjs";
15
15
  import "../redirect-C5H7VGIX.mjs";
@@ -19,17 +19,17 @@ import { n as requestCached } from "../request-cache-D4I69LeL.mjs";
19
19
  import { r as hashString } from "../zod-generator-CHnJUP2l.mjs";
20
20
  import { i as FTSManager, n as SchemaRegistry } from "../registry-Do34mz_P.mjs";
21
21
  import { r as getDb } from "../loader-ou_PXAjg.mjs";
22
- import "../apply-Ded_1vng.mjs";
23
- import "../taxonomies-Bw76xAxo.mjs";
24
- import { r as normalizeManifestRoute } from "../manifest-schema-CXAbd1vH.mjs";
25
- import "../types-DiI8NOG_.mjs";
26
- import "../error-DqnRMM5z.mjs";
27
- import { a as invalidateUrlPatternCache } from "../query-8c_meo_K.mjs";
22
+ import "../taxonomies-JmQQZiG1.mjs";
23
+ import { r as normalizeManifestRoute } from "../manifest-schema-Bp6d4d4n.mjs";
24
+ import "../types-Cug_RO3W.mjs";
25
+ import "../error-D6LuHLw9.mjs";
26
+ import { a as invalidateUrlPatternCache } from "../query-yA3-rFji.mjs";
27
+ import "../apply-C1ZORgcy.mjs";
28
28
  import "../tokens-CyRDPVW2.mjs";
29
29
  import "../bylines-DTFI8nDM.mjs";
30
30
  import "../load-DR1VwFXR.mjs";
31
31
  import "../index.mjs";
32
- import { n as VERSION, t as COMMIT } from "../version-JjSqv90m.mjs";
32
+ import { n as VERSION, t as COMMIT } from "../version-BdP--J1g.mjs";
33
33
  import { t as getAuthMode } from "../mode-YhqNVef_.mjs";
34
34
  import { a as validateEncryptionKeyAtStartup } from "../secrets-CZ8rxLX3.mjs";
35
35
  import { Kysely, sql } from "kysely";
@@ -801,7 +801,7 @@ var EmDashRuntime = class EmDashRuntime {
801
801
  }
802
802
  })();
803
803
  if (collectionCount.count === 0 && !setupDone) {
804
- const { applySeed } = await import("../apply-Ded_1vng.mjs").then((n) => n.n);
804
+ const { applySeed } = await import("../apply-C1ZORgcy.mjs").then((n) => n.n);
805
805
  const { loadSeed } = await import("../load-DR1VwFXR.mjs").then((n) => n.r);
806
806
  const { validateSeed } = await import("../validate-UK4Ja1uo.mjs").then((n) => n.n);
807
807
  const seed = await loadSeed();
@@ -1390,10 +1390,14 @@ var EmDashRuntime = class EmDashRuntime {
1390
1390
  return result;
1391
1391
  }
1392
1392
  async handleMediaUpdate(id, input) {
1393
- return handleMediaUpdate(this.db, id, input);
1393
+ const result = await handleMediaUpdate(this.db, id, input);
1394
+ if (result.success) invalidateSiteSettingsCache();
1395
+ return result;
1394
1396
  }
1395
1397
  async handleMediaDelete(id) {
1396
- return handleMediaDelete(this.db, id);
1398
+ const result = await handleMediaDelete(this.db, id);
1399
+ if (result.success) invalidateSiteSettingsCache();
1400
+ return result;
1397
1401
  }
1398
1402
  async handleRevisionList(collection, entryId, params = {}) {
1399
1403
  return handleRevisionList(this.db, collection, entryId, params);