import { z } from "zod";
import { capabilitiesToDeclaredAccess, declaredAccessToCapabilities } from "@emdash-cms/plugin-types";

//#region src/plugins/manifest-schema.ts
/**
* Zod schema for PluginManifest validation
*
* Used to validate manifest.json from plugin bundles at every parse site:
* - Client-side download (marketplace.ts extractBundle)
* - R2 load (api/handlers/marketplace.ts loadBundleFromR2)
* - CLI publish preview (cli/commands/publish.ts readManifestFromTarball)
* - Marketplace ingest extends this with publishing-specific fields
*/
/**
* Current capability names — the ones authors should use going forward.
* See `PluginCapability` in `types.ts` for documentation of each.
*/
const CURRENT_PLUGIN_CAPABILITIES = [
	"network:request",
	"network:request:unrestricted",
	"content:read",
	"content:write",
	"media:read",
	"media:write",
	"users:read",
	"email:send",
	"hooks.email-transport:register",
	"hooks.email-events:register",
	"hooks.page-fragments:register"
];
/**
* Legacy capability names accepted during the deprecation window.
* Normalized to current names via `normalizeCapability()` in types.ts
* before reaching the runtime. Plugin authors are warned at bundle/validate
* and hard-failed at publish.
*/
const DEPRECATED_PLUGIN_CAPABILITIES = [
	"network:fetch",
	"network:fetch:any",
	"read:content",
	"write:content",
	"read:media",
	"write:media",
	"read:users",
	"email:provide",
	"email:intercept",
	"page:inject"
];
/**
* Full set of accepted capability strings — current + deprecated.
*
* The manifest schema accepts both during the transition. The runtime only
* ever sees current names because `normalizeCapability()` rewrites legacy
* names at every external boundary (definePlugin, adaptSandboxEntry).
*/
const PLUGIN_CAPABILITIES = [...CURRENT_PLUGIN_CAPABILITIES, ...DEPRECATED_PLUGIN_CAPABILITIES];
/** Must stay in sync with FieldType in schema/types.ts */
const FIELD_TYPES = [
	"string",
	"text",
	"number",
	"integer",
	"boolean",
	"datetime",
	"select",
	"multiSelect",
	"portableText",
	"image",
	"file",
	"reference",
	"json",
	"slug",
	"repeater"
];
const HOOK_NAMES = [
	"plugin:install",
	"plugin:activate",
	"plugin:deactivate",
	"plugin:uninstall",
	"content:beforeSave",
	"content:afterSave",
	"content:beforeDelete",
	"content:afterDelete",
	"content:afterPublish",
	"content:afterUnpublish",
	"content:afterRestore",
	"content:afterSchedule",
	"content:afterUnschedule",
	"media:beforeUpload",
	"media:afterUpload",
	"cron",
	"email:beforeSend",
	"email:deliver",
	"email:afterSend",
	"comment:beforeCreate",
	"comment:moderate",
	"comment:afterCreate",
	"comment:afterModerate",
	"page:metadata",
	"page:fragments"
];
/**
* Structured hook entry for manifest — name plus optional metadata.
* During a transition period, both plain strings and objects are accepted.
*/
const manifestHookEntrySchema = z.object({
	name: z.enum(HOOK_NAMES),
	exclusive: z.boolean().optional(),
	priority: z.number().int().optional(),
	timeout: z.number().int().positive().optional()
});
/**
* Structured route entry for manifest — name plus optional metadata.
* Both plain strings and objects are accepted; strings are normalized
* to `{ name }` objects via `normalizeManifestRoute()`.
*/
/** Route names must be safe path segments — alphanumeric, hyphens, underscores, forward slashes */
const routeNamePattern = /^[a-zA-Z0-9][a-zA-Z0-9_\-/]*$/;
const manifestRouteEntrySchema = z.object({
	name: z.string().min(1).regex(routeNamePattern, "Route name must be a safe path segment"),
	public: z.boolean().optional()
});
/** Index field names must be valid identifiers to prevent SQL injection via JSON path expressions */
const indexFieldName = z.string().regex(/^[a-zA-Z][a-zA-Z0-9_]*$/);
const storageCollectionSchema = z.object({
	indexes: z.array(z.union([indexFieldName, z.array(indexFieldName)])),
	uniqueIndexes: z.array(z.union([indexFieldName, z.array(indexFieldName)])).optional()
});
const baseSettingFields = {
	label: z.string(),
	description: z.string().optional()
};
const settingFieldSchema = z.discriminatedUnion("type", [
	z.object({
		...baseSettingFields,
		type: z.literal("string"),
		default: z.string().optional(),
		multiline: z.boolean().optional()
	}),
	z.object({
		...baseSettingFields,
		type: z.literal("number"),
		default: z.number().optional(),
		min: z.number().optional(),
		max: z.number().optional()
	}),
	z.object({
		...baseSettingFields,
		type: z.literal("boolean"),
		default: z.boolean().optional()
	}),
	z.object({
		...baseSettingFields,
		type: z.literal("select"),
		options: z.array(z.object({
			value: z.string(),
			label: z.string()
		})),
		default: z.string().optional()
	}),
	z.object({
		...baseSettingFields,
		type: z.literal("secret")
	}),
	z.object({
		...baseSettingFields,
		type: z.literal("url"),
		default: z.string().optional(),
		placeholder: z.string().optional()
	}),
	z.object({
		...baseSettingFields,
		type: z.literal("email"),
		default: z.string().optional(),
		placeholder: z.string().optional()
	})
]);
const adminPageSchema = z.object({
	path: z.string(),
	label: z.string(),
	icon: z.string().optional()
});
const dashboardWidgetSchema = z.object({
	id: z.string(),
	size: z.enum([
		"full",
		"half",
		"third"
	]).optional(),
	title: z.string().optional()
});
const pluginAdminConfigSchema = z.object({
	entry: z.string().optional(),
	settingsSchema: z.record(z.string(), settingFieldSchema).optional(),
	pages: z.array(adminPageSchema).optional(),
	widgets: z.array(dashboardWidgetSchema).optional(),
	fieldWidgets: z.array(z.object({
		name: z.string().min(1),
		label: z.string().min(1),
		fieldTypes: z.array(z.enum(FIELD_TYPES)),
		elements: z.array(z.object({
			type: z.string(),
			action_id: z.string(),
			label: z.string().optional()
		}).passthrough()).optional()
	})).optional()
});
/**
* An operation's constraint object. Open vocabulary: keys the runtime
* recognises are enforced, others are advisory. The bundler emits `{}` for a
* granted operation; presence (not value) signals the grant.
*/
const accessConstraints = z.record(z.string(), z.unknown());
/**
* Structured trust contract embedded in the bundle manifest. Mirrors
* `DeclaredAccess` in `@emdash-cms/plugin-types`. Categories are host
* subsystems; operations are modes of participation.
*/
const declaredAccessSchema = z.object({
	content: z.object({
		read: accessConstraints.optional(),
		write: accessConstraints.optional()
	}).optional(),
	media: z.object({
		read: accessConstraints.optional(),
		write: accessConstraints.optional()
	}).optional(),
	network: z.object({ request: z.object({ allowedHosts: z.array(z.string()).min(1).optional() }).optional() }).optional(),
	email: z.object({
		send: accessConstraints.optional(),
		events: accessConstraints.optional(),
		transport: accessConstraints.optional()
	}).optional(),
	page: z.object({ fragments: accessConstraints.optional() }).optional(),
	users: z.object({ read: accessConstraints.optional() }).optional()
});
/**
* Zod schema matching the PluginManifest interface from types.ts.
*
* Every JSON.parse of a manifest.json should validate through this.
*
* `declaredAccess` is the trust contract; `capabilities`/`allowedHosts` are the
* runtime's enforcement currency. Apply `reconcileManifestAccess` after parsing
* to make them consistent (declaredAccess authoritative when present). Kept a
* plain object (no `.transform`) because callers `.pick()`/`.extend()` it.
*/
const pluginManifestSchema = z.object({
	id: z.string().min(1),
	version: z.string().min(1),
	declaredAccess: declaredAccessSchema.optional(),
	capabilities: z.array(z.enum(PLUGIN_CAPABILITIES)),
	allowedHosts: z.array(z.string()),
	storage: z.record(z.string(), storageCollectionSchema),
	hooks: z.array(z.union([z.enum(HOOK_NAMES), manifestHookEntrySchema])),
	routes: z.array(z.union([z.string().min(1).regex(routeNamePattern, "Route name must be a safe path segment"), manifestRouteEntrySchema])),
	admin: pluginAdminConfigSchema
});
/**
* Reconcile a parsed manifest's trust contract with its enforcement currency.
* `declaredAccess` is authoritative: when present, `capabilities`/`allowedHosts`
* are re-derived from it so what the runtime enforces always matches what was
* recorded and consented to. A pre-migration bundle without `declaredAccess`
* has it derived from the legacy capability list instead. The result always
* carries both, mutually consistent. Apply this at every bundle-parse site.
*/
function reconcileManifestAccess(manifest) {
	return manifest.declaredAccess ? {
		...manifest,
		...declaredAccessToCapabilities(manifest.declaredAccess)
	} : {
		...manifest,
		declaredAccess: capabilitiesToDeclaredAccess(manifest.capabilities, manifest.allowedHosts)
	};
}
/**
* Normalize a manifest route entry — plain strings become `{ name }` objects.
*/
function normalizeManifestRoute(entry) {
	if (typeof entry === "string") return { name: entry };
	return entry;
}

//#endregion
export { reconcileManifestAccess as a, pluginManifestSchema as i, PLUGIN_CAPABILITIES as n, normalizeManifestRoute as r, HOOK_NAMES as t };
//# sourceMappingURL=manifest-schema-BHlgZM1W.mjs.map