UNPKG

@protocol.land/git-remote-helper

Version:
1,185 lines (1,167 loc) 34.5 kB
#!/usr/bin/env node // src/lib/arweaveHelper.ts import { ArweaveSigner, bundleAndSignData, createData } from "arbundles"; import readline from "readline"; import fs, { promises as fsPromises } from "fs"; // src/lib/withAsync.ts async function withAsync(fn) { try { if (typeof fn !== "function") throw new Error("The first argument must be a function"); const response = await fn(); return { response, error: null }; } catch (error) { return { error, response: null }; } } // src/lib/arweaveHelper.ts async function getAddress(wallet2) { return await initArweave().wallets.jwkToAddress( wallet2 ? wallet2 : getWallet() ); } function getActivePublicKey() { const wallet2 = getWallet(); if (!wallet2) { process.exit(0); } return wallet2.n; } async function checkAccessToTty() { try { await fsPromises.access( "/dev/tty", fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK ); return true; } catch (err) { return false; } } function createTtyReadlineInterface() { const ttyReadStream = fs.createReadStream("/dev/tty"); const ttyWriteStream = fs.createWriteStream("/dev/tty"); const rl = readline.createInterface({ input: ttyReadStream, output: ttyWriteStream }); return { rl, ttyReadStream, ttyWriteStream }; } function askQuestionThroughTty(question) { return new Promise((resolve, reject) => { const { rl, ttyReadStream, ttyWriteStream } = createTtyReadlineInterface(); rl.question(question, (answer) => { rl.close(); ttyReadStream.destroy(); ttyWriteStream.end(); ttyWriteStream.on("finish", () => { resolve(answer.trim().toLowerCase()); }); }); rl.on("error", (err) => reject(err)); ttyReadStream.on("error", (err) => reject(err)); ttyWriteStream.on("error", (err) => reject(err)); }); } var shouldPushChanges = async (uploadSize, uploadCost, subsidySize) => { let hasAccessToTty = await checkAccessToTty(); if (!hasAccessToTty) return true; const thresholdCost = getThresholdCost(); let showPushConsent; if (thresholdCost === null) { showPushConsent = uploadSize > subsidySize; } else if (uploadCost > thresholdCost) { showPushConsent = uploadSize > subsidySize; } else { showPushConsent = false; } if (!showPushConsent) return true; try { const answer = await askQuestionThroughTty(" [PL] Push? (y/n): "); return answer === "yes" || answer === "y"; } catch (err) { return true; } }; async function getTurboSubsidy() { const defaultSubsidy = 107520; try { const response = await fetch("https://turbo.ardrive.io/"); if (!response.ok) return defaultSubsidy; const data = await response.json(); const freeUploadLimitBytes = +data.freeUploadLimitBytes; if (Number.isFinite(freeUploadLimitBytes)) return freeUploadLimitBytes; return defaultSubsidy; } catch (err) { } return defaultSubsidy; } async function uploadRepo(zipBuffer, tags, uploadSize, uploadCost) { try { const uploadedTx = await subsidizedUpload(zipBuffer, tags); const serviceUsed = uploadedTx.bundled ? "Turbo" : "Arweave"; log(`Posted Tx to ${serviceUsed}: ${uploadedTx.data.repoTxId}`); return { txId: uploadedTx.data.repoTxId, pushCancelled: false }; } catch (error) { const userWantsToPay = await shouldUserPayForTx(); if (!userWantsToPay) { return { txid: "", pushCancelled: true }; } } const turboSubsidySize = await getTurboSubsidy(); const subsidySize = Math.max(turboSubsidySize, 0); const pushChanges = await shouldPushChanges( uploadSize, uploadCost, subsidySize ); async function attemptUpload(uploaderName, uploader) { if (pushChanges) { const txId = await uploader(zipBuffer, tags); log(`Posted Tx to ${uploaderName}: ${txId}`); return { txId, pushCancelled: false }; } return { txid: "", pushCancelled: true }; } try { return await attemptUpload("Turbo", turboUpload); } catch (error) { log("Turbo failed, trying with Arweave..."); return await attemptUpload("Arweave", arweaveUpload); } } async function arweaveDownload(txId) { const { response, error } = await withAsync( () => fetch(`https://arweave.net/${txId}`) ); if (error) { throw new Error(error); } else if (response) { return await response.arrayBuffer(); } } async function arweaveUpload(zipBuffer, tags) { const jwk = getWallet(); if (!jwk) throw "[ arweave ] No jwk wallet supplied"; const arweave2 = initArweave(); const dataSize = zipBuffer.length; const tx = await arweave2.createTransaction({ data: zipBuffer }, jwk); for (const tag of tags) tx.addTag(tag.name, tag.value); await arweave2.transactions.sign(tx, jwk); const response = await arweave2.transactions.post(tx); log(`${response.status} - ${response.statusText}`); if (response.status !== 200) { throw `[ arweave ] Posting repo to arweave failed. Error: '${response.status}' - '${response.statusText}' Check if you have plenty $AR to upload ~${Math.ceil( dataSize / 1024 )} KB of data.`; } return tx.id; } async function turboUpload(zipBuffer, tags) { const jwk = getWallet(); if (!jwk) throw "[ turbo ] No jwk wallet supplied"; const node = "https://turbo.ardrive.io"; const uint8ArrayZip = new Uint8Array(zipBuffer); const signer = new ArweaveSigner(jwk); const dataItem = createData(uint8ArrayZip, signer, { tags }); await dataItem.sign(signer); const res = await fetch(`${node}/tx`, { method: "POST", headers: { "Content-Type": "application/octet-stream" }, body: dataItem.getRaw() }); if (res.status >= 400) throw new Error( `[ turbo ] Posting repo with turbo failed. Error: ${res.status} - ${res.statusText}` ); return dataItem.id; } async function subsidizedUpload(zipBuffer, tags) { const jwk = getWallet(); if (!jwk) throw "[ turbo ] No jwk wallet supplied"; const node = "https://subsidize.saikranthi.dev/api/v1/postrepo"; const uint8ArrayZip = new Uint8Array(zipBuffer); const signer = new ArweaveSigner(jwk); const address = await getAddress(jwk); const dataItem = createData(uint8ArrayZip, signer, { tags }); await dataItem.sign(signer); const bundle = await bundleAndSignData([dataItem], signer); const res = await fetch(`${node}`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ txBundle: bundle.getRaw(), platform: "CLI", owner: address }) }); const upload = await res.json(); if (!upload || !upload.success) throw new Error( `[ turbo ] Posting repo with turbo failed. Error: ${res.status} - ${res.statusText}` ); return upload; } async function shouldUserPayForTx() { log("[ PL SUBSIDIZE ] Failed to subsidize this transaction."); try { const answer = await askQuestionThroughTty( " [PL] Would you like to pay for this transaction yourself? (y/n): " ); return answer === "yes" || answer === "y"; } catch (err) { return true; } } // src/lib/common.ts import { execSync } from "child_process"; import { accessSync, constants, readFileSync } from "fs"; import path from "path"; import Arweave from "arweave"; var ANSI_RESET = "\x1B[0m"; var ANSI_RED = "\x1B[31m"; var ANSI_GREEN = "\x1B[32m"; var DIRTY_EXT = ".tmp"; var PL_TMP_PATH = ".protocol.land"; var GIT_CONFIG_KEYFILE = "protocol.land.keyfile"; var GIT_CONFIG_THRESHOLD_COST = "protocol.land.thresholdCost"; var AOS_PROCESS_ID = "yJZ3_Yrc-qYRt1zHmY7YeNvpmQwuqyK3dT0-gxWftew"; var gitdir = process.env.GIT_DIR; function initArweave() { return Arweave.init({ host: "arweave.net", port: 443, protocol: "https" }); } var log = (message2, options) => { if (!options) console.error(` [PL] ${message2}`); else { const { color } = options; console.error( `${color === "red" ? ANSI_RED : ANSI_GREEN} [PL] ${message2}${ANSI_RESET}` ); } }; var wallet = null; var getJwkPath = () => { try { return execSync(`git config --get ${GIT_CONFIG_KEYFILE}`).toString().trim(); } catch (error) { return ""; } }; var getThresholdCost = () => { try { const threshold = execSync( `git config --get ${GIT_CONFIG_THRESHOLD_COST}` ).toString().trim(); if (threshold === "") return null; return +threshold; } catch (error) { return null; } }; var getWallet = (params = { warn: false }) => { if (wallet) return wallet; const jwkPath = getJwkPath(); if (!jwkPath) return walletNotFoundMessage(params); try { const jwk = readFileSync(jwkPath, { encoding: "utf-8" }).toString().trim(); if (!jwk) return walletNotFoundMessage(); wallet = JSON.parse(jwk); return wallet; } catch (error) { return walletNotFoundMessage(); } }; var walletNotFoundMessage = (params = { warn: false }) => { const { warn } = params; if (warn) { log( `If you need to push to the repo, please set up the path to your Arweave JWK.`, { color: "green" } ); } else { log(`Failed to get wallet keyfile path from git config.`); log( `You need an owner or contributor wallet to have write access to the repo.`, { color: "red" } ); } log( `Run 'git config --add ${GIT_CONFIG_KEYFILE} YOUR_WALLET_KEYFILE_FULL_PATH' to set it up`, { color: "green" } ); log( `Use '--global' to have a default keyfile for all Protocol Land repos`, { color: "green" } ); return null; }; var ownerOrContributor = async (repo, wallet2, options = { pushing: false }) => { const { pushing } = options; const address = await getAddress(wallet2); const ownerOrContrib = repo.owner === address || repo.contributors.some((contributor) => contributor === address); if (!ownerOrContrib) notOwnerOrContributorMessage({ warn: !pushing }); return ownerOrContrib; }; var notOwnerOrContributorMessage = (params = { warn: false }) => { const { warn } = params; if (warn) { log( `You are not the repo owner nor a contributor. You will need an owner or contributor jwk to push to this repo.`, { color: "green" } ); } else { log( `You are not the repo owner nor a contributor. You can't push to this repo.`, { color: "red" } ); } return null; }; async function getTags(title, description) { return [ { name: "App-Name", value: "Protocol.Land" }, { name: "Content-Type", value: "application/zip" }, { name: "Creator", value: await getAddress() }, { name: "Title", value: title }, { name: "Description", value: description }, { name: "Type", value: "repo-update" } ]; } function clearCache(cachePath, options) { const { keepFolders = [] } = options; const ommitedFolders = keepFolders.map((v) => `! -name "${v}"`).join(" "); execSync( `find ${cachePath} -mindepth 1 -maxdepth 1 -type d ${ommitedFolders} -exec rm -rf {} \\;` ); } function setCacheDirty(cachePath, remoteName2) { if (!cachePath || !remoteName2) throw new Error("Cache and MutexName are required"); execSync(`touch ${path.join(cachePath, remoteName2, DIRTY_EXT)}`); } function unsetCacheDirty(cachePath, remoteName2) { if (!cachePath || !remoteName2) throw new Error("Cache and MutexName are required"); execSync(`rm -f ${path.join(cachePath, remoteName2, DIRTY_EXT)}`); } function isCacheDirty(cachePath, remoteName2) { if (!cachePath || !remoteName2) throw new Error("Cache and MutexName are required"); try { accessSync( path.join(cachePath, remoteName2, DIRTY_EXT), constants.R_OK | constants.W_OK ); return true; } catch { return false; } } var waitFor = (delay) => new Promise((res) => setTimeout(res, delay)); function isValidUuid(uuid) { const REGEX = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; return typeof uuid === "string" && REGEX.test(uuid); } function setGitRemoteOrigin(repo) { try { const remoteUrl2 = process.argv[3]; if (!remoteUrl2) return; const repoId = `${remoteUrl2.replace(/.*:\/\//, "")}`; if (isValidUuid(repoId)) return; const repoPath = gitdir.split(path.sep).slice(0, -2).join(path.sep); const currentDir = process.cwd(); process.chdir(repoPath); execSync(`git remote set-url origin proland://${repo.id}`); process.chdir(currentDir); } catch (error) { } } // src/lib/remoteHelper.ts import { existsSync as existsSync2, mkdirSync } from "fs"; import { spawn as spawn2 } from "child_process"; import readline2 from "readline"; import { Writable } from "stream"; // src/lib/aoHelper.ts import { createDataItemSigner, dryrun, message, result } from "@permaweb/aoconnect"; function capitalizeFirstLetter(str) { return str.charAt(0).toUpperCase() + str.slice(1); } function getTags2(payload) { return Object.entries(payload).map( ([key, value]) => ({ name: capitalizeFirstLetter(key), value }) ); } function extractMessage(text) { const regex = /:\s*([^:!]+)!/; const match = text.match(regex); return match ? match[1].trim() : text; } async function sendMessage({ tags, data }) { const args = { process: AOS_PROCESS_ID, tags, signer: createDataItemSigner(getWallet()) }; if (data) args.data = data; const messageId = await message(args); const { Output } = await result({ message: messageId, process: AOS_PROCESS_ID }); if (Output?.data?.output) { throw new Error(extractMessage(Output?.data?.output)); } return messageId; } async function getRepo(id) { let Messages = []; const fields = JSON.stringify([ "id", "name", "description", "owner", "fork", "parent", "dataTxId", "contributors", "githubSync", "private", "privateStateTxId" ]); if (isValidUuid(id)) { ({ Messages } = await dryrun({ process: AOS_PROCESS_ID, tags: getTags2({ Action: "Get-Repo", Id: id, Fields: fields }) })); } else { const [username, repoName] = id.split("/"); if (!username || !repoName) return; ({ Messages } = await dryrun({ process: AOS_PROCESS_ID, tags: getTags2({ Action: "Get-Repo-By-Name-Username", "Repo-Name": repoName, Username: username, Fields: fields }) })); } if (Messages.length === 0) return void 0; return JSON.parse(Messages[0].Data)?.result; } async function updateRepo(repo, newDataTxId) { if (!repo.id || !repo.name || !newDataTxId) throw "[ AO ] No id, title or dataTxId to update repo "; await waitFor(500); await sendMessage({ tags: getTags2({ Action: "Update-Repo-TxId", Id: repo.id, "Data-TxId": newDataTxId }) }); return { id: repo.id }; } // src/lib/protocolLandSync.ts import { spawn } from "child_process"; // src/lib/zipHelper.ts import { promises as fsPromises2 } from "fs"; import path2 from "path"; import JSZip from "jszip"; import { exec } from "child_process"; async function unpackGitRepo({ destPath, arrayBuffer }) { const zip = await JSZip.loadAsync(arrayBuffer); const promises = []; for (const [_, file] of Object.entries(zip.files)) { if (file.dir) { const folderPath = path2.join(destPath, file.name); promises.push(fsPromises2.mkdir(folderPath, { recursive: true })); } else { promises.push( (async () => { const filePath = path2.join(destPath, file.name); const dirPath = path2.dirname(filePath); await fsPromises2.mkdir(dirPath, { recursive: true }); const content = await file.async("nodebuffer"); fsPromises2.writeFile(filePath, content); })() ); } } await Promise.all(promises); await waitFor(1e3); return true; } async function getGitTrackedFiles() { return new Promise((resolve, reject) => { exec("git ls-files", { encoding: "utf-8" }, (error, stdout) => { if (error) { reject(new Error("Error getting git tracked files")); } else { resolve(stdout.trim().split("\n")); } }); }); } async function zipRepoJsZip(mainPath, zipRoot, folderToZip, ignoreFiles) { if (!folderToZip) folderToZip = zipRoot; const ignoreFilesList = ignoreFiles ?? []; const filesToInclude = []; const walk = async (currentPath) => { const items = await fsPromises2.readdir(currentPath); for (const item of items) { const itemPath = path2.join(currentPath, item); if (ignoreFilesList.some( (ignorePath) => itemPath.startsWith(ignorePath) )) { continue; } const stats = await fsPromises2.stat(itemPath); if (stats.isDirectory()) { await walk(itemPath); } else { filesToInclude.push(itemPath); } } }; await walk(gitdir); const gitTrackedFiles = await getGitTrackedFiles(); filesToInclude.push(...gitTrackedFiles); const zip = new JSZip(); for (const file of filesToInclude) { const content = await fsPromises2.readFile(file); const relativePath = path2.join( mainPath ? mainPath + "/" : "", path2.relative(zipRoot, file) ); zip.file(relativePath, content); } return await zip.generateAsync({ type: "nodebuffer" }); } // src/lib/protocolLandSync.ts import path3 from "path"; import { existsSync, promises as fsPromises3 } from "fs"; // src/lib/privateRepo.ts import crypto from "crypto"; var arweave = initArweave(); async function deriveAddress(publicKey) { const pubKeyBuf = arweave.utils.b64UrlToBuffer(publicKey); const sha512DigestBuf = await crypto.subtle.digest("SHA-512", pubKeyBuf); return arweave.utils.bufferTob64Url(new Uint8Array(sha512DigestBuf)); } async function decryptAesKeyWithPrivateKey(encryptedAesKey) { const privateKey = getWallet(); const key = await crypto.subtle.importKey( "jwk", privateKey, { name: "RSA-OAEP", hash: "SHA-256" }, false, ["decrypt"] ); const options = { name: "RSA-OAEP", hash: "SHA-256" }; const decryptedAesKey = await crypto.subtle.decrypt( options, key, encryptedAesKey ); return new Uint8Array(decryptedAesKey); } async function decryptFileWithAesGcm(encryptedFile, decryptedAesKey, iv) { const aesKey = await crypto.subtle.importKey( "raw", decryptedAesKey, { name: "AES-GCM", length: 256 }, true, ["decrypt"] ); const decryptedFile = await crypto.subtle.decrypt( { name: "AES-GCM", iv }, aesKey, encryptedFile ); return decryptedFile; } async function decryptRepo(repoArrayBuf, privateStateTxId) { const response = await fetch(`https://arweave.net/${privateStateTxId}`); const privateState = await response.json(); const ivArrBuff = arweave.utils.b64UrlToBuffer(privateState.iv); const pubKey = getActivePublicKey(); const address = await deriveAddress(pubKey); const encAesKeyStr = privateState.encKeys[address]; const encAesKeyBuf = arweave.utils.b64UrlToBuffer(encAesKeyStr); const aesKey = await decryptAesKeyWithPrivateKey( encAesKeyBuf ); const decryptedRepo = await decryptFileWithAesGcm( repoArrayBuf, aesKey, ivArrBuff ); return decryptedRepo; } // src/lib/prices.ts import redstone from "redstone-api"; async function getArPrice() { try { const data = await (await fetch( `https://api.coingecko.com/api/v3/simple/price?ids=arweave&vs_currencies=usd` )).json(); return data.arweave.usd; } catch (e) { const response = await redstone.getPrice("AR"); if (!response.value) { return 0; } return response.source.coingecko; } } async function getWinstonPriceForBytes(bytes) { try { const response = await fetch(`https://arweave.net/price/${bytes}`); const winston = await response.text(); return +winston; } catch (error) { throw new Error(error); } } function formatBytes(bytes) { if (bytes === 0) return "0 Bytes"; const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; const k = 1024; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; } async function calculateEstimate(bytes) { const formattedSize = formatBytes(bytes); const costInWinston = await getWinstonPriceForBytes(bytes); const costInAR = +initArweave().ar.winstonToAr(costInWinston.toString()); const costFor1ARInUSD = await getArPrice(); const costInUSD = costInAR * costFor1ARInUSD; return { formattedSize, costInAR, costInARWithPrecision: costInAR.toPrecision(5), costInUSD, costInUSDWithPrecision: costInUSD.toPrecision(5) }; } // src/lib/protocolLandSync.ts var downloadProtocolLandRepo = async (repoId, destPath) => { log(`Getting latest repo from Protocol.Land into '${destPath}' ...`); let repo; try { repo = await getRepo(repoId); } catch (err) { log(err); } if (!repo) { log(`Repo '${repoId}' not found`, { color: "red" }); log(`Please create a repo in https://protocol.land first`, { color: "green" }); process.exit(0); } const latestVersionRepoPath = path3.join(destPath, repo.dataTxId); if (existsSync(latestVersionRepoPath)) { if (!isCacheDirty(destPath, repo.dataTxId)) { log(`Using cached repo in '${latestVersionRepoPath}'`); return repo; } clearCache(destPath, { keepFolders: [] }); } log(`Downloading from arweave with txId '${repo.dataTxId}' ...`); let arrayBuffer = await arweaveDownload(repo.dataTxId); if (!arrayBuffer) { log("Failed to fetch repo data from arweave.", { color: "red" }); log("Check connection or repo integrity in https://protocol.land", { color: "green" }); process.exit(0); } const isPrivate = repo?.private || false; const privateStateTxId = repo?.privateStateTxId; if (isPrivate && privateStateTxId) { arrayBuffer = await decryptRepo(arrayBuffer, privateStateTxId); } log(`Unpacking downloaded repo ...`); const status = await unpackGitRepo({ destPath, arrayBuffer }); if (!status) { log("Unpacking failed!", { color: "red" }); log("Check repo integrity in https://protocol.land", { color: "green" }); process.exit(0); } const unpackedRepoPath = path3.join(destPath, repo.id); const bareRepoPath = path3.join(destPath, repo.dataTxId); if (repo.fork && repo.parent) { const unpackedPath = path3.join(destPath, repo.parent); if (existsSync(unpackedPath)) { await fsPromises3.rename(unpackedPath, unpackedRepoPath); } } const cloned = await runCommand( "git", ["clone", "--bare", unpackedRepoPath, bareRepoPath], { forwardStdOut: true } ); if (!cloned) { log("Failed to prepare bare remote from unpacked repo!", { color: "red" }); log("Check repo integrity in https://protocol.land", { color: "green" }); process.exit(0); } try { clearCache(destPath, { keepFolders: [repo.dataTxId] }); } catch { } return repo; }; var uploadProtocolLandRepo = async (repoPath, repo, destPath) => { let dataTxId; let pushCancelled = false; try { log("Packing repo ...\n"); let buffer = await zipRepoJsZip(repo.id, repoPath, "", [ path3.join(gitdir, PL_TMP_PATH) ]); const isPrivate = repo?.private || false; const privateStateTxId = repo?.privateStateTxId; if (isPrivate && privateStateTxId) { throw new Error("Private repos are no longer supported."); } const bufferSize = Buffer.byteLength(buffer); const { costInAR, costInARWithPrecision, costInUSDWithPrecision, formattedSize } = await calculateEstimate(bufferSize); const spaces = " ".repeat(6); log( `Cost Estimates for push: ${spaces}Size: ${formattedSize} ${spaces}Cost: ~${costInARWithPrecision} AR (~${costInUSDWithPrecision} USD) `, { color: "green" } ); log("Uploading to Arweave ..."); ({ txId: dataTxId, pushCancelled } = await uploadRepo( buffer, await getTags(repo.name, repo.description), bufferSize, costInAR )); } catch (error) { log(error?.message || error, { color: "red" }); pushCancelled = false; } if (!dataTxId) return { success: false, pushCancelled }; log("Updating in AO ..."); const updated = await updateRepo(repo, dataTxId); return { success: updated.id === repo.id, pushCancelled }; }; var runCommand = async (command, args, options) => { log(`Running '${command} ${args.join(" ")}' ...`); const child = spawn(command, args, { shell: true, stdio: ["pipe", "pipe", "pipe"] }); return await new Promise((resolve, reject) => { child.on("error", reject); if (options?.forwardStdOut) { child.stdout.on("data", (data) => log); } child.on("close", (code) => { if (code === 0) { resolve(true); } else { log(`Command Failed. Exit code: ${code}`); resolve(false); } }); }); }; // src/lib/remoteHelper.ts import path4 from "path"; // src/lib/analytics.ts import { init as amplitudeInit, track as amplitudeTrack } from "@amplitude/analytics-node"; import machine from "node-machine-id"; var AMPLITUDE_TRACKING_ID = "92a463755ed8c8b96f0f2353a37b7b2"; var PLATFORM = "@protocol.land/git-remote-helper"; var isInitialized = false; var initializeAmplitudeAnalytics = async () => { if (isInitialized) return; await amplitudeInit(AMPLITUDE_TRACKING_ID).promise; isInitialized = true; }; var trackAmplitudeAnalyticsEvent = async (category, action, label, wallet2, data) => { try { let eventOptions = { user_id: void 0, device_id: void 0 }; await initializeAmplitudeAnalytics(); const { response: userAddress } = await withAsync(() => { if (wallet2) { return getAddress(wallet2); } return ""; }); if (userAddress) { eventOptions = { user_id: userAddress }; } else { const { response: machineId } = await withAsync( () => machine.machineId(true) ); if (machineId) { eventOptions = { device_id: machineId }; } } if (eventOptions?.user_id || eventOptions?.device_id) { await amplitudeTrack( category, { action, label, platform: PLATFORM, ...data }, eventOptions ).promise; } } catch (error) { } }; var trackRepositoryUpdateEvent = async (wallet2, data) => { await trackAmplitudeAnalyticsEvent( "Repository", "Add files to repo", "Add files", wallet2, data ); }; var trackRepositoryCloneEvent = async (wallet2, data) => { await trackAmplitudeAnalyticsEvent( "Repository", "Clone a repo", "Clone repo", wallet2, data ); }; // src/lib/githubSync.ts import Arweave2 from "arweave"; import crypto2 from "crypto"; async function deriveAddress2(publicKey) { const arweave2 = Arweave2.init({ host: "ar-io.net", port: 443, protocol: "https" }); const pubKeyBuf = arweave2.utils.b64UrlToBuffer(publicKey); const sha512DigestBuf = await crypto2.subtle.digest("SHA-512", pubKeyBuf); return arweave2.utils.bufferTob64Url(new Uint8Array(sha512DigestBuf)); } async function decryptPAT(encryptedPATString, privateStateTxId) { const arweave2 = new Arweave2({ host: "ar-io.net", port: 443, protocol: "https" }); const encryptedPAT = arweave2.utils.b64UrlToBuffer(encryptedPATString); const response = await fetch(`https://arweave.net/${privateStateTxId}`); const privateState = await response.json(); const ivArrBuff = arweave2.utils.b64UrlToBuffer(privateState.iv); const pubKey = await getActivePublicKey(); const address = await deriveAddress2(pubKey); const encAesKeyStr = privateState.encKeys[address]; const encAesKeyBuf = arweave2.utils.b64UrlToBuffer(encAesKeyStr); const aesKey = await decryptAesKeyWithPrivateKey( encAesKeyBuf ); const accessToken = await decryptFileWithAesGcm( encryptedPAT, aesKey, ivArrBuff ); return new TextDecoder().decode(accessToken); } async function triggerGithubSync(repo) { try { if (!repo) return; const githubSync = repo.githubSync; if (!githubSync || !githubSync?.enabled) return; const connectedAddress = await getAddress(); const isAllowed = githubSync?.allowed?.includes(connectedAddress); if (!isAllowed || !githubSync.repository || !githubSync.workflowId || !githubSync.branch || !githubSync.accessToken || !githubSync.privateStateTxId) { return; } const accessToken = await decryptPAT( githubSync.accessToken, githubSync.privateStateTxId ); if (!accessToken) return; const response = await fetch( `https://api.github.com/repos/${githubSync?.repository}/actions/workflows/${githubSync?.workflowId}/dispatches`, { method: "POST", headers: { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", Authorization: `Bearer ${accessToken}` }, body: JSON.stringify({ ref: githubSync?.branch, inputs: { repoId: repo.id } }) } ); if (response.status === 204) { log("Successfully triggered GitHub Sync"); } } catch (err) { } } // src/lib/remoteHelper.ts var OBJECTS_PUSHED = "unpack ok"; var remoteHelper = async (params) => { const { remoteUrl: remoteUrl2, gitdir: gitdir2 } = params; const tmpPath = getTmpPath(gitdir2); const repoId = `${remoteUrl2.replace(/.*:\/\//, "")}`; const repo = await downloadProtocolLandRepo(repoId, tmpPath); const bareRemotePath = path4.join(tmpPath, repo.dataTxId); talkToGit(bareRemotePath, repo, tmpPath); }; function getTmpPath(gitdir2) { const tmpPath = path4.join(gitdir2, PL_TMP_PATH); if (!existsSync2(tmpPath)) { mkdirSync(tmpPath, { recursive: true }); if (!existsSync2(tmpPath)) throw new Error(`Failed to create the directory: ${tmpPath}`); } return tmpPath; } function talkToGit(bareRemotePath, repo, tmpPath) { const rl = readline2.createInterface({ input: process.stdin, output: new Writable({ write(chunk, encoding, callback) { callback(); } }) // Passing a null stream for output }); async function readLinesUntilEmpty() { const promptForLine = () => new Promise((resolve) => rl.question("", resolve)); while (true) { const line = (await promptForLine()).trim(); if (line === "") { rl.close(); process.exit(0); } const [command, arg] = line.split(" "); switch (command) { case "capabilities": console.log("connect"); console.log(""); break; case "connect": console.log(""); await spawnPipedGitCommand( arg, bareRemotePath, repo, tmpPath ); break; } } } readLinesUntilEmpty(); } var spawnPipedGitCommand = async (gitCommand, remoteUrl2, repo, tmpPath) => { let wallet2; if (gitCommand === "git-receive-pack") { wallet2 = getWallet({ warn: false }); if (!wallet2) process.exit(0); const ownerOrContrib = await ownerOrContributor(repo, wallet2, { pushing: true }); if (!ownerOrContrib) process.exit(0); } else { wallet2 = getWallet({ warn: true }); if (wallet2) { ownerOrContributor(repo, wallet2); } } let objectsUpdated = false; const isCloningRepo = gitCommand === "git-upload-pack" && tmpPath.split(path4.sep).length > 2; const gitProcess = spawn2(gitCommand, [remoteUrl2], { stdio: ["pipe", "pipe", "pipe"] // Pipe for stdin, stdout, and stderr }); process.stdin.pipe(gitProcess.stdin); gitProcess.stdout.pipe(process.stdout); gitProcess.stderr.pipe(process.stderr); gitProcess.stdout.on("data", (data) => { if (data.toString().includes(OBJECTS_PUSHED)) objectsUpdated = true; }); gitProcess.on("exit", async (code) => { if (code !== 0) { log( `git command '${gitCommand}' exited with error. Exit code: ${code}`, { color: "red" } ); process.exit(code ? code : 1); } if (gitCommand === "git-receive-pack" && objectsUpdated) { log( `Push to temp remote finished successfully, now syncing with Protocol Land ...` ); setCacheDirty(tmpPath, repo.dataTxId); const pathToPack = path4.join(remoteUrl2, "..", "..", ".."); waitFor(1e3); const { success, pushCancelled } = await uploadProtocolLandRepo( pathToPack, repo, tmpPath ); clearCache(tmpPath, { keepFolders: [] }); unsetCacheDirty(tmpPath, repo.dataTxId); if (pushCancelled) { log( `Pushing repo '${repo.id}' to Protocol Land was cancelled`, { color: "red" } ); process.exit(0); } if (success) { await triggerGithubSync(repo); log(`Successfully pushed repo '${repo.id}' to Protocol Land`, { color: "green" }); await trackRepositoryUpdateEvent(wallet2, { repo_name: repo.name, repo_id: repo.id, result: "SUCCESS" }); } else { log(`Failed to push repo '${repo.id}' to Protocol Land`, { color: "red" }); log( "Please run `git pull` first to clean the cache and integrate your changes", { color: "red" } ); await trackRepositoryUpdateEvent(wallet2, { repo_name: repo.name, repo_id: repo.id, result: "FAILED", error: "Failed to update repository" }); process.exit(1); } } else if (isCloningRepo) { setGitRemoteOrigin(repo); await trackRepositoryCloneEvent(wallet2, { repo_name: repo.name, repo_id: repo.id, result: "SUCCESS" }); } }); }; // src/index.ts var [remoteName, remoteUrl] = process.argv.slice(2, 4); if (!gitdir) throw new Error("Missing GIT_DIR env"); remoteHelper({ remoteName, remoteUrl, gitdir }).then();