@camstack/server 1.2.665 → 1.2.667

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,11 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.667] - 2026-10-28
11
+
12
+ ### Changed
13
+
14
+ - Rebuilt against updated dependencies.
15
+
16
+ ## [1.2.666] - 2026-10-28
17
+
18
+ ### Changed
19
+
20
+ - The hub raises a critical alert when npm or Hugging Face stays unreachable, and resolves it on the next success
21
+
10
22
  ## [1.2.665] - 2026-10-28
11
23
 
12
24
  ### Changed
13
25
 
14
26
  - A shared events link can open a track's detail page, and shared links now use the operator's colour palette
27
+ - The hub raises a critical alert when npm cannot be reached (addon and server updates are paused) and resolves it when the registry answers again
15
28
 
16
29
  ## [1.2.664] - 2026-10-27
17
30
 
@@ -213,6 +213,7 @@ class AddonPackageService {
213
213
  * the scheduler's dependencies back.
214
214
  */
215
215
  updateCheckRescheduler = null;
216
+ npmReachability = null;
216
217
  // -- Timing constants -----------------------------------------------------
217
218
  // Short TTL — operators expect to see freshly-published versions
218
219
  // within minutes of `npm publish`, not hours. The Addons page kicks
@@ -1439,6 +1440,10 @@ class AddonPackageService {
1439
1440
  // =========================================================================
1440
1441
  // Auto-update settings
1441
1442
  // =========================================================================
1443
+ /** The hub's npm sweep reports whether the registry answered at all (alert on an outage). */
1444
+ setNpmReachabilityObserver(observer) {
1445
+ this.npmReachability = observer;
1446
+ }
1442
1447
  /**
1443
1448
  * Register the availability poller's re-arm hook. Called once at boot so a
1444
1449
  * `setAutoUpdateSettings` write takes effect without a restart.
@@ -1695,8 +1700,11 @@ class AddonPackageService {
1695
1700
  const addonsDir = this.resolveAddonsDir();
1696
1701
  const updates = [];
1697
1702
  let failures = 0;
1703
+ let lookups = 0;
1704
+ let reached = 0;
1705
+ let lastLookupError = '';
1698
1706
  if (!fs.existsSync(addonsDir))
1699
- return { updates, failures };
1707
+ return { updates, failures, lookups, reached };
1700
1708
  // Collect all package.json paths -- handles both flat and scoped layouts
1701
1709
  const pkgJsonPaths = [];
1702
1710
  const topDirs = fs
@@ -1743,6 +1751,11 @@ class AddonPackageService {
1743
1751
  continue;
1744
1752
  }
1745
1753
  const lookup = await this.lookupLatestVersion(name);
1754
+ lookups += 1;
1755
+ if (lookup.ok)
1756
+ reached += 1;
1757
+ else
1758
+ lastLookupError = lookup.error;
1746
1759
  if (!lookup.ok) {
1747
1760
  failures += 1;
1748
1761
  this.logger.warn('Registry lookup failed for installed addon package', {
@@ -1772,7 +1785,21 @@ class AddonPackageService {
1772
1785
  });
1773
1786
  }
1774
1787
  }
1775
- return { updates, failures };
1788
+ this.reportNpmReachability(lookups, reached, lastLookupError);
1789
+ return { updates, failures, lookups, reached };
1790
+ }
1791
+ /**
1792
+ * One sweep is one probe: npm is unreachable only when lookups were made and
1793
+ * NONE got an answer. A sweep with no lookups says nothing either way.
1794
+ */
1795
+ reportNpmReachability(lookups, reached, lastError) {
1796
+ const observer = this.npmReachability;
1797
+ if (observer === null || lookups === 0)
1798
+ return;
1799
+ const report = reached > 0 ? observer.reached() : observer.failed(lastError);
1800
+ void report.catch((error) => {
1801
+ this.logger.warn('npm reachability report failed', { meta: { error: (0, types_2.errMsg)(error) } });
1802
+ });
1776
1803
  }
1777
1804
  // =========================================================================
1778
1805
  // Private: npm registry helpers
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RegistryReachabilityTracker = exports.REGISTRY_UNREACHABLE_CATEGORY = exports.REGISTRY_UNREACHABLE_MIN_FAILURE_MS = exports.REGISTRY_UNREACHABLE_MIN_FAILURES = void 0;
4
+ exports.registryUnreachableAlertId = registryUnreachableAlertId;
5
+ exports.buildRegistryUnreachableAlert = buildRegistryUnreachableAlert;
6
+ const types_1 = require("@camstack/types");
7
+ /** Consecutive failed probes before an episode may open. */
8
+ exports.REGISTRY_UNREACHABLE_MIN_FAILURES = 2;
9
+ /** The first failure must be at least this old when the threshold is met. */
10
+ exports.REGISTRY_UNREACHABLE_MIN_FAILURE_MS = 10 * 60_000;
11
+ exports.REGISTRY_UNREACHABLE_CATEGORY = 'registry.unreachable';
12
+ const EPISODE_KEY = 'registry-unreachable';
13
+ function registryUnreachableAlertId(registry) {
14
+ return `registry-unreachable:${registry}`;
15
+ }
16
+ const WORDS = {
17
+ npm: {
18
+ name: 'npm',
19
+ impact: 'Addon and server updates are paused',
20
+ },
21
+ huggingface: {
22
+ name: 'Hugging Face',
23
+ impact: 'Model downloads are failing',
24
+ },
25
+ };
26
+ function buildRegistryUnreachableAlert(registry, reason, now) {
27
+ const words = WORDS[registry];
28
+ return {
29
+ id: registryUnreachableAlertId(registry),
30
+ category: exports.REGISTRY_UNREACHABLE_CATEGORY,
31
+ severity: 'critical',
32
+ title: `${words.name} cannot be reached`,
33
+ message: `CamStack cannot reach ${words.name}. ${words.impact} until the connection is back (${reason}). Check the hub's internet access, DNS and proxy.`,
34
+ status: 'active',
35
+ read: false,
36
+ createdAt: now,
37
+ updatedAt: now,
38
+ source: { type: 'registry', id: registry },
39
+ metadata: { episodeKey: EPISODE_KEY, registry },
40
+ };
41
+ }
42
+ class RegistryReachabilityTracker {
43
+ #logger;
44
+ #getAlerts;
45
+ #now;
46
+ #state = new Map();
47
+ constructor(options) {
48
+ this.#logger = options.logger;
49
+ this.#getAlerts = options.getAlerts;
50
+ this.#now = options.now ?? Date.now;
51
+ }
52
+ /** A probe could not reach the registry. Idempotent per call: it only counts. */
53
+ async failed(registry, reason) {
54
+ const now = this.#now();
55
+ const state = this.#stateOf(registry);
56
+ if (state.failures === 0)
57
+ state.firstFailureAt = now;
58
+ state.failures += 1;
59
+ this.#logger.warn(`${WORDS[registry].name} unreachable`, {
60
+ meta: { registry, consecutiveFailures: state.failures, reason },
61
+ });
62
+ if (state.alerted)
63
+ return;
64
+ if (state.failures < exports.REGISTRY_UNREACHABLE_MIN_FAILURES)
65
+ return;
66
+ if (now - state.firstFailureAt < exports.REGISTRY_UNREACHABLE_MIN_FAILURE_MS)
67
+ return;
68
+ await this.#open(registry, state, reason, now);
69
+ }
70
+ /** A probe reached the registry (any answer below 5xx). Resolves an open episode. */
71
+ async reached(registry) {
72
+ const state = this.#stateOf(registry);
73
+ const wasAlerted = state.alerted;
74
+ const firstSuccess = !state.reconciled;
75
+ state.failures = 0;
76
+ state.firstFailureAt = 0;
77
+ state.alerted = false;
78
+ state.reconciled = true;
79
+ // First success after boot also closes an episode a previous run left open
80
+ // (resolving one that is not there is a no-op in the alert centre).
81
+ if (!wasAlerted && !firstSuccess)
82
+ return;
83
+ await this.#resolve(registry, wasAlerted ? 'registry reachable again' : 'startup reconcile');
84
+ }
85
+ #stateOf(registry) {
86
+ const existing = this.#state.get(registry);
87
+ if (existing !== undefined)
88
+ return existing;
89
+ const created = {
90
+ failures: 0,
91
+ firstFailureAt: 0,
92
+ alerted: false,
93
+ reconciled: false,
94
+ };
95
+ this.#state.set(registry, created);
96
+ return created;
97
+ }
98
+ async #open(registry, state, reason, now) {
99
+ const alerts = this.#getAlerts();
100
+ if (alerts === undefined) {
101
+ this.#logger.warn('Registry unreachable alert skipped — alerts capability missing', {
102
+ meta: { registry },
103
+ });
104
+ return;
105
+ }
106
+ try {
107
+ await alerts.emit(buildRegistryUnreachableAlert(registry, reason, now));
108
+ state.alerted = true;
109
+ this.#logger.warn(`${WORDS[registry].name} unreachable — alert raised`, {
110
+ meta: { registry, consecutiveFailures: state.failures },
111
+ });
112
+ }
113
+ catch (error) {
114
+ // Not alerted: the next failed probe tries again.
115
+ this.#logger.warn('Registry unreachable alert emit failed', {
116
+ meta: { registry, error: (0, types_1.errMsg)(error) },
117
+ });
118
+ }
119
+ }
120
+ async #resolve(registry, why) {
121
+ const alerts = this.#getAlerts();
122
+ if (alerts === undefined) {
123
+ this.#logger.debug('Registry alert not resolved — alerts capability missing', {
124
+ meta: { registry, why },
125
+ });
126
+ return;
127
+ }
128
+ try {
129
+ await alerts.update({
130
+ alertId: registryUnreachableAlertId(registry),
131
+ patch: { status: 'completed', read: true },
132
+ });
133
+ this.#logger.info(`${WORDS[registry].name} reachable — alert resolved`, {
134
+ meta: { registry, why },
135
+ });
136
+ }
137
+ catch (error) {
138
+ this.#logger.debug('Registry alert resolve skipped', {
139
+ meta: { registry, why, error: (0, types_1.errMsg)(error) },
140
+ });
141
+ }
142
+ }
143
+ }
144
+ exports.RegistryReachabilityTracker = RegistryReachabilityTracker;
@@ -82,6 +82,7 @@ const topology_emitter_service_1 = require("./core/topology/topology-emitter.ser
82
82
  const update_availability_emitter_js_1 = require("./core/update-availability-emitter.js");
83
83
  const update_availability_store_js_1 = require("./core/update-availability-store.js");
84
84
  const agent_installed_packages_js_1 = require("./core/updates/agent-installed-packages.js");
85
+ const registry_reachability_js_1 = require("./core/updates/registry-reachability.js");
85
86
  const update_check_scheduler_js_1 = require("./core/updates/update-check-scheduler.js");
86
87
  const wrapper_update_checker_js_1 = require("./core/updates/wrapper-update-checker.js");
87
88
  // ---------------------------------------------------------------------------
@@ -319,6 +320,23 @@ async function bootManual(opts) {
319
320
  return types_1.ServerPackageStatusSchema.parse(status);
320
321
  },
321
322
  });
323
+ // npm / Hugging Face unreachable → one critical alert episode. No clock of
324
+ // its own: npm is heard from the sweep below, Hugging Face from the model
325
+ // downloads this process makes anyway.
326
+ const registryReachability = new registry_reachability_js_1.RegistryReachabilityTracker({
327
+ logger: updateCheckLogger,
328
+ getAlerts: () => addonRegistryService.getCapabilityRegistry().getSingleton('alerts') ??
329
+ undefined,
330
+ });
331
+ addonPackageService.setNpmReachabilityObserver({
332
+ reached: () => registryReachability.reached('npm'),
333
+ failed: (reason) => registryReachability.failed('npm', reason),
334
+ });
335
+ (0, system_1.setHuggingFaceReachabilityObserver)((outcome) => {
336
+ void (outcome.reached
337
+ ? registryReachability.reached('huggingface')
338
+ : registryReachability.failed('huggingface', outcome.detail));
339
+ });
322
340
  const updateCheckScheduler = new update_check_scheduler_js_1.UpdateCheckScheduler({
323
341
  logger: updateCheckLogger,
324
342
  getIntervalSeconds: () => addonPackageService.getAutoUpdateSettings().updateCheckIntervalSeconds,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.665",
3
+ "version": "1.2.667",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -49,19 +49,19 @@
49
49
  ]
50
50
  },
51
51
  "dependencies": {
52
- "@camstack/addon-admin-ui": "1.2.498",
52
+ "@camstack/addon-admin-ui": "1.2.500",
53
53
  "@camstack/addon-agent-ui": "1.2.283",
54
54
  "@camstack/addon-auth": "1.2.170",
55
55
  "@camstack/addon-benchmark": "1.2.284",
56
56
  "@camstack/addon-decoder-nodeav": "1.2.144",
57
57
  "@camstack/addon-notifiers": "1.2.623",
58
58
  "@camstack/addon-pipeline": "1.2.631",
59
- "@camstack/addon-pipeline-orchestrator": "1.2.633",
60
- "@camstack/addon-post-analysis": "1.2.645",
59
+ "@camstack/addon-pipeline-orchestrator": "1.2.634",
60
+ "@camstack/addon-post-analysis": "1.2.646",
61
61
  "@camstack/develop": "0.1.38",
62
62
  "@camstack/sdk": "1.2.154",
63
- "@camstack/system": "1.2.502",
64
- "@camstack/types": "1.2.432",
63
+ "@camstack/system": "1.2.503",
64
+ "@camstack/types": "1.2.433",
65
65
  "@camstack/ui-library": "1.2.341",
66
66
  "@fastify/compress": "^9.0.0",
67
67
  "@fastify/cookie": "^11.0.2",