@celilo/e2e 0.5.1 → 0.6.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 (59) hide show
  1. package/README.md +2 -1
  2. package/bin/cele2e +14 -0
  3. package/bin/e2e-cache-images +57 -0
  4. package/bin/e2e-shell +36 -13
  5. package/bin/e2e-status +8 -2
  6. package/bin/e2e-up +5 -5
  7. package/completions/install-zsh-completion.sh +78 -0
  8. package/config/dns/celilo.computer.zone +16 -0
  9. package/config/dns/iamtheinternet.org.zone +7 -6
  10. package/config/dns/knot-namecheap.conf +8 -0
  11. package/config/dns/knot-tld.conf +4 -0
  12. package/config/dns/lunacycle.net.zone +16 -0
  13. package/config/dns/net.zone +11 -0
  14. package/config/dns/org.zone +5 -0
  15. package/config/dns/root.zone +2 -0
  16. package/config/dns/templates/iamtheinternet.org.zone +16 -0
  17. package/config/dns/templates/lunacycle.net.zone +16 -0
  18. package/config/resolver/unbound.conf +10 -0
  19. package/config/routing/docker-image-preload.service +14 -0
  20. package/config/routing/docker-image-preload.sh +24 -0
  21. package/config/routing/management-routes.sh +24 -2
  22. package/config/routing/resolver-internal-routes.sh +59 -0
  23. package/docker/Dockerfile.internal-resolver +16 -0
  24. package/docker/Dockerfile.isitup +37 -0
  25. package/docker/Dockerfile.management +2 -0
  26. package/docker/Dockerfile.registry +34 -0
  27. package/docker/Dockerfile.target-machine +20 -0
  28. package/docker/Dockerfile.target-machine-docker +11 -19
  29. package/docker/Dockerfile.target-technitium +97 -0
  30. package/netapps/authentik.netapp +0 -0
  31. package/netapps/caddy.netapp +0 -0
  32. package/netapps/greenwave.netapp +0 -0
  33. package/netapps/homebridge.netapp +0 -0
  34. package/netapps/iptables.netapp +0 -0
  35. package/netapps/knot-unbound-internal.netapp +0 -0
  36. package/netapps/namecheap.netapp +0 -0
  37. package/netapps/technitium.netapp +0 -0
  38. package/netapps/test-ansible-output.netapp +0 -0
  39. package/package.json +8 -1
  40. package/simulators/greenwave/state.ts +7 -12
  41. package/simulators/isitup/Caddyfile +11 -0
  42. package/simulators/isitup/entrypoint.sh +47 -0
  43. package/simulators/isitup/server.ts +112 -0
  44. package/simulators/namecheap-ddns/server.ts +31 -2
  45. package/src/cli/build.ts +214 -0
  46. package/src/cli/command-registry.ts +100 -0
  47. package/src/cli/command-tree-parser.ts +52 -0
  48. package/src/cli/completion.ts +195 -0
  49. package/src/cli/index.ts +273 -0
  50. package/src/cli/module-discovery.ts +120 -0
  51. package/src/cli/scaffold.ts +108 -0
  52. package/src/container-manager.ts +373 -76
  53. package/src/docker-compose-generator.ts +127 -23
  54. package/src/network-builder.ts +21 -0
  55. package/src/parse-line.ts +147 -0
  56. package/src/runner.ts +197 -247
  57. package/src/simulator-ips.ts +42 -0
  58. package/src/types.ts +28 -0
  59. package/bin/cache-docker-images +0 -46
@@ -0,0 +1,34 @@
1
+ FROM oven/bun:1
2
+
3
+ WORKDIR /app
4
+
5
+ # Install the shared @celilo/registry-server package.
6
+ # Both this e2e stand-in and the celilo-registry module run the same code —
7
+ # packages/registry-server is the single source of truth.
8
+ COPY registry-server/package.json ./package.json
9
+ COPY registry-server/tsconfig.json ./tsconfig.json
10
+ COPY registry-server/src ./src
11
+
12
+ RUN bun install --production
13
+
14
+ # Bootstrap mode: the shared-infra stand-in serves modules directly from the
15
+ # repo's modules/ directory (volume-mounted read-only). This preserves the
16
+ # pre-unification convenience where tests can `celilo module import
17
+ # public-registry namecheap` without having to publish namecheap first.
18
+ #
19
+ # The production celilo-registry module does NOT set BOOTSTRAP_MODULES_DIR
20
+ # and only serves pre-published packages.
21
+ ENV DATA_DIR=/var/lib/celilo-registry \
22
+ BOOTSTRAP_MODULES_DIR=/modules \
23
+ BOOTSTRAP_UPLOADS_DIR=/uploads \
24
+ BOOTSTRAP_CACHE_DIR=/cache \
25
+ PATH_PREFIX= \
26
+ PORT=80
27
+
28
+ # /uploads is the drop-zone for .netapps that tests push via
29
+ # `docker compose cp`. Create the dir at build time so `docker cp` has
30
+ # somewhere to put files before the server runs.
31
+ RUN mkdir -p /uploads
32
+
33
+ EXPOSE 80
34
+ CMD ["bun", "run", "src/index.ts"]
@@ -57,5 +57,25 @@ RUN (cd /lib/systemd/system/sysinit.target.wants/ && \
57
57
  # Re-enable the services we actually need
58
58
  RUN systemctl enable ssh target-setup
59
59
 
60
+ # --- Caddy pre-installation ---
61
+ # Temporarily disable apt proxy (not available during docker build)
62
+ RUN rm -f /etc/apt/apt.conf.d/00proxy
63
+
64
+ # Install Caddy (same steps as the Ansible role, but at image build time so
65
+ # the role finds it already installed and skips the slow cloudsmith.io downloads)
66
+ RUN apt-get update && apt-get install -y \
67
+ debian-keyring \
68
+ debian-archive-keyring \
69
+ apt-transport-https \
70
+ && rm -rf /var/lib/apt/lists/*
71
+ RUN curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \
72
+ gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
73
+ RUN curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | \
74
+ tee /etc/apt/sources.list.d/caddy-stable.list
75
+ RUN apt-get update && apt-get install -y caddy && rm -rf /var/lib/apt/lists/*
76
+
77
+ # Re-enable apt proxy for runtime
78
+ RUN echo 'Acquire::http::Proxy "http://100.64.0.2:3142";' > /etc/apt/apt.conf.d/00proxy
79
+
60
80
  STOPSIGNAL SIGRTMIN+3
61
81
  ENTRYPOINT ["/sbin/init"]
@@ -1,6 +1,6 @@
1
- # Target machine with Docker pre-installed and authentik images pre-pulled.
2
- # Used for app-zone machines that will run Docker Compose workloads.
3
- # This avoids a 10+ minute Docker pull during e2e test deploy time.
1
+ # Target machine with Docker pre-installed.
2
+ # Used for app-zone machines that run Docker Compose workloads (e.g. authentik).
3
+ # Docker itself is baked in; application images are pulled at deploy time by each module.
4
4
 
5
5
  FROM ubuntu:22.04
6
6
 
@@ -87,22 +87,14 @@ RUN mkdir -p /etc/docker && \
87
87
 
88
88
  RUN systemctl enable docker
89
89
 
90
- # Pre-cached image loading service:
91
- # At first boot, if /docker-image-cache/*.tar exists, load them into Docker.
92
- # This avoids multi-minute pulls during e2e test deploy.
93
- RUN echo '#!/bin/bash\n\
94
- set -e\n\
95
- if ls /docker-image-cache/*.tar 1>/dev/null 2>&1; then\n\
96
- for img in /docker-image-cache/*.tar; do\n\
97
- echo "Loading cached Docker image: $img"\n\
98
- docker load < "$img"\n\
99
- done\n\
100
- fi\n' > /usr/local/bin/load-cached-images.sh && \
101
- chmod +x /usr/local/bin/load-cached-images.sh
102
-
103
- RUN printf '[Unit]\nDescription=Load pre-cached Docker images\nAfter=docker.service\nRequires=docker.service\n\n[Service]\nType=oneshot\nExecStart=/usr/local/bin/load-cached-images.sh\nRemainAfterExit=yes\n\n[Install]\nWantedBy=multi-user.target\n' \
104
- > /etc/systemd/system/load-docker-cache.service && \
105
- systemctl enable load-docker-cache
90
+ # Pre-load cached Docker images on startup (e2e acceleration)
91
+ # /docker-image-cache is bind-mounted from the host by the compose generator.
92
+ # The service is a no-op if the directory is absent or empty.
93
+ COPY config/routing/docker-image-preload.sh /usr/local/bin/docker-image-preload.sh
94
+ RUN chmod +x /usr/local/bin/docker-image-preload.sh
95
+ COPY config/routing/docker-image-preload.service /etc/systemd/system/docker-image-preload.service
96
+ RUN systemctl enable docker-image-preload
97
+
106
98
 
107
99
  STOPSIGNAL SIGRTMIN+3
108
100
  ENTRYPOINT ["/sbin/init"]
@@ -0,0 +1,97 @@
1
+ # Target machine with Technitium DNS Server pre-installed.
2
+ # Based on target-machine; Technitium is installed at image build time so the
3
+ # Ansible role finds it already present and skips the download step.
4
+
5
+ FROM ubuntu:22.04
6
+
7
+ ENV DEBIAN_FRONTEND=noninteractive
8
+
9
+ RUN apt-get update && apt-get install -y \
10
+ systemd \
11
+ systemd-sysv \
12
+ openssh-server \
13
+ iproute2 \
14
+ iputils-ping \
15
+ net-tools \
16
+ dnsutils \
17
+ procps \
18
+ python3 \
19
+ python3-apt \
20
+ sqlite3 \
21
+ jq \
22
+ sudo \
23
+ curl \
24
+ ca-certificates \
25
+ && rm -rf /var/lib/apt/lists/*
26
+
27
+ # Trust the E2E forward proxy CA (for transparent HTTPS interception)
28
+ COPY config/proxy/squid-ca.crt /usr/local/share/ca-certificates/e2e-proxy-ca.crt
29
+ # Trust Pebble's test CA (so Caddy/ACME clients trust the simulated Let's Encrypt)
30
+ COPY config/pebble/pebble-ca.crt /usr/local/share/ca-certificates/pebble-ca.crt
31
+ RUN update-ca-certificates
32
+
33
+ RUN mkdir -p /run/sshd && \
34
+ sed -i 's/#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config && \
35
+ sed -i 's/#PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
36
+
37
+ # Enable SSH service
38
+ RUN systemctl enable ssh
39
+
40
+ # Point apt at the simulated apt cache proxy
41
+ RUN echo 'Acquire::http::Proxy "http://100.64.0.2:3142";' > /etc/apt/apt.conf.d/00proxy
42
+
43
+ # E2E network setup service (runs at boot to configure routing, DNS, SSH keys)
44
+ COPY config/routing/target-setup.sh /usr/local/bin/target-setup.sh
45
+ RUN chmod +x /usr/local/bin/target-setup.sh
46
+
47
+ COPY config/routing/target-setup.service /etc/systemd/system/target-setup.service
48
+ RUN systemctl enable target-setup
49
+
50
+ # Clean up unnecessary systemd services that slow down boot
51
+ RUN (cd /lib/systemd/system/sysinit.target.wants/ && \
52
+ ls | grep -v systemd-tmpfiles-setup | xargs rm -f 2>/dev/null); \
53
+ rm -f /lib/systemd/system/multi-user.target.wants/*; \
54
+ rm -f /etc/systemd/system/*.wants/*; \
55
+ rm -f /lib/systemd/system/local-fs.target.wants/*; \
56
+ rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
57
+ rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
58
+ rm -f /lib/systemd/system/basic.target.wants/*; \
59
+ true
60
+
61
+ # Re-enable the services we actually need
62
+ RUN systemctl enable ssh target-setup
63
+
64
+ # --- Technitium pre-installation (manual — installer requires running systemd) ---
65
+ # The official install.sh checks for systemd at runtime and fails during docker build.
66
+ # Instead: install ASP.NET Core runtime, download binaries, write the service file.
67
+ # Ansible tasks check for /opt/technitium/dns/DnsServerApp.dll and skip the download.
68
+
69
+ # Temporarily lift the apt proxy (not available at build time)
70
+ RUN rm -f /etc/apt/apt.conf.d/00proxy
71
+
72
+ # ASP.NET Core 8.0 runtime via Microsoft package feed
73
+ RUN curl -sSL https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb \
74
+ -o /tmp/ms-prod.deb && \
75
+ dpkg -i /tmp/ms-prod.deb && \
76
+ rm /tmp/ms-prod.deb && \
77
+ apt-get update && \
78
+ apt-get install -y aspnetcore-runtime-8.0 && \
79
+ rm -rf /var/lib/apt/lists/*
80
+
81
+ # Download and extract Technitium portable package
82
+ RUN mkdir -p /opt/technitium/dns && \
83
+ curl -sSL https://download.technitium.com/dns/DnsServerPortable.tar.gz \
84
+ -o /tmp/technitium.tar.gz && \
85
+ tar -xzf /tmp/technitium.tar.gz -C /opt/technitium/dns && \
86
+ rm -f /tmp/technitium.tar.gz
87
+
88
+ # Register systemd service (same unit the installer creates)
89
+ RUN printf '[Unit]\nDescription=Technitium DNS Server\nAfter=network.target\n\n[Service]\nExecStart=/usr/bin/dotnet /opt/technitium/dns/DnsServerApp.dll\nRestart=on-failure\nRestartSec=10\n\n[Install]\nWantedBy=multi-user.target\n' \
90
+ > /etc/systemd/system/dns.service && \
91
+ systemctl enable dns
92
+
93
+ # Re-enable apt proxy for runtime
94
+ RUN echo 'Acquire::http::Proxy "http://100.64.0.2:3142";' > /etc/apt/apt.conf.d/00proxy
95
+
96
+ STOPSIGNAL SIGRTMIN+3
97
+ ENTRYPOINT ["/sbin/init"]
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@celilo/e2e",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "E2E test infrastructure for Celilo-deployed applications. Provides a simulated internet with DNS hierarchy, ACME server, firewalls, and target machines in Docker.",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -10,21 +10,28 @@
10
10
  "./fixtures": "./src/fixtures.ts"
11
11
  },
12
12
  "bin": {
13
+ "cele2e": "./bin/cele2e",
13
14
  "celilo-e2e-run": "./bin/e2e-run",
14
15
  "celilo-e2e-build": "./bin/e2e-build",
15
16
  "celilo-e2e-load": "./bin/e2e-load",
16
17
  "celilo-e2e-infra": "./bin/e2e-infra"
17
18
  },
19
+ "scripts": {
20
+ "test": "bun test tests/",
21
+ "test:completion": "bun test tests/completion"
22
+ },
18
23
  "files": [
19
24
  "src/",
20
25
  "docker/",
21
26
  "config/",
27
+ "completions/",
22
28
  "simulators/",
23
29
  "bin/",
24
30
  "netapps/",
25
31
  "README.md"
26
32
  ],
27
33
  "dependencies": {
34
+ "@celilo/cli-display": "0.1.0",
28
35
  "yaml": "^2.8.0"
29
36
  },
30
37
  "devDependencies": {
@@ -161,28 +161,23 @@ function applyIptablesRule(mapping: PortMapping): void {
161
161
  const intIp = mapping.internalClient;
162
162
  const intPort = mapping.internalPort;
163
163
 
164
- try {
164
+ function addRules(p: string) {
165
165
  // DNAT incoming traffic on external interface destined for our public IP
166
166
  execSync(
167
- `iptables -t nat -A PREROUTING -i ${EXTERNAL_INTERFACE} -d ${PUBLIC_IP} -p ${proto} --dport ${extPort} -j DNAT --to-destination ${intIp}:${intPort}`,
167
+ `iptables -t nat -A PREROUTING -i ${EXTERNAL_INTERFACE} -d ${PUBLIC_IP} -p ${p} --dport ${extPort} -j DNAT --to-destination ${intIp}:${intPort}`,
168
168
  { timeout: 5000 },
169
169
  );
170
170
  // Allow forwarded traffic
171
171
  execSync(
172
- `iptables -A FORWARD -p ${proto} -d ${intIp} --dport ${intPort} -j ACCEPT`,
172
+ `iptables -A FORWARD -p ${p} -d ${intIp} --dport ${intPort} -j ACCEPT`,
173
173
  { timeout: 5000 },
174
174
  );
175
+ }
175
176
 
176
- // If protocol is TCP/UDP, also add UDP rules
177
+ try {
178
+ addRules(proto);
177
179
  if (mapping.protocol === 'TCP/UDP') {
178
- execSync(
179
- `iptables -t nat -A PREROUTING -i ${EXTERNAL_INTERFACE} -d ${PUBLIC_IP} -p udp --dport ${extPort} -j DNAT --to-destination ${intIp}:${intPort}`,
180
- { timeout: 5000 },
181
- );
182
- execSync(
183
- `iptables -A FORWARD -p udp -d ${intIp} --dport ${intPort} -j ACCEPT`,
184
- { timeout: 5000 },
185
- );
180
+ addRules('udp');
186
181
  }
187
182
  } catch (err) {
188
183
  console.error(`[greenwave] Failed to apply iptables rule:`, err);
@@ -0,0 +1,11 @@
1
+ {
2
+ email admin@isitup.org
3
+ # Pebble listens on 443 with a TLS cert whose SAN is
4
+ # acme-v02.api.letsencrypt.org (not an IP), so we use the hostname and
5
+ # rely on comcast-resolver's local-data override to reach 100.64.0.100.
6
+ acme_ca https://acme-v02.api.letsencrypt.org/dir
7
+ }
8
+
9
+ isitup.org {
10
+ reverse_proxy localhost:8080
11
+ }
@@ -0,0 +1,47 @@
1
+ #!/bin/bash
2
+ # Start the Bun API server in the background, then Caddy in the foreground.
3
+ # Caddy terminates TLS (with a cert issued by Pebble) and reverse-proxies
4
+ # /api.json to Bun on localhost:8080.
5
+
6
+ set -e
7
+
8
+ # Route through fw-ext so we can reach comcast-resolver on isp-external
9
+ # (Docker's default bridge gateway has no path across networks). Without
10
+ # this, DNS for acme-v02.api.letsencrypt.org fails, Caddy never gets its
11
+ # cert, and port 443 never opens. Mirrors config/routing/pebble-startup.sh.
12
+ # fw-ext is a per-test container so this route is unreachable when shared
13
+ # infra runs alone — harmless; the test brings fw-ext up before the prober
14
+ # is exercised.
15
+ ip route del default 2>/dev/null || true
16
+ ip route add default via 100.64.0.1
17
+
18
+ # Fetch Pebble's runtime ACME root CA so the simulator trusts certs presented
19
+ # by the targets it probes (e.g. Caddy on iamtheinternet.org uses a Pebble-
20
+ # issued cert). Pebble regenerates its root on every startup, so the static
21
+ # pebble-ca.crt baked in at build time isn't enough.
22
+ echo "Waiting for Pebble ACME root CA..."
23
+ for i in $(seq 1 60); do
24
+ if curl -sk https://100.64.0.100:15000/roots/0 -o /usr/local/share/ca-certificates/pebble-acme-root.crt 2>/dev/null; then
25
+ if [ -s /usr/local/share/ca-certificates/pebble-acme-root.crt ]; then
26
+ update-ca-certificates 2>/dev/null
27
+ echo "Pebble ACME root CA installed"
28
+ break
29
+ fi
30
+ fi
31
+ sleep 2
32
+ done
33
+
34
+ cd /simulator
35
+ bun run server.ts &
36
+ BUN_PID=$!
37
+
38
+ # Wait briefly for Bun to come up before Caddy starts proxying
39
+ for i in $(seq 1 10); do
40
+ if curl -sf http://127.0.0.1:8080/health >/dev/null 2>&1; then
41
+ break
42
+ fi
43
+ sleep 0.3
44
+ done
45
+
46
+ # Caddy runs in foreground so the container lifecycle follows it
47
+ exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
@@ -0,0 +1,112 @@
1
+ /**
2
+ * isitup.org Simulator
3
+ *
4
+ * Mimics the isitup.org JSON API:
5
+ * GET /api.json?url=<domain>
6
+ *
7
+ * status_code values match the real API:
8
+ * 1 = site is up
9
+ * 2 = site is down (connection failed / non-2xx/3xx)
10
+ * 3 = bad URL (malformed)
11
+ *
12
+ * The simulator is deployed on internet-external and probes the target
13
+ * from that network position — so it exercises the same public-facing
14
+ * path a real external prober would.
15
+ */
16
+
17
+ const PORT = Number(process.env.PORT) || 8080;
18
+
19
+ interface ProbeResponse {
20
+ domain: string;
21
+ port: number;
22
+ status_code: 1 | 2 | 3;
23
+ response_ip: string;
24
+ response_code: number;
25
+ response_time: number;
26
+ }
27
+
28
+ async function probe(targetUrl: string): Promise<ProbeResponse> {
29
+ let parsed: URL;
30
+ const withScheme = targetUrl.includes('://') ? targetUrl : `https://${targetUrl}`;
31
+ try {
32
+ parsed = new URL(withScheme);
33
+ } catch {
34
+ return {
35
+ domain: targetUrl,
36
+ port: 443,
37
+ status_code: 3,
38
+ response_ip: '',
39
+ response_code: 0,
40
+ response_time: 0,
41
+ };
42
+ }
43
+
44
+ const port = parsed.port
45
+ ? Number(parsed.port)
46
+ : parsed.protocol === 'https:'
47
+ ? 443
48
+ : 80;
49
+
50
+ const start = Date.now();
51
+ try {
52
+ const response = await fetch(withScheme, {
53
+ signal: AbortSignal.timeout(10_000),
54
+ redirect: 'follow',
55
+ });
56
+ const responseTime = (Date.now() - start) / 1000;
57
+ // Real isitup treats 2xx/3xx as up. Anything else (or unreachable) is down.
58
+ const isUp = response.status >= 200 && response.status < 400;
59
+ console.log(`[isitup] ${withScheme} → HTTP ${response.status} in ${responseTime.toFixed(2)}s`);
60
+ return {
61
+ domain: parsed.hostname,
62
+ port,
63
+ status_code: isUp ? 1 : 2,
64
+ response_ip: '',
65
+ response_code: response.status,
66
+ response_time: responseTime,
67
+ };
68
+ } catch (error) {
69
+ const message = error instanceof Error ? error.message : String(error);
70
+ console.log(`[isitup] ${withScheme} → probe error: ${message}`);
71
+ return {
72
+ domain: parsed.hostname,
73
+ port,
74
+ status_code: 2,
75
+ response_ip: '',
76
+ response_code: 0,
77
+ response_time: (Date.now() - start) / 1000,
78
+ };
79
+ }
80
+ }
81
+
82
+ const server = Bun.serve({
83
+ port: PORT,
84
+ async fetch(req) {
85
+ const url = new URL(req.url);
86
+
87
+ if (url.pathname === '/api.json') {
88
+ const target = url.searchParams.get('url');
89
+ if (!target) {
90
+ return Response.json({
91
+ domain: '',
92
+ port: 443,
93
+ status_code: 3,
94
+ response_ip: '',
95
+ response_code: 0,
96
+ response_time: 0,
97
+ });
98
+ }
99
+ const result = await probe(target);
100
+ console.log(`[isitup] ${target} → ${result.status_code} (${result.response_code})`);
101
+ return Response.json(result);
102
+ }
103
+
104
+ if (url.pathname === '/' || url.pathname === '/health') {
105
+ return Response.json({ ok: true, service: 'isitup.org simulator' });
106
+ }
107
+
108
+ return new Response('Not Found', { status: 404 });
109
+ },
110
+ });
111
+
112
+ console.log(`[isitup] simulator listening on port ${server.port}`);
@@ -11,9 +11,38 @@
11
11
  import { updateZone } from './zone-updater';
12
12
  import type { DdnsRecord } from './types';
13
13
 
14
- const VALID_PASSWORD = process.env.DDNS_PASSWORD || 'test123';
14
+ // Real Namecheap DDNS uses a separate password per domain. The simulator
15
+ // supports two modes:
16
+ // 1. DDNS_PASSWORDS (JSON object mapping domain→password). Per-domain
17
+ // lookup; password must match the entry for the requested domain.
18
+ // 2. DDNS_PASSWORD (single string fallback). Accepted for any domain.
19
+ // Default `test123` keeps single-domain tests working unchanged.
20
+ // Mode 1 wins when set; the cross-domain e2e test exercises it via
21
+ // docker-compose env to verify per-domain password routing.
22
+ const PASSWORD_MAP: Record<string, string> = (() => {
23
+ const raw = process.env.DDNS_PASSWORDS;
24
+ if (!raw) return {};
25
+ try {
26
+ const parsed = JSON.parse(raw);
27
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
28
+ const out: Record<string, string> = {};
29
+ for (const [k, v] of Object.entries(parsed)) {
30
+ if (typeof v === 'string') out[k] = v;
31
+ }
32
+ return out;
33
+ }
34
+ } catch {
35
+ console.warn(`[ddns] Failed to parse DDNS_PASSWORDS env var: ${raw}`);
36
+ }
37
+ return {};
38
+ })();
39
+ const FALLBACK_PASSWORD = process.env.DDNS_PASSWORD || 'test123';
15
40
  const PORT = Number(process.env.DDNS_PORT) || 8080;
16
41
 
42
+ function expectedPasswordFor(domain: string): string {
43
+ return PASSWORD_MAP[domain] ?? FALLBACK_PASSWORD;
44
+ }
45
+
17
46
  // Track registered records for debugging
18
47
  const records: Map<string, DdnsRecord> = new Map();
19
48
 
@@ -66,7 +95,7 @@ const server = Bun.serve({
66
95
  return xmlResponse(1, '', ['Missing required parameters: host, domain']);
67
96
  }
68
97
 
69
- if (password !== VALID_PASSWORD) {
98
+ if (password !== expectedPasswordFor(domain)) {
70
99
  return xmlResponse(1, ip, ['Passwords do not match']);
71
100
  }
72
101