import { i as portableTextToMarkdown, n as convertDataForWrite, r as markdownToPortableText, t as convertDataForRead } from "../portable-text-cCa0cGV6.mjs";
import { a as tokenInterceptor, i as refreshInterceptor, n as csrfInterceptor, r as devBypassInterceptor, t as createTransport } from "../transport-DPefPMcz.mjs";
import mime from "mime/lite";

//#region src/client/index.ts
/**
* EmDashClient — typed HTTP client for the EmDash REST API.
*
* Handles auth, CSRF, PT ↔ Markdown conversion, and optional `_rev`
* concurrency tokens. Shared foundation for the CLI and future MCP server.
*
* @example
* ```ts
* import { EmDashClient } from "emdash/client";
*
* const client = new EmDashClient({
*   baseUrl: "http://localhost:4321",
*   devBypass: true,
* });
*
* const posts = await client.list("posts", { status: "published" });
* ```
*/
const TRAILING_SLASH_PATTERN = /\/$/;
function mimeFromFilename(filename) {
	return mime.getType(filename) ?? "application/octet-stream";
}
var EmDashApiError = class extends Error {
	constructor(status, code, message, details) {
		super(message);
		this.status = status;
		this.code = code;
		this.details = details;
		this.name = "EmDashApiError";
	}
};
var EmDashClientError = class extends Error {
	constructor(message) {
		super(message);
		this.name = "EmDashClientError";
	}
};
var EmDashClient = class {
	baseUrl;
	transport;
	/** Cached field schemas per collection for PT conversion */
	fieldSchemaCache = /* @__PURE__ */ new Map();
	constructor(options) {
		this.baseUrl = options.baseUrl.replace(TRAILING_SLASH_PATTERN, "");
		const interceptors = [csrfInterceptor()];
		if (options.token) interceptors.push(tokenInterceptor(options.token));
		else if (options.devBypass) interceptors.push(devBypassInterceptor(this.baseUrl));
		if (options.refreshToken) interceptors.push(refreshInterceptor({
			refreshToken: options.refreshToken,
			tokenEndpoint: `${this.baseUrl}/_emdash/api/oauth/token/refresh`,
			onTokenRefreshed: options.onTokenRefresh ? (accessToken, _refreshToken, expiresAt) => {
				const expiresIn = Math.floor((new Date(expiresAt).getTime() - Date.now()) / 1e3);
				options.onTokenRefresh(accessToken, expiresIn);
			} : void 0
		}));
		if (options.interceptors) interceptors.push(...options.interceptors);
		this.transport = createTransport({ interceptors });
	}
	/** List all collections */
	async collections() {
		return (await this.request("GET", "/schema/collections")).items;
	}
	/** Get a single collection with its fields */
	async collection(slug) {
		const col = (await this.request("GET", `/schema/collections/${encodeURIComponent(slug)}?includeFields=true`)).item;
		if (col.fields) this.fieldSchemaCache.set(slug, col.fields.map((f) => ({
			slug: f.slug,
			type: f.type
		})));
		return col;
	}
	/** Create a collection */
	async createCollection(input) {
		return (await this.request("POST", "/schema/collections", input)).item;
	}
	/** Delete a collection */
	async deleteCollection(slug) {
		await this.request("DELETE", `/schema/collections/${encodeURIComponent(slug)}`);
	}
	/** Create a field on a collection */
	async createField(collection, input) {
		const data = await this.request("POST", `/schema/collections/${encodeURIComponent(collection)}/fields`, input);
		this.fieldSchemaCache.delete(collection);
		return data.item;
	}
	/** Delete a field from a collection */
	async deleteField(collection, fieldSlug) {
		await this.request("DELETE", `/schema/collections/${encodeURIComponent(collection)}/fields/${encodeURIComponent(fieldSlug)}`);
		this.fieldSchemaCache.delete(collection);
	}
	/** Get full manifest (schema + field descriptors + features) */
	async manifest() {
		return this.request("GET", "/manifest");
	}
	/** Export full schema as JSON (used by `emdash types`) */
	async schemaExport() {
		return this.request("GET", "/schema");
	}
	/** Export schema as TypeScript type definitions (used by `emdash types`) */
	async schemaTypes() {
		const response = await this.requestRaw("GET", "/schema?format=typescript");
		await this.assertOk(response);
		return response.text();
	}
	/** List content in a collection */
	async list(collection, options) {
		const params = new URLSearchParams();
		if (options?.status) params.set("status", options.status);
		if (options?.limit) params.set("limit", String(options.limit));
		if (options?.cursor) params.set("cursor", options.cursor);
		if (options?.orderBy) params.set("orderBy", options.orderBy);
		if (options?.order) params.set("order", options.order);
		if (options?.locale) params.set("locale", options.locale);
		const qs = params.toString();
		const path = `/content/${encodeURIComponent(collection)}${qs ? `?${qs}` : ""}`;
		return this.request("GET", path);
	}
	/** Async iterator that auto-follows cursors */
	async *listAll(collection, options) {
		let cursor;
		do {
			const result = await this.list(collection, {
				...options,
				cursor
			});
			for (const item of result.items) yield item;
			cursor = result.nextCursor;
		} while (cursor);
	}
	/**
	* Get a single content item. Returns the item with a `_rev` token
	* that can be passed to update() for optimistic concurrency.
	*/
	async get(collection, id, options) {
		const params = new URLSearchParams();
		if (options?.locale) params.set("locale", options.locale);
		const qs = params.size > 0 ? `?${params}` : "";
		const result = await this.requestRaw("GET", `/content/${encodeURIComponent(collection)}/${encodeURIComponent(id)}${qs}`);
		if (!result.ok) await this.assertOk(result);
		const json = (await result.json()).data;
		const item = json.item;
		if (json._rev) item._rev = json._rev;
		if (!options?.raw && item.data) {
			const fields = await this.getFieldSchemas(collection);
			item.data = convertDataForRead(item.data, fields, false);
		}
		return item;
	}
	/** Create a new content item */
	async create(collection, input) {
		const fields = await this.getFieldSchemas(collection);
		const data = convertDataForWrite(input.data, fields);
		return (await this.request("POST", `/content/${encodeURIComponent(collection)}`, {
			...input,
			data
		})).item;
	}
	/**
	* Update a content item. Pass `_rev` from a prior get() for optimistic
	* concurrency — the server returns 409 if the item has changed.
	* Omit `_rev` for a blind write (no conflict detection).
	*/
	async update(collection, id, input) {
		let data = input.data;
		if (data) {
			const fields = await this.getFieldSchemas(collection);
			data = convertDataForWrite(data, fields);
		}
		const body = {
			data,
			slug: input.slug,
			status: input.status,
			...input._rev ? { _rev: input._rev } : {}
		};
		const params = new URLSearchParams();
		if (input.locale) params.set("locale", input.locale);
		const result = await this.request("PUT", `/content/${encodeURIComponent(collection)}/${encodeURIComponent(id)}${params.toString() ? `?${params}` : ""}`, body);
		const item = result.item;
		if (result._rev) item._rev = result._rev;
		return item;
	}
	/** Delete (soft) a content item */
	async delete(collection, id) {
		await this.request("DELETE", `/content/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`);
	}
	/** Publish a content item */
	async publish(collection, id) {
		await this.request("POST", `/content/${encodeURIComponent(collection)}/${encodeURIComponent(id)}/publish`);
	}
	/** Unpublish a content item */
	async unpublish(collection, id) {
		await this.request("POST", `/content/${encodeURIComponent(collection)}/${encodeURIComponent(id)}/unpublish`);
	}
	/** Schedule publishing */
	async schedule(collection, id, options) {
		await this.request("POST", `/content/${encodeURIComponent(collection)}/${encodeURIComponent(id)}/schedule`, { scheduledAt: options.at });
	}
	/** Restore a trashed content item */
	async restore(collection, id) {
		await this.request("POST", `/content/${encodeURIComponent(collection)}/${encodeURIComponent(id)}/restore`);
	}
	/** Compare live and draft revisions */
	async compare(collection, id) {
		return this.request("GET", `/content/${encodeURIComponent(collection)}/${encodeURIComponent(id)}/compare`);
	}
	/** Discard draft revision, reverting to the published version */
	async discardDraft(collection, id) {
		await this.request("POST", `/content/${encodeURIComponent(collection)}/${encodeURIComponent(id)}/discard-draft`);
	}
	/**
	* Get all translations of a content item.
	* Returns the translation group ID and a summary of each locale version.
	*/
	async translations(collection, id) {
		return this.request("GET", `/content/${encodeURIComponent(collection)}/${encodeURIComponent(id)}/translations`);
	}
	/** List media items */
	async mediaList(options) {
		const params = new URLSearchParams();
		if (options?.mimeType) params.set("mimeType", options.mimeType);
		if (options?.limit) params.set("limit", String(options.limit));
		if (options?.cursor) params.set("cursor", options.cursor);
		const qs = params.toString();
		return this.request("GET", `/media${qs ? `?${qs}` : ""}`);
	}
	/** Get a single media item */
	async mediaGet(id) {
		return (await this.request("GET", `/media/${encodeURIComponent(id)}`)).item;
	}
	/** Upload a media file */
	async mediaUpload(file, filename, options) {
		const formData = new FormData();
		if (file instanceof Blob) formData.append("file", file, filename);
		else {
			const mimeType = options?.contentType ?? mimeFromFilename(filename);
			formData.append("file", new Blob([file], { type: mimeType }), filename);
		}
		if (options?.alt) formData.append("alt", options.alt);
		if (options?.caption) formData.append("caption", options.caption);
		const url = `${this.baseUrl}/_emdash/api/media`;
		const request = new Request(url, {
			method: "POST",
			body: formData
		});
		const response = await this.transport.fetch(request);
		await this.assertOk(response);
		return (await response.json()).data.item;
	}
	/** Delete a media item */
	async mediaDelete(id) {
		await this.request("DELETE", `/media/${encodeURIComponent(id)}`);
	}
	/** Full-text search */
	async search(query, options) {
		const params = new URLSearchParams({ q: query });
		if (options?.collection) params.set("collections", options.collection);
		if (options?.locale) params.set("locale", options.locale);
		if (options?.limit) params.set("limit", String(options.limit));
		return (await this.request("GET", `/search?${params}`)).items;
	}
	/** List taxonomies */
	async taxonomies() {
		return (await this.request("GET", "/taxonomies")).taxonomies;
	}
	/** List terms in a taxonomy */
	async terms(taxonomy, options) {
		const params = new URLSearchParams();
		if (options?.limit) params.set("limit", String(options.limit));
		if (options?.cursor) params.set("cursor", options.cursor);
		const qs = params.toString();
		return { items: (await this.request("GET", `/taxonomies/${encodeURIComponent(taxonomy)}/terms${qs ? `?${qs}` : ""}`)).terms };
	}
	/** Create a taxonomy term */
	async createTerm(taxonomy, input) {
		return this.request("POST", `/taxonomies/${encodeURIComponent(taxonomy)}/terms`, input);
	}
	/** List menus */
	async menus() {
		return this.request("GET", "/menus");
	}
	/** Get a menu with its items */
	async menu(name) {
		return this.request("GET", `/menus/${encodeURIComponent(name)}`);
	}
	/** Make a typed JSON request to the API */
	async request(method, path, body) {
		const response = await this.requestRaw(method, path, body);
		await this.assertOk(response);
		return (await response.json()).data;
	}
	/** Make a raw request — caller handles response */
	async requestRaw(method, path, body) {
		const url = `${this.baseUrl}/_emdash/api${path}`;
		const headers = { Accept: "application/json" };
		let requestBody;
		if (body !== void 0) {
			headers["Content-Type"] = "application/json";
			requestBody = JSON.stringify(body);
		}
		const request = new Request(url, {
			method,
			headers,
			body: requestBody
		});
		return this.transport.fetch(request);
	}
	/** Assert a response is OK, throw typed error if not */
	async assertOk(response) {
		if (response.ok) return;
		let code = "UNKNOWN_ERROR";
		let message = `HTTP ${response.status}`;
		let details;
		try {
			const json = await response.json();
			if (json.error) {
				code = json.error.code ?? code;
				message = json.error.message ?? message;
				details = json.error.details;
			}
		} catch {
			message = response.statusText || message;
		}
		throw new EmDashApiError(response.status, code, message, details);
	}
	/** Get cached field schemas for a collection, fetching if needed */
	async getFieldSchemas(collection) {
		let cached = this.fieldSchemaCache.get(collection);
		if (cached) return cached;
		try {
			cached = (await this.collection(collection)).fields.map((f) => ({
				slug: f.slug,
				type: f.type
			}));
			this.fieldSchemaCache.set(collection, cached);
			return cached;
		} catch {
			return [];
		}
	}
};

//#endregion
export { EmDashApiError, EmDashClient, EmDashClientError, createTransport, csrfInterceptor, devBypassInterceptor, markdownToPortableText, portableTextToMarkdown, tokenInterceptor };
//# sourceMappingURL=index.mjs.map