← back to Kickbacks Ai Upstream

src/killswitch/client.ts

59 lines

import { timeoutFetch } from "../util/http";

type Fetch = typeof fetch;
export interface KillState {
  killed: boolean;
  /** wave-2 kill hysteresis (audit #3/#6/#9): true ONLY when an HTTP 200
   *  actually said killed:true. A confirmed kill is the restore-everything
   *  signal; an UNCONFIRMED kill (offline fail-safe) must FREEZE instead —
   *  no restore, no new writes — so a wifi blip never starts the
   *  restore→re-patch oscillation on the user's Claude Code install. */
  confirmed: boolean;
  scope?: string;
  reason?: string;
  /** wave-2A-F06: distinguishes a fail-safe "treated as killed because
   *  unreachable" from a real backend killed: true response. Callers can
   *  render an "offline" status (status bar already has the `kind:"offline"`
   *  variant unused pre-fix) instead of the alarming "killed" badge that
   *  used to flicker on every brief network blip. Offline still implies
   *  killed for the purposes of "is it safe to serve an ad" (fail-safe
   *  posture for NEW writes) — but never `confirmed`, so it never restores. */
  offline?: boolean;
}

/** Polls GET /v1/killswitch. Fail-safe: any error => killed (matches the S2
 *  resolve_killed posture — never serve under a kill). When the error was
 *  unreachability rather than a real backend kill, `offline: true` is set
 *  (and `confirmed` stays false) so the caller can FREEZE rather than
 *  restore, and the status bar can render distinct UX. */
export class KillSwitchClient {
  constructor(private base: string, private f: Fetch = timeoutFetch(15000)) {}

  async checkOnce(ccVersion: string, campaignId: string): Promise<KillState> {
    try {
      const r = await this.f(
        `${this.base}/v1/killswitch?version=${encodeURIComponent(ccVersion)}` +
        `&campaign=${encodeURIComponent(campaignId)}`);
      if (!r.ok) {
        // 5xx or 4xx -> offline-equivalent (backend reachable but not
        // returning the contract). Still fail-safe killed, never confirmed.
        return { killed: true, confirmed: false, offline: true,
                 reason: `status ${r.status}` };
      }
      const j = await r.json() as KillState;
      return {
        killed: !!j.killed,
        confirmed: !!j.killed,
        scope: j.scope,
        reason: j.reason,
        offline: false,
      };
    } catch (e) {
      // Network/DNS error -> truly offline. Caller freezes; kill posture
      // preserved for NEW writes only.
      return { killed: true, confirmed: false, offline: true,
               reason: `fail-safe: ${String(e)}` };
    }
  }
}