@hoardodile/sdk-types 0.1.0 → 0.1.2
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/dist/index.d.ts +24 -11
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/{manifest-Dk6_xyNy.d.ts → manifest-JMXWSfKE.d.ts} +16 -1
- package/dist/plugin-asset-limits.d.ts +7 -1
- package/dist/plugin-asset-limits.js +2 -1
- package/dist/plugin-asset-limits.js.map +1 -1
- package/dist/plugin-capabilities.d.ts +2 -2
- package/dist/plugin-capabilities.js +1 -1
- package/dist/plugin-capabilities.js.map +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +15 -3
- package/dist/schema.js.map +1 -1
- package/package.json +1 -1
- package/src/manifest.ts +41 -5
- package/src/plugin-asset-limits.ts +7 -0
- package/src/plugin-asset.ts +3 -0
- package/src/plugin-capabilities.ts +1 -1
- package/src/plugin-definition.ts +36 -15
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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-
|
|
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-JMXWSfKE.js';
|
|
2
2
|
import { PluginAssetErrorName } from './plugin-asset-limits.js';
|
|
3
3
|
import { MediaKind } from './media-exts.js';
|
|
4
4
|
import { Result } from './result.js';
|
|
@@ -30,6 +30,9 @@ type SerializedFileList = readonly SerializedFileEntry[];
|
|
|
30
30
|
* Both sides of the plugin speak the same shapes: the server-side
|
|
31
31
|
* `ResourceAPI` (main.js hooks) and the iframe `WebPluginAPI` (render)
|
|
32
32
|
* call the same four methods with the same request/result vocabulary.
|
|
33
|
+
* `download` also accepts an **array of requests** — one batched call is
|
|
34
|
+
* ONE consent question (the dialog lists every item) and is all-or-nothing
|
|
35
|
+
* (results arrive in request order; any failure commits nothing).
|
|
33
36
|
* All methods are gated by the manifest `download` permission and
|
|
34
37
|
* denied inside the sandbox when the manifest does not declare it.
|
|
35
38
|
*
|
|
@@ -470,11 +473,19 @@ type ResourceAPI<TSchema extends PluginSchema = PluginSchema> = {
|
|
|
470
473
|
*/
|
|
471
474
|
readonly extractArchive: (filename: string) => Promise<ArchiveExtraction>;
|
|
472
475
|
/**
|
|
473
|
-
* Ensure
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
476
|
+
* Ensure remote assets exist in the plugin's own vault — one call with
|
|
477
|
+
* one request, or one call with an array of requests. When `dest` is
|
|
478
|
+
* already present the host answers `cached: true` without any dialog
|
|
479
|
+
* and without touching the network; otherwise the host asks the user
|
|
480
|
+
* (the web app shows the shared consent dialog with the URLs
|
|
481
|
+
* verbatim) and downloads on approval.
|
|
482
|
+
*
|
|
483
|
+
* An array is ONE consent question for the WHOLE batch (the dialog
|
|
484
|
+
* lists every item) and is all-or-nothing: any failure discards every
|
|
485
|
+
* staged file and rejects with the first error, so nothing is
|
|
486
|
+
* partially committed. Results arrive in request order with `cached`
|
|
487
|
+
* items keeping their positions. Cap: {@link PLUGIN_ASSET_BATCH_MAX_ITEMS}
|
|
488
|
+
* items per call. The file always lands inside
|
|
478
489
|
* `<plugin-dir>/vault/` — `dest` is vault-relative and can never
|
|
479
490
|
* reach the plugin's bundled files.
|
|
480
491
|
*
|
|
@@ -482,7 +493,7 @@ type ResourceAPI<TSchema extends PluginSchema = PluginSchema> = {
|
|
|
482
493
|
* machine-readable {@link PluginAssetErrorName} in `err.name`
|
|
483
494
|
* (`DENIED` / `UNAVAILABLE` / `POLICY`).
|
|
484
495
|
*/
|
|
485
|
-
readonly download: (request: PluginDownloadRequest) => Promise<PluginDownloadResult
|
|
496
|
+
readonly download: ((request: PluginDownloadRequest) => Promise<PluginDownloadResult>) & ((requests: readonly PluginDownloadRequest[]) => Promise<readonly PluginDownloadResult[]>);
|
|
486
497
|
/**
|
|
487
498
|
* Byte size of a vault file, or `undefined` when absent. The cheap
|
|
488
499
|
* presence check on top of which `download` resolves cached hits.
|
|
@@ -630,11 +641,13 @@ type ResourceAPIFixtureConfig<TSchema extends PluginSchema = PluginSchema> = {
|
|
|
630
641
|
*/
|
|
631
642
|
readonly assetFiles?: Readonly<Record<string, string | Uint8Array>>;
|
|
632
643
|
/**
|
|
633
|
-
* Handler for `download
|
|
634
|
-
*
|
|
635
|
-
*
|
|
644
|
+
* Handler for `download` (single request or batch of requests, typed
|
|
645
|
+
* as the union — the fixture returns the matching shape). Absent
|
|
646
|
+
* means the hosted runtime has no consent channel — `download`
|
|
647
|
+
* rejects with `UNAVAILABLE`, exactly like the CLI, workbench and
|
|
648
|
+
* offline mock hosts.
|
|
636
649
|
*/
|
|
637
|
-
readonly downloadHandler?: (request: PluginDownloadRequest) => Promise<PluginDownloadResult>;
|
|
650
|
+
readonly downloadHandler?: (request: PluginDownloadRequest | readonly PluginDownloadRequest[]) => Promise<PluginDownloadResult | readonly PluginDownloadResult[]>;
|
|
638
651
|
/**
|
|
639
652
|
* Container addressing for the fixture: maps a virtual path
|
|
640
653
|
* (`outer!inner`) to stat/sniff/probe results, so hooks that browse
|
package/dist/index.js
CHANGED
|
@@ -280,15 +280,19 @@ function createResourceAPIFixture(initialConfig = {}) {
|
|
|
280
280
|
}
|
|
281
281
|
return configured;
|
|
282
282
|
},
|
|
283
|
-
async
|
|
283
|
+
download: (async (request) => {
|
|
284
284
|
if (config.downloadHandler === void 0) {
|
|
285
285
|
throw pluginAssetError(
|
|
286
286
|
"UNAVAILABLE",
|
|
287
287
|
"ResourceAPIFixture: no download handler configured"
|
|
288
288
|
);
|
|
289
289
|
}
|
|
290
|
-
|
|
291
|
-
|
|
290
|
+
const result = await config.downloadHandler(request);
|
|
291
|
+
if (Array.isArray(request)) {
|
|
292
|
+
return Array.isArray(result) ? result : [result];
|
|
293
|
+
}
|
|
294
|
+
return Array.isArray(result) ? result[0] ?? result : result;
|
|
295
|
+
}),
|
|
292
296
|
async statAsset(path) {
|
|
293
297
|
const content = resolveValue(path, config.assetFiles, void 0);
|
|
294
298
|
if (content === void 0) return void 0;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/plugin-asset.ts","../src/media-exts.ts","../src/plugin-definition.ts","../src/result.ts"],"names":["err"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * The plugin asset contract — the download / read / delete surface of a\n * plugin's own \"vault\". The vault is a host-reserved namespace inside the\n * plugin's installed directory (`<plugin-dir>/vault/`) that the host\n * manages on the plugin's behalf: data lands there only through the\n * user-consented download API, and nothing a plugin ships in its zip can\n * ever be overwritten by downloading (see the vault-confined `dest`\n * rules).\n *\n * Both sides of the plugin speak the same shapes: the server-side\n * `ResourceAPI` (main.js hooks) and the iframe `WebPluginAPI` (render)\n * call the same four methods with the same request/result vocabulary.\n * All methods are gated by the manifest `download` permission and\n * denied inside the sandbox when the manifest does not declare it.\n *\n * Error convention (fixed rule): **classification uses `Result`,\n * API calls throw.** `detect` (and other classifiers) return a\n * {@link Result}; every other API method rejects with an `Error` whose\n * `name` carries the machine-readable code. Plugins branch on\n * {@link isPluginAssetError} — never parse messages.\n *\n * Runtime limits live behind `@hoardodile/sdk-types/plugin-asset-limits`;\n * this module exports the types and the error helpers only.\n */\nimport type { PluginAssetErrorName } from \"./plugin-asset-limits.ts\"\n\nexport type { PluginAssetErrorName }\n\n/**\n * A download request: the plugin declares the plaintext URL, the vault\n * destination, and (optionally) an integrity pin plus a reason for the\n * consent dialog. `dest` is vault-relative only — it must resolve under\n * `<plugin-dir>/vault/` and can never reach the plugin's own bundled\n * files (`main.js`, `index.html`, `assets/`, ...).\n */\nexport type PluginDownloadRequest = {\n\t/** Absolute `http(s)` URL to fetch. Shown verbatim in the consent dialog. */\n\treadonly url: string\n\t/**\n\t * Vault-relative destination path (`\"runtime/live2d.min.js\"`). The host\n\t * resolves it inside the plugin vault and rejects absolute paths,\n\t * `..` traversal, path separators crossing segments, and reserved\n\t * names — before any network request is made.\n\t */\n\treadonly dest: string\n\t/**\n\t * Optional SRI-style integrity pin (64 lowercase hex chars). When\n\t * present the host verifies the downloaded bytes against it and\n\t * discards a mismatch, so a tampered or corrupted response can\n\t * never be stored.\n\t */\n\treadonly sha256?: string\n\t/** Optional short rationale shown in the consent dialog (plugin-authored copy). */\n\treadonly reason?: string\n}\n\n/**\n * Result of {@link PluginDownloadRequest}: the stored file's identity.\n * `cached` is true when the destination already existed — the host\n * answered from the vault without any dialog and without touching the\n * network (downloads are \"ensure present\", never unconditional).\n */\nexport type PluginDownloadResult = {\n\t/** The vault-relative destination that was resolved. */\n\treadonly path: string\n\treadonly sizeBytes: number\n\t/** sha256 of the stored bytes (host-computed, always present). */\n\treadonly sha256: string\n\t/** True when the file already existed and no consent/network was needed. */\n\treadonly cached: boolean\n}\n\n/**\n * Result of {@link ResourceAPI.deleteAsset} / `WebPluginAPI.deleteAsset`.\n * Deletion is idempotent: removing nothing is not an error.\n */\nexport type PluginAssetDeleteResult = {\n\t/** True when a file was actually removed. */\n\treadonly existed: boolean\n}\n\n/**\n * Error thrown by the asset methods. The name survives both wire\n * boundaries (worker IPC and the iframe postMessage bridge), so plugin\n * code can branch on `err.name` without parsing messages:\n *\n * - `DENIED` — the user declined the consent dialog, or consent timed out.\n * - `UNAVAILABLE` — this runtime has no consent channel (CLI, workbench,\n * offline mock) or the server is in read-only archive mode.\n * - `POLICY` — the host rejected the request before downloading:\n * manifest lacks the `download` permission, the URL or `dest` is not\n * allowed, the destination is a directory, or a quota would be\n * exceeded. Also used for reserved-name conflicts.\n *\n * Transport/network failures keep their own error names (e.g. socket\n * errors) and are not part of this vocabulary.\n */\nexport class PluginAssetError extends Error {\n\tconstructor(\n\t\treadonly code: PluginAssetErrorName,\n\t\tmessage: string,\n\t) {\n\t\tsuper(message)\n\t\tthis.name = code\n\t}\n}\n\n/**\n * Narrow an asset error to a machine-readable name. Works across the\n * RPC boundaries because both preserve `Error.name` (the worker IPC and\n * the iframe bridge carry the name explicitly) — the instance is often\n * lost in transit, so the check keys on the name alone.\n */\nexport function isPluginAssetError(\n\terr: unknown,\n\tname: PluginAssetErrorName,\n): err is PluginAssetError {\n\treturn err instanceof Error && err.name === name\n}\n\n/** Build a `PluginAssetError` carrying the given machine-readable name. */\nexport function pluginAssetError(\n\tname: PluginAssetErrorName,\n\tmessage: string,\n): PluginAssetError {\n\treturn new PluginAssetError(name, message)\n}\n","/**\n * Canonical media-type knowledge: the extension sets, the extension →\n * MIME table and the MIME → media-kind mapping shared by content\n * plugins, the runtime host's sniffer and the server's classification\n * pipeline.\n *\n * Extensions are a **hint**, never the verdict: `ResourceAPI.sniff`\n * reads the file's magic bytes and only falls back to the tables here\n * when the content carries no recognizable signature (text formats).\n * The sets below therefore answer \"which extensions do we expect to\n * decode\", not \"what is this file\".\n *\n * Lower-case, with leading dot — match the output of\n * `path.extname(name).toLowerCase()`.\n *\n * Adding a new extension here widens classification everywhere at\n * once. Before adding, verify:\n * - sharp can extract width/height (image)\n * - ffprobe can read width/height/duration (video)\n * - ffprobe can read the stream/format metadata (audio), and the\n * extension has an entry in `AUDIO_FFMPEG_INPUT_FORMAT`\n * - the extension has an entry in {@link EXT_MIME}\n * - the gallery plugin's transcode-required set reflects the format\n * (browser-renderable originals stay native; HEIC/TIFF need the\n * sharp preview pipeline)\n */\nexport const IMAGE_EXTS: ReadonlySet<string> = new Set([\n\t\".jpg\",\n\t\".jpeg\",\n\t\".png\",\n\t\".webp\",\n\t\".gif\",\n\t\".bmp\",\n\t\".avif\",\n\t\".heic\",\n\t\".heif\",\n\t\".tif\",\n\t\".tiff\",\n\t\".svg\",\n\t\".jp2\",\n\t\".j2k\",\n\t\".jpx\",\n])\n\nexport const VIDEO_EXTS: ReadonlySet<string> = new Set([\n\t\".mp4\",\n\t\".webm\",\n\t\".mov\",\n\t\".mkv\",\n\t\".m4v\",\n\t\".avi\",\n\t\".3gp\",\n])\n\nexport const AUDIO_EXTS: ReadonlySet<string> = new Set([\n\t\".mp3\",\n\t\".flac\",\n\t\".ogg\",\n\t\".m4a\",\n\t\".wav\",\n\t\".opus\",\n\t\".aac\",\n])\n\n/**\n * Video containers ffmpeg can demux from a forward-only pipe (matroska,\n * avi). ISO-BMFF files (.mp4/.mov/.m4v) keep their moov index at the end\n * of the file, so a stream attempt on a zip-entry source is guaranteed to\n * fail after burning a full probesize read — consumers (thumb pipeline,\n * cover probing) send those straight to the materialized entry instead.\n */\nexport const STREAMABLE_VIDEO_EXTS: ReadonlySet<string> = new Set([\n\t\".webm\",\n\t\".mkv\",\n\t\".avi\",\n])\n\n/**\n * ffmpeg `-f` container name for piped audio bytes (no filename hint).\n * `.opus` files are Ogg containers; `.m4a` is ISO-BMFF, demuxed by the\n * mp4 demuxer.\n */\nexport const AUDIO_FFMPEG_INPUT_FORMAT: Readonly<Record<string, string>> = {\n\t\".mp3\": \"mp3\",\n\t\".flac\": \"flac\",\n\t\".ogg\": \"ogg\",\n\t\".opus\": \"ogg\",\n\t\".wav\": \"wav\",\n\t\".m4a\": \"mp4\",\n\t\".aac\": \"aac\",\n}\n\n/**\n * ffmpeg `-f` container name keyed by **sniffed MIME type**, for piped\n * sources that have no filename ffprobe could key off. Content-derived\n * routing is what lets a mislabelled file still demux correctly, so\n * this table — not the extension one — drives `ResourceAPI.probe`.\n *\n * Several spellings map to the same container because magic-byte\n * matchers and IANA disagree on the canonical name (`audio/wav` vs\n * `audio/x-wav`, `video/vnd.avi` vs `video/x-msvideo`).\n */\nexport const MIME_FFMPEG_INPUT_FORMAT: Readonly<Record<string, string>> = {\n\t\"video/mp4\": \"mp4\",\n\t\"video/quicktime\": \"mov\",\n\t\"video/webm\": \"webm\",\n\t\"video/x-matroska\": \"matroska\",\n\t\"video/matroska\": \"matroska\",\n\t\"video/vnd.avi\": \"avi\",\n\t\"video/x-msvideo\": \"avi\",\n\t\"video/avi\": \"avi\",\n\t\"video/ogg\": \"ogg\",\n\t\"audio/mpeg\": \"mp3\",\n\t\"audio/mp3\": \"mp3\",\n\t\"audio/flac\": \"flac\",\n\t\"audio/x-flac\": \"flac\",\n\t\"audio/ogg\": \"ogg\",\n\t\"audio/opus\": \"ogg\",\n\t\"audio/vorbis\": \"ogg\",\n\t\"audio/wav\": \"wav\",\n\t\"audio/x-wav\": \"wav\",\n\t\"audio/vnd.wave\": \"wav\",\n\t\"audio/wave\": \"wav\",\n\t\"audio/mp4\": \"mp4\",\n\t\"audio/x-m4a\": \"mp4\",\n\t\"audio/aac\": \"aac\",\n\t\"video/3gpp\": \"mp4\",\n\t\"application/ogg\": \"ogg\",\n\t\"application/x-matroska\": \"matroska\",\n\t\"application/mp4\": \"mp4\",\n}\n\n/**\n * The audio mirror of {@link STREAMABLE_VIDEO_EXTS}: containers whose\n * headers lead the file, so ffmpeg/ffprobe can demux them from a\n * forward-only pipe. `.m4a` is ISO-BMFF with a trailing moov index, so\n * it must be probed from a materialized (seekable) entry.\n */\nexport const STREAMABLE_AUDIO_EXTS: ReadonlySet<string> = new Set([\n\t\".mp3\",\n\t\".flac\",\n\t\".ogg\",\n\t\".opus\",\n\t\".wav\",\n\t\".aac\",\n])\n\n/**\n * Media families a file can belong to. `other` covers everything the\n * media pipeline does not decode (text, documents, archives, ...) — it\n * is a real answer, not a failure.\n */\nexport const MEDIA_KINDS = [\"image\", \"video\", \"audio\", \"other\"] as const\n\nexport type MediaKind = (typeof MEDIA_KINDS)[number]\n\n/**\n * Extension → canonical MIME type. Used as the *fallback* branch of\n * content sniffing: magic-byte detection covers binary media, while\n * text-based formats (`.txt`, `.md`, `.csv`, subtitles, ...) carry no\n * signature and can only be named by their extension.\n */\nexport const EXT_MIME: Readonly<Record<string, string>> = {\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".png\": \"image/png\",\n\t\".webp\": \"image/webp\",\n\t\".gif\": \"image/gif\",\n\t\".bmp\": \"image/bmp\",\n\t\".avif\": \"image/avif\",\n\t\".heic\": \"image/heic\",\n\t\".heif\": \"image/heif\",\n\t\".tif\": \"image/tiff\",\n\t\".tiff\": \"image/tiff\",\n\t\".jp2\": \"image/jp2\",\n\t\".j2k\": \"image/jp2\",\n\t\".jpx\": \"image/jp2\",\n\t\".mp4\": \"video/mp4\",\n\t\".m4v\": \"video/mp4\",\n\t\".webm\": \"video/webm\",\n\t\".mov\": \"video/quicktime\",\n\t\".mkv\": \"video/x-matroska\",\n\t\".avi\": \"video/vnd.avi\",\n\t\".3gp\": \"video/3gpp\",\n\t\".mp3\": \"audio/mpeg\",\n\t\".flac\": \"audio/flac\",\n\t\".ogg\": \"audio/ogg\",\n\t\".opus\": \"audio/opus\",\n\t\".m4a\": \"audio/mp4\",\n\t\".wav\": \"audio/wav\",\n\t\".aac\": \"audio/aac\",\n\t\".txt\": \"text/plain\",\n\t\".md\": \"text/markdown\",\n\t\".csv\": \"text/csv\",\n\t\".json\": \"application/json\",\n\t\".xml\": \"text/xml\",\n\t\".html\": \"text/html\",\n\t\".htm\": \"text/html\",\n\t\".svg\": \"image/svg+xml\",\n\t\".srt\": \"application/x-subrip\",\n\t\".vtt\": \"text/vtt\",\n\t\".ass\": \"text/x-ssa\",\n\t\".epub\": \"application/epub+zip\",\n\t\".pdf\": \"application/pdf\",\n\t\".zip\": \"application/zip\",\n\t\".cbz\": \"application/vnd.comicbook+zip\",\n\t\".cbr\": \"application/vnd.comicbook-rar\",\n\t\".rar\": \"application/vnd.rar\",\n\t\".7z\": \"application/x-7z-compressed\",\n\t\".cb7\": \"application/x-7z-compressed\",\n\t\".tar\": \"application/x-tar\",\n\t\".cbt\": \"application/x-tar\",\n}\n\n/**\n * Container MIME types whose top-level type does not describe the\n * payload. Ogg and Matroska carry audio *or* video, and `application/*`\n * says nothing either way — the values here are the common case, and\n * `ResourceAPI.probe` overrides them with the stream layout ffprobe\n * actually reports.\n */\nexport const MIME_KIND_OVERRIDES: Readonly<Record<string, MediaKind>> = {\n\t\"application/ogg\": \"audio\",\n\t\"application/x-matroska\": \"video\",\n\t\"application/mp4\": \"video\",\n}\n\n/**\n * Media family of a MIME type: the override table first, then the\n * top-level type. Never throws — unknown types are `other`.\n */\nexport function mimeToKind(mime: string): MediaKind {\n\tconst normalized = mime.toLowerCase()\n\tconst override = MIME_KIND_OVERRIDES[normalized]\n\tif (override !== undefined) return override\n\tif (normalized.startsWith(\"image/\")) return \"image\"\n\tif (normalized.startsWith(\"video/\")) return \"video\"\n\tif (normalized.startsWith(\"audio/\")) return \"audio\"\n\treturn \"other\"\n}\n\n/** Canonical MIME type for an extension (leading dot), or `undefined`. */\nexport function extToMime(ext: string): string | undefined {\n\treturn EXT_MIME[ext.toLowerCase()]\n}\n","/**\n * The plugin definition contract — the single source of truth shared by\n * the authoring SDK (`@hoardodile/sdk-server`), the runtime host\n * (`@hoardodile/host`) and the worker sandbox. Everything here is pure\n * TypeScript with no node or DOM dependencies, so the same contract\n * serves browser-facing packages and node runtimes alike.\n */\nimport type { MediaKind } from \"./media-exts.ts\"\nimport { extToMime, mimeToKind } from \"./media-exts.ts\"\nimport type {\n\tPluginAssetDeleteResult,\n\tPluginDownloadRequest,\n\tPluginDownloadResult,\n} from \"./plugin-asset.ts\"\nimport { pluginAssetError } from \"./plugin-asset.ts\"\nimport type { ReadFileRange } from \"./read-range.ts\"\nimport type { Result } from \"./result.ts\"\n\nexport type { MediaKind }\n\n/**\n * Schema contract shared between server and web plugin APIs.\n * Declared once per plugin and used to type both `definePlugin` and\n * `WebPluginAPI`.\n */\nexport interface PluginSchema {\n\treadonly file?: unknown\n\treadonly sourceMeta?: unknown\n\treadonly searchMeta?: unknown\n\t/**\n\t * Plugin-defined payload the `detect` hook may carry on a\n\t * successful match. The host keeps the last payload and exposes it\n\t * to the plugin's other hooks as `api.context.detect` — classify\n\t * once in `detect` instead of rescanning in every hook. Declaring\n\t * this slot types the context; hooks must still handle the absent\n\t * case (`undefined`: fresh worker, or detect never matched).\n\t */\n\treadonly detect?: unknown\n\t/**\n\t * Plugin-defined anchor location data: the payload carried inside the\n\t * wire {@link AnchorData} envelope (see {@link anchorData}). Outgoing\n\t * anchors are typed by this slot and passed raw (e.g.\n\t * `createMessage({ anchor: { page } })`); incoming anchor data is\n\t * validated by the plugin's `decodeAnchor` (see `definePluginAPI` in\n\t * `@hoardodile/sdk-react`).\n\t */\n\treadonly anchor?: unknown\n}\n\n/**\n * Server plugin detection result: the shared result vocabulary, where a\n * match carries the schema's `detect` payload (when one is declared)\n * and a miss carries its reasons. Plugins may return the literal\n * `{ ok: true } as const` / `{ ok: false, reasons }` shapes directly,\n * or use `ok()`/`err({ reasons })` from the result module.\n *\n * `TPayload` is the plugin's declared `detect` slot — the payload\n * spread onto a match is checked against it at compile time, so a\n * classification that drifts from the schema fails to build.\n */\nexport type Detection<TPayload extends object = object> = Result<\n\tTPayload,\n\t{ readonly reasons: readonly string[] }\n>\n\n/** Structured logger scoped to a single plugin. */\nexport type Logger = {\n\tinfo(message: string, data?: Record<string, unknown>): void\n\twarn(message: string, data?: Record<string, unknown>): void\n\terror(message: string, data?: Record<string, unknown>): void\n}\n\n/** Image probe payload. */\nexport type ImageInfo = {\n\treadonly width?: number\n\treadonly height?: number\n}\n\n/** Video probe payload. */\nexport type VideoInfo = {\n\treadonly width?: number\n\treadonly height?: number\n\treadonly durationMs?: number\n}\n\n/**\n * Embedded container tags carried by an audio file (ID3, Vorbis\n * comments, MP4 metadata atoms). Every field is optional — untagged\n * files are normal.\n */\nexport type AudioTags = {\n\treadonly title?: string\n\treadonly artist?: string\n\treadonly album?: string\n}\n\n/**\n * Embedded artwork carried by an audio file (ID3 APIC, FLAC PICTURE,\n * MP4 `covr`). Its presence is the signal that the host can extract a\n * real cover; the dimensions come from the same probe, so callers can\n * pre-size the cover slot without decoding the picture.\n */\nexport type AudioCoverArt = {\n\treadonly width?: number\n\treadonly height?: number\n}\n\n/**\n * Audio probe payload. Any field can be absent when the container does\n * not report it.\n */\nexport type AudioInfo = {\n\treadonly durationMs?: number\n\t/** Codec name of the first audio stream, e.g. `\"mp3\"`, `\"flac\"`. */\n\treadonly codec?: string\n\t/** Container bit rate in bits per second. */\n\treadonly bitRate?: number\n\t/** Sample rate of the first audio stream, in Hz. */\n\treadonly sampleRate?: number\n\t/** Channel count of the first audio stream. */\n\treadonly channels?: number\n\t/** Present only when the file embeds artwork. */\n\treadonly coverArt?: AudioCoverArt\n\treadonly tags?: AudioTags\n}\n\n/**\n * What a file's bytes say it is. Produced by {@link ResourceAPI.sniff}.\n *\n * `source` records who answered: `\"magic\"` means the file's own\n * signature was recognized (authoritative), `\"extension\"` means the\n * content carried no signature and the filename was used instead — the\n * normal outcome for text-based formats, which have no magic bytes.\n *\n * `kind` is provisional for container formats that can hold either\n * audio or video (Ogg, Matroska, ISO-BMFF); {@link ResourceAPI.probe}\n * overrides it with the stream layout actually found in the file.\n */\nexport type FileType = {\n\t/** Canonical MIME type, e.g. `\"image/jpeg\"`. */\n\treadonly mime: string\n\t/** Canonical extension for {@link mime}, with leading dot. */\n\treadonly ext: string\n\treadonly kind: MediaKind\n\treadonly source: \"magic\" | \"extension\"\n}\n\n/**\n * Everything one media probe pass can say about a file, discriminated\n * by the family the content really belongs to — the shape every\n * mainstream prober uses (ffprobe's `format` + `streams`, sharp's\n * `metadata()`, Tika's `MediaType`).\n *\n * `other` is a successful answer: the file was identified and is not\n * decodable media (text, documents, archives). `unknown` is the failure\n * branch and always carries a reason, so \"this host has no probe\n * backend\" is never confused with \"this file is not an image\":\n *\n * - `unsupported` — identified, but no backend decodes this format\n * - `unavailable` — the host wired no probe implementation (raw\n * directory APIs and test fixtures)\n * - `failed` — a backend ran and could not decode the bytes\n */\nexport type ProbeResult =\n\t| ({\n\t\t\treadonly kind: \"image\"\n\t\t\treadonly mime: string\n\t\t\t/** Multi-frame source: animated GIF / WebP / APNG / AVIF. */\n\t\t\treadonly animated: boolean\n\t } & ImageInfo)\n\t| ({ readonly kind: \"video\"; readonly mime: string } & VideoInfo)\n\t| ({ readonly kind: \"audio\"; readonly mime: string } & AudioInfo)\n\t| { readonly kind: \"other\"; readonly mime: string }\n\t| {\n\t\t\treadonly kind: \"unknown\"\n\t\t\treadonly reason: \"unsupported\" | \"unavailable\" | \"failed\"\n\t }\n\n/**\n * Perceptual hash kinds the host can compute for an image file.\n * `dhash` (difference hash) and `phash` (DCT-based perceptual hash)\n * are 64-bit similarity hashes compared by Hamming distance;\n * `sha256` is an exact byte hash. Animated images hash their first\n * frame. Plugins decide which kinds to request and which files to\n * hash — the host only provides the computation.\n */\nexport const IMAGE_HASH_KINDS = [\"sha256\", \"dhash\", \"phash\"] as const\nexport type ImageHashKind = (typeof IMAGE_HASH_KINDS)[number]\n\n/**\n * One content hash of a resource file, produced by the plugin's\n * `imageHashes` hook. `scope` is the archive-relative file path,\n * `type` the hash kind (`sha256`/`dhash`/`phash` or a plugin-defined\n * extension), `value` the lowercase hex digest. A resource may expose\n * several hashes (per file × per kind) or none.\n */\nexport type ImageHash = {\n\treadonly scope: string\n\treadonly type: string\n\treadonly value: string\n\t/** Bit length of the hash; required for perceptual kinds. */\n\treadonly bits?: number\n}\n\n/** Result of the `imageHashes` hook: hashes per file, possibly empty. */\nexport type ImageHashesResult = {\n\treadonly hashes: readonly ImageHash[]\n}\n\n/**\n * One file inside a container entry (zip/tar) as listed (or extracted)\n * by the plugin API. `path` is the entry's path inside the archive;\n * dimensions are present when the host probed the entry (image\n * backends) — a listing-only result carries no dimensions.\n */\nexport type ArchiveExtractionEntry = {\n\treadonly path: string\n\treadonly sizeBytes: number\n\treadonly kind: MediaKind\n\treadonly width?: number\n\treadonly height?: number\n\treadonly animated?: boolean\n}\n\n/**\n * A container listing without materialization — the cheap counterpart of\n * {@link ResourceAPI.extractArchive}. Carries entry names, sizes and\n * kinds only; no dimensions (probing those requires the bytes).\n */\nexport type ContainerListing = {\n\treadonly entries: readonly ArchiveExtractionEntry[]\n}\n\n/**\n * Result of {@link ResourceAPI.extractArchive}: the materialized\n * entries of a container entry. A completed extraction is marked by the\n * host's `index.json` manifest; extraction always writes the cache (the\n * host's `local/cache` is derived data, writable in every view mode).\n */\nexport type ArchiveExtraction = {\n\treadonly entries: readonly ArchiveExtractionEntry[]\n}\n\n/**\n * Resource-scoped API available to every plugin hook. All paths are\n * relative to the resource's source directory; the host resolves\n * absolute paths transparently.\n *\n * `TSchema` types the injected session context (`context.detect`); the\n * default keeps the API compatible with code that never reads it.\n */\nexport type ResourceAPI<TSchema extends PluginSchema = PluginSchema> = {\n\t/** Write an informational log entry. */\n\treadonly logInfo: (message: string, data?: Record<string, unknown>) => void\n\t/** Write a warning log entry. */\n\treadonly logWarn: (message: string, data?: Record<string, unknown>) => void\n\t/** Write an error log entry. */\n\treadonly logError: (message: string, data?: Record<string, unknown>) => void\n\t/**\n\t * List all regular-file names (flat list), in canonical display\n\t * order: the resource's explicit upload order when one exists (the\n\t * host's `.order` manifest), the natural name sort otherwise.\n\t * Plugins that need their own ordering should sort explicitly.\n\t *\n\t * This is the raw name list — the `listFiles` hook of the plugin\n\t * definition turns it into typed file entries.\n\t */\n\treadonly listFileNames: () => Promise<readonly string[]>\n\t/**\n\t * Read a regular file relative to the resource root.\n\t *\n\t * Without `range` the whole file is returned; hosts may reject\n\t * oversized full reads — pass a range (or use `readFileChunks` from\n\t * `@hoardodile/sdk-server/helpers`) for large files.\n\t *\n\t * Container addressing: a path of the form `outer!inner` reads the\n\t * file *inside* a zip/tar entry (e.g. `manga.cbz!Chapter 1/001.jpg`)\n\t * — the host streams the decompressed bytes. When `outer` is not a\n\t * container, or the inner entry is absent, the whole path is treated\n\t * as a literal filename.\n\t */\n\treadonly readFile: (\n\t\tpath: string,\n\t\trange?: ReadFileRange,\n\t) => Promise<Uint8Array>\n\t/**\n\t * Return the byte size of `path` without reading the file contents.\n\t * Resolves to `undefined` when the file does not exist or the artifact\n\t * is not yet committed. Supports container addressing (`outer!inner`).\n\t */\n\treadonly statFile: (\n\t\tpath: string,\n\t) => Promise<{ readonly sizeBytes: number } | undefined>\n\t/**\n\t * Batch {@link statFile}: resolves every path in one host round-trip\n\t * (positions preserved). Prefer this over a per-file fan-out of\n\t * `statFile` when statting a whole archive — one RPC instead of N.\n\t */\n\treadonly statFiles: (\n\t\tpaths: readonly string[],\n\t) => Promise<readonly ({ readonly sizeBytes: number } | undefined)[]>\n\t/**\n\t * Identify the file at `path` from its content: magic-byte\n\t * detection, falling back to the extension only for formats that\n\t * carry no signature (text, subtitles). Resolves to `undefined` when\n\t * neither can name the file. Supports container addressing.\n\t *\n\t * This is the cheap call — it reads a small header window, never\n\t * decodes. Use it to route work; use {@link probe} when you need\n\t * dimensions, duration or stream details.\n\t */\n\treadonly sniff: (path: string) => Promise<FileType | undefined>\n\t/**\n\t * Decode the media metadata of `path` in one pass, routed by\n\t * {@link sniff} rather than by the filename: images resolve through\n\t * sharp, audio and video through ffprobe (which also settles\n\t * ambiguous containers — an `.ogg` holding only audio streams comes\n\t * back as `kind: \"audio\"`). Supports container addressing.\n\t *\n\t * Always resolves, never rejects. Non-media files answer\n\t * `{ kind: \"other\" }`; the `unknown` branch carries a `reason` that\n\t * distinguishes \"no backend wired\" (`unavailable`, what raw\n\t * directory APIs and fixtures return) from a real decode failure.\n\t */\n\treadonly probe: (path: string) => Promise<ProbeResult>\n\t/**\n\t * Stream-hash the file at `path` (any file kind). Rejects when the\n\t * file is missing or the read fails; the host streams the entry so\n\t * arbitrarily large files are safe. Supports container addressing.\n\t */\n\treadonly hashBytes: (path: string, algo: \"md5\" | \"sha256\") => Promise<string>\n\t/**\n\t * Compute the requested hashes of the image at `path` in one pass:\n\t * `sha256` from the raw bytes, `dhash`/`phash` from a decoded\n\t * grayscale rendition (animated images use their first frame).\n\t * Resolves to `undefined` when the file is not a decodable image;\n\t * `kinds` names a subset of {@link IMAGE_HASH_KINDS} and the result\n\t * carries exactly those keys. Supports container addressing.\n\t */\n\treadonly computeImageHashes: (\n\t\tpath: string,\n\t\tkinds: readonly ImageHashKind[],\n\t) => Promise<Readonly<Record<ImageHashKind, string>> | undefined>\n\t/**\n\t * List the file entries of a container entry (zip/tar) without\n\t * materializing anything — the cheap call for metadata-only needs\n\t * (detect, card counts). Rejects when `filename` is not a supported\n\t * container.\n\t */\n\treadonly listContainer: (filename: string) => Promise<ContainerListing>\n\t/**\n\t * Materialize the contents of a container entry (zip/tar) into the\n\t * host's extraction cache so the browser can serve the inner files\n\t * over plain URLs. `filename` is a literal container entry — the\n\t * cache holds one directory per archive with the inner paths\n\t * preserved, plus a completion manifest.\n\t *\n\t * Idempotent: an already-materialized archive re-lists from the\n\t * manifest without re-extracting. Rejects when the entry is not a\n\t * supported container, exceeds the host's byte/entry budgets, or\n\t * when this host wires no extraction cache (test fixtures, raw\n\t * directory APIs).\n\t */\n\treadonly extractArchive: (filename: string) => Promise<ArchiveExtraction>\n\t/**\n\t * Ensure a remote asset exists in the plugin's own vault: when\n\t * `dest` is already present the host answers `cached: true` without\n\t * any dialog and without touching the network; otherwise the host\n\t * asks the user (the web app shows the consent dialog with the URL\n\t * verbatim) and downloads on approval. The file always lands inside\n\t * `<plugin-dir>/vault/` — `dest` is vault-relative and can never\n\t * reach the plugin's bundled files.\n\t *\n\t * Gated by the manifest `download` permission; rejections carry a\n\t * machine-readable {@link PluginAssetErrorName} in `err.name`\n\t * (`DENIED` / `UNAVAILABLE` / `POLICY`).\n\t */\n\treadonly download: (\n\t\trequest: PluginDownloadRequest,\n\t) => Promise<PluginDownloadResult>\n\t/**\n\t * Byte size of a vault file, or `undefined` when absent. The cheap\n\t * presence check on top of which `download` resolves cached hits.\n\t */\n\treadonly statAsset: (\n\t\tpath: string,\n\t) => Promise<{ readonly sizeBytes: number } | undefined>\n\t/** Read a vault file's bytes (bounded by the same cap as {@link readFile}). */\n\treadonly readAsset: (path: string) => Promise<Uint8Array>\n\t/**\n\t * Remove a vault file; idempotent (absent files answer\n\t * `{ existed: false }`). The plugin decides the vault's own\n\t * lifecycle — e.g. cleaning stale layouts after a plugin update.\n\t * No user consent is required: nothing leaves the host. Directories\n\t * and paths outside the vault are rejected (`POLICY`).\n\t */\n\treadonly deleteAsset: (path: string) => Promise<PluginAssetDeleteResult>\n\t/**\n\t * Session context injected by the host. `detect` carries the payload\n\t * the plugin's `detect` hook returned on its last successful match\n\t * (worker-session scope): the one-pass classification every other\n\t * hook can build on. `undefined` when detect has not matched in this\n\t * session — a fresh worker — so hooks must always handle the absent\n\t * case by re-deriving.\n\t */\n\treadonly context: { readonly detect: TSchema[\"detect\"] | undefined }\n}\n\n/**\n * Declarative description of a content plugin. Plugins export an instance\n * of this shape as their default export; the host injects the resource\n * API at call time and never invokes a factory function.\n */\nexport type PluginDefinition<TSchema extends PluginSchema = PluginSchema> = {\n\t/**\n\t * Detect whether this plugin applies to the current resource. A\n\t * successful match may carry a payload — `ok({ ...shape })` — which\n\t * the host keeps and exposes to the other hooks as\n\t * `api.context.detect`. The payload is checked against the schema's\n\t * `detect` slot (when one is declared).\n\t */\n\treadonly detect: (\n\t\tapi: ResourceAPI<TSchema>,\n\t) => Promise<Detection<TSchema[\"detect\"] & object>>\n\t/** Optional source metadata builder. */\n\treadonly sourceMeta?: (\n\t\tapi: ResourceAPI<TSchema>,\n\t) => Promise<TSchema[\"sourceMeta\"] | undefined>\n\t/** Optional search metadata builder. */\n\treadonly searchMeta?: (\n\t\tapi: ResourceAPI<TSchema>,\n\t) => Promise<TSchema[\"searchMeta\"] | undefined>\n\t/** Optional local cover source resolver. */\n\treadonly coverLocal?: (\n\t\tapi: ResourceAPI<TSchema>,\n\t) => Promise<string | undefined>\n\t/**\n\t * Optional custom file list builder. Results are cached verbatim in a\n\t * sidecar. When absent the host falls back to a bare list of source\n\t * filenames.\n\t */\n\treadonly listFiles?: (\n\t\tapi: ResourceAPI<TSchema>,\n\t) => Promise<readonly TSchema[\"file\"][]>\n\t/**\n\t * Optional content hashes for duplicate detection and image\n\t * similarity. The plugin decides the policy — which files to hash\n\t * and which kinds — by calling the API's hash primitives; a plugin\n\t * facing image-less resources simply omits this hook. Returning\n\t * `undefined` (or a hook error) keeps the resource's hash rows empty.\n\t */\n\treadonly imageHashes?: (\n\t\tapi: ResourceAPI<TSchema>,\n\t) => Promise<ImageHashesResult | undefined>\n}\n\n/** Plugin hook names the host can invoke, in contract order. */\nexport const HOOK_NAMES = [\n\t\"detect\",\n\t\"sourceMeta\",\n\t\"searchMeta\",\n\t\"coverLocal\",\n\t\"listFiles\",\n\t\"imageHashes\",\n] as const\n\nexport type HookName = (typeof HOOK_NAMES)[number]\n\nfunction isAsyncFunction(value: unknown): boolean {\n\treturn (\n\t\ttypeof value === \"function\" && value.constructor.name === \"AsyncFunction\"\n\t)\n}\n\n/**\n * Freeze and return a plugin definition. Runs shape validation upfront so\n * a malformed plugin fails at load time with a friendly message instead\n * of misbehaving at hook time.\n */\nexport function definePlugin<TSchema extends PluginSchema = PluginSchema>(\n\tdefinition: PluginDefinition<TSchema>,\n): PluginDefinition<TSchema> {\n\tassertPluginShape(definition)\n\treturn Object.freeze({ ...definition })\n}\n\n/**\n * Validate that a value satisfies the structural contract of a\n * {@link PluginDefinition}: only known hooks, all hooks async functions,\n * `detect` required. Does NOT exercise behaviour.\n */\nexport function assertPluginShape(\n\tvalue: unknown,\n): asserts value is PluginDefinition {\n\tif (typeof value !== \"object\" || value === null) {\n\t\tthrow new Error(\"PluginDefinition: expected an object with hook functions\")\n\t}\n\n\tconst definition = value as Record<string, unknown>\n\n\tconst knownHooks = new Set<string>(HOOK_NAMES)\n\tconst unknown = Object.keys(definition).filter((key) => !knownHooks.has(key))\n\tif (unknown.length > 0) {\n\t\tthrow new Error(\n\t\t\t`PluginDefinition: unknown hook(s) ${unknown.map((k) => `\"${k}\"`).join(\", \")} — expected one of: ${HOOK_NAMES.join(\", \")}`,\n\t\t)\n\t}\n\n\tfor (const hook of HOOK_NAMES) {\n\t\tconst entry = definition[hook]\n\t\tif (entry === undefined) {\n\t\t\tif (hook === \"detect\") {\n\t\t\t\tthrow new Error(\"PluginDefinition: missing detect()\")\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (!isAsyncFunction(entry)) {\n\t\t\tconst kind =\n\t\t\t\ttypeof entry === \"function\" ? \"a synchronous function\" : typeof entry\n\t\t\tthrow new Error(\n\t\t\t\t`PluginDefinition: \"${hook}\" must be an async function (got ${kind}) — hooks may do heavy work and the host awaits every hook, so declare it with \\`async\\`.`,\n\t\t\t)\n\t\t}\n\t}\n}\n\n/**\n * Convenience wrapper that builds a failing plugin definition. Used by\n * the host when a plugin directory is missing or its main.js cannot be\n * loaded.\n */\nexport function createFailingPlugin(\n\treasons: readonly string[],\n): PluginDefinition {\n\treturn definePlugin({\n\t\tdetect: async () => ({ ok: false, reasons }),\n\t})\n}\n\n/** Type guard for the success branch of a {@link Detection}. */\nexport function isDetected(\n\tdetection: Detection,\n): detection is { readonly ok: true } {\n\treturn detection.ok\n}\n\n/** Type guard for the failure branch of a {@link Detection}. */\nexport function isMissed(\n\tdetection: Detection,\n): detection is { readonly ok: false; readonly reasons: readonly string[] } {\n\treturn !detection.ok\n}\n\n/** Declarative configuration for a {@link ResourceAPI} fixture. */\nexport type ResourceAPIFixtureConfig<\n\tTSchema extends PluginSchema = PluginSchema,\n> = {\n\t/** File names returned by `listFileNames`. */\n\treadonly files?: readonly string[]\n\t/** File contents returned by `readFile`. */\n\treadonly contents?: Readonly<Record<string, string | Uint8Array>>\n\t/**\n\t * `sniff` results keyed by file path — `{ \"a.jpg\": … }` matches only\n\t * that file, keys starting with a dot match by extension suffix\n\t * (`{ \".mp4\": … }` applies to every .mp4 file, longest key wins),\n\t * and `{ \"\": … }` matches every path (the usual way to express a\n\t * default). Unconfigured paths fall back to the extension table,\n\t * exactly like the host's extension branch.\n\t */\n\treadonly types?: Readonly<Record<string, FileType | undefined>>\n\t/**\n\t * `probe` results keyed by file path (same matching rules as\n\t * {@link types}). Unconfigured paths mirror the host's routing:\n\t * identified non-media answers `{ kind: \"other\" }`, identified media\n\t * answers `{ kind: \"unknown\", reason: \"unavailable\" }` — the fixture\n\t * decodes nothing, so a hook that needs real dimensions belongs in a\n\t * sandbox test instead.\n\t */\n\treadonly probes?: Readonly<Record<string, ProbeResult | undefined>>\n\t/** Stat results. A plain value is used as the default for all paths. */\n\treadonly stats?:\n\t\t| Readonly<Record<string, { readonly sizeBytes: number } | undefined>>\n\t\t| { readonly sizeBytes: number }\n\t\t| undefined\n\t/** `hashBytes` results by path; a plain string is used for all paths. */\n\treadonly byteHashes?: Readonly<Record<string, string>> | string\n\t/**\n\t * `computeImageHashes` results by path. A plain record is used as the\n\t * default for all paths; absent paths resolve to `undefined`.\n\t */\n\treadonly imageHashes?:\n\t\t| Readonly<Record<string, ImageHashesResult>>\n\t\t| ImageHashesResult\n\t/**\n\t * `listContainer` results keyed by the archive filename. Absent\n\t * names reject, mirroring the host's \"not a supported archive\" error.\n\t */\n\treadonly containerListings?: Readonly<Record<string, ContainerListing>>\n\t/**\n\t * `extractArchive` results keyed by the archive filename. Absent\n\t * names reject, mirroring the host's \"not a supported archive\" error.\n\t */\n\treadonly extractions?: Readonly<Record<string, ArchiveExtraction>>\n\t/**\n\t * Vault file contents keyed by vault-relative path, backing\n\t * `statAsset` / `readAsset` / `deleteAsset` in the fixture.\n\t */\n\treadonly assetFiles?: Readonly<Record<string, string | Uint8Array>>\n\t/**\n\t * Handler for `download`. Absent means the hosted runtime has no\n\t * consent channel — `download` rejects with `UNAVAILABLE`, exactly\n\t * like the CLI, workbench and offline mock hosts.\n\t */\n\treadonly downloadHandler?: (\n\t\trequest: PluginDownloadRequest,\n\t) => Promise<PluginDownloadResult>\n\t/**\n\t * Container addressing for the fixture: maps a virtual path\n\t * (`outer!inner`) to stat/sniff/probe results, so hooks that browse\n\t * inside archives can be tested without real archives. Matching rules\n\t * mirror {@link types}: exact path keys, dot fragments by suffix,\n\t * `{ \"\": … }` as the default.\n\t */\n\treadonly virtualEntries?: Readonly<Record<string, ArchiveExtractionEntry>>\n\t/**\n\t * Session context handed to hooks as `api.context` — mirrors the\n\t * host injecting the payload of a prior successful `detect`. Typed\n\t * by the schema generic when one is supplied.\n\t */\n\treadonly context?: { readonly detect?: TSchema[\"detect\"] }\n}\n\nfunction resolveKeyed<T>(\n\tpath: string,\n\ttable: Readonly<Record<string, T | undefined>> | undefined,\n): T | undefined {\n\tif (table === undefined) return undefined\n\t// Keys match the path exactly, except keys that start with a dot —\n\t// those are extension fragments matching any path ending with them\n\t// (`.mp4` applies to every .mp4 file). The longest fragment wins so\n\t// a shared default is never shadowed; the empty key `\"\"` is the\n\t// catch-all default. Plain-name keys never match by substring, so\n\t// `\"a.jpg\"` cannot hijack `\"ba.jpg\"`.\n\tif (Object.hasOwn(table, path)) return table[path]\n\tlet bestKey: string | undefined\n\tlet bestLen = -1\n\tfor (const key of Object.keys(table)) {\n\t\tif (key.length > bestLen && key.startsWith(\".\") && path.endsWith(key)) {\n\t\t\tbestKey = key\n\t\t\tbestLen = key.length\n\t\t}\n\t}\n\tif (bestKey !== undefined) return table[bestKey]\n\treturn table[\"\"]\n}\n\n/** Lower-cased extension from the last dot, or `\"\"` when there is none. */\nfunction lastExt(path: string): string {\n\tconst dot = path.lastIndexOf(\".\")\n\treturn dot === -1 ? \"\" : path.slice(dot).toLowerCase()\n}\n\n/**\n * Identify a file from its name alone: the extension branch of content\n * sniffing, exposed on its own because it is also the honest answer for\n * formats that carry no signature, and the shape test doubles need when\n * standing in for a real {@link ResourceAPI}.\n */\nexport function fileTypeFromName(path: string): FileType | undefined {\n\tconst ext = lastExt(path)\n\tconst mime = extToMime(ext)\n\tif (mime === undefined) return undefined\n\treturn { mime, ext, kind: mimeToKind(mime), source: \"extension\" }\n}\n\nfunction resolveValue<T>(\n\tpath: string,\n\tvalue: Readonly<Record<string, T | undefined>> | T | undefined,\n\tdefaultValue: T | undefined,\n): T | undefined {\n\tif (value === undefined || value === null) return defaultValue\n\tif (typeof value !== \"object\" || Array.isArray(value)) return value\n\t// Matching rules mirror {@link resolveKeyed}: exact keys, dot\n\t// fragments by suffix (longest wins), empty-key default.\n\tconst table = value as Readonly<Record<string, T | undefined>>\n\tif (Object.hasOwn(table, path)) return table[path]\n\tlet bestKey: string | undefined\n\tlet bestLen = -1\n\tfor (const key of Object.keys(table)) {\n\t\tif (key.length > bestLen && key.startsWith(\".\") && path.endsWith(key)) {\n\t\t\tbestKey = key\n\t\t\tbestLen = key.length\n\t\t}\n\t}\n\tif (bestKey !== undefined) return table[bestKey]\n\treturn table[\"\"] ?? defaultValue\n}\n\nfunction virtualStat(\n\tpath: string,\n\ttable:\n\t\t| Readonly<Record<string, ArchiveExtractionEntry | undefined>>\n\t\t| undefined,\n): { readonly sizeBytes: number } | undefined {\n\tconst entry = resolveKeyed(path, table)\n\treturn entry === undefined ? undefined : { sizeBytes: entry.sizeBytes }\n}\n\nfunction virtualType(\n\tpath: string,\n\ttable:\n\t\t| Readonly<Record<string, ArchiveExtractionEntry | undefined>>\n\t\t| undefined,\n): FileType | undefined {\n\tif (resolveKeyed(path, table) === undefined) return undefined\n\treturn fileTypeFromName(path.slice(path.lastIndexOf(\"!\") + 1))\n}\n\n/**\n * Create a mutable {@link ResourceAPI} fixture driven by a declarative\n * config. No filesystem involved — the standard way to unit-test plugin\n * hooks.\n *\n * Pass the plugin's schema as the generic\n * (`createResourceAPIFixture<MySchema>()`) so the returned api carries\n * the typed session context — the same shape schema-typed hooks receive\n * from the host.\n */\nexport function createResourceAPIFixture<\n\tTSchema extends PluginSchema = PluginSchema,\n>(\n\tinitialConfig: ResourceAPIFixtureConfig<TSchema> = {},\n): {\n\treadonly api: ResourceAPI<TSchema>\n\treadonly setConfig: (next: ResourceAPIFixtureConfig<TSchema>) => void\n} {\n\tlet config: ResourceAPIFixtureConfig<TSchema> = initialConfig\n\n\tfunction setConfig(next: ResourceAPIFixtureConfig<TSchema>): void {\n\t\tconfig = next\n\t}\n\n\tconst api: ResourceAPI<TSchema> = {\n\t\tlogInfo() {},\n\t\tlogWarn() {},\n\t\tlogError() {},\n\t\tcontext: { detect: config.context?.detect },\n\t\tasync listFileNames() {\n\t\t\treturn config.files ?? []\n\t\t},\n\t\tasync readFile(path, range) {\n\t\t\tconst content = config.contents?.[path]\n\t\t\tif (content === undefined) {\n\t\t\t\tthrow new Error(`ResourceAPIFixture: no content for \"${path}\"`)\n\t\t\t}\n\t\t\tconst bytes =\n\t\t\t\ttypeof content === \"string\"\n\t\t\t\t\t? new TextEncoder().encode(content)\n\t\t\t\t\t: content\n\t\t\tif (range === undefined) return bytes\n\t\t\t// Mirrors host semantics: the range is clamped to the content size.\n\t\t\treturn bytes.slice(range.start ?? 0, range.end)\n\t\t},\n\t\tasync statFile(path) {\n\t\t\treturn (\n\t\t\t\tresolveValue(path, config.stats, undefined) ??\n\t\t\t\tvirtualStat(path, config.virtualEntries)\n\t\t\t)\n\t\t},\n\t\tasync statFiles(paths) {\n\t\t\treturn Promise.all(\n\t\t\t\tpaths.map(\n\t\t\t\t\t(path) =>\n\t\t\t\t\t\tresolveValue(path, config.stats, undefined) ??\n\t\t\t\t\t\tvirtualStat(path, config.virtualEntries),\n\t\t\t\t),\n\t\t\t)\n\t\t},\n\t\tasync sniff(path) {\n\t\t\treturn (\n\t\t\t\tresolveKeyed(path, config.types) ??\n\t\t\t\tfileTypeFromName(path) ??\n\t\t\t\tvirtualType(path, config.virtualEntries)\n\t\t\t)\n\t\t},\n\t\tasync probe(path) {\n\t\t\tconst configured = resolveKeyed(path, config.probes)\n\t\t\tif (configured !== undefined) return configured\n\t\t\t// Unconfigured paths mirror the host's own routing: a file the\n\t\t\t// fixture cannot identify is unsupported, an identified\n\t\t\t// non-media file answers `other`, and identified media needs a\n\t\t\t// real decode the fixture has no backend for.\n\t\t\tconst type =\n\t\t\t\tresolveKeyed(path, config.types) ??\n\t\t\t\tfileTypeFromName(path) ??\n\t\t\t\tvirtualType(path, config.virtualEntries)\n\t\t\tif (type === undefined) return { kind: \"unknown\", reason: \"unsupported\" }\n\t\t\tif (type.kind === \"other\") return { kind: \"other\", mime: type.mime }\n\t\t\tconst virtual = resolveKeyed(path, config.virtualEntries)\n\t\t\tif (virtual !== undefined && type.kind === \"image\") {\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"image\",\n\t\t\t\t\tmime: type.mime,\n\t\t\t\t\twidth: virtual.width,\n\t\t\t\t\theight: virtual.height,\n\t\t\t\t\tanimated: virtual.animated ?? false,\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { kind: \"unknown\", reason: \"unavailable\" }\n\t\t},\n\t\tasync hashBytes(path) {\n\t\t\tconst value = resolveValue(path, config.byteHashes, undefined)\n\t\t\tif (value === undefined) {\n\t\t\t\tthrow new Error(`ResourceAPIFixture: no byte hash for \"${path}\"`)\n\t\t\t}\n\t\t\treturn value\n\t\t},\n\t\tasync computeImageHashes(path, kinds) {\n\t\t\tconst result = resolveValue(path, config.imageHashes, undefined)\n\t\t\tif (result === undefined) return undefined\n\t\t\tconst hashes: Record<string, string> = {}\n\t\t\tfor (const entry of result.hashes) {\n\t\t\t\tif ((kinds as readonly string[]).includes(entry.type)) {\n\t\t\t\t\thashes[entry.type] = entry.value\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn hashes as Record<ImageHashKind, string>\n\t\t},\n\t\tasync extractArchive(filename) {\n\t\t\tconst configured = config.extractions?.[filename]\n\t\t\tif (configured === undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`ResourceAPIFixture: no extraction configured for \"${filename}\"`,\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn configured\n\t\t},\n\t\tasync listContainer(filename) {\n\t\t\tconst configured = config.containerListings?.[filename]\n\t\t\tif (configured === undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`ResourceAPIFixture: no container listing configured for \"${filename}\"`,\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn configured\n\t\t},\n\t\tasync download(request) {\n\t\t\tif (config.downloadHandler === undefined) {\n\t\t\t\tthrow pluginAssetError(\n\t\t\t\t\t\"UNAVAILABLE\",\n\t\t\t\t\t\"ResourceAPIFixture: no download handler configured\",\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn config.downloadHandler(request)\n\t\t},\n\t\tasync statAsset(path) {\n\t\t\tconst content = resolveValue(path, config.assetFiles, undefined)\n\t\t\tif (content === undefined) return undefined\n\t\t\treturn { sizeBytes: fixtureContentBytes(content).byteLength }\n\t\t},\n\t\tasync readAsset(path) {\n\t\t\tconst content = resolveValue(path, config.assetFiles, undefined)\n\t\t\tif (content === undefined) {\n\t\t\t\tthrow new Error(`ResourceAPIFixture: no vault file \"${path}\"`)\n\t\t\t}\n\t\t\treturn fixtureContentBytes(content)\n\t\t},\n\t\tasync deleteAsset(path) {\n\t\t\tconst table = config.assetFiles\n\t\t\tif (table === undefined || !Object.hasOwn(table, path)) {\n\t\t\t\treturn { existed: false }\n\t\t\t}\n\t\t\t// Fixture config is immutable in spirit — a delete is simulated\n\t\t\t// by copying with the entry removed, so the next call sees it gone.\n\t\t\tconst next: Record<string, string | Uint8Array> = {}\n\t\t\tfor (const [key, value] of Object.entries(table)) {\n\t\t\t\tif (key !== path) next[key] = value\n\t\t\t}\n\t\t\tconfig = { ...config, assetFiles: next }\n\t\t\treturn { existed: true }\n\t\t},\n\t}\n\n\treturn { api, setConfig }\n}\n\n/** Convert a fixture content entry (string or bytes) to `Uint8Array`. */\nfunction fixtureContentBytes(content: string | Uint8Array): Uint8Array {\n\treturn typeof content === \"string\"\n\t\t? new TextEncoder().encode(content)\n\t\t: content\n}\n\n/** Return a minimal {@link Logger} for tests. */\nexport function stubLogger(overrides?: Partial<Logger>): Logger {\n\treturn {\n\t\tinfo() {},\n\t\twarn() {},\n\t\terror() {},\n\t\t...overrides,\n\t}\n}\n","/**\n * Shared `ok: true/false` result vocabulary (Rust's `Result` in spirit,\n * spread payloads in shape): every site that answers \"did it work?\" —\n * detections, parses, validations, benchmark runs — uses one type\n * family, one pair of constructors and one pair of guards instead of\n * hand-rolling its own union.\n *\n * The payloads are spread onto the marker rather than carried in a\n * `value`/`error` channel: `ok({ start, end })` is literally\n * `{ ok: true, start, end }`. This keeps every existing consumer's\n * field access (`r.start`, `r.code`, `r.failure`) and every `toEqual`\n * assertion working unchanged, and lets the plugin-facing `{ ok: true }`\n * literal stay the contract.\n */\nexport type Ok<TPayload extends object = object> = {\n\treadonly ok: true\n} & TPayload\nexport type Err<TPayload extends object = object> = {\n\treadonly ok: false\n} & TPayload\nexport type Result<TOk extends object = object, TErr extends object = object> =\n\t| Ok<TOk>\n\t| Err<TErr>\n\n/**\n * Build the success variant; `ok()` alone yields `{ ok: true }`. The\n * cast is the constructor boundary: the runtime value is exactly\n * `{ ok: true, ...payload }`, which the generic spread cannot prove.\n */\nexport function ok<TPayload extends object = object>(\n\tpayload?: TPayload,\n): Ok<TPayload> {\n\treturn { ok: true, ...payload } as Ok<TPayload>\n}\n\n/**\n * Build the failure variant; `err()` alone yields `{ ok: false }`. See\n * {@link ok} for the constructor-boundary cast.\n */\nexport function err<TPayload extends object = object>(\n\tpayload?: TPayload,\n): Err<TPayload> {\n\treturn { ok: false, ...payload } as Err<TPayload>\n}\n\n/** Narrow a result to its success variant. */\nexport function isOk<TOk extends object, TErr extends object>(\n\tresult: Result<TOk, TErr>,\n): result is Ok<TOk> {\n\treturn result.ok === true\n}\n\n/** Narrow a result to its failure variant. */\nexport function isErr<TOk extends object, TErr extends object>(\n\tresult: Result<TOk, TErr>,\n): result is Err<TErr> {\n\treturn result.ok === false\n}\n\n/**\n * Destructure a result through one of two handlers — the pattern-match\n * combinator. Both handlers must produce `R`; the chosen one receives\n * the spread payload of its variant.\n */\nexport function matchResult<TOk extends object, TErr extends object, R>(\n\tresult: Result<TOk, TErr>,\n\thandlers: {\n\t\treadonly ok: (payload: Ok<TOk>) => R\n\t\treadonly err: (payload: Err<TErr>) => R\n\t},\n): R {\n\treturn isOk(result) ? handlers.ok(result) : handlers.err(result)\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/plugin-asset.ts","../src/media-exts.ts","../src/plugin-definition.ts","../src/result.ts"],"names":["err"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * The plugin asset contract — the download / read / delete surface of a\n * plugin's own \"vault\". The vault is a host-reserved namespace inside the\n * plugin's installed directory (`<plugin-dir>/vault/`) that the host\n * manages on the plugin's behalf: data lands there only through the\n * user-consented download API, and nothing a plugin ships in its zip can\n * ever be overwritten by downloading (see the vault-confined `dest`\n * rules).\n *\n * Both sides of the plugin speak the same shapes: the server-side\n * `ResourceAPI` (main.js hooks) and the iframe `WebPluginAPI` (render)\n * call the same four methods with the same request/result vocabulary.\n * `download` also accepts an **array of requests** — one batched call is\n * ONE consent question (the dialog lists every item) and is all-or-nothing\n * (results arrive in request order; any failure commits nothing).\n * All methods are gated by the manifest `download` permission and\n * denied inside the sandbox when the manifest does not declare it.\n *\n * Error convention (fixed rule): **classification uses `Result`,\n * API calls throw.** `detect` (and other classifiers) return a\n * {@link Result}; every other API method rejects with an `Error` whose\n * `name` carries the machine-readable code. Plugins branch on\n * {@link isPluginAssetError} — never parse messages.\n *\n * Runtime limits live behind `@hoardodile/sdk-types/plugin-asset-limits`;\n * this module exports the types and the error helpers only.\n */\nimport type { PluginAssetErrorName } from \"./plugin-asset-limits.ts\"\n\nexport type { PluginAssetErrorName }\n\n/**\n * A download request: the plugin declares the plaintext URL, the vault\n * destination, and (optionally) an integrity pin plus a reason for the\n * consent dialog. `dest` is vault-relative only — it must resolve under\n * `<plugin-dir>/vault/` and can never reach the plugin's own bundled\n * files (`main.js`, `index.html`, `assets/`, ...).\n */\nexport type PluginDownloadRequest = {\n\t/** Absolute `http(s)` URL to fetch. Shown verbatim in the consent dialog. */\n\treadonly url: string\n\t/**\n\t * Vault-relative destination path (`\"runtime/live2d.min.js\"`). The host\n\t * resolves it inside the plugin vault and rejects absolute paths,\n\t * `..` traversal, path separators crossing segments, and reserved\n\t * names — before any network request is made.\n\t */\n\treadonly dest: string\n\t/**\n\t * Optional SRI-style integrity pin (64 lowercase hex chars). When\n\t * present the host verifies the downloaded bytes against it and\n\t * discards a mismatch, so a tampered or corrupted response can\n\t * never be stored.\n\t */\n\treadonly sha256?: string\n\t/** Optional short rationale shown in the consent dialog (plugin-authored copy). */\n\treadonly reason?: string\n}\n\n/**\n * Result of {@link PluginDownloadRequest}: the stored file's identity.\n * `cached` is true when the destination already existed — the host\n * answered from the vault without any dialog and without touching the\n * network (downloads are \"ensure present\", never unconditional).\n */\nexport type PluginDownloadResult = {\n\t/** The vault-relative destination that was resolved. */\n\treadonly path: string\n\treadonly sizeBytes: number\n\t/** sha256 of the stored bytes (host-computed, always present). */\n\treadonly sha256: string\n\t/** True when the file already existed and no consent/network was needed. */\n\treadonly cached: boolean\n}\n\n/**\n * Result of {@link ResourceAPI.deleteAsset} / `WebPluginAPI.deleteAsset`.\n * Deletion is idempotent: removing nothing is not an error.\n */\nexport type PluginAssetDeleteResult = {\n\t/** True when a file was actually removed. */\n\treadonly existed: boolean\n}\n\n/**\n * Error thrown by the asset methods. The name survives both wire\n * boundaries (worker IPC and the iframe postMessage bridge), so plugin\n * code can branch on `err.name` without parsing messages:\n *\n * - `DENIED` — the user declined the consent dialog, or consent timed out.\n * - `UNAVAILABLE` — this runtime has no consent channel (CLI, workbench,\n * offline mock) or the server is in read-only archive mode.\n * - `POLICY` — the host rejected the request before downloading:\n * manifest lacks the `download` permission, the URL or `dest` is not\n * allowed, the destination is a directory, or a quota would be\n * exceeded. Also used for reserved-name conflicts.\n *\n * Transport/network failures keep their own error names (e.g. socket\n * errors) and are not part of this vocabulary.\n */\nexport class PluginAssetError extends Error {\n\tconstructor(\n\t\treadonly code: PluginAssetErrorName,\n\t\tmessage: string,\n\t) {\n\t\tsuper(message)\n\t\tthis.name = code\n\t}\n}\n\n/**\n * Narrow an asset error to a machine-readable name. Works across the\n * RPC boundaries because both preserve `Error.name` (the worker IPC and\n * the iframe bridge carry the name explicitly) — the instance is often\n * lost in transit, so the check keys on the name alone.\n */\nexport function isPluginAssetError(\n\terr: unknown,\n\tname: PluginAssetErrorName,\n): err is PluginAssetError {\n\treturn err instanceof Error && err.name === name\n}\n\n/** Build a `PluginAssetError` carrying the given machine-readable name. */\nexport function pluginAssetError(\n\tname: PluginAssetErrorName,\n\tmessage: string,\n): PluginAssetError {\n\treturn new PluginAssetError(name, message)\n}\n","/**\n * Canonical media-type knowledge: the extension sets, the extension →\n * MIME table and the MIME → media-kind mapping shared by content\n * plugins, the runtime host's sniffer and the server's classification\n * pipeline.\n *\n * Extensions are a **hint**, never the verdict: `ResourceAPI.sniff`\n * reads the file's magic bytes and only falls back to the tables here\n * when the content carries no recognizable signature (text formats).\n * The sets below therefore answer \"which extensions do we expect to\n * decode\", not \"what is this file\".\n *\n * Lower-case, with leading dot — match the output of\n * `path.extname(name).toLowerCase()`.\n *\n * Adding a new extension here widens classification everywhere at\n * once. Before adding, verify:\n * - sharp can extract width/height (image)\n * - ffprobe can read width/height/duration (video)\n * - ffprobe can read the stream/format metadata (audio), and the\n * extension has an entry in `AUDIO_FFMPEG_INPUT_FORMAT`\n * - the extension has an entry in {@link EXT_MIME}\n * - the gallery plugin's transcode-required set reflects the format\n * (browser-renderable originals stay native; HEIC/TIFF need the\n * sharp preview pipeline)\n */\nexport const IMAGE_EXTS: ReadonlySet<string> = new Set([\n\t\".jpg\",\n\t\".jpeg\",\n\t\".png\",\n\t\".webp\",\n\t\".gif\",\n\t\".bmp\",\n\t\".avif\",\n\t\".heic\",\n\t\".heif\",\n\t\".tif\",\n\t\".tiff\",\n\t\".svg\",\n\t\".jp2\",\n\t\".j2k\",\n\t\".jpx\",\n])\n\nexport const VIDEO_EXTS: ReadonlySet<string> = new Set([\n\t\".mp4\",\n\t\".webm\",\n\t\".mov\",\n\t\".mkv\",\n\t\".m4v\",\n\t\".avi\",\n\t\".3gp\",\n])\n\nexport const AUDIO_EXTS: ReadonlySet<string> = new Set([\n\t\".mp3\",\n\t\".flac\",\n\t\".ogg\",\n\t\".m4a\",\n\t\".wav\",\n\t\".opus\",\n\t\".aac\",\n])\n\n/**\n * Video containers ffmpeg can demux from a forward-only pipe (matroska,\n * avi). ISO-BMFF files (.mp4/.mov/.m4v) keep their moov index at the end\n * of the file, so a stream attempt on a zip-entry source is guaranteed to\n * fail after burning a full probesize read — consumers (thumb pipeline,\n * cover probing) send those straight to the materialized entry instead.\n */\nexport const STREAMABLE_VIDEO_EXTS: ReadonlySet<string> = new Set([\n\t\".webm\",\n\t\".mkv\",\n\t\".avi\",\n])\n\n/**\n * ffmpeg `-f` container name for piped audio bytes (no filename hint).\n * `.opus` files are Ogg containers; `.m4a` is ISO-BMFF, demuxed by the\n * mp4 demuxer.\n */\nexport const AUDIO_FFMPEG_INPUT_FORMAT: Readonly<Record<string, string>> = {\n\t\".mp3\": \"mp3\",\n\t\".flac\": \"flac\",\n\t\".ogg\": \"ogg\",\n\t\".opus\": \"ogg\",\n\t\".wav\": \"wav\",\n\t\".m4a\": \"mp4\",\n\t\".aac\": \"aac\",\n}\n\n/**\n * ffmpeg `-f` container name keyed by **sniffed MIME type**, for piped\n * sources that have no filename ffprobe could key off. Content-derived\n * routing is what lets a mislabelled file still demux correctly, so\n * this table — not the extension one — drives `ResourceAPI.probe`.\n *\n * Several spellings map to the same container because magic-byte\n * matchers and IANA disagree on the canonical name (`audio/wav` vs\n * `audio/x-wav`, `video/vnd.avi` vs `video/x-msvideo`).\n */\nexport const MIME_FFMPEG_INPUT_FORMAT: Readonly<Record<string, string>> = {\n\t\"video/mp4\": \"mp4\",\n\t\"video/quicktime\": \"mov\",\n\t\"video/webm\": \"webm\",\n\t\"video/x-matroska\": \"matroska\",\n\t\"video/matroska\": \"matroska\",\n\t\"video/vnd.avi\": \"avi\",\n\t\"video/x-msvideo\": \"avi\",\n\t\"video/avi\": \"avi\",\n\t\"video/ogg\": \"ogg\",\n\t\"audio/mpeg\": \"mp3\",\n\t\"audio/mp3\": \"mp3\",\n\t\"audio/flac\": \"flac\",\n\t\"audio/x-flac\": \"flac\",\n\t\"audio/ogg\": \"ogg\",\n\t\"audio/opus\": \"ogg\",\n\t\"audio/vorbis\": \"ogg\",\n\t\"audio/wav\": \"wav\",\n\t\"audio/x-wav\": \"wav\",\n\t\"audio/vnd.wave\": \"wav\",\n\t\"audio/wave\": \"wav\",\n\t\"audio/mp4\": \"mp4\",\n\t\"audio/x-m4a\": \"mp4\",\n\t\"audio/aac\": \"aac\",\n\t\"video/3gpp\": \"mp4\",\n\t\"application/ogg\": \"ogg\",\n\t\"application/x-matroska\": \"matroska\",\n\t\"application/mp4\": \"mp4\",\n}\n\n/**\n * The audio mirror of {@link STREAMABLE_VIDEO_EXTS}: containers whose\n * headers lead the file, so ffmpeg/ffprobe can demux them from a\n * forward-only pipe. `.m4a` is ISO-BMFF with a trailing moov index, so\n * it must be probed from a materialized (seekable) entry.\n */\nexport const STREAMABLE_AUDIO_EXTS: ReadonlySet<string> = new Set([\n\t\".mp3\",\n\t\".flac\",\n\t\".ogg\",\n\t\".opus\",\n\t\".wav\",\n\t\".aac\",\n])\n\n/**\n * Media families a file can belong to. `other` covers everything the\n * media pipeline does not decode (text, documents, archives, ...) — it\n * is a real answer, not a failure.\n */\nexport const MEDIA_KINDS = [\"image\", \"video\", \"audio\", \"other\"] as const\n\nexport type MediaKind = (typeof MEDIA_KINDS)[number]\n\n/**\n * Extension → canonical MIME type. Used as the *fallback* branch of\n * content sniffing: magic-byte detection covers binary media, while\n * text-based formats (`.txt`, `.md`, `.csv`, subtitles, ...) carry no\n * signature and can only be named by their extension.\n */\nexport const EXT_MIME: Readonly<Record<string, string>> = {\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".png\": \"image/png\",\n\t\".webp\": \"image/webp\",\n\t\".gif\": \"image/gif\",\n\t\".bmp\": \"image/bmp\",\n\t\".avif\": \"image/avif\",\n\t\".heic\": \"image/heic\",\n\t\".heif\": \"image/heif\",\n\t\".tif\": \"image/tiff\",\n\t\".tiff\": \"image/tiff\",\n\t\".jp2\": \"image/jp2\",\n\t\".j2k\": \"image/jp2\",\n\t\".jpx\": \"image/jp2\",\n\t\".mp4\": \"video/mp4\",\n\t\".m4v\": \"video/mp4\",\n\t\".webm\": \"video/webm\",\n\t\".mov\": \"video/quicktime\",\n\t\".mkv\": \"video/x-matroska\",\n\t\".avi\": \"video/vnd.avi\",\n\t\".3gp\": \"video/3gpp\",\n\t\".mp3\": \"audio/mpeg\",\n\t\".flac\": \"audio/flac\",\n\t\".ogg\": \"audio/ogg\",\n\t\".opus\": \"audio/opus\",\n\t\".m4a\": \"audio/mp4\",\n\t\".wav\": \"audio/wav\",\n\t\".aac\": \"audio/aac\",\n\t\".txt\": \"text/plain\",\n\t\".md\": \"text/markdown\",\n\t\".csv\": \"text/csv\",\n\t\".json\": \"application/json\",\n\t\".xml\": \"text/xml\",\n\t\".html\": \"text/html\",\n\t\".htm\": \"text/html\",\n\t\".svg\": \"image/svg+xml\",\n\t\".srt\": \"application/x-subrip\",\n\t\".vtt\": \"text/vtt\",\n\t\".ass\": \"text/x-ssa\",\n\t\".epub\": \"application/epub+zip\",\n\t\".pdf\": \"application/pdf\",\n\t\".zip\": \"application/zip\",\n\t\".cbz\": \"application/vnd.comicbook+zip\",\n\t\".cbr\": \"application/vnd.comicbook-rar\",\n\t\".rar\": \"application/vnd.rar\",\n\t\".7z\": \"application/x-7z-compressed\",\n\t\".cb7\": \"application/x-7z-compressed\",\n\t\".tar\": \"application/x-tar\",\n\t\".cbt\": \"application/x-tar\",\n}\n\n/**\n * Container MIME types whose top-level type does not describe the\n * payload. Ogg and Matroska carry audio *or* video, and `application/*`\n * says nothing either way — the values here are the common case, and\n * `ResourceAPI.probe` overrides them with the stream layout ffprobe\n * actually reports.\n */\nexport const MIME_KIND_OVERRIDES: Readonly<Record<string, MediaKind>> = {\n\t\"application/ogg\": \"audio\",\n\t\"application/x-matroska\": \"video\",\n\t\"application/mp4\": \"video\",\n}\n\n/**\n * Media family of a MIME type: the override table first, then the\n * top-level type. Never throws — unknown types are `other`.\n */\nexport function mimeToKind(mime: string): MediaKind {\n\tconst normalized = mime.toLowerCase()\n\tconst override = MIME_KIND_OVERRIDES[normalized]\n\tif (override !== undefined) return override\n\tif (normalized.startsWith(\"image/\")) return \"image\"\n\tif (normalized.startsWith(\"video/\")) return \"video\"\n\tif (normalized.startsWith(\"audio/\")) return \"audio\"\n\treturn \"other\"\n}\n\n/** Canonical MIME type for an extension (leading dot), or `undefined`. */\nexport function extToMime(ext: string): string | undefined {\n\treturn EXT_MIME[ext.toLowerCase()]\n}\n","/**\n * The plugin definition contract — the single source of truth shared by\n * the authoring SDK (`@hoardodile/sdk-server`), the runtime host\n * (`@hoardodile/host`) and the worker sandbox. Everything here is pure\n * TypeScript with no node or DOM dependencies, so the same contract\n * serves browser-facing packages and node runtimes alike.\n */\nimport type { MediaKind } from \"./media-exts.ts\"\nimport { extToMime, mimeToKind } from \"./media-exts.ts\"\nimport type {\n\tPluginAssetDeleteResult,\n\tPluginDownloadRequest,\n\tPluginDownloadResult,\n} from \"./plugin-asset.ts\"\nimport { pluginAssetError } from \"./plugin-asset.ts\"\nimport type { ReadFileRange } from \"./read-range.ts\"\nimport type { Result } from \"./result.ts\"\n\nexport type { MediaKind }\n\n/**\n * Schema contract shared between server and web plugin APIs.\n * Declared once per plugin and used to type both `definePlugin` and\n * `WebPluginAPI`.\n */\nexport interface PluginSchema {\n\treadonly file?: unknown\n\treadonly sourceMeta?: unknown\n\treadonly searchMeta?: unknown\n\t/**\n\t * Plugin-defined payload the `detect` hook may carry on a\n\t * successful match. The host keeps the last payload and exposes it\n\t * to the plugin's other hooks as `api.context.detect` — classify\n\t * once in `detect` instead of rescanning in every hook. Declaring\n\t * this slot types the context; hooks must still handle the absent\n\t * case (`undefined`: fresh worker, or detect never matched).\n\t */\n\treadonly detect?: unknown\n\t/**\n\t * Plugin-defined anchor location data: the payload carried inside the\n\t * wire {@link AnchorData} envelope (see {@link anchorData}). Outgoing\n\t * anchors are typed by this slot and passed raw (e.g.\n\t * `createMessage({ anchor: { page } })`); incoming anchor data is\n\t * validated by the plugin's `decodeAnchor` (see `definePluginAPI` in\n\t * `@hoardodile/sdk-react`).\n\t */\n\treadonly anchor?: unknown\n}\n\n/**\n * Server plugin detection result: the shared result vocabulary, where a\n * match carries the schema's `detect` payload (when one is declared)\n * and a miss carries its reasons. Plugins may return the literal\n * `{ ok: true } as const` / `{ ok: false, reasons }` shapes directly,\n * or use `ok()`/`err({ reasons })` from the result module.\n *\n * `TPayload` is the plugin's declared `detect` slot — the payload\n * spread onto a match is checked against it at compile time, so a\n * classification that drifts from the schema fails to build.\n */\nexport type Detection<TPayload extends object = object> = Result<\n\tTPayload,\n\t{ readonly reasons: readonly string[] }\n>\n\n/** Structured logger scoped to a single plugin. */\nexport type Logger = {\n\tinfo(message: string, data?: Record<string, unknown>): void\n\twarn(message: string, data?: Record<string, unknown>): void\n\terror(message: string, data?: Record<string, unknown>): void\n}\n\n/** Image probe payload. */\nexport type ImageInfo = {\n\treadonly width?: number\n\treadonly height?: number\n}\n\n/** Video probe payload. */\nexport type VideoInfo = {\n\treadonly width?: number\n\treadonly height?: number\n\treadonly durationMs?: number\n}\n\n/**\n * Embedded container tags carried by an audio file (ID3, Vorbis\n * comments, MP4 metadata atoms). Every field is optional — untagged\n * files are normal.\n */\nexport type AudioTags = {\n\treadonly title?: string\n\treadonly artist?: string\n\treadonly album?: string\n}\n\n/**\n * Embedded artwork carried by an audio file (ID3 APIC, FLAC PICTURE,\n * MP4 `covr`). Its presence is the signal that the host can extract a\n * real cover; the dimensions come from the same probe, so callers can\n * pre-size the cover slot without decoding the picture.\n */\nexport type AudioCoverArt = {\n\treadonly width?: number\n\treadonly height?: number\n}\n\n/**\n * Audio probe payload. Any field can be absent when the container does\n * not report it.\n */\nexport type AudioInfo = {\n\treadonly durationMs?: number\n\t/** Codec name of the first audio stream, e.g. `\"mp3\"`, `\"flac\"`. */\n\treadonly codec?: string\n\t/** Container bit rate in bits per second. */\n\treadonly bitRate?: number\n\t/** Sample rate of the first audio stream, in Hz. */\n\treadonly sampleRate?: number\n\t/** Channel count of the first audio stream. */\n\treadonly channels?: number\n\t/** Present only when the file embeds artwork. */\n\treadonly coverArt?: AudioCoverArt\n\treadonly tags?: AudioTags\n}\n\n/**\n * What a file's bytes say it is. Produced by {@link ResourceAPI.sniff}.\n *\n * `source` records who answered: `\"magic\"` means the file's own\n * signature was recognized (authoritative), `\"extension\"` means the\n * content carried no signature and the filename was used instead — the\n * normal outcome for text-based formats, which have no magic bytes.\n *\n * `kind` is provisional for container formats that can hold either\n * audio or video (Ogg, Matroska, ISO-BMFF); {@link ResourceAPI.probe}\n * overrides it with the stream layout actually found in the file.\n */\nexport type FileType = {\n\t/** Canonical MIME type, e.g. `\"image/jpeg\"`. */\n\treadonly mime: string\n\t/** Canonical extension for {@link mime}, with leading dot. */\n\treadonly ext: string\n\treadonly kind: MediaKind\n\treadonly source: \"magic\" | \"extension\"\n}\n\n/**\n * Everything one media probe pass can say about a file, discriminated\n * by the family the content really belongs to — the shape every\n * mainstream prober uses (ffprobe's `format` + `streams`, sharp's\n * `metadata()`, Tika's `MediaType`).\n *\n * `other` is a successful answer: the file was identified and is not\n * decodable media (text, documents, archives). `unknown` is the failure\n * branch and always carries a reason, so \"this host has no probe\n * backend\" is never confused with \"this file is not an image\":\n *\n * - `unsupported` — identified, but no backend decodes this format\n * - `unavailable` — the host wired no probe implementation (raw\n * directory APIs and test fixtures)\n * - `failed` — a backend ran and could not decode the bytes\n */\nexport type ProbeResult =\n\t| ({\n\t\t\treadonly kind: \"image\"\n\t\t\treadonly mime: string\n\t\t\t/** Multi-frame source: animated GIF / WebP / APNG / AVIF. */\n\t\t\treadonly animated: boolean\n\t } & ImageInfo)\n\t| ({ readonly kind: \"video\"; readonly mime: string } & VideoInfo)\n\t| ({ readonly kind: \"audio\"; readonly mime: string } & AudioInfo)\n\t| { readonly kind: \"other\"; readonly mime: string }\n\t| {\n\t\t\treadonly kind: \"unknown\"\n\t\t\treadonly reason: \"unsupported\" | \"unavailable\" | \"failed\"\n\t }\n\n/**\n * Perceptual hash kinds the host can compute for an image file.\n * `dhash` (difference hash) and `phash` (DCT-based perceptual hash)\n * are 64-bit similarity hashes compared by Hamming distance;\n * `sha256` is an exact byte hash. Animated images hash their first\n * frame. Plugins decide which kinds to request and which files to\n * hash — the host only provides the computation.\n */\nexport const IMAGE_HASH_KINDS = [\"sha256\", \"dhash\", \"phash\"] as const\nexport type ImageHashKind = (typeof IMAGE_HASH_KINDS)[number]\n\n/**\n * One content hash of a resource file, produced by the plugin's\n * `imageHashes` hook. `scope` is the archive-relative file path,\n * `type` the hash kind (`sha256`/`dhash`/`phash` or a plugin-defined\n * extension), `value` the lowercase hex digest. A resource may expose\n * several hashes (per file × per kind) or none.\n */\nexport type ImageHash = {\n\treadonly scope: string\n\treadonly type: string\n\treadonly value: string\n\t/** Bit length of the hash; required for perceptual kinds. */\n\treadonly bits?: number\n}\n\n/** Result of the `imageHashes` hook: hashes per file, possibly empty. */\nexport type ImageHashesResult = {\n\treadonly hashes: readonly ImageHash[]\n}\n\n/**\n * One file inside a container entry (zip/tar) as listed (or extracted)\n * by the plugin API. `path` is the entry's path inside the archive;\n * dimensions are present when the host probed the entry (image\n * backends) — a listing-only result carries no dimensions.\n */\nexport type ArchiveExtractionEntry = {\n\treadonly path: string\n\treadonly sizeBytes: number\n\treadonly kind: MediaKind\n\treadonly width?: number\n\treadonly height?: number\n\treadonly animated?: boolean\n}\n\n/**\n * A container listing without materialization — the cheap counterpart of\n * {@link ResourceAPI.extractArchive}. Carries entry names, sizes and\n * kinds only; no dimensions (probing those requires the bytes).\n */\nexport type ContainerListing = {\n\treadonly entries: readonly ArchiveExtractionEntry[]\n}\n\n/**\n * Result of {@link ResourceAPI.extractArchive}: the materialized\n * entries of a container entry. A completed extraction is marked by the\n * host's `index.json` manifest; extraction always writes the cache (the\n * host's `local/cache` is derived data, writable in every view mode).\n */\nexport type ArchiveExtraction = {\n\treadonly entries: readonly ArchiveExtractionEntry[]\n}\n\n/**\n * Resource-scoped API available to every plugin hook. All paths are\n * relative to the resource's source directory; the host resolves\n * absolute paths transparently.\n *\n * `TSchema` types the injected session context (`context.detect`); the\n * default keeps the API compatible with code that never reads it.\n */\nexport type ResourceAPI<TSchema extends PluginSchema = PluginSchema> = {\n\t/** Write an informational log entry. */\n\treadonly logInfo: (message: string, data?: Record<string, unknown>) => void\n\t/** Write a warning log entry. */\n\treadonly logWarn: (message: string, data?: Record<string, unknown>) => void\n\t/** Write an error log entry. */\n\treadonly logError: (message: string, data?: Record<string, unknown>) => void\n\t/**\n\t * List all regular-file names (flat list), in canonical display\n\t * order: the resource's explicit upload order when one exists (the\n\t * host's `.order` manifest), the natural name sort otherwise.\n\t * Plugins that need their own ordering should sort explicitly.\n\t *\n\t * This is the raw name list — the `listFiles` hook of the plugin\n\t * definition turns it into typed file entries.\n\t */\n\treadonly listFileNames: () => Promise<readonly string[]>\n\t/**\n\t * Read a regular file relative to the resource root.\n\t *\n\t * Without `range` the whole file is returned; hosts may reject\n\t * oversized full reads — pass a range (or use `readFileChunks` from\n\t * `@hoardodile/sdk-server/helpers`) for large files.\n\t *\n\t * Container addressing: a path of the form `outer!inner` reads the\n\t * file *inside* a zip/tar entry (e.g. `manga.cbz!Chapter 1/001.jpg`)\n\t * — the host streams the decompressed bytes. When `outer` is not a\n\t * container, or the inner entry is absent, the whole path is treated\n\t * as a literal filename.\n\t */\n\treadonly readFile: (\n\t\tpath: string,\n\t\trange?: ReadFileRange,\n\t) => Promise<Uint8Array>\n\t/**\n\t * Return the byte size of `path` without reading the file contents.\n\t * Resolves to `undefined` when the file does not exist or the artifact\n\t * is not yet committed. Supports container addressing (`outer!inner`).\n\t */\n\treadonly statFile: (\n\t\tpath: string,\n\t) => Promise<{ readonly sizeBytes: number } | undefined>\n\t/**\n\t * Batch {@link statFile}: resolves every path in one host round-trip\n\t * (positions preserved). Prefer this over a per-file fan-out of\n\t * `statFile` when statting a whole archive — one RPC instead of N.\n\t */\n\treadonly statFiles: (\n\t\tpaths: readonly string[],\n\t) => Promise<readonly ({ readonly sizeBytes: number } | undefined)[]>\n\t/**\n\t * Identify the file at `path` from its content: magic-byte\n\t * detection, falling back to the extension only for formats that\n\t * carry no signature (text, subtitles). Resolves to `undefined` when\n\t * neither can name the file. Supports container addressing.\n\t *\n\t * This is the cheap call — it reads a small header window, never\n\t * decodes. Use it to route work; use {@link probe} when you need\n\t * dimensions, duration or stream details.\n\t */\n\treadonly sniff: (path: string) => Promise<FileType | undefined>\n\t/**\n\t * Decode the media metadata of `path` in one pass, routed by\n\t * {@link sniff} rather than by the filename: images resolve through\n\t * sharp, audio and video through ffprobe (which also settles\n\t * ambiguous containers — an `.ogg` holding only audio streams comes\n\t * back as `kind: \"audio\"`). Supports container addressing.\n\t *\n\t * Always resolves, never rejects. Non-media files answer\n\t * `{ kind: \"other\" }`; the `unknown` branch carries a `reason` that\n\t * distinguishes \"no backend wired\" (`unavailable`, what raw\n\t * directory APIs and fixtures return) from a real decode failure.\n\t */\n\treadonly probe: (path: string) => Promise<ProbeResult>\n\t/**\n\t * Stream-hash the file at `path` (any file kind). Rejects when the\n\t * file is missing or the read fails; the host streams the entry so\n\t * arbitrarily large files are safe. Supports container addressing.\n\t */\n\treadonly hashBytes: (path: string, algo: \"md5\" | \"sha256\") => Promise<string>\n\t/**\n\t * Compute the requested hashes of the image at `path` in one pass:\n\t * `sha256` from the raw bytes, `dhash`/`phash` from a decoded\n\t * grayscale rendition (animated images use their first frame).\n\t * Resolves to `undefined` when the file is not a decodable image;\n\t * `kinds` names a subset of {@link IMAGE_HASH_KINDS} and the result\n\t * carries exactly those keys. Supports container addressing.\n\t */\n\treadonly computeImageHashes: (\n\t\tpath: string,\n\t\tkinds: readonly ImageHashKind[],\n\t) => Promise<Readonly<Record<ImageHashKind, string>> | undefined>\n\t/**\n\t * List the file entries of a container entry (zip/tar) without\n\t * materializing anything — the cheap call for metadata-only needs\n\t * (detect, card counts). Rejects when `filename` is not a supported\n\t * container.\n\t */\n\treadonly listContainer: (filename: string) => Promise<ContainerListing>\n\t/**\n\t * Materialize the contents of a container entry (zip/tar) into the\n\t * host's extraction cache so the browser can serve the inner files\n\t * over plain URLs. `filename` is a literal container entry — the\n\t * cache holds one directory per archive with the inner paths\n\t * preserved, plus a completion manifest.\n\t *\n\t * Idempotent: an already-materialized archive re-lists from the\n\t * manifest without re-extracting. Rejects when the entry is not a\n\t * supported container, exceeds the host's byte/entry budgets, or\n\t * when this host wires no extraction cache (test fixtures, raw\n\t * directory APIs).\n\t */\n\treadonly extractArchive: (filename: string) => Promise<ArchiveExtraction>\n\t/**\n\t * Ensure remote assets exist in the plugin's own vault — one call with\n\t * one request, or one call with an array of requests. When `dest` is\n\t * already present the host answers `cached: true` without any dialog\n\t * and without touching the network; otherwise the host asks the user\n\t * (the web app shows the shared consent dialog with the URLs\n\t * verbatim) and downloads on approval.\n\t *\n\t * An array is ONE consent question for the WHOLE batch (the dialog\n\t * lists every item) and is all-or-nothing: any failure discards every\n\t * staged file and rejects with the first error, so nothing is\n\t * partially committed. Results arrive in request order with `cached`\n\t * items keeping their positions. Cap: {@link PLUGIN_ASSET_BATCH_MAX_ITEMS}\n\t * items per call. The file always lands inside\n\t * `<plugin-dir>/vault/` — `dest` is vault-relative and can never\n\t * reach the plugin's bundled files.\n\t *\n\t * Gated by the manifest `download` permission; rejections carry a\n\t * machine-readable {@link PluginAssetErrorName} in `err.name`\n\t * (`DENIED` / `UNAVAILABLE` / `POLICY`).\n\t */\n\treadonly download: ((\n\t\trequest: PluginDownloadRequest,\n\t) => Promise<PluginDownloadResult>) &\n\t\t((\n\t\t\trequests: readonly PluginDownloadRequest[],\n\t\t) => Promise<readonly PluginDownloadResult[]>)\n\t/**\n\t * Byte size of a vault file, or `undefined` when absent. The cheap\n\t * presence check on top of which `download` resolves cached hits.\n\t */\n\treadonly statAsset: (\n\t\tpath: string,\n\t) => Promise<{ readonly sizeBytes: number } | undefined>\n\t/** Read a vault file's bytes (bounded by the same cap as {@link readFile}). */\n\treadonly readAsset: (path: string) => Promise<Uint8Array>\n\t/**\n\t * Remove a vault file; idempotent (absent files answer\n\t * `{ existed: false }`). The plugin decides the vault's own\n\t * lifecycle — e.g. cleaning stale layouts after a plugin update.\n\t * No user consent is required: nothing leaves the host. Directories\n\t * and paths outside the vault are rejected (`POLICY`).\n\t */\n\treadonly deleteAsset: (path: string) => Promise<PluginAssetDeleteResult>\n\t/**\n\t * Session context injected by the host. `detect` carries the payload\n\t * the plugin's `detect` hook returned on its last successful match\n\t * (worker-session scope): the one-pass classification every other\n\t * hook can build on. `undefined` when detect has not matched in this\n\t * session — a fresh worker — so hooks must always handle the absent\n\t * case by re-deriving.\n\t */\n\treadonly context: { readonly detect: TSchema[\"detect\"] | undefined }\n}\n\n/**\n * Declarative description of a content plugin. Plugins export an instance\n * of this shape as their default export; the host injects the resource\n * API at call time and never invokes a factory function.\n */\nexport type PluginDefinition<TSchema extends PluginSchema = PluginSchema> = {\n\t/**\n\t * Detect whether this plugin applies to the current resource. A\n\t * successful match may carry a payload — `ok({ ...shape })` — which\n\t * the host keeps and exposes to the other hooks as\n\t * `api.context.detect`. The payload is checked against the schema's\n\t * `detect` slot (when one is declared).\n\t */\n\treadonly detect: (\n\t\tapi: ResourceAPI<TSchema>,\n\t) => Promise<Detection<TSchema[\"detect\"] & object>>\n\t/** Optional source metadata builder. */\n\treadonly sourceMeta?: (\n\t\tapi: ResourceAPI<TSchema>,\n\t) => Promise<TSchema[\"sourceMeta\"] | undefined>\n\t/** Optional search metadata builder. */\n\treadonly searchMeta?: (\n\t\tapi: ResourceAPI<TSchema>,\n\t) => Promise<TSchema[\"searchMeta\"] | undefined>\n\t/** Optional local cover source resolver. */\n\treadonly coverLocal?: (\n\t\tapi: ResourceAPI<TSchema>,\n\t) => Promise<string | undefined>\n\t/**\n\t * Optional custom file list builder. Results are cached verbatim in a\n\t * sidecar. When absent the host falls back to a bare list of source\n\t * filenames.\n\t */\n\treadonly listFiles?: (\n\t\tapi: ResourceAPI<TSchema>,\n\t) => Promise<readonly TSchema[\"file\"][]>\n\t/**\n\t * Optional content hashes for duplicate detection and image\n\t * similarity. The plugin decides the policy — which files to hash\n\t * and which kinds — by calling the API's hash primitives; a plugin\n\t * facing image-less resources simply omits this hook. Returning\n\t * `undefined` (or a hook error) keeps the resource's hash rows empty.\n\t */\n\treadonly imageHashes?: (\n\t\tapi: ResourceAPI<TSchema>,\n\t) => Promise<ImageHashesResult | undefined>\n}\n\n/** Plugin hook names the host can invoke, in contract order. */\nexport const HOOK_NAMES = [\n\t\"detect\",\n\t\"sourceMeta\",\n\t\"searchMeta\",\n\t\"coverLocal\",\n\t\"listFiles\",\n\t\"imageHashes\",\n] as const\n\nexport type HookName = (typeof HOOK_NAMES)[number]\n\nfunction isAsyncFunction(value: unknown): boolean {\n\treturn (\n\t\ttypeof value === \"function\" && value.constructor.name === \"AsyncFunction\"\n\t)\n}\n\n/**\n * Freeze and return a plugin definition. Runs shape validation upfront so\n * a malformed plugin fails at load time with a friendly message instead\n * of misbehaving at hook time.\n */\nexport function definePlugin<TSchema extends PluginSchema = PluginSchema>(\n\tdefinition: PluginDefinition<TSchema>,\n): PluginDefinition<TSchema> {\n\tassertPluginShape(definition)\n\treturn Object.freeze({ ...definition })\n}\n\n/**\n * Validate that a value satisfies the structural contract of a\n * {@link PluginDefinition}: only known hooks, all hooks async functions,\n * `detect` required. Does NOT exercise behaviour.\n */\nexport function assertPluginShape(\n\tvalue: unknown,\n): asserts value is PluginDefinition {\n\tif (typeof value !== \"object\" || value === null) {\n\t\tthrow new Error(\"PluginDefinition: expected an object with hook functions\")\n\t}\n\n\tconst definition = value as Record<string, unknown>\n\n\tconst knownHooks = new Set<string>(HOOK_NAMES)\n\tconst unknown = Object.keys(definition).filter((key) => !knownHooks.has(key))\n\tif (unknown.length > 0) {\n\t\tthrow new Error(\n\t\t\t`PluginDefinition: unknown hook(s) ${unknown.map((k) => `\"${k}\"`).join(\", \")} — expected one of: ${HOOK_NAMES.join(\", \")}`,\n\t\t)\n\t}\n\n\tfor (const hook of HOOK_NAMES) {\n\t\tconst entry = definition[hook]\n\t\tif (entry === undefined) {\n\t\t\tif (hook === \"detect\") {\n\t\t\t\tthrow new Error(\"PluginDefinition: missing detect()\")\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif (!isAsyncFunction(entry)) {\n\t\t\tconst kind =\n\t\t\t\ttypeof entry === \"function\" ? \"a synchronous function\" : typeof entry\n\t\t\tthrow new Error(\n\t\t\t\t`PluginDefinition: \"${hook}\" must be an async function (got ${kind}) — hooks may do heavy work and the host awaits every hook, so declare it with \\`async\\`.`,\n\t\t\t)\n\t\t}\n\t}\n}\n\n/**\n * Convenience wrapper that builds a failing plugin definition. Used by\n * the host when a plugin directory is missing or its main.js cannot be\n * loaded.\n */\nexport function createFailingPlugin(\n\treasons: readonly string[],\n): PluginDefinition {\n\treturn definePlugin({\n\t\tdetect: async () => ({ ok: false, reasons }),\n\t})\n}\n\n/** Type guard for the success branch of a {@link Detection}. */\nexport function isDetected(\n\tdetection: Detection,\n): detection is { readonly ok: true } {\n\treturn detection.ok\n}\n\n/** Type guard for the failure branch of a {@link Detection}. */\nexport function isMissed(\n\tdetection: Detection,\n): detection is { readonly ok: false; readonly reasons: readonly string[] } {\n\treturn !detection.ok\n}\n\n/** Declarative configuration for a {@link ResourceAPI} fixture. */\nexport type ResourceAPIFixtureConfig<\n\tTSchema extends PluginSchema = PluginSchema,\n> = {\n\t/** File names returned by `listFileNames`. */\n\treadonly files?: readonly string[]\n\t/** File contents returned by `readFile`. */\n\treadonly contents?: Readonly<Record<string, string | Uint8Array>>\n\t/**\n\t * `sniff` results keyed by file path — `{ \"a.jpg\": … }` matches only\n\t * that file, keys starting with a dot match by extension suffix\n\t * (`{ \".mp4\": … }` applies to every .mp4 file, longest key wins),\n\t * and `{ \"\": … }` matches every path (the usual way to express a\n\t * default). Unconfigured paths fall back to the extension table,\n\t * exactly like the host's extension branch.\n\t */\n\treadonly types?: Readonly<Record<string, FileType | undefined>>\n\t/**\n\t * `probe` results keyed by file path (same matching rules as\n\t * {@link types}). Unconfigured paths mirror the host's routing:\n\t * identified non-media answers `{ kind: \"other\" }`, identified media\n\t * answers `{ kind: \"unknown\", reason: \"unavailable\" }` — the fixture\n\t * decodes nothing, so a hook that needs real dimensions belongs in a\n\t * sandbox test instead.\n\t */\n\treadonly probes?: Readonly<Record<string, ProbeResult | undefined>>\n\t/** Stat results. A plain value is used as the default for all paths. */\n\treadonly stats?:\n\t\t| Readonly<Record<string, { readonly sizeBytes: number } | undefined>>\n\t\t| { readonly sizeBytes: number }\n\t\t| undefined\n\t/** `hashBytes` results by path; a plain string is used for all paths. */\n\treadonly byteHashes?: Readonly<Record<string, string>> | string\n\t/**\n\t * `computeImageHashes` results by path. A plain record is used as the\n\t * default for all paths; absent paths resolve to `undefined`.\n\t */\n\treadonly imageHashes?:\n\t\t| Readonly<Record<string, ImageHashesResult>>\n\t\t| ImageHashesResult\n\t/**\n\t * `listContainer` results keyed by the archive filename. Absent\n\t * names reject, mirroring the host's \"not a supported archive\" error.\n\t */\n\treadonly containerListings?: Readonly<Record<string, ContainerListing>>\n\t/**\n\t * `extractArchive` results keyed by the archive filename. Absent\n\t * names reject, mirroring the host's \"not a supported archive\" error.\n\t */\n\treadonly extractions?: Readonly<Record<string, ArchiveExtraction>>\n\t/**\n\t * Vault file contents keyed by vault-relative path, backing\n\t * `statAsset` / `readAsset` / `deleteAsset` in the fixture.\n\t */\n\treadonly assetFiles?: Readonly<Record<string, string | Uint8Array>>\n\t/**\n\t * Handler for `download` (single request or batch of requests, typed\n\t * as the union — the fixture returns the matching shape). Absent\n\t * means the hosted runtime has no consent channel — `download`\n\t * rejects with `UNAVAILABLE`, exactly like the CLI, workbench and\n\t * offline mock hosts.\n\t */\n\treadonly downloadHandler?: (\n\t\trequest: PluginDownloadRequest | readonly PluginDownloadRequest[],\n\t) => Promise<PluginDownloadResult | readonly PluginDownloadResult[]>\n\t/**\n\t * Container addressing for the fixture: maps a virtual path\n\t * (`outer!inner`) to stat/sniff/probe results, so hooks that browse\n\t * inside archives can be tested without real archives. Matching rules\n\t * mirror {@link types}: exact path keys, dot fragments by suffix,\n\t * `{ \"\": … }` as the default.\n\t */\n\treadonly virtualEntries?: Readonly<Record<string, ArchiveExtractionEntry>>\n\t/**\n\t * Session context handed to hooks as `api.context` — mirrors the\n\t * host injecting the payload of a prior successful `detect`. Typed\n\t * by the schema generic when one is supplied.\n\t */\n\treadonly context?: { readonly detect?: TSchema[\"detect\"] }\n}\n\nfunction resolveKeyed<T>(\n\tpath: string,\n\ttable: Readonly<Record<string, T | undefined>> | undefined,\n): T | undefined {\n\tif (table === undefined) return undefined\n\t// Keys match the path exactly, except keys that start with a dot —\n\t// those are extension fragments matching any path ending with them\n\t// (`.mp4` applies to every .mp4 file). The longest fragment wins so\n\t// a shared default is never shadowed; the empty key `\"\"` is the\n\t// catch-all default. Plain-name keys never match by substring, so\n\t// `\"a.jpg\"` cannot hijack `\"ba.jpg\"`.\n\tif (Object.hasOwn(table, path)) return table[path]\n\tlet bestKey: string | undefined\n\tlet bestLen = -1\n\tfor (const key of Object.keys(table)) {\n\t\tif (key.length > bestLen && key.startsWith(\".\") && path.endsWith(key)) {\n\t\t\tbestKey = key\n\t\t\tbestLen = key.length\n\t\t}\n\t}\n\tif (bestKey !== undefined) return table[bestKey]\n\treturn table[\"\"]\n}\n\n/** Lower-cased extension from the last dot, or `\"\"` when there is none. */\nfunction lastExt(path: string): string {\n\tconst dot = path.lastIndexOf(\".\")\n\treturn dot === -1 ? \"\" : path.slice(dot).toLowerCase()\n}\n\n/**\n * Identify a file from its name alone: the extension branch of content\n * sniffing, exposed on its own because it is also the honest answer for\n * formats that carry no signature, and the shape test doubles need when\n * standing in for a real {@link ResourceAPI}.\n */\nexport function fileTypeFromName(path: string): FileType | undefined {\n\tconst ext = lastExt(path)\n\tconst mime = extToMime(ext)\n\tif (mime === undefined) return undefined\n\treturn { mime, ext, kind: mimeToKind(mime), source: \"extension\" }\n}\n\nfunction resolveValue<T>(\n\tpath: string,\n\tvalue: Readonly<Record<string, T | undefined>> | T | undefined,\n\tdefaultValue: T | undefined,\n): T | undefined {\n\tif (value === undefined || value === null) return defaultValue\n\tif (typeof value !== \"object\" || Array.isArray(value)) return value\n\t// Matching rules mirror {@link resolveKeyed}: exact keys, dot\n\t// fragments by suffix (longest wins), empty-key default.\n\tconst table = value as Readonly<Record<string, T | undefined>>\n\tif (Object.hasOwn(table, path)) return table[path]\n\tlet bestKey: string | undefined\n\tlet bestLen = -1\n\tfor (const key of Object.keys(table)) {\n\t\tif (key.length > bestLen && key.startsWith(\".\") && path.endsWith(key)) {\n\t\t\tbestKey = key\n\t\t\tbestLen = key.length\n\t\t}\n\t}\n\tif (bestKey !== undefined) return table[bestKey]\n\treturn table[\"\"] ?? defaultValue\n}\n\nfunction virtualStat(\n\tpath: string,\n\ttable:\n\t\t| Readonly<Record<string, ArchiveExtractionEntry | undefined>>\n\t\t| undefined,\n): { readonly sizeBytes: number } | undefined {\n\tconst entry = resolveKeyed(path, table)\n\treturn entry === undefined ? undefined : { sizeBytes: entry.sizeBytes }\n}\n\nfunction virtualType(\n\tpath: string,\n\ttable:\n\t\t| Readonly<Record<string, ArchiveExtractionEntry | undefined>>\n\t\t| undefined,\n): FileType | undefined {\n\tif (resolveKeyed(path, table) === undefined) return undefined\n\treturn fileTypeFromName(path.slice(path.lastIndexOf(\"!\") + 1))\n}\n\n/**\n * Create a mutable {@link ResourceAPI} fixture driven by a declarative\n * config. No filesystem involved — the standard way to unit-test plugin\n * hooks.\n *\n * Pass the plugin's schema as the generic\n * (`createResourceAPIFixture<MySchema>()`) so the returned api carries\n * the typed session context — the same shape schema-typed hooks receive\n * from the host.\n */\nexport function createResourceAPIFixture<\n\tTSchema extends PluginSchema = PluginSchema,\n>(\n\tinitialConfig: ResourceAPIFixtureConfig<TSchema> = {},\n): {\n\treadonly api: ResourceAPI<TSchema>\n\treadonly setConfig: (next: ResourceAPIFixtureConfig<TSchema>) => void\n} {\n\tlet config: ResourceAPIFixtureConfig<TSchema> = initialConfig\n\n\tfunction setConfig(next: ResourceAPIFixtureConfig<TSchema>): void {\n\t\tconfig = next\n\t}\n\n\tconst api: ResourceAPI<TSchema> = {\n\t\tlogInfo() {},\n\t\tlogWarn() {},\n\t\tlogError() {},\n\t\tcontext: { detect: config.context?.detect },\n\t\tasync listFileNames() {\n\t\t\treturn config.files ?? []\n\t\t},\n\t\tasync readFile(path, range) {\n\t\t\tconst content = config.contents?.[path]\n\t\t\tif (content === undefined) {\n\t\t\t\tthrow new Error(`ResourceAPIFixture: no content for \"${path}\"`)\n\t\t\t}\n\t\t\tconst bytes =\n\t\t\t\ttypeof content === \"string\"\n\t\t\t\t\t? new TextEncoder().encode(content)\n\t\t\t\t\t: content\n\t\t\tif (range === undefined) return bytes\n\t\t\t// Mirrors host semantics: the range is clamped to the content size.\n\t\t\treturn bytes.slice(range.start ?? 0, range.end)\n\t\t},\n\t\tasync statFile(path) {\n\t\t\treturn (\n\t\t\t\tresolveValue(path, config.stats, undefined) ??\n\t\t\t\tvirtualStat(path, config.virtualEntries)\n\t\t\t)\n\t\t},\n\t\tasync statFiles(paths) {\n\t\t\treturn Promise.all(\n\t\t\t\tpaths.map(\n\t\t\t\t\t(path) =>\n\t\t\t\t\t\tresolveValue(path, config.stats, undefined) ??\n\t\t\t\t\t\tvirtualStat(path, config.virtualEntries),\n\t\t\t\t),\n\t\t\t)\n\t\t},\n\t\tasync sniff(path) {\n\t\t\treturn (\n\t\t\t\tresolveKeyed(path, config.types) ??\n\t\t\t\tfileTypeFromName(path) ??\n\t\t\t\tvirtualType(path, config.virtualEntries)\n\t\t\t)\n\t\t},\n\t\tasync probe(path) {\n\t\t\tconst configured = resolveKeyed(path, config.probes)\n\t\t\tif (configured !== undefined) return configured\n\t\t\t// Unconfigured paths mirror the host's own routing: a file the\n\t\t\t// fixture cannot identify is unsupported, an identified\n\t\t\t// non-media file answers `other`, and identified media needs a\n\t\t\t// real decode the fixture has no backend for.\n\t\t\tconst type =\n\t\t\t\tresolveKeyed(path, config.types) ??\n\t\t\t\tfileTypeFromName(path) ??\n\t\t\t\tvirtualType(path, config.virtualEntries)\n\t\t\tif (type === undefined) return { kind: \"unknown\", reason: \"unsupported\" }\n\t\t\tif (type.kind === \"other\") return { kind: \"other\", mime: type.mime }\n\t\t\tconst virtual = resolveKeyed(path, config.virtualEntries)\n\t\t\tif (virtual !== undefined && type.kind === \"image\") {\n\t\t\t\treturn {\n\t\t\t\t\tkind: \"image\",\n\t\t\t\t\tmime: type.mime,\n\t\t\t\t\twidth: virtual.width,\n\t\t\t\t\theight: virtual.height,\n\t\t\t\t\tanimated: virtual.animated ?? false,\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { kind: \"unknown\", reason: \"unavailable\" }\n\t\t},\n\t\tasync hashBytes(path) {\n\t\t\tconst value = resolveValue(path, config.byteHashes, undefined)\n\t\t\tif (value === undefined) {\n\t\t\t\tthrow new Error(`ResourceAPIFixture: no byte hash for \"${path}\"`)\n\t\t\t}\n\t\t\treturn value\n\t\t},\n\t\tasync computeImageHashes(path, kinds) {\n\t\t\tconst result = resolveValue(path, config.imageHashes, undefined)\n\t\t\tif (result === undefined) return undefined\n\t\t\tconst hashes: Record<string, string> = {}\n\t\t\tfor (const entry of result.hashes) {\n\t\t\t\tif ((kinds as readonly string[]).includes(entry.type)) {\n\t\t\t\t\thashes[entry.type] = entry.value\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn hashes as Record<ImageHashKind, string>\n\t\t},\n\t\tasync extractArchive(filename) {\n\t\t\tconst configured = config.extractions?.[filename]\n\t\t\tif (configured === undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`ResourceAPIFixture: no extraction configured for \"${filename}\"`,\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn configured\n\t\t},\n\t\tasync listContainer(filename) {\n\t\t\tconst configured = config.containerListings?.[filename]\n\t\t\tif (configured === undefined) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`ResourceAPIFixture: no container listing configured for \"${filename}\"`,\n\t\t\t\t)\n\t\t\t}\n\t\t\treturn configured\n\t\t},\n\t\tdownload: (async (\n\t\t\trequest: PluginDownloadRequest | readonly PluginDownloadRequest[],\n\t\t) => {\n\t\t\tif (config.downloadHandler === undefined) {\n\t\t\t\tthrow pluginAssetError(\n\t\t\t\t\t\"UNAVAILABLE\",\n\t\t\t\t\t\"ResourceAPIFixture: no download handler configured\",\n\t\t\t\t)\n\t\t\t}\n\t\t\tconst result = await config.downloadHandler(request)\n\t\t\t// Mirror the real host: batch in → batch out, single in →\n\t\t\t// single out.\n\t\t\tif (Array.isArray(request)) {\n\t\t\t\treturn Array.isArray(result) ? result : [result]\n\t\t\t}\n\t\t\treturn Array.isArray(result) ? (result[0] ?? result) : result\n\t\t}) as ResourceAPI<TSchema>[\"download\"],\n\t\tasync statAsset(path) {\n\t\t\tconst content = resolveValue(path, config.assetFiles, undefined)\n\t\t\tif (content === undefined) return undefined\n\t\t\treturn { sizeBytes: fixtureContentBytes(content).byteLength }\n\t\t},\n\t\tasync readAsset(path) {\n\t\t\tconst content = resolveValue(path, config.assetFiles, undefined)\n\t\t\tif (content === undefined) {\n\t\t\t\tthrow new Error(`ResourceAPIFixture: no vault file \"${path}\"`)\n\t\t\t}\n\t\t\treturn fixtureContentBytes(content)\n\t\t},\n\t\tasync deleteAsset(path) {\n\t\t\tconst table = config.assetFiles\n\t\t\tif (table === undefined || !Object.hasOwn(table, path)) {\n\t\t\t\treturn { existed: false }\n\t\t\t}\n\t\t\t// Fixture config is immutable in spirit — a delete is simulated\n\t\t\t// by copying with the entry removed, so the next call sees it gone.\n\t\t\tconst next: Record<string, string | Uint8Array> = {}\n\t\t\tfor (const [key, value] of Object.entries(table)) {\n\t\t\t\tif (key !== path) next[key] = value\n\t\t\t}\n\t\t\tconfig = { ...config, assetFiles: next }\n\t\t\treturn { existed: true }\n\t\t},\n\t}\n\n\treturn { api, setConfig }\n}\n\n/** Convert a fixture content entry (string or bytes) to `Uint8Array`. */\nfunction fixtureContentBytes(content: string | Uint8Array): Uint8Array {\n\treturn typeof content === \"string\"\n\t\t? new TextEncoder().encode(content)\n\t\t: content\n}\n\n/** Return a minimal {@link Logger} for tests. */\nexport function stubLogger(overrides?: Partial<Logger>): Logger {\n\treturn {\n\t\tinfo() {},\n\t\twarn() {},\n\t\terror() {},\n\t\t...overrides,\n\t}\n}\n","/**\n * Shared `ok: true/false` result vocabulary (Rust's `Result` in spirit,\n * spread payloads in shape): every site that answers \"did it work?\" —\n * detections, parses, validations, benchmark runs — uses one type\n * family, one pair of constructors and one pair of guards instead of\n * hand-rolling its own union.\n *\n * The payloads are spread onto the marker rather than carried in a\n * `value`/`error` channel: `ok({ start, end })` is literally\n * `{ ok: true, start, end }`. This keeps every existing consumer's\n * field access (`r.start`, `r.code`, `r.failure`) and every `toEqual`\n * assertion working unchanged, and lets the plugin-facing `{ ok: true }`\n * literal stay the contract.\n */\nexport type Ok<TPayload extends object = object> = {\n\treadonly ok: true\n} & TPayload\nexport type Err<TPayload extends object = object> = {\n\treadonly ok: false\n} & TPayload\nexport type Result<TOk extends object = object, TErr extends object = object> =\n\t| Ok<TOk>\n\t| Err<TErr>\n\n/**\n * Build the success variant; `ok()` alone yields `{ ok: true }`. The\n * cast is the constructor boundary: the runtime value is exactly\n * `{ ok: true, ...payload }`, which the generic spread cannot prove.\n */\nexport function ok<TPayload extends object = object>(\n\tpayload?: TPayload,\n): Ok<TPayload> {\n\treturn { ok: true, ...payload } as Ok<TPayload>\n}\n\n/**\n * Build the failure variant; `err()` alone yields `{ ok: false }`. See\n * {@link ok} for the constructor-boundary cast.\n */\nexport function err<TPayload extends object = object>(\n\tpayload?: TPayload,\n): Err<TPayload> {\n\treturn { ok: false, ...payload } as Err<TPayload>\n}\n\n/** Narrow a result to its success variant. */\nexport function isOk<TOk extends object, TErr extends object>(\n\tresult: Result<TOk, TErr>,\n): result is Ok<TOk> {\n\treturn result.ok === true\n}\n\n/** Narrow a result to its failure variant. */\nexport function isErr<TOk extends object, TErr extends object>(\n\tresult: Result<TOk, TErr>,\n): result is Err<TErr> {\n\treturn result.ok === false\n}\n\n/**\n * Destructure a result through one of two handlers — the pattern-match\n * combinator. Both handlers must produce `R`; the chosen one receives\n * the spread payload of its variant.\n */\nexport function matchResult<TOk extends object, TErr extends object, R>(\n\tresult: Result<TOk, TErr>,\n\thandlers: {\n\t\treadonly ok: (payload: Ok<TOk>) => R\n\t\treadonly err: (payload: Err<TErr>) => R\n\t},\n): R {\n\treturn isOk(result) ? handlers.ok(result) : handlers.err(result)\n}\n"]}
|
|
@@ -29,7 +29,21 @@ type PluginPermissions = z.infer<typeof pluginPermissions>;
|
|
|
29
29
|
* resource's locale from this map.
|
|
30
30
|
*/
|
|
31
31
|
declare const localeString: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Icon reference, in exactly one of two static forms:
|
|
34
|
+
*
|
|
35
|
+
* - `<SolarGlyph>` — a Solar glyph name (kebab-case, or the legacy
|
|
36
|
+
* PascalCase spelling); the host renders it from its own bundled three
|
|
37
|
+
* weights (bold / boldDuotone / linear) and follows the user's icon
|
|
38
|
+
* style preference. Names outside the host's Solar index render
|
|
39
|
+
* nothing.
|
|
40
|
+
* - `<relative/path>` — an asset inside the plugin zip (`assets/icon.svg`),
|
|
41
|
+
* served from the plugin's own directory.
|
|
42
|
+
*
|
|
43
|
+
* `http(s):`/`data:` URIs and `..` path segments are rejected: the
|
|
44
|
+
* manifest stays plain static JSON and the host never fetches or
|
|
45
|
+
* executes anything an icon reference asks for.
|
|
46
|
+
*/
|
|
33
47
|
declare const iconRef: z.ZodString;
|
|
34
48
|
/**
|
|
35
49
|
* Corner template slots for one content kind. Templates are rendered by
|
|
@@ -147,6 +161,7 @@ declare const pluginManifest: z.ZodObject<{
|
|
|
147
161
|
description: z.ZodString;
|
|
148
162
|
icon: z.ZodOptional<z.ZodString>;
|
|
149
163
|
version: z.ZodString;
|
|
164
|
+
minAppVersion: z.ZodOptional<z.ZodString>;
|
|
150
165
|
permissions: z.ZodObject<{
|
|
151
166
|
sourceMeta: z.ZodDefault<z.ZodBoolean>;
|
|
152
167
|
searchMeta: z.ZodDefault<z.ZodBoolean>;
|
|
@@ -7,10 +7,16 @@
|
|
|
7
7
|
declare const PLUGIN_ASSET_DEST_MAX_LENGTH = 256;
|
|
8
8
|
/** Max length of the optional human `reason` shown in the consent dialog. */
|
|
9
9
|
declare const PLUGIN_ASSET_REASON_MAX_LENGTH = 200;
|
|
10
|
+
/**
|
|
11
|
+
* Max items in one batched `download([...])` call. One call = one consent
|
|
12
|
+
* ticket = one dialog listing every item; beyond this the host rejects
|
|
13
|
+
* with `POLICY` (a burst must not stack unbounded tickets per plugin).
|
|
14
|
+
*/
|
|
15
|
+
declare const PLUGIN_ASSET_BATCH_MAX_ITEMS = 16;
|
|
10
16
|
/** Expected shape of an SRI-style sha256 pin: 64 lowercase hex characters. */
|
|
11
17
|
declare const PLUGIN_ASSET_SHA256_PATTERN: RegExp;
|
|
12
18
|
/** The machine-readable asset error names, in contract order. */
|
|
13
19
|
declare const PLUGIN_ASSET_ERROR_NAMES: readonly ["DENIED", "UNAVAILABLE", "POLICY"];
|
|
14
20
|
type PluginAssetErrorName = (typeof PLUGIN_ASSET_ERROR_NAMES)[number];
|
|
15
21
|
|
|
16
|
-
export { PLUGIN_ASSET_DEST_MAX_LENGTH, PLUGIN_ASSET_ERROR_NAMES, PLUGIN_ASSET_REASON_MAX_LENGTH, PLUGIN_ASSET_SHA256_PATTERN, type PluginAssetErrorName };
|
|
22
|
+
export { PLUGIN_ASSET_BATCH_MAX_ITEMS, PLUGIN_ASSET_DEST_MAX_LENGTH, PLUGIN_ASSET_ERROR_NAMES, PLUGIN_ASSET_REASON_MAX_LENGTH, PLUGIN_ASSET_SHA256_PATTERN, type PluginAssetErrorName };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/plugin-asset-limits.ts
|
|
2
2
|
var PLUGIN_ASSET_DEST_MAX_LENGTH = 256;
|
|
3
3
|
var PLUGIN_ASSET_REASON_MAX_LENGTH = 200;
|
|
4
|
+
var PLUGIN_ASSET_BATCH_MAX_ITEMS = 16;
|
|
4
5
|
var PLUGIN_ASSET_SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
5
6
|
var PLUGIN_ASSET_ERROR_NAMES = [
|
|
6
7
|
"DENIED",
|
|
@@ -8,6 +9,6 @@ var PLUGIN_ASSET_ERROR_NAMES = [
|
|
|
8
9
|
"POLICY"
|
|
9
10
|
];
|
|
10
11
|
|
|
11
|
-
export { PLUGIN_ASSET_DEST_MAX_LENGTH, PLUGIN_ASSET_ERROR_NAMES, PLUGIN_ASSET_REASON_MAX_LENGTH, PLUGIN_ASSET_SHA256_PATTERN };
|
|
12
|
+
export { PLUGIN_ASSET_BATCH_MAX_ITEMS, PLUGIN_ASSET_DEST_MAX_LENGTH, PLUGIN_ASSET_ERROR_NAMES, PLUGIN_ASSET_REASON_MAX_LENGTH, PLUGIN_ASSET_SHA256_PATTERN };
|
|
12
13
|
//# sourceMappingURL=plugin-asset-limits.js.map
|
|
13
14
|
//# sourceMappingURL=plugin-asset-limits.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/plugin-asset-limits.ts"],"names":[],"mappings":";AAOO,IAAM,4BAAA,GAA+B;AAGrC,IAAM,8BAAA,GAAiC;
|
|
1
|
+
{"version":3,"sources":["../src/plugin-asset-limits.ts"],"names":[],"mappings":";AAOO,IAAM,4BAAA,GAA+B;AAGrC,IAAM,8BAAA,GAAiC;AAOvC,IAAM,4BAAA,GAA+B;AAGrC,IAAM,2BAAA,GAA8B;AAGpC,IAAM,wBAAA,GAA2B;AAAA,EACvC,QAAA;AAAA,EACA,aAAA;AAAA,EACA;AACD","file":"plugin-asset-limits.js","sourcesContent":["/**\n * Plugin asset runtime limits — the constants shared by the host, the\n * SDK validators and the tooling. Backed by the `./plugin-asset-limits`\n * subpath (plugin-facing constants never export from the root entry).\n */\n\n/** Max length of a vault-relative `dest` (a plugin path is bounded, not arbitrary). */\nexport const PLUGIN_ASSET_DEST_MAX_LENGTH = 256\n\n/** Max length of the optional human `reason` shown in the consent dialog. */\nexport const PLUGIN_ASSET_REASON_MAX_LENGTH = 200\n\n/**\n * Max items in one batched `download([...])` call. One call = one consent\n * ticket = one dialog listing every item; beyond this the host rejects\n * with `POLICY` (a burst must not stack unbounded tickets per plugin).\n */\nexport const PLUGIN_ASSET_BATCH_MAX_ITEMS = 16\n\n/** Expected shape of an SRI-style sha256 pin: 64 lowercase hex characters. */\nexport const PLUGIN_ASSET_SHA256_PATTERN = /^[0-9a-f]{64}$/\n\n/** The machine-readable asset error names, in contract order. */\nexport const PLUGIN_ASSET_ERROR_NAMES = [\n\t\"DENIED\",\n\t\"UNAVAILABLE\",\n\t\"POLICY\",\n] as const\n\nexport type PluginAssetErrorName = (typeof PLUGIN_ASSET_ERROR_NAMES)[number]\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as PluginPermissions } from './manifest-
|
|
1
|
+
import { d as PluginPermissions } from './manifest-JMXWSfKE.js';
|
|
2
2
|
import 'zod';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -50,7 +50,7 @@ declare const PLUGIN_CAPABILITY_GATES: {
|
|
|
50
50
|
readonly sandboxMethods: readonly ["listContainer", "extractArchive"];
|
|
51
51
|
};
|
|
52
52
|
readonly download: {
|
|
53
|
-
readonly description: "The plugin asset vault: user-consented downloads into the plugin's own vault/ plus the vault read/delete methods; denied by default and per-
|
|
53
|
+
readonly description: "The plugin asset vault: user-consented downloads into the plugin's own vault/ plus the vault read/delete methods; denied by default and per-request by the user. One batched call = one consent dialog listing every item (all-or-nothing), capped per call.";
|
|
54
54
|
readonly sandboxMethods: readonly ["download", "statAsset", "readAsset", "deleteAsset"];
|
|
55
55
|
};
|
|
56
56
|
};
|
|
@@ -20,7 +20,7 @@ var PLUGIN_CAPABILITY_GATES = {
|
|
|
20
20
|
sandboxMethods: ["listContainer", "extractArchive"]
|
|
21
21
|
},
|
|
22
22
|
download: {
|
|
23
|
-
description: "The plugin asset vault: user-consented downloads into the plugin's own vault/ plus the vault read/delete methods; denied by default and per-
|
|
23
|
+
description: "The plugin asset vault: user-consented downloads into the plugin's own vault/ plus the vault read/delete methods; denied by default and per-request by the user. One batched call = one consent dialog listing every item (all-or-nothing), capped per call.",
|
|
24
24
|
sandboxMethods: ["download", "statAsset", "readAsset", "deleteAsset"]
|
|
25
25
|
}
|
|
26
26
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/plugin-capabilities.ts"],"names":[],"mappings":";AA8BO,IAAM,uBAAA,GAA0B;AAAA,EACtC,UAAA,EAAY;AAAA,IACX,WAAA,EAAa;AAAA,GACd;AAAA,EACA,UAAA,EAAY;AAAA,IACX,WAAA,EAAa;AAAA,GACd;AAAA,EACA,OAAA,EAAS;AAAA,IACR,WAAA,EAAa;AAAA,GACd;AAAA,EACA,OAAA,EAAS;AAAA,IACR,WAAA,EAAa;AAAA,GACd;AAAA,EACA,WAAA,EAAa;AAAA,IACZ,WAAA,EACC;AAAA,GACF;AAAA,EACA,SAAA,EAAW;AAAA,IACV,WAAA,EACC,sGAAA;AAAA,IACD,cAAA,EAAgB,CAAC,eAAA,EAAiB,gBAAgB;AAAA,GACnD;AAAA,EACA,QAAA,EAAU;AAAA,IACT,WAAA,EACC,
|
|
1
|
+
{"version":3,"sources":["../src/plugin-capabilities.ts"],"names":[],"mappings":";AA8BO,IAAM,uBAAA,GAA0B;AAAA,EACtC,UAAA,EAAY;AAAA,IACX,WAAA,EAAa;AAAA,GACd;AAAA,EACA,UAAA,EAAY;AAAA,IACX,WAAA,EAAa;AAAA,GACd;AAAA,EACA,OAAA,EAAS;AAAA,IACR,WAAA,EAAa;AAAA,GACd;AAAA,EACA,OAAA,EAAS;AAAA,IACR,WAAA,EAAa;AAAA,GACd;AAAA,EACA,WAAA,EAAa;AAAA,IACZ,WAAA,EACC;AAAA,GACF;AAAA,EACA,SAAA,EAAW;AAAA,IACV,WAAA,EACC,sGAAA;AAAA,IACD,cAAA,EAAgB,CAAC,eAAA,EAAiB,gBAAgB;AAAA,GACnD;AAAA,EACA,QAAA,EAAU;AAAA,IACT,WAAA,EACC,8PAAA;AAAA,IACD,cAAA,EAAgB,CAAC,UAAA,EAAY,WAAA,EAAa,aAAa,aAAa;AAAA;AAEtE;AAqBO,IAAM,uBACZ,IAAI,GAAA;AAAA,EAEF,MAAA,CAAO,OAAA,CAAQ,uBAAuB,CAAA,CAIrC,OAAA;AAAA,IAAQ,CAAC,CAAC,UAAA,EAAY,IAAI,OAC1B,IAAA,CAAK,cAAA,IAAkB,EAAC,EAAG,GAAA;AAAA,MAC3B,CAAC,MAAA,KAAW,CAAC,MAAA,EAAQ,UAAU;AAAA;AAChC;AAEF","file":"plugin-capabilities.js","sourcesContent":["/**\n * The single permission→capability declaration: every manifest\n * permission key, what it gates, and which runtime layers enforce it.\n * Consumers read this table instead of hand-mirrored sets — a new\n * permission is declared once here, and the compile-time coverage\n * checks below make a missing declaration impossible.\n *\n * This module is pure TypeScript (no zod): it is type-driven off\n * {@link PluginPermissions} and consumed by the host sandbox, the\n * server domain and the tooling. Plugin bundles never import it.\n */\nimport type { PluginPermissions } from \"./manifest.ts\"\n\nexport type PluginCapabilityGate = {\n\t/** One-line contract description (mirrors the manifest schema docs). */\n\treadonly description: string\n\t/**\n\t * ResourceAPI method names gated at the sandbox RPC boundary. Absent\n\t * means the permission is enforced at the host/service layer only\n\t * (meta hooks, web routes).\n\t */\n\treadonly sandboxMethods?: readonly string[]\n}\n\n/**\n * The capability gates, keyed by the manifest permission key. The\n * `satisfies` below fails to compile when a permission is declared on\n * the manifest but missing here; the AssertTrue checks at the bottom\n * cover the reverse direction.\n */\nexport const PLUGIN_CAPABILITY_GATES = {\n\tsourceMeta: {\n\t\tdescription: \"Read/write the resource's source metadata (sourceMeta hook).\",\n\t},\n\tsearchMeta: {\n\t\tdescription: \"Produce and store search metadata facets (searchMeta hook).\",\n\t},\n\tdanmaku: {\n\t\tdescription: \"Create/list danmaku for resources this plugin renders.\",\n\t},\n\tmessage: {\n\t\tdescription: \"Create/list messages for resources this plugin renders.\",\n\t},\n\timageHashes: {\n\t\tdescription:\n\t\t\t\"Produce content hashes for duplicate detection / image similarity.\",\n\t},\n\tcontainer: {\n\t\tdescription:\n\t\t\t\"List and extract archive (zip/tar/7z/…) entries; the only API surface with a write side effect.\",\n\t\tsandboxMethods: [\"listContainer\", \"extractArchive\"],\n\t},\n\tdownload: {\n\t\tdescription:\n\t\t\t\"The plugin asset vault: user-consented downloads into the plugin's own vault/ plus the vault read/delete methods; denied by default and per-request by the user. One batched call = one consent dialog listing every item (all-or-nothing), capped per call.\",\n\t\tsandboxMethods: [\"download\", \"statAsset\", \"readAsset\", \"deleteAsset\"],\n\t},\n} as const satisfies Record<keyof PluginPermissions, PluginCapabilityGate>\n\nexport type PluginCapabilityKey = keyof typeof PLUGIN_CAPABILITY_GATES\n\n// -- compile-time coverage --------------------------------------------------\n// Every manifest permission key is declared here and nothing more — the\n// two assertions fail the build on either drift. Exported only so\n// noUnusedLocals keeps them alive — never import.\ntype AssertTrue<T extends true> = T\nexport type _ManifestKeysCovered = AssertTrue<\n\tkeyof PluginPermissions extends keyof typeof PLUGIN_CAPABILITY_GATES\n\t\t? true\n\t\t: false\n>\nexport type _TableKeysCovered = AssertTrue<\n\tkeyof typeof PLUGIN_CAPABILITY_GATES extends keyof PluginPermissions\n\t\t? true\n\t\t: false\n>\n\n/** Sandbox API method → capability key, derived once from the table. */\nexport const CAPABILITY_BY_METHOD: ReadonlyMap<string, PluginCapabilityKey> =\n\tnew Map(\n\t\t(\n\t\t\tObject.entries(PLUGIN_CAPABILITY_GATES) as [\n\t\t\t\tPluginCapabilityKey,\n\t\t\t\tPluginCapabilityGate,\n\t\t\t][]\n\t\t).flatMap(([capability, gate]) =>\n\t\t\t(gate.sandboxMethods ?? []).map(\n\t\t\t\t(method) => [method, capability] as [string, PluginCapabilityKey],\n\t\t\t),\n\t\t),\n\t)\n"]}
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
export { C as CoverKindUi, a as CoverKindUiMap, P as PluginManifest, b as PluginManifestId, c as PluginManifestUi, d as PluginPermissions, S as SearchKind, i as iconRef, l as localeString, p as pluginManifest, e as pluginManifestId, f as pluginManifestUi, g as pluginPermissions, s as searchKind } from './manifest-
|
|
2
|
+
export { C as CoverKindUi, a as CoverKindUiMap, P as PluginManifest, b as PluginManifestId, c as PluginManifestUi, d as PluginPermissions, S as SearchKind, i as iconRef, l as localeString, p as pluginManifest, e as pluginManifestId, f as pluginManifestUi, g as pluginPermissions, s as searchKind } from './manifest-JMXWSfKE.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The zod schema layer of the plugin contract: the manifest schema and
|
package/dist/schema.js
CHANGED
|
@@ -28,7 +28,12 @@ var pluginPermissions = z.object({
|
|
|
28
28
|
download: z.boolean().default(false)
|
|
29
29
|
});
|
|
30
30
|
var localeString = z.record(z.string(), z.string());
|
|
31
|
-
var iconRef = z.string().min(1)
|
|
31
|
+
var iconRef = z.string().min(1).max(512).refine(
|
|
32
|
+
(value) => !value.startsWith("http://") && !value.startsWith("https://") && !value.startsWith("data:") && !value.split("/").includes(".."),
|
|
33
|
+
{
|
|
34
|
+
message: "plugin icon must be a Solar glyph name or a zip asset path (no URLs, no data: URIs, no .. path segments)"
|
|
35
|
+
}
|
|
36
|
+
);
|
|
32
37
|
var templateValue = z.string();
|
|
33
38
|
var coverKindUi = z.object({
|
|
34
39
|
tl: z.array(templateValue).optional(),
|
|
@@ -46,7 +51,7 @@ var searchKind = z.object({
|
|
|
46
51
|
key: z.string().min(1),
|
|
47
52
|
/** i18n label key shown as the facet group's title. */
|
|
48
53
|
label: z.string().min(1),
|
|
49
|
-
/** Optional
|
|
54
|
+
/** Optional Solar glyph name (template `icon('Name')` or `asset('path')`). */
|
|
50
55
|
icon: templateValue.optional()
|
|
51
56
|
});
|
|
52
57
|
var searchUi = z.object({
|
|
@@ -97,10 +102,17 @@ var pluginManifest = z.object({
|
|
|
97
102
|
name: z.string().min(1),
|
|
98
103
|
/** One-line description shown in the plugins list. */
|
|
99
104
|
description: z.string().min(1),
|
|
100
|
-
/** Icon asset path
|
|
105
|
+
/** Icon: a Solar glyph name or a zip asset path (see {@link iconRef}). */
|
|
101
106
|
icon: iconRef.optional(),
|
|
102
107
|
/** Semantic plugin version; shown to users on the plugin card. */
|
|
103
108
|
version: z.string().min(1),
|
|
109
|
+
/**
|
|
110
|
+
* Minimum hoardodile app version this plugin runs on (e.g. `"0.1.1"`).
|
|
111
|
+
* Optional: absent means compatible with every app version. Hosts below
|
|
112
|
+
* the minimum refuse to install/update the plugin — the marketplace
|
|
113
|
+
* hides the install/update entries and the zip upload blocks it.
|
|
114
|
+
*/
|
|
115
|
+
minAppVersion: z.string().min(1).optional(),
|
|
104
116
|
/** Declared capabilities (see {@link pluginPermissions}). */
|
|
105
117
|
permissions: pluginPermissions,
|
|
106
118
|
/**
|
package/dist/schema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/manifest.ts","../src/schema.ts"],"names":["z"],"mappings":";;;AAOO,IAAM,gBAAA,GAAmB,CAAA,CAAE,MAAA,EAAO,CAAE,IAAA;AASpC,IAAM,iBAAA,GAAoB,EAAE,MAAA,CAAO;AAAA;AAAA,EAEzC,UAAA,EAAY,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAQ,KAAK,CAAA;AAAA;AAAA,EAErC,UAAA,EAAY,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAQ,KAAK,CAAA;AAAA;AAAA,EAErC,OAAA,EAAS,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAQ,KAAK,CAAA;AAAA;AAAA,EAElC,OAAA,EAAS,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAQ,KAAK,CAAA;AAAA;AAAA,EAElC,WAAA,EAAa,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAQ,KAAK,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMtC,SAAA,EAAW,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAQ,KAAK,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOpC,QAAA,EAAU,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAQ,KAAK;AACpC,CAAC;AAQM,IAAM,YAAA,GAAe,EAAE,MAAA,CAAO,CAAA,CAAE,QAAO,EAAG,CAAA,CAAE,QAAQ;AAGpD,IAAM,OAAA,GAAU,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC;AAWvC,IAAM,aAAA,GAAgB,EAAE,MAAA,EAAO;AAQ/B,IAAM,WAAA,GAAc,EAAE,MAAA,CAAO;AAAA,EAC5B,EAAA,EAAI,CAAA,CAAE,KAAA,CAAM,aAAa,EAAE,QAAA,EAAS;AAAA,EACpC,EAAA,EAAI,CAAA,CAAE,KAAA,CAAM,aAAa,EAAE,QAAA,EAAS;AAAA,EACpC,EAAA,EAAI,CAAA,CAAE,KAAA,CAAM,aAAa,EAAE,QAAA,EAAS;AAAA,EACpC,EAAA,EAAI,CAAA,CAAE,KAAA,CAAM,aAAa,EAAE,QAAA;AAC5B,CAAC,CAAA;AAQD,IAAM,cAAA,GAAiB,EAAE,MAAA,CAAO;AAAA,EAC/B,KAAA,EAAO,YAAY,QAAA,EAAS;AAAA,EAC5B,KAAA,EAAO,YAAY,QAAA,EAAS;AAAA,EAC5B,KAAA,EAAO,YAAY,QAAA,EAAS;AAAA,EAC5B,OAAA,EAAS,YAAY,QAAA;AACtB,CAAC,CAAA;AAOM,IAAM,UAAA,GAAa,EAAE,MAAA,CAAO;AAAA,EAClC,GAAA,EAAK,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA;AAAA,EAErB,KAAA,EAAO,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA;AAAA,EAEvB,IAAA,EAAM,cAAc,QAAA;AACrB,CAAC;AAGD,IAAM,QAAA,GAAW,EAAE,MAAA,CAAO;AAAA,EACzB,KAAA,EAAO,CAAA,CAAE,KAAA,CAAM,UAAU;AAC1B,CAAC,CAAA;AAED,IAAM,SAAA,GAAY,EAAE,MAAA,CAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM1B,QAAQ,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA;AAC3B,CAAC,CAAA;AAOM,IAAM,gBAAA,GAAmB,EAAE,MAAA,CAAO;AAAA;AAAA;AAAA;AAAA;AAAA,EAKxC,QAAQ,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMnC,QAAQ,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMnC,IAAA,EAAM,eAAe,QAAA,EAAS;AAAA;AAAA,EAE9B,MAAA,EAAQ,SAAS,QAAA,EAAS;AAAA;AAAA;AAAA;AAAA;AAAA,EAK1B,OAAA,EAAS,UAAU,QAAA,EAAS;AAAA;AAAA;AAAA;AAAA;AAAA,EAK5B,WAAA,EAAa,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAA;AAC1B,CAAC;AAWM,IAAM,cAAA,GAAiB,EAAE,MAAA,CAAO;AAAA,EACtC,EAAA,EAAI,gBAAA;AAAA;AAAA,EAEJ,IAAA,EAAM,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA;AAAA,EAEtB,WAAA,EAAa,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA;AAAA,EAE7B,IAAA,EAAM,QAAQ,QAAA,EAAS;AAAA;AAAA,EAEvB,OAAA,EAAS,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA;AAAA,EAEzB,WAAA,EAAa,iBAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOb,IAAA,EAAM,EAAE,MAAA,CAAO,CAAA,CAAE,QAAO,EAAG,YAAY,EAAE,QAAA,EAAS;AAAA;AAAA,EAElD,EAAA,EAAI,iBAAiB,QAAA;AACtB,CAAC;;;ACjKM,IAAM,UAAA,GAAaA,EACxB,MAAA,CAAO;AAAA,EACP,IAAA,EAAMA,CAAAA,CAAE,OAAA,EAAQ,CAAE,QAAA;AACnB,CAAC,EACA,MAAA","file":"schema.js","sourcesContent":["import { z } from \"zod\"\n\n/**\n * Plugin manifest UUID (v4). Generated once when scaffolding a plugin\n * (e.g. `crypto.randomUUID()`) and never reused across plugins — the\n * server keys installed plugins by this id.\n */\nexport const pluginManifestId = z.string().uuid()\nexport type PluginManifestId = z.infer<typeof pluginManifestId>\n\n/**\n * Declared plugin capabilities. Each flag gates the corresponding API\n * surface: a plugin without `danmaku` gets no danmaku methods and the\n * host enforces the permission at the capability guard, so a manifest\n * that does not declare a capability cannot call it.\n */\nexport const pluginPermissions = z.object({\n\t/** Read/write the resource's source metadata. */\n\tsourceMeta: z.boolean().default(false),\n\t/** Produce and store search metadata facets. */\n\tsearchMeta: z.boolean().default(false),\n\t/** Create/list danmaku for resources this plugin renders. */\n\tdanmaku: z.boolean().default(false),\n\t/** Create/list messages for resources this plugin renders. */\n\tmessage: z.boolean().default(false),\n\t/** Produce content hashes for duplicate detection / image similarity. */\n\timageHashes: z.boolean().default(false),\n\t/**\n\t * List and extract archive (zip/tar/7z/…) entries. The only API\n\t * surface with a write side effect (the host's extraction cache), so\n\t * it is denied by default.\n\t */\n\tcontainer: z.boolean().default(false),\n\t/**\n\t * The plugin asset vault: user-consented downloads into the plugin's\n\t * own `vault/` directory plus the vault read/delete methods. Denied\n\t * by default — every download needs this capability AND the user's\n\t * per-request approval.\n\t */\n\tdownload: z.boolean().default(false),\n})\nexport type PluginPermissions = z.infer<typeof pluginPermissions>\n\n/**\n * Label key → locale table: `{ \"cover.open\": { \"en\": \"Open\", \"zh-CN\": \"打开\" } }`.\n * The host's template engine resolves `t('cover.open')` against the\n * resource's locale from this map.\n */\nexport const localeString = z.record(z.string(), z.string())\n\n/** Icon reference: an asset path inside the plugin zip (`assets/icon.svg`). */\nexport const iconRef = z.string().min(1)\n\n/**\n * Corner template slot: a string rendered by the host's template engine\n * over the resource scope. The engine supports `{{data.field}}` paths,\n * pipes (`bytes`, `duration`, `number`, `inc`), comparisons\n * (`eq`/`ne`/`gt`/`lt`/`gte`/`lte`), `if(cond, a, b)`, `join`,\n * `t('key')` for i18n, `icon('Icon')`, `asset('path')`,\n * `kind(...)`, and `searchKindIcons()` (the plugin's search kinds).\n * Unknown expressions render as the empty string.\n */\nconst templateValue = z.string()\n\n/**\n * Corner template slots for one content kind. Templates are rendered by\n * the host's template engine over the resource's file list; supported\n * directives include `{{data.field}}`, `{{duration(ms)}}`, `{{inc(n)}}`\n * and `{{t('key')}}`.\n */\nconst coverKindUi = z.object({\n\ttl: z.array(templateValue).optional(),\n\ttr: z.array(templateValue).optional(),\n\tbl: z.array(templateValue).optional(),\n\tbr: z.array(templateValue).optional(),\n})\n\n/**\n * Cover templates per content kind. A plugin declares the kinds it can\n * produce; the host picks the block matching the resource's cover type\n * (`image`/`video`/`audio`/`default`) and renders each corner as\n * specified, or falls back to the default cover when no block matches.\n */\nconst coverKindUiMap = z.object({\n\timage: coverKindUi.optional(),\n\tvideo: coverKindUi.optional(),\n\taudio: coverKindUi.optional(),\n\tdefault: coverKindUi.optional(),\n})\n\n/**\n * A search facet kind: a named dimension with an icon, rendered as a\n * facet group in the host's search UI. `key` becomes the facet key in\n * the search metadata the plugin produces.\n */\nexport const searchKind = z.object({\n\tkey: z.string().min(1),\n\t/** i18n label key shown as the facet group's title. */\n\tlabel: z.string().min(1),\n\t/** Optional icon asset path in the plugin zip. */\n\ticon: templateValue.optional(),\n})\nexport type SearchKind = z.infer<typeof searchKind>\n\nconst searchUi = z.object({\n\tkinds: z.array(searchKind),\n})\n\nconst messageUi = z.object({\n\t/**\n\t * Template string for message anchor chip labels. Rendered by the\n\t * host's template engine. Supports `{{data.field}}`, `{{duration(ms)}}`,\n\t * `{{inc(n)}}`, `{{t('key')}}`, etc.\n\t */\n\tanchor: z.string().min(1).optional(),\n})\n\n/**\n * Manifest-declared UI preferences. These shape how the host app\n * presents the plugin's iframe without the plugin shipping any host\n * integration code.\n */\nexport const pluginManifestUi = z.object({\n\t/**\n\t * Preferred preview surface height (any CSS length, e.g. \"85vh\").\n\t * Applied by both the resource detail page and the preview dialog.\n\t */\n\theight: z.string().min(1).optional(),\n\t/**\n\t * Preferred preview surface aspect ratio (e.g. \"16/9\"), capped by the\n\t * host at 70vh. Intended for video-centric plugins; takes precedence\n\t * over `height`. When neither is set the host falls back to 60vh.\n\t */\n\taspect: z.string().min(1).optional(),\n\t/**\n\t * Cover template blocks per content kind. When present, the host\n\t * renders the resource cover from the plugin's file templates\n\t * instead of the built-in thumbnail pipeline.\n\t */\n\tcard: coverKindUiMap.optional(),\n\t/** Search facet kinds; enables the plugin's search integration. */\n\tsearch: searchUi.optional(),\n\t/**\n\t * Anchor chip label template for messages; declares message-anchor\n\t * support in the host UI.\n\t */\n\tmessage: messageUi.optional(),\n\t/**\n\t * Whether the plugin iframe inherits the host's app font (default true).\n\t * Set to false for plugins that must render with their own fonts.\n\t */\n\tinheritFont: z.boolean().optional(),\n})\nexport type PluginManifestUi = z.infer<typeof pluginManifestUi>\nexport type CoverKindUi = z.infer<typeof coverKindUi>\nexport type CoverKindUiMap = z.infer<typeof coverKindUiMap>\n\n/**\n * The plugin manifest contract — the single schema validated everywhere\n * via its parse: the server at install time, the build CLI, and the\n * workbench. A manifest lives at the zip root of a built plugin next to\n * `main.js` and `index.html`.\n */\nexport const pluginManifest = z.object({\n\tid: pluginManifestId,\n\t/** Display name shown in the plugins list and resource badges. */\n\tname: z.string().min(1),\n\t/** One-line description shown in the plugins list. */\n\tdescription: z.string().min(1),\n\t/** Icon asset path inside the plugin zip. */\n\ticon: iconRef.optional(),\n\t/** Semantic plugin version; shown to users on the plugin card. */\n\tversion: z.string().min(1),\n\t/** Declared capabilities (see {@link pluginPermissions}). */\n\tpermissions: pluginPermissions,\n\t/**\n\t * Localized label tables: `{ labelKey: { locale: label } }`, e.g.\n\t * `{ \"cover.open\": { \"en\": \"Open\", \"zh-CN\": \"打开\" } }` — labels are\n\t * referenced from templates with `{{t('labelKey')}}` (see\n\t * {@link localeString}).\n\t */\n\ti18n: z.record(z.string(), localeString).optional(),\n\t/** UI preferences (see {@link pluginManifestUi}). */\n\tui: pluginManifestUi.optional(),\n})\nexport type PluginManifest = z.infer<typeof pluginManifest>\n","/**\n * The zod schema layer of the plugin contract: the manifest schema and\n * the wire anchor envelope. Import this subpath\n * (`@hoardodile/sdk-types/schema`) only where a runtime validator is\n * actually needed — the host, the server, and tooling. The root entry\n * re-exports the inferred types only, so plugin bundles never pull zod.\n */\nimport { z } from \"zod\"\n\nexport * from \"./manifest.ts\"\n\n/**\n * Wire/storage envelope for a message or danmaku anchor. Carries only\n * the plugin-defined location payload in `data`; the host never\n * interprets its contents. The anchor's resource is host state — the SDK\n * injects it from the iframe's binding and the server derives it from\n * the row's `anchor_resource_id` column — so plugins never see a resId\n * here, and a plugin that sends one is rejected (strict).\n *\n * Plugin code works with the raw location data (`PluginSchema[\"anchor\"]`)\n * directly; the SDK wraps it into this envelope when it crosses the\n * wire.\n */\nexport const anchorData = z\n\t.object({\n\t\tdata: z.unknown().optional(),\n\t})\n\t.strict()\nexport type AnchorData = z.infer<typeof anchorData>\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/manifest.ts","../src/schema.ts"],"names":["z"],"mappings":";;;AAOO,IAAM,gBAAA,GAAmB,CAAA,CAAE,MAAA,EAAO,CAAE,IAAA;AASpC,IAAM,iBAAA,GAAoB,EAAE,MAAA,CAAO;AAAA;AAAA,EAEzC,UAAA,EAAY,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAQ,KAAK,CAAA;AAAA;AAAA,EAErC,UAAA,EAAY,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAQ,KAAK,CAAA;AAAA;AAAA,EAErC,OAAA,EAAS,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAQ,KAAK,CAAA;AAAA;AAAA,EAElC,OAAA,EAAS,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAQ,KAAK,CAAA;AAAA;AAAA,EAElC,WAAA,EAAa,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAQ,KAAK,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMtC,SAAA,EAAW,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAQ,KAAK,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOpC,QAAA,EAAU,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAQ,KAAK;AACpC,CAAC;AAQM,IAAM,YAAA,GAAe,EAAE,MAAA,CAAO,CAAA,CAAE,QAAO,EAAG,CAAA,CAAE,QAAQ;AAiBpD,IAAM,OAAA,GAAU,EACrB,MAAA,EAAO,CACP,IAAI,CAAC,CAAA,CACL,GAAA,CAAI,GAAG,CAAA,CACP,MAAA;AAAA,EACA,CAAC,UACA,CAAC,KAAA,CAAM,WAAW,SAAS,CAAA,IAC3B,CAAC,KAAA,CAAM,UAAA,CAAW,UAAU,KAC5B,CAAC,KAAA,CAAM,UAAA,CAAW,OAAO,CAAA,IACzB,CAAC,MAAM,KAAA,CAAM,GAAG,CAAA,CAAE,QAAA,CAAS,IAAI,CAAA;AAAA,EAChC;AAAA,IACC,OAAA,EACC;AAAA;AAEH;AAYD,IAAM,aAAA,GAAgB,EAAE,MAAA,EAAO;AAQ/B,IAAM,WAAA,GAAc,EAAE,MAAA,CAAO;AAAA,EAC5B,EAAA,EAAI,CAAA,CAAE,KAAA,CAAM,aAAa,EAAE,QAAA,EAAS;AAAA,EACpC,EAAA,EAAI,CAAA,CAAE,KAAA,CAAM,aAAa,EAAE,QAAA,EAAS;AAAA,EACpC,EAAA,EAAI,CAAA,CAAE,KAAA,CAAM,aAAa,EAAE,QAAA,EAAS;AAAA,EACpC,EAAA,EAAI,CAAA,CAAE,KAAA,CAAM,aAAa,EAAE,QAAA;AAC5B,CAAC,CAAA;AAQD,IAAM,cAAA,GAAiB,EAAE,MAAA,CAAO;AAAA,EAC/B,KAAA,EAAO,YAAY,QAAA,EAAS;AAAA,EAC5B,KAAA,EAAO,YAAY,QAAA,EAAS;AAAA,EAC5B,KAAA,EAAO,YAAY,QAAA,EAAS;AAAA,EAC5B,OAAA,EAAS,YAAY,QAAA;AACtB,CAAC,CAAA;AAOM,IAAM,UAAA,GAAa,EAAE,MAAA,CAAO;AAAA,EAClC,GAAA,EAAK,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA;AAAA,EAErB,KAAA,EAAO,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA;AAAA,EAEvB,IAAA,EAAM,cAAc,QAAA;AACrB,CAAC;AAGD,IAAM,QAAA,GAAW,EAAE,MAAA,CAAO;AAAA,EACzB,KAAA,EAAO,CAAA,CAAE,KAAA,CAAM,UAAU;AAC1B,CAAC,CAAA;AAED,IAAM,SAAA,GAAY,EAAE,MAAA,CAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM1B,QAAQ,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA;AAC3B,CAAC,CAAA;AAOM,IAAM,gBAAA,GAAmB,EAAE,MAAA,CAAO;AAAA;AAAA;AAAA;AAAA;AAAA,EAKxC,QAAQ,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMnC,QAAQ,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMnC,IAAA,EAAM,eAAe,QAAA,EAAS;AAAA;AAAA,EAE9B,MAAA,EAAQ,SAAS,QAAA,EAAS;AAAA;AAAA;AAAA;AAAA;AAAA,EAK1B,OAAA,EAAS,UAAU,QAAA,EAAS;AAAA;AAAA;AAAA;AAAA;AAAA,EAK5B,WAAA,EAAa,CAAA,CAAE,OAAA,EAAQ,CAAE,QAAA;AAC1B,CAAC;AAWM,IAAM,cAAA,GAAiB,EAAE,MAAA,CAAO;AAAA,EACtC,EAAA,EAAI,gBAAA;AAAA;AAAA,EAEJ,IAAA,EAAM,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA;AAAA,EAEtB,WAAA,EAAa,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA;AAAA,EAE7B,IAAA,EAAM,QAAQ,QAAA,EAAS;AAAA;AAAA,EAEvB,OAAA,EAAS,CAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOzB,eAAe,CAAA,CAAE,MAAA,GAAS,GAAA,CAAI,CAAC,EAAE,QAAA,EAAS;AAAA;AAAA,EAE1C,WAAA,EAAa,iBAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOb,IAAA,EAAM,EAAE,MAAA,CAAO,CAAA,CAAE,QAAO,EAAG,YAAY,EAAE,QAAA,EAAS;AAAA;AAAA,EAElD,EAAA,EAAI,iBAAiB,QAAA;AACtB,CAAC;;;ACrMM,IAAM,UAAA,GAAaA,EACxB,MAAA,CAAO;AAAA,EACP,IAAA,EAAMA,CAAAA,CAAE,OAAA,EAAQ,CAAE,QAAA;AACnB,CAAC,EACA,MAAA","file":"schema.js","sourcesContent":["import { z } from \"zod\"\n\n/**\n * Plugin manifest UUID (v4). Generated once when scaffolding a plugin\n * (e.g. `crypto.randomUUID()`) and never reused across plugins — the\n * server keys installed plugins by this id.\n */\nexport const pluginManifestId = z.string().uuid()\nexport type PluginManifestId = z.infer<typeof pluginManifestId>\n\n/**\n * Declared plugin capabilities. Each flag gates the corresponding API\n * surface: a plugin without `danmaku` gets no danmaku methods and the\n * host enforces the permission at the capability guard, so a manifest\n * that does not declare a capability cannot call it.\n */\nexport const pluginPermissions = z.object({\n\t/** Read/write the resource's source metadata. */\n\tsourceMeta: z.boolean().default(false),\n\t/** Produce and store search metadata facets. */\n\tsearchMeta: z.boolean().default(false),\n\t/** Create/list danmaku for resources this plugin renders. */\n\tdanmaku: z.boolean().default(false),\n\t/** Create/list messages for resources this plugin renders. */\n\tmessage: z.boolean().default(false),\n\t/** Produce content hashes for duplicate detection / image similarity. */\n\timageHashes: z.boolean().default(false),\n\t/**\n\t * List and extract archive (zip/tar/7z/…) entries. The only API\n\t * surface with a write side effect (the host's extraction cache), so\n\t * it is denied by default.\n\t */\n\tcontainer: z.boolean().default(false),\n\t/**\n\t * The plugin asset vault: user-consented downloads into the plugin's\n\t * own `vault/` directory plus the vault read/delete methods. Denied\n\t * by default — every download needs this capability AND the user's\n\t * per-request approval.\n\t */\n\tdownload: z.boolean().default(false),\n})\nexport type PluginPermissions = z.infer<typeof pluginPermissions>\n\n/**\n * Label key → locale table: `{ \"cover.open\": { \"en\": \"Open\", \"zh-CN\": \"打开\" } }`.\n * The host's template engine resolves `t('cover.open')` against the\n * resource's locale from this map.\n */\nexport const localeString = z.record(z.string(), z.string())\n\n/**\n * Icon reference, in exactly one of two static forms:\n *\n * - `<SolarGlyph>` — a Solar glyph name (kebab-case, or the legacy\n * PascalCase spelling); the host renders it from its own bundled three\n * weights (bold / boldDuotone / linear) and follows the user's icon\n * style preference. Names outside the host's Solar index render\n * nothing.\n * - `<relative/path>` — an asset inside the plugin zip (`assets/icon.svg`),\n * served from the plugin's own directory.\n *\n * `http(s):`/`data:` URIs and `..` path segments are rejected: the\n * manifest stays plain static JSON and the host never fetches or\n * executes anything an icon reference asks for.\n */\nexport const iconRef = z\n\t.string()\n\t.min(1)\n\t.max(512)\n\t.refine(\n\t\t(value) =>\n\t\t\t!value.startsWith(\"http://\") &&\n\t\t\t!value.startsWith(\"https://\") &&\n\t\t\t!value.startsWith(\"data:\") &&\n\t\t\t!value.split(\"/\").includes(\"..\"),\n\t\t{\n\t\t\tmessage:\n\t\t\t\t\"plugin icon must be a Solar glyph name or a zip asset path (no URLs, no data: URIs, no .. path segments)\",\n\t\t},\n\t)\n\n/**\n * Corner template slot: a string rendered by the host's template engine\n * over the resource scope. The engine supports `{{data.field}}` paths,\n * pipes (`bytes`, `duration`, `number`, `inc`), comparisons\n * (`eq`/`ne`/`gt`/`lt`/`gte`/`lte`), `if(cond, a, b)`, `join`,\n * `t('key')` for i18n, `icon('<SolarGlyph>')` (a Solar glyph name — see\n * {@link iconRef}), `asset('path')`,\n * `kind(...)`, and `searchKindIcons()` (the plugin's search kinds).\n * Unknown expressions render as the empty string.\n */\nconst templateValue = z.string()\n\n/**\n * Corner template slots for one content kind. Templates are rendered by\n * the host's template engine over the resource's file list; supported\n * directives include `{{data.field}}`, `{{duration(ms)}}`, `{{inc(n)}}`\n * and `{{t('key')}}`.\n */\nconst coverKindUi = z.object({\n\ttl: z.array(templateValue).optional(),\n\ttr: z.array(templateValue).optional(),\n\tbl: z.array(templateValue).optional(),\n\tbr: z.array(templateValue).optional(),\n})\n\n/**\n * Cover templates per content kind. A plugin declares the kinds it can\n * produce; the host picks the block matching the resource's cover type\n * (`image`/`video`/`audio`/`default`) and renders each corner as\n * specified, or falls back to the default cover when no block matches.\n */\nconst coverKindUiMap = z.object({\n\timage: coverKindUi.optional(),\n\tvideo: coverKindUi.optional(),\n\taudio: coverKindUi.optional(),\n\tdefault: coverKindUi.optional(),\n})\n\n/**\n * A search facet kind: a named dimension with an icon, rendered as a\n * facet group in the host's search UI. `key` becomes the facet key in\n * the search metadata the plugin produces.\n */\nexport const searchKind = z.object({\n\tkey: z.string().min(1),\n\t/** i18n label key shown as the facet group's title. */\n\tlabel: z.string().min(1),\n\t/** Optional Solar glyph name (template `icon('Name')` or `asset('path')`). */\n\ticon: templateValue.optional(),\n})\nexport type SearchKind = z.infer<typeof searchKind>\n\nconst searchUi = z.object({\n\tkinds: z.array(searchKind),\n})\n\nconst messageUi = z.object({\n\t/**\n\t * Template string for message anchor chip labels. Rendered by the\n\t * host's template engine. Supports `{{data.field}}`, `{{duration(ms)}}`,\n\t * `{{inc(n)}}`, `{{t('key')}}`, etc.\n\t */\n\tanchor: z.string().min(1).optional(),\n})\n\n/**\n * Manifest-declared UI preferences. These shape how the host app\n * presents the plugin's iframe without the plugin shipping any host\n * integration code.\n */\nexport const pluginManifestUi = z.object({\n\t/**\n\t * Preferred preview surface height (any CSS length, e.g. \"85vh\").\n\t * Applied by both the resource detail page and the preview dialog.\n\t */\n\theight: z.string().min(1).optional(),\n\t/**\n\t * Preferred preview surface aspect ratio (e.g. \"16/9\"), capped by the\n\t * host at 70vh. Intended for video-centric plugins; takes precedence\n\t * over `height`. When neither is set the host falls back to 60vh.\n\t */\n\taspect: z.string().min(1).optional(),\n\t/**\n\t * Cover template blocks per content kind. When present, the host\n\t * renders the resource cover from the plugin's file templates\n\t * instead of the built-in thumbnail pipeline.\n\t */\n\tcard: coverKindUiMap.optional(),\n\t/** Search facet kinds; enables the plugin's search integration. */\n\tsearch: searchUi.optional(),\n\t/**\n\t * Anchor chip label template for messages; declares message-anchor\n\t * support in the host UI.\n\t */\n\tmessage: messageUi.optional(),\n\t/**\n\t * Whether the plugin iframe inherits the host's app font (default true).\n\t * Set to false for plugins that must render with their own fonts.\n\t */\n\tinheritFont: z.boolean().optional(),\n})\nexport type PluginManifestUi = z.infer<typeof pluginManifestUi>\nexport type CoverKindUi = z.infer<typeof coverKindUi>\nexport type CoverKindUiMap = z.infer<typeof coverKindUiMap>\n\n/**\n * The plugin manifest contract — the single schema validated everywhere\n * via its parse: the server at install time, the build CLI, and the\n * workbench. A manifest lives at the zip root of a built plugin next to\n * `main.js` and `index.html`.\n */\nexport const pluginManifest = z.object({\n\tid: pluginManifestId,\n\t/** Display name shown in the plugins list and resource badges. */\n\tname: z.string().min(1),\n\t/** One-line description shown in the plugins list. */\n\tdescription: z.string().min(1),\n\t/** Icon: a Solar glyph name or a zip asset path (see {@link iconRef}). */\n\ticon: iconRef.optional(),\n\t/** Semantic plugin version; shown to users on the plugin card. */\n\tversion: z.string().min(1),\n\t/**\n\t * Minimum hoardodile app version this plugin runs on (e.g. `\"0.1.1\"`).\n\t * Optional: absent means compatible with every app version. Hosts below\n\t * the minimum refuse to install/update the plugin — the marketplace\n\t * hides the install/update entries and the zip upload blocks it.\n\t */\n\tminAppVersion: z.string().min(1).optional(),\n\t/** Declared capabilities (see {@link pluginPermissions}). */\n\tpermissions: pluginPermissions,\n\t/**\n\t * Localized label tables: `{ labelKey: { locale: label } }`, e.g.\n\t * `{ \"cover.open\": { \"en\": \"Open\", \"zh-CN\": \"打开\" } }` — labels are\n\t * referenced from templates with `{{t('labelKey')}}` (see\n\t * {@link localeString}).\n\t */\n\ti18n: z.record(z.string(), localeString).optional(),\n\t/** UI preferences (see {@link pluginManifestUi}). */\n\tui: pluginManifestUi.optional(),\n})\nexport type PluginManifest = z.infer<typeof pluginManifest>\n","/**\n * The zod schema layer of the plugin contract: the manifest schema and\n * the wire anchor envelope. Import this subpath\n * (`@hoardodile/sdk-types/schema`) only where a runtime validator is\n * actually needed — the host, the server, and tooling. The root entry\n * re-exports the inferred types only, so plugin bundles never pull zod.\n */\nimport { z } from \"zod\"\n\nexport * from \"./manifest.ts\"\n\n/**\n * Wire/storage envelope for a message or danmaku anchor. Carries only\n * the plugin-defined location payload in `data`; the host never\n * interprets its contents. The anchor's resource is host state — the SDK\n * injects it from the iframe's binding and the server derives it from\n * the row's `anchor_resource_id` column — so plugins never see a resId\n * here, and a plugin that sends one is rejected (strict).\n *\n * Plugin code works with the raw location data (`PluginSchema[\"anchor\"]`)\n * directly; the SDK wraps it into this envelope when it crosses the\n * wire.\n */\nexport const anchorData = z\n\t.object({\n\t\tdata: z.unknown().optional(),\n\t})\n\t.strict()\nexport type AnchorData = z.infer<typeof anchorData>\n"]}
|
package/package.json
CHANGED
package/src/manifest.ts
CHANGED
|
@@ -48,15 +48,44 @@ export type PluginPermissions = z.infer<typeof pluginPermissions>
|
|
|
48
48
|
*/
|
|
49
49
|
export const localeString = z.record(z.string(), z.string())
|
|
50
50
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Icon reference, in exactly one of two static forms:
|
|
53
|
+
*
|
|
54
|
+
* - `<SolarGlyph>` — a Solar glyph name (kebab-case, or the legacy
|
|
55
|
+
* PascalCase spelling); the host renders it from its own bundled three
|
|
56
|
+
* weights (bold / boldDuotone / linear) and follows the user's icon
|
|
57
|
+
* style preference. Names outside the host's Solar index render
|
|
58
|
+
* nothing.
|
|
59
|
+
* - `<relative/path>` — an asset inside the plugin zip (`assets/icon.svg`),
|
|
60
|
+
* served from the plugin's own directory.
|
|
61
|
+
*
|
|
62
|
+
* `http(s):`/`data:` URIs and `..` path segments are rejected: the
|
|
63
|
+
* manifest stays plain static JSON and the host never fetches or
|
|
64
|
+
* executes anything an icon reference asks for.
|
|
65
|
+
*/
|
|
66
|
+
export const iconRef = z
|
|
67
|
+
.string()
|
|
68
|
+
.min(1)
|
|
69
|
+
.max(512)
|
|
70
|
+
.refine(
|
|
71
|
+
(value) =>
|
|
72
|
+
!value.startsWith("http://") &&
|
|
73
|
+
!value.startsWith("https://") &&
|
|
74
|
+
!value.startsWith("data:") &&
|
|
75
|
+
!value.split("/").includes(".."),
|
|
76
|
+
{
|
|
77
|
+
message:
|
|
78
|
+
"plugin icon must be a Solar glyph name or a zip asset path (no URLs, no data: URIs, no .. path segments)",
|
|
79
|
+
},
|
|
80
|
+
)
|
|
53
81
|
|
|
54
82
|
/**
|
|
55
83
|
* Corner template slot: a string rendered by the host's template engine
|
|
56
84
|
* over the resource scope. The engine supports `{{data.field}}` paths,
|
|
57
85
|
* pipes (`bytes`, `duration`, `number`, `inc`), comparisons
|
|
58
86
|
* (`eq`/`ne`/`gt`/`lt`/`gte`/`lte`), `if(cond, a, b)`, `join`,
|
|
59
|
-
* `t('key')` for i18n, `icon('
|
|
87
|
+
* `t('key')` for i18n, `icon('<SolarGlyph>')` (a Solar glyph name — see
|
|
88
|
+
* {@link iconRef}), `asset('path')`,
|
|
60
89
|
* `kind(...)`, and `searchKindIcons()` (the plugin's search kinds).
|
|
61
90
|
* Unknown expressions render as the empty string.
|
|
62
91
|
*/
|
|
@@ -97,7 +126,7 @@ export const searchKind = z.object({
|
|
|
97
126
|
key: z.string().min(1),
|
|
98
127
|
/** i18n label key shown as the facet group's title. */
|
|
99
128
|
label: z.string().min(1),
|
|
100
|
-
/** Optional
|
|
129
|
+
/** Optional Solar glyph name (template `icon('Name')` or `asset('path')`). */
|
|
101
130
|
icon: templateValue.optional(),
|
|
102
131
|
})
|
|
103
132
|
export type SearchKind = z.infer<typeof searchKind>
|
|
@@ -167,10 +196,17 @@ export const pluginManifest = z.object({
|
|
|
167
196
|
name: z.string().min(1),
|
|
168
197
|
/** One-line description shown in the plugins list. */
|
|
169
198
|
description: z.string().min(1),
|
|
170
|
-
/** Icon asset path
|
|
199
|
+
/** Icon: a Solar glyph name or a zip asset path (see {@link iconRef}). */
|
|
171
200
|
icon: iconRef.optional(),
|
|
172
201
|
/** Semantic plugin version; shown to users on the plugin card. */
|
|
173
202
|
version: z.string().min(1),
|
|
203
|
+
/**
|
|
204
|
+
* Minimum hoardodile app version this plugin runs on (e.g. `"0.1.1"`).
|
|
205
|
+
* Optional: absent means compatible with every app version. Hosts below
|
|
206
|
+
* the minimum refuse to install/update the plugin — the marketplace
|
|
207
|
+
* hides the install/update entries and the zip upload blocks it.
|
|
208
|
+
*/
|
|
209
|
+
minAppVersion: z.string().min(1).optional(),
|
|
174
210
|
/** Declared capabilities (see {@link pluginPermissions}). */
|
|
175
211
|
permissions: pluginPermissions,
|
|
176
212
|
/**
|
|
@@ -10,6 +10,13 @@ export const PLUGIN_ASSET_DEST_MAX_LENGTH = 256
|
|
|
10
10
|
/** Max length of the optional human `reason` shown in the consent dialog. */
|
|
11
11
|
export const PLUGIN_ASSET_REASON_MAX_LENGTH = 200
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Max items in one batched `download([...])` call. One call = one consent
|
|
15
|
+
* ticket = one dialog listing every item; beyond this the host rejects
|
|
16
|
+
* with `POLICY` (a burst must not stack unbounded tickets per plugin).
|
|
17
|
+
*/
|
|
18
|
+
export const PLUGIN_ASSET_BATCH_MAX_ITEMS = 16
|
|
19
|
+
|
|
13
20
|
/** Expected shape of an SRI-style sha256 pin: 64 lowercase hex characters. */
|
|
14
21
|
export const PLUGIN_ASSET_SHA256_PATTERN = /^[0-9a-f]{64}$/
|
|
15
22
|
|
package/src/plugin-asset.ts
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* Both sides of the plugin speak the same shapes: the server-side
|
|
11
11
|
* `ResourceAPI` (main.js hooks) and the iframe `WebPluginAPI` (render)
|
|
12
12
|
* call the same four methods with the same request/result vocabulary.
|
|
13
|
+
* `download` also accepts an **array of requests** — one batched call is
|
|
14
|
+
* ONE consent question (the dialog lists every item) and is all-or-nothing
|
|
15
|
+
* (results arrive in request order; any failure commits nothing).
|
|
13
16
|
* All methods are gated by the manifest `download` permission and
|
|
14
17
|
* denied inside the sandbox when the manifest does not declare it.
|
|
15
18
|
*
|
|
@@ -52,7 +52,7 @@ export const PLUGIN_CAPABILITY_GATES = {
|
|
|
52
52
|
},
|
|
53
53
|
download: {
|
|
54
54
|
description:
|
|
55
|
-
"The plugin asset vault: user-consented downloads into the plugin's own vault/ plus the vault read/delete methods; denied by default and per-
|
|
55
|
+
"The plugin asset vault: user-consented downloads into the plugin's own vault/ plus the vault read/delete methods; denied by default and per-request by the user. One batched call = one consent dialog listing every item (all-or-nothing), capped per call.",
|
|
56
56
|
sandboxMethods: ["download", "statAsset", "readAsset", "deleteAsset"],
|
|
57
57
|
},
|
|
58
58
|
} as const satisfies Record<keyof PluginPermissions, PluginCapabilityGate>
|
package/src/plugin-definition.ts
CHANGED
|
@@ -363,11 +363,19 @@ export type ResourceAPI<TSchema extends PluginSchema = PluginSchema> = {
|
|
|
363
363
|
*/
|
|
364
364
|
readonly extractArchive: (filename: string) => Promise<ArchiveExtraction>
|
|
365
365
|
/**
|
|
366
|
-
* Ensure
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
366
|
+
* Ensure remote assets exist in the plugin's own vault — one call with
|
|
367
|
+
* one request, or one call with an array of requests. When `dest` is
|
|
368
|
+
* already present the host answers `cached: true` without any dialog
|
|
369
|
+
* and without touching the network; otherwise the host asks the user
|
|
370
|
+
* (the web app shows the shared consent dialog with the URLs
|
|
371
|
+
* verbatim) and downloads on approval.
|
|
372
|
+
*
|
|
373
|
+
* An array is ONE consent question for the WHOLE batch (the dialog
|
|
374
|
+
* lists every item) and is all-or-nothing: any failure discards every
|
|
375
|
+
* staged file and rejects with the first error, so nothing is
|
|
376
|
+
* partially committed. Results arrive in request order with `cached`
|
|
377
|
+
* items keeping their positions. Cap: {@link PLUGIN_ASSET_BATCH_MAX_ITEMS}
|
|
378
|
+
* items per call. The file always lands inside
|
|
371
379
|
* `<plugin-dir>/vault/` — `dest` is vault-relative and can never
|
|
372
380
|
* reach the plugin's bundled files.
|
|
373
381
|
*
|
|
@@ -375,9 +383,12 @@ export type ResourceAPI<TSchema extends PluginSchema = PluginSchema> = {
|
|
|
375
383
|
* machine-readable {@link PluginAssetErrorName} in `err.name`
|
|
376
384
|
* (`DENIED` / `UNAVAILABLE` / `POLICY`).
|
|
377
385
|
*/
|
|
378
|
-
readonly download: (
|
|
386
|
+
readonly download: ((
|
|
379
387
|
request: PluginDownloadRequest,
|
|
380
|
-
) => Promise<PluginDownloadResult>
|
|
388
|
+
) => Promise<PluginDownloadResult>) &
|
|
389
|
+
((
|
|
390
|
+
requests: readonly PluginDownloadRequest[],
|
|
391
|
+
) => Promise<readonly PluginDownloadResult[]>)
|
|
381
392
|
/**
|
|
382
393
|
* Byte size of a vault file, or `undefined` when absent. The cheap
|
|
383
394
|
* presence check on top of which `download` resolves cached hits.
|
|
@@ -607,13 +618,15 @@ export type ResourceAPIFixtureConfig<
|
|
|
607
618
|
*/
|
|
608
619
|
readonly assetFiles?: Readonly<Record<string, string | Uint8Array>>
|
|
609
620
|
/**
|
|
610
|
-
* Handler for `download
|
|
611
|
-
*
|
|
612
|
-
*
|
|
621
|
+
* Handler for `download` (single request or batch of requests, typed
|
|
622
|
+
* as the union — the fixture returns the matching shape). Absent
|
|
623
|
+
* means the hosted runtime has no consent channel — `download`
|
|
624
|
+
* rejects with `UNAVAILABLE`, exactly like the CLI, workbench and
|
|
625
|
+
* offline mock hosts.
|
|
613
626
|
*/
|
|
614
627
|
readonly downloadHandler?: (
|
|
615
|
-
request: PluginDownloadRequest,
|
|
616
|
-
) => Promise<PluginDownloadResult>
|
|
628
|
+
request: PluginDownloadRequest | readonly PluginDownloadRequest[],
|
|
629
|
+
) => Promise<PluginDownloadResult | readonly PluginDownloadResult[]>
|
|
617
630
|
/**
|
|
618
631
|
* Container addressing for the fixture: maps a virtual path
|
|
619
632
|
* (`outer!inner`) to stat/sniff/probe results, so hooks that browse
|
|
@@ -844,15 +857,23 @@ export function createResourceAPIFixture<
|
|
|
844
857
|
}
|
|
845
858
|
return configured
|
|
846
859
|
},
|
|
847
|
-
async
|
|
860
|
+
download: (async (
|
|
861
|
+
request: PluginDownloadRequest | readonly PluginDownloadRequest[],
|
|
862
|
+
) => {
|
|
848
863
|
if (config.downloadHandler === undefined) {
|
|
849
864
|
throw pluginAssetError(
|
|
850
865
|
"UNAVAILABLE",
|
|
851
866
|
"ResourceAPIFixture: no download handler configured",
|
|
852
867
|
)
|
|
853
868
|
}
|
|
854
|
-
|
|
855
|
-
|
|
869
|
+
const result = await config.downloadHandler(request)
|
|
870
|
+
// Mirror the real host: batch in → batch out, single in →
|
|
871
|
+
// single out.
|
|
872
|
+
if (Array.isArray(request)) {
|
|
873
|
+
return Array.isArray(result) ? result : [result]
|
|
874
|
+
}
|
|
875
|
+
return Array.isArray(result) ? (result[0] ?? result) : result
|
|
876
|
+
}) as ResourceAPI<TSchema>["download"],
|
|
856
877
|
async statAsset(path) {
|
|
857
878
|
const content = resolveValue(path, config.assetFiles, undefined)
|
|
858
879
|
if (content === undefined) return undefined
|