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.
- package/dist/{apply-Ded_1vng.mjs → apply-C1ZORgcy.mjs} +6 -226
- package/dist/apply-C1ZORgcy.mjs.map +1 -0
- package/dist/astro/index.d.mts +3 -3
- package/dist/astro/index.mjs +1 -1
- package/dist/astro/middleware/auth.d.mts +3 -3
- package/dist/astro/middleware/auth.mjs +1 -1
- package/dist/astro/middleware.mjs +16 -12
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +3 -4
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/cli/index.mjs +4 -4
- package/dist/{error-DqnRMM5z.mjs → error-D6LuHLw9.mjs} +1 -1
- package/dist/{error-DqnRMM5z.mjs.map → error-D6LuHLw9.mjs.map} +1 -1
- package/dist/{index-BogfvE-z.d.mts → index-Dlkzhb4C.d.mts} +5 -5
- package/dist/index-Dlkzhb4C.d.mts.map +1 -0
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +9 -9
- package/dist/{manifest-schema-CXAbd1vH.mjs → manifest-schema-Bp6d4d4n.mjs} +1 -1
- package/dist/{manifest-schema-CXAbd1vH.mjs.map → manifest-schema-Bp6d4d4n.mjs.map} +1 -1
- package/dist/media/local-runtime.d.mts +3 -3
- package/dist/media/local-runtime.d.mts.map +1 -1
- package/dist/media/local-runtime.mjs +6 -1
- package/dist/media/local-runtime.mjs.map +1 -1
- package/dist/page/index.d.mts +15 -4
- package/dist/page/index.d.mts.map +1 -1
- package/dist/page/index.mjs +16 -5
- package/dist/page/index.mjs.map +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +3 -3
- package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
- package/dist/{query-8c_meo_K.mjs → query-yA3-rFji.mjs} +13 -2
- package/dist/query-yA3-rFji.mjs.map +1 -0
- package/dist/runtime.d.mts +3 -3
- package/dist/{search-DuWhx4NG.mjs → search-n-ZCMfr3.mjs} +33 -16
- package/dist/search-n-ZCMfr3.mjs.map +1 -0
- package/dist/seed/index.d.mts +1 -1
- package/dist/seed/index.mjs +2 -2
- package/dist/settings-nTXPRi3D.mjs +440 -0
- package/dist/settings-nTXPRi3D.mjs.map +1 -0
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/{taxonomies-Bw76xAxo.mjs → taxonomies-JmQQZiG1.mjs} +2 -2
- package/dist/{taxonomies-Bw76xAxo.mjs.map → taxonomies-JmQQZiG1.mjs.map} +1 -1
- package/dist/{types-BTe41zL6.d.mts → types-B1gLSAH2.d.mts} +13 -9
- package/dist/{types-BTe41zL6.d.mts.map → types-B1gLSAH2.d.mts.map} +1 -1
- package/dist/{types-DiI8NOG_.mjs → types-Cug_RO3W.mjs} +1 -1
- package/dist/{types-DiI8NOG_.mjs.map → types-Cug_RO3W.mjs.map} +1 -1
- package/dist/{types-IjUrQMVe.d.mts → types-DgSc9Rpc.d.mts} +149 -4
- package/dist/types-DgSc9Rpc.d.mts.map +1 -0
- package/dist/{types-K-EkEQCI.mjs → types-PafqtQuM.mjs} +1 -1
- package/dist/{types-K-EkEQCI.mjs.map → types-PafqtQuM.mjs.map} +1 -1
- package/dist/{validate-CcVQQpmH.d.mts → validate-BcC3m2O7.d.mts} +2 -2
- package/dist/{validate-CcVQQpmH.d.mts.map → validate-BcC3m2O7.d.mts.map} +1 -1
- package/dist/version-BdP--J1g.mjs +7 -0
- package/dist/{version-JjSqv90m.mjs.map → version-BdP--J1g.mjs.map} +1 -1
- package/package.json +6 -6
- package/src/api/schemas/settings.ts +41 -9
- package/src/astro/routes/api/media/[id].ts +2 -1
- package/src/components/EmDashHead.astro +26 -5
- package/src/emdash-runtime.ts +21 -2
- package/src/media/local-runtime.ts +7 -0
- package/src/page/absolute-url.ts +146 -0
- package/src/page/jsonld.ts +10 -2
- package/src/page/seo-contributions.ts +17 -6
- package/src/plugins/context.ts +11 -1
- package/src/query.ts +12 -0
- package/src/settings/index.ts +20 -1
- package/src/settings/types.ts +12 -8
- package/dist/apply-Ded_1vng.mjs.map +0 -1
- package/dist/index-BogfvE-z.d.mts.map +0 -1
- package/dist/media-1fFhub9c.mjs +0 -209
- package/dist/media-1fFhub9c.mjs.map +0 -1
- package/dist/query-8c_meo_K.mjs.map +0 -1
- package/dist/search-DuWhx4NG.mjs.map +0 -1
- package/dist/types-IjUrQMVe.d.mts.map +0 -1
- package/dist/version-JjSqv90m.mjs +0 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"taxonomies-Bw76xAxo.mjs","names":[],"sources":["../src/i18n/resolve.ts","../src/taxonomies/index.ts"],"sourcesContent":["/**\n * Shared locale-resolution helpers.\n *\n * Matches the pattern used by `query.ts` for content: an explicit locale wins,\n * otherwise we fall back to the request-context locale, otherwise to\n * `defaultLocale` when i18n is enabled, otherwise to `undefined` (meaning \"do\n * not filter by locale\" — legacy single-locale behaviour).\n */\n\nimport { getRequestContext } from \"../request-context.js\";\nimport { getFallbackChain, getI18nConfig, isI18nEnabled } from \"./config.js\";\n\n/**\n * Resolve the locale to use for a query given an optional explicit value.\n * Returns `undefined` when no locale information is available; callers should\n * treat that as \"do not filter by locale\".\n */\nexport function resolveLocale(explicit?: string): string | undefined {\n\tif (explicit !== undefined) return explicit;\n\tconst ctxLocale = getRequestContext()?.locale;\n\tif (ctxLocale !== undefined) return ctxLocale;\n\tconst cfg = getI18nConfig();\n\tif (cfg && isI18nEnabled()) return cfg.defaultLocale;\n\treturn undefined;\n}\n\n/**\n * Fallback chain to try when looking up a single item. When i18n is disabled\n * or the locale is unspecified, returns a single-element array (or empty when\n * no locale resolves) so callers can iterate uniformly.\n */\nexport function resolveLocaleChain(explicit?: string): string[] {\n\tconst locale = resolveLocale(explicit);\n\tif (locale === undefined) return [];\n\tif (!isI18nEnabled()) return [locale];\n\treturn getFallbackChain(locale);\n}\n","/**\n * Runtime API for taxonomies.\n *\n * All helpers are locale-aware. When a locale is not passed explicitly we fall\n * back to the request context or the configured `defaultLocale` (see\n * `i18n/resolve.ts`).\n *\n * Because `content_taxonomies.taxonomy_id` stores the translation_group (not a\n * specific term id), the joins here are `taxonomies.translation_group =\n * content_taxonomies.taxonomy_id` + filter by `taxonomies.locale`, which picks\n * the right per-locale term.\n */\n\nimport { resolveLocale, resolveLocaleChain } from \"../i18n/resolve.js\";\nimport { getDb } from \"../loader.js\";\nimport { peekRequestCache, requestCached, setRequestCacheEntry } from \"../request-cache.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../utils/chunks.js\";\nimport { isMissingTableError } from \"../utils/db-errors.js\";\nimport type { TaxonomyDef, TaxonomyTerm, TaxonomyTermRow } from \"./types.js\";\n\nexport interface TaxonomyQueryOptions {\n\tlocale?: string;\n}\n\n/**\n * No-op — kept for API compatibility.\n */\nexport function invalidateTermCache(): void {\n\t// Intentionally empty.\n}\n\n/**\n * Get every taxonomy definition. Definitions are per-locale (one row per\n * locale inside the same translation_group) — by default we resolve to the\n * active locale.\n */\nexport async function getTaxonomyDefs(options: TaxonomyQueryOptions = {}): Promise<TaxonomyDef[]> {\n\tconst locale = resolveLocale(options.locale);\n\treturn requestCached(`taxonomy-defs:${locale ?? \"*\"}`, async () => {\n\t\tconst db = await getDb();\n\t\tlet query = db.selectFrom(\"_emdash_taxonomy_defs\").selectAll();\n\t\tif (locale !== undefined) query = query.where(\"locale\", \"=\", locale);\n\t\tconst rows = await query.execute();\n\t\treturn rows.map(rowToTaxonomyDef);\n\t});\n}\n\n/**\n * Get a single taxonomy definition by name. Uses the fallback chain so even\n * if there is no translation for the active locale we still return something.\n *\n * If `getTaxonomyDefs()` has already loaded the full list in this request\n * (which happens during entry-term hydration on every page that renders a\n * collection), search the matching def in memory rather than running a\n * second query against `_emdash_taxonomy_defs`.\n */\nexport async function getTaxonomyDef(\n\tname: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<TaxonomyDef | null> {\n\tconst chain = resolveLocaleChain(options.locale);\n\tconst peekKey = `taxonomy-defs:${resolveLocale(options.locale) ?? \"*\"}`;\n\tconst allDefs = peekRequestCache<TaxonomyDef[]>(peekKey);\n\tif (allDefs) {\n\t\tconst defs = await allDefs;\n\t\tif (chain.length === 0) return defs.find((d) => d.name === name) ?? null;\n\t\tfor (const locale of chain) {\n\t\t\tconst found = defs.find((d) => d.name === name && d.locale === locale);\n\t\t\tif (found) return found;\n\t\t}\n\t\treturn null;\n\t}\n\n\treturn requestCached(`taxonomy-def:${name}:${chain.join(\",\")}`, async () => {\n\t\tconst db = await getDb();\n\n\t\tif (chain.length === 0) {\n\t\t\tconst row = await db\n\t\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"name\", \"=\", name)\n\t\t\t\t.orderBy(\"locale\", \"asc\")\n\t\t\t\t.executeTakeFirst();\n\t\t\treturn row ? rowToTaxonomyDef(row) : null;\n\t\t}\n\n\t\tfor (const locale of chain) {\n\t\t\tconst row = await db\n\t\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"name\", \"=\", name)\n\t\t\t\t.where(\"locale\", \"=\", locale)\n\t\t\t\t.executeTakeFirst();\n\t\t\tif (row) return rowToTaxonomyDef(row);\n\t\t}\n\t\treturn null;\n\t});\n}\n\n/**\n * All terms of a taxonomy in a specific locale (flat for non-hierarchical,\n * tree for hierarchical).\n */\nexport async function getTaxonomyTerms(\n\ttaxonomyName: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<TaxonomyTerm[]> {\n\tconst locale = resolveLocale(options.locale);\n\treturn requestCached(`taxonomy-terms:${taxonomyName}:${locale ?? \"*\"}`, async () => {\n\t\tconst db = await getDb();\n\n\t\tconst def = await getTaxonomyDef(taxonomyName, options);\n\t\tif (!def) return [];\n\n\t\tlet termsQuery = db\n\t\t\t.selectFrom(\"taxonomies\")\n\t\t\t.selectAll()\n\t\t\t.where(\"name\", \"=\", taxonomyName)\n\t\t\t.orderBy(\"label\", \"asc\");\n\t\tif (locale !== undefined) termsQuery = termsQuery.where(\"locale\", \"=\", locale);\n\t\tconst rows = await termsQuery.execute();\n\n\t\t// Counts are keyed by translation_group (what the pivot stores).\n\t\tconst countsResult = await db\n\t\t\t.selectFrom(\"content_taxonomies\")\n\t\t\t.select([\"taxonomy_id\"])\n\t\t\t.select((eb) => eb.fn.count<number>(\"entry_id\").as(\"count\"))\n\t\t\t.groupBy(\"taxonomy_id\")\n\t\t\t.execute();\n\t\tconst counts = new Map<string, number>();\n\t\tfor (const row of countsResult) counts.set(row.taxonomy_id, row.count);\n\n\t\tconst flatTerms: TaxonomyTermRow[] = rows.map((row) => ({\n\t\t\tid: row.id,\n\t\t\tname: row.name,\n\t\t\tslug: row.slug,\n\t\t\tlabel: row.label,\n\t\t\tparent_id: row.parent_id,\n\t\t\tdata: row.data,\n\t\t\tlocale: row.locale,\n\t\t\ttranslation_group: row.translation_group,\n\t\t}));\n\n\t\tif (def.hierarchical) return buildTree(flatTerms, counts);\n\n\t\treturn flatTerms.map((term) => ({\n\t\t\tid: term.id,\n\t\t\tname: term.name,\n\t\t\tslug: term.slug,\n\t\t\tlabel: term.label,\n\t\t\tchildren: [],\n\t\t\tcount: counts.get(term.translation_group ?? term.id) ?? 0,\n\t\t\tlocale: term.locale,\n\t\t\ttranslationGroup: term.translation_group,\n\t\t}));\n\t});\n}\n\n/**\n * Get a single term by (taxonomy, slug). Honours the fallback chain — if the\n * slug exists in a fallback locale, we return that row (useful for deep-linking\n * to a term page when the translation is missing).\n */\nexport async function getTerm(\n\ttaxonomyName: string,\n\tslug: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<TaxonomyTerm | null> {\n\tconst db = await getDb();\n\tconst chain = resolveLocaleChain(options.locale);\n\n\tlet row: Awaited<ReturnType<ReturnType<typeof selectTerm>[\"executeTakeFirst\"]>>;\n\tconst selectTerm = () =>\n\t\tdb\n\t\t\t.selectFrom(\"taxonomies\")\n\t\t\t.selectAll()\n\t\t\t.where(\"name\", \"=\", taxonomyName)\n\t\t\t.where(\"slug\", \"=\", slug);\n\n\tif (chain.length === 0) {\n\t\trow = await selectTerm().orderBy(\"locale\", \"asc\").executeTakeFirst();\n\t} else {\n\t\trow = undefined;\n\t\tfor (const locale of chain) {\n\t\t\trow = await selectTerm().where(\"locale\", \"=\", locale).executeTakeFirst();\n\t\t\tif (row) break;\n\t\t}\n\t}\n\n\tif (!row) return null;\n\n\tconst countResult = await db\n\t\t.selectFrom(\"content_taxonomies\")\n\t\t.select((eb) => eb.fn.count<number>(\"entry_id\").as(\"count\"))\n\t\t.where(\"taxonomy_id\", \"=\", row.translation_group ?? row.id)\n\t\t.executeTakeFirst();\n\tconst count = countResult?.count ?? 0;\n\n\tlet childrenQuery = db\n\t\t.selectFrom(\"taxonomies\")\n\t\t.selectAll()\n\t\t.where(\"parent_id\", \"=\", row.id)\n\t\t.orderBy(\"label\", \"asc\");\n\tconst termLocale = row.locale;\n\tif (termLocale) childrenQuery = childrenQuery.where(\"locale\", \"=\", termLocale);\n\tconst childRows = await childrenQuery.execute();\n\n\tconst children = childRows.map<TaxonomyTerm>((child) => ({\n\t\tid: child.id,\n\t\tname: child.name,\n\t\tslug: child.slug,\n\t\tlabel: child.label,\n\t\tparentId: child.parent_id ?? undefined,\n\t\tchildren: [],\n\t\tlocale: child.locale,\n\t\ttranslationGroup: child.translation_group,\n\t}));\n\n\treturn {\n\t\tid: row.id,\n\t\tname: row.name,\n\t\tslug: row.slug,\n\t\tlabel: row.label,\n\t\tparentId: row.parent_id ?? undefined,\n\t\tdescription: row.data ? JSON.parse(row.data).description : undefined,\n\t\tchildren,\n\t\tcount,\n\t\tlocale: row.locale,\n\t\ttranslationGroup: row.translation_group,\n\t};\n}\n\n/**\n * Terms assigned to a content entry, resolved into the active locale. Terms\n * whose translation_group lacks a row in the requested locale are omitted.\n */\nexport function getEntryTerms(\n\tcollection: string,\n\tentryId: string,\n\ttaxonomyName?: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<TaxonomyTerm[]> {\n\tconst locale = resolveLocale(options.locale);\n\treturn requestCached(\n\t\t`terms:${collection}:${entryId}:${taxonomyName ?? \"*\"}:${locale ?? \"*\"}`,\n\t\tasync () => {\n\t\t\tconst db = await getDb();\n\n\t\t\tlet query = db\n\t\t\t\t.selectFrom(\"content_taxonomies\")\n\t\t\t\t.innerJoin(\"taxonomies\", \"taxonomies.translation_group\", \"content_taxonomies.taxonomy_id\")\n\t\t\t\t.selectAll(\"taxonomies\")\n\t\t\t\t.where(\"content_taxonomies.collection\", \"=\", collection)\n\t\t\t\t.where(\"content_taxonomies.entry_id\", \"=\", entryId);\n\n\t\t\tif (taxonomyName) query = query.where(\"taxonomies.name\", \"=\", taxonomyName);\n\t\t\tif (locale !== undefined) query = query.where(\"taxonomies.locale\", \"=\", locale);\n\n\t\t\tconst rows = await query.execute();\n\t\t\treturn rows.map<TaxonomyTerm>((row) => ({\n\t\t\t\tid: row.id,\n\t\t\t\tname: row.name,\n\t\t\t\tslug: row.slug,\n\t\t\t\tlabel: row.label,\n\t\t\t\tparentId: row.parent_id ?? undefined,\n\t\t\t\tchildren: [],\n\t\t\t\tlocale: row.locale,\n\t\t\t\ttranslationGroup: row.translation_group,\n\t\t\t}));\n\t\t},\n\t);\n}\n\n/**\n * Terms for multiple entries of one taxonomy, single query.\n */\nexport async function getTermsForEntries(\n\tcollection: string,\n\tentryIds: string[],\n\ttaxonomyName: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<Map<string, TaxonomyTerm[]>> {\n\tconst result = new Map<string, TaxonomyTerm[]>();\n\tconst uniqueIds = [...new Set(entryIds)];\n\tfor (const id of uniqueIds) result.set(id, []);\n\tif (uniqueIds.length === 0) return result;\n\n\tconst db = await getDb();\n\tconst locale = resolveLocale(options.locale);\n\n\tfor (const chunk of chunks(uniqueIds, SQL_BATCH_SIZE)) {\n\t\tlet rows;\n\t\ttry {\n\t\t\tlet query = db\n\t\t\t\t.selectFrom(\"content_taxonomies\")\n\t\t\t\t.innerJoin(\"taxonomies\", \"taxonomies.translation_group\", \"content_taxonomies.taxonomy_id\")\n\t\t\t\t.select([\n\t\t\t\t\t\"content_taxonomies.entry_id\",\n\t\t\t\t\t\"taxonomies.id\",\n\t\t\t\t\t\"taxonomies.name\",\n\t\t\t\t\t\"taxonomies.slug\",\n\t\t\t\t\t\"taxonomies.label\",\n\t\t\t\t\t\"taxonomies.parent_id\",\n\t\t\t\t\t\"taxonomies.locale\",\n\t\t\t\t\t\"taxonomies.translation_group\",\n\t\t\t\t])\n\t\t\t\t.where(\"content_taxonomies.collection\", \"=\", collection)\n\t\t\t\t.where(\"content_taxonomies.entry_id\", \"in\", chunk)\n\t\t\t\t.where(\"taxonomies.name\", \"=\", taxonomyName);\n\t\t\tif (locale !== undefined) query = query.where(\"taxonomies.locale\", \"=\", locale);\n\t\t\trows = await query.execute();\n\t\t} catch (error) {\n\t\t\tif (isMissingTableError(error)) return result;\n\t\t\tthrow error;\n\t\t}\n\n\t\tfor (const row of rows) {\n\t\t\tconst term: TaxonomyTerm = {\n\t\t\t\tid: row.id,\n\t\t\t\tname: row.name,\n\t\t\t\tslug: row.slug,\n\t\t\t\tlabel: row.label,\n\t\t\t\tparentId: row.parent_id ?? undefined,\n\t\t\t\tchildren: [],\n\t\t\t\tlocale: row.locale,\n\t\t\t\ttranslationGroup: row.translation_group,\n\t\t\t};\n\t\t\tconst terms = result.get(row.entry_id);\n\t\t\tif (terms) terms.push(term);\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Batch-fetch terms for multiple entries across ALL taxonomies in one query.\n * Primes the request-cache for subsequent per-entry calls to `getEntryTerms`.\n */\nexport async function getAllTermsForEntries(\n\tcollection: string,\n\tentryIds: string[],\n\toptions: TaxonomyQueryOptions = {},\n): Promise<Map<string, Record<string, TaxonomyTerm[]>>> {\n\tconst result = new Map<string, Record<string, TaxonomyTerm[]>>();\n\tconst uniqueIds = [...new Set(entryIds)];\n\tfor (const id of uniqueIds) result.set(id, {});\n\tif (uniqueIds.length === 0) return result;\n\n\tconst db = await getDb();\n\tconst locale = resolveLocale(options.locale);\n\tconst applicableTaxonomyNames = await getCollectionTaxonomyNames(collection, { locale });\n\n\tfor (const chunk of chunks(uniqueIds, SQL_BATCH_SIZE)) {\n\t\tlet rows;\n\t\ttry {\n\t\t\tlet query = db\n\t\t\t\t.selectFrom(\"content_taxonomies\")\n\t\t\t\t.innerJoin(\"taxonomies\", \"taxonomies.translation_group\", \"content_taxonomies.taxonomy_id\")\n\t\t\t\t.select([\n\t\t\t\t\t\"content_taxonomies.entry_id\",\n\t\t\t\t\t\"taxonomies.id\",\n\t\t\t\t\t\"taxonomies.name\",\n\t\t\t\t\t\"taxonomies.slug\",\n\t\t\t\t\t\"taxonomies.label\",\n\t\t\t\t\t\"taxonomies.parent_id\",\n\t\t\t\t\t\"taxonomies.locale\",\n\t\t\t\t\t\"taxonomies.translation_group\",\n\t\t\t\t])\n\t\t\t\t.where(\"content_taxonomies.collection\", \"=\", collection)\n\t\t\t\t.where(\"content_taxonomies.entry_id\", \"in\", chunk)\n\t\t\t\t.orderBy(\"taxonomies.label\", \"asc\");\n\t\t\tif (locale !== undefined) query = query.where(\"taxonomies.locale\", \"=\", locale);\n\t\t\trows = await query.execute();\n\t\t} catch (error) {\n\t\t\tif (isMissingTableError(error)) {\n\t\t\t\tfor (const id of uniqueIds) {\n\t\t\t\t\tprimeEntryTermsCache(collection, id, {}, applicableTaxonomyNames, locale);\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\n\t\tfor (const row of rows) {\n\t\t\tconst term: TaxonomyTerm = {\n\t\t\t\tid: row.id,\n\t\t\t\tname: row.name,\n\t\t\t\tslug: row.slug,\n\t\t\t\tlabel: row.label,\n\t\t\t\tparentId: row.parent_id ?? undefined,\n\t\t\t\tchildren: [],\n\t\t\t\tlocale: row.locale,\n\t\t\t\ttranslationGroup: row.translation_group,\n\t\t\t};\n\t\t\tconst byTaxonomy = result.get(row.entry_id);\n\t\t\tif (!byTaxonomy) continue;\n\t\t\tconst existing = byTaxonomy[row.name];\n\t\t\tif (existing) existing.push(term);\n\t\t\telse byTaxonomy[row.name] = [term];\n\t\t}\n\t}\n\n\tfor (const [entryId, byTaxonomy] of result) {\n\t\tprimeEntryTermsCache(collection, entryId, byTaxonomy, applicableTaxonomyNames, locale);\n\t}\n\n\treturn result;\n}\n\n/**\n * Return the list of taxonomy names applicable to a collection, request-\n * cached so a page render only pays for it once.\n *\n * Returns an empty list when taxonomies haven't been defined yet.\n */\nasync function getCollectionTaxonomyNames(\n\tcollection: string,\n\toptions: TaxonomyQueryOptions,\n): Promise<string[]> {\n\ttry {\n\t\tconst defs = await getTaxonomyDefs(options);\n\t\treturn defs.filter((d) => d.collections.includes(collection)).map((d) => d.name);\n\t} catch (error) {\n\t\tif (isMissingTableError(error)) return [];\n\t\tthrow error;\n\t}\n}\n\n/**\n * Pre-populate the request-cache for every getEntryTerms call-shape that\n * could hit this entry:\n *\n * getEntryTerms(collection, entryId) -> key `terms:C:E:*`\n * getEntryTerms(collection, entryId, \"tag\") -> key `terms:C:E:tag`\n * getEntryTerms(collection, entryId, \"category\") -> key `terms:C:E:category`\n * ...one per taxonomy that applies to this collection\n *\n * Taxonomies with no rows on this entry are seeded with `[]` so legacy\n * callers short-circuit to the cached empty array instead of re-querying.\n */\nfunction primeEntryTermsCache(\n\tcollection: string,\n\tentryId: string,\n\tbyTaxonomy: Record<string, TaxonomyTerm[]>,\n\tapplicableTaxonomyNames: string[],\n\tlocale: string | undefined,\n): void {\n\tconst localeKey = locale ?? \"*\";\n\tfor (const name of applicableTaxonomyNames) {\n\t\tsetRequestCacheEntry(\n\t\t\t`terms:${collection}:${entryId}:${name}:${localeKey}`,\n\t\t\tbyTaxonomy[name] ?? [],\n\t\t);\n\t}\n\tfor (const [name, terms] of Object.entries(byTaxonomy)) {\n\t\tsetRequestCacheEntry(`terms:${collection}:${entryId}:${name}:${localeKey}`, terms);\n\t}\n\tconst allTerms = Object.values(byTaxonomy).flat();\n\tsetRequestCacheEntry(`terms:${collection}:${entryId}:*:${localeKey}`, allTerms);\n}\n\n/**\n * Get entries by term. Both the lookup (term slug in the active locale) and\n * the content query respect the active locale.\n */\nexport async function getEntriesByTerm(\n\tcollection: string,\n\ttaxonomyName: string,\n\ttermSlug: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<Array<{ id: string; data: Record<string, unknown> }>> {\n\tconst { getEmDashCollection } = await import(\"../query.js\");\n\n\tconst queryOptions: Record<string, unknown> = {\n\t\twhere: { [taxonomyName]: termSlug },\n\t};\n\tif (options.locale !== undefined) queryOptions.locale = options.locale;\n\tconst { entries } = await getEmDashCollection(collection, queryOptions);\n\treturn entries;\n}\n\nfunction rowToTaxonomyDef(row: {\n\tid: string;\n\tname: string;\n\tlabel: string;\n\tlabel_singular: string | null;\n\thierarchical: number;\n\tcollections: string | null;\n\tlocale: string;\n\ttranslation_group: string | null;\n}): TaxonomyDef {\n\treturn {\n\t\tid: row.id,\n\t\tname: row.name,\n\t\tlabel: row.label,\n\t\tlabelSingular: row.label_singular ?? undefined,\n\t\thierarchical: row.hierarchical === 1,\n\t\tcollections: row.collections ? JSON.parse(row.collections) : [],\n\t\tlocale: row.locale,\n\t\ttranslationGroup: row.translation_group,\n\t};\n}\n\n/**\n * Build tree structure from flat terms\n */\nfunction buildTree(flatTerms: TaxonomyTermRow[], counts: Map<string, number>): TaxonomyTerm[] {\n\tconst map = new Map<string, TaxonomyTerm>();\n\tconst roots: TaxonomyTerm[] = [];\n\n\tfor (const term of flatTerms) {\n\t\tmap.set(term.id, {\n\t\t\tid: term.id,\n\t\t\tname: term.name,\n\t\t\tslug: term.slug,\n\t\t\tlabel: term.label,\n\t\t\tparentId: term.parent_id ?? undefined,\n\t\t\tdescription: term.data ? JSON.parse(term.data).description : undefined,\n\t\t\tchildren: [],\n\t\t\tcount: counts.get(term.translation_group ?? term.id) ?? 0,\n\t\t\tlocale: term.locale,\n\t\t\ttranslationGroup: term.translation_group,\n\t\t});\n\t}\n\n\tfor (const term of map.values()) {\n\t\tif (term.parentId && map.has(term.parentId)) {\n\t\t\tmap.get(term.parentId)!.children.push(term);\n\t\t} else {\n\t\t\troots.push(term);\n\t\t}\n\t}\n\n\treturn roots;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"taxonomies-JmQQZiG1.mjs","names":[],"sources":["../src/i18n/resolve.ts","../src/taxonomies/index.ts"],"sourcesContent":["/**\n * Shared locale-resolution helpers.\n *\n * Matches the pattern used by `query.ts` for content: an explicit locale wins,\n * otherwise we fall back to the request-context locale, otherwise to\n * `defaultLocale` when i18n is enabled, otherwise to `undefined` (meaning \"do\n * not filter by locale\" — legacy single-locale behaviour).\n */\n\nimport { getRequestContext } from \"../request-context.js\";\nimport { getFallbackChain, getI18nConfig, isI18nEnabled } from \"./config.js\";\n\n/**\n * Resolve the locale to use for a query given an optional explicit value.\n * Returns `undefined` when no locale information is available; callers should\n * treat that as \"do not filter by locale\".\n */\nexport function resolveLocale(explicit?: string): string | undefined {\n\tif (explicit !== undefined) return explicit;\n\tconst ctxLocale = getRequestContext()?.locale;\n\tif (ctxLocale !== undefined) return ctxLocale;\n\tconst cfg = getI18nConfig();\n\tif (cfg && isI18nEnabled()) return cfg.defaultLocale;\n\treturn undefined;\n}\n\n/**\n * Fallback chain to try when looking up a single item. When i18n is disabled\n * or the locale is unspecified, returns a single-element array (or empty when\n * no locale resolves) so callers can iterate uniformly.\n */\nexport function resolveLocaleChain(explicit?: string): string[] {\n\tconst locale = resolveLocale(explicit);\n\tif (locale === undefined) return [];\n\tif (!isI18nEnabled()) return [locale];\n\treturn getFallbackChain(locale);\n}\n","/**\n * Runtime API for taxonomies.\n *\n * All helpers are locale-aware. When a locale is not passed explicitly we fall\n * back to the request context or the configured `defaultLocale` (see\n * `i18n/resolve.ts`).\n *\n * Because `content_taxonomies.taxonomy_id` stores the translation_group (not a\n * specific term id), the joins here are `taxonomies.translation_group =\n * content_taxonomies.taxonomy_id` + filter by `taxonomies.locale`, which picks\n * the right per-locale term.\n */\n\nimport { resolveLocale, resolveLocaleChain } from \"../i18n/resolve.js\";\nimport { getDb } from \"../loader.js\";\nimport { peekRequestCache, requestCached, setRequestCacheEntry } from \"../request-cache.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../utils/chunks.js\";\nimport { isMissingTableError } from \"../utils/db-errors.js\";\nimport type { TaxonomyDef, TaxonomyTerm, TaxonomyTermRow } from \"./types.js\";\n\nexport interface TaxonomyQueryOptions {\n\tlocale?: string;\n}\n\n/**\n * No-op — kept for API compatibility.\n */\nexport function invalidateTermCache(): void {\n\t// Intentionally empty.\n}\n\n/**\n * Get every taxonomy definition. Definitions are per-locale (one row per\n * locale inside the same translation_group) — by default we resolve to the\n * active locale.\n */\nexport async function getTaxonomyDefs(options: TaxonomyQueryOptions = {}): Promise<TaxonomyDef[]> {\n\tconst locale = resolveLocale(options.locale);\n\treturn requestCached(`taxonomy-defs:${locale ?? \"*\"}`, async () => {\n\t\tconst db = await getDb();\n\t\tlet query = db.selectFrom(\"_emdash_taxonomy_defs\").selectAll();\n\t\tif (locale !== undefined) query = query.where(\"locale\", \"=\", locale);\n\t\tconst rows = await query.execute();\n\t\treturn rows.map(rowToTaxonomyDef);\n\t});\n}\n\n/**\n * Get a single taxonomy definition by name. Uses the fallback chain so even\n * if there is no translation for the active locale we still return something.\n *\n * If `getTaxonomyDefs()` has already loaded the full list in this request\n * (which happens during entry-term hydration on every page that renders a\n * collection), search the matching def in memory rather than running a\n * second query against `_emdash_taxonomy_defs`.\n */\nexport async function getTaxonomyDef(\n\tname: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<TaxonomyDef | null> {\n\tconst chain = resolveLocaleChain(options.locale);\n\tconst peekKey = `taxonomy-defs:${resolveLocale(options.locale) ?? \"*\"}`;\n\tconst allDefs = peekRequestCache<TaxonomyDef[]>(peekKey);\n\tif (allDefs) {\n\t\tconst defs = await allDefs;\n\t\tif (chain.length === 0) return defs.find((d) => d.name === name) ?? null;\n\t\tfor (const locale of chain) {\n\t\t\tconst found = defs.find((d) => d.name === name && d.locale === locale);\n\t\t\tif (found) return found;\n\t\t}\n\t\treturn null;\n\t}\n\n\treturn requestCached(`taxonomy-def:${name}:${chain.join(\",\")}`, async () => {\n\t\tconst db = await getDb();\n\n\t\tif (chain.length === 0) {\n\t\t\tconst row = await db\n\t\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"name\", \"=\", name)\n\t\t\t\t.orderBy(\"locale\", \"asc\")\n\t\t\t\t.executeTakeFirst();\n\t\t\treturn row ? rowToTaxonomyDef(row) : null;\n\t\t}\n\n\t\tfor (const locale of chain) {\n\t\t\tconst row = await db\n\t\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"name\", \"=\", name)\n\t\t\t\t.where(\"locale\", \"=\", locale)\n\t\t\t\t.executeTakeFirst();\n\t\t\tif (row) return rowToTaxonomyDef(row);\n\t\t}\n\t\treturn null;\n\t});\n}\n\n/**\n * All terms of a taxonomy in a specific locale (flat for non-hierarchical,\n * tree for hierarchical).\n */\nexport async function getTaxonomyTerms(\n\ttaxonomyName: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<TaxonomyTerm[]> {\n\tconst locale = resolveLocale(options.locale);\n\treturn requestCached(`taxonomy-terms:${taxonomyName}:${locale ?? \"*\"}`, async () => {\n\t\tconst db = await getDb();\n\n\t\tconst def = await getTaxonomyDef(taxonomyName, options);\n\t\tif (!def) return [];\n\n\t\tlet termsQuery = db\n\t\t\t.selectFrom(\"taxonomies\")\n\t\t\t.selectAll()\n\t\t\t.where(\"name\", \"=\", taxonomyName)\n\t\t\t.orderBy(\"label\", \"asc\");\n\t\tif (locale !== undefined) termsQuery = termsQuery.where(\"locale\", \"=\", locale);\n\t\tconst rows = await termsQuery.execute();\n\n\t\t// Counts are keyed by translation_group (what the pivot stores).\n\t\tconst countsResult = await db\n\t\t\t.selectFrom(\"content_taxonomies\")\n\t\t\t.select([\"taxonomy_id\"])\n\t\t\t.select((eb) => eb.fn.count<number>(\"entry_id\").as(\"count\"))\n\t\t\t.groupBy(\"taxonomy_id\")\n\t\t\t.execute();\n\t\tconst counts = new Map<string, number>();\n\t\tfor (const row of countsResult) counts.set(row.taxonomy_id, row.count);\n\n\t\tconst flatTerms: TaxonomyTermRow[] = rows.map((row) => ({\n\t\t\tid: row.id,\n\t\t\tname: row.name,\n\t\t\tslug: row.slug,\n\t\t\tlabel: row.label,\n\t\t\tparent_id: row.parent_id,\n\t\t\tdata: row.data,\n\t\t\tlocale: row.locale,\n\t\t\ttranslation_group: row.translation_group,\n\t\t}));\n\n\t\tif (def.hierarchical) return buildTree(flatTerms, counts);\n\n\t\treturn flatTerms.map((term) => ({\n\t\t\tid: term.id,\n\t\t\tname: term.name,\n\t\t\tslug: term.slug,\n\t\t\tlabel: term.label,\n\t\t\tchildren: [],\n\t\t\tcount: counts.get(term.translation_group ?? term.id) ?? 0,\n\t\t\tlocale: term.locale,\n\t\t\ttranslationGroup: term.translation_group,\n\t\t}));\n\t});\n}\n\n/**\n * Get a single term by (taxonomy, slug). Honours the fallback chain — if the\n * slug exists in a fallback locale, we return that row (useful for deep-linking\n * to a term page when the translation is missing).\n */\nexport async function getTerm(\n\ttaxonomyName: string,\n\tslug: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<TaxonomyTerm | null> {\n\tconst db = await getDb();\n\tconst chain = resolveLocaleChain(options.locale);\n\n\tlet row: Awaited<ReturnType<ReturnType<typeof selectTerm>[\"executeTakeFirst\"]>>;\n\tconst selectTerm = () =>\n\t\tdb\n\t\t\t.selectFrom(\"taxonomies\")\n\t\t\t.selectAll()\n\t\t\t.where(\"name\", \"=\", taxonomyName)\n\t\t\t.where(\"slug\", \"=\", slug);\n\n\tif (chain.length === 0) {\n\t\trow = await selectTerm().orderBy(\"locale\", \"asc\").executeTakeFirst();\n\t} else {\n\t\trow = undefined;\n\t\tfor (const locale of chain) {\n\t\t\trow = await selectTerm().where(\"locale\", \"=\", locale).executeTakeFirst();\n\t\t\tif (row) break;\n\t\t}\n\t}\n\n\tif (!row) return null;\n\n\tconst countResult = await db\n\t\t.selectFrom(\"content_taxonomies\")\n\t\t.select((eb) => eb.fn.count<number>(\"entry_id\").as(\"count\"))\n\t\t.where(\"taxonomy_id\", \"=\", row.translation_group ?? row.id)\n\t\t.executeTakeFirst();\n\tconst count = countResult?.count ?? 0;\n\n\tlet childrenQuery = db\n\t\t.selectFrom(\"taxonomies\")\n\t\t.selectAll()\n\t\t.where(\"parent_id\", \"=\", row.id)\n\t\t.orderBy(\"label\", \"asc\");\n\tconst termLocale = row.locale;\n\tif (termLocale) childrenQuery = childrenQuery.where(\"locale\", \"=\", termLocale);\n\tconst childRows = await childrenQuery.execute();\n\n\tconst children = childRows.map<TaxonomyTerm>((child) => ({\n\t\tid: child.id,\n\t\tname: child.name,\n\t\tslug: child.slug,\n\t\tlabel: child.label,\n\t\tparentId: child.parent_id ?? undefined,\n\t\tchildren: [],\n\t\tlocale: child.locale,\n\t\ttranslationGroup: child.translation_group,\n\t}));\n\n\treturn {\n\t\tid: row.id,\n\t\tname: row.name,\n\t\tslug: row.slug,\n\t\tlabel: row.label,\n\t\tparentId: row.parent_id ?? undefined,\n\t\tdescription: row.data ? JSON.parse(row.data).description : undefined,\n\t\tchildren,\n\t\tcount,\n\t\tlocale: row.locale,\n\t\ttranslationGroup: row.translation_group,\n\t};\n}\n\n/**\n * Terms assigned to a content entry, resolved into the active locale. Terms\n * whose translation_group lacks a row in the requested locale are omitted.\n */\nexport function getEntryTerms(\n\tcollection: string,\n\tentryId: string,\n\ttaxonomyName?: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<TaxonomyTerm[]> {\n\tconst locale = resolveLocale(options.locale);\n\treturn requestCached(\n\t\t`terms:${collection}:${entryId}:${taxonomyName ?? \"*\"}:${locale ?? \"*\"}`,\n\t\tasync () => {\n\t\t\tconst db = await getDb();\n\n\t\t\tlet query = db\n\t\t\t\t.selectFrom(\"content_taxonomies\")\n\t\t\t\t.innerJoin(\"taxonomies\", \"taxonomies.translation_group\", \"content_taxonomies.taxonomy_id\")\n\t\t\t\t.selectAll(\"taxonomies\")\n\t\t\t\t.where(\"content_taxonomies.collection\", \"=\", collection)\n\t\t\t\t.where(\"content_taxonomies.entry_id\", \"=\", entryId);\n\n\t\t\tif (taxonomyName) query = query.where(\"taxonomies.name\", \"=\", taxonomyName);\n\t\t\tif (locale !== undefined) query = query.where(\"taxonomies.locale\", \"=\", locale);\n\n\t\t\tconst rows = await query.execute();\n\t\t\treturn rows.map<TaxonomyTerm>((row) => ({\n\t\t\t\tid: row.id,\n\t\t\t\tname: row.name,\n\t\t\t\tslug: row.slug,\n\t\t\t\tlabel: row.label,\n\t\t\t\tparentId: row.parent_id ?? undefined,\n\t\t\t\tchildren: [],\n\t\t\t\tlocale: row.locale,\n\t\t\t\ttranslationGroup: row.translation_group,\n\t\t\t}));\n\t\t},\n\t);\n}\n\n/**\n * Terms for multiple entries of one taxonomy, single query.\n */\nexport async function getTermsForEntries(\n\tcollection: string,\n\tentryIds: string[],\n\ttaxonomyName: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<Map<string, TaxonomyTerm[]>> {\n\tconst result = new Map<string, TaxonomyTerm[]>();\n\tconst uniqueIds = [...new Set(entryIds)];\n\tfor (const id of uniqueIds) result.set(id, []);\n\tif (uniqueIds.length === 0) return result;\n\n\tconst db = await getDb();\n\tconst locale = resolveLocale(options.locale);\n\n\tfor (const chunk of chunks(uniqueIds, SQL_BATCH_SIZE)) {\n\t\tlet rows;\n\t\ttry {\n\t\t\tlet query = db\n\t\t\t\t.selectFrom(\"content_taxonomies\")\n\t\t\t\t.innerJoin(\"taxonomies\", \"taxonomies.translation_group\", \"content_taxonomies.taxonomy_id\")\n\t\t\t\t.select([\n\t\t\t\t\t\"content_taxonomies.entry_id\",\n\t\t\t\t\t\"taxonomies.id\",\n\t\t\t\t\t\"taxonomies.name\",\n\t\t\t\t\t\"taxonomies.slug\",\n\t\t\t\t\t\"taxonomies.label\",\n\t\t\t\t\t\"taxonomies.parent_id\",\n\t\t\t\t\t\"taxonomies.locale\",\n\t\t\t\t\t\"taxonomies.translation_group\",\n\t\t\t\t])\n\t\t\t\t.where(\"content_taxonomies.collection\", \"=\", collection)\n\t\t\t\t.where(\"content_taxonomies.entry_id\", \"in\", chunk)\n\t\t\t\t.where(\"taxonomies.name\", \"=\", taxonomyName);\n\t\t\tif (locale !== undefined) query = query.where(\"taxonomies.locale\", \"=\", locale);\n\t\t\trows = await query.execute();\n\t\t} catch (error) {\n\t\t\tif (isMissingTableError(error)) return result;\n\t\t\tthrow error;\n\t\t}\n\n\t\tfor (const row of rows) {\n\t\t\tconst term: TaxonomyTerm = {\n\t\t\t\tid: row.id,\n\t\t\t\tname: row.name,\n\t\t\t\tslug: row.slug,\n\t\t\t\tlabel: row.label,\n\t\t\t\tparentId: row.parent_id ?? undefined,\n\t\t\t\tchildren: [],\n\t\t\t\tlocale: row.locale,\n\t\t\t\ttranslationGroup: row.translation_group,\n\t\t\t};\n\t\t\tconst terms = result.get(row.entry_id);\n\t\t\tif (terms) terms.push(term);\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Batch-fetch terms for multiple entries across ALL taxonomies in one query.\n * Primes the request-cache for subsequent per-entry calls to `getEntryTerms`.\n */\nexport async function getAllTermsForEntries(\n\tcollection: string,\n\tentryIds: string[],\n\toptions: TaxonomyQueryOptions = {},\n): Promise<Map<string, Record<string, TaxonomyTerm[]>>> {\n\tconst result = new Map<string, Record<string, TaxonomyTerm[]>>();\n\tconst uniqueIds = [...new Set(entryIds)];\n\tfor (const id of uniqueIds) result.set(id, {});\n\tif (uniqueIds.length === 0) return result;\n\n\tconst db = await getDb();\n\tconst locale = resolveLocale(options.locale);\n\tconst applicableTaxonomyNames = await getCollectionTaxonomyNames(collection, { locale });\n\n\tfor (const chunk of chunks(uniqueIds, SQL_BATCH_SIZE)) {\n\t\tlet rows;\n\t\ttry {\n\t\t\tlet query = db\n\t\t\t\t.selectFrom(\"content_taxonomies\")\n\t\t\t\t.innerJoin(\"taxonomies\", \"taxonomies.translation_group\", \"content_taxonomies.taxonomy_id\")\n\t\t\t\t.select([\n\t\t\t\t\t\"content_taxonomies.entry_id\",\n\t\t\t\t\t\"taxonomies.id\",\n\t\t\t\t\t\"taxonomies.name\",\n\t\t\t\t\t\"taxonomies.slug\",\n\t\t\t\t\t\"taxonomies.label\",\n\t\t\t\t\t\"taxonomies.parent_id\",\n\t\t\t\t\t\"taxonomies.locale\",\n\t\t\t\t\t\"taxonomies.translation_group\",\n\t\t\t\t])\n\t\t\t\t.where(\"content_taxonomies.collection\", \"=\", collection)\n\t\t\t\t.where(\"content_taxonomies.entry_id\", \"in\", chunk)\n\t\t\t\t.orderBy(\"taxonomies.label\", \"asc\");\n\t\t\tif (locale !== undefined) query = query.where(\"taxonomies.locale\", \"=\", locale);\n\t\t\trows = await query.execute();\n\t\t} catch (error) {\n\t\t\tif (isMissingTableError(error)) {\n\t\t\t\tfor (const id of uniqueIds) {\n\t\t\t\t\tprimeEntryTermsCache(collection, id, {}, applicableTaxonomyNames, locale);\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\n\t\tfor (const row of rows) {\n\t\t\tconst term: TaxonomyTerm = {\n\t\t\t\tid: row.id,\n\t\t\t\tname: row.name,\n\t\t\t\tslug: row.slug,\n\t\t\t\tlabel: row.label,\n\t\t\t\tparentId: row.parent_id ?? undefined,\n\t\t\t\tchildren: [],\n\t\t\t\tlocale: row.locale,\n\t\t\t\ttranslationGroup: row.translation_group,\n\t\t\t};\n\t\t\tconst byTaxonomy = result.get(row.entry_id);\n\t\t\tif (!byTaxonomy) continue;\n\t\t\tconst existing = byTaxonomy[row.name];\n\t\t\tif (existing) existing.push(term);\n\t\t\telse byTaxonomy[row.name] = [term];\n\t\t}\n\t}\n\n\tfor (const [entryId, byTaxonomy] of result) {\n\t\tprimeEntryTermsCache(collection, entryId, byTaxonomy, applicableTaxonomyNames, locale);\n\t}\n\n\treturn result;\n}\n\n/**\n * Return the list of taxonomy names applicable to a collection, request-\n * cached so a page render only pays for it once.\n *\n * Returns an empty list when taxonomies haven't been defined yet.\n */\nasync function getCollectionTaxonomyNames(\n\tcollection: string,\n\toptions: TaxonomyQueryOptions,\n): Promise<string[]> {\n\ttry {\n\t\tconst defs = await getTaxonomyDefs(options);\n\t\treturn defs.filter((d) => d.collections.includes(collection)).map((d) => d.name);\n\t} catch (error) {\n\t\tif (isMissingTableError(error)) return [];\n\t\tthrow error;\n\t}\n}\n\n/**\n * Pre-populate the request-cache for every getEntryTerms call-shape that\n * could hit this entry:\n *\n * getEntryTerms(collection, entryId) -> key `terms:C:E:*`\n * getEntryTerms(collection, entryId, \"tag\") -> key `terms:C:E:tag`\n * getEntryTerms(collection, entryId, \"category\") -> key `terms:C:E:category`\n * ...one per taxonomy that applies to this collection\n *\n * Taxonomies with no rows on this entry are seeded with `[]` so legacy\n * callers short-circuit to the cached empty array instead of re-querying.\n */\nfunction primeEntryTermsCache(\n\tcollection: string,\n\tentryId: string,\n\tbyTaxonomy: Record<string, TaxonomyTerm[]>,\n\tapplicableTaxonomyNames: string[],\n\tlocale: string | undefined,\n): void {\n\tconst localeKey = locale ?? \"*\";\n\tfor (const name of applicableTaxonomyNames) {\n\t\tsetRequestCacheEntry(\n\t\t\t`terms:${collection}:${entryId}:${name}:${localeKey}`,\n\t\t\tbyTaxonomy[name] ?? [],\n\t\t);\n\t}\n\tfor (const [name, terms] of Object.entries(byTaxonomy)) {\n\t\tsetRequestCacheEntry(`terms:${collection}:${entryId}:${name}:${localeKey}`, terms);\n\t}\n\tconst allTerms = Object.values(byTaxonomy).flat();\n\tsetRequestCacheEntry(`terms:${collection}:${entryId}:*:${localeKey}`, allTerms);\n}\n\n/**\n * Get entries by term. Both the lookup (term slug in the active locale) and\n * the content query respect the active locale.\n */\nexport async function getEntriesByTerm(\n\tcollection: string,\n\ttaxonomyName: string,\n\ttermSlug: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<Array<{ id: string; data: Record<string, unknown> }>> {\n\tconst { getEmDashCollection } = await import(\"../query.js\");\n\n\tconst queryOptions: Record<string, unknown> = {\n\t\twhere: { [taxonomyName]: termSlug },\n\t};\n\tif (options.locale !== undefined) queryOptions.locale = options.locale;\n\tconst { entries } = await getEmDashCollection(collection, queryOptions);\n\treturn entries;\n}\n\nfunction rowToTaxonomyDef(row: {\n\tid: string;\n\tname: string;\n\tlabel: string;\n\tlabel_singular: string | null;\n\thierarchical: number;\n\tcollections: string | null;\n\tlocale: string;\n\ttranslation_group: string | null;\n}): TaxonomyDef {\n\treturn {\n\t\tid: row.id,\n\t\tname: row.name,\n\t\tlabel: row.label,\n\t\tlabelSingular: row.label_singular ?? undefined,\n\t\thierarchical: row.hierarchical === 1,\n\t\tcollections: row.collections ? JSON.parse(row.collections) : [],\n\t\tlocale: row.locale,\n\t\ttranslationGroup: row.translation_group,\n\t};\n}\n\n/**\n * Build tree structure from flat terms\n */\nfunction buildTree(flatTerms: TaxonomyTermRow[], counts: Map<string, number>): TaxonomyTerm[] {\n\tconst map = new Map<string, TaxonomyTerm>();\n\tconst roots: TaxonomyTerm[] = [];\n\n\tfor (const term of flatTerms) {\n\t\tmap.set(term.id, {\n\t\t\tid: term.id,\n\t\t\tname: term.name,\n\t\t\tslug: term.slug,\n\t\t\tlabel: term.label,\n\t\t\tparentId: term.parent_id ?? undefined,\n\t\t\tdescription: term.data ? JSON.parse(term.data).description : undefined,\n\t\t\tchildren: [],\n\t\t\tcount: counts.get(term.translation_group ?? term.id) ?? 0,\n\t\t\tlocale: term.locale,\n\t\t\ttranslationGroup: term.translation_group,\n\t\t});\n\t}\n\n\tfor (const term of map.values()) {\n\t\tif (term.parentId && map.has(term.parentId)) {\n\t\t\tmap.get(term.parentId)!.children.push(term);\n\t\t} else {\n\t\t\troots.push(term);\n\t\t}\n\t}\n\n\treturn roots;\n}\n"],"mappings":"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"}
|
|
@@ -200,19 +200,23 @@ declare const RESERVED_COLLECTION_SLUGS: string[];
|
|
|
200
200
|
* Global configuration for the site (title, logo, social links, etc.)
|
|
201
201
|
*/
|
|
202
202
|
/**
|
|
203
|
-
* Media reference for logo/favicon.
|
|
203
|
+
* Media reference for logo/favicon/seo.defaultOgImage.
|
|
204
204
|
*
|
|
205
205
|
* Stored shape is just `{ mediaId, alt? }`. The remaining fields are
|
|
206
206
|
* populated by `resolveMediaReference` on read so templates can emit
|
|
207
207
|
* correct head tags without a second round-trip to the media table.
|
|
208
208
|
*
|
|
209
|
-
* The Zod schemas at the REST/MCP boundary
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
209
|
+
* The Zod schemas at the REST/MCP boundary are split:
|
|
210
|
+
* - `mediaReferenceInput` (used by `settingsUpdateBody`) defines only
|
|
211
|
+
* `mediaId` and `alt`. Default strip-mode parsing discards any
|
|
212
|
+
* resolved fields a client posts back, so they never reach storage.
|
|
213
|
+
* - `mediaReferenceResponse` (used by `siteSettingsSchema`) includes
|
|
214
|
+
* the resolved fields so generated OpenAPI clients see them.
|
|
215
|
+
*
|
|
216
|
+
* If you ever switch `mediaReferenceInput` to `passthrough`, you must
|
|
217
|
+
* also strip the resolved fields explicitly in `setSiteSettings`, or
|
|
218
|
+
* stored options will accumulate stale `url` / `contentType` / `width`
|
|
219
|
+
* / `height` snapshots.
|
|
216
220
|
*/
|
|
217
221
|
interface MediaReference {
|
|
218
222
|
mediaId: string;
|
|
@@ -263,4 +267,4 @@ interface SiteSettings {
|
|
|
263
267
|
type SiteSettingKey = keyof SiteSettings;
|
|
264
268
|
//#endregion
|
|
265
269
|
export { RESERVED_COLLECTION_SLUGS as _, Collection as a, UpdateFieldInput as b, CollectionWithFields as c, CreateFieldInput as d, FIELD_TYPE_TO_COLUMN as f, FieldWidgetOptions as g, FieldValidation as h, SiteSettings as i, ColumnType as l, FieldType as m, SeoSettings as n, CollectionSource as o, Field as p, SiteSettingKey as r, CollectionSupport as s, MediaReference as t, CreateCollectionInput as u, RESERVED_FIELD_SLUGS as v, UpdateCollectionInput as y };
|
|
266
|
-
//# sourceMappingURL=types-
|
|
270
|
+
//# sourceMappingURL=types-B1gLSAH2.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types-
|
|
1
|
+
{"version":3,"file":"types-B1gLSAH2.d.mts","names":[],"sources":["../src/schema/types.ts","../src/settings/types.ts"],"mappings":"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"}
|
|
@@ -13,4 +13,4 @@ function isStandardPluginDefinition(value) {
|
|
|
13
13
|
|
|
14
14
|
//#endregion
|
|
15
15
|
export { normalizeCapability as a, normalizeCapabilities as i, isDeprecatedCapability as n, isStandardPluginDefinition as r, CAPABILITY_RENAMES as t };
|
|
16
|
-
//# sourceMappingURL=types-
|
|
16
|
+
//# sourceMappingURL=types-Cug_RO3W.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types-DiI8NOG_.mjs","names":[],"sources":["../src/plugins/types.ts"],"sourcesContent":["/**\n * Plugin System Types v2\n *\n * New plugin API with:\n * - Single unified context shape for all hooks and routes\n * - Paginated storage queries (no async iterators)\n * - Unified KV API (replaces settings + options)\n * - Explicit ctx.http and ctx.log\n *\n */\n\nimport type { Element } from \"@emdash-cms/blocks\";\n// The plugin capability vocabulary, the legacy-rename map, and the manifest\n// shape are authored once in @emdash-cms/plugin-types and shared between core\n// (the manifest reader at install/runtime) and @emdash-cms/registry-cli (the\n// manifest writer at bundle/publish time).\n//\n// We import-and-re-export here so existing internal callers keep working\n// (e.g. `import { PluginCapability } from \"../plugins/types.js\"`).\nimport {\n\tCAPABILITY_RENAMES,\n\tisDeprecatedCapability,\n\tnormalizeCapabilities,\n\tnormalizeCapability,\n\ttype CurrentPluginCapability,\n\ttype DeprecatedPluginCapability,\n\ttype ManifestHookEntry,\n\ttype ManifestRouteEntry,\n\ttype PluginCapability,\n\ttype PluginStorageConfig,\n\ttype StorageCollectionConfig,\n} from \"@emdash-cms/plugin-types\";\nimport type { JSX } from \"astro/jsx-runtime\";\nimport type { z } from \"astro/zod\";\n// =============================================================================\n// Core Types\n// =============================================================================\n\nimport type { FieldType } from \"../schema/types.js\";\n\nexport {\n\tCAPABILITY_RENAMES,\n\tisDeprecatedCapability,\n\tnormalizeCapabilities,\n\tnormalizeCapability,\n\ttype CurrentPluginCapability,\n\ttype DeprecatedPluginCapability,\n\ttype ManifestHookEntry,\n\ttype ManifestRouteEntry,\n\ttype PluginCapability,\n\ttype PluginStorageConfig,\n\ttype StorageCollectionConfig,\n};\n\n// =============================================================================\n// Storage Types\n// =============================================================================\n//\n// `StorageCollectionConfig` and `PluginStorageConfig` are re-exported above\n// from `@emdash-cms/plugin-types`. The manifest carries these shapes\n// verbatim; both this package (reader) and registry-cli (writer) agree on\n// the same types via the shared package.\n\n/**\n * Query filter operators\n */\nexport interface RangeFilter {\n\tgt?: number | string;\n\tgte?: number | string;\n\tlt?: number | string;\n\tlte?: number | string;\n}\n\nexport interface InFilter {\n\tin: Array<string | number>;\n}\n\nexport interface StartsWithFilter {\n\tstartsWith: string;\n}\n\n/**\n * Where clause value types\n */\nexport type WhereValue =\n\t| string\n\t| number\n\t| boolean\n\t| null\n\t| RangeFilter\n\t| InFilter\n\t| StartsWithFilter;\n\n/**\n * Where clause for storage queries\n */\nexport type WhereClause = Record<string, WhereValue>;\n\n/**\n * Query options for storage.query()\n */\nexport interface QueryOptions {\n\twhere?: WhereClause;\n\torderBy?: Record<string, \"asc\" | \"desc\">;\n\tlimit?: number; // Default 50, max 1000\n\tcursor?: string;\n}\n\n/**\n * Paginated result (used by storage.query, content.list, media.list)\n */\nexport interface PaginatedResult<T> {\n\titems: T[];\n\tcursor?: string;\n\thasMore: boolean;\n}\n\n/**\n * Storage collection interface - the API exposed to plugins\n * No async iterators - all operations return promises with pagination\n */\nexport interface StorageCollection<T = unknown> {\n\t// Basic CRUD\n\tget(id: string): Promise<T | null>;\n\tput(id: string, data: T): Promise<void>;\n\tdelete(id: string): Promise<boolean>;\n\texists(id: string): Promise<boolean>;\n\n\t// Batch operations\n\tgetMany(ids: string[]): Promise<Map<string, T>>;\n\tputMany(items: Array<{ id: string; data: T }>): Promise<void>;\n\tdeleteMany(ids: string[]): Promise<number>;\n\n\t// Query - always paginated\n\tquery(options?: QueryOptions): Promise<PaginatedResult<{ id: string; data: T }>>;\n\tcount(where?: WhereClause): Promise<number>;\n}\n\n/**\n * Plugin storage context - typed based on declared collections\n */\nexport type PluginStorage<T extends PluginStorageConfig> = {\n\t[K in keyof T]: StorageCollection;\n};\n\n// =============================================================================\n// Context APIs\n// =============================================================================\n\n/**\n * KV store interface - unified replacement for settings + options\n *\n * Convention:\n * - `settings:*` - User-configurable preferences (shown in admin UI)\n * - `state:*` - Internal plugin state (not shown to users)\n */\nexport interface KVAccess {\n\tget<T>(key: string): Promise<T | null>;\n\tset(key: string, value: unknown): Promise<void>;\n\tdelete(key: string): Promise<boolean>;\n\tlist(prefix?: string): Promise<Array<{ key: string; value: unknown }>>;\n}\n\n/**\n * SEO metadata for a content item, as stored in the core SEO panel.\n *\n * Only present on items in collections with `has_seo = 1`. For collections\n * without SEO enabled, `ContentItem.seo` is `undefined`.\n */\nexport interface ContentItemSeo {\n\ttitle: string | null;\n\tdescription: string | null;\n\timage: string | null;\n\tcanonical: string | null;\n\tnoIndex: boolean;\n}\n\n/**\n * SEO input accepted by content write operations.\n *\n * All fields are optional — only fields that are present overwrite existing\n * values. An empty object is treated as a no-op.\n */\nexport interface ContentItemSeoInput {\n\ttitle?: string | null;\n\tdescription?: string | null;\n\timage?: string | null;\n\tcanonical?: string | null;\n\tnoIndex?: boolean;\n}\n\n/**\n * Content item returned from content API\n */\nexport interface ContentItem {\n\tid: string;\n\ttype: string;\n\tslug: string | null;\n\tstatus: string;\n\tlocale: string | null;\n\tdata: Record<string, unknown>;\n\t/**\n\t * SEO metadata, populated when the collection has SEO enabled\n\t * (`has_seo = 1`). `undefined` for non-SEO collections.\n\t */\n\tseo?: ContentItemSeo;\n\tcreatedAt: string;\n\tupdatedAt: string;\n\tpublishedAt: string | null;\n}\n\nexport interface ContentListWhere {\n\t/** Exact match on `status` (e.g. `\"published\"`, `\"draft\"`). */\n\tstatus?: string;\n\t/** Exact match on `locale` (e.g. `\"en\"`, `\"fr-CA\"`). */\n\tlocale?: string;\n}\n\n/**\n * Content list options\n */\nexport interface ContentListOptions {\n\tlimit?: number;\n\tcursor?: string;\n\torderBy?: Record<string, \"asc\" | \"desc\">;\n\twhere?: ContentListWhere;\n}\n\n/**\n * Input accepted by `content.create` / `content.update`.\n *\n * Most entries are field slugs mapped to their values. The reserved `seo`\n * key is extracted and routed to the core SEO panel (the `_emdash_seo`\n * table), matching the shape accepted by the REST API. Passing `seo` for a\n * collection that does not have SEO enabled throws a validation error.\n */\nexport type ContentWriteInput = Record<string, unknown> & {\n\tseo?: ContentItemSeoInput;\n};\n\n/**\n * Content access interface - capability-gated\n */\nexport interface ContentAccess {\n\t// Read operations (requires read:content)\n\tget(collection: string, id: string): Promise<ContentItem | null>;\n\tlist(collection: string, options?: ContentListOptions): Promise<PaginatedResult<ContentItem>>;\n\n\t// Write operations (requires write:content) - optional on interface\n\tcreate?(collection: string, data: ContentWriteInput): Promise<ContentItem>;\n\tupdate?(collection: string, id: string, data: ContentWriteInput): Promise<ContentItem>;\n\tdelete?(collection: string, id: string): Promise<boolean>;\n}\n\n/**\n * Full content access with write operations\n */\nexport interface ContentAccessWithWrite extends ContentAccess {\n\tcreate(collection: string, data: ContentWriteInput): Promise<ContentItem>;\n\tupdate(collection: string, id: string, data: ContentWriteInput): Promise<ContentItem>;\n\tdelete(collection: string, id: string): Promise<boolean>;\n}\n\n/**\n * Media item returned from media API\n */\nexport interface MediaItem {\n\tid: string;\n\tfilename: string;\n\tmimeType: string;\n\tsize: number | null;\n\turl: string;\n\tcreatedAt: string;\n}\n\n/**\n * Media list options\n */\nexport interface MediaListOptions {\n\tlimit?: number;\n\tcursor?: string;\n\tmimeType?: string; // Filter by mime type prefix, e.g., \"image/\"\n}\n\n/**\n * Media access interface - capability-gated\n */\nexport interface MediaAccess {\n\t// Read operations (requires read:media)\n\tget(id: string): Promise<MediaItem | null>;\n\tlist(options?: MediaListOptions): Promise<PaginatedResult<MediaItem>>;\n\n\t// Write operations (requires write:media) - optional on interface\n\tgetUploadUrl?(\n\t\tfilename: string,\n\t\tcontentType: string,\n\t): Promise<{ uploadUrl: string; mediaId: string }>;\n\t/**\n\t * Upload media bytes directly. Preferred in sandboxed mode where\n\t * plugins cannot make external requests to a presigned URL.\n\t * Returns the created media item.\n\t */\n\tupload?(\n\t\tfilename: string,\n\t\tcontentType: string,\n\t\tbytes: ArrayBuffer,\n\t): Promise<{ mediaId: string; storageKey: string; url: string }>;\n\tdelete?(id: string): Promise<boolean>;\n}\n\n/**\n * Full media access with write operations\n */\nexport interface MediaAccessWithWrite extends MediaAccess {\n\tgetUploadUrl(\n\t\tfilename: string,\n\t\tcontentType: string,\n\t): Promise<{ uploadUrl: string; mediaId: string }>;\n\tupload(\n\t\tfilename: string,\n\t\tcontentType: string,\n\t\tbytes: ArrayBuffer,\n\t): Promise<{ mediaId: string; storageKey: string; url: string }>;\n\tdelete(id: string): Promise<boolean>;\n}\n\n/**\n * HTTP client interface - requires network:fetch capability\n */\nexport interface HttpAccess {\n\tfetch(url: string, init?: RequestInit): Promise<Response>;\n}\n\n/**\n * Logger interface - always available\n */\nexport interface LogAccess {\n\tdebug(message: string, data?: unknown): void;\n\tinfo(message: string, data?: unknown): void;\n\twarn(message: string, data?: unknown): void;\n\terror(message: string, data?: unknown): void;\n}\n\n// =============================================================================\n// Site & User Access\n// =============================================================================\n\n/**\n * Site information available to all plugins\n */\nexport interface SiteInfo {\n\t/** Site name (from settings) */\n\tname: string;\n\t/** Site URL (from settings or request) */\n\turl: string;\n\t/** Site locale (from settings, defaults to \"en\") */\n\tlocale: string;\n}\n\n/**\n * Read-only user information exposed to plugins.\n * Sensitive fields (password hashes, sessions, passkeys) are excluded.\n */\nexport interface UserInfo {\n\tid: string;\n\temail: string;\n\tname: string | null;\n\trole: number;\n\tcreatedAt: string;\n}\n\n/**\n * User access interface - requires read:users capability\n */\nexport interface UserAccess {\n\t/** Get a user by ID */\n\tget(id: string): Promise<UserInfo | null>;\n\t/** Get a user by email */\n\tgetByEmail(email: string): Promise<UserInfo | null>;\n\t/** List users with optional filters */\n\tlist(opts?: { role?: number; limit?: number; cursor?: string }): Promise<{\n\t\titems: UserInfo[];\n\t\tnextCursor?: string;\n\t}>;\n}\n\n// =============================================================================\n// Plugin Context\n// =============================================================================\n\n/**\n * The unified plugin context - same shape for all hooks and routes\n */\nexport interface PluginContext<TStorage extends PluginStorageConfig = PluginStorageConfig> {\n\t/** Plugin metadata */\n\tplugin: {\n\t\tid: string;\n\t\tversion: string;\n\t};\n\n\t/** Storage collections - only if plugin declares storage */\n\tstorage: PluginStorage<TStorage>;\n\n\t/** Key-value store for config and state */\n\tkv: KVAccess;\n\n\t/** Content access - only if read:content or write:content capability */\n\tcontent?: ContentAccess | ContentAccessWithWrite;\n\n\t/** Media access - only if read:media or write:media capability */\n\tmedia?: MediaAccess | MediaAccessWithWrite;\n\n\t/** HTTP client - only if network:fetch capability */\n\thttp?: HttpAccess;\n\n\t/** Logger - always available */\n\tlog: LogAccess;\n\n\t/** Site information - always available */\n\tsite: SiteInfo;\n\n\t/** URL helper - generates absolute URLs from paths. Always available. */\n\turl(path: string): string;\n\n\t/** User access - only if read:users capability */\n\tusers?: UserAccess;\n\n\t/** Cron task scheduling - always available, scoped to plugin */\n\tcron?: CronAccess;\n\n\t/** Email access - only if email:send capability and a provider is configured */\n\temail?: EmailAccess;\n}\n\n// =============================================================================\n// Cron Types\n// =============================================================================\n\n/**\n * Cron access interface �� always available on plugin context, scoped to plugin.\n */\nexport interface CronAccess {\n\t/** Schedule a recurring or one-shot task */\n\tschedule(name: string, opts: { schedule: string; data?: Record<string, unknown> }): Promise<void>;\n\t/** Cancel a scheduled task */\n\tcancel(name: string): Promise<void>;\n\t/** List this plugin's scheduled tasks */\n\tlist(): Promise<CronTaskInfo[]>;\n}\n\n/**\n * Task info returned from CronAccess.list()\n */\nexport interface CronTaskInfo {\n\tname: string;\n\tschedule: string;\n\tnextRunAt: string;\n\tlastRunAt: string | null;\n}\n\n/**\n * Event passed to the `cron` hook handler\n */\nexport interface CronEvent {\n\tname: string;\n\tdata?: Record<string, unknown>;\n\tscheduledAt: string;\n}\n\n/**\n * Cron hook handler type\n */\nexport type CronHandler = (event: CronEvent, ctx: PluginContext) => Promise<void>;\n\n// =============================================================================\n// Email Types\n// =============================================================================\n\n/**\n * Email access interface — requires `email:send` capability.\n * Undefined when no `email:deliver` provider is configured.\n *\n * Related capabilities:\n * - `email:send` — grants ctx.email (this interface)\n * - `email:provide` — allows registering the `email:deliver` exclusive hook\n * - `email:intercept` — allows registering `email:beforeSend` / `email:afterSend` hooks\n */\nexport interface EmailAccess {\n\tsend(message: EmailMessage): Promise<void>;\n}\n\n/**\n * Email message shape\n */\nexport interface EmailMessage {\n\tto: string;\n\tsubject: string;\n\ttext: string;\n\thtml?: string;\n}\n\n/**\n * Event passed to email:beforeSend hooks (middleware — transform, validate, cancel)\n */\nexport interface EmailBeforeSendEvent {\n\tmessage: EmailMessage;\n\t/** Where the email originated — \"system\" for auth emails, plugin ID for plugin emails */\n\tsource: string;\n}\n\n/**\n * Event passed to email:deliver hook (exclusive — exactly one provider delivers)\n */\nexport interface EmailDeliverEvent {\n\tmessage: EmailMessage;\n\tsource: string;\n}\n\n/**\n * Event passed to email:afterSend hooks (logging, analytics, fire-and-forget)\n */\nexport interface EmailAfterSendEvent {\n\tmessage: EmailMessage;\n\tsource: string;\n}\n\n/**\n * Handler type for email:beforeSend hooks.\n * Returns modified message, or false to cancel delivery.\n */\nexport type EmailBeforeSendHandler = (\n\tevent: EmailBeforeSendEvent,\n\tctx: PluginContext,\n) => Promise<EmailMessage | false>;\n\n/**\n * Handler type for email:deliver hooks (exclusive provider).\n */\nexport type EmailDeliverHandler = (event: EmailDeliverEvent, ctx: PluginContext) => Promise<void>;\n\n/**\n * Handler type for email:afterSend hooks (fire-and-forget).\n */\nexport type EmailAfterSendHandler = (\n\tevent: EmailAfterSendEvent,\n\tctx: PluginContext,\n) => Promise<void>;\n\n// =============================================================================\n// Comment Types\n// =============================================================================\n\n/**\n * Collection comment settings (read from _emdash_collections)\n */\nexport interface CollectionCommentSettings {\n\tcommentsEnabled: boolean;\n\tcommentsModeration: \"all\" | \"first_time\" | \"none\";\n\tcommentsClosedAfterDays: number;\n\tcommentsAutoApproveUsers: boolean;\n}\n\n/**\n * Event passed to comment:beforeCreate hooks (middleware — transform, enrich, reject)\n */\nexport interface CommentBeforeCreateEvent {\n\tcomment: {\n\t\tcollection: string;\n\t\tcontentId: string;\n\t\tparentId: string | null;\n\t\tauthorName: string;\n\t\tauthorEmail: string;\n\t\tauthorUserId: string | null;\n\t\tbody: string;\n\t\tipHash: string | null;\n\t\tuserAgent: string | null;\n\t};\n\t/** Metadata bag — plugins can attach signals for the moderator */\n\tmetadata: Record<string, unknown>;\n}\n\n/**\n * Event passed to comment:moderate hook (exclusive — decides initial status)\n */\nexport interface CommentModerateEvent {\n\tcomment: CommentBeforeCreateEvent[\"comment\"];\n\tmetadata: Record<string, unknown>;\n\tcollectionSettings: CollectionCommentSettings;\n\t/** Number of prior approved comments from this email address */\n\tpriorApprovedCount: number;\n}\n\n/**\n * Moderation decision returned by the comment:moderate handler\n */\nexport interface ModerationDecision {\n\tstatus: \"approved\" | \"pending\" | \"spam\";\n\t/** Optional reason for admin visibility */\n\treason?: string;\n}\n\n/**\n * Stored comment shape (full record with id, status, timestamps)\n */\nexport interface StoredComment {\n\tid: string;\n\tcollection: string;\n\tcontentId: string;\n\tparentId: string | null;\n\tauthorName: string;\n\tauthorEmail: string;\n\tauthorUserId: string | null;\n\tbody: string;\n\tstatus: string;\n\tmoderationMetadata: Record<string, unknown> | null;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n/**\n * Event passed to comment:afterCreate hooks (fire-and-forget)\n */\nexport interface CommentAfterCreateEvent {\n\tcomment: StoredComment;\n\tmetadata: Record<string, unknown>;\n\t/** The content item the comment is on */\n\tcontent: { id: string; collection: string; slug: string; title?: string };\n\t/** The content author (for notifications) */\n\tcontentAuthor?: { id: string; name: string | null; email: string };\n}\n\n/**\n * Event passed to comment:afterModerate hooks (fire-and-forget, admin status change)\n */\nexport interface CommentAfterModerateEvent {\n\tcomment: StoredComment;\n\tpreviousStatus: string;\n\tnewStatus: string;\n\t/** The admin who moderated */\n\tmoderator: { id: string; name: string | null };\n}\n\n/**\n * Handler type for comment:beforeCreate hooks.\n * Returns modified event, or false to reject the comment.\n */\nexport type CommentBeforeCreateHandler = (\n\tevent: CommentBeforeCreateEvent,\n\tctx: PluginContext,\n) => Promise<CommentBeforeCreateEvent | false | void>;\n\n/**\n * Handler type for comment:moderate hook (exclusive provider).\n */\nexport type CommentModerateHandler = (\n\tevent: CommentModerateEvent,\n\tctx: PluginContext,\n) => Promise<ModerationDecision>;\n\n/**\n * Handler type for comment:afterCreate hooks (fire-and-forget).\n */\nexport type CommentAfterCreateHandler = (\n\tevent: CommentAfterCreateEvent,\n\tctx: PluginContext,\n) => Promise<void>;\n\n/**\n * Handler type for comment:afterModerate hooks (fire-and-forget).\n */\nexport type CommentAfterModerateHandler = (\n\tevent: CommentAfterModerateEvent,\n\tctx: PluginContext,\n) => Promise<void>;\n\n// =============================================================================\n// Hook Types\n// =============================================================================\n\n/**\n * Hook configuration\n */\nexport interface HookConfig<THandler> {\n\t/** Explicit ordering - lower numbers run first (default: 100) */\n\tpriority?: number;\n\t/** Max execution time in ms (default: 5000) */\n\ttimeout?: number;\n\t/** Run after these plugins */\n\tdependencies?: string[];\n\t/** Error handling policy */\n\terrorPolicy?: \"continue\" | \"abort\";\n\t/**\n\t * Mark this hook as exclusive — only one plugin can be the active provider.\n\t * Exclusive hooks skip the priority pipeline and dispatch only to the\n\t * admin-selected provider. Used for email:deliver, search, image optimization, etc.\n\t */\n\texclusive?: boolean;\n\t/** The hook handler */\n\thandler: THandler;\n}\n\n/**\n * Content hook event\n */\nexport interface ContentHookEvent {\n\tcontent: Record<string, unknown>;\n\tcollection: string;\n\tisNew: boolean;\n}\n\n/**\n * Content delete hook event\n */\nexport interface ContentDeleteEvent {\n\tid: string;\n\tcollection: string;\n\t/** `true` when the content is permanently deleted (not just trashed). */\n\tpermanent: boolean;\n}\n\n/**\n * Content publish state change hook event (fired after publish or unpublish)\n */\nexport interface ContentPublishStateChangeEvent {\n\tcontent: Record<string, unknown>;\n\tcollection: string;\n}\n\n/**\n * Media hook event\n */\nexport interface MediaUploadEvent {\n\tfile: { name: string; type: string; size: number };\n}\n\n/**\n * Media after upload event\n */\nexport interface MediaAfterUploadEvent {\n\tmedia: MediaItem;\n}\n\n/**\n * Lifecycle hook event\n */\nexport interface LifecycleEvent {\n\t// Empty for install/activate/deactivate\n}\n\n/**\n * Uninstall hook event\n */\nexport interface UninstallEvent {\n\tdeleteData: boolean;\n}\n\n// Hook handler types - all receive (event, ctx) with unified context\nexport type ContentBeforeSaveHandler = (\n\tevent: ContentHookEvent,\n\tctx: PluginContext,\n) => Promise<Record<string, unknown> | void>;\n\nexport type ContentAfterSaveHandler = (\n\tevent: ContentHookEvent,\n\tctx: PluginContext,\n) => Promise<void>;\n\nexport type ContentBeforeDeleteHandler = (\n\tevent: ContentDeleteEvent,\n\tctx: PluginContext,\n) => Promise<boolean | void>;\n\nexport type ContentAfterDeleteHandler = (\n\tevent: ContentDeleteEvent,\n\tctx: PluginContext,\n) => Promise<void>;\n\nexport type ContentAfterPublishHandler = (\n\tevent: ContentPublishStateChangeEvent,\n\tctx: PluginContext,\n) => Promise<void>;\n\nexport type ContentAfterUnpublishHandler = (\n\tevent: ContentPublishStateChangeEvent,\n\tctx: PluginContext,\n) => Promise<void>;\n\nexport type MediaBeforeUploadHandler = (\n\tevent: MediaUploadEvent,\n\tctx: PluginContext,\n) => Promise<{ name: string; type: string; size: number } | void>;\n\nexport type MediaAfterUploadHandler = (\n\tevent: MediaAfterUploadEvent,\n\tctx: PluginContext,\n) => Promise<void>;\n\nexport type LifecycleHandler = (event: LifecycleEvent, ctx: PluginContext) => Promise<void>;\n\nexport type UninstallHandler = (event: UninstallEvent, ctx: PluginContext) => Promise<void>;\n\n// =============================================================================\n// Public Page Contribution Types\n// =============================================================================\n\n/** Placement targets for page fragment contributions */\nexport type PagePlacement = \"head\" | \"body:start\" | \"body:end\";\n\n/**\n * A single breadcrumb trail item. Used by `PublicPageContext.breadcrumbs`\n * so themes can publish breadcrumb trails that SEO plugins consume.\n */\nexport interface BreadcrumbItem {\n\t/** Display name for this crumb (e.g. \"Home\", \"Blog\", \"My Post\"). */\n\tname: string;\n\t/** Absolute or root-relative URL for this crumb. */\n\turl: string;\n}\n\n/**\n * Describes the page being rendered. Passed to page hooks so plugins\n * can decide what to contribute without fetching content themselves.\n */\nexport interface PublicPageContext {\n\turl: string;\n\tpath: string;\n\tlocale: string | null;\n\tkind: \"content\" | \"custom\";\n\tpageType: string;\n\t/** Full document title for the rendered page */\n\ttitle: string | null;\n\t/** Page-only title for OG/Twitter/JSON-LD headline output */\n\tpageTitle?: string | null;\n\tdescription: string | null;\n\tcanonical: string | null;\n\timage: string | null;\n\tcontent?: {\n\t\tcollection: string;\n\t\tid: string;\n\t\tslug: string | null;\n\t};\n\t/** SEO meta for base metadata generation in EmDashHead */\n\tseo?: {\n\t\togTitle?: string | null;\n\t\togDescription?: string | null;\n\t\togImage?: string | null;\n\t\trobots?: string | null;\n\t};\n\t/** Article metadata for Open Graph article: tags */\n\tarticleMeta?: {\n\t\tpublishedTime?: string | null;\n\t\tmodifiedTime?: string | null;\n\t\tauthor?: string | null;\n\t};\n\t/** Site name for structured data and og:site_name */\n\tsiteName?: string;\n\t/**\n\t * Optional breadcrumb trail for this page, root first. When set,\n\t * SEO plugins should use this verbatim rather than deriving a trail\n\t * from `path`. Themes typically populate this at the point they\n\t * build the context (e.g. from a content hierarchy walk, taxonomy\n\t * lookup, or per-`pageType` routing logic).\n\t *\n\t * Semantics for consumers:\n\t * - `undefined` — theme has no opinion; consumer falls back to\n\t * its own derivation.\n\t * - `[]` — this page has no breadcrumbs (e.g. homepage); consumer\n\t * should skip `BreadcrumbList` emission entirely.\n\t * - Non-empty array — used verbatim for `BreadcrumbList` output.\n\t */\n\tbreadcrumbs?: BreadcrumbItem[];\n\t/** Public-facing site URL (origin) for structured data */\n\tsiteUrl?: string;\n}\n\n// ── page:metadata ───────────────────────────────────────────────\n\nexport interface PageMetadataEvent {\n\tpage: PublicPageContext;\n}\n\n/**\n * Allowed rel values for link contributions.\n * This is a security-critical allowlist -- sandboxed plugins can only inject\n * link tags with these rel values. Adding \"stylesheet\", \"prefetch\", \"prerender\"\n * etc. would allow sandboxed plugins to inject external resources.\n */\nexport type PageMetadataLinkRel =\n\t| \"canonical\"\n\t| \"alternate\"\n\t| \"author\"\n\t| \"license\"\n\t| \"nlweb\"\n\t| \"site.standard.document\";\n\nexport type PageMetadataContribution =\n\t| { kind: \"meta\"; name: string; content: string; key?: string }\n\t| { kind: \"property\"; property: string; content: string; key?: string }\n\t| { kind: \"link\"; rel: PageMetadataLinkRel; href: string; hreflang?: string; key?: string }\n\t| {\n\t\t\tkind: \"jsonld\";\n\t\t\tid?: string;\n\t\t\tgraph: Record<string, unknown> | Array<Record<string, unknown>>;\n\t };\n\nexport type PageMetadataHandler = (\n\tevent: PageMetadataEvent,\n\tctx: PluginContext,\n) =>\n\t| PageMetadataContribution\n\t| PageMetadataContribution[]\n\t| null\n\t| Promise<PageMetadataContribution | PageMetadataContribution[] | null>;\n\n// ── page:fragments (trusted-only) ──────────────────────────────\n\nexport interface PageFragmentEvent {\n\tpage: PublicPageContext;\n}\n\nexport type PageFragmentContribution =\n\t| {\n\t\t\tkind: \"external-script\";\n\t\t\tplacement: PagePlacement;\n\t\t\tsrc: string;\n\t\t\tasync?: boolean;\n\t\t\tdefer?: boolean;\n\t\t\tattributes?: Record<string, string>;\n\t\t\tkey?: string;\n\t }\n\t| {\n\t\t\tkind: \"inline-script\";\n\t\t\tplacement: PagePlacement;\n\t\t\tcode: string;\n\t\t\tattributes?: Record<string, string>;\n\t\t\tkey?: string;\n\t }\n\t| {\n\t\t\tkind: \"html\";\n\t\t\tplacement: PagePlacement;\n\t\t\thtml: string;\n\t\t\tkey?: string;\n\t };\n\nexport type PageFragmentHandler = (\n\tevent: PageFragmentEvent,\n\tctx: PluginContext,\n) =>\n\t| PageFragmentContribution\n\t| PageFragmentContribution[]\n\t| null\n\t| Promise<PageFragmentContribution | PageFragmentContribution[] | null>;\n\n/**\n * Plugin hooks definition\n */\nexport interface PluginHooks {\n\t// Lifecycle hooks\n\t\"plugin:install\"?: HookConfig<LifecycleHandler> | LifecycleHandler;\n\t\"plugin:activate\"?: HookConfig<LifecycleHandler> | LifecycleHandler;\n\t\"plugin:deactivate\"?: HookConfig<LifecycleHandler> | LifecycleHandler;\n\t\"plugin:uninstall\"?: HookConfig<UninstallHandler> | UninstallHandler;\n\n\t// Content hooks\n\t\"content:beforeSave\"?: HookConfig<ContentBeforeSaveHandler> | ContentBeforeSaveHandler;\n\t\"content:afterSave\"?: HookConfig<ContentAfterSaveHandler> | ContentAfterSaveHandler;\n\t\"content:beforeDelete\"?: HookConfig<ContentBeforeDeleteHandler> | ContentBeforeDeleteHandler;\n\t\"content:afterDelete\"?: HookConfig<ContentAfterDeleteHandler> | ContentAfterDeleteHandler;\n\t\"content:afterPublish\"?: HookConfig<ContentAfterPublishHandler> | ContentAfterPublishHandler;\n\t\"content:afterUnpublish\"?:\n\t\t| HookConfig<ContentAfterUnpublishHandler>\n\t\t| ContentAfterUnpublishHandler;\n\n\t// Media hooks\n\t\"media:beforeUpload\"?: HookConfig<MediaBeforeUploadHandler> | MediaBeforeUploadHandler;\n\t\"media:afterUpload\"?: HookConfig<MediaAfterUploadHandler> | MediaAfterUploadHandler;\n\n\t// Cron hook\n\tcron?: HookConfig<CronHandler> | CronHandler;\n\n\t// Email hooks\n\t\"email:beforeSend\"?: HookConfig<EmailBeforeSendHandler> | EmailBeforeSendHandler;\n\t\"email:deliver\"?: HookConfig<EmailDeliverHandler> | EmailDeliverHandler;\n\t\"email:afterSend\"?: HookConfig<EmailAfterSendHandler> | EmailAfterSendHandler;\n\n\t// Comment hooks\n\t\"comment:beforeCreate\"?: HookConfig<CommentBeforeCreateHandler> | CommentBeforeCreateHandler;\n\t\"comment:moderate\"?: HookConfig<CommentModerateHandler> | CommentModerateHandler;\n\t\"comment:afterCreate\"?: HookConfig<CommentAfterCreateHandler> | CommentAfterCreateHandler;\n\t\"comment:afterModerate\"?: HookConfig<CommentAfterModerateHandler> | CommentAfterModerateHandler;\n\n\t// Public page hooks\n\t\"page:metadata\"?: HookConfig<PageMetadataHandler> | PageMetadataHandler;\n\t\"page:fragments\"?: HookConfig<PageFragmentHandler> | PageFragmentHandler;\n}\n\n/**\n * Hook names\n */\n/**\n * Hook name in a manifest. Core's exhaustive union of recognised hook names,\n * derived from the `PluginHooks` registry. The serialised manifest carries\n * these as opaque strings; this stricter type is only used for type-checking\n * inside core. `ManifestHookEntry` is re-exported from\n * `@emdash-cms/plugin-types` near the top of this file.\n */\nexport type HookName = keyof PluginHooks;\n\n/**\n * Resolved hook with normalized config\n */\nexport interface ResolvedHook<THandler> {\n\tpriority: number;\n\ttimeout: number;\n\tdependencies: string[];\n\terrorPolicy: \"continue\" | \"abort\";\n\t/** Whether this hook is exclusive (provider pattern) */\n\texclusive: boolean;\n\thandler: THandler;\n\tpluginId: string;\n}\n\n// =============================================================================\n// Request Metadata Types\n// =============================================================================\n\n/**\n * Geographic location information derived from the request.\n * Available when running on Cloudflare Workers (via the `cf` object).\n */\nexport interface GeoInfo {\n\tcountry: string | null;\n\tregion: string | null;\n\tcity: string | null;\n}\n\n/**\n * Normalized request metadata available to plugin route handlers.\n * Extracted from request headers and platform-specific properties.\n */\nexport interface RequestMeta {\n\tip: string | null;\n\tuserAgent: string | null;\n\treferer: string | null;\n\tgeo: GeoInfo | null;\n}\n\n// =============================================================================\n// Route Types\n// =============================================================================\n\n/**\n * Route handler context extends plugin context with request-specific data\n */\nexport interface RouteContext<TInput = unknown> extends PluginContext {\n\t/** Validated input from request body */\n\tinput: TInput;\n\t/** Original request */\n\trequest: Request;\n\t/** Normalized request metadata (IP, user agent, geo) */\n\trequestMeta: RequestMeta;\n}\n\n/**\n * Route definition\n */\nexport interface PluginRoute<TInput = unknown> {\n\t/** Zod schema for input validation */\n\tinput?: z.ZodType<TInput>;\n\t/**\n\t * Mark this route as publicly accessible (no authentication required).\n\t * Public routes skip session/token auth and CSRF checks.\n\t */\n\tpublic?: boolean;\n\t/** Route handler */\n\thandler: (ctx: RouteContext<TInput>) => Promise<unknown>;\n}\n\n// =============================================================================\n// Plugin Definition\n// =============================================================================\n\n/**\n * Admin page definition\n */\nexport interface PluginAdminPage {\n\tpath: string;\n\tlabel: string;\n\ticon?: string;\n}\n\n/**\n * Dashboard widget definition\n */\nexport interface PluginDashboardWidget {\n\tid: string;\n\tsize?: \"full\" | \"half\" | \"third\";\n\ttitle?: string;\n}\n\n/**\n * Settings field types (for admin UI generation)\n */\nexport type SettingFieldType =\n\t| \"string\"\n\t| \"number\"\n\t| \"boolean\"\n\t| \"select\"\n\t| \"secret\"\n\t| \"url\"\n\t| \"email\";\n\nexport interface BaseSettingField {\n\ttype: SettingFieldType;\n\tlabel: string;\n\tdescription?: string;\n}\n\nexport interface StringSettingField extends BaseSettingField {\n\ttype: \"string\";\n\tdefault?: string;\n\tmultiline?: boolean;\n}\n\nexport interface NumberSettingField extends BaseSettingField {\n\ttype: \"number\";\n\tdefault?: number;\n\tmin?: number;\n\tmax?: number;\n}\n\nexport interface BooleanSettingField extends BaseSettingField {\n\ttype: \"boolean\";\n\tdefault?: boolean;\n}\n\nexport interface SelectSettingField extends BaseSettingField {\n\ttype: \"select\";\n\toptions: Array<{ value: string; label: string }>;\n\tdefault?: string;\n}\n\nexport interface SecretSettingField extends BaseSettingField {\n\ttype: \"secret\";\n}\n\nexport interface UrlSettingField extends BaseSettingField {\n\ttype: \"url\";\n\tdefault?: string;\n\tplaceholder?: string;\n}\n\nexport interface EmailSettingField extends BaseSettingField {\n\ttype: \"email\";\n\tdefault?: string;\n\tplaceholder?: string;\n}\n\nexport type SettingField =\n\t| StringSettingField\n\t| NumberSettingField\n\t| BooleanSettingField\n\t| SelectSettingField\n\t| SecretSettingField\n\t| UrlSettingField\n\t| EmailSettingField;\n\n/**\n * Block Kit element for block editing fields.\n * This is the `Element` discriminated union from `@emdash-cms/blocks`.\n * Plugin authors should use `@emdash-cms/blocks` builder functions to create these.\n */\nexport type PortableTextBlockField = Element;\n\n/**\n * Configuration for a Portable Text block type contributed by a plugin\n */\nexport interface PortableTextBlockConfig {\n\t/** Block type name (must match the `_type` in Portable Text) */\n\ttype: string;\n\t/** Human-readable label shown in slash commands and modals */\n\tlabel: string;\n\t/** Icon key (e.g., \"video\", \"code\", \"link\", \"link-external\") */\n\ticon?: string;\n\t/** Description shown in slash command menu */\n\tdescription?: string;\n\t/** Placeholder text for the URL input */\n\tplaceholder?: string;\n\t/** Block Kit form fields for the editing UI. If declared, replaces the simple URL input. */\n\tfields?: PortableTextBlockField[];\n\t/**\n\t * Optional. Display category in the slash menu. Defaults to \"Embeds\".\n\t *\n\t * Plugin authors should pick a meaningful category that reflects what the\n\t * block actually is — e.g. \"Sections\", \"Marketing\", \"Media\", \"Embeds\",\n\t * \"Layout\". Blocks with the same category are grouped together in the\n\t * editor's slash menu.\n\t */\n\tcategory?: string;\n}\n\n/**\n * Configuration for a field widget type contributed by a plugin.\n * A field widget provides a custom editing UI for a schema field.\n * The field references the widget via `widget: \"pluginId:widgetName\"`.\n */\nexport interface FieldWidgetConfig {\n\t/** Widget name (without plugin ID prefix) */\n\tname: string;\n\t/** Human-readable label for the admin UI */\n\tlabel: string;\n\t/** Which field types this widget can edit (e.g., [\"json\", \"string\"]) */\n\tfieldTypes: FieldType[];\n\t/** Block Kit elements for sandboxed rendering. Omit for trusted plugins using React. */\n\telements?: Element[];\n}\n\n/**\n * Admin configuration\n */\nexport interface PluginAdminConfig {\n\t/** Module specifier for admin UI exports (e.g., \"@emdash-cms/plugin-audit-log/admin\") */\n\tentry?: string;\n\t/** Settings schema for auto-generated UI */\n\tsettingsSchema?: Record<string, SettingField>;\n\t/** Admin pages */\n\tpages?: PluginAdminPage[];\n\t/** Dashboard widgets */\n\twidgets?: PluginDashboardWidget[];\n\t/** Portable Text block types this plugin provides */\n\tportableTextBlocks?: PortableTextBlockConfig[];\n\t/** Field widget types this plugin provides */\n\tfieldWidgets?: FieldWidgetConfig[];\n}\n\n/**\n * Plugin definition - input to definePlugin()\n */\nexport interface PluginDefinition<TStorage extends PluginStorageConfig = PluginStorageConfig> {\n\t/** Unique plugin identifier */\n\tid: string;\n\t/** Plugin version (semver) */\n\tversion: string;\n\n\t/** Declared capabilities */\n\tcapabilities?: PluginCapability[];\n\n\t/** Allowed hosts for network:fetch (wildcards supported: *.example.com) */\n\tallowedHosts?: string[];\n\n\t/** Storage collections with indexes */\n\tstorage?: TStorage;\n\n\t/** Hooks */\n\thooks?: PluginHooks;\n\n\t/** API routes */\n\troutes?: Record<string, PluginRoute>;\n\n\t/** Admin UI configuration */\n\tadmin?: PluginAdminConfig;\n}\n\n/**\n * Resolved plugin - after definePlugin() processing\n */\nexport interface ResolvedPlugin<TStorage extends PluginStorageConfig = PluginStorageConfig> {\n\tid: string;\n\tversion: string;\n\tcapabilities: PluginCapability[];\n\tallowedHosts: string[];\n\tstorage: TStorage;\n\thooks: ResolvedPluginHooks;\n\troutes: Record<string, PluginRoute>;\n\tadmin: PluginAdminConfig;\n}\n\n/**\n * Resolved hooks with normalized config\n */\nexport interface ResolvedPluginHooks {\n\t\"plugin:install\"?: ResolvedHook<LifecycleHandler>;\n\t\"plugin:activate\"?: ResolvedHook<LifecycleHandler>;\n\t\"plugin:deactivate\"?: ResolvedHook<LifecycleHandler>;\n\t\"plugin:uninstall\"?: ResolvedHook<UninstallHandler>;\n\t\"content:beforeSave\"?: ResolvedHook<ContentBeforeSaveHandler>;\n\t\"content:afterSave\"?: ResolvedHook<ContentAfterSaveHandler>;\n\t\"content:beforeDelete\"?: ResolvedHook<ContentBeforeDeleteHandler>;\n\t\"content:afterDelete\"?: ResolvedHook<ContentAfterDeleteHandler>;\n\t\"content:afterPublish\"?: ResolvedHook<ContentAfterPublishHandler>;\n\t\"content:afterUnpublish\"?: ResolvedHook<ContentAfterUnpublishHandler>;\n\t\"media:beforeUpload\"?: ResolvedHook<MediaBeforeUploadHandler>;\n\t\"media:afterUpload\"?: ResolvedHook<MediaAfterUploadHandler>;\n\tcron?: ResolvedHook<CronHandler>;\n\t\"email:beforeSend\"?: ResolvedHook<EmailBeforeSendHandler>;\n\t\"email:deliver\"?: ResolvedHook<EmailDeliverHandler>;\n\t\"email:afterSend\"?: ResolvedHook<EmailAfterSendHandler>;\n\t\"comment:beforeCreate\"?: ResolvedHook<CommentBeforeCreateHandler>;\n\t\"comment:moderate\"?: ResolvedHook<CommentModerateHandler>;\n\t\"comment:afterCreate\"?: ResolvedHook<CommentAfterCreateHandler>;\n\t\"comment:afterModerate\"?: ResolvedHook<CommentAfterModerateHandler>;\n\t\"page:metadata\"?: ResolvedHook<PageMetadataHandler>;\n\t\"page:fragments\"?: ResolvedHook<PageFragmentHandler>;\n}\n\n// =============================================================================\n// Standard Plugin Format (Unified Plugin Format)\n// =============================================================================\n\n/**\n * Standard plugin hook handler -- same as sandbox entry format.\n * Receives the event as the first argument and a PluginContext as the second.\n *\n * Plugin authors annotate their event parameters with specific types for IDE\n * support. At the type level, we accept any function with compatible arity.\n */\n// eslint-disable-next-line typescript-eslint/no-explicit-any -- must accept handlers with specific event types\nexport type StandardHookHandler = (...args: any[]) => Promise<any>;\n\n/**\n * Standard plugin hook entry -- either a bare handler or a config object.\n */\nexport type StandardHookEntry =\n\t| StandardHookHandler\n\t| {\n\t\t\thandler: StandardHookHandler;\n\t\t\tpriority?: number;\n\t\t\ttimeout?: number;\n\t\t\tdependencies?: string[];\n\t\t\terrorPolicy?: \"continue\" | \"abort\";\n\t\t\texclusive?: boolean;\n\t };\n\n/**\n * Standard plugin route handler -- takes (routeCtx, pluginCtx) like sandbox entries.\n * The routeCtx contains input and request info; pluginCtx is the full plugin context.\n *\n * Uses `any` for routeCtx to allow plugins to access properties like\n * `routeCtx.request.url` without needing exact type matches across\n * trusted (Request object) and sandboxed (plain object) modes.\n */\n// eslint-disable-next-line typescript-eslint/no-explicit-any -- see above\nexport type StandardRouteHandler = (routeCtx: any, ctx: PluginContext) => Promise<unknown>;\n\n/**\n * Standard plugin route entry -- either a config object with handler, or just a handler.\n */\nexport interface StandardRouteEntry {\n\thandler: StandardRouteHandler;\n\tinput?: unknown;\n\tpublic?: boolean;\n}\n\n/**\n * Standard plugin definition -- the sandbox entry format.\n * Used by standard plugins that work in both trusted and sandboxed modes.\n * No id/version/capabilities -- those come from the descriptor.\n *\n * This is the input to definePlugin() for standard-format plugins.\n *\n * The hooks and routes use permissive types (Record<string, any>) so that\n * plugin authors can annotate their handlers with specific event types\n * without type errors from strictFunctionTypes contravariance.\n */\nexport interface StandardPluginDefinition {\n\t// eslint-disable-next-line typescript-eslint/no-explicit-any -- must accept handlers with specific event/route types\n\thooks?: Record<string, any>;\n\t// eslint-disable-next-line typescript-eslint/no-explicit-any -- must accept handlers with specific event/route types\n\troutes?: Record<string, any>;\n}\n\n/**\n * Check if a value is a StandardPluginDefinition (has hooks/routes but no id/version).\n */\nexport function isStandardPluginDefinition(value: unknown): value is StandardPluginDefinition {\n\tif (typeof value !== \"object\" || value === null) return false;\n\t// Standard format: has hooks or routes, but NOT id+version (which are on PluginDefinition)\n\tconst hasPluginShape = \"hooks\" in value || \"routes\" in value;\n\tconst hasNativeShape = \"id\" in value && \"version\" in value;\n\treturn hasPluginShape && !hasNativeShape;\n}\n\n// =============================================================================\n// Plugin Admin Exports\n// =============================================================================\n\n/**\n * What a plugin exports from its /admin entrypoint\n * Uses generic component type to avoid React dependency\n */\nexport interface PluginAdminExports {\n\twidgets?: Record<string, JSX.Element>;\n\tpages?: Record<string, JSX.Element>;\n\tfields?: Record<string, JSX.Element>;\n}\n\n// =============================================================================\n// Sandbox Types\n// =============================================================================\n\n/**\n * Plugin manifest — the metadata portion of a plugin bundle, used for\n * sandboxed plugins loaded from the marketplace.\n *\n * This interface is core's stricter version of the manifest contract: it\n * uses the exhaustive `HookName` union and core's typed `PluginAdminConfig`.\n * The wire-shape lives in `@emdash-cms/plugin-types` as `PluginManifest`\n * with looser types (so the registry CLI can serialise hook names it\n * doesn't know about). Both must stay structurally compatible: every value\n * of this type must be assignable to the shared one. The static assertion\n * below catches any drift at compile time.\n */\nexport interface PluginManifest {\n\tid: string;\n\tversion: string;\n\tcapabilities: PluginCapability[];\n\tallowedHosts: string[];\n\tstorage: PluginStorageConfig;\n\t/** Hook declarations — either plain name strings or structured objects */\n\thooks: Array<ManifestHookEntry | HookName>;\n\t/** Route declarations — either plain name strings or structured objects */\n\troutes: Array<ManifestRouteEntry | string>;\n\tadmin: PluginAdminConfig;\n}\n\n// Type-level guard: core's `PluginManifest` is intentionally a SUBTYPE of\n// the shared wire shape (`@emdash-cms/plugin-types` `PluginManifest`). The\n// wire shape uses looser types like `string` for hook names so the registry\n// CLI can serialise plugins targeting hook versions this core doesn't yet\n// know about. Core narrows `string` to `HookName` and `Record<string,\n// unknown>` to `PluginAdminConfig` because core's loader actually executes\n// against those types.\n//\n// We assert one direction at compile time: `core extends shared`. The\n// reverse direction (`shared extends core`) intentionally does NOT hold\n// because shared is wider -- a manifest written against the wire shape\n// could carry a hook name core doesn't know. That runtime narrowing is the\n// job of `manifest-schema.ts` (zod-validated, called at every JSON.parse\n// of a manifest.json), not of the type system. The static check below\n// catches the OTHER failure mode: core adding a required field or\n// non-assignable type that the wire shape doesn't allow.\n//\n// `type X = never` is itself legal as a type alias, so the assertion has to\n// be in a value position (`const _check: T = true`) for the compiler to\n// error when T resolves to `never`. Don't replace this with a bare type\n// alias.\ntype _AssertManifestCompat =\n\tPluginManifest extends import(\"@emdash-cms/plugin-types\").PluginManifest ? true : never;\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nconst _MANIFEST_COMPAT: _AssertManifestCompat = true;\n"],"mappings":";;;;;;AA81CA,SAAgB,2BAA2B,OAAmD;AAC7F,KAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;CAExD,MAAM,iBAAiB,WAAW,SAAS,YAAY;CACvD,MAAM,iBAAiB,QAAQ,SAAS,aAAa;AACrD,QAAO,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"types-Cug_RO3W.mjs","names":[],"sources":["../src/plugins/types.ts"],"sourcesContent":["/**\n * Plugin System Types v2\n *\n * New plugin API with:\n * - Single unified context shape for all hooks and routes\n * - Paginated storage queries (no async iterators)\n * - Unified KV API (replaces settings + options)\n * - Explicit ctx.http and ctx.log\n *\n */\n\nimport type { Element } from \"@emdash-cms/blocks\";\n// The plugin capability vocabulary, the legacy-rename map, and the manifest\n// shape are authored once in @emdash-cms/plugin-types and shared between core\n// (the manifest reader at install/runtime) and @emdash-cms/registry-cli (the\n// manifest writer at bundle/publish time).\n//\n// We import-and-re-export here so existing internal callers keep working\n// (e.g. `import { PluginCapability } from \"../plugins/types.js\"`).\nimport {\n\tCAPABILITY_RENAMES,\n\tisDeprecatedCapability,\n\tnormalizeCapabilities,\n\tnormalizeCapability,\n\ttype CurrentPluginCapability,\n\ttype DeprecatedPluginCapability,\n\ttype ManifestHookEntry,\n\ttype ManifestRouteEntry,\n\ttype PluginCapability,\n\ttype PluginStorageConfig,\n\ttype StorageCollectionConfig,\n} from \"@emdash-cms/plugin-types\";\nimport type { JSX } from \"astro/jsx-runtime\";\nimport type { z } from \"astro/zod\";\n// =============================================================================\n// Core Types\n// =============================================================================\n\nimport type { FieldType } from \"../schema/types.js\";\n\nexport {\n\tCAPABILITY_RENAMES,\n\tisDeprecatedCapability,\n\tnormalizeCapabilities,\n\tnormalizeCapability,\n\ttype CurrentPluginCapability,\n\ttype DeprecatedPluginCapability,\n\ttype ManifestHookEntry,\n\ttype ManifestRouteEntry,\n\ttype PluginCapability,\n\ttype PluginStorageConfig,\n\ttype StorageCollectionConfig,\n};\n\n// =============================================================================\n// Storage Types\n// =============================================================================\n//\n// `StorageCollectionConfig` and `PluginStorageConfig` are re-exported above\n// from `@emdash-cms/plugin-types`. The manifest carries these shapes\n// verbatim; both this package (reader) and registry-cli (writer) agree on\n// the same types via the shared package.\n\n/**\n * Query filter operators\n */\nexport interface RangeFilter {\n\tgt?: number | string;\n\tgte?: number | string;\n\tlt?: number | string;\n\tlte?: number | string;\n}\n\nexport interface InFilter {\n\tin: Array<string | number>;\n}\n\nexport interface StartsWithFilter {\n\tstartsWith: string;\n}\n\n/**\n * Where clause value types\n */\nexport type WhereValue =\n\t| string\n\t| number\n\t| boolean\n\t| null\n\t| RangeFilter\n\t| InFilter\n\t| StartsWithFilter;\n\n/**\n * Where clause for storage queries\n */\nexport type WhereClause = Record<string, WhereValue>;\n\n/**\n * Query options for storage.query()\n */\nexport interface QueryOptions {\n\twhere?: WhereClause;\n\torderBy?: Record<string, \"asc\" | \"desc\">;\n\tlimit?: number; // Default 50, max 1000\n\tcursor?: string;\n}\n\n/**\n * Paginated result (used by storage.query, content.list, media.list)\n */\nexport interface PaginatedResult<T> {\n\titems: T[];\n\tcursor?: string;\n\thasMore: boolean;\n}\n\n/**\n * Storage collection interface - the API exposed to plugins\n * No async iterators - all operations return promises with pagination\n */\nexport interface StorageCollection<T = unknown> {\n\t// Basic CRUD\n\tget(id: string): Promise<T | null>;\n\tput(id: string, data: T): Promise<void>;\n\tdelete(id: string): Promise<boolean>;\n\texists(id: string): Promise<boolean>;\n\n\t// Batch operations\n\tgetMany(ids: string[]): Promise<Map<string, T>>;\n\tputMany(items: Array<{ id: string; data: T }>): Promise<void>;\n\tdeleteMany(ids: string[]): Promise<number>;\n\n\t// Query - always paginated\n\tquery(options?: QueryOptions): Promise<PaginatedResult<{ id: string; data: T }>>;\n\tcount(where?: WhereClause): Promise<number>;\n}\n\n/**\n * Plugin storage context - typed based on declared collections\n */\nexport type PluginStorage<T extends PluginStorageConfig> = {\n\t[K in keyof T]: StorageCollection;\n};\n\n// =============================================================================\n// Context APIs\n// =============================================================================\n\n/**\n * KV store interface - unified replacement for settings + options\n *\n * Convention:\n * - `settings:*` - User-configurable preferences (shown in admin UI)\n * - `state:*` - Internal plugin state (not shown to users)\n */\nexport interface KVAccess {\n\tget<T>(key: string): Promise<T | null>;\n\tset(key: string, value: unknown): Promise<void>;\n\tdelete(key: string): Promise<boolean>;\n\tlist(prefix?: string): Promise<Array<{ key: string; value: unknown }>>;\n}\n\n/**\n * SEO metadata for a content item, as stored in the core SEO panel.\n *\n * Only present on items in collections with `has_seo = 1`. For collections\n * without SEO enabled, `ContentItem.seo` is `undefined`.\n */\nexport interface ContentItemSeo {\n\ttitle: string | null;\n\tdescription: string | null;\n\timage: string | null;\n\tcanonical: string | null;\n\tnoIndex: boolean;\n}\n\n/**\n * SEO input accepted by content write operations.\n *\n * All fields are optional — only fields that are present overwrite existing\n * values. An empty object is treated as a no-op.\n */\nexport interface ContentItemSeoInput {\n\ttitle?: string | null;\n\tdescription?: string | null;\n\timage?: string | null;\n\tcanonical?: string | null;\n\tnoIndex?: boolean;\n}\n\n/**\n * Content item returned from content API\n */\nexport interface ContentItem {\n\tid: string;\n\ttype: string;\n\tslug: string | null;\n\tstatus: string;\n\tlocale: string | null;\n\tdata: Record<string, unknown>;\n\t/**\n\t * SEO metadata, populated when the collection has SEO enabled\n\t * (`has_seo = 1`). `undefined` for non-SEO collections.\n\t */\n\tseo?: ContentItemSeo;\n\tcreatedAt: string;\n\tupdatedAt: string;\n\tpublishedAt: string | null;\n}\n\nexport interface ContentListWhere {\n\t/** Exact match on `status` (e.g. `\"published\"`, `\"draft\"`). */\n\tstatus?: string;\n\t/** Exact match on `locale` (e.g. `\"en\"`, `\"fr-CA\"`). */\n\tlocale?: string;\n}\n\n/**\n * Content list options\n */\nexport interface ContentListOptions {\n\tlimit?: number;\n\tcursor?: string;\n\torderBy?: Record<string, \"asc\" | \"desc\">;\n\twhere?: ContentListWhere;\n}\n\n/**\n * Input accepted by `content.create` / `content.update`.\n *\n * Most entries are field slugs mapped to their values. The reserved `seo`\n * key is extracted and routed to the core SEO panel (the `_emdash_seo`\n * table), matching the shape accepted by the REST API. Passing `seo` for a\n * collection that does not have SEO enabled throws a validation error.\n */\nexport type ContentWriteInput = Record<string, unknown> & {\n\tseo?: ContentItemSeoInput;\n};\n\n/**\n * Content access interface - capability-gated\n */\nexport interface ContentAccess {\n\t// Read operations (requires read:content)\n\tget(collection: string, id: string): Promise<ContentItem | null>;\n\tlist(collection: string, options?: ContentListOptions): Promise<PaginatedResult<ContentItem>>;\n\n\t// Write operations (requires write:content) - optional on interface\n\tcreate?(collection: string, data: ContentWriteInput): Promise<ContentItem>;\n\tupdate?(collection: string, id: string, data: ContentWriteInput): Promise<ContentItem>;\n\tdelete?(collection: string, id: string): Promise<boolean>;\n}\n\n/**\n * Full content access with write operations\n */\nexport interface ContentAccessWithWrite extends ContentAccess {\n\tcreate(collection: string, data: ContentWriteInput): Promise<ContentItem>;\n\tupdate(collection: string, id: string, data: ContentWriteInput): Promise<ContentItem>;\n\tdelete(collection: string, id: string): Promise<boolean>;\n}\n\n/**\n * Media item returned from media API\n */\nexport interface MediaItem {\n\tid: string;\n\tfilename: string;\n\tmimeType: string;\n\tsize: number | null;\n\turl: string;\n\tcreatedAt: string;\n}\n\n/**\n * Media list options\n */\nexport interface MediaListOptions {\n\tlimit?: number;\n\tcursor?: string;\n\tmimeType?: string; // Filter by mime type prefix, e.g., \"image/\"\n}\n\n/**\n * Media access interface - capability-gated\n */\nexport interface MediaAccess {\n\t// Read operations (requires read:media)\n\tget(id: string): Promise<MediaItem | null>;\n\tlist(options?: MediaListOptions): Promise<PaginatedResult<MediaItem>>;\n\n\t// Write operations (requires write:media) - optional on interface\n\tgetUploadUrl?(\n\t\tfilename: string,\n\t\tcontentType: string,\n\t): Promise<{ uploadUrl: string; mediaId: string }>;\n\t/**\n\t * Upload media bytes directly. Preferred in sandboxed mode where\n\t * plugins cannot make external requests to a presigned URL.\n\t * Returns the created media item.\n\t */\n\tupload?(\n\t\tfilename: string,\n\t\tcontentType: string,\n\t\tbytes: ArrayBuffer,\n\t): Promise<{ mediaId: string; storageKey: string; url: string }>;\n\tdelete?(id: string): Promise<boolean>;\n}\n\n/**\n * Full media access with write operations\n */\nexport interface MediaAccessWithWrite extends MediaAccess {\n\tgetUploadUrl(\n\t\tfilename: string,\n\t\tcontentType: string,\n\t): Promise<{ uploadUrl: string; mediaId: string }>;\n\tupload(\n\t\tfilename: string,\n\t\tcontentType: string,\n\t\tbytes: ArrayBuffer,\n\t): Promise<{ mediaId: string; storageKey: string; url: string }>;\n\tdelete(id: string): Promise<boolean>;\n}\n\n/**\n * HTTP client interface - requires network:fetch capability\n */\nexport interface HttpAccess {\n\tfetch(url: string, init?: RequestInit): Promise<Response>;\n}\n\n/**\n * Logger interface - always available\n */\nexport interface LogAccess {\n\tdebug(message: string, data?: unknown): void;\n\tinfo(message: string, data?: unknown): void;\n\twarn(message: string, data?: unknown): void;\n\terror(message: string, data?: unknown): void;\n}\n\n// =============================================================================\n// Site & User Access\n// =============================================================================\n\n/**\n * Site information available to all plugins\n */\nexport interface SiteInfo {\n\t/** Site name (from settings) */\n\tname: string;\n\t/** Site URL (from settings or request) */\n\turl: string;\n\t/** Site locale (from settings, defaults to \"en\") */\n\tlocale: string;\n}\n\n/**\n * Read-only user information exposed to plugins.\n * Sensitive fields (password hashes, sessions, passkeys) are excluded.\n */\nexport interface UserInfo {\n\tid: string;\n\temail: string;\n\tname: string | null;\n\trole: number;\n\tcreatedAt: string;\n}\n\n/**\n * User access interface - requires read:users capability\n */\nexport interface UserAccess {\n\t/** Get a user by ID */\n\tget(id: string): Promise<UserInfo | null>;\n\t/** Get a user by email */\n\tgetByEmail(email: string): Promise<UserInfo | null>;\n\t/** List users with optional filters */\n\tlist(opts?: { role?: number; limit?: number; cursor?: string }): Promise<{\n\t\titems: UserInfo[];\n\t\tnextCursor?: string;\n\t}>;\n}\n\n// =============================================================================\n// Plugin Context\n// =============================================================================\n\n/**\n * The unified plugin context - same shape for all hooks and routes\n */\nexport interface PluginContext<TStorage extends PluginStorageConfig = PluginStorageConfig> {\n\t/** Plugin metadata */\n\tplugin: {\n\t\tid: string;\n\t\tversion: string;\n\t};\n\n\t/** Storage collections - only if plugin declares storage */\n\tstorage: PluginStorage<TStorage>;\n\n\t/** Key-value store for config and state */\n\tkv: KVAccess;\n\n\t/** Content access - only if read:content or write:content capability */\n\tcontent?: ContentAccess | ContentAccessWithWrite;\n\n\t/** Media access - only if read:media or write:media capability */\n\tmedia?: MediaAccess | MediaAccessWithWrite;\n\n\t/** HTTP client - only if network:fetch capability */\n\thttp?: HttpAccess;\n\n\t/** Logger - always available */\n\tlog: LogAccess;\n\n\t/** Site information - always available */\n\tsite: SiteInfo;\n\n\t/** URL helper - generates absolute URLs from paths. Always available. */\n\turl(path: string): string;\n\n\t/** User access - only if read:users capability */\n\tusers?: UserAccess;\n\n\t/** Cron task scheduling - always available, scoped to plugin */\n\tcron?: CronAccess;\n\n\t/** Email access - only if email:send capability and a provider is configured */\n\temail?: EmailAccess;\n}\n\n// =============================================================================\n// Cron Types\n// =============================================================================\n\n/**\n * Cron access interface �� always available on plugin context, scoped to plugin.\n */\nexport interface CronAccess {\n\t/** Schedule a recurring or one-shot task */\n\tschedule(name: string, opts: { schedule: string; data?: Record<string, unknown> }): Promise<void>;\n\t/** Cancel a scheduled task */\n\tcancel(name: string): Promise<void>;\n\t/** List this plugin's scheduled tasks */\n\tlist(): Promise<CronTaskInfo[]>;\n}\n\n/**\n * Task info returned from CronAccess.list()\n */\nexport interface CronTaskInfo {\n\tname: string;\n\tschedule: string;\n\tnextRunAt: string;\n\tlastRunAt: string | null;\n}\n\n/**\n * Event passed to the `cron` hook handler\n */\nexport interface CronEvent {\n\tname: string;\n\tdata?: Record<string, unknown>;\n\tscheduledAt: string;\n}\n\n/**\n * Cron hook handler type\n */\nexport type CronHandler = (event: CronEvent, ctx: PluginContext) => Promise<void>;\n\n// =============================================================================\n// Email Types\n// =============================================================================\n\n/**\n * Email access interface — requires `email:send` capability.\n * Undefined when no `email:deliver` provider is configured.\n *\n * Related capabilities:\n * - `email:send` — grants ctx.email (this interface)\n * - `email:provide` — allows registering the `email:deliver` exclusive hook\n * - `email:intercept` — allows registering `email:beforeSend` / `email:afterSend` hooks\n */\nexport interface EmailAccess {\n\tsend(message: EmailMessage): Promise<void>;\n}\n\n/**\n * Email message shape\n */\nexport interface EmailMessage {\n\tto: string;\n\tsubject: string;\n\ttext: string;\n\thtml?: string;\n}\n\n/**\n * Event passed to email:beforeSend hooks (middleware — transform, validate, cancel)\n */\nexport interface EmailBeforeSendEvent {\n\tmessage: EmailMessage;\n\t/** Where the email originated — \"system\" for auth emails, plugin ID for plugin emails */\n\tsource: string;\n}\n\n/**\n * Event passed to email:deliver hook (exclusive — exactly one provider delivers)\n */\nexport interface EmailDeliverEvent {\n\tmessage: EmailMessage;\n\tsource: string;\n}\n\n/**\n * Event passed to email:afterSend hooks (logging, analytics, fire-and-forget)\n */\nexport interface EmailAfterSendEvent {\n\tmessage: EmailMessage;\n\tsource: string;\n}\n\n/**\n * Handler type for email:beforeSend hooks.\n * Returns modified message, or false to cancel delivery.\n */\nexport type EmailBeforeSendHandler = (\n\tevent: EmailBeforeSendEvent,\n\tctx: PluginContext,\n) => Promise<EmailMessage | false>;\n\n/**\n * Handler type for email:deliver hooks (exclusive provider).\n */\nexport type EmailDeliverHandler = (event: EmailDeliverEvent, ctx: PluginContext) => Promise<void>;\n\n/**\n * Handler type for email:afterSend hooks (fire-and-forget).\n */\nexport type EmailAfterSendHandler = (\n\tevent: EmailAfterSendEvent,\n\tctx: PluginContext,\n) => Promise<void>;\n\n// =============================================================================\n// Comment Types\n// =============================================================================\n\n/**\n * Collection comment settings (read from _emdash_collections)\n */\nexport interface CollectionCommentSettings {\n\tcommentsEnabled: boolean;\n\tcommentsModeration: \"all\" | \"first_time\" | \"none\";\n\tcommentsClosedAfterDays: number;\n\tcommentsAutoApproveUsers: boolean;\n}\n\n/**\n * Event passed to comment:beforeCreate hooks (middleware — transform, enrich, reject)\n */\nexport interface CommentBeforeCreateEvent {\n\tcomment: {\n\t\tcollection: string;\n\t\tcontentId: string;\n\t\tparentId: string | null;\n\t\tauthorName: string;\n\t\tauthorEmail: string;\n\t\tauthorUserId: string | null;\n\t\tbody: string;\n\t\tipHash: string | null;\n\t\tuserAgent: string | null;\n\t};\n\t/** Metadata bag — plugins can attach signals for the moderator */\n\tmetadata: Record<string, unknown>;\n}\n\n/**\n * Event passed to comment:moderate hook (exclusive — decides initial status)\n */\nexport interface CommentModerateEvent {\n\tcomment: CommentBeforeCreateEvent[\"comment\"];\n\tmetadata: Record<string, unknown>;\n\tcollectionSettings: CollectionCommentSettings;\n\t/** Number of prior approved comments from this email address */\n\tpriorApprovedCount: number;\n}\n\n/**\n * Moderation decision returned by the comment:moderate handler\n */\nexport interface ModerationDecision {\n\tstatus: \"approved\" | \"pending\" | \"spam\";\n\t/** Optional reason for admin visibility */\n\treason?: string;\n}\n\n/**\n * Stored comment shape (full record with id, status, timestamps)\n */\nexport interface StoredComment {\n\tid: string;\n\tcollection: string;\n\tcontentId: string;\n\tparentId: string | null;\n\tauthorName: string;\n\tauthorEmail: string;\n\tauthorUserId: string | null;\n\tbody: string;\n\tstatus: string;\n\tmoderationMetadata: Record<string, unknown> | null;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n/**\n * Event passed to comment:afterCreate hooks (fire-and-forget)\n */\nexport interface CommentAfterCreateEvent {\n\tcomment: StoredComment;\n\tmetadata: Record<string, unknown>;\n\t/** The content item the comment is on */\n\tcontent: { id: string; collection: string; slug: string; title?: string };\n\t/** The content author (for notifications) */\n\tcontentAuthor?: { id: string; name: string | null; email: string };\n}\n\n/**\n * Event passed to comment:afterModerate hooks (fire-and-forget, admin status change)\n */\nexport interface CommentAfterModerateEvent {\n\tcomment: StoredComment;\n\tpreviousStatus: string;\n\tnewStatus: string;\n\t/** The admin who moderated */\n\tmoderator: { id: string; name: string | null };\n}\n\n/**\n * Handler type for comment:beforeCreate hooks.\n * Returns modified event, or false to reject the comment.\n */\nexport type CommentBeforeCreateHandler = (\n\tevent: CommentBeforeCreateEvent,\n\tctx: PluginContext,\n) => Promise<CommentBeforeCreateEvent | false | void>;\n\n/**\n * Handler type for comment:moderate hook (exclusive provider).\n */\nexport type CommentModerateHandler = (\n\tevent: CommentModerateEvent,\n\tctx: PluginContext,\n) => Promise<ModerationDecision>;\n\n/**\n * Handler type for comment:afterCreate hooks (fire-and-forget).\n */\nexport type CommentAfterCreateHandler = (\n\tevent: CommentAfterCreateEvent,\n\tctx: PluginContext,\n) => Promise<void>;\n\n/**\n * Handler type for comment:afterModerate hooks (fire-and-forget).\n */\nexport type CommentAfterModerateHandler = (\n\tevent: CommentAfterModerateEvent,\n\tctx: PluginContext,\n) => Promise<void>;\n\n// =============================================================================\n// Hook Types\n// =============================================================================\n\n/**\n * Hook configuration\n */\nexport interface HookConfig<THandler> {\n\t/** Explicit ordering - lower numbers run first (default: 100) */\n\tpriority?: number;\n\t/** Max execution time in ms (default: 5000) */\n\ttimeout?: number;\n\t/** Run after these plugins */\n\tdependencies?: string[];\n\t/** Error handling policy */\n\terrorPolicy?: \"continue\" | \"abort\";\n\t/**\n\t * Mark this hook as exclusive — only one plugin can be the active provider.\n\t * Exclusive hooks skip the priority pipeline and dispatch only to the\n\t * admin-selected provider. Used for email:deliver, search, image optimization, etc.\n\t */\n\texclusive?: boolean;\n\t/** The hook handler */\n\thandler: THandler;\n}\n\n/**\n * Content hook event\n */\nexport interface ContentHookEvent {\n\tcontent: Record<string, unknown>;\n\tcollection: string;\n\tisNew: boolean;\n}\n\n/**\n * Content delete hook event\n */\nexport interface ContentDeleteEvent {\n\tid: string;\n\tcollection: string;\n\t/** `true` when the content is permanently deleted (not just trashed). */\n\tpermanent: boolean;\n}\n\n/**\n * Content publish state change hook event (fired after publish or unpublish)\n */\nexport interface ContentPublishStateChangeEvent {\n\tcontent: Record<string, unknown>;\n\tcollection: string;\n}\n\n/**\n * Media hook event\n */\nexport interface MediaUploadEvent {\n\tfile: { name: string; type: string; size: number };\n}\n\n/**\n * Media after upload event\n */\nexport interface MediaAfterUploadEvent {\n\tmedia: MediaItem;\n}\n\n/**\n * Lifecycle hook event\n */\nexport interface LifecycleEvent {\n\t// Empty for install/activate/deactivate\n}\n\n/**\n * Uninstall hook event\n */\nexport interface UninstallEvent {\n\tdeleteData: boolean;\n}\n\n// Hook handler types - all receive (event, ctx) with unified context\nexport type ContentBeforeSaveHandler = (\n\tevent: ContentHookEvent,\n\tctx: PluginContext,\n) => Promise<Record<string, unknown> | void>;\n\nexport type ContentAfterSaveHandler = (\n\tevent: ContentHookEvent,\n\tctx: PluginContext,\n) => Promise<void>;\n\nexport type ContentBeforeDeleteHandler = (\n\tevent: ContentDeleteEvent,\n\tctx: PluginContext,\n) => Promise<boolean | void>;\n\nexport type ContentAfterDeleteHandler = (\n\tevent: ContentDeleteEvent,\n\tctx: PluginContext,\n) => Promise<void>;\n\nexport type ContentAfterPublishHandler = (\n\tevent: ContentPublishStateChangeEvent,\n\tctx: PluginContext,\n) => Promise<void>;\n\nexport type ContentAfterUnpublishHandler = (\n\tevent: ContentPublishStateChangeEvent,\n\tctx: PluginContext,\n) => Promise<void>;\n\nexport type MediaBeforeUploadHandler = (\n\tevent: MediaUploadEvent,\n\tctx: PluginContext,\n) => Promise<{ name: string; type: string; size: number } | void>;\n\nexport type MediaAfterUploadHandler = (\n\tevent: MediaAfterUploadEvent,\n\tctx: PluginContext,\n) => Promise<void>;\n\nexport type LifecycleHandler = (event: LifecycleEvent, ctx: PluginContext) => Promise<void>;\n\nexport type UninstallHandler = (event: UninstallEvent, ctx: PluginContext) => Promise<void>;\n\n// =============================================================================\n// Public Page Contribution Types\n// =============================================================================\n\n/** Placement targets for page fragment contributions */\nexport type PagePlacement = \"head\" | \"body:start\" | \"body:end\";\n\n/**\n * A single breadcrumb trail item. Used by `PublicPageContext.breadcrumbs`\n * so themes can publish breadcrumb trails that SEO plugins consume.\n */\nexport interface BreadcrumbItem {\n\t/** Display name for this crumb (e.g. \"Home\", \"Blog\", \"My Post\"). */\n\tname: string;\n\t/** Absolute or root-relative URL for this crumb. */\n\turl: string;\n}\n\n/**\n * Describes the page being rendered. Passed to page hooks so plugins\n * can decide what to contribute without fetching content themselves.\n */\nexport interface PublicPageContext {\n\turl: string;\n\tpath: string;\n\tlocale: string | null;\n\tkind: \"content\" | \"custom\";\n\tpageType: string;\n\t/** Full document title for the rendered page */\n\ttitle: string | null;\n\t/** Page-only title for OG/Twitter/JSON-LD headline output */\n\tpageTitle?: string | null;\n\tdescription: string | null;\n\tcanonical: string | null;\n\timage: string | null;\n\tcontent?: {\n\t\tcollection: string;\n\t\tid: string;\n\t\tslug: string | null;\n\t};\n\t/** SEO meta for base metadata generation in EmDashHead */\n\tseo?: {\n\t\togTitle?: string | null;\n\t\togDescription?: string | null;\n\t\togImage?: string | null;\n\t\trobots?: string | null;\n\t};\n\t/** Article metadata for Open Graph article: tags */\n\tarticleMeta?: {\n\t\tpublishedTime?: string | null;\n\t\tmodifiedTime?: string | null;\n\t\tauthor?: string | null;\n\t};\n\t/** Site name for structured data and og:site_name */\n\tsiteName?: string;\n\t/**\n\t * Optional breadcrumb trail for this page, root first. When set,\n\t * SEO plugins should use this verbatim rather than deriving a trail\n\t * from `path`. Themes typically populate this at the point they\n\t * build the context (e.g. from a content hierarchy walk, taxonomy\n\t * lookup, or per-`pageType` routing logic).\n\t *\n\t * Semantics for consumers:\n\t * - `undefined` — theme has no opinion; consumer falls back to\n\t * its own derivation.\n\t * - `[]` — this page has no breadcrumbs (e.g. homepage); consumer\n\t * should skip `BreadcrumbList` emission entirely.\n\t * - Non-empty array — used verbatim for `BreadcrumbList` output.\n\t */\n\tbreadcrumbs?: BreadcrumbItem[];\n\t/** Public-facing site URL (origin) for structured data */\n\tsiteUrl?: string;\n}\n\n// ── page:metadata ───────────────────────────────────────────────\n\nexport interface PageMetadataEvent {\n\tpage: PublicPageContext;\n}\n\n/**\n * Allowed rel values for link contributions.\n * This is a security-critical allowlist -- sandboxed plugins can only inject\n * link tags with these rel values. Adding \"stylesheet\", \"prefetch\", \"prerender\"\n * etc. would allow sandboxed plugins to inject external resources.\n */\nexport type PageMetadataLinkRel =\n\t| \"canonical\"\n\t| \"alternate\"\n\t| \"author\"\n\t| \"license\"\n\t| \"nlweb\"\n\t| \"site.standard.document\";\n\nexport type PageMetadataContribution =\n\t| { kind: \"meta\"; name: string; content: string; key?: string }\n\t| { kind: \"property\"; property: string; content: string; key?: string }\n\t| { kind: \"link\"; rel: PageMetadataLinkRel; href: string; hreflang?: string; key?: string }\n\t| {\n\t\t\tkind: \"jsonld\";\n\t\t\tid?: string;\n\t\t\tgraph: Record<string, unknown> | Array<Record<string, unknown>>;\n\t };\n\nexport type PageMetadataHandler = (\n\tevent: PageMetadataEvent,\n\tctx: PluginContext,\n) =>\n\t| PageMetadataContribution\n\t| PageMetadataContribution[]\n\t| null\n\t| Promise<PageMetadataContribution | PageMetadataContribution[] | null>;\n\n// ── page:fragments (trusted-only) ──────────────────────────────\n\nexport interface PageFragmentEvent {\n\tpage: PublicPageContext;\n}\n\nexport type PageFragmentContribution =\n\t| {\n\t\t\tkind: \"external-script\";\n\t\t\tplacement: PagePlacement;\n\t\t\tsrc: string;\n\t\t\tasync?: boolean;\n\t\t\tdefer?: boolean;\n\t\t\tattributes?: Record<string, string>;\n\t\t\tkey?: string;\n\t }\n\t| {\n\t\t\tkind: \"inline-script\";\n\t\t\tplacement: PagePlacement;\n\t\t\tcode: string;\n\t\t\tattributes?: Record<string, string>;\n\t\t\tkey?: string;\n\t }\n\t| {\n\t\t\tkind: \"html\";\n\t\t\tplacement: PagePlacement;\n\t\t\thtml: string;\n\t\t\tkey?: string;\n\t };\n\nexport type PageFragmentHandler = (\n\tevent: PageFragmentEvent,\n\tctx: PluginContext,\n) =>\n\t| PageFragmentContribution\n\t| PageFragmentContribution[]\n\t| null\n\t| Promise<PageFragmentContribution | PageFragmentContribution[] | null>;\n\n/**\n * Plugin hooks definition\n */\nexport interface PluginHooks {\n\t// Lifecycle hooks\n\t\"plugin:install\"?: HookConfig<LifecycleHandler> | LifecycleHandler;\n\t\"plugin:activate\"?: HookConfig<LifecycleHandler> | LifecycleHandler;\n\t\"plugin:deactivate\"?: HookConfig<LifecycleHandler> | LifecycleHandler;\n\t\"plugin:uninstall\"?: HookConfig<UninstallHandler> | UninstallHandler;\n\n\t// Content hooks\n\t\"content:beforeSave\"?: HookConfig<ContentBeforeSaveHandler> | ContentBeforeSaveHandler;\n\t\"content:afterSave\"?: HookConfig<ContentAfterSaveHandler> | ContentAfterSaveHandler;\n\t\"content:beforeDelete\"?: HookConfig<ContentBeforeDeleteHandler> | ContentBeforeDeleteHandler;\n\t\"content:afterDelete\"?: HookConfig<ContentAfterDeleteHandler> | ContentAfterDeleteHandler;\n\t\"content:afterPublish\"?: HookConfig<ContentAfterPublishHandler> | ContentAfterPublishHandler;\n\t\"content:afterUnpublish\"?:\n\t\t| HookConfig<ContentAfterUnpublishHandler>\n\t\t| ContentAfterUnpublishHandler;\n\n\t// Media hooks\n\t\"media:beforeUpload\"?: HookConfig<MediaBeforeUploadHandler> | MediaBeforeUploadHandler;\n\t\"media:afterUpload\"?: HookConfig<MediaAfterUploadHandler> | MediaAfterUploadHandler;\n\n\t// Cron hook\n\tcron?: HookConfig<CronHandler> | CronHandler;\n\n\t// Email hooks\n\t\"email:beforeSend\"?: HookConfig<EmailBeforeSendHandler> | EmailBeforeSendHandler;\n\t\"email:deliver\"?: HookConfig<EmailDeliverHandler> | EmailDeliverHandler;\n\t\"email:afterSend\"?: HookConfig<EmailAfterSendHandler> | EmailAfterSendHandler;\n\n\t// Comment hooks\n\t\"comment:beforeCreate\"?: HookConfig<CommentBeforeCreateHandler> | CommentBeforeCreateHandler;\n\t\"comment:moderate\"?: HookConfig<CommentModerateHandler> | CommentModerateHandler;\n\t\"comment:afterCreate\"?: HookConfig<CommentAfterCreateHandler> | CommentAfterCreateHandler;\n\t\"comment:afterModerate\"?: HookConfig<CommentAfterModerateHandler> | CommentAfterModerateHandler;\n\n\t// Public page hooks\n\t\"page:metadata\"?: HookConfig<PageMetadataHandler> | PageMetadataHandler;\n\t\"page:fragments\"?: HookConfig<PageFragmentHandler> | PageFragmentHandler;\n}\n\n/**\n * Hook names\n */\n/**\n * Hook name in a manifest. Core's exhaustive union of recognised hook names,\n * derived from the `PluginHooks` registry. The serialised manifest carries\n * these as opaque strings; this stricter type is only used for type-checking\n * inside core. `ManifestHookEntry` is re-exported from\n * `@emdash-cms/plugin-types` near the top of this file.\n */\nexport type HookName = keyof PluginHooks;\n\n/**\n * Resolved hook with normalized config\n */\nexport interface ResolvedHook<THandler> {\n\tpriority: number;\n\ttimeout: number;\n\tdependencies: string[];\n\terrorPolicy: \"continue\" | \"abort\";\n\t/** Whether this hook is exclusive (provider pattern) */\n\texclusive: boolean;\n\thandler: THandler;\n\tpluginId: string;\n}\n\n// =============================================================================\n// Request Metadata Types\n// =============================================================================\n\n/**\n * Geographic location information derived from the request.\n * Available when running on Cloudflare Workers (via the `cf` object).\n */\nexport interface GeoInfo {\n\tcountry: string | null;\n\tregion: string | null;\n\tcity: string | null;\n}\n\n/**\n * Normalized request metadata available to plugin route handlers.\n * Extracted from request headers and platform-specific properties.\n */\nexport interface RequestMeta {\n\tip: string | null;\n\tuserAgent: string | null;\n\treferer: string | null;\n\tgeo: GeoInfo | null;\n}\n\n// =============================================================================\n// Route Types\n// =============================================================================\n\n/**\n * Route handler context extends plugin context with request-specific data\n */\nexport interface RouteContext<TInput = unknown> extends PluginContext {\n\t/** Validated input from request body */\n\tinput: TInput;\n\t/** Original request */\n\trequest: Request;\n\t/** Normalized request metadata (IP, user agent, geo) */\n\trequestMeta: RequestMeta;\n}\n\n/**\n * Route definition\n */\nexport interface PluginRoute<TInput = unknown> {\n\t/** Zod schema for input validation */\n\tinput?: z.ZodType<TInput>;\n\t/**\n\t * Mark this route as publicly accessible (no authentication required).\n\t * Public routes skip session/token auth and CSRF checks.\n\t */\n\tpublic?: boolean;\n\t/** Route handler */\n\thandler: (ctx: RouteContext<TInput>) => Promise<unknown>;\n}\n\n// =============================================================================\n// Plugin Definition\n// =============================================================================\n\n/**\n * Admin page definition\n */\nexport interface PluginAdminPage {\n\tpath: string;\n\tlabel: string;\n\ticon?: string;\n}\n\n/**\n * Dashboard widget definition\n */\nexport interface PluginDashboardWidget {\n\tid: string;\n\tsize?: \"full\" | \"half\" | \"third\";\n\ttitle?: string;\n}\n\n/**\n * Settings field types (for admin UI generation)\n */\nexport type SettingFieldType =\n\t| \"string\"\n\t| \"number\"\n\t| \"boolean\"\n\t| \"select\"\n\t| \"secret\"\n\t| \"url\"\n\t| \"email\";\n\nexport interface BaseSettingField {\n\ttype: SettingFieldType;\n\tlabel: string;\n\tdescription?: string;\n}\n\nexport interface StringSettingField extends BaseSettingField {\n\ttype: \"string\";\n\tdefault?: string;\n\tmultiline?: boolean;\n}\n\nexport interface NumberSettingField extends BaseSettingField {\n\ttype: \"number\";\n\tdefault?: number;\n\tmin?: number;\n\tmax?: number;\n}\n\nexport interface BooleanSettingField extends BaseSettingField {\n\ttype: \"boolean\";\n\tdefault?: boolean;\n}\n\nexport interface SelectSettingField extends BaseSettingField {\n\ttype: \"select\";\n\toptions: Array<{ value: string; label: string }>;\n\tdefault?: string;\n}\n\nexport interface SecretSettingField extends BaseSettingField {\n\ttype: \"secret\";\n}\n\nexport interface UrlSettingField extends BaseSettingField {\n\ttype: \"url\";\n\tdefault?: string;\n\tplaceholder?: string;\n}\n\nexport interface EmailSettingField extends BaseSettingField {\n\ttype: \"email\";\n\tdefault?: string;\n\tplaceholder?: string;\n}\n\nexport type SettingField =\n\t| StringSettingField\n\t| NumberSettingField\n\t| BooleanSettingField\n\t| SelectSettingField\n\t| SecretSettingField\n\t| UrlSettingField\n\t| EmailSettingField;\n\n/**\n * Block Kit element for block editing fields.\n * This is the `Element` discriminated union from `@emdash-cms/blocks`.\n * Plugin authors should use `@emdash-cms/blocks` builder functions to create these.\n */\nexport type PortableTextBlockField = Element;\n\n/**\n * Configuration for a Portable Text block type contributed by a plugin\n */\nexport interface PortableTextBlockConfig {\n\t/** Block type name (must match the `_type` in Portable Text) */\n\ttype: string;\n\t/** Human-readable label shown in slash commands and modals */\n\tlabel: string;\n\t/** Icon key (e.g., \"video\", \"code\", \"link\", \"link-external\") */\n\ticon?: string;\n\t/** Description shown in slash command menu */\n\tdescription?: string;\n\t/** Placeholder text for the URL input */\n\tplaceholder?: string;\n\t/** Block Kit form fields for the editing UI. If declared, replaces the simple URL input. */\n\tfields?: PortableTextBlockField[];\n\t/**\n\t * Optional. Display category in the slash menu. Defaults to \"Embeds\".\n\t *\n\t * Plugin authors should pick a meaningful category that reflects what the\n\t * block actually is — e.g. \"Sections\", \"Marketing\", \"Media\", \"Embeds\",\n\t * \"Layout\". Blocks with the same category are grouped together in the\n\t * editor's slash menu.\n\t */\n\tcategory?: string;\n}\n\n/**\n * Configuration for a field widget type contributed by a plugin.\n * A field widget provides a custom editing UI for a schema field.\n * The field references the widget via `widget: \"pluginId:widgetName\"`.\n */\nexport interface FieldWidgetConfig {\n\t/** Widget name (without plugin ID prefix) */\n\tname: string;\n\t/** Human-readable label for the admin UI */\n\tlabel: string;\n\t/** Which field types this widget can edit (e.g., [\"json\", \"string\"]) */\n\tfieldTypes: FieldType[];\n\t/** Block Kit elements for sandboxed rendering. Omit for trusted plugins using React. */\n\telements?: Element[];\n}\n\n/**\n * Admin configuration\n */\nexport interface PluginAdminConfig {\n\t/** Module specifier for admin UI exports (e.g., \"@emdash-cms/plugin-audit-log/admin\") */\n\tentry?: string;\n\t/** Settings schema for auto-generated UI */\n\tsettingsSchema?: Record<string, SettingField>;\n\t/** Admin pages */\n\tpages?: PluginAdminPage[];\n\t/** Dashboard widgets */\n\twidgets?: PluginDashboardWidget[];\n\t/** Portable Text block types this plugin provides */\n\tportableTextBlocks?: PortableTextBlockConfig[];\n\t/** Field widget types this plugin provides */\n\tfieldWidgets?: FieldWidgetConfig[];\n}\n\n/**\n * Plugin definition - input to definePlugin()\n */\nexport interface PluginDefinition<TStorage extends PluginStorageConfig = PluginStorageConfig> {\n\t/** Unique plugin identifier */\n\tid: string;\n\t/** Plugin version (semver) */\n\tversion: string;\n\n\t/** Declared capabilities */\n\tcapabilities?: PluginCapability[];\n\n\t/** Allowed hosts for network:fetch (wildcards supported: *.example.com) */\n\tallowedHosts?: string[];\n\n\t/** Storage collections with indexes */\n\tstorage?: TStorage;\n\n\t/** Hooks */\n\thooks?: PluginHooks;\n\n\t/** API routes */\n\troutes?: Record<string, PluginRoute>;\n\n\t/** Admin UI configuration */\n\tadmin?: PluginAdminConfig;\n}\n\n/**\n * Resolved plugin - after definePlugin() processing\n */\nexport interface ResolvedPlugin<TStorage extends PluginStorageConfig = PluginStorageConfig> {\n\tid: string;\n\tversion: string;\n\tcapabilities: PluginCapability[];\n\tallowedHosts: string[];\n\tstorage: TStorage;\n\thooks: ResolvedPluginHooks;\n\troutes: Record<string, PluginRoute>;\n\tadmin: PluginAdminConfig;\n}\n\n/**\n * Resolved hooks with normalized config\n */\nexport interface ResolvedPluginHooks {\n\t\"plugin:install\"?: ResolvedHook<LifecycleHandler>;\n\t\"plugin:activate\"?: ResolvedHook<LifecycleHandler>;\n\t\"plugin:deactivate\"?: ResolvedHook<LifecycleHandler>;\n\t\"plugin:uninstall\"?: ResolvedHook<UninstallHandler>;\n\t\"content:beforeSave\"?: ResolvedHook<ContentBeforeSaveHandler>;\n\t\"content:afterSave\"?: ResolvedHook<ContentAfterSaveHandler>;\n\t\"content:beforeDelete\"?: ResolvedHook<ContentBeforeDeleteHandler>;\n\t\"content:afterDelete\"?: ResolvedHook<ContentAfterDeleteHandler>;\n\t\"content:afterPublish\"?: ResolvedHook<ContentAfterPublishHandler>;\n\t\"content:afterUnpublish\"?: ResolvedHook<ContentAfterUnpublishHandler>;\n\t\"media:beforeUpload\"?: ResolvedHook<MediaBeforeUploadHandler>;\n\t\"media:afterUpload\"?: ResolvedHook<MediaAfterUploadHandler>;\n\tcron?: ResolvedHook<CronHandler>;\n\t\"email:beforeSend\"?: ResolvedHook<EmailBeforeSendHandler>;\n\t\"email:deliver\"?: ResolvedHook<EmailDeliverHandler>;\n\t\"email:afterSend\"?: ResolvedHook<EmailAfterSendHandler>;\n\t\"comment:beforeCreate\"?: ResolvedHook<CommentBeforeCreateHandler>;\n\t\"comment:moderate\"?: ResolvedHook<CommentModerateHandler>;\n\t\"comment:afterCreate\"?: ResolvedHook<CommentAfterCreateHandler>;\n\t\"comment:afterModerate\"?: ResolvedHook<CommentAfterModerateHandler>;\n\t\"page:metadata\"?: ResolvedHook<PageMetadataHandler>;\n\t\"page:fragments\"?: ResolvedHook<PageFragmentHandler>;\n}\n\n// =============================================================================\n// Standard Plugin Format (Unified Plugin Format)\n// =============================================================================\n\n/**\n * Standard plugin hook handler -- same as sandbox entry format.\n * Receives the event as the first argument and a PluginContext as the second.\n *\n * Plugin authors annotate their event parameters with specific types for IDE\n * support. At the type level, we accept any function with compatible arity.\n */\n// eslint-disable-next-line typescript-eslint/no-explicit-any -- must accept handlers with specific event types\nexport type StandardHookHandler = (...args: any[]) => Promise<any>;\n\n/**\n * Standard plugin hook entry -- either a bare handler or a config object.\n */\nexport type StandardHookEntry =\n\t| StandardHookHandler\n\t| {\n\t\t\thandler: StandardHookHandler;\n\t\t\tpriority?: number;\n\t\t\ttimeout?: number;\n\t\t\tdependencies?: string[];\n\t\t\terrorPolicy?: \"continue\" | \"abort\";\n\t\t\texclusive?: boolean;\n\t };\n\n/**\n * Standard plugin route handler -- takes (routeCtx, pluginCtx) like sandbox entries.\n * The routeCtx contains input and request info; pluginCtx is the full plugin context.\n *\n * Uses `any` for routeCtx to allow plugins to access properties like\n * `routeCtx.request.url` without needing exact type matches across\n * trusted (Request object) and sandboxed (plain object) modes.\n */\n// eslint-disable-next-line typescript-eslint/no-explicit-any -- see above\nexport type StandardRouteHandler = (routeCtx: any, ctx: PluginContext) => Promise<unknown>;\n\n/**\n * Standard plugin route entry -- either a config object with handler, or just a handler.\n */\nexport interface StandardRouteEntry {\n\thandler: StandardRouteHandler;\n\tinput?: unknown;\n\tpublic?: boolean;\n}\n\n/**\n * Standard plugin definition -- the sandbox entry format.\n * Used by standard plugins that work in both trusted and sandboxed modes.\n * No id/version/capabilities -- those come from the descriptor.\n *\n * This is the input to definePlugin() for standard-format plugins.\n *\n * The hooks and routes use permissive types (Record<string, any>) so that\n * plugin authors can annotate their handlers with specific event types\n * without type errors from strictFunctionTypes contravariance.\n */\nexport interface StandardPluginDefinition {\n\t// eslint-disable-next-line typescript-eslint/no-explicit-any -- must accept handlers with specific event/route types\n\thooks?: Record<string, any>;\n\t// eslint-disable-next-line typescript-eslint/no-explicit-any -- must accept handlers with specific event/route types\n\troutes?: Record<string, any>;\n}\n\n/**\n * Check if a value is a StandardPluginDefinition (has hooks/routes but no id/version).\n */\nexport function isStandardPluginDefinition(value: unknown): value is StandardPluginDefinition {\n\tif (typeof value !== \"object\" || value === null) return false;\n\t// Standard format: has hooks or routes, but NOT id+version (which are on PluginDefinition)\n\tconst hasPluginShape = \"hooks\" in value || \"routes\" in value;\n\tconst hasNativeShape = \"id\" in value && \"version\" in value;\n\treturn hasPluginShape && !hasNativeShape;\n}\n\n// =============================================================================\n// Plugin Admin Exports\n// =============================================================================\n\n/**\n * What a plugin exports from its /admin entrypoint\n * Uses generic component type to avoid React dependency\n */\nexport interface PluginAdminExports {\n\twidgets?: Record<string, JSX.Element>;\n\tpages?: Record<string, JSX.Element>;\n\tfields?: Record<string, JSX.Element>;\n}\n\n// =============================================================================\n// Sandbox Types\n// =============================================================================\n\n/**\n * Plugin manifest — the metadata portion of a plugin bundle, used for\n * sandboxed plugins loaded from the marketplace.\n *\n * This interface is core's stricter version of the manifest contract: it\n * uses the exhaustive `HookName` union and core's typed `PluginAdminConfig`.\n * The wire-shape lives in `@emdash-cms/plugin-types` as `PluginManifest`\n * with looser types (so the registry CLI can serialise hook names it\n * doesn't know about). Both must stay structurally compatible: every value\n * of this type must be assignable to the shared one. The static assertion\n * below catches any drift at compile time.\n */\nexport interface PluginManifest {\n\tid: string;\n\tversion: string;\n\tcapabilities: PluginCapability[];\n\tallowedHosts: string[];\n\tstorage: PluginStorageConfig;\n\t/** Hook declarations — either plain name strings or structured objects */\n\thooks: Array<ManifestHookEntry | HookName>;\n\t/** Route declarations — either plain name strings or structured objects */\n\troutes: Array<ManifestRouteEntry | string>;\n\tadmin: PluginAdminConfig;\n}\n\n// Type-level guard: core's `PluginManifest` is intentionally a SUBTYPE of\n// the shared wire shape (`@emdash-cms/plugin-types` `PluginManifest`). The\n// wire shape uses looser types like `string` for hook names so the registry\n// CLI can serialise plugins targeting hook versions this core doesn't yet\n// know about. Core narrows `string` to `HookName` and `Record<string,\n// unknown>` to `PluginAdminConfig` because core's loader actually executes\n// against those types.\n//\n// We assert one direction at compile time: `core extends shared`. The\n// reverse direction (`shared extends core`) intentionally does NOT hold\n// because shared is wider -- a manifest written against the wire shape\n// could carry a hook name core doesn't know. That runtime narrowing is the\n// job of `manifest-schema.ts` (zod-validated, called at every JSON.parse\n// of a manifest.json), not of the type system. The static check below\n// catches the OTHER failure mode: core adding a required field or\n// non-assignable type that the wire shape doesn't allow.\n//\n// `type X = never` is itself legal as a type alias, so the assertion has to\n// be in a value position (`const _check: T = true`) for the compiler to\n// error when T resolves to `never`. Don't replace this with a bare type\n// alias.\ntype _AssertManifestCompat =\n\tPluginManifest extends import(\"@emdash-cms/plugin-types\").PluginManifest ? true : never;\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nconst _MANIFEST_COMPAT: _AssertManifestCompat = true;\n"],"mappings":";;;;;;AA81CA,SAAgB,2BAA2B,OAAmD;AAC7F,KAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;CAExD,MAAM,iBAAiB,WAAW,SAAS,YAAY;CACvD,MAAM,iBAAiB,QAAQ,SAAS,aAAa;AACrD,QAAO,kBAAkB,CAAC"}
|
|
@@ -1,9 +1,154 @@
|
|
|
1
|
-
import { m as FieldType } from "./types-
|
|
1
|
+
import { m as FieldType } from "./types-B1gLSAH2.mjs";
|
|
2
2
|
import { z } from "astro/zod";
|
|
3
3
|
import { CAPABILITY_RENAMES, CurrentPluginCapability, DeprecatedPluginCapability, ManifestHookEntry, ManifestRouteEntry, PluginCapability, PluginStorageConfig, isDeprecatedCapability, normalizeCapabilities, normalizeCapability } from "@emdash-cms/plugin-types";
|
|
4
|
-
import { Element } from "@emdash-cms/blocks";
|
|
5
4
|
import { JSX } from "astro/jsx-runtime";
|
|
6
5
|
|
|
6
|
+
//#region ../blocks/dist/validation-5vL6669b.d.ts
|
|
7
|
+
//#region src/types.d.ts
|
|
8
|
+
interface ConfirmDialog {
|
|
9
|
+
title: string;
|
|
10
|
+
text: string;
|
|
11
|
+
confirm: string;
|
|
12
|
+
deny: string;
|
|
13
|
+
style?: "danger";
|
|
14
|
+
}
|
|
15
|
+
interface ButtonElement {
|
|
16
|
+
type: "button";
|
|
17
|
+
action_id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
style?: "primary" | "danger" | "secondary";
|
|
20
|
+
value?: unknown;
|
|
21
|
+
confirm?: ConfirmDialog;
|
|
22
|
+
}
|
|
23
|
+
interface TextInputElement {
|
|
24
|
+
type: "text_input";
|
|
25
|
+
action_id: string;
|
|
26
|
+
label: string;
|
|
27
|
+
placeholder?: string;
|
|
28
|
+
initial_value?: string;
|
|
29
|
+
multiline?: boolean;
|
|
30
|
+
}
|
|
31
|
+
interface NumberInputElement {
|
|
32
|
+
type: "number_input";
|
|
33
|
+
action_id: string;
|
|
34
|
+
label: string;
|
|
35
|
+
initial_value?: number;
|
|
36
|
+
min?: number;
|
|
37
|
+
max?: number;
|
|
38
|
+
}
|
|
39
|
+
interface SelectElement {
|
|
40
|
+
type: "select";
|
|
41
|
+
action_id: string;
|
|
42
|
+
label: string;
|
|
43
|
+
options: Array<{
|
|
44
|
+
label: string;
|
|
45
|
+
value: string;
|
|
46
|
+
}>;
|
|
47
|
+
initial_value?: string;
|
|
48
|
+
/** Plugin route that returns `{ items: Array<{ id, name }> }` to populate options dynamically */
|
|
49
|
+
optionsRoute?: string;
|
|
50
|
+
}
|
|
51
|
+
interface ToggleElement {
|
|
52
|
+
type: "toggle";
|
|
53
|
+
action_id: string;
|
|
54
|
+
label: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
initial_value?: boolean;
|
|
57
|
+
}
|
|
58
|
+
interface SecretInputElement {
|
|
59
|
+
type: "secret_input";
|
|
60
|
+
action_id: string;
|
|
61
|
+
label: string;
|
|
62
|
+
placeholder?: string;
|
|
63
|
+
has_value?: boolean;
|
|
64
|
+
}
|
|
65
|
+
interface CheckboxElement {
|
|
66
|
+
type: "checkbox";
|
|
67
|
+
action_id: string;
|
|
68
|
+
label: string;
|
|
69
|
+
options: Array<{
|
|
70
|
+
label: string;
|
|
71
|
+
value: string;
|
|
72
|
+
}>;
|
|
73
|
+
initial_value?: string[];
|
|
74
|
+
}
|
|
75
|
+
interface DateInputElement {
|
|
76
|
+
type: "date_input";
|
|
77
|
+
action_id: string;
|
|
78
|
+
label: string;
|
|
79
|
+
initial_value?: string;
|
|
80
|
+
placeholder?: string;
|
|
81
|
+
}
|
|
82
|
+
interface ComboboxElement {
|
|
83
|
+
type: "combobox";
|
|
84
|
+
action_id: string;
|
|
85
|
+
label: string;
|
|
86
|
+
options: Array<{
|
|
87
|
+
label: string;
|
|
88
|
+
value: string;
|
|
89
|
+
}>;
|
|
90
|
+
initial_value?: string;
|
|
91
|
+
placeholder?: string;
|
|
92
|
+
}
|
|
93
|
+
interface RadioElement {
|
|
94
|
+
type: "radio";
|
|
95
|
+
action_id: string;
|
|
96
|
+
label: string;
|
|
97
|
+
options: Array<{
|
|
98
|
+
label: string;
|
|
99
|
+
value: string;
|
|
100
|
+
}>;
|
|
101
|
+
initial_value?: string;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Sub-field types allowed inside a RepeaterElement. Limited to the scalar
|
|
105
|
+
* inputs the admin widget currently renders inline.
|
|
106
|
+
*/
|
|
107
|
+
type RepeaterSubField = TextInputElement | NumberInputElement | SelectElement | ToggleElement;
|
|
108
|
+
/**
|
|
109
|
+
* Array-of-objects field. Renders as a list of collapsible cards with inline
|
|
110
|
+
* add/remove and drag-and-drop reordering. Sub-fields are scalar Block Kit
|
|
111
|
+
* elements keyed by their `action_id`.
|
|
112
|
+
*
|
|
113
|
+
* Admin-authoring only: this element is rendered by the admin widget so plugin
|
|
114
|
+
* blocks can capture repeating data. The runtime block renderer
|
|
115
|
+
* (`renderElement`) deliberately returns `null` for `repeater` — repeater
|
|
116
|
+
* values are persisted on the parent block and consumed by the plugin's own
|
|
117
|
+
* runtime component, not re-rendered as a stand-alone block.
|
|
118
|
+
*/
|
|
119
|
+
interface RepeaterElement {
|
|
120
|
+
type: "repeater";
|
|
121
|
+
action_id: string;
|
|
122
|
+
label: string;
|
|
123
|
+
/** Singular label used in the UI (e.g. "FAQ" → "Add FAQ"). */
|
|
124
|
+
item_label?: string;
|
|
125
|
+
fields: RepeaterSubField[];
|
|
126
|
+
min_items?: number;
|
|
127
|
+
max_items?: number;
|
|
128
|
+
/**
|
|
129
|
+
* Default rows for the field. Note: the admin widget seeds new rows from
|
|
130
|
+
* the sub-field types (empty string / `false`), not from `initial_value`;
|
|
131
|
+
* plugins should populate persisted state via the form `values` payload
|
|
132
|
+
* instead of relying on `initial_value` for pre-filled rows.
|
|
133
|
+
*/
|
|
134
|
+
initial_value?: Array<Record<string, unknown>>;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Picks an item from the media library (or uploads a new one). The stored value
|
|
138
|
+
* is the selected asset's URL string, so this element is value-compatible with a
|
|
139
|
+
* plain `text_input` — existing content continues to work after swapping.
|
|
140
|
+
*/
|
|
141
|
+
interface MediaPickerElement {
|
|
142
|
+
type: "media_picker";
|
|
143
|
+
action_id: string;
|
|
144
|
+
label: string;
|
|
145
|
+
/** Mime-type prefix filter (e.g. "image/"). Defaults to "image/". */
|
|
146
|
+
mime_type_filter?: string;
|
|
147
|
+
initial_value?: string;
|
|
148
|
+
placeholder?: string;
|
|
149
|
+
}
|
|
150
|
+
type Element = ButtonElement | TextInputElement | NumberInputElement | SelectElement | ToggleElement | SecretInputElement | CheckboxElement | DateInputElement | ComboboxElement | RadioElement | RepeaterElement | MediaPickerElement;
|
|
151
|
+
//#endregion
|
|
7
152
|
//#region src/plugins/types.d.ts
|
|
8
153
|
/**
|
|
9
154
|
* Query filter operators
|
|
@@ -1100,5 +1245,5 @@ interface PluginManifest {
|
|
|
1100
1245
|
admin: PluginAdminConfig;
|
|
1101
1246
|
}
|
|
1102
1247
|
//#endregion
|
|
1103
|
-
export { ResolvedPlugin as $, PageFragmentContribution as A, PluginAdminPage as B, HttpAccess as C, MediaItem as D, MediaAccess as E, PageMetadataHandler as F, PluginManifest as G, PluginContext as H, PageMetadataLinkRel as I, PortableTextBlockConfig as J, PluginRoute as K, PagePlacement as L, PageFragmentHandler as M, PageMetadataContribution as N, MediaUploadEvent as O, PageMetadataEvent as P, ResolvedHook as Q, PluginAdminConfig as R, HookName as S, LogAccess as T, PluginDefinition as U, PluginCapability as V, PluginHooks as W, PublicPageContext as X, PortableTextBlockField as Y, RequestMeta as Z, CurrentPluginCapability as _, CommentAfterCreateHandler as a, StandardRouteEntry as at, FieldWidgetConfig as b, CommentBeforeCreateEvent as c, StoredComment as ct, CommentModerateHandler as d, normalizeCapabilities as dt, ResolvedPluginHooks as et, ContentAccess as f, normalizeCapability as ft, CronEvent as g, ContentPublishStateChangeEvent as h, CommentAfterCreateEvent as i, StandardPluginDefinition as it, PageFragmentEvent as j, ModerationDecision as k, CommentBeforeCreateHandler as l, isDeprecatedCapability as lt, ContentHookEvent as m, CAPABILITY_RENAMES as n, StandardHookEntry as nt, CommentAfterModerateEvent as o, StandardRouteHandler as ot, ContentDeleteEvent as p, PluginStorageConfig as q, CollectionCommentSettings as r, StandardHookHandler as rt, CommentAfterModerateHandler as s, StorageCollection as st, BreadcrumbItem as t, RouteContext as tt, CommentModerateEvent as u, isStandardPluginDefinition as ut, DeprecatedPluginCapability as v, KVAccess as w, HookConfig as x, EmailMessage as y, PluginAdminExports as z };
|
|
1104
|
-
//# sourceMappingURL=types-
|
|
1248
|
+
export { ResolvedPlugin as $, PageFragmentContribution as A, PluginAdminPage as B, HttpAccess as C, MediaItem as D, MediaAccess as E, PageMetadataHandler as F, PluginManifest as G, PluginContext as H, PageMetadataLinkRel as I, PortableTextBlockConfig as J, PluginRoute as K, PagePlacement as L, PageFragmentHandler as M, PageMetadataContribution as N, MediaUploadEvent as O, PageMetadataEvent as P, ResolvedHook as Q, PluginAdminConfig as R, HookName as S, LogAccess as T, PluginDefinition as U, PluginCapability as V, PluginHooks as W, PublicPageContext as X, PortableTextBlockField as Y, RequestMeta as Z, CurrentPluginCapability as _, CommentAfterCreateHandler as a, StandardRouteEntry as at, FieldWidgetConfig as b, CommentBeforeCreateEvent as c, StoredComment as ct, CommentModerateHandler as d, normalizeCapabilities as dt, ResolvedPluginHooks as et, ContentAccess as f, normalizeCapability as ft, CronEvent as g, ContentPublishStateChangeEvent as h, CommentAfterCreateEvent as i, StandardPluginDefinition as it, PageFragmentEvent as j, ModerationDecision as k, CommentBeforeCreateHandler as l, isDeprecatedCapability as lt, ContentHookEvent as m, CAPABILITY_RENAMES as n, StandardHookEntry as nt, CommentAfterModerateEvent as o, StandardRouteHandler as ot, ContentDeleteEvent as p, Element as pt, PluginStorageConfig as q, CollectionCommentSettings as r, StandardHookHandler as rt, CommentAfterModerateHandler as s, StorageCollection as st, BreadcrumbItem as t, RouteContext as tt, CommentModerateEvent as u, isStandardPluginDefinition as ut, DeprecatedPluginCapability as v, KVAccess as w, HookConfig as x, EmailMessage as y, PluginAdminExports as z };
|
|
1249
|
+
//# sourceMappingURL=types-DgSc9Rpc.d.mts.map
|