// shared/types.ts
var SENTINEL_ORIGIN = "https://blackhorsesentinel.com";

// shared/api/client.ts
var SentinelApiError = class extends Error {
  constructor(message, code, status) {
    super(message);
    this.code = code;
    this.status = status;
    this.name = "SentinelApiError";
  }
};
var SentinelClient = class {
  constructor(config = {}) {
    this.config = config;
    this.baseUrl = (config.baseUrl ?? SENTINEL_ORIGIN).replace(/\/$/, "");
  }
  baseUrl;
  setAccessToken(token) {
    this.config.accessToken = token;
  }
  setApiKey(key) {
    this.config.apiKey = key;
  }
  authExtensionUrl(redirectUri) {
    const params = new URLSearchParams({ redirect_uri: redirectUri });
    return `${this.baseUrl}/auth/extension?${params.toString()}`;
  }
  async exchangeAuthCode(code) {
    const res = await fetch(`${this.baseUrl}/api/auth/extension/token`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ code })
    });
    return parseJson(res);
  }
  async refreshAuth(refreshToken) {
    const res = await fetch(`${this.baseUrl}/api/auth/extension/refresh`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ refreshToken })
    });
    return parseJson(res);
  }
  async capture(payload) {
    const res = await fetch(`${this.baseUrl}/api/v1/capture`, {
      method: "POST",
      headers: this.headers(true),
      body: JSON.stringify(payload)
    });
    return parseJson(res);
  }
  async analyze(payload) {
    const res = await fetch(`${this.baseUrl}/api/v1/analyze`, {
      method: "POST",
      headers: this.headers(false),
      body: JSON.stringify(payload)
    });
    return parseJson(res);
  }
  async analyzeWithBestAuth(payload) {
    if (this.config.accessToken) {
      try {
        return await this.capture(payload);
      } catch (e) {
        if (!(e instanceof SentinelApiError) || e.status !== 401) throw e;
      }
    }
    if (this.config.apiKey) {
      return this.analyze(payload);
    }
    throw new SentinelApiError("Connect your Sentinel account.", "unauthorized", 401);
  }
  headers(oauthPreferred) {
    const headers = { "Content-Type": "application/json" };
    if (oauthPreferred && this.config.accessToken) {
      headers.Authorization = `Bearer ${this.config.accessToken}`;
    } else if (this.config.apiKey) {
      headers.Authorization = `Bearer ${this.config.apiKey}`;
    } else if (this.config.accessToken) {
      headers.Authorization = `Bearer ${this.config.accessToken}`;
    }
    return headers;
  }
};
async function parseJson(res) {
  let data;
  try {
    data = await res.json();
  } catch {
    throw new SentinelApiError("Invalid response from Sentinel.", "bad_response", res.status);
  }
  if (!res.ok) {
    throw new SentinelApiError(data.error ?? "Request failed", data.code ?? "error", res.status);
  }
  return data;
}
function reportUrl(baseUrl, reportId) {
  return `${baseUrl.replace(/\/$/, "")}/dashboard/reports/${reportId}`;
}

// shared/publishers.ts
var PAYWALLED_PUBLISHERS = {
  "bloomberg.com": "Bloomberg",
  "wsj.com": "Wall Street Journal",
  "ft.com": "Financial Times",
  "economist.com": "The Economist"
};
var OUTLET_BY_HOST = {
  ...PAYWALLED_PUBLISHERS,
  "federalreserve.gov": "Federal Reserve",
  "reuters.com": "Reuters",
  "npr.org": "NPR",
  "bbc.com": "BBC",
  "theguardian.com": "The Guardian"
};
function normalizeHost(hostname) {
  return hostname.toLowerCase().replace(/^www\./, "");
}
function publisherLabel(hostname) {
  const host = normalizeHost(hostname);
  for (const [domain, label] of Object.entries(PAYWALLED_PUBLISHERS)) {
    if (host === domain || host.endsWith(`.${domain}`)) return label;
  }
  return null;
}
function isSupportedPaywallHost(hostname) {
  return publisherLabel(hostname) !== null;
}

// chrome/background.ts
var STORAGE_KEYS = {
  accessToken: "sentinel_access_token",
  refreshToken: "sentinel_refresh_token",
  expiresAt: "sentinel_expires_at",
  apiKey: "sentinel_api_key",
  autoAnalyze: "sentinel_auto_analyze",
  recentReports: "sentinel_recent_reports",
  baseUrl: "sentinel_base_url"
};
async function loadState() {
  const raw = await chrome.storage.local.get(Object.values(STORAGE_KEYS));
  return {
    accessToken: raw[STORAGE_KEYS.accessToken],
    refreshToken: raw[STORAGE_KEYS.refreshToken],
    expiresAt: raw[STORAGE_KEYS.expiresAt],
    apiKey: raw[STORAGE_KEYS.apiKey],
    autoAnalyze: raw[STORAGE_KEYS.autoAnalyze],
    recentReports: raw[STORAGE_KEYS.recentReports],
    baseUrl: raw[STORAGE_KEYS.baseUrl]
  };
}
async function saveTokens(tokens) {
  await chrome.storage.local.set({
    [STORAGE_KEYS.accessToken]: tokens.accessToken,
    [STORAGE_KEYS.refreshToken]: tokens.refreshToken,
    [STORAGE_KEYS.expiresAt]: tokens.expiresAt
  });
}
async function getClient() {
  const state = await loadState();
  const baseUrl = state.baseUrl ?? SENTINEL_ORIGIN;
  const client = new SentinelClient({
    baseUrl,
    accessToken: state.accessToken ?? null,
    apiKey: state.apiKey ?? null
  });
  if (state.accessToken && state.refreshToken && state.expiresAt && Date.now() > state.expiresAt - 6e4) {
    try {
      const refreshed = await client.refreshAuth(state.refreshToken);
      await saveTokens(refreshed);
      client.setAccessToken(refreshed.accessToken);
    } catch {
      await chrome.storage.local.remove([STORAGE_KEYS.accessToken, STORAGE_KEYS.expiresAt]);
      client.setAccessToken(null);
    }
  }
  return client;
}
async function pushRecent(report) {
  const state = await loadState();
  const existing = state.recentReports ?? [];
  const next = [report, ...existing.filter((r) => r.reportId !== report.reportId)].slice(0, 5);
  await chrome.storage.local.set({ [STORAGE_KEYS.recentReports]: next });
}
async function analyzePayload(payload) {
  const state = await loadState();
  const baseUrl = state.baseUrl ?? SENTINEL_ORIGIN;
  const client = await getClient();
  const result = await client.analyzeWithBestAuth(payload);
  await pushRecent({
    reportId: result.reportId,
    title: payload.title,
    tier: result.tier,
    driftScore: result.driftScore,
    createdAt: Date.now()
  });
  return result;
}
function supportsChromeIdentity() {
  return Boolean(chrome.identity?.getRedirectURL && chrome.identity?.launchWebAuthFlow);
}
function notifySuccess(title, result, baseUrl) {
  const drift = result.driftScore != null ? ` \xB7 Drift ${result.driftScore.toFixed(2)}` : "";
  const tier = result.tier ? `Tier ${result.tier}` : "Analyzed";
  try {
    if (chrome.notifications?.create) {
      chrome.notifications.create({
        type: "basic",
        iconUrl: "icons/icon128.png",
        title: "Blackhorse Sentinel",
        message: `${tier}${drift} \u2014 ${title.slice(0, 60)}`
      });
    }
  } catch {
  }
  chrome.tabs.create({ url: reportUrl(baseUrl, result.reportId) });
}
async function connectOauthViaTab(baseUrl) {
  const redirectUri = `${baseUrl.replace(/\/$/, "")}/auth/extension/safari-callback`;
  const client = new SentinelClient({ baseUrl });
  const authUrl = client.authExtensionUrl(redirectUri);
  const tab = await chrome.tabs.create({ url: authUrl, active: true });
  if (!tab.id) throw new Error("Could not open sign-in tab.");
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      chrome.tabs.onUpdated.removeListener(onUpdated);
      reject(new Error("Sign-in timed out. Try Connect account again."));
    }, 5 * 6e4);
    function onUpdated(tabId, changeInfo) {
      if (tabId !== tab.id || !changeInfo.url) return;
      if (!changeInfo.url.startsWith(redirectUri)) return;
      try {
        const code = new URL(changeInfo.url).searchParams.get("code");
        chrome.tabs.onUpdated.removeListener(onUpdated);
        clearTimeout(timeout);
        void chrome.tabs.remove(tabId).catch(() => void 0);
        if (!code) {
          reject(new Error("No authorization code returned."));
          return;
        }
        resolve(code);
      } catch (e) {
        chrome.tabs.onUpdated.removeListener(onUpdated);
        clearTimeout(timeout);
        reject(e instanceof Error ? e : new Error("Sign-in failed"));
      }
    }
    chrome.tabs.onUpdated.addListener(onUpdated);
  });
}
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: "sentinel-analyze-page",
    title: "Analyze with Blackhorse Sentinel",
    contexts: ["page"]
  });
  chrome.contextMenus.create({
    id: "sentinel-analyze-selection",
    title: "Analyze selection with Blackhorse Sentinel",
    contexts: ["selection"]
  });
});
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
  if (!tab?.id) return;
  if (info.menuItemId === "sentinel-analyze-page") {
    await chrome.tabs.sendMessage(tab.id, { type: "EXTRACT_PAGE" });
  }
  if (info.menuItemId === "sentinel-analyze-selection" && info.selectionText) {
    await chrome.tabs.sendMessage(tab.id, { type: "EXTRACT_SELECTION", selectionText: info.selectionText });
  }
});
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  void (async () => {
    try {
      if (message.type === "GET_STATE") {
        const state = await loadState();
        sendResponse({
          ok: true,
          connected: Boolean(state.accessToken || state.apiKey),
          hasOAuth: Boolean(state.accessToken),
          hasApiKey: Boolean(state.apiKey),
          autoAnalyze: Boolean(state.autoAnalyze),
          recentReports: state.recentReports ?? [],
          baseUrl: state.baseUrl ?? SENTINEL_ORIGIN
        });
        return;
      }
      if (message.type === "SET_API_KEY") {
        await chrome.storage.local.set({ [STORAGE_KEYS.apiKey]: message.apiKey || null });
        sendResponse({ ok: true });
        return;
      }
      if (message.type === "SET_AUTO_ANALYZE") {
        await chrome.storage.local.set({ [STORAGE_KEYS.autoAnalyze]: Boolean(message.enabled) });
        sendResponse({ ok: true });
        return;
      }
      if (message.type === "DISCONNECT") {
        await chrome.storage.local.remove([
          STORAGE_KEYS.accessToken,
          STORAGE_KEYS.refreshToken,
          STORAGE_KEYS.expiresAt,
          STORAGE_KEYS.apiKey
        ]);
        sendResponse({ ok: true });
        return;
      }
      if (message.type === "CONNECT_OAUTH") {
        const state = await loadState();
        const baseUrl = state.baseUrl ?? SENTINEL_ORIGIN;
        const client = new SentinelClient({ baseUrl });
        let code;
        if (supportsChromeIdentity()) {
          const redirectUri = chrome.identity.getRedirectURL("callback");
          const authUrl = client.authExtensionUrl(redirectUri);
          const responseUrl = await chrome.identity.launchWebAuthFlow({
            url: authUrl,
            interactive: true
          });
          if (!responseUrl) throw new Error("Sign-in was cancelled.");
          code = new URL(responseUrl).searchParams.get("code") ?? "";
        } else {
          code = await connectOauthViaTab(baseUrl);
        }
        if (!code) throw new Error("No authorization code returned.");
        const tokens = await client.exchangeAuthCode(code);
        await saveTokens(tokens);
        sendResponse({ ok: true });
        return;
      }
      if (message.type === "ANALYZE_TAB") {
        const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
        if (!tab?.id) throw new Error("No active tab.");
        await chrome.tabs.sendMessage(tab.id, { type: "EXTRACT_PAGE" });
        sendResponse({ ok: true });
        return;
      }
      if (message.type === "ANALYZE_SCREENSHOT") {
        const payload = message.payload;
        if (!payload?.text || payload.text.length < 40) {
          throw new Error("Screenshot text must be at least 40 characters.");
        }
        const state = await loadState();
        if (!state.accessToken && !state.apiKey) {
          throw new Error("Connect your Sentinel account first.");
        }
        const baseUrl = state.baseUrl ?? SENTINEL_ORIGIN;
        const result = await analyzePayload({
          sourceUrl: payload.sourceUrl || "screenshot://local",
          title: payload.title || "Screenshot",
          text: payload.text,
          sourceOutlet: payload.sourceOutlet || "Social screenshot",
          voiceSegment: payload.voiceSegment || "journalist_obligation",
          contentKind: payload.contentKind || "screenshot",
          author: payload.author
        });
        notifySuccess(payload.title || "Screenshot", result, baseUrl);
        sendResponse({ ok: true, result });
        return;
      }
      if (message.type === "CAPTURE_RESULT") {
        const outcome = message.outcome;
        if (!outcome.ok) {
          sendResponse({ ok: false, error: outcome.message, code: outcome.code });
          return;
        }
        const state = await loadState();
        const baseUrl = state.baseUrl ?? SENTINEL_ORIGIN;
        const result = await analyzePayload(outcome.payload);
        notifySuccess(outcome.payload.title, result, baseUrl);
        sendResponse({ ok: true, result });
        return;
      }
      if (message.type === "TAB_UPDATED") {
        const state = await loadState();
        if (!state.autoAnalyze) {
          sendResponse({ ok: true });
          return;
        }
        const hostname = message.hostname;
        if (!isSupportedPaywallHost(hostname)) {
          sendResponse({ ok: true });
          return;
        }
        sendResponse({ ok: true, badge: "!" });
        return;
      }
      sendResponse({ ok: false, error: "Unknown message" });
    } catch (e) {
      sendResponse({ ok: false, error: e instanceof Error ? e.message : "Failed" });
    }
  })();
  return true;
});
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
  if (changeInfo.status !== "complete" || !tab.url) return;
  try {
    const host = normalizeHost(new URL(tab.url).hostname);
    if (isSupportedPaywallHost(host)) {
      chrome.action.setBadgeText({ tabId, text: "\u25CF" });
      chrome.action.setBadgeBackgroundColor({ tabId, color: "#0a0a0a" });
    } else {
      chrome.action.setBadgeText({ tabId, text: "" });
    }
  } catch {
  }
});
//# sourceMappingURL=background.js.map
