Compare commits

..
Author SHA1 Message Date
ssmithxandClaude Sonnet 5 bb0193932f 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 <noreply@anthropic.com>
2026-09-26 23:21:27 +00:00
5 changed files with 305 additions and 0 deletions
+102
View File
@@ -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
+62
View File
@@ -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#<script type="module" src="/src/main.tsx"></script>#<script src="/runtime-env.js"></script>\n <script type="module" src="/src/main.tsx"></script>#' \
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
+70
View File
@@ -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<ReturnType<typeof getViteEnv>> {
if (typeof window === "undefined") return {}
const raw = (window as unknown as Record<string, unknown>)
.__CONDUIT_RUNTIME_ENV__
if (!raw || typeof raw !== "object") return {}
const out: Record<string, string> = {}
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
if (typeof value === "string" && value.length > 0) out[key] = value
}
return out as Partial<ReturnType<typeof getViteEnv>>
}
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.")
@@ -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" <<EOF
window.__CONDUIT_RUNTIME_ENV__ = {
relayUrl: "",
defaultRelayUrl: "${CONDUIT_PRIVATE_RELAY_URLS}",
defaultRelays: "${CONDUIT_PRIVATE_RELAY_URLS}",
appWriteRelayUrls: "${CONDUIT_PRIVATE_RELAY_URLS}",
publicRelayUrls: "${CONDUIT_PRIVATE_RELAY_URLS}",
commerceRelayUrls: "${CONDUIT_PRIVATE_RELAY_URLS}",
lightningNetwork: "${CONDUIT_LIGHTNING_NETWORK:-mainnet}"
};
EOF
echo "conduit-market: CONDUIT_MODE=private -- isolated to ${CONDUIT_PRIVATE_RELAY_URLS}"
else
# public: no relay overrides at all, so Conduit's own built-in canonical
# public relay defaults (packages/core/src/config.ts,
# CANONICAL_APP_RELAY_DEFINITIONS) apply exactly as they do upstream --
# this store fully participates in the wider Conduit network.
cat > "$OUT" <<EOF
window.__CONDUIT_RUNTIME_ENV__ = {
lightningNetwork: "${CONDUIT_LIGHTNING_NETWORK:-mainnet}"
};
EOF
echo "conduit-market: CONDUIT_MODE=public -- using Conduit's default public relay network"
fi
+28
View File
@@ -0,0 +1,28 @@
server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Regenerated by docker-entrypoint.d/50-conduit-runtime-env.sh on every
# container start -- must never be cached, or a mode flip (public <->
# 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";
}
}