#!/usr/bin/env node
import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
const ROOT = process.cwd();
const TARGETS = [
'cli.js',
'dist',
'skills',
'CLAUDE.md',
'README.md',
];
const BINARY_EXTENSIONS = new Set([
'.png',
'.jpg',
'.jpeg',
'.gif',
'.webp',
'.ico',
'.zip',
'.gz',
'.tgz',
'.woff',
'.woff2',
'.ttf',
'.otf',
]);
const PATTERNS = [
{ label: 'Google API key', regex: /\bAIza[0-9A-Za-z_-]{35}\b/g },
{ label: 'Ice Puzzle private key', regex: /\bipk_[0-9A-Za-z_-]{20,}\b/g },
];
function isBinaryPath(filePath) {
const lowercase = filePath.toLowerCase();
for (const extension of BINARY_EXTENSIONS) {
if (lowercase.endsWith(extension)) {
return true;
}
}
return false;
}
function listFiles(pathName, out) {
if (!existsSync(pathName)) {
return;
}
const stat = lstatSync(pathName);
if (stat.isDirectory()) {
for (const child of readdirSync(pathName)) {
listFiles(join(pathName, child), out);
}
return;
}
if (stat.isFile()) {
out.push(pathName);
}
}
function scanFile(filePath) {
if (isBinaryPath(filePath)) {
return [];
}
let text = '';
try {
text = readFileSync(filePath, 'utf8');
} catch {
return [];
}
const findings = [];
for (const pattern of PATTERNS) {
const matches = text.match(pattern.regex);
if (matches && matches.length > 0) {
findings.push({ label: pattern.label, count: matches.length });
}
}
return findings;
}
const files = [];
for (const target of TARGETS) {
listFiles(join(ROOT, target), files);
}
const findingLines = [];
for (const filePath of files) {
const findings = scanFile(filePath);
for (const finding of findings) {
findingLines.push(`- ${finding.label}: ${filePath} (${finding.count})`);
}
}
if (findingLines.length > 0) {
console.error('Security scan failed. Potential secret material detected in publish artifacts:');
for (const line of findingLines) {
console.error(line);
}
process.exit(1);
}
console.log('Security scan passed: no API key patterns found in publish artifacts.');