@appforge-ci/core 0.2.3 → 0.3.1
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/dist/agent-readiness.d.ts +240 -1
- package/dist/agent-readiness.d.ts.map +1 -1
- package/dist/agent-readiness.js +304 -1
- package/dist/agent-readiness.js.map +1 -1
- package/dist/agent-readiness.test.js +277 -19
- package/dist/agent-readiness.test.js.map +1 -1
- package/dist/api-client.d.ts +80 -3
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +79 -0
- package/dist/api-client.js.map +1 -1
- package/dist/api-client.test.js +22 -0
- package/dist/api-client.test.js.map +1 -1
- package/dist/appforge-verify.test.d.ts +2 -0
- package/dist/appforge-verify.test.d.ts.map +1 -0
- package/dist/appforge-verify.test.js +958 -0
- package/dist/appforge-verify.test.js.map +1 -0
- package/dist/attestation-keys.d.ts +20 -0
- package/dist/attestation-keys.d.ts.map +1 -0
- package/dist/attestation-keys.js +27 -0
- package/dist/attestation-keys.js.map +1 -0
- package/dist/attestation-keys.test.d.ts +2 -0
- package/dist/attestation-keys.test.d.ts.map +1 -0
- package/dist/attestation-keys.test.js +45 -0
- package/dist/attestation-keys.test.js.map +1 -0
- package/dist/attestation-signing.d.ts +226 -0
- package/dist/attestation-signing.d.ts.map +1 -0
- package/dist/attestation-signing.js +276 -0
- package/dist/attestation-signing.js.map +1 -0
- package/dist/attestation-signing.test.d.ts +2 -0
- package/dist/attestation-signing.test.d.ts.map +1 -0
- package/dist/attestation-signing.test.js +250 -0
- package/dist/attestation-signing.test.js.map +1 -0
- package/dist/attestation-workerd.test.d.ts +2 -0
- package/dist/attestation-workerd.test.d.ts.map +1 -0
- package/dist/attestation-workerd.test.js +183 -0
- package/dist/attestation-workerd.test.js.map +1 -0
- package/dist/attestation.d.ts +412 -0
- package/dist/attestation.d.ts.map +1 -0
- package/dist/attestation.js +353 -0
- package/dist/attestation.js.map +1 -0
- package/dist/attestation.test.d.ts +2 -0
- package/dist/attestation.test.d.ts.map +1 -0
- package/dist/attestation.test.js +218 -0
- package/dist/attestation.test.js.map +1 -0
- package/dist/build-vpn.d.ts +20 -0
- package/dist/build-vpn.d.ts.map +1 -0
- package/dist/build-vpn.js +50 -0
- package/dist/build-vpn.js.map +1 -0
- package/dist/fleet-vpn.d.ts +64 -0
- package/dist/fleet-vpn.d.ts.map +1 -0
- package/dist/fleet-vpn.js +88 -0
- package/dist/fleet-vpn.js.map +1 -0
- package/dist/fleet-vpn.test.d.ts +2 -0
- package/dist/fleet-vpn.test.d.ts.map +1 -0
- package/dist/fleet-vpn.test.js +69 -0
- package/dist/fleet-vpn.test.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/schemas-repo-url.test.d.ts +2 -0
- package/dist/schemas-repo-url.test.d.ts.map +1 -0
- package/dist/schemas-repo-url.test.js +67 -0
- package/dist/schemas-repo-url.test.js.map +1 -0
- package/dist/schemas.d.ts +360 -7
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +148 -12
- package/dist/schemas.js.map +1 -1
- package/dist/ssh-deploy-key.d.ts +59 -0
- package/dist/ssh-deploy-key.d.ts.map +1 -0
- package/dist/ssh-deploy-key.js +124 -0
- package/dist/ssh-deploy-key.js.map +1 -0
- package/dist/ssh-deploy-key.test.d.ts +2 -0
- package/dist/ssh-deploy-key.test.d.ts.map +1 -0
- package/dist/ssh-deploy-key.test.js +117 -0
- package/dist/ssh-deploy-key.test.js.map +1 -0
- package/dist/types.d.ts +204 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -5
- package/dist/types.js.map +1 -1
- package/dist/vpn-attestation-workerd.test.d.ts +2 -0
- package/dist/vpn-attestation-workerd.test.d.ts.map +1 -0
- package/dist/vpn-attestation-workerd.test.js +184 -0
- package/dist/vpn-attestation-workerd.test.js.map +1 -0
- package/dist/vpn-attestation.d.ts +1015 -0
- package/dist/vpn-attestation.d.ts.map +1 -0
- package/dist/vpn-attestation.js +963 -0
- package/dist/vpn-attestation.js.map +1 -0
- package/dist/vpn-attestation.test.d.ts +2 -0
- package/dist/vpn-attestation.test.d.ts.map +1 -0
- package/dist/vpn-attestation.test.js +613 -0
- package/dist/vpn-attestation.test.js.map +1 -0
- package/dist/vpn-egress-presets.d.ts +142 -0
- package/dist/vpn-egress-presets.d.ts.map +1 -0
- package/dist/vpn-egress-presets.js +318 -0
- package/dist/vpn-egress-presets.js.map +1 -0
- package/dist/vpn-egress-presets.test.d.ts +2 -0
- package/dist/vpn-egress-presets.test.d.ts.map +1 -0
- package/dist/vpn-egress-presets.test.js +210 -0
- package/dist/vpn-egress-presets.test.js.map +1 -0
- package/dist/vpn-egress.d.ts +57 -0
- package/dist/vpn-egress.d.ts.map +1 -0
- package/dist/vpn-egress.js +126 -0
- package/dist/vpn-egress.js.map +1 -0
- package/dist/vpn-egress.test.d.ts +2 -0
- package/dist/vpn-egress.test.d.ts.map +1 -0
- package/dist/vpn-egress.test.js +133 -0
- package/dist/vpn-egress.test.js.map +1 -0
- package/dist/vpn-sandbox.d.ts +67 -0
- package/dist/vpn-sandbox.d.ts.map +1 -0
- package/dist/vpn-sandbox.js +22 -0
- package/dist/vpn-sandbox.js.map +1 -0
- package/dist/vpn-sandbox.test.d.ts +2 -0
- package/dist/vpn-sandbox.test.d.ts.map +1 -0
- package/dist/vpn-sandbox.test.js +34 -0
- package/dist/vpn-sandbox.test.js.map +1 -0
- package/dist/vpn-ssh.d.ts +138 -0
- package/dist/vpn-ssh.d.ts.map +1 -0
- package/dist/vpn-ssh.js +320 -0
- package/dist/vpn-ssh.js.map +1 -0
- package/dist/vpn-ssh.test.d.ts +2 -0
- package/dist/vpn-ssh.test.d.ts.map +1 -0
- package/dist/vpn-ssh.test.js +274 -0
- package/dist/vpn-ssh.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { AttestationSignature } from "./attestation-signing.js";
|
|
3
|
+
import type { VpnEgressPolicy, VpnJobConfig } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Per-build signed attestation (regulated-VPN program, PR-M): what the document says, how it is
|
|
6
|
+
* derived, and the evidence bundle and printable page an auditor verifies offline with
|
|
7
|
+
* `scripts/appforge-verify.mjs`. Pure and runtime-neutral (WebCrypto only), so the Worker that
|
|
8
|
+
* assembles it (apps/admin-api through `@appforge/db`), the Worker that exports it (apps/api), the
|
|
9
|
+
* agent that reports the raw evidence, and the tests that feed the real verifier all share one
|
|
10
|
+
* definition.
|
|
11
|
+
*
|
|
12
|
+
* Who says what. Three parties contribute facts, and the document records which:
|
|
13
|
+
* - the SERVER (control plane): what it leased (the egress policy it sent, the tunnel's public
|
|
14
|
+
* configuration), the build row, the VpnEvent timeline, the audit chain head, and everything it
|
|
15
|
+
* can RECOMPUTE from raw data it holds (the connection log's digest, per-destination summary,
|
|
16
|
+
* totals and completeness);
|
|
17
|
+
* - the AGENT: what ran on the Mac (helper identity, sandbox profile hashes and the self-test
|
|
18
|
+
* matrix, hygiene), reported in one JSON document plus the raw connection log;
|
|
19
|
+
* - the HELPER: the connection log itself (metadata only), written by a process on the build host.
|
|
20
|
+
* Every value the agent claims that the server can also derive is compared, and a mismatch is
|
|
21
|
+
* recorded as a failing control test (the verifier exits 2 on those). Nothing is silently trusted,
|
|
22
|
+
* and nothing is ever dropped silently either: what could not be checked is listed in `crossChecks`
|
|
23
|
+
* and `gaps`.
|
|
24
|
+
*
|
|
25
|
+
* What it is not: macOS offers no remote attestation, so this proves what the server verified and
|
|
26
|
+
* what the Mac reported, not that the Mac told the truth. docs/attestation.md lists the limits.
|
|
27
|
+
*/
|
|
28
|
+
export declare const ATTESTATION_DOC_TYPE = "appforge.vpn.build-attestation";
|
|
29
|
+
export declare const ATTESTATION_DOC_VERSION = 1;
|
|
30
|
+
export declare const EVIDENCE_BUNDLE_FORMAT = "appforge-evidence-bundle";
|
|
31
|
+
/** The rule that turns the helper's raw log lines into the `records` a bundle carries. Part of the signed document. */
|
|
32
|
+
export declare const CONNECTION_RECORDS_NORMALIZATION = "appforge-conn-records-v1";
|
|
33
|
+
/**
|
|
34
|
+
* The largest connection log the control plane will hold and process (the agent keeps up to 50 MiB; a log
|
|
35
|
+
* past this is reported as not retained and the attestation says so). 8 MiB is about 27,000 records, an order
|
|
36
|
+
* of magnitude above a real build, and keeps one Worker request well inside its CPU and memory limits.
|
|
37
|
+
*/
|
|
38
|
+
export declare const ATTESTATION_LOG_MAX_BYTES: number;
|
|
39
|
+
/** Largest `POST /agents/vpn/attestation` body. */
|
|
40
|
+
export declare const ATTESTATION_EVIDENCE_MAX_BYTES: number;
|
|
41
|
+
/**
|
|
42
|
+
* How long after the control plane first sees a strict/regulated build finished the agent may still report its
|
|
43
|
+
* evidence. After it, the cron issues a `server_observed_only` attestation and a late report is refused.
|
|
44
|
+
*/
|
|
45
|
+
export declare const ATTESTATION_AGENT_GRACE_SECONDS: number;
|
|
46
|
+
/** Distinct (destination, route, decision) groups a document lists; beyond it the summary is cut and the document says so. */
|
|
47
|
+
export declare const ATTESTATION_MAX_DESTINATIONS = 1000;
|
|
48
|
+
/** Chain entries one evidence bundle carries at most (one page of the audit export). */
|
|
49
|
+
export declare const ATTESTATION_BUNDLE_MAX_CHAIN_ENTRIES = 1000;
|
|
50
|
+
export declare const ATTESTATION_ID_PREFIX = "att_";
|
|
51
|
+
export declare const attestationIdForBuild: (buildId: string) => string;
|
|
52
|
+
/** A build id as the control plane makes them (UUIDs) and as the agent's evidence directory and the helper accept them. */
|
|
53
|
+
export declare const ATTESTATION_BUILD_ID_RE: RegExp;
|
|
54
|
+
/** Where a build's raw connection log lives in the artifacts bucket. */
|
|
55
|
+
export declare function connectionLogKey(orgId: string, buildId: string): string;
|
|
56
|
+
/** Release manifests, published next to the artifacts by scripts/r2-release.mjs. */
|
|
57
|
+
export declare function releaseManifestKey(prefix: "agent-releases" | "vpn-releases", version: string): string;
|
|
58
|
+
/** Why a document is unsigned, in the words the API and the dashboard use. */
|
|
59
|
+
export declare const ATTESTATION_UNSIGNED_NOTE = "This attestation is NOT signed: the platform's signing key was not available when it was issued. It is registered in the audit chain, but nothing authenticates it, and it cannot be verified offline.";
|
|
60
|
+
declare const selfTestResultSchema: z.ZodObject<{
|
|
61
|
+
id: z.ZodPipeline<z.ZodEffects<z.ZodString, string, string>, z.ZodString>;
|
|
62
|
+
area: z.ZodEffects<z.ZodString, string, string>;
|
|
63
|
+
title: z.ZodEffects<z.ZodString, string, string>;
|
|
64
|
+
tier: z.ZodEnum<["core", "internet", "optional"]>;
|
|
65
|
+
expect: z.ZodEnum<["blocked", "allowed", "known_gap"]>;
|
|
66
|
+
status: z.ZodEnum<["pass", "fail", "inconclusive", "na", "known_gap_open", "known_gap_closed"]>;
|
|
67
|
+
detail: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
68
|
+
}, "strip", z.ZodTypeAny, {
|
|
69
|
+
status: "fail" | "pass" | "inconclusive" | "na" | "known_gap_open" | "known_gap_closed";
|
|
70
|
+
id: string;
|
|
71
|
+
title: string;
|
|
72
|
+
expect: "blocked" | "allowed" | "known_gap";
|
|
73
|
+
area: string;
|
|
74
|
+
tier: "core" | "internet" | "optional";
|
|
75
|
+
detail?: string | undefined;
|
|
76
|
+
}, {
|
|
77
|
+
status: "fail" | "pass" | "inconclusive" | "na" | "known_gap_open" | "known_gap_closed";
|
|
78
|
+
id: string;
|
|
79
|
+
title: string;
|
|
80
|
+
expect: "blocked" | "allowed" | "known_gap";
|
|
81
|
+
area: string;
|
|
82
|
+
tier: "core" | "internet" | "optional";
|
|
83
|
+
detail?: string | undefined;
|
|
84
|
+
}>;
|
|
85
|
+
export type AgentSelfTestResult = z.infer<typeof selfTestResultSchema>;
|
|
86
|
+
declare const sandboxEvidenceSchema: z.ZodObject<{
|
|
87
|
+
profileVersion: z.ZodEffects<z.ZodNumber, number, number>;
|
|
88
|
+
templateSha256: z.ZodString;
|
|
89
|
+
renderedSha256: z.ZodString;
|
|
90
|
+
paramsSha256: z.ZodString;
|
|
91
|
+
loopbackDenyPorts: z.ZodArray<z.ZodEffects<z.ZodNumber, number, number>, "many">;
|
|
92
|
+
orgId: z.ZodEffects<z.ZodString, string, string>;
|
|
93
|
+
selfTest: z.ZodObject<{
|
|
94
|
+
verdict: z.ZodEnum<["pass", "fail", "inconclusive"]>;
|
|
95
|
+
counts: z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodNumber, number, number>>, Record<string, number>, Record<string, number>>;
|
|
96
|
+
startedAt: z.ZodEffects<z.ZodString, string, string>;
|
|
97
|
+
durationMs: z.ZodEffects<z.ZodNumber, number, number>;
|
|
98
|
+
results: z.ZodArray<z.ZodObject<{
|
|
99
|
+
id: z.ZodPipeline<z.ZodEffects<z.ZodString, string, string>, z.ZodString>;
|
|
100
|
+
area: z.ZodEffects<z.ZodString, string, string>;
|
|
101
|
+
title: z.ZodEffects<z.ZodString, string, string>;
|
|
102
|
+
tier: z.ZodEnum<["core", "internet", "optional"]>;
|
|
103
|
+
expect: z.ZodEnum<["blocked", "allowed", "known_gap"]>;
|
|
104
|
+
status: z.ZodEnum<["pass", "fail", "inconclusive", "na", "known_gap_open", "known_gap_closed"]>;
|
|
105
|
+
detail: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
106
|
+
}, "strip", z.ZodTypeAny, {
|
|
107
|
+
status: "fail" | "pass" | "inconclusive" | "na" | "known_gap_open" | "known_gap_closed";
|
|
108
|
+
id: string;
|
|
109
|
+
title: string;
|
|
110
|
+
expect: "blocked" | "allowed" | "known_gap";
|
|
111
|
+
area: string;
|
|
112
|
+
tier: "core" | "internet" | "optional";
|
|
113
|
+
detail?: string | undefined;
|
|
114
|
+
}, {
|
|
115
|
+
status: "fail" | "pass" | "inconclusive" | "na" | "known_gap_open" | "known_gap_closed";
|
|
116
|
+
id: string;
|
|
117
|
+
title: string;
|
|
118
|
+
expect: "blocked" | "allowed" | "known_gap";
|
|
119
|
+
area: string;
|
|
120
|
+
tier: "core" | "internet" | "optional";
|
|
121
|
+
detail?: string | undefined;
|
|
122
|
+
}>, "many">;
|
|
123
|
+
}, "strip", z.ZodTypeAny, {
|
|
124
|
+
counts: Record<string, number>;
|
|
125
|
+
verdict: "fail" | "pass" | "inconclusive";
|
|
126
|
+
startedAt: string;
|
|
127
|
+
durationMs: number;
|
|
128
|
+
results: {
|
|
129
|
+
status: "fail" | "pass" | "inconclusive" | "na" | "known_gap_open" | "known_gap_closed";
|
|
130
|
+
id: string;
|
|
131
|
+
title: string;
|
|
132
|
+
expect: "blocked" | "allowed" | "known_gap";
|
|
133
|
+
area: string;
|
|
134
|
+
tier: "core" | "internet" | "optional";
|
|
135
|
+
detail?: string | undefined;
|
|
136
|
+
}[];
|
|
137
|
+
}, {
|
|
138
|
+
counts: Record<string, number>;
|
|
139
|
+
verdict: "fail" | "pass" | "inconclusive";
|
|
140
|
+
startedAt: string;
|
|
141
|
+
durationMs: number;
|
|
142
|
+
results: {
|
|
143
|
+
status: "fail" | "pass" | "inconclusive" | "na" | "known_gap_open" | "known_gap_closed";
|
|
144
|
+
id: string;
|
|
145
|
+
title: string;
|
|
146
|
+
expect: "blocked" | "allowed" | "known_gap";
|
|
147
|
+
area: string;
|
|
148
|
+
tier: "core" | "internet" | "optional";
|
|
149
|
+
detail?: string | undefined;
|
|
150
|
+
}[];
|
|
151
|
+
}>;
|
|
152
|
+
scope: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
153
|
+
}, "strip", z.ZodTypeAny, {
|
|
154
|
+
orgId: string;
|
|
155
|
+
profileVersion: number;
|
|
156
|
+
templateSha256: string;
|
|
157
|
+
renderedSha256: string;
|
|
158
|
+
paramsSha256: string;
|
|
159
|
+
loopbackDenyPorts: number[];
|
|
160
|
+
selfTest: {
|
|
161
|
+
counts: Record<string, number>;
|
|
162
|
+
verdict: "fail" | "pass" | "inconclusive";
|
|
163
|
+
startedAt: string;
|
|
164
|
+
durationMs: number;
|
|
165
|
+
results: {
|
|
166
|
+
status: "fail" | "pass" | "inconclusive" | "na" | "known_gap_open" | "known_gap_closed";
|
|
167
|
+
id: string;
|
|
168
|
+
title: string;
|
|
169
|
+
expect: "blocked" | "allowed" | "known_gap";
|
|
170
|
+
area: string;
|
|
171
|
+
tier: "core" | "internet" | "optional";
|
|
172
|
+
detail?: string | undefined;
|
|
173
|
+
}[];
|
|
174
|
+
};
|
|
175
|
+
scope?: string | undefined;
|
|
176
|
+
}, {
|
|
177
|
+
orgId: string;
|
|
178
|
+
profileVersion: number;
|
|
179
|
+
templateSha256: string;
|
|
180
|
+
renderedSha256: string;
|
|
181
|
+
paramsSha256: string;
|
|
182
|
+
loopbackDenyPorts: number[];
|
|
183
|
+
selfTest: {
|
|
184
|
+
counts: Record<string, number>;
|
|
185
|
+
verdict: "fail" | "pass" | "inconclusive";
|
|
186
|
+
startedAt: string;
|
|
187
|
+
durationMs: number;
|
|
188
|
+
results: {
|
|
189
|
+
status: "fail" | "pass" | "inconclusive" | "na" | "known_gap_open" | "known_gap_closed";
|
|
190
|
+
id: string;
|
|
191
|
+
title: string;
|
|
192
|
+
expect: "blocked" | "allowed" | "known_gap";
|
|
193
|
+
area: string;
|
|
194
|
+
tier: "core" | "internet" | "optional";
|
|
195
|
+
detail?: string | undefined;
|
|
196
|
+
}[];
|
|
197
|
+
};
|
|
198
|
+
scope?: string | undefined;
|
|
199
|
+
}>;
|
|
200
|
+
export type AgentSandboxEvidence = z.infer<typeof sandboxEvidenceSchema>;
|
|
201
|
+
/**
|
|
202
|
+
* What an agent sends to `POST /agents/vpn/attestation` after a strict-egress or regulated build ends. Unknown
|
|
203
|
+
* top-level fields are dropped (a newer agent may send more), `hygiene` is kept as an opaque, size-bounded object
|
|
204
|
+
* because its shape is owned by a change that has not shipped everywhere.
|
|
205
|
+
*/
|
|
206
|
+
export declare const agentVpnEvidenceSchema: z.ZodObject<{
|
|
207
|
+
v: z.ZodLiteral<1>;
|
|
208
|
+
buildId: z.ZodString;
|
|
209
|
+
agent: z.ZodObject<{
|
|
210
|
+
version: z.ZodEffects<z.ZodString, string, string>;
|
|
211
|
+
os: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
212
|
+
arch: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
213
|
+
}, "strip", z.ZodTypeAny, {
|
|
214
|
+
version: string;
|
|
215
|
+
os?: string | undefined;
|
|
216
|
+
arch?: string | undefined;
|
|
217
|
+
}, {
|
|
218
|
+
version: string;
|
|
219
|
+
os?: string | undefined;
|
|
220
|
+
arch?: string | undefined;
|
|
221
|
+
}>;
|
|
222
|
+
/** What the agent believes happened; the control plane's own build row is what the document records. */
|
|
223
|
+
outcome: z.ZodObject<{
|
|
224
|
+
status: z.ZodEnum<["succeeded", "failed"]>;
|
|
225
|
+
failureKind: z.ZodOptional<z.ZodEnum<["user", "infra"]>>;
|
|
226
|
+
}, "strip", z.ZodTypeAny, {
|
|
227
|
+
status: "succeeded" | "failed";
|
|
228
|
+
failureKind?: "user" | "infra" | undefined;
|
|
229
|
+
}, {
|
|
230
|
+
status: "succeeded" | "failed";
|
|
231
|
+
failureKind?: "user" | "infra" | undefined;
|
|
232
|
+
}>;
|
|
233
|
+
helper: z.ZodObject<{
|
|
234
|
+
version: z.ZodNullable<z.ZodEffects<z.ZodString, string, string>>;
|
|
235
|
+
sha256: z.ZodNullable<z.ZodString>;
|
|
236
|
+
arch: z.ZodNullable<z.ZodEnum<["arm64", "amd64"]>>;
|
|
237
|
+
}, "strip", z.ZodTypeAny, {
|
|
238
|
+
sha256: string | null;
|
|
239
|
+
version: string | null;
|
|
240
|
+
arch: "arm64" | "amd64" | null;
|
|
241
|
+
}, {
|
|
242
|
+
sha256: string | null;
|
|
243
|
+
version: string | null;
|
|
244
|
+
arch: "arm64" | "amd64" | null;
|
|
245
|
+
}>;
|
|
246
|
+
egress: z.ZodObject<{
|
|
247
|
+
mode: z.ZodNullable<z.ZodEnum<["standard", "strict"]>>;
|
|
248
|
+
policyHash: z.ZodNullable<z.ZodString>;
|
|
249
|
+
}, "strip", z.ZodTypeAny, {
|
|
250
|
+
mode: "standard" | "strict" | null;
|
|
251
|
+
policyHash: string | null;
|
|
252
|
+
}, {
|
|
253
|
+
mode: "standard" | "strict" | null;
|
|
254
|
+
policyHash: string | null;
|
|
255
|
+
}>;
|
|
256
|
+
tunnel: z.ZodObject<{
|
|
257
|
+
configDigest: z.ZodNullable<z.ZodString>;
|
|
258
|
+
}, "strip", z.ZodTypeAny, {
|
|
259
|
+
configDigest: string | null;
|
|
260
|
+
}, {
|
|
261
|
+
configDigest: string | null;
|
|
262
|
+
}>;
|
|
263
|
+
sandbox: z.ZodNullable<z.ZodObject<{
|
|
264
|
+
profileVersion: z.ZodEffects<z.ZodNumber, number, number>;
|
|
265
|
+
templateSha256: z.ZodString;
|
|
266
|
+
renderedSha256: z.ZodString;
|
|
267
|
+
paramsSha256: z.ZodString;
|
|
268
|
+
loopbackDenyPorts: z.ZodArray<z.ZodEffects<z.ZodNumber, number, number>, "many">;
|
|
269
|
+
orgId: z.ZodEffects<z.ZodString, string, string>;
|
|
270
|
+
selfTest: z.ZodObject<{
|
|
271
|
+
verdict: z.ZodEnum<["pass", "fail", "inconclusive"]>;
|
|
272
|
+
counts: z.ZodEffects<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodNumber, number, number>>, Record<string, number>, Record<string, number>>;
|
|
273
|
+
startedAt: z.ZodEffects<z.ZodString, string, string>;
|
|
274
|
+
durationMs: z.ZodEffects<z.ZodNumber, number, number>;
|
|
275
|
+
results: z.ZodArray<z.ZodObject<{
|
|
276
|
+
id: z.ZodPipeline<z.ZodEffects<z.ZodString, string, string>, z.ZodString>;
|
|
277
|
+
area: z.ZodEffects<z.ZodString, string, string>;
|
|
278
|
+
title: z.ZodEffects<z.ZodString, string, string>;
|
|
279
|
+
tier: z.ZodEnum<["core", "internet", "optional"]>;
|
|
280
|
+
expect: z.ZodEnum<["blocked", "allowed", "known_gap"]>;
|
|
281
|
+
status: z.ZodEnum<["pass", "fail", "inconclusive", "na", "known_gap_open", "known_gap_closed"]>;
|
|
282
|
+
detail: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
283
|
+
}, "strip", z.ZodTypeAny, {
|
|
284
|
+
status: "fail" | "pass" | "inconclusive" | "na" | "known_gap_open" | "known_gap_closed";
|
|
285
|
+
id: string;
|
|
286
|
+
title: string;
|
|
287
|
+
expect: "blocked" | "allowed" | "known_gap";
|
|
288
|
+
area: string;
|
|
289
|
+
tier: "core" | "internet" | "optional";
|
|
290
|
+
detail?: string | undefined;
|
|
291
|
+
}, {
|
|
292
|
+
status: "fail" | "pass" | "inconclusive" | "na" | "known_gap_open" | "known_gap_closed";
|
|
293
|
+
id: string;
|
|
294
|
+
title: string;
|
|
295
|
+
expect: "blocked" | "allowed" | "known_gap";
|
|
296
|
+
area: string;
|
|
297
|
+
tier: "core" | "internet" | "optional";
|
|
298
|
+
detail?: string | undefined;
|
|
299
|
+
}>, "many">;
|
|
300
|
+
}, "strip", z.ZodTypeAny, {
|
|
301
|
+
counts: Record<string, number>;
|
|
302
|
+
verdict: "fail" | "pass" | "inconclusive";
|
|
303
|
+
startedAt: string;
|
|
304
|
+
durationMs: number;
|
|
305
|
+
results: {
|
|
306
|
+
status: "fail" | "pass" | "inconclusive" | "na" | "known_gap_open" | "known_gap_closed";
|
|
307
|
+
id: string;
|
|
308
|
+
title: string;
|
|
309
|
+
expect: "blocked" | "allowed" | "known_gap";
|
|
310
|
+
area: string;
|
|
311
|
+
tier: "core" | "internet" | "optional";
|
|
312
|
+
detail?: string | undefined;
|
|
313
|
+
}[];
|
|
314
|
+
}, {
|
|
315
|
+
counts: Record<string, number>;
|
|
316
|
+
verdict: "fail" | "pass" | "inconclusive";
|
|
317
|
+
startedAt: string;
|
|
318
|
+
durationMs: number;
|
|
319
|
+
results: {
|
|
320
|
+
status: "fail" | "pass" | "inconclusive" | "na" | "known_gap_open" | "known_gap_closed";
|
|
321
|
+
id: string;
|
|
322
|
+
title: string;
|
|
323
|
+
expect: "blocked" | "allowed" | "known_gap";
|
|
324
|
+
area: string;
|
|
325
|
+
tier: "core" | "internet" | "optional";
|
|
326
|
+
detail?: string | undefined;
|
|
327
|
+
}[];
|
|
328
|
+
}>;
|
|
329
|
+
scope: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
330
|
+
}, "strip", z.ZodTypeAny, {
|
|
331
|
+
orgId: string;
|
|
332
|
+
profileVersion: number;
|
|
333
|
+
templateSha256: string;
|
|
334
|
+
renderedSha256: string;
|
|
335
|
+
paramsSha256: string;
|
|
336
|
+
loopbackDenyPorts: number[];
|
|
337
|
+
selfTest: {
|
|
338
|
+
counts: Record<string, number>;
|
|
339
|
+
verdict: "fail" | "pass" | "inconclusive";
|
|
340
|
+
startedAt: string;
|
|
341
|
+
durationMs: number;
|
|
342
|
+
results: {
|
|
343
|
+
status: "fail" | "pass" | "inconclusive" | "na" | "known_gap_open" | "known_gap_closed";
|
|
344
|
+
id: string;
|
|
345
|
+
title: string;
|
|
346
|
+
expect: "blocked" | "allowed" | "known_gap";
|
|
347
|
+
area: string;
|
|
348
|
+
tier: "core" | "internet" | "optional";
|
|
349
|
+
detail?: string | undefined;
|
|
350
|
+
}[];
|
|
351
|
+
};
|
|
352
|
+
scope?: string | undefined;
|
|
353
|
+
}, {
|
|
354
|
+
orgId: string;
|
|
355
|
+
profileVersion: number;
|
|
356
|
+
templateSha256: string;
|
|
357
|
+
renderedSha256: string;
|
|
358
|
+
paramsSha256: string;
|
|
359
|
+
loopbackDenyPorts: number[];
|
|
360
|
+
selfTest: {
|
|
361
|
+
counts: Record<string, number>;
|
|
362
|
+
verdict: "fail" | "pass" | "inconclusive";
|
|
363
|
+
startedAt: string;
|
|
364
|
+
durationMs: number;
|
|
365
|
+
results: {
|
|
366
|
+
status: "fail" | "pass" | "inconclusive" | "na" | "known_gap_open" | "known_gap_closed";
|
|
367
|
+
id: string;
|
|
368
|
+
title: string;
|
|
369
|
+
expect: "blocked" | "allowed" | "known_gap";
|
|
370
|
+
area: string;
|
|
371
|
+
tier: "core" | "internet" | "optional";
|
|
372
|
+
detail?: string | undefined;
|
|
373
|
+
}[];
|
|
374
|
+
};
|
|
375
|
+
scope?: string | undefined;
|
|
376
|
+
}>>;
|
|
377
|
+
hygiene: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
378
|
+
connectionLog: z.ZodNullable<z.ZodObject<{
|
|
379
|
+
/** The raw file reached the control plane (`PUT /agents/vpn/evidence/:buildId/conn-log`). */
|
|
380
|
+
uploaded: z.ZodBoolean;
|
|
381
|
+
omitted: z.ZodOptional<z.ZodEnum<["too_large", "not_kept", "upload_failed", "unreadable"]>>;
|
|
382
|
+
/** sha256 of the file's exact bytes, as the agent computed it. */
|
|
383
|
+
sha256: z.ZodNullable<z.ZodString>;
|
|
384
|
+
bytes: z.ZodEffects<z.ZodNumber, number, number>;
|
|
385
|
+
lines: z.ZodEffects<z.ZodNumber, number, number>;
|
|
386
|
+
complete: z.ZodBoolean;
|
|
387
|
+
incompleteReasons: z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">;
|
|
388
|
+
/** The agent's own summary, compared with what the control plane recomputes from the file. */
|
|
389
|
+
summary: z.ZodNullable<z.ZodObject<{
|
|
390
|
+
total: z.ZodEffects<z.ZodNumber, number, number>;
|
|
391
|
+
allowed: z.ZodEffects<z.ZodNumber, number, number>;
|
|
392
|
+
denied: z.ZodEffects<z.ZodNumber, number, number>;
|
|
393
|
+
connected: z.ZodEffects<z.ZodNumber, number, number>;
|
|
394
|
+
failed: z.ZodEffects<z.ZodNumber, number, number>;
|
|
395
|
+
bytesUp: z.ZodEffects<z.ZodNumber, number, number>;
|
|
396
|
+
bytesDown: z.ZodEffects<z.ZodNumber, number, number>;
|
|
397
|
+
}, "strip", z.ZodTypeAny, {
|
|
398
|
+
failed: number;
|
|
399
|
+
connected: number;
|
|
400
|
+
denied: number;
|
|
401
|
+
allowed: number;
|
|
402
|
+
total: number;
|
|
403
|
+
bytesUp: number;
|
|
404
|
+
bytesDown: number;
|
|
405
|
+
}, {
|
|
406
|
+
failed: number;
|
|
407
|
+
connected: number;
|
|
408
|
+
denied: number;
|
|
409
|
+
allowed: number;
|
|
410
|
+
total: number;
|
|
411
|
+
bytesUp: number;
|
|
412
|
+
bytesDown: number;
|
|
413
|
+
}>>;
|
|
414
|
+
}, "strip", z.ZodTypeAny, {
|
|
415
|
+
lines: number;
|
|
416
|
+
complete: boolean;
|
|
417
|
+
sha256: string | null;
|
|
418
|
+
uploaded: boolean;
|
|
419
|
+
bytes: number;
|
|
420
|
+
incompleteReasons: string[];
|
|
421
|
+
summary: {
|
|
422
|
+
failed: number;
|
|
423
|
+
connected: number;
|
|
424
|
+
denied: number;
|
|
425
|
+
allowed: number;
|
|
426
|
+
total: number;
|
|
427
|
+
bytesUp: number;
|
|
428
|
+
bytesDown: number;
|
|
429
|
+
} | null;
|
|
430
|
+
omitted?: "too_large" | "not_kept" | "upload_failed" | "unreadable" | undefined;
|
|
431
|
+
}, {
|
|
432
|
+
lines: number;
|
|
433
|
+
complete: boolean;
|
|
434
|
+
sha256: string | null;
|
|
435
|
+
uploaded: boolean;
|
|
436
|
+
bytes: number;
|
|
437
|
+
incompleteReasons: string[];
|
|
438
|
+
summary: {
|
|
439
|
+
failed: number;
|
|
440
|
+
connected: number;
|
|
441
|
+
denied: number;
|
|
442
|
+
allowed: number;
|
|
443
|
+
total: number;
|
|
444
|
+
bytesUp: number;
|
|
445
|
+
bytesDown: number;
|
|
446
|
+
} | null;
|
|
447
|
+
omitted?: "too_large" | "not_kept" | "upload_failed" | "unreadable" | undefined;
|
|
448
|
+
}>>;
|
|
449
|
+
}, "strip", z.ZodTypeAny, {
|
|
450
|
+
sandbox: {
|
|
451
|
+
orgId: string;
|
|
452
|
+
profileVersion: number;
|
|
453
|
+
templateSha256: string;
|
|
454
|
+
renderedSha256: string;
|
|
455
|
+
paramsSha256: string;
|
|
456
|
+
loopbackDenyPorts: number[];
|
|
457
|
+
selfTest: {
|
|
458
|
+
counts: Record<string, number>;
|
|
459
|
+
verdict: "fail" | "pass" | "inconclusive";
|
|
460
|
+
startedAt: string;
|
|
461
|
+
durationMs: number;
|
|
462
|
+
results: {
|
|
463
|
+
status: "fail" | "pass" | "inconclusive" | "na" | "known_gap_open" | "known_gap_closed";
|
|
464
|
+
id: string;
|
|
465
|
+
title: string;
|
|
466
|
+
expect: "blocked" | "allowed" | "known_gap";
|
|
467
|
+
area: string;
|
|
468
|
+
tier: "core" | "internet" | "optional";
|
|
469
|
+
detail?: string | undefined;
|
|
470
|
+
}[];
|
|
471
|
+
};
|
|
472
|
+
scope?: string | undefined;
|
|
473
|
+
} | null;
|
|
474
|
+
buildId: string;
|
|
475
|
+
v: 1;
|
|
476
|
+
agent: {
|
|
477
|
+
version: string;
|
|
478
|
+
os?: string | undefined;
|
|
479
|
+
arch?: string | undefined;
|
|
480
|
+
};
|
|
481
|
+
tunnel: {
|
|
482
|
+
configDigest: string | null;
|
|
483
|
+
};
|
|
484
|
+
outcome: {
|
|
485
|
+
status: "succeeded" | "failed";
|
|
486
|
+
failureKind?: "user" | "infra" | undefined;
|
|
487
|
+
};
|
|
488
|
+
helper: {
|
|
489
|
+
sha256: string | null;
|
|
490
|
+
version: string | null;
|
|
491
|
+
arch: "arm64" | "amd64" | null;
|
|
492
|
+
};
|
|
493
|
+
egress: {
|
|
494
|
+
mode: "standard" | "strict" | null;
|
|
495
|
+
policyHash: string | null;
|
|
496
|
+
};
|
|
497
|
+
connectionLog: {
|
|
498
|
+
lines: number;
|
|
499
|
+
complete: boolean;
|
|
500
|
+
sha256: string | null;
|
|
501
|
+
uploaded: boolean;
|
|
502
|
+
bytes: number;
|
|
503
|
+
incompleteReasons: string[];
|
|
504
|
+
summary: {
|
|
505
|
+
failed: number;
|
|
506
|
+
connected: number;
|
|
507
|
+
denied: number;
|
|
508
|
+
allowed: number;
|
|
509
|
+
total: number;
|
|
510
|
+
bytesUp: number;
|
|
511
|
+
bytesDown: number;
|
|
512
|
+
} | null;
|
|
513
|
+
omitted?: "too_large" | "not_kept" | "upload_failed" | "unreadable" | undefined;
|
|
514
|
+
} | null;
|
|
515
|
+
hygiene?: Record<string, unknown> | undefined;
|
|
516
|
+
}, {
|
|
517
|
+
sandbox: {
|
|
518
|
+
orgId: string;
|
|
519
|
+
profileVersion: number;
|
|
520
|
+
templateSha256: string;
|
|
521
|
+
renderedSha256: string;
|
|
522
|
+
paramsSha256: string;
|
|
523
|
+
loopbackDenyPorts: number[];
|
|
524
|
+
selfTest: {
|
|
525
|
+
counts: Record<string, number>;
|
|
526
|
+
verdict: "fail" | "pass" | "inconclusive";
|
|
527
|
+
startedAt: string;
|
|
528
|
+
durationMs: number;
|
|
529
|
+
results: {
|
|
530
|
+
status: "fail" | "pass" | "inconclusive" | "na" | "known_gap_open" | "known_gap_closed";
|
|
531
|
+
id: string;
|
|
532
|
+
title: string;
|
|
533
|
+
expect: "blocked" | "allowed" | "known_gap";
|
|
534
|
+
area: string;
|
|
535
|
+
tier: "core" | "internet" | "optional";
|
|
536
|
+
detail?: string | undefined;
|
|
537
|
+
}[];
|
|
538
|
+
};
|
|
539
|
+
scope?: string | undefined;
|
|
540
|
+
} | null;
|
|
541
|
+
buildId: string;
|
|
542
|
+
v: 1;
|
|
543
|
+
agent: {
|
|
544
|
+
version: string;
|
|
545
|
+
os?: string | undefined;
|
|
546
|
+
arch?: string | undefined;
|
|
547
|
+
};
|
|
548
|
+
tunnel: {
|
|
549
|
+
configDigest: string | null;
|
|
550
|
+
};
|
|
551
|
+
outcome: {
|
|
552
|
+
status: "succeeded" | "failed";
|
|
553
|
+
failureKind?: "user" | "infra" | undefined;
|
|
554
|
+
};
|
|
555
|
+
helper: {
|
|
556
|
+
sha256: string | null;
|
|
557
|
+
version: string | null;
|
|
558
|
+
arch: "arm64" | "amd64" | null;
|
|
559
|
+
};
|
|
560
|
+
egress: {
|
|
561
|
+
mode: "standard" | "strict" | null;
|
|
562
|
+
policyHash: string | null;
|
|
563
|
+
};
|
|
564
|
+
connectionLog: {
|
|
565
|
+
lines: number;
|
|
566
|
+
complete: boolean;
|
|
567
|
+
sha256: string | null;
|
|
568
|
+
uploaded: boolean;
|
|
569
|
+
bytes: number;
|
|
570
|
+
incompleteReasons: string[];
|
|
571
|
+
summary: {
|
|
572
|
+
failed: number;
|
|
573
|
+
connected: number;
|
|
574
|
+
denied: number;
|
|
575
|
+
allowed: number;
|
|
576
|
+
total: number;
|
|
577
|
+
bytesUp: number;
|
|
578
|
+
bytesDown: number;
|
|
579
|
+
} | null;
|
|
580
|
+
omitted?: "too_large" | "not_kept" | "upload_failed" | "unreadable" | undefined;
|
|
581
|
+
} | null;
|
|
582
|
+
hygiene?: Record<string, unknown> | undefined;
|
|
583
|
+
}>;
|
|
584
|
+
export type AgentVpnEvidence = z.infer<typeof agentVpnEvidenceSchema>;
|
|
585
|
+
export interface ConnectionRecord {
|
|
586
|
+
/** 1-based position among this build's attributed records in the file. */
|
|
587
|
+
n: number;
|
|
588
|
+
/** The helper's own per-build record number (a gap in these is a lost record). */
|
|
589
|
+
bseq: number;
|
|
590
|
+
ts: string;
|
|
591
|
+
/** `host:port`, `[v6]:port` for an IPv6 literal. */
|
|
592
|
+
dest: string;
|
|
593
|
+
host: string;
|
|
594
|
+
port: number;
|
|
595
|
+
proto: string;
|
|
596
|
+
route: string;
|
|
597
|
+
decision: "allow" | "deny";
|
|
598
|
+
reason: string;
|
|
599
|
+
connected: boolean;
|
|
600
|
+
bytesUp: number;
|
|
601
|
+
bytesDown: number;
|
|
602
|
+
durationMs: number;
|
|
603
|
+
class?: string;
|
|
604
|
+
failure?: string;
|
|
605
|
+
}
|
|
606
|
+
export interface DestinationSummary {
|
|
607
|
+
dest: string;
|
|
608
|
+
route: string;
|
|
609
|
+
decision: string;
|
|
610
|
+
conns: number;
|
|
611
|
+
bytesUp: number;
|
|
612
|
+
bytesDown: number;
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Per-(destination, route, decision) totals, sorted by `dest + route + decision` in UTF-16 code unit order.
|
|
616
|
+
* `scripts/appforge-verify.mjs` recomputes exactly this from a bundle's raw records, so the two must agree to
|
|
617
|
+
* the byte; the order is deliberately locale-independent (a collation order differs between ICU locales and
|
|
618
|
+
* would make one verifier disagree with another about the same evidence).
|
|
619
|
+
*/
|
|
620
|
+
export declare function summariseConnections(records: readonly {
|
|
621
|
+
dest: string;
|
|
622
|
+
route: string;
|
|
623
|
+
decision: string;
|
|
624
|
+
bytesUp?: number;
|
|
625
|
+
bytesDown?: number;
|
|
626
|
+
}[]): DestinationSummary[];
|
|
627
|
+
/** The digest the document's `connections.logSha256` holds: sha256 over the canonical records, one per line, newline-terminated. */
|
|
628
|
+
export declare function connectionRecordsDigest(records: readonly ConnectionRecord[]): Promise<string>;
|
|
629
|
+
/** The helper's own tally of a build (its `summary` line). */
|
|
630
|
+
export interface HelperTally {
|
|
631
|
+
reason: string;
|
|
632
|
+
conns: number;
|
|
633
|
+
allowed: number;
|
|
634
|
+
denied: number;
|
|
635
|
+
connected: number;
|
|
636
|
+
failed: number;
|
|
637
|
+
bytesUp: number;
|
|
638
|
+
bytesDown: number;
|
|
639
|
+
dropped: number;
|
|
640
|
+
forcedClosed: number;
|
|
641
|
+
stream: boolean;
|
|
642
|
+
}
|
|
643
|
+
export interface ConnectionLogAnalysis {
|
|
644
|
+
records: ConnectionRecord[];
|
|
645
|
+
destinations: DestinationSummary[];
|
|
646
|
+
/** Groups beyond ATTESTATION_MAX_DESTINATIONS (0 = the list is complete). */
|
|
647
|
+
destinationsOmitted: number;
|
|
648
|
+
/** `errors` counts allowed requests whose dial failed. */
|
|
649
|
+
totals: {
|
|
650
|
+
allowed: number;
|
|
651
|
+
denied: number;
|
|
652
|
+
errors: number;
|
|
653
|
+
bytesUp: number;
|
|
654
|
+
bytesDown: number;
|
|
655
|
+
};
|
|
656
|
+
connected: number;
|
|
657
|
+
logRecords: number;
|
|
658
|
+
/** See `connectionRecordsDigest`. */
|
|
659
|
+
logSha256: string;
|
|
660
|
+
rawLines: number;
|
|
661
|
+
complete: boolean;
|
|
662
|
+
incompleteReasons: string[];
|
|
663
|
+
authFailures: number;
|
|
664
|
+
sessionCredentialRequests: number;
|
|
665
|
+
droppedRecords: number;
|
|
666
|
+
helperTally: HelperTally | null;
|
|
667
|
+
helperTallyAgrees: boolean | null;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Re-derives everything a document says about the connection log from the raw file, so the control plane
|
|
671
|
+
* never relies on the agent's summary. The completeness rules mirror the agent's own aggregator
|
|
672
|
+
* (packages/agent/src/lib/evidence.ts) so that, for a log nothing went wrong with, both say "complete"; a
|
|
673
|
+
* test runs the two on the same files. Trusts nothing in a line: malformed lines are counted, strings are
|
|
674
|
+
* reduced to printable ASCII and clipped, a record of another build is excluded and makes the log incomplete.
|
|
675
|
+
*
|
|
676
|
+
* `raw` is the file's text (UTF-8); callers bound its size (`ATTESTATION_LOG_MAX_BYTES`).
|
|
677
|
+
*/
|
|
678
|
+
export declare function analyzeConnectionLog(raw: string, buildId: string): Promise<ConnectionLogAnalysis>;
|
|
679
|
+
/**
|
|
680
|
+
* The tunnel configuration both sides can describe without any key material: the control plane from the lease
|
|
681
|
+
* it built, the agent from the job it received. A key-free digest on purpose: the agent's own session
|
|
682
|
+
* fingerprint (vpn-session.ts) is over the whole job, private key included, and must never be published.
|
|
683
|
+
*/
|
|
684
|
+
export type TunnelDigestInput = Pick<VpnJobConfig, "tunnelId" | "keyVersion" | "gatewayEndpoint" | "gatewayPublicKey" | "tunnelCidr" | "localAddress" | "gatewayAddress" | "lanCidrs" | "dnsServer" | "allowedDomains" | "allowPrivateEndpoint" | "egress">;
|
|
685
|
+
export declare function vpnPublicConfigDigest(job: TunnelDigestInput): Promise<string>;
|
|
686
|
+
/** What a document says about the tunnel: public configuration only, captured when the build was leased. */
|
|
687
|
+
export interface TunnelFacts {
|
|
688
|
+
id: string;
|
|
689
|
+
name: string | null;
|
|
690
|
+
keyVersion: number;
|
|
691
|
+
gatewayEndpoint: string;
|
|
692
|
+
lanCidrs: string[];
|
|
693
|
+
tunnelCidr: string;
|
|
694
|
+
dnsServer: string | null;
|
|
695
|
+
allowedDomains: string[];
|
|
696
|
+
/** First 32 hex characters of sha256(raw 32-byte WireGuard public key), so a customer can match AppForge's side of their gateway config. */
|
|
697
|
+
appforgePublicKeyFp: string | null;
|
|
698
|
+
gatewayPublicKeyFp: string | null;
|
|
699
|
+
configDigest: string;
|
|
700
|
+
}
|
|
701
|
+
export declare function buildTunnelFacts(job: TunnelDigestInput & {
|
|
702
|
+
tunnelName?: string;
|
|
703
|
+
}, appforgePublicKey: string | null): Promise<TunnelFacts>;
|
|
704
|
+
/** The egress policy as it is recorded: the helper's canonical expansion (one entry per host, explicit sorted ports). */
|
|
705
|
+
export interface AttestedEgressPolicy {
|
|
706
|
+
v: number;
|
|
707
|
+
allow: {
|
|
708
|
+
host: string;
|
|
709
|
+
ports: number[];
|
|
710
|
+
}[];
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* The policy exactly as the helper enforces it, in the form a document inlines, with the helper's own hash of it.
|
|
714
|
+
* Throws on an invalid policy (callers validated it when they built the lease).
|
|
715
|
+
*/
|
|
716
|
+
export declare function expandLeasedPolicy(policy: VpnEgressPolicy): Promise<{
|
|
717
|
+
policy: AttestedEgressPolicy;
|
|
718
|
+
helperHash: string;
|
|
719
|
+
}>;
|
|
720
|
+
/**
|
|
721
|
+
* What `scripts/r2-release.mjs` publishes next to a release's artifacts (`<prefix>/<version>/manifest.json`):
|
|
722
|
+
* the sha256 of every artifact and a few release-level facts. The control plane compares what an agent reports
|
|
723
|
+
* (the helper binary it ran, the sandbox template it rendered) with these. A release cut before manifests existed
|
|
724
|
+
* has none, which is recorded as "not checked", never as a match.
|
|
725
|
+
*/
|
|
726
|
+
export declare const releaseManifestSchema: z.ZodObject<{
|
|
727
|
+
v: z.ZodLiteral<1>;
|
|
728
|
+
prefix: z.ZodString;
|
|
729
|
+
version: z.ZodString;
|
|
730
|
+
publishedAt: z.ZodString;
|
|
731
|
+
artifacts: z.ZodArray<z.ZodObject<{
|
|
732
|
+
name: z.ZodString;
|
|
733
|
+
sha256: z.ZodString;
|
|
734
|
+
bytes: z.ZodEffects<z.ZodNumber, number, number>;
|
|
735
|
+
}, "strip", z.ZodTypeAny, {
|
|
736
|
+
name: string;
|
|
737
|
+
sha256: string;
|
|
738
|
+
bytes: number;
|
|
739
|
+
}, {
|
|
740
|
+
name: string;
|
|
741
|
+
sha256: string;
|
|
742
|
+
bytes: number;
|
|
743
|
+
}>, "many">;
|
|
744
|
+
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
745
|
+
}, "strip", z.ZodTypeAny, {
|
|
746
|
+
v: 1;
|
|
747
|
+
artifacts: {
|
|
748
|
+
name: string;
|
|
749
|
+
sha256: string;
|
|
750
|
+
bytes: number;
|
|
751
|
+
}[];
|
|
752
|
+
prefix: string;
|
|
753
|
+
version: string;
|
|
754
|
+
publishedAt: string;
|
|
755
|
+
meta?: Record<string, string> | undefined;
|
|
756
|
+
}, {
|
|
757
|
+
v: 1;
|
|
758
|
+
artifacts: {
|
|
759
|
+
name: string;
|
|
760
|
+
sha256: string;
|
|
761
|
+
bytes: number;
|
|
762
|
+
}[];
|
|
763
|
+
prefix: string;
|
|
764
|
+
version: string;
|
|
765
|
+
publishedAt: string;
|
|
766
|
+
meta?: Record<string, string> | undefined;
|
|
767
|
+
}>;
|
|
768
|
+
export type ReleaseManifest = z.infer<typeof releaseManifestSchema>;
|
|
769
|
+
export declare function parseReleaseManifest(textBody: string): ReleaseManifest | null;
|
|
770
|
+
/** The helper artifact name `scripts/r2-release.mjs` publishes for an architecture. */
|
|
771
|
+
export declare const helperArtifactName: (arch: "arm64" | "amd64") => string;
|
|
772
|
+
export type AttestationCompleteness = "full" | "partial" | "server_observed_only";
|
|
773
|
+
export type ControlsVerdict = "all_enforced" | "violations_detected" | "not_evaluated" | "network_policy_checked";
|
|
774
|
+
export type CrossCheckStatus = "match" | "mismatch" | "not_checked";
|
|
775
|
+
export interface ControlTestResult {
|
|
776
|
+
id: string;
|
|
777
|
+
area: string;
|
|
778
|
+
title: string;
|
|
779
|
+
expect: string;
|
|
780
|
+
verdict: "pass" | "fail" | "inconclusive";
|
|
781
|
+
reason: string;
|
|
782
|
+
/** Who produced the result: the on-host matrix the agent ran, or a comparison the control plane made. */
|
|
783
|
+
source: "agent-selftest" | "server-crosscheck";
|
|
784
|
+
}
|
|
785
|
+
export interface CrossCheck {
|
|
786
|
+
id: string;
|
|
787
|
+
status: CrossCheckStatus;
|
|
788
|
+
detail: string;
|
|
789
|
+
}
|
|
790
|
+
/** Everything the control plane knows about a build without asking the agent. Times are ISO-8601 UTC, whole seconds. */
|
|
791
|
+
export interface AttestationServerFacts {
|
|
792
|
+
orgId: string;
|
|
793
|
+
appId: string;
|
|
794
|
+
buildId: string;
|
|
795
|
+
workflow: string;
|
|
796
|
+
branch: string;
|
|
797
|
+
commitSha: string | null;
|
|
798
|
+
tunnelId: string | null;
|
|
799
|
+
/** 1 = strict egress, 2 = regulated (Build.vpnClass at lease time). */
|
|
800
|
+
vpnClass: 1 | 2;
|
|
801
|
+
build: {
|
|
802
|
+
status: string;
|
|
803
|
+
failureKind: string | null;
|
|
804
|
+
queuedAt: string | null;
|
|
805
|
+
startedAt: string | null;
|
|
806
|
+
finishedAt: string | null;
|
|
807
|
+
};
|
|
808
|
+
/** The Mac that leased the build, as the control plane has it (from its own agent row). */
|
|
809
|
+
agent: {
|
|
810
|
+
id: string | null;
|
|
811
|
+
version: string | null;
|
|
812
|
+
os: string | null;
|
|
813
|
+
};
|
|
814
|
+
/** What the lease carried, recorded when the build was leased. Null when there is no such record. */
|
|
815
|
+
lease: {
|
|
816
|
+
leasedAt: string | null;
|
|
817
|
+
/** The expanded egress policy exactly as sent to the helper (helper canonical form). */
|
|
818
|
+
policy: AttestedEgressPolicy | null;
|
|
819
|
+
/** The helper's canonical-form hash of `policy`. */
|
|
820
|
+
policyHash: string | null;
|
|
821
|
+
tunnel: TunnelFacts | null;
|
|
822
|
+
} | null;
|
|
823
|
+
/** The build's VpnEvent rows, oldest first. */
|
|
824
|
+
events: {
|
|
825
|
+
type: string;
|
|
826
|
+
at: string;
|
|
827
|
+
}[];
|
|
828
|
+
chain: {
|
|
829
|
+
headSeq: number;
|
|
830
|
+
headHash: string;
|
|
831
|
+
firstEventSeq: number | null;
|
|
832
|
+
};
|
|
833
|
+
issuedAt: string;
|
|
834
|
+
manifests: {
|
|
835
|
+
vpnRelease: ReleaseManifest | null;
|
|
836
|
+
agentRelease: ReleaseManifest | null;
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
export interface BuiltAttestation {
|
|
840
|
+
doc: AttestationDocument;
|
|
841
|
+
completeness: AttestationCompleteness;
|
|
842
|
+
controlsVerdict: ControlsVerdict;
|
|
843
|
+
/** Failing control-test results, agent-side and server-side. */
|
|
844
|
+
violations: number;
|
|
845
|
+
gaps: string[];
|
|
846
|
+
}
|
|
847
|
+
export interface AttestationDocument {
|
|
848
|
+
type: typeof ATTESTATION_DOC_TYPE;
|
|
849
|
+
version: number;
|
|
850
|
+
id: string;
|
|
851
|
+
issuedAt: string;
|
|
852
|
+
completeness: AttestationCompleteness;
|
|
853
|
+
subject: {
|
|
854
|
+
orgId: string;
|
|
855
|
+
appId: string;
|
|
856
|
+
buildId: string;
|
|
857
|
+
workflow: string;
|
|
858
|
+
branch: string;
|
|
859
|
+
commitSha: string | null;
|
|
860
|
+
tunnelId: string | null;
|
|
861
|
+
};
|
|
862
|
+
host: {
|
|
863
|
+
agentId: string | null;
|
|
864
|
+
agentVersion: string | null;
|
|
865
|
+
os: string | null;
|
|
866
|
+
arch: string | null;
|
|
867
|
+
};
|
|
868
|
+
helper: Record<string, unknown>;
|
|
869
|
+
tunnel: Record<string, unknown> | null;
|
|
870
|
+
policy: {
|
|
871
|
+
mode: string;
|
|
872
|
+
egressPolicy?: AttestedEgressPolicy;
|
|
873
|
+
egressPolicySha256?: string;
|
|
874
|
+
helperPolicyHash: string | null;
|
|
875
|
+
enforcement: Record<string, unknown> | null;
|
|
876
|
+
};
|
|
877
|
+
timing: Record<string, string | number | null>;
|
|
878
|
+
isolation: Record<string, unknown>;
|
|
879
|
+
connections: Record<string, unknown> & {
|
|
880
|
+
destinations?: DestinationSummary[];
|
|
881
|
+
logSha256?: string;
|
|
882
|
+
};
|
|
883
|
+
controlTests?: {
|
|
884
|
+
packVersion: string;
|
|
885
|
+
phase: string;
|
|
886
|
+
results: ControlTestResult[];
|
|
887
|
+
summary: {
|
|
888
|
+
pass: number;
|
|
889
|
+
fail: number;
|
|
890
|
+
inconclusive: number;
|
|
891
|
+
};
|
|
892
|
+
resultsSha256: string;
|
|
893
|
+
[k: string]: unknown;
|
|
894
|
+
};
|
|
895
|
+
crossChecks: CrossCheck[];
|
|
896
|
+
gaps: string[];
|
|
897
|
+
scope: {
|
|
898
|
+
statement: string;
|
|
899
|
+
limits: string[];
|
|
900
|
+
};
|
|
901
|
+
provenance: Record<string, string[]>;
|
|
902
|
+
auditChain: {
|
|
903
|
+
headSeq: number;
|
|
904
|
+
headHash: string;
|
|
905
|
+
firstEventSeq: number | null;
|
|
906
|
+
};
|
|
907
|
+
result: {
|
|
908
|
+
buildStatus: string;
|
|
909
|
+
failureKind: string | null;
|
|
910
|
+
agentReportedStatus: string | null;
|
|
911
|
+
controlsVerdict: ControlsVerdict;
|
|
912
|
+
violations: number;
|
|
913
|
+
};
|
|
914
|
+
[k: string]: unknown;
|
|
915
|
+
}
|
|
916
|
+
/** The statements every document carries, so a reader never has to guess what it does not claim. */
|
|
917
|
+
export declare const ATTESTATION_LIMITS: readonly string[];
|
|
918
|
+
export declare const ATTESTATION_PROVENANCE: Readonly<Record<string, string[]>>;
|
|
919
|
+
export declare function summariseControls(results: readonly {
|
|
920
|
+
verdict: string;
|
|
921
|
+
}[]): {
|
|
922
|
+
pass: number;
|
|
923
|
+
fail: number;
|
|
924
|
+
inconclusive: number;
|
|
925
|
+
};
|
|
926
|
+
/**
|
|
927
|
+
* A canonical-JSON-safe copy of an object whose shape this module does not own (the agent's hygiene evidence), or null when it is
|
|
928
|
+
* too large or too deep to carry. Numbers that are not safe integers are rounded and lone surrogates replaced (`normalized` says
|
|
929
|
+
* so): the evidence is kept, not dropped, because it cannot be hashed identically everywhere as sent.
|
|
930
|
+
*/
|
|
931
|
+
export declare function sanitizeOpaque(value: unknown, maxBytes: number): {
|
|
932
|
+
value: Record<string, unknown>;
|
|
933
|
+
normalized: boolean;
|
|
934
|
+
} | null;
|
|
935
|
+
export interface BuildAttestationInput {
|
|
936
|
+
facts: AttestationServerFacts;
|
|
937
|
+
/** The agent's report; null when none arrived (the degraded, server-observed document). */
|
|
938
|
+
evidence: AgentVpnEvidence | null;
|
|
939
|
+
/** The control plane's own analysis of the raw log it holds; null when it holds none. */
|
|
940
|
+
log: ConnectionLogAnalysis | null;
|
|
941
|
+
/** The raw object the control plane holds: its digest (verified by the bucket at upload) and size. */
|
|
942
|
+
rawLog: {
|
|
943
|
+
sha256: string;
|
|
944
|
+
bytes: number;
|
|
945
|
+
} | null;
|
|
946
|
+
/** Why this is a degraded document (no agent evidence), in words. */
|
|
947
|
+
degradedReason?: string;
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Assembles the attestation document for a build, in exactly the shape `scripts/appforge-verify.mjs` reads, and
|
|
951
|
+
* decides its completeness and verdict. Every comparison between a thing the agent claims and a thing the
|
|
952
|
+
* control plane knows becomes a `crossChecks` entry; a mismatch is also a failing control-test result, which
|
|
953
|
+
* makes the verifier exit 2. Throws only on a programming error (a value canonical JSON cannot represent).
|
|
954
|
+
*/
|
|
955
|
+
export declare function buildAttestationDocument(input: BuildAttestationInput): Promise<BuiltAttestation>;
|
|
956
|
+
export interface BundleChainEntry {
|
|
957
|
+
seq: number;
|
|
958
|
+
prevHash: string;
|
|
959
|
+
hash: string;
|
|
960
|
+
kind: string;
|
|
961
|
+
refId: string | null;
|
|
962
|
+
payload: unknown;
|
|
963
|
+
}
|
|
964
|
+
export interface BundleCheckpoint {
|
|
965
|
+
orgId: string;
|
|
966
|
+
seq: number;
|
|
967
|
+
hash: string;
|
|
968
|
+
issuedAt: string;
|
|
969
|
+
keyId: string;
|
|
970
|
+
signature: string;
|
|
971
|
+
}
|
|
972
|
+
export interface EvidenceBundle {
|
|
973
|
+
format: typeof EVIDENCE_BUNDLE_FORMAT;
|
|
974
|
+
version: 1;
|
|
975
|
+
attestation: {
|
|
976
|
+
doc: AttestationDocument;
|
|
977
|
+
signatures: AttestationSignature[];
|
|
978
|
+
};
|
|
979
|
+
auditChain: {
|
|
980
|
+
orgId: string;
|
|
981
|
+
anchor: {
|
|
982
|
+
seq: number;
|
|
983
|
+
hash: string;
|
|
984
|
+
} | null;
|
|
985
|
+
entries: BundleChainEntry[];
|
|
986
|
+
checkpoints: BundleCheckpoint[];
|
|
987
|
+
};
|
|
988
|
+
connectionLog?: {
|
|
989
|
+
encoding: "records";
|
|
990
|
+
normalization: string;
|
|
991
|
+
records: ConnectionRecord[];
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
/** sha256 of the canonical document: what the chain's registration entry holds as `docSha256`. */
|
|
995
|
+
export declare function attestationDocumentSha256(doc: unknown): Promise<string>;
|
|
996
|
+
/**
|
|
997
|
+
* The printable page for a bundle. A TWIN of `renderEvidenceHtml` in scripts/appforge-verify.mjs, which cannot
|
|
998
|
+
* be imported here (it is a zero-dependency single file built on node:crypto, and these Workers do not enable
|
|
999
|
+
* Node compatibility): `--render-check` re-renders the visible page from its embedded JSON with that function
|
|
1000
|
+
* and requires a byte-for-byte match, so this one must produce identical output, and packages/core's tests
|
|
1001
|
+
* require it on many bundles. Only the JSON embedded in the page is authenticated; the visible text is a
|
|
1002
|
+
* rendering of it.
|
|
1003
|
+
*/
|
|
1004
|
+
export declare function renderEvidenceHtml(bundle: EvidenceBundle, { docSha256 }: {
|
|
1005
|
+
docSha256: string;
|
|
1006
|
+
}): string;
|
|
1007
|
+
/**
|
|
1008
|
+
* The bundle and, when asked, its printable page, from an attestation's stored parts. The page is rendered from a
|
|
1009
|
+
* JSON round trip of the bundle, so the bytes the verifier re-renders from the embedded JSON are the bytes written
|
|
1010
|
+
* here (JS reorders integer-like object keys on a parse, and the round trip applies that once, up front).
|
|
1011
|
+
*/
|
|
1012
|
+
export declare function renderEvidencePage(bundle: EvidenceBundle): Promise<string>;
|
|
1013
|
+
export declare function isoNowSeconds(now?: Date): string;
|
|
1014
|
+
export {};
|
|
1015
|
+
//# sourceMappingURL=vpn-attestation.d.ts.map
|