@hoardodile/sdk-types 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.
Files changed (57) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +59 -0
  3. package/dist/image-variant.d.ts +90 -0
  4. package/dist/image-variant.js +115 -0
  5. package/dist/image-variant.js.map +1 -0
  6. package/dist/index.d.ts +772 -0
  7. package/dist/index.js +353 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/manifest-Dk6_xyNy.d.ts +204 -0
  10. package/dist/media-exts.d.ts +92 -0
  11. package/dist/media-exts.js +160 -0
  12. package/dist/media-exts.js.map +1 -0
  13. package/dist/plugin-asset-limits.d.ts +16 -0
  14. package/dist/plugin-asset-limits.js +13 -0
  15. package/dist/plugin-asset-limits.js.map +1 -0
  16. package/dist/plugin-capabilities.d.ts +64 -0
  17. package/dist/plugin-capabilities.js +37 -0
  18. package/dist/plugin-capabilities.js.map +1 -0
  19. package/dist/plugin.d.ts +49 -0
  20. package/dist/plugin.js +12 -0
  21. package/dist/plugin.js.map +1 -0
  22. package/dist/resource.d.ts +26 -0
  23. package/dist/resource.js +9 -0
  24. package/dist/resource.js.map +1 -0
  25. package/dist/result.d.ts +47 -0
  26. package/dist/result.js +20 -0
  27. package/dist/result.js.map +1 -0
  28. package/dist/schema.d.ts +29 -0
  29. package/dist/schema.js +124 -0
  30. package/dist/schema.js.map +1 -0
  31. package/dist/template.d.ts +67 -0
  32. package/dist/template.js +137 -0
  33. package/dist/template.js.map +1 -0
  34. package/dist/text-limits.d.ts +11 -0
  35. package/dist/text-limits.js +7 -0
  36. package/dist/text-limits.js.map +1 -0
  37. package/package.json +102 -0
  38. package/src/file-list.ts +14 -0
  39. package/src/image-variant.test.ts +140 -0
  40. package/src/image-variant.ts +234 -0
  41. package/src/index.ts +115 -0
  42. package/src/manifest.ts +186 -0
  43. package/src/media-exts.ts +245 -0
  44. package/src/plugin-asset-limits.ts +23 -0
  45. package/src/plugin-asset.ts +127 -0
  46. package/src/plugin-capabilities.ts +91 -0
  47. package/src/plugin-definition.test.ts +117 -0
  48. package/src/plugin-definition.ts +902 -0
  49. package/src/plugin.ts +54 -0
  50. package/src/read-range.ts +12 -0
  51. package/src/resource.ts +28 -0
  52. package/src/result.test.ts +64 -0
  53. package/src/result.ts +73 -0
  54. package/src/schema.ts +29 -0
  55. package/src/template.test.ts +116 -0
  56. package/src/template.ts +199 -0
  57. package/src/text-limits.ts +11 -0
@@ -0,0 +1,772 @@
1
+ export { C as CoverKindUi, a as CoverKindUiMap, P as PluginManifest, b as PluginManifestId, c as PluginManifestUi, d as PluginPermissions, S as SearchKind } from './manifest-Dk6_xyNy.js';
2
+ import { PluginAssetErrorName } from './plugin-asset-limits.js';
3
+ import { MediaKind } from './media-exts.js';
4
+ import { Result } from './result.js';
5
+ export { Err, Ok, err, isErr, isOk, matchResult, ok } from './result.js';
6
+ export { AnchorData } from './schema.js';
7
+ import 'zod';
8
+
9
+ /**
10
+ * One item in a serialized file list: a bare filename string or a
11
+ * metadata object. Object entries carry `filename` plus any extra
12
+ * fields; the host renders covers and chips from this shape.
13
+ */
14
+ type SerializedFileEntry = string | Record<string, string | number | boolean>;
15
+ /**
16
+ * Serialized file list as stored in the sidecar cache and sent over the
17
+ * wire. Order is the display order; the host preserves it verbatim.
18
+ */
19
+ type SerializedFileList = readonly SerializedFileEntry[];
20
+
21
+ /**
22
+ * The plugin asset contract — the download / read / delete surface of a
23
+ * plugin's own "vault". The vault is a host-reserved namespace inside the
24
+ * plugin's installed directory (`<plugin-dir>/vault/`) that the host
25
+ * manages on the plugin's behalf: data lands there only through the
26
+ * user-consented download API, and nothing a plugin ships in its zip can
27
+ * ever be overwritten by downloading (see the vault-confined `dest`
28
+ * rules).
29
+ *
30
+ * Both sides of the plugin speak the same shapes: the server-side
31
+ * `ResourceAPI` (main.js hooks) and the iframe `WebPluginAPI` (render)
32
+ * call the same four methods with the same request/result vocabulary.
33
+ * All methods are gated by the manifest `download` permission and
34
+ * denied inside the sandbox when the manifest does not declare it.
35
+ *
36
+ * Error convention (fixed rule): **classification uses `Result`,
37
+ * API calls throw.** `detect` (and other classifiers) return a
38
+ * {@link Result}; every other API method rejects with an `Error` whose
39
+ * `name` carries the machine-readable code. Plugins branch on
40
+ * {@link isPluginAssetError} — never parse messages.
41
+ *
42
+ * Runtime limits live behind `@hoardodile/sdk-types/plugin-asset-limits`;
43
+ * this module exports the types and the error helpers only.
44
+ */
45
+
46
+ /**
47
+ * A download request: the plugin declares the plaintext URL, the vault
48
+ * destination, and (optionally) an integrity pin plus a reason for the
49
+ * consent dialog. `dest` is vault-relative only — it must resolve under
50
+ * `<plugin-dir>/vault/` and can never reach the plugin's own bundled
51
+ * files (`main.js`, `index.html`, `assets/`, ...).
52
+ */
53
+ type PluginDownloadRequest = {
54
+ /** Absolute `http(s)` URL to fetch. Shown verbatim in the consent dialog. */
55
+ readonly url: string;
56
+ /**
57
+ * Vault-relative destination path (`"runtime/live2d.min.js"`). The host
58
+ * resolves it inside the plugin vault and rejects absolute paths,
59
+ * `..` traversal, path separators crossing segments, and reserved
60
+ * names — before any network request is made.
61
+ */
62
+ readonly dest: string;
63
+ /**
64
+ * Optional SRI-style integrity pin (64 lowercase hex chars). When
65
+ * present the host verifies the downloaded bytes against it and
66
+ * discards a mismatch, so a tampered or corrupted response can
67
+ * never be stored.
68
+ */
69
+ readonly sha256?: string;
70
+ /** Optional short rationale shown in the consent dialog (plugin-authored copy). */
71
+ readonly reason?: string;
72
+ };
73
+ /**
74
+ * Result of {@link PluginDownloadRequest}: the stored file's identity.
75
+ * `cached` is true when the destination already existed — the host
76
+ * answered from the vault without any dialog and without touching the
77
+ * network (downloads are "ensure present", never unconditional).
78
+ */
79
+ type PluginDownloadResult = {
80
+ /** The vault-relative destination that was resolved. */
81
+ readonly path: string;
82
+ readonly sizeBytes: number;
83
+ /** sha256 of the stored bytes (host-computed, always present). */
84
+ readonly sha256: string;
85
+ /** True when the file already existed and no consent/network was needed. */
86
+ readonly cached: boolean;
87
+ };
88
+ /**
89
+ * Result of {@link ResourceAPI.deleteAsset} / `WebPluginAPI.deleteAsset`.
90
+ * Deletion is idempotent: removing nothing is not an error.
91
+ */
92
+ type PluginAssetDeleteResult = {
93
+ /** True when a file was actually removed. */
94
+ readonly existed: boolean;
95
+ };
96
+ /**
97
+ * Error thrown by the asset methods. The name survives both wire
98
+ * boundaries (worker IPC and the iframe postMessage bridge), so plugin
99
+ * code can branch on `err.name` without parsing messages:
100
+ *
101
+ * - `DENIED` — the user declined the consent dialog, or consent timed out.
102
+ * - `UNAVAILABLE` — this runtime has no consent channel (CLI, workbench,
103
+ * offline mock) or the server is in read-only archive mode.
104
+ * - `POLICY` — the host rejected the request before downloading:
105
+ * manifest lacks the `download` permission, the URL or `dest` is not
106
+ * allowed, the destination is a directory, or a quota would be
107
+ * exceeded. Also used for reserved-name conflicts.
108
+ *
109
+ * Transport/network failures keep their own error names (e.g. socket
110
+ * errors) and are not part of this vocabulary.
111
+ */
112
+ declare class PluginAssetError extends Error {
113
+ readonly code: PluginAssetErrorName;
114
+ constructor(code: PluginAssetErrorName, message: string);
115
+ }
116
+ /**
117
+ * Narrow an asset error to a machine-readable name. Works across the
118
+ * RPC boundaries because both preserve `Error.name` (the worker IPC and
119
+ * the iframe bridge carry the name explicitly) — the instance is often
120
+ * lost in transit, so the check keys on the name alone.
121
+ */
122
+ declare function isPluginAssetError(err: unknown, name: PluginAssetErrorName): err is PluginAssetError;
123
+ /** Build a `PluginAssetError` carrying the given machine-readable name. */
124
+ declare function pluginAssetError(name: PluginAssetErrorName, message: string): PluginAssetError;
125
+
126
+ /**
127
+ * Byte range used by every file read across the plugin stack — the
128
+ * server-side `ResourceAPI.readFile` (see `@hoardodile/host`) and the
129
+ * browser-side `PluginRequests.readFile` (see `@hoardodile/sdk-web`) —
130
+ * so the contract lives exactly once. `start` is inclusive (default 0),
131
+ * `end` is exclusive (default end of file). Hosts clamp the range to the
132
+ * file size; a range at or past the end resolves to an empty result.
133
+ */
134
+ type ReadFileRange = {
135
+ readonly start?: number;
136
+ readonly end?: number;
137
+ };
138
+
139
+ /**
140
+ * The plugin definition contract — the single source of truth shared by
141
+ * the authoring SDK (`@hoardodile/sdk-server`), the runtime host
142
+ * (`@hoardodile/host`) and the worker sandbox. Everything here is pure
143
+ * TypeScript with no node or DOM dependencies, so the same contract
144
+ * serves browser-facing packages and node runtimes alike.
145
+ */
146
+
147
+ /**
148
+ * Schema contract shared between server and web plugin APIs.
149
+ * Declared once per plugin and used to type both `definePlugin` and
150
+ * `WebPluginAPI`.
151
+ */
152
+ interface PluginSchema {
153
+ readonly file?: unknown;
154
+ readonly sourceMeta?: unknown;
155
+ readonly searchMeta?: unknown;
156
+ /**
157
+ * Plugin-defined payload the `detect` hook may carry on a
158
+ * successful match. The host keeps the last payload and exposes it
159
+ * to the plugin's other hooks as `api.context.detect` — classify
160
+ * once in `detect` instead of rescanning in every hook. Declaring
161
+ * this slot types the context; hooks must still handle the absent
162
+ * case (`undefined`: fresh worker, or detect never matched).
163
+ */
164
+ readonly detect?: unknown;
165
+ /**
166
+ * Plugin-defined anchor location data: the payload carried inside the
167
+ * wire {@link AnchorData} envelope (see {@link anchorData}). Outgoing
168
+ * anchors are typed by this slot and passed raw (e.g.
169
+ * `createMessage({ anchor: { page } })`); incoming anchor data is
170
+ * validated by the plugin's `decodeAnchor` (see `definePluginAPI` in
171
+ * `@hoardodile/sdk-react`).
172
+ */
173
+ readonly anchor?: unknown;
174
+ }
175
+ /**
176
+ * Server plugin detection result: the shared result vocabulary, where a
177
+ * match carries the schema's `detect` payload (when one is declared)
178
+ * and a miss carries its reasons. Plugins may return the literal
179
+ * `{ ok: true } as const` / `{ ok: false, reasons }` shapes directly,
180
+ * or use `ok()`/`err({ reasons })` from the result module.
181
+ *
182
+ * `TPayload` is the plugin's declared `detect` slot — the payload
183
+ * spread onto a match is checked against it at compile time, so a
184
+ * classification that drifts from the schema fails to build.
185
+ */
186
+ type Detection<TPayload extends object = object> = Result<TPayload, {
187
+ readonly reasons: readonly string[];
188
+ }>;
189
+ /** Structured logger scoped to a single plugin. */
190
+ type Logger = {
191
+ info(message: string, data?: Record<string, unknown>): void;
192
+ warn(message: string, data?: Record<string, unknown>): void;
193
+ error(message: string, data?: Record<string, unknown>): void;
194
+ };
195
+ /** Image probe payload. */
196
+ type ImageInfo = {
197
+ readonly width?: number;
198
+ readonly height?: number;
199
+ };
200
+ /** Video probe payload. */
201
+ type VideoInfo = {
202
+ readonly width?: number;
203
+ readonly height?: number;
204
+ readonly durationMs?: number;
205
+ };
206
+ /**
207
+ * Embedded container tags carried by an audio file (ID3, Vorbis
208
+ * comments, MP4 metadata atoms). Every field is optional — untagged
209
+ * files are normal.
210
+ */
211
+ type AudioTags = {
212
+ readonly title?: string;
213
+ readonly artist?: string;
214
+ readonly album?: string;
215
+ };
216
+ /**
217
+ * Embedded artwork carried by an audio file (ID3 APIC, FLAC PICTURE,
218
+ * MP4 `covr`). Its presence is the signal that the host can extract a
219
+ * real cover; the dimensions come from the same probe, so callers can
220
+ * pre-size the cover slot without decoding the picture.
221
+ */
222
+ type AudioCoverArt = {
223
+ readonly width?: number;
224
+ readonly height?: number;
225
+ };
226
+ /**
227
+ * Audio probe payload. Any field can be absent when the container does
228
+ * not report it.
229
+ */
230
+ type AudioInfo = {
231
+ readonly durationMs?: number;
232
+ /** Codec name of the first audio stream, e.g. `"mp3"`, `"flac"`. */
233
+ readonly codec?: string;
234
+ /** Container bit rate in bits per second. */
235
+ readonly bitRate?: number;
236
+ /** Sample rate of the first audio stream, in Hz. */
237
+ readonly sampleRate?: number;
238
+ /** Channel count of the first audio stream. */
239
+ readonly channels?: number;
240
+ /** Present only when the file embeds artwork. */
241
+ readonly coverArt?: AudioCoverArt;
242
+ readonly tags?: AudioTags;
243
+ };
244
+ /**
245
+ * What a file's bytes say it is. Produced by {@link ResourceAPI.sniff}.
246
+ *
247
+ * `source` records who answered: `"magic"` means the file's own
248
+ * signature was recognized (authoritative), `"extension"` means the
249
+ * content carried no signature and the filename was used instead — the
250
+ * normal outcome for text-based formats, which have no magic bytes.
251
+ *
252
+ * `kind` is provisional for container formats that can hold either
253
+ * audio or video (Ogg, Matroska, ISO-BMFF); {@link ResourceAPI.probe}
254
+ * overrides it with the stream layout actually found in the file.
255
+ */
256
+ type FileType = {
257
+ /** Canonical MIME type, e.g. `"image/jpeg"`. */
258
+ readonly mime: string;
259
+ /** Canonical extension for {@link mime}, with leading dot. */
260
+ readonly ext: string;
261
+ readonly kind: MediaKind;
262
+ readonly source: "magic" | "extension";
263
+ };
264
+ /**
265
+ * Everything one media probe pass can say about a file, discriminated
266
+ * by the family the content really belongs to — the shape every
267
+ * mainstream prober uses (ffprobe's `format` + `streams`, sharp's
268
+ * `metadata()`, Tika's `MediaType`).
269
+ *
270
+ * `other` is a successful answer: the file was identified and is not
271
+ * decodable media (text, documents, archives). `unknown` is the failure
272
+ * branch and always carries a reason, so "this host has no probe
273
+ * backend" is never confused with "this file is not an image":
274
+ *
275
+ * - `unsupported` — identified, but no backend decodes this format
276
+ * - `unavailable` — the host wired no probe implementation (raw
277
+ * directory APIs and test fixtures)
278
+ * - `failed` — a backend ran and could not decode the bytes
279
+ */
280
+ type ProbeResult = ({
281
+ readonly kind: "image";
282
+ readonly mime: string;
283
+ /** Multi-frame source: animated GIF / WebP / APNG / AVIF. */
284
+ readonly animated: boolean;
285
+ } & ImageInfo) | ({
286
+ readonly kind: "video";
287
+ readonly mime: string;
288
+ } & VideoInfo) | ({
289
+ readonly kind: "audio";
290
+ readonly mime: string;
291
+ } & AudioInfo) | {
292
+ readonly kind: "other";
293
+ readonly mime: string;
294
+ } | {
295
+ readonly kind: "unknown";
296
+ readonly reason: "unsupported" | "unavailable" | "failed";
297
+ };
298
+ /**
299
+ * Perceptual hash kinds the host can compute for an image file.
300
+ * `dhash` (difference hash) and `phash` (DCT-based perceptual hash)
301
+ * are 64-bit similarity hashes compared by Hamming distance;
302
+ * `sha256` is an exact byte hash. Animated images hash their first
303
+ * frame. Plugins decide which kinds to request and which files to
304
+ * hash — the host only provides the computation.
305
+ */
306
+ declare const IMAGE_HASH_KINDS: readonly ["sha256", "dhash", "phash"];
307
+ type ImageHashKind = (typeof IMAGE_HASH_KINDS)[number];
308
+ /**
309
+ * One content hash of a resource file, produced by the plugin's
310
+ * `imageHashes` hook. `scope` is the archive-relative file path,
311
+ * `type` the hash kind (`sha256`/`dhash`/`phash` or a plugin-defined
312
+ * extension), `value` the lowercase hex digest. A resource may expose
313
+ * several hashes (per file × per kind) or none.
314
+ */
315
+ type ImageHash = {
316
+ readonly scope: string;
317
+ readonly type: string;
318
+ readonly value: string;
319
+ /** Bit length of the hash; required for perceptual kinds. */
320
+ readonly bits?: number;
321
+ };
322
+ /** Result of the `imageHashes` hook: hashes per file, possibly empty. */
323
+ type ImageHashesResult = {
324
+ readonly hashes: readonly ImageHash[];
325
+ };
326
+ /**
327
+ * One file inside a container entry (zip/tar) as listed (or extracted)
328
+ * by the plugin API. `path` is the entry's path inside the archive;
329
+ * dimensions are present when the host probed the entry (image
330
+ * backends) — a listing-only result carries no dimensions.
331
+ */
332
+ type ArchiveExtractionEntry = {
333
+ readonly path: string;
334
+ readonly sizeBytes: number;
335
+ readonly kind: MediaKind;
336
+ readonly width?: number;
337
+ readonly height?: number;
338
+ readonly animated?: boolean;
339
+ };
340
+ /**
341
+ * A container listing without materialization — the cheap counterpart of
342
+ * {@link ResourceAPI.extractArchive}. Carries entry names, sizes and
343
+ * kinds only; no dimensions (probing those requires the bytes).
344
+ */
345
+ type ContainerListing = {
346
+ readonly entries: readonly ArchiveExtractionEntry[];
347
+ };
348
+ /**
349
+ * Result of {@link ResourceAPI.extractArchive}: the materialized
350
+ * entries of a container entry. A completed extraction is marked by the
351
+ * host's `index.json` manifest; extraction always writes the cache (the
352
+ * host's `local/cache` is derived data, writable in every view mode).
353
+ */
354
+ type ArchiveExtraction = {
355
+ readonly entries: readonly ArchiveExtractionEntry[];
356
+ };
357
+ /**
358
+ * Resource-scoped API available to every plugin hook. All paths are
359
+ * relative to the resource's source directory; the host resolves
360
+ * absolute paths transparently.
361
+ *
362
+ * `TSchema` types the injected session context (`context.detect`); the
363
+ * default keeps the API compatible with code that never reads it.
364
+ */
365
+ type ResourceAPI<TSchema extends PluginSchema = PluginSchema> = {
366
+ /** Write an informational log entry. */
367
+ readonly logInfo: (message: string, data?: Record<string, unknown>) => void;
368
+ /** Write a warning log entry. */
369
+ readonly logWarn: (message: string, data?: Record<string, unknown>) => void;
370
+ /** Write an error log entry. */
371
+ readonly logError: (message: string, data?: Record<string, unknown>) => void;
372
+ /**
373
+ * List all regular-file names (flat list), in canonical display
374
+ * order: the resource's explicit upload order when one exists (the
375
+ * host's `.order` manifest), the natural name sort otherwise.
376
+ * Plugins that need their own ordering should sort explicitly.
377
+ *
378
+ * This is the raw name list — the `listFiles` hook of the plugin
379
+ * definition turns it into typed file entries.
380
+ */
381
+ readonly listFileNames: () => Promise<readonly string[]>;
382
+ /**
383
+ * Read a regular file relative to the resource root.
384
+ *
385
+ * Without `range` the whole file is returned; hosts may reject
386
+ * oversized full reads — pass a range (or use `readFileChunks` from
387
+ * `@hoardodile/sdk-server/helpers`) for large files.
388
+ *
389
+ * Container addressing: a path of the form `outer!inner` reads the
390
+ * file *inside* a zip/tar entry (e.g. `manga.cbz!Chapter 1/001.jpg`)
391
+ * — the host streams the decompressed bytes. When `outer` is not a
392
+ * container, or the inner entry is absent, the whole path is treated
393
+ * as a literal filename.
394
+ */
395
+ readonly readFile: (path: string, range?: ReadFileRange) => Promise<Uint8Array>;
396
+ /**
397
+ * Return the byte size of `path` without reading the file contents.
398
+ * Resolves to `undefined` when the file does not exist or the artifact
399
+ * is not yet committed. Supports container addressing (`outer!inner`).
400
+ */
401
+ readonly statFile: (path: string) => Promise<{
402
+ readonly sizeBytes: number;
403
+ } | undefined>;
404
+ /**
405
+ * Batch {@link statFile}: resolves every path in one host round-trip
406
+ * (positions preserved). Prefer this over a per-file fan-out of
407
+ * `statFile` when statting a whole archive — one RPC instead of N.
408
+ */
409
+ readonly statFiles: (paths: readonly string[]) => Promise<readonly ({
410
+ readonly sizeBytes: number;
411
+ } | undefined)[]>;
412
+ /**
413
+ * Identify the file at `path` from its content: magic-byte
414
+ * detection, falling back to the extension only for formats that
415
+ * carry no signature (text, subtitles). Resolves to `undefined` when
416
+ * neither can name the file. Supports container addressing.
417
+ *
418
+ * This is the cheap call — it reads a small header window, never
419
+ * decodes. Use it to route work; use {@link probe} when you need
420
+ * dimensions, duration or stream details.
421
+ */
422
+ readonly sniff: (path: string) => Promise<FileType | undefined>;
423
+ /**
424
+ * Decode the media metadata of `path` in one pass, routed by
425
+ * {@link sniff} rather than by the filename: images resolve through
426
+ * sharp, audio and video through ffprobe (which also settles
427
+ * ambiguous containers — an `.ogg` holding only audio streams comes
428
+ * back as `kind: "audio"`). Supports container addressing.
429
+ *
430
+ * Always resolves, never rejects. Non-media files answer
431
+ * `{ kind: "other" }`; the `unknown` branch carries a `reason` that
432
+ * distinguishes "no backend wired" (`unavailable`, what raw
433
+ * directory APIs and fixtures return) from a real decode failure.
434
+ */
435
+ readonly probe: (path: string) => Promise<ProbeResult>;
436
+ /**
437
+ * Stream-hash the file at `path` (any file kind). Rejects when the
438
+ * file is missing or the read fails; the host streams the entry so
439
+ * arbitrarily large files are safe. Supports container addressing.
440
+ */
441
+ readonly hashBytes: (path: string, algo: "md5" | "sha256") => Promise<string>;
442
+ /**
443
+ * Compute the requested hashes of the image at `path` in one pass:
444
+ * `sha256` from the raw bytes, `dhash`/`phash` from a decoded
445
+ * grayscale rendition (animated images use their first frame).
446
+ * Resolves to `undefined` when the file is not a decodable image;
447
+ * `kinds` names a subset of {@link IMAGE_HASH_KINDS} and the result
448
+ * carries exactly those keys. Supports container addressing.
449
+ */
450
+ readonly computeImageHashes: (path: string, kinds: readonly ImageHashKind[]) => Promise<Readonly<Record<ImageHashKind, string>> | undefined>;
451
+ /**
452
+ * List the file entries of a container entry (zip/tar) without
453
+ * materializing anything — the cheap call for metadata-only needs
454
+ * (detect, card counts). Rejects when `filename` is not a supported
455
+ * container.
456
+ */
457
+ readonly listContainer: (filename: string) => Promise<ContainerListing>;
458
+ /**
459
+ * Materialize the contents of a container entry (zip/tar) into the
460
+ * host's extraction cache so the browser can serve the inner files
461
+ * over plain URLs. `filename` is a literal container entry — the
462
+ * cache holds one directory per archive with the inner paths
463
+ * preserved, plus a completion manifest.
464
+ *
465
+ * Idempotent: an already-materialized archive re-lists from the
466
+ * manifest without re-extracting. Rejects when the entry is not a
467
+ * supported container, exceeds the host's byte/entry budgets, or
468
+ * when this host wires no extraction cache (test fixtures, raw
469
+ * directory APIs).
470
+ */
471
+ readonly extractArchive: (filename: string) => Promise<ArchiveExtraction>;
472
+ /**
473
+ * Ensure a remote asset exists in the plugin's own vault: when
474
+ * `dest` is already present the host answers `cached: true` without
475
+ * any dialog and without touching the network; otherwise the host
476
+ * asks the user (the web app shows the consent dialog with the URL
477
+ * verbatim) and downloads on approval. The file always lands inside
478
+ * `<plugin-dir>/vault/` — `dest` is vault-relative and can never
479
+ * reach the plugin's bundled files.
480
+ *
481
+ * Gated by the manifest `download` permission; rejections carry a
482
+ * machine-readable {@link PluginAssetErrorName} in `err.name`
483
+ * (`DENIED` / `UNAVAILABLE` / `POLICY`).
484
+ */
485
+ readonly download: (request: PluginDownloadRequest) => Promise<PluginDownloadResult>;
486
+ /**
487
+ * Byte size of a vault file, or `undefined` when absent. The cheap
488
+ * presence check on top of which `download` resolves cached hits.
489
+ */
490
+ readonly statAsset: (path: string) => Promise<{
491
+ readonly sizeBytes: number;
492
+ } | undefined>;
493
+ /** Read a vault file's bytes (bounded by the same cap as {@link readFile}). */
494
+ readonly readAsset: (path: string) => Promise<Uint8Array>;
495
+ /**
496
+ * Remove a vault file; idempotent (absent files answer
497
+ * `{ existed: false }`). The plugin decides the vault's own
498
+ * lifecycle — e.g. cleaning stale layouts after a plugin update.
499
+ * No user consent is required: nothing leaves the host. Directories
500
+ * and paths outside the vault are rejected (`POLICY`).
501
+ */
502
+ readonly deleteAsset: (path: string) => Promise<PluginAssetDeleteResult>;
503
+ /**
504
+ * Session context injected by the host. `detect` carries the payload
505
+ * the plugin's `detect` hook returned on its last successful match
506
+ * (worker-session scope): the one-pass classification every other
507
+ * hook can build on. `undefined` when detect has not matched in this
508
+ * session — a fresh worker — so hooks must always handle the absent
509
+ * case by re-deriving.
510
+ */
511
+ readonly context: {
512
+ readonly detect: TSchema["detect"] | undefined;
513
+ };
514
+ };
515
+ /**
516
+ * Declarative description of a content plugin. Plugins export an instance
517
+ * of this shape as their default export; the host injects the resource
518
+ * API at call time and never invokes a factory function.
519
+ */
520
+ type PluginDefinition<TSchema extends PluginSchema = PluginSchema> = {
521
+ /**
522
+ * Detect whether this plugin applies to the current resource. A
523
+ * successful match may carry a payload — `ok({ ...shape })` — which
524
+ * the host keeps and exposes to the other hooks as
525
+ * `api.context.detect`. The payload is checked against the schema's
526
+ * `detect` slot (when one is declared).
527
+ */
528
+ readonly detect: (api: ResourceAPI<TSchema>) => Promise<Detection<TSchema["detect"] & object>>;
529
+ /** Optional source metadata builder. */
530
+ readonly sourceMeta?: (api: ResourceAPI<TSchema>) => Promise<TSchema["sourceMeta"] | undefined>;
531
+ /** Optional search metadata builder. */
532
+ readonly searchMeta?: (api: ResourceAPI<TSchema>) => Promise<TSchema["searchMeta"] | undefined>;
533
+ /** Optional local cover source resolver. */
534
+ readonly coverLocal?: (api: ResourceAPI<TSchema>) => Promise<string | undefined>;
535
+ /**
536
+ * Optional custom file list builder. Results are cached verbatim in a
537
+ * sidecar. When absent the host falls back to a bare list of source
538
+ * filenames.
539
+ */
540
+ readonly listFiles?: (api: ResourceAPI<TSchema>) => Promise<readonly TSchema["file"][]>;
541
+ /**
542
+ * Optional content hashes for duplicate detection and image
543
+ * similarity. The plugin decides the policy — which files to hash
544
+ * and which kinds — by calling the API's hash primitives; a plugin
545
+ * facing image-less resources simply omits this hook. Returning
546
+ * `undefined` (or a hook error) keeps the resource's hash rows empty.
547
+ */
548
+ readonly imageHashes?: (api: ResourceAPI<TSchema>) => Promise<ImageHashesResult | undefined>;
549
+ };
550
+ /** Plugin hook names the host can invoke, in contract order. */
551
+ declare const HOOK_NAMES: readonly ["detect", "sourceMeta", "searchMeta", "coverLocal", "listFiles", "imageHashes"];
552
+ type HookName = (typeof HOOK_NAMES)[number];
553
+ /**
554
+ * Freeze and return a plugin definition. Runs shape validation upfront so
555
+ * a malformed plugin fails at load time with a friendly message instead
556
+ * of misbehaving at hook time.
557
+ */
558
+ declare function definePlugin<TSchema extends PluginSchema = PluginSchema>(definition: PluginDefinition<TSchema>): PluginDefinition<TSchema>;
559
+ /**
560
+ * Validate that a value satisfies the structural contract of a
561
+ * {@link PluginDefinition}: only known hooks, all hooks async functions,
562
+ * `detect` required. Does NOT exercise behaviour.
563
+ */
564
+ declare function assertPluginShape(value: unknown): asserts value is PluginDefinition;
565
+ /**
566
+ * Convenience wrapper that builds a failing plugin definition. Used by
567
+ * the host when a plugin directory is missing or its main.js cannot be
568
+ * loaded.
569
+ */
570
+ declare function createFailingPlugin(reasons: readonly string[]): PluginDefinition;
571
+ /** Type guard for the success branch of a {@link Detection}. */
572
+ declare function isDetected(detection: Detection): detection is {
573
+ readonly ok: true;
574
+ };
575
+ /** Type guard for the failure branch of a {@link Detection}. */
576
+ declare function isMissed(detection: Detection): detection is {
577
+ readonly ok: false;
578
+ readonly reasons: readonly string[];
579
+ };
580
+ /** Declarative configuration for a {@link ResourceAPI} fixture. */
581
+ type ResourceAPIFixtureConfig<TSchema extends PluginSchema = PluginSchema> = {
582
+ /** File names returned by `listFileNames`. */
583
+ readonly files?: readonly string[];
584
+ /** File contents returned by `readFile`. */
585
+ readonly contents?: Readonly<Record<string, string | Uint8Array>>;
586
+ /**
587
+ * `sniff` results keyed by file path — `{ "a.jpg": … }` matches only
588
+ * that file, keys starting with a dot match by extension suffix
589
+ * (`{ ".mp4": … }` applies to every .mp4 file, longest key wins),
590
+ * and `{ "": … }` matches every path (the usual way to express a
591
+ * default). Unconfigured paths fall back to the extension table,
592
+ * exactly like the host's extension branch.
593
+ */
594
+ readonly types?: Readonly<Record<string, FileType | undefined>>;
595
+ /**
596
+ * `probe` results keyed by file path (same matching rules as
597
+ * {@link types}). Unconfigured paths mirror the host's routing:
598
+ * identified non-media answers `{ kind: "other" }`, identified media
599
+ * answers `{ kind: "unknown", reason: "unavailable" }` — the fixture
600
+ * decodes nothing, so a hook that needs real dimensions belongs in a
601
+ * sandbox test instead.
602
+ */
603
+ readonly probes?: Readonly<Record<string, ProbeResult | undefined>>;
604
+ /** Stat results. A plain value is used as the default for all paths. */
605
+ readonly stats?: Readonly<Record<string, {
606
+ readonly sizeBytes: number;
607
+ } | undefined>> | {
608
+ readonly sizeBytes: number;
609
+ } | undefined;
610
+ /** `hashBytes` results by path; a plain string is used for all paths. */
611
+ readonly byteHashes?: Readonly<Record<string, string>> | string;
612
+ /**
613
+ * `computeImageHashes` results by path. A plain record is used as the
614
+ * default for all paths; absent paths resolve to `undefined`.
615
+ */
616
+ readonly imageHashes?: Readonly<Record<string, ImageHashesResult>> | ImageHashesResult;
617
+ /**
618
+ * `listContainer` results keyed by the archive filename. Absent
619
+ * names reject, mirroring the host's "not a supported archive" error.
620
+ */
621
+ readonly containerListings?: Readonly<Record<string, ContainerListing>>;
622
+ /**
623
+ * `extractArchive` results keyed by the archive filename. Absent
624
+ * names reject, mirroring the host's "not a supported archive" error.
625
+ */
626
+ readonly extractions?: Readonly<Record<string, ArchiveExtraction>>;
627
+ /**
628
+ * Vault file contents keyed by vault-relative path, backing
629
+ * `statAsset` / `readAsset` / `deleteAsset` in the fixture.
630
+ */
631
+ readonly assetFiles?: Readonly<Record<string, string | Uint8Array>>;
632
+ /**
633
+ * Handler for `download`. Absent means the hosted runtime has no
634
+ * consent channel — `download` rejects with `UNAVAILABLE`, exactly
635
+ * like the CLI, workbench and offline mock hosts.
636
+ */
637
+ readonly downloadHandler?: (request: PluginDownloadRequest) => Promise<PluginDownloadResult>;
638
+ /**
639
+ * Container addressing for the fixture: maps a virtual path
640
+ * (`outer!inner`) to stat/sniff/probe results, so hooks that browse
641
+ * inside archives can be tested without real archives. Matching rules
642
+ * mirror {@link types}: exact path keys, dot fragments by suffix,
643
+ * `{ "": … }` as the default.
644
+ */
645
+ readonly virtualEntries?: Readonly<Record<string, ArchiveExtractionEntry>>;
646
+ /**
647
+ * Session context handed to hooks as `api.context` — mirrors the
648
+ * host injecting the payload of a prior successful `detect`. Typed
649
+ * by the schema generic when one is supplied.
650
+ */
651
+ readonly context?: {
652
+ readonly detect?: TSchema["detect"];
653
+ };
654
+ };
655
+ /**
656
+ * Identify a file from its name alone: the extension branch of content
657
+ * sniffing, exposed on its own because it is also the honest answer for
658
+ * formats that carry no signature, and the shape test doubles need when
659
+ * standing in for a real {@link ResourceAPI}.
660
+ */
661
+ declare function fileTypeFromName(path: string): FileType | undefined;
662
+ /**
663
+ * Create a mutable {@link ResourceAPI} fixture driven by a declarative
664
+ * config. No filesystem involved — the standard way to unit-test plugin
665
+ * hooks.
666
+ *
667
+ * Pass the plugin's schema as the generic
668
+ * (`createResourceAPIFixture<MySchema>()`) so the returned api carries
669
+ * the typed session context — the same shape schema-typed hooks receive
670
+ * from the host.
671
+ */
672
+ declare function createResourceAPIFixture<TSchema extends PluginSchema = PluginSchema>(initialConfig?: ResourceAPIFixtureConfig<TSchema>): {
673
+ readonly api: ResourceAPI<TSchema>;
674
+ readonly setConfig: (next: ResourceAPIFixtureConfig<TSchema>) => void;
675
+ };
676
+ /** Return a minimal {@link Logger} for tests. */
677
+ declare function stubLogger(overrides?: Partial<Logger>): Logger;
678
+
679
+ /**
680
+ * @hoardodile/sdk-types — the plugin contract: manifest schema, the
681
+ * plugin definition contract (`PluginDefinition`/`ResourceAPI`/
682
+ * `definePlugin`/fixtures), and the shared message/danmaku/anchor wire
683
+ * shapes. Single source of truth consumed by every SDK package, the
684
+ * host, and the app; nothing here touches the DOM or node.
685
+ *
686
+ * This package is the contract — plugins normally import it for types
687
+ * (`PluginSchema`, `PluginManifest`, ...) and (via
688
+ * `@hoardodile/sdk-server`) the `definePlugin` factory. The zod
689
+ * runtime validators (`pluginManifest`, `anchorData`) live behind the
690
+ * `@hoardodile/sdk-types/schema` subpath so plugin bundles never pull
691
+ * zod.
692
+ *
693
+ * Plugin-facing constants (data, plus the pure lookups that read it)
694
+ * live in subpaths mirroring their source files — there is no root
695
+ * export for them:
696
+ *
697
+ * - `@hoardodile/sdk-types/image-variant` — derived-image variant
698
+ * contract: spec types, query parsing/encoding, canonical cache
699
+ * identity
700
+ * - `@hoardodile/sdk-types/media-exts` — media-type tables: extension
701
+ * sets, extension ↔ MIME, MIME ↔ media kind
702
+ * - `@hoardodile/sdk-types/plugin` — plugin runtime limits (read cap,
703
+ * probe/stat fan-out bounds)
704
+ * - `@hoardodile/sdk-types/resource` — resource caps (search-meta version,
705
+ * preview eligibility)
706
+ * - `@hoardodile/sdk-types/template` — the host cover/message template
707
+ * grammar (fragment splitter, tokeniser, parser — shared with the
708
+ * web renderer and the CLI's build-time lint)
709
+ * - `@hoardodile/sdk-types/text-limits` — plugin input limits (danmaku
710
+ * body, comment body)
711
+ *
712
+ * App-only constants live in `@hoardodile/shared` (infra limits) and
713
+ * `@hoardodile/schemas` (field lengths) instead.
714
+ */
715
+
716
+ /** Web plugin danmaku mode. */
717
+ type DanmakuMode = "scroll" | "top" | "bottom";
718
+ /**
719
+ * Client-side danmaku list filter: every entry is matched by strict
720
+ * equality against the same key in the danmaku's anchor `data`. Keys are
721
+ * plugin-defined vocabulary (e.g. `{ kind: "videoTime", filename }` in
722
+ * an official content plugin) — the SDK only defines the matching semantics,
723
+ * not which keys exist.
724
+ */
725
+ type DanmakuListFilter = Readonly<Record<string, string | number | boolean>>;
726
+ /**
727
+ * Anchor as returned in `Message` / `Danmaku` API shapes: the resource
728
+ * the anchor points into plus the plugin location payload. The server
729
+ * derives `resId` from the row's own `anchor_resource_id` column —
730
+ * plugin code never supplies it (see `anchorData` in
731
+ * `@hoardodile/sdk-types/schema`).
732
+ */
733
+ type ResAnchor = {
734
+ readonly resId: string;
735
+ readonly data?: unknown;
736
+ };
737
+ /** Web plugin message shape. */
738
+ type Message = {
739
+ readonly id: string;
740
+ readonly parentId?: string;
741
+ readonly body: string;
742
+ readonly createdAt: number;
743
+ readonly deletedAt?: number;
744
+ readonly charIds: readonly string[];
745
+ readonly resIds: readonly string[];
746
+ readonly likeCount: number;
747
+ readonly dislikeCount: number;
748
+ readonly replyCount: number;
749
+ readonly floor?: number;
750
+ readonly anchor?: ResAnchor;
751
+ };
752
+ /** Web plugin danmaku shape. */
753
+ type Danmaku = {
754
+ readonly id: string;
755
+ readonly anchor: ResAnchor;
756
+ readonly text: string;
757
+ readonly color: string;
758
+ readonly mode: DanmakuMode;
759
+ readonly createdAt: number;
760
+ };
761
+ /** Plugin-facing file stats slice of a resource. */
762
+ type FileStats = {
763
+ readonly sizeBytes?: number;
764
+ readonly count?: number;
765
+ };
766
+ /** Plugin-produced search metadata. The host enforces its own schema at ingestion time. */
767
+ type SearchMeta = {
768
+ readonly v: number;
769
+ readonly facets?: Readonly<Record<string, boolean>>;
770
+ };
771
+
772
+ export { type ArchiveExtraction, type ArchiveExtractionEntry, type AudioCoverArt, type AudioInfo, type AudioTags, type ContainerListing, type Danmaku, type DanmakuListFilter, type DanmakuMode, type Detection, type FileStats, type FileType, HOOK_NAMES, type HookName, IMAGE_HASH_KINDS, type ImageHash, type ImageHashKind, type ImageHashesResult, type ImageInfo, type Logger, MediaKind, type Message, type PluginAssetDeleteResult, PluginAssetError, PluginAssetErrorName, type PluginDefinition, type PluginDownloadRequest, type PluginDownloadResult, type PluginSchema, type ProbeResult, type ReadFileRange, type ResAnchor, type ResourceAPI, type ResourceAPIFixtureConfig, Result, type SearchMeta, type SerializedFileEntry, type SerializedFileList, type VideoInfo, assertPluginShape, createFailingPlugin, createResourceAPIFixture, definePlugin, fileTypeFromName, isDetected, isMissed, isPluginAssetError, pluginAssetError, stubLogger };