Code

/**
 * mcp/skills/getBrowserSsoCookiesInfo.ts — get_browser_sso_cookies_info
 *
 * Reports per-browser cookie counts matching an IDP domain without
 * modifying anything.  Read-only counterpart to `clear_browser_sso_cookies`.
 *
 * Smoke test
 *   npx tsx -r dotenv/config mcp/skills/getBrowserSsoCookiesInfo.ts okta.com
 *
 * NOTE: SQL predicate logic is duplicated from `clearBrowserSsoCookies.ts`
 * — keep in sync if cookie host_key matching changes.
 */
 
import { exec }       from "child_process";
import { promisify }  from "util";
import * as fs        from "fs";
import { z }          from "zod";
 
import { listCookieStores, type CookieStore, type Browser } from "./_shared/browser";
import { isDarwin } from "./_shared/platform";
 
const execAsync = promisify(exec);
 
// -- Meta ---------------------------------------------------------------------
 
export const meta = {
  name: "get_browser_sso_cookies_info",
  description:
    "Reports per-browser cookie counts matching an IDP domain without " +
    "deleting anything. Read-only counterpart to clear_browser_sso_cookies.",
  riskLevel:       "low",
  destructive:     false,
  requiresConsent: false,
  supportsDryRun:  false,
  affectedScope:   ["user"],
  auditRequired:   false,
  tccCategories:   [],
  schema: {
    domain: z
      .string()
      .min(1)
      .refine(
        (v) => !/[*?]/.test(v),
        { message: "Wildcard domains are not permitted — supply an exact host string." },
      )
      .refine(
        (v) => /^[a-zA-Z0-9.\-]+$/.test(v),
        { message: "Domain must contain only letters, digits, dots and dashes." },
      )
      .describe("Exact host string for the IDP domain (e.g. 'okta.com')."),
  },
} as const;
 
// -- Types --------------------------------------------------------------------
 
export interface SsoCookieStoreInfo {
  browser:        Browser;
  profile:        string;
  path:           string;
  matchedCookies: number;
  skipped?:       boolean;
  error?:         string;
}
 
export interface GetBrowserSsoCookiesInfoResult {
  domain:        string;
  profiles:      SsoCookieStoreInfo[];
  totalMatched:  number;
  missingSqlite?: boolean;
}
 
// -- Helpers ------------------------------------------------------------------
 
async function isSqlite3OnPath(): Promise<boolean> {
  try {
    await execAsync("sqlite3 -version", { timeout: 3_000 });
    return true;
  } catch {
    return false;
  }
}
 
function buildHostPredicate(column: string, domain: string): string {
  const esc = domain.replace(/'/g, "''");
  return (
    `(${column} = '${esc}' OR ${column} = '.${esc}' ` +
    `OR ${column} LIKE '%.${esc}' OR ${column} LIKE '%.${esc}.')`
  );
}
 
async function sqliteCount(dbPath: string, query: string): Promise<number> {
  const { stdout } = await execAsync(
    `sqlite3 ${JSON.stringify(dbPath)} ${JSON.stringify(query)}`,
    { maxBuffer: 1 * 1024 * 1024, timeout: 5_000 },
  );
  const n = parseInt(stdout.trim(), 10);
  return isNaN(n) ? 0 : n;
}
 
async function countSqliteStore(store: CookieStore, domain: string): Promise<SsoCookieStoreInfo> {
  const column = store.browser === "firefox" ? "host" : "host_key";
  const predicate = buildHostPredicate(column, domain);
  const countSql  = `SELECT COUNT(*) FROM cookies WHERE ${predicate};`;
  try {
    if (!fs.existsSync(store.path)) {
      return {
        browser: store.browser, profile: store.profile, path: store.path,
        matchedCookies: 0, skipped: true,
        error: "Cookies database does not exist",
      };
    }
    const matched = await sqliteCount(store.path, countSql);
    return {
      browser: store.browser, profile: store.profile, path: store.path,
      matchedCookies: matched,
    };
  } catch (err) {
    return {
      browser: store.browser, profile: store.profile, path: store.path,
      matchedCookies: 0, error: `sqlite error: ${(err as Error).message}`,
    };
  }
}
 
function safariUnsupported(store: CookieStore): SsoCookieStoreInfo {
  return {
    browser: store.browser, profile: store.profile, path: store.path,
    matchedCookies: 0, skipped: true,
    error: isDarwin()
      ? "Safari cookie count is unavailable in read-only mode (binary cookie format)."
      : "Safari cookie store is macOS-only.",
  };
}
 
// -- Exported run -------------------------------------------------------------
 
export async function run({
  domain,
}: { domain: string }): Promise<GetBrowserSsoCookiesInfoResult> {
  const stores = listCookieStores();
  const sqliteOk = await isSqlite3OnPath();
 
  const profiles: SsoCookieStoreInfo[] = [];
  let totalMatched = 0;
 
  for (const s of stores) {
    if (s.format === "sqlite") {
      if (!sqliteOk) {
        profiles.push({
          browser: s.browser, profile: s.profile, path: s.path,
          matchedCookies: 0, skipped: true,
          error: "sqlite3 CLI is not available on PATH — cannot count SQLite-backed cookies.",
        });
        continue;
      }
      const r = await countSqliteStore(s, domain);
      totalMatched += r.matchedCookies;
      profiles.push(r);
    } else {
      profiles.push(safariUnsupported(s));
    }
  }
 
  return {
    domain, profiles, totalMatched,
    missingSqlite: sqliteOk ? undefined : true,
  };
}
 
// -- CLI smoke test -----------------------------------------------------------
 
if (require.main === module) {
  const domain = process.argv[2] ?? "okta.com";
  run({ domain })
    .then((r) => console.log(JSON.stringify(r, null, 2)))
    .catch((err: Error) => { console.error(err.message); process.exit(1); });
}