@camstack/server 1.1.53 → 1.1.54

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.
@@ -21,7 +21,9 @@
21
21
  */
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.buildHubHealth = buildHubHealth;
24
+ exports.isAuthorizedHealthRequest = isAuthorizedHealthRequest;
24
25
  exports.registerHealthRoutes = registerHealthRoutes;
26
+ const session_cookie_js_1 = require("../../auth/session-cookie.js");
25
27
  const AGENT_HEALTH_TIMEOUT_MS = 3_000;
26
28
  function nowIso() {
27
29
  return new Date().toISOString();
@@ -68,23 +70,64 @@ async function fetchAgentHealth(deps, nodeId) {
68
70
  };
69
71
  }
70
72
  }
73
+ /**
74
+ * True when the request carries a valid SESSION-grade credential: a Bearer
75
+ * JWT or the httpOnly session cookie. Health details expose topology
76
+ * (agent ids, versions, pids) — public probes get only `{ok}`.
77
+ * Pure over (headers, verifier) so the spec exercises it directly.
78
+ */
79
+ function isAuthorizedHealthRequest(headers, verifyToken) {
80
+ const bearer = headers.authorization?.startsWith('Bearer ')
81
+ ? headers.authorization.slice('Bearer '.length)
82
+ : null;
83
+ const cookieToken = (0, session_cookie_js_1.readSessionCookieFromHeader)(headers.cookie);
84
+ for (const token of [bearer, cookieToken]) {
85
+ if (!token)
86
+ continue;
87
+ try {
88
+ if ((0, session_cookie_js_1.isSessionGradeJwtPayload)(verifyToken(token)))
89
+ return true;
90
+ }
91
+ catch {
92
+ /* invalid/expired — try the next credential */
93
+ }
94
+ }
95
+ return false;
96
+ }
71
97
  function registerHealthRoutes(fastify, deps) {
98
+ const authorized = (req) => isAuthorizedHealthRequest({
99
+ ...(typeof req.headers.authorization === 'string'
100
+ ? { authorization: req.headers.authorization }
101
+ : {}),
102
+ ...(typeof req.headers.cookie === 'string' ? { cookie: req.headers.cookie } : {}),
103
+ }, deps.verifyToken);
104
+ // PUBLIC probe: liveness only. Everything beyond `ok` (version, uptime,
105
+ // pid, agent topology) moved to the AUTHENTICATED /health/details — a
106
+ // public endpoint must not enumerate the deployment.
72
107
  fastify.get('/health', async (_req, reply) => {
108
+ const ok = deps.isReady();
109
+ if (!ok)
110
+ reply.status(503);
111
+ return { ok };
112
+ });
113
+ // AUTHENTICATED detailed health — the payload /health used to return.
114
+ fastify.get('/health/details', async (req, reply) => {
115
+ if (!authorized(req))
116
+ return reply.status(401).send({ ok: false, error: 'unauthorized' });
73
117
  const health = await buildHubHealth(deps);
74
118
  if (!health.ok)
75
119
  reply.status(503);
76
120
  return health;
77
121
  });
78
- // Zero-config LAN discovery — unauthenticated, always 200. See DiscoveryInfo.
79
- fastify.get('/discovery', async () => {
80
- const name = process.env['CAMSTACK_HUB_NAME'] ?? 'CamStack Hub';
81
- return { service: 'camstack-hub', nodeId: 'hub', version: deps.hubVersion, name };
82
- });
83
- fastify.get('/health/agents', async () => {
122
+ fastify.get('/health/agents', async (req, reply) => {
123
+ if (!authorized(req))
124
+ return reply.status(401).send({ ok: false, error: 'unauthorized' });
84
125
  const nodes = deps.agentRegistry.listNodeLiveness();
85
126
  return { agents: nodes.filter((n) => !n.isHub && n.isOnline).map((n) => n.id) };
86
127
  });
87
128
  fastify.get('/health/agents/:nodeId', async (req, reply) => {
129
+ if (!authorized(req))
130
+ return reply.status(401).send({ ok: false, error: 'unauthorized' });
88
131
  const { nodeId } = req.params;
89
132
  if (!nodeId) {
90
133
  return reply.status(400).send({ ok: false, error: 'nodeId required' });
@@ -95,7 +138,9 @@ function registerHealthRoutes(fastify, deps) {
95
138
  }
96
139
  return result;
97
140
  });
98
- fastify.get('/health/cluster', async () => {
141
+ fastify.get('/health/cluster', async (req, reply) => {
142
+ if (!authorized(req))
143
+ return reply.status(401).send({ ok: false, error: 'unauthorized' });
99
144
  const hub = await buildHubHealth(deps);
100
145
  // Enumeration only (fan-out-free); the $agent.health fan-out below is intentional.
101
146
  const nodes = deps.agentRegistry.listNodeLiveness();
package/dist/main.js CHANGED
@@ -320,6 +320,7 @@ async function bootstrap() {
320
320
  // Read the live flag at request time — flips to true once the tRPC
321
321
  // router finishes registering (see `trpcRegistered = true` below).
322
322
  isReady: () => trpcRegistered,
323
+ verifyToken: (token) => app.get(auth_service_1.AuthService).verifyToken(token),
323
324
  });
324
325
  console.log(`[bootstrap] Health routes registered (hub v${hubVersion})`);
325
326
  }
@@ -997,7 +998,6 @@ async function bootstrap() {
997
998
  url.startsWith('/api/') ||
998
999
  url.startsWith('/agent') ||
999
1000
  url.startsWith('/health') ||
1000
- url.startsWith('/discovery') ||
1001
1001
  // The viewer SPA owns /viewer/** via its own catch-all (registered
1002
1002
  // below). Fall through here so admin-ui never serves it — and so
1003
1003
  // /viewer 404s cleanly when the viewer-ui addon isn't registered.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.1.53",
3
+ "version": "1.1.54",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -23,13 +23,13 @@
23
23
  "test:watch": "vitest"
24
24
  },
25
25
  "dependencies": {
26
- "@camstack/addon-admin-ui": "1.1.45",
26
+ "@camstack/addon-admin-ui": "1.1.46",
27
27
  "@camstack/addon-advanced-notifier": "1.1.21",
28
28
  "@camstack/addon-auth": "1.1.6",
29
29
  "@camstack/addon-decoder-nodeav": "1.1.9",
30
30
  "@camstack/addon-notifiers": "1.1.21",
31
31
  "@camstack/addon-pipeline": "1.1.51",
32
- "@camstack/addon-pipeline-orchestrator": "1.1.39",
32
+ "@camstack/addon-pipeline-orchestrator": "1.1.40",
33
33
  "@camstack/addon-post-analysis": "1.1.23",
34
34
  "@camstack/sdk": "1.1.22",
35
35
  "@camstack/shm-ring": "1.0.21",