Row 24. A writable root that is a git work tree may carry a git object in the binding; the seat then has git_status, git_commit (explicit paths, seat author, Requested-by trailer from the envelope requester, push at once per D6), git_pull (ff-only) and git_push (one branch, never force), plus reserve_id and per-write clone locks under protocol vault. Git children run with no host config and one credential helper, bin/git-credential.mjs, reading the 0600 seat token file named in the binding; the fleet helper serves only the Gitea hosts. Suite 58/58, node 143. rev-code-02 APPROVED round 1 (#1509 comment 26375, tree 82ab962f). Co-Authored-By: Claude Fable 5.1 <[email protected]>
62 lines
2.2 KiB
JavaScript
Executable File
62 lines
2.2 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
// Credential helper for the Discord Sage's git verbs (row 24). git runs it
|
|
// as `credential.helper` for the seat's pushes and pulls only, through the
|
|
// allowlisted environment src/git.mjs builds. It answers `get` and nothing
|
|
// else, for https only, from the one file named in its environment.
|
|
//
|
|
// Why not the fleet helper: git-credential-mosaic serves the two Gitea
|
|
// hosts and declines every other host, and the host's own GitHub login
|
|
// (gh) must never sign a seat's push. This helper is the seat's and reads
|
|
// only the seat's token file.
|
|
//
|
|
// The token goes from the file to git's stdin pipe and nowhere else: not
|
|
// to argv, not to stderr, not to a log. Anything wrong (a missing or loose
|
|
// file, a non-https request, a value that does not look like a token) is
|
|
// a short stderr line and exit 1, so git fails the operation and the verb
|
|
// reports that failure masked.
|
|
|
|
import { lstatSync, readFileSync } from "node:fs";
|
|
|
|
const TOKEN_FILE_ENV = "MOSAIC_DISCORD_GIT_TOKEN_FILE";
|
|
const USERNAME_ENV = "MOSAIC_DISCORD_GIT_USERNAME";
|
|
|
|
function fail(msg) {
|
|
process.stderr.write(`git-credential (discord): ${msg}\n`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const action = process.argv[2];
|
|
if (action !== "get") process.exit(0);
|
|
|
|
const file = process.env[TOKEN_FILE_ENV];
|
|
const username = process.env[USERNAME_ENV];
|
|
if (!file || !file.startsWith("/")) fail(`${TOKEN_FILE_ENV} is not set`);
|
|
if (!username || !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(username)) fail(`${USERNAME_ENV} is not set`);
|
|
|
|
let input = "";
|
|
try {
|
|
input = readFileSync(0, "utf8");
|
|
} catch {
|
|
fail("no request on stdin");
|
|
}
|
|
const req = {};
|
|
for (const line of input.split("\n")) {
|
|
const i = line.indexOf("=");
|
|
if (i > 0) req[line.slice(0, i)] = line.slice(i + 1);
|
|
}
|
|
if (req.protocol !== "https") fail("only https");
|
|
if (!req.host) fail("no host");
|
|
|
|
let st;
|
|
try {
|
|
st = lstatSync(file);
|
|
} catch {
|
|
fail("token file not found");
|
|
}
|
|
if (st.isSymbolicLink() || !st.isFile()) fail("token file must be a regular file");
|
|
if ((st.mode & 0o777) !== 0o600) fail("token file must be mode 0600");
|
|
const token = readFileSync(file, "utf8").trim();
|
|
if (!/^[A-Za-z0-9_]{20,}$/.test(token)) fail("token file does not hold a token");
|
|
|
|
process.stdout.write(`username=${username}\npassword=${token}\n`);
|