@camstack/system 1.2.81 → 1.2.82

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 (51) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  13. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  14. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  15. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  16. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  17. package/dist/builtins/hub-forwarder/index.js +1 -1
  18. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  19. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  20. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  21. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  22. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  23. package/dist/builtins/local-network/local-network.addon.js +1 -1
  24. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  25. package/dist/builtins/loki-logging/index.js +1 -1
  26. package/dist/builtins/loki-logging/index.mjs +1 -1
  27. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  28. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  29. package/dist/builtins/platform-probe/index.js +1 -1
  30. package/dist/builtins/platform-probe/index.mjs +1 -1
  31. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  32. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  33. package/dist/builtins/snapshot/index.js +1 -1
  34. package/dist/builtins/snapshot/index.mjs +1 -1
  35. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  36. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  37. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  38. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  39. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  40. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  41. package/dist/builtins/system-config/system-config.addon.js +1 -1
  42. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  43. package/dist/builtins/winston-logging/index.js +1 -1
  44. package/dist/builtins/winston-logging/index.mjs +1 -1
  45. package/dist/{dist-CA2mim74.js → dist-DUDXaubL.js} +144 -6
  46. package/dist/{dist-DZOZtn-0.mjs → dist-DpL4mUA4.mjs} +144 -6
  47. package/dist/index.js +165 -36
  48. package/dist/index.mjs +165 -36
  49. package/dist/kernel/addon-installer.d.ts +54 -2
  50. package/dist/kernel/addon-loader.d.ts +33 -0
  51. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM$1, r as __exportAll, t as __commonJSMin$1 } from "./chunk-CNf5ZN-e.mjs";
2
- import { $ as objectInputDeclaresAddonId, At as emitDownForOwnedCaps, B as isArrayOutputSchema, Bt as scopeKey, Ct as ReadinessRegistry, Dt as asNumber, Et as asJsonObject$1, G as kebabToCamel, H as isObjectInput, Ht as EventCategory$1, It as parseJsonObject, K as lifecycleJobSchema, Lt as parseJsonUnknown$1, Mt as expandCapMethods, Ot as asString$1, R as extractNestedAddonId, Rt as readinessKey, V as isCollectionArrayMethod, W as isVoidInput, X as looseSchema, Y as logLevelAtMost, _t as DATAPLANE_SECRET_HEADER$1, d as METHOD_ACCESS_MAP, f as RUNTIME_DEFAULTS, ht as errMsg$1, it as scopesAllowDeviceCap, kt as createEvent, nt as procedureAuthKey, rt as scopesAllowAddon, t as ALL_CAPABILITY_DEFINITIONS, vt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, wt as ReadinessTimeoutError, y as addonSettingsCapability, yt as DEVICE_STATUS_METHOD, zt as resolveCapMount } from "./dist-DZOZtn-0.mjs";
2
+ import { $ as objectInputDeclaresAddonId, At as emitDownForOwnedCaps, B as isArrayOutputSchema, Bt as scopeKey, Ct as ReadinessRegistry, Dt as asNumber, Et as asJsonObject$1, G as kebabToCamel, H as isObjectInput, Ht as EventCategory$1, It as parseJsonObject, K as lifecycleJobSchema, Lt as parseJsonUnknown$1, Mt as expandCapMethods, Ot as asString$1, R as extractNestedAddonId, Rt as readinessKey, V as isCollectionArrayMethod, W as isVoidInput, X as looseSchema, Y as logLevelAtMost, _t as DATAPLANE_SECRET_HEADER$1, d as METHOD_ACCESS_MAP, f as RUNTIME_DEFAULTS, ht as errMsg$1, it as scopesAllowDeviceCap, kt as createEvent, nt as procedureAuthKey, rt as scopesAllowAddon, t as ALL_CAPABILITY_DEFINITIONS, vt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, wt as ReadinessTimeoutError, y as addonSettingsCapability, yt as DEVICE_STATUS_METHOD, zt as resolveCapMount } from "./dist-DpL4mUA4.mjs";
3
3
  import { AlertCenterAddon } from "./builtins/alerts/alerts.addon.mjs";
4
4
  import "./builtins/alerts/index.mjs";
5
5
  import { t as formatLogLine } from "./formatter-B7qW8bPJ.mjs";
@@ -3064,6 +3064,74 @@ function isRecord$1(value) {
3064
3064
  return typeof value === "object" && value !== null && !Array.isArray(value);
3065
3065
  }
3066
3066
  /**
3067
+ * How far up from the entry file to look for the owning `package.json`
3068
+ * (`<pkg>/dist/chunks/x.js` → 3 hops). Bounded so a loose module outside a
3069
+ * package can't walk to the filesystem root.
3070
+ */
3071
+ var PACKAGE_JSON_LOOKUP_DEPTH = 4;
3072
+ /** Version from the `package.json` that owns `entryPath`, or null. */
3073
+ function readOwningPackageVersion(entryPath) {
3074
+ let dir = path$39.dirname(entryPath);
3075
+ for (let hop = 0; hop < PACKAGE_JSON_LOOKUP_DEPTH; hop++) {
3076
+ try {
3077
+ const version = asJsonObject$1(parseJsonUnknown$1(fs$17.readFileSync(path$39.join(dir, "package.json"), "utf-8")))?.["version"];
3078
+ if (typeof version === "string" && version.length > 0) return version;
3079
+ } catch {}
3080
+ const parent = path$39.dirname(dir);
3081
+ if (parent === dir) break;
3082
+ dir = parent;
3083
+ }
3084
+ return null;
3085
+ }
3086
+ /**
3087
+ * Cache-bust key for an addon entry module: the `?v=` query that makes each
3088
+ * on-disk REVISION of `entryPath` a distinct ESM module URL. Returns null when
3089
+ * the entry cannot be stat'd — the caller then does a plain import.
3090
+ *
3091
+ * The key is `<version>-<content hash>`, and BOTH halves are load-bearing
3092
+ * because each on its own has a documented way of not changing across a real
3093
+ * redeploy:
3094
+ *
3095
+ * - **mtime does not change.** `npm pack` normalises every file's mtime in the
3096
+ * tarball to 499162500000 (1985-10-26); every install path here extracts
3097
+ * such a tarball, so the entry file of v1.0.0 and of v1.0.99 carry the same
3098
+ * mtime. An mtime-only key (what this used to be) is a NO-OP — it produced
3099
+ * one byte-identical URL for every version of every addon ever installed.
3100
+ * - **the version does not change** on a `camstack deploy` of an unbumped
3101
+ * workspace package — the whole dev loop.
3102
+ * - **the entry hash does not change** when a multi-chunk dist alters a chunk
3103
+ * the entry only re-exports; the version bump covers that case.
3104
+ *
3105
+ * Hashing the entry costs one extra read of an already-read file (~1.5 MB for
3106
+ * the largest addon bundle, single-digit ms) once per addon load. Cheap against
3107
+ * what a stale key costs: hub-main keeps validating `addons.custom` input
3108
+ * against the OLD `customActions` schema — silently STRIPPING newly-added
3109
+ * fields — and, worse, Node caches a load that THREW as a rejected module and
3110
+ * replays that rejection for every later retry at the same URL. On 2026-08-12
3111
+ * `@camstack/addon-export-hap` burned 126 identical failed load retries over
3112
+ * 10.5h, none of which could have succeeded: the fixed bytes on disk were never
3113
+ * read.
3114
+ *
3115
+ * Never throws: an unreadable version degrades to the mtime, an unreadable body
3116
+ * to mtime+size.
3117
+ */
3118
+ function addonModuleCacheKey(entryPath) {
3119
+ let stat;
3120
+ try {
3121
+ stat = fs$17.statSync(entryPath);
3122
+ } catch {
3123
+ return null;
3124
+ }
3125
+ const version = readOwningPackageVersion(entryPath) ?? `m${stat.mtimeMs}`;
3126
+ let content;
3127
+ try {
3128
+ content = createHash("sha1").update(fs$17.readFileSync(entryPath)).digest("hex").slice(0, 16);
3129
+ } catch {
3130
+ content = `m${stat.mtimeMs}s${stat.size}`;
3131
+ }
3132
+ return `${version}-${content}`;
3133
+ }
3134
+ /**
3067
3135
  * Import an addon entry module FRESH on every load, defeating Node's
3068
3136
  * per-URL ESM module cache.
3069
3137
  *
@@ -3072,23 +3140,15 @@ function isRecord$1(value) {
3072
3140
  * redeploy returns the STALE previously-cached module. The addon RUNNER child
3073
3141
  * is a fresh process so it reloads correctly — but hub-main also imports the
3074
3142
  * module to read its static catalog (`customActions` Zod schemas, addon class)
3075
- * for the `addons.custom` "validate on the way in" guard. Without cache-busting
3076
- * that guard keeps validating against the OLD schema, silently STRIPPING any
3077
- * newly-added custom-action input field until a full hub restart (observed:
3078
- * benchmark `deviceKey`/multi-device fan-out fields dropped after redeploy).
3143
+ * for the `addons.custom` "validate on the way in" guard.
3079
3144
  *
3080
- * Appending a `?v=<mtimeMs>` query makes each on-disk version a distinct module
3081
- * URL, so a redeploy (new mtime) loads the fresh module. Falls back to a plain
3082
- * import if the file can't be stat'd.
3145
+ * The distinguishing key is {@link addonModuleCacheKey}; a plain import is the
3146
+ * fallback when the entry cannot be stat'd.
3083
3147
  */
3084
3148
  async function importAddonModuleFresh(entryPath) {
3085
- let bust;
3086
- try {
3087
- bust = fs$17.statSync(entryPath).mtimeMs;
3088
- } catch {
3089
- return import(entryPath);
3090
- }
3091
- return import(`${pathToFileURL(entryPath).href}?v=${bust}`);
3149
+ const bust = addonModuleCacheKey(entryPath);
3150
+ if (bust === null) return import(entryPath);
3151
+ return import(`${pathToFileURL(entryPath).href}?v=${encodeURIComponent(bust)}`);
3092
3152
  }
3093
3153
  /**
3094
3154
  * Package.json manifest boundary: the `camstack` field is authored by third-party
@@ -4510,6 +4570,79 @@ var AddonInstaller = class AddonInstaller {
4510
4570
  force: true
4511
4571
  }).catch(() => void 0)));
4512
4572
  }
4573
+ /**
4574
+ * Make `pkgDir` (an addon tree that has NOT been swapped in yet) runnable:
4575
+ * strip the host-provided deps from its manifest, then `npm install` whatever
4576
+ * survives.
4577
+ *
4578
+ * ONE implementation for every path that materialises an addon tree —
4579
+ * `installFromTgz` (deploy/upload) and `applyUpdateFromStaged` (OTA). The OTA
4580
+ * path did not have it, and that is a whole outage class: an addon declaring
4581
+ * `camstack.runtimeDependencies` was broken by its FIRST OTA update, its dist
4582
+ * landing without `node_modules`. Every load then threw `Cannot find module`
4583
+ * and the retry loop replayed it — `@camstack/addon-export-hap`, 126 failed
4584
+ * load retries over 10.5h on 2026-08-12; `@camstack/addon-remote-storage`
4585
+ * (`@aws-sdk/*`, `webdav`) is the same landmine.
4586
+ *
4587
+ * **Strip.** `@camstack/*` are host-provided system packages that resolve via
4588
+ * the host's node_modules (Node walks up from the installed addon dir). Left
4589
+ * in, this `npm install` pulls the LAST PUBLISHED `@camstack/types` (etc.)
4590
+ * into the addon's OWN node_modules where it SHADOWS the host's fresh copy —
4591
+ * local additions to a system package silently never reach the deployed
4592
+ * addon. Addon dists are self-contained besides, so `stripBundledDeps` keeps
4593
+ * only what the manifest explicitly declares in
4594
+ * `camstack.runtimeDependencies` (see fs-utils / ADR-0027). Nothing left to
4595
+ * install is the COMMON case: skip npm rather than pay an empty round-trip.
4596
+ *
4597
+ * **Failure is FATAL.** This used to warn-and-continue on the tgz path ("the
4598
+ * addon may still load if imports are lazy") — live cost: a host with no npm
4599
+ * (packaged Electron Mac agent) installed every runtime-dep addon WITHOUT its
4600
+ * node_modules for DAYS; each reported "running" while its entry failed at
4601
+ * import (`Cannot find module 'sharp'`), the detection group ran 1/3 members,
4602
+ * and the node silently dropped out of the balancer. Throwing here is what
4603
+ * keeps the PREVIOUS install intact: every caller builds the tree before it
4604
+ * swaps, so an abort simply means the swap never happens.
4605
+ *
4606
+ * A missing/unreadable manifest is NOT fatal — it is logged and skipped. The
4607
+ * callers validate the tree themselves, and a tree with no readable
4608
+ * package.json has no declared deps to install (the loader will skip the
4609
+ * package entirely).
4610
+ */
4611
+ async installRuntimeDependencies(pkgDir, packageName) {
4612
+ const pkgJsonPath = path$39.join(pkgDir, "package.json");
4613
+ const pkgView = readPackageJson(pkgJsonPath);
4614
+ if (pkgView == null) {
4615
+ this.logger.error(`${packageName} — no readable package.json in the prepared tree; runtime dependencies NOT installed`, { meta: { pkgJsonPath } });
4616
+ return;
4617
+ }
4618
+ const strippedManifest = stripBundledDeps(pkgView.raw);
4619
+ await fs$17.promises.writeFile(pkgJsonPath, JSON.stringify(strippedManifest, null, 2));
4620
+ const deps = asJsonObject$1(strippedManifest["dependencies"]);
4621
+ const depNames = deps ? Object.keys(deps) : [];
4622
+ if (depNames.length === 0) return;
4623
+ this.logger.info(`${packageName} — installing runtime dependencies`, { meta: {
4624
+ stagingDir: pkgDir,
4625
+ dependencies: depNames
4626
+ } });
4627
+ try {
4628
+ await runNpm([
4629
+ "install",
4630
+ "--omit=dev",
4631
+ "--omit=peer",
4632
+ "--no-audit",
4633
+ "--no-fund",
4634
+ "--no-package-lock",
4635
+ ...this.registry ? ["--registry", this.registry] : []
4636
+ ], this.npmRunOptions(pkgDir, 24e4));
4637
+ } catch (err) {
4638
+ this.logger.error(`${packageName} — runtime dependency install FAILED; update ABORTED (a swapped-in copy would fail every load with "Cannot find module")`, { meta: {
4639
+ pkgDir,
4640
+ dependencies: depNames,
4641
+ error: errMsg$1(err)
4642
+ } });
4643
+ throw new Error(`${packageName} — runtime dependency install failed: ${errMsg$1(err)}`, { cause: err });
4644
+ }
4645
+ }
4513
4646
  /** Install addon from a tgz file (uploaded or downloaded) */
4514
4647
  async installFromTgz(tgzPath) {
4515
4648
  const stagingRoot = path$39.join(this.addonsDir, ".staging");
@@ -4533,25 +4666,7 @@ var AddonInstaller = class AddonInstaller {
4533
4666
  if (!pkgView) throw new Error(`Invalid package.json at ${pkgJsonPath}`);
4534
4667
  if (!pkgView.camstackAddons) throw new Error(`Package ${pkgView.name} has no camstack.addons manifest`);
4535
4668
  const targetDir = path$39.join(this.addonsDir, pkgView.name);
4536
- const strippedManifest = stripBundledDeps(pkgView.raw);
4537
- await fs$17.promises.writeFile(pkgJsonPath, JSON.stringify(strippedManifest, null, 2));
4538
- const strippedRuntimeDeps = strippedManifest["dependencies"];
4539
- if (strippedRuntimeDeps != null && typeof strippedRuntimeDeps === "object" && Object.keys(strippedRuntimeDeps).length > 0) {
4540
- this.logger.info(`${pkgView.name} — installing runtime dependencies`, { meta: { stagingDir: pkgDir } });
4541
- try {
4542
- await runNpm([
4543
- "install",
4544
- "--omit=dev",
4545
- "--omit=peer",
4546
- "--no-audit",
4547
- "--no-fund",
4548
- "--no-package-lock",
4549
- ...this.registry ? ["--registry", this.registry] : []
4550
- ], this.npmRunOptions(pkgDir, 24e4));
4551
- } catch (err) {
4552
- throw new Error(`${pkgView.name} — runtime dependency install failed: ${errMsg$1(err)}`, { cause: err });
4553
- }
4554
- }
4669
+ await this.installRuntimeDependencies(pkgDir, pkgView.name);
4555
4670
  try {
4556
4671
  await installManifestNativeDeps(pkgDir, pkgView.raw, this.logger, this.registry, this.npmCacheDir);
4557
4672
  } catch (nativeErr) {
@@ -4663,18 +4778,32 @@ var AddonInstaller = class AddonInstaller {
4663
4778
  * replaces step 2 ("install new version") with an atomic directory
4664
4779
  * move of `stagedPath` into `addonDir`.
4665
4780
  *
4781
+ * "Skips npm entirely" is true only of the REGISTRY round-trip: the staged
4782
+ * tree is the raw unpacked tarball, so the addon's own
4783
+ * `camstack.runtimeDependencies` still have to be installed into it, exactly
4784
+ * as `installFromTgz` does. Skipping that is what broke every runtime-dep
4785
+ * addon on its first OTA update — see {@link installRuntimeDependencies}.
4786
+ *
4787
+ * Order matters and is `installFromTgz`'s: the tree is COMPLETED FIRST, and
4788
+ * the live install is only then backed up and replaced. So the addon keeps
4789
+ * serving the old version for the whole (minutes-long) npm install, the swap
4790
+ * window stays two renames wide, and a dependency failure aborts with the
4791
+ * previous version untouched instead of leaving a dependency-less copy live.
4792
+ *
4666
4793
  * On cross-device move (EXDEV) falls back to a recursive copy followed
4667
4794
  * by removal of the staged directory. On any swap failure the backup is
4668
4795
  * restored and the error is rethrown — identical recovery to `applyUpdate`.
4669
4796
  *
4670
- * Throws if the package is not tracked in the manifest or if `addonDir`
4671
- * is missing (same preconditions as `applyUpdate`).
4797
+ * Throws if the package is not tracked in the manifest, if `addonDir`
4798
+ * is missing (same preconditions as `applyUpdate`), or if the runtime-dep
4799
+ * install fails.
4672
4800
  */
4673
4801
  async applyUpdateFromStaged(packageName, version, stagedPath) {
4674
4802
  const current = this.manifest.get(packageName);
4675
4803
  if (current == null) throw new Error(`Cannot update ${packageName}: not currently tracked in manifest`);
4676
4804
  const addonDir = path$39.join(this.addonsDir, packageName);
4677
4805
  if (!fs$17.existsSync(addonDir)) throw new Error(`Cannot update ${packageName}: install dir ${addonDir} missing`);
4806
+ await this.installRuntimeDependencies(stagedPath, packageName);
4678
4807
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4679
4808
  const backupRoot = path$39.join(this.addonsDir, ".backups", packageName);
4680
4809
  ensureDir(backupRoot);
@@ -302,6 +302,45 @@ export declare class AddonInstaller {
302
302
  private sweepStagingResidue;
303
303
  /** Best-effort removal of leftover `.evicting/*` dirs from earlier deploys. */
304
304
  private sweepGraveyard;
305
+ /**
306
+ * Make `pkgDir` (an addon tree that has NOT been swapped in yet) runnable:
307
+ * strip the host-provided deps from its manifest, then `npm install` whatever
308
+ * survives.
309
+ *
310
+ * ONE implementation for every path that materialises an addon tree —
311
+ * `installFromTgz` (deploy/upload) and `applyUpdateFromStaged` (OTA). The OTA
312
+ * path did not have it, and that is a whole outage class: an addon declaring
313
+ * `camstack.runtimeDependencies` was broken by its FIRST OTA update, its dist
314
+ * landing without `node_modules`. Every load then threw `Cannot find module`
315
+ * and the retry loop replayed it — `@camstack/addon-export-hap`, 126 failed
316
+ * load retries over 10.5h on 2026-08-12; `@camstack/addon-remote-storage`
317
+ * (`@aws-sdk/*`, `webdav`) is the same landmine.
318
+ *
319
+ * **Strip.** `@camstack/*` are host-provided system packages that resolve via
320
+ * the host's node_modules (Node walks up from the installed addon dir). Left
321
+ * in, this `npm install` pulls the LAST PUBLISHED `@camstack/types` (etc.)
322
+ * into the addon's OWN node_modules where it SHADOWS the host's fresh copy —
323
+ * local additions to a system package silently never reach the deployed
324
+ * addon. Addon dists are self-contained besides, so `stripBundledDeps` keeps
325
+ * only what the manifest explicitly declares in
326
+ * `camstack.runtimeDependencies` (see fs-utils / ADR-0027). Nothing left to
327
+ * install is the COMMON case: skip npm rather than pay an empty round-trip.
328
+ *
329
+ * **Failure is FATAL.** This used to warn-and-continue on the tgz path ("the
330
+ * addon may still load if imports are lazy") — live cost: a host with no npm
331
+ * (packaged Electron Mac agent) installed every runtime-dep addon WITHOUT its
332
+ * node_modules for DAYS; each reported "running" while its entry failed at
333
+ * import (`Cannot find module 'sharp'`), the detection group ran 1/3 members,
334
+ * and the node silently dropped out of the balancer. Throwing here is what
335
+ * keeps the PREVIOUS install intact: every caller builds the tree before it
336
+ * swaps, so an abort simply means the swap never happens.
337
+ *
338
+ * A missing/unreadable manifest is NOT fatal — it is logged and skipped. The
339
+ * callers validate the tree themselves, and a tree with no readable
340
+ * package.json has no declared deps to install (the loader will skip the
341
+ * package entirely).
342
+ */
343
+ private installRuntimeDependencies;
305
344
  /** Install addon from a tgz file (uploaded or downloaded) */
306
345
  installFromTgz(tgzPath: string): Promise<{
307
346
  name: string;
@@ -340,12 +379,25 @@ export declare class AddonInstaller {
340
379
  * replaces step 2 ("install new version") with an atomic directory
341
380
  * move of `stagedPath` into `addonDir`.
342
381
  *
382
+ * "Skips npm entirely" is true only of the REGISTRY round-trip: the staged
383
+ * tree is the raw unpacked tarball, so the addon's own
384
+ * `camstack.runtimeDependencies` still have to be installed into it, exactly
385
+ * as `installFromTgz` does. Skipping that is what broke every runtime-dep
386
+ * addon on its first OTA update — see {@link installRuntimeDependencies}.
387
+ *
388
+ * Order matters and is `installFromTgz`'s: the tree is COMPLETED FIRST, and
389
+ * the live install is only then backed up and replaced. So the addon keeps
390
+ * serving the old version for the whole (minutes-long) npm install, the swap
391
+ * window stays two renames wide, and a dependency failure aborts with the
392
+ * previous version untouched instead of leaving a dependency-less copy live.
393
+ *
343
394
  * On cross-device move (EXDEV) falls back to a recursive copy followed
344
395
  * by removal of the staged directory. On any swap failure the backup is
345
396
  * restored and the error is rethrown — identical recovery to `applyUpdate`.
346
397
  *
347
- * Throws if the package is not tracked in the manifest or if `addonDir`
348
- * is missing (same preconditions as `applyUpdate`).
398
+ * Throws if the package is not tracked in the manifest, if `addonDir`
399
+ * is missing (same preconditions as `applyUpdate`), or if the runtime-dep
400
+ * install fails.
349
401
  */
350
402
  applyUpdateFromStaged(packageName: string, version: string, stagedPath: string): Promise<{
351
403
  name: string;
@@ -1,4 +1,37 @@
1
1
  import { AddonDeclaration, ICamstackAddon, IScopedLogger, StorageLocationDeclaration } from '@camstack/types';
2
+ /**
3
+ * Cache-bust key for an addon entry module: the `?v=` query that makes each
4
+ * on-disk REVISION of `entryPath` a distinct ESM module URL. Returns null when
5
+ * the entry cannot be stat'd — the caller then does a plain import.
6
+ *
7
+ * The key is `<version>-<content hash>`, and BOTH halves are load-bearing
8
+ * because each on its own has a documented way of not changing across a real
9
+ * redeploy:
10
+ *
11
+ * - **mtime does not change.** `npm pack` normalises every file's mtime in the
12
+ * tarball to 499162500000 (1985-10-26); every install path here extracts
13
+ * such a tarball, so the entry file of v1.0.0 and of v1.0.99 carry the same
14
+ * mtime. An mtime-only key (what this used to be) is a NO-OP — it produced
15
+ * one byte-identical URL for every version of every addon ever installed.
16
+ * - **the version does not change** on a `camstack deploy` of an unbumped
17
+ * workspace package — the whole dev loop.
18
+ * - **the entry hash does not change** when a multi-chunk dist alters a chunk
19
+ * the entry only re-exports; the version bump covers that case.
20
+ *
21
+ * Hashing the entry costs one extra read of an already-read file (~1.5 MB for
22
+ * the largest addon bundle, single-digit ms) once per addon load. Cheap against
23
+ * what a stale key costs: hub-main keeps validating `addons.custom` input
24
+ * against the OLD `customActions` schema — silently STRIPPING newly-added
25
+ * fields — and, worse, Node caches a load that THREW as a rejected module and
26
+ * replays that rejection for every later retry at the same URL. On 2026-08-12
27
+ * `@camstack/addon-export-hap` burned 126 identical failed load retries over
28
+ * 10.5h, none of which could have succeeded: the fixed bytes on disk were never
29
+ * read.
30
+ *
31
+ * Never throws: an unreadable version degrades to the mtime, an unreadable body
32
+ * to mtime+size.
33
+ */
34
+ export declare function addonModuleCacheKey(entryPath: string): string | null;
2
35
  export interface RegisteredAddon {
3
36
  readonly declaration: AddonDeclaration;
4
37
  readonly packageName: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/system",
3
- "version": "1.2.81",
3
+ "version": "1.2.82",
4
4
  "description": "Core addon for CamStack — builtins, pipeline, process management, auth, logging, events",
5
5
  "keywords": [
6
6
  "camstack",