@cldmv/gh-broker 0.7.0 → 0.9.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.
Files changed (107) hide show
  1. package/README.md +165 -41
  2. package/config.example.json +38 -9
  3. package/config.sample.json +45 -0
  4. package/deploy/README.md +76 -17
  5. package/deploy/install.mjs +171 -104
  6. package/deploy/install.sh +0 -0
  7. package/deploy/lib/config-merge.mjs +174 -16
  8. package/deploy/lib/credentials.mjs +143 -0
  9. package/deploy/lib/install-args.mjs +144 -0
  10. package/deploy/lib/npm-setup.mjs +164 -0
  11. package/deploy/lib/secret-store.mjs +20 -2
  12. package/deploy/lib/system.mjs +26 -2
  13. package/dist/index.mjs +5864 -1809
  14. package/dist/index.mjs.map +4 -4
  15. package/package.json +18 -8
  16. package/.configs/fix-headers.json +0 -3
  17. package/.configs/vitest.config.mjs +0 -44
  18. package/.github/dependabot.yml +0 -111
  19. package/.github/workflows/branch-retention.yml +0 -44
  20. package/.github/workflows/bundle-size.yml +0 -52
  21. package/.github/workflows/ci.yml +0 -389
  22. package/.github/workflows/cla.yml +0 -66
  23. package/.github/workflows/codeql.yml +0 -87
  24. package/.github/workflows/dependabot-auto-merge.yml +0 -62
  25. package/.github/workflows/dependabot-recreate.yml +0 -67
  26. package/.github/workflows/dependency-review.yml +0 -46
  27. package/.github/workflows/feature-pr.yml +0 -75
  28. package/.github/workflows/hotfix-redirector.yml +0 -67
  29. package/.github/workflows/hotfixes-release.yml +0 -70
  30. package/.github/workflows/labeler.yml +0 -48
  31. package/.github/workflows/master-commit-audit.yml +0 -56
  32. package/.github/workflows/member-auto-merge.yml +0 -75
  33. package/.github/workflows/next-release.yml +0 -76
  34. package/.github/workflows/next-reset.yml +0 -50
  35. package/.github/workflows/pr-notify.yml +0 -44
  36. package/.github/workflows/pr-title-normalizer.yml +0 -52
  37. package/.github/workflows/provenance.yml +0 -51
  38. package/.github/workflows/publish.yml +0 -146
  39. package/.github/workflows/release-merge.yml +0 -104
  40. package/.github/workflows/release-notify.yml +0 -41
  41. package/.github/workflows/scorecard.yml +0 -64
  42. package/.github/workflows/stale.yml +0 -50
  43. package/.github/workflows/tag-health.yml +0 -70
  44. package/.github/workflows/update-major-version-tags.yml +0 -94
  45. package/.github/workflows/v4-bootstrap.yml +0 -115
  46. package/.github/workflows/welcome.yml +0 -42
  47. package/docs/TOOLS.md +0 -347
  48. package/docs/agent-user-model.md +0 -89
  49. package/docs/changelog/v0/v0.2.0.md +0 -26
  50. package/docs/changelog/v0/v0.2.1.md +0 -23
  51. package/docs/changelog/v0/v0.2.2.md +0 -23
  52. package/docs/changelog/v0/v0.2.3.md +0 -33
  53. package/docs/changelog/v0/v0.2.4.md +0 -25
  54. package/docs/changelog/v0/v0.2.5.md +0 -29
  55. package/docs/changelog/v0/v0.3.0.md +0 -84
  56. package/docs/changelog/v0/v0.3.1.md +0 -30
  57. package/docs/changelog/v0/v0.3.2.md +0 -24
  58. package/docs/changelog/v0/v0.3.3.md +0 -28
  59. package/docs/changelog/v0/v0.4.0.md +0 -58
  60. package/docs/changelog/v0/v0.4.1.md +0 -33
  61. package/docs/changelog/v0/v0.4.2.md +0 -45
  62. package/docs/changelog/v0/v0.5.0.md +0 -39
  63. package/docs/changelog/v0/v0.5.1.md +0 -28
  64. package/docs/changelog/v0/v0.6.0.md +0 -52
  65. package/docs/changelog/v0/v0.6.1.md +0 -48
  66. package/docs/changelog/v0/v0.7.0.md +0 -71
  67. package/docs/deploy-architecture.md +0 -82
  68. package/src/appauth.ts +0 -56
  69. package/src/audit.ts +0 -34
  70. package/src/catalog.ts +0 -165
  71. package/src/config.ts +0 -130
  72. package/src/index.ts +0 -107
  73. package/src/paths.ts +0 -60
  74. package/src/router.ts +0 -258
  75. package/src/secrets.ts +0 -137
  76. package/src/server.ts +0 -91
  77. package/src/types.ts +0 -104
  78. package/src/upstream.ts +0 -85
  79. package/src/verbs/issue_labels.ts +0 -329
  80. package/src/verbs/link_issue.ts +0 -84
  81. package/src/verbs/release-analysis.ts +0 -210
  82. package/src/verbs/repo-tier.ts +0 -90
  83. package/src/verbs/report_ci_status.ts +0 -358
  84. package/src/verbs/report_release_prs.ts +0 -238
  85. package/src/verbs/report_security_alerts.ts +0 -442
  86. package/src/verbs/report_unlabeled.ts +0 -194
  87. package/src/verbs/rest-client.ts +0 -181
  88. package/src/verbs/review_thread.ts +0 -138
  89. package/tests/appauth.test.vitest.mts +0 -76
  90. package/tests/audit.test.vitest.mts +0 -48
  91. package/tests/catalog.test.vitest.mts +0 -180
  92. package/tests/config-merge.test.vitest.mts +0 -58
  93. package/tests/config.test.vitest.mts +0 -314
  94. package/tests/issue-labels.test.vitest.mts +0 -347
  95. package/tests/paths.test.vitest.mts +0 -52
  96. package/tests/release-prs.test.vitest.mts +0 -237
  97. package/tests/rest-client.test.vitest.mts +0 -115
  98. package/tests/router.test.vitest.mts +0 -399
  99. package/tests/run-vitest.mjs +0 -51
  100. package/tests/secret-store.test.vitest.mts +0 -106
  101. package/tests/secrets.test.vitest.mts +0 -155
  102. package/tests/security-alerts.test.vitest.mts +0 -328
  103. package/tests/server.test.vitest.mts +0 -20
  104. package/tests/upstream.test.vitest.mts +0 -91
  105. package/tests/verb-helpers.test.vitest.mts +0 -331
  106. package/tests/verbs.test.vitest.mts +0 -624
  107. package/tsconfig.json +0 -21
@@ -1,58 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { mergeConfig } from "../deploy/lib/config-merge.mjs";
3
-
4
- const example = {
5
- listen: "unix:/run/gh-broker/gh-broker.sock",
6
- upstream: "https://api.githubcopilot.com/mcp/x/all",
7
- tools: [
8
- { name: "get_me", mode: "read" },
9
- { name: "list_issues", mode: "read" }
10
- ],
11
- owners: { cldmv: { read: "cred:cldmv-read", write: "cred:cldmv-write" } },
12
- defaultOwner: "cldmv"
13
- };
14
-
15
- describe("mergeConfig", () => {
16
- it("uses the example as-is on first install (no current)", () => {
17
- expect(mergeConfig(example, null)).toEqual(example);
18
- });
19
-
20
- it("takes tools from the example so allowlist removals/tightenings apply", () => {
21
- const current = { ...example, tools: [{ name: "get_me", mode: "read" }, { name: "removed_tool", mode: "write" }] };
22
- const merged = mergeConfig(example, current);
23
- expect(merged.tools).toEqual(example.tools);
24
- expect(merged.tools.find((t: { name: string }) => t.name === "removed_tool")).toBeUndefined();
25
- });
26
-
27
- it("preserves current owner data and adds example-only owners on upgrade", () => {
28
- const current = { ...example, owners: { cldmv: { read: "cred:cldmv-read", write: "cred:cldmv-write", installationId: 123 } } };
29
- const example2 = {
30
- ...example,
31
- owners: {
32
- cldmv: { read: "cred:cldmv-read", write: "cred:cldmv-write" },
33
- acme: { read: "cred:acme-read", write: "cred:acme-write" }
34
- }
35
- };
36
- const merged = mergeConfig(example2, current);
37
- expect(merged.owners.cldmv.installationId).toBe(123); // current wins
38
- expect(merged.owners.acme).toBeDefined(); // new example owner appears
39
- });
40
-
41
- it("lets current win for listen/upstream/defaultOwner/app/secrets", () => {
42
- const current = { ...example, listen: "unix:/tmp/custom.sock", app: { appId: 1, privateKey: "cred:k" } };
43
- const merged = mergeConfig(example, current);
44
- expect(merged.listen).toBe("unix:/tmp/custom.sock");
45
- expect(merged.app).toEqual({ appId: 1, privateKey: "cred:k" });
46
- });
47
-
48
- it("does not mutate its inputs", () => {
49
- const ex = structuredClone(example);
50
- mergeConfig(ex, { ...example, listen: "unix:/z" });
51
- expect(ex).toEqual(example);
52
- });
53
-
54
- it("rejects an array as example or current (typeof reports 'object')", () => {
55
- expect(() => mergeConfig([], null)).toThrow(/example config must be an object/);
56
- expect(() => mergeConfig(example, [])).toThrow(/current config must be an object/);
57
- });
58
- });
@@ -1,314 +0,0 @@
1
- import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
2
- import { mkdirSync, writeFileSync, rmSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { loadConfig, indexTools } from "../src/config.ts";
5
-
6
- // loadConfig reads a real file off disk, so we materialize each fixture into the
7
- // repo's gitignored tmp/ and pass the path.
8
- const DIR = join(process.cwd(), "tmp", "config-tests");
9
- let counter = 0;
10
-
11
- /** Write `contents` (string or object) to a unique tmp file and return its path. */
12
- function writeConfig(contents: unknown): string {
13
- const path = join(DIR, `cfg-${counter++}.json`);
14
- writeFileSync(path, typeof contents === "string" ? contents : JSON.stringify(contents));
15
- return path;
16
- }
17
-
18
- function baseConfig(): Record<string, unknown> {
19
- return {
20
- listen: "unix:/run/gh-broker/gh-broker.sock",
21
- upstream: "https://api.githubcopilot.com/mcp/",
22
- tools: [{ name: "get_me", mode: "read" }],
23
- owners: { cldmv: { read: "cred:cldmv-read", write: "cred:cldmv-write" } }
24
- };
25
- }
26
-
27
- beforeAll(() => {
28
- mkdirSync(DIR, { recursive: true });
29
- });
30
-
31
- afterAll(() => {
32
- rmSync(DIR, { recursive: true, force: true });
33
- });
34
-
35
- describe("loadConfig — file/parse errors", () => {
36
- it("throws when the file cannot be read", () => {
37
- expect(() => loadConfig(join(DIR, "does-not-exist.json"))).toThrow(/cannot read\/parse/);
38
- });
39
-
40
- it("throws on malformed JSON", () => {
41
- const p = writeConfig("{ not json ");
42
- expect(() => loadConfig(p)).toThrow(/cannot read\/parse/);
43
- });
44
-
45
- it("throws when the root is not an object", () => {
46
- const p = writeConfig("123");
47
- expect(() => loadConfig(p)).toThrow(/root must be an object/);
48
- });
49
-
50
- it("throws when the root is JSON null", () => {
51
- const p = writeConfig("null");
52
- expect(() => loadConfig(p)).toThrow(/root must be an object/);
53
- });
54
- });
55
-
56
- describe("loadConfig — listen / upstream", () => {
57
- it("rejects a non-string listen", () => {
58
- const c = baseConfig();
59
- delete c.listen;
60
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.listen/);
61
- });
62
-
63
- it("rejects a listen without the unix: prefix", () => {
64
- const c = baseConfig();
65
- c.listen = "tcp:0.0.0.0:9000";
66
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.listen/);
67
- });
68
-
69
- it("rejects a non-http upstream", () => {
70
- const c = baseConfig();
71
- c.upstream = "ftp://example.com";
72
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.upstream/);
73
- });
74
-
75
- it("rejects a non-string upstream", () => {
76
- const c = baseConfig();
77
- c.upstream = 42;
78
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.upstream/);
79
- });
80
- });
81
-
82
- describe("loadConfig — tools", () => {
83
- it("rejects a missing tools array", () => {
84
- const c = baseConfig();
85
- delete c.tools;
86
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.tools: must be a non-empty array/);
87
- });
88
-
89
- it("rejects an empty tools array", () => {
90
- const c = baseConfig();
91
- c.tools = [];
92
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.tools: must be a non-empty array/);
93
- });
94
-
95
- it("rejects a tool with no name", () => {
96
- const c = baseConfig();
97
- c.tools = [{ mode: "read" }];
98
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.tools\[0\]\.name: required/);
99
- });
100
-
101
- it("rejects a tool with a bad mode", () => {
102
- const c = baseConfig();
103
- c.tools = [{ name: "get_me", mode: "admin" }];
104
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.tools\[0\]\.mode: must be read\|write/);
105
- });
106
-
107
- it("rejects duplicate tool names", () => {
108
- const c = baseConfig();
109
- c.tools = [
110
- { name: "get_me", mode: "read" },
111
- { name: "get_me", mode: "write" }
112
- ];
113
- expect(() => loadConfig(writeConfig(c))).toThrow(/duplicate tool "get_me"/);
114
- });
115
-
116
- it("carries the custom flag through, defaulting to false", () => {
117
- const c = baseConfig();
118
- c.tools = [
119
- { name: "get_me", mode: "read" },
120
- { name: "link_issue", mode: "write", custom: true }
121
- ];
122
- const out = loadConfig(writeConfig(c));
123
- expect(out.tools).toEqual([
124
- { name: "get_me", mode: "read", custom: false, augment: false },
125
- { name: "link_issue", mode: "write", custom: true, augment: false }
126
- ]);
127
- });
128
-
129
- it("carries the augment flag through, defaulting to false", () => {
130
- const c = baseConfig();
131
- c.tools = [
132
- { name: "get_me", mode: "read" },
133
- { name: "pull_request_read", mode: "read", augment: true }
134
- ];
135
- const out = loadConfig(writeConfig(c));
136
- expect(out.tools).toEqual([
137
- { name: "get_me", mode: "read", custom: false, augment: false },
138
- { name: "pull_request_read", mode: "read", custom: false, augment: true }
139
- ]);
140
- });
141
-
142
- it("rejects a tool marked both custom and augment", () => {
143
- const c = baseConfig();
144
- c.tools = [{ name: "weird", mode: "read", custom: true, augment: true }];
145
- expect(() => loadConfig(writeConfig(c))).toThrow(/custom and augment are mutually exclusive/);
146
- });
147
- });
148
-
149
- describe("loadConfig — owners", () => {
150
- it("rejects a non-object owners", () => {
151
- const c = baseConfig();
152
- c.owners = "cldmv";
153
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.owners: must be an object/);
154
- });
155
-
156
- it("rejects an owner missing read/write", () => {
157
- const c = baseConfig();
158
- c.owners = { cldmv: { read: "cred:x" } };
159
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.owners\.cldmv: needs "read" and "write"/);
160
- });
161
-
162
- it("rejects a non-numeric installationId", () => {
163
- const c = baseConfig();
164
- c.owners = { cldmv: { read: "cred:r", write: "cred:w", installationId: "42" } };
165
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.owners\.cldmv\.installationId: must be a number/);
166
- });
167
-
168
- it("rejects an empty owners object", () => {
169
- const c = baseConfig();
170
- c.owners = {};
171
- expect(() => loadConfig(writeConfig(c))).toThrow(/at least one owner required/);
172
- });
173
-
174
- it("lowercases owner keys and carries installationId", () => {
175
- const c = baseConfig();
176
- c.owners = { CLDMV: { read: "cred:r", write: "cred:w", installationId: 99 } };
177
- const out = loadConfig(writeConfig(c));
178
- expect(out.owners).toEqual({ cldmv: { read: "cred:r", write: "cred:w", installationId: 99 } });
179
- });
180
-
181
- it("omits installationId when absent", () => {
182
- const out = loadConfig(writeConfig(baseConfig()));
183
- expect(out.owners.cldmv).toEqual({ read: "cred:cldmv-read", write: "cred:cldmv-write" });
184
- expect(out.owners.cldmv).not.toHaveProperty("installationId");
185
- });
186
- });
187
-
188
- describe("loadConfig — defaultOwner", () => {
189
- it("is undefined when omitted", () => {
190
- const out = loadConfig(writeConfig(baseConfig()));
191
- expect(out.defaultOwner).toBeUndefined();
192
- });
193
-
194
- it("lowercases a valid defaultOwner and carries it through", () => {
195
- const c = baseConfig();
196
- c.defaultOwner = "CLDMV";
197
- const out = loadConfig(writeConfig(c));
198
- expect(out.defaultOwner).toBe("cldmv");
199
- });
200
-
201
- it("rejects a non-string / empty defaultOwner", () => {
202
- const c = baseConfig();
203
- c.defaultOwner = "";
204
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.defaultOwner: must be a non-empty string/);
205
- });
206
-
207
- it("rejects a defaultOwner that is not a configured owner", () => {
208
- const c = baseConfig();
209
- c.defaultOwner = "nobody";
210
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.defaultOwner "nobody" is not a key of config\.owners/);
211
- });
212
- });
213
-
214
- describe("loadConfig — app", () => {
215
- it("rejects a non-numeric appId", () => {
216
- const c = baseConfig();
217
- c.app = { appId: "1", privateKey: "cred:app" };
218
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.app\.appId: must be a number/);
219
- });
220
-
221
- it("rejects a non-string privateKey", () => {
222
- const c = baseConfig();
223
- c.app = { appId: 1, privateKey: 5 };
224
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.app\.privateKey/);
225
- });
226
-
227
- it("carries a valid app config through", () => {
228
- const c = baseConfig();
229
- c.app = { appId: 12345, privateKey: "cred:gh-app-key" };
230
- const out = loadConfig(writeConfig(c));
231
- expect(out.app).toEqual({ appId: 12345, privateKey: "cred:gh-app-key" });
232
- });
233
-
234
- it("omits app when absent", () => {
235
- const out = loadConfig(writeConfig(baseConfig()));
236
- expect(out.app).toBeUndefined();
237
- });
238
- });
239
-
240
- describe("loadConfig — secrets", () => {
241
- it("rejects an array (which typeof reports as 'object')", () => {
242
- const c = baseConfig();
243
- c.secrets = [];
244
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.secrets: must be an object/);
245
- });
246
-
247
- it("rejects null", () => {
248
- const c = baseConfig();
249
- c.secrets = null;
250
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.secrets: must be an object/);
251
- });
252
-
253
- it("rejects an unknown provider", () => {
254
- const c = baseConfig();
255
- c.secrets = { provider: "vault" };
256
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.secrets\.provider/);
257
- });
258
-
259
- it("carries a valid secrets block through", () => {
260
- const c = baseConfig();
261
- c.secrets = { provider: "file", path: "/etc/gh-broker/secrets.json" };
262
- const out = loadConfig(writeConfig(c));
263
- expect(out.secrets).toEqual({ provider: "file", path: "/etc/gh-broker/secrets.json" });
264
- });
265
-
266
- it("rejects a path without the file provider (omitted → systemd-creds default)", () => {
267
- const c = baseConfig();
268
- c.secrets = { path: "/etc/gh-broker/secrets.json" };
269
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.secrets\.path is only used by the "file" provider/);
270
- });
271
-
272
- it("rejects a path with the systemd-creds provider", () => {
273
- const c = baseConfig();
274
- c.secrets = { provider: "systemd-creds", path: "/etc/gh-broker/secrets.json" };
275
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.secrets\.path is only used by the "file" provider/);
276
- });
277
-
278
- it("rejects an empty path (would be silently ignored by resolveSecretsPath)", () => {
279
- const c = baseConfig();
280
- c.secrets = { provider: "file", path: "" };
281
- expect(() => loadConfig(writeConfig(c))).toThrow(/config\.secrets\.path: must be a non-empty string/);
282
- });
283
- });
284
-
285
- describe("loadConfig — full valid config", () => {
286
- it("returns the normalized shape", () => {
287
- const c = baseConfig();
288
- c.defaultOwner = "cldmv";
289
- c.app = { appId: 7, privateKey: "cred:app" };
290
- const out = loadConfig(writeConfig(c));
291
- expect(out).toEqual({
292
- listen: "unix:/run/gh-broker/gh-broker.sock",
293
- upstream: "https://api.githubcopilot.com/mcp/",
294
- tools: [{ name: "get_me", mode: "read", custom: false, augment: false }],
295
- owners: { cldmv: { read: "cred:cldmv-read", write: "cred:cldmv-write" } },
296
- app: { appId: 7, privateKey: "cred:app" },
297
- defaultOwner: "cldmv"
298
- });
299
- });
300
- });
301
-
302
- describe("indexTools", () => {
303
- it("builds a name→rule map", () => {
304
- const rules = [
305
- { name: "get_me", mode: "read" as const },
306
- { name: "create_pull_request", mode: "write" as const, custom: false }
307
- ];
308
- const map = indexTools(rules);
309
- expect(map.size).toBe(2);
310
- expect(map.get("get_me")).toEqual({ name: "get_me", mode: "read" });
311
- expect(map.get("create_pull_request")).toEqual({ name: "create_pull_request", mode: "write", custom: false });
312
- expect(map.get("missing")).toBeUndefined();
313
- });
314
- });