@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.
- package/LICENSE +18 -0
- package/README.md +47 -0
- package/dist/index.d.ts +727 -0
- package/dist/index.js +683 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
- package/src/bridge.ts +178 -0
- package/src/codecs.ts +54 -0
- package/src/fixtures.ts +154 -0
- package/src/index.ts +92 -0
- package/src/lifecycle.ts +200 -0
- package/src/protocol.ts +448 -0
- package/src/runtime.test.ts +63 -0
- package/src/runtime.ts +279 -0
- package/src/stores.ts +103 -0
- package/src/types.ts +252 -0
- package/src/urls.ts +66 -0
package/dist/index.d.ts
ADDED
|
@@ -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 };
|