@app-vitals/agent-admin 1.12.1 → 1.14.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/openapi.json +787 -0
- package/openapi.platform.json +787 -0
- package/package.json +2 -2
- package/src/admin-boot.ts +21 -1
- package/src/admin-spec.ts +19 -0
- package/src/fixtures/github-enterprise-cassette.json +675 -0
- package/src/github-app-api.ts +111 -0
- package/src/github-app-jwt.ts +44 -0
- package/src/github-app-jwt.unit.test.ts +95 -0
- package/src/github-enterprise-api.integration.test.ts +264 -0
- package/src/github-enterprise-api.smoke.test.ts +595 -0
- package/src/github-enterprise-api.ts +750 -0
- package/src/github-enterprise-client.integration.test.ts +318 -0
- package/src/github-enterprise-client.ts +384 -0
- package/src/github-enterprise-client.unit.test.ts +97 -0
- package/src/github-enterprise-isolation.unit.test.ts +84 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/github-enterprise-client.unit.test.ts
|
|
3
|
+
* Unit tests for GitHub enterprise error mapping and client-side validation.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from "bun:test";
|
|
7
|
+
import {
|
|
8
|
+
GithubEnterpriseClient,
|
|
9
|
+
GithubEnterpriseError,
|
|
10
|
+
type GithubEnterpriseErrorKind,
|
|
11
|
+
mapGithubEnterpriseError,
|
|
12
|
+
} from "./github-enterprise-client.ts";
|
|
13
|
+
|
|
14
|
+
const h = (init?: Record<string, string>) => new Headers(init);
|
|
15
|
+
|
|
16
|
+
describe("mapGithubEnterpriseError", () => {
|
|
17
|
+
const cases: Array<
|
|
18
|
+
[number, Record<string, string>, unknown, GithubEnterpriseErrorKind]
|
|
19
|
+
> = [
|
|
20
|
+
[401, {}, { message: "Bad credentials" }, "unauthorized"],
|
|
21
|
+
[
|
|
22
|
+
403,
|
|
23
|
+
{},
|
|
24
|
+
{ message: "Resource not accessible by integration" },
|
|
25
|
+
"permission",
|
|
26
|
+
],
|
|
27
|
+
[404, {}, { message: "Not Found" }, "not_found"],
|
|
28
|
+
[422, {}, { message: "Validation Failed" }, "validation"],
|
|
29
|
+
[429, {}, { message: "API rate limit exceeded" }, "rate_limit"],
|
|
30
|
+
[403, { "retry-after": "60" }, { message: "x" }, "rate_limit"],
|
|
31
|
+
[403, { "x-ratelimit-remaining": "0" }, { message: "x" }, "rate_limit"],
|
|
32
|
+
[
|
|
33
|
+
403,
|
|
34
|
+
{},
|
|
35
|
+
{ message: "You have exceeded a secondary rate limit." },
|
|
36
|
+
"rate_limit",
|
|
37
|
+
],
|
|
38
|
+
[500, {}, { message: "Server Error" }, "server"],
|
|
39
|
+
[503, {}, undefined, "server"],
|
|
40
|
+
[418, {}, undefined, "unexpected"],
|
|
41
|
+
];
|
|
42
|
+
for (const [status, headers, body, kind] of cases) {
|
|
43
|
+
it(`maps ${status} ${JSON.stringify(headers)} to ${kind}`, () => {
|
|
44
|
+
const e = mapGithubEnterpriseError(status, h(headers), body, "ctx");
|
|
45
|
+
expect(e).toBeInstanceOf(GithubEnterpriseError);
|
|
46
|
+
expect(e.kind).toBe(kind);
|
|
47
|
+
expect(e.status).toBe(status);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
it("exposes retry-after seconds", () => {
|
|
52
|
+
const e = mapGithubEnterpriseError(
|
|
53
|
+
429,
|
|
54
|
+
h({ "retry-after": "30" }),
|
|
55
|
+
{},
|
|
56
|
+
"ctx",
|
|
57
|
+
);
|
|
58
|
+
expect(e.retryAfterSeconds).toBe(30);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("redacts secrets echoed in the message", () => {
|
|
62
|
+
const e = mapGithubEnterpriseError(
|
|
63
|
+
403,
|
|
64
|
+
h(),
|
|
65
|
+
{ message: "token ghs_secret123 denied" },
|
|
66
|
+
"ctx",
|
|
67
|
+
["ghs_secret123"],
|
|
68
|
+
);
|
|
69
|
+
expect(e.message).not.toContain("ghs_secret123");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("GithubEnterpriseClient validation", () => {
|
|
74
|
+
const neverFetch = (async () => {
|
|
75
|
+
throw new Error("fetch must not be called");
|
|
76
|
+
}) as unknown as typeof fetch;
|
|
77
|
+
const client = new GithubEnterpriseClient({
|
|
78
|
+
appId: "1",
|
|
79
|
+
enterprise: "acme",
|
|
80
|
+
privateKey: "unused",
|
|
81
|
+
fetchFn: neverFetch,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("rejects selected without repositories before any network call", async () => {
|
|
85
|
+
const err = await client
|
|
86
|
+
.installOnOrg("o", { clientId: "Iv1.x", repositorySelection: "selected" })
|
|
87
|
+
.catch((e) => e);
|
|
88
|
+
expect(err.kind).toBe("validation");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("rejects a missing clientId", async () => {
|
|
92
|
+
const err = await client
|
|
93
|
+
.installOnOrg("o", { clientId: "", repositorySelection: "all" })
|
|
94
|
+
.catch((e) => e);
|
|
95
|
+
expect(err.kind).toBe("validation");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* admin/src/github-enterprise-isolation.unit.test.ts
|
|
3
|
+
* Isolation exception guard (GHE-1.3): the GitHub Enterprise App private key
|
|
4
|
+
* is reachable only from the enterprise service, its admin API and the boot
|
|
5
|
+
* wiring. Agent config bundle, agent-bearer routes and env serialization must
|
|
6
|
+
* never import the service, touch its table or call getPrivateKey().
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "bun:test";
|
|
10
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
11
|
+
import { join, relative } from "node:path";
|
|
12
|
+
|
|
13
|
+
const REPO = new URL("../../", import.meta.url).pathname;
|
|
14
|
+
|
|
15
|
+
/** Non-test source files that may reference the key-bearing service/table. */
|
|
16
|
+
const ALLOWED = new Set([
|
|
17
|
+
"admin/src/github-enterprise-apps.ts",
|
|
18
|
+
"admin/src/github-enterprise-api.ts",
|
|
19
|
+
"admin/src/admin-boot.ts",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function sources(dir: string, out: string[] = []): string[] {
|
|
23
|
+
for (const name of readdirSync(dir)) {
|
|
24
|
+
if (name === "node_modules" || name === "client" || name.startsWith(".")) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const p = join(dir, name);
|
|
28
|
+
if (statSync(p).isDirectory()) sources(p, out);
|
|
29
|
+
else if (
|
|
30
|
+
/\.(ts|tsx)$/.test(name) &&
|
|
31
|
+
!/\.(unit|integration|smoke|fixtures)\.test\.ts$|\.test\.ts$|\.fixtures\.ts$/.test(
|
|
32
|
+
name,
|
|
33
|
+
)
|
|
34
|
+
) {
|
|
35
|
+
out.push(p);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe("GitHub Enterprise key isolation", () => {
|
|
42
|
+
const files = ["admin/src", "runtime", "lib", "chat", "index.ts"].flatMap(
|
|
43
|
+
(d) => {
|
|
44
|
+
const p = join(REPO, d);
|
|
45
|
+
return statSync(p).isDirectory() ? sources(p) : [p];
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
it("scans a non-trivial set of files", () => {
|
|
50
|
+
expect(files.length).toBeGreaterThan(50);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("only the allowlisted modules touch the key-bearing service or table", () => {
|
|
54
|
+
const offenders = files
|
|
55
|
+
.map((f) => relative(REPO, f))
|
|
56
|
+
.filter((rel) => !ALLOWED.has(rel))
|
|
57
|
+
.filter((rel) => {
|
|
58
|
+
const src = readFileSync(join(REPO, rel), "utf-8");
|
|
59
|
+
return (
|
|
60
|
+
/github-enterprise-apps/.test(src) ||
|
|
61
|
+
/gitHubEnterpriseApp\b/.test(src) ||
|
|
62
|
+
/GitHubEnterpriseApp\b/.test(src) ||
|
|
63
|
+
/privateKeyEncrypted/.test(src)
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
expect(offenders).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("getPrivateKey() is only called by the enterprise API", () => {
|
|
70
|
+
const callers = files
|
|
71
|
+
.map((f) => relative(REPO, f))
|
|
72
|
+
.filter((rel) =>
|
|
73
|
+
/\.getPrivateKey\(/.test(readFileSync(join(REPO, rel), "utf-8")),
|
|
74
|
+
);
|
|
75
|
+
expect(callers).toEqual(["admin/src/github-enterprise-api.ts"]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("the config bundle and env serializers never read the enterprise table", () => {
|
|
79
|
+
for (const rel of ["admin/src/agent-envs.ts", "admin/src/api.ts"]) {
|
|
80
|
+
const src = readFileSync(join(REPO, rel), "utf-8");
|
|
81
|
+
expect(src).not.toMatch(/enterprise/i);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|