{"version":3,"file":"api-XauujAJA.mjs","names":["TRAILING_SLASHES","TRAILING_DOT"],"sources":["../src/api/rev.ts","../src/api/handlers/validate-media-fields.ts","../src/api/handlers/content.ts","../src/api/handlers/manifest.ts","../src/api/handlers/revision.ts","../src/api/handlers/media.ts","../src/api/handlers/plugins.ts","../src/api/handlers/plugin-settings.ts","../src/plugins/marketplace.ts","../src/plugins/storage-indexes.ts","../src/api/handlers/marketplace.ts","../src/registry/artifact-fetch.ts","../src/registry/config.ts","../src/registry/plugin-id.ts","../src/api/handlers/registry.ts"],"sourcesContent":["/**\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","import type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { matchesMimeAllowlist, parseAllowedMimeTypes } from \"../../media/mime.js\";\nimport { requestCached } from \"../../request-cache.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../../utils/chunks.js\";\nimport type { ApiResult } from \"../types.js\";\n\ninterface FieldRow {\n\tslug: string;\n\ttype: string;\n\tallowedMimeTypes: string[];\n}\n\ninterface MediaRefValue {\n\tid?: unknown;\n\tprovider?: unknown;\n\tmimeType?: unknown;\n}\n\nfunction asMediaRef(value: unknown): MediaRefValue | null {\n\tif (value === null || value === undefined) return null;\n\tif (typeof value !== \"object\" || Array.isArray(value)) return null;\n\treturn value;\n}\n\nfunction fail(message: string): ApiResult<never> {\n\treturn { success: false, error: { code: \"INVALID_MIME_FOR_FIELD\", message } };\n}\n\nasync function loadMediaFieldsForCollection(\n\tdb: Kysely<Database>,\n\tcollectionSlug: string,\n): Promise<FieldRow[]> {\n\tconst rows = await db\n\t\t.selectFrom(\"_emdash_fields\")\n\t\t.innerJoin(\"_emdash_collections\", \"_emdash_collections.id\", \"_emdash_fields.collection_id\")\n\t\t.select([\"_emdash_fields.slug\", \"_emdash_fields.type\", \"_emdash_fields.validation\"])\n\t\t.where(\"_emdash_collections.slug\", \"=\", collectionSlug)\n\t\t.where(\"_emdash_fields.type\", \"in\", [\"file\", \"image\"])\n\t\t.execute();\n\n\tconst out: FieldRow[] = [];\n\tfor (const row of rows) {\n\t\tconst list = parseAllowedMimeTypes(row.validation);\n\t\tif (!list) continue;\n\t\tout.push({ slug: row.slug, type: row.type, allowedMimeTypes: list });\n\t}\n\treturn out;\n}\n\nexport async function validateMediaFields(\n\tdb: Kysely<Database>,\n\tcollectionSlug: string,\n\tdata: Record<string, unknown>,\n): Promise<ApiResult<true>> {\n\t// Cache is keyed on slug only. If a handler creates/modifies a field and\n\t// then writes content in the same request (e.g. bulk import), the cached\n\t// list will be stale for that request. This is an edge case in normal use.\n\tconst fields = await requestCached(`mediaFields:${collectionSlug}`, () =>\n\t\tloadMediaFieldsForCollection(db, collectionSlug),\n\t);\n\tif (fields.length === 0) return { success: true, data: true };\n\n\t// Collect local media ids that need a MIME lookup\n\tconst localIds = new Set<string>();\n\tfor (const field of fields) {\n\t\tconst ref = asMediaRef(data[field.slug]);\n\t\tif (!ref) continue;\n\t\tconst provider = typeof ref.provider === \"string\" ? ref.provider : \"local\";\n\t\tif (provider === \"local\" && typeof ref.id === \"string\") {\n\t\t\tlocalIds.add(ref.id);\n\t\t}\n\t}\n\n\t// Batch-load local media MIMEs\n\tconst idList = [...localIds];\n\tconst mimeById = new Map<string, string>();\n\tif (idList.length > 0) {\n\t\tfor (const batch of chunks(idList, SQL_BATCH_SIZE)) {\n\t\t\tconst rows = await db\n\t\t\t\t.selectFrom(\"media\")\n\t\t\t\t.select([\"id\", \"mime_type\"])\n\t\t\t\t.where(\"id\", \"in\", batch)\n\t\t\t\t.execute();\n\t\t\tfor (const r of rows) mimeById.set(r.id, r.mime_type);\n\t\t}\n\t}\n\n\tfor (const field of fields) {\n\t\tconst value = data[field.slug];\n\t\tif (value === null || value === undefined) continue;\n\t\tconst ref = asMediaRef(value);\n\t\tif (!ref) continue;\n\n\t\tconst provider = typeof ref.provider === \"string\" ? ref.provider : \"local\";\n\n\t\t// External providers carry mimeType in the ref; trust it as-is.\n\t\t// Local media: look up the stored mimeType by id.\n\t\tlet mime: string | undefined;\n\t\tif (provider === \"local\") {\n\t\t\tif (typeof ref.id !== \"string\") {\n\t\t\t\treturn fail(`Field '${field.slug}' references media with an invalid id`);\n\t\t\t}\n\t\t\tmime = mimeById.get(ref.id);\n\t\t\tif (!mime) {\n\t\t\t\treturn fail(`Field '${field.slug}' references media with unknown MIME type`);\n\t\t\t}\n\t\t} else {\n\t\t\tif (typeof ref.mimeType !== \"string\") {\n\t\t\t\treturn fail(`Field '${field.slug}' requires a mimeType declaration for non-local media`);\n\t\t\t}\n\t\t\t// TODO: long-term, consider a server-side HEAD probe or provider-vouched\n\t\t\t// MIMEs for non-local refs; for now the constraint is only as strong as\n\t\t\t// the client that constructed the ref.\n\t\t\tmime = ref.mimeType;\n\t\t}\n\n\t\tif (!matchesMimeAllowlist(mime, field.allowedMimeTypes)) {\n\t\t\treturn fail(`Field '${field.slug}' does not accept ${mime}`);\n\t\t}\n\t}\n\n\treturn { success: true, data: true };\n}\n","/**\n * Content CRUD handlers\n */\n\nimport type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\n\nimport type { ContentFieldFilters } from \"../../content-list-query.js\";\nimport { isSqlite } from \"../../database/dialect-helpers.js\";\nimport { BylineRepository } from \"../../database/repositories/byline.js\";\nimport type { ContentBylineInput } from \"../../database/repositories/byline.js\";\nimport { CommentRepository } from \"../../database/repositories/comment.js\";\nimport { ContentRepository, isSystemOrderField } 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 { TaxonomyRepository } from \"../../database/repositories/taxonomy.js\";\nimport {\n\tContentCollectionNotFoundError,\n\tContentMutationConflictError,\n\tEmDashValidationError,\n\tScheduledNotDueError,\n\tInvalidCursorError,\n\ttype BylineSummary,\n\ttype ContentBylineCredit,\n\ttype ContentBylineFilter,\n\ttype ContentDateField,\n\ttype ContentItem,\n\ttype ContentSeo,\n\ttype ContentSeoInput,\n\ttype FindManyOptions,\n} from \"../../database/repositories/types.js\";\nimport { UserRepository } from \"../../database/repositories/user.js\";\nimport { withTransaction } from \"../../database/transaction.js\";\nimport type { Database } from \"../../database/types.js\";\nimport { validateIdentifier } from \"../../database/validate.js\";\nimport { getI18nConfig, isI18nEnabled, resolveConfiguredLocale } from \"../../i18n/config.js\";\nimport { invalidateRedirectCache } from \"../../redirects/cache.js\";\nimport { FTSManager } from \"../../search/fts-manager.js\";\nimport { invalidateTermCache } from \"../../taxonomies/index.js\";\nimport { isMissingColumnError, isMissingTableError } from \"../../utils/db-errors.js\";\nimport { encodeRev, validateRev } from \"../rev.js\";\nimport type { ApiResult, ContentListResponse, ContentResponse } from \"../types.js\";\nimport { validateMediaFields } from \"./validate-media-fields.js\";\n\n/**\n * Narrow a caught error to one carrying a structured `apiError` discriminant.\n * Used by transaction callbacks that want to surface a specific error code\n * through the standard Error throwing path.\n */\nfunction hasApiError(error: unknown): error is Error & { apiError: { code: string } } {\n\tif (!(error instanceof Error) || !(\"apiError\" in error)) return false;\n\tconst { apiError } = error;\n\treturn (\n\t\ttypeof apiError === \"object\" &&\n\t\tapiError !== null &&\n\t\t\"code\" in apiError &&\n\t\ttypeof apiError.code === \"string\"\n\t);\n}\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\nasync function getCollectionPublishConfig(\n\tdb: Kysely<Database>,\n\tcollection: string,\n): Promise<{ supportsRevisions: boolean; routable: boolean }> {\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_collections\")\n\t\t.select([\"supports\", \"routable\"])\n\t\t.where(\"slug\", \"=\", collection)\n\t\t.executeTakeFirst();\n\tconst supports: unknown = row?.supports ? JSON.parse(row.supports) : [];\n\treturn {\n\t\tsupportsRevisions: Array.isArray(supports) && supports.includes(\"revisions\"),\n\t\troutable: row?.routable !== 0,\n\t};\n}\n\nfunction requireRoutablePublishSlug(routable: boolean, slug: string | null | undefined): void {\n\tif (routable && !slug?.trim()) {\n\t\tthrow new EmDashValidationError(\"Cannot publish routable content without a slug\");\n\t}\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\t// Strict per-locale (migration 040): a credit at locale X renders iff a\n\t// byline row exists at locale X in the credited translation_group. The\n\t// junction itself spans translations; rendering does not fall back.\n\tconst localeOpt = item.locale ? { locale: item.locale } : undefined;\n\tconst bylines = await bylineRepo.getContentBylines(collection, item.id, localeOpt);\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// `primaryBylineId` is set iff junction rows exist; non-null\n\t// suppresses author fallback even when the credit doesn't resolve\n\t// at this locale.\n\tif (item.primaryBylineId) {\n\t\titem.bylines = [];\n\t\titem.byline = null;\n\t\treturn;\n\t}\n\n\tif (item.authorId) {\n\t\t// Same strict-locale rule as explicit credits: a user-linked byline\n\t\t// renders on the entry only when a sibling exists at the entry's\n\t\t// locale. Without this we'd silently surface the default-locale\n\t\t// row, which contradicts the per-locale model.\n\t\tconst fallback = await bylineRepo.findByUserId(item.authorId, localeOpt);\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 *\n * Items may live at different locales (e.g. a list endpoint returning the\n * translations of an entry). Group by `item.locale` and call the strict\n * per-locale repo method once per group so each item resolves against its\n * own locale's byline rows.\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. Bucket items by locale so we can call the strict-locale repo\n\t//    once per bucket. Items with a null/undefined locale (pre-i18n\n\t//    rows on a single-locale install) share an \"unscoped\" bucket.\n\tconst localeBuckets = new Map<string | null, ContentItem[]>();\n\tfor (const item of items) {\n\t\tconst key = item.locale ?? null;\n\t\tconst bucket = localeBuckets.get(key);\n\t\tif (bucket) bucket.push(item);\n\t\telse localeBuckets.set(key, [item]);\n\t}\n\n\t// 2. Per-locale: fetch explicit credits. Items whose credits don't\n\t//    resolve at this locale go through a locale-agnostic \"has any\n\t//    junction\" check before being considered for author inference —\n\t//    explicit editorial intent at any locale beats inferred fallback.\n\tconst bylinesByItem = new Map<string, ContentBylineCredit[]>();\n\tconst itemsNeedingAuthorCheck: ContentItem[] = [];\n\tfor (const [locale, bucket] of localeBuckets) {\n\t\tconst localeOpt = locale ? { locale } : undefined;\n\t\tconst ids = bucket.map((i) => i.id);\n\t\tconst credits = await bylineRepo.getContentBylinesMany(collection, ids, localeOpt);\n\t\tfor (const [id, list] of credits) bylinesByItem.set(id, list);\n\n\t\tfor (const item of bucket) {\n\t\t\tif (credits.has(item.id) && credits.get(item.id)!.length > 0) continue;\n\t\t\tif (item.authorId) itemsNeedingAuthorCheck.push(item);\n\t\t}\n\t}\n\n\t// 3. Author fallback applies only when no explicit credit exists\n\t//    (primaryBylineId null).\n\tconst fallbackByItem = new Map<string, BylineSummary>();\n\tif (itemsNeedingAuthorCheck.length > 0) {\n\t\tconst authorBuckets = new Map<string | null, ContentItem[]>();\n\t\tfor (const item of itemsNeedingAuthorCheck) {\n\t\t\tif (item.primaryBylineId) continue;\n\t\t\tconst key = item.locale ?? null;\n\t\t\tconst bucket = authorBuckets.get(key);\n\t\t\tif (bucket) bucket.push(item);\n\t\t\telse authorBuckets.set(key, [item]);\n\t\t}\n\n\t\tfor (const [locale, bucket] of authorBuckets) {\n\t\t\tconst localeOpt = locale ? { locale } : undefined;\n\t\t\tconst authorIds = bucket.map((i) => i.authorId).filter((id): id is string => id !== null);\n\t\t\tconst uniqueAuthorIds = [...new Set(authorIds)];\n\t\t\tif (uniqueAuthorIds.length === 0) continue;\n\t\t\tconst authorMap = await bylineRepo.findByUserIds(uniqueAuthorIds, localeOpt);\n\t\t\tfor (const item of bucket) {\n\t\t\t\tif (!item.authorId) continue;\n\t\t\t\tconst f = authorMap.get(item.authorId);\n\t\t\t\tif (f) fallbackByItem.set(item.id, f);\n\t\t\t}\n\t\t}\n\t}\n\n\t// 4. Assign to each item.\n\tfor (const item of items) {\n\t\tconst explicit = bylinesByItem.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\tconst fallback = fallbackByItem.get(item.id);\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\tcontinue;\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(\n\t\tcollection,\n\t\tidentifier,\n\t\tlocale ? resolveConfiguredLocale(locale) : undefined,\n\t);\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(\n\t\tcollection,\n\t\tidentifier,\n\t\tlocale ? resolveConfiguredLocale(locale) : undefined,\n\t);\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 * Resolve the columns a content-list search should match against. Always\n * includes `slug` (a standard column), adds the configured `titleField` plus\n * the `title`/`name` display fields when the collection actually defines them,\n * mirroring the admin's item-title resolution (titleField -> title -> name ->\n * slug), and includes every field explicitly marked searchable. Returning only\n * schema-backed columns avoids \"no such column\" errors.\n */\nasync function resolveSearchColumns(db: Kysely<Database>, collection: string): Promise<string[]> {\n\tconst row = await db\n\t\t.selectFrom(\"_emdash_collections\")\n\t\t.select([\"id\", \"title_field\"])\n\t\t.where(\"slug\", \"=\", collection)\n\t\t.executeTakeFirst();\n\tif (!row) return [\"slug\"];\n\n\tconst fields = await db\n\t\t.selectFrom(\"_emdash_fields\")\n\t\t.select([\"slug\", \"searchable\"])\n\t\t.where(\"collection_id\", \"=\", row.id)\n\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t.execute();\n\tconst columns = new Set([\"slug\"]);\n\tconst fieldSlugs = new Set(fields.map((f) => f.slug));\n\n\t// A configured titleField takes precedence, then the conventional\n\t// title/name fields. A null title_field falls through to those defaults.\n\tif (row.title_field && fieldSlugs.has(row.title_field)) columns.add(row.title_field);\n\tfor (const candidate of [\"title\", \"name\"]) {\n\t\tif (fieldSlugs.has(candidate)) columns.add(candidate);\n\t}\n\tfor (const field of fields) {\n\t\tif (field.searchable === 1) columns.add(field.slug);\n\t}\n\treturn [...columns];\n}\n\n/**\n * Decide whether the content-list `q` filter can be served from the\n * collection's FTS5 index instead of a full-scan substring LIKE (#1517).\n *\n * Requires SQLite (FTS5 is SQLite-only), search enabled on the collection,\n * every non-slug display column present in the searchable-field set (or the\n * index would miss matches the LIKE finds), and the index table actually\n * existing.\n */\nasync function canUseFtsForListFilter(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tsearchColumns: string[],\n): Promise<boolean> {\n\tif (!isSqlite(db)) return false;\n\tconst ftsManager = new FTSManager(db);\n\tconst config = await ftsManager.getSearchConfig(collection);\n\tif (!config?.enabled) return false;\n\tconst searchable = new Set(await ftsManager.getSearchableFields(collection));\n\tconst covered = searchColumns.every((col) => col === \"slug\" || searchable.has(col));\n\tif (!covered) return false;\n\treturn ftsManager.ftsTableExists(collection);\n}\n\n/**\n * Create a 301 auto-redirect from an entry's old URL to its new one after a\n * slug change, using the collection's URL pattern. Shared by\n * handleContentUpdate (direct slug edits) and handleContentPublish (slug edits\n * staged as `_slug` in a draft revision, which only land on publish).\n */\nasync function createSlugChangeRedirect(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\toldSlug: string,\n\tnewSlug: string,\n\tcontentId: string,\n): Promise<void> {\n\t// A URL pattern has no locale token, so every locale variant of an entry\n\t// generates the same URL, and slugs are unique per (slug, locale) — a\n\t// translation may still hold the old slug. Redirecting away from a URL\n\t// another row still answers on would take that page down: the redirect\n\t// middleware runs `order: \"pre\"`, so routing never gets a chance.\n\t// Any surviving row counts, published or not: a draft that publishes later\n\t// would otherwise be shadowed by the redirect.\n\tif (await slugStillTaken(db, collection, oldSlug, contentId)) return;\n\n\tconst collectionRow = await db\n\t\t.selectFrom(\"_emdash_collections\")\n\t\t.select(\"url_pattern\")\n\t\t.where(\"slug\", \"=\", collection)\n\t\t.executeTakeFirst();\n\n\tconst redirectRepo = new RedirectRepository(db);\n\tawait redirectRepo.createAutoRedirect(\n\t\tcollection,\n\t\toldSlug,\n\t\tnewSlug,\n\t\tcontentId,\n\t\tcollectionRow?.url_pattern ?? null,\n\t);\n\tinvalidateRedirectCache();\n}\n\n/** Whether a row other than `contentId` still holds `slug` in this collection. */\nasync function slugStillTaken(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tslug: string,\n\tcontentId: string,\n): Promise<boolean> {\n\tvalidateIdentifier(collection, \"collection slug\");\n\tconst result = await sql<{ id: string }>`\n\t\tSELECT id FROM ${sql.ref(`ec_${collection}`)}\n\t\tWHERE slug = ${slug}\n\t\tAND id != ${contentId}\n\t\tAND deleted_at IS NULL\n\t\tLIMIT 1\n\t`.execute(db);\n\treturn result.rows.length > 0;\n}\n\n/** Matches a date-only `YYYY-MM-DD` bound (no time component). */\nconst DATE_ONLY_RE = /^\\d{4}-\\d{2}-\\d{2}$/;\n\n/**\n * Normalize a date-range bound to an ISO datetime for lexicographic comparison\n * against stored ISO 8601 timestamps. A bare `YYYY-MM-DD` is widened to the\n * appropriate UTC day boundary so the range stays inclusive: a `start` bound\n * becomes the start of the day and an `end` bound the end of the day.\n * Otherwise a date-only upper bound would exclude every same-day row (since\n * `2024-06-01T12:00:00Z` sorts after `2024-06-01`). Full datetimes pass\n * through unchanged.\n */\nfunction normalizeDateBound(value: string | undefined, edge: \"start\" | \"end\"): string | undefined {\n\tif (!value) return undefined;\n\tif (!DATE_ONLY_RE.test(value)) return value;\n\treturn edge === \"start\" ? `${value}T00:00:00.000Z` : `${value}T23:59:59.999Z`;\n}\n\n/**\n * Build the repository's byline filter from the wire params.\n *\n * `locale` is the locale the list is scoped to, which is the locale an\n * inferred credit has to resolve at — the admin list is always scoped to the\n * locale picked in its switcher.\n */\nfunction resolveBylineFilter(\n\tparams: { bylines?: string[]; bylinesNone?: boolean; includeInferredBylines?: boolean },\n\tlocale: string | undefined,\n): ContentBylineFilter | undefined {\n\tconst includeInferred = params.includeInferredBylines === true;\n\n\tif (params.bylinesNone) return { mode: \"none\", includeInferred, locale };\n\n\tconst bylineIds = params.bylines ?? [];\n\tif (bylineIds.length === 0) return undefined;\n\n\treturn { mode: \"any\", bylineIds, includeInferred, locale };\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\tq?: string;\n\t\tauthorId?: string;\n\t\tdateField?: ContentDateField;\n\t\tdateFrom?: string;\n\t\tdateTo?: string;\n\t\tbylines?: string[];\n\t\tbylinesNone?: boolean;\n\t\tincludeInferredBylines?: boolean;\n\t\tfieldFilters?: ContentFieldFilters;\n\t},\n): Promise<ApiResult<ContentListResponse>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst where: FindManyOptions[\"where\"] = {};\n\t\tif (params.status) where.status = params.status;\n\t\tconst locale = params.locale ? resolveConfiguredLocale(params.locale) : undefined;\n\t\tif (locale) where.locale = locale;\n\t\tif (params.authorId) where.authorId = params.authorId;\n\t\tif (params.fieldFilters && Object.keys(params.fieldFilters).length > 0) {\n\t\t\twhere.fieldFilters = params.fieldFilters;\n\t\t}\n\n\t\tconst bylineFilter = resolveBylineFilter(params, locale);\n\t\tif (bylineFilter) where.bylineFilter = bylineFilter;\n\n\t\t// A date range requires a target column; ignore stray from/to without\n\t\t// a field so a half-specified filter doesn't silently drop all rows.\n\t\tif (params.dateField && (params.dateFrom || params.dateTo)) {\n\t\t\twhere.dateFilter = {\n\t\t\t\tfield: params.dateField,\n\t\t\t\tfrom: normalizeDateBound(params.dateFrom, \"start\"),\n\t\t\t\tto: normalizeDateBound(params.dateTo, \"end\"),\n\t\t\t};\n\t\t}\n\n\t\tconst q = params.q?.trim();\n\t\tif (q) {\n\t\t\twhere.q = q;\n\t\t\twhere.searchColumns = await resolveSearchColumns(db, collection);\n\t\t\twhere.useFts = await canUseFtsForListFilter(db, collection, where.searchColumns);\n\t\t}\n\n\t\t// Sorting by a non-system field (a collection's titleField/dateField)\n\t\t// needs the collection's *actual* sort fields resolved server-side,\n\t\t// so the orderBy set stays closed. Only query when it's not a system field.\n\t\tlet sortableExtras: string[] | undefined;\n\t\tif (params.orderBy && !isSystemOrderField(params.orderBy)) {\n\t\t\tconst coll = await db\n\t\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t\t.select([\"title_field\", \"date_field\"])\n\t\t\t\t.where(\"slug\", \"=\", collection)\n\t\t\t\t.executeTakeFirst();\n\t\t\tsortableExtras = [coll?.title_field, coll?.date_field].filter(\n\t\t\t\t(slug): slug is string => !!slug,\n\t\t\t);\n\t\t}\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\tsortableExtras,\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\ttotal: result.total,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tif (error instanceof ContentCollectionNotFoundError || isMissingTableError(error)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (isMissingColumnError(error, \"deleted_at\")) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"COLLECTION_SCHEMA_MISMATCH\",\n\t\t\t\t\tmessage: `Collection '${collection}' backing table is missing the 'deleted_at' column`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\t// e.g. invalid orderBy field\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"VALIDATION_ERROR\", message: error.message },\n\t\t\t};\n\t\t}\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/** A content author option for the admin author filter. */\nexport interface ContentAuthor {\n\tid: string;\n\tname: string | null;\n\temail: string;\n\tavatarUrl: string | null;\n}\n\n/**\n * List the distinct authors of a collection's live content.\n *\n * Backs the admin content-list author filter. Unlike `/admin/users` (ADMIN\n * only), this is gated on `content:read`, so any editor can filter by author.\n * Returns only users who have authored at least one non-trashed entry, sorted\n * by display name then email for a stable dropdown order.\n */\nexport async function handleContentAuthors(\n\tdb: Kysely<Database>,\n\tcollection: string,\n): Promise<ApiResult<{ items: ContentAuthor[] }>> {\n\ttry {\n\t\tconst repo = new ContentRepository(db);\n\t\tconst authorIds = await repo.findDistinctAuthorIds(collection);\n\t\tif (authorIds.length === 0) {\n\t\t\treturn { success: true, data: { items: [] } };\n\t\t}\n\n\t\tconst userRepo = new UserRepository(db);\n\t\tconst users = await userRepo.findByIds(authorIds);\n\n\t\tconst items: ContentAuthor[] = users\n\t\t\t.map((u) => ({ id: u.id, name: u.name, email: u.email, avatarUrl: u.avatarUrl }))\n\t\t\t.toSorted((a, b) => (a.name ?? a.email).localeCompare(b.name ?? b.email));\n\n\t\treturn { success: true, data: { items } };\n\t} catch (error) {\n\t\tif (isMissingTableError(error)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Content authors error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"CONTENT_AUTHORS_ERROR\",\n\t\t\t\tmessage: \"Failed to list content authors\",\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(\n\t\t\tcollection,\n\t\t\tid,\n\t\t\tlocale ? resolveConfiguredLocale(locale) : undefined,\n\t\t);\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(\n\t\t\tcollection,\n\t\t\tid,\n\t\t\tlocale ? resolveConfiguredLocale(locale) : undefined,\n\t\t);\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 | null;\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\ttaxonomies?: Record<string, string[]>;\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\tconst mimeCheck = await validateMediaFields(db, collection, body.data);\n\t\tif (!mimeCheck.success) return mimeCheck;\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// Default to the configured site locale rather than the repo's\n\t\t\t// hard-coded \"en\" — otherwise non-English default-locale sites\n\t\t\t// silently create entries in a locale the editor never chose.\n\t\t\tconst effectiveLocale = body.locale\n\t\t\t\t? resolveConfiguredLocale(body.locale)\n\t\t\t\t: getI18nConfig()?.defaultLocale;\n\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, effectiveLocale);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (body.status === \"published\") {\n\t\t\t\tconst publishConfig = await getCollectionPublishConfig(trx, collection);\n\t\t\t\trequireRoutablePublishSlug(publishConfig.routable, slug);\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: effectiveLocale,\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\tconst credits = await bylineRepo.setContentBylines(collection, created.id, body.bylines);\n\t\t\t\t// `setContentBylines` translates wire row ids to their\n\t\t\t\t// `translation_group` before writing. The response-shape\n\t\t\t\t// `primaryBylineId` must match what's now in the DB, so read\n\t\t\t\t// it from the returned credit (whose `byline` came from a\n\t\t\t\t// hydration round-trip).\n\t\t\t\tcreated.primaryBylineId = credits[0]?.byline.translationGroup ?? null;\n\t\t\t}\n\n\t\t\t// Taxonomy assignments already belong to the content translation\n\t\t\t// group. Byline credits remain per content row and need copying.\n\t\t\t// Explicit `body.bylines` wins — `copyContentBylines` no-ops\n\t\t\t// when the target already has credits, but the cleaner guard\n\t\t\t// is to skip the call entirely.\n\t\t\tif (body.translationOf) {\n\t\t\t\tif (body.bylines === undefined) {\n\t\t\t\t\tawait bylineRepo.copyContentBylines(collection, body.translationOf, created.id);\n\t\t\t\t\t// `copyContentBylines` writes the source's primary\n\t\t\t\t\t// pointer onto the new row; reflect it in-memory so the\n\t\t\t\t\t// response includes it before hydrateBylines runs.\n\t\t\t\t\tconst source = await repo.findById(collection, body.translationOf);\n\t\t\t\t\tif (source) created.primaryBylineId = source.primaryBylineId;\n\t\t\t\t}\n\t\t\t}\n\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\t// Attach taxonomy terms in the same transaction. The MCP tool\n\t\t\t// (and the REST create body) previously accepted a `taxonomies`\n\t\t\t// field on `content_create` without doing anything with it, so\n\t\t\t// agents publishing a categorized/tagged entry had to make N\n\t\t\t// follow-up REST calls per taxonomy. This resolves each slug in\n\t\t\t// the entry's locale and pipes it through the same\n\t\t\t// `setTermsForEntry` path the `.../terms/{taxonomy}` REST route\n\t\t\t// uses, so the two entry points can't drift.\n\t\t\tif (body.taxonomies) {\n\t\t\t\tawait assignTaxonomies(trx, collection, created.id, effectiveLocale, body.taxonomies);\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\tif (isMissingTableError(error)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"VALIDATION_ERROR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\t// SQLite UNIQUE constraint OR Postgres unique_violation — slug\n\t\t// collisions and any other unique violations land here. Match\n\t\t// specifically on \"unique constraint failed\" / \"duplicate key\" so we\n\t\t// don't false-positive on NOT NULL or CHECK violations whose\n\t\t// messages also contain \"constraint failed\".\n\t\tconst message = error instanceof Error ? error.message.toLowerCase() : \"\";\n\t\tif (message.includes(\"unique constraint failed\") || message.includes(\"duplicate key\")) {\n\t\t\t// Detect slug-specific collisions by message fingerprint\n\t\t\tif (message.includes(\"slug\")) {\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: \"SLUG_CONFLICT\",\n\t\t\t\t\t\tmessage: `Slug '${body.slug ?? \"(auto-generated)\"}' already exists in collection '${collection}'`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CONFLICT\",\n\t\t\t\t\tmessage: \"Unique constraint violation\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\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 | null;\n\t\tstatus?: string;\n\t\tauthorId?: string | null;\n\t\tbylines?: ContentBylineInput[];\n\t\tlocale?: string;\n\t\t_rev?: string;\n\t\tseo?: ContentSeoInput;\n\t\ttaxonomies?: Record<string, string[]>;\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\tif (body.data) {\n\t\t\tconst mimeCheck = await validateMediaFields(db, collection, body.data);\n\t\t\tif (!mimeCheck.success) return mimeCheck;\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, body.locale)) ?? 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 !== undefined || body.status === \"published\"\n\t\t\t\t\t? await trxRepo.findById(collection, resolvedId)\n\t\t\t\t\t: 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 resultingStatus = body.status ?? existing?.status;\n\t\t\tif (resultingStatus === \"published\") {\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\t\t\t\tconst publishConfig = await getCollectionPublishConfig(trx, collection);\n\t\t\t\tconst intendedSlug = body.slug !== undefined ? body.slug : existing.slug;\n\t\t\t\trequireRoutablePublishSlug(publishConfig.routable, intendedSlug);\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\tpublishedAt: body.publishedAt,\n\t\t\t});\n\n\t\t\tif (body.bylines !== undefined) {\n\t\t\t\tconst credits = await bylineRepo.setContentBylines(collection, resolvedId, body.bylines);\n\t\t\t\t// `setContentBylines` translates wire row ids to their\n\t\t\t\t// `translation_group` before writing. Read the in-memory\n\t\t\t\t// pointer from the persisted credit so the response shape\n\t\t\t\t// matches the DB. See the matching block in handleContentCreate.\n\t\t\t\tupdated.primaryBylineId = credits[0]?.byline.translationGroup ?? 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\tawait createSlugChangeRedirect(trx, collection, oldSlug, body.slug, resolvedId);\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\t// Replace taxonomy assignments in the same transaction. Uses the\n\t\t\t// entry's own locale (post-update) to resolve slugs so an update\n\t\t\t// that also changes locale still lands on the correct term\n\t\t\t// variants. See handleContentCreate for rationale.\n\t\t\tif (body.taxonomies) {\n\t\t\t\tawait assignTaxonomies(\n\t\t\t\t\ttrx,\n\t\t\t\t\tcollection,\n\t\t\t\t\tresolvedId,\n\t\t\t\t\tupdated.locale ?? body.locale,\n\t\t\t\t\tbody.taxonomies,\n\t\t\t\t);\n\t\t\t}\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 (hasApiError(error)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: error.apiError.code, message: error.message },\n\t\t\t};\n\t\t}\n\t\tif (isMissingTableError(error)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"VALIDATION_ERROR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tconst message = error instanceof Error ? error.message.toLowerCase() : \"\";\n\t\tif (message.includes(\"unique constraint failed\") || message.includes(\"duplicate key\")) {\n\t\t\tif (message.includes(\"slug\")) {\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: \"SLUG_CONFLICT\",\n\t\t\t\t\t\tmessage: `Slug '${body.slug ?? id}' already exists in collection '${collection}'`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CONFLICT\",\n\t\t\t\t\tmessage: \"Unique constraint violation\",\n\t\t\t\t},\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; id: string }>> {\n\ttry {\n\t\tconst result = 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 {\n\t\t\t\tid: resolvedId,\n\t\t\t\tdeleted: await repo.delete(collection, resolvedId),\n\t\t\t};\n\t\t});\n\n\t\tif (!result.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, id: result.id },\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; item: ContentItem }>> {\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 resolveIdIncludingTrashed(repo, collection, id)) ?? id;\n\t\t\treturn repo.restore(collection, resolvedId);\n\t\t});\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: `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, item },\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; id: string }>> {\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, id: resolvedId },\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\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\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 existing = await repo.findByIdOrSlug(collection, id);\n\t\t\tconst resolvedId = existing?.id ?? id;\n\t\t\tif (existing) {\n\t\t\t\tconst publishConfig = await getCollectionPublishConfig(trx, collection);\n\t\t\t\trequireRoutablePublishSlug(publishConfig.routable, existing.slug);\n\t\t\t}\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\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 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 * Publication is one atomic content-row statement. On databases that support\n * transactions, the existing slug-redirect side write remains grouped with it.\n */\nexport async function handleContentPublish(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tid: string,\n\toptions: {\n\t\tpublishedAt?: string;\n\t\trequireScheduledDue?: boolean;\n\t\texpectedScheduledAt?: string;\n\t} = {},\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\tconst publishConfig = await getCollectionPublishConfig(trx, collection);\n\n\t\t\t// Capture the pre-publish state. For revision-supporting collections a\n\t\t\t// slug edit is staged as `_slug` in the draft revision and only lands\n\t\t\t// on the live `slug` column here, inside `repo.publish()` — it never\n\t\t\t// passes through handleContentUpdate, where slug-change auto-redirects\n\t\t\t// are normally created.\n\t\t\tconst existing = await repo.findById(collection, resolvedId);\n\n\t\t\tconst published = await repo.publish(\n\t\t\t\tcollection,\n\t\t\t\tresolvedId,\n\t\t\t\toptions.publishedAt,\n\t\t\t\toptions.requireScheduledDue,\n\t\t\t\toptions.expectedScheduledAt,\n\t\t\t\tpublishConfig.supportsRevisions,\n\t\t\t\tpublishConfig.routable,\n\t\t\t);\n\n\t\t\t// Leave a 301 behind when publishing changed the slug of an entry that\n\t\t\t// was already published — its old URL was live and may be indexed or\n\t\t\t// linked. A first publish is excluded: a draft's URL was never public.\n\t\t\tif (\n\t\t\t\texisting?.status === \"published\" &&\n\t\t\t\texisting.slug &&\n\t\t\t\tpublished.slug &&\n\t\t\t\texisting.slug !== published.slug\n\t\t\t) {\n\t\t\t\tawait createSlugChangeRedirect(trx, collection, existing.slug, published.slug, resolvedId);\n\t\t\t}\n\n\t\t\treturn published;\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 ContentMutationConflictError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CONFLICT\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\t// The scheduled sweep gates publish on the row still being due; a row\n\t\t// unscheduled in the meantime is a silent skip, not a failure.\n\t\tif (error instanceof ScheduledNotDueError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_DUE\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (error instanceof EmDashValidationError) {\n\t\t\t// The staged-slug pre-check tags its error so it maps to the same\n\t\t\t// 409 SLUG_CONFLICT as direct slug edits in create/update.\n\t\t\tconst details: unknown = error.details;\n\t\t\tconst isSlugConflict =\n\t\t\t\ttypeof details === \"object\" &&\n\t\t\t\tdetails !== null &&\n\t\t\t\t\"code\" in details &&\n\t\t\t\tdetails.code === \"SLUG_CONFLICT\";\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: isSlugConflict ? \"SLUG_CONFLICT\" : \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: error.message,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\t// Backstop for the pre-check inside repo.publish(): a concurrent write\n\t\t// can still take the slug between the check and the UPDATE, in which\n\t\t// case the `(slug, locale)` unique constraint fires. Same fingerprint\n\t\t// mapping as create/update — never a raw SQLite error to the client.\n\t\tconst message = error instanceof Error ? error.message.toLowerCase() : \"\";\n\t\tif (\n\t\t\t(message.includes(\"unique constraint failed\") || message.includes(\"duplicate key\")) &&\n\t\t\tmessage.includes(\"slug\")\n\t\t) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"SLUG_CONFLICT\",\n\t\t\t\t\tmessage: `The staged slug is already used by another entry in collection '${collection}'`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\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\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 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/**\n * Resolve a `{ taxonomyName: [slug, ...] }` map to term IDs and replace the\n * entry's assignments for each named taxonomy.\n *\n * Shared by handleContentCreate and handleContentUpdate so both MCP entry\n * points behave identically. Slug resolution is scoped to `locale`; passing\n * `undefined` lets `findBySlug` fall back to its default (lowest locale code)\n * so callers on single-locale sites don't need to know the site's default.\n *\n * Throws EmDashValidationError on unknown slug or wrong shape; the calling\n * handler translates that into a VALIDATION_ERROR response.\n */\nasync function assignTaxonomies(\n\ttrx: Kysely<Database>,\n\tcollection: string,\n\tentryId: string,\n\tlocale: string | undefined,\n\ttaxonomies: Record<string, string[]>,\n): Promise<void> {\n\tconst taxRepo = new TaxonomyRepository(trx);\n\tlet anyChange = false;\n\n\tfor (const [taxonomyName, slugs] of Object.entries(taxonomies)) {\n\t\tif (!Array.isArray(slugs)) {\n\t\t\tthrow new EmDashValidationError(`taxonomies.${taxonomyName} must be an array of term slugs`);\n\t\t}\n\n\t\tconst termIds: string[] = [];\n\t\tfor (const slug of slugs) {\n\t\t\tif (typeof slug !== \"string\" || slug.length === 0) {\n\t\t\t\tthrow new EmDashValidationError(\n\t\t\t\t\t`taxonomies.${taxonomyName} contains a non-string or empty slug`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst term = await taxRepo.findBySlug(taxonomyName, slug, locale);\n\t\t\tif (!term) {\n\t\t\t\tthrow new EmDashValidationError(\n\t\t\t\t\t`Unknown taxonomy term: ${taxonomyName}='${slug}'${\n\t\t\t\t\t\tlocale ? ` (locale '${locale}')` : \"\"\n\t\t\t\t\t}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\ttermIds.push(term.id);\n\t\t}\n\n\t\tawait taxRepo.setTermsForEntry(collection, entryId, taxonomyName, termIds);\n\t\tanyChange = true;\n\t}\n\n\t// Match the REST route's behaviour: taxonomy term assignments changed,\n\t// so invalidate the taxonomy object cache used during hydration.\n\tif (anyChange) invalidateTermCache();\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\troutable?: boolean;\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\troutable: definition.admin.routable ?? true,\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 { after } from \"../../after.js\";\nimport { ContentRepository } from \"../../database/repositories/content.js\";\nimport { RevisionRepository, type Revision } from \"../../database/repositories/revision.js\";\nimport { withTransaction } from \"../../database/transaction.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\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// Atomically update content and create a new revision to record the restore.\n\t\t// If either operation fails, neither is committed (on engines that support\n\t\t// transactions; on D1, withTransaction falls back to sequential execution).\n\t\tconst { item, queuedRevisionId } = await withTransaction(db, async (trx) => {\n\t\t\tconst trxContentRepo = new ContentRepository(trx);\n\t\t\tconst trxRevisionRepo = new RevisionRepository(trx);\n\n\t\t\tconst updated = await trxContentRepo.update(revision.collection, revision.entryId, {\n\t\t\t\tdata: fieldData,\n\t\t\t\tslug: typeof _slug === \"string\" ? _slug : undefined,\n\t\t\t});\n\n\t\t\tconst queuedRevision = await trxRevisionRepo.create({\n\t\t\t\tcollection: revision.collection,\n\t\t\t\tentryId: revision.entryId,\n\t\t\t\tdata: revision.data,\n\t\t\t\tauthorId: callerUserId,\n\t\t\t});\n\n\t\t\treturn { item: updated, queuedRevisionId: queuedRevision.id };\n\t\t});\n\n\t\tconst pruneRepo = new RevisionRepository(db);\n\t\tafter(async () => {\n\t\t\ttry {\n\t\t\t\tawait pruneRepo.pruneQueuedEntry(\n\t\t\t\t\trevision.collection,\n\t\t\t\t\trevision.entryId,\n\t\t\t\t\tqueuedRevisionId,\n\t\t\t\t\t50,\n\t\t\t\t);\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[revisions] Failed to prune revisions for ${revision.collection}/${revision.entryId}:`,\n\t\t\t\t\terror,\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_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 { InvalidCursorError } from \"../../database/repositories/types.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 | readonly string[];\n\t\tq?: 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\tq: params.q,\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 (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\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; storageKey: string }>> {\n\ttry {\n\t\tconst repo = new MediaRepository(db);\n\t\tconst storageKey = await repo.deleteWithStorageKey(id);\n\n\t\tif (!storageKey) {\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, storageKey },\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 * Plugin management handlers\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport type { SandboxedPluginEntry } from \"../../emdash-runtime.js\";\nimport { PluginStateRepository, type PluginState, type PluginStatus } from \"../../plugins/state.js\";\nimport type { ResolvedPlugin } from \"../../plugins/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\nexport interface PluginInfo {\n\tid: string;\n\tname: string;\n\tversion: string;\n\tpackage?: string;\n\tenabled: boolean;\n\tstatus: PluginStatus;\n\tsource?: \"config\" | \"marketplace\" | \"registry\";\n\t/** True for statically-sandboxed plugins (registered via `sandboxed: []`) */\n\tsandboxed?: boolean;\n\tmarketplaceVersion?: string;\n\t/** Publisher DID, for registry-source plugins */\n\tregistryPublisherDid?: string;\n\t/** Publisher slug, for registry-source plugins */\n\tregistrySlug?: string;\n\tcapabilities: string[];\n\thasAdminPages: boolean;\n\thasDashboardWidgets: boolean;\n\thasHooks: boolean;\n\t/** True when the plugin declares `admin.settingsSchema` (auto-generated settings form) */\n\thasSettings: boolean;\n\tinstalledAt?: string;\n\tactivatedAt?: string;\n\tdeactivatedAt?: string;\n\t/** Description of what the plugin does */\n\tdescription?: string;\n\t/** URL to the plugin icon on the marketplace */\n\ticonUrl?: string;\n\tmcpToolsEnabled: boolean;\n\tmcpTools: Array<{\n\t\tname: string;\n\t\tdescription: string;\n\t\troute: string;\n\t\tpermission: string;\n\t\tdestructive: boolean;\n\t}>;\n}\n\nexport interface PluginListResponse {\n\titems: PluginInfo[];\n}\n\nexport interface PluginResponse {\n\titem: PluginInfo;\n}\n\nfunction marketplaceIconUrl(marketplaceUrl: string, pluginId: string): string {\n\treturn `${marketplaceUrl}/api/v1/plugins/${encodeURIComponent(pluginId)}/icon`;\n}\n\n/**\n * Get plugin info from configured plugin and database state\n */\nfunction buildPluginInfo(\n\tplugin: ResolvedPlugin,\n\tstate: PluginState | null,\n\tmarketplaceUrl?: string,\n): PluginInfo {\n\t// If no state exists, plugin is considered active (default on first run)\n\tconst status = state?.status ?? \"active\";\n\tconst enabled = status === \"active\";\n\tconst isMarketplace = (state?.source ?? \"config\") === \"marketplace\";\n\n\treturn {\n\t\tid: plugin.id,\n\t\tname: state?.displayName || plugin.id,\n\t\tversion: plugin.version,\n\t\tpackage: undefined, // v2 doesn't have package field\n\t\tenabled,\n\t\tstatus,\n\t\tsource: state?.source ?? \"config\",\n\t\tmarketplaceVersion: state?.marketplaceVersion ?? undefined,\n\t\tregistryPublisherDid: state?.registryPublisherDid ?? undefined,\n\t\tregistrySlug: state?.registrySlug ?? undefined,\n\t\tcapabilities: plugin.capabilities,\n\t\thasAdminPages: (plugin.admin.pages?.length ?? 0) > 0,\n\t\thasDashboardWidgets: (plugin.admin.widgets?.length ?? 0) > 0,\n\t\thasHooks: Object.keys(plugin.hooks ?? {}).length > 0,\n\t\thasSettings: Object.keys(plugin.admin.settingsSchema ?? {}).length > 0,\n\t\tinstalledAt: state?.installedAt?.toISOString(),\n\t\tactivatedAt: state?.activatedAt?.toISOString() ?? undefined,\n\t\tdeactivatedAt: state?.deactivatedAt?.toISOString() ?? undefined,\n\t\tdescription: state?.description ?? undefined,\n\t\ticonUrl:\n\t\t\tisMarketplace && marketplaceUrl ? marketplaceIconUrl(marketplaceUrl, plugin.id) : undefined,\n\t\tmcpToolsEnabled: state?.mcpToolsEnabled ?? false,\n\t\tmcpTools: Object.entries(plugin.mcp?.tools ?? {}).flatMap(([name, tool]) => {\n\t\t\tconst permission = plugin.routes[tool.route]?.permission;\n\t\t\treturn permission\n\t\t\t\t? [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname,\n\t\t\t\t\t\t\tdescription: tool.description,\n\t\t\t\t\t\t\troute: tool.route,\n\t\t\t\t\t\t\tpermission,\n\t\t\t\t\t\t\tdestructive: tool.destructive ?? false,\n\t\t\t\t\t\t},\n\t\t\t\t\t]\n\t\t\t\t: [];\n\t\t}),\n\t};\n}\n\n/**\n * Build plugin info for a statically-sandboxed plugin entry\n */\nfunction buildSandboxedPluginInfo(\n\tentry: SandboxedPluginEntry,\n\tstate: PluginState | null,\n): PluginInfo {\n\tconst status = state?.status ?? \"active\";\n\tconst enabled = status === \"active\";\n\n\treturn {\n\t\tid: entry.id,\n\t\tname: state?.displayName || entry.id,\n\t\tversion: entry.version,\n\t\tpackage: undefined, // v2 doesn't have package field\n\t\tenabled,\n\t\tstatus,\n\t\tsource: \"config\",\n\t\tsandboxed: true,\n\t\tcapabilities: entry.capabilities,\n\t\thasAdminPages: (entry.adminPages?.length ?? 0) > 0,\n\t\thasDashboardWidgets: (entry.adminWidgets?.length ?? 0) > 0,\n\t\thasHooks: false,\n\t\thasSettings: Object.keys(entry.settingsSchema ?? {}).length > 0,\n\t\tinstalledAt: state?.installedAt?.toISOString(),\n\t\tactivatedAt: state?.activatedAt?.toISOString() ?? undefined,\n\t\tdeactivatedAt: state?.deactivatedAt?.toISOString() ?? undefined,\n\t\tdescription: state?.description ?? undefined,\n\t\tmcpToolsEnabled: state?.mcpToolsEnabled ?? false,\n\t\tmcpTools: entry.mcp?.tools.map(({ inputSchema: _, outputSchema: __, ...tool }) => tool) ?? [],\n\t};\n}\n\n/**\n * List all configured plugins with their state\n */\nexport async function handlePluginList(\n\tdb: Kysely<Database>,\n\tconfiguredPlugins: ResolvedPlugin[],\n\tsandboxedPluginEntries: SandboxedPluginEntry[],\n\tmarketplaceUrl?: string,\n\t/**\n\t * Settings-schema lookup for runtime-installed (marketplace/registry)\n\t * plugins, which aren't in either build-time list. Typically\n\t * `EmDashRuntime.getRuntimePluginSettingsSchema`.\n\t */\n\truntimeSettingsSchemaLookup?: (pluginId: string) => Record<string, unknown> | null,\n): Promise<ApiResult<PluginListResponse>> {\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst allStates = await stateRepo.getAll();\n\t\tconst stateMap = new Map(allStates.map((s) => [s.pluginId, s]));\n\n\t\tconst configuredIds = new Set(configuredPlugins.map((p) => p.id));\n\n\t\tconst items = configuredPlugins.map((plugin) => {\n\t\t\tconst state = stateMap.get(plugin.id) ?? null;\n\t\t\treturn buildPluginInfo(plugin, state, marketplaceUrl);\n\t\t});\n\n\t\t// Include statically-sandboxed plugins (registered via `sandboxed: []`\n\t\t// in astro.config.mjs).\n\t\tfor (const entry of sandboxedPluginEntries) {\n\t\t\tif (configuredIds.has(entry.id)) continue;\n\t\t\tconfiguredIds.add(entry.id);\n\t\t\titems.push(buildSandboxedPluginInfo(entry, stateMap.get(entry.id) ?? null));\n\t\t}\n\n\t\t// Include runtime-installed plugins (marketplace or registry) that\n\t\t// aren't in the configured plugins list.\n\t\tfor (const state of allStates) {\n\t\t\tif (state.source !== \"marketplace\" && state.source !== \"registry\") continue;\n\t\t\tif (configuredIds.has(state.pluginId)) continue;\n\n\t\t\titems.push({\n\t\t\t\tid: state.pluginId,\n\t\t\t\tname: state.displayName || state.pluginId,\n\t\t\t\tversion: state.marketplaceVersion ?? state.version,\n\t\t\t\tenabled: state.status === \"active\",\n\t\t\t\tstatus: state.status,\n\t\t\t\tsource: state.source,\n\t\t\t\tmarketplaceVersion: state.marketplaceVersion ?? undefined,\n\t\t\t\tregistryPublisherDid: state.registryPublisherDid ?? undefined,\n\t\t\t\tregistrySlug: state.registrySlug ?? undefined,\n\t\t\t\tcapabilities: [],\n\t\t\t\thasAdminPages: false,\n\t\t\t\thasDashboardWidgets: false,\n\t\t\t\thasHooks: false,\n\t\t\t\thasSettings: Object.keys(runtimeSettingsSchemaLookup?.(state.pluginId) ?? {}).length > 0,\n\t\t\t\tinstalledAt: state.installedAt?.toISOString(),\n\t\t\t\tactivatedAt: state.activatedAt?.toISOString() ?? undefined,\n\t\t\t\tdeactivatedAt: state.deactivatedAt?.toISOString() ?? undefined,\n\t\t\t\tdescription: state.description ?? undefined,\n\t\t\t\ticonUrl:\n\t\t\t\t\tstate.source === \"marketplace\" && marketplaceUrl\n\t\t\t\t\t\t? marketplaceIconUrl(marketplaceUrl, state.pluginId)\n\t\t\t\t\t\t: undefined,\n\t\t\t\tmcpToolsEnabled: state.mcpToolsEnabled,\n\t\t\t\tmcpTools: [],\n\t\t\t});\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: { items },\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"[plugins] list failed:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"PLUGIN_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list plugins\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Get a single plugin's info\n */\nexport async function handlePluginGet(\n\tdb: Kysely<Database>,\n\tconfiguredPlugins: ResolvedPlugin[],\n\tsandboxedPluginEntries: SandboxedPluginEntry[],\n\tpluginId: string,\n\tmarketplaceUrl?: string,\n): Promise<ApiResult<PluginResponse>> {\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst plugin = configuredPlugins.find((p) => p.id === pluginId);\n\n\t\tif (plugin) {\n\t\t\tconst state = await stateRepo.get(pluginId);\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: { item: buildPluginInfo(plugin, state, marketplaceUrl) },\n\t\t\t};\n\t\t}\n\n\t\tconst sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);\n\t\tif (sandboxed) {\n\t\t\tconst state = await stateRepo.get(pluginId);\n\t\t\treturn {\n\t\t\t\tsuccess: true,\n\t\t\t\tdata: { item: buildSandboxedPluginInfo(sandboxed, state) },\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\tmessage: `Plugin not found: ${pluginId}`,\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: \"PLUGIN_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get plugin\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Build a minimal `PluginInfo` for a plugin that exists only as a\n * `_plugin_state` row (marketplace or registry install), with no\n * matching `configuredPlugins` entry. Runtime-installed plugins don't\n * have ResolvedPlugin metadata until they're loaded into the sandbox,\n * so the enable/disable response surfaces the state-row view as a\n * stable shape the admin UI already understands.\n */\nfunction buildStateOnlyPluginInfo(\n\tstate: NonNullable<Awaited<ReturnType<PluginStateRepository[\"get\"]>>>,\n): PluginInfo {\n\treturn {\n\t\tid: state.pluginId,\n\t\tname: state.displayName || state.pluginId,\n\t\tversion: state.marketplaceVersion ?? state.version,\n\t\tenabled: state.status === \"active\",\n\t\tstatus: state.status,\n\t\tsource: state.source,\n\t\tmarketplaceVersion: state.marketplaceVersion ?? undefined,\n\t\tregistryPublisherDid: state.registryPublisherDid ?? undefined,\n\t\tregistrySlug: state.registrySlug ?? undefined,\n\t\tcapabilities: [],\n\t\thasAdminPages: false,\n\t\thasDashboardWidgets: false,\n\t\thasHooks: false,\n\t\thasSettings: false,\n\t\tinstalledAt: state.installedAt?.toISOString(),\n\t\tactivatedAt: state.activatedAt?.toISOString() ?? undefined,\n\t\tdeactivatedAt: state.deactivatedAt?.toISOString() ?? undefined,\n\t\tdescription: state.description ?? undefined,\n\t\tmcpToolsEnabled: state.mcpToolsEnabled,\n\t\tmcpTools: [],\n\t};\n}\n\n/**\n * Enable a plugin\n */\nexport async function handlePluginEnable(\n\tdb: Kysely<Database>,\n\tconfiguredPlugins: ResolvedPlugin[],\n\tsandboxedPluginEntries: SandboxedPluginEntry[],\n\tpluginId: string,\n): Promise<ApiResult<PluginResponse>> {\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst plugin = configuredPlugins.find((p) => p.id === pluginId);\n\n\t\t// Configured plugin: use its version as the source of truth.\n\t\tif (plugin) {\n\t\t\tconst state = await stateRepo.enable(pluginId, plugin.version);\n\t\t\treturn { success: true, data: { item: buildPluginInfo(plugin, state) } };\n\t\t}\n\n\t\t// Statically-sandboxed plugin: addressable via its build-time entry.\n\t\tconst sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);\n\t\tif (sandboxed) {\n\t\t\tconst state = await stateRepo.enable(pluginId, sandboxed.version);\n\t\t\treturn { success: true, data: { item: buildSandboxedPluginInfo(sandboxed, state) } };\n\t\t}\n\n\t\t// Runtime-installed plugin (marketplace or registry): only\n\t\t// addressable through the state row. Fall back to the existing\n\t\t// version recorded there.\n\t\tconst existing = await stateRepo.get(pluginId);\n\t\tif (!existing || (existing.source !== \"marketplace\" && existing.source !== \"registry\")) {\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\tconst enabled = await stateRepo.enable(pluginId, existing.version);\n\t\treturn { success: true, data: { item: buildStateOnlyPluginInfo(enabled) } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"PLUGIN_ENABLE_ERROR\",\n\t\t\t\tmessage: \"Failed to enable plugin\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Disable a plugin\n */\nexport async function handlePluginDisable(\n\tdb: Kysely<Database>,\n\tconfiguredPlugins: ResolvedPlugin[],\n\tsandboxedPluginEntries: SandboxedPluginEntry[],\n\tpluginId: string,\n): Promise<ApiResult<PluginResponse>> {\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst plugin = configuredPlugins.find((p) => p.id === pluginId);\n\n\t\tif (plugin) {\n\t\t\tconst state = await stateRepo.disable(pluginId, plugin.version);\n\t\t\treturn { success: true, data: { item: buildPluginInfo(plugin, state) } };\n\t\t}\n\n\t\tconst sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);\n\t\tif (sandboxed) {\n\t\t\tconst state = await stateRepo.disable(pluginId, sandboxed.version);\n\t\t\treturn { success: true, data: { item: buildSandboxedPluginInfo(sandboxed, state) } };\n\t\t}\n\n\t\tconst existing = await stateRepo.get(pluginId);\n\t\tif (!existing || (existing.source !== \"marketplace\" && existing.source !== \"registry\")) {\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\tconst disabled = await stateRepo.disable(pluginId, existing.version);\n\t\treturn { success: true, data: { item: buildStateOnlyPluginInfo(disabled) } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"PLUGIN_DISABLE_ERROR\",\n\t\t\t\tmessage: \"Failed to disable plugin\",\n\t\t\t},\n\t\t};\n\t}\n}\n","/**\n * Plugin settings handlers\n *\n * Auto-generated settings UI backend for plugins that declare\n * `admin.settingsSchema`. Values are stored in the options table under\n * `plugin:{pluginId}:settings:{key}` — the same keys the plugin itself\n * reads via `ctx.kv.get(\"settings:{key}\")`.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { OptionsRepository } from \"../../database/repositories/options.js\";\nimport { withTransaction } from \"../../database/transaction.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { SandboxedPluginEntry } from \"../../emdash-runtime.js\";\nimport type { ResolvedPlugin, SettingField } from \"../../plugins/types.js\";\nimport { ErrorCode } from \"../errors.js\";\nimport type { ApiResult } from \"../types.js\";\n\nexport interface PluginSettingsResponse {\n\t/** The plugin's declared settings schema, keyed by setting name */\n\tschema: Record<string, SettingField>;\n\t/**\n\t * Current values keyed by setting name. Secret fields are never\n\t * included here — check `secretsSet` instead.\n\t */\n\tvalues: Record<string, unknown>;\n\t/** For secret fields: whether a value is currently stored */\n\tsecretsSet: Record<string, boolean>;\n}\n\nfunction settingsKey(pluginId: string, key: string): string {\n\treturn `plugin:${pluginId}:settings:${key}`;\n}\n\n/**\n * Resolve a plugin's settings schema from either the configured\n * (in-process) plugin list or the statically-sandboxed entries.\n * Returns null when the plugin doesn't exist, undefined-equivalent\n * empty object when it declares no schema.\n */\nexport function getPluginSettingsSchema(\n\tconfiguredPlugins: ResolvedPlugin[],\n\tsandboxedPluginEntries: SandboxedPluginEntry[],\n\tpluginId: string,\n): Record<string, SettingField> | null {\n\tconst plugin = configuredPlugins.find((p) => p.id === pluginId);\n\tif (plugin) return plugin.admin.settingsSchema ?? {};\n\n\tconst sandboxed = sandboxedPluginEntries.find((e) => e.id === pluginId);\n\tif (sandboxed) return sandboxed.settingsSchema ?? {};\n\n\treturn null;\n}\n\n/**\n * Validate a single value against its schema field.\n * Returns an error message, or null when valid.\n */\nfunction validateValue(key: string, field: SettingField, value: unknown): string | null {\n\tswitch (field.type) {\n\t\tcase \"string\":\n\t\tcase \"secret\":\n\t\tcase \"url\":\n\t\tcase \"email\":\n\t\t\tif (typeof value !== \"string\") return `Setting \"${key}\" must be a string`;\n\t\t\tif (field.type === \"url\" && value !== \"\" && !URL.canParse(value)) {\n\t\t\t\treturn `Setting \"${key}\" must be a valid URL`;\n\t\t\t}\n\t\t\tif (field.type === \"email\" && value !== \"\" && !value.includes(\"@\")) {\n\t\t\t\treturn `Setting \"${key}\" must be a valid email address`;\n\t\t\t}\n\t\t\treturn null;\n\t\tcase \"number\": {\n\t\t\tif (typeof value !== \"number\" || Number.isNaN(value)) {\n\t\t\t\treturn `Setting \"${key}\" must be a number`;\n\t\t\t}\n\t\t\tif (field.min !== undefined && value < field.min) {\n\t\t\t\treturn `Setting \"${key}\" must be at least ${field.min}`;\n\t\t\t}\n\t\t\tif (field.max !== undefined && value > field.max) {\n\t\t\t\treturn `Setting \"${key}\" must be at most ${field.max}`;\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t\tcase \"boolean\":\n\t\t\treturn typeof value === \"boolean\" ? null : `Setting \"${key}\" must be a boolean`;\n\t\tcase \"select\":\n\t\t\tif (typeof value !== \"string\" || !field.options.some((o) => o.value === value)) {\n\t\t\t\treturn `Setting \"${key}\" must be one of the defined options`;\n\t\t\t}\n\t\t\treturn null;\n\t\tdefault: {\n\t\t\tconst _exhaustive: never = field;\n\t\t\treturn `Setting \"${key}\" has an unknown field type`;\n\t\t}\n\t}\n}\n\nasync function buildSettingsResponse(\n\toptionsRepo: OptionsRepository,\n\tpluginId: string,\n\tschema: Record<string, SettingField>,\n): Promise<PluginSettingsResponse> {\n\tconst keys = Object.keys(schema);\n\tconst stored = await optionsRepo.getMany(keys.map((key) => settingsKey(pluginId, key)));\n\n\tconst values: Record<string, unknown> = {};\n\tconst secretsSet: Record<string, boolean> = {};\n\n\tfor (const key of keys) {\n\t\tconst field = schema[key];\n\t\tif (!field) continue;\n\t\tconst storedValue = stored.get(settingsKey(pluginId, key));\n\n\t\tif (field.type === \"secret\") {\n\t\t\tsecretsSet[key] = typeof storedValue === \"string\" && storedValue.length > 0;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (storedValue !== undefined && storedValue !== null) {\n\t\t\tvalues[key] = storedValue;\n\t\t} else if (\"default\" in field && field.default !== undefined) {\n\t\t\tvalues[key] = field.default;\n\t\t} else {\n\t\t\tvalues[key] = null;\n\t\t}\n\t}\n\n\treturn { schema, values, secretsSet };\n}\n\n/**\n * Get a plugin's settings (schema + current values, secrets masked)\n */\nexport async function handlePluginSettingsGet(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n\tschema: Record<string, SettingField>,\n): Promise<ApiResult<PluginSettingsResponse>> {\n\ttry {\n\t\tconst optionsRepo = new OptionsRepository(db);\n\t\treturn { success: true, data: await buildSettingsResponse(optionsRepo, pluginId, schema) };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: ErrorCode.PLUGIN_SETTINGS_READ_ERROR,\n\t\t\t\tmessage: \"Failed to read plugin settings\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/**\n * Update a plugin's settings.\n *\n * Only keys present in `updates` are written. A `null` value deletes the\n * stored value (reverting to the schema default). Secret fields are\n * write-only: they accept a new string value or `null` to clear, and the\n * response never echoes them back.\n */\nexport async function handlePluginSettingsUpdate(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n\tschema: Record<string, SettingField>,\n\tupdates: Record<string, unknown>,\n): Promise<ApiResult<PluginSettingsResponse>> {\n\ttry {\n\t\t// Validate everything before writing anything.\n\t\tfor (const [key, value] of Object.entries(updates)) {\n\t\t\tconst field = schema[key];\n\t\t\tif (!field) {\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: ErrorCode.VALIDATION_ERROR,\n\t\t\t\t\t\tmessage: `Unknown setting \"${key}\" for plugin \"${pluginId}\"`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tif (value === null) continue;\n\t\t\tconst error = validateValue(key, field, value);\n\t\t\tif (error) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: { code: ErrorCode.VALIDATION_ERROR, message: error },\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Wrap the writes + read-back in a transaction so a partial failure\n\t\t// can't leave some settings updated and others not. On D1\n\t\t// withTransaction degrades to running the callback directly — D1 is\n\t\t// single-writer, so per-statement atomicity still holds.\n\t\tconst data = await withTransaction(db, async (trx) => {\n\t\t\tconst txRepo = new OptionsRepository(trx);\n\t\t\tfor (const [key, value] of Object.entries(updates)) {\n\t\t\t\tif (value === null) {\n\t\t\t\t\tawait txRepo.delete(settingsKey(pluginId, key));\n\t\t\t\t} else {\n\t\t\t\t\tawait txRepo.set(settingsKey(pluginId, key), value);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn buildSettingsResponse(txRepo, pluginId, schema);\n\t\t});\n\n\t\treturn { success: true, data };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: ErrorCode.PLUGIN_SETTINGS_UPDATE_ERROR,\n\t\t\t\tmessage: \"Failed to update plugin settings\",\n\t\t\t},\n\t\t};\n\t}\n}\n","/**\n * MarketplaceClient — HTTP client for the EmDash Plugin Marketplace\n *\n * Used by the install/update/proxy endpoints in EmDash core to communicate\n * with the marketplace Worker. The marketplace is a distribution channel,\n * not a runtime dependency — bundles are copied to site-local R2 at install time.\n */\n\nimport { createGzipDecoder, unpackTar } from \"modern-tar\";\n\nimport { pluginManifestSchema, reconcileManifestAccess } from \"./manifest-schema.js\";\nimport type { PluginManifest } from \"./types.js\";\n\n// ── Module-level regex patterns ───────────────────────────────────\n\nconst TRAILING_SLASHES = /\\/+$/;\nconst LEADING_DOT_SLASH = /^\\.\\//;\n\n// ── Types ──────────────────────────────────────────────────────────\n\nexport interface MarketplacePluginSummary {\n\tid: string;\n\tname: string;\n\tdescription: string | null;\n\tauthor: {\n\t\tname: string;\n\t\tverified: boolean;\n\t\tavatarUrl: string | null;\n\t};\n\tcapabilities: string[];\n\tkeywords: string[];\n\tinstallCount: number;\n\thasIcon: boolean;\n\ticonUrl: string;\n\tlatestVersion?: {\n\t\tversion: string;\n\t\taudit?: {\n\t\t\tverdict: string;\n\t\t\triskScore: number;\n\t\t};\n\t\timageAudit?: {\n\t\t\tverdict: string;\n\t\t};\n\t};\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nexport interface MarketplaceVersionSummary {\n\tversion: string;\n\tminEmDashVersion: string | null;\n\tbundleSize: number;\n\tchecksum: string;\n\tchangelog: string | null;\n\tcapabilities: string[];\n\tstatus: string;\n\tauditVerdict: string | null;\n\timageAuditVerdict: string | null;\n\tpublishedAt: string;\n}\n\nexport interface MarketplacePluginDetail extends MarketplacePluginSummary {\n\trepositoryUrl: string | null;\n\thomepageUrl: string | null;\n\tlicense: string | null;\n\tlatestVersion?: {\n\t\tversion: string;\n\t\tminEmDashVersion: string | null;\n\t\tbundleSize: number;\n\t\tchecksum: string;\n\t\tchangelog: string | null;\n\t\treadme: string | null;\n\t\thasIcon: boolean;\n\t\tscreenshotCount: number;\n\t\tscreenshotUrls: string[];\n\t\tcapabilities: string[];\n\t\tstatus: string;\n\t\taudit?: {\n\t\t\tverdict: string;\n\t\t\triskScore: number;\n\t\t};\n\t\timageAudit?: {\n\t\t\tverdict: string;\n\t\t};\n\t\tpublishedAt: string;\n\t};\n}\n\nexport interface MarketplaceSearchOpts {\n\tcategory?: string;\n\tcapability?: string;\n\tsort?: \"installs\" | \"updated\" | \"created\" | \"name\";\n\tcursor?: string;\n\tlimit?: number;\n}\n\nexport interface MarketplaceSearchResult {\n\titems: MarketplacePluginSummary[];\n\tnextCursor?: string;\n}\n\n// ── Theme types ───────────────────────────────────────────────────\n\nexport interface MarketplaceThemeSummary {\n\tid: string;\n\tname: string;\n\tdescription: string | null;\n\tauthor: {\n\t\tname: string;\n\t\tverified: boolean;\n\t\tavatarUrl: string | null;\n\t};\n\tkeywords: string[];\n\tpreviewUrl: string;\n\tdemoUrl: string | null;\n\thasThumbnail: boolean;\n\tthumbnailUrl: string | null;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nexport interface MarketplaceThemeDetail extends MarketplaceThemeSummary {\n\tauthor: {\n\t\tid: string;\n\t\tname: string;\n\t\tverified: boolean;\n\t\tavatarUrl: string | null;\n\t};\n\trepositoryUrl: string | null;\n\thomepageUrl: string | null;\n\tlicense: string | null;\n\tscreenshotCount: number;\n\tscreenshotUrls: string[];\n}\n\nexport interface MarketplaceThemeSearchOpts {\n\tkeyword?: string;\n\tsort?: \"name\" | \"created\" | \"updated\";\n\tcursor?: string;\n\tlimit?: number;\n}\n\nexport interface MarketplaceThemeSearchResult {\n\titems: MarketplaceThemeSummary[];\n\tnextCursor?: string;\n}\n\nexport interface PluginBundle {\n\tmanifest: PluginManifest;\n\tbackendCode: string;\n\tadminCode?: string;\n\tchecksum: string;\n}\n\n// ── Interface ──────────────────────────────────────────────────────\n\nexport interface MarketplaceClient {\n\t/** Search the marketplace catalog */\n\tsearch(query?: string, opts?: MarketplaceSearchOpts): Promise<MarketplaceSearchResult>;\n\n\t/** Get full plugin detail */\n\tgetPlugin(id: string): Promise<MarketplacePluginDetail>;\n\n\t/** Get version history for a plugin */\n\tgetVersions(id: string): Promise<MarketplaceVersionSummary[]>;\n\n\t/** Download and extract a plugin bundle */\n\tdownloadBundle(id: string, version: string): Promise<PluginBundle>;\n\n\t/** Fire-and-forget install stat (never throws) */\n\treportInstall(id: string, version: string): Promise<void>;\n\n\t/** Search theme listings */\n\tsearchThemes(\n\t\tquery?: string,\n\t\topts?: MarketplaceThemeSearchOpts,\n\t): Promise<MarketplaceThemeSearchResult>;\n\n\t/** Get full theme detail */\n\tgetTheme(id: string): Promise<MarketplaceThemeDetail>;\n}\n\n// ── Errors ─────────────────────────────────────────────────────────\n\nexport class MarketplaceError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic readonly status?: number,\n\t\tpublic readonly code?: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"MarketplaceError\";\n\t}\n}\n\nexport class MarketplaceUnavailableError extends MarketplaceError {\n\tconstructor(cause?: unknown) {\n\t\tsuper(\"Plugin marketplace is unavailable\", undefined, \"MARKETPLACE_UNAVAILABLE\");\n\t\tif (cause) this.cause = cause;\n\t}\n}\n\n// ── Implementation ─────────────────────────────────────────────────\n\nclass MarketplaceClientImpl implements MarketplaceClient {\n\tprivate readonly baseUrl: string;\n\tprivate readonly siteOrigin: string | undefined;\n\n\tconstructor(baseUrl: string, siteOrigin?: string) {\n\t\t// Strip trailing slash\n\t\tthis.baseUrl = baseUrl.replace(TRAILING_SLASHES, \"\");\n\t\tthis.siteOrigin = siteOrigin;\n\t}\n\n\tasync search(query?: string, opts?: MarketplaceSearchOpts): Promise<MarketplaceSearchResult> {\n\t\tconst params = new URLSearchParams();\n\t\tif (query) params.set(\"q\", query);\n\t\tif (opts?.category) params.set(\"category\", opts.category);\n\t\tif (opts?.capability) params.set(\"capability\", opts.capability);\n\t\tif (opts?.sort) params.set(\"sort\", opts.sort);\n\t\tif (opts?.cursor) params.set(\"cursor\", opts.cursor);\n\t\tif (opts?.limit) params.set(\"limit\", String(opts.limit));\n\n\t\tconst qs = params.toString();\n\t\tconst url = `${this.baseUrl}/api/v1/plugins${qs ? `?${qs}` : \"\"}`;\n\t\tconst data = await this.fetchJson<MarketplaceSearchResult>(url);\n\t\treturn data;\n\t}\n\n\tasync getPlugin(id: string): Promise<MarketplacePluginDetail> {\n\t\tconst url = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}`;\n\t\treturn this.fetchJson<MarketplacePluginDetail>(url);\n\t}\n\n\tasync getVersions(id: string): Promise<MarketplaceVersionSummary[]> {\n\t\tconst url = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}/versions`;\n\t\tconst data = await this.fetchJson<{ items: MarketplaceVersionSummary[] }>(url);\n\t\treturn data.items;\n\t}\n\n\tasync downloadBundle(id: string, version: string): Promise<PluginBundle> {\n\t\tconst bundleUrl = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}/versions/${encodeURIComponent(version)}/bundle`;\n\n\t\tconst marketplaceOrigin = new URL(this.baseUrl).origin;\n\t\tconst MAX_REDIRECTS = 5;\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tlet currentUrl = bundleUrl;\n\t\t\tresponse = await fetch(currentUrl, { redirect: \"manual\" });\n\n\t\t\t// Follow redirects manually, validating each target stays on the marketplace host\n\t\t\tfor (let i = 0; i < MAX_REDIRECTS; i++) {\n\t\t\t\tif (response.status < 300 || response.status >= 400) break;\n\n\t\t\t\tconst location = response.headers.get(\"location\");\n\t\t\t\tif (!location) break;\n\n\t\t\t\tconst target = new URL(location, currentUrl);\n\t\t\t\tif (target.origin !== marketplaceOrigin) {\n\t\t\t\t\tthrow new MarketplaceError(\n\t\t\t\t\t\t`Bundle download redirected to untrusted host: ${target.origin}`,\n\t\t\t\t\t\tresponse.status,\n\t\t\t\t\t\t\"BUNDLE_REDIRECT_UNTRUSTED\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tcurrentUrl = target.href;\n\t\t\t\tresponse = await fetch(currentUrl, { redirect: \"manual\" });\n\t\t\t}\n\n\t\t\t// If still a redirect after MAX_REDIRECTS, fail explicitly\n\t\t\tif (response.status >= 300 && response.status < 400) {\n\t\t\t\tthrow new MarketplaceError(\n\t\t\t\t\t`Bundle download exceeded maximum redirects (${MAX_REDIRECTS})`,\n\t\t\t\t\tresponse.status,\n\t\t\t\t\t\"BUNDLE_TOO_MANY_REDIRECTS\",\n\t\t\t\t);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tif (err instanceof MarketplaceError) throw err;\n\t\t\tthrow new MarketplaceUnavailableError(err);\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tthrow new MarketplaceError(\n\t\t\t\t`Failed to download bundle: ${response.status} ${response.statusText}`,\n\t\t\t\tresponse.status,\n\t\t\t\t\"BUNDLE_DOWNLOAD_FAILED\",\n\t\t\t);\n\t\t}\n\n\t\tconst tarballBytes = new Uint8Array(await response.arrayBuffer());\n\t\ttry {\n\t\t\treturn await extractBundle(tarballBytes);\n\t\t} catch (err) {\n\t\t\tif (err instanceof MarketplaceError) throw err;\n\t\t\tthrow new MarketplaceError(\n\t\t\t\t\"Failed to extract plugin bundle\",\n\t\t\t\tundefined,\n\t\t\t\t\"BUNDLE_EXTRACT_FAILED\",\n\t\t\t);\n\t\t}\n\t}\n\n\tasync reportInstall(id: string, version: string): Promise<void> {\n\t\t// Generate a stable site hash from the site origin (best-effort, non-identifying)\n\t\tconst siteHash = await generateSiteHash(this.siteOrigin);\n\t\tconst url = `${this.baseUrl}/api/v1/plugins/${encodeURIComponent(id)}/installs`;\n\n\t\ttry {\n\t\t\tawait fetch(url, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\t\t\tbody: JSON.stringify({ siteHash, version }),\n\t\t\t});\n\t\t} catch {\n\t\t\t// Fire-and-forget — never throw\n\t\t}\n\t}\n\n\tasync searchThemes(\n\t\tquery?: string,\n\t\topts?: MarketplaceThemeSearchOpts,\n\t): Promise<MarketplaceThemeSearchResult> {\n\t\tconst params = new URLSearchParams();\n\t\tif (query) params.set(\"q\", query);\n\t\tif (opts?.keyword) params.set(\"keyword\", opts.keyword);\n\t\tif (opts?.sort) params.set(\"sort\", opts.sort);\n\t\tif (opts?.cursor) params.set(\"cursor\", opts.cursor);\n\t\tif (opts?.limit) params.set(\"limit\", String(opts.limit));\n\n\t\tconst qs = params.toString();\n\t\tconst url = `${this.baseUrl}/api/v1/themes${qs ? `?${qs}` : \"\"}`;\n\t\treturn this.fetchJson<MarketplaceThemeSearchResult>(url);\n\t}\n\n\tasync getTheme(id: string): Promise<MarketplaceThemeDetail> {\n\t\tconst url = `${this.baseUrl}/api/v1/themes/${encodeURIComponent(id)}`;\n\t\treturn this.fetchJson<MarketplaceThemeDetail>(url);\n\t}\n\n\tprivate async fetchJson<T>(url: string): Promise<T> {\n\t\tlet response: Response;\n\t\ttry {\n\t\t\tresponse = await fetch(url, {\n\t\t\t\theaders: { Accept: \"application/json\" },\n\t\t\t});\n\t\t} catch (err) {\n\t\t\tthrow new MarketplaceUnavailableError(err);\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tlet errorMessage = `Marketplace request failed: ${response.status}`;\n\t\t\ttry {\n\t\t\t\tconst body: { error?: string } = await response.json();\n\t\t\t\tif (body.error) errorMessage = body.error;\n\t\t\t} catch {\n\t\t\t\t// use default message\n\t\t\t}\n\t\t\tthrow new MarketplaceError(errorMessage, response.status);\n\t\t}\n\n\t\tconst data: T = await response.json();\n\t\treturn data;\n\t}\n}\n\n// ── Bundle extraction ──────────────────────────────────────────────\n\n/**\n * Extract manifest + code files from a tarball.\n *\n * The tarball is a gzipped tar archive containing:\n * - manifest.json\n * - backend.js\n * - admin.js (optional)\n *\n * We use a minimal tar parser since we only need to read a few small files.\n */\n/**\n * Exported so the experimental registry install handler can reuse the\n * same parse / validate / hash primitive. Despite the file name, this\n * function predates the marketplace-vs-registry split and is generic\n * over plugin bundle tarballs regardless of distribution channel.\n */\n// Aligns with RFC 0001 §\"Bundle size limits\" (256 KiB decompressed,\n// 20 files). Matches `MAX_BUNDLE_SIZE` in cli/commands/bundle-utils.ts\n// (the publish-side cap). We don't import that constant to keep this\n// runtime module independent of the CLI; the two values are\n// load-bearing identical and must stay in sync.\n//\n// Tar adds per-file headers (~512 bytes each) plus directory entries,\n// so the entry count cap is set comfortably above RFC's 20-file limit.\n// Going over either is a strong signal the bundle isn't a legitimate\n// sandboxed plugin.\nconst MAX_DECOMPRESSED_BUNDLE_BYTES = 256 * 1024;\nconst MAX_BUNDLE_TAR_ENTRIES = 32;\n\nexport async function extractBundle(tarballBytes: Uint8Array): Promise<PluginBundle> {\n\t// Decompress fully into memory first, then parse the tar.\n\t// Passing a pipeThrough() stream directly to unpackTar causes a backpressure\n\t// deadlock in workerd: the tar decoder's body-stream pull() needs more\n\t// decompressed data, but the upstream pipe is stalled waiting for the\n\t// decoder's writable side to drain — a circular dependency.\n\tconst decompressedStream = new ReadableStream<Uint8Array>({\n\t\tstart(controller) {\n\t\t\tcontroller.enqueue(tarballBytes);\n\t\t\tcontroller.close();\n\t\t},\n\t}).pipeThrough(createGzipDecoder());\n\n\t// Collect decompressed bytes with a hard cap. A gzip-bomb -- a small\n\t// tarball that decompresses to gigabytes -- otherwise exhausts\n\t// worker / Node memory before we know to reject it. The cap matches\n\t// RFC 0001's publish-time bundle size limit (MAX_DECOMPRESSED_BUNDLE_BYTES);\n\t// anything past that isn't a legitimate sandboxed plugin.\n\tconst reader = decompressedStream.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\twhile (true) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\tif (!value) continue;\n\t\ttotal += value.byteLength;\n\t\tif (total > MAX_DECOMPRESSED_BUNDLE_BYTES) {\n\t\t\ttry {\n\t\t\t\tawait reader.cancel();\n\t\t\t} catch {\n\t\t\t\t// nothing to do\n\t\t\t}\n\t\t\tthrow new MarketplaceError(\n\t\t\t\t`Bundle decompressed size exceeds limit (${MAX_DECOMPRESSED_BUNDLE_BYTES} bytes)`,\n\t\t\t\tundefined,\n\t\t\t\t\"INVALID_BUNDLE\",\n\t\t\t);\n\t\t}\n\t\tchunks.push(value);\n\t}\n\tconst decompressedBytes = new Uint8Array(total);\n\t{\n\t\tlet offset = 0;\n\t\tfor (const chunk of chunks) {\n\t\t\tdecompressedBytes.set(chunk, offset);\n\t\t\toffset += chunk.byteLength;\n\t\t}\n\t}\n\n\tconst decompressed = new ReadableStream<Uint8Array>({\n\t\tstart(controller) {\n\t\t\tcontroller.enqueue(decompressedBytes);\n\t\t\tcontroller.close();\n\t\t},\n\t});\n\n\tconst entries = await unpackTar(decompressed);\n\tif (entries.length > MAX_BUNDLE_TAR_ENTRIES) {\n\t\tthrow new MarketplaceError(\n\t\t\t`Bundle has too many tar entries (${entries.length} > ${MAX_BUNDLE_TAR_ENTRIES})`,\n\t\t\tundefined,\n\t\t\t\"INVALID_BUNDLE\",\n\t\t);\n\t}\n\n\tconst decoder = new TextDecoder();\n\tconst files = new Map<string, string>();\n\tfor (const entry of entries) {\n\t\tif (entry.data && entry.header.type === \"file\") {\n\t\t\t// Strip leading ./ prefix that tar tools commonly add\n\t\t\tconst name = entry.header.name.replace(LEADING_DOT_SLASH, \"\");\n\t\t\tfiles.set(name, decoder.decode(entry.data));\n\t\t}\n\t}\n\n\tconst manifestJson = files.get(\"manifest.json\");\n\tconst backendCode = files.get(\"backend.js\");\n\n\tif (!manifestJson) {\n\t\tthrow new MarketplaceError(\n\t\t\t\"Invalid bundle: missing manifest.json\",\n\t\t\tundefined,\n\t\t\t\"INVALID_BUNDLE\",\n\t\t);\n\t}\n\tif (!backendCode) {\n\t\tthrow new MarketplaceError(\"Invalid bundle: missing backend.js\", undefined, \"INVALID_BUNDLE\");\n\t}\n\n\tlet manifest: PluginManifest;\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(manifestJson);\n\t\tconst result = pluginManifestSchema.safeParse(parsed);\n\t\tif (!result.success) {\n\t\t\tthrow new MarketplaceError(\n\t\t\t\t\"Invalid bundle: manifest.json failed validation\",\n\t\t\t\tundefined,\n\t\t\t\t\"INVALID_BUNDLE\",\n\t\t\t);\n\t\t}\n\t\tmanifest = reconcileManifestAccess(result.data);\n\t} catch (err) {\n\t\tif (err instanceof MarketplaceError) throw err;\n\t\tthrow new MarketplaceError(\n\t\t\t\"Invalid bundle: malformed manifest.json\",\n\t\t\tundefined,\n\t\t\t\"INVALID_BUNDLE\",\n\t\t);\n\t}\n\n\t// Compute SHA-256 checksum of the tarball for verification\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- Uint8Array is a valid BufferSource at runtime; TS lib mismatch\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", tarballBytes as unknown as BufferSource);\n\tconst hashArray = new Uint8Array(hashBuffer);\n\tconst checksum = Array.from(hashArray, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n\n\treturn {\n\t\tmanifest,\n\t\tbackendCode,\n\t\tadminCode: files.get(\"admin.js\"),\n\t\tchecksum,\n\t};\n}\n\n// ── Helpers ────────────────────────────────────────────────────────\n\n/**\n * Generate a stable non-identifying site hash from the site origin.\n * The same origin always produces the same hash, so the marketplace\n * installs table deduplicates correctly per (plugin_id, site_hash).\n */\nasync function generateSiteHash(siteOrigin?: string): Promise<string> {\n\tconst seed = siteOrigin ? `emdash-site:${siteOrigin}` : `emdash-anonymous`;\n\ttry {\n\t\tconst hash = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(seed));\n\t\tconst arr = new Uint8Array(hash);\n\t\treturn Array.from(arr.slice(0, 8), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n\t} catch {\n\t\t// Fallback for environments without crypto.subtle: FNV-1a hash encoded as hex.\n\t\t// Deterministic, uniform distribution, no origin leakage.\n\t\tlet h = 0x811c9dc5;\n\t\tfor (let i = 0; i < seed.length; i++) {\n\t\t\th ^= seed.charCodeAt(i);\n\t\t\th = Math.imul(h, 0x01000193);\n\t\t}\n\t\tconst h2 = h ^ (h >>> 16);\n\t\treturn (h >>> 0).toString(16).padStart(8, \"0\") + (h2 >>> 0).toString(16).padStart(8, \"0\");\n\t}\n}\n\n// ── Factory ────────────────────────────────────────────────────────\n\n/**\n * Create a MarketplaceClient for the given marketplace URL.\n *\n * @param baseUrl - The marketplace API base URL (e.g. \"https://marketplace.emdashcms.com\")\n * @param siteOrigin - The origin of the EmDash site (e.g. \"https://myblog.example.com\").\n *   Used to generate a stable, non-identifying site hash for install deduplication.\n */\nexport function createMarketplaceClient(baseUrl: string, siteOrigin?: string): MarketplaceClient {\n\treturn new MarketplaceClientImpl(baseUrl, siteOrigin);\n}\n","/**\n * Plugin Storage Index Management\n *\n * Manages expression indexes on the _plugin_storage table for efficient queries.\n *\n * @see PLUGIN-SYSTEM.md § Plugin Storage > Index Management\n */\n\nimport type { Kysely, RawBuilder } from \"kysely\";\nimport { sql } from \"kysely\";\n\nimport { jsonExtractExpr, isPostgres } from \"../database/dialect-helpers.js\";\nimport type { Database } from \"../database/types.js\";\nimport {\n\tvalidateJsonFieldName,\n\tvalidatePluginIdentifier,\n\tvalidateStorageCollectionName,\n} from \"../database/validate.js\";\n\n/**\n * Generate a deterministic index name.\n * Unique indexes use a `uidx_` prefix to avoid collisions with regular indexes on the same fields.\n */\nexport function generateIndexName(\n\tpluginId: string,\n\tcollection: string,\n\tfields: string[],\n\toptions?: { unique?: boolean },\n): string {\n\tconst prefix = options?.unique ? \"uidx\" : \"idx\";\n\tconst fieldPart = fields.join(\"_\");\n\t// SQLite index names have no length limit, but keep it reasonable\n\treturn `${prefix}_plugin_${pluginId}_${collection}_${fieldPart}`.substring(0, 128);\n}\n\n/**\n * Generate a Kysely sql expression for creating an expression index.\n *\n * Validates all inputs before interpolation. The collection uses the\n * permissive manifest-key rules rather than SQL-identifier rules — it is\n * stored as opaque text and only reaches SQL inside the generated index\n * name — so kebab-case collections index like any other.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function generateCreateIndexSql(\n\tdb: Kysely<any>,\n\tpluginId: string,\n\tcollection: string,\n\tfields: string[],\n\toptions?: { unique?: boolean },\n): RawBuilder<unknown> {\n\tvalidatePluginIdentifier(pluginId, \"plugin ID\");\n\tvalidateStorageCollectionName(collection, \"collection name\");\n\tfor (const field of fields) {\n\t\tvalidateJsonFieldName(field, \"index field name\");\n\t}\n\n\tconst indexName = generateIndexName(pluginId, collection, fields, options);\n\n\t// Build the indexed expressions\n\t// Fields are validated above, safe to interpolate into json path\n\tconst expressions = fields\n\t\t.map((field) => {\n\t\t\tif (isPostgres(db)) {\n\t\t\t\t// Postgres expression indexes need parens around the expression\n\t\t\t\treturn `(${jsonExtractExpr(db, \"data\", field)})`;\n\t\t\t}\n\t\t\treturn jsonExtractExpr(db, \"data\", field);\n\t\t})\n\t\t.join(\", \");\n\n\t// Composite non-partial index: the leading (plugin_id, collection) columns\n\t// scope it per plugin/collection — including unique-index semantics — and\n\t// let it serve the repository's bound-parameter WHERE plus the JSON\n\t// expression ORDER BY. A partial index (WHERE plugin_id = 'x' AND\n\t// collection = 'y') is never chosen by SQLite under bound parameters\n\t// unless ANALYZE has run, and D1 never runs ANALYZE.\n\tconst createKeyword = options?.unique ? \"CREATE UNIQUE INDEX\" : \"CREATE INDEX\";\n\treturn sql`${sql.raw(createKeyword)} IF NOT EXISTS ${sql.ref(indexName)}\n\t\tON _plugin_storage(plugin_id, collection, ${sql.raw(expressions)})\n\t`;\n}\n\n/**\n * Generate a Kysely sql expression for dropping an index.\n *\n * Uses sql.ref() for safe identifier quoting.\n */\nexport function generateDropIndexSql(indexName: string): RawBuilder<unknown> {\n\treturn sql`DROP INDEX IF EXISTS ${sql.ref(indexName)}`;\n}\n\n/**\n * Normalize index declarations to arrays of field arrays\n */\nexport function normalizeIndexes(indexes: Array<string | string[]>): string[][] {\n\treturn indexes.map((index) => (Array.isArray(index) ? index : [index]));\n}\n\n/**\n * Create all declared indexes for a plugin collection\n */\nexport async function createStorageIndexes(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n\tcollection: string,\n\tindexes: Array<string | string[]>,\n\toptions?: { uniqueIndexes?: Array<string | string[]> },\n): Promise<{\n\tcreated: string[];\n\terrors: Array<{ index: string; error: string }>;\n}> {\n\tconst normalized = normalizeIndexes(indexes);\n\tconst uniqueNormalized = options?.uniqueIndexes ? normalizeIndexes(options.uniqueIndexes) : [];\n\tconst uniqueSet = new Set(uniqueNormalized.map((f) => f.join(\",\")));\n\n\t// Deduplicate: if fields appear in both indexes and uniqueIndexes, only create the unique version\n\tconst deduped = normalized.filter((f) => !uniqueSet.has(f.join(\",\")));\n\tconst allEntries: Array<{ fields: string[]; unique: boolean }> = [\n\t\t...deduped.map((fields) => ({ fields, unique: false })),\n\t\t...uniqueNormalized.map((fields) => ({ fields, unique: true })),\n\t];\n\n\tconst created: string[] = [];\n\tconst errors: Array<{ index: string; error: string }> = [];\n\n\tfor (const entry of allEntries) {\n\t\tconst { fields } = entry;\n\t\tconst indexName = generateIndexName(pluginId, collection, fields, { unique: entry.unique });\n\n\t\ttry {\n\t\t\t// Create the index\n\t\t\tconst createSql = generateCreateIndexSql(db, pluginId, collection, fields, {\n\t\t\t\tunique: entry.unique,\n\t\t\t});\n\t\t\tawait createSql.execute(db);\n\n\t\t\t// Track in _plugin_indexes table\n\t\t\tawait db\n\t\t\t\t.insertInto(\"_plugin_indexes\")\n\t\t\t\t.values({\n\t\t\t\t\tplugin_id: pluginId,\n\t\t\t\t\tcollection,\n\t\t\t\t\tindex_name: indexName,\n\t\t\t\t\tfields: JSON.stringify(fields),\n\t\t\t\t})\n\t\t\t\t.onConflict((oc) =>\n\t\t\t\t\toc\n\t\t\t\t\t\t.columns([\"plugin_id\", \"collection\", \"index_name\"])\n\t\t\t\t\t\t.doUpdateSet({ fields: JSON.stringify(fields) }),\n\t\t\t\t)\n\t\t\t\t.execute();\n\n\t\t\tcreated.push(indexName);\n\t\t} catch (error) {\n\t\t\terrors.push({\n\t\t\t\tindex: indexName,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t}\n\t}\n\n\treturn { created, errors };\n}\n\n/**\n * Remove indexes that are no longer declared\n */\nexport async function removeOrphanedIndexes(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n\tcollection: string,\n\tcurrentIndexes: Array<string | string[]>,\n\toptions?: { uniqueIndexes?: Array<string | string[]> },\n): Promise<{\n\tremoved: string[];\n\terrors: Array<{ index: string; error: string }>;\n}> {\n\tconst normalized = normalizeIndexes(currentIndexes);\n\tconst uniqueNormalized = options?.uniqueIndexes ? normalizeIndexes(options.uniqueIndexes) : [];\n\tconst uniqueSet = new Set(uniqueNormalized.map((f) => f.join(\",\")));\n\n\t// Build the set of expected index names (regular + unique with correct prefix)\n\tconst currentIndexNames = new Set<string>();\n\tfor (const fields of normalized) {\n\t\t// If this field set is in both, only the unique version exists (deduplication in create)\n\t\tif (!uniqueSet.has(fields.join(\",\"))) {\n\t\t\tcurrentIndexNames.add(generateIndexName(pluginId, collection, fields));\n\t\t}\n\t}\n\tfor (const fields of uniqueNormalized) {\n\t\tcurrentIndexNames.add(generateIndexName(pluginId, collection, fields, { unique: true }));\n\t}\n\n\t// Get existing indexes from tracking table\n\tconst existingIndexes = await db\n\t\t.selectFrom(\"_plugin_indexes\")\n\t\t.select([\"index_name\"])\n\t\t.where(\"plugin_id\", \"=\", pluginId)\n\t\t.where(\"collection\", \"=\", collection)\n\t\t.execute();\n\n\tconst removed: string[] = [];\n\tconst errors: Array<{ index: string; error: string }> = [];\n\n\tfor (const { index_name } of existingIndexes) {\n\t\tif (!currentIndexNames.has(index_name)) {\n\t\t\ttry {\n\t\t\t\t// Drop the index\n\t\t\t\tawait generateDropIndexSql(index_name).execute(db);\n\n\t\t\t\t// Remove from tracking table\n\t\t\t\tawait db\n\t\t\t\t\t.deleteFrom(\"_plugin_indexes\")\n\t\t\t\t\t.where(\"plugin_id\", \"=\", pluginId)\n\t\t\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t\t\t.where(\"index_name\", \"=\", index_name)\n\t\t\t\t\t.execute();\n\n\t\t\t\tremoved.push(index_name);\n\t\t\t} catch (error) {\n\t\t\t\terrors.push({\n\t\t\t\t\tindex: index_name,\n\t\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { removed, errors };\n}\n\n/**\n * Sync indexes for a plugin collection (create new, remove old)\n */\nexport async function syncStorageIndexes(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n\tcollection: string,\n\tindexes: Array<string | string[]>,\n\toptions?: { uniqueIndexes?: Array<string | string[]> },\n): Promise<{\n\tcreated: string[];\n\tremoved: string[];\n\terrors: Array<{ index: string; error: string }>;\n}> {\n\tconst [createResult, removeResult] = await Promise.all([\n\t\tcreateStorageIndexes(db, pluginId, collection, indexes, options),\n\t\tremoveOrphanedIndexes(db, pluginId, collection, indexes, options),\n\t]);\n\n\treturn {\n\t\tcreated: createResult.created,\n\t\tremoved: removeResult.removed,\n\t\terrors: [...createResult.errors, ...removeResult.errors],\n\t};\n}\n\n/**\n * Materialize the storage indexes a set of plugins declare in their\n * manifests. Failures are logged per collection and never thrown — a missing\n * index affects query performance, not correctness, so it must not fail an\n * install or a scheduler tick.\n */\nexport async function syncDeclaredStorageIndexes(\n\tdb: Kysely<Database>,\n\tplugins: Array<{\n\t\tid: string;\n\t\tstorage?: Record<\n\t\t\tstring,\n\t\t\t{ indexes: Array<string | string[]>; uniqueIndexes?: Array<string | string[]> }\n\t\t>;\n\t}>,\n): Promise<void> {\n\tfor (const plugin of plugins) {\n\t\tfor (const [collection, config] of Object.entries(plugin.storage ?? {})) {\n\t\t\ttry {\n\t\t\t\tconst result = await syncStorageIndexes(db, plugin.id, collection, config.indexes, {\n\t\t\t\t\tuniqueIndexes: config.uniqueIndexes,\n\t\t\t\t});\n\t\t\t\tfor (const failure of result.errors) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`[plugins] Failed to sync storage index ${failure.index} for ${plugin.id}/${collection}: ${failure.error}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconsole.error(\n\t\t\t\t\t`[plugins] Failed to sync storage indexes for ${plugin.id}/${collection}:`,\n\t\t\t\t\terror,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Remove all indexes for a plugin\n */\nexport async function removeAllPluginIndexes(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n): Promise<{\n\tremoved: string[];\n\terrors: Array<{ index: string; error: string }>;\n}> {\n\tconst existingIndexes = await db\n\t\t.selectFrom(\"_plugin_indexes\")\n\t\t.select([\"index_name\", \"collection\"])\n\t\t.where(\"plugin_id\", \"=\", pluginId)\n\t\t.execute();\n\n\tconst removed: string[] = [];\n\tconst errors: Array<{ index: string; error: string }> = [];\n\n\tfor (const { index_name } of existingIndexes) {\n\t\ttry {\n\t\t\tawait generateDropIndexSql(index_name).execute(db);\n\t\t\tremoved.push(index_name);\n\t\t} catch (error) {\n\t\t\terrors.push({\n\t\t\t\tindex: index_name,\n\t\t\t\terror: error instanceof Error ? error.message : String(error),\n\t\t\t});\n\t\t}\n\t}\n\n\t// Clean up tracking table\n\tawait db.deleteFrom(\"_plugin_indexes\").where(\"plugin_id\", \"=\", pluginId).execute();\n\n\treturn { removed, errors };\n}\n\n/**\n * Get current index status for a plugin\n */\nexport async function getPluginIndexStatus(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n): Promise<\n\tArray<{\n\t\tcollection: string;\n\t\tindexName: string;\n\t\tfields: string[];\n\t\tcreatedAt: string;\n\t}>\n> {\n\tconst rows = await db\n\t\t.selectFrom(\"_plugin_indexes\")\n\t\t.select([\"collection\", \"index_name\", \"fields\", \"created_at\"])\n\t\t.where(\"plugin_id\", \"=\", pluginId)\n\t\t.execute();\n\n\treturn rows.map((row) => {\n\t\tconst parsed: unknown = JSON.parse(row.fields);\n\t\tconst fields = Array.isArray(parsed)\n\t\t\t? parsed.filter((f): f is string => typeof f === \"string\")\n\t\t\t: [];\n\t\treturn {\n\t\t\tcollection: row.collection,\n\t\t\tindexName: row.index_name,\n\t\t\tfields,\n\t\t\tcreatedAt: row.created_at,\n\t\t};\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, reconcileManifestAccess } 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 {\n\tremoveAllPluginIndexes,\n\tsyncDeclaredStorageIndexes,\n} from \"../../plugins/storage-indexes.js\";\nimport { normalizeCapabilities } from \"../../plugins/types.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\nexport function diffCapabilities(\n\toldCaps: string[],\n\tnewCaps: string[],\n): { added: string[]; removed: string[] } {\n\t// Normalize both sides before diffing so that an installed v1 manifest\n\t// declaring `read:content` and an upgrade v2 manifest declaring\n\t// `content:read` produces an empty diff — users should not see a\n\t// spurious \"capability changed\" prompt for a pure rename.\n\tconst oldNorm = normalizeCapabilities(oldCaps);\n\tconst newNorm = normalizeCapabilities(newCaps);\n\tconst oldSet = new Set(oldNorm);\n\tconst newSet = new Set(newNorm);\n\treturn {\n\t\tadded: newNorm.filter((c) => !oldSet.has(c)),\n\t\tremoved: oldNorm.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 */\nexport function 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 */\n/**\n * Storage source for an installed plugin bundle. Determines the R2\n * key prefix and is used to keep marketplace and registry installs\n * cleanly separated in object listings.\n */\nexport type PluginBundleSource = \"marketplace\" | \"registry\";\n\nfunction bundlePrefix(source: PluginBundleSource, pluginId: string, version: string): string {\n\treturn `${source}/${pluginId}/${version}`;\n}\n\nexport async function storeBundleInR2(\n\tstorage: Storage,\n\tpluginId: string,\n\tversion: string,\n\tbundle: PluginBundle,\n\tsource: PluginBundleSource = \"marketplace\",\n): Promise<void> {\n\tvalidatePluginIdentifier(pluginId, \"plugin ID\");\n\tvalidateVersion(version);\n\tconst prefix = bundlePrefix(source, 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/**\n * Load a plugin bundle from site-local R2 storage.\n *\n * `source` selects the R2 key prefix: marketplace plugins are stored\n * under `marketplace/<id>/<version>/`, registry plugins under\n * `registry/<id>/<version>/`. Defaults to `\"marketplace\"` for\n * backwards compatibility with pre-registry call sites.\n */\nexport async function loadBundleFromR2(\n\tstorage: Storage,\n\tpluginId: string,\n\tversion: string,\n\tsource: PluginBundleSource = \"marketplace\",\n): Promise<{ manifest: PluginManifest; backendCode: string; adminCode?: string } | null> {\n\tvalidatePluginIdentifier(pluginId, \"plugin ID\");\n\tvalidateVersion(version);\n\tconst prefix = bundlePrefix(source, 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\tconst manifest = reconcileManifestAccess(result.data);\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 */\nexport async function deleteBundleFromR2(\n\tstorage: Storage,\n\tpluginId: string,\n\tversion: string,\n\tsource: PluginBundleSource = \"marketplace\",\n): Promise<void> {\n\tvalidatePluginIdentifier(pluginId, \"plugin ID\");\n\tvalidateVersion(version);\n\tconst prefix = bundlePrefix(source, 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?: {\n\t\tversion?: string;\n\t\tconfiguredPluginIds?: Set<string>;\n\t\tsiteOrigin?: string;\n\t\t/**\n\t\t * When true, sandbox: false bypass mode is active. The sandbox runner\n\t\t * is the noop runner (isAvailable() === false) but the runtime will\n\t\t * load the marketplace plugin in-process via syncMarketplacePlugins().\n\t\t * Skip the SANDBOX_NOT_AVAILABLE gate so the install can proceed.\n\t\t */\n\t\tsandboxBypassed?: boolean;\n\t\tconfirmMcpTools?: boolean;\n\t},\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\t// Sandbox availability check: skip when sandbox: false bypass is active.\n\t// The runtime's syncMarketplacePlugins() will load the plugin in-process.\n\tif (!opts?.sandboxBypassed && (!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\tif ((bundle.manifest.mcp?.tools.length ?? 0) > 0 && !opts?.confirmMcpTools) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"MCP_TOOL_CONSENT_REQUIRED\",\n\t\t\t\t\tmessage: \"Plugin MCP tools require explicit consent\",\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\tmcpTools: bundle.manifest.mcp?.tools.map(\n\t\t\t\t\t\t\t({ inputSchema: _, outputSchema: __, ...tool }) => tool,\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\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\tawait syncDeclaredStorageIndexes(db, [bundle.manifest]);\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\tconfirmMcpTools?: boolean;\n\t\t/**\n\t\t * When true, sandbox: false bypass mode is active. The sandbox runner\n\t\t * is the noop runner (isAvailable() === false) but the runtime will\n\t\t * load the marketplace plugin in-process via syncMarketplacePlugins().\n\t\t * Skip the SANDBOX_NOT_AVAILABLE gate so the update can proceed.\n\t\t */\n\t\tsandboxBypassed?: 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\t// Sandbox availability check: skip when sandbox: false bypass is active.\n\t// The runtime's syncMarketplacePlugins() will load the plugin in-process.\n\tif (!opts?.sandboxBypassed && (!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\tconst oldMcpTools = [...(oldBundle?.manifest.mcp?.tools ?? [])].toSorted((a, b) =>\n\t\t\ta.name.localeCompare(b.name),\n\t\t);\n\t\tconst newMcpTools = [...(bundle.manifest.mcp?.tools ?? [])].toSorted((a, b) =>\n\t\t\ta.name.localeCompare(b.name),\n\t\t);\n\t\tif (JSON.stringify(oldMcpTools) !== JSON.stringify(newMcpTools) && !opts?.confirmMcpTools) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"MCP_TOOL_CONSENT_REQUIRED\",\n\t\t\t\t\tmessage: \"Plugin update changes its MCP tools\",\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\tmcpTools: newMcpTools.map(({ inputSchema: _, outputSchema: __, ...tool }) => tool),\n\t\t\t\t\t},\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\tmcpToolsEnabled: false,\n\t\t\tmcpToolsConsent: null,\n\t\t});\n\n\t\tawait syncDeclaredStorageIndexes(db, [bundle.manifest]);\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\ttry {\n\t\t\tawait removeAllPluginIndexes(db, pluginId);\n\t\t} catch {\n\t\t\t// Nothing to drop, or tracking table predates the feature\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","import { resolveAndValidateExternalUrlTarget, SsrfError } from \"../security/ssrf.js\";\n\nconst TRAILING_DOT = /\\.+$/;\nconst HEADER_END = new Uint8Array([13, 10, 13, 10]);\nconst CRLF = new Uint8Array([13, 10]);\nconst STATUS_LINE_RE = /^HTTP\\/1\\.[01] ([2-5][0-9]{2})(?: .*)?$/;\nconst HEADER_NAME_RE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;\nconst FOLDED_HEADER_RE = /^[ \\t]/;\nconst CONTENT_LENGTH_RE = /^(0|[1-9][0-9]*)$/;\nconst CHUNK_SIZE_RE = /^[0-9A-Fa-f]+$/;\nconst MAX_HEADER_BYTES = 32 * 1024;\nconst LOCALHOST_HOSTNAMES = new Set([\n\t\"localhost\",\n\t\"localhost.localdomain\",\n\t\"ip6-localhost\",\n\t\"ip6-loopback\",\n]);\n\nexport interface RegistryArtifactTransportInput {\n\turl: URL;\n\tallowedAddresses: readonly string[];\n\tsignal: AbortSignal;\n\tmaxResponseBytes: number;\n}\n\nexport interface RegistryArtifactTransport {\n\tfetch(input: RegistryArtifactTransportInput): Promise<{\n\t\tresponse: Response;\n\t\tconnectedAddress: string;\n\t}>;\n}\n\nexport interface RegistryArtifactFetchOptions {\n\tsignal: AbortSignal;\n\tmaxResponseBytes: number;\n}\n\nlet defaultTransport: RegistryArtifactTransport | null = null;\n\nexport function setDefaultRegistryArtifactTransport(\n\ttransport: RegistryArtifactTransport | null,\n): RegistryArtifactTransport | null {\n\tconst previous = defaultTransport;\n\tdefaultTransport = transport;\n\treturn previous;\n}\n\nfunction isLocalhostHostname(hostname: string): boolean {\n\tconst normalized = hostname.toLowerCase().replace(TRAILING_DOT, \"\");\n\treturn (\n\t\tLOCALHOST_HOSTNAMES.has(normalized) ||\n\t\tnormalized.endsWith(\".localhost\") ||\n\t\tnormalized === \"127.0.0.1\" ||\n\t\tnormalized === \"::1\" ||\n\t\tnormalized === \"[::1]\" ||\n\t\tnormalized.startsWith(\"::ffff:127.\") ||\n\t\tnormalized.startsWith(\"::ffff:7f00:\")\n\t);\n}\n\nasync function resolveSafeArtifactTarget(urlString: string): Promise<{\n\turl: URL;\n\taddresses: readonly string[];\n}> {\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(urlString);\n\t} catch {\n\t\tthrow new Error(`Invalid artifact URL: ${urlString}`);\n\t}\n\tif (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n\t\tthrow new Error(`Artifact URL protocol not allowed: ${url.protocol}`);\n\t}\n\tif (url.username || url.password) {\n\t\tthrow new Error(\"Artifact URL must not contain embedded credentials\");\n\t}\n\n\tconst rawHostname = url.hostname.toLowerCase().replace(TRAILING_DOT, \"\");\n\tconst hostname = stripIpv6Brackets(rawHostname);\n\tconst localhost = isLocalhostHostname(hostname);\n\n\tif (!import.meta.env.DEV) {\n\t\tif (url.protocol === \"http:\") {\n\t\t\tthrow new Error(\"Artifact URL must use https\");\n\t\t}\n\t\tif (localhost) {\n\t\t\tthrow new Error(`Artifact URL points to localhost: ${hostname}`);\n\t\t}\n\t} else if (url.protocol === \"http:\" && !localhost) {\n\t\tthrow new Error(\"Artifact URL must use https (http allowed only for localhost in dev)\");\n\t}\n\n\tif (localhost) {\n\t\treturn { url, addresses: [] };\n\t}\n\n\ttry {\n\t\treturn await resolveAndValidateExternalUrlTarget(url.href);\n\t} catch (error) {\n\t\tif (error instanceof SsrfError) {\n\t\t\tthrow new Error(`Artifact URL rejected: ${error.message}`, { cause: error });\n\t\t}\n\t\tthrow error;\n\t}\n}\n\nexport async function assertSafeArtifactUrl(urlString: string): Promise<URL> {\n\treturn (await resolveSafeArtifactTarget(urlString)).url;\n}\n\nexport async function fetchRegistryArtifactUrl(\n\turlString: string,\n\toptions: RegistryArtifactFetchOptions,\n): Promise<Response> {\n\tif (!Number.isSafeInteger(options.maxResponseBytes) || options.maxResponseBytes < 0) {\n\t\tthrow new TypeError(\"Registry artifact response limit is invalid\");\n\t}\n\tconst target = await resolveSafeArtifactTarget(urlString);\n\tif (target.addresses.length === 0) {\n\t\treturn globalThis.fetch(target.url, { redirect: \"manual\", signal: options.signal });\n\t}\n\n\tconst transport = defaultTransport ?? (await createRuntimeRegistryArtifactTransport());\n\tconst result = await transport.fetch({\n\t\turl: target.url,\n\t\tallowedAddresses: target.addresses,\n\t\tsignal: options.signal,\n\t\tmaxResponseBytes: options.maxResponseBytes,\n\t});\n\tif (!target.addresses.includes(result.connectedAddress)) {\n\t\tawait result.response.body?.cancel().catch(() => undefined);\n\t\tthrow new Error(\"Registry artifact transport connected outside the validated address set\");\n\t}\n\treturn result.response;\n}\n\nasync function createRuntimeRegistryArtifactTransport(): Promise<RegistryArtifactTransport> {\n\ttry {\n\t\t// @ts-ignore - virtual module\n\t\tconst sockets: unknown = await import(\"cloudflare:sockets\");\n\t\tconst connect = objectProperty(sockets, \"connect\");\n\t\tif (!isWorkerSocketConnect(connect)) throw new TypeError(\"Workers socket binding is invalid\");\n\t\treturn createWorkersRegistryArtifactTransport(connect);\n\t} catch {\n\t\treturn createNodeRegistryArtifactTransport();\n\t}\n}\n\nexport interface RegistryArtifactWorkerSocket {\n\treadonly opened: Promise<unknown>;\n\treadonly readable: ReadableStream<Uint8Array>;\n\treadonly writable: WritableStream<Uint8Array>;\n\tstartTls(options: { expectedServerHostname: string }): RegistryArtifactWorkerSocket;\n\tclose(): Promise<void>;\n}\n\nexport type RegistryArtifactWorkerSocketConnect = (\n\taddress: { hostname: string; port: number },\n\toptions: { secureTransport: \"starttls\"; allowHalfOpen: false },\n) => RegistryArtifactWorkerSocket;\n\nexport function createWorkersRegistryArtifactTransport(\n\tconnect: RegistryArtifactWorkerSocketConnect,\n): RegistryArtifactTransport {\n\treturn {\n\t\tasync fetch(input) {\n\t\t\tlet lastError: unknown;\n\t\t\tfor (const address of input.allowedAddresses) {\n\t\t\t\tif (input.signal.aborted) throw new Error(\"Registry artifact request was aborted\");\n\t\t\t\tlet socket: RegistryArtifactWorkerSocket | undefined;\n\t\t\t\tlet tls: RegistryArtifactWorkerSocket | undefined;\n\t\t\t\ttry {\n\t\t\t\t\tsocket = connect(\n\t\t\t\t\t\t{ hostname: address, port: parseHttpsPort(input.url) },\n\t\t\t\t\t\t{ secureTransport: \"starttls\", allowHalfOpen: false },\n\t\t\t\t\t);\n\t\t\t\t\tawait abortable(socket.opened, input.signal, () => socket?.close());\n\t\t\t\t\ttls = socket.startTls({\n\t\t\t\t\t\texpectedServerHostname: stripIpv6Brackets(input.url.hostname),\n\t\t\t\t\t});\n\t\t\t\t\tawait abortable(tls.opened, input.signal, () => tls?.close());\n\t\t\t\t\tconst writer = tls.writable.getWriter();\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait abortable(writer.write(buildHttpRequest(input.url)), input.signal, () =>\n\t\t\t\t\t\t\ttls?.close(),\n\t\t\t\t\t\t);\n\t\t\t\t\t} finally {\n\t\t\t\t\t\twriter.releaseLock();\n\t\t\t\t\t}\n\t\t\t\t\tconst bytes = await readWorkerSocketResponse(\n\t\t\t\t\t\ttls,\n\t\t\t\t\t\tinput.maxResponseBytes + MAX_HEADER_BYTES,\n\t\t\t\t\t\tinput.signal,\n\t\t\t\t\t);\n\t\t\t\t\tawait tls.close().catch(() => undefined);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tresponse: parsedResponseToWebResponse(\n\t\t\t\t\t\t\tparsePinnedHttpResponse(bytes, input.maxResponseBytes),\n\t\t\t\t\t\t),\n\t\t\t\t\t\tconnectedAddress: address,\n\t\t\t\t\t};\n\t\t\t\t} catch (error) {\n\t\t\t\t\tawait Promise.all([\n\t\t\t\t\t\ttls?.close().catch(() => undefined),\n\t\t\t\t\t\tsocket?.close().catch(() => undefined),\n\t\t\t\t\t]);\n\t\t\t\t\tif (input.signal.aborted) throw error;\n\t\t\t\t\tlastError = error;\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow new Error(\"Registry artifact transport could not connect to an approved address\", {\n\t\t\t\tcause: lastError,\n\t\t\t});\n\t\t},\n\t};\n}\n\nasync function createNodeRegistryArtifactTransport(): Promise<RegistryArtifactTransport> {\n\tconst { request } = await import(\"node:https\");\n\treturn {\n\t\tasync fetch(input) {\n\t\t\tlet lastError: unknown;\n\t\t\tfor (const address of input.allowedAddresses) {\n\t\t\t\ttry {\n\t\t\t\t\tconst response = await new Promise<Response>((resolve, reject) => {\n\t\t\t\t\t\tconst chunks: Uint8Array[] = [];\n\t\t\t\t\t\tlet total = 0;\n\t\t\t\t\t\tconst req = request(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tprotocol: \"https:\",\n\t\t\t\t\t\t\t\thostname: stripIpv6Brackets(input.url.hostname),\n\t\t\t\t\t\t\t\tport: parseHttpsPort(input.url),\n\t\t\t\t\t\t\t\tpath: `${input.url.pathname}${input.url.search}`,\n\t\t\t\t\t\t\t\tmethod: \"GET\",\n\t\t\t\t\t\t\t\t// A pooled socket may have been opened for the same hostname\n\t\t\t\t\t\t\t\t// before this address set was resolved.\n\t\t\t\t\t\t\t\tagent: false,\n\t\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\tHost: input.url.host,\n\t\t\t\t\t\t\t\t\tConnection: \"close\",\n\t\t\t\t\t\t\t\t\t\"Accept-Encoding\": \"identity\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tlookup: (_hostname, _options, callback) => {\n\t\t\t\t\t\t\t\t\tcallback(null, address, address.includes(\":\") ? 6 : 4);\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tsignal: input.signal,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t(upstream) => {\n\t\t\t\t\t\t\t\tconst contentEncoding = upstream.headers[\"content-encoding\"];\n\t\t\t\t\t\t\t\tif (contentEncoding && contentEncoding.toLowerCase() !== \"identity\") {\n\t\t\t\t\t\t\t\t\tupstream.destroy(\n\t\t\t\t\t\t\t\t\t\tnew Error(\"Registry artifact response content encoding is unsupported\"),\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tupstream.on(\"data\", (chunk: Uint8Array) => {\n\t\t\t\t\t\t\t\t\ttotal += chunk.byteLength;\n\t\t\t\t\t\t\t\t\tif (total > input.maxResponseBytes) {\n\t\t\t\t\t\t\t\t\t\tupstream.destroy(\n\t\t\t\t\t\t\t\t\t\t\tnew RangeError(\"Registry artifact response exceeds its byte limit\"),\n\t\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tchunks.push(new Uint8Array(chunk));\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\tupstream.once(\"error\", reject);\n\t\t\t\t\t\t\t\tupstream.once(\"end\", () => {\n\t\t\t\t\t\t\t\t\tconst bytes = concatBytes(chunks, total);\n\t\t\t\t\t\t\t\t\tconst headers = new Headers();\n\t\t\t\t\t\t\t\t\tfor (let index = 0; index < upstream.rawHeaders.length; index += 2) {\n\t\t\t\t\t\t\t\t\t\theaders.append(upstream.rawHeaders[index], upstream.rawHeaders[index + 1]);\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tresolve(\n\t\t\t\t\t\t\t\t\t\tparsedResponseToWebResponse({\n\t\t\t\t\t\t\t\t\t\t\tstatus: upstream.statusCode ?? 502,\n\t\t\t\t\t\t\t\t\t\t\theaders,\n\t\t\t\t\t\t\t\t\t\t\tbody: bytes,\n\t\t\t\t\t\t\t\t\t\t}),\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t);\n\t\t\t\t\t\treq.once(\"error\", reject);\n\t\t\t\t\t\treq.end();\n\t\t\t\t\t});\n\t\t\t\t\treturn { response, connectedAddress: address };\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (input.signal.aborted) throw error;\n\t\t\t\t\tlastError = error;\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow new Error(\"Registry artifact transport could not connect to an approved address\", {\n\t\t\t\tcause: lastError,\n\t\t\t});\n\t\t},\n\t};\n}\n\ninterface ParsedHttpResponse {\n\tstatus: number;\n\theaders: Headers;\n\tbody: Uint8Array;\n}\n\nfunction parsePinnedHttpResponse(bytes: Uint8Array, maxResponseBytes: number): ParsedHttpResponse {\n\tconst headerEnd = findSequence(bytes, HEADER_END, 0);\n\tif (headerEnd === -1 || headerEnd > MAX_HEADER_BYTES) {\n\t\tthrow new Error(\"Registry artifact response headers are invalid or too large\");\n\t}\n\tconst headerText = new TextDecoder(\"utf-8\", { fatal: true }).decode(bytes.subarray(0, headerEnd));\n\tconst lines = headerText.split(\"\\r\\n\");\n\tconst statusMatch = STATUS_LINE_RE.exec(lines.shift() ?? \"\");\n\tif (!statusMatch) throw new Error(\"Registry artifact response status line is invalid\");\n\tconst headers = new Headers();\n\tconst rawHeaders = new Map<string, string[]>();\n\tfor (const line of lines) {\n\t\tif (FOLDED_HEADER_RE.test(line)) {\n\t\t\tthrow new Error(\"Registry artifact response uses folded headers\");\n\t\t}\n\t\tconst separator = line.indexOf(\":\");\n\t\tif (separator < 1) throw new Error(\"Registry artifact response header is invalid\");\n\t\tconst name = line.slice(0, separator);\n\t\tconst value = line.slice(separator + 1).trim();\n\t\tif (!HEADER_NAME_RE.test(name) || containsInvalidHeaderValue(value)) {\n\t\t\tthrow new Error(\"Registry artifact response header is invalid\");\n\t\t}\n\t\tconst normalized = name.toLowerCase();\n\t\tconst values = rawHeaders.get(normalized) ?? [];\n\t\tvalues.push(value);\n\t\trawHeaders.set(normalized, values);\n\t\theaders.append(name, value);\n\t}\n\tconst contentEncoding = rawHeaders.get(\"content-encoding\");\n\tif (\n\t\tcontentEncoding &&\n\t\t(contentEncoding.length !== 1 || contentEncoding[0]?.toLowerCase() !== \"identity\")\n\t) {\n\t\tthrow new Error(\"Registry artifact response content encoding is unsupported\");\n\t}\n\tconst contentLength = rawHeaders.get(\"content-length\");\n\tconst transferEncoding = rawHeaders.get(\"transfer-encoding\");\n\tif (contentLength && transferEncoding) {\n\t\tthrow new Error(\"Registry artifact response framing is ambiguous\");\n\t}\n\tconst bodyBytes = bytes.subarray(headerEnd + HEADER_END.length);\n\tlet body: Uint8Array;\n\tif (transferEncoding) {\n\t\tif (transferEncoding.length !== 1 || transferEncoding[0]?.toLowerCase() !== \"chunked\") {\n\t\t\tthrow new Error(\"Registry artifact response transfer encoding is unsupported\");\n\t\t}\n\t\tbody = decodeChunkedBody(bodyBytes, maxResponseBytes);\n\t} else if (contentLength) {\n\t\tif (contentLength.length !== 1 || !CONTENT_LENGTH_RE.test(contentLength[0])) {\n\t\t\tthrow new Error(\"Registry artifact response content length is invalid\");\n\t\t}\n\t\tconst length = Number(contentLength[0]);\n\t\tif (!Number.isSafeInteger(length) || length !== bodyBytes.byteLength) {\n\t\t\tthrow new Error(\"Registry artifact response body length does not match its framing\");\n\t\t}\n\t\tbody = new Uint8Array(bodyBytes);\n\t} else {\n\t\tbody = new Uint8Array(bodyBytes);\n\t}\n\tif (body.byteLength > maxResponseBytes) {\n\t\tthrow new RangeError(\"Registry artifact response exceeds its byte limit\");\n\t}\n\treturn { status: Number(statusMatch[1]), headers, body };\n}\n\nfunction parsedResponseToWebResponse(parsed: ParsedHttpResponse): Response {\n\tconst bodyAllowed = ![204, 205, 304].includes(parsed.status);\n\tlet body: ArrayBuffer | null = null;\n\tif (bodyAllowed) {\n\t\tbody = new ArrayBuffer(parsed.body.byteLength);\n\t\tnew Uint8Array(body).set(parsed.body);\n\t}\n\treturn new Response(body, {\n\t\tstatus: parsed.status,\n\t\theaders: parsed.headers,\n\t});\n}\n\nfunction buildHttpRequest(url: URL): Uint8Array {\n\treturn new TextEncoder().encode(\n\t\t[\n\t\t\t`GET ${url.pathname}${url.search} HTTP/1.1`,\n\t\t\t`Host: ${url.host}`,\n\t\t\t\"Connection: close\",\n\t\t\t\"Accept-Encoding: identity\",\n\t\t\t\"\",\n\t\t\t\"\",\n\t\t].join(\"\\r\\n\"),\n\t);\n}\n\nasync function readWorkerSocketResponse(\n\tsocket: RegistryArtifactWorkerSocket,\n\tmaximumBytes: number,\n\tsignal: AbortSignal,\n): Promise<Uint8Array> {\n\tconst reader = socket.readable.getReader();\n\tconst chunks: Uint8Array[] = [];\n\tlet total = 0;\n\ttry {\n\t\tfor (;;) {\n\t\t\tconst next = await abortable(reader.read(), signal, () => socket.close());\n\t\t\tif (next.done) break;\n\t\t\ttotal += next.value.byteLength;\n\t\t\tif (total > maximumBytes) {\n\t\t\t\tthrow new RangeError(\"Registry artifact response exceeds its byte limit\");\n\t\t\t}\n\t\t\tchunks.push(next.value);\n\t\t}\n\t} finally {\n\t\treader.releaseLock();\n\t}\n\treturn concatBytes(chunks, total);\n}\n\nfunction decodeChunkedBody(bytes: Uint8Array, maximumBytes: number): Uint8Array {\n\tconst chunks: Uint8Array[] = [];\n\tlet offset = 0;\n\tlet total = 0;\n\tfor (;;) {\n\t\tconst lineEnd = findSequence(bytes, CRLF, offset);\n\t\tif (lineEnd === -1 || lineEnd - offset > 128) {\n\t\t\tthrow new Error(\"Registry artifact chunk size line is invalid\");\n\t\t}\n\t\tconst sizeText = new TextDecoder().decode(bytes.subarray(offset, lineEnd)).split(\";\", 1)[0];\n\t\tif (!sizeText || !CHUNK_SIZE_RE.test(sizeText)) {\n\t\t\tthrow new Error(\"Registry artifact chunk size is invalid\");\n\t\t}\n\t\tconst size = Number.parseInt(sizeText, 16);\n\t\tif (!Number.isSafeInteger(size)) throw new Error(\"Registry artifact chunk size is invalid\");\n\t\toffset = lineEnd + CRLF.length;\n\t\tif (size === 0) {\n\t\t\tif (\n\t\t\t\toffset + CRLF.length !== bytes.length ||\n\t\t\t\tbytes[offset] !== 13 ||\n\t\t\t\tbytes[offset + 1] !== 10\n\t\t\t) {\n\t\t\t\tthrow new Error(\"Registry artifact chunk trailer is invalid\");\n\t\t\t}\n\t\t\tbreak;\n\t\t}\n\t\tif (offset + size + CRLF.length > bytes.length) {\n\t\t\tthrow new Error(\"Registry artifact chunk body is truncated\");\n\t\t}\n\t\tif (bytes[offset + size] !== 13 || bytes[offset + size + 1] !== 10) {\n\t\t\tthrow new Error(\"Registry artifact chunk delimiter is invalid\");\n\t\t}\n\t\ttotal += size;\n\t\tif (total > maximumBytes) {\n\t\t\tthrow new RangeError(\"Registry artifact response exceeds its byte limit\");\n\t\t}\n\t\tchunks.push(new Uint8Array(bytes.subarray(offset, offset + size)));\n\t\toffset += size + CRLF.length;\n\t}\n\treturn concatBytes(chunks, total);\n}\n\nfunction findSequence(bytes: Uint8Array, sequence: Uint8Array, start: number): number {\n\touter: for (let offset = start; offset <= bytes.length - sequence.length; offset += 1) {\n\t\tfor (let index = 0; index < sequence.length; index += 1) {\n\t\t\tif (bytes[offset + index] !== sequence[index]) continue outer;\n\t\t}\n\t\treturn offset;\n\t}\n\treturn -1;\n}\n\nfunction concatBytes(chunks: readonly Uint8Array[], total: number): Uint8Array {\n\tconst bytes = new Uint8Array(total);\n\tlet offset = 0;\n\tfor (const chunk of chunks) {\n\t\tbytes.set(chunk, offset);\n\t\toffset += chunk.byteLength;\n\t}\n\treturn bytes;\n}\n\nfunction containsInvalidHeaderValue(value: string): boolean {\n\tfor (const character of value) {\n\t\tconst code = character.codePointAt(0)!;\n\t\tif (code === 0 || code === 10 || code === 13) return true;\n\t}\n\treturn false;\n}\n\nfunction parseHttpsPort(url: URL): number {\n\tconst port = url.port === \"\" ? 443 : Number(url.port);\n\tif (!Number.isSafeInteger(port) || port < 1 || port > 65_535) {\n\t\tthrow new TypeError(\"Registry artifact HTTPS port is invalid\");\n\t}\n\treturn port;\n}\n\nfunction stripIpv6Brackets(hostname: string): string {\n\treturn hostname.startsWith(\"[\") && hostname.endsWith(\"]\") ? hostname.slice(1, -1) : hostname;\n}\n\nfunction objectProperty(value: unknown, key: string): unknown {\n\tif (typeof value !== \"object\" || value === null) return undefined;\n\treturn Object.getOwnPropertyDescriptor(value, key)?.value;\n}\n\nfunction isWorkerSocketConnect(value: unknown): value is RegistryArtifactWorkerSocketConnect {\n\treturn typeof value === \"function\";\n}\n\nasync function abortable<T>(\n\toperation: Promise<T>,\n\tsignal: AbortSignal,\n\tonAbort: () => void | Promise<void>,\n): Promise<T> {\n\tif (signal.aborted) throw new Error(\"Registry artifact request was aborted\");\n\tlet abort: (() => void) | undefined;\n\tconst aborted = new Promise<never>((_resolve, reject) => {\n\t\tabort = () => {\n\t\t\tvoid onAbort();\n\t\t\treject(new Error(\"Registry artifact request was aborted\"));\n\t\t};\n\t\tsignal.addEventListener(\"abort\", abort, { once: true });\n\t});\n\ttry {\n\t\treturn await Promise.race([operation, aborted]);\n\t} finally {\n\t\tif (abort) signal.removeEventListener(\"abort\", abort);\n\t}\n}\n","/**\n * Helpers for normalizing the experimental registry integration option\n * (`config.experimental.registry` in `astro.config.mjs`) into the shape\n * exposed on the admin manifest.\n *\n * The integration option accepts a human-friendly duration string for\n * `policy.minimumReleaseAge` (`\"48h\"`, `\"7d\"`); the manifest exposes\n * seconds so the browser doesn't need a duration parser.\n */\n\nimport { isDid } from \"@atcute/lexicons/syntax\";\n\nimport type { RegistryConfig, RegistryConfigInput } from \"./types.js\";\n\n/**\n * Shape returned in the admin manifest's `registry` field. The browser\n * consumes this directly -- all duration normalization and aggregator URL\n * validation has already happened by the time it gets here.\n */\nexport interface ManifestRegistryConfig {\n\taggregatorUrl: string;\n\tacceptLabelers?: string;\n\tpolicy?: {\n\t\tminimumReleaseAgeSeconds?: number;\n\t\t/**\n\t\t * Allowlist of publishers / packages exempt from the\n\t\t * {@link minimumReleaseAgeSeconds} holdback. Each entry is either:\n\t\t *\n\t\t *   - A bare publisher DID: `\"did:plc:abc123\"`. Every package from\n\t\t *     that publisher is exempt.\n\t\t *   - A `<did>/<slug>` pair: only that specific package is exempt.\n\t\t *\n\t\t * Handles are not accepted because they are mutable\n\t\t * aggregator-supplied envelope data.\n\t\t *\n\t\t * Normalized to lowercase strings at config load time so the\n\t\t * browser does case-insensitive comparison. See\n\t\t * {@link releaseExemptFromMinimumAge}.\n\t\t */\n\t\tminimumReleaseAgeExclude?: string[];\n\t};\n}\n\n/**\n * Canonicalize a capabilities list for set-style comparison.\n *\n * Capabilities (the legacy declared-access shape used by the current\n * sandbox enforcer) are conceptually a *set*: order, duplicates, and\n * non-string entries don't carry meaning. The install handler's drift\n * check compares the admin's acknowledged set against the bundle\n * manifest's set; both sides pass through this canonicalizer first so\n * an aggregator-supplied array with unstable order or junk entries\n * can't cause a spurious drift rejection.\n *\n * Filters non-strings, deduplicates, and sorts lexically. Named to\n * avoid shadowing `@premium-cms/plugin-types`'s existing\n * `normalizeCapabilities` (which dedupes + applies the deprecated →\n * current alias map but does not filter junk or sort).\n *\n * Exported so the same shape is produced by the browser before sending\n * the `acknowledgedDeclaredAccess` payload and by the server before\n * comparing against the bundle.\n */\nexport function canonicalCapabilitiesForDriftCheck(value: unknown): string[] {\n\tif (!Array.isArray(value)) return [];\n\tconst seen = new Set<string>();\n\tfor (const entry of value) {\n\t\tif (typeof entry === \"string\" && entry.length > 0) {\n\t\t\tseen.add(entry);\n\t\t}\n\t}\n\treturn [...seen].toSorted();\n}\n\n/**\n * Returns whether a `(publisher_did, slug)` pair is on the\n * minimum-release-age exemption list. Exported so the same matcher is\n * used by the browser policy filter and the server-side install\n * enforcement.\n *\n * Matching is DID-only. Handles are aggregator-supplied envelope data\n * (mutable, controlled by an attacker who compromises the aggregator)\n * and cannot be used as a trust input -- a compromised aggregator\n * could claim any handle for any package and bypass the holdback. DIDs\n * are part of the AT URI of the package record and are independently\n * resolvable, so even a compromised aggregator can't lie about the\n * publisher DID without also breaking checksum verification downstream.\n *\n * Entries from config are already lowercased at manifest-build time.\n * Runtime values are lowercased here at compare time.\n */\nexport function releaseExemptFromMinimumAge(\n\texclude: readonly string[] | undefined,\n\tpublisherDid: string,\n\tslug: string,\n): boolean {\n\tif (!exclude || exclude.length === 0) return false;\n\tconst didLower = publisherDid.toLowerCase();\n\tconst slugLower = slug.toLowerCase();\n\tconst fullDid = `${didLower}/${slugLower}`;\n\n\tfor (const entry of exclude) {\n\t\tif (entry === didLower) return true;\n\t\tif (entry === fullDid) return true;\n\t}\n\treturn false;\n}\n\nconst DURATION_PATTERN = /^(\\d+)(s|m|h|d|w)$/;\n\n/** Trailing slashes on the aggregator URL, stripped during normalization. */\nconst TRAILING_SLASHES = /\\/+$/;\n\n/** Trailing dot on a hostname, stripped before URL host comparisons. */\nconst TRAILING_DOT = /\\.$/;\n\nconst REGISTRY_PACKAGE_SLUG_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;\n\n/**\n * Parse a duration string or raw second count into a non-negative\n * integer count of seconds. Throws on unrecognised input so config\n * mistakes fail at startup rather than silently disabling the policy.\n */\nexport function parseDurationSeconds(duration: string | number): number {\n\tif (typeof duration === \"number\") {\n\t\tif (!Number.isFinite(duration) || duration < 0) {\n\t\t\tthrow new Error(`Invalid duration: ${duration} (must be a non-negative finite number)`);\n\t\t}\n\t\treturn Math.floor(duration);\n\t}\n\n\tconst match = duration.match(DURATION_PATTERN);\n\tif (!match) {\n\t\tthrow new Error(\n\t\t\t`Invalid duration format: \"${duration}\". Use a duration string like \"48h\", \"7d\", \"30m\", or a number of seconds.`,\n\t\t);\n\t}\n\n\tconst value = parseInt(match[1], 10);\n\tconst unit = match[2];\n\n\tswitch (unit) {\n\t\tcase \"s\":\n\t\t\treturn value;\n\t\tcase \"m\":\n\t\t\treturn value * 60;\n\t\tcase \"h\":\n\t\t\treturn value * 60 * 60;\n\t\tcase \"d\":\n\t\t\treturn value * 24 * 60 * 60;\n\t\tcase \"w\":\n\t\t\treturn value * 7 * 24 * 60 * 60;\n\t\tdefault:\n\t\t\t// Unreachable given the regex, but keep the exhaustive arm for\n\t\t\t// future maintainers who add a unit to the pattern.\n\t\t\tthrow new Error(`Unknown duration unit: ${unit}`);\n\t}\n}\n\n/**\n * Validate that `aggregatorUrl` is a safe outbound target for the\n * registry's XRPC calls. Same posture as artifact downloads: HTTPS\n * required in production; `http://localhost` allowed only in dev.\n *\n * The aggregator's responses are the trust source for release records,\n * checksums, labels, mirrors, and `indexedAt` (until full MST\n * verification lands). Allowing plain HTTP here would let a network\n * attacker swap a release record and point the artifact URL at their\n * own HTTPS bundle, defeating the checksum trust chain because the\n * attacker controls the unsigned transport that supplied the checksum.\n */\nexport function validateAggregatorUrl(aggregatorUrl: string): URL {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(aggregatorUrl);\n\t} catch {\n\t\tthrow new Error(`registry.aggregatorUrl is not a valid URL: ${aggregatorUrl}`);\n\t}\n\tif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n\t\tthrow new Error(`registry.aggregatorUrl must use http or https: ${aggregatorUrl}`);\n\t}\n\t// Reject embedded credentials. The normalized aggregator URL ends\n\t// up in the admin manifest and is shipped to every admin browser;\n\t// browser `fetch()` also outright rejects URLs with `user:pass@`,\n\t// so leaving them in would both leak the credentials and break the\n\t// registry UI at runtime.\n\tif (parsed.username || parsed.password) {\n\t\tthrow new Error(\"registry.aggregatorUrl must not contain embedded credentials (user:pass@)\");\n\t}\n\n\t// WHATWG URL preserves the brackets on IPv6 hostnames -- strip them\n\t// before any comparison so `https://[::1]/` is recognised as localhost\n\t// and not treated as a generic domain string.\n\tconst rawHostname = parsed.hostname.toLowerCase().replace(TRAILING_DOT, \"\");\n\tconst hostname =\n\t\trawHostname.startsWith(\"[\") && rawHostname.endsWith(\"]\")\n\t\t\t? rawHostname.slice(1, -1)\n\t\t\t: rawHostname;\n\tconst isLocalhost =\n\t\thostname === \"localhost\" ||\n\t\thostname.endsWith(\".localhost\") ||\n\t\thostname === \"127.0.0.1\" ||\n\t\thostname === \"::1\" ||\n\t\t// IPv4-mapped IPv6 forms of loopback, e.g. `::ffff:127.0.0.1` and `::ffff:7f00:1`.\n\t\thostname.startsWith(\"::ffff:127.\") ||\n\t\thostname.startsWith(\"::ffff:7f00:\");\n\n\tif (!import.meta.env.DEV) {\n\t\tif (parsed.protocol === \"http:\") {\n\t\t\tthrow new Error(`registry.aggregatorUrl must use https in production: ${aggregatorUrl}`);\n\t\t}\n\t\tif (isLocalhost) {\n\t\t\tthrow new Error(\n\t\t\t\t`registry.aggregatorUrl points at localhost; allowed only in dev: ${aggregatorUrl}`,\n\t\t\t);\n\t\t}\n\t} else if (parsed.protocol === \"http:\" && !isLocalhost) {\n\t\tthrow new Error(\n\t\t\t`registry.aggregatorUrl must use https (http allowed only for localhost in dev): ${aggregatorUrl}`,\n\t\t);\n\t}\n\n\treturn parsed;\n}\n\n/**\n * Expand the `RegistryConfigInput` shorthand into the full\n * `RegistryConfig` object shape.\n *\n * Users can pass a bare aggregator URL string for the common case\n * (`experimental.registry: \"https://registry.emdashcms.com\"`); the\n * normalizer handles either form transparently.\n *\n * Returns `undefined` for `undefined` input so callers can chain with\n * optional chaining.\n */\nexport function coerceRegistryConfig(\n\tinput: RegistryConfigInput | undefined,\n): RegistryConfig | undefined {\n\tif (input === undefined) return undefined;\n\tif (typeof input === \"string\") return { aggregatorUrl: input };\n\treturn input;\n}\n\n/**\n * Normalize the user-supplied `RegistryConfigInput` into the shape that\n * ships to the admin browser via the manifest endpoint.\n *\n * Accepts either the shorthand string form\n * (`\"https://registry.emdashcms.com\"`) or the full `RegistryConfig`\n * object. Returns `null` when `input` is undefined so callers can\n * spread the result directly into the manifest object.\n *\n * Throws if the aggregator URL is malformed, points at a forbidden host,\n * or `policy.minimumReleaseAge` is unparseable. These surface at\n * runtime startup as 500s from the manifest endpoint -- intended,\n * because the alternative is silently disabling the registry on\n * misconfigured sites.\n *\n * TODO: switch to a Zod schema for richer per-field error messages and\n * to surface misconfigurations to the admin UI as a banner instead of\n * a manifest 500.\n */\nexport function normalizeRegistryConfig(\n\tinput: RegistryConfigInput | undefined,\n): ManifestRegistryConfig | null {\n\tconst config = coerceRegistryConfig(input);\n\tif (!config) return null;\n\n\tconst aggregatorUrl = config.aggregatorUrl?.trim();\n\tif (!aggregatorUrl) {\n\t\tthrow new Error(\"registry.aggregatorUrl is required when registry is configured\");\n\t}\n\n\tvalidateAggregatorUrl(aggregatorUrl);\n\n\tconst out: ManifestRegistryConfig = {\n\t\t// Strip any trailing slash so `${aggregatorUrl}/xrpc/...` works\n\t\t// regardless of how the user wrote it.\n\t\taggregatorUrl: aggregatorUrl.replace(TRAILING_SLASHES, \"\"),\n\t};\n\n\tif (config.acceptLabelers) {\n\t\tout.acceptLabelers = config.acceptLabelers;\n\t}\n\n\tconst policy: ManifestRegistryConfig[\"policy\"] = {};\n\tlet hasPolicy = false;\n\n\tif (config.policy?.minimumReleaseAge !== undefined) {\n\t\tpolicy.minimumReleaseAgeSeconds = parseDurationSeconds(config.policy.minimumReleaseAge);\n\t\thasPolicy = true;\n\t}\n\n\tif (config.policy?.minimumReleaseAgeExclude !== undefined) {\n\t\t// Normalize at load time so callers (browser and server) can do\n\t\t// plain string compares without each one re-implementing the\n\t\t// case-folding rule.\n\t\tconst list = config.policy.minimumReleaseAgeExclude.map((entry) => {\n\t\t\tconst trimmed = entry.trim();\n\t\t\tif (!trimmed) {\n\t\t\t\tthrow new Error(\"registry.policy.minimumReleaseAgeExclude entries cannot be empty\");\n\t\t\t}\n\t\t\tconst lower = trimmed.toLowerCase();\n\t\t\tconst [did, slug, ...extra] = lower.split(\"/\");\n\t\t\tif (\n\t\t\t\t!did ||\n\t\t\t\t!isDid(did) ||\n\t\t\t\textra.length > 0 ||\n\t\t\t\t(slug !== undefined && !REGISTRY_PACKAGE_SLUG_PATTERN.test(slug))\n\t\t\t) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`registry.policy.minimumReleaseAgeExclude entry must be a DID or <did>/<slug>: ${trimmed}`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn lower;\n\t\t});\n\t\tif (list.length > 0) {\n\t\t\tpolicy.minimumReleaseAgeExclude = list;\n\t\t\thasPolicy = true;\n\t\t}\n\t}\n\n\tif (hasPolicy) {\n\t\tout.policy = policy;\n\t}\n\n\treturn out;\n}\n","/**\n * Plugin identifier helpers for the experimental decentralized plugin\n * registry.\n *\n * Registry plugins are addressed by `(publisher_did, slug)`, but the\n * EmDash runtime threads a single `pluginId: string` through every\n * install primitive (R2 storage keys, `PluginStateRepository`,\n * `syncMarketplacePlugins`, sandbox cache keys). Rather than refactor\n * everything to carry a composite identifier, we normalize the registry\n * tuple to an opaque content-addressed id that satisfies the existing\n * `validatePluginIdentifier` shape (`/^[a-z][a-z0-9_-]*$/`).\n *\n * The normalized id is:\n *\n *   `r_` + base32-encoded SHA-256(publisher_did + \"\\n\" + slug), truncated.\n *\n * Properties:\n *\n *   - Deterministic. The same `(publisher, slug)` always produces the\n *     same id, so re-resolving an installed plugin's metadata against\n *     the aggregator is a straightforward lookup keyed by the columns\n *     stored alongside `plugin_id` in `plugin_states`.\n *   - Collision-resistant. 80 bits of truncated hash; a 50% birthday\n *     collision happens around 2^40 distinct plugins, well beyond what\n *     this registry will ever index.\n *   - R2-safe. Lowercase alphanumerics + underscore (no hyphens), no\n *     `:` or `/`. Existing sandbox cache keys (`${pluginId}:${version}`)\n *     keep working because the id contains no `:`.\n *   - Syntactically distinct from typical marketplace plugin ids: the\n *     `r_` prefix plus exactly 16 base32 characters is unlikely to be\n *     chosen as a marketplace id. Not formally guaranteed by the\n *     validator -- marketplace ids may begin with `r_` and contain\n *     hyphens -- so the install handler also performs an explicit\n *     pre-existing-row check at the derived id and rejects any cross-\n *     source collision (`PLUGIN_ID_COLLISION`).\n *\n * Reverse lookup (id → publisher + slug) requires the `plugin_states`\n * row -- the hash is one-way. That's intentional: any code path that\n * needs the human-meaningful pair already has the state row in hand.\n */\n\n/** Length (in base32 characters) of the truncated hash portion of the id. */\nconst HASH_LENGTH = 16;\n\n/** Total expected length of a registry plugin id. */\nexport const REGISTRY_PLUGIN_ID_LENGTH = 2 /* \"r_\" */ + HASH_LENGTH;\n\n/**\n * Regex matching a well-formed registry plugin id. Used by call sites\n * that need to distinguish registry installs from marketplace installs\n * without consulting the `source` column on `plugin_states`.\n *\n * The base32 alphabet here uses RFC 4648 lowercase without padding,\n * matching {@link base32Encode}'s output.\n */\nexport const REGISTRY_PLUGIN_ID_PATTERN = /^r_[a-z2-7]{16}$/;\n\nconst BASE32_ALPHABET = \"abcdefghijklmnopqrstuvwxyz234567\";\n\n/**\n * RFC 4648 base32 encoding without padding, lowercase. Implemented inline\n * rather than depending on a multibase library because (a) we only need\n * lowercase base32 here, (b) we need it to run identically in workerd,\n * Node, and the browser, and (c) the implementation is fewer lines than\n * the import statement would be.\n */\nfunction base32Encode(bytes: Uint8Array): string {\n\tlet bits = 0;\n\tlet value = 0;\n\tlet out = \"\";\n\tfor (const byte of bytes) {\n\t\tvalue = (value << 8) | byte;\n\t\tbits += 8;\n\t\twhile (bits >= 5) {\n\t\t\tbits -= 5;\n\t\t\tout += BASE32_ALPHABET[(value >>> bits) & 0x1f];\n\t\t}\n\t}\n\tif (bits > 0) {\n\t\tout += BASE32_ALPHABET[(value << (5 - bits)) & 0x1f];\n\t}\n\treturn out;\n}\n\n/**\n * Derive the normalized plugin id for a registry-published plugin.\n *\n * Throws if either input is empty or whitespace-only -- a missing DID\n * or slug is always a programming error in the install path, not a\n * recoverable runtime condition.\n */\nexport async function makeRegistryPluginId(publisherDid: string, slug: string): Promise<string> {\n\tconst did = publisherDid.trim();\n\tconst s = slug.trim();\n\tif (!did) throw new Error(\"makeRegistryPluginId: publisherDid is required\");\n\tif (!s) throw new Error(\"makeRegistryPluginId: slug is required\");\n\n\t// `\\n` separator avoids ambiguity: no canonical did:plc / did:web form\n\t// contains a literal newline, so `(\"a\", \"b\\nc\")` cannot hash to the\n\t// same bytes as `(\"a\\nb\", \"c\")`.\n\tconst input = `${did}\\n${s}`;\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(input));\n\tconst encoded = base32Encode(new Uint8Array(hashBuffer));\n\treturn `r_${encoded.slice(0, HASH_LENGTH)}`;\n}\n\n/**\n * Return whether `pluginId` is a well-formed registry plugin id.\n *\n * This is a syntactic check, not a database lookup -- it answers\n * \"could this id have come from `makeRegistryPluginId`?\", not \"is this\n * plugin installed?\".\n */\nexport function isRegistryPluginId(pluginId: string): boolean {\n\treturn REGISTRY_PLUGIN_ID_PATTERN.test(pluginId);\n}\n","/**\n * Registry plugin install handler.\n *\n * Installs a plugin published to the experimental decentralized plugin\n * registry described in RFC 0001. The install flow:\n *\n *   1. Resolve `(handle, slug)` to a publisher DID via the configured\n *      aggregator's `resolvePackage` XRPC.\n *   2. Look up the requested release (or the policy-filtered latest one)\n *      via `getLatestRelease` / `listReleases`.\n *   3. Require the aggregator's approved listing projection, then apply the\n *      independent release-age and environment policies.\n *   4. Fetch the bundle artifact, walking aggregator mirrors first and\n *      falling back to the publisher-declared URL.\n *   5. Verify the artifact's multibase checksum against the signed\n *      release record's `artifacts.package.checksum`.\n *   6. Extract `manifest.json` + `backend.js` + optional `admin.js` from\n *      the gzipped tar bundle.\n *   7. Store the extracted files in site-local R2 under the\n *      `registry/<plugin-id>/<version>/` prefix.\n *   8. Write a `plugin_states` row with `source = \"registry\"` and the\n *      `(publisher_did, slug)` pair so updates can be resolved later.\n *   9. Sync the runtime so the plugin becomes active immediately.\n *\n * Known gaps (tracked separately):\n *\n *   - The aggregator-supplied records are not yet cryptographically\n *     verified against the publisher's MST signature. The signed bytes\n *     and CIDs are passed through verbatim per the lexicon, but full\n *     PDS-direct verification with proof traversal is follow-up work.\n *     The artifact checksum is verified end-to-end against the value\n *     in the (aggregator-relayed) release record, which is the actual\n *     trust boundary for the bytes that end up in the sandbox.\n *   - Listing approval controls whether metadata is eligible for discovery.\n *     It is never treated as approval of plugin code: checksum, bundle,\n *     manifest, access, consent, sandbox, and environment gates remain\n *     independent below.\n */\n\nimport { ClientResponseError, ClientValidationError } from \"@atcute/client\";\nimport type { Did } from \"@atcute/lexicons\";\nimport { checkEnvCompatibility, findSkippedEnvConstraints } from \"@premium-cms/registry-client/env\";\nimport type { HostEnv } from \"@premium-cms/registry-client/env\";\nimport { evaluateRegistryReleaseWithdrawal } from \"@premium-cms/registry-client/withdrawal\";\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { extractBundle } from \"../../plugins/marketplace.js\";\nimport type { PluginBundle } from \"../../plugins/marketplace.js\";\nimport type { SandboxRunner } from \"../../plugins/sandbox/types.js\";\nimport { PluginStateRepository } from \"../../plugins/state.js\";\nimport {\n\tremoveAllPluginIndexes,\n\tsyncDeclaredStorageIndexes,\n} from \"../../plugins/storage-indexes.js\";\nimport { declaredAccessToCapabilities } from \"../../plugins/types.js\";\nimport type { DeclaredAccess } from \"../../plugins/types.js\";\nimport { assertSafeArtifactUrl, fetchRegistryArtifactUrl } from \"../../registry/artifact-fetch.js\";\nimport {\n\tcanonicalCapabilitiesForDriftCheck,\n\tcoerceRegistryConfig,\n\tparseDurationSeconds,\n\treleaseExemptFromMinimumAge,\n\tvalidateAggregatorUrl,\n} from \"../../registry/config.js\";\nimport { makeRegistryPluginId } from \"../../registry/plugin-id.js\";\nimport type { RegistryConfigInput } from \"../../registry/types.js\";\nimport { EmDashStorageError } from \"../../storage/types.js\";\nimport type { Storage } from \"../../storage/types.js\";\nimport type { ApiResult } from \"../types.js\";\nimport {\n\tdeleteBundleFromR2,\n\tdiffCapabilities,\n\tdiffRouteVisibility,\n\tloadBundleFromR2,\n\tstoreBundleInR2,\n} from \"./marketplace.js\";\n\nexport { assertSafeArtifactUrl } from \"../../registry/artifact-fetch.js\";\n\nconst RELEASE_EXTENSION_NSID = \"com.emdashcms.experimental.package.releaseExtension\";\n\n/**\n * Whether two `declaredAccess` blocks grant exactly the same enforced access --\n * the same capabilities AND the same host allow-list. Both are lowered through\n * the canonical converter so that constraint content (`allowedHosts`), not just\n * the capability set, is part of the comparison. The capability-set consent\n * gate is blind to host scope; this is what keeps a bundle from being installed\n * with a wider (or simply different) host allow-list than its published record\n * advertised and the user consented to.\n */\nexport function enforcedAccessEqual(a: DeclaredAccess, b: DeclaredAccess): boolean {\n\tconst aa = declaredAccessToCapabilities(a);\n\tconst bb = declaredAccessToCapabilities(b);\n\treturn (\n\t\tJSON.stringify(aa.capabilities.toSorted()) === JSON.stringify(bb.capabilities.toSorted()) &&\n\t\tJSON.stringify(aa.allowedHosts.toSorted()) === JSON.stringify(bb.allowedHosts.toSorted())\n\t);\n}\n\n// ── Types ──────────────────────────────────────────────────────────\n\nexport interface RegistryInstallInput {\n\t/**\n\t * Publisher DID. Required. The browser is expected to resolve\n\t * `(handle, slug) → (did, slug)` via the aggregator's\n\t * `resolvePackage` XRPC before posting -- the server then skips that\n\t * round-trip and looks up the package directly.\n\t *\n\t * Passing DID rather than handle here means installs work for\n\t * publishers whose handle the aggregator couldn't resolve at view\n\t * time (handle is \"best-effort\" per the lexicon -- absent for any\n\t * publisher whose DID document didn't resolve cleanly at ingest).\n\t */\n\tdid: string;\n\t/** Package slug (rkey of the publisher's profile record). */\n\tslug: string;\n\t/** Optional explicit version. When omitted, the aggregator's latest. */\n\tversion?: string;\n\t/**\n\t * Capabilities the admin acknowledged in the consent dialog, lifted\n\t * from the release record's `declaredAccess` block. Compared against\n\t * the bundle's `manifest.declaredAccess` to detect drift between\n\t * what the admin agreed to and what the bundle actually requests.\n\t *\n\t * When omitted, drift detection is skipped -- callers that don't\n\t * surface a consent UI before posting (e.g. CI scripts) opt out.\n\t */\n\tacknowledgedDeclaredAccess?: unknown;\n\tacknowledgedMcpTools?: unknown;\n}\n\nexport interface RegistryInstallResult {\n\t/** Hashed, opaque plugin id used everywhere in the runtime. */\n\tpluginId: string;\n\t/** Publisher DID resolved from the handle. */\n\tpublisherDid: string;\n\t/** Publisher slug (== the registry slug). */\n\tslug: string;\n\t/** Installed version. */\n\tversion: string;\n\t/** Capabilities surfaced from the bundle's manifest. */\n\tcapabilities: string[];\n}\n\n// ── Helpers ────────────────────────────────────────────────────────\n\n/** Matches a bare 64-character lowercase/uppercase hex SHA-256 digest. */\nconst SHA256_HEX_PATTERN = /^[a-f0-9]{64}$/i;\n\n/** Compute the SHA-256 of `bytes` as a lowercase hex string. */\nasync function sha256Hex(bytes: Uint8Array): Promise<string> {\n\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Uint8Array is a valid BufferSource at runtime\n\tconst buf = await crypto.subtle.digest(\"SHA-256\", bytes as unknown as BufferSource);\n\tconst arr = new Uint8Array(buf);\n\treturn Array.from(arr, (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n/** multihash code for sha2-256 (single-byte varint). */\nconst MULTIHASH_SHA256_CODE = 0x12;\n/** sha2-256 digest length in bytes (single-byte varint). */\nconst MULTIHASH_SHA256_LENGTH = 0x20;\n\n/**\n * Compute the multibase-multihash sha2-256 checksum of `bytes`, in the\n * same `b<base32>` shape the registry CLI publishes\n * (`packages/plugin-cli/src/multihash.ts`). Returns a 56-character\n * string starting with `b`.\n *\n * The trust contract is: if both sides produce the same string for\n * the same bytes, the bytes are unchanged. We don't decode the\n * publisher-supplied checksum -- we just re-encode our own and compare,\n * which is equivalent and avoids needing a base32 decoder.\n */\nasync function sha256MultibaseMultihash(bytes: Uint8Array): Promise<string> {\n\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- Uint8Array is a valid BufferSource at runtime\n\tconst digestBuf = await crypto.subtle.digest(\"SHA-256\", bytes as unknown as BufferSource);\n\tconst digest = new Uint8Array(digestBuf);\n\tconst multihash = new Uint8Array(2 + digest.length);\n\tmultihash[0] = MULTIHASH_SHA256_CODE;\n\tmultihash[1] = MULTIHASH_SHA256_LENGTH;\n\tmultihash.set(digest, 2);\n\tconst { toBase32 } = await import(\"@atcute/multibase\");\n\treturn `b${toBase32(multihash)}`;\n}\n\n/**\n * Verify that a checksum string from a release record's\n * `artifact.checksum` field corresponds to the SHA-256 of the given\n * bytes.\n *\n * Accepts two formats:\n *\n *   - Bare lowercase/uppercase hex SHA-256 (64 chars). Convenience for\n *     publishers / tools that emit hex rather than multibase.\n *   - Multibase-multihash with the `b` (base32) prefix and sha2-256.\n *     This is the format RFC 0001 mandates and the registry CLI emits\n *     (see `packages/plugin-cli/src/multihash.ts`).\n *\n * Hash functions other than sha2-256 are out of scope for this\n * initial release; the install fails closed.\n */\nexport async function verifyChecksum(bytes: Uint8Array, checksum: string): Promise<boolean> {\n\tif (SHA256_HEX_PATTERN.test(checksum)) {\n\t\tconst actual = await sha256Hex(bytes);\n\t\treturn checksum.toLowerCase() === actual;\n\t}\n\n\t// Multibase-base32 multihash with sha2-256. We re-encode our own\n\t// digest in the same shape and compare strings -- equivalent to\n\t// decoding and comparing bytes, but doesn't need a base32 decoder.\n\t// 56 chars = 'b' + base32(34 bytes) = 'b' + 55 chars.\n\tif (checksum.length === 56 && checksum.startsWith(\"b\")) {\n\t\tconst actual = await sha256MultibaseMultihash(bytes);\n\t\t// Case-insensitive: multibase 'b' is lowercase by convention but\n\t\t// some emitters use uppercase. RFC 4648 base32 alphabets are\n\t\t// case-insensitive.\n\t\treturn actual.toLowerCase() === checksum.toLowerCase();\n\t}\n\n\treturn false;\n}\n\n/**\n * Bytes-per-artifact cap on the gzipped tarball we'll download before\n * decompression. RFC 0001 caps a sandboxed plugin bundle at 256 KiB\n * decompressed (see `MAX_BUNDLE_SIZE` in cli/commands/bundle-utils.ts);\n * gzip on a mix of JSON manifest + JS code typically gives 0.3-0.6\n * ratio, so compressed bundles are well under 200 KiB in practice.\n * 512 KiB leaves margin for unusual file mixes that compress poorly\n * while still rejecting anything that's obviously not a legitimate\n * plugin bundle.\n */\nconst MAX_ARTIFACT_BYTES = 512 * 1024;\n\n/**\n * Maximum number of HTTP redirects followed during artifact download.\n * Each hop is independently URL-validated, so a malicious server cannot\n * redirect through a series of allowed-looking origins to reach a\n * forbidden one.\n */\nconst MAX_REDIRECTS = 5;\n\n/**\n * Wall-clock cap on any single artifact fetch attempt (per URL).\n * Defends against slow-loris mirrors that accept the connection but\n * never finish sending headers or body.\n */\nconst ARTIFACT_FETCH_TIMEOUT_MS = 15_000;\n\n/**\n * Total wall-clock budget for the artifact-download phase across all\n * mirrors and the declared URL. Even with the per-URL timeout, a\n * malicious mirror list could otherwise tie up the install request for\n * minutes; this caps total time at a budget interactive admins can\n * tolerate. Tuned so a fast happy path takes <1s of budget per\n * attempt and a worst case still completes in under a minute.\n */\nconst ARTIFACT_TOTAL_BUDGET_MS = 45_000;\n\n/**\n * Cap on the number of mirror URLs we try before falling back to the\n * publisher-declared URL. Matches the aggregator lexicon's\n * `mirrors` array length cap (16) but enforced here independently so\n * a misbehaving aggregator can't slow-loris us through hundreds of\n * URLs.\n */\nconst MAX_MIRRORS = 16;\n\n/**\n * Per-request timeout applied to every aggregator XRPC call\n * (`resolvePackage`, `getLatestRelease`, `listReleases`). Matches the\n * per-URL artifact-fetch cap. Without this, a slow-loris aggregator\n * can stall the install before the artifact phase even starts.\n */\nconst AGGREGATOR_REQUEST_TIMEOUT_MS = 15_000;\n\n/**\n * Total wall-clock budget for the aggregator-discovery phase\n * (resolve + selected-release lookup). Mirrors the artifact-download\n * budget. Worst case with the pinned-version path's 20-page cap is\n * 20 + 1 calls; capping the total ensures any one stalled call\n * still bounds the whole phase.\n */\nconst AGGREGATOR_TOTAL_BUDGET_MS = 30_000;\n\n/** Build a fetch function that enforces a per-request and per-budget timeout. */\nfunction timedFetch(totalDeadline: number): typeof fetch {\n\treturn (input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => {\n\t\tconst now = Date.now();\n\t\tconst remaining = Math.max(0, totalDeadline - now);\n\t\tif (remaining === 0) {\n\t\t\treturn Promise.reject(new Error(\"Aggregator request budget exhausted\"));\n\t\t}\n\t\tconst timeout = Math.min(AGGREGATOR_REQUEST_TIMEOUT_MS, remaining);\n\t\tconst controller = new AbortController();\n\t\tconst timer = setTimeout(() => controller.abort(), timeout);\n\t\tconst callerSignal = init?.signal;\n\t\tif (callerSignal) {\n\t\t\tif (callerSignal.aborted) controller.abort(callerSignal.reason);\n\t\t\telse callerSignal.addEventListener(\"abort\", () => controller.abort(callerSignal.reason));\n\t\t}\n\t\treturn fetch(input, { ...init, signal: controller.signal }).finally(() => {\n\t\t\tclearTimeout(timer);\n\t\t});\n\t};\n}\n\n/**\n * Fetch one URL with manual redirect handling so every hop is\n * URL-validated, a hard byte cap so a malicious response body cannot\n * exhaust memory before the checksum check rejects it, and a wall-clock\n * timeout that covers connect, headers, and body together. The timeout\n * is the minimum of the per-URL cap and the remaining total budget so\n * a late-arriving mirror still respects the install's global budget.\n */\nasync function fetchWithLimits(initialUrl: string, totalDeadline: number): Promise<Uint8Array> {\n\tconst now = Date.now();\n\tconst remaining = Math.max(0, totalDeadline - now);\n\tif (remaining === 0) {\n\t\tthrow new Error(\"Artifact download budget exhausted\");\n\t}\n\tconst perUrlTimeout = Math.min(ARTIFACT_FETCH_TIMEOUT_MS, remaining);\n\tconst controller = new AbortController();\n\tconst timer = setTimeout(() => controller.abort(), perUrlTimeout);\n\ttry {\n\t\tlet current = new URL(initialUrl);\n\t\tlet response: Response;\n\t\tfor (let hop = 0; hop <= MAX_REDIRECTS; hop++) {\n\t\t\tresponse = await fetchRegistryArtifactUrl(current.href, {\n\t\t\t\tsignal: controller.signal,\n\t\t\t\tmaxResponseBytes: MAX_ARTIFACT_BYTES,\n\t\t\t});\n\t\t\tif (response.status < 300 || response.status >= 400) break;\n\t\t\tconst location = response.headers.get(\"location\");\n\t\t\tif (!location) break;\n\t\t\tif (hop === MAX_REDIRECTS) {\n\t\t\t\tthrow new Error(`Too many redirects fetching artifact (>${MAX_REDIRECTS})`);\n\t\t\t}\n\t\t\tcurrent = new URL(location, current);\n\t\t}\n\t\t// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- response is assigned in the first loop iteration\n\t\tconst finalResponse = response!;\n\t\tif (!finalResponse.ok) {\n\t\t\tthrow new Error(`HTTP ${finalResponse.status}`);\n\t\t}\n\n\t\t// Check Content-Length up front when present. Untrusted servers can\n\t\t// lie or omit it; the streaming cap below is the real defense.\n\t\tconst lengthHeader = finalResponse.headers.get(\"content-length\");\n\t\tif (lengthHeader) {\n\t\t\tconst declared = Number(lengthHeader);\n\t\t\tif (Number.isFinite(declared) && declared > MAX_ARTIFACT_BYTES) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Artifact too large (declared ${declared} bytes, limit ${MAX_ARTIFACT_BYTES})`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tconst body = finalResponse.body;\n\t\tif (!body) {\n\t\t\t// Workers can't return a null body for a normal GET; defensive fallback.\n\t\t\tconst buf = new Uint8Array(await finalResponse.arrayBuffer());\n\t\t\tif (buf.byteLength > MAX_ARTIFACT_BYTES) {\n\t\t\t\tthrow new Error(`Artifact too large (limit ${MAX_ARTIFACT_BYTES} bytes)`);\n\t\t\t}\n\t\t\treturn buf;\n\t\t}\n\n\t\tconst reader = body.getReader();\n\t\tconst chunks: Uint8Array[] = [];\n\t\tlet total = 0;\n\t\twhile (true) {\n\t\t\tconst { done, value } = await reader.read();\n\t\t\tif (done) break;\n\t\t\tif (!value) continue;\n\t\t\ttotal += value.byteLength;\n\t\t\tif (total > MAX_ARTIFACT_BYTES) {\n\t\t\t\ttry {\n\t\t\t\t\tawait reader.cancel();\n\t\t\t\t} catch {\n\t\t\t\t\t// nothing to do\n\t\t\t\t}\n\t\t\t\tthrow new Error(`Artifact too large (limit ${MAX_ARTIFACT_BYTES} bytes)`);\n\t\t\t}\n\t\t\tchunks.push(value);\n\t\t}\n\n\t\tconst out = new Uint8Array(total);\n\t\tlet offset = 0;\n\t\tfor (const chunk of chunks) {\n\t\t\tout.set(chunk, offset);\n\t\t\toffset += chunk.byteLength;\n\t\t}\n\t\treturn out;\n\t} finally {\n\t\tclearTimeout(timer);\n\t}\n}\n\n/**\n * Strip query string and fragment from a URL for use in\n * client-visible error messages. Registry artifacts are often hosted\n * on storage backends that include presigned tokens in the query\n * string; surfacing the raw URL on a failed install leaks those\n * tokens into the admin's HTTP response and any log drain that\n * captures the error chain. Origin + pathname is enough to identify\n * the host and resource without exposing credentials.\n *\n * Falls back to a generic placeholder when the URL is malformed.\n */\nfunction redactUrlForError(raw: string): string {\n\ttry {\n\t\tconst u = new URL(raw);\n\t\treturn `${u.origin}${u.pathname}`;\n\t} catch {\n\t\treturn \"<malformed url>\";\n\t}\n}\n\n/** Walk artifact source URLs in priority order and return the first that fetches successfully. */\nasync function fetchArtifact(mirrors: string[], declaredUrl: string): Promise<Uint8Array> {\n\t// Clamp mirrors regardless of what the lexicon type says -- a buggy\n\t// or malicious aggregator could return more than the spec'd limit\n\t// and slow-loris each one. The declared URL is always tried last.\n\tconst clampedMirrors = mirrors.slice(0, MAX_MIRRORS);\n\tconst urls = [...clampedMirrors, declaredUrl];\n\t// Client-visible errors carry redacted URLs (origin + path only).\n\t// The full URL with any query-string token is logged server-side\n\t// so operators can still debug delivery failures.\n\tconst clientErrors: string[] = [];\n\n\tconst totalDeadline = Date.now() + ARTIFACT_TOTAL_BUDGET_MS;\n\n\tfor (const url of urls) {\n\t\tif (Date.now() >= totalDeadline) {\n\t\t\tclientErrors.push(\"(total artifact download budget exhausted)\");\n\t\t\tbreak;\n\t\t}\n\t\ttry {\n\t\t\treturn await fetchWithLimits(url, totalDeadline);\n\t\t} catch (err) {\n\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\tconsole.warn(`[registry-install] Artifact fetch failed from ${url}:`, message);\n\t\t\tclientErrors.push(`${redactUrlForError(url)}: ${message}`);\n\t\t}\n\t}\n\n\tthrow new Error(\n\t\t`Failed to download artifact from any source. Tried:\\n  ${clientErrors.join(\"\\n  \")}`,\n\t);\n}\n\n/**\n * The shape of a single env-compatibility failure returned to the admin in\n * the `ENV_INCOMPATIBLE` error's `details`.\n */\ninterface EnvIncompatibleError {\n\tcode: \"ENV_INCOMPATIBLE\";\n\tmessage: string;\n\tdetails: { requires: Record<string, string>; host: HostEnv };\n}\n\n/**\n * Gate a release's `requires` constraints against the running host\n * environment. `requires` is the lexicon-`unknown` value off the signed\n * release record — never trust its shape; `checkEnvCompatibility` guards it.\n *\n * Returns `null` when every advertised constraint is satisfied (or there are\n * none), or a structured `ENV_INCOMPATIBLE` error naming the unsatisfied\n * constraints and the host versions. The error carries the guarded `requires`\n * and `host` maps so the admin can render the same mismatch the UI gate shows.\n */\nexport function assertEnvCompatible(\n\trequires: unknown,\n\thostEnv: HostEnv,\n): EnvIncompatibleError | null {\n\t// A constraint the host can't evaluate (unknown or unparseable host\n\t// version) downgrades the gate to a no-op for that env. Log it so a\n\t// silent bypass is observable rather than invisible.\n\tfor (const skipped of findSkippedEnvConstraints(requires, hostEnv)) {\n\t\tconsole.warn(\n\t\t\t`[registry] env compatibility constraint skipped: ${skipped.key} requires ${skipped.required} but host version is ${skipped.reason}`,\n\t\t);\n\t}\n\tconst mismatches = checkEnvCompatibility(requires, hostEnv);\n\tif (mismatches.length === 0) return null;\n\tconst guarded: Record<string, string> = {};\n\tfor (const m of mismatches) guarded[m.key] = m.required;\n\tconst summary = mismatches\n\t\t.map((m) => `${m.key} requires ${m.required} but host is ${m.host}`)\n\t\t.join(\"; \");\n\treturn {\n\t\tcode: \"ENV_INCOMPATIBLE\",\n\t\tmessage: `This release is not compatible with the current environment: ${summary}.`,\n\t\tdetails: { requires: guarded, host: hostEnv },\n\t};\n}\n\n// ── Install ────────────────────────────────────────────────────────\n\nexport async function handleRegistryInstall(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n\tsandboxRunner: SandboxRunner | null,\n\tregistryConfigInput: RegistryConfigInput | undefined,\n\tinput: RegistryInstallInput,\n\topts?: { configuredPluginIds?: Set<string>; hostEnv?: HostEnv },\n): Promise<ApiResult<RegistryInstallResult>> {\n\t// Accept either the bare-string shorthand or the full\n\t// `RegistryConfig` object (see `RegistryConfigInput`).\n\tconst registryConfig = coerceRegistryConfig(registryConfigInput);\n\tif (!registryConfig) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"REGISTRY_NOT_CONFIGURED\",\n\t\t\t\tmessage: \"Registry 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 registry 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 registry plugins\",\n\t\t\t},\n\t\t};\n\t}\n\n\t// Defense in depth: validate the aggregator URL even though the same\n\t// check runs at config-normalize time. Keeps every entrypoint into\n\t// `handleRegistryInstall` safe regardless of how the caller obtained\n\t// the config.\n\ttry {\n\t\tvalidateAggregatorUrl(registryConfig.aggregatorUrl);\n\t} catch (err) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"REGISTRY_NOT_CONFIGURED\",\n\t\t\t\tmessage: err instanceof Error ? err.message : \"Invalid aggregator URL\",\n\t\t\t},\n\t\t};\n\t}\n\n\tconst { did, slug, version: requestedVersion } = input;\n\n\t// Lazy-load the discovery client. Avoids pulling @atcute/client into\n\t// every code path that imports core/api/handlers.\n\tconst { DiscoveryClient, registryLabelerPolicy } =\n\t\tawait import(\"@premium-cms/registry-client/discovery\");\n\n\t// Every aggregator XRPC call passes through `timedFetch`, which\n\t// enforces a per-request timeout and shares a single total-budget\n\t// deadline. Defends against a slow-loris aggregator stalling the\n\t// install before the artifact phase begins.\n\tconst aggregatorDeadline = Date.now() + AGGREGATOR_TOTAL_BUDGET_MS;\n\tconst discovery = new DiscoveryClient({\n\t\taggregatorUrl: registryConfig.aggregatorUrl,\n\t\tacceptLabelers: registryConfig.acceptLabelers,\n\t\tlabelerPolicy: registryLabelerPolicy(registryConfig.acceptLabelers),\n\t\tfetch: timedFetch(aggregatorDeadline),\n\t});\n\n\t// Basic shape check on the DID. The browser is expected to send a\n\t// DID resolved via the aggregator's `resolvePackage`; reject obvious\n\t// malformations here rather than letting the XRPC call fail\n\t// opaquely. The lexicon's `did:${string}:${string}` template is the\n\t// authoritative check.\n\tif (!did.startsWith(\"did:\") || did.split(\":\").length < 3) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"INVALID_DID\",\n\t\t\t\tmessage: \"DID must be a valid atproto DID (e.g. did:plc:abc123)\",\n\t\t\t},\n\t\t};\n\t}\n\n\ttry {\n\t\t// Step 1: look up the package by DID + slug. The browser already\n\t\t// resolved any handle to a DID via `resolvePackage`; we skip that\n\t\t// round-trip and go straight to `getPackage`.\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- validated above\n\t\tconst publisherDid = did as Did;\n\t\tconst packageView = await discovery.getPackage({\n\t\t\tdid: publisherDid,\n\t\t\tslug,\n\t\t});\n\n\t\t// Step 2: select the target release.\n\t\t// For an explicit version, page through listReleases until we find\n\t\t// the matching record; the aggregator returns releases ordered by\n\t\t// semver descending. For \"latest\", use the dedicated convenience\n\t\t// endpoint which applies the aggregator's policy filter (yanked\n\t\t// exclusion etc.) server-side.\n\t\t//\n\t\t// Pagination is bounded both by total pages and by repeated-cursor\n\t\t// detection: a buggy or compromised aggregator could otherwise\n\t\t// return endless distinct cursors that never include the\n\t\t// requested version, hanging the install for the platform's\n\t\t// request-time budget.\n\t\tconst MAX_LIST_PAGES = 20; // 20 * 50 limit = 1000 releases worth\n\t\tconst latestRelease = await (async () => {\n\t\t\tif (!requestedVersion) {\n\t\t\t\treturn discovery.getLatestRelease({\n\t\t\t\t\tdid: publisherDid,\n\t\t\t\t\tpackage: slug,\n\t\t\t\t});\n\t\t\t}\n\t\t\tlet cursor: string | undefined;\n\t\t\tconst seenCursors = new Set<string>();\n\t\t\tfor (let page = 0; page < MAX_LIST_PAGES; page++) {\n\t\t\t\tif (cursor !== undefined) {\n\t\t\t\t\tif (seenCursors.has(cursor)) break;\n\t\t\t\t\tseenCursors.add(cursor);\n\t\t\t\t}\n\t\t\t\tconst result = await discovery.listReleases({\n\t\t\t\t\tdid: publisherDid,\n\t\t\t\t\tpackage: slug,\n\t\t\t\t\tcursor,\n\t\t\t\t\tlimit: 50,\n\t\t\t\t});\n\t\t\t\tfor (const r of result.releases) {\n\t\t\t\t\tif (r.version === requestedVersion) return r;\n\t\t\t\t}\n\t\t\t\tif (!result.cursor) break;\n\t\t\t\tcursor = result.cursor;\n\t\t\t}\n\t\t\treturn undefined;\n\t\t})();\n\t\tconst releaseView = latestRelease;\n\n\t\tif (!releaseView) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NO_RELEASE\",\n\t\t\t\t\tmessage: requestedVersion\n\t\t\t\t\t\t? `Version ${requestedVersion} not found for ${publisherDid}/${slug}`\n\t\t\t\t\t\t: `No installable release found for ${publisherDid}/${slug}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Identity cross-check on every field the aggregator denormalises\n\t\t// onto the package and release views. A buggy or compromised\n\t\t// aggregator could otherwise return a release view for a\n\t\t// different `(did, slug, version)` than we asked for; the\n\t\t// handler would then fetch + checksum-verify + install bytes\n\t\t// under the requested package's pluginId but for a different\n\t\t// publisher's record. Checksum verification only proves the bytes\n\t\t// match the *returned* record, not that the record belongs to\n\t\t// the package we requested.\n\t\t// `releaseView.release` is validated against the release lexicon by\n\t\t// DiscoveryClient (or `null` if it didn't conform). A `null` here makes\n\t\t// the identity checks below fail closed, which is the desired outcome.\n\t\tconst signedRelease = releaseView.release;\n\t\tif (packageView.did !== publisherDid || packageView.slug !== slug) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"AGGREGATOR_IDENTITY_MISMATCH\",\n\t\t\t\t\tmessage: \"Aggregator returned a package view for a different publisher or slug.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (\n\t\t\treleaseView.did !== publisherDid ||\n\t\t\treleaseView.package !== slug ||\n\t\t\tsignedRelease?.package !== slug ||\n\t\t\t(requestedVersion !== undefined && releaseView.version !== requestedVersion) ||\n\t\t\tsignedRelease?.version !== releaseView.version\n\t\t) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"AGGREGATOR_IDENTITY_MISMATCH\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t\"Aggregator returned a release view that does not match the requested package or version.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst version = releaseView.version;\n\t\tif (evaluateRegistryReleaseWithdrawal(releaseView, discovery.labelerPolicy).withdrawn) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"RELEASE_YANKED\",\n\t\t\t\t\tmessage: \"This release has been withdrawn\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Environment compatibility remains an install-safety gate. Listing\n\t\t// approval says only that displayed metadata passed moderation. A release\n\t\t// may carry a `requires` block (`env:emdash`, `env:astro`, ...). Refuse\n\t\t// the install if the running host doesn't satisfy a constraint, so a\n\t\t// stale browser tab or non-UI caller can't bypass the admin's\n\t\t// disabled Install button. `requires` is lexicon-`unknown`; the\n\t\t// helper guards its shape.\n\t\tif (opts?.hostEnv) {\n\t\t\tconst envError = assertEnvCompatible(releaseView.release?.requires, opts.hostEnv);\n\t\t\tif (envError) return { success: false, error: envError };\n\t\t}\n\n\t\t// Step 3a: enforce the configured minimum release age. The browser\n\t\t// applies the same check up front for UX, but the gate lives here\n\t\t// -- a stale browser tab, a deep link, or a non-admin-UI caller\n\t\t// must still hit the holdback. The `minimumReleaseAgeExclude`\n\t\t// allowlist short-circuits the check for trusted publisher DIDs.\n\t\t//\n\t\t// Caveat: `releaseView.indexedAt` is aggregator-supplied envelope\n\t\t// data, not a signed timestamp. A compromised aggregator can\n\t\t// claim an arbitrary indexed-at date and bypass the holdback;\n\t\t// closing this gap requires fetching the release record's\n\t\t// signed createdAt from the publisher's PDS (deferred to the\n\t\t// follow-up that adds full MST verification). If the timestamp\n\t\t// is missing or malformed, we fail closed and reject the install.\n\t\t// `registryConfig` is the user-supplied integration option, not\n\t\t// the normalized manifest shape, so the duration parse runs once\n\t\t// per install. Catch a malformed value here -- normally caught at\n\t\t// `normalizeRegistryConfig` time, but a future config-mutation\n\t\t// path could re-enter with a bad value -- and surface it as a\n\t\t// structured error rather than letting it bubble out as a generic\n\t\t// 500.\n\t\tconst minimumReleaseAge = registryConfig.policy?.minimumReleaseAge;\n\t\tlet minimumReleaseAgeSeconds = 0;\n\t\tif (minimumReleaseAge !== undefined) {\n\t\t\ttry {\n\t\t\t\tminimumReleaseAgeSeconds = parseDurationSeconds(minimumReleaseAge);\n\t\t\t} catch (err) {\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: \"REGISTRY_POLICY_INVALID\",\n\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\terr instanceof Error\n\t\t\t\t\t\t\t\t? err.message\n\t\t\t\t\t\t\t\t: \"Invalid minimumReleaseAge value in registry config\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\t\tif (minimumReleaseAgeSeconds > 0) {\n\t\t\tconst exclude = registryConfig.policy?.minimumReleaseAgeExclude?.map((e) =>\n\t\t\t\te.trim().toLowerCase(),\n\t\t\t);\n\t\t\tconst exempt = releaseExemptFromMinimumAge(exclude, publisherDid, slug);\n\t\t\tif (!exempt) {\n\t\t\t\tconst indexedAt = Date.parse(releaseView.indexedAt);\n\t\t\t\tif (!Number.isFinite(indexedAt)) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tsuccess: false,\n\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\tcode: \"RELEASE_TIMESTAMP_INVALID\",\n\t\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\t\t\"Release record is missing a valid indexed-at timestamp; cannot evaluate minimum release age policy.\",\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\tconst ageSeconds = (Date.now() - indexedAt) / 1000;\n\t\t\t\tif (ageSeconds < minimumReleaseAgeSeconds) {\n\t\t\t\t\tconst remaining = Math.ceil(minimumReleaseAgeSeconds - ageSeconds);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tsuccess: false,\n\t\t\t\t\t\terror: {\n\t\t\t\t\t\t\tcode: \"RELEASE_TOO_NEW\",\n\t\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\t\t`This release does not meet the configured minimum release age of ` +\n\t\t\t\t\t\t\t\t`${minimumReleaseAgeSeconds}s. It will be installable in ~${remaining}s.`,\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\t// Derive the normalized opaque plugin id we'll use as the\n\t\t// runtime-wide identifier from here on. The publisher_did + slug\n\t\t// stay in the state row for update resolution and admin display.\n\t\tconst pluginId = await makeRegistryPluginId(publisherDid, slug);\n\n\t\t// Block installation if a configured (trusted) plugin shares this\n\t\t// id. Mirrors the marketplace install's PLUGIN_ID_CONFLICT check.\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: \"A configured plugin with the same derived id already exists\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Check for an existing install (any source) under the derived id.\n\t\t// We reject all pre-existing rows -- if the row is from a registry\n\t\t// install of this same package, the caller should go through the\n\t\t// (future) update flow; if it's from any other source, the\n\t\t// pluginId collision means installing would silently mutate an\n\t\t// unrelated plugin's lifecycle row.\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst existing = await stateRepo.get(pluginId);\n\t\tif (existing) {\n\t\t\tif (existing.source === \"registry\") {\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: \"ALREADY_INSTALLED\",\n\t\t\t\t\t\tmessage: `Plugin ${publisherDid}/${slug} is already installed`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"PLUGIN_ID_COLLISION\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t`A non-registry plugin already exists at the derived id ${pluginId}. ` +\n\t\t\t\t\t\t\"Uninstall it before installing this registry plugin.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Step 4: fetch the artifact bytes.\n\t\t// `releaseView.release` is lexicon-validated by DiscoveryClient (or\n\t\t// `null`); a missing url/checksum (incl. the `null` case) fails closed\n\t\t// below. Mirrors come from the envelope (aggregator operational data,\n\t\t// not part of the signed record).\n\t\tconst release = releaseView.release;\n\t\tconst declaredUrl = release?.artifacts?.package?.url;\n\t\tconst declaredChecksum = release?.artifacts?.package?.checksum;\n\n\t\tif (!declaredUrl || !declaredChecksum) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"INVALID_RELEASE\",\n\t\t\t\t\tmessage: \"Release record is missing artifact url or checksum\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst mirrors = releaseView.mirrors ?? [];\n\t\tconst artifactBytes = await fetchArtifact(mirrors, declaredUrl);\n\n\t\t// Step 5: verify the bytes against the signed record's checksum.\n\t\tconst checksumOk = await verifyChecksum(artifactBytes, declaredChecksum);\n\t\tif (!checksumOk) {\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:\n\t\t\t\t\t\t\"Artifact bytes do not match the release record's checksum, or the checksum encoding is unsupported.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Step 6: extract the bundle.\n\t\tlet bundle: PluginBundle;\n\t\ttry {\n\t\t\tbundle = await extractBundle(artifactBytes);\n\t\t} catch (err) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"INVALID_BUNDLE\",\n\t\t\t\t\tmessage: err instanceof Error ? err.message : \"Failed to extract plugin bundle\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Manifest sanity: declared version must match the release's version.\n\t\tif (bundle.manifest.version !== version) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"MANIFEST_VERSION_MISMATCH\",\n\t\t\t\t\tmessage: `Bundle manifest version (${bundle.manifest.version}) does not match release version (${version})`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Manifest identity: the bundle's `manifest.id` is the publisher's\n\t\t// natural plugin id (their slug). It MUST equal the slug the\n\t\t// install was requested for; otherwise a malicious registry bundle\n\t\t// could declare `manifest.id: \"audit-log\"` and confuse the sandbox\n\t\t// bridge, which uses `manifest.id` as the trust key for\n\t\t// per-plugin storage, cron schedules, and bridge-scoped\n\t\t// operations.\n\t\tif (bundle.manifest.id !== slug) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"MANIFEST_ID_MISMATCH\",\n\t\t\t\t\tmessage: `Bundle manifest id (${bundle.manifest.id}) does not match registry slug (${slug})`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Rewrite the manifest's id to the derived opaque pluginId before\n\t\t// it reaches R2 storage or the sandbox loader. The sandbox uses\n\t\t// `manifest.id` as its identity for per-plugin storage and bridge\n\t\t// calls; addressing it by the same pluginId we use in the runtime\n\t\t// cache, R2 prefix, and `_plugin_state` row keeps every layer\n\t\t// in sync and prevents registry installs from colliding with\n\t\t// marketplace plugins that happen to share the publisher's slug.\n\t\tbundle.manifest = { ...bundle.manifest, id: pluginId };\n\n\t\t// Integrity: the bundle that will run MUST declare exactly the access\n\t\t// the signed release record advertises. The consent dialog is driven\n\t\t// from the record's `declaredAccess`, so a bundle enforcing something\n\t\t// different -- a wider host allow-list, an extra capability -- would run\n\t\t// outside what the user reviewed. The capability-set consent gate below\n\t\t// is blind to constraint content (host scope), so compare the full\n\t\t// enforced access of record vs bundle here and refuse on any difference.\n\t\tconst recordExt =\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- extensions is the lexicon's open `unknown` map; narrow to read our own extension\n\t\t\t(release?.extensions as Record<string, { declaredAccess?: DeclaredAccess }> | undefined)?.[\n\t\t\t\tRELEASE_EXTENSION_NSID\n\t\t\t];\n\t\tif (\n\t\t\t!enforcedAccessEqual(recordExt?.declaredAccess ?? {}, bundle.manifest.declaredAccess ?? {})\n\t\t) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"DECLARED_ACCESS_DRIFT\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t\"The plugin bundle declares different permissions than its published record. Installation refused.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Capability consent gate: the admin MUST acknowledge the\n\t\t// capabilities the bundle's manifest actually declares before we\n\t\t// install it. The bundle manifest is the only source of truth\n\t\t// the runtime sandbox enforces -- the release record's\n\t\t// `declaredAccess` extension is an aggregator-supplied\n\t\t// assertion that the publisher may or may not have included,\n\t\t// and trusting it would let a malicious publisher (or a\n\t\t// compromised aggregator) ship a bundle whose manifest\n\t\t// requests `content:*` etc. behind an empty consent dialog.\n\t\t//\n\t\t// Two outcomes after normalization (filter to strings, dedupe,\n\t\t// sort):\n\t\t//\n\t\t//   1. The bundle declares no capabilities: install is allowed\n\t\t//      without any acknowledgement (nothing to consent to).\n\t\t//   2. The bundle declares capabilities: install requires the\n\t\t//      caller to send `acknowledgedDeclaredAccess`, and the\n\t\t//      sorted lists must match exactly.\n\t\t//\n\t\t// We compare against the bundle's *capabilities* (the legacy\n\t\t// shape) for v1 because EmDash's existing sandbox enforces\n\t\t// capabilities, not the RFC's structured `declaredAccess`. Once\n\t\t// the runtime starts enforcing `declaredAccess` natively, this\n\t\t// comparison switches to that shape.\n\t\tconst actualCapabilities = canonicalCapabilitiesForDriftCheck(bundle.manifest.capabilities);\n\t\tif (actualCapabilities.length > 0) {\n\t\t\tif (input.acknowledgedDeclaredAccess === undefined) {\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: \"DECLARED_ACCESS_REQUIRED\",\n\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\t\"This plugin declares capabilities that require consent. Re-open the install dialog to review and acknowledge them.\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\tconst acknowledged = canonicalCapabilitiesForDriftCheck(input.acknowledgedDeclaredAccess);\n\t\t\tif (\n\t\t\t\tacknowledged.length !== actualCapabilities.length ||\n\t\t\t\tacknowledged.some((cap, i) => cap !== actualCapabilities[i])\n\t\t\t) {\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: \"DECLARED_ACCESS_DRIFT\",\n\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\t\"Plugin manifest has changed since you consented. Re-open the install dialog to review the new permissions.\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tconst actualMcpTools = (bundle.manifest.mcp?.tools ?? []).map(\n\t\t\t({ inputSchema: _, outputSchema: __, ...tool }) => tool,\n\t\t);\n\t\tif (actualMcpTools.length > 0) {\n\t\t\tif (JSON.stringify(input.acknowledgedMcpTools) !== JSON.stringify(actualMcpTools)) {\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: \"MCP_TOOL_CONSENT_REQUIRED\",\n\t\t\t\t\t\tmessage: \"Plugin MCP tools require explicit consent\",\n\t\t\t\t\t\tdetails: { mcpTools: actualMcpTools },\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Step 7: store in R2 under the registry prefix.\n\t\tawait storeBundleInR2(storage, pluginId, version, bundle, \"registry\");\n\n\t\t// Step 8: write plugin state.\n\t\t// Display name and description come from the *package profile*\n\t\t// (the signed record from the publisher's repo), not from the\n\t\t// bundle manifest -- the manifest carries the trust contract,\n\t\t// the profile carries the marketing copy.\n\t\t//\n\t\t// On failure, we may need to clean up the R2 bundle we just\n\t\t// wrote. But two parallel installs of the same (did, slug,\n\t\t// version) both pass the earlier `existing` check at line 822\n\t\t// (the read is not transactional with the insert), both upload\n\t\t// to the same deterministic R2 prefix (overwrites are\n\t\t// content-identical because R2 keys include the version and\n\t\t// the bundle is checksum-verified upstream), and then one wins\n\t\t// the insert while the other fails with a PK constraint\n\t\t// violation.\n\t\t//\n\t\t// If we blindly clean up R2 on every state-write failure, the\n\t\t// loser of that race would delete the winner's bundle and the\n\t\t// runtime would fail to load the plugin on the next sync.\n\t\t//\n\t\t// Instead: on state-write failure, re-query the state row. If\n\t\t// a row now exists for this pluginId, we lost the race -- the\n\t\t// winner owns the R2 bundle and we must not touch it. If the\n\t\t// row doesn't exist, the failure was a real DB error and the\n\t\t// R2 bytes are orphans; clean them up.\n\t\t//\n\t\t// Cleanup is best-effort; if it also fails, the row failure\n\t\t// still surfaces to the caller and the orphan R2 bundle costs\n\t\t// only the storage of a single checksum-verified zip.\n\t\t// `packageView.profile` is lexicon-validated by DiscoveryClient (or null).\n\t\tconst profile = packageView.profile;\n\t\ttry {\n\t\t\tawait stateRepo.upsert(pluginId, version, \"active\", {\n\t\t\t\tsource: \"registry\",\n\t\t\t\tdisplayName: profile?.name ?? slug,\n\t\t\t\tdescription: profile?.description ?? undefined,\n\t\t\t\tregistryPublisherDid: publisherDid,\n\t\t\t\tregistrySlug: slug,\n\t\t\t});\n\t\t} catch (stateErr) {\n\t\t\tlet lostRace = false;\n\t\t\ttry {\n\t\t\t\tconst winner = await stateRepo.get(pluginId);\n\t\t\t\tlostRace = winner !== undefined && winner !== null;\n\t\t\t} catch (probeErr) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`[registry-install] Failed to probe state row for ${pluginId} after state-write failure; treating as orphan:`,\n\t\t\t\t\tprobeErr,\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (!lostRace) {\n\t\t\t\ttry {\n\t\t\t\t\tawait deleteBundleFromR2(storage, pluginId, version, \"registry\");\n\t\t\t\t} catch (cleanupErr) {\n\t\t\t\t\tconsole.warn(\n\t\t\t\t\t\t`[registry-install] Failed to clean up R2 bundle for ${pluginId}@${version} after state-row write failure:`,\n\t\t\t\t\t\tcleanupErr,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow stateErr;\n\t\t}\n\n\t\tawait syncDeclaredStorageIndexes(db, [bundle.manifest]);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tpluginId,\n\t\t\t\tpublisherDid,\n\t\t\t\tslug,\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 ClientValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"AGGREGATOR_RESPONSE_INVALID\",\n\t\t\t\t\tmessage: `Aggregator returned a response that does not conform to its lexicon (${err.target})`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (err instanceof ClientResponseError) {\n\t\t\tif (err.error === \"ListingUnavailable\") {\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: \"LISTING_UNAVAILABLE\",\n\t\t\t\t\t\tmessage: \"This plugin is unavailable under the active registry policy\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: err.status === 404 ? \"AGGREGATOR_NOT_FOUND\" : \"AGGREGATOR_HTTP_ERROR\",\n\t\t\t\t\tmessage: `Aggregator returned ${err.status}: ${err.error}`,\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\tconsole.error(\"[registry-install] Failed:\", 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: err instanceof Error ? err.message : \"Failed to install plugin from registry\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ── Uninstall ──────────────────────────────────────────────────────\n\nexport interface RegistryUninstallResult {\n\tpluginId: string;\n\t/** True when `_plugin_storage` rows were also deleted (opts.deleteData). */\n\tdataDeleted: boolean;\n}\n\n/**\n * Uninstall a registry-source plugin. Deletes the R2 bundle under\n * `registry/<pluginId>/<version>/`, optionally drops the plugin's\n * `_plugin_storage` rows, and removes the `_plugin_state` row. The\n * sandbox runtime is reconciled by the route's `syncRegistryPlugins`\n * call after this returns.\n *\n * Refuses to uninstall plugins whose `source` is not `\"registry\"` to\n * avoid trashing a marketplace/config plugin that happens to share the\n * pluginId namespace.\n */\nexport async function handleRegistryUninstall(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n\tpluginId: string,\n\topts?: { deleteData?: boolean },\n): Promise<ApiResult<RegistryUninstallResult>> {\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst existing = await stateRepo.get(pluginId);\n\t\tif (!existing || existing.source !== \"registry\") {\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 registry plugin found: ${pluginId}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// `_plugin_state.version` carries the installed version directly for\n\t\t// registry-source rows (there's no shadow column like marketplace's\n\t\t// `marketplaceVersion`). Use it verbatim for the R2 prefix.\n\t\tconst version = existing.version;\n\n\t\t// Order: optional storage cleanup → bundle delete → state row delete.\n\t\t// The most failure-prone step runs first so a transient DB error\n\t\t// (deadlock, contention) cascades to the outer catch with the state\n\t\t// row and bundle intact — admin retries safely. Bundle delete is\n\t\t// idempotent on misses.\n\t\tlet dataDeleted = false;\n\t\tif (opts?.deleteData) {\n\t\t\tawait db.deleteFrom(\"_plugin_storage\").where(\"plugin_id\", \"=\", pluginId).execute();\n\t\t\tdataDeleted = true;\n\t\t}\n\n\t\tif (storage) {\n\t\t\tawait deleteBundleFromR2(storage, pluginId, version, \"registry\");\n\t\t}\n\n\t\ttry {\n\t\t\tawait removeAllPluginIndexes(db, pluginId);\n\t\t} catch {\n\t\t\t// Nothing to drop, or tracking table predates the feature\n\t\t}\n\n\t\tawait stateRepo.delete(pluginId);\n\n\t\treturn { success: true, data: { pluginId, dataDeleted } };\n\t} catch (err) {\n\t\tconsole.error(\"[registry-uninstall] Failed:\", 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 ─────────────────────────────────────────────────────────\n\nexport interface RegistryUpdateResult {\n\tpluginId: string;\n\toldVersion: string;\n\tnewVersion: string;\n\tcapabilityChanges: { added: string[]; removed: string[] };\n\t/** Set only when `newlyPublic` is non-empty, mirroring marketplace. */\n\trouteVisibilityChanges?: { newlyPublic: string[] };\n}\n\n/**\n * Update a registry-source plugin to a newer release. Mirrors\n * `handleMarketplaceUpdate`: resolves the target version via the aggregator,\n * re-runs the artifact fetch / checksum / extract pipeline, diffs capabilities\n * and route visibility against the currently installed bundle, and gates\n * escalations behind `confirmCapabilityChanges` / `confirmRouteVisibilityChanges`\n * so the admin re-consents to widened permissions.\n *\n * Refuses non-registry sources. Refuses when the stored state row is missing\n * the `(publisherDid, slug)` it needs to resolve against the aggregator.\n */\nexport async function handleRegistryUpdate(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n\tsandboxRunner: SandboxRunner | null,\n\tregistryConfigInput: RegistryConfigInput | undefined,\n\tpluginId: string,\n\topts?: {\n\t\tversion?: string;\n\t\tconfirmCapabilityChanges?: boolean;\n\t\tconfirmRouteVisibilityChanges?: boolean;\n\t\tconfirmMcpTools?: boolean;\n\t\thostEnv?: HostEnv;\n\t},\n): Promise<ApiResult<RegistryUpdateResult>> {\n\tconst registryConfig = coerceRegistryConfig(registryConfigInput);\n\tif (!registryConfig) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"REGISTRY_NOT_CONFIGURED\", message: \"Registry is not configured\" },\n\t\t};\n\t}\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 registry plugin updates\",\n\t\t\t},\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\ttry {\n\t\tvalidateAggregatorUrl(registryConfig.aggregatorUrl);\n\t} catch (err) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"REGISTRY_NOT_CONFIGURED\",\n\t\t\t\tmessage: err instanceof Error ? err.message : \"Invalid aggregator URL\",\n\t\t\t},\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 !== \"registry\") {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `No registry plugin found: ${pluginId}` },\n\t\t\t};\n\t\t}\n\t\tif (!existing.registryPublisherDid || !existing.registrySlug) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"INVALID_STATE\",\n\t\t\t\t\tmessage: `Registry plugin ${pluginId} is missing publisher DID or slug in state`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconst oldVersion = existing.version;\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- existing.registryPublisherDid is a DID string written by the install handler\n\t\tconst publisherDid = existing.registryPublisherDid as Did;\n\t\tconst slug = existing.registrySlug;\n\n\t\tconst { DiscoveryClient, registryLabelerPolicy } =\n\t\t\tawait import(\"@premium-cms/registry-client/discovery\");\n\t\tconst aggregatorDeadline = Date.now() + AGGREGATOR_TOTAL_BUDGET_MS;\n\t\tconst discovery = new DiscoveryClient({\n\t\t\taggregatorUrl: registryConfig.aggregatorUrl,\n\t\t\tacceptLabelers: registryConfig.acceptLabelers,\n\t\t\tlabelerPolicy: registryLabelerPolicy(registryConfig.acceptLabelers),\n\t\t\tfetch: timedFetch(aggregatorDeadline),\n\t\t});\n\n\t\t// Resolve target release. Explicit version → paginate listReleases;\n\t\t// otherwise getLatestRelease (aggregator applies its own filters).\n\t\tconst MAX_LIST_PAGES = 20;\n\t\tconst releaseView = await (async () => {\n\t\t\tif (!opts?.version) {\n\t\t\t\treturn discovery.getLatestRelease({ did: publisherDid, package: slug });\n\t\t\t}\n\t\t\tlet cursor: string | undefined;\n\t\t\tconst seenCursors = new Set<string>();\n\t\t\tfor (let page = 0; page < MAX_LIST_PAGES; page++) {\n\t\t\t\tif (cursor !== undefined) {\n\t\t\t\t\tif (seenCursors.has(cursor)) break;\n\t\t\t\t\tseenCursors.add(cursor);\n\t\t\t\t}\n\t\t\t\tconst result = await discovery.listReleases({\n\t\t\t\t\tdid: publisherDid,\n\t\t\t\t\tpackage: slug,\n\t\t\t\t\tcursor,\n\t\t\t\t\tlimit: 50,\n\t\t\t\t});\n\t\t\t\tfor (const r of result.releases) {\n\t\t\t\t\tif (r.version === opts.version) return r;\n\t\t\t\t}\n\t\t\t\tif (!result.cursor) break;\n\t\t\t\tcursor = result.cursor;\n\t\t\t}\n\t\t\treturn undefined;\n\t\t})();\n\n\t\tif (!releaseView) {\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: opts?.version\n\t\t\t\t\t\t? `Version ${opts.version} not found for ${publisherDid}/${slug}`\n\t\t\t\t\t\t: `No installable release found for ${publisherDid}/${slug}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Identity cross-check. A buggy/compromised aggregator must not\n\t\t// trick us into installing a record signed for a different\n\t\t// (did, slug, version) under this plugin's pluginId.\n\t\tconst signedRelease = releaseView.release;\n\t\tif (\n\t\t\treleaseView.did !== publisherDid ||\n\t\t\treleaseView.package !== slug ||\n\t\t\tsignedRelease?.package !== slug ||\n\t\t\t(opts?.version !== undefined && releaseView.version !== opts.version) ||\n\t\t\tsignedRelease?.version !== releaseView.version\n\t\t) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"AGGREGATOR_IDENTITY_MISMATCH\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t\"Aggregator returned a release view that does not match the requested package or version.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst newVersion = releaseView.version;\n\t\tif (evaluateRegistryReleaseWithdrawal(releaseView, discovery.labelerPolicy).withdrawn) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"YANKED\", message: \"Release has been withdrawn\" },\n\t\t\t};\n\t\t}\n\t\tif (newVersion === oldVersion) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"ALREADY_UP_TO_DATE\",\n\t\t\t\t\tmessage: \"Plugin is already at the requested version\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Environment compatibility remains independent from listing approval.\n\t\t// An ungated update could otherwise\n\t\t// land a version whose `requires` the host doesn't satisfy. Same\n\t\t// guard as install; `requires` is lexicon-`unknown`.\n\t\tif (opts?.hostEnv) {\n\t\t\tconst envError = assertEnvCompatible(signedRelease.requires, opts.hostEnv);\n\t\t\tif (envError) return { success: false, error: envError };\n\t\t}\n\n\t\tconst declaredUrl = signedRelease.artifacts?.package?.url;\n\t\tconst declaredChecksum = signedRelease.artifacts?.package?.checksum;\n\t\tif (!declaredUrl || !declaredChecksum) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"INVALID_RELEASE\",\n\t\t\t\t\tmessage: \"Release record is missing artifact url or checksum\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// SSRF check on declared URL + each mirror.\n\t\tawait assertSafeArtifactUrl(declaredUrl);\n\t\tconst rawMirrors = releaseView.mirrors ?? [];\n\t\tconst mirrors = rawMirrors.slice(0, MAX_MIRRORS);\n\t\tfor (const mirror of mirrors) {\n\t\t\tawait assertSafeArtifactUrl(mirror);\n\t\t}\n\n\t\t// `fetchArtifact` derives its own per-call deadline internally.\n\t\tconst artifactBytes = await fetchArtifact(mirrors, declaredUrl);\n\t\tif (!(await verifyChecksum(artifactBytes, declaredChecksum))) {\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: \"Artifact bytes do not match the release's published checksum\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst bundle: PluginBundle = await extractBundle(artifactBytes);\n\n\t\tif (bundle.manifest.version !== newVersion) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"BUNDLE_VERSION_MISMATCH\",\n\t\t\t\t\tmessage: `Bundle manifest version (${bundle.manifest.version}) does not match release version (${newVersion})`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (bundle.manifest.id !== slug) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"BUNDLE_IDENTITY_MISMATCH\",\n\t\t\t\t\tmessage: `Bundle manifest id (${bundle.manifest.id}) does not match registry slug (${slug})`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Rewrite manifest.id to the opaque pluginId so the sandbox loader\n\t\t// and R2 layout stay in sync across install and update.\n\t\tbundle.manifest = { ...bundle.manifest, id: pluginId };\n\n\t\t// Integrity: same gate as install. The new bundle must declare exactly\n\t\t// the access its signed release record advertises. Without it, an update\n\t\t// that changes only the host scope (e.g. api.good.com -> evil.com) keeps\n\t\t// the capability set identical, sails through the escalation diff below,\n\t\t// and installs a bundle enforcing a scope the record never showed.\n\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- extensions is the lexicon's open `unknown` map; narrow to read our own extension\n\t\tconst updateRecordExtensions = signedRelease?.extensions as\n\t\t\t| Record<string, { declaredAccess?: DeclaredAccess }>\n\t\t\t| undefined;\n\t\tconst recordExt = updateRecordExtensions?.[RELEASE_EXTENSION_NSID];\n\t\tif (\n\t\t\t!enforcedAccessEqual(recordExt?.declaredAccess ?? {}, bundle.manifest.declaredAccess ?? {})\n\t\t) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"DECLARED_ACCESS_DRIFT\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t\"The plugin bundle declares different permissions than its published record. Update refused.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Diff capabilities + route visibility against the currently\n\t\t// installed bundle. Loading from R2 keeps us honest: the diff is\n\t\t// against the bytes the sandbox is actually running, not whatever\n\t\t// the state row claims.\n\t\tconst oldBundle = await loadBundleFromR2(storage, pluginId, oldVersion, \"registry\");\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\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\tconst routeVisibilityChanges = diffRouteVisibility(oldBundle?.manifest, bundle.manifest);\n\t\tconst hasNewPublicRoutes = routeVisibilityChanges.newlyPublic.length > 0;\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\tconst oldMcpTools = [...(oldBundle?.manifest.mcp?.tools ?? [])].toSorted((a, b) =>\n\t\t\ta.name.localeCompare(b.name),\n\t\t);\n\t\tconst newMcpTools = [...(bundle.manifest.mcp?.tools ?? [])].toSorted((a, b) =>\n\t\t\ta.name.localeCompare(b.name),\n\t\t);\n\t\tif (JSON.stringify(oldMcpTools) !== JSON.stringify(newMcpTools) && !opts?.confirmMcpTools) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"MCP_TOOL_CONSENT_REQUIRED\",\n\t\t\t\t\tmessage: \"Plugin update changes its MCP tools\",\n\t\t\t\t\tdetails: {\n\t\t\t\t\t\tmcpTools: newMcpTools.map(({ inputSchema: _, outputSchema: __, ...tool }) => tool),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Store new bundle. R2 prefix is deterministic per (pluginId, version),\n\t\t// so a retry of the same update is idempotent.\n\t\tawait storeBundleInR2(storage, pluginId, newVersion, bundle, \"registry\");\n\n\t\t// Update state. Preserve publisher/slug; refresh displayName /\n\t\t// description from the install handler's seeded values (we don't\n\t\t// re-fetch the profile here — that's a separate `getPackage` round\n\t\t// trip and the install-time values are still authoritative for\n\t\t// the same package identity).\n\t\tawait stateRepo.upsert(pluginId, newVersion, \"active\", {\n\t\t\tsource: \"registry\",\n\t\t\tregistryPublisherDid: publisherDid,\n\t\t\tregistrySlug: slug,\n\t\t\tdisplayName: existing.displayName ?? slug,\n\t\t\tdescription: existing.description ?? undefined,\n\t\t\tmcpToolsEnabled: false,\n\t\t\tmcpToolsConsent: null,\n\t\t});\n\n\t\tawait syncDeclaredStorageIndexes(db, [bundle.manifest]);\n\n\t\t// Best-effort cleanup of the old bundle. Failures here don't roll\n\t\t// back the upgrade (the new bundle is already stored and committed\n\t\t// in the state row); the orphan is just storage we'll pay for.\n\t\tdeleteBundleFromR2(storage, pluginId, oldVersion, \"registry\").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 ClientValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"AGGREGATOR_RESPONSE_INVALID\",\n\t\t\t\t\tmessage: `Aggregator returned a response that does not conform to its lexicon (${err.target})`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (err instanceof ClientResponseError) {\n\t\t\tif (err.error === \"ListingUnavailable\") {\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: \"LISTING_UNAVAILABLE\",\n\t\t\t\t\t\tmessage: \"This plugin is unavailable under the active registry policy\",\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: err.status === 404 ? \"AGGREGATOR_NOT_FOUND\" : \"AGGREGATOR_HTTP_ERROR\",\n\t\t\t\t\tmessage: `Aggregator returned ${err.status}: ${err.error}`,\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 updating plugin\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"[registry-update] Failed:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"UPDATE_FAILED\",\n\t\t\t\tmessage: err instanceof Error ? err.message : \"Failed to update plugin\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ── Update check ───────────────────────────────────────────────────\n\nexport interface RegistryUpdateCheck {\n\tpluginId: string;\n\tinstalled: string;\n\tlatest: string;\n\thasUpdate: boolean;\n\t/**\n\t * Both diff fields are `false` here by design: computing them at\n\t * update-check time would require downloading both bundles (or\n\t * extracting from the signed release extension and the installed\n\t * R2 bundle), which is too expensive for a bulk preview. The actual\n\t * escalation gate runs at update time in `handleRegistryUpdate`.\n\t * Mirrors marketplace's `hasRouteVisibilityChanges: false`.\n\t */\n\thasCapabilityChanges: boolean;\n\thasRouteVisibilityChanges: boolean;\n}\n\n/**\n * Bulk update check across every installed registry plugin. Queries the\n * aggregator for each plugin's latest release and reports `hasUpdate`\n * based on the version comparison. Plugins whose aggregator lookup fails\n * (unreachable, delisted, malformed) are skipped silently — one bad\n * publisher must not blank the whole admin Updates list.\n */\nexport async function handleRegistryUpdateCheck(\n\tdb: Kysely<Database>,\n\tregistryConfigInput: RegistryConfigInput | undefined,\n): Promise<ApiResult<{ items: RegistryUpdateCheck[] }>> {\n\tconst registryConfig = coerceRegistryConfig(registryConfigInput);\n\tif (!registryConfig) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"REGISTRY_NOT_CONFIGURED\", message: \"Registry is not configured\" },\n\t\t};\n\t}\n\n\ttry {\n\t\tconst stateRepo = new PluginStateRepository(db);\n\t\tconst registryPlugins = await stateRepo.getRegistryPlugins();\n\t\tif (registryPlugins.length === 0) {\n\t\t\treturn { success: true, data: { items: [] } };\n\t\t}\n\n\t\tconst { DiscoveryClient, registryLabelerPolicy } =\n\t\t\tawait import(\"@premium-cms/registry-client/discovery\");\n\t\tconst aggregatorDeadline = Date.now() + AGGREGATOR_TOTAL_BUDGET_MS;\n\t\tconst discovery = new DiscoveryClient({\n\t\t\taggregatorUrl: registryConfig.aggregatorUrl,\n\t\t\tacceptLabelers: registryConfig.acceptLabelers,\n\t\t\tlabelerPolicy: registryLabelerPolicy(registryConfig.acceptLabelers),\n\t\t\tfetch: timedFetch(aggregatorDeadline),\n\t\t});\n\n\t\tconst items: RegistryUpdateCheck[] = [];\n\t\tfor (const plugin of registryPlugins) {\n\t\t\tif (!plugin.registryPublisherDid || !plugin.registrySlug) continue;\n\t\t\ttry {\n\t\t\t\tconst releaseView = await discovery.getLatestRelease({\n\t\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- DID string was validated by the install handler\n\t\t\t\t\tdid: plugin.registryPublisherDid as Did,\n\t\t\t\t\tpackage: plugin.registrySlug,\n\t\t\t\t});\n\t\t\t\tif (evaluateRegistryReleaseWithdrawal(releaseView, discovery.labelerPolicy).withdrawn) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tconst latest = releaseView.version;\n\t\t\t\tif (!latest) continue;\n\t\t\t\tconst installed = plugin.version;\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,\n\t\t\t\t\thasUpdate: latest !== installed,\n\t\t\t\t\thasCapabilityChanges: false,\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. Don't fail the whole\n\t\t\t\t// list because one aggregator query went wrong.\n\t\t\t\tconsole.warn(`[registry-update-check] Skipped ${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 ClientValidationError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"AGGREGATOR_RESPONSE_INVALID\",\n\t\t\t\t\tmessage: `Aggregator returned a response that does not conform to its lexicon (${err.target})`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tif (err instanceof ClientResponseError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: err.status === 404 ? \"AGGREGATOR_NOT_FOUND\" : \"AGGREGATOR_HTTP_ERROR\",\n\t\t\t\t\tmessage: `Aggregator returned ${err.status}: ${err.error}`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"[registry-update-check] Failed:\", err);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"UPDATE_CHECK_FAILED\", message: \"Failed to check for registry updates\" },\n\t\t};\n\t}\n}\n"],"mappings":"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"}