@cldmv/gh-broker 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -12
- package/config.example.json +69 -2
- package/config.sample.json +1 -1
- package/deploy/README.md +58 -19
- package/deploy/cli.mjs +38 -0
- package/deploy/gh-broker.service +9 -2
- package/deploy/install.mjs +99 -42
- package/deploy/lib/agent-cli.mjs +166 -0
- package/deploy/lib/agents.mjs +128 -0
- package/deploy/lib/cli.mjs +72 -0
- package/deploy/lib/config-merge.mjs +29 -8
- package/deploy/lib/install-args.mjs +3 -2
- package/deploy/lib/mcp-config.mjs +211 -0
- package/deploy/lib/otp-cli.mjs +136 -0
- package/deploy/lib/otp.mjs +325 -0
- package/deploy/lib/secret-store.mjs +1 -0
- package/deploy/lib/system.mjs +24 -12
- package/deploy/real-deps.mjs +102 -0
- package/dist/index.mjs +932 -66
- package/dist/index.mjs.map +4 -4
- package/package.json +4 -3
package/dist/index.mjs
CHANGED
|
@@ -7405,6 +7405,7 @@ var require_content_type = __commonJS({
|
|
|
7405
7405
|
|
|
7406
7406
|
// src/config.ts
|
|
7407
7407
|
import { readFileSync } from "node:fs";
|
|
7408
|
+
import { isAbsolute as isAbsolute3 } from "node:path";
|
|
7408
7409
|
|
|
7409
7410
|
// src/verbs/ghcr/config.ts
|
|
7410
7411
|
import { isAbsolute, normalize } from "node:path";
|
|
@@ -7473,6 +7474,184 @@ function imageAllowed(owner2, image2) {
|
|
|
7473
7474
|
return owner2.images.some((g) => imageGlobToRegExp(g).test(image2));
|
|
7474
7475
|
}
|
|
7475
7476
|
|
|
7477
|
+
// src/otp/totp.ts
|
|
7478
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
7479
|
+
var BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
7480
|
+
var STEP_SECONDS = 30;
|
|
7481
|
+
function base32Decode(text) {
|
|
7482
|
+
const clean = text.toUpperCase().replace(/=+$/, "");
|
|
7483
|
+
const out = [];
|
|
7484
|
+
let bits = 0;
|
|
7485
|
+
let value = 0;
|
|
7486
|
+
for (const ch of clean) {
|
|
7487
|
+
const idx = BASE32_ALPHABET.indexOf(ch);
|
|
7488
|
+
if (idx < 0) throw new Error("base32: invalid character");
|
|
7489
|
+
value = value << 5 | idx;
|
|
7490
|
+
bits += 5;
|
|
7491
|
+
if (bits >= 8) {
|
|
7492
|
+
out.push(value >>> bits - 8 & 255);
|
|
7493
|
+
bits -= 8;
|
|
7494
|
+
}
|
|
7495
|
+
}
|
|
7496
|
+
return Buffer.from(out);
|
|
7497
|
+
}
|
|
7498
|
+
function hotp(key2, counter, digits = 6) {
|
|
7499
|
+
const msg = Buffer.alloc(8);
|
|
7500
|
+
msg.writeBigUInt64BE(BigInt(counter));
|
|
7501
|
+
const mac2 = createHmac("sha1", key2).update(msg).digest();
|
|
7502
|
+
const offset = mac2[mac2.length - 1] & 15;
|
|
7503
|
+
const bin = (mac2[offset] & 127) << 24 | mac2[offset + 1] << 16 | mac2[offset + 2] << 8 | mac2[offset + 3];
|
|
7504
|
+
return String(bin % 10 ** digits).padStart(digits, "0");
|
|
7505
|
+
}
|
|
7506
|
+
function stepAt(nowMs) {
|
|
7507
|
+
return Math.floor(nowMs / 1e3 / STEP_SECONDS);
|
|
7508
|
+
}
|
|
7509
|
+
function safeEqual(a, b) {
|
|
7510
|
+
const x = Buffer.from(a);
|
|
7511
|
+
const y = Buffer.from(b);
|
|
7512
|
+
return x.length === y.length && timingSafeEqual(x, y);
|
|
7513
|
+
}
|
|
7514
|
+
function matchStep(key2, code, nowMs, skew = 1, digits = 6) {
|
|
7515
|
+
if (!new RegExp(`^\\d{${digits}}$`).test(code)) return void 0;
|
|
7516
|
+
const now = stepAt(nowMs);
|
|
7517
|
+
let matched;
|
|
7518
|
+
for (let step = now - skew; step <= now + skew; step++) {
|
|
7519
|
+
if (safeEqual(hotp(key2, step, digits), code)) matched = step;
|
|
7520
|
+
}
|
|
7521
|
+
return matched;
|
|
7522
|
+
}
|
|
7523
|
+
|
|
7524
|
+
// src/otp/policy.ts
|
|
7525
|
+
var OTP_STATUS = "otp_status";
|
|
7526
|
+
var OTP_ARG = "otp";
|
|
7527
|
+
var DEFAULT_GATED = /* @__PURE__ */ new Set([
|
|
7528
|
+
"create_repository",
|
|
7529
|
+
"fork_repository",
|
|
7530
|
+
"merge_pull_request",
|
|
7531
|
+
"pull_request_review_write",
|
|
7532
|
+
"actions_run_trigger",
|
|
7533
|
+
"label_write",
|
|
7534
|
+
"delete_branch",
|
|
7535
|
+
"dismiss_alert",
|
|
7536
|
+
// Disabled by default too (src/availability.ts); gated once enabled.
|
|
7537
|
+
"create_pull_request_with_copilot",
|
|
7538
|
+
"assign_copilot_to_issue",
|
|
7539
|
+
"assign_copilot_to_issue_with_intent"
|
|
7540
|
+
]);
|
|
7541
|
+
var OTP_DEFAULTS = {
|
|
7542
|
+
windowSeconds: 300,
|
|
7543
|
+
maxFailures: 5,
|
|
7544
|
+
lockoutSeconds: 600,
|
|
7545
|
+
maxLockouts: 3,
|
|
7546
|
+
statePath: "/var/lib/gh-broker/otp-state.json"
|
|
7547
|
+
};
|
|
7548
|
+
function otpSettings(config2) {
|
|
7549
|
+
const o = config2.otp;
|
|
7550
|
+
return {
|
|
7551
|
+
windowSeconds: o?.windowSeconds ?? OTP_DEFAULTS.windowSeconds,
|
|
7552
|
+
maxFailures: o?.maxFailures ?? OTP_DEFAULTS.maxFailures,
|
|
7553
|
+
lockoutSeconds: o?.lockoutSeconds ?? OTP_DEFAULTS.lockoutSeconds,
|
|
7554
|
+
maxLockouts: o?.maxLockouts ?? OTP_DEFAULTS.maxLockouts,
|
|
7555
|
+
statePath: o?.statePath ?? OTP_DEFAULTS.statePath
|
|
7556
|
+
};
|
|
7557
|
+
}
|
|
7558
|
+
function isGated(rule) {
|
|
7559
|
+
if (rule.name === OTP_STATUS) return false;
|
|
7560
|
+
return rule.otp ?? DEFAULT_GATED.has(rule.name);
|
|
7561
|
+
}
|
|
7562
|
+
function readSeed(config2, secrets) {
|
|
7563
|
+
if (!config2.otp) return { problem: "OTP is not set up on this broker (no config.otp); re-run the installer to set it up" };
|
|
7564
|
+
const name = config2.otp.seed.replace(/^cred:/, "");
|
|
7565
|
+
if (!secrets.has(config2.otp.seed)) return { problem: `OTP is not set up on this broker: its seed credential "${name}" is not stored (re-run the installer to generate it)` };
|
|
7566
|
+
let key2;
|
|
7567
|
+
try {
|
|
7568
|
+
key2 = base32Decode(secrets.resolve(config2.otp.seed).trim());
|
|
7569
|
+
} catch {
|
|
7570
|
+
key2 = Buffer.alloc(0);
|
|
7571
|
+
}
|
|
7572
|
+
if (key2.length === 0) return { problem: `OTP is not set up on this broker: the stored seed "${name}" is not valid base32 (re-run the installer to replace it)` };
|
|
7573
|
+
return { key: key2 };
|
|
7574
|
+
}
|
|
7575
|
+
function otpSetupProblem(config2, secrets) {
|
|
7576
|
+
const seed = readSeed(config2, secrets);
|
|
7577
|
+
return "problem" in seed ? seed.problem : void 0;
|
|
7578
|
+
}
|
|
7579
|
+
|
|
7580
|
+
// src/agents.ts
|
|
7581
|
+
import { isAbsolute as isAbsolute2 } from "node:path";
|
|
7582
|
+
var AGENT_USER = /^[a-z_][a-z0-9_-]{0,15}$/;
|
|
7583
|
+
var RESERVED_AGENT_USERS = ["__proto__", "root", "gh-broker"];
|
|
7584
|
+
function isAgentUser(name) {
|
|
7585
|
+
return AGENT_USER.test(name) && !RESERVED_AGENT_USERS.includes(name);
|
|
7586
|
+
}
|
|
7587
|
+
var AGENT_GROUP_PREFIX = "gh-broker-agent-";
|
|
7588
|
+
var SERVICE_USER = "gh-broker";
|
|
7589
|
+
function agentsDir(listen) {
|
|
7590
|
+
if (!listen.startsWith("agents:")) return void 0;
|
|
7591
|
+
const dir = listen.slice("agents:".length);
|
|
7592
|
+
return isAbsolute2(dir) ? dir : void 0;
|
|
7593
|
+
}
|
|
7594
|
+
function parseAgentGroups(groupFile) {
|
|
7595
|
+
const agents = [];
|
|
7596
|
+
const skipped = [];
|
|
7597
|
+
for (const line of groupFile.split("\n")) {
|
|
7598
|
+
const [group, , gidText, memberText] = line.split(":");
|
|
7599
|
+
if (!group?.startsWith(AGENT_GROUP_PREFIX)) continue;
|
|
7600
|
+
const user = group.slice(AGENT_GROUP_PREFIX.length);
|
|
7601
|
+
const gid = Number(gidText);
|
|
7602
|
+
if (!isAgentUser(user)) {
|
|
7603
|
+
skipped.push(`${group}: "${user}" is not a valid agent user name`);
|
|
7604
|
+
continue;
|
|
7605
|
+
}
|
|
7606
|
+
if (!gidText || !Number.isSafeInteger(gid)) {
|
|
7607
|
+
skipped.push(`${group}: bad gid`);
|
|
7608
|
+
continue;
|
|
7609
|
+
}
|
|
7610
|
+
const members2 = new Set((memberText ?? "").split(",").map((m) => m.trim()).filter(Boolean));
|
|
7611
|
+
if (members2.size !== 2 || !members2.has(SERVICE_USER) || !members2.has(user)) {
|
|
7612
|
+
skipped.push(`${group}: members must be exactly ${SERVICE_USER} and ${user}`);
|
|
7613
|
+
continue;
|
|
7614
|
+
}
|
|
7615
|
+
agents.push({ user, group, gid });
|
|
7616
|
+
}
|
|
7617
|
+
return { agents, skipped };
|
|
7618
|
+
}
|
|
7619
|
+
|
|
7620
|
+
// src/denylist.ts
|
|
7621
|
+
var DENIED_TOOLS = /* @__PURE__ */ new Map([
|
|
7622
|
+
["create_or_update_file", "file writes go through git"],
|
|
7623
|
+
["push_files", "file writes go through git"],
|
|
7624
|
+
["delete_file", "file writes go through git"],
|
|
7625
|
+
["delete_repository", "deleting a repo stays a manual step in GitHub"],
|
|
7626
|
+
["create_repository_ruleset", "rulesets are managed by org bootstrap"],
|
|
7627
|
+
["custom_properties_write", "repo configuration is managed by org bootstrap"]
|
|
7628
|
+
]);
|
|
7629
|
+
var WRITE_VERBS = "create|update|delete|write|set|add|remove|edit|manage|change";
|
|
7630
|
+
var writeTo = (subjects, verbs = WRITE_VERBS) => new RegExp(`(^|_)(${verbs})_(.*_)?(${subjects})$|(^|_)(${subjects})_(${verbs})$`);
|
|
7631
|
+
var DENIED_PATTERNS = [
|
|
7632
|
+
[writeTo("files?|contents?|commits?|trees?|blobs?|refs?|git_refs?|changes", "create|update|delete|write|push"), "file writes go through git"],
|
|
7633
|
+
[writeTo("repos?|repositor(y|ies)", "delete"), "deleting a repo stays a manual step in GitHub"],
|
|
7634
|
+
// Not `create`: create_repository is a gated tool, not repo configuration.
|
|
7635
|
+
[writeTo("repos?|repositor(y|ies)|repo_settings|repository_settings|settings|visibility|default_branch|topics", "update|edit|write|set|change|manage|replace|transfer|archive|unarchive|rename"), "repo configuration is managed by org bootstrap"],
|
|
7636
|
+
[writeTo("custom_property|custom_properties|custom_property_values?"), "repo configuration is managed by org bootstrap"],
|
|
7637
|
+
[writeTo("security_and_analysis|vulnerability_alerts|automated_security_fixes", `${WRITE_VERBS}|enable|disable`), "repo configuration is managed by org bootstrap"],
|
|
7638
|
+
[writeTo("workflows?", "enable|disable|write|update|delete"), "turning workflows on or off changes how CI behaves; org bootstrap manages it"],
|
|
7639
|
+
[writeTo("rulesets?"), "rulesets are managed by org bootstrap"],
|
|
7640
|
+
[writeTo("branch_protections?|branch_protection_rules?"), "branch protection is managed by org bootstrap"],
|
|
7641
|
+
[writeTo("variables?|secrets?"), "Actions variables and secrets are managed by org bootstrap"],
|
|
7642
|
+
[writeTo("workflow_permissions|actions_permissions"), "workflow permissions are managed by org bootstrap"],
|
|
7643
|
+
[writeTo("webhooks?|hooks?|deploy_keys?|collaborators?|environments?"), "repo access and configuration are managed by org bootstrap"],
|
|
7644
|
+
[/^(bulk|batch)_(close|lock|unlock|delete|update|write|edit|label|set|add|remove)|^close_((all|bulk)_)?(issues|pull_requests|prs)$|_(issues|pull_requests)_close$/, "bulk writes aren't offered; loop over the single-item tool"],
|
|
7645
|
+
[writeTo("issues?|conversations?|pull_requests?|discussions?", "lock|unlock"), "locking conversations isn't offered"],
|
|
7646
|
+
[/^lock_write$/, "locking conversations isn't offered"]
|
|
7647
|
+
];
|
|
7648
|
+
function deniedReason(name) {
|
|
7649
|
+
const exact = DENIED_TOOLS.get(name);
|
|
7650
|
+
if (exact) return exact;
|
|
7651
|
+
for (const [pattern, why] of DENIED_PATTERNS) if (pattern.test(name)) return why;
|
|
7652
|
+
return void 0;
|
|
7653
|
+
}
|
|
7654
|
+
|
|
7476
7655
|
// src/config.ts
|
|
7477
7656
|
var MODES = ["read", "write"];
|
|
7478
7657
|
function loadConfig(path) {
|
|
@@ -7484,8 +7663,11 @@ function loadConfig(path) {
|
|
|
7484
7663
|
}
|
|
7485
7664
|
if (typeof raw !== "object" || raw === null) throw new Error("config: root must be an object");
|
|
7486
7665
|
const c = raw;
|
|
7487
|
-
if (typeof c.listen
|
|
7488
|
-
throw new Error('config.listen:
|
|
7666
|
+
if (typeof c.listen === "string" && c.listen.startsWith("unix:")) {
|
|
7667
|
+
throw new Error('config.listen: the shared "unix:" socket was replaced by one socket per agent; re-run the installer (sudo gh-broker install) to set listen to "agents:/run/gh-broker/agents"');
|
|
7668
|
+
}
|
|
7669
|
+
if (typeof c.listen !== "string" || !agentsDir(c.listen)) {
|
|
7670
|
+
throw new Error('config.listen: must be "agents:<absolute directory>", e.g. "agents:/run/gh-broker/agents"');
|
|
7489
7671
|
}
|
|
7490
7672
|
if (typeof c.upstream !== "string" || !/^https?:\/\//.test(c.upstream)) {
|
|
7491
7673
|
throw new Error("config.upstream: must be an http(s) URL");
|
|
@@ -7499,11 +7681,20 @@ function loadConfig(path) {
|
|
|
7499
7681
|
if (typeof o.name !== "string" || !o.name) throw new Error(`config.tools[${i}].name: required`);
|
|
7500
7682
|
if (!MODES.includes(o.mode)) throw new Error(`config.tools[${i}].mode: must be read|write`);
|
|
7501
7683
|
if (seen.has(o.name)) throw new Error(`config.tools: duplicate tool "${o.name}"`);
|
|
7684
|
+
const denied = deniedReason(o.name);
|
|
7685
|
+
if (denied) throw new Error(`config.tools[${i}] ("${o.name}"): never allowed in gh-broker (${denied}); remove it from config.tools`);
|
|
7502
7686
|
seen.add(o.name);
|
|
7503
7687
|
const custom2 = o.custom === true;
|
|
7504
7688
|
const augment = o.augment === true;
|
|
7505
7689
|
if (custom2 && augment) throw new Error(`config.tools[${i}] ("${o.name}"): custom and augment are mutually exclusive`);
|
|
7506
|
-
|
|
7690
|
+
if (o.otp !== void 0 && typeof o.otp !== "boolean") throw new Error(`config.tools[${i}].otp: must be true or false`);
|
|
7691
|
+
if (o.enabled !== void 0 && typeof o.enabled !== "boolean") throw new Error(`config.tools[${i}].enabled: must be true or false`);
|
|
7692
|
+
if (o.name === OTP_STATUS) {
|
|
7693
|
+
if (o.otp === true) throw new Error(`config.tools[${i}] ("${OTP_STATUS}"): can't be gated (otp: true)`);
|
|
7694
|
+
if (o.enabled === false) throw new Error(`config.tools[${i}] ("${OTP_STATUS}"): can't be disabled (enabled: false)`);
|
|
7695
|
+
if (!custom2) throw new Error(`config.tools[${i}] ("${OTP_STATUS}"): must be custom: true`);
|
|
7696
|
+
}
|
|
7697
|
+
return { name: o.name, mode: o.mode, custom: custom2, augment, ...typeof o.otp === "boolean" ? { otp: o.otp } : {}, ...typeof o.enabled === "boolean" ? { enabled: o.enabled } : {} };
|
|
7507
7698
|
});
|
|
7508
7699
|
if (typeof c.owners !== "object" || c.owners === null) throw new Error("config.owners: must be an object");
|
|
7509
7700
|
const owners = {};
|
|
@@ -7580,6 +7771,7 @@ function loadConfig(path) {
|
|
|
7580
7771
|
...Array.isArray(n.tokenOrgs) ? { tokenOrgs: n.tokenOrgs } : {}
|
|
7581
7772
|
};
|
|
7582
7773
|
}
|
|
7774
|
+
const otp = c.otp !== void 0 ? parseOtpConfig(c.otp) : void 0;
|
|
7583
7775
|
return {
|
|
7584
7776
|
listen: c.listen,
|
|
7585
7777
|
upstream: c.upstream,
|
|
@@ -7589,9 +7781,27 @@ function loadConfig(path) {
|
|
|
7589
7781
|
...secrets ? { secrets } : {},
|
|
7590
7782
|
...defaultOwner ? { defaultOwner } : {},
|
|
7591
7783
|
...npm ? { npm } : {},
|
|
7592
|
-
...c.ghcr !== void 0 ? { ghcr: parseGhcrConfig(c.ghcr) } : {}
|
|
7784
|
+
...c.ghcr !== void 0 ? { ghcr: parseGhcrConfig(c.ghcr) } : {},
|
|
7785
|
+
...otp ? { otp } : {}
|
|
7593
7786
|
};
|
|
7594
7787
|
}
|
|
7788
|
+
function parseOtpConfig(raw) {
|
|
7789
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error("config.otp: must be an object");
|
|
7790
|
+
const o = raw;
|
|
7791
|
+
if (typeof o.seed !== "string" || !o.seed.startsWith("cred:")) throw new Error('config.otp.seed: must be a "cred:<name>" ref');
|
|
7792
|
+
const out = { seed: o.seed };
|
|
7793
|
+
for (const key2 of ["windowSeconds", "maxFailures", "lockoutSeconds", "maxLockouts"]) {
|
|
7794
|
+
const v = o[key2];
|
|
7795
|
+
if (v === void 0) continue;
|
|
7796
|
+
if (typeof v !== "number" || !Number.isSafeInteger(v) || v <= 0) throw new Error(`config.otp.${key2}: must be a positive integer`);
|
|
7797
|
+
out[key2] = v;
|
|
7798
|
+
}
|
|
7799
|
+
if (o.statePath !== void 0) {
|
|
7800
|
+
if (typeof o.statePath !== "string" || !isAbsolute3(o.statePath)) throw new Error("config.otp.statePath: must be an absolute path");
|
|
7801
|
+
out.statePath = o.statePath;
|
|
7802
|
+
}
|
|
7803
|
+
return out;
|
|
7804
|
+
}
|
|
7595
7805
|
function indexTools(tools) {
|
|
7596
7806
|
return new Map(tools.map((t) => [t.name, t]));
|
|
7597
7807
|
}
|
|
@@ -7655,6 +7865,7 @@ function validateRefs(config2, read) {
|
|
|
7655
7865
|
assertSafeName(config2.npm.token.slice(CRED_PREFIX.length));
|
|
7656
7866
|
}
|
|
7657
7867
|
for (const ref of ghcrCredRefs(config2.ghcr)) credName(ref);
|
|
7868
|
+
if (config2.otp) credName(config2.otp.seed);
|
|
7658
7869
|
}
|
|
7659
7870
|
function systemdCredsStore(config2) {
|
|
7660
7871
|
const dir = process.env.CREDENTIALS_DIRECTORY;
|
|
@@ -9590,9 +9801,9 @@ var asciiTabOrNewline = /[\t\n\r]/g;
|
|
|
9590
9801
|
function stripTabAndNewline(value) {
|
|
9591
9802
|
return value.replace(asciiTabOrNewline, "");
|
|
9592
9803
|
}
|
|
9593
|
-
function urlHostnameOk(url2,
|
|
9594
|
-
|
|
9595
|
-
return
|
|
9804
|
+
function urlHostnameOk(url2, hostname2) {
|
|
9805
|
+
hostname2.lastIndex = 0;
|
|
9806
|
+
return hostname2.test(url2.hostname);
|
|
9596
9807
|
}
|
|
9597
9808
|
function urlProtocolOk(url2, protocol) {
|
|
9598
9809
|
protocol.lastIndex = 0;
|
|
@@ -10190,7 +10401,7 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
10190
10401
|
}
|
|
10191
10402
|
return propValues;
|
|
10192
10403
|
});
|
|
10193
|
-
const
|
|
10404
|
+
const isObject4 = isObject2;
|
|
10194
10405
|
const catchall = def.catchall;
|
|
10195
10406
|
let value;
|
|
10196
10407
|
const memo2 = globalConfig.memoizer;
|
|
@@ -10198,7 +10409,7 @@ var $ZodObject = /* @__PURE__ */ $constructor("$ZodObject", (inst, def) => {
|
|
|
10198
10409
|
inst._zod.parse = (payload, ctx) => {
|
|
10199
10410
|
value ?? (value = _normalized.value);
|
|
10200
10411
|
const input = payload.value;
|
|
10201
|
-
if (!
|
|
10412
|
+
if (!isObject4(input)) {
|
|
10202
10413
|
payload.issues.push({
|
|
10203
10414
|
expected: "object",
|
|
10204
10415
|
code: "invalid_type",
|
|
@@ -10334,7 +10545,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
10334
10545
|
return doc.compile();
|
|
10335
10546
|
};
|
|
10336
10547
|
let fastpass;
|
|
10337
|
-
const
|
|
10548
|
+
const isObject4 = isObject2;
|
|
10338
10549
|
const jit = !globalConfig.jitless;
|
|
10339
10550
|
const allowsEval2 = allowsEval;
|
|
10340
10551
|
const fastEnabled = jit && allowsEval2.value;
|
|
@@ -10343,7 +10554,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
10343
10554
|
inst._zod.parse = (payload, ctx) => {
|
|
10344
10555
|
value ?? (value = _normalized.value);
|
|
10345
10556
|
const input = payload.value;
|
|
10346
|
-
if (!
|
|
10557
|
+
if (!isObject4(input)) {
|
|
10347
10558
|
payload.issues.push({
|
|
10348
10559
|
expected: "object",
|
|
10349
10560
|
code: "invalid_type",
|
|
@@ -17798,8 +18009,8 @@ async function random(size) {
|
|
|
17798
18009
|
const evenDistCutoff = Math.pow(2, 8) - Math.pow(2, 8) % mask.length;
|
|
17799
18010
|
let result = "";
|
|
17800
18011
|
while (result.length < size) {
|
|
17801
|
-
const
|
|
17802
|
-
for (const randomByte of
|
|
18012
|
+
const randomBytes3 = await getRandomValues(size - result.length);
|
|
18013
|
+
for (const randomByte of randomBytes3) {
|
|
17803
18014
|
if (randomByte < evenDistCutoff) {
|
|
17804
18015
|
result += mask[randomByte % mask.length];
|
|
17805
18016
|
}
|
|
@@ -19985,8 +20196,8 @@ var stringifyIteratively = (rootValue, replacer, spaceParam) => {
|
|
|
19985
20196
|
const isFunctionReplacer = typeof replacer === "function";
|
|
19986
20197
|
const propertyList = Array.isArray(replacer) ? new Set(replacer.map(String)) : null;
|
|
19987
20198
|
const prepareVal = (parent, key2, val) => {
|
|
19988
|
-
const
|
|
19989
|
-
const hasToJSON =
|
|
20199
|
+
const isObject4 = val !== null && typeof val === "object";
|
|
20200
|
+
const hasToJSON = isObject4 && typeof val.toJSON === "function";
|
|
19990
20201
|
if (hasToJSON) {
|
|
19991
20202
|
val = val.toJSON(key2);
|
|
19992
20203
|
}
|
|
@@ -20820,7 +21031,14 @@ function audit(entry) {
|
|
|
20820
21031
|
|
|
20821
21032
|
// src/availability.ts
|
|
20822
21033
|
var GHCR_TOOLS = /* @__PURE__ */ new Set(["ghcr_push", "ghcr_push_status", "ghcr_tags"]);
|
|
21034
|
+
var DEFAULT_DISABLED = /* @__PURE__ */ new Set(["create_pull_request_with_copilot", "assign_copilot_to_issue", "assign_copilot_to_issue_with_intent"]);
|
|
21035
|
+
function isEnabled(rule) {
|
|
21036
|
+
return rule.enabled ?? !DEFAULT_DISABLED.has(rule.name);
|
|
21037
|
+
}
|
|
20823
21038
|
function unavailableReason(rule, config2, secrets) {
|
|
21039
|
+
if (!isEnabled(rule)) {
|
|
21040
|
+
return DEFAULT_DISABLED.has(rule.name) && rule.enabled === void 0 ? `it is disabled by default; set "enabled": true on its entry in config.tools to turn it on` : `it is disabled in config.tools ("enabled": false)`;
|
|
21041
|
+
}
|
|
20824
21042
|
if (rule.name === "npm_scaffold") {
|
|
20825
21043
|
if (!config2.npm) return "the npm integration is not configured (enable it with the installer's --npm)";
|
|
20826
21044
|
if (!secrets.has(config2.npm.token)) return `its credential "${config2.npm.token.replace(/^cred:/, "")}" is not stored (re-run the installer with --npm)`;
|
|
@@ -20832,6 +21050,7 @@ function unavailableReason(rule, config2, secrets) {
|
|
|
20832
21050
|
return `no GHCR credential is stored (${refs.map((r) => `"${r.replace(/^cred:/, "")}"`).join(", ")}; re-run the installer to add one)`;
|
|
20833
21051
|
}
|
|
20834
21052
|
}
|
|
21053
|
+
if (isGated(rule)) return otpSetupProblem(config2, secrets);
|
|
20835
21054
|
return void 0;
|
|
20836
21055
|
}
|
|
20837
21056
|
|
|
@@ -23295,7 +23514,7 @@ function tarballDigests(tarball) {
|
|
|
23295
23514
|
var PLACEHOLDER_VERSION = "0.0.0-stage";
|
|
23296
23515
|
var SCAFFOLD_MARKER = "gh-broker";
|
|
23297
23516
|
var DEFAULT_WORKFLOW = "publish.yml";
|
|
23298
|
-
var INPUT_KEYS2 = /* @__PURE__ */ new Set(["owner", "repo", "package", "
|
|
23517
|
+
var INPUT_KEYS2 = /* @__PURE__ */ new Set(["owner", "repo", "package", "npm_otp", "workflow"]);
|
|
23299
23518
|
var PACKAGE_NAME = /^@[a-z0-9][a-z0-9._~-]*\/[a-z0-9][a-z0-9._~-]*$/;
|
|
23300
23519
|
var NPM_SCAFFOLD_SCHEMA = {
|
|
23301
23520
|
type: "object",
|
|
@@ -23303,10 +23522,10 @@ var NPM_SCAFFOLD_SCHEMA = {
|
|
|
23303
23522
|
owner: { type: "string", description: "Repository owner (a configured owner)." },
|
|
23304
23523
|
repo: { type: "string", description: "Repository whose package.json names the package." },
|
|
23305
23524
|
package: { type: "string", description: 'Scoped package name, e.g. "@cldmv/new-thing". Must be under a scope in config.npm.scopes.' },
|
|
23306
|
-
|
|
23525
|
+
npm_otp: { type: "string", description: "npm one-time password the user read from their authenticator. Ask the user for it right before calling; codes expire in about 30 seconds." },
|
|
23307
23526
|
workflow: { type: "string", description: `The repo's publish workflow file in .github/workflows. Default "publish.yml".` }
|
|
23308
23527
|
},
|
|
23309
|
-
required: ["owner", "repo", "package", "
|
|
23528
|
+
required: ["owner", "repo", "package", "npm_otp"],
|
|
23310
23529
|
additionalProperties: false
|
|
23311
23530
|
};
|
|
23312
23531
|
var fail3 = (msg) => {
|
|
@@ -23320,7 +23539,7 @@ function validate3(input) {
|
|
|
23320
23539
|
if (typeof a.owner !== "string" || !/^[A-Za-z0-9-]+$/.test(a.owner)) fail3('"owner" must be a GitHub owner name');
|
|
23321
23540
|
if (typeof a.repo !== "string" || !/^[A-Za-z0-9._-]+$/.test(a.repo)) fail3('"repo" must be a repository name');
|
|
23322
23541
|
if (typeof a.package !== "string" || a.package.length > 214 || !PACKAGE_NAME.test(a.package)) fail3('"package" must be a lowercase scoped npm package name, e.g. "@cldmv/new-thing"');
|
|
23323
|
-
if (typeof a.
|
|
23542
|
+
if (typeof a.npm_otp !== "string" || !/^[0-9]{6,8}$/.test(a.npm_otp)) fail3('"npm_otp" must be the 6-8 digit npm code from the authenticator');
|
|
23324
23543
|
if (a.workflow !== void 0 && (typeof a.workflow !== "string" || !/^[A-Za-z0-9._-]+\.ya?ml$/.test(a.workflow))) {
|
|
23325
23544
|
fail3('"workflow" must be a workflow file name in .github/workflows, e.g. "publish.yml"');
|
|
23326
23545
|
}
|
|
@@ -23328,7 +23547,7 @@ function validate3(input) {
|
|
|
23328
23547
|
owner: a.owner,
|
|
23329
23548
|
repo: a.repo,
|
|
23330
23549
|
package: a.package,
|
|
23331
|
-
|
|
23550
|
+
npm_otp: a.npm_otp,
|
|
23332
23551
|
workflow: a.workflow ?? DEFAULT_WORKFLOW
|
|
23333
23552
|
};
|
|
23334
23553
|
}
|
|
@@ -23439,7 +23658,7 @@ async function npmScaffold(input, deps) {
|
|
|
23439
23658
|
const existing = await deps.registry.packument(args.package);
|
|
23440
23659
|
if (!existing) {
|
|
23441
23660
|
try {
|
|
23442
|
-
await deps.registry.publish(placeholderDocument(args.package, facts, access, deps.registry.registry), args.package, args.
|
|
23661
|
+
await deps.registry.publish(placeholderDocument(args.package, facts, access, deps.registry.registry), args.package, args.npm_otp);
|
|
23443
23662
|
} catch (err) {
|
|
23444
23663
|
if (err instanceof NpmOtpRequired) {
|
|
23445
23664
|
return { ...base, status: "needs_otp", done: [], remaining: ["publish", "trust"], message: "npm refused the one-time password before publishing; nothing was written. Call again with a fresh code." };
|
|
@@ -23464,7 +23683,7 @@ async function npmScaffold(input, deps) {
|
|
|
23464
23683
|
}
|
|
23465
23684
|
let out;
|
|
23466
23685
|
try {
|
|
23467
|
-
out = await deps.registry.addGithubTrust(args.package, { repository: facts.repository, file: args.workflow }, args.
|
|
23686
|
+
out = await deps.registry.addGithubTrust(args.package, { repository: facts.repository, file: args.workflow }, args.npm_otp);
|
|
23468
23687
|
} catch (err) {
|
|
23469
23688
|
if (err instanceof NpmOtpRequired) {
|
|
23470
23689
|
return {
|
|
@@ -23511,10 +23730,187 @@ async function npmScaffold(input, deps) {
|
|
|
23511
23730
|
};
|
|
23512
23731
|
}
|
|
23513
23732
|
|
|
23733
|
+
// src/verbs/minimize_comment.ts
|
|
23734
|
+
var MINIMIZE_CLASSIFIERS = ["RESOLVED", "OUTDATED", "DUPLICATE", "OFF_TOPIC", "SPAM", "ABUSE"];
|
|
23735
|
+
var MINIMIZE = `
|
|
23736
|
+
mutation($id: ID!, $classifier: ReportedContentClassifiers!) {
|
|
23737
|
+
minimizeComment(input: { subjectId: $id, classifier: $classifier }) {
|
|
23738
|
+
minimizedComment { isMinimized minimizedReason }
|
|
23739
|
+
}
|
|
23740
|
+
}`;
|
|
23741
|
+
var UNMINIMIZE = `
|
|
23742
|
+
mutation($id: ID!) {
|
|
23743
|
+
unminimizeComment(input: { subjectId: $id }) {
|
|
23744
|
+
unminimizedComment { isMinimized minimizedReason }
|
|
23745
|
+
}
|
|
23746
|
+
}`;
|
|
23747
|
+
async function nodeIdOf(octokit, input) {
|
|
23748
|
+
if (input.commentId !== void 0) {
|
|
23749
|
+
if (typeof input.commentId !== "string" || !input.commentId.trim()) throw new Error("minimize_comment: commentId must be a comment node id");
|
|
23750
|
+
if (input.commentNumber !== void 0) throw new Error("minimize_comment: pass commentId or commentNumber, not both");
|
|
23751
|
+
return input.commentId;
|
|
23752
|
+
}
|
|
23753
|
+
if (!Number.isSafeInteger(input.commentNumber) || input.commentNumber <= 0) {
|
|
23754
|
+
throw new Error("minimize_comment: pass commentId (node id), or commentNumber (REST id) with type and repo");
|
|
23755
|
+
}
|
|
23756
|
+
if (input.type !== "issue" && input.type !== "review") throw new Error('minimize_comment: commentNumber needs type "issue" (issue or PR conversation comment) or "review" (PR review comment)');
|
|
23757
|
+
if (typeof input.repo !== "string" || !input.repo) throw new Error("minimize_comment: commentNumber needs repo");
|
|
23758
|
+
const route = input.type === "issue" ? "GET /repos/{owner}/{repo}/issues/comments/{comment_id}" : "GET /repos/{owner}/{repo}/pulls/comments/{comment_id}";
|
|
23759
|
+
const res = await octokit.request(route, { owner: input.owner, repo: input.repo, comment_id: input.commentNumber });
|
|
23760
|
+
const nodeId = res.data.node_id;
|
|
23761
|
+
if (typeof nodeId !== "string" || !nodeId) throw new Error(`minimize_comment: GitHub returned no node id for ${input.type} comment ${input.commentNumber}`);
|
|
23762
|
+
return nodeId;
|
|
23763
|
+
}
|
|
23764
|
+
async function minimizeComment(octokit, input) {
|
|
23765
|
+
const classifier = input.classifier ?? "RESOLVED";
|
|
23766
|
+
if (input.unhide === true && input.classifier !== void 0) throw new Error("minimize_comment: unhide takes no classifier");
|
|
23767
|
+
if (!MINIMIZE_CLASSIFIERS.includes(classifier)) throw new Error(`minimize_comment: classifier must be one of ${MINIMIZE_CLASSIFIERS.join(", ")}`);
|
|
23768
|
+
const id = await nodeIdOf(octokit, input);
|
|
23769
|
+
if (input.unhide === true) {
|
|
23770
|
+
const data2 = await octokit.graphql(UNMINIMIZE, { id });
|
|
23771
|
+
const c2 = data2.unminimizeComment?.unminimizedComment;
|
|
23772
|
+
if (!c2) throw new Error(`minimize_comment: GitHub returned no comment for "${id}"`);
|
|
23773
|
+
return { commentId: id, isMinimized: c2.isMinimized, minimizedReason: c2.minimizedReason ?? null };
|
|
23774
|
+
}
|
|
23775
|
+
const data = await octokit.graphql(MINIMIZE, { id, classifier });
|
|
23776
|
+
const c = data.minimizeComment?.minimizedComment;
|
|
23777
|
+
if (!c) throw new Error(`minimize_comment: GitHub returned no comment for "${id}"`);
|
|
23778
|
+
return { commentId: id, isMinimized: c.isMinimized, minimizedReason: c.minimizedReason ?? null };
|
|
23779
|
+
}
|
|
23780
|
+
var MINIMIZE_COMMENT_DESCRIPTION = `Hide a comment on an issue or PR (collapsed, like "Hide \u2192 Resolved" in GitHub), or show it again with unhide: true. Meant for folding away bot review comments once they're addressed. Name the comment by node id (commentId, e.g. from pull_request_read get_review_comments), or by REST id (commentNumber) with type "issue" (issue or PR conversation comment) or "review" (PR review comment) and repo. Runs on the write PAT.`;
|
|
23781
|
+
var MINIMIZE_COMMENT_SCHEMA = {
|
|
23782
|
+
type: "object",
|
|
23783
|
+
required: ["owner"],
|
|
23784
|
+
additionalProperties: false,
|
|
23785
|
+
properties: {
|
|
23786
|
+
owner: { type: "string", description: "repository owner (org/user login); selects the write credential" },
|
|
23787
|
+
repo: { type: "string", description: "repository name (needed with commentNumber)" },
|
|
23788
|
+
commentId: { type: "string", description: 'comment node id, e.g. "IC_\u2026" or "PRRC_\u2026"' },
|
|
23789
|
+
commentNumber: { type: "number", description: "REST comment id, used with type and repo when the node id isn't at hand" },
|
|
23790
|
+
type: { type: "string", enum: ["issue", "review"], description: "with commentNumber: issue (issue or PR conversation comment) or review (PR review comment)" },
|
|
23791
|
+
classifier: { type: "string", enum: [...MINIMIZE_CLASSIFIERS], description: "why it's hidden (default RESOLVED)" },
|
|
23792
|
+
unhide: { type: "boolean", description: "true shows a hidden comment again" }
|
|
23793
|
+
}
|
|
23794
|
+
};
|
|
23795
|
+
|
|
23796
|
+
// src/verbs/delete_branch.ts
|
|
23797
|
+
var KEPT_BRANCHES = /* @__PURE__ */ new Set(["master", "main", "next", "hotfixes", "dev", "badges", "gh-pages"]);
|
|
23798
|
+
async function deleteBranch(octokit, input) {
|
|
23799
|
+
const { owner: owner2, repo } = input;
|
|
23800
|
+
if (typeof repo !== "string" || !repo) throw new Error("delete_branch: repo is required");
|
|
23801
|
+
const branch = typeof input.branch === "string" ? input.branch.replace(/^refs\/heads\//, "") : "";
|
|
23802
|
+
if (!branch) throw new Error("delete_branch: branch is required");
|
|
23803
|
+
if (KEPT_BRANCHES.has(branch)) throw new Error(`delete_branch: refusing to delete "${branch}": the release flow depends on it`);
|
|
23804
|
+
const meta2 = await octokit.request("GET /repos/{owner}/{repo}", { owner: owner2, repo });
|
|
23805
|
+
const repoData = meta2.data;
|
|
23806
|
+
const defaultBranch = repoData.default_branch;
|
|
23807
|
+
if (branch === defaultBranch) throw new Error(`delete_branch: refusing to delete "${branch}": it is ${owner2}/${repo}'s default branch`);
|
|
23808
|
+
const lookups = [
|
|
23809
|
+
{ side: "head", where: { owner: owner2, repo }, filter: { head: `${owner2}:${branch}` } },
|
|
23810
|
+
{ side: "base", where: { owner: owner2, repo }, filter: { base: branch } }
|
|
23811
|
+
];
|
|
23812
|
+
const upstreams = repoData.fork ? [repoData.parent, repoData.source] : [];
|
|
23813
|
+
const seen = /* @__PURE__ */ new Set();
|
|
23814
|
+
for (const up of upstreams) {
|
|
23815
|
+
if (!up?.owner?.login || !up.name || seen.has(`${up.owner.login}/${up.name}`)) continue;
|
|
23816
|
+
seen.add(`${up.owner.login}/${up.name}`);
|
|
23817
|
+
lookups.push({ side: "head", where: { owner: up.owner.login, repo: up.name }, filter: { head: `${owner2}:${branch}` } });
|
|
23818
|
+
}
|
|
23819
|
+
for (const { side, where, filter } of lookups) {
|
|
23820
|
+
const open2 = await octokit.request("GET /repos/{owner}/{repo}/pulls", { ...where, state: "open", ...filter, per_page: 10 });
|
|
23821
|
+
const prs = open2.data;
|
|
23822
|
+
if (prs.length > 0) {
|
|
23823
|
+
const ref2 = where.owner === owner2 && where.repo === repo ? "" : `${where.owner}/${where.repo}`;
|
|
23824
|
+
throw new Error(`delete_branch: refusing to delete "${branch}": it is the ${side} of open PR ${prs.map((p) => `${ref2}#${p.number}`).join(", ")}, which deleting it would close`);
|
|
23825
|
+
}
|
|
23826
|
+
}
|
|
23827
|
+
const missing = new Error(`delete_branch: no branch "${branch}" in ${owner2}/${repo}`);
|
|
23828
|
+
let ref;
|
|
23829
|
+
try {
|
|
23830
|
+
ref = await octokit.request("GET /repos/{owner}/{repo}/git/ref/{ref}", { owner: owner2, repo, ref: `heads/${branch}` });
|
|
23831
|
+
} catch (err) {
|
|
23832
|
+
throw err.status === 404 ? missing : err;
|
|
23833
|
+
}
|
|
23834
|
+
const sha = ref.data.object?.sha;
|
|
23835
|
+
if (typeof sha !== "string") throw missing;
|
|
23836
|
+
await octokit.request("DELETE /repos/{owner}/{repo}/git/refs/{ref}", { owner: owner2, repo, ref: `heads/${branch}` });
|
|
23837
|
+
return { deleted: branch, sha, restore: `git push origin ${sha}:refs/heads/${branch}` };
|
|
23838
|
+
}
|
|
23839
|
+
var DELETE_BRANCH_DESCRIPTION = "Delete one branch on GitHub. Refuses the repo's default branch, the release-flow branches (master, main, next, hotfixes, dev, badges, gh-pages), and any branch that is the head or base of an open PR, including a PR from a fork into its parent or the root of its fork network (deleting it would close the PR). Rulesets still apply. Returns the deleted tip's sha and the git command that restores it. Runs on the write PAT (Contents: write).";
|
|
23840
|
+
var DELETE_BRANCH_SCHEMA = {
|
|
23841
|
+
type: "object",
|
|
23842
|
+
required: ["owner", "repo", "branch"],
|
|
23843
|
+
additionalProperties: false,
|
|
23844
|
+
properties: {
|
|
23845
|
+
owner: { type: "string", description: "repository owner (org/user login); selects the write credential" },
|
|
23846
|
+
repo: { type: "string", description: "repository name" },
|
|
23847
|
+
branch: { type: "string", description: 'branch name, e.g. "feat/otp-gate" (a leading refs/heads/ is accepted)' }
|
|
23848
|
+
}
|
|
23849
|
+
};
|
|
23850
|
+
|
|
23851
|
+
// src/verbs/dismiss_alert.ts
|
|
23852
|
+
var ALERT_KINDS = ["code_scanning", "dependabot", "secret_scanning"];
|
|
23853
|
+
var DISMISS_REASONS = {
|
|
23854
|
+
code_scanning: ["false positive", "won't fix", "used in tests"],
|
|
23855
|
+
dependabot: ["fix_started", "inaccurate", "no_bandwidth", "not_used", "tolerable_risk"],
|
|
23856
|
+
secret_scanning: ["false_positive", "wont_fix", "revoked", "used_in_tests"]
|
|
23857
|
+
};
|
|
23858
|
+
var ROUTES = {
|
|
23859
|
+
code_scanning: "PATCH /repos/{owner}/{repo}/code-scanning/alerts/{alert_number}",
|
|
23860
|
+
dependabot: "PATCH /repos/{owner}/{repo}/dependabot/alerts/{alert_number}",
|
|
23861
|
+
secret_scanning: "PATCH /repos/{owner}/{repo}/secret-scanning/alerts/{alert_number}"
|
|
23862
|
+
};
|
|
23863
|
+
function bodyFor(kind, reopen, reason, comment) {
|
|
23864
|
+
if (kind === "secret_scanning") {
|
|
23865
|
+
if (reopen) return { state: "open", ...comment ? { resolution_comment: comment } : {} };
|
|
23866
|
+
return { state: "resolved", resolution: reason, ...comment ? { resolution_comment: comment } : {} };
|
|
23867
|
+
}
|
|
23868
|
+
if (reopen) return { state: "open" };
|
|
23869
|
+
return { state: "dismissed", dismissed_reason: reason, ...comment ? { dismissed_comment: comment } : {} };
|
|
23870
|
+
}
|
|
23871
|
+
async function dismissAlert(octokit, input) {
|
|
23872
|
+
const { owner: owner2, repo, kind } = input;
|
|
23873
|
+
if (typeof repo !== "string" || !repo) throw new Error("dismiss_alert: repo is required");
|
|
23874
|
+
if (!ALERT_KINDS.includes(kind)) throw new Error(`dismiss_alert: kind must be one of ${ALERT_KINDS.join(", ")}`);
|
|
23875
|
+
if (!Number.isSafeInteger(input.alertNumber) || input.alertNumber <= 0) throw new Error("dismiss_alert: alertNumber must be a positive integer");
|
|
23876
|
+
if (input.comment !== void 0 && typeof input.comment !== "string") throw new Error("dismiss_alert: comment must be a string");
|
|
23877
|
+
const reopen = input.reopen === true;
|
|
23878
|
+
if (reopen && input.reason !== void 0) throw new Error("dismiss_alert: a reopen takes no reason");
|
|
23879
|
+
if (reopen && input.comment !== void 0 && kind !== "secret_scanning") throw new Error(`dismiss_alert: GitHub stores no comment when a ${kind} alert is reopened; leave comment out`);
|
|
23880
|
+
if (!reopen && !DISMISS_REASONS[kind].includes(input.reason)) {
|
|
23881
|
+
throw new Error(`dismiss_alert: reason is required for a ${kind} alert, one of: ${DISMISS_REASONS[kind].map((r) => `"${r}"`).join(", ")}`);
|
|
23882
|
+
}
|
|
23883
|
+
const res = await octokit.request(ROUTES[kind], { owner: owner2, repo, alert_number: input.alertNumber, ...bodyFor(kind, reopen, input.reason, input.comment) });
|
|
23884
|
+
const d = res.data;
|
|
23885
|
+
const reason = kind === "secret_scanning" ? d.resolution : d.dismissed_reason;
|
|
23886
|
+
return {
|
|
23887
|
+
kind,
|
|
23888
|
+
number: typeof d.number === "number" ? d.number : input.alertNumber,
|
|
23889
|
+
state: typeof d.state === "string" ? d.state : "unknown",
|
|
23890
|
+
reason: typeof reason === "string" ? reason : null,
|
|
23891
|
+
url: typeof d.html_url === "string" ? d.html_url : null
|
|
23892
|
+
};
|
|
23893
|
+
}
|
|
23894
|
+
var DISMISS_ALERT_DESCRIPTION = `Dismiss one code scanning, Dependabot or secret scanning alert, or reopen it with reopen: true. A dismissal needs a reason, which depends on kind: code_scanning ${DISMISS_REASONS.code_scanning.map((r) => `"${r}"`).join(" / ")}; dependabot ${DISMISS_REASONS.dependabot.join(" / ")}; secret_scanning ${DISMISS_REASONS.secret_scanning.join(" / ")}. Returns the alert's number, state, reason and URL only; a secret's value is never returned. Runs on the write PAT.`;
|
|
23895
|
+
var DISMISS_ALERT_SCHEMA = {
|
|
23896
|
+
type: "object",
|
|
23897
|
+
required: ["owner", "repo", "kind", "alertNumber"],
|
|
23898
|
+
additionalProperties: false,
|
|
23899
|
+
properties: {
|
|
23900
|
+
owner: { type: "string", description: "repository owner (org/user login); selects the write credential" },
|
|
23901
|
+
repo: { type: "string", description: "repository name" },
|
|
23902
|
+
kind: { type: "string", enum: [...ALERT_KINDS], description: "which alert type" },
|
|
23903
|
+
alertNumber: { type: "number", description: "the alert number" },
|
|
23904
|
+
reason: { type: "string", description: "why it's dismissed; the accepted values depend on kind (see the description)" },
|
|
23905
|
+
comment: { type: "string", description: "optional note stored with the dismissal" },
|
|
23906
|
+
reopen: { type: "boolean", description: "true reopens the alert instead of dismissing it (no reason)" }
|
|
23907
|
+
}
|
|
23908
|
+
};
|
|
23909
|
+
|
|
23514
23910
|
// src/verbs/ghcr/archive.ts
|
|
23515
23911
|
import { closeSync, constants, fstatSync, openSync, readlinkSync, realpathSync } from "node:fs";
|
|
23516
23912
|
import { createHash as createHash2 } from "node:crypto";
|
|
23517
|
-
import { dirname, isAbsolute as
|
|
23913
|
+
import { dirname, isAbsolute as isAbsolute4, join as join3, normalize as normalize2 } from "node:path";
|
|
23518
23914
|
|
|
23519
23915
|
// src/verbs/ghcr/tar.ts
|
|
23520
23916
|
import { readSync } from "node:fs";
|
|
@@ -23685,7 +24081,7 @@ var fail5 = (msg) => {
|
|
|
23685
24081
|
};
|
|
23686
24082
|
function openArchive(input, dropDir, maxBytes) {
|
|
23687
24083
|
if (typeof input !== "string" || !input || input.length > 4096 || input.includes("\0")) fail5('"archive" must be a file path inside the drop directory');
|
|
23688
|
-
const candidate = normalize2(
|
|
24084
|
+
const candidate = normalize2(isAbsolute4(input) ? input : join3(dropDir, input));
|
|
23689
24085
|
const outside = `"archive" must be an existing file inside the drop directory ${dropDir}`;
|
|
23690
24086
|
if (!candidate.startsWith(`${dropDir}/`)) fail5(outside);
|
|
23691
24087
|
let realDrop;
|
|
@@ -24400,6 +24796,329 @@ var GHCR_CATALOG = {
|
|
|
24400
24796
|
}
|
|
24401
24797
|
};
|
|
24402
24798
|
|
|
24799
|
+
// src/otp/gate.ts
|
|
24800
|
+
import { createHmac as createHmac2, randomBytes as randomBytes2 } from "node:crypto";
|
|
24801
|
+
import { hostname } from "node:os";
|
|
24802
|
+
|
|
24803
|
+
// src/otp/state.ts
|
|
24804
|
+
import { closeSync as closeSync2, fstatSync as fstatSync2, fsyncSync, openSync as openSync2, readFileSync as readFileSync3, renameSync, statSync, unlinkSync, writeSync } from "node:fs";
|
|
24805
|
+
import { randomBytes } from "node:crypto";
|
|
24806
|
+
import { dirname as dirname2 } from "node:path";
|
|
24807
|
+
function emptyState() {
|
|
24808
|
+
return { version: 1, lastAcceptedStep: 0, users: {} };
|
|
24809
|
+
}
|
|
24810
|
+
function emptyUser() {
|
|
24811
|
+
return { failures: 0, lockedUntil: null, lockouts: [], hardLock: null };
|
|
24812
|
+
}
|
|
24813
|
+
function userState(state, user) {
|
|
24814
|
+
const u = Object.hasOwn(state.users, user) ? state.users[user] : emptyUser();
|
|
24815
|
+
return { ...u, lockouts: [...u.lockouts] };
|
|
24816
|
+
}
|
|
24817
|
+
var isIso = (v) => typeof v === "string" && !Number.isNaN(Date.parse(v));
|
|
24818
|
+
var isCount = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
|
|
24819
|
+
var isObject3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
24820
|
+
function parseUser(raw) {
|
|
24821
|
+
if (!isObject3(raw) || !isCount(raw.failures)) return void 0;
|
|
24822
|
+
if (raw.lockedUntil !== null && !isIso(raw.lockedUntil)) return void 0;
|
|
24823
|
+
if (!Array.isArray(raw.lockouts) || !raw.lockouts.every(isIso)) return void 0;
|
|
24824
|
+
let hardLock = null;
|
|
24825
|
+
if (raw.hardLock !== null) {
|
|
24826
|
+
const h = raw.hardLock;
|
|
24827
|
+
if (!isObject3(h) || !isIso(h.since) || !isCount(h.lockouts)) return void 0;
|
|
24828
|
+
hardLock = { since: h.since, lockouts: h.lockouts };
|
|
24829
|
+
}
|
|
24830
|
+
return { failures: raw.failures, lockedUntil: raw.lockedUntil, lockouts: [...raw.lockouts], hardLock };
|
|
24831
|
+
}
|
|
24832
|
+
function parseState(raw) {
|
|
24833
|
+
if (!isObject3(raw) || raw.version !== 1 || !isCount(raw.lastAcceptedStep) || !isObject3(raw.users)) return void 0;
|
|
24834
|
+
const users = {};
|
|
24835
|
+
for (const [name, entry] of Object.entries(raw.users)) {
|
|
24836
|
+
const u = parseUser(entry);
|
|
24837
|
+
if (!u || !isAgentUser(name)) return void 0;
|
|
24838
|
+
users[name] = u;
|
|
24839
|
+
}
|
|
24840
|
+
return { version: 1, lastAcceptedStep: raw.lastAcceptedStep, users };
|
|
24841
|
+
}
|
|
24842
|
+
function signature(path) {
|
|
24843
|
+
try {
|
|
24844
|
+
const st = statSync(path);
|
|
24845
|
+
return `${st.ino}:${st.mtimeMs}:${st.size}`;
|
|
24846
|
+
} catch (err) {
|
|
24847
|
+
if (err.code === "ENOENT") return "missing";
|
|
24848
|
+
return `error:${err.code}`;
|
|
24849
|
+
}
|
|
24850
|
+
}
|
|
24851
|
+
var OtpStateFile = class {
|
|
24852
|
+
path;
|
|
24853
|
+
now;
|
|
24854
|
+
sig;
|
|
24855
|
+
cached;
|
|
24856
|
+
constructor(path, now = Date.now) {
|
|
24857
|
+
this.path = path;
|
|
24858
|
+
this.now = now;
|
|
24859
|
+
}
|
|
24860
|
+
/** The current state, re-read from disk only when the file changed. */
|
|
24861
|
+
read() {
|
|
24862
|
+
const sig = signature(this.path);
|
|
24863
|
+
if (this.cached && sig === this.sig) return this.cached;
|
|
24864
|
+
this.sig = sig;
|
|
24865
|
+
this.cached = this.load(sig);
|
|
24866
|
+
return this.cached;
|
|
24867
|
+
}
|
|
24868
|
+
load(sig) {
|
|
24869
|
+
if (sig === "missing") return { ok: true, state: emptyState() };
|
|
24870
|
+
let state;
|
|
24871
|
+
let why = "unexpected shape";
|
|
24872
|
+
try {
|
|
24873
|
+
state = parseState(JSON.parse(readFileSync3(this.path, "utf8")));
|
|
24874
|
+
} catch (err) {
|
|
24875
|
+
why = err.message;
|
|
24876
|
+
}
|
|
24877
|
+
if (state) return { ok: true, state };
|
|
24878
|
+
const since = new Date(this.now()).toISOString();
|
|
24879
|
+
console.error(`gh-broker: OTP state file ${this.path} is unreadable or corrupt (${why}); failing closed: every tool except otp_status is locked for every user until "gh-broker otp unlock" rewrites it`);
|
|
24880
|
+
return { ok: false, since };
|
|
24881
|
+
}
|
|
24882
|
+
/**
|
|
24883
|
+
* Persist `state` atomically. On success the cache holds the new state; on failure the
|
|
24884
|
+
* cache still holds it (so in-process counters keep counting) and the error is thrown,
|
|
24885
|
+
* so the caller can refuse anything that depends on the write having landed.
|
|
24886
|
+
*/
|
|
24887
|
+
write(state) {
|
|
24888
|
+
this.cached = { ok: true, state };
|
|
24889
|
+
const tmp = `${this.path}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
|
|
24890
|
+
let written;
|
|
24891
|
+
try {
|
|
24892
|
+
const fd = openSync2(tmp, "wx", 384);
|
|
24893
|
+
try {
|
|
24894
|
+
writeSync(fd, `${JSON.stringify(state, null, " ")}
|
|
24895
|
+
`);
|
|
24896
|
+
fsyncSync(fd);
|
|
24897
|
+
const st = fstatSync2(fd);
|
|
24898
|
+
written = `${st.ino}:${st.mtimeMs}:${st.size}`;
|
|
24899
|
+
} finally {
|
|
24900
|
+
closeSync2(fd);
|
|
24901
|
+
}
|
|
24902
|
+
renameSync(tmp, this.path);
|
|
24903
|
+
} catch (err) {
|
|
24904
|
+
try {
|
|
24905
|
+
unlinkSync(tmp);
|
|
24906
|
+
} catch {
|
|
24907
|
+
}
|
|
24908
|
+
this.sig = signature(this.path);
|
|
24909
|
+
throw new Error(`OTP state could not be saved to ${this.path}: ${err.message}`);
|
|
24910
|
+
}
|
|
24911
|
+
this.sig = written;
|
|
24912
|
+
syncDir(dirname2(this.path));
|
|
24913
|
+
}
|
|
24914
|
+
};
|
|
24915
|
+
function syncDir(dir) {
|
|
24916
|
+
try {
|
|
24917
|
+
const fd = openSync2(dir, "r");
|
|
24918
|
+
try {
|
|
24919
|
+
fsyncSync(fd);
|
|
24920
|
+
} finally {
|
|
24921
|
+
closeSync2(fd);
|
|
24922
|
+
}
|
|
24923
|
+
} catch {
|
|
24924
|
+
}
|
|
24925
|
+
}
|
|
24926
|
+
|
|
24927
|
+
// src/otp/gate.ts
|
|
24928
|
+
var OtpRefusal = class extends Error {
|
|
24929
|
+
audit;
|
|
24930
|
+
constructor(message, audit2) {
|
|
24931
|
+
super(message);
|
|
24932
|
+
this.name = "OtpRefusal";
|
|
24933
|
+
this.audit = audit2;
|
|
24934
|
+
}
|
|
24935
|
+
};
|
|
24936
|
+
var UNLOCK_COMMAND = "sudo gh-broker otp unlock";
|
|
24937
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
24938
|
+
var UNLOCK_ALL_COMMAND = "sudo gh-broker otp unlock --all";
|
|
24939
|
+
function unlockCommand(user) {
|
|
24940
|
+
return `${UNLOCK_COMMAND} ${user}`;
|
|
24941
|
+
}
|
|
24942
|
+
var OtpGate = class {
|
|
24943
|
+
config;
|
|
24944
|
+
secrets;
|
|
24945
|
+
settings;
|
|
24946
|
+
now;
|
|
24947
|
+
host;
|
|
24948
|
+
state;
|
|
24949
|
+
/** Per-process key for window ids. */
|
|
24950
|
+
idKey = randomBytes2(32);
|
|
24951
|
+
windows = [];
|
|
24952
|
+
/** Users whose latest bad-code count is only in memory because the state file couldn't be written. */
|
|
24953
|
+
unsaved = /* @__PURE__ */ new Set();
|
|
24954
|
+
constructor(config2, secrets, options = {}) {
|
|
24955
|
+
this.config = config2;
|
|
24956
|
+
this.secrets = secrets;
|
|
24957
|
+
this.settings = otpSettings(config2);
|
|
24958
|
+
this.now = options.now ?? Date.now;
|
|
24959
|
+
this.host = options.host ?? hostname();
|
|
24960
|
+
this.state = new OtpStateFile(this.settings.statePath, this.now);
|
|
24961
|
+
}
|
|
24962
|
+
/** Whether the gate runs at all. Without config.otp no state file is ever touched. */
|
|
24963
|
+
get enabled() {
|
|
24964
|
+
return this.config.otp !== void 0;
|
|
24965
|
+
}
|
|
24966
|
+
/**
|
|
24967
|
+
* The hard-lock refusal for `user`, if that user is hard-locked (or the state file is
|
|
24968
|
+
* unusable, which locks everyone). The router checks this on every call except otp_status.
|
|
24969
|
+
*/
|
|
24970
|
+
hardLockRefusal(user) {
|
|
24971
|
+
if (!this.enabled) return void 0;
|
|
24972
|
+
const read = this.state.read();
|
|
24973
|
+
if (!read.ok) {
|
|
24974
|
+
return new OtpRefusal(
|
|
24975
|
+
`gh-broker is locked for every agent: its OTP state file is unreadable or corrupt (since ${read.since}). Ask the user to run "${UNLOCK_ALL_COMMAND}" on ${this.host}. Nothing was run.`,
|
|
24976
|
+
{ otp: "hard-locked" }
|
|
24977
|
+
);
|
|
24978
|
+
}
|
|
24979
|
+
const since = userState(read.state, user).hardLock?.since;
|
|
24980
|
+
if (since === void 0) return void 0;
|
|
24981
|
+
return new OtpRefusal(
|
|
24982
|
+
`gh-broker is locked for ${user} after repeated bad OTP codes (since ${since}). Ask the user to run "${unlockCommand(user)}" on ${this.host}. Nothing was run.`,
|
|
24983
|
+
{ otp: "hard-locked" }
|
|
24984
|
+
);
|
|
24985
|
+
}
|
|
24986
|
+
/**
|
|
24987
|
+
* Decide `user`'s call to the gated tool `tool` carrying `code` (the raw `otp` argument).
|
|
24988
|
+
* Returns the audit fields when the call may run; throws OtpRefusal when it may not.
|
|
24989
|
+
* Only call this after hardLockRefusal() and the availability check passed (OTP is set up).
|
|
24990
|
+
*/
|
|
24991
|
+
authorize(tool, code, user) {
|
|
24992
|
+
const now = this.now();
|
|
24993
|
+
const read = this.state.read();
|
|
24994
|
+
if (!read.ok || userState(read.state, user).hardLock) throw this.hardLockRefusal(user);
|
|
24995
|
+
const state = read.state;
|
|
24996
|
+
const mine = userState(state, user);
|
|
24997
|
+
const lockedUntil = mine.lockedUntil ? Date.parse(mine.lockedUntil) : 0;
|
|
24998
|
+
if (lockedUntil > now) {
|
|
24999
|
+
const minutes = Math.max(1, Math.ceil((lockedUntil - now) / 6e4));
|
|
25000
|
+
throw new OtpRefusal(`OTP is locked for ${minutes} more minute(s) after too many bad codes. Nothing was run.`, { otp: "locked" });
|
|
25001
|
+
}
|
|
25002
|
+
const unsavedRefusal = () => new OtpRefusal(`OTP could not be checked: the broker could not save its OTP state. Nothing was run.`, { otp: "refused" });
|
|
25003
|
+
if (this.unsaved.has(user)) {
|
|
25004
|
+
try {
|
|
25005
|
+
this.state.write(this.compact(state, now));
|
|
25006
|
+
this.unsaved.delete(user);
|
|
25007
|
+
} catch (err) {
|
|
25008
|
+
console.error(`gh-broker: ${err.message}`);
|
|
25009
|
+
throw unsavedRefusal();
|
|
25010
|
+
}
|
|
25011
|
+
}
|
|
25012
|
+
const refusal = () => new OtpRefusal(`OTP required for ${tool}: ask the user for a current code from their authenticator app and pass it as "${OTP_ARG}". Nothing was run.`, { otp: "refused" });
|
|
25013
|
+
if (code === void 0 || code === null || code === "") throw refusal();
|
|
25014
|
+
if (typeof code === "number" && Number.isInteger(code) && code >= 0 && code <= 999999) code = String(code).padStart(6, "0");
|
|
25015
|
+
if (typeof code !== "string") throw refusal();
|
|
25016
|
+
this.windows = this.windows.filter((w) => w.expiresAt > now);
|
|
25017
|
+
let hit;
|
|
25018
|
+
for (const w of this.windows) if (w.user === user && safeEqual(w.code, code)) hit = w;
|
|
25019
|
+
if (hit) return { otp: "window", window: hit.id };
|
|
25020
|
+
const step = matchStep(readSeed(this.config, this.secrets).key, code, now);
|
|
25021
|
+
if (step !== void 0 && step > state.lastAcceptedStep) {
|
|
25022
|
+
try {
|
|
25023
|
+
this.state.write(this.compact({ ...state, lastAcceptedStep: step, users: { ...state.users, [user]: { ...mine, failures: 0 } } }, now));
|
|
25024
|
+
} catch (err) {
|
|
25025
|
+
console.error(`gh-broker: ${err.message}`);
|
|
25026
|
+
throw unsavedRefusal();
|
|
25027
|
+
}
|
|
25028
|
+
const id = createHmac2("sha256", this.idKey).update(`${user}\0${code}`).digest("hex").slice(0, 16);
|
|
25029
|
+
const expiresAt = now + this.settings.windowSeconds * 1e3;
|
|
25030
|
+
this.windows.push({ user, code, expiresAt, id });
|
|
25031
|
+
return { otp: "opened", window: id, until: new Date(expiresAt).toISOString() };
|
|
25032
|
+
}
|
|
25033
|
+
if (step !== void 0) throw refusal();
|
|
25034
|
+
this.recordFailure(tool, state, mine, user, now);
|
|
25035
|
+
throw refusal();
|
|
25036
|
+
}
|
|
25037
|
+
/** Count one bad code for `user`, starting a short lockout (and maybe the hard lock) at the limit. */
|
|
25038
|
+
recordFailure(tool, state, mine, user, now) {
|
|
25039
|
+
const next = { ...mine, lockouts: mine.lockouts.filter((t) => Date.parse(t) > now - DAY_MS) };
|
|
25040
|
+
next.failures += 1;
|
|
25041
|
+
if (next.failures >= this.settings.maxFailures) {
|
|
25042
|
+
const at = new Date(now).toISOString();
|
|
25043
|
+
next.failures = 0;
|
|
25044
|
+
next.lockedUntil = new Date(now + this.settings.lockoutSeconds * 1e3).toISOString();
|
|
25045
|
+
next.lockouts.push(at);
|
|
25046
|
+
if (next.lockouts.length >= this.settings.maxLockouts) next.hardLock = { since: at, lockouts: next.lockouts.length };
|
|
25047
|
+
audit(
|
|
25048
|
+
next.hardLock ? { tool, user, decision: "deny", otp: "hard-lock", detail: `${next.lockouts.length} lockouts in 24 h; every tool except otp_status is refused until "${unlockCommand(user)}"` } : { tool, user, decision: "deny", otp: "lockout", until: next.lockedUntil, detail: `${this.settings.maxFailures} bad codes` }
|
|
25049
|
+
);
|
|
25050
|
+
}
|
|
25051
|
+
try {
|
|
25052
|
+
this.state.write(this.compact({ ...state, users: { ...state.users, [user]: next } }, now));
|
|
25053
|
+
} catch (err) {
|
|
25054
|
+
this.unsaved.add(user);
|
|
25055
|
+
console.error(`gh-broker: ${err.message}`);
|
|
25056
|
+
}
|
|
25057
|
+
}
|
|
25058
|
+
/**
|
|
25059
|
+
* `state` tidied for a write: every user's lockouts pruned to the last 24 h, expired short
|
|
25060
|
+
* lockouts cleared, and users left with nothing to record dropped.
|
|
25061
|
+
*/
|
|
25062
|
+
compact(state, now) {
|
|
25063
|
+
const users = {};
|
|
25064
|
+
for (const [name, u] of Object.entries(state.users)) {
|
|
25065
|
+
const lockouts = u.lockouts.filter((t) => Date.parse(t) > now - DAY_MS);
|
|
25066
|
+
const lockedUntil = u.lockedUntil && Date.parse(u.lockedUntil) > now ? u.lockedUntil : null;
|
|
25067
|
+
if (u.failures === 0 && lockedUntil === null && lockouts.length === 0 && u.hardLock === null) continue;
|
|
25068
|
+
users[name] = { failures: u.failures, lockedUntil, lockouts, hardLock: u.hardLock };
|
|
25069
|
+
}
|
|
25070
|
+
return { ...state, users };
|
|
25071
|
+
}
|
|
25072
|
+
/**
|
|
25073
|
+
* The otp_status report for `user`: their own windows and counters, plus every hard-locked
|
|
25074
|
+
* user. Never a code, the seed, or anything derived from them.
|
|
25075
|
+
*/
|
|
25076
|
+
status(user) {
|
|
25077
|
+
const now = this.now();
|
|
25078
|
+
const gatedTools = this.config.tools.filter((t) => isGated(t) && isEnabled(t)).map((t) => t.name);
|
|
25079
|
+
let mine = emptyUser();
|
|
25080
|
+
let hardLock = null;
|
|
25081
|
+
let lockedUsers = [];
|
|
25082
|
+
let corrupt = false;
|
|
25083
|
+
if (this.enabled) {
|
|
25084
|
+
const read = this.state.read();
|
|
25085
|
+
if (read.ok) {
|
|
25086
|
+
mine = userState(read.state, user);
|
|
25087
|
+
hardLock = mine.hardLock;
|
|
25088
|
+
lockedUsers = Object.entries(read.state.users).filter(([, u]) => u.hardLock).map(([name, u]) => ({ user: name, since: u.hardLock.since, lockouts: u.hardLock.lockouts, unlockCommand: unlockCommand(name) }));
|
|
25089
|
+
} else {
|
|
25090
|
+
hardLock = { since: read.since, lockouts: 0 };
|
|
25091
|
+
corrupt = true;
|
|
25092
|
+
lockedUsers = [{ user, since: read.since, lockouts: 0, unlockCommand: UNLOCK_ALL_COMMAND }];
|
|
25093
|
+
}
|
|
25094
|
+
}
|
|
25095
|
+
const open2 = this.windows.filter((w) => w.user === user && w.expiresAt > now);
|
|
25096
|
+
const next = open2.length ? Math.min(...open2.map((w) => w.expiresAt)) : void 0;
|
|
25097
|
+
const lockedUntil = mine.lockedUntil && Date.parse(mine.lockedUntil) > now ? mine.lockedUntil : null;
|
|
25098
|
+
return {
|
|
25099
|
+
configured: !("problem" in readSeed(this.config, this.secrets)),
|
|
25100
|
+
user,
|
|
25101
|
+
gatedTools,
|
|
25102
|
+
windowSeconds: this.settings.windowSeconds,
|
|
25103
|
+
openWindows: open2.length,
|
|
25104
|
+
nextWindowExpiresAt: next === void 0 ? null : new Date(next).toISOString(),
|
|
25105
|
+
failures: mine.failures,
|
|
25106
|
+
maxFailures: this.settings.maxFailures,
|
|
25107
|
+
lockedUntil,
|
|
25108
|
+
lockoutsLast24h: mine.lockouts.filter((t) => Date.parse(t) > now - DAY_MS).length,
|
|
25109
|
+
maxLockouts: this.settings.maxLockouts,
|
|
25110
|
+
hardLock,
|
|
25111
|
+
unlockCommand: corrupt ? UNLOCK_ALL_COMMAND : unlockCommand(user),
|
|
25112
|
+
lockedUsers,
|
|
25113
|
+
host: this.host
|
|
25114
|
+
};
|
|
25115
|
+
}
|
|
25116
|
+
};
|
|
25117
|
+
function windowText(seconds) {
|
|
25118
|
+
if (seconds % 60 === 0) return `${seconds / 60} minute${seconds === 60 ? "" : "s"}`;
|
|
25119
|
+
return `${seconds} seconds`;
|
|
25120
|
+
}
|
|
25121
|
+
|
|
24403
25122
|
// src/router.ts
|
|
24404
25123
|
var GHCR_TOOLS2 = /* @__PURE__ */ new Set(["ghcr_push", "ghcr_push_status", "ghcr_tags"]);
|
|
24405
25124
|
var Router = class {
|
|
@@ -24410,65 +25129,110 @@ var Router = class {
|
|
|
24410
25129
|
rules;
|
|
24411
25130
|
/** GHCR verbs run on config.ghcr's own credential, not the owner's GitHub PAT (created on first use). */
|
|
24412
25131
|
ghcr;
|
|
24413
|
-
|
|
25132
|
+
/** The OTP gate (#143). Inert without config.otp. */
|
|
25133
|
+
otp;
|
|
25134
|
+
constructor(config2, secrets, upstream, appAuth, otp) {
|
|
24414
25135
|
this.config = config2;
|
|
24415
25136
|
this.secrets = secrets;
|
|
24416
25137
|
this.upstream = upstream;
|
|
24417
25138
|
this.appAuth = appAuth;
|
|
24418
25139
|
this.rules = indexTools(config2.tools);
|
|
25140
|
+
this.otp = otp ?? new OtpGate(config2, secrets);
|
|
24419
25141
|
}
|
|
24420
25142
|
/** Tools advertised to the agent (used to build tools/list). */
|
|
24421
25143
|
advertisedTools() {
|
|
24422
25144
|
return this.config.tools.filter((rule) => !unavailableReason(rule, this.config, this.secrets));
|
|
24423
25145
|
}
|
|
24424
|
-
|
|
25146
|
+
/** Run `tool` for `caller`, the agent user whose socket the call came in on. */
|
|
25147
|
+
async call(tool, rawArgs, caller) {
|
|
25148
|
+
const user = caller.user;
|
|
24425
25149
|
const rule = this.rules.get(tool);
|
|
24426
25150
|
if (!rule) {
|
|
24427
|
-
audit({ tool, decision: "deny", detail: "not in allowlist" });
|
|
25151
|
+
audit({ tool, user, decision: "deny", detail: "not in allowlist" });
|
|
24428
25152
|
throw new Error(`tool "${tool}" is not allowed`);
|
|
24429
25153
|
}
|
|
25154
|
+
if (tool !== OTP_STATUS) {
|
|
25155
|
+
const locked = this.otp.hardLockRefusal(user);
|
|
25156
|
+
if (locked) {
|
|
25157
|
+
audit({ tool, user, decision: "deny", detail: "otp hard lock", ...locked.audit });
|
|
25158
|
+
throw locked;
|
|
25159
|
+
}
|
|
25160
|
+
}
|
|
25161
|
+
let args = rawArgs;
|
|
25162
|
+
let code;
|
|
25163
|
+
if (Object.hasOwn(rawArgs, OTP_ARG)) {
|
|
25164
|
+
const { [OTP_ARG]: given, ...rest } = rawArgs;
|
|
25165
|
+
code = given;
|
|
25166
|
+
args = rest;
|
|
25167
|
+
}
|
|
24430
25168
|
const unavailable = unavailableReason(rule, this.config, this.secrets);
|
|
24431
25169
|
if (unavailable) {
|
|
24432
|
-
audit({ tool, decision: "deny", detail: `unavailable: ${unavailable}` });
|
|
25170
|
+
audit({ tool, user, decision: "deny", detail: `unavailable: ${unavailable}` });
|
|
24433
25171
|
throw new Error(`tool "${tool}" is not available on this broker: ${unavailable}`);
|
|
24434
25172
|
}
|
|
24435
|
-
if (
|
|
24436
|
-
|
|
25173
|
+
if (tool === OTP_STATUS) {
|
|
25174
|
+
audit({ tool, user, mode: rule.mode, decision: "custom", ok: true });
|
|
25175
|
+
return { content: [{ type: "text", text: JSON.stringify(this.otp.status(user)) }] };
|
|
25176
|
+
}
|
|
25177
|
+
let createdIn;
|
|
25178
|
+
if (REPO_CREATING.has(tool) || tool === "actions_run_trigger") {
|
|
25179
|
+
try {
|
|
25180
|
+
if (tool === "actions_run_trigger") refuseLogDeletion(args);
|
|
25181
|
+
else createdIn = creationOwner(tool, args, this.config.owners);
|
|
25182
|
+
} catch (err) {
|
|
25183
|
+
const requested = typeof args.organization === "string" ? args.organization : extractOwner(args);
|
|
25184
|
+
audit({ tool, user, mode: rule.mode, ...requested ? { owner: requested.toLowerCase() } : {}, decision: "error", detail: err.message });
|
|
25185
|
+
throw err;
|
|
25186
|
+
}
|
|
25187
|
+
}
|
|
25188
|
+
let otp;
|
|
25189
|
+
if (isGated(rule)) {
|
|
25190
|
+
try {
|
|
25191
|
+
otp = this.otp.authorize(tool, code, user);
|
|
25192
|
+
} catch (err) {
|
|
25193
|
+
audit({ tool, user, decision: "deny", detail: "otp", ...err.audit });
|
|
25194
|
+
throw err;
|
|
25195
|
+
}
|
|
25196
|
+
}
|
|
25197
|
+
if (GHCR_TOOLS2.has(tool)) return this.callGhcr(tool, args, rule, user, otp);
|
|
25198
|
+
let owner2 = createdIn ?? extractOwner(args);
|
|
24437
25199
|
if (!owner2 && rule.mode === "read" && this.config.defaultOwner && this.config.owners[this.config.defaultOwner]) {
|
|
24438
25200
|
owner2 = this.config.defaultOwner.toLowerCase();
|
|
24439
25201
|
}
|
|
24440
25202
|
if (!owner2) {
|
|
24441
|
-
audit({ tool, decision: "error", detail: "no owner in args" });
|
|
25203
|
+
audit({ tool, user, decision: "error", detail: "no owner in args" });
|
|
24442
25204
|
throw new Error(`tool "${tool}": could not determine repo owner from arguments`);
|
|
24443
25205
|
}
|
|
24444
25206
|
const creds = this.config.owners[owner2];
|
|
24445
25207
|
if (!creds) {
|
|
24446
|
-
audit({ tool, owner: owner2, decision: "error", detail: "no creds for owner" });
|
|
25208
|
+
audit({ tool, user, owner: owner2, decision: "error", detail: "no creds for owner" });
|
|
24447
25209
|
throw new Error(`no credentials configured for owner "${owner2}" \u2014 add it to config.owners + creds`);
|
|
24448
25210
|
}
|
|
24449
25211
|
const token = this.secrets.resolve(rule.mode === "write" ? creds.write : creds.read);
|
|
24450
|
-
const
|
|
25212
|
+
const repoName = tool === "create_repository" ? args.name : args.repo;
|
|
25213
|
+
const repo = typeof repoName === "string" ? `${owner2}/${repoName}` : owner2;
|
|
25214
|
+
const forked = tool === "fork_repository" ? { source: `${String(args.owner)}/${String(args.repo)}` } : {};
|
|
24451
25215
|
const decision = rule.custom ? "custom" : rule.augment ? "augment" : "forward";
|
|
24452
25216
|
try {
|
|
24453
25217
|
const out = rule.custom ? await this.dispatchCustom(tool, args, owner2, token) : rule.augment ? await this.dispatchAugmented(tool, args, owner2, token) : await this.upstream.forward(tool, args, token);
|
|
24454
|
-
audit({ tool, owner: owner2, mode: rule.mode, repo, decision, ok: true });
|
|
25218
|
+
audit({ tool, user, owner: owner2, mode: rule.mode, repo, ...forked, decision, ok: true, ...otp });
|
|
24455
25219
|
return out;
|
|
24456
25220
|
} catch (err) {
|
|
24457
|
-
audit({ tool, owner: owner2, mode: rule.mode, repo, decision, ok: false, detail: err.message });
|
|
25221
|
+
audit({ tool, user, owner: owner2, mode: rule.mode, repo, ...forked, decision, ok: false, detail: err.message, ...otp });
|
|
24458
25222
|
throw err;
|
|
24459
25223
|
}
|
|
24460
25224
|
}
|
|
24461
25225
|
/** GHCR verbs: config.ghcr checks the owner and image itself; no GitHub PAT is resolved. */
|
|
24462
|
-
async callGhcr(tool, args, rule) {
|
|
25226
|
+
async callGhcr(tool, args, rule, user, otp) {
|
|
24463
25227
|
const owner2 = extractOwner(args) ?? "";
|
|
24464
25228
|
const repo = typeof args.image === "string" ? `${owner2}/${args.image}` : owner2;
|
|
24465
25229
|
try {
|
|
24466
25230
|
this.ghcr ??= new GhcrService(this.config.ghcr, this.secrets);
|
|
24467
25231
|
const out = tool === "ghcr_push" ? await this.ghcr.push(args) : tool === "ghcr_tags" ? await this.ghcr.tags(args) : this.ghcr.status(args);
|
|
24468
|
-
audit({ tool, owner: owner2, mode: rule.mode, repo, decision: "custom", ok: true });
|
|
25232
|
+
audit({ tool, user, owner: owner2, mode: rule.mode, repo, decision: "custom", ok: true, ...otp });
|
|
24469
25233
|
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
24470
25234
|
} catch (err) {
|
|
24471
|
-
audit({ tool, owner: owner2, mode: rule.mode, repo, decision: "custom", ok: false, detail: err.message });
|
|
25235
|
+
audit({ tool, user, owner: owner2, mode: rule.mode, repo, decision: "custom", ok: false, detail: err.message, ...otp });
|
|
24472
25236
|
throw err;
|
|
24473
25237
|
}
|
|
24474
25238
|
}
|
|
@@ -24543,6 +25307,18 @@ var Router = class {
|
|
|
24543
25307
|
const out = await npmScaffold(args, { octokit, registry: registry2, scopes: npm.scopes });
|
|
24544
25308
|
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
24545
25309
|
}
|
|
25310
|
+
case "minimize_comment": {
|
|
25311
|
+
const out = await minimizeComment(new Octokit({ auth: token }), args);
|
|
25312
|
+
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
25313
|
+
}
|
|
25314
|
+
case "delete_branch": {
|
|
25315
|
+
const out = await deleteBranch(new Octokit({ auth: token }), args);
|
|
25316
|
+
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
25317
|
+
}
|
|
25318
|
+
case "dismiss_alert": {
|
|
25319
|
+
const out = await dismissAlert(new Octokit({ auth: token }), args);
|
|
25320
|
+
return { content: [{ type: "text", text: JSON.stringify(out) }] };
|
|
25321
|
+
}
|
|
24546
25322
|
default:
|
|
24547
25323
|
throw new Error(`custom verb "${tool}" not implemented`);
|
|
24548
25324
|
}
|
|
@@ -24649,6 +25425,21 @@ var Router = class {
|
|
|
24649
25425
|
}
|
|
24650
25426
|
}
|
|
24651
25427
|
};
|
|
25428
|
+
var REPO_CREATING = /* @__PURE__ */ new Set(["create_repository", "fork_repository"]);
|
|
25429
|
+
function creationOwner(tool, args, owners) {
|
|
25430
|
+
if (typeof args.organization !== "string" || !args.organization) {
|
|
25431
|
+
throw new Error(`tool "${tool}": "organization" is required (the org the repo is created in); gh-broker doesn't create repos in a personal account`);
|
|
25432
|
+
}
|
|
25433
|
+
const org = args.organization.toLowerCase();
|
|
25434
|
+
if (!owners[org]) throw new Error(`no credentials configured for owner "${org}" \u2014 add it to config.owners + creds`);
|
|
25435
|
+
if (tool === "create_repository" && typeof args.private !== "boolean") {
|
|
25436
|
+
throw new Error(`tool "create_repository": "private" is required, true or false; a repo's visibility is never left to a default`);
|
|
25437
|
+
}
|
|
25438
|
+
return org;
|
|
25439
|
+
}
|
|
25440
|
+
function refuseLogDeletion(args) {
|
|
25441
|
+
if (args.method === "delete_workflow_run_logs") throw new Error(`tool "actions_run_trigger": method "delete_workflow_run_logs" is not allowed in gh-broker; a run's logs are kept`);
|
|
25442
|
+
}
|
|
24652
25443
|
function extractOwner(args) {
|
|
24653
25444
|
if (typeof args.owner === "string" && args.owner) return args.owner.toLowerCase();
|
|
24654
25445
|
if (typeof args.org === "string" && args.org) return args.org.toLowerCase();
|
|
@@ -26016,6 +26807,14 @@ var AppAuth = class {
|
|
|
26016
26807
|
}
|
|
26017
26808
|
};
|
|
26018
26809
|
|
|
26810
|
+
// src/verbs/otp_status.ts
|
|
26811
|
+
var OTP_STATUS_SCHEMA = {
|
|
26812
|
+
type: "object",
|
|
26813
|
+
additionalProperties: false,
|
|
26814
|
+
properties: {}
|
|
26815
|
+
};
|
|
26816
|
+
var OTP_STATUS_DESCRIPTION = `Report the broker's OTP gate: whether OTP is set up, which tools need an "otp" code, how many windows are open and when the next one closes, bad codes so far, any short lockout, short lockouts in the last 24 h, and the hard lock (which blocks every other tool) with the command that clears it. Never shows a code or the seed. Works even while the broker is locked. Needs no owner.`;
|
|
26817
|
+
|
|
26019
26818
|
// src/catalog.ts
|
|
26020
26819
|
async function buildCatalog(config2, secrets, upstream) {
|
|
26021
26820
|
const upstreamByName = /* @__PURE__ */ new Map();
|
|
@@ -26064,7 +26863,7 @@ async function buildCatalog(config2, secrets, upstream) {
|
|
|
26064
26863
|
} else if (rule.name === "npm_scaffold") {
|
|
26065
26864
|
tools.push({
|
|
26066
26865
|
name: "npm_scaffold",
|
|
26067
|
-
description: "Create a new npm package and register its GitHub Actions trusted publisher, so the repo's publish workflow can release it through OIDC from then on. Publishes a broker-built placeholder (0.0.0-stage, no code) and never publishes a version of an existing package. Every npm write needs its own one-time password from the user, so it takes two calls: the first publishes the placeholder and returns status needs_otp; ask the user for the next code and call again to add the trusted publisher. Ask for each code right before the call. A refused or expired code also returns needs_otp with the remaining steps. Checks first that the repo's package.json names the package and that the workflow file exists. Uses the broker's npm token (no 2FA bypass) and the read PAT.",
|
|
26866
|
+
description: "Create a new npm package and register its GitHub Actions trusted publisher, so the repo's publish workflow can release it through OIDC from then on. Publishes a broker-built placeholder (0.0.0-stage, no code) and never publishes a version of an existing package. Every npm write needs its own one-time password from the user, passed as npm_otp, so it takes two calls: the first publishes the placeholder and returns status needs_otp; ask the user for the next npm code and call again with it in npm_otp to add the trusted publisher. Ask for each code right before the call. A refused or expired code also returns needs_otp with the remaining steps. Checks first that the repo's package.json names the package and that the workflow file exists. Uses the broker's npm token (no 2FA bypass) and the read PAT.",
|
|
26068
26867
|
inputSchema: NPM_SCAFFOLD_SCHEMA
|
|
26069
26868
|
});
|
|
26070
26869
|
} else if (rule.name === "report_unlabeled") {
|
|
@@ -26109,6 +26908,14 @@ async function buildCatalog(config2, secrets, upstream) {
|
|
|
26109
26908
|
description: "Report CI health for a PR, or every open PR in a repo, at a chosen verbosity: 0 counts only, 1 adds which run/job failed, 2 adds which step failed, 3 adds a tail of the failing job's log. Detail beyond tier 0 is only fetched for runs that are actually failing. Runs on the read PAT.",
|
|
26110
26909
|
inputSchema: REPORT_CI_STATUS_SCHEMA
|
|
26111
26910
|
});
|
|
26911
|
+
} else if (rule.name === "minimize_comment") {
|
|
26912
|
+
tools.push({ name: "minimize_comment", description: MINIMIZE_COMMENT_DESCRIPTION, inputSchema: MINIMIZE_COMMENT_SCHEMA });
|
|
26913
|
+
} else if (rule.name === "delete_branch") {
|
|
26914
|
+
tools.push({ name: "delete_branch", description: DELETE_BRANCH_DESCRIPTION, inputSchema: DELETE_BRANCH_SCHEMA });
|
|
26915
|
+
} else if (rule.name === "dismiss_alert") {
|
|
26916
|
+
tools.push({ name: "dismiss_alert", description: DISMISS_ALERT_DESCRIPTION, inputSchema: DISMISS_ALERT_SCHEMA });
|
|
26917
|
+
} else if (rule.name === OTP_STATUS) {
|
|
26918
|
+
tools.push({ name: OTP_STATUS, description: OTP_STATUS_DESCRIPTION, inputSchema: OTP_STATUS_SCHEMA });
|
|
26112
26919
|
} else if (GHCR_CATALOG[rule.name]) {
|
|
26113
26920
|
tools.push({ name: rule.name, ...GHCR_CATALOG[rule.name] });
|
|
26114
26921
|
}
|
|
@@ -26154,6 +26961,21 @@ async function buildCatalog(config2, secrets, upstream) {
|
|
|
26154
26961
|
});
|
|
26155
26962
|
continue;
|
|
26156
26963
|
}
|
|
26964
|
+
if (rule.name === "create_repository" || rule.name === "fork_repository") {
|
|
26965
|
+
const schema = base.inputSchema;
|
|
26966
|
+
const needed = rule.name === "create_repository" ? ["organization", "private"] : ["organization"];
|
|
26967
|
+
const note = rule.name === "create_repository" ? ' gh-broker: "organization" and "private" are required; the repo is never created in a personal account or left public by default.' : ' gh-broker: "organization" (where the fork goes) is required; forks are never created in a personal account.';
|
|
26968
|
+
tools.push({
|
|
26969
|
+
...base,
|
|
26970
|
+
description: `${base.description ?? ""}${note}`.trim(),
|
|
26971
|
+
inputSchema: { ...schema, type: "object", required: [.../* @__PURE__ */ new Set([...schema.required ?? [], ...needed])] }
|
|
26972
|
+
});
|
|
26973
|
+
continue;
|
|
26974
|
+
}
|
|
26975
|
+
if (rule.name === "actions_run_trigger") {
|
|
26976
|
+
tools.push({ ...base, description: `${base.description ?? ""} gh-broker: method "delete_workflow_run_logs" is refused; a run's logs are kept.`.trim() });
|
|
26977
|
+
continue;
|
|
26978
|
+
}
|
|
26157
26979
|
if (rule.name === "issue_write") {
|
|
26158
26980
|
const pointer = config2.tools.some((t) => t.name === "issue_labels" && t.custom) ? " To add, remove or swap individual labels, use issue_labels instead." : " Read the current labels first and pass the full set you want to keep.";
|
|
26159
26981
|
tools.push({
|
|
@@ -26164,12 +26986,34 @@ async function buildCatalog(config2, secrets, upstream) {
|
|
|
26164
26986
|
}
|
|
26165
26987
|
tools.push(base);
|
|
26166
26988
|
}
|
|
26167
|
-
return tools;
|
|
26989
|
+
return withOtp(tools, config2);
|
|
26990
|
+
}
|
|
26991
|
+
function withOtp(tools, config2) {
|
|
26992
|
+
const gated = new Set(config2.tools.filter(isGated).map((t) => t.name));
|
|
26993
|
+
const window = windowText(otpSettings(config2).windowSeconds);
|
|
26994
|
+
return tools.map((tool) => {
|
|
26995
|
+
if (!gated.has(tool.name)) return tool;
|
|
26996
|
+
const schema = tool.inputSchema;
|
|
26997
|
+
return {
|
|
26998
|
+
...tool,
|
|
26999
|
+
description: `${tool.description ?? ""} Needs a broker OTP (gh-broker): ask the user for a current code from their authenticator app and pass it as "${OTP_ARG}". One code works for every gated tool for ${window} after the first call that uses it.`.trim(),
|
|
27000
|
+
inputSchema: {
|
|
27001
|
+
...schema,
|
|
27002
|
+
type: "object",
|
|
27003
|
+
properties: {
|
|
27004
|
+
...schema.properties ?? {},
|
|
27005
|
+
[OTP_ARG]: { type: "string", description: "gh-broker: a current 6-digit code from the user's authenticator app, or the code that opened the current window" }
|
|
27006
|
+
},
|
|
27007
|
+
required: [...(schema.required ?? []).filter((r) => r !== OTP_ARG), OTP_ARG]
|
|
27008
|
+
}
|
|
27009
|
+
};
|
|
27010
|
+
});
|
|
26168
27011
|
}
|
|
26169
27012
|
|
|
26170
27013
|
// src/server.ts
|
|
26171
27014
|
import { createServer } from "node:net";
|
|
26172
|
-
import { chmodSync, chownSync, existsSync, readFileSync as
|
|
27015
|
+
import { chmodSync, chownSync, existsSync, mkdirSync, readdirSync, readFileSync as readFileSync4, rmSync, unlinkSync as unlinkSync2 } from "node:fs";
|
|
27016
|
+
import { join as join4 } from "node:path";
|
|
26173
27017
|
|
|
26174
27018
|
// node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/server.js
|
|
26175
27019
|
var ExperimentalServerTasks = class {
|
|
@@ -26864,26 +27708,12 @@ var StdioServerTransport = class {
|
|
|
26864
27708
|
|
|
26865
27709
|
// src/server.ts
|
|
26866
27710
|
var SERVER_INFO = { name: "gh-broker", version: "0.1.0" };
|
|
26867
|
-
function
|
|
26868
|
-
if (!listen.startsWith("unix:")) throw new Error(`server: unsupported listen "${listen}"`);
|
|
26869
|
-
return listen.slice("unix:".length);
|
|
26870
|
-
}
|
|
26871
|
-
function groupGid(name) {
|
|
26872
|
-
try {
|
|
26873
|
-
for (const line of readFileSync3("/etc/group", "utf8").split("\n")) {
|
|
26874
|
-
const parts = line.split(":");
|
|
26875
|
-
if (parts[0] === name && parts[2]) return Number(parts[2]);
|
|
26876
|
-
}
|
|
26877
|
-
} catch {
|
|
26878
|
-
}
|
|
26879
|
-
return void 0;
|
|
26880
|
-
}
|
|
26881
|
-
function handleConnection(socket, router, catalog) {
|
|
27711
|
+
function handleConnection(socket, router, catalog, user) {
|
|
26882
27712
|
const server = new Server(SERVER_INFO, { capabilities: { tools: {} } });
|
|
26883
27713
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: catalog }));
|
|
26884
27714
|
server.setRequestHandler(CallToolRequestSchema, async (req2) => {
|
|
26885
27715
|
const args = req2.params.arguments ?? {};
|
|
26886
|
-
return router.call(req2.params.name, args);
|
|
27716
|
+
return router.call(req2.params.name, args, { user });
|
|
26887
27717
|
});
|
|
26888
27718
|
const transport = new StdioServerTransport(socket, socket);
|
|
26889
27719
|
server.connect(transport).catch((err) => {
|
|
@@ -26895,24 +27725,59 @@ function handleConnection(socket, router, catalog) {
|
|
|
26895
27725
|
socket.on("error", () => {
|
|
26896
27726
|
});
|
|
26897
27727
|
}
|
|
26898
|
-
function
|
|
26899
|
-
|
|
26900
|
-
|
|
27728
|
+
function readAgents(groupFile) {
|
|
27729
|
+
let text;
|
|
27730
|
+
try {
|
|
27731
|
+
text = readFileSync4(groupFile, "utf8");
|
|
27732
|
+
} catch (err) {
|
|
27733
|
+
console.error(`gh-broker: cannot read ${groupFile} (${err.message}); serving no agents`);
|
|
27734
|
+
return [];
|
|
27735
|
+
}
|
|
27736
|
+
const { agents, skipped } = parseAgentGroups(text);
|
|
27737
|
+
for (const why of skipped) console.error(`gh-broker: ignoring agent group ${why}`);
|
|
27738
|
+
return agents;
|
|
27739
|
+
}
|
|
27740
|
+
function listenFor(path, agent, router, catalog) {
|
|
27741
|
+
if (existsSync(path)) unlinkSync2(path);
|
|
26901
27742
|
return new Promise((resolve, reject) => {
|
|
26902
|
-
const server = createServer((sock) => handleConnection(sock, router, catalog));
|
|
27743
|
+
const server = createServer((sock) => handleConnection(sock, router, catalog, agent.user));
|
|
26903
27744
|
server.on("error", reject);
|
|
26904
27745
|
server.listen(path, () => {
|
|
26905
|
-
const group = process.env.GH_BROKER_SOCKET_GROUP ?? "gh-broker-clients";
|
|
26906
|
-
const gid = groupGid(group);
|
|
26907
27746
|
try {
|
|
26908
|
-
|
|
27747
|
+
chownSync(path, process.getuid?.() ?? -1, agent.gid);
|
|
27748
|
+
} catch (err) {
|
|
27749
|
+
console.error(`gh-broker: cannot give ${path} to group ${agent.group} (${err.message}); ${agent.user} can't connect`);
|
|
27750
|
+
}
|
|
27751
|
+
try {
|
|
26909
27752
|
chmodSync(path, 432);
|
|
26910
|
-
} catch {
|
|
27753
|
+
} catch (err) {
|
|
27754
|
+
server.close();
|
|
27755
|
+
reject(err);
|
|
27756
|
+
return;
|
|
26911
27757
|
}
|
|
26912
27758
|
resolve();
|
|
26913
27759
|
});
|
|
26914
27760
|
});
|
|
26915
27761
|
}
|
|
27762
|
+
async function startServer(config2, router, catalog, options = {}) {
|
|
27763
|
+
const dir = agentsDir(config2.listen);
|
|
27764
|
+
if (!dir) throw new Error(`server: unsupported listen "${config2.listen}" (expected "agents:<absolute dir>")`);
|
|
27765
|
+
const agents = readAgents(options.groupFile ?? "/etc/group");
|
|
27766
|
+
if (!existsSync(dir)) {
|
|
27767
|
+
mkdirSync(dir, { recursive: true });
|
|
27768
|
+
chmodSync(dir, 493);
|
|
27769
|
+
}
|
|
27770
|
+
const wanted = new Set(agents.map((a) => `${a.user}.sock`));
|
|
27771
|
+
for (const name of readdirSync(dir)) {
|
|
27772
|
+
if (name.endsWith(".sock") && !wanted.has(name)) rmSync(join4(dir, name), { force: true });
|
|
27773
|
+
}
|
|
27774
|
+
if (agents.length === 0) {
|
|
27775
|
+
console.error("gh-broker: no agents registered (no gh-broker-agent-<user> group with members gh-broker and <user>); serving nothing. Add one with: sudo gh-broker agent add <user>");
|
|
27776
|
+
return [];
|
|
27777
|
+
}
|
|
27778
|
+
for (const agent of agents) await listenFor(join4(dir, `${agent.user}.sock`), agent, router, catalog);
|
|
27779
|
+
return agents.map((a) => a.user);
|
|
27780
|
+
}
|
|
26916
27781
|
|
|
26917
27782
|
// src/index.ts
|
|
26918
27783
|
init_paths();
|
|
@@ -26958,12 +27823,13 @@ async function main() {
|
|
|
26958
27823
|
}
|
|
26959
27824
|
const router = new Router(config2, secrets, upstream, appAuth);
|
|
26960
27825
|
const catalog = await buildCatalog(config2, secrets, upstream);
|
|
26961
|
-
await startServer(config2, router, catalog);
|
|
27826
|
+
const agents = await startServer(config2, router, catalog);
|
|
26962
27827
|
console.log(
|
|
26963
27828
|
JSON.stringify({
|
|
26964
27829
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
26965
27830
|
event: "listening",
|
|
26966
27831
|
listen: config2.listen,
|
|
27832
|
+
agents,
|
|
26967
27833
|
owners: Object.keys(config2.owners),
|
|
26968
27834
|
app: appAuth ? "enabled" : "none",
|
|
26969
27835
|
tools: catalog.map((t) => t.name)
|