@celilo/e2e 0.11.0 → 0.11.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.
Files changed (45) hide show
  1. package/npm-registry-server/src/packument.test.ts +2 -5
  2. package/npm-registry-server/src/packument.ts +3 -3
  3. package/npm-registry-server/src/server.ts +1 -1
  4. package/package.json +3 -3
  5. package/registry-server/src/bootstrap.ts +1 -1
  6. package/registry-server/src/introspection.test.ts +8 -12
  7. package/registry-server/src/server.test.ts +2 -2
  8. package/registry-server/src/server.ts +1 -1
  9. package/registry-server/src/storage.test.ts +8 -4
  10. package/registry-server/src/storage.ts +1 -1
  11. package/registry-server/src/validation.ts +2 -4
  12. package/scripts/pack-celilo-packages.ts +1 -4
  13. package/scripts/stage-apt-repo.ts +1 -1
  14. package/scripts/stage-libsignal.ts +1 -3
  15. package/simulators/greenwave/server.ts +3 -1
  16. package/simulators/greenwave/state.ts +17 -10
  17. package/simulators/isitup/server.ts +1 -5
  18. package/simulators/namecheap-ddns/server.ts +2 -4
  19. package/simulators/namecheap-ddns/zone-updater.test.ts +3 -1
  20. package/simulators/namecheap-ddns/zone-updater.ts +6 -10
  21. package/simulators/signal-cli/server.ts +3 -1
  22. package/src/browser.ts +3 -3
  23. package/src/bus-events.ts +2 -2
  24. package/src/cli/command-tree-parser.ts +8 -2
  25. package/src/cli/completion.ts +5 -8
  26. package/src/cli/index.ts +9 -2
  27. package/src/cli/module-discovery.ts +3 -1
  28. package/src/cli-version-contract.ts +5 -6
  29. package/src/extract-failure.ts +92 -0
  30. package/src/fixtures.ts +4 -11
  31. package/src/network-builder.ts +12 -7
  32. package/src/observer.test.ts +42 -11
  33. package/src/observer.ts +12 -5
  34. package/src/registry-bundle.ts +1 -3
  35. package/src/run-lock.test.ts +2 -2
  36. package/src/run-lock.ts +19 -5
  37. package/src/runner.ts +9 -67
  38. package/src/shared-infra.ts +50 -22
  39. package/src/socks-proxy.ts +14 -17
  40. package/src/vantage-helpers.ts +5 -1
  41. package/src/vantage.test.ts +78 -24
  42. package/src/vantage.ts +24 -17
  43. package/src/wait-for-run.ts +1 -1
  44. package/src/zone-classifier.test.ts +7 -1
  45. package/src/zone-classifier.ts +2 -2
@@ -11,9 +11,13 @@
11
11
  */
12
12
 
13
13
  import { execSync } from 'node:child_process';
14
- import { join } from 'node:path';
15
14
  import { copyFileSync, existsSync, rmSync, statSync, writeFileSync } from 'node:fs';
16
- import { generateSharedInfraYaml, SHARED_PROJECT_NAME, SHARED_NETWORKS } from './docker-compose-generator';
15
+ import { join } from 'node:path';
16
+ import {
17
+ SHARED_NETWORKS,
18
+ SHARED_PROJECT_NAME,
19
+ generateSharedInfraYaml,
20
+ } from './docker-compose-generator';
17
21
  import { ensureRegistryServerBundle } from './registry-bundle';
18
22
 
19
23
  const SHARED_COMPOSE_FILE = 'docker-compose.shared.yml';
@@ -37,8 +41,10 @@ function run(cmd: string, opts?: { cwd?: string; timeout?: number }): string {
37
41
  */
38
42
  function nukeE2eResources(e2eDir: string): void {
39
43
  try {
40
- run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} down --volumes --remove-orphans`,
41
- { cwd: e2eDir, timeout: 30_000 });
44
+ run(
45
+ `docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} down --volumes --remove-orphans`,
46
+ { cwd: e2eDir, timeout: 30_000 },
47
+ );
42
48
  } catch {}
43
49
  try {
44
50
  const ids = run('docker ps -aq --filter name=celilo-e2e', { timeout: 10_000 });
@@ -49,11 +55,17 @@ function nukeE2eResources(e2eDir: string): void {
49
55
  .split('\n')
50
56
  .filter((n) => n.startsWith('celilo-e2e'));
51
57
  for (const net of nets) {
52
- try { run(`docker network rm ${net}`, { timeout: 5_000 }); } catch {}
58
+ try {
59
+ run(`docker network rm ${net}`, { timeout: 5_000 });
60
+ } catch {}
53
61
  }
54
62
  } catch {}
55
- try { run('docker network prune -f', { timeout: 10_000 }); } catch {}
56
- try { run('docker volume prune -f', { timeout: 10_000 }); } catch {}
63
+ try {
64
+ run('docker network prune -f', { timeout: 10_000 });
65
+ } catch {}
66
+ try {
67
+ run('docker volume prune -f', { timeout: 10_000 });
68
+ } catch {}
57
69
  }
58
70
 
59
71
  /**
@@ -117,7 +129,9 @@ export async function ensureSharedInfra(): Promise<void> {
117
129
  }
118
130
  } catch {
119
131
  // DNS check failed — restart shared infra
120
- console.log('[progress:start] shared infra DNS check failed, restarting | shared infra restarted');
132
+ console.log(
133
+ '[progress:start] shared infra DNS check failed, restarting | shared infra restarted',
134
+ );
121
135
  await stopSharedInfra();
122
136
  }
123
137
  }
@@ -130,7 +144,9 @@ export async function ensureSharedInfra(): Promise<void> {
130
144
  // This self-heals orphans left by a crashed / cross-path / cross-checkout
131
145
  // prior run (e.g. a botched-mount container that wedges the next `up`),
132
146
  // which the old compose-project-scoped `down` couldn't see (#212).
133
- console.log('[progress:start] cleaning up stale shared infrastructure | shared infra cleanup complete');
147
+ console.log(
148
+ '[progress:start] cleaning up stale shared infrastructure | shared infra cleanup complete',
149
+ );
134
150
  nukeE2eResources(e2eDir);
135
151
 
136
152
  // Refresh the bundled registry-server source so Dockerfile.registry's
@@ -161,12 +177,16 @@ export async function ensureSharedInfra(): Promise<void> {
161
177
 
162
178
  // Build and start
163
179
  console.log('[progress:start] building shared infrastructure images | shared images built');
164
- run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} build`,
165
- { cwd: e2eDir, timeout: 300_000 });
180
+ run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} build`, {
181
+ cwd: e2eDir,
182
+ timeout: 300_000,
183
+ });
166
184
 
167
185
  console.log('[progress:start] starting shared infrastructure | shared infra started');
168
- run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} up -d`,
169
- { cwd: e2eDir, timeout: 120_000 });
186
+ run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} up -d`, {
187
+ cwd: e2eDir,
188
+ timeout: 120_000,
189
+ });
170
190
 
171
191
  // Wait for DNS convergence — robustly. The old single 60s window with no
172
192
  // recovery was an intermittent build-infra failure on the builder (#319:
@@ -196,23 +216,29 @@ export async function ensureSharedInfra(): Promise<void> {
196
216
  if (Date.now() - lastRestart > 45_000) {
197
217
  console.log('[progress:sub] DNS not converged yet — restarting namecheap-dns');
198
218
  try {
199
- run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} restart namecheap-dns`,
200
- { cwd: e2eDir, timeout: 30_000 });
219
+ run(
220
+ `docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} restart namecheap-dns`,
221
+ { cwd: e2eDir, timeout: 30_000 },
222
+ );
201
223
  } catch {}
202
224
  lastRestart = Date.now();
203
225
  }
204
- await new Promise(r => setTimeout(r, 2000));
226
+ await new Promise((r) => setTimeout(r, 2000));
205
227
  }
206
228
  // Diagnostics before failing, so the log shows WHY it didn't converge.
207
229
  let psOut = '';
208
230
  try {
209
- psOut = run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} ps`,
210
- { cwd: e2eDir, timeout: 10_000 });
231
+ psOut = run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} ps`, {
232
+ cwd: e2eDir,
233
+ timeout: 10_000,
234
+ });
211
235
  } catch {}
212
236
  let dnsLog = '';
213
237
  try {
214
- dnsLog = run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} logs --tail 40 namecheap-dns`,
215
- { cwd: e2eDir, timeout: 10_000 });
238
+ dnsLog = run(
239
+ `docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} logs --tail 40 namecheap-dns`,
240
+ { cwd: e2eDir, timeout: 10_000 },
241
+ );
216
242
  } catch {}
217
243
  throw new Error(
218
244
  `Timeout waiting for shared infrastructure DNS convergence (${dnsTimeout / 1000}s)\n` +
@@ -227,8 +253,10 @@ export async function stopSharedInfra(): Promise<void> {
227
253
  console.log('[progress:start] stopping shared infrastructure | shared infra stopped');
228
254
  const e2eDir = getE2eDir();
229
255
  try {
230
- run(`docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} down --volumes --remove-orphans`,
231
- { cwd: e2eDir, timeout: 60_000 });
256
+ run(
257
+ `docker compose -f ${SHARED_COMPOSE_FILE} -p ${SHARED_PROJECT_NAME} down --volumes --remove-orphans`,
258
+ { cwd: e2eDir, timeout: 60_000 },
259
+ );
232
260
  } catch {
233
261
  // Best effort
234
262
  }
@@ -165,8 +165,7 @@ async function waitForTcpAccept(port: number, timeoutMs = 10_000): Promise<void>
165
165
  await new Promise((r) => setTimeout(r, 100));
166
166
  }
167
167
  throw new Error(
168
- `SOCKS proxy on 127.0.0.1:${port} never accepted connections within ${timeoutMs}ms` +
169
- (lastErr ? ` (last error: ${lastErr})` : ''),
168
+ `SOCKS proxy on 127.0.0.1:${port} never accepted connections within ${timeoutMs}ms${lastErr ? ` (last error: ${lastErr})` : ''}`,
170
169
  );
171
170
  }
172
171
 
@@ -233,8 +232,7 @@ function ensureProxyImage(): void {
233
232
  const dockerfilePath = join(PACKAGE_ROOT, PROXY_DOCKERFILE);
234
233
  if (!existsSync(dockerfilePath)) {
235
234
  throw new Error(
236
- `SOCKS proxy Dockerfile not found at ${dockerfilePath}. ` +
237
- `Expected it to ship with the @celilo/e2e package.`,
235
+ `SOCKS proxy Dockerfile not found at ${dockerfilePath}. Expected it to ship with the @celilo/e2e package.`,
238
236
  );
239
237
  }
240
238
 
@@ -247,18 +245,17 @@ function ensureProxyImage(): void {
247
245
  });
248
246
  } catch (err) {
249
247
  const msg = err instanceof Error ? err.message : String(err);
250
- throw new Error(
251
- `Failed to build ${PROXY_IMAGE} from ${PROXY_DOCKERFILE}: ${msg}`,
252
- );
248
+ throw new Error(`Failed to build ${PROXY_IMAGE} from ${PROXY_DOCKERFILE}: ${msg}`);
253
249
  }
254
250
  }
255
251
 
256
252
  function inspectContainer(name: string): boolean {
257
253
  try {
258
- const result = execSync(
259
- `docker inspect -f '{{.State.Running}}' ${name} 2>/dev/null`,
260
- { timeout: 5_000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
261
- );
254
+ const result = execSync(`docker inspect -f '{{.State.Running}}' ${name} 2>/dev/null`, {
255
+ timeout: 5_000,
256
+ encoding: 'utf-8',
257
+ stdio: ['pipe', 'pipe', 'pipe'],
258
+ });
262
259
  return result.trim() === 'true';
263
260
  } catch {
264
261
  return false;
@@ -276,15 +273,15 @@ async function waitForContainerListening(name: string): Promise<void> {
276
273
  let lastInspect = '';
277
274
  while (Date.now() < deadline) {
278
275
  try {
279
- lastInspect = execSync(
280
- `docker inspect -f '{{.State.Status}} {{.State.ExitCode}}' ${name}`,
281
- { timeout: 3_000, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
282
- ).trim();
276
+ lastInspect = execSync(`docker inspect -f '{{.State.Status}} {{.State.ExitCode}}' ${name}`, {
277
+ timeout: 3_000,
278
+ encoding: 'utf-8',
279
+ stdio: ['pipe', 'pipe', 'pipe'],
280
+ }).trim();
283
281
  } catch {
284
282
  // Container may have already self-removed (--rm) after exiting
285
283
  throw new Error(
286
- `SOCKS proxy container '${name}' disappeared during startup. ` +
287
- `It likely exited immediately. Check the image's required env vars.`,
284
+ `SOCKS proxy container '${name}' disappeared during startup. It likely exited immediately. Check the image's required env vars.`,
288
285
  );
289
286
  }
290
287
  const [status] = lastInspect.split(/\s+/);
@@ -52,7 +52,11 @@ export async function assertFrontedServicePositives(
52
52
  const url = `https://${expectation.fqdn}${expectation.path ?? ''}`;
53
53
 
54
54
  // The internet sees the service at a public IP and a valid HTTPS endpoint.
55
- await probe.assertResolvesInZone({ from: 'publicInternet', name: expectation.fqdn, zone: 'public' });
55
+ await probe.assertResolvesInZone({
56
+ from: 'publicInternet',
57
+ name: expectation.fqdn,
58
+ zone: 'public',
59
+ });
56
60
  await probe.assertHttps({
57
61
  from: 'publicInternet',
58
62
  url,
@@ -36,7 +36,9 @@ const LAN: Vantage = 'internalDevice';
36
36
 
37
37
  describe('resolveFrom', () => {
38
38
  test('defaults method to system, passes through ips', async () => {
39
- const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.226.1.253'] }) }));
39
+ const probe = new VantageProbe(
40
+ fakeTransport({ resolve: async () => ({ ips: ['10.226.1.253'] }) }),
41
+ );
40
42
  const r = await probe.resolveFrom({ from: LAN, name: 'www.example.com' });
41
43
  expect(r.method).toBe('system');
42
44
  expect(r.ips).toEqual(['10.226.1.253']);
@@ -51,7 +53,9 @@ describe('assertResolves / assertNxdomain', () => {
51
53
  );
52
54
  });
53
55
  test('assertNxdomain throws when a public vantage CAN see an internal-only name', async () => {
54
- const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.226.20.5'] }) }));
56
+ const probe = new VantageProbe(
57
+ fakeTransport({ resolve: async () => ({ ips: ['10.226.20.5'] }) }),
58
+ );
55
59
  await expect(
56
60
  probe.assertNxdomain({ from: 'publicInternet', name: 'authentik.internal' }),
57
61
  ).rejects.toThrow(/expected NXDOMAIN/);
@@ -60,25 +64,45 @@ describe('assertResolves / assertNxdomain', () => {
60
64
 
61
65
  describe('assertResolvesInZone — the killer verb', () => {
62
66
  test('ISS-0101: www at caddy DMZ container IP FAILS from internalDevice', async () => {
63
- const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.226.10.10'] }) }));
67
+ const probe = new VantageProbe(
68
+ fakeTransport({ resolve: async () => ({ ips: ['10.226.10.10'] }) }),
69
+ );
64
70
  await expect(
65
71
  probe.assertResolvesInZone({ from: LAN, name: 'www.example.com', zone: 'internal' }),
66
72
  ).rejects.toThrow(/ISS-0101/);
67
73
  });
68
74
 
69
75
  test('www at the firewall natIp PASSES from internalDevice (the right answer)', async () => {
70
- const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.226.1.253'] }) }));
71
- const r = await probe.assertResolvesInZone({ from: LAN, name: 'www.example.com', zone: 'internal' });
76
+ const probe = new VantageProbe(
77
+ fakeTransport({ resolve: async () => ({ ips: ['10.226.1.253'] }) }),
78
+ );
79
+ const r = await probe.assertResolvesInZone({
80
+ from: LAN,
81
+ name: 'www.example.com',
82
+ zone: 'internal',
83
+ });
72
84
  expect(r.ips).toEqual(['10.226.1.253']);
73
85
  });
74
86
 
75
87
  test('public vantage expecting a public answer passes; a leaked zone IP fails', async () => {
76
- const ok = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['100.100.0.100'] }) }));
77
- await ok.assertResolvesInZone({ from: 'publicInternet', name: 'www.example.com', zone: 'public' });
88
+ const ok = new VantageProbe(
89
+ fakeTransport({ resolve: async () => ({ ips: ['100.100.0.100'] }) }),
90
+ );
91
+ await ok.assertResolvesInZone({
92
+ from: 'publicInternet',
93
+ name: 'www.example.com',
94
+ zone: 'public',
95
+ });
78
96
 
79
- const leaked = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.226.1.253'] }) }));
97
+ const leaked = new VantageProbe(
98
+ fakeTransport({ resolve: async () => ({ ips: ['10.226.1.253'] }) }),
99
+ );
80
100
  await expect(
81
- leaked.assertResolvesInZone({ from: 'publicInternet', name: 'www.example.com', zone: 'public' }),
101
+ leaked.assertResolvesInZone({
102
+ from: 'publicInternet',
103
+ name: 'www.example.com',
104
+ zone: 'public',
105
+ }),
82
106
  ).rejects.toThrow(VantageAssertionError);
83
107
  });
84
108
 
@@ -94,7 +118,9 @@ describe('assertResolvesInZone — the killer verb', () => {
94
118
 
95
119
  describe('assertResolvesTo — exact-IP DNS correctness', () => {
96
120
  test('passes when the resolver returns the expected IP (via a specific server)', async () => {
97
- const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.226.1.53'] }) }));
121
+ const probe = new VantageProbe(
122
+ fakeTransport({ resolve: async () => ({ ips: ['10.226.1.53'] }) }),
123
+ );
98
124
  const r = await probe.assertResolvesTo({
99
125
  from: 'internalDevice',
100
126
  name: 'host.infra.example.com',
@@ -106,7 +132,9 @@ describe('assertResolvesTo — exact-IP DNS correctness', () => {
106
132
  });
107
133
 
108
134
  test('fails (naming the server) when the expected IP is absent', async () => {
109
- const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['10.226.10.99'] }) }));
135
+ const probe = new VantageProbe(
136
+ fakeTransport({ resolve: async () => ({ ips: ['10.226.10.99'] }) }),
137
+ );
110
138
  await expect(
111
139
  probe.assertResolvesTo({
112
140
  from: 'internalDevice',
@@ -119,31 +147,49 @@ describe('assertResolvesTo — exact-IP DNS correctness', () => {
119
147
  });
120
148
 
121
149
  test('accepts an array of expected IPs (all must be present)', async () => {
122
- const probe = new VantageProbe(fakeTransport({ resolve: async () => ({ ips: ['1.2.3.4', '5.6.7.8'] }) }));
123
- await probe.assertResolvesTo({ from: 'publicInternet', name: 'x', expected: ['1.2.3.4', '5.6.7.8'] });
150
+ const probe = new VantageProbe(
151
+ fakeTransport({ resolve: async () => ({ ips: ['1.2.3.4', '5.6.7.8'] }) }),
152
+ );
153
+ await probe.assertResolvesTo({
154
+ from: 'publicInternet',
155
+ name: 'x',
156
+ expected: ['1.2.3.4', '5.6.7.8'],
157
+ });
124
158
  await expect(
125
- probe.assertResolvesTo({ from: 'publicInternet', name: 'x', expected: ['1.2.3.4', '9.9.9.9'] }),
159
+ probe.assertResolvesTo({
160
+ from: 'publicInternet',
161
+ name: 'x',
162
+ expected: ['1.2.3.4', '9.9.9.9'],
163
+ }),
126
164
  ).rejects.toThrow(/9\.9\.9\.9/);
127
165
  });
128
166
  });
129
167
 
130
168
  describe('reachability + isolation (§D6)', () => {
131
169
  test('assertReachable throws when the connection fails (ISS-0111 git-ssh:2222)', async () => {
132
- const probe = new VantageProbe(fakeTransport({ tcpConnect: async () => ({ connected: false, detail: 'timeout' }) }));
133
- await expect(probe.assertReachable({ from: LAN, host: 'git-ssh.example.com', port: 2222 })).rejects.toThrow(
134
- /timeout/,
170
+ const probe = new VantageProbe(
171
+ fakeTransport({ tcpConnect: async () => ({ connected: false, detail: 'timeout' }) }),
135
172
  );
173
+ await expect(
174
+ probe.assertReachable({ from: LAN, host: 'git-ssh.example.com', port: 2222 }),
175
+ ).rejects.toThrow(/timeout/);
136
176
  });
137
177
  test('assertUnreachable throws when isolation is violated', async () => {
138
- const probe = new VantageProbe(fakeTransport({ tcpConnect: async () => ({ connected: true }) }));
139
- await expect(probe.assertUnreachable({ from: 'appSystem', host: '10.226.30.5', port: 443 })).rejects.toThrow(
140
- /zone isolation/,
178
+ const probe = new VantageProbe(
179
+ fakeTransport({ tcpConnect: async () => ({ connected: true }) }),
141
180
  );
181
+ await expect(
182
+ probe.assertUnreachable({ from: 'appSystem', host: '10.226.30.5', port: 443 }),
183
+ ).rejects.toThrow(/zone isolation/);
142
184
  });
143
185
  test('the reachable/unreachable happy paths do not throw', async () => {
144
- const reachable = new VantageProbe(fakeTransport({ tcpConnect: async () => ({ connected: true }) }));
186
+ const reachable = new VantageProbe(
187
+ fakeTransport({ tcpConnect: async () => ({ connected: true }) }),
188
+ );
145
189
  await reachable.assertReachable({ from: LAN, host: '10.226.1.253', port: 443 });
146
- const blocked = new VantageProbe(fakeTransport({ tcpConnect: async () => ({ connected: false }) }));
190
+ const blocked = new VantageProbe(
191
+ fakeTransport({ tcpConnect: async () => ({ connected: false }) }),
192
+ );
147
193
  await blocked.assertUnreachable({ from: 'appSystem', host: '10.226.30.5', port: 443 });
148
194
  });
149
195
  });
@@ -151,9 +197,17 @@ describe('reachability + isolation (§D6)', () => {
151
197
  describe('assertHttps — cert provenance', () => {
152
198
  test('self-signed cert is rejected (catches --insecure / skip-cert cheats)', async () => {
153
199
  const probe = new VantageProbe(
154
- fakeTransport({ httpsRequest: async () => ({ status: 200, body: 'ok', cert: realCert({ selfSigned: true }) }) }),
200
+ fakeTransport({
201
+ httpsRequest: async () => ({
202
+ status: 200,
203
+ body: 'ok',
204
+ cert: realCert({ selfSigned: true }),
205
+ }),
206
+ }),
207
+ );
208
+ await expect(probe.assertHttps({ from: LAN, url: 'https://www.example.com' })).rejects.toThrow(
209
+ /self-signed/,
155
210
  );
156
- await expect(probe.assertHttps({ from: LAN, url: 'https://www.example.com' })).rejects.toThrow(/self-signed/);
157
211
  });
158
212
  test('wrong CA issuer is rejected', async () => {
159
213
  const probe = new VantageProbe(fakeTransport());
package/src/vantage.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  * elsewhere, so the verbs stay pure and unit-testable without Docker.
15
15
  */
16
16
 
17
- import { type Vantage, type Zone } from './types';
17
+ import type { Vantage, Zone } from './types';
18
18
  import { classifyIp } from './zone-classifier';
19
19
 
20
20
  // `Vantage` is topology vocabulary — defined in ./types alongside Zone — and re-exported
@@ -64,11 +64,23 @@ export interface ResolutionInspection {
64
64
  * are the ONLY place raw shell / network calls live; the verbs below never touch I/O.
65
65
  */
66
66
  export interface ProbeTransport {
67
- resolve(opts: { from: Vantage; name: string; method: ResolveMethod; server?: string }): Promise<{ ips: string[] }>;
68
- tcpConnect(opts: { from: Vantage; host: string; port: number }): Promise<{ connected: boolean; detail?: string }>;
67
+ resolve(opts: { from: Vantage; name: string; method: ResolveMethod; server?: string }): Promise<{
68
+ ips: string[];
69
+ }>;
70
+ tcpConnect(opts: { from: Vantage; host: string; port: number }): Promise<{
71
+ connected: boolean;
72
+ detail?: string;
73
+ }>;
69
74
  ping(opts: { from: Vantage; target: string }): Promise<{ alive: boolean }>;
70
- httpsRequest(opts: { from: Vantage; url: string }): Promise<{ status: number; body: string; cert: CertInfo }>;
71
- inspectResolution(opts: { from: Vantage; name: string }): Promise<{ resolvConf: string; hostsLines: string[] }>;
75
+ httpsRequest(opts: { from: Vantage; url: string }): Promise<{
76
+ status: number;
77
+ body: string;
78
+ cert: CertInfo;
79
+ }>;
80
+ inspectResolution(opts: { from: Vantage; name: string }): Promise<{
81
+ resolvConf: string;
82
+ hostsLines: string[];
83
+ }>;
72
84
  }
73
85
 
74
86
  /** Thrown when a vantage assertion fails. Messages name the bug class they guard. */
@@ -163,10 +175,7 @@ export class VantageProbe {
163
175
  if (offenders.length > 0) {
164
176
  const detail = offenders.map((ip) => `${ip} (${describeZone(ip)})`).join(', ');
165
177
  throw new VantageAssertionError(
166
- `${opts.name} from ${opts.from} resolved to ${detail}, expected zone "${opts.zone}". ` +
167
- 'A managed-zone container IP is unroutable from outside that zone — this is the ' +
168
- 'e2e-green/prod-broke class (ISS-0101, ISS-0111). The right answer from a consumer ' +
169
- 'vantage is the firewall natIp (internal) or a public address.',
178
+ `${opts.name} from ${opts.from} resolved to ${detail}, expected zone "${opts.zone}". A managed-zone container IP is unroutable from outside that zone — this is the e2e-green/prod-broke class (ISS-0101, ISS-0111). The right answer from a consumer vantage is the firewall natIp (internal) or a public address.`,
170
179
  );
171
180
  }
172
181
  return result;
@@ -178,7 +187,9 @@ export class VantageProbe {
178
187
  * the internal split-horizon resolver vs the public resolver). Used by the DNS suites to
179
188
  * check a DDNS-registered IP, an nsupdate'd A record, or a split-horizon answer.
180
189
  */
181
- async assertResolvesTo(opts: ResolveOpts & { expected: string | string[] }): Promise<ResolveResult> {
190
+ async assertResolvesTo(
191
+ opts: ResolveOpts & { expected: string | string[] },
192
+ ): Promise<ResolveResult> {
182
193
  const result = await this.assertResolves(opts);
183
194
  const expected = Array.isArray(opts.expected) ? opts.expected : [opts.expected];
184
195
  const missing = expected.filter((ip) => !result.ips.includes(ip));
@@ -201,9 +212,7 @@ export class VantageProbe {
201
212
  const { connected, detail } = await this.transport.tcpConnect(opts);
202
213
  if (!connected) {
203
214
  throw new VantageAssertionError(
204
- `${opts.host}:${opts.port} was NOT reachable from ${opts.from}` +
205
- (detail ? ` (${detail})` : '') +
206
- ', expected a successful TCP connection.',
215
+ `${opts.host}:${opts.port} was NOT reachable from ${opts.from}${detail ? ` (${detail})` : ''}, expected a successful TCP connection.`,
207
216
  );
208
217
  }
209
218
  }
@@ -213,8 +222,7 @@ export class VantageProbe {
213
222
  const { connected } = await this.transport.tcpConnect(opts);
214
223
  if (connected) {
215
224
  throw new VantageAssertionError(
216
- `${opts.host}:${opts.port} WAS reachable from ${opts.from}, expected it to be blocked ` +
217
- '(zone isolation / firewall boundary violated).',
225
+ `${opts.host}:${opts.port} WAS reachable from ${opts.from}, expected it to be blocked (zone isolation / firewall boundary violated).`,
218
226
  );
219
227
  }
220
228
  }
@@ -229,8 +237,7 @@ export class VantageProbe {
229
237
 
230
238
  if (res.cert.selfSigned) {
231
239
  throw new VantageAssertionError(
232
- `${opts.url} from ${opts.from} presented a self-signed cert (issuer "${res.cert.issuer}"). ` +
233
- 'A real cert from the e2e CA (Pebble / Let’s Encrypt) is required — no --insecure.',
240
+ `${opts.url} from ${opts.from} presented a self-signed cert (issuer "${res.cert.issuer}"). A real cert from the e2e CA (Pebble / Let’s Encrypt) is required — no --insecure.`,
234
241
  );
235
242
  }
236
243
  if (opts.expectCN && res.cert.cn !== opts.expectCN) {
@@ -10,7 +10,7 @@
10
10
  * decides whether to spawn cele2e themselves or just observe.
11
11
  */
12
12
 
13
- import { defineEvents, openBus, type Bus, type BusEvent } from '@celilo/event-bus';
13
+ import { type Bus, type BusEvent, defineEvents, openBus } from '@celilo/event-bus';
14
14
 
15
15
  export type WaitResult =
16
16
  | {
@@ -1,5 +1,11 @@
1
1
  import { describe, expect, test } from 'bun:test';
2
- import { classifyIp, inCidr, isPublicLeakSafe, isSegmentedZoneIp, parseIpv4 } from './zone-classifier';
2
+ import {
3
+ classifyIp,
4
+ inCidr,
5
+ isPublicLeakSafe,
6
+ isSegmentedZoneIp,
7
+ parseIpv4,
8
+ } from './zone-classifier';
3
9
 
4
10
  describe('parseIpv4', () => {
5
11
  test('parses dotted-quad to unsigned 32-bit', () => {
@@ -16,7 +16,7 @@
16
16
  * caller's policy, layered on top of this primitive.)
17
17
  */
18
18
 
19
- import { type Zone, ZONE_SUBNETS } from './types';
19
+ import { ZONE_SUBNETS, type Zone } from './types';
20
20
 
21
21
  export type ZoneClass =
22
22
  | { kind: 'zone'; zone: Zone }
@@ -62,7 +62,7 @@ export function inCidr(ip: string, cidr: string): boolean {
62
62
  if (ipN === null || baseN === null || Number.isNaN(bits) || bits < 0 || bits > 32) return false;
63
63
  if (bits === 0) return true;
64
64
  const mask = (0xffffffff << (32 - bits)) >>> 0;
65
- return (ipN & mask) >>> 0 === ((baseN & mask) >>> 0);
65
+ return (ipN & mask) >>> 0 === (baseN & mask) >>> 0;
66
66
  }
67
67
 
68
68
  /** Classify a resolved IPv4 against the e2e topology. */