@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.
Files changed (144) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +73 -0
  3. package/dist/chunks/worker-entry.mjs +422 -0
  4. package/dist/container-fKxefO7p.d.ts +58 -0
  5. package/dist/contract/index.d.ts +49 -0
  6. package/dist/contract/index.js +187 -0
  7. package/dist/contract/index.js.map +1 -0
  8. package/dist/hoard/index.d.ts +754 -0
  9. package/dist/hoard/index.js +1939 -0
  10. package/dist/hoard/index.js.map +1 -0
  11. package/dist/image-Dx2Be3Bj.d.ts +168 -0
  12. package/dist/index.d.ts +788 -0
  13. package/dist/index.js +3311 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/media/index.d.ts +96 -0
  16. package/dist/media/index.js +152 -0
  17. package/dist/media/index.js.map +1 -0
  18. package/dist/probe/index.d.ts +113 -0
  19. package/dist/probe/index.js +454 -0
  20. package/dist/probe/index.js.map +1 -0
  21. package/dist/probe-cache-BkRI7HEK.d.ts +25 -0
  22. package/dist/render/index.d.ts +125 -0
  23. package/dist/render/index.js +430 -0
  24. package/dist/render/index.js.map +1 -0
  25. package/package.json +103 -0
  26. package/src/__testutils__/zip-fixtures.ts +139 -0
  27. package/src/activation.ts +116 -0
  28. package/src/api-types.ts +60 -0
  29. package/src/api.test.ts +564 -0
  30. package/src/api.ts +501 -0
  31. package/src/archive/7z.test.ts +222 -0
  32. package/src/archive/7z.ts +261 -0
  33. package/src/archive/archive-ops.test.ts +654 -0
  34. package/src/archive/extract-archive.test.ts +237 -0
  35. package/src/archive/extract-archive.ts +534 -0
  36. package/src/archive/extract.ts +693 -0
  37. package/src/archive/format.ts +49 -0
  38. package/src/archive/index.ts +116 -0
  39. package/src/archive/listing.ts +121 -0
  40. package/src/archive/materialize.ts +88 -0
  41. package/src/archive/name-decode.test.ts +34 -0
  42. package/src/archive/name-decode.ts +287 -0
  43. package/src/archive/nested-cd-cache.ts +55 -0
  44. package/src/archive/nested-entry.test.ts +194 -0
  45. package/src/archive/nested-entry.ts +254 -0
  46. package/src/archive/pack.ts +40 -0
  47. package/src/archive/zip-entries.test.ts +56 -0
  48. package/src/archive/zip-entries.ts +336 -0
  49. package/src/capability-guard.ts +73 -0
  50. package/src/container.ts +62 -0
  51. package/src/contract/container-fixture.ts +74 -0
  52. package/src/contract/index.ts +10 -0
  53. package/src/contract/suite.test.ts +101 -0
  54. package/src/contract/suite.ts +205 -0
  55. package/src/define-plugin.test.ts +53 -0
  56. package/src/define-plugin.ts +12 -0
  57. package/src/dev-runner.test.ts +98 -0
  58. package/src/dev-runner.ts +73 -0
  59. package/src/directory-api.test.ts +229 -0
  60. package/src/directory-api.ts +42 -0
  61. package/src/directory-container.ts +141 -0
  62. package/src/discovery.ts +295 -0
  63. package/src/errors.ts +74 -0
  64. package/src/fixtures.ts +8 -0
  65. package/src/hash.test.ts +301 -0
  66. package/src/hash.ts +167 -0
  67. package/src/hoard/dir-size.ts +58 -0
  68. package/src/hoard/index.ts +84 -0
  69. package/src/hoard/migrate-script.test.ts +539 -0
  70. package/src/hoard/order-manifest.test.ts +115 -0
  71. package/src/hoard/order-manifest.ts +131 -0
  72. package/src/hoard/paths.test.ts +226 -0
  73. package/src/hoard/paths.ts +569 -0
  74. package/src/hoard/plugin-vault.test.ts +160 -0
  75. package/src/hoard/plugin-vault.ts +233 -0
  76. package/src/hoard/sanitize.test.ts +149 -0
  77. package/src/hoard/sanitize.ts +230 -0
  78. package/src/hoard/staging-dir.ts +133 -0
  79. package/src/hoard/version.test.ts +285 -0
  80. package/src/hoard/version.ts +216 -0
  81. package/src/hoard/versioned-folder-ops.ts +152 -0
  82. package/src/hoard/write-versioned.test.ts +67 -0
  83. package/src/hoard/write-versioned.ts +39 -0
  84. package/src/hooks.test.ts +528 -0
  85. package/src/hooks.ts +428 -0
  86. package/src/index.ts +135 -0
  87. package/src/loader.test.ts +442 -0
  88. package/src/loader.ts +213 -0
  89. package/src/media/index.ts +16 -0
  90. package/src/media/render-cache.ts +27 -0
  91. package/src/media/seekable.ts +35 -0
  92. package/src/media/thumb-input.test.ts +109 -0
  93. package/src/media/thumb-input.ts +194 -0
  94. package/src/nested-view.test.ts +223 -0
  95. package/src/nested-view.ts +239 -0
  96. package/src/probe/audio.test.ts +135 -0
  97. package/src/probe/audio.ts +140 -0
  98. package/src/probe/av.ts +84 -0
  99. package/src/probe/ffprobe.ts +197 -0
  100. package/src/probe/image.test.ts +251 -0
  101. package/src/probe/image.ts +336 -0
  102. package/src/probe/index.ts +54 -0
  103. package/src/probe/sniff.test.ts +72 -0
  104. package/src/probe/sniff.ts +67 -0
  105. package/src/probe/video.test.ts +41 -0
  106. package/src/probe/video.ts +116 -0
  107. package/src/probe-cache.test.ts +89 -0
  108. package/src/probe-cache.ts +65 -0
  109. package/src/render/ffmpeg.ts +78 -0
  110. package/src/render/index.ts +34 -0
  111. package/src/render/pipeline.test.ts +187 -0
  112. package/src/render/pipeline.ts +479 -0
  113. package/src/sandbox/call-timers.ts +68 -0
  114. package/src/sandbox/fixtures/api-error-plugin.mjs +11 -0
  115. package/src/sandbox/fixtures/asset-plugin.mjs +13 -0
  116. package/src/sandbox/fixtures/chatty-plugin.mjs +14 -0
  117. package/src/sandbox/fixtures/container-plugin.mjs +8 -0
  118. package/src/sandbox/fixtures/context-plugin.mjs +19 -0
  119. package/src/sandbox/fixtures/crash-plugin.mjs +6 -0
  120. package/src/sandbox/fixtures/echo-plugin.mjs +17 -0
  121. package/src/sandbox/fixtures/exit-plugin.mjs +6 -0
  122. package/src/sandbox/fixtures/fetch-plugin.mjs +8 -0
  123. package/src/sandbox/fixtures/flood-api-plugin.mjs +8 -0
  124. package/src/sandbox/fixtures/flood-log-plugin.mjs +8 -0
  125. package/src/sandbox/fixtures/hostile-dynamic-plugin.mjs +9 -0
  126. package/src/sandbox/fixtures/hostile-plugin.mjs +13 -0
  127. package/src/sandbox/fixtures/huge-result-plugin.mjs +8 -0
  128. package/src/sandbox/fixtures/logging-plugin.mjs +9 -0
  129. package/src/sandbox/fixtures/range-plugin.mjs +9 -0
  130. package/src/sandbox/fixtures/slow-api-plugin.mjs +11 -0
  131. package/src/sandbox/fixtures/spin-plugin.mjs +9 -0
  132. package/src/sandbox/fixtures/stuck-plugin.mjs +12 -0
  133. package/src/sandbox/fixtures/thrower-plugin.mjs +7 -0
  134. package/src/sandbox/host.test.ts +619 -0
  135. package/src/sandbox/host.ts +933 -0
  136. package/src/sandbox/host.unit.test.ts +330 -0
  137. package/src/sandbox/protocol.test.ts +85 -0
  138. package/src/sandbox/protocol.ts +147 -0
  139. package/src/sandbox/sandboxed-plugin.ts +30 -0
  140. package/src/sandbox/worker-entry.mjs +422 -0
  141. package/src/seed.test.ts +102 -0
  142. package/src/seed.ts +160 -0
  143. package/src/settings-store.ts +26 -0
  144. package/src/types.ts +21 -0
@@ -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 };