emdash 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapters-N6BF7RCD.d.mts → adapters-C2BzVy0p.d.mts} +1 -1
- package/dist/{adapters-N6BF7RCD.d.mts.map → adapters-C2BzVy0p.d.mts.map} +1 -1
- package/dist/{apply-wmVEOSbR.mjs → apply-Cma_PiF6.mjs} +38 -23
- package/dist/apply-Cma_PiF6.mjs.map +1 -0
- package/dist/astro/index.d.mts +25 -11
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +38 -25
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.mjs +2 -2
- package/dist/astro/middleware/redirect.d.mts.map +1 -1
- package/dist/astro/middleware/redirect.mjs +20 -8
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.mjs +12 -2
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +52 -45
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +9 -9
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-1WQPlISL.mjs → byline-WuOq9MFJ.mjs} +5 -4
- package/dist/byline-WuOq9MFJ.mjs.map +1 -0
- package/dist/{bylines-BYdTYmia.mjs → bylines-C_Wsnz4L.mjs} +38 -6
- package/dist/bylines-C_Wsnz4L.mjs.map +1 -0
- package/dist/cache-E3Dts-yT.mjs +56 -0
- package/dist/cache-E3Dts-yT.mjs.map +1 -0
- package/dist/cli/index.mjs +13 -13
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/{config-Cq8H0SfX.mjs → config-DkxPrM9l.mjs} +1 -1
- package/dist/{config-Cq8H0SfX.mjs.map → config-DkxPrM9l.mjs.map} +1 -1
- package/dist/{content-BmXndhdi.mjs → content-BsBoyj8G.mjs} +20 -3
- package/dist/content-BsBoyj8G.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{default-WYlzADZL.mjs → default-PUx9RK6u.mjs} +1 -1
- package/dist/{default-WYlzADZL.mjs.map → default-PUx9RK6u.mjs.map} +1 -1
- package/dist/{dialect-helpers-B9uSp2GJ.mjs → dialect-helpers-DhTzaUxP.mjs} +4 -1
- package/dist/dialect-helpers-DhTzaUxP.mjs.map +1 -0
- package/dist/{error-DrxtnGPg.mjs → error-HBeQbVhV.mjs} +1 -1
- package/dist/{error-DrxtnGPg.mjs.map → error-HBeQbVhV.mjs.map} +1 -1
- package/dist/{index-UHEVQMus.d.mts → index-CRg3PWfZ.d.mts} +59 -33
- package/dist/index-CRg3PWfZ.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +20 -20
- package/dist/{load-Veizk2cT.mjs → load-BhSSm-TS.mjs} +1 -1
- package/dist/{load-Veizk2cT.mjs.map → load-BhSSm-TS.mjs.map} +1 -1
- package/dist/{loader-CHb2v0jm.mjs → loader-BYzwzORf.mjs} +4 -2
- package/dist/loader-BYzwzORf.mjs.map +1 -0
- package/dist/{manifest-schema-CuMio1A9.mjs → manifest-schema-BsXINkQD.mjs} +1 -1
- package/dist/{manifest-schema-CuMio1A9.mjs.map → manifest-schema-BsXINkQD.mjs.map} +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/{mode-CYeM2rPt.mjs → mode-CyPLdO3C.mjs} +1 -1
- package/dist/{mode-CYeM2rPt.mjs.map → mode-CyPLdO3C.mjs.map} +1 -1
- package/dist/page/index.d.mts +1 -1
- package/dist/patterns-CrCYkMBb.mjs +93 -0
- package/dist/patterns-CrCYkMBb.mjs.map +1 -0
- package/dist/{placeholder-bOx1xCTY.d.mts → placeholder-BBCtpTES.d.mts} +1 -1
- package/dist/{placeholder-bOx1xCTY.d.mts.map → placeholder-BBCtpTES.d.mts.map} +1 -1
- package/dist/{placeholder-aiCD8aSZ.mjs → placeholder-DntBEQo7.mjs} +1 -1
- package/dist/{placeholder-aiCD8aSZ.mjs.map → placeholder-DntBEQo7.mjs.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-5Hcv_5ER.mjs → query-B6Vu0d2i.mjs} +35 -16
- package/dist/{query-5Hcv_5ER.mjs.map → query-B6Vu0d2i.mjs.map} +1 -1
- package/dist/{redirect-DIfIni3r.mjs → redirect-7lGhLBNZ.mjs} +10 -93
- package/dist/redirect-7lGhLBNZ.mjs.map +1 -0
- package/dist/{registry-1EvbAfsC.mjs → registry-BgnP3ysR.mjs} +27 -37
- package/dist/registry-BgnP3ysR.mjs.map +1 -0
- package/dist/{runner-BoN0-FPi.mjs → runner-Cd-_WyDo.mjs} +18 -6
- package/dist/runner-Cd-_WyDo.mjs.map +1 -0
- package/dist/{runner-DTqkzOzc.d.mts → runner-DYv3rX8P.d.mts} +10 -3
- package/dist/runner-DYv3rX8P.d.mts.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-BsYMed12.mjs → search-B5p9D36n.mjs} +108 -57
- package/dist/search-B5p9D36n.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +10 -10
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +11 -3
- package/dist/storage/s3.d.mts.map +1 -1
- package/dist/storage/s3.mjs +76 -15
- package/dist/storage/s3.mjs.map +1 -1
- package/dist/{tokens-DrB-W6Q-.mjs → tokens-DKHiCYCB.mjs} +1 -1
- package/dist/{tokens-DrB-W6Q-.mjs.map → tokens-DKHiCYCB.mjs.map} +1 -1
- package/dist/transaction-Cn2rjY78.mjs +28 -0
- package/dist/transaction-Cn2rjY78.mjs.map +1 -0
- package/dist/{transport-Bl8cTdYt.mjs → transport-BtcQ-Z7T.mjs} +1 -1
- package/dist/{transport-Bl8cTdYt.mjs.map → transport-BtcQ-Z7T.mjs.map} +1 -1
- package/dist/{transport-COOs9GSE.d.mts → transport-CKQA_G44.d.mts} +1 -1
- package/dist/{transport-COOs9GSE.d.mts.map → transport-CKQA_G44.d.mts.map} +1 -1
- package/dist/{types-7-UjSEyB.d.mts → types-B6BzlZxx.d.mts} +1 -1
- package/dist/{types-7-UjSEyB.d.mts.map → types-B6BzlZxx.d.mts.map} +1 -1
- package/dist/{types-6dqxBqsH.d.mts → types-BYWYxLcp.d.mts} +109 -5
- package/dist/types-BYWYxLcp.d.mts.map +1 -0
- package/dist/{types-CIsTnQvJ.d.mts → types-BmkQR1En.d.mts} +1 -1
- package/dist/{types-CIsTnQvJ.d.mts.map → types-BmkQR1En.d.mts.map} +1 -1
- package/dist/{types-BljtYPSd.d.mts → types-DNZpaCBk.d.mts} +14 -6
- package/dist/types-DNZpaCBk.d.mts.map +1 -0
- package/dist/{types-Bec-r_3_.mjs → types-Dz9_WMS6.mjs} +1 -1
- package/dist/types-Dz9_WMS6.mjs.map +1 -0
- package/dist/{types-CcreFIIH.d.mts → types-gLYVCXCQ.d.mts} +1 -1
- package/dist/{types-CcreFIIH.d.mts.map → types-gLYVCXCQ.d.mts.map} +1 -1
- package/dist/{types-DuNbGKjF.mjs → types-xxCWI3j0.mjs} +1 -1
- package/dist/{types-DuNbGKjF.mjs.map → types-xxCWI3j0.mjs.map} +1 -1
- package/dist/{validate-B7KP7VLM.d.mts → validate-CcNRWH6I.d.mts} +4 -4
- package/dist/{validate-B7KP7VLM.d.mts.map → validate-CcNRWH6I.d.mts.map} +1 -1
- package/dist/{validate-CXnRKfJK.mjs → validate-DuZDIxfy.mjs} +2 -2
- package/dist/{validate-CXnRKfJK.mjs.map → validate-DuZDIxfy.mjs.map} +1 -1
- package/dist/{validate-CqRJb_xU.mjs → validate-VPnKoIzW.mjs} +11 -11
- package/dist/{validate-CqRJb_xU.mjs.map → validate-VPnKoIzW.mjs.map} +1 -1
- package/dist/version-DlTDRdpv.mjs +7 -0
- package/dist/version-DlTDRdpv.mjs.map +1 -0
- package/package.json +7 -5
- package/src/api/handlers/content.ts +36 -25
- package/src/api/handlers/menus.ts +19 -16
- package/src/api/handlers/redirects.ts +95 -3
- package/src/api/schemas/redirects.ts +1 -0
- package/src/astro/integration/index.ts +2 -3
- package/src/astro/integration/runtime.ts +8 -14
- package/src/astro/integration/vite-config.ts +14 -4
- package/src/astro/middleware/redirect.ts +30 -15
- package/src/astro/middleware.ts +11 -19
- package/src/astro/routes/admin.astro +2 -2
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +3 -0
- package/src/astro/routes/api/admin/bylines/index.ts +2 -0
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +2 -0
- package/src/astro/routes/api/manifest.ts +3 -1
- package/src/astro/routes/api/redirects/[id].ts +3 -0
- package/src/astro/routes/api/redirects/index.ts +2 -0
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -0
- package/src/astro/routes/api/schema/collections/index.ts +1 -0
- package/src/astro/storage/adapters.ts +19 -5
- package/src/astro/storage/types.ts +12 -4
- package/src/astro/types.ts +1 -0
- package/src/bylines/index.ts +50 -2
- package/src/cleanup.ts +3 -3
- package/src/cli/commands/bundle-utils.ts +5 -5
- package/src/database/dialect-helpers.ts +3 -0
- package/src/database/migrations/011_sections.ts +2 -2
- package/src/database/migrations/runner.ts +23 -2
- package/src/database/repositories/byline.ts +2 -1
- package/src/database/repositories/content.ts +5 -0
- package/src/database/repositories/redirect.ts +13 -0
- package/src/database/validate.ts +10 -10
- package/src/emdash-runtime.ts +23 -9
- package/src/index.ts +3 -0
- package/src/loader.ts +2 -0
- package/src/mcp/server.ts +40 -67
- package/src/menus/index.ts +4 -0
- package/src/plugins/context.ts +28 -4
- package/src/plugins/cron.ts +29 -4
- package/src/plugins/hooks.ts +22 -10
- package/src/plugins/index.ts +1 -0
- package/src/plugins/manager.ts +6 -2
- package/src/plugins/marketplace.ts +33 -3
- package/src/plugins/routes.ts +3 -3
- package/src/plugins/types.ts +7 -0
- package/src/query.ts +37 -14
- package/src/redirects/cache.ts +68 -0
- package/src/redirects/loops.ts +318 -0
- package/src/schema/registry.ts +3 -0
- package/src/search/fts-manager.ts +24 -11
- package/src/search/query.ts +8 -9
- package/src/seed/apply.ts +49 -28
- package/src/storage/s3.ts +94 -25
- package/src/storage/types.ts +13 -5
- package/src/utils/slugify.ts +11 -0
- package/src/version.ts +12 -0
- package/src/visual-editing/toolbar.ts +11 -1
- package/dist/apply-wmVEOSbR.mjs.map +0 -1
- package/dist/byline-1WQPlISL.mjs.map +0 -1
- package/dist/bylines-BYdTYmia.mjs.map +0 -1
- package/dist/content-BmXndhdi.mjs.map +0 -1
- package/dist/dialect-helpers-B9uSp2GJ.mjs.map +0 -1
- package/dist/index-UHEVQMus.d.mts.map +0 -1
- package/dist/loader-CHb2v0jm.mjs.map +0 -1
- package/dist/redirect-DIfIni3r.mjs.map +0 -1
- package/dist/registry-1EvbAfsC.mjs.map +0 -1
- package/dist/runner-BoN0-FPi.mjs.map +0 -1
- package/dist/runner-DTqkzOzc.d.mts.map +0 -1
- package/dist/search-BsYMed12.mjs.map +0 -1
- package/dist/types-6dqxBqsH.d.mts.map +0 -1
- package/dist/types-Bec-r_3_.mjs.map +0 -1
- package/dist/types-BljtYPSd.d.mts.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search-B5p9D36n.mjs","names":["SEO_DEFAULTS","z","z","z","z","z","z","authenticatorTransport","z","registrationCredential","z","z","z","z","z","z","z","z","z","z","convertList","convertCodeBlock","convertImage","convertListItem","EXCLUSIVE_HOOK_KEY_PREFIX","resolveExclusiveHooksShared","TRAILING_SLASHES","WP_JSON_SUFFIX","normalizeUrl","normalizeUrl","getWidgetComponents","getComponentRegistry"],"sources":["../src/database/repositories/user.ts","../src/database/repositories/comment.ts","../src/plugins/storage-query.ts","../src/database/repositories/plugin-storage.ts","../src/fields/image.ts","../src/fields/reference.ts","../src/fields/portable-text.ts","../src/database/repositories/seo.ts","../src/api/rev.ts","../src/api/handlers/content.ts","../src/utils/hash.ts","../src/api/handlers/manifest.ts","../src/api/handlers/revision.ts","../src/api/handlers/media.ts","../src/schema/query.ts","../src/plugins/state.ts","../src/sections/index.ts","../src/api/handlers/marketplace.ts","../src/api/parse.ts","../src/api/schemas/common.ts","../src/api/schemas/bylines.ts","../src/api/schemas/content.ts","../src/api/schemas/media.ts","../src/api/schemas/schema.ts","../src/api/schemas/comments.ts","../src/api/schemas/auth.ts","../src/utils/url.ts","../src/api/schemas/menus.ts","../src/api/schemas/taxonomies.ts","../src/api/schemas/sections.ts","../src/api/schemas/settings.ts","../src/api/schemas/search.ts","../src/api/schemas/import.ts","../src/api/schemas/setup.ts","../src/api/schemas/users.ts","../src/api/schemas/widgets.ts","../src/api/schemas/redirects.ts","../src/content/converters/prosemirror-to-portable-text.ts","../src/content/converters/portable-text-to-prosemirror.ts","../src/cli/wxr/parser.ts","../src/plugins/define-plugin.ts","../src/plugins/request-meta.ts","../src/plugins/cron.ts","../src/plugins/context.ts","../src/plugins/hooks.ts","../src/plugins/email.ts","../src/plugins/email-console.ts","../src/plugins/routes.ts","../src/plugins/manager.ts","../src/plugins/sandbox/noop.ts","../src/plugins/types.ts","../src/import/sections.ts","../src/import/registry.ts","../src/import/utils.ts","../src/import/sources/wxr.ts","../src/import/sources/wordpress-rest.ts","../src/import/sources/wordpress-plugin.ts","../src/import/index.ts","../src/preview/urls.ts","../src/preview/helpers.ts","../src/comments/query.ts","../src/menus/index.ts","../src/taxonomies/index.ts","../src/widgets/components.ts","../src/widgets/index.ts","../src/search/query.ts","../src/search/text-extraction.ts"],"sourcesContent":["import type { Kysely, Selectable, Updateable } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport type { Database, UserTable } from \"../types.js\";\nimport { encodeCursor, decodeCursor, type FindManyResult } from \"./types.js\";\n\ntype UserRow = Selectable<UserTable>;\n\n/**\n * Valid role levels matching the database schema.\n * 10=subscriber, 20=contributor, 30=author, 40=editor, 50=admin\n */\nexport type UserRole = 10 | 20 | 30 | 40 | 50;\n\n/** String role names for convenience APIs */\nexport type UserRoleName = \"subscriber\" | \"contributor\" | \"author\" | \"editor\" | \"admin\";\n\nexport interface User {\n\tid: string;\n\temail: string;\n\tname: string | null;\n\trole: UserRole;\n\tavatarUrl: string | null;\n\temailVerified: boolean;\n\tdata: Record<string, unknown> | null;\n\tcreatedAt: string;\n}\n\nexport interface CreateUserInput {\n\temail: string;\n\tname?: string;\n\trole?: UserRole | UserRoleName;\n\tavatarUrl?: string;\n\tdata?: Record<string, unknown>;\n}\n\nexport interface UpdateUserInput {\n\tname?: string;\n\trole?: UserRole | UserRoleName;\n\tavatarUrl?: string | null;\n\tdata?: Record<string, unknown>;\n}\n\n/**\n * User repository for CRUD operations\n */\nexport class UserRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Create a new user\n\t */\n\tasync create(input: CreateUserInput): Promise<User> {\n\t\tconst id = ulid();\n\n\t\tconst row: Omit<UserTable, \"created_at\" | \"updated_at\" | \"disabled\"> = {\n\t\t\tid,\n\t\t\temail: input.email.toLowerCase(),\n\t\t\tname: input.name ?? null,\n\t\t\trole: UserRepository.resolveRole(input.role ?? 10),\n\t\t\tavatar_url: input.avatarUrl ?? null,\n\t\t\temail_verified: 0,\n\t\t\tdata: input.data ? JSON.stringify(input.data) : null,\n\t\t};\n\n\t\tawait this.db.insertInto(\"users\").values(row).execute();\n\n\t\tconst user = await this.findById(id);\n\t\tif (!user) {\n\t\t\tthrow new Error(\"Failed to create user\");\n\t\t}\n\t\treturn user;\n\t}\n\n\t/**\n\t * Find user by ID\n\t */\n\tasync findById(id: string): Promise<User | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"users\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToUser(row) : null;\n\t}\n\n\t/**\n\t * Find user by email (case-insensitive)\n\t */\n\tasync findByEmail(email: string): Promise<User | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"users\")\n\t\t\t.selectAll()\n\t\t\t.where(\"email\", \"=\", email.toLowerCase())\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToUser(row) : null;\n\t}\n\n\t/**\n\t * List all users with cursor-based pagination\n\t */\n\tasync findMany(\n\t\toptions: {\n\t\t\trole?: UserRole | UserRoleName;\n\t\t\tlimit?: number;\n\t\t\tcursor?: string;\n\t\t} = {},\n\t): Promise<FindManyResult<User>> {\n\t\tconst limit = Math.min(Math.max(1, options.limit || 50), 100);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"users\")\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\tif (options.role !== undefined) {\n\t\t\tquery = query.where(\"role\", \"=\", UserRepository.resolveRole(options.role));\n\t\t}\n\n\t\tif (options.cursor) {\n\t\t\tconst decoded = decodeCursor(options.cursor);\n\t\t\tif (decoded) {\n\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(\"created_at\", \"<\", decoded.orderValue),\n\t\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \"<\", decoded.id)]),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tconst rows = await query.execute();\n\t\tconst items = rows.slice(0, limit).map((row) => this.rowToUser(row));\n\t\tconst result: FindManyResult<User> = { items };\n\n\t\tif (rows.length > limit && items.length > 0) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Update a user\n\t */\n\tasync update(id: string, input: UpdateUserInput): Promise<User | null> {\n\t\tconst existing = await this.findById(id);\n\t\tif (!existing) return null;\n\n\t\tconst updates: Updateable<UserTable> = {};\n\t\tif (input.name !== undefined) updates.name = input.name;\n\t\tif (input.role !== undefined) updates.role = UserRepository.resolveRole(input.role);\n\t\tif (input.avatarUrl !== undefined) updates.avatar_url = input.avatarUrl;\n\t\tif (input.data !== undefined) updates.data = JSON.stringify(input.data);\n\n\t\tif (Object.keys(updates).length > 0) {\n\t\t\tawait this.db.updateTable(\"users\").set(updates).where(\"id\", \"=\", id).execute();\n\t\t}\n\n\t\treturn this.findById(id);\n\t}\n\n\t/**\n\t * Delete a user\n\t */\n\tasync delete(id: string): Promise<boolean> {\n\t\tconst result = await this.db.deleteFrom(\"users\").where(\"id\", \"=\", id).executeTakeFirst();\n\n\t\treturn (result.numDeletedRows ?? 0) > 0;\n\t}\n\n\t/**\n\t * Count users\n\t */\n\tasync count(role?: UserRole | UserRoleName): Promise<number> {\n\t\tlet query = this.db.selectFrom(\"users\").select((eb) => eb.fn.count(\"id\").as(\"count\"));\n\n\t\tif (role !== undefined) {\n\t\t\tquery = query.where(\"role\", \"=\", UserRepository.resolveRole(role));\n\t\t}\n\n\t\tconst result = await query.executeTakeFirst();\n\t\treturn Number(result?.count || 0);\n\t}\n\n\t/**\n\t * Check if email exists\n\t */\n\tasync emailExists(email: string): Promise<boolean> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"users\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"email\", \"=\", email.toLowerCase())\n\t\t\t.executeTakeFirst();\n\n\t\treturn !!row;\n\t}\n\n\t/**\n\t * Convert database row to User object\n\t */\n\tprivate rowToUser(row: UserRow): User {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\temail: row.email,\n\t\t\tname: row.name,\n\t\t\trole: UserRepository.toRole(row.role),\n\t\t\tavatarUrl: row.avatar_url,\n\t\t\temailVerified: row.email_verified === 1,\n\t\t\tdata: row.data ? JSON.parse(row.data) : null,\n\t\t\tcreatedAt: row.created_at,\n\t\t};\n\t}\n\n\t/** Map of role name strings to numeric levels */\n\tprivate static readonly ROLE_NAME_TO_LEVEL: Record<UserRoleName, UserRole> = {\n\t\tsubscriber: 10,\n\t\tcontributor: 20,\n\t\tauthor: 30,\n\t\teditor: 40,\n\t\tadmin: 50,\n\t};\n\n\t/** Valid numeric role levels */\n\tprivate static readonly VALID_LEVELS = new Set<number>([10, 20, 30, 40, 50]);\n\n\t/**\n\t * Resolve a role name or number to a valid numeric UserRole.\n\t * Accepts both string names (\"admin\") and numeric levels (50).\n\t */\n\tstatic resolveRole(role: UserRole | UserRoleName): UserRole {\n\t\tif (typeof role === \"string\") {\n\t\t\tconst level = UserRepository.ROLE_NAME_TO_LEVEL[role];\n\t\t\tif (level === undefined) {\n\t\t\t\tthrow new Error(`Invalid role name: ${role}`);\n\t\t\t}\n\t\t\treturn level;\n\t\t}\n\t\tif (!UserRepository.VALID_LEVELS.has(role)) {\n\t\t\tthrow new Error(`Invalid role level: ${role}`);\n\t\t}\n\t\treturn role;\n\t}\n\n\t/**\n\t * Convert a raw DB integer to a typed UserRole.\n\t * Falls back to subscriber (10) for unknown values.\n\t */\n\tprivate static toRole(level: number): UserRole {\n\t\tif (UserRepository.VALID_LEVELS.has(level)) return level as UserRole;\n\t\treturn 10;\n\t}\n}\n","import { sql, type ExpressionBuilder, type Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport type { Database } from \"../types.js\";\nimport { encodeCursor, decodeCursor, type FindManyResult } from \"./types.js\";\n\n/** Matches LIKE wildcard characters and the escape character itself */\nconst LIKE_ESCAPE_RE = /[%_\\\\]/g;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type CommentStatus = \"pending\" | \"approved\" | \"spam\" | \"trash\";\n\nexport interface Comment {\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: CommentStatus;\n\tipHash: string | null;\n\tuserAgent: string | null;\n\tmoderationMetadata: Record<string, unknown> | null;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n/** Public-facing comment shape — no private fields */\nexport interface PublicComment {\n\tid: string;\n\tparentId: string | null;\n\tauthorName: string;\n\tisRegisteredUser: boolean;\n\tbody: string;\n\tcreatedAt: string;\n\treplies?: PublicComment[];\n}\n\nexport interface CreateCommentInput {\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?: CommentStatus;\n\tipHash?: string | null;\n\tuserAgent?: string | null;\n\tmoderationMetadata?: Record<string, unknown> | null;\n}\n\nexport interface CommentFindOptions {\n\tstatus?: CommentStatus;\n\tcollection?: string;\n\tsearch?: string;\n\tlimit?: number;\n\tcursor?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Repository\n// ---------------------------------------------------------------------------\n\nexport class CommentRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Create a new comment\n\t */\n\tasync create(input: CreateCommentInput): Promise<Comment> {\n\t\tconst id = ulid();\n\t\tconst now = new Date().toISOString();\n\n\t\tawait this.db\n\t\t\t.insertInto(\"_emdash_comments\")\n\t\t\t.values({\n\t\t\t\tid,\n\t\t\t\tcollection: input.collection,\n\t\t\t\tcontent_id: input.contentId,\n\t\t\t\tparent_id: input.parentId ?? null,\n\t\t\t\tauthor_name: input.authorName,\n\t\t\t\tauthor_email: input.authorEmail,\n\t\t\t\tauthor_user_id: input.authorUserId ?? null,\n\t\t\t\tbody: input.body,\n\t\t\t\tstatus: input.status ?? \"pending\",\n\t\t\t\tip_hash: input.ipHash ?? null,\n\t\t\t\tuser_agent: input.userAgent ?? null,\n\t\t\t\tmoderation_metadata: input.moderationMetadata\n\t\t\t\t\t? JSON.stringify(input.moderationMetadata)\n\t\t\t\t\t: null,\n\t\t\t\tcreated_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t})\n\t\t\t.execute();\n\n\t\tconst comment = await this.findById(id);\n\t\tif (!comment) {\n\t\t\tthrow new Error(\"Failed to create comment\");\n\t\t}\n\t\treturn comment;\n\t}\n\n\t/**\n\t * Find comment by ID\n\t */\n\tasync findById(id: string): Promise<Comment | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToComment(row) : null;\n\t}\n\n\t/**\n\t * Find comments for a content item with optional status filter.\n\t * Results are ordered by created_at ASC (oldest first) for display.\n\t */\n\tasync findByContent(\n\t\tcollection: string,\n\t\tcontentId: string,\n\t\toptions: { status?: CommentStatus; limit?: number; cursor?: string } = {},\n\t): Promise<FindManyResult<Comment>> {\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.selectAll()\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId);\n\n\t\tif (options.status) {\n\t\t\tquery = query.where(\"status\", \"=\", options.status);\n\t\t}\n\n\t\t// Cursor pagination (ascending by created_at)\n\t\tif (options.cursor) {\n\t\t\tconst decoded = decodeCursor(options.cursor);\n\t\t\tif (decoded) {\n\t\t\t\tquery = query.where((eb: ExpressionBuilder<Database, \"_emdash_comments\">) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(\"created_at\", \">\", decoded.orderValue),\n\t\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \">\", decoded.id)]),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tquery = query\n\t\t\t.orderBy(\"created_at\", \"asc\")\n\t\t\t.orderBy(\"id\", \"asc\")\n\t\t\t.limit(limit + 1);\n\n\t\tconst rows = await query.execute();\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit).map((r) => this.rowToComment(r));\n\n\t\tconst result: FindManyResult<Comment> = { items };\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Find comments by status (moderation inbox).\n\t * Results are ordered by created_at DESC (newest first).\n\t */\n\tasync findByStatus(\n\t\tstatus: CommentStatus,\n\t\toptions: { collection?: string; search?: string; limit?: number; cursor?: string } = {},\n\t): Promise<FindManyResult<Comment>> {\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\tlet query = this.db.selectFrom(\"_emdash_comments\").selectAll().where(\"status\", \"=\", status);\n\n\t\tif (options.collection) {\n\t\t\tquery = query.where(\"collection\", \"=\", options.collection);\n\t\t}\n\n\t\tif (options.search) {\n\t\t\t// Escape LIKE wildcards to prevent them acting as SQL pattern characters\n\t\t\tconst escaped = options.search.replace(LIKE_ESCAPE_RE, (ch) => `\\\\${ch}`);\n\t\t\tconst term = `%${escaped}%`;\n\t\t\tquery = query.where((eb: ExpressionBuilder<Database, \"_emdash_comments\">) =>\n\t\t\t\teb.or([\n\t\t\t\t\tsql<boolean>`author_name LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t\tsql<boolean>`author_email LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t\tsql<boolean>`body LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\t// Cursor pagination (descending by created_at)\n\t\tif (options.cursor) {\n\t\t\tconst decoded = decodeCursor(options.cursor);\n\t\t\tif (decoded) {\n\t\t\t\tquery = query.where((eb: ExpressionBuilder<Database, \"_emdash_comments\">) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(\"created_at\", \"<\", decoded.orderValue),\n\t\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \"<\", decoded.id)]),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tquery = query\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\tconst rows = await query.execute();\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit).map((r) => this.rowToComment(r));\n\n\t\tconst result: FindManyResult<Comment> = { items };\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Update comment status\n\t */\n\tasync updateStatus(id: string, status: CommentStatus): Promise<Comment | null> {\n\t\tconst now = new Date().toISOString();\n\n\t\tawait this.db\n\t\t\t.updateTable(\"_emdash_comments\")\n\t\t\t.set({ status, updated_at: now })\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.execute();\n\n\t\treturn this.findById(id);\n\t}\n\n\t/**\n\t * Bulk update comment statuses\n\t */\n\tasync bulkUpdateStatus(ids: string[], status: CommentStatus): Promise<number> {\n\t\tif (ids.length === 0) return 0;\n\n\t\tconst now = new Date().toISOString();\n\n\t\tconst result = await this.db\n\t\t\t.updateTable(\"_emdash_comments\")\n\t\t\t.set({ status, updated_at: now })\n\t\t\t.where(\"id\", \"in\", ids)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numUpdatedRows ?? 0);\n\t}\n\n\t/**\n\t * Hard-delete a single comment. Replies cascade via FK.\n\t */\n\tasync delete(id: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_comments\")\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn (result.numDeletedRows ?? 0) > 0;\n\t}\n\n\t/**\n\t * Bulk hard-delete comments\n\t */\n\tasync bulkDelete(ids: string[]): Promise<number> {\n\t\tif (ids.length === 0) return 0;\n\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_comments\")\n\t\t\t.where(\"id\", \"in\", ids)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Delete all comments for a content item (cascade on content deletion)\n\t */\n\tasync deleteByContent(collection: string, contentId: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_comments\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Count comments for a content item, optionally filtered by status\n\t */\n\tasync countByContent(\n\t\tcollection: string,\n\t\tcontentId: string,\n\t\tstatus?: CommentStatus,\n\t): Promise<number> {\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId);\n\n\t\tif (status) {\n\t\t\tquery = query.where(\"status\", \"=\", status);\n\t\t}\n\n\t\tconst result = await query.executeTakeFirst();\n\t\treturn Number(result?.count ?? 0);\n\t}\n\n\t/**\n\t * Count comments grouped by status (for inbox badges)\n\t *\n\t * Uses four parallel COUNT queries with WHERE filters to leverage partial indexes\n\t * (idx_comments_pending, idx_comments_approved, idx_comments_spam, idx_comments_trash)\n\t * instead of a full table GROUP BY scan.\n\t */\n\tasync countByStatus(): Promise<Record<CommentStatus, number>> {\n\t\t// Execute four parallel COUNT queries, each using its partial index\n\t\tconst [pending, approved, spam, trash] = await Promise.all([\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"pending\")\n\t\t\t\t.executeTakeFirst(),\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"approved\")\n\t\t\t\t.executeTakeFirst(),\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"spam\")\n\t\t\t\t.executeTakeFirst(),\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"trash\")\n\t\t\t\t.executeTakeFirst(),\n\t\t]);\n\n\t\treturn {\n\t\t\tpending: Number(pending?.count ?? 0),\n\t\t\tapproved: Number(approved?.count ?? 0),\n\t\t\tspam: Number(spam?.count ?? 0),\n\t\t\ttrash: Number(trash?.count ?? 0),\n\t\t};\n\t}\n\n\t/**\n\t * Count approved comments from a given email address.\n\t * Used for \"first time commenter\" moderation logic.\n\t */\n\tasync countApprovedByEmail(email: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"author_email\", \"=\", email)\n\t\t\t.where(\"status\", \"=\", \"approved\")\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result?.count ?? 0);\n\t}\n\n\t/**\n\t * Update the moderation metadata JSON on a comment\n\t */\n\tasync updateModerationMetadata(id: string, metadata: Record<string, unknown>): Promise<void> {\n\t\tawait this.db\n\t\t\t.updateTable(\"_emdash_comments\")\n\t\t\t.set({ moderation_metadata: JSON.stringify(metadata) })\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.execute();\n\t}\n\n\t// ---------------------------------------------------------------------------\n\t// Helpers\n\t// ---------------------------------------------------------------------------\n\n\t/**\n\t * Assemble a flat list of comments into a threaded structure (1-level nesting)\n\t */\n\tstatic assembleThreads(comments: Comment[]): Comment[] {\n\t\tconst roots: Comment[] = [];\n\t\tconst childrenMap = new Map<string, Comment[]>();\n\n\t\tfor (const comment of comments) {\n\t\t\tif (comment.parentId) {\n\t\t\t\tconst siblings = childrenMap.get(comment.parentId) ?? [];\n\t\t\t\tsiblings.push(comment);\n\t\t\t\tchildrenMap.set(comment.parentId, siblings);\n\t\t\t} else {\n\t\t\t\troots.push(comment);\n\t\t\t}\n\t\t}\n\n\t\t// Attach children as a non-standard property — callers map to PublicComment.replies\n\t\treturn roots.map((root) => ({\n\t\t\t...root,\n\t\t\t_replies: childrenMap.get(root.id) ?? [],\n\t\t})) as Comment[];\n\t}\n\n\t/**\n\t * Convert a Comment to its public-facing shape\n\t */\n\tstatic toPublicComment(comment: Comment & { _replies?: Comment[] }): PublicComment {\n\t\tconst pub: PublicComment = {\n\t\t\tid: comment.id,\n\t\t\tparentId: comment.parentId,\n\t\t\tauthorName: comment.authorName,\n\t\t\tisRegisteredUser: comment.authorUserId !== null,\n\t\t\tbody: comment.body,\n\t\t\tcreatedAt: comment.createdAt,\n\t\t};\n\n\t\tif (comment._replies && comment._replies.length > 0) {\n\t\t\tpub.replies = comment._replies.map((r) => CommentRepository.toPublicComment(r));\n\t\t}\n\n\t\treturn pub;\n\t}\n\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any -- selectAll returns runtime row\n\tprivate rowToComment(row: any): Comment {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tcollection: row.collection,\n\t\t\tcontentId: row.content_id,\n\t\t\tparentId: row.parent_id,\n\t\t\tauthorName: row.author_name,\n\t\t\tauthorEmail: row.author_email,\n\t\t\tauthorUserId: row.author_user_id,\n\t\t\tbody: row.body,\n\t\t\tstatus: row.status as CommentStatus,\n\t\t\tipHash: row.ip_hash,\n\t\t\tuserAgent: row.user_agent,\n\t\t\tmoderationMetadata: row.moderation_metadata ? safeJsonParse(row.moderation_metadata) : null,\n\t\t\tcreatedAt: row.created_at,\n\t\t\tupdatedAt: row.updated_at,\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Module helpers\n// ---------------------------------------------------------------------------\n\nfunction safeJsonParse(value: string): Record<string, unknown> | null {\n\ttry {\n\t\treturn JSON.parse(value) as Record<string, unknown>;\n\t} catch {\n\t\treturn null;\n\t}\n}\n","/**\n * Plugin Storage Query Validation and Building\n *\n * Validates that queries only use indexed fields and builds SQL WHERE clauses.\n *\n * @see PLUGIN-SYSTEM.md § Plugin Storage > Query Validation\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { jsonExtractExpr } from \"../database/dialect-helpers.js\";\nimport { validateJsonFieldName } from \"../database/validate.js\";\nimport type { WhereClause, WhereValue, RangeFilter, InFilter, StartsWithFilter } from \"./types.js\";\n\n/**\n * Error thrown when querying non-indexed fields\n */\nexport class StorageQueryError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic field?: string,\n\t\tpublic suggestion?: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"StorageQueryError\";\n\t}\n}\n\n/**\n * Check if a value is a range filter\n */\nexport function isRangeFilter(value: WhereValue): value is RangeFilter {\n\tif (typeof value !== \"object\" || value === null) return false;\n\treturn \"gt\" in value || \"gte\" in value || \"lt\" in value || \"lte\" in value;\n}\n\n/**\n * Check if a value is an IN filter\n */\nexport function isInFilter(value: WhereValue): value is InFilter {\n\tif (typeof value !== \"object\" || value === null) return false;\n\treturn \"in\" in value && Array.isArray(value.in);\n}\n\n/**\n * Check if a value is a startsWith filter\n */\nexport function isStartsWithFilter(value: WhereValue): value is StartsWithFilter {\n\tif (typeof value !== \"object\" || value === null) return false;\n\treturn \"startsWith\" in value && typeof value.startsWith === \"string\";\n}\n\n/**\n * Get the set of indexed fields from index declarations\n */\nexport function getIndexedFields(indexes: Array<string | string[]>): Set<string> {\n\tconst fields = new Set<string>();\n\tfor (const index of indexes) {\n\t\tif (Array.isArray(index)) {\n\t\t\tfor (const field of index) {\n\t\t\t\tfields.add(field);\n\t\t\t}\n\t\t} else {\n\t\t\tfields.add(index);\n\t\t}\n\t}\n\treturn fields;\n}\n\n/**\n * Validate that all fields in a where clause are indexed\n */\nexport function validateWhereClause(\n\twhere: WhereClause,\n\tindexedFields: Set<string>,\n\tpluginId: string,\n\tcollection: string,\n): void {\n\tfor (const field of Object.keys(where)) {\n\t\tif (!indexedFields.has(field)) {\n\t\t\tthrow new StorageQueryError(\n\t\t\t\t`Cannot query on non-indexed field '${field}'.`,\n\t\t\t\tfield,\n\t\t\t\t`Add '${field}' to storage.${collection}.indexes in plugin '${pluginId}' to enable this query.`,\n\t\t\t);\n\t\t}\n\t}\n}\n\n/**\n * Validate orderBy fields are indexed\n */\nexport function validateOrderByClause(\n\torderBy: Record<string, \"asc\" | \"desc\">,\n\tindexedFields: Set<string>,\n\tpluginId: string,\n\tcollection: string,\n): void {\n\tfor (const field of Object.keys(orderBy)) {\n\t\tif (!indexedFields.has(field)) {\n\t\t\tthrow new StorageQueryError(\n\t\t\t\t`Cannot order by non-indexed field '${field}'.`,\n\t\t\t\tfield,\n\t\t\t\t`Add '${field}' to storage.${collection}.indexes in plugin '${pluginId}' to enable ordering by this field.`,\n\t\t\t);\n\t\t}\n\t}\n}\n\n/**\n * SQL expression for extracting JSON field.\n *\n * Validates the field name before interpolation to prevent SQL injection\n * via crafted JSON path expressions.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function jsonExtract(db: Kysely<any>, field: string): string {\n\tvalidateJsonFieldName(field, \"query field name\");\n\treturn jsonExtractExpr(db, \"data\", field);\n}\n\n/**\n * Build a WHERE clause condition for a single field\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function buildCondition(\n\tdb: Kysely<any>,\n\tfield: string,\n\tvalue: WhereValue,\n): { sql: string; params: unknown[] } {\n\tconst extract = jsonExtract(db, field);\n\n\tif (value === null) {\n\t\treturn { sql: `${extract} IS NULL`, params: [] };\n\t}\n\n\tif (typeof value === \"string\" || typeof value === \"number\") {\n\t\treturn { sql: `${extract} = ?`, params: [value] };\n\t}\n\n\tif (typeof value === \"boolean\") {\n\t\t// JSON booleans are stored as true/false strings\n\t\treturn { sql: `${extract} = ?`, params: [value] };\n\t}\n\n\tif (isInFilter(value)) {\n\t\tconst placeholders = value.in.map(() => \"?\").join(\", \");\n\t\treturn {\n\t\t\tsql: `${extract} IN (${placeholders})`,\n\t\t\tparams: value.in,\n\t\t};\n\t}\n\n\tif (isStartsWithFilter(value)) {\n\t\treturn {\n\t\t\tsql: `${extract} LIKE ?`,\n\t\t\tparams: [`${value.startsWith}%`],\n\t\t};\n\t}\n\n\tif (isRangeFilter(value)) {\n\t\tconst conditions: string[] = [];\n\t\tconst params: unknown[] = [];\n\n\t\tif (value.gt !== undefined) {\n\t\t\tconditions.push(`${extract} > ?`);\n\t\t\tparams.push(value.gt);\n\t\t}\n\t\tif (value.gte !== undefined) {\n\t\t\tconditions.push(`${extract} >= ?`);\n\t\t\tparams.push(value.gte);\n\t\t}\n\t\tif (value.lt !== undefined) {\n\t\t\tconditions.push(`${extract} < ?`);\n\t\t\tparams.push(value.lt);\n\t\t}\n\t\tif (value.lte !== undefined) {\n\t\t\tconditions.push(`${extract} <= ?`);\n\t\t\tparams.push(value.lte);\n\t\t}\n\n\t\treturn {\n\t\t\tsql: conditions.join(\" AND \"),\n\t\t\tparams,\n\t\t};\n\t}\n\n\tthrow new StorageQueryError(`Unknown filter type for field '${field}'`);\n}\n\n/**\n * Build a complete WHERE clause from a WhereClause object\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function buildWhereClause(\n\tdb: Kysely<any>,\n\twhere: WhereClause,\n): {\n\tsql: string;\n\tparams: unknown[];\n} {\n\tconst conditions: string[] = [];\n\tconst params: unknown[] = [];\n\n\tfor (const [field, value] of Object.entries(where)) {\n\t\tconst condition = buildCondition(db, field, value);\n\t\tconditions.push(condition.sql);\n\t\tparams.push(...condition.params);\n\t}\n\n\tif (conditions.length === 0) {\n\t\treturn { sql: \"\", params: [] };\n\t}\n\n\treturn {\n\t\tsql: conditions.join(\" AND \"),\n\t\tparams,\n\t};\n}\n\n/**\n * Build ORDER BY clause\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function buildOrderByClause(\n\tdb: Kysely<any>,\n\torderBy: Record<string, \"asc\" | \"desc\">,\n): string {\n\tconst clauses: string[] = [];\n\n\tfor (const [field, direction] of Object.entries(orderBy)) {\n\t\tclauses.push(`${jsonExtract(db, field)} ${direction.toUpperCase()}`);\n\t}\n\n\tif (clauses.length === 0) {\n\t\treturn \"\";\n\t}\n\n\treturn `ORDER BY ${clauses.join(\", \")}`;\n}\n","/**\n * Plugin Storage Repository\n *\n * Provides a document store API for plugin data storage.\n * Uses a single _plugin_storage table with JSON documents and expression indexes.\n *\n * @see PLUGIN-SYSTEM.md § Plugin Storage > Full API Reference\n */\n\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\n\nimport {\n\tbuildWhereClause,\n\tvalidateWhereClause,\n\tvalidateOrderByClause,\n\tgetIndexedFields,\n\tjsonExtract,\n} from \"../../plugins/storage-query.js\";\nimport type {\n\tStorageCollection,\n\tQueryOptions,\n\tPaginatedResult,\n\tWhereClause,\n} from \"../../plugins/types.js\";\nimport { withTransaction } from \"../transaction.js\";\nimport type { Database } from \"../types.js\";\nimport { encodeCursor, decodeCursor } from \"./types.js\";\n\n/**\n * Plugin Storage Repository\n *\n * Implements the StorageCollection interface for a specific plugin and collection.\n */\nexport class PluginStorageRepository<T = unknown> implements StorageCollection<T> {\n\tprivate indexedFields: Set<string>;\n\n\tconstructor(\n\t\tprivate db: Kysely<Database>,\n\t\tprivate pluginId: string,\n\t\tprivate collection: string,\n\t\tindexes: Array<string | string[]>,\n\t) {\n\t\tthis.indexedFields = getIndexedFields(indexes);\n\t}\n\n\t/**\n\t * Get a document by ID\n\t */\n\tasync get(id: string): Promise<T | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_plugin_storage\")\n\t\t\t.select(\"data\")\n\t\t\t.where(\"plugin_id\", \"=\", this.pluginId)\n\t\t\t.where(\"collection\", \"=\", this.collection)\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!row) return null;\n\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- JSON.parse returns any; generic callers provide T\n\t\treturn JSON.parse(row.data) as T;\n\t}\n\n\t/**\n\t * Store a document\n\t */\n\tasync put(id: string, data: T): Promise<void> {\n\t\tconst now = new Date().toISOString();\n\t\tconst jsonData = JSON.stringify(data);\n\n\t\tawait this.db\n\t\t\t.insertInto(\"_plugin_storage\")\n\t\t\t.values({\n\t\t\t\tplugin_id: this.pluginId,\n\t\t\t\tcollection: this.collection,\n\t\t\t\tid,\n\t\t\t\tdata: jsonData,\n\t\t\t\tcreated_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t})\n\t\t\t.onConflict((oc) =>\n\t\t\t\toc.columns([\"plugin_id\", \"collection\", \"id\"]).doUpdateSet({\n\t\t\t\t\tdata: jsonData,\n\t\t\t\t\tupdated_at: now,\n\t\t\t\t}),\n\t\t\t)\n\t\t\t.execute();\n\t}\n\n\t/**\n\t * Delete a document\n\t */\n\tasync delete(id: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_plugin_storage\")\n\t\t\t.where(\"plugin_id\", \"=\", this.pluginId)\n\t\t\t.where(\"collection\", \"=\", this.collection)\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn (result.numDeletedRows ?? 0) > 0;\n\t}\n\n\t/**\n\t * Check if a document exists\n\t */\n\tasync exists(id: string): Promise<boolean> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_plugin_storage\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"plugin_id\", \"=\", this.pluginId)\n\t\t\t.where(\"collection\", \"=\", this.collection)\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn !!row;\n\t}\n\n\t/**\n\t * Get multiple documents by ID\n\t */\n\tasync getMany(ids: string[]): Promise<Map<string, T>> {\n\t\tif (ids.length === 0) return new Map();\n\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_plugin_storage\")\n\t\t\t.select([\"id\", \"data\"])\n\t\t\t.where(\"plugin_id\", \"=\", this.pluginId)\n\t\t\t.where(\"collection\", \"=\", this.collection)\n\t\t\t.where(\"id\", \"in\", ids)\n\t\t\t.execute();\n\n\t\tconst result = new Map<string, T>();\n\t\tfor (const row of rows) {\n\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- JSON.parse returns any; generic callers provide T\n\t\t\tresult.set(row.id, JSON.parse(row.data) as T);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Store multiple documents\n\t */\n\tasync putMany(items: Array<{ id: string; data: T }>): Promise<void> {\n\t\tif (items.length === 0) return;\n\n\t\tconst now = new Date().toISOString();\n\n\t\t// SQLite doesn't support batch upserts well, so we do them one at a time\n\t\t// In a transaction for atomicity\n\t\tawait withTransaction(this.db, async (trx) => {\n\t\t\tfor (const item of items) {\n\t\t\t\tconst jsonData = JSON.stringify(item.data);\n\t\t\t\tawait trx\n\t\t\t\t\t.insertInto(\"_plugin_storage\")\n\t\t\t\t\t.values({\n\t\t\t\t\t\tplugin_id: this.pluginId,\n\t\t\t\t\t\tcollection: this.collection,\n\t\t\t\t\t\tid: item.id,\n\t\t\t\t\t\tdata: jsonData,\n\t\t\t\t\t\tcreated_at: now,\n\t\t\t\t\t\tupdated_at: now,\n\t\t\t\t\t})\n\t\t\t\t\t.onConflict((oc) =>\n\t\t\t\t\t\toc.columns([\"plugin_id\", \"collection\", \"id\"]).doUpdateSet({\n\t\t\t\t\t\t\tdata: jsonData,\n\t\t\t\t\t\t\tupdated_at: now,\n\t\t\t\t\t\t}),\n\t\t\t\t\t)\n\t\t\t\t\t.execute();\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Delete multiple documents\n\t */\n\tasync deleteMany(ids: string[]): Promise<number> {\n\t\tif (ids.length === 0) return 0;\n\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_plugin_storage\")\n\t\t\t.where(\"plugin_id\", \"=\", this.pluginId)\n\t\t\t.where(\"collection\", \"=\", this.collection)\n\t\t\t.where(\"id\", \"in\", ids)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Query documents with filters\n\t */\n\tasync query(options: QueryOptions = {}): Promise<PaginatedResult<{ id: string; data: T }>> {\n\t\tconst { where = {}, orderBy = {}, cursor } = options;\n\t\tconst limit = Math.min(options.limit ?? 50, 100);\n\n\t\t// Validate that all queried fields are indexed\n\t\tvalidateWhereClause(where, this.indexedFields, this.pluginId, this.collection);\n\t\tif (Object.keys(orderBy).length > 0) {\n\t\t\tvalidateOrderByClause(orderBy, this.indexedFields, this.pluginId, this.collection);\n\t\t}\n\n\t\t// Build base query\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_plugin_storage\")\n\t\t\t.select([\"id\", \"data\", \"created_at\"])\n\t\t\t.where(\"plugin_id\", \"=\", this.pluginId)\n\t\t\t.where(\"collection\", \"=\", this.collection);\n\n\t\t// Add JSON extraction WHERE conditions\n\t\tconst whereResult = buildWhereClause(this.db, where);\n\t\tif (whereResult.sql) {\n\t\t\t// Use sql template to add the raw WHERE conditions with params\n\t\t\tconst whereSqlParts: ReturnType<typeof sql>[] = [];\n\t\t\tlet paramIndex = 0;\n\t\t\tconst sqlParts = whereResult.sql.split(\"?\");\n\t\t\tfor (let i = 0; i < sqlParts.length; i++) {\n\t\t\t\tif (i > 0) {\n\t\t\t\t\twhereSqlParts.push(sql`${whereResult.params[paramIndex++]}`);\n\t\t\t\t}\n\t\t\t\tif (sqlParts[i]) {\n\t\t\t\t\twhereSqlParts.push(sql.raw(sqlParts[i]));\n\t\t\t\t}\n\t\t\t}\n\t\t\tquery = query.where(({ eb }) => eb(sql.join(whereSqlParts, sql.raw(\"\")), \"=\", sql.raw(\"1\")));\n\t\t}\n\n\t\t// Handle cursor-based pagination\n\t\tif (cursor) {\n\t\t\tconst decoded = decodeCursor(cursor);\n\t\t\tif (decoded) {\n\t\t\t\tquery = query.where(({ eb }) =>\n\t\t\t\t\teb(sql`(created_at, id)`, \">\", sql`(${decoded.orderValue}, ${decoded.id})`),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Build ORDER BY using sql template\n\t\tif (Object.keys(orderBy).length > 0) {\n\t\t\tfor (const [field, direction] of Object.entries(orderBy)) {\n\t\t\t\tconst extract = jsonExtract(this.db, field);\n\t\t\t\tconst orderExpr =\n\t\t\t\t\tdirection === \"desc\" ? sql`${sql.raw(extract)} desc` : sql`${sql.raw(extract)} asc`;\n\t\t\t\tquery = query.orderBy(orderExpr);\n\t\t\t}\n\t\t} else {\n\t\t\t// Default ordering for consistent pagination\n\t\t\tquery = query.orderBy(\"created_at\", \"asc\").orderBy(\"id\", \"asc\");\n\t\t}\n\n\t\t// Apply limit (fetch one extra to detect if there's more)\n\t\tquery = query.limit(limit + 1);\n\n\t\tconst rows = await query.execute();\n\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit).map((row) => ({\n\t\t\tid: row.id,\n\t\t\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- JSON.parse returns any; generic callers provide T\n\t\t\tdata: JSON.parse(row.data) as T,\n\t\t}));\n\n\t\t// Generate cursor for next page if there are more results\n\t\tlet nextCursor: string | undefined;\n\t\tif (hasMore) {\n\t\t\tconst lastItem = rows[limit - 1];\n\t\t\tif (lastItem) {\n\t\t\t\tnextCursor = encodeCursor(lastItem.created_at, lastItem.id);\n\t\t\t}\n\t\t}\n\n\t\treturn { items, cursor: nextCursor, hasMore };\n\t}\n\n\t/**\n\t * Count documents matching a filter\n\t */\n\tasync count(where?: WhereClause): Promise<number> {\n\t\tif (where && Object.keys(where).length > 0) {\n\t\t\tvalidateWhereClause(where, this.indexedFields, this.pluginId, this.collection);\n\t\t}\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_plugin_storage\")\n\t\t\t.select(sql<number>`COUNT(*)`.as(\"count\"))\n\t\t\t.where(\"plugin_id\", \"=\", this.pluginId)\n\t\t\t.where(\"collection\", \"=\", this.collection);\n\n\t\t// Add JSON extraction WHERE conditions\n\t\tif (where && Object.keys(where).length > 0) {\n\t\t\tconst whereResult = buildWhereClause(this.db, where);\n\t\t\tif (whereResult.sql) {\n\t\t\t\t// Use sql template to add the raw WHERE conditions with params\n\t\t\t\tconst whereSqlParts: ReturnType<typeof sql>[] = [];\n\t\t\t\tlet paramIndex = 0;\n\t\t\t\tconst sqlParts = whereResult.sql.split(\"?\");\n\t\t\t\tfor (let i = 0; i < sqlParts.length; i++) {\n\t\t\t\t\tif (i > 0) {\n\t\t\t\t\t\twhereSqlParts.push(sql`${whereResult.params[paramIndex++]}`);\n\t\t\t\t\t}\n\t\t\t\t\tif (sqlParts[i]) {\n\t\t\t\t\t\twhereSqlParts.push(sql.raw(sqlParts[i]));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tquery = query.where(({ eb }) =>\n\t\t\t\t\teb(sql.join(whereSqlParts, sql.raw(\"\")), \"=\", sql.raw(\"1\")),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tconst result = await query.executeTakeFirst();\n\t\treturn result?.count ?? 0;\n\t}\n}\n\n/**\n * Create a scoped storage accessor for a plugin\n */\nexport function createPluginStorageAccessor(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n\tstorageConfig: Record<\n\t\tstring,\n\t\t{ indexes: Array<string | string[]>; uniqueIndexes?: Array<string | string[]> }\n\t>,\n): Record<string, StorageCollection> {\n\tconst accessor: Record<string, StorageCollection> = {};\n\n\tfor (const [collectionName, config] of Object.entries(storageConfig)) {\n\t\tconst allIndexes = [...config.indexes, ...(config.uniqueIndexes ?? [])];\n\t\taccessor[collectionName] = new PluginStorageRepository(\n\t\t\tdb,\n\t\t\tpluginId,\n\t\t\tcollectionName,\n\t\t\tallIndexes,\n\t\t);\n\t}\n\n\treturn accessor;\n}\n\n/**\n * Delete all storage data for a plugin\n */\nexport async function deleteAllPluginStorage(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n): Promise<number> {\n\tconst result = await db\n\t\t.deleteFrom(\"_plugin_storage\")\n\t\t.where(\"plugin_id\", \"=\", pluginId)\n\t\t.executeTakeFirst();\n\n\treturn Number(result.numDeletedRows ?? 0);\n}\n\n/**\n * Delete all storage data for a plugin collection\n */\nexport async function deletePluginCollection(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n\tcollection: string,\n): Promise<number> {\n\tconst result = await db\n\t\t.deleteFrom(\"_plugin_storage\")\n\t\t.where(\"plugin_id\", \"=\", pluginId)\n\t\t.where(\"collection\", \"=\", collection)\n\t\t.executeTakeFirst();\n\n\treturn Number(result.numDeletedRows ?? 0);\n}\n","import { z } from \"astro/zod\";\n\nimport type { FieldDefinition, ImageValue } from \"./types.js\";\n\n/**\n * Image field schema\n */\nconst imageSchema = z.object({\n\tid: z.string(),\n\tsrc: z.string(),\n\talt: z.string().optional(),\n\twidth: z.number().optional(),\n\theight: z.number().optional(),\n});\n\n/**\n * Image field\n * References media items from the media library\n */\nexport function image(options?: {\n\trequired?: boolean;\n\tmaxSize?: number; // in bytes\n\tallowedTypes?: string[]; // MIME types\n}): FieldDefinition<ImageValue | undefined> {\n\treturn {\n\t\ttype: \"image\",\n\t\tcolumnType: \"TEXT\",\n\t\tschema: options?.required === false ? imageSchema.optional() : imageSchema,\n\t\toptions,\n\t\tui: {\n\t\t\twidget: \"image\",\n\t\t},\n\t};\n}\n","import { z } from \"astro/zod\";\n\nimport type { FieldDefinition } from \"./types.js\";\n\n/**\n * Reference field\n * References another content item by ID\n */\nexport function reference(\n\tcollection: string,\n\toptions?: {\n\t\trequired?: boolean;\n\t},\n): FieldDefinition<string | undefined> {\n\tconst schema = z.string();\n\n\treturn {\n\t\ttype: \"reference\",\n\t\tcolumnType: \"TEXT\",\n\t\tschema: options?.required === false ? schema.optional() : schema,\n\t\toptions: {\n\t\t\t...options,\n\t\t\tcollection,\n\t\t},\n\t\tui: {\n\t\t\twidget: \"reference\",\n\t\t},\n\t};\n}\n","import { z } from \"astro/zod\";\n\nimport type { FieldDefinition, PortableTextBlock } from \"./types.js\";\n\n/**\n * Portable Text block schema\n */\nconst portableTextBlockSchema: z.ZodType<PortableTextBlock> = z\n\t.object({\n\t\t_type: z.string(),\n\t\t_key: z.string(),\n\t})\n\t.passthrough();\n\n/**\n * Portable Text field\n * Stores structured content in Portable Text format\n */\nexport function portableText(options?: {\n\trequired?: boolean;\n}): FieldDefinition<PortableTextBlock[] | undefined> {\n\tconst schema = z.array(portableTextBlockSchema);\n\n\treturn {\n\t\ttype: \"portableText\",\n\t\tcolumnType: \"JSON\",\n\t\tschema: options?.required === false ? schema.optional() : schema,\n\t\toptions,\n\t\tui: {\n\t\t\twidget: \"portableText\",\n\t\t},\n\t};\n}\n","import { sql, type Kysely } from \"kysely\";\n\nimport { chunks, SQL_BATCH_SIZE } from \"../../utils/chunks.js\";\nimport type { Database } from \"../types.js\";\nimport type { ContentSeo, ContentSeoInput } from \"./types.js\";\n\n/** Default SEO values for content without an explicit SEO row */\nconst SEO_DEFAULTS: ContentSeo = {\n\ttitle: null,\n\tdescription: null,\n\timage: null,\n\tcanonical: null,\n\tnoIndex: false,\n};\n\n/**\n * Returns true if the input has at least one explicitly-set SEO field.\n * Used to skip no-op upserts when callers pass `{ seo: {} }`.\n */\nfunction hasAnyField(input: ContentSeoInput): boolean {\n\treturn (\n\t\tinput.title !== undefined ||\n\t\tinput.description !== undefined ||\n\t\tinput.image !== undefined ||\n\t\tinput.canonical !== undefined ||\n\t\tinput.noIndex !== undefined\n\t);\n}\n\n/**\n * Repository for SEO metadata stored in `_emdash_seo`.\n *\n * SEO data lives in a separate table keyed by (collection, content_id).\n * Only collections with `has_seo = 1` should use this — callers are\n * responsible for checking the flag before reading/writing.\n */\nexport class SeoRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Check whether a collection has SEO enabled (`has_seo = 1`).\n\t * Returns `false` if the collection does not exist.\n\t */\n\tasync isEnabled(collection: string): Promise<boolean> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.select(\"has_seo\")\n\t\t\t.where(\"slug\", \"=\", collection)\n\t\t\t.executeTakeFirst();\n\t\treturn row?.has_seo === 1;\n\t}\n\n\t/**\n\t * Get SEO data for a content item. Returns null defaults if no row exists.\n\t */\n\tasync get(collection: string, contentId: string): Promise<ContentSeo> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_seo\")\n\t\t\t.selectAll()\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!row) {\n\t\t\treturn { ...SEO_DEFAULTS };\n\t\t}\n\n\t\treturn {\n\t\t\ttitle: row.seo_title ?? null,\n\t\t\tdescription: row.seo_description ?? null,\n\t\t\timage: row.seo_image ?? null,\n\t\t\tcanonical: row.seo_canonical ?? null,\n\t\t\tnoIndex: row.seo_no_index === 1,\n\t\t};\n\t}\n\n\t/**\n\t * Get SEO data for multiple content items.\n\t * Returns a Map keyed by content_id. Items without SEO rows get defaults.\n\t *\n\t * Chunks the `content_id IN (…)` clause so the total bound-parameter count\n\t * per statement (ids + the `collection = ?` filter) stays within Cloudflare\n\t * D1's 100-variable limit regardless of how many content items are passed.\n\t */\n\tasync getMany(collection: string, contentIds: string[]): Promise<Map<string, ContentSeo>> {\n\t\tconst result = new Map<string, ContentSeo>();\n\n\t\tif (contentIds.length === 0) return result;\n\n\t\t// Pre-fill with defaults so every input id has an entry even if no row exists.\n\t\tfor (const id of contentIds) {\n\t\t\tresult.set(id, { ...SEO_DEFAULTS });\n\t\t}\n\n\t\tconst uniqueContentIds = [...new Set(contentIds)];\n\t\tfor (const chunk of chunks(uniqueContentIds, SQL_BATCH_SIZE)) {\n\t\t\tconst rows = await this.db\n\t\t\t\t.selectFrom(\"_emdash_seo\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t\t.where(\"content_id\", \"in\", chunk)\n\t\t\t\t.execute();\n\n\t\t\tfor (const row of rows) {\n\t\t\t\tresult.set(row.content_id, {\n\t\t\t\t\ttitle: row.seo_title ?? null,\n\t\t\t\t\tdescription: row.seo_description ?? null,\n\t\t\t\t\timage: row.seo_image ?? null,\n\t\t\t\t\tcanonical: row.seo_canonical ?? null,\n\t\t\t\t\tnoIndex: row.seo_no_index === 1,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Upsert SEO data for a content item using INSERT ON CONFLICT DO UPDATE\n\t * for atomicity. Skips no-op writes when input has no fields set.\n\t */\n\tasync upsert(collection: string, contentId: string, input: ContentSeoInput): Promise<ContentSeo> {\n\t\t// Skip no-op: empty input (e.g., `{ seo: {} }` from form libs)\n\t\tif (!hasAnyField(input)) {\n\t\t\treturn this.get(collection, contentId);\n\t\t}\n\n\t\tconst now = new Date().toISOString();\n\n\t\t// Use INSERT ON CONFLICT for atomic upsert — avoids TOCTOU race\n\t\t// where two concurrent requests both see \"no row\" and both try INSERT.\n\t\t//\n\t\t// On conflict, we use COALESCE(excluded.col, current.col) so that\n\t\t// only explicitly-provided fields overwrite existing values.\n\t\tawait sql`\n\t\t\tINSERT INTO _emdash_seo (\n\t\t\t\tcollection, content_id,\n\t\t\t\tseo_title, seo_description, seo_image, seo_canonical, seo_no_index,\n\t\t\t\tcreated_at, updated_at\n\t\t\t) VALUES (\n\t\t\t\t${collection}, ${contentId},\n\t\t\t\t${input.title ?? null}, ${input.description ?? null},\n\t\t\t\t${input.image ?? null}, ${input.canonical ?? null},\n\t\t\t\t${input.noIndex ? 1 : 0},\n\t\t\t\t${now}, ${now}\n\t\t\t)\n\t\t\tON CONFLICT (collection, content_id) DO UPDATE SET\n\t\t\t\tseo_title = ${input.title !== undefined ? sql`${input.title}` : sql`_emdash_seo.seo_title`},\n\t\t\t\tseo_description = ${input.description !== undefined ? sql`${input.description}` : sql`_emdash_seo.seo_description`},\n\t\t\t\tseo_image = ${input.image !== undefined ? sql`${input.image}` : sql`_emdash_seo.seo_image`},\n\t\t\t\tseo_canonical = ${input.canonical !== undefined ? sql`${input.canonical}` : sql`_emdash_seo.seo_canonical`},\n\t\t\t\tseo_no_index = ${input.noIndex !== undefined ? sql`${input.noIndex ? 1 : 0}` : sql`_emdash_seo.seo_no_index`},\n\t\t\t\tupdated_at = ${now}\n\t\t`.execute(this.db);\n\n\t\treturn this.get(collection, contentId);\n\t}\n\n\t/**\n\t * Delete SEO data for a content item.\n\t */\n\tasync delete(collection: string, contentId: string): Promise<void> {\n\t\tawait this.db\n\t\t\t.deleteFrom(\"_emdash_seo\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId)\n\t\t\t.execute();\n\t}\n\n\t/**\n\t * Copy SEO data from one content item to another.\n\t * Used by duplicate. Clears canonical (it pointed to the original).\n\t */\n\tasync copyForDuplicate(collection: string, sourceId: string, targetId: string): Promise<void> {\n\t\tconst source = await this.get(collection, sourceId);\n\n\t\t// Only write if there's actual SEO data worth copying\n\t\tif (\n\t\t\tsource.title !== null ||\n\t\t\tsource.description !== null ||\n\t\t\tsource.image !== null ||\n\t\t\tsource.noIndex\n\t\t) {\n\t\t\tawait this.upsert(collection, targetId, {\n\t\t\t\ttitle: source.title,\n\t\t\t\tdescription: source.description,\n\t\t\t\timage: source.image,\n\t\t\t\tcanonical: null, // Don't copy canonical — it pointed to the original\n\t\t\t\tnoIndex: source.noIndex,\n\t\t\t});\n\t\t}\n\t}\n}\n","/**\n * Opaque _rev token generation and validation.\n *\n * Format: base64(\"version:updated_at\")\n * Stateless — server decodes and checks both components.\n *\n * Rules:\n * - No _rev sent → blind write (backwards-compatible)\n * - _rev matches → write proceeds, new _rev returned\n * - _rev mismatch → 409 Conflict\n */\n\nimport type { ContentItem } from \"../database/repositories/types.js\";\nimport { encodeBase64, decodeBase64 } from \"../utils/base64.js\";\n\n/**\n * Generate a _rev token from a content item's version and updatedAt.\n */\nexport function encodeRev(item: ContentItem): string {\n\treturn encodeBase64(`${item.version}:${item.updatedAt}`);\n}\n\n/**\n * Decode a _rev token into its components.\n * Returns null if the token is malformed.\n */\nexport function decodeRev(rev: string): { version: number; updatedAt: string } | null {\n\ttry {\n\t\tconst decoded = decodeBase64(rev);\n\t\tconst colonIdx = decoded.indexOf(\":\");\n\t\tif (colonIdx === -1) return null;\n\n\t\tconst version = parseInt(decoded.slice(0, colonIdx), 10);\n\t\tconst updatedAt = decoded.slice(colonIdx + 1);\n\n\t\tif (isNaN(version) || !updatedAt) return null;\n\t\treturn { version, updatedAt };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/**\n * Validate a _rev token against a content item.\n * Returns null if valid (or if no _rev provided), or an error message if invalid.\n */\nexport function validateRev(\n\trev: string | undefined,\n\titem: ContentItem,\n): { valid: true } | { valid: false; message: string } {\n\t// No _rev = blind write (backwards-compatible)\n\tif (!rev) return { valid: true };\n\n\tconst decoded = decodeRev(rev);\n\tif (!decoded) {\n\t\treturn { valid: false, message: \"Malformed _rev token\" };\n\t}\n\n\tif (decoded.version !== item.version || decoded.updatedAt !== item.updatedAt) {\n\t\treturn {\n\t\t\tvalid: false,\n\t\t\tmessage: \"Content has been modified since last read (version conflict)\",\n\t\t};\n\t}\n\n\treturn { valid: true };\n}\n","/**\n * Content CRUD handlers\n */\n\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\n\nimport { BylineRepository } from \"../../database/repositories/byline.js\";\nimport type { ContentBylineInput } from \"../../database/repositories/byline.js\";\nimport { CommentRepository } from \"../../database/repositories/comment.js\";\nimport { ContentRepository } from \"../../database/repositories/content.js\";\nimport { RedirectRepository } from \"../../database/repositories/redirect.js\";\nimport { RevisionRepository } from \"../../database/repositories/revision.js\";\nimport { SeoRepository } from \"../../database/repositories/seo.js\";\nimport {\n\tEmDashValidationError,\n\ttype ContentItem,\n\ttype ContentSeo,\n\ttype ContentSeoInput,\n} from \"../../database/repositories/types.js\";\nimport { withTransaction } from \"../../database/transaction.js\";\nimport type { Database } from \"../../database/types.js\";\nimport { validateIdentifier } from \"../../database/validate.js\";\nimport { isI18nEnabled } from \"../../i18n/config.js\";\nimport { invalidateRedirectCache } from \"../../redirects/cache.js\";\nimport { encodeRev, validateRev } from \"../rev.js\";\nimport type { ApiResult, ContentListResponse, ContentResponse } from \"../types.js\";\n\n/**\n * Extract a slug source (title or name) from content data.\n * Returns null if no suitable string field is found.\n */\nfunction getSlugSource(data: Record<string, unknown>): string | null {\n\tif (typeof data.title === \"string\" && data.title.length > 0) return data.title;\n\tif (typeof data.name === \"string\" && data.name.length > 0) return data.name;\n\treturn null;\n}\n\n/** Default SEO values for content without an explicit SEO row */\nconst SEO_DEFAULTS: ContentSeo = {\n\ttitle: null,\n\tdescription: null,\n\timage: null,\n\tcanonical: null,\n\tnoIndex: false,\n};\n\n/**\n * Check if a collection has SEO enabled.\n */\nasync function collectionHasSeo(db: Kysely<Database>, collection: string): Promise<boolean> {\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_collections\")\n\t\t.select(\"has_seo\")\n\t\t.where(\"slug\", \"=\", collection)\n\t\t.executeTakeFirst();\n\treturn row?.has_seo === 1;\n}\n\n/**\n * Hydrate SEO data on a single content item if the collection has SEO enabled.\n */\nasync function hydrateSeo(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\titem: ContentItem,\n\thasSeo: boolean,\n): Promise<void> {\n\tif (!hasSeo) return;\n\tconst seoRepo = new SeoRepository(db);\n\titem.seo = await seoRepo.get(collection, item.id);\n}\n\n/**\n * Hydrate SEO data on multiple content items using a single batch query.\n */\nasync function hydrateSeoMany(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\titems: ContentItem[],\n\thasSeo: boolean,\n): Promise<void> {\n\tif (!hasSeo || items.length === 0) return;\n\tconst seoRepo = new SeoRepository(db);\n\tconst seoMap = await seoRepo.getMany(\n\t\tcollection,\n\t\titems.map((i) => i.id),\n\t);\n\tfor (const item of items) {\n\t\titem.seo = seoMap.get(item.id) ?? { ...SEO_DEFAULTS };\n\t}\n}\n\nasync function hydrateBylines(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\titem: ContentItem,\n): Promise<void> {\n\tconst bylineRepo = new BylineRepository(db);\n\tconst bylines = await bylineRepo.getContentBylines(collection, item.id);\n\n\tif (bylines.length > 0) {\n\t\titem.bylines = bylines.map((c) => ({ ...c, source: \"explicit\" as const }));\n\t\titem.byline = bylines[0]?.byline ?? null;\n\t\treturn;\n\t}\n\n\t// Defensive: if primaryBylineId is set but no junction rows exist, it's orphaned\n\tif (item.primaryBylineId) {\n\t\titem.primaryBylineId = null;\n\t}\n\n\tif (item.authorId) {\n\t\tconst fallback = await bylineRepo.findByUserId(item.authorId);\n\t\tif (fallback) {\n\t\t\titem.bylines = [{ byline: fallback, sortOrder: 0, roleLabel: null, source: \"inferred\" }];\n\t\t\titem.byline = fallback;\n\t\t\treturn;\n\t\t}\n\t}\n\n\titem.bylines = [];\n\titem.byline = null;\n}\n\n/**\n * Batch-hydrate bylines for multiple items using two bulk queries instead of N+1.\n */\nasync function hydrateBylinesMany(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\titems: ContentItem[],\n): Promise<void> {\n\tif (items.length === 0) return;\n\n\tconst bylineRepo = new BylineRepository(db);\n\n\t// 1. Batch fetch all explicit byline credits\n\tconst contentIds = items.map((i) => i.id);\n\tconst bylinesMap = await bylineRepo.getContentBylinesMany(collection, contentIds);\n\n\t// 2. Collect authorIds that need fallback lookup\n\tconst fallbackAuthorIds: string[] = [];\n\tfor (const item of items) {\n\t\tif (!bylinesMap.has(item.id) && item.authorId) {\n\t\t\tfallbackAuthorIds.push(item.authorId);\n\t\t}\n\t}\n\n\t// 3. Batch fetch user-linked bylines for fallback\n\tconst uniqueAuthorIds = [...new Set(fallbackAuthorIds)];\n\tconst authorBylineMap = await bylineRepo.findByUserIds(uniqueAuthorIds);\n\n\t// 4. Assign to each item\n\tfor (const item of items) {\n\t\tconst explicit = bylinesMap.get(item.id);\n\t\tif (explicit && explicit.length > 0) {\n\t\t\titem.bylines = explicit.map((c) => ({ ...c, source: \"explicit\" as const }));\n\t\t\titem.byline = explicit[0]?.byline ?? null;\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Defensive: if primaryBylineId is set but no junction rows exist, it's orphaned\n\t\tif (item.primaryBylineId) {\n\t\t\titem.primaryBylineId = null;\n\t\t}\n\n\t\tif (item.authorId) {\n\t\t\tconst fallback = authorBylineMap.get(item.authorId);\n\t\t\tif (fallback) {\n\t\t\t\titem.bylines = [{ byline: fallback, sortOrder: 0, roleLabel: null, source: \"inferred\" }];\n\t\t\t\titem.byline = fallback;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\titem.bylines = [];\n\t\titem.byline = null;\n\t}\n}\n\n/**\n * Resolve an identifier (ID or slug) to a real content ID.\n * Returns the ID if found, null if not found.\n * When locale is provided, slug lookups are scoped to that locale.\n */\nasync function resolveId(\n\trepo: ContentRepository,\n\tcollection: string,\n\tidentifier: string,\n\tlocale?: string,\n): Promise<string | null> {\n\tconst item = await repo.findByIdOrSlug(collection, identifier, locale);\n\treturn item?.id ?? null;\n}\n\n/**\n * Resolve an identifier (ID or slug) to a real content ID,\n * including trashed (soft-deleted) items.\n */\nasync function resolveIdIncludingTrashed(\n\trepo: ContentRepository,\n\tcollection: string,\n\tidentifier: string,\n\tlocale?: string,\n): Promise<string | null> {\n\tconst item = await repo.findByIdOrSlugIncludingTrashed(collection, identifier, locale);\n\treturn item?.id ?? null;\n}\n\n/**\n * Trashed content item with deletion timestamp\n */\nexport interface TrashedContentItem {\n\tid: string;\n\ttype: string;\n\tslug: string | null;\n\tstatus: string;\n\tdata: Record<string, unknown>;\n\tauthorId: string | null;\n\tcreatedAt: string;\n\tupdatedAt: string;\n\tpublishedAt: string | null;\n\tdeletedAt: string;\n}\n\n/**\n * Create content list handler\n */\nexport async function handleContentList(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tparams: {\n\t\tcursor?: string;\n\t\tlimit?: number;\n\t\tstatus?: string;\n\t\torderBy?: string;\n\t\torder?: \"asc\" | \"desc\";\n\t\tlocale?: string;\n\t},\n): Promise<ApiResult<ContentListResponse>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst where: { status?: string; locale?: string } = {};\n\t\tif (params.status) where.status = params.status;\n\t\tif (params.locale) where.locale = params.locale;\n\n\t\tconst result = await repo.findMany(collection, {\n\t\t\tcursor: params.cursor,\n\t\t\tlimit: params.limit || 50,\n\t\t\twhere: Object.keys(where).length > 0 ? where : undefined,\n\t\t\torderBy: params.orderBy\n\t\t\t\t? { field: params.orderBy, direction: params.order || \"desc\" }\n\t\t\t\t: undefined,\n\t\t});\n\n\t\t// Hydrate SEO data if the collection has SEO enabled\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeoMany(db, collection, result.items, hasSeo);\n\t\tawait hydrateBylinesMany(db, collection, result.items);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: result.items,\n\t\t\t\tnextCursor: result.nextCursor,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content list error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get single content item\n */\nexport async function handleContentGet(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\tlocale?: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst item = await repo.findByIdOrSlug(collection, id, locale);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Hydrate SEO data if the collection has SEO enabled\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\t\tawait hydrateBylines(db, collection, item);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item, _rev: encodeRev(item) },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content get error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get a content item by id, including trashed items.\n * Used by restore endpoint for ownership checks on soft-deleted items.\n */\nexport async function handleContentGetIncludingTrashed(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\tlocale?: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst item = await repo.findByIdOrSlugIncludingTrashed(collection, id, locale);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Hydrate SEO data if the collection has SEO enabled\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\t\tawait hydrateBylines(db, collection, item);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item, _rev: encodeRev(item) },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content get error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Create content item.\n *\n * Content + SEO writes are wrapped in a transaction so either both succeed\n * or neither does. If `body.seo` is provided for a non-SEO collection, the\n * API returns a validation error rather than silently dropping it.\n */\nexport async function handleContentCreate(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tbody: {\n\t\tdata: Record<string, unknown>;\n\t\tslug?: string;\n\t\tstatus?: string;\n\t\tauthorId?: string;\n\t\tbylines?: ContentBylineInput[];\n\t\tlocale?: string;\n\t\ttranslationOf?: string;\n\t\tseo?: ContentSeoInput;\n\t\tcreatedAt?: string | null;\n\t\tpublishedAt?: string | null;\n\t},\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\n\t\t// Reject SEO input for non-SEO collections\n\t\tif (body.seo && !hasSeo) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: `Collection \"${collection}\" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Wrap content + SEO writes in a transaction for atomicity\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst bylineRepo = new BylineRepository(trx);\n\n\t\t\t// Auto-generate slug from title/name if not explicitly provided\n\t\t\tlet slug: string | null | undefined = body.slug;\n\t\t\tif (!slug) {\n\t\t\t\tconst slugSource = getSlugSource(body.data);\n\t\t\t\tif (slugSource) {\n\t\t\t\t\tslug = await repo.generateUniqueSlug(collection, slugSource, body.locale);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst created = await repo.create({\n\t\t\t\ttype: collection,\n\t\t\t\tslug,\n\t\t\t\tdata: body.data,\n\t\t\t\tstatus: body.status || \"draft\",\n\t\t\t\tauthorId: body.authorId,\n\t\t\t\tlocale: body.locale,\n\t\t\t\ttranslationOf: body.translationOf,\n\t\t\t\tcreatedAt: body.createdAt,\n\t\t\t\tpublishedAt: body.publishedAt,\n\t\t\t});\n\n\t\t\tif (body.bylines !== undefined) {\n\t\t\t\tawait bylineRepo.setContentBylines(collection, created.id, body.bylines);\n\t\t\t\tcreated.primaryBylineId = body.bylines[0]?.bylineId ?? null;\n\t\t\t}\n\t\t\tawait hydrateBylines(trx, collection, created);\n\n\t\t\t// Side-write SEO data if provided\n\t\t\tif (body.seo && hasSeo) {\n\t\t\t\tconst seoRepo = new SeoRepository(trx);\n\t\t\t\tcreated.seo = await seoRepo.upsert(collection, created.id, body.seo);\n\t\t\t} else if (hasSeo) {\n\t\t\t\t// Assign defaults in-memory — no DB round-trip needed\n\t\t\t\tcreated.seo = { ...SEO_DEFAULTS };\n\t\t\t}\n\n\t\t\treturn created;\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item, _rev: encodeRev(item) },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content create error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_CREATE_ERROR\",\n\t\t\t\tmessage: \"Failed to create content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Update content item.\n * If `_rev` is provided, validates it against the current version before writing.\n * No `_rev` = blind write (backwards-compatible for admin UI).\n *\n * Content + SEO writes are wrapped in a transaction for atomicity.\n */\nexport async function handleContentUpdate(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\tbody: {\n\t\tdata?: Record<string, unknown>;\n\t\tslug?: string;\n\t\tstatus?: string;\n\t\tauthorId?: string | null;\n\t\tbylines?: ContentBylineInput[];\n\t\t_rev?: string;\n\t\tseo?: ContentSeoInput;\n\t},\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\n\t\t// Reject SEO input for non-SEO collections\n\t\tif (body.seo && !hasSeo) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: `Collection \"${collection}\" does not have SEO enabled. Remove the seo field or enable SEO on this collection.`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst repo = new ContentRepository(db);\n\n\t\t// Resolve slug → ID if needed\n\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\n\t\t// Wrap content + SEO writes in a transaction for atomicity.\n\t\t// The _rev check is inside the transaction so the read-then-write\n\t\t// is atomic -- no concurrent write can slip between the check and update.\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst trxRepo = new ContentRepository(trx);\n\t\t\tconst bylineRepo = new BylineRepository(trx);\n\n\t\t\t// Read existing item once for both _rev check and old slug capture\n\t\t\tconst existing =\n\t\t\t\tbody._rev || body.slug ? await trxRepo.findById(collection, resolvedId) : null;\n\n\t\t\t// Validate _rev if provided (optimistic concurrency)\n\t\t\tif (body._rev) {\n\t\t\t\tif (!existing) {\n\t\t\t\t\tthrow Object.assign(new Error(`Content item not found: ${id}`), {\n\t\t\t\t\t\tapiError: { code: \"NOT_FOUND\" as const },\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tconst revCheck = validateRev(body._rev, existing);\n\t\t\t\tif (!revCheck.valid) {\n\t\t\t\t\tthrow Object.assign(new Error(revCheck.message), {\n\t\t\t\t\t\tapiError: { code: \"CONFLICT\" as const },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Capture old slug before update for auto-redirect\n\t\t\tlet oldSlug: string | undefined;\n\t\t\tif (body.slug && existing?.slug && existing.slug !== body.slug) {\n\t\t\t\toldSlug = existing.slug;\n\t\t\t}\n\n\t\t\tconst updated = await trxRepo.update(collection, resolvedId, {\n\t\t\t\tdata: body.data,\n\t\t\t\tslug: body.slug,\n\t\t\t\tstatus: body.status,\n\t\t\t\tauthorId: body.authorId,\n\t\t\t});\n\n\t\t\tif (body.bylines !== undefined) {\n\t\t\t\tawait bylineRepo.setContentBylines(collection, resolvedId, body.bylines);\n\t\t\t\tupdated.primaryBylineId = body.bylines[0]?.bylineId ?? null;\n\t\t\t}\n\n\t\t\t// Create auto-redirect when slug changes\n\t\t\tif (oldSlug && body.slug) {\n\t\t\t\tconst collectionRow = await trx\n\t\t\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t\t\t.select(\"url_pattern\")\n\t\t\t\t\t.where(\"slug\", \"=\", collection)\n\t\t\t\t\t.executeTakeFirst();\n\n\t\t\t\tconst redirectRepo = new RedirectRepository(trx);\n\t\t\t\tawait redirectRepo.createAutoRedirect(\n\t\t\t\t\tcollection,\n\t\t\t\t\toldSlug,\n\t\t\t\t\tbody.slug,\n\t\t\t\t\tresolvedId,\n\t\t\t\t\tcollectionRow?.url_pattern ?? null,\n\t\t\t\t);\n\t\t\t\tinvalidateRedirectCache();\n\t\t\t}\n\n\t\t\t// Sync non-translatable fields to sibling locales in the same\n\t\t\t// translation group. Only runs when i18n is enabled, data was updated,\n\t\t\t// and the item belongs to a translation group with siblings.\n\t\t\tif (isI18nEnabled() && body.data && updated.translationGroup) {\n\t\t\t\tawait syncNonTranslatableFields(\n\t\t\t\t\ttrx,\n\t\t\t\t\tcollection,\n\t\t\t\t\tupdated.id,\n\t\t\t\t\tupdated.translationGroup,\n\t\t\t\t\tbody.data,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Side-write SEO data if provided, always hydrate for SEO-enabled collections\n\t\t\tif (body.seo && hasSeo) {\n\t\t\t\tconst seoRepo = new SeoRepository(trx);\n\t\t\t\tupdated.seo = await seoRepo.upsert(collection, resolvedId, body.seo);\n\t\t\t} else if (hasSeo) {\n\t\t\t\tconst seoRepo = new SeoRepository(trx);\n\t\t\t\tupdated.seo = await seoRepo.get(collection, resolvedId);\n\t\t\t}\n\n\t\t\tawait hydrateBylines(trx, collection, updated);\n\n\t\t\treturn updated;\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item, _rev: encodeRev(item) },\n\t\t};\n\t} catch (error) {\n\t\t// Handle structured errors thrown from inside the transaction\n\t\t// (rev check failures, not-found)\n\t\tif (error instanceof Error && \"apiError\" in error) {\n\t\t\tconst { code } = (error as Error & { apiError: { code: string } }).apiError;\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code, message: error.message },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content update error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_UPDATE_ERROR\",\n\t\t\t\tmessage: \"Failed to update content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Duplicate content item.\n *\n * Only copies SEO data if the collection has SEO enabled.\n * Always returns consistent `seo` shape for SEO-enabled collections.\n */\nexport async function handleContentDuplicate(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\tauthorId?: string,\n): Promise<ApiResult<{ item: ContentItem }>> {\n\ttry {\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\n\t\t// Wrap duplicate + SEO copy in a transaction for atomicity\n\t\tconst duplicate = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst bylineRepo = new BylineRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\tconst dup = await repo.duplicate(collection, resolvedId, authorId);\n\n\t\t\tconst existingBylines = await bylineRepo.getContentBylines(collection, resolvedId);\n\t\t\tif (existingBylines.length > 0) {\n\t\t\t\tawait bylineRepo.setContentBylines(\n\t\t\t\t\tcollection,\n\t\t\t\t\tdup.id,\n\t\t\t\t\texistingBylines.map((entry) => ({\n\t\t\t\t\t\tbylineId: entry.byline.id,\n\t\t\t\t\t\troleLabel: entry.roleLabel,\n\t\t\t\t\t})),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (hasSeo) {\n\t\t\t\t// Copy SEO data from the original (clears canonical)\n\t\t\t\tconst seoRepo = new SeoRepository(trx);\n\t\t\t\tawait seoRepo.copyForDuplicate(collection, resolvedId, dup.id);\n\t\t\t\t// Always hydrate SEO for consistent response shape\n\t\t\t\tdup.seo = await seoRepo.get(collection, dup.id);\n\t\t\t}\n\n\t\t\tawait hydrateBylines(trx, collection, dup);\n\n\t\t\treturn dup;\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item: duplicate },\n\t\t};\n\t} catch (err) {\n\t\tif (err instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: err.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content duplicate error:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_DUPLICATE_ERROR\",\n\t\t\t\tmessage: \"Failed to duplicate content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Delete content item (soft delete - moves to trash)\n */\nexport async function handleContentDelete(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst deleted = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\treturn repo.delete(collection, resolvedId);\n\t\t});\n\n\t\tif (!deleted) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { deleted: true },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content delete error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to delete content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Restore content item from trash\n */\nexport async function handleContentRestore(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<{ restored: true }>> {\n\ttry {\n\t\tconst restored = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveIdIncludingTrashed(repo, collection, id)) ?? id;\n\t\t\treturn repo.restore(collection, resolvedId);\n\t\t});\n\n\t\tif (!restored) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Trashed content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { restored: true },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content restore error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_RESTORE_ERROR\",\n\t\t\t\tmessage: \"Failed to restore content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Permanently delete content item (cannot be undone).\n * Also cleans up associated SEO data.\n */\nexport async function handleContentPermanentDelete(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst resolvedId = (await resolveIdIncludingTrashed(repo, collection, id)) ?? id;\n\n\t\t// Wrap content delete + SEO/comment cleanup in a transaction\n\t\tconst deleted = await withTransaction(db, async (trx) => {\n\t\t\tconst trxRepo = new ContentRepository(trx);\n\t\t\tconst wasDeleted = await trxRepo.permanentDelete(collection, resolvedId);\n\n\t\t\tif (wasDeleted) {\n\t\t\t\t// Clean up SEO data for permanently deleted content\n\t\t\t\tconst seoRepo = new SeoRepository(trx);\n\t\t\t\tawait seoRepo.delete(collection, resolvedId);\n\t\t\t\t// Clean up comments for permanently deleted content\n\t\t\t\tconst commentRepo = new CommentRepository(trx);\n\t\t\t\tawait commentRepo.deleteByContent(collection, resolvedId);\n\t\t\t\t// Clean up revisions for permanently deleted content\n\t\t\t\tconst revisionRepo = new RevisionRepository(trx);\n\t\t\t\tawait revisionRepo.deleteByEntry(collection, resolvedId);\n\t\t\t}\n\n\t\t\treturn wasDeleted;\n\t\t});\n\n\t\tif (!deleted) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { deleted: true },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content permanent delete error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to permanently delete content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * List trashed content items\n */\nexport async function handleContentListTrashed(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\toptions: { limit?: number; cursor?: string } = {},\n): Promise<ApiResult<{ items: TrashedContentItem[]; nextCursor?: string }>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst result = await repo.findTrashed(collection, {\n\t\t\tlimit: options.limit,\n\t\t\tcursor: options.cursor,\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: result.items.map((item) => ({\n\t\t\t\t\tid: item.id,\n\t\t\t\t\ttype: item.type,\n\t\t\t\t\tslug: item.slug,\n\t\t\t\t\tstatus: item.status,\n\t\t\t\t\tdata: item.data,\n\t\t\t\t\tauthorId: item.authorId,\n\t\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t\t\tupdatedAt: item.updatedAt,\n\t\t\t\t\tpublishedAt: item.publishedAt,\n\t\t\t\t\tdeletedAt: item.deletedAt,\n\t\t\t\t})),\n\t\t\t\tnextCursor: result.nextCursor,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content list trashed error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list trashed content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Count trashed content items\n */\nexport async function handleContentCountTrashed(\n\tdb: Kysely<Database>,\n\tcollection: string,\n): Promise<ApiResult<{ count: number }>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst count = await repo.countTrashed(collection);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { count },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content count trashed error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_COUNT_ERROR\",\n\t\t\t\tmessage: \"Failed to count trashed content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Schedule content for future publishing\n */\nexport async function handleContentSchedule(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\tscheduledAt: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\treturn repo.schedule(collection, resolvedId, scheduledAt);\n\t\t});\n\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content schedule error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_SCHEDULE_ERROR\",\n\t\t\t\tmessage: \"Failed to schedule content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Unschedule content (revert to draft)\n */\nexport async function handleContentUnschedule(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\treturn repo.unschedule(collection, resolvedId);\n\t\t});\n\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content unschedule error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_UNSCHEDULE_ERROR\",\n\t\t\t\tmessage: \"Failed to unschedule content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Publish content immediately.\n *\n * Wrapped in a transaction because publish performs multiple writes\n * (syncDataColumns, slug sync, status/revision update) that must\n * be atomic to prevent FTS shadow table corruption on crash.\n */\nexport async function handleContentPublish(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\treturn repo.publish(collection, resolvedId);\n\t\t});\n\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content publish error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_PUBLISH_ERROR\",\n\t\t\t\tmessage: \"Failed to publish content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Unpublish content (revert to draft).\n *\n * Wrapped in a transaction — unpublish may create a draft revision\n * from the live version then update the status, which is multi-step.\n */\nexport async function handleContentUnpublish(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\treturn repo.unpublish(collection, resolvedId);\n\t\t});\n\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content unpublish error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_UNPUBLISH_ERROR\",\n\t\t\t\tmessage: \"Failed to unpublish content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Count scheduled content items\n */\nexport async function handleContentCountScheduled(\n\tdb: Kysely<Database>,\n\tcollection: string,\n): Promise<ApiResult<{ count: number }>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst count = await repo.countScheduled(collection);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { count },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content count scheduled error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_COUNT_ERROR\",\n\t\t\t\tmessage: \"Failed to count scheduled content\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Discard draft changes (revert to live version)\n */\nexport async function handleContentDiscardDraft(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst item = await withTransaction(db, async (trx) => {\n\t\t\tconst repo = new ContentRepository(trx);\n\t\t\tconst resolvedId = (await resolveId(repo, collection, id)) ?? id;\n\t\t\treturn repo.discardDraft(collection, resolvedId);\n\t\t});\n\n\t\tconst hasSeo = await collectionHasSeo(db, collection);\n\t\tawait hydrateSeo(db, collection, item, hasSeo);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content discard draft error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_DISCARD_DRAFT_ERROR\",\n\t\t\t\tmessage: \"Failed to discard draft\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Compare live and draft revisions\n */\nexport async function handleContentCompare(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<\n\tApiResult<{\n\t\thasChanges: boolean;\n\t\tlive: Record<string, unknown> | null;\n\t\tdraft: Record<string, unknown> | null;\n\t}>\n> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst entry = await repo.findByIdOrSlug(collection, id);\n\n\t\tif (!entry) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst revisionRepo = new RevisionRepository(db);\n\n\t\tconst live = entry.liveRevisionId ? await revisionRepo.findById(entry.liveRevisionId) : null;\n\t\tconst draft = entry.draftRevisionId ? await revisionRepo.findById(entry.draftRevisionId) : null;\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\thasChanges:\n\t\t\t\t\tentry.draftRevisionId !== null && entry.draftRevisionId !== entry.liveRevisionId,\n\t\t\t\tlive: live?.data ?? null,\n\t\t\t\tdraft: draft?.data ?? null,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Content compare error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_COMPARE_ERROR\",\n\t\t\t\tmessage: \"Failed to compare revisions\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get all translations for a content item.\n * Returns the item's translation group members with locale and status info.\n */\nexport async function handleContentTranslations(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n): Promise<\n\tApiResult<{\n\t\ttranslationGroup: string;\n\t\ttranslations: Array<{\n\t\t\tid: string;\n\t\t\tlocale: string | null;\n\t\t\tslug: string | null;\n\t\t\tstatus: string;\n\t\t\tupdatedAt: string;\n\t\t}>;\n\t}>\n> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst item = await repo.findByIdOrSlug(collection, id);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Content item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tif (!item.translationGroup) {\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: {\n\t\t\t\t\ttranslationGroup: item.id,\n\t\t\t\t\ttranslations: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tid: item.id,\n\t\t\t\t\t\t\tlocale: item.locale,\n\t\t\t\t\t\t\tslug: item.slug,\n\t\t\t\t\t\t\tstatus: item.status,\n\t\t\t\t\t\t\tupdatedAt: item.updatedAt,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst translations = await repo.findTranslations(collection, item.translationGroup);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\ttranslationGroup: item.translationGroup,\n\t\t\t\ttranslations: translations.map((t) => ({\n\t\t\t\t\tid: t.id,\n\t\t\t\t\tlocale: t.locale,\n\t\t\t\t\tslug: t.slug,\n\t\t\t\t\tstatus: t.status,\n\t\t\t\t\tupdatedAt: t.updatedAt,\n\t\t\t\t})),\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof Error) {\n\t\t\tconsole.error(\"Content translations error:\", error);\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_TRANSLATIONS_ERROR\",\n\t\t\t\tmessage: \"Failed to get translations\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Non-translatable field sync\n// ---------------------------------------------------------------------------\n\n/**\n * Sync non-translatable fields to sibling locales.\n *\n * When a content item is updated and it belongs to a translation group,\n * any non-translatable fields in the update data are written to all other\n * rows in the same translation group within the same transaction.\n *\n * Non-translatable fields are **copied, not linked** — each row owns its\n * own data. This keeps queries simple and avoids cross-row joins.\n */\nasync function syncNonTranslatableFields(\n\ttrx: Kysely<Database>,\n\tcollectionSlug: string,\n\tupdatedItemId: string,\n\ttranslationGroup: string,\n\tdata: Record<string, unknown>,\n): Promise<void> {\n\t// Get the collection to find its fields\n\tconst collection = await trx\n\t\t.selectFrom(\"_emdash_collections\")\n\t\t.select(\"id\")\n\t\t.where(\"slug\", \"=\", collectionSlug)\n\t\t.executeTakeFirst();\n\n\tif (!collection) return;\n\n\t// Find non-translatable fields that are present in the update data\n\tconst fields = await trx\n\t\t.selectFrom(\"_emdash_fields\")\n\t\t.select(\"slug\")\n\t\t.where(\"collection_id\", \"=\", collection.id)\n\t\t.where(\"translatable\", \"=\", 0)\n\t\t.execute();\n\n\tconst nonTranslatableSlugs = fields.map((f) => f.slug);\n\tif (nonTranslatableSlugs.length === 0) return;\n\n\t// Filter to only the non-translatable fields present in this update\n\tconst syncData: Record<string, unknown> = {};\n\tfor (const slug of nonTranslatableSlugs) {\n\t\tif (slug in data) {\n\t\t\tsyncData[slug] = data[slug];\n\t\t}\n\t}\n\tif (Object.keys(syncData).length === 0) return;\n\n\t// Build the SET clause for sibling rows\n\tvalidateIdentifier(collectionSlug, \"collection slug\");\n\tconst tableName = `ec_${collectionSlug}`;\n\n\t// Update all sibling rows (same translation_group, different id)\n\tconst setClauses = Object.entries(syncData).map(([key, value]) => {\n\t\tvalidateIdentifier(key, \"field slug\");\n\t\tconst serialized = typeof value === \"object\" && value !== null ? JSON.stringify(value) : value;\n\t\treturn sql`${sql.ref(key)} = ${serialized}`;\n\t});\n\n\tawait sql`\n\t\tUPDATE ${sql.ref(tableName)}\n\t\tSET ${sql.join(setClauses, sql`, `)}\n\t\tWHERE translation_group = ${translationGroup}\n\t\tAND id != ${updatedItemId}\n\t`.execute(trx);\n}\n","/**\n * SHA-256 hash of a string, truncated to 16 hex chars (64 bits).\n * For cache invalidation / ETags — not for security.\n */\nexport async function hashString(content: string): Promise<string> {\n\tconst buf = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(content));\n\treturn Array.from(new Uint8Array(buf).slice(0, 8), (b) => b.toString(16).padStart(2, \"0\")).join(\n\t\t\"\",\n\t);\n}\n\n/**\n * Compute content hash using Web Crypto API\n *\n * Uses SHA-1 which is the fastest option in SubtleCrypto.\n * SHA-1 is cryptographically weak but fine for content deduplication\n * where we only need to detect identical files, not resist attacks.\n *\n * Returns hex string prefixed with \"sha1:\" for future-proofing\n */\nexport async function computeContentHash(content: Uint8Array | ArrayBuffer): Promise<string> {\n\t// SubtleCrypto.digest() requires BufferSource (ArrayBuffer | ArrayBufferView<ArrayBuffer>).\n\t// Uint8Array.buffer is ArrayBufferLike which may include SharedArrayBuffer in the type system,\n\t// so we ensure we have a plain ArrayBuffer.\n\tlet buf: ArrayBuffer;\n\tif (content instanceof ArrayBuffer) {\n\t\tbuf = content;\n\t} else {\n\t\tbuf = new ArrayBuffer(content.byteLength);\n\t\tnew Uint8Array(buf).set(content);\n\t}\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-1\", buf);\n\tconst hashArray = new Uint8Array(hashBuffer);\n\tconst hashHex = Array.from(hashArray, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n\treturn `sha1:${hashHex}`;\n}\n","/**\n * Manifest generation handlers\n */\n\nimport { hashString } from \"../../utils/hash.js\";\nimport type { ManifestResponse, FieldDescriptor } from \"../types.js\";\n\n/** Pattern to add spaces before capital letters */\nconst CAMEL_CASE_PATTERN = /([A-Z])/g;\nconst FIRST_CHAR_PATTERN = /^./;\n\n// Collection definition shape for manifest generation\ninterface CollectionDefinition {\n\tschema: {\n\t\t_def?: { shape?: () => Record<string, unknown> };\n\t\tshape?: Record<string, unknown>;\n\t};\n\tadmin: {\n\t\tlabel: string;\n\t\tlabelSingular?: string;\n\t\tsupports?: string[];\n\t};\n}\ntype CollectionMap = Record<string, CollectionDefinition>;\n\n/**\n * Generate admin manifest from collections\n */\nexport async function generateManifest(\n\tcollections: CollectionMap,\n\tplugins: Record<\n\t\tstring,\n\t\t{\n\t\t\tadminPages?: Array<{ path: string; component: string }>;\n\t\t\twidgets?: string[];\n\t\t}\n\t> = {},\n): Promise<ManifestResponse> {\n\tconst manifestCollections: ManifestResponse[\"collections\"] = {};\n\n\tfor (const [name, definition] of Object.entries(collections)) {\n\t\t// Extract field descriptors from Zod schema\n\t\tconst fields = extractFieldDescriptors(definition.schema);\n\n\t\tmanifestCollections[name] = {\n\t\t\tlabel: definition.admin.label,\n\t\t\tlabelSingular: definition.admin.labelSingular || definition.admin.label,\n\t\t\tsupports: definition.admin.supports || [],\n\t\t\tfields,\n\t\t};\n\t}\n\n\t// Generate hash from collections (for cache invalidation)\n\tconst hash = await hashString(JSON.stringify(manifestCollections));\n\n\treturn {\n\t\tversion: \"0.1.0\",\n\t\thash,\n\t\tcollections: manifestCollections,\n\t\tplugins,\n\t};\n}\n\n/**\n * Extract field descriptors from Zod schema\n * Note: This is a simplified implementation that handles common types\n */\nfunction extractFieldDescriptors(schema: {\n\t_def?: { shape?: () => Record<string, unknown> };\n\tshape?: Record<string, unknown>;\n}): Record<string, FieldDescriptor> {\n\tconst fields: Record<string, FieldDescriptor> = {};\n\n\t// Handle Zod object schema\n\tconst shape = typeof schema._def?.shape === \"function\" ? schema._def.shape() : schema.shape || {};\n\n\tfor (const [name, fieldSchema] of Object.entries(shape)) {\n\t\tfields[name] = extractFieldType(name, fieldSchema);\n\t}\n\n\treturn fields;\n}\n\n/**\n * Extract field type from Zod schema\n */\n/** Type guard: check if a value is a non-null object */\nfunction isObject(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction extractFieldType(name: string, schema: unknown): FieldDescriptor {\n\tif (!isObject(schema)) {\n\t\treturn { kind: \"string\", label: formatLabel(name) };\n\t}\n\n\t// Check for custom field markers\n\tif (schema.isPortableText) {\n\t\treturn { kind: \"portableText\", label: formatLabel(name) };\n\t}\n\tif (schema.isImage) {\n\t\treturn { kind: \"image\", label: formatLabel(name) };\n\t}\n\tif (schema.isReference) {\n\t\treturn { kind: \"reference\", label: formatLabel(name) };\n\t}\n\n\t// Handle standard Zod types\n\tconst def = isObject(schema._def) ? schema._def : undefined;\n\tconst typeName = typeof def?.typeName === \"string\" ? def.typeName : undefined;\n\n\tswitch (typeName) {\n\t\tcase \"ZodString\":\n\t\t\treturn { kind: \"string\", label: formatLabel(name) };\n\t\tcase \"ZodNumber\":\n\t\t\treturn { kind: \"number\", label: formatLabel(name) };\n\t\tcase \"ZodBoolean\":\n\t\t\treturn { kind: \"boolean\", label: formatLabel(name) };\n\t\tcase \"ZodDate\":\n\t\t\treturn { kind: \"datetime\", label: formatLabel(name) };\n\t\tcase \"ZodEnum\": {\n\t\t\tconst values = Array.isArray(def?.values) ? def.values : [];\n\t\t\treturn {\n\t\t\t\tkind: \"select\",\n\t\t\t\tlabel: formatLabel(name),\n\t\t\t\toptions: values\n\t\t\t\t\t.filter((v): v is string => typeof v === \"string\")\n\t\t\t\t\t.map((v) => ({\n\t\t\t\t\t\tvalue: v,\n\t\t\t\t\t\tlabel: v.charAt(0).toUpperCase() + v.slice(1),\n\t\t\t\t\t})),\n\t\t\t};\n\t\t}\n\t\tcase \"ZodArray\":\n\t\t\treturn { kind: \"array\", label: formatLabel(name) };\n\t\tcase \"ZodObject\":\n\t\t\treturn { kind: \"object\", label: formatLabel(name) };\n\t\tcase \"ZodOptional\":\n\t\tcase \"ZodDefault\":\n\t\t\t// Unwrap optional/default types\n\t\t\tif (def?.innerType) {\n\t\t\t\treturn extractFieldType(name, def.innerType);\n\t\t\t}\n\t\t\treturn { kind: \"string\", label: formatLabel(name) };\n\t\tdefault:\n\t\t\treturn { kind: \"string\", label: formatLabel(name) };\n\t}\n}\n\n/**\n * Format field name as label\n */\nfunction formatLabel(name: string): string {\n\treturn name\n\t\t.replace(CAMEL_CASE_PATTERN, \" $1\")\n\t\t.replace(FIRST_CHAR_PATTERN, (str) => str.toUpperCase())\n\t\t.trim();\n}\n","/**\n * Revision history handlers\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { ContentRepository } from \"../../database/repositories/content.js\";\nimport { RevisionRepository, type Revision } from \"../../database/repositories/revision.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { ApiResult, ContentResponse } from \"../types.js\";\n\nexport interface RevisionListResponse {\n\titems: Revision[];\n\ttotal: number;\n}\n\nexport interface RevisionResponse {\n\titem: Revision;\n}\n\n/**\n * List revisions for a content entry\n */\nexport async function handleRevisionList(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tentryId: string,\n\tparams: { limit?: number } = {},\n): Promise<ApiResult<RevisionListResponse>> {\n\ttry {\n\t\tconst repo = new RevisionRepository(db);\n\t\tconst [items, total] = await Promise.all([\n\t\t\trepo.findByEntry(collection, entryId, { limit: Math.min(params.limit || 50, 100) }),\n\t\t\trepo.countByEntry(collection, entryId),\n\t\t]);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { items, total },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"REVISION_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list revisions\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get a specific revision\n */\nexport async function handleRevisionGet(\n\tdb: Kysely<Database>,\n\trevisionId: string,\n): Promise<ApiResult<RevisionResponse>> {\n\ttry {\n\t\tconst repo = new RevisionRepository(db);\n\t\tconst item = await repo.findById(revisionId);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Revision not found: ${revisionId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"REVISION_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get revision\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Restore a revision (updates content to this revision's data and creates new revision)\n */\nexport async function handleRevisionRestore(\n\tdb: Kysely<Database>,\n\trevisionId: string,\n\tcallerUserId: string,\n): Promise<ApiResult<ContentResponse>> {\n\ttry {\n\t\tconst revisionRepo = new RevisionRepository(db);\n\t\tconst contentRepo = new ContentRepository(db);\n\n\t\t// Get the revision\n\t\tconst revision = await revisionRepo.findById(revisionId);\n\t\tif (!revision) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Revision not found: ${revisionId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Extract _slug from revision data (stored as metadata, not a real column)\n\t\tconst { _slug, ...fieldData } = revision.data;\n\n\t\t// Update the content with the revision's data\n\t\tconst item = await contentRepo.update(revision.collection, revision.entryId, {\n\t\t\tdata: fieldData,\n\t\t\tslug: typeof _slug === \"string\" ? _slug : undefined,\n\t\t});\n\n\t\t// Create a new revision to record the restore, attributed to the caller\n\t\tawait revisionRepo.create({\n\t\t\tcollection: revision.collection,\n\t\t\tentryId: revision.entryId,\n\t\t\tdata: revision.data,\n\t\t\tauthorId: callerUserId,\n\t\t});\n\n\t\t// Fire-and-forget: prune old revisions to prevent unbounded growth\n\t\tvoid revisionRepo.pruneOldRevisions(revision.collection, revision.entryId, 50).catch(() => {});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"REVISION_RESTORE_ERROR\",\n\t\t\t\tmessage: \"Failed to restore revision\",\n\t\t\t},\n\t\t};\n\t}\n}\n","/**\n * Media CRUD handlers\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { MediaRepository, type MediaItem } from \"../../database/repositories/media.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\nexport interface MediaListResponse {\n\titems: MediaItem[];\n\tnextCursor?: string;\n}\n\nexport interface MediaResponse {\n\titem: MediaItem;\n}\n\n/**\n * List media items\n */\nexport async function handleMediaList(\n\tdb: Kysely<Database>,\n\tparams: {\n\t\tcursor?: string;\n\t\tlimit?: number;\n\t\tmimeType?: string;\n\t},\n): Promise<ApiResult<MediaListResponse>> {\n\ttry {\n\t\tconst repo = new MediaRepository(db);\n\t\tconst result = await repo.findMany({\n\t\t\tcursor: params.cursor,\n\t\t\tlimit: Math.min(params.limit || 50, 100),\n\t\t\tmimeType: params.mimeType,\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: result.items,\n\t\t\t\tnextCursor: result.nextCursor,\n\t\t\t},\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MEDIA_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list media\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get single media item\n */\nexport async function handleMediaGet(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<MediaResponse>> {\n\ttry {\n\t\tconst repo = new MediaRepository(db);\n\t\tconst item = await repo.findById(id);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Media item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MEDIA_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get media\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Create media item (after file upload)\n */\nexport async function handleMediaCreate(\n\tdb: Kysely<Database>,\n\tinput: {\n\t\tfilename: string;\n\t\tmimeType: string;\n\t\tsize?: number;\n\t\twidth?: number;\n\t\theight?: number;\n\t\talt?: string;\n\t\tstorageKey: string;\n\t\tcontentHash?: string;\n\t\tblurhash?: string;\n\t\tdominantColor?: string;\n\t\tauthorId?: string;\n\t},\n): Promise<ApiResult<MediaResponse>> {\n\ttry {\n\t\tconst repo = new MediaRepository(db);\n\t\tconst item = await repo.create(input);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MEDIA_CREATE_ERROR\",\n\t\t\t\tmessage: \"Failed to create media\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Update media metadata\n */\nexport async function handleMediaUpdate(\n\tdb: Kysely<Database>,\n\tid: string,\n\tinput: {\n\t\talt?: string;\n\t\tcaption?: string;\n\t\twidth?: number;\n\t\theight?: number;\n\t},\n): Promise<ApiResult<MediaResponse>> {\n\ttry {\n\t\tconst repo = new MediaRepository(db);\n\t\tconst item = await repo.update(id, input);\n\n\t\tif (!item) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Media item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { item },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MEDIA_UPDATE_ERROR\",\n\t\t\t\tmessage: \"Failed to update media\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Delete media item\n */\nexport async function handleMediaDelete(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst repo = new MediaRepository(db);\n\t\tconst deleted = await repo.delete(id);\n\n\t\tif (!deleted) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Media item not found: ${id}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { deleted: true },\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MEDIA_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to delete media\",\n\t\t\t},\n\t\t};\n\t}\n}\n","/**\n * Collection info query for Astro templates.\n *\n * Same pattern as getMenu() / getComments() — uses getDb() for ambient DB access.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../database/types.js\";\nimport { getDb } from \"../loader.js\";\nimport { SchemaRegistry } from \"./registry.js\";\nimport type { Collection } from \"./types.js\";\n\n/**\n * Get collection metadata by slug.\n *\n * @example\n * ```ts\n * import { getCollectionInfo } from \"emdash\";\n *\n * const info = await getCollectionInfo(\"posts\");\n * if (info?.commentsEnabled) {\n * // render comment UI\n * }\n * ```\n */\nexport async function getCollectionInfo(slug: string): Promise<Collection | null> {\n\tconst db = await getDb();\n\treturn getCollectionInfoWithDb(db, slug);\n}\n\n/**\n * Get collection metadata with an explicit db handle.\n *\n * @internal Use `getCollectionInfo()` in templates. This variant is for\n * routes that already have a database handle.\n */\nexport async function getCollectionInfoWithDb(\n\tdb: Kysely<Database>,\n\tslug: string,\n): Promise<Collection | null> {\n\tconst registry = new SchemaRegistry(db);\n\treturn registry.getCollection(slug);\n}\n","/**\n * Plugin State Repository\n *\n * Database-backed storage for plugin activation state.\n * Used by the admin API to persist plugin enable/disable across restarts.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../database/types.js\";\n\nexport type PluginStatus = \"active\" | \"inactive\";\nexport type PluginSource = \"config\" | \"marketplace\";\n\nfunction toPluginStatus(value: string): PluginStatus {\n\tif (value === \"active\") return \"active\";\n\treturn \"inactive\";\n}\n\nfunction toPluginSource(value: string | undefined | null): PluginSource {\n\tif (value === \"marketplace\") return \"marketplace\";\n\treturn \"config\";\n}\n\nexport interface PluginState {\n\tpluginId: string;\n\tstatus: PluginStatus;\n\tversion: string;\n\tinstalledAt: Date;\n\tactivatedAt: Date | null;\n\tdeactivatedAt: Date | null;\n\tsource: PluginSource;\n\tmarketplaceVersion: string | null;\n\tdisplayName: string | null;\n\tdescription: string | null;\n}\n\n/**\n * Repository for plugin state in the database\n */\nexport class PluginStateRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Get state for a specific plugin\n\t */\n\tasync get(pluginId: string): Promise<PluginState | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_plugin_state\")\n\t\t\t.selectAll()\n\t\t\t.where(\"plugin_id\", \"=\", pluginId)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!row) return null;\n\n\t\treturn {\n\t\t\tpluginId: row.plugin_id,\n\t\t\tstatus: toPluginStatus(row.status),\n\t\t\tversion: row.version,\n\t\t\tinstalledAt: new Date(row.installed_at),\n\t\t\tactivatedAt: row.activated_at ? new Date(row.activated_at) : null,\n\t\t\tdeactivatedAt: row.deactivated_at ? new Date(row.deactivated_at) : null,\n\t\t\tsource: toPluginSource(row.source),\n\t\t\tmarketplaceVersion: row.marketplace_version ?? null,\n\t\t\tdisplayName: row.display_name ?? null,\n\t\t\tdescription: row.description ?? null,\n\t\t};\n\t}\n\n\t/**\n\t * Get all plugin states\n\t */\n\tasync getAll(): Promise<PluginState[]> {\n\t\tconst rows = await this.db.selectFrom(\"_plugin_state\").selectAll().execute();\n\n\t\treturn rows.map((row) => ({\n\t\t\tpluginId: row.plugin_id,\n\t\t\tstatus: toPluginStatus(row.status),\n\t\t\tversion: row.version,\n\t\t\tinstalledAt: new Date(row.installed_at),\n\t\t\tactivatedAt: row.activated_at ? new Date(row.activated_at) : null,\n\t\t\tdeactivatedAt: row.deactivated_at ? new Date(row.deactivated_at) : null,\n\t\t\tsource: toPluginSource(row.source),\n\t\t\tmarketplaceVersion: row.marketplace_version ?? null,\n\t\t\tdisplayName: row.display_name ?? null,\n\t\t\tdescription: row.description ?? null,\n\t\t}));\n\t}\n\n\t/**\n\t * Get all marketplace-installed plugin states\n\t */\n\tasync getMarketplacePlugins(): Promise<PluginState[]> {\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_plugin_state\")\n\t\t\t.selectAll()\n\t\t\t.where(\"source\", \"=\", \"marketplace\")\n\t\t\t.execute();\n\n\t\treturn rows.map((row) => ({\n\t\t\tpluginId: row.plugin_id,\n\t\t\tstatus: toPluginStatus(row.status),\n\t\t\tversion: row.version,\n\t\t\tinstalledAt: new Date(row.installed_at),\n\t\t\tactivatedAt: row.activated_at ? new Date(row.activated_at) : null,\n\t\t\tdeactivatedAt: row.deactivated_at ? new Date(row.deactivated_at) : null,\n\t\t\tsource: toPluginSource(row.source),\n\t\t\tmarketplaceVersion: row.marketplace_version ?? null,\n\t\t\tdisplayName: row.display_name ?? null,\n\t\t\tdescription: row.description ?? null,\n\t\t}));\n\t}\n\n\t/**\n\t * Create or update plugin state\n\t */\n\tasync upsert(\n\t\tpluginId: string,\n\t\tversion: string,\n\t\tstatus: PluginStatus,\n\t\topts?: {\n\t\t\tsource?: PluginSource;\n\t\t\tmarketplaceVersion?: string;\n\t\t\tdisplayName?: string;\n\t\t\tdescription?: string;\n\t\t},\n\t): Promise<PluginState> {\n\t\tconst now = new Date().toISOString();\n\t\tconst existing = await this.get(pluginId);\n\n\t\tif (existing) {\n\t\t\t// Update existing state\n\t\t\tconst updates: Record<string, string | null> = {\n\t\t\t\tstatus,\n\t\t\t\tversion,\n\t\t\t};\n\n\t\t\tif (status === \"active\" && existing.status !== \"active\") {\n\t\t\t\tupdates.activated_at = now;\n\t\t\t} else if (status === \"inactive\" && existing.status !== \"inactive\") {\n\t\t\t\tupdates.deactivated_at = now;\n\t\t\t}\n\n\t\t\tif (opts?.source) updates.source = opts.source;\n\t\t\tif (opts?.marketplaceVersion !== undefined) {\n\t\t\t\tupdates.marketplace_version = opts.marketplaceVersion;\n\t\t\t}\n\t\t\tif (opts?.displayName !== undefined) {\n\t\t\t\tupdates.display_name = opts.displayName;\n\t\t\t}\n\t\t\tif (opts?.description !== undefined) {\n\t\t\t\tupdates.description = opts.description;\n\t\t\t}\n\n\t\t\tawait this.db\n\t\t\t\t.updateTable(\"_plugin_state\")\n\t\t\t\t.set(updates)\n\t\t\t\t.where(\"plugin_id\", \"=\", pluginId)\n\t\t\t\t.execute();\n\t\t} else {\n\t\t\t// Create new state\n\t\t\tawait this.db\n\t\t\t\t.insertInto(\"_plugin_state\")\n\t\t\t\t.values({\n\t\t\t\t\tplugin_id: pluginId,\n\t\t\t\t\tstatus,\n\t\t\t\t\tversion,\n\t\t\t\t\tinstalled_at: now,\n\t\t\t\t\tactivated_at: status === \"active\" ? now : null,\n\t\t\t\t\tdeactivated_at: null,\n\t\t\t\t\tdata: null,\n\t\t\t\t\tsource: opts?.source ?? \"config\",\n\t\t\t\t\tmarketplace_version: opts?.marketplaceVersion ?? null,\n\t\t\t\t\tdisplay_name: opts?.displayName ?? null,\n\t\t\t\t\tdescription: opts?.description ?? null,\n\t\t\t\t})\n\t\t\t\t.execute();\n\t\t}\n\n\t\treturn (await this.get(pluginId))!;\n\t}\n\n\t/**\n\t * Enable a plugin\n\t */\n\tasync enable(pluginId: string, version: string): Promise<PluginState> {\n\t\treturn this.upsert(pluginId, version, \"active\");\n\t}\n\n\t/**\n\t * Disable a plugin\n\t */\n\tasync disable(pluginId: string, version: string): Promise<PluginState> {\n\t\treturn this.upsert(pluginId, version, \"inactive\");\n\t}\n\n\t/**\n\t * Delete plugin state\n\t */\n\tasync delete(pluginId: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_plugin_state\")\n\t\t\t.where(\"plugin_id\", \"=\", pluginId)\n\t\t\t.executeTakeFirst();\n\n\t\treturn (result.numDeletedRows ?? 0) > 0;\n\t}\n}\n","/**\n * Sections runtime functions\n *\n * Sections are reusable content blocks that can be inserted into any Portable Text field.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { encodeCursor, decodeCursor, type FindManyResult } from \"../database/repositories/types.js\";\nimport type { Database } from \"../database/types.js\";\nimport { getDb } from \"../loader.js\";\nimport type { Section, SectionRow, GetSectionsOptions } from \"./types.js\";\n\nexport type {\n\tSection,\n\tSectionSource,\n\tSectionRow,\n\tCreateSectionInput,\n\tUpdateSectionInput,\n\tGetSectionsOptions,\n} from \"./types.js\";\n\n/**\n * Get a section by slug\n *\n * @example\n * ```ts\n * import { getSection } from \"emdash\";\n *\n * const section = await getSection(\"hero-centered\");\n * if (section) {\n * console.log(section.content); // Portable Text array\n * }\n * ```\n */\nexport async function getSection(slug: string): Promise<Section | null> {\n\tconst db = await getDb();\n\treturn getSectionWithDb(slug, db);\n}\n\n/**\n * Get a section by slug (with explicit db)\n *\n * @internal Use `getSection()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getSectionWithDb(\n\tslug: string,\n\tdb: Kysely<Database>,\n): Promise<Section | null> {\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_sections\")\n\t\t.selectAll()\n\t\t.$castTo<SectionRow>()\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.executeTakeFirst();\n\n\tif (!row) {\n\t\treturn null;\n\t}\n\n\treturn rowToSection(row, db);\n}\n\n/**\n * Get a section by ID\n *\n * @internal Primarily for admin use\n */\nexport async function getSectionById(id: string, db: Kysely<Database>): Promise<Section | null> {\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_sections\")\n\t\t.selectAll()\n\t\t.$castTo<SectionRow>()\n\t\t.where(\"id\", \"=\", id)\n\t\t.executeTakeFirst();\n\n\tif (!row) {\n\t\treturn null;\n\t}\n\n\treturn rowToSection(row, db);\n}\n\n/**\n * Get all sections with optional filtering\n *\n * @example\n * ```ts\n * import { getSections } from \"emdash\";\n *\n * // Get all theme-provided sections\n * const themeSections = await getSections({ source: \"theme\" });\n *\n * // Search sections\n * const results = await getSections({ search: \"pricing\" });\n * ```\n */\nexport async function getSections(\n\toptions: GetSectionsOptions = {},\n): Promise<FindManyResult<Section>> {\n\tconst db = await getDb();\n\treturn getSectionsWithDb(db, options);\n}\n\n/**\n * Get all sections with optional filtering (with explicit db)\n *\n * @internal Use `getSections()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getSectionsWithDb(\n\tdb: Kysely<Database>,\n\toptions: GetSectionsOptions = {},\n): Promise<FindManyResult<Section>> {\n\tconst limit = Math.min(Math.max(1, options.limit || 50), 100);\n\n\tlet query = db.selectFrom(\"_emdash_sections\").selectAll();\n\n\t// Filter by source\n\tif (options.source) {\n\t\tquery = query.where(\"source\", \"=\", options.source);\n\t}\n\n\t// Search - search title, description, and keywords\n\tif (options.search) {\n\t\tconst searchTerm = `%${options.search.toLowerCase()}%`;\n\t\tquery = query.where((eb) =>\n\t\t\teb.or([\n\t\t\t\teb(\"title\", \"like\", searchTerm),\n\t\t\t\teb(\"description\", \"like\", searchTerm),\n\t\t\t\teb(\"keywords\", \"like\", searchTerm),\n\t\t\t]),\n\t\t);\n\t}\n\n\t// Order by title ASC, id ASC for stable cursor pagination\n\tquery = query.orderBy(\"title\", \"asc\").orderBy(\"id\", \"asc\");\n\n\t// Cursor-based pagination\n\tif (options.cursor) {\n\t\tconst decoded = decodeCursor(options.cursor);\n\t\tif (decoded) {\n\t\t\tquery = query.where((eb) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"title\", \">\", decoded.orderValue),\n\t\t\t\t\teb.and([eb(\"title\", \"=\", decoded.orderValue), eb(\"id\", \">\", decoded.id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\t}\n\n\tquery = query.limit(limit + 1);\n\n\tconst rows = await query.$castTo<SectionRow>().execute();\n\tconst hasMore = rows.length > limit;\n\tconst sliced = rows.slice(0, limit);\n\n\t// Convert rows to sections\n\tconst items = await Promise.all(sliced.map((row) => rowToSection(row, db)));\n\tconst result: FindManyResult<Section> = { items };\n\n\tif (hasMore && items.length > 0) {\n\t\tconst last = items.at(-1)!;\n\t\tresult.nextCursor = encodeCursor(last.title, last.id);\n\t}\n\n\treturn result;\n}\n\n/**\n * Convert a section row to the API type\n */\nasync function rowToSection(row: SectionRow, db: Kysely<Database>): Promise<Section> {\n\t// Parse keywords\n\tlet keywords: string[] = [];\n\tif (row.keywords) {\n\t\ttry {\n\t\t\tkeywords = JSON.parse(row.keywords);\n\t\t} catch {\n\t\t\t// Invalid JSON, ignore\n\t\t}\n\t}\n\n\t// Parse content — stored as JSON array of Portable Text blocks\n\tlet content: Section[\"content\"] = [];\n\tif (row.content) {\n\t\ttry {\n\t\t\tconst parsed: unknown = JSON.parse(row.content);\n\t\t\tif (Array.isArray(parsed)) {\n\t\t\t\t// DB stores serialized PortableTextBlock[]; trust the schema\n\t\t\t\tcontent = parsed;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Invalid JSON, ignore\n\t\t}\n\t}\n\n\t// Get preview URL from media (if present)\n\tlet previewUrl: string | undefined;\n\tif (row.preview_media_id) {\n\t\tconst media = await db\n\t\t\t.selectFrom(\"media\")\n\t\t\t.select(\"storage_key\")\n\t\t\t.where(\"id\", \"=\", row.preview_media_id)\n\t\t\t.executeTakeFirst();\n\n\t\tif (media) {\n\t\t\tpreviewUrl = `/_emdash/media/${media.storage_key}`;\n\t\t}\n\t}\n\n\treturn {\n\t\tid: row.id,\n\t\tslug: row.slug,\n\t\ttitle: row.title,\n\t\tdescription: row.description ?? undefined,\n\t\tkeywords,\n\t\tcontent,\n\t\tpreviewUrl,\n\t\tsource: row.source,\n\t\tthemeId: row.theme_id ?? undefined,\n\t\tcreatedAt: row.created_at,\n\t\tupdatedAt: row.updated_at,\n\t};\n}\n","/**\n * Marketplace plugin handlers\n *\n * Business logic for installing, updating, uninstalling, and checking\n * updates for marketplace plugins. Routes are thin wrappers around these.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { validatePluginIdentifier } from \"../../database/validate.js\";\nimport { pluginManifestSchema } from \"../../plugins/manifest-schema.js\";\nimport { normalizeManifestRoute } from \"../../plugins/manifest-schema.js\";\nimport {\n\tcreateMarketplaceClient,\n\tMarketplaceError,\n\tMarketplaceUnavailableError,\n\ttype MarketplaceClient,\n\ttype MarketplacePluginDetail,\n\ttype MarketplaceSearchOpts,\n\ttype MarketplaceThemeSearchOpts,\n\ttype MarketplaceVersionSummary,\n\ttype PluginBundle,\n} from \"../../plugins/marketplace.js\";\nimport type { SandboxRunner } from \"../../plugins/sandbox/types.js\";\nimport { PluginStateRepository } from \"../../plugins/state.js\";\nimport type { PluginManifest } from \"../../plugins/types.js\";\nimport { EmDashStorageError } from \"../../storage/types.js\";\nimport type { Storage } from \"../../storage/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\n// ── Types ──────────────────────────────────────────────────────────\n\nexport interface MarketplaceInstallResult {\n\tpluginId: string;\n\tversion: string;\n\tcapabilities: string[];\n}\n\nexport interface MarketplaceUpdateResult {\n\tpluginId: string;\n\toldVersion: string;\n\tnewVersion: string;\n\tcapabilityChanges: {\n\t\tadded: string[];\n\t\tremoved: string[];\n\t};\n\trouteVisibilityChanges?: {\n\t\tnewlyPublic: string[];\n\t};\n}\n\nexport interface MarketplaceUpdateCheck {\n\tpluginId: string;\n\tinstalled: string;\n\tlatest: string;\n\thasUpdate: boolean;\n\thasCapabilityChanges: boolean;\n\tcapabilityChanges?: {\n\t\tadded: string[];\n\t\tremoved: string[];\n\t};\n\thasRouteVisibilityChanges: boolean;\n\trouteVisibilityChanges?: {\n\t\tnewlyPublic: string[];\n\t};\n}\n\nexport interface MarketplaceUninstallResult {\n\tpluginId: string;\n\tdataDeleted: boolean;\n}\n\n// ── Helpers ────────────────────────────────────────────────────────\n\n/** Semver-like pattern: digits, dots, hyphens, plus signs (e.g. 1.0.0, 1.0.0-beta.1) */\nconst VERSION_PATTERN = /^[a-z0-9][a-z0-9._+-]*$/i;\n\nfunction validateVersion(version: string): void {\n\tif (version.includes(\"..\")) throw new Error(\"Invalid version format\");\n\tif (!VERSION_PATTERN.test(version)) {\n\t\tthrow new Error(\"Invalid version format\");\n\t}\n}\n\nfunction getClient(\n\tmarketplaceUrl: string | undefined,\n\tsiteOrigin?: string,\n): MarketplaceClient | null {\n\tif (!marketplaceUrl) return null;\n\treturn createMarketplaceClient(marketplaceUrl, siteOrigin);\n}\n\nfunction diffCapabilities(\n\toldCaps: string[],\n\tnewCaps: string[],\n): { added: string[]; removed: string[] } {\n\tconst oldSet = new Set(oldCaps);\n\tconst newSet = new Set(newCaps);\n\treturn {\n\t\tadded: newCaps.filter((c) => !oldSet.has(c)),\n\t\tremoved: oldCaps.filter((c) => !newSet.has(c)),\n\t};\n}\n\n/**\n * Diff route visibility between two manifests.\n * Returns routes that changed from private to public (newly exposed).\n */\nfunction diffRouteVisibility(\n\toldManifest: PluginManifest | undefined,\n\tnewManifest: PluginManifest,\n): { newlyPublic: string[] } {\n\tconst oldPublicRoutes = new Set<string>();\n\tif (oldManifest) {\n\t\tfor (const entry of oldManifest.routes) {\n\t\t\tconst normalized = normalizeManifestRoute(entry);\n\t\t\tif (normalized.public === true) {\n\t\t\t\toldPublicRoutes.add(normalized.name);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst newlyPublic: string[] = [];\n\tfor (const entry of newManifest.routes) {\n\t\tconst normalized = normalizeManifestRoute(entry);\n\t\tif (normalized.public === true && !oldPublicRoutes.has(normalized.name)) {\n\t\t\tnewlyPublic.push(normalized.name);\n\t\t}\n\t}\n\n\treturn { newlyPublic };\n}\n\nasync function resolveVersionMetadata(\n\tclient: MarketplaceClient,\n\tpluginId: string,\n\tpluginDetail: MarketplacePluginDetail,\n\tversion: string,\n): Promise<MarketplaceVersionSummary | null> {\n\tif (pluginDetail.latestVersion?.version === version) {\n\t\treturn {\n\t\t\tversion: pluginDetail.latestVersion.version,\n\t\t\tminEmDashVersion: pluginDetail.latestVersion.minEmDashVersion,\n\t\t\tbundleSize: pluginDetail.latestVersion.bundleSize,\n\t\t\tchecksum: pluginDetail.latestVersion.checksum,\n\t\t\tchangelog: pluginDetail.latestVersion.changelog,\n\t\t\tcapabilities: pluginDetail.latestVersion.capabilities,\n\t\t\tstatus: pluginDetail.latestVersion.status,\n\t\t\tauditVerdict: pluginDetail.latestVersion.audit?.verdict ?? null,\n\t\t\timageAuditVerdict: pluginDetail.latestVersion.imageAudit?.verdict ?? null,\n\t\t\tpublishedAt: pluginDetail.latestVersion.publishedAt,\n\t\t};\n\t}\n\n\tconst versions = await client.getVersions(pluginId);\n\treturn versions.find((v) => v.version === version) ?? null;\n}\n\nfunction validateBundleIdentity(\n\tbundle: PluginBundle,\n\tpluginId: string,\n\tversion: string,\n): ApiResult<never> | null {\n\tif (bundle.manifest.id !== pluginId) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MANIFEST_MISMATCH\",\n\t\t\t\tmessage: `Bundle manifest ID (${bundle.manifest.id}) does not match requested plugin (${pluginId})`,\n\t\t\t},\n\t\t};\n\t}\n\n\tif (bundle.manifest.version !== version) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MANIFEST_VERSION_MISMATCH\",\n\t\t\t\tmessage: `Bundle manifest version (${bundle.manifest.version}) does not match requested version (${version})`,\n\t\t\t},\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/** Store a plugin bundle's files in site-local R2 storage */\nasync function storeBundleInR2(\n\tstorage: Storage,\n\tpluginId: string,\n\tversion: string,\n\tbundle: PluginBundle,\n): Promise<void> {\n\tvalidatePluginIdentifier(pluginId, \"plugin ID\");\n\tvalidateVersion(version);\n\tconst prefix = `marketplace/${pluginId}/${version}`;\n\n\t// Store manifest\n\tawait storage.upload({\n\t\tkey: `${prefix}/manifest.json`,\n\t\tbody: new TextEncoder().encode(JSON.stringify(bundle.manifest)),\n\t\tcontentType: \"application/json\",\n\t});\n\n\t// Store backend code\n\tawait storage.upload({\n\t\tkey: `${prefix}/backend.js`,\n\t\tbody: new TextEncoder().encode(bundle.backendCode),\n\t\tcontentType: \"application/javascript\",\n\t});\n\n\t// Store admin code if present\n\tif (bundle.adminCode) {\n\t\tawait storage.upload({\n\t\t\tkey: `${prefix}/admin.js`,\n\t\t\tbody: new TextEncoder().encode(bundle.adminCode),\n\t\t\tcontentType: \"application/javascript\",\n\t\t});\n\t}\n}\n\n/** Read a ReadableStream to string */\nasync function streamToText(stream: ReadableStream<Uint8Array>): Promise<string> {\n\treturn new Response(stream).text();\n}\n\n/** Load a plugin bundle from site-local R2 storage */\nexport async function loadBundleFromR2(\n\tstorage: Storage,\n\tpluginId: string,\n\tversion: string,\n): Promise<{ manifest: PluginManifest; backendCode: string; adminCode?: string } | null> {\n\tvalidatePluginIdentifier(pluginId, \"plugin ID\");\n\tvalidateVersion(version);\n\tconst prefix = `marketplace/${pluginId}/${version}`;\n\n\ttry {\n\t\tconst manifestResult = await storage.download(`${prefix}/manifest.json`);\n\t\tconst backendResult = await storage.download(`${prefix}/backend.js`);\n\n\t\tconst manifestText = await streamToText(manifestResult.body);\n\t\tconst backendCode = await streamToText(backendResult.body);\n\t\tconst parsed: unknown = JSON.parse(manifestText);\n\t\tconst result = pluginManifestSchema.safeParse(parsed);\n\t\tif (!result.success) return null;\n\t\t// Elements are validated as unknown[] by Zod; cast to PluginManifest\n\t\t// for the Element[] type (Block Kit validation happens at render time).\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Zod types elements as unknown[]; Element type validated at render time\n\t\tconst manifest = result.data as unknown as PluginManifest;\n\n\t\t// Try to load admin code (optional)\n\t\tlet adminCode: string | undefined;\n\t\ttry {\n\t\t\tconst adminResult = await storage.download(`${prefix}/admin.js`);\n\t\t\tadminCode = await streamToText(adminResult.body);\n\t\t} catch {\n\t\t\t// admin.js is optional\n\t\t}\n\n\t\treturn { manifest, backendCode, adminCode };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/** Delete a plugin bundle from site-local R2 storage */\nasync function deleteBundleFromR2(\n\tstorage: Storage,\n\tpluginId: string,\n\tversion: string,\n): Promise<void> {\n\tvalidatePluginIdentifier(pluginId, \"plugin ID\");\n\tvalidateVersion(version);\n\tconst prefix = `marketplace/${pluginId}/${version}`;\n\tconst files = [\"manifest.json\", \"backend.js\", \"admin.js\"];\n\n\tfor (const file of files) {\n\t\ttry {\n\t\t\tawait storage.delete(`${prefix}/${file}`);\n\t\t} catch {\n\t\t\t// Ignore missing files\n\t\t}\n\t}\n}\n\n// ── Install ────────────────────────────────────────────────────────\n\nexport async function handleMarketplaceInstall(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n\tsandboxRunner: SandboxRunner | null,\n\tmarketplaceUrl: string | undefined,\n\tpluginId: string,\n\topts?: { version?: string; configuredPluginIds?: Set<string>; siteOrigin?: string },\n): Promise<ApiResult<MarketplaceInstallResult>> {\n\tconst client = getClient(marketplaceUrl, opts?.siteOrigin);\n\tif (!client) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"MARKETPLACE_NOT_CONFIGURED\",\n\t\t\t\tmessage: \"Marketplace is not configured\",\n\t\t\t},\n\t\t};\n\t}\n\n\tif (!storage) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"STORAGE_NOT_CONFIGURED\",\n\t\t\t\tmessage: \"Storage is required for marketplace plugin installation\",\n\t\t\t},\n\t\t};\n\t}\n\n\tif (!sandboxRunner || !sandboxRunner.isAvailable()) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"SANDBOX_NOT_AVAILABLE\",\n\t\t\t\tmessage: \"Sandbox runner is required for marketplace plugins\",\n\t\t\t},\n\t\t};\n\t}\n\n\ttry {\n\t\t// Check if already installed\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst existing = await stateRepo.get(pluginId);\n\t\tif (existing && existing.source === \"marketplace\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"ALREADY_INSTALLED\",\n\t\t\t\t\tmessage: `Plugin ${pluginId} is already installed`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Block installation if a configured (trusted) plugin with the same ID exists.\n\t\t// Without this check, the sandboxed plugin could shadow the trusted plugin's\n\t\t// route handlers while auth decisions are made against the trusted plugin's metadata.\n\t\tif (opts?.configuredPluginIds?.has(pluginId)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"PLUGIN_ID_CONFLICT\",\n\t\t\t\t\tmessage: `Cannot install marketplace plugin \"${pluginId}\" — a configured plugin with the same ID already exists`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Fetch plugin detail from marketplace\n\t\tconst pluginDetail = await client.getPlugin(pluginId);\n\t\tconst version = opts?.version ?? pluginDetail.latestVersion?.version;\n\t\tif (!version) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NO_VERSION\",\n\t\t\t\t\tmessage: `No published versions found for plugin ${pluginId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst versionMetadata = await resolveVersionMetadata(client, pluginId, pluginDetail, version);\n\t\tif (!versionMetadata) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NO_VERSION\",\n\t\t\t\t\tmessage: `Version ${version} was not found for plugin ${pluginId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Block installation of plugins that haven't passed audit.\n\t\t// Both \"fail\" (explicitly malicious) and \"warn\" (audit error or\n\t\t// inconclusive) are non-installable — only \"pass\" or null (no audit\n\t\t// ran) are allowed through.\n\t\tif (versionMetadata.auditVerdict === \"fail\" || versionMetadata.auditVerdict === \"warn\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"AUDIT_FAILED\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\tversionMetadata.auditVerdict === \"fail\"\n\t\t\t\t\t\t\t? \"Plugin failed security audit and cannot be installed\"\n\t\t\t\t\t\t\t: \"Plugin audit was inconclusive and cannot be installed until reviewed\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Download and extract bundle\n\t\tconst bundle = await client.downloadBundle(pluginId, version);\n\n\t\t// Verify checksum matches marketplace-published checksum\n\t\tif (versionMetadata.checksum && bundle.checksum !== versionMetadata.checksum) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CHECKSUM_MISMATCH\",\n\t\t\t\t\tmessage: \"Bundle checksum does not match marketplace record. Download may be corrupted.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst bundleIdentityError = validateBundleIdentity(bundle, pluginId, version);\n\t\tif (bundleIdentityError) return bundleIdentityError;\n\n\t\t// Store bundle in site-local R2\n\t\tawait storeBundleInR2(storage, pluginId, version, bundle);\n\n\t\t// Write plugin state\n\t\tawait stateRepo.upsert(pluginId, version, \"active\", {\n\t\t\tsource: \"marketplace\",\n\t\t\tmarketplaceVersion: version,\n\t\t\tdisplayName: pluginDetail.name,\n\t\t\tdescription: pluginDetail.description ?? undefined,\n\t\t});\n\n\t\t// Fire-and-forget install stat\n\t\tclient.reportInstall(pluginId, version).catch(() => {\n\t\t\t// Intentional: never fails the install\n\t\t});\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tpluginId,\n\t\t\t\tversion,\n\t\t\t\tcapabilities: bundle.manifest.capabilities,\n\t\t\t},\n\t\t};\n\t} catch (err) {\n\t\tif (err instanceof MarketplaceUnavailableError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"MARKETPLACE_UNAVAILABLE\",\n\t\t\t\t\tmessage: \"Plugin marketplace is currently unavailable\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (err instanceof MarketplaceError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: err.code ?? \"MARKETPLACE_ERROR\",\n\t\t\t\t\tmessage: err.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (err instanceof EmDashStorageError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: err.code ?? \"STORAGE_ERROR\",\n\t\t\t\t\tmessage: \"Storage error while installing plugin\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (err && typeof err === \"object\" && \"code\" in err) {\n\t\t\tconst code = (err as { code?: unknown }).code;\n\t\t\tif (typeof code === \"string\" && code.trim()) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode,\n\t\t\t\t\t\tmessage: \"Failed to install plugin from marketplace\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\tconsole.error(\"Failed to install marketplace plugin:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"INSTALL_FAILED\",\n\t\t\t\tmessage: \"Failed to install plugin from marketplace\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ── Update ─────────────────────────────────────────────────────────\n\nexport async function handleMarketplaceUpdate(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n\tsandboxRunner: SandboxRunner | null,\n\tmarketplaceUrl: string | undefined,\n\tpluginId: string,\n\topts?: {\n\t\tversion?: string;\n\t\tconfirmCapabilityChanges?: boolean;\n\t\tconfirmRouteVisibilityChanges?: boolean;\n\t},\n): Promise<ApiResult<MarketplaceUpdateResult>> {\n\tconst client = getClient(marketplaceUrl);\n\tif (!client) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MARKETPLACE_NOT_CONFIGURED\", message: \"Marketplace is not configured\" },\n\t\t};\n\t}\n\tif (!storage) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"STORAGE_NOT_CONFIGURED\", message: \"Storage is required\" },\n\t\t};\n\t}\n\tif (!sandboxRunner || !sandboxRunner.isAvailable()) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SANDBOX_NOT_AVAILABLE\", message: \"Sandbox runner is required\" },\n\t\t};\n\t}\n\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst existing = await stateRepo.get(pluginId);\n\t\tif (!existing || existing.source !== \"marketplace\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `No marketplace plugin found: ${pluginId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst oldVersion = existing.marketplaceVersion ?? existing.version;\n\n\t\t// Get target version\n\t\tconst pluginDetail = await client.getPlugin(pluginId);\n\t\tconst newVersion = opts?.version ?? pluginDetail.latestVersion?.version;\n\t\tif (!newVersion) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NO_VERSION\", message: \"No newer version available\" },\n\t\t\t};\n\t\t}\n\n\t\tif (newVersion === oldVersion) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"ALREADY_UP_TO_DATE\", message: \"Plugin is already up to date\" },\n\t\t\t};\n\t\t}\n\n\t\tconst versionMetadata = await resolveVersionMetadata(\n\t\t\tclient,\n\t\t\tpluginId,\n\t\t\tpluginDetail,\n\t\t\tnewVersion,\n\t\t);\n\t\tif (!versionMetadata) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NO_VERSION\",\n\t\t\t\t\tmessage: `Version ${newVersion} was not found for plugin ${pluginId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Download new bundle\n\t\tconst bundle = await client.downloadBundle(pluginId, newVersion);\n\n\t\t// Verify checksum matches marketplace-published checksum for this version\n\t\tif (versionMetadata.checksum && bundle.checksum !== versionMetadata.checksum) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CHECKSUM_MISMATCH\",\n\t\t\t\t\tmessage: \"Bundle checksum does not match marketplace record. Download may be corrupted.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst bundleIdentityError = validateBundleIdentity(bundle, pluginId, newVersion);\n\t\tif (bundleIdentityError) return bundleIdentityError;\n\n\t\t// Diff capabilities and route visibility against old version\n\t\tconst oldBundle = await loadBundleFromR2(storage, pluginId, oldVersion);\n\t\tconst oldCaps = oldBundle?.manifest.capabilities ?? [];\n\t\tconst capabilityChanges = diffCapabilities(oldCaps, bundle.manifest.capabilities);\n\t\tconst hasEscalation = capabilityChanges.added.length > 0;\n\n\t\t// If capabilities escalated, require explicit confirmation\n\t\tif (hasEscalation && !opts?.confirmCapabilityChanges) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CAPABILITY_ESCALATION\",\n\t\t\t\t\tmessage: \"Plugin update requires new capabilities\",\n\t\t\t\t\tdetails: { capabilityChanges },\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Diff route visibility — routes going from private to public are a\n\t\t// security-sensitive change that exposes unauthenticated endpoints.\n\t\tconst routeVisibilityChanges = diffRouteVisibility(oldBundle?.manifest, bundle.manifest);\n\t\tconst hasNewPublicRoutes = routeVisibilityChanges.newlyPublic.length > 0;\n\n\t\tif (hasNewPublicRoutes && !opts?.confirmRouteVisibilityChanges) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"ROUTE_VISIBILITY_ESCALATION\",\n\t\t\t\t\tmessage: \"Plugin update exposes new public (unauthenticated) routes\",\n\t\t\t\t\tdetails: { routeVisibilityChanges, capabilityChanges },\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Store new bundle\n\t\tawait storeBundleInR2(storage, pluginId, newVersion, bundle);\n\n\t\t// Update state\n\t\tawait stateRepo.upsert(pluginId, newVersion, \"active\", {\n\t\t\tsource: \"marketplace\",\n\t\t\tmarketplaceVersion: newVersion,\n\t\t\tdisplayName: pluginDetail.name,\n\t\t\tdescription: pluginDetail.description ?? undefined,\n\t\t});\n\n\t\t// Clean up old bundle from R2 (best-effort)\n\t\tdeleteBundleFromR2(storage, pluginId, oldVersion).catch(() => {});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tpluginId,\n\t\t\t\toldVersion,\n\t\t\t\tnewVersion,\n\t\t\t\tcapabilityChanges,\n\t\t\t\trouteVisibilityChanges: hasNewPublicRoutes ? routeVisibilityChanges : undefined,\n\t\t\t},\n\t\t};\n\t} catch (err) {\n\t\tif (err instanceof MarketplaceUnavailableError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"MARKETPLACE_UNAVAILABLE\", message: \"Marketplace is unavailable\" },\n\t\t\t};\n\t\t}\n\t\tif (err instanceof MarketplaceError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: err.code ?? \"MARKETPLACE_ERROR\", message: err.message },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Failed to update marketplace plugin:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"UPDATE_FAILED\", message: \"Failed to update plugin\" },\n\t\t};\n\t}\n}\n\n// ── Uninstall ──────────────────────────────────────────────────────\n\nexport async function handleMarketplaceUninstall(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n\tpluginId: string,\n\topts?: { deleteData?: boolean },\n): Promise<ApiResult<MarketplaceUninstallResult>> {\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst existing = await stateRepo.get(pluginId);\n\t\tif (!existing || existing.source !== \"marketplace\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `No marketplace plugin found: ${pluginId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst version = existing.marketplaceVersion ?? existing.version;\n\n\t\t// Delete bundle from site R2\n\t\tif (storage) {\n\t\t\tawait deleteBundleFromR2(storage, pluginId, version);\n\t\t}\n\n\t\t// Optionally delete plugin storage data\n\t\tlet dataDeleted = false;\n\t\tif (opts?.deleteData) {\n\t\t\ttry {\n\t\t\t\tawait db.deleteFrom(\"_plugin_storage\").where(\"plugin_id\", \"=\", pluginId).execute();\n\t\t\t\tdataDeleted = true;\n\t\t\t} catch {\n\t\t\t\t// Plugin storage table may not have data for this plugin\n\t\t\t}\n\t\t}\n\n\t\t// Delete state row\n\t\tawait stateRepo.delete(pluginId);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { pluginId, dataDeleted },\n\t\t};\n\t} catch (err) {\n\t\tconsole.error(\"Failed to uninstall marketplace plugin:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"UNINSTALL_FAILED\",\n\t\t\t\tmessage: \"Failed to uninstall plugin\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ── Update check ───────────────────────────────────────────────────\n\nexport async function handleMarketplaceUpdateCheck(\n\tdb: Kysely<Database>,\n\tmarketplaceUrl: string | undefined,\n): Promise<ApiResult<{ items: MarketplaceUpdateCheck[] }>> {\n\tconst client = getClient(marketplaceUrl);\n\tif (!client) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MARKETPLACE_NOT_CONFIGURED\", message: \"Marketplace is not configured\" },\n\t\t};\n\t}\n\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst marketplacePlugins = await stateRepo.getMarketplacePlugins();\n\n\t\tconst items: MarketplaceUpdateCheck[] = [];\n\n\t\tfor (const plugin of marketplacePlugins) {\n\t\t\ttry {\n\t\t\t\tconst detail = await client.getPlugin(plugin.pluginId);\n\t\t\t\tconst latest = detail.latestVersion?.version;\n\t\t\t\tconst installed = plugin.marketplaceVersion ?? plugin.version;\n\n\t\t\t\tif (!latest) continue;\n\n\t\t\t\tconst hasUpdate = latest !== installed;\n\t\t\t\tlet capabilityChanges: { added: string[]; removed: string[] } | undefined;\n\t\t\t\tlet hasCapabilityChanges = false;\n\n\t\t\t\tif (hasUpdate && detail.latestVersion) {\n\t\t\t\t\tconst oldCaps = detail.capabilities ?? [];\n\t\t\t\t\tconst newCaps = detail.latestVersion.capabilities ?? [];\n\t\t\t\t\tcapabilityChanges = diffCapabilities(oldCaps, newCaps);\n\t\t\t\t\thasCapabilityChanges =\n\t\t\t\t\t\tcapabilityChanges.added.length > 0 || capabilityChanges.removed.length > 0;\n\t\t\t\t}\n\n\t\t\t\titems.push({\n\t\t\t\t\tpluginId: plugin.pluginId,\n\t\t\t\t\tinstalled,\n\t\t\t\t\tlatest: latest ?? installed,\n\t\t\t\t\thasUpdate,\n\t\t\t\t\thasCapabilityChanges,\n\t\t\t\t\tcapabilityChanges: hasCapabilityChanges ? capabilityChanges : undefined,\n\t\t\t\t\t// Route visibility changes require downloading both bundles to compare\n\t\t\t\t\t// manifests, which is too expensive for a preview check. The actual\n\t\t\t\t\t// enforcement happens at update time in handleMarketplaceUpdate.\n\t\t\t\t\thasRouteVisibilityChanges: false,\n\t\t\t\t});\n\t\t\t} catch (err) {\n\t\t\t\t// Skip plugins that can't be checked (marketplace down, plugin delisted)\n\t\t\t\tconsole.warn(`Failed to check updates for ${plugin.pluginId}:`, err);\n\t\t\t}\n\t\t}\n\n\t\treturn { success: true, data: { items } };\n\t} catch (err) {\n\t\tif (err instanceof MarketplaceUnavailableError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"MARKETPLACE_UNAVAILABLE\", message: \"Marketplace is unavailable\" },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Failed to check marketplace updates:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"UPDATE_CHECK_FAILED\", message: \"Failed to check for updates\" },\n\t\t};\n\t}\n}\n\n// ── Proxy ──────────────────────────────────────────────────────────\n\nexport async function handleMarketplaceSearch(\n\tmarketplaceUrl: string | undefined,\n\tquery?: string,\n\topts?: MarketplaceSearchOpts,\n): Promise<ApiResult<unknown>> {\n\tconst client = getClient(marketplaceUrl);\n\tif (!client) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MARKETPLACE_NOT_CONFIGURED\", message: \"Marketplace is not configured\" },\n\t\t};\n\t}\n\n\ttry {\n\t\tconst result = await client.search(query, opts);\n\t\treturn { success: true, data: result };\n\t} catch (err) {\n\t\tif (err instanceof MarketplaceUnavailableError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"MARKETPLACE_UNAVAILABLE\", message: \"Marketplace is unavailable\" },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Failed to search marketplace:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SEARCH_FAILED\", message: \"Failed to search marketplace\" },\n\t\t};\n\t}\n}\n\nexport async function handleMarketplaceGetPlugin(\n\tmarketplaceUrl: string | undefined,\n\tpluginId: string,\n): Promise<ApiResult<unknown>> {\n\tconst client = getClient(marketplaceUrl);\n\tif (!client) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MARKETPLACE_NOT_CONFIGURED\", message: \"Marketplace is not configured\" },\n\t\t};\n\t}\n\n\ttry {\n\t\tconst result = await client.getPlugin(pluginId);\n\t\treturn { success: true, data: result };\n\t} catch (err) {\n\t\tif (err instanceof MarketplaceError && err.status === 404) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Plugin not found: ${pluginId}` },\n\t\t\t};\n\t\t}\n\t\tif (err instanceof MarketplaceUnavailableError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"MARKETPLACE_UNAVAILABLE\", message: \"Marketplace is unavailable\" },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Failed to get marketplace plugin:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"GET_PLUGIN_FAILED\", message: \"Failed to get plugin details\" },\n\t\t};\n\t}\n}\n\n// ── Theme proxy handlers ──────────────────────────────────────────\n\nexport async function handleThemeSearch(\n\tmarketplaceUrl: string | undefined,\n\tquery?: string,\n\topts?: MarketplaceThemeSearchOpts,\n): Promise<ApiResult<unknown>> {\n\tconst client = getClient(marketplaceUrl);\n\tif (!client) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MARKETPLACE_NOT_CONFIGURED\", message: \"Marketplace is not configured\" },\n\t\t};\n\t}\n\n\ttry {\n\t\tconst result = await client.searchThemes(query, opts);\n\t\treturn { success: true, data: result };\n\t} catch (err) {\n\t\tif (err instanceof MarketplaceUnavailableError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"MARKETPLACE_UNAVAILABLE\", message: \"Marketplace is unavailable\" },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Failed to search themes:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"THEME_SEARCH_FAILED\", message: \"Failed to search themes\" },\n\t\t};\n\t}\n}\n\nexport async function handleThemeGetDetail(\n\tmarketplaceUrl: string | undefined,\n\tthemeId: string,\n): Promise<ApiResult<unknown>> {\n\tconst client = getClient(marketplaceUrl);\n\tif (!client) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"MARKETPLACE_NOT_CONFIGURED\", message: \"Marketplace is not configured\" },\n\t\t};\n\t}\n\n\ttry {\n\t\tconst result = await client.getTheme(themeId);\n\t\treturn { success: true, data: result };\n\t} catch (err) {\n\t\tif (err instanceof MarketplaceError && err.status === 404) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Theme not found: ${themeId}` },\n\t\t\t};\n\t\t}\n\t\tif (err instanceof MarketplaceUnavailableError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"MARKETPLACE_UNAVAILABLE\", message: \"Marketplace is unavailable\" },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Failed to get marketplace theme:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"GET_THEME_FAILED\", message: \"Failed to get theme details\" },\n\t\t};\n\t}\n}\n","/**\n * Request body and query parameter parsing with Zod validation.\n *\n * All API routes should use these utilities instead of `request.json() as T`\n * or raw `url.searchParams.get()` with manual coercion.\n */\n\nimport { z } from \"zod\";\n\nimport { apiError } from \"./error.js\";\n\n/** Maximum allowed JSON request body size (10 MB). */\nconst MAX_BODY_SIZE = 10 * 1024 * 1024;\n\n/**\n * Result of parsing: either the validated data or an error Response.\n * Routes should check `if (result instanceof Response) return result;`\n */\nexport type ParseResult<T> = T | Response;\n\n/**\n * Parse and validate a JSON request body against a Zod schema.\n *\n * Returns the validated data on success, or a 400 Response on failure.\n * Replaces all `(await request.json()) as T` casts.\n */\nexport async function parseBody<T extends z.ZodType>(\n\trequest: Request,\n\tschema: T,\n): Promise<ParseResult<z.infer<T>>> {\n\t// Best-effort size check via Content-Length (can be absent with chunked encoding)\n\tconst contentLength = request.headers.get(\"Content-Length\");\n\tif (contentLength && parseInt(contentLength, 10) > MAX_BODY_SIZE) {\n\t\treturn apiError(\"PAYLOAD_TOO_LARGE\", \"Request body too large\", 413);\n\t}\n\n\tlet raw: unknown;\n\ttry {\n\t\traw = await request.json();\n\t} catch {\n\t\treturn apiError(\"INVALID_JSON\", \"Request body must be valid JSON\", 400);\n\t}\n\n\treturn validate(schema, raw);\n}\n\n/**\n * Parse and validate an optional JSON request body.\n *\n * Returns `defaultValue` if the body is empty, or the validated data if present.\n * For endpoints where the body is optional (e.g., preview-url, confirm).\n */\nexport async function parseOptionalBody<T extends z.ZodType>(\n\trequest: Request,\n\tschema: T,\n\tdefaultValue: z.infer<T>,\n): Promise<ParseResult<z.infer<T>>> {\n\t// Best-effort size check via Content-Length (can be absent with chunked encoding)\n\tconst contentLength = request.headers.get(\"Content-Length\");\n\tif (contentLength && parseInt(contentLength, 10) > MAX_BODY_SIZE) {\n\t\treturn apiError(\"PAYLOAD_TOO_LARGE\", \"Request body too large\", 413);\n\t}\n\n\tlet text: string;\n\ttry {\n\t\ttext = await request.text();\n\t} catch {\n\t\treturn defaultValue;\n\t}\n\n\tif (!text.trim()) {\n\t\treturn defaultValue;\n\t}\n\n\tlet raw: unknown;\n\ttry {\n\t\traw = JSON.parse(text);\n\t} catch {\n\t\treturn apiError(\"INVALID_JSON\", \"Request body must be valid JSON\", 400);\n\t}\n\n\treturn validate(schema, raw);\n}\n\n/**\n * Parse and validate URL search params against a Zod schema.\n *\n * Converts searchParams to a plain object before validation.\n * Zod coercion handles string -> number/boolean conversion.\n * Replaces manual `url.searchParams.get()` + `parseInt()` patterns.\n */\nexport function parseQuery<T extends z.ZodType>(url: URL, schema: T): ParseResult<z.infer<T>> {\n\tconst raw: Record<string, string> = {};\n\tfor (const [key, value] of url.searchParams) {\n\t\traw[key] = value;\n\t}\n\treturn validate(schema, raw);\n}\n\n/**\n * Validate raw data against a schema. Returns data or error Response.\n */\nfunction validate<T extends z.ZodType>(schema: T, data: unknown): ParseResult<z.infer<T>> {\n\tconst result = schema.safeParse(data);\n\n\tif (result.success) {\n\t\treturn result.data as z.infer<T>;\n\t}\n\n\t// Format Zod errors into a readable structure\n\tconst issues = result.error.issues.map((issue: z.ZodIssue) => ({\n\t\tpath: issue.path.join(\".\"),\n\t\tmessage: issue.message,\n\t}));\n\n\treturn Response.json(\n\t\t{\n\t\t\terror: {\n\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\tmessage: \"Invalid request data\",\n\t\t\t\tdetails: { issues },\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tstatus: 400,\n\t\t\theaders: {\n\t\t\t\t\"Cache-Control\": \"private, no-store\",\n\t\t\t},\n\t\t},\n\t);\n}\n\n/**\n * Type guard to check if a ParseResult is an error Response.\n * Usage: `if (isParseError(result)) return result;`\n */\nexport function isParseError<T>(result: ParseResult<T>): result is Response {\n\treturn result instanceof Response;\n}\n","import { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Role level\n// ---------------------------------------------------------------------------\n\n/** Valid role level values */\nexport const VALID_ROLE_LEVELS = new Set([10, 20, 30, 40, 50]);\n\n/** Role level — coerces string/number to valid RoleLevel (10|20|30|40|50) */\nexport const roleLevel = z.coerce\n\t.number()\n\t.int()\n\t.refine((n): n is 10 | 20 | 30 | 40 | 50 => VALID_ROLE_LEVELS.has(n), {\n\t\tmessage: \"Invalid role level. Must be 10, 20, 30, 40, or 50\",\n\t});\n\n// ---------------------------------------------------------------------------\n// Pagination\n// ---------------------------------------------------------------------------\n\n/** Pagination query params — cursor-based */\nexport const cursorPaginationQuery = z\n\t.object({\n\t\tcursor: z.string().optional().meta({ description: \"Opaque cursor for pagination\" }),\n\t\tlimit: z.coerce.number().int().min(1).max(100).optional().default(50).meta({\n\t\t\tdescription: \"Maximum number of items to return (1-100, default 50)\",\n\t\t}),\n\t})\n\t.meta({ id: \"CursorPaginationQuery\" });\n\n/** Pagination query params — offset-based */\nexport const offsetPaginationQuery = z\n\t.object({\n\t\tlimit: z.coerce.number().int().min(1).max(100).optional().default(50),\n\t\toffset: z.coerce.number().int().min(0).optional().default(0),\n\t})\n\t.meta({ id: \"OffsetPaginationQuery\" });\n\n// ---------------------------------------------------------------------------\n// Shared primitives\n// ---------------------------------------------------------------------------\n\n/** Slug pattern: lowercase letters, digits, underscores; starts with letter */\nexport const slugPattern = /^[a-z][a-z0-9_]*$/;\n\n/** Matches http(s) scheme at start of URL */\nconst HTTP_SCHEME_RE = /^https?:\\/\\//i;\n\n/** Validates that a URL string uses http or https scheme. Rejects javascript:/data: URI XSS vectors. */\nexport const httpUrl = z\n\t.string()\n\t.url()\n\t.refine((url) => HTTP_SCHEME_RE.test(url), \"URL must use http or https\");\n\n/** BCP 47 locale code — language with optional script/region subtags (e.g. en, en-US, pt-BR, es-419, zh-Hant) */\nexport const localeCode = z\n\t.string()\n\t.regex(/^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i, \"Invalid locale code\")\n\t.transform((v) => v.toLowerCase());\n\n// ---------------------------------------------------------------------------\n// OpenAPI: Shared response schemas\n// ---------------------------------------------------------------------------\n\n/** Standard API error response */\nexport const apiErrorSchema = z\n\t.object({\n\t\terror: z.object({\n\t\t\tcode: z.string().meta({ description: \"Machine-readable error code\", example: \"NOT_FOUND\" }),\n\t\t\tmessage: z.string().meta({ description: \"Human-readable error message\" }),\n\t\t}),\n\t})\n\t.meta({ id: \"ApiError\" });\n\n/** Wrap a data schema in the standard success envelope: { data: T } */\nexport function successEnvelope<T extends z.ZodType>(dataSchema: T) {\n\treturn z.object({ data: dataSchema });\n}\n\n/** Standard delete response */\nexport const deleteResponseSchema = z.object({ deleted: z.literal(true) }).meta({\n\tid: \"DeleteResponse\",\n});\n\n/** Standard count response */\nexport const countResponseSchema = z\n\t.object({ count: z.number().int().min(0) })\n\t.meta({ id: \"CountResponse\" });\n","import { z } from \"zod\";\n\nimport { cursorPaginationQuery, httpUrl } from \"./common.js\";\n\n/** Slug pattern: lowercase letters, digits, and hyphens; must start with a letter */\nconst bylineSlugPattern = /^[a-z][a-z0-9-]*$/;\n\nexport const bylineSummarySchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tslug: z.string(),\n\t\tdisplayName: z.string(),\n\t\tbio: z.string().nullable(),\n\t\tavatarMediaId: z.string().nullable(),\n\t\twebsiteUrl: z.string().nullable(),\n\t\tuserId: z.string().nullable(),\n\t\tisGuest: z.boolean(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t})\n\t.meta({ id: \"BylineSummary\" });\n\nexport const bylineCreditSchema = z\n\t.object({\n\t\tbyline: bylineSummarySchema,\n\t\tsortOrder: z.number().int(),\n\t\troleLabel: z.string().nullable(),\n\t\tsource: z.enum([\"explicit\", \"inferred\"]).optional().meta({\n\t\t\tdescription: \"Whether this credit was explicitly assigned or inferred from authorId\",\n\t\t}),\n\t})\n\t.meta({ id: \"BylineCredit\" });\n\nexport const contentBylineInputSchema = z\n\t.object({\n\t\tbylineId: z.string().min(1),\n\t\troleLabel: z.string().nullish(),\n\t})\n\t.meta({ id: \"ContentBylineInput\" });\n\nexport const bylinesListQuery = cursorPaginationQuery\n\t.extend({\n\t\tsearch: z.string().optional(),\n\t\tisGuest: z.coerce.boolean().optional(),\n\t\tuserId: z.string().optional(),\n\t})\n\t.meta({ id: \"BylinesListQuery\" });\n\nexport const bylineCreateBody = z\n\t.object({\n\t\tslug: z\n\t\t\t.string()\n\t\t\t.min(1)\n\t\t\t.regex(bylineSlugPattern, \"Slug must contain only lowercase letters, digits, and hyphens\"),\n\t\tdisplayName: z.string().min(1),\n\t\tbio: z.string().nullish(),\n\t\tavatarMediaId: z.string().nullish(),\n\t\twebsiteUrl: httpUrl.nullish(),\n\t\tuserId: z.string().nullish(),\n\t\tisGuest: z.boolean().optional(),\n\t})\n\t.meta({ id: \"BylineCreateBody\" });\n\nexport const bylineUpdateBody = z\n\t.object({\n\t\tslug: z\n\t\t\t.string()\n\t\t\t.min(1)\n\t\t\t.regex(bylineSlugPattern, \"Slug must contain only lowercase letters, digits, and hyphens\")\n\t\t\t.optional(),\n\t\tdisplayName: z.string().min(1).optional(),\n\t\tbio: z.string().nullish(),\n\t\tavatarMediaId: z.string().nullish(),\n\t\twebsiteUrl: httpUrl.nullish(),\n\t\tuserId: z.string().nullish(),\n\t\tisGuest: z.boolean().optional(),\n\t})\n\t.meta({ id: \"BylineUpdateBody\" });\n\nexport const bylineListResponseSchema = z\n\t.object({\n\t\titems: z.array(bylineSummarySchema),\n\t\tnextCursor: z.string().optional(),\n\t})\n\t.meta({ id: \"BylineListResponse\" });\n","import { z } from \"zod\";\n\nimport { bylineSummarySchema, bylineCreditSchema, contentBylineInputSchema } from \"./bylines.js\";\nimport { cursorPaginationQuery, httpUrl, localeCode } from \"./common.js\";\n\n// ---------------------------------------------------------------------------\n// Content: Input schemas\n// ---------------------------------------------------------------------------\n\n/** SEO input — per-content meta fields */\nexport const contentSeoInput = z\n\t.object({\n\t\ttitle: z.string().max(200).nullish(),\n\t\tdescription: z.string().max(500).nullish(),\n\t\timage: z.string().nullish(),\n\t\tcanonical: httpUrl.nullish(),\n\t\tnoIndex: z.boolean().optional(),\n\t})\n\t.meta({ id: \"ContentSeoInput\" });\n\nexport const contentListQuery = cursorPaginationQuery\n\t.extend({\n\t\tstatus: z.string().optional(),\n\t\torderBy: z.string().optional(),\n\t\torder: z.enum([\"asc\", \"desc\"]).optional(),\n\t\tlocale: localeCode.optional(),\n\t})\n\t.meta({ id: \"ContentListQuery\" });\n\nexport const contentCreateBody = z\n\t.object({\n\t\tdata: z.record(z.string(), z.unknown()),\n\t\tslug: z.string().nullish(),\n\t\tstatus: z.enum([\"draft\"]).optional(),\n\t\tbylines: z.array(contentBylineInputSchema).optional(),\n\t\tlocale: localeCode.optional(),\n\t\ttranslationOf: z.string().optional(),\n\t\tseo: contentSeoInput.optional(),\n\t})\n\t.meta({ id: \"ContentCreateBody\" });\n\nexport const contentUpdateBody = z\n\t.object({\n\t\tdata: z.record(z.string(), z.unknown()).optional(),\n\t\tslug: z.string().nullish(),\n\t\tstatus: z.enum([\"draft\"]).optional(),\n\t\tauthorId: z.string().nullish(),\n\t\tbylines: z.array(contentBylineInputSchema).optional(),\n\t\t_rev: z\n\t\t\t.string()\n\t\t\t.optional()\n\t\t\t.meta({ description: \"Opaque revision token for optimistic concurrency\" }),\n\t\tskipRevision: z.boolean().optional(),\n\t\tseo: contentSeoInput.optional(),\n\t})\n\t.meta({ id: \"ContentUpdateBody\" });\n\nexport const contentScheduleBody = z\n\t.object({\n\t\tscheduledAt: z.string().min(1, \"scheduledAt is required\").meta({\n\t\t\tdescription: \"ISO 8601 datetime for scheduled publishing\",\n\t\t\texample: \"2025-06-15T09:00:00Z\",\n\t\t}),\n\t})\n\t.meta({ id: \"ContentScheduleBody\" });\n\nexport const contentPreviewUrlBody = z\n\t.object({\n\t\texpiresIn: z.union([z.string(), z.number()]).optional(),\n\t\tpathPattern: z.string().optional(),\n\t})\n\t.meta({ id: \"ContentPreviewUrlBody\" });\n\nexport const contentTermsBody = z\n\t.object({\n\t\ttermIds: z.array(z.string()),\n\t})\n\t.meta({ id: \"ContentTermsBody\" });\n\nexport const contentTrashQuery = cursorPaginationQuery;\n\n// ---------------------------------------------------------------------------\n// Content: Response schemas\n// ---------------------------------------------------------------------------\n\n/** SEO metadata on a content item */\nexport const contentSeoSchema = z\n\t.object({\n\t\ttitle: z.string().nullable(),\n\t\tdescription: z.string().nullable(),\n\t\timage: z.string().nullable(),\n\t\tcanonical: z.string().nullable(),\n\t\tnoIndex: z.boolean(),\n\t})\n\t.meta({ id: \"ContentSeo\" });\n\n/** A single content item as returned by the API */\nexport const contentItemSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\ttype: z.string().meta({ description: \"Collection slug this item belongs to\" }),\n\t\tslug: z.string().nullable(),\n\t\tstatus: z.string().meta({ description: \"draft, published, or scheduled\" }),\n\t\tdata: z.record(z.string(), z.unknown()).meta({\n\t\t\tdescription: \"User-defined field values\",\n\t\t}),\n\t\tauthorId: z.string().nullable(),\n\t\tprimaryBylineId: z.string().nullable(),\n\t\tbyline: bylineSummarySchema.nullable().optional(),\n\t\tbylines: z.array(bylineCreditSchema).optional(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t\tpublishedAt: z.string().nullable(),\n\t\tscheduledAt: z.string().nullable(),\n\t\tliveRevisionId: z.string().nullable(),\n\t\tdraftRevisionId: z.string().nullable(),\n\t\tversion: z.number().int(),\n\t\tlocale: z.string().nullable(),\n\t\ttranslationGroup: z.string().nullable(),\n\t\tseo: contentSeoSchema.optional(),\n\t})\n\t.meta({ id: \"ContentItem\" });\n\n/** Response for single content item endpoints (get, create, update) */\nexport const contentResponseSchema = z\n\t.object({\n\t\titem: contentItemSchema,\n\t\t_rev: z\n\t\t\t.string()\n\t\t\t.optional()\n\t\t\t.meta({ description: \"Opaque revision token for optimistic concurrency\" }),\n\t})\n\t.meta({ id: \"ContentResponse\" });\n\n/** Response for content list endpoints */\nexport const contentListResponseSchema = z\n\t.object({\n\t\titems: z.array(contentItemSchema),\n\t\tnextCursor: z.string().optional(),\n\t})\n\t.meta({ id: \"ContentListResponse\" });\n\n/** Trashed content item */\nexport const trashedContentItemSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\ttype: z.string(),\n\t\tslug: z.string().nullable(),\n\t\tstatus: z.string(),\n\t\tdata: z.record(z.string(), z.unknown()),\n\t\tauthorId: z.string().nullable(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t\tpublishedAt: z.string().nullable(),\n\t\tdeletedAt: z.string(),\n\t})\n\t.meta({ id: \"TrashedContentItem\" });\n\n/** Response for trashed content list */\nexport const trashedContentListResponseSchema = z\n\t.object({\n\t\titems: z.array(trashedContentItemSchema),\n\t\tnextCursor: z.string().optional(),\n\t})\n\t.meta({ id: \"TrashedContentListResponse\" });\n\n/** Response for content compare (live vs draft) */\nexport const contentCompareResponseSchema = z\n\t.object({\n\t\thasChanges: z.boolean(),\n\t\tlive: z.record(z.string(), z.unknown()).nullable(),\n\t\tdraft: z.record(z.string(), z.unknown()).nullable(),\n\t})\n\t.meta({ id: \"ContentCompareResponse\" });\n\n/** Translation summary for a content item */\nexport const contentTranslationSchema = z.object({\n\tid: z.string(),\n\tlocale: z.string().nullable(),\n\tslug: z.string().nullable(),\n\tstatus: z.string(),\n\tupdatedAt: z.string(),\n});\n\n/** Response for content translations endpoint */\nexport const contentTranslationsResponseSchema = z\n\t.object({\n\t\ttranslationGroup: z.string(),\n\t\ttranslations: z.array(contentTranslationSchema),\n\t})\n\t.meta({ id: \"ContentTranslationsResponse\" });\n","import { z } from \"zod\";\n\nimport { cursorPaginationQuery } from \"./common.js\";\n\n// ---------------------------------------------------------------------------\n// Media: Input schemas\n// ---------------------------------------------------------------------------\n\nexport const mediaListQuery = cursorPaginationQuery\n\t.extend({\n\t\tmimeType: z.string().optional(),\n\t})\n\t.meta({ id: \"MediaListQuery\" });\n\nexport const mediaUpdateBody = z\n\t.object({\n\t\talt: z.string().optional(),\n\t\tcaption: z.string().optional(),\n\t\twidth: z.number().int().positive().optional(),\n\t\theight: z.number().int().positive().optional(),\n\t})\n\t.meta({ id: \"MediaUpdateBody\" });\n\n/** Maximum allowed file upload size (50 MB). */\nconst MAX_UPLOAD_SIZE = 50 * 1024 * 1024;\n\nexport const mediaUploadUrlBody = z\n\t.object({\n\t\tfilename: z.string().min(1, \"filename is required\"),\n\t\tcontentType: z.string().min(1, \"contentType is required\"),\n\t\tsize: z\n\t\t\t.number()\n\t\t\t.int()\n\t\t\t.positive()\n\t\t\t.max(MAX_UPLOAD_SIZE, `File size must not exceed ${MAX_UPLOAD_SIZE / 1024 / 1024}MB`),\n\t\tcontentHash: z.string().optional(),\n\t})\n\t.meta({ id: \"MediaUploadUrlBody\" });\n\nexport const mediaConfirmBody = z\n\t.object({\n\t\tsize: z.number().int().positive().optional(),\n\t\twidth: z.number().int().positive().optional(),\n\t\theight: z.number().int().positive().optional(),\n\t})\n\t.meta({ id: \"MediaConfirmBody\" });\n\nexport const mediaProviderListQuery = cursorPaginationQuery\n\t.extend({\n\t\tquery: z.string().optional(),\n\t\tmimeType: z.string().optional(),\n\t})\n\t.meta({ id: \"MediaProviderListQuery\" });\n\n// ---------------------------------------------------------------------------\n// Media: Response schemas\n// ---------------------------------------------------------------------------\n\nconst mediaStatusSchema = z.enum([\"pending\", \"ready\", \"failed\"]);\n\nexport const mediaItemSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tfilename: z.string(),\n\t\tmimeType: z.string(),\n\t\tsize: z.number().nullable(),\n\t\twidth: z.number().nullable(),\n\t\theight: z.number().nullable(),\n\t\talt: z.string().nullable(),\n\t\tcaption: z.string().nullable(),\n\t\tstorageKey: z.string(),\n\t\tstatus: mediaStatusSchema,\n\t\tcontentHash: z.string().nullable(),\n\t\tblurhash: z.string().nullable(),\n\t\tdominantColor: z.string().nullable(),\n\t\tcreatedAt: z.string(),\n\t\tauthorId: z.string().nullable(),\n\t})\n\t.meta({ id: \"MediaItem\" });\n\nexport const mediaResponseSchema = z\n\t.object({ item: mediaItemSchema })\n\t.meta({ id: \"MediaResponse\" });\n\nexport const mediaListResponseSchema = z\n\t.object({\n\t\titems: z.array(mediaItemSchema),\n\t\tnextCursor: z.string().optional(),\n\t})\n\t.meta({ id: \"MediaListResponse\" });\n\nexport const mediaUploadUrlResponseSchema = z\n\t.object({\n\t\tuploadUrl: z.string(),\n\t\tmethod: z.literal(\"PUT\"),\n\t\theaders: z.record(z.string(), z.string()),\n\t\tmediaId: z.string(),\n\t\tstorageKey: z.string(),\n\t\texpiresAt: z.string(),\n\t})\n\t.meta({ id: \"MediaUploadUrlResponse\" });\n\nexport const mediaExistingResponseSchema = z\n\t.object({\n\t\texisting: z.literal(true),\n\t\tmediaId: z.string(),\n\t\tstorageKey: z.string(),\n\t\turl: z.string(),\n\t})\n\t.meta({ id: \"MediaExistingResponse\" });\n\nexport const mediaConfirmResponseSchema = z\n\t.object({\n\t\titem: mediaItemSchema.extend({ url: z.string() }),\n\t})\n\t.meta({ id: \"MediaConfirmResponse\" });\n","import { z } from \"zod\";\n\nimport { slugPattern } from \"./common.js\";\n\n// ---------------------------------------------------------------------------\n// Schema (collections & fields): Input schemas\n// ---------------------------------------------------------------------------\n\nconst collectionSupportValues = z.enum([\"drafts\", \"revisions\", \"preview\", \"scheduling\", \"search\"]);\n\nconst collectionSourcePattern = /^(template:.+|import:.+|manual|discovered|seed)$/;\n\nconst fieldTypeValues = z.enum([\n\t\"string\",\n\t\"text\",\n\t\"number\",\n\t\"integer\",\n\t\"boolean\",\n\t\"datetime\",\n\t\"select\",\n\t\"multiSelect\",\n\t\"portableText\",\n\t\"image\",\n\t\"file\",\n\t\"reference\",\n\t\"json\",\n\t\"slug\",\n\t\"repeater\",\n]);\n\nconst repeaterSubFieldSchema = z.object({\n\tslug: z.string().min(1).max(63).regex(slugPattern, \"Invalid slug format\"),\n\ttype: z.enum([\"string\", \"text\", \"number\", \"integer\", \"boolean\", \"datetime\", \"select\"]),\n\tlabel: z.string().min(1),\n\trequired: z.boolean().optional(),\n\toptions: z.array(z.string()).optional(),\n});\n\nconst fieldValidation = z\n\t.object({\n\t\trequired: z.boolean().optional(),\n\t\tmin: z.number().optional(),\n\t\tmax: z.number().optional(),\n\t\tminLength: z.number().int().min(0).optional(),\n\t\tmaxLength: z.number().int().min(0).optional(),\n\t\tpattern: z.string().optional(),\n\t\toptions: z.array(z.string()).optional(),\n\t\tsubFields: z.array(repeaterSubFieldSchema).min(1).optional(),\n\t\tminItems: z.number().int().min(0).optional(),\n\t\tmaxItems: z.number().int().min(1).optional(),\n\t})\n\t.optional();\n\nconst fieldWidgetOptions = z.record(z.string(), z.unknown()).optional();\n\nexport const createCollectionBody = z\n\t.object({\n\t\tslug: z.string().min(1).max(63).regex(slugPattern, \"Invalid slug format\"),\n\t\tlabel: z.string().min(1),\n\t\tlabelSingular: z.string().optional(),\n\t\tdescription: z.string().optional(),\n\t\ticon: z.string().optional(),\n\t\tsupports: z.array(collectionSupportValues).optional(),\n\t\tsource: z.string().regex(collectionSourcePattern).optional(),\n\t\turlPattern: z.string().optional(),\n\t\thasSeo: z.boolean().optional(),\n\t})\n\t.meta({ id: \"CreateCollectionBody\" });\n\nexport const updateCollectionBody = z\n\t.object({\n\t\tlabel: z.string().min(1).optional(),\n\t\tlabelSingular: z.string().optional(),\n\t\tdescription: z.string().optional(),\n\t\ticon: z.string().optional(),\n\t\tsupports: z.array(collectionSupportValues).optional(),\n\t\turlPattern: z.string().nullish(),\n\t\thasSeo: z.boolean().optional(),\n\t\tcommentsEnabled: z.boolean().optional(),\n\t\tcommentsModeration: z.enum([\"all\", \"first_time\", \"none\"]).optional(),\n\t\tcommentsClosedAfterDays: z.number().int().min(0).optional(),\n\t\tcommentsAutoApproveUsers: z.boolean().optional(),\n\t})\n\t.meta({ id: \"UpdateCollectionBody\" });\n\nexport const createFieldBody = z\n\t.object({\n\t\tslug: z.string().min(1).max(63).regex(slugPattern, \"Invalid slug format\"),\n\t\tlabel: z.string().min(1),\n\t\ttype: fieldTypeValues,\n\t\trequired: z.boolean().optional(),\n\t\tunique: z.boolean().optional(),\n\t\tdefaultValue: z.unknown().optional(),\n\t\tvalidation: fieldValidation,\n\t\twidget: z.string().optional(),\n\t\toptions: fieldWidgetOptions,\n\t\tsortOrder: z.number().int().min(0).optional(),\n\t\tsearchable: z.boolean().optional(),\n\t\ttranslatable: z.boolean().optional(),\n\t})\n\t.meta({ id: \"CreateFieldBody\" });\n\nexport const updateFieldBody = z\n\t.object({\n\t\tlabel: z.string().min(1).optional(),\n\t\trequired: z.boolean().optional(),\n\t\tunique: z.boolean().optional(),\n\t\tdefaultValue: z.unknown().optional(),\n\t\tvalidation: fieldValidation,\n\t\twidget: z.string().optional(),\n\t\toptions: fieldWidgetOptions,\n\t\tsortOrder: z.number().int().min(0).optional(),\n\t\tsearchable: z.boolean().optional(),\n\t\ttranslatable: z.boolean().optional(),\n\t})\n\t.meta({ id: \"UpdateFieldBody\" });\n\nexport const fieldReorderBody = z\n\t.object({\n\t\tfieldSlugs: z.array(z.string().min(1)),\n\t})\n\t.meta({ id: \"FieldReorderBody\" });\n\nexport const orphanRegisterBody = z\n\t.object({\n\t\tlabel: z.string().optional(),\n\t\tlabelSingular: z.string().optional(),\n\t\tdescription: z.string().optional(),\n\t})\n\t.meta({ id: \"OrphanRegisterBody\" });\n\nexport const schemaExportQuery = z.object({\n\tformat: z.string().optional(),\n});\n\nexport const collectionGetQuery = z.object({\n\tincludeFields: z\n\t\t.string()\n\t\t.transform((v) => v === \"true\")\n\t\t.optional(),\n});\n\n// ---------------------------------------------------------------------------\n// Schema: Response schemas\n// ---------------------------------------------------------------------------\n\nexport const collectionSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tslug: z.string(),\n\t\tlabel: z.string(),\n\t\tlabelSingular: z.string().nullable(),\n\t\tdescription: z.string().nullable(),\n\t\ticon: z.string().nullable(),\n\t\tsupports: z.array(z.string()),\n\t\tsource: z.string().nullable(),\n\t\turlPattern: z.string().nullable(),\n\t\thasSeo: z.boolean(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t})\n\t.meta({ id: \"Collection\" });\n\nexport const fieldSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tcollectionId: z.string(),\n\t\tslug: z.string(),\n\t\tlabel: z.string(),\n\t\ttype: fieldTypeValues,\n\t\trequired: z.boolean(),\n\t\tunique: z.boolean(),\n\t\tdefaultValue: z.unknown().nullable(),\n\t\tvalidation: z.record(z.string(), z.unknown()).nullable(),\n\t\twidget: z.string().nullable(),\n\t\toptions: z.record(z.string(), z.unknown()).nullable(),\n\t\tsortOrder: z.number().int(),\n\t\tsearchable: z.boolean(),\n\t\ttranslatable: z.boolean(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t})\n\t.meta({ id: \"Field\" });\n\nexport const collectionResponseSchema = z\n\t.object({ item: collectionSchema })\n\t.meta({ id: \"CollectionResponse\" });\n\nexport const collectionWithFieldsResponseSchema = z\n\t.object({\n\t\titem: collectionSchema.extend({ fields: z.array(fieldSchema) }),\n\t})\n\t.meta({ id: \"CollectionWithFieldsResponse\" });\n\nexport const collectionListResponseSchema = z\n\t.object({ items: z.array(collectionSchema) })\n\t.meta({ id: \"CollectionListResponse\" });\n\nexport const fieldResponseSchema = z.object({ item: fieldSchema }).meta({ id: \"FieldResponse\" });\n\nexport const fieldListResponseSchema = z\n\t.object({ items: z.array(fieldSchema) })\n\t.meta({ id: \"FieldListResponse\" });\n\nexport const orphanedTableSchema = z\n\t.object({\n\t\tslug: z.string(),\n\t\ttableName: z.string(),\n\t\trowCount: z.number().int(),\n\t})\n\t.meta({ id: \"OrphanedTable\" });\n\nexport const orphanedTableListResponseSchema = z\n\t.object({ items: z.array(orphanedTableSchema) })\n\t.meta({ id: \"OrphanedTableListResponse\" });\n","import { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Comments: Input schemas\n// ---------------------------------------------------------------------------\n\nexport const createCommentBody = z\n\t.object({\n\t\tauthorName: z.string().min(1).max(100),\n\t\tauthorEmail: z.string().email(),\n\t\tbody: z.string().min(1).max(5000),\n\t\tparentId: z.string().optional(),\n\t\t/** Honeypot field — hidden in the form, filled only by bots */\n\t\twebsite_url: z.string().optional(),\n\t})\n\t.meta({ id: \"CreateCommentBody\" });\n\nexport const commentStatusBody = z\n\t.object({\n\t\tstatus: z.enum([\"approved\", \"pending\", \"spam\", \"trash\"]),\n\t})\n\t.meta({ id: \"CommentStatusBody\" });\n\nexport const commentBulkBody = z\n\t.object({\n\t\tids: z.array(z.string().min(1)).min(1).max(100),\n\t\taction: z.enum([\"approve\", \"spam\", \"trash\", \"delete\"]),\n\t})\n\t.meta({ id: \"CommentBulkBody\" });\n\nexport const commentListQuery = z\n\t.object({\n\t\tstatus: z.enum([\"pending\", \"approved\", \"spam\", \"trash\"]).optional(),\n\t\tcollection: z.string().optional(),\n\t\tsearch: z.string().optional(),\n\t\tlimit: z.coerce.number().int().min(1).max(100).optional(),\n\t\tcursor: z.string().optional(),\n\t})\n\t.meta({ id: \"CommentListQuery\" });\n\n// ---------------------------------------------------------------------------\n// Comments: Response schemas\n// ---------------------------------------------------------------------------\n\nconst commentStatusValues = z.enum([\"pending\", \"approved\", \"spam\", \"trash\"]);\n\n/**\n * Public-facing comment (no email/IP).\n *\n * `replies` is recursive in practice (each reply can have replies), but we\n * model it as a single level here to avoid circular type inference issues\n * with tsgo. OpenAPI consumers should treat replies as the same shape.\n */\nexport const publicCommentSchema: z.ZodObject<{\n\tid: z.ZodString;\n\tauthorName: z.ZodString;\n\tisRegisteredUser: z.ZodBoolean;\n\tbody: z.ZodString;\n\tparentId: z.ZodNullable<z.ZodString>;\n\tcreatedAt: z.ZodString;\n\treplies: z.ZodOptional<z.ZodArray<z.ZodAny>>;\n}> = z\n\t.object({\n\t\tid: z.string(),\n\t\tauthorName: z.string(),\n\t\tisRegisteredUser: z.boolean(),\n\t\tbody: z.string(),\n\t\tparentId: z.string().nullable(),\n\t\tcreatedAt: z.string(),\n\t\treplies: z.array(z.any()).optional(),\n\t})\n\t.meta({ id: \"PublicComment\" });\n\n/** Admin comment with full details */\nexport const commentSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tcollection: z.string(),\n\t\tcontentId: z.string(),\n\t\tauthorName: z.string(),\n\t\tauthorEmail: z.string(),\n\t\tbody: z.string(),\n\t\tstatus: commentStatusValues,\n\t\tparentId: z.string().nullable(),\n\t\tipHash: z.string().nullable(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t})\n\t.meta({ id: \"Comment\" });\n\nexport const publicCommentListResponseSchema = z\n\t.object({\n\t\titems: z.array(publicCommentSchema),\n\t\tnextCursor: z.string().optional(),\n\t\ttotal: z.number().int(),\n\t})\n\t.meta({ id: \"PublicCommentListResponse\" });\n\nexport const adminCommentListResponseSchema = z\n\t.object({\n\t\titems: z.array(commentSchema),\n\t\tnextCursor: z.string().optional(),\n\t})\n\t.meta({ id: \"AdminCommentListResponse\" });\n\nexport const commentCountsResponseSchema = z\n\t.object({\n\t\tpending: z.number().int(),\n\t\tapproved: z.number().int(),\n\t\tspam: z.number().int(),\n\t\ttrash: z.number().int(),\n\t})\n\t.meta({ id: \"CommentCountsResponse\" });\n\nexport const commentBulkResponseSchema = z\n\t.object({ affected: z.number().int() })\n\t.meta({ id: \"CommentBulkResponse\" });\n","import { z } from \"zod\";\n\nimport { roleLevel } from \"./common.js\";\n\n// ---------------------------------------------------------------------------\n// WebAuthn credential schemas (matching @emdash-cms/auth/passkey types)\n// ---------------------------------------------------------------------------\n\nconst authenticatorTransport = z.enum([\"usb\", \"nfc\", \"ble\", \"internal\", \"hybrid\"]);\n\n/** RegistrationResponse — sent by the browser after navigator.credentials.create() */\nconst registrationCredential = z.object({\n\tid: z.string(),\n\trawId: z.string(),\n\ttype: z.literal(\"public-key\"),\n\tresponse: z.object({\n\t\tclientDataJSON: z.string(),\n\t\tattestationObject: z.string(),\n\t\ttransports: z.array(authenticatorTransport).optional(),\n\t}),\n\tauthenticatorAttachment: z.enum([\"platform\", \"cross-platform\"]).optional(),\n});\n\n/** AuthenticationResponse — sent by the browser after navigator.credentials.get() */\nconst authenticationCredential = z.object({\n\tid: z.string(),\n\trawId: z.string(),\n\ttype: z.literal(\"public-key\"),\n\tresponse: z.object({\n\t\tclientDataJSON: z.string(),\n\t\tauthenticatorData: z.string(),\n\t\tsignature: z.string(),\n\t\tuserHandle: z.string().optional(),\n\t}),\n\tauthenticatorAttachment: z.enum([\"platform\", \"cross-platform\"]).optional(),\n});\n\n// ---------------------------------------------------------------------------\n// Auth: Input schemas\n// ---------------------------------------------------------------------------\n\nexport const signupRequestBody = z\n\t.object({\n\t\temail: z.string().email(),\n\t})\n\t.meta({ id: \"SignupRequestBody\" });\n\nexport const signupCompleteBody = z\n\t.object({\n\t\ttoken: z.string().min(1),\n\t\tcredential: registrationCredential,\n\t\tname: z.string().optional(),\n\t})\n\t.meta({ id: \"SignupCompleteBody\" });\n\nexport const inviteCreateBody = z\n\t.object({\n\t\temail: z.string().email(),\n\t\trole: roleLevel.optional(),\n\t})\n\t.meta({ id: \"InviteCreateBody\" });\n\nexport const inviteCompleteBody = z\n\t.object({\n\t\ttoken: z.string().min(1),\n\t\tcredential: registrationCredential,\n\t\tname: z.string().optional(),\n\t})\n\t.meta({ id: \"InviteCompleteBody\" });\n\nexport const magicLinkSendBody = z\n\t.object({\n\t\temail: z.string().email(),\n\t})\n\t.meta({ id: \"MagicLinkSendBody\" });\n\nexport const passkeyOptionsBody = z\n\t.object({\n\t\temail: z.string().email().optional(),\n\t})\n\t.meta({ id: \"PasskeyOptionsBody\" });\n\nexport const passkeyVerifyBody = z\n\t.object({\n\t\tcredential: authenticationCredential,\n\t})\n\t.meta({ id: \"PasskeyVerifyBody\" });\n\nexport const passkeyRegisterOptionsBody = z\n\t.object({\n\t\tname: z.string().optional(),\n\t})\n\t.meta({ id: \"PasskeyRegisterOptionsBody\" });\n\nexport const passkeyRegisterVerifyBody = z\n\t.object({\n\t\tcredential: registrationCredential,\n\t\tname: z.string().optional(),\n\t})\n\t.meta({ id: \"PasskeyRegisterVerifyBody\" });\n\nexport const passkeyRenameBody = z\n\t.object({\n\t\tname: z.string().min(1),\n\t})\n\t.meta({ id: \"PasskeyRenameBody\" });\n\nexport const authMeActionBody = z\n\t.object({\n\t\taction: z.string().min(1),\n\t})\n\t.meta({ id: \"AuthMeActionBody\" });\n","/**\n * URL scheme validation utilities\n *\n * Prevents XSS via dangerous URL schemes (javascript:, data:, vbscript:, etc.)\n * by allowlisting known-safe schemes before rendering into href attributes.\n */\n\n/**\n * Matches URLs that are safe to render in href attributes.\n *\n * Allowed:\n * - http:// and https://\n * - mailto: and tel:\n * - Relative paths (starting with /)\n * - Fragment links (starting with #)\n * - Protocol-relative URLs are NOT allowed (starting with //) as they can\n * redirect to attacker-controlled hosts.\n */\nconst SAFE_URL_SCHEME_RE = /^(https?:|mailto:|tel:|\\/(?!\\/)|#)/i;\n\n/**\n * Returns the URL unchanged if it uses a safe scheme, otherwise returns \"#\".\n *\n * Use this at the render layer as the primary defense against XSS via\n * dangerous URL schemes like `javascript:`, `data:`, or `vbscript:`.\n *\n * @example\n * ```ts\n * sanitizeHref(\"https://example.com\") // \"https://example.com\"\n * sanitizeHref(\"/about\") // \"/about\"\n * sanitizeHref(\"#section\") // \"#section\"\n * sanitizeHref(\"mailto:a@b.com\") // \"mailto:a@b.com\"\n * sanitizeHref(\"javascript:alert(1)\") // \"#\"\n * sanitizeHref(\"data:text/html,<script>\") // \"#\"\n * sanitizeHref(\"\") // \"#\"\n * ```\n */\nexport function sanitizeHref(url: string | undefined | null): string {\n\tif (!url) return \"#\";\n\treturn SAFE_URL_SCHEME_RE.test(url) ? url : \"#\";\n}\n\n/**\n * Returns true if the URL uses a safe scheme for rendering in href attributes.\n */\nexport function isSafeHref(url: string): boolean {\n\treturn SAFE_URL_SCHEME_RE.test(url);\n}\n","import { z } from \"zod\";\n\nimport { isSafeHref } from \"../../utils/url.js\";\n\n// ---------------------------------------------------------------------------\n// Menus: Input schemas\n// ---------------------------------------------------------------------------\n\nconst menuItemType = z.string().min(1);\n\nconst safeHref = z\n\t.string()\n\t.trim()\n\t.refine(\n\t\tisSafeHref,\n\t\t\"URL must use http, https, mailto, tel, a relative path, or a fragment identifier\",\n\t);\n\nexport const createMenuBody = z\n\t.object({\n\t\tname: z.string().min(1),\n\t\tlabel: z.string().min(1),\n\t})\n\t.meta({ id: \"CreateMenuBody\" });\n\nexport const updateMenuBody = z\n\t.object({\n\t\tlabel: z.string().min(1).optional(),\n\t})\n\t.meta({ id: \"UpdateMenuBody\" });\n\nexport const createMenuItemBody = z\n\t.object({\n\t\ttype: menuItemType,\n\t\tlabel: z.string().min(1),\n\t\treferenceCollection: z.string().optional(),\n\t\treferenceId: z.string().optional(),\n\t\tcustomUrl: safeHref.optional(),\n\t\ttarget: z.string().optional(),\n\t\ttitleAttr: z.string().optional(),\n\t\tcssClasses: z.string().optional(),\n\t\tparentId: z.string().optional(),\n\t\tsortOrder: z.number().int().min(0).optional(),\n\t})\n\t.meta({ id: \"CreateMenuItemBody\" });\n\nexport const updateMenuItemBody = z\n\t.object({\n\t\tlabel: z.string().min(1).optional(),\n\t\tcustomUrl: safeHref.optional(),\n\t\ttarget: z.string().optional(),\n\t\ttitleAttr: z.string().optional(),\n\t\tcssClasses: z.string().optional(),\n\t\tparentId: z.string().nullish(),\n\t\tsortOrder: z.number().int().min(0).optional(),\n\t})\n\t.meta({ id: \"UpdateMenuItemBody\" });\n\nexport const menuItemDeleteQuery = z.object({\n\tid: z.string().min(1),\n});\n\nexport const menuItemUpdateQuery = z.object({\n\tid: z.string().min(1),\n});\n\nexport const reorderMenuItemsBody = z\n\t.object({\n\t\titems: z.array(\n\t\t\tz.object({\n\t\t\t\tid: z.string().min(1),\n\t\t\t\tparentId: z.string().nullable(),\n\t\t\t\tsortOrder: z.number().int().min(0),\n\t\t\t}),\n\t\t),\n\t})\n\t.meta({ id: \"ReorderMenuItemsBody\" });\n\n// ---------------------------------------------------------------------------\n// Menus: Response schemas\n// ---------------------------------------------------------------------------\n\nexport const menuSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tname: z.string(),\n\t\tlabel: z.string(),\n\t\tcreated_at: z.string(),\n\t\tupdated_at: z.string(),\n\t})\n\t.meta({ id: \"Menu\" });\n\nexport const menuItemSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tmenu_id: z.string(),\n\t\tparent_id: z.string().nullable(),\n\t\tsort_order: z.number().int(),\n\t\ttype: z.string(),\n\t\treference_collection: z.string().nullable(),\n\t\treference_id: z.string().nullable(),\n\t\tcustom_url: z.string().nullable(),\n\t\tlabel: z.string(),\n\t\ttitle_attr: z.string().nullable(),\n\t\ttarget: z.string().nullable(),\n\t\tcss_classes: z.string().nullable(),\n\t\tcreated_at: z.string(),\n\t})\n\t.meta({ id: \"MenuItem\" });\n\nexport const menuListItemSchema = menuSchema\n\t.extend({\n\t\titemCount: z.number().int(),\n\t})\n\t.meta({ id: \"MenuListItem\" });\n\nexport const menuWithItemsSchema = menuSchema\n\t.extend({\n\t\titems: z.array(menuItemSchema),\n\t})\n\t.meta({ id: \"MenuWithItems\" });\n","import { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Taxonomy definitions: Input schemas\n// ---------------------------------------------------------------------------\n\n/** Collection slug format: lowercase alphanumeric + underscores, starts with letter */\nconst collectionSlugPattern = /^[a-z][a-z0-9_]*$/;\n\nexport const createTaxonomyDefBody = z\n\t.object({\n\t\tname: z\n\t\t\t.string()\n\t\t\t.min(1)\n\t\t\t.max(63)\n\t\t\t.regex(/^[a-z][a-z0-9_]*$/, \"Name must be lowercase alphanumeric with underscores\"),\n\t\tlabel: z.string().min(1).max(200),\n\t\thierarchical: z.boolean().optional().default(false),\n\t\tcollections: z\n\t\t\t.array(\n\t\t\t\tz.string().min(1).max(63).regex(collectionSlugPattern, \"Invalid collection slug format\"),\n\t\t\t)\n\t\t\t.max(100)\n\t\t\t.optional()\n\t\t\t.default([]),\n\t})\n\t.meta({ id: \"CreateTaxonomyDefBody\" });\n\n// ---------------------------------------------------------------------------\n// Taxonomy terms: Input schemas\n// ---------------------------------------------------------------------------\n\nexport const createTermBody = z\n\t.object({\n\t\tslug: z.string().min(1),\n\t\tlabel: z.string().min(1),\n\t\tparentId: z.string().nullish(),\n\t\tdescription: z.string().optional(),\n\t})\n\t.meta({ id: \"CreateTermBody\" });\n\nexport const updateTermBody = z\n\t.object({\n\t\tslug: z.string().min(1).optional(),\n\t\tlabel: z.string().min(1).optional(),\n\t\tparentId: z.string().nullish(),\n\t\tdescription: z.string().optional(),\n\t})\n\t.meta({ id: \"UpdateTermBody\" });\n\n// ---------------------------------------------------------------------------\n// Taxonomies: Response schemas\n// ---------------------------------------------------------------------------\n\nexport const taxonomyDefSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tname: z.string(),\n\t\tlabel: z.string(),\n\t\tlabelSingular: z.string().optional(),\n\t\thierarchical: z.boolean(),\n\t\tcollections: z.array(z.string()),\n\t})\n\t.meta({ id: \"TaxonomyDef\" });\n\nexport const taxonomyListResponseSchema = z\n\t.object({ taxonomies: z.array(taxonomyDefSchema) })\n\t.meta({ id: \"TaxonomyListResponse\" });\n\nexport const termSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tname: z.string(),\n\t\tslug: z.string(),\n\t\tlabel: z.string(),\n\t\tparentId: z.string().nullable(),\n\t\tdescription: z.string().optional(),\n\t})\n\t.meta({ id: \"Term\" });\n\nexport const termWithCountSchema: z.ZodType = z\n\t.object({\n\t\tid: z.string(),\n\t\tname: z.string(),\n\t\tslug: z.string(),\n\t\tlabel: z.string(),\n\t\tparentId: z.string().nullable(),\n\t\tdescription: z.string().optional(),\n\t\tcount: z.number().int(),\n\t\tchildren: z.array(z.lazy(() => termWithCountSchema)),\n\t})\n\t.meta({ id: \"TermWithCount\" });\n\nexport const termListResponseSchema = z\n\t.object({ terms: z.array(termWithCountSchema) })\n\t.meta({ id: \"TermListResponse\" });\n\nexport const termResponseSchema = z.object({ term: termSchema }).meta({ id: \"TermResponse\" });\n\nexport const termGetResponseSchema = z\n\t.object({\n\t\tterm: termSchema.extend({\n\t\t\tcount: z.number().int(),\n\t\t\tchildren: z.array(\n\t\t\t\tz.object({\n\t\t\t\t\tid: z.string(),\n\t\t\t\t\tslug: z.string(),\n\t\t\t\t\tlabel: z.string(),\n\t\t\t\t}),\n\t\t\t),\n\t\t}),\n\t})\n\t.meta({ id: \"TermGetResponse\" });\n","import { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Sections: Input schemas\n// ---------------------------------------------------------------------------\n\nconst sectionSource = z.enum([\"theme\", \"user\", \"import\"]);\n\nexport const sectionsListQuery = z\n\t.object({\n\t\tsource: sectionSource.optional(),\n\t\tsearch: z.string().optional(),\n\t\tlimit: z.coerce.number().int().min(1).max(100).optional(),\n\t\tcursor: z.string().optional(),\n\t})\n\t.meta({ id: \"SectionsListQuery\" });\n\nexport const createSectionBody = z\n\t.object({\n\t\tslug: z.string().min(1),\n\t\ttitle: z.string().min(1),\n\t\tdescription: z.string().optional(),\n\t\tkeywords: z.array(z.string()).optional(),\n\t\tcontent: z.array(z.record(z.string(), z.unknown())),\n\t\tpreviewMediaId: z.string().optional(),\n\t\tsource: sectionSource.optional(),\n\t\tthemeId: z.string().optional(),\n\t})\n\t.meta({ id: \"CreateSectionBody\" });\n\nexport const updateSectionBody = z\n\t.object({\n\t\tslug: z.string().min(1).optional(),\n\t\ttitle: z.string().min(1).optional(),\n\t\tdescription: z.string().optional(),\n\t\tkeywords: z.array(z.string()).optional(),\n\t\tcontent: z.array(z.record(z.string(), z.unknown())).optional(),\n\t\tpreviewMediaId: z.string().nullish(),\n\t})\n\t.meta({ id: \"UpdateSectionBody\" });\n\n// ---------------------------------------------------------------------------\n// Sections: Response schemas\n// ---------------------------------------------------------------------------\n\nexport const sectionSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tslug: z.string(),\n\t\ttitle: z.string(),\n\t\tdescription: z.string().nullable(),\n\t\tkeywords: z.array(z.string()).nullable(),\n\t\tcontent: z.array(z.record(z.string(), z.unknown())),\n\t\tpreviewMediaId: z.string().nullable(),\n\t\tsource: z.string(),\n\t\tthemeId: z.string().nullable(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t})\n\t.meta({ id: \"Section\" });\n\nexport const sectionListResponseSchema = z\n\t.object({\n\t\titems: z.array(sectionSchema),\n\t\tnextCursor: z.string().optional(),\n\t})\n\t.meta({ id: \"SectionListResponse\" });\n","import { z } from \"zod\";\n\nimport { httpUrl } from \"./common.js\";\n\n// ---------------------------------------------------------------------------\n// Settings: Input schemas\n// ---------------------------------------------------------------------------\n\nconst mediaReference = z.object({\n\tmediaId: z.string(),\n\talt: z.string().optional(),\n});\n\nconst socialSettings = z.object({\n\ttwitter: z.string().optional(),\n\tgithub: z.string().optional(),\n\tfacebook: z.string().optional(),\n\tinstagram: z.string().optional(),\n\tlinkedin: z.string().optional(),\n\tyoutube: z.string().optional(),\n});\n\nconst seoSettings = z.object({\n\ttitleSeparator: z.string().max(10).optional(),\n\tdefaultOgImage: mediaReference.optional(),\n\trobotsTxt: z.string().max(5000).optional(),\n\tgoogleVerification: z.string().max(100).optional(),\n\tbingVerification: z.string().max(100).optional(),\n});\n\nexport const settingsUpdateBody = z\n\t.object({\n\t\ttitle: z.string().optional(),\n\t\ttagline: z.string().optional(),\n\t\tlogo: mediaReference.optional(),\n\t\tfavicon: mediaReference.optional(),\n\t\turl: z.union([httpUrl, z.literal(\"\")]).optional(),\n\t\tpostsPerPage: z.number().int().min(1).max(100).optional(),\n\t\tdateFormat: z.string().optional(),\n\t\ttimezone: z.string().optional(),\n\t\tsocial: socialSettings.optional(),\n\t\tseo: seoSettings.optional(),\n\t})\n\t.meta({ id: \"SettingsUpdateBody\" });\n\n// ---------------------------------------------------------------------------\n// Settings: Response schemas\n// ---------------------------------------------------------------------------\n\nexport const siteSettingsSchema = z\n\t.object({\n\t\ttitle: z.string().optional(),\n\t\ttagline: z.string().optional(),\n\t\tlogo: mediaReference.optional(),\n\t\tfavicon: mediaReference.optional(),\n\t\turl: z.string().optional(),\n\t\tpostsPerPage: z.number().int().optional(),\n\t\tdateFormat: z.string().optional(),\n\t\ttimezone: z.string().optional(),\n\t\tsocial: socialSettings.optional(),\n\t\tseo: seoSettings.optional(),\n\t})\n\t.meta({ id: \"SiteSettings\" });\n","import { z } from \"zod\";\n\nimport { localeCode } from \"./common.js\";\n\n// ---------------------------------------------------------------------------\n// Search: Input schemas\n// ---------------------------------------------------------------------------\n\nexport const searchQuery = z\n\t.object({\n\t\tq: z.string().min(1),\n\t\tcollections: z.string().optional(),\n\t\tstatus: z.string().optional(),\n\t\tlocale: localeCode.optional(),\n\t\tlimit: z.coerce.number().int().min(1).max(100).optional(),\n\t})\n\t.meta({ id: \"SearchQuery\" });\n\nexport const searchSuggestQuery = z\n\t.object({\n\t\tq: z.string().min(1),\n\t\tcollections: z.string().optional(),\n\t\tlocale: localeCode.optional(),\n\t\tlimit: z.coerce.number().int().min(1).max(20).optional(),\n\t})\n\t.meta({ id: \"SearchSuggestQuery\" });\n\nexport const searchRebuildBody = z\n\t.object({\n\t\tcollection: z.string().min(1),\n\t})\n\t.meta({ id: \"SearchRebuildBody\" });\n\nexport const searchEnableBody = z\n\t.object({\n\t\tcollection: z.string().min(1),\n\t\tenabled: z.boolean(),\n\t\tweights: z.record(z.string(), z.number()).optional(),\n\t})\n\t.meta({ id: \"SearchEnableBody\" });\n\n// ---------------------------------------------------------------------------\n// Search: Response schemas\n// ---------------------------------------------------------------------------\n\nexport const searchResultSchema = z\n\t.object({\n\t\tcollection: z.string(),\n\t\tid: z.string(),\n\t\tslug: z.string().nullable(),\n\t\tlocale: z.string(),\n\t\ttitle: z.string().optional(),\n\t\tsnippet: z.string().optional(),\n\t\tscore: z.number(),\n\t})\n\t.meta({ id: \"SearchResult\" });\n\nexport const searchResponseSchema = z\n\t.object({\n\t\titems: z.array(searchResultSchema),\n\t\tnextCursor: z.string().optional(),\n\t})\n\t.meta({ id: \"SearchResponse\" });\n","import { z } from \"zod\";\n\nimport { httpUrl } from \"./common.js\";\n\n// ---------------------------------------------------------------------------\n// Import\n// ---------------------------------------------------------------------------\n\nexport const importProbeBody = z.object({\n\turl: httpUrl,\n});\n\nexport const wpPluginAnalyzeBody = z.object({\n\turl: httpUrl,\n\ttoken: z.string().min(1),\n});\n\nexport const wpPluginExecuteBody = z.object({\n\turl: httpUrl,\n\ttoken: z.string().min(1),\n\tconfig: z.record(z.string(), z.unknown()),\n});\n\nexport const wpPrepareBody = z.object({\n\tpostTypes: z.array(\n\t\tz.object({\n\t\t\tname: z.string().min(1),\n\t\t\tcollection: z.string().min(1),\n\t\t\tfields: z\n\t\t\t\t.array(\n\t\t\t\t\tz.object({\n\t\t\t\t\t\tslug: z.string().min(1),\n\t\t\t\t\t\tlabel: z.string().min(1),\n\t\t\t\t\t\ttype: z.string().min(1),\n\t\t\t\t\t\trequired: z.boolean(),\n\t\t\t\t\t\tsearchable: z.boolean().optional(),\n\t\t\t\t\t}),\n\t\t\t\t)\n\t\t\t\t.optional(),\n\t\t}),\n\t),\n});\n\nexport const wpMediaImportBody = z.object({\n\tattachments: z.array(z.record(z.string(), z.unknown())),\n\tstream: z.boolean().optional(),\n});\n\nexport const wpRewriteUrlsBody = z.object({\n\turlMap: z.record(z.string(), z.string()),\n\tcollections: z.array(z.string()).optional(),\n});\n","import { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Setup\n// ---------------------------------------------------------------------------\n\n/** Registration credential — duplicated reference for setup flow.\n * The canonical definition lives in auth.ts but setup needs it independently\n * because setup runs before auth is configured. */\nconst authenticatorTransport = z.enum([\"usb\", \"nfc\", \"ble\", \"internal\", \"hybrid\"]);\n\nconst registrationCredential = z.object({\n\tid: z.string(),\n\trawId: z.string(),\n\ttype: z.literal(\"public-key\"),\n\tresponse: z.object({\n\t\tclientDataJSON: z.string(),\n\t\tattestationObject: z.string(),\n\t\ttransports: z.array(authenticatorTransport).optional(),\n\t}),\n\tauthenticatorAttachment: z.enum([\"platform\", \"cross-platform\"]).optional(),\n});\n\nexport const setupBody = z.object({\n\ttitle: z.string().min(1),\n\ttagline: z.string().optional(),\n\tincludeContent: z.boolean(),\n});\n\nexport const setupAdminBody = z.object({\n\temail: z.string().email(),\n\tname: z.string().optional(),\n});\n\nexport const setupAdminVerifyBody = z.object({\n\tcredential: registrationCredential,\n});\n","import { z } from \"zod\";\n\nimport { roleLevel } from \"./common.js\";\n\n// ---------------------------------------------------------------------------\n// Admin / Users: Input schemas\n// ---------------------------------------------------------------------------\n\nexport const usersListQuery = z\n\t.object({\n\t\tsearch: z.string().optional(),\n\t\trole: z.string().optional(),\n\t\tcursor: z.string().optional(),\n\t\tlimit: z.coerce.number().int().min(1).max(100).optional().default(50),\n\t})\n\t.meta({ id: \"UsersListQuery\" });\n\nexport const userUpdateBody = z\n\t.object({\n\t\tname: z.string().optional(),\n\t\temail: z.string().email().optional(),\n\t\trole: roleLevel.optional(),\n\t})\n\t.meta({ id: \"UserUpdateBody\" });\n\nexport const allowedDomainCreateBody = z\n\t.object({\n\t\tdomain: z.string().min(1),\n\t\tdefaultRole: roleLevel,\n\t})\n\t.meta({ id: \"AllowedDomainCreateBody\" });\n\nexport const allowedDomainUpdateBody = z\n\t.object({\n\t\tenabled: z.boolean().optional(),\n\t\tdefaultRole: roleLevel.optional(),\n\t})\n\t.meta({ id: \"AllowedDomainUpdateBody\" });\n\n// ---------------------------------------------------------------------------\n// Admin / Users: Response schemas\n// ---------------------------------------------------------------------------\n\nexport const userSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\temail: z.string(),\n\t\tname: z.string().nullable(),\n\t\tavatarUrl: z.string().nullable(),\n\t\trole: z.number().int(),\n\t\temailVerified: z.boolean(),\n\t\tdisabled: z.boolean(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t\tlastLogin: z.string().nullable(),\n\t\tcredentialCount: z.number().int().optional(),\n\t\toauthProviders: z.array(z.string()).optional(),\n\t})\n\t.meta({ id: \"User\" });\n\nexport const userListResponseSchema = z\n\t.object({\n\t\titems: z.array(userSchema),\n\t\tnextCursor: z.string().optional(),\n\t})\n\t.meta({ id: \"UserListResponse\" });\n\nexport const userDetailSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\temail: z.string(),\n\t\tname: z.string().nullable(),\n\t\tavatarUrl: z.string().nullable(),\n\t\trole: z.number().int(),\n\t\temailVerified: z.boolean(),\n\t\tdisabled: z.boolean(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t\tlastLogin: z.string().nullable(),\n\t\tcredentials: z.array(\n\t\t\tz.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tname: z.string().nullable(),\n\t\t\t\tdeviceType: z.string().nullable(),\n\t\t\t\tcreatedAt: z.string(),\n\t\t\t\tlastUsedAt: z.string(),\n\t\t\t}),\n\t\t),\n\t\toauthAccounts: z.array(\n\t\t\tz.object({\n\t\t\t\tprovider: z.string(),\n\t\t\t\tcreatedAt: z.string(),\n\t\t\t}),\n\t\t),\n\t})\n\t.meta({ id: \"UserDetail\" });\n","import { z } from \"zod\";\n\n// ---------------------------------------------------------------------------\n// Widgets: Input schemas\n// ---------------------------------------------------------------------------\n\nconst widgetType = z.enum([\"content\", \"menu\", \"component\"]);\n\nexport const createWidgetAreaBody = z\n\t.object({\n\t\tname: z.string().min(1),\n\t\tlabel: z.string().min(1),\n\t\tdescription: z.string().optional(),\n\t})\n\t.meta({ id: \"CreateWidgetAreaBody\" });\n\nexport const createWidgetBody = z\n\t.object({\n\t\ttype: widgetType,\n\t\ttitle: z.string().optional(),\n\t\tcontent: z.array(z.record(z.string(), z.unknown())).optional(),\n\t\tmenuName: z.string().optional(),\n\t\tcomponentId: z.string().optional(),\n\t\tcomponentProps: z.record(z.string(), z.unknown()).optional(),\n\t})\n\t.meta({ id: \"CreateWidgetBody\" });\n\nexport const updateWidgetBody = z\n\t.object({\n\t\ttype: widgetType.optional(),\n\t\ttitle: z.string().optional(),\n\t\tcontent: z.array(z.record(z.string(), z.unknown())).optional(),\n\t\tmenuName: z.string().optional(),\n\t\tcomponentId: z.string().optional(),\n\t\tcomponentProps: z.record(z.string(), z.unknown()).optional(),\n\t})\n\t.meta({ id: \"UpdateWidgetBody\" });\n\nexport const reorderWidgetsBody = z\n\t.object({\n\t\twidgetIds: z.array(z.string().min(1)),\n\t})\n\t.meta({ id: \"ReorderWidgetsBody\" });\n\n// ---------------------------------------------------------------------------\n// Widgets: Response schemas\n// ---------------------------------------------------------------------------\n\nexport const widgetAreaSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tname: z.string(),\n\t\tlabel: z.string(),\n\t\tdescription: z.string().nullable(),\n\t\tcreated_at: z.string(),\n\t\tupdated_at: z.string(),\n\t})\n\t.meta({ id: \"WidgetArea\" });\n\nexport const widgetSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tarea_id: z.string(),\n\t\ttype: z.string(),\n\t\ttitle: z.string().nullable(),\n\t\tcontent: z.string().nullable(),\n\t\tmenu_name: z.string().nullable(),\n\t\tcomponent_id: z.string().nullable(),\n\t\tcomponent_props: z.string().nullable(),\n\t\tsort_order: z.number().int(),\n\t\tcreated_at: z.string(),\n\t\tupdated_at: z.string(),\n\t})\n\t.meta({ id: \"Widget\" });\n\nexport const widgetAreaWithWidgetsSchema = widgetAreaSchema\n\t.extend({\n\t\twidgets: z.array(widgetSchema),\n\t})\n\t.meta({ id: \"WidgetAreaWithWidgets\" });\n","import { z } from \"zod\";\n\nimport { cursorPaginationQuery } from \"./common.js\";\n\n// ---------------------------------------------------------------------------\n// Redirects: Input schemas\n// ---------------------------------------------------------------------------\n\nconst redirectType = z.coerce\n\t.number()\n\t.int()\n\t.refine((n) => [301, 302, 307, 308].includes(n), {\n\t\tmessage: \"Redirect type must be 301, 302, 307, or 308\",\n\t});\n\n/** Matches CR or LF characters */\nconst CRLF = /[\\r\\n]/;\n\n/** Path must start with / and not be protocol-relative, contain no CRLF, and no path traversal */\nconst urlPath = z\n\t.string()\n\t.min(1)\n\t.refine((s) => s.startsWith(\"/\") && !s.startsWith(\"//\"), {\n\t\tmessage: \"Must be a path starting with / (no protocol-relative URLs)\",\n\t})\n\t.refine((s) => !CRLF.test(s), {\n\t\tmessage: \"URL must not contain newline characters\",\n\t})\n\t.refine(\n\t\t(s) => {\n\t\t\ttry {\n\t\t\t\treturn !decodeURIComponent(s).split(\"/\").includes(\"..\");\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\t\t{ message: \"URL must not contain path traversal segments\" },\n\t);\n\nexport const createRedirectBody = z\n\t.object({\n\t\tsource: urlPath,\n\t\tdestination: urlPath,\n\t\ttype: redirectType.optional().default(301),\n\t\tenabled: z.boolean().optional().default(true),\n\t\tgroupName: z.string().nullish(),\n\t})\n\t.meta({ id: \"CreateRedirectBody\" });\n\nexport const updateRedirectBody = z\n\t.object({\n\t\tsource: urlPath.optional(),\n\t\tdestination: urlPath.optional(),\n\t\ttype: redirectType.optional(),\n\t\tenabled: z.boolean().optional(),\n\t\tgroupName: z.string().nullish(),\n\t})\n\t.refine((o) => Object.values(o).some((v) => v !== undefined), {\n\t\tmessage: \"At least one field must be provided\",\n\t})\n\t.meta({ id: \"UpdateRedirectBody\" });\n\nexport const redirectsListQuery = cursorPaginationQuery\n\t.extend({\n\t\tsearch: z.string().optional(),\n\t\tgroup: z.string().optional(),\n\t\tenabled: z\n\t\t\t.enum([\"true\", \"false\"])\n\t\t\t.transform((v) => v === \"true\")\n\t\t\t.optional(),\n\t\tauto: z\n\t\t\t.enum([\"true\", \"false\"])\n\t\t\t.transform((v) => v === \"true\")\n\t\t\t.optional(),\n\t})\n\t.meta({ id: \"RedirectsListQuery\" });\n\n// ---------------------------------------------------------------------------\n// 404 Log: Input schemas\n// ---------------------------------------------------------------------------\n\nexport const notFoundListQuery = cursorPaginationQuery\n\t.extend({\n\t\tsearch: z.string().optional(),\n\t})\n\t.meta({ id: \"NotFoundListQuery\" });\n\nexport const notFoundSummaryQuery = z.object({\n\tlimit: z.coerce.number().int().min(1).max(100).optional().default(50),\n});\n\nexport const notFoundPruneBody = z\n\t.object({\n\t\tolderThan: z.string().datetime({ message: \"olderThan must be an ISO 8601 datetime\" }),\n\t})\n\t.meta({ id: \"NotFoundPruneBody\" });\n\n// ---------------------------------------------------------------------------\n// Redirects: Response schemas\n// ---------------------------------------------------------------------------\n\nexport const redirectSchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tsource: z.string(),\n\t\tdestination: z.string(),\n\t\ttype: z.number().int(),\n\t\tisPattern: z.boolean(),\n\t\tenabled: z.boolean(),\n\t\thits: z.number().int(),\n\t\tlastHitAt: z.string().nullable(),\n\t\tgroupName: z.string().nullable(),\n\t\tauto: z.boolean(),\n\t\tcreatedAt: z.string(),\n\t\tupdatedAt: z.string(),\n\t})\n\t.meta({ id: \"Redirect\" });\n\nexport const redirectListResponseSchema = z\n\t.object({\n\t\titems: z.array(redirectSchema),\n\t\tnextCursor: z.string().optional(),\n\t\tloopRedirectIds: z.array(z.string()).optional(),\n\t})\n\t.meta({ id: \"RedirectListResponse\" });\n\nexport const notFoundEntrySchema = z\n\t.object({\n\t\tid: z.string(),\n\t\tpath: z.string(),\n\t\treferrer: z.string().nullable(),\n\t\tuserAgent: z.string().nullable(),\n\t\tip: z.string().nullable(),\n\t\tcreatedAt: z.string(),\n\t})\n\t.meta({ id: \"NotFoundEntry\" });\n\nexport const notFoundListResponseSchema = z\n\t.object({\n\t\titems: z.array(notFoundEntrySchema),\n\t\tnextCursor: z.string().optional(),\n\t})\n\t.meta({ id: \"NotFoundListResponse\" });\n\nexport const notFoundSummarySchema = z\n\t.object({\n\t\tpath: z.string(),\n\t\tcount: z.number().int(),\n\t\tlastSeen: z.string(),\n\t\ttopReferrer: z.string().nullable(),\n\t})\n\t.meta({ id: \"NotFoundSummary\" });\n\nexport const notFoundSummaryResponseSchema = z\n\t.object({ items: z.array(notFoundSummarySchema) })\n\t.meta({ id: \"NotFoundSummaryResponse\" });\n","/**\n * ProseMirror to Portable Text Converter\n *\n * Converts TipTap's ProseMirror JSON format to Portable Text for storage.\n */\n\nimport type {\n\tProseMirrorDocument,\n\tProseMirrorNode,\n\tProseMirrorMark,\n\tPortableTextBlock,\n\tPortableTextTextBlock,\n\tPortableTextSpan,\n\tPortableTextMarkDef,\n\tPortableTextImageBlock,\n\tPortableTextCodeBlock,\n} from \"./types.js\";\n\n/**\n * Generate a unique key for Portable Text blocks\n */\nfunction generateKey(): string {\n\treturn Math.random().toString(36).substring(2, 11);\n}\n\n/**\n * Convert ProseMirror document to Portable Text\n */\nexport function prosemirrorToPortableText(doc: ProseMirrorDocument): PortableTextBlock[] {\n\tif (!doc || doc.type !== \"doc\" || !doc.content) {\n\t\treturn [];\n\t}\n\n\tconst blocks: PortableTextBlock[] = [];\n\n\tfor (const node of doc.content) {\n\t\tconst converted = convertNode(node);\n\t\tif (converted) {\n\t\t\tif (Array.isArray(converted)) {\n\t\t\t\tblocks.push(...converted);\n\t\t\t} else {\n\t\t\t\tblocks.push(converted);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn blocks;\n}\n\n/**\n * Convert a single ProseMirror node to Portable Text block(s)\n */\nfunction convertNode(node: ProseMirrorNode): PortableTextBlock | PortableTextBlock[] | null {\n\tswitch (node.type) {\n\t\tcase \"paragraph\":\n\t\t\treturn convertParagraph(node);\n\n\t\tcase \"heading\":\n\t\t\treturn convertHeading(node);\n\n\t\tcase \"bulletList\":\n\t\t\treturn convertList(node, \"bullet\");\n\n\t\tcase \"orderedList\":\n\t\t\treturn convertList(node, \"number\");\n\n\t\tcase \"blockquote\":\n\t\t\treturn convertBlockquote(node);\n\n\t\tcase \"codeBlock\":\n\t\t\treturn convertCodeBlock(node);\n\n\t\tcase \"image\":\n\t\t\treturn convertImage(node);\n\n\t\tcase \"horizontalRule\":\n\t\t\treturn {\n\t\t\t\t_type: \"break\",\n\t\t\t\t_key: generateKey(),\n\t\t\t\tstyle: \"lineBreak\",\n\t\t\t};\n\n\t\tdefault:\n\t\t\t// Preserve unknown blocks\n\t\t\treturn {\n\t\t\t\t_type: node.type,\n\t\t\t\t_key: generateKey(),\n\t\t\t\t...node.attrs,\n\t\t\t\t_pmContent: node.content,\n\t\t\t};\n\t}\n}\n\n/**\n * Convert paragraph to Portable Text block\n */\nfunction convertParagraph(node: ProseMirrorNode): PortableTextTextBlock | null {\n\tconst { children, markDefs } = convertInlineContent(node.content || []);\n\n\t// Skip empty paragraphs\n\tif (children.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\t_type: \"block\",\n\t\t_key: generateKey(),\n\t\tstyle: \"normal\",\n\t\tchildren,\n\t\tmarkDefs: markDefs.length > 0 ? markDefs : undefined,\n\t};\n}\n\n/** Map heading level number to Portable Text style */\nfunction headingLevelToStyle(level: number): PortableTextTextBlock[\"style\"] {\n\tswitch (level) {\n\t\tcase 1:\n\t\t\treturn \"h1\";\n\t\tcase 2:\n\t\t\treturn \"h2\";\n\t\tcase 3:\n\t\t\treturn \"h3\";\n\t\tcase 4:\n\t\t\treturn \"h4\";\n\t\tcase 5:\n\t\t\treturn \"h5\";\n\t\tcase 6:\n\t\t\treturn \"h6\";\n\t\tdefault:\n\t\t\treturn \"h1\";\n\t}\n}\n\n/**\n * Convert heading to Portable Text block\n */\nfunction convertHeading(node: ProseMirrorNode): PortableTextTextBlock | null {\n\tconst { children, markDefs } = convertInlineContent(node.content || []);\n\tconst rawLevel = typeof node.attrs?.level === \"number\" ? node.attrs.level : 1;\n\tconst style = headingLevelToStyle(rawLevel);\n\n\tif (children.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\t_type: \"block\",\n\t\t_key: generateKey(),\n\t\tstyle,\n\t\tchildren,\n\t\tmarkDefs: markDefs.length > 0 ? markDefs : undefined,\n\t};\n}\n\n/**\n * Convert list to Portable Text blocks\n */\nfunction convertList(\n\tnode: ProseMirrorNode,\n\tlistItem: \"bullet\" | \"number\",\n): PortableTextTextBlock[] {\n\tconst blocks: PortableTextTextBlock[] = [];\n\n\tfor (const item of node.content || []) {\n\t\tif (item.type === \"listItem\") {\n\t\t\tconst itemBlocks = convertListItem(item, listItem, 1);\n\t\t\tblocks.push(...itemBlocks);\n\t\t}\n\t}\n\n\treturn blocks;\n}\n\n/**\n * Convert list item to Portable Text blocks\n */\nfunction convertListItem(\n\titem: ProseMirrorNode,\n\tlistItem: \"bullet\" | \"number\",\n\tlevel: number,\n): PortableTextTextBlock[] {\n\tconst blocks: PortableTextTextBlock[] = [];\n\n\tfor (const child of item.content || []) {\n\t\tif (child.type === \"paragraph\") {\n\t\t\tconst { children, markDefs } = convertInlineContent(child.content || []);\n\n\t\t\tif (children.length > 0) {\n\t\t\t\tblocks.push({\n\t\t\t\t\t_type: \"block\",\n\t\t\t\t\t_key: generateKey(),\n\t\t\t\t\tstyle: \"normal\",\n\t\t\t\t\tlistItem,\n\t\t\t\t\tlevel,\n\t\t\t\t\tchildren,\n\t\t\t\t\tmarkDefs: markDefs.length > 0 ? markDefs : undefined,\n\t\t\t\t});\n\t\t\t}\n\t\t} else if (child.type === \"bulletList\") {\n\t\t\tblocks.push(...convertListItemNested(child, \"bullet\", level + 1));\n\t\t} else if (child.type === \"orderedList\") {\n\t\t\tblocks.push(...convertListItemNested(child, \"number\", level + 1));\n\t\t}\n\t}\n\n\treturn blocks;\n}\n\n/**\n * Convert nested list\n */\nfunction convertListItemNested(\n\tnode: ProseMirrorNode,\n\tlistItem: \"bullet\" | \"number\",\n\tlevel: number,\n): PortableTextTextBlock[] {\n\tconst blocks: PortableTextTextBlock[] = [];\n\n\tfor (const item of node.content || []) {\n\t\tif (item.type === \"listItem\") {\n\t\t\tblocks.push(...convertListItem(item, listItem, level));\n\t\t}\n\t}\n\n\treturn blocks;\n}\n\n/**\n * Convert blockquote to Portable Text blocks\n */\nfunction convertBlockquote(\n\tnode: ProseMirrorNode,\n): PortableTextTextBlock | PortableTextTextBlock[] | null {\n\t// Blockquotes in PT are just blocks with style: \"blockquote\"\n\tconst blocks: PortableTextTextBlock[] = [];\n\n\tfor (const child of node.content || []) {\n\t\tif (child.type === \"paragraph\") {\n\t\t\tconst { children, markDefs } = convertInlineContent(child.content || []);\n\n\t\t\tif (children.length > 0) {\n\t\t\t\tblocks.push({\n\t\t\t\t\t_type: \"block\",\n\t\t\t\t\t_key: generateKey(),\n\t\t\t\t\tstyle: \"blockquote\",\n\t\t\t\t\tchildren,\n\t\t\t\t\tmarkDefs: markDefs.length > 0 ? markDefs : undefined,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\treturn blocks.length === 1 ? blocks[0] : blocks.length > 0 ? blocks : null;\n}\n\n/**\n * Convert code block to Portable Text\n */\nfunction convertCodeBlock(node: ProseMirrorNode): PortableTextCodeBlock {\n\tconst code = node.content?.map((n) => n.text || \"\").join(\"\") || \"\";\n\tconst language = typeof node.attrs?.language === \"string\" ? node.attrs.language : undefined;\n\n\treturn {\n\t\t_type: \"code\",\n\t\t_key: generateKey(),\n\t\tcode,\n\t\tlanguage: language || undefined,\n\t};\n}\n\n/**\n * Convert image to Portable Text\n */\nfunction convertImage(node: ProseMirrorNode): PortableTextImageBlock {\n\tconst attrs = node.attrs;\n\tconst provider = typeof attrs?.provider === \"string\" ? attrs.provider : undefined;\n\tconst mediaId = typeof attrs?.mediaId === \"string\" ? attrs.mediaId : undefined;\n\tconst src = typeof attrs?.src === \"string\" ? attrs.src : \"\";\n\tconst alt = typeof attrs?.alt === \"string\" ? attrs.alt : undefined;\n\tconst title = typeof attrs?.title === \"string\" ? attrs.title : undefined;\n\tconst width = typeof attrs?.width === \"number\" ? attrs.width : undefined;\n\tconst height = typeof attrs?.height === \"number\" ? attrs.height : undefined;\n\tconst displayWidth = typeof attrs?.displayWidth === \"number\" ? attrs.displayWidth : undefined;\n\tconst displayHeight = typeof attrs?.displayHeight === \"number\" ? attrs.displayHeight : undefined;\n\n\treturn {\n\t\t_type: \"image\",\n\t\t_key: generateKey(),\n\t\tasset: {\n\t\t\t// Use mediaId as _ref if available (for proper provider lookups)\n\t\t\t_ref: mediaId || src || \"\",\n\t\t\t// Store URL for admin preview and fallback rendering\n\t\t\turl: src || \"\",\n\t\t\t// Store provider for external media\n\t\t\tprovider: provider && provider !== \"local\" ? provider : undefined,\n\t\t},\n\t\talt: alt || undefined,\n\t\tcaption: title || undefined,\n\t\twidth: width || undefined,\n\t\theight: height || undefined,\n\t\tdisplayWidth: displayWidth || undefined,\n\t\tdisplayHeight: displayHeight || undefined,\n\t};\n}\n\n/**\n * Convert inline content (text nodes with marks) to Portable Text spans\n */\nfunction convertInlineContent(nodes: ProseMirrorNode[]): {\n\tchildren: PortableTextSpan[];\n\tmarkDefs: PortableTextMarkDef[];\n} {\n\tconst children: PortableTextSpan[] = [];\n\tconst markDefs: PortableTextMarkDef[] = [];\n\tconst markDefMap = new Map<string, string>(); // href -> key\n\n\tfor (const node of nodes) {\n\t\tif (node.type === \"text\" && node.text) {\n\t\t\tconst marks: string[] = [];\n\n\t\t\tfor (const mark of node.marks || []) {\n\t\t\t\tconst markType = convertMark(mark, markDefs, markDefMap);\n\t\t\t\tif (markType) {\n\t\t\t\t\tmarks.push(markType);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tchildren.push({\n\t\t\t\t_type: \"span\",\n\t\t\t\t_key: generateKey(),\n\t\t\t\ttext: node.text,\n\t\t\t\tmarks: marks.length > 0 ? marks : undefined,\n\t\t\t});\n\t\t} else if (node.type === \"hardBreak\") {\n\t\t\t// Hard breaks become newlines in the text\n\t\t\tif (children.length > 0) {\n\t\t\t\tconst lastChild = children.at(-1)!;\n\t\t\t\tlastChild.text += \"\\n\";\n\t\t\t} else {\n\t\t\t\tchildren.push({\n\t\t\t\t\t_type: \"span\",\n\t\t\t\t\t_key: generateKey(),\n\t\t\t\t\ttext: \"\\n\",\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\t// Ensure at least one span exists\n\tif (children.length === 0) {\n\t\tchildren.push({\n\t\t\t_type: \"span\",\n\t\t\t_key: generateKey(),\n\t\t\ttext: \"\",\n\t\t});\n\t}\n\n\treturn { children, markDefs };\n}\n\n/**\n * Convert a ProseMirror mark to Portable Text mark\n */\nfunction convertMark(\n\tmark: ProseMirrorMark,\n\tmarkDefs: PortableTextMarkDef[],\n\tmarkDefMap: Map<string, string>,\n): string | null {\n\tswitch (mark.type) {\n\t\tcase \"bold\":\n\t\tcase \"strong\":\n\t\t\treturn \"strong\";\n\n\t\tcase \"italic\":\n\t\tcase \"em\":\n\t\t\treturn \"em\";\n\n\t\tcase \"underline\":\n\t\t\treturn \"underline\";\n\n\t\tcase \"strike\":\n\t\tcase \"strikethrough\":\n\t\t\treturn \"strike-through\";\n\n\t\tcase \"code\":\n\t\t\treturn \"code\";\n\n\t\tcase \"link\": {\n\t\t\tconst href = (typeof mark.attrs?.href === \"string\" ? mark.attrs.href : \"\") || \"\";\n\n\t\t\t// Check if we already have a mark def for this link\n\t\t\tif (markDefMap.has(href)) {\n\t\t\t\treturn markDefMap.get(href)!;\n\t\t\t}\n\n\t\t\t// Create new mark def\n\t\t\tconst key = generateKey();\n\t\t\tmarkDefs.push({\n\t\t\t\t_type: \"link\",\n\t\t\t\t_key: key,\n\t\t\t\thref,\n\t\t\t\tblank: mark.attrs?.target === \"_blank\",\n\t\t\t});\n\t\t\tmarkDefMap.set(href, key);\n\n\t\t\treturn key;\n\t\t}\n\n\t\tdefault:\n\t\t\t// Unknown mark - preserve as-is\n\t\t\treturn mark.type;\n\t}\n}\n","/**\n * Portable Text to ProseMirror Converter\n *\n * Converts Portable Text to TipTap's ProseMirror JSON format for editing.\n */\n\nimport type {\n\tProseMirrorDocument,\n\tProseMirrorNode,\n\tProseMirrorMark,\n\tPortableTextBlock,\n\tPortableTextTextBlock,\n\tPortableTextSpan,\n\tPortableTextMarkDef,\n\tPortableTextImageBlock,\n\tPortableTextCodeBlock,\n} from \"./types.js\";\n\n/**\n * Convert Portable Text to ProseMirror document\n */\nexport function portableTextToProsemirror(blocks: PortableTextBlock[]): ProseMirrorDocument {\n\tif (!blocks || blocks.length === 0) {\n\t\treturn {\n\t\t\ttype: \"doc\",\n\t\t\tcontent: [{ type: \"paragraph\" }],\n\t\t};\n\t}\n\n\tconst content: ProseMirrorNode[] = [];\n\tlet i = 0;\n\n\twhile (i < blocks.length) {\n\t\tconst block = blocks[i];\n\n\t\t// Check for list items\n\t\tif (isTextBlock(block) && block.listItem) {\n\t\t\t// Collect consecutive list items\n\t\t\tconst listBlocks: PortableTextTextBlock[] = [];\n\t\t\tconst listType = block.listItem;\n\n\t\t\twhile (i < blocks.length) {\n\t\t\t\tconst current = blocks[i];\n\t\t\t\tif (isTextBlock(current) && current.listItem === listType) {\n\t\t\t\t\tlistBlocks.push(current);\n\t\t\t\t\ti++;\n\t\t\t\t} else {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcontent.push(convertList(listBlocks, listType));\n\t\t} else {\n\t\t\tconst converted = convertBlock(block);\n\t\t\tif (converted) {\n\t\t\t\tcontent.push(converted);\n\t\t\t}\n\t\t\ti++;\n\t\t}\n\t}\n\n\treturn {\n\t\ttype: \"doc\",\n\t\tcontent: content.length > 0 ? content : [{ type: \"paragraph\" }],\n\t};\n}\n\n/**\n * Type guard for text blocks\n */\nfunction isTextBlock(block: PortableTextBlock): block is PortableTextTextBlock {\n\treturn block._type === \"block\";\n}\n\n/**\n * Type guard for image blocks.\n * Checks both `_type` and that `asset` is a valid object — image blocks\n * without an `asset` wrapper (e.g. `{ _type: \"image\", url: \"...\" }`) are\n * malformed and should not be cast to `PortableTextImageBlock`.\n */\nfunction isImageBlock(block: PortableTextBlock): block is PortableTextImageBlock {\n\treturn (\n\t\tblock._type === \"image\" &&\n\t\t\"asset\" in block &&\n\t\ttypeof block.asset === \"object\" &&\n\t\tblock.asset !== null\n\t);\n}\n\n/**\n * Type guard for code blocks\n */\nfunction isCodeBlock(block: PortableTextBlock): block is PortableTextCodeBlock {\n\treturn block._type === \"code\";\n}\n\n/**\n * Convert a single Portable Text block to ProseMirror node\n */\nfunction convertBlock(block: PortableTextBlock): ProseMirrorNode | null {\n\tif (isTextBlock(block)) {\n\t\treturn convertTextBlock(block);\n\t}\n\tif (isImageBlock(block)) {\n\t\treturn convertImage(block);\n\t}\n\tif (block._type === \"image\") {\n\t\t// Malformed image block (no asset wrapper) — extract url from top level\n\t\treturn convertMalformedImage(block);\n\t}\n\tif (isCodeBlock(block)) {\n\t\treturn convertCodeBlock(block);\n\t}\n\tif (block._type === \"break\") {\n\t\treturn { type: \"horizontalRule\" };\n\t}\n\t// Unknown block - wrap in a div or preserve as placeholder\n\treturn {\n\t\ttype: \"paragraph\",\n\t\tcontent: [\n\t\t\t{\n\t\t\t\ttype: \"text\",\n\t\t\t\ttext: `[Unknown block type: ${block._type}]`,\n\t\t\t\tmarks: [{ type: \"code\" }],\n\t\t\t},\n\t\t],\n\t};\n}\n\n/**\n * Convert text block to ProseMirror paragraph or heading\n */\nfunction convertTextBlock(block: PortableTextTextBlock): ProseMirrorNode | null {\n\tconst { style = \"normal\", children, markDefs = [] } = block;\n\n\t// Convert children to ProseMirror nodes\n\tconst content = convertSpans(children, markDefs);\n\n\t// Determine node type based on style\n\tswitch (style) {\n\t\tcase \"h1\":\n\t\tcase \"h2\":\n\t\tcase \"h3\":\n\t\tcase \"h4\":\n\t\tcase \"h5\":\n\t\tcase \"h6\": {\n\t\t\tconst level = parseInt(style.substring(1), 10);\n\t\t\treturn {\n\t\t\t\ttype: \"heading\",\n\t\t\t\tattrs: { level },\n\t\t\t\tcontent: content.length > 0 ? content : undefined,\n\t\t\t};\n\t\t}\n\n\t\tcase \"blockquote\":\n\t\t\treturn {\n\t\t\t\ttype: \"blockquote\",\n\t\t\t\tcontent: [\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: \"paragraph\",\n\t\t\t\t\t\tcontent: content.length > 0 ? content : undefined,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\n\n\t\tcase \"normal\":\n\t\tdefault:\n\t\t\treturn {\n\t\t\t\ttype: \"paragraph\",\n\t\t\t\tcontent: content.length > 0 ? content : undefined,\n\t\t\t};\n\t}\n}\n\n/**\n * Convert list items to ProseMirror list\n */\nfunction convertList(\n\titems: PortableTextTextBlock[],\n\tlistType: \"bullet\" | \"number\",\n): ProseMirrorNode {\n\t// Group items by level\n\tconst rootItems: ProseMirrorNode[] = [];\n\tlet i = 0;\n\n\twhile (i < items.length) {\n\t\tconst item = items[i];\n\t\tconst level = item.level || 1;\n\n\t\tif (level === 1) {\n\t\t\t// Collect nested items for this root item\n\t\t\tconst nestedItems: PortableTextTextBlock[] = [];\n\t\t\ti++;\n\n\t\t\twhile (i < items.length && (items[i].level || 1) > 1) {\n\t\t\t\tnestedItems.push(items[i]);\n\t\t\t\ti++;\n\t\t\t}\n\n\t\t\trootItems.push(convertListItem(item, nestedItems, listType));\n\t\t} else {\n\t\t\t// Orphan nested item - treat as root\n\t\t\trootItems.push(convertListItem(item, [], listType));\n\t\t\ti++;\n\t\t}\n\t}\n\n\treturn {\n\t\ttype: listType === \"bullet\" ? \"bulletList\" : \"orderedList\",\n\t\tcontent: rootItems,\n\t};\n}\n\n/**\n * Convert a single list item to ProseMirror\n */\nfunction convertListItem(\n\titem: PortableTextTextBlock,\n\tnestedItems: PortableTextTextBlock[],\n\tparentListType: \"bullet\" | \"number\",\n): ProseMirrorNode {\n\tconst content: ProseMirrorNode[] = [];\n\n\t// Add paragraph content\n\tconst spans = convertSpans(item.children, item.markDefs || []);\n\tcontent.push({\n\t\ttype: \"paragraph\",\n\t\tcontent: spans.length > 0 ? spans : undefined,\n\t});\n\n\t// Handle nested items\n\tif (nestedItems.length > 0) {\n\t\t// Group nested items by their list type\n\t\tlet j = 0;\n\n\t\twhile (j < nestedItems.length) {\n\t\t\tconst nestedListType = nestedItems[j].listItem || parentListType;\n\t\t\tconst nestedGroup: PortableTextTextBlock[] = [];\n\n\t\t\twhile (\n\t\t\t\tj < nestedItems.length &&\n\t\t\t\t(nestedItems[j].listItem || parentListType) === nestedListType\n\t\t\t) {\n\t\t\t\tnestedGroup.push(nestedItems[j]);\n\t\t\t\tj++;\n\t\t\t}\n\n\t\t\tif (nestedGroup.length > 0) {\n\t\t\t\t// Decrease level for nested conversion\n\t\t\t\tconst adjustedGroup = nestedGroup.map((ni) => ({\n\t\t\t\t\t...ni,\n\t\t\t\t\tlevel: (ni.level || 2) - 1,\n\t\t\t\t}));\n\t\t\t\tcontent.push(convertList(adjustedGroup, nestedListType));\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttype: \"listItem\",\n\t\tcontent,\n\t};\n}\n\n/**\n * Convert Portable Text spans to ProseMirror text nodes\n */\nfunction convertSpans(\n\tspans: PortableTextSpan[],\n\tmarkDefs: PortableTextMarkDef[],\n): ProseMirrorNode[] {\n\tconst nodes: ProseMirrorNode[] = [];\n\tconst markDefsMap = new Map(markDefs.map((md) => [md._key, md]));\n\n\tfor (const span of spans) {\n\t\tif (span._type !== \"span\") continue;\n\n\t\t// Handle newlines in text\n\t\tconst parts = span.text.split(\"\\n\");\n\n\t\tfor (let i = 0; i < parts.length; i++) {\n\t\t\tconst text = parts[i];\n\n\t\t\t// Add text node\n\t\t\tif (text.length > 0) {\n\t\t\t\tconst marks = convertMarks(span.marks || [], markDefsMap);\n\t\t\t\tconst node: ProseMirrorNode = {\n\t\t\t\t\ttype: \"text\",\n\t\t\t\t\ttext,\n\t\t\t\t};\n\t\t\t\tif (marks.length > 0) {\n\t\t\t\t\tnode.marks = marks;\n\t\t\t\t}\n\t\t\t\tnodes.push(node);\n\t\t\t}\n\n\t\t\t// Add hard break between parts (not after last)\n\t\t\tif (i < parts.length - 1) {\n\t\t\t\tnodes.push({ type: \"hardBreak\" });\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nodes;\n}\n\n/**\n * Convert Portable Text marks to ProseMirror marks\n */\nfunction convertMarks(\n\tmarks: string[],\n\tmarkDefs: Map<string, PortableTextMarkDef>,\n): ProseMirrorMark[] {\n\tconst pmMarks: ProseMirrorMark[] = [];\n\n\tfor (const mark of marks) {\n\t\tswitch (mark) {\n\t\t\tcase \"strong\":\n\t\t\t\tpmMarks.push({ type: \"bold\" });\n\t\t\t\tbreak;\n\n\t\t\tcase \"em\":\n\t\t\t\tpmMarks.push({ type: \"italic\" });\n\t\t\t\tbreak;\n\n\t\t\tcase \"underline\":\n\t\t\t\tpmMarks.push({ type: \"underline\" });\n\t\t\t\tbreak;\n\n\t\t\tcase \"strike-through\":\n\t\t\t\tpmMarks.push({ type: \"strike\" });\n\t\t\t\tbreak;\n\n\t\t\tcase \"code\":\n\t\t\t\tpmMarks.push({ type: \"code\" });\n\t\t\t\tbreak;\n\n\t\t\tdefault: {\n\t\t\t\t// Check if it's a mark definition reference\n\t\t\t\tconst markDef = markDefs.get(mark);\n\t\t\t\tif (markDef) {\n\t\t\t\t\tif (markDef._type === \"link\") {\n\t\t\t\t\t\tpmMarks.push({\n\t\t\t\t\t\t\ttype: \"link\",\n\t\t\t\t\t\t\tattrs: {\n\t\t\t\t\t\t\t\thref: markDef.href,\n\t\t\t\t\t\t\t\ttarget: markDef.blank ? \"_blank\" : null,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Unknown mark def type - preserve attrs\n\t\t\t\t\t\tpmMarks.push({\n\t\t\t\t\t\t\ttype: markDef._type,\n\t\t\t\t\t\t\tattrs: markDef as Record<string, unknown>,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn pmMarks;\n}\n\n/**\n * Convert image block to ProseMirror\n */\nfunction convertImage(block: PortableTextImageBlock): ProseMirrorNode {\n\treturn {\n\t\ttype: \"image\",\n\t\tattrs: {\n\t\t\tsrc: block.asset.url || block.asset._ref,\n\t\t\talt: block.alt || \"\",\n\t\t\ttitle: block.caption || \"\",\n\t\t\tmediaId: block.asset._ref,\n\t\t\tprovider: block.asset.provider,\n\t\t\twidth: block.width,\n\t\t\theight: block.height,\n\t\t\tdisplayWidth: block.displayWidth,\n\t\t\tdisplayHeight: block.displayHeight,\n\t\t},\n\t};\n}\n\n/**\n * Convert a malformed image block (missing `asset` wrapper) to ProseMirror.\n * Handles blocks like `{ _type: \"image\", url: \"...\", alt: \"...\" }` that may\n * originate from migrations or third-party imports.\n */\nfunction convertMalformedImage(block: PortableTextBlock): ProseMirrorNode {\n\t// PortableTextUnknownBlock allows indexed access via [key: string]: unknown\n\tconst url = \"url\" in block && typeof block.url === \"string\" ? block.url : \"\";\n\tconst alt = \"alt\" in block && typeof block.alt === \"string\" ? block.alt : \"\";\n\tconst caption = \"caption\" in block && typeof block.caption === \"string\" ? block.caption : \"\";\n\tconst width = \"width\" in block && typeof block.width === \"number\" ? block.width : undefined;\n\tconst height = \"height\" in block && typeof block.height === \"number\" ? block.height : undefined;\n\tconst displayWidth =\n\t\t\"displayWidth\" in block && typeof block.displayWidth === \"number\"\n\t\t\t? block.displayWidth\n\t\t\t: undefined;\n\tconst displayHeight =\n\t\t\"displayHeight\" in block && typeof block.displayHeight === \"number\"\n\t\t\t? block.displayHeight\n\t\t\t: undefined;\n\treturn {\n\t\ttype: \"image\",\n\t\tattrs: {\n\t\t\tsrc: url,\n\t\t\talt,\n\t\t\ttitle: caption,\n\t\t\tmediaId: undefined,\n\t\t\tprovider: undefined,\n\t\t\twidth,\n\t\t\theight,\n\t\t\tdisplayWidth,\n\t\t\tdisplayHeight,\n\t\t},\n\t};\n}\n\n/**\n * Convert code block to ProseMirror\n */\nfunction convertCodeBlock(block: PortableTextCodeBlock): ProseMirrorNode {\n\treturn {\n\t\ttype: \"codeBlock\",\n\t\tattrs: {\n\t\t\tlanguage: block.language || null,\n\t\t},\n\t\tcontent: block.code ? [{ type: \"text\", text: block.code }] : undefined,\n\t};\n}\n","/**\n * WordPress WXR (WordPress eXtended RSS) parser\n *\n * Uses SAX streaming parser to handle large export files efficiently.\n * WXR is an RSS extension containing WordPress content exports.\n *\n * @see https://developer.wordpress.org/plugins/data-storage/wp-xml-rpc/\n */\n\nimport type { Readable } from \"node:stream\";\n\nimport sax from \"sax\";\n\n// Regex patterns for WXR parsing\nconst PHP_SERIALIZED_STRING_PATTERN = /s:\\d+:\"([^\"]+)\"/g;\nconst PHP_SERIALIZED_STRING_MATCH_PATTERN = /s:\\d+:\"([^\"]+)\"/;\n\n/**\n * Parsed WordPress export data\n */\nexport interface WxrData {\n\t/** Site metadata */\n\tsite: WxrSite;\n\t/** Posts (including custom post types) */\n\tposts: WxrPost[];\n\t/** Media attachments */\n\tattachments: WxrAttachment[];\n\t/** Categories */\n\tcategories: WxrCategory[];\n\t/** Tags */\n\ttags: WxrTag[];\n\t/** Authors */\n\tauthors: WxrAuthor[];\n\t/** All taxonomy terms (including custom taxonomies and nav_menu) */\n\tterms: WxrTerm[];\n\t/** Parsed navigation menus */\n\tnavMenus: WxrNavMenu[];\n}\n\nexport interface WxrSite {\n\ttitle?: string;\n\tlink?: string;\n\tdescription?: string;\n\tlanguage?: string;\n\tbaseSiteUrl?: string;\n\tbaseBlogUrl?: string;\n}\n\nexport interface WxrPost {\n\tid?: number;\n\ttitle?: string;\n\tlink?: string;\n\tpubDate?: string;\n\tcreator?: string;\n\tguid?: string;\n\tdescription?: string;\n\tcontent?: string;\n\texcerpt?: string;\n\tpostDate?: string;\n\tpostDateGmt?: string;\n\tpostModified?: string;\n\tpostModifiedGmt?: string;\n\tcommentStatus?: string;\n\tpingStatus?: string;\n\tstatus?: string;\n\tpostType?: string;\n\tpostName?: string;\n\tpostPassword?: string;\n\tisSticky?: boolean;\n\t/** Parent post ID for hierarchical content (pages) */\n\tpostParent?: number;\n\t/** Menu order for sorting */\n\tmenuOrder?: number;\n\tcategories: string[];\n\ttags: string[];\n\t/** Custom taxonomy assignments beyond categories/tags */\n\tcustomTaxonomies?: Map<string, string[]>;\n\tmeta: Map<string, string>;\n}\n\nexport interface WxrAttachment {\n\tid?: number;\n\ttitle?: string;\n\turl?: string;\n\tpostDate?: string;\n\tmeta: Map<string, string>;\n}\n\nexport interface WxrCategory {\n\tid?: number;\n\tnicename?: string;\n\tname?: string;\n\tparent?: string;\n\tdescription?: string;\n}\n\nexport interface WxrTag {\n\tid?: number;\n\tslug?: string;\n\tname?: string;\n\tdescription?: string;\n}\n\n/**\n * Generic taxonomy term (categories, tags, nav_menu, custom taxonomies)\n */\nexport interface WxrTerm {\n\tid: number;\n\ttaxonomy: string; // 'category', 'post_tag', 'nav_menu', 'genre', etc.\n\tslug: string;\n\tname: string;\n\tparent?: string;\n\tdescription?: string;\n}\n\n/**\n * Navigation menu structure\n */\nexport interface WxrNavMenu {\n\tid: number;\n\tname: string; // Menu slug\n\tlabel: string; // Menu name\n\titems: WxrNavMenuItem[];\n}\n\n/**\n * Navigation menu item\n */\nexport interface WxrNavMenuItem {\n\tid: number;\n\tmenuId: number;\n\tparentId?: number;\n\tsortOrder: number;\n\ttype: \"custom\" | \"post_type\" | \"taxonomy\";\n\tobjectType?: string; // 'page', 'post', 'category'\n\tobjectId?: number;\n\turl?: string;\n\ttitle: string;\n\ttarget?: string;\n\tclasses?: string;\n}\n\nexport interface WxrAuthor {\n\tid?: number;\n\tlogin?: string;\n\temail?: string;\n\tdisplayName?: string;\n\tfirstName?: string;\n\tlastName?: string;\n}\n\n/** Extract string value from a SAX attribute (handles both Tag and QualifiedTag) */\nfunction attrStr(attr: string | { value: string } | undefined): string {\n\tif (typeof attr === \"string\") return attr;\n\tif (attr && typeof attr === \"object\" && \"value\" in attr) return attr.value;\n\treturn \"\";\n}\n\n/** Type guard for complete WxrTerm (all required fields present) */\nfunction isCompleteWxrTerm(term: Partial<WxrTerm>): term is WxrTerm {\n\treturn (\n\t\tterm.id !== undefined &&\n\t\tterm.taxonomy !== undefined &&\n\t\tterm.slug !== undefined &&\n\t\tterm.name !== undefined\n\t);\n}\n\n/**\n * Parse a WordPress WXR export file\n */\nexport function parseWxr(stream: Readable): Promise<WxrData> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst parser = sax.createStream(true, { trim: true });\n\n\t\tconst data: WxrData = {\n\t\t\tsite: {},\n\t\t\tposts: [],\n\t\t\tattachments: [],\n\t\t\tcategories: [],\n\t\t\ttags: [],\n\t\t\tauthors: [],\n\t\t\tterms: [],\n\t\t\tnavMenus: [],\n\t\t};\n\n\t\t// Parser state\n\t\tlet currentPath: string[] = [];\n\t\tlet currentText = \"\";\n\t\tlet currentItem: WxrPost | null = null;\n\t\tlet currentAttachment: WxrAttachment | null = null;\n\t\tlet currentCategory: WxrCategory | null = null;\n\t\tlet currentTag: WxrTag | null = null;\n\t\tlet currentAuthor: WxrAuthor | null = null;\n\t\tlet currentTerm: Partial<WxrTerm> | null = null;\n\t\tlet currentMetaKey = \"\";\n\n\t\t// Track nav_menu_item posts for post-processing\n\t\tconst navMenuItemPosts: WxrPost[] = [];\n\t\t// Track menu term IDs by slug for linking items to menus\n\t\tconst menuTermsBySlug = new Map<string, number>();\n\n\t\tparser.on(\"opentag\", (node) => {\n\t\t\tconst tagName = node.name.toLowerCase();\n\t\t\tcurrentPath.push(tagName);\n\t\t\tcurrentText = \"\";\n\n\t\t\t// Start new item\n\t\t\tif (tagName === \"item\") {\n\t\t\t\tcurrentItem = {\n\t\t\t\t\tcategories: [],\n\t\t\t\t\ttags: [],\n\t\t\t\t\tcustomTaxonomies: new Map(),\n\t\t\t\t\tmeta: new Map(),\n\t\t\t\t};\n\t\t\t} else if (tagName === \"wp:category\") {\n\t\t\t\tcurrentCategory = {};\n\t\t\t} else if (tagName === \"wp:tag\") {\n\t\t\t\tcurrentTag = {};\n\t\t\t} else if (tagName === \"wp:author\") {\n\t\t\t\tcurrentAuthor = {};\n\t\t\t} else if (tagName === \"wp:term\") {\n\t\t\t\tcurrentTerm = {};\n\t\t\t}\n\n\t\t\t// Handle category/tag/custom taxonomy assignment in items\n\t\t\tif (tagName === \"category\" && currentItem && node.attributes) {\n\t\t\t\tconst domain = attrStr(node.attributes.domain);\n\t\t\t\tconst nicename = attrStr(node.attributes.nicename);\n\t\t\t\tif (domain === \"category\" && nicename) {\n\t\t\t\t\tcurrentItem.categories.push(nicename);\n\t\t\t\t} else if (domain === \"post_tag\" && nicename) {\n\t\t\t\t\tcurrentItem.tags.push(nicename);\n\t\t\t\t} else if (domain && nicename && domain !== \"category\" && domain !== \"post_tag\") {\n\t\t\t\t\t// Custom taxonomy (including nav_menu)\n\t\t\t\t\tif (!currentItem.customTaxonomies) {\n\t\t\t\t\t\tcurrentItem.customTaxonomies = new Map();\n\t\t\t\t\t}\n\t\t\t\t\tconst existing = currentItem.customTaxonomies.get(domain) || [];\n\t\t\t\t\texisting.push(nicename);\n\t\t\t\t\tcurrentItem.customTaxonomies.set(domain, existing);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tparser.on(\"text\", (text) => {\n\t\t\tcurrentText += text;\n\t\t});\n\n\t\tparser.on(\"cdata\", (cdata) => {\n\t\t\tcurrentText += cdata;\n\t\t});\n\n\t\tparser.on(\"closetag\", (tagName) => {\n\t\t\tconst tag = tagName.toLowerCase();\n\t\t\tconst text = currentText.trim();\n\n\t\t\t// Site-level metadata (in channel)\n\t\t\tif (currentPath.includes(\"channel\") && !currentItem) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"title\":\n\t\t\t\t\t\tif (!data.site.title) data.site.title = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"link\":\n\t\t\t\t\t\tif (!data.site.link) data.site.link = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"description\":\n\t\t\t\t\t\tif (!data.site.description) data.site.description = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"language\":\n\t\t\t\t\t\tdata.site.language = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:base_site_url\":\n\t\t\t\t\t\tdata.site.baseSiteUrl = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:base_blog_url\":\n\t\t\t\t\t\tdata.site.baseBlogUrl = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Item (post/page/attachment) parsing\n\t\t\tif (currentItem) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"title\":\n\t\t\t\t\t\tcurrentItem.title = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"link\":\n\t\t\t\t\t\tcurrentItem.link = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"pubdate\":\n\t\t\t\t\t\tcurrentItem.pubDate = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"dc:creator\":\n\t\t\t\t\t\tcurrentItem.creator = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"guid\":\n\t\t\t\t\t\tcurrentItem.guid = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"description\":\n\t\t\t\t\t\tcurrentItem.description = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"content:encoded\":\n\t\t\t\t\t\tcurrentItem.content = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"excerpt:encoded\":\n\t\t\t\t\t\tcurrentItem.excerpt = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_id\":\n\t\t\t\t\t\tcurrentItem.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_date\":\n\t\t\t\t\t\tcurrentItem.postDate = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_date_gmt\":\n\t\t\t\t\t\tcurrentItem.postDateGmt = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_modified\":\n\t\t\t\t\t\tcurrentItem.postModified = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_modified_gmt\":\n\t\t\t\t\t\tcurrentItem.postModifiedGmt = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:comment_status\":\n\t\t\t\t\t\tcurrentItem.commentStatus = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:ping_status\":\n\t\t\t\t\t\tcurrentItem.pingStatus = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:status\":\n\t\t\t\t\t\tcurrentItem.status = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_type\":\n\t\t\t\t\t\tcurrentItem.postType = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_name\":\n\t\t\t\t\t\tcurrentItem.postName = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_parent\":\n\t\t\t\t\t\tcurrentItem.postParent = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:menu_order\":\n\t\t\t\t\t\tcurrentItem.menuOrder = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_password\":\n\t\t\t\t\t\tcurrentItem.postPassword = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:is_sticky\":\n\t\t\t\t\t\tcurrentItem.isSticky = text === \"1\";\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:meta_key\":\n\t\t\t\t\t\tcurrentMetaKey = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:meta_value\":\n\t\t\t\t\t\tif (currentMetaKey) {\n\t\t\t\t\t\t\tcurrentItem.meta.set(currentMetaKey, text);\n\t\t\t\t\t\t\tcurrentMetaKey = \"\";\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:attachment_url\":\n\t\t\t\t\t\tif (currentItem.postType === \"attachment\") {\n\t\t\t\t\t\t\t// This is actually an attachment\n\t\t\t\t\t\t\tcurrentAttachment = {\n\t\t\t\t\t\t\t\tid: currentItem.id,\n\t\t\t\t\t\t\t\ttitle: currentItem.title,\n\t\t\t\t\t\t\t\turl: text,\n\t\t\t\t\t\t\t\tpostDate: currentItem.postDate,\n\t\t\t\t\t\t\t\tmeta: currentItem.meta,\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"item\":\n\t\t\t\t\t\t// End of item - categorize and store\n\t\t\t\t\t\tif (currentAttachment) {\n\t\t\t\t\t\t\tdata.attachments.push(currentAttachment);\n\t\t\t\t\t\t\tcurrentAttachment = null;\n\t\t\t\t\t\t} else if (currentItem.postType === \"nav_menu_item\") {\n\t\t\t\t\t\t\t// Track nav_menu_item posts for post-processing into menus\n\t\t\t\t\t\t\tnavMenuItemPosts.push(currentItem);\n\t\t\t\t\t\t\tdata.posts.push(currentItem);\n\t\t\t\t\t\t} else if (currentItem.postType !== \"attachment\") {\n\t\t\t\t\t\t\t// Store all non-attachment post types (posts, pages, custom post types)\n\t\t\t\t\t\t\tdata.posts.push(currentItem);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentItem = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Category parsing\n\t\t\tif (currentCategory) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"wp:term_id\":\n\t\t\t\t\t\tcurrentCategory.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:category_nicename\":\n\t\t\t\t\t\tcurrentCategory.nicename = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:cat_name\":\n\t\t\t\t\t\tcurrentCategory.name = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:category_parent\":\n\t\t\t\t\t\tcurrentCategory.parent = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:category_description\":\n\t\t\t\t\t\tcurrentCategory.description = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:category\":\n\t\t\t\t\t\tif (currentCategory.name) {\n\t\t\t\t\t\t\tdata.categories.push(currentCategory);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentCategory = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Tag parsing\n\t\t\tif (currentTag) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"wp:term_id\":\n\t\t\t\t\t\tcurrentTag.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:tag_slug\":\n\t\t\t\t\t\tcurrentTag.slug = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:tag_name\":\n\t\t\t\t\t\tcurrentTag.name = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:tag_description\":\n\t\t\t\t\t\tcurrentTag.description = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:tag\":\n\t\t\t\t\t\tif (currentTag.name) {\n\t\t\t\t\t\t\tdata.tags.push(currentTag);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentTag = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Author parsing\n\t\t\tif (currentAuthor) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"wp:author_id\":\n\t\t\t\t\t\tcurrentAuthor.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_login\":\n\t\t\t\t\t\tcurrentAuthor.login = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_email\":\n\t\t\t\t\t\tcurrentAuthor.email = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_display_name\":\n\t\t\t\t\t\tcurrentAuthor.displayName = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_first_name\":\n\t\t\t\t\t\tcurrentAuthor.firstName = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_last_name\":\n\t\t\t\t\t\tcurrentAuthor.lastName = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author\":\n\t\t\t\t\t\tif (currentAuthor.login) {\n\t\t\t\t\t\t\tdata.authors.push(currentAuthor);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentAuthor = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Generic term parsing (wp:term elements - custom taxonomies, nav_menu, etc.)\n\t\t\tif (currentTerm) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"wp:term_id\":\n\t\t\t\t\t\tcurrentTerm.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_taxonomy\":\n\t\t\t\t\t\tcurrentTerm.taxonomy = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_slug\":\n\t\t\t\t\t\tcurrentTerm.slug = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_name\":\n\t\t\t\t\t\tcurrentTerm.name = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_parent\":\n\t\t\t\t\t\tcurrentTerm.parent = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_description\":\n\t\t\t\t\t\tcurrentTerm.description = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term\":\n\t\t\t\t\t\tif (isCompleteWxrTerm(currentTerm)) {\n\t\t\t\t\t\t\tdata.terms.push(currentTerm);\n\t\t\t\t\t\t\t// Track nav_menu terms for building menus\n\t\t\t\t\t\t\tif (currentTerm.taxonomy === \"nav_menu\") {\n\t\t\t\t\t\t\t\tmenuTermsBySlug.set(currentTerm.slug, currentTerm.id);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentTerm = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcurrentPath.pop();\n\t\t\tcurrentText = \"\";\n\t\t});\n\n\t\tparser.on(\"error\", (err) => {\n\t\t\treject(new Error(`XML parsing error: ${err.message}`));\n\t\t});\n\n\t\tparser.on(\"end\", () => {\n\t\t\t// Post-process nav_menu_item posts into structured menus\n\t\t\tdata.navMenus = buildNavMenus(navMenuItemPosts, menuTermsBySlug);\n\t\t\tresolve(data);\n\t\t});\n\n\t\t// Pipe the stream through the parser\n\t\tstream.pipe(parser);\n\t});\n}\n\n/**\n * Parse a WordPress WXR export from a string\n *\n * Uses the non-streaming SAX parser API for compatibility with\n * environments that don't have Node.js streams (e.g., Cloudflare Workers).\n */\nexport function parseWxrString(xml: string): Promise<WxrData> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst parser = sax.parser(true, { trim: false, normalize: false });\n\n\t\tconst data: WxrData = {\n\t\t\tsite: {},\n\t\t\tposts: [],\n\t\t\tattachments: [],\n\t\t\tcategories: [],\n\t\t\ttags: [],\n\t\t\tauthors: [],\n\t\t\tterms: [],\n\t\t\tnavMenus: [],\n\t\t};\n\n\t\tlet currentPath: string[] = [];\n\t\tlet currentText = \"\";\n\t\tlet currentItem: WxrPost | null = null;\n\t\tlet currentAttachment: WxrAttachment | null = null;\n\t\tlet currentCategory: WxrCategory | null = null;\n\t\tlet currentTag: WxrTag | null = null;\n\t\tlet currentAuthor: WxrAuthor | null = null;\n\t\tlet currentTerm: Partial<WxrTerm> | null = null;\n\t\tlet currentMetaKey = \"\";\n\n\t\t// Track nav_menu_item posts for post-processing\n\t\tconst navMenuItemPosts: WxrPost[] = [];\n\t\t// Track menu term IDs by slug for linking items to menus\n\t\tconst menuTermsBySlug = new Map<string, number>();\n\n\t\tparser.onopentag = (node) => {\n\t\t\tconst tag = node.name.toLowerCase();\n\t\t\tcurrentPath.push(tag);\n\t\t\tcurrentText = \"\";\n\n\t\t\t// Start new elements\n\t\t\tif (tag === \"item\") {\n\t\t\t\tcurrentItem = {\n\t\t\t\t\tcategories: [],\n\t\t\t\t\ttags: [],\n\t\t\t\t\tcustomTaxonomies: new Map(),\n\t\t\t\t\tmeta: new Map(),\n\t\t\t\t};\n\t\t\t} else if (tag === \"wp:category\") {\n\t\t\t\tcurrentCategory = {};\n\t\t\t} else if (tag === \"wp:tag\") {\n\t\t\t\tcurrentTag = {};\n\t\t\t} else if (tag === \"wp:author\") {\n\t\t\t\tcurrentAuthor = {};\n\t\t\t} else if (tag === \"wp:term\") {\n\t\t\t\tcurrentTerm = {};\n\t\t\t}\n\n\t\t\t// Handle category/tag/custom taxonomy assignment in items\n\t\t\tif (tag === \"category\" && currentItem && node.attributes) {\n\t\t\t\tconst domain = attrStr(node.attributes.domain);\n\t\t\t\tconst nicename = attrStr(node.attributes.nicename);\n\t\t\t\tif (domain === \"category\" && nicename) {\n\t\t\t\t\tcurrentItem.categories.push(nicename);\n\t\t\t\t} else if (domain === \"post_tag\" && nicename) {\n\t\t\t\t\tcurrentItem.tags.push(nicename);\n\t\t\t\t} else if (domain && nicename && domain !== \"category\" && domain !== \"post_tag\") {\n\t\t\t\t\t// Custom taxonomy (including nav_menu)\n\t\t\t\t\tif (!currentItem.customTaxonomies) {\n\t\t\t\t\t\tcurrentItem.customTaxonomies = new Map();\n\t\t\t\t\t}\n\t\t\t\t\tconst existing = currentItem.customTaxonomies.get(domain) || [];\n\t\t\t\t\texisting.push(nicename);\n\t\t\t\t\tcurrentItem.customTaxonomies.set(domain, existing);\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tparser.ontext = (text) => {\n\t\t\tcurrentText += text;\n\t\t};\n\n\t\tparser.oncdata = (cdata) => {\n\t\t\tcurrentText += cdata;\n\t\t};\n\n\t\tparser.onclosetag = (tagName) => {\n\t\t\tconst tag = tagName.toLowerCase();\n\t\t\tconst text = currentText.trim();\n\n\t\t\t// Site metadata\n\t\t\tif (currentPath.length === 2 && currentPath[0] === \"rss\") {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"title\":\n\t\t\t\t\t\tdata.site.title = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"link\":\n\t\t\t\t\t\tdata.site.link = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"description\":\n\t\t\t\t\t\tdata.site.description = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"language\":\n\t\t\t\t\t\tdata.site.language = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:base_site_url\":\n\t\t\t\t\t\tdata.site.baseSiteUrl = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:base_blog_url\":\n\t\t\t\t\t\tdata.site.baseBlogUrl = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Item (post/page/attachment) parsing\n\t\t\tif (currentItem) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"title\":\n\t\t\t\t\t\tcurrentItem.title = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"link\":\n\t\t\t\t\t\tcurrentItem.link = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"pubdate\":\n\t\t\t\t\t\tcurrentItem.pubDate = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"dc:creator\":\n\t\t\t\t\t\tcurrentItem.creator = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"guid\":\n\t\t\t\t\t\tcurrentItem.guid = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"description\":\n\t\t\t\t\t\tcurrentItem.description = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"content:encoded\":\n\t\t\t\t\t\tcurrentItem.content = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"excerpt:encoded\":\n\t\t\t\t\t\tcurrentItem.excerpt = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_id\":\n\t\t\t\t\t\tcurrentItem.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_date\":\n\t\t\t\t\t\tcurrentItem.postDate = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_date_gmt\":\n\t\t\t\t\t\tcurrentItem.postDateGmt = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_modified\":\n\t\t\t\t\t\tcurrentItem.postModified = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_modified_gmt\":\n\t\t\t\t\t\tcurrentItem.postModifiedGmt = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:comment_status\":\n\t\t\t\t\t\tcurrentItem.commentStatus = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:ping_status\":\n\t\t\t\t\t\tcurrentItem.pingStatus = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_name\":\n\t\t\t\t\t\tcurrentItem.postName = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:status\":\n\t\t\t\t\t\tcurrentItem.status = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_parent\":\n\t\t\t\t\t\tcurrentItem.postParent = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:menu_order\":\n\t\t\t\t\t\tcurrentItem.menuOrder = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_type\":\n\t\t\t\t\t\tcurrentItem.postType = text;\n\t\t\t\t\t\t// If it's an attachment, convert to attachment type\n\t\t\t\t\t\tif (text === \"attachment\") {\n\t\t\t\t\t\t\tcurrentAttachment = {\n\t\t\t\t\t\t\t\tid: currentItem.id,\n\t\t\t\t\t\t\t\ttitle: currentItem.title,\n\t\t\t\t\t\t\t\turl: currentItem.link,\n\t\t\t\t\t\t\t\tpostDate: currentItem.postDate,\n\t\t\t\t\t\t\t\tmeta: new Map(),\n\t\t\t\t\t\t\t};\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:post_password\":\n\t\t\t\t\t\tcurrentItem.postPassword = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:is_sticky\":\n\t\t\t\t\t\tcurrentItem.isSticky = text === \"1\";\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:attachment_url\":\n\t\t\t\t\t\tif (currentAttachment) {\n\t\t\t\t\t\t\tcurrentAttachment.url = text;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:meta_key\":\n\t\t\t\t\t\tcurrentMetaKey = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:meta_value\":\n\t\t\t\t\t\tif (currentMetaKey && currentItem.meta) {\n\t\t\t\t\t\t\tcurrentItem.meta.set(currentMetaKey, text);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"item\":\n\t\t\t\t\t\t// End of item - categorize and store\n\t\t\t\t\t\tif (currentAttachment) {\n\t\t\t\t\t\t\tdata.attachments.push(currentAttachment);\n\t\t\t\t\t\t\tcurrentAttachment = null;\n\t\t\t\t\t\t} else if (currentItem.postType === \"nav_menu_item\") {\n\t\t\t\t\t\t\t// Track nav_menu_item posts for post-processing into menus\n\t\t\t\t\t\t\tnavMenuItemPosts.push(currentItem);\n\t\t\t\t\t\t\tdata.posts.push(currentItem);\n\t\t\t\t\t\t} else if (currentItem.postType !== \"attachment\") {\n\t\t\t\t\t\t\tdata.posts.push(currentItem);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentItem = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Category parsing\n\t\t\tif (currentCategory) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"wp:term_id\":\n\t\t\t\t\t\tcurrentCategory.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:category_nicename\":\n\t\t\t\t\t\tcurrentCategory.nicename = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:cat_name\":\n\t\t\t\t\t\tcurrentCategory.name = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:category_parent\":\n\t\t\t\t\t\tcurrentCategory.parent = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:category_description\":\n\t\t\t\t\t\tcurrentCategory.description = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:category\":\n\t\t\t\t\t\tif (currentCategory.name) {\n\t\t\t\t\t\t\tdata.categories.push(currentCategory);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentCategory = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Tag parsing\n\t\t\tif (currentTag) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"wp:term_id\":\n\t\t\t\t\t\tcurrentTag.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:tag_slug\":\n\t\t\t\t\t\tcurrentTag.slug = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:tag_name\":\n\t\t\t\t\t\tcurrentTag.name = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:tag_description\":\n\t\t\t\t\t\tcurrentTag.description = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:tag\":\n\t\t\t\t\t\tif (currentTag.name) {\n\t\t\t\t\t\t\tdata.tags.push(currentTag);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentTag = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Author parsing\n\t\t\tif (currentAuthor) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"wp:author_id\":\n\t\t\t\t\t\tcurrentAuthor.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_login\":\n\t\t\t\t\t\tcurrentAuthor.login = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_email\":\n\t\t\t\t\t\tcurrentAuthor.email = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_display_name\":\n\t\t\t\t\t\tcurrentAuthor.displayName = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_first_name\":\n\t\t\t\t\t\tcurrentAuthor.firstName = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author_last_name\":\n\t\t\t\t\t\tcurrentAuthor.lastName = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:author\":\n\t\t\t\t\t\tif (currentAuthor.login) {\n\t\t\t\t\t\t\tdata.authors.push(currentAuthor);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentAuthor = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Generic term parsing (wp:term elements - custom taxonomies, nav_menu, etc.)\n\t\t\tif (currentTerm) {\n\t\t\t\tswitch (tag) {\n\t\t\t\t\tcase \"wp:term_id\":\n\t\t\t\t\t\tcurrentTerm.id = parseInt(text, 10);\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_taxonomy\":\n\t\t\t\t\t\tcurrentTerm.taxonomy = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_slug\":\n\t\t\t\t\t\tcurrentTerm.slug = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_name\":\n\t\t\t\t\t\tcurrentTerm.name = text;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_parent\":\n\t\t\t\t\t\tcurrentTerm.parent = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term_description\":\n\t\t\t\t\t\tcurrentTerm.description = text || undefined;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"wp:term\":\n\t\t\t\t\t\tif (isCompleteWxrTerm(currentTerm)) {\n\t\t\t\t\t\t\tdata.terms.push(currentTerm);\n\t\t\t\t\t\t\t// Track nav_menu terms for building menus\n\t\t\t\t\t\t\tif (currentTerm.taxonomy === \"nav_menu\") {\n\t\t\t\t\t\t\t\tmenuTermsBySlug.set(currentTerm.slug, currentTerm.id);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcurrentTerm = null;\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcurrentPath.pop();\n\t\t\tcurrentText = \"\";\n\t\t};\n\n\t\tparser.onerror = (err) => {\n\t\t\treject(new Error(`XML parsing error: ${err.message}`));\n\t\t};\n\n\t\tparser.onend = () => {\n\t\t\t// Post-process nav_menu_item posts into structured menus\n\t\t\tdata.navMenus = buildNavMenus(navMenuItemPosts, menuTermsBySlug);\n\t\t\tresolve(data);\n\t\t};\n\n\t\t// Parse the string (non-streaming)\n\t\tparser.write(xml).close();\n\t});\n}\n\n/**\n * Build structured navigation menus from nav_menu_item posts\n */\nfunction buildNavMenus(\n\tnavMenuItemPosts: WxrPost[],\n\tmenuTermsBySlug: Map<string, number>,\n): WxrNavMenu[] {\n\t// Group menu items by menu slug\n\tconst menuItemsByMenu = new Map<string, WxrPost[]>();\n\n\tfor (const post of navMenuItemPosts) {\n\t\t// Get the nav_menu taxonomy assignment to find which menu this item belongs to\n\t\tconst navMenuSlugs = post.customTaxonomies?.get(\"nav_menu\");\n\t\tif (!navMenuSlugs || navMenuSlugs.length === 0) continue;\n\n\t\tconst menuSlug = navMenuSlugs[0];\n\t\tif (!menuSlug) continue;\n\n\t\tconst items = menuItemsByMenu.get(menuSlug) || [];\n\t\titems.push(post);\n\t\tmenuItemsByMenu.set(menuSlug, items);\n\t}\n\n\t// Build structured menus\n\tconst menus: WxrNavMenu[] = [];\n\n\tfor (const [menuSlug, posts] of menuItemsByMenu) {\n\t\tconst menuId = menuTermsBySlug.get(menuSlug) || 0;\n\n\t\t// Convert posts to menu items\n\t\tconst items: WxrNavMenuItem[] = posts.map((post) => {\n\t\t\tconst meta = post.meta;\n\t\t\tconst menuItemTypeRaw = meta.get(\"_menu_item_type\") || \"custom\";\n\t\t\tconst menuItemType: WxrNavMenuItem[\"type\"] =\n\t\t\t\tmenuItemTypeRaw === \"post_type\" || menuItemTypeRaw === \"taxonomy\"\n\t\t\t\t\t? menuItemTypeRaw\n\t\t\t\t\t: \"custom\";\n\t\t\tconst objectType = meta.get(\"_menu_item_object\");\n\t\t\tconst objectIdStr = meta.get(\"_menu_item_object_id\");\n\t\t\tconst url = meta.get(\"_menu_item_url\");\n\t\t\tconst parentIdStr = meta.get(\"_menu_item_menu_item_parent\");\n\t\t\tconst target = meta.get(\"_menu_item_target\");\n\t\t\tconst classesStr = meta.get(\"_menu_item_classes\");\n\n\t\t\t// Parse classes (stored as serialized PHP array)\n\t\t\tlet classes: string | undefined;\n\t\t\tif (classesStr) {\n\t\t\t\t// Simple extraction of class names from serialized PHP\n\t\t\t\tconst matches = classesStr.match(PHP_SERIALIZED_STRING_PATTERN);\n\t\t\t\tif (matches) {\n\t\t\t\t\tclasses = matches\n\t\t\t\t\t\t.map((m) => m.match(PHP_SERIALIZED_STRING_MATCH_PATTERN)?.[1])\n\t\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t\t.join(\" \");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tid: post.id || 0,\n\t\t\t\tmenuId,\n\t\t\t\tparentId: parentIdStr ? parseInt(parentIdStr, 10) || undefined : undefined,\n\t\t\t\tsortOrder: post.menuOrder || 0,\n\t\t\t\ttype: menuItemType,\n\t\t\t\tobjectType: objectType || undefined,\n\t\t\t\tobjectId: objectIdStr ? parseInt(objectIdStr, 10) : undefined,\n\t\t\t\turl: url || undefined,\n\t\t\t\ttitle: post.title || \"\",\n\t\t\t\ttarget: target || undefined,\n\t\t\t\tclasses: classes || undefined,\n\t\t\t};\n\t\t});\n\n\t\t// Sort items by menu_order\n\t\titems.sort((a, b) => a.sortOrder - b.sortOrder);\n\n\t\t// Find the menu name from the terms\n\t\t// For now, use the slug as both name and label; we could enhance this\n\t\t// by looking up the actual term name from data.terms\n\t\tmenus.push({\n\t\t\tid: menuId,\n\t\t\tname: menuSlug,\n\t\t\tlabel: menuSlug, // Will be enhanced when we have term data\n\t\t\titems,\n\t\t});\n\t}\n\n\treturn menus;\n}\n","/**\n * definePlugin() Helper\n *\n * Creates a properly typed and normalized plugin definition.\n * Supports two formats:\n *\n * 1. **Native format** -- full PluginDefinition with id, version, capabilities, etc.\n * Returns a ResolvedPlugin.\n *\n * 2. **Standard format** -- just { hooks, routes }. No id/version/capabilities.\n * Returns the same object (identity function for type inference).\n * Metadata comes from the descriptor at config time.\n *\n */\n\nimport type {\n\tPluginDefinition,\n\tResolvedPlugin,\n\tPluginHooks,\n\tResolvedPluginHooks,\n\tResolvedHook,\n\tHookConfig,\n\tPluginStorageConfig,\n\tStandardPluginDefinition,\n} from \"./types.js\";\n\n// Plugin ID validation patterns\nconst SIMPLE_ID = /^[a-z0-9-]+$/;\nconst SCOPED_ID = /^@[a-z0-9-]+\\/[a-z0-9-]+$/;\nconst SEMVER_PATTERN = /^\\d+\\.\\d+\\.\\d+/;\n\n/**\n * Define an EmDash plugin.\n *\n * **Standard format** -- the canonical format for plugins that work in both\n * trusted and sandboxed modes. No id/version -- those come from the descriptor.\n *\n * @example\n * ```typescript\n * import { definePlugin } from \"emdash\";\n *\n * export default definePlugin({\n * hooks: {\n * \"content:afterSave\": {\n * handler: async (event, ctx) => {\n * await ctx.kv.set(\"lastSave\", Date.now());\n * },\n * },\n * },\n * routes: {\n * status: {\n * handler: async (routeCtx, ctx) => ({ ok: true }),\n * },\n * },\n * });\n * ```\n *\n * **Native format** -- for plugins that need React admin, direct DB access,\n * or other capabilities not available in the sandbox.\n *\n * @example\n * ```typescript\n * import { definePlugin } from \"emdash\";\n *\n * export default definePlugin({\n * id: \"my-plugin\",\n * version: \"1.0.0\",\n * capabilities: [\"read:content\"],\n * hooks: {\n * \"content:beforeSave\": async (event, ctx) => {\n * ctx.log.info(\"Saving content\", { collection: event.collection });\n * return event.content;\n * }\n * },\n * routes: {\n * \"sync\": {\n * handler: async (ctx) => {\n * return { status: \"ok\" };\n * }\n * }\n * }\n * });\n * ```\n */\n// Native overload first -- PluginDefinition (with id+version) is more specific\nexport function definePlugin<TStorage extends PluginStorageConfig>(\n\tdefinition: PluginDefinition<TStorage>,\n): ResolvedPlugin<TStorage>;\n// Standard overload second -- catches { hooks, routes } without id/version\nexport function definePlugin(definition: StandardPluginDefinition): StandardPluginDefinition;\nexport function definePlugin<TStorage extends PluginStorageConfig>(\n\tdefinition: PluginDefinition<TStorage> | StandardPluginDefinition,\n): ResolvedPlugin<TStorage> | StandardPluginDefinition {\n\t// Standard format: has hooks/routes but no id/version\n\tif (!(\"id\" in definition) || !(\"version\" in definition)) {\n\t\t// Validate that the standard format has at least hooks or routes\n\t\tif (!(\"hooks\" in definition) && !(\"routes\" in definition)) {\n\t\t\tthrow new Error(\n\t\t\t\t\"Standard plugin format requires at least `hooks` or `routes`. \" +\n\t\t\t\t\t\"For native format, provide `id` and `version`.\",\n\t\t\t);\n\t\t}\n\t\t// Identity function -- return as-is for type inference.\n\t\t// The adapter (adaptSandboxEntry) will convert this to a ResolvedPlugin at build time.\n\t\treturn definition;\n\t}\n\n\treturn defineNativePlugin(definition);\n}\n\n/**\n * Internal: define a native-format plugin with full validation and normalization.\n */\nfunction defineNativePlugin<TStorage extends PluginStorageConfig>(\n\tdefinition: PluginDefinition<TStorage>,\n): ResolvedPlugin<TStorage> {\n\tconst {\n\t\tid,\n\t\tversion,\n\t\tcapabilities = [],\n\t\tallowedHosts = [],\n\t\thooks = {},\n\t\troutes = {},\n\t\tadmin = {},\n\t} = definition;\n\n\t// Default to empty object if no storage declared.\n\t// The empty object satisfies PluginStorageConfig (Record<string, ...>).\n\t// The cast is structurally safe because an empty record has no keys to conflict.\n\tconst storage = (definition.storage ?? {}) as TStorage;\n\n\t// Validate id format: either simple (my-plugin) or scoped (@scope/my-plugin)\n\t// Simple: lowercase alphanumeric with dashes\n\t// Scoped: @scope/name where both parts are lowercase alphanumeric with dashes\n\tif (!SIMPLE_ID.test(id) && !SCOPED_ID.test(id)) {\n\t\tthrow new Error(\n\t\t\t`Invalid plugin id \"${id}\". Must be lowercase alphanumeric with dashes (e.g., \"my-plugin\" or \"@scope/my-plugin\").`,\n\t\t);\n\t}\n\n\t// Validate version format (basic semver)\n\tif (!SEMVER_PATTERN.test(version)) {\n\t\tthrow new Error(`Invalid plugin version \"${version}\". Must be semver format (e.g., \"1.0.0\").`);\n\t}\n\n\t// Validate capabilities\n\tconst validCapabilities = new Set([\n\t\t\"network:fetch\",\n\t\t\"network:fetch:any\",\n\t\t\"read:content\",\n\t\t\"write:content\",\n\t\t\"read:media\",\n\t\t\"write:media\",\n\t\t\"read:users\",\n\t\t\"email:send\",\n\t\t\"email:provide\",\n\t\t\"email:intercept\",\n\t\t\"page:inject\",\n\t]);\n\tfor (const cap of capabilities) {\n\t\tif (!validCapabilities.has(cap)) {\n\t\t\tthrow new Error(`Invalid capability \"${cap}\" in plugin \"${id}\".`);\n\t\t}\n\t}\n\n\t// Capability implications: broader capabilities imply narrower ones\n\tconst normalizedCapabilities = [...capabilities];\n\tif (capabilities.includes(\"write:content\") && !capabilities.includes(\"read:content\")) {\n\t\tnormalizedCapabilities.push(\"read:content\");\n\t}\n\tif (capabilities.includes(\"write:media\") && !capabilities.includes(\"read:media\")) {\n\t\tnormalizedCapabilities.push(\"read:media\");\n\t}\n\tif (capabilities.includes(\"network:fetch:any\") && !capabilities.includes(\"network:fetch\")) {\n\t\tnormalizedCapabilities.push(\"network:fetch\");\n\t}\n\n\t// Normalize hooks\n\tconst resolvedHooks = resolveHooks(hooks, id);\n\n\treturn {\n\t\tid,\n\t\tversion,\n\t\tcapabilities: normalizedCapabilities,\n\t\tallowedHosts,\n\t\tstorage,\n\t\thooks: resolvedHooks,\n\t\troutes,\n\t\tadmin,\n\t};\n}\n\n/**\n * Resolve hooks to normalized format with defaults.\n *\n * PluginHooks and ResolvedPluginHooks share the same keys — each input value is\n * `HookConfig<H> | H` and the output is `ResolvedHook<H>`. TS can't narrow\n * the handler type through a dynamic key, so we assert at the loop boundary.\n */\nfunction resolveHooks(hooks: PluginHooks, pluginId: string): ResolvedPluginHooks {\n\tconst resolved: ResolvedPluginHooks = {};\n\n\tfor (const key of Object.keys(hooks) as Array<keyof PluginHooks>) {\n\t\tconst hook = hooks[key];\n\t\tif (hook) {\n\t\t\t(resolved as Record<string, unknown>)[key] = resolveHook(hook, pluginId);\n\t\t}\n\t}\n\n\treturn resolved;\n}\n\n/**\n * Check if a hook value is a config object (has a `handler` property)\n */\nfunction isHookConfig<THandler>(\n\thook: HookConfig<THandler> | THandler,\n): hook is HookConfig<THandler> {\n\treturn typeof hook === \"object\" && hook !== null && \"handler\" in hook;\n}\n\n/**\n * Resolve a single hook to normalized format\n */\nfunction resolveHook<THandler>(\n\thook: HookConfig<THandler> | THandler,\n\tpluginId: string,\n): ResolvedHook<THandler> {\n\t// If it's a config object with handler property\n\tif (isHookConfig(hook)) {\n\t\tif (hook.exclusive !== undefined && typeof hook.exclusive !== \"boolean\") {\n\t\t\tthrow new Error(\n\t\t\t\t`Invalid \"exclusive\" value in hook config for plugin \"${pluginId}\". Must be boolean.`,\n\t\t\t);\n\t\t}\n\t\treturn {\n\t\t\tpriority: hook.priority ?? 100,\n\t\t\ttimeout: hook.timeout ?? 5000,\n\t\t\tdependencies: hook.dependencies ?? [],\n\t\t\terrorPolicy: hook.errorPolicy ?? \"abort\",\n\t\t\texclusive: hook.exclusive ?? false,\n\t\t\thandler: hook.handler,\n\t\t\tpluginId,\n\t\t};\n\t}\n\n\t// It's just a handler function\n\treturn {\n\t\tpriority: 100,\n\t\ttimeout: 5000,\n\t\tdependencies: [],\n\t\terrorPolicy: \"abort\",\n\t\texclusive: false,\n\t\thandler: hook,\n\t\tpluginId,\n\t};\n}\n\nexport default definePlugin;\n","/**\n * Request Metadata Extraction\n *\n * Extracts normalized metadata (IP, user agent, referer, geo) from\n * incoming requests. Used by plugin route handlers to access request\n * context without touching raw headers.\n *\n */\n\nimport type { GeoInfo, RequestMeta } from \"./types.js\";\n\n/**\n * Cloudflare Workers `cf` object shape (subset we use).\n * Present on requests when running on Cloudflare Workers.\n */\ninterface CfProperties {\n\tcountry?: string;\n\tregion?: string;\n\tcity?: string;\n}\n\n/**\n * Loose validation for IPv4 and IPv6 addresses.\n * Accepts digits, hex chars, dots, and colons — rejects anything else\n * (e.g. HTML tags, scripts, or other non-IP garbage in spoofed headers).\n */\nconst IP_PATTERN = /^[\\da-fA-F.:]+$/;\n\n/**\n * Extract the first IP from an X-Forwarded-For header value.\n * The header may contain a comma-separated list of IPs; the first\n * entry is the original client IP.\n *\n * Returns null if the extracted value doesn't look like an IP address.\n */\nfunction parseFirstForwardedIp(header: string): string | null {\n\tconst first = header.split(\",\")[0];\n\tconst trimmed = first?.trim();\n\tif (!trimmed) return null;\n\treturn IP_PATTERN.test(trimmed) ? trimmed : null;\n}\n\n/**\n * Get the Cloudflare `cf` object from the request, if present.\n * Returns undefined when not running on Cloudflare Workers.\n */\nfunction getCfObject(request: Request): CfProperties | undefined {\n\treturn (request as unknown as { cf?: CfProperties }).cf;\n}\n\n/**\n * Extract geographic information from the Cloudflare `cf` object\n * attached to the request. Returns null when not running on CF Workers.\n */\nfunction extractGeo(cf: CfProperties | undefined): GeoInfo | null {\n\tif (!cf) return null;\n\n\tconst country = cf.country ?? null;\n\tconst region = cf.region ?? null;\n\tconst city = cf.city ?? null;\n\n\t// Only return geo if at least one field is populated\n\tif (country === null && region === null && city === null) return null;\n\n\treturn { country, region, city };\n}\n\n/**\n * Extract normalized request metadata from a Request object.\n *\n * IP resolution order:\n * 1. `CF-Connecting-IP` header — only trusted when a `cf` object is\n * present on the request (proving the request came through Cloudflare's\n * edge, which strips/overwrites client-supplied values).\n * 2. `X-Forwarded-For` header (first entry) — best-effort, spoofable\n * when there is no trusted reverse proxy.\n * 3. `null`\n */\nexport function extractRequestMeta(request: Request): RequestMeta {\n\tconst headers = request.headers;\n\tconst cf = getCfObject(request);\n\n\t// IP: only trust headers when the cf object confirms we're on Cloudflare.\n\t// Without a trusted reverse proxy, X-Forwarded-For is trivially spoofable.\n\tlet ip: string | null = null;\n\tif (cf) {\n\t\tconst cfIp = headers.get(\"cf-connecting-ip\")?.trim();\n\t\tif (cfIp && IP_PATTERN.test(cfIp)) {\n\t\t\tip = cfIp;\n\t\t}\n\t}\n\tif (!ip && cf) {\n\t\t// Only trust X-Forwarded-For when we're behind Cloudflare (which\n\t\t// overwrites the header). In standalone deployments without a trusted\n\t\t// proxy, XFF is trivially spoofable.\n\t\tconst xff = headers.get(\"x-forwarded-for\");\n\t\tip = xff ? parseFirstForwardedIp(xff) : null;\n\t}\n\n\tconst userAgent = headers.get(\"user-agent\")?.trim() || null;\n\tconst referer = headers.get(\"referer\")?.trim() || null;\n\tconst geo = extractGeo(cf);\n\n\treturn { ip, userAgent, referer, geo };\n}\n\n// =============================================================================\n// Header Sanitization for Sandbox\n// =============================================================================\n\n/**\n * Headers that must never cross the RPC boundary to sandboxed plugins.\n * Session tokens, auth credentials, and infrastructure headers are stripped\n * to prevent malicious plugins from exfiltrating sensitive data.\n */\nconst SANDBOX_STRIPPED_HEADERS = new Set([\n\t\"cookie\",\n\t\"set-cookie\",\n\t\"authorization\",\n\t\"proxy-authorization\",\n\t\"cf-access-jwt-assertion\",\n\t\"cf-access-client-id\",\n\t\"cf-access-client-secret\",\n\t\"x-emdash-request\",\n]);\n\n/**\n * Copy request headers into a plain object, stripping sensitive headers\n * that must not be exposed to sandboxed plugin code.\n */\nexport function sanitizeHeadersForSandbox(headers: Headers): Record<string, string> {\n\tconst safe: Record<string, string> = {};\n\theaders.forEach((value, key) => {\n\t\tif (!SANDBOX_STRIPPED_HEADERS.has(key)) {\n\t\t\tsafe[key] = value;\n\t\t}\n\t});\n\treturn safe;\n}\n","/**\n * Plugin Cron System\n *\n * Provides scheduled task execution for plugins:\n * - CronExecutor: claims overdue tasks, invokes per-plugin cron hook, updates next run.\n * - CronAccessImpl: per-plugin API for schedule/cancel/list.\n *\n */\n\nimport { Cron } from \"croner\";\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport type { Database } from \"../database/types.js\";\nimport type { CronAccess, CronEvent, CronTaskInfo } from \"./types.js\";\n\n/** Stale lock threshold in minutes */\nconst STALE_LOCK_MINUTES = 10;\n\n/**\n * Callback to invoke a plugin's cron hook.\n * Provided by PluginManager so CronExecutor stays decoupled from the hook pipeline.\n */\nexport type InvokeCronHookFn = (pluginId: string, event: CronEvent) => Promise<void>;\n\n/**\n * Callback to notify the scheduler that the next due time may have changed.\n */\nexport type RescheduleFn = () => void;\n\n// ─── CronExecutor ──────────────────────────────────────────────────────────\n\n/**\n * Executes overdue cron tasks.\n *\n * Called by platform-specific schedulers (NodeCronScheduler, EmDashScheduler DO,\n * PiggybackScheduler). Stateless — all state lives in the database.\n */\nexport class CronExecutor {\n\tconstructor(\n\t\tprivate db: Kysely<Database>,\n\t\tprivate invokeCronHook: InvokeCronHookFn,\n\t) {}\n\n\t/**\n\t * Process all overdue tasks.\n\t *\n\t * 1. Atomically claim tasks whose next_run_at <= now, status = idle, enabled = 1.\n\t * 2. For each claimed task, invoke the plugin's cron hook.\n\t * 3. On success: compute next_run_at and reset to idle, or delete one-shots.\n\t * 4. On failure: reset to idle (retry on next tick).\n\t */\n\tasync tick(): Promise<number> {\n\t\tconst now = new Date().toISOString();\n\t\tlet processed = 0;\n\n\t\t// Claim overdue tasks atomically\n\t\tconst claimed = await sql<{\n\t\t\tid: string;\n\t\t\tplugin_id: string;\n\t\t\ttask_name: string;\n\t\t\tschedule: string;\n\t\t\tis_oneshot: number;\n\t\t\tdata: string | null;\n\t\t\tnext_run_at: string;\n\t\t}>`\n\t\t\tUPDATE _emdash_cron_tasks\n\t\t\tSET status = 'running', locked_at = ${now}\n\t\t\tWHERE id IN (\n\t\t\t\tSELECT id FROM _emdash_cron_tasks\n\t\t\t\tWHERE next_run_at <= ${now}\n\t\t\t\t AND status = 'idle'\n\t\t\t\t AND enabled = 1\n\t\t\t\tORDER BY next_run_at ASC\n\t\t\t\tLIMIT 10\n\t\t\t)\n\t\t\tRETURNING id, plugin_id, task_name, schedule, is_oneshot, data, next_run_at\n\t\t`.execute(this.db);\n\n\t\tfor (const task of claimed.rows) {\n\t\t\t// Parse task data safely ��� malformed JSON must not crash the entire batch\n\t\t\tlet parsedData: Record<string, unknown> | undefined;\n\t\t\tif (task.data) {\n\t\t\t\ttry {\n\t\t\t\t\tparsedData = JSON.parse(task.data) as Record<string, unknown>;\n\t\t\t\t} catch {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`[cron] Invalid JSON data for ${task.plugin_id}:${task.task_name}, skipping`,\n\t\t\t\t\t);\n\t\t\t\t\tawait sql`\n\t\t\t\t\t\tUPDATE _emdash_cron_tasks\n\t\t\t\t\t\tSET status = 'idle', locked_at = NULL\n\t\t\t\t\t\tWHERE id = ${task.id}\n\t\t\t\t\t`.execute(this.db);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst event: CronEvent = {\n\t\t\t\tname: task.task_name,\n\t\t\t\tdata: parsedData,\n\t\t\t\tscheduledAt: task.next_run_at,\n\t\t\t};\n\n\t\t\tlet hookFailed = false;\n\t\t\ttry {\n\t\t\t\tawait this.invokeCronHook(task.plugin_id, event);\n\t\t\t} catch (error) {\n\t\t\t\thookFailed = true;\n\t\t\t\tconsole.error(`[cron] Hook failed for ${task.plugin_id}:${task.task_name}:`, error);\n\t\t\t}\n\n\t\t\tif (task.is_oneshot) {\n\t\t\t\tif (hookFailed) {\n\t\t\t\t\t// Retry metadata is namespaced under __emdash to avoid collisions\n\t\t\t\t\t// with plugin-controlled data fields.\n\t\t\t\t\tconst meta =\n\t\t\t\t\t\tparsedData?.__emdash != null && typeof parsedData.__emdash === \"object\"\n\t\t\t\t\t\t\t? (parsedData.__emdash as Record<string, unknown>)\n\t\t\t\t\t\t\t: undefined;\n\t\t\t\t\tconst raw = meta?.retryCount;\n\t\t\t\t\tconst retryCount =\n\t\t\t\t\t\ttypeof raw === \"number\" && Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 0;\n\t\t\t\t\tconst MAX_ONESHOT_RETRIES = 5;\n\n\t\t\t\t\tif (retryCount >= MAX_ONESHOT_RETRIES) {\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t`[cron] One-shot task ${task.plugin_id}:${task.task_name} exceeded ${MAX_ONESHOT_RETRIES} retries, removing`,\n\t\t\t\t\t\t);\n\t\t\t\t\t\tawait sql`\n\t\t\t\t\t\tDELETE FROM _emdash_cron_tasks WHERE id = ${task.id}\n\t\t\t\t\t`.execute(this.db);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Retry with exponential backoff: 1m, 2m, 4m, 8m, 16m\n\t\t\t\t\t\tconst backoffMs = 60_000 * Math.pow(2, retryCount);\n\t\t\t\t\t\tconst retryAt = new Date(Date.now() + backoffMs).toISOString();\n\t\t\t\t\t\tconst updatedData = JSON.stringify({\n\t\t\t\t\t\t\t...parsedData,\n\t\t\t\t\t\t\t__emdash: { ...meta, retryCount: retryCount + 1 },\n\t\t\t\t\t\t});\n\t\t\t\t\t\tawait sql`\n\t\t\t\t\t\tUPDATE _emdash_cron_tasks\n\t\t\t\t\t\tSET status = 'idle', locked_at = NULL, next_run_at = ${retryAt}, data = ${updatedData}\n\t\t\t\t\t\tWHERE id = ${task.id}\n\t\t\t\t\t`.execute(this.db);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Success: delete the one-shot task\n\t\t\t\t\tawait sql`\n\t\t\t\t\t\tDELETE FROM _emdash_cron_tasks WHERE id = ${task.id}\n\t\t\t\t\t`.execute(this.db);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Recurring: compute next run and reset\n\t\t\t\tconst nextRun = nextCronTime(task.schedule);\n\t\t\t\tawait sql`\n\t\t\t\t\tUPDATE _emdash_cron_tasks\n\t\t\t\t\tSET status = 'idle',\n\t\t\t\t\t\tlocked_at = NULL,\n\t\t\t\t\t\tlast_run_at = ${now},\n\t\t\t\t\t\tnext_run_at = ${nextRun}\n\t\t\t\t\tWHERE id = ${task.id}\n\t\t\t\t`.execute(this.db);\n\t\t\t}\n\n\t\t\tprocessed++;\n\t\t}\n\n\t\treturn processed;\n\t}\n\n\t/**\n\t * Recover tasks stuck in 'running' for more than STALE_LOCK_MINUTES.\n\t * These likely crashed mid-execution.\n\t */\n\tasync recoverStaleLocks(): Promise<number> {\n\t\tconst cutoff = new Date(Date.now() - STALE_LOCK_MINUTES * 60 * 1000).toISOString();\n\n\t\tconst result = await sql`\n\t\t\tUPDATE _emdash_cron_tasks\n\t\t\tSET status = 'idle', locked_at = NULL\n\t\t\tWHERE status = 'running'\n\t\t\t AND locked_at < ${cutoff}\n\t\t`.execute(this.db);\n\n\t\treturn Number(result.numAffectedRows ?? 0);\n\t}\n\n\t/**\n\t * Get the next due time across all enabled tasks.\n\t * Returns null if no tasks are scheduled.\n\t */\n\tasync getNextDueTime(): Promise<string | null> {\n\t\tconst result = await sql<{ next: string | null }>`\n\t\t\tSELECT MIN(next_run_at) as next\n\t\t\tFROM _emdash_cron_tasks\n\t\t\tWHERE status = 'idle' AND enabled = 1\n\t\t`.execute(this.db);\n\n\t\treturn result.rows[0]?.next ?? null;\n\t}\n}\n\n// ─── CronAccessImpl ────────────────────────────────────────────────────────\n\n/**\n * Per-plugin cron API implementation.\n * Scoped to a single plugin ID — plugins cannot see or modify other plugins' tasks.\n */\nexport class CronAccessImpl implements CronAccess {\n\tconstructor(\n\t\tprivate db: Kysely<Database>,\n\t\tprivate pluginId: string,\n\t\tprivate reschedule: RescheduleFn,\n\t) {}\n\n\tasync schedule(\n\t\tname: string,\n\t\topts: { schedule: string; data?: Record<string, unknown> },\n\t): Promise<void> {\n\t\tvalidateTaskName(name);\n\t\tvalidateSchedule(opts.schedule);\n\n\t\tconst oneshot = isOneShot(opts.schedule);\n\t\tconst nextRun = oneshot ? opts.schedule : nextCronTime(opts.schedule);\n\t\tconst dataJson = opts.data ? JSON.stringify(opts.data) : null;\n\t\tconst id = ulid();\n\n\t\t// Upsert: if task already exists for this plugin+name, update it.\n\t\t// Guard: don't clobber a task that is currently executing.\n\t\tawait sql`\n\t\t\tINSERT INTO _emdash_cron_tasks (id, plugin_id, task_name, schedule, is_oneshot, data, next_run_at, status, enabled)\n\t\t\tVALUES (${id}, ${this.pluginId}, ${name}, ${opts.schedule}, ${oneshot ? 1 : 0}, ${dataJson}, ${nextRun}, 'idle', 1)\n\t\t\tON CONFLICT (plugin_id, task_name) DO UPDATE SET\n\t\t\t\tschedule = ${opts.schedule},\n\t\t\t\tis_oneshot = ${oneshot ? 1 : 0},\n\t\t\t\tdata = ${dataJson},\n\t\t\t\tnext_run_at = ${nextRun},\n\t\t\t\tstatus = CASE WHEN _emdash_cron_tasks.status = 'running' THEN 'running' ELSE 'idle' END,\n\t\t\t\tlocked_at = CASE WHEN _emdash_cron_tasks.status = 'running' THEN _emdash_cron_tasks.locked_at ELSE NULL END,\n\t\t\t\tenabled = 1\n\t\t`.execute(this.db);\n\n\t\tthis.reschedule();\n\t}\n\n\tasync cancel(name: string): Promise<void> {\n\t\tawait sql`\n\t\t\tDELETE FROM _emdash_cron_tasks\n\t\t\tWHERE plugin_id = ${this.pluginId} AND task_name = ${name}\n\t\t`.execute(this.db);\n\n\t\tthis.reschedule();\n\t}\n\n\tasync list(): Promise<CronTaskInfo[]> {\n\t\tconst rows = await sql<{\n\t\t\ttask_name: string;\n\t\t\tschedule: string;\n\t\t\tnext_run_at: string;\n\t\t\tlast_run_at: string | null;\n\t\t}>`\n\t\t\tSELECT task_name, schedule, next_run_at, last_run_at\n\t\t\tFROM _emdash_cron_tasks\n\t\t\tWHERE plugin_id = ${this.pluginId} AND enabled = 1\n\t\t\tORDER BY next_run_at ASC\n\t\t`.execute(this.db);\n\n\t\treturn rows.rows.map((row) => ({\n\t\t\tname: row.task_name,\n\t\t\tschedule: row.schedule,\n\t\t\tnextRunAt: row.next_run_at,\n\t\t\tlastRunAt: row.last_run_at,\n\t\t}));\n\t}\n}\n\n// ─── Cron task lifecycle helpers ────────────────────────────────────────────\n\n/**\n * Enable or disable all cron tasks for a plugin.\n * Called by admin disable/enable endpoints and PluginManager lifecycle.\n * Gracefully handles the cron table not existing yet (pre-migration).\n */\nexport async function setCronTasksEnabled(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n\tenabled: boolean,\n): Promise<void> {\n\ttry {\n\t\tawait sql`\n\t\t\tUPDATE _emdash_cron_tasks\n\t\t\tSET enabled = ${enabled ? 1 : 0}\n\t\t\tWHERE plugin_id = ${pluginId}\n\t\t`.execute(db);\n\t} catch {\n\t\t// Cron table may not exist yet (pre-migration). Non-fatal.\n\t}\n}\n\n// ─── Cron utilities ────────────────────────────────────────────────────────\n\n/**\n * Compute the next fire time for a cron expression.\n * Supports standard cron (5-field), extended (6-field with seconds), and\n * aliases like @daily, @weekly, @hourly, @monthly, @yearly.\n */\nexport function nextCronTime(expression: string): string {\n\tconst job = new Cron(expression);\n\tconst next = job.nextRun();\n\tif (!next) {\n\t\tthrow new Error(`Invalid cron expression or no future run: \"${expression}\"`);\n\t}\n\treturn next.toISOString();\n}\n\n/**\n * Check whether a string is a valid cron expression.\n */\nfunction isCronExpression(schedule: string): boolean {\n\ttry {\n\t\t// Cron constructor validates; we discard the instance immediately.\n\t\tconst _cron = new Cron(schedule);\n\t\tvoid _cron;\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Check if a schedule string is a one-shot (ISO 8601 datetime) rather than\n * a recurring cron expression.\n *\n * Tries to parse as a cron expression first. Only if that fails does it\n * attempt Date.parse. This avoids misclassifying cron range expressions\n * like \"1-5 * * * *\" which Date.parse accepts as valid dates.\n */\nexport function isOneShot(schedule: string): boolean {\n\tif (schedule.startsWith(\"@\")) return false;\n\tif (isCronExpression(schedule)) return false;\n\treturn !isNaN(Date.parse(schedule));\n}\n\n/** Max length for a task name */\nconst MAX_TASK_NAME_LENGTH = 128;\n/** Task name pattern: alphanumeric, dashes, underscores */\nconst TASK_NAME_RE = /^[a-zA-Z][a-zA-Z0-9_-]*$/;\n\n/**\n * Validate a cron task name.\n * Must be non-empty, ≤128 chars, alphanumeric with dashes/underscores.\n */\nexport function validateTaskName(name: string): void {\n\tif (!name || name.length > MAX_TASK_NAME_LENGTH) {\n\t\tthrow new Error(\n\t\t\t`Invalid task name: must be 1-${MAX_TASK_NAME_LENGTH} characters, got ${name.length}`,\n\t\t);\n\t}\n\tif (!TASK_NAME_RE.test(name)) {\n\t\tthrow new Error(\n\t\t\t`Invalid task name \"${name}\": must start with a letter and contain only letters, numbers, dashes, or underscores`,\n\t\t);\n\t}\n}\n\n/**\n * Validate a schedule string at registration time.\n * Must be a valid cron expression or a parseable ISO 8601 datetime.\n */\nexport function validateSchedule(schedule: string): void {\n\tif (!schedule || schedule.length > 256) {\n\t\tthrow new Error(`Invalid schedule: must be 1-256 characters, got ${schedule.length}`);\n\t}\n\n\t// Try cron first\n\tif (isCronExpression(schedule)) return;\n\n\tconst parsed = Date.parse(schedule);\n\tif (isNaN(parsed)) {\n\t\tthrow new Error(\n\t\t\t`Invalid schedule \"${schedule}\": must be a valid cron expression or ISO 8601 datetime`,\n\t\t);\n\t}\n}\n","/**\n * Plugin Context v2\n *\n * Creates the unified context object provided to plugins in all hooks and routes.\n *\n */\n\nimport type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { ContentRepository } from \"../database/repositories/content.js\";\nimport { MediaRepository } from \"../database/repositories/media.js\";\nimport { OptionsRepository } from \"../database/repositories/options.js\";\nimport { PluginStorageRepository } from \"../database/repositories/plugin-storage.js\";\nimport { SeoRepository } from \"../database/repositories/seo.js\";\nimport { UserRepository } from \"../database/repositories/user.js\";\nimport { withTransaction } from \"../database/transaction.js\";\nimport type { Database } from \"../database/types.js\";\nimport { validateExternalUrl, SsrfError, stripCredentialHeaders } from \"../import/ssrf.js\";\nimport type { Storage } from \"../storage/types.js\";\nimport { CronAccessImpl } from \"./cron.js\";\nimport type { EmailPipeline } from \"./email.js\";\nimport type {\n\tResolvedPlugin,\n\tPluginContext,\n\tPluginStorageConfig,\n\tStorageCollection,\n\tKVAccess,\n\tCronAccess,\n\tEmailAccess,\n\tContentAccess,\n\tContentAccessWithWrite,\n\tMediaAccess,\n\tMediaAccessWithWrite,\n\tHttpAccess,\n\tLogAccess,\n\tSiteInfo,\n\tUserAccess,\n\tUserInfo,\n\tContentItem,\n\tContentItemSeoInput,\n\tContentWriteInput,\n\tMediaItem,\n\tPaginatedResult,\n\tQueryOptions,\n\tContentListOptions,\n\tMediaListOptions,\n} from \"./types.js\";\n\n// =============================================================================\n// KV Access\n// =============================================================================\n\n/**\n * Create KV accessor for a plugin\n * All keys are automatically prefixed with the plugin ID\n */\nexport function createKVAccess(optionsRepo: OptionsRepository, pluginId: string): KVAccess {\n\tconst prefix = `plugin:${pluginId}:`;\n\n\treturn {\n\t\tasync get<T>(key: string): Promise<T | null> {\n\t\t\treturn optionsRepo.get<T>(`${prefix}${key}`);\n\t\t},\n\n\t\tasync set(key: string, value: unknown): Promise<void> {\n\t\t\tawait optionsRepo.set(`${prefix}${key}`, value);\n\t\t},\n\n\t\tasync delete(key: string): Promise<boolean> {\n\t\t\treturn optionsRepo.delete(`${prefix}${key}`);\n\t\t},\n\n\t\tasync list(keyPrefix?: string): Promise<Array<{ key: string; value: unknown }>> {\n\t\t\tconst fullPrefix = `${prefix}${keyPrefix ?? \"\"}`;\n\t\t\tconst entriesMap = await optionsRepo.getByPrefix(fullPrefix);\n\t\t\tconst result: Array<{ key: string; value: unknown }> = [];\n\t\t\tfor (const [fullKey, value] of entriesMap) {\n\t\t\t\tresult.push({\n\t\t\t\t\tkey: fullKey.slice(prefix.length),\n\t\t\t\t\tvalue,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn result;\n\t\t},\n\t};\n}\n\n// =============================================================================\n// Storage Access\n// =============================================================================\n\n/**\n * Create storage collection accessor for a plugin\n * Wraps PluginStorageRepository with the v2 interface (no async iterators)\n */\nfunction createStorageCollection<T>(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n\tcollectionName: string,\n\tindexes: Array<string | string[]>,\n): StorageCollection<T> {\n\tconst repo = new PluginStorageRepository<T>(db, pluginId, collectionName, indexes);\n\n\treturn {\n\t\tget: (id) => repo.get(id),\n\t\tput: (id, data) => repo.put(id, data),\n\t\tdelete: (id) => repo.delete(id),\n\t\texists: (id) => repo.exists(id),\n\t\tgetMany: (ids) => repo.getMany(ids),\n\t\tputMany: (items) => repo.putMany(items),\n\t\tdeleteMany: (ids) => repo.deleteMany(ids),\n\t\tcount: (where) => repo.count(where),\n\n\t\t// Query returns PaginatedResult instead of the old format\n\t\tasync query(options?: QueryOptions): Promise<PaginatedResult<{ id: string; data: T }>> {\n\t\t\tconst result = await repo.query({\n\t\t\t\twhere: options?.where,\n\t\t\t\torderBy: options?.orderBy,\n\t\t\t\tlimit: options?.limit,\n\t\t\t\tcursor: options?.cursor,\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\titems: result.items,\n\t\t\t\tcursor: result.cursor,\n\t\t\t\thasMore: result.hasMore,\n\t\t\t};\n\t\t},\n\t};\n}\n\n/**\n * Create storage accessor with all declared collections\n */\nexport function createStorageAccess<T extends PluginStorageConfig>(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n\tstorageConfig: T,\n): Record<string, StorageCollection> {\n\tconst storage: Record<string, StorageCollection> = {};\n\n\tfor (const [collectionName, config] of Object.entries(storageConfig)) {\n\t\tconst allIndexes = [...config.indexes, ...(config.uniqueIndexes ?? [])];\n\t\tstorage[collectionName] = createStorageCollection(db, pluginId, collectionName, allIndexes);\n\t}\n\n\treturn storage;\n}\n\n// =============================================================================\n// Content Access\n// =============================================================================\n\n/**\n * Extract `seo` from a plugin-supplied content write input and return both\n * parts. Mutates nothing — returns a new field map without the `seo` key.\n */\nfunction splitSeoFromInput(input: ContentWriteInput): {\n\tfields: Record<string, unknown>;\n\tseo: ContentItemSeoInput | undefined;\n} {\n\tconst { seo, ...fields } = input;\n\t// Reject non-object seo values rather than silently dropping them.\n\tif (seo !== undefined && (seo === null || typeof seo !== \"object\" || Array.isArray(seo))) {\n\t\tthrow new Error(\"content.seo must be an object\");\n\t}\n\treturn { fields, seo };\n}\n\n/**\n * Reject writing SEO to a collection that does not have it enabled.\n * Matches the REST API behavior (VALIDATION_ERROR).\n */\nasync function assertSeoEnabled(\n\tseoRepo: SeoRepository,\n\tcollection: string,\n\tseo: ContentItemSeoInput | undefined,\n): Promise<boolean> {\n\tconst hasSeo = await seoRepo.isEnabled(collection);\n\tif (seo !== undefined && !hasSeo) {\n\t\tthrow new Error(\n\t\t\t`Collection \"${collection}\" does not have SEO enabled. ` +\n\t\t\t\t`Remove the seo field or enable SEO on this collection.`,\n\t\t);\n\t}\n\treturn hasSeo;\n}\n\n/**\n * Create read-only content access\n */\nexport function createContentAccess(db: Kysely<Database>): ContentAccess {\n\tconst contentRepo = new ContentRepository(db);\n\tconst seoRepo = new SeoRepository(db);\n\n\treturn {\n\t\tasync get(collection: string, id: string): Promise<ContentItem | null> {\n\t\t\tconst item = await contentRepo.findById(collection, id);\n\t\t\tif (!item) return null;\n\n\t\t\tconst result: ContentItem = {\n\t\t\t\tid: item.id,\n\t\t\t\ttype: item.type,\n\t\t\t\tslug: item.slug,\n\t\t\t\tstatus: item.status,\n\t\t\t\tdata: item.data,\n\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t\tupdatedAt: item.updatedAt,\n\t\t\t\tlocale: item.locale,\n\t\t\t\tpublishedAt: item.publishedAt,\n\t\t\t};\n\n\t\t\tif (await seoRepo.isEnabled(collection)) {\n\t\t\t\tresult.seo = await seoRepo.get(collection, item.id);\n\t\t\t}\n\n\t\t\treturn result;\n\t\t},\n\n\t\tasync list(\n\t\t\tcollection: string,\n\t\t\toptions?: ContentListOptions,\n\t\t): Promise<PaginatedResult<ContentItem>> {\n\t\t\t// Convert orderBy format if provided\n\t\t\tlet orderBy: { field: string; direction: \"asc\" | \"desc\" } | undefined;\n\t\t\tif (options?.orderBy) {\n\t\t\t\tconst entries = Object.entries(options.orderBy);\n\t\t\t\tconst first = entries[0];\n\t\t\t\tif (first) {\n\t\t\t\t\torderBy = { field: first[0], direction: first[1] };\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst result = await contentRepo.findMany(collection, {\n\t\t\t\tlimit: options?.limit ?? 50,\n\t\t\t\tcursor: options?.cursor,\n\t\t\t\torderBy,\n\t\t\t});\n\n\t\t\tconst items: ContentItem[] = result.items.map((item) => ({\n\t\t\t\tid: item.id,\n\t\t\t\ttype: item.type,\n\t\t\t\tslug: item.slug,\n\t\t\t\tstatus: item.status,\n\t\t\t\tdata: item.data,\n\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t\tupdatedAt: item.updatedAt,\n\t\t\t\tlocale: item.locale,\n\t\t\t\tpublishedAt: item.publishedAt,\n\t\t\t}));\n\n\t\t\tif (items.length > 0 && (await seoRepo.isEnabled(collection))) {\n\t\t\t\tconst seoMap = await seoRepo.getMany(\n\t\t\t\t\tcollection,\n\t\t\t\t\titems.map((i) => i.id),\n\t\t\t\t);\n\t\t\t\tfor (const item of items) {\n\t\t\t\t\tconst seo = seoMap.get(item.id);\n\t\t\t\t\tif (seo) item.seo = seo;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\titems,\n\t\t\t\tcursor: result.nextCursor,\n\t\t\t\thasMore: !!result.nextCursor,\n\t\t\t};\n\t\t},\n\t};\n}\n\n/**\n * Create full content access with write operations.\n *\n * `create` and `update` accept a reserved `seo` key in their `data`\n * argument. When present, it is routed to the core SEO panel\n * (`_emdash_seo`) via `SeoRepository.upsert`, in the same transaction as\n * the content write. The returned `ContentItem.seo` reflects the resulting\n * SEO state for SEO-enabled collections.\n */\nexport function createContentAccessWithWrite(db: Kysely<Database>): ContentAccessWithWrite {\n\tconst readAccess = createContentAccess(db);\n\n\treturn {\n\t\t...readAccess,\n\n\t\tasync create(collection: string, data: ContentWriteInput): Promise<ContentItem> {\n\t\t\tconst { fields, seo } = splitSeoFromInput(data);\n\n\t\t\treturn withTransaction(db, async (trx) => {\n\t\t\t\tconst trxContentRepo = new ContentRepository(trx);\n\t\t\t\tconst trxSeoRepo = new SeoRepository(trx);\n\n\t\t\t\tconst hasSeo = await assertSeoEnabled(trxSeoRepo, collection, seo);\n\n\t\t\t\tconst item = await trxContentRepo.create({\n\t\t\t\t\ttype: collection,\n\t\t\t\t\tdata: fields,\n\t\t\t\t});\n\n\t\t\t\tconst result: ContentItem = {\n\t\t\t\t\tid: item.id,\n\t\t\t\t\ttype: item.type,\n\t\t\t\t\tslug: item.slug,\n\t\t\t\t\tstatus: item.status,\n\t\t\t\t\tdata: item.data,\n\t\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t\t\tupdatedAt: item.updatedAt,\n\t\t\t\t\tlocale: item.locale,\n\t\t\t\t\tpublishedAt: item.publishedAt,\n\t\t\t\t};\n\n\t\t\t\tif (hasSeo) {\n\t\t\t\t\tresult.seo =\n\t\t\t\t\t\tseo !== undefined\n\t\t\t\t\t\t\t? await trxSeoRepo.upsert(collection, item.id, seo)\n\t\t\t\t\t\t\t: await trxSeoRepo.get(collection, item.id);\n\t\t\t\t}\n\n\t\t\t\treturn result;\n\t\t\t});\n\t\t},\n\n\t\tasync update(collection: string, id: string, data: ContentWriteInput): Promise<ContentItem> {\n\t\t\tconst { fields, seo } = splitSeoFromInput(data);\n\n\t\t\treturn withTransaction(db, async (trx) => {\n\t\t\t\tconst trxContentRepo = new ContentRepository(trx);\n\t\t\t\tconst trxSeoRepo = new SeoRepository(trx);\n\n\t\t\t\tconst hasSeo = await assertSeoEnabled(trxSeoRepo, collection, seo);\n\n\t\t\t\t// Pass the `data` payload to ContentRepository.update only when\n\t\t\t\t// there are field updates — passing an empty object would still\n\t\t\t\t// bump updated_at/version, but we want a seo-only call to touch\n\t\t\t\t// only the SEO table. ContentRepository.update handles the no-op\n\t\t\t\t// path by returning the current row.\n\t\t\t\tconst hasFieldUpdates = Object.keys(fields).length > 0;\n\t\t\t\tconst item = hasFieldUpdates\n\t\t\t\t\t? await trxContentRepo.update(collection, id, { data: fields })\n\t\t\t\t\t: await (async () => {\n\t\t\t\t\t\t\tconst existing = await trxContentRepo.findById(collection, id);\n\t\t\t\t\t\t\tif (!existing) throw new Error(\"Content not found\");\n\t\t\t\t\t\t\treturn existing;\n\t\t\t\t\t\t})();\n\n\t\t\t\tconst result: ContentItem = {\n\t\t\t\t\tid: item.id,\n\t\t\t\t\ttype: item.type,\n\t\t\t\t\tslug: item.slug,\n\t\t\t\t\tstatus: item.status,\n\t\t\t\t\tdata: item.data,\n\t\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t\t\tupdatedAt: item.updatedAt,\n\t\t\t\t\tlocale: item.locale,\n\t\t\t\t\tpublishedAt: item.publishedAt,\n\t\t\t\t};\n\n\t\t\t\tif (hasSeo) {\n\t\t\t\t\tresult.seo =\n\t\t\t\t\t\tseo !== undefined\n\t\t\t\t\t\t\t? await trxSeoRepo.upsert(collection, item.id, seo)\n\t\t\t\t\t\t\t: await trxSeoRepo.get(collection, item.id);\n\t\t\t\t}\n\n\t\t\t\treturn result;\n\t\t\t});\n\t\t},\n\n\t\tasync delete(collection: string, id: string): Promise<boolean> {\n\t\t\tconst contentRepo = new ContentRepository(db);\n\t\t\treturn contentRepo.delete(collection, id);\n\t\t},\n\t};\n}\n\n// =============================================================================\n// Media Access\n// =============================================================================\n\n/**\n * Create read-only media access\n */\nexport function createMediaAccess(db: Kysely<Database>): MediaAccess {\n\tconst mediaRepo = new MediaRepository(db);\n\n\treturn {\n\t\tasync get(id: string): Promise<MediaItem | null> {\n\t\t\tconst item = await mediaRepo.findById(id);\n\t\t\tif (!item) return null;\n\n\t\t\treturn {\n\t\t\t\tid: item.id,\n\t\t\t\tfilename: item.filename,\n\t\t\t\tmimeType: item.mimeType,\n\t\t\t\tsize: item.size,\n\t\t\t\t// Construct URL from storage key (or use a sensible default path)\n\t\t\t\turl: `/media/${item.id}/${item.filename}`,\n\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t};\n\t\t},\n\n\t\tasync list(options?: MediaListOptions): Promise<PaginatedResult<MediaItem>> {\n\t\t\tconst result = await mediaRepo.findMany({\n\t\t\t\tlimit: options?.limit ?? 50,\n\t\t\t\tcursor: options?.cursor,\n\t\t\t\tmimeType: options?.mimeType,\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\titems: result.items.map((item) => ({\n\t\t\t\t\tid: item.id,\n\t\t\t\t\tfilename: item.filename,\n\t\t\t\t\tmimeType: item.mimeType,\n\t\t\t\t\tsize: item.size,\n\t\t\t\t\turl: `/media/${item.id}/${item.filename}`,\n\t\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t\t})),\n\t\t\t\tcursor: result.nextCursor,\n\t\t\t\thasMore: !!result.nextCursor,\n\t\t\t};\n\t\t},\n\t};\n}\n\n/**\n * Create full media access with write operations.\n * If storage is not provided, upload() will throw at call time.\n */\nexport function createMediaAccessWithWrite(\n\tdb: Kysely<Database>,\n\tgetUploadUrlFn: (\n\t\tfilename: string,\n\t\tcontentType: string,\n\t) => Promise<{ uploadUrl: string; mediaId: string }>,\n\tstorage?: Storage,\n): MediaAccessWithWrite {\n\tconst mediaRepo = new MediaRepository(db);\n\tconst readAccess = createMediaAccess(db);\n\n\treturn {\n\t\t...readAccess,\n\n\t\tgetUploadUrl: getUploadUrlFn,\n\n\t\tasync upload(\n\t\t\tfilename: string,\n\t\t\tcontentType: string,\n\t\t\tbytes: ArrayBuffer,\n\t\t): Promise<{ mediaId: string; storageKey: string; url: string }> {\n\t\t\tif (!storage) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"Media upload() requires a storage backend. Configure storage in PluginContextFactoryOptions.\",\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Generate a storage key with a unique prefix\n\t\t\tconst keyPrefix = ulid();\n\t\t\t// Extract extension from basename (ignore path separators)\n\t\t\tconst basename = filename.split(\"/\").pop() ?? filename;\n\t\t\tconst dotIdx = basename.lastIndexOf(\".\");\n\t\t\tconst ext = dotIdx > 0 ? basename.slice(dotIdx).toLowerCase() : \"\";\n\t\t\tconst storageKey = `${keyPrefix}${ext}`;\n\n\t\t\t// Upload to storage first\n\t\t\tawait storage.upload({\n\t\t\t\tkey: storageKey,\n\t\t\t\tbody: new Uint8Array(bytes),\n\t\t\t\tcontentType,\n\t\t\t});\n\n\t\t\t// Create DB record — clean up storage on failure\n\t\t\tlet media;\n\t\t\ttry {\n\t\t\t\tmedia = await mediaRepo.create({\n\t\t\t\t\tfilename: basename,\n\t\t\t\t\tmimeType: contentType,\n\t\t\t\t\tsize: bytes.byteLength,\n\t\t\t\t\tstorageKey,\n\t\t\t\t\tstatus: \"ready\",\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\ttry {\n\t\t\t\t\tawait storage.delete(storageKey);\n\t\t\t\t} catch {\n\t\t\t\t\t// Best-effort cleanup\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tmediaId: media.id,\n\t\t\t\tstorageKey,\n\t\t\t\turl: `/_emdash/api/media/file/${storageKey}`,\n\t\t\t};\n\t\t},\n\n\t\tasync delete(id: string): Promise<boolean> {\n\t\t\treturn mediaRepo.delete(id);\n\t\t},\n\t};\n}\n\n// =============================================================================\n// HTTP Access\n// =============================================================================\n\n/** Maximum number of redirects to follow in plugin HTTP access */\nconst MAX_PLUGIN_REDIRECTS = 5;\n\n/**\n * Check if a hostname matches any pattern in the allowed list.\n * Patterns: \"*\" matches all, \"*.example.com\" matches subdomains AND bare \"example.com\",\n * \"api.example.com\" matches exactly.\n */\nfunction isHostAllowed(host: string, allowedHosts: string[]): boolean {\n\treturn allowedHosts.some((pattern) => {\n\t\tif (pattern === \"*\") return true;\n\t\tif (pattern.startsWith(\"*.\")) {\n\t\t\tconst suffix = pattern.slice(1); // \".example.com\"\n\t\t\t// Match subdomains (foo.example.com) and bare domain (example.com)\n\t\t\treturn host.endsWith(suffix) || host === pattern.slice(2);\n\t\t}\n\t\treturn host === pattern;\n\t});\n}\n\n/**\n * Create HTTP access with host validation.\n *\n * Uses redirect: \"manual\" to re-validate each redirect target against\n * the allowedHosts list, preventing redirects to unauthorized hosts.\n */\nexport function createHttpAccess(pluginId: string, allowedHosts: string[]): HttpAccess {\n\treturn {\n\t\tasync fetch(url: string, init?: RequestInit): Promise<Response> {\n\t\t\t// Deny by default — plugins must declare allowed hosts\n\t\t\tif (allowedHosts.length === 0) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Plugin \"${pluginId}\" has no allowed hosts configured. ` +\n\t\t\t\t\t\t`Add hosts to the plugin's allowedHosts array to enable HTTP requests.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tlet currentUrl = url;\n\t\t\tlet currentInit = init;\n\n\t\t\tfor (let i = 0; i <= MAX_PLUGIN_REDIRECTS; i++) {\n\t\t\t\tconst hostname = new URL(currentUrl).hostname;\n\t\t\t\tif (!isHostAllowed(hostname, allowedHosts)) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Plugin \"${pluginId}\" is not allowed to fetch from host \"${hostname}\". ` +\n\t\t\t\t\t\t\t`Allowed hosts: ${allowedHosts.join(\", \")}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst response = await globalThis.fetch(currentUrl, {\n\t\t\t\t\t...currentInit,\n\t\t\t\t\tredirect: \"manual\",\n\t\t\t\t});\n\n\t\t\t\t// Not a redirect -- return directly\n\t\t\t\tif (response.status < 300 || response.status >= 400) {\n\t\t\t\t\treturn response;\n\t\t\t\t}\n\n\t\t\t\t// Extract redirect target\n\t\t\t\tconst location = response.headers.get(\"Location\");\n\t\t\t\tif (!location) {\n\t\t\t\t\treturn response;\n\t\t\t\t}\n\n\t\t\t\t// Resolve relative redirects; strip credentials on cross-origin hops\n\t\t\t\tconst previousOrigin = new URL(currentUrl).origin;\n\t\t\t\tcurrentUrl = new URL(location, currentUrl).href;\n\t\t\t\tconst nextOrigin = new URL(currentUrl).origin;\n\n\t\t\t\tif (previousOrigin !== nextOrigin && currentInit) {\n\t\t\t\t\tcurrentInit = stripCredentialHeaders(currentInit);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthrow new Error(`Plugin \"${pluginId}\": too many redirects (max ${MAX_PLUGIN_REDIRECTS})`);\n\t\t},\n\t};\n}\n\n/**\n * Create unrestricted HTTP access (for plugins with network:fetch:any capability).\n * No host validation, but applies SSRF protection on redirect targets to\n * prevent plugins from being tricked into reaching internal services.\n */\nexport function createUnrestrictedHttpAccess(pluginId: string): HttpAccess {\n\treturn {\n\t\tasync fetch(url: string, init?: RequestInit): Promise<Response> {\n\t\t\tlet currentUrl = url;\n\t\t\tlet currentInit = init;\n\n\t\t\tfor (let i = 0; i <= MAX_PLUGIN_REDIRECTS; i++) {\n\t\t\t\t// Validate each URL against SSRF rules (private IPs, metadata endpoints)\n\t\t\t\ttry {\n\t\t\t\t\tvalidateExternalUrl(currentUrl);\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconst msg = e instanceof SsrfError ? e.message : \"SSRF validation failed\";\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Plugin \"${pluginId}\": blocked fetch to \"${new URL(currentUrl).hostname}\": ${msg}`,\n\t\t\t\t\t\t{ cause: e },\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst response = await globalThis.fetch(currentUrl, {\n\t\t\t\t\t...currentInit,\n\t\t\t\t\tredirect: \"manual\",\n\t\t\t\t});\n\n\t\t\t\t// Not a redirect -- return directly\n\t\t\t\tif (response.status < 300 || response.status >= 400) {\n\t\t\t\t\treturn response;\n\t\t\t\t}\n\n\t\t\t\t// Extract redirect target\n\t\t\t\tconst location = response.headers.get(\"Location\");\n\t\t\t\tif (!location) {\n\t\t\t\t\treturn response;\n\t\t\t\t}\n\n\t\t\t\t// Resolve relative redirects; strip credentials on cross-origin hops\n\t\t\t\tconst previousOrigin = new URL(currentUrl).origin;\n\t\t\t\tcurrentUrl = new URL(location, currentUrl).href;\n\t\t\t\tconst nextOrigin = new URL(currentUrl).origin;\n\n\t\t\t\tif (previousOrigin !== nextOrigin && currentInit) {\n\t\t\t\t\tcurrentInit = stripCredentialHeaders(currentInit);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthrow new Error(`Plugin \"${pluginId}\": too many redirects (max ${MAX_PLUGIN_REDIRECTS})`);\n\t\t},\n\t};\n}\n\n/**\n * Create blocked HTTP access (for plugins without network:fetch capability)\n */\nexport function createBlockedHttpAccess(pluginId: string): HttpAccess {\n\treturn {\n\t\tasync fetch(): Promise<never> {\n\t\t\tthrow new Error(\n\t\t\t\t`Plugin \"${pluginId}\" does not have the \"network:fetch\" capability. ` +\n\t\t\t\t\t`Add \"network:fetch\" to the plugin's capabilities to enable HTTP requests.`,\n\t\t\t);\n\t\t},\n\t};\n}\n\n// =============================================================================\n// Log Access\n// =============================================================================\n\n/**\n * Create logger for a plugin\n */\nexport function createLogAccess(pluginId: string): LogAccess {\n\tconst prefix = `[plugin:${pluginId}]`;\n\n\treturn {\n\t\tdebug(message: string, data?: unknown): void {\n\t\t\tif (data !== undefined) {\n\t\t\t\tconsole.debug(prefix, message, data);\n\t\t\t} else {\n\t\t\t\tconsole.debug(prefix, message);\n\t\t\t}\n\t\t},\n\n\t\tinfo(message: string, data?: unknown): void {\n\t\t\tif (data !== undefined) {\n\t\t\t\tconsole.info(prefix, message, data);\n\t\t\t} else {\n\t\t\t\tconsole.info(prefix, message);\n\t\t\t}\n\t\t},\n\n\t\twarn(message: string, data?: unknown): void {\n\t\t\tif (data !== undefined) {\n\t\t\t\tconsole.warn(prefix, message, data);\n\t\t\t} else {\n\t\t\t\tconsole.warn(prefix, message);\n\t\t\t}\n\t\t},\n\n\t\terror(message: string, data?: unknown): void {\n\t\t\tif (data !== undefined) {\n\t\t\t\tconsole.error(prefix, message, data);\n\t\t\t} else {\n\t\t\t\tconsole.error(prefix, message);\n\t\t\t}\n\t\t},\n\t};\n}\n\n// =============================================================================\n// Site Info\n// =============================================================================\n\nconst TRAILING_SLASH_RE = /\\/$/;\n\n/**\n * Options for creating site info\n */\nexport interface SiteInfoOptions {\n\t/** Site name from options table */\n\tsiteName?: string;\n\t/** Site URL from options table or Astro config */\n\tsiteUrl?: string;\n\t/** Site locale from options table */\n\tlocale?: string;\n}\n\n/**\n * Create site info from config and settings.\n *\n * Resolution order for URL:\n * 1. options table (emdash:site_url)\n * 2. Astro `site` config\n * 3. fallback to empty string\n */\nexport function createSiteInfo(options: SiteInfoOptions): SiteInfo {\n\treturn {\n\t\tname: options.siteName ?? \"\",\n\t\turl: (options.siteUrl ?? \"\").replace(TRAILING_SLASH_RE, \"\"), // strip trailing slash\n\t\tlocale: options.locale ?? \"en\",\n\t};\n}\n\n/**\n * Create a URL helper that generates absolute URLs from relative paths.\n * Validates that path starts with \"/\" and rejects protocol-relative paths (\"//\").\n */\nexport function createUrlHelper(siteUrl: string): (path: string) => string {\n\tconst base = siteUrl.replace(TRAILING_SLASH_RE, \"\"); // strip trailing slash\n\n\treturn (path: string): string => {\n\t\tif (!path.startsWith(\"/\")) {\n\t\t\tthrow new Error(`URL path must start with \"/\", got: \"${path}\"`);\n\t\t}\n\t\tif (path.startsWith(\"//\")) {\n\t\t\tthrow new Error(`URL path must not be protocol-relative, got: \"${path}\"`);\n\t\t}\n\t\treturn `${base}${path}`;\n\t};\n}\n\n// =============================================================================\n// User Access\n// =============================================================================\n\n/**\n * Convert a UserRepository user to the plugin-facing UserInfo shape.\n * Strips sensitive fields (avatarUrl, emailVerified, data).\n */\nfunction toUserInfo(user: {\n\tid: string;\n\temail: string;\n\tname: string | null;\n\trole: number;\n\tcreatedAt: string;\n}): UserInfo {\n\treturn {\n\t\tid: user.id,\n\t\temail: user.email,\n\t\tname: user.name,\n\t\trole: user.role,\n\t\tcreatedAt: user.createdAt,\n\t};\n}\n\n/**\n * Create read-only user access for plugins.\n * Excludes sensitive fields (password hashes, sessions, passkeys, avatar URL, data).\n */\nexport function createUserAccess(db: Kysely<Database>): UserAccess {\n\tconst userRepo = new UserRepository(db);\n\n\treturn {\n\t\tasync get(id: string): Promise<UserInfo | null> {\n\t\t\tconst user = await userRepo.findById(id);\n\t\t\tif (!user) return null;\n\t\t\treturn toUserInfo(user);\n\t\t},\n\n\t\tasync getByEmail(email: string): Promise<UserInfo | null> {\n\t\t\tconst user = await userRepo.findByEmail(email);\n\t\t\tif (!user) return null;\n\t\t\treturn toUserInfo(user);\n\t\t},\n\n\t\tasync list(opts?: {\n\t\t\trole?: number;\n\t\t\tlimit?: number;\n\t\t\tcursor?: string;\n\t\t}): Promise<{ items: UserInfo[]; nextCursor?: string }> {\n\t\t\tconst result = await userRepo.findMany({\n\t\t\t\trole: opts?.role as 10 | 20 | 30 | 40 | 50 | undefined,\n\t\t\t\tcursor: opts?.cursor,\n\t\t\t\tlimit: opts?.limit,\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\titems: result.items.map(toUserInfo),\n\t\t\t\tnextCursor: result.nextCursor,\n\t\t\t};\n\t\t},\n\t};\n}\n\n// =============================================================================\n// Plugin Context Factory\n// =============================================================================\n\nexport interface PluginContextFactoryOptions {\n\tdb: Kysely<Database>;\n\t/**\n\t * Storage backend for direct media uploads.\n\t * If not provided, upload() will throw.\n\t */\n\tstorage?: Storage;\n\t/**\n\t * Function to generate upload URLs for media.\n\t * If not provided, media write operations will throw.\n\t */\n\tgetUploadUrl?: (\n\t\tfilename: string,\n\t\tcontentType: string,\n\t) => Promise<{ uploadUrl: string; mediaId: string }>;\n\t/**\n\t * Site information for ctx.site and ctx.url().\n\t * If not provided, site info will have empty defaults.\n\t */\n\tsiteInfo?: SiteInfoOptions;\n\t/**\n\t * Callback to notify the cron scheduler that the next due time may have changed.\n\t * If not provided, ctx.cron will not be available.\n\t */\n\tcronReschedule?: () => void;\n\t/**\n\t * Email pipeline instance for ctx.email.\n\t * If not provided (or no provider configured), ctx.email will be undefined.\n\t */\n\temailPipeline?: EmailPipeline;\n}\n\n/**\n * Factory for creating plugin contexts\n */\nexport class PluginContextFactory {\n\tprivate optionsRepo: OptionsRepository;\n\tprivate db: Kysely<Database>;\n\tprivate storage?: Storage;\n\tprivate getUploadUrl?: (\n\t\tfilename: string,\n\t\tcontentType: string,\n\t) => Promise<{ uploadUrl: string; mediaId: string }>;\n\tprivate site: SiteInfo;\n\tprivate urlHelper: (path: string) => string;\n\tprivate cronReschedule?: () => void;\n\tprivate emailPipeline?: EmailPipeline;\n\n\tconstructor(options: PluginContextFactoryOptions) {\n\t\tthis.db = options.db;\n\t\tthis.optionsRepo = new OptionsRepository(options.db);\n\t\tthis.storage = options.storage;\n\t\tthis.getUploadUrl = options.getUploadUrl;\n\t\tthis.site = createSiteInfo(options.siteInfo ?? {});\n\t\tthis.urlHelper = createUrlHelper(this.site.url);\n\t\tthis.cronReschedule = options.cronReschedule;\n\t\tthis.emailPipeline = options.emailPipeline;\n\t}\n\n\t/**\n\t * Create the unified plugin context\n\t */\n\tcreateContext(plugin: ResolvedPlugin): PluginContext {\n\t\tconst capabilities = new Set(plugin.capabilities);\n\n\t\t// Always available\n\t\tconst kv = createKVAccess(this.optionsRepo, plugin.id);\n\t\tconst log = createLogAccess(plugin.id);\n\t\tconst storage = createStorageAccess(this.db, plugin.id, plugin.storage);\n\n\t\t// Capability-gated: content\n\t\tlet content: ContentAccess | ContentAccessWithWrite | undefined;\n\t\tif (capabilities.has(\"write:content\")) {\n\t\t\tcontent = createContentAccessWithWrite(this.db);\n\t\t} else if (capabilities.has(\"read:content\")) {\n\t\t\tcontent = createContentAccess(this.db);\n\t\t}\n\n\t\t// Capability-gated: media\n\t\tlet media: MediaAccess | MediaAccessWithWrite | undefined;\n\t\tif (capabilities.has(\"write:media\") && this.getUploadUrl) {\n\t\t\tmedia = createMediaAccessWithWrite(this.db, this.getUploadUrl, this.storage);\n\t\t} else if (capabilities.has(\"read:media\")) {\n\t\t\tmedia = createMediaAccess(this.db);\n\t\t}\n\n\t\t// Capability-gated: http\n\t\tlet http: HttpAccess | undefined;\n\t\tif (capabilities.has(\"network:fetch:any\")) {\n\t\t\thttp = createUnrestrictedHttpAccess(plugin.id);\n\t\t} else if (capabilities.has(\"network:fetch\")) {\n\t\t\thttp = createHttpAccess(plugin.id, plugin.allowedHosts);\n\t\t}\n\n\t\t// Capability-gated: users\n\t\tlet users: UserAccess | undefined;\n\t\tif (capabilities.has(\"read:users\")) {\n\t\t\tusers = createUserAccess(this.db);\n\t\t}\n\n\t\t// Cron access ��� always available (scoped to plugin), but only if\n\t\t// the runtime provided a reschedule callback (i.e. cron is wired up).\n\t\tlet cron: CronAccess | undefined;\n\t\tif (this.cronReschedule) {\n\t\t\tcron = new CronAccessImpl(this.db, plugin.id, this.cronReschedule);\n\t\t}\n\n\t\t// Email access — requires email:send capability AND a configured provider\n\t\tlet email: EmailAccess | undefined;\n\t\tif (capabilities.has(\"email:send\") && this.emailPipeline?.isAvailable()) {\n\t\t\tconst pipeline = this.emailPipeline;\n\t\t\tconst pluginId = plugin.id;\n\t\t\temail = {\n\t\t\t\tsend: (message) => pipeline.send(message, pluginId),\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tplugin: {\n\t\t\t\tid: plugin.id,\n\t\t\t\tversion: plugin.version,\n\t\t\t},\n\t\t\tstorage,\n\t\t\tkv,\n\t\t\tcontent,\n\t\t\tmedia,\n\t\t\thttp,\n\t\t\tlog,\n\t\t\tsite: this.site,\n\t\t\turl: this.urlHelper,\n\t\t\tusers,\n\t\t\tcron,\n\t\t\temail,\n\t\t};\n\t}\n}\n\n/**\n * Create a plugin context for a resolved plugin\n */\nexport function createPluginContext(\n\toptions: PluginContextFactoryOptions,\n\tplugin: ResolvedPlugin,\n): PluginContext {\n\tconst factory = new PluginContextFactory(options);\n\treturn factory.createContext(plugin);\n}\n","/**\n * Plugin Hooks System v2\n *\n * Uses the unified PluginContext for all hooks.\n * Manages lifecycle hooks with:\n * - Deterministic ordering via priority + dependencies\n * - Timeout enforcement\n * - Error isolation\n * - Observability\n *\n */\n\nimport { PluginContextFactory, type PluginContextFactoryOptions } from \"./context.js\";\nimport type {\n\tResolvedPlugin,\n\tResolvedHook,\n\tPluginContext,\n\tContentHookEvent,\n\tContentDeleteEvent,\n\tContentPublishStateChangeEvent,\n\tMediaUploadEvent,\n\tMediaAfterUploadEvent,\n\tLifecycleEvent,\n\tUninstallEvent,\n\tCronEvent,\n\tEmailBeforeSendEvent,\n\tEmailBeforeSendHandler,\n\tEmailDeliverHandler,\n\tEmailAfterSendHandler,\n\tContentBeforeSaveHandler,\n\tContentAfterSaveHandler,\n\tContentBeforeDeleteHandler,\n\tContentAfterDeleteHandler,\n\tContentAfterPublishHandler,\n\tContentAfterUnpublishHandler,\n\tMediaBeforeUploadHandler,\n\tMediaAfterUploadHandler,\n\tLifecycleHandler,\n\tUninstallHandler,\n\tCronHandler,\n\tEmailMessage,\n\tCommentBeforeCreateEvent,\n\tCommentBeforeCreateHandler,\n\tCommentModerateHandler,\n\tCommentAfterCreateEvent,\n\tCommentAfterCreateHandler,\n\tCommentAfterModerateEvent,\n\tCommentAfterModerateHandler,\n\tPageMetadataEvent,\n\tPageMetadataHandler,\n\tPageMetadataContribution,\n\tPageFragmentEvent,\n\tPageFragmentHandler,\n\tPageFragmentContribution,\n} from \"./types.js\";\n\n// Hook name type for v2\ntype HookNameV2 =\n\t| \"plugin:install\"\n\t| \"plugin:activate\"\n\t| \"plugin:deactivate\"\n\t| \"plugin:uninstall\"\n\t| \"content:beforeSave\"\n\t| \"content:afterSave\"\n\t| \"content:beforeDelete\"\n\t| \"content:afterDelete\"\n\t| \"content:afterPublish\"\n\t| \"content:afterUnpublish\"\n\t| \"media:beforeUpload\"\n\t| \"media:afterUpload\"\n\t| \"cron\"\n\t| \"email:beforeSend\"\n\t| \"email:deliver\"\n\t| \"email:afterSend\"\n\t| \"comment:beforeCreate\"\n\t| \"comment:moderate\"\n\t| \"comment:afterCreate\"\n\t| \"comment:afterModerate\"\n\t| \"page:metadata\"\n\t| \"page:fragments\";\n\n/**\n * Map from hook name to handler type — used for type-safe hook retrieval\n */\ninterface HookHandlerMap {\n\t\"plugin:install\": LifecycleHandler;\n\t\"plugin:activate\": LifecycleHandler;\n\t\"plugin:deactivate\": LifecycleHandler;\n\t\"plugin:uninstall\": UninstallHandler;\n\t\"content:beforeSave\": ContentBeforeSaveHandler;\n\t\"content:afterSave\": ContentAfterSaveHandler;\n\t\"content:beforeDelete\": ContentBeforeDeleteHandler;\n\t\"content:afterDelete\": ContentAfterDeleteHandler;\n\t\"content:afterPublish\": ContentAfterPublishHandler;\n\t\"content:afterUnpublish\": ContentAfterUnpublishHandler;\n\t\"media:beforeUpload\": MediaBeforeUploadHandler;\n\t\"media:afterUpload\": MediaAfterUploadHandler;\n\tcron: CronHandler;\n\t\"email:beforeSend\": EmailBeforeSendHandler;\n\t\"email:deliver\": EmailDeliverHandler;\n\t\"email:afterSend\": EmailAfterSendHandler;\n\t\"comment:beforeCreate\": CommentBeforeCreateHandler;\n\t\"comment:moderate\": CommentModerateHandler;\n\t\"comment:afterCreate\": CommentAfterCreateHandler;\n\t\"comment:afterModerate\": CommentAfterModerateHandler;\n\t\"page:metadata\": PageMetadataHandler;\n\t\"page:fragments\": PageFragmentHandler;\n}\n\n/**\n * Hook execution result\n */\nexport interface HookResult<T> {\n\tsuccess: boolean;\n\tvalue?: T;\n\terror?: Error;\n\tpluginId: string;\n\tduration: number;\n}\n\n/**\n * Hook pipeline for executing hooks in order\n */\nexport class HookPipeline {\n\tprivate hooks: Map<HookNameV2, Array<ResolvedHook<unknown>>> = new Map();\n\tprivate pluginMap: Map<string, ResolvedPlugin> = new Map();\n\tprivate contextFactory: PluginContextFactory | null = null;\n\t/** Stored so setContextFactory can merge incrementally. */\n\tprivate contextFactoryOptions: Partial<PluginContextFactoryOptions> = {};\n\n\t/** Hook names where at least one handler declared exclusive: true */\n\tprivate exclusiveHookNames: Set<string> = new Set();\n\n\t/**\n\t * Selected provider plugin ID for each exclusive hook.\n\t * Set by the PluginManager after resolution.\n\t */\n\tprivate exclusiveSelections: Map<string, string> = new Map();\n\n\tconstructor(plugins: ResolvedPlugin[], factoryOptions?: PluginContextFactoryOptions) {\n\t\tif (factoryOptions) {\n\t\t\tthis.contextFactory = new PluginContextFactory(factoryOptions);\n\t\t\tthis.contextFactoryOptions = { ...factoryOptions };\n\t\t}\n\n\t\tfor (const plugin of plugins) {\n\t\t\tthis.pluginMap.set(plugin.id, plugin);\n\t\t}\n\t\tthis.registerPlugins(plugins);\n\t}\n\n\t/**\n\t * Set or update the context factory options.\n\t *\n\t * When called on a pipeline that already has a factory, the new options\n\t * are merged on top of the existing ones so that callers don't need to\n\t * repeat every field (e.g. adding `cronReschedule` without losing\n\t * `storage` / `getUploadUrl`).\n\t */\n\tsetContextFactory(options: Partial<PluginContextFactoryOptions>): void {\n\t\tconst merged = { ...this.contextFactoryOptions, ...options };\n\t\t// The first call must include `db`; subsequent calls merge incrementally.\n\t\tthis.contextFactory = new PluginContextFactory(merged as PluginContextFactoryOptions);\n\t\tthis.contextFactoryOptions = merged;\n\t}\n\n\t/**\n\t * Get context for a plugin\n\t */\n\tprivate getContext(pluginId: string): PluginContext {\n\t\tconst plugin = this.pluginMap.get(pluginId);\n\t\tif (!plugin) {\n\t\t\tthrow new Error(`Plugin \"${pluginId}\" not found`);\n\t\t}\n\t\tif (!this.contextFactory) {\n\t\t\tthrow new Error(\"Context factory not initialized - call setContextFactory first\");\n\t\t}\n\t\treturn this.contextFactory.createContext(plugin);\n\t}\n\n\t/**\n\t * Get typed hooks for a specific hook name.\n\t * The internal map stores ResolvedHook<unknown>, but we know each name\n\t * maps to a specific handler type via HookHandlerMap.\n\t *\n\t * Exclusive hooks that have a selected provider are filtered out — they\n\t * should only run via invokeExclusiveHook(), not in the regular pipeline.\n\t */\n\tprivate getTypedHooks<N extends HookNameV2>(name: N): Array<ResolvedHook<HookHandlerMap[N]>> {\n\t\t// The map stores hooks as ResolvedHook<unknown>. Each hook name corresponds\n\t\t// to a specific handler type. The cast here is the single point where we\n\t\t// bridge the untyped map to the typed API — callers never need to cast.\n\t\tconst all = (this.hooks.get(name) ?? []) as Array<ResolvedHook<HookHandlerMap[N]>>;\n\n\t\t// If this hook has an exclusive selection, filter out all exclusive handlers\n\t\t// so they don't run in the regular pipeline\n\t\tif (this.exclusiveSelections.has(name)) {\n\t\t\treturn all.filter((h) => !h.exclusive);\n\t\t}\n\n\t\treturn all;\n\t}\n\n\t/**\n\t * Register all hooks from plugins.\n\t *\n\t * Registers each hook name individually to preserve type safety. The\n\t * internal map stores ResolvedHook<unknown> since it's keyed by string,\n\t * but getTypedHooks() restores the correct handler type on retrieval.\n\t */\n\tprivate registerPlugins(plugins: ResolvedPlugin[]): void {\n\t\tfor (const plugin of plugins) {\n\t\t\tthis.registerPluginHook(plugin, \"plugin:install\");\n\t\t\tthis.registerPluginHook(plugin, \"plugin:activate\");\n\t\t\tthis.registerPluginHook(plugin, \"plugin:deactivate\");\n\t\t\tthis.registerPluginHook(plugin, \"plugin:uninstall\");\n\t\t\tthis.registerPluginHook(plugin, \"content:beforeSave\");\n\t\t\tthis.registerPluginHook(plugin, \"content:afterSave\");\n\t\t\tthis.registerPluginHook(plugin, \"content:beforeDelete\");\n\t\t\tthis.registerPluginHook(plugin, \"content:afterDelete\");\n\t\t\tthis.registerPluginHook(plugin, \"content:afterPublish\");\n\t\t\tthis.registerPluginHook(plugin, \"content:afterUnpublish\");\n\t\t\tthis.registerPluginHook(plugin, \"media:beforeUpload\");\n\t\t\tthis.registerPluginHook(plugin, \"media:afterUpload\");\n\t\t\tthis.registerPluginHook(plugin, \"cron\");\n\t\t\tthis.registerPluginHook(plugin, \"email:beforeSend\");\n\t\t\tthis.registerPluginHook(plugin, \"email:deliver\");\n\t\t\tthis.registerPluginHook(plugin, \"email:afterSend\");\n\t\t\tthis.registerPluginHook(plugin, \"comment:beforeCreate\");\n\t\t\tthis.registerPluginHook(plugin, \"comment:moderate\");\n\t\t\tthis.registerPluginHook(plugin, \"comment:afterCreate\");\n\t\t\tthis.registerPluginHook(plugin, \"comment:afterModerate\");\n\t\t\tthis.registerPluginHook(plugin, \"page:metadata\");\n\t\t\tthis.registerPluginHook(plugin, \"page:fragments\");\n\t\t}\n\n\t\t// Sort hooks by priority and dependencies\n\t\tfor (const [hookName, hooks] of this.hooks) {\n\t\t\tthis.hooks.set(hookName, this.sortHooks(hooks));\n\t\t}\n\t}\n\n\t/**\n\t * Maps hook names to the capability required to register them.\n\t *\n\t * Hooks not listed here have no capability requirement (e.g. lifecycle\n\t * hooks, cron). Any plugin declaring a listed hook without the required\n\t * capability will have that hook silently skipped at registration time.\n\t */\n\tprivate static readonly HOOK_REQUIRED_CAPABILITY: ReadonlyMap<string, string> = new Map([\n\t\t// Email\n\t\t[\"email:beforeSend\", \"email:intercept\"],\n\t\t[\"email:afterSend\", \"email:intercept\"],\n\t\t[\"email:deliver\", \"email:provide\"],\n\t\t// Content — beforeSave can mutate content, so requires write:content.\n\t\t// afterSave is read-only notification, so read:content suffices.\n\t\t[\"content:beforeSave\", \"write:content\"],\n\t\t[\"content:afterSave\", \"read:content\"],\n\t\t[\"content:beforeDelete\", \"read:content\"],\n\t\t[\"content:afterDelete\", \"read:content\"],\n\t\t[\"content:afterPublish\", \"read:content\"],\n\t\t[\"content:afterUnpublish\", \"read:content\"],\n\t\t// Media\n\t\t[\"media:beforeUpload\", \"write:media\"],\n\t\t[\"media:afterUpload\", \"read:media\"],\n\t\t// Comments — hooks expose author email, IP hash, user agent\n\t\t[\"comment:beforeCreate\", \"read:users\"],\n\t\t[\"comment:moderate\", \"read:users\"],\n\t\t[\"comment:afterCreate\", \"read:users\"],\n\t\t[\"comment:afterModerate\", \"read:users\"],\n\t\t// Page fragments — can inject arbitrary scripts into every public page\n\t\t[\"page:fragments\", \"page:inject\"],\n\t]);\n\n\t/**\n\t * Register a single plugin's hook by name\n\t */\n\tprivate registerPluginHook(plugin: ResolvedPlugin, name: HookNameV2): void {\n\t\tconst hook = plugin.hooks[name];\n\t\tif (!hook) return;\n\n\t\t// Hooks that expose sensitive data or inject into pages require specific\n\t\t// capabilities. Plugins without the required capability have the hook\n\t\t// silently skipped to prevent unauthorized data access or page injection.\n\t\tconst requiredCapability = HookPipeline.HOOK_REQUIRED_CAPABILITY.get(name);\n\t\tif (requiredCapability && !plugin.capabilities.includes(requiredCapability as never)) {\n\t\t\tconsole.warn(\n\t\t\t\t`[hooks] Plugin \"${plugin.id}\" declares ${name} hook without ${requiredCapability} capability — skipping`,\n\t\t\t);\n\t\t\treturn;\n\t\t}\n\n\t\t// Track exclusive hooks\n\t\tif (hook.exclusive) {\n\t\t\tthis.exclusiveHookNames.add(name);\n\t\t}\n\n\t\t// ResolvedHook<SpecificHandler> is assignable to ResolvedHook<unknown>\n\t\t// because the handler property is covariant\n\t\tthis.registerHook(name, hook);\n\t}\n\n\t/**\n\t * Register a single hook\n\t */\n\tprivate registerHook(name: HookNameV2, hook: ResolvedHook<unknown>): void {\n\t\tconst existing = this.hooks.get(name) || [];\n\t\texisting.push(hook);\n\t\tthis.hooks.set(name, existing);\n\t}\n\n\t/**\n\t * Sort hooks by priority and dependencies\n\t */\n\tprivate sortHooks(hooks: Array<ResolvedHook<unknown>>): Array<ResolvedHook<unknown>> {\n\t\tconst sorted: Array<ResolvedHook<unknown>> = [];\n\t\tconst remaining = [...hooks];\n\n\t\t// Simple topological sort with priority as tiebreaker\n\t\twhile (remaining.length > 0) {\n\t\t\t// Find hooks whose dependencies are satisfied\n\t\t\tconst ready = remaining.filter((hook) =>\n\t\t\t\thook.dependencies.every((dep) => sorted.some((s) => s.pluginId === dep)),\n\t\t\t);\n\n\t\t\tif (ready.length === 0) {\n\t\t\t\t// Circular dependency or missing dependency - log warning and fall back to priority\n\t\t\t\tconst pluginIds = remaining.map((h) => h.pluginId).join(\", \");\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`[hooks] Hook dependency cycle or missing dependency detected among plugins: ${pluginIds}. Falling back to priority order.`,\n\t\t\t\t);\n\t\t\t\tremaining.sort((a, b) => a.priority - b.priority);\n\t\t\t\tsorted.push(...remaining);\n\t\t\t\tbreak;\n\t\t\t}\n\n\t\t\t// Sort ready hooks by priority and add the first one\n\t\t\tready.sort((a, b) => a.priority - b.priority);\n\t\t\tconst next = ready[0];\n\t\t\tsorted.push(next);\n\t\t\tremaining.splice(remaining.indexOf(next), 1);\n\t\t}\n\n\t\treturn sorted;\n\t}\n\n\t/**\n\t * Execute a hook with timeout\n\t */\n\tprivate async executeWithTimeout<T>(fn: () => Promise<T>, timeout: number): Promise<T> {\n\t\tlet timer: ReturnType<typeof setTimeout>;\n\t\tconst timeoutPromise = new Promise<T>(\n\t\t\t(_, reject) =>\n\t\t\t\t(timer = setTimeout(() => reject(new Error(`Hook timeout after ${timeout}ms`)), timeout)),\n\t\t);\n\t\ttry {\n\t\t\treturn await Promise.race([fn(), timeoutPromise]);\n\t\t} finally {\n\t\t\tclearTimeout(timer!);\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Lifecycle Hooks\n\t// =========================================================================\n\n\t/**\n\t * Run plugin:install hooks\n\t */\n\tasync runPluginInstall(pluginId: string): Promise<HookResult<void>[]> {\n\t\treturn this.runLifecycleHook(\"plugin:install\", pluginId);\n\t}\n\n\t/**\n\t * Run plugin:activate hooks\n\t */\n\tasync runPluginActivate(pluginId: string): Promise<HookResult<void>[]> {\n\t\treturn this.runLifecycleHook(\"plugin:activate\", pluginId);\n\t}\n\n\t/**\n\t * Run plugin:deactivate hooks\n\t */\n\tasync runPluginDeactivate(pluginId: string): Promise<HookResult<void>[]> {\n\t\treturn this.runLifecycleHook(\"plugin:deactivate\", pluginId);\n\t}\n\n\t/**\n\t * Run plugin:uninstall hooks\n\t */\n\tasync runPluginUninstall(pluginId: string, deleteData: boolean): Promise<HookResult<void>[]> {\n\t\tconst hooks = this.getTypedHooks(\"plugin:uninstall\");\n\t\tconst results: HookResult<void>[] = [];\n\n\t\t// Only run the hook for the specific plugin being uninstalled\n\t\tconst hook = hooks.find((h) => h.pluginId === pluginId);\n\t\tif (!hook) return results;\n\n\t\tconst { handler } = hook;\n\t\tconst event: UninstallEvent = { deleteData };\n\t\tconst ctx = this.getContext(pluginId);\n\t\tconst start = Date.now();\n\n\t\ttry {\n\t\t\tawait this.executeWithTimeout(() => handler(event, ctx), hook.timeout);\n\t\t\tresults.push({\n\t\t\t\tsuccess: true,\n\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\tduration: Date.now() - start,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tresults.push({\n\t\t\t\tsuccess: false,\n\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\tduration: Date.now() - start,\n\t\t\t});\n\t\t}\n\n\t\treturn results;\n\t}\n\n\tprivate async runLifecycleHook(\n\t\thookName: \"plugin:install\" | \"plugin:activate\" | \"plugin:deactivate\",\n\t\tpluginId: string,\n\t): Promise<HookResult<void>[]> {\n\t\tconst hooks = this.getTypedHooks(hookName);\n\t\tconst results: HookResult<void>[] = [];\n\n\t\t// Only run the hook for the specific plugin\n\t\tconst hook = hooks.find((h) => h.pluginId === pluginId);\n\t\tif (!hook) return results;\n\n\t\tconst { handler } = hook;\n\t\tconst event: LifecycleEvent = {};\n\t\tconst ctx = this.getContext(pluginId);\n\t\tconst start = Date.now();\n\n\t\ttry {\n\t\t\tawait this.executeWithTimeout(() => handler(event, ctx), hook.timeout);\n\t\t\tresults.push({\n\t\t\t\tsuccess: true,\n\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\tduration: Date.now() - start,\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tresults.push({\n\t\t\t\tsuccess: false,\n\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\tduration: Date.now() - start,\n\t\t\t});\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t// =========================================================================\n\t// Content Hooks\n\t// =========================================================================\n\n\t/**\n\t * Run content:beforeSave hooks\n\t * Returns modified content from the pipeline\n\t */\n\tasync runContentBeforeSave(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t\tisNew: boolean,\n\t): Promise<{\n\t\tcontent: Record<string, unknown>;\n\t\tresults: HookResult<Record<string, unknown>>[];\n\t}> {\n\t\tconst hooks = this.getTypedHooks(\"content:beforeSave\");\n\t\tconst results: HookResult<Record<string, unknown>>[] = [];\n\t\tlet currentContent = content;\n\n\t\tfor (const hook of hooks) {\n\t\t\tconst { handler } = hook;\n\t\t\tconst event: ContentHookEvent = {\n\t\t\t\tcontent: currentContent,\n\t\t\t\tcollection,\n\t\t\t\tisNew,\n\t\t\t};\n\t\t\tconst ctx = this.getContext(hook.pluginId);\n\t\t\tconst start = Date.now();\n\n\t\t\ttry {\n\t\t\t\tconst result = await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);\n\t\t\t\t// Handler can return modified content or void (keep current)\n\t\t\t\tif (result !== undefined) {\n\t\t\t\t\tcurrentContent = result;\n\t\t\t\t}\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: true,\n\t\t\t\t\tvalue: currentContent,\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\n\t\t\t\tif (hook.errorPolicy === \"abort\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn { content: currentContent, results };\n\t}\n\n\t/**\n\t * Run content:afterSave hooks\n\t */\n\tasync runContentAfterSave(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t\tisNew: boolean,\n\t): Promise<HookResult<void>[]> {\n\t\tconst hooks = this.getTypedHooks(\"content:afterSave\");\n\t\tconst results: HookResult<void>[] = [];\n\n\t\tfor (const hook of hooks) {\n\t\t\tconst { handler } = hook;\n\t\t\tconst event: ContentHookEvent = { content, collection, isNew };\n\t\t\tconst ctx = this.getContext(hook.pluginId);\n\t\t\tconst start = Date.now();\n\n\t\t\ttry {\n\t\t\t\tawait this.executeWithTimeout(() => handler(event, ctx), hook.timeout);\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: true,\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\n\t\t\t\tif (hook.errorPolicy === \"abort\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t/**\n\t * Run content:beforeDelete hooks\n\t * Returns whether deletion is allowed\n\t */\n\tasync runContentBeforeDelete(\n\t\tid: string,\n\t\tcollection: string,\n\t): Promise<{ allowed: boolean; results: HookResult<boolean>[] }> {\n\t\tconst hooks = this.getTypedHooks(\"content:beforeDelete\");\n\t\tconst results: HookResult<boolean>[] = [];\n\t\tlet allowed = true;\n\n\t\tfor (const hook of hooks) {\n\t\t\tconst { handler } = hook;\n\t\t\tconst event: ContentDeleteEvent = { id, collection, permanent: false };\n\t\t\tconst ctx = this.getContext(hook.pluginId);\n\t\t\tconst start = Date.now();\n\n\t\t\ttry {\n\t\t\t\tconst result = await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);\n\t\t\t\t// Handler returns false to block, true or void to allow\n\t\t\t\tif (result === false) {\n\t\t\t\t\tallowed = false;\n\t\t\t\t}\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: true,\n\t\t\t\t\tvalue: result !== false,\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\n\t\t\t\tif (hook.errorPolicy === \"abort\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn { allowed, results };\n\t}\n\n\t/**\n\t * Run content:afterDelete hooks\n\t */\n\tasync runContentAfterDelete(\n\t\tid: string,\n\t\tcollection: string,\n\t\tpermanent: boolean,\n\t): Promise<HookResult<void>[]> {\n\t\tconst hooks = this.getTypedHooks(\"content:afterDelete\");\n\t\tconst results: HookResult<void>[] = [];\n\n\t\tfor (const hook of hooks) {\n\t\t\tconst { handler } = hook;\n\t\t\tconst event: ContentDeleteEvent = { id, collection, permanent };\n\t\t\tconst ctx = this.getContext(hook.pluginId);\n\t\t\tconst start = Date.now();\n\n\t\t\ttry {\n\t\t\t\tawait this.executeWithTimeout(() => handler(event, ctx), hook.timeout);\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: true,\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\n\t\t\t\tif (hook.errorPolicy === \"abort\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t/**\n\t * Run content:afterPublish hooks (fire-and-forget).\n\t */\n\tasync runContentAfterPublish(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t): Promise<HookResult<void>[]> {\n\t\tconst hooks = this.getTypedHooks(\"content:afterPublish\");\n\t\tconst results: HookResult<void>[] = [];\n\n\t\tfor (const hook of hooks) {\n\t\t\tconst { handler } = hook;\n\t\t\tconst event: ContentPublishStateChangeEvent = { content, collection };\n\t\t\tconst ctx = this.getContext(hook.pluginId);\n\t\t\tconst start = Date.now();\n\n\t\t\ttry {\n\t\t\t\tawait this.executeWithTimeout(() => handler(event, ctx), hook.timeout);\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: true,\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\n\t\t\t\tif (hook.errorPolicy === \"abort\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t/**\n\t * Run content:afterUnpublish hooks (fire-and-forget).\n\t */\n\tasync runContentAfterUnpublish(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t): Promise<HookResult<void>[]> {\n\t\tconst hooks = this.getTypedHooks(\"content:afterUnpublish\");\n\t\tconst results: HookResult<void>[] = [];\n\n\t\tfor (const hook of hooks) {\n\t\t\tconst { handler } = hook;\n\t\t\tconst event: ContentPublishStateChangeEvent = { content, collection };\n\t\t\tconst ctx = this.getContext(hook.pluginId);\n\t\t\tconst start = Date.now();\n\n\t\t\ttry {\n\t\t\t\tawait this.executeWithTimeout(() => handler(event, ctx), hook.timeout);\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: true,\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\n\t\t\t\tif (hook.errorPolicy === \"abort\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t// =========================================================================\n\t// Media Hooks\n\t// =========================================================================\n\n\t/**\n\t * Run media:beforeUpload hooks\n\t */\n\tasync runMediaBeforeUpload(file: { name: string; type: string; size: number }): Promise<{\n\t\tfile: { name: string; type: string; size: number };\n\t\tresults: HookResult<{ name: string; type: string; size: number }>[];\n\t}> {\n\t\tconst hooks = this.getTypedHooks(\"media:beforeUpload\");\n\t\tconst results: HookResult<{\n\t\t\tname: string;\n\t\t\ttype: string;\n\t\t\tsize: number;\n\t\t}>[] = [];\n\t\tlet currentFile = file;\n\n\t\tfor (const hook of hooks) {\n\t\t\tconst { handler } = hook;\n\t\t\tconst event: MediaUploadEvent = { file: currentFile };\n\t\t\tconst ctx = this.getContext(hook.pluginId);\n\t\t\tconst start = Date.now();\n\n\t\t\ttry {\n\t\t\t\tconst result = await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);\n\t\t\t\t// Handler can return modified file info or void\n\t\t\t\tif (result !== undefined) {\n\t\t\t\t\tcurrentFile = result;\n\t\t\t\t}\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: true,\n\t\t\t\t\tvalue: currentFile,\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\n\t\t\t\tif (hook.errorPolicy === \"abort\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn { file: currentFile, results };\n\t}\n\n\t/**\n\t * Run media:afterUpload hooks\n\t */\n\tasync runMediaAfterUpload(media: {\n\t\tid: string;\n\t\tfilename: string;\n\t\tmimeType: string;\n\t\tsize: number | null;\n\t\turl: string;\n\t\tcreatedAt: string;\n\t}): Promise<HookResult<void>[]> {\n\t\tconst hooks = this.getTypedHooks(\"media:afterUpload\");\n\t\tconst results: HookResult<void>[] = [];\n\n\t\tfor (const hook of hooks) {\n\t\t\tconst { handler } = hook;\n\t\t\tconst event: MediaAfterUploadEvent = { media };\n\t\t\tconst ctx = this.getContext(hook.pluginId);\n\t\t\tconst start = Date.now();\n\n\t\t\ttry {\n\t\t\t\tawait this.executeWithTimeout(() => handler(event, ctx), hook.timeout);\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: true,\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\n\t\t\t\tif (hook.errorPolicy === \"abort\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t// =========================================================================\n\t// Cron Hook (per-plugin dispatch)\n\t// =========================================================================\n\n\t/**\n\t * Invoke the cron hook for a specific plugin.\n\t *\n\t * Unlike other hooks which broadcast to all plugins, the cron hook is\n\t * dispatched only to the target plugin — the one that owns the task.\n\t */\n\tasync invokeCronHook(pluginId: string, event: CronEvent): Promise<HookResult<void>> {\n\t\tconst hooks = this.getTypedHooks(\"cron\");\n\t\tconst hook = hooks.find((h) => h.pluginId === pluginId);\n\n\t\tif (!hook) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: new Error(`Plugin \"${pluginId}\" has no cron hook registered`),\n\t\t\t\tpluginId,\n\t\t\t\tduration: 0,\n\t\t\t};\n\t\t}\n\n\t\tconst { handler } = hook;\n\t\tconst ctx = this.getContext(pluginId);\n\t\tconst start = Date.now();\n\n\t\ttry {\n\t\t\tawait this.executeWithTimeout(() => handler(event, ctx), hook.timeout);\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tpluginId,\n\t\t\t\tduration: Date.now() - start,\n\t\t\t};\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\tpluginId,\n\t\t\t\tduration: Date.now() - start,\n\t\t\t};\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Email Hooks\n\t// =========================================================================\n\n\t/**\n\t * Run email:beforeSend hooks (middleware pipeline).\n\t *\n\t * Each handler receives the message and returns a modified message or\n\t * `false` to cancel delivery. The pipeline chains message transformations —\n\t * each handler receives the output of the previous one.\n\t */\n\tasync runEmailBeforeSend(\n\t\tmessage: EmailMessage,\n\t\tsource: string,\n\t): Promise<{ message: EmailMessage | false; results: HookResult<EmailMessage | false>[] }> {\n\t\tconst hooks = this.getTypedHooks(\"email:beforeSend\");\n\t\tconst results: HookResult<EmailMessage | false>[] = [];\n\t\tlet currentMessage: EmailMessage = message;\n\n\t\tfor (const hook of hooks) {\n\t\t\tconst { handler } = hook;\n\t\t\t// Shallow-clone message to prevent handlers from mutating\n\t\t\t// the shared reference and leaking changes to subsequent stages\n\t\t\tconst event: EmailBeforeSendEvent = { message: { ...currentMessage }, source };\n\t\t\tconst ctx = this.getContext(hook.pluginId);\n\t\t\tconst start = Date.now();\n\n\t\t\ttry {\n\t\t\t\tconst result = await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);\n\n\t\t\t\tif (result === false) {\n\t\t\t\t\t// Cancelled\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\tsuccess: true,\n\t\t\t\t\t\tvalue: false,\n\t\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t\t});\n\t\t\t\t\treturn { message: false, results };\n\t\t\t\t}\n\n\t\t\t\t// Handler returned a modified message\n\t\t\t\tif (result && typeof result === \"object\") {\n\t\t\t\t\tcurrentMessage = result;\n\t\t\t\t}\n\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: true,\n\t\t\t\t\tvalue: currentMessage,\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\n\t\t\t\tif (hook.errorPolicy === \"abort\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn { message: currentMessage, results };\n\t}\n\n\t/**\n\t * Run email:afterSend hooks (fire-and-forget).\n\t *\n\t * Errors are logged but don't propagate — they don't affect the caller.\n\t */\n\tasync runEmailAfterSend(message: EmailMessage, source: string): Promise<HookResult<void>[]> {\n\t\tconst hooks = this.getTypedHooks(\"email:afterSend\");\n\t\tconst results: HookResult<void>[] = [];\n\n\t\tfor (const hook of hooks) {\n\t\t\tconst { handler } = hook;\n\t\t\tconst event = { message, source };\n\t\t\tconst ctx = this.getContext(hook.pluginId);\n\t\t\tconst start = Date.now();\n\n\t\t\ttry {\n\t\t\t\tawait this.executeWithTimeout(() => handler(event, ctx), hook.timeout);\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: true,\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\t// Fire-and-forget: log but don't propagate\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[email:afterSend] Plugin \"${hook.pluginId}\" error:`,\n\t\t\t\t\terror instanceof Error ? error.message : error,\n\t\t\t\t);\n\t\t\t\tresults.push({\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\t\tpluginId: hook.pluginId,\n\t\t\t\t\tduration: Date.now() - start,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t// =========================================================================\n\t// Comment Hooks\n\t// =========================================================================\n\n\t/**\n\t * Run comment:beforeCreate hooks (middleware pipeline).\n\t *\n\t * Each handler receives the event and returns a modified event or\n\t * `false` to reject the comment. The pipeline chains transformations —\n\t * each handler receives the output of the previous one.\n\t */\n\tasync runCommentBeforeCreate(\n\t\tevent: CommentBeforeCreateEvent,\n\t): Promise<CommentBeforeCreateEvent | false> {\n\t\tconst hooks = this.getTypedHooks(\"comment:beforeCreate\");\n\t\tlet currentEvent = event;\n\n\t\tfor (const hook of hooks) {\n\t\t\tconst { handler } = hook;\n\t\t\tconst ctx = this.getContext(hook.pluginId);\n\t\t\tconst start = Date.now();\n\n\t\t\ttry {\n\t\t\t\tconst result = await this.executeWithTimeout(\n\t\t\t\t\t() => handler({ ...currentEvent }, ctx),\n\t\t\t\t\thook.timeout,\n\t\t\t\t);\n\n\t\t\t\tif (result === false) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\n\t\t\t\tif (result && typeof result === \"object\") {\n\t\t\t\t\tcurrentEvent = result;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[comment:beforeCreate] Plugin \"${hook.pluginId}\" error (${Date.now() - start}ms):`,\n\t\t\t\t\terror instanceof Error ? error.message : error,\n\t\t\t\t);\n\n\t\t\t\tif (hook.errorPolicy === \"abort\") {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn currentEvent;\n\t}\n\n\t/**\n\t * Run comment:afterCreate hooks (fire-and-forget).\n\t *\n\t * Errors are logged but don't propagate — they don't affect the caller.\n\t */\n\tasync runCommentAfterCreate(event: CommentAfterCreateEvent): Promise<void> {\n\t\tconst hooks = this.getTypedHooks(\"comment:afterCreate\");\n\n\t\tfor (const hook of hooks) {\n\t\t\tconst { handler } = hook;\n\t\t\tconst ctx = this.getContext(hook.pluginId);\n\n\t\t\ttry {\n\t\t\t\tawait this.executeWithTimeout(() => handler(event, ctx), hook.timeout);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[comment:afterCreate] Plugin \"${hook.pluginId}\" error:`,\n\t\t\t\t\terror instanceof Error ? error.message : error,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Run comment:afterModerate hooks (fire-and-forget).\n\t *\n\t * Errors are logged but don't propagate — they don't affect the caller.\n\t */\n\tasync runCommentAfterModerate(event: CommentAfterModerateEvent): Promise<void> {\n\t\tconst hooks = this.getTypedHooks(\"comment:afterModerate\");\n\n\t\tfor (const hook of hooks) {\n\t\t\tconst { handler } = hook;\n\t\t\tconst ctx = this.getContext(hook.pluginId);\n\n\t\t\ttry {\n\t\t\t\tawait this.executeWithTimeout(() => handler(event, ctx), hook.timeout);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[comment:afterModerate] Plugin \"${hook.pluginId}\" error:`,\n\t\t\t\t\terror instanceof Error ? error.message : error,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Public Page Hooks\n\t// =========================================================================\n\n\t/**\n\t * Run page:metadata hooks. Each handler returns contributions that are\n\t * merged by the metadata collector. Errors are logged but don't propagate.\n\t */\n\tasync runPageMetadata(\n\t\tevent: PageMetadataEvent,\n\t): Promise<Array<{ pluginId: string; contributions: PageMetadataContribution[] }>> {\n\t\tconst hooks = this.getTypedHooks(\"page:metadata\");\n\t\tconst results: Array<{ pluginId: string; contributions: PageMetadataContribution[] }> = [];\n\n\t\tfor (const hook of hooks) {\n\t\t\tconst { handler } = hook;\n\t\t\tconst ctx = this.getContext(hook.pluginId);\n\n\t\t\ttry {\n\t\t\t\tconst result = await this.executeWithTimeout(\n\t\t\t\t\t() => Promise.resolve(handler(event, ctx)),\n\t\t\t\t\thook.timeout,\n\t\t\t\t);\n\n\t\t\t\tif (result != null) {\n\t\t\t\t\tconst contributions = Array.isArray(result) ? result : [result];\n\t\t\t\t\tresults.push({ pluginId: hook.pluginId, contributions });\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[page:metadata] Plugin \"${hook.pluginId}\" error:`,\n\t\t\t\t\terror instanceof Error ? error.message : error,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t/**\n\t * Run page:fragments hooks. Only trusted plugins should be registered\n\t * for this hook. Errors are logged but don't propagate.\n\t */\n\tasync runPageFragments(\n\t\tevent: PageFragmentEvent,\n\t): Promise<Array<{ pluginId: string; contributions: PageFragmentContribution[] }>> {\n\t\tconst hooks = this.getTypedHooks(\"page:fragments\");\n\t\tconst results: Array<{ pluginId: string; contributions: PageFragmentContribution[] }> = [];\n\n\t\tfor (const hook of hooks) {\n\t\t\tconst { handler } = hook;\n\t\t\tconst ctx = this.getContext(hook.pluginId);\n\n\t\t\ttry {\n\t\t\t\tconst result = await this.executeWithTimeout(\n\t\t\t\t\t() => Promise.resolve(handler(event, ctx)),\n\t\t\t\t\thook.timeout,\n\t\t\t\t);\n\n\t\t\t\tif (result != null) {\n\t\t\t\t\tconst contributions = Array.isArray(result) ? result : [result];\n\t\t\t\t\tresults.push({ pluginId: hook.pluginId, contributions });\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[page:fragments] Plugin \"${hook.pluginId}\" error:`,\n\t\t\t\t\terror instanceof Error ? error.message : error,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t// =========================================================================\n\t// Utilities\n\t// =========================================================================\n\n\t/**\n\t * Check if any hooks are registered for a given name\n\t */\n\thasHooks(name: HookNameV2): boolean {\n\t\tconst hooks = this.hooks.get(name);\n\t\treturn hooks !== undefined && hooks.length > 0;\n\t}\n\n\t/**\n\t * Get hook count for debugging\n\t */\n\tgetHookCount(name: HookNameV2): number {\n\t\treturn this.hooks.get(name)?.length || 0;\n\t}\n\n\t/**\n\t * Get all registered hook names\n\t */\n\tgetRegisteredHooks(): HookNameV2[] {\n\t\treturn [...this.hooks.keys()];\n\t}\n\n\t// =========================================================================\n\t// Exclusive Hook Support\n\t// =========================================================================\n\n\t/**\n\t * Returns hook names where at least one handler declared exclusive: true\n\t */\n\tgetRegisteredExclusiveHooks(): string[] {\n\t\treturn [...this.exclusiveHookNames];\n\t}\n\n\t/**\n\t * Check if a hook is exclusive\n\t */\n\tisExclusiveHook(name: string): boolean {\n\t\treturn this.exclusiveHookNames.has(name);\n\t}\n\n\t/**\n\t * Set the selected provider for an exclusive hook.\n\t * Called by PluginManager after resolution.\n\t */\n\tsetExclusiveSelection(hookName: string, pluginId: string): void {\n\t\tthis.exclusiveSelections.set(hookName, pluginId);\n\t}\n\n\t/**\n\t * Clear the selected provider for an exclusive hook.\n\t */\n\tclearExclusiveSelection(hookName: string): void {\n\t\tthis.exclusiveSelections.delete(hookName);\n\t}\n\n\t/**\n\t * Get the selected provider for an exclusive hook (if any).\n\t */\n\tgetExclusiveSelection(hookName: string): string | undefined {\n\t\treturn this.exclusiveSelections.get(hookName);\n\t}\n\n\t/**\n\t * Get all plugins that registered a handler for a given exclusive hook.\n\t */\n\tgetExclusiveHookProviders(hookName: string): Array<{ pluginId: string }> {\n\t\tconst hooks = this.hooks.get(hookName as HookNameV2) ?? [];\n\t\treturn hooks.filter((h) => h.exclusive).map((h) => ({ pluginId: h.pluginId }));\n\t}\n\n\t/**\n\t * Invoke an exclusive hook — dispatch only to the selected provider.\n\t * Returns null if no provider is selected or if the selected hook\n\t * is not found in the pipeline.\n\t *\n\t * This is a generic dispatch used by the email pipeline and other\n\t * exclusive hook consumers. The handler type is unknown — callers\n\t * must know the expected signature.\n\t *\n\t * Errors are isolated: a failing handler returns an error result\n\t * instead of propagating the exception to the caller.\n\t */\n\tasync invokeExclusiveHook(\n\t\thookName: string,\n\t\tevent: unknown,\n\t): Promise<{ result: unknown; pluginId: string; error?: Error; duration: number } | null> {\n\t\tconst selectedPluginId = this.exclusiveSelections.get(hookName);\n\t\tif (!selectedPluginId) return null;\n\n\t\tconst hooks = this.hooks.get(hookName as HookNameV2) ?? [];\n\t\tconst hook = hooks.find((h) => h.pluginId === selectedPluginId && h.exclusive);\n\t\tif (!hook) return null;\n\n\t\tconst start = Date.now();\n\t\ttry {\n\t\t\tconst ctx = this.getContext(selectedPluginId);\n\t\t\tconst handler = hook.handler as (event: unknown, ctx: PluginContext) => Promise<unknown>;\n\t\t\tconst result = await this.executeWithTimeout(() => handler(event, ctx), hook.timeout);\n\t\t\treturn { result, pluginId: selectedPluginId, duration: Date.now() - start };\n\t\t} catch (error) {\n\t\t\treturn {\n\t\t\t\tresult: undefined,\n\t\t\t\tpluginId: selectedPluginId,\n\t\t\t\terror: error instanceof Error ? error : new Error(String(error)),\n\t\t\t\tduration: Date.now() - start,\n\t\t\t};\n\t\t}\n\t}\n}\n\n/**\n * Create a hook pipeline from plugins\n */\nexport function createHookPipeline(\n\tplugins: ResolvedPlugin[],\n\tfactoryOptions?: PluginContextFactoryOptions,\n): HookPipeline {\n\treturn new HookPipeline(plugins, factoryOptions);\n}\n\n// ── Shared exclusive hook resolution ─────────────────────────────────────────\n\n/**\n * Options for exclusive hook resolution.\n */\nexport interface ExclusiveHookResolutionOptions {\n\tpipeline: HookPipeline;\n\t/**\n\t * Check whether a plugin ID is currently active.\n\t * Used to filter providers — only active providers participate in selection.\n\t */\n\tisActive: (pluginId: string) => boolean;\n\t/** Read an option value from persistent storage. */\n\tgetOption: (key: string) => Promise<string | null>;\n\t/** Write an option value to persistent storage. */\n\tsetOption: (key: string, value: string) => Promise<void>;\n\t/** Delete an option from persistent storage. */\n\tdeleteOption: (key: string) => Promise<void>;\n\t/**\n\t * Map of pluginId → hook names the plugin prefers to handle.\n\t * Used as a tiebreaker when no DB selection exists and multiple providers are active.\n\t */\n\tpreferredHints?: Map<string, string[]>;\n}\n\n/** Options table key prefix for exclusive hook selections */\nconst EXCLUSIVE_HOOK_KEY_PREFIX = \"emdash:exclusive_hook:\";\n\n/**\n * Resolve exclusive hook selections.\n *\n * Shared algorithm used by both PluginManager and EmDashRuntime:\n * 1. If a DB selection exists and that plugin is active → keep it.\n * 2. If DB selection is stale (plugin inactive/gone) → clear it.\n * 3. If no selection and only one active provider → auto-select it.\n * 4. If preferred hints match an active provider → first match wins.\n * 5. If multiple providers and no hint → leave unselected (admin must choose).\n */\nexport async function resolveExclusiveHooks(opts: ExclusiveHookResolutionOptions): Promise<void> {\n\tconst { pipeline, isActive, getOption, setOption, deleteOption, preferredHints } = opts;\n\tconst exclusiveHookNames = pipeline.getRegisteredExclusiveHooks();\n\n\tfor (const hookName of exclusiveHookNames) {\n\t\tconst providers = pipeline.getExclusiveHookProviders(hookName);\n\t\tconst activeProviderIds = new Set(\n\t\t\tproviders.map((p) => p.pluginId).filter((id) => isActive(id)),\n\t\t);\n\n\t\tconst key = `${EXCLUSIVE_HOOK_KEY_PREFIX}${hookName}`;\n\t\tlet currentSelection: string | null = null;\n\t\ttry {\n\t\t\tcurrentSelection = await getOption(key);\n\t\t} catch {\n\t\t\t// Options table may not be ready\n\t\t\tcontinue;\n\t\t}\n\n\t\t// If selection exists and the plugin is still active → keep it\n\t\tif (currentSelection && activeProviderIds.has(currentSelection)) {\n\t\t\tpipeline.setExclusiveSelection(hookName, currentSelection);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Selection is stale or missing — clear it\n\t\tif (currentSelection) {\n\t\t\ttry {\n\t\t\t\tawait deleteOption(key);\n\t\t\t} catch {\n\t\t\t\t// Non-fatal\n\t\t\t}\n\t\t}\n\n\t\t// Auto-select if only one active provider\n\t\tif (activeProviderIds.size === 1) {\n\t\t\tconst [onlyProvider] = activeProviderIds;\n\t\t\ttry {\n\t\t\t\tawait setOption(key, onlyProvider);\n\t\t\t} catch {\n\t\t\t\t// Non-fatal\n\t\t\t}\n\t\t\tpipeline.setExclusiveSelection(hookName, onlyProvider);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Check preferred hints\n\t\tif (preferredHints) {\n\t\t\tlet found = false;\n\t\t\tfor (const [pluginId, hooks] of preferredHints) {\n\t\t\t\tif (hooks.includes(hookName) && activeProviderIds.has(pluginId)) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait setOption(key, pluginId);\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t// Non-fatal\n\t\t\t\t\t}\n\t\t\t\t\tpipeline.setExclusiveSelection(hookName, pluginId);\n\t\t\t\t\tfound = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (found) continue;\n\t\t}\n\n\t\t// Multiple providers, no hint — leave unselected\n\t\tpipeline.clearExclusiveSelection(hookName);\n\t}\n}\n","/**\n * Email Pipeline\n *\n * Orchestrates the three-stage email pipeline:\n * 1. email:beforeSend hooks (middleware — transform, validate, cancel)\n * 2. email:deliver hook (exclusive — exactly one provider delivers)\n * 3. email:afterSend hooks (logging, analytics, fire-and-forget)\n *\n * Security features:\n * - Recursion guard prevents re-entrant sends (e.g. plugin calling ctx.email.send from a hook)\n * - System emails (source=\"system\") bypass email:beforeSend and email:afterSend hooks entirely\n * to protect auth tokens from exfiltration by plugin hooks\n *\n */\n\nimport { AsyncLocalStorage } from \"node:async_hooks\";\n\nimport type { HookPipeline } from \"./hooks.js\";\nimport type { EmailDeliverEvent, EmailMessage } from \"./types.js\";\n\n/** Hook name for the exclusive email delivery hook */\nconst EMAIL_DELIVER_HOOK = \"email:deliver\";\n\n/** Source value used for auth emails (magic links, invites, password resets) */\nconst SYSTEM_SOURCE = \"system\";\n\n/**\n * Error thrown when ctx.email.send() is called but no provider is configured.\n */\nexport class EmailNotConfiguredError extends Error {\n\tconstructor() {\n\t\tsuper(\n\t\t\t\"No email provider is configured. Install and activate an email provider plugin, \" +\n\t\t\t\t\"then select it in Settings > Email.\",\n\t\t);\n\t\tthis.name = \"EmailNotConfiguredError\";\n\t}\n}\n\n/**\n * Error thrown when a recursive email send is detected.\n */\nexport class EmailRecursionError extends Error {\n\tconstructor() {\n\t\tsuper(\n\t\t\t\"Recursive email send detected. A plugin hook attempted to send an email \" +\n\t\t\t\t\"from within the email pipeline, which would cause infinite recursion.\",\n\t\t);\n\t\tthis.name = \"EmailRecursionError\";\n\t}\n}\n\n/**\n * Recursion guard using AsyncLocalStorage.\n *\n * EmailPipeline is a singleton (worker-lifetime cached via EmDashRuntime).\n * Instance state like `sendDepth` would false-positive under concurrent\n * requests because two unrelated sends would increment the same counter.\n * ALS scopes the guard to the current async execution context, so concurrent\n * requests each get their own independent recursion tracking.\n */\nconst emailSendALS = new AsyncLocalStorage<{ depth: number }>();\n\n/**\n * EmailPipeline orchestrates email delivery through the plugin hook system.\n *\n * The pipeline runs in three stages:\n * 1. email:beforeSend — middleware hooks that can transform or cancel messages\n * 2. email:deliver — exclusive hook dispatching to the selected provider\n * 3. email:afterSend — fire-and-forget hooks for logging/analytics\n */\nexport class EmailPipeline {\n\tprivate pipeline: HookPipeline;\n\n\tconstructor(pipeline: HookPipeline) {\n\t\tthis.pipeline = pipeline;\n\t}\n\n\t/**\n\t * Replace the underlying hook pipeline.\n\t *\n\t * Called by the runtime when rebuilding the hook pipeline after a\n\t * plugin is enabled or disabled, so the email pipeline dispatches\n\t * to the current set of active hooks.\n\t */\n\tsetPipeline(pipeline: HookPipeline): void {\n\t\tthis.pipeline = pipeline;\n\t}\n\n\t/**\n\t * Send an email through the full pipeline.\n\t *\n\t * @param message - The email to send\n\t * @param source - Where the email originated (\"system\" for auth, plugin ID for plugins)\n\t * @throws EmailNotConfiguredError if no provider is selected\n\t * @throws EmailRecursionError if called re-entrantly from within a hook\n\t * @throws Error if the provider handler throws\n\t */\n\tasync send(message: EmailMessage, source: string): Promise<void> {\n\t\t// Recursion guard: a plugin with email:send + email:intercept calling\n\t\t// ctx.email.send() from an email hook would loop forever.\n\t\t// Uses AsyncLocalStorage so concurrent requests don't interfere —\n\t\t// each async context tracks its own depth independently.\n\t\tconst store = emailSendALS.getStore();\n\t\tif (store && store.depth > 0) {\n\t\t\tthrow new EmailRecursionError();\n\t\t}\n\n\t\tconst run = () => this.sendInner(message, source);\n\t\tif (store) {\n\t\t\t// Already inside an ALS context (e.g. nested call) — increment depth\n\t\t\tstore.depth++;\n\t\t\ttry {\n\t\t\t\tawait run();\n\t\t\t} finally {\n\t\t\t\tstore.depth--;\n\t\t\t}\n\t\t} else {\n\t\t\t// First call — create new ALS context\n\t\t\tawait emailSendALS.run({ depth: 1 }, run);\n\t\t}\n\t}\n\n\t/**\n\t * Inner send implementation, separated from the recursion guard.\n\t */\n\tprivate async sendInner(message: EmailMessage, source: string): Promise<void> {\n\t\t// Validate message fields at the pipeline boundary. TypeScript enforces\n\t\t// this at compile time, but sandboxed plugins cross an RPC boundary\n\t\t// where runtime types aren't guaranteed.\n\t\tif (!message || typeof message !== \"object\") {\n\t\t\tthrow new Error(\"Invalid email message: message must be an object\");\n\t\t}\n\t\tif (!message.to || typeof message.to !== \"string\") {\n\t\t\tthrow new Error(\"Invalid email message: 'to' is required and must be a string\");\n\t\t}\n\t\tif (!message.subject || typeof message.subject !== \"string\") {\n\t\t\tthrow new Error(\"Invalid email message: 'subject' is required and must be a string\");\n\t\t}\n\t\tif (!message.text || typeof message.text !== \"string\") {\n\t\t\tthrow new Error(\"Invalid email message: 'text' is required and must be a string\");\n\t\t}\n\n\t\tconst isSystemEmail = source === SYSTEM_SOURCE;\n\n\t\t// System emails (auth tokens, magic links, invites) skip the\n\t\t// email:beforeSend pipeline entirely. These contain sensitive tokens\n\t\t// that must never be exposed to plugin hooks — a malicious interceptor\n\t\t// could rewrite the body/URL to steal auth tokens even if the `to`\n\t\t// field is protected.\n\t\tlet finalMessage: EmailMessage;\n\t\tif (isSystemEmail) {\n\t\t\tfinalMessage = message;\n\t\t} else {\n\t\t\t// Stage 1: email:beforeSend middleware (can transform or cancel)\n\t\t\tconst beforeResult = await this.pipeline.runEmailBeforeSend(message, source);\n\n\t\t\tif (beforeResult.message === false) {\n\t\t\t\t// Cancelled by middleware — find which plugin cancelled for audit log\n\t\t\t\tconst cancellingResult = beforeResult.results.find((r) => r.value === false);\n\t\t\t\tconst cancelledBy = cancellingResult?.pluginId ?? \"unknown\";\n\n\t\t\t\tconsole.info(`[email] Email to \"${message.to}\" cancelled by plugin \"${cancelledBy}\"`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tfinalMessage = beforeResult.message;\n\t\t}\n\n\t\t// Stage 2: email:deliver (exclusive hook)\n\t\tconst deliverEvent: EmailDeliverEvent = { message: finalMessage, source };\n\t\tconst deliverResult = await this.pipeline.invokeExclusiveHook(EMAIL_DELIVER_HOOK, deliverEvent);\n\n\t\tif (!deliverResult) {\n\t\t\tthrow new EmailNotConfiguredError();\n\t\t}\n\n\t\tif (deliverResult.error) {\n\t\t\tthrow deliverResult.error;\n\t\t}\n\n\t\t// Stage 3: email:afterSend (fire-and-forget)\n\t\t// System emails skip afterSend for the same reason they skip beforeSend:\n\t\t// the message contains plaintext auth tokens that must not be exposed to\n\t\t// plugin hooks. A logging/analytics hook could exfiltrate magic link URLs.\n\t\t// Errors are logged internally by the pipeline, not propagated.\n\t\tif (!isSystemEmail) {\n\t\t\tthis.pipeline\n\t\t\t\t.runEmailAfterSend(finalMessage, source)\n\t\t\t\t.catch((err) =>\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\"[email] afterSend pipeline error:\",\n\t\t\t\t\t\terr instanceof Error ? err.message : err,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Check if an email provider is configured and available.\n\t *\n\t * Returns true if an email:deliver provider is selected in the exclusive\n\t * hook system. Plugins and auth code use this to decide whether to show\n\t * \"send invite\" vs \"copy invite link\" UI.\n\t */\n\tisAvailable(): boolean {\n\t\treturn this.pipeline.getExclusiveSelection(EMAIL_DELIVER_HOOK) !== undefined;\n\t}\n}\n","/**\n * Dev Console Email Provider\n *\n * Built-in plugin that registers email:deliver as an exclusive hook.\n * Logs emails to console and stores them in memory (capped at 100).\n * Auto-activated when import.meta.env.DEV is true and no other provider is selected.\n *\n */\n\nimport type { EmailDeliverEvent, EmailMessage, PluginContext } from \"./types.js\";\n\n/** Plugin ID for the dev console email provider */\nexport const DEV_CONSOLE_EMAIL_PLUGIN_ID = \"emdash-console-email\";\n\n/** Maximum number of emails to keep in memory */\nconst MAX_STORED_EMAILS = 100;\n\n/**\n * Stored email record (in-memory only)\n */\nexport interface StoredEmail {\n\tmessage: EmailMessage;\n\tsource: string;\n\tsentAt: string;\n}\n\n/** In-memory store for dev emails */\nconst storedEmails: StoredEmail[] = [];\n\n/**\n * Get all stored dev emails (most recent first).\n */\nexport function getDevEmails(): StoredEmail[] {\n\treturn storedEmails.toReversed();\n}\n\n/**\n * Clear all stored dev emails.\n */\nexport function clearDevEmails(): void {\n\tstoredEmails.length = 0;\n}\n\n/**\n * The email:deliver handler for the dev console provider.\n * Logs to console and stores in memory.\n */\nexport async function devConsoleEmailDeliver(\n\tevent: EmailDeliverEvent,\n\t_ctx: PluginContext,\n): Promise<void> {\n\tconst { message, source } = event;\n\n\tconsole.log(\n\t\t`\\n📧 [dev-email] Email sent\\n` +\n\t\t\t` From: ${source}\\n` +\n\t\t\t` To: ${message.to}\\n` +\n\t\t\t` Subject: ${message.subject}\\n` +\n\t\t\t` Text: ${message.text.slice(0, 200)}${message.text.length > 200 ? \"...\" : \"\"}\\n`,\n\t);\n\n\t// Store the email\n\tstoredEmails.push({\n\t\tmessage,\n\t\tsource,\n\t\tsentAt: new Date().toISOString(),\n\t});\n\n\t// Cap at MAX_STORED_EMAILS\n\twhile (storedEmails.length > MAX_STORED_EMAILS) {\n\t\tstoredEmails.shift();\n\t}\n}\n","/**\n * Plugin Routes v2\n *\n * Handles plugin API route invocation with:\n * - Input validation via Zod schemas\n * - Route context creation\n * - Error handling\n *\n */\n\nimport { PluginContextFactory, type PluginContextFactoryOptions } from \"./context.js\";\nimport { extractRequestMeta } from \"./request-meta.js\";\nimport type { ResolvedPlugin, RouteContext, PluginRoute } from \"./types.js\";\n\n/**\n * Route metadata (public flag) without the handler.\n * Used by the catch-all route to decide auth before dispatch.\n */\nexport interface RouteMeta {\n\tpublic: boolean;\n}\n\n/**\n * Result from a route invocation\n */\nexport interface RouteResult<T = unknown> {\n\tsuccess: boolean;\n\tdata?: T;\n\terror?: {\n\t\tcode: string;\n\t\tmessage: string;\n\t\tdetails?: unknown;\n\t};\n\tstatus: number;\n}\n\n/**\n * Route invocation options\n */\nexport interface InvokeRouteOptions {\n\t/** The original request */\n\trequest: Request;\n\t/** Request body (already parsed) */\n\tbody?: unknown;\n}\n\n/**\n * Route handler for a plugin\n */\nexport class PluginRouteHandler {\n\tprivate contextFactory: PluginContextFactory;\n\tprivate plugin: ResolvedPlugin;\n\n\tconstructor(plugin: ResolvedPlugin, factoryOptions: PluginContextFactoryOptions) {\n\t\tthis.plugin = plugin;\n\t\tthis.contextFactory = new PluginContextFactory(factoryOptions);\n\t}\n\n\t/**\n\t * Invoke a route by name\n\t */\n\tasync invoke(routeName: string, options: InvokeRouteOptions): Promise<RouteResult> {\n\t\tconst route = this.plugin.routes[routeName];\n\n\t\tif (!route) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"ROUTE_NOT_FOUND\",\n\t\t\t\t\tmessage: `Route \"${routeName}\" not found in plugin \"${this.plugin.id}\"`,\n\t\t\t\t},\n\t\t\t\tstatus: 404,\n\t\t\t};\n\t\t}\n\n\t\t// Validate input if schema is provided\n\t\tlet validatedInput: unknown;\n\t\tif (route.input) {\n\t\t\tconst parseResult = route.input.safeParse(options.body);\n\t\t\tif (!parseResult.success) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\t\tmessage: \"Invalid request body\",\n\t\t\t\t\t\tdetails: parseResult.error.format(),\n\t\t\t\t\t},\n\t\t\t\t\tstatus: 400,\n\t\t\t\t};\n\t\t\t}\n\t\t\tvalidatedInput = parseResult.data;\n\t\t} else {\n\t\t\tvalidatedInput = options.body;\n\t\t}\n\n\t\t// Create route context\n\t\tconst baseContext = this.contextFactory.createContext(this.plugin);\n\t\tconst routeContext: RouteContext = {\n\t\t\t...baseContext,\n\t\t\tinput: validatedInput,\n\t\t\trequest: options.request,\n\t\t\trequestMeta: extractRequestMeta(options.request),\n\t\t};\n\n\t\t// Execute handler\n\t\ttry {\n\t\t\tconst result = await route.handler(routeContext);\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: result,\n\t\t\t\tstatus: 200,\n\t\t\t};\n\t\t} catch (error) {\n\t\t\t// Handle known error types\n\t\t\tif (error instanceof PluginRouteError) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: error.code,\n\t\t\t\t\t\tmessage: error.message,\n\t\t\t\t\t\tdetails: error.details,\n\t\t\t\t\t},\n\t\t\t\t\tstatus: error.status,\n\t\t\t\t};\n\t\t\t}\n\n\t\t\t// Unknown error -- log internally, return generic message\n\t\t\tconsole.error(`[plugin:${this.plugin.id}] Route handler failed:`, error);\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"INTERNAL_ERROR\",\n\t\t\t\t\tmessage: \"An internal error occurred\",\n\t\t\t\t},\n\t\t\t\tstatus: 500,\n\t\t\t};\n\t\t}\n\t}\n\n\t/**\n\t * Get all route names\n\t */\n\tgetRouteNames(): string[] {\n\t\treturn Object.keys(this.plugin.routes);\n\t}\n\n\t/**\n\t * Check if a route exists\n\t */\n\thasRoute(name: string): boolean {\n\t\treturn name in this.plugin.routes;\n\t}\n\n\t/**\n\t * Get route metadata without invoking the handler.\n\t * Returns null if the route doesn't exist.\n\t */\n\tgetRouteMeta(name: string): RouteMeta | null {\n\t\tconst route: PluginRoute | undefined = this.plugin.routes[name];\n\t\tif (!route) return null;\n\t\treturn { public: route.public === true };\n\t}\n}\n\n/**\n * Error class for plugin routes\n * Allows plugins to return structured errors with specific HTTP status codes\n */\nexport class PluginRouteError extends Error {\n\tconstructor(\n\t\tpublic code: string,\n\t\tmessage: string,\n\t\tpublic status: number = 400,\n\t\tpublic details?: unknown,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"PluginRouteError\";\n\t}\n\n\t/**\n\t * Create a bad request error (400)\n\t */\n\tstatic badRequest(message: string, details?: unknown): PluginRouteError {\n\t\treturn new PluginRouteError(\"BAD_REQUEST\", message, 400, details);\n\t}\n\n\t/**\n\t * Create an unauthorized error (401)\n\t */\n\tstatic unauthorized(message: string = \"Unauthorized\"): PluginRouteError {\n\t\treturn new PluginRouteError(\"UNAUTHORIZED\", message, 401);\n\t}\n\n\t/**\n\t * Create a forbidden error (403)\n\t */\n\tstatic forbidden(message: string = \"Forbidden\"): PluginRouteError {\n\t\treturn new PluginRouteError(\"FORBIDDEN\", message, 403);\n\t}\n\n\t/**\n\t * Create a not found error (404)\n\t */\n\tstatic notFound(message: string = \"Not found\"): PluginRouteError {\n\t\treturn new PluginRouteError(\"NOT_FOUND\", message, 404);\n\t}\n\n\t/**\n\t * Create a conflict error (409)\n\t */\n\tstatic conflict(message: string, details?: unknown): PluginRouteError {\n\t\treturn new PluginRouteError(\"CONFLICT\", message, 409, details);\n\t}\n\n\t/**\n\t * Create an internal error (500)\n\t */\n\tstatic internal(message: string = \"Internal error\"): PluginRouteError {\n\t\treturn new PluginRouteError(\"INTERNAL_ERROR\", message, 500);\n\t}\n}\n\n/**\n * Registry for all plugin route handlers\n */\nexport class PluginRouteRegistry {\n\tprivate handlers: Map<string, PluginRouteHandler> = new Map();\n\n\tconstructor(private factoryOptions: PluginContextFactoryOptions) {}\n\n\t/**\n\t * Register a plugin's routes\n\t */\n\tregister(plugin: ResolvedPlugin): void {\n\t\tconst handler = new PluginRouteHandler(plugin, this.factoryOptions);\n\t\tthis.handlers.set(plugin.id, handler);\n\t}\n\n\t/**\n\t * Unregister a plugin's routes\n\t */\n\tunregister(pluginId: string): void {\n\t\tthis.handlers.delete(pluginId);\n\t}\n\n\t/**\n\t * Invoke a plugin route\n\t */\n\tasync invoke(\n\t\tpluginId: string,\n\t\trouteName: string,\n\t\toptions: InvokeRouteOptions,\n\t): Promise<RouteResult> {\n\t\tconst handler = this.handlers.get(pluginId);\n\n\t\tif (!handler) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"PLUGIN_NOT_FOUND\",\n\t\t\t\t\tmessage: `Plugin \"${pluginId}\" not found`,\n\t\t\t\t},\n\t\t\t\tstatus: 404,\n\t\t\t};\n\t\t}\n\n\t\treturn handler.invoke(routeName, options);\n\t}\n\n\t/**\n\t * Get all registered plugin IDs\n\t */\n\tgetPluginIds(): string[] {\n\t\treturn [...this.handlers.keys()];\n\t}\n\n\t/**\n\t * Get routes for a plugin\n\t */\n\tgetRoutes(pluginId: string): string[] {\n\t\treturn this.handlers.get(pluginId)?.getRouteNames() ?? [];\n\t}\n\n\t/**\n\t * Get route metadata for a specific plugin route.\n\t * Returns null if the plugin or route doesn't exist.\n\t */\n\tgetRouteMeta(pluginId: string, routeName: string): RouteMeta | null {\n\t\tconst handler = this.handlers.get(pluginId);\n\t\tif (!handler) return null;\n\t\treturn handler.getRouteMeta(routeName);\n\t}\n}\n\n/**\n * Create a route registry\n */\nexport function createRouteRegistry(\n\tfactoryOptions: PluginContextFactoryOptions,\n): PluginRouteRegistry {\n\treturn new PluginRouteRegistry(factoryOptions);\n}\n","/**\n * Plugin Manager v2\n *\n * Central orchestrator for the plugin system:\n * - Loads and resolves plugins\n * - Manages plugin lifecycle (install, activate, deactivate, uninstall)\n * - Dispatches hooks across all plugins\n * - Routes API requests to plugins\n *\n */\n\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\n\nimport { OptionsRepository } from \"../database/repositories/options.js\";\nimport type { Database } from \"../database/types.js\";\nimport type { Storage } from \"../storage/types.js\";\nimport type { PluginContextFactoryOptions } from \"./context.js\";\nimport { setCronTasksEnabled } from \"./cron.js\";\nimport { definePlugin } from \"./define-plugin.js\";\nimport type { EmailPipeline } from \"./email.js\";\nimport {\n\tHookPipeline,\n\ttype HookResult,\n\tresolveExclusiveHooks as resolveExclusiveHooksShared,\n} from \"./hooks.js\";\nimport { PluginRouteRegistry, type RouteResult, type InvokeRouteOptions } from \"./routes.js\";\nimport type {\n\tPluginDefinition,\n\tResolvedPlugin,\n\tPluginStorageConfig,\n\tMediaItem,\n\tCronEvent,\n} from \"./types.js\";\n\n/** Options table key prefix for exclusive hook DB reads via PluginManager */\nconst EXCLUSIVE_HOOK_KEY_PREFIX = \"emdash:exclusive_hook:\";\n\n/**\n * Plugin state in the manager\n */\nexport type PluginState = \"registered\" | \"installed\" | \"active\" | \"inactive\";\n\n/**\n * Plugin entry in the manager\n */\ninterface PluginEntry {\n\tplugin: ResolvedPlugin;\n\tstate: PluginState;\n}\n\n/**\n * Plugin manager options\n */\nexport interface PluginManagerOptions {\n\t/** Database instance */\n\tdb: Kysely<Database>;\n\t/** Storage backend for direct media uploads */\n\tstorage?: Storage;\n\t/** Function to generate upload URLs for media */\n\tgetUploadUrl?: (\n\t\tfilename: string,\n\t\tcontentType: string,\n\t) => Promise<{ uploadUrl: string; mediaId: string }>;\n}\n\n/**\n * Plugin Manager v2\n *\n * Manages the full lifecycle of plugins and coordinates hooks/routes.\n */\nexport class PluginManager {\n\tprivate plugins: Map<string, PluginEntry> = new Map();\n\tprivate hookPipeline: HookPipeline | null = null;\n\tprivate routeRegistry: PluginRouteRegistry | null = null;\n\tprivate factoryOptions: PluginContextFactoryOptions;\n\tprivate initialized = false;\n\n\tconstructor(private options: PluginManagerOptions) {\n\t\tthis.factoryOptions = {\n\t\t\tdb: options.db,\n\t\t\tstorage: options.storage,\n\t\t\tgetUploadUrl: options.getUploadUrl,\n\t\t};\n\t}\n\n\t/**\n\t * Set the email pipeline used when creating plugin contexts.\n\t * Reinitializes routes/hooks if already initialized so ctx.email is available immediately.\n\t */\n\tsetEmailPipeline(pipeline: EmailPipeline): void {\n\t\tthis.factoryOptions.emailPipeline = pipeline;\n\t\tif (this.initialized) {\n\t\t\tthis.reinitialize();\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Plugin Registration\n\t// =========================================================================\n\n\t/**\n\t * Register a plugin definition\n\t * This resolves the definition and adds it to the manager, but doesn't install it\n\t */\n\tregister<TStorage extends PluginStorageConfig>(\n\t\tdefinition: PluginDefinition<TStorage>,\n\t): ResolvedPlugin<TStorage> {\n\t\tconst resolved = definePlugin(definition);\n\n\t\tif (this.plugins.has(resolved.id)) {\n\t\t\tthrow new Error(`Plugin \"${resolved.id}\" is already registered`);\n\t\t}\n\n\t\tthis.plugins.set(resolved.id, {\n\t\t\tplugin: resolved,\n\t\t\tstate: \"registered\",\n\t\t});\n\n\t\t// Mark as needing reinitialization\n\t\tthis.initialized = false;\n\n\t\treturn resolved;\n\t}\n\n\t/**\n\t * Register multiple plugins\n\t */\n\tregisterAll(definitions: PluginDefinition[]): void {\n\t\tfor (const def of definitions) {\n\t\t\tthis.register(def);\n\t\t}\n\t}\n\n\t/**\n\t * Unregister a plugin\n\t * Plugin must be inactive or just registered\n\t */\n\tunregister(pluginId: string): boolean {\n\t\tconst entry = this.plugins.get(pluginId);\n\t\tif (!entry) return false;\n\n\t\tif (entry.state === \"active\") {\n\t\t\tthrow new Error(`Cannot unregister active plugin \"${pluginId}\". Deactivate it first.`);\n\t\t}\n\n\t\tthis.plugins.delete(pluginId);\n\t\tthis.initialized = false;\n\t\treturn true;\n\t}\n\n\t// =========================================================================\n\t// Plugin Lifecycle\n\t// =========================================================================\n\n\t/**\n\t * Install a plugin (run install hooks, set up storage)\n\t */\n\tasync install(pluginId: string): Promise<HookResult<void>[]> {\n\t\tconst entry = this.plugins.get(pluginId);\n\t\tif (!entry) {\n\t\t\tthrow new Error(`Plugin \"${pluginId}\" not found`);\n\t\t}\n\n\t\tif (entry.state !== \"registered\") {\n\t\t\tthrow new Error(`Plugin \"${pluginId}\" is already installed (state: ${entry.state})`);\n\t\t}\n\n\t\tthis.ensureInitialized();\n\n\t\t// Run install hooks\n\t\tconst results = await this.hookPipeline!.runPluginInstall(pluginId);\n\n\t\t// Check for errors\n\t\tconst failed = results.find((r) => !r.success);\n\t\tif (failed) {\n\t\t\tthrow new Error(`Plugin install failed: ${failed.error?.message ?? \"Unknown error\"}`);\n\t\t}\n\n\t\tentry.state = \"installed\";\n\t\treturn results;\n\t}\n\n\t/**\n\t * Activate a plugin (run activate hooks, enable hooks/routes)\n\t */\n\tasync activate(pluginId: string): Promise<HookResult<void>[]> {\n\t\tconst entry = this.plugins.get(pluginId);\n\t\tif (!entry) {\n\t\t\tthrow new Error(`Plugin \"${pluginId}\" not found`);\n\t\t}\n\n\t\tif (entry.state === \"active\") {\n\t\t\treturn []; // Already active\n\t\t}\n\n\t\tif (entry.state === \"registered\") {\n\t\t\t// Auto-install if not installed\n\t\t\tawait this.install(pluginId);\n\t\t}\n\n\t\tthis.ensureInitialized();\n\n\t\t// Run activate hooks\n\t\tconst results = await this.hookPipeline!.runPluginActivate(pluginId);\n\n\t\t// Check for errors\n\t\tconst failed = results.find((r) => !r.success);\n\t\tif (failed) {\n\t\t\tthrow new Error(`Plugin activation failed: ${failed.error?.message ?? \"Unknown error\"}`);\n\t\t}\n\n\t\tentry.state = \"active\";\n\n\t\t// Re-enable cron tasks for the activated plugin\n\t\tawait setCronTasksEnabled(this.options.db, pluginId, true);\n\n\t\t// Reinitialize pipeline so the newly active plugin's hooks are registered\n\t\tthis.reinitialize();\n\n\t\t// Resolve exclusive hooks (new provider may need auto-selection)\n\t\tawait this.resolveExclusiveHooks();\n\n\t\treturn results;\n\t}\n\n\t/**\n\t * Deactivate a plugin (run deactivate hooks, disable hooks/routes)\n\t */\n\tasync deactivate(pluginId: string): Promise<HookResult<void>[]> {\n\t\tconst entry = this.plugins.get(pluginId);\n\t\tif (!entry) {\n\t\t\tthrow new Error(`Plugin \"${pluginId}\" not found`);\n\t\t}\n\n\t\tif (entry.state !== \"active\") {\n\t\t\treturn []; // Not active\n\t\t}\n\n\t\tthis.ensureInitialized();\n\n\t\t// Run deactivate hooks\n\t\tconst results = await this.hookPipeline!.runPluginDeactivate(pluginId);\n\n\t\t// Disable cron tasks for the deactivated plugin\n\t\tawait setCronTasksEnabled(this.options.db, pluginId, false);\n\n\t\tentry.state = \"inactive\";\n\n\t\t// Reinitialize pipeline so the deactivated plugin's hooks are removed\n\t\tthis.reinitialize();\n\n\t\t// Resolve exclusive hooks (deactivated provider may need clearing)\n\t\tawait this.resolveExclusiveHooks();\n\n\t\treturn results;\n\t}\n\n\t/**\n\t * Uninstall a plugin (run uninstall hooks, optionally delete data)\n\t */\n\tasync uninstall(pluginId: string, deleteData: boolean = false): Promise<HookResult<void>[]> {\n\t\tconst entry = this.plugins.get(pluginId);\n\t\tif (!entry) {\n\t\t\tthrow new Error(`Plugin \"${pluginId}\" not found`);\n\t\t}\n\n\t\t// Deactivate first if active (this also resolves exclusive hooks)\n\t\tif (entry.state === \"active\") {\n\t\t\tawait this.deactivate(pluginId);\n\t\t}\n\n\t\tthis.ensureInitialized();\n\n\t\t// Run uninstall hooks\n\t\tconst results = await this.hookPipeline!.runPluginUninstall(pluginId, deleteData);\n\n\t\t// Delete all cron tasks for the uninstalled plugin\n\t\tawait this.deleteCronTasks(pluginId);\n\n\t\t// Remove from manager\n\t\tthis.plugins.delete(pluginId);\n\t\tthis.initialized = false;\n\n\t\t// Resolve exclusive hooks after removal\n\t\tawait this.resolveExclusiveHooks();\n\n\t\treturn results;\n\t}\n\n\t// =========================================================================\n\t// Hook Dispatch\n\t// =========================================================================\n\n\t/**\n\t * Run content:beforeSave hooks across all active plugins\n\t */\n\tasync runContentBeforeSave(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t\tisNew: boolean,\n\t): Promise<{\n\t\tcontent: Record<string, unknown>;\n\t\tresults: HookResult<Record<string, unknown>>[];\n\t}> {\n\t\tthis.ensureInitialized();\n\t\treturn this.hookPipeline!.runContentBeforeSave(content, collection, isNew);\n\t}\n\n\t/**\n\t * Run content:afterSave hooks across all active plugins\n\t */\n\tasync runContentAfterSave(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t\tisNew: boolean,\n\t): Promise<HookResult<void>[]> {\n\t\tthis.ensureInitialized();\n\t\treturn this.hookPipeline!.runContentAfterSave(content, collection, isNew);\n\t}\n\n\t/**\n\t * Run content:beforeDelete hooks across all active plugins\n\t */\n\tasync runContentBeforeDelete(\n\t\tid: string,\n\t\tcollection: string,\n\t): Promise<{ allowed: boolean; results: HookResult<boolean>[] }> {\n\t\tthis.ensureInitialized();\n\t\treturn this.hookPipeline!.runContentBeforeDelete(id, collection);\n\t}\n\n\t/**\n\t * Run content:afterDelete hooks across all active plugins\n\t */\n\tasync runContentAfterDelete(\n\t\tid: string,\n\t\tcollection: string,\n\t\tpermanent: boolean,\n\t): Promise<HookResult<void>[]> {\n\t\tthis.ensureInitialized();\n\t\treturn this.hookPipeline!.runContentAfterDelete(id, collection, permanent);\n\t}\n\n\t/**\n\t * Run content:afterPublish hooks across all active plugins\n\t */\n\tasync runContentAfterPublish(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t): Promise<HookResult<void>[]> {\n\t\tthis.ensureInitialized();\n\t\treturn this.hookPipeline!.runContentAfterPublish(content, collection);\n\t}\n\n\t/**\n\t * Run content:afterUnpublish hooks across all active plugins\n\t */\n\tasync runContentAfterUnpublish(\n\t\tcontent: Record<string, unknown>,\n\t\tcollection: string,\n\t): Promise<HookResult<void>[]> {\n\t\tthis.ensureInitialized();\n\t\treturn this.hookPipeline!.runContentAfterUnpublish(content, collection);\n\t}\n\n\t/**\n\t * Run media:beforeUpload hooks across all active plugins\n\t */\n\tasync runMediaBeforeUpload(file: { name: string; type: string; size: number }): Promise<{\n\t\tfile: { name: string; type: string; size: number };\n\t\tresults: HookResult<{ name: string; type: string; size: number }>[];\n\t}> {\n\t\tthis.ensureInitialized();\n\t\treturn this.hookPipeline!.runMediaBeforeUpload(file);\n\t}\n\n\t/**\n\t * Run media:afterUpload hooks across all active plugins\n\t */\n\tasync runMediaAfterUpload(media: MediaItem): Promise<HookResult<void>[]> {\n\t\tthis.ensureInitialized();\n\t\treturn this.hookPipeline!.runMediaAfterUpload(media);\n\t}\n\n\t/**\n\t * Invoke the cron hook for a specific plugin (per-plugin dispatch).\n\t * Used as the InvokeCronHookFn callback for CronExecutor.\n\t */\n\tasync invokeCronHook(pluginId: string, event: CronEvent): Promise<void> {\n\t\tthis.ensureInitialized();\n\t\tconst result = await this.hookPipeline!.invokeCronHook(pluginId, event);\n\t\tif (!result.success && result.error) {\n\t\t\tthrow result.error;\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Route Dispatch\n\t// =========================================================================\n\n\t/**\n\t * Invoke a plugin route\n\t */\n\tasync invokeRoute(\n\t\tpluginId: string,\n\t\trouteName: string,\n\t\toptions: InvokeRouteOptions,\n\t): Promise<RouteResult> {\n\t\tthis.ensureInitialized();\n\t\treturn this.routeRegistry!.invoke(pluginId, routeName, options);\n\t}\n\n\t/**\n\t * Get all routes for a plugin\n\t */\n\tgetPluginRoutes(pluginId: string): string[] {\n\t\tthis.ensureInitialized();\n\t\treturn this.routeRegistry!.getRoutes(pluginId);\n\t}\n\n\t// =========================================================================\n\t// Query Methods\n\t// =========================================================================\n\n\t/**\n\t * Get a plugin by ID\n\t */\n\tgetPlugin(pluginId: string): ResolvedPlugin | undefined {\n\t\treturn this.plugins.get(pluginId)?.plugin;\n\t}\n\n\t/**\n\t * Get plugin state\n\t */\n\tgetPluginState(pluginId: string): PluginState | undefined {\n\t\treturn this.plugins.get(pluginId)?.state;\n\t}\n\n\t/**\n\t * Get all registered plugins\n\t */\n\tgetAllPlugins(): Array<{ plugin: ResolvedPlugin; state: PluginState }> {\n\t\treturn Array.from(this.plugins.values(), (entry) => ({\n\t\t\tplugin: entry.plugin,\n\t\t\tstate: entry.state,\n\t\t}));\n\t}\n\n\t/**\n\t * Get all active plugins\n\t */\n\tgetActivePlugins(): ResolvedPlugin[] {\n\t\treturn [...this.plugins.values()]\n\t\t\t.filter((entry) => entry.state === \"active\")\n\t\t\t.map((entry) => entry.plugin);\n\t}\n\n\t/**\n\t * Check if a plugin exists\n\t */\n\thasPlugin(pluginId: string): boolean {\n\t\treturn this.plugins.has(pluginId);\n\t}\n\n\t/**\n\t * Check if a plugin is active\n\t */\n\tisActive(pluginId: string): boolean {\n\t\treturn this.plugins.get(pluginId)?.state === \"active\";\n\t}\n\n\t// =========================================================================\n\t// Exclusive Hooks\n\t// =========================================================================\n\n\t/**\n\t * Get all plugins that registered a handler for an exclusive hook.\n\t */\n\tgetExclusiveHookProviders(hookName: string): Array<{ pluginId: string; pluginName: string }> {\n\t\tthis.ensureInitialized();\n\t\treturn this.hookPipeline!.getExclusiveHookProviders(hookName).map((p) => {\n\t\t\tconst plugin = this.plugins.get(p.pluginId);\n\t\t\treturn {\n\t\t\t\tpluginId: p.pluginId,\n\t\t\t\tpluginName: plugin?.plugin.id ?? p.pluginId,\n\t\t\t};\n\t\t});\n\t}\n\n\t/**\n\t * Read the selected provider for an exclusive hook from the options table.\n\t */\n\tasync getExclusiveHookSelection(hookName: string): Promise<string | null> {\n\t\tconst optionsRepo = new OptionsRepository(this.options.db);\n\t\treturn optionsRepo.get<string>(`${EXCLUSIVE_HOOK_KEY_PREFIX}${hookName}`);\n\t}\n\n\t/**\n\t * Set the selected provider for an exclusive hook in the options table.\n\t * Pass null to clear the selection.\n\t */\n\tasync setExclusiveHookSelection(hookName: string, pluginId: string | null): Promise<void> {\n\t\tconst optionsRepo = new OptionsRepository(this.options.db);\n\t\tconst key = `${EXCLUSIVE_HOOK_KEY_PREFIX}${hookName}`;\n\n\t\tif (pluginId === null) {\n\t\t\tawait optionsRepo.delete(key);\n\t\t\tthis.hookPipeline?.clearExclusiveSelection(hookName);\n\t\t\treturn;\n\t\t}\n\n\t\t// Validate plugin exists and is active\n\t\tconst entry = this.plugins.get(pluginId);\n\t\tif (!entry) {\n\t\t\tthrow new Error(`Plugin \"${pluginId}\" not found`);\n\t\t}\n\t\tif (entry.state !== \"active\") {\n\t\t\tthrow new Error(`Plugin \"${pluginId}\" is not active`);\n\t\t}\n\n\t\tawait optionsRepo.set(key, pluginId);\n\t\tthis.hookPipeline?.setExclusiveSelection(hookName, pluginId);\n\t}\n\n\t/**\n\t * Resolution algorithm for exclusive hooks.\n\t *\n\t * Delegates to the shared resolveExclusiveHooks() function.\n\t * See hooks.ts for the full algorithm description.\n\t */\n\tasync resolveExclusiveHooks(preferredHints?: Map<string, string[]>): Promise<void> {\n\t\tthis.ensureInitialized();\n\n\t\tconst optionsRepo = new OptionsRepository(this.options.db);\n\n\t\tawait resolveExclusiveHooksShared({\n\t\t\tpipeline: this.hookPipeline!,\n\t\t\tisActive: (pluginId) => this.isActive(pluginId),\n\t\t\tgetOption: (key) => optionsRepo.get<string>(key),\n\t\t\tsetOption: (key, value) => optionsRepo.set(key, value),\n\t\t\tdeleteOption: async (key) => {\n\t\t\t\tawait optionsRepo.delete(key);\n\t\t\t},\n\t\t\tpreferredHints,\n\t\t});\n\t}\n\n\t/**\n\t * Get all exclusive hooks with their providers and current selections.\n\t * Used by the admin API.\n\t */\n\tasync getExclusiveHooksInfo(): Promise<\n\t\tArray<{\n\t\t\thookName: string;\n\t\t\tproviders: Array<{ pluginId: string }>;\n\t\t\tselectedPluginId: string | null;\n\t\t}>\n\t> {\n\t\tthis.ensureInitialized();\n\t\tconst exclusiveHookNames = this.hookPipeline!.getRegisteredExclusiveHooks();\n\t\tconst result = [];\n\n\t\tfor (const hookName of exclusiveHookNames) {\n\t\t\tconst providers = this.hookPipeline!.getExclusiveHookProviders(hookName);\n\t\t\tconst selection = await this.getExclusiveHookSelection(hookName);\n\t\t\tresult.push({\n\t\t\t\thookName,\n\t\t\t\tproviders,\n\t\t\t\tselectedPluginId: selection,\n\t\t\t});\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t// =========================================================================\n\t// Internal Methods\n\t// =========================================================================\n\n\t/**\n\t * Initialize or reinitialize the hook pipeline and route registry\n\t */\n\tprivate ensureInitialized(): void {\n\t\tif (this.initialized) return;\n\n\t\t// Get all active plugins for hooks\n\t\tconst activePlugins = this.getActivePlugins();\n\n\t\t// Create hook pipeline with active plugins\n\t\tthis.hookPipeline = new HookPipeline(activePlugins, this.factoryOptions);\n\n\t\t// Create route registry\n\t\tthis.routeRegistry = new PluginRouteRegistry(this.factoryOptions);\n\n\t\t// Register routes for active plugins\n\t\tfor (const plugin of activePlugins) {\n\t\t\tthis.routeRegistry.register(plugin);\n\t\t}\n\n\t\tthis.initialized = true;\n\t}\n\n\t/**\n\t * Force reinitialization (useful after plugin state changes)\n\t */\n\treinitialize(): void {\n\t\tthis.initialized = false;\n\t\tthis.ensureInitialized();\n\t}\n\n\t/**\n\t * Delete all cron tasks for a plugin.\n\t * Used during uninstall.\n\t */\n\tprivate async deleteCronTasks(pluginId: string): Promise<void> {\n\t\ttry {\n\t\t\tawait sql`\n\t\t\t\tDELETE FROM _emdash_cron_tasks\n\t\t\t\tWHERE plugin_id = ${pluginId}\n\t\t\t`.execute(this.options.db);\n\t\t} catch {\n\t\t\t// Cron table may not exist yet (pre-migration). Non-fatal.\n\t\t}\n\t}\n}\n\n/**\n * Create a plugin manager\n */\nexport function createPluginManager(options: PluginManagerOptions): PluginManager {\n\treturn new PluginManager(options);\n}\n","/**\n * No-op Sandbox Runner\n *\n * Default implementation that doesn't support sandboxing.\n * Used on platforms without Worker Loader (Node.js, Deno, etc.).\n *\n */\n\nimport type { PluginManifest } from \"../types.js\";\nimport type { SandboxRunner, SandboxedPlugin, SandboxOptions } from \"./types.js\";\n\n/**\n * Error thrown when attempting to use sandboxing on an unsupported platform.\n */\nexport class SandboxNotAvailableError extends Error {\n\tconstructor() {\n\t\tsuper(\n\t\t\t\"Plugin sandboxing is not available on this platform. \" +\n\t\t\t\t\"Sandboxed plugins require Cloudflare Workers with Worker Loader. \" +\n\t\t\t\t\"Use trusted plugins (from config) instead, or deploy to Cloudflare.\",\n\t\t);\n\t\tthis.name = \"SandboxNotAvailableError\";\n\t}\n}\n\n/**\n * No-op sandbox runner for platforms without isolation support.\n *\n * - `isAvailable()` returns false\n * - `load()` throws SandboxNotAvailableError\n * - `terminateAll()` is a no-op\n *\n * This is the default runner when no platform adapter is configured.\n */\nexport class NoopSandboxRunner implements SandboxRunner {\n\t/**\n\t * Always returns false - sandboxing is not available.\n\t */\n\tisAvailable(): boolean {\n\t\treturn false;\n\t}\n\n\t/**\n\t * Always throws - can't load sandboxed plugins without isolation.\n\t */\n\tasync load(\n\t\t// eslint-disable-next-line @typescript-eslint/no-unused-vars\n\t\t_manifest: PluginManifest,\n\t\t// eslint-disable-next-line @typescript-eslint/no-unused-vars\n\t\t_code: string,\n\t): Promise<SandboxedPlugin> {\n\t\tthrow new SandboxNotAvailableError();\n\t}\n\n\t/**\n\t * No-op - sandboxing not available, email callback is irrelevant.\n\t */\n\tsetEmailSend(): void {\n\t\t// Nothing to do\n\t}\n\n\t/**\n\t * No-op - nothing to terminate.\n\t */\n\tasync terminateAll(): Promise<void> {\n\t\t// Nothing to do\n\t}\n}\n\n/**\n * Create a no-op sandbox runner.\n * This is used as the default when no platform adapter is configured.\n */\nexport function createNoopSandboxRunner(_options?: SandboxOptions): SandboxRunner {\n\treturn new NoopSandboxRunner();\n}\n","/**\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\";\nimport type { JSX } from \"astro/jsx-runtime\";\nimport type { z } from \"astro/zod\";\n\nimport type { FieldType } from \"../schema/types.js\";\n\n// =============================================================================\n// Core Types\n// =============================================================================\n\n/**\n * Plugin capabilities determine what APIs are available in context\n */\nexport type PluginCapability =\n\t| \"network:fetch\" // ctx.http is available (host-restricted via allowedHosts)\n\t| \"network:fetch:any\" // ctx.http is available (unrestricted outbound — use for user-configured URLs)\n\t| \"read:content\" // ctx.content.get/list available\n\t| \"write:content\" // ctx.content.create/update/delete available\n\t| \"read:media\" // ctx.media.get/list available\n\t| \"write:media\" // ctx.media.getUploadUrl/delete available\n\t| \"read:users\" // ctx.users is available\n\t| \"email:send\" // ctx.email is available (when a provider is configured)\n\t| \"email:provide\" // can register email:deliver exclusive hook (transport provider)\n\t| \"email:intercept\" // can register email:beforeSend / email:afterSend hooks\n\t| \"page:inject\"; // can register page:fragments hook (inject scripts/styles into pages)\n\n// =============================================================================\n// Storage Types\n// =============================================================================\n\n/**\n * Storage collection declaration in plugin definition\n */\nexport interface StorageCollectionConfig {\n\t/**\n\t * Fields to index for querying.\n\t * Each entry can be a single field name or an array for composite indexes.\n\t */\n\tindexes: Array<string | string[]>;\n\t/**\n\t * Fields with unique constraints.\n\t * Each entry can be a single field name or an array for composite unique indexes.\n\t * Unique indexes are also queryable (no need to duplicate in `indexes`).\n\t */\n\tuniqueIndexes?: Array<string | string[]>;\n}\n\n/**\n * Plugin storage configuration\n */\nexport type PluginStorageConfig = Record<string, StorageCollectionConfig>;\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\n/**\n * Content list options\n */\nexport interface ContentListOptions {\n\tlimit?: number;\n\tcursor?: string;\n\torderBy?: Record<string, \"asc\" | \"desc\">;\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 */\nexport type HookName = keyof PluginHooks;\n\n/**\n * Hook metadata entry in a plugin manifest.\n * Replaces the plain hook name string with structured metadata.\n */\nexport interface ManifestHookEntry {\n\tname: string;\n\texclusive?: boolean;\n\tpriority?: number;\n\ttimeout?: number;\n}\n\n/**\n * Route metadata entry in a plugin manifest.\n * Replaces the plain route name string with structured metadata.\n */\nexport interface ManifestRouteEntry {\n\tname: string;\n\tpublic?: boolean;\n}\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 = \"string\" | \"number\" | \"boolean\" | \"select\" | \"secret\";\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 type SettingField =\n\t| StringSettingField\n\t| NumberSettingField\n\t| BooleanSettingField\n\t| SelectSettingField\n\t| SecretSettingField;\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}\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\n * Used for sandboxed plugins loaded from marketplace\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 * Sections import functions\n *\n * Import reusable blocks from WordPress WXR exports as EmDash sections.\n */\n\nimport type { PortableTextBlock } from \"@emdash-cms/gutenberg-to-portable-text\";\nimport { gutenbergToPortableText } from \"@emdash-cms/gutenberg-to-portable-text\";\nimport type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport type { WxrPost } from \"../cli/wxr/parser.js\";\nimport type { Database } from \"../database/types.js\";\nimport { slugify } from \"../utils/slugify.js\";\n\n/**\n * Result of sections import operation\n */\nexport interface SectionsImportResult {\n\t/** Number of sections created */\n\tsectionsCreated: number;\n\t/** Number of sections skipped (already exist) */\n\tsectionsSkipped: number;\n\t/** Errors encountered during import */\n\terrors: Array<{ title: string; error: string }>;\n}\n\n/**\n * Import reusable blocks (wp_block post type) from WXR as sections\n *\n * @param posts - All posts from WXR (will filter to wp_block)\n * @param db - Database connection\n * @returns Import result with counts\n */\nexport async function importReusableBlocksAsSections(\n\tposts: WxrPost[],\n\tdb: Kysely<Database>,\n): Promise<SectionsImportResult> {\n\tconst result: SectionsImportResult = {\n\t\tsectionsCreated: 0,\n\t\tsectionsSkipped: 0,\n\t\terrors: [],\n\t};\n\n\t// Filter to only wp_block posts\n\tconst reusableBlocks = posts.filter((post) => post.postType === \"wp_block\");\n\n\tif (reusableBlocks.length === 0) {\n\t\treturn result;\n\t}\n\n\tfor (const block of reusableBlocks) {\n\t\ttry {\n\t\t\tconst slug = block.postName || slugify(block.title || `block-${block.id || Date.now()}`);\n\n\t\t\t// Check if section already exists\n\t\t\tconst existing = await db\n\t\t\t\t.selectFrom(\"_emdash_sections\")\n\t\t\t\t.select(\"id\")\n\t\t\t\t.where(\"slug\", \"=\", slug)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (existing) {\n\t\t\t\tresult.sectionsSkipped++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Convert Gutenberg content to Portable Text\n\t\t\tconst content: PortableTextBlock[] = block.content\n\t\t\t\t? gutenbergToPortableText(block.content)\n\t\t\t\t: [];\n\n\t\t\tconst id = ulid();\n\t\t\tconst now = new Date().toISOString();\n\n\t\t\tawait db\n\t\t\t\t.insertInto(\"_emdash_sections\")\n\t\t\t\t.values({\n\t\t\t\t\tid,\n\t\t\t\t\tslug,\n\t\t\t\t\ttitle: block.title || \"Untitled Block\",\n\t\t\t\t\tdescription: null,\n\t\t\t\t\tkeywords: null,\n\t\t\t\t\tcontent: JSON.stringify(content),\n\t\t\t\t\tpreview_media_id: null,\n\t\t\t\t\tsource: \"import\",\n\t\t\t\t\ttheme_id: null,\n\t\t\t\t\tcreated_at: now,\n\t\t\t\t\tupdated_at: now,\n\t\t\t\t})\n\t\t\t\t.execute();\n\n\t\t\tresult.sectionsCreated++;\n\t\t} catch (error) {\n\t\t\tresult.errors.push({\n\t\t\t\ttitle: block.title || \"Untitled Block\",\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t}\n\t}\n\n\treturn result;\n}\n","/**\n * Import source registry\n *\n * Manages available import sources and provides URL probing.\n */\n\nimport { validateExternalUrl } from \"./ssrf.js\";\nimport type { ImportSource, ProbeResult, SourceProbeResult } from \"./types.js\";\n\n// Regex pattern for URL normalization\nconst TRAILING_SLASHES_PATTERN = /\\/+$/;\n\n/** Registered import sources */\nconst sources = new Map<string, ImportSource>();\n\n/**\n * Register an import source\n */\nexport function registerSource(source: ImportSource): void {\n\tsources.set(source.id, source);\n}\n\n/**\n * Get a source by ID\n */\nexport function getSource(id: string): ImportSource | undefined {\n\treturn sources.get(id);\n}\n\n/**\n * Get all registered sources\n */\nexport function getAllSources(): ImportSource[] {\n\treturn [...sources.values()];\n}\n\n/**\n * Get sources that can handle file uploads\n */\nexport function getFileSources(): ImportSource[] {\n\treturn getAllSources().filter((s) => s.requiresFile);\n}\n\n/**\n * Get sources that can probe URLs\n */\nexport function getUrlSources(): ImportSource[] {\n\treturn getAllSources().filter((s) => s.canProbe);\n}\n\n/**\n * Probe a URL against all registered sources\n *\n * Returns probe results sorted by confidence (definite > likely > possible)\n */\nexport async function probeUrl(url: string): Promise<ProbeResult> {\n\t// Normalize URL\n\tlet normalizedUrl = url.trim();\n\tif (!normalizedUrl.startsWith(\"http\")) {\n\t\tnormalizedUrl = `https://${normalizedUrl}`;\n\t}\n\n\t// Remove trailing slash for consistency\n\tnormalizedUrl = normalizedUrl.replace(TRAILING_SLASHES_PATTERN, \"\");\n\n\t// SSRF: reject internal/private network targets\n\tvalidateExternalUrl(normalizedUrl);\n\n\tconst results: SourceProbeResult[] = [];\n\tconst urlSources = getUrlSources();\n\n\t// Probe all sources in parallel\n\tconst probePromises = urlSources.map(async (source) => {\n\t\ttry {\n\t\t\tconst result = await source.probe?.(normalizedUrl);\n\t\t\tif (result) {\n\t\t\t\treturn result;\n\t\t\t}\n\t\t} catch (error) {\n\t\t\t// Probe failed, skip this source\n\t\t\tconsole.debug(`Probe failed for ${source.id}:`, error);\n\t\t}\n\t\treturn null;\n\t});\n\n\tconst probeResults = await Promise.allSettled(probePromises);\n\n\tfor (const result of probeResults) {\n\t\tif (result.status === \"fulfilled\" && result.value) {\n\t\t\tresults.push(result.value);\n\t\t}\n\t}\n\n\t// Sort by confidence\n\tconst confidenceOrder = { definite: 0, likely: 1, possible: 2 };\n\tresults.sort((a, b) => confidenceOrder[a.confidence] - confidenceOrder[b.confidence]);\n\n\treturn {\n\t\turl: normalizedUrl,\n\t\tisWordPress: results.length > 0,\n\t\tbestMatch: results[0] ?? null,\n\t\tallMatches: results,\n\t};\n}\n\n/**\n * Clear all registered sources (useful for testing)\n */\nexport function clearSources(): void {\n\tsources.clear();\n}\n","/**\n * Shared import utilities\n *\n * Common constants and functions used across all WordPress import sources.\n */\n\nimport mime from \"mime/lite\";\n\nimport type { ImportFieldDef, CollectionSchemaStatus } from \"./types.js\";\n\n// =============================================================================\n// Constants\n// =============================================================================\n\n/** Internal WordPress post types that should be excluded from import */\nexport const INTERNAL_POST_TYPES = [\n\t\"revision\",\n\t\"nav_menu_item\",\n\t\"custom_css\",\n\t\"customize_changeset\",\n\t\"oembed_cache\",\n\t\"wp_global_styles\",\n\t\"wp_navigation\",\n\t\"wp_template\",\n\t\"wp_template_part\",\n\t\"attachment\", // Handled separately as media\n\t\"wp_block\", // Handled separately as sections (reusable blocks)\n];\n\n/** Internal meta key prefixes to filter out */\nexport const INTERNAL_META_PREFIXES = [\"_edit_\", \"_wp_\"];\n\nconst NUMERIC_PATTERN = /^-?\\d+(\\.\\d+)?$/;\nconst TRAILING_SLASHES = /\\/+$/;\nconst WP_JSON_SUFFIX = /\\/wp-json\\/?.*$/;\n\n/** Specific internal meta keys */\nexport const INTERNAL_META_KEYS = [\"_edit_last\", \"_edit_lock\", \"_pingme\", \"_encloseme\"];\n\n/** Base fields required for any WordPress import */\nexport const BASE_REQUIRED_FIELDS: ImportFieldDef[] = [\n\t{ slug: \"title\", label: \"Title\", type: \"string\", required: true, searchable: true },\n\t{ slug: \"content\", label: \"Content\", type: \"portableText\", required: false, searchable: true },\n\t{ slug: \"excerpt\", label: \"Excerpt\", type: \"text\", required: false },\n];\n\n/** Featured image field - only added to post types that have _thumbnail_id */\nexport const FEATURED_IMAGE_FIELD: ImportFieldDef = {\n\tslug: \"featured_image\",\n\tlabel: \"Featured Image\",\n\ttype: \"image\",\n\trequired: false,\n};\n\n// =============================================================================\n// Type Guards\n// =============================================================================\n\n/**\n * Check if a post type is internal/should be excluded\n */\nexport function isInternalPostType(type: string): boolean {\n\treturn INTERNAL_POST_TYPES.includes(type);\n}\n\n/**\n * Check if a meta key is internal/should be filtered out\n */\nexport function isInternalMetaKey(key: string): boolean {\n\t// Check specific keys\n\tif (INTERNAL_META_KEYS.includes(key)) return true;\n\n\t// Check prefixes\n\tfor (const prefix of INTERNAL_META_PREFIXES) {\n\t\tif (key.startsWith(prefix)) return true;\n\t}\n\n\t// Keep these useful ones\n\tif (key === \"_thumbnail_id\") return false;\n\tif (key.startsWith(\"_yoast_\")) return false;\n\tif (key.startsWith(\"_rank_math_\")) return false;\n\n\t// Other underscore prefixes are usually internal\n\tif (key.startsWith(\"_\")) return true;\n\n\treturn false;\n}\n\n// =============================================================================\n// Status Mapping\n// =============================================================================\n\n/** Valid WordPress statuses */\nexport type WpStatus = \"publish\" | \"draft\" | \"pending\" | \"private\" | \"future\";\n\n/**\n * Map WordPress status to normalized status\n */\nexport function mapWpStatus(status: string | undefined): WpStatus {\n\tswitch (status) {\n\t\tcase \"publish\":\n\t\t\treturn \"publish\";\n\t\tcase \"draft\":\n\t\t\treturn \"draft\";\n\t\tcase \"pending\":\n\t\t\treturn \"pending\";\n\t\tcase \"private\":\n\t\t\treturn \"private\";\n\t\tcase \"future\":\n\t\t\treturn \"future\";\n\t\tdefault:\n\t\t\treturn \"draft\";\n\t}\n}\n\n// =============================================================================\n// Collection Mapping\n// =============================================================================\n\n/** Default mappings from WordPress post types to EmDash collections */\nconst POST_TYPE_TO_COLLECTION: Record<string, string> = {\n\tpost: \"posts\",\n\tpage: \"pages\",\n\tattachment: \"media\",\n\tproduct: \"products\",\n\tportfolio: \"portfolio\",\n\ttestimonial: \"testimonials\",\n\tteam: \"team\",\n\tevent: \"events\",\n\tfaq: \"faqs\",\n};\n\n/**\n * Map WordPress post type to EmDash collection name\n */\nexport function mapPostTypeToCollection(postType: string): string {\n\treturn POST_TYPE_TO_COLLECTION[postType] || postType;\n}\n\n// =============================================================================\n// Meta Key Mapping\n// =============================================================================\n\n/**\n * Map WordPress meta key to EmDash field slug\n */\nexport function mapMetaKeyToField(key: string): string {\n\t// SEO plugins\n\tif (key === \"_yoast_wpseo_title\") return \"seo_title\";\n\tif (key === \"_yoast_wpseo_metadesc\") return \"seo_description\";\n\tif (key === \"_rank_math_title\") return \"seo_title\";\n\tif (key === \"_rank_math_description\") return \"seo_description\";\n\tif (key === \"_thumbnail_id\") return \"featured_image\";\n\n\t// Remove leading underscore\n\tif (key.startsWith(\"_\")) return key.slice(1);\n\n\treturn key;\n}\n\n/**\n * Infer field type from meta key name and sample value\n */\nexport function inferMetaType(\n\tkey: string,\n\tvalue: string | undefined,\n): \"string\" | \"number\" | \"boolean\" | \"date\" | \"json\" {\n\tif (key.endsWith(\"_id\") || key === \"_thumbnail_id\") return \"string\";\n\tif (key.endsWith(\"_date\") || key.endsWith(\"_time\")) return \"date\";\n\tif (key.endsWith(\"_count\") || key.endsWith(\"_number\")) return \"number\";\n\n\tif (!value) return \"string\";\n\n\t// Serialized PHP or JSON\n\tif (value.startsWith(\"a:\") || value.startsWith(\"{\") || value.startsWith(\"[\")) return \"json\";\n\n\t// Number\n\tif (NUMERIC_PATTERN.test(value)) return \"number\";\n\n\t// Boolean\n\tif ([\"0\", \"1\", \"true\", \"false\"].includes(value)) return \"boolean\";\n\n\treturn \"string\";\n}\n\n// =============================================================================\n// String Utilities\n// =============================================================================\n\nexport { slugify } from \"../utils/slugify.js\";\n\n/**\n * Normalize URL for API requests\n */\nexport function normalizeUrl(url: string): string {\n\tlet normalized = url.trim();\n\n\t// Add protocol if missing\n\tif (!normalized.startsWith(\"http\")) {\n\t\tnormalized = `https://${normalized}`;\n\t}\n\n\t// Remove trailing slash\n\tnormalized = normalized.replace(TRAILING_SLASHES, \"\");\n\n\t// Remove /wp-json if included\n\tnormalized = normalized.replace(WP_JSON_SUFFIX, \"\");\n\n\treturn normalized;\n}\n\n// =============================================================================\n// File Utilities\n// =============================================================================\n\n/**\n * Extract filename from URL\n */\nexport function getFilenameFromUrl(url: string): string | undefined {\n\ttry {\n\t\tconst parsed = new URL(url);\n\t\tconst segments = parsed.pathname.split(\"/\").filter(Boolean);\n\t\treturn segments.pop();\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Guess MIME type from filename\n */\nexport function guessMimeType(filename: string): string | undefined {\n\treturn mime.getType(filename) ?? undefined;\n}\n\n// =============================================================================\n// Attachment Map Builder\n// =============================================================================\n\n/**\n * Build a map of attachment IDs to URLs for resolving featured images\n */\nexport function buildAttachmentMap(\n\tattachments: Array<{ id?: number | string; url?: string }>,\n): Map<string, string> {\n\tconst map = new Map<string, string>();\n\tfor (const att of attachments) {\n\t\tif (att.id && att.url) {\n\t\t\tmap.set(String(att.id), att.url);\n\t\t}\n\t}\n\treturn map;\n}\n\n// =============================================================================\n// Schema Compatibility\n// =============================================================================\n\n/**\n * Check if two field types are compatible for import\n */\nexport function isTypeCompatible(requiredType: string, existingType: string): boolean {\n\tif (requiredType === existingType) return true;\n\n\tconst compatibleTypes: Record<string, string[]> = {\n\t\tstring: [\"string\", \"text\", \"slug\"],\n\t\ttext: [\"string\", \"text\"],\n\t\tportableText: [\"portableText\", \"json\"],\n\t\tnumber: [\"number\", \"integer\"],\n\t\tinteger: [\"number\", \"integer\"],\n\t};\n\n\tconst compatible = compatibleTypes[requiredType];\n\treturn compatible?.includes(existingType) ?? false;\n}\n\n// =============================================================================\n// Byline Import Utilities\n// =============================================================================\n\nimport type { BylineRepository } from \"../database/repositories/byline.js\";\nimport { slugify as slugifyFn } from \"../utils/slugify.js\";\n\nconst MAX_SLUG_COLLISION_ATTEMPTS = 1000;\n\n/**\n * Find or create a unique byline slug, capped at MAX_SLUG_COLLISION_ATTEMPTS.\n */\nexport async function ensureUniqueBylineSlug(\n\tbylineRepo: BylineRepository,\n\tbaseSlug: string,\n): Promise<string> {\n\tlet candidate = baseSlug;\n\tlet suffix = 2;\n\twhile (await bylineRepo.findBySlug(candidate)) {\n\t\tif (suffix > MAX_SLUG_COLLISION_ATTEMPTS) {\n\t\t\tthrow new Error(\n\t\t\t\t`Byline slug collision limit exceeded for base slug \"${baseSlug}\". ` +\n\t\t\t\t\t`Tried ${MAX_SLUG_COLLISION_ATTEMPTS} variants.`,\n\t\t\t);\n\t\t}\n\t\tcandidate = `${baseSlug}-${suffix}`;\n\t\tsuffix++;\n\t}\n\treturn candidate;\n}\n\n/**\n * Resolve (find-or-create) a byline for an imported WordPress author.\n * Caches results in `cache` keyed by `authorLogin:mappedUserId`.\n */\nexport async function resolveImportByline(\n\tauthorLogin: string | undefined,\n\tdisplayName: string | undefined,\n\tmappedUserId: string | undefined,\n\tbylineRepo: BylineRepository,\n\tcache: Map<string, string>,\n): Promise<string | undefined> {\n\tif (!authorLogin) return undefined;\n\tconst cacheKey = `${authorLogin}:${mappedUserId ?? \"\"}`;\n\tconst cached = cache.get(cacheKey);\n\tif (cached) return cached;\n\n\tif (mappedUserId) {\n\t\tconst existingForUser = await bylineRepo.findByUserId(mappedUserId);\n\t\tif (existingForUser) {\n\t\t\tcache.set(cacheKey, existingForUser.id);\n\t\t\treturn existingForUser.id;\n\t\t}\n\t}\n\n\tconst name = displayName || authorLogin;\n\tconst slugBase = slugifyFn(authorLogin);\n\tconst slug = await ensureUniqueBylineSlug(bylineRepo, slugBase || \"author\");\n\tconst created = await bylineRepo.create({\n\t\tslug,\n\t\tdisplayName: name,\n\t\tuserId: mappedUserId ?? null,\n\t\tisGuest: !mappedUserId,\n\t});\n\n\tcache.set(cacheKey, created.id);\n\treturn created.id;\n}\n\n// =============================================================================\n// Schema Compatibility\n// =============================================================================\n\n/**\n * Check schema compatibility between required fields and existing collection\n */\nexport function checkSchemaCompatibility(\n\trequiredFields: ImportFieldDef[],\n\texistingCollection: { slug: string; fields: Map<string, { type: string }> } | undefined,\n): CollectionSchemaStatus {\n\tif (!existingCollection) {\n\t\t// Collection doesn't exist - will need to create it\n\t\tconst fieldStatus: CollectionSchemaStatus[\"fieldStatus\"] = {};\n\t\tfor (const field of requiredFields) {\n\t\t\tfieldStatus[field.slug] = {\n\t\t\t\tstatus: \"missing\",\n\t\t\t\trequiredType: field.type,\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\texists: false,\n\t\t\tfieldStatus,\n\t\t\tcanImport: true,\n\t\t};\n\t}\n\n\t// Collection exists - check field compatibility\n\tconst fieldStatus: CollectionSchemaStatus[\"fieldStatus\"] = {};\n\tconst incompatibleFields: string[] = [];\n\n\tfor (const field of requiredFields) {\n\t\tconst existingField = existingCollection.fields.get(field.slug);\n\n\t\tif (!existingField) {\n\t\t\tfieldStatus[field.slug] = {\n\t\t\t\tstatus: \"missing\",\n\t\t\t\trequiredType: field.type,\n\t\t\t};\n\t\t} else if (isTypeCompatible(field.type, existingField.type)) {\n\t\t\tfieldStatus[field.slug] = {\n\t\t\t\tstatus: \"compatible\",\n\t\t\t\texistingType: existingField.type,\n\t\t\t\trequiredType: field.type,\n\t\t\t};\n\t\t} else {\n\t\t\tfieldStatus[field.slug] = {\n\t\t\t\tstatus: \"type_mismatch\",\n\t\t\t\texistingType: existingField.type,\n\t\t\t\trequiredType: field.type,\n\t\t\t};\n\t\t\tincompatibleFields.push(field.slug);\n\t\t}\n\t}\n\n\tconst canImport = incompatibleFields.length === 0;\n\tconst reason = canImport\n\t\t? undefined\n\t\t: `Incompatible field types: ${incompatibleFields.join(\", \")}`;\n\n\treturn {\n\t\texists: true,\n\t\tfieldStatus,\n\t\tcanImport,\n\t\treason,\n\t};\n}\n","/**\n * WXR (WordPress eXtended RSS) import source\n *\n * Handles WordPress export file uploads (.xml).\n * This wraps the existing WXR parsing and analysis logic.\n */\n\nimport { gutenbergToPortableText } from \"@emdash-cms/gutenberg-to-portable-text\";\n\nimport { parseWxrString, type WxrData, type WxrPost } from \"../../cli/wxr/parser.js\";\nimport type {\n\tImportSource,\n\tImportAnalysis,\n\tImportContext,\n\tSourceInput,\n\tFetchOptions,\n\tNormalizedItem,\n\tPostTypeAnalysis,\n\tAttachmentInfo,\n\tNavMenuAnalysis,\n\tTaxonomyAnalysis,\n\tReusableBlockAnalysis,\n} from \"../types.js\";\nimport {\n\tBASE_REQUIRED_FIELDS,\n\tFEATURED_IMAGE_FIELD,\n\tisInternalPostType,\n\tisInternalMetaKey,\n\tmapWpStatus,\n\tmapPostTypeToCollection,\n\tmapMetaKeyToField,\n\tinferMetaType,\n\tslugify,\n\tbuildAttachmentMap,\n\tgetFilenameFromUrl,\n\tguessMimeType,\n\tcheckSchemaCompatibility,\n} from \"../utils.js\";\n\nexport const wxrSource: ImportSource = {\n\tid: \"wxr\",\n\tname: \"WordPress Export File\",\n\tdescription: \"Upload a WordPress export file (.xml)\",\n\ticon: \"upload\",\n\trequiresFile: true,\n\tcanProbe: false,\n\n\tasync analyze(input: SourceInput, context: ImportContext): Promise<ImportAnalysis> {\n\t\tif (input.type !== \"file\") {\n\t\t\tthrow new Error(\"WXR source requires a file input\");\n\t\t}\n\n\t\tconst text = await input.file.text();\n\t\tconst wxr = await parseWxrString(text);\n\n\t\t// Get existing collections for schema compatibility check\n\t\tconst existingCollections = context.getExistingCollections\n\t\t\t? await context.getExistingCollections()\n\t\t\t: new Map();\n\n\t\treturn analyzeWxrData(wxr, existingCollections);\n\t},\n\n\tasync *fetchContent(input: SourceInput, options: FetchOptions): AsyncGenerator<NormalizedItem> {\n\t\tif (input.type !== \"file\") {\n\t\t\tthrow new Error(\"WXR source requires a file input\");\n\t\t}\n\n\t\tconst text = await input.file.text();\n\t\tconst wxr = await parseWxrString(text);\n\n\t\t// Build attachment ID -> URL map for resolving featured images\n\t\tconst attachmentMap = buildAttachmentMap(wxr.attachments);\n\n\t\tlet count = 0;\n\t\tfor (const post of wxr.posts) {\n\t\t\tconst postType = post.postType || \"post\";\n\n\t\t\t// Skip if not in requested post types\n\t\t\tif (!options.postTypes.includes(postType)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Skip internal post types\n\t\t\tif (isInternalPostType(postType)) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Skip drafts if not requested\n\t\t\tif (!options.includeDrafts && post.status !== \"publish\") {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Convert to normalized item\n\t\t\tyield wxrPostToNormalizedItem(post, attachmentMap);\n\n\t\t\tcount++;\n\t\t\tif (options.limit && count >= options.limit) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t},\n};\n\n/**\n * Analyze WXR data and return normalized ImportAnalysis\n */\nfunction analyzeWxrData(\n\twxr: WxrData,\n\texistingCollections: Map<string, { slug: string; fields: Map<string, { type: string }> }>,\n): ImportAnalysis {\n\t// Count post types and track which have featured images\n\tconst postTypeCounts = new Map<string, number>();\n\tconst postTypesWithThumbnails = new Set<string>();\n\tconst metaKeys = new Map<string, { count: number; samples: string[]; isInternal: boolean }>();\n\tconst authorPostCounts = new Map<string, number>();\n\n\tfor (const post of wxr.posts) {\n\t\tconst type = post.postType || \"post\";\n\t\tpostTypeCounts.set(type, (postTypeCounts.get(type) || 0) + 1);\n\n\t\t// Count posts per author (by login)\n\t\tif (post.creator) {\n\t\t\tauthorPostCounts.set(post.creator, (authorPostCounts.get(post.creator) || 0) + 1);\n\t\t}\n\n\t\t// Track if this post type has featured images\n\t\tif (post.meta.has(\"_thumbnail_id\")) {\n\t\t\tpostTypesWithThumbnails.add(type);\n\t\t}\n\n\t\t// Analyze meta keys\n\t\tfor (const [key, value] of post.meta) {\n\t\t\tconst existing = metaKeys.get(key);\n\t\t\tif (existing) {\n\t\t\t\texisting.count++;\n\t\t\t\tif (existing.samples.length < 3 && value) {\n\t\t\t\t\texisting.samples.push(value.slice(0, 100));\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tmetaKeys.set(key, {\n\t\t\t\t\tcount: 1,\n\t\t\t\t\tsamples: value ? [value.slice(0, 100)] : [],\n\t\t\t\t\tisInternal: isInternalMetaKey(key),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\t// Map meta keys to fields (for custom fields analysis)\n\tconst customFields = [...metaKeys.entries()]\n\t\t.filter(([_, info]) => !info.isInternal)\n\t\t.map(([key, info]) => ({\n\t\t\tkey,\n\t\t\tcount: info.count,\n\t\t\tsamples: info.samples,\n\t\t\tsuggestedField: mapMetaKeyToField(key),\n\t\t\tsuggestedType: inferMetaType(key, info.samples[0]),\n\t\t\tisInternal: info.isInternal,\n\t\t}))\n\t\t.toSorted((a, b) => b.count - a.count);\n\n\t// Build post type analysis with schema compatibility\n\tconst postTypes: PostTypeAnalysis[] = [...postTypeCounts.entries()]\n\t\t.filter(([type]) => !isInternalPostType(type))\n\t\t.map(([name, count]) => {\n\t\t\tconst suggestedCollection = mapPostTypeToCollection(name);\n\t\t\tconst existingCollection = existingCollections.get(suggestedCollection);\n\n\t\t\t// Build required fields - add featured_image only if posts have thumbnails\n\t\t\tconst requiredFields = [...BASE_REQUIRED_FIELDS];\n\t\t\tif (postTypesWithThumbnails.has(name)) {\n\t\t\t\trequiredFields.push(FEATURED_IMAGE_FIELD);\n\t\t\t}\n\n\t\t\tconst schemaStatus = checkSchemaCompatibility(requiredFields, existingCollection);\n\n\t\t\treturn {\n\t\t\t\tname,\n\t\t\t\tcount,\n\t\t\t\tsuggestedCollection,\n\t\t\t\trequiredFields,\n\t\t\t\tschemaStatus,\n\t\t\t};\n\t\t})\n\t\t.toSorted((a, b) => b.count - a.count);\n\n\t// Build attachment info list\n\tconst attachmentItems: AttachmentInfo[] = wxr.attachments.map((att) => {\n\t\tconst filename = att.url ? getFilenameFromUrl(att.url) : undefined;\n\t\tconst mimeType = filename ? guessMimeType(filename) : undefined;\n\t\treturn {\n\t\t\tid: att.id,\n\t\t\ttitle: att.title,\n\t\t\turl: att.url,\n\t\t\tfilename,\n\t\t\tmimeType,\n\t\t};\n\t});\n\n\t// Analyze navigation menus\n\tconst navMenus: NavMenuAnalysis[] = wxr.navMenus.map((menu) => ({\n\t\tname: menu.name,\n\t\tlabel: menu.label,\n\t\titemCount: menu.items.length,\n\t}));\n\n\t// Analyze custom taxonomies (from wp:term elements, excluding category/post_tag/nav_menu)\n\tconst taxonomyMap = new Map<string, { count: number; samples: string[] }>();\n\tfor (const term of wxr.terms) {\n\t\tif (\n\t\t\tterm.taxonomy === \"category\" ||\n\t\t\tterm.taxonomy === \"post_tag\" ||\n\t\t\tterm.taxonomy === \"nav_menu\"\n\t\t) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst existing = taxonomyMap.get(term.taxonomy);\n\t\tif (existing) {\n\t\t\texisting.count++;\n\t\t\tif (existing.samples.length < 3) {\n\t\t\t\texisting.samples.push(term.name);\n\t\t\t}\n\t\t} else {\n\t\t\ttaxonomyMap.set(term.taxonomy, {\n\t\t\t\tcount: 1,\n\t\t\t\tsamples: [term.name],\n\t\t\t});\n\t\t}\n\t}\n\n\tconst customTaxonomies: TaxonomyAnalysis[] = Array.from(\n\t\ttaxonomyMap.entries(),\n\t\t([slug, info]) => ({\n\t\t\tslug,\n\t\t\ttermCount: info.count,\n\t\t\tsampleTerms: info.samples,\n\t\t}),\n\t).toSorted((a, b) => b.termCount - a.termCount);\n\n\t// Analyze reusable blocks (wp_block post type)\n\tconst reusableBlocks: ReusableBlockAnalysis[] = wxr.posts\n\t\t.filter((post) => post.postType === \"wp_block\")\n\t\t.map((post) => ({\n\t\t\tid: post.id || 0,\n\t\t\ttitle: post.title || \"Untitled Block\",\n\t\t\tslug: post.postName || slugify(post.title || `block-${post.id || Date.now()}`),\n\t\t}));\n\n\treturn {\n\t\tsourceId: \"wxr\",\n\t\tsite: {\n\t\t\ttitle: wxr.site.title || \"WordPress Site\",\n\t\t\turl: wxr.site.link || \"\",\n\t\t},\n\t\tpostTypes,\n\t\tattachments: {\n\t\t\tcount: wxr.attachments.length,\n\t\t\titems: attachmentItems,\n\t\t},\n\t\tcategories: wxr.categories.length,\n\t\ttags: wxr.tags.length,\n\t\tauthors: wxr.authors.map((a) => ({\n\t\t\tid: a.id,\n\t\t\tlogin: a.login,\n\t\t\temail: a.email,\n\t\t\tdisplayName: a.displayName || a.login || \"Unknown\",\n\t\t\tpostCount: a.login ? authorPostCounts.get(a.login) || 0 : 0,\n\t\t})),\n\t\tnavMenus: navMenus.length > 0 ? navMenus : undefined,\n\t\tcustomTaxonomies: customTaxonomies.length > 0 ? customTaxonomies : undefined,\n\t\treusableBlocks: reusableBlocks.length > 0 ? reusableBlocks : undefined,\n\t\tcustomFields,\n\t};\n}\n\n/**\n * Convert a WXR post to a normalized item\n */\nfunction wxrPostToNormalizedItem(\n\tpost: WxrPost,\n\tattachmentMap: Map<string, string>,\n): NormalizedItem {\n\tconst content = post.content ? gutenbergToPortableText(post.content) : [];\n\n\t// Resolve featured image: _thumbnail_id is the attachment ID, look up the URL\n\tconst thumbnailId = post.meta.get(\"_thumbnail_id\");\n\tconst featuredImage = thumbnailId ? attachmentMap.get(String(thumbnailId)) : undefined;\n\n\t// Convert custom taxonomies Map to Record\n\tlet customTaxonomies: Record<string, string[]> | undefined;\n\tif (post.customTaxonomies && post.customTaxonomies.size > 0) {\n\t\tcustomTaxonomies = Object.fromEntries(post.customTaxonomies);\n\t}\n\n\treturn {\n\t\tsourceId: post.id || 0,\n\t\tpostType: post.postType || \"post\",\n\t\tstatus: mapWpStatus(post.status),\n\t\tslug: post.postName || slugify(post.title || `post-${post.id || Date.now()}`),\n\t\ttitle: post.title || \"Untitled\",\n\t\tcontent,\n\t\texcerpt: post.excerpt,\n\t\tdate: parseWxrDate(post.postDateGmt, post.pubDate, post.postDate) ?? new Date(),\n\t\tmodified: parseWxrDate(post.postModifiedGmt, undefined, post.postModified),\n\t\tauthor: post.creator,\n\t\tcategories: post.categories,\n\t\ttags: post.tags,\n\t\tmeta: Object.fromEntries(post.meta),\n\t\tfeaturedImage,\n\t\t// Hierarchical content support\n\t\tparentId: post.postParent && post.postParent !== 0 ? post.postParent : undefined,\n\t\tmenuOrder: post.menuOrder,\n\t\t// Custom taxonomy assignments\n\t\tcustomTaxonomies,\n\t};\n}\n\n/**\n * WordPress uses \"0000-00-00 00:00:00\" as a sentinel for missing GMT dates\n * (e.g. unpublished drafts). This must be treated as absent.\n */\nexport const WXR_ZERO_DATE = \"0000-00-00 00:00:00\";\n\n/**\n * Parse a WXR date with the correct fallback chain:\n * 1. GMT date (always UTC, most reliable)\n * 2. pubDate (RFC 2822, includes timezone offset)\n * 3. Site-local date (MySQL datetime without timezone, imprecise but best available)\n *\n * Returns undefined when none of the inputs yield a valid date.\n * Callers that need a guaranteed Date should use `?? new Date()`.\n */\nexport function parseWxrDate(\n\tgmtDate: string | undefined,\n\tpubDate: string | undefined,\n\tlocalDate: string | undefined,\n): Date | undefined {\n\tif (gmtDate && gmtDate !== WXR_ZERO_DATE) {\n\t\t// GMT dates from WordPress are \"YYYY-MM-DD HH:MM:SS\" in UTC.\n\t\t// Append \"Z\" so the JS Date constructor treats them as UTC.\n\t\treturn new Date(gmtDate.replace(\" \", \"T\") + \"Z\");\n\t}\n\n\tif (pubDate) {\n\t\t// RFC 2822 format includes timezone offset, JS Date parses it correctly\n\t\tconst d = new Date(pubDate);\n\t\tif (!isNaN(d.getTime())) return d;\n\t}\n\n\tif (localDate) {\n\t\t// Site-local time without timezone. Normalize to ISO-like form so\n\t\t// runtimes that reject \"YYYY-MM-DD HH:MM:SS\" can still parse it as\n\t\t// local time. If parsing still fails, return undefined.\n\t\tconst d = new Date(localDate.replace(\" \", \"T\"));\n\t\tif (!isNaN(d.getTime())) return d;\n\t}\n\n\treturn undefined;\n}\n\n// Export for use in other sources\nexport { analyzeWxrData, wxrPostToNormalizedItem };\n\n// Re-export shared utilities that other sources may need\nexport {\n\tBASE_REQUIRED_FIELDS,\n\tFEATURED_IMAGE_FIELD,\n\tmapPostTypeToCollection,\n\tisInternalPostType,\n\tcheckSchemaCompatibility,\n} from \"../utils.js\";\n","/**\n * WordPress REST API probe\n *\n * Probes self-hosted WordPress sites to detect capabilities.\n * This source is probe-only - it tells users what's available\n * and suggests next steps (usually: upload WXR file).\n */\n\nimport { ssrfSafeFetch, validateExternalUrl } from \"../ssrf.js\";\nimport type {\n\tImportSource,\n\tImportAnalysis,\n\tImportContext,\n\tSourceInput,\n\tSourceProbeResult,\n\tFetchOptions,\n\tNormalizedItem,\n} from \"../types.js\";\n\nconst TRAILING_SLASHES = /\\/+$/;\nconst WP_JSON_SUFFIX = /\\/wp-json\\/?$/;\n\n/** WordPress REST API discovery response */\ninterface WpApiDiscovery {\n\tname?: string;\n\tdescription?: string;\n\turl?: string;\n\thome?: string;\n\tgmt_offset?: number;\n\ttimezone_string?: string;\n\tnamespaces?: string[];\n\tauthentication?: Record<string, unknown>;\n\troutes?: Record<string, unknown>;\n}\n\nexport const wordpressRestSource: ImportSource = {\n\tid: \"wordpress-rest\",\n\tname: \"WordPress Site\",\n\tdescription: \"Connect to a self-hosted WordPress site\",\n\ticon: \"globe\",\n\trequiresFile: false,\n\tcanProbe: true,\n\n\tasync probe(url: string): Promise<SourceProbeResult | null> {\n\t\ttry {\n\t\t\tconst siteUrl = normalizeUrl(url);\n\n\t\t\t// SSRF protection: validate URL before any outbound requests\n\t\t\tvalidateExternalUrl(siteUrl);\n\n\t\t\t// Try to fetch the WP REST API root\n\t\t\tconst apiUrl = `${siteUrl}/wp-json/`;\n\t\t\tconst response = await ssrfSafeFetch(apiUrl, {\n\t\t\t\theaders: { Accept: \"application/json\" },\n\t\t\t\tsignal: AbortSignal.timeout(10000),\n\t\t\t});\n\n\t\t\tif (!response.ok) {\n\t\t\t\t// Try alternate location (some sites use different prefix)\n\t\t\t\tconst altResponse = await ssrfSafeFetch(`${siteUrl}/?rest_route=/`, {\n\t\t\t\t\theaders: { Accept: \"application/json\" },\n\t\t\t\t\tsignal: AbortSignal.timeout(10000),\n\t\t\t\t});\n\n\t\t\t\tif (!altResponse.ok) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst data: WpApiDiscovery = await response.json();\n\n\t\t\t// Check if this looks like WordPress\n\t\t\tif (!data.namespaces?.includes(\"wp/v2\")) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// Get content counts (unauthenticated - published only)\n\t\t\tconst preview = await getPublicContentCounts(siteUrl);\n\n\t\t\t// Check for authentication methods\n\t\t\tconst hasAppPasswords = !!data.authentication?.[\"application-passwords\"];\n\n\t\t\treturn {\n\t\t\t\tsourceId: \"wordpress-rest\",\n\t\t\t\tconfidence: \"definite\",\n\t\t\t\tdetected: {\n\t\t\t\t\tplatform: \"wordpress\",\n\t\t\t\t\tsiteTitle: data.name,\n\t\t\t\t\tsiteUrl: data.url || data.home || siteUrl,\n\t\t\t\t},\n\t\t\t\tcapabilities: {\n\t\t\t\t\tpublicContent: true,\n\t\t\t\t\tprivateContent: false, // Would need auth\n\t\t\t\t\tcustomPostTypes: false, // Only if show_in_rest: true\n\t\t\t\t\tallMeta: false, // Only if registered for REST\n\t\t\t\t\tmediaStream: true,\n\t\t\t\t},\n\t\t\t\tauth: hasAppPasswords\n\t\t\t\t\t? {\n\t\t\t\t\t\t\ttype: \"password\",\n\t\t\t\t\t\t\tinstructions:\n\t\t\t\t\t\t\t\t\"To import drafts and private content, create an Application Password in WordPress → Users → Your Profile → Application Passwords\",\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined,\n\t\t\t\tpreview,\n\t\t\t\tsuggestedAction: {\n\t\t\t\t\ttype: \"upload\",\n\t\t\t\t\tinstructions:\n\t\t\t\t\t\t\"For a complete import including drafts, custom post types, and all metadata, export your content from WordPress (Tools → Export) and upload the file here.\",\n\t\t\t\t},\n\t\t\t};\n\t\t} catch {\n\t\t\t// Probe failed - not a WordPress site or not accessible\n\t\t\treturn null;\n\t\t}\n\t},\n\n\tasync analyze(_input: SourceInput, _context: ImportContext): Promise<ImportAnalysis> {\n\t\t// REST-only import not implemented - we use this for probe only\n\t\t// and suggest WXR upload for actual import\n\t\tthrow new Error(\"Direct REST API import not implemented. Please upload a WXR export file.\");\n\t},\n\n\t// eslint-disable-next-line require-yield\n\tasync *fetchContent(_input: SourceInput, _options: FetchOptions): AsyncGenerator<NormalizedItem> {\n\t\tthrow new Error(\"Direct REST API import not implemented. Please upload a WXR export file.\");\n\t},\n};\n\n/**\n * Normalize a URL for API requests\n */\nfunction normalizeUrl(url: string): string {\n\tlet normalized = url.trim();\n\n\t// Add protocol if missing\n\tif (!normalized.startsWith(\"http\")) {\n\t\tnormalized = `https://${normalized}`;\n\t}\n\n\t// Remove trailing slash\n\tnormalized = normalized.replace(TRAILING_SLASHES, \"\");\n\n\t// Remove /wp-json if included\n\tnormalized = normalized.replace(WP_JSON_SUFFIX, \"\");\n\n\treturn normalized;\n}\n\n/**\n * Get public content counts from REST API\n */\nasync function getPublicContentCounts(\n\tsiteUrl: string,\n): Promise<{ posts?: number; pages?: number; media?: number }> {\n\tconst result: { posts?: number; pages?: number; media?: number } = {};\n\n\ttry {\n\t\t// Fetch with per_page=1 to get total from headers\n\t\tconst [postsRes, pagesRes, mediaRes] = await Promise.allSettled([\n\t\t\tssrfSafeFetch(`${siteUrl}/wp-json/wp/v2/posts?per_page=1`, {\n\t\t\t\tsignal: AbortSignal.timeout(5000),\n\t\t\t}),\n\t\t\tssrfSafeFetch(`${siteUrl}/wp-json/wp/v2/pages?per_page=1`, {\n\t\t\t\tsignal: AbortSignal.timeout(5000),\n\t\t\t}),\n\t\t\tssrfSafeFetch(`${siteUrl}/wp-json/wp/v2/media?per_page=1`, {\n\t\t\t\tsignal: AbortSignal.timeout(5000),\n\t\t\t}),\n\t\t]);\n\n\t\tif (postsRes.status === \"fulfilled\" && postsRes.value.ok) {\n\t\t\tconst total = postsRes.value.headers.get(\"X-WP-Total\");\n\t\t\tif (total) result.posts = parseInt(total, 10);\n\t\t}\n\n\t\tif (pagesRes.status === \"fulfilled\" && pagesRes.value.ok) {\n\t\t\tconst total = pagesRes.value.headers.get(\"X-WP-Total\");\n\t\t\tif (total) result.pages = parseInt(total, 10);\n\t\t}\n\n\t\tif (mediaRes.status === \"fulfilled\" && mediaRes.value.ok) {\n\t\t\tconst total = mediaRes.value.headers.get(\"X-WP-Total\");\n\t\t\tif (total) result.media = parseInt(total, 10);\n\t\t}\n\t} catch {\n\t\t// Counts are optional, continue without them\n\t}\n\n\treturn result;\n}\n","/**\n * WordPress Plugin (EmDash Exporter) import source\n *\n * Connects to self-hosted WordPress sites running the EmDash Exporter plugin.\n * Provides full access to all content including drafts, custom post types, and ACF fields.\n */\n\nimport { gutenbergToPortableText } from \"@emdash-cms/gutenberg-to-portable-text\";\n\nimport { encodeBase64 } from \"../../utils/base64.js\";\nimport { ssrfSafeFetch, validateExternalUrl } from \"../ssrf.js\";\nimport type {\n\tImportSource,\n\tImportAnalysis,\n\tImportContext,\n\tSourceInput,\n\tSourceProbeResult,\n\tI18nDetection,\n\tFetchOptions,\n\tNormalizedItem,\n\tPostTypeAnalysis,\n\tAttachmentInfo,\n} from \"../types.js\";\nimport {\n\tBASE_REQUIRED_FIELDS,\n\tFEATURED_IMAGE_FIELD,\n\tmapPostTypeToCollection,\n\tmapWpStatus,\n\tnormalizeUrl,\n\tcheckSchemaCompatibility,\n} from \"../utils.js\";\n\n// =============================================================================\n// API Response Types\n// =============================================================================\n\n/** Detected i18n plugin info from the WordPress site */\ninterface PluginI18nInfo {\n\t/** Which multilingual plugin is active */\n\tplugin: \"wpml\" | \"polylang\";\n\t/** BCP 47 default locale */\n\tdefault_locale: string;\n\t/** All configured locales */\n\tlocales: string[];\n}\n\n/** Probe response from /emdash/v1/probe */\ninterface PluginProbeResponse {\n\temdash_exporter: string;\n\twordpress_version: string;\n\tsite: {\n\t\ttitle: string;\n\t\tdescription: string;\n\t\turl: string;\n\t\thome: string;\n\t\tlanguage: string;\n\t\ttimezone: string;\n\t};\n\tcapabilities: {\n\t\tapplication_passwords: boolean;\n\t\tacf: boolean;\n\t\tyoast: boolean;\n\t\trankmath: boolean;\n\t};\n\tpost_types: Array<{\n\t\tname: string;\n\t\tlabel: string;\n\t\tcount: number;\n\t}>;\n\tmedia_count: number;\n\tendpoints: Record<string, string>;\n\tauth_instructions: {\n\t\tmethod: string;\n\t\tinstructions: string;\n\t\turl?: string;\n\t};\n\t/** Detected multilingual plugin (WPML or Polylang). Absent when neither is active. */\n\ti18n?: PluginI18nInfo;\n}\n\n/** Analyze response from /emdash/v1/analyze */\ninterface PluginAnalyzeResponse {\n\tsite: {\n\t\ttitle: string;\n\t\turl: string;\n\t};\n\tpost_types: Array<{\n\t\tname: string;\n\t\tlabel: string;\n\t\tlabel_singular: string;\n\t\ttotal: number;\n\t\tby_status: Record<string, number>;\n\t\tsupports: Record<string, unknown>;\n\t\ttaxonomies: string[];\n\t\tcustom_fields: Array<{\n\t\t\tkey: string;\n\t\t\tcount: number;\n\t\t\tinferred_type: string;\n\t\t\tsample: string | null;\n\t\t}>;\n\t\thierarchical: boolean;\n\t\thas_archive: boolean;\n\t}>;\n\ttaxonomies: Array<{\n\t\tname: string;\n\t\tlabel: string;\n\t\thierarchical: boolean;\n\t\tterm_count: number;\n\t\tobject_types: string[];\n\t}>;\n\tauthors: Array<{\n\t\tid: number;\n\t\tlogin: string;\n\t\temail: string;\n\t\tdisplay_name: string;\n\t\tpost_count: number;\n\t}>;\n\tattachments: {\n\t\tcount: number;\n\t\tby_type: Record<string, number>;\n\t};\n\tacf?: Array<{\n\t\tkey: string;\n\t\ttitle: string;\n\t\tfields: Array<{\n\t\t\tkey: string;\n\t\t\tname: string;\n\t\t\tlabel: string;\n\t\t\ttype: string;\n\t\t\trequired: boolean;\n\t\t}>;\n\t}>;\n\t/** Detected multilingual plugin (WPML or Polylang). Absent when neither is active. */\n\ti18n?: PluginI18nInfo;\n}\n\n/** Content response from /emdash/v1/content */\ninterface PluginContentResponse {\n\titems: PluginPost[];\n\ttotal: number;\n\tpages: number;\n\tpage: number;\n\tper_page: number;\n}\n\n/** Single post from plugin API */\ninterface PluginPost {\n\tid: number;\n\tpost_type: string;\n\tstatus: string;\n\tslug: string;\n\ttitle: string;\n\tcontent: string;\n\texcerpt: string;\n\tdate: string;\n\tdate_gmt: string;\n\tmodified: string;\n\tmodified_gmt: string;\n\tauthor: {\n\t\tid: number;\n\t\tlogin: string;\n\t\temail: string;\n\t\tdisplay_name: string;\n\t} | null;\n\tparent: number | null;\n\tmenu_order: number;\n\ttaxonomies: Record<string, Array<{ id: number; name: string; slug: string }>>;\n\tfeatured_image?: {\n\t\tid: number;\n\t\turl: string;\n\t\tfilename: string;\n\t\tmime_type: string;\n\t\talt: string;\n\t\ttitle: string;\n\t\tcaption: string;\n\t\twidth: number | null;\n\t\theight: number | null;\n\t};\n\tmeta: Record<string, unknown>;\n\tacf?: Record<string, unknown>;\n\tyoast?: Record<string, string>;\n\trankmath?: Record<string, string>;\n\t/** BCP 47 locale from WPML/Polylang (when detected) */\n\tlocale?: string;\n\t/** Translation group ID from WPML trid or Polylang (when detected) */\n\ttranslation_group?: string;\n}\n\n/** Media response from /emdash/v1/media */\ninterface PluginMediaResponse {\n\titems: PluginMediaItem[];\n\ttotal: number;\n\tpages: number;\n\tpage: number;\n\tper_page: number;\n}\n\ninterface PluginMediaItem {\n\tid: number;\n\turl: string;\n\tfilename: string;\n\tmime_type: string;\n\ttitle: string;\n\talt: string;\n\tcaption: string;\n\tdescription: string;\n\twidth?: number;\n\theight?: number;\n\tfilesize?: number;\n}\n\n// =============================================================================\n// Constants\n// =============================================================================\n\n/** Pattern to remove spaces from application passwords */\nconst SPACE_PATTERN = /\\s/g;\n\n// =============================================================================\n// Import Source\n// =============================================================================\n\nexport const wordpressPluginSource: ImportSource = {\n\tid: \"wordpress-plugin\",\n\tname: \"WordPress (EmDash Exporter)\",\n\tdescription: \"Import from WordPress sites with the EmDash Exporter plugin installed\",\n\ticon: \"plug\",\n\trequiresFile: false,\n\tcanProbe: true,\n\n\tasync probe(url: string): Promise<SourceProbeResult | null> {\n\t\ttry {\n\t\t\tconst siteUrl = normalizeUrl(url);\n\n\t\t\t// SSRF protection: validate URL before any outbound requests\n\t\t\tvalidateExternalUrl(siteUrl);\n\n\t\t\tconst probeUrl = `${siteUrl}/wp-json/emdash/v1/probe`;\n\n\t\t\tconst response = await ssrfSafeFetch(probeUrl, {\n\t\t\t\theaders: { Accept: \"application/json\" },\n\t\t\t\tsignal: AbortSignal.timeout(10000),\n\t\t\t});\n\n\t\t\tif (!response.ok) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst data: PluginProbeResponse = await response.json();\n\n\t\t\t// Verify it's actually our plugin\n\t\t\tif (!data.emdash_exporter) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tsourceId: \"wordpress-plugin\",\n\t\t\t\tconfidence: \"definite\",\n\t\t\t\tdetected: {\n\t\t\t\t\tplatform: \"wordpress\",\n\t\t\t\t\tversion: data.wordpress_version,\n\t\t\t\t\tsiteTitle: data.site.title,\n\t\t\t\t\tsiteUrl: data.site.url,\n\t\t\t\t},\n\t\t\t\tcapabilities: {\n\t\t\t\t\tpublicContent: true,\n\t\t\t\t\tprivateContent: true, // Full access with auth\n\t\t\t\t\tcustomPostTypes: true,\n\t\t\t\t\tallMeta: true,\n\t\t\t\t\tmediaStream: true,\n\t\t\t\t},\n\t\t\t\tauth: data.capabilities.application_passwords\n\t\t\t\t\t? {\n\t\t\t\t\t\t\ttype: \"password\",\n\t\t\t\t\t\t\tinstructions: data.auth_instructions.instructions,\n\t\t\t\t\t\t}\n\t\t\t\t\t: undefined,\n\t\t\t\tpreview: {\n\t\t\t\t\tposts: data.post_types.find((p) => p.name === \"post\")?.count,\n\t\t\t\t\tpages: data.post_types.find((p) => p.name === \"page\")?.count,\n\t\t\t\t\tmedia: data.media_count,\n\t\t\t\t},\n\t\t\t\tsuggestedAction: {\n\t\t\t\t\ttype: \"proceed\",\n\t\t\t\t},\n\t\t\t\ti18n: pluginI18nToDetection(data.i18n),\n\t\t\t};\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t},\n\n\tasync analyze(input: SourceInput, context: ImportContext): Promise<ImportAnalysis> {\n\t\tconst { siteUrl, headers } = getRequestConfig(input);\n\n\t\tconst response = await ssrfSafeFetch(`${siteUrl}/wp-json/emdash/v1/analyze`, {\n\t\t\theaders,\n\t\t\tsignal: AbortSignal.timeout(30000),\n\t\t});\n\n\t\tif (!response.ok) {\n\t\t\tconst error = await response.json().catch(() => ({}));\n\t\t\tthrow new Error(error.message || `Failed to analyze site: ${response.statusText}`);\n\t\t}\n\n\t\tconst data: PluginAnalyzeResponse = await response.json();\n\n\t\t// Get existing collections for schema check\n\t\tconst existingCollections = context.getExistingCollections\n\t\t\t? await context.getExistingCollections()\n\t\t\t: new Map();\n\n\t\t// Build post type analysis\n\t\tconst postTypes: PostTypeAnalysis[] = data.post_types\n\t\t\t.filter((pt) => pt.total > 0)\n\t\t\t.map((pt) => {\n\t\t\t\tconst suggestedCollection = mapPostTypeToCollection(pt.name);\n\t\t\t\tconst existingCollection = existingCollections.get(suggestedCollection);\n\n\t\t\t\t// Include featured_image if post type supports thumbnails\n\t\t\t\tconst supportsThumbnail = pt.supports && \"thumbnail\" in pt.supports;\n\t\t\t\tconst requiredFields = supportsThumbnail\n\t\t\t\t\t? [...BASE_REQUIRED_FIELDS, FEATURED_IMAGE_FIELD]\n\t\t\t\t\t: [...BASE_REQUIRED_FIELDS];\n\n\t\t\t\treturn {\n\t\t\t\t\tname: pt.name,\n\t\t\t\t\tcount: pt.total,\n\t\t\t\t\tsuggestedCollection,\n\t\t\t\t\trequiredFields,\n\t\t\t\t\tschemaStatus: checkSchemaCompatibility(requiredFields, existingCollection),\n\t\t\t\t};\n\t\t\t});\n\n\t\t// Fetch media list for attachment info\n\t\tconst attachments: AttachmentInfo[] = [];\n\t\tif (data.attachments.count > 0) {\n\t\t\ttry {\n\t\t\t\t// Fetch first page of media to populate attachment info\n\t\t\t\tconst mediaResponse = await ssrfSafeFetch(\n\t\t\t\t\t`${siteUrl}/wp-json/emdash/v1/media?per_page=500`,\n\t\t\t\t\t{\n\t\t\t\t\t\theaders,\n\t\t\t\t\t\tsignal: AbortSignal.timeout(30000),\n\t\t\t\t\t},\n\t\t\t\t);\n\t\t\t\tif (mediaResponse.ok) {\n\t\t\t\t\tconst mediaData: PluginMediaResponse = await mediaResponse.json();\n\t\t\t\t\tfor (const item of mediaData.items) {\n\t\t\t\t\t\tattachments.push({\n\t\t\t\t\t\t\tid: item.id,\n\t\t\t\t\t\t\turl: item.url,\n\t\t\t\t\t\t\tfilename: item.filename,\n\t\t\t\t\t\t\tmimeType: item.mime_type,\n\t\t\t\t\t\t\ttitle: item.title,\n\t\t\t\t\t\t\talt: item.alt,\n\t\t\t\t\t\t\tcaption: item.caption,\n\t\t\t\t\t\t\twidth: item.width,\n\t\t\t\t\t\t\theight: item.height,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch (e) {\n\t\t\t\tconsole.warn(\"Failed to fetch media list:\", e);\n\t\t\t}\n\t\t}\n\n\t\t// Count categories and tags\n\t\tconst categoryTaxonomy = data.taxonomies.find((t) => t.name === \"category\");\n\t\tconst tagTaxonomy = data.taxonomies.find((t) => t.name === \"post_tag\");\n\n\t\treturn {\n\t\t\tsourceId: \"wordpress-plugin\",\n\t\t\tsite: {\n\t\t\t\ttitle: data.site.title,\n\t\t\t\turl: data.site.url,\n\t\t\t},\n\t\t\tpostTypes,\n\t\t\tattachments: {\n\t\t\t\tcount: data.attachments.count,\n\t\t\t\titems: attachments,\n\t\t\t},\n\t\t\tcategories: categoryTaxonomy?.term_count ?? 0,\n\t\t\ttags: tagTaxonomy?.term_count ?? 0,\n\t\t\tauthors: data.authors.map((a) => ({\n\t\t\t\tid: a.id,\n\t\t\t\tlogin: a.login,\n\t\t\t\temail: a.email,\n\t\t\t\tdisplayName: a.display_name,\n\t\t\t\tpostCount: a.post_count,\n\t\t\t})),\n\t\t\ti18n: pluginI18nToDetection(data.i18n),\n\t\t};\n\t},\n\n\tasync *fetchContent(input: SourceInput, options: FetchOptions): AsyncGenerator<NormalizedItem> {\n\t\tconst { siteUrl, headers } = getRequestConfig(input);\n\n\t\tfor (const postType of options.postTypes) {\n\t\t\tlet page = 1;\n\t\t\tlet totalPages = 1;\n\t\t\tlet yielded = 0;\n\n\t\t\twhile (page <= totalPages) {\n\t\t\t\tconst status = options.includeDrafts ? \"any\" : \"publish\";\n\t\t\t\tconst url = `${siteUrl}/wp-json/emdash/v1/content?post_type=${postType}&status=${status}&per_page=100&page=${page}`;\n\n\t\t\t\tconst response = await ssrfSafeFetch(url, {\n\t\t\t\t\theaders,\n\t\t\t\t\tsignal: AbortSignal.timeout(60000),\n\t\t\t\t});\n\n\t\t\t\tif (!response.ok) {\n\t\t\t\t\tthrow new Error(`Failed to fetch ${postType}: ${response.statusText}`);\n\t\t\t\t}\n\n\t\t\t\tconst data: PluginContentResponse = await response.json();\n\t\t\t\ttotalPages = data.pages;\n\n\t\t\t\tfor (const post of data.items) {\n\t\t\t\t\tyield pluginPostToNormalizedItem(post);\n\t\t\t\t\tyielded++;\n\n\t\t\t\t\tif (options.limit && yielded >= options.limit) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tpage++;\n\t\t\t}\n\t\t}\n\t},\n\n\tasync fetchMedia(url: string, _input: SourceInput): Promise<Blob> {\n\t\t// SSRF protection: validate media URL before fetching\n\t\tvalidateExternalUrl(url);\n\n\t\t// Media URLs are publicly accessible on WP (ssrfSafeFetch validates redirects)\n\t\tconst response = await ssrfSafeFetch(url);\n\t\tif (!response.ok) {\n\t\t\tthrow new Error(`Failed to fetch media: ${response.statusText}`);\n\t\t}\n\t\treturn response.blob();\n\t},\n};\n\n// =============================================================================\n// Helper Functions\n// =============================================================================\n\n/**\n * Convert plugin i18n info to the shared I18nDetection type.\n * Returns undefined when no multilingual plugin is detected.\n */\nfunction pluginI18nToDetection(i18n: PluginI18nInfo | undefined): I18nDetection | undefined {\n\tif (!i18n) return undefined;\n\treturn {\n\t\tplugin: i18n.plugin,\n\t\tdefaultLocale: i18n.default_locale,\n\t\tlocales: i18n.locales,\n\t};\n}\n\n/**\n * Get request configuration from input\n */\nfunction getRequestConfig(input: SourceInput): {\n\tsiteUrl: string;\n\theaders: HeadersInit;\n} {\n\tif (input.type === \"url\") {\n\t\tconst siteUrl = normalizeUrl(input.url);\n\n\t\t// SSRF protection: validate URL before any outbound requests\n\t\tvalidateExternalUrl(siteUrl);\n\t\tconst headers: HeadersInit = {\n\t\t\tAccept: \"application/json\",\n\t\t};\n\n\t\tif (input.token) {\n\t\t\t// Token format: \"username:password\" base64 encoded\n\t\t\theaders[\"Authorization\"] = `Basic ${input.token}`;\n\t\t}\n\n\t\treturn { siteUrl, headers };\n\t}\n\n\tif (input.type === \"oauth\") {\n\t\tconst oauthSiteUrl = normalizeUrl(input.url);\n\n\t\t// SSRF protection: validate URL before any outbound requests\n\t\tvalidateExternalUrl(oauthSiteUrl);\n\n\t\treturn {\n\t\t\tsiteUrl: oauthSiteUrl,\n\t\t\theaders: {\n\t\t\t\tAccept: \"application/json\",\n\t\t\t\tAuthorization: `Bearer ${input.accessToken}`,\n\t\t\t},\n\t\t};\n\t}\n\n\tthrow new Error(\"WordPress plugin source requires URL or OAuth input\");\n}\n\n/**\n * Convert plugin post to normalized item\n */\nfunction pluginPostToNormalizedItem(post: PluginPost): NormalizedItem {\n\tconst content = post.content ? gutenbergToPortableText(post.content) : [];\n\n\t// Extract categories and tags from taxonomies\n\tconst categories =\n\t\tpost.taxonomies?.category?.map((c) => c.slug) ??\n\t\tpost.taxonomies?.categories?.map((c) => c.slug) ??\n\t\t[];\n\tconst tags =\n\t\tpost.taxonomies?.post_tag?.map((t) => t.slug) ??\n\t\tpost.taxonomies?.tags?.map((t) => t.slug) ??\n\t\t[];\n\n\t// Build meta from various sources\n\tconst meta: Record<string, unknown> = { ...post.meta };\n\n\t// Include ACF fields in meta\n\tif (post.acf) {\n\t\tmeta._acf = post.acf;\n\t}\n\n\t// Include SEO data in meta\n\tif (post.yoast) {\n\t\tmeta._yoast = post.yoast;\n\t}\n\tif (post.rankmath) {\n\t\tmeta._rankmath = post.rankmath;\n\t}\n\n\treturn {\n\t\tsourceId: post.id,\n\t\tpostType: post.post_type,\n\t\tstatus: mapWpStatus(post.status),\n\t\tslug: post.slug,\n\t\ttitle: post.title,\n\t\tcontent,\n\t\texcerpt: post.excerpt || undefined,\n\t\tdate: new Date(post.date_gmt || post.date),\n\t\tmodified: post.modified_gmt ? new Date(post.modified_gmt) : new Date(post.modified),\n\t\tauthor: post.author?.login,\n\t\tcategories,\n\t\ttags,\n\t\tmeta,\n\t\tfeaturedImage: post.featured_image?.url,\n\t\tlocale: post.locale,\n\t\ttranslationGroup: post.translation_group,\n\t};\n}\n\n// =============================================================================\n// Utility Functions for External Use\n// =============================================================================\n\n/**\n * Create a Basic Auth token from username and password\n */\nexport function createBasicAuthToken(username: string, password: string): string {\n\t// Remove spaces from application password (WP formats them with spaces)\n\tconst cleanPassword = password.replace(SPACE_PATTERN, \"\");\n\treturn encodeBase64(`${username}:${cleanPassword}`);\n}\n\n/**\n * Fetch media list from plugin API\n */\nexport async function fetchPluginMedia(\n\tsiteUrl: string,\n\tauthToken: string,\n\tpage = 1,\n\tperPage = 100,\n): Promise<PluginMediaResponse> {\n\tconst normalizedSiteUrl = normalizeUrl(siteUrl);\n\n\t// SSRF protection: validate URL before any outbound requests\n\tvalidateExternalUrl(normalizedSiteUrl);\n\n\tconst url = `${normalizedSiteUrl}/wp-json/emdash/v1/media?per_page=${perPage}&page=${page}`;\n\n\tconst response = await ssrfSafeFetch(url, {\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Basic ${authToken}`,\n\t\t},\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to fetch media: ${response.statusText}`);\n\t}\n\n\treturn response.json();\n}\n\n/**\n * Fetch taxonomies from plugin API\n */\nexport async function fetchPluginTaxonomies(\n\tsiteUrl: string,\n\tauthToken: string,\n): Promise<\n\tArray<{\n\t\tname: string;\n\t\tlabel: string;\n\t\thierarchical: boolean;\n\t\tterms: Array<{\n\t\t\tid: number;\n\t\t\tname: string;\n\t\t\tslug: string;\n\t\t\tdescription: string;\n\t\t\tparent: number | null;\n\t\t\tcount: number;\n\t\t}>;\n\t}>\n> {\n\tconst normalizedSiteUrl = normalizeUrl(siteUrl);\n\n\t// SSRF protection: validate URL before any outbound requests\n\tvalidateExternalUrl(normalizedSiteUrl);\n\n\tconst url = `${normalizedSiteUrl}/wp-json/emdash/v1/taxonomies`;\n\n\tconst response = await ssrfSafeFetch(url, {\n\t\theaders: {\n\t\t\tAccept: \"application/json\",\n\t\t\tAuthorization: `Basic ${authToken}`,\n\t\t},\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Failed to fetch taxonomies: ${response.statusText}`);\n\t}\n\n\treturn response.json();\n}\n","/**\n * Import system\n *\n * Provides a pluggable system for importing content from various sources.\n */\n\n// Core types\nexport type {\n\tImportSource,\n\tImportAnalysis,\n\tImportContext,\n\tSourceInput,\n\tFileInput,\n\tUrlInput,\n\tOAuthInput,\n\tSourceProbeResult,\n\tProbeResult,\n\tSourceAuth,\n\tSourceCapabilities,\n\tSuggestedAction,\n\tPostTypeAnalysis,\n\tImportFieldDef,\n\tFieldCompatibility,\n\tCollectionSchemaStatus,\n\tAttachmentInfo,\n\tNormalizedItem,\n\tImportConfig,\n\tImportResult,\n\tFetchOptions,\n\tPostTypeMapping,\n\tNavMenuAnalysis,\n\tTaxonomyAnalysis,\n} from \"./types.js\";\n\n// Menu import\nexport {\n\timportMenusFromWxr,\n\timportMenusFromPlugin,\n\ttype MenuImportResult,\n\ttype PluginMenu,\n\ttype PluginMenuItem,\n} from \"./menus.js\";\n\n// Sections import\nexport { importReusableBlocksAsSections, type SectionsImportResult } from \"./sections.js\";\n\n// Site settings import\nexport {\n\timportSiteSettings,\n\tparseSiteSettingsFromPlugin,\n\ttype SiteSettingsAnalysis,\n\ttype SettingsImportResult,\n\ttype WidgetAreaAnalysis,\n} from \"./settings.js\";\n\n// Registry\nexport {\n\tregisterSource,\n\tgetSource,\n\tgetAllSources,\n\tgetFileSources,\n\tgetUrlSources,\n\tprobeUrl,\n\tclearSources,\n} from \"./registry.js\";\n\n// SSRF protection\nexport { validateExternalUrl, ssrfSafeFetch, SsrfError } from \"./ssrf.js\";\n\n// Sources\nexport { wxrSource, parseWxrDate } from \"./sources/wxr.js\";\nexport { wordpressRestSource } from \"./sources/wordpress-rest.js\";\nexport {\n\twordpressPluginSource,\n\tcreateBasicAuthToken,\n\tfetchPluginMedia,\n\tfetchPluginTaxonomies,\n} from \"./sources/wordpress-plugin.js\";\n\n// Auto-register built-in sources\nimport { registerSource } from \"./registry.js\";\nimport { wordpressPluginSource } from \"./sources/wordpress-plugin.js\";\nimport { wordpressRestSource } from \"./sources/wordpress-rest.js\";\nimport { wxrSource } from \"./sources/wxr.js\";\n\n// Register in priority order (most specific first)\n// Plugin source first - if they have our plugin, use it\nregisterSource(wordpressPluginSource);\nregisterSource(wordpressRestSource);\nregisterSource(wxrSource);\n","/**\n * Preview URL generation\n *\n * Creates preview URLs that include a signed token for accessing draft content.\n */\n\nimport { generatePreviewToken } from \"./tokens.js\";\n\n/**\n * Options for generating a preview URL\n */\nexport interface GetPreviewUrlOptions {\n\t/** Collection slug (e.g., \"posts\") */\n\tcollection: string;\n\t/** Content ID or slug */\n\tid: string;\n\t/** Secret key for signing the token */\n\tsecret: string;\n\t/** How long the preview URL is valid. Default: \"1h\" */\n\texpiresIn?: string | number;\n\t/** Base URL of the site. If not provided, returns a relative URL. */\n\tbaseUrl?: string;\n\t/** Custom path pattern. Use {collection} and {id} as placeholders. Default: \"/{collection}/{id}\" */\n\tpathPattern?: string;\n}\n\n/**\n * Generate a preview URL for content\n *\n * The URL includes a `_preview` query parameter with a signed token.\n *\n * @example\n * ```ts\n * const url = await getPreviewUrl({\n * collection: \"posts\",\n * id: \"hello-world\",\n * secret: process.env.PREVIEW_SECRET!,\n * });\n * // Returns: /posts/hello-world?_preview=eyJj...\n *\n * // With base URL:\n * const fullUrl = await getPreviewUrl({\n * collection: \"posts\",\n * id: \"hello-world\",\n * secret: process.env.PREVIEW_SECRET!,\n * baseUrl: \"https://example.com\",\n * });\n * // Returns: https://example.com/posts/hello-world?_preview=eyJj...\n *\n * // Custom path pattern:\n * const customUrl = await getPreviewUrl({\n * collection: \"posts\",\n * id: \"hello-world\",\n * secret: process.env.PREVIEW_SECRET!,\n * pathPattern: \"/blog/{id}\",\n * });\n * // Returns: /blog/hello-world?_preview=eyJj...\n * ```\n */\nexport async function getPreviewUrl(options: GetPreviewUrlOptions): Promise<string> {\n\tconst {\n\t\tcollection,\n\t\tid,\n\t\tsecret,\n\t\texpiresIn = \"1h\",\n\t\tbaseUrl,\n\t\tpathPattern = \"/{collection}/{id}\",\n\t} = options;\n\n\t// Generate the signed token\n\tconst token = await generatePreviewToken({\n\t\tcontentId: `${collection}:${id}`,\n\t\texpiresIn,\n\t\tsecret,\n\t});\n\n\t// Build the path\n\tconst path = pathPattern.replace(\"{collection}\", collection).replace(\"{id}\", id);\n\n\t// Add token as query parameter\n\tconst url = new URL(path, baseUrl || \"http://placeholder\");\n\turl.searchParams.set(\"_preview\", token);\n\n\t// Return relative URL if no baseUrl provided\n\tif (!baseUrl) {\n\t\treturn `${url.pathname}${url.search}`;\n\t}\n\n\treturn url.toString();\n}\n\n/**\n * Build a preview URL from a token (when you already have the token)\n *\n * @example\n * ```ts\n * const url = buildPreviewUrl({\n * path: \"/posts/hello-world\",\n * token: existingToken,\n * });\n * ```\n */\nexport function buildPreviewUrl(options: {\n\tpath: string;\n\ttoken: string;\n\tbaseUrl?: string;\n}): string {\n\tconst { path, token, baseUrl } = options;\n\n\tconst url = new URL(path, baseUrl || \"http://placeholder\");\n\turl.searchParams.set(\"_preview\", token);\n\n\tif (!baseUrl) {\n\t\treturn `${url.pathname}${url.search}`;\n\t}\n\n\treturn url.toString();\n}\n","/**\n * Preview helpers for Astro pages\n */\n\n/**\n * Check if a request is a preview request\n *\n * @example\n * ```ts\n * const isPreview = isPreviewRequest(Astro.url);\n * ```\n */\nexport function isPreviewRequest(url: URL): boolean {\n\treturn url.searchParams.has(\"_preview\");\n}\n\n/**\n * Get the preview token from a URL\n *\n * @example\n * ```ts\n * const token = getPreviewToken(Astro.url);\n * ```\n */\nexport function getPreviewToken(url: URL): string | null {\n\treturn url.searchParams.get(\"_preview\");\n}\n","/**\n * Comment query functions for Astro templates\n *\n * Same pattern as getMenu() — uses getDb() for ambient DB access.\n * These are called from .astro pages/components, not from API routes.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { CommentRepository } from \"../database/repositories/comment.js\";\nimport type { PublicComment } from \"../database/repositories/comment.js\";\nimport type { Database } from \"../database/types.js\";\nimport { getDb } from \"../loader.js\";\n\nexport interface GetCommentsOptions {\n\tcollection: string;\n\tcontentId: string;\n\tthreaded?: boolean;\n}\n\nexport interface GetCommentsResult {\n\titems: PublicComment[];\n\ttotal: number;\n}\n\n/**\n * Get approved comments for a content item.\n *\n * @example\n * ```ts\n * import { getComments } from \"emdash\";\n *\n * const { items, total } = await getComments({\n * collection: \"posts\",\n * contentId: post.id,\n * threaded: true,\n * });\n * ```\n */\nexport async function getComments(options: GetCommentsOptions): Promise<GetCommentsResult> {\n\tconst db = await getDb();\n\treturn getCommentsWithDb(db, options);\n}\n\n/**\n * Get approved comments with an explicit db handle.\n *\n * @internal Use `getComments()` in templates. This variant is for routes\n * that already have a database handle.\n */\nexport async function getCommentsWithDb(\n\tdb: Kysely<Database>,\n\toptions: GetCommentsOptions,\n): Promise<GetCommentsResult> {\n\tconst repo = new CommentRepository(db);\n\n\tconst total = await repo.countByContent(options.collection, options.contentId, \"approved\");\n\n\t// Server-rendered: fetch all comments (capped for safety).\n\t// The API route handles paginated access; this is for full-page renders.\n\tconst MAX_COMMENTS = 500;\n\n\tconst result = await repo.findByContent(options.collection, options.contentId, {\n\t\tstatus: \"approved\",\n\t\tlimit: MAX_COMMENTS,\n\t});\n\n\tif (options.threaded) {\n\t\tconst threaded = CommentRepository.assembleThreads(result.items);\n\t\tconst items = threaded.map((c) => CommentRepository.toPublicComment(c));\n\t\treturn { items, total };\n\t}\n\n\tconst items = result.items.map((c) => CommentRepository.toPublicComment(c));\n\treturn { items, total };\n}\n\n/**\n * Get the count of approved comments for a content item.\n *\n * @example\n * ```ts\n * import { getCommentCount } from \"emdash\";\n *\n * const count = await getCommentCount(\"posts\", post.id);\n * ```\n */\nexport async function getCommentCount(collection: string, contentId: string): Promise<number> {\n\tconst db = await getDb();\n\treturn getCommentCountWithDb(db, collection, contentId);\n}\n\n/**\n * Get comment count with an explicit db handle.\n *\n * @internal Use `getCommentCount()` in templates.\n */\nexport async function getCommentCountWithDb(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tcontentId: string,\n): Promise<number> {\n\tconst repo = new CommentRepository(db);\n\treturn repo.countByContent(collection, contentId, \"approved\");\n}\n","/**\n * Navigation menu runtime functions\n *\n * These are called from templates to query menus and resolve URLs.\n */\n\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\n\nimport type { Database } from \"../database/types.js\";\nimport { validateIdentifier } from \"../database/validate.js\";\nimport { getDb } from \"../loader.js\";\nimport { sanitizeHref } from \"../utils/url.js\";\nimport type { Menu, MenuItem, MenuItemRow } from \"./types.js\";\n\n/**\n * Get menu by name with resolved URLs\n *\n * @example\n * ```ts\n * import { getMenu } from \"emdash\";\n *\n * const menu = await getMenu(\"primary\");\n * if (menu) {\n * console.log(menu.items); // Array of MenuItem with resolved URLs\n * }\n * ```\n */\nexport async function getMenu(name: string): Promise<Menu | null> {\n\tconst db = await getDb();\n\treturn getMenuWithDb(name, db);\n}\n\n/**\n * Get menu by name with resolved URLs (with explicit db)\n *\n * @internal Use `getMenu()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getMenuWithDb(name: string, db: Kysely<Database>): Promise<Menu | null> {\n\t// Get menu\n\tconst menuRow = await db\n\t\t.selectFrom(\"_emdash_menus\")\n\t\t.selectAll()\n\t\t.where(\"name\", \"=\", name)\n\t\t.executeTakeFirst();\n\n\tif (!menuRow) {\n\t\treturn null;\n\t}\n\n\t// Get all menu items\n\tconst itemRows = await db\n\t\t.selectFrom(\"_emdash_menu_items\")\n\t\t.selectAll()\n\t\t.$castTo<MenuItemRow>()\n\t\t.where(\"menu_id\", \"=\", menuRow.id)\n\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t.execute();\n\n\t// Resolve URLs and build tree\n\tconst items = await buildMenuTree(itemRows, db);\n\n\treturn {\n\t\tid: menuRow.id,\n\t\tname: menuRow.name,\n\t\tlabel: menuRow.label,\n\t\titems,\n\t};\n}\n\n/**\n * Get all menus (without items - for admin list)\n *\n * @example\n * ```ts\n * import { getMenus } from \"emdash\";\n *\n * const menus = await getMenus();\n * console.log(menus); // [{ id, name, label }]\n * ```\n */\nexport async function getMenus(): Promise<Array<{ id: string; name: string; label: string }>> {\n\tconst db = await getDb();\n\treturn getMenusWithDb(db);\n}\n\n/**\n * Get all menus (with explicit db)\n *\n * @internal Use `getMenus()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getMenusWithDb(\n\tdb: Kysely<Database>,\n): Promise<Array<{ id: string; name: string; label: string }>> {\n\tconst rows = await db\n\t\t.selectFrom(\"_emdash_menus\")\n\t\t.select([\"id\", \"name\", \"label\"])\n\t\t.orderBy(\"name\", \"asc\")\n\t\t.execute();\n\n\treturn rows;\n}\n\n/**\n * Build hierarchical menu tree from flat array of items\n */\nasync function buildMenuTree(items: MenuItemRow[], db: Kysely<Database>): Promise<MenuItem[]> {\n\t// Pre-load URL patterns for all collections referenced in this menu\n\tconst collectionSlugs = new Set<string>();\n\tfor (const item of items) {\n\t\tif (item.reference_collection) {\n\t\t\tcollectionSlugs.add(item.reference_collection);\n\t\t}\n\t\tif (item.type === \"page\" || item.type === \"post\") {\n\t\t\tcollectionSlugs.add(item.reference_collection || `${item.type}s`);\n\t\t}\n\t}\n\n\tconst urlPatterns = new Map<string, string | null>();\n\tif (collectionSlugs.size > 0) {\n\t\tconst rows = await db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.select([\"slug\", \"url_pattern\"])\n\t\t\t.where(\"slug\", \"in\", [...collectionSlugs])\n\t\t\t.execute();\n\t\tfor (const row of rows) {\n\t\t\turlPatterns.set(row.slug, row.url_pattern);\n\t\t}\n\t}\n\n\t// Resolve all URLs first\n\tconst resolvedItems = await Promise.all(\n\t\titems.map((item) => resolveMenuItem(item, db, urlPatterns)),\n\t);\n\n\t// Filter out items that couldn't be resolved (e.g., deleted content)\n\tconst validItems = resolvedItems.filter((item) => item !== null);\n\n\t// Build tree structure\n\tconst itemMap = new Map<string, MenuItem & { children: MenuItem[] }>();\n\tconst rootItems: MenuItem[] = [];\n\n\t// First pass: create all items\n\tfor (const item of validItems) {\n\t\titemMap.set(item.id, { ...item, children: [] });\n\t}\n\n\t// Second pass: build parent-child relationships\n\tfor (const item of items) {\n\t\tconst menuItem = itemMap.get(item.id);\n\t\tif (!menuItem) continue;\n\n\t\tif (item.parent_id) {\n\t\t\tconst parent = itemMap.get(item.parent_id);\n\t\t\tif (parent) {\n\t\t\t\tparent.children.push(menuItem);\n\t\t\t} else {\n\t\t\t\t// Parent not found, treat as root\n\t\t\t\trootItems.push(menuItem);\n\t\t\t}\n\t\t} else {\n\t\t\trootItems.push(menuItem);\n\t\t}\n\t}\n\n\treturn rootItems;\n}\n\n/**\n * Resolve a single menu item's URL\n *\n * Returns null if the referenced content no longer exists (item should be skipped)\n */\nasync function resolveMenuItem(\n\titem: MenuItemRow,\n\tdb: Kysely<Database>,\n\turlPatterns: Map<string, string | null>,\n): Promise<MenuItem | null> {\n\tlet url: string | null;\n\n\ttry {\n\t\tswitch (item.type) {\n\t\t\tcase \"custom\":\n\t\t\t\turl = item.custom_url || \"#\";\n\t\t\t\tbreak;\n\n\t\t\tcase \"page\":\n\t\t\tcase \"post\":\n\t\t\t\turl = await resolveContentUrl(\n\t\t\t\t\t// Default to plural collection name (pages/posts) if not specified\n\t\t\t\t\titem.reference_collection || `${item.type}s`,\n\t\t\t\t\titem.reference_id,\n\t\t\t\t\tdb,\n\t\t\t\t\turlPatterns,\n\t\t\t\t);\n\t\t\t\t// Skip items where content no longer exists\n\t\t\t\tif (url === null) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"taxonomy\":\n\t\t\t\turl = await resolveTaxonomyUrl(item.reference_id, db);\n\t\t\t\t// Skip items where taxonomy no longer exists\n\t\t\t\tif (url === null) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"collection\":\n\t\t\t\turl = `/${item.reference_collection}/`;\n\t\t\t\tbreak;\n\n\t\t\tdefault:\n\t\t\t\tif (item.reference_collection && item.reference_id) {\n\t\t\t\t\turl = await resolveContentUrl(\n\t\t\t\t\t\titem.reference_collection,\n\t\t\t\t\t\titem.reference_id,\n\t\t\t\t\t\tdb,\n\t\t\t\t\t\turlPatterns,\n\t\t\t\t\t);\n\t\t\t\t\tif (url === null) {\n\t\t\t\t\t\treturn null;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\turl = \"#\";\n\t\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\t// If resolution fails, skip this item\n\t\tconsole.error(`Failed to resolve menu item ${item.id}:`, error);\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tid: item.id,\n\t\tlabel: item.label,\n\t\turl: sanitizeHref(url),\n\t\ttarget: item.target || undefined,\n\t\ttitleAttr: item.title_attr || undefined,\n\t\tcssClasses: item.css_classes || undefined,\n\t\tchildren: [], // Will be populated by buildMenuTree\n\t};\n}\n\nconst SLUG_PLACEHOLDER = /\\{slug\\}/g;\nconst ID_PLACEHOLDER = /\\{id\\}/g;\n\n/**\n * Interpolate a URL pattern with entry data\n *\n * Replaces `{slug}` and `{id}` placeholders.\n */\nfunction interpolateUrlPattern(pattern: string, slug: string, id: string): string {\n\treturn pattern.replace(SLUG_PLACEHOLDER, slug).replace(ID_PLACEHOLDER, id);\n}\n\n/**\n * Resolve URL for a content entry (page/post)\n *\n * Uses the collection's url_pattern if set, otherwise falls back to /{collection}/{slug}.\n * Returns null if content not found (item should be skipped).\n */\nasync function resolveContentUrl(\n\tcollection: string,\n\tentryId: string | null,\n\tdb: Kysely<Database>,\n\turlPatterns: Map<string, string | null>,\n): Promise<string | null> {\n\tif (!entryId) {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\t// Validate collection name before interpolating into table reference\n\t\tvalidateIdentifier(collection, \"menu item collection\");\n\n\t\t// Dynamic content tables (ec_*) aren't in the Database type, so use sql\n\t\tconst result = await sql<{ slug: string }>`\n\t\t\tSELECT slug FROM ${sql.ref(`ec_${collection}`)} WHERE id = ${entryId} LIMIT 1\n\t\t`.execute(db);\n\n\t\tconst row = result.rows[0];\n\t\tif (row) {\n\t\t\tconst pattern = urlPatterns.get(collection);\n\t\t\tif (pattern) {\n\t\t\t\treturn interpolateUrlPattern(pattern, row.slug, entryId);\n\t\t\t}\n\t\t\treturn `/${collection}/${row.slug}`;\n\t\t}\n\n\t\t// Content not found, skip item\n\t\treturn null;\n\t} catch (error) {\n\t\t// Table might not exist or query failed\n\t\tconsole.error(`Failed to resolve content URL for ${collection}/${entryId}:`, error);\n\t\treturn null;\n\t}\n}\n\n/**\n * Resolve URL for a taxonomy term\n *\n * Returns null if taxonomy not found (item should be skipped)\n */\nasync function resolveTaxonomyUrl(\n\ttaxonomyId: string | null,\n\tdb: Kysely<Database>,\n): Promise<string | null> {\n\tif (!taxonomyId) {\n\t\treturn null;\n\t}\n\n\tconst taxonomy = await db\n\t\t.selectFrom(\"taxonomies\")\n\t\t.select([\"name\", \"slug\"])\n\t\t.where(\"id\", \"=\", taxonomyId)\n\t\t.executeTakeFirst();\n\n\tif (!taxonomy) {\n\t\t// Taxonomy not found, skip item\n\t\treturn null;\n\t}\n\n\t// Use taxonomy name as base (e.g., \"categories\" or \"tags\")\n\treturn `/${taxonomy.name}/${taxonomy.slug}`;\n}\n","/**\n * Runtime API for taxonomies\n *\n * Provides functions to query taxonomy definitions and terms.\n */\n\nimport { getDb } from \"../loader.js\";\nimport type { TaxonomyDef, TaxonomyTerm, TaxonomyTermRow } from \"./types.js\";\n\n/**\n * Get all taxonomy definitions\n */\nexport async function getTaxonomyDefs(): Promise<TaxonomyDef[]> {\n\tconst db = await getDb();\n\n\tconst rows = await db.selectFrom(\"_emdash_taxonomy_defs\").selectAll().execute();\n\n\treturn rows.map((row) => ({\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}));\n}\n\n/**\n * Get a single taxonomy definition by name\n */\nexport async function getTaxonomyDef(name: string): Promise<TaxonomyDef | null> {\n\tconst db = await getDb();\n\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t.selectAll()\n\t\t.where(\"name\", \"=\", name)\n\t\t.executeTakeFirst();\n\n\tif (!row) return null;\n\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};\n}\n\n/**\n * Get all terms for a taxonomy (as tree for hierarchical, flat for tags)\n */\nexport async function getTaxonomyTerms(taxonomyName: string): Promise<TaxonomyTerm[]> {\n\tconst db = await getDb();\n\n\t// Get taxonomy definition to check if hierarchical\n\tconst def = await getTaxonomyDef(taxonomyName);\n\tif (!def) return [];\n\n\t// Get all terms for this taxonomy\n\tconst rows = await db\n\t\t.selectFrom(\"taxonomies\")\n\t\t.selectAll()\n\t\t.where(\"name\", \"=\", taxonomyName)\n\t\t.orderBy(\"label\", \"asc\")\n\t\t.execute();\n\n\t// Count entries for each term\n\tconst countsResult = await db\n\t\t.selectFrom(\"content_taxonomies\")\n\t\t.select([\"taxonomy_id\"])\n\t\t.select((eb) => eb.fn.count<number>(\"entry_id\").as(\"count\"))\n\t\t.groupBy(\"taxonomy_id\")\n\t\t.execute();\n\n\tconst counts = new Map<string, number>();\n\tfor (const row of countsResult) {\n\t\tcounts.set(row.taxonomy_id, row.count);\n\t}\n\n\tconst flatTerms: TaxonomyTermRow[] = rows.map((row) => ({\n\t\tid: row.id,\n\t\tname: row.name,\n\t\tslug: row.slug,\n\t\tlabel: row.label,\n\t\tparent_id: row.parent_id,\n\t\tdata: row.data,\n\t}));\n\n\t// If hierarchical, build tree. Otherwise return flat\n\tif (def.hierarchical) {\n\t\treturn buildTree(flatTerms, counts);\n\t}\n\n\treturn flatTerms.map((term) => ({\n\t\tid: term.id,\n\t\tname: term.name,\n\t\tslug: term.slug,\n\t\tlabel: term.label,\n\t\tchildren: [],\n\t\tcount: counts.get(term.id) ?? 0,\n\t}));\n}\n\n/**\n * Get a single term by taxonomy and slug\n */\nexport async function getTerm(taxonomyName: string, slug: string): Promise<TaxonomyTerm | null> {\n\tconst db = await getDb();\n\n\tconst row = await db\n\t\t.selectFrom(\"taxonomies\")\n\t\t.selectAll()\n\t\t.where(\"name\", \"=\", taxonomyName)\n\t\t.where(\"slug\", \"=\", slug)\n\t\t.executeTakeFirst();\n\n\tif (!row) return null;\n\n\t// Get entry count\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.id)\n\t\t.executeTakeFirst();\n\n\tconst count = countResult?.count ?? 0;\n\n\t// Get children if hierarchical\n\tconst childRows = await 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\t\t.execute();\n\n\tconst children = childRows.map((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}));\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};\n}\n\n/**\n * Get terms assigned to an entry\n */\nexport async function getEntryTerms(\n\tcollection: string,\n\tentryId: string,\n\ttaxonomyName?: string,\n): Promise<TaxonomyTerm[]> {\n\tconst db = await getDb();\n\n\tlet query = db\n\t\t.selectFrom(\"content_taxonomies\")\n\t\t.innerJoin(\"taxonomies\", \"taxonomies.id\", \"content_taxonomies.taxonomy_id\")\n\t\t.selectAll(\"taxonomies\")\n\t\t.where(\"content_taxonomies.collection\", \"=\", collection)\n\t\t.where(\"content_taxonomies.entry_id\", \"=\", entryId);\n\n\tif (taxonomyName) {\n\t\tquery = query.where(\"taxonomies.name\", \"=\", taxonomyName);\n\t}\n\n\tconst rows = await query.execute();\n\n\treturn rows.map((row) => ({\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\tchildren: [],\n\t}));\n}\n\n/**\n * Get terms for multiple entries in a single query (batched API)\n *\n * This is more efficient than calling getEntryTerms for each entry\n * when you need terms for a list of entries.\n *\n * @param collection - The collection type (e.g., \"posts\")\n * @param entryIds - Array of entry IDs\n * @param taxonomyName - The taxonomy name (e.g., \"categories\")\n * @returns Map from entry ID to array of terms\n */\nexport async function getTermsForEntries(\n\tcollection: string,\n\tentryIds: string[],\n\ttaxonomyName: string,\n): Promise<Map<string, TaxonomyTerm[]>> {\n\tconst result = new Map<string, TaxonomyTerm[]>();\n\n\t// Initialize all entry IDs with empty arrays\n\tfor (const id of entryIds) {\n\t\tresult.set(id, []);\n\t}\n\n\tif (entryIds.length === 0) {\n\t\treturn result;\n\t}\n\n\tconst db = await getDb();\n\n\tconst rows = await db\n\t\t.selectFrom(\"content_taxonomies\")\n\t\t.innerJoin(\"taxonomies\", \"taxonomies.id\", \"content_taxonomies.taxonomy_id\")\n\t\t.select([\n\t\t\t\"content_taxonomies.entry_id\",\n\t\t\t\"taxonomies.id\",\n\t\t\t\"taxonomies.name\",\n\t\t\t\"taxonomies.slug\",\n\t\t\t\"taxonomies.label\",\n\t\t\t\"taxonomies.parent_id\",\n\t\t])\n\t\t.where(\"content_taxonomies.collection\", \"=\", collection)\n\t\t.where(\"content_taxonomies.entry_id\", \"in\", entryIds)\n\t\t.where(\"taxonomies.name\", \"=\", taxonomyName)\n\t\t.execute();\n\n\tfor (const row of rows) {\n\t\tconst entryId = row.entry_id;\n\t\tconst term: TaxonomyTerm = {\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\tparentId: row.parent_id ?? undefined,\n\t\t\tchildren: [],\n\t\t};\n\n\t\tconst terms = result.get(entryId);\n\t\tif (terms) {\n\t\t\tterms.push(term);\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Get entries by term (wraps getEmDashCollection)\n */\nexport async function getEntriesByTerm(\n\tcollection: string,\n\ttaxonomyName: string,\n\ttermSlug: string,\n): Promise<Array<{ id: string; data: Record<string, unknown> }>> {\n\tconst { getEmDashCollection } = await import(\"../query.js\");\n\n\t// Build options as the expected type — getEmDashCollection accepts\n\t// a generic options object with `where` for filtering by taxonomy\n\tconst options: Record<string, unknown> = {\n\t\twhere: { [taxonomyName]: termSlug },\n\t};\n\tconst { entries } = await getEmDashCollection(collection, options);\n\n\treturn entries;\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\t// First pass: create nodes\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.id) ?? 0,\n\t\t});\n\t}\n\n\t// Second pass: build tree\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","import type { WidgetComponentDef } from \"./types.js\";\n\n/**\n * Core widget components registry\n * These are built-in widgets that ship with EmDash\n */\nexport const coreWidgetComponents: WidgetComponentDef[] = [\n\t{\n\t\tid: \"core:recent-posts\",\n\t\tlabel: \"Recent Posts\",\n\t\tdescription: \"Display a list of recent posts\",\n\t\tprops: {\n\t\t\tcount: {\n\t\t\t\ttype: \"number\",\n\t\t\t\tlabel: \"Number of posts\",\n\t\t\t\tdefault: 5,\n\t\t\t},\n\t\t\tshowThumbnails: {\n\t\t\t\ttype: \"boolean\",\n\t\t\t\tlabel: \"Show thumbnails\",\n\t\t\t\tdefault: false,\n\t\t\t},\n\t\t\tshowDate: {\n\t\t\t\ttype: \"boolean\",\n\t\t\t\tlabel: \"Show date\",\n\t\t\t\tdefault: true,\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tid: \"core:categories\",\n\t\tlabel: \"Categories\",\n\t\tdescription: \"Display category list\",\n\t\tprops: {\n\t\t\tshowCount: {\n\t\t\t\ttype: \"boolean\",\n\t\t\t\tlabel: \"Show post count\",\n\t\t\t\tdefault: true,\n\t\t\t},\n\t\t\thierarchical: {\n\t\t\t\ttype: \"boolean\",\n\t\t\t\tlabel: \"Show hierarchy\",\n\t\t\t\tdefault: true,\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tid: \"core:tags\",\n\t\tlabel: \"Tags\",\n\t\tdescription: \"Display tag cloud\",\n\t\tprops: {\n\t\t\tshowCount: {\n\t\t\t\ttype: \"boolean\",\n\t\t\t\tlabel: \"Show count\",\n\t\t\t\tdefault: false,\n\t\t\t},\n\t\t\tlimit: {\n\t\t\t\ttype: \"number\",\n\t\t\t\tlabel: \"Maximum tags\",\n\t\t\t\tdefault: 20,\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tid: \"core:search\",\n\t\tlabel: \"Search\",\n\t\tdescription: \"Search form\",\n\t\tprops: {\n\t\t\tplaceholder: {\n\t\t\t\ttype: \"string\",\n\t\t\t\tlabel: \"Placeholder text\",\n\t\t\t\tdefault: \"Search...\",\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tid: \"core:archives\",\n\t\tlabel: \"Archives\",\n\t\tdescription: \"Monthly/yearly archives\",\n\t\tprops: {\n\t\t\ttype: {\n\t\t\t\ttype: \"select\",\n\t\t\t\tlabel: \"Group by\",\n\t\t\t\tdefault: \"monthly\",\n\t\t\t\toptions: [\n\t\t\t\t\t{ value: \"monthly\", label: \"Monthly\" },\n\t\t\t\t\t{ value: \"yearly\", label: \"Yearly\" },\n\t\t\t\t],\n\t\t\t},\n\t\t\tlimit: {\n\t\t\t\ttype: \"number\",\n\t\t\t\tlabel: \"Limit\",\n\t\t\t\tdefault: 12,\n\t\t\t},\n\t\t},\n\t},\n];\n\n/**\n * Get all widget component definitions (core + plugin-registered)\n * For now, only returns core components. Plugin widgets will be added later.\n */\nexport function getWidgetComponents(): WidgetComponentDef[] {\n\treturn [...coreWidgetComponents];\n}\n","import { getDb } from \"../loader.js\";\nimport { getWidgetComponents as getComponentRegistry } from \"./components.js\";\nimport type { Widget, WidgetArea, WidgetRow, WidgetComponentDef } from \"./types.js\";\n\nexport type {\n\tWidget,\n\tWidgetArea,\n\tWidgetType,\n\tWidgetComponentDef,\n\tPropDef,\n\tCreateWidgetAreaInput,\n\tCreateWidgetInput,\n\tUpdateWidgetInput,\n\tReorderWidgetsInput,\n} from \"./types.js\";\n\n/**\n * Get a widget area by name, with all its widgets\n */\nexport async function getWidgetArea(name: string): Promise<WidgetArea | null> {\n\tconst db = await getDb();\n\t// Get the area\n\tconst areaRow = await db\n\t\t.selectFrom(\"_emdash_widget_areas\")\n\t\t.selectAll()\n\t\t.where(\"name\", \"=\", name)\n\t\t.executeTakeFirst();\n\n\tif (!areaRow) {\n\t\treturn null;\n\t}\n\n\t// Get widgets for this area, ordered by sort_order\n\tconst widgetRows = await db\n\t\t.selectFrom(\"_emdash_widgets\")\n\t\t.selectAll()\n\t\t.$castTo<WidgetRow>()\n\t\t.where(\"area_id\", \"=\", areaRow.id)\n\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t.execute();\n\n\t// Map to API types\n\tconst widgets: Widget[] = widgetRows.map((row) => rowToWidget(row));\n\n\treturn {\n\t\tid: areaRow.id,\n\t\tname: areaRow.name,\n\t\tlabel: areaRow.label,\n\t\tdescription: areaRow.description ?? undefined,\n\t\twidgets,\n\t};\n}\n\n/**\n * Get all widget areas with their widgets\n */\nexport async function getWidgetAreas(): Promise<WidgetArea[]> {\n\tconst db = await getDb();\n\t// Get all areas\n\tconst areaRows = await db.selectFrom(\"_emdash_widget_areas\").selectAll().execute();\n\n\t// Get all widgets\n\tconst widgetRows = await db\n\t\t.selectFrom(\"_emdash_widgets\")\n\t\t.selectAll()\n\t\t.$castTo<WidgetRow>()\n\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t.execute();\n\n\t// Group widgets by area\n\tconst widgetsByArea = new Map<string, Widget[]>();\n\tfor (const row of widgetRows) {\n\t\tif (!widgetsByArea.has(row.area_id)) {\n\t\t\twidgetsByArea.set(row.area_id, []);\n\t\t}\n\t\twidgetsByArea.get(row.area_id)!.push(rowToWidget(row));\n\t}\n\n\t// Combine\n\treturn areaRows.map((areaRow) => ({\n\t\tid: areaRow.id,\n\t\tname: areaRow.name,\n\t\tlabel: areaRow.label,\n\t\tdescription: areaRow.description ?? undefined,\n\t\twidgets: widgetsByArea.get(areaRow.id) || [],\n\t}));\n}\n\n/**\n * Get available widget components (for admin UI)\n */\nexport function getWidgetComponents(): WidgetComponentDef[] {\n\treturn getComponentRegistry();\n}\n\n/**\n * Convert a widget row to the API type\n */\nfunction rowToWidget(row: WidgetRow): Widget {\n\tconst widget: Widget = {\n\t\tid: row.id,\n\t\ttype: row.type,\n\t\ttitle: row.title ?? undefined,\n\t};\n\n\t// Type-specific fields\n\tif (row.type === \"content\" && row.content) {\n\t\ttry {\n\t\t\twidget.content = JSON.parse(row.content);\n\t\t} catch {\n\t\t\t// Invalid JSON, ignore\n\t\t}\n\t}\n\n\tif (row.type === \"menu\" && row.menu_name) {\n\t\twidget.menuName = row.menu_name;\n\t}\n\n\tif (row.type === \"component\" && row.component_id) {\n\t\twidget.componentId = row.component_id;\n\t\tif (row.component_props) {\n\t\t\ttry {\n\t\t\t\twidget.componentProps = JSON.parse(row.component_props);\n\t\t\t} catch {\n\t\t\t\t// Invalid JSON, ignore\n\t\t\t}\n\t\t}\n\t}\n\n\treturn widget;\n}\n","/**\n * Search Query Functions\n *\n * Programmatic API for searching content using FTS5.\n */\n\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\n\nimport type { Database } from \"../database/types.js\";\nimport { validateIdentifier } from \"../database/validate.js\";\nimport { getDb } from \"../loader.js\";\nimport { FTSManager } from \"./fts-manager.js\";\nimport type {\n\tSearchOptions,\n\tCollectionSearchOptions,\n\tSearchResult,\n\tSearchResponse,\n\tSuggestOptions,\n\tSuggestion,\n\tSearchStats,\n} from \"./types.js\";\n\n/** Pattern to split on whitespace for query term extraction */\nconst WHITESPACE_SPLIT_PATTERN = /\\s+/;\nconst FTS_OPERATORS_PATTERN = /\\b(AND|OR|NOT|NEAR)\\b/i;\nconst DOUBLE_QUOTE_PATTERN = /\"/g;\n\n/**\n * Search across multiple collections\n *\n * Public API that auto-injects the database.\n *\n * @param query - Search query (FTS5 syntax supported)\n * @param options - Search options\n * @returns Search results with pagination\n *\n * @example\n * ```typescript\n * import { search } from \"emdash\";\n *\n * const results = await search(\"hello world\", {\n * collections: [\"posts\", \"pages\"],\n * limit: 20\n * });\n * ```\n */\nexport async function search(query: string, options: SearchOptions = {}): Promise<SearchResponse> {\n\tconst db = await getDb();\n\treturn searchWithDb(db, query, options);\n}\n\n/**\n * Search across multiple collections (with explicit db)\n *\n * @internal Use `search()` in templates. This variant is for admin routes\n * that already have a database handle.\n *\n * @param db - Kysely database instance\n * @param query - Search query (FTS5 syntax supported)\n * @param options - Search options\n * @returns Search results with pagination\n */\nexport async function searchWithDb(\n\tdb: Kysely<Database>,\n\tquery: string,\n\toptions: SearchOptions = {},\n): Promise<SearchResponse> {\n\tconst ftsManager = new FTSManager(db);\n\tconst limit = options.limit ?? 20;\n\tconst status = options.status ?? \"published\";\n\n\t// Get searchable collections\n\tlet collections = options.collections;\n\tif (!collections || collections.length === 0) {\n\t\tcollections = await getSearchableCollections(db);\n\t}\n\n\tif (collections.length === 0) {\n\t\treturn { items: [] };\n\t}\n\n\t// Search each collection and merge results\n\tconst allResults: SearchResult[] = [];\n\n\tfor (const collection of collections) {\n\t\tconst config = await ftsManager.getSearchConfig(collection);\n\t\tif (!config?.enabled) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst collectionResults = await searchSingleCollection(\n\t\t\tdb,\n\t\t\tcollection,\n\t\t\tquery,\n\t\t\t{\n\t\t\t\tstatus,\n\t\t\t\tlocale: options.locale,\n\t\t\t\tlimit: limit * 2, // Get extra for merging\n\t\t\t},\n\t\t\tconfig.weights,\n\t\t);\n\n\t\tallResults.push(...collectionResults);\n\t}\n\n\t// Sort by score descending\n\tallResults.sort((a, b) => b.score - a.score);\n\n\t// Apply limit\n\tconst items = allResults.slice(0, limit);\n\n\treturn { items };\n}\n\n/**\n * Search within a single collection\n *\n * @param db - Kysely database instance\n * @param collection - Collection slug\n * @param query - Search query (FTS5 syntax supported)\n * @param options - Search options\n * @returns Search results with pagination\n *\n * @example\n * ```typescript\n * const results = await searchCollection(db, \"posts\", \"hello world\", {\n * limit: 10\n * });\n * ```\n */\nexport async function searchCollection(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tquery: string,\n\toptions: CollectionSearchOptions = {},\n): Promise<SearchResponse> {\n\tconst ftsManager = new FTSManager(db);\n\tconst config = await ftsManager.getSearchConfig(collection);\n\n\tif (!config?.enabled) {\n\t\treturn { items: [] };\n\t}\n\n\tconst items = await searchSingleCollection(db, collection, query, options, config.weights);\n\n\treturn { items };\n}\n\n/**\n * Internal function to search a single collection\n */\nasync function searchSingleCollection(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tquery: string,\n\toptions: CollectionSearchOptions,\n\tweights?: Record<string, number>,\n): Promise<SearchResult[]> {\n\t// Validate before any raw SQL interpolation\n\tvalidateIdentifier(collection, \"collection slug\");\n\n\tconst ftsManager = new FTSManager(db);\n\tconst ftsTable = ftsManager.getFtsTableName(collection);\n\tconst contentTable = ftsManager.getContentTableName(collection);\n\tconst limit = options.limit ?? 20;\n\tconst status = options.status ?? \"published\";\n\tconst locale = options.locale;\n\n\t// Check if FTS table exists\n\tif (!(await ftsManager.ftsTableExists(collection))) {\n\t\treturn [];\n\t}\n\n\t// Escape the query for FTS5\n\tconst escapedQuery = escapeQuery(query);\n\tif (!escapedQuery) {\n\t\treturn [];\n\t}\n\n\t// Get searchable fields for snippet generation\n\tconst searchableFields = await ftsManager.getSearchableFields(collection);\n\n\t// Build weight string for bm25 if weights provided\n\t// Format: bm25(table, weight1, weight2, ...)\n\t// First two weights are for 'id' and 'locale' columns (UNINDEXED, so 0)\n\tlet bm25Args = \"\";\n\tif (weights && searchableFields.length > 0) {\n\t\tconst weightValues = [\"0\", \"0\"]; // id column, locale column\n\t\tfor (const field of searchableFields) {\n\t\t\tweightValues.push(String(weights[field] ?? 1));\n\t\t}\n\t\tbm25Args = weightValues.join(\", \");\n\t}\n\n\t// Build and execute the search query\n\t// Using raw SQL because Kysely doesn't have FTS5 support\n\tconst bm25Expr = bm25Args ? `bm25(\"${ftsTable}\", ${bm25Args})` : `bm25(\"${ftsTable}\")`;\n\n\t// Snippet column index is 2 (after id=0, locale=1, first searchable field=2)\n\tconst results = await sql<{\n\t\tid: string;\n\t\tslug: string | null;\n\t\tlocale: string;\n\t\ttitle: string | null;\n\t\tsnippet: string;\n\t\tscore: number;\n\t}>`\n\t\tSELECT \n\t\t\tc.id,\n\t\t\tc.slug,\n\t\t\tc.locale,\n\t\t\tc.title,\n\t\t\tsnippet(\"${sql.raw(ftsTable)}\", 2, '<mark>', '</mark>', '...', 32) as snippet,\n\t\t\t${sql.raw(bm25Expr)} as score\n\t\tFROM \"${sql.raw(ftsTable)}\" f\n\t\tJOIN \"${sql.raw(contentTable)}\" c ON f.id = c.id\n\t\tWHERE \"${sql.raw(ftsTable)}\" MATCH ${escapedQuery}\n\t\tAND c.status = ${status}\n\t\tAND c.deleted_at IS NULL\n\t\t${locale ? sql`AND c.locale = ${locale}` : sql``}\n\t\tORDER BY score\n\t\tLIMIT ${limit}\n\t`.execute(db);\n\n\treturn results.rows.map((row) => ({\n\t\tcollection,\n\t\tid: row.id,\n\t\tslug: row.slug,\n\t\tlocale: row.locale,\n\t\ttitle: row.title ?? undefined,\n\t\tsnippet: row.snippet,\n\t\tscore: Math.abs(row.score), // bm25 returns negative scores\n\t}));\n}\n\n/**\n * Get search suggestions for autocomplete\n *\n * @param db - Kysely database instance\n * @param query - Partial search query\n * @param options - Suggestion options\n * @returns Array of suggestions\n */\nexport async function getSuggestions(\n\tdb: Kysely<Database>,\n\tquery: string,\n\toptions: SuggestOptions = {},\n): Promise<Suggestion[]> {\n\tconst limit = options.limit ?? 5;\n\tconst locale = options.locale;\n\n\t// Get searchable collections\n\tlet collections = options.collections;\n\tif (!collections || collections.length === 0) {\n\t\tcollections = await getSearchableCollections(db);\n\t}\n\n\tif (collections.length === 0) {\n\t\treturn [];\n\t}\n\n\tconst suggestions: Suggestion[] = [];\n\n\tfor (const collection of collections) {\n\t\tconst ftsManager = new FTSManager(db);\n\t\tconst config = await ftsManager.getSearchConfig(collection);\n\t\tif (!config?.enabled) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Validate before raw SQL interpolation\n\t\tvalidateIdentifier(collection, \"collection slug\");\n\n\t\tconst ftsTable = ftsManager.getFtsTableName(collection);\n\t\tconst contentTable = ftsManager.getContentTableName(collection);\n\n\t\t// Use prefix search for autocomplete\n\t\tconst prefixQuery = `${escapeQuery(query)}*`;\n\t\tif (!prefixQuery || prefixQuery === \"*\") {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst results = await sql<{\n\t\t\tid: string;\n\t\t\ttitle: string;\n\t\t}>`\n\t\t\tSELECT \n\t\t\t\tc.id,\n\t\t\t\tc.title\n\t\t\tFROM \"${sql.raw(ftsTable)}\" f\n\t\t\tJOIN \"${sql.raw(contentTable)}\" c ON f.id = c.id\n\t\t\tWHERE \"${sql.raw(ftsTable)}\" MATCH ${prefixQuery}\n\t\t\tAND c.status = 'published'\n\t\t\tAND c.deleted_at IS NULL\n\t\t\tAND c.title IS NOT NULL\n\t\t\t${locale ? sql`AND c.locale = ${locale}` : sql``}\n\t\t\tORDER BY bm25(\"${sql.raw(ftsTable)}\")\n\t\t\tLIMIT ${limit}\n\t\t`.execute(db);\n\n\t\tfor (const row of results.rows) {\n\t\t\tsuggestions.push({\n\t\t\t\tcollection,\n\t\t\t\tid: row.id,\n\t\t\t\ttitle: row.title,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn suggestions.slice(0, limit);\n}\n\n/**\n * Get search statistics for all collections\n */\nexport async function getSearchStats(db: Kysely<Database>): Promise<SearchStats> {\n\tconst ftsManager = new FTSManager(db);\n\tconst collections = await getSearchableCollections(db);\n\tconst stats: SearchStats = { collections: {} };\n\n\tfor (const collection of collections) {\n\t\tconst collectionStats = await ftsManager.getIndexStats(collection);\n\t\tif (collectionStats) {\n\t\t\tstats.collections[collection] = collectionStats;\n\t\t}\n\t}\n\n\treturn stats;\n}\n\n/**\n * Get list of collections with search enabled\n */\nasync function getSearchableCollections(db: Kysely<Database>): Promise<string[]> {\n\tconst results = await db\n\t\t.selectFrom(\"_emdash_collections\")\n\t\t.select([\"slug\", \"search_config\"])\n\t\t.execute();\n\n\treturn results\n\t\t.filter((r) => {\n\t\t\tif (!r.search_config) return false;\n\t\t\ttry {\n\t\t\t\tconst config = JSON.parse(r.search_config);\n\t\t\t\treturn config.enabled === true;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t})\n\t\t.map((r) => r.slug);\n}\n\n/**\n * Escape a query string for FTS5\n *\n * Handles special characters and prevents injection.\n */\nfunction escapeQuery(query: string): string {\n\tif (!query || typeof query !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\t// Trim whitespace\n\tquery = query.trim();\n\n\tif (query.length === 0) {\n\t\treturn \"\";\n\t}\n\n\t// If already a quoted phrase, escape only interior quotes and preserve phrase syntax\n\tif (query.startsWith('\"') && query.endsWith('\"') && query.length >= 2) {\n\t\tconst inner = query.slice(1, -1);\n\t\treturn `\"${inner.replace(DOUBLE_QUOTE_PATTERN, '\"\"')}\"`;\n\t}\n\n\t// Escape any existing quotes\n\tconst escaped = query.replace(DOUBLE_QUOTE_PATTERN, '\"\"');\n\n\t// If the query contains FTS5 operators (AND, OR, NOT, NEAR),\n\t// pass through with quotes escaped but operators preserved\n\tif (FTS_OPERATORS_PATTERN.test(query)) {\n\t\treturn escaped;\n\t}\n\n\t// For simple queries, wrap each word to handle special chars\n\tconst terms = escaped.split(WHITESPACE_SPLIT_PATTERN).filter((t) => t.length > 0);\n\tif (terms.length === 0) {\n\t\treturn \"\";\n\t}\n\n\t// Join with implicit AND, add prefix matching (*) to all terms\n\t// This allows \"hel wor\" to match \"hello world\"\n\treturn terms.map((t) => `\"${t}\"*`).join(\" \");\n}\n","/**\n * Text Extraction\n *\n * Extracts plain text from Portable Text blocks for FTS indexing.\n * Uses @portabletext/toolkit as base with extensions for custom block types.\n */\n\nimport { toPlainText } from \"@portabletext/toolkit\";\n\nimport type { PortableTextBlock } from \"../content/converters/types.js\";\n\n/**\n * Validate that a value looks like a Portable Text block array.\n * Each element must have at least a `_type` string property.\n */\nfunction isPortableTextArray(value: unknown[]): value is PortableTextBlock[] {\n\treturn value.every(\n\t\t(item) =>\n\t\t\ttypeof item === \"object\" &&\n\t\t\titem !== null &&\n\t\t\t\"_type\" in item &&\n\t\t\ttypeof item._type === \"string\",\n\t);\n}\n\n/**\n * Extract additional text from custom block types that toPlainText doesn't handle\n */\nfunction extractCustomBlockText(block: PortableTextBlock): string {\n\t// Code blocks - include the code content\n\tif (block._type === \"code\" && \"code\" in block && typeof block.code === \"string\") {\n\t\treturn block.code;\n\t}\n\n\t// Image blocks - include alt text and caption\n\tif (block._type === \"image\") {\n\t\tconst parts: string[] = [];\n\t\tif (\"alt\" in block && typeof block.alt === \"string\" && block.alt) {\n\t\t\tparts.push(block.alt);\n\t\t}\n\t\tif (\"caption\" in block && typeof block.caption === \"string\" && block.caption) {\n\t\t\tparts.push(block.caption);\n\t\t}\n\t\treturn parts.join(\" \");\n\t}\n\n\treturn \"\";\n}\n\n/**\n * Extract plain text from Portable Text blocks\n *\n * Uses @portabletext/toolkit's toPlainText for standard blocks,\n * plus extracts text from custom block types (code, images with alt/caption).\n *\n * @param blocks - Array of Portable Text blocks (or a JSON string)\n * @returns Plain text content\n *\n * @example\n * ```typescript\n * const text = extractPlainText([\n * {\n * _type: \"block\",\n * _key: \"abc\",\n * children: [{ _type: \"span\", _key: \"s1\", text: \"Hello World\" }]\n * }\n * ]);\n * // Returns: \"Hello World\"\n * ```\n */\nexport function extractPlainText(blocks: PortableTextBlock[] | string | null | undefined): string {\n\tif (!blocks) {\n\t\treturn \"\";\n\t}\n\n\t// Handle JSON string input\n\tlet parsedBlocks: PortableTextBlock[];\n\tif (typeof blocks === \"string\") {\n\t\ttry {\n\t\t\tparsedBlocks = JSON.parse(blocks);\n\t\t} catch {\n\t\t\t// If it's not valid JSON, treat as plain text\n\t\t\treturn blocks;\n\t\t}\n\t} else {\n\t\tparsedBlocks = blocks;\n\t}\n\n\tif (!Array.isArray(parsedBlocks)) {\n\t\treturn \"\";\n\t}\n\n\t// Use official toPlainText for standard blocks.\n\t// toPlainText expects `{ _type: string; [key: string]: any }[]` but our blocks use\n\t// `unknown` index sigs. They're structurally compatible at runtime — spread each block\n\t// to satisfy the wider index signature without an unsafe cast.\n\tconst toolkitBlocks = parsedBlocks.map((b) => {\n\t\tconst obj: Record<string, unknown> & { _type: string } = { _type: b._type };\n\t\tfor (const [key, val] of Object.entries(b)) {\n\t\t\tobj[key] = val;\n\t\t}\n\t\treturn obj;\n\t});\n\tconst standardText = toPlainText(toolkitBlocks);\n\n\t// Extract text from custom block types that toPlainText doesn't handle\n\tconst customTexts = parsedBlocks.map(extractCustomBlockText).filter((text) => text.length > 0);\n\n\t// Combine both\n\tconst allTexts = [standardText, ...customTexts].filter((t) => t.length > 0);\n\treturn allTexts.join(\"\\n\");\n}\n\n/**\n * Extract searchable text from a content entry\n *\n * Extracts text from specified fields, handling both plain text and Portable Text.\n *\n * @param entry - Content entry data\n * @param fields - Field names to extract text from\n * @returns Object mapping field names to extracted text\n */\nexport function extractSearchableFields(\n\tentry: Record<string, unknown>,\n\tfields: string[],\n): Record<string, string> {\n\tconst result: Record<string, string> = {};\n\n\tfor (const field of fields) {\n\t\tconst value = entry[field];\n\n\t\tif (value === null || value === undefined) {\n\t\t\tresult[field] = \"\";\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (typeof value === \"string\") {\n\t\t\t// Could be plain text or JSON Portable Text\n\t\t\tif (value.startsWith(\"[\")) {\n\t\t\t\tresult[field] = extractPlainText(value);\n\t\t\t} else {\n\t\t\t\tresult[field] = value;\n\t\t\t}\n\t\t} else if (Array.isArray(value)) {\n\t\t\t// Validate the array looks like Portable Text before treating it as such\n\t\t\tif (isPortableTextArray(value)) {\n\t\t\t\tresult[field] = extractPlainText(value);\n\t\t\t} else {\n\t\t\t\tresult[field] = JSON.stringify(value);\n\t\t\t}\n\t\t} else if (typeof value === \"object\") {\n\t\t\t// Object — serialize to JSON for searchable text\n\t\t\tresult[field] = JSON.stringify(value);\n\t\t} else if (typeof value === \"number\" || typeof value === \"boolean\") {\n\t\t\tresult[field] = `${value}`;\n\t\t} else {\n\t\t\tresult[field] = \"\";\n\t\t}\n\t}\n\n\treturn result;\n}\n"],"mappings":"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"}
|