#!/usr/bin/env node /** * Repository-local Git credential helper for git.mosaicstack.dev. * * Git invokes this helper with "get", "store", or "erase" and consumes its * stdout directly. Never invoke it manually, enable shell tracing around it, * or add credential values to logs. * * The credential file is intentionally not part of Git and must remain mode * 0600. Override its location with MOSAIC_GITEA_CREDENTIAL_FILE if needed. */ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import process from "node:process"; const operation = process.argv[2] ?? ""; // Git may offer credentials back through stdin for store/erase. This helper is // read-only: ignore those operations and never persist or print their input. if (operation !== "get") { process.exit(0); } const defaultCredentialFile = path.join( os.homedir(), "secrets", "mosaic.gitea.json", ); const credentialFile = process.env.MOSAIC_GITEA_CREDENTIAL_FILE ?? defaultCredentialFile; function fail(message) { process.stderr.write(`git-credential-mosaic: ${message}\n`); process.exit(1); } let stat; try { stat = fs.lstatSync(credentialFile); } catch { fail("credential file is unavailable"); } if (!stat.isFile() || stat.isSymbolicLink()) { fail("credential path must be a regular, non-symbolic-link file"); } if ((stat.mode & 0o077) !== 0) { fail("credential file permissions must be 0600 or stricter"); } if (typeof process.getuid === "function" && stat.uid !== process.getuid()) { fail("credential file must be owned by the current user"); } let document; try { document = JSON.parse(fs.readFileSync(credentialFile, "utf8")); } catch { fail("credential file is not valid JSON"); } const entry = document?.mosaicstack; const configuredUrl = entry?.url; const username = entry?.user; const token = entry?.api_token; if ( typeof configuredUrl !== "string" || typeof username !== "string" || typeof token !== "string" || username.length === 0 || token.length === 0 || /[\r\n]/.test(username) || /[\r\n]/.test(token) ) { fail("credential file is missing valid mosaicstack url/user/api_token fields"); } let credentialUrl; try { credentialUrl = new URL(configuredUrl); } catch { fail("configured credential URL is invalid"); } if ( credentialUrl.protocol !== "https:" || credentialUrl.hostname !== "git.mosaicstack.dev" ) { fail("credential URL is not the approved HTTPS Gitea host"); } const request = {}; for (const line of fs.readFileSync(0, "utf8").split("\n")) { const separator = line.indexOf("="); if (separator > 0) { request[line.slice(0, separator)] = line.slice(separator + 1); } } // Fail closed: emit credentials only for the approved HTTPS host. A host may // include an explicit port; it must match the configured URL exactly. if ( request.protocol !== "https" || request.host !== credentialUrl.host ) { process.exit(0); } // stdout is the Git credential-helper protocol channel, consumed directly by // Git. Do not add status messages here. process.stdout.write(`username=${username}\npassword=${token}\n`);