@camstack/system 1.1.35 → 1.1.37

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 (53) hide show
  1. package/dist/addon-runner.js +87 -35
  2. package/dist/addon-runner.mjs +87 -35
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.d.ts +8 -4
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +22 -10
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +23 -11
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.d.ts +4 -0
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +23 -9
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +24 -10
  9. package/dist/builtins/alerts/alerts.addon.js +1 -1
  10. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  13. package/dist/builtins/console-logging/index.js +1 -1
  14. package/dist/builtins/console-logging/index.mjs +1 -1
  15. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  16. package/dist/builtins/device-manager/device-manager.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/local-auth/local-auth.addon.js +1 -1
  20. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  21. package/dist/builtins/local-network/local-network.addon.js +1 -1
  22. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  23. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  24. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  25. package/dist/builtins/platform-probe/index.js +1 -1
  26. package/dist/builtins/platform-probe/index.mjs +1 -1
  27. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  28. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  29. package/dist/builtins/snapshot/index.js +1 -1
  30. package/dist/builtins/snapshot/index.mjs +1 -1
  31. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  32. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  33. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  34. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  35. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  36. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  37. package/dist/builtins/system-config/system-config.addon.js +1 -1
  38. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  39. package/dist/builtins/winston-logging/index.js +1 -1
  40. package/dist/builtins/winston-logging/index.mjs +1 -1
  41. package/dist/{dist-DlGZpWo8.mjs → dist-CU3lb_Ys.mjs} +401 -125
  42. package/dist/{dist-DriXJoVg.js → dist-fRVWwrkF.js} +401 -125
  43. package/dist/index.js +26 -7
  44. package/dist/index.mjs +26 -7
  45. package/dist/kernel/index.d.ts +1 -1
  46. package/dist/kernel/moleculer/child-cap-descriptors.d.ts +34 -0
  47. package/dist/kernel/moleculer/child-cap-dispatch.d.ts +10 -2
  48. package/dist/kernel/moleculer/node-registry.d.ts +22 -1
  49. package/dist/kernel/moleculer/register-node-client.d.ts +8 -5
  50. package/dist/kernel/transport/child-cap-protocol.d.ts +20 -0
  51. package/dist/{manifest-python-deps-MUu1e33G.mjs → manifest-python-deps-4QvOLiBn.mjs} +5 -3
  52. package/dist/{manifest-python-deps-jZA_Qorz.js → manifest-python-deps-B6ahe90k.js} +5 -3
  53. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
- const require_manifest_python_deps = require("./manifest-python-deps-jZA_Qorz.js");
2
+ const require_manifest_python_deps = require("./manifest-python-deps-B6ahe90k.js");
3
3
  const require_custom_action_registry = require("./custom-action-registry-vLYEFTtv.js");
4
4
  let node_fs = require("node:fs");
5
5
  node_fs = require_chunk.__toESM(node_fs);
@@ -115,6 +115,17 @@ function isCapMethod(value) {
115
115
  return typeof value === "function";
116
116
  }
117
117
  /**
118
+ * Extract a string `addonId` out of arbitrary method args — the fallback
119
+ * provider-selection hint for calls that arrive WITHOUT the out-of-band
120
+ * `CapCallMessage.addonId` (e.g. a sibling child's unowned collection call
121
+ * forwarded by the parent, where the raw input still carries `addonId`).
122
+ */
123
+ function extractArgsAddonId(args) {
124
+ if (args === null || typeof args !== "object") return void 0;
125
+ const raw = Reflect.get(args, "addonId");
126
+ return typeof raw === "string" ? raw : void 0;
127
+ }
128
+ /**
118
129
  * Creates a unified dispatcher that routes a {@link CapCallInput} to either a
119
130
  * device-scoped or singleton provider, then invokes the requested method.
120
131
  *
@@ -123,12 +134,13 @@ function isCapMethod(value) {
123
134
  function createChildCapDispatch(deps) {
124
135
  return async (call) => {
125
136
  const { capName, method, args, deviceId } = call;
137
+ const addonId = call.addonId ?? extractArgsAddonId(args);
126
138
  let provider;
127
139
  if (deviceId !== void 0) {
128
- provider = deps.getDeviceProvider(capName, deviceId) ?? deps.getSingletonProvider(capName);
140
+ provider = deps.getDeviceProvider(capName, deviceId) ?? deps.getSingletonProvider(capName, addonId);
129
141
  if (provider == null) throw new Error(`child-cap-dispatch: no provider for '${capName}' (device '${String(deviceId)}' or singleton)`);
130
142
  } else {
131
- provider = deps.getSingletonProvider(capName);
143
+ provider = deps.getSingletonProvider(capName, addonId);
132
144
  if (provider == null) throw new Error(`child-cap-dispatch: no singleton provider for '${capName}'`);
133
145
  }
134
146
  const fn = Reflect.get(provider, method);
@@ -137,6 +149,51 @@ function createChildCapDispatch(deps) {
137
149
  };
138
150
  }
139
151
  //#endregion
152
+ //#region src/kernel/moleculer/child-cap-descriptors.ts
153
+ /**
154
+ * Builds the {@link ChildCapDescriptor} array that the UDS child transport
155
+ * sends to the parent on registration.
156
+ *
157
+ * - Singleton caps come from the runner UNION manifest (all caps registered by
158
+ * every addon in this runner). One descriptor per unique (addonId, capName)
159
+ * pair, mode `'singleton'` and no `deviceId`. The `addonId` stamp lets the
160
+ * parent's manifest builder register each hosted addon's caps under the
161
+ * REAL addon id — a GROUPED runner (`execution.group`) hosts several addons,
162
+ * and deduplicating by capName alone would collapse two co-located providers
163
+ * of the same collection cap (e.g. `network-access` from cloudflare-tunnel
164
+ * AND tailscale-ingress) into one descriptor registered under a synthetic
165
+ * `addonId = childId` (the group name) — the "@camstack/addon-net-access"
166
+ * admin-UI bug.
167
+ * - Device-scoped caps come from the native-cap snapshot — one descriptor per
168
+ * (capName, deviceId) pair, mode `'collection'`, stamped with the owning
169
+ * addon's id.
170
+ *
171
+ * Pure module — extracted from `addon-runner.ts` (whose `main()` runs at
172
+ * import time) so it is unit-testable.
173
+ */
174
+ function buildChildCapDescriptors(manifest, nativeCapSnapshot) {
175
+ const descriptors = [];
176
+ const seen = /* @__PURE__ */ new Set();
177
+ for (const entry of manifest) for (const capName of entry.capabilities) {
178
+ const key = `${entry.addonId}::${capName}`;
179
+ if (!seen.has(key)) {
180
+ seen.add(key);
181
+ descriptors.push({
182
+ capName,
183
+ mode: "singleton",
184
+ addonId: entry.addonId
185
+ });
186
+ }
187
+ }
188
+ for (const { capName, addonId, deviceIds } of nativeCapSnapshot) for (const deviceId of deviceIds) descriptors.push({
189
+ capName,
190
+ mode: "collection",
191
+ deviceId,
192
+ addonId
193
+ });
194
+ return descriptors;
195
+ }
196
+ //#endregion
140
197
  //#region src/kernel/moleculer/child-addon-call-dispatch.ts
141
198
  function isRecord$1(value) {
142
199
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -333,33 +390,6 @@ async function loadAddonModule(addonId, addonDir) {
333
390
  AddonClass
334
391
  };
335
392
  }
336
- /**
337
- * Builds the {@link ChildCapDescriptor} array that the UDS child transport
338
- * sends to the parent on registration.
339
- *
340
- * - Singleton caps come from the runner UNION manifest (all caps registered by
341
- * every addon in this runner). One descriptor per unique cap name, mode
342
- * `'singleton'` and no `deviceId`.
343
- * - Device-scoped caps come from the native-cap snapshot — one descriptor per
344
- * (capName, deviceId) pair, mode `'collection'`.
345
- */
346
- function buildChildCapDescriptors(manifest, nativeCapSnapshot) {
347
- const descriptors = [];
348
- const seen = /* @__PURE__ */ new Set();
349
- for (const entry of manifest) for (const capName of entry.capabilities) if (!seen.has(capName)) {
350
- seen.add(capName);
351
- descriptors.push({
352
- capName,
353
- mode: "singleton"
354
- });
355
- }
356
- for (const { capName, deviceIds } of nativeCapSnapshot) for (const deviceId of deviceIds) descriptors.push({
357
- capName,
358
- mode: "collection",
359
- deviceId
360
- });
361
- return descriptors;
362
- }
363
393
  async function main() {
364
394
  const runnerId = process.env["CAMSTACK_RUNNER_ID"];
365
395
  const addonsJson = process.env["CAMSTACK_RUNNER_ADDONS"];
@@ -427,14 +457,34 @@ async function main() {
427
457
  const runnerManifest = [];
428
458
  /**
429
459
  * Aggregated singleton (system-scoped) providers across ALL addons in this
430
- * runner — keyed by capName. Read by the UDS child-cap dispatch to resolve
431
- * singleton calls (no Moleculer). Populated during the addon-init loop; a
432
- * cap-call that arrives before its addon initialises is rejected by the
460
+ * runner — keyed capName → (addonId → provider). Read by the UDS child-cap
461
+ * dispatch to resolve singleton calls (no Moleculer). The nested per-addon
462
+ * keying matters for GROUPED runners (`execution.group`): two co-located
463
+ * addons can register the SAME collection cap (e.g. `network-access` from
464
+ * cloudflare-tunnel AND tailscale-ingress) — a flat capName map would keep
465
+ * only the last-registered provider. Populated during the addon-init loop;
466
+ * a cap-call that arrives before its addon initialises is rejected by the
433
467
  * dispatcher (the parent only routes caps the child has registered via the
434
468
  * UDS `register`/`updateCaps` handshake).
435
469
  */
436
470
  const allSingletonProviders = /* @__PURE__ */ new Map();
437
471
  /**
472
+ * Resolve a singleton provider for the dispatcher: exact (capName, addonId)
473
+ * match when the hint names a hosted provider, else the cap's first
474
+ * registered provider (an unmatched `addonId` is method DATA — e.g.
475
+ * `addons.restartAddon({ addonId })` — not a routing key).
476
+ */
477
+ const resolveSingletonProvider = (capName, addonId) => {
478
+ const byAddon = allSingletonProviders.get(capName);
479
+ if (!byAddon || byAddon.size === 0) return null;
480
+ if (addonId !== void 0) {
481
+ const exact = byAddon.get(addonId);
482
+ if (exact !== void 0) return exact;
483
+ }
484
+ const first = byAddon.values().next();
485
+ return first.done ? null : first.value;
486
+ };
487
+ /**
438
488
  * Per-addon route-provider map — keyed by addonId → the addon's live
439
489
  * `addon-routes` provider object. Resolves `getRouteProvider(addonId)`
440
490
  * per-addon rather than returning a single global provider. Populated
@@ -473,7 +523,7 @@ async function main() {
473
523
  childId: runnerId,
474
524
  caps: buildChildCapDescriptors(runnerManifest, require_manifest_python_deps.getWorkerNativeCapSnapshot()),
475
525
  dispatch: createChildCapDispatch({
476
- getSingletonProvider: (capName) => allSingletonProviders.get(capName) ?? null,
526
+ getSingletonProvider: (capName, addonId) => resolveSingletonProvider(capName, addonId),
477
527
  getDeviceProvider: (capName, deviceId) => require_manifest_python_deps.getWorkerNativeCapProvider(capName, deviceId) ?? null
478
528
  })
479
529
  });
@@ -538,7 +588,9 @@ async function main() {
538
588
  context.registerProvider(capName, reg.provider);
539
589
  const prov = reg.provider;
540
590
  if (prov !== null && typeof prov === "object" && !Array.isArray(prov)) {
541
- allSingletonProviders.set(capName, prov);
591
+ const byAddon = allSingletonProviders.get(capName) ?? /* @__PURE__ */ new Map();
592
+ byAddon.set(spec.addonId, prov);
593
+ allSingletonProviders.set(capName, byAddon);
542
594
  if (capName === "addon-routes") addonRouteProviders.set(spec.addonId, prov);
543
595
  }
544
596
  }
@@ -1,4 +1,4 @@
1
- import { F as createUdsLoggerWithControl, I as LocalChildClient, i as createUdsAddonContext, it as setWorkerNativeCapsChangeListener, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestPythonDeps, tt as getWorkerNativeCapProvider, vt as installManifestNativeDeps, yt as resolveAddonClass } from "./manifest-python-deps-MUu1e33G.mjs";
1
+ import { F as createUdsLoggerWithControl, I as LocalChildClient, i as createUdsAddonContext, it as setWorkerNativeCapsChangeListener, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestPythonDeps, tt as getWorkerNativeCapProvider, vt as installManifestNativeDeps, yt as resolveAddonClass } from "./manifest-python-deps-4QvOLiBn.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
3
3
  import { register } from "node:module";
4
4
  import * as fs from "node:fs";
@@ -111,6 +111,17 @@ function isCapMethod(value) {
111
111
  return typeof value === "function";
112
112
  }
113
113
  /**
114
+ * Extract a string `addonId` out of arbitrary method args — the fallback
115
+ * provider-selection hint for calls that arrive WITHOUT the out-of-band
116
+ * `CapCallMessage.addonId` (e.g. a sibling child's unowned collection call
117
+ * forwarded by the parent, where the raw input still carries `addonId`).
118
+ */
119
+ function extractArgsAddonId(args) {
120
+ if (args === null || typeof args !== "object") return void 0;
121
+ const raw = Reflect.get(args, "addonId");
122
+ return typeof raw === "string" ? raw : void 0;
123
+ }
124
+ /**
114
125
  * Creates a unified dispatcher that routes a {@link CapCallInput} to either a
115
126
  * device-scoped or singleton provider, then invokes the requested method.
116
127
  *
@@ -119,12 +130,13 @@ function isCapMethod(value) {
119
130
  function createChildCapDispatch(deps) {
120
131
  return async (call) => {
121
132
  const { capName, method, args, deviceId } = call;
133
+ const addonId = call.addonId ?? extractArgsAddonId(args);
122
134
  let provider;
123
135
  if (deviceId !== void 0) {
124
- provider = deps.getDeviceProvider(capName, deviceId) ?? deps.getSingletonProvider(capName);
136
+ provider = deps.getDeviceProvider(capName, deviceId) ?? deps.getSingletonProvider(capName, addonId);
125
137
  if (provider == null) throw new Error(`child-cap-dispatch: no provider for '${capName}' (device '${String(deviceId)}' or singleton)`);
126
138
  } else {
127
- provider = deps.getSingletonProvider(capName);
139
+ provider = deps.getSingletonProvider(capName, addonId);
128
140
  if (provider == null) throw new Error(`child-cap-dispatch: no singleton provider for '${capName}'`);
129
141
  }
130
142
  const fn = Reflect.get(provider, method);
@@ -133,6 +145,51 @@ function createChildCapDispatch(deps) {
133
145
  };
134
146
  }
135
147
  //#endregion
148
+ //#region src/kernel/moleculer/child-cap-descriptors.ts
149
+ /**
150
+ * Builds the {@link ChildCapDescriptor} array that the UDS child transport
151
+ * sends to the parent on registration.
152
+ *
153
+ * - Singleton caps come from the runner UNION manifest (all caps registered by
154
+ * every addon in this runner). One descriptor per unique (addonId, capName)
155
+ * pair, mode `'singleton'` and no `deviceId`. The `addonId` stamp lets the
156
+ * parent's manifest builder register each hosted addon's caps under the
157
+ * REAL addon id — a GROUPED runner (`execution.group`) hosts several addons,
158
+ * and deduplicating by capName alone would collapse two co-located providers
159
+ * of the same collection cap (e.g. `network-access` from cloudflare-tunnel
160
+ * AND tailscale-ingress) into one descriptor registered under a synthetic
161
+ * `addonId = childId` (the group name) — the "@camstack/addon-net-access"
162
+ * admin-UI bug.
163
+ * - Device-scoped caps come from the native-cap snapshot — one descriptor per
164
+ * (capName, deviceId) pair, mode `'collection'`, stamped with the owning
165
+ * addon's id.
166
+ *
167
+ * Pure module — extracted from `addon-runner.ts` (whose `main()` runs at
168
+ * import time) so it is unit-testable.
169
+ */
170
+ function buildChildCapDescriptors(manifest, nativeCapSnapshot) {
171
+ const descriptors = [];
172
+ const seen = /* @__PURE__ */ new Set();
173
+ for (const entry of manifest) for (const capName of entry.capabilities) {
174
+ const key = `${entry.addonId}::${capName}`;
175
+ if (!seen.has(key)) {
176
+ seen.add(key);
177
+ descriptors.push({
178
+ capName,
179
+ mode: "singleton",
180
+ addonId: entry.addonId
181
+ });
182
+ }
183
+ }
184
+ for (const { capName, addonId, deviceIds } of nativeCapSnapshot) for (const deviceId of deviceIds) descriptors.push({
185
+ capName,
186
+ mode: "collection",
187
+ deviceId,
188
+ addonId
189
+ });
190
+ return descriptors;
191
+ }
192
+ //#endregion
136
193
  //#region src/kernel/moleculer/child-addon-call-dispatch.ts
137
194
  function isRecord$1(value) {
138
195
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -329,33 +386,6 @@ async function loadAddonModule(addonId, addonDir) {
329
386
  AddonClass
330
387
  };
331
388
  }
332
- /**
333
- * Builds the {@link ChildCapDescriptor} array that the UDS child transport
334
- * sends to the parent on registration.
335
- *
336
- * - Singleton caps come from the runner UNION manifest (all caps registered by
337
- * every addon in this runner). One descriptor per unique cap name, mode
338
- * `'singleton'` and no `deviceId`.
339
- * - Device-scoped caps come from the native-cap snapshot — one descriptor per
340
- * (capName, deviceId) pair, mode `'collection'`.
341
- */
342
- function buildChildCapDescriptors(manifest, nativeCapSnapshot) {
343
- const descriptors = [];
344
- const seen = /* @__PURE__ */ new Set();
345
- for (const entry of manifest) for (const capName of entry.capabilities) if (!seen.has(capName)) {
346
- seen.add(capName);
347
- descriptors.push({
348
- capName,
349
- mode: "singleton"
350
- });
351
- }
352
- for (const { capName, deviceIds } of nativeCapSnapshot) for (const deviceId of deviceIds) descriptors.push({
353
- capName,
354
- mode: "collection",
355
- deviceId
356
- });
357
- return descriptors;
358
- }
359
389
  async function main() {
360
390
  const runnerId = process.env["CAMSTACK_RUNNER_ID"];
361
391
  const addonsJson = process.env["CAMSTACK_RUNNER_ADDONS"];
@@ -423,14 +453,34 @@ async function main() {
423
453
  const runnerManifest = [];
424
454
  /**
425
455
  * Aggregated singleton (system-scoped) providers across ALL addons in this
426
- * runner — keyed by capName. Read by the UDS child-cap dispatch to resolve
427
- * singleton calls (no Moleculer). Populated during the addon-init loop; a
428
- * cap-call that arrives before its addon initialises is rejected by the
456
+ * runner — keyed capName → (addonId → provider). Read by the UDS child-cap
457
+ * dispatch to resolve singleton calls (no Moleculer). The nested per-addon
458
+ * keying matters for GROUPED runners (`execution.group`): two co-located
459
+ * addons can register the SAME collection cap (e.g. `network-access` from
460
+ * cloudflare-tunnel AND tailscale-ingress) — a flat capName map would keep
461
+ * only the last-registered provider. Populated during the addon-init loop;
462
+ * a cap-call that arrives before its addon initialises is rejected by the
429
463
  * dispatcher (the parent only routes caps the child has registered via the
430
464
  * UDS `register`/`updateCaps` handshake).
431
465
  */
432
466
  const allSingletonProviders = /* @__PURE__ */ new Map();
433
467
  /**
468
+ * Resolve a singleton provider for the dispatcher: exact (capName, addonId)
469
+ * match when the hint names a hosted provider, else the cap's first
470
+ * registered provider (an unmatched `addonId` is method DATA — e.g.
471
+ * `addons.restartAddon({ addonId })` — not a routing key).
472
+ */
473
+ const resolveSingletonProvider = (capName, addonId) => {
474
+ const byAddon = allSingletonProviders.get(capName);
475
+ if (!byAddon || byAddon.size === 0) return null;
476
+ if (addonId !== void 0) {
477
+ const exact = byAddon.get(addonId);
478
+ if (exact !== void 0) return exact;
479
+ }
480
+ const first = byAddon.values().next();
481
+ return first.done ? null : first.value;
482
+ };
483
+ /**
434
484
  * Per-addon route-provider map — keyed by addonId → the addon's live
435
485
  * `addon-routes` provider object. Resolves `getRouteProvider(addonId)`
436
486
  * per-addon rather than returning a single global provider. Populated
@@ -469,7 +519,7 @@ async function main() {
469
519
  childId: runnerId,
470
520
  caps: buildChildCapDescriptors(runnerManifest, getWorkerNativeCapSnapshot()),
471
521
  dispatch: createChildCapDispatch({
472
- getSingletonProvider: (capName) => allSingletonProviders.get(capName) ?? null,
522
+ getSingletonProvider: (capName, addonId) => resolveSingletonProvider(capName, addonId),
473
523
  getDeviceProvider: (capName, deviceId) => getWorkerNativeCapProvider(capName, deviceId) ?? null
474
524
  })
475
525
  });
@@ -534,7 +584,9 @@ async function main() {
534
584
  context.registerProvider(capName, reg.provider);
535
585
  const prov = reg.provider;
536
586
  if (prov !== null && typeof prov === "object" && !Array.isArray(prov)) {
537
- allSingletonProviders.set(capName, prov);
587
+ const byAddon = allSingletonProviders.get(capName) ?? /* @__PURE__ */ new Map();
588
+ byAddon.set(spec.addonId, prov);
589
+ allSingletonProviders.set(capName, byAddon);
538
590
  if (capName === "addon-routes") addonRouteProviders.set(spec.addonId, prov);
539
591
  }
540
592
  }
@@ -2,6 +2,8 @@ import { BaseAddon, ProviderRegistration } from '@camstack/types';
2
2
  export declare class AddonPagesAggregatorAddon extends BaseAddon {
3
3
  readonly id = "addon-pages-aggregator";
4
4
  private resolvedPaths;
5
+ /** Content-hash cache-buster memo, keyed `path:size:mtime`. */
6
+ private readonly bundleHashCache;
5
7
  /**
6
8
  * Last successful `listPages()` snapshot per source. Used as the
7
9
  * "stale-but-valid" fallback when a source transiently fails — drops
@@ -27,10 +29,12 @@ export declare class AddonPagesAggregatorAddon extends BaseAddon {
27
29
  private scheduleRetry;
28
30
  private retrySource;
29
31
  /**
30
- * Build `/api/addon-pages/<addonId>/<bundle>?v=<mtime>`. Falls back
31
- * to `Date.now()` when the bundle path can't be stat'd (remote addon
32
- * with no local file, addon not yet on disk, etc.) — the browser
33
- * just gets a fresh URL on each call instead of cache-friendly mtime.
32
+ * Build `/api/addon-pages/<addonId>/<bundle>?v=<contentHash>`. A CONTENT
33
+ * hash, NOT mtime: reproducible builds stamp a FIXED mtime (observed
34
+ * 1985-10-26) so an mtime `?v=` never changes across redeploys and a
35
+ * caching proxy (Cloudflare tunnel) serves a STALE bundle. Hashing the
36
+ * (small) MF entry file busts the cache exactly when the bundle changes.
37
+ * Memoised per (path,size,mtime); `Date.now()` when the file is absent.
34
38
  */
35
39
  private makeBundleUrl;
36
40
  /**
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DriXJoVg.js");
6
+ const require_dist = require("../../dist-fRVWwrkF.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -88,6 +88,8 @@ var PER_SOURCE_TIMEOUT_MS = 2500;
88
88
  var AddonPagesAggregatorAddon = class extends require_dist.BaseAddon {
89
89
  id = "addon-pages-aggregator";
90
90
  resolvedPaths = null;
91
+ /** Content-hash cache-buster memo, keyed `path:size:mtime`. */
92
+ bundleHashCache = /* @__PURE__ */ new Map();
91
93
  /**
92
94
  * Last successful `listPages()` snapshot per source. Used as the
93
95
  * "stale-but-valid" fallback when a source transiently fails — drops
@@ -205,18 +207,28 @@ var AddonPagesAggregatorAddon = class extends require_dist.BaseAddon {
205
207
  }
206
208
  }
207
209
  /**
208
- * Build `/api/addon-pages/<addonId>/<bundle>?v=<mtime>`. Falls back
209
- * to `Date.now()` when the bundle path can't be stat'd (remote addon
210
- * with no local file, addon not yet on disk, etc.) — the browser
211
- * just gets a fresh URL on each call instead of cache-friendly mtime.
210
+ * Build `/api/addon-pages/<addonId>/<bundle>?v=<contentHash>`. A CONTENT
211
+ * hash, NOT mtime: reproducible builds stamp a FIXED mtime (observed
212
+ * 1985-10-26) so an mtime `?v=` never changes across redeploys and a
213
+ * caching proxy (Cloudflare tunnel) serves a STALE bundle. Hashing the
214
+ * (small) MF entry file busts the cache exactly when the bundle changes.
215
+ * Memoised per (path,size,mtime); `Date.now()` when the file is absent.
212
216
  */
213
217
  makeBundleUrl(addonId, bundle) {
214
218
  const bundlePath = this.resolveBundlePath(addonId, bundle);
215
- let mtime = Date.now();
216
- if (bundlePath !== null) try {
217
- mtime = node_fs.statSync(bundlePath).mtimeMs;
218
- } catch {}
219
- return `/api/addon-pages/${addonId}/${bundle}?v=${Math.floor(mtime)}`;
219
+ if (bundlePath === null) return `/api/addon-pages/${addonId}/${bundle}?v=${Math.floor(Date.now())}`;
220
+ try {
221
+ const stat = node_fs.statSync(bundlePath);
222
+ const cacheKey = `${bundlePath}:${stat.size}:${stat.mtimeMs}`;
223
+ let v = this.bundleHashCache.get(cacheKey);
224
+ if (v === void 0) {
225
+ v = (0, node_crypto.createHash)("sha1").update(node_fs.readFileSync(bundlePath)).digest("hex").slice(0, 12);
226
+ this.bundleHashCache.set(cacheKey, v);
227
+ }
228
+ return `/api/addon-pages/${addonId}/${bundle}?v=${v}`;
229
+ } catch {
230
+ return `/api/addon-pages/${addonId}/${bundle}?v=${Math.floor(Date.now())}`;
231
+ }
220
232
  }
221
233
  /**
222
234
  * Resolve the local filesystem path for an addon's bundle. Mirrors
@@ -1,8 +1,8 @@
1
- import { $ as errMsg, et as BaseAddon, f as addonPagesCapability, st as EventCategory } from "../../dist-DlGZpWo8.mjs";
1
+ import { $ as errMsg, et as BaseAddon, f as addonPagesCapability, st as EventCategory } from "../../dist-CU3lb_Ys.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import * as path$1 from "node:path";
5
- import { randomUUID } from "node:crypto";
5
+ import { createHash, randomUUID } from "node:crypto";
6
6
  //#region src/builtins/addon-pages-aggregator/dedupe-pages.ts
7
7
  /** Stable identity for an addon page, independent of which node reported it. */
8
8
  function pageKey(info) {
@@ -81,6 +81,8 @@ var PER_SOURCE_TIMEOUT_MS = 2500;
81
81
  var AddonPagesAggregatorAddon = class extends BaseAddon {
82
82
  id = "addon-pages-aggregator";
83
83
  resolvedPaths = null;
84
+ /** Content-hash cache-buster memo, keyed `path:size:mtime`. */
85
+ bundleHashCache = /* @__PURE__ */ new Map();
84
86
  /**
85
87
  * Last successful `listPages()` snapshot per source. Used as the
86
88
  * "stale-but-valid" fallback when a source transiently fails — drops
@@ -198,18 +200,28 @@ var AddonPagesAggregatorAddon = class extends BaseAddon {
198
200
  }
199
201
  }
200
202
  /**
201
- * Build `/api/addon-pages/<addonId>/<bundle>?v=<mtime>`. Falls back
202
- * to `Date.now()` when the bundle path can't be stat'd (remote addon
203
- * with no local file, addon not yet on disk, etc.) — the browser
204
- * just gets a fresh URL on each call instead of cache-friendly mtime.
203
+ * Build `/api/addon-pages/<addonId>/<bundle>?v=<contentHash>`. A CONTENT
204
+ * hash, NOT mtime: reproducible builds stamp a FIXED mtime (observed
205
+ * 1985-10-26) so an mtime `?v=` never changes across redeploys and a
206
+ * caching proxy (Cloudflare tunnel) serves a STALE bundle. Hashing the
207
+ * (small) MF entry file busts the cache exactly when the bundle changes.
208
+ * Memoised per (path,size,mtime); `Date.now()` when the file is absent.
205
209
  */
206
210
  makeBundleUrl(addonId, bundle) {
207
211
  const bundlePath = this.resolveBundlePath(addonId, bundle);
208
- let mtime = Date.now();
209
- if (bundlePath !== null) try {
210
- mtime = fs.statSync(bundlePath).mtimeMs;
211
- } catch {}
212
- return `/api/addon-pages/${addonId}/${bundle}?v=${Math.floor(mtime)}`;
212
+ if (bundlePath === null) return `/api/addon-pages/${addonId}/${bundle}?v=${Math.floor(Date.now())}`;
213
+ try {
214
+ const stat = fs.statSync(bundlePath);
215
+ const cacheKey = `${bundlePath}:${stat.size}:${stat.mtimeMs}`;
216
+ let v = this.bundleHashCache.get(cacheKey);
217
+ if (v === void 0) {
218
+ v = createHash("sha1").update(fs.readFileSync(bundlePath)).digest("hex").slice(0, 12);
219
+ this.bundleHashCache.set(cacheKey, v);
220
+ }
221
+ return `/api/addon-pages/${addonId}/${bundle}?v=${v}`;
222
+ } catch {
223
+ return `/api/addon-pages/${addonId}/${bundle}?v=${Math.floor(Date.now())}`;
224
+ }
213
225
  }
214
226
  /**
215
227
  * Resolve the local filesystem path for an addon's bundle. Mirrors
@@ -2,6 +2,8 @@ import { BaseAddon, ProviderRegistration } from '@camstack/types';
2
2
  export declare class AddonWidgetsAggregatorAddon extends BaseAddon {
3
3
  readonly id = "addon-widgets-aggregator";
4
4
  private resolvedPaths;
5
+ /** Content-hash cache-buster memo, keyed `path:size:mtime`. */
6
+ private readonly bundleHashCache;
5
7
  /**
6
8
  * Last successful `listWidgets()` snapshot per source. Used as the
7
9
  * "stale-but-valid" fallback when a source transiently fails — drops
@@ -41,6 +43,8 @@ export declare class AddonWidgetsAggregatorAddon extends BaseAddon {
41
43
  * just gets a fresh URL on each call instead of cache-friendly mtime.
42
44
  */
43
45
  private makeBundleUrl;
46
+ /** sha1(content) 12-hex of a bundle entry file, memoised; `Date.now()` when absent. */
47
+ private bundleVersionTag;
44
48
  private resolveBundlePath;
45
49
  private resolvePaths;
46
50
  }
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DriXJoVg.js");
6
+ const require_dist = require("../../dist-fRVWwrkF.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_fs = require("node:fs");
9
9
  node_fs = require_chunk.__toESM(node_fs);
@@ -18,9 +18,10 @@ let node_crypto = require("node:crypto");
18
18
  * Mirrors `addon-pages-aggregator` exactly: walks every registered
19
19
  * `addon-widgets-source` (collection) provider and emits an enriched
20
20
  * widget metadata list with versioned `bundleUrl`s pointing at
21
- * `/api/addon-widgets/<addonId>/<bundle>?v=<mtime>`. The filesystem
22
- * `mtime` cache-buster lets the browser pick up addon rebuilds without
23
- * manual reload.
21
+ * `/api/addon-widgets/<addonId>/<bundle>?v=<contentHash>`. The
22
+ * content-hash cache-buster lets the browser (and any caching proxy)
23
+ * pick up addon rebuilds without manual reload — see `makeBundleUrl`
24
+ * for why mtime is unusable (reproducible builds fix it).
24
25
  *
25
26
  * The static file endpoint (`/api/addon-widgets/:addonId/*`) is served
26
27
  * by `AddonWidgetsService.resolveBundle()` on the server side; this
@@ -57,6 +58,8 @@ var PER_SOURCE_TIMEOUT_MS = 2500;
57
58
  var AddonWidgetsAggregatorAddon = class extends require_dist.BaseAddon {
58
59
  id = "addon-widgets-aggregator";
59
60
  resolvedPaths = null;
61
+ /** Content-hash cache-buster memo, keyed `path:size:mtime`. */
62
+ bundleHashCache = /* @__PURE__ */ new Map();
60
63
  /**
61
64
  * Last successful `listWidgets()` snapshot per source. Used as the
62
65
  * "stale-but-valid" fallback when a source transiently fails — drops
@@ -195,11 +198,22 @@ var AddonWidgetsAggregatorAddon = class extends require_dist.BaseAddon {
195
198
  */
196
199
  makeBundleUrl(addonId, bundle) {
197
200
  const bundlePath = this.resolveBundlePath(addonId, bundle);
198
- let mtime = Date.now();
199
- if (bundlePath !== null) try {
200
- mtime = node_fs.statSync(bundlePath).mtimeMs;
201
- } catch {}
202
- return `/api/addon-widgets/${addonId}/${bundle}?v=${Math.floor(mtime)}`;
201
+ return `/api/addon-widgets/${addonId}/${bundle}?v=${this.bundleVersionTag(bundlePath)}`;
202
+ }
203
+ /** sha1(content) 12-hex of a bundle entry file, memoised; `Date.now()` when absent. */
204
+ bundleVersionTag(bundlePath) {
205
+ if (bundlePath === null) return String(Math.floor(Date.now()));
206
+ try {
207
+ const stat = node_fs.statSync(bundlePath);
208
+ const cacheKey = `${bundlePath}:${stat.size}:${stat.mtimeMs}`;
209
+ const cached = this.bundleHashCache.get(cacheKey);
210
+ if (cached !== void 0) return cached;
211
+ const hash = (0, node_crypto.createHash)("sha1").update(node_fs.readFileSync(bundlePath)).digest("hex").slice(0, 12);
212
+ this.bundleHashCache.set(cacheKey, hash);
213
+ return hash;
214
+ } catch {
215
+ return String(Math.floor(Date.now()));
216
+ }
203
217
  }
204
218
  resolveBundlePath(addonId, bundle) {
205
219
  const paths = this.resolvedPaths;
@@ -1,8 +1,8 @@
1
- import { $ as errMsg, et as BaseAddon, m as addonWidgetsCapability, st as EventCategory } from "../../dist-DlGZpWo8.mjs";
1
+ import { $ as errMsg, et as BaseAddon, m as addonWidgetsCapability, st as EventCategory } from "../../dist-CU3lb_Ys.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import * as path$1 from "node:path";
5
- import { randomUUID } from "node:crypto";
5
+ import { createHash, randomUUID } from "node:crypto";
6
6
  //#region src/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.ts
7
7
  /**
8
8
  * Addon Widgets Aggregator — hub-local builtin that owns the singleton
@@ -11,9 +11,10 @@ import { randomUUID } from "node:crypto";
11
11
  * Mirrors `addon-pages-aggregator` exactly: walks every registered
12
12
  * `addon-widgets-source` (collection) provider and emits an enriched
13
13
  * widget metadata list with versioned `bundleUrl`s pointing at
14
- * `/api/addon-widgets/<addonId>/<bundle>?v=<mtime>`. The filesystem
15
- * `mtime` cache-buster lets the browser pick up addon rebuilds without
16
- * manual reload.
14
+ * `/api/addon-widgets/<addonId>/<bundle>?v=<contentHash>`. The
15
+ * content-hash cache-buster lets the browser (and any caching proxy)
16
+ * pick up addon rebuilds without manual reload — see `makeBundleUrl`
17
+ * for why mtime is unusable (reproducible builds fix it).
17
18
  *
18
19
  * The static file endpoint (`/api/addon-widgets/:addonId/*`) is served
19
20
  * by `AddonWidgetsService.resolveBundle()` on the server side; this
@@ -50,6 +51,8 @@ var PER_SOURCE_TIMEOUT_MS = 2500;
50
51
  var AddonWidgetsAggregatorAddon = class extends BaseAddon {
51
52
  id = "addon-widgets-aggregator";
52
53
  resolvedPaths = null;
54
+ /** Content-hash cache-buster memo, keyed `path:size:mtime`. */
55
+ bundleHashCache = /* @__PURE__ */ new Map();
53
56
  /**
54
57
  * Last successful `listWidgets()` snapshot per source. Used as the
55
58
  * "stale-but-valid" fallback when a source transiently fails — drops
@@ -188,11 +191,22 @@ var AddonWidgetsAggregatorAddon = class extends BaseAddon {
188
191
  */
189
192
  makeBundleUrl(addonId, bundle) {
190
193
  const bundlePath = this.resolveBundlePath(addonId, bundle);
191
- let mtime = Date.now();
192
- if (bundlePath !== null) try {
193
- mtime = fs.statSync(bundlePath).mtimeMs;
194
- } catch {}
195
- return `/api/addon-widgets/${addonId}/${bundle}?v=${Math.floor(mtime)}`;
194
+ return `/api/addon-widgets/${addonId}/${bundle}?v=${this.bundleVersionTag(bundlePath)}`;
195
+ }
196
+ /** sha1(content) 12-hex of a bundle entry file, memoised; `Date.now()` when absent. */
197
+ bundleVersionTag(bundlePath) {
198
+ if (bundlePath === null) return String(Math.floor(Date.now()));
199
+ try {
200
+ const stat = fs.statSync(bundlePath);
201
+ const cacheKey = `${bundlePath}:${stat.size}:${stat.mtimeMs}`;
202
+ const cached = this.bundleHashCache.get(cacheKey);
203
+ if (cached !== void 0) return cached;
204
+ const hash = createHash("sha1").update(fs.readFileSync(bundlePath)).digest("hex").slice(0, 12);
205
+ this.bundleHashCache.set(cacheKey, hash);
206
+ return hash;
207
+ } catch {
208
+ return String(Math.floor(Date.now()));
209
+ }
196
210
  }
197
211
  resolveBundlePath(addonId, bundle) {
198
212
  const paths = this.resolvedPaths;
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-DriXJoVg.js");
6
+ const require_dist = require("../../dist-fRVWwrkF.js");
7
7
  //#region src/builtins/alerts/alerts.addon.ts
8
8
  var CATEGORY_MAPPINGS = {
9
9
  "addon.crashed": {