refactor(backend): extract types, constants, and guards from index.ts (phase 0) (#730)

* refactor(backend): extract types, constants, and guards from index.ts (phase 0)

Additive, behavior-preserving first step of the modular backend refactor.
Moves purely static artifacts out of backend/src/index.ts so later phases can
extract routes and middleware without touching shared symbols.

New modules:
- types/express.ts: Express Request augmentation
- helpers/constants.ts: PORT, password policy, label colors, cookie names,
  MFA TTLs, hot-path cache TTLs
- helpers/proxyExemptPaths.ts: PROXY_EXEMPT_PREFIXES + isProxyExemptPath
- helpers/cookies.ts: isSecureRequest, getCookieOptions
- helpers/policyGate.ts: buildPolicyGateOptions, runPolicyGate,
  triggerPostDeployScan
- middleware/permissions.ts: ROLE_PERMISSIONS, checkPermission,
  requirePermission
- middleware/tierGates.ts: requirePaid, requireAdmiral, requireAdmin,
  requireNodeProxy, requireScheduledTaskTier + effectiveTier/Variant

index.ts shrinks by ~260 lines; no runtime behavior changes. All 64 vitest
files and 1,278 tests pass.

* refactor(backend): drop unused imports left after phase 0 extraction

LicenseTier, LicenseVariant, DIGEST_CACHE_TTL_MS, and isProxyExemptPath
were imported into index.ts but no longer referenced there after the
phase 0 move; CI lint flagged them as errors.

isProxyExemptPath will be re-imported in phase 1 when the JSON parser
bypass and nodeContext middleware get extracted. Silence the
no-namespace warning on the Express augmentation since the namespace
syntax is required for TypeScript module augmentation.
This commit is contained in:
Anso
2026-04-23 17:58:04 -04:00
committed by GitHub
parent 1ef96582e1
commit 929e2fa6b1
8 changed files with 370 additions and 294 deletions
+65
View File
@@ -0,0 +1,65 @@
import type { Request, Response } from 'express';
import { LicenseService, type LicenseTier, type LicenseVariant } from '../services/LicenseService';
// Tier-based route guards. Each returns true when the request may proceed and
// false after sending the appropriate 403 response. Callers MUST check the
// return value and `return;` on false.
//
// Guards trust req.proxyTier/proxyVariant (set by authMiddleware for
// node_proxy tokens) ahead of the local LicenseService so a primary Sencho
// instance can assert license state for its remote fleet nodes.
const PAID_MESSAGE = 'This feature requires a Skipper or Admiral license.';
const ADMIRAL_MESSAGE = 'This feature requires a Sencho Admiral license.';
/** Effective license tier for this request (proxy header if trusted, else local). */
export const effectiveTier = (req: Request): LicenseTier =>
req.proxyTier ?? LicenseService.getInstance().getTier();
/** Effective license variant for this request (proxy header if trusted, else local). */
export const effectiveVariant = (req: Request): LicenseVariant =>
req.proxyVariant ?? LicenseService.getInstance().getVariant();
const deny = (res: Response, code: string, error: string): false => {
res.status(403).json({ error, code });
return false;
};
/** Paid feature guard: requires Skipper or Admiral. */
export const requirePaid = (req: Request, res: Response): boolean => {
if (effectiveTier(req) !== 'paid') return deny(res, 'PAID_REQUIRED', PAID_MESSAGE);
return true;
};
/** Admiral feature guard: requires paid tier with the admiral variant. */
export const requireAdmiral = (req: Request, res: Response): boolean => {
// Resolve both before branching so every caller observes the same
// tier/variant pair (the original behavior; tests mock LicenseService
// getters and rely on both being consumed per gate invocation).
const tier = effectiveTier(req);
const variant = effectiveVariant(req);
if (tier !== 'paid') return deny(res, 'PAID_REQUIRED', PAID_MESSAGE);
if (variant !== 'admiral') return deny(res, 'ADMIRAL_REQUIRED', ADMIRAL_MESSAGE);
return true;
};
/** Admin role guard: the request must be authenticated as an `admin` user. */
export const requireAdmin = (req: Request, res: Response): boolean => {
if (req.user?.role !== 'admin') return deny(res, 'ADMIN_REQUIRED', 'Admin access required.');
return true;
};
/**
* Accept only calls from a sibling Sencho using its node_proxy Bearer token.
* Browser sessions, API tokens, and console tokens are all rejected.
*/
export const requireNodeProxy = (req: Request, res: Response): boolean => {
if (req.user?.username !== 'node-proxy') return deny(res, 'NODE_PROXY_REQUIRED', 'Node proxy authentication required.');
return true;
};
/** Tier gate for scheduled tasks: `update` and `scan` require Skipper+, everything else requires Admiral. */
export const requireScheduledTaskTier = (action: string, req: Request, res: Response): boolean => {
if (action === 'update' || action === 'scan') return requirePaid(req, res);
return requireAdmiral(req, res);
};