import { mkdirSync, existsSync, writeFileSync, renameSync, chmodSync, readFileSync, readdirSync, } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import assert from "node:assert/strict"; import QRCode from "qrcode"; import { authConfig, newAuthConfig, signSession, totp, isLoginRedirect, } from "../src/lib/auth"; import { createDocumentKey, revokeDocumentKey } from "../src/lib/document-keys"; import { readDocument, saveDocument } from "../src/lib/docs"; import { consumeCode, initializeStore } from "../src/lib/auth-store"; async function main() { process.umask(0o077); const command = process.argv[2]; const file = process.env.AUTH_CONFIG_FILE; if (!file) throw Error("Set AUTH_CONFIG_FILE before using the authentication tool"); if (command === "init" || command === "rotate") { if (command === "rotate" && !process.argv.includes("--confirm")) throw Error( "Rotation requires --confirm; all previous authenticators and sessions will stop working", ); if (command === "init" && existsSync(file)) { authConfig(); initializeStore(); console.log("Existing authentication key preserved"); return; } mkdirSync(dirname(file), { recursive: true, mode: 0o700 }); chmodSync(dirname(file), 0o700); const temporary = file + "." + process.pid + ".tmp"; writeFileSync(temporary, JSON.stringify(newAuthConfig()) + "\n", { mode: 0o600, flag: "wx", }); renameSync(temporary, file); initializeStore(); console.log( "Private authentication configuration initialized; no key displayed", ); return; } const config = authConfig(); if (!config) throw Error("Authentication is not configured"); if (command === "state-check") { assert.equal(consumeCode(config, "invalid"), "limited"); console.log("Durable attempt limit survives restart"); return; } if (command === "qr" || command === "qr-png") { if (process.env.CI) throw Error("Never display or export authentication QR codes in CI"); const uri = "otpauth://totp/Tabularium:shared?" + new URLSearchParams({ secret: config.secret, issuer: "Tabularium", algorithm: "SHA1", digits: "6", period: "30", }); if (command === "qr-png") { process.stdout.write( await QRCode.toBuffer(uri, { width: 660, margin: 4, errorCorrectionLevel: "M", }), ); return; } if (!process.stdout.isTTY) throw Error( "Use an interactive SSH terminal for qr, or redirect qr-png to a private local PNG", ); console.log( await QRCode.toString(uri, { type: "terminal", small: true, errorCorrectionLevel: "M", }), ); return; } if (command === "token") { if (!process.stdout.isTTY) throw Error( "Tokens can only be displayed in an interactive SSH terminal", ); console.log(signSession(config, undefined, 3600)); return; } const base = process.env.AUTH_CHECK_BASE_URL || "http://127.0.0.1:3000"; if (command === "run") { const script = process.argv[3]; if (!script) throw Error("Provide a script path"); const original = globalThis.fetch; const token = signSession(config, undefined, 600); globalThis.fetch = (input, init) => { const url = new URL(input instanceof Request ? input.url : String(input)); if (url.origin === new URL(base).origin) { const headers = new Headers( init?.headers || (input instanceof Request ? input.headers : undefined), ); headers.set("Authorization", "Bearer " + token); return original(input, { ...init, headers }); } return original(input, init); }; await import(pathToFileURL(resolve(script)).href); return; } if (command !== "verify" && command !== "exercise") throw Error( "Usage: init | qr | qr-png | rotate --confirm | token | run