@celilo/e2e 0.6.0 → 0.6.2

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 (43) hide show
  1. package/README.md +2 -1
  2. package/config/dns/celilo.computer.zone +16 -0
  3. package/config/dns/iamtheinternet.org.zone +6 -4
  4. package/config/dns/knot-namecheap.conf +8 -0
  5. package/config/dns/knot-tld.conf +4 -0
  6. package/config/dns/lunacycle.net.zone +16 -0
  7. package/config/dns/net.zone +11 -0
  8. package/config/dns/org.zone +5 -0
  9. package/config/dns/root.zone +2 -0
  10. package/config/dns/templates/iamtheinternet.org.zone +16 -0
  11. package/config/dns/templates/lunacycle.net.zone +16 -0
  12. package/config/resolver/unbound.conf +10 -0
  13. package/config/routing/management-routes.sh +19 -0
  14. package/config/routing/resolver-internal-routes.sh +59 -0
  15. package/docker/Dockerfile.internal-resolver +16 -0
  16. package/docker/Dockerfile.isitup +37 -0
  17. package/docker/Dockerfile.registry +34 -0
  18. package/netapps/authentik.netapp +0 -0
  19. package/netapps/caddy.netapp +0 -0
  20. package/netapps/greenwave.netapp +0 -0
  21. package/netapps/homebridge.netapp +0 -0
  22. package/netapps/iptables.netapp +0 -0
  23. package/netapps/knot-unbound-internal.netapp +0 -0
  24. package/netapps/namecheap.netapp +0 -0
  25. package/netapps/technitium.netapp +0 -0
  26. package/netapps/test-ansible-output.netapp +0 -0
  27. package/package.json +2 -1
  28. package/simulators/isitup/Caddyfile +11 -0
  29. package/simulators/isitup/entrypoint.sh +47 -0
  30. package/simulators/isitup/server.ts +112 -0
  31. package/simulators/namecheap-ddns/server.ts +31 -2
  32. package/src/cli/build.ts +17 -3
  33. package/src/cli/command-registry.ts +3 -2
  34. package/src/cli/completion.ts +29 -5
  35. package/src/cli/index.ts +85 -15
  36. package/src/cli/module-discovery.ts +120 -0
  37. package/src/cli/scaffold.ts +4 -5
  38. package/src/container-manager.ts +358 -71
  39. package/src/docker-compose-generator.ts +119 -17
  40. package/src/parse-line.ts +14 -3
  41. package/src/runner.ts +122 -165
  42. package/src/simulator-ips.ts +42 -0
  43. package/src/types.ts +24 -0
package/README.md CHANGED
@@ -187,7 +187,8 @@ c machine add 192.168.0.254 --ssh-user root --earmark iptables
187
187
  c machine add 10.0.10.10 --ssh-user root
188
188
 
189
189
  # Pre-configure secrets and non-derivable config
190
- c module secret set namecheap ddns_password test123
190
+ c module config set namecheap domains '["iamtheinternet.org"]'
191
+ c module secret set namecheap ddns_passwords '{"iamtheinternet.org":"test123"}'
191
192
  c module config set greenwave router_ip 192.168.0.1
192
193
  c module secret set greenwave router_username admin
193
194
  c module secret set greenwave router_password admin
@@ -0,0 +1,16 @@
1
+ $ORIGIN celilo.computer.
2
+ $TTL 300
3
+
4
+ @ IN SOA ns1.celilo.computer. admin.celilo.computer. (
5
+ 2024020117 300 60 604800 300
6
+ )
7
+
8
+ ; Public DNS — RFC 1918 addresses NEVER appear here. The whole point of
9
+ ; the e2e simulation is to model real internet connectivity, where
10
+ ; private IPs are unreachable from outside. All public-facing names
11
+ ; resolve to the firewall's external IP (100.100.0.100), which DNATs
12
+ ; inbound to the right internal host.
13
+ @ IN NS ns1.celilo.computer.
14
+ ns1 IN A 100.64.0.55
15
+ @ IN A 100.100.0.100
16
+ www IN A 100.100.0.100
@@ -2,14 +2,16 @@ $ORIGIN iamtheinternet.org.
2
2
  $TTL 300
3
3
 
4
4
  @ IN SOA ns1.iamtheinternet.org. admin.iamtheinternet.org. (
5
- 2024010821 300 60 604800 300
5
+ 2024020250 300 60 604800 300
6
6
  )
7
7
 
8
+ ; Public DNS — RFC 1918 addresses NEVER appear here. The whole point of
9
+ ; the e2e simulation is to model real internet connectivity, where
10
+ ; private IPs are unreachable from outside. All public-facing names
11
+ ; resolve to the firewall's external IP (100.100.0.100), which DNATs
12
+ ; inbound to the right internal host.
8
13
  @ IN NS ns1.iamtheinternet.org.
9
14
  ns1 IN A 100.64.0.55
10
15
  @ IN A 0.0.0.0
11
- caddy IN A 100.100.0.100
12
16
  www IN A 100.100.0.100
13
- deathstar IN A 100.100.0.100
14
- test IN A 0.0.0.0
15
17
  auth IN A 100.100.0.100
@@ -6,6 +6,14 @@ zone:
6
6
  file: "/config/iamtheinternet.org.zone"
7
7
  storage: "/var/lib/knot"
8
8
 
9
+ - domain: "celilo.computer"
10
+ file: "/config/celilo.computer.zone"
11
+ storage: "/var/lib/knot"
12
+
13
+ - domain: "lunacycle.net"
14
+ file: "/config/lunacycle.net.zone"
15
+ storage: "/var/lib/knot"
16
+
9
17
  - domain: "park-your-domain.com"
10
18
  file: "/config/park-your-domain.com.zone"
11
19
  storage: "/var/lib/knot"
@@ -9,3 +9,7 @@ zone:
9
9
  - domain: "org"
10
10
  file: "/config/org.zone"
11
11
  storage: "/var/lib/knot"
12
+
13
+ - domain: "net"
14
+ file: "/config/net.zone"
15
+ storage: "/var/lib/knot"
@@ -0,0 +1,16 @@
1
+ $ORIGIN lunacycle.net.
2
+ $TTL 300
3
+
4
+ @ IN SOA ns1.lunacycle.net. admin.lunacycle.net. (
5
+ 2024020108 300 60 604800 300
6
+ )
7
+
8
+ ; Public DNS — RFC 1918 addresses NEVER appear here. The whole point of
9
+ ; the e2e simulation is to model real internet connectivity, where
10
+ ; private IPs are unreachable from outside. All public-facing names
11
+ ; resolve to the firewall's external IP (100.100.0.100), which DNATs
12
+ ; inbound to the right internal host.
13
+ @ IN NS ns1.lunacycle.net.
14
+ ns1 IN A 100.64.0.55
15
+ @ IN A 100.100.0.100
16
+ www IN A 100.100.0.100
@@ -0,0 +1,11 @@
1
+ $ORIGIN net.
2
+ $TTL 86400
3
+
4
+ @ IN SOA tld-dns.lab. admin.lab. (
5
+ 2024010101 1800 900 604800 86400
6
+ )
7
+
8
+ net. IN NS tld-dns.lab.
9
+
10
+ lunacycle IN NS ns1.lunacycle.net.
11
+ ns1.lunacycle.net. IN A 100.64.0.55
@@ -10,5 +10,10 @@ org. IN NS tld-dns.lab.
10
10
  iamtheinternet IN NS ns1.iamtheinternet.org.
11
11
  ns1.iamtheinternet.org. IN A 100.64.0.55
12
12
 
13
+ ; External prober simulator — mimics isitup.org's JSON API.
14
+ ; Served directly by tld-dns rather than delegated, since we only need a
15
+ ; single A record (no subdomains, no SOA of its own).
16
+ isitup IN A 100.64.0.57
17
+
13
18
  ; Note: Let's Encrypt ACME endpoints are handled by unbound local-data
14
19
  ; (resolves acme-v02.api.letsencrypt.org → 100.64.0.100 directly)
@@ -12,3 +12,5 @@ com. IN NS tld-dns.lab.
12
12
  tld-dns.lab. IN A 100.64.0.54
13
13
 
14
14
  org. IN NS tld-dns.lab.
15
+
16
+ net. IN NS tld-dns.lab.
@@ -0,0 +1,16 @@
1
+ $ORIGIN iamtheinternet.org.
2
+ $TTL 300
3
+
4
+ @ IN SOA ns1.iamtheinternet.org. admin.iamtheinternet.org. (
5
+ 2024020247 300 60 604800 300
6
+ )
7
+
8
+ ; Public DNS — RFC 1918 addresses NEVER appear here. The whole point of
9
+ ; the e2e simulation is to model real internet connectivity, where
10
+ ; private IPs are unreachable from outside. All public-facing names
11
+ ; resolve to the firewall's external IP (100.100.0.100), which DNATs
12
+ ; inbound to the right internal host.
13
+ @ IN NS ns1.iamtheinternet.org.
14
+ ns1 IN A 100.64.0.55
15
+ @ IN A 0.0.0.0
16
+ www IN A 100.100.0.100
@@ -0,0 +1,16 @@
1
+ $ORIGIN lunacycle.net.
2
+ $TTL 300
3
+
4
+ @ IN SOA ns1.lunacycle.net. admin.lunacycle.net. (
5
+ 2024020108 300 60 604800 300
6
+ )
7
+
8
+ ; Public DNS — RFC 1918 addresses NEVER appear here. The whole point of
9
+ ; the e2e simulation is to model real internet connectivity, where
10
+ ; private IPs are unreachable from outside. All public-facing names
11
+ ; resolve to the firewall's external IP (100.100.0.100), which DNATs
12
+ ; inbound to the right internal host.
13
+ @ IN NS ns1.lunacycle.net.
14
+ ns1 IN A 100.64.0.55
15
+ @ IN A 100.100.0.100
16
+ www IN A 100.100.0.100
@@ -11,12 +11,22 @@ server:
11
11
  # Let's Encrypt ACME endpoints → Pebble
12
12
  local-data: "acme-v02.api.letsencrypt.org. IN A 100.64.0.100"
13
13
  local-data: "acme-staging-v02.api.letsencrypt.org. IN A 100.64.0.100"
14
+ # E2E registry container
15
+ local-data: "e2e-registry.lab. IN A 100.64.0.56"
16
+ # External website prober simulator — without this override, isitup.org
17
+ # falls through the "forward-zone: ." below and resolves to the REAL
18
+ # Namecheap-parked domain on the public internet.
19
+ local-data: "isitup.org. IN A 100.64.0.57"
14
20
 
15
21
  # Simulated domains → namecheap-dns (authoritative)
16
22
  stub-zone:
17
23
  name: "iamtheinternet.org."
18
24
  stub-addr: 100.64.0.55
19
25
 
26
+ stub-zone:
27
+ name: "lunacycle.net."
28
+ stub-addr: 100.64.0.55
29
+
20
30
  stub-zone:
21
31
  name: "park-your-domain.com."
22
32
  stub-addr: 100.64.0.55
@@ -34,6 +34,25 @@ ip route add 10.0.30.0/24 via 192.168.0.254 # secure via fw-main
34
34
  # Fall back to the simulated public resolver if Technitium is not yet up.
35
35
  printf 'nameserver 192.168.0.10\nnameserver 100.100.0.1\n' > /etc/resolv.conf
36
36
 
37
+ # Fetch Pebble's runtime ACME root CA (Pebble generates a fresh root at each
38
+ # startup, unlike the static /config/pebble/pebble-ca.crt baked in at build
39
+ # time which only signs Pebble's own API endpoint). Without this, management
40
+ # can't verify certs that Caddy obtained from Pebble (iamtheinternet.org,
41
+ # isitup.org, etc.). Mirrors the equivalent block in fw-ext-routes.sh.
42
+ echo "Waiting for Pebble ACME root CA..."
43
+ (
44
+ for i in $(seq 1 60); do
45
+ 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
46
+ if [ -s /usr/local/share/ca-certificates/pebble-acme-root.crt ]; then
47
+ update-ca-certificates 2>/dev/null
48
+ echo "Pebble ACME root CA installed"
49
+ break
50
+ fi
51
+ fi
52
+ sleep 2
53
+ done
54
+ ) &
55
+
37
56
  # Create celilo data directory (Linux default: /var/lib/celilo/)
38
57
  mkdir -p /var/lib/celilo/modules
39
58
 
@@ -0,0 +1,59 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # CADDY_IP: IP of the caddy container in the DMZ (default 10.0.10.10).
5
+ # All test-domain queries resolve to this IP so management can reach
6
+ # caddy directly without hairpin NAT through the external interface.
7
+ CADDY_IP="${CADDY_IP:-10.0.10.10}"
8
+
9
+ # DOMAINS: comma-separated list of e2e test domains that should be
10
+ # split-horizon redirected to caddy. Default covers the canonical
11
+ # (iamtheinternet.org) plus the cross-domain test's second domain
12
+ # (lunacycle.net). Tests can override via DEFAULT_GATEWAY/DOMAINS env
13
+ # vars in docker-compose.
14
+ DOMAINS="${DOMAINS:-iamtheinternet.org,lunacycle.net}"
15
+
16
+ # Routing — reach comcast-resolver (100.100.0.1) via fw-main or fw-isp.
17
+ # DEFAULT_GATEWAY is set by docker-compose from the topology config.
18
+ GATEWAY="${DEFAULT_GATEWAY:-192.168.0.254}"
19
+ ip route del default 2>/dev/null || true
20
+ ip route add default via "$GATEWAY"
21
+
22
+ # Build per-domain split-horizon blocks.
23
+ # local-zone "redirect" returns the single A record for ALL names under
24
+ # the zone (including subdomains), so every hostname in the test domain
25
+ # reaches caddy which then reverse-proxies to the correct backend.
26
+ SPLIT_HORIZON_BLOCK=""
27
+ IFS=',' read -ra DOMAIN_LIST <<< "${DOMAINS}"
28
+ for d in "${DOMAIN_LIST[@]}"; do
29
+ SPLIT_HORIZON_BLOCK="${SPLIT_HORIZON_BLOCK} local-zone: \"${d}.\" redirect\n local-data: \"${d}. IN A ${CADDY_IP}\"\n"
30
+ done
31
+
32
+ # Generate unbound config with split-horizon for each test domain plus
33
+ # the standard simulator overrides (Pebble, registry).
34
+ cat > /etc/unbound/unbound.conf << EOF
35
+ server:
36
+ interface: 0.0.0.0
37
+ access-control: 0.0.0.0/0 allow
38
+ cache-min-ttl: 0
39
+ cache-max-ttl: 60
40
+ verbosity: 1
41
+ do-daemonize: no
42
+ do-not-query-localhost: no
43
+
44
+ $(printf "${SPLIT_HORIZON_BLOCK}")
45
+ # Mirror the public resolver's simulated endpoint overrides so that
46
+ # management uses Pebble for ACME and the e2e registry for modules.
47
+ local-data: "acme-v02.api.letsencrypt.org. IN A 100.64.0.100"
48
+ local-data: "acme-staging-v02.api.letsencrypt.org. IN A 100.64.0.100"
49
+ local-data: "e2e-registry.lab. IN A 100.64.0.56"
50
+
51
+ forward-zone:
52
+ name: "."
53
+ forward-addr: 100.100.0.1
54
+ EOF
55
+
56
+ /usr/sbin/unbound -d &
57
+
58
+ echo "internal resolver ready (split-horizon: ${DOMAINS} -> ${CADDY_IP})"
59
+ sleep infinity
@@ -0,0 +1,16 @@
1
+ FROM ubuntu:22.04
2
+
3
+ RUN apt-get update && apt-get install -y \
4
+ unbound \
5
+ iproute2 \
6
+ iputils-ping \
7
+ net-tools \
8
+ dnsutils \
9
+ procps \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ COPY config/routing/resolver-internal-routes.sh /startup.sh
13
+ RUN chmod +x /startup.sh
14
+
15
+ EXPOSE 53/tcp 53/udp
16
+ CMD ["/startup.sh"]
@@ -0,0 +1,37 @@
1
+ FROM oven/bun:latest
2
+
3
+ # Caddy (for ACME/TLS termination) + tools needed by entrypoint.sh.
4
+ # iproute2 provides `ip` so the entrypoint can rewrite the default route
5
+ # through fw-ext (same pattern as Dockerfile.pebble); without it DNS for
6
+ # simulated public hostnames fails.
7
+ RUN apt-get update && apt-get install -y \
8
+ ca-certificates \
9
+ curl \
10
+ iproute2 \
11
+ debian-keyring \
12
+ debian-archive-keyring \
13
+ apt-transport-https \
14
+ gnupg \
15
+ && curl -fsSL 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
16
+ | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg \
17
+ && curl -fsSL 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
18
+ > /etc/apt/sources.list.d/caddy-stable.list \
19
+ && apt-get update && apt-get install -y caddy \
20
+ && rm -rf /var/lib/apt/lists/*
21
+
22
+ # Trust Pebble CA so Caddy's ACME client accepts the directory endpoint
23
+ COPY config/pebble/pebble-ca.crt /usr/local/share/ca-certificates/pebble-ca.crt
24
+ RUN update-ca-certificates
25
+
26
+ # Simulator app
27
+ COPY simulators/isitup/server.ts /simulator/server.ts
28
+
29
+ # Caddy config
30
+ COPY simulators/isitup/Caddyfile /etc/caddy/Caddyfile
31
+
32
+ # Entrypoint
33
+ COPY simulators/isitup/entrypoint.sh /entrypoint.sh
34
+ RUN chmod +x /entrypoint.sh
35
+
36
+ EXPOSE 80 443
37
+ CMD ["/entrypoint.sh"]
@@ -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"]
Binary file
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.6.0",
3
+ "version": "0.6.2",
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",
@@ -31,6 +31,7 @@
31
31
  "README.md"
32
32
  ],
33
33
  "dependencies": {
34
+ "@celilo/cli-display": "0.1.4",
34
35
  "yaml": "^2.8.0"
35
36
  },
36
37
  "devDependencies": {
@@ -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
 
package/src/cli/build.ts CHANGED
@@ -110,17 +110,27 @@ function buildDockerImages(pkgDir: string): void {
110
110
  const total = dockerfiles.length;
111
111
  const buildStart = Date.now();
112
112
 
113
+ // Per-Dockerfile build-context overrides. Default context is the e2e
114
+ // package root. Dockerfiles that need access to sibling packages (e.g.
115
+ // Dockerfile.registry COPYs from packages/registry-server) get `..`
116
+ // (= packages/). Must match the compose-time context in
117
+ // docker-compose-generator.ts.
118
+ const contextOverrides: Record<string, string> = {
119
+ 'Dockerfile.registry': '..',
120
+ };
121
+
113
122
  for (let i = 0; i < total; i++) {
114
123
  const file = dockerfiles[i];
115
124
  const name = file.replace('Dockerfile.', '');
116
125
  const tag = `celilo-e2e/${name}`;
126
+ const context = contextOverrides[file] ?? '.';
117
127
 
118
128
  process.stdout.write(` [${i + 1}/${total}] ${String(name).padEnd(25)} `);
119
129
 
120
130
  const start = Date.now();
121
131
  const result = spawnSync(
122
132
  'docker',
123
- ['build', '-t', tag, '-f', join(dockerDir, file), '.'],
133
+ ['build', '-t', tag, '-f', join(dockerDir, file), context],
124
134
  { cwd: pkgDir, stdio: 'pipe' },
125
135
  );
126
136
  const elapsed = Math.round((Date.now() - start) / 1000);
@@ -178,11 +188,15 @@ export function runBuild(options: BuildOptions): void {
178
188
  mkdirSync(netappsDir, { recursive: true });
179
189
 
180
190
  if (!skipModules) {
181
- // Explicit dirs take precedence; fall back to auto-discovering all modules.
191
+ // Explicit dirs take precedence; fall back to auto-discovering all
192
+ // modules. Try CWD first so a globally-installed cele2e finds the
193
+ // user's monorepo (the package's own dir is in
194
+ // ~/.bun/install/global/... and won't have a `modules/` sibling).
195
+ // Fall back to the package dir for the in-monorepo dev case.
182
196
  const dirs = moduleDirs.length > 0
183
197
  ? moduleDirs
184
198
  : (() => {
185
- const root = findMonorepoRoot(pkgDir);
199
+ const root = findMonorepoRoot(process.cwd()) ?? findMonorepoRoot(pkgDir);
186
200
  return root ? discoverModuleDirs(root) : [];
187
201
  })();
188
202