@hoardodile/host 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -5,6 +5,7 @@ import { I as ImageMetadataInput, b as ImageSourceProbe, A as AvProbeOptions, E
5
5
  import { R as ResourceContainer } from './container-fKxefO7p.js';
6
6
  import { P as PluginProbeCache } from './probe-cache-BkRI7HEK.js';
7
7
  export { a as PLUGIN_PROBE_CACHE_MAX_ENTRIES, c as createProbeCache } from './probe-cache-BkRI7HEK.js';
8
+ export { p as packZipDirectory } from './pack-6IscrwYN.js';
8
9
  import { PluginCapabilityKey } from '@hoardodile/sdk-types/plugin-capabilities';
9
10
  import 'sharp';
10
11
 
@@ -684,6 +685,18 @@ type PluginHooks = {
684
685
  * shape-validated (hex values, per-resource cap).
685
686
  */
686
687
  readonly runImageHashes: (api: ResourceAPI, pluginId: PluginManifestId) => Promise<ImageHashesResult | undefined>;
688
+ /**
689
+ * Run the plugin's optional `onInstall` hook — best-effort, invoked
690
+ * by the host after a successful install/update commit (marketplace
691
+ * install/update or a zip upload; never seed/dev plugins). The hook
692
+ * receives an install-scoped {@link ResourceAPI}: no resource is
693
+ * attached (the file surface answers empty, `context.detect` is
694
+ * `undefined`), while the asset methods still work and stay gated by
695
+ * the shared consent dialog. A throwing (or consent-denied) hook is
696
+ * logged via `hookFailureText` and swallowed — the install itself is
697
+ * never failed; plugins must re-check at runtime.
698
+ */
699
+ readonly runInstallHook: (pluginId: PluginManifestId) => Promise<void>;
687
700
  };
688
701
  declare function createPluginHooks(deps: PluginHooksDeps): PluginHooks;
689
702
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createWriteStream, createReadStream, existsSync, readFileSync, renameSync, rmSync, mkdirSync, readdirSync, cpSync, statSync, realpathSync } from 'fs';
2
- import { dirname, isAbsolute, normalize, resolve, sep, join } from 'path';
2
+ import { dirname, resolve, sep, isAbsolute, normalize, join } from 'path';
3
3
  import { pluginAssetError, fileTypeFromName, createFailingPlugin, definePlugin } from '@hoardodile/sdk-types';
4
4
  export { HOOK_NAMES, assertPluginShape, createFailingPlugin, createResourceAPIFixture, definePlugin, isDetected, isMissed, stubLogger } from '@hoardodile/sdk-types';
5
5
  import { mimeToKind, MIME_FFMPEG_INPUT_FORMAT } from '@hoardodile/sdk-types/media-exts';
@@ -14,6 +14,7 @@ import { fork, spawn } from 'child_process';
14
14
  import { createRequire } from 'module';
15
15
  import { buffer } from 'stream/consumers';
16
16
  import { createInflateRaw } from 'zlib';
17
+ import yazl from 'yazl';
17
18
  import { CAPABILITY_BY_METHOD } from '@hoardodile/sdk-types/plugin-capabilities';
18
19
  import { pathToFileURL, fileURLToPath } from 'url';
19
20
  import { pluginManifest } from '@hoardodile/sdk-types/schema';
@@ -661,16 +662,16 @@ function parseSltListing(stdout, legacyNames) {
661
662
  }
662
663
  continue;
663
664
  }
664
- const sep4 = line.indexOf(" = ");
665
- if (sep4 < 0) continue;
666
- const key = line.slice(0, sep4);
665
+ const sep5 = line.indexOf(" = ");
666
+ if (sep5 < 0) continue;
667
+ const key = line.slice(0, sep5);
667
668
  if (key === "Path") {
668
- const raw = Buffer.from(line.slice(sep4 + 3), "latin1");
669
+ const raw = Buffer.from(line.slice(sep5 + 3), "latin1");
669
670
  const decoded = legacyNames ? decodeLegacyZipName(raw) : raw.toString("utf8");
670
671
  name = decoded.replace(/\\/g, "/");
671
672
  continue;
672
673
  }
673
- const value = line.slice(sep4 + 3);
674
+ const value = line.slice(sep5 + 3);
674
675
  switch (key) {
675
676
  case "Size":
676
677
  sizeBytes = Number.parseInt(value, 10) || 0;
@@ -916,11 +917,11 @@ var globalMaterializeInflight = /* @__PURE__ */ new Map();
916
917
  // src/archive/nested-entry.ts
917
918
  var VIRTUAL_PATH_SEPARATOR = "!";
918
919
  function splitVirtualPath(path) {
919
- const sep4 = path.indexOf(VIRTUAL_PATH_SEPARATOR);
920
- if (sep4 <= 0) return void 0;
921
- const inner = path.slice(sep4 + 1);
920
+ const sep5 = path.indexOf(VIRTUAL_PATH_SEPARATOR);
921
+ if (sep5 <= 0) return void 0;
922
+ const inner = path.slice(sep5 + 1);
922
923
  if (inner.length === 0) return void 0;
923
- return { outer: path.slice(0, sep4), inner };
924
+ return { outer: path.slice(0, sep5), inner };
924
925
  }
925
926
  function createOuterArchiveSource(outer, outerName, size) {
926
927
  return {
@@ -972,7 +973,7 @@ function createNestedResolver(outer, opts = {}) {
972
973
  zip: r
973
974
  }));
974
975
  }
975
- async function resolve5(path) {
976
+ async function resolve6(path) {
976
977
  const parts = splitVirtualPath(path);
977
978
  if (parts === void 0) return { kind: "literal" };
978
979
  const { outer: outerName, inner } = parts;
@@ -999,7 +1000,7 @@ function createNestedResolver(outer, opts = {}) {
999
1000
  );
1000
1001
  return work;
1001
1002
  }
1002
- return { resolve: resolve5, list };
1003
+ return { resolve: resolve6, list };
1003
1004
  }
1004
1005
  function listedWithStream(outer, outerName, entry) {
1005
1006
  return {
@@ -1321,13 +1322,40 @@ function createNestedCdCache(opts) {
1321
1322
  }
1322
1323
  return { get, set, clear };
1323
1324
  }
1325
+ async function packZipDirectory(srcDir, zipPath) {
1326
+ const root = resolve(srcDir);
1327
+ const files = listFilesSorted(root);
1328
+ const zip = new yazl.ZipFile();
1329
+ for (const abs of files) {
1330
+ const rel = abs.slice(root.length + 1).split(sep).join("/");
1331
+ zip.addFile(abs, rel, { compress: true });
1332
+ }
1333
+ zip.end();
1334
+ await pipeline(zip.outputStream, createWriteStream(resolve(zipPath)));
1335
+ }
1336
+ function listFilesSorted(root) {
1337
+ const out = [];
1338
+ const walk = (dir) => {
1339
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1340
+ const abs = join(dir, entry.name);
1341
+ if (entry.isDirectory()) {
1342
+ walk(abs);
1343
+ continue;
1344
+ }
1345
+ if (!entry.isFile()) continue;
1346
+ out.push(abs);
1347
+ }
1348
+ };
1349
+ walk(root);
1350
+ return out.sort();
1351
+ }
1324
1352
  var PHASH_GRID = 32;
1325
1353
  var MIN_PERCEPTUAL_STDDEV = 8;
1326
1354
  function hashStream(stream, algo) {
1327
- return new Promise((resolve5, reject) => {
1355
+ return new Promise((resolve6, reject) => {
1328
1356
  const hash = createHash(algo);
1329
1357
  stream.on("data", (chunk) => hash.update(chunk));
1330
- stream.on("end", () => resolve5(hash.digest("hex")));
1358
+ stream.on("end", () => resolve6(hash.digest("hex")));
1331
1359
  stream.on("error", reject);
1332
1360
  });
1333
1361
  }
@@ -1456,14 +1484,14 @@ function createNestedAwareContainer(base, nestedCdCache, scope, extractCacheDir)
1456
1484
  sizeBytes: entry.sizeBytes
1457
1485
  };
1458
1486
  }
1459
- async function resolve5(rel) {
1487
+ async function resolve6(rel) {
1460
1488
  const nested = await resolver.resolve(rel);
1461
1489
  if (nested.kind === "container") return nested;
1462
1490
  const materialized = await resolveMaterialized(rel);
1463
1491
  return materialized ?? nested;
1464
1492
  }
1465
1493
  async function readEntrySlice(rel, start, end) {
1466
- const resolved = await resolve5(rel);
1494
+ const resolved = await resolve6(rel);
1467
1495
  if (resolved.kind === "literal") {
1468
1496
  return base.readEntrySlice(rel, start, end);
1469
1497
  }
@@ -1473,7 +1501,7 @@ function createNestedAwareContainer(base, nestedCdCache, scope, extractCacheDir)
1473
1501
  return readVirtualSlice(() => resolved.openStream(), start, end);
1474
1502
  }
1475
1503
  async function openEntryStream(rel) {
1476
- const resolved = await resolve5(rel);
1504
+ const resolved = await resolve6(rel);
1477
1505
  if (resolved.kind === "literal") {
1478
1506
  return base.openEntryStream(rel);
1479
1507
  }
@@ -1487,7 +1515,7 @@ function createNestedAwareContainer(base, nestedCdCache, scope, extractCacheDir)
1487
1515
  return { stream: resolved.openStream(), size: resolved.entry.sizeBytes };
1488
1516
  }
1489
1517
  async function readEntry(rel) {
1490
- const resolved = await resolve5(rel);
1518
+ const resolved = await resolve6(rel);
1491
1519
  if (resolved.kind === "literal") {
1492
1520
  return base.readEntry(rel);
1493
1521
  }
@@ -1497,7 +1525,7 @@ function createNestedAwareContainer(base, nestedCdCache, scope, extractCacheDir)
1497
1525
  return buffer(resolved.openStream());
1498
1526
  }
1499
1527
  async function resolveByteRange(rel) {
1500
- const resolved = await resolve5(rel);
1528
+ const resolved = await resolve6(rel);
1501
1529
  if (resolved.kind === "literal") {
1502
1530
  return base.resolveByteRange(rel);
1503
1531
  }
@@ -1507,7 +1535,7 @@ function createNestedAwareContainer(base, nestedCdCache, scope, extractCacheDir)
1507
1535
  return { size: resolved.entry.sizeBytes };
1508
1536
  }
1509
1537
  async function resolveSeekablePath(rel) {
1510
- const resolved = await resolve5(rel);
1538
+ const resolved = await resolve6(rel);
1511
1539
  if (resolved.kind === "literal") {
1512
1540
  return base.resolveSeekablePath?.(rel);
1513
1541
  }
@@ -2270,6 +2298,59 @@ function parseManifest(dirPath, dirName) {
2270
2298
  }
2271
2299
  return result.data;
2272
2300
  }
2301
+ function createInstallScopeApi() {
2302
+ const noResource = (method) => new Error(
2303
+ `${method}() \u2014 no resource is attached to the onInstall hook; use the asset methods (download/statAsset/readAsset/deleteAsset) for install-time work`
2304
+ );
2305
+ return {
2306
+ logInfo() {
2307
+ },
2308
+ logWarn() {
2309
+ },
2310
+ logError() {
2311
+ },
2312
+ context: { detect: void 0 },
2313
+ listFileNames: async () => [],
2314
+ readFile: async () => {
2315
+ throw noResource("readFile");
2316
+ },
2317
+ statFile: async () => void 0,
2318
+ statFiles: async (paths) => paths.map(() => void 0),
2319
+ sniff: async () => void 0,
2320
+ probe: async () => ({ kind: "unknown", reason: "unavailable" }),
2321
+ hashBytes: async () => {
2322
+ throw noResource("hashBytes");
2323
+ },
2324
+ computeImageHashes: async () => void 0,
2325
+ listContainer: async () => {
2326
+ throw noResource("listContainer");
2327
+ },
2328
+ extractArchive: async () => {
2329
+ throw noResource("extractArchive");
2330
+ },
2331
+ download: async () => {
2332
+ throw pluginAssetError(
2333
+ "UNAVAILABLE",
2334
+ "download() \u2014 this host has no plugin asset service; only the app server host can download into the plugin vault"
2335
+ );
2336
+ },
2337
+ statAsset: async () => {
2338
+ throw unavailableAsset2("statAsset");
2339
+ },
2340
+ readAsset: async () => {
2341
+ throw unavailableAsset2("readAsset");
2342
+ },
2343
+ deleteAsset: async () => {
2344
+ throw unavailableAsset2("deleteAsset");
2345
+ }
2346
+ };
2347
+ }
2348
+ function unavailableAsset2(method) {
2349
+ return pluginAssetError(
2350
+ "UNAVAILABLE",
2351
+ `${method}() \u2014 this host has no plugin asset vault; only the app server host manages vault files`
2352
+ );
2353
+ }
2273
2354
 
2274
2355
  // src/hooks.ts
2275
2356
  var MAX_IMAGE_HASHES_PER_RESOURCE = 2e3;
@@ -2444,6 +2525,17 @@ function createPluginHooks(deps) {
2444
2525
  pluginId
2445
2526
  );
2446
2527
  }
2528
+ async function runInstallHook(pluginId) {
2529
+ const entry = getRegistry().getById(pluginId);
2530
+ if (entry === void 0) return;
2531
+ await invokeHook(
2532
+ entry,
2533
+ entry.plugin.onInstall,
2534
+ createInstallScopeApi(),
2535
+ "onInstall",
2536
+ "warn"
2537
+ );
2538
+ }
2447
2539
  return {
2448
2540
  defaultPluginId,
2449
2541
  getEffectiveEntry,
@@ -2455,7 +2547,8 @@ function createPluginHooks(deps) {
2455
2547
  resolveLocalCoverSource,
2456
2548
  runMetaHooks,
2457
2549
  supportsImageHashes,
2458
- runImageHashes
2550
+ runImageHashes,
2551
+ runInstallHook
2459
2552
  };
2460
2553
  }
2461
2554
  var HEX_VALUE = /^[0-9a-f]+$/;
@@ -2664,7 +2757,7 @@ function createPluginSandbox(config = DEFAULT_SANDBOX_CONFIG) {
2664
2757
  await teardownChild(previous);
2665
2758
  }
2666
2759
  if (!opts.eager) {
2667
- void teardownChild(state);
2760
+ await teardownChild(state);
2668
2761
  }
2669
2762
  return createSandboxedPlugin(
2670
2763
  state.hooks ?? ["detect"],
@@ -2848,6 +2941,16 @@ function createPluginSandbox(config = DEFAULT_SANDBOX_CONFIG) {
2848
2941
  if (child === void 0) {
2849
2942
  throw new Error(`plugin ${state.id} sandbox unavailable`);
2850
2943
  }
2944
+ try {
2945
+ return await invokeOnChild(state, child, hook, api);
2946
+ } finally {
2947
+ if (hook === "onInstall") {
2948
+ state.respawnTimes = [];
2949
+ await teardownChild(state);
2950
+ }
2951
+ }
2952
+ }
2953
+ function invokeOnChild(state, child, hook, api) {
2851
2954
  const callId = nextCallId++;
2852
2955
  return new Promise((resolveCall, reject) => {
2853
2956
  const call = {
@@ -3305,6 +3408,6 @@ function createProbeCache(maxEntries = PLUGIN_PROBE_CACHE_MAX_ENTRIES) {
3305
3408
  };
3306
3409
  }
3307
3410
 
3308
- export { DEFAULT_PLUGIN_EXTRACT_MAX_BYTES, DEFAULT_PLUGIN_EXTRACT_MAX_ENTRIES, DEFAULT_SANDBOX_CONFIG, DomainError, MIN_PERCEPTUAL_STDDEV, 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_PROBE_CACHE_MAX_ENTRIES, PLUGIN_WATCHDOG_TIMEOUT_MS, PLUGIN_WORKER_MAX_OLD_SPACE_MB, PLUGIN_WORKER_MAX_RESPAWNS, PLUGIN_WORKER_RESPAWN_WINDOW_MS, buildRegistry, computeDHash, computePHash, conflict, createCapabilityGuard, createDirectoryContainer, createDirectoryResourceAPI, createDirectoryResourceAPI as createImportResourceAPI, createNestedAwareContainer, createNestedCdCache, createPluginActivation, createPluginDiscovery, createPluginHooks, createPluginLoader, createPluginResourceAPI, createPluginSandbox, createProbeCache, grayStddev, invalid, listZipEntries, materializeFile, notFound, parseManifest, resolveSafeImportPath, runPluginHook, seedPlugins };
3411
+ export { DEFAULT_PLUGIN_EXTRACT_MAX_BYTES, DEFAULT_PLUGIN_EXTRACT_MAX_ENTRIES, DEFAULT_SANDBOX_CONFIG, DomainError, MIN_PERCEPTUAL_STDDEV, 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_PROBE_CACHE_MAX_ENTRIES, PLUGIN_WATCHDOG_TIMEOUT_MS, PLUGIN_WORKER_MAX_OLD_SPACE_MB, PLUGIN_WORKER_MAX_RESPAWNS, PLUGIN_WORKER_RESPAWN_WINDOW_MS, buildRegistry, computeDHash, computePHash, conflict, createCapabilityGuard, createDirectoryContainer, createDirectoryResourceAPI, createDirectoryResourceAPI as createImportResourceAPI, createNestedAwareContainer, createNestedCdCache, createPluginActivation, createPluginDiscovery, createPluginHooks, createPluginLoader, createPluginResourceAPI, createPluginSandbox, createProbeCache, grayStddev, invalid, listZipEntries, materializeFile, notFound, packZipDirectory, parseManifest, resolveSafeImportPath, runPluginHook, seedPlugins };
3309
3412
  //# sourceMappingURL=index.js.map
3310
3413
  //# sourceMappingURL=index.js.map