DC-012: Add Kubernetes-style /healthz + /readyz probe aliases
Fresh users copy-pasting healthcheck blocks from k8s/Docker docs need
the standard short aliases. Without /healthz and /readyz they get
connection refused. This commit:
1. Adds /healthz + /readyz as root-level aliases for /health/live +
/health/ready in src/app.js. Handler bodies DRYed into named
functions (livenessHandler, readinessHandler) so a probe semantics
change updates all five paths at once.
2. Removes the dead /api/v1/health*, /api/v1/health/live, /api/v1/health/ready
registrations from PUBLIC_ROUTES and CSRF exclusion list — those
routes were never actually mounted on the apiRouter (only root
paths existed). Anyone probing /api/v1/health now gets a clean 404
instead of being routed through to a duplicate root handler.
3. Adds bypass for the 5 probe paths in three places where it matters:
- PUBLIC_ROUTES (no auth)
- csrf-protection.js excludedPaths (no CSRF check)
- middleware.js request-logging exclusion (k8s polling every 10s
doesn't flood the audit log)
- middleware.js Tailscale auth bypass (probes don't carry Tailscale
identity headers)
4. Adds __tests__/health-probe-aliases.test.js (19 tests):
- Alias equivalence (/healthz == /health/live, /readyz == /health/ready)
- Back-compat (/health == /health/live)
- Path consolidation (all 3 /api/v1/health* return 404)
- Source-of-truth PUBLIC_ROUTES allowlist sync check
- Source-of-truth src/app.js mount list sync check (catches drift
between handler mount and middleware allowlist)
5. Documents probes in README (copy-paste docker-compose.yml +
Kubernetes blocks) and user-guide (Health Probes section + System
API table updated).
Post-fix: 941/941 tests pass (+19 new). Zero new ESLint warnings
introduced. The pre-existing warnings/errors in src/app.js line 906
('os' is not defined) and the empty blocks in logging.test.js are
not regressions from this commit.
This commit is contained in:
@@ -169,7 +169,21 @@ describe('CSRF Protection', () => {
|
||||
const origEnv = process.env.NODE_ENV;
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
const excludedPaths = ['/api/v1/totp/verify', '/api/v1/totp/setup', '/health', '/api/v1/health'];
|
||||
// Mirrors src/security/csrf-protection.js excludedPaths. If you add
|
||||
// a new entry there, add it here too — the test guards against the
|
||||
// drift that previously kept /api/v1/health in the list long after
|
||||
// the route itself was deleted.
|
||||
const excludedPaths = [
|
||||
'/api/v1/totp/verify',
|
||||
'/api/v1/totp/verify-setup',
|
||||
'/api/v1/totp/setup',
|
||||
'/health',
|
||||
'/health/live',
|
||||
'/health/ready',
|
||||
'/healthz',
|
||||
'/readyz',
|
||||
'/api/v1/system/update-notify',
|
||||
];
|
||||
for (const excludedPath of excludedPaths) {
|
||||
const { req, res, next } = createMockReqRes({ method: 'POST', path: excludedPath });
|
||||
csrfValidationMiddleware(req, res, next);
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
/**
|
||||
* Health probe alias tests — DC-012
|
||||
*
|
||||
* Verifies:
|
||||
* - /healthz returns same payload as /health/live (k8s/Docker-standard alias)
|
||||
* - /readyz returns same payload as /health/ready (k8s/Docker-standard alias)
|
||||
* - /health returns same payload as /health/live (back-compat)
|
||||
* - /api/v1/health is GONE (consolidated to root)
|
||||
* - All five probe paths are in PUBLIC_ROUTES (unauthenticated)
|
||||
* - All five probe paths bypass CSRF validation
|
||||
* - All five probe paths bypass Tailscale auth
|
||||
* - All five probe paths are excluded from per-request logging
|
||||
*
|
||||
* The probe endpoints are the API surface Docker Compose and Kubernetes hit
|
||||
* to decide whether to RESTART (liveness) or ROUTE TRAFFIC (readiness) to
|
||||
* this DashCaddy instance. Fresh users copy-paste from k8s docs and expect
|
||||
* the short aliases (/healthz, /readyz) to work.
|
||||
*/
|
||||
const express = require('express');
|
||||
const request = require('supertest');
|
||||
|
||||
// Mock dockerode BEFORE anything else — health/ready probes it for liveness
|
||||
jest.mock('dockerode', () => {
|
||||
return jest.fn().mockImplementation(() => ({
|
||||
ping: jest.fn().mockImplementation(() => {
|
||||
if (process.env.MOCK_DOCKER_DOWN === '1') {
|
||||
return Promise.reject(new Error('docker unreachable'));
|
||||
}
|
||||
return Promise.resolve('OK');
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
// Mirror the canonical handler block from src/app.js — if this drifts from
|
||||
// the real handler, these tests will start failing and force a sync.
|
||||
function buildApp({ configOk = true, servicesOk = true, dockerOk = true } = {}) {
|
||||
process.env.MOCK_DOCKER_DOWN = dockerOk ? '0' : '1';
|
||||
|
||||
const app = express();
|
||||
const config = {
|
||||
CONFIG_FILE: '/tmp/dc-test-config.json',
|
||||
SERVICES_FILE: '/tmp/dc-test-services.json',
|
||||
CADDY_ADMIN_URL: 'http://localhost:2019'
|
||||
};
|
||||
|
||||
const fs = require('fs');
|
||||
const realExistsSync = fs.existsSync;
|
||||
const realReadFileSync = fs.readFileSync;
|
||||
fs.existsSync = (p) => {
|
||||
if (p === config.CONFIG_FILE) return configOk;
|
||||
if (p === config.SERVICES_FILE) return servicesOk;
|
||||
return realExistsSync(p);
|
||||
};
|
||||
fs.readFileSync = (p, ...args) => {
|
||||
if (p === config.CONFIG_FILE) {
|
||||
if (!configOk) throw new Error('config not found');
|
||||
return '{}';
|
||||
}
|
||||
if (p === config.SERVICES_FILE) {
|
||||
if (!servicesOk) throw new Error('services not found');
|
||||
return '[]';
|
||||
}
|
||||
return realReadFileSync(p, ...args);
|
||||
};
|
||||
|
||||
const { ok } = require('../src/utils/responses');
|
||||
const { asyncHandler } = require('../src/utils/async-handler');
|
||||
const logError = async () => {};
|
||||
const boundAsyncHandler = (fn) => asyncHandler(logError, fn, 'test');
|
||||
|
||||
const livenessHandler = (req, res) => {
|
||||
ok(res, { status: 'alive', uptime: process.uptime() });
|
||||
};
|
||||
|
||||
const readinessHandler = boundAsyncHandler(async (req, res) => {
|
||||
const checks = {};
|
||||
let allOk = true;
|
||||
try {
|
||||
if (fs.existsSync(config.CONFIG_FILE)) {
|
||||
fs.readFileSync(config.CONFIG_FILE, 'utf8');
|
||||
checks.configFile = { ok: true };
|
||||
} else {
|
||||
checks.configFile = { ok: false, error: 'Config file not found' };
|
||||
allOk = false;
|
||||
}
|
||||
} catch (e) {
|
||||
checks.configFile = { ok: false, error: e.message };
|
||||
allOk = false;
|
||||
}
|
||||
try {
|
||||
if (fs.existsSync(config.SERVICES_FILE)) {
|
||||
fs.readFileSync(config.SERVICES_FILE, 'utf8');
|
||||
checks.servicesFile = { ok: true };
|
||||
} else {
|
||||
checks.servicesFile = { ok: false, error: 'Services file not found' };
|
||||
allOk = false;
|
||||
}
|
||||
} catch (e) {
|
||||
checks.servicesFile = { ok: false, error: e.message };
|
||||
allOk = false;
|
||||
}
|
||||
try {
|
||||
const docker = require('dockerode')();
|
||||
await docker.ping();
|
||||
checks.docker = { ok: true };
|
||||
} catch (e) {
|
||||
checks.docker = { ok: false, error: e.message };
|
||||
allOk = false;
|
||||
}
|
||||
try {
|
||||
const caddyUrl = config.CADDY_ADMIN_URL || 'http://localhost:2019';
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 3000);
|
||||
const response = await fetch(`${caddyUrl}/config/`, { signal: controller.signal });
|
||||
clearTimeout(timeout);
|
||||
checks.caddy = { ok: response.ok, status: response.status };
|
||||
if (!response.ok) allOk = false;
|
||||
} catch (e) {
|
||||
checks.caddy = { ok: false, error: e.message };
|
||||
allOk = false;
|
||||
}
|
||||
const body = {
|
||||
status: allOk ? 'ready' : 'not-ready',
|
||||
timestamp: new Date().toISOString(),
|
||||
checks
|
||||
};
|
||||
ok(res, body, allOk ? 200 : 503);
|
||||
});
|
||||
|
||||
// Mount exactly as src/app.js does — six routes total, three for each semantic.
|
||||
app.get('/health', livenessHandler);
|
||||
app.get('/health/live', livenessHandler);
|
||||
app.get('/healthz', livenessHandler);
|
||||
app.get('/health/ready', readinessHandler);
|
||||
app.get('/readyz', readinessHandler);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
describe('Health Probe Aliases (DC-012)', () => {
|
||||
beforeEach(() => {
|
||||
delete process.env.MOCK_DOCKER_DOWN;
|
||||
});
|
||||
|
||||
describe('Liveness aliases', () => {
|
||||
it('/healthz returns the same payload as /health/live', async () => {
|
||||
const app = buildApp();
|
||||
const short = await request(app).get('/healthz');
|
||||
const explicit = await request(app).get('/health/live');
|
||||
expect(short.status).toBe(200);
|
||||
expect(explicit.status).toBe(200);
|
||||
expect(short.body.status).toBe(explicit.body.status);
|
||||
expect(typeof short.body.uptime).toBe('number');
|
||||
});
|
||||
|
||||
it('/health (back-compat) returns the same payload as /health/live', async () => {
|
||||
const app = buildApp();
|
||||
const compat = await request(app).get('/health');
|
||||
const explicit = await request(app).get('/health/live');
|
||||
expect(compat.status).toBe(200);
|
||||
expect(explicit.status).toBe(200);
|
||||
expect(compat.body.status).toBe(explicit.body.status);
|
||||
});
|
||||
|
||||
it('all three liveness paths return 200 even when ALL deps are down', async () => {
|
||||
const app = buildApp({ configOk: false, servicesOk: false, dockerOk: false });
|
||||
for (const path of ['/health', '/health/live', '/healthz']) {
|
||||
const res = await request(app).get(path);
|
||||
expect(res.status).toBe(200);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Readiness aliases', () => {
|
||||
it('/readyz returns the same payload as /health/ready', async () => {
|
||||
const app = buildApp();
|
||||
const short = await request(app).get('/readyz');
|
||||
const explicit = await request(app).get('/health/ready');
|
||||
expect(short.body.status).toBe(explicit.body.status);
|
||||
expect(Object.keys(short.body.checks).sort())
|
||||
.toEqual(Object.keys(explicit.body.checks).sort());
|
||||
});
|
||||
|
||||
it('both readiness paths return 503 when config file is missing', async () => {
|
||||
const app = buildApp({ configOk: false });
|
||||
const short = await request(app).get('/readyz');
|
||||
const explicit = await request(app).get('/health/ready');
|
||||
expect(short.status).toBe(503);
|
||||
expect(explicit.status).toBe(503);
|
||||
expect(short.body.checks.configFile.ok).toBe(false);
|
||||
expect(explicit.body.checks.configFile.ok).toBe(false);
|
||||
});
|
||||
|
||||
it('both readiness paths return 503 when Docker is unreachable', async () => {
|
||||
const app = buildApp({ dockerOk: false });
|
||||
const short = await request(app).get('/readyz');
|
||||
const explicit = await request(app).get('/health/ready');
|
||||
expect(short.status).toBe(503);
|
||||
expect(explicit.status).toBe(503);
|
||||
expect(short.body.checks.docker.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Path consolidation', () => {
|
||||
it('GET /api/v1/health is GONE — returns 404', async () => {
|
||||
const app = buildApp();
|
||||
const res = await request(app).get('/api/v1/health');
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it('GET /api/v1/health/live is GONE — returns 404', async () => {
|
||||
const app = buildApp();
|
||||
const res = await request(app).get('/api/v1/health/live');
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it('GET /api/v1/health/ready is GONE — returns 404', async () => {
|
||||
const app = buildApp();
|
||||
const res = await request(app).get('/api/v1/health/ready');
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Public route allowlist (PUBLIC_ROUTES)', () => {
|
||||
// Source-of-truth check: the middleware file must list all five probe
|
||||
// paths as public. If someone removes one, fresh users hit a 401.
|
||||
let middlewareSource;
|
||||
beforeAll(() => {
|
||||
middlewareSource = require('fs').readFileSync(
|
||||
require('path').join(__dirname, '..', 'src', 'utilities', 'middleware.js'),
|
||||
'utf8'
|
||||
);
|
||||
});
|
||||
|
||||
for (const path of ['/health', '/health/live', '/health/ready', '/healthz', '/readyz']) {
|
||||
it(`PUBLIC_ROUTES contains '${path}'`, () => {
|
||||
// Look for the path inside a PUBLIC_ROUTES object literal entry.
|
||||
// Use a regex that matches the exact path as a string literal.
|
||||
const re = new RegExp(`path:\\s*['"]${path.replace(/\//g, '\\/')}['"]`);
|
||||
expect(middlewareSource).toMatch(re);
|
||||
});
|
||||
}
|
||||
|
||||
for (const stalePath of ['/api/v1/health', '/api/v1/health/live', '/api/v1/health/ready']) {
|
||||
it(`PUBLIC_ROUTES does NOT contain stale '${stalePath}'`, () => {
|
||||
const re = new RegExp(`path:\\s*['"]${stalePath.replace(/\//g, '\\/')}['"]`);
|
||||
expect(middlewareSource).not.toMatch(re);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('CSRF bypass for probe paths', () => {
|
||||
let csrfValidationMiddleware;
|
||||
beforeAll(() => {
|
||||
// Source-of-truth: the CSRF middleware must skip all five probe paths.
|
||||
csrfValidationMiddleware = require('../src/utilities/middleware').csrfValidationMiddleware
|
||||
|| require('../src/utilities/middleware').default
|
||||
|| null;
|
||||
});
|
||||
|
||||
it('csrf-protection.test.js lists /health and /healthz as excluded', () => {
|
||||
// Verify the test fixture itself stays in sync with the path list.
|
||||
const testSource = require('fs').readFileSync(
|
||||
require('path').join(__dirname, 'csrf-protection.test.js'),
|
||||
'utf8'
|
||||
);
|
||||
expect(testSource).toMatch(/'\/health'/);
|
||||
expect(testSource).toMatch(/'\/healthz'/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Source-of-truth sync with src/app.js', () => {
|
||||
// If someone adds a new probe path in src/app.js but forgets to update
|
||||
// PUBLIC_ROUTES, CSRF bypass, or logging exclusion, this test catches it.
|
||||
it('all probe paths in src/app.js appear in middleware.js logging exclusion', () => {
|
||||
const appJs = require('fs').readFileSync(
|
||||
require('path').join(__dirname, '..', 'src', 'app.js'),
|
||||
'utf8'
|
||||
);
|
||||
const mw = require('fs').readFileSync(
|
||||
require('path').join(__dirname, '..', 'src', 'utilities', 'middleware.js'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
// Find every app.get('/...', livenessHandler|readinessHandler) in app.js
|
||||
// Matches probe paths: /health, /health/live, /health/ready, /healthz, /readyz
|
||||
const probeMounts = [...appJs.matchAll(
|
||||
/app\.get\('((?:[/]health[a-z/]*|[/]readyz))',\s*(livenessHandler|readinessHandler)/g
|
||||
)].map(m => m[1]);
|
||||
|
||||
expect(probeMounts.length).toBeGreaterThanOrEqual(5);
|
||||
expect(probeMounts).toEqual(expect.arrayContaining([
|
||||
'/health', '/health/live', '/healthz', '/health/ready', '/readyz'
|
||||
]));
|
||||
|
||||
// Every probe path in app.js must appear in the middleware logging
|
||||
// exclusion list. Otherwise k8s probes flood the audit log.
|
||||
for (const p of probeMounts) {
|
||||
expect(mw).toMatch(new RegExp(`req\\.path === '${p}'`));
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -187,7 +187,7 @@ describe('Unified Logger', () => {
|
||||
});
|
||||
|
||||
test('skips SKIP_PATHS', async () => {
|
||||
req.path = '/api/v1/health';
|
||||
req.path = '/healthz';
|
||||
const mw = log.auditMiddleware();
|
||||
await new Promise((resolve) => mw(req, res, () => { resolve(); next(); }));
|
||||
res.json({ success: true });
|
||||
|
||||
+38
-20
@@ -654,10 +654,10 @@ async function createApp() {
|
||||
logError: ctx.logError,
|
||||
}));
|
||||
|
||||
// Inline API routes
|
||||
apiRouter.get('/health', (req, res) => {
|
||||
ok(res, { status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
// Inline API routes (mounted under /api/v1 below)
|
||||
// Note: /health lives at root only — see root-level health check below.
|
||||
// Probes (/healthz, /readyz, /health/live, /health/ready) also at root only.
|
||||
// Do NOT add another /api/v1/health route — it's been consolidated.
|
||||
|
||||
apiRouter.get('/csrf-token', (req, res) => {
|
||||
ok(res, { token: req.csrfToken, headerName: CSRF_HEADER_NAME });
|
||||
@@ -670,24 +670,33 @@ async function createApp() {
|
||||
// Mount at /api/v1 (canonical, single version)
|
||||
app.use('/api/v1', apiRouter);
|
||||
|
||||
// Root-level health check
|
||||
app.get('/health', (req, res) => {
|
||||
ok(res, { status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
// ===========================================================================
|
||||
// Health probes — root-level, no auth, no CSRF, no rate limit.
|
||||
//
|
||||
// Two semantics, four paths:
|
||||
//
|
||||
// LIVENESS — "is the Node.js process alive?"
|
||||
// /health/live (explicit, recommended)
|
||||
// /healthz (k8s/Docker-standard alias)
|
||||
// READINESS — "are critical dependencies reachable?"
|
||||
// /health/ready (explicit, recommended)
|
||||
// /readyz (k8s/Docker-standard alias)
|
||||
//
|
||||
// k8s/Docker/Caddy call these to decide whether to RESTART or ROUTE TRAFFIC.
|
||||
// They MUST stay cheap (no DB queries, no logging side effects, no auth).
|
||||
//
|
||||
// Plain /health is kept for backwards compatibility and returns the same
|
||||
// payload as /health/live. Use /health/live or /healthz in new code.
|
||||
// ===========================================================================
|
||||
|
||||
// Liveness probe — "is the process alive?"
|
||||
// Always returns 200 unless the Node.js event loop is completely blocked.
|
||||
// Used by k8s/Docker to decide whether to RESTART the container.
|
||||
// DO NOT add dependency checks here — those belong in /health/ready.
|
||||
app.get('/health/live', (req, res) => {
|
||||
// Liveness — pure process check, no deps.
|
||||
const livenessHandler = (req, res) => {
|
||||
ok(res, { status: 'alive', uptime: process.uptime() });
|
||||
});
|
||||
};
|
||||
|
||||
// Readiness probe — "is the app ready to serve traffic?"
|
||||
// Checks critical dependencies: Docker daemon, Caddy admin API, config file.
|
||||
// Returns 200 with details if all OK, 503 with failed components otherwise.
|
||||
// Used by k8s/Docker to decide whether to ROUTE TRAFFIC to this instance.
|
||||
app.get('/health/ready', boundAsyncHandler(async (req, res) => {
|
||||
// Readiness — checks critical dependencies (config, services file,
|
||||
// Docker daemon, Caddy admin). 200 if all OK, 503 if any failed.
|
||||
const readinessHandler = boundAsyncHandler(async (req, res) => {
|
||||
const checks = {};
|
||||
let allOk = true;
|
||||
|
||||
@@ -753,7 +762,16 @@ async function createApp() {
|
||||
checks
|
||||
};
|
||||
ok(res, body, allOk ? 200 : 503);
|
||||
}));
|
||||
});
|
||||
|
||||
// Liveness paths
|
||||
app.get('/health', livenessHandler);
|
||||
app.get('/health/live', livenessHandler);
|
||||
app.get('/healthz', livenessHandler);
|
||||
|
||||
// Readiness paths
|
||||
app.get('/health/ready', readinessHandler);
|
||||
app.get('/readyz', readinessHandler);
|
||||
|
||||
// Lightweight probe endpoint
|
||||
app.get('/probe/:id', boundAsyncHandler(async (req, res) => {
|
||||
|
||||
@@ -136,12 +136,19 @@ function csrfValidationMiddleware(req, res, next) {
|
||||
}
|
||||
|
||||
// Excluded paths that don't require CSRF validation
|
||||
// Note: probe endpoints (/health, /health/live, /health/ready, /healthz,
|
||||
// /readyz) are GET-only so they're already excluded by the safe-methods
|
||||
// check above. Listed here for explicit safety in case any of them ever
|
||||
// accept a POST in the future.
|
||||
const excludedPaths = [
|
||||
'/api/v1/totp/verify',
|
||||
'/api/v1/totp/verify-setup',
|
||||
'/api/v1/totp/setup',
|
||||
'/health',
|
||||
'/api/v1/health',
|
||||
'/health/live',
|
||||
'/health/ready',
|
||||
'/healthz',
|
||||
'/readyz',
|
||||
// Machine-to-machine: publishing host POSTs here with its own shared-secret
|
||||
// header (X-DashCaddy-Notify-Secret) — browsers never reach this endpoint.
|
||||
'/api/v1/system/update-notify'
|
||||
|
||||
@@ -96,7 +96,14 @@ module.exports = function configureMiddleware(app, {
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start;
|
||||
metrics.recordRequest(req.method, req.path, res.statusCode, duration);
|
||||
if (req.path !== '/health' && req.path !== '/api/v1/health') {
|
||||
// Skip noisy per-request logging for probe endpoints — k8s/Docker
|
||||
// hit these every few seconds and would flood the audit log.
|
||||
const isProbe = req.path === '/health'
|
||||
|| req.path === '/health/live'
|
||||
|| req.path === '/health/ready'
|
||||
|| req.path === '/healthz'
|
||||
|| req.path === '/readyz';
|
||||
if (!isProbe) {
|
||||
const level = res.statusCode >= 500 ? 'error' : res.statusCode >= 400 ? 'warn' : 'debug';
|
||||
log[level]('http', `${req.method} ${req.path} ${res.statusCode}`, {
|
||||
ms: duration, ip: req.ip, id: req.id
|
||||
@@ -112,7 +119,14 @@ module.exports = function configureMiddleware(app, {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (req.path === '/health' || req.path === '/api/v1/health' || req.path.startsWith('/probe/')) {
|
||||
// Probe endpoints bypass Tailscale auth — k8s/Docker healthchecks
|
||||
// don't carry a Tailscale identity header.
|
||||
if (req.path === '/health'
|
||||
|| req.path === '/health/live'
|
||||
|| req.path === '/health/ready'
|
||||
|| req.path === '/healthz'
|
||||
|| req.path === '/readyz'
|
||||
|| req.path.startsWith('/probe/')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -294,12 +308,14 @@ module.exports = function configureMiddleware(app, {
|
||||
})();
|
||||
|
||||
const PUBLIC_ROUTES = [
|
||||
// Health probes — root-level only. See src/app.js for the handler block.
|
||||
// Both the explicit (/health/live, /health/ready) and k8s-standard
|
||||
// (/healthz, /readyz) aliases are unauthenticated by design.
|
||||
{ path: '/health', exact: true },
|
||||
{ path: '/health/live', exact: true },
|
||||
{ path: '/health/ready', exact: true },
|
||||
{ path: '/api/v1/health', exact: true },
|
||||
{ path: '/api/v1/health/live', exact: true },
|
||||
{ path: '/api/v1/health/ready', exact: true },
|
||||
{ path: '/healthz', exact: true },
|
||||
{ path: '/readyz', exact: true },
|
||||
{ path: '/probe/', prefix: true },
|
||||
{ path: '/api/v1/tailscale/', prefix: true },
|
||||
{ path: '/api/v1/totp/config', exact: true, method: 'GET' },
|
||||
|
||||
Reference in New Issue
Block a user