From bb0193932fa68d9000980bef4900ec3514b4fd32 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Sat, 26 Sep 2026 23:21:27 +0000 Subject: [PATCH] Package Conduit Market as an Archipelago app Pure client-side Nostr+Lightning marketplace (apps/market from Conduit-BTC/conduit-mono) built into a static nginx image. Public/private is a genuine runtime toggle, not two separate builds: Vite bakes VITE_* relay config into the bundle at build time, so config.ts is patched (apply-runtime-override.py) to merge in a window.__CONDUIT_RUNTIME_ENV__ override written by docker-entrypoint.sh from CONDUIT_MODE / CONDUIT_PRIVATE_RELAY_URLS at container start -- flipping mode only needs a restart, not a rebuild. Verified end-to-end: build succeeds, the override survives bundling/minification, and public/private/missing-relay all produce the correct runtime-env.js (private with no relay URL refuses to start rather than silently falling back to the public network). Co-Authored-By: Claude Sonnet 5 --- apps/conduit-market/manifest.yml | 102 ++++++++++++++++++ docker/conduit-market/Dockerfile | 62 +++++++++++ .../conduit-market/apply-runtime-override.py | 70 ++++++++++++ docker/conduit-market/docker-entrypoint.sh | 43 ++++++++ docker/conduit-market/nginx.conf | 28 +++++ 5 files changed, 305 insertions(+) create mode 100644 apps/conduit-market/manifest.yml create mode 100644 docker/conduit-market/Dockerfile create mode 100755 docker/conduit-market/apply-runtime-override.py create mode 100644 docker/conduit-market/docker-entrypoint.sh create mode 100644 docker/conduit-market/nginx.conf diff --git a/apps/conduit-market/manifest.yml b/apps/conduit-market/manifest.yml new file mode 100644 index 00000000..01275f67 --- /dev/null +++ b/apps/conduit-market/manifest.yml @@ -0,0 +1,102 @@ +app: + id: conduit-market + name: Conduit Market + version: 1.0.0 + # Built by this project from a pinned upstream commit (Conduit ships no + # Dockerfile of its own) — there is no upstream release feed/tag to watch, + # so re-pin CONDUIT_COMMIT in the Dockerfile deliberately, by hand. + upstream: + kind: github + repo: Conduit-BTC/conduit-mono + description: | + Decentralized Nostr + Lightning marketplace (apps/market from + Conduit-BTC/conduit-mono). Pure client-side SPA — listings are NIP-99 + events, orders are encrypted DMs, payments settle over Lightning via + NWC — confirmed by reading packages/core/src/config.ts and + apps/market/package.json directly; there is no custom backend to run. + + Public/private is a genuine runtime toggle, not two separate builds: + Vite bakes VITE_* relay env vars into the bundle at compile time (see + the comment on getViteEnv() in config.ts), so a single image built once + could never be reconfigured afterward via ordinary env vars. This + package patches config.ts (docker/conduit-market/apply-runtime-override.py) + to merge in a window.__CONDUIT_RUNTIME_ENV__ override object, written by + docker-entrypoint.sh from CONDUIT_MODE/CONDUIT_PRIVATE_RELAY_URLS at + container *start*. Flipping CONDUIT_MODE and restarting the container is + enough — no rebuild required. CONDUIT_MODE=private with no relay URL set + refuses to start rather than silently falling back to the public network. + category: money + + container: + build: + context: /opt/archipelago/docker/conduit-market + dockerfile: Dockerfile + tag: localhost/conduit-market:local + network: archy-net + + dependencies: [] + + resources: + memory_limit: 64Mi + + security: + capabilities: [] + # false, not true: docker-entrypoint.sh regenerates runtime-env.js under + # /usr/share/nginx/html (part of the image root fs, not a volume) on + # every container start — that's the actual public/private switch, so + # the root fs must stay writable for the mode flip to take effect. + readonly_root: false + no_new_privileges: true + network_policy: bridge + + ports: + - host: 8091 + container: 8080 + protocol: tcp + bind: 127.0.0.1 + auth: none + auth_rationale: >- + Public storefront meant for anonymous shoppers, not an admin + surface — Conduit has no login of its own (identity is a Nostr + keypair held client-side) and a cookie/session gate in front of it + would just block real customers from browsing or checking out. + + volumes: [] + + # CONDUIT_MODE (public|private) and CONDUIT_PRIVATE_RELAY_URLS are the + # actual public/private switch, read by docker-entrypoint.sh at container + # start — see the description above. Defaults here are the safe/inert + # choice (public, using Conduit's own canonical relay network); an + # operator who wants an isolated single-relay storefront sets both. + environment: + - CONDUIT_MODE=public + - CONDUIT_PRIVATE_RELAY_URLS= + - CONDUIT_LIGHTNING_NETWORK=mainnet + + health_check: + type: http + endpoint: http://127.0.0.1:8091 + path: /health + interval: 30s + timeout: 5s + retries: 3 + + interfaces: + main: + name: Storefront + description: Conduit Market storefront + type: ui + port: 8091 + protocol: http + path: / + + metadata: + category: money + tier: optional + author: Conduit-BTC + repo: https://github.com/Conduit-BTC/conduit-mono + features: + - Nostr-native marketplace listings (NIP-99) + - Encrypted order flow over Nostr DMs + - Lightning checkout via Nostr Wallet Connect + - Runtime-configurable public or private relay mode, no rebuild diff --git a/docker/conduit-market/Dockerfile b/docker/conduit-market/Dockerfile new file mode 100644 index 00000000..30e405b2 --- /dev/null +++ b/docker/conduit-market/Dockerfile @@ -0,0 +1,62 @@ +# syntax=docker/dockerfile:1.6 +# +# Packages Conduit Market (github.com/Conduit-BTC/conduit-mono, apps/market) +# as a static Archipelago app. Conduit itself is a pure client-side SPA -- +# products are Nostr events (NIP-99), orders are encrypted DMs, payments go +# over Lightning via NWC -- no custom backend, confirmed by reading +# packages/core/src/config.ts and apps/market/package.json directly rather +# than assumed. Upstream ships no Dockerfile; this one is new. +# +# Pinned to a specific upstream commit (not a moving branch) for +# reproducibility, matching the archy convention already used for +# apps/cuprate. Re-pin deliberately, not on every rebuild. +# +# oven/bun:1.1-slim is pinned to an EOL Debian bullseye base whose +# -security pool no longer serves the package versions it references +# (404s on ca-certificates/curl et al). oven/bun:1-slim currently resolves +# to bun 1.4.x on Debian trixie (current), so use that instead -- still a +# floating minor tag, but the alternative (hand-pinning a bullseye/bookworm +# digest) just reintroduces the same staleness problem later. +FROM oven/bun:1-slim AS build + +ARG CONDUIT_COMMIT=c6606382dda8953763646f4498b2c4a4103da0a3 + +WORKDIR /build +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl python3 \ + && rm -rf /var/lib/apt/lists/* + +RUN curl -fsSL "https://github.com/Conduit-BTC/conduit-mono/archive/${CONDUIT_COMMIT}.tar.gz" \ + -o /tmp/conduit.tar.gz \ + && tar xzf /tmp/conduit.tar.gz --strip-components=1 \ + && rm /tmp/conduit.tar.gz + +# Full workspace install: apps/market depends on @conduit/core and +# @conduit/ui via workspace:*, so bun needs the whole monorepo present to +# link them, even though only the market app actually gets built below. +RUN bun install --frozen-lockfile + +COPY apply-runtime-override.py /build/apply-runtime-override.py +RUN python3 apply-runtime-override.py + +# Loads window.__CONDUIT_RUNTIME_ENV__ (written by docker-entrypoint.sh at +# container start) before the app bundle runs. A plain (non-module) script +# tag with an absolute path: Vite copies these through to dist/index.html +# verbatim without trying to resolve them as part of the module graph, and +# does not require the file to exist in the source tree at build time. +RUN sed -i 's##\n #' \ + apps/market/index.html + +# Leave every VITE_* relay/network var unset here on purpose -- the runtime +# override (see above) is the actual public/private switch; baking any of +# these in at build time would defeat the point of a live-configurable +# single image. +RUN bun run --filter '@conduit/market' build + +FROM nginx:1.27-alpine + +COPY --from=build /build/apps/market/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY docker-entrypoint.sh /docker-entrypoint.d/50-conduit-runtime-env.sh +RUN chmod +x /docker-entrypoint.d/50-conduit-runtime-env.sh + +EXPOSE 8080 diff --git a/docker/conduit-market/apply-runtime-override.py b/docker/conduit-market/apply-runtime-override.py new file mode 100755 index 00000000..dc4c7258 --- /dev/null +++ b/docker/conduit-market/apply-runtime-override.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Patches packages/core/src/config.ts so it can be reconfigured +public/private at container-start time instead of only at build time. + +Vite bakes VITE_* env vars into the bundle as literal strings when it +compiles (import.meta.env.VITE_FOO is a static replacement, not a runtime +lookup) -- confirmed directly in config.ts's own comment on getViteEnv(). +That means a container image built once can never be flipped between an +isolated private relay and the public Conduit network via ordinary env +vars after the fact; the values are frozen into the compiled JS. + +This packaging needs exactly that live toggle, so docker-entrypoint.sh +writes a small /runtime-env.js at *container start* (from real env vars) +setting window.__CONDUIT_RUNTIME_ENV__ before the app bundle runs. This +script inserts one small override function right before the single call +site that consumes getViteEnv()'s result (`const env = getViteEnv()`, +confirmed to be the only call site in the file and to run once at module +load), so only that one merge point needs to change. Only non-empty +string fields override the Vite-baked default; a build with no runtime +override behaves identically to stock upstream Conduit. + +Uses a literal string anchor rather than a line-numbered diff/patch -- +more robust against upstream reformatting a comment or reflowing +unrelated lines than a traditional unified diff would be. +""" +import sys + +CONFIG_PATH = "packages/core/src/config.ts" +ANCHOR = "const env = getViteEnv()" + +OVERRIDE_BLOCK = '''// --- Archipelago runtime relay override ------------------------------------- +// See docker/conduit-market/apply-runtime-override.py in the archy repo for +// why this exists: Vite bakes VITE_* env vars into the bundle at build +// time, so a container image built once can never be reconfigured +// public/private afterward via ordinary env vars. docker-entrypoint.sh +// writes window.__CONDUIT_RUNTIME_ENV__ from real environment variables at +// container start, before this bundle runs. +function getRuntimeOverrides(): Partial> { + if (typeof window === "undefined") return {} + const raw = (window as unknown as Record) + .__CONDUIT_RUNTIME_ENV__ + if (!raw || typeof raw !== "object") return {} + const out: Record = {} + for (const [key, value] of Object.entries(raw as Record)) { + if (typeof value === "string" && value.length > 0) out[key] = value + } + return out as Partial> +} + +const env = { ...getViteEnv(), ...getRuntimeOverrides() }''' + +with open(CONFIG_PATH) as f: + content = f.read() + +count = content.count(ANCHOR) +if count != 1: + print( + f"FATAL: expected exactly 1 occurrence of anchor line in {CONFIG_PATH}, " + f"found {count}. Upstream config.ts has likely changed in a way that " + "needs this script re-checked by hand before it can safely patch it.", + file=sys.stderr, + ) + sys.exit(1) + +content = content.replace(ANCHOR, OVERRIDE_BLOCK, 1) + +with open(CONFIG_PATH, "w") as f: + f.write(content) + +print(f"Patched {CONFIG_PATH}: runtime relay override installed.") diff --git a/docker/conduit-market/docker-entrypoint.sh b/docker/conduit-market/docker-entrypoint.sh new file mode 100644 index 00000000..09a38756 --- /dev/null +++ b/docker/conduit-market/docker-entrypoint.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# Runs via nginx's own /docker-entrypoint.d/ hook mechanism (official nginx +# image sources every executable script there before starting nginx) -- +# no custom ENTRYPOINT needed. Writes /runtime-env.js, loaded by index.html +# before the app bundle (see Dockerfile), so this container's own env vars +# can flip Conduit between an isolated private relay and the public Conduit +# network without rebuilding the image -- see +# packages/core/src/config.ts's getRuntimeOverrides() (installed by +# apply-runtime-override.py) for the other half of this. +set -eu + +CONDUIT_MODE="${CONDUIT_MODE:-public}" +OUT=/usr/share/nginx/html/runtime-env.js + +if [ "$CONDUIT_MODE" = "private" ]; then + if [ -z "${CONDUIT_PRIVATE_RELAY_URLS:-}" ]; then + echo "conduit-market: CONDUIT_MODE=private requires CONDUIT_PRIVATE_RELAY_URLS (comma-separated wss:// URLs) -- refusing to start with no relay configured" >&2 + exit 1 + fi + cat > "$OUT" < "$OUT" < + # private) followed by a container restart would silently keep serving + # the old relay config to anyone with a warm cache. + location = /runtime-env.js { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + expires 0; + } + + # Client-side routing (@tanstack/react-router) -- unknown paths fall + # through to index.html rather than 404ing. + location / { + try_files $uri $uri/ /index.html; + } + + location = /health { + access_log off; + default_type text/plain; + return 200 "ok\n"; + } +}