@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
package/src/hooks.ts ADDED
@@ -0,0 +1,428 @@
1
+ import type {
2
+ ImageHash,
3
+ ImageHashesResult,
4
+ PluginManifestId,
5
+ SerializedFileList,
6
+ } from "@hoardodile/sdk-types"
7
+ import type { PluginRegistry, PluginRegistryEntry } from "./api-types.ts"
8
+ import { createCapabilityGuard } from "./capability-guard.ts"
9
+ import type { Detection, ResourceAPI } from "./types.ts"
10
+
11
+ /** Absolute cap of hash rows one resource may contribute (host policy). */
12
+ export const MAX_IMAGE_HASHES_PER_RESOURCE = 2000
13
+
14
+ /**
15
+ * Run `detect` over the given registry entries in order; returns the id of
16
+ * the first matching plugin, or `undefined` when none match. A throwing
17
+ * detector is logged (tagged with `action`) and skipped — one crash must
18
+ * never abort the scan.
19
+ */
20
+ async function runDetectors(
21
+ entries: readonly PluginRegistryEntry[],
22
+ api: ResourceAPI,
23
+ action: string,
24
+ ): Promise<PluginManifestId | undefined> {
25
+ for (const entry of entries) {
26
+ const result = await invokeHook(entry, entry.plugin.detect, api, action)
27
+ if (result?.ok) return entry.id
28
+ }
29
+ return undefined
30
+ }
31
+
32
+ /**
33
+ * Invoke one hook of a registry entry with the uniform policy: an
34
+ * unimplemented hook answers `undefined` without running, a throw is
35
+ * logged via {@link hookFailureText} and also answers `undefined`. This
36
+ * is the "throw means absence" policy — callers that must tell a hook
37
+ * failure apart from a hook that ran (meta-hook key presence) keep
38
+ * their own try/catch instead.
39
+ */
40
+ async function invokeHook<T>(
41
+ entry: PluginRegistryEntry,
42
+ hook: ((api: ResourceAPI) => T | Promise<T>) | undefined,
43
+ api: ResourceAPI,
44
+ action: string,
45
+ level: "warn" | "error" = "error",
46
+ ): Promise<T | undefined> {
47
+ if (hook === undefined) return undefined
48
+ try {
49
+ return await hook(api)
50
+ } catch (err) {
51
+ const log = level === "warn" ? console.warn : console.error
52
+ log(hookFailureText(action, entry.id, err))
53
+ return undefined
54
+ }
55
+ }
56
+
57
+ function errorText(err: unknown): string {
58
+ return err instanceof Error ? err.message : String(err)
59
+ }
60
+
61
+ function hookFailureText(
62
+ action: string,
63
+ pluginId: PluginManifestId,
64
+ err: unknown,
65
+ ): string {
66
+ return `[plugin-hooks] ${action} failed for plugin ${pluginId}: ${errorText(err)}`
67
+ }
68
+
69
+ export type PluginHooksDeps = {
70
+ /**
71
+ * Live accessor for the current registry — called on every hook
72
+ * invocation so a `rescan()` replacing the registry never leaves
73
+ * consumers holding a stale snapshot.
74
+ */
75
+ readonly getRegistry: () => PluginRegistry
76
+ }
77
+
78
+ /**
79
+ * Result of {@link PluginHooks.runMetaHooks}. A present key means the hook
80
+ * ran (permission granted + implemented); `value` is the raw hook result,
81
+ * which may still be `undefined` when the hook itself returned nothing.
82
+ */
83
+ export type PluginMetaHookResults = {
84
+ readonly sourceMeta?: { readonly value: unknown }
85
+ readonly searchMeta?: { readonly value: unknown }
86
+ }
87
+
88
+ /**
89
+ * The single entry point for executing plugin hooks. Owns every
90
+ * hook-invocation policy: priority iteration, builtin fallback, error
91
+ * swallowing/logging, capability checks, and result validation.
92
+ *
93
+ * Methods receive a ready-built {@link ResourceAPI} — this module knows
94
+ * nothing about resources, archives, or paths.
95
+ */
96
+ export type PluginHooks = {
97
+ /** The builtin fallback plugin id. Throws when no builtin is registered. */
98
+ readonly defaultPluginId: () => PluginManifestId
99
+ /**
100
+ * The registry entry that should serve a resource's read paths right
101
+ * now: the resource's stored plugin when it is healthy (registered,
102
+ * enabled, not missing), otherwise the builtin fallback plugin.
103
+ * Throws when no builtin is registered.
104
+ */
105
+ readonly getEffectiveEntry: (
106
+ resPluginId: PluginManifestId | null,
107
+ ) => PluginRegistryEntry
108
+ /** Run all enabled plugins' detectors in priority order. Returns the first matching plugin id. Throws if no match (builtin should always match). */
109
+ readonly detectFirstMatch: (api: ResourceAPI) => Promise<PluginManifestId>
110
+ /** Validate that the current plugin still matches. Returns confirmed plugin id or falls back to builtin id. */
111
+ readonly revalidate: (
112
+ api: ResourceAPI,
113
+ currentPluginId: PluginManifestId,
114
+ ) => Promise<PluginManifestId>
115
+ /** Run a specific plugin's detector. */
116
+ readonly detectForPlugin: (
117
+ api: ResourceAPI,
118
+ pluginId: PluginManifestId,
119
+ ) => Promise<Detection>
120
+ /**
121
+ * Detector pass for folder-import candidates: non-builtin detectors in
122
+ * priority order, falling back to the builtin plugin without invoking
123
+ * its detector.
124
+ */
125
+ readonly detectForImportDir: (api: ResourceAPI) => Promise<PluginManifestId>
126
+ /**
127
+ * Ask the owning plugin for a custom file list. Returns `undefined`
128
+ * when the plugin has no file list hook (or it failed).
129
+ */
130
+ readonly buildFileList: (
131
+ api: ResourceAPI,
132
+ pluginId: PluginManifestId,
133
+ ) => Promise<SerializedFileList | undefined>
134
+ /** Ask the owning plugin which file should be used for the local cover. */
135
+ readonly resolveLocalCoverSource: (
136
+ api: ResourceAPI,
137
+ pluginId: PluginManifestId,
138
+ ) => Promise<string | undefined>
139
+ /**
140
+ * Run the capability-gated meta hooks (`sourceMeta`, `searchMeta`) of
141
+ * the owning plugin. Keys are absent when the permission is not
142
+ * granted or the hook is not implemented.
143
+ */
144
+ readonly runMetaHooks: (
145
+ api: ResourceAPI,
146
+ pluginId: PluginManifestId,
147
+ ) => Promise<PluginMetaHookResults>
148
+ /**
149
+ * True when the plugin's manifest grants `imageHashes` and the plugin
150
+ * implements the hook. Used to decide whether a missing hash state is
151
+ * a rebuild gap or the plugin's legitimate choice.
152
+ */
153
+ readonly supportsImageHashes: (pluginId: PluginManifestId) => boolean
154
+ /**
155
+ * Run the capability-gated `imageHashes` hook of the owning plugin.
156
+ * Resolves to `undefined` when the permission is not granted, the
157
+ * hook is not implemented, or it threw. The returned entries are
158
+ * shape-validated (hex values, per-resource cap).
159
+ */
160
+ readonly runImageHashes: (
161
+ api: ResourceAPI,
162
+ pluginId: PluginManifestId,
163
+ ) => Promise<ImageHashesResult | undefined>
164
+ }
165
+
166
+ export function createPluginHooks(deps: PluginHooksDeps): PluginHooks {
167
+ const { getRegistry } = deps
168
+ const guard = createCapabilityGuard()
169
+
170
+ function defaultPluginId(): PluginManifestId {
171
+ const builtin = getRegistry().getBuiltin()
172
+ if (builtin === undefined) {
173
+ throw new Error(
174
+ "No builtin plugin in registry — cannot determine default plugin",
175
+ )
176
+ }
177
+ return builtin.id
178
+ }
179
+
180
+ function getEffectiveEntry(
181
+ resPluginId: PluginManifestId | null,
182
+ ): PluginRegistryEntry {
183
+ if (resPluginId !== null) {
184
+ const entry = getRegistry().getById(resPluginId)
185
+ if (entry?.enabled === true && !entry.missing) {
186
+ return entry
187
+ }
188
+ }
189
+ const builtin = getRegistry().getBuiltin()
190
+ if (builtin === undefined) {
191
+ throw new Error(
192
+ "No builtin plugin in registry — cannot determine effective plugin",
193
+ )
194
+ }
195
+ return builtin
196
+ }
197
+
198
+ async function detectFirstMatch(api: ResourceAPI): Promise<PluginManifestId> {
199
+ const matched = await runDetectors(
200
+ getRegistry().getEnabled(),
201
+ api,
202
+ "detect",
203
+ )
204
+ if (matched !== undefined) return matched
205
+ const builtin = getRegistry().getBuiltin()
206
+ throw new Error(
207
+ `No plugin matched resource. Builtin plugin ${builtin?.id ?? "unknown"} should have matched but did not.`,
208
+ )
209
+ }
210
+
211
+ async function revalidate(
212
+ api: ResourceAPI,
213
+ currentPluginId: PluginManifestId,
214
+ ): Promise<PluginManifestId> {
215
+ const registry = getRegistry()
216
+ const builtin = registry.getBuiltin()
217
+ if (builtin === undefined) {
218
+ throw new Error("No builtin plugin available for fallback")
219
+ }
220
+
221
+ const enabled = registry.getEnabled()
222
+ const startIndex = enabled.findIndex((e) => e.id === currentPluginId)
223
+ if (startIndex < 0) {
224
+ return builtin.id
225
+ }
226
+
227
+ const matched = await runDetectors(
228
+ enabled.slice(startIndex),
229
+ api,
230
+ "revalidate detect",
231
+ )
232
+ return matched ?? builtin.id
233
+ }
234
+
235
+ async function detectForPlugin(
236
+ api: ResourceAPI,
237
+ pluginId: PluginManifestId,
238
+ ): Promise<Detection> {
239
+ const entry = getRegistry().getById(pluginId)
240
+ if (entry === undefined) {
241
+ return { ok: false, reasons: [`unknown plugin: ${pluginId}`] }
242
+ }
243
+ const result = await invokeHook(
244
+ entry,
245
+ entry.plugin.detect,
246
+ api,
247
+ "detectForPlugin",
248
+ )
249
+ if (result === undefined) {
250
+ return { ok: false, reasons: ["detect threw an exception"] }
251
+ }
252
+ if (result.ok) return { ok: true }
253
+ return { ok: false, reasons: result.reasons.slice() }
254
+ }
255
+
256
+ async function detectForImportDir(
257
+ api: ResourceAPI,
258
+ ): Promise<PluginManifestId> {
259
+ const fallback = defaultPluginId()
260
+ const detectors = getRegistry()
261
+ .getEnabled()
262
+ .filter((e) => !e.builtin)
263
+ .sort((a, b) => a.priority - b.priority)
264
+ const matched = await runDetectors(detectors, api, "import detect")
265
+ return matched ?? fallback
266
+ }
267
+
268
+ async function buildFileList(
269
+ api: ResourceAPI,
270
+ pluginId: PluginManifestId,
271
+ ): Promise<SerializedFileList | undefined> {
272
+ const entry = getRegistry().getById(pluginId)
273
+ if (entry === undefined) return undefined
274
+ const pluginResult = await invokeHook(
275
+ entry,
276
+ entry.plugin.listFiles,
277
+ api,
278
+ "listFiles",
279
+ )
280
+ if (pluginResult === undefined) return undefined
281
+ for (const item of pluginResult) {
282
+ if (typeof item === "string") continue
283
+ if (typeof item === "object" && item !== null && !Array.isArray(item)) {
284
+ for (const value of Object.values(item)) {
285
+ if (value === undefined) continue
286
+ const t = typeof value
287
+ if (t === "string" || t === "number" || t === "boolean") continue
288
+ throw new Error(
289
+ `Plugin ${String(pluginId)} returned an invalid file list item value type: ${t}`,
290
+ )
291
+ }
292
+ continue
293
+ }
294
+ throw new Error(
295
+ `Plugin ${String(pluginId)} returned an invalid file list item`,
296
+ )
297
+ }
298
+ return pluginResult as SerializedFileList
299
+ }
300
+
301
+ async function resolveLocalCoverSource(
302
+ api: ResourceAPI,
303
+ pluginId: PluginManifestId,
304
+ ): Promise<string | undefined> {
305
+ const entry = getRegistry().getById(pluginId)
306
+ if (entry === undefined) return undefined
307
+ return invokeHook(entry, entry.plugin.coverLocal, api, "coverLocal", "warn")
308
+ }
309
+
310
+ async function runMetaHooks(
311
+ api: ResourceAPI,
312
+ pluginId: PluginManifestId,
313
+ ): Promise<PluginMetaHookResults> {
314
+ const entry = getRegistry().getById(pluginId)
315
+ if (entry === undefined) return {}
316
+ const { manifest, plugin } = entry
317
+ const results: {
318
+ sourceMeta?: { readonly value: unknown }
319
+ searchMeta?: { readonly value: unknown }
320
+ } = {}
321
+ /**
322
+ * Run one capability-gated meta hook. A present result key means
323
+ * the hook ran; a throw leaves the key absent (a failed hook must
324
+ * not wipe previously computed meta — the patch stays stale).
325
+ */
326
+ async function runMetaHook(
327
+ name: "sourceMeta" | "searchMeta",
328
+ ): Promise<void> {
329
+ const hook = plugin[name]
330
+ if (!guard.check(manifest, name) || hook === undefined) return
331
+ try {
332
+ results[name] = { value: await hook(api) }
333
+ } catch (err) {
334
+ console.error(hookFailureText(name, pluginId, err))
335
+ }
336
+ }
337
+ await runMetaHook("sourceMeta")
338
+ await runMetaHook("searchMeta")
339
+ return results
340
+ }
341
+
342
+ function entrySupportsImageHashes(entry: PluginRegistryEntry): boolean {
343
+ return (
344
+ guard.check(entry.manifest, "imageHashes") &&
345
+ entry.plugin.imageHashes !== undefined
346
+ )
347
+ }
348
+
349
+ function supportsImageHashes(pluginId: PluginManifestId): boolean {
350
+ const entry = getRegistry().getById(pluginId)
351
+ return entry !== undefined && entrySupportsImageHashes(entry)
352
+ }
353
+
354
+ async function runImageHashes(
355
+ api: ResourceAPI,
356
+ pluginId: PluginManifestId,
357
+ ): Promise<ImageHashesResult | undefined> {
358
+ const entry = getRegistry().getById(pluginId)
359
+ if (entry === undefined || !entrySupportsImageHashes(entry))
360
+ return undefined
361
+ return sanitizeImageHashes(
362
+ await invokeHook(entry, entry.plugin.imageHashes, api, "imageHashes"),
363
+ pluginId,
364
+ )
365
+ }
366
+
367
+ return {
368
+ defaultPluginId,
369
+ getEffectiveEntry,
370
+ detectFirstMatch,
371
+ revalidate,
372
+ detectForPlugin,
373
+ detectForImportDir,
374
+ buildFileList,
375
+ resolveLocalCoverSource,
376
+ runMetaHooks,
377
+ supportsImageHashes,
378
+ runImageHashes,
379
+ }
380
+ }
381
+
382
+ const HEX_VALUE = /^[0-9a-f]+$/
383
+
384
+ /**
385
+ * Validate the raw hook result: a `{ hashes }` shape, hex string values,
386
+ * and a bounded row count (excess entries are dropped with a warning —
387
+ * the hook must never be able to bloat the hashes table).
388
+ */
389
+ function sanitizeImageHashes(
390
+ value: unknown,
391
+ pluginId: string,
392
+ ): ImageHashesResult | undefined {
393
+ if (typeof value !== "object" || value === null) return undefined
394
+ const hashes = (value as { hashes?: unknown }).hashes
395
+ if (!Array.isArray(hashes)) return undefined
396
+ const valid: ImageHash[] = []
397
+ for (const item of hashes) {
398
+ if (typeof item !== "object" || item === null) continue
399
+ const { scope, type, value: hex, bits } = item as Record<string, unknown>
400
+ if (
401
+ typeof scope === "string" &&
402
+ scope.length > 0 &&
403
+ typeof type === "string" &&
404
+ type.length > 0 &&
405
+ typeof hex === "string" &&
406
+ HEX_VALUE.test(hex)
407
+ ) {
408
+ // `bits` always defaults to the hex length (4 bits per char) so
409
+ // consumers can classify perceptual vs exact hashes by bits.
410
+ valid.push({
411
+ scope,
412
+ type,
413
+ value: hex,
414
+ bits:
415
+ typeof bits === "number" && Number.isInteger(bits) && bits > 0
416
+ ? bits
417
+ : hex.length * 4,
418
+ })
419
+ }
420
+ }
421
+ if (valid.length > MAX_IMAGE_HASHES_PER_RESOURCE) {
422
+ console.warn(
423
+ `[plugin-hooks] imageHashes for plugin ${pluginId} produced ${valid.length} entries — capped at ${MAX_IMAGE_HASHES_PER_RESOURCE}`,
424
+ )
425
+ valid.length = MAX_IMAGE_HASHES_PER_RESOURCE
426
+ }
427
+ return { hashes: valid }
428
+ }
package/src/index.ts ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @hoardodile/host — the plugin runtime host: plugin loading,
3
+ * capability-sandboxed child processes (Node permission model + module
4
+ * policy), hook invocation strategy, ResourceAPI builders, probe caching,
5
+ * and capability gating. The server, the CLI, and future shells (desktop,
6
+ * MCP) all consume this same implementation so that "what you test is
7
+ * what runs in production".
8
+ *
9
+ * This root entry is the app-side runtime surface — the plugin
10
+ * authoring API lives in `@hoardodile/sdk-server` (which re-exports the
11
+ * authoring subset of the contract from `@hoardodile/sdk-types`).
12
+ * `runPluginHook` is the one dev-only helper plugin authors use from
13
+ * tests (as a devDependency — never shipped in a plugin bundle).
14
+ */
15
+
16
+ export type {
17
+ PluginActivation,
18
+ PluginActivationDeps,
19
+ } from "./activation.ts"
20
+ export { createPluginActivation } from "./activation.ts"
21
+ export {
22
+ type CreatePluginResourceAPIDeps,
23
+ createImportResourceAPI,
24
+ createPluginResourceAPI,
25
+ DEFAULT_PLUGIN_EXTRACT_MAX_BYTES,
26
+ DEFAULT_PLUGIN_EXTRACT_MAX_ENTRIES,
27
+ } from "./api.ts"
28
+ export type {
29
+ FoundPlugin,
30
+ MissingPlugin,
31
+ PluginDefinition,
32
+ PluginRegistry,
33
+ PluginRegistryEntry,
34
+ } from "./api-types.ts"
35
+ export type { NestedCdCache } from "./archive/index.ts"
36
+ export {
37
+ createNestedCdCache,
38
+ listZipEntries,
39
+ materializeFile,
40
+ } from "./archive/index.ts"
41
+ export type {
42
+ CapabilityGuard,
43
+ PluginCapability,
44
+ } from "./capability-guard.ts"
45
+ export { createCapabilityGuard } from "./capability-guard.ts"
46
+ export type { ResourceContainer } from "./container.ts"
47
+ export {
48
+ assertPluginShape,
49
+ createFailingPlugin,
50
+ definePlugin,
51
+ isDetected,
52
+ isMissed,
53
+ } from "./define-plugin.ts"
54
+ export { type PluginHookName, runPluginHook } from "./dev-runner.ts"
55
+ export {
56
+ createDirectoryResourceAPI,
57
+ resolveSafeImportPath,
58
+ } from "./directory-api.ts"
59
+ export { createDirectoryContainer } from "./directory-container.ts"
60
+ export type {
61
+ PluginDiscovery,
62
+ PluginDiscoveryDeps,
63
+ } from "./discovery.ts"
64
+ export { createPluginDiscovery, parseManifest } from "./discovery.ts"
65
+ export type { DomainErrorCode } from "./errors.ts"
66
+ export {
67
+ conflict,
68
+ DomainError,
69
+ invalid,
70
+ notFound,
71
+ } from "./errors.ts"
72
+ export type { ResourceAPIFixtureConfig } from "./fixtures.ts"
73
+ export { createResourceAPIFixture, stubLogger } from "./fixtures.ts"
74
+ export {
75
+ computeDHash,
76
+ computePHash,
77
+ grayStddev,
78
+ MIN_PERCEPTUAL_STDDEV,
79
+ PERCEPTUAL_HASH_KINDS,
80
+ PHASH_GRID,
81
+ } from "./hash.ts"
82
+ export type {
83
+ PluginHooks,
84
+ PluginHooksDeps,
85
+ PluginMetaHookResults,
86
+ } from "./hooks.ts"
87
+ export { createPluginHooks } from "./hooks.ts"
88
+ export type {
89
+ PluginLoader,
90
+ PluginLoaderDeps,
91
+ } from "./loader.ts"
92
+ export {
93
+ buildRegistry,
94
+ createPluginLoader,
95
+ seedPlugins,
96
+ } from "./loader.ts"
97
+ export { createNestedAwareContainer } from "./nested-view.ts"
98
+ export {
99
+ createProbeCache,
100
+ PLUGIN_PROBE_CACHE_MAX_ENTRIES,
101
+ type PluginProbeCache,
102
+ } from "./probe-cache.ts"
103
+ export type {
104
+ PluginAssetHandler,
105
+ PluginSandbox,
106
+ PluginSandboxConfig,
107
+ } from "./sandbox/host.ts"
108
+ export {
109
+ createPluginSandbox,
110
+ DEFAULT_SANDBOX_CONFIG,
111
+ PLUGIN_HOOK_HARD_TIMEOUT_MS,
112
+ PLUGIN_MAX_API_CALLS_PER_HOOK,
113
+ PLUGIN_MAX_LOGS_PER_HOOK,
114
+ PLUGIN_MAX_RESULT_BYTES,
115
+ PLUGIN_WATCHDOG_TIMEOUT_MS,
116
+ PLUGIN_WORKER_MAX_OLD_SPACE_MB,
117
+ PLUGIN_WORKER_MAX_RESPAWNS,
118
+ PLUGIN_WORKER_RESPAWN_WINDOW_MS,
119
+ } from "./sandbox/host.ts"
120
+ export { HOOK_NAMES, type HookName } from "./sandbox/protocol.ts"
121
+ export type {
122
+ PluginSettingsRow,
123
+ PluginSettingsStore,
124
+ } from "./settings-store.ts"
125
+ export type {
126
+ AudioCoverArt,
127
+ AudioInfo,
128
+ AudioTags,
129
+ Detection,
130
+ ImageInfo,
131
+ Logger,
132
+ ReadFileRange,
133
+ ResourceAPI,
134
+ VideoInfo,
135
+ } from "./types.ts"