/** * Canonical bridge/manifest types for a Caspian canvas. Dependency-free (no Node/Electron/ * React imports) so it can be bundled straight into a sandboxed iframe with no special * privileges. Copy this file into your canvas's own sdk/ folder alongside bridge.ts. */ // --------------------------------------------------------------------------- // Capability model // --------------------------------------------------------------------------- export const ALL_CAPABILITIES = [ 'read-frontmatter', 'write-frontmatter', 'create-notes', 'delete-notes', 'network', 'bundled-assets', 'canvas-sidecar' ] as const export type Capability = (typeof ALL_CAPABILITIES)[number] // --------------------------------------------------------------------------- // Canvas manifest (canvas.json) // --------------------------------------------------------------------------- export type SchemaFieldType = 'string' | 'number' | 'boolean' | 'enum' | 'id' | 'id-list' export interface SchemaField { type: SchemaFieldType values?: string[] // for enum default?: unknown } export type CanvasSchema = Record /** One optional, visitor-facing feature of a canvas: a control, panel, or mode * that a publisher can switch off for the public copy (a "Pull bones apart" * slider, an info sidebar, a "Reset view" button). Declared by the canvas itself * in canvas.json, tagged when the canvas is authored or changed, never inferred * at publish time. The publish dialog turns this list into checkboxes; unchecked * ids are passed to the published renderer via `DISABLED_FEATURES_GLOBAL`, which * the renderer honours by removing that feature's controls and holding it at its * default state. */ export interface CanvasFeature { /** Stable kebab-case id: the publish on/off choice and the renderer's * disabled-list check are both keyed by this. */ id: string /** Short, control-style name, e.g. "Pull bones apart". */ label: string /** One line: what a visitor does with it. */ description?: string } /** Global the published renderer reads on startup: a `string[]` of * `CanvasFeature` ids the publisher turned OFF. Absent/empty = everything on. * Injected into the published dist/renderer.html at publish time. */ export const DISABLED_FEATURES_GLOBAL = '__CASPIAN_DISABLED_FEATURES__' export interface CanvasManifest { id: string name: string version: string author: string apiVersion: string renderer: string description?: string schema: CanvasSchema capabilities: Capability[] /** Publisher-toggleable features this canvas declares. Absent on canvases * authored before the feature manifest existed. See `sdk/features.ts`. */ features?: CanvasFeature[] } /** Bridge/manifest contract version this build of the host implements. */ export const HOST_API_VERSION = '1' // --------------------------------------------------------------------------- // Frontmatter / notes // --------------------------------------------------------------------------- /** Engine-owned frontmatter keys, namespaced so they never collide with user fields. */ export interface CaspianCoreFrontmatter { caspian_id: string caspian_canvas: string caspian_type?: string } export type FrontmatterValue = string | number | boolean | string[] | null export type NoteFrontmatter = CaspianCoreFrontmatter & Record export interface NoteRecord { id: string path: string // path relative to vault root, posix-style ('/'-separated) frontmatter: NoteFrontmatter /** Derived from the note's first `#` heading (falling back to its filename). Read-only, * never written back: a display convenience so canvases don't need a full body fetch * just to label a node. */ title: string /** true if the file failed to parse; frontmatter will be a best-effort empty shell */ parseError?: string /** Last-modified time (epoch ms), for the explorer's "sort by modified". */ mtime: number } export interface NoteContent { id: string path: string title: string frontmatter: NoteFrontmatter body: string } /** * What actually crosses the bridge into a sandboxed canvas. No `path`: canvases * address and receive notes by `caspian_id` only, never by filesystem location. */ export interface CanvasNoteView { id: string title: string frontmatter: NoteFrontmatter body: string } // --------------------------------------------------------------------------- // Bridge API: the fixed verb set a canvas renderer may call // --------------------------------------------------------------------------- export interface AnchorClickParams { id: string } export interface AnchorCreateParams { id?: string // if omitted, host generates one initialFrontmatter: Record title?: string } export interface AnchorDeleteParams { id: string } export interface NoteGetMetaParams { id: string } export interface NoteSetMetaParams { id: string patch: Record } export interface SidecarReadParams { // no params: always the calling canvas's own sidecar } export interface SidecarWriteParams { data: unknown } /** * Asks the user for a single line of text, via a dialog the *trusted host* renders and * shows, never the canvas's own sandboxed iframe. Two reasons it has to work this way * rather than a canvas just building its own `` overlay (which map/webpage/etc. * already do for things like a pin's title): the iframe is sandboxed with * `allow-scripts` only, so even the browser's native `window.prompt()` is unavailable * inside it; and a value like an API key deserves a dialog the user can trust wasn't * drawn by arbitrary third-party canvas code. The host always labels the dialog with the * requesting canvas's name so it's never mistaken for a message from Caspian itself. */ export interface HostPromptParams { title: string body?: string placeholder?: string /** Masks the input, for secrets like API keys. */ secret?: boolean confirmLabel?: string } export interface BridgeMethodMap { 'anchor.click': { params: AnchorClickParams; result: { note: CanvasNoteView | null } } 'anchor.create': { params: AnchorCreateParams; result: { note: CanvasNoteView } } 'anchor.delete': { params: AnchorDeleteParams; result: { ok: true } } 'note.getMeta': { params: NoteGetMetaParams; result: { frontmatter: NoteFrontmatter | null } } 'note.setMeta': { params: NoteSetMetaParams; result: { frontmatter: NoteFrontmatter } } 'note.list': { params: Record; result: { notes: NoteRecord[] } } 'sidecar.read': { params: SidecarReadParams; result: { data: unknown } } 'sidecar.write': { params: SidecarWriteParams; result: { ok: true } } 'asset.import': { params: { kind?: 'image' | 'model' } result: { assetId: string; url: string } | null } 'asset.url': { params: { assetId: string }; result: { url: string | null } } /** `value` is `null` if the user cancelled, never an empty string (the host dialog * won't confirm an empty input). Handled entirely by the trusted host, before the * request ever reaches the main process. See HostPromptParams' own comment. */ 'host.prompt': { params: HostPromptParams; result: { value: string | null } } } export type BridgeMethod = keyof BridgeMethodMap export const BRIDGE_CHANNEL = 'caspian-bridge' export interface BridgeRequestMessage { channel: typeof BRIDGE_CHANNEL type: 'request' id: string method: M params: BridgeMethodMap[M]['params'] } export interface BridgeResponseMessage { channel: typeof BRIDGE_CHANNEL type: 'response' id: string ok: boolean result?: BridgeMethodMap[M]['result'] error?: string } export type BridgeEventName = 'ready' | 'note.changed' | 'anchor.selected' | 'capabilities' /** Payload shape for each host-pushed event, mirroring `BridgeMethodMap`'s per-method * typing, giving both the host and `bridge.ts` real types instead of `unknown`. */ export interface BridgeEventPayloadMap { ready: Record 'note.changed': Record /** Sent when the host wants this canvas to reveal a specific note's anchor, e.g. after * "Go to file" picks a note this canvas owns. */ 'anchor.selected': { id: string } capabilities: { capabilities: Capability[] } } export interface BridgeEventMessage { channel: typeof BRIDGE_CHANNEL type: 'event' event: E payload: BridgeEventPayloadMap[E] } export type BridgeMessage = BridgeRequestMessage | BridgeResponseMessage | BridgeEventMessage /** Maps each bridge method to the capability required to call it. `null` = always allowed. */ export const METHOD_CAPABILITY: Record = { 'anchor.click': null, 'anchor.create': 'create-notes', 'anchor.delete': 'delete-notes', 'note.getMeta': 'read-frontmatter', 'note.setMeta': 'write-frontmatter', 'note.list': 'read-frontmatter', 'sidecar.read': 'canvas-sidecar', 'sidecar.write': 'canvas-sidecar', 'asset.import': 'canvas-sidecar', 'asset.url': 'canvas-sidecar', // Shows a dialog and returns text the user typed; no vault access, so (like // anchor.click) it needs no capability and works even before the canvas is approved. 'host.prompt': null }