/** * `definePlugin` — the top-level helper for authoring a Paperclip plugin. * * Plugin authors call `definePlugin()` and export the result as the default * export from their worker entrypoint. The host imports the worker module, * calls `setup()` with a `PluginContext`, and from that point the plugin * responds to events, jobs, webhooks, and UI requests through the context. * * @see PLUGIN_SPEC.md §14.1 — Example SDK Shape * * @example * ```ts * // dist/worker.ts * import { definePlugin } from "@paperclipai/plugin-sdk"; * * export default definePlugin({ * async setup(ctx) { * ctx.logger.info("Linear sync plugin starting"); * * // Subscribe to events * ctx.events.on("issue.created", async (event) => { * const config = await ctx.config.get(); * await ctx.http.fetch(`https://api.linear.app/...`, { * method: "POST", * headers: { Authorization: `Bearer ${await ctx.secrets.resolve(config.apiKeyRef as string)}` }, * body: JSON.stringify({ title: event.payload.title }), * }); * }); * * // Register a job handler * ctx.jobs.register("full-sync", async (job) => { * ctx.logger.info("Running full-sync job", { runId: job.runId }); * // ... sync logic * }); * * // Register data for the UI * ctx.data.register("sync-health", async ({ companyId }) => { * const state = await ctx.state.get({ * scopeKind: "company", * scopeId: String(companyId), * stateKey: "last-sync", * }); * return { lastSync: state }; * }); * }, * }); * ``` */ import type { PluginContext } from "./types.js"; // --------------------------------------------------------------------------- // Health check result // --------------------------------------------------------------------------- /** * Optional plugin-reported diagnostics returned from the `health()` RPC method. * * @see PLUGIN_SPEC.md §13.2 — `health` */ export interface PluginHealthDiagnostics { /** Machine-readable status: `"ok"` | `"degraded"` | `"error"`. */ status: "ok" | "degraded" | "error"; /** Human-readable description of the current health state. */ message?: string; /** Plugin-reported key-value diagnostics (e.g. connection status, queue depth). */ details?: Record; } // --------------------------------------------------------------------------- // Config validation result // --------------------------------------------------------------------------- /** * Result returned from the `validateConfig()` RPC method. * * @see PLUGIN_SPEC.md §13.3 — `validateConfig` */ export interface PluginConfigValidationResult { /** Whether the config is valid. */ ok: boolean; /** Non-fatal warnings about the config. */ warnings?: string[]; /** Validation errors (populated when `ok` is `false`). */ errors?: string[]; } // --------------------------------------------------------------------------- // Webhook handler input // --------------------------------------------------------------------------- /** * Input received by the plugin worker's `handleWebhook` handler. * * @see PLUGIN_SPEC.md §13.7 — `handleWebhook` */ export interface PluginWebhookInput { /** Endpoint key matching the manifest declaration. */ endpointKey: string; /** Inbound request headers. */ headers: Record; /** Raw request body as a UTF-8 string. */ rawBody: string; /** Parsed JSON body (if applicable and parseable). */ parsedBody?: unknown; /** Unique request identifier for idempotency checks. */ requestId: string; } // --------------------------------------------------------------------------- // Plugin definition // --------------------------------------------------------------------------- /** * The plugin definition shape passed to `definePlugin()`. * * The only required field is `setup`, which receives the `PluginContext` and * is where the plugin registers its handlers (events, jobs, data, actions, * tools, etc.). * * All other lifecycle hooks are optional. If a hook is not implemented the * host applies default behaviour (e.g. restarting the worker on config change * instead of calling `onConfigChanged`). * * @see PLUGIN_SPEC.md §13 — Host-Worker Protocol */ export interface PluginDefinition { /** * Called once when the plugin worker starts up, after `initialize` completes. * * This is where the plugin registers all its handlers: event subscriptions, * job handlers, data/action handlers, and tool registrations. Registration * must be synchronous after `setup` resolves — do not register handlers * inside async callbacks that may resolve after `setup` returns. * * @param ctx - The full plugin context provided by the host */ setup(ctx: PluginContext): Promise; /** * Called when the host wants to know if the plugin is healthy. * * The host polls this on a regular interval and surfaces the result in the * plugin health dashboard. If not implemented, the host infers health from * worker process liveness. * * @see PLUGIN_SPEC.md §13.2 — `health` */ onHealth?(): Promise; /** * Called when the operator updates the plugin's instance configuration at * runtime, without restarting the worker. * * If not implemented, the host restarts the worker to apply the new config. * * @param newConfig - The newly resolved configuration * @see PLUGIN_SPEC.md §13.4 — `configChanged` */ onConfigChanged?(newConfig: Record): Promise; /** * Called when the host is about to shut down the plugin worker. * * The worker has at most 10 seconds (configurable via plugin config) to * finish in-flight work and resolve this promise. After the deadline the * host sends SIGTERM, then SIGKILL. * * @see PLUGIN_SPEC.md §12.5 — Graceful Shutdown Policy */ onShutdown?(): Promise; /** * Called to validate the current plugin configuration. * * The host calls this: * - after the plugin starts (to surface config errors immediately) * - after the operator saves a new config (to validate before persisting) * - via the "Test Connection" button in the settings UI * * @param config - The configuration to validate * @see PLUGIN_SPEC.md §13.3 — `validateConfig` */ onValidateConfig?(config: Record): Promise; /** * Called to handle an inbound webhook delivery. * * The host routes `POST /api/plugins/:pluginId/webhooks/:endpointKey` to * this handler. The plugin is responsible for signature verification using * a resolved secret ref. * * If not implemented but webhooks are declared in the manifest, the host * returns HTTP 501 for webhook deliveries. * * @param input - Webhook delivery metadata and payload * @see PLUGIN_SPEC.md §13.7 — `handleWebhook` */ onWebhook?(input: PluginWebhookInput): Promise; } // --------------------------------------------------------------------------- // PaperclipPlugin — the sealed object returned by definePlugin() // --------------------------------------------------------------------------- /** * The sealed plugin object returned by `definePlugin()`. * * Plugin authors export this as the default export from their worker * entrypoint. The host imports it and calls the lifecycle methods. * * @see PLUGIN_SPEC.md §14 — SDK Surface */ export interface PaperclipPlugin { /** The original plugin definition passed to `definePlugin()`. */ readonly definition: PluginDefinition; } // --------------------------------------------------------------------------- // definePlugin — top-level factory // --------------------------------------------------------------------------- /** * Define a Paperclip plugin. * * Call this function in your worker entrypoint and export the result as the * default export. The host will import the module and call lifecycle methods * on the returned object. * * @param definition - Plugin lifecycle handlers * @returns A sealed `PaperclipPlugin` object for the host to consume * * @example * ```ts * import { definePlugin } from "@paperclipai/plugin-sdk"; * * export default definePlugin({ * async setup(ctx) { * ctx.logger.info("Plugin started"); * ctx.events.on("issue.created", async (event) => { * // handle event * }); * }, * * async onHealth() { * return { status: "ok" }; * }, * }); * ``` * * @see PLUGIN_SPEC.md §14.1 — Example SDK Shape */ export function definePlugin(definition: PluginDefinition): PaperclipPlugin { return Object.freeze({ definition }); }