Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| sudo: Access service tokens listed in STAFF_SERVICE_TOKENS are staff, recorded as <name>@service.g1t.sh | 1 | #!/usr/bin/env node |
| 2 | // Uses sudo (https://sudo.g1t.sh) without a browser, through the Access | |
| 3 | // service token in .credentials/sudo-service-token.json. sudo records it | |
| 4 | // as claude@service.g1t.sh (apps/sudo/README.md, "Service tokens"). | |
| 5 | // | |
| 6 | // node scripts/ops/sudo.mjs get /costs | |
| 7 | // node scripts/ops/sudo.mjs post /costs intent=run | |
| 8 | // node scripts/ops/sudo.mjs get /users/g1t-reviewer --html | |
| 9 | // | |
| 10 | // Prints the status, where a redirect goes, and the page as text (or the | |
| 11 | // raw HTML with --html). POSTs are form-encoded, the way sudo's pages send | |
| 12 | // them, with sudo's own Origin. The secret is never printed. | |
| 13 | import { readFileSync } from "node:fs"; | |
| 14 | ||
| 15 | const SUDO = "https://sudo.g1t.sh"; | |
| 16 | const CREDENTIALS = new URL("../../.credentials/sudo-service-token.json", import.meta.url); | |
| 17 | ||
| 18 | /** The headers that get a request past Access. */ | |
| 19 | export function accessHeaders() { | |
| 20 | const { client_id, client_secret } = JSON.parse(readFileSync(CREDENTIALS, "utf8")); | |
| 21 | if (!client_id || !client_secret) throw new Error("sudo-service-token.json needs client_id and client_secret"); | |
| 22 | return { "CF-Access-Client-Id": client_id, "CF-Access-Client-Secret": client_secret }; | |
| 23 | } | |
| 24 | ||
| 25 | /** A page's readable text: no scripts, styles or tags, one line per block. */ | |
| 26 | export function pageText(html) { | |
| 27 | return html | |
| 28 | .replace(/<(script|style)\b[\s\S]*?<\/\1>/gi, "") | |
| 29 | .replace(/<(br|\/p|\/div|\/li|\/tr|\/h[1-6]|\/section|\/summary)\b[^>]*>/gi, "\n") | |
| 30 | .replace(/<\/t[dh]>/gi, "\t") | |
| 31 | .replace(/<[^>]+>/g, "") | |
| 32 | .replace(/ /g, " ") | |
| 33 | .replace(/&/g, "&") | |
| 34 | .replace(/</g, "<") | |
| 35 | .replace(/>/g, ">") | |
| 36 | .replace(/"/g, '"') | |
| 37 | .replace(/'/g, "'") | |
| 38 | .split("\n") | |
| 39 | .map((line) => line.replace(/[ \t]+/g, " ").trim()) | |
| 40 | .filter(Boolean) | |
| 41 | .join("\n"); | |
| 42 | } | |
| 43 | ||
| 44 | /** GETs or POSTs `path`; `fields` are the form's name=value pairs. */ | |
| 45 | export async function sudo(method, path, fields = []) { | |
| 46 | const headers = { ...accessHeaders() }; | |
| 47 | let body; | |
| 48 | if (method === "POST") { | |
| 49 | headers.origin = SUDO; | |
| 50 | headers["content-type"] = "application/x-www-form-urlencoded"; | |
| 51 | body = new URLSearchParams(fields.map((field) => field.split(/=(.*)/s).slice(0, 2))).toString(); | |
| 52 | } | |
| 53 | const response = await fetch(new URL(path, SUDO), { method, headers, body, redirect: "manual" }); | |
| 54 | return { status: response.status, location: response.headers.get("location"), html: await response.text() }; | |
| 55 | } | |
| 56 | ||
| 57 | if (process.argv[1]?.replace(/\\/g, "/").endsWith("scripts/ops/sudo.mjs")) { | |
| 58 | const [verb = "get", path = "/", ...rest] = process.argv.slice(2); | |
| 59 | const html = rest.includes("--html"); | |
| 60 | const fields = rest.filter((arg) => arg !== "--html"); | |
| 61 | const result = await sudo(verb.toUpperCase() === "POST" ? "POST" : "GET", path, fields); | |
| 62 | console.log(`${result.status}${result.location ? ` -> ${result.location}` : ""}`); | |
| 63 | if (result.location?.includes("cloudflareaccess.com")) { | |
| 64 | console.log("Access did not accept the service token: the sudo application needs a Service Auth policy that includes it."); | |
| 65 | } else { | |
| 66 | console.log(html ? result.html : pageText(result.html)); | |
| 67 | } | |
| 68 | } |