@appforge-ci/core 0.2.3 → 0.3.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 (60) hide show
  1. package/dist/agent-readiness.d.ts +232 -1
  2. package/dist/agent-readiness.d.ts.map +1 -1
  3. package/dist/agent-readiness.js +294 -1
  4. package/dist/agent-readiness.js.map +1 -1
  5. package/dist/agent-readiness.test.js +253 -19
  6. package/dist/agent-readiness.test.js.map +1 -1
  7. package/dist/api-client.d.ts +42 -3
  8. package/dist/api-client.d.ts.map +1 -1
  9. package/dist/api-client.js +27 -0
  10. package/dist/api-client.js.map +1 -1
  11. package/dist/api-client.test.js +22 -0
  12. package/dist/api-client.test.js.map +1 -1
  13. package/dist/attestation.d.ts +265 -0
  14. package/dist/attestation.d.ts.map +1 -0
  15. package/dist/attestation.js +327 -0
  16. package/dist/attestation.js.map +1 -0
  17. package/dist/attestation.test.d.ts +2 -0
  18. package/dist/attestation.test.d.ts.map +1 -0
  19. package/dist/attestation.test.js +218 -0
  20. package/dist/attestation.test.js.map +1 -0
  21. package/dist/build-vpn.d.ts +10 -0
  22. package/dist/build-vpn.d.ts.map +1 -0
  23. package/dist/build-vpn.js +23 -0
  24. package/dist/build-vpn.js.map +1 -0
  25. package/dist/index.d.ts +5 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +5 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/schemas.d.ts +342 -7
  30. package/dist/schemas.d.ts.map +1 -1
  31. package/dist/schemas.js +92 -2
  32. package/dist/schemas.js.map +1 -1
  33. package/dist/types.d.ts +149 -0
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js.map +1 -1
  36. package/dist/vpn-egress-presets.d.ts +142 -0
  37. package/dist/vpn-egress-presets.d.ts.map +1 -0
  38. package/dist/vpn-egress-presets.js +318 -0
  39. package/dist/vpn-egress-presets.js.map +1 -0
  40. package/dist/vpn-egress-presets.test.d.ts +2 -0
  41. package/dist/vpn-egress-presets.test.d.ts.map +1 -0
  42. package/dist/vpn-egress-presets.test.js +210 -0
  43. package/dist/vpn-egress-presets.test.js.map +1 -0
  44. package/dist/vpn-egress.d.ts +57 -0
  45. package/dist/vpn-egress.d.ts.map +1 -0
  46. package/dist/vpn-egress.js +126 -0
  47. package/dist/vpn-egress.js.map +1 -0
  48. package/dist/vpn-egress.test.d.ts +2 -0
  49. package/dist/vpn-egress.test.d.ts.map +1 -0
  50. package/dist/vpn-egress.test.js +133 -0
  51. package/dist/vpn-egress.test.js.map +1 -0
  52. package/dist/vpn-ssh.d.ts +45 -0
  53. package/dist/vpn-ssh.d.ts.map +1 -0
  54. package/dist/vpn-ssh.js +108 -0
  55. package/dist/vpn-ssh.js.map +1 -0
  56. package/dist/vpn-ssh.test.d.ts +2 -0
  57. package/dist/vpn-ssh.test.d.ts.map +1 -0
  58. package/dist/vpn-ssh.test.js +75 -0
  59. package/dist/vpn-ssh.test.js.map +1 -0
  60. package/package.json +1 -1
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Strict egress policy for the site-to-site VPN helper (`VpnJobConfig.egress`,
3
+ * see packages/vpn-proxy/PROTOCOL.md, "Strict egress"). Pure: validation, the
4
+ * canonical form and its SHA-256, so the control plane, the agent and the
5
+ * helper agree on exactly which policy a build ran under.
6
+ *
7
+ * This file is the TypeScript twin of `packages/vpn-proxy/internal/policy`.
8
+ * Both must accept and reject the same inputs and produce the same canonical
9
+ * bytes; `packages/vpn-proxy/testdata/egress-policy-vectors.json` is the shared
10
+ * proof and both test suites run it.
11
+ *
12
+ * Hosts are ASCII/punycode only (the control plane converts Unicode names
13
+ * before storing them): that keeps the two implementations free of IDNA and
14
+ * Unicode case-mapping differences.
15
+ */
16
+ import type { VpnEgressPolicy } from "./types.js";
17
+ /** Mirrors `policy.MaxRules` / `policy.MaxPortsPerRule` / `policy.DefaultPort` / `policy.Version`. */
18
+ export declare const VPN_EGRESS_MAX_RULES = 512;
19
+ export declare const VPN_EGRESS_MAX_PORTS_PER_RULE = 16;
20
+ export declare const VPN_EGRESS_DEFAULT_PORT = 443;
21
+ export declare const VPN_EGRESS_POLICY_VERSION = 1;
22
+ /**
23
+ * Mirrors `config.MaxJobBytes`: the most stdin the helper reads for one job.
24
+ * It is sized so the largest valid policy (512 rules, 253-byte hosts, 16 ports
25
+ * each: about 190 KB as compact JSON) plus the rest of the job fits; the agent
26
+ * still checks the serialised job against it before spawning, because extra
27
+ * keys inside rules are ignored by validation but would count against the cap.
28
+ */
29
+ export declare const VPN_JOB_MAX_BYTES: number;
30
+ /**
31
+ * Normalises one allow-list host: lowercase; either `name.tld` or `*.name.tld`
32
+ * with at least two labels after any `*.`; no IP literal in any spelling (a
33
+ * numeric last label is refused, so `10.0.0.1`, `2130706433`, `0x7f.1` all
34
+ * fail), no trailing dot, no port/scheme/path, no Unicode. Returns `null` when
35
+ * the host is not acceptable.
36
+ */
37
+ export declare function normalizeEgressHost(raw: unknown): string | null;
38
+ /**
39
+ * Every problem with an egress policy (an empty array means valid). Problem
40
+ * strings name the field, like the helper's own validation
41
+ * (`egress.allow[3].host`), and never echo a rejected value.
42
+ */
43
+ export declare function validateVpnEgressPolicy(policy: unknown): string[];
44
+ /**
45
+ * The exact bytes the helper hashes:
46
+ *
47
+ * {"v":1,"allow":[{"host":"*.googleapis.com","ports":[443]},{"host":"pub.dev","ports":[443]}]}
48
+ *
49
+ * Hosts lowercased; one entry per host (duplicates merge, ports unioned);
50
+ * sorted by host in byte order (so `*.` sorts before letters); ports ascending
51
+ * and deduplicated with the default 443 made explicit; no whitespace.
52
+ * Throws on an invalid policy (callers validate first and report the problems).
53
+ */
54
+ export declare function canonicalVpnEgressPolicy(policy: VpnEgressPolicy): string;
55
+ /** Lowercase hex SHA-256 of `canonicalVpnEgressPolicy`: what the helper echoes as `policyHash`. */
56
+ export declare function vpnEgressPolicyHash(policy: VpnEgressPolicy): Promise<string>;
57
+ //# sourceMappingURL=vpn-egress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vpn-egress.d.ts","sourceRoot":"","sources":["../src/vpn-egress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,sGAAsG;AACtG,eAAO,MAAM,oBAAoB,MAAM,CAAC;AACxC,eAAO,MAAM,6BAA6B,KAAK,CAAC;AAChD,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C,eAAO,MAAM,yBAAyB,IAAI,CAAC;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,QAAa,CAAC;AAK5C;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAe/D;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,CAiCjE;AAED;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CAmBxE;AAED,mGAAmG;AACnG,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAIlF"}
@@ -0,0 +1,126 @@
1
+ /** Mirrors `policy.MaxRules` / `policy.MaxPortsPerRule` / `policy.DefaultPort` / `policy.Version`. */
2
+ export const VPN_EGRESS_MAX_RULES = 512;
3
+ export const VPN_EGRESS_MAX_PORTS_PER_RULE = 16;
4
+ export const VPN_EGRESS_DEFAULT_PORT = 443;
5
+ export const VPN_EGRESS_POLICY_VERSION = 1;
6
+ /**
7
+ * Mirrors `config.MaxJobBytes`: the most stdin the helper reads for one job.
8
+ * It is sized so the largest valid policy (512 rules, 253-byte hosts, 16 ports
9
+ * each: about 190 KB as compact JSON) plus the rest of the job fits; the agent
10
+ * still checks the serialised job against it before spawning, because extra
11
+ * keys inside rules are ignored by validation but would count against the cap.
12
+ */
13
+ export const VPN_JOB_MAX_BYTES = 256 * 1024;
14
+ /** Mirrors Go `route.Normalize`: LDH plus `_`, labels 1..63, total <= 253. */
15
+ const LABEL = /^[a-z0-9_-]{1,63}$/;
16
+ /**
17
+ * Normalises one allow-list host: lowercase; either `name.tld` or `*.name.tld`
18
+ * with at least two labels after any `*.`; no IP literal in any spelling (a
19
+ * numeric last label is refused, so `10.0.0.1`, `2130706433`, `0x7f.1` all
20
+ * fail), no trailing dot, no port/scheme/path, no Unicode. Returns `null` when
21
+ * the host is not acceptable.
22
+ */
23
+ export function normalizeEgressHost(raw) {
24
+ if (typeof raw !== "string")
25
+ return null;
26
+ // ASCII only, BEFORE lowercasing: toLowerCase() folds the Kelvin sign (U+212A)
27
+ // to "k". Mirrors the Go implementation.
28
+ if (!/^[\x00-\x7f]*$/.test(raw))
29
+ return null;
30
+ const host = raw.toLowerCase();
31
+ const wild = host.startsWith("*.");
32
+ const name = wild ? host.slice(2) : host;
33
+ if (name === "" || name.length > 253 || name.endsWith(".") || name.includes("*"))
34
+ return null;
35
+ const labels = name.split(".");
36
+ if (labels.length < 2)
37
+ return null;
38
+ for (const l of labels)
39
+ if (!LABEL.test(l))
40
+ return null;
41
+ const last = labels[labels.length - 1];
42
+ if (last.startsWith("0x") || /^[0-9]+$/.test(last))
43
+ return null; // an IP in some spelling
44
+ return wild ? `*.${name}` : name;
45
+ }
46
+ /**
47
+ * Every problem with an egress policy (an empty array means valid). Problem
48
+ * strings name the field, like the helper's own validation
49
+ * (`egress.allow[3].host`), and never echo a rejected value.
50
+ */
51
+ export function validateVpnEgressPolicy(policy) {
52
+ const problems = [];
53
+ if (policy === null || typeof policy !== "object" || Array.isArray(policy))
54
+ return ["egress: must be an object"];
55
+ const allow = policy.allow;
56
+ if (!Array.isArray(allow))
57
+ return ["egress.allow: must be an array"];
58
+ if (allow.length > VPN_EGRESS_MAX_RULES)
59
+ return [`egress.allow: may contain at most ${VPN_EGRESS_MAX_RULES} entries`];
60
+ allow.forEach((rule, i) => {
61
+ const at = `egress.allow[${i}]`;
62
+ if (rule === null || typeof rule !== "object" || Array.isArray(rule)) {
63
+ problems.push(`${at}: must be an object`);
64
+ return;
65
+ }
66
+ const { host, ports } = rule;
67
+ if (normalizeEgressHost(host) === null) {
68
+ problems.push(`${at}.host: must be a hostname like example.com or *.example.com (no IP literals, no bare *, no trailing dot)`);
69
+ return;
70
+ }
71
+ if (ports === undefined || ports === null)
72
+ return;
73
+ if (!Array.isArray(ports)) {
74
+ problems.push(`${at}.ports: must be an array`);
75
+ return;
76
+ }
77
+ if (ports.length > VPN_EGRESS_MAX_PORTS_PER_RULE) {
78
+ problems.push(`${at}.ports: may contain at most ${VPN_EGRESS_MAX_PORTS_PER_RULE} ports`);
79
+ return;
80
+ }
81
+ if (!ports.every((p) => typeof p === "number" && Number.isInteger(p) && p >= 1 && p <= 65535)) {
82
+ problems.push(`${at}.ports: each port must be 1-65535`);
83
+ }
84
+ });
85
+ return problems;
86
+ }
87
+ /**
88
+ * The exact bytes the helper hashes:
89
+ *
90
+ * {"v":1,"allow":[{"host":"*.googleapis.com","ports":[443]},{"host":"pub.dev","ports":[443]}]}
91
+ *
92
+ * Hosts lowercased; one entry per host (duplicates merge, ports unioned);
93
+ * sorted by host in byte order (so `*.` sorts before letters); ports ascending
94
+ * and deduplicated with the default 443 made explicit; no whitespace.
95
+ * Throws on an invalid policy (callers validate first and report the problems).
96
+ */
97
+ export function canonicalVpnEgressPolicy(policy) {
98
+ const problems = validateVpnEgressPolicy(policy);
99
+ if (problems.length > 0)
100
+ throw new Error(`invalid egress policy: ${problems.join("; ")}`);
101
+ const merged = new Map();
102
+ for (const rule of policy.allow) {
103
+ const host = normalizeEgressHost(rule.host);
104
+ let set = merged.get(host);
105
+ if (!set)
106
+ merged.set(host, (set = new Set()));
107
+ const ports = rule.ports && rule.ports.length > 0 ? rule.ports : [VPN_EGRESS_DEFAULT_PORT];
108
+ for (const p of ports)
109
+ set.add(p);
110
+ }
111
+ // Default Array.prototype.sort compares UTF-16 code units, which for ASCII is
112
+ // the same order as Go's byte-wise slices.Sort.
113
+ const hosts = [...merged.keys()].sort();
114
+ const entries = hosts.map((h) => {
115
+ const ports = [...merged.get(h)].sort((a, b) => a - b);
116
+ return `{"host":"${h}","ports":[${ports.join(",")}]}`;
117
+ });
118
+ return `{"v":${VPN_EGRESS_POLICY_VERSION},"allow":[${entries.join(",")}]}`;
119
+ }
120
+ /** Lowercase hex SHA-256 of `canonicalVpnEgressPolicy`: what the helper echoes as `policyHash`. */
121
+ export async function vpnEgressPolicyHash(policy) {
122
+ const bytes = new TextEncoder().encode(canonicalVpnEgressPolicy(policy));
123
+ const digest = await crypto.subtle.digest("SHA-256", bytes);
124
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, "0")).join("");
125
+ }
126
+ //# sourceMappingURL=vpn-egress.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vpn-egress.js","sourceRoot":"","sources":["../src/vpn-egress.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=vpn-egress.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vpn-egress.test.d.ts","sourceRoot":"","sources":["../src/vpn-egress.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,133 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFileSync } from "node:fs";
4
+ import { createHash } from "node:crypto";
5
+ import { fileURLToPath } from "node:url";
6
+ import { canonicalVpnEgressPolicy, normalizeEgressHost, validateVpnEgressPolicy, vpnEgressPolicyHash, VPN_EGRESS_MAX_PORTS_PER_RULE, VPN_EGRESS_MAX_RULES, VPN_JOB_MAX_BYTES, } from "./vpn-egress.js";
7
+ import { vpnEgressPolicySchema } from "./schemas.js";
8
+ // The Go implementation (packages/vpn-proxy/internal/policy) runs the same file.
9
+ const vectors = JSON.parse(readFileSync(fileURLToPath(new URL("../../vpn-proxy/testdata/egress-policy-vectors.json", import.meta.url)), "utf8"));
10
+ test("shared vectors: same validity, same canonical bytes, same hash as the Go helper", async () => {
11
+ assert.ok(vectors.length >= 20);
12
+ assert.ok(vectors.some((v) => v.valid) && vectors.some((v) => !v.valid));
13
+ for (const v of vectors) {
14
+ const policy = { allow: v.allow };
15
+ const problems = validateVpnEgressPolicy(policy);
16
+ if (!v.valid) {
17
+ assert.ok(problems.length > 0, `${v.name}: should be rejected`);
18
+ assert.throws(() => canonicalVpnEgressPolicy(policy), /invalid egress policy/, v.name);
19
+ continue;
20
+ }
21
+ assert.deepEqual(problems, [], `${v.name}: should be valid`);
22
+ assert.equal(canonicalVpnEgressPolicy(policy), v.canonical, `${v.name}: canonical`);
23
+ assert.equal(createHash("sha256").update(v.canonical).digest("hex"), v.sha256, `${v.name}: vector hash`);
24
+ assert.equal(await vpnEgressPolicyHash(policy), v.sha256, `${v.name}: hash`);
25
+ }
26
+ });
27
+ test("canonical form is insensitive to order, case, duplicates and explicit default ports", () => {
28
+ const a = canonicalVpnEgressPolicy({
29
+ allow: [{ host: "B.example.com", ports: [8443, 443] }, { host: "a.example.com" }, { host: "*.c.example.com", ports: [443] }],
30
+ });
31
+ const b = canonicalVpnEgressPolicy({
32
+ allow: [
33
+ { host: "*.C.example.com" },
34
+ { host: "a.example.com", ports: [443] },
35
+ { host: "b.example.com", ports: [443] },
36
+ { host: "b.example.com", ports: [8443] },
37
+ { host: "A.EXAMPLE.COM" },
38
+ ],
39
+ });
40
+ assert.equal(a, b);
41
+ assert.equal(a, '{"v":1,"allow":[{"host":"*.c.example.com","ports":[443]},{"host":"a.example.com","ports":[443]},{"host":"b.example.com","ports":[443,8443]}]}');
42
+ assert.equal(canonicalVpnEgressPolicy({ allow: [] }), '{"v":1,"allow":[]}');
43
+ assert.equal(canonicalVpnEgressPolicy({ allow: [{ host: "x.example.com", ports: [] }] }), '{"v":1,"allow":[{"host":"x.example.com","ports":[443]}]}');
44
+ });
45
+ test("the hash is the SHA-256 of the canonical bytes and changes with the policy", async () => {
46
+ const p = { allow: [{ host: "pub.dev" }] };
47
+ const h = await vpnEgressPolicyHash(p);
48
+ assert.match(h, /^[0-9a-f]{64}$/);
49
+ assert.equal(h, createHash("sha256").update(canonicalVpnEgressPolicy(p)).digest("hex"));
50
+ assert.notEqual(h, await vpnEgressPolicyHash({ allow: [{ host: "pub.dev", ports: [443, 80] }] }));
51
+ assert.notEqual(h, await vpnEgressPolicyHash({ allow: [] }));
52
+ await assert.rejects(() => vpnEgressPolicyHash({ allow: [{ host: "*" }] }), /invalid egress policy/);
53
+ });
54
+ test("normalizeEgressHost accepts hostnames and wildcards, refuses everything IP-shaped or overbroad", () => {
55
+ for (const [raw, want] of [
56
+ ["pub.dev", "pub.dev"],
57
+ ["Repo.Maven.Apache.ORG", "repo.maven.apache.org"],
58
+ ["*.GoogleAPIs.com", "*.googleapis.com"],
59
+ ["xn--bcher-kva.example", "xn--bcher-kva.example"],
60
+ ["_dmarc.example.com", "_dmarc.example.com"],
61
+ ]) {
62
+ assert.equal(normalizeEgressHost(raw), want, raw);
63
+ }
64
+ for (const bad of [
65
+ "", "*", "*.", "*.com", "intranet", "a.*.example.com", "*.*.example.com", "a*.example.com", "example.*",
66
+ "10.0.0.1", "*.10.0.0.1", "2130706433", "0x7f.0.0.1", "127.1", "::1", "[::1]", "example.com.", ".example.com",
67
+ "a..example.com", "https://example.com", "example.com/x", "example.com:443", "a@example.com", "a b.example.com",
68
+ "bücher.example", "EXAMPLE.com", "Kubernetes.io", "example.123", `${"a".repeat(64)}.example.com`,
69
+ `${"a.".repeat(130)}com`,
70
+ ]) {
71
+ assert.equal(normalizeEgressHost(bad), null, JSON.stringify(bad));
72
+ }
73
+ for (const nonString of [undefined, null, 5, {}, []])
74
+ assert.equal(normalizeEgressHost(nonString), null);
75
+ });
76
+ test("validateVpnEgressPolicy reports every problem, names the field, never echoes the value", () => {
77
+ const secret = "SUPERSECRET";
78
+ const problems = validateVpnEgressPolicy({
79
+ allow: [
80
+ { host: "ok.example.com" },
81
+ { host: `${secret}/x` },
82
+ { host: "10.0.0.1" },
83
+ { host: "ok.example.com", ports: [0] },
84
+ { host: "ok.example.com", ports: [65536] },
85
+ { host: "ok.example.com", ports: [1.5] },
86
+ { host: "ok.example.com", ports: ["443"] },
87
+ { host: "ok.example.com", ports: "443" },
88
+ { host: "ok.example.com", ports: Array.from({ length: VPN_EGRESS_MAX_PORTS_PER_RULE + 1 }, (_, i) => i + 1) },
89
+ null,
90
+ "pub.dev",
91
+ ],
92
+ });
93
+ assert.equal(problems.length, 10);
94
+ assert.ok(problems[0].startsWith("egress.allow[1].host"));
95
+ assert.ok(problems.every((p) => p.startsWith("egress.allow[")));
96
+ assert.ok(!problems.some((p) => p.includes(secret)));
97
+ assert.deepEqual(validateVpnEgressPolicy({ allow: [] }), []);
98
+ assert.deepEqual(validateVpnEgressPolicy({ allow: [{ host: "pub.dev", ports: null }] }), []);
99
+ for (const bad of [null, undefined, 5, "x", [], {}, { allow: "pub.dev" }, { allow: null }]) {
100
+ assert.equal(validateVpnEgressPolicy(bad).length, 1, JSON.stringify(bad));
101
+ }
102
+ const tooMany = { allow: Array.from({ length: VPN_EGRESS_MAX_RULES + 1 }, (_, i) => ({ host: `h${i}.example.com` })) };
103
+ assert.equal(validateVpnEgressPolicy(tooMany).length, 1);
104
+ assert.deepEqual(validateVpnEgressPolicy({ allow: tooMany.allow.slice(0, VPN_EGRESS_MAX_RULES) }), []);
105
+ });
106
+ test("vpnEgressPolicySchema bounds shape and size", () => {
107
+ assert.ok(vpnEgressPolicySchema.safeParse({ allow: [] }).success);
108
+ assert.ok(vpnEgressPolicySchema.safeParse({ allow: [{ host: "pub.dev" }, { host: "*.example.com", ports: [443, 8443] }] }).success);
109
+ for (const bad of [
110
+ {}, { allow: "x" }, { allow: [{}] }, { allow: [{ host: "" }] }, { allow: [{ host: "a.example.com", ports: [0] }] },
111
+ { allow: [{ host: "a.example.com", ports: [1.5] }] }, { allow: [{ host: "a.example.com", ports: [70000] }] },
112
+ { allow: Array.from({ length: 513 }, () => ({ host: "a.example.com" })) },
113
+ ]) {
114
+ assert.ok(!vpnEgressPolicySchema.safeParse(bad).success, JSON.stringify(bad).slice(0, 60));
115
+ }
116
+ });
117
+ test("wrapper shape: missing or null allow, and wrong-case keys, are invalid (the Go decoder agrees)", () => {
118
+ for (const bad of [{}, { allow: null }, { Allow: [] }, { allow: [{ Host: "pub.dev" }] }, { allow: [null] }]) {
119
+ assert.ok(validateVpnEgressPolicy(bad).length > 0, JSON.stringify(bad));
120
+ }
121
+ });
122
+ test("the largest valid policy fits in the helper's stdin cap with room for the rest of the job", () => {
123
+ const label = (c, n) => c.repeat(n);
124
+ const allow = Array.from({ length: VPN_EGRESS_MAX_RULES }, (_, i) => ({
125
+ host: `${String(i).padStart(3, "0")}${"a".repeat(60)}.${label("b", 63)}.${label("c", 63)}.${label("d", 61)}`,
126
+ ports: Array.from({ length: VPN_EGRESS_MAX_PORTS_PER_RULE }, (_, k) => 60000 + k),
127
+ }));
128
+ assert.equal(allow[0].host.length, 253);
129
+ assert.deepEqual(validateVpnEgressPolicy({ allow }), []);
130
+ const bytes = Buffer.byteLength(JSON.stringify({ egress: { allow } }));
131
+ assert.ok(bytes + 8 * 1024 < VPN_JOB_MAX_BYTES, `${bytes} bytes leaves too little room under ${VPN_JOB_MAX_BYTES}`);
132
+ });
133
+ //# sourceMappingURL=vpn-egress.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vpn-egress.test.js","sourceRoot":"","sources":["../src/vpn-egress.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * SSH material for a VPN build (`VpnJobConfig.sshKnownHosts` / `sshDeployKey`,
3
+ * the "git over SSH through the tunnel" part of the regulated-VPN program).
4
+ *
5
+ * Pure validation shared by whoever stores the values (the control plane) and
6
+ * whoever applies them (the agent, `packages/agent/src/lib/vpn-ssh.ts`), so the
7
+ * two can never disagree about what is acceptable:
8
+ *
9
+ * - `sanitizeKnownHosts`: customer-supplied public host keys (or host CAs).
10
+ * Free text in the dashboard, so anything that is not exactly one recognised
11
+ * host-key line is refused rather than repaired, and a rejected line is never
12
+ * echoed back (a pasted private key must not travel into a log or an error).
13
+ * There is no trust-on-first-use anywhere: these lines are the ONLY host
14
+ * keys a VPN build's ssh will accept.
15
+ * - `isPlausibleSshPrivateKey`: a cheap shape check for the deploy key. It
16
+ * does not prove the key is usable (ssh-add does that); it keeps obviously
17
+ * wrong input (a public key, a huge blob, binary) out of the lease.
18
+ */
19
+ /** At most this many known_hosts lines are accepted per tunnel. */
20
+ export declare const MAX_KNOWN_HOSTS_LINES = 64;
21
+ /** Longest accepted known_hosts line (a 4096-bit RSA key with a comment is ~800). */
22
+ export declare const MAX_KNOWN_HOSTS_LINE_LENGTH = 2048;
23
+ /** Longest accepted deploy key (an RSA-4096 OpenSSH key is ~3.4 KB). */
24
+ export declare const MAX_SSH_DEPLOY_KEY_LENGTH: number;
25
+ export interface SanitizedKnownHosts {
26
+ /** Normalised, de-duplicated lines, safe to write to a known_hosts file. */
27
+ lines: string[];
28
+ /** 1-based indexes of input lines that were refused. Contents are never echoed. */
29
+ rejected: number[];
30
+ }
31
+ /**
32
+ * Validates customer-supplied known_hosts lines. Comments (`# ...`) and blank
33
+ * lines are dropped silently (`ssh-keyscan` output has them); duplicates are
34
+ * dropped; every other line must be exactly one recognised host-key (or
35
+ * `@cert-authority`) line whose key blob really is of the stated type, or it is
36
+ * listed in `rejected`. Input past `MAX_KNOWN_HOSTS_LINES` accepted lines is
37
+ * rejected, not silently truncated.
38
+ */
39
+ export declare function sanitizeKnownHosts(input: readonly unknown[]): SanitizedKnownHosts;
40
+ /**
41
+ * A cheap shape check for an OpenSSH/PEM private key: one BEGIN/END pair of a
42
+ * `... PRIVATE KEY` block, bounded size, no NUL. Not a proof of validity.
43
+ */
44
+ export declare function isPlausibleSshPrivateKey(value: unknown): value is string;
45
+ //# sourceMappingURL=vpn-ssh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vpn-ssh.d.ts","sourceRoot":"","sources":["../src/vpn-ssh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,KAAK,CAAC;AACxC,qFAAqF;AACrF,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAChD,wEAAwE;AACxE,eAAO,MAAM,yBAAyB,QAAY,CAAC;AAoBnD,MAAM,WAAW,mBAAmB;IAClC,4EAA4E;IAC5E,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,mFAAmF;IACnF,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AA0BD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,mBAAmB,CAqBjF;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAIxE"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * SSH material for a VPN build (`VpnJobConfig.sshKnownHosts` / `sshDeployKey`,
3
+ * the "git over SSH through the tunnel" part of the regulated-VPN program).
4
+ *
5
+ * Pure validation shared by whoever stores the values (the control plane) and
6
+ * whoever applies them (the agent, `packages/agent/src/lib/vpn-ssh.ts`), so the
7
+ * two can never disagree about what is acceptable:
8
+ *
9
+ * - `sanitizeKnownHosts`: customer-supplied public host keys (or host CAs).
10
+ * Free text in the dashboard, so anything that is not exactly one recognised
11
+ * host-key line is refused rather than repaired, and a rejected line is never
12
+ * echoed back (a pasted private key must not travel into a log or an error).
13
+ * There is no trust-on-first-use anywhere: these lines are the ONLY host
14
+ * keys a VPN build's ssh will accept.
15
+ * - `isPlausibleSshPrivateKey`: a cheap shape check for the deploy key. It
16
+ * does not prove the key is usable (ssh-add does that); it keeps obviously
17
+ * wrong input (a public key, a huge blob, binary) out of the lease.
18
+ */
19
+ /** At most this many known_hosts lines are accepted per tunnel. */
20
+ export const MAX_KNOWN_HOSTS_LINES = 64;
21
+ /** Longest accepted known_hosts line (a 4096-bit RSA key with a comment is ~800). */
22
+ export const MAX_KNOWN_HOSTS_LINE_LENGTH = 2048;
23
+ /** Longest accepted deploy key (an RSA-4096 OpenSSH key is ~3.4 KB). */
24
+ export const MAX_SSH_DEPLOY_KEY_LENGTH = 16 * 1024;
25
+ const KEY_TYPES = [
26
+ "ssh-ed25519",
27
+ "ecdsa-sha2-nistp256",
28
+ "ecdsa-sha2-nistp384",
29
+ "ecdsa-sha2-nistp521",
30
+ "ssh-rsa",
31
+ "sk-ssh-ed25519@openssh.com",
32
+ "sk-ecdsa-sha2-nistp256@openssh.com",
33
+ ];
34
+ // [@cert-authority ]<host patterns> <keytype> <base64> [comment]. No other
35
+ // markers (@revoked is not accepted: the customer revokes by removing the key),
36
+ // no leading ssh option fields (`command=...`), single spaces only.
37
+ const HOSTS = String.raw `[A-Za-z0-9.*?!,:_\-\[\]|=+/]+`;
38
+ const KNOWN_HOSTS_LINE = new RegExp(`^(@cert-authority )?(${HOSTS}) (${KEY_TYPES.map((t) => t.replace(/[.]/g, "\\.")).join("|")}) ([A-Za-z0-9+/]{20,}={0,3})( [\\x20-\\x7e]{0,200})?$`);
39
+ /** True when the base64 blob is a well-formed SSH wire key of type `keyType`:
40
+ * a sequence of length-prefixed strings that consumes the blob exactly (so a
41
+ * truncated or padded blob is refused), the first of which is the type name
42
+ * (what ssh itself checks first), followed by at least one key field. */
43
+ function blobMatchesType(b64, keyType) {
44
+ let bin;
45
+ try {
46
+ bin = atob(b64);
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ let pos = 0;
52
+ const fields = [];
53
+ while (pos < bin.length) {
54
+ if (pos + 4 > bin.length)
55
+ return false;
56
+ const len = (bin.charCodeAt(pos) * 0x1000000) + ((bin.charCodeAt(pos + 1) << 16) | (bin.charCodeAt(pos + 2) << 8) | bin.charCodeAt(pos + 3));
57
+ pos += 4;
58
+ if (len > bin.length - pos)
59
+ return false;
60
+ fields.push(bin.slice(pos, pos + len));
61
+ pos += len;
62
+ }
63
+ return fields.length >= 2 && fields[0] === keyType;
64
+ }
65
+ /**
66
+ * Validates customer-supplied known_hosts lines. Comments (`# ...`) and blank
67
+ * lines are dropped silently (`ssh-keyscan` output has them); duplicates are
68
+ * dropped; every other line must be exactly one recognised host-key (or
69
+ * `@cert-authority`) line whose key blob really is of the stated type, or it is
70
+ * listed in `rejected`. Input past `MAX_KNOWN_HOSTS_LINES` accepted lines is
71
+ * rejected, not silently truncated.
72
+ */
73
+ export function sanitizeKnownHosts(input) {
74
+ const lines = [];
75
+ const rejected = [];
76
+ const seen = new Set();
77
+ input.forEach((raw, i) => {
78
+ if (typeof raw !== "string") {
79
+ rejected.push(i + 1);
80
+ return;
81
+ }
82
+ const line = raw.trim();
83
+ if (line === "" || line.startsWith("#"))
84
+ return;
85
+ const m = line.length <= MAX_KNOWN_HOSTS_LINE_LENGTH ? KNOWN_HOSTS_LINE.exec(line) : null;
86
+ if (!m || !blobMatchesType(m[4], m[3]) || lines.length >= MAX_KNOWN_HOSTS_LINES) {
87
+ rejected.push(i + 1);
88
+ return;
89
+ }
90
+ if (seen.has(line))
91
+ return;
92
+ seen.add(line);
93
+ lines.push(line);
94
+ });
95
+ return { lines, rejected };
96
+ }
97
+ /**
98
+ * A cheap shape check for an OpenSSH/PEM private key: one BEGIN/END pair of a
99
+ * `... PRIVATE KEY` block, bounded size, no NUL. Not a proof of validity.
100
+ */
101
+ export function isPlausibleSshPrivateKey(value) {
102
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_SSH_DEPLOY_KEY_LENGTH)
103
+ return false;
104
+ if (value.includes("\u0000"))
105
+ return false;
106
+ return /^-----BEGIN [A-Z0-9 ]{0,30}PRIVATE KEY-----\r?\n[A-Za-z0-9+/=\r\n: ,.-]+\r?\n-----END [A-Z0-9 ]{0,30}PRIVATE KEY-----\s*$/.test(value);
107
+ }
108
+ //# sourceMappingURL=vpn-ssh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vpn-ssh.js","sourceRoot":"","sources":["../src/vpn-ssh.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=vpn-ssh.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vpn-ssh.test.d.ts","sourceRoot":"","sources":["../src/vpn-ssh.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,75 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { isPlausibleSshPrivateKey, MAX_KNOWN_HOSTS_LINES, sanitizeKnownHosts } from "./vpn-ssh.js";
4
+ const ED = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK91pDfs6YOw64Pk/1OQrblxsDl0obAVDxwCRQdDWeVM";
5
+ const ECDSA = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBvuulkZYdqW+h/n3m7I39sGrSATHP+KNj8oDXKvXuio3bne5GoiF6ePP8jCxnH/fhJ3rMy4Sidwg3aYllX4110=";
6
+ test("sanitizeKnownHosts accepts host-key and cert-authority lines, drops comments, blanks and duplicates", () => {
7
+ const r = sanitizeKnownHosts([
8
+ "# ssh-keyscan -t ed25519 git.corp.test",
9
+ "",
10
+ " ",
11
+ `git.corp.test ${ED}`,
12
+ `[git.corp.test]:2222,10.20.0.5 ${ED} root@host`,
13
+ `git.corp.test ${ED}`,
14
+ `@cert-authority *.corp.test ${ED}`,
15
+ `|1|c2FsdHNhbHRzYWx0c2FsdA==|aGFzaGhhc2hoYXNoaGFzaA== ${ED}`,
16
+ `git2.corp.test ${ECDSA}`,
17
+ ]);
18
+ assert.equal(r.lines.length, 5);
19
+ assert.deepEqual(r.rejected, []);
20
+ assert.equal(r.lines[0], `git.corp.test ${ED}`);
21
+ });
22
+ test("sanitizeKnownHosts refuses anything that is not exactly one host-key line, without echoing it", () => {
23
+ const r = sanitizeKnownHosts([
24
+ "-----BEGIN OPENSSH PRIVATE KEY-----",
25
+ `@revoked git.corp.test ${ED}`,
26
+ `command="rm -rf /" git.corp.test ${ED}`,
27
+ "git.corp.test ssh-dss AAAAB3NzaC1kc3MAAACBAP",
28
+ `git.corp.test ${ED}\nsecond line smuggled`,
29
+ `git.corp.test ${ED} ` + "x".repeat(3000),
30
+ "git.corp.test",
31
+ `git.corp.test ${ED}`, // two spaces
32
+ `git.corp.test\t${ED}`, // tab
33
+ `git.corp.test ${ED} comment\u0000with-nul`,
34
+ `git.corp.test ${ED} comment\u2028with-separator`,
35
+ 42,
36
+ null,
37
+ ]);
38
+ assert.deepEqual(r.lines, []);
39
+ assert.deepEqual(r.rejected, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]);
40
+ });
41
+ test("sanitizeKnownHosts requires the key blob to really be of the stated type", () => {
42
+ const mismatched = "ssh-ed25519 " + ECDSA.split(" ")[1];
43
+ const garbage = "ssh-ed25519 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
44
+ const notBase64Length = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK91pDfs6YOw64Pk/1OQrblxsDl0obAVDxwCRQdDWeV";
45
+ const r = sanitizeKnownHosts([`h ${mismatched}`, `h ${garbage}`, `h ${notBase64Length}`, `h ${ED}`]);
46
+ assert.deepEqual(r.lines, [`h ${ED}`]);
47
+ assert.deepEqual(r.rejected, [1, 2, 3]);
48
+ });
49
+ test("sanitizeKnownHosts caps the number of lines and rejects (not truncates) the rest", () => {
50
+ const many = Array.from({ length: 100 }, (_, i) => `h${i}.corp.test ${ED}`);
51
+ const r = sanitizeKnownHosts(many);
52
+ assert.equal(r.lines.length, MAX_KNOWN_HOSTS_LINES);
53
+ assert.equal(r.rejected.length, 36);
54
+ assert.equal(r.rejected[0], 65);
55
+ });
56
+ test("isPlausibleSshPrivateKey: a PEM/OpenSSH private-key block of sane size, nothing else", () => {
57
+ const body = "b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZWQyNTUxOQ";
58
+ assert.equal(isPlausibleSshPrivateKey(`-----BEGIN OPENSSH PRIVATE KEY-----\n${body}\n-----END OPENSSH PRIVATE KEY-----\n`), true);
59
+ assert.equal(isPlausibleSshPrivateKey(`-----BEGIN RSA PRIVATE KEY-----\r\n${body}\r\n-----END RSA PRIVATE KEY-----`), true);
60
+ for (const bad of [
61
+ "",
62
+ ED, // a PUBLIC key
63
+ "-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----\n",
64
+ `-----BEGIN OPENSSH PRIVATE KEY-----\n${body}\u0000\n-----END OPENSSH PRIVATE KEY-----\n`,
65
+ `-----BEGIN OPENSSH PRIVATE KEY-----\n${"A".repeat(20000)}\n-----END OPENSSH PRIVATE KEY-----\n`,
66
+ `-----BEGIN OPENSSH PRIVATE KEY-----\n${body}\n`,
67
+ `x-----BEGIN OPENSSH PRIVATE KEY-----\n${body}\n-----END OPENSSH PRIVATE KEY-----\n`,
68
+ 42,
69
+ null,
70
+ undefined,
71
+ ]) {
72
+ assert.equal(isPlausibleSshPrivateKey(bad), false, String(bad).slice(0, 40));
73
+ }
74
+ });
75
+ //# sourceMappingURL=vpn-ssh.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vpn-ssh.test.js","sourceRoot":"","sources":["../src/vpn-ssh.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appforge-ci/core",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"