/**
* mcp/skills/getCachedCredentialsCount.ts — get_cached_credentials_count
*
* Reports per-domain cached credential counts in macOS Keychain (login)
* or Windows Credential Manager without modifying anything. Read-only
* counterpart to `purge_cached_credentials`.
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/getCachedCredentialsCount.ts okta.com
*
* NOTE: enumeration logic is duplicated from `purgeCachedCredentials.ts`
* (the dry-run "found" branch) — keep in sync if the `security` /
* `cmdkey` enumeration commands change.
*/
import { exec } from "child_process" ;
import { promisify } from "util" ;
import { z } from "zod" ;
import { isDarwin, isWin32 } from "./_shared/platform" ;
const execAsync = promisify (exec);
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "get_cached_credentials_count" ,
description:
"Reports per-domain cached credential counts in macOS Keychain (login) " +
"or Windows Credential Manager without modifying anything. Read-only " +
"counterpart to purge_cached_credentials." ,
riskLevel: "low" ,
destructive: false ,
requiresConsent: false ,
supportsDryRun: false ,
affectedScope: [ "user" ],
auditRequired: false ,
tccCategories: [],
schema: {
domains: z
. array (z. string (). min ( 1 ))
. min ( 1 )
. refine (
( arr ) => arr. every (( d ) => ! / [*?] / . test (d)),
{ message: "Wildcard domains are not permitted — supply exact host strings." },
)
. refine (
( arr ) => arr. every (( d ) => / ^ [a-zA-Z0-9. \- ] +$ / . test (d)),
{ message: "Domains must contain only letters, digits, dots and dashes." },
)
. describe ( "Exact domain strings to probe." ),
},
} as const ;
// -- Types --------------------------------------------------------------------
export interface DomainCredentialCount {
domain : string ;
count : number ;
sample ?: string [];
error ?: string ;
}
export interface GetCachedCredentialsCountResult {
platform : NodeJS . Platform | "other" ;
domains : DomainCredentialCount [];
totalCount : number ;
}
// -- Helpers ------------------------------------------------------------------
function shellQuote ( s : string ) : string {
return `'${ s . replace ( / ' / g , `' \\ ''` ) }'` ;
}
async function countDarwin ( domain : string ) : Promise < DomainCredentialCount > {
const sample : string [] = [];
let count = 0 ;
try {
const { stdout } = await execAsync (
`security find-internet-password -s ${ shellQuote ( domain ) } 2>&1 || true` ,
{ maxBuffer: 2 * 1024 * 1024 , timeout: 5_000 },
);
const hits = stdout. match ( / "acct"<blob>=" [ ^ "] * " / g ) ?? [];
count += hits. length ;
sample. push ( ... hits. slice ( 0 , 5 ). map (( s ) => s. replace ( / "acct"<blob>= / , "" )));
} catch { /* no match */ }
try {
const { stdout } = await execAsync (
`security find-generic-password -s ${ shellQuote ( domain ) } 2>&1 || true` ,
{ maxBuffer: 2 * 1024 * 1024 , timeout: 5_000 },
);
const hits = stdout. match ( / "acct"<blob>=" [ ^ "] * " / g ) ?? [];
count += hits. length ;
sample. push ( ... hits. slice ( 0 , 5 ). map (( s ) => s. replace ( / "acct"<blob>= / , "" )));
} catch { /* no match */ }
return { domain, count, sample: sample. slice ( 0 , 10 ) };
}
async function countWin32 ( domain : string ) : Promise < DomainCredentialCount > {
try {
const { stdout } = await execAsync ( `cmdkey /list` , {
maxBuffer: 2 * 1024 * 1024 , timeout: 5_000 ,
});
const lines = stdout. split ( / \r ? \n / );
const targets : string [] = [];
for ( const line of lines) {
const m = line. match ( / ^ \s * Target: \s * ( . +? ) \s *$ / );
if ( ! m) continue ;
const raw = m[ 1 ]. trim ();
const target = raw. replace ( / ^ [ ^ :] + :target= / , "" );
if (target. toLowerCase (). includes (domain. toLowerCase ())) {
targets. push (raw);
}
}
return { domain, count: targets. length , sample: targets. slice ( 0 , 10 ) };
} catch (err) {
return { domain, count: 0 , error: (err as Error ).message };
}
}
// -- Exported run -------------------------------------------------------------
export async function run ({
domains ,
} : { domains : string [] }) : Promise < GetCachedCredentialsCountResult > {
const platform : "darwin" | "win32" | "other" =
isDarwin () ? "darwin" : isWin32 () ? "win32" : "other" ;
const results : DomainCredentialCount [] = [];
for ( const domain of domains) {
if (platform === "darwin" ) {
results. push ( await countDarwin (domain));
} else if (platform === "win32" ) {
results. push ( await countWin32 (domain));
} else {
results. push ({ domain, count: 0 , error: "Unsupported platform" });
}
}
const totalCount = results. reduce (( s , r ) => s + r.count, 0 );
return { platform, domains: results, totalCount };
}
// -- CLI smoke test -----------------------------------------------------------
if (require.main === module ) {
const argv = process.argv. slice ( 2 );
const domains = argv. length > 0 ? argv : [ "okta.com" ];
run ({ domains })
. then (( r ) => console. log ( JSON . stringify (r, null , 2 )))
. catch (( err : Error ) => { console. error (err.message); process. exit ( 1 ); });
}