Files
wursor/packages/platform/src/plugin-api.ts
T
2026-04-26 09:09:55 +01:00

122 lines
5.4 KiB
TypeScript

import { z } from 'zod';
import type { Artboard, InsertOrigin, IntentDiff } from '@originmain/origin-graph';
// ── Plugin Manifest ───────────────────────────────────────────────────────────
// Plugins declare their identity, permissions, and extension points in a
// manifest. The host validates this on install and on every load.
export const PluginPermissionSchema = z.enum([
'artboards:read', // read artboard metadata and component trees
'artboards:write', // create artboards and origins (requires approval)
'diffs:read', // read IntentDiff objects
'diffs:export', // export diffs to external tools
'completion-zones:register', // register custom Completion Zone types
'ingesters:register', // register custom ingestion connectors
'design-language:read', // read the workspace Design Language File
]);
export type PluginPermission = z.infer<typeof PluginPermissionSchema>;
export const PluginManifestSchema = z.object({
/** Unique reverse-DNS identifier: e.g. "com.acme.my-plugin" */
id: z.string().regex(/^[a-z0-9]+(\.[a-z0-9-]+)+$/, 'Must be reverse-DNS format'),
name: z.string().min(1).max(80),
version: z.string().regex(/^\d+\.\d+\.\d+$/, 'Must be semver'),
description: z.string().max(300),
/** URL where the plugin's sandboxed JS bundle is hosted */
entryUrl: z.string().url(),
permissions: z.array(PluginPermissionSchema),
/** SHA-256 hash of the entryUrl bundle — required; verified before execution */
bundleHash: z.string().regex(/^[0-9a-f]{64}$/),
});
export type PluginManifest = z.infer<typeof PluginManifestSchema>;
// ── Plugin Context ────────────────────────────────────────────────────────────
// The API surface exposed to a sandboxed plugin via postMessage.
// Split into read and write sides so permission checks are explicit.
export interface PluginReadAPI {
getArtboard(id: string): Promise<Artboard | null>;
listArtboards(): Promise<readonly Artboard[]>;
getDiff(id: string): Promise<IntentDiff | null>;
listPendingDiffs(): Promise<readonly IntentDiff[]>;
getDesignLanguageFile(): Promise<unknown | null>;
}
export interface PluginWriteAPI {
/**
* Creates a new artboard linked to the given origin.
* The host injects workspaceId from PluginContext.workspaceId — plugins
* do not set workspace scoping; it is enforced server-side.
*/
createArtboard(params: {
name: string;
origin: InsertOrigin;
}): Promise<{ artboardId: string }>;
exportDiff(diffId: string, format: 'json' | 'markdown'): Promise<string>;
}
export interface PluginContext {
/** Plugin's declared manifest (read-only inside sandbox) */
readonly manifest: PluginManifest;
/** Workspace ID the plugin is operating within */
readonly workspaceId: string;
/** Read APIs — available if 'artboards:read' or 'diffs:read' is granted */
readonly read: PluginReadAPI;
/** Write APIs — available only if 'artboards:write' is granted */
readonly write: PluginWriteAPI | null;
/** Emits a UI notification to the host application */
notify(message: string, level?: 'info' | 'warning' | 'error'): void;
}
// ── Custom Completion Zone ────────────────────────────────────────────────────
// Plugins can register custom Completion Zone types with their own AI prompts
// and rendering logic.
export interface CustomCompletionZoneDefinition {
/** Unique type identifier: e.g. "com.acme.marketing-copy" */
type: string;
label: string;
description: string;
/** System prompt appended to the standard Completion Zone system prompt */
systemPromptAddendum: string;
/** JSON Schema describing the expected output format */
outputSchema: Record<string, unknown>;
}
// ── Custom Ingester ───────────────────────────────────────────────────────────
// Plugins can register additional ingestion connectors beyond the 4 first-party set.
export interface CustomIngesterDefinition {
/** Matches the 'type' field in the plugin manifest */
sourceType: string;
label: string;
/** Webhook URL path registered with the host's Edge Function router */
webhookPath: string;
/** The plugin's JS bundle handles validatePayload + ingest via sandboxed eval */
handleWebhook(rawBody: string, headers: Record<string, string>): Promise<{
artboardTitle: string;
renderUrl?: string;
sourceRef: string;
metadata: Record<string, unknown>;
}>;
}
// ── Plugin Registry ───────────────────────────────────────────────────────────
export interface InstalledPlugin {
manifest: PluginManifest;
installedAt: string;
installedByUserId: string;
enabled: boolean;
}
export interface PluginRegistry {
list(workspaceId: string): Promise<readonly InstalledPlugin[]>;
install(workspaceId: string, manifest: PluginManifest, byUserId: string): Promise<void>;
uninstall(workspaceId: string, pluginId: string): Promise<void>;
setEnabled(workspaceId: string, pluginId: string, enabled: boolean): Promise<void>;
}