@camstack/system 1.2.414 → 1.2.416

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 (92) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/addon-utils.js +1 -1
  4. package/dist/addon-utils.mjs +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  9. package/dist/builtins/alerts/alerts.addon.js +1 -1
  10. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  11. package/dist/builtins/autotrack/index.js +1 -1
  12. package/dist/builtins/autotrack/index.mjs +1 -1
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  14. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  15. package/dist/builtins/calendars/calendars.addon.js +1 -1
  16. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  17. package/dist/builtins/camera-grid/index.js +1 -1
  18. package/dist/builtins/camera-grid/index.mjs +1 -1
  19. package/dist/builtins/composer/composer.addon.js +2 -2
  20. package/dist/builtins/composer/composer.addon.mjs +2 -2
  21. package/dist/builtins/console-logging/index.js +1 -1
  22. package/dist/builtins/console-logging/index.mjs +1 -1
  23. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  24. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  25. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  26. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  27. package/dist/builtins/hub-forwarder/index.js +1 -1
  28. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  29. package/dist/builtins/keychain/index.js +3 -3
  30. package/dist/builtins/keychain/index.mjs +3 -3
  31. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  32. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  33. package/dist/builtins/local-auth/local-auth.addon.js +9 -8
  34. package/dist/builtins/local-auth/local-auth.addon.mjs +4 -3
  35. package/dist/builtins/local-network/hub-mdns-ciao.d.ts +7 -0
  36. package/dist/builtins/local-network/hub-mdns.d.ts +58 -0
  37. package/dist/builtins/local-network/local-network.addon.d.ts +11 -0
  38. package/dist/builtins/local-network/local-network.addon.js +194 -3
  39. package/dist/builtins/local-network/local-network.addon.mjs +195 -3
  40. package/dist/builtins/loki-logging/index.js +1 -1
  41. package/dist/builtins/loki-logging/index.mjs +1 -1
  42. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  43. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  44. package/dist/builtins/platform-probe/index.js +1 -1
  45. package/dist/builtins/platform-probe/index.mjs +1 -1
  46. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  47. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  48. package/dist/builtins/snapshot/index.js +1 -1
  49. package/dist/builtins/snapshot/index.mjs +1 -1
  50. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  51. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  52. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  53. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  54. package/dist/builtins/storage-orchestrator/storage-location-band-monitor.d.ts +43 -0
  55. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +6 -0
  56. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +112 -1
  57. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +112 -1
  58. package/dist/builtins/system-config/system-config.addon.js +80 -785
  59. package/dist/builtins/system-config/system-config.addon.mjs +52 -757
  60. package/dist/builtins/winston-logging/index.js +1 -1
  61. package/dist/builtins/winston-logging/index.mjs +1 -1
  62. package/dist/{child-cap-dispatch-fnYkJDjG.js → child-cap-dispatch-B87B0FrC.js} +1 -1
  63. package/dist/{child-cap-dispatch-CpYnlLNt.mjs → child-cap-dispatch-CmfT-OR2.mjs} +1 -1
  64. package/dist/{composition-sources-BX6Caql_.js → composition-sources-DBcOVpGn.js} +1 -1
  65. package/dist/{composition-sources-QndhKNb5.mjs → composition-sources-DEZqZLPT.mjs} +1 -1
  66. package/dist/{constants-vMEWqCqG.mjs → constants-BSBtmzm3.mjs} +1 -1
  67. package/dist/{constants-CMXQ1QIC.js → constants-C4jZO_qO.js} +1 -1
  68. package/dist/{dist-ItJ3qAAA.mjs → dist-DwE9yHdk.mjs} +67 -3
  69. package/dist/{dist-BcnmH70Z.js → dist-DxiDI8hZ.js} +78 -2
  70. package/dist/{event-loop-stall-monitor-DoFaUgFf.js → event-loop-stall-monitor-E2hFvhDg.js} +1 -1
  71. package/dist/{event-loop-stall-monitor-CogzcENb.mjs → event-loop-stall-monitor-LBZskC1S.mjs} +1 -1
  72. package/dist/index.js +7 -7
  73. package/dist/index.mjs +7 -7
  74. package/dist/{jwt-signing-key-CST1LdM6.js → jwt-signing-key-CsC-FxSm.js} +1 -1
  75. package/dist/{jwt-signing-key-CpMSrZuI.mjs → jwt-signing-key-DLbjTIuA.mjs} +1 -1
  76. package/dist/lib-B_R6sPC3.mjs +7106 -0
  77. package/dist/lib-DOJ5yyWA.js +7110 -0
  78. package/dist/{ms-DlIWNkkk.mjs → ms-CgH2OFlD.mjs} +2 -42
  79. package/dist/{ms-t68_FaqE.js → ms-LB3giGoU.js} +1 -48
  80. package/dist/{restore-archive-pSlTKRgU.js → restore-archive-BtKjaVua.js} +1 -1
  81. package/dist/{restore-archive-8F_M_wW0.mjs → restore-archive-CImzbuRo.mjs} +1 -1
  82. package/dist/{retired-settings-keys-D3E47aR_.js → retired-settings-keys-Cl1tnkmq.js} +1 -1
  83. package/dist/{retired-settings-keys-CbkMnFXg.mjs → retired-settings-keys-D1WsCdUY.mjs} +1 -1
  84. package/dist/safe-buffer-B1OjL1I9.mjs +42 -0
  85. package/dist/safe-buffer-D3aoa-7X.js +46 -0
  86. package/dist/src-CnAqG6EF.js +714 -0
  87. package/dist/src-hG4grMU4.mjs +709 -0
  88. package/dist/{tls-BWpwU-d5.js → tls-C0TRPfqc.js} +1 -1
  89. package/dist/{tls-C2NT-Orr.mjs → tls-D-2dPY-5.mjs} +1 -1
  90. package/dist/{x509.es-Xf3K_brW.mjs → x509.es-CAjSsrkk.mjs} +643 -2
  91. package/dist/{x509.es-BhTzcFhZ.js → x509.es-Dzl0BbmG.js} +653 -0
  92. package/package.json +2 -1
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-BcnmH70Z.js");
4
- const require_tls = require("../../tls-BWpwU-d5.js");
3
+ const require_dist = require("../../dist-DxiDI8hZ.js");
4
+ const require_tls = require("../../tls-C0TRPfqc.js");
5
5
  let zod = require("zod");
6
6
  let node_net = require("node:net");
7
7
  let node_os = require("node:os");
@@ -337,6 +337,163 @@ function parseIpv6Groups(address) {
337
337
  ];
338
338
  }
339
339
  //#endregion
340
+ //#region src/builtins/local-network/hub-mdns.ts
341
+ /**
342
+ * Hub mDNS advertisement (D838) — how `camstack login` / `camstack discover`
343
+ * find a hub on the LAN.
344
+ *
345
+ * The hub publishes ONE `_camstack._tcp` service for its HTTPS endpoint. There
346
+ * is no mesh channel in it (D826 retired mesh UDP discovery on purpose) and
347
+ * nothing secret: the TXT record carries the node id, the version, the role
348
+ * and the address URL — finding a hub grants nothing, every client still signs
349
+ * in.
350
+ *
351
+ * The advertised address is NOT ranked here. It is the `internal` role of the
352
+ * address book (the address flagged for LAN clients, else the automatic LAN
353
+ * pick) — this module only reads the answer `local-network` already gives.
354
+ *
355
+ * The responder is injected so the logic is tested with a fake; the real one
356
+ * wraps `@homebridge/ciao`.
357
+ */
358
+ /** The announcement for the current address, or null when nothing qualifies. */
359
+ function buildHubAnnouncement(inputs) {
360
+ if (inputs.internalUrl === null) return null;
361
+ let url;
362
+ try {
363
+ url = new URL(inputs.internalUrl);
364
+ } catch {
365
+ return null;
366
+ }
367
+ if (url.protocol !== "https:") return null;
368
+ const port = url.port === "" ? 443 : Number.parseInt(url.port, 10);
369
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
370
+ const host = url.hostname.replace(/^\[|\]$/g, "");
371
+ const txt = {
372
+ [require_dist.HUB_MDNS_TXT_KEYS.nodeId]: inputs.nodeId,
373
+ [require_dist.HUB_MDNS_TXT_KEYS.role]: "hub",
374
+ [require_dist.HUB_MDNS_TXT_KEYS.url]: url.origin
375
+ };
376
+ if (inputs.version !== null) txt[require_dist.HUB_MDNS_TXT_KEYS.version] = inputs.version;
377
+ return {
378
+ name: `camstack-${inputs.nodeId}`,
379
+ type: require_dist.HUB_MDNS_SERVICE_TYPE,
380
+ port,
381
+ address: (0, node_net.isIP)(host) !== 0 ? host : null,
382
+ txt
383
+ };
384
+ }
385
+ function announcementKey(a) {
386
+ return JSON.stringify([
387
+ a.name,
388
+ a.type,
389
+ a.port,
390
+ a.address,
391
+ a.txt
392
+ ]);
393
+ }
394
+ /**
395
+ * Keeps one mDNS service in step with the hub's `internal` address. `sync()`
396
+ * is safe to call on any change; it never throws — a failure to advertise is
397
+ * logged and the boot goes on.
398
+ */
399
+ var HubMdnsAdvertiser = class {
400
+ options;
401
+ current = null;
402
+ running = false;
403
+ chain = Promise.resolve();
404
+ constructor(options) {
405
+ this.options = options;
406
+ }
407
+ async start() {
408
+ this.running = true;
409
+ await this.sync();
410
+ }
411
+ /** Re-read the address and re-announce when it changed. Serialised. */
412
+ sync() {
413
+ this.chain = this.chain.then(() => this.syncOnce());
414
+ return this.chain;
415
+ }
416
+ async stop() {
417
+ this.running = false;
418
+ await (this.chain = this.chain.then(() => this.withdraw()));
419
+ }
420
+ async syncOnce() {
421
+ if (!this.running) return;
422
+ const { logger } = this.options;
423
+ try {
424
+ const internalUrl = await this.options.readInternalUrl();
425
+ const next = buildHubAnnouncement({
426
+ nodeId: this.options.nodeId,
427
+ version: this.options.version,
428
+ internalUrl
429
+ });
430
+ if (next === null) {
431
+ if (this.current !== null) {
432
+ await this.withdraw();
433
+ logger.info("hub mDNS: no internal address, advertisement withdrawn");
434
+ }
435
+ return;
436
+ }
437
+ const key = announcementKey(next);
438
+ if (this.current?.key === key) return;
439
+ await this.withdraw();
440
+ const handle = await this.options.publisher.publish(next);
441
+ this.current = {
442
+ key,
443
+ handle
444
+ };
445
+ logger.info("hub mDNS: advertising _camstack._tcp", { meta: {
446
+ url: next.txt[require_dist.HUB_MDNS_TXT_KEYS.url],
447
+ port: next.port
448
+ } });
449
+ } catch (err) {
450
+ logger.warn("hub mDNS: could not advertise the hub (logins still work with --server)", { meta: { error: err instanceof Error ? err.message : String(err) } });
451
+ }
452
+ }
453
+ async withdraw() {
454
+ const held = this.current;
455
+ this.current = null;
456
+ if (held === null) return;
457
+ try {
458
+ await held.handle.stop();
459
+ } catch (err) {
460
+ this.options.logger.warn("hub mDNS: withdraw failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
461
+ }
462
+ }
463
+ };
464
+ /**
465
+ * The address a hub advertises: the `internal` role's answer, exactly as the
466
+ * address book resolved it. Never re-ranked here (D729, D732).
467
+ */
468
+ function internalUrlOf(book) {
469
+ return book.roles.internal.resolved;
470
+ }
471
+ //#endregion
472
+ //#region src/builtins/local-network/hub-mdns-ciao.ts
473
+ function createCiaoPublisher() {
474
+ return { async publish(announcement) {
475
+ const { getResponder } = await Promise.resolve().then(() => /* @__PURE__ */ require_chunk.__toESM(require("../../lib-DOJ5yyWA.js").default));
476
+ const responder = getResponder();
477
+ const service = responder.createService({
478
+ name: announcement.name,
479
+ type: announcement.type,
480
+ port: announcement.port,
481
+ txt: { ...announcement.txt },
482
+ ...announcement.address !== null ? { restrictedAddresses: [announcement.address] } : {}
483
+ });
484
+ try {
485
+ await service.advertise();
486
+ } catch (err) {
487
+ await responder.shutdown().catch(() => void 0);
488
+ throw err;
489
+ }
490
+ return { stop: async () => {
491
+ await service.destroy();
492
+ await responder.shutdown();
493
+ } };
494
+ } };
495
+ }
496
+ //#endregion
340
497
  //#region src/builtins/local-network/local-hostname.ts
341
498
  var DNS_NAME = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])$/;
342
499
  /**
@@ -419,6 +576,7 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
419
576
  * `null` when no mesh provider is joined.
420
577
  */
421
578
  meshEndpoint = null;
579
+ mdns = null;
422
580
  constructor() {
423
581
  super({
424
582
  allowedAddresses: [],
@@ -430,7 +588,8 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
430
588
  httpPort: require_tls.DEFAULT_LAN_HTTP_PORT,
431
589
  localHostname: "",
432
590
  internalUrl: "",
433
- publicEndpointUrl: ""
591
+ publicEndpointUrl: "",
592
+ mdnsAdvertise: true
434
593
  });
435
594
  }
436
595
  /**
@@ -481,6 +640,7 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
481
640
  customAddresses: changed.custom
482
641
  }, detectedUrls);
483
642
  await this.updateGlobalSettings(addressBookPatch(after));
643
+ await this.mdns?.sync();
484
644
  }
485
645
  /** The deprecated `getHubAddresses` shape, from the role resolvers. */
486
646
  async snapshotHubAddresses() {
@@ -716,6 +876,7 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
716
876
  } catch (err) {
717
877
  this.ctx.logger.warn("local-network: LAN HTTP not applied at boot", { meta: { error: err instanceof Error ? err.message : String(err) } });
718
878
  }
879
+ await this.applyMdnsAdvertisement();
719
880
  return [{
720
881
  capability: require_dist.localNetworkCapability,
721
882
  provider
@@ -749,6 +910,14 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
749
910
  equals: true
750
911
  }
751
912
  }),
913
+ this.field({
914
+ type: "boolean",
915
+ key: "mdnsAdvertise",
916
+ label: "Announce this hub on the LAN (mDNS)",
917
+ description: "Lets `camstack login` find the hub without an address. Advertises only the HTTPS address, never a secret. Needs UDP 5353 multicast to reach the LAN (host networking or a dedicated container IP).",
918
+ style: "switch",
919
+ default: true
920
+ }),
752
921
  this.field({
753
922
  type: "text",
754
923
  key: "localHostname",
@@ -762,8 +931,29 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
762
931
  }
763
932
  async onConfigChanged() {
764
933
  await this.applyLanAccessFromSettings();
934
+ await this.applyMdnsAdvertisement();
935
+ }
936
+ /**
937
+ * Start, re-announce or stop the hub's mDNS service (D838). Never throws:
938
+ * the advertiser logs its own failures and boot goes on.
939
+ */
940
+ async applyMdnsAdvertisement() {
941
+ if (this.config.mdnsAdvertise === false) {
942
+ await this.mdns?.stop();
943
+ return;
944
+ }
945
+ if (this.mdns === null) this.mdns = new HubMdnsAdvertiser({
946
+ publisher: createCiaoPublisher(),
947
+ logger: this.ctx.logger,
948
+ nodeId: this.ctx.kernel.localNodeId ?? "hub",
949
+ version: null,
950
+ readInternalUrl: async () => internalUrlOf(await this.addressBook("https"))
951
+ });
952
+ await this.mdns.start();
765
953
  }
766
954
  async onShutdown() {
955
+ await this.mdns?.stop();
956
+ this.mdns = null;
767
957
  if (this.pollTimer) {
768
958
  clearInterval(this.pollTimer);
769
959
  this.pollTimer = null;
@@ -959,6 +1149,7 @@ var LocalNetworkAddon = class extends require_dist.BaseAddon {
959
1149
  } });
960
1150
  this.lastSnapshotKey = key;
961
1151
  this.persistReconciledAllowlist(interfaces);
1152
+ this.mdns?.sync();
962
1153
  this.ctx.eventBus?.emit({
963
1154
  id: `local-network-changed-${Date.now()}`,
964
1155
  timestamp: /* @__PURE__ */ new Date(),
@@ -1,5 +1,6 @@
1
- import { Bt as isDefaultAppConnectionEndpoint, Dn as resolveNotificationTapAddress, Lt as isAppLinkBase, Wn as validateCustomAddress, Xt as localNetworkCapability, bn as resolveAppConnections, dr as BaseAddon, pn as pickAppLinkHub, ri as EventCategory, rt as checkRoleAddress, s as CAMSTACK_APP_LINK_ORIGIN, wn as resolveHubAddressRoles } from "../../dist-ItJ3qAAA.mjs";
2
- import { P as readLanHttpState, _ as writeExtraSans, h as readTlsMode, i as storeUploadedCertificate, j as applyLanHttp, k as DEFAULT_LAN_HTTP_PORT, m as readTlsAccessStatus, n as regenerateCertificate, p as readExtraSans, r as revertToGeneratedCertificate, t as applyCertificateToListener, u as servedHttpsLeafFingerprint, y as ensureTlsCert } from "../../tls-C2NT-Orr.mjs";
1
+ import { o as __toESM } from "../../chunk-CNf5ZN-e.mjs";
2
+ import { D as HUB_MDNS_TXT_KEYS, E as HUB_MDNS_SERVICE_TYPE, En as resolveHubAddressRoles, Ht as isDefaultAppConnectionEndpoint, Kn as validateCustomAddress, Qt as localNetworkCapability, Sn as resolveAppConnections, ai as EventCategory, at as checkRoleAddress, hn as pickAppLinkHub, kn as resolveNotificationTapAddress, pr as BaseAddon, s as CAMSTACK_APP_LINK_ORIGIN, zt as isAppLinkBase } from "../../dist-DwE9yHdk.mjs";
3
+ import { P as readLanHttpState, _ as writeExtraSans, h as readTlsMode, i as storeUploadedCertificate, j as applyLanHttp, k as DEFAULT_LAN_HTTP_PORT, m as readTlsAccessStatus, n as regenerateCertificate, p as readExtraSans, r as revertToGeneratedCertificate, t as applyCertificateToListener, u as servedHttpsLeafFingerprint, y as ensureTlsCert } from "../../tls-D-2dPY-5.mjs";
3
4
  import { z } from "zod";
4
5
  import { isIP } from "node:net";
5
6
  import * as os from "node:os";
@@ -334,6 +335,163 @@ function parseIpv6Groups(address) {
334
335
  ];
335
336
  }
336
337
  //#endregion
338
+ //#region src/builtins/local-network/hub-mdns.ts
339
+ /**
340
+ * Hub mDNS advertisement (D838) — how `camstack login` / `camstack discover`
341
+ * find a hub on the LAN.
342
+ *
343
+ * The hub publishes ONE `_camstack._tcp` service for its HTTPS endpoint. There
344
+ * is no mesh channel in it (D826 retired mesh UDP discovery on purpose) and
345
+ * nothing secret: the TXT record carries the node id, the version, the role
346
+ * and the address URL — finding a hub grants nothing, every client still signs
347
+ * in.
348
+ *
349
+ * The advertised address is NOT ranked here. It is the `internal` role of the
350
+ * address book (the address flagged for LAN clients, else the automatic LAN
351
+ * pick) — this module only reads the answer `local-network` already gives.
352
+ *
353
+ * The responder is injected so the logic is tested with a fake; the real one
354
+ * wraps `@homebridge/ciao`.
355
+ */
356
+ /** The announcement for the current address, or null when nothing qualifies. */
357
+ function buildHubAnnouncement(inputs) {
358
+ if (inputs.internalUrl === null) return null;
359
+ let url;
360
+ try {
361
+ url = new URL(inputs.internalUrl);
362
+ } catch {
363
+ return null;
364
+ }
365
+ if (url.protocol !== "https:") return null;
366
+ const port = url.port === "" ? 443 : Number.parseInt(url.port, 10);
367
+ if (!Number.isInteger(port) || port < 1 || port > 65535) return null;
368
+ const host = url.hostname.replace(/^\[|\]$/g, "");
369
+ const txt = {
370
+ [HUB_MDNS_TXT_KEYS.nodeId]: inputs.nodeId,
371
+ [HUB_MDNS_TXT_KEYS.role]: "hub",
372
+ [HUB_MDNS_TXT_KEYS.url]: url.origin
373
+ };
374
+ if (inputs.version !== null) txt[HUB_MDNS_TXT_KEYS.version] = inputs.version;
375
+ return {
376
+ name: `camstack-${inputs.nodeId}`,
377
+ type: HUB_MDNS_SERVICE_TYPE,
378
+ port,
379
+ address: isIP(host) !== 0 ? host : null,
380
+ txt
381
+ };
382
+ }
383
+ function announcementKey(a) {
384
+ return JSON.stringify([
385
+ a.name,
386
+ a.type,
387
+ a.port,
388
+ a.address,
389
+ a.txt
390
+ ]);
391
+ }
392
+ /**
393
+ * Keeps one mDNS service in step with the hub's `internal` address. `sync()`
394
+ * is safe to call on any change; it never throws — a failure to advertise is
395
+ * logged and the boot goes on.
396
+ */
397
+ var HubMdnsAdvertiser = class {
398
+ options;
399
+ current = null;
400
+ running = false;
401
+ chain = Promise.resolve();
402
+ constructor(options) {
403
+ this.options = options;
404
+ }
405
+ async start() {
406
+ this.running = true;
407
+ await this.sync();
408
+ }
409
+ /** Re-read the address and re-announce when it changed. Serialised. */
410
+ sync() {
411
+ this.chain = this.chain.then(() => this.syncOnce());
412
+ return this.chain;
413
+ }
414
+ async stop() {
415
+ this.running = false;
416
+ await (this.chain = this.chain.then(() => this.withdraw()));
417
+ }
418
+ async syncOnce() {
419
+ if (!this.running) return;
420
+ const { logger } = this.options;
421
+ try {
422
+ const internalUrl = await this.options.readInternalUrl();
423
+ const next = buildHubAnnouncement({
424
+ nodeId: this.options.nodeId,
425
+ version: this.options.version,
426
+ internalUrl
427
+ });
428
+ if (next === null) {
429
+ if (this.current !== null) {
430
+ await this.withdraw();
431
+ logger.info("hub mDNS: no internal address, advertisement withdrawn");
432
+ }
433
+ return;
434
+ }
435
+ const key = announcementKey(next);
436
+ if (this.current?.key === key) return;
437
+ await this.withdraw();
438
+ const handle = await this.options.publisher.publish(next);
439
+ this.current = {
440
+ key,
441
+ handle
442
+ };
443
+ logger.info("hub mDNS: advertising _camstack._tcp", { meta: {
444
+ url: next.txt[HUB_MDNS_TXT_KEYS.url],
445
+ port: next.port
446
+ } });
447
+ } catch (err) {
448
+ logger.warn("hub mDNS: could not advertise the hub (logins still work with --server)", { meta: { error: err instanceof Error ? err.message : String(err) } });
449
+ }
450
+ }
451
+ async withdraw() {
452
+ const held = this.current;
453
+ this.current = null;
454
+ if (held === null) return;
455
+ try {
456
+ await held.handle.stop();
457
+ } catch (err) {
458
+ this.options.logger.warn("hub mDNS: withdraw failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
459
+ }
460
+ }
461
+ };
462
+ /**
463
+ * The address a hub advertises: the `internal` role's answer, exactly as the
464
+ * address book resolved it. Never re-ranked here (D729, D732).
465
+ */
466
+ function internalUrlOf(book) {
467
+ return book.roles.internal.resolved;
468
+ }
469
+ //#endregion
470
+ //#region src/builtins/local-network/hub-mdns-ciao.ts
471
+ function createCiaoPublisher() {
472
+ return { async publish(announcement) {
473
+ const { getResponder } = await import("../../lib-B_R6sPC3.mjs").then((m) => /* @__PURE__ */ __toESM(m.default));
474
+ const responder = getResponder();
475
+ const service = responder.createService({
476
+ name: announcement.name,
477
+ type: announcement.type,
478
+ port: announcement.port,
479
+ txt: { ...announcement.txt },
480
+ ...announcement.address !== null ? { restrictedAddresses: [announcement.address] } : {}
481
+ });
482
+ try {
483
+ await service.advertise();
484
+ } catch (err) {
485
+ await responder.shutdown().catch(() => void 0);
486
+ throw err;
487
+ }
488
+ return { stop: async () => {
489
+ await service.destroy();
490
+ await responder.shutdown();
491
+ } };
492
+ } };
493
+ }
494
+ //#endregion
337
495
  //#region src/builtins/local-network/local-hostname.ts
338
496
  var DNS_NAME = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])$/;
339
497
  /**
@@ -416,6 +574,7 @@ var LocalNetworkAddon = class extends BaseAddon {
416
574
  * `null` when no mesh provider is joined.
417
575
  */
418
576
  meshEndpoint = null;
577
+ mdns = null;
419
578
  constructor() {
420
579
  super({
421
580
  allowedAddresses: [],
@@ -427,7 +586,8 @@ var LocalNetworkAddon = class extends BaseAddon {
427
586
  httpPort: DEFAULT_LAN_HTTP_PORT,
428
587
  localHostname: "",
429
588
  internalUrl: "",
430
- publicEndpointUrl: ""
589
+ publicEndpointUrl: "",
590
+ mdnsAdvertise: true
431
591
  });
432
592
  }
433
593
  /**
@@ -478,6 +638,7 @@ var LocalNetworkAddon = class extends BaseAddon {
478
638
  customAddresses: changed.custom
479
639
  }, detectedUrls);
480
640
  await this.updateGlobalSettings(addressBookPatch(after));
641
+ await this.mdns?.sync();
481
642
  }
482
643
  /** The deprecated `getHubAddresses` shape, from the role resolvers. */
483
644
  async snapshotHubAddresses() {
@@ -713,6 +874,7 @@ var LocalNetworkAddon = class extends BaseAddon {
713
874
  } catch (err) {
714
875
  this.ctx.logger.warn("local-network: LAN HTTP not applied at boot", { meta: { error: err instanceof Error ? err.message : String(err) } });
715
876
  }
877
+ await this.applyMdnsAdvertisement();
716
878
  return [{
717
879
  capability: localNetworkCapability,
718
880
  provider
@@ -746,6 +908,14 @@ var LocalNetworkAddon = class extends BaseAddon {
746
908
  equals: true
747
909
  }
748
910
  }),
911
+ this.field({
912
+ type: "boolean",
913
+ key: "mdnsAdvertise",
914
+ label: "Announce this hub on the LAN (mDNS)",
915
+ description: "Lets `camstack login` find the hub without an address. Advertises only the HTTPS address, never a secret. Needs UDP 5353 multicast to reach the LAN (host networking or a dedicated container IP).",
916
+ style: "switch",
917
+ default: true
918
+ }),
749
919
  this.field({
750
920
  type: "text",
751
921
  key: "localHostname",
@@ -759,8 +929,29 @@ var LocalNetworkAddon = class extends BaseAddon {
759
929
  }
760
930
  async onConfigChanged() {
761
931
  await this.applyLanAccessFromSettings();
932
+ await this.applyMdnsAdvertisement();
933
+ }
934
+ /**
935
+ * Start, re-announce or stop the hub's mDNS service (D838). Never throws:
936
+ * the advertiser logs its own failures and boot goes on.
937
+ */
938
+ async applyMdnsAdvertisement() {
939
+ if (this.config.mdnsAdvertise === false) {
940
+ await this.mdns?.stop();
941
+ return;
942
+ }
943
+ if (this.mdns === null) this.mdns = new HubMdnsAdvertiser({
944
+ publisher: createCiaoPublisher(),
945
+ logger: this.ctx.logger,
946
+ nodeId: this.ctx.kernel.localNodeId ?? "hub",
947
+ version: null,
948
+ readInternalUrl: async () => internalUrlOf(await this.addressBook("https"))
949
+ });
950
+ await this.mdns.start();
762
951
  }
763
952
  async onShutdown() {
953
+ await this.mdns?.stop();
954
+ this.mdns = null;
764
955
  if (this.pollTimer) {
765
956
  clearInterval(this.pollTimer);
766
957
  this.pollTimer = null;
@@ -956,6 +1147,7 @@ var LocalNetworkAddon = class extends BaseAddon {
956
1147
  } });
957
1148
  this.lastSnapshotKey = key;
958
1149
  this.persistReconciledAllowlist(interfaces);
1150
+ this.mdns?.sync();
959
1151
  this.ctx.eventBus?.emit({
960
1152
  id: `local-network-changed-${Date.now()}`,
961
1153
  timestamp: /* @__PURE__ */ new Date(),
@@ -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-BcnmH70Z.js");
6
+ const require_dist = require("../../dist-DxiDI8hZ.js");
7
7
  //#region src/builtins/loki-logging/loki-payload.ts
8
8
  /**
9
9
  * Loki rejects a label name that is not a valid Prometheus label
@@ -1,4 +1,4 @@
1
- import { Qt as logDestinationCapability, dr as BaseAddon } from "../../dist-ItJ3qAAA.mjs";
1
+ import { en as logDestinationCapability, pr as BaseAddon } from "../../dist-DwE9yHdk.mjs";
2
2
  //#region src/builtins/loki-logging/loki-payload.ts
3
3
  /**
4
4
  * Loki rejects a label name that is not a valid Prometheus label
@@ -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-BcnmH70Z.js");
6
+ const require_dist = require("../../dist-DxiDI8hZ.js");
7
7
  const require_proc_stat = require("../../proc-stat-DL-WUOhl.js");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  let node_os = require("node:os");
@@ -1,4 +1,4 @@
1
- import { Er as createEvent, Nt as foldSnapshotByFunction, an as metricsProviderCapability, dr as BaseAddon, ri as EventCategory, ur as errMsg, vn as reducePoints, xn as resolveBucketMs } from "../../dist-ItJ3qAAA.mjs";
1
+ import { Cn as resolveBucketMs, Ft as foldSnapshotByFunction, Or as createEvent, ai as EventCategory, bn as reducePoints, fr as errMsg, pr as BaseAddon, sn as metricsProviderCapability } from "../../dist-DwE9yHdk.mjs";
2
2
  import { a as parseProcStat, i as nodeProcReader, n as elapsedSecSince, o as parseProcStatusRss, r as lifetimeCpuPercent, t as commandFromCmdline } from "../../proc-stat-CbC55Kqi.mjs";
3
3
  import { access, readFile, readdir } from "node:fs/promises";
4
4
  import * as os from "node:os";
@@ -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-BcnmH70Z.js");
6
+ const require_dist = require("../../dist-DxiDI8hZ.js");
7
7
  let node_os = require("node:os");
8
8
  node_os = require_chunk.__toESM(node_os);
9
9
  let node_fs = require("node:fs");
@@ -1,5 +1,5 @@
1
1
  import { o as __toESM } from "../../chunk-CNf5ZN-e.mjs";
2
- import { Ar as emitReadiness, dr as BaseAddon, hn as platformProbeCapability, jn as scoreRuntimes, ri as EventCategory, ur as errMsg, wt as enumerateInferenceDevices } from "../../dist-ItJ3qAAA.mjs";
2
+ import { Et as enumerateInferenceDevices, Mr as emitReadiness, Nn as scoreRuntimes, _n as platformProbeCapability, ai as EventCategory, fr as errMsg, pr as BaseAddon } from "../../dist-DwE9yHdk.mjs";
3
3
  import * as os from "node:os";
4
4
  import * as fs from "node:fs";
5
5
  import { execFile } from "node:child_process";
@@ -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-BcnmH70Z.js");
6
+ const require_dist = require("../../dist-DxiDI8hZ.js");
7
7
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
8
8
  /**
9
9
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -1,4 +1,4 @@
1
- import { dr as BaseAddon, ri as EventCategory } from "../../dist-ItJ3qAAA.mjs";
1
+ import { ai as EventCategory, pr as BaseAddon } from "../../dist-DwE9yHdk.mjs";
2
2
  //#region src/builtins/remote-access-orchestrator/enabled-providers-reconcile.ts
3
3
  /**
4
4
  * Reconcile the durable `enabledProviders` set against authoritative
@@ -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-BcnmH70Z.js");
6
+ const require_dist = require("../../dist-DxiDI8hZ.js");
7
7
  const require_snapshot_courtesy = require("../../snapshot-courtesy-D_C5-nC0.js");
8
8
  let zod = require("zod");
9
9
  let node_crypto = require("node:crypto");
@@ -1,4 +1,4 @@
1
- import { Bn as streamQualityLabel, Pn as snapshotCapability, Q as batteryCapability, Vr as nodePin, Z as bareAddonId, a as BatteryStatusSchema, dr as BaseAddon, in as memoizePinnedFfmpeg, lr as DeviceType, ri as EventCategory, sr as DeviceFeature, ur as errMsg, vt as deriveBatteryPresence } from "../../dist-ItJ3qAAA.mjs";
1
+ import { $ as bareAddonId, Hn as streamQualityLabel, In as snapshotCapability, Ur as nodePin, a as BatteryStatusSchema, ai as EventCategory, bt as deriveBatteryPresence, dr as DeviceType, et as batteryCapability, fr as errMsg, lr as DeviceFeature, on as memoizePinnedFfmpeg, pr as BaseAddon } from "../../dist-DwE9yHdk.mjs";
2
2
  import { i as renderCourtesyOverlayJpeg, n as courtesyCacheKey, r as renderCourtesyJpeg } from "../../snapshot-courtesy-CyFs1M3I.mjs";
3
3
  import { z } from "zod";
4
4
  import { randomUUID } from "node:crypto";
@@ -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-BcnmH70Z.js");
6
+ const require_dist = require("../../dist-DxiDI8hZ.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs_promises = require("node:fs/promises");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Mt as filesystemBrowseCapability, Rn as storageProviderCapability, dr as BaseAddon } from "../../dist-ItJ3qAAA.mjs";
1
+ import { Bn as storageProviderCapability, Pt as filesystemBrowseCapability, pr as BaseAddon } from "../../dist-DwE9yHdk.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { mkdir, readdir, realpath, statfs } from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -0,0 +1,43 @@
1
+ /**
2
+ * StorageLocationBandMonitor — free-space bands for Notification Center.
3
+ *
4
+ * Capacity that failed to measure is `null` and becomes CRITICAL (D393) —
5
+ * never folded into 0% or 100%. Disabled / non-managed locations are absent
6
+ * from the sweep input and clear any open episode without an `ok` event.
7
+ *
8
+ * Pure, injected clock, one transition per band change.
9
+ */
10
+ export type StorageLocationBand = "ok" | "low" | "critical";
11
+ export interface StorageLocationBandConfig {
12
+ /** Free percent at/below which the location is low (default 15). */
13
+ readonly lowPercent: number;
14
+ /** Free percent at/below which the location is critical (default 5). */
15
+ readonly criticalPercent: number;
16
+ }
17
+ export declare const DEFAULT_STORAGE_LOCATION_BAND_CONFIG: StorageLocationBandConfig;
18
+ export interface StorageLocationBandSample {
19
+ readonly locationId: string;
20
+ readonly nodeId?: string;
21
+ readonly locationName?: string;
22
+ /** Free percent, or `null` when the probe failed. */
23
+ readonly freePercent: number | null;
24
+ }
25
+ export interface StorageLocationBandEvent {
26
+ readonly locationId: string;
27
+ readonly nodeId?: string;
28
+ readonly locationName?: string;
29
+ readonly band: StorageLocationBand;
30
+ readonly freePercent: number | null;
31
+ readonly reason: string;
32
+ readonly previous: StorageLocationBand | null;
33
+ }
34
+ export declare class StorageLocationBandMonitor {
35
+ private readonly config;
36
+ private readonly band;
37
+ constructor(config?: StorageLocationBandConfig);
38
+ /**
39
+ * Observe one sweep of managed locations. Locations missing from `samples`
40
+ * drop their episode without emitting `ok` (disabled / removed).
41
+ */
42
+ observe(samples: readonly StorageLocationBandSample[]): readonly StorageLocationBandEvent[];
43
+ }