@openparachute/hub 0.7.4-rc.14 → 0.7.4-rc.16

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/src/help.ts CHANGED
@@ -17,6 +17,7 @@ Usage:
17
17
  parachute install <service> install and register a service
18
18
  services: ${services}
19
19
  parachute status show installed services, run state, health
20
+ parachute doctor run health checks + tell you the one thing to fix
20
21
  parachute start [service] start a module via the supervisor (or ensure the hub is up)
21
22
  parachute stop [service] stop a module via the supervisor (or stop the hub unit)
22
23
  parachute restart [service] restart a module via the supervisor (or restart the hub unit)
@@ -367,6 +368,43 @@ Example:
367
368
  `;
368
369
  }
369
370
 
371
+ export function doctorHelp(): string {
372
+ return `parachute doctor — health / diagnostics for your Parachute install
373
+
374
+ Usage:
375
+ parachute doctor [--json]
376
+
377
+ What it does:
378
+ Runs a set of independent health checks and prints a grouped report
379
+ (✓ pass / ⚠ warn / ✗ fail), each with a one-line detail and — where there
380
+ is one — a copy-pasteable fix-it command. The single command that answers
381
+ "is my Parachute healthy, and if not, what's the one thing to fix?"
382
+
383
+ Checks (each PASSES on a fresh / fully-current install — doctor positively
384
+ detects a known-bad condition and never treats "not configured" as broken):
385
+ - Hub supervisor reachable on :1939 (/health).
386
+ - Each CONFIGURED module alive via its loopback /health (2xx or 401 = live).
387
+ - services.json parses + required fields valid (a missing file is the
388
+ fresh pre-install state, not a failure).
389
+ - operator.token exists, parses, and its issuer matches the hub (the
390
+ recurring "not signed in to the hub" / issuer-mismatch class).
391
+ - Each first-party module bin is executable (catches the lost-+x-bit
392
+ start-failure class).
393
+ - Migration: legacy detached install? known cruft at the ecosystem root?
394
+ (allowlist detectors only — a fresh root flags nothing).
395
+ - Exposure: if exposed, is the public origin reachable? If not exposed,
396
+ "loopback only" is reported as benign info, never a warning.
397
+ - Version freshness (cosmetic) — drift is WARN at most, never a failure.
398
+
399
+ Flags:
400
+ --json emit a single JSON object instead of the human report
401
+
402
+ Exit codes:
403
+ 0 no failures (warnings are advisory and still exit 0)
404
+ 1 one or more checks failed
405
+ `;
406
+ }
407
+
370
408
  export function exposeHelp(): string {
371
409
  return `parachute expose — route your services behind HTTPS on a network layer
372
410
 
package/src/rate-limit.ts CHANGED
@@ -87,6 +87,21 @@ export const CHANGE_PASSWORD_WINDOW_MS = 5 * 60 * 1000;
87
87
  * cookie attacker shouldn't get a 5-shot grind window.
88
88
  */
89
89
  export const CHANGE_PASSWORD_MAX_ATTEMPTS = 3;
90
+ /**
91
+ * `/api/account/2fa/confirm` (TOTP enrollment seal) window: 15 minutes. This is
92
+ * the SELF-only, already-session-authenticated enrollment step — the operator is
93
+ * typing the first live code off their own authenticator while it drifts into
94
+ * sync, so legitimate mistypes are common and must not be punished. The threat
95
+ * is only a hijacked session grinding the (client-held, not-yet-persisted)
96
+ * in-flight secret, which the 10^6 code space + replay cache already make
97
+ * effectively non-exploitable — so this is defense-in-depth, deliberately MORE
98
+ * generous than the 3/5-min change-password bucket. NOT the `/login/2fa` bucket:
99
+ * that one is the strict, pre-auth brute-force door (5/15-min); enrollment is a
100
+ * different, lower-risk surface and gets its own lenient bucket.
101
+ */
102
+ export const TOTP_ENROLL_CONFIRM_WINDOW_MS = 15 * 60 * 1000;
103
+ /** `/api/account/2fa/confirm` attempts allowed per window. 11th is denied. */
104
+ export const TOTP_ENROLL_CONFIRM_MAX_ATTEMPTS = 10;
90
105
  /**
91
106
  * `/login/2fa` window length: 15 minutes — same as `/login`. The second-
92
107
  * factor step (hub#473) sits behind a verified password + a short-lived
@@ -289,6 +304,18 @@ export const changePasswordRateLimiter = new RateLimiter(
289
304
  */
290
305
  export const totpRateLimiter = new RateLimiter(TOTP_MAX_ATTEMPTS, TOTP_WINDOW_MS);
291
306
 
307
+ /**
308
+ * `/api/account/2fa/confirm` enrollment-seal bucket. Lenient (10 / 15 min),
309
+ * keyed by `user.id` (the session already establishes identity). Separate from
310
+ * `totpRateLimiter` so an enrollment mistype and a `/login/2fa` failure never
311
+ * share a window — different surfaces, different threat models (see the const
312
+ * docs above).
313
+ */
314
+ export const totpEnrollConfirmRateLimiter = new RateLimiter(
315
+ TOTP_ENROLL_CONFIRM_MAX_ATTEMPTS,
316
+ TOTP_ENROLL_CONFIRM_WINDOW_MS,
317
+ );
318
+
292
319
  /**
293
320
  * Coarse per-IP CEILING rate limiter — 60 attempts / 15 min, keyed by client
294
321
  * IP ONLY. Shared by all interactive auth doors (`/login`, the
@@ -342,6 +369,7 @@ export function __resetForTests(): void {
342
369
  loginRateLimiter.reset();
343
370
  changePasswordRateLimiter.reset();
344
371
  totpRateLimiter.reset();
372
+ totpEnrollConfirmRateLimiter.reset();
345
373
  vaultTokenMintRateLimiter.reset();
346
374
  signupRateLimiter.reset();
347
375
  authIpCeilingRateLimiter.reset();