@hoardodile/host 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +18 -0
- package/README.md +73 -0
- package/dist/chunks/worker-entry.mjs +422 -0
- package/dist/container-fKxefO7p.d.ts +58 -0
- package/dist/contract/index.d.ts +49 -0
- package/dist/contract/index.js +187 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/hoard/index.d.ts +754 -0
- package/dist/hoard/index.js +1939 -0
- package/dist/hoard/index.js.map +1 -0
- package/dist/image-Dx2Be3Bj.d.ts +168 -0
- package/dist/index.d.ts +788 -0
- package/dist/index.js +3311 -0
- package/dist/index.js.map +1 -0
- package/dist/media/index.d.ts +96 -0
- package/dist/media/index.js +152 -0
- package/dist/media/index.js.map +1 -0
- package/dist/probe/index.d.ts +113 -0
- package/dist/probe/index.js +454 -0
- package/dist/probe/index.js.map +1 -0
- package/dist/probe-cache-BkRI7HEK.d.ts +25 -0
- package/dist/render/index.d.ts +125 -0
- package/dist/render/index.js +430 -0
- package/dist/render/index.js.map +1 -0
- package/package.json +103 -0
- package/src/__testutils__/zip-fixtures.ts +139 -0
- package/src/activation.ts +116 -0
- package/src/api-types.ts +60 -0
- package/src/api.test.ts +564 -0
- package/src/api.ts +501 -0
- package/src/archive/7z.test.ts +222 -0
- package/src/archive/7z.ts +261 -0
- package/src/archive/archive-ops.test.ts +654 -0
- package/src/archive/extract-archive.test.ts +237 -0
- package/src/archive/extract-archive.ts +534 -0
- package/src/archive/extract.ts +693 -0
- package/src/archive/format.ts +49 -0
- package/src/archive/index.ts +116 -0
- package/src/archive/listing.ts +121 -0
- package/src/archive/materialize.ts +88 -0
- package/src/archive/name-decode.test.ts +34 -0
- package/src/archive/name-decode.ts +287 -0
- package/src/archive/nested-cd-cache.ts +55 -0
- package/src/archive/nested-entry.test.ts +194 -0
- package/src/archive/nested-entry.ts +254 -0
- package/src/archive/pack.ts +40 -0
- package/src/archive/zip-entries.test.ts +56 -0
- package/src/archive/zip-entries.ts +336 -0
- package/src/capability-guard.ts +73 -0
- package/src/container.ts +62 -0
- package/src/contract/container-fixture.ts +74 -0
- package/src/contract/index.ts +10 -0
- package/src/contract/suite.test.ts +101 -0
- package/src/contract/suite.ts +205 -0
- package/src/define-plugin.test.ts +53 -0
- package/src/define-plugin.ts +12 -0
- package/src/dev-runner.test.ts +98 -0
- package/src/dev-runner.ts +73 -0
- package/src/directory-api.test.ts +229 -0
- package/src/directory-api.ts +42 -0
- package/src/directory-container.ts +141 -0
- package/src/discovery.ts +295 -0
- package/src/errors.ts +74 -0
- package/src/fixtures.ts +8 -0
- package/src/hash.test.ts +301 -0
- package/src/hash.ts +167 -0
- package/src/hoard/dir-size.ts +58 -0
- package/src/hoard/index.ts +84 -0
- package/src/hoard/migrate-script.test.ts +539 -0
- package/src/hoard/order-manifest.test.ts +115 -0
- package/src/hoard/order-manifest.ts +131 -0
- package/src/hoard/paths.test.ts +226 -0
- package/src/hoard/paths.ts +569 -0
- package/src/hoard/plugin-vault.test.ts +160 -0
- package/src/hoard/plugin-vault.ts +233 -0
- package/src/hoard/sanitize.test.ts +149 -0
- package/src/hoard/sanitize.ts +230 -0
- package/src/hoard/staging-dir.ts +133 -0
- package/src/hoard/version.test.ts +285 -0
- package/src/hoard/version.ts +216 -0
- package/src/hoard/versioned-folder-ops.ts +152 -0
- package/src/hoard/write-versioned.test.ts +67 -0
- package/src/hoard/write-versioned.ts +39 -0
- package/src/hooks.test.ts +528 -0
- package/src/hooks.ts +428 -0
- package/src/index.ts +135 -0
- package/src/loader.test.ts +442 -0
- package/src/loader.ts +213 -0
- package/src/media/index.ts +16 -0
- package/src/media/render-cache.ts +27 -0
- package/src/media/seekable.ts +35 -0
- package/src/media/thumb-input.test.ts +109 -0
- package/src/media/thumb-input.ts +194 -0
- package/src/nested-view.test.ts +223 -0
- package/src/nested-view.ts +239 -0
- package/src/probe/audio.test.ts +135 -0
- package/src/probe/audio.ts +140 -0
- package/src/probe/av.ts +84 -0
- package/src/probe/ffprobe.ts +197 -0
- package/src/probe/image.test.ts +251 -0
- package/src/probe/image.ts +336 -0
- package/src/probe/index.ts +54 -0
- package/src/probe/sniff.test.ts +72 -0
- package/src/probe/sniff.ts +67 -0
- package/src/probe/video.test.ts +41 -0
- package/src/probe/video.ts +116 -0
- package/src/probe-cache.test.ts +89 -0
- package/src/probe-cache.ts +65 -0
- package/src/render/ffmpeg.ts +78 -0
- package/src/render/index.ts +34 -0
- package/src/render/pipeline.test.ts +187 -0
- package/src/render/pipeline.ts +479 -0
- package/src/sandbox/call-timers.ts +68 -0
- package/src/sandbox/fixtures/api-error-plugin.mjs +11 -0
- package/src/sandbox/fixtures/asset-plugin.mjs +13 -0
- package/src/sandbox/fixtures/chatty-plugin.mjs +14 -0
- package/src/sandbox/fixtures/container-plugin.mjs +8 -0
- package/src/sandbox/fixtures/context-plugin.mjs +19 -0
- package/src/sandbox/fixtures/crash-plugin.mjs +6 -0
- package/src/sandbox/fixtures/echo-plugin.mjs +17 -0
- package/src/sandbox/fixtures/exit-plugin.mjs +6 -0
- package/src/sandbox/fixtures/fetch-plugin.mjs +8 -0
- package/src/sandbox/fixtures/flood-api-plugin.mjs +8 -0
- package/src/sandbox/fixtures/flood-log-plugin.mjs +8 -0
- package/src/sandbox/fixtures/hostile-dynamic-plugin.mjs +9 -0
- package/src/sandbox/fixtures/hostile-plugin.mjs +13 -0
- package/src/sandbox/fixtures/huge-result-plugin.mjs +8 -0
- package/src/sandbox/fixtures/logging-plugin.mjs +9 -0
- package/src/sandbox/fixtures/range-plugin.mjs +9 -0
- package/src/sandbox/fixtures/slow-api-plugin.mjs +11 -0
- package/src/sandbox/fixtures/spin-plugin.mjs +9 -0
- package/src/sandbox/fixtures/stuck-plugin.mjs +12 -0
- package/src/sandbox/fixtures/thrower-plugin.mjs +7 -0
- package/src/sandbox/host.test.ts +619 -0
- package/src/sandbox/host.ts +933 -0
- package/src/sandbox/host.unit.test.ts +330 -0
- package/src/sandbox/protocol.test.ts +85 -0
- package/src/sandbox/protocol.ts +147 -0
- package/src/sandbox/sandboxed-plugin.ts +30 -0
- package/src/sandbox/worker-entry.mjs +422 -0
- package/src/seed.test.ts +102 -0
- package/src/seed.ts +160 -0
- package/src/settings-store.ts +26 -0
- package/src/types.ts +21 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
import { PluginManifestId, PluginManifest, PluginDefinition, PluginDownloadRequest, PluginDownloadResult, PluginAssetDeleteResult, PluginPermissions, PluginSchema, ResourceAPI, ProbeResult, HookName, ImageHashKind, Detection, SerializedFileList, ImageHashesResult } from '@hoardodile/sdk-types';
|
|
2
|
+
export { AudioCoverArt, AudioInfo, AudioTags, Detection, HOOK_NAMES, HookName, ImageInfo, Logger, PluginDefinition, ReadFileRange, ResourceAPI, ResourceAPIFixtureConfig, VideoInfo, assertPluginShape, createFailingPlugin, createResourceAPIFixture, definePlugin, isDetected, isMissed, stubLogger } from '@hoardodile/sdk-types';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
4
|
+
import { I as ImageMetadataInput, b as ImageSourceProbe, A as AvProbeOptions, E as ExtractProgress } from './image-Dx2Be3Bj.js';
|
|
5
|
+
import { R as ResourceContainer } from './container-fKxefO7p.js';
|
|
6
|
+
import { P as PluginProbeCache } from './probe-cache-BkRI7HEK.js';
|
|
7
|
+
export { a as PLUGIN_PROBE_CACHE_MAX_ENTRIES, c as createProbeCache } from './probe-cache-BkRI7HEK.js';
|
|
8
|
+
import { PluginCapabilityKey } from '@hoardodile/sdk-types/plugin-capabilities';
|
|
9
|
+
import 'sharp';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Process-wide cache of nested container listings (central directories
|
|
13
|
+
* of zip/tar entries addressed via virtual paths). Resolving
|
|
14
|
+
* `outer!inner` re-parses the outer archive's CD on every hook call;
|
|
15
|
+
* this cache keeps the parsed listing resident so a multi-hook pass
|
|
16
|
+
* (sourceMeta → cover → hashes → listFiles) parses each nested archive
|
|
17
|
+
* once.
|
|
18
|
+
*
|
|
19
|
+
* Keyed by the outer entry name only — callers scope the cache instance
|
|
20
|
+
* to a fixed (resId, fileVersion), and archives are immutable per
|
|
21
|
+
* version, so a key never outlives its content.
|
|
22
|
+
*/
|
|
23
|
+
type NestedCdCache = {
|
|
24
|
+
readonly get: (key: string) => Promise<unknown> | undefined;
|
|
25
|
+
readonly set: (key: string, value: Promise<unknown>) => void;
|
|
26
|
+
readonly clear: () => void;
|
|
27
|
+
};
|
|
28
|
+
type NestedCdCacheOptions = {
|
|
29
|
+
/** Max number of nested archives kept resident. */
|
|
30
|
+
readonly maxEntries: number;
|
|
31
|
+
};
|
|
32
|
+
/** Create an LRU-bounded nested listing cache. */
|
|
33
|
+
declare function createNestedCdCache(opts?: Partial<NestedCdCacheOptions>): NestedCdCache;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Central directory entry shape exposed for the read-path cache. Mirrors
|
|
37
|
+
* the fields yauzl surfaces, plus `dataOffset` resolved from the local
|
|
38
|
+
* header. `dataOffset` and `dataSize` together pinpoint the entry's raw
|
|
39
|
+
* bytes inside the zip file; for STORED entries those bytes are the file
|
|
40
|
+
* itself and can be streamed via {@link streamRange} with a
|
|
41
|
+
* `[start, end]` window.
|
|
42
|
+
*
|
|
43
|
+
* The central directory is parsed by yauzl (the only listing engine —
|
|
44
|
+
* the hand-rolled fast parser was removed; ZIP64 falls out naturally).
|
|
45
|
+
*/
|
|
46
|
+
type ZipEntry = {
|
|
47
|
+
readonly name: string;
|
|
48
|
+
readonly compressionMethod: number;
|
|
49
|
+
/** General-purpose bit 0: the entry data is encrypted. */
|
|
50
|
+
readonly encrypted: boolean;
|
|
51
|
+
readonly uncompressedSize: number;
|
|
52
|
+
readonly compressedSize: number;
|
|
53
|
+
readonly crc32: number;
|
|
54
|
+
readonly localHeaderOffset: number;
|
|
55
|
+
/**
|
|
56
|
+
* Absolute byte offset of the entry's raw data inside the zip file.
|
|
57
|
+
* Computed as `localHeaderOffset + 30 + nameLen + extraLen` where
|
|
58
|
+
* nameLen and extraLen are read from the local file header (not the
|
|
59
|
+
* central directory) because they can differ between the two.
|
|
60
|
+
*/
|
|
61
|
+
readonly dataOffset: number;
|
|
62
|
+
readonly dataSize: number;
|
|
63
|
+
readonly modifiedAt: number;
|
|
64
|
+
};
|
|
65
|
+
/** Read the central directory of the zip at `zipPath`. */
|
|
66
|
+
declare function listZipEntries(zipPath: string, opts?: {
|
|
67
|
+
readonly dataOffsets?: boolean;
|
|
68
|
+
}): Promise<readonly ZipEntry[]>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Materialization primitives shared by every code path that writes
|
|
72
|
+
* derived files into the extraction cache: the host's container
|
|
73
|
+
* extractor and the server's artifact view (thumb/probe materialization
|
|
74
|
+
* for both literal and virtual entries). One partial+rename+size-check
|
|
75
|
+
* recipe and one single-flight guard, instead of a copy per caller.
|
|
76
|
+
*/
|
|
77
|
+
/**
|
|
78
|
+
* Write `openStream`'s bytes to `target` atomically: stream to a
|
|
79
|
+
* `.partial` sibling, verify the written size matches `expectedSize`,
|
|
80
|
+
* then rename into place. On any failure the partial is removed and the
|
|
81
|
+
* error rethrown. An already-correct `target` is left untouched.
|
|
82
|
+
*
|
|
83
|
+
* With `key` set, concurrent calls for the same key share one in-flight
|
|
84
|
+
* write instead of racing it (via {@link globalMaterializeInflight}).
|
|
85
|
+
*/
|
|
86
|
+
declare function materializeFile(opts: {
|
|
87
|
+
readonly openStream: () => Readable | Promise<Readable>;
|
|
88
|
+
readonly target: string;
|
|
89
|
+
readonly expectedSize: number;
|
|
90
|
+
readonly key?: string;
|
|
91
|
+
}): Promise<void>;
|
|
92
|
+
|
|
93
|
+
/** A plugin discovered on disk — not yet imported/activated. */
|
|
94
|
+
type FoundPlugin = {
|
|
95
|
+
readonly id: PluginManifestId;
|
|
96
|
+
readonly manifest: PluginManifest;
|
|
97
|
+
readonly diskPath: string;
|
|
98
|
+
readonly source: "builtin" | "dev" | "disk";
|
|
99
|
+
readonly enabled: boolean;
|
|
100
|
+
readonly priority: number;
|
|
101
|
+
readonly pinned: boolean;
|
|
102
|
+
readonly color: string;
|
|
103
|
+
};
|
|
104
|
+
/** A plugin known to the DB but missing from disk. */
|
|
105
|
+
type MissingPlugin = {
|
|
106
|
+
readonly id: PluginManifestId;
|
|
107
|
+
readonly manifest: PluginManifest;
|
|
108
|
+
readonly enabled: boolean;
|
|
109
|
+
readonly priority: number;
|
|
110
|
+
readonly pinned: boolean;
|
|
111
|
+
readonly color: string;
|
|
112
|
+
};
|
|
113
|
+
/** A loaded plugin entry in the runtime registry. */
|
|
114
|
+
type PluginRegistryEntry = {
|
|
115
|
+
readonly id: PluginManifestId;
|
|
116
|
+
readonly manifest: PluginManifest;
|
|
117
|
+
readonly enabled: boolean;
|
|
118
|
+
readonly priority: number;
|
|
119
|
+
readonly pinned: boolean;
|
|
120
|
+
readonly color: string;
|
|
121
|
+
readonly missing: boolean;
|
|
122
|
+
readonly builtin: boolean;
|
|
123
|
+
readonly dev: boolean;
|
|
124
|
+
readonly plugin: PluginDefinition;
|
|
125
|
+
/** Absolute path to the plugin directory on disk. Undefined for builtin / in-memory plugins. */
|
|
126
|
+
readonly diskPath?: string;
|
|
127
|
+
};
|
|
128
|
+
/** The plugin registry — built at startup and kept in memory. */
|
|
129
|
+
type PluginRegistry = {
|
|
130
|
+
getAll(): readonly PluginRegistryEntry[];
|
|
131
|
+
getEnabled(): readonly PluginRegistryEntry[];
|
|
132
|
+
getById(id: PluginManifestId): PluginRegistryEntry | undefined;
|
|
133
|
+
getBuiltin(): PluginRegistryEntry | undefined;
|
|
134
|
+
getForResource(resPluginId: PluginManifestId): PluginRegistryEntry | undefined;
|
|
135
|
+
updateEntry(id: PluginManifestId, patch: Partial<Pick<PluginRegistryEntry, "enabled" | "priority" | "pinned" | "color" | "missing">>): void;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The plugin asset handler: the host-side implementation of the
|
|
140
|
+
* `download`/`statAsset`/`readAsset`/`deleteAsset` ResourceAPI methods,
|
|
141
|
+
* bound to the plugin id (the sandbox knows which plugin issued the
|
|
142
|
+
* call). Wired by the app server; hosts without a consent channel (CLI,
|
|
143
|
+
* workbench) omit it and the methods answer `UNAVAILABLE`.
|
|
144
|
+
*/
|
|
145
|
+
type PluginAssetHandler = {
|
|
146
|
+
readonly download: (pluginId: string, request: PluginDownloadRequest) => Promise<PluginDownloadResult>;
|
|
147
|
+
readonly statAsset: (pluginId: string, path: string) => Promise<{
|
|
148
|
+
readonly sizeBytes: number;
|
|
149
|
+
} | undefined>;
|
|
150
|
+
readonly readAsset: (pluginId: string, path: string) => Promise<Uint8Array>;
|
|
151
|
+
readonly deleteAsset: (pluginId: string, path: string) => Promise<PluginAssetDeleteResult>;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Kill a plugin sandbox process when an invocation neither returns nor
|
|
155
|
+
* shows resource-API activity for this long. Hooks that keep calling the
|
|
156
|
+
* API reset the watchdog continuously and never trip it; time spent inside
|
|
157
|
+
* a host-side API call does not count as inactivity.
|
|
158
|
+
*/
|
|
159
|
+
declare const PLUGIN_WATCHDOG_TIMEOUT_MS = 60000;
|
|
160
|
+
/** Absolute cap for a single plugin hook invocation, regardless of activity. */
|
|
161
|
+
declare const PLUGIN_HOOK_HARD_TIMEOUT_MS: number;
|
|
162
|
+
/** V8 old-generation memory cap per plugin sandbox process, in MiB. */
|
|
163
|
+
declare const PLUGIN_WORKER_MAX_OLD_SPACE_MB = 512;
|
|
164
|
+
/**
|
|
165
|
+
* Absolute cap for one hook result crossing the IPC boundary back to the
|
|
166
|
+
* host. Exceeding it turns the hook into an error instead of letting a
|
|
167
|
+
* hostile bundle clone a giant payload into the host process.
|
|
168
|
+
*/
|
|
169
|
+
declare const PLUGIN_MAX_RESULT_BYTES: number;
|
|
170
|
+
/** Cap on `log*` messages per hook invocation (see {@link PluginSandboxConfig}). */
|
|
171
|
+
declare const PLUGIN_MAX_LOGS_PER_HOOK = 1000;
|
|
172
|
+
/** Cap on ResourceAPI calls per hook invocation (see {@link PluginSandboxConfig}). */
|
|
173
|
+
declare const PLUGIN_MAX_API_CALLS_PER_HOOK = 100000;
|
|
174
|
+
/**
|
|
175
|
+
* Max sandbox spawns per plugin within {@link PLUGIN_WORKER_RESPAWN_WINDOW_MS}
|
|
176
|
+
* before the plugin is degraded. It recovers automatically once the crash
|
|
177
|
+
* window slides clean, or immediately on disable/rescan.
|
|
178
|
+
*/
|
|
179
|
+
declare const PLUGIN_WORKER_MAX_RESPAWNS = 3;
|
|
180
|
+
/** Sliding window for {@link PLUGIN_WORKER_MAX_RESPAWNS}. */
|
|
181
|
+
declare const PLUGIN_WORKER_RESPAWN_WINDOW_MS = 60000;
|
|
182
|
+
type PluginSandboxConfig = {
|
|
183
|
+
/**
|
|
184
|
+
* Kill the sandbox process when an invocation neither returns nor shows
|
|
185
|
+
* API activity for this long. Long-running hooks that keep calling the
|
|
186
|
+
* resource API reset the watchdog continuously and never trip it;
|
|
187
|
+
* time spent inside a host-side API call does not count as inactivity.
|
|
188
|
+
*/
|
|
189
|
+
readonly watchdogMs: number;
|
|
190
|
+
/** Absolute per-invocation cap, regardless of activity. */
|
|
191
|
+
readonly hardTimeoutMs: number;
|
|
192
|
+
/** V8 old-generation cap per sandbox process; exceeding it aborts it. */
|
|
193
|
+
readonly maxOldSpaceMb: number;
|
|
194
|
+
/**
|
|
195
|
+
* Max sandbox spawns per plugin within {@link respawnWindowMs} before the
|
|
196
|
+
* plugin is degraded (all invocations reject). The plugin recovers
|
|
197
|
+
* automatically once the crash window slides clean; `unloadPlugin`
|
|
198
|
+
* (disable or rescan) resets the budget immediately.
|
|
199
|
+
*/
|
|
200
|
+
readonly maxRespawns: number;
|
|
201
|
+
readonly respawnWindowMs: number;
|
|
202
|
+
/** Absolute cap for one hook result returning to the host. */
|
|
203
|
+
readonly maxResultBytes: number;
|
|
204
|
+
/**
|
|
205
|
+
* Cap on `log*` messages per hook invocation; exceeding it fails the
|
|
206
|
+
* hook (log messages reset the watchdog, so a log flood would
|
|
207
|
+
* otherwise stay alive until the hard timeout).
|
|
208
|
+
*/
|
|
209
|
+
readonly maxLogsPerHook: number;
|
|
210
|
+
/**
|
|
211
|
+
* Cap on ResourceAPI calls per hook invocation; exceeding it fails the
|
|
212
|
+
* hook. Sized generously so large per-file scans (hashBytes over tens
|
|
213
|
+
* of thousands of files) keep working — the cap bounds a runaway RPC
|
|
214
|
+
* fan-out that would otherwise pin the host's CPU.
|
|
215
|
+
*/
|
|
216
|
+
readonly maxApiCallsPerHook: number;
|
|
217
|
+
/**
|
|
218
|
+
* Permission-model flag to pass to the sandbox child, or `undefined` to
|
|
219
|
+
* probe the running Node for the first accepted name. Tests override
|
|
220
|
+
* this to exercise the fail-closed path.
|
|
221
|
+
*/
|
|
222
|
+
readonly permissionFlag?: string;
|
|
223
|
+
/**
|
|
224
|
+
* Host-managed plugin vault directory (`<plugin-dir>/vault/`, or the
|
|
225
|
+
* versioned-storage vault for dev plugins). When set, the child gets
|
|
226
|
+
* an extra fs-read grant and the module policy gate allows loading
|
|
227
|
+
* files from it — downloaded runtimes (JS/WASM) importable from the
|
|
228
|
+
* vault are the point of the asset API. A function form lets one
|
|
229
|
+
* shared sandbox resolve the path per plugin (disk vs dev plugins
|
|
230
|
+
* store their vault differently).
|
|
231
|
+
*/
|
|
232
|
+
readonly assetVaultDir?: string | ((pluginId: string) => string | undefined);
|
|
233
|
+
/**
|
|
234
|
+
* Plugin asset handler wired by the app server. Absent → the asset
|
|
235
|
+
* methods answer `UNAVAILABLE` (CLI, workbench, tests without a
|
|
236
|
+
* consent channel).
|
|
237
|
+
*/
|
|
238
|
+
readonly pluginAssets?: PluginAssetHandler;
|
|
239
|
+
};
|
|
240
|
+
declare const DEFAULT_SANDBOX_CONFIG: PluginSandboxConfig;
|
|
241
|
+
type PluginSandbox = {
|
|
242
|
+
/**
|
|
243
|
+
* Register and load a plugin bundle. With `eager` the sandbox process
|
|
244
|
+
* stays alive; without it the hook list is probed and the process
|
|
245
|
+
* immediately idles (it respawns lazily on first invocation — disabled
|
|
246
|
+
* plugins still serve their bound resources without holding a process).
|
|
247
|
+
*
|
|
248
|
+
* Reloading an already-registered id keeps the previous process alive
|
|
249
|
+
* until the new bundle loads successfully — a failed reload returns
|
|
250
|
+
* the previous definition instead of stranding a disposed registry.
|
|
251
|
+
*
|
|
252
|
+
* Returns `undefined` when the bundle cannot be loaded and there is no
|
|
253
|
+
* previous definition to fall back on (already logged) — callers fall
|
|
254
|
+
* back to a failing plugin.
|
|
255
|
+
*/
|
|
256
|
+
readonly loadPlugin: (opts: {
|
|
257
|
+
readonly id: string;
|
|
258
|
+
readonly mainPath: string;
|
|
259
|
+
readonly eager: boolean;
|
|
260
|
+
/**
|
|
261
|
+
* Manifest permissions the sandbox enforces on every API call
|
|
262
|
+
* (e.g. the `container` gate). Absent keys count as denied here,
|
|
263
|
+
* so a caller that omits this opts into the strictest view.
|
|
264
|
+
*/
|
|
265
|
+
readonly permissions?: PluginPermissions;
|
|
266
|
+
}) => Promise<PluginDefinition | undefined>;
|
|
267
|
+
/**
|
|
268
|
+
* Terminate the plugin's sandbox process (if any) and reset its respawn
|
|
269
|
+
* budget. The hook list stays known; the next invocation lazily respawns.
|
|
270
|
+
*/
|
|
271
|
+
readonly unloadPlugin: (id: string) => void;
|
|
272
|
+
/**
|
|
273
|
+
* Terminate and forget every plugin whose id is not in `keepIds`.
|
|
274
|
+
* Registered ids keep their processes — used by the loader after a
|
|
275
|
+
* successful reload to free plugins that left the registry.
|
|
276
|
+
*/
|
|
277
|
+
readonly disposeExcept: (keepIds: ReadonlySet<string>) => Promise<void>;
|
|
278
|
+
/** Terminate every process and forget all plugins. Pending invocations reject. */
|
|
279
|
+
readonly disposeAll: () => Promise<void>;
|
|
280
|
+
};
|
|
281
|
+
declare function createPluginSandbox(config?: PluginSandboxConfig): PluginSandbox;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Plugin activation: turns discovered plugin candidates (found on disk
|
|
285
|
+
* or recorded as missing) into ready {@link PluginRegistryEntry}s by
|
|
286
|
+
* loading their `main.js` into the sandbox. Loads run in parallel —
|
|
287
|
+
* sequential worker spawns would multiply the boot cost on slow or
|
|
288
|
+
* antivirus-scanned disks.
|
|
289
|
+
*/
|
|
290
|
+
type PluginActivation = {
|
|
291
|
+
/** Load every found plugin's `main.js` into the sandbox and return the full registry entries. */
|
|
292
|
+
readonly activateAll: (found: readonly FoundPlugin[]) => Promise<PluginRegistryEntry[]>;
|
|
293
|
+
/**
|
|
294
|
+
* Build registry entries for plugins whose directory is missing, each
|
|
295
|
+
* carrying a failing definition so callers can still render them as
|
|
296
|
+
* broken in the UI.
|
|
297
|
+
*/
|
|
298
|
+
readonly createFailingEntries: (missing: readonly MissingPlugin[]) => PluginRegistryEntry[];
|
|
299
|
+
};
|
|
300
|
+
type PluginActivationDeps = {
|
|
301
|
+
readonly sandbox: PluginSandbox;
|
|
302
|
+
};
|
|
303
|
+
/**
|
|
304
|
+
* Create the activation pipeline. The sandbox decides worker lifetimes:
|
|
305
|
+
* enabled plugins keep their worker alive, disabled ones only probe the
|
|
306
|
+
* hook list and respawn lazily if a bound resource still invokes a hook.
|
|
307
|
+
*/
|
|
308
|
+
declare function createPluginActivation(deps: PluginActivationDeps): PluginActivation;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* A {@link ResourceContainer} over a raw filesystem directory. Used to
|
|
312
|
+
* run plugin hooks against a plain directory — during import, before
|
|
313
|
+
* resources exist, or from the CLI. Reads only; the directory is treated
|
|
314
|
+
* as an immutable snapshot for the container's lifetime.
|
|
315
|
+
*/
|
|
316
|
+
declare function createDirectoryContainer(dir: string): ResourceContainer;
|
|
317
|
+
/**
|
|
318
|
+
* Resolve a plugin-supplied relative path against an import directory,
|
|
319
|
+
* rejecting attempts to escape the directory or use absolute paths.
|
|
320
|
+
*/
|
|
321
|
+
declare function resolveSafeImportPath(dir: string, relPath: string): string;
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Create a minimal {@link ResourceAPI} backed by a raw filesystem
|
|
325
|
+
* directory. Used to run plugin hooks against a plain directory — during
|
|
326
|
+
* import, before resources exist, or from the CLI. Built on the shared
|
|
327
|
+
* {@link createPluginResourceAPI} so directory behavior matches the
|
|
328
|
+
* archive-backed and fixture backends; probes are unsupported and resolve
|
|
329
|
+
* to `undefined`.
|
|
330
|
+
*/
|
|
331
|
+
declare function createDirectoryResourceAPI<TSchema extends PluginSchema = PluginSchema>(dir: string, opts?: {
|
|
332
|
+
readonly maxReadFileBytes?: number;
|
|
333
|
+
/**
|
|
334
|
+
* Writable directory for `extractArchive` materialization (the
|
|
335
|
+
* CLI passes a temp dir). Virtual-path reads (`outer!inner`) work
|
|
336
|
+
* without it; only the materializing hook needs it.
|
|
337
|
+
*/
|
|
338
|
+
readonly extractCacheDir?: string;
|
|
339
|
+
/**
|
|
340
|
+
* Session context handed to hooks as `api.context.detect` (see
|
|
341
|
+
* `CreatePluginResourceAPIDeps.detectContext`).
|
|
342
|
+
*/
|
|
343
|
+
readonly detectContext?: unknown;
|
|
344
|
+
}): ResourceAPI<TSchema>;
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Construct a {@link ResourceAPI} on top of a {@link ResourceContainer}.
|
|
348
|
+
* The container abstracts away the storage shape (bare-file resource
|
|
349
|
+
* folder, raw directory, in-memory fixture) so plugin code stays
|
|
350
|
+
* unaware of how source bytes are stored.
|
|
351
|
+
*/
|
|
352
|
+
type CreatePluginResourceAPIDeps = {
|
|
353
|
+
readonly view: ResourceContainer;
|
|
354
|
+
/**
|
|
355
|
+
* Probe implementations, both optional: when absent the matching
|
|
356
|
+
* branch of `probe` resolves to
|
|
357
|
+
* `{ kind: "unknown", reason: "unavailable" }` without opening any
|
|
358
|
+
* stream — the directory backend ships without probes by design.
|
|
359
|
+
* `sniff` never needs a backend; it reads the file's own header.
|
|
360
|
+
*/
|
|
361
|
+
readonly probeImage?: (source: ImageMetadataInput, extHint?: string) => Promise<ImageSourceProbe | undefined>;
|
|
362
|
+
readonly probeAv?: (source: string | Readable, opts: AvProbeOptions) => Promise<ProbeResult>;
|
|
363
|
+
/** Per-call `readFile` byte cap. Defaults to {@link PLUGIN_READ_FILE_MAX_BYTES}. */
|
|
364
|
+
readonly maxReadFileBytes?: number;
|
|
365
|
+
/**
|
|
366
|
+
* Process-wide probe cache. Only active together with
|
|
367
|
+
* {@link cacheScope}; without it every probe opens a fresh stream.
|
|
368
|
+
*/
|
|
369
|
+
readonly probeCache?: PluginProbeCache;
|
|
370
|
+
/**
|
|
371
|
+
* Cache namespace for this API instance, typically
|
|
372
|
+
* `${resId}:${fileVersion}`. Archives are immutable per version, so
|
|
373
|
+
* cached probe results never need explicit invalidation.
|
|
374
|
+
*/
|
|
375
|
+
readonly cacheScope?: string;
|
|
376
|
+
/**
|
|
377
|
+
* Absolute directory for plugin container extractions (e.g. the
|
|
378
|
+
* server's `local/cache/resources/<id>/extracted/v<N>/archives`, or a
|
|
379
|
+
* temp dir for the CLI). When absent, `extractArchive` rejects with
|
|
380
|
+
* a clear message.
|
|
381
|
+
*/
|
|
382
|
+
readonly extractCacheDir?: string;
|
|
383
|
+
/**
|
|
384
|
+
* Hard caps for one `extractArchive` call. Defaults to
|
|
385
|
+
* {@link DEFAULT_PLUGIN_EXTRACT_MAX_BYTES} /
|
|
386
|
+
* {@link DEFAULT_PLUGIN_EXTRACT_MAX_ENTRIES}.
|
|
387
|
+
*/
|
|
388
|
+
readonly maxExtractBytes?: number;
|
|
389
|
+
readonly maxExtractEntries?: number;
|
|
390
|
+
/**
|
|
391
|
+
* Called with per-entry materialization progress during
|
|
392
|
+
* `extractArchive`, so the server can surface it to the user.
|
|
393
|
+
*/
|
|
394
|
+
readonly onExtractProgress?: (progress: ExtractProgress) => void;
|
|
395
|
+
/**
|
|
396
|
+
* Process-wide nested central-directory cache. When provided, virtual
|
|
397
|
+
* path resolution (`outer!inner`) reuses parsed listings across hook
|
|
398
|
+
* calls instead of re-reading each archive's CD per invocation. The
|
|
399
|
+
* server passes one cache per (resId, fileVersion) scope.
|
|
400
|
+
*/
|
|
401
|
+
readonly nestedCdCache?: NestedCdCache;
|
|
402
|
+
/**
|
|
403
|
+
* Session context handed to hooks as `api.context.detect`: the
|
|
404
|
+
* payload a prior `detect` invocation returned on a successful
|
|
405
|
+
* match. The sandbox worker injects it itself; this escape hatch
|
|
406
|
+
* lets in-process hosts (the dev runner) do the same.
|
|
407
|
+
*/
|
|
408
|
+
readonly detectContext?: unknown;
|
|
409
|
+
};
|
|
410
|
+
declare const DEFAULT_PLUGIN_EXTRACT_MAX_BYTES: number;
|
|
411
|
+
declare const DEFAULT_PLUGIN_EXTRACT_MAX_ENTRIES = 200000;
|
|
412
|
+
declare function createPluginResourceAPI<TSchema extends PluginSchema = PluginSchema>(deps: CreatePluginResourceAPIDeps): ResourceAPI<TSchema>;
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Capabilities a plugin manifest can declare — derived from the single
|
|
416
|
+
* {@link PLUGIN_CAPABILITY_GATES} table (which is itself keyed by the
|
|
417
|
+
* manifest's `PluginPermissions` keys), so the guard can never drift.
|
|
418
|
+
*/
|
|
419
|
+
type PluginCapability = PluginCapabilityKey;
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Permission checks against a plugin's declared manifest permissions,
|
|
423
|
+
* reading the capability vocabulary from the shared gates table.
|
|
424
|
+
* The server uses these before routing any plugin-scoped work so a
|
|
425
|
+
* manifest that does not declare a capability cannot trigger it.
|
|
426
|
+
*/
|
|
427
|
+
type CapabilityGuard = {
|
|
428
|
+
/** Check whether a manifest grants the given capability. */
|
|
429
|
+
readonly check: (manifest: PluginManifest, capability: PluginCapability) => boolean;
|
|
430
|
+
/** Assert that a manifest grants the given capability; throw if not. */
|
|
431
|
+
readonly require: (manifest: PluginManifest, capability: PluginCapability) => void;
|
|
432
|
+
/** Filter entries to only those that grant the given capability. */
|
|
433
|
+
readonly filter: (entries: readonly PluginRegistryEntry[], capability: PluginCapability) => readonly PluginRegistryEntry[];
|
|
434
|
+
};
|
|
435
|
+
/** Create a stateless capability guard over manifest permissions. */
|
|
436
|
+
declare function createCapabilityGuard(): CapabilityGuard;
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Plugin hooks the dev runner can invoke against a directory — the
|
|
440
|
+
* contract-order list shared with the sandbox host and the CLI.
|
|
441
|
+
*/
|
|
442
|
+
type PluginHookName = HookName;
|
|
443
|
+
/**
|
|
444
|
+
* Load a built plugin `main.js` and invoke one hook with a
|
|
445
|
+
* directory-backed {@link ResourceAPI}. Development aid only — the plugin
|
|
446
|
+
* runs unsandboxed in the current process, so only run code you trust.
|
|
447
|
+
* Consumed from plugin tests as a devDependency; never bundled into a
|
|
448
|
+
* shipped plugin.
|
|
449
|
+
*
|
|
450
|
+
* Before the target hook runs, `detect` is invoked once (when the
|
|
451
|
+
* target is not detect itself) so hooks see the session context a real
|
|
452
|
+
* sandbox session would have — `api.context.detect` carries the
|
|
453
|
+
* detection payload. A throwing or missed detection leaves the context
|
|
454
|
+
* absent, exactly like a fresh worker in production.
|
|
455
|
+
*/
|
|
456
|
+
declare function runPluginHook(opts: {
|
|
457
|
+
readonly mainPath: string;
|
|
458
|
+
readonly hook: PluginHookName;
|
|
459
|
+
readonly dir: string;
|
|
460
|
+
/** Scratch dir for `extractArchive` materialization. */
|
|
461
|
+
readonly extractCacheDir?: string;
|
|
462
|
+
}): Promise<{
|
|
463
|
+
readonly result: unknown;
|
|
464
|
+
readonly durationMs: number;
|
|
465
|
+
}>;
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* A plugin's persisted settings — the union of what the server's DB row
|
|
469
|
+
* and the CLI's defaults provide. `undefined` field values mean "no
|
|
470
|
+
* setting recorded" (fresh install), not a stored default.
|
|
471
|
+
*/
|
|
472
|
+
type PluginSettingsRow = {
|
|
473
|
+
readonly id: PluginManifestId;
|
|
474
|
+
/** Raw `manifest.json` text as stored by the consumer. */
|
|
475
|
+
readonly manifest: string;
|
|
476
|
+
readonly enabled: boolean;
|
|
477
|
+
readonly priority: number;
|
|
478
|
+
readonly pinned: boolean;
|
|
479
|
+
readonly color: string;
|
|
480
|
+
};
|
|
481
|
+
/**
|
|
482
|
+
* Storage seam for plugin settings (enablement, priority, pin, color).
|
|
483
|
+
* The server implements it over its `content_plugins` table; the CLI uses
|
|
484
|
+
* an in-memory default. Keeps the host free of any database dependency.
|
|
485
|
+
*/
|
|
486
|
+
type PluginSettingsStore = {
|
|
487
|
+
readonly get: (id: PluginManifestId) => PluginSettingsRow | undefined;
|
|
488
|
+
readonly all: () => readonly PluginSettingsRow[];
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
type PluginDiscoveryDeps = {
|
|
492
|
+
/** Directory of the mandatory builtin plugin (`manifest.json` at its root). */
|
|
493
|
+
readonly builtinDir?: string;
|
|
494
|
+
/** Extra plugin directories for development (override same-id disk plugins). */
|
|
495
|
+
readonly devPluginDirs?: readonly string[];
|
|
496
|
+
/** Directory where installed plugins live, one subdirectory per plugin. */
|
|
497
|
+
readonly pluginsDir: string;
|
|
498
|
+
/** DB-backed per-plugin settings (enabled/priority/pinned/color). */
|
|
499
|
+
readonly settings: PluginSettingsStore;
|
|
500
|
+
/** Skip dev plugins entirely (e.g. production configuration). */
|
|
501
|
+
readonly disableDevPlugins?: boolean;
|
|
502
|
+
};
|
|
503
|
+
/**
|
|
504
|
+
* Plugin discovery: finds plugins on disk (builtin, dev, installed),
|
|
505
|
+
* merges their DB settings, and reports which known plugins are missing.
|
|
506
|
+
*/
|
|
507
|
+
type PluginDiscovery = {
|
|
508
|
+
readonly discover: () => Promise<{
|
|
509
|
+
found: FoundPlugin[];
|
|
510
|
+
missing: MissingPlugin[];
|
|
511
|
+
}>;
|
|
512
|
+
};
|
|
513
|
+
/**
|
|
514
|
+
* Create the discovery pipeline. Order matters: the builtin plugin wins
|
|
515
|
+
* priority, dev plugins override same-id installed ones, and anything in
|
|
516
|
+
* settings but absent from disk is reported missing.
|
|
517
|
+
*/
|
|
518
|
+
declare function createPluginDiscovery(deps: PluginDiscoveryDeps): PluginDiscovery;
|
|
519
|
+
/**
|
|
520
|
+
* Read and validate `manifest.json` in `dirPath` against the zod
|
|
521
|
+
* contract. Logs a warning and returns `undefined` for every failure
|
|
522
|
+
* mode (missing file, unreadable, invalid JSON, schema violation) so
|
|
523
|
+
* callers can skip the directory without crashing the scan.
|
|
524
|
+
*/
|
|
525
|
+
declare function parseManifest(dirPath: string, dirName: string): PluginManifest | undefined;
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Host-local domain error. Mirrors the wire-format taxonomy of the app's
|
|
529
|
+
* domain errors (`@hoardodile/shared`) so errors raised here keep their
|
|
530
|
+
* code/kind/details across the package boundary — the server's error
|
|
531
|
+
* translation recognizes this shape structurally via `isDomainError`.
|
|
532
|
+
*
|
|
533
|
+
* The host deliberately keeps its dependency graph minimal (consts +
|
|
534
|
+
* sdk-types only) and does not depend on `@hoardodile/shared`; the
|
|
535
|
+
* mirror is kept in sync by the bridge test in
|
|
536
|
+
* `apps/server/src/infra/host-errors.test.ts`.
|
|
537
|
+
*/
|
|
538
|
+
declare const domainErrorCodes: readonly ["NOT_FOUND", "CONFLICT", "VALIDATION", "UNAUTHORIZED", "FORBIDDEN", "RATE_LIMITED", "UNSUPPORTED", "INTERNAL"];
|
|
539
|
+
type DomainErrorCode = (typeof domainErrorCodes)[number];
|
|
540
|
+
/**
|
|
541
|
+
* Host-local domain error carrying a wire-compatible code/kind/details
|
|
542
|
+
* triple. Thrown by host services (containers, loaders, hooks) so the
|
|
543
|
+
* server can translate them into its own domain errors verbatim.
|
|
544
|
+
*/
|
|
545
|
+
declare class DomainError extends Error {
|
|
546
|
+
readonly code: DomainErrorCode;
|
|
547
|
+
readonly kind: string;
|
|
548
|
+
readonly details?: Readonly<Record<string, unknown>>;
|
|
549
|
+
constructor(code: DomainErrorCode, kind: string, message: string, details?: Readonly<Record<string, unknown>>);
|
|
550
|
+
}
|
|
551
|
+
/** Convenience constructor for `VALIDATION` errors. */
|
|
552
|
+
declare function invalid(kind: string, message: string, details?: Readonly<Record<string, unknown>>): DomainError;
|
|
553
|
+
/** Convenience constructor for `CONFLICT` errors. */
|
|
554
|
+
declare function conflict(kind: string, message: string, details?: Readonly<Record<string, unknown>>): DomainError;
|
|
555
|
+
/** Convenience constructor for `NOT_FOUND` errors. */
|
|
556
|
+
declare function notFound(kind: string, message: string, details?: Readonly<Record<string, unknown>>): DomainError;
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Content hashing for the `imageHashes` plugin hook. All perceptual
|
|
560
|
+
* hashes derive from one 32×32 grayscale rendition (single sharp
|
|
561
|
+
* decode), so requesting several kinds costs one decode:
|
|
562
|
+
* - `sha256`: exact digest of the raw entry bytes (streamed).
|
|
563
|
+
* - `dhash`: difference hash — 9 samples per row of the 32×32 grid
|
|
564
|
+
* (nearest-neighbour spread), adjacent pairs compared, equivalent to
|
|
565
|
+
* the standard 9×8 dHash.
|
|
566
|
+
* - `phash`: standard DCT-based perceptual hash — the 32×32 grid is
|
|
567
|
+
* mean-pooled to 8×8, then the 8×8 DCT coefficients are thresholded
|
|
568
|
+
* at the median.
|
|
569
|
+
*
|
|
570
|
+
* The 64-bit values are hex strings (16 chars), compared by Hamming
|
|
571
|
+
* distance in `@hoardodile/sdk-server` / the server's hash service.
|
|
572
|
+
*/
|
|
573
|
+
/** Side of the grayscale grid every perceptual hash is derived from. */
|
|
574
|
+
declare const PHASH_GRID = 32;
|
|
575
|
+
/**
|
|
576
|
+
* Minimum grayscale standard deviation (0–255 scale) for an image to
|
|
577
|
+
* yield perceptual hashes. Near-flat images (solid fills, blank pages)
|
|
578
|
+
* hash to distance-0 neighbours of every other flat image, so they are
|
|
579
|
+
* skipped — `sha256` exact hashing is unaffected.
|
|
580
|
+
*/
|
|
581
|
+
declare const MIN_PERCEPTUAL_STDDEV = 8;
|
|
582
|
+
/**
|
|
583
|
+
* dHash over a `PHASH_GRID × PHASH_GRID` grayscale buffer: 9 samples per
|
|
584
|
+
* row (nearest-neighbour spread across the width), each of 8×8 bits
|
|
585
|
+
* comparing a sample against its right neighbour, packed MSB-first into
|
|
586
|
+
* a 16-char hex string.
|
|
587
|
+
*/
|
|
588
|
+
declare function computeDHash(gray: Uint8Array, grid?: number): string;
|
|
589
|
+
/**
|
|
590
|
+
* pHash over a `PHASH_GRID × PHASH_GRID` grayscale buffer: the grid is
|
|
591
|
+
* mean-pooled to 8×8, then the 8×8 type-II DCT coefficients (precomputed
|
|
592
|
+
* cosine tables) are thresholded at their median, each bit MSB-first
|
|
593
|
+
* into a 16-char hex string.
|
|
594
|
+
*/
|
|
595
|
+
declare function computePHash(gray: Uint8Array, grid?: number): string;
|
|
596
|
+
/**
|
|
597
|
+
* Population standard deviation of a grayscale buffer. Perceptual hashes
|
|
598
|
+
* of near-flat images (solid fills, blank pages) cluster at distance 0
|
|
599
|
+
* regardless of content — callers gate hash emission on this measure.
|
|
600
|
+
*/
|
|
601
|
+
declare function grayStddev(gray: Uint8Array): number;
|
|
602
|
+
/** Kinds whose values come from the single grayscale decode. */
|
|
603
|
+
declare const PERCEPTUAL_HASH_KINDS: readonly ImageHashKind[];
|
|
604
|
+
|
|
605
|
+
type PluginHooksDeps = {
|
|
606
|
+
/**
|
|
607
|
+
* Live accessor for the current registry — called on every hook
|
|
608
|
+
* invocation so a `rescan()` replacing the registry never leaves
|
|
609
|
+
* consumers holding a stale snapshot.
|
|
610
|
+
*/
|
|
611
|
+
readonly getRegistry: () => PluginRegistry;
|
|
612
|
+
};
|
|
613
|
+
/**
|
|
614
|
+
* Result of {@link PluginHooks.runMetaHooks}. A present key means the hook
|
|
615
|
+
* ran (permission granted + implemented); `value` is the raw hook result,
|
|
616
|
+
* which may still be `undefined` when the hook itself returned nothing.
|
|
617
|
+
*/
|
|
618
|
+
type PluginMetaHookResults = {
|
|
619
|
+
readonly sourceMeta?: {
|
|
620
|
+
readonly value: unknown;
|
|
621
|
+
};
|
|
622
|
+
readonly searchMeta?: {
|
|
623
|
+
readonly value: unknown;
|
|
624
|
+
};
|
|
625
|
+
};
|
|
626
|
+
/**
|
|
627
|
+
* The single entry point for executing plugin hooks. Owns every
|
|
628
|
+
* hook-invocation policy: priority iteration, builtin fallback, error
|
|
629
|
+
* swallowing/logging, capability checks, and result validation.
|
|
630
|
+
*
|
|
631
|
+
* Methods receive a ready-built {@link ResourceAPI} — this module knows
|
|
632
|
+
* nothing about resources, archives, or paths.
|
|
633
|
+
*/
|
|
634
|
+
type PluginHooks = {
|
|
635
|
+
/** The builtin fallback plugin id. Throws when no builtin is registered. */
|
|
636
|
+
readonly defaultPluginId: () => PluginManifestId;
|
|
637
|
+
/**
|
|
638
|
+
* The registry entry that should serve a resource's read paths right
|
|
639
|
+
* now: the resource's stored plugin when it is healthy (registered,
|
|
640
|
+
* enabled, not missing), otherwise the builtin fallback plugin.
|
|
641
|
+
* Throws when no builtin is registered.
|
|
642
|
+
*/
|
|
643
|
+
readonly getEffectiveEntry: (resPluginId: PluginManifestId | null) => PluginRegistryEntry;
|
|
644
|
+
/** Run all enabled plugins' detectors in priority order. Returns the first matching plugin id. Throws if no match (builtin should always match). */
|
|
645
|
+
readonly detectFirstMatch: (api: ResourceAPI) => Promise<PluginManifestId>;
|
|
646
|
+
/** Validate that the current plugin still matches. Returns confirmed plugin id or falls back to builtin id. */
|
|
647
|
+
readonly revalidate: (api: ResourceAPI, currentPluginId: PluginManifestId) => Promise<PluginManifestId>;
|
|
648
|
+
/** Run a specific plugin's detector. */
|
|
649
|
+
readonly detectForPlugin: (api: ResourceAPI, pluginId: PluginManifestId) => Promise<Detection>;
|
|
650
|
+
/**
|
|
651
|
+
* Detector pass for folder-import candidates: non-builtin detectors in
|
|
652
|
+
* priority order, falling back to the builtin plugin without invoking
|
|
653
|
+
* its detector.
|
|
654
|
+
*/
|
|
655
|
+
readonly detectForImportDir: (api: ResourceAPI) => Promise<PluginManifestId>;
|
|
656
|
+
/**
|
|
657
|
+
* Ask the owning plugin for a custom file list. Returns `undefined`
|
|
658
|
+
* when the plugin has no file list hook (or it failed).
|
|
659
|
+
*/
|
|
660
|
+
readonly buildFileList: (api: ResourceAPI, pluginId: PluginManifestId) => Promise<SerializedFileList | undefined>;
|
|
661
|
+
/** Ask the owning plugin which file should be used for the local cover. */
|
|
662
|
+
readonly resolveLocalCoverSource: (api: ResourceAPI, pluginId: PluginManifestId) => Promise<string | undefined>;
|
|
663
|
+
/**
|
|
664
|
+
* Run the capability-gated meta hooks (`sourceMeta`, `searchMeta`) of
|
|
665
|
+
* the owning plugin. Keys are absent when the permission is not
|
|
666
|
+
* granted or the hook is not implemented.
|
|
667
|
+
*/
|
|
668
|
+
readonly runMetaHooks: (api: ResourceAPI, pluginId: PluginManifestId) => Promise<PluginMetaHookResults>;
|
|
669
|
+
/**
|
|
670
|
+
* True when the plugin's manifest grants `imageHashes` and the plugin
|
|
671
|
+
* implements the hook. Used to decide whether a missing hash state is
|
|
672
|
+
* a rebuild gap or the plugin's legitimate choice.
|
|
673
|
+
*/
|
|
674
|
+
readonly supportsImageHashes: (pluginId: PluginManifestId) => boolean;
|
|
675
|
+
/**
|
|
676
|
+
* Run the capability-gated `imageHashes` hook of the owning plugin.
|
|
677
|
+
* Resolves to `undefined` when the permission is not granted, the
|
|
678
|
+
* hook is not implemented, or it threw. The returned entries are
|
|
679
|
+
* shape-validated (hex values, per-resource cap).
|
|
680
|
+
*/
|
|
681
|
+
readonly runImageHashes: (api: ResourceAPI, pluginId: PluginManifestId) => Promise<ImageHashesResult | undefined>;
|
|
682
|
+
};
|
|
683
|
+
declare function createPluginHooks(deps: PluginHooksDeps): PluginHooks;
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Copy the configured plugin directories into `pluginsDir` so discovery
|
|
687
|
+
* can load them as installed disk plugins. Each source is itself a plugin
|
|
688
|
+
* directory (`manifest.json` at its root). A seed replaces
|
|
689
|
+
* `pluginsDir/<manifest.id>` only when the file set, sizes, or content
|
|
690
|
+
* hashes differ — identical trees are left untouched so a boot does not
|
|
691
|
+
* dirty a synced `versions/` tree.
|
|
692
|
+
*/
|
|
693
|
+
declare function seedPlugins(pluginsDir: string, seedPluginDirs: readonly string[] | undefined): void;
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Loads plugins into a live {@link PluginRegistry}: seed the configured
|
|
697
|
+
* plugin directories, discover, activate, and sort by priority.
|
|
698
|
+
* `loadAll`/`rescan` are serialized — an overlapping run would replace
|
|
699
|
+
* workers out from under the previous run's in-flight loads.
|
|
700
|
+
*
|
|
701
|
+
* The previous registry and its workers are only replaced AFTER the new
|
|
702
|
+
* registry finished activating: a failed load never strands a stale
|
|
703
|
+
* registry whose workers were already disposed (every hook would then
|
|
704
|
+
* fail until the next restart).
|
|
705
|
+
*/
|
|
706
|
+
type PluginLoader = {
|
|
707
|
+
readonly loadAll: () => Promise<PluginRegistry>;
|
|
708
|
+
readonly rescan: () => Promise<PluginRegistry>;
|
|
709
|
+
/** The current registry; throws when `loadAll` has not run yet. */
|
|
710
|
+
readonly getRegistry: () => PluginRegistry;
|
|
711
|
+
};
|
|
712
|
+
type PluginLoaderDeps = {
|
|
713
|
+
readonly builtinDir?: string;
|
|
714
|
+
readonly devPluginDirs?: readonly string[];
|
|
715
|
+
/**
|
|
716
|
+
* Plugin directories (each with `manifest.json` at its root, e.g. a
|
|
717
|
+
* built plugin's `dist/`) that are seeded into `pluginsDir` on every
|
|
718
|
+
* load. Each plugin is copied into `pluginsDir/<manifest.id>` before
|
|
719
|
+
* discovery runs when the destination tree differs — so a seeded
|
|
720
|
+
* plugin behaves like a regular installed one (DB settings, caching,
|
|
721
|
+
* uninstall). The server passes an empty list and seeds through
|
|
722
|
+
* `writeVersioned` instead. When omitted, nothing is seeded.
|
|
723
|
+
*/
|
|
724
|
+
readonly seedPluginDirs?: readonly string[];
|
|
725
|
+
readonly pluginsDir: string;
|
|
726
|
+
readonly settings: PluginSettingsStore;
|
|
727
|
+
readonly disableDevPlugins?: boolean;
|
|
728
|
+
/**
|
|
729
|
+
* Worker-thread sandbox that executes plugin hooks. Optional so tests
|
|
730
|
+
* without any `main.js` on disk can omit it; the default spawns real
|
|
731
|
+
* workers when a loadable bundle is found.
|
|
732
|
+
*/
|
|
733
|
+
readonly sandbox?: PluginSandbox;
|
|
734
|
+
/**
|
|
735
|
+
* Optional timing sink for boot diagnostics — receives the duration of
|
|
736
|
+
* each `loadAll` step. Defaults to a no-op so tests stay quiet.
|
|
737
|
+
*/
|
|
738
|
+
readonly onTiming?: (step: string, ms: number) => void;
|
|
739
|
+
};
|
|
740
|
+
/**
|
|
741
|
+
* Create the plugin loader. Workers are respawned on every load so a
|
|
742
|
+
* rescan picks up changed plugin code — the new worker re-imports
|
|
743
|
+
* `main.js`, bypassing the main thread's ESM module cache. Configured
|
|
744
|
+
* {@link PluginLoaderDeps.seedPluginDirs} are copied into `pluginsDir`
|
|
745
|
+
* on each load.
|
|
746
|
+
*/
|
|
747
|
+
declare function createPluginLoader(deps: PluginLoaderDeps): PluginLoader;
|
|
748
|
+
/**
|
|
749
|
+
* Build the immutable-ish registry from entries, keyed by plugin UUID.
|
|
750
|
+
* Duplicate ids keep the first entry and warn (the UUID contract is
|
|
751
|
+
* unique — duplicates signal a broken install). Duplicates are dropped
|
|
752
|
+
* from every accessor, not just the id map: keeping a duplicate in
|
|
753
|
+
* `getAll()` would make enabled-list iteration invoke a plugin whose
|
|
754
|
+
* worker activation already replaced (and disposed) the first load.
|
|
755
|
+
* `updateEntry` patches a single entry in place and re-sorts by priority.
|
|
756
|
+
*/
|
|
757
|
+
declare function buildRegistry(entries: readonly PluginRegistryEntry[]): PluginRegistry;
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Wrap a {@link ResourceContainer} so every read operation understands
|
|
761
|
+
* virtual paths (`outer!inner`, see `./archive`). The plugin API,
|
|
762
|
+
* the server artifact view and the CLI directory container all funnel
|
|
763
|
+
* entry reads through this wrapper, so nested-container support lands
|
|
764
|
+
* everywhere (hooks, cover pipeline, thumbnails, the HTTP file route)
|
|
765
|
+
* without each consumer knowing about containers.
|
|
766
|
+
*
|
|
767
|
+
* A virtual path resolves in two steps:
|
|
768
|
+
* 1. nested zip addressing — the resolver reads the outer entry's
|
|
769
|
+
* central directory and streams the *decompressed* inner bytes;
|
|
770
|
+
* 2. materialized cache addressing — when `extractCacheDir` is wired
|
|
771
|
+
* and the outer archive was extracted by the plugin API (its
|
|
772
|
+
* manifest exists in `<extractCacheDir>/<outer>/`), the inner file
|
|
773
|
+
* is served straight from disk. This is how non-zip containers
|
|
774
|
+
* (rar/7z/tar — not expressible as random-access sources) get
|
|
775
|
+
* `outer!inner` addressing after extraction, and it is strictly
|
|
776
|
+
* better than streaming: entries are seekable.
|
|
777
|
+
*
|
|
778
|
+
* Literal entries pass through untouched; virtual entries are only
|
|
779
|
+
* addressable by name, never enumerated.
|
|
780
|
+
*
|
|
781
|
+
* `scope` prefixes nested-cache keys when the cache is shared across
|
|
782
|
+
* containers (see {@link createNestedResolver}); `resolveSeekablePath`
|
|
783
|
+
* forwards the base container's capability for literal entries and the
|
|
784
|
+
* extracted file path for materialized ones.
|
|
785
|
+
*/
|
|
786
|
+
declare function createNestedAwareContainer(base: ResourceContainer, nestedCdCache?: NestedCdCache, scope?: string, extractCacheDir?: string): ResourceContainer;
|
|
787
|
+
|
|
788
|
+
export { type CapabilityGuard, type CreatePluginResourceAPIDeps, DEFAULT_PLUGIN_EXTRACT_MAX_BYTES, DEFAULT_PLUGIN_EXTRACT_MAX_ENTRIES, DEFAULT_SANDBOX_CONFIG, DomainError, type DomainErrorCode, type FoundPlugin, MIN_PERCEPTUAL_STDDEV, type MissingPlugin, type NestedCdCache, PERCEPTUAL_HASH_KINDS, PHASH_GRID, PLUGIN_HOOK_HARD_TIMEOUT_MS, PLUGIN_MAX_API_CALLS_PER_HOOK, PLUGIN_MAX_LOGS_PER_HOOK, PLUGIN_MAX_RESULT_BYTES, PLUGIN_WATCHDOG_TIMEOUT_MS, PLUGIN_WORKER_MAX_OLD_SPACE_MB, PLUGIN_WORKER_MAX_RESPAWNS, PLUGIN_WORKER_RESPAWN_WINDOW_MS, type PluginActivation, type PluginActivationDeps, type PluginAssetHandler, type PluginCapability, type PluginDiscovery, type PluginDiscoveryDeps, type PluginHookName, type PluginHooks, type PluginHooksDeps, type PluginLoader, type PluginLoaderDeps, type PluginMetaHookResults, PluginProbeCache, type PluginRegistry, type PluginRegistryEntry, type PluginSandbox, type PluginSandboxConfig, type PluginSettingsRow, type PluginSettingsStore, ResourceContainer, buildRegistry, computeDHash, computePHash, conflict, createCapabilityGuard, createDirectoryContainer, createDirectoryResourceAPI, createDirectoryResourceAPI as createImportResourceAPI, createNestedAwareContainer, createNestedCdCache, createPluginActivation, createPluginDiscovery, createPluginHooks, createPluginLoader, createPluginResourceAPI, createPluginSandbox, grayStddev, invalid, listZipEntries, materializeFile, notFound, parseManifest, resolveSafeImportPath, runPluginHook, seedPlugins };
|