@hoardodile/sdk-web 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,727 @@
1
+ import * as _hoardodile_sdk_types from '@hoardodile/sdk-types';
2
+ import { SerializedFileList, ReadFileRange, PluginDownloadRequest, PluginDownloadResult, PluginAssetDeleteResult, SearchMeta, FileStats, PluginSchema, Message, DanmakuListFilter, Danmaku, DanmakuMode, AnchorData } from '@hoardodile/sdk-types';
3
+ export { AnchorData, Danmaku, DanmakuListFilter, DanmakuMode, FileStats, Message, ReadFileRange, ResAnchor } from '@hoardodile/sdk-types';
4
+ import { ImageVariantSpec } from '@hoardodile/sdk-types/image-variant';
5
+ export { ImageVariantSpec } from '@hoardodile/sdk-types/image-variant';
6
+
7
+ /**
8
+ * Wire protocol version shared between the plugin SDK and the browser
9
+ * host. Bumped only on incompatible protocol changes. Plugins stamp every
10
+ * outbound message with it; the host warns loudly when a plugin was built
11
+ * against a different version.
12
+ */
13
+ declare const PROTOCOL_VERSION: 1;
14
+
15
+ type PluginResolvedTheme = "light" | "dark";
16
+ /**
17
+ * Canonical list of theme palette ids — the single source of truth shared
18
+ * by the host app and every plugin. `mono` is the default and has no CSS
19
+ * class (it lives in `:root` / `.dark`); every other id maps to a
20
+ * `.theme-<id>` block in `@hoardodile/ui/theme.css` and a
21
+ * `theme.palette.<id>` i18n label.
22
+ */
23
+ declare const pluginThemePalettes: readonly ["mono", "sage", "parchment", "azure", "hoardodile"];
24
+ type PluginThemePalette = (typeof pluginThemePalettes)[number];
25
+ /** Icon rendering style as chosen in host Settings → Icons. */
26
+ type PluginIconStyle = "duotone" | "grayscale" | "linear";
27
+ /** Host app font as observed by the plugin. */
28
+ type PluginFonts = {
29
+ /** CSS `font-family` stack; empty when the plugin opted out of inheritance. */
30
+ readonly family: string;
31
+ /** Absolute paths (`/fonts/...`) of the preset stylesheets backing the stack. */
32
+ readonly cssPaths: readonly string[];
33
+ };
34
+ /**
35
+ * Context injected into the iframe as `window.__context__` and pushed via
36
+ * the `context` host push. Not a one-shot: the host may push a replacement
37
+ * context at any time (a pooled iframe is rebound across resources without
38
+ * a reload), and every push re-invokes the `mountPlugin` mount callback.
39
+ */
40
+ type PluginIframeContext = {
41
+ readonly pluginId: string;
42
+ readonly resId: string;
43
+ readonly resName: string;
44
+ readonly sourceMeta: unknown;
45
+ readonly searchMeta: SearchMeta | undefined;
46
+ readonly fileStats: FileStats | undefined;
47
+ readonly contentPluginId: string;
48
+ /** Current UI language code. The iframe uses this to select its own locale bundle. */
49
+ readonly language: string;
50
+ /** Current resolved theme (light or dark). */
51
+ readonly resolvedTheme: PluginResolvedTheme;
52
+ /** Current theme palette. */
53
+ readonly palette: PluginThemePalette;
54
+ /** Current icon rendering style (Settings → Icons). */
55
+ readonly iconStyle: PluginIconStyle;
56
+ /**
57
+ * Host app font to apply inside the iframe: a CSS `font-family` stack
58
+ * plus the preset stylesheets that back it. An empty family means the
59
+ * plugin opted out (`ui.inheritFont: false`) and keeps its own fonts.
60
+ */
61
+ readonly fonts: PluginFonts;
62
+ /** Initial plugin-scoped prefs (unprefixed keys) loaded from server. */
63
+ readonly initialPrefs: Record<string, string>;
64
+ /** Initial plugin+resId cache entries (unprefixed keys) loaded from server. */
65
+ readonly initialCache: Record<string, string>;
66
+ /**
67
+ * Short-lived token that lets the sandboxed iframe fetch resource files
68
+ * without a session cookie (null-origin iframe cannot send SameSite cookies).
69
+ */
70
+ readonly fileToken: string;
71
+ /**
72
+ * Short-lived token for the plugin's own asset vault URLs
73
+ * (`/api/plugin-assets/<pluginId>/<token>/<path>`), issued only when
74
+ * the plugin's manifest declares the `download` permission. Empty
75
+ * string otherwise — `resolveAssetUrl` throws on the empty token
76
+ * (never builds a malformed `/…//path` URL); check the permission
77
+ * before relying on vault URLs.
78
+ */
79
+ readonly assetToken: string;
80
+ };
81
+ /** Wire request from plugin (iframe) to host. */
82
+ type PluginRequest = {
83
+ readonly type: "request";
84
+ readonly id: number;
85
+ readonly method: string;
86
+ readonly params?: unknown;
87
+ /** Wire protocol version the plugin was built against (see {@link PROTOCOL_VERSION}). */
88
+ readonly proto?: number;
89
+ /**
90
+ * SDK-internal scope stamp: the resource the request was issued for,
91
+ * captured by the runtime when the plugin called the API. The host
92
+ * drops the request as stale when the stamp no longer matches the
93
+ * iframe's binding (e.g. an unmount flush racing a rebind), so late
94
+ * requests never leak into the wrong resource. Plugin code never
95
+ * sets this.
96
+ */
97
+ readonly resId?: string;
98
+ };
99
+ /** Wire response from host to plugin for a prior request. */
100
+ type HostResponse = {
101
+ readonly type: "response";
102
+ readonly id: number;
103
+ readonly ok: boolean;
104
+ readonly data?: unknown;
105
+ readonly error?: string;
106
+ /**
107
+ * Optional machine-readable plugin error code (e.g. the asset
108
+ * `DENIED` / `UNAVAILABLE` / `POLICY` vocabulary) so the bridge can
109
+ * reject with an Error carrying the code — plugin code branches on
110
+ * `err.name` across the postMessage boundary.
111
+ */
112
+ readonly errorCode?: string;
113
+ /**
114
+ * Legacy alias of {@link errorCode} (same value) kept for host
115
+ * builds predating the unified field. The bridge reads
116
+ * `errorCode ?? errorName`.
117
+ */
118
+ readonly errorName?: string;
119
+ };
120
+ /** Wire push event from host to plugin. */
121
+ type HostPush = {
122
+ readonly type: "push";
123
+ readonly key: string;
124
+ readonly data?: unknown;
125
+ };
126
+ /** Wire subscription request from plugin to host. */
127
+ type PluginSubscribe = {
128
+ readonly type: "subscribe";
129
+ readonly key: string;
130
+ /** Wire protocol version the plugin was built against (see {@link PROTOCOL_VERSION}). */
131
+ readonly proto?: number;
132
+ };
133
+ /**
134
+ * Wire acknowledgement from plugin to host: the pushed context has been
135
+ * applied *and painted* — the mount callback returned (for React plugins
136
+ * the new tree is already committed via flushSync) and a frame with the
137
+ * new content has reached the compositor. The host keeps a freshly
138
+ * claimed pooled iframe transparent until this arrives, so the previous
139
+ * resource's content never shows under a new claim.
140
+ */
141
+ type PluginContextPainted = {
142
+ readonly type: "contextPainted";
143
+ readonly resId: string;
144
+ /** Wire protocol version the plugin was built against (see {@link PROTOCOL_VERSION}). */
145
+ readonly proto?: number;
146
+ };
147
+ /** Union of all messages a plugin can send to the host. */
148
+ type PluginMessage = PluginRequest | PluginSubscribe | PluginContextPainted;
149
+ /** Union of all messages the host can send to a plugin. */
150
+ type HostMessage = HostResponse | HostPush;
151
+ /** Type-safe request protocol table. Each entry declares input and output. */
152
+ type PluginRequests = {
153
+ logInfo: {
154
+ readonly input: {
155
+ readonly message: string;
156
+ readonly data?: Record<string, unknown>;
157
+ };
158
+ readonly output: undefined;
159
+ };
160
+ logWarn: {
161
+ readonly input: {
162
+ readonly message: string;
163
+ readonly data?: Record<string, unknown>;
164
+ };
165
+ readonly output: undefined;
166
+ };
167
+ logError: {
168
+ readonly input: {
169
+ readonly message: string;
170
+ readonly data?: Record<string, unknown>;
171
+ };
172
+ readonly output: undefined;
173
+ };
174
+ listFiles: {
175
+ readonly input: undefined;
176
+ readonly output: SerializedFileList;
177
+ };
178
+ readFile: {
179
+ readonly input: {
180
+ readonly path: string;
181
+ /** Byte range (see {@link ReadFileRange}); omitted = whole file. */
182
+ readonly range?: ReadFileRange;
183
+ };
184
+ readonly output: ArrayBuffer;
185
+ };
186
+ listMessages: {
187
+ readonly input: undefined;
188
+ readonly output: readonly _hoardodile_sdk_types.Message[];
189
+ };
190
+ createMessage: {
191
+ readonly input: {
192
+ readonly body: string;
193
+ /** Wire anchor envelope; plugins pass raw data, the SDK wraps it. */
194
+ readonly anchor?: _hoardodile_sdk_types.AnchorData;
195
+ };
196
+ readonly output: _hoardodile_sdk_types.Message;
197
+ };
198
+ listDanmaku: {
199
+ readonly input: {
200
+ readonly filter?: _hoardodile_sdk_types.DanmakuListFilter;
201
+ };
202
+ readonly output: readonly _hoardodile_sdk_types.Danmaku[];
203
+ };
204
+ createDanmaku: {
205
+ readonly input: {
206
+ readonly text: string;
207
+ /** Wire anchor envelope (see {@link PluginRequests.createMessage}). */
208
+ readonly anchor: _hoardodile_sdk_types.AnchorData;
209
+ readonly mode?: _hoardodile_sdk_types.DanmakuMode;
210
+ };
211
+ readonly output: _hoardodile_sdk_types.Danmaku;
212
+ };
213
+ setPref: {
214
+ /** Persist a plugin-wide preference; host broadcasts `prefsChanged`. */
215
+ readonly input: {
216
+ readonly key: string;
217
+ readonly value: string;
218
+ };
219
+ readonly output: undefined;
220
+ };
221
+ setCache: {
222
+ /**
223
+ * Persist a per-resource cache entry; host broadcasts
224
+ * `cacheChanged`.
225
+ */
226
+ readonly input: {
227
+ readonly key: string;
228
+ readonly value: string;
229
+ };
230
+ readonly output: undefined;
231
+ };
232
+ invalidate: {
233
+ /** Request the host to invalidate cached data for a target. */
234
+ readonly input: {
235
+ readonly target: InvalidateTarget;
236
+ };
237
+ readonly output: undefined;
238
+ };
239
+ /**
240
+ * User-consented download into the plugin's own asset vault (see
241
+ * `@hoardodile/sdk-types/plugin-asset`). The host asks the user with
242
+ * the shared consent dialog; cached destinations resolve without any
243
+ * dialog. Rejections carry a machine-readable `err.name`
244
+ * (`DENIED` / `UNAVAILABLE` / `POLICY`).
245
+ *
246
+ * Timeout: {@link pluginRequestTimeouts.download} — the client-side
247
+ * ceiling for the whole flow (consent dialog + transfer), declared in
248
+ * the protocol meta rather than ad hoc at the call site.
249
+ */
250
+ download: {
251
+ readonly input: PluginDownloadRequest;
252
+ readonly output: PluginDownloadResult;
253
+ };
254
+ /**
255
+ * Remove a vault file (idempotent); the plugin decides its own vault
256
+ * lifecycle — no user consent, nothing leaves the host.
257
+ */
258
+ deleteAsset: {
259
+ readonly input: {
260
+ readonly path: string;
261
+ };
262
+ readonly output: PluginAssetDeleteResult;
263
+ };
264
+ };
265
+ /** Type-safe push protocol table. */
266
+ type HostPushes = {
267
+ context: PluginIframeContext;
268
+ visibility: {
269
+ readonly visible: boolean;
270
+ };
271
+ themeChanged: {
272
+ readonly resolvedTheme: string;
273
+ readonly palette: string;
274
+ /** Active icon rendering style — host applies it as `data-icon-style`. */
275
+ readonly iconStyle: PluginIconStyle;
276
+ };
277
+ fontsChanged: PluginFonts;
278
+ /**
279
+ * The wire payload is a bare language-code string: it predates the
280
+ * typed protocol table and must stay stable for already-installed
281
+ * plugin builds (see `pushLanguageChanged` in apps/web — do not wrap
282
+ * it in an object).
283
+ */
284
+ languageChanged: string;
285
+ prefsChanged: {
286
+ readonly key: string;
287
+ readonly value?: string;
288
+ };
289
+ /**
290
+ * A plugin+resource cache entry changed. With data, carries the single
291
+ * changed entry; without data (undefined), all entries were cleared and
292
+ * the plugin should drop its whole cache store.
293
+ */
294
+ cacheChanged: {
295
+ readonly resId: string;
296
+ readonly key: string;
297
+ readonly value?: string;
298
+ } | undefined;
299
+ /**
300
+ * Host-initiated request to jump to an anchor (e.g. the user clicked a
301
+ * comment anchor in the host UI). Carries the plugin-defined anchor data
302
+ * only — the resource is always the iframe's own.
303
+ */
304
+ anchorJump: _hoardodile_sdk_types.AnchorData;
305
+ "res:invalidate": undefined;
306
+ "resources:invalidate": undefined;
307
+ "messages:invalidate": undefined;
308
+ "danmaku:invalidate": undefined;
309
+ };
310
+ /** Extract the input type for a request key. */
311
+ type RequestInput<K extends keyof PluginRequests> = PluginRequests[K]["input"];
312
+ /** Extract the output type for a request key. */
313
+ type RequestOutput<K extends keyof PluginRequests> = PluginRequests[K]["output"];
314
+ /** Targets that can be invalidated from the plugin runtime. */
315
+ type InvalidateTarget = "resource" | "resources" | "messages" | "danmaku";
316
+ /** Wire keys for host→plugin pushes, mirroring {@link HostPushes}. */
317
+ declare const hostPushKeys: {
318
+ readonly context: "context";
319
+ readonly visibility: "visibility";
320
+ readonly themeChanged: "themeChanged";
321
+ readonly fontsChanged: "fontsChanged";
322
+ readonly languageChanged: "languageChanged";
323
+ readonly prefsChanged: "prefsChanged";
324
+ readonly cacheChanged: "cacheChanged";
325
+ readonly anchorJump: "anchorJump";
326
+ readonly resInvalidate: "res:invalidate";
327
+ readonly resourcesInvalidate: "resources:invalidate";
328
+ readonly messagesInvalidate: "messages:invalidate";
329
+ readonly danmakuInvalidate: "danmaku:invalidate";
330
+ };
331
+ /** Wire method names for plugin→host requests, mirroring {@link PluginRequests}. */
332
+ declare const pluginMethods: {
333
+ readonly readFile: "readFile";
334
+ readonly listFiles: "listFiles";
335
+ readonly listMessages: "listMessages";
336
+ readonly createMessage: "createMessage";
337
+ readonly listDanmaku: "listDanmaku";
338
+ readonly createDanmaku: "createDanmaku";
339
+ readonly setPref: "setPref";
340
+ readonly setCache: "setCache";
341
+ readonly invalidate: "invalidate";
342
+ readonly download: "download";
343
+ readonly deleteAsset: "deleteAsset";
344
+ readonly logInfo: "logInfo";
345
+ readonly logWarn: "logWarn";
346
+ readonly logError: "logError";
347
+ };
348
+ /** Push key broadcast after each {@link InvalidateTarget} is invalidated. */
349
+ declare const invalidatePushKeys: {
350
+ readonly resource: "res:invalidate";
351
+ readonly resources: "resources:invalidate";
352
+ readonly messages: "messages:invalidate";
353
+ readonly danmaku: "danmaku:invalidate";
354
+ };
355
+ /**
356
+ * Type-safe host bridge. The runtime still serialises messages as plain
357
+ * postMessage objects; this contract gives compile-time guarantees to callers.
358
+ *
359
+ * Per-method timeouts are declared on the protocol table entries
360
+ * (`timeoutMs`) — the bridge reads them, callers never pass one.
361
+ */
362
+ type Host = {
363
+ request<K extends keyof PluginRequests>(method: K, ...args: RequestInput<K> extends void ? [] : [RequestInput<K>]): Promise<RequestOutput<K>>;
364
+ subscribe<K extends keyof HostPushes>(key: K, handler: (data: HostPushes[K]) => void): () => void;
365
+ /**
366
+ * Internal — returns a Host whose requests are stamped with the given
367
+ * resource scope (see {@link PluginRequest.resId}). Used by the runtime
368
+ * to bind one API instance to the resource it was created for.
369
+ */
370
+ withScope: (resId: string) => Host;
371
+ };
372
+
373
+ /**
374
+ * Narrow an unknown value to a plain record. Handy for decoding
375
+ * plugin-defined payloads (e.g. anchor data) without assertion casts.
376
+ */
377
+ declare function isRecord(value: unknown): value is Record<string, unknown>;
378
+ /**
379
+ * Lazily create (and then reuse) the singleton postMessage bridge to the
380
+ * host parent window: request/response with a 10s timeout plus push
381
+ * subscriptions. Only the host window may drive the bridge — messages
382
+ * from any other source are ignored. Called automatically by
383
+ * {@link createIframeHostAPI}; you only need this when talking to the
384
+ * host outside the plugin API surface.
385
+ */
386
+ declare function ensureHostBridge(): Host;
387
+
388
+ /**
389
+ * What {@link WebPluginAPI.resolveFileUrl} may address: the original
390
+ * bytes (`"original"`, or omit the argument), the default preview
391
+ * variant (`"preview"`), or a custom derived image via
392
+ * {@link ImageVariantSpec}.
393
+ */
394
+ type FileUrlVariant = "original" | "preview" | ImageVariantSpec;
395
+ /**
396
+ * The current resource as seen by the plugin iframe: id/name plus the
397
+ * schema-typed metadata (`sourceMeta`, `searchMeta`, `fileStats`) that
398
+ * the host derived at import time. Injected via the iframe context; the
399
+ * reactive hooks derive from it, so render code reads the live value
400
+ * from `usePluginAPI().resource`.
401
+ */
402
+ type PluginResource<TSchema extends PluginSchema = PluginSchema> = {
403
+ readonly id: string;
404
+ readonly name: string;
405
+ readonly sourceMeta: TSchema["sourceMeta"];
406
+ readonly searchMeta: TSchema["searchMeta"];
407
+ readonly fileStats: FileStats | undefined;
408
+ readonly contentPluginId: string;
409
+ };
410
+ /** Encode/decode pair for typed preference values. */
411
+ type Codec<T> = {
412
+ readonly encode: (value: T) => string;
413
+ readonly decode: (raw: string) => T | undefined;
414
+ };
415
+ /** Reactive query state returned by hooks. */
416
+ type QueryState<T> = {
417
+ readonly data: T | undefined;
418
+ readonly isLoading: boolean;
419
+ readonly isError: boolean;
420
+ readonly error: Error | null;
421
+ };
422
+ /** Reactive mutation state returned by hooks. */
423
+ type MutationState<TInput, TOutput> = {
424
+ readonly mutate: (input: TInput) => Promise<TOutput>;
425
+ readonly isPending: boolean;
426
+ };
427
+ /** Current theme as observed by the plugin. */
428
+ type Theme = {
429
+ readonly resolvedTheme: string;
430
+ readonly palette: string;
431
+ /** Icon rendering style (`duotone` | `grayscale` | `linear`). */
432
+ readonly iconStyle: string;
433
+ };
434
+ /**
435
+ * Imperative, framework-agnostic API surface injected into plugin render
436
+ * modules. Reactive hooks (see {@link ReactivePluginAPI}) are provided by
437
+ * framework adapters — `@hoardodile/sdk-react` composes both into the
438
+ * full API seen by React plugin components.
439
+ */
440
+ type WebPluginAPI<TSchema extends PluginSchema = PluginSchema> = {
441
+ /** Logging */
442
+ readonly logInfo: (message: string, data?: Record<string, unknown>) => void;
443
+ readonly logWarn: (message: string, data?: Record<string, unknown>) => void;
444
+ readonly logError: (message: string, data?: Record<string, unknown>) => void;
445
+ /** Resource context. */
446
+ readonly resource: PluginResource<TSchema>;
447
+ /** Files. */
448
+ readonly listFiles: () => Promise<readonly TSchema["file"][]>;
449
+ /**
450
+ * Read a file relative to the resource root. Without `range` the whole
451
+ * file is returned; large files should be read in bounded chunks via
452
+ * the byte range (mirrors the server-side `ResourceAPI.readFile`).
453
+ */
454
+ readonly readFile: (path: string, range?: ReadFileRange) => Promise<ArrayBuffer>;
455
+ /**
456
+ * Resolve a server-rendered URL for a file inside the resource.
457
+ * Without `variant` (or with `"original"`) the URL addresses the
458
+ * original bytes; `"preview"` selects the default preview variant
459
+ * (AVIF, fit inside the standard area cap); pass an
460
+ * {@link ImageVariantSpec} to request a custom derived image —
461
+ * e.g. `{ format: "webp", fit: "exact" }` transcodes to WebP at the
462
+ * source's exact pixel dimensions (no resize), and
463
+ * `{ maxArea: 2_000_000 }` caps a downscale. Variant renders are
464
+ * cached by the host; pick the `file.preview` flag to gate an
465
+ * original/preview toggle.
466
+ */
467
+ readonly resolveFileUrl: (filename: string, variant?: FileUrlVariant) => string;
468
+ /**
469
+ * Resolve the URL of a file materialized by the plugin's
470
+ * `extractArchive` hook: an inner entry of an archive (zip/tar)
471
+ * served from the host's extraction cache. `path` is the entry's
472
+ * relative path inside the archive, exactly as returned by
473
+ * `extractArchive` / the `listFiles` hook. Tokenized like
474
+ * `resolveFileUrl`.
475
+ */
476
+ readonly resolveExtractedUrl: (path: string) => string;
477
+ /**
478
+ * Resolve the URL of the host's in-flight extraction progress for
479
+ * this resource (see `extractArchive`). Returns
480
+ * `{ done, total }` while materializing, `null` otherwise. Tokenized
481
+ * like `resolveFileUrl`; polls are cheap (no-store JSON).
482
+ */
483
+ readonly extractProgressUrl: () => string;
484
+ /**
485
+ * Root URL of the current resource's files directory, trailing-slash
486
+ * included. For vendor SDKs that internally join relative paths and need
487
+ * a base.
488
+ */
489
+ readonly resolveBaseUrl: () => string;
490
+ /**
491
+ * Resolve a server-rendered frame thumbnail URL for a video file at the
492
+ * given timestamp (in milliseconds, measured from the start of the
493
+ * file). The server decodes the requested frame on demand; callers
494
+ * should debounce frequent invocations (e.g. while scrubbing) to avoid
495
+ * a flood of decode requests.
496
+ */
497
+ readonly resolveFrameUrl: (filename: string, timeMs: number) => string;
498
+ /** Plugin asset vault. */
499
+ /**
500
+ * User-consented download into the plugin's own asset vault: when the
501
+ * destination already exists the host answers `cached: true` (no
502
+ * dialog, no network); otherwise the host asks the user (shared
503
+ * consent dialog, URL shown verbatim) and downloads on approval.
504
+ * Rejections carry a machine-readable `err.name`
505
+ * (`DENIED` / `UNAVAILABLE` / `POLICY`).
506
+ */
507
+ readonly download: (request: PluginDownloadRequest) => Promise<PluginDownloadResult>;
508
+ /**
509
+ * Resolve the tokenized URL of a file in the plugin's own vault
510
+ * (`/api/plugin-assets/<pluginId>/<token>/<path>`). Use it to load a
511
+ * downloaded runtime or asset from inside the sandboxed iframe, e.g.
512
+ * `<script src={api.resolveAssetUrl("runtime/live2d.min.js")} />`
513
+ * (served with an exact JS MIME + `nosniff`, so classic scripts,
514
+ * module imports and `fetch` all work).
515
+ */
516
+ readonly resolveAssetUrl: (path: string) => string;
517
+ /**
518
+ * Remove a vault file (idempotent: an absent file answers
519
+ * `{ existed: false }`). The plugin decides the vault's lifecycle —
520
+ * no user consent, nothing leaves the host.
521
+ */
522
+ readonly deleteAsset: (path: string) => Promise<PluginAssetDeleteResult>;
523
+ /** Messages. */
524
+ readonly listMessages: () => Promise<readonly Message[]>;
525
+ readonly createMessage: (input: {
526
+ readonly body: string;
527
+ /** Raw plugin location data (see {@link PluginSchema.anchor}). */
528
+ readonly anchor?: TSchema["anchor"];
529
+ }) => Promise<Message>;
530
+ /** Danmaku. */
531
+ readonly listDanmaku: (filter?: DanmakuListFilter) => Promise<readonly Danmaku[]>;
532
+ readonly createDanmaku: (input: {
533
+ readonly text: string;
534
+ /** Raw plugin location data (see {@link PluginSchema.anchor}). */
535
+ readonly anchor: TSchema["anchor"];
536
+ readonly mode?: DanmakuMode;
537
+ }) => Promise<Danmaku>;
538
+ /** Preferences. */
539
+ readonly getPref: (key: string) => string | undefined;
540
+ readonly setPref: (key: string, value: string) => void;
541
+ /** Cache. */
542
+ readonly getCache: (key: string) => string | undefined;
543
+ readonly setCache: (key: string, value: string) => void;
544
+ readonly listCache: () => readonly {
545
+ readonly key: string;
546
+ readonly value: string;
547
+ }[];
548
+ /** Invalidation. */
549
+ readonly invalidate: (target: InvalidateTarget) => Promise<void>;
550
+ /**
551
+ * Subscribe to host-initiated anchor jumps (e.g. the user clicked a
552
+ * comment anchor in the host UI). The callback receives the raw wire
553
+ * envelope ({@link AnchorData}) — decode `anchor.data` yourself, or
554
+ * use the typed `useAnchorJump` from `@hoardodile/sdk-react`, which
555
+ * decodes at the SDK boundary. Always targets the iframe's own
556
+ * resource. Returns an unsubscribe function.
557
+ */
558
+ readonly onAnchorJump: (cb: (anchor: AnchorData) => void) => () => void;
559
+ };
560
+ /**
561
+ * Reactive (hook-based) API surface, implemented by framework adapters.
562
+ * Plain `@hoardodile/sdk-web` consumers get the imperative
563
+ * {@link WebPluginAPI} only; `@hoardodile/sdk-react` provides these via
564
+ * `createPluginQueryAPI`.
565
+ */
566
+ type ReactivePluginAPI<TSchema extends PluginSchema = PluginSchema> = {
567
+ readonly useFileList: () => QueryState<readonly TSchema["file"][]>;
568
+ readonly useMessageList: () => QueryState<readonly Message[]>;
569
+ readonly useCreateMessage: () => MutationState<{
570
+ readonly body: string;
571
+ readonly anchor?: TSchema["anchor"];
572
+ }, Message>;
573
+ readonly useDanmakuList: (filter?: DanmakuListFilter) => QueryState<readonly Danmaku[]>;
574
+ readonly useCreateDanmaku: () => MutationState<{
575
+ readonly text: string;
576
+ readonly anchor: TSchema["anchor"];
577
+ readonly mode?: DanmakuMode;
578
+ }, Danmaku>;
579
+ readonly usePref: <T>(key: string, defaultValue: T, codec?: Codec<T>) => readonly [T, (value: T) => void];
580
+ readonly useTheme: () => Theme;
581
+ readonly useFont: () => PluginFonts;
582
+ };
583
+
584
+ /**
585
+ * JSON codec covering the common case (`number`, `string`, `boolean`, plain
586
+ * objects). Falls back to `undefined` on parse errors so callers see "missing"
587
+ * instead of a corrupted value.
588
+ */
589
+ declare function jsonCodec<T>(): Codec<T>;
590
+ /**
591
+ * Plain-string number codec. Avoids the `""` → `0` ambiguity of
592
+ * {@link jsonCodec} by treating any non-finite parse as missing.
593
+ */
594
+ declare function numberCodec(fallback?: number): Codec<number>;
595
+ /**
596
+ * Boolean codec using `"1"`/`"0"` for compact storage. Also accepts
597
+ * `"true"`/`"false"` for interoperability.
598
+ */
599
+ declare function booleanCodec(): Codec<boolean>;
600
+
601
+ /**
602
+ * Deep-partial override type for {@link createWebPluginAPI}: nested
603
+ * objects and functions can be overridden selectively; functions may
604
+ * also be replaced by `undefined` to drop the default no-op.
605
+ */
606
+ type DeepPartial<T> = {
607
+ [K in keyof T]?: T[K] extends (...args: never[]) => unknown ? T[K] | undefined : T[K] extends object ? DeepPartial<T[K]> : T[K];
608
+ };
609
+ /** The complete plugin API shape stubbed by {@link createWebPluginAPI}. */
610
+ type StubbedPluginAPI = WebPluginAPI & ReactivePluginAPI;
611
+ /**
612
+ * Returns a minimal complete plugin API for render tests — the imperative
613
+ * surface plus no-op reactive hooks. All fields return empty/loading/no-op
614
+ * values; override via `overrides` to exercise plugin-specific code paths.
615
+ *
616
+ * Framework-agnostic counterpart of the React `createWebPluginAPI` in
617
+ * `@hoardodile/sdk-react`: this one returns a plain object (no React
618
+ * provider), that one wraps the same stub in a `StubPluginAPIProvider`
619
+ * so component tests can render against it.
620
+ */
621
+ declare function createWebPluginAPI(overrides?: DeepPartial<StubbedPluginAPI>): StubbedPluginAPI;
622
+
623
+ /** The last context pushed by the host, if any. */
624
+ declare function getPluginContext(): PluginIframeContext | undefined;
625
+ /**
626
+ * Subscribe to iframe visibility changes (tab hidden, iframe released,
627
+ * overlays opened by the host). The callback receives the current
628
+ * visibility; returns an unsubscribe function. Backs `useVisibility`
629
+ * in `@hoardodile/sdk-react`.
630
+ */
631
+ declare function subscribeToVisibility(cb: (visible: boolean) => void): () => void;
632
+ /** Current visibility snapshot without subscribing. */
633
+ declare function getVisibilitySnapshot(): boolean;
634
+ /**
635
+ * Sets up listeners for host→plugin communication via `postMessage` and
636
+ * `CustomEvent` fallback. The host pushes context and visibility updates;
637
+ * this function invokes `mount(ctx)` whenever a new context arrives.
638
+ *
639
+ * Lifecycle contract: the host may replace the context at any time — a
640
+ * pooled iframe document is reused across resources without a reload — so
641
+ * `mount` runs once per context, not once per page load. Clean up
642
+ * per-resource state yourself, or rely on `createPluginRoot` from
643
+ * `@hoardodile/sdk-react`, which remounts the tree by `resId` by
644
+ * default. When the host rebinds the iframe to a new resource, a late
645
+ * unmount cache flush stamped with the old resId is stale-dropped by the
646
+ * host (the plugin's debounced write still lands via the new binding).
647
+ */
648
+ declare function mountPlugin(mount: (ctx: PluginIframeContext) => void): void;
649
+ /** Applies theme classes to `document.documentElement` so CSS variables update. */
650
+ declare function applyTheme(resolvedTheme: string, palette: string, iconStyle: string): void;
651
+ /**
652
+ * Applies the host app font to the iframe document: injects each preset
653
+ * stylesheet once (idempotent per path — they are absolute `/fonts/...`
654
+ * URLs statically served by the host, which the sandboxed iframe can
655
+ * load), then points `--font-app` at the family stack. An empty family
656
+ * means the plugin opted out of font inheritance: the variable is
657
+ * removed and the document falls back to the plugin's own `--font-sans`.
658
+ */
659
+ declare function applyFonts(family: string, cssPaths: readonly string[]): void;
660
+
661
+ /**
662
+ * Extract `{ resolvedTheme, palette, iconStyle }` from an unknown host
663
+ * payload. Malformed input yields `undefined` fields rather than throwing —
664
+ * theme pushes are best-effort.
665
+ */
666
+ declare function extractThemePayload(data: unknown): {
667
+ resolvedTheme: string | undefined;
668
+ palette: string | undefined;
669
+ iconStyle: string | undefined;
670
+ };
671
+ /**
672
+ * Extract the host app font payload (`family` + stylesheet paths) from
673
+ * an unknown host push; `undefined` when the shape does not match.
674
+ */
675
+ declare function extractFontsPayload(data: unknown): PluginFonts | undefined;
676
+ /**
677
+ * Extract a `{ key, value }` pref update from an unknown host push;
678
+ * `undefined` when the shape does not match. `value` may be `undefined`
679
+ * for a removal.
680
+ */
681
+ declare function extractPrefPayload(data: unknown): {
682
+ readonly key: string;
683
+ readonly value: string | undefined;
684
+ } | undefined;
685
+ /**
686
+ * Builds the full {@link WebPluginAPI} for a plugin running inside a sandboxed
687
+ * iframe. Communicates with the host via postMessage.
688
+ */
689
+ declare function createIframeHostAPI<TSchema extends PluginSchema = PluginSchema>(ctx: PluginIframeContext): WebPluginAPI<TSchema>;
690
+
691
+ /**
692
+ * Reset both stores and seed them from the iframe context. Called once
693
+ * by the runtime on mount; must run before any pref/cache access.
694
+ */
695
+ declare function seedPluginStores(ctx: PluginIframeContext): void;
696
+ /** Read-only view of the plugin's in-memory pref store. */
697
+ declare function getPluginPrefStore(): ReadonlyMap<string, string>;
698
+ /** Write a pref locally; mirror it to the host immediately. */
699
+ declare function setPluginPref(key: string, value: string): void;
700
+ /** Read-only view of the plugin's in-memory cache store. */
701
+ declare function getPluginCacheStore(): ReadonlyMap<string, string>;
702
+ /**
703
+ * Write a cache entry locally and mirror it to the host for persistence.
704
+ * Continuously-changing state (scroll positions, resume timestamps)
705
+ * should go through the debounced `useCacheWriter` in
706
+ * `@hoardodile/sdk-react` instead of calling this directly on every
707
+ * change.
708
+ */
709
+ declare function setPluginCache(key: string, value: string): void;
710
+ /**
711
+ * Snapshot of all cache entries, as returned by `api.listCache`. Values
712
+ * are the raw serialized strings stored via {@link setPluginCache}.
713
+ */
714
+ declare function snapshotCacheEntries(): {
715
+ readonly key: string;
716
+ readonly value: string;
717
+ }[];
718
+ /**
719
+ * Subscribe to pref changes for `key` (both local writes and host-pushed
720
+ * updates). Returns an unsubscribe function. Backs the reactive
721
+ * `usePref` hook in `@hoardodile/sdk-react`.
722
+ */
723
+ declare function subscribeToPrefChanges(key: string, cb: () => void): () => void;
724
+ /** Notify all subscribers of `key` about a value change. */
725
+ declare function broadcastPrefChange(key: string): void;
726
+
727
+ export { type Codec, type DeepPartial, type FileUrlVariant, type Host, type HostMessage, type HostPush, type HostPushes, type HostResponse, type InvalidateTarget, type MutationState, PROTOCOL_VERSION, type PluginContextPainted, type PluginFonts, type PluginIframeContext, type PluginMessage, type PluginRequest, type PluginRequests, type PluginResolvedTheme, type PluginResource, type PluginSubscribe, type PluginThemePalette, type QueryState, type ReactivePluginAPI, type RequestInput, type RequestOutput, type StubbedPluginAPI, type Theme, type WebPluginAPI, applyFonts, applyTheme, booleanCodec, broadcastPrefChange, createIframeHostAPI, createWebPluginAPI, ensureHostBridge, extractFontsPayload, extractPrefPayload, extractThemePayload, getPluginCacheStore, getPluginContext, getPluginPrefStore, getVisibilitySnapshot, hostPushKeys, invalidatePushKeys, isRecord, jsonCodec, mountPlugin, numberCodec, pluginMethods, pluginThemePalettes, seedPluginStores, setPluginCache, setPluginPref, snapshotCacheEntries, subscribeToPrefChanges, subscribeToVisibility };