@camstack/system 1.2.415 → 1.2.417

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 (103) hide show
  1. package/dist/addon-runner.js +9969 -27
  2. package/dist/addon-runner.mjs +9930 -9
  3. package/dist/addon-utils.js +19 -20
  4. package/dist/addon-utils.mjs +1 -2
  5. package/dist/auth/auth-audit-log.d.ts +1 -1
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  9. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  10. package/dist/builtins/alerts/alerts.addon.js +1 -1
  11. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  12. package/dist/builtins/autotrack/index.js +1 -1
  13. package/dist/builtins/autotrack/index.mjs +1 -1
  14. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  15. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  16. package/dist/builtins/calendars/calendars.addon.js +1 -1
  17. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  18. package/dist/builtins/camera-grid/index.js +1 -1
  19. package/dist/builtins/camera-grid/index.mjs +1 -1
  20. package/dist/builtins/composer/composer.addon.js +2 -2
  21. package/dist/builtins/composer/composer.addon.mjs +2 -2
  22. package/dist/builtins/console-logging/index.js +1 -1
  23. package/dist/builtins/console-logging/index.mjs +1 -1
  24. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  25. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  26. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  27. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  28. package/dist/builtins/hub-forwarder/index.js +1 -1
  29. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  30. package/dist/builtins/keychain/index.js +3 -3
  31. package/dist/builtins/keychain/index.mjs +3 -3
  32. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  33. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  34. package/dist/builtins/local-auth/local-auth.addon.js +12 -9
  35. package/dist/builtins/local-auth/local-auth.addon.mjs +7 -4
  36. package/dist/builtins/local-network/hub-mdns-ciao.d.ts +7 -0
  37. package/dist/builtins/local-network/hub-mdns.d.ts +58 -0
  38. package/dist/builtins/local-network/local-network.addon.d.ts +11 -0
  39. package/dist/builtins/local-network/local-network.addon.js +194 -3
  40. package/dist/builtins/local-network/local-network.addon.mjs +195 -3
  41. package/dist/builtins/loki-logging/index.js +1 -1
  42. package/dist/builtins/loki-logging/index.mjs +1 -1
  43. package/dist/builtins/native-metrics/native-metrics.addon.js +102 -10
  44. package/dist/builtins/native-metrics/native-metrics.addon.mjs +92 -4
  45. package/dist/builtins/platform-probe/index.js +1 -1
  46. package/dist/builtins/platform-probe/index.mjs +1 -1
  47. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  48. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  49. package/dist/builtins/snapshot/index.js +1 -1
  50. package/dist/builtins/snapshot/index.mjs +1 -1
  51. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  52. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  53. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  54. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  55. package/dist/builtins/storage-orchestrator/index.mjs +1 -1
  56. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +84 -5
  57. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +80 -5
  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/{composition-sources-YlkCjf-1.js → composition-sources-ChAX2Kdo.js} +1 -1
  63. package/dist/{composition-sources-CXxkMoXq.mjs → composition-sources-CndJVssc.mjs} +1 -1
  64. package/dist/{constants-BT-fudVK.mjs → constants-BzxwnfDc.mjs} +1 -1
  65. package/dist/{constants-DChnQrhM.js → constants-VXvZkGgw.js} +1 -1
  66. package/dist/{event-loop-stall-monitor-LZvGqiHI.js → custom-action-registry-Bb28ibZV.js} +63 -1
  67. package/dist/{event-loop-stall-monitor-BEfMVUxz.mjs → custom-action-registry-DHv0kYcT.mjs} +58 -2
  68. package/dist/{dist-CvarXqQt.js → dist-BMOD9OyH.js} +32 -111
  69. package/dist/{dist-dEsHYY8m.mjs → dist-H_RapIeM.mjs} +21 -100
  70. package/dist/index.js +22786 -10067
  71. package/dist/index.mjs +22602 -9883
  72. package/dist/{jwt-signing-key-CfiteETM.js → jwt-signing-key-BiKHENST.js} +1 -1
  73. package/dist/{jwt-signing-key-SFIxaH2x.mjs → jwt-signing-key-xQxqpwMC.mjs} +1 -1
  74. package/dist/kernel/moleculer/broker-factory.d.ts +2 -4
  75. package/dist/kernel/transport/local-child-registry.d.ts +1 -1
  76. package/dist/lib-B_R6sPC3.mjs +7106 -0
  77. package/dist/lib-DOJ5yyWA.js +7110 -0
  78. package/dist/logging/log-redactor.d.ts +1 -1
  79. package/dist/{ms-DlIWNkkk.mjs → ms-CgH2OFlD.mjs} +2 -42
  80. package/dist/{ms-t68_FaqE.js → ms-LB3giGoU.js} +1 -48
  81. package/dist/{resource-monitor-B2WZ8_q9.mjs → resource-monitor-Di9daRwI.mjs} +1 -1
  82. package/dist/{resource-monitor-BgWGRxN5.js → resource-monitor-mio9dSCT.js} +5 -5
  83. package/dist/{restore-archive-CAw_3iNq.js → restore-archive-BobsETEp.js} +1 -1
  84. package/dist/{restore-archive-Bww1Fl04.mjs → restore-archive-CCAQicV-.mjs} +1 -1
  85. package/dist/{retired-settings-keys-YLflwTPH.js → retired-settings-keys-BMq2u4Wv.js} +1 -1
  86. package/dist/{retired-settings-keys-ChQ3qH-V.mjs → retired-settings-keys-CwL4zHLg.mjs} +1 -1
  87. package/dist/safe-buffer-B1OjL1I9.mjs +42 -0
  88. package/dist/safe-buffer-D3aoa-7X.js +46 -0
  89. package/dist/src-CnAqG6EF.js +714 -0
  90. package/dist/src-hG4grMU4.mjs +709 -0
  91. package/dist/{tls-BWpwU-d5.js → tls-C0TRPfqc.js} +1 -1
  92. package/dist/{tls-C2NT-Orr.mjs → tls-D-2dPY-5.mjs} +1 -1
  93. package/dist/{x509.es-Xf3K_brW.mjs → x509.es-CAjSsrkk.mjs} +643 -2
  94. package/dist/{x509.es-BhTzcFhZ.js → x509.es-Dzl0BbmG.js} +653 -0
  95. package/package.json +3 -2
  96. package/dist/child-cap-dispatch-DFBrdGH2.js +0 -13986
  97. package/dist/child-cap-dispatch-SF8oiwgo.mjs +0 -12916
  98. package/dist/custom-action-registry-B4Cf4O_X.mjs +0 -57
  99. package/dist/custom-action-registry-acBY3mHA.js +0 -62
  100. package/dist/hub-cap-forward-CDiN1p98.mjs +0 -78
  101. package/dist/hub-cap-forward-W8oEm4ME.js +0 -101
  102. package/dist/proc-stat-CbC55Kqi.mjs +0 -92
  103. package/dist/proc-stat-DL-WUOhl.js +0 -127
@@ -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-CvarXqQt.js");
4
- const require_tls = require("../../tls-BWpwU-d5.js");
3
+ const require_dist = require("../../dist-BMOD9OyH.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-dEsHYY8m.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, Gn as validateCustomAddress, Ht as isDefaultAppConnectionEndpoint, On as resolveNotificationTapAddress, Qt as localNetworkCapability, Tn as resolveHubAddressRoles, at as checkRoleAddress, fr as BaseAddon, mn as pickAppLinkHub, ri as EventCategory, s as CAMSTACK_APP_LINK_ORIGIN, xn as resolveAppConnections, zt as isAppLinkBase } from "../../dist-H_RapIeM.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-CvarXqQt.js");
6
+ const require_dist = require("../../dist-BMOD9OyH.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-dEsHYY8m.mjs";
1
+ import { en as logDestinationCapability, fr as BaseAddon } from "../../dist-H_RapIeM.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,8 +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-CvarXqQt.js");
7
- const require_proc_stat = require("../../proc-stat-DL-WUOhl.js");
6
+ const require_dist = require("../../dist-BMOD9OyH.js");
8
7
  let node_fs_promises = require("node:fs/promises");
9
8
  let node_os = require("node:os");
10
9
  node_os = require_chunk.__toESM(node_os);
@@ -2126,7 +2125,7 @@ var NativeMetricsProvider = class {
2126
2125
  return getCpuTemperatureInternal();
2127
2126
  }
2128
2127
  async getProcessStats(input) {
2129
- const { getPidStats } = await Promise.resolve().then(() => require("../../resource-monitor-BgWGRxN5.js")).then((n) => n.resource_monitor_exports);
2128
+ const { getPidStats } = await Promise.resolve().then(() => require("../../resource-monitor-mio9dSCT.js")).then((n) => n.resource_monitor_exports);
2130
2129
  const raw = await getPidStats(input.pids);
2131
2130
  const result = [];
2132
2131
  for (const [pid, s] of raw) result.push({
@@ -2462,6 +2461,95 @@ function execAsync(cmd, args, timeoutMs = 5e3) {
2462
2461
  });
2463
2462
  });
2464
2463
  }
2464
+ /**
2465
+ * Parse one `/proc/<pid>/stat` line. `null` for a line that does not parse —
2466
+ * a process that exited between the readdir and the read leaves a truncated
2467
+ * or empty file, and that is normal, not an error.
2468
+ */
2469
+ function parseProcStat(line) {
2470
+ const close = line.lastIndexOf(")");
2471
+ const open = line.indexOf("(");
2472
+ if (close < 0 || open < 0 || close < open) return null;
2473
+ const pid = Number(line.slice(0, open).trim());
2474
+ const comm = line.slice(open + 1, close);
2475
+ const rest = line.slice(close + 1).trim().split(/\s+/);
2476
+ const state = rest[0];
2477
+ const ppid = Number(rest[1]);
2478
+ const pgid = Number(rest[2]);
2479
+ const utime = Number(rest[11]);
2480
+ const stime = Number(rest[12]);
2481
+ const startTicks = Number(rest[19]);
2482
+ const rssPages = Number(rest[21]);
2483
+ if (state === void 0 || state === "") return null;
2484
+ if (![
2485
+ pid,
2486
+ ppid,
2487
+ pgid,
2488
+ utime,
2489
+ stime,
2490
+ startTicks,
2491
+ rssPages
2492
+ ].every(Number.isFinite)) return null;
2493
+ return {
2494
+ pid,
2495
+ comm,
2496
+ state,
2497
+ ppid,
2498
+ pgid,
2499
+ ticks: utime + stime,
2500
+ startTicks,
2501
+ rssPages
2502
+ };
2503
+ }
2504
+ function statusKb(status, key) {
2505
+ const match = status.match(new RegExp(`^${key}:\\s+(\\d+)`, "m"));
2506
+ return match ? parseInt(match[1], 10) * 1024 : void 0;
2507
+ }
2508
+ /** Read `VmRSS` / `RssAnon` / `RssFile` (kB in the file) out of `/proc/<pid>/status`. */
2509
+ function parseProcStatusRss(status) {
2510
+ return {
2511
+ rssBytes: statusKb(status, "VmRSS"),
2512
+ privateBytes: statusKb(status, "RssAnon"),
2513
+ sharedBytes: statusKb(status, "RssFile")
2514
+ };
2515
+ }
2516
+ /** Seconds the process has been alive — `ps etime`, whole seconds. */
2517
+ function elapsedSecSince(input) {
2518
+ return Math.max(0, Math.floor(input.uptimeSec - input.startTicks / 100));
2519
+ }
2520
+ /**
2521
+ * `ps pcpu`: CPU time over elapsed time, as a percentage with one decimal.
2522
+ *
2523
+ * This is deliberately the LIFETIME average `ps` prints, not an instantaneous
2524
+ * rate: the columns fed from it have always shown that quantity, and the
2525
+ * instantaneous split is a different number with a different owner
2526
+ * (`cpuMainPercent` / `cpuGcPercent` from `thread-cpu-sampler.ts`). A process
2527
+ * with no elapsed window yet reports 0, never a division by zero.
2528
+ */
2529
+ function lifetimeCpuPercent(input) {
2530
+ const elapsedSec = input.uptimeSec - input.startTicks / 100;
2531
+ if (!(elapsedSec > 0)) return 0;
2532
+ const cpuSec = input.ticks / 100;
2533
+ return Math.round(cpuSec / elapsedSec * 1e3) / 10;
2534
+ }
2535
+ var PID_RE = /^\d+$/;
2536
+ var nodeProcReader = {
2537
+ listPids: async () => (await (0, node_fs_promises.readdir)("/proc")).filter((entry) => PID_RE.test(entry)).map((entry) => Number(entry)),
2538
+ readStat: (pid) => (0, node_fs_promises.readFile)(`/proc/${pid}/stat`, "latin1"),
2539
+ readStatus: (pid) => (0, node_fs_promises.readFile)(`/proc/${pid}/status`, "latin1"),
2540
+ readCmdline: (pid) => (0, node_fs_promises.readFile)(`/proc/${pid}/cmdline`, "latin1"),
2541
+ readUptimeSec: async () => {
2542
+ const [first] = (await (0, node_fs_promises.readFile)("/proc/uptime", "latin1")).trim().split(/\s+/);
2543
+ const seconds = Number(first);
2544
+ if (!Number.isFinite(seconds)) throw new Error(`/proc/uptime unreadable: ${String(first)}`);
2545
+ return seconds;
2546
+ }
2547
+ };
2548
+ /** `ps command=`: argv joined by spaces, or `[comm]` when there is no argv. */
2549
+ function commandFromCmdline(cmdline, comm) {
2550
+ const joined = cmdline.split("\0").filter((part) => part.length > 0).join(" ");
2551
+ return joined.length > 0 ? joined : `[${comm}]`;
2552
+ }
2465
2553
  //#endregion
2466
2554
  //#region src/builtins/native-metrics/proc-process-table.ts
2467
2555
  /**
@@ -2493,28 +2581,28 @@ async function readProcRow(pid, uptimeSec, reader) {
2493
2581
  } catch {
2494
2582
  return null;
2495
2583
  }
2496
- const parsed = require_proc_stat.parseProcStat(stat);
2584
+ const parsed = parseProcStat(stat);
2497
2585
  if (parsed === null) return null;
2498
- const rss = require_proc_stat.parseProcStatusRss(status);
2586
+ const rss = parseProcStatusRss(status);
2499
2587
  return {
2500
2588
  pid: parsed.pid,
2501
2589
  ppid: parsed.ppid,
2502
2590
  pgid: parsed.pgid,
2503
- cpuPercent: require_proc_stat.lifetimeCpuPercent({
2591
+ cpuPercent: lifetimeCpuPercent({
2504
2592
  ticks: parsed.ticks,
2505
2593
  startTicks: parsed.startTicks,
2506
2594
  uptimeSec
2507
2595
  }),
2508
2596
  memoryRssBytes: rss.rssBytes ?? 0,
2509
- uptimeSec: require_proc_stat.elapsedSecSince({
2597
+ uptimeSec: elapsedSecSince({
2510
2598
  startTicks: parsed.startTicks,
2511
2599
  uptimeSec
2512
2600
  }),
2513
- command: require_proc_stat.commandFromCmdline(cmdline, parsed.comm)
2601
+ command: commandFromCmdline(cmdline, parsed.comm)
2514
2602
  };
2515
2603
  }
2516
2604
  /** Every process in `/proc`, as the rows `ps -eo …` would have printed. */
2517
- async function readProcProcessTable(reader = require_proc_stat.nodeProcReader) {
2605
+ async function readProcProcessTable(reader = nodeProcReader) {
2518
2606
  const [pids, uptimeSec] = await Promise.all([reader.listPids(), reader.readUptimeSec()]);
2519
2607
  return (await Promise.all(pids.map((pid) => readProcRow(pid, uptimeSec, reader)))).filter((row) => row !== null);
2520
2608
  }
@@ -2569,7 +2657,7 @@ async function spawnPsTable() {
2569
2657
  }
2570
2658
  var defaultDeps = {
2571
2659
  platform: (0, node_os.platform)(),
2572
- reader: require_proc_stat.nodeProcReader,
2660
+ reader: nodeProcReader,
2573
2661
  spawnPs: spawnPsTable
2574
2662
  };
2575
2663
  /**
@@ -3481,3 +3569,7 @@ var NativeMetricsAddon = class NativeMetricsAddon extends require_dist.BaseAddon
3481
3569
  //#endregion
3482
3570
  exports.NativeMetricsProvider = NativeMetricsProvider;
3483
3571
  exports.default = NativeMetricsAddon;
3572
+ exports.lifetimeCpuPercent = lifetimeCpuPercent;
3573
+ exports.nodeProcReader = nodeProcReader;
3574
+ exports.parseProcStat = parseProcStat;
3575
+ exports.parseProcStatusRss = parseProcStatusRss;