diff --git a/apps/PORTS.md b/apps/PORTS.md index d3fc4f0f..fab15e1f 100644 --- a/apps/PORTS.md +++ b/apps/PORTS.md @@ -25,6 +25,7 @@ This document lists all port assignments for Archipelago apps. | did-wallet | 8083 | TCP | Web UI | 18083 | | router | 8084, 5353, 1900 | TCP/UDP | Web UI, mDNS, SSDP | 18084, 15353, 11900 | | meshtastic | 4403, 1883 | TCP | HTTP API, MQTT | 14403, 11883 | +| podsteadr | 8095, 1935, 8889, 8189, 8890, 8098 | TCP/UDP | Web UI/API/RSS, RTMP ingest, WebRTC/WHIP ingest, WebRTC ICE (udp), HLS playback, Blossom blobs | 18095, 11935, 18889, 18189, 18890, 18098 | ## Development Ports (Offset: +10000) diff --git a/apps/README.md b/apps/README.md index 05f1cacd..d2f077d0 100644 --- a/apps/README.md +++ b/apps/README.md @@ -20,6 +20,7 @@ Containerized applications for the Archipelago Bitcoin Node OS. All apps run in - **did-wallet** — Web5 DID Wallet ### Self-Hosted Services +- **podsteadr** — Nostr-native podcast publishing and livestreaming (RTMP/WebRTC ingest, HLS, RSS, Blossom media) - **nextcloud** (v28), **jellyfin** (v10.8.13), **immich** (release), **photoprism** (v240915) - **vaultwarden** (v1.30.0-alpine), **penpot** (v2.4) - **homeassistant** (v2024.1), **filebrowser** (v2.27.0), **searxng** (2024.11.17) diff --git a/apps/podsteadr-blossom/manifest.yml b/apps/podsteadr-blossom/manifest.yml new file mode 100644 index 00000000..8d559668 --- /dev/null +++ b/apps/podsteadr-blossom/manifest.yml @@ -0,0 +1,125 @@ +app: + id: podsteadr-blossom + name: podsteadr Blossom + version: "4" + description: Blossom (BUD-02) sha256-addressed media blob server backing podsteadr's episode uploads and covers. + category: media + + # Hyphenated name matches the podsteadr repo's docker-compose container_name + # (podsteadr-blossom); alias `blossom` is the short hostname podsteadr's + # server reaches it by (BLOSSOM_URL_INTERNAL=http://blossom:3000). + container_name: podsteadr-blossom + + container: + image: ghcr.io/hzrd149/blossom-server:4 + pull_policy: if-not-present + network: podsteadr-net + network_aliases: [blossom] + # Image runs as container-root (no USER directive) writing to a + # bind-mounted /app/data — CHOWN/DAC_OVERRIDE cover the fresh-bind-dir + # ownership gap the same way apps/botfights and apps/immich document. + # Unverified against a real install; check first-boot logs. + data_uid: "0:0" + + dependencies: + - storage: 20Gi + + resources: + cpu_limit: 1 + memory_limit: 512Mi + disk_limit: 20Gi + + security: + capabilities: [CHOWN, DAC_OVERRIDE, FOWNER] + readonly_root: false + no_new_privileges: true + network_policy: isolated + + ports: + - host: 8098 + container: 3000 + protocol: tcp + auth: none + auth_rationale: >- + Media blobs (episode audio/video, covers) must be publicly fetchable + by podcast clients as RSS enclosure URLs — that's the entire purpose + of this port. Uploads are separately gated by blossom's own BUD-02 + signed-nostr-event auth (upload.requireAuth below), not a node + session; reads are intentionally public per the config's own header + comment. + + volumes: + - type: bind + source: /var/lib/archipelago/podsteadr-blossom/data + target: /app/data + options: [rw] + - type: bind + source: /var/lib/archipelago/podsteadr-blossom/config/config.yml + target: /app/config.yml + options: [ro] + + environment: [] + + files: + - path: /var/lib/archipelago/podsteadr-blossom/config/config.yml + overwrite: true + content: | + # blossom-server (v4.x) configuration for podsteadr. + # Uploads require a signed nostr auth event (BUD-02, kind 24242); + # reads are public so podcast apps can fetch enclosures. + # + # NOTE (blossom-server 4.4.1 gotcha, do not rediscover): `rules:` MUST + # be nested under `storage:` — a top-level `rules:` key is silently + # ignored, the ruleset ends up empty, and every upload fails 401 + # "Server dose not accept video/mp4 blobs" (typo is theirs). The + # GitHub master branch is a Deno rewrite with a different schema + # (storage.rules, BUD-11, range support); the `:4` image is the older + # node/koa codebase this config targets. + + publicDomain: "" + + databasePath: data/sqlite.db + + dashboard: + enabled: false + + discovery: + nostr: + enabled: false + relays: [] + upstream: + enabled: false + domains: [] + + storage: + backend: local + local: + dir: ./data/blobs + removeWhenNoOwners: false + # "expiration" is time since a blob was last accessed — unaccessed + # blobs get pruned after this. Podcast media should effectively + # never expire, so keep this long. + rules: + - type: "*" + expiration: 10 years + + upload: + enabled: true + requireAuth: true + requirePubkeyInRule: false + + list: + requireAuth: false + allowListOthers: true + + tor: + enabled: false + proxy: "" + + health_check: + # No documented health endpoint; TCP liveness on the app port. + type: tcp + endpoint: localhost:3000 + interval: 30s + timeout: 5s + retries: 3 diff --git a/apps/podsteadr-mediamtx/manifest.yml b/apps/podsteadr-mediamtx/manifest.yml new file mode 100644 index 00000000..18c6cd6a --- /dev/null +++ b/apps/podsteadr-mediamtx/manifest.yml @@ -0,0 +1,164 @@ +app: + id: podsteadr-mediamtx + name: podsteadr MediaMTX + version: "1.19.2" + description: MediaMTX ingest/output backend for podsteadr — RTMP + WebRTC/WHIP ingest, HLS playback, stream recording. + category: media + + # Hyphenated name matches the podsteadr repo's docker-compose container_name + # (podsteadr-mediamtx); alias `mediamtx` is the short hostname podsteadr's + # server reaches it by (MEDIAMTX_API_URL=http://mediamtx:9997) and the one + # baked into mediamtx.yml's authHTTPAddress callback below. + container_name: podsteadr-mediamtx + + container: + image: docker.io/bluenviron/mediamtx:1.19.2 + pull_policy: if-not-present + network: podsteadr-net + network_aliases: [mediamtx] + derived_env: + # Browsers need a reachable ICE host candidate for WebRTC/WHIP; without + # this, the offer only advertises container-internal addresses and + # publish/playback negotiation fails for anyone off-host. + - key: MTX_WEBRTCADDITIONALHOSTS + template: "{{HOST_MDNS}}" + + dependencies: + - storage: 10Gi + + resources: + cpu_limit: 1 + memory_limit: 512Mi + disk_limit: 10Gi + + security: + # Stock mediamtx image runs as container-root (no USER directive) but + # only ever writes to the bind-mounted /recordings — CHOWN/DAC_OVERRIDE + # cover the fresh-bind-dir-ownership gap the same way apps/botfights and + # apps/immich document (root uid inside the container does not + # automatically bypass DAC checks once cap-drop ALL applies). Unverified + # against a real install; check first-boot logs on initial deploy. + capabilities: [CHOWN, DAC_OVERRIDE] + readonly_root: true + no_new_privileges: true + network_policy: isolated + + ports: + - host: 1935 + container: 1935 + protocol: tcp + auth: none + auth_rationale: >- + RTMP ingest (OBS). Not HTTP, so the node's session gate has no login + page to serve here; publish auth is delegated to podsteadr's own + HTTP auth webhook (authHTTPAddress below), which checks a per-stream + secret key never exposed in this port mapping. + - host: 8889 + container: 8889 + protocol: tcp + auth: none + auth_rationale: >- + WebRTC/WHIP ingest — browsers publish directly with a per-stream + bearer secret checked by podsteadr's auth webhook, the same + protocol-level auth as the RTMP port above. + - host: 8189 + container: 8189 + protocol: udp + auth: none + auth_rationale: >- + WebRTC ICE/UDP media transport. Raw UDP has no HTTP session concept + for the gate to enforce. + - host: 8890 + container: 8888 + protocol: tcp + auth: none + auth_rationale: >- + Public HLS playback URL, handed out to viewers and podcast/livestream + clients outside the node (zap.stream, third-party players). A login + page here would break every external viewer; playback is read-only. + + volumes: + - type: bind + # Shared with apps/podsteadr (mounted read-only there) so the app can + # list and remux finished recordings for one-click episode publishing. + source: /var/lib/archipelago/podsteadr/recordings + target: /recordings + options: [rw] + - type: bind + source: /var/lib/archipelago/podsteadr-mediamtx/config/mediamtx.yml + target: /mediamtx.yml + options: [ro] + + environment: [] + + files: + - path: /var/lib/archipelago/podsteadr-mediamtx/config/mediamtx.yml + overwrite: true + content: | + # MediaMTX configuration for podsteadr. + # Ingest: RTMP (OBS) + WebRTC/WHIP (browser). Output: HLS. Publish auth is + # delegated to podsteadr via HTTP; stream status is polled from the API. + + logLevel: info + + api: yes + apiAddress: :9997 + + # ---- authentication ------------------------------------------------------ + authMethod: http + authHTTPAddress: http://podsteadr-app:8095/api/mediamtx/auth + authHTTPExclude: + - action: api + - action: metrics + - action: pprof + + # ---- protocols ----------------------------------------------------------- + rtsp: no + srt: no + moq: no + + rtmp: yes + rtmpAddress: :1935 + + hls: yes + hlsAddress: :8888 + # Standard HLS, not lowLatency: LL-HLS's small per-part buffering window has very little + # tolerance for B-frame reordering (common in most OBS encoder presets), and a real test + # stream crashed the muxer twice in ~2 minutes with "too many reordered frames" / "unable to + # extract DTS" once frame timing got even slightly irregular. Standard HLS buffers a full + # segment before finalizing, which absorbs that jitter — a few extra seconds of latency + # instead of intermittent muxer crashes / viewer buffering. + hlsVariant: mpegts + hlsAlwaysRemux: yes + hlsAllowOrigins: ["*"] + + webrtc: yes + webrtcAddress: :8889 + webrtcLocalUDPAddress: :8189 + webrtcAllowOrigins: ["*"] + + # ---- recording ----------------------------------------------------------- + pathDefaults: + record: yes + recordPath: /recordings/%path/%Y-%m-%d_%H-%M-%S-%f + recordFormat: fmp4 + recordPartDuration: 1s + recordSegmentDuration: 1h + recordDeleteAfter: 168h + + paths: + # Streams live at live/; publish requires the stream secret, + # which podsteadr checks in the auth webhook. + "~^live/[A-Za-z0-9]+$": {} + + health_check: + # Stock mediamtx image has no shell, so an in-container HTTP probe of the + # API isn't meaningfully cheaper than TCP; RTMP liveness is enough (same + # polling-not-hooks rationale as podsteadr's own status poller, which + # exists precisely because runOn*-style shell hooks aren't available on + # this image). + type: tcp + endpoint: localhost:1935 + interval: 30s + timeout: 5s + retries: 3 diff --git a/apps/podsteadr/Dockerfile b/apps/podsteadr/Dockerfile new file mode 100644 index 00000000..68e8ecf3 --- /dev/null +++ b/apps/podsteadr/Dockerfile @@ -0,0 +1,44 @@ +# Vendored copy of the podsteadr repo's own Dockerfile (source lives outside +# this tree — http://146.59.87.168:3000/ssmithx/podsteadr). Re-sync by hand if +# the upstream Dockerfile changes; build with build-from-prototype.sh, which +# passes the podsteadr repo root as build context (this Dockerfile expects +# frontend/ and server/ subdirectories at the context root, not this apps/ +# directory). +# +# ---- frontend ---- +FROM node:22-bookworm-slim AS frontend-build +WORKDIR /build/frontend +COPY frontend/package*.json ./ +RUN npm ci +COPY frontend/ ./ +RUN npm run build + +# ---- server ---- +FROM node:22-bookworm-slim AS server-build +WORKDIR /build/server +COPY server/package*.json ./ +RUN npm ci +COPY server/ ./ +RUN npm run build && npm prune --omit=dev + +# ---- runtime ---- +FROM node:22-bookworm-slim +RUN apt-get update \ + && apt-get install -y --no-install-recommends ffmpeg curl \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY --from=server-build /build/server/node_modules ./node_modules +COPY --from=server-build /build/server/package.json ./package.json +COPY --from=server-build /build/server/dist ./dist +COPY --from=frontend-build /build/frontend/dist ./public +# Named volumes inherit ownership from the image path: keep /data writable by node +RUN mkdir -p /data && chown node:node /data +USER node +ENV NODE_ENV=production \ + PORT=8095 \ + DATA_DIR=/data \ + STATIC_DIR=/app/public +EXPOSE 8095 +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD curl -fsS http://localhost:8095/api/health || exit 1 +CMD ["node", "dist/index.js"] diff --git a/apps/podsteadr/README.md b/apps/podsteadr/README.md new file mode 100644 index 00000000..68be1111 --- /dev/null +++ b/apps/podsteadr/README.md @@ -0,0 +1,85 @@ +# podsteadr — Nostr-native Podcasting & Livestreaming + +Self-hosted, nostr-native podcast publishing and livestreaming. Log in with a +NIP-07 nostr identity (no passwords, no email), upload an mp4 to publish an +RSS 2.0 feed with Podcasting 2.0 lightning payment info, or go live via OBS +(RTMP) or the browser (WebRTC/WHIP) — the stream is announced on nostr as a +NIP-53 live event and viewers watch over HLS. + +This is a three-container stack: + +| App | Manifest | Role | +|---|---|---| +| `podsteadr` | `apps/podsteadr/manifest.yml` | Fastify API + built Vue UI + RSS feeds | +| `podsteadr-mediamtx` | `apps/podsteadr-mediamtx/manifest.yml` | RTMP/WHIP ingest, HLS output, recording | +| `podsteadr-blossom` | `apps/podsteadr-blossom/manifest.yml` | BUD-02 sha256-addressed media blobs | + +All three join a dedicated `podsteadr-net` bridge network and resolve each +other by short DNS aliases (`podsteadr-app`, `mediamtx`, `blossom`). + +## Building the Image + +The app image is built from the **podsteadr** repo, source of truth at +`http://146.59.87.168:3000/ssmithx/podsteadr`. + +### Option 1: Use the build script + +```bash +# From archy repo root +./apps/podsteadr/build-from-prototype.sh +``` + +### Option 2: Build from source directory + +```bash +cd ~/podsteadr +podman build -t localhost/podsteadr:1.0.0 -f ~/archy/apps/podsteadr/Dockerfile . +``` + +### Publishing to the shared registry + +```bash +./apps/podsteadr/push-to-registry.sh 1.0.0 +``` + +Then update `apps/podsteadr/manifest.yml`'s `container.image` to the pushed +tag so other nodes pull instead of building locally. + +## Ports + +See `apps/PORTS.md`. Summary: 8095 (web UI/API/RSS), 1935 (RTMP), 8889 +(WebRTC/WHIP), 8189/udp (WebRTC ICE), 8890 (HLS), 8098 (Blossom). + +All of podsteadr's ports are `auth: none` — this is a public podcast/livestream +server, not a private personal app; RSS feeds, HLS playback, and blob reads +must stay reachable by third-party clients with no Archipelago session, and +the app enforces its own NIP-98 signed-request auth for sensitive routes and +per-stream secret keys for RTMP/WHIP publish. See the `auth_rationale` on each +port mapping. + +## Nostr Identity + +podsteadr's frontend vendors a copy of Archipelago's `nostr-provider.js` shim +and references it directly from `index.html` (its Fastify server isn't the +nginx-served SPA shape the platform auto-patches — see "Nostr Signer Bridge" +in `docs/app-developer-guide.md`). `apps/podsteadr/manifest.yml` declares a +`post_install` hook that re-copies the canonical +`/opt/archipelago/web-ui/nostr-provider.js` over the vendored copy on every +install/reinstall, so it doesn't go stale across OTA releases. + +## Data + +- `/var/lib/archipelago/podsteadr` — SQLite DB, server's own nostr key, + covers, and (read-only here) shared stream recordings. +- `/var/lib/archipelago/podsteadr/recordings` — stream recordings (writable + by `podsteadr-mediamtx`, read-only for `podsteadr`), 7-day retention. +- `/var/lib/archipelago/podsteadr-blossom/data` — media blobs. + +## Known gotchas + +See the podsteadr repo's `docs/STATUS.md` for the full list (blossom v4 +config `rules:` nesting, no HTTP range support in blossom 4.x, split-horizon +blossom URL, MediaMTX has no shell so status is polled not hooked, standard +vs. low-latency HLS). The blossom and mediamtx config files embedded in +`apps/podsteadr-blossom/manifest.yml` / `apps/podsteadr-mediamtx/manifest.yml` +already carry the load-bearing ones inline as comments. diff --git a/apps/podsteadr/build-from-prototype.sh b/apps/podsteadr/build-from-prototype.sh new file mode 100755 index 00000000..dfa90950 --- /dev/null +++ b/apps/podsteadr/build-from-prototype.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Build the podsteadr container image from the podsteadr repo. +# Usage: ./build-from-prototype.sh [path-to-podsteadr-repo] + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEFAULT_REPO="$HOME/podsteadr" +REPO_DIR="${1:-$DEFAULT_REPO}" +IMAGE_TAG="localhost/podsteadr:1.0.0" + +if [ ! -d "$REPO_DIR" ]; then + echo "podsteadr repo not found at: $REPO_DIR" + echo " Set path: $0 /path/to/podsteadr" + exit 1 +fi + +if [ ! -f "$REPO_DIR/server/package.json" ] || [ ! -f "$REPO_DIR/frontend/package.json" ]; then + echo "No server/package.json or frontend/package.json found in $REPO_DIR — is this the right directory?" + exit 1 +fi + +# Determine container runtime +RUNTIME="podman" +if ! command -v podman >/dev/null 2>&1; then + RUNTIME="docker" +fi + +echo "Building podsteadr from $REPO_DIR using $SCRIPT_DIR/Dockerfile" +$RUNTIME build -t "$IMAGE_TAG" -f "$SCRIPT_DIR/Dockerfile" "$REPO_DIR" + +echo "Built $IMAGE_TAG" +echo "" +echo "You can now install podsteadr from the App Store in Archipelago." +echo "Or run directly: $RUNTIME run -d --name podsteadr-app -p 8095:8095 $IMAGE_TAG" diff --git a/apps/podsteadr/manifest.yml b/apps/podsteadr/manifest.yml new file mode 100644 index 00000000..51848e3a --- /dev/null +++ b/apps/podsteadr/manifest.yml @@ -0,0 +1,143 @@ +app: + id: podsteadr + name: podsteadr + version: "1.0.0" + description: Self-hosted, nostr-native podcast publishing and livestreaming. Log in with Nostr, upload episodes or go live via OBS/WebRTC, publish to RSS with Podcasting 2.0 lightning payments. + category: media + + # Container/DNS-alias name deliberately NOT "podsteadr" — on a host whose own + # hostname happens to be "podsteadr", the host's own /etc/hosts self-hostname + # entry (127.0.1.1, e.g. from cloud-init) shadows the container network's DNS + # alias for other containers looking up "podsteadr", and mediamtx's auth-webhook + # callback resolves to the host's loopback instead of this container — every + # RTMP publish gets rejected with "connection refused" (observed on + # podsteadr.atobitcoin.io, 2026-07-30; see docker-compose.yml in the podsteadr + # repo for the original writeup). Carried forward unchanged into the manifest. + container_name: podsteadr-app + + container: + # Built locally from the podsteadr repo (source lives outside this tree — + # see apps/podsteadr/README.md + build-from-prototype.sh), same pattern as + # apps/indeedhub. Not yet pushed to the shared registry; push-to-registry.sh + # is there for when fleet-wide install is needed. + image: localhost/podsteadr:1.0.0 + pull_policy: if-not-present + network: podsteadr-net + network_aliases: [podsteadr-app] + derived_env: + - key: PUBLIC_URL + template: "http://{{HOST_MDNS}}:8095" + - key: MEDIAMTX_RTMP_PUBLIC + template: "rtmp://{{HOST_MDNS}}:1935" + - key: MEDIAMTX_WHIP_PUBLIC + template: "http://{{HOST_MDNS}}:8889" + - key: MEDIAMTX_HLS_PUBLIC + template: "http://{{HOST_MDNS}}:8890" + - key: BLOSSOM_URL_DEFAULT + template: "http://{{HOST_MDNS}}:8098" + # node:22-bookworm-slim's built-in `node` user is uid:gid 1000:1000. The + # image's own Dockerfile chowns /data to node:node, but that only affects + # the image layer — the actual runtime mount is the bind volume below, so + # the host directory needs the same ownership or the read-only-root, + # non-root `node` process can't open the SQLite DB (unverified against a + # real node install; flagging per this repo's convention of documenting + # bind-mount ownership assumptions, e.g. apps/botfights/manifest.yml). + data_uid: "1000:1000" + + dependencies: + - app_id: podsteadr-mediamtx + - app_id: podsteadr-blossom + - storage: 2Gi + + resources: + cpu_limit: 2 + memory_limit: 1Gi + disk_limit: 2Gi + + security: + capabilities: [] + readonly_root: true + no_new_privileges: true + network_policy: isolated + + ports: + - host: 8095 + container: 8095 + protocol: tcp + auth: none + auth_rationale: >- + podsteadr is a public podcast/livestream server: RSS feeds and the + marketplace/catalog API must stay fetchable by third-party podcast + clients, crawlers, and other podsteadr instances with no Archipelago + session, and the app already gates its own sensitive routes with + NIP-98 signed-request auth (see server/src/plugins/nostr-auth.ts in + the podsteadr repo). Putting the node's session gate in front would + block every external RSS/API consumer without adding real protection. + + volumes: + - type: bind + source: /var/lib/archipelago/podsteadr + target: /data + options: [rw] + # Shares podsteadr-mediamtx's recordings directory (rw there, ro here) so + # the app can list/remux finished recordings for one-click episode + # publishing without granting it write access to live segments. + - type: bind + source: /var/lib/archipelago/podsteadr/recordings + target: /recordings + options: [ro] + + environment: + - NODE_ENV=production + - PORT=8095 + - DATA_DIR=/data + - RECORDINGS_DIR=/recordings + - MEDIAMTX_API_URL=http://mediamtx:9997 + - BLOSSOM_URL_INTERNAL=http://blossom:3000 + - NOSTR_RELAYS=wss://relay.damus.io,wss://nos.lol,wss://relay.nostr.band + - CASHU_MINT_URL_DEFAULT=https://mint.minibits.cash/Bitcoin + + # podsteadr's Fastify server (fastify-static) isn't the nginx-served SPA + # shape the platform auto-patches for NIP-07 injection (see "Nostr Signer + # Bridge" in docs/app-developer-guide.md) — its frontend already + # self-references /nostr-provider.js from index.html and vendors a copy at + # build time (podsteadr commit 133558d). That vendored copy goes stale + # across archy OTA releases, so re-copy the canonical host script over it + # on every install/reinstall instead of trusting the baked-in one. + hooks: + post_install: + - copy_from_host: + src: "web-ui/nostr-provider.js" + dest: /app/public/nostr-provider.js + + health_check: + type: http + endpoint: http://localhost:8095 + path: /api/health + interval: 30s + timeout: 5s + retries: 3 + + interfaces: + main: + name: Web UI + description: Podcast dashboard, upload/live wizard, and stream management + type: ui + port: 8095 + protocol: http + path: / + + metadata: + author: podsteadr + icon: /assets/img/app-icons/podsteadr.png + repo: http://146.59.87.168:3000/ssmithx/podsteadr + license: MIT + tags: + - nostr + - podcast + - livestream + - media + - rss + - lightning + launch: + open_in_new_tab: false diff --git a/apps/podsteadr/push-to-registry.sh b/apps/podsteadr/push-to-registry.sh new file mode 100755 index 00000000..06af8dde --- /dev/null +++ b/apps/podsteadr/push-to-registry.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# Build and push the podsteadr container image to a registry. +# Usage: ./push-to-registry.sh [version] +# +# Environment variables: +# REGISTRY - Registry host (default: 146.59.87.168:3000, same as indeedhub/botfights) +# NAMESPACE - Registry namespace (default: lfg2025) +# RUNTIME - Container runtime (default: podman) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="${PODSTEADR_REPO:-$HOME/podsteadr}" +VERSION="${1:-1.0.0}" +REGISTRY="${REGISTRY:-146.59.87.168:3000}" +NAMESPACE="${NAMESPACE:-lfg2025}" +IMAGE_NAME="podsteadr" +RUNTIME="${RUNTIME:-podman}" + +FULL_TAG="${REGISTRY}/${NAMESPACE}/${IMAGE_NAME}:${VERSION}" + +if [ ! -d "$REPO_DIR" ]; then + echo "podsteadr repo not found at: $REPO_DIR" + echo "Set PODSTEADR_REPO=/path/to/podsteadr" + exit 1 +fi + +echo "=== podsteadr Container Registry Push ===" +echo "Source: $REPO_DIR" +echo "Image: $FULL_TAG" +echo "Runtime: $RUNTIME" +echo "" + +echo "[1/3] Building image..." +$RUNTIME build --platform linux/amd64 \ + -t "$FULL_TAG" \ + -t "localhost/${IMAGE_NAME}:${VERSION}" \ + -f "$SCRIPT_DIR/Dockerfile" \ + "$REPO_DIR" + +echo "[2/3] Pushing to registry..." +if ! $RUNTIME login --get-login "$REGISTRY" >/dev/null 2>&1; then + echo "" + echo "Not logged in to $REGISTRY." + echo "Run: $RUNTIME login $REGISTRY" + exit 1 +fi + +$RUNTIME push "$FULL_TAG" + +echo "" +echo "[3/3] Done!" +echo "" +echo "Image pushed: $FULL_TAG" +echo "" +echo "Update apps/podsteadr/manifest.yml's container.image to $FULL_TAG so" +echo "nodes pull it instead of building locally." diff --git a/core/archipelago/src/fips/app_ports.rs b/core/archipelago/src/fips/app_ports.rs index 6c483d74..e20c4197 100644 --- a/core/archipelago/src/fips/app_ports.rs +++ b/core/archipelago/src/fips/app_ports.rs @@ -6,7 +6,41 @@ //! no listener, so allowing them is inert. pub const APP_LAUNCH_PORTS: &[u16] = &[ - 2283, 2342, 3000, 3001, 3002, 4080, 5180, 7778, 8080, 8081, 8082, 8083, 8084, 8085, 8087, 8088, - 8089, 8090, 8096, 8123, 8175, 8176, 8240, 8334, 8336, 8888, 8999, 9000, 9100, 10380, 11434, - 18081, 18083, 23000, 32838, 50002, + 2283, + 2342, + 3000, + 3001, + 3002, + 4080, + 5180, + 7778, + 8080, + 8081, + 8082, + 8083, + 8084, + 8085, + 8087, + 8088, + 8089, + 8090, + 8095, + 8096, + 8123, + 8175, + 8176, + 8240, + 8334, + 8336, + 8888, + 8999, + 9000, + 9100, + 10380, + 11434, + 18081, + 18083, + 23000, + 32838, + 50002, ]; diff --git a/core/container/src/manifest.rs b/core/container/src/manifest.rs index 7b3fc1ac..11e81979 100644 --- a/core/container/src/manifest.rs +++ b/core/container/src/manifest.rs @@ -1717,17 +1717,25 @@ app: } } exempt.sort(); - // 25 as of the v1.7.123 port-policy round: bitcoin p2p (8333 ×2), - // core-lightning 9736/9835, electrumx 50001, fedimint 8173/8174, - // fedimint-gateway 8176/9737, gitea ssh 2222, lightning-stack - // 8091/9738/10010, lnd 9735/10009/18080, netbird 3478/8086/8087, - // pine TLS 10381 + the three voice ports (10200/10300/10400 — the - // disclosed known gap), router SSDP/mDNS 1900/5353. Every one is a - // deliberate, rationale-carrying exemption; the release-gate test - // stage timed out that cycle, so the count here lagged at 17. + // 31 as of the podsteadr app-package round: the prior 25 (bitcoin p2p + // (8333 ×2), core-lightning 9736/9835, electrumx 50001, fedimint + // 8173/8174, fedimint-gateway 8176/9737, gitea ssh 2222, + // lightning-stack 8091/9738/10010, lnd 9735/10009/18080, netbird + // 3478/8086/8087, pine TLS 10381 + the three voice ports + // (10200/10300/10400 — the disclosed known gap), router SSDP/mDNS + // 1900/5353) plus 6 new ones: podsteadr 8095 (web UI/API/RSS — + // third-party podcast clients and other podsteadr instances must + // fetch feeds/marketplace data with no node session; the app gates + // its own sensitive routes with NIP-98), podsteadr-blossom 8098 + // (public blob reads for RSS enclosures; uploads are BUD-02 + // signed-auth gated by blossom itself), podsteadr-mediamtx + // 1935/8189/8889/8890 (RTMP/ICE/WHIP ingest + HLS playback — none of + // these are HTTP-session-shaped, and publish is protocol-gated by a + // per-stream secret checked via podsteadr's own auth webhook). Every + // one is a deliberate, rationale-carrying exemption. assert_eq!( exempt.len(), - 25, + 31, "unauthenticated port set changed — review before updating this count: {exempt:?}" ); } diff --git a/neode-ui/src/views/appSession/generatedAppSessionConfig.ts b/neode-ui/src/views/appSession/generatedAppSessionConfig.ts index 38185346..e1da18fd 100644 --- a/neode-ui/src/views/appSession/generatedAppSessionConfig.ts +++ b/neode-ui/src/views/appSession/generatedAppSessionConfig.ts @@ -28,6 +28,7 @@ export const GENERATED_APP_PORTS: Record = { "nostr-rs-relay": 18081, "photoprism": 2342, "pine": 10380, + "podsteadr": 8095, "portainer": 9000, "router": 8084, "searxng": 8888, @@ -87,6 +88,9 @@ export const GENERATED_APP_TITLES: Record = { "pine-openwakeword": "Pine Wake Word (openWakeWord)", "pine-piper": "Pine Piper (TTS)", "pine-whisper": "Pine Whisper (STT)", + "podsteadr": "podsteadr", + "podsteadr-blossom": "podsteadr Blossom", + "podsteadr-mediamtx": "podsteadr MediaMTX", "portainer": "Portainer", "router": "Mesh Router", "searxng": "SearXNG",