Files
stack/scripts/git-credential-mosaic.mjs
T
jason.woltje c2365ae519 chore: baseline container POC and atomic foundation plan
- Containerized Pi hello-world proof (image mosaic-poc-agent:0.84.4, non-root)
- Four immutable contract fixtures loaded into a generated system prompt
- build/hello/verify/reset scripts with exact-match gating and reset safety
- Documented Pi discovery (v0.84.4, -p mode, --system-prompt, container auth)
- Append-only BUILD-LOG with corrections; deferred layers in LAYERS.md
- Architecture plan: docs/plans/2026-09-02_atomic-mosaic-foundation.md
2026-09-02 18:24:36 -05:00

114 lines
3.0 KiB
JavaScript
Executable File

#!/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`);