# Runtime API

The plugin runs in an isolated runtime. Its only entry point is the default
export of an `XLibraryPlugin` object.

## Lifecycle

```ts
interface XLibraryPlugin {
  initialize?(context: PluginRuntimeContext, host: PluginHost): Promise<void> | void;
  invoke(invocation: PluginInvocation, host: PluginHost): Promise<unknown> | unknown;
  deactivate?(context: PluginRuntimeContext, host: PluginHost): Promise<void> | void;
}
```

- `initialize` runs once when the runtime starts. Use it to validate settings or
  warm a cache, not to create an unbounded background loop.
- `invoke` receives one of the supported methods below. Throw for an unsupported
  method.
- `deactivate` runs before shutdown. Release in-memory resources and write a safe
  diagnostic event if useful; do not delete persistent data here.

`context` contains only `apiVersion`, the plugin id/name/version, and granted
permissions. `host` is the only way to access application data and services.

## Invocation methods

Payload and result types are available from `@xlibrary/plugin-sdk` through
`PluginInvocationMap`. A switch on `invocation.method` narrows the payload type.

| Method | Payload | Return |
| --- | --- | --- |
| `game-sources.search` | `sourceId`, `query`, optional `cursor`/`locale`, `limit` | `{candidates, nextCursor?}` |
| `game-sources.resolve` | `sourceId`, optional `externalId` or `url`, locale | `PluginGameSourceDraft` |
| `game-sources.refresh` | source id, external id, URL, and current `id/name/version` | A draft plus a non-empty `changedFields` array |
| `game-sources.parse-page` | source id, optional identity, and `PluginGameSourcePageCapture` | `PluginGameSourceDraft` |
| `imports.parse` | source id and metadata for the selected input | `{games, warnings}` in the standard import format |
| `exports.serialize` | target id, selected `games?`, and format metadata | `{completed: true}` after all `host.exportOutput.write` calls |
| `filters.evaluate` | selected facets and the complete `games` list | `{matches: Record<facetId, gameId[]>}` for every facet |
| `backup.capture` | declared `dataVersion` | `{dataVersion, data}` with JSON-safe data |
| `backup.restore` | version, data, and `dryRun` | `{restored: true}`; do not write during a dry run |
| `ui.render` | contribution id, slot, and context | A data-only `PluginUiPanel` |
| `ui.action` | contribution id, action id, and context | Optional `{panel}` |
| `tracking.session-event` | provider id and a started/ended event | Optional custom-field values |
| `game-actions.execute` | action id, game, and `dryRun` | Message, optional `gamePatch`, and field values |
| `jobs.run` | job id, trigger, and optional tracking event | Any JSON result, usually `undefined` |
| `settings.migrate` | previous data version and values | `{values}` for the new version |
| `storage.migrate` | previous data version and JSON values | `{values}` for the new version |
| `lifecycle.deactivate` | `{}` | `undefined`; the runtime calls `deactivate` |

The runtime and application service validate result shapes again. Do not return
`Date`, `Map`, `Error`, class instances, functions, or circular objects: the
transport is a bounded JSON protocol.

## Host API

| API | Example | Permission/purpose |
| --- | --- | --- |
| `host.storage.get/set` | `await host.storage.set('cursor', {value})` | `plugin.storage`; persistent plugin-owned JSON. |
| `host.cache.get/set/delete` | `set(key, value, {ttlSeconds: 300})` | `plugin.cache`; disposable TTL cache, not a source of truth. |
| `host.settings.get/set` | `const values = await host.settings.get()` | `plugin.settings`; schema and defaults come from the manifest. |
| `host.network.request` | HTTPS GET/POST | `network.http`; only allowlisted approved hosts. |
| `host.notifications.show` | `{title, message, level}` | `notifications.show`; host notification with a quota. |
| `host.games.list` | `const games = await host.games.list()` | `library.games.read`; normalized read models. |
| `host.sessions.list` | `const sessions = await host.sessions.list()` | `sessions.events.read`; bounded session read models. |
| `host.gameFields.get` | `await host.gameFields.get(gameId)` | `game.custom-fields`; values in the plugin's namespace. |
| `host.importInput.readChunk` | `(inputId, offset, maxBytes)` | `imports.sources`; base64 bytes and `eof`, never a file path. |
| `host.exportOutput.write` | `await host.exportOutput.write(dataBase64)` | `exports.targets`; streamed base64 bytes to a staged target. |
| `host.auth.getStatus/begin/clear` | `await host.auth.begin('account')` | `plugin.auth`; the broker owns login flow and token lifecycle. |
| `host.auth.request` | `await host.auth.request('account', request)` | `plugin.auth`; authenticated request through the broker. |
| `host.diagnostics.log` | `{level, event, message, context}` | Always available; messages are redacted and must contain no secret/cookie/path. |

Example of a safe host call:

```ts
const cached = await host.cache.get<{items: unknown[]}>('catalog:search');
if (cached) return cached.items;

const response = await host.network.request({
  url: 'https://catalog.example/api/search?q=game',
  headers: {'accept': 'application/json'},
});
if (response.status !== 200) {
  await host.diagnostics.log({
    level: 'warn',
    event: 'catalog.search.failed',
    message: 'Provider returned a non-success status',
    context: {status: response.status},
  });
}
```

Do not log a complete response body, credential-bearing URL, authorization
header, cookie, passkey, magnet, or local path.

## Standard data contracts

### Game source candidate

```ts
{
  identity: {
    externalId: 'game-123',
    canonicalUrl: 'https://catalog.example/games/game-123'
  },
  title: 'Example Game',
  developer: 'Example Studio',
  version: '1.0.0',
  coverUrl: 'https://catalog.example/covers/game-123.jpg',
  summary: 'Short description',
  score: 0.98
}
```

URLs must use HTTPS. The application adds `providerId` after verifying that the
URL belongs to the hosts declared by the provider contribution.

### Game source draft

`name` and identity are required. Other fields may be partial; use
`isPartial: true` and `missingFields` when the provider could not supply them.

### Import result

```ts
return {
  games: [{
    name: 'Example Game',
    description: 'Description',
    cover: 'https://catalog.example/cover.jpg',
    tags: ['action'],
    externalLinks: [{providerId: 'catalog', externalId: 'game-123'}],
  }],
  warnings: [{code: 'missing-version', message: 'Version was not available'}],
};
```

An importer does not modify the library directly. It returns normalized preview
data; the standard import flow displays the preview and applies the plan.

### UI panel

```ts
return {
  title: 'Catalog tools',
  description: 'Actions provided by the plugin',
  nodes: [
    {type: 'stat', label: 'Score', value: '92'},
    {type: 'notice', tone: 'success', text: 'Synchronized'},
    {type: 'action', id: 'sync', label: 'Sync now'},
  ],
};
```

UI is a data-only contract. It cannot contain a Vue/React component, arbitrary
HTML, CSS, JavaScript callback, or iframe.
